@hadoku/watchparty-ui 0.45.1 → 0.45.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{AdminPanel-BLS2jVbU.js → AdminPanel-BQAtJ9Th.js} +4 -4
- package/dist/chunks/{AdminPanel-BLS2jVbU.js.map → AdminPanel-BQAtJ9Th.js.map} +1 -1
- package/dist/chunks/{GameModule-Cc9rFmYY.js → GameModule-CIJ-c1IW.js} +4 -4
- package/dist/chunks/{GameModule-Cc9rFmYY.js.map → GameModule-CIJ-c1IW.js.map} +1 -1
- package/dist/chunks/{MapEditor-BYytZ19I.js → MapEditor-BI2ZVk5C.js} +2 -2
- package/dist/chunks/{MapEditor-BYytZ19I.js.map → MapEditor-BI2ZVk5C.js.map} +1 -1
- package/dist/chunks/{MediaDrawer-CHn7DAlP.js → MediaDrawer-BgILxxh4.js} +4 -4
- package/dist/chunks/{MediaDrawer-CHn7DAlP.js.map → MediaDrawer-BgILxxh4.js.map} +1 -1
- package/dist/chunks/{PlaylistPanel-CuWYKMjh.js → PlaylistPanel-2V6H_3Gw.js} +3 -3
- package/dist/chunks/{PlaylistPanel-CuWYKMjh.js.map → PlaylistPanel-2V6H_3Gw.js.map} +1 -1
- package/dist/chunks/{Room-DWs1NSwe.js → Room-DleCsQL0.js} +1448 -1429
- package/dist/chunks/Room-DleCsQL0.js.map +1 -0
- package/dist/chunks/{index-B9K6iuIS.js → index-HIbKUgoo.js} +3 -3
- package/dist/chunks/{index-B9K6iuIS.js.map → index-HIbKUgoo.js.map} +1 -1
- package/dist/chunks/{submissionsApi-95aogA8-.js → submissionsApi-mPYMf-jY.js} +2 -2
- package/dist/chunks/{submissionsApi-95aogA8-.js.map → submissionsApi-mPYMf-jY.js.map} +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js +1 -1
- package/dist/watchparty-ui.css +1 -1
- package/package.json +2 -2
- package/dist/chunks/Room-DWs1NSwe.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MediaDrawer-CHn7DAlP.js","sources":["../../src/lib/catalog.ts","../../src/lib/catalogProbe.ts","../../src/lib/ecchiShows.ts","../../src/lib/mediaTreeExpansion.ts","../../src/lib/catalogTree.ts","../../src/lib/mediaSearchApi.ts","../../src/components/MediaSearchResults.tsx","../../src/components/AddMediaDrawer.tsx","../../src/components/GamePicker.tsx","../../src/components/MediaDrawer.tsx"],"sourcesContent":["import { getRuntimeServerOrigin } from './config';\nimport { isDebugEnabled, debug } from './debug';\n\nexport interface CatalogDir { name: string; path: string; }\nexport interface CatalogFile { id: string; title: string; masterUrl: string; poster: string | null; durationMs: number | null; }\nexport interface CatalogResponse { dirs: CatalogDir[]; files: CatalogFile[]; }\n\n// One entry PER PREFIX. This used to be a single `{prefix, data}` slot, and the\n// media drawer asks for ~18 prefixes per build, so every call evicted the one\n// before it: after a build the slot held only the last prefix, and the next\n// open missed all the others even inside CACHE_MS. A cache that never hits.\nconst cache = new Map<string, { ts: number; data: CatalogResponse }>();\n// Concurrent callers of the same prefix share one request — the drawer's\n// mount prefetch and an early open can overlap.\nconst inflight = new Map<string, Promise<CatalogResponse>>();\nconst CACHE_MS = 60000;\n\nexport async function fetchCatalog(prefix: string = ''): Promise<CatalogResponse> {\n const now = Date.now();\n const hit = cache.get(prefix);\n if (hit && (now - hit.ts) < CACHE_MS) return hit.data;\n const pending = inflight.get(prefix);\n if (pending) return pending;\n\n const request = (async () => {\n const origin = getRuntimeServerOrigin();\n const url = `${origin}/api/catalog${prefix ? `?prefix=${encodeURIComponent(prefix)}` : ''}`;\n const res = await fetch(url, { cache: 'no-store' });\n if (!res.ok) throw new Error(`catalog_http_${res.status}`);\n // The response is not schema-validated (there is no Zod schema for the\n // catalog on either end); this asserts the shape the server documents so the\n // `any` from res.json() does not leak into the cache and the return value.\n const data = (await res.json()) as CatalogResponse;\n if (isDebugEnabled()) debug('catalog', 'Catalog fetched', { prefix, dirs: data.dirs?.length, files: data.files?.length });\n cache.set(prefix, { ts: now, data });\n return data;\n })();\n inflight.set(prefix, request);\n try {\n return await request;\n } finally {\n inflight.delete(prefix);\n }\n}\n\n/** Drop one prefix, or everything when called with no prefix / ''. */\nexport function invalidateCatalog(prefix?: string) {\n if (!prefix) cache.clear();\n else cache.delete(prefix);\n}\n","import { FRAGMENT_MP4_FILENAME } from '@watchparty/shared';\nimport { fetchCatalog } from './catalog';\nimport { isDebugEnabled, debug, warn } from './debug';\n\n// Developer utility (verbose mode) to sanity check media catalog + sample media accessibility.\n// Invoked when MediaDrawer opens in verbose mode.\nexport async function probeMediaEndpoints() {\n if (!isDebugEnabled()) return;\n try {\n const categories = ['output/fmp4/anime','output/fmp4/movie','output/fmp4/tv'];\n for (const cat of categories) {\n const catData = await fetchCatalog(cat);\n debug('catalog', '[probe] catalog ' + cat, { dirs: catData.dirs.length, files: catData.files.length });\n // Try first playable leaf if present\n const candidate = catData.files[0];\n if (candidate) {\n const base = mediaBase();\n const indexUrl = `${base}/${candidate.id}/index.json`.replace(/\\\\/g,'/');\n const mediaUrl = `${base}/${candidate.id}/${FRAGMENT_MP4_FILENAME}`.replace(/\\\\/g,'/');\n await fetchAndLog(indexUrl, 'index');\n // Range fetch first ~1KB\n await fetchAndLog(mediaUrl, 'fragment', { headers: { Range: 'bytes=0-1023' } });\n }\n }\n } catch (err: unknown) {\n warn('catalog', '[probe] error', { error: err instanceof Error ? err.message : String(err) });\n }\n}\n\nfunction mediaBase() {\n return '/media';\n}\n\nasync function fetchAndLog(url: string, label: string, init?: RequestInit) {\n const t0 = performance.now();\n const res = await fetch(url, init);\n const ms = Math.round(performance.now() - t0);\n debug('catalog', '[probe] ' + label, { url, status: res.status, ms, bytes: res.headers.get('content-length') });\n return res;\n}\n","/**\n * Anime series (by output folder name) that are shown in a separate,\n * collapsed-by-default \"Ecchi\" section at the BOTTOM of the media drawer,\n * instead of mixed into the main Anime list.\n *\n * Edit this set to recategorize — matching is case-insensitive and ignores\n * non-alphanumerics, so it tolerates folder-name punctuation/spacing.\n * (WarriorPrincess is intentionally left in the main list — move it here if you\n * consider it ecchi.)\n */\n\nconst ECCHI_SERIES = [\n 'yuuna',\n 'gushingovermagicalgirls',\n 'immoralguild',\n 'ingokudanchi',\n 'isekaionsenparadise',\n];\n\nconst norm = (name: string) => name.toLowerCase().replace(/[^a-z0-9]/g, '');\n\n/**\n * The matcher, over an explicit list. Split out from `isEcchiSeries` so tests\n * can exercise the normalisation rules — which are the part that actually\n * breaks — against a fixed list, independently of what this deployment happens\n * to categorise.\n */\nexport function matchesSeriesList(name: string, series: readonly string[]): boolean {\n return new Set(series.map(norm)).has(norm(name));\n}\n\nexport function isEcchiSeries(name: string): boolean {\n return matchesSeriesList(name, ECCHI_SERIES);\n}\n","/**\n * Carry the user's expand/collapse state across a MediaDrawer refetch.\n *\n * `MediaDrawer` is mounted for the lifetime of the room (`Room.tsx` renders it\n * unconditionally and gates on `open` INSIDE the component, after the hooks),\n * so its `tree` state survives closing the drawer. Every open then refires\n * `buildTree()`, which walks the catalog in ~18 SEQUENTIAL requests — 1.45s\n * measured from the server's own host, several seconds for a viewer over the\n * tunnel.\n *\n * For that whole window the drawer shows the PREVIOUS tree, fully interactive.\n * A user who opens the drawer and expands a series is working on a tree that is\n * about to be thrown away: when the refetch lands, `setTree(result)` replaces it\n * with freshly-built nodes whose `expanded` flags are back at their defaults,\n * and the selection silently collapses a few seconds after opening. It only\n * happens from the second open onward, because the first has no stale tree to\n * show — which is exactly why it reads as intermittent.\n *\n * `mergeExpansion` re-applies the old flags onto the new tree by NAME, so a\n * background refresh can never undo a deliberate interaction. Names are the\n * right key here: they are what the user actually clicked on, they are stable\n * across refetches, and they are unique within their level (a category label, a\n * series directory name). Nodes that are genuinely new keep the defaults\n * `buildTree` gave them — a series added by an ingest since the last open\n * arrives collapsed, and the Ecchi category still starts collapsed the first\n * time it appears.\n */\n\nexport interface ExpandableNode {\n name: string;\n expanded?: boolean;\n}\n\nexport interface ExpandableParent<C extends ExpandableNode> extends ExpandableNode {\n children: C[];\n}\n\n/** Snapshot of which nodes are open, keyed by name at each level. */\nexport type ExpansionState = Map<string, { expanded: boolean; children: Map<string, boolean> }>;\n\n/**\n * Read the current expand/collapse state out of a tree.\n *\n * `childrenOf` returns the nested expandable nodes for a top-level node\n * (series inside a category); levels with no expandable children yield an\n * empty map, which merges as a no-op.\n */\nexport function captureExpansion<T extends ExpandableNode>(\n tree: readonly T[],\n childrenOf: (node: T) => readonly ExpandableNode[]\n): ExpansionState {\n const state: ExpansionState = new Map();\n for (const node of tree) {\n const children = new Map<string, boolean>();\n for (const child of childrenOf(node)) {\n children.set(child.name, child.expanded === true);\n }\n state.set(node.name, { expanded: node.expanded === true, children });\n }\n return state;\n}\n\n/**\n * Re-apply a captured expansion state onto a freshly-built tree.\n *\n * Returns NEW node objects rather than mutating: `tree` is React state, and the\n * caller renders straight off it. A node absent from `state` is untouched, so\n * newly-ingested media keeps whatever default the builder chose.\n */\nexport function mergeExpansion<T extends ExpandableNode>(\n tree: readonly T[],\n state: ExpansionState,\n childrenOf: (node: T) => readonly ExpandableNode[],\n withChildren: (node: T, children: ExpandableNode[]) => T\n): T[] {\n return tree.map((node) => {\n const prev = state.get(node.name);\n if (!prev) return node;\n\n const children = childrenOf(node);\n const mergedChildren = children.map((child) => {\n const wasExpanded = prev.children.get(child.name);\n if (wasExpanded === undefined) return child;\n return { ...child, expanded: wasExpanded };\n });\n\n const merged = withChildren({ ...node, expanded: prev.expanded }, mergedChildren);\n return merged;\n });\n}\n\n/**\n * The one category that starts OPEN when the drawer opens on Watch. Every other\n * top-level node — Playlists, Ecchi, and Movie/TV should they ever hold\n * anything — starts closed, and every series inside starts closed.\n *\n * A name, not a flag on the node: the builder and the reset below must agree\n * on it, and a rule written in two places is how they stop agreeing.\n */\nexport const DEFAULT_OPEN_CATEGORY = 'Anime';\n\nexport function isOpenByDefault(categoryName: string): boolean {\n return categoryName === DEFAULT_OPEN_CATEGORY;\n}\n\n/**\n * Put a tree back to its defaults: {@link DEFAULT_OPEN_CATEGORY} open,\n * everything else closed, every series collapsed.\n *\n * Called when the drawer OPENS, and only then. This is not a change to\n * {@link mergeExpansion}, and the two are not in tension: they answer different\n * moments. Opening the drawer is a fresh start, so it gets the defaults every\n * time rather than whatever state the last visit left behind. A refetch that\n * lands WHILE the drawer is open is not a fresh start, and must still carry the\n * user's clicks over — that is the bug `mergeExpansion` exists for, and it only\n * stays fixed because the reset runs BEFORE the refetch rather than after it:\n * the refetch then merges onto the defaulted tree plus anything clicked since.\n *\n * Returns new objects; `tree` is React state.\n */\nexport function resetToDefaultExpansion<T extends ExpandableNode>(\n tree: readonly T[],\n childrenOf: (node: T) => readonly ExpandableNode[],\n withChildren: (node: T, children: ExpandableNode[]) => T\n): T[] {\n return tree.map((node) =>\n withChildren(\n { ...node, expanded: isOpenByDefault(node.name) },\n childrenOf(node).map((child) => ({ ...child, expanded: false }))\n )\n );\n}\n","/**\n * Builds the media drawer's Watch tree from the catalog API.\n *\n * Every fetch that does not depend on another runs CONCURRENTLY: the category\n * listings at once, then each category's series listings at once — two round\n * trips deep instead of ~18. It used to be a serial `for … await` waterfall,\n * and the server answers each listing in 1-5ms, so the waterfall WAS the load\n * time: ~1.3s measured from the origin box through the edge (~70ms a hop), and\n * a remote viewer pays their own RTT on top of every one of those hops.\n *\n * Order is preserved (Promise.all keeps input order), so the tree reads the\n * same as the serial version. A failed listing drops only that branch.\n */\nimport type { CatalogResponse } from './catalog';\nimport { warn, errorFields } from './debug';\nimport { isEcchiSeries } from './ecchiShows';\nimport { isOpenByDefault } from './mediaTreeExpansion';\n\nexport interface EpisodeNode { type: 'episode'; name: string; mediaId: string; }\nexport interface SeriesNode { type: 'series'; name: string; episodes: EpisodeNode[]; expanded?: boolean; }\nexport interface CategoryNode { type: 'category'; name: string; series: SeriesNode[]; expanded?: boolean; }\nexport interface PlaylistNode { type: 'playlist'; name: string; playlistId: string; }\nexport interface PlaylistCategoryNode { type: 'playlist_category'; name: string; playlists: PlaylistNode[]; expanded?: boolean; }\nexport type MediaTree = (CategoryNode | PlaylistCategoryNode)[];\n\nexport type CatalogFetcher = (prefix: string) => Promise<CatalogResponse>;\n\nconst ROOT = 'output/fmp4';\nconst CATEGORIES: { key: string; label: string }[] = [\n { key: 'anime', label: 'Anime' },\n { key: 'movie', label: 'Movie' },\n { key: 'tv', label: 'TV' },\n];\n\nasync function tryFetch(fetcher: CatalogFetcher, prefix: string): Promise<CatalogResponse | null> {\n try {\n return await fetcher(prefix);\n } catch (err) {\n warn('catalog', 'Fetch failed', { prefix, ...errorFields(err) });\n return null;\n }\n}\n\nexport async function loadCatalogTree(fetcher: CatalogFetcher): Promise<MediaTree> {\n const [categoryListings, playlistListing] = await Promise.all([\n Promise.all(CATEGORIES.map(cat => tryFetch(fetcher, `${ROOT}/${cat.key}`))),\n tryFetch(fetcher, `${ROOT}/playlists`),\n ]);\n\n const seriesPerCategory = await Promise.all(categoryListings.map(listing =>\n Promise.all((listing?.dirs ?? []).map(async (d): Promise<SeriesNode | null> => {\n const series = await tryFetch(fetcher, d.path);\n const episodes: EpisodeNode[] = (series?.files ?? []).map(f => ({ type: 'episode', name: f.title, mediaId: f.id }));\n return episodes.length ? { type: 'series', name: d.name, episodes, expanded: false } : null;\n }))));\n\n const result: MediaTree = [];\n // Ecchi anime is pulled out of the main Anime list and shown in its own\n // collapsed category at the very bottom of the drawer.\n const ecchiSeries: SeriesNode[] = [];\n CATEGORIES.forEach((cat, i) => {\n const seriesNodes: SeriesNode[] = [];\n for (const node of seriesPerCategory[i]) {\n if (!node) continue;\n if (cat.key === 'anime' && isEcchiSeries(node.name)) ecchiSeries.push(node);\n else seriesNodes.push(node);\n }\n if (seriesNodes.length) result.push({ type: 'category', name: cat.label, series: seriesNodes, expanded: isOpenByDefault(cat.label) });\n });\n const playlists: PlaylistNode[] = (playlistListing?.dirs ?? []).map(d => ({ type: 'playlist', name: d.name, playlistId: d.name }));\n if (playlists.length) result.push({ type: 'playlist_category', name: 'Playlists', playlists, expanded: isOpenByDefault('Playlists') });\n // Ecchi category goes LAST. Every default above reads the same rule\n // (`isOpenByDefault`) so the builder and the on-open reset cannot drift.\n if (ecchiSeries.length) result.push({ type: 'category', name: 'Ecchi', series: ecchiSeries, expanded: isOpenByDefault('Ecchi') });\n return result;\n}\n","/**\n * Search in the Add media drawer: shows (to add to the scraper's watch list)\n * and YouTube videos (to submit as a link).\n *\n * The drawer's one field takes a link, a magnet, or words. Words search;\n * anything with a scheme keeps the existing submit path — `isSearchText` is\n * that split, and it is pure so it is tested rather than living in the\n * component.\n *\n * The add sends `{source, id}` and nothing else: the server re-reads the show\n * and derives every name it forwards (see `apps/server/src/http/routes/search.ts`).\n */\nimport {\n SEARCH_MAX_QUERY,\n SEARCH_MIN_QUERY,\n type TShowResult,\n type TShowSearchOut,\n type TShowSource,\n type TVideoResult,\n type TVideoSearchOut,\n type TWatchlistAddOut,\n} from '@watchparty/shared';\nimport { getRuntimeServerOrigin } from './config';\nimport { debug, warn, errorFields } from './debug';\n\n/** The two schemes the submit path accepts mean a link, never a search. Not\n * any scheme: `Re:Zero` is a title. */\nconst HAS_SCHEME = /^(?:https?|magnet):/i;\n\nexport function isSearchText(raw: string): boolean {\n const text = raw.trim();\n return text.length >= SEARCH_MIN_QUERY && text.length <= SEARCH_MAX_QUERY && !HAS_SCHEME.test(text);\n}\n\n/** Server-relative proxy path → absolute URL on this app's server. */\nexport function resolveSearchImage(pathOrNull: string | null): string | null {\n return pathOrNull ? `${getRuntimeServerOrigin()}${pathOrNull}` : null;\n}\n\n/** `204` → `3:24`, `3725` → `1:02:05`. */\nexport function formatDuration(totalS: number | null): string {\n if (totalS === null || !Number.isFinite(totalS) || totalS < 0) return '';\n const s = Math.floor(totalS % 60);\n const m = Math.floor(totalS / 60) % 60;\n const h = Math.floor(totalS / 3600);\n const ss = String(s).padStart(2, '0');\n return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${ss}` : `${m}:${ss}`;\n}\n\nexport type SearchOutcome<T> = { ok: true; value: T } | { ok: false; status: number | 'network' | 'aborted' };\n\nasync function getJson<T>(path: string, signal: AbortSignal): Promise<SearchOutcome<T>> {\n try {\n const res = await fetch(`${getRuntimeServerOrigin()}${path}`, { signal });\n if (!res.ok) return { ok: false, status: res.status };\n return { ok: true, value: await res.json() as T };\n } catch (err: unknown) {\n if (signal.aborted) return { ok: false, status: 'aborted' };\n warn('search', 'Search request threw', errorFields(err));\n return { ok: false, status: 'network' };\n }\n}\n\nexport function searchShows(q: string, signal: AbortSignal): Promise<SearchOutcome<TShowSearchOut>> {\n return getJson(`/api/search/shows?q=${encodeURIComponent(q.trim())}`, signal);\n}\n\nexport function searchVideos(q: string, signal: AbortSignal): Promise<SearchOutcome<TVideoSearchOut>> {\n return getJson(`/api/search/youtube?q=${encodeURIComponent(q.trim())}`, signal);\n}\n\nexport type WatchlistAddOutcome =\n | { ok: true; value: TWatchlistAddOut }\n /** Already on the list — as good as added, from the user's side. */\n | { ok: false; code: 'exists' | 'failed'; status: number | 'network' };\n\nexport async function addShowToWatchlist(source: TShowSource, id: string): Promise<WatchlistAddOutcome> {\n try {\n const res = await fetch(`${getRuntimeServerOrigin()}/api/watchlist`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ source, id }),\n });\n if (res.status === 201) {\n const value = await res.json() as TWatchlistAddOut;\n debug('search', 'Added to watch list', { source, id, series: value.series });\n return { ok: true, value };\n }\n return { ok: false, code: res.status === 409 ? 'exists' : 'failed', status: res.status };\n } catch (err: unknown) {\n warn('search', 'Watch-list add threw', errorFields(err));\n return { ok: false, code: 'failed', status: 'network' };\n }\n}\n\nexport type { TShowResult, TVideoResult };\n","/**\n * Search results under the Add media field: shows and YouTube videos.\n *\n * A show's `+` adds it to the scraper's watch list, so its episodes arrive on\n * their own; a video's `+` submits its link exactly as pasting it would (a\n * `pending` request). Both searches run in parallel, each landing when it\n * lands — the show lookup answers in a few hundred ms, yt-dlp in a second or\n * so, and neither waits for the other.\n *\n * Results are stored WITH the query they answer and shown only while that is\n * still the query, so typing on never flashes the previous word's results\n * and no effect has to clear state synchronously.\n */\nimport { useEffect, useState } from 'react';\nimport { Icon } from '@wolffm/themes';\nimport type { TSubmission } from '@watchparty/shared';\nimport {\n addShowToWatchlist,\n formatDuration,\n isSearchText,\n resolveSearchImage,\n searchShows,\n searchVideos,\n type SearchOutcome,\n type TShowResult,\n type TVideoResult,\n} from '../lib/mediaSearchApi';\nimport { submissionFailureLabel, submitSubmission } from '../lib/submissionsApi';\n\n/** Pause after the last keystroke before searching. */\nconst DEBOUNCE_MS = 350;\n\ntype Answer<T> = { q: string; outcome: SearchOutcome<T[]> };\ntype RowState = 'adding' | 'added' | string;\n\ninterface Props {\n query: string;\n onSubmitted: (row: TSubmission) => void;\n}\n\nexport default function MediaSearchResults({ query, onSubmitted }: Props) {\n const q = query.trim();\n const active = isSearchText(q);\n const [shows, setShows] = useState<Answer<TShowResult> | null>(null);\n const [videos, setVideos] = useState<Answer<TVideoResult> | null>(null);\n const [rowState, setRowState] = useState<Record<string, RowState>>({});\n\n useEffect(() => {\n if (!active) return;\n const ctrl = new AbortController();\n const timer = setTimeout(() => {\n void searchShows(q, ctrl.signal).then(r => {\n if (ctrl.signal.aborted) return;\n setShows({ q, outcome: r.ok ? { ok: true, value: r.value.results } : r });\n });\n void searchVideos(q, ctrl.signal).then(r => {\n if (ctrl.signal.aborted) return;\n setVideos({ q, outcome: r.ok ? { ok: true, value: r.value.results } : r });\n });\n }, DEBOUNCE_MS);\n return () => { clearTimeout(timer); ctrl.abort(); };\n }, [q, active]);\n\n if (!active) return null;\n\n const showAnswer = shows?.q === q ? shows.outcome : null;\n const videoAnswer = videos?.q === q ? videos.outcome : null;\n\n async function addShow(show: TShowResult) {\n const key = `${show.source}:${show.id}`;\n setRowState(s => ({ ...s, [key]: 'adding' }));\n const r = await addShowToWatchlist(show.source, show.id);\n setRowState(s => ({ ...s, [key]: r.ok || r.code === 'exists' ? 'added' : `error ${r.status}` }));\n }\n\n async function addVideo(video: TVideoResult) {\n const key = `yt:${video.id}`;\n setRowState(s => ({ ...s, [key]: 'adding' }));\n const r = await submitSubmission(video.url, video.title);\n if (r.ok) {\n setRowState(s => ({ ...s, [key]: 'added' }));\n onSubmitted(r.value);\n } else {\n setRowState(s => ({ ...s, [key]: submissionFailureLabel(r.failure) }));\n }\n }\n\n const bothEmpty = showAnswer?.ok && videoAnswer?.ok\n && showAnswer.value.length === 0 && videoAnswer.value.length === 0;\n\n return (\n <div className=\"border-b border-border\" data-testid=\"media-search\">\n {bothEmpty && <div className=\"px-3 py-2 text-[11px] text-text-muted\">no results</div>}\n <Section label=\"Shows\" answer={showAnswer} testId=\"search-shows\">\n {showAnswer?.ok && showAnswer.value.map(show => {\n const key = `${show.source}:${show.id}`;\n const state = show.onWatchlist ? 'added' : rowState[key];\n return (\n <ResultRow\n key={key}\n testId={`show-${key}`}\n image={resolveSearchImage(show.poster)}\n imageClass=\"w-8 h-11\"\n title={show.title}\n meta={[show.year, show.format, show.episodes ? `${show.episodes} ep` : null]}\n state={state}\n canAdd={show.addable}\n addLabel={`Add ${show.title} to watch list`}\n onAdd={() => { void addShow(show); }}\n />\n );\n })}\n </Section>\n <Section label=\"YouTube\" answer={videoAnswer} testId=\"search-youtube\">\n {videoAnswer?.ok && videoAnswer.value.map(video => (\n <ResultRow\n key={video.id}\n testId={`video-${video.id}`}\n image={resolveSearchImage(video.thumb)}\n imageClass=\"w-16 h-9\"\n title={video.title}\n meta={[video.channel, formatDuration(video.durationS)]}\n state={rowState[`yt:${video.id}`]}\n canAdd\n addLabel={`Request ${video.title}`}\n onAdd={() => { void addVideo(video); }}\n />\n ))}\n </Section>\n </div>\n );\n}\n\nfunction Section<T>({ label, answer, testId, children }: {\n label: string;\n answer: SearchOutcome<T[]> | null;\n testId: string;\n children: React.ReactNode;\n}) {\n if (answer?.ok && answer.value.length === 0) return null;\n const failed = answer && !answer.ok && answer.status !== 'aborted';\n return (\n <div data-testid={testId} data-loaded={answer ? 'true' : 'false'}>\n <div className=\"px-3 pt-2 pb-1 flex items-center gap-2 text-[10px] uppercase tracking-wider text-text-muted\">\n <span>{label}</span>\n {!answer && <span className=\"text-text-tertiary\">…</span>}\n {failed && <span className=\"bg-danger-bg text-on-danger-bg px-1 rounded normal-case\">unavailable</span>}\n </div>\n {children}\n </div>\n );\n}\n\nfunction ResultRow({ testId, image, imageClass, title, meta, state, canAdd, addLabel, onAdd }: {\n testId: string;\n image: string | null;\n imageClass: string;\n title: string;\n meta: Array<string | number | null>;\n state: RowState | undefined;\n canAdd: boolean;\n addLabel: string;\n onAdd: () => void;\n}) {\n const metaText = meta.filter(m => m !== null && m !== '').join(' · ');\n return (\n <div className=\"px-3 py-1 flex items-center gap-2\" data-testid={testId} data-state={state ?? ''}>\n {image\n ? <img src={image} alt=\"\" loading=\"lazy\" className={`${imageClass} object-cover rounded shrink-0 bg-bg-alt`} />\n : <div className={`${imageClass} rounded shrink-0 bg-bg-alt`} />}\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[11px] text-text truncate\" title={title}>{title}</div>\n {metaText && <div className=\"text-[10px] text-text-muted truncate\">{metaText}</div>}\n </div>\n {state === 'added'\n ? <Icon name=\"check\" variant=\"tint\" family=\"success\" round className=\"shrink-0\" />\n : state && state !== 'adding'\n ? <span className=\"text-[10px] bg-danger-bg text-on-danger-bg px-1 rounded shrink-0\">{state}</span>\n : canAdd && (\n <button\n onClick={onAdd}\n disabled={state === 'adding'}\n aria-label={addLabel}\n className=\"drawer-btn shrink-0\"\n >\n <Icon name=\"plus\" />\n </button>\n )}\n </div>\n );\n}\n","/**\n * Ask for media — a link, a magnet, or a file — and the state of everything\n * you have asked for.\n *\n * The first user-facing surface of the submissions programme. Moderation is\n * approve-first, so nothing typed here reaches the catalog on its own: a row\n * lands in `pending` and an operator moves it. That is why this drawer shows\n * the caller's OWN queue rather than a confirmation toast — the honest answer\n * to \"did that work?\" is the row's state, and it keeps changing after the\n * click.\n *\n * ── Three ways in, one field each ──────────────────────────────────────────\n * The link field takes words as well: anything without a scheme searches\n * (`MediaSearchResults` — shows to add to the scraper's watch list, YouTube\n * videos to submit as links), and Submit stands down while it does.\n * Otherwise it takes a URL or a magnet (kind is read off the scheme —\n * `guessSubmissionKind` — never chosen from a select). Reading as a magnet\n * reveals two more fields: `series` (the scraper's destination folder — an\n * anime series already in the catalog is offered as a suggestion, a new name\n * is accepted) and a title, PRE-FILLED from the magnet's `dn=` so a caller\n * who already typed a well-formed link rarely has to type anything twice.\n * Both are sent to the server and are a COURTESY only — `checkSource('magnet',\n * …)` is the one place either requirement is enforced; see the wave brief's\n * mutation check (a).\n *\n * A file goes a different way entirely: it chunk-uploads straight to\n * filetransfer (own protocol, verified against that service's own browser\n * client — see `lib/submissionsApi.ts`), addressed to watchparty's own\n * identity, and only once that lands does a submission get recorded — with\n * the filetransfer file id as `source`. The 4 GB cap is shown and enforced\n * client-side BEFORE the upload starts, as a courtesy; the authoritative\n * check is server-side, on bytes actually received (wave 4, lane A).\n *\n * ── Through DrawerShell, not a third backdrop ──────────────────────────────\n * MediaDrawer and the old GameDrawer each hand-copied the backdrop/panel/\n * Escape chrome and had already drifted (only one closed on a backdrop\n * click). The shell exists so a third drawer is not a third copy.\n *\n * ── Buttons carry a real class ─────────────────────────────────────────────\n * `styles.css` has an unlayered `button { background: none; color: inherit;\n * border: none }` reset, and unlayered CSS beats anything in a `@layer`\n * regardless of specificity — so every `bg-*`, `text-*` and `border-*` utility\n * on a `<button>` is silently discarded. `.drawer-btn` and its modifiers are\n * real class selectors and outrank the reset.\n *\n * ── The list polls while open ──────────────────────────────────────────────\n * A submission's state is moved by an operator and then by a server-side\n * runner; nothing pushes it to this client. The poll is scoped to the drawer\n * being open and torn down with it.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { Icon } from '@wolffm/themes';\nimport type { TSubmission, TSubmissionState } from '@watchparty/shared';\nimport DrawerShell, { DRAWER_BTN } from './DrawerShell';\nimport { fetchCatalog } from '../lib/catalog';\nimport {\n extractMagnetDisplayName,\n fetchMySubmissions,\n fetchUploadTarget,\n guessSubmissionKind,\n hasEpisodeCode,\n submissionFailureLabel,\n submitMagnetSubmission,\n submitSubmission,\n submitUploadSubmission,\n uploadFailureLabel,\n uploadFileForSubmission,\n formatSubmissionBytes,\n type SubmissionFailure,\n type UploadFailure,\n type UploadPhase,\n type UploadTarget,\n} from '../lib/submissionsApi';\nimport { isSearchText } from '../lib/mediaSearchApi';\nimport { debug, errorFields } from '../lib/debug';\nimport MediaSearchResults from './MediaSearchResults';\n\ninterface AddMediaDrawerProps { open: boolean; onClose: () => void; }\n\n/** How often the caller's own queue is re-read while this drawer is open. */\nconst POLL_MS = 4000;\n\n/**\n * State → colour. Terminal-good is success, terminal-bad is danger, and\n * everything in flight is the primary accent — the three groups a user\n * actually distinguishes.\n */\nconst STATE_CLASS: Record<TSubmissionState, string> = {\n pending: 'bg-neutral-bg text-on-neutral-bg',\n approved: 'bg-primary-bg text-on-primary-bg',\n fetching: 'bg-primary-bg text-on-primary-bg',\n ingesting: 'bg-primary-bg text-on-primary-bg',\n ready: 'bg-success-bg text-on-success-bg',\n rejected: 'bg-danger-bg text-on-danger-bg',\n failed: 'bg-danger-bg text-on-danger-bg',\n};\n\n/** `TSubmission` as read today, plus the field `series` adds once the shared\n * schema carries it (packages/shared/src/submissions.ts). A cast rather than\n * a shared-type edit — this file does not own that schema — and harmless\n * once the real field lands: the shapes agree. */\ntype SubmissionWithSeries = TSubmission & { series?: string };\n\nfunction SubmissionRow({ item }: { item: TSubmission }) {\n const withSeries = item as SubmissionWithSeries;\n const label = item.title || item.source;\n return (\n <div\n className=\"px-3 py-1.5 border-b border-border/50 flex items-center gap-2\"\n data-submission-id={item.id}\n data-submission-state={item.state}\n data-submission-kind={item.kind}\n data-submission-series={withSeries.series ?? ''}\n >\n <span className=\"text-[10px] uppercase tracking-wider text-text-muted shrink-0\">{item.kind}</span>\n <span className=\"text-[11px] text-text-tertiary truncate flex-1\" title={item.source}>\n {withSeries.series ? `${withSeries.series} — ${label}` : label}\n </span>\n <span className={`text-[10px] uppercase tracking-wider px-1 rounded ${STATE_CLASS[item.state] ?? 'text-text-muted'}`}>\n {item.state}\n </span>\n {item.error && <span className=\"text-[10px] bg-danger-bg text-on-danger-bg px-1 rounded truncate max-w-[10rem]\">{item.error}</span>}\n </div>\n );\n}\n\nexport default function AddMediaDrawer({ open, onClose }: AddMediaDrawerProps) {\n const [source, setSource] = useState('');\n const [series, setSeries] = useState('');\n const [title, setTitle] = useState('');\n const [titleTouched, setTitleTouched] = useState(false);\n const [seriesOptions, setSeriesOptions] = useState<string[]>([]);\n const [busy, setBusy] = useState(false);\n const [failure, setFailure] = useState<SubmissionFailure | null>(null);\n const [rows, setRows] = useState<TSubmission[] | null>(null);\n const [listFailure, setListFailure] = useState<SubmissionFailure | null>(null);\n\n const [uploadTarget, setUploadTarget] = useState<UploadTarget | null>(null);\n const [selectedFile, setSelectedFile] = useState<File | null>(null);\n const [uploadBusy, setUploadBusy] = useState(false);\n const [uploadProgress, setUploadProgress] = useState<{ sent: number; total: number; phase: UploadPhase } | null>(null);\n const [uploadFailure, setUploadFailure] = useState<UploadFailure | SubmissionFailure | null>(null);\n const fileInputRef = useRef<HTMLInputElement | null>(null);\n\n // A poll that resolves after the drawer closed must not resurrect its state.\n const liveRef = useRef(false);\n\n const kind = useMemo(() => guessSubmissionKind(source), [source]);\n const isMagnet = kind === 'magnet' && source.trim().length > 0;\n const searching = isSearchText(source);\n\n const refresh = useCallback(() => fetchMySubmissions().then(res => {\n if (!liveRef.current) return;\n if (res.ok) { setRows(res.value); setListFailure(null); }\n else { setListFailure(res.failure); }\n }), []);\n\n useEffect(() => {\n if (!open) return;\n liveRef.current = true;\n void refresh();\n const timer = setInterval(() => { void refresh(); }, POLL_MS);\n return () => { liveRef.current = false; clearInterval(timer); };\n }, [open, refresh]);\n\n // The upload target (recipient + cap) is fetched once per open, ahead of any\n // file being picked, so the cap is on screen before the user chooses a file\n // rather than only after they commit to uploading one.\n useEffect(() => {\n if (!open) return;\n let live = true;\n void fetchUploadTarget().then(res => { if (live && res.ok) setUploadTarget(res.value); });\n return () => { live = false; };\n }, [open]);\n\n // Existing anime series, as suggestions only — a new name is accepted.\n useEffect(() => {\n if (!open) return;\n let live = true;\n // The anime SERIES live at output/fmp4/anime. This read 'anime', a prefix\n // that does not exist, so the suggestion list was always empty.\n void fetchCatalog('output/fmp4/anime').then(cat => {\n if (live) setSeriesOptions(cat.dirs.map(d => d.name));\n }).catch((err: unknown) => {\n // Suggestions are a nicety: their absence changes nothing but this line.\n debug('media', 'series suggestions unavailable', errorFields(err));\n });\n return () => { live = false; };\n }, [open]);\n\n /**\n * The link field's `onChange`. Prefilling the title from a magnet's `dn=`\n * lives HERE, in the event handler that changes `source`, rather than in a\n * `useEffect` watching it — a `useEffect` that calls `setState` in its body\n * (as opposed to synchronizing with something external) is exactly the\n * pattern `react-hooks/set-state-in-effect` flags, and the derivation is\n * already keyed on the very event that should trigger it, so there is\n * nothing left for an effect to \"synchronize\". Never clobbers a manual\n * title edit — a caller who already typed their own title keeps it even if\n * the source field changes afterward.\n */\n function handleSourceChange(value: string) {\n setSource(value);\n const nowMagnet = guessSubmissionKind(value) === 'magnet' && value.trim().length > 0;\n if (nowMagnet && !titleTouched) {\n const dn = extractMagnetDisplayName(value);\n if (dn) setTitle(dn);\n }\n }\n\n function resetLinkFields() {\n setSource('');\n setSeries('');\n setTitle('');\n setTitleTouched(false);\n }\n\n const magnetReady = !isMagnet || (series.trim().length > 0 && hasEpisodeCode(title));\n\n async function handleSubmit() {\n const trimmed = source.trim();\n if (!trimmed || busy || searching) return;\n if (isMagnet && !magnetReady) return;\n setBusy(true);\n setFailure(null);\n const res = isMagnet\n ? await submitMagnetSubmission(trimmed, { title: title.trim() || undefined, series: series.trim() })\n : await submitSubmission(trimmed);\n setBusy(false);\n if (!res.ok) { setFailure(res.failure); return; }\n debug('submissions', 'Submit accepted', { id: res.value.id, state: res.value.state });\n resetLinkFields();\n // Show the new row at once rather than waiting out a poll tick: the\n // response already carries it, and a field that empties with nothing new\n // beneath it reads as a click that did nothing.\n setRows(prev => [res.value, ...(prev ?? []).filter(r => r.id !== res.value.id)]);\n void refresh();\n }\n\n function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {\n const file = e.target.files?.[0] ?? null;\n setUploadFailure(null);\n if (file && uploadTarget && file.size > uploadTarget.maxBytes) {\n setSelectedFile(null);\n setUploadFailure({ code: 'too_large', maxBytes: uploadTarget.maxBytes });\n if (fileInputRef.current) fileInputRef.current.value = '';\n return;\n }\n setSelectedFile(file);\n }\n\n async function handleUpload() {\n if (!selectedFile || uploadBusy) return;\n setUploadBusy(true);\n setUploadFailure(null);\n\n let target = uploadTarget;\n if (!target) {\n const res = await fetchUploadTarget();\n if (!res.ok) { setUploadBusy(false); setUploadFailure(res.failure); return; }\n target = res.value;\n setUploadTarget(target);\n }\n if (selectedFile.size > target.maxBytes) {\n setUploadBusy(false);\n setUploadFailure({ code: 'too_large', maxBytes: target.maxBytes });\n return;\n }\n\n const uploaded = await uploadFileForSubmission(selectedFile, target, (sent, total, phase) => {\n setUploadProgress({ sent, total, phase });\n });\n if (!uploaded.ok) {\n setUploadBusy(false);\n setUploadProgress(null);\n setUploadFailure(uploaded.failure);\n return;\n }\n\n const recorded = await submitUploadSubmission(uploaded.value.fileId, selectedFile.name);\n setUploadBusy(false);\n setUploadProgress(null);\n if (!recorded.ok) { setUploadFailure(recorded.failure); return; }\n\n debug('submissions', 'Upload accepted', { id: recorded.value.id });\n setSelectedFile(null);\n if (fileInputRef.current) fileInputRef.current.value = '';\n setRows(prev => [recorded.value, ...(prev ?? []).filter(r => r.id !== recorded.value.id)]);\n void refresh();\n }\n\n const uploadStatus = uploadFailure\n ? ('maxBytes' in uploadFailure || uploadFailure.code === 'target_unavailable' || uploadFailure.code === 'upload_failed'\n ? uploadFailureLabel(uploadFailure as UploadFailure)\n : submissionFailureLabel(uploadFailure as SubmissionFailure))\n : null;\n\n return (\n <DrawerShell\n open={open}\n onClose={onClose}\n header={\n <>\n <span className=\"text-xs uppercase tracking-wider text-text-muted\">Add media</span>\n <button\n onClick={() => { void refresh(); }}\n aria-label=\"Refresh submissions\"\n className={`${DRAWER_BTN} ml-auto`}\n >\n <Icon name=\"refresh\" />\n </button>\n </>\n }\n >\n <div className=\"p-3 border-b border-border flex flex-col gap-2\">\n <input\n value={source}\n onChange={e => handleSourceChange(e.target.value)}\n onKeyDown={e => { if (e.key === 'Enter' && !isMagnet) void handleSubmit(); }}\n aria-label=\"Media link\"\n placeholder=\"search, https:// or magnet:\"\n className=\"w-full bg-bg-alt border border-border focus:border-border-light outline-none px-2 py-1 rounded text-xs\"\n />\n {isMagnet && (\n <div className=\"flex flex-col gap-1.5\" data-testid=\"magnet-fields\">\n <input\n value={series}\n onChange={e => setSeries(e.target.value)}\n list=\"add-media-series-options\"\n aria-label=\"Series\"\n placeholder=\"series\"\n className=\"w-full bg-bg-alt border border-border focus:border-border-light outline-none px-2 py-1 rounded text-xs\"\n />\n <datalist id=\"add-media-series-options\">\n {seriesOptions.map(name => <option key={name} value={name} />)}\n </datalist>\n <input\n value={title}\n onChange={e => { setTitleTouched(true); setTitle(e.target.value); }}\n aria-label=\"Title\"\n placeholder=\"title (SxxExx)\"\n className=\"w-full bg-bg-alt border border-border focus:border-border-light outline-none px-2 py-1 rounded text-xs\"\n />\n </div>\n )}\n <div className=\"flex items-center gap-2\">\n <button\n onClick={() => { void handleSubmit(); }}\n disabled={busy || !source.trim() || searching || (isMagnet && !magnetReady)}\n aria-label=\"Submit media\"\n className=\"drawer-btn drawer-btn-primary\"\n >\n {busy ? 'Submitting…' : 'Submit'}\n </button>\n {failure && (\n <span data-testid=\"submit-failure\" className=\"text-[11px] bg-danger-bg text-on-danger-bg px-1.5 py-0.5 rounded\">\n {submissionFailureLabel(failure)}\n </span>\n )}\n </div>\n </div>\n\n <MediaSearchResults\n query={source}\n onSubmitted={row => setRows(prev => [row, ...(prev ?? []).filter(r => r.id !== row.id)])}\n />\n\n <div className=\"p-3 border-b border-border flex flex-col gap-2\">\n <div className=\"flex items-center gap-2\">\n <input\n ref={fileInputRef}\n type=\"file\"\n onChange={handleFileChange}\n aria-label=\"Upload file\"\n className=\"text-[11px] flex-1 min-w-0\"\n />\n <button\n onClick={() => { void handleUpload(); }}\n disabled={!selectedFile || uploadBusy}\n aria-label=\"Upload\"\n className=\"drawer-btn drawer-btn-primary\"\n >\n {uploadBusy\n ? (uploadProgress?.phase === 'assembling' ? 'Assembling…' : 'Uploading…')\n : 'Upload'}\n </button>\n </div>\n <span className=\"text-[10px] text-text-muted\" data-testid=\"upload-cap\">\n {uploadTarget ? `cap ${formatSubmissionBytes(uploadTarget.maxBytes)}` : ''}\n </span>\n {uploadStatus && (\n <span data-testid=\"upload-failure\" className=\"text-[11px] bg-danger-bg text-on-danger-bg px-1.5 py-0.5 rounded\">{uploadStatus}</span>\n )}\n </div>\n\n <div className=\"flex-1 overflow-auto\" data-testid=\"my-submissions\">\n {listFailure && (\n <div className=\"mx-3 my-2 px-3 py-2 text-[11px] bg-danger-bg text-on-danger-bg rounded\">{submissionFailureLabel(listFailure)}</div>\n )}\n {rows?.map(item => <SubmissionRow key={item.id} item={item} />)}\n </div>\n </DrawerShell>\n );\n}\n","import { useAppStore } from '../lib/store';\nimport { emitGameSelect } from '../lib/socket';\nimport { debug } from '../lib/debug';\nimport { useGameCatalog } from '../lib/gameCatalog';\nimport bombermanCover from '../assets/games/bomberman/cover.webp';\n\n// The games themselves are DATA — the server's GAMES config, via\n// lib/gameCatalog.ts. A new game brings its own `cover` URL; this is only the\n// built-in game's art, which ships inside this bundle.\nconst BUILT_IN_COVERS: Record<string, string> = { bomberman: bombermanCover };\n\nfunction stopGame() {\n emitGameSelect(null);\n debug('game', 'game_select', { gameId: null });\n}\n\n/**\n * The media drawer's \"Play\" tab: pick a game to turn on for the whole room\n * (broadcast via game_select → every client's idle screen mounts it), or stop\n * the active one.\n *\n * This used to be its own drawer behind its own toolbar button. It is content\n * now, rendered inside `MediaDrawer` beside \"Watch\", so the leader has ONE\n * place to decide what the room does next rather than two buttons that each\n * open a drawer. Like the drawer it lives in, it is only reachable by a leader\n * — the Select Media entry point is leader-gated — so it carries no gate of\n * its own.\n */\nexport function GameGrid({ onDone }: { onDone: () => void }) {\n const activeGameId = useAppStore(s => s.activeGameId);\n const games = useGameCatalog();\n\n function start(gameId: string) {\n emitGameSelect(gameId);\n debug('game', 'game_select', { gameId });\n onDone();\n }\n function stop() {\n stopGame();\n onDone();\n }\n\n return (\n <div className=\"flex-1 overflow-auto p-3\">\n <div className=\"grid grid-cols-2 gap-3\">\n {games.map(g => {\n const isActive = activeGameId === g.id;\n const cover = g.cover ?? BUILT_IN_COVERS[g.id];\n return (\n <button\n key={g.id}\n onClick={() => (isActive ? stop() : start(g.id))}\n className={`group relative aspect-square overflow-hidden rounded-lg border-2 transition-all cursor-pointer ${\n isActive\n ? 'border-primary shadow-lg shadow-primary/30'\n : 'border-border hover:border-primary'\n }`}\n title={isActive ? `Stop ${g.name}` : `Start ${g.name}`}\n >\n {cover ? (\n <img\n src={cover}\n alt={g.name}\n className=\"absolute inset-0 w-full h-full object-cover transition-transform group-hover:scale-105\"\n />\n ) : (\n <div className=\"absolute inset-0 bg-bg-alt\" />\n )}\n {/* Bottom-up gradient for the name label so it stays readable on any cover. */}\n <div className=\"absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/85 via-black/40 to-transparent px-2 pt-6 pb-2\">\n <div className=\"text-sm font-medium text-white leading-tight\">{g.name}</div>\n </div>\n {isActive && (\n <span className=\"absolute top-2 left-2 px-1.5 py-0.5 bg-primary text-[10px] uppercase tracking-wide rounded text-white\">\n Active\n </span>\n )}\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n\n/** Header control for the Play tab; renders nothing when no game is running. */\nexport function StopGameButton({ onDone }: { onDone: () => void }) {\n const activeGameId = useAppStore(s => s.activeGameId);\n if (!activeGameId) return null;\n return (\n <button onClick={() => { stopGame(); onDone(); }} className=\"drawer-btn drawer-btn-danger ml-auto\">\n Stop game\n </button>\n );\n}\n","import { useCallback, useEffect, useMemo, useState } from 'react';\nimport { fetchCatalog, invalidateCatalog } from '../lib/catalog';\nimport { sendLoad, sendHome } from '../lib/controls';\nimport { isDebugEnabled, debug, warn, errorFields } from '../lib/debug';\nimport { probeMediaEndpoints } from '../lib/catalogProbe';\nimport { loadCatalogTree, type CategoryNode, type MediaTree, type PlaylistCategoryNode, type SeriesNode } from '../lib/catalogTree';\nimport { emitPlaylistLoad } from '../lib/socket';\nimport { captureExpansion, mergeExpansion, resetToDefaultExpansion } from '../lib/mediaTreeExpansion';\nimport { useLeaderAffordance } from '../hooks/useStoreSelectors';\nimport { useAppStore } from '../lib/store';\nimport { canSubmitMedia } from '../lib/submissionsApi';\nimport AddMediaDrawer from './AddMediaDrawer';\nimport DrawerShell, { DRAWER_BTN } from './DrawerShell';\nimport { GameGrid, StopGameButton } from './GamePicker';\n\n/** What the room does next: put something on (Watch) or start a waiting-room game (Play). */\ntype DrawerMode = 'watch' | 'play';\n\ninterface MediaDrawerProps { open: boolean; onClose: () => void; }\n\n// A category's expandable children are its series; a playlist category's\n// entries are leaves, so it contributes nothing to merge and is left alone.\nconst childrenOf = (node: CategoryNode | PlaylistCategoryNode) =>\n node.type === 'category' ? node.series : [];\nconst withChildren = (\n node: CategoryNode | PlaylistCategoryNode,\n children: { name: string; expanded?: boolean }[]\n) => (node.type === 'category' ? { ...node, series: children as SeriesNode[] } : node);\n\nexport default function MediaDrawer({ open, onClose }: MediaDrawerProps) {\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [tree, setTree] = useState<MediaTree>([]);\n const [filter, setFilter] = useState('');\n // The submit surface is a SECOND drawer, not a section of this one: it owns\n // a field, a queue and a poll, and folding it in here would put all three\n // behind the catalog fetches this drawer performs on open.\n // Only one is rendered at a time (see `open={open && !addOpen}` below), so\n // there is never a second backdrop or a second Escape handler in play.\n const [addOpen, setAddOpen] = useState(false);\n const [mode, setMode] = useState<DrawerMode>('watch');\n const mediaId = useAppStore(s => s.snapshot.mediaId);\n // Friend tier and above — the same boundary `POST /api/submissions` enforces.\n // Read once per render rather than stored: auth is module state set from\n // mount props before this component ever mounts.\n const canSubmit = canSubmitMedia();\n\n // Callers raise `loading` and clear `error` first; this only lands the result.\n // Never rejects: every failure becomes `error`.\n const buildTree = useCallback(() => loadCatalogTree(fetchCatalog)\n .then(result => {\n // The drawer stays mounted, so it renders the PREVIOUS tree while this\n // rebuild is in flight. Anything the user expanded in that window is on\n // a tree about to be replaced — carry those flags over, or a background\n // refresh silently collapses a deliberate click right after the drawer\n // opens. Uses the functional form so it merges against the tree as it is\n // AT THAT MOMENT, including expansions made while the fetch was in flight.\n setTree(prev => mergeExpansion(result, captureExpansion(prev, childrenOf), childrenOf, withChildren));\n // The probe logs its own failures.\n if (isDebugEnabled()) void probeMediaEndpoints();\n })\n .catch((e: unknown) => {\n setError((e instanceof Error && e.message) || 'catalog_error');\n })\n .finally(() => setLoading(false)), []);\n\n // Build the tree when the room loads, not when the drawer first opens. This\n // component is mounted (closed) for the life of the room, so without this the\n // leader's first click paid for the whole catalog. Only a leader can open the\n // drawer, so nobody else fetches it. The JSON is a few KB — the tierlist's\n // \"fetch the board on MOUNT\" trade, not its \"warm images on intent\" one.\n // State is set only when the data lands (never synchronously in the effect\n // body), and with no loading flag: nobody is looking at the drawer yet.\n const isLeader = useLeaderAffordance();\n useEffect(() => {\n if (!isLeader) return;\n let live = true;\n loadCatalogTree(fetchCatalog).then(\n result => {\n if (live) setTree(prev => mergeExpansion(result, captureExpansion(prev, childrenOf), childrenOf, withChildren));\n },\n (err: unknown) => warn('catalog', 'Catalog prefetch failed', errorFields(err)),\n );\n return () => { live = false; };\n }, [isLeader]);\n\n // Every open is a fresh start on Watch, with only Anime expanded — not a\n // resumption of whatever the last visit left open. The reset MUST run before\n // the rebuild: `buildTree` merges its result onto the tree as it stands when\n // the fetch lands, so resetting first means a series the user expands during\n // that fetch window still survives, while everything untouched shows\n // the defaults. Resetting after would discard that click. Pinned in\n // mediaTreeExpansion.test.ts. The mount prefetch means the tree is already\n // on screen; this rebuild refreshes it behind the user (a cache hit within\n // lib/catalog.ts's CACHE_MS, a background refetch after).\n const [wasOpen, setWasOpen] = useState(false);\n if (open !== wasOpen) {\n setWasOpen(open);\n if (open) {\n setMode('watch');\n setTree(t => resetToDefaultExpansion(t, childrenOf, withChildren));\n setLoading(true); setError(null);\n }\n }\n useEffect(() => {\n if (open) void buildTree();\n }, [open, buildTree]);\n\n function handleEpisodeSelect(mediaId: string) {\n sendLoad(mediaId);\n debug('catalog', 'control_load', { mediaId });\n onClose();\n }\n function handlePlaylistSelect(playlistId: string) {\n emitPlaylistLoad(playlistId);\n debug('catalog', 'playlist_load', { playlistId });\n onClose();\n }\n // Room-wide unload: control/home resets the room's media to null for\n // EVERY viewer and ends any active playlist — it is not a second way to\n // dismiss this drawer. It used to be labelled \"Home\", which read as exactly\n // that next to Close, and with no media loaded it genuinely is a no-op that\n // only closes the drawer. Hence the honest label, and hiding it unless\n // there is something to unload.\n function handleUnload() {\n sendHome();\n debug('controls', 'Unload media clicked (media drawer)');\n onClose();\n }\n function toggleSeries(ci: number, si: number) {\n setTree(t => t.map((c,i)=> (i!==ci || !('series' in c))? c : ({ ...c, series: c.series.map((s,j)=> j!==si? s : ({ ...s, expanded: !s.expanded })) })));\n }\n function toggleCategory(ci: number) {\n setTree(t => t.map((c,i)=> i!==ci? c : ({ ...c, expanded: !c.expanded })));\n }\n function refreshAll() {\n invalidateCatalog('');\n setLoading(true); setError(null);\n void buildTree();\n }\n\n const filteredTree = useMemo(() => {\n if (!filter.trim()) return tree;\n const q = filter.toLowerCase();\n return tree.map(node => {\n if (node.type === 'playlist_category') {\n const playlists = node.playlists.filter(p => p.name.toLowerCase().includes(q));\n if (playlists.length) return { ...node, playlists, expanded: true } as PlaylistCategoryNode;\n if (node.name.toLowerCase().includes(q)) return { ...node, expanded: true } as PlaylistCategoryNode;\n return null;\n }\n const seriesFiltered = node.series.map(s => {\n const eps = s.episodes.filter(e => e.name.toLowerCase().includes(q));\n if (eps.length) return { ...s, episodes: eps, expanded: true } as SeriesNode;\n if (s.name.toLowerCase().includes(q)) return { ...s, expanded: true } as SeriesNode;\n return null;\n }).filter(Boolean) as SeriesNode[];\n if (seriesFiltered.length) return { ...node, series: seriesFiltered, expanded: true } as CategoryNode;\n if (node.name.toLowerCase().includes(q)) return { ...node, expanded: true } as CategoryNode;\n return null;\n }).filter(Boolean) as MediaTree;\n }, [tree, filter]);\n\n return (\n <>\n <AddMediaDrawer open={addOpen} onClose={() => setAddOpen(false)} />\n <DrawerShell\n open={open && !addOpen}\n onClose={onClose}\n header={\n <>\n {/* Reuses the tierlist's segmented-tab styling rather than a second\n copy of it. Real classes, not Tailwind utilities: the unlayered\n `button` reset makes every colour/background utility inert. */}\n <div className=\"tl-tabs\" role=\"tablist\" aria-label=\"Media drawer\">\n {(['watch', 'play'] as const).map(m => (\n <button\n key={m}\n role=\"tab\"\n aria-selected={mode === m}\n onClick={() => setMode(m)}\n className=\"tl-tab\"\n >\n {m === 'watch' ? 'Watch' : 'Play'}\n </button>\n ))}\n </div>\n {mode === 'play' && <StopGameButton onDone={onClose} />}\n {mode === 'watch' && <>\n {canSubmit && (\n <button\n onClick={() => setAddOpen(true)}\n aria-label=\"Add Media\"\n className={DRAWER_BTN}\n >\n Add\n </button>\n )}\n {mediaId && (\n <button\n onClick={handleUnload}\n title=\"Unload the current media for everyone in the room\"\n className=\"drawer-btn drawer-btn-primary\"\n >\n Unload\n </button>\n )}\n {/* The spinner is the whole point of this control: a reload that\n hits a warm catalog cache returns too fast to see, so without a\n visible in-flight state you cannot tell a refresh happened.\n Disabled while in flight so a second click can't stack rebuilds. */}\n <button onClick={refreshAll} disabled={loading} className={`${DRAWER_BTN} inline-flex items-center gap-1.5`}>\n {loading && <span className=\"inline-block w-3 h-3 border-2 border-text-muted border-t-transparent rounded-full animate-spin\" />}\n {loading ? 'Reloading…' : 'Reload'}\n </button>\n <input value={filter} onChange={e=>setFilter(e.target.value)} placeholder=\"search\" className=\"ml-auto bg-bg-alt border border-border focus:border-border-light outline-none px-2 py-1 rounded text-xs\" />\n </>}\n </>\n }\n footer={isDebugEnabled() ? (\n <div className=\"p-2 text-[10px] text-text-muted border-t border-border flex justify-end\">\n <span className=\"opacity-70\">verbose</span>\n </div>\n ) : undefined}\n >\n {mode === 'play' ? <GameGrid onDone={onClose} /> : <>\n {loading && !tree.length && <div className=\"px-4 py-2 text-xs text-text-muted\">Loading catalog…</div>}\n {error && <div className=\"mx-3 px-3 py-2 text-xs bg-danger-bg text-on-danger-bg rounded\">{error}</div>}\n <div className=\"flex-1 overflow-auto text-sm py-2\">\n {filteredTree.map((node, ci) => (\n <div key={node.name} className=\"px-3 pb-3\">\n {/* data-* expose the STATE, so E2E asserts which categories are\n open rather than reading the −/+ glyph. */}\n <div\n className=\"flex items-center gap-2 cursor-pointer select-none group\"\n data-category={node.name}\n data-expanded={node.expanded ? 'true' : 'false'}\n onClick={()=>toggleCategory(ci)}\n >\n <span className=\"text-xs uppercase tracking-wider text-text-muted group-hover:text-text-secondary\">{node.name}</span>\n <span className=\"text-[10px] opacity-50\">{node.expanded? '−':'+'}</span>\n </div>\n {node.expanded && node.type === 'playlist_category' && (\n <div className=\"mt-2 space-y-1\">\n {node.playlists.map(p => (\n <button key={p.playlistId} onClick={()=>handlePlaylistSelect(p.playlistId)} className=\"w-full text-left px-2 py-1 rounded hover:bg-bg-alt flex items-center gap-2 group\">\n <span className=\"text-[11px] text-text-tertiary group-hover:text-text-secondary truncate\">{p.name}</span>\n <span className=\"ml-auto text-[10px] text-text underline decoration-primary underline-offset-2 opacity-0 group-hover:opacity-100 transition-opacity\">Load</span>\n </button>\n ))}\n {node.playlists.length === 0 && <div className=\"text-[11px] text-text-muted italic\">(no playlists)</div>}\n </div>\n )}\n {node.expanded && node.type === 'category' && (\n <div className=\"mt-2 space-y-2\">\n {node.series.map((s, si) => (\n <div key={s.name}>\n <div className=\"flex items-center gap-2 cursor-pointer text-text-secondary hover:text-text\" onClick={()=>toggleSeries(ci, si)}>\n <span className=\"text-xs font-medium\">{s.name}</span>\n <span className=\"text-[10px] opacity-40\">{s.expanded? '−':'+'}</span>\n </div>\n {s.expanded && (\n <div className=\"mt-1 ml-3 border-l border-border pl-3 space-y-1\">\n {s.episodes.map(ep => (\n <button key={ep.mediaId} onClick={()=>handleEpisodeSelect(ep.mediaId)} className=\"w-full text-left px-2 py-1 rounded hover:bg-bg-alt flex items-center gap-2 group\">\n <span className=\"text-[11px] text-text-tertiary group-hover:text-text-secondary truncate\">{ep.name}</span>\n <span className=\"ml-auto text-[10px] text-text underline decoration-primary underline-offset-2 opacity-0 group-hover:opacity-100 transition-opacity\">Load</span>\n </button>\n ))}\n {s.episodes.length === 0 && <div className=\"text-[11px] text-text-muted italic\">(no episodes)</div>}\n </div>\n )}\n </div>\n ))}\n {node.series.length === 0 && <div className=\"text-[11px] text-text-muted italic\">(empty)</div>}\n </div>\n )}\n </div>\n ))}\n {!loading && !filteredTree.length && <div className=\"px-4 py-6 text-center text-xs text-text-muted\">No matches</div>}\n </div>\n </>}\n </DrawerShell>\n </>\n );\n}\n"],"names":["cache","inflight","CACHE_MS","fetchCatalog","prefix","now","hit","pending","request","url","getRuntimeServerOrigin","res","data","isDebugEnabled","debug","invalidateCatalog","probeMediaEndpoints","categories","cat","catData","candidate","base","mediaBase","indexUrl","mediaUrl","FRAGMENT_MP4_FILENAME","fetchAndLog","err","warn","label","init","t0","ms","ECCHI_SERIES","norm","name","matchesSeriesList","series","isEcchiSeries","captureExpansion","tree","childrenOf","state","node","children","child","mergeExpansion","withChildren","prev","mergedChildren","wasExpanded","DEFAULT_OPEN_CATEGORY","isOpenByDefault","categoryName","resetToDefaultExpansion","ROOT","CATEGORIES","tryFetch","fetcher","errorFields","loadCatalogTree","categoryListings","playlistListing","seriesPerCategory","listing","d","episodes","f","result","ecchiSeries","i","seriesNodes","playlists","HAS_SCHEME","isSearchText","raw","text","SEARCH_MIN_QUERY","SEARCH_MAX_QUERY","resolveSearchImage","pathOrNull","formatDuration","totalS","s","m","h","ss","getJson","path","signal","searchShows","q","searchVideos","addShowToWatchlist","source","id","value","DEBOUNCE_MS","MediaSearchResults","query","onSubmitted","active","shows","setShows","useState","videos","setVideos","rowState","setRowState","useEffect","ctrl","timer","r","showAnswer","videoAnswer","addShow","show","key","addVideo","video","submitSubmission","submissionFailureLabel","bothEmpty","jsxs","jsx","Section","ResultRow","answer","testId","failed","image","imageClass","title","meta","canAdd","addLabel","onAdd","metaText","Icon","POLL_MS","STATE_CLASS","SubmissionRow","item","withSeries","AddMediaDrawer","open","onClose","setSource","setSeries","setTitle","titleTouched","setTitleTouched","seriesOptions","setSeriesOptions","busy","setBusy","failure","setFailure","rows","setRows","listFailure","setListFailure","uploadTarget","setUploadTarget","selectedFile","setSelectedFile","uploadBusy","setUploadBusy","uploadProgress","setUploadProgress","uploadFailure","setUploadFailure","fileInputRef","useRef","liveRef","isMagnet","useMemo","guessSubmissionKind","searching","refresh","useCallback","fetchMySubmissions","live","fetchUploadTarget","handleSourceChange","dn","extractMagnetDisplayName","resetLinkFields","magnetReady","hasEpisodeCode","handleSubmit","trimmed","submitMagnetSubmission","handleFileChange","e","file","handleUpload","target","uploaded","uploadFileForSubmission","sent","total","phase","recorded","submitUploadSubmission","uploadStatus","uploadFailureLabel","DrawerShell","Fragment","DRAWER_BTN","row","formatSubmissionBytes","BUILT_IN_COVERS","bombermanCover","stopGame","emitGameSelect","GameGrid","onDone","activeGameId","useAppStore","games","useGameCatalog","start","gameId","stop","g","isActive","cover","StopGameButton","MediaDrawer","loading","setLoading","error","setError","setTree","filter","setFilter","addOpen","setAddOpen","mode","setMode","mediaId","canSubmit","canSubmitMedia","buildTree","isLeader","useLeaderAffordance","wasOpen","setWasOpen","t","handleEpisodeSelect","sendLoad","handlePlaylistSelect","playlistId","emitPlaylistLoad","handleUnload","sendHome","toggleSeries","ci","si","c","j","toggleCategory","refreshAll","filteredTree","p","seriesFiltered","eps","ep"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MediaDrawer-BgILxxh4.js","sources":["../../src/lib/catalog.ts","../../src/lib/catalogProbe.ts","../../src/lib/ecchiShows.ts","../../src/lib/mediaTreeExpansion.ts","../../src/lib/catalogTree.ts","../../src/lib/mediaSearchApi.ts","../../src/components/MediaSearchResults.tsx","../../src/components/AddMediaDrawer.tsx","../../src/components/GamePicker.tsx","../../src/components/MediaDrawer.tsx"],"sourcesContent":["import { getRuntimeServerOrigin } from './config';\nimport { isDebugEnabled, debug } from './debug';\n\nexport interface CatalogDir { name: string; path: string; }\nexport interface CatalogFile { id: string; title: string; masterUrl: string; poster: string | null; durationMs: number | null; }\nexport interface CatalogResponse { dirs: CatalogDir[]; files: CatalogFile[]; }\n\n// One entry PER PREFIX. This used to be a single `{prefix, data}` slot, and the\n// media drawer asks for ~18 prefixes per build, so every call evicted the one\n// before it: after a build the slot held only the last prefix, and the next\n// open missed all the others even inside CACHE_MS. A cache that never hits.\nconst cache = new Map<string, { ts: number; data: CatalogResponse }>();\n// Concurrent callers of the same prefix share one request — the drawer's\n// mount prefetch and an early open can overlap.\nconst inflight = new Map<string, Promise<CatalogResponse>>();\nconst CACHE_MS = 60000;\n\nexport async function fetchCatalog(prefix: string = ''): Promise<CatalogResponse> {\n const now = Date.now();\n const hit = cache.get(prefix);\n if (hit && (now - hit.ts) < CACHE_MS) return hit.data;\n const pending = inflight.get(prefix);\n if (pending) return pending;\n\n const request = (async () => {\n const origin = getRuntimeServerOrigin();\n const url = `${origin}/api/catalog${prefix ? `?prefix=${encodeURIComponent(prefix)}` : ''}`;\n const res = await fetch(url, { cache: 'no-store' });\n if (!res.ok) throw new Error(`catalog_http_${res.status}`);\n // The response is not schema-validated (there is no Zod schema for the\n // catalog on either end); this asserts the shape the server documents so the\n // `any` from res.json() does not leak into the cache and the return value.\n const data = (await res.json()) as CatalogResponse;\n if (isDebugEnabled()) debug('catalog', 'Catalog fetched', { prefix, dirs: data.dirs?.length, files: data.files?.length });\n cache.set(prefix, { ts: now, data });\n return data;\n })();\n inflight.set(prefix, request);\n try {\n return await request;\n } finally {\n inflight.delete(prefix);\n }\n}\n\n/** Drop one prefix, or everything when called with no prefix / ''. */\nexport function invalidateCatalog(prefix?: string) {\n if (!prefix) cache.clear();\n else cache.delete(prefix);\n}\n","import { FRAGMENT_MP4_FILENAME } from '@watchparty/shared';\nimport { fetchCatalog } from './catalog';\nimport { isDebugEnabled, debug, warn } from './debug';\n\n// Developer utility (verbose mode) to sanity check media catalog + sample media accessibility.\n// Invoked when MediaDrawer opens in verbose mode.\nexport async function probeMediaEndpoints() {\n if (!isDebugEnabled()) return;\n try {\n const categories = ['output/fmp4/anime','output/fmp4/movie','output/fmp4/tv'];\n for (const cat of categories) {\n const catData = await fetchCatalog(cat);\n debug('catalog', '[probe] catalog ' + cat, { dirs: catData.dirs.length, files: catData.files.length });\n // Try first playable leaf if present\n const candidate = catData.files[0];\n if (candidate) {\n const base = mediaBase();\n const indexUrl = `${base}/${candidate.id}/index.json`.replace(/\\\\/g,'/');\n const mediaUrl = `${base}/${candidate.id}/${FRAGMENT_MP4_FILENAME}`.replace(/\\\\/g,'/');\n await fetchAndLog(indexUrl, 'index');\n // Range fetch first ~1KB\n await fetchAndLog(mediaUrl, 'fragment', { headers: { Range: 'bytes=0-1023' } });\n }\n }\n } catch (err: unknown) {\n warn('catalog', '[probe] error', { error: err instanceof Error ? err.message : String(err) });\n }\n}\n\nfunction mediaBase() {\n return '/media';\n}\n\nasync function fetchAndLog(url: string, label: string, init?: RequestInit) {\n const t0 = performance.now();\n const res = await fetch(url, init);\n const ms = Math.round(performance.now() - t0);\n debug('catalog', '[probe] ' + label, { url, status: res.status, ms, bytes: res.headers.get('content-length') });\n return res;\n}\n","/**\n * Anime series (by output folder name) that are shown in a separate,\n * collapsed-by-default \"Ecchi\" section at the BOTTOM of the media drawer,\n * instead of mixed into the main Anime list.\n *\n * Edit this set to recategorize — matching is case-insensitive and ignores\n * non-alphanumerics, so it tolerates folder-name punctuation/spacing.\n * (WarriorPrincess is intentionally left in the main list — move it here if you\n * consider it ecchi.)\n */\n\nconst ECCHI_SERIES = [\n 'yuuna',\n 'gushingovermagicalgirls',\n 'immoralguild',\n 'ingokudanchi',\n 'isekaionsenparadise',\n];\n\nconst norm = (name: string) => name.toLowerCase().replace(/[^a-z0-9]/g, '');\n\n/**\n * The matcher, over an explicit list. Split out from `isEcchiSeries` so tests\n * can exercise the normalisation rules — which are the part that actually\n * breaks — against a fixed list, independently of what this deployment happens\n * to categorise.\n */\nexport function matchesSeriesList(name: string, series: readonly string[]): boolean {\n return new Set(series.map(norm)).has(norm(name));\n}\n\nexport function isEcchiSeries(name: string): boolean {\n return matchesSeriesList(name, ECCHI_SERIES);\n}\n","/**\n * Carry the user's expand/collapse state across a MediaDrawer refetch.\n *\n * `MediaDrawer` is mounted for the lifetime of the room (`Room.tsx` renders it\n * unconditionally and gates on `open` INSIDE the component, after the hooks),\n * so its `tree` state survives closing the drawer. Every open then refires\n * `buildTree()`, which walks the catalog in ~18 SEQUENTIAL requests — 1.45s\n * measured from the server's own host, several seconds for a viewer over the\n * tunnel.\n *\n * For that whole window the drawer shows the PREVIOUS tree, fully interactive.\n * A user who opens the drawer and expands a series is working on a tree that is\n * about to be thrown away: when the refetch lands, `setTree(result)` replaces it\n * with freshly-built nodes whose `expanded` flags are back at their defaults,\n * and the selection silently collapses a few seconds after opening. It only\n * happens from the second open onward, because the first has no stale tree to\n * show — which is exactly why it reads as intermittent.\n *\n * `mergeExpansion` re-applies the old flags onto the new tree by NAME, so a\n * background refresh can never undo a deliberate interaction. Names are the\n * right key here: they are what the user actually clicked on, they are stable\n * across refetches, and they are unique within their level (a category label, a\n * series directory name). Nodes that are genuinely new keep the defaults\n * `buildTree` gave them — a series added by an ingest since the last open\n * arrives collapsed, and the Ecchi category still starts collapsed the first\n * time it appears.\n */\n\nexport interface ExpandableNode {\n name: string;\n expanded?: boolean;\n}\n\nexport interface ExpandableParent<C extends ExpandableNode> extends ExpandableNode {\n children: C[];\n}\n\n/** Snapshot of which nodes are open, keyed by name at each level. */\nexport type ExpansionState = Map<string, { expanded: boolean; children: Map<string, boolean> }>;\n\n/**\n * Read the current expand/collapse state out of a tree.\n *\n * `childrenOf` returns the nested expandable nodes for a top-level node\n * (series inside a category); levels with no expandable children yield an\n * empty map, which merges as a no-op.\n */\nexport function captureExpansion<T extends ExpandableNode>(\n tree: readonly T[],\n childrenOf: (node: T) => readonly ExpandableNode[]\n): ExpansionState {\n const state: ExpansionState = new Map();\n for (const node of tree) {\n const children = new Map<string, boolean>();\n for (const child of childrenOf(node)) {\n children.set(child.name, child.expanded === true);\n }\n state.set(node.name, { expanded: node.expanded === true, children });\n }\n return state;\n}\n\n/**\n * Re-apply a captured expansion state onto a freshly-built tree.\n *\n * Returns NEW node objects rather than mutating: `tree` is React state, and the\n * caller renders straight off it. A node absent from `state` is untouched, so\n * newly-ingested media keeps whatever default the builder chose.\n */\nexport function mergeExpansion<T extends ExpandableNode>(\n tree: readonly T[],\n state: ExpansionState,\n childrenOf: (node: T) => readonly ExpandableNode[],\n withChildren: (node: T, children: ExpandableNode[]) => T\n): T[] {\n return tree.map((node) => {\n const prev = state.get(node.name);\n if (!prev) return node;\n\n const children = childrenOf(node);\n const mergedChildren = children.map((child) => {\n const wasExpanded = prev.children.get(child.name);\n if (wasExpanded === undefined) return child;\n return { ...child, expanded: wasExpanded };\n });\n\n const merged = withChildren({ ...node, expanded: prev.expanded }, mergedChildren);\n return merged;\n });\n}\n\n/**\n * The one category that starts OPEN when the drawer opens on Watch. Every other\n * top-level node — Playlists, Ecchi, and Movie/TV should they ever hold\n * anything — starts closed, and every series inside starts closed.\n *\n * A name, not a flag on the node: the builder and the reset below must agree\n * on it, and a rule written in two places is how they stop agreeing.\n */\nexport const DEFAULT_OPEN_CATEGORY = 'Anime';\n\nexport function isOpenByDefault(categoryName: string): boolean {\n return categoryName === DEFAULT_OPEN_CATEGORY;\n}\n\n/**\n * Put a tree back to its defaults: {@link DEFAULT_OPEN_CATEGORY} open,\n * everything else closed, every series collapsed.\n *\n * Called when the drawer OPENS, and only then. This is not a change to\n * {@link mergeExpansion}, and the two are not in tension: they answer different\n * moments. Opening the drawer is a fresh start, so it gets the defaults every\n * time rather than whatever state the last visit left behind. A refetch that\n * lands WHILE the drawer is open is not a fresh start, and must still carry the\n * user's clicks over — that is the bug `mergeExpansion` exists for, and it only\n * stays fixed because the reset runs BEFORE the refetch rather than after it:\n * the refetch then merges onto the defaulted tree plus anything clicked since.\n *\n * Returns new objects; `tree` is React state.\n */\nexport function resetToDefaultExpansion<T extends ExpandableNode>(\n tree: readonly T[],\n childrenOf: (node: T) => readonly ExpandableNode[],\n withChildren: (node: T, children: ExpandableNode[]) => T\n): T[] {\n return tree.map((node) =>\n withChildren(\n { ...node, expanded: isOpenByDefault(node.name) },\n childrenOf(node).map((child) => ({ ...child, expanded: false }))\n )\n );\n}\n","/**\n * Builds the media drawer's Watch tree from the catalog API.\n *\n * Every fetch that does not depend on another runs CONCURRENTLY: the category\n * listings at once, then each category's series listings at once — two round\n * trips deep instead of ~18. It used to be a serial `for … await` waterfall,\n * and the server answers each listing in 1-5ms, so the waterfall WAS the load\n * time: ~1.3s measured from the origin box through the edge (~70ms a hop), and\n * a remote viewer pays their own RTT on top of every one of those hops.\n *\n * Order is preserved (Promise.all keeps input order), so the tree reads the\n * same as the serial version. A failed listing drops only that branch.\n */\nimport type { CatalogResponse } from './catalog';\nimport { warn, errorFields } from './debug';\nimport { isEcchiSeries } from './ecchiShows';\nimport { isOpenByDefault } from './mediaTreeExpansion';\n\nexport interface EpisodeNode { type: 'episode'; name: string; mediaId: string; }\nexport interface SeriesNode { type: 'series'; name: string; episodes: EpisodeNode[]; expanded?: boolean; }\nexport interface CategoryNode { type: 'category'; name: string; series: SeriesNode[]; expanded?: boolean; }\nexport interface PlaylistNode { type: 'playlist'; name: string; playlistId: string; }\nexport interface PlaylistCategoryNode { type: 'playlist_category'; name: string; playlists: PlaylistNode[]; expanded?: boolean; }\nexport type MediaTree = (CategoryNode | PlaylistCategoryNode)[];\n\nexport type CatalogFetcher = (prefix: string) => Promise<CatalogResponse>;\n\nconst ROOT = 'output/fmp4';\nconst CATEGORIES: { key: string; label: string }[] = [\n { key: 'anime', label: 'Anime' },\n { key: 'movie', label: 'Movie' },\n { key: 'tv', label: 'TV' },\n];\n\nasync function tryFetch(fetcher: CatalogFetcher, prefix: string): Promise<CatalogResponse | null> {\n try {\n return await fetcher(prefix);\n } catch (err) {\n warn('catalog', 'Fetch failed', { prefix, ...errorFields(err) });\n return null;\n }\n}\n\nexport async function loadCatalogTree(fetcher: CatalogFetcher): Promise<MediaTree> {\n const [categoryListings, playlistListing] = await Promise.all([\n Promise.all(CATEGORIES.map(cat => tryFetch(fetcher, `${ROOT}/${cat.key}`))),\n tryFetch(fetcher, `${ROOT}/playlists`),\n ]);\n\n const seriesPerCategory = await Promise.all(categoryListings.map(listing =>\n Promise.all((listing?.dirs ?? []).map(async (d): Promise<SeriesNode | null> => {\n const series = await tryFetch(fetcher, d.path);\n const episodes: EpisodeNode[] = (series?.files ?? []).map(f => ({ type: 'episode', name: f.title, mediaId: f.id }));\n return episodes.length ? { type: 'series', name: d.name, episodes, expanded: false } : null;\n }))));\n\n const result: MediaTree = [];\n // Ecchi anime is pulled out of the main Anime list and shown in its own\n // collapsed category at the very bottom of the drawer.\n const ecchiSeries: SeriesNode[] = [];\n CATEGORIES.forEach((cat, i) => {\n const seriesNodes: SeriesNode[] = [];\n for (const node of seriesPerCategory[i]) {\n if (!node) continue;\n if (cat.key === 'anime' && isEcchiSeries(node.name)) ecchiSeries.push(node);\n else seriesNodes.push(node);\n }\n if (seriesNodes.length) result.push({ type: 'category', name: cat.label, series: seriesNodes, expanded: isOpenByDefault(cat.label) });\n });\n const playlists: PlaylistNode[] = (playlistListing?.dirs ?? []).map(d => ({ type: 'playlist', name: d.name, playlistId: d.name }));\n if (playlists.length) result.push({ type: 'playlist_category', name: 'Playlists', playlists, expanded: isOpenByDefault('Playlists') });\n // Ecchi category goes LAST. Every default above reads the same rule\n // (`isOpenByDefault`) so the builder and the on-open reset cannot drift.\n if (ecchiSeries.length) result.push({ type: 'category', name: 'Ecchi', series: ecchiSeries, expanded: isOpenByDefault('Ecchi') });\n return result;\n}\n","/**\n * Search in the Add media drawer: shows (to add to the scraper's watch list)\n * and YouTube videos (to submit as a link).\n *\n * The drawer's one field takes a link, a magnet, or words. Words search;\n * anything with a scheme keeps the existing submit path — `isSearchText` is\n * that split, and it is pure so it is tested rather than living in the\n * component.\n *\n * The add sends `{source, id}` and nothing else: the server re-reads the show\n * and derives every name it forwards (see `apps/server/src/http/routes/search.ts`).\n */\nimport {\n SEARCH_MAX_QUERY,\n SEARCH_MIN_QUERY,\n type TShowResult,\n type TShowSearchOut,\n type TShowSource,\n type TVideoResult,\n type TVideoSearchOut,\n type TWatchlistAddOut,\n} from '@watchparty/shared';\nimport { getRuntimeServerOrigin } from './config';\nimport { debug, warn, errorFields } from './debug';\n\n/** The two schemes the submit path accepts mean a link, never a search. Not\n * any scheme: `Re:Zero` is a title. */\nconst HAS_SCHEME = /^(?:https?|magnet):/i;\n\nexport function isSearchText(raw: string): boolean {\n const text = raw.trim();\n return text.length >= SEARCH_MIN_QUERY && text.length <= SEARCH_MAX_QUERY && !HAS_SCHEME.test(text);\n}\n\n/** Server-relative proxy path → absolute URL on this app's server. */\nexport function resolveSearchImage(pathOrNull: string | null): string | null {\n return pathOrNull ? `${getRuntimeServerOrigin()}${pathOrNull}` : null;\n}\n\n/** `204` → `3:24`, `3725` → `1:02:05`. */\nexport function formatDuration(totalS: number | null): string {\n if (totalS === null || !Number.isFinite(totalS) || totalS < 0) return '';\n const s = Math.floor(totalS % 60);\n const m = Math.floor(totalS / 60) % 60;\n const h = Math.floor(totalS / 3600);\n const ss = String(s).padStart(2, '0');\n return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${ss}` : `${m}:${ss}`;\n}\n\nexport type SearchOutcome<T> = { ok: true; value: T } | { ok: false; status: number | 'network' | 'aborted' };\n\nasync function getJson<T>(path: string, signal: AbortSignal): Promise<SearchOutcome<T>> {\n try {\n const res = await fetch(`${getRuntimeServerOrigin()}${path}`, { signal });\n if (!res.ok) return { ok: false, status: res.status };\n return { ok: true, value: await res.json() as T };\n } catch (err: unknown) {\n if (signal.aborted) return { ok: false, status: 'aborted' };\n warn('search', 'Search request threw', errorFields(err));\n return { ok: false, status: 'network' };\n }\n}\n\nexport function searchShows(q: string, signal: AbortSignal): Promise<SearchOutcome<TShowSearchOut>> {\n return getJson(`/api/search/shows?q=${encodeURIComponent(q.trim())}`, signal);\n}\n\nexport function searchVideos(q: string, signal: AbortSignal): Promise<SearchOutcome<TVideoSearchOut>> {\n return getJson(`/api/search/youtube?q=${encodeURIComponent(q.trim())}`, signal);\n}\n\nexport type WatchlistAddOutcome =\n | { ok: true; value: TWatchlistAddOut }\n /** Already on the list — as good as added, from the user's side. */\n | { ok: false; code: 'exists' | 'failed'; status: number | 'network' };\n\nexport async function addShowToWatchlist(source: TShowSource, id: string): Promise<WatchlistAddOutcome> {\n try {\n const res = await fetch(`${getRuntimeServerOrigin()}/api/watchlist`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ source, id }),\n });\n if (res.status === 201) {\n const value = await res.json() as TWatchlistAddOut;\n debug('search', 'Added to watch list', { source, id, series: value.series });\n return { ok: true, value };\n }\n return { ok: false, code: res.status === 409 ? 'exists' : 'failed', status: res.status };\n } catch (err: unknown) {\n warn('search', 'Watch-list add threw', errorFields(err));\n return { ok: false, code: 'failed', status: 'network' };\n }\n}\n\nexport type { TShowResult, TVideoResult };\n","/**\n * Search results under the Add media field: shows and YouTube videos.\n *\n * A show's `+` adds it to the scraper's watch list, so its episodes arrive on\n * their own; a video's `+` submits its link exactly as pasting it would (a\n * `pending` request). Both searches run in parallel, each landing when it\n * lands — the show lookup answers in a few hundred ms, yt-dlp in a second or\n * so, and neither waits for the other.\n *\n * Results are stored WITH the query they answer and shown only while that is\n * still the query, so typing on never flashes the previous word's results\n * and no effect has to clear state synchronously.\n */\nimport { useEffect, useState } from 'react';\nimport { Icon } from '@wolffm/themes';\nimport type { TSubmission } from '@watchparty/shared';\nimport {\n addShowToWatchlist,\n formatDuration,\n isSearchText,\n resolveSearchImage,\n searchShows,\n searchVideos,\n type SearchOutcome,\n type TShowResult,\n type TVideoResult,\n} from '../lib/mediaSearchApi';\nimport { submissionFailureLabel, submitSubmission } from '../lib/submissionsApi';\n\n/** Pause after the last keystroke before searching. */\nconst DEBOUNCE_MS = 350;\n\ntype Answer<T> = { q: string; outcome: SearchOutcome<T[]> };\ntype RowState = 'adding' | 'added' | string;\n\ninterface Props {\n query: string;\n onSubmitted: (row: TSubmission) => void;\n}\n\nexport default function MediaSearchResults({ query, onSubmitted }: Props) {\n const q = query.trim();\n const active = isSearchText(q);\n const [shows, setShows] = useState<Answer<TShowResult> | null>(null);\n const [videos, setVideos] = useState<Answer<TVideoResult> | null>(null);\n const [rowState, setRowState] = useState<Record<string, RowState>>({});\n\n useEffect(() => {\n if (!active) return;\n const ctrl = new AbortController();\n const timer = setTimeout(() => {\n void searchShows(q, ctrl.signal).then(r => {\n if (ctrl.signal.aborted) return;\n setShows({ q, outcome: r.ok ? { ok: true, value: r.value.results } : r });\n });\n void searchVideos(q, ctrl.signal).then(r => {\n if (ctrl.signal.aborted) return;\n setVideos({ q, outcome: r.ok ? { ok: true, value: r.value.results } : r });\n });\n }, DEBOUNCE_MS);\n return () => { clearTimeout(timer); ctrl.abort(); };\n }, [q, active]);\n\n if (!active) return null;\n\n const showAnswer = shows?.q === q ? shows.outcome : null;\n const videoAnswer = videos?.q === q ? videos.outcome : null;\n\n async function addShow(show: TShowResult) {\n const key = `${show.source}:${show.id}`;\n setRowState(s => ({ ...s, [key]: 'adding' }));\n const r = await addShowToWatchlist(show.source, show.id);\n setRowState(s => ({ ...s, [key]: r.ok || r.code === 'exists' ? 'added' : `error ${r.status}` }));\n }\n\n async function addVideo(video: TVideoResult) {\n const key = `yt:${video.id}`;\n setRowState(s => ({ ...s, [key]: 'adding' }));\n const r = await submitSubmission(video.url, video.title);\n if (r.ok) {\n setRowState(s => ({ ...s, [key]: 'added' }));\n onSubmitted(r.value);\n } else {\n setRowState(s => ({ ...s, [key]: submissionFailureLabel(r.failure) }));\n }\n }\n\n const bothEmpty = showAnswer?.ok && videoAnswer?.ok\n && showAnswer.value.length === 0 && videoAnswer.value.length === 0;\n\n return (\n <div className=\"border-b border-border\" data-testid=\"media-search\">\n {bothEmpty && <div className=\"px-3 py-2 text-[11px] text-text-muted\">no results</div>}\n <Section label=\"Shows\" answer={showAnswer} testId=\"search-shows\">\n {showAnswer?.ok && showAnswer.value.map(show => {\n const key = `${show.source}:${show.id}`;\n const state = show.onWatchlist ? 'added' : rowState[key];\n return (\n <ResultRow\n key={key}\n testId={`show-${key}`}\n image={resolveSearchImage(show.poster)}\n imageClass=\"w-8 h-11\"\n title={show.title}\n meta={[show.year, show.format, show.episodes ? `${show.episodes} ep` : null]}\n state={state}\n canAdd={show.addable}\n addLabel={`Add ${show.title} to watch list`}\n onAdd={() => { void addShow(show); }}\n />\n );\n })}\n </Section>\n <Section label=\"YouTube\" answer={videoAnswer} testId=\"search-youtube\">\n {videoAnswer?.ok && videoAnswer.value.map(video => (\n <ResultRow\n key={video.id}\n testId={`video-${video.id}`}\n image={resolveSearchImage(video.thumb)}\n imageClass=\"w-16 h-9\"\n title={video.title}\n meta={[video.channel, formatDuration(video.durationS)]}\n state={rowState[`yt:${video.id}`]}\n canAdd\n addLabel={`Request ${video.title}`}\n onAdd={() => { void addVideo(video); }}\n />\n ))}\n </Section>\n </div>\n );\n}\n\nfunction Section<T>({ label, answer, testId, children }: {\n label: string;\n answer: SearchOutcome<T[]> | null;\n testId: string;\n children: React.ReactNode;\n}) {\n if (answer?.ok && answer.value.length === 0) return null;\n const failed = answer && !answer.ok && answer.status !== 'aborted';\n return (\n <div data-testid={testId} data-loaded={answer ? 'true' : 'false'}>\n <div className=\"px-3 pt-2 pb-1 flex items-center gap-2 text-[10px] uppercase tracking-wider text-text-muted\">\n <span>{label}</span>\n {!answer && <span className=\"text-text-tertiary\">…</span>}\n {failed && <span className=\"bg-danger-bg text-on-danger-bg px-1 rounded normal-case\">unavailable</span>}\n </div>\n {children}\n </div>\n );\n}\n\nfunction ResultRow({ testId, image, imageClass, title, meta, state, canAdd, addLabel, onAdd }: {\n testId: string;\n image: string | null;\n imageClass: string;\n title: string;\n meta: Array<string | number | null>;\n state: RowState | undefined;\n canAdd: boolean;\n addLabel: string;\n onAdd: () => void;\n}) {\n const metaText = meta.filter(m => m !== null && m !== '').join(' · ');\n return (\n <div className=\"px-3 py-1 flex items-center gap-2\" data-testid={testId} data-state={state ?? ''}>\n {image\n ? <img src={image} alt=\"\" loading=\"lazy\" className={`${imageClass} object-cover rounded shrink-0 bg-bg-alt`} />\n : <div className={`${imageClass} rounded shrink-0 bg-bg-alt`} />}\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[11px] text-text truncate\" title={title}>{title}</div>\n {metaText && <div className=\"text-[10px] text-text-muted truncate\">{metaText}</div>}\n </div>\n {state === 'added'\n ? <Icon name=\"check\" variant=\"tint\" family=\"success\" round className=\"shrink-0\" />\n : state && state !== 'adding'\n ? <span className=\"text-[10px] bg-danger-bg text-on-danger-bg px-1 rounded shrink-0\">{state}</span>\n : canAdd && (\n <button\n onClick={onAdd}\n disabled={state === 'adding'}\n aria-label={addLabel}\n className=\"drawer-btn shrink-0\"\n >\n <Icon name=\"plus\" />\n </button>\n )}\n </div>\n );\n}\n","/**\n * Ask for media — a link, a magnet, or a file — and the state of everything\n * you have asked for.\n *\n * The first user-facing surface of the submissions programme. Moderation is\n * approve-first, so nothing typed here reaches the catalog on its own: a row\n * lands in `pending` and an operator moves it. That is why this drawer shows\n * the caller's OWN queue rather than a confirmation toast — the honest answer\n * to \"did that work?\" is the row's state, and it keeps changing after the\n * click.\n *\n * ── Three ways in, one field each ──────────────────────────────────────────\n * The link field takes words as well: anything without a scheme searches\n * (`MediaSearchResults` — shows to add to the scraper's watch list, YouTube\n * videos to submit as links), and Submit stands down while it does.\n * Otherwise it takes a URL or a magnet (kind is read off the scheme —\n * `guessSubmissionKind` — never chosen from a select). Reading as a magnet\n * reveals two more fields: `series` (the scraper's destination folder — an\n * anime series already in the catalog is offered as a suggestion, a new name\n * is accepted) and a title, PRE-FILLED from the magnet's `dn=` so a caller\n * who already typed a well-formed link rarely has to type anything twice.\n * Both are sent to the server and are a COURTESY only — `checkSource('magnet',\n * …)` is the one place either requirement is enforced; see the wave brief's\n * mutation check (a).\n *\n * A file goes a different way entirely: it chunk-uploads straight to\n * filetransfer (own protocol, verified against that service's own browser\n * client — see `lib/submissionsApi.ts`), addressed to watchparty's own\n * identity, and only once that lands does a submission get recorded — with\n * the filetransfer file id as `source`. The 4 GB cap is shown and enforced\n * client-side BEFORE the upload starts, as a courtesy; the authoritative\n * check is server-side, on bytes actually received (wave 4, lane A).\n *\n * ── Through DrawerShell, not a third backdrop ──────────────────────────────\n * MediaDrawer and the old GameDrawer each hand-copied the backdrop/panel/\n * Escape chrome and had already drifted (only one closed on a backdrop\n * click). The shell exists so a third drawer is not a third copy.\n *\n * ── Buttons carry a real class ─────────────────────────────────────────────\n * `styles.css` has an unlayered `button { background: none; color: inherit;\n * border: none }` reset, and unlayered CSS beats anything in a `@layer`\n * regardless of specificity — so every `bg-*`, `text-*` and `border-*` utility\n * on a `<button>` is silently discarded. `.drawer-btn` and its modifiers are\n * real class selectors and outrank the reset.\n *\n * ── The list polls while open ──────────────────────────────────────────────\n * A submission's state is moved by an operator and then by a server-side\n * runner; nothing pushes it to this client. The poll is scoped to the drawer\n * being open and torn down with it.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { Icon } from '@wolffm/themes';\nimport type { TSubmission, TSubmissionState } from '@watchparty/shared';\nimport DrawerShell, { DRAWER_BTN } from './DrawerShell';\nimport { fetchCatalog } from '../lib/catalog';\nimport {\n extractMagnetDisplayName,\n fetchMySubmissions,\n fetchUploadTarget,\n guessSubmissionKind,\n hasEpisodeCode,\n submissionFailureLabel,\n submitMagnetSubmission,\n submitSubmission,\n submitUploadSubmission,\n uploadFailureLabel,\n uploadFileForSubmission,\n formatSubmissionBytes,\n type SubmissionFailure,\n type UploadFailure,\n type UploadPhase,\n type UploadTarget,\n} from '../lib/submissionsApi';\nimport { isSearchText } from '../lib/mediaSearchApi';\nimport { debug, errorFields } from '../lib/debug';\nimport MediaSearchResults from './MediaSearchResults';\n\ninterface AddMediaDrawerProps { open: boolean; onClose: () => void; }\n\n/** How often the caller's own queue is re-read while this drawer is open. */\nconst POLL_MS = 4000;\n\n/**\n * State → colour. Terminal-good is success, terminal-bad is danger, and\n * everything in flight is the primary accent — the three groups a user\n * actually distinguishes.\n */\nconst STATE_CLASS: Record<TSubmissionState, string> = {\n pending: 'bg-neutral-bg text-on-neutral-bg',\n approved: 'bg-primary-bg text-on-primary-bg',\n fetching: 'bg-primary-bg text-on-primary-bg',\n ingesting: 'bg-primary-bg text-on-primary-bg',\n ready: 'bg-success-bg text-on-success-bg',\n rejected: 'bg-danger-bg text-on-danger-bg',\n failed: 'bg-danger-bg text-on-danger-bg',\n};\n\n/** `TSubmission` as read today, plus the field `series` adds once the shared\n * schema carries it (packages/shared/src/submissions.ts). A cast rather than\n * a shared-type edit — this file does not own that schema — and harmless\n * once the real field lands: the shapes agree. */\ntype SubmissionWithSeries = TSubmission & { series?: string };\n\nfunction SubmissionRow({ item }: { item: TSubmission }) {\n const withSeries = item as SubmissionWithSeries;\n const label = item.title || item.source;\n return (\n <div\n className=\"px-3 py-1.5 border-b border-border/50 flex items-center gap-2\"\n data-submission-id={item.id}\n data-submission-state={item.state}\n data-submission-kind={item.kind}\n data-submission-series={withSeries.series ?? ''}\n >\n <span className=\"text-[10px] uppercase tracking-wider text-text-muted shrink-0\">{item.kind}</span>\n <span className=\"text-[11px] text-text-tertiary truncate flex-1\" title={item.source}>\n {withSeries.series ? `${withSeries.series} — ${label}` : label}\n </span>\n <span className={`text-[10px] uppercase tracking-wider px-1 rounded ${STATE_CLASS[item.state] ?? 'text-text-muted'}`}>\n {item.state}\n </span>\n {item.error && <span className=\"text-[10px] bg-danger-bg text-on-danger-bg px-1 rounded truncate max-w-[10rem]\">{item.error}</span>}\n </div>\n );\n}\n\nexport default function AddMediaDrawer({ open, onClose }: AddMediaDrawerProps) {\n const [source, setSource] = useState('');\n const [series, setSeries] = useState('');\n const [title, setTitle] = useState('');\n const [titleTouched, setTitleTouched] = useState(false);\n const [seriesOptions, setSeriesOptions] = useState<string[]>([]);\n const [busy, setBusy] = useState(false);\n const [failure, setFailure] = useState<SubmissionFailure | null>(null);\n const [rows, setRows] = useState<TSubmission[] | null>(null);\n const [listFailure, setListFailure] = useState<SubmissionFailure | null>(null);\n\n const [uploadTarget, setUploadTarget] = useState<UploadTarget | null>(null);\n const [selectedFile, setSelectedFile] = useState<File | null>(null);\n const [uploadBusy, setUploadBusy] = useState(false);\n const [uploadProgress, setUploadProgress] = useState<{ sent: number; total: number; phase: UploadPhase } | null>(null);\n const [uploadFailure, setUploadFailure] = useState<UploadFailure | SubmissionFailure | null>(null);\n const fileInputRef = useRef<HTMLInputElement | null>(null);\n\n // A poll that resolves after the drawer closed must not resurrect its state.\n const liveRef = useRef(false);\n\n const kind = useMemo(() => guessSubmissionKind(source), [source]);\n const isMagnet = kind === 'magnet' && source.trim().length > 0;\n const searching = isSearchText(source);\n\n const refresh = useCallback(() => fetchMySubmissions().then(res => {\n if (!liveRef.current) return;\n if (res.ok) { setRows(res.value); setListFailure(null); }\n else { setListFailure(res.failure); }\n }), []);\n\n useEffect(() => {\n if (!open) return;\n liveRef.current = true;\n void refresh();\n const timer = setInterval(() => { void refresh(); }, POLL_MS);\n return () => { liveRef.current = false; clearInterval(timer); };\n }, [open, refresh]);\n\n // The upload target (recipient + cap) is fetched once per open, ahead of any\n // file being picked, so the cap is on screen before the user chooses a file\n // rather than only after they commit to uploading one.\n useEffect(() => {\n if (!open) return;\n let live = true;\n void fetchUploadTarget().then(res => { if (live && res.ok) setUploadTarget(res.value); });\n return () => { live = false; };\n }, [open]);\n\n // Existing anime series, as suggestions only — a new name is accepted.\n useEffect(() => {\n if (!open) return;\n let live = true;\n // The anime SERIES live at output/fmp4/anime. This read 'anime', a prefix\n // that does not exist, so the suggestion list was always empty.\n void fetchCatalog('output/fmp4/anime').then(cat => {\n if (live) setSeriesOptions(cat.dirs.map(d => d.name));\n }).catch((err: unknown) => {\n // Suggestions are a nicety: their absence changes nothing but this line.\n debug('media', 'series suggestions unavailable', errorFields(err));\n });\n return () => { live = false; };\n }, [open]);\n\n /**\n * The link field's `onChange`. Prefilling the title from a magnet's `dn=`\n * lives HERE, in the event handler that changes `source`, rather than in a\n * `useEffect` watching it — a `useEffect` that calls `setState` in its body\n * (as opposed to synchronizing with something external) is exactly the\n * pattern `react-hooks/set-state-in-effect` flags, and the derivation is\n * already keyed on the very event that should trigger it, so there is\n * nothing left for an effect to \"synchronize\". Never clobbers a manual\n * title edit — a caller who already typed their own title keeps it even if\n * the source field changes afterward.\n */\n function handleSourceChange(value: string) {\n setSource(value);\n const nowMagnet = guessSubmissionKind(value) === 'magnet' && value.trim().length > 0;\n if (nowMagnet && !titleTouched) {\n const dn = extractMagnetDisplayName(value);\n if (dn) setTitle(dn);\n }\n }\n\n function resetLinkFields() {\n setSource('');\n setSeries('');\n setTitle('');\n setTitleTouched(false);\n }\n\n const magnetReady = !isMagnet || (series.trim().length > 0 && hasEpisodeCode(title));\n\n async function handleSubmit() {\n const trimmed = source.trim();\n if (!trimmed || busy || searching) return;\n if (isMagnet && !magnetReady) return;\n setBusy(true);\n setFailure(null);\n const res = isMagnet\n ? await submitMagnetSubmission(trimmed, { title: title.trim() || undefined, series: series.trim() })\n : await submitSubmission(trimmed);\n setBusy(false);\n if (!res.ok) { setFailure(res.failure); return; }\n debug('submissions', 'Submit accepted', { id: res.value.id, state: res.value.state });\n resetLinkFields();\n // Show the new row at once rather than waiting out a poll tick: the\n // response already carries it, and a field that empties with nothing new\n // beneath it reads as a click that did nothing.\n setRows(prev => [res.value, ...(prev ?? []).filter(r => r.id !== res.value.id)]);\n void refresh();\n }\n\n function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {\n const file = e.target.files?.[0] ?? null;\n setUploadFailure(null);\n if (file && uploadTarget && file.size > uploadTarget.maxBytes) {\n setSelectedFile(null);\n setUploadFailure({ code: 'too_large', maxBytes: uploadTarget.maxBytes });\n if (fileInputRef.current) fileInputRef.current.value = '';\n return;\n }\n setSelectedFile(file);\n }\n\n async function handleUpload() {\n if (!selectedFile || uploadBusy) return;\n setUploadBusy(true);\n setUploadFailure(null);\n\n let target = uploadTarget;\n if (!target) {\n const res = await fetchUploadTarget();\n if (!res.ok) { setUploadBusy(false); setUploadFailure(res.failure); return; }\n target = res.value;\n setUploadTarget(target);\n }\n if (selectedFile.size > target.maxBytes) {\n setUploadBusy(false);\n setUploadFailure({ code: 'too_large', maxBytes: target.maxBytes });\n return;\n }\n\n const uploaded = await uploadFileForSubmission(selectedFile, target, (sent, total, phase) => {\n setUploadProgress({ sent, total, phase });\n });\n if (!uploaded.ok) {\n setUploadBusy(false);\n setUploadProgress(null);\n setUploadFailure(uploaded.failure);\n return;\n }\n\n const recorded = await submitUploadSubmission(uploaded.value.fileId, selectedFile.name);\n setUploadBusy(false);\n setUploadProgress(null);\n if (!recorded.ok) { setUploadFailure(recorded.failure); return; }\n\n debug('submissions', 'Upload accepted', { id: recorded.value.id });\n setSelectedFile(null);\n if (fileInputRef.current) fileInputRef.current.value = '';\n setRows(prev => [recorded.value, ...(prev ?? []).filter(r => r.id !== recorded.value.id)]);\n void refresh();\n }\n\n const uploadStatus = uploadFailure\n ? ('maxBytes' in uploadFailure || uploadFailure.code === 'target_unavailable' || uploadFailure.code === 'upload_failed'\n ? uploadFailureLabel(uploadFailure as UploadFailure)\n : submissionFailureLabel(uploadFailure as SubmissionFailure))\n : null;\n\n return (\n <DrawerShell\n open={open}\n onClose={onClose}\n header={\n <>\n <span className=\"text-xs uppercase tracking-wider text-text-muted\">Add media</span>\n <button\n onClick={() => { void refresh(); }}\n aria-label=\"Refresh submissions\"\n className={`${DRAWER_BTN} ml-auto`}\n >\n <Icon name=\"refresh\" />\n </button>\n </>\n }\n >\n <div className=\"p-3 border-b border-border flex flex-col gap-2\">\n <input\n value={source}\n onChange={e => handleSourceChange(e.target.value)}\n onKeyDown={e => { if (e.key === 'Enter' && !isMagnet) void handleSubmit(); }}\n aria-label=\"Media link\"\n placeholder=\"search, https:// or magnet:\"\n className=\"w-full bg-bg-alt border border-border focus:border-border-light outline-none px-2 py-1 rounded text-xs\"\n />\n {isMagnet && (\n <div className=\"flex flex-col gap-1.5\" data-testid=\"magnet-fields\">\n <input\n value={series}\n onChange={e => setSeries(e.target.value)}\n list=\"add-media-series-options\"\n aria-label=\"Series\"\n placeholder=\"series\"\n className=\"w-full bg-bg-alt border border-border focus:border-border-light outline-none px-2 py-1 rounded text-xs\"\n />\n <datalist id=\"add-media-series-options\">\n {seriesOptions.map(name => <option key={name} value={name} />)}\n </datalist>\n <input\n value={title}\n onChange={e => { setTitleTouched(true); setTitle(e.target.value); }}\n aria-label=\"Title\"\n placeholder=\"title (SxxExx)\"\n className=\"w-full bg-bg-alt border border-border focus:border-border-light outline-none px-2 py-1 rounded text-xs\"\n />\n </div>\n )}\n <div className=\"flex items-center gap-2\">\n <button\n onClick={() => { void handleSubmit(); }}\n disabled={busy || !source.trim() || searching || (isMagnet && !magnetReady)}\n aria-label=\"Submit media\"\n className=\"drawer-btn drawer-btn-primary\"\n >\n {busy ? 'Submitting…' : 'Submit'}\n </button>\n {failure && (\n <span data-testid=\"submit-failure\" className=\"text-[11px] bg-danger-bg text-on-danger-bg px-1.5 py-0.5 rounded\">\n {submissionFailureLabel(failure)}\n </span>\n )}\n </div>\n </div>\n\n <MediaSearchResults\n query={source}\n onSubmitted={row => setRows(prev => [row, ...(prev ?? []).filter(r => r.id !== row.id)])}\n />\n\n <div className=\"p-3 border-b border-border flex flex-col gap-2\">\n <div className=\"flex items-center gap-2\">\n <input\n ref={fileInputRef}\n type=\"file\"\n onChange={handleFileChange}\n aria-label=\"Upload file\"\n className=\"text-[11px] flex-1 min-w-0\"\n />\n <button\n onClick={() => { void handleUpload(); }}\n disabled={!selectedFile || uploadBusy}\n aria-label=\"Upload\"\n className=\"drawer-btn drawer-btn-primary\"\n >\n {uploadBusy\n ? (uploadProgress?.phase === 'assembling' ? 'Assembling…' : 'Uploading…')\n : 'Upload'}\n </button>\n </div>\n <span className=\"text-[10px] text-text-muted\" data-testid=\"upload-cap\">\n {uploadTarget ? `cap ${formatSubmissionBytes(uploadTarget.maxBytes)}` : ''}\n </span>\n {uploadStatus && (\n <span data-testid=\"upload-failure\" className=\"text-[11px] bg-danger-bg text-on-danger-bg px-1.5 py-0.5 rounded\">{uploadStatus}</span>\n )}\n </div>\n\n <div className=\"flex-1 overflow-auto\" data-testid=\"my-submissions\">\n {listFailure && (\n <div className=\"mx-3 my-2 px-3 py-2 text-[11px] bg-danger-bg text-on-danger-bg rounded\">{submissionFailureLabel(listFailure)}</div>\n )}\n {rows?.map(item => <SubmissionRow key={item.id} item={item} />)}\n </div>\n </DrawerShell>\n );\n}\n","import { useAppStore } from '../lib/store';\nimport { emitGameSelect } from '../lib/socket';\nimport { debug } from '../lib/debug';\nimport { useGameCatalog } from '../lib/gameCatalog';\nimport bombermanCover from '../assets/games/bomberman/cover.webp';\n\n// The games themselves are DATA — the server's GAMES config, via\n// lib/gameCatalog.ts. A new game brings its own `cover` URL; this is only the\n// built-in game's art, which ships inside this bundle.\nconst BUILT_IN_COVERS: Record<string, string> = { bomberman: bombermanCover };\n\nfunction stopGame() {\n emitGameSelect(null);\n debug('game', 'game_select', { gameId: null });\n}\n\n/**\n * The media drawer's \"Play\" tab: pick a game to turn on for the whole room\n * (broadcast via game_select → every client's idle screen mounts it), or stop\n * the active one.\n *\n * This used to be its own drawer behind its own toolbar button. It is content\n * now, rendered inside `MediaDrawer` beside \"Watch\", so the leader has ONE\n * place to decide what the room does next rather than two buttons that each\n * open a drawer. Like the drawer it lives in, it is only reachable by a leader\n * — the Select Media entry point is leader-gated — so it carries no gate of\n * its own.\n */\nexport function GameGrid({ onDone }: { onDone: () => void }) {\n const activeGameId = useAppStore(s => s.activeGameId);\n const games = useGameCatalog();\n\n function start(gameId: string) {\n emitGameSelect(gameId);\n debug('game', 'game_select', { gameId });\n onDone();\n }\n function stop() {\n stopGame();\n onDone();\n }\n\n return (\n <div className=\"flex-1 overflow-auto p-3\">\n <div className=\"grid grid-cols-2 gap-3\">\n {games.map(g => {\n const isActive = activeGameId === g.id;\n const cover = g.cover ?? BUILT_IN_COVERS[g.id];\n return (\n <button\n key={g.id}\n onClick={() => (isActive ? stop() : start(g.id))}\n className={`group relative aspect-square overflow-hidden rounded-lg border-2 transition-all cursor-pointer ${\n isActive\n ? 'border-primary shadow-lg shadow-primary/30'\n : 'border-border hover:border-primary'\n }`}\n title={isActive ? `Stop ${g.name}` : `Start ${g.name}`}\n >\n {cover ? (\n <img\n src={cover}\n alt={g.name}\n className=\"absolute inset-0 w-full h-full object-cover transition-transform group-hover:scale-105\"\n />\n ) : (\n <div className=\"absolute inset-0 bg-bg-alt\" />\n )}\n {/* Bottom-up gradient for the name label so it stays readable on any cover. */}\n <div className=\"absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/85 via-black/40 to-transparent px-2 pt-6 pb-2\">\n <div className=\"text-sm font-medium text-white leading-tight\">{g.name}</div>\n </div>\n {isActive && (\n <span className=\"absolute top-2 left-2 px-1.5 py-0.5 bg-primary text-[10px] uppercase tracking-wide rounded text-white\">\n Active\n </span>\n )}\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n\n/** Header control for the Play tab; renders nothing when no game is running. */\nexport function StopGameButton({ onDone }: { onDone: () => void }) {\n const activeGameId = useAppStore(s => s.activeGameId);\n if (!activeGameId) return null;\n return (\n <button onClick={() => { stopGame(); onDone(); }} className=\"drawer-btn drawer-btn-danger ml-auto\">\n Stop game\n </button>\n );\n}\n","import { useCallback, useEffect, useMemo, useState } from 'react';\nimport { fetchCatalog, invalidateCatalog } from '../lib/catalog';\nimport { sendLoad, sendHome } from '../lib/controls';\nimport { isDebugEnabled, debug, warn, errorFields } from '../lib/debug';\nimport { probeMediaEndpoints } from '../lib/catalogProbe';\nimport { loadCatalogTree, type CategoryNode, type MediaTree, type PlaylistCategoryNode, type SeriesNode } from '../lib/catalogTree';\nimport { emitPlaylistLoad } from '../lib/socket';\nimport { captureExpansion, mergeExpansion, resetToDefaultExpansion } from '../lib/mediaTreeExpansion';\nimport { useLeaderAffordance } from '../hooks/useStoreSelectors';\nimport { useAppStore } from '../lib/store';\nimport { canSubmitMedia } from '../lib/submissionsApi';\nimport AddMediaDrawer from './AddMediaDrawer';\nimport DrawerShell, { DRAWER_BTN } from './DrawerShell';\nimport { GameGrid, StopGameButton } from './GamePicker';\n\n/** What the room does next: put something on (Watch) or start a waiting-room game (Play). */\ntype DrawerMode = 'watch' | 'play';\n\ninterface MediaDrawerProps { open: boolean; onClose: () => void; }\n\n// A category's expandable children are its series; a playlist category's\n// entries are leaves, so it contributes nothing to merge and is left alone.\nconst childrenOf = (node: CategoryNode | PlaylistCategoryNode) =>\n node.type === 'category' ? node.series : [];\nconst withChildren = (\n node: CategoryNode | PlaylistCategoryNode,\n children: { name: string; expanded?: boolean }[]\n) => (node.type === 'category' ? { ...node, series: children as SeriesNode[] } : node);\n\nexport default function MediaDrawer({ open, onClose }: MediaDrawerProps) {\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [tree, setTree] = useState<MediaTree>([]);\n const [filter, setFilter] = useState('');\n // The submit surface is a SECOND drawer, not a section of this one: it owns\n // a field, a queue and a poll, and folding it in here would put all three\n // behind the catalog fetches this drawer performs on open.\n // Only one is rendered at a time (see `open={open && !addOpen}` below), so\n // there is never a second backdrop or a second Escape handler in play.\n const [addOpen, setAddOpen] = useState(false);\n const [mode, setMode] = useState<DrawerMode>('watch');\n const mediaId = useAppStore(s => s.snapshot.mediaId);\n // Friend tier and above — the same boundary `POST /api/submissions` enforces.\n // Read once per render rather than stored: auth is module state set from\n // mount props before this component ever mounts.\n const canSubmit = canSubmitMedia();\n\n // Callers raise `loading` and clear `error` first; this only lands the result.\n // Never rejects: every failure becomes `error`.\n const buildTree = useCallback(() => loadCatalogTree(fetchCatalog)\n .then(result => {\n // The drawer stays mounted, so it renders the PREVIOUS tree while this\n // rebuild is in flight. Anything the user expanded in that window is on\n // a tree about to be replaced — carry those flags over, or a background\n // refresh silently collapses a deliberate click right after the drawer\n // opens. Uses the functional form so it merges against the tree as it is\n // AT THAT MOMENT, including expansions made while the fetch was in flight.\n setTree(prev => mergeExpansion(result, captureExpansion(prev, childrenOf), childrenOf, withChildren));\n // The probe logs its own failures.\n if (isDebugEnabled()) void probeMediaEndpoints();\n })\n .catch((e: unknown) => {\n setError((e instanceof Error && e.message) || 'catalog_error');\n })\n .finally(() => setLoading(false)), []);\n\n // Build the tree when the room loads, not when the drawer first opens. This\n // component is mounted (closed) for the life of the room, so without this the\n // leader's first click paid for the whole catalog. Only a leader can open the\n // drawer, so nobody else fetches it. The JSON is a few KB — the tierlist's\n // \"fetch the board on MOUNT\" trade, not its \"warm images on intent\" one.\n // State is set only when the data lands (never synchronously in the effect\n // body), and with no loading flag: nobody is looking at the drawer yet.\n const isLeader = useLeaderAffordance();\n useEffect(() => {\n if (!isLeader) return;\n let live = true;\n loadCatalogTree(fetchCatalog).then(\n result => {\n if (live) setTree(prev => mergeExpansion(result, captureExpansion(prev, childrenOf), childrenOf, withChildren));\n },\n (err: unknown) => warn('catalog', 'Catalog prefetch failed', errorFields(err)),\n );\n return () => { live = false; };\n }, [isLeader]);\n\n // Every open is a fresh start on Watch, with only Anime expanded — not a\n // resumption of whatever the last visit left open. The reset MUST run before\n // the rebuild: `buildTree` merges its result onto the tree as it stands when\n // the fetch lands, so resetting first means a series the user expands during\n // that fetch window still survives, while everything untouched shows\n // the defaults. Resetting after would discard that click. Pinned in\n // mediaTreeExpansion.test.ts. The mount prefetch means the tree is already\n // on screen; this rebuild refreshes it behind the user (a cache hit within\n // lib/catalog.ts's CACHE_MS, a background refetch after).\n const [wasOpen, setWasOpen] = useState(false);\n if (open !== wasOpen) {\n setWasOpen(open);\n if (open) {\n setMode('watch');\n setTree(t => resetToDefaultExpansion(t, childrenOf, withChildren));\n setLoading(true); setError(null);\n }\n }\n useEffect(() => {\n if (open) void buildTree();\n }, [open, buildTree]);\n\n function handleEpisodeSelect(mediaId: string) {\n sendLoad(mediaId);\n debug('catalog', 'control_load', { mediaId });\n onClose();\n }\n function handlePlaylistSelect(playlistId: string) {\n emitPlaylistLoad(playlistId);\n debug('catalog', 'playlist_load', { playlistId });\n onClose();\n }\n // Room-wide unload: control/home resets the room's media to null for\n // EVERY viewer and ends any active playlist — it is not a second way to\n // dismiss this drawer. It used to be labelled \"Home\", which read as exactly\n // that next to Close, and with no media loaded it genuinely is a no-op that\n // only closes the drawer. Hence the honest label, and hiding it unless\n // there is something to unload.\n function handleUnload() {\n sendHome();\n debug('controls', 'Unload media clicked (media drawer)');\n onClose();\n }\n function toggleSeries(ci: number, si: number) {\n setTree(t => t.map((c,i)=> (i!==ci || !('series' in c))? c : ({ ...c, series: c.series.map((s,j)=> j!==si? s : ({ ...s, expanded: !s.expanded })) })));\n }\n function toggleCategory(ci: number) {\n setTree(t => t.map((c,i)=> i!==ci? c : ({ ...c, expanded: !c.expanded })));\n }\n function refreshAll() {\n invalidateCatalog('');\n setLoading(true); setError(null);\n void buildTree();\n }\n\n const filteredTree = useMemo(() => {\n if (!filter.trim()) return tree;\n const q = filter.toLowerCase();\n return tree.map(node => {\n if (node.type === 'playlist_category') {\n const playlists = node.playlists.filter(p => p.name.toLowerCase().includes(q));\n if (playlists.length) return { ...node, playlists, expanded: true } as PlaylistCategoryNode;\n if (node.name.toLowerCase().includes(q)) return { ...node, expanded: true } as PlaylistCategoryNode;\n return null;\n }\n const seriesFiltered = node.series.map(s => {\n const eps = s.episodes.filter(e => e.name.toLowerCase().includes(q));\n if (eps.length) return { ...s, episodes: eps, expanded: true } as SeriesNode;\n if (s.name.toLowerCase().includes(q)) return { ...s, expanded: true } as SeriesNode;\n return null;\n }).filter(Boolean) as SeriesNode[];\n if (seriesFiltered.length) return { ...node, series: seriesFiltered, expanded: true } as CategoryNode;\n if (node.name.toLowerCase().includes(q)) return { ...node, expanded: true } as CategoryNode;\n return null;\n }).filter(Boolean) as MediaTree;\n }, [tree, filter]);\n\n return (\n <>\n <AddMediaDrawer open={addOpen} onClose={() => setAddOpen(false)} />\n <DrawerShell\n open={open && !addOpen}\n onClose={onClose}\n header={\n <>\n {/* Reuses the tierlist's segmented-tab styling rather than a second\n copy of it. Real classes, not Tailwind utilities: the unlayered\n `button` reset makes every colour/background utility inert. */}\n <div className=\"tl-tabs\" role=\"tablist\" aria-label=\"Media drawer\">\n {(['watch', 'play'] as const).map(m => (\n <button\n key={m}\n role=\"tab\"\n aria-selected={mode === m}\n onClick={() => setMode(m)}\n className=\"tl-tab\"\n >\n {m === 'watch' ? 'Watch' : 'Play'}\n </button>\n ))}\n </div>\n {mode === 'play' && <StopGameButton onDone={onClose} />}\n {mode === 'watch' && <>\n {canSubmit && (\n <button\n onClick={() => setAddOpen(true)}\n aria-label=\"Add Media\"\n className={DRAWER_BTN}\n >\n Add\n </button>\n )}\n {mediaId && (\n <button\n onClick={handleUnload}\n title=\"Unload the current media for everyone in the room\"\n className=\"drawer-btn drawer-btn-primary\"\n >\n Unload\n </button>\n )}\n {/* The spinner is the whole point of this control: a reload that\n hits a warm catalog cache returns too fast to see, so without a\n visible in-flight state you cannot tell a refresh happened.\n Disabled while in flight so a second click can't stack rebuilds. */}\n <button onClick={refreshAll} disabled={loading} className={`${DRAWER_BTN} inline-flex items-center gap-1.5`}>\n {loading && <span className=\"inline-block w-3 h-3 border-2 border-text-muted border-t-transparent rounded-full animate-spin\" />}\n {loading ? 'Reloading…' : 'Reload'}\n </button>\n <input value={filter} onChange={e=>setFilter(e.target.value)} placeholder=\"search\" className=\"ml-auto bg-bg-alt border border-border focus:border-border-light outline-none px-2 py-1 rounded text-xs\" />\n </>}\n </>\n }\n footer={isDebugEnabled() ? (\n <div className=\"p-2 text-[10px] text-text-muted border-t border-border flex justify-end\">\n <span className=\"opacity-70\">verbose</span>\n </div>\n ) : undefined}\n >\n {mode === 'play' ? <GameGrid onDone={onClose} /> : <>\n {loading && !tree.length && <div className=\"px-4 py-2 text-xs text-text-muted\">Loading catalog…</div>}\n {error && <div className=\"mx-3 px-3 py-2 text-xs bg-danger-bg text-on-danger-bg rounded\">{error}</div>}\n <div className=\"flex-1 overflow-auto text-sm py-2\">\n {filteredTree.map((node, ci) => (\n <div key={node.name} className=\"px-3 pb-3\">\n {/* data-* expose the STATE, so E2E asserts which categories are\n open rather than reading the −/+ glyph. */}\n <div\n className=\"flex items-center gap-2 cursor-pointer select-none group\"\n data-category={node.name}\n data-expanded={node.expanded ? 'true' : 'false'}\n onClick={()=>toggleCategory(ci)}\n >\n <span className=\"text-xs uppercase tracking-wider text-text-muted group-hover:text-text-secondary\">{node.name}</span>\n <span className=\"text-[10px] opacity-50\">{node.expanded? '−':'+'}</span>\n </div>\n {node.expanded && node.type === 'playlist_category' && (\n <div className=\"mt-2 space-y-1\">\n {node.playlists.map(p => (\n <button key={p.playlistId} onClick={()=>handlePlaylistSelect(p.playlistId)} className=\"w-full text-left px-2 py-1 rounded hover:bg-bg-alt flex items-center gap-2 group\">\n <span className=\"text-[11px] text-text-tertiary group-hover:text-text-secondary truncate\">{p.name}</span>\n <span className=\"ml-auto text-[10px] text-text underline decoration-primary underline-offset-2 opacity-0 group-hover:opacity-100 transition-opacity\">Load</span>\n </button>\n ))}\n {node.playlists.length === 0 && <div className=\"text-[11px] text-text-muted italic\">(no playlists)</div>}\n </div>\n )}\n {node.expanded && node.type === 'category' && (\n <div className=\"mt-2 space-y-2\">\n {node.series.map((s, si) => (\n <div key={s.name}>\n <div className=\"flex items-center gap-2 cursor-pointer text-text-secondary hover:text-text\" onClick={()=>toggleSeries(ci, si)}>\n <span className=\"text-xs font-medium\">{s.name}</span>\n <span className=\"text-[10px] opacity-40\">{s.expanded? '−':'+'}</span>\n </div>\n {s.expanded && (\n <div className=\"mt-1 ml-3 border-l border-border pl-3 space-y-1\">\n {s.episodes.map(ep => (\n <button key={ep.mediaId} onClick={()=>handleEpisodeSelect(ep.mediaId)} className=\"w-full text-left px-2 py-1 rounded hover:bg-bg-alt flex items-center gap-2 group\">\n <span className=\"text-[11px] text-text-tertiary group-hover:text-text-secondary truncate\">{ep.name}</span>\n <span className=\"ml-auto text-[10px] text-text underline decoration-primary underline-offset-2 opacity-0 group-hover:opacity-100 transition-opacity\">Load</span>\n </button>\n ))}\n {s.episodes.length === 0 && <div className=\"text-[11px] text-text-muted italic\">(no episodes)</div>}\n </div>\n )}\n </div>\n ))}\n {node.series.length === 0 && <div className=\"text-[11px] text-text-muted italic\">(empty)</div>}\n </div>\n )}\n </div>\n ))}\n {!loading && !filteredTree.length && <div className=\"px-4 py-6 text-center text-xs text-text-muted\">No matches</div>}\n </div>\n </>}\n </DrawerShell>\n </>\n );\n}\n"],"names":["cache","inflight","CACHE_MS","fetchCatalog","prefix","now","hit","pending","request","url","getRuntimeServerOrigin","res","data","isDebugEnabled","debug","invalidateCatalog","probeMediaEndpoints","categories","cat","catData","candidate","base","mediaBase","indexUrl","mediaUrl","FRAGMENT_MP4_FILENAME","fetchAndLog","err","warn","label","init","t0","ms","ECCHI_SERIES","norm","name","matchesSeriesList","series","isEcchiSeries","captureExpansion","tree","childrenOf","state","node","children","child","mergeExpansion","withChildren","prev","mergedChildren","wasExpanded","DEFAULT_OPEN_CATEGORY","isOpenByDefault","categoryName","resetToDefaultExpansion","ROOT","CATEGORIES","tryFetch","fetcher","errorFields","loadCatalogTree","categoryListings","playlistListing","seriesPerCategory","listing","d","episodes","f","result","ecchiSeries","i","seriesNodes","playlists","HAS_SCHEME","isSearchText","raw","text","SEARCH_MIN_QUERY","SEARCH_MAX_QUERY","resolveSearchImage","pathOrNull","formatDuration","totalS","s","m","h","ss","getJson","path","signal","searchShows","q","searchVideos","addShowToWatchlist","source","id","value","DEBOUNCE_MS","MediaSearchResults","query","onSubmitted","active","shows","setShows","useState","videos","setVideos","rowState","setRowState","useEffect","ctrl","timer","r","showAnswer","videoAnswer","addShow","show","key","addVideo","video","submitSubmission","submissionFailureLabel","bothEmpty","jsxs","jsx","Section","ResultRow","answer","testId","failed","image","imageClass","title","meta","canAdd","addLabel","onAdd","metaText","Icon","POLL_MS","STATE_CLASS","SubmissionRow","item","withSeries","AddMediaDrawer","open","onClose","setSource","setSeries","setTitle","titleTouched","setTitleTouched","seriesOptions","setSeriesOptions","busy","setBusy","failure","setFailure","rows","setRows","listFailure","setListFailure","uploadTarget","setUploadTarget","selectedFile","setSelectedFile","uploadBusy","setUploadBusy","uploadProgress","setUploadProgress","uploadFailure","setUploadFailure","fileInputRef","useRef","liveRef","isMagnet","useMemo","guessSubmissionKind","searching","refresh","useCallback","fetchMySubmissions","live","fetchUploadTarget","handleSourceChange","dn","extractMagnetDisplayName","resetLinkFields","magnetReady","hasEpisodeCode","handleSubmit","trimmed","submitMagnetSubmission","handleFileChange","e","file","handleUpload","target","uploaded","uploadFileForSubmission","sent","total","phase","recorded","submitUploadSubmission","uploadStatus","uploadFailureLabel","DrawerShell","Fragment","DRAWER_BTN","row","formatSubmissionBytes","BUILT_IN_COVERS","bombermanCover","stopGame","emitGameSelect","GameGrid","onDone","activeGameId","useAppStore","games","useGameCatalog","start","gameId","stop","g","isActive","cover","StopGameButton","MediaDrawer","loading","setLoading","error","setError","setTree","filter","setFilter","addOpen","setAddOpen","mode","setMode","mediaId","canSubmit","canSubmitMedia","buildTree","isLeader","useLeaderAffordance","wasOpen","setWasOpen","t","handleEpisodeSelect","sendLoad","handlePlaylistSelect","playlistId","emitPlaylistLoad","handleUnload","sendHome","toggleSeries","ci","si","c","j","toggleCategory","refreshAll","filteredTree","p","seriesFiltered","eps","ep"],"mappings":";;;;;;;AAWA,MAAMA,yBAAY,IAAA,GAGZC,yBAAe,IAAA,GACfC,KAAW;AAEjB,eAAsBC,EAAaC,IAAiB,IAA8B;AAChF,QAAMC,IAAM,KAAK,IAAA,GACXC,IAAMN,GAAM,IAAII,CAAM;AAC5B,MAAIE,KAAQD,IAAMC,EAAI,KAAMJ,WAAiBI,EAAI;AACjD,QAAMC,IAAUN,GAAS,IAAIG,CAAM;AACnC,MAAIG,EAAS,QAAOA;AAEpB,QAAMC,KAAW,YAAY;AAE3B,UAAMC,IAAM,GADGC,EAAA,CACM,eAAeN,IAAS,WAAW,mBAAmBA,CAAM,CAAC,KAAK,EAAE,IACnFO,IAAM,MAAM,MAAMF,GAAK,EAAE,OAAO,YAAY;AAClD,QAAI,CAACE,EAAI,GAAI,OAAM,IAAI,MAAM,gBAAgBA,EAAI,MAAM,EAAE;AAIzD,UAAMC,IAAQ,MAAMD,EAAI,KAAA;AACxB,WAAIE,EAAA,KAAkBC,EAAM,WAAW,mBAAmB,EAAE,QAAAV,GAAQ,MAAMQ,EAAK,MAAM,QAAQ,OAAOA,EAAK,OAAO,QAAQ,GACxHZ,GAAM,IAAII,GAAQ,EAAE,IAAIC,GAAK,MAAAO,GAAM,GAC5BA;AAAA,EACT,GAAA;AACA,EAAAX,GAAS,IAAIG,GAAQI,CAAO;AAC5B,MAAI;AACF,WAAO,MAAMA;AAAA,EACf,UAAA;AACE,IAAAP,GAAS,OAAOG,CAAM;AAAA,EACxB;AACF;AAGO,SAASW,GAAkBX,GAAiB;AACpC,EAAAJ,GAAM,MAAA;AAErB;AC3CA,eAAsBgB,KAAsB;AAC1C,MAAKH;AACL,QAAI;AACF,YAAMI,IAAa,CAAC,qBAAoB,qBAAoB,gBAAgB;AAC5E,iBAAWC,KAAOD,GAAY;AAC5B,cAAME,IAAU,MAAMhB,EAAae,CAAG;AACtC,QAAAJ,EAAM,WAAW,qBAAqBI,GAAK,EAAE,MAAMC,EAAQ,KAAK,QAAQ,OAAOA,EAAQ,MAAM,OAAA,CAAQ;AAErG,cAAMC,IAAYD,EAAQ,MAAM,CAAC;AACjC,YAAIC,GAAW;AACb,gBAAMC,IAAOC,GAAA,GACPC,IAAW,GAAGF,CAAI,IAAID,EAAU,EAAE,cAAc,QAAQ,OAAM,GAAG,GACjEI,IAAW,GAAGH,CAAI,IAAID,EAAU,EAAE,IAAIK,EAAqB,GAAG,QAAQ,OAAM,GAAG;AACrF,gBAAMC,GAAYH,GAAU,OAAO,GAEnC,MAAMG,GAAYF,GAAU,YAAY,EAAE,SAAS,EAAE,OAAO,eAAA,GAAkB;AAAA,QAChF;AAAA,MACF;AAAA,IACF,SAASG,GAAc;AACrB,MAAAC,EAAK,WAAW,iBAAiB,EAAE,OAAOD,aAAe,QAAQA,EAAI,UAAU,OAAOA,CAAG,EAAA,CAAG;AAAA,IAC9F;AACF;AAEA,SAASL,KAAY;AACnB,SAAO;AACT;AAEA,eAAeI,GAAYjB,GAAaoB,GAAeC,GAAoB;AACzE,QAAMC,IAAK,YAAY,IAAA,GACjBpB,IAAM,MAAM,MAAMF,GAAKqB,CAAI,GAC3BE,IAAK,KAAK,MAAM,YAAY,IAAA,IAAQD,CAAE;AAC5C,SAAAjB,EAAM,WAAW,aAAae,GAAO,EAAE,KAAApB,GAAK,QAAQE,EAAI,QAAQ,IAAAqB,GAAI,OAAOrB,EAAI,QAAQ,IAAI,gBAAgB,GAAG,GACvGA;AACT;AC5BA,MAAMsB,KAAe;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAEMC,KAAO,CAACC,MAAiBA,EAAK,cAAc,QAAQ,cAAc,EAAE;AAQnE,SAASC,GAAkBD,GAAcE,GAAoC;AAClF,SAAO,IAAI,IAAIA,EAAO,IAAIH,EAAI,CAAC,EAAE,IAAIA,GAAKC,CAAI,CAAC;AACjD;AAEO,SAASG,GAAcH,GAAuB;AACnD,SAAOC,GAAkBD,GAAMF,EAAY;AAC7C;ACcO,SAASM,GACdC,GACAC,GACgB;AAChB,QAAMC,wBAA4B,IAAA;AAClC,aAAWC,KAAQH,GAAM;AACvB,UAAMI,wBAAe,IAAA;AACrB,eAAWC,KAASJ,EAAWE,CAAI;AACjC,MAAAC,EAAS,IAAIC,EAAM,MAAMA,EAAM,aAAa,EAAI;AAElD,IAAAH,EAAM,IAAIC,EAAK,MAAM,EAAE,UAAUA,EAAK,aAAa,IAAM,UAAAC,GAAU;AAAA,EACrE;AACA,SAAOF;AACT;AASO,SAASI,GACdN,GACAE,GACAD,GACAM,GACK;AACL,SAAOP,EAAK,IAAI,CAACG,MAAS;AACxB,UAAMK,IAAON,EAAM,IAAIC,EAAK,IAAI;AAChC,QAAI,CAACK,EAAM,QAAOL;AAGlB,UAAMM,IADWR,EAAWE,CAAI,EACA,IAAI,CAACE,MAAU;AAC7C,YAAMK,IAAcF,EAAK,SAAS,IAAIH,EAAM,IAAI;AAChD,aAAIK,MAAgB,SAAkBL,IAC/B,EAAE,GAAGA,GAAO,UAAUK,EAAA;AAAA,IAC/B,CAAC;AAGD,WADeH,EAAa,EAAE,GAAGJ,GAAM,UAAUK,EAAK,SAAA,GAAYC,CAAc;AAAA,EAElF,CAAC;AACH;AAUO,MAAME,KAAwB;AAE9B,SAASC,EAAgBC,GAA+B;AAC7D,SAAOA,MAAiBF;AAC1B;AAiBO,SAASG,GACdd,GACAC,GACAM,GACK;AACL,SAAOP,EAAK;AAAA,IAAI,CAACG,MACfI;AAAA,MACE,EAAE,GAAGJ,GAAM,UAAUS,EAAgBT,EAAK,IAAI,EAAA;AAAA,MAC9CF,EAAWE,CAAI,EAAE,IAAI,CAACE,OAAW,EAAE,GAAGA,GAAO,UAAU,KAAQ;AAAA,IAAA;AAAA,EACjE;AAEJ;ACxGA,MAAMU,KAAO,eACPC,KAA+C;AAAA,EACnD,EAAE,KAAK,SAAS,OAAO,QAAA;AAAA,EACvB,EAAE,KAAK,SAAS,OAAO,QAAA;AAAA,EACvB,EAAE,KAAK,MAAM,OAAO,KAAA;AACtB;AAEA,eAAeC,GAASC,GAAyBtD,GAAiD;AAChG,MAAI;AACF,WAAO,MAAMsD,EAAQtD,CAAM;AAAA,EAC7B,SAASuB,GAAK;AACZ,WAAAC,EAAK,WAAW,gBAAgB,EAAE,QAAAxB,GAAQ,GAAGuD,EAAYhC,CAAG,GAAG,GACxD;AAAA,EACT;AACF;AAEA,eAAsBiC,GAAgBF,GAA6C;AACjF,QAAM,CAACG,GAAkBC,CAAe,IAAI,MAAM,QAAQ,IAAI;AAAA,IAC5D,QAAQ,IAAIN,GAAW,IAAI,OAAOC,GAASC,GAAS,GAAGH,EAAI,IAAIrC,EAAI,GAAG,EAAE,CAAC,CAAC;AAAA,IAC1EuC,GAASC,GAAS,GAAGH,EAAI,YAAY;AAAA,EAAA,CACtC,GAEKQ,IAAoB,MAAM,QAAQ,IAAIF,EAAiB,IAAI,CAAAG,MAC/D,QAAQ,KAAKA,GAAS,QAAQ,CAAA,GAAI,IAAI,OAAOC,MAAkC;AAE7E,UAAMC,MADS,MAAMT,GAASC,GAASO,EAAE,IAAI,IACJ,SAAS,CAAA,GAAI,IAAI,CAAAE,OAAM,EAAE,MAAM,WAAW,MAAMA,EAAE,OAAO,SAASA,EAAE,KAAK;AAClH,WAAOD,EAAS,SAAS,EAAE,MAAM,UAAU,MAAMD,EAAE,MAAM,UAAAC,GAAU,UAAU,GAAA,IAAU;AAAA,EACzF,CAAC,CAAC,CAAC,CAAC,GAEAE,IAAoB,CAAA,GAGpBC,IAA4B,CAAA;AAClC,EAAAb,GAAW,QAAQ,CAACtC,GAAKoD,MAAM;AAC7B,UAAMC,IAA4B,CAAA;AAClC,eAAW5B,KAAQoB,EAAkBO,CAAC;AACpC,MAAK3B,MACDzB,EAAI,QAAQ,WAAWoB,GAAcK,EAAK,IAAI,IAAG0B,EAAY,KAAK1B,CAAI,IACrE4B,EAAY,KAAK5B,CAAI;AAE5B,IAAI4B,EAAY,UAAQH,EAAO,KAAK,EAAE,MAAM,YAAY,MAAMlD,EAAI,OAAO,QAAQqD,GAAa,UAAUnB,EAAgBlC,EAAI,KAAK,GAAG;AAAA,EACtI,CAAC;AACD,QAAMsD,KAA6BV,GAAiB,QAAQ,CAAA,GAAI,IAAI,CAAAG,OAAM,EAAE,MAAM,YAAY,MAAMA,EAAE,MAAM,YAAYA,EAAE,OAAO;AACjI,SAAIO,EAAU,UAAQJ,EAAO,KAAK,EAAE,MAAM,qBAAqB,MAAM,aAAa,WAAAI,GAAW,UAAUpB,EAAgB,WAAW,GAAG,GAGjIiB,EAAY,UAAQD,EAAO,KAAK,EAAE,MAAM,YAAY,MAAM,SAAS,QAAQC,GAAa,UAAUjB,EAAgB,OAAO,GAAG,GACzHgB;AACT;AChDA,MAAMK,KAAa;AAEZ,SAASC,GAAaC,GAAsB;AACjD,QAAMC,IAAOD,EAAI,KAAA;AACjB,SAAOC,EAAK,UAAUC,MAAoBD,EAAK,UAAUE,MAAoB,CAACL,GAAW,KAAKG,CAAI;AACpG;AAGO,SAASG,GAAmBC,GAA0C;AAC3E,SAAOA,IAAa,GAAGtE,EAAA,CAAwB,GAAGsE,CAAU,KAAK;AACnE;AAGO,SAASC,GAAeC,GAA+B;AAC5D,MAAIA,MAAW,QAAQ,CAAC,OAAO,SAASA,CAAM,KAAKA,IAAS,EAAG,QAAO;AACtE,QAAMC,IAAI,KAAK,MAAMD,IAAS,EAAE,GAC1BE,IAAI,KAAK,MAAMF,IAAS,EAAE,IAAI,IAC9BG,IAAI,KAAK,MAAMH,IAAS,IAAI,GAC5BI,IAAK,OAAOH,CAAC,EAAE,SAAS,GAAG,GAAG;AACpC,SAAOE,IAAI,IAAI,GAAGA,CAAC,IAAI,OAAOD,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC,IAAIE,CAAE,KAAK,GAAGF,CAAC,IAAIE,CAAE;AACxE;AAIA,eAAeC,GAAWC,GAAcC,GAAgD;AACtF,MAAI;AACF,UAAM9E,IAAM,MAAM,MAAM,GAAGD,GAAwB,GAAG8E,CAAI,IAAI,EAAE,QAAAC,GAAQ;AACxE,WAAK9E,EAAI,KACF,EAAE,IAAI,IAAM,OAAO,MAAMA,EAAI,OAAK,IADrB,EAAE,IAAI,IAAO,QAAQA,EAAI,OAAA;AAAA,EAE/C,SAASgB,GAAc;AACrB,WAAI8D,EAAO,UAAgB,EAAE,IAAI,IAAO,QAAQ,UAAA,KAChD7D,EAAK,UAAU,wBAAwB+B,EAAYhC,CAAG,CAAC,GAChD,EAAE,IAAI,IAAO,QAAQ,UAAA;AAAA,EAC9B;AACF;AAEO,SAAS+D,GAAYC,GAAWF,GAA6D;AAClG,SAAOF,GAAQ,uBAAuB,mBAAmBI,EAAE,MAAM,CAAC,IAAIF,CAAM;AAC9E;AAEO,SAASG,GAAaD,GAAWF,GAA8D;AACpG,SAAOF,GAAQ,yBAAyB,mBAAmBI,EAAE,MAAM,CAAC,IAAIF,CAAM;AAChF;AAOA,eAAsBI,GAAmBC,GAAqBC,GAA0C;AACtG,MAAI;AACF,UAAMpF,IAAM,MAAM,MAAM,GAAGD,EAAA,CAAwB,kBAAkB;AAAA,MACnE,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAA;AAAA,MAC3B,MAAM,KAAK,UAAU,EAAE,QAAAoF,GAAQ,IAAAC,GAAI;AAAA,IAAA,CACpC;AACD,QAAIpF,EAAI,WAAW,KAAK;AACtB,YAAMqF,IAAQ,MAAMrF,EAAI,KAAA;AACxB,aAAAG,EAAM,UAAU,uBAAuB,EAAE,QAAAgF,GAAQ,IAAAC,GAAI,QAAQC,EAAM,QAAQ,GACpE,EAAE,IAAI,IAAM,OAAAA,EAAA;AAAA,IACrB;AACA,WAAO,EAAE,IAAI,IAAO,MAAMrF,EAAI,WAAW,MAAM,WAAW,UAAU,QAAQA,EAAI,OAAA;AAAA,EAClF,SAASgB,GAAc;AACrB,WAAAC,EAAK,UAAU,wBAAwB+B,EAAYhC,CAAG,CAAC,GAChD,EAAE,IAAI,IAAO,MAAM,UAAU,QAAQ,UAAA;AAAA,EAC9C;AACF;AC/DA,MAAMsE,KAAc;AAUpB,SAAwBC,GAAmB,EAAE,OAAAC,GAAO,aAAAC,KAAsB;AACxE,QAAMT,IAAIQ,EAAM,KAAA,GACVE,IAAS3B,GAAaiB,CAAC,GACvB,CAACW,GAAOC,CAAQ,IAAIC,EAAqC,IAAI,GAC7D,CAACC,GAAQC,CAAS,IAAIF,EAAsC,IAAI,GAChE,CAACG,GAAUC,CAAW,IAAIJ,EAAmC,CAAA,CAAE;AAkBrE,MAhBAK,EAAU,MAAM;AACd,QAAI,CAACR,EAAQ;AACb,UAAMS,IAAO,IAAI,gBAAA,GACXC,IAAQ,WAAW,MAAM;AAC7B,MAAKrB,GAAYC,GAAGmB,EAAK,MAAM,EAAE,KAAK,CAAAE,MAAK;AACzC,QAAIF,EAAK,OAAO,WAChBP,EAAS,EAAE,GAAAZ,GAAG,SAASqB,EAAE,KAAK,EAAE,IAAI,IAAM,OAAOA,EAAE,MAAM,QAAA,IAAYA,GAAG;AAAA,MAC1E,CAAC,GACIpB,GAAaD,GAAGmB,EAAK,MAAM,EAAE,KAAK,CAAAE,MAAK;AAC1C,QAAIF,EAAK,OAAO,WAChBJ,EAAU,EAAE,GAAAf,GAAG,SAASqB,EAAE,KAAK,EAAE,IAAI,IAAM,OAAOA,EAAE,MAAM,QAAA,IAAYA,GAAG;AAAA,MAC3E,CAAC;AAAA,IACH,GAAGf,EAAW;AACd,WAAO,MAAM;AAAE,mBAAac,CAAK,GAAGD,EAAK,MAAA;AAAA,IAAS;AAAA,EACpD,GAAG,CAACnB,GAAGU,CAAM,CAAC,GAEV,CAACA,EAAQ,QAAO;AAEpB,QAAMY,IAAaX,GAAO,MAAMX,IAAIW,EAAM,UAAU,MAC9CY,IAAcT,GAAQ,MAAMd,IAAIc,EAAO,UAAU;AAEvD,iBAAeU,EAAQC,GAAmB;AACxC,UAAMC,IAAM,GAAGD,EAAK,MAAM,IAAIA,EAAK,EAAE;AACrC,IAAAR,EAAY,CAAAzB,OAAM,EAAE,GAAGA,GAAG,CAACkC,CAAG,GAAG,WAAW;AAC5C,UAAML,IAAI,MAAMnB,GAAmBuB,EAAK,QAAQA,EAAK,EAAE;AACvD,IAAAR,EAAY,QAAM,EAAE,GAAGzB,GAAG,CAACkC,CAAG,GAAGL,EAAE,MAAMA,EAAE,SAAS,WAAW,UAAU,SAASA,EAAE,MAAM,KAAK;AAAA,EACjG;AAEA,iBAAeM,EAASC,GAAqB;AAC3C,UAAMF,IAAM,MAAME,EAAM,EAAE;AAC1B,IAAAX,EAAY,CAAAzB,OAAM,EAAE,GAAGA,GAAG,CAACkC,CAAG,GAAG,WAAW;AAC5C,UAAML,IAAI,MAAMQ,GAAiBD,EAAM,KAAKA,EAAM,KAAK;AACvD,IAAIP,EAAE,MACJJ,EAAY,CAAAzB,OAAM,EAAE,GAAGA,GAAG,CAACkC,CAAG,GAAG,UAAU,GAC3CjB,EAAYY,EAAE,KAAK,KAEnBJ,EAAY,CAAAzB,OAAM,EAAE,GAAGA,GAAG,CAACkC,CAAG,GAAGI,EAAuBT,EAAE,OAAO,EAAA,EAAI;AAAA,EAEzE;AAEA,QAAMU,IAAYT,GAAY,MAAMC,GAAa,MAC5CD,EAAW,MAAM,WAAW,KAAKC,EAAY,MAAM,WAAW;AAEnE,SACE,gBAAAS,EAAC,OAAA,EAAI,WAAU,0BAAyB,eAAY,gBACjD,UAAA;AAAA,IAAAD,KAAa,gBAAAE,EAAC,OAAA,EAAI,WAAU,yCAAwC,UAAA,cAAU;AAAA,IAC/E,gBAAAA,EAACC,IAAA,EAAQ,OAAM,SAAQ,QAAQZ,GAAY,QAAO,gBAC/C,UAAAA,GAAY,MAAMA,EAAW,MAAM,IAAI,CAAAG,MAAQ;AAC9C,YAAMC,IAAM,GAAGD,EAAK,MAAM,IAAIA,EAAK,EAAE,IAC/B1E,IAAQ0E,EAAK,cAAc,UAAUT,EAASU,CAAG;AACvD,aACE,gBAAAO;AAAA,QAACE;AAAA,QAAA;AAAA,UAEC,QAAQ,QAAQT,CAAG;AAAA,UACnB,OAAOtC,GAAmBqC,EAAK,MAAM;AAAA,UACrC,YAAW;AAAA,UACX,OAAOA,EAAK;AAAA,UACZ,MAAM,CAACA,EAAK,MAAMA,EAAK,QAAQA,EAAK,WAAW,GAAGA,EAAK,QAAQ,QAAQ,IAAI;AAAA,UAC3E,OAAA1E;AAAA,UACA,QAAQ0E,EAAK;AAAA,UACb,UAAU,OAAOA,EAAK,KAAK;AAAA,UAC3B,OAAO,MAAM;AAAE,YAAKD,EAAQC,CAAI;AAAA,UAAG;AAAA,QAAA;AAAA,QAT9BC;AAAA,MAAA;AAAA,IAYX,CAAC,EAAA,CACH;AAAA,IACA,gBAAAO,EAACC,IAAA,EAAQ,OAAM,WAAU,QAAQX,GAAa,QAAO,kBAClD,UAAAA,GAAa,MAAMA,EAAY,MAAM,IAAI,CAAAK,MACxC,gBAAAK;AAAA,MAACE;AAAA,MAAA;AAAA,QAEC,QAAQ,SAASP,EAAM,EAAE;AAAA,QACzB,OAAOxC,GAAmBwC,EAAM,KAAK;AAAA,QACrC,YAAW;AAAA,QACX,OAAOA,EAAM;AAAA,QACb,MAAM,CAACA,EAAM,SAAStC,GAAesC,EAAM,SAAS,CAAC;AAAA,QACrD,OAAOZ,EAAS,MAAMY,EAAM,EAAE,EAAE;AAAA,QAChC,QAAM;AAAA,QACN,UAAU,WAAWA,EAAM,KAAK;AAAA,QAChC,OAAO,MAAM;AAAE,UAAKD,EAASC,CAAK;AAAA,QAAG;AAAA,MAAA;AAAA,MAThCA,EAAM;AAAA,IAAA,CAWd,EAAA,CACH;AAAA,EAAA,GACF;AAEJ;AAEA,SAASM,GAAW,EAAE,OAAAhG,GAAO,QAAAkG,GAAQ,QAAAC,GAAQ,UAAApF,KAK1C;AACD,MAAImF,GAAQ,MAAMA,EAAO,MAAM,WAAW,EAAG,QAAO;AACpD,QAAME,IAASF,KAAU,CAACA,EAAO,MAAMA,EAAO,WAAW;AACzD,2BACG,OAAA,EAAI,eAAaC,GAAQ,eAAaD,IAAS,SAAS,SACvD,UAAA;AAAA,IAAA,gBAAAJ,EAAC,OAAA,EAAI,WAAU,+FACb,UAAA;AAAA,MAAA,gBAAAC,EAAC,UAAM,UAAA/F,EAAA,CAAM;AAAA,MACZ,CAACkG,KAAU,gBAAAH,EAAC,QAAA,EAAK,WAAU,sBAAqB,UAAA,KAAC;AAAA,MACjDK,KAAU,gBAAAL,EAAC,QAAA,EAAK,WAAU,2DAA0D,UAAA,cAAA,CAAW;AAAA,IAAA,GAClG;AAAA,IACChF;AAAA,EAAA,GACH;AAEJ;AAEA,SAASkF,GAAU,EAAE,QAAAE,GAAQ,OAAAE,GAAO,YAAAC,GAAY,OAAAC,GAAO,MAAAC,GAAM,OAAA3F,GAAO,QAAA4F,GAAQ,UAAAC,GAAU,OAAAC,EAAA,GAUnF;AACD,QAAMC,IAAWJ,EAAK,OAAO,CAAAjD,MAAKA,MAAM,QAAQA,MAAM,EAAE,EAAE,KAAK,KAAK;AACpE,SACE,gBAAAuC,EAAC,SAAI,WAAU,qCAAoC,eAAaK,GAAQ,cAAYtF,KAAS,IAC1F,UAAA;AAAA,IAAAwF,sBACI,OAAA,EAAI,KAAKA,GAAO,KAAI,IAAG,SAAQ,QAAO,WAAW,GAAGC,CAAU,4CAA4C,IAC3G,gBAAAP,EAAC,SAAI,WAAW,GAAGO,CAAU,+BAA+B;AAAA,IAChE,gBAAAR,EAAC,OAAA,EAAI,WAAU,kBACb,UAAA;AAAA,MAAA,gBAAAC,EAAC,OAAA,EAAI,WAAU,kCAAiC,OAAAQ,GAAe,UAAAA,GAAM;AAAA,MACpEK,KAAY,gBAAAb,EAAC,OAAA,EAAI,WAAU,wCAAwC,UAAAa,EAAA,CAAS;AAAA,IAAA,GAC/E;AAAA,IACC/F,MAAU,UACP,gBAAAkF,EAACc,IAAA,EAAK,MAAK,SAAQ,SAAQ,QAAO,QAAO,WAAU,OAAK,IAAC,WAAU,WAAA,CAAW,IAC9EhG,KAASA,MAAU,WACjB,gBAAAkF,EAAC,UAAK,WAAU,oEAAoE,UAAAlF,EAAA,CAAM,IAC1F4F,KACA,gBAAAV;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,SAASY;AAAA,QACT,UAAU9F,MAAU;AAAA,QACpB,cAAY6F;AAAA,QACZ,WAAU;AAAA,QAEV,UAAA,gBAAAX,EAACc,IAAA,EAAK,MAAK,OAAA,CAAO;AAAA,MAAA;AAAA,IAAA;AAAA,EACpB,GAER;AAEJ;AC9GA,MAAMC,KAAU,KAOVC,KAAgD;AAAA,EACpD,SAAS;AAAA,EACT,UAAU;AAAA,EACV,UAAU;AAAA,EACV,WAAW;AAAA,EACX,OAAO;AAAA,EACP,UAAU;AAAA,EACV,QAAQ;AACV;AAQA,SAASC,GAAc,EAAE,MAAAC,KAA+B;AACtD,QAAMC,IAAaD,GACbjH,IAAQiH,EAAK,SAASA,EAAK;AACjC,SACE,gBAAAnB;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAU;AAAA,MACV,sBAAoBmB,EAAK;AAAA,MACzB,yBAAuBA,EAAK;AAAA,MAC5B,wBAAsBA,EAAK;AAAA,MAC3B,0BAAwBC,EAAW,UAAU;AAAA,MAE7C,UAAA;AAAA,QAAA,gBAAAnB,EAAC,QAAA,EAAK,WAAU,iEAAiE,UAAAkB,EAAK,MAAK;AAAA,QAC3F,gBAAAlB,EAAC,QAAA,EAAK,WAAU,kDAAiD,OAAOkB,EAAK,QAC1E,UAAAC,EAAW,SAAS,GAAGA,EAAW,MAAM,MAAMlH,CAAK,KAAKA,GAC3D;AAAA,QACA,gBAAA+F,EAAC,QAAA,EAAK,WAAW,qDAAqDgB,GAAYE,EAAK,KAAK,KAAK,iBAAiB,IAC/G,UAAAA,EAAK,MAAA,CACR;AAAA,QACCA,EAAK,SAAS,gBAAAlB,EAAC,UAAK,WAAU,kFAAkF,YAAK,MAAA,CAAM;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGlI;AAEA,SAAwBoB,GAAe,EAAE,MAAAC,GAAM,SAAAC,KAAgC;AAC7E,QAAM,CAACpD,GAAQqD,CAAS,IAAI3C,EAAS,EAAE,GACjC,CAACnE,GAAQ+G,CAAS,IAAI5C,EAAS,EAAE,GACjC,CAAC4B,GAAOiB,CAAQ,IAAI7C,EAAS,EAAE,GAC/B,CAAC8C,GAAcC,CAAe,IAAI/C,EAAS,EAAK,GAChD,CAACgD,GAAeC,CAAgB,IAAIjD,EAAmB,CAAA,CAAE,GACzD,CAACkD,GAAMC,CAAO,IAAInD,EAAS,EAAK,GAChC,CAACoD,GAASC,CAAU,IAAIrD,EAAmC,IAAI,GAC/D,CAACsD,GAAMC,CAAO,IAAIvD,EAA+B,IAAI,GACrD,CAACwD,GAAaC,CAAc,IAAIzD,EAAmC,IAAI,GAEvE,CAAC0D,GAAcC,CAAe,IAAI3D,EAA8B,IAAI,GACpE,CAAC4D,GAAcC,CAAe,IAAI7D,EAAsB,IAAI,GAC5D,CAAC8D,GAAYC,CAAa,IAAI/D,EAAS,EAAK,GAC5C,CAACgE,GAAgBC,CAAiB,IAAIjE,EAAqE,IAAI,GAC/G,CAACkE,GAAeC,CAAgB,IAAInE,EAAmD,IAAI,GAC3FoE,IAAeC,GAAgC,IAAI,GAGnDC,IAAUD,GAAO,EAAK,GAGtBE,IADOC,GAAQ,MAAMC,GAAoBnF,CAAM,GAAG,CAACA,CAAM,CAAC,MACtC,YAAYA,EAAO,KAAA,EAAO,SAAS,GACvDoF,KAAYxG,GAAaoB,CAAM,GAE/BqF,IAAUC,GAAY,MAAMC,GAAA,EAAqB,KAAK,CAAA1K,MAAO;AACjE,IAAKmK,EAAQ,YACTnK,EAAI,MAAMoJ,EAAQpJ,EAAI,KAAK,GAAGsJ,EAAe,IAAI,KAC9CA,EAAetJ,EAAI,OAAO;AAAA,EACnC,CAAC,GAAG,CAAA,CAAE;AAEN,EAAAkG,EAAU,MAAM;AACd,QAAI,CAACoC,EAAM;AACX,IAAA6B,EAAQ,UAAU,IACbK,EAAA;AACL,UAAMpE,IAAQ,YAAY,MAAM;AAAE,MAAKoE,EAAA;AAAA,IAAW,GAAGxC,EAAO;AAC5D,WAAO,MAAM;AAAE,MAAAmC,EAAQ,UAAU,IAAO,cAAc/D,CAAK;AAAA,IAAG;AAAA,EAChE,GAAG,CAACkC,GAAMkC,CAAO,CAAC,GAKlBtE,EAAU,MAAM;AACd,QAAI,CAACoC,EAAM;AACX,QAAIqC,IAAO;AACX,WAAKC,GAAA,EAAoB,KAAK,CAAA5K,MAAO;AAAE,MAAI2K,KAAQ3K,EAAI,MAAIwJ,EAAgBxJ,EAAI,KAAK;AAAA,IAAG,CAAC,GACjF,MAAM;AAAE,MAAA2K,IAAO;AAAA,IAAO;AAAA,EAC/B,GAAG,CAACrC,CAAI,CAAC,GAGTpC,EAAU,MAAM;AACd,QAAI,CAACoC,EAAM;AACX,QAAIqC,IAAO;AAGX,WAAKnL,EAAa,mBAAmB,EAAE,KAAK,CAAAe,MAAO;AACjD,MAAIoK,OAAuBpK,EAAI,KAAK,IAAI,CAAA+C,MAAKA,EAAE,IAAI,CAAC;AAAA,IACtD,CAAC,EAAE,MAAM,CAACtC,MAAiB;AAEzB,MAAAb,EAAM,SAAS,kCAAkC6C,EAAYhC,CAAG,CAAC;AAAA,IACnE,CAAC,GACM,MAAM;AAAE,MAAA2J,IAAO;AAAA,IAAO;AAAA,EAC/B,GAAG,CAACrC,CAAI,CAAC;AAaT,WAASuC,GAAmBxF,GAAe;AAGzC,QAFAmD,EAAUnD,CAAK,GACGiF,GAAoBjF,CAAK,MAAM,YAAYA,EAAM,OAAO,SAAS,KAClE,CAACsD,GAAc;AAC9B,YAAMmC,IAAKC,GAAyB1F,CAAK;AACzC,MAAIyF,OAAaA,CAAE;AAAA,IACrB;AAAA,EACF;AAEA,WAASE,KAAkB;AACzB,IAAAxC,EAAU,EAAE,GACZC,EAAU,EAAE,GACZC,EAAS,EAAE,GACXE,EAAgB,EAAK;AAAA,EACvB;AAEA,QAAMqC,KAAc,CAACb,KAAa1I,EAAO,OAAO,SAAS,KAAKwJ,GAAezD,CAAK;AAElF,iBAAe0D,KAAe;AAC5B,UAAMC,IAAUjG,EAAO,KAAA;AAEvB,QADI,CAACiG,KAAWrC,KAAQwB,MACpBH,KAAY,CAACa,GAAa;AAC9B,IAAAjC,EAAQ,EAAI,GACZE,EAAW,IAAI;AACf,UAAMlJ,IAAMoK,IACR,MAAMiB,GAAuBD,GAAS,EAAE,OAAO3D,EAAM,KAAA,KAAU,QAAW,QAAQ/F,EAAO,KAAA,EAAK,CAAG,IACjG,MAAMmF,GAAiBuE,CAAO;AAElC,QADApC,EAAQ,EAAK,GACT,CAAChJ,EAAI,IAAI;AAAE,MAAAkJ,EAAWlJ,EAAI,OAAO;AAAG;AAAA,IAAQ;AAChD,IAAAG,EAAM,eAAe,mBAAmB,EAAE,IAAIH,EAAI,MAAM,IAAI,OAAOA,EAAI,MAAM,MAAA,CAAO,GACpFgL,GAAA,GAIA5B,EAAQ,OAAQ,CAACpJ,EAAI,OAAO,IAAIqC,KAAQ,CAAA,GAAI,OAAO,CAAAgE,MAAKA,EAAE,OAAOrG,EAAI,MAAM,EAAE,CAAC,CAAC,GAC1EwK,EAAA;AAAA,EACP;AAEA,WAASc,GAAiBC,GAAwC;AAChE,UAAMC,IAAOD,EAAE,OAAO,QAAQ,CAAC,KAAK;AAEpC,QADAvB,EAAiB,IAAI,GACjBwB,KAAQjC,KAAgBiC,EAAK,OAAOjC,EAAa,UAAU;AAC7D,MAAAG,EAAgB,IAAI,GACpBM,EAAiB,EAAE,MAAM,aAAa,UAAUT,EAAa,UAAU,GACnEU,EAAa,YAASA,EAAa,QAAQ,QAAQ;AACvD;AAAA,IACF;AACA,IAAAP,EAAgB8B,CAAI;AAAA,EACtB;AAEA,iBAAeC,KAAe;AAC5B,QAAI,CAAChC,KAAgBE,EAAY;AACjC,IAAAC,EAAc,EAAI,GAClBI,EAAiB,IAAI;AAErB,QAAI0B,IAASnC;AACb,QAAI,CAACmC,GAAQ;AACX,YAAM1L,IAAM,MAAM4K,GAAA;AAClB,UAAI,CAAC5K,EAAI,IAAI;AAAE,QAAA4J,EAAc,EAAK,GAAGI,EAAiBhK,EAAI,OAAO;AAAG;AAAA,MAAQ;AAC5E,MAAA0L,IAAS1L,EAAI,OACbwJ,EAAgBkC,CAAM;AAAA,IACxB;AACA,QAAIjC,EAAa,OAAOiC,EAAO,UAAU;AACvC,MAAA9B,EAAc,EAAK,GACnBI,EAAiB,EAAE,MAAM,aAAa,UAAU0B,EAAO,UAAU;AACjE;AAAA,IACF;AAEA,UAAMC,IAAW,MAAMC,GAAwBnC,GAAciC,GAAQ,CAACG,GAAMC,GAAOC,OAAU;AAC3F,MAAAjC,EAAkB,EAAE,MAAA+B,GAAM,OAAAC,GAAO,OAAAC,GAAA,CAAO;AAAA,IAC1C,CAAC;AACD,QAAI,CAACJ,EAAS,IAAI;AAChB,MAAA/B,EAAc,EAAK,GACnBE,EAAkB,IAAI,GACtBE,EAAiB2B,EAAS,OAAO;AACjC;AAAA,IACF;AAEA,UAAMK,IAAW,MAAMC,GAAuBN,EAAS,MAAM,QAAQlC,EAAa,IAAI;AAGtF,QAFAG,EAAc,EAAK,GACnBE,EAAkB,IAAI,GAClB,CAACkC,EAAS,IAAI;AAAE,MAAAhC,EAAiBgC,EAAS,OAAO;AAAG;AAAA,IAAQ;AAEhE,IAAA7L,EAAM,eAAe,mBAAmB,EAAE,IAAI6L,EAAS,MAAM,IAAI,GACjEtC,EAAgB,IAAI,GAChBO,EAAa,YAASA,EAAa,QAAQ,QAAQ,KACvDb,EAAQ,OAAQ,CAAC4C,EAAS,OAAO,IAAI3J,KAAQ,CAAA,GAAI,OAAO,CAAAgE,MAAKA,EAAE,OAAO2F,EAAS,MAAM,EAAE,CAAC,CAAC,GACpFxB,EAAA;AAAA,EACP;AAEA,QAAM0B,KAAenC,IAChB,cAAcA,KAAiBA,EAAc,SAAS,wBAAwBA,EAAc,SAAS,kBACpGoC,GAAmBpC,CAA8B,IACjDjD,EAAuBiD,CAAkC,IAC3D;AAEJ,SACE,gBAAA/C;AAAA,IAACoF;AAAA,IAAA;AAAA,MACC,MAAA9D;AAAA,MACA,SAAAC;AAAA,MACA,QACE,gBAAAvB,EAAAqF,GAAA,EACE,UAAA;AAAA,QAAA,gBAAApF,EAAC,QAAA,EAAK,WAAU,oDAAmD,UAAA,aAAS;AAAA,QAC5E,gBAAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,SAAS,MAAM;AAAE,cAAKuD,EAAA;AAAA,YAAW;AAAA,YACjC,cAAW;AAAA,YACX,WAAW,GAAG8B,EAAU;AAAA,YAExB,UAAA,gBAAArF,EAACc,IAAA,EAAK,MAAK,UAAA,CAAU;AAAA,UAAA;AAAA,QAAA;AAAA,MACvB,GACF;AAAA,MAGF,UAAA;AAAA,QAAA,gBAAAf,EAAC,OAAA,EAAI,WAAU,kDACb,UAAA;AAAA,UAAA,gBAAAC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,OAAO9B;AAAA,cACP,UAAU,CAAAoG,MAAKV,GAAmBU,EAAE,OAAO,KAAK;AAAA,cAChD,WAAW,CAAAA,MAAK;AAAE,gBAAIA,EAAE,QAAQ,WAAW,CAACnB,KAAee,GAAA;AAAA,cAAgB;AAAA,cAC3E,cAAW;AAAA,cACX,aAAY;AAAA,cACZ,WAAU;AAAA,YAAA;AAAA,UAAA;AAAA,UAEXf,KACC,gBAAApD,EAAC,OAAA,EAAI,WAAU,yBAAwB,eAAY,iBACjD,UAAA;AAAA,YAAA,gBAAAC;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,OAAOvF;AAAA,gBACP,UAAU,CAAA6J,MAAK9C,EAAU8C,EAAE,OAAO,KAAK;AAAA,gBACvC,MAAK;AAAA,gBACL,cAAW;AAAA,gBACX,aAAY;AAAA,gBACZ,WAAU;AAAA,cAAA;AAAA,YAAA;AAAA,YAEZ,gBAAAtE,EAAC,YAAA,EAAS,IAAG,4BACV,UAAA4B,EAAc,IAAI,CAAArH,MAAQ,gBAAAyF,EAAC,UAAA,EAAkB,OAAOzF,EAAA,GAAbA,CAAmB,CAAE,GAC/D;AAAA,YACA,gBAAAyF;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,OAAOQ;AAAA,gBACP,UAAU,CAAA8D,MAAK;AAAE,kBAAA3C,EAAgB,EAAI,GAAGF,EAAS6C,EAAE,OAAO,KAAK;AAAA,gBAAG;AAAA,gBAClE,cAAW;AAAA,gBACX,aAAY;AAAA,gBACZ,WAAU;AAAA,cAAA;AAAA,YAAA;AAAA,UACZ,GACF;AAAA,UAEF,gBAAAvE,EAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,YAAA,gBAAAC;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,SAAS,MAAM;AAAE,kBAAKkE,GAAA;AAAA,gBAAgB;AAAA,gBACtC,UAAUpC,KAAQ,CAAC5D,EAAO,UAAUoF,MAAcH,KAAY,CAACa;AAAA,gBAC/D,cAAW;AAAA,gBACX,WAAU;AAAA,gBAET,cAAO,gBAAgB;AAAA,cAAA;AAAA,YAAA;AAAA,YAEzBhC,uBACE,QAAA,EAAK,eAAY,kBAAiB,WAAU,oEAC1C,UAAAnC,EAAuBmC,CAAO,EAAA,CACjC;AAAA,UAAA,EAAA,CAEJ;AAAA,QAAA,GACF;AAAA,QAEA,gBAAAhC;AAAA,UAAC1B;AAAA,UAAA;AAAA,YACC,OAAOJ;AAAA,YACP,aAAa,CAAAoH,MAAOnD,EAAQ,CAAA/G,MAAQ,CAACkK,GAAK,IAAIlK,KAAQ,CAAA,GAAI,OAAO,CAAAgE,MAAKA,EAAE,OAAOkG,EAAI,EAAE,CAAC,CAAC;AAAA,UAAA;AAAA,QAAA;AAAA,QAGzF,gBAAAvF,EAAC,OAAA,EAAI,WAAU,kDACb,UAAA;AAAA,UAAA,gBAAAA,EAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,YAAA,gBAAAC;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,KAAKgD;AAAA,gBACL,MAAK;AAAA,gBACL,UAAUqB;AAAA,gBACV,cAAW;AAAA,gBACX,WAAU;AAAA,cAAA;AAAA,YAAA;AAAA,YAEZ,gBAAArE;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,SAAS,MAAM;AAAE,kBAAKwE,GAAA;AAAA,gBAAgB;AAAA,gBACtC,UAAU,CAAChC,KAAgBE;AAAA,gBAC3B,cAAW;AAAA,gBACX,WAAU;AAAA,gBAET,UAAAA,IACIE,GAAgB,UAAU,eAAe,gBAAgB,eAC1D;AAAA,cAAA;AAAA,YAAA;AAAA,UACN,GACF;AAAA,UACA,gBAAA5C,EAAC,QAAA,EAAK,WAAU,+BAA8B,eAAY,cACvD,UAAAsC,IAAe,OAAOiD,GAAsBjD,EAAa,QAAQ,CAAC,KAAK,IAC1E;AAAA,UACC2C,MACC,gBAAAjF,EAAC,QAAA,EAAK,eAAY,kBAAiB,WAAU,oEAAoE,UAAAiF,GAAA,CAAa;AAAA,QAAA,GAElI;AAAA,QAEA,gBAAAlF,EAAC,OAAA,EAAI,WAAU,wBAAuB,eAAY,kBAC/C,UAAA;AAAA,UAAAqC,uBACE,OAAA,EAAI,WAAU,0EAA0E,UAAAvC,EAAuBuC,CAAW,GAAE;AAAA,UAE9HF,GAAM,IAAI,CAAAhB,MAAQ,gBAAAlB,EAACiB,MAA4B,MAAAC,EAAA,GAATA,EAAK,EAAgB,CAAE;AAAA,QAAA,EAAA,CAChE;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;AC1YA,MAAMsE,KAA0C,EAAE,WAAWC,GAAA;AAE7D,SAASC,KAAW;AAClB,EAAAC,GAAe,IAAI,GACnBzM,EAAM,QAAQ,eAAe,EAAE,QAAQ,MAAM;AAC/C;AAcO,SAAS0M,GAAS,EAAE,QAAAC,KAAkC;AAC3D,QAAMC,IAAeC,GAAY,CAAA,MAAK,EAAE,YAAY,GAC9CC,IAAQC,GAAA;AAEd,WAASC,EAAMC,GAAgB;AAC7B,IAAAR,GAAeQ,CAAM,GACrBjN,EAAM,QAAQ,eAAe,EAAE,QAAAiN,EAAA,CAAQ,GACvCN,EAAA;AAAA,EACF;AACA,WAASO,IAAO;AACd,IAAAV,GAAA,GACAG,EAAA;AAAA,EACF;AAEA,SACE,gBAAA7F,EAAC,OAAA,EAAI,WAAU,4BACb,UAAA,gBAAAA,EAAC,SAAI,WAAU,0BACZ,UAAAgG,EAAM,IAAI,CAAAK,MAAK;AACd,UAAMC,IAAWR,MAAiBO,EAAE,IAC9BE,IAAQF,EAAE,SAASb,GAAgBa,EAAE,EAAE;AAC7C,WACE,gBAAAtG;AAAA,MAAC;AAAA,MAAA;AAAA,QAEC,SAAS,MAAOuG,IAAWF,MAASF,EAAMG,EAAE,EAAE;AAAA,QAC9C,WAAW,kGACTC,IACI,+CACA,oCACN;AAAA,QACA,OAAOA,IAAW,QAAQD,EAAE,IAAI,KAAK,SAASA,EAAE,IAAI;AAAA,QAEnD,UAAA;AAAA,UAAAE,IACC,gBAAAvG;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,KAAKuG;AAAA,cACL,KAAKF,EAAE;AAAA,cACP,WAAU;AAAA,YAAA;AAAA,UAAA,IAGZ,gBAAArG,EAAC,OAAA,EAAI,WAAU,6BAAA,CAA6B;AAAA,UAG9C,gBAAAA,EAAC,OAAA,EAAI,WAAU,yGACb,UAAA,gBAAAA,EAAC,SAAI,WAAU,gDAAgD,UAAAqG,EAAE,KAAA,CAAK,EAAA,CACxE;AAAA,UACCC,KACC,gBAAAtG,EAAC,QAAA,EAAK,WAAU,yGAAwG,UAAA,SAAA,CAExH;AAAA,QAAA;AAAA,MAAA;AAAA,MAzBGqG,EAAE;AAAA,IAAA;AAAA,EA6Bb,CAAC,GACH,GACF;AAEJ;AAGO,SAASG,GAAe,EAAE,QAAAX,KAAkC;AAEjE,SADqBE,GAAY,CAAAxI,MAAKA,EAAE,YAAY,IAGlD,gBAAAyC,EAAC,UAAA,EAAO,SAAS,MAAM;AAAE,IAAA0F,GAAA,GAAYG,EAAA;AAAA,EAAU,GAAG,WAAU,wCAAuC,UAAA,YAAA,CAEnG,IAJwB;AAM5B;ACxEA,MAAMhL,IAAa,CAACE,MAClBA,EAAK,SAAS,aAAaA,EAAK,SAAS,CAAA,GACrCI,KAAe,CACnBJ,GACAC,MACID,EAAK,SAAS,aAAa,EAAE,GAAGA,GAAM,QAAQC,EAAA,IAA6BD;AAEjF,SAAwB0L,GAAY,EAAE,MAAApF,GAAM,SAAAC,KAA6B;AACvE,QAAM,CAACoF,GAASC,CAAU,IAAI/H,EAAS,EAAK,GACtC,CAACgI,GAAOC,CAAQ,IAAIjI,EAAwB,IAAI,GAChD,CAAChE,GAAMkM,CAAO,IAAIlI,EAAoB,CAAA,CAAE,GACxC,CAACmI,GAAQC,CAAS,IAAIpI,EAAS,EAAE,GAMjC,CAACqI,GAASC,CAAU,IAAItI,EAAS,EAAK,GACtC,CAACuI,GAAMC,CAAO,IAAIxI,EAAqB,OAAO,GAC9CyI,IAAUtB,GAAY,CAAAxI,MAAKA,EAAE,SAAS,OAAO,GAI7C+J,IAAYC,GAAA,GAIZC,IAAYhE,GAAY,MAAMxH,GAAgBzD,CAAY,EAC7D,KAAK,CAAAiE,MAAU;AAOd,IAAAsK,EAAQ,CAAA1L,MAAQF,GAAesB,GAAQ7B,GAAiBS,GAAMP,CAAU,GAAGA,GAAYM,EAAY,CAAC,GAEhGlC,EAAA,KAAuBG,GAAA;AAAA,EAC7B,CAAC,EACA,MAAM,CAACkL,MAAe;AACrB,IAAAuC,EAAUvC,aAAa,SAASA,EAAE,WAAY,eAAe;AAAA,EAC/D,CAAC,EACA,QAAQ,MAAMqC,EAAW,EAAK,CAAC,GAAG,EAAE,GASjCc,IAAWC,GAAA;AACjB,EAAAzI,EAAU,MAAM;AACd,QAAI,CAACwI,EAAU;AACf,QAAI/D,IAAO;AACX,WAAA1H,GAAgBzD,CAAY,EAAE;AAAA,MAC5B,CAAAiE,MAAU;AACR,QAAIkH,KAAMoD,EAAQ,CAAA1L,MAAQF,GAAesB,GAAQ7B,GAAiBS,GAAMP,CAAU,GAAGA,GAAYM,EAAY,CAAC;AAAA,MAChH;AAAA,MACA,CAACpB,MAAiBC,EAAK,WAAW,2BAA2B+B,EAAYhC,CAAG,CAAC;AAAA,IAAA,GAExE,MAAM;AAAE,MAAA2J,IAAO;AAAA,IAAO;AAAA,EAC/B,GAAG,CAAC+D,CAAQ,CAAC;AAWb,QAAM,CAACE,GAASC,CAAU,IAAIhJ,EAAS,EAAK;AAC5C,EAAIyC,MAASsG,MACXC,EAAWvG,CAAI,GACXA,MACF+F,EAAQ,OAAO,GACfN,EAAQ,CAAAe,MAAKnM,GAAwBmM,GAAGhN,GAAYM,EAAY,CAAC,GACjEwL,EAAW,EAAI,GAAGE,EAAS,IAAI,KAGnC5H,EAAU,MAAM;AACd,IAAIoC,KAAWmG,EAAA;AAAA,EACjB,GAAG,CAACnG,GAAMmG,CAAS,CAAC;AAEpB,WAASM,EAAoBT,GAAiB;AAC5C,IAAAU,GAASV,CAAO,GAChBnO,EAAM,WAAW,gBAAgB,EAAE,SAAAmO,GAAS,GAC5C/F,EAAA;AAAA,EACF;AACA,WAAS0G,EAAqBC,GAAoB;AAChD,IAAAC,GAAiBD,CAAU,GAC3B/O,EAAM,WAAW,iBAAiB,EAAE,YAAA+O,EAAA,CAAY,GAChD3G,EAAA;AAAA,EACF;AAOA,WAAS6G,IAAe;AACtB,IAAAC,GAAA,GACAlP,EAAM,YAAY,qCAAqC,GACvDoI,EAAA;AAAA,EACF;AACA,WAAS+G,EAAaC,GAAYC,GAAY;AAC5C,IAAAzB,EAAQ,CAAAe,MAAKA,EAAE,IAAI,CAACW,GAAE9L,MAAMA,MAAI4L,KAAM,EAAE,YAAYE,KAAKA,IAAK,EAAE,GAAGA,GAAG,QAAQA,EAAE,OAAO,IAAI,CAACjL,GAAEkL,MAAKA,MAAIF,IAAIhL,IAAK,EAAE,GAAGA,GAAG,UAAU,CAACA,EAAE,SAAA,CAAW,EAAA,CAAI,CAAC;AAAA,EACvJ;AACA,WAASmL,EAAeJ,GAAY;AAClC,IAAAxB,EAAQ,OAAKe,EAAE,IAAI,CAACW,GAAE9L,MAAKA,MAAI4L,IAAIE,IAAK,EAAE,GAAGA,GAAG,UAAU,CAACA,EAAE,SAAA,CAAW,CAAC;AAAA,EAC3E;AACA,WAASG,IAAa;AACpB,IAAAxP,GAAoB,GACpBwN,EAAW,EAAI,GAAGE,EAAS,IAAI,GAC1BW,EAAA;AAAA,EACP;AAEA,QAAMoB,IAAexF,GAAQ,MAAM;AACjC,QAAI,CAAC2D,EAAO,KAAA,EAAQ,QAAOnM;AAC3B,UAAMmD,IAAIgJ,EAAO,YAAA;AACjB,WAAOnM,EAAK,IAAI,CAAAG,MAAQ;AACtB,UAAIA,EAAK,SAAS,qBAAqB;AACrC,cAAM6B,IAAY7B,EAAK,UAAU,OAAO,CAAA8N,MAAKA,EAAE,KAAK,YAAA,EAAc,SAAS9K,CAAC,CAAC;AAC7E,eAAInB,EAAU,SAAe,EAAE,GAAG7B,GAAM,WAAA6B,GAAW,UAAU,GAAA,IACzD7B,EAAK,KAAK,YAAA,EAAc,SAASgD,CAAC,IAAU,EAAE,GAAGhD,GAAM,UAAU,GAAA,IAC9D;AAAA,MACT;AACA,YAAM+N,IAAiB/N,EAAK,OAAO,IAAI,CAAAwC,MAAK;AAC1C,cAAMwL,IAAMxL,EAAE,SAAS,OAAO,CAAA+G,MAAKA,EAAE,KAAK,YAAA,EAAc,SAASvG,CAAC,CAAC;AACnE,eAAIgL,EAAI,SAAe,EAAE,GAAGxL,GAAG,UAAUwL,GAAK,UAAU,GAAA,IACpDxL,EAAE,KAAK,YAAA,EAAc,SAASQ,CAAC,IAAU,EAAE,GAAGR,GAAG,UAAU,GAAA,IACxD;AAAA,MACT,CAAC,EAAE,OAAO,OAAO;AACjB,aAAIuL,EAAe,SAAe,EAAE,GAAG/N,GAAM,QAAQ+N,GAAgB,UAAU,GAAA,IAC3E/N,EAAK,KAAK,YAAA,EAAc,SAASgD,CAAC,IAAU,EAAE,GAAGhD,GAAM,UAAU,GAAA,IAC9D;AAAA,IACT,CAAC,EAAE,OAAO,OAAO;AAAA,EACnB,GAAG,CAACH,GAAMmM,CAAM,CAAC;AAEjB,SACE,gBAAAhH,EAAAqF,GAAA,EACA,UAAA;AAAA,IAAA,gBAAApF,EAACoB,MAAe,MAAM6F,GAAS,SAAS,MAAMC,EAAW,EAAK,GAAG;AAAA,IACjE,gBAAAlH;AAAA,MAACmF;AAAA,MAAA;AAAA,QACC,MAAM9D,KAAQ,CAAC4F;AAAA,QACf,SAAA3F;AAAA,QACA,QACE,gBAAAvB,EAAAqF,GAAA,EAIE,UAAA;AAAA,UAAA,gBAAApF,EAAC,OAAA,EAAI,WAAU,WAAU,MAAK,WAAU,cAAW,gBAC/C,UAAA,CAAC,SAAS,MAAM,EAAY,IAAI,CAAAxC,MAChC,gBAAAwC;AAAA,YAAC;AAAA,YAAA;AAAA,cAEC,MAAK;AAAA,cACL,iBAAemH,MAAS3J;AAAA,cACxB,SAAS,MAAM4J,EAAQ5J,CAAC;AAAA,cACxB,WAAU;AAAA,cAET,UAAAA,MAAM,UAAU,UAAU;AAAA,YAAA;AAAA,YANtBA;AAAA,UAAA,CAQR,GACH;AAAA,UACC2J,MAAS,UAAU,gBAAAnH,EAACwG,IAAA,EAAe,QAAQlF,GAAS;AAAA,UACpD6F,MAAS,WAAW,gBAAApH,EAAAqF,GAAA,EACpB,UAAA;AAAA,YAAAkC,KACC,gBAAAtH;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,SAAS,MAAMkH,EAAW,EAAI;AAAA,gBAC9B,cAAW;AAAA,gBACX,WAAW7B;AAAA,gBACZ,UAAA;AAAA,cAAA;AAAA,YAAA;AAAA,YAIFgC,KACC,gBAAArH;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,SAASmI;AAAA,gBACT,OAAM;AAAA,gBACN,WAAU;AAAA,gBACX,UAAA;AAAA,cAAA;AAAA,YAAA;AAAA,YAQH,gBAAApI,EAAC,YAAO,SAAS4I,GAAY,UAAUjC,GAAS,WAAW,GAAGrB,EAAU,qCACrE,UAAA;AAAA,cAAAqB,KAAW,gBAAA1G,EAAC,QAAA,EAAK,WAAU,iGAAA,CAAiG;AAAA,cAC5H0G,IAAU,eAAe;AAAA,YAAA,GAC5B;AAAA,YACA,gBAAA1G,EAAC,SAAA,EAAM,OAAO+G,GAAQ,UAAU,CAAAzC,MAAG0C,EAAU1C,EAAE,OAAO,KAAK,GAAG,aAAY,UAAS,WAAU,0GAAA,CAA0G;AAAA,UAAA,EAAA,CACvM;AAAA,QAAA,GACF;AAAA,QAEF,QAAQrL,EAAA,IACN,gBAAA+G,EAAC,OAAA,EAAI,WAAU,2EACb,UAAA,gBAAAA,EAAC,QAAA,EAAK,WAAU,cAAa,UAAA,UAAA,CAAO,GACtC,IACE;AAAA,QAED,gBAAS,SAAS,gBAAAA,EAAC4F,MAAS,QAAQtE,EAAA,CAAS,IAAK,gBAAAvB,EAAAqF,GAAA,EAClD,UAAA;AAAA,UAAAsB,KAAW,CAAC9L,EAAK,4BAAW,OAAA,EAAI,WAAU,qCAAoC,UAAA,oBAAgB;AAAA,UAC9FgM,KAAS,gBAAA5G,EAAC,OAAA,EAAI,WAAU,iEAAiE,UAAA4G,GAAM;AAAA,UAChG,gBAAA7G,EAAC,OAAA,EAAI,WAAU,qCACZ,UAAA;AAAA,YAAA6I,EAAa,IAAI,CAAC7N,GAAMuN,MACvB,gBAAAvI,EAAC,OAAA,EAAoB,WAAU,aAG7B,UAAA;AAAA,cAAA,gBAAAA;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,WAAU;AAAA,kBACV,iBAAehF,EAAK;AAAA,kBACpB,iBAAeA,EAAK,WAAW,SAAS;AAAA,kBACxC,SAAS,MAAI2N,EAAeJ,CAAE;AAAA,kBAE9B,UAAA;AAAA,oBAAA,gBAAAtI,EAAC,QAAA,EAAK,WAAU,oFAAoF,UAAAjF,EAAK,MAAK;AAAA,sCAC7G,QAAA,EAAK,WAAU,0BAA0B,UAAAA,EAAK,WAAU,MAAI,IAAA,CAAI;AAAA,kBAAA;AAAA,gBAAA;AAAA,cAAA;AAAA,cAElEA,EAAK,YAAYA,EAAK,SAAS,uBAC9B,gBAAAgF,EAAC,OAAA,EAAI,WAAU,kBACZ,UAAA;AAAA,gBAAAhF,EAAK,UAAU,IAAI,CAAA8N,MAClB,gBAAA9I,EAAC,UAAA,EAA0B,SAAS,MAAIiI,EAAqBa,EAAE,UAAU,GAAG,WAAU,oFACpF,UAAA;AAAA,kBAAA,gBAAA7I,EAAC,QAAA,EAAK,WAAU,2EAA2E,UAAA6I,EAAE,MAAK;AAAA,kBAClG,gBAAA7I,EAAC,QAAA,EAAK,WAAU,sIAAqI,UAAA,OAAA,CAAI;AAAA,gBAAA,KAF9I6I,EAAE,UAGf,CACD;AAAA,gBACA9N,EAAK,UAAU,WAAW,uBAAM,OAAA,EAAI,WAAU,sCAAqC,UAAA,iBAAA,CAAc;AAAA,cAAA,GACpG;AAAA,cAEDA,EAAK,YAAYA,EAAK,SAAS,cAC9B,gBAAAgF,EAAC,OAAA,EAAI,WAAU,kBACZ,UAAA;AAAA,gBAAAhF,EAAK,OAAO,IAAI,CAACwC,GAAGgL,wBAClB,OAAA,EACC,UAAA;AAAA,kBAAA,gBAAAxI,EAAC,OAAA,EAAI,WAAU,8EAA6E,SAAS,MAAIsI,EAAaC,GAAIC,CAAE,GAC1H,UAAA;AAAA,oBAAA,gBAAAvI,EAAC,QAAA,EAAK,WAAU,uBAAuB,UAAAzC,EAAE,MAAK;AAAA,sCAC7C,QAAA,EAAK,WAAU,0BAA0B,UAAAA,EAAE,WAAU,MAAI,IAAA,CAAI;AAAA,kBAAA,GAChE;AAAA,kBACCA,EAAE,YACD,gBAAAwC,EAAC,OAAA,EAAI,WAAU,mDACZ,UAAA;AAAA,oBAAAxC,EAAE,SAAS,IAAI,CAAAyL,MACd,gBAAAjJ,EAAC,UAAA,EAAwB,SAAS,MAAI+H,EAAoBkB,EAAG,OAAO,GAAG,WAAU,oFAC/E,UAAA;AAAA,sBAAA,gBAAAhJ,EAAC,QAAA,EAAK,WAAU,2EAA2E,UAAAgJ,EAAG,MAAK;AAAA,sBACnG,gBAAAhJ,EAAC,QAAA,EAAK,WAAU,sIAAqI,UAAA,OAAA,CAAI;AAAA,oBAAA,KAF9IgJ,EAAG,OAGhB,CACD;AAAA,oBACAzL,EAAE,SAAS,WAAW,uBAAM,OAAA,EAAI,WAAU,sCAAqC,UAAA,gBAAA,CAAa;AAAA,kBAAA,EAAA,CAC/F;AAAA,gBAAA,KAdMA,EAAE,IAgBZ,CACD;AAAA,gBACAxC,EAAK,OAAO,WAAW,uBAAM,OAAA,EAAI,WAAU,sCAAqC,UAAA,UAAA,CAAO;AAAA,cAAA,EAAA,CAC1F;AAAA,YAAA,KA7CMA,EAAK,IA+Cf,CACD;AAAA,YACA,CAAC2L,KAAW,CAACkC,EAAa,UAAU,gBAAA5I,EAAC,OAAA,EAAI,WAAU,iDAAgD,UAAA,aAAA,CAAU;AAAA,UAAA,EAAA,CAChH;AAAA,QAAA,EAAA,CACA;AAAA,MAAA;AAAA,IAAA;AAAA,EACJ,GACA;AAEJ;"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as e, jsxs as i } from "react/jsx-runtime";
|
|
2
2
|
import P, { useState as y, useEffect as _ } from "react";
|
|
3
|
-
import { h as v, s as O, j as L, O as T, aN as D, aO as I, aP as C, aL as A, d as R, aQ as V, am as B, aR as U, U as W, aS as F, aD as j, aB as K } from "./index-
|
|
4
|
-
import { d as $, S as M } from "./Room-
|
|
3
|
+
import { h as v, s as O, j as L, O as T, aN as D, aO as I, aP as C, aL as A, d as R, aQ as V, am as B, aR as U, U as W, aS as F, aD as j, aB as K } from "./index-HIbKUgoo.js";
|
|
4
|
+
import { d as $, S as M } from "./Room-DleCsQL0.js";
|
|
5
5
|
const z = 3e3, G = {
|
|
6
6
|
super_upvote: 4,
|
|
7
7
|
upvote: 3,
|
|
@@ -316,4 +316,4 @@ function ne() {
|
|
|
316
316
|
export {
|
|
317
317
|
ne as default
|
|
318
318
|
};
|
|
319
|
-
//# sourceMappingURL=PlaylistPanel-
|
|
319
|
+
//# sourceMappingURL=PlaylistPanel-2V6H_3Gw.js.map
|