diff --git a/website/src/repl/FilesTab.jsx b/website/src/repl/FilesTab.jsx new file mode 100644 index 00000000..ed8d1215 --- /dev/null +++ b/website/src/repl/FilesTab.jsx @@ -0,0 +1,187 @@ +import { Fragment, useEffect } from 'react'; +import { getAudioContext, loadBuffer, processSampleMap } from '@strudel.cycles/webaudio'; +import React, { useMemo, useState } from 'react'; + +const TAURI = window.__TAURI__; +const { BaseDirectory, readDir, readBinaryFile, writeTextFile, readTextFile } = TAURI?.fs || {}; + +async function loadFiles() { + return readDir('', { dir: BaseDirectory.Audio, recursive: true }); +} + +const walkFileTree = (node, fn) => { + if (!Array.isArray(node?.children)) { + return; + } + for (const entry of node.children) { + entry.subpath = (node.subpath || []).concat([node.name]); + fn(entry, node); + if (entry.children) { + walkFileTree(entry, fn); + } + } +}; + +const isAudioFile = (filename) => ['wav', 'mp3'].includes(filename.split('.').slice(-1)[0]); +function uint8ArrayToDataURL(uint8Array) { + const blob = new Blob([uint8Array], { type: 'audio/*' }); + const dataURL = URL.createObjectURL(blob); + return dataURL; +} + +const loadCache = {}; // caches local urls to data urls +async function resolveFileURL(url) { + if (loadCache[url]) { + return loadCache[url]; + } + loadCache[url] = (async () => { + const contents = await readBinaryFile(url, { + dir: BaseDirectory.Audio, + }); + return uint8ArrayToDataURL(contents); + })(); + return loadCache[url]; +} + +export function FilesTab() { + const [path, setPath] = useState([]); + useEffect(() => { + let init = false; + loadFiles().then((_tree) => setPath([{ name: 'files', children: _tree, path: BaseDirectory.Audio }])); + return () => { + init = true; + }; + }, []); + const current = useMemo(() => path[path.length - 1], [path]); + const strudelJson = useMemo(() => current?.children?.find((e) => e.name === 'strudel.json'), [current]); + const subpath = useMemo( + () => + path + .slice(1) + .map((p) => p.name) + .join('/'), + [path], + ); + const folders = useMemo(() => current?.children.filter((e) => !!e.children), [current]); + + const files = useMemo( + () => current?.children.filter((e) => !e.children && isAudioFile(e.name) /* || e.name === 'strudel.json' */), + [current], + ); + const audioFiles = useMemo(() => { + let files = []; + walkFileTree(current, (entry) => { + if (isAudioFile(entry.name)) { + files.push(entry); + } + }); + return files; + }, [current]); + const select = (e) => setPath((p) => p.concat([e])); + + return ( +
+
+
+ {path?.map((p, i) => { + if (i < path.length - 1) { + return ( + + setPath((p) => p.slice(0, i + 1))}> + {p.name} + {' '} + /{' '} + + ); + } else { + return ( + + {p.name}{' '} + + ); + } + })} +
+
+
+ {folders?.map((e, i) => ( +
select(e)}> + {e.name} +
+ ))} + {files?.map((e, i) => ( +
{ + const url = await resolveFileURL(`${subpath}/${e.name}`); + const ac = getAudioContext(); + const bufferSource = ac.createBufferSource(); + bufferSource.buffer = await loadBuffer(url, ac); + bufferSource.connect(ac.destination); + bufferSource.start(ac.currentTime); + }} + > + {e.name} +
+ ))} +
+ +
+ + {strudelJson && ( + <> + + {/* */} + + )} +
+
+ ); +} diff --git a/website/src/repl/Footer.jsx b/website/src/repl/Footer.jsx index 417fb8ed..ed0efaef 100644 --- a/website/src/repl/Footer.jsx +++ b/website/src/repl/Footer.jsx @@ -9,6 +9,9 @@ import { themes } from './themes.mjs'; import { useSettings, settingsMap, setActiveFooter, defaultSettings } from '../settings.mjs'; import { getAudioContext, soundMap } from '@strudel.cycles/webaudio'; import { useStore } from '@nanostores/react'; +import { FilesTab } from './FilesTab'; + +const TAURI = window.__TAURI__; export function Footer({ context }) { const footerContent = useRef(); @@ -77,6 +80,7 @@ export function Footer({ context }) { + {TAURI && } {activeFooter !== '' && (