diff --git a/App.tsx b/App.tsx index 461623a..1a2bf0e 100644 --- a/App.tsx +++ b/App.tsx @@ -408,19 +408,34 @@ function AppContent() { return; } - let nextIndex; - if (isShuffle) { - do { - nextIndex = Math.floor(Math.random() * queue.length); - } while (queue.length > 1 && nextIndex === currentIndex); - } else { - nextIndex = (currentIndex + 1) % queue.length; + // Find next playable song (has audioUrl and not generating) + const queueLen = queue.length; + for (let i = 1; i <= queueLen; i++) { + let nextIndex; + if (isShuffle) { + nextIndex = Math.floor(Math.random() * queueLen); + if (queueLen > 1 && nextIndex === currentIndex) continue; + } else { + nextIndex = currentIndex + i; + // In 'none' repeat mode, stop at end of queue + if (repeatMode === 'none' && nextIndex >= queueLen) { + setIsPlaying(false); + return; + } + nextIndex = nextIndex % queueLen; + } + + const candidate = queue[nextIndex]; + if (candidate.audioUrl && !candidate.isGenerating) { + setQueueIndex(nextIndex); + setCurrentSong(candidate); + setIsPlaying(true); + return; + } } - const nextSong = queue[nextIndex]; - setQueueIndex(nextIndex); - setCurrentSong(nextSong); - setIsPlaying(true); + // No playable songs found + setIsPlaying(false); }, [currentSong, queueIndex, isShuffle, repeatMode, playQueue, songs]); const playPrevious = useCallback(() => { @@ -438,16 +453,35 @@ function AppContent() { return; } - let prevIndex = (currentIndex - 1 + queue.length) % queue.length; - if (isShuffle) { - prevIndex = Math.floor(Math.random() * queue.length); + // Find previous playable song (has audioUrl and not generating) + const queueLen = queue.length; + for (let i = 1; i <= queueLen; i++) { + let prevIndex; + if (isShuffle) { + prevIndex = Math.floor(Math.random() * queueLen); + if (queueLen > 1 && prevIndex === currentIndex) continue; + } else { + prevIndex = currentIndex - i; + // In 'none' repeat mode, stop at beginning of queue + if (repeatMode === 'none' && prevIndex < 0) { + if (audioRef.current) audioRef.current.currentTime = 0; + return; + } + prevIndex = (prevIndex + queueLen) % queueLen; + } + + const candidate = queue[prevIndex]; + if (candidate.audioUrl && !candidate.isGenerating) { + setQueueIndex(prevIndex); + setCurrentSong(candidate); + setIsPlaying(true); + return; + } } - const prevSong = queue[prevIndex]; - setQueueIndex(prevIndex); - setCurrentSong(prevSong); - setIsPlaying(true); - }, [currentSong, queueIndex, currentTime, isShuffle, playQueue, songs]); + // No playable songs found + setIsPlaying(false); + }, [currentSong, queueIndex, currentTime, isShuffle, repeatMode, playQueue, songs]); useEffect(() => { playNextRef.current = playNext; @@ -565,6 +599,29 @@ function AppContent() { } }, [playbackRate]); + // Spacebar play/pause + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.code !== 'Space') return; + const tag = (e.target as HTMLElement)?.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (e.target as HTMLElement)?.isContentEditable) return; + e.preventDefault(); + if (currentSong) { + if (currentSong.audioUrl) { + setIsPlaying(prev => !prev); + } + } else { + // No song selected — play first available + const available = songs.filter(s => s.audioUrl && !s.isGenerating); + if (available.length > 0) { + playSong(available[0], available); + } + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [currentSong, songs]); + // Helper to cleanup a job and check if all jobs are done const cleanupJob = useCallback((jobId: string, tempId: string) => { const jobData = activeJobsRef.current.get(jobId); @@ -862,9 +919,20 @@ function AppContent() { const togglePlay = () => { if (!currentSong) return; + if (!currentSong.audioUrl) { + showToast(t('songNotAvailable'), 'error'); + return; + } setIsPlaying(!isPlaying); }; + const playFirst = () => { + const available = songs.filter(s => s.audioUrl && !s.isGenerating); + if (available.length > 0) { + playSong(available[0], available); + } + }; + const playSong = (song: Song, list?: Song[]) => { const nextQueue = list && list.length > 0 ? list @@ -1293,6 +1361,9 @@ function AppContent() { isLiked={selectedSong ? likedSongIds.has(selectedSong.id) : false} onToggleLike={toggleLike} onDelete={handleDeleteSong} + onPlay={playSong} + isPlaying={isPlaying} + currentSong={currentSong} /> )} @@ -1371,6 +1442,7 @@ function AppContent() { onReusePrompt={() => currentSong && handleReuse(currentSong)} onAddToPlaylist={() => currentSong && openAddToPlaylistModal(currentSong)} onDelete={() => currentSong && handleDeleteSong(currentSong)} + onPlayFirst={playFirst} /> diff --git a/components/Player.tsx b/components/Player.tsx index fb3985c..331abbb 100644 --- a/components/Player.tsx +++ b/components/Player.tsx @@ -33,6 +33,7 @@ interface PlayerProps { onReusePrompt?: () => void; onAddToPlaylist?: () => void; onDelete?: () => void; + onPlayFirst?: () => void; } export const Player: React.FC = ({ @@ -59,7 +60,8 @@ export const Player: React.FC = ({ onOpenVideo, onReusePrompt, onAddToPlaylist, - onDelete + onDelete, + onPlayFirst }) => { const { user } = useAuth(); const { isMobile } = useResponsive(); @@ -100,12 +102,15 @@ export const Player: React.FC = ({ if (!currentSong) { return (
-
+
+ {t('selectSongToPlay')} +
); } @@ -321,6 +326,12 @@ export const Player: React.FC = ({ /> )} + + setShareModalOpen(false)} + song={currentSong} + /> ); } diff --git a/i18n/translations.ts b/i18n/translations.ts index 87d5426..2e0e782 100644 --- a/i18n/translations.ts +++ b/i18n/translations.ts @@ -474,10 +474,11 @@ export const translations = { // Player nowPlaying: 'Now Playing', + selectSongToPlay: 'Select a song to play', downloadAudio: 'Download Audio', openInEditor: 'Open in Editor', anonymous: 'Anonymous', - + // PlaylistDetail loadingPlaylist: 'Loading playlist...', playlistNotFound: 'Playlist not found', @@ -1069,10 +1070,11 @@ export const translations = { // Player nowPlaying: '正在播放', + selectSongToPlay: '选择一首歌曲播放', downloadAudio: '下载音频', openInEditor: '在编辑器中打开', anonymous: '匿名用户', - + // PlaylistDetail loadingPlaylist: '加载播放列表中...', playlistNotFound: '播放列表未找到', @@ -1664,10 +1666,11 @@ export const translations = { // Player nowPlaying: '再生中', + selectSongToPlay: '曲を選択して再生', downloadAudio: 'オーディオをダウンロード', openInEditor: 'エディターで開く', anonymous: '匿名ユーザー', - + // PlaylistDetail loadingPlaylist: 'プレイリストを読み込み中...', playlistNotFound: 'プレイリストが見つかりません', @@ -2259,10 +2262,11 @@ export const translations = { // Player nowPlaying: '재생 중', + selectSongToPlay: '재생할 곡을 선택하세요', downloadAudio: '오디오 다운로드', openInEditor: '편집기에서 열기', anonymous: '익명 사용자', - + // PlaylistDetail loadingPlaylist: '재생목록 로딩 중...', playlistNotFound: '재생목록을 찾을 수 없습니다',