From 5a17c0a2e0aa0a5cc7bb3bc531d799e2657b1cef Mon Sep 17 00:00:00 2001 From: Jaime Idolpx Date: Mon, 8 Jun 2026 12:06:29 -0400 Subject: [PATCH] fix: update base path in Vite configuration to root --- AGENTS.md | 4 +- src/app/App.tsx | 12 +- src/app/components/DeviceDetailOverlay.tsx | 14 +- src/app/components/IECPage.tsx | 14 +- .../{FileBrowser.tsx => MediaBrowser.tsx} | 4 +- .../{FileManager.tsx => MediaManager.tsx} | 341 ++++++++++++------ vite.config.ts | 2 +- 7 files changed, 262 insertions(+), 129 deletions(-) rename src/app/components/{FileBrowser.tsx => MediaBrowser.tsx} (99%) rename src/app/components/{FileManager.tsx => MediaManager.tsx} (80%) diff --git a/AGENTS.md b/AGENTS.md index bf34d20..a34df19 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,7 @@ src/ ToolsPage.tsx # Tools SearchOverlay.tsx WiFiScanOverlay.tsx - FileBrowser.tsx # WebDAV file browser (file click = select, kebab menu) + MediaBrowser.tsx # WebDAV file browser (file click = select, kebab menu) figma/ # Figma-generated components ui/ # shadcn/Radix UI wrappers vendor/ @@ -93,7 +93,7 @@ All individual app pages are currently stubs (`AppPage` component with "coming s 11. **WebDAV client** — replaced `webdav@5` npm package with vendored `webdav-component` (browser-native `fetch` + `DOMParser`, no external deps). All pages import from `webdav.ts` abstraction layer. 12. **WebDAV path fixes** — `webdav.ts`: always `decodeURIComponent` paths; use `entry.uri` (not broken `entry.path`) for servers returning relative hrefs 13. **`webdav3.py` server fixes** — `displayname` now returns leaf name only (not full path); PROPFIND depth-1 guard prevents crash when called on a file -14. **FileBrowser redesign** — file click = `onSelect` + close; folder click = navigate; per-row kebab (`MoreVert`) opens a Dialog with contextual actions; permanent "Select Folder" button in footer; no mode-toggle buttons +14. **MediaBrowser redesign** — file click = `onSelect` + close; folder click = navigate; per-row kebab (`MoreVert`) opens a Dialog with contextual actions; permanent "Select Folder" button in footer; no mode-toggle buttons 15. **Settings persistence** — `settings.ts` + `useSettings()` hook: loads `/.sys/config.json` via WebDAV on mount, auto-saves 3 s after last change, exposes `saveStatus` / `pendingCount` / `flushNow`; `beforeunload` flushes via `fetch keepalive` 16. **Save-status badge** — `SaveStatusBadge` in `App.tsx` header shows: idle (hidden), loading spinner, amber "N unsaved + Save button", saving spinner, saved checkmark, red error + retry diff --git a/src/app/App.tsx b/src/app/App.tsx index c168a7a..f6f1ebd 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -9,7 +9,7 @@ import IECPage from './components/IECPage'; import OtherPage from './components/OtherPage'; import ToolsPage from './components/ToolsPage'; import SearchOverlay from './components/SearchOverlay'; -import FileManager from './components/FileManager'; +import MediaManager from './components/MediaManager'; import logoSvg from '../imports/logo.svg'; import { useSettings } from './settings'; @@ -60,7 +60,7 @@ export default function App() {

Management

- } label="File Manager" onClick={() => setCurrentPage('file-manager')} /> + } label="Media Manager" onClick={() => setCurrentPage('file-manager')} /> } label="Print Manager" onClick={() => setCurrentPage('print-manager')} /> } label="Serial Console" onClick={() => setCurrentPage('serial-console')} /> } label="Short Codes" onClick={() => setCurrentPage('serial-console')} /> @@ -70,11 +70,11 @@ export default function App() {

Disk

+ } label="RAM/ROM Explorer" onClick={() => setCurrentPage('ramrom-explorer')} /> + } label="BAM Editor" onClick={() => setCurrentPage('bam-editor')} /> } label="Directory Editor" onClick={() => setCurrentPage('directory-editor')} /> } label="Sector Editor" onClick={() => setCurrentPage('sector-editor')} /> - } label="BAM Editor" onClick={() => setCurrentPage('bam-editor')} /> } label="Disk Visualizer" onClick={() => setCurrentPage('disk-visualizer')} /> - } label="RAM/ROM Explorer" onClick={() => setCurrentPage('ramrom-explorer')} /> } label="Dump Disk Image" onClick={() => setCurrentPage('dump-disk-image')} /> } label="Write Disk Image" onClick={() => setCurrentPage('write-disk-image')} />
@@ -111,13 +111,13 @@ export default function App() {
), // Individual app pages - 'file-manager': setCurrentPage('apps')} config={config} setConfig={setConfig} onNavigateToDevice={(id) => { setCurrentPage('devices'); setDevicesOpenId(id); }} />, - 'print-manager': setCurrentPage('apps')} diff --git a/src/app/components/DeviceDetailOverlay.tsx b/src/app/components/DeviceDetailOverlay.tsx index 8d34417..dbf0a7e 100644 --- a/src/app/components/DeviceDetailOverlay.tsx +++ b/src/app/components/DeviceDetailOverlay.tsx @@ -3,7 +3,7 @@ import { X, ChevronLeft, ChevronRight, Printer, HardDrive, Network, Box, FolderO import { motion, AnimatePresence } from 'motion/react'; import { toast } from 'sonner'; import { getFileContents, joinPath } from '../webdav'; -import FileBrowser from './FileBrowser'; +import MediaBrowser from './MediaBrowser'; import MediaSet from './MediaSet'; interface Device { @@ -37,7 +37,7 @@ export default function DeviceDetailOverlay({ }: DeviceDetailOverlayProps) { const [touchStart, setTouchStart] = useState(0); const [touchEnd, setTouchEnd] = useState(0); - const [showFileBrowser, setShowFileBrowser] = useState(false); + const [showMediaBrowser, setShowMediaBrowser] = useState(false); const [showCommandMenu, setShowCommandMenu] = useState(false); const minSwipeDistance = 50; @@ -332,7 +332,7 @@ export default function DeviceDetailOverlay({ className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg" />
- {showFileBrowser && ( - { void handleFileSelect(path); setShowFileBrowser(false); }} - onClose={() => setShowFileBrowser(false)} + onSelect={(path) => { void handleFileSelect(path); setShowMediaBrowser(false); }} + onClose={() => setShowMediaBrowser(false)} /> )} diff --git a/src/app/components/IECPage.tsx b/src/app/components/IECPage.tsx index 372c986..c610ec3 100644 --- a/src/app/components/IECPage.tsx +++ b/src/app/components/IECPage.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { FolderOpen } from 'lucide-react'; -import FileBrowser from './FileBrowser'; +import MediaBrowser from './MediaBrowser'; interface IECPageProps { config: any; @@ -8,7 +8,7 @@ interface IECPageProps { } export default function IECPage({ config, setConfig }: IECPageProps) { - const [showFileBrowser, setShowFileBrowser] = useState(false); + const [showMediaBrowser, setShowMediaBrowser] = useState(false); const updateSetting = (path: string[], value: any) => { const newConfig = JSON.parse(JSON.stringify(config)); let current = newConfig; @@ -70,7 +70,7 @@ export default function IECPage({ config, setConfig }: IECPageProps) { className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg" /> + + + {fm.clipboard && ( + + )} + + {!fm.isRoot &&
} + + )} + + {/* Open folder — list item only (not current-folder context) */} + {isFolder && !fm && ( + + )} + + {/* File actions */} + {!isFolder && ( + <> + + + {availableViewers(entry).filter(m => m !== defaultViewMode(entry)).map(mode => ( + + ))} + + + )} + + {/* Rename / Copy / Move / Delete */} + {(!fm || !fm.isRoot) && ( + <> +
+ + {!fm && ( + <> + + + + )} + + + )} +
+ )} + + + ); +} + // ─── Props ──────────────────────────────────────────────────────────────────── -interface FileManagerProps { +interface MediaManagerProps { initialPath?: string; rootPath?: string; title?: string; @@ -305,7 +451,7 @@ async function _getEntryBytes(entry: EntryInfo): Promise { const FM_PATH_KEY = 'fileManager.path'; -export default function FileManager({ initialPath = '/', rootPath, title, config, setConfig, onBack, onNavigateToDevice }: FileManagerProps) { +export default function MediaManager({ initialPath = '/', rootPath, title, config, setConfig, onBack, onNavigateToDevice }: MediaManagerProps) { const pathKey = rootPath ? `fileManager.path:${rootPath}` : FM_PATH_KEY; const [path, setPath] = useState(() => normalizePath(localStorage.getItem(pathKey) || rootPath || initialPath)); const [entries, setEntries] = useState([]); @@ -317,7 +463,10 @@ export default function FileManager({ initialPath = '/', rootPath, title, config const [sortAsc, setSortAsc] = useState(() => localStorage.getItem('fileManager.sortAsc') !== 'false'); const [clipboard, setClipboard] = useState(null); const [actionEntry, setActionEntry] = useState(null); + const [folderActionOpen, setFolderActionOpen] = useState(false); const [dragOver, setDragOver] = useState(false); + const [showNewFile, setShowNewFile] = useState(false); + const [newFileName, setNewFileName] = useState(''); // Viewer const [viewEntry, setViewEntry] = useState(null); @@ -514,6 +663,7 @@ export default function FileManager({ initialPath = '/', rootPath, title, config setRenameEntry(entry); setRenameName(entry.name); setActionEntry(null); + setFolderActionOpen(false); setTimeout(() => renameInputRef.current?.focus(), 50); }; @@ -626,6 +776,19 @@ export default function FileManager({ initialPath = '/', rootPath, title, config } catch (e: any) { toast.error(`Failed to create folder: ${e?.message ?? e}`); } }; + // ── New file ───────────────────────────────────────────────────────────── + + const handleCreateFile = async () => { + const name = newFileName.trim(); + if (!name) return; + try { + await putFileContents(joinPath(path, name), new Uint8Array(0)); + toast.success(`Created "${name}"`); + setShowNewFile(false); setNewFileName(''); + void load(path); + } catch (e: any) { toast.error(`Failed to create file: ${e?.message ?? e}`); } + }; + // ── Upload ─────────────────────────────────────────────────────────────── const handleUpload = async (file: File) => { @@ -660,6 +823,23 @@ export default function FileManager({ initialPath = '/', rootPath, title, config const pathParts = path.split('/').filter(Boolean); const selCount = selected.size; + const deviceBaseUrl = useMemo(() => { + if (!config?.iec?.devices) return null; + const groups = ['drive', 'meatloaf', 'printer', 'network', 'other']; + for (const t of groups) { + for (const dev of Object.values(config.iec.devices[t] ?? {}) as any[]) { + if (dev?.base_url && (path.startsWith(dev.base_url) || dev.base_url === rootPath)) + return dev.base_url as string; + } + } + for (const t of groups) { + for (const dev of Object.values(config.iec.devices[t] ?? {}) as any[]) { + if (dev?.base_url) return dev.base_url as string; + } + } + return null; + }, [config, path, rootPath]); + // ── Render ─────────────────────────────────────────────────────────────── return ( @@ -678,16 +858,22 @@ export default function FileManager({ initialPath = '/', rootPath, title, config )} -

{title ?? 'File Manager'}

+

{title ?? 'Media Manager'}

- + +
@@ -708,7 +894,28 @@ export default function FileManager({ initialPath = '/', rootPath, title, config ))} + {deviceBaseUrl && ( +
Base: {deviceBaseUrl}
+ )} + {showNewFile && ( +
+ setNewFileName(e.target.value)} + onKeyDown={e => { + if (e.key === 'Enter') void handleCreateFile(); + if (e.key === 'Escape') { setShowNewFile(false); setNewFileName(''); } + }} + placeholder="New file name" + className="flex-1 px-2 py-1 text-sm border border-neutral-300 rounded" + autoFocus + /> + +
+ )} {showNewFolder && (
)} - {/* ── Per-entry action menu ── */} - !open && setActionEntry(null)}> - - - {actionEntry?.name} - - {actionEntry?.type === 'folder' ? 'Folder' : humanFileSize(actionEntry?.size ?? 0)} - - -
- {/* Folder: open */} - {actionEntry?.type === 'folder' && ( - - )} - - {/* File: mount (primary default) */} - {actionEntry?.type === 'file' && ( - - )} - - {/* File: open/view */} - {actionEntry?.type === 'file' && ( - - )} - - {/* Alternate viewers */} - {actionEntry?.type === 'file' && availableViewers(actionEntry) - .filter(m => m !== defaultViewMode(actionEntry)) - .map(mode => { - const entry = actionEntry; - return ( - - ); - })} - - {actionEntry?.type === 'file' && ( - - )} - -
- - - - - -
- -
+ {/* ── Actions modal (per-entry + current-folder context) ── */} + { setActionEntry(null); setFolderActionOpen(false); }} + onOpen={(e, mode) => void openEntry(e, mode)} + onMount={e => setMountEntry(e)} + onDownload={e => void downloadEntry(e)} + onRename={e => startRename(e)} + onCopy={e => cutOrCopyEntry(e, 'copy')} + onCut={e => cutOrCopyEntry(e, 'move')} + onDelete={e => void deleteEntry(e)} + folderManagement={folderActionOpen ? { + onNewFolder: () => { setShowNewFolder(true); setShowNewFile(false); }, + onNewFile: () => { setShowNewFile(true); setShowNewFolder(false); }, + onUpload: () => fileInputRef.current?.click(), + clipboard, + onPaste: () => void paste(), + selectedCount: selected.size, + totalCount: visible.length, + onSelectAll: selectAll, + isRoot: path === (rootPath ?? '/'), + } : undefined} + /> {/* ── File viewer overlay ── */} {viewEntry && ( diff --git a/vite.config.ts b/vite.config.ts index e282c62..7adbf69 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,7 +17,7 @@ function figmaAssetResolver() { } export default defineConfig({ - base: process.env.BASE_PATH || '/config/', + base: process.env.BASE_PATH || '/', plugins: [ figmaAssetResolver(), // The React and Tailwind plugins are both required for Make, even if