diff --git a/app/frontend/src/apps/AppStore.tsx b/app/frontend/src/apps/AppStore.tsx index f9186e6..d5e6094 100644 --- a/app/frontend/src/apps/AppStore.tsx +++ b/app/frontend/src/apps/AppStore.tsx @@ -46,7 +46,7 @@ function AppsTab() { if (!cancelled && catalog) setRemote(catalog.apps) }) - Promise.all([loadBundled, loadRemote]).finally(() => { + void Promise.all([loadBundled, loadRemote]).finally(() => { if (!cancelled) setLoading(false) }) @@ -269,7 +269,7 @@ function ThemesTab({ activeTheme, onApplyTheme }: ThemesTabProps) { const [remoteThemes, setRemoteThemes] = useState([]) useEffect(() => { - fetchRemoteCatalog().then(catalog => { + void fetchRemoteCatalog().then(catalog => { if (catalog?.themes) setRemoteThemes(catalog.themes) }) }, []) diff --git a/app/frontend/src/apps/remoteRegistry.ts b/app/frontend/src/apps/remoteRegistry.ts index 14ec9a7..d7e745b 100644 --- a/app/frontend/src/apps/remoteRegistry.ts +++ b/app/frontend/src/apps/remoteRegistry.ts @@ -28,7 +28,7 @@ export interface RemoteAppEntry { description: string author: string version: string - category: 'productivity' | 'development' | 'utilities' | string + category: string repo: string official: boolean bundleUrl: string diff --git a/app/frontend/src/fullscreen/FileExplorer.tsx b/app/frontend/src/fullscreen/FileExplorer.tsx index ff61f7c..098fa5b 100644 --- a/app/frontend/src/fullscreen/FileExplorer.tsx +++ b/app/frontend/src/fullscreen/FileExplorer.tsx @@ -255,6 +255,30 @@ export default function FileExplorer({ root, selectedPath, onSelect, onRefresh, onRefresh() }, [newItem, onRefresh, loadDir]) + const handleDuplicate = useCallback(async (node: FileNode) => { + const dir = parentDir(node.path) + const siblings = dirCache.get(dir) ?? [] + const existingNames = new Set(siblings.map(n => n.name)) + + const dotIdx = node.name.lastIndexOf('.') + const hasDot = dotIdx > 0 + const base = hasDot ? node.name.slice(0, dotIdx) : node.name + const ext = hasDot ? node.name.slice(dotIdx) : '' + + let copyName = `${base} copy${ext}` + if (existingNames.has(copyName)) { + let n = 2 + while (existingNames.has(`${base} copy ${n}${ext}`)) n++ + copyName = `${base} copy ${n}${ext}` + } + + const destPath = `${dir}/${copyName}` + await invoke('fs.copy', { from: node.path, to: destPath }) + setRenaming(destPath) + setRenameVal(copyName) + void loadDir(dir) + }, [dirCache, loadDir]) + const handleCopyPath = useCallback((node: FileNode) => { void navigator.clipboard.writeText(node.path) }, []) @@ -276,6 +300,9 @@ export default function FileExplorer({ root, selectedPath, onSelect, onRefresh, { label: 'Rename', action: () => startRename(node) }, { label: 'Copy Path', action: () => handleCopyPath(node) }, ] + if (!node.isDir) { + items.push({ label: 'Duplicate', action: () => { void handleDuplicate(node) } }) + } // Apps contribute their own items here (e.g. Live Preview adds "Open Live // Preview" for .md/.html files) instead of the host hardcoding knowledge // of specific apps. @@ -292,7 +319,7 @@ export default function FileExplorer({ root, selectedPath, onSelect, onRefresh, } items.push({ divider: true }, { label: 'Delete', danger: true, action: () => handleDelete(node) }) return items - }, [handleNewFile, handleNewFolder, startRename, handleCopyPath, handleDelete, onAddToGitIgnore, installedApps]) + }, [handleNewFile, handleNewFolder, startRename, handleCopyPath, handleDuplicate, handleDelete, onAddToGitIgnore, installedApps]) // ── Empty-area context menu — acts on root dir ───────────────────────────── const buildAreaMenu = useCallback((node: FileNode): ContextMenuItem[] => [ diff --git a/cpp/src/fileops.cpp b/cpp/src/fileops.cpp index cf817c1..9964a34 100644 --- a/cpp/src/fileops.cpp +++ b/cpp/src/fileops.cpp @@ -249,6 +249,13 @@ bool make_dirs(const std::string& path) { return !ec; } +bool copy_path(const std::string& from, const std::string& to) { + std::error_code ec; + fs::copy(from_u8(from), from_u8(to), + fs::copy_options::recursive | fs::copy_options::copy_symlinks, ec); + return !ec; +} + // ─── IPC dispatch ───────────────────────────────────────────────────────────── bool dispatch(const std::string& type, const json& msg, @@ -304,6 +311,11 @@ bool dispatch(const std::string& type, const json& msg, reply({{"ok", make_dirs(msg.value("path", std::string{}))}}); return true; } + if (type == "fs.copy") { + reply({{"ok", copy_path(msg.value("from", std::string{}), + msg.value("to", std::string{}))}}); + return true; + } return false; } diff --git a/cpp/src/fileops.hpp b/cpp/src/fileops.hpp index 62fa9be..c0534bc 100644 --- a/cpp/src/fileops.hpp +++ b/cpp/src/fileops.hpp @@ -31,6 +31,9 @@ bool create_file(const std::string& path); // Create directories recursively. bool make_dirs(const std::string& path); +// Copy a file or directory tree to a new path. +bool copy_path(const std::string& from, const std::string& to); + // Monaco language ID — byte-for-byte match with Go's detectLanguage. std::string detect_language(const std::string& path);