Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/frontend/src/apps/AppStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

Expand Down Expand Up @@ -269,7 +269,7 @@ function ThemesTab({ activeTheme, onApplyTheme }: ThemesTabProps) {
const [remoteThemes, setRemoteThemes] = useState<RemoteThemeEntry[]>([])

useEffect(() => {
fetchRemoteCatalog().then(catalog => {
void fetchRemoteCatalog().then(catalog => {
if (catalog?.themes) setRemoteThemes(catalog.themes)
})
}, [])
Expand Down
2 changes: 1 addition & 1 deletion app/frontend/src/apps/remoteRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 28 additions & 1 deletion app/frontend/src/fullscreen/FileExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}, [])
Expand All @@ -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.
Expand All @@ -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[] => [
Expand Down
12 changes: 12 additions & 0 deletions cpp/src/fileops.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}

Expand Down
3 changes: 3 additions & 0 deletions cpp/src/fileops.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Loading