diff --git a/src/components/navigator/FilePanel.tsx b/src/components/navigator/FilePanel.tsx index 3d32e6b..0257f89 100644 --- a/src/components/navigator/FilePanel.tsx +++ b/src/components/navigator/FilePanel.tsx @@ -1,6 +1,6 @@ import { Button, Divider, Tooltip } from '@heroui/react' import { useQuery } from '@tanstack/react-query' -import { FolderPlusIcon } from 'lucide-react' +import { ChevronDownIcon, ChevronUpIcon, FolderPlusIcon } from 'lucide-react' import { forwardRef, startTransition, @@ -352,12 +352,54 @@ const FilePanel = forwardRef<
-
Name
-
Size
-
Last Modified
+ {( + [ + { column: 'name', label: 'Name', className: 'pl-2' }, + { column: 'size', label: 'Size', className: '' }, + { + column: 'modTime', + label: 'Last Modified', + className: '', + }, + ] as const + ).map(({ column, label, className }) => { + const isSorted = nav.sortDescriptor.column === column + const nextDirection = + isSorted && nav.sortDescriptor.direction === 'ascending' + ? 'descending' + : 'ascending' + const sortStatus = isSorted + ? `, sorted ${nav.sortDescriptor.direction}` + : '' + return ( +
+ +
+ ) + })}
diff --git a/src/components/navigator/useFileNavigation.ts b/src/components/navigator/useFileNavigation.ts index 673624c..0231609 100644 --- a/src/components/navigator/useFileNavigation.ts +++ b/src/components/navigator/useFileNavigation.ts @@ -27,6 +27,11 @@ import { serializeRemotePath, } from './utils' +const nameCollator = new Intl.Collator(undefined, { + numeric: true, + sensitivity: 'base', +}) + export default function useFileNavigation({ initialRemote, initialPath, @@ -56,6 +61,10 @@ export default function useFileNavigation({ const [cwd, setCwd] = useState(initialPath ?? '') const [pathInput, setPathInput] = useState('') const [searchTerm, setSearchTerm] = useState('') + const [sortDescriptor, setSortDescriptor] = useState<{ + column: 'name' | 'size' | 'modTime' + direction: 'ascending' | 'descending' + }>({ column: 'name', direction: 'ascending' }) const [items, setItems] = useState([]) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) @@ -88,10 +97,65 @@ export default function useFileNavigation({ const visibleItems = useMemo(() => { const base = allowFiles ? items : items.filter((it) => it.isDir) - if (!searchTerm) return base const lower = searchTerm.toLowerCase() - return base.filter((item) => item.name.toLowerCase().includes(lower)) - }, [allowFiles, items, searchTerm]) + const filtered = searchTerm + ? base.filter((item) => item.name.toLowerCase().includes(lower)) + : base + const direction = sortDescriptor.direction === 'ascending' ? 1 : -1 + + return [...filtered].sort((a, b) => { + if (a.isDir !== b.isDir) return a.isDir ? -1 : 1 + + const nameComparison = + nameCollator.compare(a.name, b.name) || + a.name.localeCompare(b.name) || + a.key.localeCompare(b.key) + + if (sortDescriptor.column === 'name') return nameComparison * direction + + const aValue = + sortDescriptor.column === 'size' + ? typeof a.size === 'number' && a.size >= 0 + ? a.size + : undefined + : a.modTime + ? Date.parse(a.modTime) + : undefined + const bValue = + sortDescriptor.column === 'size' + ? typeof b.size === 'number' && b.size >= 0 + ? b.size + : undefined + : b.modTime + ? Date.parse(b.modTime) + : undefined + const normalizedA = + aValue !== undefined && Number.isFinite(aValue) ? aValue : undefined + const normalizedB = + bValue !== undefined && Number.isFinite(bValue) ? bValue : undefined + + if (normalizedA === undefined && normalizedB !== undefined) return 1 + if (normalizedA !== undefined && normalizedB === undefined) return -1 + if ( + normalizedA !== undefined && + normalizedB !== undefined && + normalizedA !== normalizedB + ) { + return (normalizedA - normalizedB) * direction + } + return nameComparison + }) + }, [allowFiles, items, searchTerm, sortDescriptor]) + + const handleSort = useCallback((column: 'name' | 'size' | 'modTime') => { + setSortDescriptor((current) => ({ + column, + direction: + current.column === column && current.direction === 'ascending' + ? 'descending' + : 'ascending', + })) + }, []) const virtualizedItems: (VirtualizedEntry | PaddingItem)[] = useMemo(() => { const base: (VirtualizedEntry | PaddingItem)[] = visibleItems.map((item) => ({ @@ -783,6 +847,7 @@ export default function useFileNavigation({ error, isUpDisabled, searchTerm, + sortDescriptor, selectedPaths, selectedCount, isRemote, @@ -795,6 +860,7 @@ export default function useFileNavigation({ // Actions setPathInput, setSearchTerm, + handleSort, handleNavigate, navigateUp, navigateTo,