Player queue

This commit is contained in:
LittleSheep 2024-08-28 01:23:37 +08:00
parent 5a53fc7268
commit 785da526d3
6 changed files with 250 additions and 9 deletions

View File

@ -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);
} }

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

View File

@ -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),

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

View File

@ -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:

View File

@ -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: