import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/theme/colors.dart'; import '../../providers/music_provider.dart'; import '../../pages/player/queue_view_page.dart'; /// Mini Player Widget class MiniPlayer extends ConsumerWidget { final bool compact; const MiniPlayer({ super.key, this.compact = false, }); @override Widget build(BuildContext context, WidgetRef ref) { final playerState = ref.watch(playerProvider); final currentTrack = playerState.currentTrack; final isPlaying = playerState.isPlaying; return GestureDetector( onTap: () { // TODO: Open fullscreen player }, child: Container( height: 64, decoration: BoxDecoration( color: AppColors.surface, border: Border( top: 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: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ // Album art _buildAlbumArt(currentTrack), const SizedBox(width: 12), // Track info Expanded( child: _buildTrackInfo(currentTrack, playerState), ), const SizedBox(width: 12), // Controls if (!compact) _buildControls(ref, isPlaying) else _buildCompactControls(ref, isPlaying), // Queue button if (!compact) _buildQueueButton(context, ref), ], ), ), ), ); } Widget _buildAlbumArt(dynamic currentTrack) { return Container( width: 48, height: 48, decoration: BoxDecoration( gradient: AppColors.accentGradient, borderRadius: BorderRadius.circular(6), boxShadow: AppColors.violetGlow, ), child: currentTrack?.imageUrl != null ? ClipRRect( borderRadius: BorderRadius.circular(6), child: Image.network( currentTrack!.imageUrl!, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return const Icon( Icons.music_note, color: AppColors.onBackground, size: 24, ); }, ), ) : const Icon( Icons.music_note, color: AppColors.onBackground, size: 24, ), ); } Widget _buildTrackInfo(dynamic currentTrack, PlayerState playerState) { if (currentTrack == null) { return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: const [ Text( 'No track playing', style: TextStyle( color: AppColors.onSurface, fontSize: 14, fontWeight: FontWeight.w500, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), SizedBox(height: 2), Text( 'Tap to select music', style: TextStyle( color: AppColors.muted, fontSize: 12, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Row( children: [ if (playerState.isPlaying) Container( width: 4, height: 4, margin: const EdgeInsets.only(right: 6), decoration: const BoxDecoration( color: AppColors.vert, shape: BoxShape.circle, ), ), Expanded( child: Text( currentTrack.title, style: const TextStyle( color: AppColors.onSurface, fontSize: 14, fontWeight: FontWeight.w500, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 2), Text( currentTrack.artist?.name ?? 'Unknown Artist', style: const TextStyle( color: AppColors.muted, fontSize: 12, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ); } Widget _buildControls(WidgetRef ref, bool isPlaying) { return Row( children: [ _ControlButton( icon: Icons.skip_previous, onTap: () { ref.read(playerProvider.notifier).previous(); }, ), const SizedBox(width: 8), _ControlButton( icon: isPlaying ? Icons.pause : Icons.play_arrow, isPrimary: true, onTap: () { if (isPlaying) { ref.read(playerProvider.notifier).pause(); } else { ref.read(playerProvider.notifier).play(); } }, ), const SizedBox(width: 8), _ControlButton( icon: Icons.skip_next, onTap: () { ref.read(playerProvider.notifier).next(); }, ), ], ); } Widget _buildCompactControls(WidgetRef ref, bool isPlaying) { return _ControlButton( icon: isPlaying ? Icons.pause : Icons.play_arrow, isPrimary: true, size: 40, onTap: () { if (isPlaying) { ref.read(playerProvider.notifier).pause(); } else { ref.read(playerProvider.notifier).play(); } }, ); } Widget _buildQueueButton(BuildContext context, WidgetRef ref) { final queueData = ref.watch(queueProvider); return Row( children: [ const SizedBox(width: 8), MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () => _openQueueView(context), child: Container( width: 40, height: 40, decoration: BoxDecoration( color: AppColors.surfaceVariant, borderRadius: BorderRadius.circular(8), border: Border.all( color: queueData.hasNextTracks ? AppColors.violet.withOpacity(0.5) : Colors.transparent, width: 1, ), ), child: Stack( alignment: Alignment.center, children: [ const Icon( Icons.queue_music, color: AppColors.onSurface, size: 20, ), if (queueData.hasNextTracks) Positioned( top: 6, right: 6, child: Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration( color: AppColors.violet, shape: BoxShape.circle, ), child: Text( queueData.queueCount > 9 ? '9+' : '${queueData.queueCount}', style: const TextStyle( color: AppColors.primary, fontSize: 8, fontWeight: FontWeight.w600, ), ), ), ), ], ), ), ), ), ], ); } void _openQueueView(BuildContext context) { Navigator.of(context).push( PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) { return const QueueViewPage(); }, transitionsBuilder: (context, animation, secondaryAnimation, child) { const begin = Offset(0.0, 1.0); const end = Offset.zero; const curve = Curves.easeInOut; var tween = Tween(begin: begin, end: end).chain( CurveTween(curve: curve), ); return SlideTransition( position: animation.drive(tween), child: child, ); }, ), ); } } /// Control Button class _ControlButton extends StatefulWidget { final IconData icon; final bool isPrimary; final double? size; final VoidCallback onTap; const _ControlButton({ required this.icon, this.isPrimary = false, this.size, required this.onTap, }); @override State<_ControlButton> createState() => _ControlButtonState(); } class _ControlButtonState extends State<_ControlButton> with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _scaleAnimation; bool _isPressed = false; @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 100), vsync: this, ); _scaleAnimation = Tween(begin: 1.0, end: 0.95).animate( CurvedAnimation( parent: _animationController, curve: Curves.easeOut, ), ); } @override void dispose() { _animationController.dispose(); super.dispose(); } void _handleTapDown(TapDownDetails details) { setState(() => _isPressed = true); _animationController.forward(); } void _handleTapUp(TapUpDetails details) { setState(() => _isPressed = false); _animationController.reverse(); } void _handleTapCancel() { setState(() => _isPressed = false); _animationController.reverse(); } @override Widget build(BuildContext context) { final size = widget.size ?? (widget.isPrimary ? 50 : 40); return GestureDetector( onTapDown: _handleTapDown, onTapUp: _handleTapUp, onTapCancel: _handleTapCancel, onTap: widget.onTap, child: ScaleTransition( scale: _scaleAnimation, child: Container( width: size, height: size, decoration: BoxDecoration( color: widget.isPrimary ? AppColors.cyan : AppColors.surfaceVariant, shape: BoxShape.circle, boxShadow: widget.isPrimary ? AppColors.cyanGlow : null, ), child: Icon( widget.icon, color: widget.isPrimary ? AppColors.primary : AppColors.onSurface, size: size * 0.5, ), ), ), ); } }