Skip to content

Commit

Permalink
Merge pull request #17 from raotaohub/master
Browse files Browse the repository at this point in the history
feature: drag the progress bar
  • Loading branch information
SevenOutman authored May 22, 2023
2 parents 19b5e2e + 06952c9 commit 69db2b9
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 28 deletions.
10 changes: 6 additions & 4 deletions src/components/controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ type PlaybackControlsProps = {
currentTime: number | undefined;
audioDurationSeconds: number | undefined;
bufferedSeconds: number | undefined;
onSeek?: (second: number) => void;
onToggleMenu?: () => void;
onToggleMuted: () => void;
order: PlaylistOrder;
onOrderChange: (order: PlaylistOrder) => void;
loop: PlaylistLoop;
onLoopChange: (loop: PlaylistLoop) => void;
progressBarRef: React.RefObject<HTMLDivElement>;
playedPercentage: number;
};

export function PlaybackControls({
Expand All @@ -37,13 +38,14 @@ export function PlaybackControls({
currentTime,
audioDurationSeconds,
bufferedSeconds,
onSeek,
onToggleMenu,
onToggleMuted,
order,
onOrderChange,
loop,
onLoopChange,
progressBarRef,
playedPercentage,
}: PlaybackControlsProps) {
const handleVolumeBarMouseDown = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
Expand Down Expand Up @@ -88,10 +90,10 @@ export function PlaybackControls({
return (
<div className="aplayer-controller">
<ProgressBar
ref={progressBarRef}
themeColor={themeColor}
playedPercentage={currentTime / audioDurationSeconds}
playedPercentage={playedPercentage}
bufferedPercentage={bufferedSeconds / audioDurationSeconds}
onSeek={(percentage) => onSeek?.(percentage * audioDurationSeconds)}
/>
<div className="aplayer-time">
<span className="aplayer-time-inner">
Expand Down
3 changes: 2 additions & 1 deletion src/components/player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,13 @@ export function APlayer({
currentTime={audioControl.currentTime}
audioDurationSeconds={audioControl.duration}
bufferedSeconds={audioControl.bufferedSeconds}
onSeek={(second) => audioControl.seek(second)}
onToggleMenu={() => setPlaylistOpen((open) => !open)}
order={playlist.order}
onOrderChange={playlist.setOrder}
loop={playlist.loop}
onLoopChange={playlist.setLoop}
progressBarRef={audioControl.progressBarRef}
playedPercentage={audioControl.playedPercentage}
/>
</div>
<div className="aplayer-notice" style={notice.style}>
Expand Down
29 changes: 7 additions & 22 deletions src/components/progress.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,18 @@
import React, { useCallback } from "react";
import React, { Ref } from "react";
import { ReactComponent as IconLoading } from "../assets/loading.svg";

type ProgressBarProps = {
themeColor: string;
bufferedPercentage: number;
playedPercentage: number;

onSeek?: (percentage: number) => void;
};

export function ProgressBar({
themeColor,
bufferedPercentage,
playedPercentage,
onSeek,
}: ProgressBarProps) {
const handleMouseDown = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const barDimensions = e.currentTarget.getBoundingClientRect();
const deltaX = e.clientX - barDimensions.x;
const percentage = deltaX / barDimensions.width;

onSeek?.(percentage);
},
[onSeek]
);

export const ProgressBar = React.forwardRef(function ProgressBar(
{ themeColor, bufferedPercentage, playedPercentage }: ProgressBarProps,
ref: Ref<HTMLDivElement>
) {
return (
<div className="aplayer-bar-wrap" onMouseDown={handleMouseDown}>
<div ref={ref} className="aplayer-bar-wrap">
<div className="aplayer-bar">
<div
className="aplayer-loaded"
Expand All @@ -52,4 +37,4 @@ export function ProgressBar({
</div>
</div>
);
}
});
97 changes: 96 additions & 1 deletion src/hooks/useAudioControl.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import React, { useCallback, useEffect, useRef } from "react";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useSyncExternalStore } from "use-sync-external-store/shim/index.js";
import { computePercentage } from "../utils/computePercentage";

type CreateAudioElementOptions = {
initialVolume?: number;
Expand Down Expand Up @@ -69,8 +76,88 @@ function useCreateAudioElement(options?: CreateAudioElementOptions) {
return audioElementRef;
}

function useProgressBar(audio: ReturnType<typeof useCreateAudioElement>) {
const progressBarRef = useRef<HTMLDivElement>(null);
const [barState, setBarState] = useState({
percentage: 0,
isThumbDown: false,
});

const seek = useCallback(
(second: number) => {
if (audio.current && !Number.isNaN(second)) {
audio.current.currentTime = second;
}
},
[audio]
);

const handlePercentage = useCallback((percentage: number) => {
if (!Number.isNaN(percentage)) {
setBarState((prev) => ({ ...prev, percentage: percentage }));
}
}, []);

const handleIsThumbDown = useCallback((is: boolean) => {
setBarState((prev) => ({ ...prev, isThumbDown: is }));
}, []);

const thumbMove = useCallback(
(e: MouseEvent) => {
if (!progressBarRef.current || !audio.current) return;

const percentage = computePercentage(e, progressBarRef);
handlePercentage(percentage);
},
[audio, handlePercentage]
);

const thumbUp = useCallback(
(e: MouseEvent) => {
if (!progressBarRef.current || !audio.current) return;

document.removeEventListener("mouseup", thumbUp);
document.removeEventListener("mousemove", thumbMove);
const percentage = computePercentage(e, progressBarRef);
const currentTime = audio.current.duration * percentage;

handlePercentage(percentage);
handleIsThumbDown(false);
seek(currentTime);
},
[audio, handleIsThumbDown, handlePercentage, seek, thumbMove]
);

useEffect(() => {
const ref = progressBarRef.current;
if (ref) {
ref.addEventListener("mousedown", (e) => {
handleIsThumbDown(true);
const percentage = computePercentage(e, progressBarRef);
handlePercentage(percentage);

document.addEventListener("mousemove", thumbMove);
document.addEventListener("mouseup", thumbUp);
});
}

return () => {
if (ref) {
ref.removeEventListener("mousedown", () => {
document.addEventListener("mousemove", thumbMove);
document.addEventListener("mouseup", thumbUp);
});
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return { progressBarRef, barState };
}

export function useAudioControl(options: CreateAudioElementOptions) {
const audioElementRef = useCreateAudioElement(options);
const { progressBarRef, barState } = useProgressBar(audioElementRef);

const playAudio = useCallback(
async (src: string) => {
Expand Down Expand Up @@ -293,6 +380,12 @@ export function useAudioControl(options: CreateAudioElementOptions) {
() => undefined
);

const playedPercentage = useMemo(() => {
if (barState.isThumbDown) return barState.percentage;
if (!currentTime || !duration) return 0;
return currentTime / duration;
}, [barState.isThumbDown, barState.percentage, currentTime, duration]);

return {
volume,
setVolume,
Expand All @@ -306,5 +399,7 @@ export function useAudioControl(options: CreateAudioElementOptions) {
togglePlay,
seek,
isLoading,
progressBarRef,
playedPercentage,
};
}
56 changes: 56 additions & 0 deletions src/utils/computePercentage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { expect, test } from "vitest";
import { computePercentage } from "./computePercentage";

test("Return 0 if progressBarRef.current is undefined", () => {
expect(
computePercentage(new MouseEvent("mouseup", {}), { current: null })
).toBe(0);
});

test("Return 0 if progressBarRef.current is undefined", () => {
expect(
computePercentage(new MouseEvent("mousemove"), { current: null })
).toBe(0);
});

test("Return 0 if progressBarRef.current is undefined", () => {
expect(
computePercentage(new MouseEvent("mousedown"), {
current: null,
})
).toBe(0);
});

/* MOCK DOM */
test("Return percentage when mousedown event", () => {
const container = document.createElement("div");
container.style.width = "200px";
container.style.height = "2px";
const mouseEvent = new MouseEvent("mousedown", {
clientX: 50,
clientY: 50,
});

/* hack ! no value in the node environment , so overwrite they */
container.clientWidth = 200;
container.getBoundingClientRect = () => ({
x: 10,
y: 10,
width: 300,
height: 2,
top: 10,
right: 300,
bottom: 10,
left: 10,
toJSON: function () {
return "";
},
});

container.addEventListener("mousedown", function (e) {
const val = computePercentage(e, { current: container });
expect(val).toBe(0.2);
});

container.dispatchEvent(mouseEvent);
});
14 changes: 14 additions & 0 deletions src/utils/computePercentage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function computePercentage(
eventTarget: MouseEvent,
progressBarRef: React.RefObject<HTMLDivElement>
) {
if (!progressBarRef.current) return 0;
let percentage =
(eventTarget.clientX -
progressBarRef.current.getBoundingClientRect().left) /
progressBarRef.current.clientWidth;
percentage = Math.max(percentage, 0);
percentage = Math.min(percentage, 1);
percentage = Math.floor(percentage * 100) / 100;
return percentage;
}

1 comment on commit 69db2b9

@vercel
Copy link

@vercel vercel bot commented on 69db2b9 May 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.