✨ Player queue
This commit is contained in:
parent
5a53fc7268
commit
785da526d3
@ -311,6 +311,15 @@ class AudioPlayerProvider extends GetxController {
|
|||||||
newIndex > state.value.tracks.length - 1 ||
|
newIndex > state.value.tracks.length - 1 ||
|
||||||
oldIndex > state.value.tracks.length - 1) return;
|
oldIndex > state.value.tracks.length - 1) return;
|
||||||
|
|
||||||
|
final item = state.value.playlist.medias.removeAt(oldIndex);
|
||||||
|
|
||||||
|
state.value = state.value.copyWith(
|
||||||
|
playlist: state.value.playlist.copyWith(
|
||||||
|
medias: state.value.playlist.medias
|
||||||
|
..insert(oldIndex < newIndex ? newIndex - 1 : 0, item),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
await audioPlayer.moveTrack(oldIndex, newIndex);
|
await audioPlayer.moveTrack(oldIndex, newIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
26
lib/screens/player/queue.dart
Normal file
26
lib/screens/player/queue.dart
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:rhythm_box/widgets/player/music_queue.dart';
|
||||||
|
|
||||||
|
class PlayerQueuePopup extends StatelessWidget {
|
||||||
|
const PlayerQueuePopup({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.85,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Queue',
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||||
|
const Expanded(
|
||||||
|
child: PlayerQueue(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:rhythm_box/providers/audio_player.dart';
|
import 'package:rhythm_box/providers/audio_player.dart';
|
||||||
|
import 'package:rhythm_box/screens/player/queue.dart';
|
||||||
import 'package:rhythm_box/services/artist.dart';
|
import 'package:rhythm_box/services/artist.dart';
|
||||||
import 'package:rhythm_box/services/audio_player/audio_player.dart';
|
import 'package:rhythm_box/services/audio_player/audio_player.dart';
|
||||||
import 'package:rhythm_box/widgets/auto_cache_image.dart';
|
import 'package:rhythm_box/widgets/auto_cache_image.dart';
|
||||||
@ -39,20 +41,13 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||||||
|
|
||||||
bool get _isPlaying => _playback.isPlaying.value;
|
bool get _isPlaying => _playback.isPlaying.value;
|
||||||
bool get _isFetchingActiveTrack => _query.isQueryingTrackInfo.value;
|
bool get _isFetchingActiveTrack => _query.isQueryingTrackInfo.value;
|
||||||
|
PlaylistMode get _loopMode => _playback.state.value.loopMode;
|
||||||
|
|
||||||
double _bufferProgress = 0;
|
double _bufferProgress = 0;
|
||||||
|
|
||||||
Duration _durationCurrent = Duration.zero;
|
Duration _durationCurrent = Duration.zero;
|
||||||
Duration _durationTotal = Duration.zero;
|
Duration _durationTotal = Duration.zero;
|
||||||
|
|
||||||
void _updateDurationCurrent(Duration dur) {
|
|
||||||
setState(() => _durationCurrent = dur);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateDurationTotal(Duration dur) {
|
|
||||||
setState(() => _durationTotal = dur);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<StreamSubscription>? _subscriptions;
|
List<StreamSubscription>? _subscriptions;
|
||||||
|
|
||||||
Future<void> _togglePlayState() async {
|
Future<void> _togglePlayState() async {
|
||||||
@ -199,6 +194,26 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
StreamBuilder<bool>(
|
||||||
|
stream: audioPlayer.shuffledStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final shuffled = snapshot.data ?? false;
|
||||||
|
return IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
shuffled ? Icons.shuffle_on_outlined : Icons.shuffle,
|
||||||
|
),
|
||||||
|
onPressed: _isFetchingActiveTrack
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
if (shuffled) {
|
||||||
|
audioPlayer.setShuffle(false);
|
||||||
|
} else {
|
||||||
|
audioPlayer.setShuffle(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.skip_previous),
|
icon: const Icon(Icons.skip_previous),
|
||||||
onPressed: _isFetchingActiveTrack
|
onPressed: _isFetchingActiveTrack
|
||||||
@ -233,8 +248,65 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||||||
onPressed:
|
onPressed:
|
||||||
_isFetchingActiveTrack ? null : audioPlayer.skipToNext,
|
_isFetchingActiveTrack ? null : audioPlayer.skipToNext,
|
||||||
),
|
),
|
||||||
|
Obx(
|
||||||
|
() => IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_loopMode == PlaylistMode.none
|
||||||
|
? Icons.repeat
|
||||||
|
: _loopMode == PlaylistMode.loop
|
||||||
|
? Icons.repeat_on_outlined
|
||||||
|
: Icons.repeat_one_on_outlined,
|
||||||
|
),
|
||||||
|
onPressed: _isFetchingActiveTrack
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
await audioPlayer.setLoopMode(
|
||||||
|
switch (_loopMode) {
|
||||||
|
PlaylistMode.loop => PlaylistMode.single,
|
||||||
|
PlaylistMode.single => PlaylistMode.none,
|
||||||
|
PlaylistMode.none => PlaylistMode.loop,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
|
const Gap(20),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextButton.icon(
|
||||||
|
icon: const Icon(Icons.queue_music),
|
||||||
|
label: const Text('Queue'),
|
||||||
|
onPressed: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
useRootNavigator: true,
|
||||||
|
isScrollControlled: true,
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const PlayerQueuePopup(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
Expanded(
|
||||||
|
child: TextButton.icon(
|
||||||
|
icon: const Icon(Icons.lyrics),
|
||||||
|
label: const Text('Lyrics'),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
Expanded(
|
||||||
|
child: TextButton.icon(
|
||||||
|
icon: const Icon(Icons.merge),
|
||||||
|
label: const Text('Sources'),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
).marginAll(24),
|
).marginAll(24),
|
||||||
|
125
lib/widgets/player/music_queue.dart
Normal file
125
lib/widgets/player/music_queue.dart
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:rhythm_box/providers/audio_player.dart';
|
||||||
|
import 'package:rhythm_box/widgets/auto_cache_image.dart';
|
||||||
|
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:rhythm_box/services/artist.dart';
|
||||||
|
|
||||||
|
class PlayerQueue extends StatefulWidget {
|
||||||
|
const PlayerQueue({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PlayerQueue> createState() => _PlayerQueueState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlayerQueueState extends State<PlayerQueue> {
|
||||||
|
final AutoScrollController _autoScrollController = AutoScrollController();
|
||||||
|
|
||||||
|
final AudioPlayerProvider _playback = Get.find();
|
||||||
|
|
||||||
|
List<Track> get _tracks => _playback.state.value.tracks;
|
||||||
|
|
||||||
|
bool _getIsActiveTrack(Track track) {
|
||||||
|
return track.id == _playback.state.value.activeTrack!.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (_playback.state.value.activeTrack != null) {
|
||||||
|
final idx = _tracks
|
||||||
|
.indexWhere((x) => x.id == _playback.state.value.activeTrack!.id);
|
||||||
|
if (idx != -1) {
|
||||||
|
_autoScrollController.scrollToIndex(
|
||||||
|
idx,
|
||||||
|
preferPosition: AutoScrollPosition.middle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_autoScrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Obx(
|
||||||
|
() => CustomScrollView(
|
||||||
|
controller: _autoScrollController,
|
||||||
|
slivers: [
|
||||||
|
SliverReorderableList(
|
||||||
|
itemCount: _tracks.length,
|
||||||
|
onReorder: (prev, now) async {
|
||||||
|
_playback.moveTrack(prev, now);
|
||||||
|
},
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final item = _tracks[idx];
|
||||||
|
return AutoScrollTag(
|
||||||
|
key: ValueKey<int>(idx),
|
||||||
|
controller: _autoScrollController,
|
||||||
|
index: idx,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
ReorderableDragStartListener(
|
||||||
|
index: idx,
|
||||||
|
child: const Icon(Icons.drag_indicator).paddingOnly(
|
||||||
|
left: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListTile(
|
||||||
|
tileColor: _getIsActiveTrack(item)
|
||||||
|
? Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.secondaryContainer
|
||||||
|
: null,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(8),
|
||||||
|
bottomLeft: Radius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 8,
|
||||||
|
right: 24,
|
||||||
|
),
|
||||||
|
leading: ClipRRect(
|
||||||
|
borderRadius:
|
||||||
|
const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: AutoCacheImage(
|
||||||
|
item.album!.images!.first.url!,
|
||||||
|
width: 64.0,
|
||||||
|
height: 64.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(item.name ?? 'Loading...'),
|
||||||
|
subtitle: Text(
|
||||||
|
item.artists?.asString() ?? 'Please stand by...',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_playback.jumpToTrack(item);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1030,6 +1030,14 @@ packages:
|
|||||||
url: "https://github.com/KRTirtho/scrobblenaut.git"
|
url: "https://github.com/KRTirtho/scrobblenaut.git"
|
||||||
source: git
|
source: git
|
||||||
version: "3.0.0"
|
version: "3.0.0"
|
||||||
|
scroll_to_index:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: scroll_to_index
|
||||||
|
sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.1"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -82,6 +82,7 @@ dependencies:
|
|||||||
ref: dart-3-support
|
ref: dart-3-support
|
||||||
dismissible_page: ^1.0.2
|
dismissible_page: ^1.0.2
|
||||||
shared_preferences: ^2.3.2
|
shared_preferences: ^2.3.2
|
||||||
|
scroll_to_index: ^3.0.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
Reference in New Issue
Block a user