import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../app/theme.dart'; import '../../audio/output_device.dart'; import '../../audio/player_providers.dart'; import '../../core/models.dart'; import 'artwork.dart'; /// Persistent bar above the tabs. Tap opens Now Playing; swipe left/right skips. class MiniPlayer extends ConsumerWidget { const MiniPlayer({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final item = ref.watch(mediaItemProvider).value; if (item == null) return const SizedBox.shrink(); final playing = ref.watch(playbackStateProvider).value?.playing ?? false; final position = ref.watch(positionProvider).value ?? Duration.zero; final total = item.duration ?? Duration.zero; final progress = total.inMilliseconds == 0 ? 0.0 : (position.inMilliseconds / total.inMilliseconds).clamp(0.0, 1.0); final handler = ref.read(audioHandlerProvider); final output = ref.watch(outputDeviceProvider).value; return Padding( padding: const EdgeInsets.fromLTRB(10, 6, 10, 4), child: GestureDetector( onHorizontalDragEnd: (d) { final v = d.primaryVelocity ?? 0; if (v < -300) handler.skipToNext(); if (v > 300) handler.skipToPrevious(); }, child: Material( color: NkColors.miniPlayer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: const BorderSide(color: NkColors.outline), ), clipBehavior: Clip.antiAlias, child: InkWell( onTap: () => context.push('/player'), child: Stack( children: [ Padding( padding: const EdgeInsets.all(8), child: Row( children: [ Hero( tag: 'now-playing-art', child: TrackArtwork( coverUrl: item.artUri?.toString(), coverAsset: item.extras?['coverAsset'] as String?, occasion: Occasion.fromId(item.extras?['occasion'] as String?), size: 48, ), ), const SizedBox(width: 12), Expanded( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15)), Row( children: [ if (output != null && output != OutputDevice.speaker) ...[ Icon(output.icon, size: 14, color: NkColors.teaLeaf), const SizedBox(width: 4), ], Expanded( child: Text( output != null && output != OutputDevice.speaker ? output.label : (item.artist ?? ''), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: output != null && output != OutputDevice.speaker ? NkColors.teaLeaf : NkColors.muted, fontSize: 13, ), ), ), ], ), ], ), ), IconButton( tooltip: 'Previous', visualDensity: VisualDensity.compact, icon: const Icon(Icons.skip_previous_rounded), onPressed: handler.skipToPrevious, ), IconButton( tooltip: playing ? 'Pause' : 'Play', iconSize: 32, visualDensity: VisualDensity.compact, icon: Icon(playing ? Icons.pause_rounded : Icons.play_arrow_rounded), onPressed: () => ref.read(playerControllerProvider).togglePlay(), ), IconButton( tooltip: 'Next', visualDensity: VisualDensity.compact, icon: const Icon(Icons.skip_next_rounded), onPressed: handler.skipToNext, ), ], ), ), Positioned( left: 0, bottom: 0, right: 0, child: Align( alignment: Alignment.centerLeft, child: FractionallySizedBox( widthFactor: progress, child: Container(height: 3, color: NkColors.sun), ), ), ), ], ), ), ), ), ); } }