- FileManagerView: Removed decorative scale transform (group-hover:scale-110 → removed) - LiveView: Removed decorative image zoom (group-hover:scale-105 → removed) and icon scale (hover:scale-110 → hover:text-kodo-gold/80) - FileDetailsView: Replaced decorative gradient with solid color (bg-gradient-to-br from-kodo-cyan to-kodo-cyan → bg-kodo-cyan/20, removed decorative shadow) - CartView: Removed decorative shadow (shadow-neon-cyan/20 → removed) - CheckoutView: Removed decorative shadow (shadow-neon-cyan/20 → removed) - Preserved: Functional overlay gradients for text readability (ProfileView, DiscoverView) - Action 11.5.1.5: Apply direction to all pages - Batch 2 complete (5 views)
449 lines
16 KiB
TypeScript
449 lines
16 KiB
TypeScript
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-8 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-2xl font-display font-bold text-white mb-1">
|
|
FILE MANAGER
|
|
</h2>
|
|
<div className="flex items-center gap-2 text-sm text-kodo-content-dim">
|
|
<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-kodo-content-dim 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-kodo-content-dim 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-4 bg-kodo-steel/10 px-4 py-2 rounded-lg border border-kodo-steel/30 animate-fadeIn w-full md:w-auto">
|
|
<span className="text-sm font-bold text-kodo-steel">
|
|
{selectedFiles.length} Selected
|
|
</span>
|
|
<div className="h-4 w-px bg-kodo-cyan/30 mx-2"></div>
|
|
<button
|
|
onClick={() => handleBulkAction('Downloaded')}
|
|
className="text-kodo-text-main hover:text-white"
|
|
title="Download"
|
|
>
|
|
<Download className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleBulkAction('Moved')}
|
|
className="text-kodo-text-main hover:text-white"
|
|
title="Move"
|
|
>
|
|
<Move className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleBulkAction('Deleted')}
|
|
className="text-kodo-red hover:text-kodo-red/80"
|
|
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-kodo-content-dim 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-steel" />
|
|
) : (
|
|
<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-kodo-content-dim hover:text-white"
|
|
>
|
|
{selectedFiles.includes(file.id) ? (
|
|
<CheckSquare className="w-4 h-4 text-kodo-steel" />
|
|
) : (
|
|
<Square className="w-4 h-4" />
|
|
)}
|
|
</div>
|
|
</td>
|
|
<td className="p-4">
|
|
<div
|
|
className="flex items-center gap-4 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-steel" />
|
|
)}
|
|
{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-kodo-content-dim" />}
|
|
<span className="font-medium text-kodo-text-main 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-kodo-content-dim">
|
|
{file.type}
|
|
</span>
|
|
</td>
|
|
<td className="p-4 text-sm text-kodo-content-dim font-mono">
|
|
{file.size}
|
|
</td>
|
|
<td className="p-4 text-sm text-kodo-content-dim">
|
|
{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-kodo-content-dim'}`}
|
|
>
|
|
<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-kodo-steel'}`}
|
|
></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
|
|
variant="ghost"
|
|
size="icon"
|
|
className="p-1.5"
|
|
onClick={() => handleFileClick(file)}
|
|
>
|
|
<Eye className="w-4 h-4" />
|
|
</Button>
|
|
<Button variant="ghost" size="icon" className="p-1.5">
|
|
<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-4 cursor-pointer hover:border-kodo-steel/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-kodo-content-dim hover:text-white" />
|
|
)}
|
|
</div>
|
|
|
|
<div className="w-16 h-16 rounded-2xl bg-kodo-ink flex items-center justify-center shadow-lg transition-colors duration-200">
|
|
{file.type === 'folder' && (
|
|
<Folder className="w-8 h-8 text-kodo-gold" />
|
|
)}
|
|
{file.type === 'audio' && (
|
|
<Music className="w-8 h-8 text-kodo-steel" />
|
|
)}
|
|
{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-kodo-content-dim" />
|
|
)}
|
|
</div>
|
|
<div className="w-full">
|
|
<h4 className="font-bold text-white text-sm truncate w-full">
|
|
{file.name}
|
|
</h4>
|
|
<p className="text-xs text-kodo-content-dim 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>
|
|
);
|
|
};
|