Experiment to rebuild Diffuse using web applets.

feat: blur artwork controller progress bar + timestamps

+184 -9
+1
deno.lock
··· 23 23 "dependencies": [ 24 24 "npm:98.css@~0.1.21", 25 25 "npm:@automerge/automerge@^3.0.0-beta.0", 26 + "npm:@js-temporal/polyfill@~0.5.1", 26 27 "npm:@jsr/bradenmacdonald__s3-lite-client@0.9", 27 28 "npm:@jsr/std__media-types@^1.1.0", 28 29 "npm:@orama/orama@^3.1.7",
+19
package-lock.json
··· 7 7 "dependencies": { 8 8 "@automerge/automerge": "^3.0.0-beta.0", 9 9 "@bradenmacdonald/s3-lite-client": "npm:@jsr/bradenmacdonald__s3-lite-client@^0.9.0", 10 + "@js-temporal/polyfill": "^0.5.1", 10 11 "@orama/orama": "^3.1.7", 11 12 "@orama/plugin-qps": "^3.1.7", 12 13 "@picocss/pico": "^2.1.1", ··· 1091 1092 "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 1092 1093 "dev": true, 1093 1094 "license": "MIT" 1095 + }, 1096 + "node_modules/@js-temporal/polyfill": { 1097 + "version": "0.5.1", 1098 + "resolved": "https://registry.npmjs.org/@js-temporal/polyfill/-/polyfill-0.5.1.tgz", 1099 + "integrity": "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==", 1100 + "license": "ISC", 1101 + "dependencies": { 1102 + "jsbi": "^4.3.0" 1103 + }, 1104 + "engines": { 1105 + "node": ">=12" 1106 + } 1094 1107 }, 1095 1108 "node_modules/@orama/orama": { 1096 1109 "version": "3.1.7", ··· 4145 4158 "bin": { 4146 4159 "js-yaml": "bin/js-yaml.js" 4147 4160 } 4161 + }, 4162 + "node_modules/jsbi": { 4163 + "version": "4.3.2", 4164 + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.2.tgz", 4165 + "integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==", 4166 + "license": "Apache-2.0" 4148 4167 }, 4149 4168 "node_modules/jszip": { 4150 4169 "version": "3.10.1",
+1
package.json
··· 2 2 "dependencies": { 3 3 "@automerge/automerge": "^3.0.0-beta.0", 4 4 "@bradenmacdonald/s3-lite-client": "npm:@jsr/bradenmacdonald__s3-lite-client@^0.9.0", 5 + "@js-temporal/polyfill": "^0.5.1", 5 6 "@orama/orama": "^3.1.7", 6 7 "@orama/plugin-qps": "^3.1.7", 7 8 "@picocss/pico": "^2.1.1",
+162 -7
src/pages/constituents/blur/artwork-controller/_applet.astro
··· 69 69 z-index: 10; 70 70 } 71 71 72 + /* Now playing */ 73 + 72 74 cite { 75 + display: block; 73 76 font-style: normal; 77 + line-height: var(--leading-snug); 78 + } 79 + 80 + /* Progress */ 81 + 82 + .progress { 83 + cursor: pointer; 84 + 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%); 107 + } 108 + 109 + .timestamps { 110 + display: flex; 111 + font-size: var(--fs-2xs); 112 + font-weight: 500; 113 + justify-content: space-between; 114 + margin-top: var(--space-3xs); 115 + opacity: 0.4; 116 + } 117 + 118 + .timestamps time { 74 119 } 75 120 76 121 /* Controls */ 77 122 78 123 .controller menu { 124 + align-items: center; 79 125 display: flex; 80 - font-size: var(--fs-2xs); 81 - gap: var(--space-sm); 82 - margin: var(--space-md) 0; 126 + font-size: var(--fs-md); 127 + gap: var(--space-lg); 128 + justify-content: center; 129 + margin: var(--space-sm) 0; 83 130 padding: 0; 131 + text-align: center; 84 132 } 85 133 86 134 .controller command { 87 135 cursor: pointer; 136 + line-height: 0; 137 + } 138 + 139 + .controller .iconoir-pause-solid, 140 + .controller .iconoir-play-solid { 141 + font-size: var(--fs-lg); 88 142 } 89 143 90 144 /* Gradient blur */ ··· 202 256 203 257 <script> 204 258 import { FastAverageColor } from "fast-average-color"; 259 + import { Temporal } from "@js-temporal/polyfill"; 205 260 206 261 import { computed, effect, type Signal, signal, throttled } from "spellcaster"; 207 262 import { tags, text, type ElementConfigurator } from "spellcaster/hyperscript.js"; ··· 232 287 // Signals 233 288 const [activeTrack, setActiveTrack] = signal<Track | undefined>(undefined); 234 289 const [artwork, setArtwork] = signal<Artwork[]>([]); 290 + const [duration, setDuration] = signal<string>("0:00"); 235 291 const [groupId, setGroupId] = signal<string | undefined>(context.groupId); 236 292 const [isPlaying, setIsPlaying] = signal<boolean>(false); 237 293 const [progress, setProgress] = signal<number>(0); 294 + const [time, setTime] = signal<string>("0:00"); 238 295 239 296 // Applet connections 240 297 const configurator = { ··· 259 316 }; 260 317 261 318 //////////////////////////////////////////// 319 + // ✨ EFFECTS 320 + // ⌚️ Time 321 + //////////////////////////////////////////// 322 + const formatTimestamps = () => { 323 + const prog = progress(); 324 + const curr = engine.queue.data.now; 325 + const audio = curr ? engine.audio.data.items[curr.id] : undefined; 326 + 327 + if (audio) { 328 + const p = Temporal.Duration.from({ 329 + milliseconds: Math.round(audio.duration * prog * 1000), 330 + }).round({ 331 + largestUnit: "hours", 332 + }); 333 + 334 + const d = Temporal.Duration.from({ milliseconds: Math.round(audio.duration * 1000) }).round({ 335 + largestUnit: "hours", 336 + }); 337 + 338 + // const diff = 339 + 340 + setTime(formatTime(p)); 341 + setDuration(formatTime(d)); 342 + } else { 343 + setTime("0:00"); 344 + setDuration("0:00"); 345 + } 346 + }; 347 + 348 + effect(formatTimestamps); 349 + 350 + function formatTime(duration: Temporal.Duration) { 351 + return `${duration.hours > 0 ? duration.hours.toFixed(0) + ":" : ""}${duration.hours > 0 ? (duration.minutes > 9 ? duration.minutes.toFixed(0) : "0" + duration.minutes.toFixed(0)) : duration.minutes.toFixed(0)}:${duration.seconds > 9 ? duration.seconds.toFixed(0) : "0" + duration.seconds.toFixed(0)}`; 352 + } 353 + 354 + //////////////////////////////////////////// 262 355 // 🔊 AUDIO 263 356 //////////////////////////////////////////// 264 357 ··· 380 473 controller.appendChild(NowPlaying); 381 474 382 475 //////////////////////////////////////////// 476 + // UI ░ PROGRESS 477 + //////////////////////////////////////////// 478 + 479 + const ProgressBar = h( 480 + "progress", 481 + computed(() => ({ max: "100", value: `${progress() * 100}` })), 482 + [], 483 + ); 484 + 485 + const Time = h("div", { className: "timestamps" }, [ 486 + h( 487 + "time", 488 + computed(() => { 489 + return { attrs: { datetime: time() } }; 490 + }), 491 + text(time), 492 + ), 493 + h( 494 + "time", 495 + computed(() => { 496 + return { attrs: { datetime: duration() } }; 497 + }), 498 + text(duration), 499 + ), 500 + ]); 501 + 502 + const Progress = h("div", { className: "progress", onclick: seek }, [ProgressBar, Time]); 503 + 504 + function seek(event: MouseEvent) { 505 + const mouseEvent = event; 506 + const percentage = mouseEvent.offsetX / (event.target as HTMLProgressElement).clientWidth; 507 + engine.audio.sendAction("seek", { audioId: engine.queue.data.now?.id, percentage }); 508 + } 509 + 510 + controller.appendChild(Progress); 511 + 512 + //////////////////////////////////////////// 383 513 // UI ░ CONTROLS 384 514 //////////////////////////////////////////// 385 515 516 + const Control = ( 517 + label: string, 518 + icon: string, 519 + props: Record<string, any> | Signal<Record<string, any>>, 520 + ) => { 521 + return h("command", props, [h("i", { className: icon, title: label })]); 522 + }; 523 + 386 524 const Controls = h("menu", {}, [ 387 - h("command", { onclick: () => {} }, text("Previous")), 388 - h("command", { onclick: playPause }, text(computed(() => (isPlaying() ? "Pause" : "Play")))), 389 - h("command", { onclick: nextTrack }, text("Next")), 525 + Control("Previous track", "iconoir-rewind-solid", { onclick: previous }), 526 + Control( 527 + "Play", 528 + "iconoir-play-solid", 529 + computed(() => { 530 + const style = `display: ${!isPlaying() ? "inline" : "none"}`; 531 + return { onclick: playPause, style }; 532 + }), 533 + ), 534 + Control( 535 + "Pause", 536 + "iconoir-pause-solid", 537 + computed(() => { 538 + const style = `display: ${isPlaying() ? "inline" : "none"}`; 539 + return { onclick: playPause, style }; 540 + }), 541 + ), 542 + Control("Next track", "iconoir-forward-solid", { onclick: next }), 390 543 ]); 391 544 392 545 function playPause() { ··· 401 554 } 402 555 } 403 556 404 - function nextTrack() { 557 + function previous() {} 558 + 559 + function next() { 405 560 engine.queue.sendAction("shift"); 406 561 } 407 562
+1 -2
src/styles/diffuse/fonts.css
··· 7 7 :root { 8 8 font-family: "InterVariable", sans-serif; 9 9 font-feature-settings: 10 - "zero" 2, 11 - "ss03" 2; 10 + /* "zero" 2, */ "ss03" 2; 12 11 font-optical-sizing: auto; 13 12 } 14 13 }