This commit is contained in:
michalcourson
2026-02-04 18:13:56 -05:00
commit 51ad065047
26 changed files with 8383 additions and 0 deletions

4
electron-ui/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
*.log
.DS_Store
dist/

17
electron-ui/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"args" : ["."],
"outputCapture": "std"
}
]
}

BIN
electron-ui/build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

BIN
electron-ui/build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

5558
electron-ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

51
electron-ui/package.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "audio-clipper",
"version": "1.0.0",
"main": "src/main.js",
"dependencies": {
"chokidar": "^3.5.3",
"electron-reload": "^2.0.0-alpha.1",
"python-shell": "^5.0.0",
"wavefile": "^11.0.0",
"wavesurfer.js": "^6.6.4"
},
"scripts": {
"start": "electron .",
"dev": "electron . --enable-logging",
"build": "electron-builder",
"build:win": "electron-builder --win",
"build:mac": "electron-builder --mac",
"build:linux": "electron-builder --linux"
},
"build": {
"appId": "com.michalcourson.cliptrimserivce",
"productName": "ClipTrim",
"directories": {
"output": "dist"
},
"extraResources": [
{
"from": "../audio-service",
"to": "audio-service",
"filter": ["**/*"]
}
],
"win": {
"target": ["nsis"],
"icon": "build/icon.ico"
},
"mac": {
"target": ["dmg"],
"icon": "build/icon.icns"
},
"linux": {
"target": ["AppImage"],
"icon": "build/icon.png"
}
},
"devDependencies": {
"electron-builder": "^25.1.8",
"electron": "^13.1.7"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html>
<head>
<title>Audio Clip Trimmer</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="titlebar"></div>
<div class="app-container">
<div class="sidebar">
<div class="sidebar-section">
<h3>Collections</h3>
<div id="collections-list"></div>
<button id="add-collection-btn" class="add-collection-btn">+ New Collection</button>
</div>
<div class="sidebar-section">
<div id="nav-buttons">
<button id="settings-btn" class="nav-btn">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.06-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.06,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
</svg>
</button>
<button id="restart-btn" class="nav-btn">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
</svg>
</button>
</div>
</div>
</div>
<div class="main-content">
<div class="audio-trimmers-section">
<div id="audio-trimmers-list" class="audio-trimmers-list">
<!-- Audio trimmers will be dynamically added here -->
</div>
</div>
</div>
</div>
<!-- Settings Modal -->
<div id="settings-modal" class="modal">
<div class="modal-content">
<span class="close-modal">&times;</span>
<h2>Settings</h2>
<div class="settings-group">
<label for="recording-length">Recording Length (seconds):</label>
<input type="number" id="recording-length" min="1" max="300">
</div>
<div class="settings-group">
<label for="osc-port">OSC port:</label>
<input type="number" id="osc-port" min="5000" max="6000">
</div>
<div class="settings-group">
<label for="output-folder">Output Folder:</label>
<input type="text" id="output-folder" readonly>
<button id="select-output-folder">Browse</button>
</div>
<div class="settings-group">
<label for="input-device">Input Device:</label>
<select id="input-device"></select>
</div>
<button id="save-settings">Save Settings</button>
</div>
</div>
<script src="node_modules/wavesurfer.js/dist/wavesurfer.min.js"></script>
<script src="node_modules/wavesurfer.js/dist/plugin/wavesurfer.regions.js"></script>
<script src="renderer.js"></script>
</body>
</html>

480
electron-ui/src/main.js Normal file
View File

@ -0,0 +1,480 @@
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();
}
});

View File

@ -0,0 +1,234 @@
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;

823
electron-ui/src/renderer.js Normal file
View File

@ -0,0 +1,823 @@
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 = "<option>No microphones found</option>";
} else {
inputDeviceSelect.innerHTML = devices
.map(
(device) => `<option value="${device.id}">${device.name}</option>`
)
.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 = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
`;
const saveTrimButton = document.createElement("button");
saveTrimButton.classList.add("save-trim");
saveTrimButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/>
</svg>
`;
const deletebutton = document.createElement("button");
deletebutton.classList.add("play-pause-btn");
deletebutton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
</svg>
`;
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 = `
<div id="${waveformId}" class="waveform"></div>
`;
trimmerContainer.appendChild(waveformContainer);
// Time displays
const timeInfo = document.createElement("div");
timeInfo.classList.add("trim-info");
timeInfo.innerHTML = `
<div class="trim-time">
<span>Start: </span>
<span class="trim-start-time">0:00</span>
</div>
<div class="trim-time">
<span>End: </span>
<span class="trim-end-time">0:00</span>
</div>
`;
// const zoomContainer = document.createElement('div');
// zoomContainer.className = 'zoom-controls';
// zoomContainer.innerHTML = `
// <button class="zoom-in">+</button>
// <button class="zoom-out">-</button>
// <input type="range" min="1" max="200" value="100" class="zoom-slider">
// `;
// 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 = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
`;
} else {
// Always start from the trim start
wavesurfer.play(instanceState.trimStart, instanceState.trimEnd);
playPauseBtn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
</svg>
`;
}
};
// 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 = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
`;
});
// 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 = `
<div id="save-collection-dialog"
style="
position: fixed;
top: 50%;
left: 50%;
background-color: #2a2a2a;
transform: translate(-50%, -50%);
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
z-index: 1000;
">
<div style="">
<input type="text" id="new-save-title" placeholder="Title">
</div>
<select id="existing-collections" style="width: 100%; margin-top: 10px;">
${collections
.map((col) =>
col === "untrimmed"
? ""
: `<option value="${col}" ${
globalState.currentSection === col ? "selected" : ""
}>${col}</option>`
)
.join("")}
</select>
<div style="margin-top: 10px; display: flex; justify-content: space-between;">
<button class="play-pause-btn" id="cancel-save-btn" style="width: 48%; ">Cancel</button>
<button class="play-pause-btn" id="save-to-collection-btn" style="width: 48%;">Save</button>
</div>
</div>
`;
// 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 = `
<div id="new-collection-dialog" style="
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #2a2a2a;
padding: 0px 10px 10px 10px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
z-index: 1000;
">
<h4>Create New Collection</h4>
<input
type="text"
id="new-collection-input"
placeholder="Enter collection name"
style="width: 100%; align-self: center; padding: 10px; margin-bottom: 10px;"
>
<div style="display: flex; justify-content: space-between;">
<button id="create-collection-cancel-btn" class="play-pause-btn" style="width: 48%; ">Cancel</button>
<button id="create-collection-confirm-btn" class="play-pause-btn" style="width: 48%; ">Create</button>
</div>
</div>
`;
// 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);
}
});

355
electron-ui/src/styles.css Normal file
View File

@ -0,0 +1,355 @@
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%;
}

374
electron-ui/temp.json Normal file
View File

@ -0,0 +1,374 @@
[
{
"index": 0,
"name": "Microsoft Sound Mapper - Input",
"max_input_channels": 2,
"default_samplerate": 44100.0
},
{
"index": 1,
"name": "Voicemeeter Out B1 (VB-Audio Vo",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 2,
"name": "Headset Microphone (3- Arctis 7",
"max_input_channels": 1,
"default_samplerate": 44100.0
},
{
"index": 3,
"name": "Voicemeeter Out B3 (VB-Audio Vo",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 4,
"name": "Voicemeeter Out A5 (VB-Audio Vo",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 5,
"name": "Voicemeeter Out A3 (VB-Audio Vo",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 6,
"name": "Voicemeeter Out B2 (VB-Audio Vo",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 7,
"name": "Voicemeeter Out A1 (VB-Audio Vo",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 8,
"name": "Analogue 1 + 2 (Focusrite USB A",
"max_input_channels": 2,
"default_samplerate": 44100.0
},
{
"index": 9,
"name": "Voicemeeter Out A4 (VB-Audio Vo",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 10,
"name": "Headset Microphone (Oculus Virt",
"max_input_channels": 1,
"default_samplerate": 44100.0
},
{
"index": 11,
"name": "Voicemeeter Out A2 (VB-Audio Vo",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 28,
"name": "Primary Sound Capture Driver",
"max_input_channels": 2,
"default_samplerate": 44100.0
},
{
"index": 29,
"name": "Voicemeeter Out B1 (VB-Audio Voicemeeter VAIO)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 30,
"name": "Headset Microphone (3- Arctis 7 Chat)",
"max_input_channels": 1,
"default_samplerate": 44100.0
},
{
"index": 31,
"name": "Voicemeeter Out B3 (VB-Audio Voicemeeter VAIO)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 32,
"name": "Voicemeeter Out A5 (VB-Audio Voicemeeter VAIO)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 33,
"name": "Voicemeeter Out A3 (VB-Audio Voicemeeter VAIO)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 34,
"name": "Voicemeeter Out B2 (VB-Audio Voicemeeter VAIO)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 35,
"name": "Voicemeeter Out A1 (VB-Audio Voicemeeter VAIO)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 36,
"name": "Analogue 1 + 2 (Focusrite USB Audio)",
"max_input_channels": 2,
"default_samplerate": 44100.0
},
{
"index": 37,
"name": "Voicemeeter Out A4 (VB-Audio Voicemeeter VAIO)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 38,
"name": "Headset Microphone (Oculus Virtual Audio Device)",
"max_input_channels": 1,
"default_samplerate": 44100.0
},
{
"index": 39,
"name": "Voicemeeter Out A2 (VB-Audio Voicemeeter VAIO)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 71,
"name": "Headset Microphone (3- Arctis 7 Chat)",
"max_input_channels": 1,
"default_samplerate": 48000.0
},
{
"index": 72,
"name": "Voicemeeter Out B3 (VB-Audio Voicemeeter VAIO)",
"max_input_channels": 2,
"default_samplerate": 48000.0
},
{
"index": 73,
"name": "Voicemeeter Out A5 (VB-Audio Voicemeeter VAIO)",
"max_input_channels": 2,
"default_samplerate": 48000.0
},
{
"index": 74,
"name": "Voicemeeter Out A3 (VB-Audio Voicemeeter VAIO)",
"max_input_channels": 2,
"default_samplerate": 48000.0
},
{
"index": 75,
"name": "Voicemeeter Out B2 (VB-Audio Voicemeeter VAIO)",
"max_input_channels": 2,
"default_samplerate": 48000.0
},
{
"index": 76,
"name": "Voicemeeter Out A1 (VB-Audio Voicemeeter VAIO)",
"max_input_channels": 2,
"default_samplerate": 48000.0
},
{
"index": 77,
"name": "Analogue 1 + 2 (Focusrite USB Audio)",
"max_input_channels": 2,
"default_samplerate": 48000.0
},
{
"index": 78,
"name": "Voicemeeter Out A4 (VB-Audio Voicemeeter VAIO)",
"max_input_channels": 2,
"default_samplerate": 48000.0
},
{
"index": 79,
"name": "Headset Microphone (Oculus Virtual Audio Device)",
"max_input_channels": 1,
"default_samplerate": 48000.0
},
{
"index": 80,
"name": "Voicemeeter Out A2 (VB-Audio Voicemeeter VAIO)",
"max_input_channels": 2,
"default_samplerate": 48000.0
},
{
"index": 81,
"name": "Voicemeeter Out B1 (VB-Audio Voicemeeter VAIO)",
"max_input_channels": 2,
"default_samplerate": 48000.0
},
{
"index": 82,
"name": "Microphone (Voicemod VAD Wave)",
"max_input_channels": 2,
"default_samplerate": 48000.0
},
{
"index": 85,
"name": "Input (Voicemeeter Point 2)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 87,
"name": "Input (Voicemeeter Point 5)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 89,
"name": "Input (Voicemeeter Point 8)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 90,
"name": "Voicemeeter Out 2 (Voicemeeter Point 2)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 91,
"name": "Voicemeeter Out 5 (Voicemeeter Point 5)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 92,
"name": "Voicemeeter Out 8 (Voicemeeter Point 8)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 94,
"name": "Input (Voicemeeter Point 3)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 96,
"name": "Input (Voicemeeter Point 6)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 97,
"name": "Voicemeeter Out 3 (Voicemeeter Point 3)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 98,
"name": "Voicemeeter Out 6 (Voicemeeter Point 6)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 100,
"name": "Input (Voicemeeter Point 1)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 102,
"name": "Input (Voicemeeter Point 4)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 104,
"name": "Input (Voicemeeter Point 7)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 105,
"name": "Voicemeeter Out 1 (Voicemeeter Point 1)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 106,
"name": "Voicemeeter Out 4 (Voicemeeter Point 4)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 107,
"name": "Voicemeeter Out 7 (Voicemeeter Point 7)",
"max_input_channels": 8,
"default_samplerate": 44100.0
},
{
"index": 108,
"name": "Stereo Mix (Realtek HD Audio Stereo input)",
"max_input_channels": 2,
"default_samplerate": 48000.0
},
{
"index": 109,
"name": "Line In (Realtek HD Audio Line input)",
"max_input_channels": 2,
"default_samplerate": 44100.0
},
{
"index": 110,
"name": "Microphone (Realtek HD Audio Mic input)",
"max_input_channels": 2,
"default_samplerate": 44100.0
},
{
"index": 114,
"name": "SteelSeries Sonar - Microphone (SteelSeries_Sonar_VAD Chat Capture Wave)",
"max_input_channels": 2,
"default_samplerate": 44100.0
},
{
"index": 120,
"name": "SteelSeries Sonar - Stream (SteelSeries_Sonar_VAD Stream Wave)",
"max_input_channels": 2,
"default_samplerate": 44100.0
},
{
"index": 123,
"name": "Input (OCULUSVAD Wave Speaker Headphone)",
"max_input_channels": 2,
"default_samplerate": 44100.0
},
{
"index": 124,
"name": "Headset Microphone (OCULUSVAD Wave Microphone Headphone)",
"max_input_channels": 1,
"default_samplerate": 48000.0
},
{
"index": 125,
"name": "Headset Microphone (Arctis 7 Chat)",
"max_input_channels": 1,
"default_samplerate": 44100.0
},
{
"index": 129,
"name": "Analogue 1 + 2 (wc4800_8016)",
"max_input_channels": 2,
"default_samplerate": 48000.0
},
{
"index": 133,
"name": "Headset (@System32\\drivers\\bthhfenum.sys,#2;%1 Hands-Free%0\r\n;(Michal<61>s AirPods Pro - Find My))",
"max_input_channels": 1,
"default_samplerate": 8000.0
}
]