Experiment to rebuild Diffuse using web applets.
0
fork

Configure Feed

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

feat: improve audio volume management

+38 -47
+12 -41
src/pages/constituents/blur/artwork-controller/_applet.astro
··· 90 90 91 91 .controller { 92 92 flex-shrink: 0; 93 - padding: var(--space-md); 93 + padding: 0 var(--space-md) var(--space-md); 94 94 position: relative; 95 95 } 96 96 ··· 184 184 185 185 .gradient-blur { 186 186 bottom: 0; 187 - height: 125%; 187 + height: 150%; 188 188 left: 0; 189 189 pointer-events: none; 190 190 position: absolute; ··· 308 308 309 309 import type { Artwork } from "@applets/processor/artwork/types"; 310 310 311 - // Types 312 - type State = { 313 - volume?: number; 314 - }; 315 - 316 311 // Register 317 312 const context = register(); 318 313 319 - // Stored state 320 - const STORE_PREFIX = `@applets/constituents/blur/artwork-controller/${context.groupId || "main"}`; 321 - const STATE_KEY = `${STORE_PREFIX}/state`; 322 - const stored = localStorage.getItem(STATE_KEY); 323 - 324 - let state: State = Object.freeze(stored ? JSON.parse(stored) : {}); 325 - 326 - function updateState(partial: Partial<State>) { 327 - state = Object.freeze({ ...state, ...partial }); 328 - localStorage.setItem(STATE_KEY, JSON.stringify(state)); 329 - } 330 - 331 314 // Signals 332 315 const [activeTrack, setActiveTrack] = signal<Track | undefined>(undefined); 333 316 const [artwork, setArtwork] = signal<Artwork[]>([]); ··· 336 319 const [isPlaying, setIsPlaying] = signal<boolean>(false); 337 320 const [progress, setProgress] = signal<number>(0); 338 321 const [time, setTime] = signal<string>("0:00"); 339 - const [volume, setVolume] = signal<number>(state.volume || 0.5); 322 + const [volume, setVolume] = signal<number>(0); 340 323 341 324 // Applet connections 342 325 const configurator = { ··· 380 363 largestUnit: "hours", 381 364 }); 382 365 383 - // const diff = 384 - 385 366 setTime(formatTime(p)); 386 367 setDuration(formatTime(d)); 387 368 } else { ··· 397 378 } 398 379 399 380 //////////////////////////////////////////// 400 - // ✨ EFFECTS 401 - // 🔊 Volume 402 - //////////////////////////////////////////// 403 - effect(() => { 404 - // Save volume in local state store 405 - updateState({ volume: volume() }); 406 - }); 407 - 408 - //////////////////////////////////////////// 409 381 // 🔊 AUDIO 410 382 //////////////////////////////////////////// 411 383 412 384 reactive( 413 385 engine.audio, 414 386 (data) => data.items[engine.queue.data.now?.id ?? Infinity]?.isPlaying ?? false, 415 - (isPlaying) => throttled(() => setIsPlaying(isPlaying))(), 387 + (isPlaying) => setTimeout(() => setIsPlaying(isPlaying), 0), 416 388 ); 417 389 418 390 reactive( 419 391 engine.audio, 420 392 (data) => data.items[engine.queue.data.now?.id ?? Infinity]?.progress ?? 0, 421 - setProgress, 393 + (progress) => setTimeout(() => setProgress(progress), 0), 394 + ); 395 + 396 + reactive( 397 + engine.audio, 398 + (data) => data.volume.default, 399 + (volume) => setTimeout(() => setVolume(volume), 0), 422 400 ); 423 401 424 402 //////////////////////////////////////////// ··· 515 493 // UI ░ NOW PLAYING 516 494 //////////////////////////////////////////// 517 495 518 - // effect(() => { 519 - // const track = activeTrack() 520 - // }) 521 - 522 496 const NowPlaying = h("cite", {}, [ 523 497 h("strong", {}, text(computed(() => activeTrack()?.tags?.title || "Diffuse"))), 524 498 tags.br(), ··· 631 605 "progress", 632 606 computed(() => ({ 633 607 max: "100", 634 - value: volume() * 100, 608 + value: (volume() * 100).toString(), 635 609 })), 636 610 ), 637 611 ], ··· 645 619 646 620 function volumeClickHandler(event: MouseEvent) { 647 621 const percentage = event.offsetX / (event.target as HTMLProgressElement).clientWidth; 648 - setVolume(percentage); 649 622 engine.audio.sendAction("volume", { volume: percentage }); 650 623 } 651 624 652 625 function fullVolume() { 653 - setVolume(1); 654 626 engine.audio.sendAction("volume", { volume: 1 }); 655 627 } 656 628 657 629 function mute() { 658 - setVolume(0); 659 630 engine.audio.sendAction("volume", { volume: 0 }); 660 631 } 661 632
+23 -1
src/pages/engine/audio/_applet.astro
··· 1 1 <script> 2 + import { effect, signal } from "spellcaster"; 3 + 2 4 import type { State, Audio, AudioState } from "./types"; 3 5 import { register } from "@scripts/applets/common"; 6 + import type { AppletEvent } from "@web-applets/sdk"; 4 7 5 8 //////////////////////////////////////////// 6 9 // CONSTANTS ··· 18 21 container.id = "container"; 19 22 document.body.appendChild(container); 20 23 24 + // Default volume 25 + const VOLUME_KEY = `@applets/engine/audio/${context.groupId || "main"}/volume`; 26 + const vol = localStorage.getItem(VOLUME_KEY); 27 + 21 28 // Initial state 22 29 context.data = { 23 30 items: {}, 31 + volume: { 32 + default: vol ? parseFloat(vol) : 0.5, 33 + }, 24 34 }; 25 35 26 36 // State helpers ··· 38 48 }); 39 49 } 40 50 51 + // Effects 52 + const [defaultVolume, setDefaultVolume] = signal<number | undefined>(undefined); 53 + context.scope.ondata = (event: AppletEvent) => setDefaultVolume(event.data.volume.default); 54 + 55 + effect(() => { 56 + const volume = defaultVolume(); 57 + if (volume === undefined) return; 58 + localStorage.setItem(VOLUME_KEY, volume.toString()); 59 + }); 60 + 41 61 //////////////////////////////////////////// 42 62 // ACTIONS 43 63 //////////////////////////////////////////// ··· 54 74 55 75 function play({ audioId, volume }: { audioId: string; volume?: number }) { 56 76 withAudioNode(audioId, (audio) => { 57 - audio.volume = volume ?? 0.5; 77 + audio.volume = volume ?? context.data.volume.default; 58 78 audio.muted = false; 59 79 60 80 if (audio.readyState === 0) audio.load(); ··· 110 130 } 111 131 112 132 function volume(args: { audioId?: string; volume: number }) { 133 + if (!args.audioId) update({ volume: { default: args.volume } }); 134 + 113 135 Array.from(container.querySelectorAll("audio")).forEach((node) => { 114 136 const audio = node as HTMLAudioElement; 115 137 if (audio.getAttribute("data-is-preload") === "true") return;
+1 -1
src/pages/engine/audio/_manifest.json
··· 112 112 }, 113 113 "volume": { 114 114 "title": "Volume", 115 - "description": "Set the volume of all audio, or a specific audio node.", 115 + "description": "Set the volume of all audio and the default value, or a specific audio node.", 116 116 "params_schema": { 117 117 "type": "object", 118 118 "properties": {
+1
src/pages/engine/audio/types.d.ts
··· 1 1 export interface State { 2 2 items: Record<string, AudioState>; 3 + volume: { default: number }; 3 4 } 4 5 5 6 export interface Audio {
-2
src/pages/orchestrator/queue-audio-tracks/_applet.astro
··· 82 82 (data) => data.now?.id, 83 83 async () => { 84 84 const playingNow = engine.queue.data.now; 85 - const volume = engine.audio.data.volume; 86 85 87 86 // Play new active queue item 88 87 // TODO: Take URL expiration timestamp into account ··· 103 102 play: playingNow 104 103 ? { 105 104 audioId: playingNow.id, 106 - volume, 107 105 } 108 106 : undefined, 109 107 },
+1 -2
src/scripts/applets/common.ts
··· 3 3 import * as Uint8 from "uint8arrays"; 4 4 import { applets } from "@web-applets/sdk"; 5 5 import { type ElementConfigurator, h } from "spellcaster/hyperscript.js"; 6 - import { effect, isSignal, type Signal, signal } from "spellcaster/spellcaster.js"; 6 + import { effect, isSignal, type Signal, signal, throttled } from "spellcaster/spellcaster.js"; 7 7 import { xxh32 } from "xxh32"; 8 8 import QS from "query-string"; 9 9 ··· 284 284 285 285 effect(() => { 286 286 effectFn(getter(), setter); 287 - return undefined; 288 287 }); 289 288 290 289 applet.addEventListener("data", (event: AppletEvent) => {