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 loading animations

+115 -19
+58 -11
src/pages/constituent/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/animations.css"; 5 6 import "@styles/icons/phosphor.css"; 6 7 7 8 import "@styles/diffuse/colors.css"; ··· 192 193 text-align: center; 193 194 } 194 195 196 + .controller .menu__loader { 197 + line-height: 0; 198 + transform-origin: center; 199 + } 200 + 195 201 .controller command { 196 202 cursor: pointer; 197 203 line-height: 0; 204 + transition-duration: var(--transition-durition); 205 + transition-property: opacity; 198 206 } 199 207 200 208 .controller .ph-pause, 201 - .controller .ph-play { 209 + .controller .ph-play, 210 + .controller .menu__loader { 202 211 font-size: var(--fs-xl); 203 212 } 204 213 ··· 354 363 const [artworkColor, setArtworkColor] = signal<string | undefined>(undefined); 355 364 const [artworkLightMode, setArtworkLightMode] = signal<boolean>(false); 356 365 const [duration, setDuration] = signal<string>("0:00"); 357 - const [groupId, setGroupId] = signal<string | undefined>(context.groupId); 366 + const [isLoading, setIsLoading] = signal<boolean>(true); 358 367 const [isPlaying, setIsPlaying] = signal<boolean>(false); 359 368 const [progress, setProgress] = signal<number>(0); 360 369 const [time, setTime] = signal<string>("0:00"); 361 370 const [volume, setVolume] = signal<number>(0); 371 + 372 + // Is main group 373 + function isMainGroup() { 374 + return context.groupId === undefined || context.groupId.toLowerCase() === "main"; 375 + } 362 376 363 377 // Applet connections 364 378 const configurator = { 365 379 input: await applet("/configurator/input"), 366 - output: await applet<ManagedOutput>("/configurator/output"), 367 380 }; 368 381 369 382 const engine = { 370 - audio: await applet<AudioEngine.State>("/engine/audio", { groupId: groupId() }), 371 - queue: await applet<QueueEngine.State>("/engine/queue", { groupId: groupId() }), 383 + audio: await applet<AudioEngine.State>("/engine/audio", { groupId: context.groupId }), 384 + queue: await applet<QueueEngine.State>("/engine/queue", { groupId: context.groupId }), 372 385 }; 373 386 374 387 const _orchestrator = { 375 388 queueAudio: await applet("/orchestrator/queue-audio", { 376 - groupId: groupId(), 389 + groupId: context.groupId, 377 390 }), 378 391 379 392 // When using the `main` group, load additional orchestrators: ··· 426 439 } 427 440 428 441 //////////////////////////////////////////// 442 + // ✨ EFFECTS 443 + // ⏳️ Loading 444 + //////////////////////////////////////////// 445 + 446 + const debouncedSetIsLoading = debounce(2000, setIsLoading); 447 + 448 + //////////////////////////////////////////// 429 449 // 🔊 AUDIO 430 450 //////////////////////////////////////////// 431 451 432 452 reactive( 433 453 engine.audio, 454 + (data) => data.items[engine.queue.data.now?.id ?? Infinity]?.loadingState === "loaded", 455 + (isLoaded) => debouncedSetIsLoading(!isLoaded), 456 + ); 457 + 458 + reactive( 459 + engine.audio, 434 460 (data) => 435 461 data.isPlaying && (data.items[engine.queue.data.now?.id ?? Infinity]?.isPlaying ?? false), 436 462 (isPlaying) => setIsPlaying(isPlaying), ··· 600 626 const GroupId = h( 601 627 "label", 602 628 computed(() => { 603 - const gid = groupId(); 604 - const display = gid === undefined || gid.toLowerCase() === "main" ? "none" : "block"; 629 + const display = isMainGroup() ? "none" : "block"; 605 630 606 631 return { 607 632 attrs: { ··· 621 646 const NowPlaying = h("cite", {}, [ 622 647 h("strong", {}, text(computed(() => activeTrack()?.tags?.title || "Diffuse"))), 623 648 tags.br(), 624 - h("span", {}, text(computed(() => activeTrack()?.tags?.artist || ""))), 649 + h( 650 + "span", 651 + computed(() => { 652 + return { 653 + style: isMainGroup() && !activeTrack() ? "font-style: italic" : "", 654 + }; 655 + }), 656 + text( 657 + computed( 658 + () => 659 + activeTrack()?.tags?.artist || 660 + (isMainGroup() && !activeTrack() ? "Waiting for tracks to be queued ..." : ""), 661 + ), 662 + ), 663 + ), 625 664 ]); 626 665 627 666 controller.appendChild(NowPlaying); ··· 676 715 677 716 const Controls = h("menu", {}, [ 678 717 Control("Previous track", "ph-fill ph-rewind", { onclick: previous }), 718 + h( 719 + "div", 720 + computed(() => { 721 + const style = `display: ${activeTrack() && isLoading() ? "inherit" : "none"}`; 722 + return { className: "animate-bounce menu__loader", style }; 723 + }), 724 + [h("i", { className: "ph-fill ph-vinyl-record", title: "Loading ..." })], 725 + ), 679 726 Control( 680 727 "Play", 681 728 "ph-fill ph-play", 682 729 computed(() => { 683 - const style = `display: ${!isPlaying() ? "inline" : "none"}`; 730 + const style = `display: ${(!isPlaying() && !isLoading()) || !activeTrack() ? "inline" : "none"}`; 684 731 return { onclick: playPause, style }; 685 732 }), 686 733 ), ··· 688 735 "Pause", 689 736 "ph-fill ph-pause", 690 737 computed(() => { 691 - const style = `display: ${isPlaying() ? "inline" : "none"}`; 738 + const style = `display: ${isPlaying() && !isLoading() ? "inline" : "none"}`; 692 739 return { onclick: playPause, style }; 693 740 }), 694 741 ),
+3 -8
src/scripts/applet/common.ts
··· 305 305 } 306 306 307 307 export function makeConnect<X>(context: BroadcastedApplet<X>) { 308 - return <D, T>( 309 - applet: Applet<D>, 310 - dataFn: (data: D) => T, 311 - effectFn: (t: T, setter: (t: T) => void) => void, 312 - ) => { 313 - // if (!context.isMainInstance()) return; 314 - return reactive(applet, dataFn, (t, setter) => { 315 - if (context.isMainInstance()) effectFn(t, setter); 308 + return <D, T>(applet: Applet<D>, dataFn: (data: D) => T, effectFn: (t: T) => void) => { 309 + return reactive(applet, dataFn, (t: T) => { 310 + if (context.isMainInstance()) effectFn(t); 316 311 }); 317 312 }; 318 313 }
+54
src/styles/animations.css
··· 1 + :root { 2 + --animate-spin: spin 1s linear infinite; 3 + --animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; 4 + --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; 5 + --animate-bounce: bounce 1s infinite; 6 + } 7 + 8 + .animate-spin { 9 + animation: var(--animate-spin); 10 + } 11 + 12 + .animate-ping { 13 + animation: var(--animate-ping); 14 + } 15 + 16 + .animate-pulse { 17 + animation: var(--animate-pulse); 18 + } 19 + 20 + .animate-bounce { 21 + animation: var(--animate-bounce); 22 + } 23 + 24 + @keyframes spin { 25 + to { 26 + transform: rotate(360deg); 27 + } 28 + } 29 + 30 + @keyframes ping { 31 + 75%, 32 + 100% { 33 + transform: scale(2); 34 + opacity: 0; 35 + } 36 + } 37 + 38 + @keyframes pulse { 39 + 50% { 40 + opacity: 0.5; 41 + } 42 + } 43 + 44 + @keyframes bounce { 45 + 0%, 46 + 100% { 47 + transform: translateY(-25%); 48 + animation-timing-function: cubic-bezier(0.8, 0, 1, 1); 49 + } 50 + 50% { 51 + transform: none; 52 + animation-timing-function: cubic-bezier(0, 0, 0.2, 1); 53 + } 54 + }