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.

feat: artwork controller time status & progress bar

+340 -161
+28
src/common/constituents/default.js
··· 61 61 if (!trigger) queue.shift(); 62 62 }); 63 63 }); 64 + 65 + // Return elements 66 + return { 67 + GROUP, 68 + 69 + configurator: { 70 + input, 71 + }, 72 + engine: { 73 + queue, 74 + }, 75 + input: { 76 + openSubsonic, 77 + s3, 78 + }, 79 + orchestrator: { 80 + queueTracks: oqt, 81 + }, 82 + output: { 83 + indexedDB: idb, 84 + }, 85 + transformer: { 86 + jsonStringOutput: json, 87 + refiner: { 88 + default: refiner, 89 + }, 90 + }, 91 + }; 64 92 }
+93 -49
src/components/engine/audio/element.js
··· 1 1 import { BroadcastableDiffuseElement } from "@common/element.js"; 2 - import { signal } from "@common/signal.js"; 2 + import { computed, signal } from "@common/signal.js"; 3 3 4 4 /** 5 - * @import {Actions, Audio, AudioState, State} from "./types.d.ts" 5 + * @import {Actions, Audio, AudioState, AudioStateReadOnly, LoadingState} from "./types.d.ts" 6 6 * @import {RenderArg} from "@common/element.d.ts" 7 + * @import {SignalReader} from "@common/signal.d.ts" 7 8 */ 8 9 9 10 //////////////////////////////////////////// ··· 112 113 */ 113 114 adjustVolume(args) { 114 115 if (args.audioId) { 115 - this.withAudioNode(args.audioId, (audio) => { 116 + this.#withAudioNode(args.audioId, (audio) => { 116 117 audio.volume = args.volume; 117 118 }); 118 119 } else { ··· 124 125 * @type {Actions["pause"]} 125 126 */ 126 127 pause({ audioId }) { 127 - this.withAudioNode(audioId, (audio) => audio.pause()); 128 + this.#withAudioNode(audioId, (audio) => audio.pause()); 128 129 } 129 130 130 131 /** 131 132 * @type {Actions["play"]} 132 133 */ 133 134 play({ audioId, volume }) { 134 - this.withAudioNode(audioId, (audio, item) => { 135 + this.#withAudioNode(audioId, (audio, item) => { 135 136 audio.volume = volume ?? this.volume(); 136 137 audio.muted = false; 137 138 ··· 139 140 if (!audio.isConnected) return; 140 141 141 142 const promise = audio.play() || Promise.resolve(); 142 - item.state({ isPlaying: true }); 143 + item.$state.isPlaying.set(true); 143 144 144 145 promise.catch((e) => { 145 146 if (!audio.isConnected) { ··· 148 149 const err = 149 150 "Couldn't play audio automatically. Please resume playback manually."; 150 151 console.error(err, e); 151 - item.state({ isPlaying: false }); 152 + item.$state.isPlaying.set(false); 152 153 }); 153 154 }); 154 155 } ··· 157 158 * @type {Actions["reload"]} 158 159 */ 159 160 reload(args) { 160 - this.withAudioNode(args.audioId, (audio, item) => { 161 + this.#withAudioNode(args.audioId, (audio, item) => { 161 162 if (audio.readyState === 0 || audio.error?.code === 2) { 162 163 audio.load(); 163 164 ··· 179 180 * @type {Actions["seek"]} 180 181 */ 181 182 seek({ audioId, percentage }) { 182 - this.withAudioNode(audioId, (audio) => { 183 + this.#withAudioNode(audioId, (audio) => { 183 184 if (!isNaN(audio.duration)) { 184 185 audio.currentTime = audio.duration * percentage; 185 186 } ··· 237 238 // 🛠️ 238 239 239 240 /** 241 + * Get the state of a single audio item. 242 + * 243 + * @param {string} audioId 244 + * @returns {SignalReader<AudioStateReadOnly | undefined>} 245 + */ 246 + _state(audioId) { 247 + return computed(() => { 248 + const _trigger = this.#items.value; 249 + 250 + const s = this.#itemElement(audioId)?.state; 251 + return s ? { ...s } : undefined; 252 + }); 253 + } 254 + 255 + /** 256 + * Get the state of a single audio item. 257 + * 240 258 * @param {string} audioId 241 - * @param {(audio: HTMLAudioElement, item: AudioEngineItem) => void} fn 259 + * @returns {AudioStateReadOnly | undefined} 242 260 */ 243 - withAudioNode(audioId, fn) { 261 + state(audioId) { 262 + return this._state(audioId)(); 263 + } 264 + 265 + /** 266 + * @param {string} audioId 267 + */ 268 + #itemElement(audioId) { 244 269 const node = this.querySelector( 245 270 `de-audio-item[id="${audioId}"]:not([preload])`, 246 271 ); 247 272 248 273 if (node) { 249 274 const item = /** @type {AudioEngineItem} */ (node); 250 - if (item) fn(item.audio, item); 275 + return item; 251 276 } 252 277 } 278 + 279 + /** 280 + * @param {string} audioId 281 + * @param {(audio: HTMLAudioElement, item: AudioEngineItem) => void} fn 282 + */ 283 + #withAudioNode(audioId, fn) { 284 + const item = this.#itemElement(audioId); 285 + if (item) fn(item.audio, item); 286 + } 253 287 } 254 288 255 289 export default AudioEngine; ··· 259 293 //////////////////////////////////////////// 260 294 261 295 class AudioEngineItem extends HTMLElement { 262 - /** 263 - * @type {AudioState} 264 - */ 265 - #state; 266 - 267 296 constructor() { 268 297 super(); 269 298 270 299 const ip = this.getAttribute("initial-progress"); 271 300 272 - this.#state = { 273 - duration: 0, 274 - hasEnded: false, 275 - id: this.id, 276 - isPlaying: true, 277 - isPreload: this.hasAttribute("preload"), 278 - loadingState: "loading", 279 - mimeType: this.getAttribute("mime-type") ?? undefined, 280 - progress: ip ? parseFloat(ip) : 0, 281 - url: this.getAttribute("url") ?? "", 301 + /** 302 + * @type {AudioState} 303 + */ 304 + this.$state = { 305 + duration: signal(0), 306 + hasEnded: signal(false), 307 + isPlaying: signal(true), 308 + isPreload: signal(this.hasAttribute("preload")), 309 + loadingState: signal(/** @type {LoadingState} */ ("loading")), 310 + progress: signal(ip ? parseFloat(ip) : 0), 282 311 }; 283 312 284 313 const audio = this.audio; ··· 294 323 audio.addEventListener("waiting", this.waitingEvent); 295 324 } 296 325 326 + // LIFECYCLE 327 + 328 + connectedCallback() { 329 + } 330 + 331 + // STATE 332 + 333 + /** 334 + * @type {AudioStateReadOnly} 335 + */ 336 + get state() { 337 + return { 338 + id: this.id, 339 + mimeType: (this.getAttribute("mime-type") ?? undefined), 340 + url: (this.getAttribute("url") ?? ""), 341 + 342 + duration: this.$state.duration.get, 343 + hasEnded: this.$state.hasEnded.get, 344 + isPlaying: this.$state.isPlaying.get, 345 + isPreload: this.$state.isPreload.get, 346 + loadingState: this.$state.loadingState.get, 347 + progress: this.$state.progress.get, 348 + }; 349 + } 350 + 297 351 // RELATED ELEMENTS 298 352 299 353 get audio() { ··· 308 362 else return null; 309 363 } 310 364 311 - // STATE 312 - 313 - /** 314 - * @param {Partial<AudioState> | undefined} [s] 315 - */ 316 - state(s) { 317 - if (s) this.#state = { ...this.#state, ...s }; 318 - else return { ...this.#state }; 319 - } 320 - 321 365 // EVENTS 322 366 323 367 /** ··· 349 393 const audio = /** @type {HTMLAudioElement} */ (event.target); 350 394 351 395 if (!isNaN(audio.duration)) { 352 - engineItem(audio)?.state({ duration: audio.duration }); 396 + engineItem(audio)?.$state.duration.set(audio.duration); 353 397 } 354 398 } 355 399 ··· 360 404 const audio = /** @type {HTMLAudioElement} */ (event.target); 361 405 audio.currentTime = 0; 362 406 363 - engineItem(audio)?.state({ hasEnded: true }); 407 + engineItem(audio)?.$state.hasEnded.set(true); 364 408 engineItem(audio)?.engine?.$hasEnded.set(true); 365 409 } 366 410 ··· 371 415 const audio = /** @type {HTMLAudioElement} */ (event.target); 372 416 const code = audio.error?.code || 0; 373 417 374 - engineItem(audio)?.state({ loadingState: { error: { code } } }); 418 + engineItem(audio)?.$state.loadingState.set({ error: { code } }); 375 419 } 376 420 377 421 /** ··· 381 425 const audio = /** @type {HTMLAudioElement} */ (event.target); 382 426 383 427 const item = engineItem(audio); 384 - const itemState = item?.state(); 428 + const itemState = item?.$state; 385 429 const ended = itemState 386 - ? itemState.hasEnded || itemState.progress === 1 430 + ? itemState.hasEnded.value || itemState.progress.value === 1 387 431 : false; 388 432 389 - item?.state({ isPlaying: false }); 433 + item?.$state.isPlaying.set(false); 390 434 item?.engine?.$isPlaying.set(ended); 391 435 } 392 436 ··· 396 440 playEvent(event) { 397 441 const audio = /** @type {HTMLAudioElement} */ (event.target); 398 442 399 - engineItem(audio)?.state({ isPlaying: true }); 443 + engineItem(audio)?.$state.isPlaying.set(true); 400 444 engineItem(audio)?.engine?.$isPlaying.set(true); 401 445 402 446 // In case audio was preloaded: ··· 416 460 timeupdateEvent(event) { 417 461 const audio = /** @type {HTMLAudioElement} */ (event.target); 418 462 419 - engineItem(audio)?.state({ 420 - progress: isNaN(audio.duration) || audio.duration === 0 463 + engineItem(audio)?.$state.progress.set( 464 + isNaN(audio.duration) || audio.duration === 0 421 465 ? 0 422 466 : audio.currentTime / audio.duration, 423 - }); 467 + ); 424 468 } 425 469 426 470 /** ··· 451 495 */ 452 496 function finishedLoading(event) { 453 497 const audio = /** @type {HTMLAudioElement} */ (event.target); 454 - engineItem(audio)?.state({ loadingState: "loaded" }); 498 + engineItem(audio)?.$state.loadingState.set("loaded"); 455 499 } 456 500 457 501 /** ··· 460 504 function initiateLoading(event) { 461 505 const audio = /** @type {HTMLAudioElement} */ (event.target); 462 506 if (audio.readyState < 4) { 463 - engineItem(audio)?.state({ loadingState: "loading" }); 507 + engineItem(audio)?.$state.loadingState.set("loading"); 464 508 } 465 509 } 466 510
+27 -14
src/components/engine/audio/types.d.ts
··· 1 - import type { SignalReader } from "@common/signal.d.ts"; 1 + import type { Signal, SignalReader } from "@common/signal.d.ts"; 2 2 3 3 export type Actions = { 4 4 adjustVolume: (_: { audioId?: string; volume: number }) => void; ··· 15 15 id: string; 16 16 isPreload: boolean; 17 17 mimeType?: string; 18 + // NOTE: Initial progress 18 19 progress?: number; 19 20 url: string; 20 21 }; 21 22 22 23 export type AudioState = { 23 - duration: number; 24 + duration: Signal<number>; 25 + hasEnded: Signal<boolean>; 26 + isPlaying: Signal<boolean>; 27 + isPreload: Signal<boolean>; 28 + loadingState: Signal<LoadingState>; 29 + progress: Signal<number>; 30 + }; 31 + 32 + export type AudioStateReadOnly = { 24 33 id: string; 25 - hasEnded: boolean; 26 - loadingState: 27 - | "initialisation" 28 - | "loading" 29 - | "loaded" 30 - | { 31 - error: { code: number }; 32 - }; 33 - isPlaying: boolean; 34 - isPreload: boolean; 35 - mimeType?: string; 36 - progress: number; 37 34 url: string; 35 + mimeType: string | undefined; 36 + 37 + duration: SignalReader<number>; 38 + hasEnded: SignalReader<boolean>; 39 + isPlaying: SignalReader<boolean>; 40 + isPreload: SignalReader<boolean>; 41 + loadingState: SignalReader<LoadingState>; 42 + progress: SignalReader<number>; 38 43 }; 44 + 45 + export type LoadingState = 46 + | "initialisation" 47 + | "loading" 48 + | "loaded" 49 + | { 50 + error: { code: number }; 51 + }; 39 52 40 53 export type State = { 41 54 isPlaying: SignalReader<boolean>;
+180 -97
src/themes/blur/artwork-controller/element.js
··· 10 10 /** 11 11 * @import {RenderArg} from "@common/element.d.ts" 12 12 * @import {Track} from "@definitions/types.d.ts" 13 + * @import {AudioStateReadOnly} from "@components/engine/audio/types.d.ts" 13 14 * @import {InputElement} from "@components/input/types.d.ts" 14 15 * @import {Artwork} from "@components/processor/artwork/types.d.ts" 15 16 */ ··· 22 23 23 24 // SIGNALS 24 25 25 - // activeTrack = signal(/** @type {Track | undefined} */ (undefined)); 26 + // #audio = signal(/** @type {AudioStateReadOnly | undefined} */ (undefined)); 26 27 #artwork = signal(/** @type {Artwork[]} */ ([])); 27 28 #artworkColor = signal(/** @type {string | undefined} */ (undefined)); 28 29 #artworkLightMode = signal(false); ··· 62 63 // Changed artwork based on active queue item. 63 64 const debouncedChangeArtwork = debounce( 64 65 1000, 65 - this.#changeArtwork.bind(this), 66 + this.#setArtwork.bind(this), 66 67 ); 67 68 68 69 this.effect(() => { 69 70 debouncedChangeArtwork(queue.now()); 70 71 }); 71 72 72 - // Force render when elements are defined 73 + this.effect(() => { 74 + const curr = queue.now(); 75 + const aud = curr ? audio.state(curr.id) : undefined; 76 + 77 + console.log("NOW", curr, aud); 78 + }); 73 79 74 - // this.effect(() => { 75 - // this.forceRender(); 76 - // }); 80 + this.effect(() => this.#changeArtworkInDOM()); 81 + this.effect(() => this.#formatTimestamps()); 82 + this.effect(() => this.#lightOrDark()); 77 83 }); 84 + } 78 85 79 - this.#artworkEffects(); 86 + //////////////////////////////////////////// 87 + // ✨ EFFECTS 88 + // 🖼️ Artwork 89 + //////////////////////////////////////////// 90 + 91 + /** @type {Record<string, ReturnType<typeof setTimeout>>} */ 92 + #timeouts = {}; 93 + 94 + #changeArtworkInDOM() { 95 + const art = this.#artwork.value; 96 + 97 + // No artwork, fade out existing. 98 + if (art.length === 0) { 99 + this.root().querySelectorAll(".artwork img").forEach((el) => { 100 + const element = /** @type {HTMLElement} */ (el); 101 + element.style.opacity = "0"; 102 + const hash = element.getAttribute("data-hash"); 103 + if (hash) { 104 + this.#timeouts[hash] = setTimeout(() => element.remove(), 1000); 105 + } 106 + }); 107 + return; 108 + } 109 + 110 + // Determine if the current artwork needs to be replaced. 111 + const hash = xxh32r(art[0].bytes).toString(); 112 + 113 + /** @type {HTMLImageElement | null} */ 114 + const existingArtwork = this.root().querySelector( 115 + `.artwork img[data-hash="${hash}"]`, 116 + ); 117 + 118 + // If the artwork is the same, stop here. 119 + if (existingArtwork) { 120 + const timeoutId = this.#timeouts[hash]; 121 + if (timeoutId) clearTimeout(timeoutId); 122 + existingArtwork.style.opacity = "1"; 123 + return; 124 + } 125 + 126 + // Add new artwork 127 + const blob = new Blob( 128 + [/** @type {ArrayBuffer} */ (art[0].bytes.buffer)], 129 + { type: art[0].mime }, 130 + ); 131 + const url = URL.createObjectURL(blob); 132 + 133 + /** @type {HTMLImageElement} */ 134 + const img = document.createElement("img"); 135 + img.setAttribute("data-hash", hash); 136 + img.src = url; 137 + 138 + // Extract average color 139 + img.onload = () => { 140 + const fac = new FastAverageColor(); 141 + const color = fac.getColor(img); 142 + const rgb = color.value; 143 + const o = Math.round( 144 + (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000, 145 + ); 146 + 147 + this.#artworkColor.value = color.rgba; 148 + this.#artworkLightMode.value = o > 165; 149 + 150 + /** @type {HTMLElement | null} */ 151 + const bg = this.root().querySelector(".controller__background"); 152 + if (bg) bg.style.backgroundColor = color.rgba; 153 + 154 + /** @type {HTMLElement | null} */ 155 + const main = this.root().querySelector("main"); 156 + if (main) main.style.backgroundColor = color.rgba; 157 + 158 + img.style.opacity = "1"; 159 + 160 + this.root().querySelectorAll(".artwork img").forEach((el) => { 161 + if (el === img) return; 162 + 163 + const element = /** @type {HTMLElement} */ (el); 164 + element.style.opacity = "0"; 165 + this.#timeouts[hash] = setTimeout(() => element.remove(), 1000); 166 + }); 167 + }; 168 + 169 + // Insert new artwork 170 + this.root().querySelector(".artwork")?.appendChild(img); 80 171 } 81 172 82 - // EFFECTS 173 + #lightOrDark() { 174 + const controller = this.root().querySelector(".controller__inner"); 175 + if (!controller) return; 176 + 177 + if (this.#artworkLightMode.value) { 178 + controller.classList.add("controller__inner--light-mode"); 179 + } else controller.classList.remove("controller__inner--light-mode"); 180 + } 83 181 84 182 /** 85 183 * @param {Track | null} track 86 184 */ 87 - async #changeArtwork(track) { 88 - console.log("QUEUE NOW", track); 89 - 185 + async #setArtwork(track) { 90 186 if (!track) { 91 187 this.#artwork.value = []; 92 188 return; ··· 99 195 method: "HEAD", 100 196 uri: track.uri, 101 197 }); 102 - 103 - console.log(resGet, this.input); 104 198 105 199 if (!resGet) return; 106 200 ··· 121 215 122 216 const art = await this.artwork?.artwork(request) ?? []; 123 217 124 - console.log(art); 218 + console.log("ART", art); 125 219 126 220 const currCacheId = track ? await trackArtworkCacheId(track) : undefined; 127 221 if (cacheId === currCacheId) this.#artwork.set(art); 128 222 } 129 223 130 - #artworkEffects() { 131 - /** @type {Record<string, ReturnType<typeof setTimeout>>} */ 132 - const timeouts = {}; 224 + //////////////////////////////////////////// 225 + // ✨ EFFECTS 226 + // ⌚️ Time 227 + //////////////////////////////////////////// 228 + #formatTimestamps() { 229 + const curr = this.queue?.now?.() ?? undefined; 230 + const audio = curr ? this.audio?.state(curr.id) : undefined; 231 + const prog = audio?.progress() ?? 0; 232 + const dur = curr?.stats?.duration ?? audio?.duration(); 133 233 134 - this.effect(() => { 135 - const art = this.#artwork.value; 234 + if (audio && dur != undefined && !isNaN(dur)) { 235 + const p = Temporal.Duration.from({ 236 + milliseconds: Math.round(dur * prog * 1000), 237 + }).round({ 238 + largestUnit: "hours", 239 + }); 136 240 137 - // No artwork, fade out existing. 138 - if (art.length === 0) { 139 - this.root().querySelectorAll(".artwork img").forEach((el) => { 140 - const element = /** @type {HTMLElement} */ (el); 141 - element.style.opacity = "0"; 142 - const hash = element.getAttribute("data-hash"); 143 - if (hash) timeouts[hash] = setTimeout(() => element.remove(), 1000); 241 + const d = Temporal.Duration.from({ milliseconds: Math.round(dur * 1000) }) 242 + .round({ 243 + largestUnit: "hours", 144 244 }); 145 - return; 146 - } 147 245 148 - // Determine if the current artwork needs to be replaced. 149 - const hash = xxh32r(art[0].bytes).toString(); 150 - 151 - /** @type {HTMLImageElement | null} */ 152 - const existingArtwork = this.root().querySelector( 153 - `.artwork img[data-hash="${hash}"]`, 154 - ); 155 - 156 - // If the artwork is the same, stop here. 157 - if (existingArtwork) { 158 - const timeoutId = timeouts[hash]; 159 - if (timeoutId) clearTimeout(timeoutId); 160 - existingArtwork.style.opacity = "1"; 161 - return; 162 - } 163 - 164 - // Add new artwork 165 - const blob = new Blob( 166 - [/** @type {ArrayBuffer} */ (art[0].bytes.buffer)], 167 - { type: art[0].mime }, 168 - ); 169 - const url = URL.createObjectURL(blob); 170 - 171 - /** @type {HTMLImageElement} */ 172 - const img = document.createElement("img"); 173 - img.setAttribute("data-hash", hash); 174 - img.src = url; 175 - 176 - // Extract average color 177 - img.onload = () => { 178 - const fac = new FastAverageColor(); 179 - const color = fac.getColor(img); 180 - const rgb = color.value; 181 - const o = Math.round( 182 - (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000, 183 - ); 184 - 185 - this.#artworkColor.value = color.rgba; 186 - this.#artworkLightMode.value = o > 165; 187 - 188 - /** @type {HTMLElement | null} */ 189 - const bg = this.root().querySelector(".controller__background"); 190 - if (bg) bg.style.backgroundColor = color.rgba; 191 - 192 - /** @type {HTMLElement | null} */ 193 - const main = this.root().querySelector("main"); 194 - if (main) main.style.backgroundColor = color.rgba; 195 - 196 - img.style.opacity = "1"; 197 - 198 - this.root().querySelectorAll(".artwork img").forEach((el) => { 199 - if (el === img) return; 246 + this.#time.value = this.#formatTime(p); 247 + this.#duration.value = this.#formatTime(d); 248 + } else { 249 + this.#time.value = "0:00"; 250 + this.#duration.value = "0:00"; 251 + } 252 + } 200 253 201 - const element = /** @type {HTMLElement} */ (el); 202 - element.style.opacity = "0"; 203 - timeouts[hash] = setTimeout(() => element.remove(), 1000); 204 - }); 205 - }; 254 + /** 255 + * @param {Temporal.Duration} duration 256 + */ 257 + #formatTime(duration) { 258 + return `${duration.hours > 0 ? duration.hours.toFixed(0) + ":" : ""}${ 259 + duration.hours > 0 260 + ? (duration.minutes > 9 261 + ? duration.minutes.toFixed(0) 262 + : "0" + duration.minutes.toFixed(0)) 263 + : duration.minutes.toFixed(0) 264 + }:${ 265 + duration.seconds > 9 266 + ? duration.seconds.toFixed(0) 267 + : "0" + duration.seconds.toFixed(0) 268 + }`; 269 + } 206 270 207 - // Insert new artwork 208 - this.root().querySelector(".artwork")?.appendChild(img); 209 - }); 271 + // EVENTS 210 272 211 - this.effect(() => { 212 - const controller = this.root().querySelector(".controller__inner"); 213 - if (!controller) return; 273 + /** 274 + * @param {MouseEvent} event 275 + */ 276 + seek(event) { 277 + const target = event.target 278 + ? /** @type {HTMLProgressElement} */ (event.target) 279 + : null; 280 + const percentage = target ? event.offsetX / target.clientWidth : 0; 281 + const audioId = this.queue?.now()?.id; 214 282 215 - if (this.#artworkLightMode.value) { 216 - controller.classList.add("controller__inner--light-mode"); 217 - } else controller.classList.remove("controller__inner--light-mode"); 218 - }); 283 + if (audioId) this.audio?.seek({ audioId, percentage }); 219 284 } 220 285 221 286 // RENDER ··· 247 312 248 313 <div class="controller__background"></div> 249 314 250 - <!-- Content --> 251 - <section class="controller__inner"></section> 315 + <section class="controller__inner"> 316 + <!-- Now playing --> 317 + <cite> 318 + <strong>Diffuse</strong> 319 + <br /> 320 + <span></span> 321 + </cite> 322 + 323 + <!-- Progress --> 324 + <div class="progress"> 325 + <progress max="100" value="${0}"></progress> 326 + <div class="timestamps"> 327 + <time datetime="${this.#time.value}">${this.#time.value}</time> 328 + <time datetime="${this.#time.value}">${this.#duration 329 + .value}</time> 330 + </div> 331 + </div> 332 + 333 + <!-- Controls --> 334 + </section> 252 335 </section> 253 336 </main> 254 337 `;
+12 -1
src/themes/blur/artwork-controller/index.vto
··· 23 23 24 24 <script type="module"> 25 25 import { config } from "../../../common/constituents/default.js" 26 + import QueueAudioOrchestrator from "../../../components/orchestrator/queue-audio/element.js"; 26 27 27 28 import "../../../components/engine/audio/element.js" 28 29 import "../../../components/processor/artwork/element.js" 29 30 30 31 // Prepare default constituents setup 31 - config() 32 + const defaults = config() 32 33 33 34 // Only then initiate artwork controller 34 35 import("./element.js") 36 + 37 + // Orchestrators 38 + 39 + const oqa = new QueueAudioOrchestrator(); 40 + oqa.setAttribute("group", defaults.GROUP); 41 + oqa.setAttribute("input-selector", defaults.configurator.input.localName); 42 + oqa.setAttribute("audio-engine-selector", "de-audio"); 43 + oqa.setAttribute("queue-engine-selector", defaults.engine.queue.localName); 44 + 45 + document.body.append(oqa); 35 46 </script>