diff --git a/src/app/components/MediaBrowser.tsx b/src/app/components/MediaBrowser.tsx index deab65b..fd35a66 100644 --- a/src/app/components/MediaBrowser.tsx +++ b/src/app/components/MediaBrowser.tsx @@ -33,6 +33,7 @@ import { DialogDescription, } from './ui/dialog'; import { MediaEntry } from './MediaEntry'; +import { ConfirmDialog, type ConfirmOptions } from './ui/confirm-dialog'; interface MediaBrowserProps { currentPath: string; @@ -49,6 +50,7 @@ export default function MediaBrowser({ currentPath, onSelect, onClose }: MediaBr const [newFolderName, setNewFolderName] = useState(''); const [actionEntry, setActionEntry] = useState(null); const fileInputRef = useRef(null); + const [confirm, setConfirm] = useState(null); useEffect(() => { const initial = normalizePath(currentPath || '/'); @@ -91,21 +93,38 @@ export default function MediaBrowser({ currentPath, onSelect, onClose }: MediaBr } catch (e: any) { toast.error(`Failed to create folder: ${e?.message ?? e}`); } }; - const handleDelete = async (entry: EntryInfo) => { - if (!window.confirm(entry.type === 'folder' ? `Delete folder "${entry.name}" and all its contents?` : `Delete file "${entry.name}"?`)) return; + const handleDelete = (entry: EntryInfo) => { setActionEntry(null); - try { await deletePath(entry.path); toast.success(`Deleted ${entry.name}`); if (path) void load(path); } - catch (e: any) { toast.error(`Failed to delete: ${e?.message ?? e}`); } + setConfirm({ + title: entry.type === 'folder' ? `Delete folder "${entry.name}"?` : `Delete "${entry.name}"?`, + description: entry.type === 'folder' ? 'This will delete the folder and all its contents.' : undefined, + confirmLabel: 'Delete', + destructive: true, + onConfirm: async () => { + try { await deletePath(entry.path); toast.success(`Deleted ${entry.name}`); if (path) void load(path); } + catch (e: any) { toast.error(`Failed to delete: ${e?.message ?? e}`); } + }, + }); }; const handleUpload = async (file: File) => { if (!path) return; const target = joinPath(path, file.name); + const doUpload = async () => { + try { await putFileContents(target, await file.arrayBuffer()); toast.success(`Uploaded ${file.name}`); void load(path!); } + catch (e: any) { toast.error(`Failed to upload ${file.name}: ${e?.message ?? e}`); } + }; if (await fileExists(target)) { - if (!window.confirm(`"${file.name}" already exists. Overwrite?`)) return; + setConfirm({ + title: `"${file.name}" already exists`, + description: 'Replace the existing file?', + confirmLabel: 'Overwrite', + destructive: true, + onConfirm: doUpload, + }); + return; } - try { await putFileContents(target, await file.arrayBuffer()); toast.success(`Uploaded ${file.name}`); void load(path); } - catch (e: any) { toast.error(`Failed to upload ${file.name}: ${e?.message ?? e}`); } + await doUpload(); }; const onPickFiles = (e: React.ChangeEvent) => { @@ -200,6 +219,12 @@ export default function MediaBrowser({ currentPath, onSelect, onClose }: MediaBr + {} })} + onCancel={() => setConfirm(null)} + /> + !open && setActionEntry(null)}> diff --git a/src/app/components/MediaManager.tsx b/src/app/components/MediaManager.tsx index 5eba12d..f75aa15 100644 --- a/src/app/components/MediaManager.tsx +++ b/src/app/components/MediaManager.tsx @@ -61,6 +61,7 @@ import { DialogTitle, DialogDescription, } from './ui/dialog'; +import { ConfirmDialog, type ConfirmOptions } from './ui/confirm-dialog'; // ─── Types ─────────────────────────────────────────────────────────────────── @@ -372,6 +373,7 @@ export default function MediaManager({ initialPath = '/', rootPath, title, confi const fileInputRef = useRef(null); const renameInputRef = useRef(null); const dragCounter = useRef(0); + const [confirm, setConfirm] = useState(null); // ── Directory loading ──────────────────────────────────────────────────── @@ -564,23 +566,34 @@ export default function MediaManager({ initialPath = '/', rootPath, title, confi // ── Delete ─────────────────────────────────────────────────────────────── - const deleteEntry = async (entry: EntryInfo) => { - if (!window.confirm(entry.type === 'folder' - ? `Delete folder "${entry.name}" and all its contents?` - : `Delete "${entry.name}"?`)) return; - try { await deletePath(entry.path); toast.success(`Deleted ${entry.name}`); void load(path); } - catch (e: any) { toast.error(`Delete failed: ${e?.message ?? e}`); } + const deleteEntry = (entry: EntryInfo) => { + setConfirm({ + title: entry.type === 'folder' ? `Delete folder "${entry.name}"?` : `Delete "${entry.name}"?`, + description: entry.type === 'folder' ? 'This will delete the folder and all its contents.' : undefined, + confirmLabel: 'Delete', + destructive: true, + onConfirm: async () => { + try { await deletePath(entry.path); toast.success(`Deleted ${entry.name}`); void load(path); } + catch (e: any) { toast.error(`Delete failed: ${e?.message ?? e}`); } + }, + }); }; - const deleteSelected = async () => { + const deleteSelected = () => { const targets = entries.filter(e => selected.has(e.path)); - if (!window.confirm(`Delete ${targets.length} item${targets.length !== 1 ? 's' : ''}?`)) return; - let failed = 0; - for (const e of targets) { try { await deletePath(e.path); } catch { failed++; } } - if (failed) toast.error(`${failed} item${failed !== 1 ? 's' : ''} failed to delete`); - else toast.success(`Deleted ${targets.length} item${targets.length !== 1 ? 's' : ''}`); - void load(path); - setSelected(new Set()); + setConfirm({ + title: `Delete ${targets.length} item${targets.length !== 1 ? 's' : ''}?`, + confirmLabel: 'Delete', + destructive: true, + onConfirm: async () => { + let failed = 0; + for (const e of targets) { try { await deletePath(e.path); } catch { failed++; } } + if (failed) toast.error(`${failed} item${failed !== 1 ? 's' : ''} failed to delete`); + else toast.success(`Deleted ${targets.length} item${targets.length !== 1 ? 's' : ''}`); + void load(path); + setSelected(new Set()); + }, + }); }; // ── Rename ─────────────────────────────────────────────────────────────── @@ -752,14 +765,24 @@ export default function MediaManager({ initialPath = '/', rootPath, title, confi const handleUpload = async (file: File) => { const target = joinPath(path, file.name); + const doUpload = async () => { + try { + await putFileContents(target, await file.arrayBuffer()); + toast.success(`Uploaded ${file.name}`); + void load(path); + } catch (e: any) { toast.error(`Upload failed for ${file.name}: ${e?.message ?? e}`); } + }; if (await fileExists(target)) { - if (!window.confirm(`"${file.name}" already exists. Overwrite?`)) return; + setConfirm({ + title: `"${file.name}" already exists`, + description: 'Replace the existing file?', + confirmLabel: 'Overwrite', + destructive: true, + onConfirm: doUpload, + }); + return; } - try { - await putFileContents(target, await file.arrayBuffer()); - toast.success(`Uploaded ${file.name}`); - void load(path); - } catch (e: any) { toast.error(`Upload failed for ${file.name}: ${e?.message ?? e}`); } + await doUpload(); }; const onPickFiles = (e: React.ChangeEvent) => { @@ -1128,6 +1151,12 @@ export default function MediaManager({ initialPath = '/', rootPath, title, confi )} + {} })} + onCancel={() => setConfirm(null)} + /> + {/* ── Mount dialog ── */} !open && setMountEntry(null)}> diff --git a/src/app/components/StatusPage.tsx b/src/app/components/StatusPage.tsx index 61ac238..7d24360 100644 --- a/src/app/components/StatusPage.tsx +++ b/src/app/components/StatusPage.tsx @@ -8,6 +8,7 @@ import DeviceDetailOverlay from './DeviceDetailOverlay'; import MediaSet from './MediaSet'; import { ImageWithFallback } from './figma/ImageWithFallback'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; +import { ConfirmDialog, type ConfirmOptions } from './ui/confirm-dialog'; interface StatusPageProps { config: any; @@ -88,6 +89,7 @@ export default function StatusPage({ config, setConfig }: StatusPageProps) { const [printFiles, setPrintFiles] = useState([]); const [printLoading, setPrintLoading] = useState(true); const [printActionEntry, setPrintActionEntry] = useState(null); + const [confirm, setConfirm] = useState(null); const loadPrintFiles = () => { setPrintLoading(true); @@ -111,13 +113,19 @@ export default function StatusPage({ config, setConfig }: StatusPageProps) { } catch (e: any) { toast.error(`Download failed: ${e?.message ?? e}`); } }; - const deletePrintFile = async (entry: EntryInfo) => { - if (!window.confirm(`Delete "${entry.name}"?`)) return; - try { - await deletePath(entry.path); - toast.success(`Deleted ${entry.name}`); - loadPrintFiles(); - } catch (e: any) { toast.error(`Delete failed: ${e?.message ?? e}`); } + const deletePrintFile = (entry: EntryInfo) => { + setConfirm({ + title: `Delete "${entry.name}"?`, + confirmLabel: 'Delete', + destructive: true, + onConfirm: async () => { + try { + await deletePath(entry.path); + toast.success(`Deleted ${entry.name}`); + loadPrintFiles(); + } catch (e: any) { toast.error(`Delete failed: ${e?.message ?? e}`); } + }, + }); }; @@ -163,7 +171,7 @@ export default function StatusPage({ config, setConfig }: StatusPageProps) { {/* New device info cards */}
-
Last File
+
Last File Access
{lastFile}
@@ -459,6 +467,11 @@ export default function StatusPage({ config, setConfig }: StatusPageProps) {
+ {} })} + onCancel={() => setConfirm(null)} + /> ); } diff --git a/src/app/components/ToolsPage.tsx b/src/app/components/ToolsPage.tsx index 2012e71..c6ff121 100644 --- a/src/app/components/ToolsPage.tsx +++ b/src/app/components/ToolsPage.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { Wrench, Download, Upload, RotateCcw, Database, FileText, HardDrive, X, Loader2, Cpu, ChevronRight, AlertCircle } from 'lucide-react'; import { toast } from 'sonner'; import { getFileContents, putFileContents, humanFileSize } from '../webdav'; +import { ConfirmDialog, type ConfirmOptions } from './ui/confirm-dialog'; interface ToolsPageProps { config: any; @@ -139,6 +140,7 @@ function FirmwareOverlay({ onClose }: { onClose: () => void }) { export default function ToolsPage({ config }: ToolsPageProps) { const [showFirmware, setShowFirmware] = useState(false); + const [confirm, setConfirm] = useState(null); const handleBackup = () => { const dataStr = JSON.stringify(config, null, 2); @@ -175,15 +177,23 @@ export default function ToolsPage({ config }: ToolsPageProps) { }; const handleFactoryReset = () => { - if (confirm('Are you sure you want to reset to factory defaults? This cannot be undone.')) { - toast.success('Configuration reset to factory defaults'); - } + setConfirm({ + title: 'Factory Reset', + description: 'Reset to default settings? This cannot be undone.', + confirmLabel: 'Reset', + destructive: true, + onConfirm: () => toast.success('Configuration reset to factory defaults'), + }); }; const handleFormatSD = () => { - if (confirm('Are you sure you want to format the SD card? All data will be lost.')) { - toast.success('SD card formatting started'); - } + setConfirm({ + title: 'Format SD Card', + description: 'All data on the SD card will be lost.', + confirmLabel: 'Format', + destructive: true, + onConfirm: () => toast.success('SD card formatting started'), + }); }; const handleSystemUpdate = () => { @@ -341,6 +351,12 @@ export default function ToolsPage({ config }: ToolsPageProps) { {showFirmware && setShowFirmware(false)} />} + + {} })} + onCancel={() => setConfirm(null)} + /> ); } diff --git a/src/app/components/ui/confirm-dialog.tsx b/src/app/components/ui/confirm-dialog.tsx new file mode 100644 index 0000000..c472de6 --- /dev/null +++ b/src/app/components/ui/confirm-dialog.tsx @@ -0,0 +1,41 @@ +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './dialog'; + +export interface ConfirmOptions { + title: string; + description?: string; + confirmLabel?: string; + destructive?: boolean; + onConfirm: () => void; +} + +interface Props extends ConfirmOptions { + open: boolean; + onCancel: () => void; +} + +export function ConfirmDialog({ open, title, description, confirmLabel = 'Confirm', destructive = false, onConfirm, onCancel }: Props) { + return ( + { if (!o) onCancel(); }}> + + + {title} + {description && {description}} + + + + + + + + ); +}