diff --git a/App.tsx b/App.tsx index 3f00307..dd51392 100644 --- a/App.tsx +++ b/App.tsx @@ -1081,6 +1081,26 @@ export default function App() { setMobileShowList(false); }; + const handleUseUploadAsReference = (track: { audio_url: string; filename: string }) => { + setPendingAudioSelection({ + target: 'reference', + url: track.audio_url, + title: track.filename.replace(/\.[^/.]+$/, ''), + }); + setCurrentView('create'); + setMobileShowList(false); + }; + + const handleCoverUpload = (track: { audio_url: string; filename: string }) => { + setPendingAudioSelection({ + target: 'source', + url: track.audio_url, + title: track.filename.replace(/\.[^/.]+$/, ''), + }); + setCurrentView('create'); + setMobileShowList(false); + }; + const handleBackFromPlaylist = () => { setViewingPlaylistId(null); setCurrentView('library'); @@ -1232,6 +1252,8 @@ export default function App() { onDeleteMany={handleDeleteSongs} onUseAsReference={handleUseAsReference} onCoverSong={handleCoverSong} + onUseUploadAsReference={handleUseUploadAsReference} + onCoverUpload={handleCoverUpload} /> diff --git a/components/CreatePanel.tsx b/components/CreatePanel.tsx index 268f880..2bc83c1 100644 --- a/components/CreatePanel.tsx +++ b/components/CreatePanel.tsx @@ -705,7 +705,7 @@ export const CreatePanel: React.FC = ({ }; const handleWorkspaceDrop = (e: React.DragEvent) => { - if (e.dataTransfer.files?.length) { + if (e.dataTransfer.files?.length || e.dataTransfer.types.includes('application/x-ace-audio')) { handleDrop(e, audioTab); } }; @@ -815,9 +815,11 @@ export const CreatePanel: React.FC = ({
-
- {dragKind === 'audio' ? : } -
+ {dragKind !== 'audio' && ( +
+ +
+ )}
{dragKind === 'audio' ? 'Drop to use audio' : 'Drop to upload'}
diff --git a/components/SongDropdownMenu.tsx b/components/SongDropdownMenu.tsx index efb5378..31ba6dc 100644 --- a/components/SongDropdownMenu.tsx +++ b/components/SongDropdownMenu.tsx @@ -189,23 +189,29 @@ export const SongDropdownMenu: React.FC = ({ label="Extract Stems" onClick={onExtractStems ? () => handleAction(onExtractStems) : handleExtractStems} /> - } - label="Reuse Prompt" - onClick={() => handleAction(onReusePrompt)} - /> - } - label="Use as Reference" - onClick={() => handleAction(onUseAsReference)} - disabled={!song.audioUrl} - /> - } - label="Cover Song" - onClick={() => handleAction(onCoverSong)} - disabled={!song.audioUrl} - /> + {onReusePrompt && ( + } + label="Reuse Prompt" + onClick={() => handleAction(onReusePrompt)} + /> + )} + {onUseAsReference && ( + } + label="Use as Reference" + onClick={() => handleAction(onUseAsReference)} + disabled={!song.audioUrl} + /> + )} + {onCoverSong && ( + } + label="Cover Song" + onClick={() => handleAction(onCoverSong)} + disabled={!song.audioUrl} + /> + )} diff --git a/components/SongList.tsx b/components/SongList.tsx index b126869..4584858 100644 --- a/components/SongList.tsx +++ b/components/SongList.tsx @@ -25,6 +25,8 @@ interface SongListProps { 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 ... @@ -41,6 +43,38 @@ const FILTERS: { id: FilterType; label: string; icon: React.ReactNode }[] = [ { id: 'generating', label: 'Generating', icon: }, ]; +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, @@ -59,7 +93,9 @@ export const SongList: React.FC = ({ onDelete, onDeleteMany, onUseAsReference, - onCoverSong + onCoverSong, + onUseUploadAsReference, + onCoverUpload }) => { const { user } = useAuth(); const [searchQuery, setSearchQuery] = useState(''); @@ -350,6 +386,8 @@ export const SongList: React.FC = ({ isPublic: false, } as Song); }} + onUseAsReference={() => onUseUploadAsReference?.(item.track)} + onCoverSong={() => onCoverUpload?.(item.track)} /> ) )) @@ -422,6 +460,18 @@ const SongItem: React.FC = ({ 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' : ''}`} > @@ -617,7 +667,7 @@ const SongItem: React.FC = ({ onClose={() => setShowDropdown(false)} isOwner={isOwner} onCreateVideo={() => onOpenVideo?.(song)} - onReusePrompt={() => onReusePrompt?.(song)} + onReusePrompt={onReusePrompt ? () => onReusePrompt?.(song) : undefined} onAddToPlaylist={() => onAddToPlaylist?.(song)} onDelete={() => onDelete?.(song)} onShare={() => setShareModalOpen(true)} @@ -651,50 +701,46 @@ const SongItem: React.FC = ({ const UploadItem: React.FC<{ track: { id: string; filename: string; audio_url: string; duration?: number | null }; onPlay: (audioUrl: string, title: string) => void; -}> = ({ track, onPlay }) => { + 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 ( -
{ - e.dataTransfer.effectAllowed = 'copy'; - e.dataTransfer.setData('application/x-ace-audio', JSON.stringify({ - url: track.audio_url, - title, - source: 'upload', - })); - }} - > -
- -
{ - e.stopPropagation(); - onPlay(track.audio_url, title); - }} - > -
- -
-
-
- -
-
- {title} -
-
Upload
-
- -
- {duration} -
-
+ 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} + /> ); };