188 lines
6.5 KiB
TypeScript
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>
|
|
);
|
|
};
|