Compare commits
2 Commits
51ad065047
...
c292350b25
| Author | SHA1 | Date | |
|---|---|---|---|
| c292350b25 | |||
| 17bace5eaf |
12
electron-ui/.editorconfig
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
7
electron-ui/.erb/configs/.eslintrc
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"rules": {
|
||||||
|
"no-console": "off",
|
||||||
|
"global-require": "off",
|
||||||
|
"import/no-dynamic-require": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
electron-ui/.erb/configs/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const tailwindcss = require('@tailwindcss/postcss');
|
||||||
|
const autoprefixer = require('autoprefixer');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
plugins: [tailwindcss, autoprefixer],
|
||||||
|
};
|
||||||
54
electron-ui/.erb/configs/webpack.config.base.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* Base webpack config used across other specific configs
|
||||||
|
*/
|
||||||
|
|
||||||
|
import webpack from 'webpack';
|
||||||
|
import TsconfigPathsPlugins from 'tsconfig-paths-webpack-plugin';
|
||||||
|
import webpackPaths from './webpack.paths';
|
||||||
|
import { dependencies as externals } from '../../release/app/package.json';
|
||||||
|
|
||||||
|
const configuration: webpack.Configuration = {
|
||||||
|
externals: [...Object.keys(externals || {})],
|
||||||
|
|
||||||
|
stats: 'errors-only',
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.[jt]sx?$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: 'ts-loader',
|
||||||
|
options: {
|
||||||
|
// Remove this line to enable type checking in webpack builds
|
||||||
|
transpileOnly: true,
|
||||||
|
compilerOptions: {
|
||||||
|
module: 'nodenext',
|
||||||
|
moduleResolution: 'nodenext',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
path: webpackPaths.srcPath,
|
||||||
|
// https://github.com/webpack/webpack/issues/1114
|
||||||
|
library: { type: 'commonjs2' },
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the array of extensions that should be used to resolve modules.
|
||||||
|
*/
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
|
||||||
|
modules: [webpackPaths.srcPath, 'node_modules'],
|
||||||
|
// There is no need to add aliases here, the paths in tsconfig get mirrored
|
||||||
|
plugins: [new TsconfigPathsPlugins()],
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [new webpack.EnvironmentPlugin({ NODE_ENV: 'production' })],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default configuration;
|
||||||
3
electron-ui/.erb/configs/webpack.config.eslint.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/* eslint import/no-unresolved: off, import/no-self-import: off */
|
||||||
|
|
||||||
|
module.exports = require('./webpack.config.renderer.dev').default;
|
||||||
63
electron-ui/.erb/configs/webpack.config.main.dev.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* Webpack config for development electron main process
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import webpack from 'webpack';
|
||||||
|
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||||
|
import { merge } from 'webpack-merge';
|
||||||
|
import checkNodeEnv from '../scripts/check-node-env';
|
||||||
|
import baseConfig from './webpack.config.base';
|
||||||
|
import webpackPaths from './webpack.paths';
|
||||||
|
|
||||||
|
// When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
|
||||||
|
// at the dev webpack config is not accidentally run in a production environment
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
checkNodeEnv('development');
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuration: webpack.Configuration = {
|
||||||
|
devtool: 'inline-source-map',
|
||||||
|
|
||||||
|
mode: 'development',
|
||||||
|
|
||||||
|
target: 'electron-main',
|
||||||
|
|
||||||
|
entry: {
|
||||||
|
main: path.join(webpackPaths.srcMainPath, 'main.ts'),
|
||||||
|
preload: path.join(webpackPaths.srcMainPath, 'preload.ts'),
|
||||||
|
},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
path: webpackPaths.dllPath,
|
||||||
|
filename: '[name].bundle.dev.js',
|
||||||
|
library: {
|
||||||
|
type: 'umd',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
new BundleAnalyzerPlugin({
|
||||||
|
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
|
||||||
|
analyzerPort: 8888,
|
||||||
|
}),
|
||||||
|
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.type': '"browser"',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables webpack processing of __dirname and __filename.
|
||||||
|
* If you run the bundle in node.js it falls back to these values of node.js.
|
||||||
|
* https://github.com/webpack/webpack/issues/2010
|
||||||
|
*/
|
||||||
|
node: {
|
||||||
|
__dirname: false,
|
||||||
|
__filename: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default merge(baseConfig, configuration);
|
||||||
83
electron-ui/.erb/configs/webpack.config.main.prod.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Webpack config for production electron main process
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import webpack from 'webpack';
|
||||||
|
import { merge } from 'webpack-merge';
|
||||||
|
import TerserPlugin from 'terser-webpack-plugin';
|
||||||
|
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||||
|
import baseConfig from './webpack.config.base';
|
||||||
|
import webpackPaths from './webpack.paths';
|
||||||
|
import checkNodeEnv from '../scripts/check-node-env';
|
||||||
|
import deleteSourceMaps from '../scripts/delete-source-maps';
|
||||||
|
|
||||||
|
checkNodeEnv('production');
|
||||||
|
deleteSourceMaps();
|
||||||
|
|
||||||
|
const configuration: webpack.Configuration = {
|
||||||
|
devtool: 'source-map',
|
||||||
|
|
||||||
|
mode: 'production',
|
||||||
|
|
||||||
|
target: 'electron-main',
|
||||||
|
|
||||||
|
entry: {
|
||||||
|
main: path.join(webpackPaths.srcMainPath, 'main.ts'),
|
||||||
|
preload: path.join(webpackPaths.srcMainPath, 'preload.ts'),
|
||||||
|
},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
path: webpackPaths.distMainPath,
|
||||||
|
filename: '[name].js',
|
||||||
|
library: {
|
||||||
|
type: 'umd',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
optimization: {
|
||||||
|
minimizer: [
|
||||||
|
new TerserPlugin({
|
||||||
|
parallel: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
new BundleAnalyzerPlugin({
|
||||||
|
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
|
||||||
|
analyzerPort: 8888,
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create global constants which can be configured at compile time.
|
||||||
|
*
|
||||||
|
* Useful for allowing different behaviour between development builds and
|
||||||
|
* release builds
|
||||||
|
*
|
||||||
|
* NODE_ENV should be production so that modules do not perform certain
|
||||||
|
* development checks
|
||||||
|
*/
|
||||||
|
new webpack.EnvironmentPlugin({
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
DEBUG_PROD: false,
|
||||||
|
START_MINIMIZED: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.type': '"browser"',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables webpack processing of __dirname and __filename.
|
||||||
|
* If you run the bundle in node.js it falls back to these values of node.js.
|
||||||
|
* https://github.com/webpack/webpack/issues/2010
|
||||||
|
*/
|
||||||
|
node: {
|
||||||
|
__dirname: false,
|
||||||
|
__filename: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default merge(baseConfig, configuration);
|
||||||
71
electron-ui/.erb/configs/webpack.config.preload.dev.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import webpack from 'webpack';
|
||||||
|
import { merge } from 'webpack-merge';
|
||||||
|
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||||
|
import baseConfig from './webpack.config.base';
|
||||||
|
import webpackPaths from './webpack.paths';
|
||||||
|
import checkNodeEnv from '../scripts/check-node-env';
|
||||||
|
|
||||||
|
// When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
|
||||||
|
// at the dev webpack config is not accidentally run in a production environment
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
checkNodeEnv('development');
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuration: webpack.Configuration = {
|
||||||
|
devtool: 'inline-source-map',
|
||||||
|
|
||||||
|
mode: 'development',
|
||||||
|
|
||||||
|
target: 'electron-preload',
|
||||||
|
|
||||||
|
entry: path.join(webpackPaths.srcMainPath, 'preload.ts'),
|
||||||
|
|
||||||
|
output: {
|
||||||
|
path: webpackPaths.dllPath,
|
||||||
|
filename: 'preload.js',
|
||||||
|
library: {
|
||||||
|
type: 'umd',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
new BundleAnalyzerPlugin({
|
||||||
|
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create global constants which can be configured at compile time.
|
||||||
|
*
|
||||||
|
* Useful for allowing different behaviour between development builds and
|
||||||
|
* release builds
|
||||||
|
*
|
||||||
|
* NODE_ENV should be production so that modules do not perform certain
|
||||||
|
* development checks
|
||||||
|
*
|
||||||
|
* By default, use 'development' as NODE_ENV. This can be overriden with
|
||||||
|
* 'staging', for example, by changing the ENV variables in the npm scripts
|
||||||
|
*/
|
||||||
|
new webpack.EnvironmentPlugin({
|
||||||
|
NODE_ENV: 'development',
|
||||||
|
}),
|
||||||
|
|
||||||
|
new webpack.LoaderOptionsPlugin({
|
||||||
|
debug: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables webpack processing of __dirname and __filename.
|
||||||
|
* If you run the bundle in node.js it falls back to these values of node.js.
|
||||||
|
* https://github.com/webpack/webpack/issues/2010
|
||||||
|
*/
|
||||||
|
node: {
|
||||||
|
__dirname: false,
|
||||||
|
__filename: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default merge(baseConfig, configuration);
|
||||||
77
electron-ui/.erb/configs/webpack.config.renderer.dev.dll.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Builds the DLL for development electron renderer process
|
||||||
|
*/
|
||||||
|
|
||||||
|
import webpack from 'webpack';
|
||||||
|
import path from 'path';
|
||||||
|
import { merge } from 'webpack-merge';
|
||||||
|
import baseConfig from './webpack.config.base';
|
||||||
|
import webpackPaths from './webpack.paths';
|
||||||
|
import { dependencies } from '../../package.json';
|
||||||
|
import checkNodeEnv from '../scripts/check-node-env';
|
||||||
|
|
||||||
|
checkNodeEnv('development');
|
||||||
|
|
||||||
|
const dist = webpackPaths.dllPath;
|
||||||
|
|
||||||
|
const configuration: webpack.Configuration = {
|
||||||
|
context: webpackPaths.rootPath,
|
||||||
|
|
||||||
|
devtool: 'eval',
|
||||||
|
|
||||||
|
mode: 'development',
|
||||||
|
|
||||||
|
target: 'electron-renderer',
|
||||||
|
|
||||||
|
externals: ['fsevents', 'crypto-browserify'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use `module` from `webpack.config.renderer.dev.js`
|
||||||
|
*/
|
||||||
|
module: require('./webpack.config.renderer.dev').default.module,
|
||||||
|
|
||||||
|
entry: {
|
||||||
|
renderer: Object.keys(dependencies || {}),
|
||||||
|
},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
path: dist,
|
||||||
|
filename: '[name].dev.dll.js',
|
||||||
|
library: {
|
||||||
|
name: 'renderer',
|
||||||
|
type: 'var',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
new webpack.DllPlugin({
|
||||||
|
path: path.join(dist, '[name].json'),
|
||||||
|
name: '[name]',
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create global constants which can be configured at compile time.
|
||||||
|
*
|
||||||
|
* Useful for allowing different behaviour between development builds and
|
||||||
|
* release builds
|
||||||
|
*
|
||||||
|
* NODE_ENV should be production so that modules do not perform certain
|
||||||
|
* development checks
|
||||||
|
*/
|
||||||
|
new webpack.EnvironmentPlugin({
|
||||||
|
NODE_ENV: 'development',
|
||||||
|
}),
|
||||||
|
|
||||||
|
new webpack.LoaderOptionsPlugin({
|
||||||
|
debug: true,
|
||||||
|
options: {
|
||||||
|
context: webpackPaths.srcPath,
|
||||||
|
output: {
|
||||||
|
path: webpackPaths.dllPath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default merge(baseConfig, configuration);
|
||||||
236
electron-ui/.erb/configs/webpack.config.renderer.dev.ts
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
import 'webpack-dev-server';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import webpack from 'webpack';
|
||||||
|
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { merge } from 'webpack-merge';
|
||||||
|
import { execSync, spawn } from 'child_process';
|
||||||
|
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
|
||||||
|
import baseConfig from './webpack.config.base';
|
||||||
|
import webpackPaths from './webpack.paths';
|
||||||
|
import checkNodeEnv from '../scripts/check-node-env';
|
||||||
|
|
||||||
|
// When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
|
||||||
|
// at the dev webpack config is not accidentally run in a production environment
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
checkNodeEnv('development');
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = process.env.PORT || 1212;
|
||||||
|
const manifest = path.resolve(webpackPaths.dllPath, 'renderer.json');
|
||||||
|
const skipDLLs =
|
||||||
|
module.parent?.filename.includes('webpack.config.renderer.dev.dll') ||
|
||||||
|
module.parent?.filename.includes('webpack.config.eslint');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warn if the DLL is not built
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
!skipDLLs &&
|
||||||
|
!(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest))
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
chalk.black.bgYellow.bold(
|
||||||
|
'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
execSync('npm run postinstall');
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuration: webpack.Configuration = {
|
||||||
|
devtool: 'inline-source-map',
|
||||||
|
|
||||||
|
mode: 'development',
|
||||||
|
|
||||||
|
target: ['web', 'electron-renderer'],
|
||||||
|
|
||||||
|
entry: [
|
||||||
|
`webpack-dev-server/client?http://localhost:${port}/dist`,
|
||||||
|
'webpack/hot/only-dev-server',
|
||||||
|
path.join(webpackPaths.srcRendererPath, 'index.tsx'),
|
||||||
|
],
|
||||||
|
|
||||||
|
output: {
|
||||||
|
path: webpackPaths.distRendererPath,
|
||||||
|
publicPath: '/',
|
||||||
|
filename: 'renderer.dev.js',
|
||||||
|
library: {
|
||||||
|
type: 'umd',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.s?(c|a)ss$/,
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
modules: true,
|
||||||
|
sourceMap: true,
|
||||||
|
importLoaders: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'sass-loader',
|
||||||
|
{
|
||||||
|
loader: 'postcss-loader',
|
||||||
|
options: {
|
||||||
|
postcssOptions: {
|
||||||
|
plugins: [
|
||||||
|
require('@tailwindcss/postcss'),
|
||||||
|
require('autoprefixer'),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
include: /\.module\.s?(c|a)ss$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.s?css$/,
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
'css-loader',
|
||||||
|
'sass-loader',
|
||||||
|
{
|
||||||
|
loader: 'postcss-loader',
|
||||||
|
options: {
|
||||||
|
postcssOptions: {
|
||||||
|
plugins: [require('@tailwindcss/postcss')],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exclude: /\.module\.s?(c|a)ss$/,
|
||||||
|
},
|
||||||
|
// Fonts
|
||||||
|
{
|
||||||
|
test: /\.(woff|woff2|eot|ttf|otf)$/i,
|
||||||
|
type: 'asset/resource',
|
||||||
|
},
|
||||||
|
// Images
|
||||||
|
{
|
||||||
|
test: /\.(png|jpg|jpeg|gif)$/i,
|
||||||
|
type: 'asset/resource',
|
||||||
|
},
|
||||||
|
// SVG
|
||||||
|
{
|
||||||
|
test: /\.svg$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: '@svgr/webpack',
|
||||||
|
options: {
|
||||||
|
prettier: false,
|
||||||
|
svgo: false,
|
||||||
|
svgoConfig: {
|
||||||
|
plugins: [{ removeViewBox: false }],
|
||||||
|
},
|
||||||
|
titleProp: true,
|
||||||
|
ref: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'file-loader',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
...(skipDLLs
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
new webpack.DllReferencePlugin({
|
||||||
|
context: webpackPaths.dllPath,
|
||||||
|
manifest: require(manifest),
|
||||||
|
sourceType: 'var',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
|
||||||
|
new webpack.NoEmitOnErrorsPlugin(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create global constants which can be configured at compile time.
|
||||||
|
*
|
||||||
|
* Useful for allowing different behaviour between development builds and
|
||||||
|
* release builds
|
||||||
|
*
|
||||||
|
* NODE_ENV should be production so that modules do not perform certain
|
||||||
|
* development checks
|
||||||
|
*
|
||||||
|
* By default, use 'development' as NODE_ENV. This can be overriden with
|
||||||
|
* 'staging', for example, by changing the ENV variables in the npm scripts
|
||||||
|
*/
|
||||||
|
new webpack.EnvironmentPlugin({
|
||||||
|
NODE_ENV: 'development',
|
||||||
|
}),
|
||||||
|
|
||||||
|
new webpack.LoaderOptionsPlugin({
|
||||||
|
debug: true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
new ReactRefreshWebpackPlugin(),
|
||||||
|
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
filename: path.join('index.html'),
|
||||||
|
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
|
||||||
|
minify: {
|
||||||
|
collapseWhitespace: true,
|
||||||
|
removeAttributeQuotes: true,
|
||||||
|
removeComments: true,
|
||||||
|
},
|
||||||
|
isBrowser: false,
|
||||||
|
env: process.env.NODE_ENV,
|
||||||
|
isDevelopment: process.env.NODE_ENV !== 'production',
|
||||||
|
nodeModules: webpackPaths.appNodeModulesPath,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
node: {
|
||||||
|
__dirname: false,
|
||||||
|
__filename: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
devServer: {
|
||||||
|
port,
|
||||||
|
compress: true,
|
||||||
|
hot: true,
|
||||||
|
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||||
|
static: {
|
||||||
|
publicPath: '/',
|
||||||
|
},
|
||||||
|
historyApiFallback: {
|
||||||
|
verbose: true,
|
||||||
|
},
|
||||||
|
setupMiddlewares(middlewares) {
|
||||||
|
console.log('Starting preload.js builder...');
|
||||||
|
const preloadProcess = spawn('npm', ['run', 'start:preload'], {
|
||||||
|
shell: true,
|
||||||
|
stdio: 'inherit',
|
||||||
|
})
|
||||||
|
.on('close', (code: number) => process.exit(code!))
|
||||||
|
.on('error', (spawnError) => console.error(spawnError));
|
||||||
|
|
||||||
|
console.log('Starting Main Process...');
|
||||||
|
let args = ['run', 'start:main'];
|
||||||
|
if (process.env.MAIN_ARGS) {
|
||||||
|
args = args.concat(
|
||||||
|
['--', ...process.env.MAIN_ARGS.matchAll(/"[^"]+"|[^\s"]+/g)].flat(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
spawn('npm', args, {
|
||||||
|
shell: true,
|
||||||
|
stdio: 'inherit',
|
||||||
|
})
|
||||||
|
.on('close', (code: number) => {
|
||||||
|
preloadProcess.kill();
|
||||||
|
process.exit(code!);
|
||||||
|
})
|
||||||
|
.on('error', (spawnError) => console.error(spawnError));
|
||||||
|
return middlewares;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default merge(baseConfig, configuration);
|
||||||
161
electron-ui/.erb/configs/webpack.config.renderer.prod.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
/**
|
||||||
|
* Build config for electron renderer process
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import webpack from 'webpack';
|
||||||
|
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||||
|
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||||
|
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||||
|
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
|
||||||
|
import { merge } from 'webpack-merge';
|
||||||
|
import TerserPlugin from 'terser-webpack-plugin';
|
||||||
|
import baseConfig from './webpack.config.base';
|
||||||
|
import webpackPaths from './webpack.paths';
|
||||||
|
import checkNodeEnv from '../scripts/check-node-env';
|
||||||
|
import deleteSourceMaps from '../scripts/delete-source-maps';
|
||||||
|
|
||||||
|
checkNodeEnv('production');
|
||||||
|
deleteSourceMaps();
|
||||||
|
|
||||||
|
const configuration: webpack.Configuration = {
|
||||||
|
devtool: 'source-map',
|
||||||
|
|
||||||
|
mode: 'production',
|
||||||
|
|
||||||
|
target: ['web', 'electron-renderer'],
|
||||||
|
|
||||||
|
entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')],
|
||||||
|
|
||||||
|
output: {
|
||||||
|
path: webpackPaths.distRendererPath,
|
||||||
|
publicPath: './',
|
||||||
|
filename: 'renderer.js',
|
||||||
|
library: {
|
||||||
|
type: 'umd',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.s?(a|c)ss$/,
|
||||||
|
use: [
|
||||||
|
MiniCssExtractPlugin.loader,
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
modules: true,
|
||||||
|
sourceMap: true,
|
||||||
|
importLoaders: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'sass-loader',
|
||||||
|
{
|
||||||
|
loader: 'postcss-loader',
|
||||||
|
options: {
|
||||||
|
postcssOptions: {
|
||||||
|
plugins: [require('@tailwindcss/postcss')],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
include: /\.module\.s?(c|a)ss$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.s?(a|c)ss$/,
|
||||||
|
use: [
|
||||||
|
MiniCssExtractPlugin.loader,
|
||||||
|
'css-loader',
|
||||||
|
'sass-loader',
|
||||||
|
{
|
||||||
|
loader: 'postcss-loader',
|
||||||
|
options: {
|
||||||
|
postcssOptions: {
|
||||||
|
plugins: [require('@tailwindcss/postcss')],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exclude: /\.module\.s?(c|a)ss$/,
|
||||||
|
},
|
||||||
|
// Fonts
|
||||||
|
{
|
||||||
|
test: /\.(woff|woff2|eot|ttf|otf)$/i,
|
||||||
|
type: 'asset/resource',
|
||||||
|
},
|
||||||
|
// Images
|
||||||
|
{
|
||||||
|
test: /\.(png|jpg|jpeg|gif)$/i,
|
||||||
|
type: 'asset/resource',
|
||||||
|
},
|
||||||
|
// SVG
|
||||||
|
{
|
||||||
|
test: /\.svg$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: '@svgr/webpack',
|
||||||
|
options: {
|
||||||
|
prettier: false,
|
||||||
|
svgo: false,
|
||||||
|
svgoConfig: {
|
||||||
|
plugins: [{ removeViewBox: false }],
|
||||||
|
},
|
||||||
|
titleProp: true,
|
||||||
|
ref: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'file-loader',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
optimization: {
|
||||||
|
minimize: true,
|
||||||
|
minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
/**
|
||||||
|
* Create global constants which can be configured at compile time.
|
||||||
|
*
|
||||||
|
* Useful for allowing different behaviour between development builds and
|
||||||
|
* release builds
|
||||||
|
*
|
||||||
|
* NODE_ENV should be production so that modules do not perform certain
|
||||||
|
* development checks
|
||||||
|
*/
|
||||||
|
new webpack.EnvironmentPlugin({
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
DEBUG_PROD: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: 'style.css',
|
||||||
|
}),
|
||||||
|
|
||||||
|
new BundleAnalyzerPlugin({
|
||||||
|
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
|
||||||
|
analyzerPort: 8889,
|
||||||
|
}),
|
||||||
|
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
filename: 'index.html',
|
||||||
|
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
|
||||||
|
minify: {
|
||||||
|
collapseWhitespace: true,
|
||||||
|
removeAttributeQuotes: true,
|
||||||
|
removeComments: true,
|
||||||
|
},
|
||||||
|
isBrowser: false,
|
||||||
|
isDevelopment: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.type': '"renderer"',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default merge(baseConfig, configuration);
|
||||||
42
electron-ui/.erb/configs/webpack.paths.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const rootPath = path.join(__dirname, '../..');
|
||||||
|
|
||||||
|
const erbPath = path.join(__dirname, '..');
|
||||||
|
const erbNodeModulesPath = path.join(erbPath, 'node_modules');
|
||||||
|
|
||||||
|
const dllPath = path.join(__dirname, '../dll');
|
||||||
|
|
||||||
|
const srcPath = path.join(rootPath, 'src');
|
||||||
|
const srcMainPath = path.join(srcPath, 'main');
|
||||||
|
const srcRendererPath = path.join(srcPath, 'renderer');
|
||||||
|
|
||||||
|
const releasePath = path.join(rootPath, 'release');
|
||||||
|
const appPath = path.join(releasePath, 'app');
|
||||||
|
const appPackagePath = path.join(appPath, 'package.json');
|
||||||
|
const appNodeModulesPath = path.join(appPath, 'node_modules');
|
||||||
|
const srcNodeModulesPath = path.join(srcPath, 'node_modules');
|
||||||
|
|
||||||
|
const distPath = path.join(appPath, 'dist');
|
||||||
|
const distMainPath = path.join(distPath, 'main');
|
||||||
|
const distRendererPath = path.join(distPath, 'renderer');
|
||||||
|
|
||||||
|
const buildPath = path.join(releasePath, 'build');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
rootPath,
|
||||||
|
erbNodeModulesPath,
|
||||||
|
dllPath,
|
||||||
|
srcPath,
|
||||||
|
srcMainPath,
|
||||||
|
srcRendererPath,
|
||||||
|
releasePath,
|
||||||
|
appPath,
|
||||||
|
appPackagePath,
|
||||||
|
appNodeModulesPath,
|
||||||
|
srcNodeModulesPath,
|
||||||
|
distPath,
|
||||||
|
distMainPath,
|
||||||
|
distRendererPath,
|
||||||
|
buildPath,
|
||||||
|
};
|
||||||
32
electron-ui/.erb/img/erb-banner.svg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
electron-ui/.erb/img/erb-logo.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
6
electron-ui/.erb/img/palette-sponsor-banner.svg
Normal file
|
After Width: | Height: | Size: 33 KiB |
1
electron-ui/.erb/mocks/fileMock.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default 'test-file-stub';
|
||||||
8
electron-ui/.erb/scripts/.eslintrc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"rules": {
|
||||||
|
"no-console": "off",
|
||||||
|
"global-require": "off",
|
||||||
|
"import/no-dynamic-require": "off",
|
||||||
|
"import/no-extraneous-dependencies": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
electron-ui/.erb/scripts/check-build-exists.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Check if the renderer and main bundles are built
|
||||||
|
import path from 'path';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { TextEncoder, TextDecoder } from 'node:util';
|
||||||
|
import webpackPaths from '../configs/webpack.paths';
|
||||||
|
|
||||||
|
const mainPath = path.join(webpackPaths.distMainPath, 'main.js');
|
||||||
|
const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js');
|
||||||
|
|
||||||
|
if (!fs.existsSync(mainPath)) {
|
||||||
|
throw new Error(
|
||||||
|
chalk.whiteBright.bgRed.bold(
|
||||||
|
'The main process is not built yet. Build it by running "npm run build:main"',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(rendererPath)) {
|
||||||
|
throw new Error(
|
||||||
|
chalk.whiteBright.bgRed.bold(
|
||||||
|
'The renderer process is not built yet. Build it by running "npm run build:renderer"',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSDOM does not implement TextEncoder and TextDecoder
|
||||||
|
if (!global.TextEncoder) {
|
||||||
|
global.TextEncoder = TextEncoder;
|
||||||
|
}
|
||||||
|
if (!global.TextDecoder) {
|
||||||
|
// @ts-ignore
|
||||||
|
global.TextDecoder = TextDecoder;
|
||||||
|
}
|
||||||
54
electron-ui/.erb/scripts/check-native-dep.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { dependencies } from '../../package.json';
|
||||||
|
|
||||||
|
if (dependencies) {
|
||||||
|
const dependenciesKeys = Object.keys(dependencies);
|
||||||
|
const nativeDeps = fs
|
||||||
|
.readdirSync('node_modules')
|
||||||
|
.filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`));
|
||||||
|
if (nativeDeps.length === 0) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Find the reason for why the dependency is installed. If it is installed
|
||||||
|
// because of a devDependency then that is okay. Warn when it is installed
|
||||||
|
// because of a dependency
|
||||||
|
const { dependencies: dependenciesObject } = JSON.parse(
|
||||||
|
execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString(),
|
||||||
|
);
|
||||||
|
const rootDependencies = Object.keys(dependenciesObject);
|
||||||
|
const filteredRootDependencies = rootDependencies.filter((rootDependency) =>
|
||||||
|
dependenciesKeys.includes(rootDependency),
|
||||||
|
);
|
||||||
|
if (filteredRootDependencies.length > 0) {
|
||||||
|
const plural = filteredRootDependencies.length > 1;
|
||||||
|
console.log(`
|
||||||
|
${chalk.whiteBright.bgYellow.bold(
|
||||||
|
'Webpack does not work with native dependencies.',
|
||||||
|
)}
|
||||||
|
${chalk.bold(filteredRootDependencies.join(', '))} ${
|
||||||
|
plural ? 'are native dependencies' : 'is a native dependency'
|
||||||
|
} and should be installed inside of the "./release/app" folder.
|
||||||
|
First, uninstall the packages from "./package.json":
|
||||||
|
${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')}
|
||||||
|
${chalk.bold(
|
||||||
|
'Then, instead of installing the package to the root "./package.json":',
|
||||||
|
)}
|
||||||
|
${chalk.whiteBright.bgRed.bold('npm install your-package')}
|
||||||
|
${chalk.bold('Install the package to "./release/app/package.json"')}
|
||||||
|
${chalk.whiteBright.bgGreen.bold(
|
||||||
|
'cd ./release/app && npm install your-package',
|
||||||
|
)}
|
||||||
|
Read more about native dependencies at:
|
||||||
|
${chalk.bold(
|
||||||
|
'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure',
|
||||||
|
)}
|
||||||
|
`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.log('Native dependencies could not be checked');
|
||||||
|
}
|
||||||
|
}
|
||||||
16
electron-ui/.erb/scripts/check-node-env.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
export default function checkNodeEnv(expectedEnv) {
|
||||||
|
if (!expectedEnv) {
|
||||||
|
throw new Error('"expectedEnv" not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== expectedEnv) {
|
||||||
|
console.log(
|
||||||
|
chalk.whiteBright.bgRed.bold(
|
||||||
|
`"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
electron-ui/.erb/scripts/check-port-in-use.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import chalk from 'chalk';
|
||||||
|
import detectPort from 'detect-port';
|
||||||
|
|
||||||
|
const port = process.env.PORT || '1212';
|
||||||
|
|
||||||
|
detectPort(port, (_err, availablePort) => {
|
||||||
|
if (port !== String(availablePort)) {
|
||||||
|
throw new Error(
|
||||||
|
chalk.whiteBright.bgRed.bold(
|
||||||
|
`Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
13
electron-ui/.erb/scripts/clean.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { rimrafSync } from 'rimraf';
|
||||||
|
import fs from 'fs';
|
||||||
|
import webpackPaths from '../configs/webpack.paths';
|
||||||
|
|
||||||
|
const foldersToRemove = [
|
||||||
|
webpackPaths.distPath,
|
||||||
|
webpackPaths.buildPath,
|
||||||
|
webpackPaths.dllPath,
|
||||||
|
];
|
||||||
|
|
||||||
|
foldersToRemove.forEach((folder) => {
|
||||||
|
if (fs.existsSync(folder)) rimrafSync(folder);
|
||||||
|
});
|
||||||
15
electron-ui/.erb/scripts/delete-source-maps.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { rimrafSync } from 'rimraf';
|
||||||
|
import webpackPaths from '../configs/webpack.paths';
|
||||||
|
|
||||||
|
export default function deleteSourceMaps() {
|
||||||
|
if (fs.existsSync(webpackPaths.distMainPath))
|
||||||
|
rimrafSync(path.join(webpackPaths.distMainPath, '*.js.map'), {
|
||||||
|
glob: true,
|
||||||
|
});
|
||||||
|
if (fs.existsSync(webpackPaths.distRendererPath))
|
||||||
|
rimrafSync(path.join(webpackPaths.distRendererPath, '*.js.map'), {
|
||||||
|
glob: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
20
electron-ui/.erb/scripts/electron-rebuild.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { execSync } from 'child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { dependencies } from '../../release/app/package.json';
|
||||||
|
import webpackPaths from '../configs/webpack.paths';
|
||||||
|
|
||||||
|
if (
|
||||||
|
Object.keys(dependencies || {}).length > 0 &&
|
||||||
|
fs.existsSync(webpackPaths.appNodeModulesPath)
|
||||||
|
) {
|
||||||
|
const electronRebuildCmd =
|
||||||
|
'../../node_modules/.bin/electron-rebuild --force --types prod,dev,optional --module-dir .';
|
||||||
|
const cmd =
|
||||||
|
process.platform === 'win32'
|
||||||
|
? electronRebuildCmd.replace(/\//g, '\\')
|
||||||
|
: electronRebuildCmd;
|
||||||
|
execSync(cmd, {
|
||||||
|
cwd: webpackPaths.appPath,
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
}
|
||||||
14
electron-ui/.erb/scripts/link-modules.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import webpackPaths from '../configs/webpack.paths';
|
||||||
|
|
||||||
|
const { srcNodeModulesPath, appNodeModulesPath, erbNodeModulesPath } =
|
||||||
|
webpackPaths;
|
||||||
|
|
||||||
|
if (fs.existsSync(appNodeModulesPath)) {
|
||||||
|
if (!fs.existsSync(srcNodeModulesPath)) {
|
||||||
|
fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction');
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(erbNodeModulesPath)) {
|
||||||
|
fs.symlinkSync(appNodeModulesPath, erbNodeModulesPath, 'junction');
|
||||||
|
}
|
||||||
|
}
|
||||||
38
electron-ui/.erb/scripts/notarize.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
const { notarize } = require('@electron/notarize');
|
||||||
|
const { build } = require('../../package.json');
|
||||||
|
|
||||||
|
exports.default = async function notarizeMacos(context) {
|
||||||
|
const { electronPlatformName, appOutDir } = context;
|
||||||
|
if (electronPlatformName !== 'darwin') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.CI !== 'true') {
|
||||||
|
console.warn('Skipping notarizing step. Packaging is not running in CI');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
'APPLE_ID' in process.env &&
|
||||||
|
'APPLE_ID_PASS' in process.env &&
|
||||||
|
'APPLE_TEAM_ID' in process.env
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
'Skipping notarizing step. APPLE_ID, APPLE_ID_PASS, and APPLE_TEAM_ID env variables must be set',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appName = context.packager.appInfo.productFilename;
|
||||||
|
|
||||||
|
await notarize({
|
||||||
|
tool: 'notarytool',
|
||||||
|
appBundleId: build.appId,
|
||||||
|
appPath: `${appOutDir}/${appName}.app`,
|
||||||
|
appleId: process.env.APPLE_ID,
|
||||||
|
appleIdPassword: process.env.APPLE_ID_PASS,
|
||||||
|
teamId: process.env.APPLE_TEAM_ID,
|
||||||
|
});
|
||||||
|
};
|
||||||
33
electron-ui/.eslintignore
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Dependency directory
|
||||||
|
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# OSX
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
release/app/dist
|
||||||
|
release/build
|
||||||
|
.erb/dll
|
||||||
|
|
||||||
|
.idea
|
||||||
|
npm-debug.log.*
|
||||||
|
*.css.d.ts
|
||||||
|
*.sass.d.ts
|
||||||
|
*.scss.d.ts
|
||||||
|
|
||||||
|
# eslint ignores hidden directories by default:
|
||||||
|
# https://github.com/eslint/eslint/issues/8429
|
||||||
|
!.erb
|
||||||
38
electron-ui/.eslintrc.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: 'erb',
|
||||||
|
plugins: ['@typescript-eslint'],
|
||||||
|
rules: {
|
||||||
|
// A temporary hack related to IDE not resolving correct package.json
|
||||||
|
'import/no-extraneous-dependencies': 'off',
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'react/jsx-filename-extension': 'off',
|
||||||
|
'import/extensions': 'off',
|
||||||
|
'import/no-unresolved': 'off',
|
||||||
|
'import/no-import-module-exports': 'off',
|
||||||
|
'no-shadow': 'off',
|
||||||
|
'@typescript-eslint/no-shadow': 'error',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'error',
|
||||||
|
'react/require-default-props': 'off',
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
// See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below
|
||||||
|
node: {
|
||||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||||
|
moduleDirectory: ['node_modules', 'src/'],
|
||||||
|
},
|
||||||
|
webpack: {
|
||||||
|
config: require.resolve('./.erb/configs/webpack.config.eslint.ts'),
|
||||||
|
},
|
||||||
|
typescript: {},
|
||||||
|
},
|
||||||
|
'import/parsers': {
|
||||||
|
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
12
electron-ui/.gitattributes
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
* text eol=lf
|
||||||
|
*.exe binary
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.ico binary
|
||||||
|
*.icns binary
|
||||||
|
*.eot binary
|
||||||
|
*.otf binary
|
||||||
|
*.ttf binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
25
electron-ui/.gitignore
vendored
@ -1,4 +1,27 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
release/app/dist
|
||||||
|
release/build
|
||||||
|
.erb/dll
|
||||||
|
|
||||||
|
.idea
|
||||||
|
npm-debug.log.*
|
||||||
|
*.css.d.ts
|
||||||
|
*.sass.d.ts
|
||||||
|
*.scss.d.ts
|
||||||
35
electron-ui/assets/assets.d.ts
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
type Styles = Record<string, string>;
|
||||||
|
|
||||||
|
declare module '*.svg' {
|
||||||
|
import React = require('react');
|
||||||
|
|
||||||
|
export const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||||
|
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.png' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.jpg' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.scss' {
|
||||||
|
const content: Styles;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.sass' {
|
||||||
|
const content: Styles;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.css' {
|
||||||
|
const content: Styles;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
8
electron-ui/assets/entitlements.mac.plist
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
electron-ui/assets/icon.icns
Normal file
BIN
electron-ui/assets/icon.ico
Normal file
|
After Width: | Height: | Size: 361 KiB |
BIN
electron-ui/assets/icon.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
23
electron-ui/assets/icon.svg
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<svg width="232" height="232" viewBox="0 0 232 232" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g filter="url(#filter0_b)">
|
||||||
|
<path d="M231.5 1V0.5H231H1H0.5V1V231V231.5H1H231H231.5V231V1ZM40.5 25C40.5 33.0082 34.0082 39.5 26 39.5C17.9918 39.5 11.5 33.0082 11.5 25C11.5 16.9918 17.9918 10.5 26 10.5C34.0082 10.5 40.5 16.9918 40.5 25ZM220.5 25C220.5 33.0082 214.008 39.5 206 39.5C197.992 39.5 191.5 33.0082 191.5 25C191.5 16.9918 197.992 10.5 206 10.5C214.008 10.5 220.5 16.9918 220.5 25ZM40.5 205C40.5 213.008 34.0082 219.5 26 219.5C17.9918 219.5 11.5 213.008 11.5 205C11.5 196.992 17.9918 190.5 26 190.5C34.0082 190.5 40.5 196.992 40.5 205ZM220.5 205C220.5 213.008 214.008 219.5 206 219.5C197.992 219.5 191.5 213.008 191.5 205C191.5 196.992 197.992 190.5 206 190.5C214.008 190.5 220.5 196.992 220.5 205ZM209.5 111C209.5 162.639 167.639 204.5 116 204.5C64.3613 204.5 22.5 162.639 22.5 111C22.5 59.3613 64.3613 17.5 116 17.5C167.639 17.5 209.5 59.3613 209.5 111Z" fill="white" stroke="white"/>
|
||||||
|
<path d="M63.5 146.5C63.5 149.959 60.8969 152.5 58 152.5C55.1031 152.5 52.5 149.959 52.5 146.5C52.5 143.041 55.1031 140.5 58 140.5C60.8969 140.5 63.5 143.041 63.5 146.5Z" stroke="white" stroke-width="5"/>
|
||||||
|
<path d="M54.9856 139.466C54.9856 139.466 51.1973 116.315 83.1874 93.1647C115.178 70.014 133.698 69.5931 133.698 69.5931" stroke="white" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
<path d="M178.902 142.686C177.27 139.853 173.652 138.88 170.819 140.512C167.987 142.144 167.014 145.762 168.646 148.595C170.277 151.427 173.896 152.4 176.728 150.768C179.561 149.137 180.534 145.518 178.902 142.686Z" stroke="white" stroke-width="5"/>
|
||||||
|
<path d="M169.409 151.555C169.409 151.555 151.24 166.394 115.211 150.232C79.182 134.07 69.5718 118.232 69.5718 118.232" stroke="white" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
<path d="M109.577 41.9707C107.966 44.8143 108.964 48.4262 111.808 50.038C114.651 51.6498 118.263 50.6512 119.875 47.8075C121.487 44.9639 120.488 41.3521 117.645 39.7403C114.801 38.1285 111.189 39.1271 109.577 41.9707Z" stroke="white" stroke-width="5"/>
|
||||||
|
<path d="M122.038 45.6467C122.038 45.6467 144.047 53.7668 148.412 93.0129C152.778 132.259 144.012 148.579 144.012 148.579" stroke="white" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
<path d="M59.6334 105C59.6334 105 50.4373 82.1038 61.3054 73.3616C72.1736 64.6194 96 69.1987 96 69.1987" stroke="white" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
<path d="M149.532 66.9784C149.532 66.9784 174.391 68.9134 177.477 82.6384C180.564 96.3634 165.799 115.833 165.799 115.833" stroke="white" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
<path d="M138.248 163.363C138.248 163.363 124.023 183.841 110.618 179.573C97.2129 175.305 87.8662 152.728 87.8662 152.728" stroke="white" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
<path d="M116 119C120.418 119 124 115.642 124 111.5C124 107.358 120.418 104 116 104C111.582 104 108 107.358 108 111.5C108 115.642 111.582 119 116 119Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_b" x="-4" y="-4" width="240" height="240" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feGaussianBlur in="BackgroundImage" stdDeviation="2"/>
|
||||||
|
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
BIN
electron-ui/assets/icons/1024x1024.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
electron-ui/assets/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
electron-ui/assets/icons/16x16.png
Normal file
|
After Width: | Height: | Size: 954 B |
BIN
electron-ui/assets/icons/24x24.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
electron-ui/assets/icons/256x256.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
electron-ui/assets/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
electron-ui/assets/icons/48x48.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
electron-ui/assets/icons/512x512.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
electron-ui/assets/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
electron-ui/assets/icons/96x96.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
0
electron-ui/electron-ui/src/old/renderer.js
Normal file
121
electron-ui/electron-ui/src/renderer/App.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import WaveSurfer from 'wavesurfer.js';
|
||||||
|
import RegionsPlugin from 'wavesurfer.js/dist/plugin/wavesurfer.regions.js';
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
interface AudioTrimmerProps {
|
||||||
|
filePath: string;
|
||||||
|
section: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AudioTrimmer: React.FC<AudioTrimmerProps> = ({ filePath, section }) => {
|
||||||
|
const [trimStart, setTrimStart] = useState<number>(0);
|
||||||
|
const [trimEnd, setTrimEnd] = useState<number>(0);
|
||||||
|
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||||
|
const waveformRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const wavesurferRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadTrimInfo = async () => {
|
||||||
|
const savedTrimInfo = await ipcRenderer.invoke('get-trim-info', section, path.basename(filePath));
|
||||||
|
setTrimStart(savedTrimInfo.trimStart || 0);
|
||||||
|
setTrimEnd(savedTrimInfo.trimEnd || 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
loadTrimInfo();
|
||||||
|
|
||||||
|
wavesurferRef.current = WaveSurfer.create({
|
||||||
|
container: waveformRef.current!,
|
||||||
|
waveColor: '#ccb1ff',
|
||||||
|
progressColor: '#6e44ba',
|
||||||
|
responsive: true,
|
||||||
|
height: 100,
|
||||||
|
hideScrollbar: true,
|
||||||
|
plugins: [
|
||||||
|
RegionsPlugin.create({
|
||||||
|
color: 'rgba(132, 81, 224, 0.3)',
|
||||||
|
drag: false,
|
||||||
|
resize: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
wavesurferRef.current.load(`file://${filePath}`);
|
||||||
|
|
||||||
|
wavesurferRef.current.on('ready', () => {
|
||||||
|
wavesurferRef.current.addRegion({
|
||||||
|
start: trimStart,
|
||||||
|
end: trimEnd,
|
||||||
|
color: 'rgba(132, 81, 224, 0.3)',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
wavesurferRef.current.on('region-update-end', (region: any) => {
|
||||||
|
setTrimStart(region.start);
|
||||||
|
setTrimEnd(region.end);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
wavesurferRef.current.destroy();
|
||||||
|
};
|
||||||
|
}, [filePath, section, trimStart, trimEnd]);
|
||||||
|
|
||||||
|
const handlePlayPause = () => {
|
||||||
|
if (isPlaying) {
|
||||||
|
wavesurferRef.current.pause();
|
||||||
|
} else {
|
||||||
|
wavesurferRef.current.play(trimStart, trimEnd);
|
||||||
|
}
|
||||||
|
setIsPlaying(!isPlaying);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveTrim = async () => {
|
||||||
|
const newTitle = prompt('Enter a title for the trimmed audio:');
|
||||||
|
if (newTitle) {
|
||||||
|
await ipcRenderer.invoke('save-trimmed-file', {
|
||||||
|
originalFilePath: filePath,
|
||||||
|
trimStart,
|
||||||
|
trimEnd,
|
||||||
|
title: newTitle,
|
||||||
|
});
|
||||||
|
alert('Trimmed audio saved successfully!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const confirmDelete = confirm('Are you sure you want to delete this audio file?');
|
||||||
|
if (confirmDelete) {
|
||||||
|
await ipcRenderer.invoke('delete-file', filePath);
|
||||||
|
alert('File deleted successfully!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="audio-trimmer-item">
|
||||||
|
<div className="audio-trimmer-header">
|
||||||
|
<div className="audio-trimmer-title">{path.basename(filePath)}</div>
|
||||||
|
<div className="audio-trimmer-controls">
|
||||||
|
<button onClick={handlePlayPause}>
|
||||||
|
{isPlaying ? 'Pause' : 'Play'}
|
||||||
|
</button>
|
||||||
|
<button onClick={handleSaveTrim}>Save Trim</button>
|
||||||
|
<button onClick={handleDelete}>Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref={waveformRef} className="waveform"></div>
|
||||||
|
<div className="trim-info">
|
||||||
|
<div>Start: {formatTime(trimStart)}</div>
|
||||||
|
<div>End: {formatTime(trimEnd)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = Math.floor(seconds % 60);
|
||||||
|
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AudioTrimmer;
|
||||||
138
electron-ui/electron-ui/src/renderer/components/AudioTrimmer.tsx
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import WaveSurfer from 'wavesurfer.js';
|
||||||
|
import RegionsPlugin from 'wavesurfer.js/dist/plugin/wavesurfer.regions.js';
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
interface AudioTrimmerProps {
|
||||||
|
filePath: string;
|
||||||
|
section: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AudioTrimmer: React.FC<AudioTrimmerProps> = ({ filePath, section }) => {
|
||||||
|
const [trimStart, setTrimStart] = useState(0);
|
||||||
|
const [trimEnd, setTrimEnd] = useState(0);
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
const waveformRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const wavesurferRef = useRef<WaveSurfer | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadTrimInfo = async () => {
|
||||||
|
const savedTrimInfo = await ipcRenderer.invoke('get-trim-info', section, path.basename(filePath));
|
||||||
|
setTrimStart(savedTrimInfo.trimStart || 0);
|
||||||
|
setTrimEnd(savedTrimInfo.trimEnd || 0);
|
||||||
|
setTitle(savedTrimInfo.title || path.basename(filePath));
|
||||||
|
};
|
||||||
|
|
||||||
|
loadTrimInfo();
|
||||||
|
}, [filePath, section]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (waveformRef.current) {
|
||||||
|
wavesurferRef.current = WaveSurfer.create({
|
||||||
|
container: waveformRef.current,
|
||||||
|
waveColor: '#ccb1ff',
|
||||||
|
progressColor: '#6e44ba',
|
||||||
|
responsive: true,
|
||||||
|
height: 100,
|
||||||
|
hideScrollbar: true,
|
||||||
|
plugins: [
|
||||||
|
RegionsPlugin.create({
|
||||||
|
color: 'rgba(132, 81, 224, 0.3)',
|
||||||
|
drag: false,
|
||||||
|
resize: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
wavesurferRef.current.load(`file://${filePath}`);
|
||||||
|
|
||||||
|
wavesurferRef.current.on('ready', () => {
|
||||||
|
wavesurferRef.current?.addRegion({
|
||||||
|
start: trimStart,
|
||||||
|
end: trimEnd,
|
||||||
|
color: 'rgba(132, 81, 224, 0.3)',
|
||||||
|
drag: false,
|
||||||
|
resize: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
wavesurferRef.current.on('region-update-end', (region) => {
|
||||||
|
setTrimStart(region.start);
|
||||||
|
setTrimEnd(region.end);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
wavesurferRef.current?.destroy();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [filePath, trimStart, trimEnd]);
|
||||||
|
|
||||||
|
const handlePlayPause = () => {
|
||||||
|
if (wavesurferRef.current) {
|
||||||
|
if (wavesurferRef.current.isPlaying()) {
|
||||||
|
wavesurferRef.current.pause();
|
||||||
|
} else {
|
||||||
|
wavesurferRef.current.play(trimStart, trimEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveTrim = async () => {
|
||||||
|
const newTitle = title.trim();
|
||||||
|
await ipcRenderer.invoke('save-trimmed-file', {
|
||||||
|
originalFilePath: filePath,
|
||||||
|
trimStart,
|
||||||
|
trimEnd,
|
||||||
|
title: newTitle,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const confirmDelete = window.confirm('Are you sure you want to delete this audio file?');
|
||||||
|
if (confirmDelete) {
|
||||||
|
await ipcRenderer.invoke('delete-file', filePath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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}</div>
|
||||||
|
<div className="audio-trimmer-filename">{path.basename(filePath)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="audio-trimmer-controls">
|
||||||
|
<button className="play-pause-btn" onClick={handlePlayPause}>
|
||||||
|
Play/Pause
|
||||||
|
</button>
|
||||||
|
<button className="save-trim" onClick={handleSaveTrim}>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button className="delete-btn" onClick={handleDelete}>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="waveform-container" ref={waveformRef}></div>
|
||||||
|
<div className="trim-info">
|
||||||
|
<div className="trim-time">
|
||||||
|
<span>Start: </span>
|
||||||
|
<span>{formatTime(trimStart)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="trim-time">
|
||||||
|
<span>End: </span>
|
||||||
|
<span>{formatTime(trimEnd)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = Math.floor(seconds % 60);
|
||||||
|
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AudioTrimmer;
|
||||||
16
electron-ui/electron-ui/src/renderer/types/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// This file is intended for defining TypeScript types and interfaces that can be used throughout the application.
|
||||||
|
|
||||||
|
export interface TrimInfo {
|
||||||
|
title?: string;
|
||||||
|
trimStart: number;
|
||||||
|
trimEnd: number;
|
||||||
|
originalPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AudioTrimmerProps {
|
||||||
|
filePath: string;
|
||||||
|
section: string;
|
||||||
|
savedTrimInfo: TrimInfo;
|
||||||
|
onSave: (trimInfo: TrimInfo) => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
}
|
||||||
29622
electron-ui/package-lock.json
generated
@ -1,51 +1,273 @@
|
|||||||
{
|
{
|
||||||
"name": "audio-clipper",
|
"name": "electron-react-boilerplate",
|
||||||
"version": "1.0.0",
|
"description": "A foundation for scalable desktop apps",
|
||||||
"main": "src/main.js",
|
"keywords": [
|
||||||
"dependencies": {
|
"electron",
|
||||||
"chokidar": "^3.5.3",
|
"boilerplate",
|
||||||
|
"react",
|
||||||
"electron-reload": "^2.0.0-alpha.1",
|
"typescript",
|
||||||
"python-shell": "^5.0.0",
|
"ts",
|
||||||
"wavefile": "^11.0.0",
|
"sass",
|
||||||
"wavesurfer.js": "^6.6.4"
|
"webpack",
|
||||||
},
|
"hot",
|
||||||
"scripts": {
|
"reload"
|
||||||
"start": "electron .",
|
],
|
||||||
"dev": "electron . --enable-logging",
|
"homepage": "https://github.com/electron-react-boilerplate/electron-react-boilerplate#readme",
|
||||||
"build": "electron-builder",
|
"bugs": {
|
||||||
"build:win": "electron-builder --win",
|
"url": "https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues"
|
||||||
"build:mac": "electron-builder --mac",
|
},
|
||||||
"build:linux": "electron-builder --linux"
|
"repository": {
|
||||||
},
|
"type": "git",
|
||||||
"build": {
|
"url": "git+https://github.com/electron-react-boilerplate/electron-react-boilerplate.git"
|
||||||
"appId": "com.michalcourson.cliptrimserivce",
|
},
|
||||||
"productName": "ClipTrim",
|
"license": "MIT",
|
||||||
"directories": {
|
"author": {
|
||||||
"output": "dist"
|
"name": "Electron React Boilerplate Maintainers",
|
||||||
},
|
"email": "electronreactboilerplate@gmail.com",
|
||||||
"extraResources": [
|
"url": "https://electron-react-boilerplate.js.org"
|
||||||
{
|
},
|
||||||
"from": "../audio-service",
|
"contributors": [
|
||||||
"to": "audio-service",
|
{
|
||||||
"filter": ["**/*"]
|
"name": "Amila Welihinda",
|
||||||
}
|
"email": "amilajack@gmail.com",
|
||||||
],
|
"url": "https://github.com/amilajack"
|
||||||
"win": {
|
|
||||||
"target": ["nsis"],
|
|
||||||
"icon": "build/icon.ico"
|
|
||||||
},
|
|
||||||
"mac": {
|
|
||||||
"target": ["dmg"],
|
|
||||||
"icon": "build/icon.icns"
|
|
||||||
},
|
|
||||||
"linux": {
|
|
||||||
"target": ["AppImage"],
|
|
||||||
"icon": "build/icon.png"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"electron-builder": "^25.1.8",
|
|
||||||
"electron": "^13.1.7"
|
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"main": "./.erb/dll/main.bundle.dev.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
|
||||||
|
"build:dll": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts",
|
||||||
|
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack --config ./.erb/configs/webpack.config.main.prod.ts",
|
||||||
|
"build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
|
||||||
|
"postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && npm run build:dll",
|
||||||
|
"lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx",
|
||||||
|
"lint:fix": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
||||||
|
"package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never && npm run build:dll",
|
||||||
|
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
|
||||||
|
"prestart": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack --config ./.erb/configs/webpack.config.main.dev.ts",
|
||||||
|
"start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run prestart && npm run start:renderer",
|
||||||
|
"start:main": "concurrently -k -P \"cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --watch --config ./.erb/configs/webpack.config.main.dev.ts\" \"electronmon . -- {@}\" --",
|
||||||
|
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
|
||||||
|
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"extends browserslist-config-erb"
|
||||||
|
],
|
||||||
|
"prettier": {
|
||||||
|
"singleQuote": true,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
".prettierrc",
|
||||||
|
".eslintrc"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"parser": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleDirectories": [
|
||||||
|
"node_modules",
|
||||||
|
"release/app/node_modules",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"jsx",
|
||||||
|
"ts",
|
||||||
|
"tsx",
|
||||||
|
"json"
|
||||||
|
],
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/.erb/mocks/fileMock.js",
|
||||||
|
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
|
||||||
|
},
|
||||||
|
"setupFiles": [
|
||||||
|
"./.erb/scripts/check-build-exists.ts"
|
||||||
|
],
|
||||||
|
"testEnvironment": "jsdom",
|
||||||
|
"testEnvironmentOptions": {
|
||||||
|
"url": "http://localhost/"
|
||||||
|
},
|
||||||
|
"testPathIgnorePatterns": [
|
||||||
|
"release/app/dist",
|
||||||
|
".erb/dll"
|
||||||
|
],
|
||||||
|
"transform": {
|
||||||
|
"\\.(ts|tsx|js|jsx)$": "ts-jest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@electron/notarize": "^3.0.0",
|
||||||
|
"@tailwindcss/cli": "^4.1.18",
|
||||||
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
|
"@wavesurfer/react": "^1.0.12",
|
||||||
|
"electron-debug": "^4.1.0",
|
||||||
|
"electron-log": "^5.3.2",
|
||||||
|
"electron-updater": "^6.3.9",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.3.0",
|
||||||
|
"tailwindcss": "^4.1.18",
|
||||||
|
"wavesurfer.js": "^7.12.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@electron/rebuild": "^3.7.1",
|
||||||
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||||
|
"@svgr/webpack": "^8.1.0",
|
||||||
|
"@teamsupercell/typings-for-css-modules-loader": "^2.5.2",
|
||||||
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
"@testing-library/react": "^16.2.0",
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
|
"@types/node": "22.13.10",
|
||||||
|
"@types/react": "^19.0.11",
|
||||||
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@types/react-test-renderer": "^19.0.0",
|
||||||
|
"@types/webpack-bundle-analyzer": "^4.7.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||||
|
"@typescript-eslint/parser": "^8.26.1",
|
||||||
|
"autoprefixer": "^10.4.24",
|
||||||
|
"browserslist-config-erb": "^0.0.3",
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"concurrently": "^9.1.2",
|
||||||
|
"core-js": "^3.41.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"css-loader": "^7.1.2",
|
||||||
|
"css-minimizer-webpack-plugin": "^7.0.2",
|
||||||
|
"detect-port": "^2.1.0",
|
||||||
|
"electron": "^35.0.2",
|
||||||
|
"electron-builder": "^25.1.8",
|
||||||
|
"electron-devtools-installer": "^4.0.0",
|
||||||
|
"electronmon": "^2.0.3",
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
|
"eslint-config-erb": "^4.1.0",
|
||||||
|
"eslint-import-resolver-typescript": "^4.1.1",
|
||||||
|
"eslint-import-resolver-webpack": "^0.13.10",
|
||||||
|
"eslint-plugin-compat": "^6.0.2",
|
||||||
|
"eslint-plugin-import": "^2.31.0",
|
||||||
|
"eslint-plugin-jest": "^28.11.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
|
"eslint-plugin-promise": "^7.2.1",
|
||||||
|
"eslint-plugin-react": "^7.37.4",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"html-webpack-plugin": "^5.6.3",
|
||||||
|
"identity-obj-proxy": "^3.0.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
|
"mini-css-extract-plugin": "^2.9.2",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"postcss-loader": "^8.2.0",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"react-refresh": "^0.16.0",
|
||||||
|
"react-test-renderer": "^19.0.0",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
|
"sass": "^1.86.0",
|
||||||
|
"sass-loader": "^16.0.5",
|
||||||
|
"style-loader": "^4.0.0",
|
||||||
|
"terser-webpack-plugin": "^5.3.14",
|
||||||
|
"ts-jest": "^29.2.6",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths-webpack-plugin": "^4.2.0",
|
||||||
|
"typescript": "^5.8.2",
|
||||||
|
"url-loader": "^4.1.1",
|
||||||
|
"webpack": "^5.98.0",
|
||||||
|
"webpack-bundle-analyzer": "^4.10.2",
|
||||||
|
"webpack-cli": "^6.0.1",
|
||||||
|
"webpack-dev-server": "^5.2.0",
|
||||||
|
"webpack-merge": "^6.0.1"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"productName": "ElectronReact",
|
||||||
|
"appId": "org.erb.ElectronReact",
|
||||||
|
"asar": true,
|
||||||
|
"afterSign": ".erb/scripts/notarize.js",
|
||||||
|
"asarUnpack": "**\\*.{node,dll}",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
"package.json"
|
||||||
|
],
|
||||||
|
"mac": {
|
||||||
|
"notarize": false,
|
||||||
|
"target": {
|
||||||
|
"target": "default",
|
||||||
|
"arch": [
|
||||||
|
"arm64",
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "distribution",
|
||||||
|
"hardenedRuntime": true,
|
||||||
|
"entitlements": "assets/entitlements.mac.plist",
|
||||||
|
"entitlementsInherit": "assets/entitlements.mac.plist",
|
||||||
|
"gatekeeperAssess": false
|
||||||
|
},
|
||||||
|
"dmg": {
|
||||||
|
"contents": [
|
||||||
|
{
|
||||||
|
"x": 130,
|
||||||
|
"y": 220
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 410,
|
||||||
|
"y": 220,
|
||||||
|
"type": "link",
|
||||||
|
"path": "/Applications"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"target": [
|
||||||
|
"nsis"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"target": [
|
||||||
|
"AppImage"
|
||||||
|
],
|
||||||
|
"category": "Development"
|
||||||
|
},
|
||||||
|
"directories": {
|
||||||
|
"app": "release/app",
|
||||||
|
"buildResources": "assets",
|
||||||
|
"output": "release/build"
|
||||||
|
},
|
||||||
|
"extraResources": [
|
||||||
|
"./assets/**"
|
||||||
|
],
|
||||||
|
"publish": {
|
||||||
|
"provider": "github",
|
||||||
|
"owner": "electron-react-boilerplate",
|
||||||
|
"repo": "electron-react-boilerplate"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"collective": {
|
||||||
|
"url": "https://opencollective.com/electron-react-boilerplate-594"
|
||||||
|
},
|
||||||
|
"devEngines": {
|
||||||
|
"runtime": {
|
||||||
|
"name": "node",
|
||||||
|
"version": ">=14.x",
|
||||||
|
"onFail": "error"
|
||||||
|
},
|
||||||
|
"packageManager": {
|
||||||
|
"name": "npm",
|
||||||
|
"version": ">=7.x",
|
||||||
|
"onFail": "error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"electronmon": {
|
||||||
|
"patterns": [
|
||||||
|
"!**/**",
|
||||||
|
"src/main/**",
|
||||||
|
".erb/dll/**"
|
||||||
|
],
|
||||||
|
"logLevel": "quiet"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
electron-ui/release/app/package-lock.json
generated
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "electron-react-boilerplate",
|
||||||
|
"version": "4.6.0",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "electron-react-boilerplate",
|
||||||
|
"version": "4.6.0",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
electron-ui/release/app/package.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "electron-react-boilerplate",
|
||||||
|
"version": "4.6.0",
|
||||||
|
"description": "A foundation for scalable desktop apps",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": {
|
||||||
|
"name": "Electron React Boilerplate Maintainers",
|
||||||
|
"email": "electronreactboilerplate@gmail.com",
|
||||||
|
"url": "https://github.com/electron-react-boilerplate"
|
||||||
|
},
|
||||||
|
"main": "./dist/main/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js",
|
||||||
|
"postinstall": "npm run rebuild && npm run link-modules",
|
||||||
|
"link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
||||||
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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 { app, BrowserWindow, ipcMain, Tray, Menu, dialog } = require('electron');
|
||||||
const path = require("path");
|
const path = require('path');
|
||||||
const os = require("os");
|
const os = require('os');
|
||||||
const spawn = require('child_process').spawn;
|
const spawn = require('child_process').spawn;
|
||||||
require("electron-reload")(__dirname);
|
require('electron-reload')(__dirname);
|
||||||
const fs = require("fs").promises;
|
const fs = require('fs').promises;
|
||||||
const chokidar = require("chokidar");
|
const chokidar = require('chokidar');
|
||||||
const wavefile = require("wavefile");
|
const wavefile = require('wavefile');
|
||||||
const MetadataManager = require("./metatadata");
|
const MetadataManager = require('./metatadata');
|
||||||
|
|
||||||
const { webContents } = require("electron");
|
const { webContents } = require('electron');
|
||||||
|
|
||||||
// import { app, BrowserWindow, ipcMain, Tray, Menu, dialog } from "electron";
|
// import { app, BrowserWindow, ipcMain, Tray, Menu, dialog } from "electron";
|
||||||
// import path from "path";
|
// import path from "path";
|
||||||
@ -20,35 +20,34 @@ const { webContents } = require("electron");
|
|||||||
// import MetadataManager from "./metatadata.cjs";
|
// import MetadataManager from "./metatadata.cjs";
|
||||||
// import { webContents } from "electron";
|
// import { webContents } from "electron";
|
||||||
|
|
||||||
|
|
||||||
let mainWindow;
|
let mainWindow;
|
||||||
let tray;
|
let tray;
|
||||||
let audioServiceProcess;
|
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);
|
const metadataManager = new MetadataManager(metadataPath);
|
||||||
|
|
||||||
async function createPythonService() {
|
async function createPythonService() {
|
||||||
const pythonPath =
|
const pythonPath =
|
||||||
process.platform === "win32"
|
process.platform === 'win32'
|
||||||
? path.join(
|
? path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
"..",
|
'..',
|
||||||
"..",
|
'..',
|
||||||
"audio-service",
|
'audio-service',
|
||||||
"venv",
|
'venv',
|
||||||
"Scripts",
|
'Scripts',
|
||||||
"python.exe"
|
'python.exe',
|
||||||
)
|
)
|
||||||
: path.join(__dirname, "..", "audio-service", "venv", "bin", "python");
|
: path.join(__dirname, '..', 'audio-service', 'venv', 'bin', 'python');
|
||||||
|
|
||||||
const scriptPath = path.join(
|
const scriptPath = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
"..",
|
'..',
|
||||||
"..",
|
'..',
|
||||||
"audio-service",
|
'audio-service',
|
||||||
"src",
|
'src',
|
||||||
"main.py"
|
'main.py',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load settings to pass as arguments
|
// Load settings to pass as arguments
|
||||||
@ -56,75 +55,81 @@ async function createPythonService() {
|
|||||||
|
|
||||||
const args = [
|
const args = [
|
||||||
scriptPath,
|
scriptPath,
|
||||||
'--recording-length', settings.recordingLength.toString(),
|
'--recording-length',
|
||||||
'--save-path', path.join(settings.outputFolder, "original"),
|
settings.recordingLength.toString(),
|
||||||
'--osc-port', settings.oscPort.toString() // Or make this configurable
|
'--save-path',
|
||||||
|
path.join(settings.outputFolder, 'original'),
|
||||||
|
'--osc-port',
|
||||||
|
settings.oscPort.toString(), // Or make this configurable
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add input device if specified
|
// Add input device if specified
|
||||||
if (settings.inputDevice) {
|
if (settings.inputDevice) {
|
||||||
const devices = await listAudioDevices();
|
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, {
|
audioServiceProcess = spawn(pythonPath, args, {
|
||||||
detached: false,
|
detached: false,
|
||||||
stdio: "pipe",
|
stdio: 'pipe',
|
||||||
});
|
});
|
||||||
|
|
||||||
audioServiceProcess.stdout.on("data", (data) => {
|
audioServiceProcess.stdout.on('data', (data) => {
|
||||||
console.log(`Audio Service: ${data}`);
|
console.log(`Audio Service: ${data}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
audioServiceProcess.stderr.on("data", (data) => {
|
audioServiceProcess.stderr.on('data', (data) => {
|
||||||
console.error(`Audio Service Error: ${data}`);
|
console.error(`Audio Service Error: ${data}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
audioServiceProcess.on("close", (code) => {
|
audioServiceProcess.on('close', (code) => {
|
||||||
console.log(`Audio Service process exited with code ${code}`);
|
console.log(`Audio Service process exited with code ${code}`);
|
||||||
audioServiceProcess = null;
|
audioServiceProcess = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTray() {
|
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([
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
{
|
{
|
||||||
label: "Show",
|
label: 'Show',
|
||||||
click: () => {
|
click: () => {
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Quit",
|
label: 'Quit',
|
||||||
click: () => {
|
click: () => {
|
||||||
// Properly terminate the Python service
|
// Properly terminate the Python service
|
||||||
|
|
||||||
stopService();
|
stopService();
|
||||||
app.quit();
|
app.quit();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
tray.setToolTip("Audio Trimmer");
|
tray.setToolTip('Audio Trimmer');
|
||||||
tray.setContextMenu(contextMenu);
|
tray.setContextMenu(contextMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkNewWavFile(filePath) {
|
async function checkNewWavFile(filePath) {
|
||||||
// Only process .wav files
|
// Only process .wav files
|
||||||
if (path.extname(filePath).toLowerCase() === ".wav") {
|
if (path.extname(filePath).toLowerCase() === '.wav') {
|
||||||
try {
|
try {
|
||||||
await metadataManager.addUntrimmedFile(filePath);
|
await metadataManager.addUntrimmedFile(filePath);
|
||||||
|
|
||||||
// Notify renderer if window is ready
|
// Notify renderer if window is ready
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.webContents.send("new-untrimmed-file", filePath);
|
mainWindow.webContents.send('new-untrimmed-file', filePath);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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() {
|
function stopService() {
|
||||||
if (audioServiceProcess) {
|
if (audioServiceProcess) {
|
||||||
try {
|
try {
|
||||||
if (process.platform === "win32") {
|
if (process.platform === 'win32') {
|
||||||
spawn("taskkill", ["/pid", audioServiceProcess.pid, "/f", "/t"]);
|
spawn('taskkill', ['/pid', audioServiceProcess.pid, '/f', '/t']);
|
||||||
} else {
|
} else {
|
||||||
audioServiceProcess.kill("SIGTERM");
|
audioServiceProcess.kill('SIGTERM');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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() {
|
async function loadSettings() {
|
||||||
try {
|
try {
|
||||||
const settingsPath = path.join(app.getPath("userData"), "settings.json");
|
const settingsPath = path.join(app.getPath('userData'), 'settings.json');
|
||||||
const settingsData = await fs.readFile(settingsPath, "utf8");
|
const settingsData = await fs.readFile(settingsPath, 'utf8');
|
||||||
return JSON.parse(settingsData);
|
return JSON.parse(settingsData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If no settings file exists, return default settings
|
// If no settings file exists, return default settings
|
||||||
return {
|
return {
|
||||||
recordingLength: 30,
|
recordingLength: 30,
|
||||||
outputFolder: path.join(os.homedir(), "AudioTrimmer"),
|
outputFolder: path.join(os.homedir(), 'AudioTrimmer'),
|
||||||
inputDevice: null,
|
inputDevice: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listAudioDevices() {
|
async function listAudioDevices() {
|
||||||
try {
|
try {
|
||||||
// Use a webContents to access navigator.mediaDevices
|
// Use a webContents to access navigator.mediaDevices
|
||||||
|
|
||||||
const contents = webContents.getAllWebContents()[0];
|
const contents = webContents.getAllWebContents()[0];
|
||||||
|
|
||||||
const devices = await contents.executeJavaScript(`
|
const devices = await contents.executeJavaScript(`
|
||||||
navigator.mediaDevices.enumerateDevices()
|
navigator.mediaDevices.enumerateDevices()
|
||||||
.then(devices => devices.filter(device => device.kind === 'audioinput'))
|
.then(devices => devices.filter(device => device.kind === 'audioinput'))
|
||||||
.then(audioDevices => audioDevices.map(device => ({
|
.then(audioDevices => audioDevices.map(device => ({
|
||||||
@ -180,12 +185,12 @@ async function listAudioDevices() {
|
|||||||
name: device.label || 'Unknown Microphone'
|
name: device.label || 'Unknown Microphone'
|
||||||
})))
|
})))
|
||||||
`);
|
`);
|
||||||
|
|
||||||
return devices;
|
return devices;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting input devices:", error);
|
console.error('Error getting input devices:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function createWindow() {
|
async function createWindow() {
|
||||||
// Initialize metadata
|
// Initialize metadata
|
||||||
@ -197,7 +202,7 @@ async function createWindow() {
|
|||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
titleBarStyle: 'hidden',
|
titleBarStyle: 'hidden',
|
||||||
frame: false,
|
frame: false,
|
||||||
|
|
||||||
// titleBarOverlay: {
|
// titleBarOverlay: {
|
||||||
// color: '#1e1e1e',
|
// color: '#1e1e1e',
|
||||||
// symbolColor: '#ffffff',
|
// symbolColor: '#ffffff',
|
||||||
@ -209,23 +214,27 @@ async function createWindow() {
|
|||||||
// Add these to help with graphics issues
|
// Add these to help with graphics issues
|
||||||
},
|
},
|
||||||
// These additional options can help with graphics rendering
|
// These additional options can help with graphics rendering
|
||||||
backgroundColor: "#1e1e1e",
|
backgroundColor: '#1e1e1e',
|
||||||
...(process.platform !== 'darwin' ? { titleBarOverlay: {
|
...(process.platform !== 'darwin'
|
||||||
color: '#262626',
|
? {
|
||||||
symbolColor: '#ffffff',
|
titleBarOverlay: {
|
||||||
height: 30
|
color: '#262626',
|
||||||
} } : {})
|
symbolColor: '#ffffff',
|
||||||
|
height: 30,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
mainWindow.loadFile("src/index.html");
|
mainWindow.loadFile('src/index.html');
|
||||||
|
|
||||||
// Create Python ser
|
// Create Python ser
|
||||||
const settings = await loadSettings(); // Assuming you have a method to load settings
|
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
|
// Ensure recordings directory exists
|
||||||
try {
|
try {
|
||||||
await fs.mkdir(recordingsPath, { recursive: true });
|
await fs.mkdir(recordingsPath, { recursive: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating recordings directory:", error);
|
console.error('Error creating recordings directory:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch for new WAV files
|
// 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);
|
await checkNewWavFile(filePath);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("get-collections", () => {
|
ipcMain.handle('get-collections', () => {
|
||||||
return metadataManager.getCollections();
|
return metadataManager.getCollections();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("get-collection-files", (event, collectionPath) => {
|
ipcMain.handle('get-collection-files', (event, collectionPath) => {
|
||||||
return metadataManager.getFilesInCollection(collectionPath);
|
return metadataManager.getFilesInCollection(collectionPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("add-untrimmed-file", (event, filePath) => {
|
ipcMain.handle('add-untrimmed-file', (event, filePath) => {
|
||||||
return metadataManager.addUntrimmedFile(filePath);
|
return metadataManager.addUntrimmedFile(filePath);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"save-trimmed-file",
|
'save-trimmed-file',
|
||||||
(event, fileName, previousPath, savePath, trimStart, trimEnd, title) => {
|
(event, fileName, previousPath, savePath, trimStart, trimEnd, title) => {
|
||||||
return metadataManager.saveTrimmedFile(
|
return metadataManager.saveTrimmedFile(
|
||||||
fileName,
|
fileName,
|
||||||
@ -270,29 +279,23 @@ async function createWindow() {
|
|||||||
savePath,
|
savePath,
|
||||||
trimStart,
|
trimStart,
|
||||||
trimEnd,
|
trimEnd,
|
||||||
title
|
title,
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle('restart', (event) => {
|
||||||
"restart",
|
restartService();
|
||||||
(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(
|
ipcMain.handle(
|
||||||
"delete-old-file",
|
'save-trimmed-audio',
|
||||||
(event, outputFolder, section, title) => {
|
|
||||||
if(section === 'untrimmed') return;
|
|
||||||
const collectionPath = path.join(outputFolder, section);
|
|
||||||
const outputFilePath = path.join(collectionPath, `${title}.wav`);
|
|
||||||
fs.unlink(outputFilePath);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
ipcMain.handle(
|
|
||||||
"save-trimmed-audio",
|
|
||||||
async (
|
async (
|
||||||
event,
|
event,
|
||||||
{
|
{
|
||||||
@ -302,7 +305,7 @@ async function createWindow() {
|
|||||||
title,
|
title,
|
||||||
trimStart,
|
trimStart,
|
||||||
trimEnd,
|
trimEnd,
|
||||||
}
|
},
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// Ensure the collection folder exists
|
// Ensure the collection folder exists
|
||||||
@ -314,7 +317,7 @@ async function createWindow() {
|
|||||||
|
|
||||||
// Read the original WAV file
|
// Read the original WAV file
|
||||||
const originalWaveFile = new wavefile.WaveFile(
|
const originalWaveFile = new wavefile.WaveFile(
|
||||||
await fs.readFile(originalFilePath)
|
await fs.readFile(originalFilePath),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate trim points in samples
|
// Calculate trim points in samples
|
||||||
@ -353,7 +356,7 @@ async function createWindow() {
|
|||||||
originalWaveFile.fmt.numChannels,
|
originalWaveFile.fmt.numChannels,
|
||||||
sampleRate,
|
sampleRate,
|
||||||
bitDepth, // Always use 32-bit float
|
bitDepth, // Always use 32-bit float
|
||||||
trimmedSamples
|
trimmedSamples,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Write the trimmed WAV file
|
// Write the trimmed WAV file
|
||||||
@ -364,84 +367,84 @@ async function createWindow() {
|
|||||||
filePath: outputFilePath,
|
filePath: outputFilePath,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving trimmed audio:", error);
|
console.error('Error saving trimmed audio:', error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
ipcMain.handle("delete-file", async (event, filePath) => {
|
ipcMain.handle('delete-file', async (event, filePath) => {
|
||||||
try {
|
try {
|
||||||
const settings = await loadSettings();
|
const settings = await loadSettings();
|
||||||
return metadataManager.deletefile(filePath, settings.outputFolder);
|
return metadataManager.deletefile(filePath, settings.outputFolder);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error Deleting file:", error);
|
console.error('Error Deleting file:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("add-new-collection", (event, collectionName) => {
|
ipcMain.handle('add-new-collection', (event, collectionName) => {
|
||||||
try {
|
try {
|
||||||
return metadataManager.addNewCollection(collectionName);
|
return metadataManager.addNewCollection(collectionName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error adding collection:", error);
|
console.error('Error adding collection:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ipcMain.handle("get-trim-info", (event, collectionName, filePath) => {
|
ipcMain.handle('get-trim-info', (event, collectionName, filePath) => {
|
||||||
return metadataManager.getTrimInfo(collectionName, filePath);
|
return metadataManager.getTrimInfo(collectionName, filePath);
|
||||||
});
|
});
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"set-trim-info",
|
'set-trim-info',
|
||||||
(event, collectionName, filePath, trim_info) => {
|
(event, collectionName, filePath, trim_info) => {
|
||||||
return metadataManager.setTrimInfo(collectionName, filePath, trim_info);
|
return metadataManager.setTrimInfo(collectionName, filePath, trim_info);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add these IPC handlers
|
// Add these IPC handlers
|
||||||
ipcMain.handle("select-output-folder", async (event) => {
|
ipcMain.handle('select-output-folder', async (event) => {
|
||||||
const result = await dialog.showOpenDialog({
|
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 {
|
return {
|
||||||
recordingLength: 30,
|
recordingLength: 30,
|
||||||
outputFolder: path.join(os.homedir(), "AudioTrimmer"),
|
outputFolder: path.join(os.homedir(), 'AudioTrimmer'),
|
||||||
inputDevice: null,
|
inputDevice: null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("save-settings", async (event, settings) => {
|
ipcMain.handle('save-settings', async (event, settings) => {
|
||||||
try {
|
try {
|
||||||
// Ensure output folder exists
|
// Ensure output folder exists
|
||||||
await fs.mkdir(settings.outputFolder, { recursive: true });
|
await fs.mkdir(settings.outputFolder, { recursive: true });
|
||||||
|
|
||||||
// Save settings to a file
|
// 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));
|
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
||||||
restartService();
|
restartService();
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving settings:", error);
|
console.error('Error saving settings:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("load-settings", async () => {
|
ipcMain.handle('load-settings', async () => {
|
||||||
return loadSettings();
|
return loadSettings();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("get-input-devices", async () => {
|
ipcMain.handle('get-input-devices', async () => {
|
||||||
return await listAudioDevices();
|
return await listAudioDevices();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Minimize to tray instead of closing
|
// Minimize to tray instead of closing
|
||||||
mainWindow.on("close", (event) => {
|
mainWindow.on('close', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
mainWindow.hide();
|
mainWindow.hide();
|
||||||
});
|
});
|
||||||
@ -455,25 +458,25 @@ async function createWindow() {
|
|||||||
app.disableHardwareAcceleration();
|
app.disableHardwareAcceleration();
|
||||||
app.whenReady().then(createWindow);
|
app.whenReady().then(createWindow);
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
app.on('window-all-closed', () => {
|
||||||
// Do nothing - we handle closing via tray
|
// Do nothing - we handle closing via tray
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ensure Python service is killed when app quits
|
// Ensure Python service is killed when app quits
|
||||||
app.on("before-quit", () => {
|
app.on('before-quit', () => {
|
||||||
if (audioServiceProcess) {
|
if (audioServiceProcess) {
|
||||||
try {
|
try {
|
||||||
if (process.platform === "win32") {
|
if (process.platform === 'win32') {
|
||||||
spawn("taskkill", ["/pid", audioServiceProcess.pid, "/f", "/t"]);
|
spawn('taskkill', ['/pid', audioServiceProcess.pid, '/f', '/t']);
|
||||||
} else {
|
} else {
|
||||||
audioServiceProcess.kill("SIGTERM");
|
audioServiceProcess.kill('SIGTERM');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createWindow();
|
createWindow();
|
||||||
}
|
}
|
||||||
@ -1,41 +1,41 @@
|
|||||||
const { ipcRenderer } = require("electron");
|
const { ipcRenderer } = require('electron');
|
||||||
const path = require("path");
|
// const path = require('path');
|
||||||
const WaveSurfer = require("wavesurfer.js");
|
const WaveSurfer = require('wavesurfer.js');
|
||||||
const RegionsPlugin = require("wavesurfer.js/dist/plugin/wavesurfer.regions.js");
|
const RegionsPlugin = require('wavesurfer.js/dist/plugin/wavesurfer.regions.js');
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
// Settings Modal Logic
|
// Settings Modal Logic
|
||||||
const settingsModal = document.getElementById("settings-modal");
|
const settingsModal = document.getElementById('settings-modal');
|
||||||
const settingsBtn = document.getElementById("settings-btn");
|
const settingsBtn = document.getElementById('settings-btn');
|
||||||
const restartBtn = document.getElementById("restart-btn");
|
const restartBtn = document.getElementById('restart-btn');
|
||||||
const closeModalBtn = document.querySelector(".close-modal");
|
const closeModalBtn = document.querySelector('.close-modal');
|
||||||
const saveSettingsBtn = document.getElementById("save-settings");
|
const saveSettingsBtn = document.getElementById('save-settings');
|
||||||
const selectOutputFolderBtn = document.getElementById("select-output-folder");
|
const selectOutputFolderBtn = document.getElementById('select-output-folder');
|
||||||
const recordingLengthInput = document.getElementById("recording-length");
|
const recordingLengthInput = document.getElementById('recording-length');
|
||||||
const oscPortInput = document.getElementById("osc-port");
|
const oscPortInput = document.getElementById('osc-port');
|
||||||
const outputFolderInput = document.getElementById("output-folder");
|
const outputFolderInput = document.getElementById('output-folder');
|
||||||
const inputDeviceSelect = document.getElementById("input-device");
|
const inputDeviceSelect = document.getElementById('input-device');
|
||||||
|
|
||||||
// Open settings modal
|
// Open settings modal
|
||||||
settingsBtn.addEventListener("click", async () => {
|
settingsBtn.addEventListener('click', async () => {
|
||||||
try {
|
try {
|
||||||
// Request microphone permissions first
|
// Request microphone permissions first
|
||||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
|
||||||
// Load current settings
|
// Load current settings
|
||||||
const settings = await ipcRenderer.invoke("load-settings");
|
const settings = await ipcRenderer.invoke('load-settings');
|
||||||
|
|
||||||
// Populate input devices
|
// Populate input devices
|
||||||
const devices = await ipcRenderer.invoke("get-input-devices");
|
const devices = await ipcRenderer.invoke('get-input-devices');
|
||||||
|
|
||||||
if (devices.length === 0) {
|
if (devices.length === 0) {
|
||||||
inputDeviceSelect.innerHTML = "<option>No microphones found</option>";
|
inputDeviceSelect.innerHTML = '<option>No microphones found</option>';
|
||||||
} else {
|
} else {
|
||||||
inputDeviceSelect.innerHTML = devices
|
inputDeviceSelect.innerHTML = devices
|
||||||
.map(
|
.map(
|
||||||
(device) => `<option value="${device.id}">${device.name}</option>`
|
(device) => `<option value="${device.id}">${device.name}</option>`,
|
||||||
)
|
)
|
||||||
.join("");
|
.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set current settings
|
// Set current settings
|
||||||
@ -44,37 +44,37 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
inputDeviceSelect.value = settings.inputDevice;
|
inputDeviceSelect.value = settings.inputDevice;
|
||||||
oscPortInput.value = settings.oscPort;
|
oscPortInput.value = settings.oscPort;
|
||||||
|
|
||||||
settingsModal.style.display = "block";
|
settingsModal.style.display = 'block';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading settings or devices:", error);
|
console.error('Error loading settings or devices:', error);
|
||||||
alert("Please grant microphone permissions to list audio devices");
|
alert('Please grant microphone permissions to list audio devices');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
restartBtn.addEventListener("click", async () => {
|
restartBtn.addEventListener('click', async () => {
|
||||||
try {
|
try {
|
||||||
await ipcRenderer.invoke("restart");
|
await ipcRenderer.invoke('restart');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error restarting:", error);
|
console.error('Error restarting:', error);
|
||||||
alert("Failed to restart Clipper");
|
alert('Failed to restart Clipper');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close settings modal
|
// Close settings modal
|
||||||
closeModalBtn.addEventListener("click", () => {
|
closeModalBtn.addEventListener('click', () => {
|
||||||
settingsModal.style.display = "none";
|
settingsModal.style.display = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Select output folder
|
// Select output folder
|
||||||
selectOutputFolderBtn.addEventListener("click", async () => {
|
selectOutputFolderBtn.addEventListener('click', async () => {
|
||||||
const folderPath = await ipcRenderer.invoke("select-output-folder");
|
const folderPath = await ipcRenderer.invoke('select-output-folder');
|
||||||
if (folderPath) {
|
if (folderPath) {
|
||||||
outputFolderInput.value = folderPath;
|
outputFolderInput.value = folderPath;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save settings
|
// Save settings
|
||||||
saveSettingsBtn.addEventListener("click", async () => {
|
saveSettingsBtn.addEventListener('click', async () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
recordingLength: parseInt(recordingLengthInput.value),
|
recordingLength: parseInt(recordingLengthInput.value),
|
||||||
oscPort: parseInt(oscPortInput.value),
|
oscPort: parseInt(oscPortInput.value),
|
||||||
@ -82,68 +82,68 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
inputDevice: inputDeviceSelect.value,
|
inputDevice: inputDeviceSelect.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
const saved = await ipcRenderer.invoke("save-settings", settings);
|
const saved = await ipcRenderer.invoke('save-settings', settings);
|
||||||
if (saved) {
|
if (saved) {
|
||||||
settingsModal.style.display = "none";
|
settingsModal.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
alert("Failed to save settings");
|
alert('Failed to save settings');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close modal if clicked outside
|
// Close modal if clicked outside
|
||||||
window.addEventListener("click", (event) => {
|
window.addEventListener('click', (event) => {
|
||||||
if (event.target === settingsModal) {
|
if (event.target === settingsModal) {
|
||||||
settingsModal.style.display = "none";
|
settingsModal.style.display = 'none';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const audioTrimmersList = document.getElementById("audio-trimmers-list");
|
const audioTrimmersList = document.getElementById('audio-trimmers-list');
|
||||||
const collectionsList = document.getElementById("collections-list");
|
const collectionsList = document.getElementById('collections-list');
|
||||||
//const currentSectionTitle = document.getElementById("current-section-title");
|
//const currentSectionTitle = document.getElementById("current-section-title");
|
||||||
|
|
||||||
// Global state to persist wavesurfer instances and trimmer states
|
// Global state to persist wavesurfer instances and trimmer states
|
||||||
const globalState = {
|
const globalState = {
|
||||||
wavesurferInstances: {},
|
wavesurferInstances: {},
|
||||||
trimmerStates: {},
|
trimmerStates: {},
|
||||||
currentSection: "untrimmed",
|
currentSection: 'untrimmed',
|
||||||
trimmerElements: {},
|
trimmerElements: {},
|
||||||
};
|
};
|
||||||
// Utility function to format time
|
// Utility function to format time
|
||||||
function formatTime(seconds) {
|
function formatTime(seconds) {
|
||||||
const minutes = Math.floor(seconds / 60);
|
const minutes = Math.floor(seconds / 60);
|
||||||
const remainingSeconds = 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
|
// Populate collections list
|
||||||
async function populateCollectionsList() {
|
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
|
// Always add Untrimmed section first
|
||||||
const untrimmedItem = document.createElement("div");
|
const untrimmedItem = document.createElement('div');
|
||||||
untrimmedItem.classList.add("collection-item");
|
untrimmedItem.classList.add('collection-item');
|
||||||
untrimmedItem.textContent = "Untrimmed";
|
untrimmedItem.textContent = 'Untrimmed';
|
||||||
untrimmedItem.dataset.collection = "untrimmed";
|
untrimmedItem.dataset.collection = 'untrimmed';
|
||||||
|
|
||||||
untrimmedItem.addEventListener("click", () => {
|
untrimmedItem.addEventListener('click', () => {
|
||||||
loadCollectionFiles("untrimmed");
|
loadCollectionFiles('untrimmed');
|
||||||
});
|
});
|
||||||
|
|
||||||
collectionsList.appendChild(untrimmedItem);
|
collectionsList.appendChild(untrimmedItem);
|
||||||
|
|
||||||
// Add other collections
|
// Add other collections
|
||||||
collections.forEach((collection) => {
|
collections.forEach((collection) => {
|
||||||
if (collection === "untrimmed") {
|
if (collection === 'untrimmed') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const collectionItem = document.createElement("div");
|
const collectionItem = document.createElement('div');
|
||||||
collectionItem.classList.add("collection-item");
|
collectionItem.classList.add('collection-item');
|
||||||
collectionItem.textContent = collection;
|
collectionItem.textContent = collection;
|
||||||
collectionItem.dataset.collection = collection;
|
collectionItem.dataset.collection = collection;
|
||||||
|
|
||||||
collectionItem.addEventListener("click", () => {
|
collectionItem.addEventListener('click', () => {
|
||||||
loadCollectionFiles(collection);
|
loadCollectionFiles(collection);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -169,18 +169,18 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reset active states
|
// Reset active states
|
||||||
document.querySelectorAll(".collection-item").forEach((el) => {
|
document.querySelectorAll('.collection-item').forEach((el) => {
|
||||||
el.classList.remove("active");
|
el.classList.remove('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set active state only for existing items
|
// Set active state only for existing items
|
||||||
const activeItem = document.querySelector(
|
const activeItem = document.querySelector(
|
||||||
`.collection-item[data-collection="${collection}"]`
|
`.collection-item[data-collection="${collection}"]`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only add active class if the item exists
|
// Only add active class if the item exists
|
||||||
if (activeItem) {
|
if (activeItem) {
|
||||||
activeItem.classList.add("active");
|
activeItem.classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update section title and global state
|
// Update section title and global state
|
||||||
@ -188,7 +188,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
globalState.currentSection = collection;
|
globalState.currentSection = collection;
|
||||||
|
|
||||||
// Load files
|
// 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
|
// Add new trimmers with saved trim information
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
@ -217,73 +217,73 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const savedTrimInfo = await ipcRenderer.invoke(
|
const savedTrimInfo = await ipcRenderer.invoke(
|
||||||
"get-trim-info",
|
'get-trim-info',
|
||||||
globalState.currentSection,
|
globalState.currentSection,
|
||||||
path.basename(filePath)
|
path.basename(filePath),
|
||||||
);
|
);
|
||||||
// Create trimmer container
|
// Create trimmer container
|
||||||
const trimmerContainer = document.createElement("div");
|
const trimmerContainer = document.createElement('div');
|
||||||
trimmerContainer.classList.add("audio-trimmer-item");
|
trimmerContainer.classList.add('audio-trimmer-item');
|
||||||
trimmerContainer.dataset.filepath = filePath;
|
trimmerContainer.dataset.filepath = filePath;
|
||||||
|
|
||||||
// Create header with title and controls
|
// Create header with title and controls
|
||||||
const trimmerHeader = document.createElement("div");
|
const trimmerHeader = document.createElement('div');
|
||||||
trimmerHeader.classList.add("audio-trimmer-header");
|
trimmerHeader.classList.add('audio-trimmer-header');
|
||||||
|
|
||||||
// Title container
|
// Title container
|
||||||
const titleContainer = document.createElement("div");
|
const titleContainer = document.createElement('div');
|
||||||
titleContainer.classList.add("audio-trimmer-title-container");
|
titleContainer.classList.add('audio-trimmer-title-container');
|
||||||
|
|
||||||
if (savedTrimInfo.title) {
|
if (savedTrimInfo.title) {
|
||||||
// Title
|
// Title
|
||||||
const title = document.createElement("div");
|
const title = document.createElement('div');
|
||||||
title.classList.add("audio-trimmer-title");
|
title.classList.add('audio-trimmer-title');
|
||||||
title.textContent = savedTrimInfo.title;
|
title.textContent = savedTrimInfo.title;
|
||||||
titleContainer.appendChild(title);
|
titleContainer.appendChild(title);
|
||||||
|
|
||||||
// Filename
|
// Filename
|
||||||
const fileName = document.createElement("div");
|
const fileName = document.createElement('div');
|
||||||
fileName.classList.add("audio-trimmer-filename");
|
fileName.classList.add('audio-trimmer-filename');
|
||||||
fileName.textContent = path.basename(filePath);
|
fileName.textContent = path.basename(filePath);
|
||||||
titleContainer.appendChild(fileName);
|
titleContainer.appendChild(fileName);
|
||||||
} else {
|
} else {
|
||||||
// Title (using filename if no custom title)
|
// Title (using filename if no custom title)
|
||||||
const title = document.createElement("div");
|
const title = document.createElement('div');
|
||||||
title.classList.add("audio-trimmer-title");
|
title.classList.add('audio-trimmer-title');
|
||||||
title.textContent = path.basename(filePath);
|
title.textContent = path.basename(filePath);
|
||||||
titleContainer.appendChild(title);
|
titleContainer.appendChild(title);
|
||||||
|
|
||||||
// Filename
|
// Filename
|
||||||
const fileName = document.createElement("div");
|
const fileName = document.createElement('div');
|
||||||
fileName.classList.add("audio-trimmer-filename");
|
fileName.classList.add('audio-trimmer-filename');
|
||||||
fileName.textContent = "hidden";
|
fileName.textContent = 'hidden';
|
||||||
fileName.style.opacity = 0;
|
fileName.style.opacity = 0;
|
||||||
titleContainer.appendChild(fileName);
|
titleContainer.appendChild(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Controls container
|
// Controls container
|
||||||
const controlsContainer = document.createElement("div");
|
const controlsContainer = document.createElement('div');
|
||||||
controlsContainer.classList.add("audio-trimmer-controls");
|
controlsContainer.classList.add('audio-trimmer-controls');
|
||||||
|
|
||||||
// Play/Pause and Save buttons
|
// Play/Pause and Save buttons
|
||||||
const playPauseBtn = document.createElement("button");
|
const playPauseBtn = document.createElement('button');
|
||||||
playPauseBtn.classList.add("play-pause-btn");
|
playPauseBtn.classList.add('play-pause-btn');
|
||||||
playPauseBtn.innerHTML = `
|
playPauseBtn.innerHTML = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
<path d="M8 5v14l11-7z"/>
|
<path d="M8 5v14l11-7z"/>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const saveTrimButton = document.createElement("button");
|
const saveTrimButton = document.createElement('button');
|
||||||
saveTrimButton.classList.add("save-trim");
|
saveTrimButton.classList.add('save-trim');
|
||||||
saveTrimButton.innerHTML = `
|
saveTrimButton.innerHTML = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<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"/>
|
<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>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const deletebutton = document.createElement("button");
|
const deletebutton = document.createElement('button');
|
||||||
deletebutton.classList.add("play-pause-btn");
|
deletebutton.classList.add('play-pause-btn');
|
||||||
deletebutton.innerHTML = `
|
deletebutton.innerHTML = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<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"/>
|
<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);
|
trimmerContainer.appendChild(trimmerHeader);
|
||||||
|
|
||||||
// Waveform container
|
// Waveform container
|
||||||
const waveformContainer = document.createElement("div");
|
const waveformContainer = document.createElement('div');
|
||||||
waveformContainer.classList.add("waveform-container");
|
waveformContainer.classList.add('waveform-container');
|
||||||
const waveformId = `waveform-${path.basename(
|
const waveformId = `waveform-${path.basename(
|
||||||
filePath,
|
filePath,
|
||||||
path.extname(filePath)
|
path.extname(filePath),
|
||||||
)}`;
|
)}`;
|
||||||
waveformContainer.innerHTML = `
|
waveformContainer.innerHTML = `
|
||||||
<div id="${waveformId}" class="waveform"></div>
|
<div id="${waveformId}" class="waveform"></div>
|
||||||
@ -312,8 +312,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
trimmerContainer.appendChild(waveformContainer);
|
trimmerContainer.appendChild(waveformContainer);
|
||||||
|
|
||||||
// Time displays
|
// Time displays
|
||||||
const timeInfo = document.createElement("div");
|
const timeInfo = document.createElement('div');
|
||||||
timeInfo.classList.add("trim-info");
|
timeInfo.classList.add('trim-info');
|
||||||
timeInfo.innerHTML = `
|
timeInfo.innerHTML = `
|
||||||
<div class="trim-time">
|
<div class="trim-time">
|
||||||
<span>Start: </span>
|
<span>Start: </span>
|
||||||
@ -324,59 +324,58 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
<span class="trim-end-time">0:00</span>
|
<span class="trim-end-time">0:00</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
// const zoomContainer = document.createElement('div');
|
// const zoomContainer = document.createElement('div');
|
||||||
// zoomContainer.className = 'zoom-controls';
|
// zoomContainer.className = 'zoom-controls';
|
||||||
// zoomContainer.innerHTML = `
|
// zoomContainer.innerHTML = `
|
||||||
// <button class="zoom-in">+</button>
|
// <button class="zoom-in">+</button>
|
||||||
// <button class="zoom-out">-</button>
|
// <button class="zoom-out">-</button>
|
||||||
// <input type="range" min="1" max="200" value="100" class="zoom-slider">
|
// <input type="range" min="1" max="200" value="100" class="zoom-slider">
|
||||||
// `;
|
// `;
|
||||||
// timeInfo.appendChild(zoomContainer);
|
// timeInfo.appendChild(zoomContainer);
|
||||||
|
|
||||||
// const zoomInBtn = zoomContainer.querySelector('.zoom-in');
|
// const zoomInBtn = zoomContainer.querySelector('.zoom-in');
|
||||||
// const zoomOutBtn = zoomContainer.querySelector('.zoom-out');
|
// const zoomOutBtn = zoomContainer.querySelector('.zoom-out');
|
||||||
// const zoomSlider = zoomContainer.querySelector('.zoom-slider');
|
// const zoomSlider = zoomContainer.querySelector('.zoom-slider');
|
||||||
|
|
||||||
// // Zoom functionality
|
// // Zoom functionality
|
||||||
// const updateZoom = (zoomLevel) => {
|
// const updateZoom = (zoomLevel) => {
|
||||||
// // Get the current scroll position and width
|
// // Get the current scroll position and width
|
||||||
// const scrollContainer = wavesurfer.container.querySelector('wave');
|
// const scrollContainer = wavesurfer.container.querySelector('wave');
|
||||||
// const currentScroll = scrollContainer.scrollLeft;
|
// const currentScroll = scrollContainer.scrollLeft;
|
||||||
// const containerWidth = scrollContainer.clientWidth;
|
// const containerWidth = scrollContainer.clientWidth;
|
||||||
|
|
||||||
|
|
||||||
// // Calculate the center point of the current view
|
|
||||||
// //const centerTime = wavesurfer.getCurrentTime();
|
|
||||||
|
|
||||||
// // Apply zoom
|
|
||||||
// wavesurfer.zoom(zoomLevel);
|
|
||||||
|
|
||||||
// // Recalculate scroll to keep the center point in view
|
|
||||||
// const newDuration = wavesurfer.getDuration();
|
|
||||||
// const pixelsPerSecond = wavesurfer.drawer.width / newDuration;
|
|
||||||
// const centerPixel = centerTime * pixelsPerSecond;
|
|
||||||
|
|
||||||
// // Adjust scroll to keep the center point in the same relative position
|
|
||||||
// const newScrollLeft = centerPixel - (containerWidth / 2);
|
|
||||||
// scrollContainer.scrollLeft = Math.max(0, newScrollLeft);
|
|
||||||
// console.log(currentScroll, newScrollLeft);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// zoomInBtn.addEventListener('click', () => {
|
// // Calculate the center point of the current view
|
||||||
// const currentZoom = parseInt(zoomSlider.value);
|
// //const centerTime = wavesurfer.getCurrentTime();
|
||||||
// zoomSlider.value = Math.min(currentZoom + 20, 200);
|
|
||||||
// updateZoom(zoomSlider.value);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// zoomOutBtn.addEventListener('click', () => {
|
// // Apply zoom
|
||||||
// const currentZoom = parseInt(zoomSlider.value);
|
// wavesurfer.zoom(zoomLevel);
|
||||||
// zoomSlider.value = Math.max(currentZoom - 20, 1);
|
|
||||||
// updateZoom(zoomSlider.value);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// zoomSlider.addEventListener('input', (e) => {
|
// // Recalculate scroll to keep the center point in view
|
||||||
// updateZoom(e.target.value);
|
// 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);
|
trimmerContainer.appendChild(timeInfo);
|
||||||
|
|
||||||
@ -386,25 +385,25 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
|
|
||||||
// Determine the file to load (original or current)
|
// Determine the file to load (original or current)
|
||||||
const fileToLoad =
|
const fileToLoad =
|
||||||
section === "untrimmed"
|
section === 'untrimmed'
|
||||||
? filePath
|
? filePath
|
||||||
: globalState.trimmerStates[filePath]?.originalPath || filePath;
|
: globalState.trimmerStates[filePath]?.originalPath || filePath;
|
||||||
|
|
||||||
// Setup wavesurfer
|
// Setup wavesurfer
|
||||||
const wavesurfer = WaveSurfer.create({
|
const wavesurfer = WaveSurfer.create({
|
||||||
container: `#${waveformId}`,
|
container: `#${waveformId}`,
|
||||||
waveColor: "#ccb1ff",
|
waveColor: '#ccb1ff',
|
||||||
progressColor: "#6e44ba",
|
progressColor: '#6e44ba',
|
||||||
responsive: true,
|
responsive: true,
|
||||||
height: 100,
|
height: 100,
|
||||||
hideScrollbar: true,
|
hideScrollbar: true,
|
||||||
// barWidth: 2,
|
// barWidth: 2,
|
||||||
// barRadius: 3,
|
// barRadius: 3,
|
||||||
cursorWidth: 1,
|
cursorWidth: 1,
|
||||||
backend: "WebAudio",
|
backend: 'WebAudio',
|
||||||
plugins: [
|
plugins: [
|
||||||
RegionsPlugin.create({
|
RegionsPlugin.create({
|
||||||
color: "rgba(132, 81, 224, 0.3)",
|
color: 'rgba(132, 81, 224, 0.3)',
|
||||||
drag: false,
|
drag: false,
|
||||||
resize: true,
|
resize: true,
|
||||||
dragSelection: {
|
dragSelection: {
|
||||||
@ -420,7 +419,6 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Store wavesurfer instance in global state
|
// Store wavesurfer instance in global state
|
||||||
globalState.wavesurferInstances[filePath] = wavesurfer;
|
globalState.wavesurferInstances[filePath] = wavesurfer;
|
||||||
|
|
||||||
@ -433,8 +431,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
regionEnd: undefined,
|
regionEnd: undefined,
|
||||||
originalPath: fileToLoad,
|
originalPath: fileToLoad,
|
||||||
};
|
};
|
||||||
const startTimeDisplay = timeInfo.querySelector(".trim-start-time");
|
const startTimeDisplay = timeInfo.querySelector('.trim-start-time');
|
||||||
const endTimeDisplay = timeInfo.querySelector(".trim-end-time");
|
const endTimeDisplay = timeInfo.querySelector('.trim-end-time');
|
||||||
|
|
||||||
// Load audio file
|
// Load audio file
|
||||||
wavesurfer.load(`file://${fileToLoad}`);
|
wavesurfer.load(`file://${fileToLoad}`);
|
||||||
@ -461,20 +459,20 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// When audio is ready
|
// When audio is ready
|
||||||
wavesurfer.on("ready", async () => {
|
wavesurfer.on('ready', async () => {
|
||||||
const instanceState = globalState.trimmerStates[filePath];
|
const instanceState = globalState.trimmerStates[filePath];
|
||||||
|
|
||||||
// Set trim times based on saved state or full duration
|
// Set trim times based on saved state or full duration
|
||||||
if(instanceState.trimStart){
|
if (instanceState.trimStart) {
|
||||||
// Create initial region covering trim or full duration
|
// Create initial region covering trim or full duration
|
||||||
wavesurfer.clearRegions();
|
wavesurfer.clearRegions();
|
||||||
const region = wavesurfer.addRegion({
|
const region = wavesurfer.addRegion({
|
||||||
start: instanceState.trimStart,
|
start: instanceState.trimStart,
|
||||||
end: instanceState.trimEnd,
|
end: instanceState.trimEnd,
|
||||||
color: "rgba(132, 81, 224, 0.3)",
|
color: 'rgba(132, 81, 224, 0.3)',
|
||||||
drag: false,
|
drag: false,
|
||||||
resize: true,
|
resize: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
instanceState.trimStart = instanceState.trimStart || 0;
|
instanceState.trimStart = instanceState.trimStart || 0;
|
||||||
instanceState.trimEnd = instanceState.trimEnd || wavesurfer.getDuration();
|
instanceState.trimEnd = instanceState.trimEnd || wavesurfer.getDuration();
|
||||||
@ -483,19 +481,17 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
startTimeDisplay.textContent = formatTime(instanceState.trimStart);
|
startTimeDisplay.textContent = formatTime(instanceState.trimStart);
|
||||||
endTimeDisplay.textContent = formatTime(instanceState.trimEnd);
|
endTimeDisplay.textContent = formatTime(instanceState.trimEnd);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Store region details
|
// Store region details
|
||||||
instanceState.regionStart = instanceState.trimStart;
|
instanceState.regionStart = instanceState.trimStart;
|
||||||
instanceState.regionEnd = instanceState.trimEnd;
|
instanceState.regionEnd = instanceState.trimEnd;
|
||||||
|
|
||||||
// Listen for region updates
|
// 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
|
// Ensure the region doesn't exceed audio duration
|
||||||
instanceState.trimStart = Math.max(0, updatedRegion.start);
|
instanceState.trimStart = Math.max(0, updatedRegion.start);
|
||||||
instanceState.trimEnd = Math.min(
|
instanceState.trimEnd = Math.min(
|
||||||
wavesurfer.getDuration(),
|
wavesurfer.getDuration(),
|
||||||
updatedRegion.end
|
updatedRegion.end,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update time displays
|
// Update time displays
|
||||||
@ -516,7 +512,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle region creation
|
// Handle region creation
|
||||||
wavesurfer.on("region-created", (newRegion) => {
|
wavesurfer.on('region-created', (newRegion) => {
|
||||||
// Remove all other regions
|
// Remove all other regions
|
||||||
Object.keys(wavesurfer.regions.list).forEach((id) => {
|
Object.keys(wavesurfer.regions.list).forEach((id) => {
|
||||||
if (wavesurfer.regions.list[id] !== newRegion) {
|
if (wavesurfer.regions.list[id] !== newRegion) {
|
||||||
@ -526,7 +522,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Reset to trim start when audio finishes
|
// Reset to trim start when audio finishes
|
||||||
wavesurfer.on("finish", () => {
|
wavesurfer.on('finish', () => {
|
||||||
wavesurfer.setCurrentTime(instanceState.trimStart);
|
wavesurfer.setCurrentTime(instanceState.trimStart);
|
||||||
playPauseBtn.innerHTML = `
|
playPauseBtn.innerHTML = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<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
|
// Save trimmed audio functionality
|
||||||
saveTrimButton.addEventListener("click", async () => {
|
saveTrimButton.addEventListener('click', async () => {
|
||||||
try {
|
try {
|
||||||
// Get current collections
|
// 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
|
// Create a dialog to select or create a collection
|
||||||
const dialogHtml = `
|
const dialogHtml = `
|
||||||
@ -561,13 +557,13 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
<select id="existing-collections" style="width: 100%; margin-top: 10px;">
|
<select id="existing-collections" style="width: 100%; margin-top: 10px;">
|
||||||
${collections
|
${collections
|
||||||
.map((col) =>
|
.map((col) =>
|
||||||
col === "untrimmed"
|
col === 'untrimmed'
|
||||||
? ""
|
? ''
|
||||||
: `<option value="${col}" ${
|
: `<option value="${col}" ${
|
||||||
globalState.currentSection === col ? "selected" : ""
|
globalState.currentSection === col ? 'selected' : ''
|
||||||
}>${col}</option>`
|
}>${col}</option>`,
|
||||||
)
|
)
|
||||||
.join("")}
|
.join('')}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div style="margin-top: 10px; display: flex; justify-content: space-between;">
|
<div style="margin-top: 10px; display: flex; justify-content: space-between;">
|
||||||
@ -578,69 +574,67 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Create dialog overlay
|
// Create dialog overlay
|
||||||
const overlay = document.createElement("div");
|
const overlay = document.createElement('div');
|
||||||
overlay.style.position = "fixed";
|
overlay.style.position = 'fixed';
|
||||||
overlay.style.top = "0";
|
overlay.style.top = '0';
|
||||||
overlay.style.left = "0";
|
overlay.style.left = '0';
|
||||||
overlay.style.width = "100%";
|
overlay.style.width = '100%';
|
||||||
overlay.style.height = "100%";
|
overlay.style.height = '100%';
|
||||||
overlay.style.backgroundColor = "rgba(0,0,0,0.5)";
|
overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
|
||||||
overlay.style.zIndex = "999";
|
overlay.style.zIndex = '999';
|
||||||
overlay.innerHTML = dialogHtml;
|
overlay.innerHTML = dialogHtml;
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
const existingCollectionsSelect = overlay.querySelector(
|
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(
|
const createCollectionBtn = overlay.querySelector(
|
||||||
"#create-collection-btn"
|
'#create-collection-btn',
|
||||||
);
|
);
|
||||||
const saveToCollectionBtn = overlay.querySelector(
|
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) {
|
if (savedTrimInfo.title) {
|
||||||
newSaveTitleInput.value = savedTrimInfo.title;
|
newSaveTitleInput.value = savedTrimInfo.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to collection
|
// Save to collection
|
||||||
saveToCollectionBtn.addEventListener("click", async () => {
|
saveToCollectionBtn.addEventListener('click', async () => {
|
||||||
const newTitle = document
|
const newTitle = document
|
||||||
.getElementById("new-save-title")
|
.getElementById('new-save-title')
|
||||||
.value.trim();
|
.value.trim();
|
||||||
const settings = await ipcRenderer.invoke("load-settings");
|
const settings = await ipcRenderer.invoke('load-settings');
|
||||||
|
|
||||||
const selectedCollection = existingCollectionsSelect.value;
|
const selectedCollection = existingCollectionsSelect.value;
|
||||||
|
|
||||||
if (!selectedCollection) {
|
if (!selectedCollection) {
|
||||||
alert("Please select or create a collection");
|
alert('Please select or create a collection');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ipcRenderer.invoke(
|
await ipcRenderer.invoke(
|
||||||
"delete-old-file",
|
'delete-old-file',
|
||||||
settings.outputFolder,
|
settings.outputFolder,
|
||||||
globalState.currentSection,
|
globalState.currentSection,
|
||||||
savedTrimInfo.title
|
savedTrimInfo.title,
|
||||||
);
|
);
|
||||||
await ipcRenderer.invoke(
|
await ipcRenderer.invoke(
|
||||||
"save-trimmed-file",
|
'save-trimmed-file',
|
||||||
path.basename(filePath),
|
path.basename(filePath),
|
||||||
globalState.currentSection,
|
globalState.currentSection,
|
||||||
selectedCollection,
|
selectedCollection,
|
||||||
instanceState.trimStart,
|
instanceState.trimStart,
|
||||||
instanceState.trimEnd,
|
instanceState.trimEnd,
|
||||||
newTitle
|
newTitle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const saveResult = await ipcRenderer.invoke(
|
const saveResult = await ipcRenderer.invoke(
|
||||||
"save-trimmed-audio",
|
'save-trimmed-audio',
|
||||||
{
|
{
|
||||||
originalFilePath: filePath,
|
originalFilePath: filePath,
|
||||||
outputFolder: settings.outputFolder,
|
outputFolder: settings.outputFolder,
|
||||||
@ -648,7 +642,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
title: newTitle,
|
title: newTitle,
|
||||||
trimStart: instanceState.trimStart,
|
trimStart: instanceState.trimStart,
|
||||||
trimEnd: instanceState.trimEnd,
|
trimEnd: instanceState.trimEnd,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (saveResult.success) {
|
if (saveResult.success) {
|
||||||
@ -667,39 +661,40 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
|
|
||||||
// Refresh the view
|
// Refresh the view
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert("Error saving file: " + error.message);
|
alert('Error saving file: ' + error.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cancel button
|
// Cancel button
|
||||||
cancelSaveBtn.addEventListener("click", () => {
|
cancelSaveBtn.addEventListener('click', () => {
|
||||||
document.body.removeChild(overlay);
|
document.body.removeChild(overlay);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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
|
// Create confirmation dialog
|
||||||
const confirmDelete =
|
const confirmDelete = confirm(
|
||||||
confirm(`Are you sure you want to delete this audio file?\nThis will remove the original file and any trimmed versions.`);
|
`Are you sure you want to delete this audio file?\nThis will remove the original file and any trimmed versions.`,
|
||||||
|
);
|
||||||
|
|
||||||
if (confirmDelete) {
|
if (confirmDelete) {
|
||||||
try {
|
try {
|
||||||
// Delete original file
|
// Delete original file
|
||||||
await ipcRenderer.invoke("delete-file", filePath);
|
await ipcRenderer.invoke('delete-file', filePath);
|
||||||
|
|
||||||
// Remove from UI
|
// Remove from UI
|
||||||
trimmerContainer.remove();
|
trimmerContainer.remove();
|
||||||
|
|
||||||
// Optional: Notify user
|
// Optional: Notify user
|
||||||
alert("File deleted successfully");
|
alert('File deleted successfully');
|
||||||
|
|
||||||
// Refresh the current section view
|
// Refresh the current section view
|
||||||
await loadCollectionFiles(globalState.currentSection);
|
await loadCollectionFiles(globalState.currentSection);
|
||||||
await populateCollectionsList();
|
await populateCollectionsList();
|
||||||
} catch (error) {
|
} 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
|
// Initial load of untrimmed files and collections
|
||||||
await loadCollectionFiles("untrimmed");
|
await loadCollectionFiles('untrimmed');
|
||||||
await populateCollectionsList();
|
await populateCollectionsList();
|
||||||
|
|
||||||
// Listen for new untrimmed files
|
// 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
|
// Refresh the untrimmed section
|
||||||
await loadCollectionFiles("untrimmed");
|
await loadCollectionFiles('untrimmed');
|
||||||
await populateCollectionsList();
|
await populateCollectionsList();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -728,8 +723,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
|
|
||||||
// Add collection button handler
|
// Add collection button handler
|
||||||
document
|
document
|
||||||
.getElementById("add-collection-btn")
|
.getElementById('add-collection-btn')
|
||||||
.addEventListener("click", async () => {
|
.addEventListener('click', async () => {
|
||||||
try {
|
try {
|
||||||
// Create a dialog to input new collection name
|
// Create a dialog to input new collection name
|
||||||
const dialogHtml = `
|
const dialogHtml = `
|
||||||
@ -760,32 +755,32 @@ document
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Create dialog overlay
|
// Create dialog overlay
|
||||||
const overlay = document.createElement("div");
|
const overlay = document.createElement('div');
|
||||||
overlay.style.position = "fixed";
|
overlay.style.position = 'fixed';
|
||||||
overlay.style.top = "0";
|
overlay.style.top = '0';
|
||||||
overlay.style.left = "0";
|
overlay.style.left = '0';
|
||||||
overlay.style.width = "100%";
|
overlay.style.width = '100%';
|
||||||
overlay.style.height = "100%";
|
overlay.style.height = '100%';
|
||||||
overlay.style.backgroundColor = "rgba(0,0,0,0.5)";
|
overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
|
||||||
overlay.style.zIndex = "999";
|
overlay.style.zIndex = '999';
|
||||||
overlay.innerHTML = dialogHtml;
|
overlay.innerHTML = dialogHtml;
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
const newCollectionInput = overlay.querySelector("#new-collection-input");
|
const newCollectionInput = overlay.querySelector('#new-collection-input');
|
||||||
const createCollectionConfirmBtn = overlay.querySelector(
|
const createCollectionConfirmBtn = overlay.querySelector(
|
||||||
"#create-collection-confirm-btn"
|
'#create-collection-confirm-btn',
|
||||||
);
|
);
|
||||||
const createCollectionCancelBtn = overlay.querySelector(
|
const createCollectionCancelBtn = overlay.querySelector(
|
||||||
"#create-collection-cancel-btn"
|
'#create-collection-cancel-btn',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create collection when confirm button is clicked
|
// Create collection when confirm button is clicked
|
||||||
createCollectionConfirmBtn.addEventListener("click", async () => {
|
createCollectionConfirmBtn.addEventListener('click', async () => {
|
||||||
const newCollectionName = newCollectionInput.value.trim();
|
const newCollectionName = newCollectionInput.value.trim();
|
||||||
|
|
||||||
if (newCollectionName) {
|
if (newCollectionName) {
|
||||||
try {
|
try {
|
||||||
await ipcRenderer.invoke("add-new-collection", newCollectionName);
|
await ipcRenderer.invoke('add-new-collection', newCollectionName);
|
||||||
|
|
||||||
// Remove dialog
|
// Remove dialog
|
||||||
document.body.removeChild(overlay);
|
document.body.removeChild(overlay);
|
||||||
@ -794,30 +789,30 @@ document
|
|||||||
await populateCollectionsList();
|
await populateCollectionsList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Show error in the dialog
|
// Show error in the dialog
|
||||||
const errorDiv = document.createElement("div");
|
const errorDiv = document.createElement('div');
|
||||||
errorDiv.textContent = error.message;
|
errorDiv.textContent = error.message;
|
||||||
errorDiv.style.color = "red";
|
errorDiv.style.color = 'red';
|
||||||
errorDiv.style.marginTop = "10px";
|
errorDiv.style.marginTop = '10px';
|
||||||
overlay.querySelector("div").appendChild(errorDiv);
|
overlay.querySelector('div').appendChild(errorDiv);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Show error if input is empty
|
// Show error if input is empty
|
||||||
const errorDiv = document.createElement("div");
|
const errorDiv = document.createElement('div');
|
||||||
errorDiv.textContent = "Collection name cannot be empty";
|
errorDiv.textContent = 'Collection name cannot be empty';
|
||||||
errorDiv.style.color = "red";
|
errorDiv.style.color = 'red';
|
||||||
errorDiv.style.marginTop = "10px";
|
errorDiv.style.marginTop = '10px';
|
||||||
overlay.querySelector("div").appendChild(errorDiv);
|
overlay.querySelector('div').appendChild(errorDiv);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cancel button closes the dialog
|
// Cancel button closes the dialog
|
||||||
createCollectionCancelBtn.addEventListener("click", () => {
|
createCollectionCancelBtn.addEventListener('click', () => {
|
||||||
document.body.removeChild(overlay);
|
document.body.removeChild(overlay);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Focus the input when dialog opens
|
// Focus the input when dialog opens
|
||||||
newCollectionInput.focus();
|
newCollectionInput.focus();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating new collection dialog:", error);
|
console.error('Error creating new collection dialog:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
49
electron-ui/src/renderer/App.css
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
@import "tailwindcss";
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
/*
|
||||||
|
* @NOTE: Prepend a `~` to css file paths that are in your node_modules
|
||||||
|
* See https://github.com/webpack-contrib/sass-loader#imports
|
||||||
|
*/
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-midnight: #1E1E1E;
|
||||||
|
--color-plum: #4f3186;
|
||||||
|
--color-offwhite: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #4f3186;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
59
electron-ui/src/renderer/App.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { MemoryRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
|
// import 'tailwindcss/tailwind.css';
|
||||||
|
import icon from '../../assets/icon.svg';
|
||||||
|
import './App.css';
|
||||||
|
import AudioTrimmer from './components/AudioTrimer';
|
||||||
|
|
||||||
|
function Hello() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen min-w-screen flex flex-col items-center justify-center bg-midnight text-offwhite">
|
||||||
|
{/* <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 className="bg-midnight min-w-screen">
|
||||||
|
<AudioTrimmer
|
||||||
|
title="audio_capture_20251206_123108.wav"
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
121
electron-ui/src/renderer/components/AudioTrimer.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import React, {
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
import Regions from 'wavesurfer.js/dist/plugins/regions.esm.js';
|
||||||
|
import { useWavesurfer } from '@wavesurfer/react';
|
||||||
|
|
||||||
|
export interface AudioTrimmerProps {
|
||||||
|
filePath: string;
|
||||||
|
section: string;
|
||||||
|
title?: string;
|
||||||
|
trimStart?: number;
|
||||||
|
trimEnd?: number;
|
||||||
|
onSave?: (trimStart: number, trimEnd: number, title?: string) => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AudioTrimmer({
|
||||||
|
filePath,
|
||||||
|
section,
|
||||||
|
title,
|
||||||
|
trimStart,
|
||||||
|
trimEnd,
|
||||||
|
onSave,
|
||||||
|
onDelete,
|
||||||
|
}: AudioTrimmerProps) {
|
||||||
|
const [blobUrl, setBlobUrl] = useState<string | undefined>(undefined);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
|
const plugins = useMemo(() => [Regions.create()], []);
|
||||||
|
|
||||||
|
const { wavesurfer, isReady, isPlaying } = useWavesurfer({
|
||||||
|
container: containerRef,
|
||||||
|
height: 100,
|
||||||
|
waveColor: '#ccb1ff',
|
||||||
|
progressColor: '#6e44ba',
|
||||||
|
url: blobUrl,
|
||||||
|
plugins,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onRegionCreated = useCallback(
|
||||||
|
(newRegion: any) => {
|
||||||
|
if (wavesurfer === null) return;
|
||||||
|
const allRegions = plugins[0].getRegions();
|
||||||
|
allRegions.forEach((region) => {
|
||||||
|
if (region.id !== newRegion.id) {
|
||||||
|
region.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[plugins, wavesurfer],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('ready, setting up regions plugin', wavesurfer);
|
||||||
|
if (trimStart !== undefined && trimEnd !== undefined) {
|
||||||
|
plugins[0].addRegion({
|
||||||
|
start: trimStart,
|
||||||
|
end: trimEnd,
|
||||||
|
color: 'rgba(132, 81, 224, 0.3)',
|
||||||
|
drag: false,
|
||||||
|
resize: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins[0].enableDragSelection({
|
||||||
|
color: 'rgba(132, 81, 224, 0.3)',
|
||||||
|
});
|
||||||
|
plugins[0].on('region-created', onRegionCreated);
|
||||||
|
}, [isReady, plugins, wavesurfer, onRegionCreated, trimStart, trimEnd]);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchAudio();
|
||||||
|
return () => {
|
||||||
|
if (url) URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
}, [filePath]);
|
||||||
|
|
||||||
|
const onPlayPause = () => {
|
||||||
|
if (wavesurfer === null) return;
|
||||||
|
if (isPlaying) {
|
||||||
|
wavesurfer.pause();
|
||||||
|
} else {
|
||||||
|
const allRegions = plugins[0].getRegions();
|
||||||
|
if (allRegions.length > 0) {
|
||||||
|
wavesurfer.play(allRegions[0].start, allRegions[0].end);
|
||||||
|
} else {
|
||||||
|
wavesurfer.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="shadow-[0_4px_6px_rgba(0,0,0,0.5)] m-2 p-4 rounded-lg bg-darkDrop">
|
||||||
|
<div>
|
||||||
|
<text className="m-2 font-bold text-lg">{title}</text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-[100%] m-2 ">
|
||||||
|
<div ref={containerRef} />
|
||||||
|
<button type="button" onClick={onPlayPause}>
|
||||||
|
{isPlaying ? 'Pause' : 'Play'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
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
@ -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
@ -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
19
electron-ui/tailwind.config.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import colors from 'tailwindcss/colors';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
purge: [],
|
||||||
|
darkMode: false, // or 'media' or 'class'
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
darkDrop: '#1e1e1e',
|
||||||
|
sky: colors.sky,
|
||||||
|
cyan: colors.cyan,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
18
electron-ui/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"incremental": true,
|
||||||
|
"target": "es2022",
|
||||||
|
"module": "node16",
|
||||||
|
"lib": ["dom", "es2022"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"moduleResolution": "node16",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"outDir": ".erb/dll"
|
||||||
|
},
|
||||||
|
"exclude": ["test", "release/build", "release/app/dist", ".erb/dll"]
|
||||||
|
}
|
||||||