···1616</main>
17171818<script>
1919- import { computed, effect, type Signal, signal } from "spellcaster";
2020- import { repeat, tags, text } from "spellcaster/hyperscript.js";
2121- import { type FileSystemDirectoryHandle, showDirectoryPicker } from "native-file-system-adapter";
2222- import * as IDB from "idb-keyval";
2323- import * as URI from "uri-js";
2424- import QS from "query-string";
2525-1919+ import type { Actions } from "@scripts/input/native-fs/worker";
2620 import type { Track } from "@applets/core/types.d.ts";
2727- import { isAudioFile } from "@scripts/input/common";
2821 import { register } from "@scripts/applet/common";
2929-3030- import manifest from "./_manifest.json";
3131-3232- type Handles = Record<string, FileSystemDirectoryHandle>;
3333-3434- // TODO: Add ability to list cached tracks from other devices (ie. unknown handle ids)
2222+ import { endpoint, inIframe } from "@scripts/common";
2323+ import * as Mounting from "@scripts/input/native-fs/mounting";
35243625 ////////////////////////////////////////////
3726 // SETUP
3827 ////////////////////////////////////////////
3939- const IDB_PREFIX = "@applets/input/native-fs";
4040- const IDB_HANDLES = `${IDB_PREFIX}/handles`;
4141- const SCHEME = manifest.input_properties.scheme;
2828+ const worker = endpoint<Actions>(
2929+ new Worker("../../../scripts/input/native-fs/worker", { type: "module" }),
3030+ );
42314332 // Register applet
4433 const context = register();
45344635 ////////////////////////////////////////////
4747- // UI
4848- ////////////////////////////////////////////
4949- const [mounts, setMounts] = signal(await fetchHandlesList());
5050-5151- // Mount button
5252- document.getElementById("mount")?.addEventListener("click", () => mount());
5353-5454- // Directories
5555- const dirList = computed(() => {
5656- return new Map(
5757- mounts().map((mount) => {
5858- return [mount.id, mount];
5959- }),
6060- );
6161- });
6262-6363- const Item = (signal: Signal<{ id: string; handle: FileSystemDirectoryHandle }>) => {
6464- const { id, handle } = signal();
6565-6666- return tags.li({}, [
6767- tags.span(
6868- { onclick: () => unmount(id), style: "cursor: pointer;", title: "Click/tap to delete" },
6969- text(handle.name),
7070- ),
7171- ]);
7272- };
7373-7474- const Directories = computed(() => {
7575- if (mounts().length === 0) {
7676- return tags.p({ id: "directories" }, [
7777- tags.small({}, [
7878- tags.em({}, text("No audio added yet, click the button below to add some.")),
7979- ]),
8080- ]);
8181- }
8282-8383- return tags.ul({ id: "directories" }, repeat(dirList, Item));
8484- });
8585-8686- // Add to DOM
8787- effect(() => {
8888- document.getElementById("directories")?.replaceWith(Directories());
8989- });
9090-9191- ////////////////////////////////////////////
9236 // ACTIONS
9337 ////////////////////////////////////////////
9438 const consult = async (fileUriOrScheme: string) => {
9595- if (!isSupported()) {
9696- return { supported: false, reason: "File System Access API is not supported" };
9797- }
9898-9999- if (!fileUriOrScheme.includes(":")) {
100100- if (fileUriOrScheme !== SCHEME) return { supported: false, reason: "Scheme does not match" };
101101- return { supported: true };
102102- }
103103-104104- const handles = await fetchHandles();
105105- const uri = URI.parse(fileUriOrScheme);
106106- if (uri.scheme !== SCHEME) return { supported: false, reason: "Scheme does not match" };
107107- return { supported: true, consultation: uri.host && !!handles[uri.host] };
3939+ return await worker.call.consult(fileUriOrScheme);
10840 };
10941110110- const contextualize = async (cachedTracks: Track[]) => {};
4242+ const contextualize = async (cachedTracks: Track[]) => {
4343+ return await worker.call.contextualize(cachedTracks);
4444+ };
1114511246 const list = async (cachedTracks: Track[] = []) => {
113113- if (!isSupported()) {
114114- return cachedTracks;
115115- }
116116-117117- // Continue if supported
118118- const handles = await fetchHandlesList();
119119-120120- // Recursive listing of all tracks of available handles
121121- const processed: Track[][] = await Promise.all(
122122- handles.map(({ id, handle }) => {
123123- return recursiveList(handle, id, []);
124124- }),
125125- );
126126-127127- // Group tracks by handle id & index by track uri
128128- const cache = cachedTracks.reduce(
129129- (acc: Record<string, Record<string, Track>>, track: Track) => {
130130- const handleId = trackHandleId(track);
131131- if (!handleId) return acc;
132132-133133- return { ...acc, [handleId]: { ...(acc[handleId] || {}), [track.uri]: track } };
134134- },
135135- {},
136136- );
137137-138138- // Replace indexes in groups of which we have the handle.
139139- // Keeping around tracks with handles we don't have access to,
140140- // and removing tracks that are no longer available (for handles we do have access to).
141141- const groups = processed.flat(1).reduce(
142142- (acc, track) => {
143143- const handleId = trackHandleId(track);
144144- if (!handleId) throw new Error("New tracks are missing a handle id!");
145145-146146- return { ...acc, [handleId]: { ...acc[handleId], [track.uri]: track } };
147147- },
148148- handles.reduce((acc: Record<string, Record<string, Track>>, handle) => {
149149- return { ...acc, [handle.id]: {} };
150150- }, cache),
151151- );
152152-153153- // Transform in track list and sort by uri
154154- const data = Object.values(groups)
155155- .map((tracks) => Object.values(tracks))
156156- .flat(1)
157157- .sort((a: any, b: any) => {
158158- if (a.uri < b.uri) return -1;
159159- if (a.uri > b.uri) return 1;
160160- return 0;
161161- });
162162-163163- // Fin
164164- return data;
4747+ return await worker.call.list(cachedTracks);
16548 };
1664916750 const resolve = async (args: { uri: string }) => {
168168- const fileUri = args.uri;
169169-170170- if (!isSupported()) {
171171- return undefined;
172172- }
173173-174174- const uri = URI.parse(fileUri);
175175- if (uri.scheme !== SCHEME) return undefined;
176176- if (!uri.host || !uri.path) return undefined;
177177-178178- const handles = await fetchHandles();
179179- const handle = handles[uri.host];
180180- if (!handle) return undefined;
181181-182182- const path = URI.unescapeComponent(uri.path);
183183- const parts = (path.startsWith("/") ? path.slice(1) : path).split("/");
184184- const filename = parts[parts.length - 1];
185185-186186- const dirHandle = await parts
187187- .slice(0, -1)
188188- .reduce(
189189- async (
190190- acc: Promise<FileSystemDirectoryHandle>,
191191- part: string,
192192- ): Promise<FileSystemDirectoryHandle> => {
193193- const h = await acc;
194194- return await h.getDirectoryHandle(part);
195195- },
196196- Promise.resolve(handle),
197197- );
198198-199199- const fileHandle = await dirHandle.getFileHandle(filename);
200200- const file = await fileHandle.getFile();
201201- const url = URL.createObjectURL(file);
202202-203203- return { expiresAt: Infinity, url };
5151+ return await worker.call.resolve(args);
20452 };
2055320654 const mount = async () => {
207207- await showDirectoryPicker()
208208- .then(async (handle) => {
209209- const existingHandles = await fetchHandles();
210210- const id = crypto.randomUUID();
211211-212212- await handle.requestPermission({ mode: "read" });
213213- await IDB.set(IDB_HANDLES, { ...existingHandles, [id]: handle });
214214- setMounts(await fetchHandlesList());
215215- })
216216- .catch(() => {});
5555+ return Mounting.mount();
21756 };
2185721958 const unmount = async (handleId: string) => {
220220- const handles = await fetchHandles();
221221- delete handles[handleId];
222222- await IDB.set(IDB_HANDLES, { ...handles });
223223- setMounts(await fetchHandlesList());
5959+ return Mounting.unmount(handleId);
22460 };
2256122662 context.setActionHandler("consult", consult);
···23167 context.setActionHandler("unmount", unmount);
2326823369 ////////////////////////////////////////////
234234- // 🛠️
7070+ // UI
23571 ////////////////////////////////////////////
236236- async function fetchHandles(): Promise<Handles> {
237237- return (await IDB.get(IDB_HANDLES)) ?? {};
238238- }
239239-240240- async function fetchHandlesList() {
241241- return Object.entries(await fetchHandles()).map(([id, handle]) => {
242242- return { id, handle };
243243- });
244244- }
245245-246246- function isSupported() {
247247- return !!(globalThis as any).showDirectoryPicker;
248248- }
249249-250250- function trackCid(track: Track): string | undefined {
251251- const a = URI.parse(track.uri);
252252- const cid = a.query ? QS.parse(a.query).cid || undefined : undefined;
253253- return Array.isArray(cid) && cid[0] ? cid[0] : typeof cid === "string" ? cid : undefined;
254254- }
255255-256256- function trackHandleId(track: Track): string | undefined {
257257- const a = URI.parse(track.uri);
258258- return a.host;
259259- }
260260-261261- async function recursiveList(
262262- dir: FileSystemDirectoryHandle,
263263- rootHandleId: string,
264264- path: string[],
265265- ): Promise<Track[]> {
266266- const tracks: Track[] = [];
267267-268268- for await (const item of dir.values()) {
269269- if (item.kind === "file" && isAudioFile(item.name)) {
270270- const uri = URI.serialize({
271271- scheme: SCHEME,
272272- host: rootHandleId,
273273- path: `${path.length ? "/" + path.join("/") : ""}/${item.name}`,
274274- });
275275-276276- const track: Track = {
277277- id: crypto.randomUUID(),
278278- uri,
279279- };
280280-281281- tracks.push(track);
282282- } else if (item.kind === "directory") {
283283- const nestedItems = await recursiveList(item as FileSystemDirectoryHandle, rootHandleId, [
284284- ...path,
285285- item.name,
286286- ]);
287287-288288- tracks.push(...nestedItems);
289289- }
290290- }
291291-292292- return tracks;
293293- }
7272+ const ui = inIframe() ? undefined : await import("@scripts/input/native-fs/ui");
29473</script>
+4
src/pages/orchestrator/input-cache/_applet.astro
···4343 const output = await configurator.output;
44444545 const cachedTracks = output.data.tracks.collection;
4646+4747+ // TODO: Is there a better time to do this?
4848+ // Goal = figure out servers/buckets/context used,
4949+ // which are then used in the following `list` action.
4650 await input.sendAction("contextualize", cachedTracks, {
4751 timeoutDuration: 60000 * 5,
4852 });
+71
src/scripts/input/native-fs/common.ts
···11+import { type FileSystemDirectoryHandle } from "native-file-system-adapter";
22+import * as IDB from "idb-keyval";
33+import * as URI from "uri-js";
44+import QS from "query-string";
55+66+import type { Track } from "@applets/core/types.d.ts";
77+import type { Handles } from "./types";
88+import { isAudioFile } from "@scripts/input/common";
99+import { IDB_HANDLES, SCHEME } from "./constants";
1010+1111+////////////////////////////////////////////
1212+// 🛠️
1313+////////////////////////////////////////////
1414+export async function fetchHandles(): Promise<Handles> {
1515+ return (await IDB.get(IDB_HANDLES)) ?? {};
1616+}
1717+1818+export async function fetchHandlesList() {
1919+ return Object.entries(await fetchHandles()).map(([id, handle]) => {
2020+ return { id, handle };
2121+ });
2222+}
2323+2424+export function isSupported() {
2525+ return !!(globalThis as any).showDirectoryPicker;
2626+}
2727+2828+export function trackCid(track: Track): string | undefined {
2929+ const a = URI.parse(track.uri);
3030+ const cid = a.query ? QS.parse(a.query).cid || undefined : undefined;
3131+ return Array.isArray(cid) && cid[0] ? cid[0] : typeof cid === "string" ? cid : undefined;
3232+}
3333+3434+export function trackHandleId(track: Track): string | undefined {
3535+ const a = URI.parse(track.uri);
3636+ return a.host;
3737+}
3838+3939+export async function recursiveList(
4040+ dir: FileSystemDirectoryHandle,
4141+ rootHandleId: string,
4242+ path: string[],
4343+): Promise<Track[]> {
4444+ const tracks: Track[] = [];
4545+4646+ for await (const item of dir.values()) {
4747+ if (item.kind === "file" && isAudioFile(item.name)) {
4848+ const uri = URI.serialize({
4949+ scheme: SCHEME,
5050+ host: rootHandleId,
5151+ path: `${path.length ? "/" + path.join("/") : ""}/${item.name}`,
5252+ });
5353+5454+ const track: Track = {
5555+ id: crypto.randomUUID(),
5656+ uri,
5757+ };
5858+5959+ tracks.push(track);
6060+ } else if (item.kind === "directory") {
6161+ const nestedItems = await recursiveList(item as FileSystemDirectoryHandle, rootHandleId, [
6262+ ...path,
6363+ item.name,
6464+ ]);
6565+6666+ tracks.push(...nestedItems);
6767+ }
6868+ }
6969+7070+ return tracks;
7171+}
···11+import type { FileSystemDirectoryHandle } from "native-file-system-adapter";
22+33+export type Handles = Record<string, FileSystemDirectoryHandle>;