OptionsModal.tsx
⎇
Raw
1import { type Accessor, createEffect, type Setter, on, createSignal, Switch, Match, For } from "solid-js";
2import { FileDisplay, type AppOptions } from "../types";
3import "./OptionsModal.css";
4import "./Dialog.css";
5import "toastify-js/src/toastify.css";
6import { toast } from "../utils";
7import { createStore } from "solid-js/store";
8import { MediaContainer, VideoCodec, AudioCodec, VideoEncodingSetting } from "music-server-shared/types";
9
10interface OptionsModalProps {
11 showOptions: Accessor<boolean>;
12 setShowOptions: Setter<boolean>;
13 options: AppOptions;
14 setOptions: (options: AppOptions) => void;
15}
16
17function supportedVideoCodecs(container: MediaContainer) {
18 let codecs: VideoCodec[] = [];
19 switch (container) {
20 case MediaContainer.mp3:
21 break;
22 case MediaContainer.webm:
23 codecs = [VideoCodec.av1, VideoCodec.vp8, VideoCodec.vp9];
24 break;
25 case MediaContainer.ogg:
26 codecs = [VideoCodec.theora];
27 break;
28 default: {
29 throw new Error(`Unsupported container: ${container}`);
30 }
31 }
32 return codecs.concat([VideoCodec.none]);
33}
34
35function supportedAudioCodecs(container: MediaContainer) {
36 let codecs: AudioCodec[] = [];
37 switch (container) {
38 case MediaContainer.mp3:
39 codecs = [AudioCodec.mp3];
40 break;
41 case MediaContainer.webm:
42 codecs = [AudioCodec.opus, AudioCodec.vorbis];
43 break;
44 case MediaContainer.ogg:
45 codecs = [AudioCodec.opus, AudioCodec.vorbis];
46 break;
47 default: {
48 throw new Error(`Unsupported container: ${container}`);
49 }
50 }
51 return codecs.concat([AudioCodec.none]);
52}
53
54export default function OptionsModal(props: OptionsModalProps) {
55 //clone, otherwise it shares the reference
56 const [newOptions, setNewOptions] = createStore<AppOptions>({ ...props.options });
57 const [optionsPage, setOptionsPage] = createSignal(1);
58 let dialogElement!: HTMLDialogElement;
59 let formElement!: HTMLFormElement;
60
61 createEffect(
62 on(props.showOptions, () => {
63 if (props.showOptions()) {
64 dialogElement.showModal();
65 setNewOptions(props.options);
66 setOptionsPage(1);
67 }
68 }),
69 );
70
71 function handleSave(e: Event) {
72 e.preventDefault();
73 if (!formElement.reportValidity()) return;
74 toast("Saved changes", "success");
75 props.setOptions(newOptions);
76 handleClose();
77 }
78
79 function handleClose() {
80 dialogElement.close();
81 props.setShowOptions(false);
82 }
83
84 async function deleteServerCache() {
85 const response = await fetch("/reset-cache", { method: "POST", credentials: "same-origin" }).catch(() => {});
86 if (!response || response.status !== 200) {
87 toast("Clearing server cache failed", "error");
88 } else {
89 toast("Server cache cleared", "info");
90 }
91 }
92
93 return (
94 <dialog ref={dialogElement} onClose={handleClose} onclick={(e) => e.target.nodeName === "DIALOG" && handleClose()}>
95 <form class="option-content" ref={formElement}>
96 <div class="title">Player Options</div>
97 <hr />
98 <div class="options-page-buttons">
99 <button
100 onClick={(e) => {
101 e.preventDefault();
102 return setOptionsPage(1);
103 }}
104 classList={{ "page-button-active": optionsPage() === 1 }}
105 >
106 1
107 </button>
108 <button
109 onClick={(e) => {
110 e.preventDefault();
111 return setOptionsPage(2);
112 }}
113 classList={{ "page-button-active": optionsPage() === 2 }}
114 >
115 2
116 </button>
117 <button
118 onClick={(e) => {
119 e.preventDefault();
120 return setOptionsPage(3);
121 }}
122 classList={{ "page-button-active": optionsPage() === 3 }}
123 >
124 3
125 </button>
126 </div>
127 <Switch>
128 <Match when={optionsPage() === 1}>
129 <div>
130 <label for="transcoding-checkbox">Enable transcoding:</label>
131 <input
132 id="transcoding-checkbox"
133 type="checkbox"
134 checked={newOptions.enableTranscoding}
135 onChange={(e) => setNewOptions("enableTranscoding", e.target.checked)}
136 />
137 </div>
138 <div>
139 <label for="transcoding-buffer-checkbox">Disable chunked transcoding:</label>
140 <input
141 id="transcoding-buffer-checkbox"
142 type="checkbox"
143 checked={newOptions.disableChunkedTranscoding}
144 onChange={(e) => setNewOptions("disableChunkedTranscoding", e.target.checked)}
145 disabled={!newOptions.enableTranscoding}
146 />
147 </div>
148 <div>
149 <label for="audio-bitrate-select">Audio bitrate:</label>
150 <select
151 id="audio-bitrate-select"
152 value={newOptions.audioBitrate}
153 onChange={(e) => setNewOptions("audioBitrate", Number.parseInt(e.target.value))}
154 disabled={!newOptions.enableTranscoding}
155 >
156 <option value="32000">32 kbps</option>
157 <option value="48000">48 kbps</option>
158 <option value="64000">64 kbps</option>
159 <option value="96000">96 kbps</option>
160 <option value="128000">128 kbps</option>
161 <option value="192000">192 kbps</option>
162 <option value="320000">320 kbps</option>
163 </select>
164 </div>
165 <div>
166 <label for="video-bitrate-select">Video bitrate:</label>
167 <select
168 id="video-bitrate-select"
169 value={newOptions.videoBitrate}
170 onChange={(e) => setNewOptions("videoBitrate", Number.parseInt(e.target.value))}
171 disabled={!newOptions.enableTranscoding}
172 >
173 <option value="0">0 kbps</option>
174 </select>
175 </div>
176 <div>
177 <label for="container-select">Container:</label>
178 <select
179 id="container-select"
180 value={newOptions.container}
181 onChange={(e) => setNewOptions("container", e.target.value as MediaContainer)}
182 disabled={!newOptions.enableTranscoding}
183 >
184 <For each={Object.values(MediaContainer)}>
185 {(format) => {
186 return <option value={format}>{format}</option>;
187 }}
188 </For>
189 </select>
190 </div>
191 <div>
192 <label for="audio-codec-select">Audio codec:</label>
193 <select
194 id="audio-codec-select"
195 value={newOptions.audioCodec}
196 onChange={(e) => setNewOptions("audioCodec", e.target.value as AudioCodec)}
197 disabled={!newOptions.enableTranscoding}
198 >
199 <For each={supportedAudioCodecs(newOptions.container)}>
200 {(codec) => {
201 return <option value={codec}>{codec}</option>;
202 }}
203 </For>
204 </select>
205 </div>
206 <div>
207 <label for="video-codec-select">Audio codec:</label>
208 <select
209 id="video-codec-select"
210 value={newOptions.videoCodec}
211 onChange={(e) => setNewOptions("videoCodec", e.target.value as VideoCodec)}
212 disabled={!newOptions.enableTranscoding}
213 >
214 <For each={supportedVideoCodecs(newOptions.container)}>
215 {(codec) => {
216 return <option value={codec}>{codec}</option>;
217 }}
218 </For>
219 </select>
220 </div>
221 <div>
222 <label for="video-settings-select">Video encoding settings:</label>
223 <select
224 id="video-settings-select"
225 value={newOptions.videoEncodingSettings}
226 onChange={(e) => setNewOptions("videoEncodingSettings", e.target.value as VideoEncodingSetting)}
227 disabled={!newOptions.enableTranscoding}
228 >
229 <For each={Object.values(VideoEncodingSetting)}>
230 {(setting) => {
231 return <option value={setting}>{setting}</option>;
232 }}
233 </For>
234 </select>
235 </div>
236 <div>
237 <label for="max-transcodes-input">Max concurrent transcodes:</label>
238 <input
239 id="max-transcodes-input"
240 type="number"
241 min={1}
242 required
243 value={newOptions.maxTranscodes}
244 disabled={!newOptions.enableTranscoding}
245 onChange={(e) => setNewOptions("maxTranscodes", e.target.valueAsNumber)}
246 />
247 </div>
248 <div>
249 <label for="max-downloads-input">Max concurrent downloads:</label>
250 <input
251 id="max-downloads-input"
252 type="number"
253 min={1}
254 required
255 value={newOptions.maxDownloads}
256 onChange={(e) => setNewOptions("maxDownloads", e.target.valueAsNumber)}
257 />
258 </div>
259 </Match>
260 <Match when={optionsPage() === 2}>
261 <div>
262 <label for="hide-volume-checkbox">Hide volume:</label>
263 <input
264 id="hide-volume-checkbox"
265 type="checkbox"
266 checked={newOptions.hideVolume}
267 onChange={(e) => setNewOptions("hideVolume", e.target.checked)}
268 />
269 </div>
270 <div>
271 <label for="hide-cover-checkbox">Hide file browser cover:</label>
272 <input
273 id="hide-cover-checkbox"
274 type="checkbox"
275 checked={newOptions.hideCover}
276 onChange={(e) => setNewOptions("hideCover", e.target.checked)}
277 />
278 </div>
279 <div>
280 <label for="hide-playlist-cover-checkbox">Hide current track cover:</label>
281 <input
282 id="hide-playlist-cover-checkbox"
283 type="checkbox"
284 checked={newOptions.hidePlaylistCover}
285 onChange={(e) => setNewOptions("hidePlaylistCover", e.target.checked)}
286 />
287 </div>
288 <div>
289 <label for="filebrowser-display-select">Filebrowser:</label>
290 <select
291 id="filebrowser-display-select"
292 value={newOptions.filebrowserDisplay}
293 onChange={(e) => setNewOptions("filebrowserDisplay", Number.parseInt(e.target.value))}
294 >
295 <option value={FileDisplay.FILENAME}>Filename</option>
296 <option value={FileDisplay.TITLE}>Media title</option>
297 <option value={FileDisplay.TRACKNO_TITLE}>Track number + Media title</option>
298 </select>
299 </div>
300 <div>
301 <label for="playlist-display-select">Playlist:</label>
302 <select
303 id="playlist-display-select"
304 value={newOptions.playlistDisplay}
305 onChange={(e) => setNewOptions("playlistDisplay", Number.parseInt(e.target.value))}
306 >
307 <option value={FileDisplay.FILENAME}>Filename</option>
308 <option value={FileDisplay.TITLE}>Media title</option>
309 <option value={FileDisplay.TRACKNO_TITLE}>Track number + Media title</option>
310 </select>
311 </div>
312 <div>
313 <label for="controls-display-select">Current track:</label>
314 <select
315 id="controls-display-select"
316 value={newOptions.controlsDisplay}
317 onChange={(e) => setNewOptions("controlsDisplay", Number.parseInt(e.target.value))}
318 >
319 <option value={FileDisplay.FILENAME}>Filename</option>
320 <option value={FileDisplay.TITLE}>Media title</option>
321 <option value={FileDisplay.TRACKNO_TITLE}>Track number + Media title</option>
322 </select>
323 </div>
324
325 <div>
326 <label for="font-resolution-select">Font resolution:</label>
327 <select
328 id="font-resolution-select"
329 value={newOptions.fontResolution}
330 onChange={(e) => setNewOptions("fontResolution", Number.parseInt(e.target.value))}
331 >
332 <option value={10}>10px</option>
333 <option value={12}>12px</option>
334 <option value={16}>16px</option>
335 </select>
336 </div>
337 <div>
338 <label for="progress-height-input">Progress bar height:</label>
339 <input
340 id="progress-height-input"
341 type="number"
342 min={0}
343 required
344 value={newOptions.progressBarHeight}
345 onChange={(e) => setNewOptions("progressBarHeight", e.target.valueAsNumber)}
346 />
347 </div>
348 </Match>
349 <Match when={optionsPage() === 3}>
350 <div>
351 <label for="mse-checkbox">Allow Media Source Extensions (MSE):</label>
352 <input
353 id="mse-checkbox"
354 type="checkbox"
355 checked={newOptions.useMSE}
356 onChange={(e) => setNewOptions("useMSE", e.target.checked)}
357 />
358 </div>
359 <div>
360 <label for="remember-dir-checkbox">Remember last directory:</label>
361 <input
362 id="remember-dir-checkbox"
363 type="checkbox"
364 checked={newOptions.rememberDir}
365 onChange={(e) => setNewOptions("rememberDir", e.target.checked)}
366 />
367 </div>
368 <div>
369 <label for="duplicate-checkbox">Warn when adding duplicate to playlist:</label>
370 <input
371 id="duplicate-checkbox"
372 type="checkbox"
373 checked={newOptions.duplicateWarning}
374 onChange={(e) => setNewOptions("duplicateWarning", e.target.checked)}
375 />
376 </div>
377 <div>
378 <label for="transcode-covers-checkbox">Transcode covers to avif:</label>
379 <input
380 id="transcode-covers-checkbox"
381 type="checkbox"
382 checked={newOptions.transcodeCovers}
383 onChange={(e) => setNewOptions("transcodeCovers", e.target.checked)}
384 />
385 </div>
386 </Match>
387 </Switch>
388 <div class="option-buttons">
389 <button title="Clear server caches" type="button" class="cache-button" onclick={deleteServerCache}>
390 Clear server caches
391 </button>
392 <button title="Save settings" type="button" onClick={handleSave}>
393 Save
394 </button>
395 <button title="Cancel changes settings" type="button" class="cancel-button" onClick={handleClose}>
396 Cancel
397 </button>
398 </div>
399 </form>
400 </dialog>
401 );
402}
403