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