import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
import { Folder, Trash2, Pencil, MoreVertical, MessageSquare, Scissors, Copy, Check, Play, Download } from 'lucide-react';
import { getIcon } from 'material-file-icons';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubTrigger,
ContextMenuSubContent,
ContextMenuTrigger,
} from '@/components/ui/context-menu';
import { cardStyle } from '@/components/Card';
import type { DirEntry, TaskSummary } from 'apps/FileBrowser';
export type FileItemProps = {
entry: DirEntry;
viewMode: 'grid' | 'list';
selected: boolean;
anySelected: boolean;
selectedCount: number;
isCut: boolean;
onOpen: (entry: DirEntry) => void;
onDelete: (entry: DirEntry) => void;
onRename: (entry: DirEntry, newName: string) => void;
onChat: (entry: DirEntry) => void;
onDownload: (entry: DirEntry) => void;
onSelect: (entry: DirEntry, ev: React.MouseEvent) => void;
onCut: () => void;
onCopy: () => void;
forceRename: boolean;
onRenamingChange: (name: string | null) => void;
matchingTasks: TaskSummary[];
onRunTask: (task: TaskSummary, entry: DirEntry) => void;
};
function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
function formatDate(ms: number): string {
return new Date(ms).toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}
type MenuItemsProps = {
entry: DirEntry;
multiSelected: boolean;
onDelete: (e: DirEntry) => void;
onStartRename: () => void;
onChat: (e: DirEntry) => void;
onDownload: (e: DirEntry) => void;
onCut: () => void;
onCopy: () => void;
matchingTasks: TaskSummary[];
onRunTask: (task: TaskSummary, entry: DirEntry) => void;
};
const DropdownMenuItems = ({
entry,
multiSelected,
onDelete,
onStartRename,
onChat,
onDownload,
onCut,
onCopy,
matchingTasks,
onRunTask,
}: MenuItemsProps) => (
<>
onChat(entry)} className="cursor-pointer">
Chat...
onDownload(entry)} className="cursor-pointer">
Download
{matchingTasks.length > 0 && (
Run Task
{matchingTasks.map((task) => (
onRunTask(task, entry)} className="cursor-pointer">
{task.name}
))}
)}
Cut
Copy
{!multiSelected && (
Rename
)}
onDelete(entry)} className="text-red-600 cursor-pointer">
Delete
>
);
const ContextMenuItems = ({
entry,
multiSelected,
onDelete,
onStartRename,
onChat,
onDownload,
onCut,
onCopy,
matchingTasks,
onRunTask,
}: MenuItemsProps) => (
<>
onChat(entry)} className="cursor-pointer">
Chat...
onDownload(entry)} className="cursor-pointer">
Download
{matchingTasks.length > 0 && (
Run Task
{matchingTasks.map((task) => (
onRunTask(task, entry)} className="cursor-pointer">
{task.name}
))}
)}
Cut
Copy
{!multiSelected && (
Rename
)}
onDelete(entry)} className="text-red-600 cursor-pointer">
Delete
>
);
const EllipsisMenu = (props: MenuItemsProps) => (
ev.stopPropagation()}>
ev.preventDefault()}>
);
const InlineRenameInput = ({
initialName,
onCommit,
onCancel,
}: {
initialName: string;
onCommit: (name: string) => void;
onCancel: () => void;
}) => {
const [value, setValue] = useState(initialName);
const mountRef = useCallback((node: HTMLInputElement | null) => {
if (!node) return;
requestAnimationFrame(() => {
node.focus();
const dotIndex = initialName.lastIndexOf('.');
if (dotIndex > 0) {
node.setSelectionRange(0, dotIndex);
} else {
node.select();
}
});
}, []);
const commit = () => {
const trimmed = value.trim();
if (trimmed && trimmed !== initialName) {
onCommit(trimmed);
} else {
onCancel();
}
};
return (
setValue(ev.target.value)}
onBlur={commit}
onKeyDown={(ev) => {
if (ev.key === 'Enter') commit();
if (ev.key === 'Escape') onCancel();
}}
onClick={(ev) => ev.stopPropagation()}
className="text-sm font-medium text-duck-dark bg-background border border-duck-teal/50 rounded px-1 py-0.5 outline-none w-full text-center"
/>
);
};
const Checkbox = ({
checked,
anySelected,
onClick,
}: {
checked: boolean;
anySelected: boolean;
onClick: (ev: React.MouseEvent) => void;
}) => (
);
const MaterialFileIcon = ({ name, className }: { name: string; className?: string }) => {
const svg = useMemo(() => getIcon(name).svg, [name]);
return ;
};
export const FileItem = ({
entry,
viewMode,
selected,
anySelected,
selectedCount,
isCut,
onOpen,
onDelete,
onRename,
onChat,
onDownload,
onSelect,
onCut,
onCopy,
forceRename,
onRenamingChange,
matchingTasks,
onRunTask,
}: FileItemProps) => {
const [renaming, setRenaming] = useState(false);
const clickTimer = useRef | null>(null);
const isDir = entry.type === 'directory';
const icon = isDir ? (
) : (
);
const iconLarge = isDir ? (
) : (
);
useEffect(() => {
return () => {
if (clickTimer.current) clearTimeout(clickTimer.current);
};
}, []);
useEffect(() => {
if (forceRename) {
setRenaming(true);
onRenamingChange(null);
}
}, [forceRename]);
const handleCommitRename = (newName: string) => {
setRenaming(false);
onRename(entry, newName);
};
const handleClick = (ev: React.MouseEvent) => {
if (renaming) return;
// Modifier clicks select immediately (intentional multi-select)
if (ev.ctrlKey || ev.metaKey || ev.shiftKey) {
onSelect(entry, ev);
return;
}
// Delay plain click so double-click doesn't trigger selection + layout shift
const syntheticEv = { ctrlKey: false, shiftKey: false, metaKey: false } as React.MouseEvent;
clickTimer.current = setTimeout(() => {
clickTimer.current = null;
onSelect(entry, syntheticEv);
}, 200);
};
const handleDoubleClick = () => {
if (renaming) return;
if (clickTimer.current) {
clearTimeout(clickTimer.current);
clickTimer.current = null;
}
onOpen(entry);
};
const handleContextMenu = (ev: React.MouseEvent) => {
ev.stopPropagation();
if (!selected) {
onSelect(entry, { ...ev, ctrlKey: false, shiftKey: false, metaKey: false } as React.MouseEvent);
}
};
const menuProps: MenuItemsProps = {
entry,
multiSelected: selectedCount > 1,
onDelete,
onStartRename: () => setRenaming(true),
onChat,
onDownload,
onCut,
onCopy,
matchingTasks,
onRunTask,
};
const cutOpacity = isCut ? 'opacity-50' : '';
if (viewMode === 'list') {
return (
ev.stopPropagation()}
>
onSelect(entry, ev)} />
{icon}
{renaming ? (
setRenaming(false)}
/>
) : (
{entry.name}
)}
{isDir ? '--' : formatSize(entry.size)}
{formatDate(entry.modifiedAt)}
);
}
return (
ev.stopPropagation()}
>
onSelect(entry, ev)} />
{iconLarge}
{renaming ? (
setRenaming(false)}
/>
) : (
{entry.name}
)}
{isDir ? 'Folder' : formatSize(entry.size)}
{' ยท '}
{formatDate(entry.modifiedAt)}
);
};