init
This commit is contained in:
commit
d205277cdd
182 changed files with 22978 additions and 0 deletions
136
lib/widgets/mini_player.dart
Normal file
136
lib/widgets/mini_player.dart
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
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(playbackProvider.select((s) => s.current));
|
||||
if (current == null) return const SizedBox.shrink();
|
||||
|
||||
final playing = ref.watch(playbackProvider.select((s) => s.playing));
|
||||
final controller = ref.read(playbackProvider.notifier);
|
||||
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: RatuneColors.surface,
|
||||
padding: const EdgeInsets.only(left: RatuneSpacing.md),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: ColoredBox(
|
||||
color: RatuneColors.background,
|
||||
child: artUri != null
|
||||
? Image.network(
|
||||
artUri,
|
||||
key: ValueKey(artUri),
|
||||
fit: BoxFit.cover,
|
||||
gaplessPlayback: true,
|
||||
errorBuilder: (_, _, _) => const _ArtFallback(),
|
||||
)
|
||||
: const _ArtFallback(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: RatuneSpacing.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
current.title ?? 'Untitled',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: RatuneColors.foreground,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
current.artist ?? 'Unknown artist',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: RatuneColors.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 = RatuneColors.foreground}) {
|
||||
return IconButton(
|
||||
onPressed: onTap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: Icon(icon, color: color, 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(playbackProvider.select((s) => s.progress));
|
||||
final accent = Theme.of(context).colorScheme.primary;
|
||||
return SizedBox(
|
||||
height: 2,
|
||||
child: LinearProgressIndicator(
|
||||
value: progress,
|
||||
minHeight: 2,
|
||||
backgroundColor: RatuneColors.border,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(accent),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ArtFallback extends StatelessWidget {
|
||||
const _ArtFallback();
|
||||
@override
|
||||
Widget build(BuildContext context) => const Center(
|
||||
child: Icon(Icons.album_outlined,
|
||||
color: RatuneColors.dimmed, size: 22),
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue