import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../state/providers.dart'; import '../theme/tokens.dart'; /// Persistent mini-player pinned above the tab bar. Visible only while a track /// is loaded; tapping the body jumps to the Now Playing tab. /// /// Watches `current`/`playing` via `.select()` so it doesn't rebuild on every /// position tick — the moving progress line is isolated in [_MiniProgress]. class MiniPlayer extends ConsumerWidget { const MiniPlayer({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { // Redundant (and steals vertical space) on the Now Playing tab itself. if (ref.watch(selectedTabProvider) == nowPlayingTabIndex) { return const SizedBox.shrink(); } final current = ref.watch(activePlaybackProvider.select((s) => s.current)); if (current == null) return const SizedBox.shrink(); final playing = ref.watch(activePlaybackProvider.select((s) => s.playing)); final controller = ref.read(playbackCommandsProvider); final client = ref.watch(subsonicClientProvider); final accent = Theme.of(context).colorScheme.primary; final artUri = (client != null && current.coverArt != null) ? client.coverArtUri(current.coverArt!, size: 128).toString() : null; return Column( mainAxisSize: MainAxisSize.min, children: [ const _MiniProgress(), InkWell( onTap: () => ref.read(selectedTabProvider.notifier).state = nowPlayingTabIndex, child: Container( height: 52, color: TimbreColors.surface, padding: const EdgeInsets.only(left: TimbreSpacing.md), child: Row( children: [ SizedBox( width: 40, height: 40, child: ColoredBox( color: TimbreColors.background, child: artUri != null ? Image.network( artUri, key: ValueKey(artUri), fit: BoxFit.cover, gaplessPlayback: true, errorBuilder: (_, _, _) => const _ArtFallback(), ) : const _ArtFallback(), ), ), const SizedBox(width: TimbreSpacing.md), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( current.title ?? 'Untitled', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: TimbreColors.foreground, fontWeight: FontWeight.w700, ), ), Text( current.artist ?? 'Unknown artist', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: TimbreColors.dimmed), ), ], ), ), _btn(Icons.skip_previous, controller.previous), _btn(playing ? Icons.pause : Icons.play_arrow, controller.togglePlayPause, color: accent), _btn(Icons.skip_next, controller.next), ], ), ), ), ], ); } Widget _btn(IconData icon, VoidCallback onTap, {Color? color}) { return IconButton( onPressed: onTap, visualDensity: VisualDensity.compact, icon: Icon(icon, color: color ?? TimbreColors.foreground, size: 24), ); } } /// The hairline progress line on the top edge. Isolated so only this 2px strip /// rebuilds on position ticks. class _MiniProgress extends ConsumerWidget { const _MiniProgress(); @override Widget build(BuildContext context, WidgetRef ref) { final progress = ref.watch(activePlaybackProvider.select((s) => s.progress)); final accent = Theme.of(context).colorScheme.primary; return SizedBox( height: 2, child: LinearProgressIndicator( value: progress, minHeight: 2, backgroundColor: TimbreColors.border, valueColor: AlwaysStoppedAnimation(accent), ), ); } } class _ArtFallback extends StatelessWidget { const _ArtFallback(); @override Widget build(BuildContext context) => Center( child: Icon(Icons.album_outlined, color: TimbreColors.dimmed, size: 22), ); }