[v4] feat: add output configurator #1

merged
opened by tokono.ma targeting v4 from v4_dc-output
Changed files
+502 -272
src
common
constituents
components
configurator
output
engine
audio
orchestrator
queue-tracks
output
transformer
output
refiner
default
string
json
themes
blur
artwork-controller
webamp
+13 -3
src/common/constituents/default.js
··· 1 import InputConfigurator from "@components/configurator/input/element.js"; 2 import Queue from "@components/engine/queue/element.js"; 3 import OpenSubsonic from "@components/input/opensubsonic/element.js"; 4 import S3 from "@components/input/s3/element.js"; ··· 32 33 // Output 34 const idb = new IndexedDBOutput(); 35 const json = new JsonStringOutput(); 36 - json.setAttribute("output-selector", idb.localName); 37 38 const refiner = new DefaultRefiner(); 39 refiner.setAttribute("id", "output"); 40 - refiner.setAttribute("output-selector", json.localName); 41 42 - document.body.append(idb, json, refiner); 43 44 // Orchestrators 45 const oqt = new QueueTracksOrchestrator(); ··· 68 69 configurator: { 70 input, 71 }, 72 engine: { 73 queue,
··· 1 import InputConfigurator from "@components/configurator/input/element.js"; 2 + import OutputConfigurator from "@components/configurator/output/element.js"; 3 import Queue from "@components/engine/queue/element.js"; 4 import OpenSubsonic from "@components/input/opensubsonic/element.js"; 5 import S3 from "@components/input/s3/element.js"; ··· 33 34 // Output 35 const idb = new IndexedDBOutput(); 36 + idb.setAttribute("id", "idb-json-output") 37 + idb.setAttribute("namespace", "json") 38 + 39 const json = new JsonStringOutput(); 40 + json.setAttribute("id", "idb-json") 41 + json.setAttribute("output-selector", "#idb-json-output"); 42 + 43 + const output = new OutputConfigurator(); 44 + output.setAttribute("default", "idb-json"); 45 + output.append(json); 46 47 const refiner = new DefaultRefiner(); 48 refiner.setAttribute("id", "output"); 49 + refiner.setAttribute("output-selector", output.localName); 50 51 + document.body.append(idb, output, refiner); 52 53 // Orchestrators 54 const oqt = new QueueTracksOrchestrator(); ··· 77 78 configurator: { 79 input, 80 + output, 81 }, 82 engine: { 83 queue,
+11 -2
src/common/element.js
··· 20 * around rendering and managing signals. 21 */ 22 export class DiffuseElement extends HTMLElement { 23 #connected = Promise.withResolvers(); 24 #disposables = /** @type {Array<() => void>} */ ([]); 25 ··· 61 } 62 63 /** */ 64 - nameWithGroup() { 65 return `${this.constructor.prototype.constructor.NAME}/${this.group}`; 66 } 67 ··· 93 // LIFECYCLE 94 95 connectedCallback() { 96 this.#connected.resolve(null); 97 98 if (!("render" in this && typeof this.render === "function")) return; ··· 104 } 105 106 disconnectedCallback() { 107 this.#teardown(); 108 } 109 ··· 131 ); 132 133 // Setup worker 134 - const name = this.nameWithGroup(); 135 const url = import.meta.resolve("./" + WORKER_URL) + `?${query}`; 136 137 let worker;
··· 20 * around rendering and managing signals. 21 */ 22 export class DiffuseElement extends HTMLElement { 23 + $connected = signal(false) 24 + 25 #connected = Promise.withResolvers(); 26 #disposables = /** @type {Array<() => void>} */ ([]); 27 ··· 63 } 64 65 /** */ 66 + get label() { 67 + return this.getAttribute("label") ?? this.id ?? this.localName; 68 + } 69 + 70 + /** */ 71 + get nameWithGroup() { 72 return `${this.constructor.prototype.constructor.NAME}/${this.group}`; 73 } 74 ··· 100 // LIFECYCLE 101 102 connectedCallback() { 103 + this.$connected.value = true 104 this.#connected.resolve(null); 105 106 if (!("render" in this && typeof this.render === "function")) return; ··· 112 } 113 114 disconnectedCallback() { 115 + this.$connected.value = false 116 this.#teardown(); 117 } 118 ··· 140 ); 141 142 // Setup worker 143 + const name = this.nameWithGroup; 144 const url = import.meta.resolve("./" + WORKER_URL) + `?${query}`; 145 146 let worker;
+161
src/components/configurator/output/element.js
···
··· 1 + import { DiffuseElement } from "@common/element.js"; 2 + import { computed, signal } from "@common/signal.js"; 3 + 4 + /** 5 + * @import {Track} from "@definitions/types.d.ts" 6 + * @import {OutputManager, OutputElement} from "@components/output/types.d.ts" 7 + */ 8 + 9 + /** 10 + * @typedef {OutputElement<Track[]>} Output 11 + */ 12 + 13 + const STORAGE_PREFIX = "diffuse/configurator/output"; 14 + 15 + //////////////////////////////////////////// 16 + // ELEMENT 17 + //////////////////////////////////////////// 18 + 19 + /** 20 + * @implements {OutputManager<Track[]>} 21 + */ 22 + class OutputConfigurator extends DiffuseElement { 23 + static NAME = "diffuse/configurator/output"; 24 + 25 + constructor() { 26 + super(); 27 + 28 + /** @type {OutputManager<Track[]>} */ 29 + const manager = { 30 + tracks: { 31 + collection: computed(() => { 32 + const out = this.#selectedOutput.value; 33 + if (out) return out.tracks.collection(); 34 + return this.#memory.tracks.value; 35 + }), 36 + reload: () => { 37 + const out = this.#selectedOutput.value; 38 + if (out) return out.tracks.reload(); 39 + return Promise.resolve(); 40 + }, 41 + save: async (newTracks) => { 42 + const out = this.#selectedOutput.value; 43 + if (out) return await out.tracks.save(newTracks); 44 + this.#memory.tracks.value = newTracks; 45 + }, 46 + state: computed(() => { 47 + const out = this.#selectedOutput.value; 48 + if (out) return out.tracks.state(); 49 + return out === undefined ? "loading" : "loaded"; 50 + }), 51 + }, 52 + }; 53 + 54 + // Assign manager properties to class 55 + this.tracks = manager.tracks; 56 + } 57 + 58 + // SIGNALS 59 + 60 + #memory = { 61 + tracks: signal(/** @type {Track[]} */ ([])), 62 + }; 63 + 64 + #selectedOutput = signal( 65 + /** @type {Output | null | undefined} */ (undefined), 66 + ); 67 + 68 + // LIFECYCLE 69 + 70 + /** 71 + * @override 72 + */ 73 + async connectedCallback() { 74 + super.connectedCallback(); 75 + this.#selectedOutput.value = await this.#findSelectedOutput(); 76 + } 77 + 78 + // MISC 79 + 80 + async #findSelectedOutput() { 81 + const id = localStorage.getItem(`${STORAGE_PREFIX}/selected/id`) ?? 82 + this.getAttribute("default"); 83 + const el = id ? this.root().querySelector(`#${id}`) : null; 84 + 85 + if (!el) return null; 86 + 87 + await customElements.whenDefined(el.localName); 88 + 89 + if ( 90 + "nameWithGroup" in el === false || 91 + "tracks" in el === false 92 + ) { 93 + return null; 94 + } 95 + 96 + return /** @type {Output} */ (/** @type {unknown} */ (el)); 97 + } 98 + 99 + /** 100 + * @override 101 + */ 102 + dependencies() { 103 + return Object.fromEntries( 104 + Array.from(this.children).flatMap((element) => { 105 + if (element.hasAttribute("id") === false) { 106 + console.warn( 107 + "Missing `id` for output configurator child element with `localName` '" + 108 + element.localName + "'", 109 + ); 110 + return []; 111 + } 112 + 113 + const d = /** @type {DiffuseElement} */ (element); 114 + return [[d.id, d]]; 115 + }), 116 + ); 117 + } 118 + 119 + // ADDITIONAL ACTIONS 120 + 121 + async deselect() { 122 + localStorage.removeItem(`${STORAGE_PREFIX}/selected/id`); 123 + this.#selectedOutput.value = await this.#findSelectedOutput(); 124 + } 125 + 126 + async options() { 127 + const deps = this.dependencies(); 128 + const entries = Object.entries(deps); 129 + 130 + await Promise.all( 131 + entries.map(([_k, v]) => customElements.whenDefined(v.localName)), 132 + ); 133 + 134 + return entries.map(([k, v]) => { 135 + return { 136 + id: k, 137 + label: v.label, 138 + element: v, 139 + }; 140 + }); 141 + } 142 + 143 + /** 144 + * @param {string} id 145 + */ 146 + async select(id) { 147 + localStorage.setItem(`${STORAGE_PREFIX}/selected/id`, id); 148 + this.#selectedOutput.value = await this.#findSelectedOutput(); 149 + } 150 + } 151 + 152 + export default OutputConfigurator; 153 + 154 + //////////////////////////////////////////// 155 + // REGISTER 156 + //////////////////////////////////////////// 157 + 158 + export const CLASS = OutputConfigurator; 159 + export const NAME = "dc-output"; 160 + 161 + customElements.define(NAME, CLASS);
+2 -2
src/components/engine/audio/element.js
··· 51 // Setup broadcasting if part of group 52 if (this.hasAttribute("group")) { 53 const actions = this.broadcast( 54 - this.nameWithGroup(), 55 { 56 adjustVolume: { strategy: "replicate", fn: this.adjustVolume }, 57 pause: { strategy: "leaderOnly", fn: this.pause }, ··· 405 // Setup broadcasting if part of group 406 if (this.hasAttribute("group")) { 407 const actions = this.broadcast( 408 - this.nameWithGroup(), 409 { 410 getDuration: { strategy: "leaderOnly", fn: this.$state.duration.get }, 411 getHasEnded: { strategy: "leaderOnly", fn: this.$state.hasEnded.get },
··· 51 // Setup broadcasting if part of group 52 if (this.hasAttribute("group")) { 53 const actions = this.broadcast( 54 + this.nameWithGroup, 55 { 56 adjustVolume: { strategy: "replicate", fn: this.adjustVolume }, 57 pause: { strategy: "leaderOnly", fn: this.pause }, ··· 405 // Setup broadcasting if part of group 406 if (this.hasAttribute("group")) { 407 const actions = this.broadcast( 408 + this.nameWithGroup, 409 { 410 getDuration: { strategy: "leaderOnly", fn: this.$state.duration.get }, 411 getHasEnded: { strategy: "leaderOnly", fn: this.$state.hasEnded.get },
+1 -1
src/components/orchestrator/queue-tracks/element.js
··· 45 async connectedCallback() { 46 // Broadcast if needed 47 if (this.hasAttribute("group")) { 48 - this.broadcast(this.nameWithGroup(), {}); 49 } 50 51 // Super
··· 45 async connectedCallback() { 46 // Broadcast if needed 47 if (this.hasAttribute("group")) { 48 + this.broadcast(this.nameWithGroup, {}); 49 } 50 51 // Super
+2 -2
src/components/output/common.js
··· 1 - import { signal } from "@common/signal.js"; 2 3 /** 4 * @import {OutputManager, OutputManagerProperties} from "./types.d.ts" ··· 19 ts.value = "loaded"; 20 } 21 22 - loadTracks(); 23 24 return { 25 tracks: {
··· 1 + import { effect, signal } from "@common/signal.js"; 2 3 /** 4 * @import {OutputManager, OutputManagerProperties} from "./types.d.ts" ··· 19 ts.value = "loaded"; 20 } 21 22 + effect(loadTracks); 23 24 return { 25 tracks: {
+1 -1
src/components/output/polymorphic/indexed-db/constants.js
··· 1 - export const IDB_PREFIX = "@components/output/polymorphic/indexed-db";
··· 1 + export const IDB_PREFIX = "diffuse/output/polymorphic/indexed-db";
+17 -5
src/components/output/polymorphic/indexed-db/element.js
··· 4 /** 5 * @import {ProxiedActions} from "@common/worker.d.ts" 6 * @import {OutputManager, OutputWorkerActions} from "../../types.d.ts" 7 */ 8 9 //////////////////////////////////////////// ··· 20 constructor() { 21 super(); 22 23 - /** @type {ProxiedActions<OutputWorkerActions>} */ 24 const p = this.workerProxy(); 25 26 - // Manager 27 const manager = outputManager({ 28 tracks: { 29 - empty: () => [], 30 - get: p.getTracks, 31 - put: p.putTracks, 32 }, 33 }); 34 35 this.tracks = manager.tracks; 36 } 37 } 38
··· 4 /** 5 * @import {ProxiedActions} from "@common/worker.d.ts" 6 * @import {OutputManager, OutputWorkerActions} from "../../types.d.ts" 7 + * @import {SupportedDataTypes} from "./types.d.ts" 8 */ 9 10 //////////////////////////////////////////// ··· 21 constructor() { 22 super(); 23 24 + /** @type {ProxiedActions<OutputWorkerActions<SupportedDataTypes>>} */ 25 const p = this.workerProxy(); 26 27 + /** @type {OutputManager<SupportedDataTypes>} */ 28 const manager = outputManager({ 29 + init: this.whenConnected.bind(this), 30 tracks: { 31 + empty: () => undefined, 32 + get: () => p.get({ name: this.#cat("tracks") }), 33 + put: (data) => p.put({ name: this.#cat("tracks"), data }), 34 }, 35 }); 36 37 this.tracks = manager.tracks; 38 + } 39 + 40 + // 🛠️ 41 + 42 + /** @param {string} name */ 43 + #cat(name) { 44 + const namespace = this.hasAttribute("namespace") 45 + ? this.getAttribute("namespace") + "/" 46 + : ""; 47 + return `${namespace}${name}`; 48 } 49 } 50
+1
src/components/output/polymorphic/indexed-db/types.d.ts
···
··· 1 + export type SupportedDataTypes = any;
+10 -30
src/components/output/polymorphic/indexed-db/worker.js
··· 4 import { ostiary, rpc } from "@common/worker.js"; 5 6 /** 7 - * @import {Track} from "@definitions/types.d.ts"; 8 */ 9 10 //////////////////////////////////////////// ··· 12 //////////////////////////////////////////// 13 14 /** 15 - * @returns {Promise<Track[]>} 16 */ 17 - export async function getTracks() { 18 - /** @type {Track[] | null} */ 19 - const tracks = await get({ name: "tracks.json" }); 20 - return tracks ?? []; 21 } 22 23 /** 24 - * @param {Track[]} tracks 25 */ 26 - export async function putTracks(tracks) { 27 - await put({ name: "tracks.json", data: tracks }); 28 } 29 - 30 //////////////////////////////////////////// 31 // ⚡️ 32 //////////////////////////////////////////// 33 34 ostiary((context) => { 35 rpc(context, { 36 - getTracks, 37 - putTracks, 38 }); 39 }); 40 - 41 - //////////////////////////////////////////// 42 - // ⛔️ 43 - //////////////////////////////////////////// 44 - 45 - /** 46 - * @param {{ name: string }} _ 47 - */ 48 - async function get({ name }) { 49 - return await IDB.get(`${IDB_PREFIX}/${name}`); 50 - } 51 - 52 - /** 53 - * @param {{ data: any; name: string }} _ 54 - */ 55 - async function put({ data, name }) { 56 - return await IDB.set(`${IDB_PREFIX}/${name}`, data); 57 - }
··· 4 import { ostiary, rpc } from "@common/worker.js"; 5 6 /** 7 + * @import {OutputWorkerActions} from "@components/output/types.d.ts"; 8 + * @import {SupportedDataTypes} from "./types.d.ts" 9 */ 10 11 //////////////////////////////////////////// ··· 13 //////////////////////////////////////////// 14 15 /** 16 + * @type {OutputWorkerActions<SupportedDataTypes>["get"]} 17 */ 18 + export async function get({ name }) { 19 + return await IDB.get(`${IDB_PREFIX}/${name}`); 20 } 21 22 /** 23 + * @type {OutputWorkerActions<SupportedDataTypes>["put"]} 24 */ 25 + export async function put({ data, name }) { 26 + return await IDB.set(`${IDB_PREFIX}/${name}`, data); 27 } 28 //////////////////////////////////////////// 29 // ⚡️ 30 //////////////////////////////////////////// 31 32 ostiary((context) => { 33 rpc(context, { 34 + get, 35 + put, 36 }); 37 });
+3 -4
src/components/output/types.d.ts
··· 1 import type { SignalReader } from "@common/signal.d.ts"; 2 - import type { Track } from "@definitions/types.d.ts"; 3 import type { DiffuseElement } from "@common/element.js"; 4 5 export type OutputElement<Tracks> = DiffuseElement & OutputManager<Tracks>; ··· 22 }; 23 }; 24 25 - export type OutputWorkerActions = { 26 - getTracks(): Promise<Track[]>; 27 - putTracks(tracks: Track[]): Promise<void>; 28 };
··· 1 import type { SignalReader } from "@common/signal.d.ts"; 2 import type { DiffuseElement } from "@common/element.js"; 3 4 export type OutputElement<Tracks> = DiffuseElement & OutputManager<Tracks>; ··· 21 }; 22 }; 23 24 + export type OutputWorkerActions<DataType> = { 25 + get(args: { name: string }): Promise<DataType>; 26 + put(args: { data: DataType; name: string }): Promise<void>; 27 };
+65
src/components/transformer/output/base.js
···
··· 1 + import { DiffuseElement, query } from "@common/element.js"; 2 + import { computed, signal } from "@common/signal.js"; 3 + 4 + /** 5 + * @import { OutputElement, OutputManager } from "../../output/types.d.ts" 6 + */ 7 + 8 + /** 9 + * @template T 10 + */ 11 + export class OutputTransformer extends DiffuseElement { 12 + // SIGNALS 13 + 14 + #output = signal(/** @type {OutputElement<T> | undefined} */ (undefined)); 15 + #outputWhenDefined = Promise.withResolvers(); 16 + 17 + output = { 18 + whenDefined: this.#outputWhenDefined.promise, 19 + signal: this.#output.get, 20 + }; 21 + 22 + // LIFECYCLE 23 + 24 + /** 25 + * @override 26 + */ 27 + connectedCallback() { 28 + super.connectedCallback(); 29 + 30 + /** @type {OutputElement<T>} */ 31 + const output = query(this, "output-selector"); 32 + 33 + // When defined 34 + customElements.whenDefined(output.localName).then(() => { 35 + this.#output.value = output; 36 + this.#outputWhenDefined.resolve(null); 37 + }); 38 + } 39 + 40 + // MANAGER 41 + 42 + base() { 43 + /** @type {OutputManager<T | undefined>} */ 44 + const m = { 45 + tracks: { 46 + collection: computed(() => { 47 + return this.output.signal()?.tracks?.collection(); 48 + }), 49 + reload: () => { 50 + return this.output.signal()?.tracks?.reload() ?? Promise.resolve(); 51 + }, 52 + save: async (newTracks) => { 53 + if (newTracks === undefined) return; 54 + await this.output.whenDefined; 55 + await this.output.signal()?.tracks.save(newTracks); 56 + }, 57 + state: computed(() => { 58 + return this.output.signal()?.tracks.state() ?? "loading" 59 + }), 60 + }, 61 + }; 62 + 63 + return m; 64 + } 65 + }
+12 -39
src/components/transformer/output/refiner/default/element.js
··· 1 - import { DiffuseElement, query } from "@common/element.js"; 2 - import { computed, signal } from "@common/signal.js"; 3 4 /** 5 - * @import { OutputElement, OutputManager } from "../../../../output/types.d.ts" 6 * @import { Track } from "@definitions/types.d.ts" 7 */ 8 9 - class DefaultOutputRefinerTransformer extends DiffuseElement { 10 constructor() { 11 super(); 12 13 /** @type {OutputManager<Track[]>} */ 14 const manager = { 15 tracks: { 16 collection: computed(() => { 17 - return this.#defined.value 18 - ? this.output?.tracks?.collection() ?? [] 19 - : []; 20 }), 21 - reload: () => this.output?.tracks?.reload() ?? Promise.resolve(), 22 save: async (newTracks) => { 23 const filtered = newTracks.filter((t) => !t.ephemeral); 24 - 25 - if (!this.output) return; 26 - 27 - await customElements.whenDefined(this.output.localName); 28 - await this.output.tracks.save(filtered); 29 }, 30 - state: computed(() => this.output?.tracks.state() ?? "loading"), 31 }, 32 }; 33 34 // Assign manager properties to class 35 this.tracks = manager.tracks; 36 - } 37 - 38 - /** @type {OutputElement<Track[]> | undefined} */ 39 - output = undefined; 40 - 41 - // SIGNALS 42 - 43 - #defined = signal(false); 44 - 45 - // LIFECYCLE 46 - 47 - /** 48 - * @override 49 - */ 50 - connectedCallback() { 51 - super.connectedCallback(); 52 - 53 - /** @type {OutputElement<Track[]>} */ 54 - const output = query(this, "output-selector"); 55 - this.output = output; 56 - 57 - // When defined 58 - customElements.whenDefined(this.output.localName).then( 59 - () => this.#defined.value = true, 60 - ); 61 } 62 } 63
··· 1 + import { computed } from "@common/signal.js"; 2 + import { OutputTransformer } from "../../base.js"; 3 4 /** 5 + * @import { OutputManager } from "../../../../output/types.d.ts" 6 * @import { Track } from "@definitions/types.d.ts" 7 */ 8 9 + /** 10 + * @extends {OutputTransformer<Track[]>} 11 + */ 12 + class DefaultOutputRefinerTransformer extends OutputTransformer { 13 constructor() { 14 super(); 15 + 16 + const base = this.base(); 17 18 /** @type {OutputManager<Track[]>} */ 19 const manager = { 20 tracks: { 21 + ...base.tracks, 22 collection: computed(() => { 23 + return base.tracks.collection() ?? []; 24 }), 25 save: async (newTracks) => { 26 const filtered = newTracks.filter((t) => !t.ephemeral); 27 + await base.tracks.save(filtered); 28 }, 29 }, 30 }; 31 32 // Assign manager properties to class 33 this.tracks = manager.tracks; 34 } 35 } 36
+13 -43
src/components/transformer/output/string/json/element.js
··· 1 - import { DiffuseElement, query } from "@common/element.js"; 2 - import { computed, signal } from "@common/signal.js"; 3 4 /** 5 - * @import { OutputElement, OutputManager } from "../../../../output/types.d.ts" 6 * @import { Track } from "@definitions/types.d.ts" 7 */ 8 9 - class JsonStringOutputTransformer extends DiffuseElement { 10 constructor() { 11 super(); 12 13 /** @type {OutputManager<Track[]>} */ 14 const manager = { 15 tracks: { 16 collection: computed(() => { 17 - const json = this.#defined.value 18 - ? this.output?.tracks?.collection() ?? [] 19 - : []; 20 - 21 - // In addition to the above, Some polymorphic outputs 22 - // use an empty array as the default return value. 23 - if (Array.isArray(json)) return json; 24 25 // Try parsing JSON 26 try { ··· 32 return []; 33 } 34 }), 35 - reload: () => this.output?.tracks?.reload() ?? Promise.resolve(), 36 save: async (newTracks) => { 37 const json = JSON.stringify(newTracks); 38 - 39 - if (!this.output) return; 40 - 41 - await customElements.whenDefined(this.output.localName); 42 - await this.output.tracks.save(json); 43 }, 44 - state: computed(() => this.output?.tracks?.state() ?? "loading"), 45 }, 46 }; 47 48 // Assign manager properties to class 49 this.tracks = manager.tracks; 50 - } 51 - 52 - /** @type {OutputElement<string> | undefined} */ 53 - output = undefined; 54 - 55 - // SIGNALS 56 - 57 - #defined = signal(false); 58 - 59 - // LIFECYCLE 60 - 61 - /** 62 - * @override 63 - */ 64 - connectedCallback() { 65 - super.connectedCallback(); 66 - 67 - /** @type {OutputElement<string>} */ 68 - const output = query(this, "output-selector"); 69 - this.output = output; 70 - 71 - // When defined 72 - customElements.whenDefined(this.output.localName).then( 73 - () => this.#defined.value = true, 74 - ); 75 } 76 } 77
··· 1 + import { computed } from "@common/signal.js"; 2 + import { OutputTransformer } from "../../base.js"; 3 4 /** 5 + * @import { OutputManager } from "../../../../output/types.d.ts" 6 * @import { Track } from "@definitions/types.d.ts" 7 */ 8 9 + /** 10 + * @extends {OutputTransformer<string>} 11 + */ 12 + class JsonStringOutputTransformer extends OutputTransformer { 13 constructor() { 14 super(); 15 16 + const base = this.base(); 17 + 18 /** @type {OutputManager<Track[]>} */ 19 const manager = { 20 tracks: { 21 + ...base.tracks, 22 collection: computed(() => { 23 + let json = base.tracks.collection(); 24 + if (typeof json !== "string") json = "[]" 25 26 // Try parsing JSON 27 try { ··· 33 return []; 34 } 35 }), 36 save: async (newTracks) => { 37 const json = JSON.stringify(newTracks); 38 + await base.tracks.save(json); 39 }, 40 }, 41 }; 42 43 // Assign manager properties to class 44 this.tracks = manager.tracks; 45 } 46 } 47
+2 -3
src/index.vto
··· 23 configurators: 24 - url: "components/configurator/input/element.js" 25 title: "Input" 26 - desc: "Add multiple inputs." 27 - url: "components/configurator/output/element.js" 28 title: "Output" 29 - desc: "Allows the user to configure a specific output." 30 - todo: true 31 - url: "components/configurator/scrobbles/element.js" 32 title: "Scrobbles" 33 desc: "Configure multiple scrobblers (music trackers)."
··· 23 configurators: 24 - url: "components/configurator/input/element.js" 25 title: "Input" 26 + desc: "Allows for multiple inputs to be used at once." 27 - url: "components/configurator/output/element.js" 28 title: "Output" 29 + desc: "Enables the user to configure a specific output. If no default output is set, it creates a temporary session by storing everything in memory." 30 - url: "components/configurator/scrobbles/element.js" 31 title: "Scrobbles" 32 desc: "Configure multiple scrobblers (music trackers)."
+3
src/themes/blur/artwork-controller/element.css
··· 1 :host { 2 --transition-durition: 750ms; 3 }
··· 1 + @import "../../../styles/vendor/phosphor/fill/style.css"; 2 + @import "../../../styles/animations.css"; 3 + 4 :host { 5 --transition-durition: 750ms; 6 }
+2 -5
src/themes/blur/artwork-controller/element.js
··· 121 122 this.effect(() => { 123 const now = !!queue.now(); 124 - const bool = !now || 125 - (now && this.#audio()?.loadingState() !== "loaded"); 126 127 if (this.#isLoadingTimeout) { 128 clearTimeout(this.#isLoadingTimeout); ··· 398 399 return html` 400 <style> 401 - @import "../../../styles/vendor/phosphor/fill/style.css"; 402 - @import "../../../styles/animations.css"; 403 - @import "./element.css"; 404 </style> 405 406 <main style="background-color: ${this.#artworkColor.value ??
··· 121 122 this.effect(() => { 123 const now = !!queue.now(); 124 + const bool = (now && this.#audio()?.loadingState() !== "loaded"); 125 126 if (this.#isLoadingTimeout) { 127 clearTimeout(this.#isLoadingTimeout); ··· 397 398 return html` 399 <style> 400 + @import "${import.meta.resolve('./element.css')}"; 401 </style> 402 403 <main style="background-color: ${this.#artworkColor.value ??
+7 -7
src/themes/blur/artwork-controller/index.vto
··· 3 base: ../../../ 4 5 styles: 6 - - ../../../styles/vendor/phosphor/fill/style.css 7 - - ../../../styles/base.css 8 --- 9 10 <!-- ELEMENTS --> ··· 23 <!-- SCRIPTS --> 24 25 <script type="module"> 26 - import { config } from "../../../common/constituents/default.js" 27 - import QueueAudioOrchestrator from "../../../components/orchestrator/queue-audio/element.js"; 28 29 - import "../../../components/engine/audio/element.js" 30 - import "../../../components/processor/artwork/element.js" 31 32 // Prepare default constituents setup 33 const defaults = config() 34 35 // Only then initiate artwork controller 36 - import("./element.js") 37 38 // Orchestrators 39
··· 3 base: ../../../ 4 5 styles: 6 + - styles/vendor/phosphor/fill/style.css 7 + - styles/base.css 8 --- 9 10 <!-- ELEMENTS --> ··· 23 <!-- SCRIPTS --> 24 25 <script type="module"> 26 + import { config } from "./common/constituents/default.js" 27 + import QueueAudioOrchestrator from "./components/orchestrator/queue-audio/element.js"; 28 29 + import "./components/engine/audio/element.js" 30 + import "./components/processor/artwork/element.js" 31 32 // Prepare default constituents setup 33 const defaults = config() 34 35 // Only then initiate artwork controller 36 + import("./themes/blur/artwork-controller/element.js") 37 38 // Orchestrators 39
+4 -1
src/themes/webamp/browser/element.js
··· 98 ***********************************/ 99 100 .sunken-panel { 101 - content-visibility: auto; 102 height: 30dvh; 103 min-height: 80px; 104 resize: both; ··· 116 &:first-child { 117 width: 40%; 118 } 119 } 120 121 table td {
··· 98 ***********************************/ 99 100 .sunken-panel { 101 height: 30dvh; 102 min-height: 80px; 103 resize: both; ··· 115 &:first-child { 116 width: 40%; 117 } 118 + } 119 + 120 + table tbody tr { 121 + content-visibility: auto; 122 } 123 124 table td {
-61
src/themes/webamp/index.css
··· 82 } 83 } 84 } 85 - 86 - /*********************************** 87 - * Windows 88 - ***********************************/ 89 - 90 - .windows dtw-window { 91 - left: 12px; 92 - position: absolute; 93 - top: 12px; 94 - z-index: 999; 95 - 96 - /* Waiting on https://developer.mozilla.org/en-US/docs/Web/CSS/sibling-index#browser_compatibility */ 97 - &:nth-child(1) { 98 - left: 24px; 99 - top: 24px; 100 - } 101 - 102 - &:nth-child(2) { 103 - left: 36px; 104 - top: 36px; 105 - } 106 - 107 - &:nth-child(3) { 108 - left: 48px; 109 - top: 48px; 110 - } 111 - 112 - &:nth-child(4) { 113 - left: 60px; 114 - top: 60px; 115 - } 116 - 117 - &:nth-child(5) { 118 - left: 72px; 119 - top: 72px; 120 - } 121 - 122 - &:nth-child(6) { 123 - left: 84px; 124 - top: 84px; 125 - } 126 - 127 - &:nth-child(7) { 128 - left: 96px; 129 - top: 96px; 130 - } 131 - 132 - &:nth-child(8) { 133 - left: 108px; 134 - top: 108px; 135 - } 136 - 137 - &:nth-child(9) { 138 - left: 120px; 139 - top: 120px; 140 - } 141 - } 142 - 143 - .windows section { 144 - z-index: 999; 145 - }
··· 82 } 83 } 84 }
+16 -6
src/themes/webamp/index.js
··· 1 - // import "@components/orchestrator/process-tracks/element.js"; 2 - import "@components/orchestrator/queue-tracks/element.js"; 3 import "@components/input/opensubsonic/element.js"; 4 import "@components/input/s3/element.js"; 5 import "@components/output/polymorphic/indexed-db/element.js"; 6 import "@components/processor/metadata/element.js"; 7 import "@components/transformer/output/string/json/element.js"; ··· 15 16 import "./browser/element.js"; 17 import "./window/element.js"; 18 - import "./window-manager/element.js"; 19 import WebampElement from "./webamp/element.js"; 20 21 const input = component(Input); 22 const queue = component(Queue); 23 24 globalThis.queue = queue; 25 26 //////////////////////////////////////////// 27 // 📡 ··· 155 if (element instanceof HTMLElement) { 156 element.addEventListener("dblclick", () => { 157 const f = element.querySelector("label")?.getAttribute("for"); 158 - if (f) { 159 - document.body.querySelector(`dtw-window#${f}`)?.toggleAttribute("open"); 160 - } 161 }); 162 } 163 }); ··· 180 181 // TODO: 182 // amp.onMinimize(() => amp.close());
··· 1 + import "@components/configurator/output/element.js"; 2 import "@components/input/opensubsonic/element.js"; 3 import "@components/input/s3/element.js"; 4 + import "@components/orchestrator/process-tracks/element.js"; 5 + import "@components/orchestrator/queue-tracks/element.js"; 6 import "@components/output/polymorphic/indexed-db/element.js"; 7 import "@components/processor/metadata/element.js"; 8 import "@components/transformer/output/string/json/element.js"; ··· 16 17 import "./browser/element.js"; 18 import "./window/element.js"; 19 + import WindowManager from "./window-manager/element.js"; 20 import WebampElement from "./webamp/element.js"; 21 22 const input = component(Input); 23 const queue = component(Queue); 24 25 globalThis.queue = queue; 26 + globalThis.output = document.querySelector("#output"); 27 28 //////////////////////////////////////////// 29 // 📡 ··· 157 if (element instanceof HTMLElement) { 158 element.addEventListener("dblclick", () => { 159 const f = element.querySelector("label")?.getAttribute("for"); 160 + if (f) windowManager()?.toggleWindow(f); 161 }); 162 } 163 }); ··· 180 181 // TODO: 182 // amp.onMinimize(() => amp.close()); 183 + 184 + //////////////////////////////////////////// 185 + // 🛠️ 186 + //////////////////////////////////////////// 187 + 188 + function windowManager() { 189 + const w = document.body.querySelector("dtw-window-manager"); 190 + if (w instanceof WindowManager) return w; 191 + return null; 192 + }
+21 -32
src/themes/webamp/index.vto
··· 10 <body> 11 <!-- 12 13 - UI 14 15 --> 16 <main> 17 <section class="windows"> 18 - <dtw-window-manager> 19 - <!-- INPUT --> 20 - <dtw-window id="input-window"> 21 - <span slot="title-icon"><img src="../../images/icons/windows_98/cd_audio_cd_a-0.png" height="14" /></span> 22 - <span slot="title">Manage audio inputs</span> 23 - <p>👀</p> 24 - </dtw-window> 25 26 - <!-- OUTPUT --> 27 - <dtw-window id="output-window"> 28 - <span slot="title-icon"><img src="../../images/icons/windows_98/computer_user_pencil-0.png" height="14" /></span> 29 - <span slot="title">Manage user data</span> 30 - <p>👀</p> 31 - </dtw-window> 32 33 - <!-- BROWSER --> 34 - <dtw-window id="browser-window" open> 35 - <span slot="title-icon"><img src="../../images/icons/windows_98/directory_explorer-4.png" height="14" /></span> 36 - <span slot="title">Browse collection</span> 37 - <dtw-browser 38 - input-selector="#input" 39 - output-selector="#output" 40 - queue-engine-selector="de-queue" 41 - ></dtw-browser> 42 - </dtw-window> 43 - </dtw-window-manager> 44 - </section> 45 <section class="desktop"> 46 <!-- WINAMP --> 47 <a class="button desktop__item" id="desktop-winamp"> ··· 67 <label for="browser-window">Browse collection</label> 68 </a> 69 </section> 70 <dtw-webamp></dtw-webamp> 71 </main> 72 73 <!-- 74 75 - COMPONENTS 76 77 --> 78 <de-queue></de-queue> 79 80 - <!-- Inputs, Output & Processors --> 81 - <dop-indexed-db></dop-indexed-db> 82 <dp-metadata></dp-metadata> 83 84 <dc-input id="input"> 85 <di-opensubsonic></di-opensubsonic> 86 <di-s3></di-s3> 87 </dc-input> 88 89 - <!-- Transformers --> 90 - <dtor-default id="output" output-selector="dtos-json"></dtor-default> 91 - <dtos-json output-selector="dop-indexed-db"></dtos-json> 92 93 <!-- Orchestrators --> 94 <do-process-tracks
··· 10 <body> 11 <!-- 12 13 + ################################### 14 + # UI 15 + ################################### 16 17 --> 18 <main> 19 + <!-- 🪟 --> 20 <section class="windows"> 21 + <dtw-window-manager></dtw-window-manager> 22 + </section> 23 24 25 + <!-- 🛋️ --> 26 <section class="desktop"> 27 <!-- WINAMP --> 28 <a class="button desktop__item" id="desktop-winamp"> ··· 48 <label for="browser-window">Browse collection</label> 49 </a> 50 </section> 51 + <!-- ⚡️ --> 52 <dtw-webamp></dtw-webamp> 53 </main> 54 55 <!-- 56 57 + ################################### 58 + # COMPONENTS 59 + ################################### 60 61 --> 62 <de-queue></de-queue> 63 64 + <!-- Processors --> 65 <dp-metadata></dp-metadata> 66 67 + <!-- Input --> 68 <dc-input id="input"> 69 <di-opensubsonic></di-opensubsonic> 70 <di-s3></di-s3> 71 </dc-input> 72 73 + <!-- Output --> 74 + <dop-indexed-db id="idb-json-output" namespace="json"></dop-indexed-db> 75 + 76 + <dc-output default="idb-json"> 77 + <dtos-json id="idb-json" output-selector="#idb-json-output"></dtos-json> 78 + </dc-output> 79 + 80 + <dtor-default id="output" output-selector="dc-output"></dtor-default> 81 82 <!-- Orchestrators --> 83 <do-process-tracks
+2 -1
src/themes/webamp/window/element.js
··· 66 <div class="window"> 67 <div 68 class="title-bar" 69 - @mousedown="${this.titleBarMouseDown}" 70 > 71 <div class="title-bar-icon"> 72 <slot name="title-icon"></slot> ··· 99 bubbles: true, 100 composed: true, 101 detail: { 102 x: mouse.x, 103 xElement: mouse.layerX, 104 y: mouse.y,
··· 66 <div class="window"> 67 <div 68 class="title-bar" 69 + @mousedown="${this.titleBarMouseDown.bind(this)}" 70 > 71 <div class="title-bar-icon"> 72 <slot name="title-icon"></slot> ··· 99 bubbles: true, 100 composed: true, 101 detail: { 102 + element: this, 103 x: mouse.x, 104 xElement: mouse.layerX, 105 y: mouse.y,
+133 -24
src/themes/webamp/window-manager/element.js
··· 2 import { signal } from "@common/signal.js"; 3 import { debounceMicrotask } from "@vicary/debounce-microtask"; 4 5 /** 6 * @import {RenderArg} from "@common/element.d.ts" 7 - * @import WindowElement from "../window/element.js"; 8 */ 9 10 //////////////////////////////////////////// ··· 15 constructor() { 16 super(); 17 this.attachShadow({ mode: "open" }); 18 } 19 20 // SIGNALS ··· 27 /** 28 * @override 29 */ 30 - connectedCallback() { 31 super.connectedCallback(); 32 33 // Events 34 - this.addEventListener("mousedown", this.focusOnWindow); 35 - this.addEventListener("dtw-window-start-move", this.windowMoveStart); 36 37 // Webamp stuff 38 document.body.addEventListener( ··· 53 disconnectedCallback() { 54 super.disconnectedCallback(); 55 56 - this.removeEventListener("mousedown", this.focusOnWindow); 57 - this.removeEventListener("dtw-window-start-move", this.windowMoveStart); 58 59 document.body.removeEventListener( 60 "mousedown", ··· 94 */ 95 async setWindowStatuses(activeId) { 96 await customElements.whenDefined("dtw-window"); 97 - 98 - this.querySelectorAll("dtw-window").forEach( 99 - (window) => { 100 - const win = /** @type {WindowElement} */ (window); 101 - 102 - if (activeId && window.id === activeId) { 103 - win.activate(); 104 - } else { 105 - win.deactivate(); 106 - } 107 - }, 108 - ); 109 } 110 111 /** ··· 119 if (event instanceof MouseEvent) { 120 const x = event.x - ogEvent.detail.xElement; 121 const y = event.y - ogEvent.detail.yElement; 122 - const target = ogEvent.target; 123 124 if (target) { 125 target.style.left = `${x}px`; ··· 131 }); 132 133 const stopMove = () => { 134 - this.removeEventListener("mousemove", moveFn); 135 - 136 document.removeEventListener("mouseup", stopMove); 137 document.removeEventListener("mouseleave", stopMove); 138 }; 139 140 - this.addEventListener("mousemove", moveFn); 141 - 142 document.addEventListener("mouseup", stopMove); 143 document.addEventListener("mouseleave", stopMove); 144 } 145 146 // RENDER 147 148 /** ··· 150 */ 151 render({ html }) { 152 return html` 153 <style> 154 :host { 155 user-select: none; 156 } 157 </style> 158 159 - <slot></slot> 160 `; 161 } 162 }
··· 2 import { signal } from "@common/signal.js"; 3 import { debounceMicrotask } from "@vicary/debounce-microtask"; 4 5 + import WindowElement from "../window/element.js" 6 + 7 /** 8 * @import {RenderArg} from "@common/element.d.ts" 9 */ 10 11 //////////////////////////////////////////// ··· 16 constructor() { 17 super(); 18 this.attachShadow({ mode: "open" }); 19 + 20 + this.focusOnWindow = this.focusOnWindow.bind(this) 21 + this.windowMoveStart = this.windowMoveStart.bind(this) 22 } 23 24 // SIGNALS ··· 31 /** 32 * @override 33 */ 34 + async connectedCallback() { 35 super.connectedCallback(); 36 37 // Events 38 + this.root().addEventListener("mousedown", this.focusOnWindow); 39 + this.root().addEventListener("dtw-window-start-move", this.windowMoveStart); 40 41 // Webamp stuff 42 document.body.addEventListener( ··· 57 disconnectedCallback() { 58 super.disconnectedCallback(); 59 60 + this.root().removeEventListener("mousedown", this.focusOnWindow); 61 + this.root().removeEventListener("dtw-window-start-move", this.windowMoveStart); 62 63 document.body.removeEventListener( 64 "mousedown", ··· 98 */ 99 async setWindowStatuses(activeId) { 100 await customElements.whenDefined("dtw-window"); 101 + this.activateWindow(activeId) 102 } 103 104 /** ··· 112 if (event instanceof MouseEvent) { 113 const x = event.x - ogEvent.detail.xElement; 114 const y = event.y - ogEvent.detail.yElement; 115 + const target = ogEvent.detail.element; 116 117 if (target) { 118 target.style.left = `${x}px`; ··· 124 }); 125 126 const stopMove = () => { 127 + document.removeEventListener("mousemove", moveFn); 128 document.removeEventListener("mouseup", stopMove); 129 document.removeEventListener("mouseleave", stopMove); 130 }; 131 132 + document.addEventListener("mousemove", moveFn); 133 document.addEventListener("mouseup", stopMove); 134 document.addEventListener("mouseleave", stopMove); 135 } 136 137 + // ACTIONS 138 + 139 + /** 140 + * @param {string} id 141 + */ 142 + activateWindow(id) { 143 + this.querySelectorAll("dtw-window").forEach(w => { 144 + if (w instanceof WindowElement === false) return 145 + 146 + if (activeId && w.id === activeId) { 147 + w.activate(); 148 + } else { 149 + w.deactivate(); 150 + } 151 + }) 152 + } 153 + 154 + /** 155 + * @param {string} id 156 + */ 157 + toggleWindow(id) { 158 + const w = this.root().querySelector(`dtw-window#${id}`) 159 + if (w instanceof WindowElement === false) return 160 + 161 + w.toggleAttribute("open") 162 + 163 + if (w.hasAttribute("open")) { 164 + this.activateWindow(id) 165 + this.#lastZindex++; 166 + w.style.zIndex = this.#lastZindex.toString(); 167 + } 168 + } 169 + 170 // RENDER 171 172 /** ··· 174 */ 175 render({ html }) { 176 return html` 177 + <link rel="stylesheet" href="../../styles/vendor/98.css" /> 178 + 179 <style> 180 :host { 181 user-select: none; 182 } 183 + 184 + dtw-window { 185 + left: 12px; 186 + position: absolute; 187 + top: 12px; 188 + z-index: 999; 189 + 190 + /* Waiting on https://developer.mozilla.org/en-US/docs/Web/CSS/sibling-index#browser_compatibility */ 191 + &:nth-child(1) { 192 + left: 24px; 193 + top: 24px; 194 + } 195 + 196 + &:nth-child(2) { 197 + left: 36px; 198 + top: 36px; 199 + } 200 + 201 + &:nth-child(3) { 202 + left: 48px; 203 + top: 48px; 204 + } 205 + 206 + &:nth-child(4) { 207 + left: 60px; 208 + top: 60px; 209 + } 210 + 211 + &:nth-child(5) { 212 + left: 72px; 213 + top: 72px; 214 + } 215 + 216 + &:nth-child(6) { 217 + left: 84px; 218 + top: 84px; 219 + } 220 + 221 + &:nth-child(7) { 222 + left: 96px; 223 + top: 96px; 224 + } 225 + 226 + &:nth-child(8) { 227 + left: 108px; 228 + top: 108px; 229 + } 230 + 231 + &:nth-child(9) { 232 + left: 120px; 233 + top: 120px; 234 + } 235 + } 236 </style> 237 238 + <!-- INPUT --> 239 + <dtw-window id="input-window"> 240 + <span slot="title-icon"><img src="../../images/icons/windows_98/cd_audio_cd_a-0.png" height="14" /></span> 241 + <span slot="title">Manage audio inputs</span> 242 + <p>👀</p> 243 + </dtw-window> 244 + 245 + <!-- OUTPUT --> 246 + <dtw-window id="output-window"> 247 + <span slot="title-icon"><img src="../../images/icons/windows_98/computer_user_pencil-0.png" height="14" /></span> 248 + <span slot="title">Manage user data</span> 249 + 250 + <form> 251 + <p>Where do you want to keep your data?</p> 252 + <div class="field-row"> 253 + <input id="idb-json" type="radio" checked /> 254 + <label for="idb-json">Local only</label> 255 + </div> 256 + </form> 257 + </dtw-window> 258 + 259 + <!-- BROWSER --> 260 + <dtw-window id="browser-window" open> 261 + <span slot="title-icon"><img src="../../images/icons/windows_98/directory_explorer-4.png" height="14" /></span> 262 + <span slot="title">Browse collection</span> 263 + <dtw-browser 264 + input-selector="#input" 265 + output-selector="#output" 266 + queue-engine-selector="de-queue" 267 + ></dtw-browser> 268 + </dtw-window> 269 `; 270 } 271 }