A music player that connects to your cloud/distributed storage.

feat: index page

+171 -2144
-15
_backup/components/Applet.astro
··· 1 - --- 2 - import List from "../components/List.astro"; 3 - 4 - const { list, title } = Astro.props; 5 - --- 6 - 7 - <div class="applet"> 8 - <h3>{title}</h3> 9 - 10 - <p> 11 - <em><slot /></em> 12 - </p> 13 - 14 - <List items={list} /> 15 - </div>
-17
_backup/components/List.astro
··· 1 - --- 2 - const { items } = Astro.props; 3 - --- 4 - 5 - <ul> 6 - { 7 - items.map((item: { title: string; url: string }) => ( 8 - <li> 9 - {item.title.startsWith("(TODO) ") ? ( 10 - <span>{item.title}</span> 11 - ) : ( 12 - <a href={item.url}>{item.title}</a> 13 - )} 14 - </li> 15 - )) 16 - } 17 - </ul>
-8
_backup/content.config.ts
··· 1 - import { defineCollection } from "astro:content"; 2 - import { glob } from "astro/loaders"; 3 - 4 - const manifests = defineCollection({ 5 - loader: glob({ pattern: "**/_manifest.json", base: "./src/pages" }), 6 - }); 7 - 8 - export const collections = { manifests };
-2
_backup/env.d.ts
··· 1 - /// <reference types="astro/client" /> 2 - /// <reference path="../.astro/types.d.ts" />
-24
_backup/layouts/applet-pico-ui.astro
··· 1 - --- 2 - import "@styles/reset.css"; 3 - import "@styles/variables.css"; 4 - import "@styles/fonts.css"; 5 - import "@styles/icons/iconoir.css"; 6 - import "@styles/pico.scss"; 7 - import "@styles/applet/common.css"; 8 - 9 - const { title } = Astro.props; 10 - --- 11 - 12 - <html lang="en"> 13 - <head> 14 - <meta charset="UTF-8" /> 15 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 16 - <meta name="color-scheme" content="light dark" /> 17 - <link rel="manifest" href="manifest.json" /> 18 - 19 - <title>{title}</title> 20 - </head> 21 - <body> 22 - <slot /> 23 - </body> 24 - </html>
-17
_backup/layouts/applet.astro
··· 1 - --- 2 - const { title } = Astro.props; 3 - --- 4 - 5 - <html lang="en"> 6 - <head> 7 - <meta charset="UTF-8" /> 8 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 9 - <meta name="color-scheme" content="light dark" /> 10 - <link rel="manifest" href="manifest.json" /> 11 - 12 - <title>{title}</title> 13 - </head> 14 - <body> 15 - <slot /> 16 - </body> 17 - </html>
-21
_backup/layouts/page.astro
··· 1 - --- 2 - const { title } = Astro.props; 3 - --- 4 - 5 - <html lang="en"> 6 - <head> 7 - <meta charset="UTF-8" /> 8 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 9 - 10 - <title>{title}</title> 11 - 12 - <style> 13 - @import "../styles/reset.css"; 14 - @import "../styles/fonts.css"; 15 - @import "../styles/variables.css"; 16 - </style> 17 - </head> 18 - <body> 19 - <slot /> 20 - </body> 21 - </html>
-19
_backup/pages/[...manifest].json.ts
··· 1 - import type { APIRoute } from "astro"; 2 - import { getCollection } from "astro:content"; 3 - 4 - // API Route 5 - export const GET: APIRoute = ({ params, props, request }) => { 6 - return new Response(JSON.stringify(props.manifest)); 7 - }; 8 - 9 - // Generate static paths 10 - export async function getStaticPaths() { 11 - const manifests = await getCollection("manifests"); 12 - 13 - return manifests.map((manifest) => { 14 - return { 15 - params: { manifest: manifest.id.replace("/_manifest", "/manifest") }, 16 - props: { manifest: manifest.data }, 17 - }; 18 - }); 19 - }
-360
_backup/pages/engine/audio/_applet.astro
··· 1 - <script> 2 - import { effect, signal } from "@scripts/spellcaster"; 3 - 4 - import type { State, Audio, AudioState } from "./types"; 5 - import { register } from "@scripts/applet/common"; 6 - 7 - //////////////////////////////////////////// 8 - // CONSTANTS 9 - //////////////////////////////////////////// 10 - const SILENT_MP3 = 11 - "data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV"; 12 - 13 - //////////////////////////////////////////// 14 - // SETUP 15 - //////////////////////////////////////////// 16 - const context = register<State>(); 17 - const groupId = context.groupId ?? "main"; 18 - 19 - // Audio elements container 20 - const container = document.createElement("div"); 21 - container.id = "container"; 22 - document.body.appendChild(container); 23 - 24 - // Default volume 25 - const VOLUME_KEY = `@applets/engine/audio/${groupId}/volume`; 26 - const vol = localStorage.getItem(VOLUME_KEY); 27 - 28 - // Initial state 29 - if (context.isMainInstance()) 30 - context.data = { 31 - isPlaying: false, 32 - items: {}, 33 - volume: { 34 - default: vol ? parseFloat(vol) : 0.5, 35 - }, 36 - }; 37 - 38 - // State helpers 39 - function update(partial: Partial<State>): void { 40 - context.data = { ...context.data, ...partial }; 41 - } 42 - 43 - function updateItems(audioId: string, partial: Partial<AudioState>): void { 44 - update({ 45 - ...context.data, 46 - items: { 47 - ...(context.data?.items || {}), 48 - [audioId]: { ...(context.data?.items?.[audioId] || {}), ...partial }, 49 - }, 50 - }); 51 - } 52 - 53 - // Effects 54 - const defaultVolume = signal<number | undefined>(undefined); 55 - context.scope.ondata = (event: any) => defaultVolume(event.data.volume.default); 56 - 57 - effect(() => { 58 - if (context.isMainInstance()) { 59 - const volume = defaultVolume(); 60 - if (volume === undefined) return; 61 - localStorage.setItem(VOLUME_KEY, volume.toString()); 62 - } 63 - }); 64 - 65 - // Unload 66 - context.unloadHandler = async () => { 67 - await context.settled(); 68 - hydrateItems(); 69 - }; 70 - 71 - function hydrateItems() { 72 - const playingItem = context.data.isPlaying 73 - ? Object.values(context.data.items).find((item) => item.isPlaying) 74 - : undefined; 75 - 76 - render({ 77 - audio: Object.values(context.data.items).map((item: AudioState) => { 78 - return { 79 - id: item.id, 80 - isPreload: item.isPreload, 81 - mimeType: item.mimeType, 82 - progress: item.progress, 83 - url: item.url, 84 - }; 85 - }), 86 - play: playingItem ? { audioId: playingItem.id } : undefined, 87 - }); 88 - } 89 - 90 - //////////////////////////////////////////// 91 - // ACTIONS 92 - //////////////////////////////////////////// 93 - context.setActionHandler("pause", pause); 94 - context.setActionHandler("play", play); 95 - context.setActionHandler("reload", reload); 96 - context.setActionHandler("render", render); 97 - context.setActionHandler("seek", seek); 98 - context.setActionHandler("volume", volume); 99 - 100 - function pause({ audioId }: { audioId: string }) { 101 - withAudioNode(audioId, (audio) => audio.pause()); 102 - } 103 - 104 - function play({ audioId, volume }: { audioId: string; volume?: number }) { 105 - withAudioNode(audioId, (audio) => { 106 - audio.volume = volume ?? context.data.volume.default; 107 - audio.muted = false; 108 - 109 - if (audio.readyState === 0) audio.load(); 110 - if (!audio.isConnected) return; 111 - 112 - const promise = audio.play() || Promise.resolve(); 113 - const didPreload = audio.getAttribute("data-did-preload") === "true"; 114 - const isPreload = audio.getAttribute("data-is-preload") === "true"; 115 - 116 - if (didPreload && !isPreload) { 117 - audio.removeAttribute("data-did-preload"); 118 - } 119 - 120 - updateItems(audio.id, { isPlaying: true }); 121 - 122 - promise.catch((e) => { 123 - if (!audio.isConnected) 124 - return; /* The node was removed from the DOM, we can ignore this error */ 125 - const err = "Couldn't play audio automatically. Please resume playback manually."; 126 - console.error(err, e); 127 - updateItems(audioId, { isPlaying: false }); 128 - }); 129 - }); 130 - } 131 - 132 - function reload(args: { play: boolean; progress?: number; audioId: string }) { 133 - withAudioNode(args.audioId, (audio) => { 134 - if (audio.readyState === 0 || audio.error?.code === 2) { 135 - audio.load(); 136 - 137 - if (args.progress !== undefined) { 138 - audio.setAttribute("data-initial-progress", JSON.stringify(args.progress)); 139 - } 140 - 141 - if (args.play) { 142 - play({ audioId: args.audioId, volume: audio.volume }); 143 - } 144 - } 145 - }); 146 - } 147 - 148 - async function render(args: { play?: { audioId: string; volume?: number }; audio: Audio[] }) { 149 - await renderAudio(args.audio); 150 - if (args.play) play({ audioId: args.play.audioId, volume: args.play.volume }); 151 - } 152 - 153 - function seek({ percentage, audioId }: { percentage: number; audioId: string }) { 154 - withAudioNode(audioId, (audio) => { 155 - if (!isNaN(audio.duration)) { 156 - audio.currentTime = audio.duration * percentage; 157 - } 158 - }); 159 - } 160 - 161 - function volume(args: { audioId?: string; volume: number }) { 162 - if (!args.audioId) update({ volume: { default: args.volume } }); 163 - 164 - Array.from(container.querySelectorAll("audio")).forEach((node) => { 165 - const audio = node as HTMLAudioElement; 166 - if (audio.getAttribute("data-is-preload") === "true") return; 167 - if (args.audioId === undefined || args.audioId === audio.id) { 168 - audio.volume = args.volume; 169 - } 170 - }); 171 - } 172 - 173 - //////////////////////////////////////////// 174 - // RENDER 175 - //////////////////////////////////////////// 176 - async function renderAudio(audio: Array<Audio>) { 177 - const ids = audio.map((a) => a.id); 178 - const existingNodes: Record<string, HTMLAudioElement> = {}; 179 - 180 - // Manage existing nodes 181 - Array.from(container.querySelectorAll("audio")).map((node: HTMLAudioElement) => { 182 - if (ids.includes(node.id)) { 183 - existingNodes[node.id] = node; 184 - } else { 185 - node.src = SILENT_MP3; 186 - container?.removeChild(node); 187 - } 188 - }); 189 - 190 - // Adjust existing and add new 191 - await audio.reduce(async (acc: Promise<void>, item: Audio) => { 192 - await acc; 193 - 194 - const existingNode = existingNodes[item.id]; 195 - 196 - if (existingNode) { 197 - const isPreload = existingNode.getAttribute("data-is-preload"); 198 - if (isPreload === "true") existingNode.setAttribute("data-did-preload", "true"); 199 - 200 - existingNode.setAttribute("data-is-preload", item.isPreload ? "true" : "false"); 201 - } else { 202 - await createElement(item); 203 - } 204 - }, Promise.resolve()); 205 - 206 - // Now playing state 207 - const items = audio.reduce((acc, item) => { 208 - return { 209 - ...acc, 210 - [item.id]: context.data?.items?.[item.id] || { 211 - duration: 0, 212 - id: item.id, 213 - loadingState: "loading", 214 - isPlaying: true, 215 - isPreload: item.isPreload ?? false, 216 - mimeType: item.mimeType, 217 - progress: item.progress ?? 0, 218 - url: item.url, 219 - }, 220 - }; 221 - }, {}); 222 - 223 - update({ items }); 224 - } 225 - 226 - export async function createElement(audio: Audio) { 227 - const source = document.createElement("source"); 228 - if (audio.mimeType) source.setAttribute("type", audio.mimeType); 229 - source.setAttribute("src", audio.url); 230 - 231 - // Audio node 232 - const node = new Audio(); 233 - node.setAttribute("id", audio.id); 234 - node.setAttribute("crossorigin", "anonymous"); 235 - node.setAttribute("data-is-preload", audio.isPreload ? "true" : "false"); 236 - node.setAttribute("muted", "true"); 237 - node.setAttribute("preload", "auto"); 238 - 239 - if (audio.progress !== undefined) { 240 - node.setAttribute("data-initial-progress", JSON.stringify(audio.progress)); 241 - } 242 - 243 - node.appendChild(source); 244 - 245 - node.addEventListener("canplay", canplayEvent); 246 - node.addEventListener("durationchange", durationchangeEvent); 247 - node.addEventListener("ended", endedEvent); 248 - node.addEventListener("error", errorEvent); 249 - node.addEventListener("pause", pauseEvent); 250 - node.addEventListener("play", playEvent); 251 - node.addEventListener("suspend", suspendEvent); 252 - node.addEventListener("timeupdate", timeupdateEvent); 253 - node.addEventListener("waiting", waitingEvent); 254 - 255 - container?.appendChild(node); 256 - } 257 - 258 - //////////////////////////////////////////// 259 - // AUDIO EVENTS 260 - //////////////////////////////////////////// 261 - 262 - function canplayEvent(event: Event) { 263 - const target = event.target as HTMLAudioElement; 264 - 265 - if ( 266 - target.hasAttribute("data-initial-progress") && 267 - target.duration && 268 - !isNaN(target.duration) 269 - ) { 270 - const progress = JSON.parse(target.getAttribute("data-initial-progress") as string); 271 - target.currentTime = target.duration * progress; 272 - target.removeAttribute("data-initial-progress"); 273 - } 274 - 275 - finishedLoading(event); 276 - } 277 - 278 - function durationchangeEvent(event: Event) { 279 - const audio = event.target as HTMLAudioElement; 280 - 281 - if (!isNaN(audio.duration)) { 282 - updateItems(audio.id, { duration: audio.duration }); 283 - } 284 - } 285 - 286 - function endedEvent(event: Event) { 287 - const audio = event.target as HTMLAudioElement; 288 - audio.currentTime = 0; 289 - updateItems(audio.id, { hasEnded: true }); 290 - } 291 - 292 - function errorEvent(event: Event) { 293 - const audio = event.target as HTMLAudioElement; 294 - const code = audio.error?.code || 0; 295 - updateItems(audio.id, { loadingState: { error: { code } } }); 296 - } 297 - 298 - function pauseEvent(event: Event) { 299 - const audio = event.target as HTMLAudioElement; 300 - const item = context.data.items[audio.id]; 301 - const ended = item ? item.hasEnded || item.progress === 1 : false; 302 - updateItems(audio.id, { isPlaying: false }); 303 - update({ isPlaying: ended }); 304 - } 305 - 306 - function playEvent(event: Event) { 307 - const audio = event.target as HTMLAudioElement; 308 - updateItems(audio.id, { isPlaying: true }); 309 - update({ isPlaying: true }); 310 - 311 - // In case audio was preloaded: 312 - if (audio.readyState === 4) finishedLoading(event); 313 - } 314 - 315 - function suspendEvent(event: Event) { 316 - finishedLoading(event); 317 - } 318 - 319 - function timeupdateEvent(event: Event) { 320 - const audio = event.target as HTMLAudioElement; 321 - 322 - updateItems(audio.id, { 323 - progress: 324 - isNaN(audio.duration) || audio.duration === 0 ? 0 : audio.currentTime / audio.duration, 325 - }); 326 - } 327 - 328 - function waitingEvent(event: Event) { 329 - initiateLoading(event); 330 - } 331 - 332 - //////////////////////////////////////////// 333 - // 🛠️ 334 - //////////////////////////////////////////// 335 - 336 - function finishedLoading(event: Event) { 337 - const audio = event.target as HTMLAudioElement; 338 - updateItems(audio.id, { loadingState: "loaded" }); 339 - } 340 - 341 - function initiateLoading(event: Event) { 342 - const audio = event.target as HTMLAudioElement; 343 - if (audio.readyState < 4) updateItems(audio.id, { loadingState: "loading" }); 344 - } 345 - 346 - function withActiveAudioNodes(fn: (node: HTMLAudioElement) => void): void { 347 - const nonPreloadNodes: HTMLAudioElement[] = Array.from( 348 - container.querySelectorAll(`audio[data-is-preload="false"]`), 349 - ); 350 - 351 - const playingNodes = nonPreloadNodes.filter((n) => n.paused === false); 352 - const node = playingNodes.length ? playingNodes[0] : nonPreloadNodes[0]; 353 - if (node) fn(node); 354 - } 355 - 356 - function withAudioNode(audioId: string, fn: (node: HTMLAudioElement) => void): void { 357 - const node = container.querySelector(`audio[id="${audioId}"][data-is-preload="false"]`); 358 - if (node) fn(node as HTMLAudioElement); 359 - } 360 - </script>
-131
_backup/pages/engine/audio/_manifest.json
··· 1 - { 2 - "name": "diffuse/engine/audio", 3 - "title": "Diffuse Audio", 4 - "entrypoint": "index.html", 5 - "actions": { 6 - "pause": { 7 - "title": "Pause", 8 - "description": "Pause audio", 9 - "params_schema": { 10 - "type": "object", 11 - "properties": { 12 - "audioId": { 13 - "type": "string" 14 - } 15 - }, 16 - "required": ["audioId"] 17 - } 18 - }, 19 - "play": { 20 - "title": "Play", 21 - "description": "Play audio", 22 - "params_schema": { 23 - "type": "object", 24 - "properties": { 25 - "audioId": { 26 - "type": "string" 27 - }, 28 - "volume": { 29 - "type": "number", 30 - "default": 0.5 31 - } 32 - }, 33 - "required": ["audioId"] 34 - } 35 - }, 36 - "render": { 37 - "title": "Render", 38 - "description": "Determine the active set of audio elements.", 39 - "params_schema": { 40 - "type": "object", 41 - "properties": { 42 - "audio": { 43 - "type": "array", 44 - "description": "The audio items we want to render. These represent the audio elements that are in the DOM.", 45 - "items": { 46 - "type": "object", 47 - "properties": { 48 - "id": { "type": "string" }, 49 - "isPreload": { "type": "boolean" }, 50 - "mimeType": { "type": "string" }, 51 - "progress": { "type": "number" }, 52 - "url": { "type": "string" } 53 - }, 54 - "required": ["id", "url"] 55 - } 56 - }, 57 - "play": { 58 - "type": "object", 59 - "description": "Pass in this object to immediately start playing one of the rendered audio items.", 60 - "properties": { 61 - "audioId": { 62 - "type": "string", 63 - "description": "The id of the rendered audio item we want to play." 64 - }, 65 - "volume": { 66 - "type": "number", 67 - "default": 0.5, 68 - "description": "A number equal to, or between, 0 and 1, that determines how loud the audio should play." 69 - } 70 - }, 71 - "required": ["audioId"] 72 - } 73 - }, 74 - "required": ["audio"] 75 - } 76 - }, 77 - "reload": { 78 - "title": "Reload", 79 - "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.", 80 - "params_schema": { 81 - "type": "object", 82 - "properties": { 83 - "audioId": { 84 - "type": "string" 85 - }, 86 - "play": { 87 - "type": "boolean" 88 - }, 89 - "progress": { 90 - "type": "number" 91 - } 92 - }, 93 - "required": ["audioId", "percentage"] 94 - } 95 - }, 96 - "seek": { 97 - "title": "Seek", 98 - "description": "Seek audio to a given position", 99 - "params_schema": { 100 - "type": "object", 101 - "properties": { 102 - "audioId": { 103 - "type": "string" 104 - }, 105 - "percentage": { 106 - "type": "number", 107 - "description": "A number between 0 and 1 that determines the new current position in the audio" 108 - } 109 - }, 110 - "required": ["audioId", "percentage"] 111 - } 112 - }, 113 - "volume": { 114 - "title": "Volume", 115 - "description": "Set the volume of all audio and the default value, or a specific audio node.", 116 - "params_schema": { 117 - "type": "object", 118 - "properties": { 119 - "audioId": { 120 - "type": "string" 121 - }, 122 - "volume": { 123 - "type": "number", 124 - "description": "A number between 0 and 1 that determines the new volume of all audio elements" 125 - } 126 - }, 127 - "required": ["volume"] 128 - } 129 - } 130 - } 131 - }
-9
_backup/pages/engine/audio/index.astro
··· 1 - --- 2 - import Layout from "@layouts/applet.astro"; 3 - import Applet from "./_applet.astro"; 4 - import { title } from "./_manifest.json"; 5 - --- 6 - 7 - <Layout title={title}> 8 - <Applet /> 9 - </Layout>
-59
_backup/pages/engine/queue/_applet.astro
··· 1 - <script> 2 - import type { Tasks } from "@scripts/engine/queue/worker"; 3 - import type { Track } from "@applets/core/types"; 4 - import type { State } from "./types.d.ts"; 5 - 6 - import { register } from "@scripts/applet/common"; 7 - import { endpoint, SharedWorker, sync, transfer } from "@scripts/common"; 8 - import manifest from "./_manifest.json"; 9 - 10 - //////////////////////////////////////////// 11 - // SETUP 12 - //////////////////////////////////////////// 13 - const port = new SharedWorker(new URL("../../../scripts/engine/queue/worker", import.meta.url), { 14 - type: "module", 15 - name: manifest.name, 16 - }).port; 17 - 18 - const worker = endpoint<Tasks>(port); 19 - 20 - // Register applet 21 - const context = register<State>({ mode: "shared-worker", worker }); 22 - const groupId = context.groupId || "main"; 23 - 24 - // Initial state 25 - context.data = { 26 - future: [], 27 - now: null, 28 - past: [], 29 - }; 30 - 31 - context.data = await worker.data(groupId); 32 - 33 - // Keep applet data with worker data in sync 34 - sync(context, port, { groupId }); 35 - 36 - //////////////////////////////////////////// 37 - // ACTIONS 38 - //////////////////////////////////////////// 39 - context.setActionHandler("add", add); 40 - context.setActionHandler("pool", pool); 41 - context.setActionHandler("shift", shift); 42 - context.setActionHandler("unshift", unshift); 43 - 44 - async function add(items: Track[]) { 45 - await worker.add({ groupId, items }); 46 - } 47 - 48 - async function pool(tracks: Track[]) { 49 - await worker.pool({ groupId, tracks }); 50 - } 51 - 52 - async function shift() { 53 - await worker.shift({ groupId }); 54 - } 55 - 56 - async function unshift() { 57 - await worker.unshift({ groupId }); 58 - } 59 - </script>
-47
_backup/pages/engine/queue/_manifest.json
··· 1 - { 2 - "name": "diffuse/engine/queue", 3 - "title": "Diffuse Queue", 4 - "entrypoint": "index.html", 5 - "actions": { 6 - "add": { 7 - "title": "Add", 8 - "description": "Add tracks to the queue.", 9 - "params_schema": { 10 - "type": "array", 11 - "description": "Array of tracks", 12 - "items": { 13 - "type": "object", 14 - "properties": { 15 - "id": { "type": "string" }, 16 - "uri": { "type": "string" } 17 - }, 18 - "required": ["id", "uri"] 19 - } 20 - } 21 - }, 22 - "pool": { 23 - "title": "Pool", 24 - "description": "Set the queue pool.", 25 - "params_schema": { 26 - "type": "array", 27 - "description": "Array of tracks", 28 - "items": { 29 - "type": "object", 30 - "properties": { 31 - "id": { "type": "string" }, 32 - "uri": { "type": "string" } 33 - }, 34 - "required": ["id", "uri"] 35 - } 36 - } 37 - }, 38 - "shift": { 39 - "title": "Shift", 40 - "description": "Shift the queue, picking the first item from the up next array and putting the currently playing item into the history list." 41 - }, 42 - "unshift": { 43 - "title": "Unshift", 44 - "description": "Unshift the queue, going backwards in time, picking the last item from the history array and putting the currently playing item into the up next list." 45 - } 46 - } 47 - }
-9
_backup/pages/engine/queue/index.astro
··· 1 - --- 2 - import Layout from "@layouts/applet.astro"; 3 - import Applet from "./_applet.astro"; 4 - import { title } from "./_manifest.json"; 5 - --- 6 - 7 - <Layout title={title}> 8 - <Applet /> 9 - </Layout>
-1
_backup/pages/engine/queue/types.d.ts
··· 1 - export * from "@scripts/engine/queue/types.d.ts";
-265
_backup/pages/index.astro
··· 1 - --- 2 - import Applet from "../components/Applet.astro"; 3 - import List from "../components/List.astro"; 4 - import Page from "../layouts/page.astro"; 5 - 6 - import "../styles/page/index.css"; 7 - 8 - // Types 9 - type Ref = { 10 - url: string; 11 - title: string; 12 - }; 13 - 14 - // Links 15 - const WEB_APPLETS_HREF = "https://unternet.co/docs/web-applets/introduction"; 16 - 17 - // Themes 18 - const themes = [ 19 - { url: "theme/blur/", title: "(WIP) Blur" }, 20 - { url: "theme/pilot/", title: "(WIP) Pilot" }, 21 - { url: "theme/webamp/", title: "Webamp" }, 22 - ]; 23 - 24 - // Abstractions 25 - // TODO 26 - 27 - // Constituents 28 - const constituents = [ 29 - { url: "constituent/blur/artwork-controller/", title: "Blur ⦚ Artwork Controller" }, 30 - { url: "constituent/blur/browser/", title: "(WIP) Blur ⦚ Browser" }, 31 - ]; 32 - 33 - // Applets 34 - const configurators = [ 35 - { url: "configurator/input/", title: "Input" }, 36 - { url: "configurator/output/", title: "Output" }, 37 - ]; 38 - 39 - const engines = [ 40 - { url: "engine/audio/", title: "Audio" }, 41 - { url: "engine/queue/", title: "Queue" }, 42 - ]; 43 - 44 - const input = [ 45 - { url: "input/native-fs/", title: "Native File System" }, 46 - { url: "input/opensubsonic/", title: "Opensubsonic" }, 47 - { url: "input/s3/", title: "S3-Compatible API" }, 48 - ]; 49 - 50 - const orchestrators = [ 51 - { url: "orchestrator/queue-audio/", title: "Queue ⭤ Audio" }, 52 - { url: "orchestrator/queue-tracks/", title: "Queue ⭤ Tracks" }, 53 - { url: "orchestrator/process-tracks/", title: "Process inputs into tracks" }, 54 - ]; 55 - 56 - const output = [ 57 - { url: "output/indexed-db/", title: "IndexedDB" }, 58 - { url: "output/native-fs/", title: "Native File System" }, 59 - { url: "output/storacha-automerge/", title: "(WIP) Storacha Storage + Automerge CRDT" }, 60 - { url: "output/todo/", title: "(TODO) Keyhive/Beelay" }, 61 - { url: "output/todo/", title: "(TODO) Dialog DB" }, 62 - ]; 63 - 64 - const processors = [ 65 - { url: "processor/artwork/", title: "Artwork retrieval" }, 66 - { url: "processor/metadata/", title: "Metadata retrieval" }, 67 - { url: "processor/search/", title: "Search" }, 68 - ]; 69 - 70 - // Demos 71 - const demos = [{ url: "demo/s3-tracks/", title: "Add sample S3 music" }]; 72 - --- 73 - 74 - <Page title="Diffuse"> 75 - <header> 76 - <h1> 77 - <svg viewBox="0 0 902 134" width="160"> 78 - <title>Diffuse</title> 79 - <use 80 - xlink:href="/images/diffuse-current.svg#diffuse" 81 - href="/images/diffuse-current.svg#diffuse"></use> 82 - </svg> 83 - </h1> 84 - <p> 85 - Diffuse is a collection of <a href={WEB_APPLETS_HREF}>web applets</a> that make it possible to 86 - listen to audio from various sources on your devices and the web, and to create the ideal digital 87 - listening experience for you. 88 - </p> 89 - <p>These applets can be used in various ways. The main ways so far are through:</p> 90 - <ul> 91 - <li><a href="#themes">Themes</a>, a traditional browser (web application) approach.</li> 92 - <li><a href="#abstractions">Abstractions</a>, for non-browser systems.</li> 93 - <li> 94 - Using <a href="#constituents">various parts</a> of themes and abstractions separately in different 95 - browser tabs. 96 - </li> 97 - </ul> 98 - <p> 99 - <strong>⚠️ Heavily experimental</strong> 100 - </p> 101 - </header> 102 - <main> 103 - <div class="columns"> 104 - <!-- THEMES --> 105 - <section> 106 - <h2 id="themes">Themes</h2> 107 - 108 - <p> 109 - Themes are “applet compositions” and provide a traditional browser web application way of 110 - using them. Each theme is unique, not just a skin (eg. not like winamp skins). 111 - </p> 112 - 113 - <p> 114 - For example, most themes here will limit the currently playing audio tracks to one item, 115 - but you might as well create a DJ theme that can play multiple items at the same time. 116 - </p> 117 - 118 - <List items={themes} /> 119 - </section> 120 - 121 - <!-- ABSTRACTIONS --> 122 - <section> 123 - <h2 id="abstractions">Abstractions</h2> 124 - 125 - <p> 126 - These are applet configurations that enable certain use cases outside the traditional web 127 - app experience. Just like themes, these include various assumptions of how certain parts 128 - of the system should interact. 129 - </p> 130 - 131 - <p><em>TODO: Enable intelligent user (ai) agent use-case.</em></p> 132 - 133 - <List items={[]} /> 134 - </section> 135 - </div> 136 - 137 - <!-- CONSTITUENTS --> 138 - <section> 139 - <h2 id="constituents">Constituents</h2> 140 - 141 - <p> 142 - Constituents are UI applets that are used in themes and abstractions. These are organised 143 - per theme or abstraction, but that doesn't mean they are restricted to that theme or 144 - abstraction, you can mix and match as you like. You can even use them on their own. 145 - </p> 146 - 147 - <p> 148 - Some themes may be constructed out of various applets that are not listed here. The reason 149 - for that is those applets cannot be used solely on their own, they require an external 150 - context to coordinate them. 151 - </p> 152 - 153 - <p> 154 - There's tradeoffs to both approaches. A particular tradeoff to keep in mind for constituents 155 - is that they'll have nested dependencies. So when overriding applets dependencies, the 156 - overrides need to be passed down the tree. 157 - </p> 158 - 159 - <List items={constituents} /> 160 - </section> 161 - 162 - <!-- APPLETS --> 163 - <section> 164 - <h2 id="applets">Applets</h2> 165 - 166 - <p> 167 - Applets are <a href={WEB_APPLETS_HREF}>web applets</a>, the components of the system. These 168 - are then recombined into an entire music player experience, or whatever you want to build. 169 - </p> 170 - 171 - <div class="columns"> 172 - <Applet title="Configurators" list={configurators}> 173 - Applets that serve as an intermediate in order to make a particular kind of applet 174 - configurable. In other words, these allow for an applet to be swapped out with another 175 - that takes the same, or a subset of the actions and data output. 176 - </Applet> 177 - 178 - <Applet title="Engines" list={engines}> 179 - Applets with each a singular purpose and don't have any UI. There are specialised UI and 180 - orchestrators applets that control these. 181 - </Applet> 182 - 183 - <Applet title="Input" list={input}> 184 - Inputs are sources of audio tracks. Each track is an entry in the list of possible items 185 - to play. These can be files or streams, static or dynamic. 186 - </Applet> 187 - 188 - <Applet title="Orchestrators" list={orchestrators}> 189 - These too are applet compositions. However, unlike themes, these are purely logical. 190 - Mostly exist in order to construct sensible defaults to use across themes and 191 - abstractions. 192 - </Applet> 193 - 194 - <Applet title="Output" list={output}> 195 - Output is application-derived data such as playlists. These applets can receive such data 196 - and keep it around. 197 - </Applet> 198 - 199 - <Applet title="Processors" list={processors}> 200 - These applets work with the tracks generated by the input applets to add more data to 201 - them, or process them in some other way. 202 - </Applet> 203 - 204 - <Applet title="Supplements" list={[]}>Additional applets, such as scrobblers.</Applet> 205 - </div> 206 - </section> 207 - 208 - <!-- DEMOS --> 209 - <section> 210 - <h2 id="demos">Demos</h2> 211 - 212 - <p>Just some utility web pages to help demo the system.</p> 213 - 214 - <List items={demos} /> 215 - </section> 216 - 217 - <!-- CROSS-ORIGIN --> 218 - <section> 219 - <h2 id="cross-origin">Cross-Origin</h2> 220 - 221 - <p> 222 - You can use applets from different origins. However, one important caveat to remember is <a 223 - href="https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/State_Partitioning#state_partitioning" 224 - >state partitioning</a 225 - >. This causes local state (eg. IndexedDB) to be different when the applet is used on a 226 - different domain versus using the applet directly. There might be ways around state 227 - partitioning, but I haven't found a user-friendly way of doing so. 228 - </p> 229 - 230 - <p> 231 - <strong 232 - >That said, there isn't that much local state that needs to be shared cross origin.</strong 233 - > Much of it is associated with the input applets and that configuration is saved in your chosen 234 - output applet. So essentially, state partitioning behaves as if the two domains are on different 235 - devices; so using an output applet to sync data between devices works nicely. It does mean you 236 - have to somehow configure the same output applet in both contexts. 237 - </p> 238 - </section> 239 - 240 - <!-- BUILD YOUR OWN --> 241 - <section> 242 - <h2 id="yours">Build your own</h2> 243 - 244 - <p> 245 - Ideally you'd be able to mix and match these applets to build your own music player. There 246 - are still a few todos and unknowns here: 247 - </p> 248 - 249 - <ul> 250 - <li> 251 - This is currently using a fork of the web applets SDK that adds a few features (waiting on 252 - PRs). Meaning that it won't be as easy to write your own applets in the same way as done 253 - here. 254 - </li> 255 - <li> 256 - I wrote some custom code on top of the web applets SDK that makes the applets talk to each 257 - other over a broadcast channel. This should ideally become a JS library or added to the 258 - web applets SDK. Though you can just copy-paste from the Diffuse repo if you want (it's 259 - not that much code). 260 - </li> 261 - <li><em>... Probably a bunch of stuff I'm forgetting, teaching materials, etc ...</em></li> 262 - </ul> 263 - </section> 264 - </main> 265 - </Page>
-31
_backup/pages/test/cross-origin/blur.astro
··· 1 - --- 2 - import Page from "../../../layouts/page.astro"; 3 - import "@styles/theme/blur/index.css"; 4 - --- 5 - 6 - <Page title="Diffuse"> 7 - <script> 8 - import { applet } from "@scripts/applet/common"; 9 - 10 - //////////////////////////////////////////// 11 - // 🗂️ Applets 12 - //////////////////////////////////////////// 13 - const container = document.querySelector("main"); 14 - if (!container) throw new Error("Missing container"); 15 - 16 - const labelA = "Deck A"; 17 - const labelB = "Deck B"; 18 - 19 - const constituent = { 20 - a: applet("https://applets.diffuse.sh/constituent/blur/artwork-controller", { 21 - container, 22 - }), 23 - b: applet("https://applets.diffuse.sh/constituent/blur/artwork-controller", { 24 - container, 25 - groupId: labelB, 26 - }), 27 - }; 28 - </script> 29 - 30 - <main></main> 31 - </Page>
-15
_backup/pages/test/cross-origin/output.astro
··· 1 - --- 2 - import Page from "../../../layouts/page.astro"; 3 - --- 4 - 5 - <Page title="Diffuse Configurator | Output"> 6 - <iframe 7 - src="https://applets.diffuse.sh/configurator/output/?ui=true" 8 - style="border: 0; display: block; height: 100dvh; width: 100dvw;"></iframe> 9 - 10 - <style is:global> 11 - iframe { 12 - display: none; 13 - } 14 - </style> 15 - </Page>
-147
_backup/pages/theme/pilot/audio/_applet.astro
··· 1 - --- 2 - import "@styles/reset.css"; 3 - import "@styles/variables.css"; 4 - import "@styles/fonts.css"; 5 - import "@styles/icons/iconoir.css"; 6 - import "@styles/theme/pilot/variables.css"; 7 - --- 8 - 9 - <main> 10 - <div class="queue-entry"></div> 11 - <div class="playback-info"> 12 - <div class="controls"> 13 - <button class="controls__playpause"> 14 - <i class="iconoir-play-solid"></i> 15 - </button> 16 - </div> 17 - <div class="time"> 18 - <progress max="100" value="0"></progress> 19 - </div> 20 - </div> 21 - <div class="additional"></div> 22 - </main> 23 - 24 - <style> 25 - main { 26 - align-items: center; 27 - background: var(--made-in-the-shade); 28 - color: oklch(from var(--made-in-the-shade) calc(l + 0.375) c h); 29 - display: flex; 30 - justify-content: center; 31 - text-align: center; 32 - } 33 - 34 - button { 35 - background: transparent; 36 - border: 0; 37 - color: inherit; 38 - cursor: pointer; 39 - display: inline-block; 40 - line-height: 0; 41 - } 42 - 43 - /*********************************** 44 - * Playback info 45 - ***********************************/ 46 - .playback-info { 47 - align-items: center; 48 - display: flex; 49 - flex: 1; 50 - flex-direction: column; 51 - max-width: var(--container-lg); 52 - margin-top: var(--space-2xs); 53 - padding: var(--space-2xs) var(--space-md); 54 - } 55 - 56 - .controls { 57 - align-items: center; 58 - display: flex; 59 - justify-content: center; 60 - 61 - & .controls__playpause { 62 - font-size: var(--fs-lg); 63 - } 64 - } 65 - 66 - .time { 67 - align-self: stretch; 68 - display: flex; 69 - padding: var(--space-2xs) 0; 70 - } 71 - 72 - progress { 73 - appearance: none; 74 - border: 0; 75 - flex: 1; 76 - height: 4px; 77 - } 78 - 79 - progress::-webkit-progress-bar { 80 - background-color: oklch(from var(--made-in-the-shade) calc(l + 0.15) c h); 81 - overflow: hidden; 82 - border-radius: 4px; 83 - } 84 - 85 - progress::-webkit-progress-value { 86 - background-color: oklch(from var(--made-in-the-shade) calc(l + 0.3) c h); 87 - border-radius: 4px; 88 - } 89 - 90 - progress::-moz-progress-bar { 91 - } 92 - </style> 93 - 94 - <script> 95 - // @ts-ignore 96 - import scope from "astro:scope"; 97 - 98 - import type { State } from "./types.d.ts"; 99 - import { register } from "@scripts/applet/common"; 100 - 101 - //////////////////////////////////////////// 102 - // SETUP 103 - //////////////////////////////////////////// 104 - const context = register<State>(); 105 - 106 - // Initial state 107 - context.data = { 108 - isPlaying: false, 109 - }; 110 - 111 - //////////////////////////////////////////// 112 - // ACTIONS 113 - //////////////////////////////////////////// 114 - context.setActionHandler("modifyIsPlaying", (isPlaying: boolean) => { 115 - // NOTE: Doesn't trigger a `data` event 116 - context.data.isPlaying = isPlaying; 117 - render(); 118 - }); 119 - 120 - context.setActionHandler("modifyProgress", (progress: number) => { 121 - const p = isNaN(progress) || !isFinite(progress) ? 0 : Math.min(Math.max(progress, 0), 1); 122 - const el = document.body.querySelector("progress"); 123 - if (el) el.value = p * 100; 124 - }); 125 - 126 - //////////////////////////////////////////// 127 - // UI 128 - //////////////////////////////////////////// 129 - document.body.querySelector(".controls__playpause")?.addEventListener("click", () => { 130 - context.data = { ...context.data, isPlaying: !(context.data?.isPlaying ?? false) }; 131 - }); 132 - 133 - document.body.querySelector(".time")?.addEventListener("click", (event: Event) => { 134 - const mouseEvent = event as MouseEvent; 135 - const seekPosition = mouseEvent.offsetX / (event.target as HTMLProgressElement).clientWidth; 136 - context.data = { ...context.data, seekPosition }; 137 - }); 138 - 139 - function render() { 140 - const button = document.body.querySelector("button"); 141 - 142 - if (button) 143 - button.innerHTML = context.data.isPlaying 144 - ? `<i class="iconoir-pause-solid" data-astro-cid-${scope}></i>` 145 - : `<i class="iconoir-play-solid" data-astro-cid-${scope}></i>`; 146 - } 147 - </script>
-21
_backup/pages/theme/pilot/audio/_manifest.json
··· 1 - { 2 - "name": "diffuse/constituent/pilot/audio", 3 - "title": "", 4 - "entrypoint": "index.html", 5 - "actions": { 6 - "modifyIsPlaying": { 7 - "title": "Set is-playing state", 8 - "description": "Indicate if audio is playing or not.", 9 - "params_schema": { 10 - "type": "boolean" 11 - } 12 - }, 13 - "modifyProgress": { 14 - "title": "Set progress", 15 - "description": "Indicate how far the audio has progressed.", 16 - "params_schema": { 17 - "type": "number" 18 - } 19 - } 20 - } 21 - }
-9
_backup/pages/theme/pilot/audio/index.astro
··· 1 - --- 2 - import Layout from "@layouts/applet.astro"; 3 - import Applet from "./_applet.astro"; 4 - import { title } from "./_manifest.json"; 5 - --- 6 - 7 - <Layout title={title}> 8 - <Applet /> 9 - </Layout>
-4
_backup/pages/theme/pilot/audio/types.d.ts
··· 1 - export interface State { 2 - isPlaying: boolean; 3 - seekPosition?: number; 4 - }
-11
_backup/pages/theme/pilot/index.astro
··· 1 - --- 2 - import Page from "../../../layouts/page.astro"; 3 - import "@styles/theme/pilot/index.css"; 4 - --- 5 - 6 - <Page title="Diffuse"> 7 - <script src="../../../scripts/theme/pilot/index.js"></script> 8 - 9 - <!-- Temporary filler to push audio UI down to the bottom --> 10 - <div class="filler" style="flex: 1;"></div> 11 - </Page>
-496
_backup/scripts/applet/common.ts
··· 1 - import type { Applet, AppletEvent, AppletScope } from "@web-applets/sdk"; 2 - import * as Comlink from "comlink"; 3 - 4 - import { applets } from "@web-applets/sdk"; 5 - import QS from "query-string"; 6 - 7 - import { type ElementConfigurator, h as hyperscript } from "@scripts/spellcaster/hyperscript.js"; 8 - import { isSignal, type Signal, signal } from "@scripts/spellcaster"; 9 - 10 - import type { ResolvedUri } from "@applets/core/types"; 11 - import { transfer, type WorkerTasks } from "@scripts/common"; 12 - 13 - //////////////////////////////////////////// 14 - // 🪟 Applet connecting 15 - //////////////////////////////////////////// 16 - export async function applet<D>( 17 - src: string, 18 - opts: { 19 - addSlashSuffix?: boolean; 20 - container?: HTMLElement | Element; 21 - context?: Window; 22 - frameId?: string; 23 - groupId?: string; 24 - newInstance?: boolean; 25 - setHeight?: boolean; 26 - } = {}, 27 - ): Promise<Applet<D>> { 28 - src = `${src}${ 29 - src.endsWith("/") 30 - ? "" 31 - : opts.addSlashSuffix === undefined || opts.addSlashSuffix === true 32 - ? "/" 33 - : "" 34 - }`; 35 - 36 - let query: undefined | Record<string, string>; 37 - query = { groupId: opts.groupId || "main" }; 38 - 39 - if (query) { 40 - src = QS.stringifyUrl({ url: src, query }); 41 - } 42 - 43 - let context = opts.newInstance ? self : opts.context || self.top || self.parent; 44 - 45 - let existingFrame: HTMLIFrameElement | null; 46 - 47 - // TODO: Ideally we do some cross-origin detection here 48 - try { 49 - existingFrame = opts.newInstance ? null : context.document.querySelector(`[src="${src}"]`); 50 - } catch (err) { 51 - existingFrame = null; 52 - context = self; 53 - } 54 - 55 - let frame; 56 - 57 - if (existingFrame) { 58 - frame = existingFrame; 59 - } else { 60 - frame = document.createElement("iframe"); 61 - frame.loading = "eager"; 62 - frame.src = src; 63 - if (opts.frameId) frame.id = opts.frameId; 64 - 65 - if (opts.container) { 66 - opts.container.appendChild(frame); 67 - } else { 68 - context.document.body.appendChild(frame); 69 - } 70 - } 71 - 72 - if (frame.contentWindow === null) { 73 - throw new Error("iframe does not have a contentWindow"); 74 - } 75 - 76 - const applet = await applets.connect<D>(frame.contentWindow, { context }).catch((err) => { 77 - console.error("Error connecting to " + src, err); 78 - throw err; 79 - }); 80 - 81 - if (opts.setHeight) { 82 - applet.onresize = () => { 83 - frame.height = `${applet.height}px`; 84 - frame.classList.add("has-loaded"); 85 - }; 86 - } else { 87 - if (frame.contentDocument?.readyState === "complete") { 88 - frame.classList.add("has-loaded"); 89 - } 90 - 91 - frame.addEventListener("load", () => { 92 - frame.classList.add("has-loaded"); 93 - }); 94 - } 95 - 96 - return applet; 97 - } 98 - 99 - export function tunnel( 100 - worker: Comlink.Remote<WorkerTasks>, 101 - connections: Record<string, Applet | Promise<Applet>>, 102 - ) { 103 - Object.entries(connections).forEach(([scheme, promise]) => { 104 - Promise.resolve(promise).then((conn) => { 105 - return worker._manage(scheme, transfer(conn.ports.worker)); 106 - }); 107 - }); 108 - } 109 - 110 - //////////////////////////////////////////// 111 - // 🪟 Applet registration 112 - //////////////////////////////////////////// 113 - export type DiffuseApplet<T> = { 114 - groupId: string | undefined; 115 - scope: AppletScope<T>; 116 - 117 - settled(): Promise<void>; 118 - 119 - get instanceId(): string; 120 - set data(data: T); 121 - 122 - codec: Codec<T>; 123 - unloadHandler?: () => void; 124 - 125 - isMainInstance(): boolean | null; 126 - setActionHandler<H extends Function>(actionId: string, actionHandler: H): void; 127 - }; 128 - 129 - export type Codec<T> = { 130 - decode(data: any): T; 131 - encode(data: T): any; 132 - }; 133 - 134 - export function lookupGroupId() { 135 - const url = new URL(location.href); 136 - return url.searchParams.get("groupId") || "main"; 137 - } 138 - 139 - export function register<DataType = any>( 140 - options: { mode?: "broadcast" | "shared-worker"; worker?: Comlink.Remote<WorkerTasks> } = {}, 141 - ): DiffuseApplet<DataType> { 142 - const mode = options.mode ?? "broadcast"; 143 - const scope = applets.register<DataType>(); 144 - 145 - const groupId = lookupGroupId(); 146 - const channelId = `${location.host}${location.pathname}/${groupId}`; 147 - const instanceId = crypto.randomUUID(); 148 - 149 - // Codec 150 - const codec = { 151 - decode: (data: any) => data as DataType, 152 - encode: (data: DataType) => data as any, 153 - }; 154 - 155 - // Context 156 - const context: DiffuseApplet<DataType> = { 157 - groupId, 158 - scope, 159 - 160 - settled() { 161 - return channelContext?.promise.then(() => {}) ?? Promise.resolve(); 162 - }, 163 - 164 - get instanceId() { 165 - return instanceId; 166 - }, 167 - 168 - get data() { 169 - return scope.data; 170 - }, 171 - 172 - set data(data: DataType) { 173 - scope.data = data; 174 - }, 175 - 176 - codec, 177 - 178 - isMainInstance() { 179 - return channelContext?.mainSignal() ?? null; 180 - }, 181 - 182 - setActionHandler: <H extends Function>(actionId: string, actionHandler: H) => { 183 - switch (mode) { 184 - case "broadcast": 185 - return channelContext?.setActionHandler(actionId, actionHandler); 186 - 187 - case "shared-worker": 188 - return scope.setActionHandler(actionId, actionHandler); 189 - } 190 - }, 191 - }; 192 - 193 - if (options.worker) { 194 - context.scope.onworkerport = (event) => { 195 - if (!event.port) return; 196 - options.worker?._listen(transfer(event.port)); 197 - }; 198 - } 199 - 200 - // Channel 201 - const channelContext = 202 - mode === "broadcast" 203 - ? broadcastChannel<DataType>({ 204 - channelId, 205 - context, 206 - instanceId, 207 - scope, 208 - }) 209 - : undefined; 210 - 211 - return context; 212 - } 213 - 214 - function broadcastChannel<DataType>({ 215 - channelId, 216 - context, 217 - instanceId, 218 - scope, 219 - }: { 220 - channelId: string; 221 - context: DiffuseApplet<DataType>; 222 - instanceId: string; 223 - scope: AppletScope<DataType>; 224 - }) { 225 - const isMain = signal<boolean>(true); 226 - 227 - // One instance to rule them all 228 - // 229 - // Ping other instances to see if there are any. 230 - // As long as there aren't any, it is considered the main instance. 231 - // 232 - // Actions are performed on the main instance, 233 - // and data is replicated from main to the other instances. 234 - const channel = new BroadcastChannel(channelId); 235 - 236 - channel.addEventListener("message", async (event) => { 237 - switch (event.data?.type) { 238 - case "PING": { 239 - channel.postMessage({ 240 - type: "PONG", 241 - instanceId: event.data.instanceId, 242 - originInstanceId: instanceId, 243 - }); 244 - 245 - if (isMain() && event.data?.isInitialPing === true) { 246 - channel.postMessage({ 247 - type: "data", 248 - data: context.codec.encode(scope.data), 249 - }); 250 - } 251 - break; 252 - } 253 - 254 - case "PONG": { 255 - if (event.data.instanceId === instanceId) { 256 - isMain(false); 257 - } 258 - break; 259 - } 260 - 261 - case "UNLOADED": { 262 - if (!context.isMainInstance()) { 263 - // We need to wait until the other side is actually unloaded 🤷‍♀️ 264 - setTimeout(async () => { 265 - const promised = await makeMainPromise(); 266 - isMain(promised.isMain); 267 - if (promised.isMain) context.unloadHandler?.(); 268 - }, 250); 269 - } 270 - break; 271 - } 272 - 273 - case "action": { 274 - if (isMain()) { 275 - const result = await scope.actionHandlers[event.data.actionId]?.(...event.data.arguments); 276 - channel.postMessage({ 277 - type: "actioncomplete", 278 - actionInstanceId: event.data.actionInstanceId, 279 - result, 280 - }); 281 - } 282 - break; 283 - } 284 - 285 - case "data": { 286 - scope.data = context.codec.decode(event.data.data); 287 - break; 288 - } 289 - } 290 - }); 291 - 292 - // Promise that fullfills whenever it figures out its the main instance or not. 293 - let pinged = false; 294 - 295 - function makeMainPromise(timeoutDuration: number = 500) { 296 - return new Promise<{ isMain: boolean }>((resolve) => { 297 - const timeoutId = setTimeout(() => { 298 - channel.removeEventListener("message", handler); 299 - resolve({ isMain: true }); 300 - }, timeoutDuration); 301 - 302 - const handler = (event: MessageEvent) => { 303 - if ( 304 - (event.data?.type === "PONG" || event.data?.type === "PING") && 305 - event.data?.instanceId === instanceId 306 - ) { 307 - clearTimeout(timeoutId); 308 - channel.removeEventListener("message", handler); 309 - resolve({ isMain: false }); 310 - } 311 - }; 312 - 313 - channel.addEventListener("message", handler); 314 - channel.postMessage({ 315 - type: "PING", 316 - instanceId, 317 - isInitialPing: !pinged, 318 - }); 319 - 320 - pinged = true; 321 - }); 322 - } 323 - 324 - const promise = makeMainPromise(); 325 - 326 - // If the data on the main instance changes, 327 - // pass it on to other instances. 328 - scope.addEventListener("data", async (event: AppletEvent) => { 329 - await promise; 330 - 331 - if (isMain()) { 332 - channel.postMessage({ 333 - type: "data", 334 - data: context.codec.encode(event.data), 335 - }); 336 - } 337 - }); 338 - 339 - // Action handler 340 - const setActionHandler = <H extends Function>(actionId: string, actionHandler: H) => { 341 - const handler = async (...args: any) => { 342 - if (isMain()) { 343 - return actionHandler(...args); 344 - } 345 - 346 - // Check if a main instance is still available, 347 - // if not, then this is the new main. 348 - const promised = await makeMainPromise(); 349 - isMain(promised.isMain); 350 - 351 - if (isMain()) { 352 - return actionHandler(...args); 353 - } 354 - 355 - const actionMessage = { 356 - actionInstanceId: crypto.randomUUID(), 357 - actionId, 358 - type: "action", 359 - arguments: args, 360 - }; 361 - 362 - return await new Promise((resolve) => { 363 - const actionCallback = (event: MessageEvent) => { 364 - if ( 365 - event.data?.type === "actioncomplete" && 366 - event.data?.actionInstanceId === actionMessage.actionInstanceId 367 - ) { 368 - channel.removeEventListener("message", actionCallback); 369 - resolve(event.data.result); 370 - } 371 - }; 372 - 373 - channel.addEventListener("message", actionCallback); 374 - channel.postMessage(actionMessage); 375 - }); 376 - }; 377 - 378 - scope.setActionHandler(actionId, handler); 379 - }; 380 - 381 - // Before unload 382 - self.addEventListener("beforeunload", (event) => { 383 - if (context.isMainInstance()) { 384 - channel.postMessage({ 385 - type: "UNLOADED", 386 - }); 387 - } 388 - }); 389 - 390 - // Fin 391 - return { 392 - channel, 393 - mainSignal: isMain, 394 - promise, 395 - setActionHandler, 396 - }; 397 - } 398 - 399 - //////////////////////////////////////////// 400 - // 🔮 Reactive state management 401 - //////////////////////////////////////////// 402 - export function reactive<D, T>( 403 - applet: Applet<D> | AppletScope<D>, 404 - dataFn: (data: D) => T, 405 - effectFn: (t: T) => void, 406 - ) { 407 - let value = dataFn(applet.data); 408 - effectFn(value); 409 - 410 - applet.addEventListener("data", (event: AppletEvent) => { 411 - const newData = dataFn(event.data); 412 - if (newData !== value) { 413 - value = newData; 414 - effectFn(value); 415 - } 416 - }); 417 - } 418 - 419 - //////////////////////////////////////////// 420 - // ⚡️ COMMON ACTION CALLS 421 - //////////////////////////////////////////// 422 - 423 - export async function inputUrl(input: Applet, uri: string, method = "GET") { 424 - return await input.sendAction<ResolvedUri>( 425 - "resolve", 426 - { 427 - method, 428 - uri, 429 - }, 430 - { 431 - timeoutDuration: 60000 * 5, 432 - worker: true, 433 - }, 434 - ); 435 - } 436 - 437 - //////////////////////////////////////////// 438 - // 🛠️ 439 - //////////////////////////////////////////// 440 - export function addScope<O extends object>(astroScope: string, object: O): O { 441 - return { 442 - ...object, 443 - attrs: { 444 - ...((object as any).attrs || {}), 445 - [`data-astro-cid-${astroScope}`]: "", 446 - }, 447 - }; 448 - } 449 - 450 - export function appletScopePort() { 451 - let port: MessagePort | undefined; 452 - 453 - function connection(event: AppletEvent) { 454 - if (event.data?.type === "appletconnect") { 455 - window.removeEventListener("message", connection); 456 - port = (event as any).ports[0]; 457 - } 458 - } 459 - 460 - window.addEventListener("message", connection); 461 - 462 - return () => port; 463 - } 464 - 465 - export const hs = 466 - (astroScope: string) => 467 - ( 468 - tag: string, 469 - props?: Record<string, any> | (() => Record<string, any>), 470 - configure?: ElementConfigurator, 471 - ) => { 472 - const propsWithScope = 473 - props && isSignal(props) 474 - ? () => addScope(astroScope, props()) 475 - : addScope(astroScope, props || {}); 476 - 477 - return hyperscript(tag, propsWithScope, configure); 478 - }; 479 - 480 - export function wait<A>(applet: Applet<A>, dataFn: (a: A | undefined) => boolean): Promise<void> { 481 - return new Promise((resolve) => { 482 - if (dataFn(applet.data) === true) { 483 - resolve(); 484 - return; 485 - } 486 - 487 - const callback = (event: AppletEvent) => { 488 - if (dataFn(event.data) === true) { 489 - applet.removeEventListener("data", callback); 490 - resolve(); 491 - } 492 - }; 493 - 494 - applet.addEventListener("data", callback); 495 - }); 496 - }
-11
_backup/scripts/engine/queue/types.d.ts
··· 1 - import type { Track } from "@applets/core/types"; 2 - 3 - export type Item<Stats = TrackStats, Tags = TrackTags> = Track & { 4 - manualEntry?: boolean; 5 - }; 6 - 7 - export interface State<Stats = TrackStats, Tags = TrackTags> { 8 - past: Item<Stats, Tags>[]; 9 - now: Item<Stats, Tags> | null; 10 - future: Item<Stats, Tags>[]; 11 - }
-147
_backup/scripts/engine/queue/worker.ts
··· 1 - import { getTransferables } from "@okikio/transferables"; 2 - 3 - import type { Track } from "@applets/core/types.js"; 4 - import type { Item, State } from "./types"; 5 - import { arrayShuffle, postMessages, provide } from "@scripts/common.ts"; 6 - 7 - //////////////////////////////////////////// 8 - // SETUP 9 - //////////////////////////////////////////// 10 - 11 - const actions = { 12 - add, 13 - pool, 14 - shift, 15 - unshift, 16 - }; 17 - 18 - const { ports, tasks } = provide({ 19 - actions, 20 - tasks: { ...actions, data }, 21 - }); 22 - 23 - export type Actions = typeof actions; 24 - export type Tasks = typeof tasks; 25 - 26 - //////////////////////////////////////////// 27 - // STATE 28 - //////////////////////////////////////////// 29 - 30 - const QUEUE_SIZE = 25; 31 - 32 - const _internal: Record<string, { pool: Track[] }> = {}; 33 - const _state: Record<string, State> = {}; 34 - 35 - function data(groupId: string) { 36 - return state(groupId); 37 - } 38 - 39 - function emptyState(groupId: string): State { 40 - return { 41 - future: [], 42 - now: null, 43 - past: [], 44 - }; 45 - } 46 - 47 - function notify(groupId: string) { 48 - const d = data(groupId); 49 - 50 - postMessages({ 51 - data: { 52 - type: "data", 53 - data: d, 54 - groupId, 55 - }, 56 - ports: ports.applets, 57 - transfer: getTransferables(d), 58 - }); 59 - } 60 - 61 - function internal(groupId: string) { 62 - _internal[groupId] ??= { pool: [] }; 63 - return _internal[groupId]; 64 - } 65 - 66 - function state(groupId: string) { 67 - _state[groupId] ??= emptyState(groupId); 68 - return _state[groupId]; 69 - } 70 - 71 - //////////////////////////////////////////// 72 - // ACTIONS 73 - //////////////////////////////////////////// 74 - 75 - function add({ groupId, items }: { groupId: string; items: Item[] }) { 76 - state(groupId).future = [...state(groupId).future, ...items]; 77 - notify(groupId); 78 - } 79 - 80 - function pool({ groupId, tracks }: { groupId: string; tracks: Track[] }) { 81 - internal(groupId).pool = tracks; 82 - const queue = state(groupId); 83 - 84 - // TODO: If the pool changes, only remove non-existing tracks 85 - // instead of resetting the whole future queue. 86 - // 87 - // What about past queue items? 88 - 89 - queue.future = []; 90 - fill(groupId); 91 - 92 - // Automatically insert track if there isn't any 93 - if (!queue.now) return shift({ groupId }); 94 - else notify(groupId); 95 - } 96 - 97 - function shift({ groupId }: { groupId: string }) { 98 - const queue = state(groupId); 99 - const now = queue.future[0] ?? null; 100 - queue.now = now; 101 - 102 - queue.future = queue.future.slice(1); 103 - queue.past = now ? [...queue.past, now] : queue.past; 104 - 105 - fill(groupId); 106 - } 107 - 108 - function unshift({ groupId }: { groupId: string }) { 109 - const queue = state(groupId); 110 - if (queue.past.length === 0) return; 111 - 112 - const [last] = queue.past.splice(queue.past.length - 1, 1); 113 - const now = last ?? null; 114 - 115 - queue.now = now; 116 - queue.future = now ? [now, ...queue.future] : queue.future; 117 - 118 - notify(groupId); 119 - } 120 - 121 - // 🛠️ 122 - 123 - // TODO: Most likely there's a more performant solution 124 - function fill(groupId: string) { 125 - const queue = state(groupId); 126 - if (queue.future.length >= QUEUE_SIZE) return; 127 - 128 - const pool: Track[] = []; 129 - 130 - let past = new Set(queue.past.map((t) => t.id)); 131 - let reducedPool = pool; 132 - 133 - internal(groupId).pool.forEach((track: Track) => { 134 - if (past.has(track.id)) { 135 - past = past.difference(new Set(track.id)); 136 - } else { 137 - pool.push(track); 138 - } 139 - }); 140 - 141 - if (reducedPool.length === 0) { 142 - reducedPool = internal(groupId).pool; 143 - } 144 - 145 - const poolSelection = arrayShuffle(reducedPool).slice(0, QUEUE_SIZE - queue.future.length); 146 - add({ groupId, items: poolSelection }); 147 - }
-46
_backup/scripts/theme/blur/index.ts
··· 1 - import type { ManagedOutput } from "@applets/core/types"; 2 - import { applet } from "@scripts/applet/common"; 3 - 4 - //////////////////////////////////////////// 5 - // 🗂️ Applets 6 - //////////////////////////////////////////// 7 - import type * as QueueEngine from "@applets/engine/queue/types.d.ts"; 8 - 9 - const container = document.querySelector("main"); 10 - if (!container) throw new Error("Missing container"); 11 - 12 - const labelA = "Deck A"; 13 - const labelB = "Deck B"; 14 - 15 - // const configurator = { 16 - // output: await applet<ManagedOutput>("/configurator/output"), 17 - // }; 18 - 19 - const constituent = { 20 - a: applet("/constituent/blur/artwork-controller", { 21 - container, 22 - groupId: labelA, 23 - }), 24 - b: applet("/constituent/blur/artwork-controller", { 25 - container, 26 - groupId: labelB, 27 - }), 28 - browser: applet("/constituent/blur/browser", { 29 - container, 30 - }), 31 - }; 32 - 33 - // TODO: 34 - // const _orchestrator = { 35 - // primary: applet("/orchestrator/primary", { groupId: labelA }), 36 - // }; 37 - 38 - // const engine = { 39 - // queue: { 40 - // a: await applet<QueueEngine.State>("/engine/queue", { groupId: labelA }), 41 - // b: await applet<QueueEngine.State>("/engine/queue", { groupId: labelB }), 42 - // }, 43 - // }; 44 - 45 - // const deckA = engine.queue.a; 46 - // const deckB = engine.queue.b;
-90
_backup/scripts/theme/pilot/index.ts
··· 1 - import { applet, reactive } from "@scripts/applet/common"; 2 - 3 - //////////////////////////////////////////// 4 - // 🗂️ Applets 5 - //////////////////////////////////////////// 6 - import type * as AudioEngine from "@applets/engine/audio/types.d.ts"; 7 - import type * as QueueEngine from "@applets/engine/queue/types.d.ts"; 8 - 9 - import type * as AudioUI from "@applets/theme/pilot/audio/types"; 10 - 11 - const engine = { 12 - audio: await applet<AudioEngine.State>("/engine/audio"), 13 - queue: await applet<QueueEngine.State>("/engine/queue"), 14 - }; 15 - 16 - const orchestrator = { 17 - queueAudio: applet("/orchestrator/queue-audio"), 18 - queueTracks: applet("/orchestrator/queue-tracks"), 19 - processTracks: applet("/orchestrator/process-tracks"), 20 - }; 21 - 22 - const ui = { 23 - audio: await applet<AudioUI.State>("/theme/pilot/audio/", { setHeight: true }), 24 - }; 25 - 26 - //////////////////////////////////////////// 27 - // ⚙️ [Connections → Engines] 28 - // 🔉 AUDIO 29 - //////////////////////////////////////////// 30 - 31 - // NOTE: 32 - // These could probably be optimised, but it works. 33 - 34 - reactive( 35 - engine.audio, 36 - (data) => 37 - data.isPlaying && (data.items[engine.queue.data.now?.id ?? Infinity]?.isPlaying ?? false), 38 - (isPlaying) => ui.audio.sendAction("modifyIsPlaying", isPlaying), 39 - ); 40 - 41 - reactive( 42 - engine.audio, 43 - (data) => data.items[engine.queue.data.now?.id ?? Infinity]?.progress ?? 0, 44 - (progress: number) => ui.audio.sendAction("modifyProgress", progress), 45 - ); 46 - 47 - //////////////////////////////////////////// 48 - // 🌅 [Connections → UI] 49 - // 🔉 AUDIO 50 - //////////////////////////////////////////// 51 - 52 - let initialAudioChecked = false; 53 - 54 - reactive( 55 - ui.audio, 56 - (data) => data.isPlaying, 57 - async (isPlaying) => { 58 - const audioId = engine.queue.data.now?.id; 59 - 60 - // Sync audio state and ui state 61 - // TODO: Figure out a better way to do this 62 - if (!initialAudioChecked) { 63 - if (engine.audio.data.isPlaying && !isPlaying) { 64 - ui.audio.sendAction("modifyIsPlaying", true); 65 - initialAudioChecked = true; 66 - return; 67 - } 68 - } 69 - 70 - // Otherwise just control the audio 71 - if (isPlaying) { 72 - engine.audio.sendAction("play", { audioId }); 73 - } else { 74 - engine.audio.sendAction("pause", { audioId }); 75 - } 76 - }, 77 - ); 78 - 79 - reactive( 80 - ui.audio, 81 - (data: AudioUI.State) => data.seekPosition, 82 - (seekPosition) => { 83 - if (seekPosition !== undefined && engine.queue.data.now?.id) { 84 - engine.audio.sendAction("seek", { 85 - percentage: seekPosition, 86 - audioId: engine.queue.data.now.id, 87 - }); 88 - } 89 - }, 90 - );
+6
_config.ts
··· 26 26 site.use(postcss({ includes: false })); 27 27 28 28 site.add([".css"]); 29 + 30 + // BINARY ASSETS 31 + 32 + site.add("/favicons"); 33 + site.add("/fonts"); 34 + site.add("/images");
public/favicons/android-chrome-192x192.png src/favicons/android-chrome-192x192.png
public/favicons/android-chrome-512x512.png src/favicons/android-chrome-512x512.png
public/favicons/apple-touch-icon.png src/favicons/apple-touch-icon.png
public/favicons/browserconfig.xml src/favicons/browserconfig.xml
public/favicons/favicon-16x16.png src/favicons/favicon-16x16.png
public/favicons/favicon-32x32.png src/favicons/favicon-32x32.png
public/favicons/favicon.ico src/favicons/favicon.ico
public/favicons/mstile-150x150.png src/favicons/mstile-150x150.png
public/favicons/safari-pinned-tab.svg src/favicons/safari-pinned-tab.svg
public/fonts/InterVariable-Italic.woff2 src/fonts/InterVariable-Italic.woff2
public/fonts/InterVariable.woff2 src/fonts/InterVariable.woff2
public/images/background/1.jpg src/images/background/1.jpg
public/images/background/10.jpg src/images/background/10.jpg
public/images/background/11.jpg src/images/background/11.jpg
public/images/background/12.jpg src/images/background/12.jpg
public/images/background/13.jpg src/images/background/13.jpg
public/images/background/14.jpg src/images/background/14.jpg
public/images/background/15.jpg src/images/background/15.jpg
public/images/background/16.jpg src/images/background/16.jpg
public/images/background/17.jpg src/images/background/17.jpg
public/images/background/18.jpg src/images/background/18.jpg
public/images/background/19.jpg src/images/background/19.jpg
public/images/background/2.jpg src/images/background/2.jpg
public/images/background/20.jpg src/images/background/20.jpg
public/images/background/21.jpg src/images/background/21.jpg
public/images/background/22.jpg src/images/background/22.jpg
public/images/background/23.jpg src/images/background/23.jpg
public/images/background/24.jpg src/images/background/24.jpg
public/images/background/25.jpg src/images/background/25.jpg
public/images/background/26.jpg src/images/background/26.jpg
public/images/background/27.jpg src/images/background/27.jpg
public/images/background/28.jpg src/images/background/28.jpg
public/images/background/29.jpg src/images/background/29.jpg
public/images/background/3.jpg src/images/background/3.jpg
public/images/background/30.jpg src/images/background/30.jpg
public/images/background/4.jpg src/images/background/4.jpg
public/images/background/5.jpg src/images/background/5.jpg
public/images/background/6.jpg src/images/background/6.jpg
public/images/background/7.jpg src/images/background/7.jpg
public/images/background/8.jpg src/images/background/8.jpg
public/images/background/9.jpg src/images/background/9.jpg
public/images/background/thumbnails/1.jpg src/images/background/thumbnails/1.jpg
public/images/background/thumbnails/10.jpg src/images/background/thumbnails/10.jpg
public/images/background/thumbnails/11.jpg src/images/background/thumbnails/11.jpg
public/images/background/thumbnails/12.jpg src/images/background/thumbnails/12.jpg
public/images/background/thumbnails/13.jpg src/images/background/thumbnails/13.jpg
public/images/background/thumbnails/14.jpg src/images/background/thumbnails/14.jpg
public/images/background/thumbnails/15.jpg src/images/background/thumbnails/15.jpg
public/images/background/thumbnails/16.jpg src/images/background/thumbnails/16.jpg
public/images/background/thumbnails/17.jpg src/images/background/thumbnails/17.jpg
public/images/background/thumbnails/18.jpg src/images/background/thumbnails/18.jpg
public/images/background/thumbnails/19.jpg src/images/background/thumbnails/19.jpg
public/images/background/thumbnails/2.jpg src/images/background/thumbnails/2.jpg
public/images/background/thumbnails/20.jpg src/images/background/thumbnails/20.jpg
public/images/background/thumbnails/21.jpg src/images/background/thumbnails/21.jpg
public/images/background/thumbnails/22.jpg src/images/background/thumbnails/22.jpg
public/images/background/thumbnails/23.jpg src/images/background/thumbnails/23.jpg
public/images/background/thumbnails/24.jpg src/images/background/thumbnails/24.jpg
public/images/background/thumbnails/25.jpg src/images/background/thumbnails/25.jpg
public/images/background/thumbnails/26.jpg src/images/background/thumbnails/26.jpg
public/images/background/thumbnails/27.jpg src/images/background/thumbnails/27.jpg
public/images/background/thumbnails/28.jpg src/images/background/thumbnails/28.jpg
public/images/background/thumbnails/29.jpg src/images/background/thumbnails/29.jpg
public/images/background/thumbnails/3.jpg src/images/background/thumbnails/3.jpg
public/images/background/thumbnails/30.jpg src/images/background/thumbnails/30.jpg
public/images/background/thumbnails/4.jpg src/images/background/thumbnails/4.jpg
public/images/background/thumbnails/5.jpg src/images/background/thumbnails/5.jpg
public/images/background/thumbnails/6.jpg src/images/background/thumbnails/6.jpg
public/images/background/thumbnails/7.jpg src/images/background/thumbnails/7.jpg
public/images/background/thumbnails/8.jpg src/images/background/thumbnails/8.jpg
public/images/background/thumbnails/9.jpg src/images/background/thumbnails/9.jpg
public/images/diffuse-current.svg src/images/diffuse-current.svg
public/images/diffuse-dark.svg src/images/diffuse-dark.svg
public/images/diffuse-grey.svg src/images/diffuse-grey.svg
public/images/diffuse-light.svg src/images/diffuse-light.svg
public/images/diffuse__icon-dark.svg src/images/diffuse__icon-dark.svg
public/images/diffuse__icon-grey.svg src/images/diffuse__icon-grey.svg
public/images/diffuse__icon-light.svg src/images/diffuse__icon-light.svg
public/images/icon-square-ws.png src/images/icon-square-ws.png
public/images/icon-square.png src/images/icon-square.png
public/images/icon.png src/images/icon.png
public/images/icons/windows_98/cd_audio_cd_a-4.png src/images/icons/windows_98/cd_audio_cd_a-4.png
public/images/icons/windows_98/directory_open_cool-0.png src/images/icons/windows_98/directory_open_cool-0.png
public/images/icons/windows_98/directory_open_file_mydocs_2k-2.png src/images/icons/windows_98/directory_open_file_mydocs_2k-2.png
+9
src/_components/element.vto
··· 1 + <div class="element"> 2 + <h3>{{title}}</h3> 3 + 4 + <p> 5 + <em>{{content}}</em> 6 + </p> 7 + 8 + {{ await comp.list({ items }) }} 9 + </div>
+11
src/_components/list.vto
··· 1 + <ul> 2 + {{ for item of items }} 3 + <li> 4 + {{ if item.title.startsWith("(TODO) ") }} 5 + <span>{{item.title}}</span> 6 + {{ else }} 7 + <a href="{{item.url}}">{{item.title}}</a> 8 + {{ /if }} 9 + </li> 10 + {{ /for }} 11 + </ul>
+21
src/_includes/layouts/diffuse.vto
··· 1 + --- 2 + title: "Diffuse" 3 + --- 4 + 5 + <html lang="en"> 6 + <head> 7 + <meta charset="UTF-8" /> 8 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 9 + 10 + <title>{{title}}</title> 11 + 12 + <style> 13 + @import "./styles/reset.css"; 14 + @import "./styles/fonts.css"; 15 + @import "./styles/variables.css"; 16 + </style> 17 + </head> 18 + <body> 19 + {{ content }} 20 + </body> 21 + </html>
+116
src/index.vto
··· 1 + --- 2 + layout: layouts/diffuse.vto 3 + 4 + engines: 5 + - url: "engine/audio/" 6 + title: "Audio" 7 + - url: "engine/queue/" 8 + title: "Queue" 9 + 10 + --- 11 + <link rel="stylesheet" href="./styles/page/index.css" /> 12 + 13 + <header> 14 + <h1> 15 + <svg viewBox="0 0 902 134" width="160"> 16 + <title>Diffuse</title> 17 + <use 18 + xlink:href="/images/diffuse-current.svg#diffuse" 19 + href="/images/diffuse-current.svg#diffuse"></use> 20 + </svg> 21 + </h1> 22 + <p> 23 + Diffuse is a collection of custom elements that make it possible to 24 + listen to audio from various sources on your devices and the web, and to create the ideal digital 25 + listening experience for you. 26 + </p> 27 + <p> 28 + <strong>⚠️ Heavily experimental</strong> 29 + </p> 30 + </header> 31 + <main> 32 + <div class="columns"> 33 + <!-- THEMES --> 34 + <section> 35 + <h2 id="themes">Themes</h2> 36 + 37 + <p> 38 + Themes are compositions and provide a traditional browser web application way of 39 + using them. Each theme is unique, not just a skin (ie. not like winamp skins). 40 + </p> 41 + 42 + <p> 43 + For example, most themes here will limit the currently playing audio tracks to one item, 44 + but you might as well create a DJ theme that can play multiple items at the same time. 45 + </p> 46 + </section> 47 + 48 + <!-- ABSTRACTIONS --> 49 + <!--<section></section>--> 50 + </div> 51 + 52 + <!-- CONSTITUENTS --> 53 + <!--<section></section>--> 54 + 55 + <!-- ELEMENTS --> 56 + <section> 57 + <h2 id="elements">Elements</h2> 58 + 59 + <p> 60 + The (web) components of the system. These 61 + are then recombined into an entire music player experience, or whatever you want to build. 62 + </p> 63 + 64 + <div class="columns"> 65 + {{ await comp.element({ 66 + title: "Engines", 67 + items: engines, 68 + content: ` 69 + Elements with each a singular purpose and don't have any UI. There are specialised UI and 70 + orchestrator elements that control these. 71 + ` 72 + }) }} 73 + 74 + <!--<Applet title="Configurators" list={configurators}> 75 + Applets that serve as an intermediate in order to make a particular kind of applet 76 + configurable. In other words, these allow for an applet to be swapped out with another 77 + that takes the same, or a subset of the actions and data output. 78 + </Applet> 79 + 80 + <Applet title="Input" list={input}> 81 + Inputs are sources of audio tracks. Each track is an entry in the list of possible items 82 + to play. These can be files or streams, static or dynamic. 83 + </Applet> 84 + 85 + <Applet title="Orchestrators" list={orchestrators}> 86 + These too are applet compositions. However, unlike themes, these are purely logical. 87 + Mostly exist in order to construct sensible defaults to use across themes and 88 + abstractions. 89 + </Applet> 90 + 91 + <Applet title="Output" list={output}> 92 + Output is application-derived data such as playlists. These applets can receive such data 93 + and keep it around. 94 + </Applet> 95 + 96 + <Applet title="Processors" list={processors}> 97 + These applets work with the tracks generated by the input applets to add more data to 98 + them, or process them in some other way. 99 + </Applet> 100 + 101 + <Applet title="Supplements" list={[]}>Additional applets, such as scrobblers.</Applet>--> 102 + </div> 103 + </section> 104 + 105 + <!-- DEMOS --> 106 + <!--<section> 107 + <h2 id="demos">Demos</h2> 108 + 109 + <p>Just some utility web pages to help demo the system.</p> 110 + 111 + <List items={demos} /> 112 + </section>--> 113 + 114 + <!-- BUILD YOUR OWN --> 115 + <!--<section></section>--> 116 + </main>
-5
src/styles/applet/common.css
··· 1 - .with-icon { 2 - align-items: center; 3 - display: inline-flex; 4 - gap: 0.75em; 5 - }
+2 -2
src/styles/fonts.css
··· 3 3 font-family: InterVariable; 4 4 font-style: normal; 5 5 font-weight: 100 900; 6 - src: url("/fonts/InterVariable.woff2") format("woff2"); 6 + src: url("../fonts/InterVariable.woff2") format("woff2"); 7 7 } 8 8 9 9 @font-face { ··· 11 11 font-family: InterVariable; 12 12 font-style: italic; 13 13 font-weight: 100 900; 14 - src: url("/fonts/InterVariable-Italic.woff2") format("woff2"); 14 + src: url("../fonts/InterVariable-Italic.woff2") format("woff2"); 15 15 }
+6 -1
src/styles/page/index.css
··· 16 16 text-underline-offset: 6px; 17 17 } 18 18 19 + h1 { 20 + margin: var(--space-lg) 0 var(--space-lg); 21 + padding-top: var(--space-2xs); 22 + } 23 + 19 24 h1 svg { 20 25 fill: oklch(from var(--bg-color) calc(l - 0.5) c h); 21 26 opacity: 0.125; ··· 70 75 gap: 0 var(--space-3xl); 71 76 } 72 77 73 - .applet { 78 + .element { 74 79 min-width: min(var(--container-xs), 100%); 75 80 width: 32.5%; 76 81 }
-78
src/styles/theme/pilot/index.css
··· 1 - @import "./variables.css"; 2 - 3 - /*********************************** 4 - * Fonts 5 - ***********************************/ 6 - :root { 7 - font-family: "Inter", sans-serif; 8 - font-size: var(--fs-base); 9 - } 10 - 11 - @supports (font-variation-settings: normal) { 12 - :root { 13 - font-family: "InterVariable", sans-serif; 14 - font-feature-settings: 15 - "ss03" 2, 16 - "ss02" 2; 17 - font-optical-sizing: auto; 18 - } 19 - } 20 - 21 - body { 22 - background-color: var(--delicate-cloud); 23 - color: var(--made-in-the-shade); 24 - display: flex; 25 - flex-direction: column; 26 - overflow: hidden; 27 - height: 100dvh; 28 - } 29 - 30 - iframe { 31 - border: 0; 32 - } 33 - 34 - /*********************************** 35 - * Applets (UI) 36 - ***********************************/ 37 - #applet__ui__audio { 38 - opacity: 0; 39 - pointer-events: none; 40 - transition: 375ms opacity; 41 - transition-delay: 250ms; 42 - width: 100%; 43 - 44 - &.has-loaded { 45 - opacity: 1; 46 - pointer-events: initial; 47 - } 48 - } 49 - 50 - /*********************************** 51 - * Applets (No UI) 52 - ***********************************/ 53 - iframe[src*="/configurator/"], 54 - iframe[src*="/engine/"], 55 - iframe[src*="/input/"], 56 - iframe[src*="/orchestrator/"], 57 - iframe[src*="/processor/"], 58 - iframe[src*="/output/"] { 59 - height: 0; 60 - left: 110vw; 61 - opacity: 0; 62 - overflow: hidden; 63 - pointer-events: none; 64 - position: absolute; 65 - top: 110vh; 66 - width: 0; 67 - } 68 - 69 - /* Audio is a special case, iframe needs to be "visible" in order to play the audio. */ 70 - #applet__engine__audio { 71 - height: 1px; 72 - left: 0; 73 - opacity: 0; 74 - pointer-events: none; 75 - position: absolute; 76 - top: 0; 77 - width: 1px; 78 - }
-26
src/styles/theme/pilot/variables.css
··· 1 - :root { 2 - /* Colors */ 3 - /* https://farbvelo.elastiq.ch/?s=eyJzIjoiZTBjNjIyMTdiNTcxZSIsImEiOjYsImNnIjo0LCJoZyI6dHJ1ZSwiaGIiOmZhbHNlLCJobyI6ZmFsc2UsImhjIjpmYWxzZSwiaHQiOmZhbHNlLCJiIjpmYWxzZSwicCI6MC4xNzUsIm1kIjo2MCwiY20iOiJsYWIiLCJmIjoiTGVnYWN5IiwiYyI6ImhzbHV2Iiwic2MiOmZhbHNlLCJidyI6dHJ1ZSwiYWgiOmZhbHNlLCJpdSI6IiIsImxtIjp0cnVlLCJzbSI6ZmFsc2UsImN2IjoiaGV4IiwicW0iOiJhcnQtcGFsZXR0ZSIsIm5sIjoiYmVzdE9mIn0= */ 4 - --moonscape: #7f6c71; 5 - --grandma’s-pink-tiles: #e1bac0; 6 - --cinderella: #f8d1c6; 7 - --young-apricot: #f8d7b6; 8 - --cereal-flake: #f0d8ad; 9 - --oatmeal: #cdc5b9; 10 - 11 - /* https://farbvelo.elastiq.ch/?s=eyJzIjoiZmZjY2JkZDg2ZjEzYiIsImEiOjYsImNnIjo0LCJoZyI6dHJ1ZSwiaGIiOmZhbHNlLCJobyI6ZmFsc2UsImhjIjpmYWxzZSwiaHQiOmZhbHNlLCJiIjpmYWxzZSwicCI6MC4xNzgzMDcwODQxNjMzNDY2LCJtZCI6NjAsImNtIjoibGFiIiwiZiI6IkxlZ2FjeSIsImMiOiJoc2x1diIsInNjIjpmYWxzZSwiYnciOnRydWUsImFoIjpmYWxzZSwiaXUiOiIiLCJsbSI6dHJ1ZSwic20iOmZhbHNlLCJjdiI6ImhzbCIsInFtIjoiYXJ0LXBhbGV0dGUiLCJubCI6ImJlc3RPZiJ9 */ 12 - --made-in-the-shade: #67717c; 13 - --misty-mountains: #b8cce0; 14 - --lucid-dreams: #c7e6f4; 15 - --icy-breeze: #c2eff1; 16 - --crushed-ice: #bdf5ed; 17 - --water-leaf: #b7efe7; 18 - 19 - /* https://farbvelo.elastiq.ch/?s=eyJzIjoiODJiN2FjMjU1ODRiOCIsImEiOjYsImNnIjo0LCJoZyI6dHJ1ZSwiaGIiOmZhbHNlLCJobyI6ZmFsc2UsImhjIjpmYWxzZSwiaHQiOmZhbHNlLCJiIjpmYWxzZSwicCI6MC4yMTkxOTgyMDcxNzEzMTQ3LCJtZCI6NjAsImNtIjoibGFiIiwiZiI6IkxlZ2FjeSIsImMiOiJoc2x1diIsInNjIjpmYWxzZSwiYnciOnRydWUsImFoIjpmYWxzZSwiaXUiOiIiLCJsbSI6dHJ1ZSwic20iOmZhbHNlLCJjdiI6ImhleCIsInFtIjoiYXJ0LXBhbGV0dGUiLCJubCI6ImJlc3RPZiJ9 */ 20 - --wizards-brew: #9d8bb3; 21 - --innocent-snowdrop: #cec0fa; 22 - --foggy-plateau: #d5d2fb; 23 - --puffy-cloud: #dce3fb; 24 - --diamond-white: #e1f4fb; 25 - --delicate-cloud: #d9dbe4; 26 - }