Compare commits

...

6 Commits

Author SHA1 Message Date
Hördle Bot
ece3991d37 Bump version to 0.1.6.20 2025-12-05 21:57:36 +01:00
Hördle Bot
fa3b64f490 Add 'Play Full Title' button to waveform editor 2025-12-05 21:57:34 +01:00
Hördle Bot
fa6f1097dd Bump version to 0.1.6.19 2025-12-05 21:48:00 +01:00
Hördle Bot
d2ec0119ce Fix waveform editor: show end marker for last segment and fix play full section stop functionality 2025-12-05 21:47:57 +01:00
Hördle Bot
8914c552cd Bump version to 0.1.6.18 2025-12-05 21:33:44 +01:00
Hördle Bot
d816422419 Update song list start time after saving changes in waveform editor 2025-12-05 21:33:41 +01:00
4 changed files with 155 additions and 57 deletions

View File

@@ -17,9 +17,11 @@ export default function SpecialEditorPage() {
const [special, setSpecial] = useState<CurateSpecial | null>(null); const [special, setSpecial] = useState<CurateSpecial | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { const fetchSpecial = async (showLoading = true) => {
const fetchSpecial = async () => {
try { try {
if (showLoading) {
setLoading(true);
}
const res = await fetch(`/api/specials/${specialId}`); const res = await fetch(`/api/specials/${specialId}`);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
@@ -28,11 +30,14 @@ export default function SpecialEditorPage() {
} catch (error) { } catch (error) {
console.error('Error fetching special:', error); console.error('Error fetching special:', error);
} finally { } finally {
if (showLoading) {
setLoading(false); setLoading(false);
} }
}
}; };
fetchSpecial(); useEffect(() => {
fetchSpecial(true);
}, [specialId]); }, [specialId]);
const handleSaveStartTime = async (songId: number, startTime: number) => { const handleSaveStartTime = async (songId: number, startTime: number) => {
@@ -46,6 +51,9 @@ export default function SpecialEditorPage() {
const errorText = await res.text().catch(() => res.statusText || 'Unknown error'); const errorText = await res.text().catch(() => res.statusText || 'Unknown error');
console.error('Error updating special song (admin):', res.status, errorText); console.error('Error updating special song (admin):', res.status, errorText);
throw new Error(`Failed to save start time: ${errorText}`); throw new Error(`Failed to save start time: ${errorText}`);
} else {
// Reload special data to update the start time in the song list
await fetchSpecial(false);
} }
}; };

View File

@@ -25,10 +25,11 @@ export default function CuratorSpecialEditorPage() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { const fetchSpecial = async (showLoading = true) => {
const fetchSpecial = async () => {
try { try {
if (showLoading) {
setLoading(true); setLoading(true);
}
const res = await fetch(`/api/curator/specials/${specialId}`, { const res = await fetch(`/api/curator/specials/${specialId}`, {
headers: getCuratorAuthHeaders(), headers: getCuratorAuthHeaders(),
}); });
@@ -45,12 +46,15 @@ export default function CuratorSpecialEditorPage() {
} catch (e) { } catch (e) {
setError('Failed to load special'); setError('Failed to load special');
} finally { } finally {
if (showLoading) {
setLoading(false); setLoading(false);
} }
}
}; };
useEffect(() => {
if (specialId) { if (specialId) {
fetchSpecial(); fetchSpecial(true);
} }
}, [specialId, t]); }, [specialId, t]);
@@ -67,6 +71,9 @@ export default function CuratorSpecialEditorPage() {
setError(t('specialForbidden')); setError(t('specialForbidden'));
} else if (!res.ok) { } else if (!res.ok) {
setError('Failed to save changes'); setError('Failed to save changes');
} else {
// Reload special data to update the start time in the song list
await fetchSpecial(false);
} }
}; };

View File

@@ -16,6 +16,7 @@ export default function WaveformEditor({ audioUrl, startTime, duration, unlockSt
const [audioDuration, setAudioDuration] = useState(0); const [audioDuration, setAudioDuration] = useState(0);
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [playingSegment, setPlayingSegment] = useState<number | null>(null); const [playingSegment, setPlayingSegment] = useState<number | null>(null);
const [isPlayingFullTitle, setIsPlayingFullTitle] = useState(false);
const [zoom, setZoom] = useState(1); // 1 = full view, higher = zoomed in const [zoom, setZoom] = useState(1); // 1 = full view, higher = zoomed in
const [viewOffset, setViewOffset] = useState(0); // Offset in seconds for panning const [viewOffset, setViewOffset] = useState(0); // Offset in seconds for panning
const [playbackPosition, setPlaybackPosition] = useState<number | null>(null); // Current playback position in seconds const [playbackPosition, setPlaybackPosition] = useState<number | null>(null); // Current playback position in seconds
@@ -133,6 +134,24 @@ export default function WaveformEditor({ audioUrl, startTime, duration, unlockSt
cumulativeTime = step; cumulativeTime = step;
}); });
// Draw end marker for the last segment (at startTime + duration)
const endTime = startTime + duration;
const endPx = ((endTime - visibleStart) / visibleDuration) * width;
if (endPx >= 0 && endPx <= width) {
ctx.beginPath();
ctx.moveTo(endPx, 0);
ctx.lineTo(endPx, height);
ctx.stroke();
// Draw "End" label
ctx.setLineDash([]);
ctx.fillStyle = '#ef4444';
ctx.font = 'bold 12px sans-serif';
ctx.fillText('End', endPx + 3, 15);
ctx.setLineDash([5, 5]);
}
ctx.setLineDash([]); ctx.setLineDash([]);
// Draw hover preview (semi-transparent) // Draw hover preview (semi-transparent)
@@ -219,6 +238,7 @@ export default function WaveformEditor({ audioUrl, startTime, duration, unlockSt
sourceRef.current?.stop(); sourceRef.current?.stop();
setIsPlaying(false); setIsPlaying(false);
setPlayingSegment(null); setPlayingSegment(null);
setIsPlayingFullTitle(false);
setPlaybackPosition(null); setPlaybackPosition(null);
if (animationFrameRef.current) { if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current); cancelAnimationFrame(animationFrameRef.current);
@@ -287,9 +307,16 @@ export default function WaveformEditor({ audioUrl, startTime, duration, unlockSt
const handlePlayFull = () => { const handlePlayFull = () => {
if (!audioBuffer || !audioContextRef.current) return; if (!audioBuffer || !audioContextRef.current) return;
if (isPlaying) { // If full selection playback is already playing, stop it
if (isPlaying && playingSegment === null && !isPlayingFullTitle) {
stopPlayback(); stopPlayback();
} else { return;
}
// Stop any current playback (segment, full selection, or full title)
stopPlayback();
// Start full selection playback
const source = audioContextRef.current.createBufferSource(); const source = audioContextRef.current.createBufferSource();
source.buffer = audioBuffer; source.buffer = audioBuffer;
source.connect(audioContextRef.current.destination); source.connect(audioContextRef.current.destination);
@@ -300,17 +327,59 @@ export default function WaveformEditor({ audioUrl, startTime, duration, unlockSt
source.start(0, startTime, duration); source.start(0, startTime, duration);
sourceRef.current = source; sourceRef.current = source;
setIsPlaying(true); setIsPlaying(true);
setPlayingSegment(null);
setIsPlayingFullTitle(false);
setPlaybackPosition(startTime); setPlaybackPosition(startTime);
source.onended = () => { source.onended = () => {
setIsPlaying(false); setIsPlaying(false);
setPlayingSegment(null);
setIsPlayingFullTitle(false);
setPlaybackPosition(null); setPlaybackPosition(null);
if (animationFrameRef.current) { if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current); cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null; animationFrameRef.current = null;
} }
}; };
};
const handlePlayFullTitle = () => {
if (!audioBuffer || !audioContextRef.current) return;
// If full title playback is already playing, stop it
if (isPlaying && isPlayingFullTitle) {
stopPlayback();
return;
} }
// Stop any current playback (segment, full selection, or full title)
stopPlayback();
// Start full title playback (from 0 to audioDuration)
const source = audioContextRef.current.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContextRef.current.destination);
playbackStartTimeRef.current = audioContextRef.current.currentTime;
playbackOffsetRef.current = 0;
source.start(0, 0, audioDuration);
sourceRef.current = source;
setIsPlaying(true);
setPlayingSegment(null);
setIsPlayingFullTitle(true);
setPlaybackPosition(0);
source.onended = () => {
setIsPlaying(false);
setPlayingSegment(null);
setIsPlayingFullTitle(false);
setPlaybackPosition(null);
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
}
};
}; };
const handleZoomIn = () => setZoom(prev => Math.min(prev * 1.5, 10)); const handleZoomIn = () => setZoom(prev => Math.min(prev * 1.5, 10));
@@ -401,7 +470,21 @@ export default function WaveformEditor({ audioUrl, startTime, duration, unlockSt
fontWeight: 'bold' fontWeight: 'bold'
}} }}
> >
{isPlaying && playingSegment === null ? '⏸ Pause' : '▶ Play Full Selection'} {isPlaying && playingSegment === null && !isPlayingFullTitle ? '⏸ Pause' : '▶ Play Full Selection'}
</button>
<button
onClick={handlePlayFullTitle}
style={{
padding: '0.5rem 1rem',
background: '#10b981',
color: 'white',
border: 'none',
borderRadius: '0.5rem',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
{isPlaying && isPlayingFullTitle ? '⏸ Pause' : '▶ Play Full Title'}
</button> </button>
<div style={{ fontSize: '0.875rem', color: '#666' }}> <div style={{ fontSize: '0.875rem', color: '#666' }}>

View File

@@ -1,6 +1,6 @@
{ {
"name": "hoerdle", "name": "hoerdle",
"version": "0.1.6.17", "version": "0.1.6.20",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",