A music player that connects to your cloud/distributed storage.
1import deepDiff from "@fry69/deep-diff";
2
3import * as Output from "~/common/output.js";
4import { BroadcastableDiffuseElement, defineElement, query } from "~/common/element.js";
5import { groupTracksPerScheme } from "~/common/utils.js";
6import { signal } from "~/common/signal.js";
7
8import { DISABLED_KEY } from "./constants.js";
9import { parseDisabledUris, uriKey } from "./common.js";
10
11/**
12 * @import {InputElement, Source} from "~/components/input/types.d.ts"
13 * @import {OutputElement} from "~/components/output/types.d.ts"
14 */
15
16////////////////////////////////////////////
17// ELEMENT
18////////////////////////////////////////////
19
20class Sources extends BroadcastableDiffuseElement {
21 static NAME = "diffuse/orchestrator/sources";
22
23 // SIGNALS
24
25 #sources = signal(/** @type {{ [scheme: string]: Source[] }} */ ({}));
26 #disabled = signal(/** @type {string[]} */ ([]));
27
28 // STATE
29
30 sources = this.#sources.get;
31 disabled = this.#disabled.get;
32
33 #output = signal(/** @type {OutputElement | null} */ (null));
34
35 // METHODS
36
37 /**
38 * Returns whether the given source URI is disabled.
39 * Strips query params before comparing, matching how {@link toggle} stores keys.
40 *
41 * @param {string} uri
42 * @returns {boolean}
43 */
44 isDisabled(uri) {
45 return this.#disabled.get().includes(uriKey(uri));
46 }
47
48 /**
49 * @param {string} uri
50 */
51 async toggle(uri) {
52 const key = uriKey(uri);
53
54 const output = this.#output.value;
55 if (!output) {
56 console.warn("Output element is not available yet.");
57 return;
58 }
59
60 const settings = await Output.data(output.settings);
61 const existing = settings.find((s) => s.key === DISABLED_KEY);
62
63 let disabled = parseDisabledUris(settings);
64
65 if (disabled.includes(key)) {
66 disabled = disabled.filter((u) => u !== key);
67 } else {
68 disabled = [...disabled, key];
69 }
70
71 const value = JSON.stringify(disabled);
72 const updated = existing
73 ? settings.map((s) =>
74 s.key === DISABLED_KEY ? { ...s, value } : s
75 )
76 : [
77 ...settings,
78 {
79 $type: /** @type {"sh.diffuse.output.setting"} */ (
80 "sh.diffuse.output.setting"
81 ),
82 id: crypto.randomUUID(),
83 key: DISABLED_KEY,
84 value,
85 },
86 ];
87
88 await output.settings.save(updated);
89 }
90
91 // LIFECYCLE
92
93 /**
94 * @override
95 */
96 async connectedCallback() {
97 super.connectedCallback();
98
99 /** @type {InputElement} */
100 const input = query(this, "input-selector");
101
102 /** @type {OutputElement} */
103 const output = query(this, "output-selector");
104
105 // Wait until defined
106 await customElements.whenDefined(input.localName);
107 await customElements.whenDefined(output.localName);
108
109 // Signals
110 this.#output.value = output;
111
112 // Effects
113 this.effect(() => {
114 const col = output.settings.collection();
115 if (col.state !== "loaded") { this.#disabled.value = []; return; }
116 this.#disabled.value = parseDisabledUris(col.data);
117 });
118
119 // Single input mode + dependencies
120 const singleInputMode = !!input.SCHEME;
121 const deps =
122 /** @type {{ [k: string]: InputElement }} */ (singleInputMode
123 ? {}
124 : input.dependencies());
125
126 // Effects
127 this.effect(() => {
128 const col = output.tracks.collection();
129 const tracks = col.state === "loaded" ? col.data : [];
130 const groups = groupTracksPerScheme(tracks);
131
132 /** @type {{ [scheme: string]: Source[] }} */
133 const record = {};
134
135 Object.entries(groups).map(([scheme, tracks]) => {
136 /** @type {Source[]} */
137 let sources;
138
139 if (singleInputMode) {
140 if (input.SCHEME === scheme) {
141 sources = input.sources(tracks);
142 } else {
143 sources = [];
144 }
145 } else {
146 const dep = deps[scheme];
147 if (!dep) sources = tracks.map((t) => ({ label: t.uri, uri: t.uri }));
148 else sources = dep.sources(tracks);
149 }
150
151 record[scheme] = sources;
152 });
153
154 if (deepDiff(this.#sources.value, record)) {
155 this.#sources.value = record;
156 }
157 });
158 }
159}
160
161export default Sources;
162
163////////////////////////////////////////////
164// REGISTER
165////////////////////////////////////////////
166
167export const CLASS = Sources;
168export const NAME = "do-sources";
169
170defineElement(NAME, CLASS);