start of react migration

This commit is contained in:
michalcourson
2026-02-04 20:42:14 -05:00
parent 51ad065047
commit 17bace5eaf
71 changed files with 26503 additions and 6037 deletions

View File

@ -1,68 +0,0 @@
<!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>

View File

@ -0,0 +1,149 @@
/* eslint global-require: off, no-console: off, promise/always-return: off */
/**
* This module executes inside of electron's main process. You can start
* electron renderer process from here and communicate with the other processes
* through IPC.
*
* When running `npm run build` or `npm run build:main`, this file is compiled to
* `./src/main.js` using webpack. This gives us some performance wins.
*/
import fs from 'fs';
import path from 'path';
import { app, BrowserWindow, shell, ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater';
import log from 'electron-log';
import MenuBuilder from './menu';
import { resolveHtmlPath } from './util';
class AppUpdater {
constructor() {
log.transports.file.level = 'info';
autoUpdater.logger = log;
autoUpdater.checkForUpdatesAndNotify();
}
}
let mainWindow: BrowserWindow | null = null;
ipcMain.on('ipc-example', async (event, arg) => {
const msgTemplate = (pingPong: string) => `IPC test: ${pingPong}`;
console.log(msgTemplate(arg));
event.reply('ipc-example', msgTemplate('pong'));
});
if (process.env.NODE_ENV === 'production') {
const sourceMapSupport = require('source-map-support');
sourceMapSupport.install();
}
const isDebug =
process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true';
if (isDebug) {
require('electron-debug').default();
}
const installExtensions = async () => {
const installer = require('electron-devtools-installer');
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
const extensions = ['REACT_DEVELOPER_TOOLS'];
return installer
.default(
extensions.map((name) => installer[name]),
forceDownload,
)
.catch(console.log);
};
const createWindow = async () => {
if (isDebug) {
await installExtensions();
}
const RESOURCES_PATH = app.isPackaged
? path.join(process.resourcesPath, 'assets')
: path.join(__dirname, '../../assets');
const getAssetPath = (...paths: string[]): string => {
return path.join(RESOURCES_PATH, ...paths);
};
mainWindow = new BrowserWindow({
show: false,
width: 1024,
height: 728,
icon: getAssetPath('icon.png'),
webPreferences: {
preload: app.isPackaged
? path.join(__dirname, 'preload.js')
: path.join(__dirname, '../../.erb/dll/preload.js'),
},
});
mainWindow.loadURL(resolveHtmlPath('index.html'));
mainWindow.on('ready-to-show', () => {
if (!mainWindow) {
throw new Error('"mainWindow" is not defined');
}
if (process.env.START_MINIMIZED) {
mainWindow.minimize();
} else {
mainWindow.show();
}
});
mainWindow.on('closed', () => {
mainWindow = null;
});
const menuBuilder = new MenuBuilder(mainWindow);
menuBuilder.buildMenu();
// Open urls in the user's browser
mainWindow.webContents.setWindowOpenHandler((edata) => {
shell.openExternal(edata.url);
return { action: 'deny' };
});
ipcMain.handle('load-audio-buffer', async (event, filePath) => {
try {
// console.log(`Loading audio file: ${filePath}`);
const buffer = fs.readFileSync(filePath);
// console.log(buffer);
return buffer;
} catch (err) {
return { error: err.message };
}
});
// Remove this if your app does not use auto updates
// eslint-disable-next-line
new AppUpdater();
};
/**
* Add event listeners...
*/
app.on('window-all-closed', () => {
// Respect the OSX convention of having the application in memory even
// after all windows have been closed
if (process.platform !== 'darwin') {
app.quit();
}
});
app
.whenReady()
.then(() => {
createWindow();
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) createWindow();
});
})
.catch(console.log);

View File

@ -0,0 +1,290 @@
import {
app,
Menu,
shell,
BrowserWindow,
MenuItemConstructorOptions,
} from 'electron';
interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions {
selector?: string;
submenu?: DarwinMenuItemConstructorOptions[] | Menu;
}
export default class MenuBuilder {
mainWindow: BrowserWindow;
constructor(mainWindow: BrowserWindow) {
this.mainWindow = mainWindow;
}
buildMenu(): Menu {
if (
process.env.NODE_ENV === 'development' ||
process.env.DEBUG_PROD === 'true'
) {
this.setupDevelopmentEnvironment();
}
const template =
process.platform === 'darwin'
? this.buildDarwinTemplate()
: this.buildDefaultTemplate();
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
return menu;
}
setupDevelopmentEnvironment(): void {
this.mainWindow.webContents.on('context-menu', (_, props) => {
const { x, y } = props;
Menu.buildFromTemplate([
{
label: 'Inspect element',
click: () => {
this.mainWindow.webContents.inspectElement(x, y);
},
},
]).popup({ window: this.mainWindow });
});
}
buildDarwinTemplate(): MenuItemConstructorOptions[] {
const subMenuAbout: DarwinMenuItemConstructorOptions = {
label: 'Electron',
submenu: [
{
label: 'About ElectronReact',
selector: 'orderFrontStandardAboutPanel:',
},
{ type: 'separator' },
{ label: 'Services', submenu: [] },
{ type: 'separator' },
{
label: 'Hide ElectronReact',
accelerator: 'Command+H',
selector: 'hide:',
},
{
label: 'Hide Others',
accelerator: 'Command+Shift+H',
selector: 'hideOtherApplications:',
},
{ label: 'Show All', selector: 'unhideAllApplications:' },
{ type: 'separator' },
{
label: 'Quit',
accelerator: 'Command+Q',
click: () => {
app.quit();
},
},
],
};
const subMenuEdit: DarwinMenuItemConstructorOptions = {
label: 'Edit',
submenu: [
{ label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' },
{ label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' },
{ type: 'separator' },
{ label: 'Cut', accelerator: 'Command+X', selector: 'cut:' },
{ label: 'Copy', accelerator: 'Command+C', selector: 'copy:' },
{ label: 'Paste', accelerator: 'Command+V', selector: 'paste:' },
{
label: 'Select All',
accelerator: 'Command+A',
selector: 'selectAll:',
},
],
};
const subMenuViewDev: MenuItemConstructorOptions = {
label: 'View',
submenu: [
{
label: 'Reload',
accelerator: 'Command+R',
click: () => {
this.mainWindow.webContents.reload();
},
},
{
label: 'Toggle Full Screen',
accelerator: 'Ctrl+Command+F',
click: () => {
this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
},
},
{
label: 'Toggle Developer Tools',
accelerator: 'Alt+Command+I',
click: () => {
this.mainWindow.webContents.toggleDevTools();
},
},
],
};
const subMenuViewProd: MenuItemConstructorOptions = {
label: 'View',
submenu: [
{
label: 'Toggle Full Screen',
accelerator: 'Ctrl+Command+F',
click: () => {
this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
},
},
],
};
const subMenuWindow: DarwinMenuItemConstructorOptions = {
label: 'Window',
submenu: [
{
label: 'Minimize',
accelerator: 'Command+M',
selector: 'performMiniaturize:',
},
{ label: 'Close', accelerator: 'Command+W', selector: 'performClose:' },
{ type: 'separator' },
{ label: 'Bring All to Front', selector: 'arrangeInFront:' },
],
};
const subMenuHelp: MenuItemConstructorOptions = {
label: 'Help',
submenu: [
{
label: 'Learn More',
click() {
shell.openExternal('https://electronjs.org');
},
},
{
label: 'Documentation',
click() {
shell.openExternal(
'https://github.com/electron/electron/tree/main/docs#readme',
);
},
},
{
label: 'Community Discussions',
click() {
shell.openExternal('https://www.electronjs.org/community');
},
},
{
label: 'Search Issues',
click() {
shell.openExternal('https://github.com/electron/electron/issues');
},
},
],
};
const subMenuView =
process.env.NODE_ENV === 'development' ||
process.env.DEBUG_PROD === 'true'
? subMenuViewDev
: subMenuViewProd;
return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp];
}
buildDefaultTemplate() {
const templateDefault = [
{
label: '&File',
submenu: [
{
label: '&Open',
accelerator: 'Ctrl+O',
},
{
label: '&Close',
accelerator: 'Ctrl+W',
click: () => {
this.mainWindow.close();
},
},
],
},
{
label: '&View',
submenu:
process.env.NODE_ENV === 'development' ||
process.env.DEBUG_PROD === 'true'
? [
{
label: '&Reload',
accelerator: 'Ctrl+R',
click: () => {
this.mainWindow.webContents.reload();
},
},
{
label: 'Toggle &Full Screen',
accelerator: 'F11',
click: () => {
this.mainWindow.setFullScreen(
!this.mainWindow.isFullScreen(),
);
},
},
{
label: 'Toggle &Developer Tools',
accelerator: 'Alt+Ctrl+I',
click: () => {
this.mainWindow.webContents.toggleDevTools();
},
},
]
: [
{
label: 'Toggle &Full Screen',
accelerator: 'F11',
click: () => {
this.mainWindow.setFullScreen(
!this.mainWindow.isFullScreen(),
);
},
},
],
},
{
label: 'Help',
submenu: [
{
label: 'Learn More',
click() {
shell.openExternal('https://electronjs.org');
},
},
{
label: 'Documentation',
click() {
shell.openExternal(
'https://github.com/electron/electron/tree/main/docs#readme',
);
},
},
{
label: 'Community Discussions',
click() {
shell.openExternal('https://www.electronjs.org/community');
},
},
{
label: 'Search Issues',
click() {
shell.openExternal('https://github.com/electron/electron/issues');
},
},
],
},
];
return templateDefault;
}
}

View File

@ -0,0 +1,32 @@
// Disable no-unused-vars, broken for spread args
/* eslint no-unused-vars: off */
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
export type Channels = 'ipc-example';
const electronHandler = {
ipcRenderer: {
sendMessage(channel: Channels, ...args: unknown[]) {
ipcRenderer.send(channel, ...args);
},
on(channel: Channels, func: (...args: unknown[]) => void) {
const subscription = (_event: IpcRendererEvent, ...args: unknown[]) =>
func(...args);
ipcRenderer.on(channel, subscription);
return () => {
ipcRenderer.removeListener(channel, subscription);
};
},
once(channel: Channels, func: (...args: unknown[]) => void) {
ipcRenderer.once(channel, (_event, ...args) => func(...args));
},
loadAudioBuffer: (filePath: string) =>
ipcRenderer.invoke('load-audio-buffer', filePath),
},
};
contextBridge.exposeInMainWorld('electron', electronHandler);
export type ElectronHandler = typeof electronHandler;

View File

@ -0,0 +1,13 @@
/* eslint import/prefer-default-export: off */
import { URL } from 'url';
import path from 'path';
export function resolveHtmlPath(htmlFileName: string) {
if (process.env.NODE_ENV === 'development') {
const port = process.env.PORT || 1212;
const url = new URL(`http://localhost:${port}`);
url.pathname = htmlFileName;
return url.href;
}
return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`;
}

View File

@ -0,0 +1,74 @@
<!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>

View File

@ -1,14 +1,14 @@
const { app, BrowserWindow, ipcMain, Tray, Menu, dialog } = require("electron");
const path = require("path");
const os = require("os");
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");
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");
const { webContents } = require('electron');
// import { app, BrowserWindow, ipcMain, Tray, Menu, dialog } from "electron";
// import path from "path";
@ -20,35 +20,34 @@ const { webContents } = require("electron");
// 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 metadataPath = path.join(app.getPath('userData'), 'audio_metadata.json');
const metadataManager = new MetadataManager(metadataPath);
async function createPythonService() {
const pythonPath =
process.platform === "win32"
process.platform === 'win32'
? path.join(
__dirname,
"..",
"..",
"audio-service",
"venv",
"Scripts",
"python.exe"
'..',
'..',
'audio-service',
'venv',
'Scripts',
'python.exe',
)
: path.join(__dirname, "..", "audio-service", "venv", "bin", "python");
: path.join(__dirname, '..', 'audio-service', 'venv', 'bin', 'python');
const scriptPath = path.join(
__dirname,
"..",
"..",
"audio-service",
"src",
"main.py"
'..',
'..',
'audio-service',
'src',
'main.py',
);
// Load settings to pass as arguments
@ -56,75 +55,81 @@ async function createPythonService() {
const args = [
scriptPath,
'--recording-length', settings.recordingLength.toString(),
'--save-path', path.join(settings.outputFolder, "original"),
'--osc-port', settings.oscPort.toString() // Or make this configurable
'--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);
args.push(
'--input-device',
devices.find((device) => device.id === settings.inputDevice)?.name,
);
}
console.log(args)
console.log(args);
audioServiceProcess = spawn(pythonPath, args, {
detached: false,
stdio: "pipe",
stdio: 'pipe',
});
audioServiceProcess.stdout.on("data", (data) => {
audioServiceProcess.stdout.on('data', (data) => {
console.log(`Audio Service: ${data}`);
});
audioServiceProcess.stderr.on("data", (data) => {
audioServiceProcess.stderr.on('data', (data) => {
console.error(`Audio Service Error: ${data}`);
});
audioServiceProcess.on("close", (code) => {
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
tray = new Tray(path.join(__dirname, 'assets', 'tray-icon.png')); // You'll need to create this icon
const contextMenu = Menu.buildFromTemplate([
{
label: "Show",
label: 'Show',
click: () => {
mainWindow.show();
},
},
{
label: "Quit",
label: 'Quit',
click: () => {
// Properly terminate the Python service
stopService();
app.quit();
},
},
]);
tray.setToolTip("Audio Trimmer");
tray.setToolTip('Audio Trimmer');
tray.setContextMenu(contextMenu);
}
async function checkNewWavFile(filePath) {
// Only process .wav files
if (path.extname(filePath).toLowerCase() === ".wav") {
// 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);
mainWindow.webContents.send('new-untrimmed-file', filePath);
}
} catch (error) {
console.error("Error adding untrimmed file:", error);
console.error('Error adding untrimmed file:', error);
}
}
}
@ -132,13 +137,13 @@ if (path.extname(filePath).toLowerCase() === ".wav") {
function stopService() {
if (audioServiceProcess) {
try {
if (process.platform === "win32") {
spawn("taskkill", ["/pid", audioServiceProcess.pid, "/f", "/t"]);
if (process.platform === 'win32') {
spawn('taskkill', ['/pid', audioServiceProcess.pid, '/f', '/t']);
} else {
audioServiceProcess.kill("SIGTERM");
audioServiceProcess.kill('SIGTERM');
}
} catch (error) {
console.error("Error killing audio service:", error);
console.error('Error killing audio service:', error);
}
}
}
@ -152,27 +157,27 @@ function restartService() {
}
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,
};
}
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(`
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 => ({
@ -180,12 +185,12 @@ async function listAudioDevices() {
name: device.label || 'Unknown Microphone'
})))
`);
return devices;
} catch (error) {
console.error("Error getting input devices:", error);
return [];
}
return devices;
} catch (error) {
console.error('Error getting input devices:', error);
return [];
}
}
async function createWindow() {
// Initialize metadata
@ -197,7 +202,7 @@ async function createWindow() {
autoHideMenuBar: true,
titleBarStyle: 'hidden',
frame: false,
// titleBarOverlay: {
// color: '#1e1e1e',
// symbolColor: '#ffffff',
@ -209,23 +214,27 @@ async function createWindow() {
// 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
} } : {})
backgroundColor: '#1e1e1e',
...(process.platform !== 'darwin'
? {
titleBarOverlay: {
color: '#262626',
symbolColor: '#ffffff',
height: 30,
},
}
: {}),
});
mainWindow.loadFile("src/index.html");
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");
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);
console.error('Error creating recordings directory:', error);
}
// Watch for new WAV files
@ -245,24 +254,24 @@ async function createWindow() {
});
});
watcher.on("add", async (filePath) => {
watcher.on('add', async (filePath) => {
await checkNewWavFile(filePath);
});
ipcMain.handle("get-collections", () => {
ipcMain.handle('get-collections', () => {
return metadataManager.getCollections();
});
ipcMain.handle("get-collection-files", (event, collectionPath) => {
ipcMain.handle('get-collection-files', (event, collectionPath) => {
return metadataManager.getFilesInCollection(collectionPath);
});
ipcMain.handle("add-untrimmed-file", (event, filePath) => {
ipcMain.handle('add-untrimmed-file', (event, filePath) => {
return metadataManager.addUntrimmedFile(filePath);
});
ipcMain.handle(
"save-trimmed-file",
'save-trimmed-file',
(event, fileName, previousPath, savePath, trimStart, trimEnd, title) => {
return metadataManager.saveTrimmedFile(
fileName,
@ -270,29 +279,23 @@ async function createWindow() {
savePath,
trimStart,
trimEnd,
title
title,
);
}
},
);
ipcMain.handle(
"restart",
(event) => {
restartService();
}
);
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(
"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",
'save-trimmed-audio',
async (
event,
{
@ -302,7 +305,7 @@ async function createWindow() {
title,
trimStart,
trimEnd,
}
},
) => {
try {
// Ensure the collection folder exists
@ -314,7 +317,7 @@ async function createWindow() {
// Read the original WAV file
const originalWaveFile = new wavefile.WaveFile(
await fs.readFile(originalFilePath)
await fs.readFile(originalFilePath),
);
// Calculate trim points in samples
@ -353,7 +356,7 @@ async function createWindow() {
originalWaveFile.fmt.numChannels,
sampleRate,
bitDepth, // Always use 32-bit float
trimmedSamples
trimmedSamples,
);
// Write the trimmed WAV file
@ -364,84 +367,84 @@ async function createWindow() {
filePath: outputFilePath,
};
} catch (error) {
console.error("Error saving trimmed audio:", error);
console.error('Error saving trimmed audio:', error);
return {
success: false,
error: error.message,
};
}
}
},
);
ipcMain.handle("delete-file", async (event, filePath) => {
ipcMain.handle('delete-file', async (event, filePath) => {
try {
const settings = await loadSettings();
const settings = await loadSettings();
return metadataManager.deletefile(filePath, settings.outputFolder);
} catch (error) {
console.error("Error Deleting file:", error);
console.error('Error Deleting file:', error);
throw error;
}
});
ipcMain.handle("add-new-collection", (event, collectionName) => {
ipcMain.handle('add-new-collection', (event, collectionName) => {
try {
return metadataManager.addNewCollection(collectionName);
} catch (error) {
console.error("Error adding collection:", error);
console.error('Error adding collection:', error);
throw error;
}
});
ipcMain.handle("get-trim-info", (event, collectionName, filePath) => {
ipcMain.handle('get-trim-info', (event, collectionName, filePath) => {
return metadataManager.getTrimInfo(collectionName, filePath);
});
ipcMain.handle(
"set-trim-info",
'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) => {
ipcMain.handle('select-output-folder', async (event) => {
const result = await dialog.showOpenDialog({
properties: ["openDirectory"],
properties: ['openDirectory'],
});
return result.filePaths[0] || "";
return result.filePaths[0] || '';
});
ipcMain.handle("get-default-settings", () => {
ipcMain.handle('get-default-settings', () => {
return {
recordingLength: 30,
outputFolder: path.join(os.homedir(), "AudioTrimmer"),
outputFolder: path.join(os.homedir(), 'AudioTrimmer'),
inputDevice: null,
};
});
ipcMain.handle("save-settings", async (event, settings) => {
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");
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);
console.error('Error saving settings:', error);
return false;
}
});
ipcMain.handle("load-settings", async () => {
ipcMain.handle('load-settings', async () => {
return loadSettings();
});
ipcMain.handle("get-input-devices", async () => {
ipcMain.handle('get-input-devices', async () => {
return await listAudioDevices();
});
// Minimize to tray instead of closing
mainWindow.on("close", (event) => {
mainWindow.on('close', (event) => {
event.preventDefault();
mainWindow.hide();
});
@ -455,25 +458,25 @@ async function createWindow() {
app.disableHardwareAcceleration();
app.whenReady().then(createWindow);
app.on("window-all-closed", () => {
app.on('window-all-closed', () => {
// Do nothing - we handle closing via tray
});
// Ensure Python service is killed when app quits
app.on("before-quit", () => {
app.on('before-quit', () => {
if (audioServiceProcess) {
try {
if (process.platform === "win32") {
spawn("taskkill", ["/pid", audioServiceProcess.pid, "/f", "/t"]);
if (process.platform === 'win32') {
spawn('taskkill', ['/pid', audioServiceProcess.pid, '/f', '/t']);
} else {
audioServiceProcess.kill("SIGTERM");
audioServiceProcess.kill('SIGTERM');
}
} catch (error) {
console.error("Error killing audio service:", error);
console.error('Error killing audio service:', error);
}
}
});
app.on("activate", () => {
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}

View File

@ -1,41 +1,41 @@
const { ipcRenderer } = require("electron");
const path = require("path");
const WaveSurfer = require("wavesurfer.js");
const RegionsPlugin = require("wavesurfer.js/dist/plugin/wavesurfer.regions.js");
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 () => {
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");
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 () => {
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");
const settings = await ipcRenderer.invoke('load-settings');
// Populate input devices
const devices = await ipcRenderer.invoke("get-input-devices");
const devices = await ipcRenderer.invoke('get-input-devices');
if (devices.length === 0) {
inputDeviceSelect.innerHTML = "<option>No microphones found</option>";
inputDeviceSelect.innerHTML = '<option>No microphones found</option>';
} else {
inputDeviceSelect.innerHTML = devices
.map(
(device) => `<option value="${device.id}">${device.name}</option>`
(device) => `<option value="${device.id}">${device.name}</option>`,
)
.join("");
.join('');
}
// Set current settings
@ -44,37 +44,37 @@ document.addEventListener("DOMContentLoaded", async () => {
inputDeviceSelect.value = settings.inputDevice;
oscPortInput.value = settings.oscPort;
settingsModal.style.display = "block";
settingsModal.style.display = 'block';
} catch (error) {
console.error("Error loading settings or devices:", error);
alert("Please grant microphone permissions to list audio devices");
console.error('Error loading settings or devices:', error);
alert('Please grant microphone permissions to list audio devices');
}
});
restartBtn.addEventListener("click", async () => {
restartBtn.addEventListener('click', async () => {
try {
await ipcRenderer.invoke("restart");
await ipcRenderer.invoke('restart');
} catch (error) {
console.error("Error restarting:", error);
alert("Failed to restart Clipper");
console.error('Error restarting:', error);
alert('Failed to restart Clipper');
}
});
// Close settings modal
closeModalBtn.addEventListener("click", () => {
settingsModal.style.display = "none";
closeModalBtn.addEventListener('click', () => {
settingsModal.style.display = 'none';
});
// Select output folder
selectOutputFolderBtn.addEventListener("click", async () => {
const folderPath = await ipcRenderer.invoke("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 () => {
saveSettingsBtn.addEventListener('click', async () => {
const settings = {
recordingLength: parseInt(recordingLengthInput.value),
oscPort: parseInt(oscPortInput.value),
@ -82,68 +82,68 @@ document.addEventListener("DOMContentLoaded", async () => {
inputDevice: inputDeviceSelect.value,
};
const saved = await ipcRenderer.invoke("save-settings", settings);
const saved = await ipcRenderer.invoke('save-settings', settings);
if (saved) {
settingsModal.style.display = "none";
settingsModal.style.display = 'none';
} else {
alert("Failed to save settings");
alert('Failed to save settings');
}
});
// Close modal if clicked outside
window.addEventListener("click", (event) => {
window.addEventListener('click', (event) => {
if (event.target === settingsModal) {
settingsModal.style.display = "none";
settingsModal.style.display = 'none';
}
});
const audioTrimmersList = document.getElementById("audio-trimmers-list");
const collectionsList = document.getElementById("collections-list");
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",
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")}`;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
// Populate collections list
async function populateCollectionsList() {
const collections = await ipcRenderer.invoke("get-collections");
const collections = await ipcRenderer.invoke('get-collections');
collectionsList.innerHTML = "";
collectionsList.innerHTML = '';
// Always add Untrimmed section first
const untrimmedItem = document.createElement("div");
untrimmedItem.classList.add("collection-item");
untrimmedItem.textContent = "Untrimmed";
untrimmedItem.dataset.collection = "untrimmed";
const untrimmedItem = document.createElement('div');
untrimmedItem.classList.add('collection-item');
untrimmedItem.textContent = 'Untrimmed';
untrimmedItem.dataset.collection = 'untrimmed';
untrimmedItem.addEventListener("click", () => {
loadCollectionFiles("untrimmed");
untrimmedItem.addEventListener('click', () => {
loadCollectionFiles('untrimmed');
});
collectionsList.appendChild(untrimmedItem);
// Add other collections
collections.forEach((collection) => {
if (collection === "untrimmed") {
if (collection === 'untrimmed') {
return;
}
const collectionItem = document.createElement("div");
collectionItem.classList.add("collection-item");
const collectionItem = document.createElement('div');
collectionItem.classList.add('collection-item');
collectionItem.textContent = collection;
collectionItem.dataset.collection = collection;
collectionItem.addEventListener("click", () => {
collectionItem.addEventListener('click', () => {
loadCollectionFiles(collection);
});
@ -169,18 +169,18 @@ document.addEventListener("DOMContentLoaded", async () => {
}
// Reset active states
document.querySelectorAll(".collection-item").forEach((el) => {
el.classList.remove("active");
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}"]`
`.collection-item[data-collection="${collection}"]`,
);
// Only add active class if the item exists
if (activeItem) {
activeItem.classList.add("active");
activeItem.classList.add('active');
}
// Update section title and global state
@ -188,7 +188,7 @@ document.addEventListener("DOMContentLoaded", async () => {
globalState.currentSection = collection;
// Load files
const files = await ipcRenderer.invoke("get-collection-files", collection);
const files = await ipcRenderer.invoke('get-collection-files', collection);
// Add new trimmers with saved trim information
for (const file of files) {
@ -217,73 +217,73 @@ document.addEventListener("DOMContentLoaded", async () => {
}
const savedTrimInfo = await ipcRenderer.invoke(
"get-trim-info",
'get-trim-info',
globalState.currentSection,
path.basename(filePath)
path.basename(filePath),
);
// Create trimmer container
const trimmerContainer = document.createElement("div");
trimmerContainer.classList.add("audio-trimmer-item");
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");
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");
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");
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");
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");
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";
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");
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");
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");
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");
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"/>
@ -300,11 +300,11 @@ document.addEventListener("DOMContentLoaded", async () => {
trimmerContainer.appendChild(trimmerHeader);
// Waveform container
const waveformContainer = document.createElement("div");
waveformContainer.classList.add("waveform-container");
const waveformContainer = document.createElement('div');
waveformContainer.classList.add('waveform-container');
const waveformId = `waveform-${path.basename(
filePath,
path.extname(filePath)
path.extname(filePath),
)}`;
waveformContainer.innerHTML = `
<div id="${waveformId}" class="waveform"></div>
@ -312,8 +312,8 @@ document.addEventListener("DOMContentLoaded", async () => {
trimmerContainer.appendChild(waveformContainer);
// Time displays
const timeInfo = document.createElement("div");
timeInfo.classList.add("trim-info");
const timeInfo = document.createElement('div');
timeInfo.classList.add('trim-info');
timeInfo.innerHTML = `
<div class="trim-time">
<span>Start: </span>
@ -324,59 +324,58 @@ document.addEventListener("DOMContentLoaded", async () => {
<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 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');
// 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);
// };
// // 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;
// zoomInBtn.addEventListener('click', () => {
// const currentZoom = parseInt(zoomSlider.value);
// zoomSlider.value = Math.min(currentZoom + 20, 200);
// updateZoom(zoomSlider.value);
// });
// // Calculate the center point of the current view
// //const centerTime = wavesurfer.getCurrentTime();
// zoomOutBtn.addEventListener('click', () => {
// const currentZoom = parseInt(zoomSlider.value);
// zoomSlider.value = Math.max(currentZoom - 20, 1);
// updateZoom(zoomSlider.value);
// });
// // Apply zoom
// wavesurfer.zoom(zoomLevel);
// zoomSlider.addEventListener('input', (e) => {
// updateZoom(e.target.value);
// });
// // 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);
@ -386,25 +385,25 @@ document.addEventListener("DOMContentLoaded", async () => {
// Determine the file to load (original or current)
const fileToLoad =
section === "untrimmed"
section === 'untrimmed'
? filePath
: globalState.trimmerStates[filePath]?.originalPath || filePath;
// Setup wavesurfer
const wavesurfer = WaveSurfer.create({
container: `#${waveformId}`,
waveColor: "#ccb1ff",
progressColor: "#6e44ba",
waveColor: '#ccb1ff',
progressColor: '#6e44ba',
responsive: true,
height: 100,
hideScrollbar: true,
hideScrollbar: true,
// barWidth: 2,
// barRadius: 3,
cursorWidth: 1,
backend: "WebAudio",
backend: 'WebAudio',
plugins: [
RegionsPlugin.create({
color: "rgba(132, 81, 224, 0.3)",
color: 'rgba(132, 81, 224, 0.3)',
drag: false,
resize: true,
dragSelection: {
@ -420,7 +419,6 @@ document.addEventListener("DOMContentLoaded", async () => {
],
});
// Store wavesurfer instance in global state
globalState.wavesurferInstances[filePath] = wavesurfer;
@ -433,8 +431,8 @@ document.addEventListener("DOMContentLoaded", async () => {
regionEnd: undefined,
originalPath: fileToLoad,
};
const startTimeDisplay = timeInfo.querySelector(".trim-start-time");
const endTimeDisplay = timeInfo.querySelector(".trim-end-time");
const startTimeDisplay = timeInfo.querySelector('.trim-start-time');
const endTimeDisplay = timeInfo.querySelector('.trim-end-time');
// Load audio file
wavesurfer.load(`file://${fileToLoad}`);
@ -461,20 +459,20 @@ document.addEventListener("DOMContentLoaded", async () => {
};
// When audio is ready
wavesurfer.on("ready", async () => {
wavesurfer.on('ready', async () => {
const instanceState = globalState.trimmerStates[filePath];
// Set trim times based on saved state or full duration
if(instanceState.trimStart){
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,
});
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();
@ -483,19 +481,17 @@ document.addEventListener("DOMContentLoaded", async () => {
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) => {
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
updatedRegion.end,
);
// Update time displays
@ -516,7 +512,7 @@ document.addEventListener("DOMContentLoaded", async () => {
});
// Handle region creation
wavesurfer.on("region-created", (newRegion) => {
wavesurfer.on('region-created', (newRegion) => {
// Remove all other regions
Object.keys(wavesurfer.regions.list).forEach((id) => {
if (wavesurfer.regions.list[id] !== newRegion) {
@ -526,7 +522,7 @@ document.addEventListener("DOMContentLoaded", async () => {
});
// Reset to trim start when audio finishes
wavesurfer.on("finish", () => {
wavesurfer.on('finish', () => {
wavesurfer.setCurrentTime(instanceState.trimStart);
playPauseBtn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
@ -536,10 +532,10 @@ document.addEventListener("DOMContentLoaded", async () => {
});
// Save trimmed audio functionality
saveTrimButton.addEventListener("click", async () => {
saveTrimButton.addEventListener('click', async () => {
try {
// Get current collections
const collections = await ipcRenderer.invoke("get-collections");
const collections = await ipcRenderer.invoke('get-collections');
// Create a dialog to select or create a collection
const dialogHtml = `
@ -561,13 +557,13 @@ document.addEventListener("DOMContentLoaded", async () => {
<select id="existing-collections" style="width: 100%; margin-top: 10px;">
${collections
.map((col) =>
col === "untrimmed"
? ""
col === 'untrimmed'
? ''
: `<option value="${col}" ${
globalState.currentSection === col ? "selected" : ""
}>${col}</option>`
globalState.currentSection === col ? 'selected' : ''
}>${col}</option>`,
)
.join("")}
.join('')}
</select>
<div style="margin-top: 10px; display: flex; justify-content: space-between;">
@ -578,69 +574,67 @@ document.addEventListener("DOMContentLoaded", async () => {
`;
// 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";
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"
'#existing-collections',
);
const newSaveTitleInput = overlay.querySelector("#new-save-title");
const newSaveTitleInput = overlay.querySelector('#new-save-title');
const createCollectionBtn = overlay.querySelector(
"#create-collection-btn"
'#create-collection-btn',
);
const saveToCollectionBtn = overlay.querySelector(
"#save-to-collection-btn"
'#save-to-collection-btn',
);
const cancelSaveBtn = overlay.querySelector("#cancel-save-btn");
const cancelSaveBtn = overlay.querySelector('#cancel-save-btn');
if (savedTrimInfo.title) {
newSaveTitleInput.value = savedTrimInfo.title;
}
// Save to collection
saveToCollectionBtn.addEventListener("click", async () => {
saveToCollectionBtn.addEventListener('click', async () => {
const newTitle = document
.getElementById("new-save-title")
.getElementById('new-save-title')
.value.trim();
const settings = await ipcRenderer.invoke("load-settings");
const settings = await ipcRenderer.invoke('load-settings');
const selectedCollection = existingCollectionsSelect.value;
if (!selectedCollection) {
alert("Please select or create a collection");
alert('Please select or create a collection');
return;
}
try {
await ipcRenderer.invoke(
"delete-old-file",
'delete-old-file',
settings.outputFolder,
globalState.currentSection,
savedTrimInfo.title
savedTrimInfo.title,
);
await ipcRenderer.invoke(
"save-trimmed-file",
'save-trimmed-file',
path.basename(filePath),
globalState.currentSection,
selectedCollection,
instanceState.trimStart,
instanceState.trimEnd,
newTitle
newTitle,
);
const saveResult = await ipcRenderer.invoke(
"save-trimmed-audio",
'save-trimmed-audio',
{
originalFilePath: filePath,
outputFolder: settings.outputFolder,
@ -648,7 +642,7 @@ document.addEventListener("DOMContentLoaded", async () => {
title: newTitle,
trimStart: instanceState.trimStart,
trimEnd: instanceState.trimEnd,
}
},
);
if (saveResult.success) {
@ -667,39 +661,40 @@ document.addEventListener("DOMContentLoaded", async () => {
// Refresh the view
} catch (error) {
alert("Error saving file: " + error.message);
alert('Error saving file: ' + error.message);
}
});
// Cancel button
cancelSaveBtn.addEventListener("click", () => {
cancelSaveBtn.addEventListener('click', () => {
document.body.removeChild(overlay);
});
} catch (error) {
console.error("Error creating save dialog:", error);
console.error('Error creating save dialog:', error);
}
});
deletebutton.addEventListener("click", async () => {
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.`);
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);
await ipcRenderer.invoke('delete-file', filePath);
// Remove from UI
trimmerContainer.remove();
// Optional: Notify user
alert("File deleted successfully");
alert('File deleted successfully');
// Refresh the current section view
await loadCollectionFiles(globalState.currentSection);
await populateCollectionsList();
} catch (error) {
console.error("Error deleting file:", error);
console.error('Error deleting file:', error);
}
}
});
@ -709,13 +704,13 @@ document.addEventListener("DOMContentLoaded", async () => {
}
// Initial load of untrimmed files and collections
await loadCollectionFiles("untrimmed");
await loadCollectionFiles('untrimmed');
await populateCollectionsList();
// Listen for new untrimmed files
ipcRenderer.on("new-untrimmed-file", async (event, filePath) => {
ipcRenderer.on('new-untrimmed-file', async (event, filePath) => {
// Refresh the untrimmed section
await loadCollectionFiles("untrimmed");
await loadCollectionFiles('untrimmed');
await populateCollectionsList();
});
@ -728,8 +723,8 @@ document.addEventListener("DOMContentLoaded", async () => {
// Add collection button handler
document
.getElementById("add-collection-btn")
.addEventListener("click", async () => {
.getElementById('add-collection-btn')
.addEventListener('click', async () => {
try {
// Create a dialog to input new collection name
const dialogHtml = `
@ -760,32 +755,32 @@ document
`;
// 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";
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 newCollectionInput = overlay.querySelector('#new-collection-input');
const createCollectionConfirmBtn = overlay.querySelector(
"#create-collection-confirm-btn"
'#create-collection-confirm-btn',
);
const createCollectionCancelBtn = overlay.querySelector(
"#create-collection-cancel-btn"
'#create-collection-cancel-btn',
);
// Create collection when confirm button is clicked
createCollectionConfirmBtn.addEventListener("click", async () => {
createCollectionConfirmBtn.addEventListener('click', async () => {
const newCollectionName = newCollectionInput.value.trim();
if (newCollectionName) {
try {
await ipcRenderer.invoke("add-new-collection", newCollectionName);
await ipcRenderer.invoke('add-new-collection', newCollectionName);
// Remove dialog
document.body.removeChild(overlay);
@ -794,30 +789,30 @@ document
await populateCollectionsList();
} catch (error) {
// Show error in the dialog
const errorDiv = document.createElement("div");
const errorDiv = document.createElement('div');
errorDiv.textContent = error.message;
errorDiv.style.color = "red";
errorDiv.style.marginTop = "10px";
overlay.querySelector("div").appendChild(errorDiv);
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);
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", () => {
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);
console.error('Error creating new collection dialog:', error);
}
});

View File

@ -0,0 +1,62 @@
/*
* @NOTE: Prepend a `~` to css file paths that are in your node_modules
* See https://github.com/webpack-contrib/sass-loader#imports
*/
body {
position: relative;
color: white;
height: 100vh;
background: linear-gradient(
200.96deg,
#fedc2a -29.09%,
#dd5789 51.77%,
#7a2c9e 129.35%
);
font-family: sans-serif;
overflow-y: hidden;
display: flex;
justify-content: center;
align-items: center;
}
button {
background-color: white;
padding: 10px 20px;
border-radius: 10px;
border: none;
appearance: none;
font-size: 1.3rem;
box-shadow: 0px 8px 28px -6px rgba(24, 39, 75, 0.12),
0px 18px 88px -4px rgba(24, 39, 75, 0.14);
transition: all ease-in 0.1s;
cursor: pointer;
opacity: 0.9;
}
button:hover {
transform: scale(1.05);
opacity: 1;
}
li {
list-style: none;
}
a {
text-decoration: none;
height: fit-content;
width: fit-content;
margin: 10px;
}
a:hover {
opacity: 1;
text-decoration: none;
}
.Hello {
display: flex;
justify-content: center;
align-items: center;
margin: 20px 0;
}

View File

@ -0,0 +1,57 @@
import { MemoryRouter as Router, Routes, Route } from 'react-router-dom';
import icon from '../../assets/icon.svg';
import './App.css';
import AudioTrimmer from './components/AudioTrimer';
function Hello() {
return (
<div>
{/* <div className="Hello">
<img width="200" alt="icon" src={icon} />
</div>
<h1>electron-react-boilerplate</h1>
<div className="Hello">
<a
href="https://electron-react-boilerplate.js.org/"
target="_blank"
rel="noreferrer"
>
<button type="button">
<span role="img" aria-label="books">
📚
</span>
Read our docs
</button>
</a>
<a
href="https://github.com/sponsors/electron-react-boilerplate"
target="_blank"
rel="noreferrer"
>
<button type="button">
<span role="img" aria-label="folded hands">
🙏
</span>
Donate
</button>
</a>
</div> */}
<div>
<AudioTrimmer
filePath="C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250118_000351.wav"
// section="Section 1"
/>
</div>
</div>
);
}
export default function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Hello />} />
</Routes>
</Router>
);
}

View File

@ -0,0 +1,235 @@
import React, { useEffect, useMemo, useState } from 'react';
// import WaveSurfer from 'wavesurfer.js';
import Regions from 'wavesurfer.js/dist/plugins/regions.esm.js';
// import { useWavesurfer } from '@wavesurfer/react';
import { BlockList } from 'net';
import WavesurferPlayer from '@wavesurfer/react';
// import { IpcRenderer } from 'electron';
export interface AudioTrimmerProps {
filePath: string;
section: string;
title?: string;
trimStart?: number;
trimEnd?: number;
onSave?: (trimStart: number, trimEnd: number, title?: string) => void;
onDelete?: () => void;
}
function getBaseName(filePath: string) {
return filePath.split(/[\\/]/).pop() || filePath;
}
export default function AudioTrimmer({ filePath }: { filePath: string }) {
const [blobUrl, setBlobUrl] = useState<string | undefined>(undefined);
const [wavesurfer, setWavesurfer] = useState(null);
const [isPlaying, setIsPlaying] = useState(false);
const plugins = useMemo(() => {
return [Regions.create()];
}, []);
useEffect(() => {
let url: string | null = null;
async function fetchAudio() {
// console.log('Loading audio buffer for file:', filePath);
const buffer =
await window.electron.ipcRenderer.loadAudioBuffer(filePath);
if (buffer && !buffer.error) {
const audioData = buffer.data ? new Uint8Array(buffer.data) : buffer;
url = URL.createObjectURL(new Blob([audioData]));
setBlobUrl(url);
console.log('Audio blob URL created:', url);
}
}
fetchAudio();
return () => {
if (url) URL.revokeObjectURL(url);
};
}, [filePath]);
const onReady = (ws) => {
setWavesurfer(ws);
setIsPlaying(false);
// setDuration(ws.getDuration());
// console.log('Wavesurfer ready, duration:', ws.getDuration());
// console.log('Wavesurfer regions plugin:', ws.plugins[0]);
ws.plugins[0].addRegion?.({
start: 0,
end: ws.getDuration(),
color: 'rgba(132, 81, 224, 0.3)',
drag: false,
resize: true,
});
};
const onPlayPause = () => {
if (wavesurfer === null) return;
wavesurfer.playPause();
};
// useEffect(() => {
// if (!containerRef.current || !blobUrl) return;
// const ws = WaveSurfer.create({
// container: containerRef.current,
// waveColor: 'purple',
// url: blobUrl,
// height: 100,
// width: 600,
// });
// return () => ws.destroy();
// }, [blobUrl]);
return (
<div>
{/* <div ref={containerRef} /> */}
<WavesurferPlayer
height={100}
width={600}
url={blobUrl}
onReady={onReady}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
plugins={plugins}
/>
<button type="button" onClick={onPlayPause}>
{isPlaying ? 'Pause' : 'Play'}
</button>
</div>
);
}
// export default function AudioTrimmer({
// filePath,
// section,
// title,
// trimStart = 0,
// trimEnd,
// onSave,
// onDelete,
// }: AudioTrimmerProps) {
// const [wavesurfer, setWavesurfer] = useState(null);
// const waveformRef = useRef<HTMLDivElement>(null);
// const wavesurferRef = useRef<WaveSurfer | null>(null);
// const [region, setRegion] = useState<{ start: number; end: number }>({
// start: trimStart,
// end: trimEnd || 0,
// });
// // eslint-disable-next-line @typescript-eslint/no-unused-vars
// const [duration, setDuration] = useState<number>(0);
// useEffect(() => {
// if (!waveformRef.current) return;
// // const regions = Regions.create({
// // color: 'rgba(132, 81, 224, 0.3)',
// // drag: false,
// // resize: true,
// // });
// const regions = Regions.create();
// const ws = WaveSurfer.create({
// container: waveformRef.current,
// waveColor: '#ccb1ff',
// progressColor: '#6e44ba',
// // responsive: true,
// height: 100,
// hideScrollbar: true,
// backend: 'WebAudio',
// plugins: [regions],
// });
// wavesurferRef.current = ws;
// ws.load(`file://${filePath}`);
// ws.on('ready', () => {
// setDuration(ws.getDuration());
// regions.clearRegions();
// // ws.clearRegions();
// regions.addRegion({
// start: trimStart,
// end: trimEnd || ws.getDuration(),
// color: 'rgba(132, 81, 224, 0.3)',
// drag: false,
// resize: true,
// });
// });
// regions.on('region-updated', (updatedRegion: any) => {
// setRegion({
// start: Math.max(0, updatedRegion.start),
// end: Math.min(ws.getDuration(), updatedRegion.end),
// });
// });
// // eslint-disable-next-line consistent-return
// return () => {
// ws.destroy();
// };
// }, [filePath, trimStart, trimEnd]);
// const formatTime = (seconds: number) => {
// const minutes = Math.floor(seconds / 60);
// const remainingSeconds = Math.floor(seconds % 60);
// return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
// };
// return (
// <div className="audio-trimmer-item" data-filepath={filePath}>
// <div className="audio-trimmer-header">
// <div className="audio-trimmer-title-container">
// <div className="audio-trimmer-title">
// {title || getBaseName(filePath)}
// </div>
// <div className="audio-trimmer-filename">
// {title ? getBaseName(filePath) : 'hidden'}
// </div>
// <div className="audio-trimmer-section">{section}</div>
// </div>
// <div className="audio-trimmer-controls">
// <button
// type="button"
// className="play-pause-btn"
// onClick={() => {
// const ws = wavesurferRef.current;
// if (!ws) return;
// if (ws.isPlaying()) {
// ws.pause();
// } else {
// ws.play(region.start, region.end);
// }
// }}
// >
// ▶️
// </button>
// <button
// type="button"
// className="save-trim"
// onClick={() => onSave?.(region.start, region.end, title)}
// >
// 💾
// </button>
// <button type="button" className="delete-btn" onClick={onDelete}>
// 🗑️
// </button>
// </div>
// </div>
// <div className="waveform-container">
// <div ref={waveformRef} className="waveform" />
// </div>
// <div className="trim-info">
// <div className="trim-time">
// <span>Start: </span>
// <span className="trim-start-time">{formatTime(region.start)}</span>
// </div>
// <div className="trim-time">
// <span>End: </span>
// <span className="trim-end-time">{formatTime(region.end)}</span>
// </div>
// </div>
// </div>
// );
// }
// export default AudioTrimmer;

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline'"
/>
<title>Hello Electron React!</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,13 @@
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root') as HTMLElement;
const root = createRoot(container);
root.render(<App />);
// calling IPC exposed from preload script
window.electron?.ipcRenderer.once('ipc-example', (arg) => {
// eslint-disable-next-line no-console
console.log(arg);
});
window.electron?.ipcRenderer.sendMessage('ipc-example', ['ping']);

10
electron-ui/src/renderer/preload.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
import { ElectronHandler } from '../main/preload';
declare global {
// eslint-disable-next-line no-unused-vars
interface Window {
electron: ElectronHandler;
}
}
export {};

View File