import React, { useState, useMemo, useRef, useEffect } from 'react'; import { Song } from '../types'; import { Play, MoreHorizontal, Heart, ThumbsDown, ListPlus, Pause, Search, Filter, Check, Globe, Lock, Loader2, ThumbsUp, Share2, Video, Info, Clock } from 'lucide-react'; import { useAuth } from '../context/AuthContext'; import { useI18n } from '../context/I18nContext'; import { SongDropdownMenu } from './SongDropdownMenu'; import { ShareModal } from './ShareModal'; import { AlbumCover } from './AlbumCover'; import { songsApi } from '../services/api'; interface SongListProps { songs: Song[]; currentSong: Song | null; selectedSong: Song | null; likedSongIds: Set; isPlaying: boolean; referenceTracks?: { id: string; filename: string; audio_url: string; duration?: number | null; created_at?: string }[]; onPlay: (song: Song) => void; onSelect: (song: Song) => void; onToggleLike: (songId: string) => void; onAddToPlaylist: (song: Song) => void; onOpenVideo?: (song: Song) => void; onShowDetails?: (song: Song) => void; onNavigateToProfile?: (username: string) => void; onReusePrompt?: (song: Song) => void; onDelete?: (song: Song) => void; onSongUpdate?: (updatedSong: Song) => void; onDeleteMany?: (songs: Song[]) => void; onUseAsReference?: (song: Song) => void; onCoverSong?: (song: Song) => void; onUseUploadAsReference?: (track: { audio_url: string; filename: string }) => void; onCoverUpload?: (track: { audio_url: string; filename: string }) => void; } // ... existing code ... // Define Filter Types type FilterType = 'liked' | 'public' | 'private' | 'generating'; // Map model ID to short display name const getModelDisplayName = (modelId?: string): string => { if (!modelId) return 'v1.5'; const mapping: Record = { 'acestep-v15-base': '1.5B', 'acestep-v15-sft': '1.5S', 'acestep-v15-turbo-shift1': '1.5TS1', 'acestep-v15-turbo-shift3': '1.5TS3', 'acestep-v15-turbo-continuous': '1.5TC', 'acestep-v15-turbo': '1.5T', }; return mapping[modelId] || 'v1.5'; }; const createDragPreview = (element: HTMLElement) => { const clone = element.cloneNode(true) as HTMLElement; clone.style.width = `${element.offsetWidth}px`; clone.style.position = 'fixed'; clone.style.top = '-1000px'; clone.style.left = '-1000px'; clone.style.pointerEvents = 'none'; clone.style.opacity = '0.95'; const badge = document.createElement('div'); badge.textContent = '+'; badge.style.position = 'absolute'; badge.style.left = '8px'; badge.style.bottom = '8px'; badge.style.width = '24px'; badge.style.height = '24px'; badge.style.display = 'flex'; badge.style.alignItems = 'center'; badge.style.justifyContent = 'center'; badge.style.borderRadius = '9999px'; badge.style.background = '#22c55e'; badge.style.color = 'white'; badge.style.boxShadow = '0 6px 16px rgba(0,0,0,0.25)'; badge.style.fontSize = '16px'; badge.style.lineHeight = '1'; clone.style.position = 'relative'; clone.appendChild(badge); document.body.appendChild(clone); return clone; }; export const SongList: React.FC = ({ songs, currentSong, selectedSong, likedSongIds, isPlaying, referenceTracks = [], onPlay, onSelect, onToggleLike, onAddToPlaylist, onOpenVideo, onShowDetails, onNavigateToProfile, onReusePrompt, onDelete, onSongUpdate, onDeleteMany, onUseAsReference, onCoverSong, onUseUploadAsReference, onCoverUpload }) => { const { user } = useAuth(); const { t } = useI18n(); const [searchQuery, setSearchQuery] = useState(''); const [activeFilters, setActiveFilters] = useState>(new Set()); const [isFilterOpen, setIsFilterOpen] = useState(false); const [isSelecting, setIsSelecting] = useState(false); const [selectedIds, setSelectedIds] = useState>(new Set()); const filterRef = useRef(null); const FILTERS: { id: FilterType; label: string; icon: React.ReactNode }[] = [ { id: 'liked', label: t('liked'), icon: }, { id: 'public', label: t('public'), icon: }, { id: 'private', label: t('private'), icon: }, { id: 'generating', label: t('generatingStatus'), icon: } ]; // Close filter dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (filterRef.current && !filterRef.current.contains(event.target as Node)) { setIsFilterOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); useEffect(() => { setSelectedIds(prev => { if (prev.size === 0) return prev; const validIds = new Set(songs.map(song => song.id)); const next = new Set(); prev.forEach(id => { if (validIds.has(id)) next.add(id); }); return next; }); }, [songs]); const toggleFilter = (filterId: FilterType) => { setActiveFilters(prev => { const newFilters = new Set(prev); if (newFilters.has(filterId)) { newFilters.delete(filterId); } else { newFilters.add(filterId); } return newFilters; }); }; const filteredSongs = useMemo(() => { return songs.filter(song => { // 1. Search Logic const matchesSearch = song.title.toLowerCase().includes(searchQuery.toLowerCase()) || song.style.toLowerCase().includes(searchQuery.toLowerCase()) || song.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())); if (!matchesSearch) return false; // 2. Filter Logic if (activeFilters.size === 0) return true; if (activeFilters.has('liked') && !likedSongIds.has(song.id)) return false; if (activeFilters.has('public') && !song.isPublic) return false; if (activeFilters.has('private') && song.isPublic) return false; if (activeFilters.has('generating') && !song.isGenerating) return false; return true; }); }, [songs, searchQuery, activeFilters, likedSongIds]); const filteredUploads = useMemo(() => { if (activeFilters.size > 0) return []; if (!referenceTracks.length) return []; return referenceTracks.filter(track => { const title = track.filename.replace(/\.[^/.]+$/, ''); return title.toLowerCase().includes(searchQuery.toLowerCase()); }); }, [referenceTracks, searchQuery, activeFilters]); const listItems = useMemo(() => { const songItems = filteredSongs.map(song => ({ type: 'song' as const, id: song.id, createdAt: song.createdAt, song })); const uploadItems = filteredUploads.map(track => ({ type: 'upload' as const, id: track.id, createdAt: new Date(track.created_at || Date.now()), track })); return [...songItems, ...uploadItems].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); }, [filteredSongs, filteredUploads]); const selectableSongs = useMemo( () => filteredSongs.filter(song => !song.isGenerating), [filteredSongs] ); const allSelected = selectableSongs.length > 0 && selectableSongs.every(song => selectedIds.has(song.id)); const selectedSongs = selectableSongs.filter(song => selectedIds.has(song.id)); return (
{/* Container constraint */} {/* Header */}
Workspaces My Workspace
setSearchQuery(e.target.value)} placeholder={t('searchYourSongs')} className="w-full bg-zinc-100 dark:bg-[#121214] border border-zinc-200 dark:border-white/10 rounded-lg pl-10 pr-4 py-2.5 text-sm text-zinc-900 dark:text-white focus:outline-none focus:border-zinc-400 dark:focus:border-white/20 placeholder-zinc-500 dark:placeholder-zinc-600 transition-colors" />
{/* Filter Dropdown */} {isFilterOpen && (
{t('refineBy')}
{FILTERS.map(filter => ( ))}
)}
{isSelecting && (
{selectedSongs.length} selected
)}
{/* List */}
{/* Reduced vertical spacing */} {listItems.length === 0 ? (

{t('noSongsMatchFilters')}

) : ( listItems.map((item) => ( item.type === 'song' ? ( onPlay(item.song)} onSelect={() => onSelect(item.song)} onToggleSelect={() => { if (item.song.isGenerating) return; setSelectedIds(prev => { const next = new Set(prev); if (next.has(item.song.id)) next.delete(item.song.id); else next.add(item.song.id); return next; }); }} onToggleLike={() => onToggleLike(item.song.id)} onAddToPlaylist={() => onAddToPlaylist(item.song)} onOpenVideo={() => onOpenVideo && onOpenVideo(item.song)} onShowDetails={() => onShowDetails && onShowDetails(item.song)} onNavigateToProfile={onNavigateToProfile} onReusePrompt={() => onReusePrompt?.(item.song)} onDelete={() => onDelete?.(item.song)} onSongUpdate={onSongUpdate} onUseAsReference={() => onUseAsReference?.(item.song)} onCoverSong={() => onCoverSong?.(item.song)} /> ) : ( { onPlay({ id: `upload_${item.id}`, title, lyrics: '', style: 'Upload', coverUrl: '', duration: '0:00', createdAt: item.createdAt, tags: [], audioUrl, isPublic: false, } as Song); }} onUseAsReference={() => onUseUploadAsReference?.(item.track)} onCoverSong={() => onCoverUpload?.(item.track)} /> ) )) )}
{/* End container */}
); }; interface SongItemProps { song: Song; isCurrent: boolean; isSelected: boolean; isSelectionMode: boolean; isChecked: boolean; isLiked: boolean; isPlaying: boolean; isOwner: boolean; onPlay: () => void; onSelect: () => void; onToggleSelect: () => void; onToggleLike: () => void; onAddToPlaylist: () => void; onOpenVideo?: () => void; onShowDetails?: () => void; onNavigateToProfile?: (username: string) => void; onReusePrompt?: () => void; onDelete?: () => void; onSongUpdate?: (updatedSong: Song) => void; onUseAsReference?: () => void; onCoverSong?: () => void; } const SongItem: React.FC = ({ song, isCurrent, isSelected, isSelectionMode, isChecked, isLiked, isPlaying, isOwner, onPlay, onSelect, onToggleSelect, onToggleLike, onAddToPlaylist, onOpenVideo, onShowDetails, onNavigateToProfile, onReusePrompt, onDelete, onSongUpdate, onUseAsReference, onCoverSong }) => { const { token } = useAuth(); const [showDropdown, setShowDropdown] = useState(false); const [shareModalOpen, setShareModalOpen] = useState(false); const [imageError, setImageError] = useState(false); const [isEditingTitle, setIsEditingTitle] = useState(false); const [editedTitle, setEditedTitle] = useState(song.title); const titleInputRef = useRef(null); useEffect(() => { if (isEditingTitle && titleInputRef.current) { titleInputRef.current.focus(); titleInputRef.current.select(); } }, [isEditingTitle]); const handleSaveTitle = async () => { if (!token || !isOwner || !editedTitle.trim() || editedTitle === song.title) { setIsEditingTitle(false); setEditedTitle(song.title); return; } try { const response = await songsApi.updateSong(song.id, { title: editedTitle.trim() }, token); setIsEditingTitle(false); // Update the parent component's song list if (onSongUpdate && response.song) { onSongUpdate(response.song); } } catch (error) { console.error('Failed to update title:', error); setEditedTitle(song.title); setIsEditingTitle(false); } }; const handleTitleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { handleSaveTitle(); } else if (e.key === 'Escape') { setEditedTitle(song.title); setIsEditingTitle(false); } }; return ( <>
{ if (!song.audioUrl || song.isGenerating) return; e.dataTransfer.effectAllowed = 'copy'; e.dataTransfer.setData('application/x-ace-audio', JSON.stringify({ url: song.audioUrl, title: song.title || 'Untitled', source: 'song', })); const preview = createDragPreview(e.currentTarget); const rect = e.currentTarget.getBoundingClientRect(); const offsetX = Math.max(0, Math.min(rect.width, e.clientX - rect.left)); const offsetY = Math.max(0, Math.min(rect.height, e.clientY - rect.top)); e.dataTransfer.setDragImage(preview, offsetX, offsetY); setTimeout(() => { try { preview.remove(); } catch { // ignore } }, 0); }} className={`group flex items-center gap-4 p-2 rounded-lg hover:bg-zinc-100 dark:hover:bg-[#18181b] transition-all cursor-pointer border ${isSelected ? 'bg-zinc-100 dark:bg-[#18181b] border-zinc-200 dark:border-white/10' : 'border-transparent bg-transparent'} ${song.audioUrl && !song.isGenerating ? 'cursor-grab active:cursor-grabbing' : ''}`} > {isSelectionMode && ( )} {/* Cover Art - Reduced size */}
{/* Use gradient fallback if no coverUrl or image fails to load */} {(!song.coverUrl || imageError) ? ( ) : ( {song.title} setImageError(true)} /> )} {song.isGenerating ? (
{song.queuePosition ? ( /* Queue indicator */ <>
Queue #{song.queuePosition} ) : ( /* Generating - Music Waveform Animation */
)}
) : (
{ e.stopPropagation(); onPlay(); }} >
{isCurrent && isPlaying ? ( ) : ( )}
)}
{/* Content */}
{isEditingTitle && isOwner ? ( setEditedTitle(e.target.value)} onBlur={handleSaveTitle} onKeyDown={handleTitleKeyDown} onClick={(e) => e.stopPropagation()} className="font-bold text-lg bg-zinc-100 dark:bg-zinc-800 px-2 py-0.5 rounded border border-pink-500 focus:outline-none text-zinc-900 dark:text-white min-w-0 flex-1" /> ) : (

{ if (isOwner && !song.isGenerating) { e.stopPropagation(); setIsEditingTitle(true); } }} > {song.title || (song.isGenerating ? (song.queuePosition ? "Queued..." : "Creating...") : "Untitled")}

)} {getModelDisplayName(song.ditModel)} {song.isPublic === false && ( )}
{ e.stopPropagation(); if (song.creator && onNavigateToProfile) { onNavigateToProfile(song.creator); } }} >
{(song.creator?.[0] || 'U').toUpperCase()}
{song.creator || 'Unknown'}

{song.style}

{song.isGenerating && (
1 ? (song.progress ?? 0) / 100 : (song.progress ?? 0)) * 100) )}%`, }} />
)}
{/* Actions Row - Hidden while generating */} {!song.isGenerating && (
{/* Info Button - Visible only on small/medium screens where sidebar is hidden */}
setShowDropdown(false)} isOwner={isOwner} onCreateVideo={() => onOpenVideo?.(song)} onReusePrompt={onReusePrompt ? () => onReusePrompt?.(song) : undefined} onAddToPlaylist={() => onAddToPlaylist?.(song)} onDelete={() => onDelete?.(song)} onShare={() => setShareModalOpen(true)} onUseAsReference={() => onUseAsReference?.()} onCoverSong={() => onCoverSong?.()} />
)}
{/* Timestamp */}
{song.isGenerating ? ( {song.queuePosition ? `#${song.queuePosition}` : 'Creating...'} ) : song.duration}
setShareModalOpen(false)} song={song} /> ); }; const UploadItem: React.FC<{ track: { id: string; filename: string; audio_url: string; duration?: number | null }; onPlay: (audioUrl: string, title: string) => void; onUseAsReference?: () => void; onCoverSong?: () => void; }> = ({ track, onPlay, onUseAsReference, onCoverSong }) => { const title = track.filename.replace(/\.[^/.]+$/, ''); const duration = track.duration ? `${Math.floor(track.duration / 60)}:${String(Math.floor(track.duration % 60)).padStart(2, '0')}` : '--:--'; return ( onPlay(track.audio_url, title)} onSelect={() => onPlay(track.audio_url, title)} onToggleSelect={() => undefined} onToggleLike={() => undefined} onAddToPlaylist={() => undefined} onOpenVideo={() => undefined} onShowDetails={() => undefined} onNavigateToProfile={() => undefined} onReusePrompt={undefined} onDelete={() => undefined} onUseAsReference={onUseAsReference} onCoverSong={onCoverSong} /> ); };