import "./PlayerControls.css"; import { type Accessor, createEffect, createSignal, For, onMount, type Setter, Show } from "solid-js"; import type { SetStoreFunction } from "solid-js/store"; import type { AppOptions, PlayerState } from "../types"; import { formatFilename, formatTime, LocalStorageValues } from "../utils"; import { LoadingSpinner } from "./LoadingScreen"; import { ClearSVG, DownSVG, FullscreenSVG, InfoSVG, NextTrackSVG, PauseSVG, PlaySVG, PreviousTrackSVG, RepeatSVG, ShuffleSVG, UpSVG, VolumeSVG, } from "./SVG"; interface PlayerControlsProps { playerState: PlayerState; setPlayerState: SetStoreFunction; togglePlay: () => void; playPrevious: () => void; playNext: () => void; isShuffled: Accessor; setShuffled: Setter; isRepeated: Accessor; setRepeated: Setter; needMSESeek: (seekTo: number) => boolean; seekTo: (seekTo: number) => void; options: AppOptions; videoElement: HTMLVideoElement; } export default function PlayerControls(props: PlayerControlsProps) { const [controlsDisabled, setControlsDisabled] = createSignal(false); createEffect(() => { setControlsDisabled(!props.playerState.currentTrack); }); function setVolume(e: Event) { const input = e.target as HTMLInputElement; const volume = Number.parseFloat(input.value); props.setPlayerState("volume", volume); if (props.videoElement) { props.videoElement.volume = volume; } } function progressClick(e: MouseEvent) { const progressBar = e.currentTarget as HTMLDivElement; const rect = progressBar.getBoundingClientRect(); seekTo((e.clientX - rect.left) / rect.width); } // Seek to position function seekTo(posPercent: number) { const playerState = props.playerState; if (!props.videoElement || !playerState.currentTrack) return; const duration = playerState.currentTrack.metadata.duration; const seekTime = Math.max(0, posPercent * duration); if (props.needMSESeek(seekTime)) { props.seekTo(seekTime); } else { props.setPlayerState("waitingForStart", true); props.videoElement.currentTime = seekTime; } } const [isDragging, setIsDragging] = createSignal(false); const [dragPos, setDragPos] = createSignal(0); createEffect(() => { if (isDragging()) return; setDragPos( props.playerState.currentTrack ? Math.min( 100, (props.playerState.currentTime / (props.playerState.currentTrack?.metadata.duration ?? Number.POSITIVE_INFINITY)) * 100, ) : 0, ); }); let progressContainerRef!: HTMLDivElement; let handleRef!: HTMLDivElement; //pointer instead drag events needed, because for some reason e.clientX is always 0 on firefox during the drag function handlePointerDown(e: PointerEvent) { if (e.button !== 0) return; e.preventDefault(); handleRef.setPointerCapture(e.pointerId); //needed for mobile drag to work setIsDragging(true); handlePointerMove(e); window.addEventListener("pointermove", handlePointerMove); window.addEventListener("pointerup", handlePointerUp); window.addEventListener("pointercancel", handlePointerUp); //just for safety } function handlePointerMove(e: PointerEvent) { if (!isDragging() || !props.playerState.currentTrack) return; e.preventDefault(); const rect = progressContainerRef.getBoundingClientRect(); const pos = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width)); setDragPos(pos * 100); } function handlePointerUp(e: PointerEvent) { if (!isDragging() || !props.playerState.currentTrack) return; setIsDragging(false); if (e.type !== "pointercancel") { const rect = progressContainerRef.getBoundingClientRect(); seekTo(Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width))); } window.removeEventListener("pointermove", handlePointerMove); window.removeEventListener("pointerup", handlePointerUp); window.removeEventListener("pointercancel", handlePointerUp); handleRef.releasePointerCapture(e.pointerId); } const [showPlayerControls, setShowPlayerControls] = createSignal(true); onMount(() => { const savePlayerControls = localStorage.getItem(LocalStorageValues.playerControls); if (savePlayerControls !== null) setShowPlayerControls(savePlayerControls === "true"); createEffect(() => localStorage.setItem(LocalStorageValues.playerControls, showPlayerControls().toString())); }); let metadataDialogRef!: HTMLDialogElement; function handleClose() { metadataDialogRef.close(); } function MetadataEntry(label: string, value: unknown, suffix?: string) { if (!value) return undefined; return (
{label}: {value.toString()} {suffix}
); } function bitrateDisplay(bitrate: number | undefined, divisor: number) { if (!bitrate) return undefined; return bitrate / divisor; } return ( <> e.target.nodeName === "DIALOG" && handleClose()}>
Metadata

{props.playerState.currentTrack ? formatFilename(props.playerState.currentTrack, props.options.controlsDisplay) : "No track selected"} {props.playerState.waitingForStart ? (
) : ( "" )}
{(bufferRange) => { return (
); }}
{formatTime(props.playerState.currentTime)}/ {formatTime(props.playerState.currentTrack?.metadata.duration || 0)}
); }