summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-24 17:39:41 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-24 17:39:41 +0200
commit8c780b9928db8145b7fe18ecd137384ccfe25d8f (patch)
tree5cfc06eec0fdf9bb7fe0a14f45e3ef803bb86a20 /packages/meshbay-hub/src/meshbay_hub/static/group-page.js
parent941d1a135dd7b03834576855e8e9fdaa24c4e406 (diff)
downloadmeshbay-8c780b9928db8145b7fe18ecd137384ccfe25d8f.tar.gz
feat(hub): Music app client — album grid, flat list, persistent player
Implements the client half of docs/musicbay.md against MNP 0.8: - music-app.js: album grid (grouped by artist -> album, from index-time artist/album fields) or flat folder view, per-group localStorage toggle like Videos. MusicBrainz (music_meta_req) is only looked up when a track has no embedded cover at all — well-tagged files never trigger a network call, unlike Videos where TMDB is unconditional. Reuses video-app.js's MediaThumb/LazyTile (now exported) rather than duplicating the chunk-path thumbnail decode + virtualization. - music-player.js: the persistent player bar — queue, shuffle (Fisher- Yates, keeps the current track in place when toggled), repeat (off/ all/one), volume (localStorage), prev/next, a one-track prefetch cache. No MSE, no node-side streaming: a track is downloaded and decrypted once via file-utils.js's pipelinedDownload, same chunk pipeline Files already uses, then played from a blob URL. - group-page.js: owns musicQueue/musicbrainzConfig state and renders MusicPlayerBar outside the tab-switched area — deliberately, so playback survives navigating to Chat/Files, the same reasoning the video/preview modals are shell-owned rather than app-owned. - apps.js: registers "music". transport.js: fetchMusicMeta (keyed by path, same reordering-hazard fix as fetchMediaMeta), setMusicbrainzConfig/setMusicbrainzEnabled (signed ops, mirroring TMDB's), and the three new ack handlers. group-settings.js: a MusicBrainz settings section (contact string, per-group toggle) — the existing Applications checklist already picks up "music" for free, per apps.md's own claim. - icon.js: music/pause/skip-next/skip-prev/shuffle/repeat/volume, drawn in the same stroked style as the existing set. - i18n: group.tab_music, the music.* and settings_node.musicbrainz_* keys, translated (not just copied) across all ten locales, Polish carrying full one/few/many/other plural forms for music.n_tracks. - webapp.py's _ASSETS, test_hook_ordering.py's STATIC_FILES and test_transport_contracts.py's SPLIT_FILES gain the two new files. Full suite (common + hub + node): 1116 passed, no regressions. `npm run sync-ui` in meshbay-client confirmed both files copied. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBi7ALLGfwcjBXt57yNMcy
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-page.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-page.js33
1 files changed, 33 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
index aa6b29c..01ab4e0 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
@@ -11,6 +11,7 @@ import {
import { visibleApps } from './apps.js';
import { FilePreview } from './files-app.js';
import { VideoPlayer } from './video-player.js';
+import { MusicPlayerBar } from './music-player.js';
import { GroupSettingsPanel } from './group-settings.js';
/**
@@ -86,6 +87,18 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// Which folder is the Videos app's entry point for this group — ''
// (the default) means the whole group index. Set from Files, per-group.
const [videoRoot, setVideoRoot] = useState('');
+ // MusicBrainz on/off (per-group) + whether a contact string is configured
+ // (node-wide) — docs/musicbay.md §3.2, same shape as tmdbConfig above.
+ const [musicbrainzConfig, setMusicbrainzConfig] = useState(null);
+ // What music-app.js hands over when a track/album is clicked — owned here
+ // (not by music-app.js) so playback survives switching tabs, the same
+ // reasoning the video/preview modals are shell-owned. `nonce` makes
+ // "play this same album again from track 0" a distinct value every time,
+ // so MusicPlayerBar's queue-init effect always re-runs.
+ const [musicQueue, setMusicQueue] = useState(null);
+ const onPlayQueue = useCallback((tracks, startIndex) => {
+ setMusicQueue({ tracks, startIndex, nonce: Date.now() });
+ }, []);
// Paired ≠ operator account. `is_node_admin` says the hub account owning this
// node is the one connecting; this says the node pinned *this browser's* key
// as an operator key. Only the second one lets you sign an invite, and only
@@ -214,6 +227,10 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
language: ack.tmdb_language || '',
});
setVideoRoot(ack.video_root || '');
+ setMusicbrainzConfig({
+ enabled: ack.musicbrainz_enabled !== false,
+ contactConfigured: !!ack.musicbrainz_contact_configured,
+ });
// Changed while we are connected, by an operator who may be someone
// else entirely. Without this the button stays until a reconnection,
// and a button that is still there is a button people press.
@@ -227,6 +244,10 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
transport.onTmdbConfig = (cfg) => setTmdbConfig((prev) => ({ ...(prev || {}), ...cfg }));
transport.onTmdbEnabled = (enabled) => setTmdbConfig((prev) => ({ ...(prev || {}), enabled }));
transport.onVideoRoot = (path) => setVideoRoot(path);
+ transport.onMusicbrainzConfig = (cfg) =>
+ setMusicbrainzConfig((prev) => ({ ...(prev || {}), ...cfg }));
+ transport.onMusicbrainzEnabled = (enabled) =>
+ setMusicbrainzConfig((prev) => ({ ...(prev || {}), enabled }));
// The node's own scan (a root added while we were already connected,
// or reconcile catching one back up) — never the entries, just
// enough to animate the sidebar dot. Guaranteed a final push at the
@@ -418,6 +439,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
onRefreshIndex: refreshIndex, onActivity: touchActivity,
videoRoot, onVideoRoot: (path) => setVideoRoot(path),
tmdbConfig,
+ musicbrainzConfig, onPlayQueue,
};
return html`
@@ -534,6 +556,9 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
tmdbConfig=${tmdbConfig}
onTmdbConfig=${(cfg) => setTmdbConfig((prev) => ({ ...(prev || {}), ...cfg }))}
onTmdbEnabled=${(enabled) => setTmdbConfig((prev) => ({ ...(prev || {}), enabled }))}
+ musicbrainzConfig=${musicbrainzConfig}
+ onMusicbrainzConfig=${(cfg) => setMusicbrainzConfig((prev) => ({ ...(prev || {}), ...cfg }))}
+ onMusicbrainzEnabled=${(enabled) => setMusicbrainzConfig((prev) => ({ ...(prev || {}), enabled }))}
entries=${entries} nodeDirs=${nodeDirs}
videoRoot=${videoRoot}
onVideoRoot=${(path) => setVideoRoot(path)}
@@ -566,6 +591,14 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
onClose=${() => setVideoEntry(null)}
onDownload=${() => downloadFileForModal(videoEntry)} />
`}
+ ${/* Outside the tab-switched area on purpose (docs/musicbay.md §2.3):
+ once something has been played this session, the bar stays
+ mounted and keeps playing regardless of which tab is active —
+ switching to Chat or Files must not stop the music. Renders
+ nothing of its own until onPlayQueue has been called once. */
+ musicQueue && html`
+ <${MusicPlayerBar} transportRef=${transportRef} gekRef=${gekRef} queue=${musicQueue} />
+ `}
</div>
`;
}