2026-01-07 09:31:02 +00:00
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
import { useAudio } from '../../../context/AudioContext';
|
|
|
|
|
import { Card } from '../../ui/card';
|
|
|
|
|
import { Button } from '../../ui/button';
|
|
|
|
|
import { SaveQueueAsPlaylistModal } from './SaveQueueAsPlaylistModal';
|
2026-01-13 18:47:57 +00:00
|
|
|
import {
|
|
|
|
|
Play,
|
|
|
|
|
Pause,
|
|
|
|
|
X,
|
|
|
|
|
GripVertical,
|
|
|
|
|
Trash2,
|
|
|
|
|
Save,
|
|
|
|
|
ListMusic,
|
|
|
|
|
} from 'lucide-react';
|
2026-01-26 13:12:17 +00:00
|
|
|
import { useToast } from '../../../components/feedback/ToastProvider';
|
2026-01-07 09:31:02 +00:00
|
|
|
|
|
|
|
|
export const QueueView: React.FC = () => {
|
2026-01-13 18:47:57 +00:00
|
|
|
const {
|
|
|
|
|
queue,
|
|
|
|
|
currentTrack,
|
|
|
|
|
reorderQueue,
|
|
|
|
|
removeFromQueue,
|
|
|
|
|
clearQueue,
|
|
|
|
|
playTrack,
|
|
|
|
|
isPlaying,
|
|
|
|
|
togglePlay,
|
|
|
|
|
autoplay,
|
|
|
|
|
toggleAutoplay,
|
|
|
|
|
} = useAudio();
|
|
|
|
|
const { addToast } = useToast();
|
|
|
|
|
const [showSaveModal, setShowSaveModal] = useState(false);
|
|
|
|
|
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
const handleDragStart = (e: React.DragEvent, index: number) => {
|
|
|
|
|
setDraggedIndex(index);
|
|
|
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
|
|
|
// Transparent ghost image
|
|
|
|
|
const ghost = document.createElement('div');
|
|
|
|
|
ghost.style.opacity = '0';
|
|
|
|
|
document.body.appendChild(ghost);
|
|
|
|
|
e.dataTransfer.setDragImage(ghost, 0, 0);
|
|
|
|
|
setTimeout(() => document.body.removeChild(ghost), 0);
|
|
|
|
|
};
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
const handleDragOver = (e: React.DragEvent, index: number) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (draggedIndex === null || draggedIndex === index) return;
|
|
|
|
|
reorderQueue(draggedIndex, index);
|
|
|
|
|
setDraggedIndex(index);
|
|
|
|
|
};
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
const handleSavePlaylist = (name: string, _isPublic: boolean) => {
|
|
|
|
|
addToast(`Queue saved as "${name}"`, 'success');
|
|
|
|
|
// Logic to actually save would connect to backend/context here
|
|
|
|
|
};
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
return (
|
|
|
|
|
<div className="max-w-4xl mx-auto space-y-6 animate-fadeIn pb-20">
|
|
|
|
|
<div className="flex flex-col md:flex-row justify-between items-end border-b border-kodo-steel/50 pb-6 gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-3xl font-display font-bold text-white mb-2">
|
|
|
|
|
PLAY QUEUE
|
|
|
|
|
</h1>
|
2026-01-16 00:59:31 +00:00
|
|
|
<p className="text-kodo-content-dim font-mono text-sm">
|
2026-01-13 18:47:57 +00:00
|
|
|
{queue.length} tracks upcoming
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
aesthetic-improvements: align spacing to 8px grid (Action 11.2.1.3)
- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid
- Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px)
- Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation
- Modified files across all components to ensure consistent 8px grid alignment
- Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
2026-01-16 10:50:46 +00:00
|
|
|
<div className="flex gap-4">
|
2026-01-13 18:47:57 +00:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={() => setShowSaveModal(true)}
|
|
|
|
|
icon={<Save className="w-4 h-4" />}
|
|
|
|
|
>
|
|
|
|
|
Save Queue
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
className="text-kodo-red hover:bg-kodo-red/10"
|
|
|
|
|
onClick={clearQueue}
|
|
|
|
|
icon={<Trash2 className="w-4 h-4" />}
|
|
|
|
|
>
|
|
|
|
|
Clear
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Current Track */}
|
|
|
|
|
{currentTrack && (
|
|
|
|
|
<div>
|
2026-01-16 00:59:31 +00:00
|
|
|
<h3 className="text-xs font-bold text-kodo-content-dim uppercase tracking-widest mb-3">
|
2026-01-13 18:47:57 +00:00
|
|
|
Now Playing
|
|
|
|
|
</h3>
|
|
|
|
|
<Card
|
2026-01-26 13:12:17 +00:00
|
|
|
variant="glass"
|
2026-01-13 18:47:57 +00:00
|
|
|
className="flex items-center gap-4 p-4 border-l-4 border-l-kodo-cyan"
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className="relative w-16 h-16 rounded overflow-hidden flex-shrink-0 group cursor-pointer"
|
|
|
|
|
onClick={togglePlay}
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
src={currentTrack.coverUrl}
|
|
|
|
|
className="w-full h-full object-cover"
|
|
|
|
|
/>
|
|
|
|
|
<div className="absolute inset-0 bg-black/40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
|
|
|
|
{isPlaying ? (
|
|
|
|
|
<Pause className="w-6 h-6 text-white" />
|
|
|
|
|
) : (
|
|
|
|
|
<Play className="w-6 h-6 text-white fill-current ml-1" />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{isPlaying && (
|
|
|
|
|
<div className="absolute bottom-1 right-1 flex gap-0.5 items-end h-3">
|
|
|
|
|
<div className="w-1 bg-kodo-cyan animate-[bounce_1s_infinite] h-full"></div>
|
|
|
|
|
<div className="w-1 bg-kodo-cyan animate-[bounce_1.2s_infinite] h-2/3"></div>
|
|
|
|
|
<div className="w-1 bg-kodo-cyan animate-[bounce_0.8s_infinite] h-full"></div>
|
2026-01-07 10:15:48 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
)}
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
<div className="flex-1">
|
|
|
|
|
<h2 className="text-xl font-bold text-white">
|
|
|
|
|
{currentTrack.title}
|
|
|
|
|
</h2>
|
aesthetic-improvements: reduce decorative cyan in player, library, and views (80/20 rule, batch 12)
- Player: VisualizerSettingsModal decorative icon (1 instance)
- Library: QueueView decorative artist text, AutoMetadataDetectionModal decorative icon and loading spinner border and fileName text and detected key text, SaveQueueAsPlaylistModal decorative icon, EditPlaylistModal decorative icon, PlaylistsView loading spinner, CreatePlaylistModal decorative icon (7 instances)
- Views: StudioView decorative icon, FileDetailsView decorative icon, GearView decorative icons and order number text, ProfileView loading spinner and social icons, AnalyticsView loading spinner and decorative chart legend dot and chart bars and device icon and revenue text, DiscoverView loading spinner and decorative icon and weekly mix text, FileManagerView decorative music icons (14 instances)
- Total: ~22 files, ~22 instances replaced
- Preserved: Active/selected states (LyricsPanel autoScroll active state, VisualizerSettingsModal selected mode, PlayerControls shuffle/repeatMode/showVisualizer active states, MiniPlayer isQueueOpen active state, AddToPlaylistModal selected playlist, PlaylistDetailView dragged item, StudioView active tab, SearchBar focused state, CheckoutView checkbox accents - focus/interaction, SearchPageView radio button accent - focus/interaction, FileManagerView selected files checkmarks - active states, ProfileView social links - functional links, LiveView links - functional links), primary actions, design system variants
- Action 11.3.1.3 in progress (twelfth batch: player, library, and views components)
2026-01-16 10:30:07 +00:00
|
|
|
<p className="text-kodo-steel">{currentTrack.artist}</p>
|
2026-01-13 18:47:57 +00:00
|
|
|
</div>
|
2026-01-16 00:59:31 +00:00
|
|
|
<div className="text-kodo-content-dim font-mono text-sm hidden md:block">
|
2026-01-13 18:47:57 +00:00
|
|
|
{currentTrack.duration}
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
{/* Up Next */}
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex justify-between items-center mb-3">
|
2026-01-16 00:59:31 +00:00
|
|
|
<h3 className="text-xs font-bold text-kodo-content-dim uppercase tracking-widest">
|
2026-01-13 18:47:57 +00:00
|
|
|
Up Next
|
|
|
|
|
</h3>
|
|
|
|
|
<div
|
|
|
|
|
className="flex items-center gap-2 cursor-pointer group"
|
|
|
|
|
onClick={toggleAutoplay}
|
|
|
|
|
>
|
|
|
|
|
<span
|
2026-01-16 00:59:31 +00:00
|
|
|
className={`text-xs font-bold ${autoplay ? 'text-kodo-lime' : 'text-kodo-content-dim'}`}
|
2026-01-13 18:47:57 +00:00
|
|
|
>
|
|
|
|
|
Autoplay
|
|
|
|
|
</span>
|
|
|
|
|
<div
|
2026-01-16 00:59:31 +00:00
|
|
|
className={`w-8 h-4 rounded-full relative transition-colors ${autoplay ? 'bg-kodo-lime' : 'bg-kodo-steel'}`}
|
2026-01-13 18:47:57 +00:00
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className={`absolute top-0.5 w-3 h-3 bg-white rounded-full transition-all ${autoplay ? 'left-4.5' : 'left-0.5'}`}
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
<div className="space-y-2">
|
|
|
|
|
{queue.length === 0 ? (
|
2026-01-16 00:59:31 +00:00
|
|
|
<div className="text-center py-12 border-2 border-dashed border-kodo-steel rounded-xl text-kodo-content-dim">
|
2026-01-13 18:47:57 +00:00
|
|
|
<ListMusic className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
|
|
|
|
<p className="text-sm">
|
|
|
|
|
Queue is empty. Add tracks to keep the vibe going.
|
|
|
|
|
</p>
|
|
|
|
|
{autoplay && (
|
|
|
|
|
<p className="text-xs text-kodo-lime mt-2">
|
|
|
|
|
Autoplay is on. We'll pick a song for you.
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
queue.map((track, i) => (
|
|
|
|
|
<div
|
|
|
|
|
key={`${track.id}-${i}`}
|
|
|
|
|
draggable
|
|
|
|
|
onDragStart={(e) => handleDragStart(e, i)}
|
|
|
|
|
onDragOver={(e) => handleDragOver(e, i)}
|
|
|
|
|
onDragEnd={() => setDraggedIndex(null)}
|
aesthetic-improvements: align spacing to 8px grid (Action 11.2.1.3)
- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid
- Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px)
- Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation
- Modified files across all components to ensure consistent 8px grid alignment
- Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
2026-01-16 10:50:46 +00:00
|
|
|
className={`flex items-center gap-4 p-4 bg-kodo-ink rounded-lg border border-transparent hover:border-kodo-steel transition-all group ${draggedIndex === i ? 'opacity-50 border-kodo-cyan' : ''}`}
|
2026-01-13 18:47:57 +00:00
|
|
|
>
|
2026-01-16 00:59:31 +00:00
|
|
|
<div className="text-kodo-content-dim cursor-grab active:cursor-grabbing hover:text-white p-1">
|
2026-01-13 18:47:57 +00:00
|
|
|
<GripVertical className="w-5 h-5" />
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
<div className="w-10 h-10 rounded overflow-hidden flex-shrink-0 relative">
|
|
|
|
|
<img
|
|
|
|
|
src={track.coverUrl}
|
|
|
|
|
className="w-full h-full object-cover"
|
|
|
|
|
/>
|
|
|
|
|
<div
|
|
|
|
|
className="absolute inset-0 bg-black/50 hidden group-hover:flex items-center justify-center cursor-pointer"
|
|
|
|
|
onClick={() => playTrack(track)}
|
|
|
|
|
>
|
|
|
|
|
<Play className="w-4 h-4 text-white fill-current" />
|
|
|
|
|
</div>
|
2026-01-07 10:15:48 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<div className="text-sm font-bold text-white truncate">
|
|
|
|
|
{track.title}
|
|
|
|
|
</div>
|
2026-01-16 00:59:31 +00:00
|
|
|
<div className="text-xs text-kodo-content-dim truncate">
|
2026-01-13 18:47:57 +00:00
|
|
|
{track.artist}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-16 00:59:31 +00:00
|
|
|
<div className="text-kodo-content-dim font-mono text-xs hidden sm:block">
|
2026-01-13 18:47:57 +00:00
|
|
|
{track.duration}
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
2026-01-16 00:59:31 +00:00
|
|
|
className="p-2 text-kodo-content-dim hover:text-kodo-red opacity-0 group-hover:opacity-100 transition-opacity"
|
2026-01-13 18:47:57 +00:00
|
|
|
onClick={() => removeFromQueue(track.id)}
|
|
|
|
|
>
|
|
|
|
|
<X className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
))
|
|
|
|
|
)}
|
2026-01-07 10:15:48 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{showSaveModal && (
|
|
|
|
|
<SaveQueueAsPlaylistModal
|
|
|
|
|
onClose={() => setShowSaveModal(false)}
|
|
|
|
|
onSave={handleSavePlaylist}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|