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/music-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/music-app.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/music-app.js | 39 |
1 files changed, 29 insertions, 10 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js index dd354d9..418da89 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js @@ -174,8 +174,21 @@ function groupMusicEntries(entries, audioRoot) { // -- album cover, MusicBrainz fetched lazily and only when actually needed -- +const _musicMetaRetryListeners = new Set(); +function bumpMusicMetaGeneration() { + for (const fn of _musicMetaRetryListeners) fn(); +} + function useMusicMeta(transportRef, fileId, active) { const [meta, setMeta] = useState(null); + const [retryToken, setRetryToken] = useState(0); + + useEffect(() => { + const listener = () => setRetryToken((n) => n + 1); + _musicMetaRetryListeners.add(listener); + return () => _musicMetaRetryListeners.delete(listener); + }, []); + useEffect(() => { if (!active || !fileId) return; let cancelled = false; @@ -188,7 +201,7 @@ function useMusicMeta(transportRef, fileId, active) { } catch { if (!cancelled) setMeta({ confidence: 0 }); } })(); return () => { cancelled = true; }; - }, [fileId, active]); + }, [fileId, active, retryToken]); return meta; } @@ -236,21 +249,24 @@ function DiscPlaceholder({ cls }) { function AlbumCard({ album, transportRef, gekRef, musicbrainzEnabled, onOpen }) { const repTrack = album.tracks.find((tr) => tr.thumb_hash) || album.tracks[0]; - // Only when nothing in the library already gives us a cover -- the common - // case (a well-tagged rip with embedded art) needs no network call at all. + const tRef = repTrack._tRef || transportRef; + const gRef = repTrack._gRef || gekRef; const needsLookup = musicbrainzEnabled && !repTrack.thumb_hash; - const meta = useMusicMeta(transportRef, repTrack.id, needsLookup); + const meta = useMusicMeta(tRef, repTrack.id, needsLookup); const coverHash = repTrack.thumb_hash || (meta && meta.cover_thumb_hash) || null; return html` <div class="music-card" onClick=${onOpen}> ${coverHash ? html`<${MediaThumb} thumbHash=${coverHash} alt=${album.album} - cls="music-cover" transportRef=${transportRef} gekRef=${gekRef} />` + cls="music-cover" transportRef=${tRef} gekRef=${gRef} />` : html`<${DiscPlaceholder} cls="music-cover" />`} <div class="music-card-info"> <div class="music-card-title">${album.album}</div> <div class="music-card-sub">${album.artist}</div> + ${repTrack.groupName && html` + <div class="music-card-group">${repTrack.groupName}</div> + `} </div> </div> `; @@ -260,8 +276,10 @@ function AlbumCard({ album, transportRef, gekRef, musicbrainzEnabled, onOpen }) function MusicDetailModal({ album, transportRef, gekRef, musicbrainzEnabled, onClose, onPlayQueue }) { const repTrack = album.tracks.find((tr) => tr.thumb_hash) || album.tracks[0]; + const tRef = repTrack._tRef || transportRef; + const gRef = repTrack._gRef || gekRef; const needsLookup = musicbrainzEnabled && !repTrack.thumb_hash; - const meta = useMusicMeta(transportRef, repTrack.id, needsLookup); + const meta = useMusicMeta(tRef, repTrack.id, needsLookup); const coverHash = repTrack.thumb_hash || (meta && meta.cover_thumb_hash) || null; return html` @@ -278,7 +296,7 @@ function MusicDetailModal({ album, transportRef, gekRef, musicbrainzEnabled, onC <div class="music-detail-header"> ${coverHash ? html`<${MediaThumb} thumbHash=${coverHash} alt=${album.album} - cls="music-detail-cover" transportRef=${transportRef} gekRef=${gekRef} />` + cls="music-detail-cover" transportRef=${tRef} gekRef=${gRef} />` : html`<${DiscPlaceholder} cls="music-detail-cover" />`} <div class="music-detail-meta"> <div class="music-detail-artist">${album.artist}</div> @@ -437,6 +455,7 @@ function FlatList({ tracks, artists, onPlayQueue }) { function MusicApp({ groupId, transportRef, gekRef, status, entries, audioRoot, musicbrainzConfig, onPlayQueue, + hideFilter, }) { const [mode, setMode] = useState(loadViewMode); const [filter, setFilter] = useState(''); @@ -487,11 +506,11 @@ function MusicApp({ onClick=${() => setModeAndSave('flat')}> ${t('music.mode_flat')} </button> - <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> ${empty && html`<p class="page-message">${t('music.empty')}</p>`} ${!empty && needle && filteredArtists.length === 0 && filteredTracks.length === 0 && html` @@ -506,4 +525,4 @@ function MusicApp({ `; } -export { MusicApp }; +export { MusicApp, groupMusicEntries, bumpMusicMetaGeneration }; |