draggables, ipc stuff

This commit is contained in:
michalcourson
2026-02-11 19:42:52 -05:00
parent 0346efd504
commit 5516ce9212
17 changed files with 380 additions and 377 deletions

View 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;

View 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 };
}
},
);
}

View 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[];
}

View 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;

View File

@ -15,6 +15,7 @@ import { autoUpdater } from 'electron-updater';
import log from 'electron-log';
import MenuBuilder from './menu';
import { resolveHtmlPath } from './util';
import registerFileIpcHandlers from '../ipc/audio/main';
class AppUpdater {
constructor() {
@ -108,17 +109,7 @@ const createWindow = async () => {
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 };
}
});
registerFileIpcHandlers();
// Remove this if your app does not use auto updates
// eslint-disable-next-line
new AppUpdater();

View File

@ -1,6 +1,10 @@
// Disable no-unused-vars, broken for spread args
/* eslint no-unused-vars: off */
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';
@ -22,11 +26,27 @@ const electronHandler = {
ipcRenderer.once(channel, (_event, ...args) => func(...args));
},
loadAudioBuffer: (filePath: string) =>
ipcRenderer.invoke('load-audio-buffer', filePath),
invoke: (event: string, ...args: unknown[]) =>
ipcRenderer.invoke(event, ...args),
},
};
contextBridge.exposeInMainWorld('electron', 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;

View File

@ -548,7 +548,7 @@ document.addEventListener('DOMContentLoaded', async () => {
transform: translate(-50%, -50%);
padding: 10px;
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;
">
<div style="">

View File

@ -2,65 +2,17 @@ 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';
import ClipList from './components/ClipList';
function Hello() {
return (
<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>
);
function MainPage() {
return <ClipList />;
}
export default function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Hello />} />
<Route path="/" element={<MainPage />} />
</Routes>
</Router>
);

View File

@ -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 PlayArrowIcon from '@mui/icons-material/PlayArrow';
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 { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { ClipMetadata } from '../../ipc/audio/types';
export interface AudioTrimmerProps {
filePath: string;
section: string;
title?: string;
trimStart?: number;
trimEnd?: number;
metadata: ClipMetadata;
onSave?: (trimStart: number, trimEnd: number, title?: string) => void;
onDelete?: () => void;
}
export default function AudioTrimmer({
filePath,
section,
title,
trimStart,
trimEnd,
metadata,
onSave,
onDelete,
}: AudioTrimmerProps) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: metadata.filePath });
const [blobUrl, setBlobUrl] = useState<string | undefined>(undefined);
const containerRef = useRef(null);
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({
container: containerRef,
height: 100,
@ -74,12 +75,12 @@ export default function AudioTrimmer({
useEffect(() => {
console.log('ready, setting up regions plugin', wavesurfer);
if (trimStart !== undefined && trimEnd !== undefined) {
setClipStart(trimStart);
setClipEnd(trimEnd);
if (metadata.startTime !== undefined && metadata.endTime !== undefined) {
setClipStart(metadata.startTime);
setClipEnd(metadata.endTime);
plugins[0].addRegion({
start: trimStart,
end: trimEnd,
start: metadata.startTime,
end: metadata.endTime,
color: 'rgba(132, 81, 224, 0.3)',
drag: false,
resize: true,
@ -93,17 +94,20 @@ export default function AudioTrimmer({
color: 'rgba(132, 81, 224, 0.3)',
});
plugins[0].on('region-created', onRegionCreated);
}, [isReady, plugins, wavesurfer, onRegionCreated, trimStart, trimEnd]);
}, [isReady, plugins, wavesurfer, onRegionCreated, metadata]);
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;
const buffer = await window.audio.loadAudioBuffer(metadata.filePath);
console.log('Received buffer:', buffer.buffer);
if (buffer.buffer && !buffer.error) {
const audioData = buffer.buffer
? new Uint8Array(buffer.buffer)
: buffer;
url = URL.createObjectURL(new Blob([audioData]));
console.log('Created blob URL:', url);
setBlobUrl(url);
}
}
@ -111,7 +115,7 @@ export default function AudioTrimmer({
return () => {
if (url) URL.revokeObjectURL(url);
};
}, [filePath]);
}, [metadata.filePath]);
const onPlayPause = () => {
if (wavesurfer === null) return;
@ -134,13 +138,36 @@ export default function AudioTrimmer({
};
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="">
<div className="ml-4 mr-2 p-2">
<div className="grid justify-items-stretch grid-cols-2">
<div className="m-1 mb-5px flex flex-col">
<text className="font-bold text-lg">{title}</text>
<text className="text-sm text-neutral-500">{filePath}</text>
<div className="mb-5px flex flex-col">
<text className="font-bold text-lg">{metadata.name}</text>
<text className="text-sm text-neutral-500">{fileBaseName}</text>
</div>
<div className="flex justify-end">
<button
@ -153,9 +180,8 @@ export default function AudioTrimmer({
<button
type="button"
className="bg-plum hover:bg-plumDark text-white font-bold h-11 w-11 rounded-md ml-1"
onClick={onSave}
>
<SaveIcon />
<ArrowForwardIcon />
</button>
<button
type="button"
@ -170,11 +196,10 @@ export default function AudioTrimmer({
<div ref={containerRef} className="wavesurfer-inner" />
</div>
<div className="grid justify-items-stretch grid-cols-2 text-neutral-500">
<div className="m-1">
<text className="text-sm ">Start: {formatTime(clipStart)}</text>
</div>
<div className="mx-1 flex justify-end">
<text className="text-sm">End: {formatTime(clipEnd)}</text>
<div className="m-1 flex justify-start">
<text className="text-sm ">
Clip: {formatTime(clipStart)} - {formatTime(clipEnd)}
</text>
</div>
</div>
</div>

View 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>
);
}

View File

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