import React, { useCallback, useEffect, useRef, useState } from 'react'; import { api, fileUrl, fileToBase64 } from './api.js'; const isImage = (m) => /^image\//.test(m || ''); const isText = (f) => f.kind === 'text' || /^text\/|json|markdown|javascript|xml|csv/.test(f.mime || ''); const human = (n) => (n < 1024 ? `${n} B` : n < 1048576 ? `${(n / 1024).toFixed(1)} KB` : `${(n / 1048576).toFixed(1)} MB`); // Netdisk file browser. `onPick` (optional) turns rows into a picker for the whiteboard. export default function Netdisk({ onPick, compact }) { const [files, setFiles] = useState([]); const [folder, setFolder] = useState(''); const [sel, setSel] = useState(null); const [err, setErr] = useState(''); const [busy, setBusy] = useState(false); const fileInput = useRef(null); const load = useCallback(async () => { setErr(''); try { const d = await api('/files'); setFiles(d.files || []); } catch (e) { setErr(e.message); } }, []); useEffect(() => { load(); }, [load]); const folders = Array.from(new Set(['', ...files.map((f) => f.folder || '')])).sort(); const shown = files.filter((f) => (folder ? (f.folder || '') === folder : true)); const upload = async (fileList) => { setBusy(true); setErr(''); try { for (const file of fileList) { const data = await fileToBase64(file); await api('/files', { method: 'POST', body: { name: file.name, folder, mime: file.type || 'application/octet-stream', data } }); } await load(); } catch (e) { setErr(e.message); } finally { setBusy(false); } }; const newTextFile = async () => { const name = window.prompt('New text file name', 'note.md'); if (!name) return; try { await api('/files', { method: 'POST', body: { name, folder, kind: 'text', text: '', mime: name.endsWith('.md') ? 'text/markdown' : 'text/plain' } }); load(); } catch (e) { setErr(e.message); } }; const del = async (id) => { if (!window.confirm('Delete file?')) return; try { await api(`/files/${id}`, { method: 'DELETE' }); setSel(null); load(); } catch (e) { setErr(e.message); } }; return (
setFolder(e.target.value)} /> { upload([...e.target.files]); e.target.value = ''; }} />
{err &&

{err}

}
{shown.map((f) => (
setSel(f)}> {isImage(f.mime) ? '๐Ÿ–ผ' : isText(f) ? '๐Ÿ“„' : '๐Ÿ“ฆ'} {f.name} {f.folder || '/'} ยท {human(f.size)} {onPick && }
))} {!shown.length &&
no files โ€” upload or create one
}
{!compact && }
); } export function FilePreview({ file, onChange }) { const [editing, setEditing] = useState(false); const [text, setText] = useState(''); const [err, setErr] = useState(''); useEffect(() => { setEditing(false); setErr(''); if (file && (file.kind === 'text')) { api(`/files/${file.id}`).then((d) => setText(d.file.text_body || '')).catch((e) => setErr(e.message)); } }, [file]); if (!file) return
select a file
; const save = async () => { try { await api(`/files/${file.id}`, { method: 'PATCH', body: { text } }); setEditing(false); onChange && onChange(); } catch (e) { setErr(e.message); } }; return (
{file.name} {file.mime} ยท {human(file.size)} Open โ†— {file.kind === 'text' && !editing && } {editing && }
{err &&

{err}

}
{isImage(file.mime) && {file.name}} {file.kind === 'text' && editing &&