diff --git a/.gitignore b/.gitignore index 600961b..173c5de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,66 @@ +# Dependencies node_modules/ +package-lock.json +.pnpm-store/ + +# Build output dist/ out/ +release/ +*.tsbuildinfo + +# Electron builder +builder-debug.yml +builder-effective-config.yaml + +# Logs *.log -.DS_Store -Thumbs.db npm-debug.log* yarn-debug.log* yarn-error.log* -package-lock.json +lerna-debug.log* +pnpm-debug.log* + +# OS files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +desktop.ini + +# IDE & Editor +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.idea/ +*.swp +*.swo +*~ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Test data & databases +test-data/ +*.db +*.db-shm +*.db-wal + +# Environment & Config +.env +.env.local +.env.*.local +.npmrc + +# Temporary files +*.tmp +*.temp +.cache/ diff --git a/package.json b/package.json index 51e5766..1c5ffa8 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ "build:main": "tsc -p tsconfig.main.json", "lint": "tsc --noEmit", "start": "cross-env NODE_ENV=production electron ./dist/main/main/index.js", + "pack": "npm run build && electron-builder --dir", + "dist": "npm run build && electron-builder", + "dist:win": "npm run build && electron-builder --win", "test": "node --import tsx --test src/test/DatabaseService.test.ts src/test/TagService.test.ts src/test/LibraryService.test.ts src/test/SearchService.test.ts", "test:tag": "node --import tsx --test src/test/TagService.test.ts", "test:library": "node --import tsx --test src/test/LibraryService.test.ts", @@ -39,9 +42,35 @@ "concurrently": "^8.2.2", "cross-env": "^7.0.3", "electron": "^29.4.4", + "electron-builder": "^26.0.12", "tree-kill": "^1.2.2", "tsx": "^4.15.7", "typescript": "^5.3.3", "vite": "^5.1.6" + }, + "build": { + "appId": "com.audiosort.app", + "productName": "AudioSort", + "files": [ + "dist/**/*", + "UCS.csv" + ], + "directories": { + "output": "release" + }, + "win": { + "target": ["nsis"], + "icon": "build/icon.ico" + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true + }, + "extraResources": [ + { + "from": "UCS.csv", + "to": "UCS.csv" + } + ] } } diff --git a/src/main/MainApp.ts b/src/main/MainApp.ts index 4f87115..bb24b57 100644 --- a/src/main/MainApp.ts +++ b/src/main/MainApp.ts @@ -138,9 +138,9 @@ export class MainApp { ipcMain.removeHandler(IPC_CHANNELS.libraryMove); ipcMain.removeHandler(IPC_CHANNELS.libraryOrganize); ipcMain.removeHandler(IPC_CHANNELS.libraryBuffer); - ipcMain.removeHandler(IPC_CHANNELS.libraryMetadata); - ipcMain.removeHandler(IPC_CHANNELS.libraryMetadataSuggestions); - ipcMain.removeHandler(IPC_CHANNELS.libraryUpdateMetadata); + ipcMain.removeHandler(IPC_CHANNELS.libraryMetadata); + ipcMain.removeHandler(IPC_CHANNELS.libraryMetadataSuggestions); + ipcMain.removeHandler(IPC_CHANNELS.libraryUpdateMetadata); ipcMain.removeHandler(IPC_CHANNELS.libraryWaveformPreview); ipcMain.removeHandler(IPC_CHANNELS.tagsUpdate); ipcMain.removeHandler(IPC_CHANNELS.categoriesList); @@ -163,7 +163,7 @@ export class MainApp { * Creates the renderer window and loads the UI. */ private createWindow(): void { - const preloadPath = this.resolvePreloadPath(); + const preloadPath = this.resolvePreloadPath(); this.mainWindow = new BrowserWindow({ width: 1280, height: 800, @@ -186,18 +186,16 @@ export class MainApp { this.mainWindow = null; }); - const devServerUrl = (globalThis as { process?: { env?: Record } }).process?.env - ?.VITE_DEV_SERVER_URL; + const devServerUrl = (globalThis as { process?: { env?: Record } }).process?.env?.VITE_DEV_SERVER_URL; if (devServerUrl) { this.mainWindow.loadURL(devServerUrl).catch((error: unknown) => { - // eslint-disable-next-line no-console -- Logging is useful during development to diagnose boot issues. console.error('Failed to load renderer URL', error); }); } else { const rendererIndex = this.resolveRendererIndex(); - this.mainWindow - .loadFile(rendererIndex) - .catch((error: unknown) => console.error('Failed to load renderer bundle', error)); + this.mainWindow.loadFile(rendererIndex).catch((error: unknown) => { + console.error('Failed to load renderer bundle', error); + }); } } @@ -214,7 +212,7 @@ export class MainApp { }); ipcMain.handle(IPC_CHANNELS.dialogSelectLibrary, async () => { - const options: Electron.OpenDialogOptions = { properties: ['openDirectory'] }; + const options: Electron.OpenDialogOptions = { properties: ['openDirectory'] }; const targetWindow = this.mainWindow; const result = targetWindow ? await dialog.showOpenDialog(targetWindow, options) @@ -241,8 +239,8 @@ export class MainApp { ipcMain.handle( IPC_CHANNELS.libraryOrganize, - async (_event: IpcMainInvokeEvent, fileId: number, metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }) => - this.requireLibrary().organizeFile(fileId, metadata) + async (_event: IpcMainInvokeEvent, fileId: number, metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }) => + this.requireLibrary().organizeFile(fileId, metadata) ); ipcMain.handle( @@ -285,7 +283,7 @@ export class MainApp { ipcMain.handle( IPC_CHANNELS.libraryUpdateMetadata, - async (_event: IpcMainInvokeEvent, fileId: number, metadata: { author?: string | null; copyright?: string | null; rating?: number }) => + async (_event: IpcMainInvokeEvent, fileId: number, metadata: { author?: string | null; copyright?: string | null; rating?: number }) => this.requireLibrary().updateFileMetadata(fileId, metadata) ); } diff --git a/src/main/index.ts b/src/main/index.ts index a868aea..d464fc5 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -6,7 +6,6 @@ const mainApp = new MainApp(); app.whenReady() .then(() => mainApp.initialize()) .catch((error: unknown) => { - // eslint-disable-next-line no-console -- Logging critical boot failures is necessary for troubleshooting. console.error('Failed to initialise application', error); app.quit(); }); diff --git a/src/main/services/LibraryService.ts b/src/main/services/LibraryService.ts index c5c8e95..6040995 100644 --- a/src/main/services/LibraryService.ts +++ b/src/main/services/LibraryService.ts @@ -84,12 +84,7 @@ export class LibraryService { this.resetMetadataSuggestionsCache(); this.waveformPreviewCache.clear(); - // Clean up any orphaned temp files before scanning const cleanedTempFiles = await this.cleanupTempFiles(); - if (cleanedTempFiles > 0) { - console.log(`Cleaned up ${cleanedTempFiles} orphaned temp files`); - } - const libraryRoot = this.settings.ensureLibraryPath(); const existing = this.database.listFiles(); const existingByPath = new Map(existing.map((file) => [file.absolutePath, file] as const)); @@ -213,18 +208,13 @@ export class LibraryService { // Check if the intended destination exists try { await fs.access(intendedPath); - // Final file exists, safe to delete temp await fs.unlink(tempFile); - console.log(`Cleaned up temp file (final exists): ${tempFile}`); cleanedCount++; } catch { - // Final file doesn't exist, rename temp to final await fs.rename(tempFile, intendedPath); - console.log(`Recovered temp file: ${tempFile} -> ${intendedPath}`); cleanedCount++; } } else { - // Unknown temp file pattern, log but don't delete console.warn(`Found temp file with unknown pattern: ${tempFile}`); } } catch (error) { @@ -448,8 +438,6 @@ export class LibraryService { metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number } ): Promise { const record = this.database.getFileById(fileId); - console.log(`[organizeFile] START: fileId=${fileId}, fileName=${record.fileName}, customName=${metadata.customName}`); - const category = this.organization.getPrimaryCategory(record); if (!category) { throw new Error('Cannot organize file without category assignment'); @@ -465,9 +453,6 @@ export class LibraryService { .listConflictingFiles(folderPath, baseName) .filter((file) => file.id !== record.id); const requireNumbering = !effectiveCustomName || conflicts.length > 0; - - console.log(`[organizeFile] fileId=${fileId}, baseName=${baseName}, conflicts=${conflicts.length}, requireNumbering=${requireNumbering}`); - const targetDirectory = path.resolve(libraryRoot, folderPath); this.assertWithinLibrary(libraryRoot, targetDirectory); const currentFolder = this.normalizeRelativePath(path.dirname(record.relativePath)); @@ -553,23 +538,16 @@ export class LibraryService { const conflictFolder = this.normalizeRelativePath(path.dirname(conflict.relativePath)); const isInTargetFolder = conflictFolder === targetFolderNormalized; const hasNumberedName = pattern.test(conflict.fileName); - console.log(`[organizeFile] Conflict fileId=${conflict.id}, fileName=${conflict.fileName}, isInTarget=${isInTargetFolder}, hasNumber=${hasNumberedName}`); - // Only renumber if it's not in the target folder OR doesn't have a numbered name return !isInTargetFolder || !hasNumberedName; }); - console.log(`[organizeFile] fileId=${fileId}, conflictsNeedingRenumber=${conflictsNeedingRenumber.length}/${conflicts.length}`); - if (conflictsNeedingRenumber.length > 0) { await this.renumberConflictingFiles(conflictsNeedingRenumber, folderPath, baseName, libraryRoot); - // After renumbering conflicts, recalculate the target filename for this file - // because the available numbers have changed const nextIndex = await this.findNextAvailableNumberWithFilesystemCheck(folderPath, baseName, targetDirectory); targetFileName = `${baseName}_${this.organization.formatSequenceNumber(nextIndex)}.wav`; targetPath = path.join(targetDirectory, targetFileName); targetRelativePath = this.toLibraryRelativePath(folderPath, targetFileName); - console.log(`[organizeFile] After renumbering, recalculated target for fileId=${fileId}: ${targetFileName}`); } } @@ -590,26 +568,17 @@ export class LibraryService { } catch (error: unknown) { // Check if this is a UNIQUE constraint error on absolute_path const errorMessage = error instanceof Error ? error.message : String(error); - const errorString = JSON.stringify(error); if (errorMessage.includes('UNIQUE constraint') && errorMessage.includes('absolute_path')) { - console.log(`[LibraryService] Caught UNIQUE constraint error for ${targetPath}, retrying with numbering...`); - - // Rollback filesystem change await fs.rename(targetPath, record.absolutePath); - // Find next available number and retry const safeIndex = await this.findNextAvailableNumberWithFilesystemCheck(folderPath, baseName, targetDirectory); targetFileName = `${baseName}_${this.organization.formatSequenceNumber(safeIndex)}.wav`; targetPath = path.join(targetDirectory, targetFileName); targetRelativePath = this.toLibraryRelativePath(folderPath, targetFileName); - console.log(`[LibraryService] Retrying with numbered filename: ${targetFileName}`); - - // Retry filesystem operation await fs.rename(record.absolutePath, targetPath); - // Retry database operation updated = this.database.updateFileLocation( fileId, targetPath, @@ -618,7 +587,7 @@ export class LibraryService { path.basename(targetFileName, '.wav') ); } else { - console.error(`[LibraryService] Unexpected error during updateFileLocation:`, errorMessage, errorString); + console.error(`Unexpected error during updateFileLocation:`, errorMessage); throw error; } } @@ -659,8 +628,6 @@ export class LibraryService { this.resetMetadataSuggestionsCache(); this.waveformPreviewCache.delete(fileId); this.search.rebuildIndex(); - - console.log(`[organizeFile] SUCCESS: fileId=${fileId}, finalPath=${updated.absolutePath}, finalName=${updated.fileName}`); return updated; } @@ -848,7 +815,6 @@ export class LibraryService { const finalAbsolutePath = path.join(targetDirectory, finalFileName); const relativePath = this.toLibraryRelativePath(folderPath, finalFileName); const tempPath = `${finalAbsolutePath}.${Date.now()}-${file.id}-${Math.random().toString(16).slice(2)}.tmp`; - console.log(`[renumberConflictingFiles] plan fileId=${file.id}, current=${file.fileName}, final=${finalFileName}`); return { file, tempPath, @@ -862,18 +828,13 @@ export class LibraryService { const movedToTemp: typeof plans = []; try { - // Phase 1: Move all files to temp names for (const plan of plans) { - // Use the fresh absolutePath from database await fs.rename(plan.file.absolutePath, plan.tempPath); - console.log(`[renumberConflictingFiles] moved to temp: ${plan.file.absolutePath} -> ${plan.tempPath}`); movedToTemp.push(plan); } - // Phase 2: Move all temp files to final names and update database for (const plan of plans) { await fs.rename(plan.tempPath, plan.finalAbsolutePath); - console.log(`[renumberConflictingFiles] finalized: ${plan.tempPath} -> ${plan.finalAbsolutePath}`); this.database.updateFileLocation( plan.file.id, plan.finalAbsolutePath, @@ -883,23 +844,17 @@ export class LibraryService { ); } } catch (error) { - // Rollback: attempt to restore any files that were moved to temp console.error('Error during batch rename, attempting rollback:', error); for (const plan of movedToTemp) { try { - // Check if temp file exists await fs.access(plan.tempPath); - // Try to restore to original location await fs.rename(plan.tempPath, plan.file.absolutePath); - console.log(`Rolled back: ${plan.tempPath} -> ${plan.file.absolutePath}`); } catch (rollbackError) { console.error(`Failed to rollback ${plan.tempPath}:`, rollbackError); - // If rollback fails, try to at least rename temp to final destination try { await fs.rename(plan.tempPath, plan.finalAbsolutePath); - console.log(`Recovered: ${plan.tempPath} -> ${plan.finalAbsolutePath}`); - } catch (recoveryError) { - console.error(`Failed to recover ${plan.tempPath}:`, recoveryError); + } catch { + // Recovery failed, file may be lost } } } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 6a868e3..2e55832 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -132,9 +132,6 @@ function App(): JSX.Element { libraryStore.selectFile(fileId, options); }; - /** - * Selects every visible file so keyboard shortcuts mirror multi-select gestures. - */ const handleSelectAllFiles = () => { libraryStore.selectAllVisibleFiles(); }; diff --git a/src/renderer/src/components/FileList.tsx b/src/renderer/src/components/FileList.tsx index 92c27d3..6dcb779 100644 --- a/src/renderer/src/components/FileList.tsx +++ b/src/renderer/src/components/FileList.tsx @@ -23,11 +23,11 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea const waveformCacheRef = useRef>({}); const [, forceWaveformUpdate] = useReducer((value: number) => value + 1, 0); const dragGhostRef = useRef(null); - const dragRotation = useRef(0); - const lastDragX = useRef(0); + const dragRotation = useRef(0); + const lastDragX = useRef(0); const dropTargetPosition = useRef<{ x: number; y: number } | null>(null); - const dragOffset = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); - const dragStartTime = useRef(0); + const dragOffset = useRef({ x: 0, y: 0 }); + const dragStartTime = useRef(0); const listContainerRef = useRef(null); useEffect(() => { @@ -44,7 +44,6 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea if (isTypingContext || isEditable) { return; } - // Keep keyboard focus aligned with the active selection. button.focus(); }, [selectedId]); @@ -197,8 +196,6 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea onClick={(event) => handleClick(file.id, event)} onDoubleClick={() => handleDoubleClick(file)} onDragStart={(event) => { - // If dragging an unselected item, only drag that item - // If dragging a selected item, drag all selected items const draggedIds = selectedIds.has(file.id) ? Array.from(selectedIds) : [file.id]; event.dataTransfer.setData('application/audiosort-file', JSON.stringify({ @@ -207,7 +204,6 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea })); event.dataTransfer.effectAllowed = 'copy'; - // Create transparent drag image to hide the default const transparent = document.createElement('div'); transparent.style.width = '1px'; transparent.style.height = '1px'; @@ -216,7 +212,6 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea event.dataTransfer.setDragImage(transparent, 0, 0); setTimeout(() => transparent.remove(), 0); - // Calculate offset from click position to element center const sourceElement = event.currentTarget as HTMLElement; const rect = sourceElement.getBoundingClientRect(); const elementCenterX = rect.left + rect.width / 2; @@ -226,21 +221,18 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea y: elementCenterY - event.clientY }; - // Calculate transition duration based on offset distance const offsetDistance = Math.sqrt( dragOffset.current.x * dragOffset.current.x + dragOffset.current.y * dragOffset.current.y ); const maxDistance = Math.sqrt(rect.width * rect.width + rect.height * rect.height) / 2; const normalizedDistance = Math.min(offsetDistance / maxDistance, 1); - const transitionDuration = 0.1 + normalizedDistance * 0.2; // 0.1s to 0.3s + const transitionDuration = 0.1 + normalizedDistance * 0.2; dragStartTime.current = Date.now(); - // Clone the current element for custom floating preview const ghost = sourceElement.cloneNode(true) as HTMLElement; - // Create a wrapper for rotation that doesn't affect position transition const rotationWrapper = document.createElement('div'); rotationWrapper.style.position = 'fixed'; rotationWrapper.style.top = `${event.clientY}px`; @@ -264,7 +256,6 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea rotationWrapper.appendChild(ghost); document.body.appendChild(rotationWrapper); - // Trigger the offset transition to center requestAnimationFrame(() => { if (ghost.parentNode) { ghost.style.transform = `translate(-50%, -50%)`; @@ -286,25 +277,19 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea const clamped = Math.max(-12, Math.min(12, blended)); dragRotation.current = clamped; - // Update wrapper position (no rotation here) dragGhostRef.current.style.top = `${event.clientY}px`; dragGhostRef.current.style.left = `${event.clientX}px`; - - // Apply rotation directly to wrapper without transition dragGhostRef.current.style.transform = `rotate(${clamped}deg)`; - // Track position for potential drop animation dropTargetPosition.current = { x: event.clientX, y: event.clientY }; }} onDragEnd={(event) => { const wrapper = dragGhostRef.current; if (!wrapper) return; - // Check if this was a successful drop (dropEffect will be 'copy' if accepted) const wasDropped = event.dataTransfer.dropEffect === 'copy'; if (wasDropped && dropTargetPosition.current) { - // Animate shrink into drop position wrapper.style.transition = 'all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)'; wrapper.style.transform = `rotate(0deg) scale(0)`; wrapper.style.opacity = '0'; @@ -314,7 +299,6 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea dragGhostRef.current = null; }, 300); } else { - // No drop or cancelled - remove immediately wrapper.remove(); dragGhostRef.current = null; } diff --git a/src/renderer/src/components/TagEditor.tsx b/src/renderer/src/components/TagEditor.tsx index a6f3e46..fc41228 100644 --- a/src/renderer/src/components/TagEditor.tsx +++ b/src/renderer/src/components/TagEditor.tsx @@ -55,18 +55,14 @@ export function TagEditor({ tags, categories, availableCategories, onSave, showH setTagDraft(tags.join(', ')); setSelectedCategories(new Set(categories)); - // Sync collapsed state from module state when file changes (but not on first render) if (!isFirstRender.current) { const moduleState = getCollapsedGroups(); - console.log('TagEditor syncing collapsed state from module:', Array.from(moduleState!)); setCollapsedGroups(new Set(moduleState!)); } else { isFirstRender.current = false; } }, [tags, categories]); - // No need for this effect anymore - categories don't change dynamically - const { groupedCategories, filteredResults } = useMemo(() => { const filter = categoryFilter.trim().toLowerCase(); const filtered = filter.length === 0 @@ -139,7 +135,6 @@ export function TagEditor({ tags, categories, availableCategories, onSave, showH moduleState.add(groupName); } setModuleCollapsedGroups(moduleState); - console.log('TagEditor toggleGroup:', groupName, 'collapsed:', next.has(groupName), 'module state:', Array.from(moduleState)); setCollapsedGroups(next); }; @@ -192,7 +187,7 @@ export function TagEditor({ tags, categories, availableCategories, onSave, showH return (
- {showHeading &&

Tags

} + {showHeading &&

Tags

}