From 00832b4edab3c3ba08a093d5ab6b0cf08afc940a Mon Sep 17 00:00:00 2001 From: cyph3rasi Date: Mon, 13 Jul 2026 03:34:38 -0700 Subject: [PATCH] Let playback traverse the full loaded library and show accessible checkmark feedback when tracks are added to the queue. Hop-State: A_06FNP2RD4Z5F8MM3JJDGC90 Hop-Proposal: R_06FNP2QTMQDCQ4AD45XGSN8 Hop-Task: T_06FNP1W0F7R3YK8AM2XH0EG Hop-Attempt: AT_06FNP1W0F4Z53V2FHTSWX7R --- src/App.tsx | 35 ++++++++++++++++++++++++++++++++--- src/hooks/usePlayer.test.ts | 28 ++++++++++++++++++++++++++++ src/hooks/usePlayer.ts | 6 +++--- src/styles.css | 6 +++++- 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index c1b3e92..c9eb15d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,8 @@ -import { FormEvent, useEffect, useMemo, useState } from "react"; +import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; import { Album as AlbumIcon, AudioLines, + Check, ChevronRight, CircleUserRound, Disc3, @@ -46,6 +47,7 @@ import { buildApiUrl, requestSubsonic, streamUrl } from "./lib/subsonic"; import type { Album, ConnectionStatus, CoverTone, ServerConfig, Track, View } from "./types"; const tones: CoverTone[] = ["ember", "moss", "dusk", "clay", "indigo", "sand"]; +const queuePreviewLimit = 100; function Cover({ album, size = "medium" }: { album: Pick; size?: "small" | "medium" | "large" }) { return ( @@ -76,6 +78,9 @@ export default function App() { const [view, setView] = useState("home"); const [mobileNav, setMobileNav] = useState(false); const [queueOpen, setQueueOpen] = useState(false); + const [queuedTrackIds, setQueuedTrackIds] = useState(() => new Set()); + const [queueAnnouncement, setQueueAnnouncement] = useState({ key: 0, message: "" }); + const queueFeedbackTimers = useRef(new Map()); const [query, setQuery] = useState(""); const [libraryAlbums, setLibraryAlbums] = useState(demoAlbums); const [libraryTracks, setLibraryTracks] = useState(demoTracks); @@ -120,6 +125,29 @@ export default function App() { return next; }); + const addTrackToQueue = (track: Track) => { + player.addToQueue(track); + setQueuedTrackIds((current) => new Set(current).add(track.id)); + setQueueAnnouncement((current) => ({ key: current.key + 1, message: `${track.title} added to queue` })); + + const existingTimer = queueFeedbackTimers.current.get(track.id); + if (existingTimer !== undefined) window.clearTimeout(existingTimer); + const timer = window.setTimeout(() => { + setQueuedTrackIds((current) => { + const next = new Set(current); + next.delete(track.id); + return next; + }); + queueFeedbackTimers.current.delete(track.id); + }, 1500); + queueFeedbackTimers.current.set(track.id, timer); + }; + + useEffect(() => () => { + queueFeedbackTimers.current.forEach((timer) => window.clearTimeout(timer)); + queueFeedbackTimers.current.clear(); + }, []); + const fetchServerLibrary = async (config: ServerConfig): Promise => { await requestSubsonic(config, "ping"); const [serverAlbums, serverSongs] = await Promise.all([ @@ -406,14 +434,15 @@ export default function App() {
{view === "favorites" ? "Kept close" : view === "songs" ? "Every track" : "A familiar thread"}

{view === "favorites" ? "Favorite tracks" : view === "songs" ? "Songs" : "Heavy rotation"}

{visibleTracks.length} tracks
-
{visibleTracks.slice(0, view === "songs" ? visibleTracks.length : view === "home" && !query ? 6 : 30).map((track, index) =>
{track.album} toggleFavorite(track.id)}>{formatTime(track.duration)} player.addToQueue(track)}>
)}
+
{visibleTracks.slice(0, view === "songs" ? visibleTracks.length : view === "home" && !query ? 6 : 30).map((track, index) => { const justQueued = queuedTrackIds.has(track.id); return
{track.album} toggleFavorite(track.id)}>{formatTime(track.duration)} addTrackToQueue(track)}>{justQueued ? : }
; })}
+ {queueAnnouncement.message}
} )} - {hasLibraryTracks && } + {hasLibraryTracks && } {hasLibraryTracks ?