Files
Psychotoxical-psysonic/src/utils/playSong.ts
T
Frank Stellmacher e3aabd98b7 feat(tracks): add Tracks library hub page (closes #299) (#300)
New /tracks route with three sections:

- Hero "Track of the moment" — random pick with play / enqueue / reroll
- Random Pick rail — 18 song cards, rerollable; hero song deduped
- Browse all tracks — virtualized list (@tanstack/react-virtual),
  paginated 50 at a time

Browse uses Navidrome's native /api/song?_sort=title&_order=ASC for
proper A-Z order (no Subsonic equivalent), with automatic fallback
to search3 on non-Navidrome servers. Search input drives search3
with 300ms debounce. Bearer token cached module-level, re-auth on 401.

Play button on rows + cards calls a new enqueueAndPlay() helper that
appends to the existing queue (skip if duplicate) and jumps to the
song — different from playSongNow which replaces the queue. Enqueue
button stays opaque (no hover-only).

i18n keys for sidebar.tracks + tracks.* namespace in all 8 locales.
New AudioLines sidebar icon. Sidebar entry inserted between
"All Albums" and "Build a Mix".

Performance: cover thumbnails dropped (uniform layout instead),
RAF-throttled scroll prefetch, hover transforms removed from cards
(WebKitGTK compositing-friendly).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:23:15 +02:00

62 lines
1.9 KiB
TypeScript

import { usePlayerStore, songToTrack } from '../store/playerStore';
import type { SubsonicSong } from '../api/subsonic';
function fadeOut(setVolume: (v: number) => void, from: number, durationMs: number): Promise<void> {
return new Promise(resolve => {
const steps = 16;
const stepMs = durationMs / steps;
let step = 0;
const id = setInterval(() => {
step++;
setVolume(Math.max(0, from * (1 - step / steps)));
if (step >= steps) {
clearInterval(id);
resolve();
}
}, stepMs);
});
}
/**
* Play a single song. When `queue` is provided, surrounds the chosen song with that queue
* so Next/Prev work — pass the rail / pool the click came from. Mirrors playAlbum's fade-out.
*/
export async function playSongNow(song: SubsonicSong, queue?: SubsonicSong[]): Promise<void> {
const track = songToTrack(song);
const tracks = queue && queue.length > 0
? queue.map(songToTrack)
: [track];
const store = usePlayerStore.getState();
const { isPlaying, volume } = store;
if (isPlaying) {
await fadeOut(store.setVolume, volume, 700);
usePlayerStore.setState({ volume });
}
usePlayerStore.getState().playTrack(track, tracks);
}
/**
* Append the song to the existing queue (if not already there) and immediately jump to it.
* Existing queue stays intact — different from playSongNow which replaces the queue.
*/
export async function enqueueAndPlay(song: SubsonicSong): Promise<void> {
const track = songToTrack(song);
const store = usePlayerStore.getState();
const { isPlaying, volume, queue } = store;
if (isPlaying) {
await fadeOut(store.setVolume, volume, 700);
usePlayerStore.setState({ volume });
}
if (!queue.some(t => t.id === track.id)) {
usePlayerStore.getState().enqueue([track]);
}
// playTrack with no queue arg uses the current state.queue, finds the track by id,
// and sets queueIndex accordingly.
usePlayerStore.getState().playTrack(track);
}