feat: filter public media by role
This commit is contained in:
@@ -23,7 +23,7 @@ def _public_role_allowed(entity_type: str | None, role: object) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def published_assets(entity_type: str | None = None) -> list[dict]:
|
||||
def published_assets(entity_type: str | None = None, media_role: str | None = None) -> list[dict]:
|
||||
manifest = json.loads((MEDIA_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||
result = []
|
||||
for item in manifest.get("assets", []):
|
||||
@@ -31,6 +31,8 @@ def published_assets(entity_type: str | None = None) -> list[dict]:
|
||||
continue
|
||||
if entity_type and item.get("entity_type") != entity_type:
|
||||
continue
|
||||
if media_role and item.get("media_role") != media_role:
|
||||
continue
|
||||
if not _public_role_allowed(item.get("entity_type"), item.get("media_role")):
|
||||
continue
|
||||
source_page = str(item.get("source_page") or "")
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from ..media_catalog import published_assets, published_file
|
||||
from ..media_catalog import KNOWN_MEDIA_ROLES, published_assets, published_file
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/api/v1/media/catalog")
|
||||
def media_catalog(entity_type: str | None = Query(None, pattern="^(fish|waterbody|tackle|reference)$")) -> list[dict]:
|
||||
return published_assets(entity_type)
|
||||
def media_catalog(
|
||||
entity_type: str | None = Query(None, pattern="^(fish|waterbody|tackle|reference)$"),
|
||||
media_role: str | None = Query(None, pattern="^(waterbody_cover|waterbody_map|waterbody_depth_map|waterbody_screenshot|tackle_card|tackle_detail|rig_diagram|tackle_screenshot)$"),
|
||||
) -> list[dict]:
|
||||
if media_role and entity_type not in {"waterbody", "tackle"}:
|
||||
raise HTTPException(status_code=422, detail="media_role requires waterbody or tackle entity_type")
|
||||
if media_role and media_role not in KNOWN_MEDIA_ROLES:
|
||||
raise HTTPException(status_code=422, detail="unknown media role")
|
||||
return published_assets(entity_type, media_role)
|
||||
|
||||
|
||||
@router.get("/api/v1/media/assets/{digest}", response_class=FileResponse)
|
||||
|
||||
@@ -7,8 +7,11 @@ import { api, type MediaAsset } from "../lib/api";
|
||||
const allowed = new Set(["fish", "waterbody", "tackle", "reference"]);
|
||||
const requested = Astro.url.searchParams.get("type") ?? "fish";
|
||||
const type = allowed.has(requested) ? requested : "fish";
|
||||
const requestedRole = Astro.url.searchParams.get("role") ?? "";
|
||||
const roleOptions: Record<string, Array<[string, string]>> = { waterbody: [["waterbody_cover", "Заставки"], ["waterbody_map", "Карты"], ["waterbody_depth_map", "Карты глубин"], ["waterbody_screenshot", "Скриншоты"]], tackle: [["tackle_card", "Карточки снастей"], ["tackle_detail", "Детали снастей"], ["rig_diagram", "Схемы монтажей"], ["tackle_screenshot", "Скриншоты снастей"]] };
|
||||
const selectedRole = (roleOptions[type] ?? []).some(([value]) => value === requestedRole) ? requestedRole : "";
|
||||
let assets: MediaAsset[] = [], unavailable = false;
|
||||
try { assets = await api<MediaAsset[]>(`/api/v1/media/catalog?entity_type=${type}`); } catch { unavailable = true; }
|
||||
try { const query = new URLSearchParams({ entity_type: type }); if (selectedRole) query.set("media_role", selectedRole); assets = await api<MediaAsset[]>(`/api/v1/media/catalog?${query}`); } catch { unavailable = true; }
|
||||
if (unavailable) {
|
||||
Astro.response.status = 503;
|
||||
Astro.response.headers.set("Retry-After", "60");
|
||||
@@ -20,6 +23,7 @@ const labels: Record<string,string> = {fish:"Рыбы",waterbody:"Водоёмы
|
||||
<PageHero eyebrow="Визуальный справочник" title="Медиатека" description="Собранные материалы RF4 с источником у каждого изображения. Каталог пополняется по мере импорта и проверки." variant="fish" count={assets.length} />
|
||||
<section class="media-library content-grid">
|
||||
<nav class="media-library__intro" aria-label="Разделы медиатеки"><strong>{labels[type]} · {assets.length}</strong><span><a href="/media?type=fish">Рыбы</a> · <a href="/media?type=waterbody">Водоёмы</a> · <a href="/media?type=tackle">Снасти</a> · <a href="/media?type=reference">Справка</a></span></nav>
|
||||
{roleOptions[type]?.length > 0 && <form class="media-role-filter" method="get"><input type="hidden" name="type" value={type} /><label>Тип материала<select name="role"><option value="">Все роли</option>{roleOptions[type].map(([value, label]) => <option value={value} selected={selectedRole === value}>{label}</option>)}</select></label><button data-action="secondary">Показать</button>{selectedRole && <a data-action="quiet" href={`/media?type=${type}`}>Сбросить</a>}</form>}
|
||||
{unavailable ? <StatePanel tone="unavailable" title="Медиатека временно недоступна" /> : assets.length ? <div class="media-library__grid">{assets.map(asset => <article class="media-library__card"><EntityMedia asset={asset} sourceLink /><h2>{asset.label ?? "Без подписи"}</h2><span>{labels[asset.entity_type]}</span></article>)}</div> : <StatePanel title="В этом разделе пока нет изображений" description="Материалы появятся после следующего импорта." />}
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
.entity-media{margin:0;min-width:0}.entity-media__frame{position:relative;display:grid;place-items:center;overflow:hidden;min-height:190px;padding:18px;border:1px solid var(--border-soft);border-radius:16px;background:radial-gradient(circle at 50% 46%,color-mix(in srgb,var(--lime) 15%,var(--surface)) 0 18%,var(--surface-soft) 62%)}.entity-media__frame:after{content:"";position:absolute;inset:12px;border:1px solid color-mix(in srgb,var(--border) 55%,transparent);border-radius:11px;pointer-events:none}.entity-media img{position:relative;z-index:1;display:block;width:100%;height:180px;object-fit:contain;filter:drop-shadow(0 12px 18px #08222624);image-rendering:auto}.entity-media figcaption{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:9px;color:var(--text-muted);font-size:11px}.entity-media figcaption>span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.entity-media--compact{grid-column:2;grid-row:1/4;width:118px}.entity-media--compact .entity-media__frame{min-height:88px;height:88px;padding:10px;border:0;background:color-mix(in srgb,var(--lime) 8%,var(--surface-soft))}.entity-media--compact img{height:72px}.entity-media--compact figcaption{justify-content:flex-end}.entity-media--compact figcaption>span:last-child{display:none}.entity-media--compact .source-chip{transform:scale(.9);transform-origin:right center}.media-library{padding:34px 0 100px}.media-library__intro{display:flex;justify-content:space-between;align-items:center;gap:20px;margin-bottom:24px;color:var(--text-muted)}.media-library__grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px}.media-library__card{padding:12px;border:1px solid var(--border-soft);border-radius:18px;background:var(--surface)}.media-library__card .entity-media__frame{min-height:170px}.media-library__card h2{margin:12px 3px 3px;font:400 19px Georgia,serif}.media-library__card>span{margin-left:3px;color:var(--text-subtle);font-size:10px;text-transform:uppercase;letter-spacing:.09em}@media(max-width:1050px){.media-library__grid{grid-template-columns:repeat(3,1fr)}}@media(max-width:720px){.media-library__grid{grid-template-columns:repeat(2,1fr)}.entity-media--compact{width:90px}.entity-media--compact .entity-media__frame{height:76px}.media-library__intro{display:block}}@media(max-width:440px){.media-library__grid{grid-template-columns:1fr}}
|
||||
.entity-feature{display:grid;grid-template-columns:minmax(280px,420px) 1fr;align-items:center;gap:48px;padding-block:36px;border-bottom:1px solid var(--border)}.entity-feature .entity-media__frame{min-height:260px}.entity-feature .entity-media img{height:230px}.entity-feature h2{margin:10px 0 12px;font:400 clamp(34px,4vw,58px)/.95 Georgia,serif}.entity-feature p{max-width:620px;color:var(--text-muted);line-height:1.6}@media(max-width:720px){.entity-feature{grid-template-columns:1fr;gap:24px;padding-block:26px}}
|
||||
|
||||
.media-role-filter{display:flex;align-items:end;gap:10px;margin:0 0 22px;padding:14px 16px;border:1px solid var(--border-soft);border-radius:12px;background:var(--surface-soft)}.media-role-filter label{display:grid;gap:6px;color:var(--text-muted);font-size:11px;font-weight:750;text-transform:uppercase;letter-spacing:.08em}.media-role-filter select{min-width:220px;height:40px;padding:0 10px;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:var(--text)}.media-role-filter a{padding:11px 4px;color:var(--text-muted);font-size:12px}@media(max-width:720px){.media-role-filter{align-items:stretch;flex-wrap:wrap}.media-role-filter label{flex:1 1 100%}.media-role-filter select{width:100%}}
|
||||
|
||||
@media (min-width:721px) and (max-width:1050px){.media-library__grid{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
||||
|
||||
Reference in New Issue
Block a user