ge-tool/components/FileList.tsx
2025-12-10 13:41:43 +07:00

193 lines
8.9 KiB
TypeScript
Executable File

import React, { memo } from 'react';
import type { FileSystemItem as FileSystemItemType } from '../types';
import FileItem from './FileItem';
import Spinner from './Spinner';
import { PathBar } from './PathBar';
interface FileListProps {
items: FileSystemItemType[];
isLoading: boolean;
onDownload: () => void;
selectedIds: string[];
onSelectItem: (id: string) => void;
onFolderDoubleClick?: (folder: FileSystemItemType) => void;
onNavigateBack?: () => void;
onNavigateForward?: () => void;
canNavigateBack?: boolean;
canNavigateForward?: boolean;
currentPath?: string;
currentMode?: 'api' | 'sharing' | null;
isDownloadButtonLoading?: boolean;
hasCustomPath?: boolean;
isCustomPath?: boolean;
onSaveCustomPath?: () => void;
isSavingCustomPath?: boolean;
initialSearchQuery?: string; // Auto-fill search input from parent
}
const FileList: React.FC<FileListProps> = ({
items,
isLoading,
onDownload,
selectedIds,
onSelectItem,
onFolderDoubleClick,
onNavigateBack,
onNavigateForward,
canNavigateBack = false,
canNavigateForward = false,
currentPath = '',
currentMode = null,
isDownloadButtonLoading = false,
hasCustomPath = false,
isCustomPath = false,
onSaveCustomPath,
isSavingCustomPath = false,
initialSearchQuery = ''
}) => {
// Search state
const [searchQuery, setSearchQuery] = React.useState(initialSearchQuery);
const [debouncedQuery, setDebouncedQuery] = React.useState(initialSearchQuery);
// Update search query when initialSearchQuery changes (from parent)
React.useEffect(() => {
setSearchQuery(initialSearchQuery);
setDebouncedQuery(initialSearchQuery);
}, [initialSearchQuery]);
// Debounce search query (300ms)
React.useEffect(() => {
const timer = setTimeout(() => {
setDebouncedQuery(searchQuery);
}, 300);
return () => clearTimeout(timer);
}, [searchQuery]);
// Filter items based on search query
const filteredItems = React.useMemo(() => {
if (!debouncedQuery.trim()) {
return items;
}
const query = debouncedQuery.toLowerCase();
return items.filter(item =>
item.name.toLowerCase().includes(query)
);
}, [items, debouncedQuery]);
const hasFilesOrFolders = filteredItems.length > 0;
const selectedCount = selectedIds.length;
const buttonText = selectedCount > 0 ? `Tải xuống ${selectedCount} mục` : 'Tải xuống tất cả';
const isDownloadDisabled = selectedCount === 0 && !hasFilesOrFolders;
const renderContent = () => {
if (isLoading) {
return (
<div className="text-center py-10 px-6 flex justify-center items-center gap-2">
<Spinner />
<p className="text-slate-400">Đang tải danh sách tệp...</p>
</div>
);
}
if (items.length > 0) {
return (
<>
<div className="grid grid-cols-12 gap-4 px-4 py-3 bg-slate-900/50 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 flex-shrink-0">
<div className="col-span-6">Tên</div>
<div className="col-span-3 text-right">Ngày sửa đi</div>
<div className="col-span-2 text-right">Kích thước</div>
<div className="col-span-1"></div>
</div>
<div className="divide-y divide-slate-700/50 flex-1 overflow-y-auto">
{filteredItems.length > 0 ? (
filteredItems.map((item) => (
<FileItem
key={item.id}
item={item}
isSelected={selectedIds.includes(item.id)}
onSelect={onSelectItem}
onDoubleClick={item.type === 'folder' && onFolderDoubleClick ? () => onFolderDoubleClick(item) : undefined}
/>
))
) : (
<div className="text-center py-10 px-6">
<p className="text-slate-400">Không tìm thấy kết quả cho "{debouncedQuery}"</p>
</div>
)}
</div>
</>
)
}
return (
<div className="text-center py-10 px-6 flex-1 flex items-center justify-center">
<p className="text-slate-400">Không tệp hoặc thư mục nào. Vui lòng thực hiện tìm kiếm.</p>
</div>
)
};
return (
<div className="bg-slate-800/50 backdrop-blur-sm rounded-2xl shadow-2xl border border-slate-700 h-full flex flex-col">
{/* Header */}
<div className="p-4 flex justify-between items-center border-b border-slate-700 flex-shrink-0">
<h2 className="text-xl font-semibold text-white">File & Folder</h2>
<div className="flex gap-3">
<button
onClick={() => {
// TODO: Open CustomPathManagerModal
if (typeof (window as any).openCustomPathManager === 'function') {
(window as any).openCustomPathManager();
}
}}
className="flex justify-center items-center gap-2 text-white bg-slate-700 hover:bg-slate-600 focus:ring-4 focus:outline-none focus:ring-slate-600 font-medium rounded-lg text-sm px-4 py-2 text-center transition-all duration-200"
title="Quản lý Custom Paths"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path fillRule="evenodd" d="M11.078 2.25c-.917 0-1.699.663-1.85 1.567L9.05 4.889c-.02.12-.115.26-.297.348a7.493 7.493 0 0 0-.986.57c-.166.115-.334.126-.45.083L6.3 5.508a1.875 1.875 0 0 0-2.282.819l-.922 1.597a1.875 1.875 0 0 0 .432 2.385l.84.692c.095.078.17.229.154.43a7.598 7.598 0 0 0 0 1.139c.015.2-.059.352-.153.43l-.841.692a1.875 1.875 0 0 0-.432 2.385l.922 1.597a1.875 1.875 0 0 0 2.282.818l1.019-.382c.115-.043.283-.031.45.082.312.214.641.405.985.57.182.088.277.228.297.35l.178 1.071c.151.904.933 1.567 1.85 1.567h1.844c.916 0 1.699-.663 1.85-1.567l.178-1.072c.02-.12.114-.26.297-.349.344-.165.673-.356.985-.57.167-.114.335-.125.45-.082l1.02.382a1.875 1.875 0 0 0 2.28-.819l.923-1.597a1.875 1.875 0 0 0-.432-2.385l-.84-.692c-.095-.078-.17-.229-.154-.43a7.614 7.614 0 0 0 0-1.139c-.016-.2.059-.352.153-.43l.84-.692c.708-.582.891-1.59.433-2.385l-.922-1.597a1.875 1.875 0 0 0-2.282-.818l-1.02.382c-.114.043-.282.031-.449-.083a7.49 7.49 0 0 0-.985-.57c-.183-.087-.277-.227-.297-.348l-.179-1.072a1.875 1.875 0 0 0-1.85-1.567h-1.843ZM12 15.75a3.75 3.75 0 1 0 0-7.5 3.75 3.75 0 0 0 0 7.5Z" clipRule="evenodd" />
</svg>
Custom Paths
</button>
<button
onClick={onDownload}
disabled={isDownloadDisabled || isLoading || isDownloadButtonLoading}
className="flex justify-center items-center gap-2 text-white bg-indigo-600 hover:bg-indigo-700 focus:ring-4 focus:outline-none focus:ring-indigo-800 font-medium rounded-lg text-sm px-4 py-2 text-center transition-all duration-200 disabled:bg-slate-600 disabled:text-slate-400 disabled:cursor-not-allowed"
>
{isDownloadButtonLoading && <Spinner />}
{buttonText}
</button>
</div>
</div>
{/* Path Bar */}
{(onNavigateBack || onNavigateForward) && currentPath && (
<div className="flex-shrink-0">
<PathBar
currentPath={currentPath}
canGoBack={canNavigateBack}
canGoForward={canNavigateForward}
onNavigateBack={onNavigateBack || (() => { })}
onNavigateForward={onNavigateForward || (() => { })}
currentMode={currentMode}
hasCustomPath={hasCustomPath}
isCustomPath={isCustomPath}
onSaveCustomPath={onSaveCustomPath}
isSavingCustomPath={isSavingCustomPath}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
/>
</div>
)}
{/* File List Content */}
<div className="flex-1 overflow-hidden flex flex-col">
{renderContent()}
</div>
</div>
);
};
export default memo(FileList);