Experiment to rebuild Diffuse using web applets.
0
fork

Configure Feed

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

feat: Initial output storages and improved storage configurator

+524 -108
+1 -1
deno.lock
··· 26 26 "npm:astro@^5.7.4", 27 27 "npm:iconoir@^7.11.0", 28 28 "npm:idb-keyval@^6.2.1", 29 - "npm:spellcaster@^5.0.2", 29 + "npm:native-file-system-adapter@^3.0.1", 30 30 "npm:throttle-debounce@^5.0.2", 31 31 "npm:xxh32@^2.0.5" 32 32 ]
+80 -7
package-lock.json
··· 9 9 "@web-applets/sdk": "file:../../unternet-co/web-applets/sdk/", 10 10 "iconoir": "^7.11.0", 11 11 "idb-keyval": "^6.2.1", 12 - "spellcaster": "^5.0.2", 12 + "native-file-system-adapter": "^3.0.1", 13 + "spellcaster": "icidasset/spellcaster#fix/hyper-data", 13 14 "throttle-debounce": "^5.0.2", 14 15 "xxh32": "^2.0.5" 15 16 }, ··· 2184 2185 } 2185 2186 } 2186 2187 }, 2188 + "node_modules/fetch-blob": { 2189 + "version": "3.2.0", 2190 + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", 2191 + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", 2192 + "funding": [ 2193 + { 2194 + "type": "github", 2195 + "url": "https://github.com/sponsors/jimmywarting" 2196 + }, 2197 + { 2198 + "type": "paypal", 2199 + "url": "https://paypal.me/jimmywarting" 2200 + } 2201 + ], 2202 + "optional": true, 2203 + "dependencies": { 2204 + "node-domexception": "^1.0.0", 2205 + "web-streams-polyfill": "^3.0.3" 2206 + }, 2207 + "engines": { 2208 + "node": "^12.20 || >= 14.13" 2209 + } 2210 + }, 2187 2211 "node_modules/flattie": { 2188 2212 "version": "1.1.1", 2189 2213 "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", ··· 3471 3495 "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 3472 3496 } 3473 3497 }, 3498 + "node_modules/native-file-system-adapter": { 3499 + "version": "3.0.1", 3500 + "resolved": "https://registry.npmjs.org/native-file-system-adapter/-/native-file-system-adapter-3.0.1.tgz", 3501 + "integrity": "sha512-ocuhsYk2SY0906LPc3QIMW+rCV3MdhqGiy7wV5Bf0e8/5TsMjDdyIwhNiVPiKxzTJLDrLT6h8BoV9ERfJscKhw==", 3502 + "funding": [ 3503 + { 3504 + "type": "github", 3505 + "url": "https://github.com/sponsors/jimmywarting" 3506 + }, 3507 + { 3508 + "type": "paypal", 3509 + "url": "https://paypal.me/jimmywarting" 3510 + } 3511 + ], 3512 + "engines": { 3513 + "node": ">=14.8.0" 3514 + }, 3515 + "optionalDependencies": { 3516 + "fetch-blob": "^3.2.0" 3517 + } 3518 + }, 3474 3519 "node_modules/neotraverse": { 3475 3520 "version": "0.6.18", 3476 3521 "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", ··· 3493 3538 "url": "https://opencollective.com/unified" 3494 3539 } 3495 3540 }, 3541 + "node_modules/node-domexception": { 3542 + "version": "1.0.0", 3543 + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 3544 + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", 3545 + "deprecated": "Use your platform's native DOMException instead", 3546 + "funding": [ 3547 + { 3548 + "type": "github", 3549 + "url": "https://github.com/sponsors/jimmywarting" 3550 + }, 3551 + { 3552 + "type": "github", 3553 + "url": "https://paypal.me/jimmywarting" 3554 + } 3555 + ], 3556 + "optional": true, 3557 + "engines": { 3558 + "node": ">=10.5.0" 3559 + } 3560 + }, 3496 3561 "node_modules/node-fetch": { 3497 3562 "version": "2.7.0", 3498 3563 "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", ··· 4101 4166 } 4102 4167 }, 4103 4168 "node_modules/signal-polyfill": { 4104 - "version": "0.1.2", 4105 - "resolved": "https://registry.npmjs.org/signal-polyfill/-/signal-polyfill-0.1.2.tgz", 4106 - "integrity": "sha512-HT9d+L9NMiTzMxb/tU2Baym6129ROyRETSjvchvSkQa7wN0+SrG/IUlsaBLqKn2c+4mlze6CgQBEvgBjxOpiaQ==" 4169 + "version": "0.2.2", 4170 + "resolved": "https://registry.npmjs.org/signal-polyfill/-/signal-polyfill-0.2.2.tgz", 4171 + "integrity": "sha512-p63Y4Er5/eMQ9RHg0M0Y64NlsQKpiu6MDdhBXpyywRuWiPywhJTpKJ1iB5K2hJEbFZ0BnDS7ZkJ+0AfTuL37Rg==" 4107 4172 }, 4108 4173 "node_modules/simple-swizzle": { 4109 4174 "version": "0.2.2", ··· 4154 4219 }, 4155 4220 "node_modules/spellcaster": { 4156 4221 "version": "5.0.2", 4157 - "resolved": "https://registry.npmjs.org/spellcaster/-/spellcaster-5.0.2.tgz", 4158 - "integrity": "sha512-suCHnQlCyXAV1OCrL0jEjE8lCK+f2bfmnvBfIqkG6Q3fNiQ7mAaeXtSyEKCI5p2ifSieC5bS/59EcIfDh5PWMA==", 4222 + "resolved": "git+ssh://git@github.com/icidasset/spellcaster.git#85038476a872463bb8ef4687418cecb1dd4807f1", 4159 4223 "dependencies": { 4160 - "signal-polyfill": "^0.1.0" 4224 + "signal-polyfill": "^0.2.0" 4161 4225 } 4162 4226 }, 4163 4227 "node_modules/string-width": { ··· 4744 4808 "funding": { 4745 4809 "type": "github", 4746 4810 "url": "https://github.com/sponsors/wooorm" 4811 + } 4812 + }, 4813 + "node_modules/web-streams-polyfill": { 4814 + "version": "3.3.3", 4815 + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", 4816 + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", 4817 + "optional": true, 4818 + "engines": { 4819 + "node": ">= 8" 4747 4820 } 4748 4821 }, 4749 4822 "node_modules/webidl-conversions": {
+2 -1
package.json
··· 4 4 "@web-applets/sdk": "file:../../unternet-co/web-applets/sdk/", 5 5 "iconoir": "^7.11.0", 6 6 "idb-keyval": "^6.2.1", 7 - "spellcaster": "^5.0.2", 7 + "native-file-system-adapter": "^3.0.1", 8 + "spellcaster": "icidasset/spellcaster#fix/hyper-data", 8 9 "throttle-debounce": "^5.0.2", 9 10 "xxh32": "^2.0.5" 10 11 },
+179 -69
src/applets/configurator/storage/output/applet.astro
··· 1 1 <main class="container"> 2 2 <h1>Storage configuration</h1> 3 - <p>Here you can select one or more storages (applets) to keep your user data on.</p> 3 + <p> 4 + Here you can select where you want to keep your user data. 5 + <br /> 6 + By default this will be your browser. 7 + <br /> 8 + Click or tap on one to activate it. 9 + </p> 4 10 <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> 11 + <span class="with-icon"> 12 + <i class="iconoir-bonfire"></i> 13 + <small>Just a moment, loading storage options.</small> 14 + </span> 44 15 </div> 16 + <div id="iframes"></div> 45 17 </main> 46 18 47 19 <style> 48 20 @import "../../../../styles/configurator.css"; 49 21 22 + #iframes { 23 + display: none; 24 + } 25 + 50 26 .with-icon { 51 27 align-items: center; 52 28 display: inline-flex; 53 - gap: 6px; 29 + gap: 10px; 30 + } 31 + 32 + [data-storage] { 33 + cursor: pointer; 54 34 } 55 35 </style> 56 36 57 37 <script> 38 + // @ts-ignore 39 + import scope from "astro:scope"; 58 40 import * as IDB from "idb-keyval"; 41 + import { type Signal, computed, effect, signal } from "spellcaster/spellcaster.js"; 42 + import { repeat, text } from "spellcaster/hyperscript.js"; 59 43 import { applets } from "@web-applets/sdk"; 60 44 61 - type Getter = ({ name }: { name: string }) => Promise<Uint8Array | undefined>; 62 - type Setter = ({ data, name }: { data: Uint8Array; name: string }) => Promise<void>; 45 + import { applet, hs } from "../../../../scripts/theme"; 46 + import { OutputGetter, OutputSetter } from "../../../core/types"; 47 + 48 + const METHODS = ["browser", "device"] as const; 49 + 50 + type Method = (typeof METHODS)[number]; 51 + type List<M extends Method = Method> = Map<string, ListItem<M>>; 52 + type ListItem<M> = { activated: boolean; icon: string; method: M; title: string }; 53 + 54 + const DEFAULT_METHOD: Method = "browser"; 55 + const LOCALSTORAGE_KEY = "applets/configurator/storage/output/active-storage"; 63 56 64 57 //////////////////////////////////////////// 65 58 // SETUP 66 59 //////////////////////////////////////////// 67 60 const context = applets.register(); 61 + const container = document.getElementById("iframes"); 62 + if (!container) throw new Error("Missing iframe container"); 68 63 69 64 // TODO: Should migrate + merge data when switching storages 70 65 66 + // Applet connections 67 + const storage = { 68 + output: { 69 + indexedDB: await applet("../../../storage/output/indexed-db/", { container }), 70 + nativeFs: await applet("../../../storage/output/native-fs/", { container }), 71 + }, 72 + }; 73 + 71 74 // Initial state 72 75 context.data = undefined; 73 76 77 + // Signals 78 + const stored = localStorage.getItem(LOCALSTORAGE_KEY); 79 + const [active, setActive] = signal<Method>( 80 + stored && METHODS.includes(stored as Method) ? (stored as Method) : DEFAULT_METHOD, 81 + ); 82 + 83 + effect(() => { 84 + localStorage.setItem(LOCALSTORAGE_KEY, active()); 85 + }); 86 + 87 + // Mount + Unmount 88 + async function mountStorageMethod(method: Method) { 89 + switch (method) { 90 + case "browser": 91 + await storage.output.indexedDB.sendAction("mount"); 92 + break; 93 + case "device": 94 + await storage.output.nativeFs.sendAction("mount"); 95 + break; 96 + } 97 + } 98 + 99 + async function unmountStorageMethod(method: Method) { 100 + switch (method) { 101 + case "browser": 102 + await storage.output.indexedDB.sendAction("unmount"); 103 + break; 104 + case "device": 105 + await storage.output.nativeFs.sendAction("unmount"); 106 + break; 107 + } 108 + } 109 + 74 110 //////////////////////////////////////////// 75 - // ACTIONS 111 + // UI 76 112 //////////////////////////////////////////// 77 - context.setActionHandler("get", get); 78 - context.setActionHandler("put", put); 113 + const list = computed<List>(() => { 114 + const a = active(); 115 + 116 + return new Map([ 117 + [ 118 + `browser-${a === "browser"}`, 119 + { 120 + title: "Browser storage", 121 + icon: "iconoir-app-window", 122 + method: "browser", 123 + activated: a === "browser", 124 + }, 125 + ], 126 + [ 127 + `device-${a === "device"}`, 128 + { 129 + title: "Device storage", 130 + icon: "iconoir-laptop", 131 + method: "device", 132 + activated: a === "device", 133 + }, 134 + ], 135 + ]); 136 + }); 137 + 138 + const Item = (signal: Signal<ListItem<Method>>) => { 139 + const item = signal(); 79 140 80 - function selectedStorage() { 81 - return localStorage.getItem("storage") || "indexeddb"; 141 + return hs( 142 + "p", 143 + scope, 144 + { 145 + onclick: clickHandler(item.method), 146 + style: "cursor: pointer", 147 + }, 148 + [ 149 + hs("span", scope, { className: "with-icon" }, [ 150 + hs("i", scope, { className: item.icon }), 151 + hs("strong", scope, {}, text(item.title)), 152 + ]), 153 + hs("br", scope), 154 + hs( 155 + "span", 156 + scope, 157 + { 158 + className: 159 + "with-icon " + (item.activated ? "pico-color-jade-500" : "pico-color-grey-500"), 160 + }, 161 + [ 162 + hs("i", scope, { 163 + className: item.activated ? "iconoir-check-circle-solid" : "iconoir-xmark-circle", 164 + }), 165 + hs("span", scope, {}, text(item.activated ? "Activated" : "Deactivated")), 166 + ], 167 + ), 168 + ], 169 + ); 170 + }; 171 + 172 + const Options = computed(() => hs("div", scope, { id: "options" }, repeat(list, Item))); 173 + document.getElementById("options")?.replaceWith(Options()); 174 + 175 + function clickHandler(method: Method) { 176 + return async () => { 177 + const currentlyActive = active(); 178 + if (currentlyActive === method) return; 179 + if (currentlyActive) unmountStorageMethod(currentlyActive); 180 + 181 + await mountStorageMethod(method); 182 + setActive(method); 183 + }; 82 184 } 83 185 84 - async function get(args: Parameters<Getter>[0]) { 85 - let data; 186 + //////////////////////////////////////////// 187 + // ACTIONS 188 + //////////////////////////////////////////// 189 + const get: OutputGetter = async (args) => { 190 + let data: Uint8Array | undefined; 86 191 87 - switch (selectedStorage()) { 88 - case "indexeddb": { 89 - data = await idbGet(args); 192 + switch (active()) { 193 + case "browser": { 194 + // TODO 195 + await storage.output.indexedDB.sendAction("get", args); 196 + data = storage.output.indexedDB.data as Uint8Array | undefined; 197 + break; 198 + } 199 + case "device": { 200 + // TODO 201 + await storage.output.nativeFs.sendAction("get", args); 202 + data = storage.output.nativeFs.data as Uint8Array | undefined; 90 203 break; 91 204 } 92 205 } ··· 96 209 } else { 97 210 context.data = undefined; 98 211 } 99 - } 100 212 101 - async function put(args: Parameters<Setter>[0]) { 102 - switch (selectedStorage()) { 103 - case "indexeddb": 104 - return await idbPut(args); 213 + return data; 214 + }; 215 + 216 + const put: OutputSetter = async (args) => { 217 + switch (active()) { 218 + case "browser": 219 + await storage.output.indexedDB.sendAction("put", args); 220 + break; 221 + case "device": 222 + await storage.output.nativeFs.sendAction("put", args); 223 + break; 105 224 } 106 - } 107 - 108 - //////////////////////////////////////////// 109 - // [Storages] 110 - // INDEXED DB 111 - //////////////////////////////////////////// 112 - const idbGet: Getter = async ({ name }) => { 113 - return await IDB.get(name); 114 225 }; 115 226 116 - const idbPut: Setter = async ({ data, name }) => { 117 - return await IDB.set(name, data); 118 - }; 227 + context.setActionHandler("get", get); 228 + context.setActionHandler("put", put); 119 229 </script>
+4 -2
src/applets/configurator/storage/output/manifest.json
··· 10 10 "type": "object", 11 11 "properties": { 12 12 "name": { "type": "string" } 13 - } 13 + }, 14 + "required": ["name"] 14 15 } 15 16 }, 16 17 "put": { ··· 21 22 "properties": { 22 23 "data": { "type": "object" }, 23 24 "name": { "type": "string" } 24 - } 25 + }, 26 + "required": ["data", "name"] 25 27 } 26 28 } 27 29 }
+6
src/applets/core/types.d.ts
··· 1 + /* OUTPUT */ 2 + 1 3 export interface Output<T = TrackTags> { 2 4 sources: Source[]; 3 5 tracks: Track<T>[]; 4 6 } 5 7 8 + export type OutputGetter = ({ name }: { name: string }) => Promise<Uint8Array | undefined>; 9 + export type OutputSetter = ({ data, name }: { data: Uint8Array; name: string }) => Promise<void>; 10 + 6 11 /* SOURCES */ 12 + // TODO: Rename to input? 7 13 8 14 export interface Source<Meta = Record<string, string>> { 9 15 id: string;
+17 -7
src/applets/orchestrator/storage/applet.astro
··· 2 2 import { applets } from "@web-applets/sdk"; 3 3 4 4 import type { Output, Source, Track } from "../../core/types.d.ts"; 5 - import { applet, reactive } from "../../../scripts/theme"; 5 + import { applet } from "../../../scripts/theme"; 6 6 7 7 //////////////////////////////////////////// 8 8 // SETUP ··· 56 56 async function loadSources(): Promise<Source[]> { 57 57 // TODO: This is not concurrency safe! 58 58 await configurator.storage.output.sendAction("get", { 59 - name: "sources", 59 + name: "sources.json", 60 60 }); 61 61 62 62 const data = configurator.storage.output.data; 63 - if (!data) return [SAMPLE_SOURCE]; 63 + 64 + if (!data) { 65 + saveSources([SAMPLE_SOURCE]); 66 + return [SAMPLE_SOURCE]; 67 + } 68 + 64 69 return decode(data as Uint8Array); 65 70 } 66 71 67 72 async function loadTracks(): Promise<Track[]> { 68 73 // TODO: This is not concurrency safe! 69 74 await configurator.storage.output.sendAction("get", { 70 - name: "tracks", 75 + name: "tracks.json", 71 76 }); 72 77 73 78 const data = configurator.storage.output.data; 74 - if (!data) return SAMPLE_TRACKS; 79 + 80 + if (!data) { 81 + saveTracks(SAMPLE_TRACKS); 82 + return SAMPLE_TRACKS; 83 + } 84 + 75 85 return decode(data as Uint8Array); 76 86 } 77 87 ··· 86 96 const data = encode(sources); 87 97 88 98 configurator.storage.output.sendAction("put", { 89 - name: "sources", 99 + name: "sources.json", 90 100 data, 91 101 }); 92 102 } ··· 95 105 const data = encode(tracks); 96 106 97 107 configurator.storage.output.sendAction("put", { 98 - name: "tracks", 108 + name: "tracks.json", 99 109 data, 100 110 }); 101 111 }
+32
src/applets/storage/output/indexed-db/applet.astro
··· 1 + <script> 2 + import * as IDB from "idb-keyval"; 3 + import { applets } from "@web-applets/sdk"; 4 + 5 + import type { OutputGetter, OutputSetter } from "../../../core/types.d.ts"; 6 + 7 + //////////////////////////////////////////// 8 + // SETUP 9 + //////////////////////////////////////////// 10 + const IDB_PREFIX = "@applets/storage/output/indexed-db"; 11 + const context = applets.register(); 12 + 13 + //////////////////////////////////////////// 14 + // ACTIONS 15 + //////////////////////////////////////////// 16 + const get: OutputGetter = async ({ name }) => { 17 + return await IDB.get(`${IDB_PREFIX}/${name}`); 18 + }; 19 + 20 + const put: OutputSetter = async ({ data, name }) => { 21 + return await IDB.set(`${IDB_PREFIX}/${name}`, data); 22 + }; 23 + 24 + const mount = async () => {}; 25 + 26 + const unmount = async () => {}; 27 + 28 + context.setActionHandler("get", get); 29 + context.setActionHandler("put", put); 30 + context.setActionHandler("mount", mount); 31 + context.setActionHandler("unmount", unmount); 32 + </script>
+38
src/applets/storage/output/indexed-db/manifest.json
··· 1 + { 2 + "name": "diffuse/storage/output/indexed-db", 3 + "title": "Diffuse Storage | Output | IndexedDB", 4 + "entrypoint": "index.html", 5 + "actions": { 6 + "get": { 7 + "title": "Get", 8 + "description": "Retrieve data.", 9 + "params_schema": { 10 + "type": "object", 11 + "properties": { 12 + "name": { "type": "string" } 13 + }, 14 + "required": ["name"] 15 + } 16 + }, 17 + "put": { 18 + "title": "Put", 19 + "description": "Store data.", 20 + "params_schema": { 21 + "type": "object", 22 + "properties": { 23 + "data": { "type": "object" }, 24 + "name": { "type": "string" } 25 + }, 26 + "required": ["data", "name"] 27 + } 28 + }, 29 + "mount": { 30 + "title": "Mount", 31 + "description": "Prepare for usage." 32 + }, 33 + "unmount": { 34 + "title": "Unmount", 35 + "description": "Callback after usage." 36 + } 37 + } 38 + }
+64
src/applets/storage/output/native-fs/applet.astro
··· 1 + <script> 2 + import * as IDB from "idb-keyval"; 3 + import { applets } from "@web-applets/sdk"; 4 + import { type FileSystemDirectoryHandle, showDirectoryPicker } from "native-file-system-adapter"; 5 + 6 + import type { OutputGetter, OutputSetter } from "../../../core/types.d.ts"; 7 + 8 + //////////////////////////////////////////// 9 + // SETUP 10 + //////////////////////////////////////////// 11 + const IDB_PREFIX = "@applets/storage/output/native-fs"; 12 + const IDB_DEVICE_KEY = `${IDB_PREFIX}/device`; 13 + 14 + const context = applets.register(); 15 + 16 + //////////////////////////////////////////// 17 + // ACTIONS 18 + //////////////////////////////////////////// 19 + const get: OutputGetter = async ({ name }) => { 20 + const handle: FileSystemDirectoryHandle | null = await IDB.get(IDB_DEVICE_KEY); 21 + if (!handle) throw new Error("Storage not configured properly, handle not found."); 22 + 23 + try { 24 + const fileHandle = await handle.getFileHandle(name); 25 + const file = await fileHandle.getFile(); 26 + const data = await file.bytes(); 27 + // TODO: Should be able to just return the data in the handler 28 + context.data = data; 29 + return data; 30 + } catch (err) { 31 + context.data = undefined; 32 + return undefined; 33 + } 34 + }; 35 + 36 + const put: OutputSetter = async ({ data, name }) => { 37 + const handle: FileSystemDirectoryHandle | null = await IDB.get(IDB_DEVICE_KEY); 38 + if (!handle) throw new Error("Storage not configured properly, handle not found."); 39 + const fileHandle = await handle.getFileHandle(name, { create: true }); 40 + const stream = await fileHandle.createWritable(); 41 + await stream.write(data); 42 + await stream.close(); 43 + }; 44 + 45 + const mount = async () => { 46 + const existingHandle = await IDB.get(IDB_DEVICE_KEY); 47 + if (!existingHandle) { 48 + const directoryHandle = await showDirectoryPicker(); 49 + await IDB.set(IDB_DEVICE_KEY, directoryHandle); 50 + await directoryHandle.requestPermission({ mode: "readwrite" }); 51 + } 52 + }; 53 + 54 + const unmount = async () => { 55 + try { 56 + await IDB.del(IDB_DEVICE_KEY); 57 + } catch (err) {} 58 + }; 59 + 60 + context.setActionHandler("get", get); 61 + context.setActionHandler("put", put); 62 + context.setActionHandler("mount", mount); 63 + context.setActionHandler("unmount", unmount); 64 + </script>
+38
src/applets/storage/output/native-fs/manifest.json
··· 1 + { 2 + "name": "diffuse/storage/output/native-fs", 3 + "title": "Diffuse Storage | Output | Native File System", 4 + "entrypoint": "index.html", 5 + "actions": { 6 + "get": { 7 + "title": "Get", 8 + "description": "Retrieve data.", 9 + "params_schema": { 10 + "type": "object", 11 + "properties": { 12 + "name": { "type": "string" } 13 + }, 14 + "required": ["name"] 15 + } 16 + }, 17 + "put": { 18 + "title": "Put", 19 + "description": "Store data.", 20 + "params_schema": { 21 + "type": "object", 22 + "properties": { 23 + "data": { "type": "object" }, 24 + "name": { "type": "string" } 25 + }, 26 + "required": ["data", "name"] 27 + } 28 + }, 29 + "mount": { 30 + "title": "Mount", 31 + "description": "Prepare for usage." 32 + }, 33 + "unmount": { 34 + "title": "Unmount", 35 + "description": "Callback after usage." 36 + } 37 + } 38 + }
+34 -19
src/pages/index.astro
··· 16 16 { url: "orchestrator/storage/", title: "Storage" }, 17 17 ]; 18 18 19 - const storages = []; 19 + const storages = [ 20 + { url: "storage/output/indexed-db", title: "Output / IndexedDB" }, 21 + { url: "storage/output/native-fs", title: "Output / Native File System" }, 22 + ]; 23 + 24 + const processors = []; 20 25 21 26 const themes = [{ url: "themes/pilot/", title: "Pilot" }]; 22 27 --- ··· 56 61 57 62 <section> 58 63 <h2>Applets</h2> 64 + 65 + <h3>Configurators</h3> 66 + 67 + <p> 68 + <em 69 + >Applets that serve as an intermediate in order to make a particular kind of applet 70 + configurable. In other words, these allow for an applet to be swapped out with another 71 + that takes the same actions and data output.</em 72 + > 73 + </p> 74 + 75 + <ul> 76 + { 77 + configurators.map((item: any) => ( 78 + <li> 79 + <a href={item.url}>{item.title}</a> 80 + </li> 81 + )) 82 + } 83 + </ul> 59 84 60 85 <h3>Engines</h3> 61 86 ··· 96 121 } 97 122 </ul> 98 123 99 - <h3>Storages</h3> 124 + <h3>Processors</h3> 100 125 101 126 <p> 102 127 <em 103 - >Input and output managers of the system. Where input is audio files or streams, and 104 - output is derived data such as a music playlist.</em 128 + >These applets interact with the bytes provided by the data storage applets, or provide 129 + to. This processed data can then be passed on to the UI layer and engine applets.</em 105 130 > 106 131 </p> 107 132 108 133 <ul> 109 134 { 110 - storages.map((item: any) => ( 135 + processors.map((item: any) => ( 111 136 <li> 112 137 <a href={item.url}>{item.title}</a> 113 138 </li> ··· 115 140 } 116 141 </ul> 117 142 118 - <h3>Processors</h3> 119 - 120 - <p> 121 - <em 122 - >These applets interact with the bytes provided by the data storage applets, or provide 123 - to. This processed data can then be passed on to the UI layer and engine applets.</em 124 - > 125 - </p> 126 - 127 - <h3>Configurators</h3> 143 + <h3>Storages</h3> 128 144 129 145 <p> 130 146 <em 131 - >Applets that serve as an intermediate in order to make a particular kind of applet 132 - configurable. In other words, these allow for an applet to be swapped out with another 133 - that takes the same actions and data output.</em 147 + >Input and output managers of the system. Where input is audio files or streams, and 148 + output is derived data such as a music playlist.</em 134 149 > 135 150 </p> 136 151 137 152 <ul> 138 153 { 139 - configurators.map((item: any) => ( 154 + storages.map((item: any) => ( 140 155 <li> 141 156 <a href={item.url}>{item.title}</a> 142 157 </li>
+29 -2
src/scripts/theme.ts
··· 1 1 import type { Applet, AppletEvent } from "@web-applets/sdk"; 2 2 3 3 import { applets } from "@web-applets/sdk"; 4 - import { effect, signal } from "spellcaster/spellcaster.js"; 4 + import { ElementConfigurator, h } from "spellcaster/hyperscript.js"; 5 + import { effect, isSignal, sample, Signal, signal } from "spellcaster/spellcaster.js"; 5 6 import { xxh32 } from "xxh32"; 6 7 7 8 //////////////////////////////////////////// ··· 12 13 opts: { 13 14 addSlashSuffix?: boolean; 14 15 context?: Window; 16 + container?: HTMLElement; 15 17 id?: string; 16 18 setHeight?: boolean; 17 19 } = {}, ··· 37 39 frame.src = src; 38 40 if (opts.id) frame.id = opts.id; 39 41 40 - (opts.context || window).document.body.appendChild(frame); 42 + if (opts.container) { 43 + opts.container.appendChild(frame); 44 + } else { 45 + (opts.context || window).document.body.appendChild(frame); 46 + } 41 47 } 42 48 43 49 if (frame.contentWindow === null) { ··· 84 90 //////////////////////////////////////////// 85 91 // 🛠️ 86 92 //////////////////////////////////////////// 93 + export function addScope<O extends Object>(astroScope: string, object: O): O { 94 + return { 95 + ...object, 96 + [`data-astro-cid-${astroScope}`]: "", 97 + }; 98 + } 99 + 87 100 export function comparable(value: unknown) { 88 101 return xxh32(JSON.stringify(value)); 102 + } 103 + 104 + export function hs( 105 + tag: string, 106 + astroScope: string, 107 + props?: Record<string, unknown> | Signal<Record<string, unknown>>, 108 + configure?: ElementConfigurator, 109 + ) { 110 + const propsWithScope = 111 + props && isSignal(props) 112 + ? () => addScope(astroScope, props()) 113 + : addScope(astroScope, props || {}); 114 + 115 + return h(tag, propsWithScope, configure); 89 116 } 90 117 91 118 export function isPrimitive(test: unknown) {