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 0e0d32fd2baa97616c50a2a3b6b60bff3fad5382 442 lines 12 kB view raw
1import { BroadcastableDiffuseElement, defineElement } from "~/common/element.js"; 2import { batch, computed, signal } from "~/common/signal.js"; 3 4/** 5 * @import {DiffuseElement} from "~/common/element.js" 6 * @import {Facet, PlaylistItem, Setting, Track} from "~/definitions/types.d.ts" 7 * @import {OutputManagerDeputy, OutputElement} from "~/components/output/types.d.ts" 8 * 9 * @import {OutputConfiguratorElement} from "./types.d.ts" 10 */ 11 12/** 13 * @typedef {OutputElement} Output 14 */ 15 16const STORAGE_PREFIX = "diffuse/configurator/output"; 17 18//////////////////////////////////////////// 19// ELEMENT 20//////////////////////////////////////////// 21 22/** 23 * @implements {OutputConfiguratorElement} 24 */ 25class OutputConfigurator extends BroadcastableDiffuseElement { 26 static NAME = "diffuse/configurator/output"; 27 28 constructor() { 29 super(); 30 31 /** @type {OutputManagerDeputy} */ 32 const manager = { 33 facets: { 34 collection: computed(() => { 35 const out = this.#selected.value; 36 if (out) return out.facets.collection(); 37 38 const def = this.#defaultOutput.value; 39 if (def) return def.facets.collection(); 40 if (this.hasDefault()) return { state: "loading" }; 41 42 return this.#setupFinished.value 43 ? { state: "loaded", data: this.#memory.facets.value } 44 : { state: "loading" }; 45 }), 46 reload: () => { 47 const def = this.#defaultOutput.value; 48 if (def) def.facets.reload(); 49 50 const out = this.#selected.value; 51 if (out) return out.facets.reload(); 52 53 return Promise.resolve(); 54 }, 55 save: async (newFacets) => { 56 const out = this.#selected.value; 57 if (out) return await out.facets.save(newFacets); 58 59 const def = this.#defaultOutput.value; 60 if (def) return await def.facets.save(newFacets); 61 62 this.#memory.facets.value = newFacets; 63 }, 64 }, 65 playlistItems: { 66 collection: computed(() => { 67 const out = this.#selected.value; 68 if (out) return out.playlistItems.collection(); 69 70 const def = this.#defaultOutput.value; 71 if (def) return def.playlistItems.collection(); 72 if (this.hasDefault()) return { state: "loading" }; 73 74 return this.#setupFinished.value 75 ? { state: "loaded", data: this.#memory.playlistItems.value } 76 : { state: "loading" }; 77 }), 78 reload: () => { 79 const def = this.#defaultOutput.value; 80 if (def) def.playlistItems.reload(); 81 82 const out = this.#selected.value; 83 if (out) return out.playlistItems.reload(); 84 85 return Promise.resolve(); 86 }, 87 save: async (newPlaylistItems) => { 88 const out = this.#selected.value; 89 if (out) return await out.playlistItems.save(newPlaylistItems); 90 91 const def = this.#defaultOutput.value; 92 if (def) return await def.playlistItems.save(newPlaylistItems); 93 94 this.#memory.playlistItems.value = newPlaylistItems; 95 }, 96 }, 97 settings: { 98 collection: computed(() => { 99 const out = this.#selected.value; 100 if (out) return out.settings.collection(); 101 102 const def = this.#defaultOutput.value; 103 if (def) return def.settings.collection(); 104 if (this.hasDefault()) return { state: "loading" }; 105 106 return this.#setupFinished.value 107 ? { state: "loaded", data: this.#memory.settings.value } 108 : { state: "loading" }; 109 }), 110 reload: () => { 111 const def = this.#defaultOutput.value; 112 if (def) def.settings.reload(); 113 114 const out = this.#selected.value; 115 if (out) return out.settings.reload(); 116 117 return Promise.resolve(); 118 }, 119 save: async (newSettings) => { 120 const out = this.#selected.value; 121 if (out) return await out.settings.save(newSettings); 122 123 const def = this.#defaultOutput.value; 124 if (def) return await def.settings.save(newSettings); 125 126 this.#memory.settings.value = newSettings; 127 }, 128 }, 129 tracks: { 130 collection: computed(() => { 131 const out = this.#selected.value; 132 if (out) return out.tracks.collection(); 133 134 const def = this.#defaultOutput.value; 135 if (def) return def.tracks.collection(); 136 if (this.hasDefault()) return { state: "loading" }; 137 138 return this.#setupFinished.value 139 ? { state: "loaded", data: this.#memory.tracks.value } 140 : { state: "loading" }; 141 }), 142 reload: () => { 143 const def = this.#defaultOutput.value; 144 if (def) def.tracks.reload(); 145 146 const out = this.#selected.value; 147 if (out) return out.tracks.reload(); 148 149 return Promise.resolve(); 150 }, 151 save: async (newTracks) => { 152 const out = this.#selected.value; 153 if (out) return await out.tracks.save(newTracks); 154 155 const def = this.#defaultOutput.value; 156 if (def) return await def.tracks.save(newTracks); 157 158 this.#memory.tracks.value = newTracks; 159 }, 160 }, 161 162 // Other 163 ready: computed(() => { 164 const out = this.#selected.value; 165 if (out) return out.ready(); 166 167 const def = this.#defaultOutput.value; 168 if (def) return def.ready(); 169 170 return this.#setupFinished.value; 171 }), 172 }; 173 174 // Assign manager properties to class 175 this.facets = manager.facets; 176 this.playlistItems = manager.playlistItems; 177 this.settings = manager.settings; 178 this.tracks = manager.tracks; 179 this.ready = manager.ready; 180 181 // Effects 182 183 /** 184 * When there's a selected output and its collection changes, 185 * save it to the default output or memory. 186 */ 187 this.effect(() => { 188 const out = this.#selected.value; 189 if (!out) return; 190 191 const col = out.facets.collection(); 192 if (col.state !== "loaded") return; 193 194 const def = this.#defaultOutput.value; 195 if (def) def.facets.save(col.data); 196 else this.#memory.facets.set(col.data); 197 }); 198 199 this.effect(() => { 200 const out = this.#selected.value; 201 if (!out) return; 202 203 const col = out.playlistItems.collection(); 204 if (col.state !== "loaded") return; 205 206 const def = this.#defaultOutput.value; 207 if (def) def.playlistItems.save(col.data); 208 else this.#memory.playlistItems.set(col.data); 209 }); 210 211 this.effect(() => { 212 const out = this.#selected.value; 213 if (!out) return; 214 215 const col = out.settings.collection(); 216 if (col.state !== "loaded") return; 217 218 const def = this.#defaultOutput.value; 219 if (def) def.settings.save(col.data); 220 else this.#memory.settings.set(col.data); 221 }); 222 223 this.effect(() => { 224 const out = this.#selected.value; 225 if (!out) return; 226 227 const col = out.tracks.collection(); 228 if (col.state !== "loaded") return; 229 230 const def = this.#defaultOutput.value; 231 if (def) def.tracks.save(col.data); 232 else this.#memory.tracks.set(col.data); 233 }); 234 } 235 236 // SIGNALS 237 238 #activated = signal(/** @type {Set<string>} */ (new Set())); 239 240 #defaultOutput = signal( 241 /** @type {Output | null | undefined} */ (undefined), 242 ); 243 244 #memory = { 245 facets: signal(/** @type {Facet[]} */ ([])), 246 playlistItems: signal(/** @type {PlaylistItem[]} */ ([])), 247 settings: signal(/** @type {Setting[]} */ ([])), 248 tracks: signal(/** @type {Track[]} */ ([])), 249 }; 250 251 #selected = signal( 252 /** @type {Output | null | undefined} */ (undefined), 253 ); 254 255 #setupFinished = signal(false); 256 257 // STATE 258 259 activated = this.#activated.get; 260 selected = computed(() => this.#selected.value ?? null); 261 262 // LIFECYCLE 263 264 /** 265 * @override 266 */ 267 async connectedCallback() { 268 // Broadcast if needed 269 if (this.hasAttribute("group")) { 270 const actions = this.broadcast(this.identifier, { 271 selectOutput: { 272 strategy: "replicate", 273 fn: this.#selectOutput, 274 }, 275 }); 276 277 if (actions) { 278 this.#selectOutput = actions.selectOutput; 279 } 280 } 281 282 // Super 283 super.connectedCallback(); 284 285 // Outputs 286 const def_ault = this.getAttribute("default"); 287 const selectedOutputId = this.#selectedOutputId(); 288 289 batch(() => { 290 /** @type {Set<string>} */ 291 const activated = new Set(); 292 293 if (def_ault) { 294 activated.add(def_ault); 295 } 296 297 if (selectedOutputId) { 298 activated.add(selectedOutputId); 299 } 300 301 this.#activated.value = activated; 302 }); 303 304 /** @type {Output | null} */ 305 const defaultOutput = def_ault ? await this.#findOutput(def_ault) : null; 306 const selectedOutput = await this.#findOutput(selectedOutputId); 307 308 batch(() => { 309 this.#selected.value = selectedOutput; 310 this.#defaultOutput.value = defaultOutput; 311 this.#setupFinished.value = true; 312 }); 313 } 314 315 // MISC 316 317 /** 318 * @param {string | null} id 319 */ 320 async #findOutput(id) { 321 const el = id ? this.root().querySelector(`#${id}`) : null; 322 if (!el) return null; 323 324 await customElements.whenDefined(el.localName); 325 326 if ( 327 "identifier" in el === false || 328 "tracks" in el === false 329 ) { 330 return null; 331 } 332 333 return /** @type {Output} */ (/** @type {unknown} */ (el)); 334 } 335 336 /** 337 * @param {string | null} id 338 */ 339 #selectOutput = async (id) => { 340 if (id) { 341 this.#activated.value = new Set([...this.#activated.value.values(), id]); 342 } 343 344 this.#selected.value = await this.#findOutput(id); 345 }; 346 347 #selectedOutputId() { 348 return localStorage.getItem(`${STORAGE_PREFIX}/selected/id`); 349 } 350 351 /** 352 * @override 353 */ 354 dependencies = () => { 355 return Object.fromEntries( 356 Array.from(this.root().children).flatMap((element) => { 357 if (element.hasAttribute("id") === false) { 358 console.warn( 359 "Missing `id` for output configurator child element with `localName` '" + 360 element.localName + "'", 361 ); 362 return []; 363 } 364 365 const d = /** @type {DiffuseElement} */ (element); 366 return [[d.id, d]]; 367 }), 368 ); 369 }; 370 371 // ADDITIONAL ACTIONS 372 373 deselect = async () => { 374 localStorage.removeItem(`${STORAGE_PREFIX}/selected/id`); 375 await this.#selectOutput(null); 376 }; 377 378 hasDefault() { 379 return this.hasAttribute("default"); 380 } 381 382 hasSelected() { 383 return this.#selectedOutputId() !== null; 384 } 385 386 loadSelected = async () => { 387 const selectedOutput = await this.#findOutput(this.#selectedOutputId()); 388 this.#selected.value = selectedOutput; 389 }; 390 391 options = async () => { 392 const deps = this.dependencies(); 393 const entries = Object.entries(deps); 394 395 return entries.map(([k, v]) => { 396 return { 397 id: k, 398 label: v.label ?? v.getAttribute("label"), 399 element: /** @type {OutputElement} */ (v), 400 }; 401 }); 402 }; 403 404 /** 405 * @param {string} id 406 */ 407 select = async (id) => { 408 localStorage.setItem(`${STORAGE_PREFIX}/selected/id`, id); 409 await this.#selectOutput(id); 410 }; 411 412 /** 413 * @param {string} label 414 * @returns {Promise<{ id: string, label: string, element: OutputElement }>} 415 */ 416 waitForOption = (label) => { 417 return new Promise((resolve) => { 418 const check = async () => { 419 const opt = (await this.options()).find((o) => o.label === label); 420 if (opt) { 421 observer.disconnect(); 422 resolve(opt); 423 } 424 }; 425 426 const observer = new MutationObserver(check); 427 observer.observe(this, { childList: true }); 428 check(); 429 }); 430 }; 431} 432 433export default OutputConfigurator; 434 435//////////////////////////////////////////// 436// REGISTER 437//////////////////////////////////////////// 438 439export const CLASS = OutputConfigurator; 440export const NAME = "dc-output"; 441 442defineElement(NAME, CLASS);