Experiment to rebuild Diffuse using web applets.
0
fork

Configure Feed

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

feat: artwork controller volume

+149 -48
+1
deno.lock
··· 28 28 "npm:@jsr/std__media-types@^1.1.0", 29 29 "npm:@orama/orama@^3.1.7", 30 30 "npm:@orama/plugin-qps@^3.1.7", 31 + "npm:@phosphor-icons/web@^2.1.2", 31 32 "npm:@picocss/pico@^2.1.1", 32 33 "npm:@tokenizer/http@~0.9.2", 33 34 "npm:@tokenizer/range@0.13",
+7
package-lock.json
··· 10 10 "@js-temporal/polyfill": "^0.5.1", 11 11 "@orama/orama": "^3.1.7", 12 12 "@orama/plugin-qps": "^3.1.7", 13 + "@phosphor-icons/web": "^2.1.2", 13 14 "@picocss/pico": "^2.1.1", 14 15 "@std/media-types": "npm:@jsr/std__media-types@^1.1.0", 15 16 "@tokenizer/http": "^0.9.2", ··· 1439 1440 "type": "opencollective", 1440 1441 "url": "https://opencollective.com/parcel" 1441 1442 } 1443 + }, 1444 + "node_modules/@phosphor-icons/web": { 1445 + "version": "2.1.2", 1446 + "resolved": "https://registry.npmjs.org/@phosphor-icons/web/-/web-2.1.2.tgz", 1447 + "integrity": "sha512-rPAR9o/bEcp4Cw4DEeZHXf+nlGCMNGkNDRizYHM47NLxz9vvEHp/Tt6FMK1NcWadzw/pFDPnRBGi/ofRya958A==", 1448 + "license": "MIT" 1442 1449 }, 1443 1450 "node_modules/@picocss/pico": { 1444 1451 "version": "2.1.1",
+1
package.json
··· 5 5 "@js-temporal/polyfill": "^0.5.1", 6 6 "@orama/orama": "^3.1.7", 7 7 "@orama/plugin-qps": "^3.1.7", 8 + "@phosphor-icons/web": "^2.1.2", 8 9 "@picocss/pico": "^2.1.1", 9 10 "@std/media-types": "npm:@jsr/std__media-types@^1.1.0", 10 11 "@tokenizer/http": "^0.9.2",
+1 -1
src/layouts/applet-pico-ui.astro
··· 2 2 import "@styles/reset.css"; 3 3 import "@styles/variables.css"; 4 4 import "@styles/fonts.css"; 5 - import "@styles/icons.css"; 5 + import "@styles/icons/iconoir.css"; 6 6 import "@styles/pico.scss"; 7 7 import "@styles/applets/common.css"; 8 8
+132 -39
src/pages/constituents/blur/artwork-controller/_applet.astro
··· 2 2 import "@styles/reset.css"; 3 3 import "@styles/variables.css"; 4 4 import "@styles/fonts.css"; 5 - import "@styles/icons.css"; 5 + import "@styles/icons/phosphor.css"; 6 6 7 7 import "@styles/diffuse/colors.css"; 8 8 import "@styles/diffuse/fonts.css"; 9 9 --- 10 10 11 11 <main> 12 - <div class="controller"> 12 + <section class="controller"> 13 13 <div class="gradient-blur"> 14 14 <div></div> 15 15 <div></div> ··· 22 22 </div> 23 23 24 24 <!-- Content --> 25 - <div class="controller__inner"></div> 26 - </div> 25 + <section class="controller__inner"></section> 26 + </section> 27 27 </main> 28 28 29 29 <style> ··· 35 35 max-width: var(--container-3xs); 36 36 overflow: hidden; 37 37 position: relative; 38 - transition: background-color 500ms; 38 + transition: 39 + background-color 500ms, 40 + color 500ms; 39 41 } 40 42 41 43 /* Artwork */ ··· 53 55 z-index: 0; 54 56 } 55 57 58 + /* Progress bars */ 59 + 60 + progress { 61 + appearance: none; 62 + border: 0; 63 + display: block; 64 + height: 4px; 65 + width: 100%; 66 + } 67 + 68 + progress, 69 + progress::-webkit-progress-bar { 70 + background-color: color-mix(in oklch, currentColor 40%, transparent); 71 + overflow: hidden; 72 + border-radius: 4px; 73 + } 74 + 75 + progress[value]::-webkit-progress-value, 76 + progress[value]::-moz-progress-bar { 77 + border-radius: 4px; 78 + background-color: color-mix(in oklch, currentColor 50%, transparent); 79 + } 80 + 56 81 /* Controller */ 57 82 58 83 .controller { ··· 75 100 display: block; 76 101 font-style: normal; 77 102 line-height: var(--leading-snug); 103 + text-shadow: 104 + 0px 1px 0px rgba(0, 0, 0, 0.08), 105 + 0px 1px 1px rgba(0, 0, 0, 0.08), 106 + 0px 2px 2px rgba(0, 0, 0, 0.08); 78 107 } 79 108 80 109 /* Progress */ ··· 82 111 .progress { 83 112 cursor: pointer; 84 113 margin: var(--space-xs) 0; 85 - padding: var(--space-2xs) 0; 86 - } 87 - 88 - progress { 89 - appearance: none; 90 - border: 0; 91 - display: block; 92 - height: 4px; 93 - width: 100%; 94 - } 95 - 96 - progress, 97 - progress::-webkit-progress-bar { 98 - background-color: oklch(100% 0 0 / 40%); 99 - overflow: hidden; 100 - border-radius: 4px; 101 - } 102 - 103 - progress[value]::-webkit-progress-value, 104 - progress[value]::-moz-progress-bar { 105 - border-radius: 4px; 106 - background-color: oklch(100% 0 0 / 50%); 114 + padding-top: var(--space-2xs); 107 115 } 108 116 109 117 .timestamps { ··· 113 121 justify-content: space-between; 114 122 margin-top: var(--space-3xs); 115 123 opacity: 0.4; 116 - } 117 - 118 - .timestamps time { 124 + text-shadow: 0px 1px 1px rgb(0 0 0 / 0.2); 119 125 } 120 126 121 127 /* Controls */ ··· 136 142 line-height: 0; 137 143 } 138 144 139 - .controller .iconoir-pause-solid, 140 - .controller .iconoir-play-solid { 145 + .controller .ph-pause, 146 + .controller .ph-play { 141 147 font-size: var(--fs-lg); 142 148 } 143 149 150 + /* Volume */ 151 + 152 + footer { 153 + align-items: center; 154 + display: flex; 155 + font-size: var(--fs-xs); 156 + gap: var(--space-2xs); 157 + justify-content: space-between; 158 + } 159 + 160 + footer .progress-bar { 161 + cursor: pointer; 162 + flex: 1; 163 + padding: var(--space-2xs) 0; 164 + } 165 + 166 + footer i { 167 + cursor: pointer; 168 + } 169 + 144 170 /* Gradient blur */ 145 171 146 172 .gradient-blur { ··· 281 307 282 308 import type { Artwork } from "@applets/processor/artwork/types"; 283 309 310 + // Types 311 + type State = { 312 + volume?: number; 313 + }; 314 + 284 315 // Register 285 316 const context = register(); 286 317 318 + // Stored state 319 + const STORE_PREFIX = "@applets/constituents/blur/artwork-controller"; 320 + const STATE_KEY = `${STORE_PREFIX}/state`; 321 + const stored = localStorage.getItem(STATE_KEY); 322 + 323 + let state: State = Object.freeze(stored ? JSON.parse(stored) : {}); 324 + 325 + function updateState(partial: Partial<State>) { 326 + state = Object.freeze({ ...state, ...partial }); 327 + localStorage.setItem(STATE_KEY, JSON.stringify(state)); 328 + } 329 + 287 330 // Signals 288 331 const [activeTrack, setActiveTrack] = signal<Track | undefined>(undefined); 289 332 const [artwork, setArtwork] = signal<Artwork[]>([]); ··· 292 335 const [isPlaying, setIsPlaying] = signal<boolean>(false); 293 336 const [progress, setProgress] = signal<number>(0); 294 337 const [time, setTime] = signal<string>("0:00"); 338 + const [volume, setVolume] = signal<number>(state.volume || 0.5); 295 339 296 340 // Applet connections 297 341 const configurator = { ··· 352 396 } 353 397 354 398 //////////////////////////////////////////// 399 + // ✨ EFFECTS 400 + // 🔊 Volume 401 + //////////////////////////////////////////// 402 + effect(() => { 403 + // Save volume in local state store 404 + updateState({ volume: volume() }); 405 + }); 406 + 407 + //////////////////////////////////////////// 355 408 // 🔊 AUDIO 356 409 //////////////////////////////////////////// 357 410 ··· 502 555 const Progress = h("div", { className: "progress", onclick: seek }, [ProgressBar, Time]); 503 556 504 557 function seek(event: MouseEvent) { 505 - const mouseEvent = event; 506 - const percentage = mouseEvent.offsetX / (event.target as HTMLProgressElement).clientWidth; 558 + const percentage = event.offsetX / (event.target as HTMLProgressElement).clientWidth; 507 559 engine.audio.sendAction("seek", { audioId: engine.queue.data.now?.id, percentage }); 508 560 } 509 561 ··· 522 574 }; 523 575 524 576 const Controls = h("menu", {}, [ 525 - Control("Previous track", "iconoir-rewind-solid", { onclick: previous }), 577 + Control("Previous track", "ph-fill ph-rewind", { onclick: previous }), 526 578 Control( 527 579 "Play", 528 - "iconoir-play-solid", 580 + "ph-fill ph-play", 529 581 computed(() => { 530 582 const style = `display: ${!isPlaying() ? "inline" : "none"}`; 531 583 return { onclick: playPause, style }; ··· 533 585 ), 534 586 Control( 535 587 "Pause", 536 - "iconoir-pause-solid", 588 + "ph-fill ph-pause", 537 589 computed(() => { 538 590 const style = `display: ${isPlaying() ? "inline" : "none"}`; 539 591 return { onclick: playPause, style }; 540 592 }), 541 593 ), 542 - Control("Next track", "iconoir-forward-solid", { onclick: next }), 594 + Control("Next track", "ph-fill ph-fast-forward", { onclick: next }), 543 595 ]); 544 596 545 597 function playPause() { ··· 563 615 controller.appendChild(Controls); 564 616 565 617 //////////////////////////////////////////// 566 - // UI ░ MISC 618 + // UI ░ VOLUME 567 619 //////////////////////////////////////////// 620 + 621 + const VolumeBar = h( 622 + "div", 623 + { 624 + className: "progress-bar", 625 + onclick: volumeClickHandler, 626 + }, 627 + [ 628 + h( 629 + "progress", 630 + computed(() => ({ 631 + max: "100", 632 + value: volume() * 100, 633 + })), 634 + ), 635 + ], 636 + ); 637 + 638 + const Volume = h("footer", {}, [ 639 + h("i", { className: "ph-fill ph-speaker-none", onclick: mute }), 640 + VolumeBar, 641 + h("i", { className: "ph-fill ph-speaker-high", onclick: fullVolume }), 642 + ]); 643 + 644 + function volumeClickHandler(event: MouseEvent) { 645 + const percentage = event.offsetX / (event.target as HTMLProgressElement).clientWidth; 646 + setVolume(percentage); 647 + engine.audio.sendAction("volume", { volume: percentage }); 648 + } 649 + 650 + function fullVolume() { 651 + setVolume(1); 652 + engine.audio.sendAction("volume", { volume: 1 }); 653 + } 654 + 655 + function mute() { 656 + setVolume(0); 657 + engine.audio.sendAction("volume", { volume: 0 }); 658 + } 659 + 660 + controller.appendChild(Volume); 568 661 </script>
+1 -1
src/pages/constituents/pilot/audio/_applet.astro
··· 2 2 import "@styles/reset.css"; 3 3 import "@styles/variables.css"; 4 4 import "@styles/fonts.css"; 5 - import "@styles/icons.css"; 5 + import "@styles/icons/iconoir.css"; 6 6 import "@styles/themes/pilot/variables.css"; 7 7 --- 8 8
+3 -5
src/pages/engine/audio/_applet.astro
··· 21 21 // Initial state 22 22 context.data = { 23 23 items: {}, 24 - volume: 0.5, 25 - // TODO: Store volume level in indexedb or localstorage 26 - // TODO: Have an action to tweak this value 27 24 }; 28 25 29 26 // State helpers ··· 113 110 } 114 111 115 112 function volume(args: { audioId?: string; volume: number }) { 116 - Array.from(container.querySelectorAll('audio[data-is-preload="false"]')).forEach((node) => { 113 + Array.from(container.querySelectorAll("audio")).forEach((node) => { 117 114 const audio = node as HTMLAudioElement; 115 + if (audio.getAttribute("data-is-preload") === "true") return; 118 116 if (args.audioId === undefined || args.audioId === audio.id) { 119 117 audio.volume = args.volume; 120 118 } ··· 288 286 if (audio.readyState < 4) updateItems(audio.id, { loadingState: "loading" }); 289 287 } 290 288 291 - function withActiveAudioNode(fn: (node: HTMLAudioElement) => void): void { 289 + function withActiveAudioNodes(fn: (node: HTMLAudioElement) => void): void { 292 290 const nonPreloadNodes: HTMLAudioElement[] = Array.from( 293 291 container.querySelectorAll(`audio[data-is-preload="false"]`), 294 292 );
-1
src/pages/engine/audio/types.d.ts
··· 1 1 export interface State { 2 2 items: Record<string, AudioState>; 3 - volume: number; 4 3 } 5 4 6 5 export interface Audio {
src/styles/icons.css src/styles/icons/iconoir.css
+1
src/styles/icons/phosphor.css
··· 1 + @import "@phosphor-icons/fill/style.css";
+2 -1
tsconfig.json
··· 25 25 "@pages/*": ["src/pages/*"], 26 26 "@scripts/*": ["src/scripts/*"], 27 27 "@styles/*": ["src/styles/*"], 28 - "@src/*": ["src/*"] 28 + "@src/*": ["src/*"], 29 + "@phosphor-icons/*": ["node_modules/@phosphor-icons/web/src/*"] 29 30 } 30 31 } 31 32 }