src
pages
configurator
engine
input
orchestrator
input-cache
output-management
single-queue
output
processor
scripts
···
22
22
"packageJson": {
23
23
"dependencies": [
24
24
"npm:98.css@~0.1.21",
25
25
-
"npm:@automerge/automerge@^2.2.9",
25
25
+
"npm:@automerge/automerge@^3.0.0-beta.0",
26
26
"npm:@jsr/bradenmacdonald__s3-lite-client@0.9",
27
27
"npm:@jsr/std__media-types@^1.1.0",
28
28
"npm:@picocss/pico@^2.1.1",
···
5
5
"packages": {
6
6
"": {
7
7
"dependencies": {
8
8
-
"@automerge/automerge": "^2.2.9",
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
10
"@picocss/pico": "^2.1.1",
11
11
"@std/media-types": "npm:@jsr/std__media-types@^1.1.0",
···
112
112
}
113
113
},
114
114
"node_modules/@automerge/automerge": {
115
115
-
"version": "2.2.9",
116
116
-
"resolved": "https://registry.npmjs.org/@automerge/automerge/-/automerge-2.2.9.tgz",
117
117
-
"integrity": "sha512-6HM52Ops79hAQBWMg/t0MNfGOdEiXyenQjO9F1hKZq0RWDsMLpPa1SzRy/C4/4UyX67sTHuA5CwBpH34SpfZlA==",
115
115
+
"version": "3.0.0-preview.13",
116
116
+
"resolved": "https://registry.npmjs.org/@automerge/automerge/-/automerge-3.0.0-preview.13.tgz",
117
117
+
"integrity": "sha512-1r7ggaTqsQ4PHGv45QjVOxPOvJIKjSrHY+HTiFxCU04Qlx3kvXxDLVyBbZeN1jg2I+Y8tpuG0eVtC4QxL9wGIg==",
118
118
"license": "MIT",
119
119
"dependencies": {
120
120
"uuid": "^9.0.0"
···
1
1
{
2
2
"dependencies": {
3
3
-
"@automerge/automerge": "^2.2.9",
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
5
"@picocss/pico": "^2.1.1",
6
6
"@std/media-types": "npm:@jsr/std__media-types@^1.1.0",
···
30
30
</style>
31
31
32
32
<script>
33
33
-
import { applets } from "@web-applets/sdk";
34
34
-
35
33
import type { Track } from "@applets/core/types.d.ts";
36
36
-
import { applet } from "@scripts/theme";
34
34
+
import { applet, register } from "@scripts/applets/common";
37
35
38
36
////////////////////////////////////////////
39
37
// SETUP
40
38
////////////////////////////////////////////
41
41
-
const context = applets.register<{ ready: boolean }>();
39
39
+
const context = register<{ ready: boolean }>();
42
40
43
41
// Initial state
44
42
context.data = {
···
39
39
import scope from "astro:scope";
40
40
import { type Signal, computed, effect, signal } from "spellcaster/spellcaster.js";
41
41
import { type ElementConfigurator, repeat, text } from "spellcaster/hyperscript.js";
42
42
-
import { applets } from "@web-applets/sdk";
43
42
44
44
-
import { applet, hs } from "@src/scripts/theme";
43
43
+
import { applet, hs, register } from "@scripts/applets/common";
45
44
import type { OutputGetter, OutputSetter } from "@applets/core/types.d.ts";
46
45
47
46
const METHODS = ["browser", "custom", "device"] as const;
···
63
62
////////////////////////////////////////////
64
63
// SETUP
65
64
////////////////////////////////////////////
66
66
-
const context = applets.register<{ ready: boolean }>();
65
65
+
const context = register<{ ready: boolean }>();
67
66
68
67
// Applets container
69
68
const container = document.createElement("div");
···
1
1
<script>
2
2
-
import { applets } from "@web-applets/sdk";
3
3
-
import { State, Track, TrackState } from "./types";
2
2
+
import type { State, Track, TrackState } from "./types";
3
3
+
import { register } from "@scripts/applets/common";
4
4
5
5
////////////////////////////////////////////
6
6
// CONSTANTS
···
11
11
////////////////////////////////////////////
12
12
// SETUP
13
13
////////////////////////////////////////////
14
14
-
const context = applets.register<State>();
14
14
+
const context = register<State>();
15
15
+
16
16
+
// Audio elements container
15
17
const container = document.createElement("div");
16
16
-
17
18
container.id = "container";
18
19
document.body.appendChild(container);
19
20
···
40
41
////////////////////////////////////////////
41
42
// ACTIONS
42
43
////////////////////////////////////////////
43
43
-
context.setActionHandler(
44
44
-
"render",
45
45
-
async (args: { play?: { trackId: string; volume?: number }; tracks: Track[] }) => {
46
46
-
await render(args.tracks);
47
47
-
if (args.play) play({ trackId: args.play.trackId, volume: args.play.volume });
48
48
-
},
49
49
-
);
50
50
-
51
44
context.setActionHandler("pause", pause);
52
45
context.setActionHandler("play", play);
53
46
context.setActionHandler("reload", reload);
47
47
+
context.setActionHandler("render", render);
54
48
context.setActionHandler("seek", seek);
55
49
context.setActionHandler("volume", volume);
56
50
···
102
96
});
103
97
}
104
98
99
99
+
async function render(args: { play?: { trackId: string; volume?: number }; tracks: Track[] }) {
100
100
+
await renderTracks(args.tracks);
101
101
+
if (args.play) play({ trackId: args.play.trackId, volume: args.play.volume });
102
102
+
}
103
103
+
105
104
function seek({ percentage, trackId }: { percentage: number; trackId: string }) {
106
105
withAudioNode(trackId, (audio) => {
107
106
if (!isNaN(audio.duration)) {
···
122
121
////////////////////////////////////////////
123
122
// RENDER
124
123
////////////////////////////////////////////
125
125
-
async function render(tracks: Array<Track>) {
124
124
+
async function renderTracks(tracks: Array<Track>) {
126
125
const ids = tracks.map((e) => e.id);
127
126
const existingNodes: Record<string, HTMLAudioElement> = {};
128
127
···
1
1
<script>
2
2
-
import { applets } from "@web-applets/sdk";
3
2
import { QueueItem, State } from "./types";
3
3
+
import { register } from "@scripts/applets/common";
4
4
5
5
////////////////////////////////////////////
6
6
// SETUP
7
7
////////////////////////////////////////////
8
8
-
const context = applets.register<State>();
8
8
+
const context = register<State>();
9
9
10
10
// Initial state
11
11
context.data = {
···
16
16
</main>
17
17
18
18
<script>
19
19
-
import { applets } from "@web-applets/sdk";
20
19
import { computed, effect, Signal, signal } from "spellcaster";
21
20
import { repeat, tags, text } from "spellcaster/hyperscript.js";
22
21
import { type FileSystemDirectoryHandle, showDirectoryPicker } from "native-file-system-adapter";
···
26
25
27
26
import type { Track } from "@applets/core/types.d.ts";
28
27
import { isAudioFile } from "@scripts/inputs/common";
28
28
+
import { register } from "@scripts/applets/common";
29
29
30
30
import manifest from "./_manifest.json";
31
31
···
41
41
const SCHEME = manifest.input_properties.scheme;
42
42
43
43
// Register applet
44
44
-
const context = applets.register();
44
44
+
const context = register();
45
45
46
46
////////////////////////////////////////////
47
47
// UI
···
38
38
39
39
<script>
40
40
import { S3Client } from "@bradenmacdonald/s3-lite-client";
41
41
-
import { applets } from "@web-applets/sdk";
42
41
import { computed, effect, Signal, signal } from "spellcaster";
43
42
import { type Props, repeat, tags, text } from "spellcaster/hyperscript.js";
44
43
import * as IDB from "idb-keyval";
···
46
45
import QS from "query-string";
47
46
48
47
import type { Track } from "@applets/core/types.d.ts";
49
49
-
50
50
-
import manifest from "./_manifest.json";
51
48
import { isAudioFile } from "@scripts/inputs/common";
49
49
+
import { register } from "@scripts/applets/common";
50
50
+
import manifest from "./_manifest.json";
52
51
53
52
type Bucket = {
54
53
accessKey: string;
···
86
85
const SCHEME = manifest.input_properties.scheme;
87
86
88
87
// Register applet
89
89
-
const context = applets.register();
88
88
+
const context = register();
90
89
91
90
////////////////////////////////////////////
92
91
// UI
···
1
1
<script>
2
2
-
import { applets } from "@web-applets/sdk";
3
3
-
4
2
import type { ResolvedUri, Track } from "@applets/core/types.d.ts";
5
5
-
import { applet, waitUntilAppletData, waitUntilAppletIsReady } from "@scripts/theme";
3
3
+
4
4
+
import {
5
5
+
applet,
6
6
+
register,
7
7
+
waitUntilAppletData,
8
8
+
waitUntilAppletIsReady,
9
9
+
} from "@scripts/applets/common";
6
10
7
11
////////////////////////////////////////////
8
12
// SETUP
9
13
////////////////////////////////////////////
10
14
import type * as OutputOrchestrator from "@applets/orchestrator/output-management/types.d.ts";
11
15
12
12
-
const context = applets.register<{ isProcessing: boolean; ready: boolean }>();
13
13
-
const topContext = self.top || self.parent;
16
16
+
const context = register<{ isProcessing: boolean; ready: boolean }>();
14
17
15
18
// Initial data
16
19
context.data = {
···
20
23
21
24
// Applet connections
22
25
const configurator = {
23
23
-
input: await applet("../../configurator/input", { context: topContext }),
26
26
+
input: await applet("../../configurator/input"),
24
27
};
25
28
26
29
const orchestrator = {
27
27
-
output: await applet<OutputOrchestrator.State>("../../orchestrator/output-management", {
28
28
-
context: topContext,
29
29
-
}),
30
30
+
output: await applet<OutputOrchestrator.State>("../../orchestrator/output-management"),
30
31
};
31
32
32
33
const processor = {
33
33
-
metadataFetcher: await applet("../../processor/metadata-fetcher", {
34
34
-
context: topContext,
35
35
-
}),
34
34
+
metadataFetcher: await applet("../../processor/metadata-fetcher"),
36
35
};
37
36
38
37
// 🚀
···
68
67
"resolve",
69
68
{ method: "GET", uri: track.uri },
70
69
{
71
71
-
timeoutDuration: 60000,
70
70
+
timeoutDuration: 60000 * 5,
72
71
},
73
72
);
74
73
···
76
75
"resolve",
77
76
{ method: "HEAD", uri: track.uri },
78
77
{
79
79
-
timeoutDuration: 60000,
78
78
+
timeoutDuration: 60000 * 5,
80
79
},
81
80
);
82
81
···
86
85
"extract",
87
86
{ urls: { get: resGet.url, head: resHead?.url || resGet.url } },
88
87
{
89
89
-
timeoutDuration: 60000,
88
88
+
timeoutDuration: 60000 * 15,
90
89
},
91
90
);
92
91
···
99
98
100
99
// Save
101
100
await orchestrator.output.sendAction("tracks", tracksWithMetadata, {
102
102
-
timeoutDuration: 60000 * 2,
101
101
+
timeoutDuration: 60000 * 5,
103
102
});
104
103
105
104
// Fin
···
1
1
<!-- TODO: Button to export all user/output data. --><!-- TODO: Button to import data? -->
2
2
<script>
3
3
-
import { applets } from "@web-applets/sdk";
4
3
import { debounce } from "throttle-debounce";
5
4
import * as Automerge from "@automerge/automerge";
6
5
7
6
import type { Track } from "@applets/core/types.d.ts";
8
7
import type { State } from "./types.d.ts";
9
9
-
import { applet, waitUntilAppletIsReady } from "@scripts/theme";
8
8
+
import { applet, register, waitUntilAppletIsReady } from "@scripts/applets/common";
10
9
11
10
////////////////////////////////////////////
12
11
// SETUP
13
12
////////////////////////////////////////////
14
14
-
const context = applets.register<State>();
13
13
+
const context = register<State>();
15
14
16
15
// Initial data
17
16
context.data = {
18
18
-
tracks: Automerge.from({ collection: [] }, {}),
17
17
+
tracks: Automerge.from({ collection: [] }),
19
18
20
19
hasSyncedTracks: false,
21
20
···
24
23
25
24
// Applet connections
26
25
const configurator = {
27
27
-
output: await applet("../../configurator/output", { context: self.top || self.parent }),
26
26
+
output: await applet("../../configurator/output"),
28
27
};
29
28
30
30
-
// Load tracks
31
31
-
loadTracks().then((doc) => {
32
32
-
if (doc) {
33
33
-
const mergedDoc = Automerge.merge(doc, context.data.tracks);
34
34
-
update({ tracks: mergedDoc });
35
35
-
}
29
29
+
// Load tracks if needed
30
30
+
if (context.isMainInstance())
31
31
+
loadTracks().then((doc) => {
32
32
+
if (doc) {
33
33
+
const mergedDoc = Automerge.merge(doc, context.data.tracks);
34
34
+
update({ tracks: mergedDoc });
35
35
+
}
36
36
37
37
-
update({ hasSyncedTracks: true });
38
38
-
});
37
37
+
update({ hasSyncedTracks: true });
38
38
+
});
39
39
40
40
// State helpers
41
41
function update(partial: Partial<State>): void {
···
43
43
}
44
44
45
45
function updateTracks(tracks: Track[]): Automerge.Doc<{ collection: Track[] }> {
46
46
+
console.log(context.data.tracks);
47
47
+
46
48
const doc = Automerge.change(context.data.tracks, (d) => {
47
49
d.collection = cleanUndefinedValuesForTracks(tracks);
48
50
});
···
1
1
<script>
2
2
-
import { applets } from "@web-applets/sdk";
3
3
-
4
2
import type { ResolvedUri, Track } from "@applets/core/types.d.ts";
5
5
-
import { applet, comparable, reactive } from "@scripts/theme";
3
3
+
import { applet, comparable, reactive, register } from "@scripts/applets/common";
6
4
7
5
////////////////////////////////////////////
8
6
// SETUP
···
12
10
import type * as OutputOrchestrator from "@applets/orchestrator/output-management/types.d.ts";
13
11
14
12
// Register applet
15
15
-
const context = applets.register<unknown>();
13
13
+
const context = register<unknown>();
16
14
17
15
// Applet connections
18
16
const configurator = {
19
19
-
input: await applet("../../configurator/input", {
20
20
-
context: self.top || self.parent,
21
21
-
}),
17
17
+
input: await applet("../../configurator/input"),
22
18
};
23
19
24
20
const engine = {
25
25
-
audio: await applet<AudioEngine.State>("../../engine/audio", {
26
26
-
context: self.top || self.parent,
27
27
-
}),
28
28
-
queue: await applet<QueueEngine.State>("../../engine/queue", {
29
29
-
context: self.top || self.parent,
30
30
-
}),
21
21
+
audio: await applet<AudioEngine.State>("../../engine/audio"),
22
22
+
queue: await applet<QueueEngine.State>("../../engine/queue"),
31
23
};
32
24
33
25
const orchestrator = {
34
34
-
output: await applet<OutputOrchestrator.State>("../../orchestrator/output-management", {
35
35
-
context: self.top || self.parent,
36
36
-
}),
26
26
+
output: await applet<OutputOrchestrator.State>("../../orchestrator/output-management"),
37
27
};
38
28
39
29
////////////////////////////////////////////
···
1
1
<script>
2
2
import * as IDB from "idb-keyval";
3
3
-
import { applets } from "@web-applets/sdk";
4
3
5
4
import type { OutputGetter, OutputSetter } from "@applets/core/types.d.ts";
5
5
+
import { register } from "@scripts/applets/common";
6
6
7
7
////////////////////////////////////////////
8
8
// SETUP
9
9
////////////////////////////////////////////
10
10
const IDB_PREFIX = "@applets/output/indexed-db";
11
11
-
const context = applets.register();
11
11
+
const context = register();
12
12
13
13
////////////////////////////////////////////
14
14
// ACTIONS
···
1
1
<script>
2
2
import * as IDB from "idb-keyval";
3
3
-
import { applets } from "@web-applets/sdk";
4
3
import { type FileSystemDirectoryHandle, showDirectoryPicker } from "native-file-system-adapter";
5
4
6
5
import type { OutputGetter, OutputSetter } from "@applets/core/types.d.ts";
6
6
+
import { register } from "@scripts/applets/common";
7
7
8
8
////////////////////////////////////////////
9
9
// SETUP
···
11
11
const IDB_PREFIX = "@applets/output/native-fs";
12
12
const IDB_DEVICE_KEY = `${IDB_PREFIX}/device`;
13
13
14
14
-
const context = applets.register();
14
14
+
const context = register();
15
15
16
16
////////////////////////////////////////////
17
17
// ACTIONS
···
24
24
async function extract(args: { mimeType?: string; stream?: ReadableStream; urls?: Urls }) {
25
25
// Construct records
26
26
// TODO: Use other metadata lib as fallback: https://github.com/buzz/mediainfo.js
27
27
-
const { stats, tags } = await musicMetadataTags(args, false);
27
27
+
const { stats, tags } = await musicMetadataTags(args, false).catch(() => ({
28
28
+
stats: undefined,
29
29
+
tags: undefined,
30
30
+
}));
28
31
29
32
// Fin
30
33
return { stats, tags };
···
1
1
+
import type { Applet, AppletEvent } from "@web-applets/sdk";
2
2
+
3
3
+
import { applets } from "@web-applets/sdk";
4
4
+
import { type ElementConfigurator, h } from "spellcaster/hyperscript.js";
5
5
+
import { effect, isSignal, Signal, signal } from "spellcaster/spellcaster.js";
6
6
+
import { xxh32 } from "xxh32";
7
7
+
8
8
+
////////////////////////////////////////////
9
9
+
// 🪟 Applet connector
10
10
+
////////////////////////////////////////////
11
11
+
export async function applet<D>(
12
12
+
src: string,
13
13
+
opts: {
14
14
+
addSlashSuffix?: boolean;
15
15
+
container?: HTMLElement | Element;
16
16
+
id?: string;
17
17
+
setHeight?: boolean;
18
18
+
} = {},
19
19
+
): Promise<Applet<D>> {
20
20
+
src = `${src}${
21
21
+
src.endsWith("/")
22
22
+
? ""
23
23
+
: opts.addSlashSuffix === undefined || opts.addSlashSuffix === true
24
24
+
? "/"
25
25
+
: ""
26
26
+
}`;
27
27
+
28
28
+
const existingFrame: HTMLIFrameElement | null = window.document.querySelector(`[src="${src}"]`);
29
29
+
30
30
+
let frame;
31
31
+
32
32
+
if (existingFrame) {
33
33
+
frame = existingFrame;
34
34
+
} else {
35
35
+
frame = document.createElement("iframe");
36
36
+
frame.src = src;
37
37
+
if (opts.id) frame.id = opts.id;
38
38
+
39
39
+
if (opts.container) {
40
40
+
opts.container.appendChild(frame);
41
41
+
} else {
42
42
+
window.document.body.appendChild(frame);
43
43
+
}
44
44
+
}
45
45
+
46
46
+
if (frame.contentWindow === null) {
47
47
+
throw new Error("iframe does not have a contentWindow");
48
48
+
}
49
49
+
50
50
+
const applet = await applets.connect<D>(frame.contentWindow).catch((err) => {
51
51
+
console.error("Error connecting to " + src, err);
52
52
+
throw err;
53
53
+
});
54
54
+
55
55
+
if (opts.setHeight) {
56
56
+
applet.onresize = () => {
57
57
+
frame.height = `${applet.height}px`;
58
58
+
frame.classList.add("has-loaded");
59
59
+
};
60
60
+
} else {
61
61
+
if (frame.contentDocument?.readyState === "complete") {
62
62
+
frame.classList.add("has-loaded");
63
63
+
}
64
64
+
65
65
+
frame.addEventListener("load", () => {
66
66
+
frame.classList.add("has-loaded");
67
67
+
});
68
68
+
}
69
69
+
70
70
+
return applet;
71
71
+
}
72
72
+
73
73
+
////////////////////////////////////////////
74
74
+
// 🪟 Applet registration
75
75
+
////////////////////////////////////////////
76
76
+
export function register<DataType = any>() {
77
77
+
const id = `${location.host}${location.pathname}`;
78
78
+
const scope = applets.register<DataType>();
79
79
+
80
80
+
let isMainInstance = true;
81
81
+
let waitingForPong = true;
82
82
+
83
83
+
// One instance to rule them all
84
84
+
//
85
85
+
// Ping other instances to see if there are any.
86
86
+
// As long as there aren't any, it is considered the main instance.
87
87
+
//
88
88
+
// Actions are performed on the main instance,
89
89
+
// and data is replicated from main to the other instances.
90
90
+
const channel = new BroadcastChannel(id);
91
91
+
92
92
+
channel.addEventListener("message", async (event) => {
93
93
+
if (event.data === "PING") {
94
94
+
channel.postMessage("PONG");
95
95
+
} else if (event.data?.type === "data") {
96
96
+
scope.data = event.data.data;
97
97
+
} else if (waitingForPong && event.data === "PONG") {
98
98
+
waitingForPong = false;
99
99
+
isMainInstance = false;
100
100
+
} else if (isMainInstance && event.data?.type === "action" && event.data?.actionId) {
101
101
+
const result = await scope.actionHandlers[event.data.actionId]?.(...event.data.arguments);
102
102
+
channel.postMessage({
103
103
+
type: "actioncomplete",
104
104
+
id: event.data.id,
105
105
+
result,
106
106
+
});
107
107
+
}
108
108
+
});
109
109
+
110
110
+
setTimeout(() => (waitingForPong = false), 1000);
111
111
+
112
112
+
channel.postMessage("PING");
113
113
+
114
114
+
scope.ondata = (event) => {
115
115
+
if (isMainInstance) {
116
116
+
channel.postMessage({
117
117
+
type: "data",
118
118
+
data: event.data,
119
119
+
});
120
120
+
}
121
121
+
};
122
122
+
123
123
+
return {
124
124
+
get id() {
125
125
+
return id;
126
126
+
},
127
127
+
128
128
+
get data() {
129
129
+
return scope.data;
130
130
+
},
131
131
+
132
132
+
set data(data: DataType) {
133
133
+
scope.data = data;
134
134
+
},
135
135
+
136
136
+
isMainInstance() {
137
137
+
return isMainInstance;
138
138
+
},
139
139
+
140
140
+
setActionHandler: <H extends Function>(actionId: string, actionHandler: H) => {
141
141
+
const handler = (...args: any) => {
142
142
+
if (isMainInstance) {
143
143
+
return actionHandler(...args);
144
144
+
}
145
145
+
146
146
+
const actionMessage = {
147
147
+
id: crypto.randomUUID(),
148
148
+
type: "action",
149
149
+
actionId,
150
150
+
arguments: args,
151
151
+
};
152
152
+
153
153
+
return new Promise((resolve) => {
154
154
+
const actionCallback = (event: MessageEvent) => {
155
155
+
if (event.data?.type === "actioncomplete" && event.data?.id === actionMessage.id) {
156
156
+
channel.removeEventListener("message", actionCallback);
157
157
+
resolve(event.data.result);
158
158
+
}
159
159
+
};
160
160
+
161
161
+
channel.addEventListener("message", actionCallback);
162
162
+
channel.postMessage(actionMessage);
163
163
+
});
164
164
+
};
165
165
+
166
166
+
scope.setActionHandler(actionId, handler);
167
167
+
},
168
168
+
};
169
169
+
}
170
170
+
171
171
+
////////////////////////////////////////////
172
172
+
// 🔮 Reactive state management
173
173
+
////////////////////////////////////////////
174
174
+
export function reactive<D, T>(
175
175
+
applet: Applet<D>,
176
176
+
dataFn: (data: D) => T,
177
177
+
effectFn: (t: T) => void,
178
178
+
) {
179
179
+
const [getter, setter] = signal(dataFn(applet.data));
180
180
+
181
181
+
effect(() => {
182
182
+
effectFn(getter());
183
183
+
return undefined;
184
184
+
});
185
185
+
186
186
+
applet.addEventListener("data", (event: AppletEvent) => {
187
187
+
setter(dataFn(event.data));
188
188
+
});
189
189
+
}
190
190
+
191
191
+
////////////////////////////////////////////
192
192
+
// 🛠️
193
193
+
////////////////////////////////////////////
194
194
+
export function addScope<O extends object>(astroScope: string, object: O): O {
195
195
+
return {
196
196
+
...object,
197
197
+
attrs: {
198
198
+
...((object as any).attrs || {}),
199
199
+
[`data-astro-cid-${astroScope}`]: "",
200
200
+
},
201
201
+
};
202
202
+
}
203
203
+
204
204
+
export function appletScopePort() {
205
205
+
let port: MessagePort | undefined;
206
206
+
207
207
+
function connection(event: AppletEvent) {
208
208
+
if (event.data?.type === "appletconnect") {
209
209
+
window.removeEventListener("message", connection);
210
210
+
port = (event as any).ports[0];
211
211
+
}
212
212
+
}
213
213
+
214
214
+
window.addEventListener("message", connection);
215
215
+
216
216
+
return () => port;
217
217
+
}
218
218
+
219
219
+
export function comparable(value: unknown) {
220
220
+
return xxh32(JSON.stringify(value));
221
221
+
}
222
222
+
223
223
+
export function hs(
224
224
+
tag: string,
225
225
+
astroScope: string,
226
226
+
props?: Record<string, unknown> | Signal<Record<string, unknown>>,
227
227
+
configure?: ElementConfigurator,
228
228
+
) {
229
229
+
const propsWithScope =
230
230
+
props && isSignal(props)
231
231
+
? () => addScope(astroScope, props())
232
232
+
: addScope(astroScope, props || {});
233
233
+
234
234
+
return h(tag, propsWithScope, configure);
235
235
+
}
236
236
+
237
237
+
export function isPrimitive(test: unknown) {
238
238
+
return test !== Object(test);
239
239
+
}
240
240
+
241
241
+
export function waitUntilAppletData<A>(
242
242
+
applet: Applet<A>,
243
243
+
dataFn: (a: A | undefined) => boolean,
244
244
+
): Promise<void> {
245
245
+
return new Promise((resolve) => {
246
246
+
if (dataFn(applet.data) === true) {
247
247
+
resolve();
248
248
+
return;
249
249
+
}
250
250
+
251
251
+
const callback = (event: AppletEvent) => {
252
252
+
if (dataFn(event.data) === true) {
253
253
+
applet.removeEventListener("data", callback);
254
254
+
resolve();
255
255
+
}
256
256
+
};
257
257
+
258
258
+
applet.addEventListener("data", callback);
259
259
+
});
260
260
+
}
261
261
+
262
262
+
export function waitUntilAppletIsReady(applet: Applet): Promise<void> {
263
263
+
return waitUntilAppletData(applet, (data) => !!data?.ready);
264
264
+
}
···
1
1
-
import type { Applet, AppletEvent } from "@web-applets/sdk";
2
2
-
3
3
-
import { applets } from "@web-applets/sdk";
4
4
-
import { type ElementConfigurator, h } from "spellcaster/hyperscript.js";
5
5
-
import { effect, isSignal, Signal, signal } from "spellcaster/spellcaster.js";
6
6
-
import { xxh32 } from "xxh32";
7
7
-
8
8
-
////////////////////////////////////////////
9
9
-
// 🪟 Applet initialiser
10
10
-
////////////////////////////////////////////
11
11
-
export async function applet<D>(
12
12
-
src: string,
13
13
-
opts: {
14
14
-
addSlashSuffix?: boolean;
15
15
-
context?: Window;
16
16
-
container?: HTMLElement | Element;
17
17
-
id?: string;
18
18
-
setHeight?: boolean;
19
19
-
} = {},
20
20
-
): Promise<Applet<D>> {
21
21
-
src = `${src}${
22
22
-
src.endsWith("/")
23
23
-
? ""
24
24
-
: opts.addSlashSuffix === undefined || opts.addSlashSuffix === true
25
25
-
? "/"
26
26
-
: ""
27
27
-
}`;
28
28
-
29
29
-
const existingFrame: HTMLIFrameElement | null = (opts.context || window).document.querySelector(
30
30
-
`[src="${src}"]`,
31
31
-
);
32
32
-
33
33
-
let frame;
34
34
-
35
35
-
if (existingFrame) {
36
36
-
frame = existingFrame;
37
37
-
} else {
38
38
-
frame = document.createElement("iframe");
39
39
-
frame.src = src;
40
40
-
if (opts.id) frame.id = opts.id;
41
41
-
42
42
-
if (opts.container) {
43
43
-
opts.container.appendChild(frame);
44
44
-
} else {
45
45
-
(opts.context || window).document.body.appendChild(frame);
46
46
-
}
47
47
-
}
48
48
-
49
49
-
if (frame.contentWindow === null) {
50
50
-
throw new Error("iframe does not have a contentWindow");
51
51
-
}
52
52
-
53
53
-
const applet = await applets
54
54
-
.connect<D>(frame.contentWindow, {
55
55
-
context: opts.context,
56
56
-
})
57
57
-
.catch((err) => {
58
58
-
console.error("Error connecting to " + src, err);
59
59
-
throw err;
60
60
-
});
61
61
-
62
62
-
if (opts.setHeight) {
63
63
-
applet.onresize = () => {
64
64
-
frame.height = `${applet.height}px`;
65
65
-
frame.classList.add("has-loaded");
66
66
-
};
67
67
-
} else {
68
68
-
if (frame.contentDocument?.readyState === "complete") {
69
69
-
frame.classList.add("has-loaded");
70
70
-
}
71
71
-
72
72
-
frame.addEventListener("load", () => {
73
73
-
frame.classList.add("has-loaded");
74
74
-
});
75
75
-
}
76
76
-
77
77
-
return applet;
78
78
-
}
79
79
-
80
80
-
////////////////////////////////////////////
81
81
-
// 🔮 Reactive state management
82
82
-
////////////////////////////////////////////
83
83
-
export function reactive<D, T>(
84
84
-
applet: Applet<D>,
85
85
-
dataFn: (data: D) => T,
86
86
-
effectFn: (t: T) => void,
87
87
-
) {
88
88
-
const [getter, setter] = signal(dataFn(applet.data));
89
89
-
90
90
-
effect(() => {
91
91
-
effectFn(getter());
92
92
-
return undefined;
93
93
-
});
94
94
-
95
95
-
applet.addEventListener("data", (event: AppletEvent) => {
96
96
-
setter(dataFn(event.data));
97
97
-
});
98
98
-
}
99
99
-
100
100
-
////////////////////////////////////////////
101
101
-
// 🛠️
102
102
-
////////////////////////////////////////////
103
103
-
export function addScope<O extends object>(astroScope: string, object: O): O {
104
104
-
return {
105
105
-
...object,
106
106
-
attrs: {
107
107
-
...((object as any).attrs || {}),
108
108
-
[`data-astro-cid-${astroScope}`]: "",
109
109
-
},
110
110
-
};
111
111
-
}
112
112
-
113
113
-
export function comparable(value: unknown) {
114
114
-
return xxh32(JSON.stringify(value));
115
115
-
}
116
116
-
117
117
-
export function hs(
118
118
-
tag: string,
119
119
-
astroScope: string,
120
120
-
props?: Record<string, unknown> | Signal<Record<string, unknown>>,
121
121
-
configure?: ElementConfigurator,
122
122
-
) {
123
123
-
const propsWithScope =
124
124
-
props && isSignal(props)
125
125
-
? () => addScope(astroScope, props())
126
126
-
: addScope(astroScope, props || {});
127
127
-
128
128
-
return h(tag, propsWithScope, configure);
129
129
-
}
130
130
-
131
131
-
export function isPrimitive(test: unknown) {
132
132
-
return test !== Object(test);
133
133
-
}
134
134
-
135
135
-
export function waitUntilAppletData<A>(
136
136
-
applet: Applet<A>,
137
137
-
dataFn: (a: A | undefined) => boolean,
138
138
-
): Promise<void> {
139
139
-
return new Promise((resolve) => {
140
140
-
if (dataFn(applet.data) === true) {
141
141
-
resolve();
142
142
-
return;
143
143
-
}
144
144
-
145
145
-
const callback = (event: AppletEvent) => {
146
146
-
if (dataFn(event.data) === true) {
147
147
-
applet.removeEventListener("data", callback);
148
148
-
resolve();
149
149
-
}
150
150
-
};
151
151
-
152
152
-
applet.addEventListener("data", callback);
153
153
-
});
154
154
-
}
155
155
-
156
156
-
export function waitUntilAppletIsReady(applet: Applet): Promise<void> {
157
157
-
return waitUntilAppletData(applet, (data) => !!data?.ready);
158
158
-
}
···
1
1
-
import type { Output, Track } from "@applets/core/types.d.ts";
2
2
-
import { applet, reactive } from "../../theme.ts";
1
1
+
import { applet, reactive } from "@scripts/applets/common";
3
2
4
3
////////////////////////////////////////////
5
4
// 🎨 Styles
···
14
13
15
14
import type * as AudioUI from "@applets/themes/pilot/ui/audio/types.d.ts";
16
15
17
17
-
const _configurator = {
18
18
-
input: await applet("../../configurator/input"),
19
19
-
output: await applet("../../configurator/output"),
20
20
-
};
21
21
-
22
16
const engine = {
23
17
audio: await applet<AudioEngine.State>("../../engine/audio"),
24
18
queue: await applet<QueueEngine.State>("../../engine/queue"),
25
19
};
26
20
27
21
const _orchestrator = {
28
28
-
input: await applet<Output>("../../orchestrator/input-cache"),
29
29
-
output: await applet<Output>("../../orchestrator/output-management"),
22
22
+
input: await applet("../../orchestrator/input-cache"),
23
23
+
output: await applet("../../orchestrator/output-management"),
30
24
queue: await applet("../../orchestrator/single-queue"),
31
25
};
32
26
···
2
2
import { URLTrack } from "webamp";
3
3
4
4
import type { ResolvedUri, Track } from "@applets/core/types.d.ts";
5
5
-
import { applet } from "../../theme.ts";
5
5
+
import { applet } from "@scripts/applets/common";
6
6
7
7
////////////////////////////////////////////
8
8
// 🎨 Styles