veza/apps/web/src/components/views/FileManagerView.tsx

252 lines
15 KiB
TypeScript
Raw Normal View History

import React, { useState } from 'react';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
import { SearchInput } from '../ui/input';
import { FileNode } from '../../types';
import {
LayoutGrid, List, Filter, MoreVertical, Download,
Trash2, Move, Folder, Music, Image as ImageIcon, File,
CheckSquare, Square, Wand2, Stamp, Eye, ChevronRight
} from 'lucide-react';
import { useToast } from '../../context/ToastContext';
import { AutoMetadataDetectionModal } from '../library/AutoMetadataDetectionModal';
import { WatermarkSettingsModal } from '../library/WatermarkSettingsModal';
import { FileDetailsView } from './FileDetailsView';
// Mock Files
const MOCK_FILES: FileNode[] = [
{ id: '1', name: 'Vocals_Main.wav', type: 'audio', size: '24 MB', modified: '2h ago', status: 'ready' },
{ id: '2', name: 'Project_Alpha_v3', type: 'folder', size: '-', modified: '1d ago', status: 'ready' },
{ id: '3', name: 'Bass_Drop_F#m.wav', type: 'audio', size: '12 MB', modified: '3d ago', status: 'processing' },
{ id: '4', name: 'Album_Cover_Art.png', type: 'image', size: '4 MB', modified: '1w ago', status: 'ready' },
{ id: '5', name: 'Reference_Track_01.mp3', type: 'audio', size: '8 MB', modified: '2w ago', status: 'ready' },
{ id: '6', name: 'Legal_Contracts.pdf', type: 'document', size: '2 MB', modified: '3w ago', status: 'archived' },
{ id: '7', name: 'Drums_Stem_02.wav', type: 'audio', size: '45 MB', modified: '1h ago', status: 'ready' },
];
export const FileManagerView: React.FC = () => {
const { addToast } = useToast();
const [viewMode, setViewMode] = useState<'list' | 'grid'>('list');
const [searchQuery, setSearchQuery] = useState('');
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
const [currentFolder, setCurrentFolder] = useState('Root');
// Navigation State
const [selectedFileId, setSelectedFileId] = useState<string | null>(null);
// Modals
const [showMetadataModal, setShowMetadataModal] = useState(false);
const [showWatermarkModal, setShowWatermarkModal] = useState(false);
// --- Handlers ---
const handleFileClick = (file: FileNode) => {
if (file.type === 'folder') {
setCurrentFolder(file.name);
addToast(`Navigated to ${file.name}`, 'info');
} else {
setSelectedFileId(file.id);
}
};
const toggleSelection = (id: string) => {
setSelectedFiles(prev =>
prev.includes(id) ? prev.filter(fid => fid !== id) : [...prev, id]
);
};
const selectAll = () => {
if (selectedFiles.length === MOCK_FILES.length) setSelectedFiles([]);
else setSelectedFiles(MOCK_FILES.map(f => f.id));
};
const handleBulkAction = (action: string) => {
addToast(`${action} ${selectedFiles.length} files`, 'success');
setSelectedFiles([]);
};
const filteredFiles = MOCK_FILES.filter(f => f.name.toLowerCase().includes(searchQuery.toLowerCase()));
// --- Render Detail View if selected ---
if (selectedFileId) {
return <FileDetailsView fileId={selectedFileId} onBack={() => setSelectedFileId(null)} />;
}
// --- Main Render ---
return (
<div className="space-y-6 animate-fadeIn pb-20">
{/* Header & Controls */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div>
<h2 className="text-3xl font-display font-bold text-white mb-1">FILE MANAGER</h2>
<div className="flex items-center gap-2 text-sm text-gray-400">
<span className="hover:text-white cursor-pointer" onClick={() => setCurrentFolder('Root')}>Root</span>
{currentFolder !== 'Root' && (
<>
<ChevronRight className="w-4 h-4" />
<span className="text-white font-bold">{currentFolder}</span>
</>
)}
</div>
</div>
<div className="flex gap-2 w-full md:w-auto">
<Button variant="ghost" onClick={() => setShowMetadataModal(true)} title="AI Auto-Tag" aria-label="AI Auto-Tag">
<Wand2 className="w-4 h-4" />
</Button>
<Button variant="ghost" onClick={() => setShowWatermarkModal(true)} title="Watermark Settings" aria-label="Paramètres de filigrane">
<Stamp className="w-4 h-4" />
</Button>
<div className="bg-kodo-ink p-1 rounded-lg border border-kodo-steel flex">
<button
onClick={() => setViewMode('list')}
className={`p-2 rounded ${viewMode === 'list' ? 'bg-kodo-slate text-white' : 'text-gray-400 hover:text-white'}`}
aria-label="Vue liste"
aria-pressed={viewMode === 'list'}
>
<List className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('grid')}
className={`p-2 rounded ${viewMode === 'grid' ? 'bg-kodo-slate text-white' : 'text-gray-400 hover:text-white'}`}
aria-label="Vue grille"
aria-pressed={viewMode === 'grid'}
>
<LayoutGrid className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Search & Filter Bar */}
<div className="bg-kodo-ink/50 p-4 rounded-xl border border-kodo-steel/50 flex flex-col md:flex-row gap-4 items-center justify-between">
<div className="w-full md:w-96">
<SearchInput placeholder="Search files..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} />
</div>
{selectedFiles.length > 0 ? (
<div className="flex items-center gap-3 bg-kodo-cyan/10 px-4 py-2 rounded-lg border border-kodo-cyan/30 animate-fadeIn w-full md:w-auto">
<span className="text-sm font-bold text-kodo-cyan">{selectedFiles.length} Selected</span>
<div className="h-4 w-px bg-kodo-cyan/30 mx-2"></div>
<button onClick={() => handleBulkAction('Downloaded')} className="text-gray-300 hover:text-white" title="Download"><Download className="w-4 h-4" /></button>
<button onClick={() => handleBulkAction('Moved')} className="text-gray-300 hover:text-white" title="Move"><Move className="w-4 h-4" /></button>
<button onClick={() => handleBulkAction('Deleted')} className="text-red-400 hover:text-red-300" title="Delete"><Trash2 className="w-4 h-4" /></button>
</div>
) : (
<div className="flex gap-2">
<Button variant="ghost" size="sm" icon={<Filter className="w-3 h-3" />}>Type</Button>
<Button variant="ghost" size="sm" icon={<Filter className="w-3 h-3" />}>Date</Button>
</div>
)}
</div>
{/* Content Area */}
<div className="min-h-[500px]">
{viewMode === 'list' ? (
<Card variant="default" className="p-0 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-kodo-steel bg-kodo-ink/50 text-xs font-mono text-gray-500 uppercase tracking-wider">
<th className="p-4 w-10">
<div onClick={selectAll} className="cursor-pointer hover:text-white">
{selectedFiles.length === MOCK_FILES.length ? <CheckSquare className="w-4 h-4 text-kodo-cyan" /> : <Square className="w-4 h-4" />}
</div>
</th>
<th className="p-4">Name</th>
<th className="p-4">Type</th>
<th className="p-4">Size</th>
<th className="p-4">Modified</th>
<th className="p-4">Status</th>
<th className="p-4 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-kodo-steel/30">
{filteredFiles.map((file) => (
<tr key={file.id} className={`group hover:bg-white/5 transition-colors ${selectedFiles.includes(file.id) ? 'bg-kodo-cyan/5' : ''}`}>
<td className="p-4">
<div onClick={() => toggleSelection(file.id)} className="cursor-pointer text-gray-500 hover:text-white">
{selectedFiles.includes(file.id) ? <CheckSquare className="w-4 h-4 text-kodo-cyan" /> : <Square className="w-4 h-4" />}
</div>
</td>
<td className="p-4">
<div className="flex items-center gap-3 cursor-pointer" onClick={() => handleFileClick(file)}>
{file.type === 'folder' && <Folder className="w-5 h-5 text-kodo-gold" />}
{file.type === 'audio' && <Music className="w-5 h-5 text-kodo-cyan" />}
{file.type === 'image' && <ImageIcon className="w-5 h-5 text-kodo-magenta" />}
{['document', 'archive', 'project'].includes(file.type) && <File className="w-5 h-5 text-gray-400" />}
<span className="font-medium text-gray-200 group-hover:text-white transition-colors">{file.name}</span>
</div>
</td>
<td className="p-4">
<span className="px-2 py-1 bg-kodo-ink rounded text-[10px] uppercase font-bold text-gray-400">{file.type}</span>
</td>
<td className="p-4 text-sm text-gray-400 font-mono">{file.size}</td>
<td className="p-4 text-sm text-gray-400">{file.modified}</td>
<td className="p-4">
<span className={`flex items-center gap-1.5 text-xs font-bold capitalize ${file.status === 'ready' ? 'text-kodo-lime' : file.status === 'processing' ? 'text-kodo-gold' : 'text-gray-500'}`}>
<div className={`w-1.5 h-1.5 rounded-full ${file.status === 'ready' ? 'bg-kodo-lime' : file.status === 'processing' ? 'bg-kodo-gold animate-pulse' : 'bg-gray-500'}`}></div>
{file.status}
</span>
</td>
<td className="p-4 text-right">
<div className="flex justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button className="p-1.5 hover:bg-white/10 rounded text-gray-400 hover:text-white"><Eye className="w-4 h-4" onClick={() => handleFileClick(file)} /></button>
<button className="p-1.5 hover:bg-white/10 rounded text-gray-400 hover:text-white"><MoreVertical className="w-4 h-4" /></button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
) : (
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4">
{filteredFiles.map((file) => (
<Card
key={file.id}
variant="default"
className={`p-4 flex flex-col items-center text-center gap-3 cursor-pointer hover:border-kodo-cyan/50 transition-all group relative ${selectedFiles.includes(file.id) ? 'border-kodo-cyan bg-kodo-cyan/5' : ''}`}
onClick={() => handleFileClick(file)}
>
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity" onClick={(e) => { e.stopPropagation(); toggleSelection(file.id); }}>
{selectedFiles.includes(file.id) ? <CheckSquare className="w-4 h-4 text-kodo-cyan" /> : <Square className="w-4 h-4 text-gray-400 hover:text-white" />}
</div>
<div className="w-16 h-16 rounded-2xl bg-kodo-ink flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform duration-300">
{file.type === 'folder' && <Folder className="w-8 h-8 text-kodo-gold" />}
{file.type === 'audio' && <Music className="w-8 h-8 text-kodo-cyan" />}
{file.type === 'image' && <ImageIcon className="w-8 h-8 text-kodo-magenta" />}
{['document', 'archive', 'project'].includes(file.type) && <File className="w-8 h-8 text-gray-400" />}
</div>
<div className="w-full">
<h4 className="font-bold text-white text-sm truncate w-full">{file.name}</h4>
<p className="text-xs text-gray-500 mt-1">{file.size}</p>
</div>
</Card>
))}
</div>
)}
</div>
{/* Modals */}
{showMetadataModal && (
<AutoMetadataDetectionModal
fileName={selectedFileId ? MOCK_FILES.find(f => f.id === selectedFileId)?.name || 'Selected File' : 'Scan Library'}
onClose={() => setShowMetadataModal(false)}
onApply={(data) => { addToast(`Applied: ${data.genre} - ${data.bpm}BPM`, 'success'); setShowMetadataModal(false); }}
/>
)}
{showWatermarkModal && (
<WatermarkSettingsModal
onClose={() => setShowWatermarkModal(false)}
onSave={() => addToast("Watermark settings updated", 'success')}
/>
)}
</div>
);
};