create folders in Commander #177

This commit is contained in:
FTCHD
2026-03-17 00:05:01 +02:00
parent df3c1a8057
commit 2db938d8e4
2 changed files with 199 additions and 1 deletions
+18
View File
@@ -110,6 +110,24 @@ export const SUPPORTS_ABOUT = [
'local',
] as const
export const CANNOT_PERSIST_EMPTY_FOLDERS = [
's3',
'gcs',
'azureblob',
'b2',
'swift',
'oracleobjectstorage',
'oos',
'qingstor',
'storj',
'memory',
] as const
export function supportsPersistentEmptyFolders(backendType?: string | null) {
if (!backendType) return true
return !CANNOT_PERSIST_EMPTY_FOLDERS.includes(backendType.toLowerCase())
}
// export const SUPPORTED_OPERATIONS = [
// {
// id: 'uncategorized',
+181 -1
View File
@@ -27,6 +27,7 @@ import {
ChevronUpIcon,
CopyIcon,
ExternalLinkIcon,
FolderPlusIcon,
LoaderIcon,
MoveIcon,
SearchCheckIcon,
@@ -39,11 +40,14 @@ import { getFsInfo } from '../../lib/format'
import { formatBytes } from '../../lib/format.ts'
import { startCopy, startMove } from '../../lib/rclone/api'
import rclone from '../../lib/rclone/client'
import { supportsPersistentEmptyFolders } from '../../lib/rclone/constants'
import { openWindow } from '../../lib/window'
import { FileIcon } from '../components/navigator'
import { FilePanel, type FilePanelHandle } from '../components/navigator'
import { FilePanel, type FilePanelHandle, type ToolbarButtons } from '../components/navigator'
import type { Entry, SelectItem } from '../components/navigator/types'
const RE_TRAILING_SEPARATORS = /[\\/]+$/
export default function Browser() {
const leftPanelRef = useRef<FilePanelHandle>(null)
const rightPanelRef = useRef<FilePanelHandle>(null)
@@ -88,6 +92,59 @@ export default function Browser() {
const remotes = remotesQuery.data ?? []
const firstRemote = remotes[0] ?? null
const [leftPanelLocation, setLeftPanelLocation] = useState<{
remote: string | null
path: string
}>({ remote: 'UI_LOCAL_FS', path: '' })
const [rightPanelLocation, setRightPanelLocation] = useState<{
remote: string | null
path: string
}>({ remote: null, path: '' })
const remoteTypesQuery = useQuery({
queryKey: ['remotes', 'types'],
queryFn: async () => {
const dump = await rclone('/config/dump')
const types: Record<string, string> = {}
if (dump && typeof dump === 'object') {
for (const [name, config] of Object.entries(dump)) {
if (config && typeof config === 'object' && 'type' in config) {
types[name] = (config as { type: string }).type
}
}
}
return types
},
staleTime: 1000 * 60,
})
const remoteTypes = remoteTypesQuery.data ?? {}
const getBackendTypeForRemote = useCallback(
(remote: string | null) => {
if (!remote || remote === 'UI_FAVORITES') return null
if (remote === 'UI_LOCAL_FS') return 'local'
return remoteTypes[remote] ?? null
},
[remoteTypes]
)
const canCreateFolderAtRemote = useCallback(
(remote: string | null) => {
if (!remote || remote === 'UI_FAVORITES') return false
if (remote === 'UI_LOCAL_FS') return true
return supportsPersistentEmptyFolders(getBackendTypeForRemote(remote))
},
[getBackendTypeForRemote]
)
const handleLeftNavigate = useCallback((remote: string, path: string) => {
setLeftPanelLocation({ remote, path })
}, [])
const handleRightNavigate = useCallback((remote: string, path: string) => {
setRightPanelLocation({ remote, path })
}, [])
const handleDrop = useCallback(
(items: SelectItem[], destination: string, _sourceSide: 'left' | 'right') => {
@@ -235,6 +292,125 @@ export default function Browser() {
[]
)
const handleCreateFolder = useCallback(
async (panelSide: 'left' | 'right') => {
const panelRef = panelSide === 'left' ? leftPanelRef : rightPanelRef
const panel = panelRef.current
if (!panel) return
const currentPath = panel.getCurrentPath()
if (!currentPath.remote || currentPath.remote === 'UI_FAVORITES') return
if (!canCreateFolderAtRemote(currentPath.remote)) {
await message(
'This backend does not support persistent empty folders. Create a folder by uploading a file into it.',
{
title: 'Unsupported Backend',
kind: 'warning',
}
)
return
}
const folderName = await invoke<string | null>('prompt', {
title: 'New Folder',
message: 'Enter a name for the new folder',
default: 'New Folder',
sensitive: false,
})
const normalizedFolderName = folderName?.trim()
if (!normalizedFolderName) return
try {
const normalizedPath = currentPath.path.replace(RE_TRAILING_SEPARATORS, '')
const fullTargetPath =
currentPath.remote === 'UI_LOCAL_FS'
? `${normalizedPath}${normalizedPath ? '/' : ''}${normalizedFolderName}`
: `${currentPath.remote}:/${normalizedPath}${normalizedPath ? '/' : ''}${normalizedFolderName}`
const info = getFsInfo(fullTargetPath)
await rclone('/operations/mkdir' as any, {
params: {
query: {
fs: info.root === ':local:' ? ':local:/' : info.root,
remote: info.filePath,
},
},
})
panel.refresh()
} catch (error) {
await message(error instanceof Error ? error.message : 'Create folder failed', {
title: 'Error',
kind: 'error',
})
}
},
[canCreateFolderAtRemote]
)
const renderLeftToolbar = useCallback(
(buttons: ToolbarButtons) => [
[buttons.BackButton, buttons.RefreshButton],
[
buttons.SearchInput,
...(canCreateFolderAtRemote(leftPanelLocation.remote)
? [
<Tooltip
key="left-new-folder-tooltip"
content="Create a new folder in this directory"
size="lg"
color="foreground"
>
<Button
color="primary"
size="sm"
radius="full"
startContent={<FolderPlusIcon className="size-4" />}
className="gap-1 min-w-fit"
onPress={() => handleCreateFolder('left')}
>
NEW FOLDER
</Button>
</Tooltip>,
]
: []),
],
],
[canCreateFolderAtRemote, leftPanelLocation.remote, handleCreateFolder]
)
const renderRightToolbar = useCallback(
(buttons: ToolbarButtons) => [
[buttons.BackButton, buttons.RefreshButton],
[
buttons.SearchInput,
...(canCreateFolderAtRemote(rightPanelLocation.remote)
? [
<Tooltip
key="right-new-folder-tooltip"
content="Create a new folder in this directory"
size="lg"
color="foreground"
>
<Button
color="primary"
size="sm"
radius="full"
startContent={<FolderPlusIcon className="size-4" />}
className="gap-1 min-w-fit"
onPress={() => handleCreateFolder('right')}
>
NEW FOLDER
</Button>
</Tooltip>,
]
: []),
],
],
[canCreateFolderAtRemote, rightPanelLocation.remote, handleCreateFolder]
)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'r' && (e.metaKey || e.ctrlKey)) {
@@ -272,6 +448,8 @@ export default function Browser() {
onDownload={handleDownload}
onRename={handleRename}
onDelete={handleDelete}
onNavigate={handleLeftNavigate}
renderToolbar={renderLeftToolbar}
allowedKeys={['REMOTES', 'LOCAL_FS', 'FAVORITES']}
isActive={true}
/>
@@ -292,6 +470,8 @@ export default function Browser() {
onDownload={handleDownload}
onRename={handleRename}
onDelete={handleDelete}
onNavigate={handleRightNavigate}
renderToolbar={renderRightToolbar}
allowedKeys={['REMOTES', 'LOCAL_FS', 'FAVORITES']}
isActive={true}
/>