A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at v4 549 lines 16 kB view raw
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}