PlayerControls.tsx
⎇
Raw
1import "./PlayerControls.css";
2import { type AppOptions, getItemName, type PlayerState } from "../types";
3import { type Accessor, createEffect, createSignal, type Setter } from "solid-js";
4import { NextTrackSVG, PauseSVG, PlaySVG, PreviousTrackSVG, RepeatSVG, ShuffleSVG, VolumeSVG } from "./SVG";
5import { formatTime } from "../utils";
6import type { SetStoreFunction } from "solid-js/store";
7
8interface PlayerControlsProps {
9 playerState: PlayerState;
10 setPlayerState: SetStoreFunction<PlayerState>;
11 togglePlay: () => void;
12 playPrevious: () => void;
13 playNext: () => void;
14 isShuffled: Accessor<boolean>;
15 setShuffled: Setter<boolean>;
16 isRepeated: Accessor<boolean>;
17 setRepeated: Setter<boolean>;
18 options: AppOptions;
19 audioElement: HTMLAudioElement;
20}
21
22export default function PlayerControls(props: PlayerControlsProps) {
23 const [controlsDisabled, setControlsDisabled] = createSignal(false);
24 createEffect(() => {
25 setControlsDisabled(!props.playerState.currentTrack);
26 });
27
28 function setVolume(e: Event) {
29 const input = e.target as HTMLInputElement;
30 const volume = Number.parseFloat(input.value);
31 props.setPlayerState("volume", volume);
32
33 if (props.audioElement) {
34 props.audioElement.volume = volume;
35 }
36 }
37
38 // Seek to position
39 function seekTo(e: MouseEvent) {
40 const playerState = props.playerState;
41 if (!props.audioElement || !playerState.currentTrack) return;
42
43 const progressBar = e.currentTarget as HTMLDivElement;
44 const rect = progressBar.getBoundingClientRect();
45 const pos = (e.clientX - rect.left) / rect.width;
46
47 const duration = playerState.currentTrack.metadata.duration;
48 props.audioElement.currentTime = Math.min(pos * duration, playerState.buffered - 1);
49 props.setPlayerState("currentTime", pos * duration)
50 }
51
52 return (
53 <div class="player-controls">
54 <div class="track-info">{getItemName(props.playerState.currentTrack) || "No track selected"}</div>
55 <div class="progress-container">
56 <div class="progress-bar" onClick={seekTo} style={controlsDisabled() ? { cursor: "default" } : {}}>
57 <div
58 class="buffered-fill"
59 style={{
60 width: `${
61 props.playerState.currentTrack
62 ? Math.min(
63 100,
64 (props.playerState.buffered /
65 (props.playerState.currentTrack?.metadata.duration ?? Number.POSITIVE_INFINITY)) *
66 100,
67 )
68 : 0
69 }%`,
70 }}
71 />
72 <div
73 class="progress-fill"
74 style={{
75 width: `${
76 props.playerState.currentTrack
77 ? Math.min(
78 100,
79 (props.playerState.currentTime /
80 (props.playerState.currentTrack?.metadata.duration ?? Number.POSITIVE_INFINITY)) *
81 100,
82 )
83 : 0
84 }%`,
85 }}
86 />
87 </div>
88 </div>
89 <div class="time-container">
90 <span>{formatTime(props.playerState.currentTime)}</span>/
91 <span>{formatTime(props.playerState.currentTrack?.metadata.duration || 0)}</span>
92 </div>
93
94 <div class="control-buttons">
95 <button title="Previous track" disabled={controlsDisabled()} onClick={props.playPrevious}>
96 <PreviousTrackSVG disabled={controlsDisabled} />
97 </button>
98 <button title="Toggle play/pause" disabled={controlsDisabled()} onClick={props.togglePlay}>
99 {props.playerState.isPlaying ? (
100 <PauseSVG disabled={controlsDisabled} />
101 ) : (
102 <PlaySVG disabled={controlsDisabled} />
103 )}
104 </button>
105 <button title="Next track" disabled={controlsDisabled()} onClick={props.playNext}>
106 <NextTrackSVG disabled={controlsDisabled} />
107 </button>
108
109 <button title="Repeat Playlist" onClick={() => props.setRepeated(!props.isRepeated())}>
110 <RepeatSVG disabled={() => !props.isRepeated()} />
111 </button>
112
113 <button title="Shuffle" onClick={() => props.setShuffled(!props.isShuffled())}>
114 <ShuffleSVG disabled={() => !props.isShuffled()} />
115 </button>
116
117 <div class="volume-control" style={props.options.hideVolume ? { display: "none" } : {}}>
118 <VolumeSVG />
119 <input type="range" min="0" max="1" step="0.01" value={props.playerState.volume} onInput={setVolume} />
120 </div>
121 <div>
122 <select
123 value={props.playerState.rate}
124 onchange={(e) => props.setPlayerState("rate", Number.parseFloat(e.target.value))}
125 >
126 <option value={0.25}>0.25x</option>
127 <option value={0.5}>0.5x</option>
128 <option value={0.75}>0.75x</option>
129 <option value={1}>1x</option>
130 <option value={1.25}>1.25x</option>
131 <option value={1.5}>1.5x</option>
132 <option value={1.75}>1.75x</option>
133 <option value={2}>2x</option>
134 <option value={2.5}>2.5x</option>
135 <option value={3}>3x</option>
136 </select>
137 </div>
138 </div>
139 </div>
140 );
141}
142