draggables, ipc stuff
This commit is contained in:
@ -1,121 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,138 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
71
electron-ui/package-lock.json
generated
71
electron-ui/package-lock.json
generated
@ -8,6 +8,9 @@
|
|||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@electron/notarize": "^3.0.0",
|
"@electron/notarize": "^3.0.0",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
@ -2104,6 +2107,73 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dnd-kit/accessibility": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/core": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/modifiers": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.0",
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/sortable": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.0",
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/utilities": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@electron/asar": {
|
"node_modules/@electron/asar": {
|
||||||
"version": "3.4.1",
|
"version": "3.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
|
||||||
@ -22969,7 +23039,6 @@
|
|||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/tsutils": {
|
"node_modules/tsutils": {
|
||||||
|
|||||||
@ -101,6 +101,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@electron/notarize": "^3.0.0",
|
"@electron/notarize": "^3.0.0",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
|
|||||||
12
electron-ui/src/ipc/audio/channels.ts
Normal file
12
electron-ui/src/ipc/audio/channels.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
const AudioChannels = {
|
||||||
|
LOAD_AUDIO_BUFFER: 'audio:loadAudioBuffer',
|
||||||
|
TRIM_FILE: 'audio:trimFile',
|
||||||
|
DELETE_FILE: 'audio:deleteFile',
|
||||||
|
GET_COLLECTION_METADATA: 'audio:getCollectionMetadata',
|
||||||
|
SET_COLLECTION_METADATA: 'audio:setCollectionMetadata',
|
||||||
|
ADD_COLLECTION: 'audio:addCollection',
|
||||||
|
REMOVE_COLLECTION: 'audio:removeCollection',
|
||||||
|
GET_COLLECTIONS: 'audio:getCollections',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default AudioChannels;
|
||||||
18
electron-ui/src/ipc/audio/main.ts
Normal file
18
electron-ui/src/ipc/audio/main.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { ipcMain } from 'electron';
|
||||||
|
import fs from 'fs';
|
||||||
|
import AudioChannels from './channels';
|
||||||
|
import { LoadAudioBufferArgs, LoadAudioBufferResult } from './types';
|
||||||
|
|
||||||
|
export default function registerAudioIpcHandlers() {
|
||||||
|
ipcMain.handle(
|
||||||
|
AudioChannels.LOAD_AUDIO_BUFFER,
|
||||||
|
async (_, args: LoadAudioBufferArgs): Promise<LoadAudioBufferResult> => {
|
||||||
|
try {
|
||||||
|
const buffer = await fs.promises.readFile(args.filePath);
|
||||||
|
return { buffer };
|
||||||
|
} catch (err: any) {
|
||||||
|
return { error: err.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
32
electron-ui/src/ipc/audio/types.ts
Normal file
32
electron-ui/src/ipc/audio/types.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
export interface LoadAudioBufferArgs {
|
||||||
|
filePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadAudioBufferResult {
|
||||||
|
buffer?: Buffer;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PlaybackType {
|
||||||
|
PlayStop = 'playStop',
|
||||||
|
PlayOverlap = 'playOverlap',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClipMetadata {
|
||||||
|
name: string;
|
||||||
|
filePath: string;
|
||||||
|
volume: number;
|
||||||
|
startTime: number | undefined;
|
||||||
|
endTime: number | undefined;
|
||||||
|
playbackType: PlaybackType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CollectionMetadata {
|
||||||
|
name: string;
|
||||||
|
clips: ClipMetadata[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetadataFile {
|
||||||
|
uncategorizedClips: ClipMetadata[];
|
||||||
|
collections: CollectionMetadata[];
|
||||||
|
}
|
||||||
8
electron-ui/src/ipc/settings/channels.ts
Normal file
8
electron-ui/src/ipc/settings/channels.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const SettingsChannels = {
|
||||||
|
GET_DEFAULTS: 'settings:get-defaults',
|
||||||
|
GET_SETTINGS: 'settings:get-settings',
|
||||||
|
SET_SETTINGS: 'settings:set-settings',
|
||||||
|
GET_INPUT_DEVICES: 'settings:get-input-devices',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default SettingsChannels;
|
||||||
@ -15,6 +15,7 @@ import { autoUpdater } from 'electron-updater';
|
|||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
import MenuBuilder from './menu';
|
import MenuBuilder from './menu';
|
||||||
import { resolveHtmlPath } from './util';
|
import { resolveHtmlPath } from './util';
|
||||||
|
import registerFileIpcHandlers from '../ipc/audio/main';
|
||||||
|
|
||||||
class AppUpdater {
|
class AppUpdater {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -108,17 +109,7 @@ const createWindow = async () => {
|
|||||||
return { action: 'deny' };
|
return { action: 'deny' };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('load-audio-buffer', async (event, filePath) => {
|
registerFileIpcHandlers();
|
||||||
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
|
// Remove this if your app does not use auto updates
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
new AppUpdater();
|
new AppUpdater();
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
// Disable no-unused-vars, broken for spread args
|
// Disable no-unused-vars, broken for spread args
|
||||||
/* eslint no-unused-vars: off */
|
/* eslint no-unused-vars: off */
|
||||||
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
|
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
|
||||||
|
import FileChannels from '../ipc/audio/channels';
|
||||||
|
import { LoadAudioBufferArgs, ReadTextArgs } from '../ipc/audio/types';
|
||||||
|
import AudioChannels from '../ipc/audio/channels';
|
||||||
|
// import '../ipc/file/preload'; // Import file API preload to ensure it runs and exposes the API
|
||||||
|
|
||||||
export type Channels = 'ipc-example';
|
export type Channels = 'ipc-example';
|
||||||
|
|
||||||
@ -22,11 +26,27 @@ const electronHandler = {
|
|||||||
ipcRenderer.once(channel, (_event, ...args) => func(...args));
|
ipcRenderer.once(channel, (_event, ...args) => func(...args));
|
||||||
},
|
},
|
||||||
|
|
||||||
loadAudioBuffer: (filePath: string) =>
|
invoke: (event: string, ...args: unknown[]) =>
|
||||||
ipcRenderer.invoke('load-audio-buffer', filePath),
|
ipcRenderer.invoke(event, ...args),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electron', electronHandler);
|
contextBridge.exposeInMainWorld('electron', electronHandler);
|
||||||
|
|
||||||
export type ElectronHandler = typeof electronHandler;
|
export type ElectronHandler = typeof electronHandler;
|
||||||
|
|
||||||
|
const audioHandler = {
|
||||||
|
loadAudioBuffer: (filePath: string) =>
|
||||||
|
ipcRenderer.invoke(AudioChannels.LOAD_AUDIO_BUFFER, {
|
||||||
|
filePath,
|
||||||
|
} satisfies LoadAudioBufferArgs),
|
||||||
|
|
||||||
|
readText: (filePath: string) =>
|
||||||
|
ipcRenderer.invoke(AudioChannels.READ_TEXT, {
|
||||||
|
filePath,
|
||||||
|
} satisfies ReadTextArgs),
|
||||||
|
};
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('audio', audioHandler);
|
||||||
|
|
||||||
|
export type AudioHandler = typeof audioHandler;
|
||||||
|
|||||||
@ -548,7 +548,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
box-shadow: 0 4px 2px rgba(0,0,0,0.1);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
">
|
">
|
||||||
<div style="">
|
<div style="">
|
||||||
|
|||||||
@ -2,65 +2,17 @@ import { MemoryRouter as Router, Routes, Route } from 'react-router-dom';
|
|||||||
// import 'tailwindcss/tailwind.css';
|
// import 'tailwindcss/tailwind.css';
|
||||||
import icon from '../../assets/icon.svg';
|
import icon from '../../assets/icon.svg';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import AudioTrimmer from './components/AudioTrimer';
|
import ClipList from './components/ClipList';
|
||||||
|
|
||||||
function Hello() {
|
function MainPage() {
|
||||||
return (
|
return <ClipList />;
|
||||||
<div className="min-h-screen flex flex-col 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> */}
|
|
||||||
<AudioTrimmer
|
|
||||||
title="audio_capture_20251206_123108.wav"
|
|
||||||
filePath="C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250118_000351.wav"
|
|
||||||
// section="Section 1"
|
|
||||||
/>
|
|
||||||
<AudioTrimmer
|
|
||||||
title="audio_capture_20251206_123108.wav"
|
|
||||||
filePath="C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250118_000351.wav"
|
|
||||||
// section="Section 1"
|
|
||||||
/>
|
|
||||||
<AudioTrimmer
|
|
||||||
title="audio_capture_20251206_123108.wav"
|
|
||||||
filePath="C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250118_000351.wav"
|
|
||||||
// section="Section 1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Hello />} />
|
<Route path="/" element={<MainPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -10,28 +10,26 @@ import Regions from 'wavesurfer.js/dist/plugins/regions.esm.js';
|
|||||||
import ZoomPlugin from 'wavesurfer.js/dist/plugins/zoom.esm.js';
|
import ZoomPlugin from 'wavesurfer.js/dist/plugins/zoom.esm.js';
|
||||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||||
import PauseIcon from '@mui/icons-material/Pause';
|
import PauseIcon from '@mui/icons-material/Pause';
|
||||||
import SaveIcon from '@mui/icons-material/Save';
|
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import { ClipMetadata } from '../../ipc/audio/types';
|
||||||
|
|
||||||
export interface AudioTrimmerProps {
|
export interface AudioTrimmerProps {
|
||||||
filePath: string;
|
metadata: ClipMetadata;
|
||||||
section: string;
|
|
||||||
title?: string;
|
|
||||||
trimStart?: number;
|
|
||||||
trimEnd?: number;
|
|
||||||
onSave?: (trimStart: number, trimEnd: number, title?: string) => void;
|
onSave?: (trimStart: number, trimEnd: number, title?: string) => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AudioTrimmer({
|
export default function AudioTrimmer({
|
||||||
filePath,
|
metadata,
|
||||||
section,
|
|
||||||
title,
|
|
||||||
trimStart,
|
|
||||||
trimEnd,
|
|
||||||
onSave,
|
onSave,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: AudioTrimmerProps) {
|
}: AudioTrimmerProps) {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||||
|
useSortable({ id: metadata.filePath });
|
||||||
|
|
||||||
const [blobUrl, setBlobUrl] = useState<string | undefined>(undefined);
|
const [blobUrl, setBlobUrl] = useState<string | undefined>(undefined);
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const [clipStart, setClipStart] = useState<number>(0);
|
const [clipStart, setClipStart] = useState<number>(0);
|
||||||
@ -47,6 +45,9 @@ export default function AudioTrimmer({
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fileBaseName =
|
||||||
|
metadata.filePath.split('\\').pop()?.split('/').pop() || 'Unknown';
|
||||||
|
|
||||||
const { wavesurfer, isReady, isPlaying } = useWavesurfer({
|
const { wavesurfer, isReady, isPlaying } = useWavesurfer({
|
||||||
container: containerRef,
|
container: containerRef,
|
||||||
height: 100,
|
height: 100,
|
||||||
@ -74,12 +75,12 @@ export default function AudioTrimmer({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('ready, setting up regions plugin', wavesurfer);
|
console.log('ready, setting up regions plugin', wavesurfer);
|
||||||
if (trimStart !== undefined && trimEnd !== undefined) {
|
if (metadata.startTime !== undefined && metadata.endTime !== undefined) {
|
||||||
setClipStart(trimStart);
|
setClipStart(metadata.startTime);
|
||||||
setClipEnd(trimEnd);
|
setClipEnd(metadata.endTime);
|
||||||
plugins[0].addRegion({
|
plugins[0].addRegion({
|
||||||
start: trimStart,
|
start: metadata.startTime,
|
||||||
end: trimEnd,
|
end: metadata.endTime,
|
||||||
color: 'rgba(132, 81, 224, 0.3)',
|
color: 'rgba(132, 81, 224, 0.3)',
|
||||||
drag: false,
|
drag: false,
|
||||||
resize: true,
|
resize: true,
|
||||||
@ -93,17 +94,20 @@ export default function AudioTrimmer({
|
|||||||
color: 'rgba(132, 81, 224, 0.3)',
|
color: 'rgba(132, 81, 224, 0.3)',
|
||||||
});
|
});
|
||||||
plugins[0].on('region-created', onRegionCreated);
|
plugins[0].on('region-created', onRegionCreated);
|
||||||
}, [isReady, plugins, wavesurfer, onRegionCreated, trimStart, trimEnd]);
|
}, [isReady, plugins, wavesurfer, onRegionCreated, metadata]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let url: string | null = null;
|
let url: string | null = null;
|
||||||
async function fetchAudio() {
|
async function fetchAudio() {
|
||||||
// console.log('Loading audio buffer for file:', filePath);
|
// console.log('Loading audio buffer for file:', filePath);
|
||||||
const buffer =
|
const buffer = await window.audio.loadAudioBuffer(metadata.filePath);
|
||||||
await window.electron.ipcRenderer.loadAudioBuffer(filePath);
|
console.log('Received buffer:', buffer.buffer);
|
||||||
if (buffer && !buffer.error) {
|
if (buffer.buffer && !buffer.error) {
|
||||||
const audioData = buffer.data ? new Uint8Array(buffer.data) : buffer;
|
const audioData = buffer.buffer
|
||||||
|
? new Uint8Array(buffer.buffer)
|
||||||
|
: buffer;
|
||||||
url = URL.createObjectURL(new Blob([audioData]));
|
url = URL.createObjectURL(new Blob([audioData]));
|
||||||
|
console.log('Created blob URL:', url);
|
||||||
setBlobUrl(url);
|
setBlobUrl(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,7 +115,7 @@ export default function AudioTrimmer({
|
|||||||
return () => {
|
return () => {
|
||||||
if (url) URL.revokeObjectURL(url);
|
if (url) URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
}, [filePath]);
|
}, [metadata.filePath]);
|
||||||
|
|
||||||
const onPlayPause = () => {
|
const onPlayPause = () => {
|
||||||
if (wavesurfer === null) return;
|
if (wavesurfer === null) return;
|
||||||
@ -134,13 +138,36 @@ export default function AudioTrimmer({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shadow-[0_4px_6px_rgba(0,0,0,0.5)] m-2 p-4 rounded-lg bg-darkDrop">
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={{
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'stretch',
|
||||||
|
}}
|
||||||
|
className="shadow-[0_2px_8px_rgba(0,0,0,0.5)] m-2 rounded-lg bg-darkDrop"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: '10px',
|
||||||
|
borderRadius: '5px 0 0 5px',
|
||||||
|
cursor: 'grab',
|
||||||
|
}}
|
||||||
|
className="bg-neutral-800"
|
||||||
|
/>
|
||||||
{/* <div className="flex flex-col"> */}
|
{/* <div className="flex flex-col"> */}
|
||||||
<div className="">
|
<div className="ml-4 mr-2 p-2">
|
||||||
<div className="grid justify-items-stretch grid-cols-2">
|
<div className="grid justify-items-stretch grid-cols-2">
|
||||||
<div className="m-1 mb-5px flex flex-col">
|
<div className="mb-5px flex flex-col">
|
||||||
<text className="font-bold text-lg">{title}</text>
|
<text className="font-bold text-lg">{metadata.name}</text>
|
||||||
<text className="text-sm text-neutral-500">{filePath}</text>
|
<text className="text-sm text-neutral-500">{fileBaseName}</text>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button
|
<button
|
||||||
@ -153,9 +180,8 @@ export default function AudioTrimmer({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-plum hover:bg-plumDark text-white font-bold h-11 w-11 rounded-md ml-1"
|
className="bg-plum hover:bg-plumDark text-white font-bold h-11 w-11 rounded-md ml-1"
|
||||||
onClick={onSave}
|
|
||||||
>
|
>
|
||||||
<SaveIcon />
|
<ArrowForwardIcon />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -170,11 +196,10 @@ export default function AudioTrimmer({
|
|||||||
<div ref={containerRef} className="wavesurfer-inner" />
|
<div ref={containerRef} className="wavesurfer-inner" />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid justify-items-stretch grid-cols-2 text-neutral-500">
|
<div className="grid justify-items-stretch grid-cols-2 text-neutral-500">
|
||||||
<div className="m-1">
|
<div className="m-1 flex justify-start">
|
||||||
<text className="text-sm ">Start: {formatTime(clipStart)}</text>
|
<text className="text-sm ">
|
||||||
</div>
|
Clip: {formatTime(clipStart)} - {formatTime(clipEnd)}
|
||||||
<div className="mx-1 flex justify-end">
|
</text>
|
||||||
<text className="text-sm">End: {formatTime(clipEnd)}</text>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
147
electron-ui/src/renderer/components/ClipList.tsx
Normal file
147
electron-ui/src/renderer/components/ClipList.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import { use, useEffect, useState } from 'react';
|
||||||
|
import AudioTrimmer from './AudioTrimer';
|
||||||
|
import { ClipMetadata, PlaybackType } from '../../ipc/audio/types';
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
closestCenter,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
import {
|
||||||
|
arrayMove,
|
||||||
|
SortableContext,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
useSortable,
|
||||||
|
} from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
||||||
|
|
||||||
|
export interface ClipListProps {
|
||||||
|
collection: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testData: ClipMetadata[] = [
|
||||||
|
{
|
||||||
|
name: 'test 1',
|
||||||
|
filePath:
|
||||||
|
'C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250118_000351.wav',
|
||||||
|
volume: 1,
|
||||||
|
startTime: undefined,
|
||||||
|
endTime: undefined,
|
||||||
|
playbackType: PlaybackType.PlayStop,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'test 2',
|
||||||
|
filePath:
|
||||||
|
'C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250105_131700.wav',
|
||||||
|
volume: 1,
|
||||||
|
startTime: undefined,
|
||||||
|
endTime: undefined,
|
||||||
|
playbackType: PlaybackType.PlayStop,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'test 3',
|
||||||
|
filePath:
|
||||||
|
'C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250117_194006.wav',
|
||||||
|
volume: 1,
|
||||||
|
startTime: undefined,
|
||||||
|
endTime: undefined,
|
||||||
|
playbackType: PlaybackType.PlayStop,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function SortableTrimmer({ trimmer, id }) {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||||
|
useSortable({ id });
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
marginBottom: '1rem',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={setNodeRef} style={style}>
|
||||||
|
{/* Only this div is draggable */}
|
||||||
|
<div
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
style={{
|
||||||
|
cursor: 'grab',
|
||||||
|
padding: '4px',
|
||||||
|
background: '#6e44ba',
|
||||||
|
color: 'white',
|
||||||
|
borderRadius: '4px',
|
||||||
|
marginBottom: '8px',
|
||||||
|
width: 'fit-content',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Drag
|
||||||
|
</div>
|
||||||
|
{/* Wavesurfer and rest of AudioTrimmer are NOT draggable */}
|
||||||
|
<AudioTrimmer metadata={trimmer} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ClipList({ collection }: ClipListProps) {
|
||||||
|
const [metadata, setMetadata] = useState<ClipMetadata[]>(testData);
|
||||||
|
useEffect(() => {
|
||||||
|
setMetadata(testData);
|
||||||
|
}, [collection]);
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleDragEnd(event) {
|
||||||
|
const { active, over } = event;
|
||||||
|
if (active.id !== over?.id) {
|
||||||
|
const oldIndex = metadata.findIndex(
|
||||||
|
(item) => item.filePath === active.id,
|
||||||
|
);
|
||||||
|
const newIndex = metadata.findIndex((item) => item.filePath === over.id);
|
||||||
|
const newMetadata = arrayMove(metadata, oldIndex, newIndex);
|
||||||
|
console.log('New order:', newMetadata);
|
||||||
|
setMetadata(newMetadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col justify-center bg-midnight text-offwhite">
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
modifiers={[restrictToVerticalAxis]}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={metadata.map((item) => item.filePath)}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
{metadata.map((trimmer) => (
|
||||||
|
<AudioTrimmer key={trimmer.filePath} metadata={trimmer} />
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
</div>
|
||||||
|
// <div className="min-h-screen flex flex-col justify-center bg-midnight text-offwhite">
|
||||||
|
// <AudioTrimmer
|
||||||
|
// title="audio_capture_20251206_123108.wav"
|
||||||
|
// filePath="C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250118_000351.wav"
|
||||||
|
// // section="Section 1"
|
||||||
|
// />
|
||||||
|
// <AudioTrimmer
|
||||||
|
// title="audio_capture_20251206_123108.wav"
|
||||||
|
// filePath="C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250118_000351.wav"
|
||||||
|
// // section="Section 1"
|
||||||
|
// />
|
||||||
|
// <AudioTrimmer
|
||||||
|
// title="audio_capture_20251206_123108.wav"
|
||||||
|
// filePath="C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250118_000351.wav"
|
||||||
|
// // section="Section 1"
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
electron-ui/src/renderer/preload.d.ts
vendored
3
electron-ui/src/renderer/preload.d.ts
vendored
@ -1,9 +1,10 @@
|
|||||||
import { ElectronHandler } from '../main/preload';
|
import { ElectronHandler, FileHandler } from '../main/preload';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
interface Window {
|
interface Window {
|
||||||
electron: ElectronHandler;
|
electron: ElectronHandler;
|
||||||
|
audio: FileHandler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user