139 lines
4.7 KiB
Dart
139 lines
4.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../state/providers.dart';
|
|
import '../theme/tokens.dart';
|
|
import 'art_image.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 accent = Theme.of(context).colorScheme.primary;
|
|
final artUri = resolveArtUriW(
|
|
ref,
|
|
coverArt: current.coverArt,
|
|
size: 128,
|
|
)?.toString();
|
|
|
|
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,
|
|
// Keep the mini player's darker `background` fill behind the
|
|
// art (ArtImage's own fill is `surface`) by handing it a
|
|
// background-tinted placeholder for the null/error cases.
|
|
child: ArtImage(
|
|
artUri,
|
|
fit: BoxFit.cover,
|
|
placeholder: ColoredBox(
|
|
color: TimbreColors.background,
|
|
child: 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<Color>(accent),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ArtFallback extends StatelessWidget {
|
|
const _ArtFallback();
|
|
@override
|
|
Widget build(BuildContext context) => Center(
|
|
child: Icon(Icons.album_outlined, color: TimbreColors.dimmed, size: 22),
|
|
);
|
|
}
|