rename and delete in Commander, #18
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Button, Checkbox, Listbox, ListboxItem, cn } from '@heroui/react'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { DownloadIcon, EyeIcon, StarIcon } from 'lucide-react'
|
||||
import { DownloadIcon, EyeIcon, PencilIcon, StarIcon, Trash2Icon } from 'lucide-react'
|
||||
import { useCallback } from 'react'
|
||||
import { formatBytes } from '../../../lib/format.ts'
|
||||
import FileIcon from './FileIcon'
|
||||
@@ -24,6 +24,8 @@ export default function FileList({
|
||||
favoritedKeys,
|
||||
onToggleFavorite,
|
||||
onDownload,
|
||||
onRename,
|
||||
onDelete,
|
||||
listHeight,
|
||||
}: {
|
||||
items: (VirtualizedEntry | PaddingItem)[]
|
||||
@@ -42,6 +44,8 @@ export default function FileList({
|
||||
favoritedKeys?: Record<string, boolean>
|
||||
onToggleFavorite?: (entry: Entry, isFavorited: boolean) => void
|
||||
onDownload?: (entry: Entry) => void
|
||||
onRename?: (entry: Entry) => void
|
||||
onDelete?: (entry: Entry) => void
|
||||
listHeight: number
|
||||
}) {
|
||||
const showCheckbox = selectionMode === 'checkbox' || selectionMode === 'both'
|
||||
@@ -158,7 +162,7 @@ export default function FileList({
|
||||
}
|
||||
|
||||
const gridCols = showPreviewColumn
|
||||
? 'grid-cols-[2.5rem_1fr_6rem_9rem_5rem]'
|
||||
? 'grid-cols-[2.5rem_1fr_6rem_9rem_9rem]'
|
||||
: 'grid-cols-[2.5rem_1fr_6rem_9rem_2.5rem]'
|
||||
|
||||
return (
|
||||
@@ -294,6 +298,29 @@ export default function FileList({
|
||||
<DownloadIcon className="size-5" />
|
||||
</Button>
|
||||
)}
|
||||
{onRename && (
|
||||
<Button
|
||||
isIconOnly={true}
|
||||
size="sm"
|
||||
variant="light"
|
||||
className="transition-opacity duration-300 opacity-0 group-hover:opacity-100"
|
||||
onPress={() => onRename(entry)}
|
||||
>
|
||||
<PencilIcon className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
{onDelete && (
|
||||
<Button
|
||||
isIconOnly={true}
|
||||
size="sm"
|
||||
variant="light"
|
||||
color="danger"
|
||||
className="transition-opacity duration-300 opacity-0 group-hover:opacity-100"
|
||||
onPress={() => onDelete(entry)}
|
||||
>
|
||||
<Trash2Icon className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center w-10 shrink-0">
|
||||
|
||||
@@ -36,6 +36,8 @@ const FilePanel = forwardRef<
|
||||
showPreviewColumn?: boolean
|
||||
onPreviewRequest?: (item: Entry) => void
|
||||
onDownload?: (item: Entry) => void
|
||||
onRename?: (item: Entry) => void
|
||||
onDelete?: (item: Entry) => void
|
||||
contextMenuItems?: ContextMenuItem[]
|
||||
allowedKeys?: AllowedKey[]
|
||||
renderToolbar?: (buttons: ToolbarButtons) => React.ReactNode[][]
|
||||
@@ -57,6 +59,8 @@ const FilePanel = forwardRef<
|
||||
showPreviewColumn = true,
|
||||
onPreviewRequest,
|
||||
onDownload,
|
||||
onRename,
|
||||
onDelete,
|
||||
contextMenuItems,
|
||||
allowedKeys = ['REMOTES', 'LOCAL_FS'],
|
||||
renderToolbar,
|
||||
@@ -301,7 +305,7 @@ const FilePanel = forwardRef<
|
||||
|
||||
<div className="relative flex flex-col w-full h-full overflow-hidden">
|
||||
<div
|
||||
className={`sticky top-0 z-10 grid ${showPreviewColumn ? 'grid-cols-[2.5rem_1fr_6rem_9rem_5rem]' : 'grid-cols-[2.5rem_1fr_6rem_9rem_2.5rem]'} items-center py-2 bg-default-100`}
|
||||
className={`sticky top-0 z-10 grid ${showPreviewColumn ? 'grid-cols-[2.5rem_1fr_6rem_9rem_9rem]' : 'grid-cols-[2.5rem_1fr_6rem_9rem_2.5rem]'} items-center py-2 bg-default-100`}
|
||||
>
|
||||
<div />
|
||||
<div className="pl-2 font-semibold text-small">Name</div>
|
||||
@@ -323,6 +327,8 @@ const FilePanel = forwardRef<
|
||||
showPreviewColumn={showPreviewColumn}
|
||||
onPreviewClick={handlePreviewClick}
|
||||
onDownload={onDownload}
|
||||
onRename={onRename}
|
||||
onDelete={onDelete}
|
||||
draggable={selectionMode === 'drag' || selectionMode === 'both'}
|
||||
onDragStart={handleDragStartInternal}
|
||||
// handled at the Browser level
|
||||
|
||||
+92
-1
@@ -17,7 +17,8 @@ import {
|
||||
Tooltip,
|
||||
} from '@heroui/react'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { message, save } from '@tauri-apps/plugin-dialog'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { ask, message, save } from '@tauri-apps/plugin-dialog'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import {
|
||||
@@ -148,6 +149,92 @@ export default function Browser() {
|
||||
setContextMenu(null)
|
||||
}, [])
|
||||
|
||||
const handleDelete = useCallback(
|
||||
async (entry: Entry) => {
|
||||
const confirmed = await ask(
|
||||
`Are you sure you want to delete "${entry.name}"?`,
|
||||
{ title: 'Confirm Delete', kind: 'warning' }
|
||||
)
|
||||
if (!confirmed) return
|
||||
|
||||
try {
|
||||
const source = entry.fullPath + (entry.isDir ? '/' : '')
|
||||
const info = getFsInfo(source)
|
||||
const endpoint = entry.isDir ? '/operations/purge' : '/operations/deletefile'
|
||||
|
||||
await rclone(endpoint as any, {
|
||||
params: {
|
||||
query: {
|
||||
fs: info.root,
|
||||
remote: info.filePath,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
leftPanelRef.current?.refresh()
|
||||
rightPanelRef.current?.refresh()
|
||||
} catch (error) {
|
||||
await message(error instanceof Error ? error.message : 'Delete failed', {
|
||||
title: 'Error',
|
||||
kind: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const handleRename = useCallback(
|
||||
async (entry: Entry) => {
|
||||
const newName = await invoke<string | null>('prompt', {
|
||||
title: 'Rename',
|
||||
message: `Enter a new name for "${entry.name}"`,
|
||||
default: entry.name,
|
||||
sensitive: false,
|
||||
})
|
||||
if (!newName || newName === entry.name) return
|
||||
|
||||
try {
|
||||
const info = getFsInfo(entry.fullPath)
|
||||
const parentDir = info.filePath.includes('/')
|
||||
? info.filePath.slice(0, info.filePath.lastIndexOf('/') + 1)
|
||||
: ''
|
||||
const dstRemote = `${parentDir}${newName}`
|
||||
|
||||
if (entry.isDir) {
|
||||
await rclone('/sync/move' as any, {
|
||||
params: {
|
||||
query: {
|
||||
srcFs: `${info.root}${info.filePath}/`,
|
||||
dstFs: `${info.root}${dstRemote}/`,
|
||||
deleteEmptySrcDirs: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
await rclone('/operations/movefile' as any, {
|
||||
params: {
|
||||
query: {
|
||||
srcFs: info.root,
|
||||
srcRemote: info.filePath,
|
||||
dstFs: info.root,
|
||||
dstRemote,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
leftPanelRef.current?.refresh()
|
||||
rightPanelRef.current?.refresh()
|
||||
} catch (error) {
|
||||
await message(error instanceof Error ? error.message : 'Rename failed', {
|
||||
title: 'Error',
|
||||
kind: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'r' && (e.metaKey || e.ctrlKey)) {
|
||||
@@ -183,6 +270,8 @@ export default function Browser() {
|
||||
showPreviewColumn={true}
|
||||
onDrop={(items, dest) => handleDrop(items, dest, 'left')}
|
||||
onDownload={handleDownload}
|
||||
onRename={handleRename}
|
||||
onDelete={handleDelete}
|
||||
allowedKeys={['REMOTES', 'LOCAL_FS', 'FAVORITES']}
|
||||
isActive={true}
|
||||
/>
|
||||
@@ -201,6 +290,8 @@ export default function Browser() {
|
||||
showPreviewColumn={true}
|
||||
onDrop={(items, dest) => handleDrop(items, dest, 'right')}
|
||||
onDownload={handleDownload}
|
||||
onRename={handleRename}
|
||||
onDelete={handleDelete}
|
||||
allowedKeys={['REMOTES', 'LOCAL_FS', 'FAVORITES']}
|
||||
isActive={true}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user