+13
-3
src/common/constituents/default.js
+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
+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
+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
+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
+1
-1
src/components/orchestrator/queue-tracks/element.js
+2
-2
src/components/output/common.js
+2
-2
src/components/output/common.js
+1
-1
src/components/output/polymorphic/indexed-db/constants.js
+1
-1
src/components/output/polymorphic/indexed-db/constants.js
+17
-5
src/components/output/polymorphic/indexed-db/element.js
+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
src/components/output/polymorphic/indexed-db/types.d.ts
···
···
1
+
export type SupportedDataTypes = any;
+10
-30
src/components/output/polymorphic/indexed-db/worker.js
+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
+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
+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
+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
+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
+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
+3
src/themes/blur/artwork-controller/element.css
+2
-5
src/themes/blur/artwork-controller/element.js
+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
+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
+4
-1
src/themes/webamp/browser/element.js
-61
src/themes/webamp/index.css
-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
-
}
+16
-6
src/themes/webamp/index.js
+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
+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
+2
-1
src/themes/webamp/window/element.js
···
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
+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
}