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