import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Sidebar } from './components/Sidebar'; import { CreatePanel } from './components/CreatePanel'; import { SongList } from './components/SongList'; import { RightSidebar } from './components/RightSidebar'; import { Player } from './components/Player'; import { LibraryView } from './components/LibraryView'; import { CreatePlaylistModal, AddToPlaylistModal } from './components/PlaylistModals'; import { VideoGeneratorModal } from './components/VideoGeneratorModal'; import { UsernameModal } from './components/UsernameModal'; import { UserProfile } from './components/UserProfile'; import { SettingsModal } from './components/SettingsModal'; import { SongProfile } from './components/SongProfile'; import { Song, GenerationParams, View, Playlist } from './types'; import { generateApi, songsApi, playlistsApi, getAudioUrl } from './services/api'; import { useAuth } from './context/AuthContext'; import { useResponsive } from './context/ResponsiveContext'; import { List } from 'lucide-react'; import { PlaylistDetail } from './components/PlaylistDetail'; import { Toast, ToastType } from './components/Toast'; import { SearchPage } from './components/SearchPage'; export default function App() { // Responsive const { isMobile, isDesktop } = useResponsive(); // Auth const { user, token, isAuthenticated, isLoading: authLoading, setupUser, logout } = useAuth(); const [showUsernameModal, setShowUsernameModal] = useState(false); // Track multiple concurrent generation jobs const activeJobsRef = useRef }>>(new Map()); const [activeJobCount, setActiveJobCount] = useState(0); // Theme State const [theme, setTheme] = useState<'dark' | 'light'>(() => { const stored = localStorage.getItem('theme'); if (stored === 'dark' || stored === 'light') return stored; return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; }); // Navigation State - default to create view const [currentView, setCurrentView] = useState('create'); // Content State const [songs, setSongs] = useState([]); const [playlists, setPlaylists] = useState([]); const [likedSongIds, setLikedSongIds] = useState>(new Set()); const [referenceTracks, setReferenceTracks] = useState([]); const [playQueue, setPlayQueue] = useState([]); const [queueIndex, setQueueIndex] = useState(-1); // Selection State const [currentSong, setCurrentSong] = useState(null); const [selectedSong, setSelectedSong] = useState(null); const [selectedPlaylist, setSelectedPlaylist] = useState(null); // Player State const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [volume, setVolume] = useState(0.8); const [isShuffle, setIsShuffle] = useState(false); const [repeatMode, setRepeatMode] = useState<'none' | 'all' | 'one'>('all'); // UI State const [isGenerating, setIsGenerating] = useState(false); const [showRightSidebar, setShowRightSidebar] = useState(true); // Mobile UI Toggle const [mobileShowList, setMobileShowList] = useState(false); // Modals const [isCreatePlaylistModalOpen, setIsCreatePlaylistModalOpen] = useState(false); const [isAddToPlaylistModalOpen, setIsAddToPlaylistModalOpen] = useState(false); const [songToAddToPlaylist, setSongToAddToPlaylist] = useState(null); // Video Modal const [isVideoModalOpen, setIsVideoModalOpen] = useState(false); const [songForVideo, setSongForVideo] = useState(null); // Settings Modal const [showSettingsModal, setShowSettingsModal] = useState(false); // Profile View const [viewingUsername, setViewingUsername] = useState(null); // Song View const [viewingSongId, setViewingSongId] = useState(null); // Playlist View const [viewingPlaylistId, setViewingPlaylistId] = useState(null); // Reuse State const [reuseData, setReuseData] = useState<{ song: Song, timestamp: number } | null>(null); const audioRef = useRef(null); const pendingSeekRef = useRef(null); const playNextRef = useRef<() => void>(() => {}); // Mobile Details Modal State const [showMobileDetails, setShowMobileDetails] = useState(false); // Toast State const [toast, setToast] = useState<{ message: string; type: ToastType; isVisible: boolean }>({ message: '', type: 'success', isVisible: false, }); interface ReferenceTrack { id: string; filename: string; storage_key: string; duration: number | null; file_size_bytes: number | null; tags: string[] | null; created_at: string; audio_url: string; } const showToast = (message: string, type: ToastType = 'success') => { setToast({ message, type, isVisible: true }); }; const closeToast = () => { setToast(prev => ({ ...prev, isVisible: false })); }; // Show username modal if not authenticated and not loading useEffect(() => { if (!authLoading && !isAuthenticated) { setShowUsernameModal(true); } }, [authLoading, isAuthenticated]); // Load Playlists useEffect(() => { if (token) { playlistsApi.getMyPlaylists(token) .then(res => setPlaylists(res.playlists)) .catch(err => console.error('Failed to load playlists', err)); } else { setPlaylists([]); } }, [token]); // Cleanup active jobs on unmount useEffect(() => { return () => { // Clear all polling intervals when component unmounts activeJobsRef.current.forEach(({ pollInterval }) => { clearInterval(pollInterval); }); activeJobsRef.current.clear(); }; }, []); const handleShowDetails = (song: Song) => { setSelectedSong(song); setShowMobileDetails(true); }; // Reuse Handler const handleReuse = (song: Song) => { setReuseData({ song, timestamp: Date.now() }); setCurrentView('create'); setMobileShowList(false); }; // Song Update Handler const handleSongUpdate = (updatedSong: Song) => { setSongs(prev => prev.map(s => s.id === updatedSong.id ? updatedSong : s)); if (selectedSong?.id === updatedSong.id) { setSelectedSong(updatedSong); } }; // Navigate to Profile Handler const handleNavigateToProfile = (username: string) => { setViewingUsername(username); setCurrentView('profile'); window.history.pushState({}, '', `/@${username}`); }; // Back from Profile Handler const handleBackFromProfile = () => { setViewingUsername(null); setCurrentView('create'); window.history.pushState({}, '', '/'); }; // Navigate to Song Handler const handleNavigateToSong = (songId: string) => { setViewingSongId(songId); setCurrentView('song'); window.history.pushState({}, '', `/song/${songId}`); }; // Back from Song Handler const handleBackFromSong = () => { setViewingSongId(null); setCurrentView('create'); window.history.pushState({}, '', '/'); }; // Theme Effect useEffect(() => { localStorage.setItem('theme', theme); if (theme === 'dark') { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } }, [theme]); const toggleTheme = () => { setTheme(prev => prev === 'dark' ? 'light' : 'dark'); }; // URL Routing Effect useEffect(() => { const handleUrlChange = () => { const path = window.location.pathname; const params = new URLSearchParams(window.location.search); // Handle ?song= query parameter const songParam = params.get('song'); if (songParam) { setViewingSongId(songParam); setCurrentView('song'); window.history.replaceState({}, '', `/song/${songParam}`); return; } if (path === '/create' || path === '/') { setCurrentView('create'); setMobileShowList(false); } else if (path === '/library') { setCurrentView('library'); } else if (path.startsWith('/@')) { const username = path.substring(2); if (username) { setViewingUsername(username); setCurrentView('profile'); } } else if (path.startsWith('/song/')) { const songId = path.substring(6); if (songId) { setViewingSongId(songId); setCurrentView('song'); } } else if (path.startsWith('/playlist/')) { const playlistId = path.substring(10); if (playlistId) { setViewingPlaylistId(playlistId); setCurrentView('playlist'); } } else if (path === '/search') { setCurrentView('search'); } }; handleUrlChange(); window.addEventListener('popstate', handleUrlChange); return () => window.removeEventListener('popstate', handleUrlChange); }, []); // Load Songs Effect useEffect(() => { if (!isAuthenticated || !token) return; const loadSongs = async () => { try { const [mySongsRes, likedSongsRes] = await Promise.all([ songsApi.getMySongs(token), songsApi.getLikedSongs(token) ]); const mapSong = (s: any): Song => ({ id: s.id, title: s.title, lyrics: s.lyrics, style: s.style, coverUrl: `https://picsum.photos/seed/${s.id}/400/400`, duration: s.duration && s.duration > 0 ? `${Math.floor(s.duration / 60)}:${String(Math.floor(s.duration % 60)).padStart(2, '0')}` : '0:00', createdAt: new Date(s.created_at || s.createdAt), tags: s.tags || [], audioUrl: getAudioUrl(s.audio_url, s.id), isPublic: s.is_public, likeCount: s.like_count || 0, viewCount: s.view_count || 0, userId: s.user_id, creator: s.creator, generationParams: (() => { try { if (!s.generation_params) return undefined; return typeof s.generation_params === 'string' ? JSON.parse(s.generation_params) : s.generation_params; } catch { return undefined; } })(), }); const mySongs = mySongsRes.songs.map(mapSong); const likedSongs = likedSongsRes.songs.map(mapSong); const songsMap = new Map(); [...mySongs, ...likedSongs].forEach(s => songsMap.set(s.id, s)); // Preserve any generating songs (temp songs) setSongs(prev => { const generatingSongs = prev.filter(s => s.isGenerating); const loadedSongs = Array.from(songsMap.values()); return [...generatingSongs, ...loadedSongs]; }); const likedIds = new Set(likedSongs.map(s => s.id)); setLikedSongIds(likedIds); } catch (error) { console.error('Failed to load songs:', error); } }; loadSongs(); }, [isAuthenticated, token]); const loadReferenceTracks = useCallback(async () => { if (!isAuthenticated || !token) return; try { const response = await fetch('/api/reference-tracks', { headers: { Authorization: `Bearer ${token}` } }); if (!response.ok) return; const data = await response.json(); setReferenceTracks(data.tracks || []); } catch (error) { console.error('Failed to load reference tracks:', error); } }, [isAuthenticated, token]); // Load reference tracks for Library useEffect(() => { loadReferenceTracks(); }, [loadReferenceTracks]); useEffect(() => { if (currentView === 'library') { loadReferenceTracks(); } }, [currentView, loadReferenceTracks]); // Player Logic const getActiveQueue = (song?: Song) => { if (playQueue.length > 0) return playQueue; if (song && songs.some(s => s.id === song.id)) return songs; return songs; }; const playNext = useCallback(() => { if (!currentSong) return; const queue = getActiveQueue(currentSong); if (queue.length === 0) return; const currentIndex = queueIndex >= 0 && queue[queueIndex]?.id === currentSong.id ? queueIndex : queue.findIndex(s => s.id === currentSong.id); if (currentIndex === -1) return; if (repeatMode === 'one') { if (audioRef.current) { audioRef.current.currentTime = 0; audioRef.current.play(); } 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; } const nextSong = queue[nextIndex]; setQueueIndex(nextIndex); setCurrentSong(nextSong); setIsPlaying(true); }, [currentSong, queueIndex, isShuffle, repeatMode, playQueue, songs]); const playPrevious = useCallback(() => { if (!currentSong) return; const queue = getActiveQueue(currentSong); if (queue.length === 0) return; const currentIndex = queueIndex >= 0 && queue[queueIndex]?.id === currentSong.id ? queueIndex : queue.findIndex(s => s.id === currentSong.id); if (currentIndex === -1) return; if (currentTime > 3) { if (audioRef.current) audioRef.current.currentTime = 0; return; } let prevIndex = (currentIndex - 1 + queue.length) % queue.length; if (isShuffle) { prevIndex = Math.floor(Math.random() * queue.length); } const prevSong = queue[prevIndex]; setQueueIndex(prevIndex); setCurrentSong(prevSong); setIsPlaying(true); }, [currentSong, queueIndex, currentTime, isShuffle, playQueue, songs]); useEffect(() => { playNextRef.current = playNext; }, [playNext]); // Audio Setup useEffect(() => { audioRef.current = new Audio(); audioRef.current.crossOrigin = "anonymous"; const audio = audioRef.current; audio.volume = volume; const onTimeUpdate = () => setCurrentTime(audio.currentTime); const applyPendingSeek = () => { if (pendingSeekRef.current === null) return; if (audio.seekable.length === 0) return; const target = pendingSeekRef.current; const safeTarget = Number.isFinite(audio.duration) ? Math.min(Math.max(target, 0), audio.duration) : Math.max(target, 0); audio.currentTime = safeTarget; setCurrentTime(safeTarget); pendingSeekRef.current = null; }; const onLoadedMetadata = () => { setDuration(audio.duration); applyPendingSeek(); }; const onCanPlay = () => { applyPendingSeek(); }; const onProgress = () => { applyPendingSeek(); }; const onEnded = () => { playNextRef.current(); }; const onError = (e: Event) => { if (audio.error && audio.error.code !== 1) { console.error("Audio playback error:", audio.error); if (audio.error.code === 4) { showToast('This song is no longer available.', 'error'); } else { showToast('Unable to play this song.', 'error'); } } setIsPlaying(false); }; audio.addEventListener('timeupdate', onTimeUpdate); audio.addEventListener('loadedmetadata', onLoadedMetadata); audio.addEventListener('canplay', onCanPlay); audio.addEventListener('progress', onProgress); audio.addEventListener('ended', onEnded); audio.addEventListener('error', onError); return () => { audio.pause(); audio.removeEventListener('timeupdate', onTimeUpdate); audio.removeEventListener('loadedmetadata', onLoadedMetadata); audio.removeEventListener('canplay', onCanPlay); audio.removeEventListener('progress', onProgress); audio.removeEventListener('ended', onEnded); audio.removeEventListener('error', onError); }; }, []); // Handle Playback State useEffect(() => { const audio = audioRef.current; if (!audio || !currentSong?.audioUrl) return; const playAudio = async () => { try { await audio.play(); } catch (err) { if (err instanceof Error && err.name !== 'AbortError') { console.error("Playback failed:", err); if (err.name === 'NotSupportedError') { showToast('This song is no longer available.', 'error'); } setIsPlaying(false); } } }; if (audio.src !== currentSong.audioUrl) { audio.src = currentSong.audioUrl; audio.load(); if (isPlaying) playAudio(); } else { if (isPlaying) playAudio(); else audio.pause(); } }, [currentSong, isPlaying]); // Handle Volume useEffect(() => { if (audioRef.current) { audioRef.current.volume = volume; } }, [volume]); // 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); if (jobData) { clearInterval(jobData.pollInterval); activeJobsRef.current.delete(jobId); } // Remove temp song setSongs(prev => prev.filter(s => s.id !== tempId)); // Update active job count setActiveJobCount(activeJobsRef.current.size); // If no more active jobs, set isGenerating to false if (activeJobsRef.current.size === 0) { setIsGenerating(false); } }, []); // Refresh songs list (called when any job completes successfully) const refreshSongsList = useCallback(async () => { if (!token) return; try { const response = await songsApi.getMySongs(token); const loadedSongs: Song[] = response.songs.map(s => ({ id: s.id, title: s.title, lyrics: s.lyrics, style: s.style, coverUrl: `https://picsum.photos/seed/${s.id}/400/400`, duration: s.duration && s.duration > 0 ? `${Math.floor(s.duration / 60)}:${String(Math.floor(s.duration % 60)).padStart(2, '0')}` : '0:00', createdAt: new Date(s.created_at), tags: s.tags || [], audioUrl: getAudioUrl(s.audio_url, s.id), isPublic: s.is_public, likeCount: s.like_count || 0, viewCount: s.view_count || 0, userId: s.user_id, creator: s.creator, generationParams: (() => { try { if (!s.generation_params) return undefined; return typeof s.generation_params === 'string' ? JSON.parse(s.generation_params) : s.generation_params; } catch { return undefined; } })(), })); // Preserve any generating songs that aren't in the loaded list setSongs(prev => { const generatingSongs = prev.filter(s => s.isGenerating); const mergedSongs = [...generatingSongs]; for (const song of loadedSongs) { if (!mergedSongs.some(s => s.id === song.id)) { mergedSongs.push(song); } } // Sort by creation date, newest first return mergedSongs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); }); // If the current selection was a temp/generating song, replace it with newest real song if (selectedSong?.isGenerating || (selectedSong && !loadedSongs.some(s => s.id === selectedSong.id))) { setSelectedSong(loadedSongs[0] ?? null); } } catch (error) { console.error('Failed to refresh songs:', error); } }, [token]); // Handlers const handleGenerate = async (params: GenerationParams) => { if (!isAuthenticated || !token) { setShowUsernameModal(true); return; } setIsGenerating(true); setCurrentView('create'); setMobileShowList(false); // Create unique temp ID for this job const tempId = `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const tempSong: Song = { id: tempId, title: params.title || 'Generating...', lyrics: '', style: params.style, coverUrl: 'https://picsum.photos/200/200?blur=10', duration: '--:--', createdAt: new Date(), isGenerating: true, tags: params.customMode ? ['custom'] : ['simple'], isPublic: true }; setSongs(prev => [tempSong, ...prev]); setSelectedSong(tempSong); setShowRightSidebar(true); try { const job = await generateApi.startGeneration({ customMode: params.customMode, songDescription: params.songDescription, lyrics: params.lyrics, style: params.style, title: params.title, instrumental: params.instrumental, vocalLanguage: params.vocalLanguage, duration: params.duration && params.duration > 0 ? params.duration : undefined, bpm: params.bpm, keyScale: params.keyScale, timeSignature: params.timeSignature, inferenceSteps: params.inferenceSteps, guidanceScale: params.guidanceScale, batchSize: params.batchSize, randomSeed: params.randomSeed, seed: params.seed, thinking: params.thinking, audioFormat: params.audioFormat, inferMethod: params.inferMethod, shift: params.shift, lmTemperature: params.lmTemperature, lmCfgScale: params.lmCfgScale, lmTopK: params.lmTopK, lmTopP: params.lmTopP, lmNegativePrompt: params.lmNegativePrompt, referenceAudioUrl: params.referenceAudioUrl, sourceAudioUrl: params.sourceAudioUrl, referenceAudioTitle: params.referenceAudioTitle, sourceAudioTitle: params.sourceAudioTitle, audioCodes: params.audioCodes, repaintingStart: params.repaintingStart, repaintingEnd: params.repaintingEnd, instruction: params.instruction, audioCoverStrength: params.audioCoverStrength, taskType: params.taskType, useAdg: params.useAdg, cfgIntervalStart: params.cfgIntervalStart, cfgIntervalEnd: params.cfgIntervalEnd, customTimesteps: params.customTimesteps, useCotMetas: params.useCotMetas, useCotCaption: params.useCotCaption, useCotLanguage: params.useCotLanguage, autogen: params.autogen, constrainedDecodingDebug: params.constrainedDecodingDebug, allowLmBatch: params.allowLmBatch, getScores: params.getScores, getLrc: params.getLrc, scoreScale: params.scoreScale, lmBatchChunkSize: params.lmBatchChunkSize, trackName: params.trackName, completeTrackClasses: params.completeTrackClasses, isFormatCaption: params.isFormatCaption, }, token); // Poll for completion - each job has its own polling interval const pollInterval = setInterval(async () => { try { const status = await generateApi.getStatus(job.jobId, token); const normalizedProgress = Number.isFinite(Number(status.progress)) ? (Number(status.progress) > 1 ? Number(status.progress) / 100 : Number(status.progress)) : undefined; // Update queue position on the temp song setSongs(prev => prev.map(s => { if (s.id === tempId) { return { ...s, queuePosition: status.status === 'queued' ? status.queuePosition : undefined, progress: normalizedProgress ?? s.progress, stage: status.stage ?? s.stage, }; } return s; })); if (status.status === 'succeeded' && status.result) { cleanupJob(job.jobId, tempId); await refreshSongsList(); if (window.innerWidth < 768) { setMobileShowList(true); } } else if (status.status === 'failed') { cleanupJob(job.jobId, tempId); console.error(`Job ${job.jobId} failed:`, status.error); showToast(`Generation failed: ${status.error || 'Unknown error'}`, 'error'); } } catch (pollError) { console.error(`Polling error for job ${job.jobId}:`, pollError); cleanupJob(job.jobId, tempId); } }, 2000); // Track this job activeJobsRef.current.set(job.jobId, { tempId, pollInterval }); setActiveJobCount(activeJobsRef.current.size); // Timeout after 10 minutes setTimeout(() => { if (activeJobsRef.current.has(job.jobId)) { console.warn(`Job ${job.jobId} timed out`); cleanupJob(job.jobId, tempId); showToast('Generation timed out', 'error'); } }, 600000); } catch (e) { console.error('Generation error:', e); setSongs(prev => prev.filter(s => s.id !== tempId)); // Only set isGenerating to false if no other jobs are running if (activeJobsRef.current.size === 0) { setIsGenerating(false); } showToast('Generation failed. Please try again.', 'error'); } }; const togglePlay = () => { if (!currentSong) return; setIsPlaying(!isPlaying); }; const playSong = (song: Song, list?: Song[]) => { const nextQueue = list && list.length > 0 ? list : (playQueue.length > 0 && playQueue.some(s => s.id === song.id)) ? playQueue : (songs.some(s => s.id === song.id) ? songs : [song]); const nextIndex = nextQueue.findIndex(s => s.id === song.id); setPlayQueue(nextQueue); setQueueIndex(nextIndex); if (currentSong?.id !== song.id) { const updatedSong = { ...song, viewCount: (song.viewCount || 0) + 1 }; setCurrentSong(updatedSong); setSelectedSong(updatedSong); setIsPlaying(true); setSongs(prev => prev.map(s => s.id === song.id ? updatedSong : s)); songsApi.trackPlay(song.id, token).catch(err => console.error('Failed to track play:', err)); } else { togglePlay(); } if (currentSong?.id === song.id) { setSelectedSong(song); } setShowRightSidebar(true); }; const handleSeek = (time: number) => { const audio = audioRef.current; if (!audio) return; if (Number.isNaN(audio.duration) || audio.readyState < 1 || audio.seekable.length === 0) { pendingSeekRef.current = time; return; } audio.currentTime = time; setCurrentTime(time); }; const toggleLike = async (songId: string) => { if (!token) return; const isLiked = likedSongIds.has(songId); // Optimistic update setLikedSongIds(prev => { const next = new Set(prev); if (isLiked) next.delete(songId); else next.add(songId); return next; }); setSongs(prev => prev.map(s => { if (s.id === songId) { const newCount = (s.likeCount || 0) + (isLiked ? -1 : 1); return { ...s, likeCount: Math.max(0, newCount) }; } return s; })); if (selectedSong?.id === songId) { setSelectedSong(prev => prev ? { ...prev, likeCount: Math.max(0, (prev.likeCount || 0) + (isLiked ? -1 : 1)) } : null); } // Persist to database try { await songsApi.toggleLike(songId, token); } catch (error) { console.error('Failed to toggle like:', error); // Revert on error setLikedSongIds(prev => { const next = new Set(prev); if (isLiked) next.add(songId); else next.delete(songId); return next; }); } }; const handleDeleteSong = async (song: Song) => { if (!token) return; // Show confirmation dialog const confirmed = window.confirm( `Are you sure you want to delete "${song.title}"? This action cannot be undone.` ); if (!confirmed) return; try { // Call API to delete song await songsApi.deleteSong(song.id, token); // Remove from songs list setSongs(prev => prev.filter(s => s.id !== song.id)); // Remove from liked songs if it was liked setLikedSongIds(prev => { const next = new Set(prev); next.delete(song.id); return next; }); // Handle if deleted song is currently selected if (selectedSong?.id === song.id) { setSelectedSong(null); } // Handle if deleted song is currently playing if (currentSong?.id === song.id) { setCurrentSong(null); setIsPlaying(false); if (audioRef.current) { audioRef.current.pause(); audioRef.current.src = ''; } } // Remove from play queue if present setPlayQueue(prev => prev.filter(s => s.id !== song.id)); showToast('Song deleted successfully'); } catch (error) { console.error('Failed to delete song:', error); showToast('Failed to delete song', 'error'); } }; const handleDeleteReferenceTrack = async (trackId: string) => { if (!token) return; const confirmed = window.confirm('Delete this upload? This action cannot be undone.'); if (!confirmed) return; try { const response = await fetch(`/api/reference-tracks/${trackId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` } }); if (!response.ok) { throw new Error('Failed to delete upload'); } setReferenceTracks(prev => prev.filter(track => track.id !== trackId)); showToast('Upload deleted successfully'); } catch (error) { console.error('Failed to delete upload:', error); showToast('Failed to delete upload', 'error'); } }; const createPlaylist = async (name: string, description: string) => { if (!token) return; try { const res = await playlistsApi.create(name, description, true, token); setPlaylists(prev => [res.playlist, ...prev]); if (songToAddToPlaylist) { await playlistsApi.addSong(res.playlist.id, songToAddToPlaylist.id, token); setSongToAddToPlaylist(null); playlistsApi.getMyPlaylists(token).then(r => setPlaylists(r.playlists)); } showToast('Playlist created successfully!'); } catch (error) { console.error('Create playlist error:', error); showToast('Failed to create playlist', 'error'); } }; const openAddToPlaylistModal = (song: Song) => { setSongToAddToPlaylist(song); setIsAddToPlaylistModalOpen(true); }; const addSongToPlaylist = async (playlistId: string) => { if (!songToAddToPlaylist || !token) return; try { await playlistsApi.addSong(playlistId, songToAddToPlaylist.id, token); setSongToAddToPlaylist(null); showToast('Song added to playlist'); playlistsApi.getMyPlaylists(token).then(r => setPlaylists(r.playlists)); } catch (error) { console.error('Add song error:', error); showToast('Failed to add song to playlist', 'error'); } }; const handleNavigateToPlaylist = (playlistId: string) => { setViewingPlaylistId(playlistId); setCurrentView('playlist'); window.history.pushState({}, '', `/playlist/${playlistId}`); }; const handleBackFromPlaylist = () => { setViewingPlaylistId(null); setCurrentView('library'); window.history.pushState({}, '', '/library'); }; const openVideoGenerator = (song: Song) => { if (isPlaying) { setIsPlaying(false); if (audioRef.current) audioRef.current.pause(); } setSongForVideo(song); setIsVideoModalOpen(true); }; // Handle username setup const handleUsernameSubmit = async (username: string) => { await setupUser(username); setShowUsernameModal(false); }; // Render Layout Logic const renderContent = () => { switch (currentView) { case 'library': { const allSongs = user ? songs.filter(s => s.userId === user.id) : []; return ( likedSongIds.has(s.id))} playlists={playlists} referenceTracks={referenceTracks} onPlaySong={playSong} onCreatePlaylist={() => { setSongToAddToPlaylist(null); setIsCreatePlaylistModalOpen(true); }} onSelectPlaylist={(p) => handleNavigateToPlaylist(p.id)} onAddToPlaylist={openAddToPlaylistModal} onOpenVideo={openVideoGenerator} onReusePrompt={handleReuse} onDeleteSong={handleDeleteSong} onDeleteReferenceTrack={handleDeleteReferenceTrack} /> ); } case 'profile': if (!viewingUsername) return null; return ( ); case 'playlist': if (!viewingPlaylistId) return null; return ( { setSelectedSong(s); setShowRightSidebar(true); }} onNavigateToProfile={handleNavigateToProfile} /> ); case 'song': if (!viewingSongId) return null; return ( ); case 'search': return ( ); case 'create': default: return (
{/* Create Panel */}
{/* Song List */}
{ setSelectedSong(s); setShowRightSidebar(true); }} onToggleLike={toggleLike} onAddToPlaylist={openAddToPlaylistModal} onOpenVideo={openVideoGenerator} onShowDetails={handleShowDetails} onNavigateToProfile={handleNavigateToProfile} onReusePrompt={handleReuse} onDelete={handleDeleteSong} />
{/* Right Sidebar */} {showRightSidebar && (
setShowRightSidebar(false)} onOpenVideo={() => selectedSong && openVideoGenerator(selectedSong)} onReuse={handleReuse} onSongUpdate={handleSongUpdate} onNavigateToProfile={handleNavigateToProfile} onNavigateToSong={handleNavigateToSong} isLiked={selectedSong ? likedSongIds.has(selectedSong.id) : false} onToggleLike={toggleLike} onPlay={playSong} isPlaying={isPlaying} currentSong={currentSong} onDelete={handleDeleteSong} />
)} {/* Mobile Toggle Button */}
); } }; return (
{ setCurrentView(v); if (v === 'create') { setMobileShowList(false); window.history.pushState({}, '', '/'); } else if (v === 'library') { window.history.pushState({}, '', '/library'); } else if (v === 'search') { window.history.pushState({}, '', '/search'); } }} theme={theme} onToggleTheme={toggleTheme} user={user} onLogin={() => setShowUsernameModal(true)} onLogout={logout} onOpenSettings={() => setShowSettingsModal(true)} />
{renderContent()}
setIsShuffle(!isShuffle)} repeatMode={repeatMode} onToggleRepeat={() => setRepeatMode(prev => prev === 'none' ? 'all' : prev === 'all' ? 'one' : 'none')} isLiked={currentSong ? likedSongIds.has(currentSong.id) : false} onToggleLike={() => currentSong && toggleLike(currentSong.id)} onNavigateToSong={handleNavigateToSong} onOpenVideo={() => currentSong && openVideoGenerator(currentSong)} onReusePrompt={() => currentSong && handleReuse(currentSong)} onAddToPlaylist={() => currentSong && openAddToPlaylistModal(currentSong)} onDelete={() => currentSong && handleDeleteSong(currentSong)} /> setIsCreatePlaylistModalOpen(false)} onCreate={createPlaylist} /> setIsAddToPlaylistModalOpen(false)} playlists={playlists} onSelect={addSongToPlaylist} onCreateNew={() => { setIsAddToPlaylistModalOpen(false); setIsCreatePlaylistModalOpen(true); }} /> setIsVideoModalOpen(false)} song={songForVideo} /> setShowSettingsModal(false)} theme={theme} onToggleTheme={toggleTheme} onNavigateToProfile={handleNavigateToProfile} /> {/* Mobile Details Modal */} {showMobileDetails && selectedSong && (
setShowMobileDetails(false)} />
setShowMobileDetails(false)} onOpenVideo={() => selectedSong && openVideoGenerator(selectedSong)} onReuse={handleReuse} onSongUpdate={handleSongUpdate} onNavigateToProfile={handleNavigateToProfile} onNavigateToSong={handleNavigateToSong} isLiked={selectedSong ? likedSongIds.has(selectedSong.id) : false} onToggleLike={toggleLike} onPlay={playSong} isPlaying={isPlaying} currentSong={currentSong} onDelete={handleDeleteSong} />
)}
); }