Experiment to rebuild Diffuse using web applets.
fork

Configure Feed

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

feat: input configurator

+244 -54
public/images/icons/windows_98/cd_audio_cd_a-4.png

This is a binary file and will not be displayed.

+106
src/pages/configurator/input/_applet.astro
··· 1 + <main class="container"> 2 + <h1>Input configuration</h1> 3 + <p> 4 + Here you can add your audio from various places. 5 + <br />Add audio from: 6 + </p> 7 + <div id="options"> 8 + <p> 9 + <a href="../../input/native-fs/" class="with-icon"> 10 + <i class="iconoir-open-in-window"></i> 11 + <strong>My device</strong> 12 + </a> 13 + </p> 14 + </div> 15 + <p> 16 + <small 17 + ><em><strong>More options coming soon!</strong><br />S3-compatible APIs, Dropbox, etc.</em 18 + ></small 19 + > 20 + </p> 21 + </main> 22 + 23 + <div id="iframes"></div> 24 + 25 + <style> 26 + #iframes { 27 + display: none; 28 + } 29 + </style> 30 + 31 + <script> 32 + import { applets } from "@web-applets/sdk"; 33 + 34 + import type { Output, Track } from "@applets/core/types.d.ts"; 35 + import { applet } from "@scripts/theme"; 36 + 37 + //////////////////////////////////////////// 38 + // SETUP 39 + //////////////////////////////////////////// 40 + const container = document.querySelector("#iframes") || undefined; 41 + 42 + // Applet connections 43 + const input = { 44 + nativeFs: await applet("../../input/native-fs", { container }), 45 + }; 46 + 47 + // Register applet 48 + const context = applets.register<Output>(); 49 + 50 + //////////////////////////////////////////// 51 + // ACTIONS 52 + //////////////////////////////////////////// 53 + 54 + const list = async (cachedTracks: Track[] = []) => { 55 + const groups = cachedTracks.reduce( 56 + (acc: Record<string, Track[]>, track: Track) => { 57 + const scheme = track.uri.split(":", 1)[0]; 58 + return { ...acc, [scheme]: [...(acc.scheme || []), track] }; 59 + }, 60 + { 61 + [input.nativeFs.manifest.input_properties.scheme]: [], 62 + }, 63 + ); 64 + 65 + const promises = Object.entries(groups).map( 66 + async ([scheme, cachedTracks]: [string, Track[]]) => { 67 + switch (scheme) { 68 + case input.nativeFs.manifest.input_properties.scheme: 69 + await input.nativeFs.sendAction("list", cachedTracks); 70 + return input.nativeFs.data; 71 + 72 + default: 73 + return cachedTracks; 74 + } 75 + }, 76 + ); 77 + 78 + const nested = await Promise.all(promises); 79 + const tracks = nested.flat(1); 80 + 81 + // TODO: Ideally promise returns data 82 + context.data = tracks as any; 83 + 84 + return tracks; 85 + }; 86 + 87 + const resolve = async (fileUri: string) => { 88 + const scheme = fileUri.split(":", 1)[0]; 89 + 90 + switch (scheme) { 91 + case input.nativeFs.manifest.input_properties.scheme: 92 + await input.nativeFs.sendAction("resolve", fileUri); 93 + // TODO: Ideally promise returns data 94 + context.data = input.nativeFs.data as any; 95 + return input.nativeFs.data; 96 + 97 + default: 98 + // TODO: Ideally promise returns data 99 + context.data = undefined as any; 100 + return undefined; 101 + } 102 + }; 103 + 104 + context.setActionHandler("list", list); 105 + context.setActionHandler("resolve", resolve); 106 + </script>
+31
src/pages/configurator/input/_manifest.json
··· 1 + { 2 + "name": "diffuse/configurator/input", 3 + "title": "Diffuse Configurator | Input", 4 + "entrypoint": "index.html", 5 + "actions": { 6 + "list": { 7 + "title": "List", 8 + "description": "List tracks from all inputs.", 9 + "params_schema": { 10 + "type": "object", 11 + "properties": { 12 + "tracks": { 13 + "type": "array", 14 + "description": "A list of (cached) tracks", 15 + "items": { 16 + "type": "object" 17 + } 18 + } 19 + } 20 + } 21 + }, 22 + "resolve": { 23 + "title": "Resolve", 24 + "description": "Potentially translates a track uri with a matching scheme into a URL pointing at the audio bytes. If it can be resolved that is, otherwise you'll get `undefined`.", 25 + "params_schema": { 26 + "type": "string", 27 + "description": "The uri to resolve" 28 + } 29 + } 30 + } 31 + }
+9
src/pages/configurator/input/index.astro
··· 1 + --- 2 + import Layout from "@layouts/applet-pico-ui.astro"; 3 + import Applet from "./_applet.astro"; 4 + import { title } from "./_manifest.json"; 5 + --- 6 + 7 + <Layout title={title}> 8 + <Applet /> 9 + </Layout>
+5 -2
src/pages/index.astro
··· 24 24 // TODO 25 25 26 26 // Applets 27 - const configurators = [{ url: "configurator/output/", title: "Output" }]; 27 + const configurators = [ 28 + { url: "configurator/input/", title: "Input" }, 29 + { url: "configurator/output/", title: "Output" }, 30 + ]; 28 31 29 32 const engines = [ 30 33 { url: "engine/audio/", title: "Audio" }, ··· 121 124 <Applet title="Configurators" list={configurators}> 122 125 Applets that serve as an intermediate in order to make a particular kind of applet 123 126 configurable. In other words, these allow for an applet to be swapped out with another 124 - that takes the same actions and data output. 127 + that takes the same, or a subset of the actions and data output. 125 128 </Applet> 126 129 127 130 <Applet title="Engines" list={engines}>
+34 -15
src/pages/input/native-fs/_applet.astro
··· 1 1 <main class="container"> 2 - <h1>Native File System Input</h1> 3 - <p>Add music from your device.</p> 2 + <h1>Native file system input</h1> 3 + <p> 4 + Add music from your device. 5 + <br />Music added so far: 6 + </p> 4 7 <div id="directories"> 5 8 <p> 6 9 <span class="with-icon"> ··· 16 19 import * as IDB from "idb-keyval"; 17 20 18 21 import { applets } from "@web-applets/sdk"; 19 - import { computed, Signal, signal } from "spellcaster"; 22 + import { computed, effect, Signal, signal } from "spellcaster"; 20 23 import { repeat, tags, text } from "spellcaster/hyperscript.js"; 21 24 import { type FileSystemDirectoryHandle, showDirectoryPicker } from "native-file-system-adapter"; 22 25 import * as URI from "uri-js"; ··· 36 39 //////////////////////////////////////////// 37 40 const IDB_PREFIX = "@applets/input/native-fs"; 38 41 const IDB_HANDLES = `${IDB_PREFIX}/handles`; 39 - const INPUT_SCHEME = manifest.input_properties.scheme; 42 + const SCHEME = manifest.input_properties.scheme; 40 43 41 44 const context = applets.register(); 42 45 ··· 69 72 }; 70 73 71 74 const Directories = computed(() => { 75 + if (mounts().length === 0) { 76 + return tags.p({ id: "directories" }, [ 77 + tags.small({}, [ 78 + tags.em({}, text("No audio added yet, click the button below to add some.")), 79 + ]), 80 + ]); 81 + } 82 + 72 83 return tags.ul({ id: "directories" }, repeat(dirList, Item)); 73 84 }); 74 85 75 86 // Add to DOM 76 - document.getElementById("directories")?.replaceWith(Directories()); 87 + effect(() => { 88 + document.getElementById("directories")?.replaceWith(Directories()); 89 + }); 77 90 78 91 //////////////////////////////////////////// 79 92 // ACTIONS 80 93 //////////////////////////////////////////// 81 - const isAvailable = async (fileUri: string) => { 94 + const consult = async (fileUriOrScheme: string) => { 82 95 const isSupported = !!(globalThis as any).showDirectoryPicker; 83 96 if (!isSupported) { 84 - console.warn( 85 - "`input/native-fs` is not supported on this platform, missing File System Access API.", 86 - ); 87 - return false; 97 + return { supported: false, reason: "File System Access API is not supported" }; 98 + } 99 + 100 + if (!fileUriOrScheme.includes(":")) { 101 + if (fileUriOrScheme !== SCHEME) return { supported: false, reason: "Scheme does not match" }; 102 + return { supported: true }; 88 103 } 89 104 90 105 const handles = await fetchHandles(); 91 - const uri = URI.parse(fileUri); 92 - return uri.host && !!handles[uri.host]; 106 + const uri = URI.parse(fileUriOrScheme); 107 + if (uri.scheme !== SCHEME) return { supported: false, reason: "Scheme does not match" }; 108 + return { supported: true, consultation: uri.host && !!handles[uri.host] }; 93 109 }; 94 110 95 111 const list = async (cachedTracks: Track[] = []) => { ··· 137 153 }; 138 154 139 155 const resolve = async (fileUri: string) => { 156 + const isSupported = !!(globalThis as any).showDirectoryPicker; 157 + if (!isSupported) return undefined; 158 + 140 159 const uri = URI.parse(fileUri); 141 - if (uri.scheme !== INPUT_SCHEME) return undefined; 160 + if (uri.scheme !== SCHEME) return undefined; 142 161 if (!uri.host || !uri.path) return undefined; 143 162 144 163 const handles = await fetchHandles(); ··· 191 210 setMounts(await fetchHandlesList()); 192 211 }; 193 212 194 - context.setActionHandler("isAvailable", isAvailable); 213 + context.setActionHandler("consult", consult); 195 214 context.setActionHandler("list", list); 196 215 context.setActionHandler("resolve", resolve); 197 216 context.setActionHandler("mount", mount); ··· 231 250 for await (const item of dir.values()) { 232 251 if (item.kind === "file" && isAudioFile(item.name)) { 233 252 const uri = URI.serialize({ 234 - scheme: INPUT_SCHEME, 253 + scheme: SCHEME, 235 254 host: rootHandleId, 236 255 path: `${path.length ? "/" + path.join("/") : ""}/${item.name}`, 237 256 });
+1 -1
src/pages/input/native-fs/_manifest.json
··· 32 32 }, 33 33 "resolve": { 34 34 "title": "Resolve", 35 - "description": "Potentially translates a track uri with a matching scheme into a URL pointing at the audio bytes. If it can be resolved that is, otherwise you'll get `undefined`.", 35 + "description": "Potentially translates a track uri with a matching scheme into a URL pointing at the audio bytes. If it can be resolved that is, otherwise you'll get `undefined`. Use the `consult` action to get a more detailed answer.", 36 36 "params_schema": { 37 37 "type": "string", 38 38 "description": "The uri to resolve"
+25
src/pages/orchestrator/input-cache/_applet.astro
··· 1 + <script> 2 + import { applets } from "@web-applets/sdk"; 3 + 4 + import type { Output, Track } from "@applets/core/types.d.ts"; 5 + import { applet } from "@scripts/theme"; 6 + 7 + //////////////////////////////////////////// 8 + // SETUP 9 + //////////////////////////////////////////// 10 + // Register applet 11 + const context = applets.register<Output>(); 12 + 13 + // Applet connections 14 + const orchestrator = { 15 + output: await applet("../../configurator/output", { context: self.parent }), 16 + }; 17 + 18 + //////////////////////////////////////////// 19 + // ACTIONS 20 + //////////////////////////////////////////// 21 + 22 + //////////////////////////////////////////// 23 + // 🛠️ 24 + //////////////////////////////////////////// 25 + </script>
+6
src/pages/orchestrator/input-cache/_manifest.json
··· 1 + { 2 + "name": "diffuse/orchestrator/input-cache", 3 + "title": "Diffuse Orchestrator | Input cache", 4 + "entrypoint": "index.html", 5 + "actions": {} 6 + }
+9
src/pages/orchestrator/input-cache/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 -4
src/pages/themes/webamp/index.astro
··· 9 9 <main> 10 10 <div class="desktop"> 11 11 <!-- INPUT --> 12 - <a href="/input/native-fs/" target="_blank" class="button desktop__item"> 13 - <img src="/images/icons/windows_98/directory_open_cool-0.png" width="32" /> 14 - <label>Manage input (music)</label> 12 + <a href="/configurator/input/" target="_blank" class="button desktop__item"> 13 + <img src="/images/icons/windows_98/cd_audio_cd_a-4.png" width="32" /> 14 + <label>Manage audio inputs</label> 15 15 </a> 16 16 17 17 <!-- OUTPUT --> 18 18 <a href="/configurator/output/" target="_blank" class="button desktop__item"> 19 19 <img src="/images/icons/windows_98/directory_open_file_mydocs_2k-2.png" width="32" /> 20 - <label>Manage output (data)</label> 20 + <label>Manage user data</label> 21 21 </a> 22 22 </div> 23 23 </main>
+1 -1
src/scripts/theme.ts
··· 13 13 opts: { 14 14 addSlashSuffix?: boolean; 15 15 context?: Window; 16 - container?: HTMLElement; 16 + container?: HTMLElement | Element; 17 17 id?: string; 18 18 setHeight?: boolean; 19 19 } = {},
+13 -31
src/scripts/themes/webamp/index.ts
··· 13 13 // 🗂️ Applets 14 14 //////////////////////////////////////////// 15 15 16 - // const _configurator = { 17 - // output: await applet("../../configurator/output"), 18 - // }; 19 - 20 - const input = { 21 - nativeFs: await applet("../../input/native-fs"), 16 + const configurator = { 17 + input: await applet("../../configurator/input"), 22 18 }; 23 19 24 - const _orchestrator = { 25 - output: await applet<Output>("../../orchestrator/output-management"), 26 - }; 20 + // const orchestrator = { 21 + // output: await applet<Output>("../../orchestrator/output-management"), 22 + // }; 27 23 28 24 //////////////////////////////////////////// 29 25 // ⚡ 30 26 //////////////////////////////////////////// 31 27 const amp = new Webamp({ 32 - filePickers: [ 33 - { 34 - contextMenuName: "(applet) Mount directory", 35 - filePicker: async () => { 36 - try { 37 - await input.nativeFs.sendAction("mount"); 38 - } catch (_err) {} 39 - amp.setTracksToPlay(await listMountDirectories()); 40 - return []; 41 - }, 42 - requiresNetwork: false, 43 - }, 44 - ], 45 - 46 - // Tracks 47 - initialTracks: await listMountDirectories(), 28 + initialTracks: await makeList(), 48 29 }); 49 30 50 31 const ampNode = document.createElement("div"); ··· 56 37 // 🛠️ 57 38 //////////////////////////////////////////// 58 39 59 - async function listMountDirectories() { 60 - await input.nativeFs.sendAction("list"); 61 - const list = input.nativeFs.data as Track[]; 40 + async function makeList() { 41 + await configurator.input.sendAction("list"); 42 + const list = configurator.input.data as Track[]; 62 43 63 44 return list.reduce(async (acc: Promise<Array<{ url: string }>>, track: Track) => { 64 - await input.nativeFs.sendAction("resolve", track.uri); 65 - const url = input.nativeFs.data as string; 66 - return [...(await acc), { url }]; 45 + await configurator.input.sendAction("resolve", track.uri); 46 + const url = configurator.input.data as string | undefined; 47 + if (url) return [...(await acc), { url }]; 48 + return await acc; 67 49 }, Promise.resolve([])); 68 50 }