Initial commit: RehearsalHub POC
Full-stack self-hosted band rehearsal platform: Backend (FastAPI + SQLAlchemy 2.0 async): - Auth with JWT (register, login, /me, settings) - Band management with Nextcloud folder integration - Song management with audio version tracking - Nextcloud scan to auto-import audio files - Band membership with link-based invite system - Song comments - Audio analysis worker (BPM, key, loudness, waveform) - Nextcloud activity watcher for auto-import - WebSocket support for real-time annotation updates - Alembic migrations (0001–0003) - Repository pattern, Ruff + mypy configured Frontend (React 18 + Vite + TypeScript strict): - Login/register page with post-login redirect - Home page with band list and creation form - Band page with member panel, invite link, song list, NC scan - Song page with waveform player, annotations, comment thread - Settings page for per-user Nextcloud credentials - Invite acceptance page (/invite/:token) - ESLint v9 flat config + TypeScript strict mode Infrastructure: - Docker Compose: PostgreSQL, Redis, API, worker, watcher, nginx - nginx reverse proxy for static files + /api/ proxy - make check runs all linters before docker compose build Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
67
web/src/hooks/useWaveform.ts
Normal file
67
web/src/hooks/useWaveform.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import WaveSurfer from "wavesurfer.js";
|
||||
|
||||
export interface UseWaveformOptions {
|
||||
url: string | null;
|
||||
peaksUrl: string | null;
|
||||
onReady?: (duration: number) => void;
|
||||
onTimeUpdate?: (currentTime: number) => void;
|
||||
}
|
||||
|
||||
export function useWaveform(
|
||||
containerRef: React.RefObject<HTMLDivElement>,
|
||||
options: UseWaveformOptions
|
||||
) {
|
||||
const wsRef = useRef<WaveSurfer | null>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !options.url) return;
|
||||
|
||||
const ws = WaveSurfer.create({
|
||||
container: containerRef.current,
|
||||
waveColor: "#2A3050",
|
||||
progressColor: "#F0A840",
|
||||
cursorColor: "#FFD080",
|
||||
barWidth: 2,
|
||||
barRadius: 2,
|
||||
height: 80,
|
||||
normalize: true,
|
||||
});
|
||||
|
||||
ws.load(options.url);
|
||||
|
||||
ws.on("ready", () => {
|
||||
setIsReady(true);
|
||||
options.onReady?.(ws.getDuration());
|
||||
});
|
||||
|
||||
ws.on("audioprocess", (time) => {
|
||||
setCurrentTime(time);
|
||||
options.onTimeUpdate?.(time);
|
||||
});
|
||||
|
||||
ws.on("play", () => setIsPlaying(true));
|
||||
ws.on("pause", () => setIsPlaying(false));
|
||||
ws.on("finish", () => setIsPlaying(false));
|
||||
|
||||
wsRef.current = ws;
|
||||
return () => {
|
||||
ws.destroy();
|
||||
wsRef.current = null;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [options.url]);
|
||||
|
||||
const play = () => wsRef.current?.play();
|
||||
const pause = () => wsRef.current?.pause();
|
||||
const seekTo = (time: number) => {
|
||||
if (wsRef.current && isReady) {
|
||||
wsRef.current.setTime(time);
|
||||
}
|
||||
};
|
||||
|
||||
return { isPlaying, isReady, currentTime, play, pause, seekTo };
|
||||
}
|
||||
Reference in New Issue
Block a user