use wavesurefer and some styling

This commit is contained in:
michalcourson
2026-02-06 19:46:33 -05:00
parent 17bace5eaf
commit c292350b25
10 changed files with 883 additions and 261 deletions

View File

@ -0,0 +1,6 @@
const tailwindcss = require('@tailwindcss/postcss');
const autoprefixer = require('autoprefixer');
module.exports = {
plugins: [tailwindcss, autoprefixer],
};

View File

@ -75,12 +75,35 @@ const configuration: webpack.Configuration = {
},
},
'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'],
use: [
'style-loader',
'css-loader',
'sass-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [require('@tailwindcss/postcss')],
},
},
},
],
exclude: /\.module\.s?(c|a)ss$/,
},
// Fonts

View File

@ -51,12 +51,32 @@ const configuration: webpack.Configuration = {
},
},
'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'],
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [require('@tailwindcss/postcss')],
},
},
},
],
exclude: /\.module\.s?(c|a)ss$/,
},
// Fonts

File diff suppressed because it is too large Load Diff

View File

@ -102,6 +102,8 @@
},
"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",
@ -109,6 +111,7 @@
"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": {
@ -126,6 +129,7 @@
"@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",
@ -156,6 +160,8 @@
"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",

View File

@ -1,36 +1,23 @@
@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
*/
body {
position: relative;
color: white;
height: 100vh;
background: linear-gradient(
200.96deg,
#fedc2a -29.09%,
#dd5789 51.77%,
#7a2c9e 129.35%
);
font-family: sans-serif;
overflow-y: hidden;
display: flex;
justify-content: center;
align-items: center;
@theme {
--color-midnight: #1E1E1E;
--color-plum: #4f3186;
--color-offwhite: #d4d4d4;
}
button {
background-color: white;
background-color: #4f3186;
padding: 10px 20px;
border-radius: 10px;
border: none;
appearance: none;
font-size: 1.3rem;
box-shadow: 0px 8px 28px -6px rgba(24, 39, 75, 0.12),
0px 18px 88px -4px rgba(24, 39, 75, 0.14);
transition: all ease-in 0.1s;
cursor: pointer;
opacity: 0.9;
}
button:hover {

View File

@ -1,11 +1,12 @@
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>
<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>
@ -36,8 +37,9 @@ function Hello() {
</button>
</a>
</div> */}
<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"
/>

View File

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

View File

@ -0,0 +1,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: [],
};

View File