veza/apps/web/src/features/studio/components/cloud-file-browser/CloudFileBrowser.tsx

188 lines
6.5 KiB
TypeScript

import React, { useState, useEffect, useMemo } from 'react';
import { useToast } from '@/components/feedback/ToastProvider';
import { FileDetailsView } from '@/components/views/FileDetailsView';
import { AutoMetadataDetectionModal } from '@/components/library/AutoMetadataDetectionModal';
import { WatermarkSettingsModal } from '@/components/library/WatermarkSettingsModal';
import { storageService } from '@/services/storageService';
import { logger } from '@/utils/logger';
import { FileToolbar } from './FileToolbar';
import { FileTable } from './FileTable';
import { FileGrid } from './FileGrid';
import { CloudFileBrowserSkeleton } from './CloudFileBrowserSkeleton';
import type { CloudFileNode, SortField, SortOrder, ViewMode } from './types';
import { FILE_BROWSER_DEFAULT_TAGS } from './types';
export const CloudFileBrowser: React.FC = () => {
const { addToast } = useToast();
const [viewMode, setViewMode] = useState<ViewMode>('list');
const [searchQuery, setSearchQuery] = useState('');
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
const [, setCurrentFolder] = useState('Root');
const [files, setFiles] = useState<CloudFileNode[]>([]);
const [loading, setLoading] = useState(true);
const [selectedFileId, setSelectedFileId] = useState<string | null>(null);
const [sortField, setSortField] = useState<SortField>('modified');
const [sortOrder, setSortOrder] = useState<SortOrder>('desc');
const [activeTags, setActiveTags] = useState<string[]>([]);
const [showMetadataModal, setShowMetadataModal] = useState(false);
const [showWatermarkModal, setShowWatermarkModal] = useState(false);
useEffect(() => {
const loadFiles = async () => {
setLoading(true);
try {
const data = await storageService.listFiles();
setFiles(data);
} catch (e) {
logger.error('Error loading files', {
error: e instanceof Error ? e.message : String(e),
stack: e instanceof Error ? e.stack : undefined,
});
} finally {
setLoading(false);
}
};
loadFiles();
}, []);
const filteredFiles = useMemo(() => {
return files
.filter((f) => f.name.toLowerCase().includes(searchQuery.toLowerCase()))
.filter(
(f) =>
activeTags.length === 0 || f.tags?.some((t) => activeTags.includes(t))
)
.sort((a, b) => {
let valA: string | number = a[sortField] ?? '';
let valB: string | number = b[sortField] ?? '';
if (sortField === 'size') {
valA = parseInt(String(a.size || '0'), 10) || 0;
valB = parseInt(String(b.size || '0'), 10) || 0;
}
if (valA < valB) return sortOrder === 'asc' ? -1 : 1;
if (valA > valB) return sortOrder === 'asc' ? 1 : -1;
return 0;
});
}, [files, searchQuery, activeTags, sortField, sortOrder]);
const handleSort = (field: SortField) => {
setSortField((prev) => {
if (prev === field) {
setSortOrder((o) => (o === 'asc' ? 'desc' : 'asc'));
return prev;
}
setSortOrder('asc');
return field;
});
};
const toggleTag = (tag: string) => {
setActiveTags((prev) =>
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
);
};
const toggleSelection = (id: string) => {
setSelectedFiles((prev) =>
prev.includes(id) ? prev.filter((f) => f !== id) : [...prev, id]
);
};
const selectAll = () => {
if (selectedFiles.length === filteredFiles.length) setSelectedFiles([]);
else setSelectedFiles(filteredFiles.map((f) => f.id));
};
const handleFileClick = (file: CloudFileNode) => {
if (file.type === 'folder') {
setCurrentFolder(file.name);
addToast(`Navigated to ${file.name}`, 'info');
} else {
setSelectedFileId(file.id);
}
};
const handleBulkAction = (action: string) => {
if (selectedFiles.length === 0) return;
addToast(`${action} ${selectedFiles.length} items`, 'success');
setSelectedFiles([]);
};
if (selectedFileId) {
return (
<FileDetailsView
fileId={selectedFileId}
onBack={() => setSelectedFileId(null)}
/>
);
}
if (loading) {
return <CloudFileBrowserSkeleton viewMode={viewMode} />;
}
return (
<div className="space-y-8 h-full flex flex-col">
<FileToolbar
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
activeTags={activeTags}
onTagToggle={toggleTag}
sortField={sortField}
sortOrder={sortOrder}
onSortFieldChange={setSortField}
onSortOrderToggle={() =>
setSortOrder((o) => (o === 'asc' ? 'desc' : 'asc'))
}
viewMode={viewMode}
onViewModeChange={setViewMode}
selectedCount={selectedFiles.length}
onBulkDownload={() => handleBulkAction('Downloaded')}
onBulkTag={() => handleBulkAction('Tagged')}
onBulkDelete={() => handleBulkAction('Deleted')}
onMetadataClick={() => setShowMetadataModal(true)}
onWatermarkClick={() => setShowWatermarkModal(true)}
availableTags={FILE_BROWSER_DEFAULT_TAGS}
/>
<div className="flex-1 overflow-y-auto custom-scrollbar min-h-layout-page-sm">
{viewMode === 'list' ? (
<FileTable
files={filteredFiles}
selectedIds={selectedFiles}
onSelectAll={selectAll}
onToggleSelect={toggleSelection}
sortField={sortField}
onSort={handleSort}
onFileClick={handleFileClick}
onRowAction={(action) => {
if (action === 'ai') addToast('Sent to AI Tools');
}}
/>
) : (
<FileGrid
files={filteredFiles}
selectedIds={selectedFiles}
onFileClick={handleFileClick}
onToggleSelect={toggleSelection}
/>
)}
</div>
{showMetadataModal && (
<AutoMetadataDetectionModal
fileName={
selectedFileId
? 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>
);
};