Better now playing and detail of album

This commit is contained in:
2025-12-14 22:25:57 +08:00
parent 49854b44e1
commit a64c613d00
8 changed files with 765 additions and 283 deletions

View File

@@ -1,18 +1,24 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:media_kit/media_kit.dart';
import '../../providers/audio_provider.dart';
import '../../logic/metadata_service.dart';
import '../screens/player_screen.dart';
class MiniPlayer extends HookConsumerWidget {
const MiniPlayer({super.key});
final bool enableTapToOpen;
const MiniPlayer({super.key, this.enableTapToOpen = true});
@override
Widget build(BuildContext context, WidgetRef ref) {
final audioHandler = ref.watch(audioHandlerProvider);
final player = audioHandler.player;
final isDragging = useState(false);
final dragValue = useState(0.0);
return StreamBuilder<Playlist>(
stream: player.stream.playlist,
initialData: player.state.playlist,
@@ -29,95 +35,163 @@ class MiniPlayer extends HookConsumerWidget {
final metadataAsync = ref.watch(trackMetadataProvider(filePath));
return GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
fullscreenDialog: true,
builder: (_) => const PlayerScreen(),
),
);
},
child: Container(
height: 64,
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
border: Border(
top: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant,
width: 1,
),
Widget content = Container(
height: 80, // Increased height for slider
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
border: Border(
top: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant,
width: 1,
),
),
child: Row(
children: [
// Cover Art (Small)
AspectRatio(
aspectRatio: 1,
child: metadataAsync.when(
data: (meta) => meta.artBytes != null
? Image.memory(meta.artBytes!, fit: BoxFit.cover)
: Container(
color: Colors.grey[800],
child: const Icon(
Icons.music_note,
color: Colors.white54,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Progress Bar
SizedBox(
height: 4,
width: double.infinity,
child: StreamBuilder<Duration>(
stream: player.stream.position,
initialData: player.state.position,
builder: (context, snapshot) {
final position = snapshot.data ?? Duration.zero;
return StreamBuilder<Duration>(
stream: player.stream.duration,
initialData: player.state.duration,
builder: (context, durationSnapshot) {
final total = durationSnapshot.data ?? Duration.zero;
final max = total.inMilliseconds.toDouble();
final positionValue = position.inMilliseconds
.toDouble()
.clamp(0.0, max > 0 ? max : 0.0);
final currentValue = isDragging.value
? dragValue.value
: positionValue;
return SliderTheme(
data: SliderTheme.of(context).copyWith(
// Let's keep a small thumb or make it visible on hover/touch.
// Standard Slider has a thumb.
trackHeight: 2,
overlayShape: SliderComponentShape.noOverlay,
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 6,
),
),
loading: () => Container(color: Colors.grey[800]),
error: (_, __) => Container(color: Colors.grey[800]),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
metadataAsync.when(
data: (meta) => Text(
meta.title ??
Uri.parse(media.uri).pathSegments.last,
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: Slider(
value: currentValue,
min: 0,
max: max > 0 ? max : 1.0,
onChanged: (val) {
isDragging.value = true;
dragValue.value = val;
},
onChangeEnd: (val) {
isDragging.value = false;
player.seek(Duration(milliseconds: val.toInt()));
},
),
loading: () => const Text('Loading...'),
error: (_, __) =>
Text(Uri.parse(media.uri).pathSegments.last),
),
metadataAsync.when(
data: (meta) => Text(
meta.artist ?? 'Unknown Artist',
style: Theme.of(context).textTheme.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
loading: () => const SizedBox.shrink(),
error: (_, __) => const SizedBox.shrink(),
),
],
),
),
),
StreamBuilder<bool>(
stream: player.stream.playing,
builder: (context, snapshot) {
final playing = snapshot.data ?? false;
return IconButton(
icon: Icon(playing ? Icons.pause : Icons.play_arrow),
onPressed: playing ? player.pause : player.play,
);
},
);
},
),
const SizedBox(width: 8),
],
),
),
Expanded(
child: Row(
children: [
// Cover Art (Small)
Padding(
padding: const EdgeInsets.all(8.0),
child: AspectRatio(
aspectRatio: 1,
child: metadataAsync.when(
data: (meta) => meta.artBytes != null
? Image.memory(meta.artBytes!, fit: BoxFit.cover)
: Container(
color: Colors.grey[800],
child: const Icon(
Icons.music_note,
color: Colors.white54,
),
),
loading: () => Container(color: Colors.grey[800]),
error: (_, __) => Container(color: Colors.grey[800]),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
metadataAsync.when(
data: (meta) => Text(
meta.title ??
Uri.parse(media.uri).pathSegments.last,
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
loading: () => const Text('Loading...'),
error: (_, __) =>
Text(Uri.parse(media.uri).pathSegments.last),
),
metadataAsync.when(
data: (meta) => Text(
meta.artist ?? 'Unknown Artist',
style: Theme.of(context).textTheme.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
loading: () => const SizedBox.shrink(),
error: (_, __) => const SizedBox.shrink(),
),
],
),
),
),
StreamBuilder<bool>(
stream: player.stream.playing,
builder: (context, snapshot) {
final playing = snapshot.data ?? false;
return IconButton(
icon: Icon(playing ? Icons.pause : Icons.play_arrow),
onPressed: playing ? player.pause : player.play,
);
},
),
const SizedBox(width: 8),
],
),
),
],
),
);
if (enableTapToOpen) {
return GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
fullscreenDialog: true,
builder: (_) => const PlayerScreen(),
),
);
},
child: content,
);
} else {
return content;
}
},
);
}