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

Configure Feed

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

chore: some webamp input configurator improvements + refactoring

+217 -47
+1 -1
.env
··· 1 - DISABLE_AUTOMATIC_TRACKS_PROCESSING=t 1 + #DISABLE_AUTOMATIC_TRACKS_PROCESSING=t
+6 -1
deno.jsonc
··· 49 49 "lume/": "https://cdn.jsdelivr.net/gh/lumeland/lume@3.1.4/", 50 50 "lume/jsx-runtime": "https://cdn.jsdelivr.net/gh/oscarotero/ssx@0.1.14/jsx-runtime.ts", 51 51 }, 52 - "exports": {}, 52 + "exports": { 53 + "./common/*": "./src/common/*", 54 + "./components/*": "./src/components/*", 55 + "./definitions/*": "./src/definitions/*", 56 + "./themes/*": "./src/themes/*", 57 + }, 53 58 "tasks": { 54 59 "build": { 55 60 "description": "Build the site for production",
src/common/index.js src/common/utils.js
+1 -1
src/components/configurator/input/worker.js
··· 1 1 import * as URI from "uri-js"; 2 2 3 - import { groupTracksPerScheme } from "@common/index.js"; 3 + import { groupTracksPerScheme } from "@common/utils.js"; 4 4 import { ostiary, rpc, workerProxy } from "@common/worker.js"; 5 5 6 6 /**
+1 -1
src/components/engine/queue/element.js
··· 1 1 import { DiffuseElement } from "@common/element.js"; 2 2 import { signal } from "@common/signal.js"; 3 3 import { listen } from "@common/worker.js"; 4 - import { hash } from "@common/index.js"; 4 + import { hash } from "@common/utils.js"; 5 5 6 6 /** 7 7 * @import {ProxiedActions} from "@common/worker.d.ts";
+1 -1
src/components/engine/queue/worker.js
··· 1 1 import { announce, ostiary, rpc } from "@common/worker.js"; 2 2 import { effect, signal } from "@common/signal.js"; 3 - import { arrayShuffle, hash } from "@common/index.js"; 3 + import { arrayShuffle, hash } from "@common/utils.js"; 4 4 5 5 /** 6 6 * @import {Actions, Item} from "./types.d.ts"
+50 -7
src/components/input/opensubsonic/element.js
··· 1 1 import { DiffuseElement } from "@common/element.js"; 2 + import { computed, signal } from "@common/signal.js"; 3 + import { listen } from "@common/worker.js"; 2 4 import { SCHEME } from "./constants.js"; 3 5 4 6 /** 5 7 * @import {InputActions, InputSchemeProvider} from "@components/input/types.d.ts" 6 8 * @import {ProxiedActions} from "@common/worker.d.ts" 9 + * 10 + * @import {Server, State} from "./types.d.ts" 7 11 */ 8 12 9 13 //////////////////////////////////////////// ··· 23 27 constructor() { 24 28 super(); 25 29 26 - /** @type {ProxiedActions<InputActions>} */ 27 - const p = this.workerProxy(); 30 + /** @type {ProxiedActions<InputActions & State>} */ 31 + this.proxy = this.workerProxy(); 32 + 33 + this.consult = this.proxy.consult; 34 + this.contextualize = this.proxy.contextualize; 35 + this.groupConsult = this.proxy.groupConsult; 36 + this.list = this.proxy.list; 37 + this.resolve = this.proxy.resolve; 38 + } 39 + 40 + // SIGNALS 41 + 42 + #servers = signal(/** @type {Record<string, Server>} */ ({})); 43 + 44 + // STATE 45 + 46 + servers = this.#servers.get; 47 + 48 + // LIFECYCLE 49 + 50 + /** 51 + * @override 52 + */ 53 + connectedCallback() { 54 + super.connectedCallback(); 28 55 29 - this.consult = p.consult; 30 - this.contextualize = p.contextualize; 31 - this.groupConsult = p.groupConsult; 32 - this.list = p.list; 33 - this.resolve = p.resolve; 56 + // Sync data with worker 57 + const link = this.workerLink(); 58 + 59 + // Listen for remote data changes 60 + listen("servers", this.#servers.set, link); 61 + 62 + // Fetch current data state 63 + this.proxy.servers().then(this.#servers.set); 34 64 } 65 + 66 + // 🛠️ 67 + 68 + serverList = computed(() => { 69 + const servers = this.#servers.value; 70 + 71 + return Object.values(servers).map((server) => { 72 + return { 73 + label: `${server.host} (${server.username ?? server.apiKey})`, 74 + server, 75 + }; 76 + }); 77 + }); 35 78 } 36 79 37 80 export default OpensubsonicInput;
+6
src/components/input/opensubsonic/types.d.ts
··· 1 + import type { SignalReader } from "@common/signal.d.ts"; 2 + 1 3 // https://opensubsonic.netlify.app/docs/api-reference/ 2 4 export type Server = { 3 5 apiKey?: string; ··· 6 8 tls: boolean; 7 9 username?: string; 8 10 }; 11 + 12 + export type State = { 13 + servers: SignalReader<Record<string, Server>>; 14 + };
+51 -8
src/components/input/s3/element.js
··· 1 1 import { DiffuseElement } from "@common/element.js"; 2 2 import { SCHEME } from "./constants.js"; 3 + import { computed, signal } from "@common/signal.js"; 4 + import { listen } from "@common/worker.js"; 3 5 4 6 /** 5 7 * @import {InputActions, InputSchemeProvider} from "@components/input/types.d.ts" 6 8 * @import {ProxiedActions} from "@common/worker.d.ts" 9 + * 10 + * @import {Bucket, State} from "./types.d.ts" 7 11 */ 8 12 9 13 //////////////////////////////////////////// ··· 23 27 constructor() { 24 28 super(); 25 29 26 - /** @type {ProxiedActions<InputActions & { demo: () => Promise<void> }>} */ 27 - const p = this.workerProxy(); 30 + /** @type {ProxiedActions<InputActions & State & { demo: () => Promise<void> }>} */ 31 + this.proxy = this.workerProxy(); 32 + 33 + this.consult = this.proxy.consult; 34 + this.contextualize = this.proxy.contextualize; 35 + this.groupConsult = this.proxy.groupConsult; 36 + this.list = this.proxy.list; 37 + this.resolve = this.proxy.resolve; 38 + 39 + this.demo = this.proxy.demo; 40 + } 41 + 42 + // SIGNALS 43 + 44 + #buckets = signal(/** @type {Record<string, Bucket>} */ ({})); 45 + 46 + // STATE 47 + 48 + buckets = this.#buckets.get; 49 + 50 + // LIFECYCLE 51 + 52 + /** 53 + * @override 54 + */ 55 + connectedCallback() { 56 + super.connectedCallback(); 28 57 29 - this.consult = p.consult; 30 - this.contextualize = p.contextualize; 31 - this.groupConsult = p.groupConsult; 32 - this.list = p.list; 33 - this.resolve = p.resolve; 58 + // Sync data with worker 59 + const link = this.workerLink(); 60 + 61 + // Listen for remote data changes 62 + listen("buckets", this.#buckets.set, link); 34 63 35 - this.demo = p.demo; 64 + // Fetch current data state 65 + this.proxy.buckets().then(this.#buckets.set); 36 66 } 67 + 68 + // 🛠️ 69 + 70 + bucketList = computed(() => { 71 + const buckets = this.#buckets.value; 72 + 73 + return Object.values(buckets).map((bucket) => { 74 + return { 75 + label: `${bucket.bucketName} (${bucket.accessKey}, ${bucket.host})`, 76 + bucket, 77 + }; 78 + }); 79 + }); 37 80 } 38 81 39 82 export default S3Input;
+6
src/components/input/s3/types.d.ts
··· 1 + import type { SignalReader } from "@common/signal.d.ts"; 2 + 1 3 export type Bucket = { 2 4 accessKey: string; 3 5 bucketName: string; ··· 6 8 region: string; 7 9 secretKey: string; 8 10 }; 11 + 12 + export type State = { 13 + buckets: SignalReader<Record<string, Bucket>>; 14 + };
+12 -5
src/components/orchestrator/process-tracks/element.js
··· 77 77 // Process whenever tracks are initially loaded 78 78 if (this.hasAttribute("process-when-ready")) { 79 79 this.effect(() => { 80 - const skip = 81 - /** @type {any} */ (import.meta).env 82 - ?.DISABLE_AUTOMATIC_TRACKS_PROCESSING ?? false; 83 - if (skip) return; 84 - 85 80 const state = output.tracks.state(); 86 81 if (state !== "loaded") return; 82 + 83 + const skip = /** @type {any} */ (import.meta).env 84 + ?.DISABLE_AUTOMATIC_TRACKS_PROCESSING ?? false; 85 + if (skip) { 86 + // Should still trigger contextualize which `process` normally does for us. 87 + untracked(() => { 88 + input.contextualize( 89 + output.tracks.collection(), 90 + ); 91 + }); 92 + return; 93 + } 87 94 88 95 untracked(() => this.process()); 89 96 });
+1 -1
src/themes/blur/artwork-controller/element.js
··· 11 11 whenElementsDefined, 12 12 } from "@common/element.js"; 13 13 14 - import { trackArtworkCacheId } from "@common/index.js"; 14 + import { trackArtworkCacheId } from "@common/utils.js"; 15 15 import { computed, signal, untracked } from "@common/signal.js"; 16 16 17 17 /**
+81 -21
src/themes/webamp/configurators/input/element.js
··· 1 - import { DiffuseElement, query, whenElementsDefined } from "@common/element.js"; 1 + import { 2 + DiffuseElement, 3 + nothing, 4 + query, 5 + whenElementsDefined, 6 + } from "@common/element.js"; 2 7 import { signal } from "@common/signal.js"; 3 8 4 9 import { buildURI as buildOpenSubsonicURI } from "@components/input/opensubsonic/common.js"; ··· 58 63 /** 59 64 * @param {Event} event 60 65 */ 61 - #addOpenSubsonicServer = (event) => { 66 + #addOpenSubsonicServer = async (event) => { 62 67 event.preventDefault(); 68 + 69 + /** @type {HTMLButtonElement | null} */ 70 + const button = this.root().querySelector("#opensubsonic-submit"); 71 + if (button) button.disabled = true; 63 72 64 73 const host = this.formElement("opensubsonic-host")?.value; 65 74 const tls = this.formElement("opensubsonic-tls")?.value === "true"; ··· 81 90 }; 82 91 83 92 const uri = buildOpenSubsonicURI(server); 84 - return this.addSource(uri); 93 + await this.addSource(uri); 94 + 95 + if (button) button.disabled = false; 85 96 }; 86 97 87 98 /** 88 99 * @param {Event} event 89 100 */ 90 - #addS3Bucket = (event) => { 101 + #addS3Bucket = async (event) => { 91 102 event.preventDefault(); 92 103 104 + /** @type {HTMLButtonElement | null} */ 105 + const button = this.root().querySelector("#opensubsonic-submit"); 106 + if (button) button.disabled = true; 107 + 93 108 const accessKey = this.formElement("s3-access-key")?.value; 94 109 const bucketName = this.formElement("s3-bucket-name")?.value; 95 - const host = this.formElement("s3-host")?.value ?? "s3.amazonaws.com"; 96 - const path = this.formElement("s3-path")?.value ?? "/"; 97 - const region = this.formElement("s3-region")?.value ?? "us-east-1"; 110 + const host = this.formElement("s3-host")?.value; 111 + const path = this.formElement("s3-path")?.value; 112 + const region = this.formElement("s3-region")?.value; 98 113 const secretKey = this.formElement("s3-secret-key")?.value; 99 114 100 - if (!accessKey) throw new Error("Missing required `accessKey` input value"); 115 + if (!accessKey) { 116 + throw new Error("Missing required `accessKey` input value"); 117 + } 101 118 if (!bucketName) { 102 119 throw new Error("Missing required `bucketName` input value"); 103 120 } 104 - if (!secretKey) throw new Error("Missing required `secretKey` input value"); 121 + if (!secretKey) { 122 + throw new Error("Missing required `secretKey` input value"); 123 + } 105 124 106 125 /** @type {S3Bucket} */ 107 126 const bucket = { 108 127 accessKey, 109 128 bucketName, 110 - host, 111 - path, 112 - region, 129 + host: host?.length ? host : "s3.amazonaws.com", 130 + path: path?.length ? path : "/", 131 + region: region?.length ? region : "us-east-1", 113 132 secretKey, 114 133 }; 115 134 116 135 const uri = buildS3cURI(bucket); 117 - return this.addSource(uri); 136 + await this.addSource(uri); 137 + 138 + if (button) button.disabled = false; 118 139 }; 119 140 120 141 // 🛠️ ··· 122 143 /** 123 144 * @param {string} uri 124 145 */ 125 - addSource(uri) { 146 + async addSource(uri) { 126 147 /** @type {Track} */ 127 148 const track = { 128 149 $type: "sh.diffuse.output.tracks", ··· 134 155 const output = this.$output.value; 135 156 if (!output) throw new Error("Output isn't ready yet!"); 136 157 137 - output.tracks.save( 158 + await output.tracks.save( 138 159 [...output.tracks.collection(), track], 139 160 ); 140 161 } 141 162 163 + // 🔮 164 + 165 + openSubsonicServers() { 166 + const input = document.querySelector("di-opensubsonic"); 167 + return input 168 + ? /** @type {import("@components/input/opensubsonic/element.js").CLASS} */ (input) 169 + .serverList() 170 + : []; 171 + } 172 + 173 + s3Buckets() { 174 + const input = document.querySelector("di-s3"); 175 + return input 176 + ? /** @type {import("@components/input/s3/element.js").CLASS} */ (input) 177 + .bucketList() 178 + : []; 179 + } 180 + 142 181 /** 143 182 * @param {string} id 144 183 * @returns {HTMLInputElement | null} ··· 153 192 * @param {RenderArg} _ 154 193 */ 155 194 render({ html }) { 195 + const opensubsonicList = this.openSubsonicServers(); 196 + const s3List = this.s3Buckets(); 197 + 156 198 return html` 157 199 <link rel="stylesheet" href="styles/vendor/98.css" /> 158 200 ··· 235 277 <div class="window-body" id="opensubsonic-contents"> 236 278 <fieldset> 237 279 <legend>Added servers</legend> 238 - 239 - <p>TODO</p> 280 + ${this.renderList(html, opensubsonicList)} 240 281 </fieldset> 241 282 242 283 <form @submit="${this.#addOpenSubsonicServer}"> ··· 285 326 </fieldset> 286 327 287 328 <p> 288 - <input type="submit" value="Add server" /> 329 + <button type="submit" id="opensubsonic-submit">Add server</button> 289 330 </p> 290 331 </form> 291 332 </div> ··· 294 335 <div class="window-body" id="s3-contents"> 295 336 <fieldset> 296 337 <legend>Added buckets</legend> 297 - 298 - <p>TODO</p> 338 + ${this.renderList(html, s3List)} 299 339 </fieldset> 300 340 301 341 <form @submit="${this.#addS3Bucket}"> ··· 346 386 </fieldset> 347 387 348 388 <p> 349 - <input type="submit" value="Add bucket" /> 389 + <button type="submit" id="s3-submit">Add bucket</button> 350 390 </p> 351 391 </form> 352 392 </div> 353 393 </div> 354 394 </div> 355 395 `; 396 + } 397 + 398 + /** 399 + * @param {RenderArg["html"]} html 400 + * @param {Array<{ label: string}>} list 401 + */ 402 + renderList(html, list) { 403 + return list.length 404 + ? html` 405 + <ul class="tree-view"> 406 + ${list.map((item) => { 407 + return html` 408 + <li> 409 + ${item.label} 410 + </li> 411 + `; 412 + })} 413 + </ul> 414 + ` 415 + : nothing; 356 416 } 357 417 } 358 418