start of react migration
This commit is contained in:
@ -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">×</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>
|
||||
149
electron-ui/src/main/main.ts
Normal file
149
electron-ui/src/main/main.ts
Normal 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);
|
||||
290
electron-ui/src/main/menu.ts
Normal file
290
electron-ui/src/main/menu.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
32
electron-ui/src/main/preload.ts
Normal file
32
electron-ui/src/main/preload.ts
Normal 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;
|
||||
13
electron-ui/src/main/util.ts
Normal file
13
electron-ui/src/main/util.ts
Normal 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)}`;
|
||||
}
|
||||
74
electron-ui/src/old/index.html
Normal file
74
electron-ui/src/old/index.html
Normal 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">×</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>
|
||||
@ -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();
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
62
electron-ui/src/renderer/App.css
Normal file
62
electron-ui/src/renderer/App.css
Normal 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;
|
||||
}
|
||||
57
electron-ui/src/renderer/App.tsx
Normal file
57
electron-ui/src/renderer/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
235
electron-ui/src/renderer/components/AudioTrimer.tsx
Normal file
235
electron-ui/src/renderer/components/AudioTrimer.tsx
Normal 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;
|
||||
14
electron-ui/src/renderer/index.ejs
Normal file
14
electron-ui/src/renderer/index.ejs
Normal 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>
|
||||
13
electron-ui/src/renderer/index.tsx
Normal file
13
electron-ui/src/renderer/index.tsx
Normal 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
10
electron-ui/src/renderer/preload.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
import { ElectronHandler } from '../main/preload';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
interface Window {
|
||||
electron: ElectronHandler;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
0
electron-ui/src/renderer/types/index.ts
Normal file
0
electron-ui/src/renderer/types/index.ts
Normal file
Reference in New Issue
Block a user