From 6527c42f41bc65cfcf6ea77502c1a2c74499c13c Mon Sep 17 00:00:00 2001 From: riversedge Date: Wed, 4 Feb 2026 23:38:39 -0500 Subject: [PATCH] Multi select and cover/reference is playable --- App.tsx | 55 ++++++++++++++++++ components/RightSidebar.tsx | 58 +++++++++++++++---- components/SongList.tsx | 110 +++++++++++++++++++++++++++++++++++- 3 files changed, 210 insertions(+), 13 deletions(-) diff --git a/App.tsx b/App.tsx index e5af1ca..0a7e37f 100644 --- a/App.tsx +++ b/App.tsx @@ -950,6 +950,60 @@ export default function App() { } }; + const handleDeleteSongs = async (songsToDelete: Song[]) => { + if (!token || songsToDelete.length === 0) return; + + const confirmed = window.confirm( + `Delete ${songsToDelete.length} songs? This action cannot be undone.` + ); + if (!confirmed) return; + + const idsToDelete = new Set(songsToDelete.map(song => song.id)); + const succeeded: string[] = []; + const failed: string[] = []; + + for (const song of songsToDelete) { + try { + await songsApi.deleteSong(song.id, token); + succeeded.push(song.id); + } catch (error) { + console.error('Failed to delete song:', error); + failed.push(song.id); + } + } + + if (succeeded.length > 0) { + setSongs(prev => prev.filter(s => !idsToDelete.has(s.id) || failed.includes(s.id))); + + setLikedSongIds(prev => { + const next = new Set(prev); + succeeded.forEach(id => next.delete(id)); + return next; + }); + + if (selectedSong?.id && succeeded.includes(selectedSong.id)) { + setSelectedSong(null); + } + + if (currentSong?.id && succeeded.includes(currentSong.id)) { + setCurrentSong(null); + setIsPlaying(false); + if (audioRef.current) { + audioRef.current.pause(); + audioRef.current.src = ''; + } + } + + setPlayQueue(prev => prev.filter(s => !idsToDelete.has(s.id) || failed.includes(s.id))); + } + + if (failed.length > 0) { + showToast(`Deleted ${succeeded.length}/${songsToDelete.length} songs`, 'error'); + } else { + showToast('Songs deleted successfully'); + } + }; + const handleDeleteReferenceTrack = async (trackId: string) => { if (!token) return; const confirmed = window.confirm('Delete this upload? This action cannot be undone.'); @@ -1156,6 +1210,7 @@ export default function App() { onNavigateToProfile={handleNavigateToProfile} onReusePrompt={handleReuse} onDelete={handleDeleteSong} + onDeleteMany={handleDeleteSongs} /> diff --git a/components/RightSidebar.tsx b/components/RightSidebar.tsx index ef7c02d..e97fc61 100644 --- a/components/RightSidebar.tsx +++ b/components/RightSidebar.tsx @@ -276,12 +276,29 @@ export const RightSidebar: React.FC = ({ song, onClose, onOpe - + )} {song.generationParams?.sourceAudioUrl && ( @@ -295,12 +312,29 @@ export const RightSidebar: React.FC = ({ song, onClose, onOpe - + )} diff --git a/components/SongList.tsx b/components/SongList.tsx index 2f17a2b..70515ae 100644 --- a/components/SongList.tsx +++ b/components/SongList.tsx @@ -21,6 +21,7 @@ interface SongListProps { onNavigateToProfile?: (username: string) => void; onReusePrompt?: (song: Song) => void; onDelete?: (song: Song) => void; + onDeleteMany?: (songs: Song[]) => void; } // ... existing code ... @@ -51,12 +52,15 @@ export const SongList: React.FC = ({ onShowDetails, onNavigateToProfile, onReusePrompt, - onDelete + onDelete, + onDeleteMany }) => { const { user } = useAuth(); 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); // Close filter dropdown when clicking outside @@ -70,6 +74,18 @@ export const SongList: React.FC = ({ 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); @@ -104,6 +120,14 @@ export const SongList: React.FC = ({ }); }, [songs, searchQuery, activeFilters, likedSongIds]); + 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 */} @@ -175,7 +199,57 @@ export const SongList: React.FC = ({
)}
+ + + + {isSelecting && ( +
+
+ {selectedSongs.length} selected +
+
+ + +
+
+ )} {/* List */} @@ -200,11 +274,22 @@ export const SongList: React.FC = ({ song={song} isCurrent={currentSong?.id === song.id} isSelected={selectedSong?.id === song.id} + isSelectionMode={isSelecting} + isChecked={selectedIds.has(song.id)} isLiked={likedSongIds.has(song.id)} isPlaying={isPlaying} isOwner={user?.id === song.userId} onPlay={() => onPlay(song)} onSelect={() => onSelect(song)} + onToggleSelect={() => { + if (song.isGenerating) return; + setSelectedIds(prev => { + const next = new Set(prev); + if (next.has(song.id)) next.delete(song.id); + else next.add(song.id); + return next; + }); + }} onToggleLike={() => onToggleLike(song.id)} onAddToPlaylist={() => onAddToPlaylist(song)} onOpenVideo={() => onOpenVideo && onOpenVideo(song)} @@ -225,11 +310,14 @@ 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; @@ -243,11 +331,14 @@ const SongItem: React.FC = ({ song, isCurrent, isSelected, + isSelectionMode, + isChecked, isLiked, isPlaying, isOwner, onPlay, onSelect, + onToggleSelect, onToggleLike, onAddToPlaylist, onOpenVideo, @@ -266,6 +357,23 @@ const SongItem: React.FC = ({ onClick={onSelect} 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'}`} > + {isSelectionMode && ( + + )} {/* Cover Art - Reduced size */}