OptionsModal.tsx
⎇
Raw
1import { type Accessor, createEffect, type Setter, on, createSignal, Switch, Match } 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 { MediaFormat } from "music-server-shared/types";
8import { createStore } from "solid-js/store";
9
10interface OptionsModalProps {
11 showOptions: Accessor<boolean>;
12 setShowOptions: Setter<boolean>;
13 options: AppOptions;
14 setOptions: (options: AppOptions) => void;
15}
16
17export default function OptionsModal(props: OptionsModalProps) {
18 //clone, otherwise it shares the reference
19 const [newOptions, setNewOptions] = createStore<AppOptions>({ ...props.options });
20 const [optionsPage, setOptionsPage] = createSignal(1);
21 let dialogElement!: HTMLDialogElement;
22 let formElement!: HTMLFormElement;
23
24 createEffect(
25 on(props.showOptions, () => {
26 if (props.showOptions()) {
27 dialogElement.showModal();
28 setNewOptions(props.options);
29 setOptionsPage(1);
30 }
31 }),
32 );
33
34 function handleSave(e: Event) {
35 e.preventDefault();
36 if (!formElement.reportValidity()) return;
37 toast("Saved changes", "success");
38 props.setOptions(newOptions);
39 handleClose();
40 }
41
42 function handleClose() {
43 dialogElement.close();
44 props.setShowOptions(false);
45 }
46
47 async function deleteServerCache() {
48 const response = await fetch("/reset-cache", { method: "POST", credentials: "same-origin" }).catch(() => {});
49 if (!response || response.status !== 200) {
50 toast("Clearing server cache failed", "error");
51 } else {
52 toast("Server cache cleared", "info");
53 }
54 }
55
56 return (
57 <dialog ref={dialogElement} onClose={handleClose} onclick={(e) => e.target.nodeName === "DIALOG" && handleClose()}>
58 <form class="option-content" ref={formElement}>
59 <div class="title">Player Options</div>
60 <hr />
61 <div class="options-page-buttons">
62 <button
63 onClick={(e) => {
64 e.preventDefault();
65 return setOptionsPage(1);
66 }}
67 classList={{ "page-button-active": optionsPage() === 1 }}
68 >
69 1
70 </button>
71 <button
72 onClick={(e) => {
73 e.preventDefault();
74 return setOptionsPage(2);
75 }}
76 classList={{ "page-button-active": optionsPage() === 2 }}
77 >
78 2
79 </button>
80 <button
81 onClick={(e) => {
82 e.preventDefault();
83 return setOptionsPage(3);
84 }}
85 classList={{ "page-button-active": optionsPage() === 3 }}
86 >
87 3
88 </button>
89 </div>
90 <Switch>
91 <Match when={optionsPage() === 1}>
92 <div>
93 <label for="transcoding-checkbox">Enable transcoding:</label>
94 <input
95 id="transcoding-checkbox"
96 type="checkbox"
97 checked={newOptions.enableTranscoding}
98 onChange={(e) => setNewOptions("enableTranscoding", e.target.checked)}
99 />
100 </div>
101 <div>
102 <label for="transcoding-buffer-checkbox">Disable chunked transcoding:</label>
103 <input
104 id="transcoding-buffer-checkbox"
105 type="checkbox"
106 checked={newOptions.disableChunkedTranscoding}
107 onChange={(e) => setNewOptions("disableChunkedTranscoding", e.target.checked)}
108 disabled={!newOptions.enableTranscoding}
109 />
110 </div>
111 <div>
112 <label for="bitrate-select">Bitrate:</label>
113 <select
114 id="bitrate-select"
115 value={newOptions.bitrate}
116 onChange={(e) => setNewOptions("bitrate", Number.parseInt(e.target.value))}
117 disabled={!newOptions.enableTranscoding}
118 >
119 <option value="32000">32 kbps</option>
120 <option value="48000">48 kbps</option>
121 <option value="64000">64 kbps</option>
122 <option value="96000">96 kbps</option>
123 <option value="128000">128 kbps</option>
124 <option value="192000">192 kbps</option>
125 <option value="320000">320 kbps</option>
126 </select>
127 </div>
128 <div>
129 <label for="codec-select">Format:</label>
130 <select
131 id="codec-select"
132 value={newOptions.format}
133 onChange={(e) => setNewOptions("format", e.target.value as MediaFormat)}
134 disabled={!newOptions.enableTranscoding}
135 >
136 <option>{MediaFormat["opus/ogg"]}</option>
137 <option>{MediaFormat["opus/webm"]}</option>
138 <option>{MediaFormat.mp3}</option>
139 <option>{MediaFormat["vorbis/ogg"]}</option>
140 <option>{MediaFormat["vorbis/webm"]}</option>
141 </select>
142 </div>
143 <div>
144 <label for="max-transcodes-input">Max concurrent transcodes:</label>
145 <input
146 id="max-transcodes-input"
147 type="number"
148 min={1}
149 required
150 value={newOptions.maxTranscodes}
151 disabled={!newOptions.enableTranscoding}
152 onChange={(e) => setNewOptions("maxTranscodes", e.target.valueAsNumber)}
153 />
154 </div>
155 <div>
156 <label for="max-downloads-input">Max concurrent downloads:</label>
157 <input
158 id="max-downloads-input"
159 type="number"
160 min={1}
161 required
162 value={newOptions.maxDownloads}
163 onChange={(e) => setNewOptions("maxDownloads", e.target.valueAsNumber)}
164 />
165 </div>
166 <div>
167 <label for="transcode-covers-checkbox">Transcode covers to avif:</label>
168 <input
169 id="transcode-covers-checkbox"
170 type="checkbox"
171 checked={newOptions.transcodeCovers}
172 onChange={(e) => setNewOptions("transcodeCovers", e.target.checked)}
173 />
174 </div>
175 </Match>
176 <Match when={optionsPage() === 2}>
177 <div>
178 <label for="hide-volume-checkbox">Hide volume:</label>
179 <input
180 id="hide-volume-checkbox"
181 type="checkbox"
182 checked={newOptions.hideVolume}
183 onChange={(e) => setNewOptions("hideVolume", e.target.checked)}
184 />
185 </div>
186 <div>
187 <label for="hide-cover-checkbox">Hide file browser cover:</label>
188 <input
189 id="hide-cover-checkbox"
190 type="checkbox"
191 checked={newOptions.hideCover}
192 onChange={(e) => setNewOptions("hideCover", e.target.checked)}
193 />
194 </div>
195 <div>
196 <label for="hide-playlist-cover-checkbox">Hide current track cover:</label>
197 <input
198 id="hide-playlist-cover-checkbox"
199 type="checkbox"
200 checked={newOptions.hidePlaylistCover}
201 onChange={(e) => setNewOptions("hidePlaylistCover", e.target.checked)}
202 />
203 </div>
204 <div>
205 <label for="filebrowser-display-select">Filebrowser:</label>
206 <select
207 id="filebrowser-display-select"
208 value={newOptions.filebrowserDisplay}
209 onChange={(e) => setNewOptions("filebrowserDisplay", Number.parseInt(e.target.value))}
210 >
211 <option value={FileDisplay.FILENAME}>Filename</option>
212 <option value={FileDisplay.TITLE}>Media title</option>
213 <option value={FileDisplay.TRACKNO_TITLE}>Track number + Media title</option>
214 </select>
215 </div>
216 <div>
217 <label for="playlist-display-select">Playlist:</label>
218 <select
219 id="playlist-display-select"
220 value={newOptions.playlistDisplay}
221 onChange={(e) => setNewOptions("playlistDisplay", Number.parseInt(e.target.value))}
222 >
223 <option value={FileDisplay.FILENAME}>Filename</option>
224 <option value={FileDisplay.TITLE}>Media title</option>
225 <option value={FileDisplay.TRACKNO_TITLE}>Track number + Media title</option>
226 </select>
227 </div>
228 <div>
229 <label for="controls-display-select">Current track:</label>
230 <select
231 id="controls-display-select"
232 value={newOptions.controlsDisplay}
233 onChange={(e) => setNewOptions("controlsDisplay", Number.parseInt(e.target.value))}
234 >
235 <option value={FileDisplay.FILENAME}>Filename</option>
236 <option value={FileDisplay.TITLE}>Media title</option>
237 <option value={FileDisplay.TRACKNO_TITLE}>Track number + Media title</option>
238 </select>
239 </div>
240
241 <div>
242 <label for="font-resolution-select">Font resolution:</label>
243 <select
244 id="font-resolution-select"
245 value={newOptions.fontResolution}
246 onChange={(e) => setNewOptions("fontResolution", Number.parseInt(e.target.value))}
247 >
248 <option value={10}>10px</option>
249 <option value={12}>12px</option>
250 <option value={16}>16px</option>
251 </select>
252 </div>
253 <div>
254 <label for="progress-height-input">Progress bar height:</label>
255 <input
256 id="progress-height-input"
257 type="number"
258 min={0}
259 required
260 value={newOptions.progressBarHeight}
261 onChange={(e) => setNewOptions("progressBarHeight", e.target.valueAsNumber)}
262 />
263 </div>
264 </Match>
265 <Match when={optionsPage() === 3}>
266 <div>
267 <label for="mse-checkbox">Allow Media Source Extensions (MSE):</label>
268 <input
269 id="mse-checkbox"
270 type="checkbox"
271 checked={newOptions.useMSE}
272 onChange={(e) => setNewOptions("useMSE", e.target.checked)}
273 />
274 </div>
275 <div>
276 <label for="remember-dir-checkbox">Remember last directory:</label>
277 <input
278 id="remember-dir-checkbox"
279 type="checkbox"
280 checked={newOptions.rememberDir}
281 onChange={(e) => setNewOptions("rememberDir", e.target.checked)}
282 />
283 </div>
284 <div>
285 <label for="duplicate-checkbox">Warn when adding duplicate to playlist:</label>
286 <input
287 id="duplicate-checkbox"
288 type="checkbox"
289 checked={newOptions.duplicateWarning}
290 onChange={(e) => setNewOptions("duplicateWarning", e.target.checked)}
291 />
292 </div>
293 </Match>
294 </Switch>
295 <div class="option-buttons">
296 <button title="Clear server caches" type="button" class="cache-button" onclick={deleteServerCache}>
297 Clear server caches
298 </button>
299 <button title="Save settings" type="button" onClick={handleSave}>
300 Save
301 </button>
302 <button title="Cancel changes settings" type="button" class="cancel-button" onClick={handleClose}>
303 Cancel
304 </button>
305 </div>
306 </form>
307 </dialog>
308 );
309}
310