diff --git a/deno.lock b/deno.lock index 42e0218..eb62dc4 100644 --- a/deno.lock +++ b/deno.lock @@ -2722,6 +2722,29 @@ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==" } }, + "redirects": { + "https://esm.sh/gh/unternet-co/web-applets/sdk": "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/sdk", + "https://esm.sh/gh/unternet-co/web-applets/sdk/src/index.ts": "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/sdk/src/index.ts" + }, + "remote": { + "https://denopkg.com/unternet-co/web-applets@e3e9ca6/sdk/src/index.ts": "46abe7bc8b6b678e4ecaee4905c9073816c62d22c5cbc5d6f5af0cda5ab27932", + "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/denonext/sdk.mjs": "70d2b9a7dfca16423d99246050c7abec35cf661fe39a544559b7a5dd226118a8", + "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/denonext/sdk/src/applets/applet-factory.nobundle.mjs": "61956718100039677766b50b1a7e506b6ce18e6222d4d22c53480b458a2f60d9", + "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/denonext/sdk/src/applets/applet-scope.nobundle.mjs": "219c4fd6a6ae2cc6741a91562f5d33f892f8c6c8d1709ce5077032844ac18c95", + "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/denonext/sdk/src/applets/applet.nobundle.mjs": "38bf32e3fedca7d37f042a347417659d6bac8fe8ed04021730a6b7b95a102eab", + "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/denonext/sdk/src/applets/errors.nobundle.mjs": "e1b6e635b6cfa5d2fe35517b6d2b82c667174aa541fdb21bce87ec26233d052b", + "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/denonext/sdk/src/applets/events.nobundle.mjs": "e1cacd49dda2242f31a298352fc6a30f993bd0c380c5b18bfbf0eacac510d661", + "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/denonext/sdk/src/constants.nobundle.mjs": "9a9aaeb0797585f3d662cc5e4f9e097e84cedf9d731d1ea8eaef379ac45dea14", + "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/denonext/sdk/src/debug.nobundle.mjs": "85fe667af343ef0692b2c77d041cf85fd5a00a106946c481c68d609380349b6d", + "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/denonext/sdk/src/elements/applet-frame.nobundle.mjs": "a96218721638a5c47b6393871f595e4074931c5ab1c28d5a77f1d3df579e5e1a", + "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/denonext/sdk/src/index.nobundle.mjs": "5fa4b5a068f064c6f04ad93a21480226242746c7f5339141d2855280962e9bd0", + "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/denonext/sdk/src/index.ts.mjs": "adcbf7cd6f62cd5eb1352f654e47b44d597c859132cb9b22196db28498172fee", + "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/denonext/sdk/src/index.ts.nobundle.mjs": "56dc8dd8586a38eb48dee0d7046ab9488b0a95b13324bcf35779efbe6d0cbd93", + "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/denonext/sdk/src/utils.nobundle.mjs": "e3ccd0d6e05ee075c23fcca025c2cd4100a1e94e73b7df049a5e1b4358849d59", + "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/sdk": "65f33669ecd21ed2e5d40f99f375b2e443740ee0db358a5876841a3c5b717cf7", + "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/sdk/src/index.ts": "83d82acfcb7be175545d516c26ed04a7a828b5b2612a78a556370ab0bfe160be", + "https://esm.sh/gh/unternet-co/web-applets@e3e9ca6/sdk/src/index.ts?bundle=false": "f83daf050b47537389e05b8fd38c3b6db1a32725876a56b21e91df901af8ac92" + }, "workspace": { "dependencies": [ "npm:@picocss/pico@^2.0.6", diff --git a/src/scripts/theme.ts b/src/scripts/theme.ts new file mode 100644 index 0000000..b83bebf --- /dev/null +++ b/src/scripts/theme.ts @@ -0,0 +1,61 @@ +import type { + Applet, + AppletEvent, +} from "https://denopkg.com/unternet-co/web-applets@e3e9ca6/sdk/src/index.ts"; +import { applets } from "@web-applets/sdk"; +import { effect, signal } from "spellcaster/spellcaster.js"; + +//////////////////////////////////////////// +// 🪟 Applet initialiser +//////////////////////////////////////////// +export async function applet( + src: string, + opts: { setHeight?: boolean } = {}, +): Promise> { + const frame: HTMLIFrameElement | null = document.querySelector( + `[src="${src}${src.endsWith("/") ? "" : "/"}"]`, + ); + + if (frame === null) throw new Error("iframe element not found, src: " + src); + if (frame.contentWindow === null) { + throw new Error("iframe does not have a contentWindow"); + } + + const applet = await applets.connect(frame.contentWindow); + + if (opts.setHeight) { + applet.onresize = () => { + frame.height = `${applet.height}px`; + frame.classList.add("has-loaded"); + }; + } else { + if (frame.contentDocument?.readyState === "complete") { + frame.classList.add("has-loaded"); + } + + frame.addEventListener("load", () => { + frame.classList.add("has-loaded"); + }); + } + + return applet; +} + +//////////////////////////////////////////// +// 🔮 Reactive state management +//////////////////////////////////////////// +export function reactive( + applet: Applet, + dataFn: (data: D) => T, + effectFn: (t: T) => void, +) { + const [getter, setter] = signal( + dataFn(applet.data), + ); + + effect(() => effectFn(getter())); + + applet.addEventListener("data", (event: AppletEvent) => { + setter(dataFn(event.data)); + }); +} diff --git a/src/scripts/themes/pilot/index.ts b/src/scripts/themes/pilot/index.ts index d963eaa..ef8369f 100644 --- a/src/scripts/themes/pilot/index.ts +++ b/src/scripts/themes/pilot/index.ts @@ -1,29 +1,33 @@ -import { type Applet, type AppletEvent, applets } from "@web-applets/sdk"; -import { effect, signal } from "spellcaster/spellcaster.js"; - -import type * as AudioEngine from "../../../applets/engine/audio/types.ts"; -import type * as AudioUI from "../../../applets/themes/pilot/ui/audio/types.ts"; +import { applet, reactive } from "../../theme.ts"; +//////////////////////////////////////////// +// 🎨 Styles +//////////////////////////////////////////// import "../../../styles/themes/pilot/index.css"; //////////////////////////////////////////// // 🗂️ Applets //////////////////////////////////////////// +import type * as AudioEngine from "../../../applets/engine/audio/types.ts"; +import type * as AudioUI from "../../../applets/themes/pilot/ui/audio/types.ts"; + const engine = { audio: await applet("../../engine/audio"), }; const ui = { - audio: await applet("ui/audio", { setHeight: true }), + audio: await applet("ui/audio", { setHeight: true }), }; //////////////////////////////////////////// -// ⚡ Connect applets +// ▒▒ [Connections ⚡] +// ▒▒ Audio UI → Audio Engine //////////////////////////////////////////// reactive( ui.audio, + // TODO: Shouldn't need to pass in the type here (data: AudioUI.State) => data.isPlaying, - (isPlaying: boolean) => { + (isPlaying) => { if (isPlaying) { // TODO: Replace with an actual queue system (shift queue) engine.audio.sendAction("render", { @@ -47,7 +51,7 @@ reactive( reactive( ui.audio, (data: AudioUI.State) => data.seekPosition, - (seekPosition: undefined | number) => { + (seekPosition) => { if (seekPosition) { engine.audio.sendAction("seek", { percentage: seekPosition, @@ -57,12 +61,16 @@ reactive( }, ); +//////////////////////////////////////////// +// ▒▒ [Connections ⚡] +// ▒▒ Audio Engine → Audio UI +//////////////////////////////////////////// reactive( engine.audio, (data: AudioEngine.State) => // TODO: Simplify using queue engine Object.values(data.nowPlaying).some((s) => s.isPlaying), - (isPlaying: boolean) => ui.audio.sendAction("set_is_playing", isPlaying), + (isPlaying) => ui.audio.sendAction("set_is_playing", isPlaying), ); reactive( @@ -75,56 +83,3 @@ reactive( }, (progress: number) => ui.audio.sendAction("set_progress", progress), ); - -//////////////////////////////////////////// -// 🪟 Applet initialiser -//////////////////////////////////////////// -async function applet(src: string, opts: { setHeight?: boolean } = {}) { - const frame: HTMLIFrameElement | null = document.querySelector( - `[src="${src}${src.endsWith("/") ? "" : "/"}"]`, - ); - - if (frame === null) throw new Error("iframe element not found, src: " + src); - if (frame.contentWindow === null) { - throw new Error("iframe does not have a contentWindow"); - } - - const applet: Applet = await applets.connect(frame.contentWindow); - - if (opts.setHeight) { - applet.onresize = () => { - frame.height = `${applet.height}px`; - frame.classList.add("has-loaded"); - }; - } else { - if (frame.contentDocument?.readyState === "complete") { - frame.classList.add("has-loaded"); - } - - frame.addEventListener("load", () => { - frame.classList.add("has-loaded"); - }); - } - - return applet; -} - -//////////////////////////////////////////// -// 🔮 Reactive state management -//////////////////////////////////////////// -// TODO: Applet should have a subtype -function reactive( - applet: Applet, - dataFn: (data: any /* TODO: Proper types pls */) => T, - effectFn: (t: T) => void, -) { - const [getter, setter] = signal( - dataFn(applet.data as any), - ); - - effect(() => effectFn(getter())); - - applet.addEventListener("data", (event: AppletEvent) => { - setter(dataFn(event.data)); - }); -}