Skip to content

Commit 2c5ec40

Browse files
committed
chore: move playback progress to a hook
1 parent baf0acc commit 2c5ec40

File tree

4 files changed

+223
-177
lines changed

4 files changed

+223
-177
lines changed

src/App.jsx

Lines changed: 45 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { useNetwork } from "./hooks/useNetwork";
1414
import { useGradientState } from "./hooks/useGradientState";
1515
import { useSpotifyData } from "./hooks/useSpotifyData";
1616
import { useSpotifyPlayerState } from "./hooks/useSpotifyPlayerState";
17+
import { usePlaybackProgress, PlaybackProgressContext } from "./hooks/usePlaybackProgress";
1718
import { useSpotifyPlayerControls, DeviceSwitcherContext } from "./hooks/useSpotifyPlayerControls";
1819
import { useBluetooth } from "./hooks/useBluetooth";
1920

@@ -57,6 +58,9 @@ function App() {
5758
refreshPlaybackState,
5859
} = useSpotifyPlayerState(accessToken);
5960

61+
// Use the new playback progress hook
62+
const playbackProgress = usePlaybackProgress(accessToken);
63+
6064
const handleOpenDeviceSwitcher = () => {
6165
setIsDeviceSwitcherOpen(true);
6266
};
@@ -253,46 +257,48 @@ function App() {
253257
}
254258

255259
return (
256-
<DeviceSwitcherContext.Provider value={deviceSwitcherContextValue}>
257-
<Router>
258-
<main className="overflow-hidden relative min-h-screen rounded-2xl">
259-
<div
260-
style={{
261-
backgroundImage: generateMeshGradient([
262-
currentColor1,
263-
currentColor2,
264-
currentColor3,
265-
currentColor4,
266-
]),
267-
transition: "background-image 0.5s linear",
268-
}}
269-
className="absolute inset-0 bg-black"
270-
/>
271-
272-
<div className="relative z-10">
273-
{content}
274-
{!isConnected && showNoNetwork && <NetworkScreen />}
275-
<BluetoothPairingModal
276-
pairingRequest={pairingRequest}
277-
isConnecting={isConnecting}
278-
onAccept={acceptPairing}
279-
onDeny={denyPairing}
280-
/>
281-
<BluetoothNetworkModal
282-
show={showNetworkPrompt && !isConnected}
283-
deviceName={lastConnectedDevice?.name}
284-
onCancel={handleNetworkCancel}
285-
isConnecting={isConnecting}
260+
<PlaybackProgressContext.Provider value={playbackProgress}>
261+
<DeviceSwitcherContext.Provider value={deviceSwitcherContextValue}>
262+
<Router>
263+
<main className="overflow-hidden relative min-h-screen rounded-2xl">
264+
<div
265+
style={{
266+
backgroundImage: generateMeshGradient([
267+
currentColor1,
268+
currentColor2,
269+
currentColor3,
270+
currentColor4,
271+
]),
272+
transition: "background-image 0.5s linear",
273+
}}
274+
className="absolute inset-0 bg-black"
286275
/>
287-
<DeviceSwitcherModal
288-
isOpen={isDeviceSwitcherOpen}
289-
onClose={handleCloseDeviceSwitcher}
290-
accessToken={accessToken}
291-
/>
292-
</div>
293-
</main>
294-
</Router>
295-
</DeviceSwitcherContext.Provider>
276+
277+
<div className="relative z-10">
278+
{content}
279+
{!isConnected && showNoNetwork && <NetworkScreen />}
280+
<BluetoothPairingModal
281+
pairingRequest={pairingRequest}
282+
isConnecting={isConnecting}
283+
onAccept={acceptPairing}
284+
onDeny={denyPairing}
285+
/>
286+
<BluetoothNetworkModal
287+
show={showNetworkPrompt && !isConnected}
288+
deviceName={lastConnectedDevice?.name}
289+
onCancel={handleNetworkCancel}
290+
isConnecting={isConnecting}
291+
/>
292+
<DeviceSwitcherModal
293+
isOpen={isDeviceSwitcherOpen}
294+
onClose={handleCloseDeviceSwitcher}
295+
accessToken={accessToken}
296+
/>
297+
</div>
298+
</main>
299+
</Router>
300+
</DeviceSwitcherContext.Provider>
301+
</PlaybackProgressContext.Provider>
296302
);
297303
}
298304

src/components/player/NowPlaying.jsx

Lines changed: 13 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useSpotifyPlayerControls } from "../../hooks/useSpotifyPlayerControls";
44
import { useGradientState } from "../../hooks/useGradientState";
55
import { useNavigation } from "../../hooks/useNavigation";
66
import { useLyrics } from "../../hooks/useLyrics";
7+
import { usePlaybackProgress } from "../../hooks/usePlaybackProgress";
78
import ProgressBar from "./ProgressBar";
89
import {
910
HeartIcon,
@@ -21,12 +22,9 @@ import {
2122
const NowPlaying = ({ accessToken, currentPlayback, onClose, updateGradientColors, onOpenDeviceSwitcher }) => {
2223
const [isLiked, setIsLiked] = useState(false);
2324
const [isCheckingLike, setIsCheckingLike] = useState(false);
24-
const [realTimeProgress, setRealTimeProgress] = useState(0);
2525
const [isProgressScrubbing, setIsProgressScrubbing] = useState(false);
26-
const progressTimerRef = useRef(null);
27-
const lastUpdateTimeRef = useRef(Date.now());
28-
const currentTrackIdRef = useRef(null);
2926
const containerRef = useRef(null);
27+
const currentTrackIdRef = useRef(null);
3028
const isDJPlaylist =
3129
currentPlayback?.context?.uri === "spotify:playlist:37i9dQZF1EYkqdzj48dyYq";
3230

@@ -42,6 +40,14 @@ const NowPlaying = ({ accessToken, currentPlayback, onClose, updateGradientColor
4240
sendDJSignal,
4341
} = useSpotifyPlayerControls(accessToken);
4442

43+
const {
44+
progressMs,
45+
isPlaying,
46+
duration,
47+
progressPercentage,
48+
updateProgress
49+
} = usePlaybackProgress(accessToken);
50+
4551
const handlePlayPause = async () => {
4652
if (isPlaying) {
4753
await pausePlayback();
@@ -92,9 +98,6 @@ const NowPlaying = ({ accessToken, currentPlayback, onClose, updateGradientColor
9298
: currentPlayback.item.album.images[0].url
9399
: "/images/not-playing.webp";
94100

95-
const isPlaying = currentPlayback?.is_playing || false;
96-
const duration = currentPlayback?.item?.duration_ms || 1;
97-
const progressPercentage = (realTimeProgress / duration) * 100;
98101
const trackId = currentPlayback?.item?.id;
99102

100103
useEffect(() => {
@@ -131,51 +134,16 @@ const NowPlaying = ({ accessToken, currentPlayback, onClose, updateGradientColor
131134
checkCurrentTrackLiked();
132135
}, [trackId, checkIsTrackLiked, currentPlayback?.item?.type]);
133136

134-
useEffect(() => {
135-
if (currentPlayback?.progress_ms !== undefined && !isProgressScrubbing) {
136-
setRealTimeProgress(currentPlayback.progress_ms);
137-
lastUpdateTimeRef.current = Date.now();
138-
}
139-
}, [currentPlayback?.progress_ms, isProgressScrubbing]);
140-
141-
useEffect(() => {
142-
if (progressTimerRef.current) {
143-
clearInterval(progressTimerRef.current);
144-
progressTimerRef.current = null;
145-
}
146-
147-
if (isPlaying && !isProgressScrubbing) {
148-
progressTimerRef.current = setInterval(() => {
149-
const now = Date.now();
150-
const elapsed = now - lastUpdateTimeRef.current;
151-
lastUpdateTimeRef.current = now;
152-
153-
setRealTimeProgress((prev) => {
154-
const newProgress = Math.min(prev + elapsed, duration);
155-
return newProgress;
156-
});
157-
}, 100);
158-
}
159-
160-
return () => {
161-
if (progressTimerRef.current) {
162-
clearInterval(progressTimerRef.current);
163-
progressTimerRef.current = null;
164-
}
165-
};
166-
}, [isPlaying, isProgressScrubbing, duration]);
167-
168137
const handleSkipNext = async () => {
169138
await skipToNext();
170139
};
171140

172141
const handleSkipPrevious = async () => {
173142
const RESTART_THRESHOLD_MS = 3000;
174143

175-
if (realTimeProgress > RESTART_THRESHOLD_MS) {
144+
if (progressMs > RESTART_THRESHOLD_MS) {
176145
await seekToPosition(0);
177-
setRealTimeProgress(0);
178-
lastUpdateTimeRef.current = Date.now();
146+
updateProgress(0);
179147
} else {
180148
await skipToPrevious();
181149
}
@@ -203,8 +171,7 @@ const NowPlaying = ({ accessToken, currentPlayback, onClose, updateGradientColor
203171
try {
204172
if (currentPlayback?.item) {
205173
await seekToPosition(position);
206-
setRealTimeProgress(position);
207-
lastUpdateTimeRef.current = Date.now();
174+
updateProgress(position);
208175
}
209176
} catch (error) {
210177
console.error("Error seeking:", error);

src/components/player/ProgressBar.jsx

Lines changed: 25 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,24 @@
11
import React, { useState, useEffect, useRef } from "react";
2+
import { usePlaybackProgressConsumer } from "../../hooks/usePlaybackProgress";
23

34
const ProgressBar = ({
4-
progress,
5-
isPlaying,
6-
durationMs,
5+
progress: externalProgress,
6+
isPlaying: externalIsPlaying,
7+
durationMs: externalDurationMs,
78
onSeek,
89
onPlayPause,
910
onScrubbingChange,
10-
onProgressUpdate,
1111
}) => {
12-
const [displayProgress, setDisplayProgress] = useState(progress);
12+
const progressContext = usePlaybackProgressConsumer();
13+
14+
const progress = externalProgress ?? progressContext.progressPercentage;
15+
const isPlaying = externalIsPlaying ?? progressContext.isPlaying;
16+
const durationMs = externalDurationMs ?? progressContext.duration;
17+
1318
const [isScrubbing, setIsScrubbing] = useState(false);
1419
const [scrubbingProgress, setScrubbingProgress] = useState(null);
15-
const animationFrameRef = useRef(null);
1620
const wasPlayingRef = useRef(false);
1721
const containerRef = useRef(null);
18-
const isMountedRef = useRef(false);
19-
const lastTimestampRef = useRef(performance.now());
20-
21-
const progressUpdateRef = useRef(onProgressUpdate);
22-
const displayProgressRef = useRef(displayProgress);
23-
const scrubbingProgressRef = useRef(scrubbingProgress);
24-
25-
useEffect(() => {
26-
progressUpdateRef.current = onProgressUpdate;
27-
}, [onProgressUpdate]);
28-
29-
useEffect(() => {
30-
displayProgressRef.current = displayProgress;
31-
}, [displayProgress]);
32-
33-
useEffect(() => {
34-
scrubbingProgressRef.current = scrubbingProgress;
35-
}, [scrubbingProgress]);
36-
37-
useEffect(() => {
38-
if (progressUpdateRef.current) {
39-
progressUpdateRef.current(
40-
scrubbingProgressRef.current ?? displayProgressRef.current
41-
);
42-
}
43-
}, [displayProgress, scrubbingProgress]);
44-
45-
useEffect(() => {
46-
if (!isScrubbing && progress !== null && progress !== displayProgress) {
47-
setDisplayProgress(progress);
48-
}
49-
}, [progress, isScrubbing, displayProgress]);
50-
51-
useEffect(() => {
52-
if (animationFrameRef.current) {
53-
cancelAnimationFrame(animationFrameRef.current);
54-
animationFrameRef.current = null;
55-
}
56-
57-
if (isPlaying && !isScrubbing && durationMs > 0) {
58-
lastTimestampRef.current = performance.now();
59-
60-
const animate = (timestamp) => {
61-
if (!isPlaying || !durationMs || isScrubbing) {
62-
return;
63-
}
64-
65-
const deltaTime = timestamp - lastTimestampRef.current;
66-
lastTimestampRef.current = timestamp;
67-
68-
const progressIncrement = (deltaTime / durationMs) * 100;
69-
70-
setDisplayProgress((prevProgress) => {
71-
return Math.min(prevProgress + progressIncrement, 100);
72-
});
73-
74-
animationFrameRef.current = requestAnimationFrame(animate);
75-
};
76-
77-
animationFrameRef.current = requestAnimationFrame(animate);
78-
}
79-
80-
return () => {
81-
if (animationFrameRef.current) {
82-
cancelAnimationFrame(animationFrameRef.current);
83-
animationFrameRef.current = null;
84-
}
85-
};
86-
}, [isPlaying, isScrubbing, durationMs]);
8722

8823
const handleClick = () => {
8924
setIsScrubbing(true);
@@ -102,14 +37,14 @@ const ProgressBar = ({
10237

10338
setScrubbingProgress((prev) => {
10439
const nextValue =
105-
(prev ?? displayProgress) + (delta > 0 ? step : -step);
40+
(prev ?? progress) + (delta > 0 ? step : -step);
10641
return Math.max(0, Math.min(100, nextValue));
10742
});
10843
};
10944

11045
window.addEventListener("wheel", handleWheel, { passive: false });
11146
return () => window.removeEventListener("wheel", handleWheel);
112-
}, [isScrubbing, displayProgress]);
47+
}, [isScrubbing, progress]);
11348

11449
useEffect(() => {
11550
const handleKeyDown = (event) => {
@@ -124,7 +59,9 @@ const ProgressBar = ({
12459
if (scrubbingProgress !== null) {
12560
const seekMs = Math.floor((scrubbingProgress / 100) * durationMs);
12661
onSeek(seekMs);
127-
setDisplayProgress(scrubbingProgress);
62+
if (progressContext.updateProgress) {
63+
progressContext.updateProgress(seekMs);
64+
}
12865
}
12966

13067
setScrubbingProgress(null);
@@ -136,7 +73,6 @@ const ProgressBar = ({
13673
setIsScrubbing(false);
13774
onScrubbingChange(false);
13875
setScrubbingProgress(null);
139-
setDisplayProgress(progress);
14076
return false;
14177
}
14278
};
@@ -151,23 +87,21 @@ const ProgressBar = ({
15187
onSeek,
15288
onPlayPause,
15389
onScrubbingChange,
154-
progress,
90+
progressContext,
15591
]);
15692

157-
const finalProgress = scrubbingProgress ?? displayProgress;
93+
const finalProgress = scrubbingProgress ?? progress;
15894
const shouldShowTimestampOutside = finalProgress < 8;
15995

16096
return (
16197
<div
16298
ref={containerRef}
163-
className={`relative transition-all duration-200 ease-in-out ${
164-
isScrubbing ? "translate-y-8" : ""
165-
}`}
99+
className={`relative transition-all duration-200 ease-in-out ${isScrubbing ? "translate-y-8" : ""
100+
}`}
166101
>
167102
<div
168-
className={`relative w-full bg-white/20 rounded-full overflow-hidden cursor-pointer transition-all duration-300 ${
169-
isScrubbing ? "h-8" : "h-2 mt-4"
170-
}`}
103+
className={`relative w-full bg-white/20 rounded-full overflow-hidden cursor-pointer transition-all duration-300 ${isScrubbing ? "h-8" : "h-2 mt-4"
104+
}`}
171105
onClick={handleClick}
172106
>
173107
<div
@@ -184,11 +118,10 @@ const ProgressBar = ({
184118
}}
185119
>
186120
<span
187-
className={`text-lg font-[580] absolute ${
188-
shouldShowTimestampOutside
189-
? "left-2 text-black/40"
190-
: "right-full pr-2 text-black/40"
191-
}`}
121+
className={`text-lg font-[580] absolute ${shouldShowTimestampOutside
122+
? "left-2 text-black/40"
123+
: "right-full pr-2 text-black/40"
124+
}`}
192125
>
193126
{formatTime(Math.floor((finalProgress / 100) * durationMs))}
194127
</span>

0 commit comments

Comments
 (0)