diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-16 12:10:25 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-16 12:10:25 +0200 |
| commit | 9cbff21274604e37c0986d57937deef85819c396 (patch) | |
| tree | bdfb55978bb105fd78d1d35db33ff8bb35457f67 /packages/meshbay-hub/src/meshbay_hub/static/music-app.js | |
| parent | 2e973795383b71f63ae9e3bef0e5dfc7930b4c90 (diff) | |
| download | meshbay-9cbff21274604e37c0986d57937deef85819c396.tar.gz | |
music: the playlist menus
One button in Music's sticky toolbar — load, create, delete, remove a
track, sync now — and "add to playlist" on every cover and row. Both
surfaces share one list, read from the manifest, so they open instantly
with every node offline and no body is fetched until one is wanted.
Submenus expand in place rather than flying out: the account menu's
language list already does this, and a flyout has nowhere to go at 400px.
The tracklist under "remove a track" loads when it is expanded.
A name is typed into a field. Electron has no prompt — it throws.
Also splits the two playback failures: a decode failure belongs to that
file and keeps the bounded counter, a connection failure belongs to the
group and skips all of its queued tracks at once. Six dead tracks are
one more than the bound, which is where a playlist would otherwise stop.
Co-Authored-By: Claude Opus 5 <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 | 79 |
1 files changed, 77 insertions, 2 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 15fbcf9..8fc6cb3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js @@ -8,6 +8,8 @@ import { formatTime } from './music-player.js'; import { SourceTag } from './group-name.js'; import { usePager, Pager, pageSizeFrom } from './pager.js'; import { Menu, MenuDots, useMenu } from './menu.js'; +import { PlaylistMenuButton, NameModal, usePlaylists } from './playlist-menu.js'; +import * as P from './playlists.js'; // -- Music -------------------------------------------------------------------- // @@ -497,7 +499,7 @@ function FlatList({ items, onPlayQueue, onMenu }) { function MusicApp({ groupId, transportRef, gekRef, status, entries, availableEntries, - musicDirectories, musicbrainzConfig, onPlayQueue, + musicDirectories, musicbrainzConfig, onPlayQueue, userId, hideFilter, userPrefs, pageResetKey, }) { const [mode, setMode] = useState(loadViewMode); @@ -506,6 +508,42 @@ function MusicApp({ // One menu for the whole view. Per-card state would mean a hundred open // handlers on a full grid, and two menus could be open at once. const { menu, openAt, close: closeMenu } = useMenu(); + // One list, shared by the toolbar button and the per-item submenu below. + const { lists: playlists, reload: reloadPlaylists } = usePlaylists(userId); + const [pendingAdd, setPendingAdd] = useState(null); + const [note, setNote] = useState(''); + + const say = useCallback((text) => { + setNote(text); + setTimeout(() => setNote(''), 4000); + }, []); + + // Sync rides the connection this group already has open — §7's whole point + // is that playlists add no dialing. Once per group opened, and again only + // when the reader asks. + const syncNow = useCallback(async () => { + const tr = transportRef && transportRef.current; + if (!tr || !userId) return { ok: false, reason: 'offline' }; + const r = await P.syncWith(tr, userId); + await reloadPlaylists(); + return r; + }, [transportRef, userId, reloadPlaylists]); + + useEffect(() => { + if (status !== 'connected' || !userId) return; + syncNow().catch(() => {}); + }, [status, userId, groupId]); + + const addToPlaylist = useCallback(async (id, tracks) => { + const added = await P.addTracks(userId, id, tracks, groupId, t('playlists.favorites')); + await reloadPlaylists(); + say(added + ? t('playlists.added', { n: added }) + : t('playlists.already_there')); + // Straight on to whatever node this group is on, so the edit is not only + // in this browser. Best-effort: it is durable locally either way. + syncNow().catch(() => {}); + }, [userId, groupId, reloadPlaylists, say, syncNow]); // The queue verbs, for an album (every track, from the first) or for one // track. `startIndex` only means anything to "play": the other two do not @@ -523,8 +561,33 @@ function MusicApp({ onSelect: () => onPlayQueue(tracks, 0, 'next') }, { label: t('music.menu_enqueue'), icon: 'plus', onSelect: () => onPlayQueue(tracks, 0, 'append') }, + { divider: true }, + { + label: t('playlists.add_to'), icon: 'playlist', + // Drawn from the manifest, so it opens instantly with every node + // offline. Favourites is first, and is there on a fresh account + // because `livePlaylists` puts the reserved id first whether or not + // it has been used yet. + items: [ + ...(playlists.some((p) => p.id === P.FAVORITES_ID) ? [] : [{ + key: P.FAVORITES_ID, label: t('playlists.favorites'), icon: 'check', + onSelect: () => addToPlaylist(P.FAVORITES_ID, tracks), + }]), + ...playlists.map((p) => ({ + key: p.id, + label: p.id === P.FAVORITES_ID ? t('playlists.favorites') : p.name, + hint: t('music.n_tracks', { n: p.count || 0 }), + onSelect: () => addToPlaylist(p.id, tracks), + })), + { divider: true }, + { + label: t('playlists.create'), icon: 'plus', + onSelect: () => setPendingAdd(tracks), + }, + ], + }, ]); - }, [openAt, onPlayQueue]); + }, [openAt, onPlayQueue, playlists, addToPlaylist]); useEffect(() => { setMode(loadViewMode()); }, [groupId]); useEffect(() => { setFilter(''); }, [groupId]); @@ -592,6 +655,9 @@ function MusicApp({ onClick=${() => setModeAndSave('flat')}> ${t('music.mode_flat')} </button> + ${userId && html`<${PlaylistMenuButton} userId=${userId} + lists=${playlists} reload=${reloadPlaylists} + onPlayQueue=${onPlayQueue} onSync=${syncNow} />`} <${Pager} pager=${pager} /> ${!hideFilter && html`<div class="tb-search"> <${Icon} name="search" /> @@ -611,6 +677,15 @@ function MusicApp({ onPlayQueue=${onPlayQueue} onMenu=${onMenu} />`} `} ${menu && html`<${Menu} ...${menu} onClose=${closeMenu} />`} + ${note && html`<div class="playlist-note">${note}</div>`} + ${pendingAdd && html` + <${NameModal} title=${t('playlists.create')} + onSubmit=${async (name) => { + const id = await P.createPlaylist(userId, name); + await addToPlaylist(id, pendingAdd); + }} + onClose=${() => setPendingAdd(null)} /> + `} `; } |