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)}
); };