mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
e3aabd98b7
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>
62 lines
1.9 KiB
TypeScript
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);
|
|
}
|