+13
-3
src/common/constituents/default.js
+13
-3
src/common/constituents/default.js
···
1
1
import InputConfigurator from "@components/configurator/input/element.js";
2
+
import OutputConfigurator from "@components/configurator/output/element.js";
2
3
import Queue from "@components/engine/queue/element.js";
3
4
import OpenSubsonic from "@components/input/opensubsonic/element.js";
4
5
import S3 from "@components/input/s3/element.js";
···
32
33
33
34
// Output
34
35
const idb = new IndexedDBOutput();
36
+
idb.setAttribute("id", "idb-json-output")
37
+
idb.setAttribute("namespace", "json")
38
+
35
39
const json = new JsonStringOutput();
36
-
json.setAttribute("output-selector", idb.localName);
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);
37
46
38
47
const refiner = new DefaultRefiner();
39
48
refiner.setAttribute("id", "output");
40
-
refiner.setAttribute("output-selector", json.localName);
49
+
refiner.setAttribute("output-selector", output.localName);
41
50
42
-
document.body.append(idb, json, refiner);
51
+
document.body.append(idb, output, refiner);
43
52
44
53
// Orchestrators
45
54
const oqt = new QueueTracksOrchestrator();
···
68
77
69
78
configurator: {
70
79
input,
80
+
output,
71
81
},
72
82
engine: {
73
83
queue,
+11
-2
src/common/element.js
+11
-2
src/common/element.js
···
20
20
* around rendering and managing signals.
21
21
*/
22
22
export class DiffuseElement extends HTMLElement {
23
+
$connected = signal(false)
24
+
23
25
#connected = Promise.withResolvers();
24
26
#disposables = /** @type {Array<() => void>} */ ([]);
25
27
···
61
63
}
62
64
63
65
/** */
64
-
nameWithGroup() {
66
+
get label() {
67
+
return this.getAttribute("label") ?? this.id ?? this.localName;
68
+
}
69
+
70
+
/** */
71
+
get nameWithGroup() {
65
72
return `${this.constructor.prototype.constructor.NAME}/${this.group}`;
66
73
}
67
74
···
93
100
// LIFECYCLE
94
101
95
102
connectedCallback() {
103
+
this.$connected.value = true
96
104
this.#connected.resolve(null);
97
105
98
106
if (!("render" in this && typeof this.render === "function")) return;
···
104
112
}
105
113
106
114
disconnectedCallback() {
115
+
this.$connected.value = false
107
116
this.#teardown();
108
117
}
109
118
···
131
140
);
132
141
133
142
// Setup worker
134
-
const name = this.nameWithGroup();
143
+
const name = this.nameWithGroup;
135
144
const url = import.meta.resolve("./" + WORKER_URL) + `?${query}`;
136
145
137
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
51
// Setup broadcasting if part of group
52
52
if (this.hasAttribute("group")) {
53
53
const actions = this.broadcast(
54
-
this.nameWithGroup(),
54
+
this.nameWithGroup,
55
55
{
56
56
adjustVolume: { strategy: "replicate", fn: this.adjustVolume },
57
57
pause: { strategy: "leaderOnly", fn: this.pause },
···
405
405
// Setup broadcasting if part of group
406
406
if (this.hasAttribute("group")) {
407
407
const actions = this.broadcast(
408
-
this.nameWithGroup(),
408
+
this.nameWithGroup,
409
409
{
410
410
getDuration: { strategy: "leaderOnly", fn: this.$state.duration.get },
411
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
-
import { signal } from "@common/signal.js";
1
+
import { effect, signal } from "@common/signal.js";
2
2
3
3
/**
4
4
* @import {OutputManager, OutputManagerProperties} from "./types.d.ts"
···
19
19
ts.value = "loaded";
20
20
}
21
21
22
-
loadTracks();
22
+
effect(loadTracks);
23
23
24
24
return {
25
25
tracks: {
+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
4
/**
5
5
* @import {ProxiedActions} from "@common/worker.d.ts"
6
6
* @import {OutputManager, OutputWorkerActions} from "../../types.d.ts"
7
+
* @import {SupportedDataTypes} from "./types.d.ts"
7
8
*/
8
9
9
10
////////////////////////////////////////////
···
20
21
constructor() {
21
22
super();
22
23
23
-
/** @type {ProxiedActions<OutputWorkerActions>} */
24
+
/** @type {ProxiedActions<OutputWorkerActions<SupportedDataTypes>>} */
24
25
const p = this.workerProxy();
25
26
26
-
// Manager
27
+
/** @type {OutputManager<SupportedDataTypes>} */
27
28
const manager = outputManager({
29
+
init: this.whenConnected.bind(this),
28
30
tracks: {
29
-
empty: () => [],
30
-
get: p.getTracks,
31
-
put: p.putTracks,
31
+
empty: () => undefined,
32
+
get: () => p.get({ name: this.#cat("tracks") }),
33
+
put: (data) => p.put({ name: this.#cat("tracks"), data }),
32
34
},
33
35
});
34
36
35
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}`;
36
48
}
37
49
}
38
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
4
import { ostiary, rpc } from "@common/worker.js";
5
5
6
6
/**
7
-
* @import {Track} from "@definitions/types.d.ts";
7
+
* @import {OutputWorkerActions} from "@components/output/types.d.ts";
8
+
* @import {SupportedDataTypes} from "./types.d.ts"
8
9
*/
9
10
10
11
////////////////////////////////////////////
···
12
13
////////////////////////////////////////////
13
14
14
15
/**
15
-
* @returns {Promise<Track[]>}
16
+
* @type {OutputWorkerActions<SupportedDataTypes>["get"]}
16
17
*/
17
-
export async function getTracks() {
18
-
/** @type {Track[] | null} */
19
-
const tracks = await get({ name: "tracks.json" });
20
-
return tracks ?? [];
18
+
export async function get({ name }) {
19
+
return await IDB.get(`${IDB_PREFIX}/${name}`);
21
20
}
22
21
23
22
/**
24
-
* @param {Track[]} tracks
23
+
* @type {OutputWorkerActions<SupportedDataTypes>["put"]}
25
24
*/
26
-
export async function putTracks(tracks) {
27
-
await put({ name: "tracks.json", data: tracks });
25
+
export async function put({ data, name }) {
26
+
return await IDB.set(`${IDB_PREFIX}/${name}`, data);
28
27
}
29
-
30
28
////////////////////////////////////////////
31
29
// ⚡️
32
30
////////////////////////////////////////////
33
31
34
32
ostiary((context) => {
35
33
rpc(context, {
36
-
getTracks,
37
-
putTracks,
34
+
get,
35
+
put,
38
36
});
39
37
});
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
-
}
+3
-4
src/components/output/types.d.ts
+3
-4
src/components/output/types.d.ts
···
1
1
import type { SignalReader } from "@common/signal.d.ts";
2
-
import type { Track } from "@definitions/types.d.ts";
3
2
import type { DiffuseElement } from "@common/element.js";
4
3
5
4
export type OutputElement<Tracks> = DiffuseElement & OutputManager<Tracks>;
···
22
21
};
23
22
};
24
23
25
-
export type OutputWorkerActions = {
26
-
getTracks(): Promise<Track[]>;
27
-
putTracks(tracks: Track[]): Promise<void>;
24
+
export type OutputWorkerActions<DataType> = {
25
+
get(args: { name: string }): Promise<DataType>;
26
+
put(args: { data: DataType; name: string }): Promise<void>;
28
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";
1
+
import { computed } from "@common/signal.js";
2
+
import { OutputTransformer } from "../../base.js";
3
3
4
4
/**
5
-
* @import { OutputElement, OutputManager } from "../../../../output/types.d.ts"
5
+
* @import { OutputManager } from "../../../../output/types.d.ts"
6
6
* @import { Track } from "@definitions/types.d.ts"
7
7
*/
8
8
9
-
class DefaultOutputRefinerTransformer extends DiffuseElement {
9
+
/**
10
+
* @extends {OutputTransformer<Track[]>}
11
+
*/
12
+
class DefaultOutputRefinerTransformer extends OutputTransformer {
10
13
constructor() {
11
14
super();
15
+
16
+
const base = this.base();
12
17
13
18
/** @type {OutputManager<Track[]>} */
14
19
const manager = {
15
20
tracks: {
21
+
...base.tracks,
16
22
collection: computed(() => {
17
-
return this.#defined.value
18
-
? this.output?.tracks?.collection() ?? []
19
-
: [];
23
+
return base.tracks.collection() ?? [];
20
24
}),
21
-
reload: () => this.output?.tracks?.reload() ?? Promise.resolve(),
22
25
save: async (newTracks) => {
23
26
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);
27
+
await base.tracks.save(filtered);
29
28
},
30
-
state: computed(() => this.output?.tracks.state() ?? "loading"),
31
29
},
32
30
};
33
31
34
32
// Assign manager properties to class
35
33
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
34
}
62
35
}
63
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";
1
+
import { computed } from "@common/signal.js";
2
+
import { OutputTransformer } from "../../base.js";
3
3
4
4
/**
5
-
* @import { OutputElement, OutputManager } from "../../../../output/types.d.ts"
5
+
* @import { OutputManager } from "../../../../output/types.d.ts"
6
6
* @import { Track } from "@definitions/types.d.ts"
7
7
*/
8
8
9
-
class JsonStringOutputTransformer extends DiffuseElement {
9
+
/**
10
+
* @extends {OutputTransformer<string>}
11
+
*/
12
+
class JsonStringOutputTransformer extends OutputTransformer {
10
13
constructor() {
11
14
super();
12
15
16
+
const base = this.base();
17
+
13
18
/** @type {OutputManager<Track[]>} */
14
19
const manager = {
15
20
tracks: {
21
+
...base.tracks,
16
22
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;
23
+
let json = base.tracks.collection();
24
+
if (typeof json !== "string") json = "[]"
24
25
25
26
// Try parsing JSON
26
27
try {
···
32
33
return [];
33
34
}
34
35
}),
35
-
reload: () => this.output?.tracks?.reload() ?? Promise.resolve(),
36
36
save: async (newTracks) => {
37
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);
38
+
await base.tracks.save(json);
43
39
},
44
-
state: computed(() => this.output?.tracks?.state() ?? "loading"),
45
40
},
46
41
};
47
42
48
43
// Assign manager properties to class
49
44
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
45
}
76
46
}
77
47
+2
-3
src/index.vto
+2
-3
src/index.vto
···
23
23
configurators:
24
24
- url: "components/configurator/input/element.js"
25
25
title: "Input"
26
-
desc: "Add multiple inputs."
26
+
desc: "Allows for multiple inputs to be used at once."
27
27
- url: "components/configurator/output/element.js"
28
28
title: "Output"
29
-
desc: "Allows the user to configure a specific output."
30
-
todo: true
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."
31
30
- url: "components/configurator/scrobbles/element.js"
32
31
title: "Scrobbles"
33
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
121
122
122
this.effect(() => {
123
123
const now = !!queue.now();
124
-
const bool = !now ||
125
-
(now && this.#audio()?.loadingState() !== "loaded");
124
+
const bool = (now && this.#audio()?.loadingState() !== "loaded");
126
125
127
126
if (this.#isLoadingTimeout) {
128
127
clearTimeout(this.#isLoadingTimeout);
···
398
397
399
398
return html`
400
399
<style>
401
-
@import "../../../styles/vendor/phosphor/fill/style.css";
402
-
@import "../../../styles/animations.css";
403
-
@import "./element.css";
400
+
@import "${import.meta.resolve('./element.css')}";
404
401
</style>
405
402
406
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
3
base: ../../../
4
4
5
5
styles:
6
-
- ../../../styles/vendor/phosphor/fill/style.css
7
-
- ../../../styles/base.css
6
+
- styles/vendor/phosphor/fill/style.css
7
+
- styles/base.css
8
8
---
9
9
10
10
<!-- ELEMENTS -->
···
23
23
<!-- SCRIPTS -->
24
24
25
25
<script type="module">
26
-
import { config } from "../../../common/constituents/default.js"
27
-
import QueueAudioOrchestrator from "../../../components/orchestrator/queue-audio/element.js";
26
+
import { config } from "./common/constituents/default.js"
27
+
import QueueAudioOrchestrator from "./components/orchestrator/queue-audio/element.js";
28
28
29
-
import "../../../components/engine/audio/element.js"
30
-
import "../../../components/processor/artwork/element.js"
29
+
import "./components/engine/audio/element.js"
30
+
import "./components/processor/artwork/element.js"
31
31
32
32
// Prepare default constituents setup
33
33
const defaults = config()
34
34
35
35
// Only then initiate artwork controller
36
-
import("./element.js")
36
+
import("./themes/blur/artwork-controller/element.js")
37
37
38
38
// Orchestrators
39
39
+4
-1
src/themes/webamp/browser/element.js
+4
-1
src/themes/webamp/browser/element.js
···
98
98
***********************************/
99
99
100
100
.sunken-panel {
101
-
content-visibility: auto;
102
101
height: 30dvh;
103
102
min-height: 80px;
104
103
resize: both;
···
116
115
&:first-child {
117
116
width: 40%;
118
117
}
118
+
}
119
+
120
+
table tbody tr {
121
+
content-visibility: auto;
119
122
}
120
123
121
124
table td {
-61
src/themes/webamp/index.css
-61
src/themes/webamp/index.css
···
82
82
}
83
83
}
84
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";
1
+
import "@components/configurator/output/element.js";
3
2
import "@components/input/opensubsonic/element.js";
4
3
import "@components/input/s3/element.js";
4
+
import "@components/orchestrator/process-tracks/element.js";
5
+
import "@components/orchestrator/queue-tracks/element.js";
5
6
import "@components/output/polymorphic/indexed-db/element.js";
6
7
import "@components/processor/metadata/element.js";
7
8
import "@components/transformer/output/string/json/element.js";
···
15
16
16
17
import "./browser/element.js";
17
18
import "./window/element.js";
18
-
import "./window-manager/element.js";
19
+
import WindowManager from "./window-manager/element.js";
19
20
import WebampElement from "./webamp/element.js";
20
21
21
22
const input = component(Input);
22
23
const queue = component(Queue);
23
24
24
25
globalThis.queue = queue;
26
+
globalThis.output = document.querySelector("#output");
25
27
26
28
////////////////////////////////////////////
27
29
// 📡
···
155
157
if (element instanceof HTMLElement) {
156
158
element.addEventListener("dblclick", () => {
157
159
const f = element.querySelector("label")?.getAttribute("for");
158
-
if (f) {
159
-
document.body.querySelector(`dtw-window#${f}`)?.toggleAttribute("open");
160
-
}
160
+
if (f) windowManager()?.toggleWindow(f);
161
161
});
162
162
}
163
163
});
···
180
180
181
181
// TODO:
182
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
10
<body>
11
11
<!--
12
12
13
-
UI
13
+
###################################
14
+
# UI
15
+
###################################
14
16
15
17
-->
16
18
<main>
19
+
<!-- 🪟 -->
17
20
<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>
21
+
<dtw-window-manager></dtw-window-manager>
22
+
</section>
25
23
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
24
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>
25
+
<!-- 🛋️ -->
45
26
<section class="desktop">
46
27
<!-- WINAMP -->
47
28
<a class="button desktop__item" id="desktop-winamp">
···
67
48
<label for="browser-window">Browse collection</label>
68
49
</a>
69
50
</section>
51
+
<!-- ⚡️ -->
70
52
<dtw-webamp></dtw-webamp>
71
53
</main>
72
54
73
55
<!--
74
56
75
-
COMPONENTS
57
+
###################################
58
+
# COMPONENTS
59
+
###################################
76
60
77
61
-->
78
62
<de-queue></de-queue>
79
63
80
-
<!-- Inputs, Output & Processors -->
81
-
<dop-indexed-db></dop-indexed-db>
64
+
<!-- Processors -->
82
65
<dp-metadata></dp-metadata>
83
66
67
+
<!-- Input -->
84
68
<dc-input id="input">
85
69
<di-opensubsonic></di-opensubsonic>
86
70
<di-s3></di-s3>
87
71
</dc-input>
88
72
89
-
<!-- Transformers -->
90
-
<dtor-default id="output" output-selector="dtos-json"></dtor-default>
91
-
<dtos-json output-selector="dop-indexed-db"></dtos-json>
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>
92
81
93
82
<!-- Orchestrators -->
94
83
<do-process-tracks
+2
-1
src/themes/webamp/window/element.js
+2
-1
src/themes/webamp/window/element.js
···
66
66
<div class="window">
67
67
<div
68
68
class="title-bar"
69
-
@mousedown="${this.titleBarMouseDown}"
69
+
@mousedown="${this.titleBarMouseDown.bind(this)}"
70
70
>
71
71
<div class="title-bar-icon">
72
72
<slot name="title-icon"></slot>
···
99
99
bubbles: true,
100
100
composed: true,
101
101
detail: {
102
+
element: this,
102
103
x: mouse.x,
103
104
xElement: mouse.layerX,
104
105
y: mouse.y,
+133
-24
src/themes/webamp/window-manager/element.js
+133
-24
src/themes/webamp/window-manager/element.js
···
2
2
import { signal } from "@common/signal.js";
3
3
import { debounceMicrotask } from "@vicary/debounce-microtask";
4
4
5
+
import WindowElement from "../window/element.js"
6
+
5
7
/**
6
8
* @import {RenderArg} from "@common/element.d.ts"
7
-
* @import WindowElement from "../window/element.js";
8
9
*/
9
10
10
11
////////////////////////////////////////////
···
15
16
constructor() {
16
17
super();
17
18
this.attachShadow({ mode: "open" });
19
+
20
+
this.focusOnWindow = this.focusOnWindow.bind(this)
21
+
this.windowMoveStart = this.windowMoveStart.bind(this)
18
22
}
19
23
20
24
// SIGNALS
···
27
31
/**
28
32
* @override
29
33
*/
30
-
connectedCallback() {
34
+
async connectedCallback() {
31
35
super.connectedCallback();
32
36
33
37
// Events
34
-
this.addEventListener("mousedown", this.focusOnWindow);
35
-
this.addEventListener("dtw-window-start-move", this.windowMoveStart);
38
+
this.root().addEventListener("mousedown", this.focusOnWindow);
39
+
this.root().addEventListener("dtw-window-start-move", this.windowMoveStart);
36
40
37
41
// Webamp stuff
38
42
document.body.addEventListener(
···
53
57
disconnectedCallback() {
54
58
super.disconnectedCallback();
55
59
56
-
this.removeEventListener("mousedown", this.focusOnWindow);
57
-
this.removeEventListener("dtw-window-start-move", this.windowMoveStart);
60
+
this.root().removeEventListener("mousedown", this.focusOnWindow);
61
+
this.root().removeEventListener("dtw-window-start-move", this.windowMoveStart);
58
62
59
63
document.body.removeEventListener(
60
64
"mousedown",
···
94
98
*/
95
99
async setWindowStatuses(activeId) {
96
100
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
-
);
101
+
this.activateWindow(activeId)
109
102
}
110
103
111
104
/**
···
119
112
if (event instanceof MouseEvent) {
120
113
const x = event.x - ogEvent.detail.xElement;
121
114
const y = event.y - ogEvent.detail.yElement;
122
-
const target = ogEvent.target;
115
+
const target = ogEvent.detail.element;
123
116
124
117
if (target) {
125
118
target.style.left = `${x}px`;
···
131
124
});
132
125
133
126
const stopMove = () => {
134
-
this.removeEventListener("mousemove", moveFn);
135
-
127
+
document.removeEventListener("mousemove", moveFn);
136
128
document.removeEventListener("mouseup", stopMove);
137
129
document.removeEventListener("mouseleave", stopMove);
138
130
};
139
131
140
-
this.addEventListener("mousemove", moveFn);
141
-
132
+
document.addEventListener("mousemove", moveFn);
142
133
document.addEventListener("mouseup", stopMove);
143
134
document.addEventListener("mouseleave", stopMove);
144
135
}
145
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
+
146
170
// RENDER
147
171
148
172
/**
···
150
174
*/
151
175
render({ html }) {
152
176
return html`
177
+
<link rel="stylesheet" href="../../styles/vendor/98.css" />
178
+
153
179
<style>
154
180
:host {
155
181
user-select: none;
156
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
+
}
157
236
</style>
158
237
159
-
<slot></slot>
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>
160
269
`;
161
270
}
162
271
}