diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-28 15:35:20 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-28 15:35:58 +0200 |
| commit | eba2e6b14484c124f3c87ff95cd7ee640833e5d3 (patch) | |
| tree | ba109b719c879df3ea40090e4fd6ff80b403a3a5 /packages/meshbay-hub/src/meshbay_hub/static/video-app.js | |
| parent | e6917349c47cc4b7e04e1ecfab27c5291a5fe522 (diff) | |
| download | meshbay-eba2e6b14484c124f3c87ff95cd7ee640833e5d3.tar.gz | |
feat(hub): cross-group search with reuse of existing views
Search page fetches indexes from all groups, then renders consolidated
entries through the existing FilesPanel, VideoApp, and MusicApp
components — no reimplemented views. Groups appear as top-level
directories in the file browser; video/music entries use a synthetic
root with per-entry transport refs for thumbnails and metadata across
groups. Music player lifted to app.js with getConnection(groupId) for
cross-group playback. Connection pool (max 3, LRU eviction) manages
lazy WebRTC connections. All 10 locales updated with search keys.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 | 46 |
1 files changed, 36 insertions, 10 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 dd120da..df930fd 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js @@ -147,6 +147,13 @@ function MediaThumb({ thumbHash, transportRef, gekRef, alt, cls = 'video-thumb', onReady, emptyIcon = 'video', }) { const [blobUrl, setBlobUrl] = useState(() => _thumbBlobCache.get(thumbHash) || null); + const [retryToken, setRetryToken] = useState(0); + + useEffect(() => { + const listener = () => setRetryToken((n) => n + 1); + _thumbRetryListeners.add(listener); + return () => _thumbRetryListeners.delete(listener); + }, []); // Re-checks the cache by the CURRENT thumbHash on every change rather than // trusting the `blobUrl` state variable — a PosterCard swaps this same @@ -181,7 +188,7 @@ function MediaThumb({ } })(); return () => { cancelled = true; }; - }, [thumbHash]); + }, [thumbHash, retryToken]); 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" />`; @@ -201,6 +208,11 @@ function bumpMediaMetaGeneration() { for (const fn of _mediaMetaListeners) fn(); } +const _thumbRetryListeners = new Set(); +function bumpThumbGeneration() { + for (const fn of _thumbRetryListeners) fn(); +} + function useMediaMeta(transportRef, fileId, active) { const [meta, setMeta] = useState(null); const [refetchToken, setRefetchToken] = useState(0); @@ -266,7 +278,9 @@ function useSeasonMeta(transportRef, tmdbId, season, active) { // ── Mode A: poster grid ────────────────────────────────────────────────────── function PosterCard({ title, subtitle, repEntry, transportRef, gekRef, onOpen, groupKey, onMetaResolved }) { - const meta = useMediaMeta(transportRef, repEntry.id, true); + const tRef = repEntry._tRef || transportRef; + const gRef = repEntry._gRef || gekRef; + const meta = useMediaMeta(tRef, repEntry.id, true); const confident = Boolean(meta && meta.confidence && meta.tmdb_id); const metaReady = meta !== null; @@ -318,7 +332,7 @@ function PosterCard({ title, subtitle, repEntry, transportRef, gekRef, onOpen, g <div class="video-poster-slot" style=${ready ? '' : 'display:none'}> ${metaReady && html` <${MediaThumb} thumbHash=${posterHash} alt=${title} - cls="video-poster" transportRef=${transportRef} gekRef=${gekRef} + cls="video-poster" transportRef=${tRef} gekRef=${gRef} onReady=${handleImageReady} /> `} </div> @@ -330,6 +344,9 @@ function PosterCard({ title, subtitle, repEntry, transportRef, gekRef, onOpen, g ${confident && meta.first_air_date ? yearOf(meta.first_air_date) : ''} ${subtitle ? ` · ${subtitle}` : ''} </div> + ${repEntry.groupName && html` + <div class="video-card-group">${repEntry.groupName}</div> + `} </div> `} </div> @@ -534,7 +551,7 @@ 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=${transportRef} gekRef=${gekRef} /> + cls="video-episode-thumb" transportRef=${ep._tRef || transportRef} gekRef=${ep._gRef || gekRef} /> </${LazyTile}> <span class="video-episode-label"> S${ep.season}E${String(ep.episode).padStart(2, '0')} @@ -615,7 +632,9 @@ function PosterGrid({ movies, shows, transportRef, gekRef, onPreview, tmdbEnable }, [shows, metaByGroup]); const openDetail = (title, repEntry, show) => setDetail({ title, repEntry, show }); - const detailMeta = useMediaMeta(transportRef, detail ? detail.repEntry.id : null, !!detail); + const detailTRef = detail && detail.repEntry._tRef ? detail.repEntry._tRef : transportRef; + const detailGRef = detail && detail.repEntry._gRef ? detail.repEntry._gRef : gekRef; + const detailMeta = useMediaMeta(detailTRef, detail ? detail.repEntry.id : null, !!detail); return html` <div class="video-grid"> @@ -666,7 +685,7 @@ function PosterGrid({ movies, shows, transportRef, gekRef, onPreview, tmdbEnable ${detail && html` <${VideoDetailModal} title=${detail.title} meta=${detailMeta} repEntry=${detail.repEntry} show=${detail.show} - transportRef=${transportRef} gekRef=${gekRef} isNodeAdmin=${isNodeAdmin} + transportRef=${detailTRef} gekRef=${detailGRef} isNodeAdmin=${isNodeAdmin} onClose=${() => setDetail(null)} onPlay=${(entry) => { setDetail(null); onPreview(entry); }} /> `} @@ -683,6 +702,8 @@ function PosterGrid({ movies, shows, transportRef, gekRef, onPreview, tmdbEnable // 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 }) { + const tRef = entry._tRef || transportRef; + const gRef = entry._gRef || gekRef; const isEpisode = seasonContext && entry.season != null && entry.episode != null; const hasOwnTitle = entry.display_title && entry.display_title !== seasonContext; const label = isEpisode @@ -694,7 +715,7 @@ 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=${transportRef} gekRef=${gekRef} /> + cls="video-flat-thumb" transportRef=${tRef} gekRef=${gRef} /> </${LazyTile}> <div class="video-flat-info"> <div class="video-flat-title">${label}</div> @@ -703,6 +724,10 @@ function FlatMovieRow({ entry, transportRef, gekRef, onPreview, seasonContext }) ${' · '}${formatSize(entry.size)} </div> </div> + ${entry.groupName && html` + <a href="#/group/${entry.groupId}" class="badge search-group-badge" + onClick=${(e) => e.stopPropagation()}>${entry.groupName}</a> + `} </div> `; } @@ -755,6 +780,7 @@ function FlatList({ movies, shows, transportRef, gekRef, onPreview }) { function VideoApp({ groupId, transportRef, gekRef, status, entries, onPreview, videoRoot, tmdbConfig, isNodeAdmin, + hideFilter, }) { const [mode, setMode] = useState(loadViewMode); const [filter, setFilter] = useState(''); @@ -812,11 +838,11 @@ function VideoApp({ ${t('video.filter_series')} </button> </div> - <div class="tb-search"> + ${!hideFilter && html`<div class="tb-search"> <${Icon} name="search" /> <input type="text" placeholder="${t('group.filter')}" value=${filter} onInput=${(e) => setFilter(e.target.value)} /> - </div> + </div>`} </div> ${filteredMovies.length === 0 && filteredShows.length === 0 && html` <p class="page-message">${needle ? t('group.empty_filter') : t('video.empty')}</p> @@ -836,4 +862,4 @@ function VideoApp({ // blob" and "mount only once actually scrolled near" mechanisms apply to a // track's cover art unchanged, so Music imports them here rather than // re-implementing (apps.md §4's checklist). -export { VideoApp, MediaThumb, LazyTile }; +export { VideoApp, MediaThumb, LazyTile, groupVideoEntries, bumpMediaMetaGeneration, bumpThumbGeneration }; |