From 2db938d8e4607551d3af434ae03ed2ad177fe7b0 Mon Sep 17 00:00:00 2001 From: FTCHD <144691102+FTCHD@users.noreply.github.com> Date: Tue, 17 Mar 2026 00:00:44 +0200 Subject: [PATCH] create folders in Commander #177 --- lib/rclone/constants.ts | 18 ++++ src/pages/Commander.tsx | 182 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 199 insertions(+), 1 deletion(-) diff --git a/lib/rclone/constants.ts b/lib/rclone/constants.ts index 5e5fcb2..b971bbe 100644 --- a/lib/rclone/constants.ts +++ b/lib/rclone/constants.ts @@ -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', diff --git a/src/pages/Commander.tsx b/src/pages/Commander.tsx index dd48c7a..1dc032f 100644 --- a/src/pages/Commander.tsx +++ b/src/pages/Commander.tsx @@ -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(null) const rightPanelRef = useRef(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 = {} + 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('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) + ? [ + + + , + ] + : []), + ], + ], + [canCreateFolderAtRemote, leftPanelLocation.remote, handleCreateFolder] + ) + + const renderRightToolbar = useCallback( + (buttons: ToolbarButtons) => [ + [buttons.BackButton, buttons.RefreshButton], + [ + buttons.SearchInput, + ...(canCreateFolderAtRemote(rightPanelLocation.remote) + ? [ + + + , + ] + : []), + ], + ], + [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} />