Experiment to rebuild Diffuse using web applets.
0
fork

Configure Feed

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

feat: initial styling and functionality for artwork controller

+540 -228
+1
deno.lock
··· 35 35 "npm:astro-purgecss@^5.2.2", 36 36 "npm:astro-scope@^3.0.1", 37 37 "npm:astro@^5.7.4", 38 + "npm:fast-average-color@^9.5.0", 38 39 "npm:iconoir@^7.11.0", 39 40 "npm:idb-keyval@^6.2.1", 40 41 "npm:music-metadata@^11.2.3",
+10
package-lock.json
··· 15 15 "@tokenizer/range": "^0.13.0", 16 16 "@web-applets/sdk": "https://gitpkg.vercel.app/unternet-co/web-applets/sdk?tokono.ma/experiment&scripts.postinstall=npm%20i%20%40types%2Fnode%20%26%26%20npx%20tsc", 17 17 "98.css": "^0.1.21", 18 + "fast-average-color": "^9.5.0", 18 19 "iconoir": "^7.11.0", 19 20 "idb-keyval": "^6.2.1", 20 21 "music-metadata": "^11.2.3", ··· 3284 3285 "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", 3285 3286 "dev": true, 3286 3287 "license": "MIT" 3288 + }, 3289 + "node_modules/fast-average-color": { 3290 + "version": "9.5.0", 3291 + "resolved": "https://registry.npmjs.org/fast-average-color/-/fast-average-color-9.5.0.tgz", 3292 + "integrity": "sha512-nC6x2YIlJ9xxgkMFMd1BNoM1ctMjNoRKfRliPmiEWW3S6rLTHiQcy9g3pt/xiKv/D0NAAkhb9VyV+WJFvTqMGg==", 3293 + "license": "MIT", 3294 + "engines": { 3295 + "node": ">= 12" 3296 + } 3287 3297 }, 3288 3298 "node_modules/fast-deep-equal": { 3289 3299 "version": "3.1.3",
+1
package.json
··· 10 10 "@tokenizer/range": "^0.13.0", 11 11 "@web-applets/sdk": "https://gitpkg.vercel.app/unternet-co/web-applets/sdk?tokono.ma/experiment&scripts.postinstall=npm%20i%20%40types%2Fnode%20%26%26%20npx%20tsc", 12 12 "98.css": "^0.1.21", 13 + "fast-average-color": "^9.5.0", 13 14 "iconoir": "^7.11.0", 14 15 "idb-keyval": "^6.2.1", 15 16 "music-metadata": "^11.2.3",
+486
src/pages/constituents/blur/artwork-controller/_applet.astro
··· 1 + --- 2 + import "@styles/reset.css"; 3 + import "@styles/variables.css"; 4 + import "@styles/fonts.css"; 5 + import "@styles/icons.css"; 6 + 7 + import "@styles/diffuse/colors.css"; 8 + import "@styles/diffuse/fonts.css"; 9 + --- 10 + 11 + <main> 12 + <div class="controller"> 13 + <div class="gradient-blur"> 14 + <div></div> 15 + <div></div> 16 + <div></div> 17 + <div></div> 18 + <div></div> 19 + <div></div> 20 + <div></div> 21 + <div></div> 22 + </div> 23 + 24 + <!-- Content --> 25 + <div class="controller__inner"></div> 26 + </div> 27 + </main> 28 + 29 + <style> 30 + main { 31 + background: var(--color-3); 32 + color: white; 33 + font-size: var(--fs-sm); 34 + height: 100vh; 35 + max-width: var(--container-3xs); 36 + overflow: hidden; 37 + position: relative; 38 + transition: background-color 500ms; 39 + } 40 + 41 + /* Artwork */ 42 + 43 + .artwork { 44 + aspect-ratio: 1 / 1; 45 + height: 90%; 46 + left: 50%; 47 + object-fit: cover; 48 + pointer-events: none; 49 + position: absolute; 50 + top: 0; 51 + transform: translateX(-50%); 52 + width: 100%; 53 + z-index: 0; 54 + } 55 + 56 + /* Controller */ 57 + 58 + .controller { 59 + bottom: 0; 60 + left: 0; 61 + padding: var(--space-md); 62 + position: absolute; 63 + right: 0; 64 + z-index: 10; 65 + } 66 + 67 + .controller__inner { 68 + position: relative; 69 + z-index: 10; 70 + } 71 + 72 + cite { 73 + font-style: normal; 74 + } 75 + 76 + /* Controls */ 77 + 78 + .controller menu { 79 + display: flex; 80 + font-size: 70%; 81 + gap: var(--space-sm); 82 + margin: var(--space-md) 0; 83 + padding: 0; 84 + } 85 + 86 + .controller command { 87 + cursor: pointer; 88 + } 89 + 90 + /* Gradient blur */ 91 + 92 + .gradient-blur { 93 + bottom: 0; 94 + height: 200%; 95 + left: 0; 96 + pointer-events: none; 97 + position: absolute; 98 + right: 0; 99 + z-index: 0; 100 + } 101 + 102 + .gradient-blur > div { 103 + position: absolute; 104 + inset: 0; 105 + } 106 + 107 + .gradient-blur > div:nth-of-type(1) { 108 + backdrop-filter: blur(0.5px); 109 + mask: linear-gradient( 110 + to bottom, 111 + rgba(0, 0, 0, 0) 0%, 112 + rgba(0, 0, 0, 1) 12.5%, 113 + rgba(0, 0, 0, 1) 25%, 114 + rgba(0, 0, 0, 0) 37.5% 115 + ); 116 + z-index: 1; 117 + } 118 + 119 + .gradient-blur > div:nth-of-type(2) { 120 + backdrop-filter: blur(1px); 121 + mask: linear-gradient( 122 + to bottom, 123 + rgba(0, 0, 0, 0) 12.5%, 124 + rgba(0, 0, 0, 1) 25%, 125 + rgba(0, 0, 0, 1) 37.5%, 126 + rgba(0, 0, 0, 0) 50% 127 + ); 128 + z-index: 2; 129 + } 130 + 131 + .gradient-blur > div:nth-of-type(3) { 132 + backdrop-filter: blur(2px); 133 + mask: linear-gradient( 134 + to bottom, 135 + rgba(0, 0, 0, 0) 25%, 136 + rgba(0, 0, 0, 1) 37.5%, 137 + rgba(0, 0, 0, 1) 50%, 138 + rgba(0, 0, 0, 0) 62.5% 139 + ); 140 + z-index: 3; 141 + } 142 + 143 + .gradient-blur > div:nth-of-type(4) { 144 + backdrop-filter: blur(4px); 145 + mask: linear-gradient( 146 + to bottom, 147 + rgba(0, 0, 0, 0) 37.5%, 148 + rgba(0, 0, 0, 1) 50%, 149 + rgba(0, 0, 0, 1) 62.5%, 150 + rgba(0, 0, 0, 0) 75% 151 + ); 152 + z-index: 4; 153 + } 154 + 155 + .gradient-blur > div:nth-of-type(5) { 156 + backdrop-filter: blur(8px); 157 + mask: linear-gradient( 158 + to bottom, 159 + rgba(0, 0, 0, 0) 50%, 160 + rgba(0, 0, 0, 1) 62.5%, 161 + rgba(0, 0, 0, 1) 75%, 162 + rgba(0, 0, 0, 0) 87.5% 163 + ); 164 + z-index: 5; 165 + } 166 + 167 + .gradient-blur > div:nth-of-type(6) { 168 + backdrop-filter: blur(16px); 169 + mask: linear-gradient( 170 + to bottom, 171 + rgba(0, 0, 0, 0) 62.5%, 172 + rgba(0, 0, 0, 1) 75%, 173 + rgba(0, 0, 0, 1) 87.5%, 174 + rgba(0, 0, 0, 0) 100% 175 + ); 176 + z-index: 6; 177 + } 178 + 179 + .gradient-blur > div:nth-of-type(7) { 180 + backdrop-filter: blur(32px); 181 + mask: linear-gradient( 182 + to bottom, 183 + rgba(0, 0, 0, 0) 75%, 184 + rgba(0, 0, 0, 1) 87.5%, 185 + rgba(0, 0, 0, 1) 100% 186 + ); 187 + z-index: 7; 188 + } 189 + 190 + .gradient-blur > div:nth-of-type(8) { 191 + backdrop-filter: blur(64px); 192 + mask: linear-gradient(to bottom, rgba(0, 0, 0, 0) 87.5%, rgba(0, 0, 0, 1) 100%); 193 + z-index: 8; 194 + } 195 + </style> 196 + 197 + <style is:global> 198 + iframe { 199 + display: none; 200 + } 201 + </style> 202 + 203 + <script> 204 + import { FastAverageColor } from "fast-average-color"; 205 + 206 + import { computed, effect, type Signal, signal } from "spellcaster"; 207 + import { repeat, tags, text, type ElementConfigurator } from "spellcaster/hyperscript.js"; 208 + 209 + import type { ManagedOutput, Track } from "@applets/core/types"; 210 + import { 211 + applet, 212 + comparable, 213 + hs, 214 + inputUrl, 215 + reactive, 216 + register, 217 + trackArtworkCacheId, 218 + wait, 219 + } from "@scripts/applets/common"; 220 + import { arrayShuffle } from "@scripts/common"; 221 + import scope from "astro:scope"; 222 + 223 + //////////////////////////////////////////// 224 + // SETUP 225 + //////////////////////////////////////////// 226 + import type * as AudioEngine from "@applets/engine/audio/types.d.ts"; 227 + import type * as QueueEngine from "@applets/engine/queue/types.d.ts"; 228 + 229 + import type { Artwork } from "@applets/processor/artwork/types"; 230 + 231 + // Register 232 + const context = register(); 233 + 234 + // Signals 235 + const [activeTrack, setActiveTrack] = signal<Track | undefined>(undefined); 236 + const [artwork, setArtwork] = signal<Artwork[]>([]); 237 + const [groupId, setGroupId] = signal<string | undefined>(context.groupId); 238 + const [isPlaying, setIsPlaying] = signal<boolean>(false); 239 + const [progress, setProgress] = signal<number>(0); 240 + 241 + // Applet connections 242 + const configurator = { 243 + input: await applet("../../../configurator/input"), 244 + output: await applet<ManagedOutput>("../../../configurator/output"), 245 + }; 246 + 247 + const engine = { 248 + audio: await applet<AudioEngine.State>("../../engine/audio", { groupId: groupId() }), 249 + queue: await applet<QueueEngine.State>("../../engine/queue", { groupId: groupId() }), 250 + }; 251 + 252 + const orchestrator = { 253 + inputCache: await applet("../../../orchestrator/input-cache"), 254 + }; 255 + 256 + const processor = { 257 + artwork: await applet("../../../processor/artwork"), 258 + }; 259 + 260 + //////////////////////////////////////////// 261 + // 🔊 AUDIO 262 + //////////////////////////////////////////// 263 + 264 + reactive( 265 + engine.audio, 266 + (data) => data.items[engine.queue.data.now?.id ?? Infinity]?.isPlaying ?? false, 267 + (isPlaying) => setIsPlaying(isPlaying), 268 + ); 269 + 270 + reactive( 271 + engine.audio, 272 + (data) => data.items[engine.queue.data.now?.id ?? Infinity]?.progress ?? 0, 273 + (progress: number) => setProgress(progress), 274 + ); 275 + 276 + //////////////////////////////////////////// 277 + // 🎢 QUEUE 278 + //////////////////////////////////////////// 279 + 280 + // TODO: Shuffle, limit amount, etc. 281 + async function fillQueue() { 282 + await engine.queue.sendAction("add", arrayShuffle(configurator.output.data.tracks.collection), { 283 + timeoutDuration: 60000, 284 + }); 285 + } 286 + 287 + // When the active audio has ended, 288 + // shift the queue. 289 + 290 + // NOTE: 291 + // This could probably be optimised, but it works. 292 + 293 + reactive( 294 + engine.audio, 295 + (data) => data.items[engine.queue.data.now?.id ?? Infinity]?.hasEnded ?? false, 296 + (hasEnded) => { 297 + if (hasEnded) engine.queue.sendAction("shift"); 298 + }, 299 + ); 300 + 301 + // When the active queue item has changed, 302 + // coordinate the audio engine accordingly. 303 + 304 + reactive( 305 + engine.queue, 306 + (data) => data.now?.id, 307 + async () => { 308 + const playingNow = engine.queue.data.now; 309 + const volume = engine.audio.data.volume; 310 + 311 + // Play new active queue item 312 + // TODO: Take URL expiration timestamp into account 313 + // TODO: Preload next queue item 314 + engine.audio.sendAction( 315 + "render", 316 + { 317 + audio: playingNow 318 + ? [ 319 + { 320 + id: playingNow.id, 321 + isPreload: false, 322 + url: await inputUrl(configurator.input, playingNow.uri).then((a) => a?.url), 323 + }, 324 + ] 325 + : // NOTE: This probably isn't correct, keep preloads? 326 + [], 327 + 328 + // TODO: Only play if currently playing, otherwise keep paused. 329 + play: playingNow 330 + ? { 331 + audioId: playingNow.id, 332 + volume, 333 + } 334 + : undefined, 335 + }, 336 + { 337 + timeoutDuration: 60000, 338 + }, 339 + ); 340 + 341 + // Add more tracks to the queue if needed 342 + if (playingNow) fillQueue(); 343 + }, 344 + ); 345 + 346 + // Add tracks to the queue once the tracks have been loaded. 347 + 348 + wait(configurator.output, (d) => d?.tracks.state === "loaded").then(() => { 349 + reactive(configurator.output, (d) => d.tracks.cacheId, fillQueue); 350 + }); 351 + 352 + // React to active queue item. 353 + 354 + reactive( 355 + engine.queue, 356 + (data) => comparable(data.future), 357 + async () => { 358 + const track = engine.queue.data.now || engine.queue.data.future[0]; 359 + if (!track) { 360 + setActiveTrack(undefined); 361 + setArtwork([]); 362 + return; 363 + } 364 + 365 + setActiveTrack(track); 366 + 367 + const cacheId = await trackArtworkCacheId(track); 368 + const art = await processor.artwork.sendAction( 369 + "artwork", 370 + { 371 + cacheId, 372 + tags: track.tags, 373 + urls: { 374 + get: await inputUrl(configurator.input, track.uri, "GET").then((a) => a?.url), 375 + head: await inputUrl(configurator.input, track.uri, "HEAD").then((a) => a?.url), 376 + }, 377 + }, 378 + { 379 + timeoutDuration: 60000 * 5, 380 + }, 381 + ); 382 + 383 + setArtwork(art); 384 + }, 385 + ); 386 + 387 + //////////////////////////////////////////// 388 + // UI 389 + //////////////////////////////////////////// 390 + const main = document.body.querySelector("main"); 391 + const controller = document.body.querySelector(".controller__inner"); 392 + 393 + if (!main || !controller) throw new Error("Missing DOM elements"); 394 + 395 + const h = ( 396 + tag: string, 397 + props?: Record<string, any> | Signal<Record<string, any>>, 398 + configure?: ElementConfigurator, 399 + ) => hs(tag, scope, props, configure); 400 + 401 + //////////////////////////////////////////// 402 + // UI ░ ARTWORK 403 + //////////////////////////////////////////// 404 + 405 + effect(() => { 406 + const art = artwork(); 407 + 408 + // TODO: Remove existing art? 409 + if (art.length === 0) return; 410 + 411 + // Show artwork 412 + const blob = new Blob([art[0].bytes], { type: art[0].mime }); 413 + const url = URL.createObjectURL(blob); 414 + 415 + // Remove existing artwork 416 + // TODO: Fade in new artwork and then remove other 417 + const existingArtwork = document.querySelector(".artwork"); 418 + existingArtwork?.remove(); 419 + 420 + // Create img for new artwork 421 + const img = h("img", { src: url, className: "artwork" }); 422 + 423 + // Extract average color 424 + img.onload = () => { 425 + const fac = new FastAverageColor(); 426 + const color = fac.getColor(img as HTMLImageElement); 427 + main.style.backgroundColor = color.rgba; 428 + }; 429 + 430 + // TODO: Switch to dark/light based on color? 431 + // USE color.isDark OR color.isLight 432 + 433 + // Insert new artwork 434 + main.appendChild(img); 435 + }); 436 + 437 + //////////////////////////////////////////// 438 + // UI ░ NOW PLAYING 439 + //////////////////////////////////////////// 440 + 441 + // effect(() => { 442 + // const track = activeTrack() 443 + // }) 444 + 445 + const NowPlaying = h("cite", {}, [ 446 + h("strong", {}, text(computed(() => activeTrack()?.tags?.title || "Diffuse"))), 447 + tags.br(), 448 + h("span", {}, text(computed(() => activeTrack()?.tags?.artist || ""))), 449 + ]); 450 + 451 + controller.appendChild(NowPlaying); 452 + 453 + //////////////////////////////////////////// 454 + // UI ░ CONTROLS 455 + //////////////////////////////////////////// 456 + 457 + const Controls = h("menu", {}, [ 458 + h("command", { onclick: () => {} }, text("Previous")), 459 + h("command", { onclick: playPause }, text(computed(() => (isPlaying() ? "Pause" : "Play")))), 460 + h("command", { onclick: nextTrack }, text("Next")), 461 + ]); 462 + 463 + function playPause() { 464 + const audioId = engine.queue.data.now?.id; 465 + 466 + console.log(isPlaying(), audioId); 467 + 468 + if (isPlaying() && audioId) { 469 + engine.audio.sendAction("pause", { audioId }); 470 + } else if (audioId) { 471 + engine.audio.sendAction("play", { audioId }); 472 + } else { 473 + engine.queue.sendAction("shift"); 474 + } 475 + } 476 + 477 + function nextTrack() { 478 + engine.queue.sendAction("shift"); 479 + } 480 + 481 + controller.appendChild(Controls); 482 + 483 + //////////////////////////////////////////// 484 + // UI ░ MISC 485 + //////////////////////////////////////////// 486 + </script>
-201
src/pages/constituents/desktop/artwork-controller/_applet.astro
··· 1 - --- 2 - import "@styles/reset.css"; 3 - import "@styles/variables.css"; 4 - import "@styles/fonts.css"; 5 - import "@styles/icons.css"; 6 - 7 - import "@styles/diffuse/colors.css"; 8 - import "@styles/diffuse/fonts.css"; 9 - --- 10 - 11 - <main>TODO</main> 12 - 13 - <style> 14 - main { 15 - background: var(--color-3); 16 - background-size: cover; 17 - color: var(--color-2); 18 - padding: var(--space-md); 19 - } 20 - </style> 21 - 22 - <style is:global> 23 - iframe { 24 - display: none; 25 - } 26 - </style> 27 - 28 - <script> 29 - import * as Uint8 from "uint8arrays"; 30 - 31 - import { computed, effect, type Signal, signal } from "spellcaster"; 32 - import { repeat, tags, text } from "spellcaster/hyperscript.js"; 33 - import { xxh32 } from "xxh32"; 34 - 35 - import { applets } from "@web-applets/sdk"; 36 - 37 - import type { ManagedOutput } from "@applets/core/types"; 38 - import { applet, comparable, inputUrl, reactive, wait } from "@scripts/applets/common"; 39 - 40 - //////////////////////////////////////////// 41 - // SETUP 42 - //////////////////////////////////////////// 43 - import type * as AudioEngine from "@applets/engine/audio/types.d.ts"; 44 - import type * as QueueEngine from "@applets/engine/queue/types.d.ts"; 45 - 46 - import type { Artwork } from "@applets/processor/artwork/types"; 47 - 48 - const context = applets.register(); 49 - 50 - // Initial state 51 - const [artwork, setArtwork] = signal<Artwork[]>([]); 52 - const [groupId, setGroupId] = signal(crypto.randomUUID()); 53 - const [isPlaying, setIsPlaying] = signal(false); 54 - 55 - // Applet connections 56 - const configurator = { 57 - input: await applet("../../../configurator/input"), 58 - output: await applet<ManagedOutput>("../../../configurator/output"), 59 - }; 60 - 61 - const engine = { 62 - audio: await applet<AudioEngine.State>("../../engine/audio", { groupId: groupId() }), 63 - queue: await applet<QueueEngine.State>("../../engine/queue", { groupId: groupId() }), 64 - }; 65 - 66 - const orchestrator = { 67 - inputCache: await applet("../../../orchestrator/input-cache"), 68 - }; 69 - 70 - const processor = { 71 - artwork: await applet("../../../processor/artwork"), 72 - }; 73 - 74 - //////////////////////////////////////////// 75 - // 🎢 QUEUE 76 - //////////////////////////////////////////// 77 - 78 - // TODO: Shuffle, limit amount, etc. 79 - async function fillQueue() { 80 - await engine.queue.sendAction("add", configurator.output.data.tracks.collection, { 81 - timeoutDuration: 60000, 82 - }); 83 - } 84 - 85 - // When the active audio has ended, 86 - // shift the queue. 87 - 88 - // NOTE: 89 - // This could probably be optimised, but it works. 90 - 91 - reactive( 92 - engine.audio, 93 - (data) => data.items[engine.queue.data.now?.id ?? Infinity]?.hasEnded ?? false, 94 - (hasEnded) => { 95 - if (hasEnded) engine.queue.sendAction("shift"); 96 - }, 97 - ); 98 - 99 - // When the active queue item has changed, 100 - // coordinate the audio engine accordingly. 101 - 102 - reactive( 103 - engine.queue, 104 - (data) => data.now?.id, 105 - async () => { 106 - const playingNow = engine.queue.data.now; 107 - const volume = engine.audio.data.volume; 108 - 109 - // Play new active queue item 110 - // TODO: Take URL expiration timestamp into account 111 - // TODO: Preload next queue item 112 - engine.audio.sendAction( 113 - "render", 114 - { 115 - audio: playingNow 116 - ? [ 117 - { 118 - id: playingNow.id, 119 - isPreload: false, 120 - url: await inputUrl(configurator.input, playingNow.uri), 121 - }, 122 - ] 123 - : // NOTE: This probably isn't correct, keep preloads? 124 - [], 125 - play: playingNow 126 - ? { 127 - audioId: playingNow.id, 128 - volume, 129 - } 130 - : undefined, 131 - }, 132 - { 133 - timeoutDuration: 60000, 134 - }, 135 - ); 136 - 137 - // Add more tracks to the queue if needed 138 - if (playingNow) fillQueue(); 139 - }, 140 - ); 141 - 142 - // Add tracks to the queue once the tracks have been loaded. 143 - 144 - wait(configurator.output, (d) => d?.tracks.state === "loaded").then(() => { 145 - reactive(configurator.output, (d) => d.tracks.cacheId, fillQueue); 146 - }); 147 - 148 - // Show artwork of active queue item. 149 - 150 - reactive( 151 - engine.queue, 152 - (data) => comparable(data.future), 153 - async () => { 154 - const track = engine.queue.data.now || engine.queue.data.future[0]; 155 - 156 - console.log(track); 157 - 158 - if (!track) return; 159 - 160 - const art = await processor.artwork.sendAction( 161 - "artwork", 162 - { 163 - cacheId: xxh32(track.uri), // TODO: Probably switch to SHA256 or another more secure hashing algo 164 - tags: track.tags, 165 - urls: { 166 - get: await inputUrl(configurator.input, track.uri, "GET").then((a) => a?.url), 167 - head: await inputUrl(configurator.input, track.uri, "HEAD").then((a) => a?.url), 168 - }, 169 - }, 170 - { 171 - timeoutDuration: 60000 * 5, 172 - }, 173 - ); 174 - 175 - console.log("Art", art); 176 - 177 - setArtwork(art); 178 - }, 179 - ); 180 - 181 - //////////////////////////////////////////// 182 - // UI 183 - //////////////////////////////////////////// 184 - 185 - effect(() => { 186 - const art = artwork(); 187 - 188 - // TODO: Remove existing art? 189 - if (art.length === 0) return; 190 - 191 - // Show artwork 192 - const blob = new Blob([art[0].bytes], { type: art[0].mime }); 193 - const url = URL.createObjectURL(blob); 194 - 195 - document.querySelector("main")?.setAttribute("style", `background-image: url(${url})`); 196 - }); 197 - 198 - function render() { 199 - // TODO 200 - } 201 - </script>
+2 -2
src/pages/constituents/desktop/artwork-controller/_manifest.json src/pages/constituents/blur/artwork-controller/_manifest.json
··· 1 1 { 2 - "name": "diffuse/constituents/desktop/artwork-controller", 3 - "title": "Diffuse Desktop Theme | Artwork Controller", 2 + "name": "diffuse/constituents/blur/artwork-controller", 3 + "title": "Diffuse Blur Theme | Artwork Controller", 4 4 "entrypoint": "index.html", 5 5 "actions": { 6 6 "modifyIsPlaying": {
src/pages/constituents/desktop/artwork-controller/index.astro src/pages/constituents/blur/artwork-controller/index.astro
+2 -2
src/pages/constituents/pilot/audio/_applet.astro
··· 94 94 <script> 95 95 // @ts-ignore 96 96 import scope from "astro:scope"; 97 - import { applets } from "@web-applets/sdk"; 98 97 99 98 import type { State } from "./types.d.ts"; 99 + import { register } from "@scripts/applets/common"; 100 100 101 101 //////////////////////////////////////////// 102 102 // SETUP 103 103 //////////////////////////////////////////// 104 - const context = applets.register<State>(); 104 + const context = register<State>(); 105 105 106 106 // Initial state 107 107 context.data = {
+2 -2
src/pages/index.astro
··· 16 16 17 17 // Themes 18 18 const themes = [ 19 - { url: "themes/desktop/", title: "(WIP) Desktop" }, 19 + { url: "themes/blur/", title: "(WIP) Blur" }, 20 20 { url: "themes/pilot/", title: "(WIP) Pilot" }, 21 21 { url: "themes/webamp/", title: "Webamp" }, 22 22 ]; ··· 26 26 27 27 // Constituents 28 28 const constituents = [ 29 - { url: "constituents/desktop/artwork-controller", title: "(WIP) Desktop ⦚ Artwork Controller" }, 29 + { url: "constituents/blur/artwork-controller", title: "(WIP) Blur ⦚ Artwork Controller" }, 30 30 ]; 31 31 32 32 // Applets
+1 -8
src/pages/processor/artwork/_applet.astro
··· 121 121 async function processRequest(req: ArtworkRequest): Promise<Artwork[]> { 122 122 // Check if already processed 123 123 // TODO: Retry if none was found? 124 - const cache = await IDB.get(`${IDB_PREFIX}/${req.cacheId}`); 125 - console.log("fromCache", cache); 124 + const cache = await IDB.get(`${IDB_ARTWORK_PREFIX}/${req.cacheId}`); 126 125 if (cache) return cache; 127 - 128 - console.log(req); 129 126 130 127 // 🚀 131 128 let art: Artwork[] = []; ··· 142 139 143 140 if (!req.tags) req.tags = meta.tags; 144 141 145 - console.log(meta); 146 - 147 142 // Add artwork from metadata 148 143 const fromMeta = 149 144 meta.artwork?.map((a: IPicture) => { 150 145 return { bytes: a.data, mime: a.format }; 151 146 }) || []; 152 - 153 - console.log(fromMeta); 154 147 155 148 art.push(...fromMeta); 156 149
+7
src/pages/themes/blur/index.astro
··· 1 + --- 2 + import Page from "../../../layouts/page.astro"; 3 + --- 4 + 5 + <Page title="Diffuse"> 6 + <script src="../../../scripts/themes/blur/index.js"></script> 7 + </Page>
-7
src/pages/themes/desktop/index.astro
··· 1 - --- 2 - import Page from "../../../layouts/page.astro"; 3 - --- 4 - 5 - <Page title="Diffuse"> 6 - <script src="../../../scripts/themes/desktop/index.js"></script> 7 - </Page>
+11 -2
src/scripts/applets/common.ts
··· 1 1 import type { Applet, AppletEvent, AppletScope } from "@web-applets/sdk"; 2 2 3 - import QS from "query-string"; 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 6 import { effect, isSignal, type Signal, signal } from "spellcaster/spellcaster.js"; 7 7 import { xxh32 } from "xxh32"; 8 + import QS from "query-string"; 8 9 9 10 import type { ResolvedUri, Track } from "@applets/core/types"; 10 11 ··· 88 89 // 🪟 Applet registration 89 90 //////////////////////////////////////////// 90 91 export type BroadcastedApplet<T> = { 92 + groupId: string | undefined; 91 93 scope: AppletScope<T>; 92 94 93 95 settled(): Promise<void>; ··· 104 106 setActionHandler<H extends Function>(actionId: string, actionHandler: H): void; 105 107 }; 106 108 107 - export function register<DataType = any>() { 109 + export function register<DataType = any>(): BroadcastedApplet<DataType> { 108 110 const url = new URL(location.href); 109 111 const scope = applets.register<DataType>(); 110 112 ··· 205 207 206 208 // Context 207 209 const context: BroadcastedApplet<DataType> = { 210 + groupId, 208 211 scope, 209 212 210 213 settled() { ··· 379 382 380 383 export function jsonEncode<T>(a: T): Uint8Array { 381 384 return new TextEncoder().encode(JSON.stringify(a)); 385 + } 386 + 387 + export async function trackArtworkCacheId(track: Track): Promise<string> { 388 + return await crypto.subtle 389 + .digest("SHA-256", new TextEncoder().encode(track.uri)) 390 + .then((a) => Uint8.toString(new Uint8Array(a), "base64url")); 382 391 } 383 392 384 393 export function wait<A>(applet: Applet<A>, dataFn: (a: A | undefined) => boolean): Promise<void> {
+16
src/scripts/common.ts
··· 1 + export function arrayShuffle<T>(array: Array<T>): Array<T> { 2 + if (array.length === 0) { 3 + return []; 4 + } 5 + 6 + array = [...array]; 7 + 8 + for (let index = array.length - 1; index > 0; index--) { 9 + const randArr = crypto.getRandomValues(new Uint32Array(1)); 10 + const randVal = randArr[0] / 2 ** 32; 11 + const newIndex = Math.floor(randVal * (index + 1)); 12 + [array[index], array[newIndex]] = [array[newIndex], array[index]]; 13 + } 14 + 15 + return array; 16 + }
+1 -4
src/scripts/themes/desktop/index.ts src/scripts/themes/blur/index.ts
··· 1 - import * as Uint8 from "uint8arrays"; 2 - import { applet, reactive, wait } from "@scripts/applets/common"; 3 - 4 1 //////////////////////////////////////////// 5 2 // 🎨 Styles 6 3 //////////////////////////////////////////// 7 - import "@styles/themes/desktop/index.css"; 4 + import "@styles/themes/blur/index.css"; 8 5 9 6 //////////////////////////////////////////// 10 7 // 🗂️ Applets
src/styles/themes/desktop/index.css src/styles/themes/blur/index.css
src/styles/themes/desktop/variables.css src/styles/themes/blur/variables.css