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

Configure Feed

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

feat: input configurator worker

+168 -757
-92
_backup/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 - <!-- TODO: Need to check if supported on this device --> 10 - <a href="../../input/native-fs/" class="with-icon"> 11 - <i class="iconoir-open-in-window"></i> 12 - <strong>My device</strong> 13 - </a> 14 - <br /> 15 - <a href="../../input/opensubsonic/" class="with-icon"> 16 - <i class="iconoir-open-in-window"></i> 17 - <strong>Opensubsonic server</strong> 18 - </a> 19 - <br /> 20 - <a href="../../input/s3/" class="with-icon"> 21 - <i class="iconoir-open-in-window"></i> 22 - <strong>S3-compatible service</strong> 23 - </a> 24 - </p> 25 - </div> 26 - </main> 27 - 28 - <style is:global> 29 - iframe { 30 - display: none; 31 - } 32 - </style> 33 - 34 - <script> 35 - import type { Track } from "@applets/core/types"; 36 - import type { Tasks } from "@scripts/configurator/input/worker"; 37 - import { applet, register, tunnel } from "@scripts/applet/common"; 38 - import { endpoint, transfer } from "@scripts/common"; 39 - import manifest from "./_manifest.json"; 40 - 41 - //////////////////////////////////////////// 42 - // SETUP 43 - //////////////////////////////////////////// 44 - const worker = endpoint<Tasks>( 45 - new Worker(new URL("../../../scripts/configurator/input/worker", import.meta.url), { 46 - type: "module", 47 - name: manifest.name, 48 - }), 49 - ); 50 - 51 - // Register applet + worker 52 - const context = register({ worker }); 53 - 54 - // Applet connections 55 - const input = { 56 - "file+local": applet("/input/native-fs", { context: self }), 57 - opensubsonic: applet("/input/opensubsonic", { context: self }), 58 - s3: applet("/input/s3", { context: self }), 59 - }; 60 - 61 - // Provide tunnel to worker 62 - tunnel(worker, input); 63 - 64 - //////////////////////////////////////////// 65 - // ACTIONS 66 - //////////////////////////////////////////// 67 - const consult = async (fileUriOrScheme: string) => { 68 - return await worker.consult(fileUriOrScheme); 69 - }; 70 - 71 - const contextualize = async (cachedTracks: Track[]) => { 72 - return await worker.contextualize(transfer(cachedTracks)); 73 - }; 74 - 75 - const groupConsult = async (tracks: Track[]) => { 76 - return await worker.groupConsult(transfer(tracks)); 77 - }; 78 - 79 - const list = async (cachedTracks: Track[] = []) => { 80 - return await worker.list(transfer(cachedTracks)); 81 - }; 82 - 83 - const resolve = async (args: { method: string; uri: string }) => { 84 - return await worker.resolve(args); 85 - }; 86 - 87 - context.setActionHandler("consult", consult); 88 - context.setActionHandler("contextualize", contextualize); 89 - context.setActionHandler("groupConsult", groupConsult); 90 - context.setActionHandler("list", list); 91 - context.setActionHandler("resolve", resolve); 92 - </script>
-49
_backup/pages/configurator/input/_manifest.json
··· 1 - { 2 - "name": "diffuse/configurator/input", 3 - "title": "Diffuse Configurator | Input", 4 - "entrypoint": "index.html", 5 - "actions": { 6 - "consult": { 7 - "title": "Consult", 8 - "params_schema": { 9 - "type": "string", 10 - "description": "The uri to check the availability of." 11 - } 12 - }, 13 - "contextualize": { 14 - "title": "Contextualize", 15 - "description": "Provide context to all inputs.", 16 - "params_schema": { 17 - "type": "array", 18 - "description": "Array of tracks", 19 - "items": { "type": "object" } 20 - } 21 - }, 22 - "list": { 23 - "title": "List", 24 - "description": "List tracks from all inputs.", 25 - "params_schema": { 26 - "type": "array", 27 - "description": "A list of (cached) tracks", 28 - "items": { 29 - "type": "object" 30 - } 31 - } 32 - }, 33 - "resolve": { 34 - "title": "Resolve", 35 - "description": "Potentially translates a track uri with a matching scheme into a URL pointing at the audio bytes or an audio stream. If it can be resolved that is, otherwise you'll get `undefined`.", 36 - "params_schema": { 37 - "type": "object", 38 - "properties": { 39 - "method": { 40 - "type": "string", 41 - "description": "The HTTP method that is going to be used on the resolved URI." 42 - }, 43 - "uri": { "type": "string", "description": "The URI to resolve." } 44 - }, 45 - "required": ["method", "uri"] 46 - } 47 - } 48 - } 49 - }
-9
_backup/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>
-86
_backup/pages/configurator/output/_applet.astro
··· 1 - <main class="container"> 2 - <h1>Storage configuration</h1> 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> 10 - <div id="options"> 11 - <p> 12 - <span class="with-icon"> 13 - <i class="iconoir-bonfire"></i> 14 - <small>Just a moment, loading storage options.</small> 15 - </span> 16 - </p> 17 - </div> 18 - <!-- Warning about cross sync --> 19 - <p> 20 - <small> 21 - <mark style="display: inline-block"> 22 - <span class="with-icon"> 23 - <i class="iconoir-warning-triangle"></i> 24 - Data is currently not synced across different storages! 25 - </span> 26 - </mark> 27 - </small> 28 - </p> 29 - <!-- Snapshots --> 30 - <h2>Export & Import</h2> 31 - <p> 32 - <a id="download" download="diffuse.json" style="cursor: pointer">Download a snapshot</a> of your 33 - data. 34 - </p> 35 - </main> 36 - 37 - <style is:global> 38 - iframe { 39 - display: none; 40 - } 41 - </style> 42 - 43 - <script> 44 - import { INITIAL_MANAGED_OUTPUT } from "@scripts/output/common"; 45 - import { inIframe } from "@scripts/common"; 46 - import { effect } from "@scripts/spellcaster"; 47 - 48 - import { connection } from "@scripts/configurator/output/connections"; 49 - import { active } from "@scripts/configurator/output/signals"; 50 - import { context } from "@scripts/configurator/output/context"; 51 - import { setContextData } from "@scripts/configurator/output/events"; 52 - 53 - //////////////////////////////////////////// 54 - // SETUP 55 - //////////////////////////////////////////// 56 - context.data = INITIAL_MANAGED_OUTPUT; 57 - 58 - //////////////////////////////////////////// 59 - // EFFECTS 60 - //////////////////////////////////////////// 61 - effect(() => { 62 - const method = active(); 63 - 64 - // Monitor data 65 - (async () => { 66 - const conn = await connection(method); 67 - context.data = conn.data; 68 - conn.addEventListener("data", setContextData); 69 - })(); 70 - }); 71 - 72 - //////////////////////////////////////////// 73 - // ACTIONS 74 - //////////////////////////////////////////// 75 - const tracks = async (...args: unknown[]) => { 76 - const conn = await connection(active()); 77 - await conn.sendAction("tracks", ...args, { timeoutDuration: 60000 * 5 }); 78 - }; 79 - 80 - context.setActionHandler("tracks", tracks); 81 - 82 - //////////////////////////////////////////// 83 - // UI 84 - //////////////////////////////////////////// 85 - const ui = inIframe() ? undefined : await import("@scripts/configurator/output/ui"); 86 - </script>
-18
_backup/pages/configurator/output/_manifest.json
··· 1 - { 2 - "name": "diffuse/configurator/output", 3 - "title": "Diffuse Configurator | Output", 4 - "entrypoint": "index.html", 5 - "actions": { 6 - "tracks": { 7 - "title": "Tracks", 8 - "description": "Store or retrieve tracks. Passing in an array of tracks, stores them; passing no parameter, retrieves them.", 9 - "params_schema": { 10 - "type": "array", 11 - "description": "List of tracks", 12 - "items": { 13 - "type": "object" 14 - } 15 - } 16 - } 17 - } 18 - }
-9
_backup/pages/configurator/output/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>
-140
_backup/scripts/configurator/input/worker.ts
··· 1 - import * as URI from "uri-js"; 2 - 3 - import type { 4 - Consult, 5 - ConsultGrouping, 6 - GroupConsult, 7 - InputWorkerTasks, 8 - Track, 9 - } from "@applets/core/types"; 10 - import { groupTracksPerScheme, initialConnections, provide } from "@scripts/common"; 11 - 12 - //////////////////////////////////////////// 13 - // SETUP 14 - //////////////////////////////////////////// 15 - const actions = { 16 - consult, 17 - contextualize, 18 - groupConsult, 19 - list, 20 - resolve, 21 - }; 22 - 23 - const { connections, tasks } = provide({ 24 - actions, 25 - connections: initialConnections<InputWorkerTasks>(["file+local", "opensubsonic", "s3"]), 26 - tasks: actions, 27 - }); 28 - 29 - export type Actions = typeof actions; 30 - export type Tasks = typeof tasks; 31 - 32 - function isSupportedScheme(scheme: string) { 33 - return !!connections[scheme]; 34 - } 35 - 36 - //////////////////////////////////////////// 37 - // ACTIONS 38 - //////////////////////////////////////////// 39 - 40 - async function consult(fileUriOrScheme: string): Promise<Consult> { 41 - const scheme = fileUriOrScheme.includes(":") 42 - ? URI.parse(fileUriOrScheme).scheme || fileUriOrScheme 43 - : fileUriOrScheme; 44 - 45 - if (!isSupportedScheme(scheme)) { 46 - return { supported: false, reason: "Unsupported scheme" }; 47 - } 48 - 49 - const conn = await connections[scheme].promise; 50 - return conn.consult(fileUriOrScheme); 51 - } 52 - 53 - async function contextualize(tracks: Track[]) { 54 - const groups = groupTracks(tracks); 55 - const promises = Object.entries(groups).map(async ([scheme, tracksGroup]: [string, Track[]]) => { 56 - if (!isSupportedScheme(scheme) || tracksGroup.length === 0) return; 57 - const conn = await connections[scheme].promise; 58 - await conn.contextualize(tracksGroup); 59 - }); 60 - 61 - await Promise.all(promises); 62 - } 63 - 64 - async function groupConsult(tracks: Track[]) { 65 - const groups = groupTracksPerScheme(tracks); 66 - 67 - const consultations: GroupConsult[] = await Promise.all( 68 - Object.keys(groups).map(async (scheme) => { 69 - if (!isSupportedScheme(scheme)) { 70 - return { 71 - [scheme]: { 72 - available: false, 73 - reason: "Unsupported scheme", 74 - tracks: groups[scheme] || [], 75 - }, 76 - }; 77 - } 78 - 79 - const conn = await connections[scheme].promise; 80 - const result = await conn.groupConsult(groups[scheme] || {}); 81 - 82 - return result; 83 - }), 84 - ); 85 - 86 - return consultations.reduce((acc: GroupConsult, c: GroupConsult) => { 87 - return { ...acc, ...c }; 88 - }, {}); 89 - } 90 - 91 - async function list(cachedTracks: Track[] = []) { 92 - const groups = await groupConsult(cachedTracks); 93 - 94 - Object.keys(connections).forEach((scheme) => { 95 - if (!groups[scheme]) groups[scheme] = { available: true, tracks: [] }; 96 - }); 97 - 98 - const promises = Object.entries(groups).map( 99 - async ([scheme, { available, tracks }]: [string, ConsultGrouping]) => { 100 - if (!available) return tracks; 101 - if (!isSupportedScheme(scheme)) return tracks; 102 - const conn = await connections[scheme].promise; 103 - return conn.list(tracks); 104 - }, 105 - ); 106 - 107 - const nested = await Promise.all(promises); 108 - const tracks = nested.flat(1); 109 - 110 - return tracks; 111 - } 112 - 113 - async function resolve(args: { method: string; uri: string }) { 114 - const scheme = args.uri.split(":", 1)[0]; 115 - if (!isSupportedScheme(scheme)) return undefined; 116 - 117 - try { 118 - const conn = await connections[scheme].promise; 119 - return await conn.resolve(args); 120 - } catch (err) { 121 - console.error(`[configuration/input] Resolve error for scheme '${scheme}'.`, err); 122 - } 123 - } 124 - 125 - //////////////////////////////////////////// 126 - // 🛠️ 127 - //////////////////////////////////////////// 128 - 129 - function groupTracks(tracks: Track[]) { 130 - const grouped = groupTracksPerScheme( 131 - tracks, 132 - Object.fromEntries( 133 - Object.entries(connections).map(([k, _v]) => { 134 - return [k, []]; 135 - }), 136 - ), 137 - ); 138 - 139 - return grouped; 140 - }
-25
_backup/scripts/configurator/output/connections.ts
··· 1 - import type { Applet } from "@web-applets/sdk"; 2 - 3 - import type { ManagedOutput } from "@applets/core/types"; 4 - import type { Method } from "./types"; 5 - import { CONNECTIONS, CUSTOM_KEY } from "./constants"; 6 - import { applet } from "@scripts/applet/common"; 7 - 8 - const connections: Record<string, Applet<ManagedOutput>> = {}; 9 - 10 - export async function connection(method: Method) { 11 - if (connections[method]) return connections[method]; 12 - 13 - let href; 14 - 15 - if (method === "custom") { 16 - href = localStorage.getItem(CUSTOM_KEY); 17 - if (!href) throw new Error("Missing custom applet URL"); 18 - } else { 19 - href = CONNECTIONS[method]; 20 - if (!href) throw new Error("No href defined for this connection method."); 21 - } 22 - 23 - connections[method] = await applet(href); 24 - return connections[method]; 25 - }
-11
_backup/scripts/configurator/output/constants.ts
··· 1 - export const METHODS = ["browser", "custom", "device"] as const; 2 - 3 - export const CONNECTIONS = { 4 - browser: "/output/indexed-db/", 5 - custom: undefined, 6 - device: "/output/native-fs/", 7 - }; 8 - 9 - export const DEFAULT_METHOD: (typeof METHODS)[number] = "browser"; 10 - export const LOCALSTORAGE_KEY = "applets/configurator/output/active-output"; 11 - export const CUSTOM_KEY = "applets/configurator/output/custom-applet";
-4
_backup/scripts/configurator/output/context.ts
··· 1 - import type { ManagedOutput } from "@applets/core/types"; 2 - import { register } from "@scripts/applet/common"; 3 - 4 - export const context = register<ManagedOutput>();
-6
_backup/scripts/configurator/output/events.ts
··· 1 - import type { AppletEvent } from "@web-applets/sdk"; 2 - import { context } from "./context"; 3 - 4 - export function setContextData(event: AppletEvent) { 5 - context.data = event.data; 6 - }
-45
_backup/scripts/configurator/output/signals.ts
··· 1 - import { effect, signal } from "@scripts/spellcaster"; 2 - 3 - import type { Method } from "./types"; 4 - import { DEFAULT_METHOD, LOCALSTORAGE_KEY, METHODS } from "./constants"; 5 - 6 - export const stored = localStorage.getItem(LOCALSTORAGE_KEY); 7 - export const active = signal<Method>( 8 - stored && METHODS.includes(stored as Method) ? (stored as Method) : DEFAULT_METHOD, 9 - ); 10 - 11 - // 🚀 12 - 13 - // storage().then((s) => { 14 - // const stored = s.getItem(LOCALSTORAGE_KEY); 15 - // active(stored && METHODS.includes(stored as Method) ? (stored as Method) : DEFAULT_METHOD); 16 - // }); 17 - 18 - // EFFECT 19 - 20 - effect(() => { 21 - const method = active(); 22 - storage().then((s) => s.setItem(LOCALSTORAGE_KEY, method)); 23 - }); 24 - 25 - async function storage() { 26 - // const hasStorageAccess = 27 - // (await document.hasStorageAccess()) || 28 - // ( 29 - // await navigator.permissions.query({ 30 - // name: "storage-access", 31 - // }) 32 - // ).state === "granted"; 33 - 34 - // if (hasStorageAccess) { 35 - // // @ts-ignore 36 - // const handle: any = await document.requestStorageAccess({ 37 - // localStorage: true, 38 - // }); 39 - 40 - // return handle ? handle.localStorage : localStorage; 41 - // } else { 42 - // return localStorage; 43 - // } 44 - return localStorage; 45 - }
-5
_backup/scripts/configurator/output/types.d.ts
··· 1 - import { METHODS } from "./constants"; 2 - 3 - export type Method = (typeof METHODS)[number]; 4 - export type List<M extends Method = Method> = Map<string, ListItem<M>>; 5 - export type ListItem<M> = { activated: boolean; icon: string; method: M; title: string };
-258
_backup/scripts/configurator/output/ui.ts
··· 1 - import { type Signal, computed, effect, signal } from "@scripts/spellcaster"; 2 - import { h, repeat, text } from "@scripts/spellcaster/hyperscript.js"; 3 - 4 - import { applet, reactive } from "@scripts/applet/common"; 5 - import { CUSTOM_KEY } from "./constants"; 6 - import { active } from "./signals"; 7 - import { connection } from "./connections"; 8 - import { context } from "./context"; 9 - import type { List, ListItem, Method } from "./types"; 10 - import { setContextData } from "./events"; 11 - 12 - //////////////////////////////////////////// 13 - // EFFECTS 14 - //////////////////////////////////////////// 15 - reactive( 16 - context.scope, 17 - (data) => data.tracks.cacheId, 18 - () => { 19 - // Export data URI 20 - const dl = document.querySelector("#download"); 21 - if (dl) { 22 - const json = JSON.stringify(context.data.tracks.collection, null, 2); 23 - const href = URL.createObjectURL(new Blob([json], { type: "application/json" })); 24 - dl.setAttribute("href", href); 25 - } 26 - }, 27 - ); 28 - 29 - // Mount + Unmount 30 - async function mountStorageMethod(method: Method) { 31 - switch (method) { 32 - case "custom": 33 - modalIsOpen(true); 34 - break; 35 - default: 36 - const conn = await connection(method); 37 - try { 38 - await conn.sendAction("mount", undefined, { timeoutDuration: 60000 }); 39 - active(method); 40 - } catch (err) { 41 - const msg: string = 42 - err && typeof err === "object" && "message" in err ? `${err.message}` : `${err}`; 43 - if (msg.startsWith("[user] ")) alert(msg.slice(7)); 44 - } 45 - break; 46 - } 47 - } 48 - 49 - async function unmountStorageMethod(method: Method) { 50 - const conn = await connection(method); 51 - conn.removeEventListener("data", setContextData); 52 - await conn.sendAction("unmount", undefined, { timeoutDuration: 60000 }); 53 - } 54 - 55 - //////////////////////////////////////////// 56 - // LIST 57 - //////////////////////////////////////////// 58 - const list = computed<List>(() => { 59 - const a = active(); 60 - 61 - return new Map([ 62 - [ 63 - `browser-${a === "browser"}`, 64 - { 65 - title: "Browser storage", 66 - icon: "iconoir-app-window", 67 - method: "browser", 68 - activated: a === "browser", 69 - }, 70 - ], 71 - [ 72 - `device-${a === "device"}`, 73 - { 74 - title: "Device storage", 75 - icon: "iconoir-laptop", 76 - method: "device", 77 - activated: a === "device", 78 - }, 79 - ], 80 - [ 81 - `custom-${a === "custom"}`, 82 - { 83 - title: "Custom applet", 84 - icon: "iconoir-globe", 85 - method: "custom", 86 - activated: a === "custom", 87 - }, 88 - ], 89 - ]); 90 - }); 91 - 92 - const Item = (signal: Signal<ListItem<Method>>) => { 93 - const item = signal(); 94 - 95 - const colorClass = item.activated ? "pico-color-jade-500" : "pico-color-grey-500"; 96 - const icon = item.activated ? "iconoir-check-circle-solid" : "iconoir-check-circle"; 97 - 98 - return h( 99 - "p", 100 - { 101 - onclick: clickHandler(item.method), 102 - style: "cursor: pointer", 103 - }, 104 - [ 105 - h("span", { className: "with-icon" }, [ 106 - h("i", { className: item.icon }), 107 - h("strong", {}, text(item.title)), 108 - ]), 109 - h("br"), 110 - h("span", { className: `with-icon ${colorClass}` }, [ 111 - h("i", { className: icon }), 112 - h("span", {}, text(item.activated ? "Active" : "Select")), 113 - ]), 114 - ], 115 - ); 116 - }; 117 - 118 - function clickHandler(method: Method) { 119 - return async () => { 120 - const currentlyActive = active(); 121 - 122 - // const permission = await navigator.permissions.query({ 123 - // name: "storage-access", 124 - // }); 125 - 126 - // if (permission.state === "prompt") { 127 - // // @ts-ignore 128 - // await document.requestStorageAccess({ 129 - // localStorage: true, 130 - // }); 131 - // } 132 - 133 - if (currentlyActive === method && currentlyActive !== "custom") return; 134 - if (currentlyActive) unmountStorageMethod(currentlyActive); 135 - await mountStorageMethod(method); 136 - }; 137 - } 138 - 139 - const Options = computed(() => { 140 - return h("div", { id: "options" }, repeat(list, Item)); 141 - }); 142 - 143 - // Add to DOM 144 - document.getElementById("options")?.replaceWith(Options()); 145 - 146 - //////////////////////////////////////////// 147 - // CUSTOM APPLET 148 - //////////////////////////////////////////// 149 - type CustomAppletState = "waiting" | "connecting" | { error: string } | "connected"; 150 - 151 - const modalIsOpen = signal(false); 152 - const customState = signal<CustomAppletState>("waiting"); 153 - 154 - const Modal = () => { 155 - const Header = h("header", {}, [ 156 - h("button", { 157 - attrs: { rel: "prev" }, 158 - ariaLabel: "Close", 159 - onclick: close, 160 - }), 161 - h("p", {}, [ 162 - h("strong", {}, [ 163 - h("span", { className: "with-icon" }, [ 164 - h("i", { className: "iconoir-globe" }), 165 - h("span", {}, text("Load a custom applet")), 166 - ]), 167 - ]), 168 - ]), 169 - ]); 170 - 171 - const Content = h("form", { onsubmit: submit }, [ 172 - h("fieldset", { role: "group" }, [ 173 - h("input", { 174 - type: "url", 175 - name: "url", 176 - placeholder: "https://applets.diffuse.sh/storage/output/indexed-db/", 177 - required: true, 178 - value: localStorage.getItem(CUSTOM_KEY) || "", 179 - }), 180 - h("input", { type: "submit", value: "Connect" }), 181 - ]), 182 - h("p", {}, [ 183 - h("small", { className: "with-icon" }, (element) => { 184 - const comp = computed(() => { 185 - const s = customState(); 186 - 187 - if (s === "connecting") { 188 - return [ 189 - h("i", { className: "iconoir-ev-plug-charging" }), 190 - h("span", {}, text("Connecting ...")), 191 - ]; 192 - } else if (typeof s !== "string") { 193 - return [ 194 - h("i", { className: "iconoir-warning-circle" }), 195 - h("span", {}, text(`Error: ${s.error}`)), 196 - ]; 197 - } 198 - 199 - return [h("span", {}, text("Enter the URL to the applet."))]; 200 - }); 201 - 202 - effect(() => { 203 - element.replaceChildren(...comp()); 204 - }); 205 - }), 206 - ]), 207 - ]); 208 - 209 - return h( 210 - "dialog", 211 - computed(() => ({ open: modalIsOpen() })), 212 - [h("article", {}, [Header, Content])], 213 - ); 214 - }; 215 - 216 - // Events 217 - function close() { 218 - modalIsOpen(false); 219 - } 220 - 221 - async function submit(event: SubmitEvent) { 222 - event.preventDefault(); 223 - 224 - const input: HTMLInputElement | null = (event.target as HTMLFormElement).querySelector( 225 - `input[type="url"]`, 226 - ); 227 - 228 - if (!input) return; 229 - 230 - const url = input.value; 231 - customState("connecting"); 232 - 233 - const apl = await applet(url).catch((err) => { 234 - customState({ error: "Failed to connect" }); 235 - throw err; 236 - }); 237 - 238 - let missingAction; 239 - 240 - ["tracks", "mount", "unmount"].forEach((method) => { 241 - if (!apl.manifest.actions?.[method]) missingAction = method; 242 - }); 243 - 244 - if (missingAction) { 245 - customState({ error: `Applet is missing a required action: "${missingAction}"` }); 246 - return; 247 - } 248 - 249 - localStorage.setItem(CUSTOM_KEY, url); 250 - await apl.sendAction("mount", undefined, { timeoutDuration: 60000 }); 251 - 252 - active("custom"); 253 - modalIsOpen(false); 254 - customState("waiting"); 255 - } 256 - 257 - // Add to DOM 258 - document.querySelector("main")?.appendChild(Modal());
+12
src/component/configurator/common.js
··· 1 + import QS from "query-string"; 2 + 3 + /** 4 + * @param {Location} loc 5 + * @returns {Record<string, Worker>} 6 + */ 7 + export function connectionsFromQuery(loc) { 8 + const qs = QS.parse(loc.search); 9 + console.log(qs); 10 + 11 + return {}; 12 + }
+156
src/component/configurator/input/worker.js
··· 1 + import * as URI from "uri-js"; 2 + 3 + import { groupTracksPerScheme } from "@common/index.js"; 4 + import { connectionsFromQuery } from "../common.js"; 5 + import { use } from "@common/worker.js"; 6 + 7 + /** 8 + * @import { GroupConsult, InputActions as Actions, Track } from "@component/core/types.d.ts"; 9 + */ 10 + 11 + //////////////////////////////////////////// 12 + // ⚡️ 13 + //////////////////////////////////////////// 14 + 15 + const connections = connectionsFromQuery(location); 16 + 17 + /** 18 + * @param {string} scheme 19 + * @param {string} actionName 20 + */ 21 + function proxy(scheme, actionName) { 22 + const worker = connections[scheme]; 23 + const proxyFn = use(actionName, worker); 24 + 25 + return proxyFn; 26 + } 27 + 28 + /** 29 + * @param {string} scheme 30 + */ 31 + function isSupportedScheme(scheme) { 32 + return !!connections[scheme]; 33 + } 34 + 35 + //////////////////////////////////////////// 36 + // ACTIONS 37 + //////////////////////////////////////////// 38 + 39 + /** 40 + * @type {Actions['consult']} 41 + */ 42 + export async function consult(fileUriOrScheme) { 43 + const scheme = fileUriOrScheme.includes(":") 44 + ? URI.parse(fileUriOrScheme).scheme || fileUriOrScheme 45 + : fileUriOrScheme; 46 + 47 + if (!isSupportedScheme(scheme)) { 48 + return { supported: false, reason: "Unsupported scheme" }; 49 + } 50 + 51 + return await proxy(scheme, "consult")(fileUriOrScheme); 52 + } 53 + 54 + /** 55 + * @type {Actions['contextualize']} 56 + */ 57 + export async function contextualize(tracks) { 58 + const groups = groupTracks(tracks); 59 + const promises = Object.entries(groups).map( 60 + async ([scheme, tracksGroup]) => { 61 + if (!isSupportedScheme(scheme) || tracksGroup.length === 0) return; 62 + return await proxy(scheme, "contextualize")(tracksGroup); 63 + }, 64 + ); 65 + 66 + await Promise.all(promises); 67 + } 68 + 69 + /** 70 + * @type {Actions['groupConsult']} 71 + */ 72 + export async function groupConsult(tracks) { 73 + const groups = groupTracksPerScheme(tracks); 74 + 75 + /** @type {GroupConsult[]} */ 76 + const consultations = await Promise.all( 77 + Object.keys(groups).map(async (scheme) => { 78 + if (!isSupportedScheme(scheme)) { 79 + return { 80 + [scheme]: { 81 + available: false, 82 + reason: "Unsupported scheme", 83 + tracks: groups[scheme] || [], 84 + }, 85 + }; 86 + } 87 + 88 + return await proxy(scheme, "groupConsult")(groups[scheme] || {}); 89 + }), 90 + ); 91 + 92 + return consultations.reduce((acc, c) => { 93 + return { ...acc, ...c }; 94 + }, {}); 95 + } 96 + 97 + /** 98 + * @type {Actions['list']} 99 + */ 100 + export async function list(cachedTracks = []) { 101 + const groups = await groupConsult(cachedTracks); 102 + 103 + Object.keys(connections).forEach((scheme) => { 104 + if (!groups[scheme]) groups[scheme] = { available: true, tracks: [] }; 105 + }); 106 + 107 + const promises = Object.entries(groups).map( 108 + async ([scheme, { available, tracks }]) => { 109 + if (!available) return tracks; 110 + if (!isSupportedScheme(scheme)) return tracks; 111 + return await proxy(scheme, "list")(tracks); 112 + }, 113 + ); 114 + 115 + const nested = await Promise.all(promises); 116 + const tracks = nested.flat(1); 117 + 118 + return tracks; 119 + } 120 + 121 + /** 122 + * @type {Actions['resolve']} 123 + */ 124 + export async function resolve(args) { 125 + const scheme = args.uri.split(":", 1)[0]; 126 + if (!isSupportedScheme(scheme)) return undefined; 127 + 128 + try { 129 + return await proxy(scheme, "resolve")(args); 130 + } catch (err) { 131 + console.error( 132 + `[configurator/input] Resolve error for scheme '${scheme}'.`, 133 + err, 134 + ); 135 + } 136 + } 137 + 138 + //////////////////////////////////////////// 139 + // 🛠️ 140 + //////////////////////////////////////////// 141 + 142 + /** 143 + * @param {Track[]} tracks 144 + */ 145 + function groupTracks(tracks) { 146 + const grouped = groupTracksPerScheme( 147 + tracks, 148 + Object.fromEntries( 149 + Object.entries(connections).map(([k, _v]) => { 150 + return [k, []]; 151 + }), 152 + ), 153 + ); 154 + 155 + return grouped; 156 + }