Experiment to rebuild Diffuse using web applets.
0

Configure Feed

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

wip

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