forked from
tokono.ma/diffuse
A music player that connects to your cloud/distributed storage.
1import { signal } from "~/common/signal.js";
2
3/**
4 * @import { DiffuseElement } from "~/common/element.js";
5 * @import { Signal } from "~/common/signal.d.ts";
6 * @import { ScrobbleElement } from "~/components/supplement/types.d.ts";
7 */
8
9const url = new URL(document.location.href);
10export const GROUP = url.searchParams.get("group") ?? "facets";
11
12/**
13 * [PRIVATE] Signals.
14 */
15const signals = {
16 configurator: {
17 artwork: signal(
18 /** @type {import("~/components/configurator/artwork/element.js").CLASS | null} */ (null),
19 ),
20 metadata: signal(
21 /** @type {import("~/components/configurator/metadata/element.js").CLASS | null} */ (null),
22 ),
23 input: signal(
24 /** @type {import("~/components/configurator/input/element.js").CLASS | null} */ (null),
25 ),
26 scrobbles: signal(
27 /** @type {import("~/components/configurator/scrobbles/element.js").CLASS | null} */ (null),
28 ),
29 },
30
31 engine: {
32 audio: signal(
33 /** @type {import("~/components/engine/audio/element.js").CLASS | null} */ (null),
34 ),
35 queue: signal(
36 /** @type {import("~/components/engine/queue/element.js").CLASS | null} */ (null),
37 ),
38 repeatShuffle: signal(
39 /** @type {import("~/components/engine/repeat-shuffle/element.js").CLASS | null} */ (null),
40 ),
41 scope: signal(
42 /** @type {import("~/components/engine/scope/element.js").CLASS | null} */ (null),
43 ),
44 },
45
46 orchestrator: {
47 artwork: signal(
48 /** @type {import("~/components/orchestrator/artwork/element.js").CLASS | null} */ (null),
49 ),
50 controller: signal(
51 /** @type {import("~/components/orchestrator/controller/element.js").CLASS | null} */ (null),
52 ),
53 coverGroups: signal(
54 /** @type {import("~/components/orchestrator/cover-groups/element.js").CLASS | null} */ (null),
55 ),
56 autoQueue: signal(
57 /** @type {import("~/components/orchestrator/auto-queue/element.js").CLASS | null} */ (null),
58 ),
59 favourites: signal(
60 /** @type {import("~/components/orchestrator/favourites/element.js").CLASS | null} */ (null),
61 ),
62 mediaSession: signal(
63 /** @type {import("~/components/orchestrator/media-session/element.js").CLASS | null} */ (null),
64 ),
65 output: signal(
66 /** @type {import("~/components/orchestrator/output/element.js").CLASS | null} */ (null),
67 ),
68 pathCollections: signal(
69 /** @type {import("~/components/orchestrator/path-collections/element.js").CLASS | null} */ (null),
70 ),
71 processTracks: signal(
72 /** @type {import("~/components/orchestrator/process-tracks/element.js").CLASS | null} */ (null),
73 ),
74 queueAudio: signal(
75 /** @type {import("~/components/orchestrator/queue-audio/element.js").CLASS | null} */ (null),
76 ),
77 scopedTracks: signal(
78 /** @type {import("~/components/orchestrator/scoped-tracks/element.js").CLASS | null} */ (null),
79 ),
80 scrobbleAudio: signal(
81 /** @type {import("~/components/orchestrator/scrobble-audio/element.js").CLASS | null} */ (null),
82 ),
83 sources: signal(
84 /** @type {import("~/components/orchestrator/sources/element.js").CLASS | null} */ (null),
85 ),
86 },
87};
88
89/**
90 * Default config for facets.
91 */
92export const config = {
93 GROUP,
94
95 // Elements
96 configurator: {
97 artwork: configuratorArtwork,
98 metadata: configuratorMetadata,
99 input,
100 scrobbles,
101 },
102
103 engine: {
104 audio,
105 queue,
106 repeatShuffle,
107 scope,
108 },
109
110 orchestrator: {
111 artwork,
112 controller,
113 coverGroups,
114 autoQueue,
115 favourites,
116 mediaSession,
117 output,
118 pathCollections,
119 processTracks,
120 queueAudio,
121 scopedTracks,
122 scrobbleAudio,
123 sources,
124 },
125
126 /**
127 * Element signals
128 */
129 signals: {
130 configurator: {
131 artwork: signals.configurator.artwork.get,
132 metadata: signals.configurator.metadata.get,
133 input: signals.configurator.input.get,
134 scrobbles: signals.configurator.scrobbles.get,
135 },
136
137 engine: {
138 audio: signals.engine.audio.get,
139 queue: signals.engine.queue.get,
140 repeatShuffle: signals.engine.repeatShuffle.get,
141 scope: signals.engine.scope.get,
142 },
143
144 orchestrator: {
145 artwork: signals.orchestrator.artwork.get,
146 controller: signals.orchestrator.controller.get,
147 coverGroups: signals.orchestrator.coverGroups.get,
148 autoQueue: signals.orchestrator.autoQueue.get,
149 favourites: signals.orchestrator.favourites.get,
150 mediaSession: signals.orchestrator.mediaSession.get,
151 output: signals.orchestrator.output.get,
152 pathCollections: signals.orchestrator.pathCollections.get,
153 processTracks: signals.orchestrator.processTracks.get,
154 queueAudio: signals.orchestrator.queueAudio.get,
155 scopedTracks: signals.orchestrator.scopedTracks.get,
156 scrobbleAudio: signals.orchestrator.scrobbleAudio.get,
157 sources: signals.orchestrator.sources.get,
158 },
159 },
160
161 // Utilities
162
163 container: () => {
164 return document.body.querySelector("#container");
165 },
166
167 hideLoader: () => {
168 const loader = document.querySelector("#diffuse-loader");
169
170 if (loader) {
171 loader.classList.add("loaded");
172 setTimeout(() => {
173 loader.remove();
174 loader.parentElement?.remove();
175 }, 750);
176 }
177 },
178
179 /**
180 * @param {{ title: string }} options
181 */
182 setup: ({ title }) => {
183 document.title = title;
184 },
185
186 /**
187 * Hide the loader and fade in the facet content by adding the `has-loaded`
188 * class to `#container` after two animation frames (so the initial opacity:0
189 * is painted first and the CSS transition actually runs).
190 */
191 ready: () => {
192 config.hideLoader();
193
194 requestAnimationFrame(() => {
195 requestAnimationFrame(() => {
196 document.querySelector("#container")?.classList.add("has-loaded");
197 });
198 });
199 },
200};
201
202export default config;
203
204// 🥡
205
206// Configurators
207
208async function configuratorArtwork() {
209 const { CLASS: ArtworkConfigurator } = await import(
210 "~/components/configurator/artwork/element.js"
211 );
212
213 const ac = new ArtworkConfigurator();
214 ac.setAttribute("group", GROUP);
215 ac.setAttribute("id", "artwork");
216
217 return findExistingOrAdd(ac, signals.configurator.artwork);
218}
219
220async function configuratorMetadata() {
221 const { CLASS: MetadataConfigurator } = await import(
222 "~/components/configurator/metadata/element.js"
223 );
224
225 const mc = new MetadataConfigurator();
226 mc.setAttribute("group", GROUP);
227 mc.setAttribute("id", "metadata");
228
229 return findExistingOrAdd(mc, signals.configurator.metadata);
230}
231
232async function input() {
233 const { CLASS: InputConfigurator } = await import(
234 "~/components/configurator/input/element.js"
235 );
236
237 const i = new InputConfigurator();
238 i.setAttribute("group", GROUP);
239 i.setAttribute("id", "input");
240
241 return findExistingOrAdd(i, signals.configurator.input);
242}
243
244/**
245 * @returns {Promise<ScrobbleElement>}
246 */
247async function scrobbles() {
248 const { CLASS: ScrobblesConfigurator } = await import(
249 "~/components/configurator/scrobbles/element.js"
250 );
251
252 const sc = new ScrobblesConfigurator();
253 sc.setAttribute("group", GROUP);
254 sc.setAttribute("id", "scrobbles");
255
256 const existing = document.body.querySelector(sc.selector);
257
258 if (existing) {
259 return /** @type {ScrobbleElement} */ (existing);
260 }
261
262 document.body.append(sc);
263 return /** @type {ScrobbleElement} */ (/** @type {unknown} */ (sc));
264}
265
266// Engines
267async function audio() {
268 const { CLASS: AudioEngine } = await import(
269 "~/components/engine/audio/element.js"
270 );
271
272 const a = new AudioEngine();
273 a.setAttribute("group", GROUP);
274
275 return findExistingOrAdd(a, signals.engine.audio);
276}
277
278async function queue() {
279 const { CLASS: Queue } = await import(
280 "~/components/engine/queue/element.js"
281 );
282
283 const q = new Queue();
284 q.setAttribute("group", GROUP);
285
286 return findExistingOrAdd(q, signals.engine.queue);
287}
288
289async function repeatShuffle() {
290 const { CLASS: RepeatShuffleEngine } = await import(
291 "~/components/engine/repeat-shuffle/element.js"
292 );
293
294 const r = new RepeatShuffleEngine();
295 r.setAttribute("group", GROUP);
296
297 return findExistingOrAdd(r, signals.engine.repeatShuffle);
298}
299
300async function scope() {
301 const { CLASS: ScopeEngine } = await import(
302 "~/components/engine/scope/element.js"
303 );
304
305 const s = new ScopeEngine();
306 s.setAttribute("group", GROUP);
307
308 return findExistingOrAdd(s, signals.engine.scope);
309}
310
311// Orchestrators
312
313async function artwork() {
314 const [{ CLASS: ArtworkOrchestrator }, ac] = await Promise.all([
315 import("~/components/orchestrator/artwork/element.js"),
316 configuratorArtwork(),
317 ]);
318
319 const a = new ArtworkOrchestrator();
320 a.setAttribute("group", GROUP);
321 a.setAttribute("artwork-selector", ac.selector);
322
323 return findExistingOrAdd(a, signals.orchestrator.artwork);
324}
325
326async function autoQueue() {
327 const [{ CLASS: AutoQueueOrchestrator }, q, r, t] = await Promise.all([
328 import("~/components/orchestrator/auto-queue/element.js"),
329 queue(),
330 repeatShuffle(),
331 scopedTracks(),
332 ]);
333
334 const aqo = new AutoQueueOrchestrator();
335 aqo.setAttribute("group", GROUP);
336 aqo.setAttribute("queue-engine-selector", q.selector);
337 aqo.setAttribute("repeat-shuffle-engine-selector", r.selector);
338 aqo.setAttribute("tracks-selector", t.selector);
339
340 return findExistingOrAdd(aqo, signals.orchestrator.autoQueue);
341}
342
343async function controller() {
344 const [{ CLASS: ControllerOrchestrator }, a, o, q] = await Promise.all([
345 import("~/components/orchestrator/controller/element.js"),
346 audio(),
347 output(),
348 queue(),
349 ]);
350
351 const co = new ControllerOrchestrator();
352 co.setAttribute("audio-engine-selector", a.selector);
353 co.setAttribute("output-selector", o.selector);
354 co.setAttribute("queue-engine-selector", q.selector);
355
356 return findExistingOrAdd(co, signals.orchestrator.controller);
357}
358
359async function coverGroups() {
360 const [{ CLASS: CoverGroupsOrchestrator }, t] = await Promise.all([
361 import("~/components/orchestrator/cover-groups/element.js"),
362 scopedTracks(),
363 ]);
364
365 const cgo = new CoverGroupsOrchestrator();
366 cgo.setAttribute("tracks-selector", t.selector);
367
368 return findExistingOrAdd(cgo, signals.orchestrator.coverGroups);
369}
370
371async function favourites() {
372 const [{ CLASS: FavouritesOrchestrator }, o] = await Promise.all([
373 import("~/components/orchestrator/favourites/element.js"),
374 output(),
375 ]);
376
377 const fo = new FavouritesOrchestrator();
378 fo.setAttribute("group", GROUP);
379 fo.setAttribute("output-selector", o.selector);
380
381 return findExistingOrAdd(fo, signals.orchestrator.favourites);
382}
383
384async function mediaSession() {
385 const [{ CLASS: MediaSessionOrchestrator }, a, aw, o, q] = await Promise
386 .all([
387 import("~/components/orchestrator/media-session/element.js"),
388 audio(),
389 artwork(),
390 output(),
391 queue(),
392 ]);
393
394 const mso = new MediaSessionOrchestrator();
395 mso.setAttribute("group", GROUP);
396 mso.setAttribute("audio-engine-selector", a.selector);
397 mso.setAttribute("artwork-selector", aw.selector);
398 mso.setAttribute("output-selector", o.selector);
399 mso.setAttribute("queue-engine-selector", q.selector);
400
401 return findExistingOrAdd(mso, signals.orchestrator.mediaSession);
402}
403
404/**
405 * @param {Object} [options] - Options
406 * @param {string} [options.namespace] - The namespace to use for the output.
407 */
408async function output(options) {
409 const { CLASS: OutputOrchestrator } = await import(
410 "~/components/orchestrator/output/element.js"
411 );
412
413 const o = new OutputOrchestrator();
414 o.setAttribute("group", GROUP);
415 o.setAttribute("id", "output");
416
417 if (options?.namespace) o.setAttribute("namespace", options.namespace);
418
419 return findExistingOrAdd(o, signals.orchestrator.output);
420}
421
422/**
423 * @param {Object} opts - Options
424 * @param {boolean} [opts.disableWhenReady] - Whether to disable processing when ready.
425 */
426async function processTracks(opts = { disableWhenReady: false }) {
427 const [{ CLASS: ProcessTracksOrchestrator }, i, o, m] = await Promise.all([
428 import("~/components/orchestrator/process-tracks/element.js"),
429 input(),
430 output(),
431 configuratorMetadata(),
432 ]);
433
434 const opt = new ProcessTracksOrchestrator();
435 opt.setAttribute("group", GROUP);
436 opt.setAttribute("input-selector", i.selector);
437 opt.setAttribute("output-selector", o.selector);
438 opt.setAttribute("metadata-selector", m.selector);
439
440 if (!opts.disableWhenReady) {
441 opt.toggleAttribute("process-when-ready");
442 }
443
444 return findExistingOrAdd(opt, signals.orchestrator.processTracks);
445}
446
447async function queueAudio() {
448 const [{ CLASS: QueueAudioOrchestrator }, a, i, o, q, r] = await Promise
449 .all([
450 import("~/components/orchestrator/queue-audio/element.js"),
451 audio(),
452 input(),
453 output(),
454 queue(),
455 repeatShuffle(),
456 ]);
457
458 const oqa = new QueueAudioOrchestrator();
459 oqa.setAttribute("group", GROUP);
460 oqa.setAttribute("audio-engine-selector", a.selector);
461 oqa.setAttribute("input-selector", i.selector);
462 oqa.setAttribute("output-selector", o.selector);
463 oqa.setAttribute("queue-engine-selector", q.selector);
464 oqa.setAttribute("repeat-shuffle-engine-selector", r.selector);
465
466 return findExistingOrAdd(oqa, signals.orchestrator.queueAudio);
467}
468
469async function scopedTracks() {
470 const [{ CLASS: ScopedTracksOrchestrator }, i, o, e] = await Promise.all([
471 import("~/components/orchestrator/scoped-tracks/element.js"),
472 input(),
473 output(),
474 scope(),
475 ]);
476
477 const sto = new ScopedTracksOrchestrator();
478 sto.setAttribute("group", GROUP);
479 sto.setAttribute("input-selector", i.selector);
480 sto.setAttribute("output-selector", o.selector);
481 sto.setAttribute("scope-engine-selector", e.selector);
482
483 return findExistingOrAdd(sto, signals.orchestrator.scopedTracks);
484}
485
486async function scrobbleAudio() {
487 const [{ CLASS: ScrobbleAudioOrchestrator }, a, sc] = await Promise.all([
488 import("~/components/orchestrator/scrobble-audio/element.js"),
489 audio(),
490 scrobbles(),
491 ]);
492
493 const sao = new ScrobbleAudioOrchestrator();
494 sao.setAttribute("group", GROUP);
495 sao.setAttribute("audio-engine-selector", a.selector);
496 sao.setAttribute("scrobble-selector", sc.selector);
497
498 return findExistingOrAdd(sao, signals.orchestrator.scrobbleAudio);
499}
500
501async function pathCollections() {
502 const [{ CLASS: PathCollectionsOrchestrator }, o] = await Promise.all([
503 import("~/components/orchestrator/path-collections/element.js"),
504 output(),
505 ]);
506
507 const pco = new PathCollectionsOrchestrator();
508 pco.setAttribute("group", GROUP);
509 pco.setAttribute("output-selector", o.selector);
510
511 return findExistingOrAdd(pco, signals.orchestrator.pathCollections);
512}
513
514async function sources() {
515 const [{ CLASS: SourcesOrchestrator }, i, o] = await Promise.all([
516 import("~/components/orchestrator/sources/element.js"),
517 input(),
518 output(),
519 ]);
520
521 const so = new SourcesOrchestrator();
522 so.setAttribute("group", GROUP);
523 so.setAttribute("input-selector", i.selector);
524 so.setAttribute("output-selector", o.selector);
525
526 return findExistingOrAdd(so, signals.orchestrator.sources);
527}
528
529// 🛠️
530
531/**
532 * @template {DiffuseElement} T
533 * @param {T} element
534 * @param {Signal<T | null>} signal
535 * @returns {T}
536 */
537export function findExistingOrAdd(element, signal) {
538 /** @type {T | null} */
539 const alreadyAdded = document.body.querySelector(element.selector);
540
541 if (!alreadyAdded) {
542 document.body.append(element);
543 signal.value = element;
544 return element;
545 }
546
547 signal.value = alreadyAdded;
548 return alreadyAdded;
549}