Skip to content

Commit 69db2b9

Browse files
authored
Merge pull request #17 from raotaohub/master
feature: drag the progress bar
2 parents 19b5e2e + 06952c9 commit 69db2b9

File tree

6 files changed

+181
-28
lines changed

6 files changed

+181
-28
lines changed

src/components/controller.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ type PlaybackControlsProps = {
2020
currentTime: number | undefined;
2121
audioDurationSeconds: number | undefined;
2222
bufferedSeconds: number | undefined;
23-
onSeek?: (second: number) => void;
2423
onToggleMenu?: () => void;
2524
onToggleMuted: () => void;
2625
order: PlaylistOrder;
2726
onOrderChange: (order: PlaylistOrder) => void;
2827
loop: PlaylistLoop;
2928
onLoopChange: (loop: PlaylistLoop) => void;
29+
progressBarRef: React.RefObject<HTMLDivElement>;
30+
playedPercentage: number;
3031
};
3132

3233
export function PlaybackControls({
@@ -37,13 +38,14 @@ export function PlaybackControls({
3738
currentTime,
3839
audioDurationSeconds,
3940
bufferedSeconds,
40-
onSeek,
4141
onToggleMenu,
4242
onToggleMuted,
4343
order,
4444
onOrderChange,
4545
loop,
4646
onLoopChange,
47+
progressBarRef,
48+
playedPercentage,
4749
}: PlaybackControlsProps) {
4850
const handleVolumeBarMouseDown = useCallback(
4951
(e: React.MouseEvent<HTMLDivElement>) => {
@@ -88,10 +90,10 @@ export function PlaybackControls({
8890
return (
8991
<div className="aplayer-controller">
9092
<ProgressBar
93+
ref={progressBarRef}
9194
themeColor={themeColor}
92-
playedPercentage={currentTime / audioDurationSeconds}
95+
playedPercentage={playedPercentage}
9396
bufferedPercentage={bufferedSeconds / audioDurationSeconds}
94-
onSeek={(percentage) => onSeek?.(percentage * audioDurationSeconds)}
9597
/>
9698
<div className="aplayer-time">
9799
<span className="aplayer-time-inner">

src/components/player.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,13 @@ export function APlayer({
187187
currentTime={audioControl.currentTime}
188188
audioDurationSeconds={audioControl.duration}
189189
bufferedSeconds={audioControl.bufferedSeconds}
190-
onSeek={(second) => audioControl.seek(second)}
191190
onToggleMenu={() => setPlaylistOpen((open) => !open)}
192191
order={playlist.order}
193192
onOrderChange={playlist.setOrder}
194193
loop={playlist.loop}
195194
onLoopChange={playlist.setLoop}
195+
progressBarRef={audioControl.progressBarRef}
196+
playedPercentage={audioControl.playedPercentage}
196197
/>
197198
</div>
198199
<div className="aplayer-notice" style={notice.style}>

src/components/progress.tsx

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,18 @@
1-
import React, { useCallback } from "react";
1+
import React, { Ref } from "react";
22
import { ReactComponent as IconLoading } from "../assets/loading.svg";
33

44
type ProgressBarProps = {
55
themeColor: string;
66
bufferedPercentage: number;
77
playedPercentage: number;
8-
9-
onSeek?: (percentage: number) => void;
108
};
119

12-
export function ProgressBar({
13-
themeColor,
14-
bufferedPercentage,
15-
playedPercentage,
16-
onSeek,
17-
}: ProgressBarProps) {
18-
const handleMouseDown = useCallback(
19-
(e: React.MouseEvent<HTMLDivElement>) => {
20-
const barDimensions = e.currentTarget.getBoundingClientRect();
21-
const deltaX = e.clientX - barDimensions.x;
22-
const percentage = deltaX / barDimensions.width;
23-
24-
onSeek?.(percentage);
25-
},
26-
[onSeek]
27-
);
28-
10+
export const ProgressBar = React.forwardRef(function ProgressBar(
11+
{ themeColor, bufferedPercentage, playedPercentage }: ProgressBarProps,
12+
ref: Ref<HTMLDivElement>
13+
) {
2914
return (
30-
<div className="aplayer-bar-wrap" onMouseDown={handleMouseDown}>
15+
<div ref={ref} className="aplayer-bar-wrap">
3116
<div className="aplayer-bar">
3217
<div
3318
className="aplayer-loaded"
@@ -52,4 +37,4 @@ export function ProgressBar({
5237
</div>
5338
</div>
5439
);
55-
}
40+
});

src/hooks/useAudioControl.ts

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
import React, { useCallback, useEffect, useRef } from "react";
1+
import React, {
2+
useCallback,
3+
useEffect,
4+
useMemo,
5+
useRef,
6+
useState,
7+
} from "react";
28
import { useSyncExternalStore } from "use-sync-external-store/shim/index.js";
9+
import { computePercentage } from "../utils/computePercentage";
310

411
type CreateAudioElementOptions = {
512
initialVolume?: number;
@@ -69,8 +76,88 @@ function useCreateAudioElement(options?: CreateAudioElementOptions) {
6976
return audioElementRef;
7077
}
7178

79+
function useProgressBar(audio: ReturnType<typeof useCreateAudioElement>) {
80+
const progressBarRef = useRef<HTMLDivElement>(null);
81+
const [barState, setBarState] = useState({
82+
percentage: 0,
83+
isThumbDown: false,
84+
});
85+
86+
const seek = useCallback(
87+
(second: number) => {
88+
if (audio.current && !Number.isNaN(second)) {
89+
audio.current.currentTime = second;
90+
}
91+
},
92+
[audio]
93+
);
94+
95+
const handlePercentage = useCallback((percentage: number) => {
96+
if (!Number.isNaN(percentage)) {
97+
setBarState((prev) => ({ ...prev, percentage: percentage }));
98+
}
99+
}, []);
100+
101+
const handleIsThumbDown = useCallback((is: boolean) => {
102+
setBarState((prev) => ({ ...prev, isThumbDown: is }));
103+
}, []);
104+
105+
const thumbMove = useCallback(
106+
(e: MouseEvent) => {
107+
if (!progressBarRef.current || !audio.current) return;
108+
109+
const percentage = computePercentage(e, progressBarRef);
110+
handlePercentage(percentage);
111+
},
112+
[audio, handlePercentage]
113+
);
114+
115+
const thumbUp = useCallback(
116+
(e: MouseEvent) => {
117+
if (!progressBarRef.current || !audio.current) return;
118+
119+
document.removeEventListener("mouseup", thumbUp);
120+
document.removeEventListener("mousemove", thumbMove);
121+
const percentage = computePercentage(e, progressBarRef);
122+
const currentTime = audio.current.duration * percentage;
123+
124+
handlePercentage(percentage);
125+
handleIsThumbDown(false);
126+
seek(currentTime);
127+
},
128+
[audio, handleIsThumbDown, handlePercentage, seek, thumbMove]
129+
);
130+
131+
useEffect(() => {
132+
const ref = progressBarRef.current;
133+
if (ref) {
134+
ref.addEventListener("mousedown", (e) => {
135+
handleIsThumbDown(true);
136+
const percentage = computePercentage(e, progressBarRef);
137+
handlePercentage(percentage);
138+
139+
document.addEventListener("mousemove", thumbMove);
140+
document.addEventListener("mouseup", thumbUp);
141+
});
142+
}
143+
144+
return () => {
145+
if (ref) {
146+
ref.removeEventListener("mousedown", () => {
147+
document.addEventListener("mousemove", thumbMove);
148+
document.addEventListener("mouseup", thumbUp);
149+
});
150+
}
151+
};
152+
// eslint-disable-next-line react-hooks/exhaustive-deps
153+
}, []);
154+
155+
return { progressBarRef, barState };
156+
}
157+
72158
export function useAudioControl(options: CreateAudioElementOptions) {
73159
const audioElementRef = useCreateAudioElement(options);
160+
const { progressBarRef, barState } = useProgressBar(audioElementRef);
74161

75162
const playAudio = useCallback(
76163
async (src: string) => {
@@ -293,6 +380,12 @@ export function useAudioControl(options: CreateAudioElementOptions) {
293380
() => undefined
294381
);
295382

383+
const playedPercentage = useMemo(() => {
384+
if (barState.isThumbDown) return barState.percentage;
385+
if (!currentTime || !duration) return 0;
386+
return currentTime / duration;
387+
}, [barState.isThumbDown, barState.percentage, currentTime, duration]);
388+
296389
return {
297390
volume,
298391
setVolume,
@@ -306,5 +399,7 @@ export function useAudioControl(options: CreateAudioElementOptions) {
306399
togglePlay,
307400
seek,
308401
isLoading,
402+
progressBarRef,
403+
playedPercentage,
309404
};
310405
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { expect, test } from "vitest";
2+
import { computePercentage } from "./computePercentage";
3+
4+
test("Return 0 if progressBarRef.current is undefined", () => {
5+
expect(
6+
computePercentage(new MouseEvent("mouseup", {}), { current: null })
7+
).toBe(0);
8+
});
9+
10+
test("Return 0 if progressBarRef.current is undefined", () => {
11+
expect(
12+
computePercentage(new MouseEvent("mousemove"), { current: null })
13+
).toBe(0);
14+
});
15+
16+
test("Return 0 if progressBarRef.current is undefined", () => {
17+
expect(
18+
computePercentage(new MouseEvent("mousedown"), {
19+
current: null,
20+
})
21+
).toBe(0);
22+
});
23+
24+
/* MOCK DOM */
25+
test("Return percentage when mousedown event", () => {
26+
const container = document.createElement("div");
27+
container.style.width = "200px";
28+
container.style.height = "2px";
29+
const mouseEvent = new MouseEvent("mousedown", {
30+
clientX: 50,
31+
clientY: 50,
32+
});
33+
34+
/* hack ! no value in the node environment , so overwrite they */
35+
container.clientWidth = 200;
36+
container.getBoundingClientRect = () => ({
37+
x: 10,
38+
y: 10,
39+
width: 300,
40+
height: 2,
41+
top: 10,
42+
right: 300,
43+
bottom: 10,
44+
left: 10,
45+
toJSON: function () {
46+
return "";
47+
},
48+
});
49+
50+
container.addEventListener("mousedown", function (e) {
51+
const val = computePercentage(e, { current: container });
52+
expect(val).toBe(0.2);
53+
});
54+
55+
container.dispatchEvent(mouseEvent);
56+
});

src/utils/computePercentage.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export function computePercentage(
2+
eventTarget: MouseEvent,
3+
progressBarRef: React.RefObject<HTMLDivElement>
4+
) {
5+
if (!progressBarRef.current) return 0;
6+
let percentage =
7+
(eventTarget.clientX -
8+
progressBarRef.current.getBoundingClientRect().left) /
9+
progressBarRef.current.clientWidth;
10+
percentage = Math.max(percentage, 0);
11+
percentage = Math.min(percentage, 1);
12+
percentage = Math.floor(percentage * 100) / 100;
13+
return percentage;
14+
}

0 commit comments

Comments
 (0)