Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 8 additions & 46 deletions src/components/controller.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { ReactComponent as IconVolumeUp } from "../assets/volume-up.svg";
import { ReactComponent as IconVolumeDown } from "../assets/volume-down.svg";
import { ReactComponent as IconVolumeOff } from "../assets/volume-off.svg";
import { ReactComponent as IconMenu } from "../assets/menu.svg";
import { ReactComponent as IconOrderList } from "../assets/order-list.svg";
import { ReactComponent as IconOrderRandom } from "../assets/order-random.svg";
Expand All @@ -11,6 +8,7 @@ import { formatAudioDuration } from "../utils/formatAudioDuration";
import { ProgressBar } from "./progress";
import React, { useCallback } from "react";
import { PlaylistLoop, PlaylistOrder } from "../hooks/usePlaylist";
import { Volume } from "./volume";

type PlaybackControlsProps = {
themeColor: string;
Expand Down Expand Up @@ -45,21 +43,6 @@ export function PlaybackControls({
onLoopChange,
onSeek,
}: PlaybackControlsProps) {
const handleVolumeBarMouseDown = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const volumeBarElement = e.currentTarget;
const volumeBarRect = volumeBarElement.getBoundingClientRect();

onChangeVolume(
Math.min(
1,
Math.max(0, (volumeBarRect.bottom - e.clientY) / volumeBarRect.height)
)
);
},
[onChangeVolume]
);

// Switch order between "list" and "random"
const handleOrderButtonClick = useCallback(() => {
const nextOrder: PlaylistOrder = (
Expand Down Expand Up @@ -116,34 +99,13 @@ export function PlaybackControls({
<span className="aplayer-icon aplayer-icon-back"></span>
<span className="aplayer-icon aplayer-icon-play"></span>
<span className="aplayer-icon aplayer-icon-forward"></span>
<div className="aplayer-volume-wrap">
<button
className="aplayer-icon aplayer-icon-volume-down"
onClick={() => onToggleMuted()}
>
{muted ? (
<IconVolumeOff />
) : volume >= 1 ? (
<IconVolumeUp />
) : (
<IconVolumeDown />
)}
</button>
<div
className="aplayer-volume-bar-wrap"
onMouseDown={handleVolumeBarMouseDown}
>
<div className="aplayer-volume-bar">
<div
className="aplayer-volume"
style={{
backgroundColor: themeColor,
height: muted ? 0 : `${volume * 100}%`,
}}
></div>
</div>
</div>
</div>
<Volume
themeColor={themeColor}
volume={volume}
muted={muted}
onToggleMuted={onToggleMuted}
onChangeVolume={onChangeVolume}
/>
<button
className="aplayer-icon aplayer-icon-order"
onClick={handleOrderButtonClick}
Expand Down
82 changes: 82 additions & 0 deletions src/components/volume.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useCallback, useRef, useState } from "react";
import { ReactComponent as IconVolumeUp } from "../assets/volume-up.svg";
import { ReactComponent as IconVolumeDown } from "../assets/volume-down.svg";
import { ReactComponent as IconVolumeOff } from "../assets/volume-off.svg";
import { computePercentageOfY } from "../utils/computePercentage";
import clsx from "clsx";

type VolumeProps = {
themeColor: string;
volume: number;
muted: boolean;
onToggleMuted: () => void;
onChangeVolume: (volume: number) => void;
};

export function Volume({
themeColor,
volume,
muted,
onToggleMuted,
onChangeVolume,
}: VolumeProps) {
const volumeBarRef = useRef<HTMLDivElement>(null);
const [isDragging, setDragging] = useState(false); // ensure related element in :hover

const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
onChangeVolume(computePercentageOfY(e, volumeBarRef));
setDragging(true);

const handleMouseMove = (e: MouseEvent) => {
onChangeVolume(computePercentageOfY(e, volumeBarRef));
};

const handleMouseUp = (e: MouseEvent) => {
document.removeEventListener("mouseup", handleMouseUp);
document.removeEventListener("mousemove", handleMouseMove);

setDragging(false);
onChangeVolume(computePercentageOfY(e, volumeBarRef));
};

document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[onChangeVolume]
);

return (
<div className="aplayer-volume-wrap">
<button
className="aplayer-icon aplayer-icon-volume-down"
onClick={() => onToggleMuted()}
>
{muted || !volume ? (
<IconVolumeOff />
) : volume >= 1 ? (
<IconVolumeUp />
) : (
<IconVolumeDown />
)}
</button>
<div
className={clsx("aplayer-volume-bar-wrap", {
"aplayer-volume-bar-wrap-active": isDragging,
})}
ref={volumeBarRef}
onMouseDown={handleMouseDown}
>
<div className="aplayer-volume-bar">
<div
className="aplayer-volume"
style={{
backgroundColor: themeColor,
height: muted ? 0 : `${volume * 100}%`,
}}
></div>
</div>
</div>
</div>
);
}
165 changes: 116 additions & 49 deletions src/utils/computePercentage.test.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,123 @@
import { expect, test } from "vitest";
import { computePercentage } from "./computePercentage";
import { computePercentage, computePercentageOfY } from "./computePercentage";
import { describe } from "vitest";

test("Return 0 if progressBarRef.current is undefined", () => {
expect(
computePercentage(new MouseEvent("mouseup", {}), { current: null })
).toBe(0);
});
describe("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("mousemove"), { current: null })
).toBe(0);
});

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

describe("Given an valid percentage,when the mouse moves on the X axis", () => {
/* MOCK DOM */
test("Return valid percentage,when input two valid Event Objet", () => {
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 "";
},
});

test("Return 0 if progressBarRef.current is undefined", () => {
expect(
computePercentage(new MouseEvent("mousedown"), {
current: null,
})
).toBe(0);
container.addEventListener("mousedown", function (e) {
const val = computePercentage(e, { current: container });
expect(val).toBe(0.2);
});

container.dispatchEvent(mouseEvent);
});
});
});

/* 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);
describe("computePercentageOfY", () => {
test("Return 0 if volumeBarRef.current is undefined", () => {
expect(
computePercentageOfY(new MouseEvent("mouseup"), {
current: null,
})
).toBe(0);
});

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

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

describe("Given an valid percentage,when the mouse moves on the Y axis", () => {
/* MOCK DOM */
test("Return valid percentage,when input two valid Event Objet", () => {
const container = document.createElement("div");
container.style.width = "10px";
container.style.height = "300px";
const mouseEvent = new MouseEvent("mousedown", {
clientX: 50,
clientY: 100,
});

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

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

container.dispatchEvent(mouseEvent);
});
});
});
15 changes: 15 additions & 0 deletions src/utils/computePercentage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,18 @@ export function computePercentage(
percentage = Math.floor(percentage * 100) / 100;
return percentage;
}

export function computePercentageOfY(
eventTarget: Pick<MouseEvent, "clientY">,
volumeBarRef: React.RefObject<HTMLDivElement>
) {
if (!volumeBarRef.current) return 0;
let percentage =
1 -
(eventTarget.clientY - volumeBarRef.current.getBoundingClientRect().top) /
volumeBarRef.current.clientHeight;
percentage = Math.max(percentage, 0);
percentage = Math.min(percentage, 1);
percentage = Math.floor(percentage * 100) / 100;
return percentage;
}