From 7a694197e31af3f4ea896587ae2af70febf203a6 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Mon, 9 Jun 2025 18:49:42 +0200 Subject: [PATCH] feat: load artwork into artwork controller --- .../desktop/artwork-controller/_applet.astro | 179 ++++++++++++++++-- .../desktop/artwork-controller/_manifest.json | 2 +- .../desktop/artwork-controller/types.d.ts | 4 - .../constituents/pilot/audio/_applet.astro | 9 +- src/pages/engine/audio/_applet.astro | 108 +++++------ src/pages/engine/audio/_manifest.json | 74 ++++---- src/pages/engine/audio/types.d.ts | 6 +- src/pages/engine/queue/_manifest.json | 5 +- src/pages/engine/queue/types.d.ts | 14 +- src/pages/index.astro | 12 ++ .../orchestrator/input-cache/_applet.astro | 6 +- .../orchestrator/single-queue/_applet.astro | 117 ++++++------ src/pages/processor/artwork/_applet.astro | 55 ++++-- src/scripts/applets/common.ts | 80 +++++--- src/scripts/themes/desktop/index.ts | 39 +--- src/scripts/themes/webamp/index.ts | 4 +- src/styles/diffuse/colors.css | 17 ++ src/styles/diffuse/fonts.css | 14 ++ src/styles/pages/index.css | 34 +--- 19 files changed, 480 insertions(+), 299 deletions(-) delete mode 100644 src/pages/constituents/desktop/artwork-controller/types.d.ts create mode 100644 src/styles/diffuse/colors.css create mode 100644 src/styles/diffuse/fonts.css diff --git a/src/pages/constituents/desktop/artwork-controller/_applet.astro b/src/pages/constituents/desktop/artwork-controller/_applet.astro index b778142..97e1815 100644 --- a/src/pages/constituents/desktop/artwork-controller/_applet.astro +++ b/src/pages/constituents/desktop/artwork-controller/_applet.astro @@ -3,52 +3,197 @@ import "@styles/reset.css"; import "@styles/variables.css"; import "@styles/fonts.css"; import "@styles/icons.css"; + +import "@styles/diffuse/colors.css"; +import "@styles/diffuse/fonts.css"; ---
TODO
+ + diff --git a/src/pages/engine/audio/_manifest.json b/src/pages/engine/audio/_manifest.json index efcf2c6..8b3444e 100644 --- a/src/pages/engine/audio/_manifest.json +++ b/src/pages/engine/audio/_manifest.json @@ -5,24 +5,24 @@ "actions": { "pause": { "title": "Pause", - "description": "Pause a track", + "description": "Pause audio", "params_schema": { "type": "object", "properties": { - "trackId": { + "audioId": { "type": "string" } }, - "required": ["trackId"] + "required": ["audioId"] } }, "play": { "title": "Play", - "description": "Play a track", + "description": "Play audio", "params_schema": { "type": "object", "properties": { - "trackId": { + "audioId": { "type": "string" }, "volume": { @@ -30,7 +30,7 @@ "default": 0.5 } }, - "required": ["trackId"] + "required": ["audioId"] } }, "render": { @@ -39,25 +39,9 @@ "params_schema": { "type": "object", "properties": { - "play": { - "type": "object", - "description": "Pass in this object to immediately start playing one of the rendered tracks.", - "properties": { - "trackId": { - "type": "string", - "description": "The id of the rendered track we want to play." - }, - "volume": { - "type": "number", - "default": 0.5, - "description": "A number equal to, or between, 0 and 1, that determines how loud the track should play." - } - }, - "required": ["trackId"] - }, - "tracks": { + "audio": { "type": "array", - "description": "The tracks we want to render.", + "description": "The audio items we want to render. These represent the audio elements that are in the DOM.", "items": { "type": "object", "properties": { @@ -69,54 +53,70 @@ }, "required": ["id", "url"] } + }, + "play": { + "type": "object", + "description": "Pass in this object to immediately start playing one of the rendered audio items.", + "properties": { + "audioId": { + "type": "string", + "description": "The id of the rendered audio item we want to play." + }, + "volume": { + "type": "number", + "default": 0.5, + "description": "A number equal to, or between, 0 and 1, that determines how loud the audio should play." + } + }, + "required": ["audioId"] } }, - "required": ["tracks"] + "required": ["audio"] } }, "reload": { "title": "Reload", - "description": "Make sure the audio node with the given track id is loading properly. This should be used when for example, the internet connection comes back and the loading of the track depended on said internet connection.", + "description": "Make sure the audio with the given id is loading properly. This should be used when for example, the internet connection comes back and the loading of the audio depended on said internet connection.", "params_schema": { "type": "object", "properties": { + "audioId": { + "type": "string" + }, "play": { "type": "boolean" }, "progress": { "type": "number" - }, - "trackId": { - "type": "string" } }, - "required": ["percentage", "trackId"] + "required": ["audioId", "percentage"] } }, "seek": { "title": "Seek", - "description": "Seek a track to a given position", + "description": "Seek audio to a given position", "params_schema": { "type": "object", "properties": { + "audioId": { + "type": "string" + }, "percentage": { "type": "number", "description": "A number between 0 and 1 that determines the new current position in the audio" - }, - "trackId": { - "type": "string" } }, - "required": ["percentage", "trackId"] + "required": ["audioId", "percentage"] } }, "volume": { "title": "Volume", - "description": "Set the volume of all tracks, or a specific track.", + "description": "Set the volume of all audio, or a specific audio node.", "params_schema": { "type": "object", "properties": { - "trackId": { + "audioId": { "type": "string" }, "volume": { diff --git a/src/pages/engine/audio/types.d.ts b/src/pages/engine/audio/types.d.ts index aa61738..1ba9eea 100644 --- a/src/pages/engine/audio/types.d.ts +++ b/src/pages/engine/audio/types.d.ts @@ -1,9 +1,9 @@ export interface State { - items: Record; + items: Record; volume: number; } -export interface Track { +export interface Audio { id: string; isPreload: boolean; mimeType?: string; @@ -11,7 +11,7 @@ export interface Track { url: string; } -export interface TrackState { +export interface AudioState { duration: number; id: string; hasEnded: boolean; diff --git a/src/pages/engine/queue/_manifest.json b/src/pages/engine/queue/_manifest.json index e1273e1..1de329e 100644 --- a/src/pages/engine/queue/_manifest.json +++ b/src/pages/engine/queue/_manifest.json @@ -13,11 +13,10 @@ { "type": "object", "properties": { - "expiresAt": { "type": "number" }, "id": { "type": "string" }, - "url": { "type": "string" } + "uri": { "type": "string" } }, - "required": ["expiresAt", "id", "url"] + "required": ["id", "uri"] } ] } diff --git a/src/pages/engine/queue/types.d.ts b/src/pages/engine/queue/types.d.ts index 229c328..4795771 100644 --- a/src/pages/engine/queue/types.d.ts +++ b/src/pages/engine/queue/types.d.ts @@ -1,11 +1,7 @@ -export interface QueueItem { - expiresAt: number; - id: string; - url: string; -} +import type { Track } from "@applets/core/types"; -export interface State { - past: QueueItem[]; - now: QueueItem | null; - future: QueueItem[]; +export interface State { + past: Track[]; + now: Track | null; + future: Track[]; } diff --git a/src/pages/index.astro b/src/pages/index.astro index 2452d7a..70b453f 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -133,6 +133,18 @@ const processors = [ abstraction, you can mix and match as you like. You can even use them on their own.

+

+ Some themes may be constructed out of various applets that are not listed here. The reason + for that is those applets cannot be used solely on their own, they require an external + context to coordinate them. +

+ +

+ There's tradeoffs to both approaches. A particular tradeoff to keep in mind for constituents + is that they'll have nested dependencies. So when overriding applets dependencies, the + overrides need to passed down the tree. +

+ diff --git a/src/pages/orchestrator/input-cache/_applet.astro b/src/pages/orchestrator/input-cache/_applet.astro index edb7d13..2eb41b2 100644 --- a/src/pages/orchestrator/input-cache/_applet.astro +++ b/src/pages/orchestrator/input-cache/_applet.astro @@ -1,6 +1,6 @@ diff --git a/src/scripts/applets/common.ts b/src/scripts/applets/common.ts index 2b9654e..a7b354b 100644 --- a/src/scripts/applets/common.ts +++ b/src/scripts/applets/common.ts @@ -6,18 +6,18 @@ import { type ElementConfigurator, h } from "spellcaster/hyperscript.js"; import { effect, isSignal, type Signal, signal } from "spellcaster/spellcaster.js"; import { xxh32 } from "xxh32"; -import type { Track } from "@applets/core/types"; +import type { ResolvedUri, Track } from "@applets/core/types"; //////////////////////////////////////////// -// 🪟 Applet connector +// 🪟 Applet connecting //////////////////////////////////////////// export async function applet( src: string, opts: { addSlashSuffix?: boolean; - applets?: Record; container?: HTMLElement | Element; - id?: string; + frameId?: string; + groupId?: string; setHeight?: boolean; } = {}, ): Promise> { @@ -29,8 +29,14 @@ export async function applet( : "" }`; - if (opts.applets) { - src = QS.stringifyUrl({ url: src, query: opts.applets }); + let query: undefined | Record; + + if (opts.groupId) { + query = { groupId: opts.groupId }; + } + + if (query) { + src = QS.stringifyUrl({ url: src, query }); } const existingFrame: HTMLIFrameElement | null = window.document.querySelector(`[src="${src}"]`); @@ -42,7 +48,7 @@ export async function applet( } else { frame = document.createElement("iframe"); frame.src = src; - if (opts.id) frame.id = opts.id; + if (opts.frameId) frame.id = opts.frameId; if (opts.container) { opts.container.appendChild(frame); @@ -86,7 +92,7 @@ export type BroadcastedApplet = { settled(): Promise; - get id(): string; + get instanceId(): string; set data(data: T); codec: { @@ -99,9 +105,12 @@ export type BroadcastedApplet = { }; export function register() { - const channelId = `${location.host}${location.pathname}`; + const url = new URL(location.href); const scope = applets.register(); - const id = crypto.randomUUID(); + + const groupId = url.searchParams.get("groupId") || "main"; + const channelId = `${location.host}${location.pathname}/${groupId}`; + const instanceId = crypto.randomUUID(); let isMainInstance = true; @@ -119,7 +128,7 @@ export function register() { case "PING": { channel.postMessage({ type: "PONG", - id: event.data.id, + instanceId: event.data.instanceId, }); if (isMainInstance) { @@ -132,7 +141,7 @@ export function register() { } case "PONG": { - if (event.data.id === id) { + if (event.data.instanceId === instanceId) { isMainInstance = false; } break; @@ -143,7 +152,7 @@ export function register() { const result = await scope.actionHandlers[event.data.actionId]?.(...event.data.arguments); channel.postMessage({ type: "actioncomplete", - id: event.data.id, + actionInstanceId: event.data.actionInstanceId, result, }); } @@ -159,14 +168,14 @@ export function register() { // Promise that fullfills whenever it figures out its the main instance or not. const promise = new Promise((resolve) => { - const id = setTimeout(() => { + const timeoutId = setTimeout(() => { channel.removeEventListener("message", handler); resolve(undefined); }, 1000); const handler = (event: MessageEvent) => { if (event.data === "pong" || event.data === "ping") { - clearTimeout(id); + clearTimeout(timeoutId); channel.removeEventListener("message", handler); resolve(undefined); } @@ -178,7 +187,7 @@ export function register() { // Send out ping channel.postMessage({ type: "PING", - id, + instanceId, }); // If the data on the main instance changes, @@ -202,8 +211,8 @@ export function register() { return promise; }, - get id() { - return id; + get instanceId() { + return instanceId; }, get data() { @@ -230,15 +239,18 @@ export function register() { } const actionMessage = { - id: crypto.randomUUID(), - type: "action", + actionInstanceId: crypto.randomUUID(), actionId, + type: "action", arguments: args, }; return new Promise((resolve) => { const actionCallback = (event: MessageEvent) => { - if (event.data?.type === "actioncomplete" && event.data?.id === actionMessage.id) { + if ( + event.data?.type === "actioncomplete" && + event.data?.actionInstanceId === actionMessage.actionInstanceId + ) { channel.removeEventListener("message", actionCallback); resolve(event.data.result); } @@ -276,6 +288,23 @@ export function reactive( }); } +//////////////////////////////////////////// +// ⚡️ COMMON ACTION CALLS +//////////////////////////////////////////// + +export async function inputUrl(input: Applet, uri: string, method = "GET") { + return await input.sendAction( + "resolve", + { + method, + uri, + }, + { + timeoutDuration: 60000 * 5, + }, + ); +} + //////////////////////////////////////////// // 🛠️ //////////////////////////////////////////// @@ -352,10 +381,7 @@ export function jsonEncode(a: T): Uint8Array { return new TextEncoder().encode(JSON.stringify(a)); } -export function waitUntilAppletData( - applet: Applet, - dataFn: (a: A | undefined) => boolean, -): Promise { +export function wait(applet: Applet, dataFn: (a: A | undefined) => boolean): Promise { return new Promise((resolve) => { if (dataFn(applet.data) === true) { resolve(); @@ -372,7 +398,3 @@ export function waitUntilAppletData( applet.addEventListener("data", callback); }); } - -export function waitUntilAppletIsReady(applet: Applet): Promise { - return waitUntilAppletData(applet, (data) => !!data?.ready); -} diff --git a/src/scripts/themes/desktop/index.ts b/src/scripts/themes/desktop/index.ts index 27cbd5a..3ad7e9c 100644 --- a/src/scripts/themes/desktop/index.ts +++ b/src/scripts/themes/desktop/index.ts @@ -1,5 +1,5 @@ import * as Uint8 from "uint8arrays"; -import { applet, reactive, waitUntilAppletData } from "@scripts/applets/common"; +import { applet, reactive, wait } from "@scripts/applets/common"; //////////////////////////////////////////// // 🎨 Styles @@ -9,31 +9,6 @@ import "@styles/themes/desktop/index.css"; //////////////////////////////////////////// // 🗂️ Applets //////////////////////////////////////////// -import type * as AudioEngine from "@applets/engine/audio/types.d.ts"; -import type * as QueueEngine from "@applets/engine/queue/types.d.ts"; -import type { ManagedOutput, Track } from "@applets/core/types"; -import type { Urls } from "@applets/processor/artwork/types"; - -const configurator = { - input: await applet("../../configurator/input"), - output: await applet("../../configurator/output"), -}; - -const engine = { - audio: await applet("../../engine/audio"), - queue: await applet("../../engine/queue"), -}; - -const _orchestrator = { - input: await applet("../../orchestrator/input-cache"), - queue: await applet("../../orchestrator/single-queue"), -}; - -const processor = { - artwork: await applet("../../processor/artwork"), - search: await applet("../../processor/search"), -}; - const ui = {}; //////////////////////////////////////////// @@ -52,10 +27,8 @@ const ui = {}; // TESTING -waitUntilAppletData(configurator.output, (data) => data?.tracks?.state === "loaded").then( - async () => { - await processor.search.sendAction("supply", configurator.output.data.tracks.collection); - const tracks = await processor.search.sendAction("search", "artist lee last"); - tracks.forEach((t: Track) => console.log(t.tags)); - }, -); +// wait(configurator.output, (data) => data?.tracks?.state === "loaded").then(async () => { +// await processor.search.sendAction("supply", configurator.output.data.tracks.collection); +// const tracks = await processor.search.sendAction("search", "artist lee last"); +// tracks.forEach((t: Track) => console.log(t.tags)); +// }); diff --git a/src/scripts/themes/webamp/index.ts b/src/scripts/themes/webamp/index.ts index a57d4f5..9d14b10 100644 --- a/src/scripts/themes/webamp/index.ts +++ b/src/scripts/themes/webamp/index.ts @@ -2,7 +2,7 @@ import type { URLTrack } from "webamp"; import Webamp from "webamp"; import type { ManagedOutput, ResolvedUri, Track } from "@applets/core/types.d.ts"; -import { applet, waitUntilAppletData } from "@scripts/applets/common"; +import { applet, wait } from "@scripts/applets/common"; //////////////////////////////////////////// // 🎨 Styles @@ -33,7 +33,7 @@ ampNode.style = "height: 100vh; left: 0; position: absolute; top: 0; width: 100% document.body.appendChild(ampNode); amp.renderWhenReady(ampNode); -waitUntilAppletData(configurator.output, (d) => d?.tracks.state === "loaded").then(loadAndInsert); +wait(configurator.output, (d) => d?.tracks.state === "loaded").then(loadAndInsert); configurator.output.ondata = loadAndInsert; let inserting = false; diff --git a/src/styles/diffuse/colors.css b/src/styles/diffuse/colors.css new file mode 100644 index 0000000..182d2fb --- /dev/null +++ b/src/styles/diffuse/colors.css @@ -0,0 +1,17 @@ +:root { + /* Colors */ + --color-1: oklch(4.1308% 0.25306 109.22); + --color-2: oklch(98.369% 0.01834 67.664); + --color-3: oklch(26.787% 0.00168 186.65); + --accent: oklch(86.947% 0.25527 28.789); + + --bg-color: var(--color-2); + --text-color: var(--color-1); +} + +@media (prefers-color-scheme: dark) { + :root { + --bg-color: var(--color-3); + --text-color: var(--color-2); + } +} diff --git a/src/styles/diffuse/fonts.css b/src/styles/diffuse/fonts.css new file mode 100644 index 0000000..7f7b659 --- /dev/null +++ b/src/styles/diffuse/fonts.css @@ -0,0 +1,14 @@ +:root { + font-family: "Inter", sans-serif; + font-size: var(--fs-base); +} + +@supports (font-variation-settings: normal) { + :root { + font-family: "InterVariable", sans-serif; + font-feature-settings: + "zero" 2, + "ss03" 2; + font-optical-sizing: auto; + } +} diff --git a/src/styles/pages/index.css b/src/styles/pages/index.css index 82b06e1..d1a2355 100644 --- a/src/styles/pages/index.css +++ b/src/styles/pages/index.css @@ -1,35 +1,5 @@ -@import "../variables.css"; - -:root { - font-family: "Inter", sans-serif; - font-size: var(--fs-base); - - /* Colors */ - --color-1: oklch(4.1308% 0.25306 109.22); - --color-2: oklch(98.369% 0.01834 67.664); - --color-3: oklch(26.787% 0.00168 186.65); - --accent: oklch(86.947% 0.25527 28.789); - - --bg-color: var(--color-2); - --text-color: var(--color-1); -} - -@media (prefers-color-scheme: dark) { - :root { - --bg-color: var(--color-3); - --text-color: var(--color-2); - } -} - -@supports (font-variation-settings: normal) { - :root { - font-family: "InterVariable", sans-serif; - font-feature-settings: - "zero" 2, - "ss03" 2; - font-optical-sizing: auto; - } -} +@import "../diffuse/colors.css"; +@import "../diffuse/fonts.css"; body { background-color: var(--bg-color); -- 2.51.2