diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/video-app.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/video-app.js | 81 |
1 files changed, 63 insertions, 18 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js index c0102e5..143cf4a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js @@ -145,6 +145,7 @@ const _thumbBlobCache = new Map(); function MediaThumb({ thumbHash, transportRef, gekRef, alt, cls = 'video-thumb', onReady, emptyIcon = 'video', + reloadKey, }) { const [blobUrl, setBlobUrl] = useState(() => _thumbBlobCache.get(thumbHash) || null); const [retryToken, setRetryToken] = useState(0); @@ -188,7 +189,12 @@ function MediaThumb({ } })(); return () => { cancelled = true; }; - }, [thumbHash, retryToken]); + // `reloadKey` — bumped by the caller when the transport behind `transportRef` + // was replaced (a search-page pool reconnect). Without it, a thumb whose + // first fetch hit a not-yet-connected transport and bailed above would stay + // an empty placeholder for good, since neither `thumbHash` nor `retryToken` + // changes when only the connection does. + }, [thumbHash, retryToken, reloadKey]); if (!blobUrl) return html`<div class="${cls} video-thumb-empty"><${Icon} name=${emptyIcon} /></div>`; return html`<img class=${cls} src=${blobUrl} alt=${alt || ''} loading="lazy" />`; @@ -219,7 +225,13 @@ function bumpThumbGeneration() { // would otherwise storm TMDB with its raw filename), so the client must // refetch once the enriched fields arrive on an index delta — the fileId // (a content hash) never changes, so nothing else would trigger it. -function useMediaMeta(transportRef, fileId, active, enrichSig) { +// `reloadKey` — an optional value the caller bumps when the transport behind +// `transportRef` was swapped out (search-page's connection pool evicted and +// later rebuilt this group's connection). The first fetch after a mount onto a +// not-yet-connected transport returns early below; nothing else here would ever +// re-run it, so the tile would sit on a spinner for good. Threading the group's +// `_connGen` in as `reloadKey` is what unsticks it. +function useMediaMeta(transportRef, fileId, active, enrichSig, reloadKey) { const [meta, setMeta] = useState(null); const [refetchToken, setRefetchToken] = useState(0); @@ -244,7 +256,7 @@ function useMediaMeta(transportRef, fileId, active, enrichSig) { } catch { if (!cancelled) setMeta({ confidence: 0 }); } })(); return () => { cancelled = true; }; - }, [fileId, active, refetchToken, enrichSig]); + }, [fileId, active, refetchToken, enrichSig, reloadKey]); return meta; } @@ -283,10 +295,24 @@ function useSeasonMeta(transportRef, tmdbId, season, active) { // ── Mode A: poster grid ────────────────────────────────────────────────────── -function PosterCard({ title, subtitle, repEntry, transportRef, gekRef, onOpen, groupKey, onMetaResolved }) { +function PosterCard({ + title, subtitle, repEntry, transportRef, gekRef, onOpen, groupKey, onMetaResolved, onNeedConn, +}) { const tRef = repEntry._tRef || transportRef; const gRef = repEntry._gRef || gekRef; - const meta = useMediaMeta(tRef, repEntry.id, true, repEntry.display_title || ''); + + // Search view only: ask for this group's connection the moment the tile + // actually mounts (it is inside a LazyTile, so that means "scrolled near"). + // Groups the pre-connect walk did not reach light up here instead of never. + // A no-op in a single-group page — there is no groupId and no onNeedConn. + useEffect(() => { + if (onNeedConn && repEntry.groupId) { + Promise.resolve(onNeedConn(repEntry.groupId)).catch(() => {}); + } + }, [onNeedConn, repEntry.groupId]); + + const meta = useMediaMeta( + tRef, repEntry.id, true, repEntry.display_title || '', repEntry._connGen); const confident = Boolean(meta && meta.confidence && meta.tmdb_id); const metaReady = meta !== null; @@ -339,6 +365,7 @@ function PosterCard({ title, subtitle, repEntry, transportRef, gekRef, onOpen, g ${metaReady && html` <${MediaThumb} thumbHash=${posterHash} alt=${title} cls="video-poster" transportRef=${tRef} gekRef=${gRef} + reloadKey=${repEntry._connGen} onReady=${handleImageReady} /> `} </div> @@ -588,7 +615,8 @@ function VideoDetailModal({ <button class="video-episode-row" key=${ep.id} onClick=${() => onPlay(ep)}> <${LazyTile} cls="video-episode-thumb-slot"> <${MediaThumb} thumbHash=${ep.thumb_hash} alt=${ep.display_title || ep.name} - cls="video-episode-thumb" transportRef=${ep._tRef || transportRef} gekRef=${ep._gRef || gekRef} /> + cls="video-episode-thumb" transportRef=${ep._tRef || transportRef} gekRef=${ep._gRef || gekRef} + reloadKey=${ep._connGen} /> </${LazyTile}> <span class="video-episode-label"> S${ep.season}E${String(ep.episode).padStart(2, '0')} @@ -615,7 +643,9 @@ function VideoDetailModal({ `; } -function PosterGrid({ movies, shows, transportRef, gekRef, onPreview, tmdbEnabled, isNodeAdmin }) { +function PosterGrid({ + movies, shows, transportRef, gekRef, onPreview, tmdbEnabled, isNodeAdmin, onNeedConn, +}) { const [detail, setDetail] = useState(null); // { title, repEntry, show? } // raw (per-folder-parsed-title) show title -> its own resolved media_meta_resp. const [metaByGroup, setMetaByGroup] = useState({}); @@ -673,7 +703,8 @@ function PosterGrid({ movies, shows, transportRef, gekRef, onPreview, tmdbEnable const detailGRef = detail && detail.repEntry._gRef ? detail.repEntry._gRef : gekRef; const detailMeta = useMediaMeta( detailTRef, detail ? detail.repEntry.id : null, !!detail, - detail ? (detail.repEntry.display_title || '') : ''); + detail ? (detail.repEntry.display_title || '') : '', + detail ? detail.repEntry._connGen : 0); return html` <div class="video-grid"> @@ -683,6 +714,7 @@ function PosterGrid({ movies, shows, transportRef, gekRef, onPreview, tmdbEnable subtitle=${formatDuration(e.duration)} repEntry=${e} groupKey=${`movie:${e.id}`} transportRef=${transportRef} gekRef=${gekRef} + onNeedConn=${onNeedConn} onOpen=${() => (tmdbEnabled // With TMDB off there is nothing the detail modal would show // for a movie (no overview, no season list to pick from, @@ -716,6 +748,7 @@ function PosterGrid({ movies, shows, transportRef, gekRef, onPreview, tmdbEnable repEntry=${repEntry} groupKey=${s.title} onMetaResolved=${handleMetaResolved} + onNeedConn=${onNeedConn} transportRef=${transportRef} gekRef=${gekRef} onOpen=${() => openDetail(s.title, repEntry, s)} /> </${LazyTile}> @@ -740,9 +773,16 @@ function PosterGrid({ movies, shows, transportRef, gekRef, onPreview, tmdbEnable // is actually inside a season group (`seasonContext` set); a real, // distinct per-episode title (a show that *does* carry one) still wins // over the generic "Episode N" label. -function FlatMovieRow({ entry, transportRef, gekRef, onPreview, seasonContext }) { +function FlatMovieRow({ entry, transportRef, gekRef, onPreview, seasonContext, onNeedConn }) { const tRef = entry._tRef || transportRef; const gRef = entry._gRef || gekRef; + + useEffect(() => { + if (onNeedConn && entry.groupId) { + Promise.resolve(onNeedConn(entry.groupId)).catch(() => {}); + } + }, [onNeedConn, entry.groupId]); + const isEpisode = seasonContext && entry.season != null && entry.episode != null; const hasOwnTitle = entry.display_title && entry.display_title !== seasonContext; const label = isEpisode @@ -754,7 +794,8 @@ function FlatMovieRow({ entry, transportRef, gekRef, onPreview, seasonContext }) <div class="video-flat-row" onClick=${() => onPreview(entry)}> <${LazyTile} cls="video-flat-thumb-slot"> <${MediaThumb} thumbHash=${entry.thumb_hash} alt=${entry.display_title || entry.name} - cls="video-flat-thumb" transportRef=${tRef} gekRef=${gRef} /> + cls="video-flat-thumb" transportRef=${tRef} gekRef=${gRef} + reloadKey=${entry._connGen} /> </${LazyTile}> <div class="video-flat-info"> <div class="video-flat-title">${label}</div> @@ -771,7 +812,7 @@ function FlatMovieRow({ entry, transportRef, gekRef, onPreview, seasonContext }) `; } -function FlatShowFolder({ show, transportRef, gekRef, onPreview }) { +function FlatShowFolder({ show, transportRef, gekRef, onPreview, onNeedConn }) { const [open, setOpen] = useState(false); return html` <div class="video-flat-folder"> @@ -790,7 +831,8 @@ function FlatShowFolder({ show, transportRef, gekRef, onPreview }) { </div> ${s.episodes.map((ep) => html` <${FlatMovieRow} key=${ep.id} entry=${ep} seasonContext=${show.title} - transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview} /> + transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview} + onNeedConn=${onNeedConn} /> `)} </div> `)} @@ -798,7 +840,7 @@ function FlatShowFolder({ show, transportRef, gekRef, onPreview }) { `; } -function FlatList({ movies, shows, transportRef, gekRef, onPreview }) { +function FlatList({ movies, shows, transportRef, gekRef, onPreview, onNeedConn }) { const items = [ ...movies.map((e) => ({ key: e.display_title || e.name, kind: 'movie', entry: e })), ...shows.map((s) => ({ key: s.title, kind: 'show', show: s })), @@ -808,9 +850,11 @@ function FlatList({ movies, shows, transportRef, gekRef, onPreview }) { <div class="video-flat-list"> ${items.map((it) => it.kind === 'movie' ? html`<${FlatMovieRow} key=${it.entry.id} entry=${it.entry} - transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview} />` + transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview} + onNeedConn=${onNeedConn} />` : html`<${FlatShowFolder} key=${it.show.title} show=${it.show} - transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview} />`)} + transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview} + onNeedConn=${onNeedConn} />`)} </div> `; } @@ -819,7 +863,7 @@ function FlatList({ movies, shows, transportRef, gekRef, onPreview }) { function VideoApp({ groupId, transportRef, gekRef, status, entries, onPreview, videoRoot, tmdbConfig, isNodeAdmin, - hideFilter, + hideFilter, onNeedConn, }) { const [mode, setMode] = useState(loadViewMode); const [filter, setFilter] = useState(''); @@ -889,9 +933,10 @@ function VideoApp({ ${mode === 'poster' ? html`<${PosterGrid} movies=${filteredMovies} shows=${filteredShows} transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview} - tmdbEnabled=${tmdbEnabled} isNodeAdmin=${isNodeAdmin} />` + tmdbEnabled=${tmdbEnabled} isNodeAdmin=${isNodeAdmin} onNeedConn=${onNeedConn} />` : html`<${FlatList} movies=${filteredMovies} shows=${filteredShows} - transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview} />`} + transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview} + onNeedConn=${onNeedConn} />`} `} `; } |