Compare commits
6 Commits
v0.1.6.17
...
ece3991d37
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ece3991d37 | ||
|
|
fa3b64f490 | ||
|
|
fa6f1097dd | ||
|
|
d2ec0119ce | ||
|
|
8914c552cd | ||
|
|
d816422419 |
@@ -17,22 +17,27 @@ 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) {
|
||||||
const res = await fetch(`/api/specials/${specialId}`);
|
setLoading(true);
|
||||||
if (res.ok) {
|
}
|
||||||
const data = await res.json();
|
const res = await fetch(`/api/specials/${specialId}`);
|
||||||
setSpecial(data);
|
if (res.ok) {
|
||||||
}
|
const data = await res.json();
|
||||||
} catch (error) {
|
setSpecial(data);
|
||||||
console.error('Error fetching special:', error);
|
}
|
||||||
} finally {
|
} catch (error) {
|
||||||
|
console.error('Error fetching special:', error);
|
||||||
|
} 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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -25,32 +25,36 @@ 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}`, {
|
}
|
||||||
headers: getCuratorAuthHeaders(),
|
const res = await fetch(`/api/curator/specials/${specialId}`, {
|
||||||
});
|
headers: getCuratorAuthHeaders(),
|
||||||
if (res.status === 403) {
|
});
|
||||||
setError(t('specialForbidden'));
|
if (res.status === 403) {
|
||||||
return;
|
setError(t('specialForbidden'));
|
||||||
}
|
return;
|
||||||
if (!res.ok) {
|
}
|
||||||
setError('Failed to load special');
|
if (!res.ok) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = await res.json();
|
|
||||||
setSpecial(data);
|
|
||||||
} catch (e) {
|
|
||||||
setError('Failed to load special');
|
setError('Failed to load special');
|
||||||
} finally {
|
return;
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
setSpecial(data);
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to load special');
|
||||||
|
} 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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,30 +307,79 @@ 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;
|
||||||
const source = audioContextRef.current.createBufferSource();
|
|
||||||
source.buffer = audioBuffer;
|
|
||||||
source.connect(audioContextRef.current.destination);
|
|
||||||
|
|
||||||
playbackStartTimeRef.current = audioContextRef.current.currentTime;
|
|
||||||
playbackOffsetRef.current = startTime;
|
|
||||||
|
|
||||||
source.start(0, startTime, duration);
|
|
||||||
sourceRef.current = source;
|
|
||||||
setIsPlaying(true);
|
|
||||||
setPlaybackPosition(startTime);
|
|
||||||
|
|
||||||
source.onended = () => {
|
|
||||||
setIsPlaying(false);
|
|
||||||
setPlaybackPosition(null);
|
|
||||||
if (animationFrameRef.current) {
|
|
||||||
cancelAnimationFrame(animationFrameRef.current);
|
|
||||||
animationFrameRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop any current playback (segment, full selection, or full title)
|
||||||
|
stopPlayback();
|
||||||
|
|
||||||
|
// Start full selection playback
|
||||||
|
const source = audioContextRef.current.createBufferSource();
|
||||||
|
source.buffer = audioBuffer;
|
||||||
|
source.connect(audioContextRef.current.destination);
|
||||||
|
|
||||||
|
playbackStartTimeRef.current = audioContextRef.current.currentTime;
|
||||||
|
playbackOffsetRef.current = startTime;
|
||||||
|
|
||||||
|
source.start(0, startTime, duration);
|
||||||
|
sourceRef.current = source;
|
||||||
|
setIsPlaying(true);
|
||||||
|
setPlayingSegment(null);
|
||||||
|
setIsPlayingFullTitle(false);
|
||||||
|
setPlaybackPosition(startTime);
|
||||||
|
|
||||||
|
source.onended = () => {
|
||||||
|
setIsPlaying(false);
|
||||||
|
setPlayingSegment(null);
|
||||||
|
setIsPlayingFullTitle(false);
|
||||||
|
setPlaybackPosition(null);
|
||||||
|
if (animationFrameRef.current) {
|
||||||
|
cancelAnimationFrame(animationFrameRef.current);
|
||||||
|
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' }}>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user