aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/music-app.js
diff options
context:
space:
mode:
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.js79
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)} />
+ `}
`;
}