diff --git a/audio-service/metadata.json b/audio-service/metadata.json
index 2e586f0..e83672c 100644
--- a/audio-service/metadata.json
+++ b/audio-service/metadata.json
@@ -1,13 +1,34 @@
-{
- "Test": [],
- "Uncategorized": [
- {
- "endTime": 20.085836909871187,
- "filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260214_114317.wav",
- "name": "Farts",
- "playbackType": "playStop",
- "startTime": 17.124463519313306,
- "volume": 0.8
- }
- ]
-}
\ No newline at end of file
+[
+ {
+ "name": "Uncategorized",
+ "id": 0,
+ "clips": []
+ },
+ {
+ "name": "Test",
+ "id": 1,
+ "clips": []
+ },
+ {
+ "name": "New",
+ "id": 2,
+ "clips": [
+ {
+ "endTime": 30,
+ "filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260220_193822.wav",
+ "name": "Pee pee poo poo",
+ "playbackType": "playStop",
+ "startTime": 27.756510985786615,
+ "volume": 1
+ },
+ {
+ "endTime": 28.597210828548004,
+ "filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260220_200442.wav",
+ "name": "Clip 20260220_200442",
+ "playbackType": "playStop",
+ "startTime": 26.1853978671042,
+ "volume": 1
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/audio-service/src/__pycache__/audio_recorder.cpython-313.pyc b/audio-service/src/__pycache__/audio_recorder.cpython-313.pyc
index 856cf1f..3fd36bd 100644
Binary files a/audio-service/src/__pycache__/audio_recorder.cpython-313.pyc and b/audio-service/src/__pycache__/audio_recorder.cpython-313.pyc differ
diff --git a/audio-service/src/__pycache__/metadata_manager.cpython-313.pyc b/audio-service/src/__pycache__/metadata_manager.cpython-313.pyc
index 7780509..b63a332 100644
Binary files a/audio-service/src/__pycache__/metadata_manager.cpython-313.pyc and b/audio-service/src/__pycache__/metadata_manager.cpython-313.pyc differ
diff --git a/audio-service/src/audio_recorder.py b/audio-service/src/audio_recorder.py
index 5c79f26..05d38e2 100644
--- a/audio-service/src/audio_recorder.py
+++ b/audio-service/src/audio_recorder.py
@@ -104,10 +104,7 @@ class AudioRecorder:
"volume": 1.0,
}
- meta.add_clip_to_collection("Uncategorized",
- {
- clip_metadata
- })
+ meta.add_clip_to_collection("Uncategorized", clip_metadata )
return clip_metadata
diff --git a/audio-service/src/metadata_manager.py b/audio-service/src/metadata_manager.py
index 613dc32..cf2cc5c 100644
--- a/audio-service/src/metadata_manager.py
+++ b/audio-service/src/metadata_manager.py
@@ -17,71 +17,82 @@ class MetaDataManager:
self.collections = json.load(f)
else:
self.collections = {}
- if(collections := self.collections.get("Uncategorized")) is None:
- self.collections["Uncategorized"] = []
+ if(collections := next((c for c in self.collections if c.get("name") == "Uncategorized"), None)) is None:
+ self.collections.append({"name": "Uncategorized", "id": 0, "clips": []})
self.save_metadata()
def create_collection(self, name):
- if name in self.collections:
+ if any(c.get("name") == name for c in self.collections):
raise ValueError(f"Collection '{name}' already exists.")
- self.collections[name] = []
+ new_id = max((c.get("id", 0) for c in self.collections), default=0) + 1
+ self.collections.append({"name": name, "id": new_id, "clips": []})
self.save_metadata()
def delete_collection(self, name):
- if name not in self.collections:
+ collection = next((c for c in self.collections if c.get("name") == name), None)
+ if collection is None:
raise ValueError(f"Collection '{name}' does not exist.")
- del self.collections[name]
+ self.collections.remove(collection)
self.save_metadata()
def add_clip_to_collection(self, collection_name, clip_metadata):
- if collection_name not in self.collections:
+ collection = next((c for c in self.collections if c.get("name") == collection_name), None)
+ if collection is None:
raise ValueError(f"Collection '{collection_name}' does not exist.")
- self.collections[collection_name].append(clip_metadata)
+ collection["clips"].append(clip_metadata)
self.save_metadata()
def remove_clip_from_collection(self, collection_name, clip_metadata):
- if collection_name not in self.collections:
+ collection = next((c for c in self.collections if c.get("name") == collection_name), None)
+ if collection is None:
raise ValueError(f"Collection '{collection_name}' does not exist.")
# Remove all clips with the same file name as clip_metadata["file_name"]
- in_list = any(clip.get("filename") == clip_metadata.get("filename") for clip in self.collections[collection_name])
+ in_list = any(clip.get("filename") == clip_metadata.get("filename") for clip in collection["clips"])
if not in_list:
raise ValueError(f"Clip with filename '{clip_metadata.get('filename')}' not found in collection '{collection_name}'.")
- self.collections[collection_name] = [
- clip for clip in self.collections[collection_name]
+ collection["clips"] = [
+ clip for clip in collection["clips"]
if clip.get("filename") != clip_metadata.get("filename")
]
self.save_metadata()
+
+ def move_clip_to_collection(self, source_collection, target_collection, clip_metadata):
+ self.remove_clip_from_collection(source_collection, clip_metadata)
+ self.add_clip_to_collection(target_collection, clip_metadata)
def edit_clip_in_collection(self, collection_name, new_clip_metadata):
- if collection_name not in self.collections:
+ collection = next((c for c in self.collections if c.get("name") == collection_name), None)
+ if collection is None:
raise ValueError(f"Collection '{collection_name}' does not exist.")
# Find the index of the clip with the same file name as old_clip_metadata["file_name"]
- index = next((i for i, clip in enumerate(self.collections[collection_name]) if clip.get("filename") == new_clip_metadata.get("filename")), None)
+ index = next((i for i, clip in enumerate(collection["clips"]) if clip.get("filename") == new_clip_metadata.get("filename")), None)
if index is None:
raise ValueError(f"Clip with filename '{new_clip_metadata.get('filename')}' not found in collection '{collection_name}'.")
- self.collections[collection_name][index] = new_clip_metadata
+ collection["clips"][index] = new_clip_metadata
self.save_metadata()
def get_collections(self):
- return list(self.collections.keys())
+ return list(map(lambda c: {"name": c.get("name"), "id": c.get("id")}, self.collections))
def get_clips_in_collection(self, collection_name):
- if collection_name not in self.collections:
+ collection = next((c for c in self.collections if c.get("name") == collection_name), None)
+ if collection is None:
raise ValueError(f"Collection '{collection_name}' does not exist.")
- return self.collections[collection_name]
+ return collection["clips"]
def reorder_clips_in_collection(self, collection_name, new_order):
- if collection_name not in self.collections:
+ collection = next((c for c in self.collections if c.get("name") == collection_name), None)
+ if collection is None:
raise ValueError(f"Collection '{collection_name}' does not exist.")
- existing_filenames = {clip.get("filename") for clip in self.collections[collection_name]}
+ existing_filenames = {clip.get("filename") for clip in collection["clips"]}
new_filenames = {clip.get("filename") for clip in new_order}
if not new_filenames.issubset(existing_filenames):
raise ValueError("New order contains clips that do not exist in the collection.")
- self.collections[collection_name] = new_order
+ collection["clips"] = new_order
self.save_metadata()
def save_metadata(self):
diff --git a/audio-service/src/routes/__pycache__/metadata.cpython-313.pyc b/audio-service/src/routes/__pycache__/metadata.cpython-313.pyc
index 6b3418d..6fa02b0 100644
Binary files a/audio-service/src/routes/__pycache__/metadata.cpython-313.pyc and b/audio-service/src/routes/__pycache__/metadata.cpython-313.pyc differ
diff --git a/audio-service/src/routes/metadata.py b/audio-service/src/routes/metadata.py
index e2bc6bb..8cbb5da 100644
--- a/audio-service/src/routes/metadata.py
+++ b/audio-service/src/routes/metadata.py
@@ -73,6 +73,19 @@ def remove_clip_from_collection():
except ValueError as e:
return jsonify({'status': 'error', 'message': str(e)}), 400
+@metadata_bp.route('/meta/collection/clips/move', methods=['POST'])
+def move_clip_to_collection():
+ meta_manager = MetaDataManager()
+ sourceCollection = request.json.get('sourceCollection')
+ targetCollection = request.json.get('targetCollection')
+ clip_metadata = request.json.get('clip')
+ try:
+ meta_manager.move_clip_to_collection(sourceCollection, targetCollection, clip_metadata)
+ collections = meta_manager.collections
+ return jsonify({'status': 'success', 'collections': collections})
+ except ValueError as e:
+ return jsonify({'status': 'error', 'message': str(e)}), 400
+
@metadata_bp.route('/meta/collection/clips/edit', methods=['POST'])
def edit_clip_in_collection():
meta_manager = MetaDataManager()
diff --git a/electron-ui/.eslintrc.js b/electron-ui/.eslintrc.js
index d3b1c03..9512cc7 100644
--- a/electron-ui/.eslintrc.js
+++ b/electron-ui/.eslintrc.js
@@ -14,6 +14,8 @@ module.exports = {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'react/require-default-props': 'off',
+ 'react/jsx-no-bind': 'off',
+ 'jsx-a11y/no-autofocus': 'off',
'no-console': 'off',
},
parserOptions: {
diff --git a/electron-ui/src/old/index.html b/electron-ui/src/old/index.html
deleted file mode 100644
index e254099..0000000
--- a/electron-ui/src/old/index.html
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
- Audio Clip Trimmer
-
-
-
-
-
-
-
-
-
×
-
Settings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/electron-ui/src/old/main.js b/electron-ui/src/old/main.js
deleted file mode 100644
index 5aeb7ae..0000000
--- a/electron-ui/src/old/main.js
+++ /dev/null
@@ -1,483 +0,0 @@
-const { app, BrowserWindow, ipcMain, Tray, Menu, dialog } = require('electron');
-const path = require('path');
-const os = require('os');
-const spawn = require('child_process').spawn;
-require('electron-reload')(__dirname);
-const fs = require('fs').promises;
-const chokidar = require('chokidar');
-const wavefile = require('wavefile');
-const MetadataManager = require('./metatadata');
-
-const { webContents } = require('electron');
-
-// import { app, BrowserWindow, ipcMain, Tray, Menu, dialog } from "electron";
-// import path from "path";
-// import os from "os";
-// import spawn from 'child_process';
-// import fs from "fs";
-// import chokidar from "chokidar";
-// import wavefile from "wavefile";
-// import MetadataManager from "./metatadata.cjs";
-// import { webContents } from "electron";
-
-let mainWindow;
-let tray;
-let audioServiceProcess;
-
-const metadataPath = path.join(app.getPath('userData'), 'audio_metadata.json');
-const metadataManager = new MetadataManager(metadataPath);
-
-async function createPythonService() {
- const pythonPath =
- process.platform === 'win32'
- ? path.join(
- __dirname,
- '..',
- '..',
- 'audio-service',
- 'venv',
- 'Scripts',
- 'python.exe',
- )
- : path.join(__dirname, '..', 'audio-service', 'venv', 'bin', 'python');
-
- const scriptPath = path.join(
- __dirname,
- '..',
- '..',
- 'audio-service',
- 'src',
- 'main.py',
- );
-
- // Load settings to pass as arguments
- const settings = await loadSettings(); // Assuming you have a method to load settings
-
- const args = [
- scriptPath,
- '--recording-length',
- settings.recordingLength.toString(),
- '--save-path',
- path.join(settings.outputFolder, 'original'),
- '--osc-port',
- settings.oscPort.toString(), // Or make this configurable
- ];
-
- // Add input device if specified
- if (settings.inputDevice) {
- const devices = await listAudioDevices();
- args.push(
- '--input-device',
- devices.find((device) => device.id === settings.inputDevice)?.name,
- );
- }
-
- console.log(args);
-
- audioServiceProcess = spawn(pythonPath, args, {
- detached: false,
- stdio: 'pipe',
- });
-
- audioServiceProcess.stdout.on('data', (data) => {
- console.log(`Audio Service: ${data}`);
- });
-
- audioServiceProcess.stderr.on('data', (data) => {
- console.error(`Audio Service Error: ${data}`);
- });
-
- audioServiceProcess.on('close', (code) => {
- console.log(`Audio Service process exited with code ${code}`);
- audioServiceProcess = null;
- });
-}
-
-function createTray() {
- tray = new Tray(path.join(__dirname, 'assets', 'tray-icon.png')); // You'll need to create this icon
-
- const contextMenu = Menu.buildFromTemplate([
- {
- label: 'Show',
- click: () => {
- mainWindow.show();
- },
- },
- {
- label: 'Quit',
- click: () => {
- // Properly terminate the Python service
-
- stopService();
- app.quit();
- },
- },
- ]);
-
- tray.setToolTip('Audio Trimmer');
- tray.setContextMenu(contextMenu);
-}
-
-async function checkNewWavFile(filePath) {
- // Only process .wav files
- if (path.extname(filePath).toLowerCase() === '.wav') {
- try {
- await metadataManager.addUntrimmedFile(filePath);
-
- // Notify renderer if window is ready
- if (mainWindow) {
- mainWindow.webContents.send('new-untrimmed-file', filePath);
- }
- } catch (error) {
- console.error('Error adding untrimmed file:', error);
- }
- }
-}
-
-function stopService() {
- if (audioServiceProcess) {
- try {
- if (process.platform === 'win32') {
- spawn('taskkill', ['/pid', audioServiceProcess.pid, '/f', '/t']);
- } else {
- audioServiceProcess.kill('SIGTERM');
- }
- } catch (error) {
- console.error('Error killing audio service:', error);
- }
- }
-}
-
-function restartService() {
- // Properly terminate the Python service
- stopService();
- //delay for 2 seconds
- setTimeout(createPythonService, 4000);
- //createPythonService();
-}
-
-async function loadSettings() {
- try {
- const settingsPath = path.join(app.getPath('userData'), 'settings.json');
- const settingsData = await fs.readFile(settingsPath, 'utf8');
- return JSON.parse(settingsData);
- } catch (error) {
- // If no settings file exists, return default settings
- return {
- recordingLength: 30,
- outputFolder: path.join(os.homedir(), 'AudioTrimmer'),
- inputDevice: null,
- };
- }
-}
-
-async function listAudioDevices() {
- try {
- // Use a webContents to access navigator.mediaDevices
-
- const contents = webContents.getAllWebContents()[0];
-
- const devices = await contents.executeJavaScript(`
- navigator.mediaDevices.enumerateDevices()
- .then(devices => devices.filter(device => device.kind === 'audioinput'))
- .then(audioDevices => audioDevices.map(device => ({
- id: device.deviceId,
- name: device.label || 'Unknown Microphone'
- })))
- `);
-
- return devices;
- } catch (error) {
- console.error('Error getting input devices:', error);
- return [];
- }
-}
-async function createWindow() {
- // Initialize metadata
- await metadataManager.initialize();
-
- mainWindow = new BrowserWindow({
- width: 1200,
- height: 800,
- autoHideMenuBar: true,
- titleBarStyle: 'hidden',
- frame: false,
-
- // titleBarOverlay: {
- // color: '#1e1e1e',
- // symbolColor: '#ffffff',
- // height: 30
- // },
- webPreferences: {
- nodeIntegration: true,
- contextIsolation: false,
- // Add these to help with graphics issues
- },
- // These additional options can help with graphics rendering
- backgroundColor: '#1e1e1e',
- ...(process.platform !== 'darwin'
- ? {
- titleBarOverlay: {
- color: '#262626',
- symbolColor: '#ffffff',
- height: 30,
- },
- }
- : {}),
- });
- mainWindow.loadFile('src/index.html');
-
- // Create Python ser
- const settings = await loadSettings(); // Assuming you have a method to load settings
- const recordingsPath = path.join(settings.outputFolder, 'original');
- // Ensure recordings directory exists
- try {
- await fs.mkdir(recordingsPath, { recursive: true });
- } catch (error) {
- console.error('Error creating recordings directory:', error);
- }
-
- // Watch for new WAV files
- const watcher = chokidar.watch(recordingsPath, {
- ignored: /(^|[\/\\])\../, // ignore dotfiles
- persistent: true,
- depth: 0,
- awaitWriteFinish: {
- stabilityThreshold: 2000,
- pollInterval: 100,
- },
- });
-
- fs.readdir(recordingsPath).then((files) => {
- files.forEach((file) => {
- checkNewWavFile(path.join(recordingsPath, file));
- });
- });
-
- watcher.on('add', async (filePath) => {
- await checkNewWavFile(filePath);
- });
-
- ipcMain.handle('get-collections', () => {
- return metadataManager.getCollections();
- });
-
- ipcMain.handle('get-collection-files', (event, collectionPath) => {
- return metadataManager.getFilesInCollection(collectionPath);
- });
-
- ipcMain.handle('add-untrimmed-file', (event, filePath) => {
- return metadataManager.addUntrimmedFile(filePath);
- });
-
- ipcMain.handle(
- 'save-trimmed-file',
- (event, fileName, previousPath, savePath, trimStart, trimEnd, title) => {
- return metadataManager.saveTrimmedFile(
- fileName,
- previousPath,
- savePath,
- trimStart,
- trimEnd,
- title,
- );
- },
- );
-
- ipcMain.handle('restart', (event) => {
- restartService();
- });
-
- ipcMain.handle('delete-old-file', (event, outputFolder, section, title) => {
- if (section === 'untrimmed') return;
- const collectionPath = path.join(outputFolder, section);
- const outputFilePath = path.join(collectionPath, `${title}.wav`);
- fs.unlink(outputFilePath);
- });
- ipcMain.handle(
- 'save-trimmed-audio',
- async (
- event,
- {
- originalFilePath,
- outputFolder,
- collectionName,
- title,
- trimStart,
- trimEnd,
- },
- ) => {
- try {
- // Ensure the collection folder exists
- const collectionPath = path.join(outputFolder, collectionName);
- await fs.mkdir(collectionPath, { recursive: true });
-
- // Generate output file path
- const outputFilePath = path.join(collectionPath, `${title}.wav`);
-
- // Read the original WAV file
- const originalWaveFile = new wavefile.WaveFile(
- await fs.readFile(originalFilePath),
- );
-
- // Calculate trim points in samples
- const sampleRate = originalWaveFile.fmt.sampleRate;
- const startSample = Math.floor(trimStart * sampleRate);
- const endSample = Math.floor(trimEnd * sampleRate);
-
- // Extract trimmed audio samples
- const originalSamples = originalWaveFile.getSamples(false);
- const trimmedSamples = [
- originalSamples[0].slice(startSample, endSample),
- originalSamples[1].slice(startSample, endSample),
- ];
-
- // Normalize samples if they are Int16 or Int32
- let normalizedSamples;
- const bitDepth = originalWaveFile.fmt.bitsPerSample;
-
- // if (bitDepth === 16) {
- // // For 16-bit audio, convert to Float32
- // normalizedSamples = [new Float32Array(trimmedSamples[0].length),new Float32Array(trimmedSamples[0].length)];
- // for (let i = 0; i < trimmedSamples[0].length; i++) {
- // normalizedSamples[0][i] = trimmedSamples[0][i] / 32768.0;
- // normalizedSamples[1][i] = trimmedSamples[1][i] / 32768.0;
- // }
- // } else if (bitDepth === 32) {
- // // For 32-bit float audio, just convert to Float32
- // normalizedSamples = [new Float32Array(trimmedSamples[0]),new Float32Array(trimmedSamples[1])];
- // } else {
- // throw new Error(`Unsupported bit depth: ${bitDepth}`);
- // }
-
- // Create a new WaveFile with normalized samples
- const trimmedWaveFile = new wavefile.WaveFile();
- trimmedWaveFile.fromScratch(
- originalWaveFile.fmt.numChannels,
- sampleRate,
- bitDepth, // Always use 32-bit float
- trimmedSamples,
- );
-
- // Write the trimmed WAV file
- await fs.writeFile(outputFilePath, trimmedWaveFile.toBuffer());
-
- return {
- success: true,
- filePath: outputFilePath,
- };
- } catch (error) {
- console.error('Error saving trimmed audio:', error);
- return {
- success: false,
- error: error.message,
- };
- }
- },
- );
- ipcMain.handle('delete-file', async (event, filePath) => {
- try {
- const settings = await loadSettings();
- return metadataManager.deletefile(filePath, settings.outputFolder);
- } catch (error) {
- console.error('Error Deleting file:', error);
- throw error;
- }
- });
-
- ipcMain.handle('add-new-collection', (event, collectionName) => {
- try {
- return metadataManager.addNewCollection(collectionName);
- } catch (error) {
- console.error('Error adding collection:', error);
- throw error;
- }
- });
- ipcMain.handle('get-trim-info', (event, collectionName, filePath) => {
- return metadataManager.getTrimInfo(collectionName, filePath);
- });
- ipcMain.handle(
- 'set-trim-info',
- (event, collectionName, filePath, trim_info) => {
- return metadataManager.setTrimInfo(collectionName, filePath, trim_info);
- },
- );
-
- // Add these IPC handlers
- ipcMain.handle('select-output-folder', async (event) => {
- const result = await dialog.showOpenDialog({
- properties: ['openDirectory'],
- });
- return result.filePaths[0] || '';
- });
-
- ipcMain.handle('get-default-settings', () => {
- return {
- recordingLength: 30,
- outputFolder: path.join(os.homedir(), 'AudioTrimmer'),
- inputDevice: null,
- };
- });
-
- ipcMain.handle('save-settings', async (event, settings) => {
- try {
- // Ensure output folder exists
- await fs.mkdir(settings.outputFolder, { recursive: true });
-
- // Save settings to a file
- const settingsPath = path.join(app.getPath('userData'), 'settings.json');
- await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
- restartService();
- return true;
- } catch (error) {
- console.error('Error saving settings:', error);
- return false;
- }
- });
-
- ipcMain.handle('load-settings', async () => {
- return loadSettings();
- });
-
- ipcMain.handle('get-input-devices', async () => {
- return await listAudioDevices();
- });
-
- // Minimize to tray instead of closing
- mainWindow.on('close', (event) => {
- event.preventDefault();
- mainWindow.hide();
- });
-
- // Create system tray
- createTray();
-
- // Launch Python audio service
- createPythonService();
-}
-app.disableHardwareAcceleration();
-app.whenReady().then(createWindow);
-
-app.on('window-all-closed', () => {
- // Do nothing - we handle closing via tray
-});
-
-// Ensure Python service is killed when app quits
-app.on('before-quit', () => {
- if (audioServiceProcess) {
- try {
- if (process.platform === 'win32') {
- spawn('taskkill', ['/pid', audioServiceProcess.pid, '/f', '/t']);
- } else {
- audioServiceProcess.kill('SIGTERM');
- }
- } catch (error) {
- console.error('Error killing audio service:', error);
- }
- }
-});
-app.on('activate', () => {
- if (BrowserWindow.getAllWindows().length === 0) {
- createWindow();
- }
-});
diff --git a/electron-ui/src/old/metatadata.js b/electron-ui/src/old/metatadata.js
deleted file mode 100644
index e2c4877..0000000
--- a/electron-ui/src/old/metatadata.js
+++ /dev/null
@@ -1,234 +0,0 @@
-const fs = require('fs').promises;
-const path = require('path');
-
-// import fs from 'fs';
-// import path from 'path';
-
-class MetadataManager {
- constructor(metadataPath) {
- this.metadataPath = metadataPath;
- this.metadata = {};
- //this.initialize();
- }
-
- async initialize() {
- try {
- // Create metadata file if it doesn't exist
- console.log(this.metadataPath);
- await this.ensureMetadataFileExists();
-
- // Load existing metadata
- const rawData = await fs.readFile(this.metadataPath, 'utf8');
- this.metadata = JSON.parse(rawData);
- } catch (error) {
- console.error('Error initializing metadata:', error);
- this.metadata = {};
- }
- }
-
- async ensureMetadataFileExists() {
- try {
- await fs.access(this.metadataPath);
- } catch (error) {
- // File doesn't exist, create it with an empty object
- await fs.writeFile(this.metadataPath, JSON.stringify({
- collections: {
- untrimmed: {}
- }
- }, null, 2));
- }
- }
-
- async addUntrimmedFile(filePath) {
- try {
- // Read existing metadata
- const metadata = this.metadata;
-
- // Check if file is already in untrimmed files
- const fileName = path.basename(filePath);
- const existingUntrimmedFiles = Object.keys(metadata.collections.untrimmed) || [];
-
- // Check if the file is already in trimmed files across all collections
- const collections = Object.keys(metadata.collections || {});
- const isAlreadyTrimmed = collections.some(collection => {
- return (Object.keys(metadata.collections[collection] || {})).some(name => {
- return fileName === name;
- });
- });
-
- // If already trimmed, don't add to untrimmed files
- if (isAlreadyTrimmed) {
- return false;
- }
-
- // Prevent duplicates
- if (!existingUntrimmedFiles.includes(filePath)) {
- const d = new Date()
- metadata.collections.untrimmed[fileName] = {
- originalPath:filePath,
- addedAt:d.toISOString()
- }
- // Write updated metadata
- await this.saveMetadata();
- return true;
- }
-
- return false;
- } catch (error) {
- console.error('Error adding untrimmed file:', error);
- return false;
- }
- }
- async saveTrimmedFile(fileName, previousPath, savePath, trimStart, trimEnd, title) {
- console.log(title);
- // Ensure collection exists
- if (!this.metadata.collections[savePath]) {
- this.metadata.collections[savePath] = {};
- }
-
- // Find the original untrimmed file
- const original = this.metadata.collections[previousPath][fileName];
-
- // Add to specified collection
- this.metadata.collections[savePath][fileName] = {
- ...original,
- title,
- trimStart,
- trimEnd,
- };
-
-
- // Remove from untrimmed if it exists
- if(previousPath !== savePath) {
- // if(previousPath !== 'untrimmed') {
- // const prevmeta = this.metadata.collections[previousPath][fileName];
- // let delete_path = path.concat(previousPath, prevmeta.title + ".wav");
- // }
- delete this.metadata.collections[previousPath][fileName];
- }
-
- await this.saveMetadata();
-
- return fileName;
- }
-
- async saveMetadata() {
- try {
- await fs.writeFile(
- this.metadataPath,
- JSON.stringify(this.metadata, null, 2)
- );
- } catch (error) {
- console.error('Error saving metadata:', error);
- }
- }
-
- async getUntrimmedFiles() {
- try {
- // Read the metadata file
- const metadata = await this.readMetadataFile();
-
- // Get all collections
- const collections = Object.keys(metadata.collections || {});
-
- // Collect all trimmed file names across all collections
- const trimmedFiles = new Set();
- collections.forEach(collection => {
- const collectionTrimmedFiles = metadata.collections[collection]?.trimmedFiles || [];
- collectionTrimmedFiles.forEach(trimmedFile => {
- trimmedFiles.add(trimmedFile.originalFileName);
- });
- });
-
- // Filter out untrimmed files that have been trimmed
- const untrimmedFiles = (metadata.untrimmedFiles || []).filter(file =>
- !trimmedFiles.has(path.basename(file))
- );
-
- return untrimmedFiles;
- } catch (error) {
- console.error('Error getting untrimmed files:', error);
- return [];
- }
- }
-
- async deletefile(filePath, collectionPath) {
- try {
- const fileName = path.basename(filePath);
- for (const collection in this.metadata.collections) {
- if (this.metadata.collections[collection][fileName]) {
- let delete_path = this.metadata.collections[collection][fileName].originalPath;
- fs.unlink(delete_path);
- if(collection !== 'untrimmed') {
- delete_path = path.join(collectionPath, collection, this.metadata.collections[collection][fileName].title + ".wav");
- fs.unlink(delete_path);
- }
- delete this.metadata.collections[collection][fileName];
- this.saveMetadata();
- return 0
- }
- }
- }catch (error) {
- console.log(error)
- }
- return 0
- }
-
- getCollections() {
- return Object.keys(this.metadata.collections);
- }
-
- getTrimInfo(collectionName, filePath) {
- return this.metadata.collections[collectionName][filePath] || {
- trimStart: 0,
- trimEnd: 0
- };
- }
-
- setTrimInfo(collectionName, filePath, trimInfo) {
- this.metadata.collections[collectionName][filePath].trimStart = trimInfo.trimStart;
- this.metadata.collections[collectionName][filePath].trimEnd = trimInfo.trimEnd;
- this.saveMetadata();
- }
-
- getFilesInCollection(collectionPath) {
- // if(collectionPath === 'untrimmed') {
- // return Object.keys(this.metadata.untrimmed).map(fileName => ({
- // fileName,
- // ...this.metadata.untrimmed[fileName]
- // }));
- // }
- return Object.keys(this.metadata.collections[collectionPath] || {}).map(fileName => {
- const fileInfo = this.metadata.collections[collectionPath][fileName];
- return {
- fileName,
- ...this.metadata.collections[collectionPath][fileName],
- };
- });
- }
-
- async addNewCollection(collectionName) {
- // Ensure collection name is valid
- if (!collectionName || collectionName.trim() === '') {
- throw new Error('Collection name cannot be empty');
- }
-
- // Normalize collection name (remove leading/trailing spaces, convert to lowercase)
- const normalizedName = collectionName.trim().toLowerCase();
-
- // Check if collection already exists
- if (this.metadata.collections[normalizedName]) {
- throw new Error(`Collection '${normalizedName}' already exists`);
- }
-
- // Add new collection
- this.metadata.collections[normalizedName] = {};
-
- // Save updated metadata
- await this.saveMetadata();
-
- return normalizedName;
- }
-}
-
-module.exports = MetadataManager;
\ No newline at end of file
diff --git a/electron-ui/src/old/renderer.js b/electron-ui/src/old/renderer.js
deleted file mode 100644
index 461de7b..0000000
--- a/electron-ui/src/old/renderer.js
+++ /dev/null
@@ -1,818 +0,0 @@
-const { ipcRenderer } = require('electron');
-// const path = require('path');
-const WaveSurfer = require('wavesurfer.js');
-const RegionsPlugin = require('wavesurfer.js/dist/plugin/wavesurfer.regions.js');
-
-document.addEventListener('DOMContentLoaded', async () => {
- // Settings Modal Logic
- const settingsModal = document.getElementById('settings-modal');
- const settingsBtn = document.getElementById('settings-btn');
- const restartBtn = document.getElementById('restart-btn');
- const closeModalBtn = document.querySelector('.close-modal');
- const saveSettingsBtn = document.getElementById('save-settings');
- const selectOutputFolderBtn = document.getElementById('select-output-folder');
- const recordingLengthInput = document.getElementById('recording-length');
- const oscPortInput = document.getElementById('osc-port');
- const outputFolderInput = document.getElementById('output-folder');
- const inputDeviceSelect = document.getElementById('input-device');
-
- // Open settings modal
- settingsBtn.addEventListener('click', async () => {
- try {
- // Request microphone permissions first
- await navigator.mediaDevices.getUserMedia({ audio: true });
-
- // Load current settings
- const settings = await ipcRenderer.invoke('load-settings');
-
- // Populate input devices
- const devices = await ipcRenderer.invoke('get-input-devices');
-
- if (devices.length === 0) {
- inputDeviceSelect.innerHTML = '';
- } else {
- inputDeviceSelect.innerHTML = devices
- .map(
- (device) => ``,
- )
- .join('');
- }
-
- // Set current settings
- recordingLengthInput.value = settings.recordingLength;
- outputFolderInput.value = settings.outputFolder;
- inputDeviceSelect.value = settings.inputDevice;
- oscPortInput.value = settings.oscPort;
-
- settingsModal.style.display = 'block';
- } catch (error) {
- console.error('Error loading settings or devices:', error);
- alert('Please grant microphone permissions to list audio devices');
- }
- });
-
- restartBtn.addEventListener('click', async () => {
- try {
- await ipcRenderer.invoke('restart');
- } catch (error) {
- console.error('Error restarting:', error);
- alert('Failed to restart Clipper');
- }
- });
-
- // Close settings modal
- closeModalBtn.addEventListener('click', () => {
- settingsModal.style.display = 'none';
- });
-
- // Select output folder
- selectOutputFolderBtn.addEventListener('click', async () => {
- const folderPath = await ipcRenderer.invoke('select-output-folder');
- if (folderPath) {
- outputFolderInput.value = folderPath;
- }
- });
-
- // Save settings
- saveSettingsBtn.addEventListener('click', async () => {
- const settings = {
- recordingLength: parseInt(recordingLengthInput.value),
- oscPort: parseInt(oscPortInput.value),
- outputFolder: outputFolderInput.value,
- inputDevice: inputDeviceSelect.value,
- };
-
- const saved = await ipcRenderer.invoke('save-settings', settings);
- if (saved) {
- settingsModal.style.display = 'none';
- } else {
- alert('Failed to save settings');
- }
- });
-
- // Close modal if clicked outside
- window.addEventListener('click', (event) => {
- if (event.target === settingsModal) {
- settingsModal.style.display = 'none';
- }
- });
-
- const audioTrimmersList = document.getElementById('audio-trimmers-list');
- const collectionsList = document.getElementById('collections-list');
- //const currentSectionTitle = document.getElementById("current-section-title");
-
- // Global state to persist wavesurfer instances and trimmer states
- const globalState = {
- wavesurferInstances: {},
- trimmerStates: {},
- currentSection: 'untrimmed',
- trimmerElements: {},
- };
- // Utility function to format time
- function formatTime(seconds) {
- const minutes = Math.floor(seconds / 60);
- const remainingSeconds = Math.floor(seconds % 60);
- return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
- }
-
- // Populate collections list
- async function populateCollectionsList() {
- const collections = await ipcRenderer.invoke('get-collections');
-
- collectionsList.innerHTML = '';
-
- // Always add Untrimmed section first
- const untrimmedItem = document.createElement('div');
- untrimmedItem.classList.add('collection-item');
- untrimmedItem.textContent = 'Untrimmed';
- untrimmedItem.dataset.collection = 'untrimmed';
-
- untrimmedItem.addEventListener('click', () => {
- loadCollectionFiles('untrimmed');
- });
-
- collectionsList.appendChild(untrimmedItem);
-
- // Add other collections
- collections.forEach((collection) => {
- if (collection === 'untrimmed') {
- return;
- }
- const collectionItem = document.createElement('div');
- collectionItem.classList.add('collection-item');
- collectionItem.textContent = collection;
- collectionItem.dataset.collection = collection;
-
- collectionItem.addEventListener('click', () => {
- loadCollectionFiles(collection);
- });
-
- collectionsList.appendChild(collectionItem);
- });
- }
-
- // Modify loadCollectionFiles function
- async function loadCollectionFiles(collection) {
- if (collection !== globalState.currentSection) {
- //Clear existing trimmers and reset global state
- Object.keys(globalState.trimmerElements).forEach((filePath) => {
- const trimmerElement = globalState.trimmerElements[filePath];
- if (trimmerElement && trimmerElement.parentNode) {
- trimmerElement.parentNode.removeChild(trimmerElement);
- }
- });
-
- // Reset global state
- globalState.trimmerElements = {};
- globalState.wavesurferInstances = {};
- globalState.trimmerStates = {};
- }
-
- // Reset active states
- document.querySelectorAll('.collection-item').forEach((el) => {
- el.classList.remove('active');
- });
-
- // Set active state only for existing items
- const activeItem = document.querySelector(
- `.collection-item[data-collection="${collection}"]`,
- );
-
- // Only add active class if the item exists
- if (activeItem) {
- activeItem.classList.add('active');
- }
-
- // Update section title and global state
- //currentSectionTitle.textContent = collection;
- globalState.currentSection = collection;
-
- // Load files
- const files = await ipcRenderer.invoke('get-collection-files', collection);
-
- // Add new trimmers with saved trim information
- for (const file of files) {
- const filePath = file.originalPath || file.fileName;
-
- // If loading a collection, use saved trim information
- //if (collection !== "untrimmed") {
- // Store trim information in global state before creating trimmer
- // globalState.trimmerStates[filePath] = {
- // trimStart: file.trimStart || 0,
- // trimEnd: file.trimEnd || 0,
- // regionStart: file.trimStart || 0,
- // regionEnd: file.trimEnd || 0,
- // originalPath: file.originalPath,
- // };
- //}
-
- createAudioTrimmer(filePath, collection);
- }
- }
- // Create audio trimmer for a single file
- async function createAudioTrimmer(filePath, section) {
- // Check if trimmer already exists
- if (globalState.trimmerElements[filePath]) {
- return globalState.trimmerElements[filePath];
- }
-
- const savedTrimInfo = await ipcRenderer.invoke(
- 'get-trim-info',
- globalState.currentSection,
- path.basename(filePath),
- );
- // Create trimmer container
- const trimmerContainer = document.createElement('div');
- trimmerContainer.classList.add('audio-trimmer-item');
- trimmerContainer.dataset.filepath = filePath;
-
- // Create header with title and controls
- const trimmerHeader = document.createElement('div');
- trimmerHeader.classList.add('audio-trimmer-header');
-
- // Title container
- const titleContainer = document.createElement('div');
- titleContainer.classList.add('audio-trimmer-title-container');
-
- if (savedTrimInfo.title) {
- // Title
- const title = document.createElement('div');
- title.classList.add('audio-trimmer-title');
- title.textContent = savedTrimInfo.title;
- titleContainer.appendChild(title);
-
- // Filename
- const fileName = document.createElement('div');
- fileName.classList.add('audio-trimmer-filename');
- fileName.textContent = path.basename(filePath);
- titleContainer.appendChild(fileName);
- } else {
- // Title (using filename if no custom title)
- const title = document.createElement('div');
- title.classList.add('audio-trimmer-title');
- title.textContent = path.basename(filePath);
- titleContainer.appendChild(title);
-
- // Filename
- const fileName = document.createElement('div');
- fileName.classList.add('audio-trimmer-filename');
- fileName.textContent = 'hidden';
- fileName.style.opacity = 0;
- titleContainer.appendChild(fileName);
- }
-
- // Controls container
- const controlsContainer = document.createElement('div');
- controlsContainer.classList.add('audio-trimmer-controls');
-
- // Play/Pause and Save buttons
- const playPauseBtn = document.createElement('button');
- playPauseBtn.classList.add('play-pause-btn');
- playPauseBtn.innerHTML = `
-
- `;
-
- const saveTrimButton = document.createElement('button');
- saveTrimButton.classList.add('save-trim');
- saveTrimButton.innerHTML = `
-
- `;
-
- const deletebutton = document.createElement('button');
- deletebutton.classList.add('play-pause-btn');
- deletebutton.innerHTML = `
-
- `;
-
- controlsContainer.appendChild(playPauseBtn);
- controlsContainer.appendChild(saveTrimButton);
- controlsContainer.appendChild(deletebutton);
-
- // Assemble header
- trimmerHeader.appendChild(titleContainer);
- trimmerHeader.appendChild(controlsContainer);
- trimmerContainer.appendChild(trimmerHeader);
-
- // Waveform container
- const waveformContainer = document.createElement('div');
- waveformContainer.classList.add('waveform-container');
- const waveformId = `waveform-${path.basename(
- filePath,
- path.extname(filePath),
- )}`;
- waveformContainer.innerHTML = `
-
- `;
- trimmerContainer.appendChild(waveformContainer);
-
- // Time displays
- const timeInfo = document.createElement('div');
- timeInfo.classList.add('trim-info');
- timeInfo.innerHTML = `
-
- Start:
- 0:00
-
-
- End:
- 0:00
-
- `;
- // const zoomContainer = document.createElement('div');
- // zoomContainer.className = 'zoom-controls';
- // zoomContainer.innerHTML = `
- //
- //
- //
- // `;
- // timeInfo.appendChild(zoomContainer);
-
- // const zoomInBtn = zoomContainer.querySelector('.zoom-in');
- // const zoomOutBtn = zoomContainer.querySelector('.zoom-out');
- // const zoomSlider = zoomContainer.querySelector('.zoom-slider');
-
- // // Zoom functionality
- // const updateZoom = (zoomLevel) => {
- // // Get the current scroll position and width
- // const scrollContainer = wavesurfer.container.querySelector('wave');
- // const currentScroll = scrollContainer.scrollLeft;
- // const containerWidth = scrollContainer.clientWidth;
-
- // // Calculate the center point of the current view
- // //const centerTime = wavesurfer.getCurrentTime();
-
- // // Apply zoom
- // wavesurfer.zoom(zoomLevel);
-
- // // Recalculate scroll to keep the center point in view
- // const newDuration = wavesurfer.getDuration();
- // const pixelsPerSecond = wavesurfer.drawer.width / newDuration;
- // const centerPixel = centerTime * pixelsPerSecond;
-
- // // Adjust scroll to keep the center point in the same relative position
- // const newScrollLeft = centerPixel - (containerWidth / 2);
- // scrollContainer.scrollLeft = Math.max(0, newScrollLeft);
- // console.log(currentScroll, newScrollLeft);
- // };
-
- // zoomInBtn.addEventListener('click', () => {
- // const currentZoom = parseInt(zoomSlider.value);
- // zoomSlider.value = Math.min(currentZoom + 20, 200);
- // updateZoom(zoomSlider.value);
- // });
-
- // zoomOutBtn.addEventListener('click', () => {
- // const currentZoom = parseInt(zoomSlider.value);
- // zoomSlider.value = Math.max(currentZoom - 20, 1);
- // updateZoom(zoomSlider.value);
- // });
-
- // zoomSlider.addEventListener('input', (e) => {
- // updateZoom(e.target.value);
- // });
-
- trimmerContainer.appendChild(timeInfo);
-
- // Add to list and global state
- audioTrimmersList.appendChild(trimmerContainer);
- globalState.trimmerElements[filePath] = trimmerContainer;
-
- // Determine the file to load (original or current)
- const fileToLoad =
- section === 'untrimmed'
- ? filePath
- : globalState.trimmerStates[filePath]?.originalPath || filePath;
-
- // Setup wavesurfer
- const wavesurfer = WaveSurfer.create({
- container: `#${waveformId}`,
- waveColor: '#ccb1ff',
- progressColor: '#6e44ba',
- responsive: true,
- height: 100,
- hideScrollbar: true,
- // barWidth: 2,
- // barRadius: 3,
- cursorWidth: 1,
- backend: 'WebAudio',
- plugins: [
- RegionsPlugin.create({
- color: 'rgba(132, 81, 224, 0.3)',
- drag: false,
- resize: true,
- dragSelection: {
- slop: 20,
- },
- }),
- // ZoomPlugin.create({
- // // the amount of zoom per wheel step, e.g. 0.5 means a 50% magnification per scroll
- // scale: 0.5,
- // // Optionally, specify the maximum pixels-per-second factor while zooming
- // maxZoom: 100,
- // }),
- ],
- });
-
- // Store wavesurfer instance in global state
- globalState.wavesurferInstances[filePath] = wavesurfer;
-
- // Use existing trim state or create new one
- globalState.trimmerStates[filePath] = globalState.trimmerStates[filePath] ||
- savedTrimInfo || {
- trimStart: 0,
- trimEnd: 0,
- regionStart: undefined,
- regionEnd: undefined,
- originalPath: fileToLoad,
- };
- const startTimeDisplay = timeInfo.querySelector('.trim-start-time');
- const endTimeDisplay = timeInfo.querySelector('.trim-end-time');
-
- // Load audio file
- wavesurfer.load(`file://${fileToLoad}`);
-
- // Setup play/pause button
- playPauseBtn.onclick = () => {
- const instanceState = globalState.trimmerStates[filePath];
- if (wavesurfer.isPlaying()) {
- wavesurfer.pause();
- playPauseBtn.innerHTML = `
-
- `;
- } else {
- // Always start from the trim start
- wavesurfer.play(instanceState.trimStart, instanceState.trimEnd);
- playPauseBtn.innerHTML = `
-
- `;
- }
- };
-
- // When audio is ready
- wavesurfer.on('ready', async () => {
- const instanceState = globalState.trimmerStates[filePath];
-
- // Set trim times based on saved state or full duration
- if (instanceState.trimStart) {
- // Create initial region covering trim or full duration
- wavesurfer.clearRegions();
- const region = wavesurfer.addRegion({
- start: instanceState.trimStart,
- end: instanceState.trimEnd,
- color: 'rgba(132, 81, 224, 0.3)',
- drag: false,
- resize: true,
- });
- }
- instanceState.trimStart = instanceState.trimStart || 0;
- instanceState.trimEnd = instanceState.trimEnd || wavesurfer.getDuration();
-
- // Update time displays
- startTimeDisplay.textContent = formatTime(instanceState.trimStart);
- endTimeDisplay.textContent = formatTime(instanceState.trimEnd);
-
- // Store region details
- instanceState.regionStart = instanceState.trimStart;
- instanceState.regionEnd = instanceState.trimEnd;
-
- // Listen for region updates
- wavesurfer.on('region-update-end', async (updatedRegion) => {
- // Ensure the region doesn't exceed audio duration
- instanceState.trimStart = Math.max(0, updatedRegion.start);
- instanceState.trimEnd = Math.min(
- wavesurfer.getDuration(),
- updatedRegion.end,
- );
-
- // Update time displays
- startTimeDisplay.textContent = formatTime(instanceState.trimStart);
- endTimeDisplay.textContent = formatTime(instanceState.trimEnd);
-
- // Store updated region details
- instanceState.regionStart = instanceState.trimStart;
- instanceState.regionEnd = instanceState.trimEnd;
-
- globalState.trimmerStates[filePath] = instanceState;
-
- // Adjust region if it exceeds bounds
- updatedRegion.update({
- start: instanceState.trimStart,
- end: instanceState.trimEnd,
- });
- });
-
- // Handle region creation
- wavesurfer.on('region-created', (newRegion) => {
- // Remove all other regions
- Object.keys(wavesurfer.regions.list).forEach((id) => {
- if (wavesurfer.regions.list[id] !== newRegion) {
- wavesurfer.regions.list[id].remove();
- }
- });
- });
-
- // Reset to trim start when audio finishes
- wavesurfer.on('finish', () => {
- wavesurfer.setCurrentTime(instanceState.trimStart);
- playPauseBtn.innerHTML = `
-
- `;
- });
-
- // Save trimmed audio functionality
- saveTrimButton.addEventListener('click', async () => {
- try {
- // Get current collections
- const collections = await ipcRenderer.invoke('get-collections');
-
- // Create a dialog to select or create a collection
- const dialogHtml = `
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- // Create dialog overlay
- const overlay = document.createElement('div');
- overlay.style.position = 'fixed';
- overlay.style.top = '0';
- overlay.style.left = '0';
- overlay.style.width = '100%';
- overlay.style.height = '100%';
- overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
- overlay.style.zIndex = '999';
- overlay.innerHTML = dialogHtml;
- document.body.appendChild(overlay);
-
- const existingCollectionsSelect = overlay.querySelector(
- '#existing-collections',
- );
-
- const newSaveTitleInput = overlay.querySelector('#new-save-title');
- const createCollectionBtn = overlay.querySelector(
- '#create-collection-btn',
- );
- const saveToCollectionBtn = overlay.querySelector(
- '#save-to-collection-btn',
- );
- const cancelSaveBtn = overlay.querySelector('#cancel-save-btn');
-
- if (savedTrimInfo.title) {
- newSaveTitleInput.value = savedTrimInfo.title;
- }
-
- // Save to collection
- saveToCollectionBtn.addEventListener('click', async () => {
- const newTitle = document
- .getElementById('new-save-title')
- .value.trim();
- const settings = await ipcRenderer.invoke('load-settings');
-
- const selectedCollection = existingCollectionsSelect.value;
-
- if (!selectedCollection) {
- alert('Please select or create a collection');
- return;
- }
-
- try {
- await ipcRenderer.invoke(
- 'delete-old-file',
- settings.outputFolder,
- globalState.currentSection,
- savedTrimInfo.title,
- );
- await ipcRenderer.invoke(
- 'save-trimmed-file',
- path.basename(filePath),
- globalState.currentSection,
- selectedCollection,
- instanceState.trimStart,
- instanceState.trimEnd,
- newTitle,
- );
-
- const saveResult = await ipcRenderer.invoke(
- 'save-trimmed-audio',
- {
- originalFilePath: filePath,
- outputFolder: settings.outputFolder,
- collectionName: selectedCollection,
- title: newTitle,
- trimStart: instanceState.trimStart,
- trimEnd: instanceState.trimEnd,
- },
- );
-
- if (saveResult.success) {
- // Close save dialog
- // Remove dialog
- document.body.removeChild(overlay);
- trimmerContainer.remove();
- await loadCollectionFiles(globalState.currentSection);
- await populateCollectionsList();
-
- // Optional: Show success message
- //alert(`Trimmed audio saved to ${saveResult.filePath}`);
- } else {
- alert(`Failed to save trimmed audio: ${saveResult.error}`);
- }
-
- // Refresh the view
- } catch (error) {
- alert('Error saving file: ' + error.message);
- }
- });
-
- // Cancel button
- cancelSaveBtn.addEventListener('click', () => {
- document.body.removeChild(overlay);
- });
- } catch (error) {
- console.error('Error creating save dialog:', error);
- }
- });
- deletebutton.addEventListener('click', async () => {
- // Create confirmation dialog
- const confirmDelete = confirm(
- `Are you sure you want to delete this audio file?\nThis will remove the original file and any trimmed versions.`,
- );
-
- if (confirmDelete) {
- try {
- // Delete original file
- await ipcRenderer.invoke('delete-file', filePath);
-
- // Remove from UI
- trimmerContainer.remove();
-
- // Optional: Notify user
- alert('File deleted successfully');
-
- // Refresh the current section view
- await loadCollectionFiles(globalState.currentSection);
- await populateCollectionsList();
- } catch (error) {
- console.error('Error deleting file:', error);
- }
- }
- });
- });
-
- return trimmerContainer;
- }
-
- // Initial load of untrimmed files and collections
- await loadCollectionFiles('untrimmed');
- await populateCollectionsList();
-
- // Listen for new untrimmed files
- ipcRenderer.on('new-untrimmed-file', async (event, filePath) => {
- // Refresh the untrimmed section
- await loadCollectionFiles('untrimmed');
- await populateCollectionsList();
- });
-
- // Periodic refresh
- setInterval(async () => {
- await populateCollectionsList();
- await loadCollectionFiles(globalState.currentSection);
- }, 5000);
-});
-
-// Add collection button handler
-document
- .getElementById('add-collection-btn')
- .addEventListener('click', async () => {
- try {
- // Create a dialog to input new collection name
- const dialogHtml = `
-
-
Create New Collection
-
-
-
-
-
-
-
- `;
-
- // Create dialog overlay
- const overlay = document.createElement('div');
- overlay.style.position = 'fixed';
- overlay.style.top = '0';
- overlay.style.left = '0';
- overlay.style.width = '100%';
- overlay.style.height = '100%';
- overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
- overlay.style.zIndex = '999';
- overlay.innerHTML = dialogHtml;
- document.body.appendChild(overlay);
-
- const newCollectionInput = overlay.querySelector('#new-collection-input');
- const createCollectionConfirmBtn = overlay.querySelector(
- '#create-collection-confirm-btn',
- );
- const createCollectionCancelBtn = overlay.querySelector(
- '#create-collection-cancel-btn',
- );
-
- // Create collection when confirm button is clicked
- createCollectionConfirmBtn.addEventListener('click', async () => {
- const newCollectionName = newCollectionInput.value.trim();
-
- if (newCollectionName) {
- try {
- await ipcRenderer.invoke('add-new-collection', newCollectionName);
-
- // Remove dialog
- document.body.removeChild(overlay);
-
- // Refresh collections list
- await populateCollectionsList();
- } catch (error) {
- // Show error in the dialog
- const errorDiv = document.createElement('div');
- errorDiv.textContent = error.message;
- errorDiv.style.color = 'red';
- errorDiv.style.marginTop = '10px';
- overlay.querySelector('div').appendChild(errorDiv);
- }
- } else {
- // Show error if input is empty
- const errorDiv = document.createElement('div');
- errorDiv.textContent = 'Collection name cannot be empty';
- errorDiv.style.color = 'red';
- errorDiv.style.marginTop = '10px';
- overlay.querySelector('div').appendChild(errorDiv);
- }
- });
-
- // Cancel button closes the dialog
- createCollectionCancelBtn.addEventListener('click', () => {
- document.body.removeChild(overlay);
- });
-
- // Focus the input when dialog opens
- newCollectionInput.focus();
- } catch (error) {
- console.error('Error creating new collection dialog:', error);
- }
- });
diff --git a/electron-ui/src/old/styles.css b/electron-ui/src/old/styles.css
deleted file mode 100644
index 496928b..0000000
--- a/electron-ui/src/old/styles.css
+++ /dev/null
@@ -1,355 +0,0 @@
-body {
- font-family: Arial, sans-serif;
- margin: 0;
- padding: 0;
- background-color: #1e1e1e;
- height: 100vh;
- overflow: hidden;
- color: #ffffffd3;
-}
-
-.titlebar {
- height: 30px;
- background: #262626;
- -webkit-app-region: drag;
- color: white;
- display: flex;
- justify-content: center;
- align-items: center;
- border: none;
- box-shadow: none;
-}
-
-.app-container {
- display: flex;
- height: calc(100vh);
- margin-top: 0px;
-}
-
-.sidebar {
- width: 250px;
- background-color: #1e1e1e;
- border-right: 1px solid #303030;
- padding: 5px 20px 20px 20px;
- overflow-y: auto;
-}
-
-.sidebar-section {
- margin-bottom: 20px;
-}
-
-.sidebar-section h3 {
- margin-bottom: 10px;
- border-bottom: 1px solid #393939;
- padding-bottom: 5px;
-}
-
-.sidebar-section .add-collection-btn {
- width: 100%;
- padding: 10px;
- margin-top: 10px;
- background-color: #6e44ba;
- border: none;
- border-radius: 5px;
- cursor: pointer;
- color: #ffffffd3;
- }
-
- .sidebar-section .add-collection-btn:hover {
- background-color: #4f3186;
- }
-
-.section-item, .collection-item {
- padding: 10px;
- cursor: pointer;
- transition: background-color 0.2s;
- margin-bottom: 3px;
- border-radius: 5px;
-}
-
-.section-item:hover, .collection-item:hover {
- background-color: #303030;
-}
-
-.section-item.active, .collection-item.active {
- background-color: rgba(110, 68, 186, 0.3);
- color: #ccb1ff;
- font-weight: bold;
-}
-
-.main-content {
- flex-grow: 1;
- overflow: hidden;
- display: flex;
- flex-direction: column;
-}
-
-.audio-trimmers-section {
- background-color: #1e1e1e;
- flex-grow: 1;
- overflow: hidden;
- display: flex;
- flex-direction: column;
-}
-
-.audio-trimmers-list {
- flex-grow: 1;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- gap: 20px;
- padding: 20px;
- scrollbar-width: none; /* Firefox */
- -ms-overflow-style: none; /* Internet Explorer 10+ */
-}
-
-.audio-trimmers-list::-webkit-scrollbar { /* WebKit */
- width: 0;
- height: 0;
-}
-
-.audio-trimmer-item {
- position: relative;
- background-color: #1e1e1e;
- border-radius: 8px;
- padding: 15px;
- margin-bottom: 0px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.5);
-}
-
-.audio-trimmer-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 15px;
-}
-
-.audio-trimmer-title-container {
- display: flex;
- flex-direction: column;
-}
-
-.audio-trimmer-title {
- font-weight: bold;
- font-size: 18px;
- margin-bottom: 5px;
-}
-
-.audio-trimmer-filename {
- color: #888;
- font-weight: regular;
- font-size: 12px;
-}
-
-.audio-trimmer-controls {
- display: flex;
- gap: 5px;
-}
-
-.waveform-container {
- width: 100%;
- margin-bottom: 20px;
- overflow-x: hidden;
-}
-
-.waveform {
- overflow-x: hidden;
-}
-
-.audio-controls {
- display: flex;
- gap: 5px;
-}
-
-.trim-info {
- display: flex;
- justify-content: space-between;
- /* margin-bottom: 20px; */
-}
-
-.trim-time {
- font-size: 14px;
- color: #666;
-}
-
-
-.play-pause-btn, .save-trim {
- background-color: #6e44ba;
- border: none;
- padding: 10px;
- border-radius: 5px;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 40px;
- height: 40px;
- color: #ffffffd3;
-}
-
-.play-pause-btn:hover, .save-trim:hover {
- background-color: #4f3186;
-}
-
-.play-pause-btn svg, .save-trim svg {
- width: 20px;
- height: 20px;
- fill: white;
-}
-
-select {
- padding: 10px;
- border: 1px solid #ccc;
- background-color: #383838;
- border-radius: 5px;
- border-color:#303030;
- color: #ffffffd3;
- box-sizing: border-box;
-}
-
-input{
- padding: 10px;
- border: 1px solid #ccc;
- background-color: #383838;
- border-radius: 5px;
- border-color:#303030;
- color: #ffffffd3;
- box-sizing: border-box;
-}
-
-input:focus {
- border-color: #303030;
- outline: none;
-}
-
-select:focus {
- border-color: #303030;
- outline: none;
-}
-
-select:active {
- border-color: #303030;
- outline: none;
-}
-
-/* Settings Modal */
-.modal {
- display: none;
- position: fixed;
- z-index: 1000;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- overflow: auto;
- background-color: rgba(0,0,0,0.5);
-}
-
-.modal-content {
- position: relative;
- top: 50%;
- transform: translateY(-50%);
- background-color: #2a2a2a;
- margin: auto;
- padding: 20px;
- border-radius: 10px;
- width: 500px;
- color: #ffffffd3;
-}
-
-.settings-group {
- margin-bottom: 20px;
-}
-
-.settings-group label {
- display: block;
- margin-bottom: 10px;
-}
-
-
-.close-modal {
- color: #aaa;
- float: right;
- font-size: 28px;
- font-weight: bold;
- cursor: pointer;
-}
-
-.close-modal:hover {
- color: white;
-}
-
-#save-settings {
- width: 100%;
- padding: 10px;
- background-color: #6e44ba;
- color: #ffffffd3;
- border: none;
- border-radius: 5px;
- cursor: pointer;
-}
-
-#save-settings:hover {
- background-color: #4f3186;
-}
-
-#select-output-folder {
- width: 15%;
- height: 36px;
- padding: 10px;
- background-color: #6e44ba;
- color: #ffffffd3;
- border: none;
- border-radius: 5px;
- cursor: pointer;
- display: inline;
-}
-
-#select-output-folder:hover {
- background-color: #4f3186;
-}
-
-
-#input-device {
- width: 100%;
- padding: 10px;
-}
-#output-folder {
- width: 84%;
- padding: 10px;
-}
-
-.nav-btn {
- background: none;
- border: none;
- cursor: pointer;
- padding: 5px;
-}
-
-.nav-btn svg {
- width: 24px;
- height: 24px;
- fill: #ffffffd3;
-}
-
- /* Zoom controls styling */
- .zoom-controls {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-top: 10px;
-}
-.zoom-controls button {
- width: 30px;
- height: 30px;
- background-color: #f0f0f0;
- border: 1px solid #ccc;
- cursor: pointer;
-}
-.zoom-controls .zoom-slider {
- flex-grow: 1;
-}
-
-#recording-length, #osc-port {
- width: 20%;
-}
\ No newline at end of file
diff --git a/electron-ui/src/redux/main.ts b/electron-ui/src/redux/main.ts
index 3a5f7c7..eaa92bc 100644
--- a/electron-ui/src/redux/main.ts
+++ b/electron-ui/src/redux/main.ts
@@ -2,7 +2,7 @@ import { createSlice, configureStore } from '@reduxjs/toolkit';
import { ClipMetadata, MetadataState } from './types';
const initialState: MetadataState = {
- collections: {},
+ collections: [],
};
const metadataSlice = createSlice({
name: 'metadata',
@@ -13,32 +13,87 @@ const metadataSlice = createSlice({
},
setCollections(state, action) {
const { collection, newMetadata } = action.payload;
- state.collections[collection] = newMetadata;
+ const index = state.collections.findIndex(
+ (col) => col.name === collection,
+ );
+ if (index !== -1) {
+ state.collections[index] = newMetadata;
+ }
+ },
+ addCollection(state, action) {
+ const name = action.payload;
+ if (!state.collections.find((col) => col.name === name)) {
+ state.collections.push({ name, id: Date.now(), clips: [] });
+ }
},
editClip(state, action) {
const { collection, clip } = action.payload;
- const clips = state.collections[collection];
+ const collectionState = state.collections.find(
+ (col) => col.name === collection,
+ );
// console.log('Editing clip in collection:', collection, clip);
- if (clips) {
- const index = clips.findIndex((c) => c.filename === clip.filename);
+ if (collectionState) {
+ const index = collectionState.clips.findIndex(
+ (c) => c.filename === clip.filename,
+ );
if (index !== -1) {
- clips[index] = clip;
+ collectionState.clips[index] = clip;
}
}
},
+ deleteClip(state, action) {
+ const { collection, clip } = action.payload;
+ const collectionState = state.collections.find(
+ (col) => col.name === collection,
+ );
+ if (collectionState) {
+ collectionState.clips = collectionState.clips.filter(
+ (c) => c.filename !== clip.filename,
+ );
+ }
+ },
+ moveClip(state, action) {
+ const { sourceCollection, targetCollection, clip } = action.payload;
+ const sourceState = state.collections.find(
+ (col) => col.name === sourceCollection,
+ );
+ const targetState = state.collections.find(
+ (col) => col.name === targetCollection,
+ );
+ if (sourceState && targetState) {
+ sourceState.clips = sourceState.clips.filter(
+ (c) => c.filename !== clip.filename,
+ );
+ targetState.clips.push(clip);
+ }
+ },
addNewClips(state, action) {
const { collections } = action.payload;
Object.keys(collections).forEach((collection) => {
- if (!state.collections[collection]) {
- state.collections[collection] = [];
+ const collectionState = state.collections.find(
+ (col) => col.name === collection,
+ );
+ if (!collectionState) {
+ state.collections.push({
+ name: collection,
+ id: Date.now(),
+ clips: [],
+ });
}
const existingFilenames = new Set(
- state.collections[collection].map((clip) => clip.filename),
+ state.collections
+ .find((col) => col.name === collection)
+ ?.clips.map((clip) => clip.filename) || [],
);
const newClips = collections[collection].filter(
(clip: ClipMetadata) => !existingFilenames.has(clip.filename),
);
- state.collections[collection].push(...newClips);
+ // const collectionState = state.collections.find(
+ // (col) => col.name === collection,
+ // );
+ if (collectionState) {
+ collectionState.clips.push(...newClips);
+ }
});
},
},
@@ -58,5 +113,6 @@ export type RootState = ReturnType;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = AppStore['dispatch'];
-export const { setCollections, addNewClips } = metadataSlice.actions;
+export const { setCollections, addNewClips, addCollection } =
+ metadataSlice.actions;
export default metadataSlice.reducer;
diff --git a/electron-ui/src/redux/types.ts b/electron-ui/src/redux/types.ts
index a7e8197..c55a2dc 100644
--- a/electron-ui/src/redux/types.ts
+++ b/electron-ui/src/redux/types.ts
@@ -12,6 +12,12 @@ export interface ClipMetadata {
playbackType: PlaybackType;
}
-export interface MetadataState {
- collections: Record;
+export interface CollectionState {
+ name: string;
+ id: number;
+ clips: ClipMetadata[];
+}
+
+export interface MetadataState {
+ collections: CollectionState[];
}
diff --git a/electron-ui/src/renderer/App.tsx b/electron-ui/src/renderer/App.tsx
index c2b884d..aaaec0e 100644
--- a/electron-ui/src/renderer/App.tsx
+++ b/electron-ui/src/renderer/App.tsx
@@ -1,42 +1,152 @@
import { MemoryRouter as Router, Routes, Route } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { Provider } from 'react-redux';
+import Dialog from '@mui/material/Dialog';
+import DialogTitle from '@mui/material/DialogTitle';
+import DialogContent from '@mui/material/DialogContent';
+import DialogActions from '@mui/material/DialogActions';
// import 'tailwindcss/tailwind.css';
-import icon from '../../assets/icon.svg';
import './App.css';
import ClipList from './components/ClipList';
-import { useAppDispatch } from './hooks';
+import { useAppDispatch, useAppSelector } from './hooks';
import { store } from '../redux/main';
function MainPage() {
- const [collection, setCollection] = useState('Uncategorized');
-
const dispatch = useAppDispatch();
+ const collections = useAppSelector((state) =>
+ state.collections.map((col) => col.name),
+ );
+ const [selectedCollection, setSelectedCollection] = useState(
+ collections[0] || 'Uncategorized',
+ );
+ const [newCollectionOpen, setNewCollectionOpen] = useState(false);
+ const [newCollectionName, setNewCollectionName] = useState('');
+
useEffect(() => {
const fetchMetadata = async () => {
try {
const response = await fetch('http://localhost:5010/meta');
const data = await response.json();
- // console.log('Fetched collections:', data.collections);
- dispatch({ type: 'metadata/addNewClips', payload: data });
+ dispatch({ type: 'metadata/setAllData', payload: data });
} catch (error) {
console.error('Error fetching metadata:', error);
}
};
-
fetchMetadata();
+ const intervalId = setInterval(fetchMetadata, 5000);
+ return () => clearInterval(intervalId);
+ }, [dispatch]);
- // 1. Set up the interval
- const intervalId = setInterval(async () => {
- fetchMetadata();
- }, 5000); // 1000 milliseconds delay
+ useEffect(() => {
+ // Update selected collection if collections change
+ if (collections.length > 0 && !collections.includes(selectedCollection)) {
+ setSelectedCollection(collections[0]);
+ }
+ }, [collections, selectedCollection]);
- // 2. Return a cleanup function to clear the interval when the component unmounts
- return () => {
- clearInterval(intervalId);
- };
- }, [dispatch]); //
- return ;
+ const handleNewCollectionSave = () => {
+ if (
+ newCollectionName.trim() &&
+ !collections.includes(newCollectionName.trim())
+ ) {
+ dispatch({
+ type: 'metadata/addCollection',
+ payload: newCollectionName.trim(),
+ });
+ setSelectedCollection(newCollectionName.trim());
+ fetch('http://localhost:5010/meta/collections/add', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name: newCollectionName.trim() }),
+ })
+ .then((res) => res.json())
+ .catch((err) => console.error('Error creating collection:', err));
+ }
+ setNewCollectionOpen(false);
+ setNewCollectionName('');
+ };
+
+ return (
+
+ {/* Left Nav Bar - sticky */}
+
+
+ {/* Main Content */}
+
+
+
+
+ );
}
export default function App() {
diff --git a/electron-ui/src/renderer/components/AudioTrimer.tsx b/electron-ui/src/renderer/components/AudioTrimer.tsx
index b83d12f..81487af 100644
--- a/electron-ui/src/renderer/components/AudioTrimer.tsx
+++ b/electron-ui/src/renderer/components/AudioTrimer.tsx
@@ -10,41 +10,42 @@ import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import { useWavesurfer } from '@wavesurfer/react';
-import Regions from 'wavesurfer.js/dist/plugins/regions.esm.js';
+import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.esm.js';
import ZoomPlugin from 'wavesurfer.js/dist/plugins/zoom.esm.js';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import PauseIcon from '@mui/icons-material/Pause';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
+import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import DeleteIcon from '@mui/icons-material/Delete';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
-import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.esm.js';
import { ClipMetadata } from '../../redux/types';
import { useAppSelector } from '../hooks';
export interface AudioTrimmerProps {
- filename: string;
+ metadata: ClipMetadata;
onSave?: (metadata: ClipMetadata) => void;
- onDelete?: () => void;
+ onDelete?: (metadata: ClipMetadata) => void;
+ onMove?: (newCollection: string, metadata: ClipMetadata) => void;
}
export default function AudioTrimmer({
- filename,
+ metadata,
onSave,
onDelete,
+ onMove,
}: AudioTrimmerProps) {
const { attributes, listeners, setNodeRef, transform, transition } =
- useSortable({ id: filename });
+ useSortable({ id: metadata.filename });
- const metadata = useAppSelector((state) => {
- const clip = Object.values(state.collections)
- .flat()
- .find((c) => c.filename === filename);
- return clip ?? ({ filename, name: 'Unknown Clip' } as ClipMetadata);
- });
// Dialog state for editing name
const [editDialogOpen, setEditDialogOpen] = useState(false);
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [dropdownOpen, setDropdownOpen] = useState(false);
const [nameInput, setNameInput] = useState(metadata.name);
+ const collectionNames = useAppSelector((state) =>
+ state.collections.map((col) => col.name),
+ );
useEffect(() => {
setNameInput(metadata.name);
@@ -68,7 +69,7 @@ export default function AudioTrimmer({
const plugins = useMemo(
() => [
- Regions.create(),
+ RegionsPlugin.create(),
ZoomPlugin.create({
scale: 0.25,
}),
@@ -282,7 +283,7 @@ export default function AudioTrimmer({
>
{metadata.name}
- {fileBaseName}
+ {fileBaseName}
+
+
+
+
+ {dropdownOpen && (
+
+ {collectionNames.map((name) => (
+
+ ))}
+
+ )}
+
-
diff --git a/electron-ui/src/renderer/components/ClipList.tsx b/electron-ui/src/renderer/components/ClipList.tsx
index 736ef3f..6b76707 100644
--- a/electron-ui/src/renderer/components/ClipList.tsx
+++ b/electron-ui/src/renderer/components/ClipList.tsx
@@ -1,3 +1,4 @@
+import React from 'react';
import {
DndContext,
closestCenter,
@@ -21,7 +22,8 @@ export interface ClipListProps {
export default function ClipList({ collection }: ClipListProps) {
const metadata = useAppSelector(
- (state) => state.collections[collection] || [],
+ (state) =>
+ state.collections.find((col) => col.name === collection) || { clips: [] },
);
const dispatch = useAppDispatch();
@@ -29,14 +31,19 @@ export default function ClipList({ collection }: ClipListProps) {
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
);
- async function handleDragEnd(event) {
+ async function handleDragEnd(event: any) {
const { active, over } = event;
if (active.id !== over?.id) {
- const oldIndex = metadata.findIndex(
+ const oldIndex = metadata.clips.findIndex(
(item) => item.filename === active.id,
);
- const newIndex = metadata.findIndex((item) => item.filename === over.id);
- const newMetadata = arrayMove(metadata, oldIndex, newIndex);
+ const newIndex = metadata.clips.findIndex(
+ (item) => item.filename === over.id,
+ );
+ const newMetadata = {
+ ...metadata,
+ clips: arrayMove(metadata.clips, oldIndex, newIndex),
+ };
console.log('New order:', newMetadata);
dispatch({
type: 'metadata/setCollections',
@@ -52,7 +59,7 @@ export default function ClipList({ collection }: ClipListProps) {
},
body: JSON.stringify({
name: collection,
- clips: newMetadata,
+ clips: newMetadata.clips,
}),
},
);
@@ -66,6 +73,47 @@ export default function ClipList({ collection }: ClipListProps) {
}
}
+ async function handleDelete(meta: ClipMetadata) {
+ dispatch({
+ type: 'metadata/deleteClip',
+ payload: { collection, clip: meta },
+ });
+ fetch('http://localhost:5010/meta/collection/clips/remove', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ name: collection,
+ clip: meta,
+ }),
+ })
+ .then((res) => res.json())
+ .catch((err) => console.error('Error deleting clip:', err));
+ console.log('Deleting clip:', meta);
+ }
+
+ async function handleClipMove(targetCollection: string, meta: ClipMetadata) {
+ console.log('Moving clip:', meta, 'to collection:', targetCollection);
+ dispatch({
+ type: 'metadata/moveClip',
+ payload: { sourceCollection: collection, targetCollection, clip: meta },
+ });
+ fetch('http://localhost:5010/meta/collection/clips/move', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ sourceCollection: collection,
+ targetCollection,
+ clip: meta,
+ }),
+ })
+ .then((res) => res.json())
+ .catch((err) => console.error('Error moving clip:', err));
+ }
+
async function handleClipSave(meta: ClipMetadata) {
try {
dispatch({
@@ -85,7 +133,7 @@ export default function ClipList({ collection }: ClipListProps) {
}),
},
);
- const data = await response.json();
+ await response.json();
// console.log('handle clip save return:', data.collections);
dispatch({
type: 'metadata/editClip',
@@ -97,24 +145,42 @@ export default function ClipList({ collection }: ClipListProps) {
}
return (
-
+
item.filename)}
+ items={metadata.clips.map((item) => item.filename)}
strategy={verticalListSortingStrategy}
>
- {metadata.map((trimmer) => (
+ {metadata.clips.map((trimmer, idx) => (
+
+
+ {(idx + 1) % 10 === 0 && idx !== metadata.clips.length - 1 && (
+
+
+ -- Page {Math.ceil((idx + 1) / 10) + 1} --
+
+
+ )}
+
+ ))}
+ {/* {metadata.map((trimmer) => (
- ))}
+ ))} */}