A music player that connects to your cloud/distributed storage.
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

at v4 260 lines 8.9 kB view raw
1import { html, render as litRender } from "lit-html"; 2 3import * as Output from "~/common/output.js"; 4import foundation from "~/common/foundation.js"; 5import { effect, signal } 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 [ 33 inputConfigurator, 34 sourcesOrchestrator, 35 outputOrchestrator, 36 processOrchestrator, 37] = await Promise.all([ 38 foundation.configurator.input(), 39 foundation.orchestrator.sources(), 40 foundation.orchestrator.output(), 41 foundation.orchestrator.processTracks({ disableWhenReady: true }), 42]); 43 44await Promise.all([ 45 customElements.whenDefined(inputConfigurator.localName), 46 customElements.whenDefined(sourcesOrchestrator.localName), 47 customElements.whenDefined(outputOrchestrator.localName), 48]); 49 50//////////////////////////////////////////// 51// PROCESS BUTTON 52//////////////////////////////////////////// 53 54const processBtn = 55 /** @type {HTMLButtonElement} */ (document.querySelector("#process-btn")); 56const processIcon = 57 /** @type {HTMLElement} */ (document.querySelector("#process-icon")); 58const processLabel = 59 /** @type {HTMLElement} */ (document.querySelector("#process-label")); 60 61effect(() => { 62 const isProcessing = processOrchestrator.isProcessing(); 63 const { processed, total } = processOrchestrator.progress(); 64 const pct = total > 0 ? Math.round((processed / total) * 100) : null; 65 66 processBtn.disabled = isProcessing; 67 processIcon.className = isProcessing 68 ? "ph-fill ph-arrows-clockwise animate-spin" 69 : "ph-fill ph-arrows-clockwise"; 70 processLabel.textContent = isProcessing 71 ? (pct !== null ? `Processing (${pct}%)` : "Listing") 72 : "Process"; 73}); 74 75processBtn.addEventListener("click", async () => { 76 const output = await foundation.orchestrator.output(); 77 await Output.data(output.tracks); 78 await processOrchestrator.process(); 79}); 80 81//////////////////////////////////////////// 82// UI 83//////////////////////////////////////////// 84 85const list = 86 /** @type {HTMLElement} */ (document.querySelector("#sources-list")); 87const empty = 88 /** @type {HTMLElement} */ (document.querySelector("#sources-empty")); 89 90/** @param {string} uri */ 91const trackPrefix = (uri) => { 92 const q = uri.indexOf("?"); 93 return q === -1 ? uri : uri.slice(0, q); 94}; 95 96//////////////////////////////////////////// 97// ONLINE STATUS 98//////////////////////////////////////////// 99 100 101const onlineMap = signal(/** @type {Record<string, boolean | null>} */ ({})); 102 103/** @param {{ [scheme: string]: import("~/components/input/types.d.ts").Source[] }} sourcesRecord */ 104async function checkOnlineStatus(sourcesRecord) { 105 const sources = Object.values(sourcesRecord).flat(); 106 const entries = await Promise.all( 107 sources.map(async ({ uri }) => { 108 const result = await inputConfigurator.consult(uri); 109 const online = 110 result.supported && result.consult !== "undetermined" 111 ? result.consult 112 : null; 113 return /** @type {[string, boolean | null]} */ ([trackPrefix(uri), online]); 114 }), 115 ); 116 onlineMap.value = Object.fromEntries(entries); 117} 118 119effect(() => { 120 checkOnlineStatus(sourcesOrchestrator.sources()); 121}); 122 123effect(() => { 124 const sourcesRecord = sourcesOrchestrator.sources(); 125 const statusMap = onlineMap.get(); 126 127 const tracksCol = outputOrchestrator.tracks.collection(); 128 const tracks = tracksCol.state === "loaded" ? tracksCol.data : []; 129 130 /** @param {string} uri */ 131 const statusClass = (uri) => { 132 const status = statusMap[trackPrefix(uri)]; 133 if (status === true) return "sources-item__status--online"; 134 if (status === false) return "sources-item__status--offline"; 135 return "sources-item__status--unknown"; 136 }; 137 138 /** @param {string} uri */ 139 const statusTitle = (uri) => { 140 const status = statusMap[trackPrefix(uri)]; 141 if (status === true) return "Online"; 142 if (status === false) return "Offline"; 143 return "Status unknown"; 144 }; 145 146 const entries = Object.entries(sourcesRecord).filter( 147 ([, sources]) => sources.length > 0, 148 ); 149 150 list.hidden = entries.length === 0; 151 empty.hidden = entries.length > 0; 152 153 litRender( 154 html` 155 ${entries.map(([scheme, sources]) => { 156 if (scheme === SCHEME_EPHEMERAL_CACHE) { 157 const uri = `${SCHEME_EPHEMERAL_CACHE}://`; 158 const isDisabled = sourcesOrchestrator.isDisabled(uri); 159 const trackCount = tracks.filter((t) => t.uri.startsWith(uri)).length; 160 return html` 161 <li class="sources-scheme">${SCHEME_NAMES[scheme] ?? scheme}</li> 162 <li class="sources-item ${isDisabled 163 ? "sources-item--disabled" 164 : ""}"> 165 <div class="sources-item__info"> 166 <span class="sources-item__name">Files stored in the browser</span> 167 <span class="sources-item__detail"> 168 <span class="sources-item__status ${statusClass(uri)}" title="${statusTitle(uri)}"></span> 169 ${trackCount} track${trackCount === 1 ? "" : "s"} 170 </span> 171 </div> 172 <button 173 class="button--plain" 174 title="${isDisabled ? "Enable source" : "Disable source"}" 175 @click="${() => sourcesOrchestrator.toggle(uri)}" 176 > 177 <i class="ph-fill ${isDisabled 178 ? "ph-eye-slash" 179 : "ph-eye"}"></i> 180 </button> 181 <button 182 class="button--plain button--icon" 183 title="Remove source" 184 @click="${() => removeEphemeralSources()}" 185 > 186 <i class="ph-fill ph-skull"></i> 187 </button> 188 </li> 189 `; 190 } 191 192 return html` 193 <li class="sources-scheme">${SCHEME_NAMES[scheme] ?? scheme}</li> 194 ${sources.map(({ label, uri }) => { 195 const isDisabled = sourcesOrchestrator.isDisabled(uri); 196 const trackCount = tracks.filter((t) => 197 t.uri.startsWith(trackPrefix(uri)) 198 ).length; 199 return html` 200 <li class="sources-item ${isDisabled 201 ? "sources-item--disabled" 202 : ""}"> 203 <div class="sources-item__info"> 204 <span class="sources-item__name">${label}</span> 205 <span class="sources-item__detail"> 206 <span class="sources-item__status ${statusClass(uri)}" title="${statusTitle(uri)}"></span> 207 ${trackCount} track${trackCount === 1 ? "" : "s"} 208 </span> 209 </div> 210 <button 211 class="button--plain button--icon" 212 title="${isDisabled ? "Enable source" : "Disable source"}" 213 @click="${() => sourcesOrchestrator.toggle(uri)}" 214 > 215 <i class="ph-fill ${isDisabled 216 ? "ph-eye-slash" 217 : "ph-eye"}"></i> 218 </button> 219 <button 220 class="button--plain button--icon" 221 title="Remove source" 222 @click="${() => removeSource(uri)}" 223 > 224 <i class="ph-fill ph-skull"></i> 225 </button> 226 </li> 227 `; 228 })} 229 `; 230 })} 231 `, 232 list, 233 ); 234}); 235 236//////////////////////////////////////////// 237// ACTIONS 238//////////////////////////////////////////// 239 240async function removeEphemeralSources() { 241 return removeSource(SCHEME_EPHEMERAL_CACHE); 242} 243 244/** @param {string} uri */ 245async function removeSource(uri) { 246 const tracks = await Output.data(outputOrchestrator.tracks); 247 248 const detachedTracks = await inputConfigurator.detach({ 249 fileUriOrScheme: uri, 250 tracks, 251 }); 252 253 if (detachedTracks) await outputOrchestrator.tracks.save(detachedTracks); 254} 255 256//////////////////////////////////////////// 257// 🚀 258//////////////////////////////////////////// 259 260foundation.ready();