✨ Player queue
This commit is contained in:
		@@ -311,6 +311,15 @@ class AudioPlayerProvider extends GetxController {
 | 
			
		||||
        newIndex > state.value.tracks.length - 1 ||
 | 
			
		||||
        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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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:get/get.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/screens/player/queue.dart';
 | 
			
		||||
import 'package:rhythm_box/services/artist.dart';
 | 
			
		||||
import 'package:rhythm_box/services/audio_player/audio_player.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 _isFetchingActiveTrack => _query.isQueryingTrackInfo.value;
 | 
			
		||||
  PlaylistMode get _loopMode => _playback.state.value.loopMode;
 | 
			
		||||
 | 
			
		||||
  double _bufferProgress = 0;
 | 
			
		||||
 | 
			
		||||
  Duration _durationCurrent = Duration.zero;
 | 
			
		||||
  Duration _durationTotal = Duration.zero;
 | 
			
		||||
 | 
			
		||||
  void _updateDurationCurrent(Duration dur) {
 | 
			
		||||
    setState(() => _durationCurrent = dur);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _updateDurationTotal(Duration dur) {
 | 
			
		||||
    setState(() => _durationTotal = dur);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  List<StreamSubscription>? _subscriptions;
 | 
			
		||||
 | 
			
		||||
  Future<void> _togglePlayState() async {
 | 
			
		||||
@@ -199,6 +194,26 @@ class _PlayerScreenState extends State<PlayerScreen> {
 | 
			
		||||
              Row(
 | 
			
		||||
                mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                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(
 | 
			
		||||
                    icon: const Icon(Icons.skip_previous),
 | 
			
		||||
                    onPressed: _isFetchingActiveTrack
 | 
			
		||||
@@ -233,8 +248,65 @@ class _PlayerScreenState extends State<PlayerScreen> {
 | 
			
		||||
                    onPressed:
 | 
			
		||||
                        _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),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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"
 | 
			
		||||
    source: git
 | 
			
		||||
    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:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
 
 | 
			
		||||
@@ -82,6 +82,7 @@ dependencies:
 | 
			
		||||
      ref: dart-3-support
 | 
			
		||||
  dismissible_page: ^1.0.2
 | 
			
		||||
  shared_preferences: ^2.3.2
 | 
			
		||||
  scroll_to_index: ^3.0.1
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
  flutter_test:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user