H2IBYJKO64HU6N2V2WUQPONZZGKAHLR7EG7T2UYMY5ENHSGGZZJAC // File scanning stateconst [audioFiles, setAudioFiles] = useState<FileSystemFileHandle[]>([]);const [scanProgress, setScanProgress] = useState<{ current: number; total: number }>({ current: 0, total: 0 });const [processProgress, setProcessProgress] = useState<{ current: number; total: number }>({ current: 0, total: 0 });
const scanAudioFiles = useCallback(async (directoryHandle: FileSystemDirectoryHandle) => {const audioFiles: FileSystemFileHandle[] = [];const audioExtensions = ['.wav', '.WAV'];const scanDirectory = async (dirHandle: FileSystemDirectoryHandle, currentPath = '') => {for await (const [name, handle] of dirHandle.entries()) {if (handle.kind === 'file') {// Check if it's an audio fileconst hasAudioExtension = audioExtensions.some(ext => name.endsWith(ext));if (hasAudioExtension) {try {// Type cast to FileSystemFileHandleconst fileHandle = handle as FileSystemFileHandle;// Get file info to check sizeconst file = await fileHandle.getFile();if (file.size > 0) {// Only include files with size > 0kbaudioFiles.push(fileHandle);}} catch (error) {console.warn(`Could not access file ${name}:`, error);}}// Update scan progresssetScanProgress(prev => ({ ...prev, current: prev.current + 1 }));} else if (handle.kind === 'directory') {// Recursively scan subdirectoriesconst dirHandle = handle as FileSystemDirectoryHandle;await scanDirectory(dirHandle, `${currentPath}/${name}`);}}};// First pass: count total files to set progress totallet totalFiles = 0;const countFiles = async (dirHandle: FileSystemDirectoryHandle) => {for await (const [, handle] of dirHandle.entries()) {if (handle.kind === 'file') {totalFiles++;} else if (handle.kind === 'directory') {const subDirHandle = handle as FileSystemDirectoryHandle;await countFiles(subDirHandle);}}};await countFiles(directoryHandle);setScanProgress({ current: 0, total: totalFiles });
const handleStartImport = () => {// TODO: Implement in next phaseconsole.log('Starting import for cluster:', clusterId, 'dataset:', datasetId, 'location:', locationId);// TODO: Call onImportComplete() when import is finished// For now, just close the dialog as a placeholdersetTimeout(() => {onImportComplete();handleClose();}, 100);};
// Second pass: scan for audio filesawait scanDirectory(directoryHandle);return audioFiles;}, []);const handleStartImport = useCallback(async () => {if (!selectedFolder) return;try {// Start scanningsetImportState('scanning');console.log('Starting file scan for folder:', folderPath);const scannedFiles = await scanAudioFiles(selectedFolder);setAudioFiles(scannedFiles);console.log(`Found ${scannedFiles.length} audio files`);// Move to ready statesetImportState('ready');// Start processing immediately for now (just a loop doing nothing)setImportState('importing');setProcessProgress({ current: 0, total: scannedFiles.length });// Process files one by one (doing nothing for now, just showing progress)for (let i = 0; i < scannedFiles.length; i++) {const fileHandle = scannedFiles[i];const file = await fileHandle.getFile();// Simulate some processing timeawait new Promise(resolve => setTimeout(resolve, 10));console.log(`Processing file ${i + 1}/${scannedFiles.length}:`, file.name);setProcessProgress({ current: i + 1, total: scannedFiles.length });}// CompletesetImportState('completed');setTimeout(() => {onImportComplete();handleClose();}, 1000);} catch (error) {console.error('Error during import:', error);// Handle error state}}, [selectedFolder, folderPath, scanAudioFiles, onImportComplete, handleClose]);
) : importState === 'scanning' ? (// Scanning state<div className="text-center"><div className="mb-4"><svg className="w-12 h-12 text-blue-500 mx-auto animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg></div><h3 className="text-lg font-medium text-gray-900 mb-2">Scanning Folder</h3><p className="text-sm text-gray-600 mb-4">Looking for audio files...</p>{scanProgress.total > 0 && (<div className="w-full bg-gray-200 rounded-full h-2"><divclassName="bg-blue-600 h-2 rounded-full transition-all duration-300"style={{ width: `${(scanProgress.current / scanProgress.total) * 100}%` }}></div></div>)}<p className="text-xs text-gray-500 mt-2">{scanProgress.current} / {scanProgress.total} files scanned</p></div>) : importState === 'ready' ? (// Ready state - show found files<div className="text-center"><div className="mb-4"><svg className="w-12 h-12 text-green-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg></div><h3 className="text-lg font-medium text-gray-900 mb-2">Ready to Import</h3><p className="text-sm text-gray-600 mb-4">Found {audioFiles.length} audio files ready for import</p></div>) : importState === 'importing' ? (// Importing state<div className="text-center"><div className="mb-4"><svg className="w-12 h-12 text-blue-500 mx-auto animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /></svg></div><h3 className="text-lg font-medium text-gray-900 mb-2">Processing Files</h3><p className="text-sm text-gray-600 mb-4">Importing audio files to cluster...</p>{processProgress.total > 0 && (<div className="w-full bg-gray-200 rounded-full h-2"><divclassName="bg-green-600 h-2 rounded-full transition-all duration-300"style={{ width: `${(processProgress.current / processProgress.total) * 100}%` }}></div></div>)}<p className="text-xs text-gray-500 mt-2">{processProgress.current} / {processProgress.total} files processed</p></div>) : importState === 'completed' ? (// Completed state<div className="text-center"><div className="mb-4"><svg className="w-12 h-12 text-green-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg></div><h3 className="text-lg font-medium text-gray-900 mb-2">Import Complete</h3><p className="text-sm text-gray-600 mb-4">Successfully imported {audioFiles.length} audio files</p></div>
Thats great, thanks. Now, when I click on a cluster with no files we need to be able to import files into the ││ cluster. This is quite complicated so we need to first formulate a plan. ││ Adding files requires the following tables be updated: file, moth_metadata, file_dataset ││ we need to add all the .WAV or .wav files in a given folder opened using the file system access api. ││ file: ││ id nanoid(21) ││ path (path of folder so user can find the files again) ││ we need to make an xxh64 hash of the audio using a reproducible method so we can always recognise the file, I ││ will supply example code. ││ we need to record location_id and cluster_id from component props ││ we need to grab metadata if present. If the file was recorded by an audio moth we need to parse the metadata, ││ including time ││ if no metadata present we need to parse the filenames using the cluster:timezone_id to get a datestamp. ││ we need to use suncalc to calculate moon phase and if the file is recoded during solar or civil night ││ moth_metadata: ││ parsed from file metadata if recorded by an audio moth (will supply example code) ││ file_dataset: ││ junction table to associate files and datasets, should be updated ││ Lets break this task up: ││ First, lets create a new dialog to open and display files in a folder using the file system access api