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