mirror of
https://github.com/litruv/AudioSort.git
synced 2026-07-24 02:36:01 +10:00
Enhance project configuration and cleanup
- Updated .gitignore to include additional files and directories for better exclusion. - Modified package.json to add new build scripts and electron-builder configuration. - Refactored MainApp.ts for improved IPC handler management and code formatting. - Cleaned up LibraryService.ts by removing unnecessary logging and comments. - Adjusted App.tsx and FileList.tsx for code clarity and consistency. - Removed redundant console logs in TagEditor.tsx and CategoryUIState.ts. - Updated tsconfig files for module resolution consistency.
This commit is contained in:
62
.gitignore
vendored
62
.gitignore
vendored
@@ -1,10 +1,66 @@
|
|||||||
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
|
# Build output
|
||||||
dist/
|
dist/
|
||||||
out/
|
out/
|
||||||
|
release/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Electron builder
|
||||||
|
builder-debug.yml
|
||||||
|
builder-effective-config.yaml
|
||||||
|
|
||||||
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.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/
|
||||||
|
|||||||
29
package.json
29
package.json
@@ -14,6 +14,9 @@
|
|||||||
"build:main": "tsc -p tsconfig.main.json",
|
"build:main": "tsc -p tsconfig.main.json",
|
||||||
"lint": "tsc --noEmit",
|
"lint": "tsc --noEmit",
|
||||||
"start": "cross-env NODE_ENV=production electron ./dist/main/main/index.js",
|
"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": "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:tag": "node --import tsx --test src/test/TagService.test.ts",
|
||||||
"test:library": "node --import tsx --test src/test/LibraryService.test.ts",
|
"test:library": "node --import tsx --test src/test/LibraryService.test.ts",
|
||||||
@@ -39,9 +42,35 @@
|
|||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"electron": "^29.4.4",
|
"electron": "^29.4.4",
|
||||||
|
"electron-builder": "^26.0.12",
|
||||||
"tree-kill": "^1.2.2",
|
"tree-kill": "^1.2.2",
|
||||||
"tsx": "^4.15.7",
|
"tsx": "^4.15.7",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^5.1.6"
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,9 +138,9 @@ export class MainApp {
|
|||||||
ipcMain.removeHandler(IPC_CHANNELS.libraryMove);
|
ipcMain.removeHandler(IPC_CHANNELS.libraryMove);
|
||||||
ipcMain.removeHandler(IPC_CHANNELS.libraryOrganize);
|
ipcMain.removeHandler(IPC_CHANNELS.libraryOrganize);
|
||||||
ipcMain.removeHandler(IPC_CHANNELS.libraryBuffer);
|
ipcMain.removeHandler(IPC_CHANNELS.libraryBuffer);
|
||||||
ipcMain.removeHandler(IPC_CHANNELS.libraryMetadata);
|
ipcMain.removeHandler(IPC_CHANNELS.libraryMetadata);
|
||||||
ipcMain.removeHandler(IPC_CHANNELS.libraryMetadataSuggestions);
|
ipcMain.removeHandler(IPC_CHANNELS.libraryMetadataSuggestions);
|
||||||
ipcMain.removeHandler(IPC_CHANNELS.libraryUpdateMetadata);
|
ipcMain.removeHandler(IPC_CHANNELS.libraryUpdateMetadata);
|
||||||
ipcMain.removeHandler(IPC_CHANNELS.libraryWaveformPreview);
|
ipcMain.removeHandler(IPC_CHANNELS.libraryWaveformPreview);
|
||||||
ipcMain.removeHandler(IPC_CHANNELS.tagsUpdate);
|
ipcMain.removeHandler(IPC_CHANNELS.tagsUpdate);
|
||||||
ipcMain.removeHandler(IPC_CHANNELS.categoriesList);
|
ipcMain.removeHandler(IPC_CHANNELS.categoriesList);
|
||||||
@@ -163,7 +163,7 @@ export class MainApp {
|
|||||||
* Creates the renderer window and loads the UI.
|
* Creates the renderer window and loads the UI.
|
||||||
*/
|
*/
|
||||||
private createWindow(): void {
|
private createWindow(): void {
|
||||||
const preloadPath = this.resolvePreloadPath();
|
const preloadPath = this.resolvePreloadPath();
|
||||||
this.mainWindow = new BrowserWindow({
|
this.mainWindow = new BrowserWindow({
|
||||||
width: 1280,
|
width: 1280,
|
||||||
height: 800,
|
height: 800,
|
||||||
@@ -186,18 +186,16 @@ export class MainApp {
|
|||||||
this.mainWindow = null;
|
this.mainWindow = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const devServerUrl = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env
|
const devServerUrl = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.VITE_DEV_SERVER_URL;
|
||||||
?.VITE_DEV_SERVER_URL;
|
|
||||||
if (devServerUrl) {
|
if (devServerUrl) {
|
||||||
this.mainWindow.loadURL(devServerUrl).catch((error: unknown) => {
|
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);
|
console.error('Failed to load renderer URL', error);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const rendererIndex = this.resolveRendererIndex();
|
const rendererIndex = this.resolveRendererIndex();
|
||||||
this.mainWindow
|
this.mainWindow.loadFile(rendererIndex).catch((error: unknown) => {
|
||||||
.loadFile(rendererIndex)
|
console.error('Failed to load renderer bundle', error);
|
||||||
.catch((error: unknown) => console.error('Failed to load renderer bundle', error));
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +212,7 @@ export class MainApp {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.dialogSelectLibrary, async () => {
|
ipcMain.handle(IPC_CHANNELS.dialogSelectLibrary, async () => {
|
||||||
const options: Electron.OpenDialogOptions = { properties: ['openDirectory'] };
|
const options: Electron.OpenDialogOptions = { properties: ['openDirectory'] };
|
||||||
const targetWindow = this.mainWindow;
|
const targetWindow = this.mainWindow;
|
||||||
const result = targetWindow
|
const result = targetWindow
|
||||||
? await dialog.showOpenDialog(targetWindow, options)
|
? await dialog.showOpenDialog(targetWindow, options)
|
||||||
@@ -241,8 +239,8 @@ export class MainApp {
|
|||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
IPC_CHANNELS.libraryOrganize,
|
IPC_CHANNELS.libraryOrganize,
|
||||||
async (_event: IpcMainInvokeEvent, fileId: number, metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }) =>
|
async (_event: IpcMainInvokeEvent, fileId: number, metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }) =>
|
||||||
this.requireLibrary().organizeFile(fileId, metadata)
|
this.requireLibrary().organizeFile(fileId, metadata)
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
@@ -285,7 +283,7 @@ export class MainApp {
|
|||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
IPC_CHANNELS.libraryUpdateMetadata,
|
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)
|
this.requireLibrary().updateFileMetadata(fileId, metadata)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ const mainApp = new MainApp();
|
|||||||
app.whenReady()
|
app.whenReady()
|
||||||
.then(() => mainApp.initialize())
|
.then(() => mainApp.initialize())
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
// eslint-disable-next-line no-console -- Logging critical boot failures is necessary for troubleshooting.
|
|
||||||
console.error('Failed to initialise application', error);
|
console.error('Failed to initialise application', error);
|
||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -84,12 +84,7 @@ export class LibraryService {
|
|||||||
this.resetMetadataSuggestionsCache();
|
this.resetMetadataSuggestionsCache();
|
||||||
this.waveformPreviewCache.clear();
|
this.waveformPreviewCache.clear();
|
||||||
|
|
||||||
// Clean up any orphaned temp files before scanning
|
|
||||||
const cleanedTempFiles = await this.cleanupTempFiles();
|
const cleanedTempFiles = await this.cleanupTempFiles();
|
||||||
if (cleanedTempFiles > 0) {
|
|
||||||
console.log(`Cleaned up ${cleanedTempFiles} orphaned temp files`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const libraryRoot = this.settings.ensureLibraryPath();
|
const libraryRoot = this.settings.ensureLibraryPath();
|
||||||
const existing = this.database.listFiles();
|
const existing = this.database.listFiles();
|
||||||
const existingByPath = new Map(existing.map((file) => [file.absolutePath, file] as const));
|
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
|
// Check if the intended destination exists
|
||||||
try {
|
try {
|
||||||
await fs.access(intendedPath);
|
await fs.access(intendedPath);
|
||||||
// Final file exists, safe to delete temp
|
|
||||||
await fs.unlink(tempFile);
|
await fs.unlink(tempFile);
|
||||||
console.log(`Cleaned up temp file (final exists): ${tempFile}`);
|
|
||||||
cleanedCount++;
|
cleanedCount++;
|
||||||
} catch {
|
} catch {
|
||||||
// Final file doesn't exist, rename temp to final
|
|
||||||
await fs.rename(tempFile, intendedPath);
|
await fs.rename(tempFile, intendedPath);
|
||||||
console.log(`Recovered temp file: ${tempFile} -> ${intendedPath}`);
|
|
||||||
cleanedCount++;
|
cleanedCount++;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unknown temp file pattern, log but don't delete
|
|
||||||
console.warn(`Found temp file with unknown pattern: ${tempFile}`);
|
console.warn(`Found temp file with unknown pattern: ${tempFile}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -448,8 +438,6 @@ export class LibraryService {
|
|||||||
metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }
|
metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }
|
||||||
): Promise<AudioFileSummary> {
|
): Promise<AudioFileSummary> {
|
||||||
const record = this.database.getFileById(fileId);
|
const record = this.database.getFileById(fileId);
|
||||||
console.log(`[organizeFile] START: fileId=${fileId}, fileName=${record.fileName}, customName=${metadata.customName}`);
|
|
||||||
|
|
||||||
const category = this.organization.getPrimaryCategory(record);
|
const category = this.organization.getPrimaryCategory(record);
|
||||||
if (!category) {
|
if (!category) {
|
||||||
throw new Error('Cannot organize file without category assignment');
|
throw new Error('Cannot organize file without category assignment');
|
||||||
@@ -465,9 +453,6 @@ export class LibraryService {
|
|||||||
.listConflictingFiles(folderPath, baseName)
|
.listConflictingFiles(folderPath, baseName)
|
||||||
.filter((file) => file.id !== record.id);
|
.filter((file) => file.id !== record.id);
|
||||||
const requireNumbering = !effectiveCustomName || conflicts.length > 0;
|
const requireNumbering = !effectiveCustomName || conflicts.length > 0;
|
||||||
|
|
||||||
console.log(`[organizeFile] fileId=${fileId}, baseName=${baseName}, conflicts=${conflicts.length}, requireNumbering=${requireNumbering}`);
|
|
||||||
|
|
||||||
const targetDirectory = path.resolve(libraryRoot, folderPath);
|
const targetDirectory = path.resolve(libraryRoot, folderPath);
|
||||||
this.assertWithinLibrary(libraryRoot, targetDirectory);
|
this.assertWithinLibrary(libraryRoot, targetDirectory);
|
||||||
const currentFolder = this.normalizeRelativePath(path.dirname(record.relativePath));
|
const currentFolder = this.normalizeRelativePath(path.dirname(record.relativePath));
|
||||||
@@ -553,23 +538,16 @@ export class LibraryService {
|
|||||||
const conflictFolder = this.normalizeRelativePath(path.dirname(conflict.relativePath));
|
const conflictFolder = this.normalizeRelativePath(path.dirname(conflict.relativePath));
|
||||||
const isInTargetFolder = conflictFolder === targetFolderNormalized;
|
const isInTargetFolder = conflictFolder === targetFolderNormalized;
|
||||||
const hasNumberedName = pattern.test(conflict.fileName);
|
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;
|
return !isInTargetFolder || !hasNumberedName;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[organizeFile] fileId=${fileId}, conflictsNeedingRenumber=${conflictsNeedingRenumber.length}/${conflicts.length}`);
|
|
||||||
|
|
||||||
if (conflictsNeedingRenumber.length > 0) {
|
if (conflictsNeedingRenumber.length > 0) {
|
||||||
await this.renumberConflictingFiles(conflictsNeedingRenumber, folderPath, baseName, libraryRoot);
|
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);
|
const nextIndex = await this.findNextAvailableNumberWithFilesystemCheck(folderPath, baseName, targetDirectory);
|
||||||
targetFileName = `${baseName}_${this.organization.formatSequenceNumber(nextIndex)}.wav`;
|
targetFileName = `${baseName}_${this.organization.formatSequenceNumber(nextIndex)}.wav`;
|
||||||
targetPath = path.join(targetDirectory, targetFileName);
|
targetPath = path.join(targetDirectory, targetFileName);
|
||||||
targetRelativePath = this.toLibraryRelativePath(folderPath, 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) {
|
} catch (error: unknown) {
|
||||||
// Check if this is a UNIQUE constraint error on absolute_path
|
// Check if this is a UNIQUE constraint error on absolute_path
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
const errorString = JSON.stringify(error);
|
|
||||||
|
|
||||||
if (errorMessage.includes('UNIQUE constraint') && errorMessage.includes('absolute_path')) {
|
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);
|
await fs.rename(targetPath, record.absolutePath);
|
||||||
|
|
||||||
// Find next available number and retry
|
|
||||||
const safeIndex = await this.findNextAvailableNumberWithFilesystemCheck(folderPath, baseName, targetDirectory);
|
const safeIndex = await this.findNextAvailableNumberWithFilesystemCheck(folderPath, baseName, targetDirectory);
|
||||||
targetFileName = `${baseName}_${this.organization.formatSequenceNumber(safeIndex)}.wav`;
|
targetFileName = `${baseName}_${this.organization.formatSequenceNumber(safeIndex)}.wav`;
|
||||||
targetPath = path.join(targetDirectory, targetFileName);
|
targetPath = path.join(targetDirectory, targetFileName);
|
||||||
targetRelativePath = this.toLibraryRelativePath(folderPath, targetFileName);
|
targetRelativePath = this.toLibraryRelativePath(folderPath, targetFileName);
|
||||||
|
|
||||||
console.log(`[LibraryService] Retrying with numbered filename: ${targetFileName}`);
|
|
||||||
|
|
||||||
// Retry filesystem operation
|
|
||||||
await fs.rename(record.absolutePath, targetPath);
|
await fs.rename(record.absolutePath, targetPath);
|
||||||
|
|
||||||
// Retry database operation
|
|
||||||
updated = this.database.updateFileLocation(
|
updated = this.database.updateFileLocation(
|
||||||
fileId,
|
fileId,
|
||||||
targetPath,
|
targetPath,
|
||||||
@@ -618,7 +587,7 @@ export class LibraryService {
|
|||||||
path.basename(targetFileName, '.wav')
|
path.basename(targetFileName, '.wav')
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error(`[LibraryService] Unexpected error during updateFileLocation:`, errorMessage, errorString);
|
console.error(`Unexpected error during updateFileLocation:`, errorMessage);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -659,8 +628,6 @@ export class LibraryService {
|
|||||||
this.resetMetadataSuggestionsCache();
|
this.resetMetadataSuggestionsCache();
|
||||||
this.waveformPreviewCache.delete(fileId);
|
this.waveformPreviewCache.delete(fileId);
|
||||||
this.search.rebuildIndex();
|
this.search.rebuildIndex();
|
||||||
|
|
||||||
console.log(`[organizeFile] SUCCESS: fileId=${fileId}, finalPath=${updated.absolutePath}, finalName=${updated.fileName}`);
|
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,7 +815,6 @@ export class LibraryService {
|
|||||||
const finalAbsolutePath = path.join(targetDirectory, finalFileName);
|
const finalAbsolutePath = path.join(targetDirectory, finalFileName);
|
||||||
const relativePath = this.toLibraryRelativePath(folderPath, finalFileName);
|
const relativePath = this.toLibraryRelativePath(folderPath, finalFileName);
|
||||||
const tempPath = `${finalAbsolutePath}.${Date.now()}-${file.id}-${Math.random().toString(16).slice(2)}.tmp`;
|
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 {
|
return {
|
||||||
file,
|
file,
|
||||||
tempPath,
|
tempPath,
|
||||||
@@ -862,18 +828,13 @@ export class LibraryService {
|
|||||||
const movedToTemp: typeof plans = [];
|
const movedToTemp: typeof plans = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Phase 1: Move all files to temp names
|
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
// Use the fresh absolutePath from database
|
|
||||||
await fs.rename(plan.file.absolutePath, plan.tempPath);
|
await fs.rename(plan.file.absolutePath, plan.tempPath);
|
||||||
console.log(`[renumberConflictingFiles] moved to temp: ${plan.file.absolutePath} -> ${plan.tempPath}`);
|
|
||||||
movedToTemp.push(plan);
|
movedToTemp.push(plan);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 2: Move all temp files to final names and update database
|
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
await fs.rename(plan.tempPath, plan.finalAbsolutePath);
|
await fs.rename(plan.tempPath, plan.finalAbsolutePath);
|
||||||
console.log(`[renumberConflictingFiles] finalized: ${plan.tempPath} -> ${plan.finalAbsolutePath}`);
|
|
||||||
this.database.updateFileLocation(
|
this.database.updateFileLocation(
|
||||||
plan.file.id,
|
plan.file.id,
|
||||||
plan.finalAbsolutePath,
|
plan.finalAbsolutePath,
|
||||||
@@ -883,23 +844,17 @@ export class LibraryService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Rollback: attempt to restore any files that were moved to temp
|
|
||||||
console.error('Error during batch rename, attempting rollback:', error);
|
console.error('Error during batch rename, attempting rollback:', error);
|
||||||
for (const plan of movedToTemp) {
|
for (const plan of movedToTemp) {
|
||||||
try {
|
try {
|
||||||
// Check if temp file exists
|
|
||||||
await fs.access(plan.tempPath);
|
await fs.access(plan.tempPath);
|
||||||
// Try to restore to original location
|
|
||||||
await fs.rename(plan.tempPath, plan.file.absolutePath);
|
await fs.rename(plan.tempPath, plan.file.absolutePath);
|
||||||
console.log(`Rolled back: ${plan.tempPath} -> ${plan.file.absolutePath}`);
|
|
||||||
} catch (rollbackError) {
|
} catch (rollbackError) {
|
||||||
console.error(`Failed to rollback ${plan.tempPath}:`, rollbackError);
|
console.error(`Failed to rollback ${plan.tempPath}:`, rollbackError);
|
||||||
// If rollback fails, try to at least rename temp to final destination
|
|
||||||
try {
|
try {
|
||||||
await fs.rename(plan.tempPath, plan.finalAbsolutePath);
|
await fs.rename(plan.tempPath, plan.finalAbsolutePath);
|
||||||
console.log(`Recovered: ${plan.tempPath} -> ${plan.finalAbsolutePath}`);
|
} catch {
|
||||||
} catch (recoveryError) {
|
// Recovery failed, file may be lost
|
||||||
console.error(`Failed to recover ${plan.tempPath}:`, recoveryError);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,9 +132,6 @@ function App(): JSX.Element {
|
|||||||
libraryStore.selectFile(fileId, options);
|
libraryStore.selectFile(fileId, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Selects every visible file so keyboard shortcuts mirror multi-select gestures.
|
|
||||||
*/
|
|
||||||
const handleSelectAllFiles = () => {
|
const handleSelectAllFiles = () => {
|
||||||
libraryStore.selectAllVisibleFiles();
|
libraryStore.selectAllVisibleFiles();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea
|
|||||||
const waveformCacheRef = useRef<Record<number, WaveformVisual>>({});
|
const waveformCacheRef = useRef<Record<number, WaveformVisual>>({});
|
||||||
const [, forceWaveformUpdate] = useReducer((value: number) => value + 1, 0);
|
const [, forceWaveformUpdate] = useReducer((value: number) => value + 1, 0);
|
||||||
const dragGhostRef = useRef<HTMLElement | null>(null);
|
const dragGhostRef = useRef<HTMLElement | null>(null);
|
||||||
const dragRotation = useRef<number>(0);
|
const dragRotation = useRef(0);
|
||||||
const lastDragX = useRef<number>(0);
|
const lastDragX = useRef(0);
|
||||||
const dropTargetPosition = useRef<{ x: number; y: number } | null>(null);
|
const dropTargetPosition = useRef<{ x: number; y: number } | null>(null);
|
||||||
const dragOffset = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
const dragOffset = useRef({ x: 0, y: 0 });
|
||||||
const dragStartTime = useRef<number>(0);
|
const dragStartTime = useRef(0);
|
||||||
const listContainerRef = useRef<HTMLDivElement | null>(null);
|
const listContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -44,7 +44,6 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea
|
|||||||
if (isTypingContext || isEditable) {
|
if (isTypingContext || isEditable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Keep keyboard focus aligned with the active selection.
|
|
||||||
button.focus();
|
button.focus();
|
||||||
}, [selectedId]);
|
}, [selectedId]);
|
||||||
|
|
||||||
@@ -197,8 +196,6 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea
|
|||||||
onClick={(event) => handleClick(file.id, event)}
|
onClick={(event) => handleClick(file.id, event)}
|
||||||
onDoubleClick={() => handleDoubleClick(file)}
|
onDoubleClick={() => handleDoubleClick(file)}
|
||||||
onDragStart={(event) => {
|
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];
|
const draggedIds = selectedIds.has(file.id) ? Array.from(selectedIds) : [file.id];
|
||||||
|
|
||||||
event.dataTransfer.setData('application/audiosort-file', JSON.stringify({
|
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';
|
event.dataTransfer.effectAllowed = 'copy';
|
||||||
|
|
||||||
// Create transparent drag image to hide the default
|
|
||||||
const transparent = document.createElement('div');
|
const transparent = document.createElement('div');
|
||||||
transparent.style.width = '1px';
|
transparent.style.width = '1px';
|
||||||
transparent.style.height = '1px';
|
transparent.style.height = '1px';
|
||||||
@@ -216,7 +212,6 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea
|
|||||||
event.dataTransfer.setDragImage(transparent, 0, 0);
|
event.dataTransfer.setDragImage(transparent, 0, 0);
|
||||||
setTimeout(() => transparent.remove(), 0);
|
setTimeout(() => transparent.remove(), 0);
|
||||||
|
|
||||||
// Calculate offset from click position to element center
|
|
||||||
const sourceElement = event.currentTarget as HTMLElement;
|
const sourceElement = event.currentTarget as HTMLElement;
|
||||||
const rect = sourceElement.getBoundingClientRect();
|
const rect = sourceElement.getBoundingClientRect();
|
||||||
const elementCenterX = rect.left + rect.width / 2;
|
const elementCenterX = rect.left + rect.width / 2;
|
||||||
@@ -226,21 +221,18 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea
|
|||||||
y: elementCenterY - event.clientY
|
y: elementCenterY - event.clientY
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate transition duration based on offset distance
|
|
||||||
const offsetDistance = Math.sqrt(
|
const offsetDistance = Math.sqrt(
|
||||||
dragOffset.current.x * dragOffset.current.x +
|
dragOffset.current.x * dragOffset.current.x +
|
||||||
dragOffset.current.y * dragOffset.current.y
|
dragOffset.current.y * dragOffset.current.y
|
||||||
);
|
);
|
||||||
const maxDistance = Math.sqrt(rect.width * rect.width + rect.height * rect.height) / 2;
|
const maxDistance = Math.sqrt(rect.width * rect.width + rect.height * rect.height) / 2;
|
||||||
const normalizedDistance = Math.min(offsetDistance / maxDistance, 1);
|
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();
|
dragStartTime.current = Date.now();
|
||||||
|
|
||||||
// Clone the current element for custom floating preview
|
|
||||||
const ghost = sourceElement.cloneNode(true) as HTMLElement;
|
const ghost = sourceElement.cloneNode(true) as HTMLElement;
|
||||||
|
|
||||||
// Create a wrapper for rotation that doesn't affect position transition
|
|
||||||
const rotationWrapper = document.createElement('div');
|
const rotationWrapper = document.createElement('div');
|
||||||
rotationWrapper.style.position = 'fixed';
|
rotationWrapper.style.position = 'fixed';
|
||||||
rotationWrapper.style.top = `${event.clientY}px`;
|
rotationWrapper.style.top = `${event.clientY}px`;
|
||||||
@@ -264,7 +256,6 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea
|
|||||||
rotationWrapper.appendChild(ghost);
|
rotationWrapper.appendChild(ghost);
|
||||||
document.body.appendChild(rotationWrapper);
|
document.body.appendChild(rotationWrapper);
|
||||||
|
|
||||||
// Trigger the offset transition to center
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (ghost.parentNode) {
|
if (ghost.parentNode) {
|
||||||
ghost.style.transform = `translate(-50%, -50%)`;
|
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));
|
const clamped = Math.max(-12, Math.min(12, blended));
|
||||||
dragRotation.current = clamped;
|
dragRotation.current = clamped;
|
||||||
|
|
||||||
// Update wrapper position (no rotation here)
|
|
||||||
dragGhostRef.current.style.top = `${event.clientY}px`;
|
dragGhostRef.current.style.top = `${event.clientY}px`;
|
||||||
dragGhostRef.current.style.left = `${event.clientX}px`;
|
dragGhostRef.current.style.left = `${event.clientX}px`;
|
||||||
|
|
||||||
// Apply rotation directly to wrapper without transition
|
|
||||||
dragGhostRef.current.style.transform = `rotate(${clamped}deg)`;
|
dragGhostRef.current.style.transform = `rotate(${clamped}deg)`;
|
||||||
|
|
||||||
// Track position for potential drop animation
|
|
||||||
dropTargetPosition.current = { x: event.clientX, y: event.clientY };
|
dropTargetPosition.current = { x: event.clientX, y: event.clientY };
|
||||||
}}
|
}}
|
||||||
onDragEnd={(event) => {
|
onDragEnd={(event) => {
|
||||||
const wrapper = dragGhostRef.current;
|
const wrapper = dragGhostRef.current;
|
||||||
if (!wrapper) return;
|
if (!wrapper) return;
|
||||||
|
|
||||||
// Check if this was a successful drop (dropEffect will be 'copy' if accepted)
|
|
||||||
const wasDropped = event.dataTransfer.dropEffect === 'copy';
|
const wasDropped = event.dataTransfer.dropEffect === 'copy';
|
||||||
|
|
||||||
if (wasDropped && dropTargetPosition.current) {
|
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.transition = 'all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)';
|
||||||
wrapper.style.transform = `rotate(0deg) scale(0)`;
|
wrapper.style.transform = `rotate(0deg) scale(0)`;
|
||||||
wrapper.style.opacity = '0';
|
wrapper.style.opacity = '0';
|
||||||
@@ -314,7 +299,6 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea
|
|||||||
dragGhostRef.current = null;
|
dragGhostRef.current = null;
|
||||||
}, 300);
|
}, 300);
|
||||||
} else {
|
} else {
|
||||||
// No drop or cancelled - remove immediately
|
|
||||||
wrapper.remove();
|
wrapper.remove();
|
||||||
dragGhostRef.current = null;
|
dragGhostRef.current = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,18 +55,14 @@ export function TagEditor({ tags, categories, availableCategories, onSave, showH
|
|||||||
setTagDraft(tags.join(', '));
|
setTagDraft(tags.join(', '));
|
||||||
setSelectedCategories(new Set(categories));
|
setSelectedCategories(new Set(categories));
|
||||||
|
|
||||||
// Sync collapsed state from module state when file changes (but not on first render)
|
|
||||||
if (!isFirstRender.current) {
|
if (!isFirstRender.current) {
|
||||||
const moduleState = getCollapsedGroups();
|
const moduleState = getCollapsedGroups();
|
||||||
console.log('TagEditor syncing collapsed state from module:', Array.from(moduleState!));
|
|
||||||
setCollapsedGroups(new Set(moduleState!));
|
setCollapsedGroups(new Set(moduleState!));
|
||||||
} else {
|
} else {
|
||||||
isFirstRender.current = false;
|
isFirstRender.current = false;
|
||||||
}
|
}
|
||||||
}, [tags, categories]);
|
}, [tags, categories]);
|
||||||
|
|
||||||
// No need for this effect anymore - categories don't change dynamically
|
|
||||||
|
|
||||||
const { groupedCategories, filteredResults } = useMemo(() => {
|
const { groupedCategories, filteredResults } = useMemo(() => {
|
||||||
const filter = categoryFilter.trim().toLowerCase();
|
const filter = categoryFilter.trim().toLowerCase();
|
||||||
const filtered = filter.length === 0
|
const filtered = filter.length === 0
|
||||||
@@ -139,7 +135,6 @@ export function TagEditor({ tags, categories, availableCategories, onSave, showH
|
|||||||
moduleState.add(groupName);
|
moduleState.add(groupName);
|
||||||
}
|
}
|
||||||
setModuleCollapsedGroups(moduleState);
|
setModuleCollapsedGroups(moduleState);
|
||||||
console.log('TagEditor toggleGroup:', groupName, 'collapsed:', next.has(groupName), 'module state:', Array.from(moduleState));
|
|
||||||
setCollapsedGroups(next);
|
setCollapsedGroups(next);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -192,7 +187,7 @@ export function TagEditor({ tags, categories, availableCategories, onSave, showH
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="tag-editor">
|
<section className="tag-editor">
|
||||||
{showHeading && <h2>Tags</h2>}
|
{showHeading && <h2>Tags</h2>}
|
||||||
<textarea
|
<textarea
|
||||||
value={tagDraft}
|
value={tagDraft}
|
||||||
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setTagDraft(event.target.value)}
|
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setTagDraft(event.target.value)}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export function initializeCollapsedGroups(categories: string[]): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setCollapsedGroups(groups: Set<string>): void {
|
export function setCollapsedGroups(groups: Set<string>): void {
|
||||||
console.log('CategoryUIState: setCollapsedGroups called with', Array.from(groups));
|
|
||||||
collapsedGroupsState = groups;
|
collapsedGroupsState = groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class LibraryStore extends EventTarget {
|
|||||||
visibleFiles: [],
|
visibleFiles: [],
|
||||||
selectedFileId: null,
|
selectedFileId: null,
|
||||||
selectedFileIds: new Set(),
|
selectedFileIds: new Set(),
|
||||||
focusedFile: null,
|
focusedFile: null,
|
||||||
categories: [],
|
categories: [],
|
||||||
settings: { libraryPath: null },
|
settings: { libraryPath: null },
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2021",
|
"target": "ES2021",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Bundler",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"extends": "./tsconfig.base.json",
|
"extends": "./tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "Node10",
|
||||||
"outDir": "dist/main",
|
"outDir": "dist/main",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
|
|||||||
Reference in New Issue
Block a user