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.

fix: audio rendering, phosphor icons, etc.

+179 -61
+4 -2
_config.ts
··· 64 64 65 65 site.add("/definitions"); 66 66 67 + // PHOSPHOR ICONS 68 + 67 69 function phosphor(path: string) { 68 70 site.remoteFile( 69 71 `styles/vendor/phosphor/${path}`, 70 72 import.meta.resolve(`./node_modules/@phosphor-icons/web/src/${path}`), 71 73 ); 74 + 75 + site.add(`styles/vendor/phosphor/${path}`); 72 76 } 73 77 74 78 phosphor("fill/style.css"); ··· 76 80 phosphor("fill/Phosphor-Fill.ttf"); 77 81 phosphor("fill/Phosphor-Fill.woff"); 78 82 phosphor("fill/Phosphor-Fill.woff2"); 79 - 80 - // PHOSPHOR ICONS 81 83 82 84 // MISC 83 85
+2
src/common/element.js
··· 6 6 import { rpc, workerLink, workerProxy, workerTunnel } from "./worker.js"; 7 7 import { BrowserPostMessageIo } from "./worker/rpc.js"; 8 8 9 + export { keyed } from "lit-html/directives/keyed.js"; 10 + 9 11 /** 10 12 * @import {BroadcastingStatus, ProvisionedWorker, ProvisionedWorkers} from "./element.d.ts" 11 13 * @import {ProxiedActions, Tunnel} from "./worker.d.ts";
+33 -21
src/components/engine/audio/element.js
··· 1 - import { BroadcastableDiffuseElement } from "@common/element.js"; 1 + import { BroadcastableDiffuseElement, keyed } from "@common/element.js"; 2 2 import { computed, signal } from "@common/signal.js"; 3 3 4 4 /** ··· 10 10 //////////////////////////////////////////// 11 11 // CONSTANTS 12 12 //////////////////////////////////////////// 13 - const _SILENT_MP3 = 13 + const SILENT_MP3 = 14 14 "data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV"; 15 15 16 16 //////////////////////////////////////////// ··· 201 201 * @param {RenderArg} _ 202 202 */ 203 203 render({ html }) { 204 + const ids = this.items().map((i) => i.id); 205 + 206 + this.querySelectorAll("de-audio-item").forEach((element) => { 207 + if (ids.includes(element.id)) return; 208 + 209 + const source = element.querySelector("source"); 210 + if (source) source.src = SILENT_MP3; 211 + }); 212 + 204 213 const nodes = this.items().map((audio) => { 205 214 const ip = audio.progress === undefined 206 215 ? "0" 207 216 : JSON.stringify(audio.progress); 208 217 209 - return html` 210 - <de-audio-item 211 - id="${audio.id}" 212 - initial-progress="${ip}" 213 - url="${audio.url}" 214 - ${audio.isPreload ? "preload" : ""} 215 - ${audio.mimeType ? 'mime-type="' + audio.mimeType + '"' : ""} 216 - > 217 - <audio 218 - crossorigin="anonymous" 219 - muted="true" 220 - preload="auto" 218 + return keyed( 219 + audio.id, 220 + html` 221 + <de-audio-item 222 + id="${audio.id}" 223 + initial-progress="${ip}" 224 + url="${audio.url}" 225 + ${audio.isPreload ? "preload" : ""} 226 + ${audio.mimeType ? 'mime-type="' + audio.mimeType + '"' : ""} 221 227 > 222 - <source 223 - src="${audio.url}" 224 - ${audio.mimeType ? 'type="' + audio.mimeType + '"' : ""} 225 - /> 226 - </audio> 227 - </de-audio-item> 228 - `; 228 + <audio 229 + crossorigin="anonymous" 230 + muted="true" 231 + preload="auto" 232 + > 233 + <source 234 + src="${audio.url}" 235 + ${audio.mimeType ? 'type="' + audio.mimeType + '"' : ""} 236 + /> 237 + </audio> 238 + </de-audio-item> 239 + `, 240 + ); 229 241 }); 230 242 231 243 return html`
+2 -1
src/themes/blur/artwork-controller/element.css
··· 163 163 transform-origin: center; 164 164 } 165 165 166 - .controller command { 166 + .controller li { 167 167 cursor: pointer; 168 168 line-height: 0; 169 + list-style: none; 169 170 transition-duration: var(--transition-durition); 170 171 transition-property: opacity; 171 172 }
+137 -37
src/themes/blur/artwork-controller/element.js
··· 5 5 6 6 import { DiffuseElement, query, whenElementsDefined } from "@common/element.js"; 7 7 import { trackArtworkCacheId } from "@common/index.js"; 8 - import { signal } from "@common/signal.js"; 8 + import { computed, signal } from "@common/signal.js"; 9 9 10 10 /** 11 11 * @import {RenderArg} from "@common/element.d.ts" 12 + * @import {Signal} from "@common/signal.d.ts" 12 13 * @import {Track} from "@definitions/types.d.ts" 13 - * @import {AudioStateReadOnly} from "@components/engine/audio/types.d.ts" 14 + * 14 15 * @import {InputElement} from "@components/input/types.d.ts" 15 16 * @import {Artwork} from "@components/processor/artwork/types.d.ts" 17 + * @import AudioEngine from "@components/engine/audio/element.js" 18 + * @import QueueEngine from "@components/engine/queue/element.js" 19 + * @import ArtworkProcessor from "@components/processor/artwork/element.js" 16 20 */ 17 21 18 22 class ArtworkController extends DiffuseElement { 19 23 constructor() { 20 24 super(); 21 - this.attachShadow({ mode: "open" }); 25 + 26 + // Bind event handlers to self 27 + this.next = this.next.bind(this); 28 + this.playPause = this.playPause.bind(this); 29 + this.previous = this.previous.bind(this); 30 + this.seek = this.seek.bind(this); 22 31 } 23 32 33 + // VARIABLES 34 + 35 + /** @type {number | undefined} */ 36 + #isLoadingTimeout = undefined; 37 + 24 38 // SIGNALS 25 39 26 - // #audio = signal(/** @type {AudioStateReadOnly | undefined} */ (undefined)); 27 40 #artwork = signal(/** @type {Artwork[]} */ ([])); 28 41 #artworkColor = signal(/** @type {string | undefined} */ (undefined)); 29 42 #artworkLightMode = signal(false); 30 43 #duration = signal("0:00"); 31 - // isLoading = signal(true); 32 - // isPlaying = signal(false); 33 - // progress = signal(0); 44 + #isLoading = signal(false); 34 45 #time = signal("0:00"); 35 - // volume = signal(0); 46 + 47 + // SIGNALS - DEPENDENCIES 48 + 49 + $artwork = signal(/** @type {ArtworkProcessor | undefined} */ (undefined)); 50 + $audio = signal(/** @type {AudioEngine | undefined} */ (undefined)); 51 + $input = signal(/** @type {InputElement | undefined} */ (undefined)); 52 + $queue = signal(/** @type {QueueEngine | undefined} */ (undefined)); 53 + 54 + // SIGNALS - COMPUTED 55 + 56 + #audio = computed(() => { 57 + const curr = this.$queue.value?.now(); 58 + return curr ? this.$audio.value?.state(curr.id) : undefined; 59 + }); 60 + 61 + #isPlaying = computed(() => { 62 + return !!this.$queue.value?.now() && 63 + this.$audio.value?.isPlaying() === true; 64 + }); 36 65 37 66 // LIFECYCLE 38 67 ··· 42 71 connectedCallback() { 43 72 super.connectedCallback(); 44 73 45 - /** @type {import("@components/processor/artwork/element.js").CLASS} */ 74 + /** @type {ArtworkProcessor} */ 46 75 const artwork = query(this, "artwork-processor-selector"); 47 76 48 - /** @type {import("@components/engine/audio/element.js").CLASS} */ 77 + /** @type {AudioEngine} */ 49 78 const audio = query(this, "audio-engine-selector"); 50 79 51 80 /** @type {InputElement} */ 52 81 const input = query(this, "input-selector"); 53 82 54 - /** @type {import("@components/engine/queue/element.js").CLASS} */ 83 + /** @type {QueueEngine} */ 55 84 const queue = query(this, "queue-engine-selector"); 56 85 57 - this.artwork = artwork; 58 - this.audio = audio; 59 - this.input = input; 60 - this.queue = queue; 86 + this.$artwork.value = artwork; 87 + this.$audio.value = audio; 88 + this.$input.value = input; 89 + this.$queue.value = queue; 61 90 62 91 whenElementsDefined({ audio, artwork, input, queue }).then(() => { 63 92 // Changed artwork based on active queue item. ··· 66 95 this.#setArtwork.bind(this), 67 96 ); 68 97 69 - this.effect(() => { 70 - debouncedChangeArtwork(queue.now()); 71 - }); 98 + // this.effect(() => { 99 + // debouncedChangeArtwork(queue.now()); 100 + // }); 101 + 102 + // this.effect(() => this.#changeArtworkInDOM()); 103 + this.effect(() => this.#formatTimestamps()); 104 + this.effect(() => this.#lightOrDark()); 72 105 73 106 this.effect(() => { 74 - const curr = queue.now(); 75 - const aud = curr ? audio.state(curr.id) : undefined; 107 + const now = !!queue.now(); 108 + const bool = !now || 109 + (now && this.#audio()?.loadingState() !== "loaded"); 76 110 77 - console.log("NOW", curr, aud); 78 - }); 111 + if (this.#isLoadingTimeout) { 112 + clearTimeout(this.#isLoadingTimeout); 113 + } 79 114 80 - this.effect(() => this.#changeArtworkInDOM()); 81 - this.effect(() => this.#formatTimestamps()); 82 - this.effect(() => this.#lightOrDark()); 115 + if (bool) { 116 + this.#isLoadingTimeout = setTimeout( 117 + () => this.#isLoading.value = true, 118 + 2000, 119 + ); 120 + } else { 121 + this.#isLoading.set(false); 122 + } 123 + }); 83 124 }); 84 125 } 85 126 ··· 190 231 191 232 const cacheId = await trackArtworkCacheId(track); 192 233 193 - const resGet = await this.input?.resolve({ method: "GET", uri: track.uri }); 194 - const resHead = await this.input?.resolve({ 234 + const resGet = await this.$input.value?.resolve({ 235 + method: "GET", 236 + uri: track.uri, 237 + }); 238 + 239 + const resHead = await this.$input.value?.resolve({ 195 240 method: "HEAD", 196 241 uri: track.uri, 197 242 }); ··· 213 258 }, 214 259 }; 215 260 216 - const art = await this.artwork?.artwork(request) ?? []; 261 + const art = await this.$artwork.value?.artwork(request) ?? []; 217 262 218 263 console.log("ART", art); 219 264 ··· 226 271 // ⌚️ Time 227 272 //////////////////////////////////////////// 228 273 #formatTimestamps() { 229 - const curr = this.queue?.now?.() ?? undefined; 230 - const audio = curr ? this.audio?.state(curr.id) : undefined; 274 + const curr = this.$queue.value?.now?.() ?? undefined; 275 + const audio = this.#audio(); 231 276 const prog = audio?.progress() ?? 0; 232 277 const dur = curr?.stats?.duration ?? audio?.duration(); 233 278 ··· 270 315 271 316 // EVENTS 272 317 318 + playPause() { 319 + const audioId = this.$queue.value?.now()?.id; 320 + 321 + if (this.#isPlaying() && audioId) { 322 + this.$audio.value?.pause({ audioId }); 323 + } else if (audioId) { 324 + this.$audio.value?.play({ audioId }); 325 + } 326 + } 327 + 328 + previous() { 329 + this.$queue.value?.unshift(); 330 + } 331 + 332 + next() { 333 + this.$queue.value?.shift(); 334 + } 335 + 273 336 /** 274 337 * @param {MouseEvent} event 275 338 */ ··· 278 341 ? /** @type {HTMLProgressElement} */ (event.target) 279 342 : null; 280 343 const percentage = target ? event.offsetX / target.clientWidth : 0; 281 - const audioId = this.queue?.now()?.id; 344 + const audioId = this.$queue.value?.now()?.id; 282 345 283 - if (audioId) this.audio?.seek({ audioId, percentage }); 346 + if (audioId) this.$audio.value?.seek({ audioId, percentage }); 284 347 } 285 348 286 349 // RENDER ··· 291 354 render({ html }) { 292 355 return html` 293 356 <style> 294 - @import "../../../styles/vendor/phosphor/fill/style.css"; 295 357 @import "./element.css"; 296 358 </style> 297 359 ··· 315 377 <section class="controller__inner"> 316 378 <!-- Now playing --> 317 379 <cite> 318 - <strong>Diffuse</strong> 380 + <strong>${this.$queue.value?.now()?.tags?.title || 381 + "Diffuse"}</strong> 319 382 <br /> 320 - <span></span> 383 + <span style="font-style: italic"></span> 321 384 </cite> 322 385 323 386 <!-- Progress --> 324 - <div class="progress"> 325 - <progress max="100" value="${0}"></progress> 387 + <div class="progress" @click="${this.seek}"> 388 + <progress max="100" value="${(this.#audio()?.progress() ?? 389 + 0) * 100}"></progress> 326 390 <div class="timestamps"> 327 391 <time datetime="${this.#time.value}">${this.#time.value}</time> 328 392 <time datetime="${this.#time.value}">${this.#duration ··· 331 395 </div> 332 396 333 397 <!-- Controls --> 398 + <menu> 399 + <!-- previous --> 400 + <li @click="${this.previous}"> 401 + <i class="ph-fill ph-rewind" title="Previous track"></i> 402 + </li> 403 + <!-- loading ... --> 404 + <div 405 + class="animate-bounce menu__loader" 406 + style="display: ${this.#isLoading.value ? `inherit` : `none`};" 407 + > 408 + <i class="ph-fill ph-vinyl-record" title="Loading ..."></i> 409 + </div> 410 + <!-- play --> 411 + <li 412 + @click="${this.playPause}" 413 + style="display: ${!this.#isLoading.value && 414 + !this.#isPlaying() 415 + ? `inline` 416 + : `none`};" 417 + > 418 + <i class="ph-fill ph-play" title="Play"></i> 419 + </li> 420 + <!-- pause --> 421 + <li 422 + @click="${this.playPause}" 423 + style="display: ${!this.#isLoading.value && this.#isPlaying() 424 + ? `inline` 425 + : `none`};" 426 + > 427 + <i class="ph-fill ph-pause" title="Pause"></i> 428 + </li> 429 + <!-- next --> 430 + <li @click="${this.next}"> 431 + <i class="ph-fill ph-fast-forward" title="Next track"></i> 432 + </li> 433 + </menu> 334 434 </section> 335 435 </section> 336 436 </main>
+1
src/themes/blur/artwork-controller/index.vto
··· 3 3 base: ../../../ 4 4 5 5 styles: 6 + - ../../../styles/vendor/phosphor/fill/style.css 6 7 - ../../../styles/base.css 7 8 --- 8 9