💄 Enchanced mini player
This commit is contained in:
@@ -1,17 +1,37 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:groovybox/data/db.dart' as db;
|
||||||
import 'package:groovybox/logic/metadata_service.dart';
|
import 'package:groovybox/logic/metadata_service.dart';
|
||||||
import 'package:groovybox/providers/audio_provider.dart';
|
import 'package:groovybox/providers/audio_provider.dart';
|
||||||
import 'package:groovybox/ui/screens/player_screen.dart';
|
import 'package:groovybox/ui/screens/player_screen.dart';
|
||||||
|
import 'package:groovybox/ui/widgets/track_tile.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
class MiniPlayer extends HookConsumerWidget {
|
class MiniPlayer extends HookConsumerWidget {
|
||||||
final bool enableTapToOpen;
|
final bool enableTapToOpen;
|
||||||
|
|
||||||
const MiniPlayer({super.key, this.enableTapToOpen = true});
|
const MiniPlayer({super.key, this.enableTapToOpen = true});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isDesktop = MediaQuery.sizeOf(context).width > 800;
|
||||||
|
|
||||||
|
if (isDesktop) {
|
||||||
|
return _DesktopMiniPlayer(enableTapToOpen: enableTapToOpen);
|
||||||
|
} else {
|
||||||
|
return _MobileMiniPlayer(enableTapToOpen: enableTapToOpen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobileMiniPlayer extends HookConsumerWidget {
|
||||||
|
final bool enableTapToOpen;
|
||||||
|
|
||||||
|
const _MobileMiniPlayer({required this.enableTapToOpen});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final audioHandler = ref.watch(audioHandlerProvider);
|
final audioHandler = ref.watch(audioHandlerProvider);
|
||||||
@@ -31,7 +51,6 @@ class MiniPlayer extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
final media = medias[index];
|
final media = medias[index];
|
||||||
final path = Uri.parse(media.uri).path;
|
final path = Uri.parse(media.uri).path;
|
||||||
// Using common parse for path if it's a file URI
|
|
||||||
final filePath = Uri.decodeFull(path);
|
final filePath = Uri.decodeFull(path);
|
||||||
|
|
||||||
final metadataAsync = ref.watch(trackMetadataProvider(filePath));
|
final metadataAsync = ref.watch(trackMetadataProvider(filePath));
|
||||||
@@ -76,8 +95,6 @@ class MiniPlayer extends HookConsumerWidget {
|
|||||||
|
|
||||||
return SliderTheme(
|
return SliderTheme(
|
||||||
data: SliderTheme.of(context).copyWith(
|
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,
|
trackHeight: 2,
|
||||||
overlayShape: SliderComponentShape.noOverlay,
|
overlayShape: SliderComponentShape.noOverlay,
|
||||||
thumbShape: const RoundSliderThumbShape(
|
thumbShape: const RoundSliderThumbShape(
|
||||||
@@ -108,7 +125,7 @@ class MiniPlayer extends HookConsumerWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// Cover Art (Small)
|
// Cover Art
|
||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
child: metadataAsync.when(
|
child: metadataAsync.when(
|
||||||
@@ -126,6 +143,7 @@ class MiniPlayer extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
|
// Title & Artist
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
@@ -160,13 +178,14 @@ class MiniPlayer extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Play/Pause Button
|
||||||
StreamBuilder<bool>(
|
StreamBuilder<bool>(
|
||||||
stream: player.stream.playing,
|
stream: player.stream.playing,
|
||||||
initialData: player.state.playing,
|
initialData: player.state.playing,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final playing = snapshot.data ?? false;
|
final playing = snapshot.data ?? false;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: IconButton.filled(
|
child: IconButton.filled(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
@@ -190,6 +209,12 @@ class MiniPlayer extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
// Next Button
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.skip_next),
|
||||||
|
onPressed: player.next,
|
||||||
|
iconSize: 24,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -216,3 +241,505 @@ class MiniPlayer extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _DesktopMiniPlayer extends HookConsumerWidget {
|
||||||
|
final bool enableTapToOpen;
|
||||||
|
|
||||||
|
const _DesktopMiniPlayer({required this.enableTapToOpen});
|
||||||
|
|
||||||
|
@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,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final index = snapshot.data?.index ?? 0;
|
||||||
|
final medias = snapshot.data?.medias ?? [];
|
||||||
|
if (medias.isEmpty || index < 0 || index >= medias.length) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
final media = medias[index];
|
||||||
|
final path = Uri.parse(media.uri).path;
|
||||||
|
final filePath = Uri.decodeFull(path);
|
||||||
|
|
||||||
|
final metadataAsync = ref.watch(trackMetadataProvider(filePath));
|
||||||
|
|
||||||
|
Widget content = Container(
|
||||||
|
height: 72,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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(
|
||||||
|
trackHeight: 2,
|
||||||
|
overlayShape: SliderComponentShape.noOverlay,
|
||||||
|
thumbShape: const RoundSliderThumbShape(
|
||||||
|
enabledThumbRadius: 6,
|
||||||
|
),
|
||||||
|
trackShape: const RectangularSliderTrackShape(),
|
||||||
|
),
|
||||||
|
child: Slider(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
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()));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
flex: 3,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Cover Art
|
||||||
|
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]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
// Title & Artist
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Playback Controls
|
||||||
|
Flexible(
|
||||||
|
flex: 7,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Loop Toggle
|
||||||
|
StreamBuilder<PlaylistMode>(
|
||||||
|
stream: player.stream.playlistMode,
|
||||||
|
initialData: player.state.playlistMode,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final mode = snapshot.data ?? PlaylistMode.none;
|
||||||
|
IconData icon;
|
||||||
|
Color? color;
|
||||||
|
switch (mode) {
|
||||||
|
case PlaylistMode.none:
|
||||||
|
icon = Icons.repeat;
|
||||||
|
color = Theme.of(context).disabledColor;
|
||||||
|
break;
|
||||||
|
case PlaylistMode.single:
|
||||||
|
icon = Icons.repeat_one;
|
||||||
|
color = Theme.of(context).colorScheme.primary;
|
||||||
|
break;
|
||||||
|
case PlaylistMode.loop:
|
||||||
|
icon = Icons.repeat;
|
||||||
|
color = Theme.of(context).colorScheme.primary;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return IconButton(
|
||||||
|
icon: Icon(icon, color: color),
|
||||||
|
onPressed: () {
|
||||||
|
final mode = player.state.playlistMode;
|
||||||
|
switch (mode) {
|
||||||
|
case PlaylistMode.none:
|
||||||
|
player.setPlaylistMode(
|
||||||
|
PlaylistMode.single,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case PlaylistMode.single:
|
||||||
|
player.setPlaylistMode(PlaylistMode.loop);
|
||||||
|
break;
|
||||||
|
case PlaylistMode.loop:
|
||||||
|
player.setPlaylistMode(PlaylistMode.none);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
iconSize: 20,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.skip_previous),
|
||||||
|
onPressed: player.previous,
|
||||||
|
iconSize: 24,
|
||||||
|
),
|
||||||
|
StreamBuilder<bool>(
|
||||||
|
stream: player.stream.playing,
|
||||||
|
initialData: player.state.playing,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final playing = snapshot.data ?? false;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
),
|
||||||
|
child: IconButton.filled(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primaryContainer,
|
||||||
|
icon: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
transitionBuilder:
|
||||||
|
(
|
||||||
|
Widget child,
|
||||||
|
Animation<double> animation,
|
||||||
|
) {
|
||||||
|
return ScaleTransition(
|
||||||
|
scale: animation,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Icon(
|
||||||
|
playing ? Icons.pause : Icons.play_arrow,
|
||||||
|
key: ValueKey<bool>(playing),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: playing
|
||||||
|
? player.pause
|
||||||
|
: player.play,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.skip_next),
|
||||||
|
onPressed: player.next,
|
||||||
|
iconSize: 24,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.queue_music),
|
||||||
|
onPressed: () =>
|
||||||
|
_showQueueDialog(context, ref, player),
|
||||||
|
iconSize: 24,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Volume Slider
|
||||||
|
Flexible(
|
||||||
|
flex: 3,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.volume_up,
|
||||||
|
size: 16,
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Expanded(
|
||||||
|
child: StreamBuilder<double>(
|
||||||
|
stream: player.stream.volume,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final volume = snapshot.data ?? 100.0;
|
||||||
|
return Slider(
|
||||||
|
value: volume,
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
divisions: 100,
|
||||||
|
label: volume.round().toString(),
|
||||||
|
onChanged: (value) {
|
||||||
|
player.setVolume(value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).padding(right: 24),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (enableTapToOpen) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
fullscreenDialog: true,
|
||||||
|
builder: (_) => const PlayerScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: content,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showQueueDialog(BuildContext context, WidgetRef ref, Player player) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (bottomSheetContext) => SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.7,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 12, 24, 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Text('Queue', style: TextStyle(fontSize: 20)),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () => Navigator.of(bottomSheetContext).pop(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Expanded(
|
||||||
|
child: StreamBuilder<Playlist>(
|
||||||
|
stream: player.stream.playlist,
|
||||||
|
initialData: player.state.playlist,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final playlist = snapshot.data;
|
||||||
|
if (playlist == null || playlist.medias.isEmpty) {
|
||||||
|
return const Center(child: Text('No tracks in queue'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReorderableListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: playlist.medias.length,
|
||||||
|
buildDefaultDragHandles: false,
|
||||||
|
onReorder: (oldIndex, newIndex) {
|
||||||
|
if (oldIndex < newIndex) {
|
||||||
|
newIndex -= 1;
|
||||||
|
}
|
||||||
|
player.move(oldIndex, newIndex);
|
||||||
|
},
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final media = playlist.medias[index];
|
||||||
|
final isCurrent = index == playlist.index;
|
||||||
|
final trackPath = Uri.decodeFull(
|
||||||
|
Uri.parse(media.uri).path,
|
||||||
|
);
|
||||||
|
final trackAsync = ref.watch(
|
||||||
|
trackByPathProvider(trackPath),
|
||||||
|
);
|
||||||
|
|
||||||
|
return trackAsync.when(
|
||||||
|
loading: () => SizedBox(
|
||||||
|
key: Key('loading_$index'),
|
||||||
|
height: 72,
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
error: (error, stack) => Dismissible(
|
||||||
|
key: Key('queue_item_error_$index'),
|
||||||
|
direction: DismissDirection.endToStart,
|
||||||
|
background: Container(
|
||||||
|
color: Colors.red,
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
padding: const EdgeInsets.only(right: 20),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDismissed: (direction) => player.remove(index),
|
||||||
|
child: TrackTile(
|
||||||
|
key: Key('track_tile_error_$index'),
|
||||||
|
leading: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
child: Text(
|
||||||
|
(index + 1).toString().padLeft(2, '0'),
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
track: db.Track(
|
||||||
|
id: -1,
|
||||||
|
path: trackPath,
|
||||||
|
title: Uri.parse(media.uri).pathSegments.last,
|
||||||
|
artist:
|
||||||
|
media.extras?['artist'] as String? ??
|
||||||
|
'Unknown Artist',
|
||||||
|
album: media.extras?['album'] as String?,
|
||||||
|
duration: null,
|
||||||
|
artUri: null,
|
||||||
|
lyrics: null,
|
||||||
|
lyricsOffset: 0,
|
||||||
|
addedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
isPlaying: isCurrent,
|
||||||
|
onTap: () => player.jump(index),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
data: (track) => ClipRRect(
|
||||||
|
key: Key('queue_item_$index'),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: ReorderableDelayedDragStartListener(
|
||||||
|
index: index,
|
||||||
|
child: Dismissible(
|
||||||
|
key: Key('dismissible_$index'),
|
||||||
|
direction: DismissDirection.endToStart,
|
||||||
|
background: Container(
|
||||||
|
color: Colors.red,
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
padding: const EdgeInsets.only(right: 20),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDismissed: (direction) => player.remove(index),
|
||||||
|
child: TrackTile(
|
||||||
|
leading: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
child: Text(
|
||||||
|
(index + 1).toString().padLeft(2, '0'),
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
track:
|
||||||
|
track ??
|
||||||
|
db.Track(
|
||||||
|
id: -1,
|
||||||
|
path: trackPath,
|
||||||
|
title: Uri.parse(
|
||||||
|
media.uri,
|
||||||
|
).pathSegments.last,
|
||||||
|
artist:
|
||||||
|
media.extras?['artist'] as String? ??
|
||||||
|
'Unknown Artist',
|
||||||
|
album: media.extras?['album'] as String?,
|
||||||
|
duration: null,
|
||||||
|
artUri: null,
|
||||||
|
lyrics: null,
|
||||||
|
lyricsOffset: 0,
|
||||||
|
addedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
isPlaying: isCurrent,
|
||||||
|
onTap: () => player.jump(index),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user