summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-page.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/music-app.js56
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/music-player.js66
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css3
14 files changed, 161 insertions, 18 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 01ab4e0..d2d56b5 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
@@ -99,6 +99,12 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
const onPlayQueue = useCallback((tracks, startIndex) => {
setMusicQueue({ tracks, startIndex, nonce: Date.now() });
}, []);
+ // This component instance is not remounted when switching to a *different*
+ // group on the same /group/:id route (only groupId as a prop changes, see
+ // the connect effect's own comment below) — so leaving music playing here
+ // would carry it into whatever group is opened next. Tab switches inside
+ // one group must not stop it; leaving the group itself must.
+ useEffect(() => { setMusicQueue(null); }, [groupId]);
// 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
@@ -595,9 +601,13 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
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. */
+ nothing of its own until onPlayQueue has been called once.
+ Its own close button, and the effect above on leaving the
+ group, both go through the same setMusicQueue(null) — the
+ bar's own unmount cleanup is what actually stops playback. */
musicQueue && html`
- <${MusicPlayerBar} transportRef=${transportRef} gekRef=${gekRef} queue=${musicQueue} />
+ <${MusicPlayerBar} transportRef=${transportRef} gekRef=${gekRef} queue=${musicQueue}
+ onClose=${() => setMusicQueue(null)} />
`}
</div>
`;
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index cb542a6..70d2b95 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -182,6 +182,7 @@ export default {
'music.mode_flat': 'Flache Liste',
'music.empty': 'Keine Musik gefunden.',
'music.unknown_album': 'Unbekanntes Album',
+ 'music.various': 'Verschiedenes',
'music.play_all': 'Alle abspielen',
'music.n_tracks': {
one: '{n} Titel',
@@ -197,6 +198,9 @@ export default {
'music.player_repeat_all': 'Alle wiederholen',
'music.player_repeat_one': 'Einzelnen wiederholen',
'music.player_volume': 'Lautstärke',
+ 'music.player_close': 'Player schließen',
+ 'music.player_queue': 'Aktuelle Wiedergabeliste',
+ 'music.queue_title': 'Wird wiedergegeben',
// LAN-Cast
'cast.start': 'Auf Gerät übertragen',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index e05b398..0ee0972 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -180,6 +180,7 @@ export default {
'music.mode_flat': 'Flat list',
'music.empty': 'No music found.',
'music.unknown_album': 'Unknown album',
+ 'music.various': 'Various',
'music.play_all': 'Play all',
'music.n_tracks': {
one: '{n} track',
@@ -195,6 +196,9 @@ export default {
'music.player_repeat_all': 'Repeat all',
'music.player_repeat_one': 'Repeat one',
'music.player_volume': 'Volume',
+ 'music.player_close': 'Close player',
+ 'music.player_queue': 'Current queue',
+ 'music.queue_title': 'Playing now',
// LAN cast
'cast.start': 'Cast to device',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index 113ae7a..69e2687 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -180,6 +180,7 @@ export default {
'music.mode_flat': 'Lista plana',
'music.empty': 'No se encontró música.',
'music.unknown_album': 'Álbum desconocido',
+ 'music.various': 'Varios',
'music.play_all': 'Reproducir todo',
'music.n_tracks': {
one: '{n} pista',
@@ -195,6 +196,9 @@ export default {
'music.player_repeat_all': 'Repetir todo',
'music.player_repeat_one': 'Repetir una',
'music.player_volume': 'Volumen',
+ 'music.player_close': 'Cerrar reproductor',
+ 'music.player_queue': 'Cola actual',
+ 'music.queue_title': 'Reproduciendo ahora',
// LAN cast
'cast.start': 'Enviar a dispositivo',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index 6a57174..8d52d37 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -181,6 +181,7 @@ export default {
'music.mode_flat': 'Liste à plat',
'music.empty': 'Aucune musique trouvée.',
'music.unknown_album': 'Album inconnu',
+ 'music.various': 'Divers',
'music.play_all': 'Tout lire',
'music.n_tracks': {
one: '{n} piste',
@@ -196,6 +197,9 @@ export default {
'music.player_repeat_all': 'Tout répéter',
'music.player_repeat_one': 'Répéter le morceau',
'music.player_volume': 'Volume',
+ 'music.player_close': 'Fermer le lecteur',
+ 'music.player_queue': 'File en cours',
+ 'music.queue_title': 'En cours de lecture',
// LAN cast
'cast.start': 'Diffuser sur un appareil',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index 257318a..dfd4bbc 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -181,6 +181,7 @@ export default {
'music.mode_flat': 'Elenco semplice',
'music.empty': 'Nessuna musica trovata.',
'music.unknown_album': 'Album sconosciuto',
+ 'music.various': 'Vari',
'music.play_all': 'Riproduci tutto',
'music.n_tracks': {
one: '{n} traccia',
@@ -196,6 +197,9 @@ export default {
'music.player_repeat_all': 'Ripeti tutto',
'music.player_repeat_one': 'Ripeti brano',
'music.player_volume': 'Volume',
+ 'music.player_close': 'Chiudi lettore',
+ 'music.player_queue': 'Coda attuale',
+ 'music.queue_title': 'In riproduzione',
// LAN cast
'cast.start': 'Trasmetti al dispositivo',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index 44f9037..10d2088 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -178,6 +178,7 @@ export default {
'music.mode_flat': 'フラットリスト',
'music.empty': '音楽が見つかりません。',
'music.unknown_album': '不明なアルバム',
+ 'music.various': 'その他',
'music.play_all': 'すべて再生',
'music.n_tracks': {
one: '{n}曲',
@@ -193,6 +194,9 @@ export default {
'music.player_repeat_all': 'すべてリピート',
'music.player_repeat_one': '1曲リピート',
'music.player_volume': '音量',
+ 'music.player_close': 'プレーヤーを閉じる',
+ 'music.player_queue': '再生中のキュー',
+ 'music.queue_title': '再生中',
// LAN cast
'cast.start': 'デバイスにキャスト',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index 2fedd70..3342559 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -182,6 +182,7 @@ export default {
'music.mode_flat': 'Platte lijst',
'music.empty': 'Geen muziek gevonden.',
'music.unknown_album': 'Onbekend album',
+ 'music.various': 'Diversen',
'music.play_all': 'Alles afspelen',
'music.n_tracks': {
one: '{n} nummer',
@@ -197,6 +198,9 @@ export default {
'music.player_repeat_all': 'Alles herhalen',
'music.player_repeat_one': 'Nummer herhalen',
'music.player_volume': 'Volume',
+ 'music.player_close': 'Speler sluiten',
+ 'music.player_queue': 'Huidige wachtrij',
+ 'music.queue_title': 'Nu aan het afspelen',
// LAN cast
'cast.start': 'Naar apparaat casten',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index dac2359..1b52a8b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -189,6 +189,7 @@ export default {
'music.mode_flat': 'Lista płaska',
'music.empty': 'Nie znaleziono muzyki.',
'music.unknown_album': 'Nieznany album',
+ 'music.various': 'Różne',
'music.play_all': 'Odtwórz wszystko',
'music.n_tracks': {
one: '{n} utwór',
@@ -206,6 +207,9 @@ export default {
'music.player_repeat_all': 'Powtórz wszystko',
'music.player_repeat_one': 'Powtórz utwór',
'music.player_volume': 'Głośność',
+ 'music.player_close': 'Zamknij odtwarzacz',
+ 'music.player_queue': 'Aktualna kolejka',
+ 'music.queue_title': 'Teraz odtwarzane',
// LAN cast
'cast.start': 'Przesyłaj na urządzenie',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index 4251a4b..1b8c50c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -182,6 +182,7 @@ export default {
'music.mode_flat': 'Lista simples',
'music.empty': 'Nenhuma música encontrada.',
'music.unknown_album': 'Álbum desconhecido',
+ 'music.various': 'Diversos',
'music.play_all': 'Reproduzir tudo',
'music.n_tracks': {
one: '{n} faixa',
@@ -197,6 +198,9 @@ export default {
'music.player_repeat_all': 'Repetir tudo',
'music.player_repeat_one': 'Repetir faixa',
'music.player_volume': 'Volume',
+ 'music.player_close': 'Fechar player',
+ 'music.player_queue': 'Fila atual',
+ 'music.queue_title': 'Tocando agora',
// LAN cast
'cast.start': 'Transmitir para dispositivo',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index 7e7676b..5cc6cb3 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -175,6 +175,7 @@ export default {
'music.mode_flat': '平铺列表',
'music.empty': '未找到音乐。',
'music.unknown_album': '未知专辑',
+ 'music.various': '其他',
'music.play_all': '全部播放',
'music.n_tracks': {
one: '{n} 首曲目',
@@ -190,6 +191,9 @@ export default {
'music.player_repeat_all': '全部重复',
'music.player_repeat_one': '单曲重复',
'music.player_volume': '音量',
+ 'music.player_close': '关闭播放器',
+ 'music.player_queue': '当前队列',
+ 'music.queue_title': '正在播放',
// LAN cast
'cast.start': '投射到设备',
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 87e1319..7e16b53 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js
@@ -36,13 +36,15 @@ function saveViewMode(mode) {
// -- grouping -------------------------------------------------------------
-// Two tags differing only in case ("Racing With The Sun" vs "Racing with
-// the sun" -- found live, a single mistagged track split one real album
-// into two cards) are the same artist/album for grouping purposes. Folded
-// for the *key* only; the first-seen spelling is kept as the display
-// string, so this never rewrites anyone's tags.
+// Two tags differing only in case (found live: a single mistagged track
+// split one real album into two cards) are the same artist/album for
+// grouping purposes. Same for an "&" vs "and" spelling of the same act,
+// tagged both ways across different rips of the same catalogue. Folded for
+// the *key* only; the first-seen spelling is kept as the display string, so
+// this never rewrites anyone's tags.
function foldKey(s) {
- return (s || '').trim().replace(/\s+/g, ' ').toLowerCase();
+ return (s || '').trim().replace(/\s+/g, ' ').toLowerCase()
+ .replace(/\s*&\s*/g, ' and ').replace(/\s+/g, ' ').trim();
}
function groupMusicEntries(entries) {
@@ -74,18 +76,42 @@ function groupMusicEntries(entries) {
tracks.sort(byTitle);
const artists = [...byArtistKey.values()].map(({ artist, albumsByKey, loose }) => {
- const albums = [...albumsByKey.values()].sort((a, b) => a.album.localeCompare(b.album));
- for (const album of albums) {
+ const sortedAlbums = [...albumsByKey.values()].sort((a, b) => a.album.localeCompare(b.album));
+
+ // A "singleton" -- an album bucket down to exactly one track, because
+ // that's a real album tag but this person only has one song from it,
+ // not the whole release -- clutters the grid exactly the way an
+ // untagged loose track does. Found live: one artist's folder listing a
+ // dozen near-empty one-track album cards alongside the genuine
+ // multi-track albums. Both kinds fold into a single "<artist> -
+ // Various" tile, unless there is only one leftover track overall, where
+ // relabeling away a real album name (or minting "Various" for one
+ // file) buys nothing.
+ const realAlbums = [];
+ const misc = [...loose];
+ const miscSourceAlbums = [];
+ for (const album of sortedAlbums) {
+ if (album.tracks.length > 1) { realAlbums.push(album); continue; }
+ misc.push(...album.tracks);
+ miscSourceAlbums.push(album);
+ }
+ for (const album of realAlbums) {
album.tracks.sort((a, b) => (trackNo(a) - trackNo(b)) || byTitle(a, b));
}
- if (loose.length) {
- // Appended after every real album, never sorted in among them by
- // whatever the placeholder text happens to alphabetize to -- it
- // isn't a release, and translating "Unknown album" must not move it.
- loose.sort((a, b) => (trackNo(a) - trackNo(b)) || byTitle(a, b));
- albums.push({ artist, album: t('music.unknown_album'), isUnknown: true, tracks: loose });
+
+ if (misc.length === 1) {
+ // Keep the one leftover's own real album name if it had one; the
+ // generic placeholder only for a single untagged track with nothing
+ // else to call it.
+ realAlbums.push(miscSourceAlbums[0]
+ || { artist, album: t('music.unknown_album'), isUnknown: true, tracks: misc });
+ } else if (misc.length > 1) {
+ misc.sort((a, b) => (trackNo(a) - trackNo(b)) || byTitle(a, b));
+ realAlbums.push({
+ artist, album: `${artist} - ${t('music.various')}`, isUnknown: true, tracks: misc,
+ });
}
- return { artist, albums };
+ return { artist, albums: realAlbums };
}).sort((a, b) => a.artist.localeCompare(b.artist));
const albums = artists.flatMap((a) => a.albums);
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 386d3f6..eac011a 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js
@@ -86,7 +86,39 @@ function saveRepeat(v) {
try { localStorage.setItem('meshbay_music_repeat', v); } catch { /* per-device only */ }
}
-function MusicPlayerBar({ transportRef, gekRef, queue }) {
+// The current queue, tracklist form -- "go back to what's playing" without
+// switching tabs or hunting for the album/folder it came from. Works the
+// same regardless of how the queue was built (an album, a consolidated
+// misc/loose bucket, a single standalone track).
+function QueuePanel({ tracks, order, pos, onSelect, onClose }) {
+ return html`
+ <div class="video-overlay" onClick=${(e) => {
+ if (e.target.classList.contains('video-overlay')) onClose();
+ }}>
+ <div class="music-detail">
+ <div class="video-top-bar">
+ <span class="video-title">${t('music.queue_title')}</span>
+ <button class="video-close" onClick=${onClose} title=${t('video.close')}>
+ <${Icon} name="close" /></button>
+ </div>
+ <div class="music-detail-body">
+ <div class="music-tracklist">
+ ${order.map((idx, i) => html`
+ <button class="music-track-row ${i === pos ? 'active' : ''}" key=${tracks[idx].id}
+ onClick=${() => { onSelect(i); onClose(); }}>
+ <span class="music-track-no">${i + 1}</span>
+ <span class="music-track-title">${tracks[idx].display_title || tracks[idx].name}</span>
+ <span class="music-track-duration">${formatTime(tracks[idx].duration || 0)}</span>
+ </button>
+ `)}
+ </div>
+ </div>
+ </div>
+ </div>
+ `;
+}
+
+function MusicPlayerBar({ transportRef, gekRef, queue, onClose }) {
const audioRef = useRef(null);
const blobCacheRef = useRef(new Map()); // file id -> { url, order: insertion index }
const blobInsertRef = useRef(0);
@@ -103,9 +135,23 @@ function MusicPlayerBar({ transportRef, gekRef, queue }) {
const [volume, setVolume] = useState(loadVolume);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
+ const [showQueue, setShowQueue] = useState(false);
const currentTrack = tracks[order[pos]] || null;
+ // Stops playback the moment this bar goes away for any reason -- the
+ // close button below, or the shell tearing it down on its own (leaving
+ // the group, switching to a different one). A component removed from the
+ // DOM should already stop an <audio> element, but that's the browser's
+ // behaviour to rely on, not this app's to assert; pausing explicitly, and
+ // releasing every cached blob URL rather than leaving them for the tab's
+ // lifetime, costs nothing and isn't optional either way.
+ useEffect(() => () => {
+ const audio = audioRef.current;
+ if (audio) { audio.pause(); audio.src = ''; }
+ for (const { url } of blobCacheRef.current.values()) URL.revokeObjectURL(url);
+ }, []);
+
const evictOldBlobs = useCallback(() => {
const cache = blobCacheRef.current;
while (cache.size > MAX_CACHED_BLOBS) {
@@ -258,6 +304,12 @@ function MusicPlayerBar({ transportRef, gekRef, queue }) {
if (audio && isFinite(audio.duration)) audio.currentTime = parseFloat(e.target.value);
}, []);
+ const handleClose = useCallback(() => {
+ const audio = audioRef.current;
+ if (audio) audio.pause();
+ if (onClose) onClose();
+ }, [onClose]);
+
if (!currentTrack) return null;
const title = currentTrack.display_title || currentTrack.name;
@@ -312,8 +364,20 @@ function MusicPlayerBar({ transportRef, gekRef, queue }) {
title=${t('music.player_volume')}
onInput=${(e) => setVolume(parseFloat(e.target.value))} />
</div>
+ <div class="music-player-extra">
+ <button class="music-player-btn" title=${t('music.player_queue')} onClick=${() => setShowQueue(true)}>
+ <${Icon} name="menu" />
+ </button>
+ <button class="music-player-btn" title=${t('music.player_close')} onClick=${handleClose}>
+ <${Icon} name="close" />
+ </button>
+ </div>
${error && html`<div class="music-player-error">${error}</div>`}
</div>
+ ${showQueue && html`
+ <${QueuePanel} tracks=${tracks} order=${order} pos=${pos}
+ onSelect=${skipTo} onClose=${() => setShowQueue(false)} />
+ `}
`;
}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css
index 3d411f7..59ec919 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/style.css
+++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css
@@ -2821,6 +2821,7 @@ a.transfer-name {
text-align: left;
}
.music-track-row:hover { background: var(--bg-raised); border-color: var(--border); }
+.music-track-row.active { border-color: var(--accent); color: var(--accent); }
.music-track-no { width: 24px; flex-shrink: 0; color: var(--text-dim); text-align: right; }
.music-track-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.music-track-duration { color: var(--text-dim); font-size: 0.9em; flex-shrink: 0; }
@@ -2947,6 +2948,8 @@ a.transfer-name {
.music-player-volume .icon { width: 16px; height: 16px; color: var(--text-dim); }
.music-player-volume input[type="range"] { width: 80px; }
+.music-player-extra { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
+
.music-player-error { flex-basis: 100%; font-size: 0.78em; color: var(--error); }
@media (max-width: 640px) {