import { type Accessor, createEffect, createSignal, For, type JSX, Match, type Setter, Show, Switch } from "solid-js"; import { type AppOptions, AUDIO_BITRATES, FileDisplay, StreamingMode, VIDEO_BITRATES } from "../types"; import "./OptionsModal.css"; import "./Dialog.css"; import "toastify-js/src/toastify.css"; import { MediaContainer, VideoCodec, VideoEncodingSetting } from "music-server-shared/types"; import { audioCodecHasBitrate } from "music-server-shared/utils"; import { createStore } from "solid-js/store"; import { type Combination, checkContainerSupport, checkSupport, containerAudioCodecs, containerVideoCodecs, hasMediaSource, playableSettings, type Support, selectableVideoCodecs, } from "../capabilities"; import { handleUnauthorized, toast } from "../utils"; import { Modal } from "./Modal"; interface OptionsModalProps { showOptions: Accessor; setShowOptions: Setter; options: AppOptions; setOptions: (options: AppOptions) => void; } const STREAMING_MODE_HINTS: Record = { [StreamingMode.mse]: "Media Source Extensions: the transcode is streamed into a SourceBuffer. The only mode that can seek within a transcode, but limited to the containers and codecs that support MSE.", [StreamingMode.progressive]: "The transcode is streamed straight to the player as it is produced. Plays whatever the browser can decode, but cannot seek.", [StreamingMode.buffered]: "The server transcodes the whole file before sending anything, so the response is a normal sized download instead of a chunked one. Much slower to start, and only needed for browsers that refuse chunked media sources (Safari).", }; type Profile = "videoSettings" | "audioSettings"; function bitrateLabel(bitrate: number): string { return bitrate < 1_000_000 ? `${bitrate / 1000} kbps` : `${bitrate / 1_000_000} Mbps`; } function SupportedOption(props: { value: string; support: Support }) { return ( ); } //all three display settings offer the same choices function FileDisplaySelect(props: { id: string; label: string; value: FileDisplay; onPick: (value: FileDisplay) => void; }) { return (
); } //the parts of a transcoding profile that video and music have in common. the video-only controls are //passed as children, so they land between the container and the audio codec like they did per-page function ProfileSection(props: { title: string; idPrefix: string; settings: AppOptions["videoSettings"] | AppOptions["audioSettings"]; setField: (field: "streamingMode" | "container" | "audioCodec" | "audioBitrate", value: string | number) => void; children?: JSX.Element; }) { return ( <>
{props.title}
{props.children}
); } export default function OptionsModal(props: OptionsModalProps) { //clone, otherwise it shares the reference const [newOptions, setNewOptions] = createStore({ ...props.options }); const [optionsPage, setOptionsPage] = createSignal(1); let formElement!: HTMLFormElement; //the last two pages only exist while transcoding is on const pages = () => (newOptions.enableTranscoding ? [1, 2, 3, 4, 5] : [1, 2, 3]); //keep the selection playable, so the greyed-out entries in the lists are only ever ones the user could //pick and never the one they are already on. the store setter merges key by key and skips values it //already holds, so a settled selection writes nothing, notifies nobody and this re-run is the last function coerceProfile(profile: Profile) { setNewOptions(profile, playableSettings(newOptions[profile])); } createEffect(() => coerceProfile("videoSettings")); createEffect(() => coerceProfile("audioSettings")); function handleSave(close: () => void) { if (!formElement.reportValidity()) return; toast("Saved changes", "success"); props.setOptions(newOptions); close(); } async function deleteServerCache() { const response = await fetch("/reset-cache", { method: "POST", credentials: "same-origin" }).catch(() => {}); if (handleUnauthorized(response)) return; if (response?.status !== 200) { toast("Clearing server cache failed", "error"); } else { toast("Server cache cleared", "info"); } } return ( { setNewOptions(props.options); setOptionsPage(1); }} > {(close) => (
Player Options

{(page) => ( )}
setNewOptions("enableTranscoding", e.target.checked)} />
setNewOptions("languages", e.target.value)} />
setNewOptions("audioLanguagePreference", e.target.value)} disabled={!newOptions.enableTranscoding} />
setNewOptions("maxTranscodes", e.target.valueAsNumber)} />
setNewOptions("maxDownloads", e.target.valueAsNumber)} />
setNewOptions("hideVolume", e.target.checked)} />
setNewOptions("hideCover", e.target.checked)} />
setNewOptions("hidePlaylistCover", e.target.checked)} />
setNewOptions("filebrowserDisplay", value)} /> setNewOptions("playlistDisplay", value)} /> setNewOptions("controlsDisplay", value)} />
setNewOptions("progressBarHeight", e.target.valueAsNumber)} />
setNewOptions("rememberDir", e.target.checked)} />
setNewOptions("duplicateWarning", e.target.checked)} />
setNewOptions("transcodeCovers", e.target.checked)} />
setNewOptions("videoSettings", field as "container", value as MediaContainer) } > 0}>
setNewOptions("audioSettings", field as "container", value as MediaContainer) } />
)}
); }