diff options
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)} /> + `} `; } |