nkbmusic/lib/shared/widgets/mini_player.dart
metatroncubeswdev ad8c4f1c6b App: speaker/Bluetooth picker, Settings & help, Privacy Policy, Terms, FAQ, song reports
- Now Playing shows where music plays and opens the phone's output switcher
- Settings hub: Wi-Fi-only downloads (default on), storage, sound, help, legal
- Privacy Policy and Terms bundled as Markdown (assets/legal), shown in-app
- 'Is this your song?' report form feeding takedown_requests (+ kind, user_id)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:00:16 -04:00

137 lines
5.6 KiB
Dart

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),
),
),
),
],
),
),
),
),
);
}
}