193 lines
8.9 KiB
TypeScript
193 lines
8.9 KiB
TypeScript
|
|
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 có 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);
|