alpha
Login
or
Join now
tokono.ma
/
diffuse-applets
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Experiment to rebuild Diffuse using web applets.
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
feat: artwork controller volume
author
Steven Vandevelde
date
1 year ago
(Jun 14, 2025, 3:02 PM +0200)
commit
a8617c69
a8617c69cfc58a41dd779450033472a9370cbd9c
parent
1f79ea3b
1f79ea3b953559a6fb1739f213d4a3a9dc6a43b2
+149
-48
11 changed files
Expand all
Collapse all
Unified
Split
deno.lock
package-lock.json
package.json
src
layouts
applet-pico-ui.astro
pages
constituents
blur
artwork-controller
_applet.astro
pilot
audio
_applet.astro
engine
audio
_applet.astro
types.d.ts
styles
icons
iconoir.css
phosphor.css
tsconfig.json
+1
deno.lock
Reviewed
···
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
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
Reviewed
···
10
10
"@js-temporal/polyfill": "^0.5.1",
11
11
"@orama/orama": "^3.1.7",
12
12
"@orama/plugin-qps": "^3.1.7",
13
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
1443
+
},
1444
1444
+
"node_modules/@phosphor-icons/web": {
1445
1445
+
"version": "2.1.2",
1446
1446
+
"resolved": "https://registry.npmjs.org/@phosphor-icons/web/-/web-2.1.2.tgz",
1447
1447
+
"integrity": "sha512-rPAR9o/bEcp4Cw4DEeZHXf+nlGCMNGkNDRizYHM47NLxz9vvEHp/Tt6FMK1NcWadzw/pFDPnRBGi/ofRya958A==",
1448
1448
+
"license": "MIT"
1442
1449
},
1443
1450
"node_modules/@picocss/pico": {
1444
1451
"version": "2.1.1",
+1
package.json
Reviewed
···
5
5
"@js-temporal/polyfill": "^0.5.1",
6
6
"@orama/orama": "^3.1.7",
7
7
"@orama/plugin-qps": "^3.1.7",
8
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
Reviewed
···
2
2
import "@styles/reset.css";
3
3
import "@styles/variables.css";
4
4
import "@styles/fonts.css";
5
5
-
import "@styles/icons.css";
5
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
Reviewed
···
2
2
import "@styles/reset.css";
3
3
import "@styles/variables.css";
4
4
import "@styles/fonts.css";
5
5
-
import "@styles/icons.css";
5
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
12
-
<div class="controller">
12
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
25
-
<div class="controller__inner"></div>
26
26
-
</div>
25
25
+
<section class="controller__inner"></section>
26
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
38
-
transition: background-color 500ms;
38
38
+
transition:
39
39
+
background-color 500ms,
40
40
+
color 500ms;
39
41
}
40
42
41
43
/* Artwork */
···
53
55
z-index: 0;
54
56
}
55
57
58
58
+
/* Progress bars */
59
59
+
60
60
+
progress {
61
61
+
appearance: none;
62
62
+
border: 0;
63
63
+
display: block;
64
64
+
height: 4px;
65
65
+
width: 100%;
66
66
+
}
67
67
+
68
68
+
progress,
69
69
+
progress::-webkit-progress-bar {
70
70
+
background-color: color-mix(in oklch, currentColor 40%, transparent);
71
71
+
overflow: hidden;
72
72
+
border-radius: 4px;
73
73
+
}
74
74
+
75
75
+
progress[value]::-webkit-progress-value,
76
76
+
progress[value]::-moz-progress-bar {
77
77
+
border-radius: 4px;
78
78
+
background-color: color-mix(in oklch, currentColor 50%, transparent);
79
79
+
}
80
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
103
+
text-shadow:
104
104
+
0px 1px 0px rgba(0, 0, 0, 0.08),
105
105
+
0px 1px 1px rgba(0, 0, 0, 0.08),
106
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
85
-
padding: var(--space-2xs) 0;
86
86
-
}
87
87
-
88
88
-
progress {
89
89
-
appearance: none;
90
90
-
border: 0;
91
91
-
display: block;
92
92
-
height: 4px;
93
93
-
width: 100%;
94
94
-
}
95
95
-
96
96
-
progress,
97
97
-
progress::-webkit-progress-bar {
98
98
-
background-color: oklch(100% 0 0 / 40%);
99
99
-
overflow: hidden;
100
100
-
border-radius: 4px;
101
101
-
}
102
102
-
103
103
-
progress[value]::-webkit-progress-value,
104
104
-
progress[value]::-moz-progress-bar {
105
105
-
border-radius: 4px;
106
106
-
background-color: oklch(100% 0 0 / 50%);
114
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
116
-
}
117
117
-
118
118
-
.timestamps time {
124
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
139
-
.controller .iconoir-pause-solid,
140
140
-
.controller .iconoir-play-solid {
145
145
+
.controller .ph-pause,
146
146
+
.controller .ph-play {
141
147
font-size: var(--fs-lg);
142
148
}
143
149
150
150
+
/* Volume */
151
151
+
152
152
+
footer {
153
153
+
align-items: center;
154
154
+
display: flex;
155
155
+
font-size: var(--fs-xs);
156
156
+
gap: var(--space-2xs);
157
157
+
justify-content: space-between;
158
158
+
}
159
159
+
160
160
+
footer .progress-bar {
161
161
+
cursor: pointer;
162
162
+
flex: 1;
163
163
+
padding: var(--space-2xs) 0;
164
164
+
}
165
165
+
166
166
+
footer i {
167
167
+
cursor: pointer;
168
168
+
}
169
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
310
+
// Types
311
311
+
type State = {
312
312
+
volume?: number;
313
313
+
};
314
314
+
284
315
// Register
285
316
const context = register();
286
317
318
318
+
// Stored state
319
319
+
const STORE_PREFIX = "@applets/constituents/blur/artwork-controller";
320
320
+
const STATE_KEY = `${STORE_PREFIX}/state`;
321
321
+
const stored = localStorage.getItem(STATE_KEY);
322
322
+
323
323
+
let state: State = Object.freeze(stored ? JSON.parse(stored) : {});
324
324
+
325
325
+
function updateState(partial: Partial<State>) {
326
326
+
state = Object.freeze({ ...state, ...partial });
327
327
+
localStorage.setItem(STATE_KEY, JSON.stringify(state));
328
328
+
}
329
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
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
399
+
// ✨ EFFECTS
400
400
+
// 🔊 Volume
401
401
+
////////////////////////////////////////////
402
402
+
effect(() => {
403
403
+
// Save volume in local state store
404
404
+
updateState({ volume: volume() });
405
405
+
});
406
406
+
407
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
505
-
const mouseEvent = event;
506
506
-
const percentage = mouseEvent.offsetX / (event.target as HTMLProgressElement).clientWidth;
558
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
525
-
Control("Previous track", "iconoir-rewind-solid", { onclick: previous }),
577
577
+
Control("Previous track", "ph-fill ph-rewind", { onclick: previous }),
526
578
Control(
527
579
"Play",
528
528
-
"iconoir-play-solid",
580
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
536
-
"iconoir-pause-solid",
588
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
542
-
Control("Next track", "iconoir-forward-solid", { onclick: next }),
594
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
566
-
// UI ░ MISC
618
618
+
// UI ░ VOLUME
567
619
////////////////////////////////////////////
620
620
+
621
621
+
const VolumeBar = h(
622
622
+
"div",
623
623
+
{
624
624
+
className: "progress-bar",
625
625
+
onclick: volumeClickHandler,
626
626
+
},
627
627
+
[
628
628
+
h(
629
629
+
"progress",
630
630
+
computed(() => ({
631
631
+
max: "100",
632
632
+
value: volume() * 100,
633
633
+
})),
634
634
+
),
635
635
+
],
636
636
+
);
637
637
+
638
638
+
const Volume = h("footer", {}, [
639
639
+
h("i", { className: "ph-fill ph-speaker-none", onclick: mute }),
640
640
+
VolumeBar,
641
641
+
h("i", { className: "ph-fill ph-speaker-high", onclick: fullVolume }),
642
642
+
]);
643
643
+
644
644
+
function volumeClickHandler(event: MouseEvent) {
645
645
+
const percentage = event.offsetX / (event.target as HTMLProgressElement).clientWidth;
646
646
+
setVolume(percentage);
647
647
+
engine.audio.sendAction("volume", { volume: percentage });
648
648
+
}
649
649
+
650
650
+
function fullVolume() {
651
651
+
setVolume(1);
652
652
+
engine.audio.sendAction("volume", { volume: 1 });
653
653
+
}
654
654
+
655
655
+
function mute() {
656
656
+
setVolume(0);
657
657
+
engine.audio.sendAction("volume", { volume: 0 });
658
658
+
}
659
659
+
660
660
+
controller.appendChild(Volume);
568
661
</script>
+1
-1
src/pages/constituents/pilot/audio/_applet.astro
Reviewed
···
2
2
import "@styles/reset.css";
3
3
import "@styles/variables.css";
4
4
import "@styles/fonts.css";
5
5
-
import "@styles/icons.css";
5
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
Reviewed
···
21
21
// Initial state
22
22
context.data = {
23
23
items: {},
24
24
-
volume: 0.5,
25
25
-
// TODO: Store volume level in indexedb or localstorage
26
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
116
-
Array.from(container.querySelectorAll('audio[data-is-preload="false"]')).forEach((node) => {
113
113
+
Array.from(container.querySelectorAll("audio")).forEach((node) => {
117
114
const audio = node as HTMLAudioElement;
115
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
291
-
function withActiveAudioNode(fn: (node: HTMLAudioElement) => void): void {
289
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
Reviewed
···
1
1
export interface State {
2
2
items: Record<string, AudioState>;
3
3
-
volume: number;
4
3
}
5
4
6
5
export interface Audio {
src/styles/icons.css
src/styles/icons/iconoir.css
Reviewed
+1
src/styles/icons/phosphor.css
Reviewed
···
1
1
+
@import "@phosphor-icons/fill/style.css";
+2
-1
tsconfig.json
Reviewed
···
25
25
"@pages/*": ["src/pages/*"],
26
26
"@scripts/*": ["src/scripts/*"],
27
27
"@styles/*": ["src/styles/*"],
28
28
-
"@src/*": ["src/*"]
28
28
+
"@src/*": ["src/*"],
29
29
+
"@phosphor-icons/*": ["node_modules/@phosphor-icons/web/src/*"]
29
30
}
30
31
}
31
32
}