From 0c24ba7111360a108902f2a935ddcc85fa675082 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 23 Apr 2026 11:13:34 -0500 Subject: [PATCH] Share playback state across views --- frontend/src/hooks/useClipPlayback.ts | 20 ++++++- frontend/src/pages/GamedayPage.tsx | 81 ++++++++------------------- 2 files changed, 41 insertions(+), 60 deletions(-) diff --git a/frontend/src/hooks/useClipPlayback.ts b/frontend/src/hooks/useClipPlayback.ts index f8bea7e..5361b9f 100644 --- a/frontend/src/hooks/useClipPlayback.ts +++ b/frontend/src/hooks/useClipPlayback.ts @@ -7,17 +7,25 @@ type PlayClipArgs = { url?: string | null; startMs: number; endMs: number; + title?: string; + subtitle?: string; +}; + +export type ClipPlaybackDetails = { + key: string; + title?: string; + subtitle?: string; }; export function useClipPlayback() { const audioRef = useRef(null); const audioContextRef = useRef(null); - const mediaSourceRef = useRef(null); const gainNodeRef = useRef(null); const playbackRangeRef = useRef<{ endSeconds: number } | null>(null); const fadeOutTimerRef = useRef(null); const activeKeyRef = useRef(null); const [activeKey, setActiveKey] = useState(null); + const [activeDetails, setActiveDetails] = useState(null); const [isPlaying, setIsPlaying] = useState(false); const [currentTimeMs, setCurrentTimeMs] = useState(null); @@ -56,7 +64,6 @@ export function useClipPlayback() { source.connect(gain); gain.connect(context.destination); audioContextRef.current = context; - mediaSourceRef.current = source; gainNodeRef.current = gain; } } @@ -100,6 +107,7 @@ export function useClipPlayback() { activeKeyRef.current = null; playbackRangeRef.current = null; setActiveKey(null); + setActiveDetails(null); setIsPlaying(false); setCurrentTimeMs(null); } @@ -131,7 +139,7 @@ export function useClipPlayback() { }, safeDuration); } - async function playClip({ key, url, startMs, endMs }: PlayClipArgs) { + async function playClip({ key, url, startMs, endMs, title, subtitle }: PlayClipArgs) { if (!url) { return; } @@ -146,6 +154,11 @@ export function useClipPlayback() { const nextAudio = ensureAudio(); activeKeyRef.current = key; setActiveKey(key); + setActiveDetails({ + key, + title, + subtitle, + }); setIsPlaying(false); nextAudio.pause(); setPlaybackGain(1); @@ -183,6 +196,7 @@ export function useClipPlayback() { return { activeKey, + activeDetails, currentTimeMs, fadeOutClip, isPlaying, diff --git a/frontend/src/pages/GamedayPage.tsx b/frontend/src/pages/GamedayPage.tsx index 2b1f648..f234064 100644 --- a/frontend/src/pages/GamedayPage.tsx +++ b/frontend/src/pages/GamedayPage.tsx @@ -23,12 +23,6 @@ function clipKey(kind: "assignment" | "library", id: number | string): string { return `${kind}:${id}`; } -type NowPlaying = { - key: string; - title: string; - subtitle: string; -}; - const DEFAULT_FADE_OUT_MS = 1000; function getAvailabilityIconClass(statusCode: number | null | undefined): string { @@ -65,12 +59,18 @@ export function GamedayPage() { const [expandedPlayerId, setExpandedPlayerId] = useState(""); const [playerFilter, setPlayerFilter] = useState<"players" | "nonPlayers" | "all">("players"); const [playerFilterMenuOpen, setPlayerFilterMenuOpen] = useState(false); - const [nowPlaying, setNowPlaying] = useState(null); const selectedPlayerWasManualRef = useRef(false); const playerFilterMenuRef = useRef(null); const hasInitializedExpandedPlayerRef = useRef(false); const teamId = walkup.selectedTeamId; - const { activeKey: playingClipKey, fadeOutClip, isPlaying: isPlaybackPlaying, playClip: playClipPreview, stopClip } = useClipPlayback(); + const { + activeKey: playingClipKey, + activeDetails: nowPlaying, + fadeOutClip, + isPlaying: isPlaybackPlaying, + playClip: playClipPreview, + stopClip, + } = useClipPlayback(); useEffect(() => { const requestedGameId = searchParams.get("gameId"); @@ -84,14 +84,14 @@ export function GamedayPage() { }, [searchParams, selectedGameId, walkup.nextGame]); useEffect(() => { - stopPlayback(); + stopClip(); setExpandedPlayerId(""); hasInitializedExpandedPlayerRef.current = false; - }, [selectedGameId]); + }, [selectedGameId, stopClip]); useEffect(() => { - stopPlayback(); - }, [selectedPlayerId]); + stopClip(); + }, [selectedPlayerId, stopClip]); useEffect(() => { if (!playerFilterMenuOpen) { @@ -214,56 +214,23 @@ export function GamedayPage() { setSelectedGameId(gameId); setSelectedPlayerId(""); setExpandedPlayerId(""); - stopPlayback(); - setSearchParams({ gameId }); - } - - function stopPlayback() { stopClip(); - setNowPlaying(null); + setSearchParams({ gameId }); } function fadeOutPlayback(durationMs = DEFAULT_FADE_OUT_MS) { fadeOutClip(durationMs); } - async function playAudio( - url: string | null | undefined, - key: string, - playingItem: NowPlaying, - startMs: number, - endMs: number, - ) { - if (!url) { - return; - } - - if (playingClipKey === key && isPlaybackPlaying) { - stopPlayback(); - return; - } - - setNowPlaying(playingItem); - try { - await playClipPreview({ key, url, startMs, endMs }); - } catch (error) { - stopPlayback(); - throw error; - } - } - async function playClip(clip: AudioClip) { - await playAudio( - clip.normalized_url, - clipKey("library", clip.id), - { - key: clipKey("library", clip.id), - title: clip.label, - subtitle: formatMemberName(selectedPlayer), - }, - clip.start_ms, - clip.end_ms, - ); + await playClipPreview({ + key: clipKey("library", clip.id), + url: clip.normalized_url, + startMs: clip.start_ms, + endMs: clip.end_ms, + title: clip.label, + subtitle: formatMemberName(selectedPlayer), + }); } if (!walkup.isTeamSnap) { @@ -280,11 +247,11 @@ export function GamedayPage() {
Now Playing - {nowPlaying.title} - {nowPlaying.subtitle} + {nowPlaying.title ?? "Clip"} + {nowPlaying.subtitle ? {nowPlaying.subtitle} : null}
-