src
applets
common
layouts
pages
styles
···
1
1
+
<div id="container">
2
2
+
<audio
3
3
+
src="https://archive.org/download/lp_moonlight-sonata_ludwig-van-beethoven-frdric-chopin-alexand/disc1%2F01.02.%20Moonlight%20Sonata%20Op.%2027%2C%20No.%202%20In%20C%20Sharp%20Minor%3A%20Allegretto.mp3?tunnel=1"
4
4
+
></audio>
5
5
+
</div>
6
6
+
7
7
+
<script>
8
8
+
import { applets } from "@web-applets/sdk";
9
9
+
10
10
+
interface State {
11
11
+
isPlaying: boolean;
12
12
+
progress: number;
13
13
+
}
14
14
+
15
15
+
const context = applets.register<State>();
16
16
+
const container = document.querySelector("#container");
17
17
+
const audio = document.querySelector("audio");
18
18
+
19
19
+
////////////////////////////////////////////
20
20
+
// Initial state
21
21
+
////////////////////////////////////////////
22
22
+
context.data = {
23
23
+
isPlaying: false,
24
24
+
progress: 0,
25
25
+
};
26
26
+
27
27
+
////////////////////////////////////////////
28
28
+
// Audio events
29
29
+
////////////////////////////////////////////
30
30
+
audio.ontimeupdate = (event) => {
31
31
+
const progress =
32
32
+
isNaN(audio.duration) || audio.duration === 0 ? 0 : audio.currentTime / audio.duration;
33
33
+
context.data = { ...context.data, progress };
34
34
+
};
35
35
+
36
36
+
audio.onpause = () => (context.data = { ...context.data, isPlaying: false });
37
37
+
audio.onplay = () => (context.data = { ...context.data, isPlaying: true });
38
38
+
39
39
+
////////////////////////////////////////////
40
40
+
// Actions
41
41
+
////////////////////////////////////////////
42
42
+
context.setActionHandler("load", (src: string) => {
43
43
+
audio.src = src;
44
44
+
});
45
45
+
46
46
+
context.setActionHandler("play", () => {
47
47
+
audio.play();
48
48
+
});
49
49
+
50
50
+
context.setActionHandler("pause", () => {
51
51
+
audio.pause();
52
52
+
});
53
53
+
</script>
···
1
1
+
{
2
2
+
"name": "diffuse/engine/audio",
3
3
+
"entrypoint": "index.html",
4
4
+
"actions": {
5
5
+
"load": {
6
6
+
"title": "Load",
7
7
+
"description": "Load a given audio src",
8
8
+
"params_schema": {
9
9
+
"type": "string",
10
10
+
"description": "String to be used as the audio `src`"
11
11
+
}
12
12
+
},
13
13
+
"pause": {
14
14
+
"title": "Pause",
15
15
+
"description": "Indicate the active audio should be paused",
16
16
+
"params_schema": {
17
17
+
"type": "null"
18
18
+
}
19
19
+
},
20
20
+
"play": {
21
21
+
"title": "Play",
22
22
+
"description": "Indicate the active audio should be playing",
23
23
+
"params_schema": {
24
24
+
"type": "null"
25
25
+
}
26
26
+
}
27
27
+
}
28
28
+
}
···
1
1
+
<link
2
2
+
rel="stylesheet"
3
3
+
href="https://cdn.jsdelivr.net/gh/iconoir-icons/iconoir@main/css/iconoir.css"
4
4
+
/>
5
5
+
6
6
+
<div>
7
7
+
<button>▶️</button>
8
8
+
<progress max="100" value="0"></progress>
9
9
+
</div>
10
10
+
11
11
+
<style>
12
12
+
@import "../../../../../styles/themes/pilot/variables.css";
13
13
+
14
14
+
div {
15
15
+
/* 5% darker version */
16
16
+
background: oklch(from var(--delicate-cloud) calc(l - 0.1) c h);
17
17
+
18
18
+
height: 100px;
19
19
+
line-height: 100px;
20
20
+
text-align: center;
21
21
+
}
22
22
+
</style>
23
23
+
24
24
+
<script>
25
25
+
import { applets } from "@web-applets/sdk";
26
26
+
27
27
+
interface State {
28
28
+
isPlaying: boolean;
29
29
+
}
30
30
+
31
31
+
const context = applets.register<State>();
32
32
+
33
33
+
////////////////////////////////////////////
34
34
+
// Initial state
35
35
+
////////////////////////////////////////////
36
36
+
context.data = {
37
37
+
isPlaying: false,
38
38
+
};
39
39
+
40
40
+
////////////////////////////////////////////
41
41
+
// Actions
42
42
+
////////////////////////////////////////////
43
43
+
context.setActionHandler("set_is_playing", (isPlaying: boolean) => {
44
44
+
context.data.isPlaying = isPlaying;
45
45
+
render();
46
46
+
});
47
47
+
48
48
+
context.setActionHandler("set_progress", (progress: number) => {
49
49
+
document.body.querySelector("progress").value = Math.round(progress * 100);
50
50
+
render();
51
51
+
});
52
52
+
53
53
+
////////////////////////////////////////////
54
54
+
// DOM
55
55
+
////////////////////////////////////////////
56
56
+
document.body.querySelector("button").onclick = () => {
57
57
+
context.data = { isPlaying: !(context.data?.isPlaying ?? false) };
58
58
+
};
59
59
+
60
60
+
function render() {
61
61
+
document.body.querySelector("button").innerText = context.data.isPlaying ? "⏸️" : "▶️";
62
62
+
}
63
63
+
64
64
+
render();
65
65
+
</script>
···
1
1
+
{
2
2
+
"name": "diffuse/ui/audio",
3
3
+
"entrypoint": "index.html",
4
4
+
"actions": {
5
5
+
"set_is_playing": {
6
6
+
"title": "Set is-playing state",
7
7
+
"description": "Indicate if audio is playing or not.",
8
8
+
"params_schema": {
9
9
+
"type": "boolean"
10
10
+
}
11
11
+
},
12
12
+
"set_progress": {
13
13
+
"title": "Set progress",
14
14
+
"description": "Indicate how far the audio has progressed.",
15
15
+
"params_schema": {
16
16
+
"type": "number"
17
17
+
}
18
18
+
}
19
19
+
}
20
20
+
}
···
1
1
-
export function manifest(obj: Record<string, unknown>) {
2
2
-
return function GET() {
3
3
-
return new Response(
4
4
-
JSON.stringify(obj),
5
5
-
);
6
6
-
};
7
7
-
}
···
1
1
+
import { defineCollection, z } from "astro:content";
2
2
+
import { glob } from "astro/loaders";
3
3
+
4
4
+
const applets = defineCollection({
5
5
+
loader: glob({ pattern: "**/*.json", base: "./src/applets" }),
6
6
+
});
7
7
+
8
8
+
export const collections = { applets };
···
11
11
<title>{title}</title>
12
12
13
13
<style>
14
14
-
html,
15
15
-
body {
16
16
-
margin: 0;
17
17
-
}
14
14
+
@import "../styles/reset.css";
15
15
+
@import "../styles/fonts.css";
16
16
+
@import "../styles/variables.css";
18
17
</style>
19
18
</head>
20
19
<body>
···
1
1
+
---
2
2
+
import { getCollection } from "astro:content";
3
3
+
import AppletLayout from "../layouts/applet.astro";
4
4
+
5
5
+
// Generate static paths
6
6
+
export async function getStaticPaths() {
7
7
+
async function gen(path: string[]): Promise<{
8
8
+
applet: string;
9
9
+
title: string;
10
10
+
Component: (_props: Record<string, any>) => any;
11
11
+
}> {
12
12
+
let Applet;
13
13
+
let manifest;
14
14
+
15
15
+
if (path.length === 2) {
16
16
+
Applet = await import(`../applets/${path[0]}/${path[1]}/applet.astro`);
17
17
+
manifest = await import(`../applets/${path[0]}/${path[1]}/manifest.json`);
18
18
+
}
19
19
+
20
20
+
if (path.length === 4) {
21
21
+
Applet = await import(`../applets/${path[0]}/${path[1]}/${path[2]}/${path[3]}/applet.astro`);
22
22
+
manifest = await import(
23
23
+
`../applets/${path[0]}/${path[1]}/${path[2]}/${path[3]}/manifest.json`
24
24
+
);
25
25
+
}
26
26
+
27
27
+
if (Applet === undefined || manifest === undefined) {
28
28
+
throw new Error("Unsupported path length");
29
29
+
}
30
30
+
31
31
+
return {
32
32
+
applet: path.join("/"),
33
33
+
title: manifest.default.name,
34
34
+
Component: Applet.default,
35
35
+
};
36
36
+
}
37
37
+
38
38
+
const applets = await getCollection("applets");
39
39
+
const pages = await Promise.all(
40
40
+
applets.map((applet) => {
41
41
+
return gen(applet.id.split("/").slice(0, -1));
42
42
+
}),
43
43
+
);
44
44
+
45
45
+
return pages.map(({ applet, Component, title }) => {
46
46
+
return {
47
47
+
params: { applet },
48
48
+
props: { Component, title },
49
49
+
};
50
50
+
});
51
51
+
}
52
52
+
53
53
+
// Render props
54
54
+
const { Component, title } = Astro.props;
55
55
+
---
56
56
+
57
57
+
<AppletLayout title={title}>
58
58
+
<Component />
59
59
+
</AppletLayout>
···
1
1
+
import type { APIRoute } from "astro";
2
2
+
import { getCollection } from "astro:content";
3
3
+
4
4
+
// API Route
5
5
+
export const GET: APIRoute = ({ params, props, request }) => {
6
6
+
return new Response(
7
7
+
JSON.stringify(props.manifest),
8
8
+
);
9
9
+
};
10
10
+
11
11
+
// Generate static paths
12
12
+
export async function getStaticPaths() {
13
13
+
const applets = await getCollection("applets");
14
14
+
15
15
+
return applets.map((applet) => {
16
16
+
return {
17
17
+
params: { manifest: applet.id },
18
18
+
props: { manifest: applet.data },
19
19
+
};
20
20
+
});
21
21
+
}
···
1
1
-
---
2
2
-
import Applet from "../../../layouts/applet.astro";
3
3
-
---
4
4
-
5
5
-
<Applet title="Diffuse engine/audio applet">
6
6
-
<div id="container">
7
7
-
<audio
8
8
-
src="https://archive.org/download/lp_moonlight-sonata_ludwig-van-beethoven-frdric-chopin-alexand/disc1%2F01.02.%20Moonlight%20Sonata%20Op.%2027%2C%20No.%202%20In%20C%20Sharp%20Minor%3A%20Allegretto.mp3?tunnel=1"
9
9
-
></audio>
10
10
-
</div>
11
11
-
12
12
-
<script>
13
13
-
import { applets } from "@web-applets/sdk";
14
14
-
15
15
-
interface State {
16
16
-
isPlaying: boolean;
17
17
-
progress: number;
18
18
-
}
19
19
-
20
20
-
const context = applets.register<State>();
21
21
-
const container = document.querySelector("#container");
22
22
-
const audio = document.querySelector("audio");
23
23
-
24
24
-
////////////////////////////////////////////
25
25
-
// Initial state
26
26
-
////////////////////////////////////////////
27
27
-
context.data = {
28
28
-
isPlaying: false,
29
29
-
progress: 0,
30
30
-
};
31
31
-
32
32
-
////////////////////////////////////////////
33
33
-
// Audio events
34
34
-
////////////////////////////////////////////
35
35
-
audio.ontimeupdate = (event) => {
36
36
-
const progress =
37
37
-
isNaN(audio.duration) || audio.duration === 0 ? 0 : audio.currentTime / audio.duration;
38
38
-
context.data = { ...context.data, progress };
39
39
-
};
40
40
-
41
41
-
audio.onpause = () => (context.data = { ...context.data, isPlaying: false });
42
42
-
audio.onplay = () => (context.data = { ...context.data, isPlaying: true });
43
43
-
44
44
-
////////////////////////////////////////////
45
45
-
// Actions
46
46
-
////////////////////////////////////////////
47
47
-
context.setActionHandler("load", (src: string) => {
48
48
-
audio.src = src;
49
49
-
});
50
50
-
51
51
-
context.setActionHandler("play", () => {
52
52
-
audio.play();
53
53
-
});
54
54
-
55
55
-
context.setActionHandler("pause", () => {
56
56
-
audio.pause();
57
57
-
});
58
58
-
</script>
59
59
-
</Applet>
···
1
1
-
import { manifest } from "../../../common/pages/manifest.ts";
2
2
-
3
3
-
export const GET = manifest({
4
4
-
name: "diffuse/engine/audio",
5
5
-
entrypoint: "index.html",
6
6
-
actions: {
7
7
-
load: {
8
8
-
title: "Load",
9
9
-
description: "Load a given audio src",
10
10
-
params_schema: {
11
11
-
type: "string",
12
12
-
description: "String to be used as the audio `src`",
13
13
-
},
14
14
-
},
15
15
-
pause: {
16
16
-
title: "Pause",
17
17
-
description: "Indicate the active audio should be paused",
18
18
-
params_schema: {
19
19
-
type: "null",
20
20
-
},
21
21
-
},
22
22
-
play: {
23
23
-
title: "Play",
24
24
-
description: "Indicate the active audio should be playing",
25
25
-
params_schema: {
26
26
-
type: "null",
27
27
-
},
28
28
-
},
29
29
-
},
30
30
-
});
···
7
7
import "../styles/pages/index.css";
8
8
9
9
const engines = [{ url: "engine/audio/", title: "Audio" }];
10
10
-
const themes = [{ url: "themes/sakura/", title: "Sakura" }];
10
10
+
const themes = [{ url: "themes/pilot/", title: "Pilot" }];
11
11
---
12
12
13
13
<Page title="Index">
···
4
4
5
5
<Page title="Diffuse Applets Usage Example">
6
6
<!-- Theme applets -->
7
7
-
<iframe src="ui/audio/" frameborder="0" style="width: 100%"></iframe>
7
7
+
<iframe id="#applet__ui__audio" src="ui/audio/" frameborder="0" style="width: 100%"></iframe>
8
8
9
9
<!-- Other applets -->
10
10
-
<iframe src="../../engine/audio/" frameborder="0" height="0" width="0"></iframe>
10
10
+
<iframe id="#applet__engine__audio" src="../../engine/audio/" frameborder="0" height="0" width="0"
11
11
+
></iframe>
12
12
+
13
13
+
<div>Testing</div>
11
14
</Page>
12
15
16
16
+
<style is:global>
17
17
+
@import "../../../styles/reset.css";
18
18
+
@import "../../../styles/fonts.css";
19
19
+
@import "../../../styles/variables.css";
20
20
+
@import "../../../styles/themes/pilot/variables.css";
21
21
+
</style>
22
22
+
13
23
<style>
14
14
-
@import "../../../styles/pages/themes/sakura/variables.css";
24
24
+
/***********************************
25
25
+
* Fonts
26
26
+
***********************************/
27
27
+
:root {
28
28
+
font-family: "Inter", sans-serif;
29
29
+
font-size: var(--fs-base);
30
30
+
}
31
31
+
32
32
+
@supports (font-variation-settings: normal) {
33
33
+
:root {
34
34
+
font-family: "InterVariable", sans-serif;
35
35
+
font-feature-settings:
36
36
+
"ss03" 2,
37
37
+
"ss02" 2;
38
38
+
font-optical-sizing: auto;
39
39
+
}
40
40
+
}
15
41
16
42
body {
17
17
-
background-color: var(--reading-tea-leaves);
43
43
+
background-color: var(--delicate-cloud);
44
44
+
color: var(--made-in-the-shade);
45
45
+
overflow: hidden;
46
46
+
}
47
47
+
48
48
+
/***********************************
49
49
+
* Applets | Position
50
50
+
***********************************/
51
51
+
#applet__ui__audio {
52
52
+
}
53
53
+
54
54
+
/* Position engines outside the viewframe (no UI for these) */
55
55
+
#applet__engine__audio {
56
56
+
left: 110vw;
57
57
+
position: absolute;
58
58
+
top: 110vh;
18
59
}
19
60
</style>
20
61
···
1
1
-
---
2
2
-
import Applet from "../../../../../layouts/applet.astro";
3
3
-
---
4
4
-
5
5
-
<Applet title="Diffuse ui/audio applet">
6
6
-
<div>
7
7
-
<button>▶️</button>
8
8
-
<progress max="100" value="0"></progress>
9
9
-
</div>
10
10
-
11
11
-
<style>
12
12
-
@import "../../../../../styles/pages/themes/sakura/variables.css";
13
13
-
14
14
-
div {
15
15
-
/* 5% darker version */
16
16
-
background: oklch(from var(--reading-tea-leaves) calc(l - 0.05) c h);
17
17
-
18
18
-
height: 100px;
19
19
-
line-height: 100px;
20
20
-
text-align: center;
21
21
-
}
22
22
-
</style>
23
23
-
24
24
-
<script>
25
25
-
import { applets } from "@web-applets/sdk";
26
26
-
27
27
-
interface State {
28
28
-
isPlaying: boolean;
29
29
-
}
30
30
-
31
31
-
const context = applets.register<State>();
32
32
-
33
33
-
////////////////////////////////////////////
34
34
-
// Initial state
35
35
-
////////////////////////////////////////////
36
36
-
context.data = {
37
37
-
isPlaying: false,
38
38
-
};
39
39
-
40
40
-
////////////////////////////////////////////
41
41
-
// Actions
42
42
-
////////////////////////////////////////////
43
43
-
context.setActionHandler("set_is_playing", (isPlaying: boolean) => {
44
44
-
context.data.isPlaying = isPlaying;
45
45
-
render();
46
46
-
});
47
47
-
48
48
-
context.setActionHandler("set_progress", (progress: number) => {
49
49
-
document.body.querySelector("progress").value = Math.round(progress * 100);
50
50
-
render();
51
51
-
});
52
52
-
53
53
-
////////////////////////////////////////////
54
54
-
// DOM
55
55
-
////////////////////////////////////////////
56
56
-
document.body.querySelector("button").onclick = () => {
57
57
-
context.data = { isPlaying: !(context.data?.isPlaying ?? false) };
58
58
-
};
59
59
-
60
60
-
function render() {
61
61
-
document.body.querySelector("button").innerText = context.data.isPlaying ? "⏸️" : "▶️";
62
62
-
}
63
63
-
64
64
-
render();
65
65
-
</script>
66
66
-
</Applet>
···
1
1
-
import { manifest } from "../../../../../common/pages/manifest.ts";
2
2
-
3
3
-
export const GET = manifest({
4
4
-
name: "diffuse/ui/audio",
5
5
-
entrypoint: "index.html",
6
6
-
actions: {
7
7
-
set_is_playing: {
8
8
-
title: "Set is-playing state",
9
9
-
description: "Indicate if audio is playing or not.",
10
10
-
params_schema: {
11
11
-
type: "boolean",
12
12
-
},
13
13
-
},
14
14
-
set_progress: {
15
15
-
title: "Set progress",
16
16
-
description: "Indicate how far the audio has progressed.",
17
17
-
params_schema: {
18
18
-
type: "number",
19
19
-
},
20
20
-
},
21
21
-
},
22
22
-
});
···
1
1
-
:root {
2
2
-
/* Colors */
3
3
-
--reading-tea-leaves: #7b6467;
4
4
-
--sakura: #e1b3b9;
5
5
-
}
···
1
1
+
:root {
2
2
+
/* Colors */
3
3
+
/* https://farbvelo.elastiq.ch/?s=eyJzIjoiZTBjNjIyMTdiNTcxZSIsImEiOjYsImNnIjo0LCJoZyI6dHJ1ZSwiaGIiOmZhbHNlLCJobyI6ZmFsc2UsImhjIjpmYWxzZSwiaHQiOmZhbHNlLCJiIjpmYWxzZSwicCI6MC4xNzUsIm1kIjo2MCwiY20iOiJsYWIiLCJmIjoiTGVnYWN5IiwiYyI6ImhzbHV2Iiwic2MiOmZhbHNlLCJidyI6dHJ1ZSwiYWgiOmZhbHNlLCJpdSI6IiIsImxtIjp0cnVlLCJzbSI6ZmFsc2UsImN2IjoiaGV4IiwicW0iOiJhcnQtcGFsZXR0ZSIsIm5sIjoiYmVzdE9mIn0= */
4
4
+
--moonscape: #7f6c71;
5
5
+
--grandma’s-pink-tiles: #e1bac0;
6
6
+
--cinderella: #f8d1c6;
7
7
+
--young-apricot: #f8d7b6;
8
8
+
--cereal-flake: #f0d8ad;
9
9
+
--oatmeal: #cdc5b9;
10
10
+
11
11
+
/* https://farbvelo.elastiq.ch/?s=eyJzIjoiZmZjY2JkZDg2ZjEzYiIsImEiOjYsImNnIjo0LCJoZyI6dHJ1ZSwiaGIiOmZhbHNlLCJobyI6ZmFsc2UsImhjIjpmYWxzZSwiaHQiOmZhbHNlLCJiIjpmYWxzZSwicCI6MC4xNzgzMDcwODQxNjMzNDY2LCJtZCI6NjAsImNtIjoibGFiIiwiZiI6IkxlZ2FjeSIsImMiOiJoc2x1diIsInNjIjpmYWxzZSwiYnciOnRydWUsImFoIjpmYWxzZSwiaXUiOiIiLCJsbSI6dHJ1ZSwic20iOmZhbHNlLCJjdiI6ImhzbCIsInFtIjoiYXJ0LXBhbGV0dGUiLCJubCI6ImJlc3RPZiJ9 */
12
12
+
--made-in-the-shade: #67717c;
13
13
+
--misty-mountains: #b8cce0;
14
14
+
--lucid-dreams: #c7e6f4;
15
15
+
--icy-breeze: #c2eff1;
16
16
+
--crushed-ice: #bdf5ed;
17
17
+
--water-leaf: #b7efe7;
18
18
+
19
19
+
/* https://farbvelo.elastiq.ch/?s=eyJzIjoiODJiN2FjMjU1ODRiOCIsImEiOjYsImNnIjo0LCJoZyI6dHJ1ZSwiaGIiOmZhbHNlLCJobyI6ZmFsc2UsImhjIjpmYWxzZSwiaHQiOmZhbHNlLCJiIjpmYWxzZSwicCI6MC4yMTkxOTgyMDcxNzEzMTQ3LCJtZCI6NjAsImNtIjoibGFiIiwiZiI6IkxlZ2FjeSIsImMiOiJoc2x1diIsInNjIjpmYWxzZSwiYnciOnRydWUsImFoIjpmYWxzZSwiaXUiOiIiLCJsbSI6dHJ1ZSwic20iOmZhbHNlLCJjdiI6ImhleCIsInFtIjoiYXJ0LXBhbGV0dGUiLCJubCI6ImJlc3RPZiJ9 */
20
20
+
--wizards-brew: #9d8bb3;
21
21
+
--innocent-snowdrop: #cec0fa;
22
22
+
--foggy-plateau: #d5d2fb;
23
23
+
--puffy-cloud: #dce3fb;
24
24
+
--diamond-white: #e1f4fb;
25
25
+
--delicate-cloud: #d9dbe4;
26
26
+
}