feat: added spacebar navigation to audioplayer

This commit is contained in:
averel10
2026-04-03 10:23:19 +02:00
parent 6986e88560
commit 2812689e59
2 changed files with 121 additions and 44 deletions

View File

@@ -1,19 +1,39 @@
'use client'; 'use client';
import React, { createContext, useContext, useState, ReactNode } from 'react'; import React, { createContext, useContext, useState, ReactNode, useCallback } from 'react';
interface AudioContextType { interface AudioContextType {
currentAudioId: string | null; currentAudioId: string | null;
setCurrentAudio: (id: string | null) => void; setCurrentAudio: (id: string | null) => void;
playerCount: number;
registerPlayer: (id: string) => void;
unregisterPlayer: (id: string) => void;
} }
const AudioContext = createContext<AudioContextType | undefined>(undefined); const AudioContext = createContext<AudioContextType | undefined>(undefined);
export function AudioProvider({ children }: { children: ReactNode }) { export function AudioProvider({ children }: { children: ReactNode }) {
const [currentAudioId, setCurrentAudioId] = useState<string | null>(null); const [currentAudioId, setCurrentAudioId] = useState<string | null>(null);
const [playerCount, setPlayerCount] = useState(0);
const registerPlayer = useCallback((id: string) => {
setPlayerCount((prev) => prev + 1);
}, []);
const unregisterPlayer = useCallback((id: string) => {
setPlayerCount((prev) => Math.max(0, prev - 1));
}, []);
return ( return (
<AudioContext.Provider value={{ currentAudioId, setCurrentAudio: setCurrentAudioId }}> <AudioContext.Provider
value={{
currentAudioId,
setCurrentAudio: setCurrentAudioId,
playerCount,
registerPlayer,
unregisterPlayer,
}}
>
{children} {children}
</AudioContext.Provider> </AudioContext.Provider>
); );

View File

@@ -25,11 +25,12 @@ export default function WaveformPlayer({
showWaveform = true, showWaveform = true,
playMode = 'pause', playMode = 'pause',
}: WaveformPlayerProps) { }: WaveformPlayerProps) {
const { currentAudioId, setCurrentAudio } = useAudio(); const { currentAudioId, setCurrentAudio, playerCount, registerPlayer, unregisterPlayer } = useAudio();
// Generate unique ID for this player instance // Generate unique ID for this player instance
const playerId = useRef(Math.random().toString(36).slice(2)).current; const playerId = useRef(Math.random().toString(36).slice(2)).current;
const isActive = currentAudioId === playerId; const isActive = currentAudioId === playerId;
const isOnlyPlayer = playerCount === 1;
const audioRef = useRef<HTMLAudioElement>(null); const audioRef = useRef<HTMLAudioElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
@@ -41,6 +42,14 @@ export default function WaveformPlayer({
const [duration, setDuration] = useState(0); const [duration, setDuration] = useState(0);
const [peaks, setPeaks] = useState<number[]>([]); const [peaks, setPeaks] = useState<number[]>([]);
// Register/unregister player on mount/unmount
useEffect(() => {
registerPlayer(playerId);
return () => {
unregisterPlayer(playerId);
};
}, [playerId, registerPlayer, unregisterPlayer]);
// Stop if another player became active // Stop if another player became active
useEffect(() => { useEffect(() => {
if (!isActive && isPlaying) { if (!isActive && isPlaying) {
@@ -112,10 +121,37 @@ export default function WaveformPlayer({
} }
}, [onFullyPlayed]); }, [onFullyPlayed]);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.code === 'Space' && isOnlyPlayer) {
e.preventDefault();
if (isPlaying) {
if (playMode === 'stop') {
handleStop();
} else {
handlePause();
}
} else {
handlePlay();
}
}
},
[isOnlyPlayer, isPlaying, playMode, handlePlay, handlePause, handleStop]
);
const handleLoadedMetadata = useCallback(() => { const handleLoadedMetadata = useCallback(() => {
if (audioRef.current) setDuration(audioRef.current.duration); if (audioRef.current) setDuration(audioRef.current.duration);
}, []); }, []);
// Add spacebar listener when only one player exists
useEffect(() => {
if (!isOnlyPlayer) return;
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [isOnlyPlayer, handleKeyDown]);
// Decode audio and generate waveform peaks (only if showWaveform is true) // Decode audio and generate waveform peaks (only if showWaveform is true)
useEffect(() => { useEffect(() => {
if (!showWaveform) return; if (!showWaveform) return;
@@ -188,38 +224,50 @@ export default function WaveformPlayer({
// Simple player button (no waveform) // Simple player button (no waveform)
if (!showWaveform) { if (!showWaveform) {
return ( return (
<div className="flex items-center gap-2"> <div className="flex flex-col gap-1">
<audio <div className="flex items-center gap-2">
ref={audioRef} <audio
src={src} ref={audioRef}
onEnded={handleEnded} src={src}
/> onEnded={handleEnded}
<button />
onClick={isPlaying ? (playMode === 'stop' ? handleStop : handlePause) : handlePlay} <div className="flex items-center gap-1">
className={`inline-flex items-center justify-center w-8 h-8 rounded-full transition-colors ${ <button
isPlaying onClick={isPlaying ? (playMode === 'stop' ? handleStop : handlePause) : handlePlay}
? 'bg-red-500 hover:bg-red-600 text-white' className={`inline-flex items-center justify-center w-8 h-8 rounded-full transition-colors ${
: 'bg-blue-500 hover:bg-blue-600 text-white' isPlaying
}`} ? 'bg-red-500 hover:bg-red-600 text-white'
title={isPlaying ? (playMode === 'stop' ? 'Stop' : 'Pause') : 'Play'} : 'bg-blue-500 hover:bg-blue-600 text-white'
> }`}
{isPlaying ? ( title={`${isPlaying ? (playMode === 'stop' ? 'Stop' : 'Pause') : 'Play'}`}
playMode === 'stop' ? ( >
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> {isPlaying ? (
<rect x="5" y="5" width="10" height="10" /> playMode === 'stop' ? (
</svg> <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<rect x="5" y="5" width="10" height="10" />
</svg>
) : (
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<rect x="6" y="4" width="2" height="12" />
<rect x="12" y="4" width="2" height="12" />
</svg>
)
) : ( ) : (
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<rect x="6" y="4" width="2" height="12" /> <path d="M6.3 2.841A1.5 1.5 0 004 4.11v11.78a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z" />
<rect x="12" y="4" width="2" height="12" />
</svg> </svg>
) )}
) : ( </button>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> {isOnlyPlayer && (
<path d="M6.3 2.841A1.5 1.5 0 004 4.11v11.78a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z" /> <div
</svg> className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-gray-300 text-gray-700 text-xs font-bold cursor-help"
)} title="Leertaste zum Abspielen/Pausieren verwenden"
</button> >
?
</div>
)}
</div>
</div>
</div> </div>
); );
} }
@@ -235,16 +283,17 @@ export default function WaveformPlayer({
preload="metadata" preload="metadata"
/> />
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{/* Play / Pause button */} {/* Play / Pause button with optional info icon */}
<button <div className="flex items-center gap-1">
onClick={isPlaying ? (playMode === 'stop' ? handleStop : handlePause) : handlePlay} <button
className={`flex-shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-full transition-colors ${ onClick={isPlaying ? (playMode === 'stop' ? handleStop : handlePause) : handlePlay}
isPlaying className={`flex-shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-full transition-colors ${
? 'bg-red-500 hover:bg-red-600 text-white' isPlaying
: 'bg-blue-500 hover:bg-blue-600 text-white' ? 'bg-red-500 hover:bg-red-600 text-white'
}`} : 'bg-blue-500 hover:bg-blue-600 text-white'
title={isPlaying ? (playMode === 'stop' ? 'Stop' : 'Pause') : 'Abspielen'} }`}
> title={isPlaying ? (playMode === 'stop' ? 'Stop' : 'Pause') : 'Abspielen'}
>
{isPlaying ? ( {isPlaying ? (
playMode === 'stop' ? ( playMode === 'stop' ? (
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
@@ -261,8 +310,16 @@ export default function WaveformPlayer({
<path d="M6.3 2.841A1.5 1.5 0 004 4.11v11.78a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z" /> <path d="M6.3 2.841A1.5 1.5 0 004 4.11v11.78a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z" />
</svg> </svg>
)} )}
</button> </button>
{isOnlyPlayer && (
<div
className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-gray-300 text-gray-700 text-xs font-bold cursor-help"
title="Leertaste zum Abspielen/Pausieren verwenden"
>
?
</div>
)}
</div>
{/* Waveform canvas - hidden on small screens */} {/* Waveform canvas - hidden on small screens */}
<canvas <canvas
ref={canvasRef} ref={canvasRef}