File browser as widget
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import { useRef, useCallback } from 'react';
|
||||
import type { DirEntry, TaskSummary } from 'widgets/FileBrowser';
|
||||
import { FileItem } from './FileItem';
|
||||
|
||||
type ClipboardState = { paths: string[]; mode: 'copy' | 'cut' } | null;
|
||||
|
||||
type FileGridProps = {
|
||||
entries: DirEntry[];
|
||||
viewMode: 'grid' | 'list';
|
||||
currentPath: string;
|
||||
selected: Set<string>;
|
||||
clipboard: ClipboardState;
|
||||
onOpen: (entry: DirEntry) => void;
|
||||
onDelete: (entry: DirEntry) => void;
|
||||
onRename: (entry: DirEntry, newName: string) => void;
|
||||
onChat: (entry: DirEntry) => void;
|
||||
onSelect: (names: Set<string>) => void;
|
||||
onCut: () => void;
|
||||
onCopy: () => void;
|
||||
renamingName: string | null;
|
||||
onRenamingChange: (name: string | null) => void;
|
||||
getMatchingTasks: (fileName: string, entryType: 'file' | 'directory') => TaskSummary[];
|
||||
onRunTask: (task: TaskSummary, entry: DirEntry) => void;
|
||||
};
|
||||
|
||||
export const FileGrid = ({
|
||||
entries,
|
||||
viewMode,
|
||||
currentPath,
|
||||
selected,
|
||||
clipboard,
|
||||
onOpen,
|
||||
onDelete,
|
||||
onRename,
|
||||
onChat,
|
||||
onSelect,
|
||||
onCut,
|
||||
onCopy,
|
||||
renamingName,
|
||||
onRenamingChange,
|
||||
getMatchingTasks,
|
||||
onRunTask,
|
||||
}: FileGridProps) => {
|
||||
const lastClickedIdx = useRef<number>(-1);
|
||||
|
||||
// Sort: directories first, then files, alphabetically within each group
|
||||
const sorted = [...entries].sort((a, b) => {
|
||||
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(entry: DirEntry, ev: React.MouseEvent) => {
|
||||
const idx = sorted.findIndex((e) => e.name === entry.name);
|
||||
|
||||
if (ev.shiftKey && lastClickedIdx.current >= 0) {
|
||||
const start = Math.min(lastClickedIdx.current, idx);
|
||||
const end = Math.max(lastClickedIdx.current, idx);
|
||||
const next = new Set(selected);
|
||||
for (let i = start; i <= end; i++) {
|
||||
next.add(sorted[i]!.name);
|
||||
}
|
||||
onSelect(next);
|
||||
} else if (ev.ctrlKey || ev.metaKey) {
|
||||
const next = new Set(selected);
|
||||
if (next.has(entry.name)) {
|
||||
next.delete(entry.name);
|
||||
} else {
|
||||
next.add(entry.name);
|
||||
}
|
||||
onSelect(next);
|
||||
lastClickedIdx.current = idx;
|
||||
} else {
|
||||
// Plain select (from context menu or checkbox click without modifiers)
|
||||
onSelect(new Set([entry.name]));
|
||||
lastClickedIdx.current = idx;
|
||||
}
|
||||
},
|
||||
[sorted, selected, onSelect],
|
||||
);
|
||||
|
||||
const cutPaths = clipboard?.mode === 'cut' ? new Set(clipboard.paths) : new Set<string>();
|
||||
const anySelected = selected.size > 0;
|
||||
|
||||
if (entries.length === 0) {
|
||||
return <div className="flex items-center justify-center py-20 text-duck-dark/40 text-sm">This folder is empty</div>;
|
||||
}
|
||||
|
||||
const entryPath = (name: string) => (currentPath === '/' ? `/${name}` : `${currentPath}/${name}`);
|
||||
|
||||
const items = sorted.map((entry) => (
|
||||
<FileItem
|
||||
key={entry.name}
|
||||
entry={entry}
|
||||
viewMode={viewMode}
|
||||
selected={selected.has(entry.name)}
|
||||
anySelected={anySelected}
|
||||
selectedCount={selected.size}
|
||||
isCut={cutPaths.has(entryPath(entry.name))}
|
||||
onOpen={onOpen}
|
||||
onDelete={onDelete}
|
||||
onRename={onRename}
|
||||
onChat={onChat}
|
||||
onSelect={handleSelect}
|
||||
onCut={onCut}
|
||||
onCopy={onCopy}
|
||||
forceRename={renamingName === entry.name}
|
||||
onRenamingChange={onRenamingChange}
|
||||
matchingTasks={getMatchingTasks(entry.name, entry.type)}
|
||||
onRunTask={onRunTask}
|
||||
/>
|
||||
));
|
||||
|
||||
if (viewMode === 'list') {
|
||||
return <div className="file-grid flex flex-col">{items}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="file-grid grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-3">
|
||||
{items}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user