Experiment to rebuild Diffuse using web applets.
0
fork

Configure Feed

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

feat: Basics for storage configurator

+375 -132
+6
astro.config.js
··· 1 + import { defineConfig } from "astro/config"; 2 + import scope from "astro-scope"; 3 + 4 + export default defineConfig({ 5 + integrations: [scope()], 6 + });
+3
deno.lock
··· 22 22 "packageJson": { 23 23 "dependencies": [ 24 24 "npm:@picocss/pico@^2.1.1", 25 + "npm:astro-scope@^3.0.1", 25 26 "npm:astro@^5.7.4", 27 + "npm:iconoir@^7.11.0", 28 + "npm:idb-keyval@^6.2.1", 26 29 "npm:spellcaster@^5.0.2", 27 30 "npm:throttle-debounce@^5.0.2", 28 31 "npm:xxh32@^2.0.5"
+28 -1
package-lock.json
··· 7 7 "dependencies": { 8 8 "@picocss/pico": "^2.1.1", 9 9 "@web-applets/sdk": "file:../../unternet-co/web-applets/sdk/", 10 + "iconoir": "^7.11.0", 11 + "idb-keyval": "^6.2.1", 10 12 "spellcaster": "^5.0.2", 11 13 "throttle-debounce": "^5.0.2", 12 14 "xxh32": "^2.0.5" 13 15 }, 14 16 "devDependencies": { 15 - "astro": "^5.7.4" 17 + "astro": "^5.7.4", 18 + "astro-scope": "^3.0.1" 16 19 } 17 20 }, 18 21 "../../unternet-co/web-applets/sdk": { 22 + "name": "@web-applets/sdk", 19 23 "version": "0.2.6", 20 24 "license": "MIT", 21 25 "devDependencies": { ··· 1587 1591 "sharp": "^0.33.3" 1588 1592 } 1589 1593 }, 1594 + "node_modules/astro-scope": { 1595 + "version": "3.0.1", 1596 + "resolved": "https://registry.npmjs.org/astro-scope/-/astro-scope-3.0.1.tgz", 1597 + "integrity": "sha512-/mdiiv0BELoDvhHPMCBokTymW11KOp+zTh4OMqmYGb8DxpOxE59O2VhtiWseoZdKJNQkNyyN/9Nsu3VZrPGWHA==", 1598 + "dev": true, 1599 + "peerDependencies": { 1600 + "astro": "^4.2.2 || 5" 1601 + } 1602 + }, 1590 1603 "node_modules/axobject-query": { 1591 1604 "version": "4.1.0", 1592 1605 "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", ··· 2454 2467 "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", 2455 2468 "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", 2456 2469 "dev": true 2470 + }, 2471 + "node_modules/iconoir": { 2472 + "version": "7.11.0", 2473 + "resolved": "https://registry.npmjs.org/iconoir/-/iconoir-7.11.0.tgz", 2474 + "integrity": "sha512-F9T/E08aJBaQ+VOBjn+ChWKn3hFwsaK5VZ024OFMxdDaxKjLGDpU/OsU7MO9wXM+mDs4ZImypdXIn0fFZAXKmA==", 2475 + "funding": { 2476 + "type": "opencollective", 2477 + "url": "https://opencollective.com/iconoir" 2478 + } 2479 + }, 2480 + "node_modules/idb-keyval": { 2481 + "version": "6.2.1", 2482 + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", 2483 + "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==" 2457 2484 }, 2458 2485 "node_modules/import-meta-resolve": { 2459 2486 "version": "4.1.0",
+4 -1
package.json
··· 2 2 "dependencies": { 3 3 "@picocss/pico": "^2.1.1", 4 4 "@web-applets/sdk": "file:../../unternet-co/web-applets/sdk/", 5 + "iconoir": "^7.11.0", 6 + "idb-keyval": "^6.2.1", 5 7 "spellcaster": "^5.0.2", 6 8 "throttle-debounce": "^5.0.2", 7 9 "xxh32": "^2.0.5" 8 10 }, 9 11 "devDependencies": { 10 - "astro": "^5.7.4" 12 + "astro": "^5.7.4", 13 + "astro-scope": "^3.0.1" 11 14 } 12 15 }
+119
src/applets/configurator/storage/output/applet.astro
··· 1 + <main class="container"> 2 + <h1>Storage configuration</h1> 3 + <p>Here you can select one or more storages (applets) to keep your user data on.</p> 4 + <div id="options"> 5 + <p> 6 + <span class="with-icon"> 7 + <i class="iconoir-app-window"></i> 8 + <strong>Browser storage</strong> 9 + </span> 10 + 11 + <br /> 12 + 13 + <span class="pico-color-jade-500 with-icon"> 14 + <i class="iconoir-check-circle-solid"></i> 15 + Activated 16 + </span> 17 + </p> 18 + <p> 19 + <span class="with-icon"> 20 + <i class="iconoir-laptop"></i> 21 + <strong>Device storage</strong> 22 + </span> 23 + 24 + <br /> 25 + 26 + <span class="pico-color-amber-500 with-icon"> 27 + <i class="iconoir-settings"></i> 28 + Needs configuration 29 + </span> 30 + </p> 31 + <p> 32 + <span class="with-icon"> 33 + <i class="iconoir-cloud"></i> 34 + <strong>Dropbox</strong> 35 + </span> 36 + 37 + <br /> 38 + 39 + <span class="pico-color-grey-500 with-icon"> 40 + <i class="iconoir-xmark-circle"></i> 41 + Deactivated 42 + </span> 43 + </p> 44 + </div> 45 + </main> 46 + 47 + <style> 48 + @import "../../../../styles/configurator.css"; 49 + 50 + .with-icon { 51 + align-items: center; 52 + display: inline-flex; 53 + gap: 6px; 54 + } 55 + </style> 56 + 57 + <script> 58 + import * as IDB from "idb-keyval"; 59 + import { applets } from "@web-applets/sdk"; 60 + 61 + type Getter = ({ name }: { name: string }) => Promise<Uint8Array | undefined>; 62 + type Setter = ({ data, name }: { data: Uint8Array; name: string }) => Promise<void>; 63 + 64 + //////////////////////////////////////////// 65 + // SETUP 66 + //////////////////////////////////////////// 67 + const context = applets.register(); 68 + 69 + // TODO: Should migrate + merge data when switching storages 70 + 71 + // Initial state 72 + context.data = undefined; 73 + 74 + //////////////////////////////////////////// 75 + // ACTIONS 76 + //////////////////////////////////////////// 77 + context.setActionHandler("get", get); 78 + context.setActionHandler("put", put); 79 + 80 + function selectedStorage() { 81 + return localStorage.getItem("storage") || "indexeddb"; 82 + } 83 + 84 + async function get(args: Parameters<Getter>[0]) { 85 + let data; 86 + 87 + switch (selectedStorage()) { 88 + case "indexeddb": { 89 + data = await idbGet(args); 90 + break; 91 + } 92 + } 93 + 94 + if (data) { 95 + context.data = data; 96 + } else { 97 + context.data = undefined; 98 + } 99 + } 100 + 101 + async function put(args: Parameters<Setter>[0]) { 102 + switch (selectedStorage()) { 103 + case "indexeddb": 104 + return await idbPut(args); 105 + } 106 + } 107 + 108 + //////////////////////////////////////////// 109 + // [Storages] 110 + // INDEXED DB 111 + //////////////////////////////////////////// 112 + const idbGet: Getter = async ({ name }) => { 113 + return await IDB.get(name); 114 + }; 115 + 116 + const idbPut: Setter = async ({ data, name }) => { 117 + return await IDB.set(name, data); 118 + }; 119 + </script>
+28
src/applets/configurator/storage/output/manifest.json
··· 1 + { 2 + "name": "diffuse/configurator/storage/output", 3 + "title": "Diffuse Configurator | Storage | Output", 4 + "entrypoint": "index.html", 5 + "actions": { 6 + "get": { 7 + "title": "Get", 8 + "description": "Get data from the configured storage", 9 + "params_schema": { 10 + "type": "object", 11 + "properties": { 12 + "name": { "type": "string" } 13 + } 14 + } 15 + }, 16 + "put": { 17 + "title": "Put", 18 + "description": "Put data on the configured storage", 19 + "params_schema": { 20 + "type": "object", 21 + "properties": { 22 + "data": { "type": "object" }, 23 + "name": { "type": "string" } 24 + } 25 + } 26 + } 27 + } 28 + }
+19 -13
src/applets/orchestrator/queue/applet.astro
··· 1 1 <script> 2 2 import { applets } from "@web-applets/sdk"; 3 + 4 + import type { Track, Output } from "../../core/types.d.ts"; 3 5 import { applet, comparable, reactive } from "../../../scripts/theme"; 4 6 5 7 //////////////////////////////////////////// 6 8 // SETUP 7 9 //////////////////////////////////////////// 8 - import type { Track, Output } from "../../core/types.d.ts"; 9 - 10 10 import type * as AudioEngine from "../../engine/audio/types.d.ts"; 11 11 import type * as QueueEngine from "../../engine/queue/types.d.ts"; 12 12 ··· 19 19 queue: await applet<QueueEngine.State>("../../engine/queue", { context: self.parent }), 20 20 }; 21 21 22 - const storage = { 23 - output: { 24 - memory: await applet<Output>("../../storage/output/memory", { context: self.parent }), 25 - }, 22 + const orchestrator = { 23 + storage: await applet<Output>("../../orchestrator/storage", { context: self.parent }), 26 24 }; 27 25 28 26 //////////////////////////////////////////// ··· 35 33 // into a usable audio URL. 36 34 engine.queue.sendAction( 37 35 "add", 38 - storage.output.memory.data.tracks.map((track: Track) => { 36 + orchestrator.storage.data.tracks.map((track: Track) => { 39 37 return { 40 38 expiresAt: Infinity, 41 39 id: track.id, ··· 57 55 engine.audio, 58 56 (data) => data.items[engine.queue.data.now?.id ?? Infinity]?.hasEnded ?? false, 59 57 (hasEnded) => { 60 - if (hasEnded) engine.queue.sendAction("shift", null); 58 + if (hasEnded) engine.queue.sendAction("shift"); 61 59 }, 62 60 ); 63 61 ··· 99 97 ); 100 98 101 99 //////////////////////////////////////////// 102 - // ⚙️ [Connections → Engines] 103 - // 📦 Storage 100 + // 🎻 [Connections → Orchestrators] 101 + // 📦 STORAGE 104 102 //////////////////////////////////////////// 105 103 reactive( 106 - storage.output.memory, 107 - (data) => comparable(data.tracks), 108 - () => fill(), 104 + orchestrator.storage, 105 + (data) => (data ? comparable(data.tracks) : undefined), 106 + (hash) => { 107 + if (hash) fill(); 108 + }, 109 109 ); 110 110 </script> 111 + 112 + <style> 113 + iframe { 114 + display: none; 115 + } 116 + </style>
+6 -1
src/applets/orchestrator/queue/manifest.json
··· 2 2 "name": "diffuse/orchestrator/queue", 3 3 "title": "Diffuse Orchestrator | Queue", 4 4 "entrypoint": "index.html", 5 - "actions": {} 5 + "actions": { 6 + "fill": { 7 + "title": "Fill", 8 + "description": "Fill up the queue." 9 + } 10 + } 6 11 }
+119
src/applets/orchestrator/storage/applet.astro
··· 1 + <script> 2 + import { applets } from "@web-applets/sdk"; 3 + 4 + import type { Output, Source, Track } from "../../core/types.d.ts"; 5 + import { applet, reactive } from "../../../scripts/theme"; 6 + 7 + //////////////////////////////////////////// 8 + // SETUP 9 + //////////////////////////////////////////// 10 + // Register applet 11 + const context = applets.register<Output>(); 12 + 13 + // Applet connections 14 + const configurator = { 15 + storage: { 16 + output: await applet("../../configurator/storage/output", { context: self.parent }), 17 + }, 18 + }; 19 + 20 + // Sample content 21 + const SAMPLE_SOURCE = { 22 + id: crypto.randomUUID(), 23 + meta: {}, 24 + 25 + appletURI: "TODO", 26 + }; 27 + 28 + const SAMPLE_TRACKS: Track[] = [ 29 + { 30 + id: crypto.randomUUID(), 31 + sourceId: SAMPLE_SOURCE.id, 32 + uri: "https://archive.org/download/SUSPENSE_Radio_Digitally_Restored_Collection/%2040-07-22%20The%20Lodger%20%28audition%29%20%28Herbert%20Marshall%2C%20Alfred%20Hitchcock%2C%20Edmund%20Gwenn%29.mp3", 33 + tags: { 34 + title: "Yours Truly, Johnny Dollar", 35 + }, 36 + }, 37 + { 38 + id: crypto.randomUUID(), 39 + sourceId: SAMPLE_SOURCE.id, 40 + uri: "https://archive.org/download/OTRR_Dimension_X_Singles/Dimension_X_1950-04-08__01_OuterLimit.mp3", 41 + tags: { 42 + title: "Dimension X", 43 + }, 44 + }, 45 + ]; 46 + 47 + // Initial state 48 + context.data = { 49 + sources: await loadSources(), 50 + tracks: await loadTracks(), 51 + }; 52 + 53 + //////////////////////////////////////////// 54 + // LOADERS 55 + //////////////////////////////////////////// 56 + async function loadSources(): Promise<Source[]> { 57 + // TODO: This is not concurrency safe! 58 + await configurator.storage.output.sendAction("get", { 59 + name: "sources", 60 + }); 61 + 62 + const data = configurator.storage.output.data; 63 + if (!data) return [SAMPLE_SOURCE]; 64 + return decode(data as Uint8Array); 65 + } 66 + 67 + async function loadTracks(): Promise<Track[]> { 68 + // TODO: This is not concurrency safe! 69 + await configurator.storage.output.sendAction("get", { 70 + name: "tracks", 71 + }); 72 + 73 + const data = configurator.storage.output.data; 74 + if (!data) return SAMPLE_TRACKS; 75 + return decode(data as Uint8Array); 76 + } 77 + 78 + //////////////////////////////////////////// 79 + // ACTIONS 80 + //////////////////////////////////////////// 81 + 82 + // UPDATE SOURCES: addSource, removeSource → SAVE_SOURCES 83 + // UPDATE TRACKS: addTracks → SAVE_TRACKS 84 + 85 + function saveSources(sources: Source[]) { 86 + const data = encode(sources); 87 + 88 + configurator.storage.output.sendAction("put", { 89 + name: "sources", 90 + data, 91 + }); 92 + } 93 + 94 + function saveTracks(tracks: Track[]) { 95 + const data = encode(tracks); 96 + 97 + configurator.storage.output.sendAction("put", { 98 + name: "tracks", 99 + data, 100 + }); 101 + } 102 + 103 + //////////////////////////////////////////// 104 + // 🛠️ 105 + //////////////////////////////////////////// 106 + function decode(data: Uint8Array) { 107 + return JSON.parse(new TextDecoder().decode(data)); 108 + } 109 + 110 + function encode(data: Object) { 111 + return new TextEncoder().encode(JSON.stringify(data)); 112 + } 113 + </script> 114 + 115 + <style> 116 + iframe { 117 + display: none; 118 + } 119 + </style>
+6
src/applets/orchestrator/storage/manifest.json
··· 1 + { 2 + "name": "diffuse/orchestrator/storage", 3 + "title": "Diffuse Orchestrator | Storage", 4 + "entrypoint": "index.html", 5 + "actions": {} 6 + }
-58
src/applets/storage/output/memory/applet.astro
··· 1 - <script> 2 - import { applets } from "@web-applets/sdk"; 3 - 4 - import type { Output, Track } from "../../../core/types.d.ts"; 5 - import { applet, reactive } from "../../../../scripts/theme"; 6 - 7 - //////////////////////////////////////////// 8 - // SETUP 9 - //////////////////////////////////////////// 10 - const context = applets.register<Output>(); 11 - 12 - const SAMPLE_SOURCE = { 13 - id: crypto.randomUUID(), 14 - meta: {}, 15 - 16 - appletURI: "TODO", 17 - }; 18 - 19 - const SAMPLE_TRACKS: Track[] = [ 20 - { 21 - id: crypto.randomUUID(), 22 - sourceId: SAMPLE_SOURCE.id, 23 - uri: "https://archive.org/download/SUSPENSE_Radio_Digitally_Restored_Collection/%2040-07-22%20The%20Lodger%20%28audition%29%20%28Herbert%20Marshall%2C%20Alfred%20Hitchcock%2C%20Edmund%20Gwenn%29.mp3", 24 - tags: { 25 - title: "Yours Truly, Johnny Dollar", 26 - }, 27 - }, 28 - { 29 - id: crypto.randomUUID(), 30 - sourceId: SAMPLE_SOURCE.id, 31 - uri: "https://archive.org/download/OTRR_Dimension_X_Singles/Dimension_X_1950-04-08__01_OuterLimit.mp3", 32 - tags: { 33 - title: "Dimension X", 34 - }, 35 - }, 36 - ]; 37 - 38 - // Initial state 39 - context.data = { 40 - sources: [SAMPLE_SOURCE], 41 - tracks: SAMPLE_TRACKS, // [] 42 - }; 43 - 44 - // State helpers 45 - function update(partial: Partial<Output>): void { 46 - context.data = { ...context.data, ...partial }; 47 - } 48 - 49 - //////////////////////////////////////////// 50 - // ACTIONS 51 - //////////////////////////////////////////// 52 - </script> 53 - 54 - <style> 55 - iframe { 56 - display: none; 57 - } 58 - </style>
-6
src/applets/storage/output/memory/manifest.json
··· 1 - { 2 - "name": "diffuse/storage/output/memory", 3 - "title": "Diffuse Storage | Output | Memory", 4 - "entrypoint": "index.html", 5 - "actions": {} 6 - }
+5 -7
src/applets/themes/pilot/ui/audio/applet.astro
··· 1 - <link 2 - rel="stylesheet" 3 - href="https://cdn.jsdelivr.net/gh/iconoir-icons/iconoir@main/css/iconoir.css" 4 - /> 5 - 6 1 <main> 7 2 <div class="queue-entry"></div> 8 3 <div class="playback-info"> ··· 20 15 21 16 <style> 22 17 @import "../../../../../styles/themes/pilot/variables.css"; 18 + @import "../../../../../styles/icons.css"; 23 19 24 20 main { 25 21 align-items: center; ··· 91 87 </style> 92 88 93 89 <script> 90 + // @ts-ignore 91 + import scope from "astro:scope"; 94 92 import { applets } from "@web-applets/sdk"; 95 93 import { State } from "./types"; 96 94 ··· 131 129 132 130 function render() { 133 131 document.body.querySelector("button").innerHTML = context.data.isPlaying 134 - ? `<i class="iconoir-pause-solid"></i>` 135 - : `<i class="iconoir-play-solid"></i>`; 132 + ? `<i class="iconoir-pause-solid" data-astro-cid-${scope}></i>` 133 + : `<i class="iconoir-play-solid" data-astro-cid-${scope}></i>`; 136 134 } 137 135 </script>
+19 -4
src/pages/index.astro
··· 4 4 // import "@picocss/pico/css/pico.colors.css"; 5 5 import "../styles/pages/index.css"; 6 6 7 + const configurators = [{ url: "configurator/storage/output/", title: "Storage / Output" }]; 8 + 7 9 const engines = [ 8 10 { url: "engine/audio/", title: "Audio" }, 9 11 { url: "engine/queue/", title: "Queue" }, 10 12 ]; 11 13 12 - const orchestrators = [{ url: "orchestrator/queue/", title: "Queue" }]; 14 + const orchestrators = [ 15 + { url: "orchestrator/queue/", title: "Queue" }, 16 + { url: "orchestrator/storage/", title: "Storage" }, 17 + ]; 13 18 14 - const storages = [{ url: "storage/output/memory/", title: "Output / Memory" }]; 19 + const storages = []; 15 20 16 21 const themes = [{ url: "themes/pilot/", title: "Pilot" }]; 17 22 --- ··· 76 81 <p> 77 82 <em 78 83 >These too are applet compositions. However, unlike themes, these are purely logical, and 79 - optional. Mostly exist in order to construct sensible defaults (eg. applet connections you 80 - want to reuse across themes).</em 84 + reuse applet instances from the parent context (when available). Mostly exist in order to 85 + construct sensible defaults (eg. applet connections you want to reuse across themes).</em 81 86 > 82 87 </p> 83 88 ··· 128 133 that takes the same actions and data output.</em 129 134 > 130 135 </p> 136 + 137 + <ul> 138 + { 139 + configurators.map((item: any) => ( 140 + <li> 141 + <a href={item.url}>{item.title}</a> 142 + </li> 143 + )) 144 + } 145 + </ul> 131 146 132 147 <h3>Supplements</h3> 133 148
+9 -41
src/scripts/themes/pilot/index.ts
··· 1 + import type { Output } from "../../../applets/core/types.d.ts"; 1 2 import { applet, comparable, reactive } from "../../theme.ts"; 2 3 3 4 //////////////////////////////////////////// ··· 8 9 //////////////////////////////////////////// 9 10 // 🗂️ Applets 10 11 //////////////////////////////////////////// 11 - import type { Output, Track } from "../../../applets/core/types.d.ts"; 12 - 13 12 import type * as AudioEngine from "../../../applets/engine/audio/types.d.ts"; 14 13 import type * as QueueEngine from "../../../applets/engine/queue/types.d.ts"; 15 14 16 15 import type * as AudioUI from "../../../applets/themes/pilot/ui/audio/types.ts"; 17 16 17 + const configurator = { 18 + storage: { 19 + output: await applet("../../configurator/storage/output"), 20 + }, 21 + }; 22 + 18 23 const engine = { 19 24 audio: await applet<AudioEngine.State>("../../engine/audio"), 20 25 queue: await applet<QueueEngine.State>("../../engine/queue"), ··· 22 27 23 28 const orchestrator = { 24 29 queue: await applet("../../orchestrator/queue"), 25 - }; 26 - 27 - const storage = { 28 - output: { 29 - memory: await applet<Output>("../../storage/output/memory"), 30 - }, 30 + storage: await applet<Output>("../../orchestrator/storage"), 31 31 }; 32 32 33 33 const ui = { ··· 64 64 ); 65 65 66 66 //////////////////////////////////////////// 67 - // 📦 [Connections → Storages/Output] 68 - // 🧠 Memory 69 - //////////////////////////////////////////// 70 - 71 - // Track changes to in-memory user data, 72 - // reflect to local & remote data stores. 73 - // TODO: Make configurator applet. 74 - // Move to orchestrator? 75 - 76 - reactive( 77 - storage.output.memory, 78 - (data) => comparable(data), 79 - (data) => { 80 - // TODO: Store locally 81 - // TODO: Store remotely 82 - // 83 - // NOTE: 84 - // Make data object an automerge document 85 - // and compact on every page load? 86 - // We should be able to track what property 87 - // of the data has changed. 88 - }, 89 - ); 90 - 91 - // NOTE: How do we sync new remote data with our local/in-memory data? 92 - // 93 - // storage.output.memory.data = { 94 - // sources: [], 95 - // tracks: [] 96 - // } 97 - 98 - //////////////////////////////////////////// 99 67 // 🌅 [Connections → UI] 100 68 // 🔉 AUDIO 101 69 //////////////////////////////////////////// ··· 109 77 110 78 // Automatically start playing something if nothing is playing yet. 111 79 if (!trackId) { 112 - if (isPlaying) engine.queue.sendAction("shift", null); 80 + if (isPlaying) engine.queue.sendAction("shift"); 113 81 return; 114 82 } 115 83
+3
src/styles/configurator.css
··· 1 + @import "@picocss/pico/css/pico.css"; 2 + @import "@picocss/pico/css/pico.colors.css"; 3 + @import "./icons.css";
+1
src/styles/icons.css
··· 1 + @import "iconoir/css/iconoir.css";