fix: update base path in Vite configuration to root
This commit is contained in:
parent
d7076f12da
commit
5a17c0a2e0
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<div>
|
||||
<h2 className="text-lg font-semibold mb-4 text-blue-700">Management</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<AppCard icon={<Folder className="w-7 h-7" />} label="File Manager" onClick={() => setCurrentPage('file-manager')} />
|
||||
<AppCard icon={<Folder className="w-7 h-7" />} label="Media Manager" onClick={() => setCurrentPage('file-manager')} />
|
||||
<AppCard icon={<Printer className="w-7 h-7" />} label="Print Manager" onClick={() => setCurrentPage('print-manager')} />
|
||||
<AppCard icon={<Terminal className="w-7 h-7" />} label="Serial Console" onClick={() => setCurrentPage('serial-console')} />
|
||||
<AppCard icon={<Link className="w-7 h-7" />} label="Short Codes" onClick={() => setCurrentPage('serial-console')} />
|
||||
|
|
@ -70,11 +70,11 @@ export default function App() {
|
|||
<div>
|
||||
<h2 className="text-lg font-semibold mb-4 text-blue-700">Disk</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<AppCard icon={<Database className="w-7 h-7" />} label="RAM/ROM Explorer" onClick={() => setCurrentPage('ramrom-explorer')} />
|
||||
<AppCard icon={<LayoutList className="w-7 h-7" />} label="BAM Editor" onClick={() => setCurrentPage('bam-editor')} />
|
||||
<AppCard icon={<Folder className="w-7 h-7" />} label="Directory Editor" onClick={() => setCurrentPage('directory-editor')} />
|
||||
<AppCard icon={<Edit className="w-7 h-7" />} label="Sector Editor" onClick={() => setCurrentPage('sector-editor')} />
|
||||
<AppCard icon={<LayoutList className="w-7 h-7" />} label="BAM Editor" onClick={() => setCurrentPage('bam-editor')} />
|
||||
<AppCard icon={<Eye className="w-7 h-7" />} label="Disk Visualizer" onClick={() => setCurrentPage('disk-visualizer')} />
|
||||
<AppCard icon={<Database className="w-7 h-7" />} label="RAM/ROM Explorer" onClick={() => setCurrentPage('ramrom-explorer')} />
|
||||
<AppCard icon={<Download className="w-7 h-7" />} label="Dump Disk Image" onClick={() => setCurrentPage('dump-disk-image')} />
|
||||
<AppCard icon={<Upload className="w-7 h-7" />} label="Write Disk Image" onClick={() => setCurrentPage('write-disk-image')} />
|
||||
</div>
|
||||
|
|
@ -111,13 +111,13 @@ export default function App() {
|
|||
</div>
|
||||
),
|
||||
// Individual app pages
|
||||
'file-manager': <FileManager
|
||||
'file-manager': <MediaManager
|
||||
onBack={() => setCurrentPage('apps')}
|
||||
config={config}
|
||||
setConfig={setConfig}
|
||||
onNavigateToDevice={(id) => { setCurrentPage('devices'); setDevicesOpenId(id); }}
|
||||
/>,
|
||||
'print-manager': <FileManager
|
||||
'print-manager': <MediaManager
|
||||
title="Print Manager"
|
||||
rootPath="/sd/.print"
|
||||
onBack={() => setCurrentPage('apps')}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowFileBrowser(true)}
|
||||
onClick={() => setShowMediaBrowser(true)}
|
||||
className="px-3 py-2 border border-neutral-300 rounded-lg bg-neutral-50 hover:bg-neutral-100"
|
||||
>
|
||||
<FolderOpen className="w-5 h-5" />
|
||||
|
|
@ -395,11 +395,11 @@ export default function DeviceDetailOverlay({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{showFileBrowser && (
|
||||
<FileBrowser
|
||||
{showMediaBrowser && (
|
||||
<MediaBrowser
|
||||
currentPath={deviceData.url || '/'}
|
||||
onSelect={(path) => { void handleFileSelect(path); setShowFileBrowser(false); }}
|
||||
onClose={() => setShowFileBrowser(false)}
|
||||
onSelect={(path) => { void handleFileSelect(path); setShowMediaBrowser(false); }}
|
||||
onClose={() => setShowMediaBrowser(false)}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowFileBrowser(true)}
|
||||
onClick={() => setShowMediaBrowser(true)}
|
||||
className="px-3 py-2 border border-neutral-300 rounded-lg bg-neutral-50 hover:bg-neutral-100"
|
||||
title="Browse files"
|
||||
>
|
||||
|
|
@ -79,14 +79,14 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{showFileBrowser && (
|
||||
<FileBrowser
|
||||
{showMediaBrowser && (
|
||||
<MediaBrowser
|
||||
currentPath={iec.boot_disk || '/'}
|
||||
onSelect={(path) => {
|
||||
updateSetting(['iec', 'boot_disk'], path);
|
||||
setShowFileBrowser(false);
|
||||
setShowMediaBrowser(false);
|
||||
}}
|
||||
onClose={() => setShowFileBrowser(false)}
|
||||
onClose={() => setShowMediaBrowser(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -50,13 +50,13 @@ function EntryIcon({ entry }: { entry: EntryInfo }) {
|
|||
return <File className="w-5 h-5 text-neutral-400 flex-shrink-0" />;
|
||||
}
|
||||
|
||||
interface FileBrowserProps {
|
||||
interface MediaBrowserProps {
|
||||
currentPath: string;
|
||||
onSelect: (path: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function FileBrowser({ currentPath, onSelect, onClose }: FileBrowserProps) {
|
||||
export default function MediaBrowser({ currentPath, onSelect, onClose }: MediaBrowserProps) {
|
||||
// Resolve the initial path: if `currentPath` is itself a file, jump
|
||||
// to its parent so we never try to list a file as if it were a folder.
|
||||
const [path, setPath] = useState<string | null>(null);
|
||||
|
|
@ -1,17 +1,20 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
AlignLeft,
|
||||
ArrowLeft,
|
||||
BookOpen,
|
||||
Braces,
|
||||
Check,
|
||||
CheckSquare,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ClipboardPaste,
|
||||
Code2,
|
||||
Copy,
|
||||
Download,
|
||||
Eye,
|
||||
File,
|
||||
FilePlus,
|
||||
FileText,
|
||||
Folder,
|
||||
FolderPlus,
|
||||
|
|
@ -20,6 +23,7 @@ import {
|
|||
Home,
|
||||
Image as ImageIcon,
|
||||
Loader2,
|
||||
Menu,
|
||||
MoreVertical,
|
||||
Move,
|
||||
Pencil,
|
||||
|
|
@ -243,9 +247,151 @@ function EntryIcon({ entry }: { entry: EntryInfo }) {
|
|||
return <File className="w-5 h-5 text-neutral-400 flex-shrink-0" />;
|
||||
}
|
||||
|
||||
// ─── ActionsModal ─────────────────────────────────────────────────────────────
|
||||
|
||||
interface FolderManagementActions {
|
||||
onNewFolder: () => void;
|
||||
onNewFile: () => void;
|
||||
onUpload: () => void;
|
||||
clipboard: Clipboard | null;
|
||||
onPaste: () => void;
|
||||
selectedCount: number;
|
||||
totalCount: number;
|
||||
onSelectAll: () => void;
|
||||
isRoot: boolean;
|
||||
}
|
||||
|
||||
interface ActionsModalProps {
|
||||
entry: EntryInfo | null;
|
||||
onClose: () => void;
|
||||
onOpen: (entry: EntryInfo, mode?: ViewMode) => void;
|
||||
onMount: (entry: EntryInfo) => void;
|
||||
onDownload: (entry: EntryInfo) => void;
|
||||
onRename: (entry: EntryInfo) => void;
|
||||
onCopy: (entry: EntryInfo) => void;
|
||||
onCut: (entry: EntryInfo) => void;
|
||||
onDelete: (entry: EntryInfo) => void;
|
||||
folderManagement?: FolderManagementActions;
|
||||
}
|
||||
|
||||
function ActionsModal({ entry, onClose, onOpen, onMount, onDownload, onRename, onCopy, onCut, onDelete, folderManagement }: ActionsModalProps) {
|
||||
const isFolder = entry?.type === 'folder';
|
||||
const fm = folderManagement;
|
||||
|
||||
return (
|
||||
<Dialog open={entry !== null} onOpenChange={open => !open && onClose()}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="truncate">{entry?.name || '/'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isFolder ? 'Folder' : humanFileSize(entry?.size ?? 0)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{entry && (
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
{/* Folder management items — current-folder context (header Actions) */}
|
||||
{fm && (
|
||||
<>
|
||||
<button onClick={() => { onClose(); fm.onNewFolder(); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
|
||||
<FolderPlus className="w-4 h-4 text-neutral-500" /> <span>New Folder</span>
|
||||
</button>
|
||||
<button onClick={() => { onClose(); fm.onNewFile(); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
|
||||
<FilePlus className="w-4 h-4 text-neutral-500" /> <span>New File</span>
|
||||
</button>
|
||||
<button onClick={() => { onClose(); fm.onUpload(); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
|
||||
<Upload className="w-4 h-4 text-neutral-500" /> <span>Upload Files</span>
|
||||
</button>
|
||||
{fm.clipboard && (
|
||||
<button onClick={() => { onClose(); fm.onPaste(); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
|
||||
<ClipboardPaste className="w-4 h-4 text-neutral-500" />
|
||||
<span>{fm.clipboard.op === 'copy' ? 'Copy' : 'Move'} {fm.clipboard.paths.length} item{fm.clipboard.paths.length !== 1 ? 's' : ''} here</span>
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => { onClose(); fm.onSelectAll(); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
|
||||
<CheckSquare className="w-4 h-4 text-neutral-500" />
|
||||
<span>{fm.selectedCount === fm.totalCount && fm.totalCount > 0 ? 'Deselect All' : 'Select All'}</span>
|
||||
</button>
|
||||
{!fm.isRoot && <div className="border-t border-neutral-100" />}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Open folder — list item only (not current-folder context) */}
|
||||
{isFolder && !fm && (
|
||||
<button onClick={() => { onClose(); onOpen(entry); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-blue-50 hover:border-blue-300 inline-flex items-center gap-3">
|
||||
<Folder className="w-4 h-4 text-blue-600" /> <span>Open folder</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* File actions */}
|
||||
{!isFolder && (
|
||||
<>
|
||||
<button onClick={() => { onClose(); onMount(entry); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-blue-50 hover:border-blue-300 inline-flex items-center gap-3">
|
||||
<HardDrive className="w-4 h-4 text-amber-600" /> <span>Mount on virtual drive</span>
|
||||
</button>
|
||||
<button onClick={() => { onClose(); onOpen(entry); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
|
||||
<Eye className="w-4 h-4 text-blue-600" />
|
||||
<span className="flex-1">Open / View</span>
|
||||
<span className="text-xs text-neutral-400">{VIEWER_LABEL[defaultViewMode(entry)]}</span>
|
||||
</button>
|
||||
{availableViewers(entry).filter(m => m !== defaultViewMode(entry)).map(mode => (
|
||||
<button key={mode} onClick={() => { onClose(); onOpen(entry, mode); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
|
||||
<ViewerModeIcon mode={mode} className="w-4 h-4 text-neutral-500" />
|
||||
<span>Open as {VIEWER_LABEL[mode]}</span>
|
||||
</button>
|
||||
))}
|
||||
<button onClick={() => { onClose(); onDownload(entry); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
|
||||
<Download className="w-4 h-4" /> <span>Download</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Rename / Copy / Move / Delete */}
|
||||
{(!fm || !fm.isRoot) && (
|
||||
<>
|
||||
<div className="border-t border-neutral-100" />
|
||||
<button onClick={() => { onClose(); onRename(entry); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
|
||||
<Pencil className="w-4 h-4" /> <span>Rename</span>
|
||||
</button>
|
||||
{!fm && (
|
||||
<>
|
||||
<button onClick={() => { onClose(); onCopy(entry); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
|
||||
<Copy className="w-4 h-4" /> <span>Copy</span>
|
||||
</button>
|
||||
<button onClick={() => { onClose(); onCut(entry); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
|
||||
<Move className="w-4 h-4" /> <span>Move (Cut)</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button onClick={() => { onClose(); onDelete(entry); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-red-200 hover:bg-red-50 text-red-700 inline-flex items-center gap-3">
|
||||
<Trash2 className="w-4 h-4" /> <span>Delete</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Props ────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface FileManagerProps {
|
||||
interface MediaManagerProps {
|
||||
initialPath?: string;
|
||||
rootPath?: string;
|
||||
title?: string;
|
||||
|
|
@ -305,7 +451,7 @@ async function _getEntryBytes(entry: EntryInfo): Promise<Uint8Array> {
|
|||
|
||||
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<EntryInfo[]>([]);
|
||||
|
|
@ -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<Clipboard | null>(null);
|
||||
const [actionEntry, setActionEntry] = useState<EntryInfo | null>(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<EntryInfo | null>(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
|
|||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
<h2 className="font-semibold flex-1 text-sm">{title ?? 'File Manager'}</h2>
|
||||
<h2 className="font-semibold flex-1 text-sm">{title ?? 'Media Manager'}</h2>
|
||||
<button onClick={() => void load(path)} className="p-1.5 rounded hover:bg-neutral-100" title="Refresh">
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => setShowNewFolder(v => !v)} className="p-1.5 rounded hover:bg-neutral-100" title="New Folder">
|
||||
<button onClick={() => { setShowNewFile(v => !v); setShowNewFolder(false); }} className="p-1.5 rounded hover:bg-neutral-100" title="New File">
|
||||
<FilePlus className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => { setShowNewFolder(v => !v); setShowNewFile(false); }} className="p-1.5 rounded hover:bg-neutral-100" title="New Folder">
|
||||
<FolderPlus className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => fileInputRef.current?.click()} className="p-1.5 rounded hover:bg-neutral-100" title="Upload">
|
||||
<Upload className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => setFolderActionOpen(true)} className="p-1.5 rounded hover:bg-neutral-100" title="Actions">
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
</button>
|
||||
<input ref={fileInputRef} type="file" multiple className="hidden" onChange={onPickFiles} />
|
||||
</div>
|
||||
|
||||
|
|
@ -708,7 +894,28 @@ export default function FileManager({ initialPath = '/', rootPath, title, config
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
{deviceBaseUrl && (
|
||||
<div className="text-xs text-neutral-400 mt-0.5 truncate">Base: {deviceBaseUrl}</div>
|
||||
)}
|
||||
|
||||
{showNewFile && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<input
|
||||
value={newFileName}
|
||||
onChange={e => 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
|
||||
/>
|
||||
<button onClick={() => void handleCreateFile()} className="px-3 py-1 text-sm bg-blue-600 text-white rounded">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{showNewFolder && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<input
|
||||
|
|
@ -921,105 +1128,31 @@ export default function FileManager({ initialPath = '/', rootPath, title, config
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Per-entry action menu ── */}
|
||||
<Dialog open={actionEntry !== null} onOpenChange={open => !open && setActionEntry(null)}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="truncate">{actionEntry?.name}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{actionEntry?.type === 'folder' ? 'Folder' : humanFileSize(actionEntry?.size ?? 0)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Folder: open */}
|
||||
{actionEntry?.type === 'folder' && (
|
||||
<button
|
||||
onClick={() => { const e = actionEntry; setActionEntry(null); if (e) void openEntry(e); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-blue-50 hover:border-blue-300 inline-flex items-center gap-3"
|
||||
>
|
||||
<Folder className="w-4 h-4 text-blue-600" />
|
||||
<span>Open folder</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* File: mount (primary default) */}
|
||||
{actionEntry?.type === 'file' && (
|
||||
<button
|
||||
onClick={() => { const e = actionEntry; setActionEntry(null); if (e) setMountEntry(e); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-blue-50 hover:border-blue-300 inline-flex items-center gap-3"
|
||||
>
|
||||
<HardDrive className="w-4 h-4 text-amber-600" />
|
||||
<span>Mount on virtual drive</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* File: open/view */}
|
||||
{actionEntry?.type === 'file' && (
|
||||
<button
|
||||
onClick={() => { const e = actionEntry; setActionEntry(null); if (e) void openEntry(e); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3"
|
||||
>
|
||||
<Eye className="w-4 h-4 text-blue-600" />
|
||||
<span className="flex-1">Open / View</span>
|
||||
<span className="text-xs text-neutral-400">{VIEWER_LABEL[defaultViewMode(actionEntry)]}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Alternate viewers */}
|
||||
{actionEntry?.type === 'file' && availableViewers(actionEntry)
|
||||
.filter(m => m !== defaultViewMode(actionEntry))
|
||||
.map(mode => {
|
||||
const entry = actionEntry;
|
||||
return (
|
||||
<button
|
||||
key={mode}
|
||||
onClick={() => { setActionEntry(null); void openEntry(entry, mode); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3"
|
||||
>
|
||||
<ViewerModeIcon mode={mode} className="w-4 h-4 text-neutral-500" />
|
||||
<span>Open as {VIEWER_LABEL[mode]}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{actionEntry?.type === 'file' && (
|
||||
<button
|
||||
onClick={() => { const e = actionEntry; setActionEntry(null); if (e) void downloadEntry(e); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3"
|
||||
>
|
||||
<Download className="w-4 h-4" /> <span>Download</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="border-t border-neutral-100" />
|
||||
|
||||
<button
|
||||
onClick={() => { if (actionEntry) startRename(actionEntry); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3"
|
||||
>
|
||||
<Pencil className="w-4 h-4" /> <span>Rename</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { if (actionEntry) cutOrCopyEntry(actionEntry, 'copy'); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3"
|
||||
>
|
||||
<Copy className="w-4 h-4" /> <span>Copy</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { if (actionEntry) cutOrCopyEntry(actionEntry, 'move'); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3"
|
||||
>
|
||||
<Move className="w-4 h-4" /> <span>Move (Cut)</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { const e = actionEntry; setActionEntry(null); if (e) void deleteEntry(e); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-red-200 hover:bg-red-50 text-red-700 inline-flex items-center gap-3"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" /> <span>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/* ── Actions modal (per-entry + current-folder context) ── */}
|
||||
<ActionsModal
|
||||
entry={folderActionOpen
|
||||
? { name: splitPath(path).name || '/', path, type: 'folder', size: 0, lastModified: null, contentType: null }
|
||||
: actionEntry}
|
||||
onClose={() => { 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 && (
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user