nkbmusic/lib/shared/widgets/mini_player.dart
metatroncubeswdev 178d2d00ab Nanga Kathai: Flutter app, self-hosted backend and song tools
- Flutter app (Android/iOS): home, explore, albums, search, player with
  equalizer, mini player, sign up/sign in with email codes, profile,
  encrypted offline downloads, About page
- server/: one-command deploy of self-hosted Supabase behind HestiaCP/nginx
  (or Caddy), migrations, backups, email templates
- supabase/migrations: schema, row-level security, profiles
- tools/: song scanner and importer

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

116 lines
4.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/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);
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)),
Text(item.artist ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: 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),
),
),
),
],
),
),
),
),
);
}
}