forked from
tokono.ma/diffuse
A music player that connects to your cloud/distributed storage.
1import { html, render as litRender } from "lit-html";
2
3import * as Output from "~/common/output.js";
4import foundation from "~/common/foundation.js";
5import { effect } from "~/common/signal.js";
6
7import { SCHEME as SCHEME_DROPBOX } from "~/components/input/dropbox/constants.js";
8import { SCHEME as SCHEME_EPHEMERAL_CACHE } from "~/components/input/ephemeral-cache/constants.js";
9import { SCHEME as SCHEME_HTTPS } from "~/components/input/https/constants.js";
10import { SCHEME as SCHEME_ICECAST } from "~/components/input/icecast/constants.js";
11import { SCHEME as SCHEME_LOCAL } from "~/components/input/local/constants.js";
12import { SCHEME as SCHEME_OPENSUBSONIC } from "~/components/input/opensubsonic/constants.js";
13import { SCHEME as SCHEME_S3 } from "~/components/input/s3/constants.js";
14
15/** @type {Record<string, string>} */
16const SCHEME_NAMES = {
17 [SCHEME_DROPBOX]: "Dropbox",
18 [SCHEME_EPHEMERAL_CACHE]: "Browser storage",
19 [SCHEME_HTTPS]: "HTTPS",
20 [SCHEME_ICECAST]: "Icecast",
21 [SCHEME_LOCAL]: "Local directories & files",
22 [SCHEME_OPENSUBSONIC]: "OpenSubsonic",
23 [SCHEME_S3]: "S3",
24};
25
26foundation.setup({ title: "Sources | Diffuse" });
27
28////////////////////////////////////////////
29// SETUP
30////////////////////////////////////////////
31
32const [inputConfigurator, sourcesOrchestrator, outputOrchestrator, processOrchestrator] =
33 await Promise.all([
34 foundation.configurator.input(),
35 foundation.orchestrator.sources(),
36 foundation.orchestrator.output(),
37 foundation.orchestrator.processTracks({ disableWhenReady: true }),
38 ]);
39
40await Promise.all([
41 customElements.whenDefined(inputConfigurator.localName),
42 customElements.whenDefined(sourcesOrchestrator.localName),
43 customElements.whenDefined(outputOrchestrator.localName),
44]);
45
46
47////////////////////////////////////////////
48// PROCESS BUTTON
49////////////////////////////////////////////
50
51const processBtn = /** @type {HTMLButtonElement} */ (document.querySelector("#process-btn"));
52const processIcon = /** @type {HTMLElement} */ (document.querySelector("#process-icon"));
53const processLabel = /** @type {HTMLElement} */ (document.querySelector("#process-label"));
54
55effect(() => {
56 const isProcessing = processOrchestrator.isProcessing();
57
58 processBtn.disabled = isProcessing;
59 processIcon.className = isProcessing
60 ? "ph-fill ph-arrows-clockwise animate-spin"
61 : "ph-fill ph-arrows-clockwise";
62 processLabel.textContent = isProcessing ? "Processing" : "Process";
63});
64
65processBtn.addEventListener("click", async () => {
66 const output = await foundation.orchestrator.output();
67 await Output.data(output.tracks);
68 await processOrchestrator.process();
69});
70
71////////////////////////////////////////////
72// UI
73////////////////////////////////////////////
74
75const list =
76 /** @type {HTMLElement} */ (document.querySelector("#sources-list"));
77const empty =
78 /** @type {HTMLElement} */ (document.querySelector("#sources-empty"));
79
80/** @param {string} uri */
81const trackPrefix = (uri) => { const q = uri.indexOf("?"); return q === -1 ? uri : uri.slice(0, q); };
82
83effect(() => {
84 const sourcesRecord = sourcesOrchestrator.sources();
85
86 const tracksCol = outputOrchestrator.tracks.collection();
87 const tracks = tracksCol.state === "loaded" ? tracksCol.data : [];
88
89 const entries = Object.entries(sourcesRecord).filter(
90 ([, sources]) => sources.length > 0,
91 );
92
93 list.hidden = entries.length === 0;
94 empty.hidden = entries.length > 0;
95
96 litRender(
97 html`
98 ${entries.map(([scheme, sources]) => {
99 if (scheme === SCHEME_EPHEMERAL_CACHE) {
100 const uri = `${SCHEME_EPHEMERAL_CACHE}://`;
101 const isDisabled = sourcesOrchestrator.isDisabled(uri);
102 const trackCount = tracks.filter((t) =>
103 t.uri.startsWith(uri)
104 ).length;
105 return html`
106 <li class="sources-scheme">${SCHEME_NAMES[scheme] ?? scheme}</li>
107 <li class="sources-item ${isDisabled
108 ? "sources-item--disabled"
109 : ""}">
110 <div class="sources-item__info">
111 <span class="sources-item__name">Files stored in the browser</span>
112 <span class="sources-item__detail">${trackCount} track${trackCount ===
113 1
114 ? ""
115 : "s"}</span>
116 </div>
117 <button
118 class="button--plain"
119 title="${isDisabled ? "Enable source" : "Disable source"}"
120 @click="${() => sourcesOrchestrator.toggle(uri)}"
121 >
122 <i class="ph-fill ${isDisabled
123 ? "ph-eye-slash"
124 : "ph-eye"}"></i>
125 </button>
126 <button
127 class="button--plain button--icon"
128 title="Remove source"
129 @click="${() => removeEphemeralSources()}"
130 >
131 <i class="ph-fill ph-skull"></i>
132 </button>
133 </li>
134 `;
135 }
136
137 return html`
138 <li class="sources-scheme">${SCHEME_NAMES[scheme] ?? scheme}</li>
139 ${sources.map(({ label, uri }) => {
140 const isDisabled = sourcesOrchestrator.isDisabled(uri);
141 const trackCount = tracks.filter((t) =>
142 t.uri.startsWith(trackPrefix(uri))
143 ).length;
144 return html`
145 <li class="sources-item ${isDisabled
146 ? "sources-item--disabled"
147 : ""}">
148 <div class="sources-item__info">
149 <span class="sources-item__name">${label}</span>
150 <span class="sources-item__detail">${trackCount} track${trackCount ===
151 1
152 ? ""
153 : "s"}</span>
154 </div>
155 <button
156 class="button--plain button--icon"
157 title="${isDisabled ? "Enable source" : "Disable source"}"
158 @click="${() => sourcesOrchestrator.toggle(uri)}"
159 >
160 <i class="ph-fill ${isDisabled
161 ? "ph-eye-slash"
162 : "ph-eye"}"></i>
163 </button>
164 <button
165 class="button--plain button--icon"
166 title="Remove source"
167 @click="${() => removeSource(uri)}"
168 >
169 <i class="ph-fill ph-skull"></i>
170 </button>
171 </li>
172 `;
173 })}
174 `;
175 })}
176 `,
177 list,
178 );
179});
180
181////////////////////////////////////////////
182// ACTIONS
183////////////////////////////////////////////
184
185async function removeEphemeralSources() {
186 return removeSource(SCHEME_EPHEMERAL_CACHE);
187}
188
189/** @param {string} uri */
190async function removeSource(uri) {
191 const tracks = await Output.data(outputOrchestrator.tracks);
192
193 const detachedTracks = await inputConfigurator.detach({
194 fileUriOrScheme: uri,
195 tracks,
196 });
197
198 if (detachedTracks) await outputOrchestrator.tracks.save(detachedTracks);
199}
200
201////////////////////////////////////////////
202// 🚀
203////////////////////////////////////////////
204
205foundation.ready();