import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/theme/colors.dart'; import '../../../domain/entities/track.dart'; import '../../providers/music_provider.dart'; import '../../widgets/player/queue_track_tile.dart'; /// Queue View Page /// /// Complete queue management interface with: /// - Now Playing section (top) /// - Queue list section (bottom) /// - Swipe to remove /// - Drag to reorder /// - Clear queue functionality class QueueViewPage extends ConsumerStatefulWidget { const QueueViewPage({super.key}); @override ConsumerState createState() => _QueueViewPageState(); } class _QueueViewPageState extends ConsumerState { @override Widget build(BuildContext context) { final queueData = ref.watch(queueProvider); return Scaffold( backgroundColor: AppColors.primary, body: SafeArea( child: Column( children: [ // Header _buildHeader(queueData), // Content Expanded( child: queueData.hasQueue ? Column( children: [ // Now Playing Section _buildNowPlayingSection(queueData), const SizedBox(height: 8), // Queue Section Expanded( child: _buildQueueSection(queueData), ), ], ) : _buildEmptyQueue(), ), ], ), ), ); } Widget _buildHeader(QueueViewData queueData) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: AppColors.surface, border: Border( bottom: BorderSide( color: AppColors.cyan.withOpacity(0.2), width: 1, ), ), boxShadow: [ BoxShadow( color: AppColors.cyan.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Row( children: [ // Back button IconButton( icon: const Icon(Icons.arrow_back), color: AppColors.onSurface, onPressed: () => Navigator.of(context).pop(), ), const SizedBox(width: 8), // Title Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ const Text( 'Queue', style: TextStyle( color: AppColors.onSurface, fontSize: 18, fontWeight: FontWeight.w600, ), ), if (queueData.hasQueue) Text( '${queueData.queueCount} tracks', style: const TextStyle( color: AppColors.muted, fontSize: 12, ), ), ], ), ), // Clear queue button if (queueData.hasNextTracks) TextButton.icon( onPressed: () => _showClearQueueDialog(queueData), icon: const Icon( Icons.clear_all, size: 18, color: AppColors.rouge, ), label: const Text( 'Clear', style: TextStyle( color: AppColors.rouge, fontSize: 14, fontWeight: FontWeight.w500, ), ), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), backgroundColor: AppColors.rouge.withOpacity(0.1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ), ], ), ); } Widget _buildNowPlayingSection(QueueViewData queueData) { final currentTrack = queueData.currentTrack; if (currentTrack == null) { return const SizedBox.shrink(); } return Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ AppColors.surfaceVariant, AppColors.surfaceElevated, ], ), borderRadius: BorderRadius.circular(16), border: Border.all( color: AppColors.cyan.withOpacity(0.3), width: 1, ), boxShadow: [ BoxShadow( color: AppColors.cyan.withOpacity(0.1), blurRadius: 20, spreadRadius: 2, ), ], ), child: Column( children: [ // Section label Row( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: AppColors.cyan.withOpacity(0.2), borderRadius: BorderRadius.circular(6), ), child: const Text( 'NOW PLAYING', style: TextStyle( color: AppColors.cyan, fontSize: 10, fontWeight: FontWeight.w600, letterSpacing: 1, ), ), ), const Spacer(), _buildPlayingIndicator(queueData.isPlaying), ], ), const SizedBox(height: 16), // Track info Row( children: [ // Album art _buildLargeAlbumArt(currentTrack), const SizedBox(width: 16), // Track details Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( currentTrack.title, style: const TextStyle( color: AppColors.onSurface, fontSize: 16, fontWeight: FontWeight.w600, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( currentTrack.artist?.name ?? 'Unknown Artist', style: const TextStyle( color: AppColors.muted, fontSize: 14, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), Row( children: [ _buildControlButton( icon: Icons.skip_previous, onTap: () => _playPrevious(), ), const SizedBox(width: 12), _buildPlayPauseButton(queueData.isPlaying), const SizedBox(width: 12), _buildControlButton( icon: Icons.skip_next, onTap: () => _playNext(), ), ], ), ], ), ), ], ), ], ), ); } Widget _buildLargeAlbumArt(Track track) { return Container( width: 80, height: 80, decoration: BoxDecoration( gradient: AppColors.accentGradient, borderRadius: BorderRadius.circular(12), boxShadow: AppColors.violetGlow, ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: track.imageUrl != null ? Image.network( track.imageUrl!, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return const Icon( Icons.music_note, color: AppColors.onBackground, size: 40, ); }, ) : const Icon( Icons.music_note, color: AppColors.onBackground, size: 40, ), ), ); } Widget _buildPlayingIndicator(bool isPlaying) { if (!isPlaying) { return const SizedBox.shrink(); } return Row( children: [ Container( width: 6, height: 6, decoration: const BoxDecoration( color: AppColors.vert, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: AppColors.vert, blurRadius: 8, spreadRadius: 2, ), ], ), ), const SizedBox(width: 4), const Text( 'Playing', style: TextStyle( color: AppColors.vert, fontSize: 11, fontWeight: FontWeight.w500, ), ), ], ); } Widget _buildControlButton({ required IconData icon, required VoidCallback onTap, }) { return MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: AppColors.surfaceElevated, borderRadius: BorderRadius.circular(12), ), child: Icon( icon, color: AppColors.onSurface, size: 24, ), ), ), ); } Widget _buildPlayPauseButton(bool isPlaying) { return MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () => _togglePlayPause(isPlaying), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( gradient: AppColors.primaryGradient, borderRadius: BorderRadius.circular(12), boxShadow: AppColors.cyanGlow, ), child: Icon( isPlaying ? Icons.pause : Icons.play_arrow, color: AppColors.primary, size: 28, ), ), ), ); } Widget _buildQueueSection(QueueViewData queueData) { if (!queueData.hasNextTracks) { return const SizedBox.shrink(); } return Column( children: [ // Queue header Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: AppColors.violet.withOpacity(0.2), borderRadius: BorderRadius.circular(6), ), child: const Text( 'NEXT UP', style: TextStyle( color: AppColors.violet, fontSize: 10, fontWeight: FontWeight.w600, letterSpacing: 1, ), ), ), const SizedBox(width: 8), Text( '${queueData.nextTracks.length} tracks', style: const TextStyle( color: AppColors.muted, fontSize: 12, ), ), ], ), ), const SizedBox(height: 8), // Queue list Expanded( child: ReorderableListView.builder( padding: const EdgeInsets.only(bottom: 16), itemCount: queueData.nextTracks.length, onReorder: (oldIndex, newIndex) { _reorderQueue(oldIndex, newIndex, queueData); }, itemBuilder: (context, index) { final track = queueData.nextTracks[index]; final actualIndex = queueData.currentIndex + 1 + index; return Dismissible( key: Key('queue_${track.id}_$index'), direction: DismissDirection.endToStart, onDismissed: (direction) { _removeFromQueue(actualIndex); }, background: Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), decoration: BoxDecoration( color: AppColors.rouge, borderRadius: BorderRadius.circular(12), ), alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 20), child: const Icon( Icons.delete, color: AppColors.primary, ), ), child: QueueTrackTile( key: Key('queue_${track.id}_$index'), track: track, index: index, onTap: () => _playTrack(actualIndex), onRemove: () => _removeFromQueue(actualIndex), ), ); }, ), ), ], ); } Widget _buildEmptyQueue() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 120, height: 120, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ AppColors.cyan.withOpacity(0.2), AppColors.violet.withOpacity(0.2), ], ), borderRadius: BorderRadius.circular(30), ), child: const Icon( Icons.queue_music, color: AppColors.muted, size: 60, ), ), const SizedBox(height: 24), const Text( 'Queue is empty', style: TextStyle( color: AppColors.onSurface, fontSize: 20, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), const Text( 'Add tracks to build your queue', style: TextStyle( color: AppColors.muted, fontSize: 14, ), ), ], ), ); } void _togglePlayPause(bool isPlaying) { final notifier = ref.read(playerProvider.notifier); if (isPlaying) { notifier.pause(); } else { notifier.play(); } } void _playNext() { ref.read(playerProvider.notifier).next(); } void _playPrevious() { ref.read(playerProvider.notifier).previous(); } void _playTrack(int index) { final queueData = ref.read(queueProvider); final track = queueData.queue[index]; ref.read(playerProvider.notifier).loadTrack(track).then((_) { ref.read(playerProvider.notifier).play(); }); } void _removeFromQueue(int index) { ref.read(playerProvider.notifier).removeFromQueue(index); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Removed from queue'), backgroundColor: AppColors.surfaceElevated, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), duration: const Duration(seconds: 2), ), ); } void _reorderQueue(int oldIndex, int newIndex, QueueViewData queueData) { if (oldIndex == newIndex) return; // Adjust for the actual queue position final actualOldIndex = queueData.currentIndex + 1 + oldIndex; int actualNewIndex = queueData.currentIndex + 1 + newIndex; if (newIndex > oldIndex) { actualNewIndex--; } final notifier = ref.read(playerProvider.notifier); final queue = List.from(queueData.queue); final item = queue.removeAt(actualOldIndex); queue.insert(actualNewIndex, item); notifier.setQueue(queue, startIndex: queueData.currentIndex); } void _showClearQueueDialog(QueueViewData queueData) { showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: AppColors.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), title: const Text( 'Clear Queue?', style: TextStyle( color: AppColors.onSurface, fontSize: 18, fontWeight: FontWeight.w600, ), ), content: Text( 'Remove all ${queueData.nextTracks.length} upcoming tracks from the queue?', style: const TextStyle( color: AppColors.muted, fontSize: 14, ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text( 'Cancel', style: TextStyle( color: AppColors.muted, fontSize: 14, fontWeight: FontWeight.w500, ), ), ), TextButton( onPressed: () { _clearQueue(queueData); Navigator.of(context).pop(); }, style: TextButton.styleFrom( backgroundColor: AppColors.rouge.withOpacity(0.2), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: const Text( 'Clear', style: TextStyle( color: AppColors.rouge, fontSize: 14, fontWeight: FontWeight.w600, ), ), ), ], ), ); } void _clearQueue(QueueViewData queueData) { if (queueData.currentTrack == null) return; // Keep only the current track ref.read(playerProvider.notifier).setQueue( [queueData.currentTrack!], startIndex: 0, ); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Queue cleared'), backgroundColor: AppColors.surfaceElevated, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), duration: Duration(seconds: 2), ), ); } }