From 3f7539563437cf2d4ac3092a1f1d1653a10c8cd4 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Sat, 20 Dec 2025 23:35:14 +0100 Subject: [PATCH] wip: output configurator --- src/components/configurator/output/element.js | 67 +++++++++++++++++++ .../output/polymorphic/indexed-db/element.js | 17 +++-- .../output/polymorphic/indexed-db/types.d.ts | 1 + .../output/polymorphic/indexed-db/worker.js | 40 +++-------- src/components/output/types.d.ts | 7 +- src/components/transformer/output/base.js | 65 ++++++++++++++++++ .../output/refiner/default/element.js | 51 ++++---------- .../transformer/output/string/json/element.js | 55 ++++----------- src/themes/webamp/index.js | 2 +- 9 files changed, 183 insertions(+), 122 deletions(-) create mode 100644 src/components/configurator/output/element.js create mode 100644 src/components/output/polymorphic/indexed-db/types.d.ts create mode 100644 src/components/transformer/output/base.js diff --git a/src/components/configurator/output/element.js b/src/components/configurator/output/element.js new file mode 100644 index 00000000..70551c15 --- /dev/null +++ b/src/components/configurator/output/element.js @@ -0,0 +1,67 @@ +import { DiffuseElement } from "@common/element.js"; +import { computed, signal } from "@common/signal.js"; + +/** + * @import {ProxiedActions} from "@common/worker.d.ts" + * @import {Track} from "@definitions/types.d.ts" + * @import {OutputManager, OutputElement} from "@components/output/types.d.ts" + */ + +//////////////////////////////////////////// +// ELEMENT +//////////////////////////////////////////// + +/** + * @implements {OutputManager} + */ +class OutputConfigurator extends DiffuseElement { + static NAME = "diffuse/configurator/output"; + static WORKER_URL = "components/configurator/output/worker.js"; + + constructor() { + super(); + + /** @type {OutputManager} */ + const manager = { + tracks: { + collection: computed(() => { + return this.#memory.tracks.value; + }), + reload: async () => {}, + save: async (newTracks) => { + this.#memory.tracks.value = newTracks; + }, + state: () => "loaded", + }, + }; + + // Assign manager properties to class + this.tracks = manager.tracks; + } + + // SIGNALS + + #memory = { + tracks: signal(/** @type {Track[]} */ ([])), + }; + + // LIFECYCLE + + /** + * @override + */ + async connectedCallback() { + super.connectedCallback(); + } +} + +export default OutputConfigurator; + +//////////////////////////////////////////// +// REGISTER +//////////////////////////////////////////// + +export const CLASS = OutputConfigurator; +export const NAME = "dc-output"; + +customElements.define(NAME, CLASS); diff --git a/src/components/output/polymorphic/indexed-db/element.js b/src/components/output/polymorphic/indexed-db/element.js index 5c2c2128..e99e2b3e 100644 --- a/src/components/output/polymorphic/indexed-db/element.js +++ b/src/components/output/polymorphic/indexed-db/element.js @@ -4,6 +4,7 @@ import { outputManager } from "../../common.js"; /** * @import {ProxiedActions} from "@common/worker.d.ts" * @import {OutputManager, OutputWorkerActions} from "../../types.d.ts" + * @import {SupportedDataTypes} from "./types.d.ts" */ //////////////////////////////////////////// @@ -20,20 +21,26 @@ class IndexedDBOutput extends DiffuseElement { constructor() { super(); - /** @type {ProxiedActions} */ + /** @type {ProxiedActions>} */ const p = this.workerProxy(); - // Manager + /** @type {OutputManager} */ const manager = outputManager({ tracks: { - empty: () => [], - get: p.getTracks, - put: p.putTracks, + empty: () => undefined, + get: () => p.get({ name: this.#cat("tracks") }), + put: (data) => p.put({ name: this.#cat("tracks"), data }), }, }); this.tracks = manager.tracks; } + + /** @param {string} name */ + #cat(name) { + const key = this.hasAttribute("key") ? this.getAttribute("key") + "/" : ""; + return `${key}${name}`; + } } export default IndexedDBOutput; diff --git a/src/components/output/polymorphic/indexed-db/types.d.ts b/src/components/output/polymorphic/indexed-db/types.d.ts new file mode 100644 index 00000000..0c35ba7e --- /dev/null +++ b/src/components/output/polymorphic/indexed-db/types.d.ts @@ -0,0 +1 @@ +export type SupportedDataTypes = any; diff --git a/src/components/output/polymorphic/indexed-db/worker.js b/src/components/output/polymorphic/indexed-db/worker.js index e6b21440..2c2b0386 100644 --- a/src/components/output/polymorphic/indexed-db/worker.js +++ b/src/components/output/polymorphic/indexed-db/worker.js @@ -4,7 +4,8 @@ import { IDB_PREFIX } from "./constants.js"; import { ostiary, rpc } from "@common/worker.js"; /** - * @import {Track} from "@definitions/types.d.ts"; + * @import {OutputWorkerActions} from "@components/output/types.d.ts"; + * @import {SupportedDataTypes} from "./types.d.ts" */ //////////////////////////////////////////// @@ -12,46 +13,25 @@ import { ostiary, rpc } from "@common/worker.js"; //////////////////////////////////////////// /** - * @returns {Promise} + * @type {OutputWorkerActions["get"]} */ -export async function getTracks() { - /** @type {Track[] | null} */ - const tracks = await get({ name: "tracks.json" }); - return tracks ?? []; +export async function get({ name }) { + return await IDB.get(`${IDB_PREFIX}/${name}`); } /** - * @param {Track[]} tracks + * @type {OutputWorkerActions["put"]} */ -export async function putTracks(tracks) { - await put({ name: "tracks.json", data: tracks }); +export async function put({ data, name }) { + return await IDB.set(`${IDB_PREFIX}/${name}`, data); } - //////////////////////////////////////////// // ⚡️ //////////////////////////////////////////// ostiary((context) => { rpc(context, { - getTracks, - putTracks, + get, + put, }); }); - -//////////////////////////////////////////// -// ⛔️ -//////////////////////////////////////////// - -/** - * @param {{ name: string }} _ - */ -async function get({ name }) { - return await IDB.get(`${IDB_PREFIX}/${name}`); -} - -/** - * @param {{ data: any; name: string }} _ - */ -async function put({ data, name }) { - return await IDB.set(`${IDB_PREFIX}/${name}`, data); -} diff --git a/src/components/output/types.d.ts b/src/components/output/types.d.ts index d46bb8ae..5f7bf279 100644 --- a/src/components/output/types.d.ts +++ b/src/components/output/types.d.ts @@ -1,5 +1,4 @@ import type { SignalReader } from "@common/signal.d.ts"; -import type { Track } from "@definitions/types.d.ts"; import type { DiffuseElement } from "@common/element.js"; export type OutputElement = DiffuseElement & OutputManager; @@ -22,7 +21,7 @@ export type OutputManagerProperties = { }; }; -export type OutputWorkerActions = { - getTracks(): Promise; - putTracks(tracks: Track[]): Promise; +export type OutputWorkerActions = { + get(args: { name: string }): Promise; + put(args: { data: DataType; name: string }): Promise; }; diff --git a/src/components/transformer/output/base.js b/src/components/transformer/output/base.js new file mode 100644 index 00000000..9de69c6a --- /dev/null +++ b/src/components/transformer/output/base.js @@ -0,0 +1,65 @@ +import { DiffuseElement, query } from "@common/element.js"; +import { computed, signal } from "@common/signal.js"; + +/** + * @import { OutputElement, OutputManager } from "../../output/types.d.ts" + */ + +/** + * @template T + */ +export class OutputTransformer extends DiffuseElement { + // SIGNALS + + #output = signal(/** @type {OutputElement | undefined} */ (undefined)); + #outputWhenDefined = Promise.withResolvers(); + + output = { + whenDefined: this.#outputWhenDefined.promise, + signal: this.#output.get, + }; + + // LIFECYCLE + + /** + * @override + */ + connectedCallback() { + super.connectedCallback(); + + /** @type {OutputElement} */ + const output = query(this, "output-selector"); + + // When defined + customElements.whenDefined(output.localName).then(() => { + this.#output.value = output; + this.#outputWhenDefined.resolve(null); + }); + } + + // MANAGER + + base() { + /** @type {OutputManager} */ + const m = { + tracks: { + collection: computed(() => { + return this.output.signal()?.tracks?.collection(); + }), + reload: () => { + return this.output.signal()?.tracks?.reload() ?? Promise.resolve(); + }, + save: async (newTracks) => { + if (newTracks === undefined) return; + await this.output.whenDefined; + await this.output.signal()?.tracks.save(newTracks); + }, + state: computed(() => + this.output.signal()?.tracks.state() ?? "loading" + ), + }, + }; + + return m; + } +} diff --git a/src/components/transformer/output/refiner/default/element.js b/src/components/transformer/output/refiner/default/element.js index b15012b8..716dc941 100644 --- a/src/components/transformer/output/refiner/default/element.js +++ b/src/components/transformer/output/refiner/default/element.js @@ -1,64 +1,37 @@ -import { DiffuseElement, query } from "@common/element.js"; -import { computed, signal } from "@common/signal.js"; +import { computed } from "@common/signal.js"; +import { OutputTransformer } from "../../base.js"; /** - * @import { OutputElement, OutputManager } from "../../../../output/types.d.ts" + * @import { OutputManager } from "../../../../output/types.d.ts" * @import { Track } from "@definitions/types.d.ts" */ -class DefaultOutputRefinerTransformer extends DiffuseElement { +/** + * @extends {OutputTransformer} + */ +class DefaultOutputRefinerTransformer extends OutputTransformer { constructor() { super(); + const base = this.base(); + /** @type {OutputManager} */ const manager = { tracks: { + ...base.tracks, collection: computed(() => { - return this.#defined.value - ? this.output?.tracks?.collection() ?? [] - : []; + return base.tracks.collection() ?? []; }), - reload: () => this.output?.tracks?.reload() ?? Promise.resolve(), save: async (newTracks) => { const filtered = newTracks.filter((t) => !t.ephemeral); - - if (!this.output) return; - - await customElements.whenDefined(this.output.localName); - await this.output.tracks.save(filtered); + await base.tracks.save(filtered); }, - state: computed(() => this.output?.tracks.state() ?? "loading"), }, }; // Assign manager properties to class this.tracks = manager.tracks; } - - /** @type {OutputElement | undefined} */ - output = undefined; - - // SIGNALS - - #defined = signal(false); - - // LIFECYCLE - - /** - * @override - */ - connectedCallback() { - super.connectedCallback(); - - /** @type {OutputElement} */ - const output = query(this, "output-selector"); - this.output = output; - - // When defined - customElements.whenDefined(this.output.localName).then( - () => this.#defined.value = true, - ); - } } export default DefaultOutputRefinerTransformer; diff --git a/src/components/transformer/output/string/json/element.js b/src/components/transformer/output/string/json/element.js index 1356c7e5..7867d19b 100644 --- a/src/components/transformer/output/string/json/element.js +++ b/src/components/transformer/output/string/json/element.js @@ -1,26 +1,26 @@ -import { DiffuseElement, query } from "@common/element.js"; -import { computed, signal } from "@common/signal.js"; +import { computed } from "@common/signal.js"; +import { OutputTransformer } from "../../base.js"; /** - * @import { OutputElement, OutputManager } from "../../../../output/types.d.ts" + * @import { OutputManager } from "../../../../output/types.d.ts" * @import { Track } from "@definitions/types.d.ts" */ -class JsonStringOutputTransformer extends DiffuseElement { +/** + * @extends {OutputTransformer} + */ +class JsonStringOutputTransformer extends OutputTransformer { constructor() { super(); + const base = this.base(); + /** @type {OutputManager} */ const manager = { tracks: { + ...base.tracks, collection: computed(() => { - const json = this.#defined.value - ? this.output?.tracks?.collection() ?? [] - : []; - - // In addition to the above, Some polymorphic outputs - // use an empty array as the default return value. - if (Array.isArray(json)) return json; + const json = base.tracks.collection() ?? "[]"; // Try parsing JSON try { @@ -32,47 +32,16 @@ class JsonStringOutputTransformer extends DiffuseElement { return []; } }), - reload: () => this.output?.tracks?.reload() ?? Promise.resolve(), save: async (newTracks) => { const json = JSON.stringify(newTracks); - - if (!this.output) return; - - await customElements.whenDefined(this.output.localName); - await this.output.tracks.save(json); + await base.tracks.save(json); }, - state: computed(() => this.output?.tracks?.state() ?? "loading"), }, }; // Assign manager properties to class this.tracks = manager.tracks; } - - /** @type {OutputElement | undefined} */ - output = undefined; - - // SIGNALS - - #defined = signal(false); - - // LIFECYCLE - - /** - * @override - */ - connectedCallback() { - super.connectedCallback(); - - /** @type {OutputElement} */ - const output = query(this, "output-selector"); - this.output = output; - - // When defined - customElements.whenDefined(this.output.localName).then( - () => this.#defined.value = true, - ); - } } export default JsonStringOutputTransformer; diff --git a/src/themes/webamp/index.js b/src/themes/webamp/index.js index 1f906fa2..b154e1eb 100644 --- a/src/themes/webamp/index.js +++ b/src/themes/webamp/index.js @@ -1,4 +1,4 @@ -// import "@components/orchestrator/process-tracks/element.js"; +import "@components/orchestrator/process-tracks/element.js"; import "@components/orchestrator/queue-tracks/element.js"; import "@components/input/opensubsonic/element.js"; import "@components/input/s3/element.js"; -- 2.43.0 From 49d7032c024f53537971b0975f40a7a5f7ac40c8 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Sun, 21 Dec 2025 23:32:27 +0100 Subject: [PATCH] feat: output configurator --- src/components/configurator/output/element.js | 90 ++++++++++++++++++- .../polymorphic/indexed-db/constants.js | 2 +- src/themes/webamp/index.js | 6 +- src/themes/webamp/index.vto | 15 ++-- 4 files changed, 101 insertions(+), 12 deletions(-) diff --git a/src/components/configurator/output/element.js b/src/components/configurator/output/element.js index 70551c15..873306fe 100644 --- a/src/components/configurator/output/element.js +++ b/src/components/configurator/output/element.js @@ -2,11 +2,16 @@ import { DiffuseElement } from "@common/element.js"; import { computed, signal } from "@common/signal.js"; /** - * @import {ProxiedActions} from "@common/worker.d.ts" * @import {Track} from "@definitions/types.d.ts" * @import {OutputManager, OutputElement} from "@components/output/types.d.ts" */ +/** + * @typedef {OutputElement} Output + */ + +const STORAGE_PREFIX = "diffuse/configurator/output"; + //////////////////////////////////////////// // ELEMENT //////////////////////////////////////////// @@ -16,7 +21,6 @@ import { computed, signal } from "@common/signal.js"; */ class OutputConfigurator extends DiffuseElement { static NAME = "diffuse/configurator/output"; - static WORKER_URL = "components/configurator/output/worker.js"; constructor() { super(); @@ -25,13 +29,25 @@ class OutputConfigurator extends DiffuseElement { const manager = { tracks: { collection: computed(() => { + const out = this.#selectedOutput.value; + if (out) return out.tracks.collection(); return this.#memory.tracks.value; }), - reload: async () => {}, + reload: () => { + const out = this.#selectedOutput.value; + if (out) return out.tracks.reload(); + return Promise.resolve(); + }, save: async (newTracks) => { + const out = this.#selectedOutput.value; + if (out) return await out.tracks.save(newTracks); this.#memory.tracks.value = newTracks; }, - state: () => "loaded", + state: computed(() => { + const out = this.#selectedOutput.value; + if (out) return out.tracks.state(); + return out === undefined ? "loading" : "loaded"; + }), }, }; @@ -45,6 +61,10 @@ class OutputConfigurator extends DiffuseElement { tracks: signal(/** @type {Track[]} */ ([])), }; + #selectedOutput = signal( + /** @type {Output | null | undefined} */ (undefined), + ); + // LIFECYCLE /** @@ -52,6 +72,68 @@ class OutputConfigurator extends DiffuseElement { */ async connectedCallback() { super.connectedCallback(); + this.#selectedOutput.value = await this.#findSelectedOutput(); + + this.effect(() => { + console.log("selectedOutput changed", this.#selectedOutput.value); + }); + + this.effect(() => { + console.log("collection changed", this.tracks.collection()); + }); + + this.effect(() => { + console.log("state changed", this.tracks.state()); + }); + } + + // MISC + + async #findSelectedOutput() { + const id = localStorage.getItem(`${STORAGE_PREFIX}/selected/id`); + const el = id ? this.root().querySelector(`#${id}`) : null; + + if (!el) return null; + + await customElements.whenDefined(el.localName); + + if ( + "nameWithGroup" in el === false || + "tracks" in el === false + ) { + return null; + } + + return /** @type {Output} */ (/** @type {unknown} */ (el)); + } + + /** + * @override + */ + dependencies() { + return Object.fromEntries( + Array.from(this.children).flatMap((element) => { + if (element.hasAttribute("id") === false) { + console.warn( + "Missing `id` for output configurator child element with `localName` '" + + element.localName + "'", + ); + return []; + } + + const d = /** @type {DiffuseElement} */ (element); + return [[d.id, d]]; + }), + ); + } + + // ADDITIONAL ACTIONS + + /** + * @param {string} id + */ + selectOutput(id) { + localStorage.setItem(`${STORAGE_PREFIX}/selected/id`, id); } } diff --git a/src/components/output/polymorphic/indexed-db/constants.js b/src/components/output/polymorphic/indexed-db/constants.js index 6e78669b..f40032da 100644 --- a/src/components/output/polymorphic/indexed-db/constants.js +++ b/src/components/output/polymorphic/indexed-db/constants.js @@ -1 +1 @@ -export const IDB_PREFIX = "@components/output/polymorphic/indexed-db"; +export const IDB_PREFIX = "diffuse/output/polymorphic/indexed-db"; diff --git a/src/themes/webamp/index.js b/src/themes/webamp/index.js index b154e1eb..601f2e9e 100644 --- a/src/themes/webamp/index.js +++ b/src/themes/webamp/index.js @@ -1,7 +1,8 @@ -import "@components/orchestrator/process-tracks/element.js"; -import "@components/orchestrator/queue-tracks/element.js"; +import "@components/configurator/output/element.js"; import "@components/input/opensubsonic/element.js"; import "@components/input/s3/element.js"; +import "@components/orchestrator/process-tracks/element.js"; +import "@components/orchestrator/queue-tracks/element.js"; import "@components/output/polymorphic/indexed-db/element.js"; import "@components/processor/metadata/element.js"; import "@components/transformer/output/string/json/element.js"; @@ -22,6 +23,7 @@ const input = component(Input); const queue = component(Queue); globalThis.queue = queue; +globalThis.output = document.querySelector("#output"); //////////////////////////////////////////// // 📡 diff --git a/src/themes/webamp/index.vto b/src/themes/webamp/index.vto index 442c3218..86c6e2b8 100644 --- a/src/themes/webamp/index.vto +++ b/src/themes/webamp/index.vto @@ -77,18 +77,23 @@ --> - - + + - - - + + + + + + + + Date: Sun, 21 Dec 2025 23:35:00 +0100 Subject: [PATCH] chore: remove logs --- src/components/configurator/output/element.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/components/configurator/output/element.js b/src/components/configurator/output/element.js index 873306fe..36080c8f 100644 --- a/src/components/configurator/output/element.js +++ b/src/components/configurator/output/element.js @@ -73,18 +73,6 @@ class OutputConfigurator extends DiffuseElement { async connectedCallback() { super.connectedCallback(); this.#selectedOutput.value = await this.#findSelectedOutput(); - - this.effect(() => { - console.log("selectedOutput changed", this.#selectedOutput.value); - }); - - this.effect(() => { - console.log("collection changed", this.tracks.collection()); - }); - - this.effect(() => { - console.log("state changed", this.tracks.state()); - }); } // MISC -- 2.43.0 From 016cd75c9fa4230c45c739b9d736171ad99b89df Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Sun, 21 Dec 2025 23:50:52 +0100 Subject: [PATCH] feat: default output --- src/components/configurator/output/element.js | 11 +++++++++-- src/themes/webamp/index.vto | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/configurator/output/element.js b/src/components/configurator/output/element.js index 36080c8f..4216638d 100644 --- a/src/components/configurator/output/element.js +++ b/src/components/configurator/output/element.js @@ -78,7 +78,8 @@ class OutputConfigurator extends DiffuseElement { // MISC async #findSelectedOutput() { - const id = localStorage.getItem(`${STORAGE_PREFIX}/selected/id`); + const id = localStorage.getItem(`${STORAGE_PREFIX}/selected/id`) ?? + this.getAttribute("default"); const el = id ? this.root().querySelector(`#${id}`) : null; if (!el) return null; @@ -117,11 +118,17 @@ class OutputConfigurator extends DiffuseElement { // ADDITIONAL ACTIONS + async deselectOutput() { + localStorage.removeItem(`${STORAGE_PREFIX}/selected/id`); + this.#selectedOutput.value = await this.#findSelectedOutput(); + } + /** * @param {string} id */ - selectOutput(id) { + async selectOutput(id) { localStorage.setItem(`${STORAGE_PREFIX}/selected/id`, id); + this.#selectedOutput.value = await this.#findSelectedOutput(); } } diff --git a/src/themes/webamp/index.vto b/src/themes/webamp/index.vto index 86c6e2b8..f8d49872 100644 --- a/src/themes/webamp/index.vto +++ b/src/themes/webamp/index.vto @@ -89,7 +89,7 @@ - + -- 2.43.0 From 6d95f3fb22036429d1a0a1de8f8d00918bd820ad Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Mon, 22 Dec 2025 18:52:08 +0100 Subject: [PATCH] feat: output configurator --- src/common/element.js | 9 +- src/components/configurator/output/element.js | 21 ++- src/components/engine/audio/element.js | 4 +- .../orchestrator/queue-tracks/element.js | 2 +- .../output/polymorphic/indexed-db/element.js | 6 +- src/index.vto | 5 +- src/themes/webamp/browser/element.js | 5 +- src/themes/webamp/index.css | 61 ------- src/themes/webamp/index.js | 18 +- src/themes/webamp/index.vto | 42 ++--- src/themes/webamp/window-manager/element.js | 158 +++++++++++++++--- src/themes/webamp/window/element.js | 3 +- 12 files changed, 204 insertions(+), 130 deletions(-) diff --git a/src/common/element.js b/src/common/element.js index 6ef7e24e..cf2fc701 100644 --- a/src/common/element.js +++ b/src/common/element.js @@ -61,7 +61,12 @@ export class DiffuseElement extends HTMLElement { } /** */ - nameWithGroup() { + get label() { + return this.getAttribute("label") ?? this.id ?? this.localName; + } + + /** */ + get nameWithGroup() { return `${this.constructor.prototype.constructor.NAME}/${this.group}`; } @@ -131,7 +136,7 @@ export class DiffuseElement extends HTMLElement { ); // Setup worker - const name = this.nameWithGroup(); + const name = this.nameWithGroup; const url = import.meta.resolve("./" + WORKER_URL) + `?${query}`; let worker; diff --git a/src/components/configurator/output/element.js b/src/components/configurator/output/element.js index 4216638d..6e81ba6e 100644 --- a/src/components/configurator/output/element.js +++ b/src/components/configurator/output/element.js @@ -118,15 +118,32 @@ class OutputConfigurator extends DiffuseElement { // ADDITIONAL ACTIONS - async deselectOutput() { + async deselect() { localStorage.removeItem(`${STORAGE_PREFIX}/selected/id`); this.#selectedOutput.value = await this.#findSelectedOutput(); } + async options() { + const deps = this.dependencies(); + const entries = Object.entries(deps); + + await Promise.all( + entries.map(([_k, v]) => customElements.whenDefined(v.localName)), + ); + + return entries.map(([k, v]) => { + return { + id: k, + label: v.label, + element: v, + }; + }); + } + /** * @param {string} id */ - async selectOutput(id) { + async select(id) { localStorage.setItem(`${STORAGE_PREFIX}/selected/id`, id); this.#selectedOutput.value = await this.#findSelectedOutput(); } diff --git a/src/components/engine/audio/element.js b/src/components/engine/audio/element.js index 8ebcaa0b..7113b275 100644 --- a/src/components/engine/audio/element.js +++ b/src/components/engine/audio/element.js @@ -51,7 +51,7 @@ class AudioEngine extends BroadcastableDiffuseElement { // Setup broadcasting if part of group if (this.hasAttribute("group")) { const actions = this.broadcast( - this.nameWithGroup(), + this.nameWithGroup, { adjustVolume: { strategy: "replicate", fn: this.adjustVolume }, pause: { strategy: "leaderOnly", fn: this.pause }, @@ -405,7 +405,7 @@ class AudioEngineItem extends BroadcastableDiffuseElement { // Setup broadcasting if part of group if (this.hasAttribute("group")) { const actions = this.broadcast( - this.nameWithGroup(), + this.nameWithGroup, { getDuration: { strategy: "leaderOnly", fn: this.$state.duration.get }, getHasEnded: { strategy: "leaderOnly", fn: this.$state.hasEnded.get }, diff --git a/src/components/orchestrator/queue-tracks/element.js b/src/components/orchestrator/queue-tracks/element.js index 428f7196..32d0db58 100644 --- a/src/components/orchestrator/queue-tracks/element.js +++ b/src/components/orchestrator/queue-tracks/element.js @@ -45,7 +45,7 @@ class QueueTracksOrchestrator extends BroadcastableDiffuseElement { async connectedCallback() { // Broadcast if needed if (this.hasAttribute("group")) { - this.broadcast(this.nameWithGroup(), {}); + this.broadcast(this.nameWithGroup, {}); } // Super diff --git a/src/components/output/polymorphic/indexed-db/element.js b/src/components/output/polymorphic/indexed-db/element.js index e99e2b3e..87f1e5da 100644 --- a/src/components/output/polymorphic/indexed-db/element.js +++ b/src/components/output/polymorphic/indexed-db/element.js @@ -38,8 +38,10 @@ class IndexedDBOutput extends DiffuseElement { /** @param {string} name */ #cat(name) { - const key = this.hasAttribute("key") ? this.getAttribute("key") + "/" : ""; - return `${key}${name}`; + const namespace = this.hasAttribute("namespace") + ? this.getAttribute("namespace") + "/" + : ""; + return `${namespace}${name}`; } } diff --git a/src/index.vto b/src/index.vto index 8fa7754c..46fd96df 100644 --- a/src/index.vto +++ b/src/index.vto @@ -23,11 +23,10 @@ themes: configurators: - url: "components/configurator/input/element.js" title: "Input" - desc: "Add multiple inputs." + desc: "Allows for multiple inputs to be used at once." - url: "components/configurator/output/element.js" title: "Output" - desc: "Allows the user to configure a specific output." - todo: true + 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." - url: "components/configurator/scrobbles/element.js" title: "Scrobbles" desc: "Configure multiple scrobblers (music trackers)." diff --git a/src/themes/webamp/browser/element.js b/src/themes/webamp/browser/element.js index 4389d130..d8ea954e 100644 --- a/src/themes/webamp/browser/element.js +++ b/src/themes/webamp/browser/element.js @@ -98,7 +98,6 @@ class Browser extends DiffuseElement { ***********************************/ .sunken-panel { - content-visibility: auto; height: 30dvh; min-height: 80px; resize: both; @@ -118,6 +117,10 @@ class Browser extends DiffuseElement { } } + table tbody tr { + content-visibility: auto; + } + table td { contain-intrinsic-size: auto 14px; overflow: hidden; diff --git a/src/themes/webamp/index.css b/src/themes/webamp/index.css index 85a7d537..36dcfb14 100644 --- a/src/themes/webamp/index.css +++ b/src/themes/webamp/index.css @@ -82,64 +82,3 @@ main > section { } } } - -/*********************************** - * Windows - ***********************************/ - -.windows dtw-window { - left: 12px; - position: absolute; - top: 12px; - z-index: 999; - - /* Waiting on https://developer.mozilla.org/en-US/docs/Web/CSS/sibling-index#browser_compatibility */ - &:nth-child(1) { - left: 24px; - top: 24px; - } - - &:nth-child(2) { - left: 36px; - top: 36px; - } - - &:nth-child(3) { - left: 48px; - top: 48px; - } - - &:nth-child(4) { - left: 60px; - top: 60px; - } - - &:nth-child(5) { - left: 72px; - top: 72px; - } - - &:nth-child(6) { - left: 84px; - top: 84px; - } - - &:nth-child(7) { - left: 96px; - top: 96px; - } - - &:nth-child(8) { - left: 108px; - top: 108px; - } - - &:nth-child(9) { - left: 120px; - top: 120px; - } -} - -.windows section { - z-index: 999; -} diff --git a/src/themes/webamp/index.js b/src/themes/webamp/index.js index 601f2e9e..fc8060f2 100644 --- a/src/themes/webamp/index.js +++ b/src/themes/webamp/index.js @@ -1,7 +1,7 @@ import "@components/configurator/output/element.js"; import "@components/input/opensubsonic/element.js"; import "@components/input/s3/element.js"; -import "@components/orchestrator/process-tracks/element.js"; +// import "@components/orchestrator/process-tracks/element.js"; import "@components/orchestrator/queue-tracks/element.js"; import "@components/output/polymorphic/indexed-db/element.js"; import "@components/processor/metadata/element.js"; @@ -16,7 +16,7 @@ import { effect, signal, untracked } from "@common/signal.js"; import "./browser/element.js"; import "./window/element.js"; -import "./window-manager/element.js"; +import WindowManager from "./window-manager/element.js"; import WebampElement from "./webamp/element.js"; const input = component(Input); @@ -157,9 +157,7 @@ document.body.querySelectorAll(".desktop__item").forEach((element) => { if (element instanceof HTMLElement) { element.addEventListener("dblclick", () => { const f = element.querySelector("label")?.getAttribute("for"); - if (f) { - document.body.querySelector(`dtw-window#${f}`)?.toggleAttribute("open"); - } + if (f) windowManager()?.toggleWindow(f); }); } }); @@ -182,3 +180,13 @@ amp.onClose(() => winampIsShown = false); // TODO: // amp.onMinimize(() => amp.close()); + +//////////////////////////////////////////// +// 🛠️ +//////////////////////////////////////////// + +function windowManager() { + const w = document.body.querySelector("dtw-window-manager"); + if (w instanceof WindowManager) return w; + return null; +} diff --git a/src/themes/webamp/index.vto b/src/themes/webamp/index.vto index f8d49872..c4211bce 100644 --- a/src/themes/webamp/index.vto +++ b/src/themes/webamp/index.vto @@ -10,38 +10,19 @@
+
- - - - - Manage audio inputs -

👀

-
- - - - - Manage user data -

👀

-
- - - - - Browse collection - - -
+
+ + +
@@ -67,12 +48,15 @@
+
@@ -87,7 +71,7 @@ - + diff --git a/src/themes/webamp/window-manager/element.js b/src/themes/webamp/window-manager/element.js index 3063718c..e7cf9bb8 100644 --- a/src/themes/webamp/window-manager/element.js +++ b/src/themes/webamp/window-manager/element.js @@ -2,9 +2,10 @@ import { DiffuseElement } from "@common/element.js"; import { signal } from "@common/signal.js"; import { debounceMicrotask } from "@vicary/debounce-microtask"; +import WindowElement from "../window/element.js" + /** * @import {RenderArg} from "@common/element.d.ts" - * @import WindowElement from "../window/element.js"; */ //////////////////////////////////////////// @@ -15,6 +16,9 @@ class WindowManager extends DiffuseElement { constructor() { super(); this.attachShadow({ mode: "open" }); + + this.focusOnWindow = this.focusOnWindow.bind(this) + this.windowMoveStart = this.windowMoveStart.bind(this) } // SIGNALS @@ -27,7 +31,7 @@ class WindowManager extends DiffuseElement { /** * @override */ - connectedCallback() { + async connectedCallback() { super.connectedCallback(); // Events @@ -85,7 +89,7 @@ class WindowManager extends DiffuseElement { if (win.id) this.$activeWindow.value = win.id; this.#lastZindex++; - win.style.zIndex = this.#lastZindex.toString(); + this.setWindowZindex(win.id, this.#lastZindex) } } @@ -94,18 +98,7 @@ class WindowManager extends DiffuseElement { */ async setWindowStatuses(activeId) { await customElements.whenDefined("dtw-window"); - - this.querySelectorAll("dtw-window").forEach( - (window) => { - const win = /** @type {WindowElement} */ (window); - - if (activeId && window.id === activeId) { - win.activate(); - } else { - win.deactivate(); - } - }, - ); + this.activateWindow(activeId) } /** @@ -119,7 +112,7 @@ class WindowManager extends DiffuseElement { if (event instanceof MouseEvent) { const x = event.x - ogEvent.detail.xElement; const y = event.y - ogEvent.detail.yElement; - const target = ogEvent.target; + const target = ogEvent.detail.element; if (target) { target.style.left = `${x}px`; @@ -131,18 +124,56 @@ class WindowManager extends DiffuseElement { }); const stopMove = () => { - this.removeEventListener("mousemove", moveFn); - + document.removeEventListener("mousemove", moveFn); document.removeEventListener("mouseup", stopMove); document.removeEventListener("mouseleave", stopMove); }; - this.addEventListener("mousemove", moveFn); - + document.addEventListener("mousemove", moveFn); document.addEventListener("mouseup", stopMove); document.addEventListener("mouseleave", stopMove); } + // ACTIONS + + /** + * @param {string} id + */ + activateWindow(id) { + this.querySelectorAll("dtw-window").forEach(w => { + if (w instanceof WindowElement === false) return + + if (activeId && w.id === activeId) { + w.activate(); + } else { + w.deactivate(); + } + }) + } + + /** + * @param {string} id + * @param {number} index + */ + setWindowZindex(id, index) { + const w = this.root().querySelector(`dtw-window#${id}`) + w.style.zIndex = index.toString(); + } + + /** + * @param {string} id + */ + toggleWindow(id) { + const w = this.root().querySelector(`dtw-window#${id}`) + if (w instanceof WindowElement === false) return + + w.toggleAttribute("open") + + if (w.hasAttribute("open")) { + this.activateWindow(id) + } + } + // RENDER /** @@ -150,13 +181,98 @@ class WindowManager extends DiffuseElement { */ render({ html }) { return html` + + - + + + + Manage audio inputs +

👀

+
+ + + + + Manage user data + +
+

Where do you want to keep your data?

+
+ + +
+
+
+ + + + + Browse collection + + `; } } diff --git a/src/themes/webamp/window/element.js b/src/themes/webamp/window/element.js index 1650c317..090d75f5 100644 --- a/src/themes/webamp/window/element.js +++ b/src/themes/webamp/window/element.js @@ -66,7 +66,7 @@ class WindowElement extends DiffuseElement {
@@ -99,6 +99,7 @@ class WindowElement extends DiffuseElement { bubbles: true, composed: true, detail: { + element: this, x: mouse.x, xElement: mouse.layerX, y: mouse.y, -- 2.43.0 From 203ffd41e38c547bb7faf346d859e7450774cf4d Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Mon, 22 Dec 2025 18:56:41 +0100 Subject: [PATCH] fix: focus --- src/themes/webamp/window-manager/element.js | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/themes/webamp/window-manager/element.js b/src/themes/webamp/window-manager/element.js index e7cf9bb8..8134a566 100644 --- a/src/themes/webamp/window-manager/element.js +++ b/src/themes/webamp/window-manager/element.js @@ -35,8 +35,8 @@ class WindowManager extends DiffuseElement { super.connectedCallback(); // Events - this.addEventListener("mousedown", this.focusOnWindow); - this.addEventListener("dtw-window-start-move", this.windowMoveStart); + this.root().addEventListener("mousedown", this.focusOnWindow); + this.root().addEventListener("dtw-window-start-move", this.windowMoveStart); // Webamp stuff document.body.addEventListener( @@ -57,8 +57,8 @@ class WindowManager extends DiffuseElement { disconnectedCallback() { super.disconnectedCallback(); - this.removeEventListener("mousedown", this.focusOnWindow); - this.removeEventListener("dtw-window-start-move", this.windowMoveStart); + this.root().removeEventListener("mousedown", this.focusOnWindow); + this.root().removeEventListener("dtw-window-start-move", this.windowMoveStart); document.body.removeEventListener( "mousedown", @@ -89,7 +89,7 @@ class WindowManager extends DiffuseElement { if (win.id) this.$activeWindow.value = win.id; this.#lastZindex++; - this.setWindowZindex(win.id, this.#lastZindex) + win.style.zIndex = this.#lastZindex.toString(); } } @@ -151,15 +151,6 @@ class WindowManager extends DiffuseElement { }) } - /** - * @param {string} id - * @param {number} index - */ - setWindowZindex(id, index) { - const w = this.root().querySelector(`dtw-window#${id}`) - w.style.zIndex = index.toString(); - } - /** * @param {string} id */ @@ -171,6 +162,8 @@ class WindowManager extends DiffuseElement { if (w.hasAttribute("open")) { this.activateWindow(id) + this.#lastZindex++; + w.style.zIndex = this.#lastZindex.toString(); } } -- 2.43.0 From 0a8778502114134bc5514db2fdfd258b4f5c5b24 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Tue, 23 Dec 2025 17:11:50 +0100 Subject: [PATCH] feat: use output configurator in constituents --- src/common/constituents/default.js | 16 +++++++++++++--- src/common/element.js | 4 ++++ src/components/output/common.js | 4 ++-- .../output/polymorphic/indexed-db/element.js | 12 ++++++++++-- src/components/transformer/output/base.js | 6 +++--- .../transformer/output/string/json/element.js | 3 ++- src/themes/blur/artwork-controller/element.css | 3 +++ src/themes/blur/artwork-controller/element.js | 7 ++----- src/themes/blur/artwork-controller/index.vto | 14 +++++++------- src/themes/webamp/index.js | 2 +- 10 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/common/constituents/default.js b/src/common/constituents/default.js index 956569d3..6761aadf 100644 --- a/src/common/constituents/default.js +++ b/src/common/constituents/default.js @@ -1,4 +1,5 @@ import InputConfigurator from "@components/configurator/input/element.js"; +import OutputConfigurator from "@components/configurator/output/element.js"; import Queue from "@components/engine/queue/element.js"; import OpenSubsonic from "@components/input/opensubsonic/element.js"; import S3 from "@components/input/s3/element.js"; @@ -32,14 +33,22 @@ export function config() { // Output const idb = new IndexedDBOutput(); + idb.setAttribute("id", "idb-json-output") + idb.setAttribute("namespace", "json") + const json = new JsonStringOutput(); - json.setAttribute("output-selector", idb.localName); + json.setAttribute("id", "idb-json") + json.setAttribute("output-selector", "#idb-json-output"); + + const output = new OutputConfigurator(); + output.setAttribute("default", "idb-json"); + output.append(json); const refiner = new DefaultRefiner(); refiner.setAttribute("id", "output"); - refiner.setAttribute("output-selector", json.localName); + refiner.setAttribute("output-selector", output.localName); - document.body.append(idb, json, refiner); + document.body.append(idb, output, refiner); // Orchestrators const oqt = new QueueTracksOrchestrator(); @@ -68,6 +77,7 @@ export function config() { configurator: { input, + output, }, engine: { queue, diff --git a/src/common/element.js b/src/common/element.js index cf2fc701..7ec054bd 100644 --- a/src/common/element.js +++ b/src/common/element.js @@ -20,6 +20,8 @@ export const DEFAULT_GROUP = "default"; * around rendering and managing signals. */ export class DiffuseElement extends HTMLElement { + $connected = signal(false) + #connected = Promise.withResolvers(); #disposables = /** @type {Array<() => void>} */ ([]); @@ -98,6 +100,7 @@ export class DiffuseElement extends HTMLElement { // LIFECYCLE connectedCallback() { + this.$connected.value = true this.#connected.resolve(null); if (!("render" in this && typeof this.render === "function")) return; @@ -109,6 +112,7 @@ export class DiffuseElement extends HTMLElement { } disconnectedCallback() { + this.$connected.value = false this.#teardown(); } diff --git a/src/components/output/common.js b/src/components/output/common.js index bfb8ac37..b522d0ff 100644 --- a/src/components/output/common.js +++ b/src/components/output/common.js @@ -1,4 +1,4 @@ -import { signal } from "@common/signal.js"; +import { effect, signal } from "@common/signal.js"; /** * @import {OutputManager, OutputManagerProperties} from "./types.d.ts" @@ -19,7 +19,7 @@ export function outputManager({ init, tracks }) { ts.value = "loaded"; } - loadTracks(); + effect(loadTracks); return { tracks: { diff --git a/src/components/output/polymorphic/indexed-db/element.js b/src/components/output/polymorphic/indexed-db/element.js index 87f1e5da..4552d5ae 100644 --- a/src/components/output/polymorphic/indexed-db/element.js +++ b/src/components/output/polymorphic/indexed-db/element.js @@ -28,14 +28,22 @@ class IndexedDBOutput extends DiffuseElement { const manager = outputManager({ tracks: { empty: () => undefined, - get: () => p.get({ name: this.#cat("tracks") }), - put: (data) => p.put({ name: this.#cat("tracks"), data }), + get: () => { + if (!this.$connected.value) return undefined + return p.get({ name: this.#cat("tracks") }) + }, + put: (data) => { + if (!this.$connected.value) return + return p.put({ name: this.#cat("tracks"), data }) + }, }, }); this.tracks = manager.tracks; } + // 🛠️ + /** @param {string} name */ #cat(name) { const namespace = this.hasAttribute("namespace") diff --git a/src/components/transformer/output/base.js b/src/components/transformer/output/base.js index 9de69c6a..5716e96b 100644 --- a/src/components/transformer/output/base.js +++ b/src/components/transformer/output/base.js @@ -54,9 +54,9 @@ export class OutputTransformer extends DiffuseElement { await this.output.whenDefined; await this.output.signal()?.tracks.save(newTracks); }, - state: computed(() => - this.output.signal()?.tracks.state() ?? "loading" - ), + state: computed(() => { + return this.output.signal()?.tracks.state() ?? "loading" + }), }, }; diff --git a/src/components/transformer/output/string/json/element.js b/src/components/transformer/output/string/json/element.js index 7867d19b..2d5d32f8 100644 --- a/src/components/transformer/output/string/json/element.js +++ b/src/components/transformer/output/string/json/element.js @@ -20,7 +20,8 @@ class JsonStringOutputTransformer extends OutputTransformer { tracks: { ...base.tracks, collection: computed(() => { - const json = base.tracks.collection() ?? "[]"; + let json = base.tracks.collection(); + if (typeof json !== "string") json = "[]" // Try parsing JSON try { diff --git a/src/themes/blur/artwork-controller/element.css b/src/themes/blur/artwork-controller/element.css index eb8f3e37..cf007a28 100644 --- a/src/themes/blur/artwork-controller/element.css +++ b/src/themes/blur/artwork-controller/element.css @@ -1,3 +1,6 @@ +@import "../../../styles/vendor/phosphor/fill/style.css"; +@import "../../../styles/animations.css"; + :host { --transition-durition: 750ms; } diff --git a/src/themes/blur/artwork-controller/element.js b/src/themes/blur/artwork-controller/element.js index 0ee6eb2a..717bf712 100644 --- a/src/themes/blur/artwork-controller/element.js +++ b/src/themes/blur/artwork-controller/element.js @@ -121,8 +121,7 @@ class ArtworkController extends DiffuseElement { this.effect(() => { const now = !!queue.now(); - const bool = !now || - (now && this.#audio()?.loadingState() !== "loaded"); + const bool = (now && this.#audio()?.loadingState() !== "loaded"); if (this.#isLoadingTimeout) { clearTimeout(this.#isLoadingTimeout); @@ -398,9 +397,7 @@ class ArtworkController extends DiffuseElement { return html`
- import { config } from "../../../common/constituents/default.js" - import QueueAudioOrchestrator from "../../../components/orchestrator/queue-audio/element.js"; + import { config } from "./common/constituents/default.js" + import QueueAudioOrchestrator from "./components/orchestrator/queue-audio/element.js"; - import "../../../components/engine/audio/element.js" - import "../../../components/processor/artwork/element.js" + import "./components/engine/audio/element.js" + import "./components/processor/artwork/element.js" // Prepare default constituents setup const defaults = config() // Only then initiate artwork controller - import("./element.js") + import("./themes/blur/artwork-controller/element.js") // Orchestrators diff --git a/src/themes/webamp/index.js b/src/themes/webamp/index.js index fc8060f2..1551440c 100644 --- a/src/themes/webamp/index.js +++ b/src/themes/webamp/index.js @@ -1,7 +1,7 @@ import "@components/configurator/output/element.js"; import "@components/input/opensubsonic/element.js"; import "@components/input/s3/element.js"; -// import "@components/orchestrator/process-tracks/element.js"; +import "@components/orchestrator/process-tracks/element.js"; import "@components/orchestrator/queue-tracks/element.js"; import "@components/output/polymorphic/indexed-db/element.js"; import "@components/processor/metadata/element.js"; -- 2.43.0 From 834fdd4c1d0896af3712cd7f45900adfde5bc676 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Tue, 23 Dec 2025 17:23:21 +0100 Subject: [PATCH] fix: idb whenConnected --- .../output/polymorphic/indexed-db/element.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/output/polymorphic/indexed-db/element.js b/src/components/output/polymorphic/indexed-db/element.js index 4552d5ae..650727e8 100644 --- a/src/components/output/polymorphic/indexed-db/element.js +++ b/src/components/output/polymorphic/indexed-db/element.js @@ -26,16 +26,11 @@ class IndexedDBOutput extends DiffuseElement { /** @type {OutputManager} */ const manager = outputManager({ + init: this.whenConnected.bind(this), tracks: { empty: () => undefined, - get: () => { - if (!this.$connected.value) return undefined - return p.get({ name: this.#cat("tracks") }) - }, - put: (data) => { - if (!this.$connected.value) return - return p.put({ name: this.#cat("tracks"), data }) - }, + get: () => p.get({ name: this.#cat("tracks") }), + put: (data) => p.put({ name: this.#cat("tracks"), data }), }, }); -- 2.43.0