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