From eba2e6b14484c124f3c87ff95cd7ee640833e5d3 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Fri, 28 Aug 2026 15:35:20 +0200 Subject: feat(hub): cross-group search with reuse of existing views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/meshbay_hub/static/music-player.js | 32 ++++++++-------------- 1 file changed, 12 insertions(+), 20 deletions(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/music-player.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js index ffe092d..2d3c002 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js @@ -8,13 +8,14 @@ import { CHUNK_SIZE, pipelinedDownload } from './file-utils.js'; /** * The Music app's persistent player bar (docs/musicbay.md §2.3, §7.2). * - * Owned and rendered by group-page.js, *not* by music-app.js: it is the one - * piece of this feature that lives outside the tab-switched area, so - * playback survives navigating to Chat or Files, exactly the way the - * video/preview modals are shell-owned rather than owned by whichever app - * opened them. music-app.js never touches audio state directly — it only - * calls `onPlayQueue(tracks, startIndex)`, threaded down from group-page.js, - * to hand this component a new queue. + * Owned and rendered by app.js — the router's parent — so playback survives + * navigating between groups, search, and other pages. Both group-page.js + * and search-page.js trigger playback by calling `onPlayQueue(tracks, + * startIndex)`, which flows up to app.js. + * + * Each track carries a `groupId`; the player calls `getConnection(groupId)` + * per track to obtain the right transport — a pool-based lazy connection + * that transparently handles single-group and cross-group queues. * * No streaming, no MSE, no node-side transcode pool for the common case: a * track is a few megabytes, so it is downloaded and decrypted once through @@ -177,7 +178,7 @@ function QueuePanel({ tracks, order, pos, onSelect, onClose }) { `; } -function MusicPlayerBar({ transportRef, gekRef, queue, onClose, userPrefs }) { +function MusicPlayerBar({ getConnection, queue, onClose, userPrefs }) { const audioRef = useRef(null); const blobCacheRef = useRef(new Map()); // file id -> { url, order: insertion index } const blobInsertRef = useRef(0); @@ -284,13 +285,8 @@ function MusicPlayerBar({ transportRef, gekRef, queue, onClose, userPrefs }) { const fetchTrackBlob = useCallback(async (entry) => { const cached = blobCacheRef.current.get(entry.id); if (cached) return cached.url; - const transport = transportRef.current; + const { transport, gek } = await getConnection(entry.groupId); if (!transport) throw new Error(t('music.err_transport')); - // A track ending (or "next") right after a screen-lock reconnect started - // is exactly when this used to throw: `connected` was still false because - // the reconnect it only had to wait a few seconds for hadn't landed yet. - // waitForReconnect is a no-op when nothing is in flight, so this costs - // nothing on the ordinary path. if (!transport.connected) await transport.waitForReconnect(); if (!transport.connected) throw new Error(t('music.err_transport')); @@ -310,17 +306,13 @@ function MusicPlayerBar({ transportRef, gekRef, queue, onClose, userPrefs }) { } const totalChunks = Math.ceil(downloadSize / CHUNK_SIZE); - const chunks = await pipelinedDownload(transport, gekRef.current, downloadId, totalChunks); + const chunks = await pipelinedDownload(transport, gek, downloadId, totalChunks); const blob = new Blob(chunks, { type: mime }); const url = URL.createObjectURL(blob); - // Keyed by the track's own id, not `downloadId` — the transcode cache - // hash is an implementation detail of getting there, and a second play - // of the same track must still hit this cache rather than re-requesting - // a transcode the node already ran once. blobCacheRef.current.set(entry.id, { url, order: blobInsertRef.current++ }); evictOldBlobs(); return url; - }, [transportRef, gekRef, evictOldBlobs]); + }, [getConnection, evictOldBlobs]); // Silently warms the cache for the next tracks so pressing "next" doesn't // visibly wait (musicbay.md §2.2) — best-effort, never surfaces an error. -- cgit v1.2.3