From e8d070f449e1e2f3cd11017ac854afcb68465ef9 Mon Sep 17 00:00:00 2001 From: Khaled Date: Sat, 28 Dec 2024 17:04:04 +0600 Subject: [PATCH] feat: Allow sorting the player queue closes #27 --- .../preferences/components/choice_dialog.dart | 24 ++++++++--- .../components/player_state_indicator.dart | 2 - lib/app/player/player_queue_sheet.dart | 24 +++++++++++ lib/model/common.dart | 3 ++ lib/provider/player_provider.dart | 43 ++++++++++++++++--- 5 files changed, 82 insertions(+), 14 deletions(-) diff --git a/lib/app/more/preferences/components/choice_dialog.dart b/lib/app/more/preferences/components/choice_dialog.dart index 2713295..3b8c2a5 100644 --- a/lib/app/more/preferences/components/choice_dialog.dart +++ b/lib/app/more/preferences/components/choice_dialog.dart @@ -35,13 +35,23 @@ class ChoiceDialog extends StatelessWidget { child: ListView.builder( shrinkWrap: true, itemCount: keys.length, - itemBuilder: (context, i) => RadioListTile( - dense: true, - value: options[keys[i]], - groupValue: selected, - onChanged: (v) => Navigator.pop(context, v), - title: Text(keys[i]), - ), + itemBuilder: (context, i) { + final value = options[keys[i]]; + if (selected == null) { + return ListTile( + dense: true, + onTap: () => Navigator.pop(context, value), + title: Text(keys[i]), + ); + } + return RadioListTile( + dense: true, + value: value, + groupValue: selected, + onChanged: (v) => Navigator.pop(context, v), + title: Text(keys[i]), + ); + }, ), ), actions: [ diff --git a/lib/app/player/components/player_state_indicator.dart b/lib/app/player/components/player_state_indicator.dart index 32046eb..1795225 100644 --- a/lib/app/player/components/player_state_indicator.dart +++ b/lib/app/player/components/player_state_indicator.dart @@ -4,8 +4,6 @@ import 'package:syncara/app/player/player_menu_sheet.dart'; import 'package:syncara/extensions.dart'; import 'package:syncara/provider/player_provider.dart'; -// https://github.com/material-components/material-components-android/blob/master/docs/components/Button.md#connected-button-group -// TODO There's no flutter component. Push this upstream someday class PlayerStateIndicator extends StatelessWidget { const PlayerStateIndicator({super.key}) : _static = false; diff --git a/lib/app/player/player_queue_sheet.dart b/lib/app/player/player_queue_sheet.dart index b7e60a5..f166b33 100644 --- a/lib/app/player/player_queue_sheet.dart +++ b/lib/app/player/player_queue_sheet.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:just_audio/just_audio.dart'; import 'package:provider/provider.dart'; +import 'package:syncara/app/more/preferences/components/choice_dialog.dart'; import 'package:syncara/app/player/components/queue_playlist_filter.dart'; import 'package:syncara/app/playlist/media_entry_builder.dart'; +import 'package:syncara/model/common.dart'; import 'package:syncara/model/media.dart'; import 'package:syncara/provider/player_provider.dart'; @@ -95,6 +97,28 @@ class PlayerQueueSheet extends StatelessWidget { ), ), ), + IconButton( + onPressed: () => sortQueueSelector(context), + icon: const Icon(Icons.sort_rounded), + ), ]; } + + Future sortQueueSelector(BuildContext context) async { + final result = await showDialog( + context: context, + builder: (_) => ChoiceDialog( + title: "Sort Current Queue", + icon: const Icon(Icons.speed_rounded), + options: { + for (final option in SortOption.values) ...{option.name: option} + }, + ), + ); + + if (!context.mounted || result == null) return false; + + context.read().sortQueue(result); + return true; + } } diff --git a/lib/model/common.dart b/lib/model/common.dart index 99bb495..6984c08 100644 --- a/lib/model/common.dart +++ b/lib/model/common.dart @@ -76,3 +76,6 @@ class LastPlayedMedia { @override int get hashCode => playlistId.hashCode ^ mediaId.hashCode; } + +//ignore: constant_identifier_names +enum SortOption { Ascending, Descending, Reverse, Author, Reset } diff --git a/lib/provider/player_provider.dart b/lib/provider/player_provider.dart index c5a1779..3bae8c3 100644 --- a/lib/provider/player_provider.dart +++ b/lib/provider/player_provider.dart @@ -20,6 +20,7 @@ class PlayerProvider extends ChangeNotifier { final List _playlistInfo = List.empty(growable: true); final List _playlist = List.empty(growable: true); + final List _originalPlaylistOrderIds = List.empty(growable: true); List get playlistInfo => List.of(_playlistInfo); @@ -60,6 +61,7 @@ class PlayerProvider extends ChangeNotifier { }) { _playlistInfo.add(provider.playlist); _playlist.addAll(provider.medias); + _originalPlaylistOrderIds.addAll(_playlist.map((e) => e.id)); prepare?.call(this); nowPlaying = ValueNotifier(start ?? _playlist.first); nowPlaying.addListener(beginPlay); @@ -107,16 +109,18 @@ class PlayerProvider extends ChangeNotifier { if (_playlistInfo.contains(provider.playlist)) return; _playlistInfo.add(provider.playlist); - _playlist.addAll(provider.medias.where( + final uniqueMedias = provider.medias.where( (media) => !_playlist.contains(media), - )); + ); + _playlist.addAll(uniqueMedias); + _originalPlaylistOrderIds.addAll(uniqueMedias.map((e) => e.id)); notifyListeners(); } Future beginPlay() async { final media = nowPlaying.value; try { - // HACK: Quickly toggle _disposed flag so stop event doesn't get emitted by notificationState + // HACK: Quickly toggle _disposed flag so stop event doesn't get emitted by notificationState causing spam _buffering = true; _disposed = true; await player.stop(); @@ -236,12 +240,12 @@ class PlayerProvider extends ChangeNotifier { void jumpTo(int index) => nowPlaying.value = _playlist[index]; - void reorderList(int oldIndex, int newIndex) { + void reorderList(int oldIndex, int newIndex, {bool notify = true}) { if (oldIndex < newIndex) newIndex -= 1; final item = _playlist.removeAt(oldIndex); _playlist.insert(newIndex, item); - notifyListeners(); + if (notify) notifyListeners(); } /// preserveCurrentIndex: Put currently playing song at first @@ -264,6 +268,35 @@ class PlayerProvider extends ChangeNotifier { player.setSpeed(speed); } + void sortQueue(SortOption option) { + switch (option) { + case SortOption.Ascending: + _playlist.sort((a, b) => a.title.compareTo(b.title)); + break; + case SortOption.Descending: + _playlist.sort((a, b) => b.title.compareTo(a.title)); + break; + case SortOption.Reverse: + final reversed = _playlist.reversed.toList(); + _playlist.clear(); + _playlist.addAll(reversed); + break; + + case SortOption.Author: + _playlist.sort((a, b) => a.author.compareTo(b.author)); + break; + + case SortOption.Reset: + for (final (index, id) in _originalPlaylistOrderIds.indexed) { + final old = _playlist.indexWhere((element) => element.id == id); + reorderList(old, index, notify: false); + } + break; + } + + notifyListeners(); + } + /// Passing null duration & afterSong will cancel the timer void setSleepTimer({Duration? duration, bool? afterSong}) { if (duration == null && afterSong == null) {