From 9a56beeca0db118f294f4921923601b9cf31b216 Mon Sep 17 00:00:00 2001 From: anandnet Date: Sun, 6 Oct 2024 22:07:02 +0530 Subject: [PATCH] Feature: Queue loop #234 --- lib/services/audio_handler.dart | 8 +- lib/ui/home.dart | 63 +++++++--- lib/ui/player/components/mini_player.dart | 10 +- lib/ui/player/components/player_control.dart | 3 +- lib/ui/player/player.dart | 118 ++++++++++++++----- lib/ui/player/player_controller.dart | 44 ++++++- lib/ui/widgets/up_next_queue.dart | 3 +- localization/en.json | 4 + 8 files changed, 203 insertions(+), 50 deletions(-) diff --git a/lib/services/audio_handler.dart b/lib/services/audio_handler.dart index 5f6952b2..ac7ed5c2 100644 --- a/lib/services/audio_handler.dart +++ b/lib/services/audio_handler.dart @@ -45,6 +45,7 @@ class MyAudioHandler extends BaseAudioHandler with GetxServiceMixin { late String? currentSongUrl; bool isPlayingUsingLockCachingSource = false; bool loopModeEnabled = false; + bool queueLoopModeEnabled = false; bool shuffleModeEnabled = false; bool loudnessNormalizationEnabled = false; // var networkErrorPause = false; @@ -80,6 +81,7 @@ class MyAudioHandler extends BaseAudioHandler with GetxServiceMixin { _player.setSkipSilenceEnabled(appPrefsBox.get("skipSilenceEnabled")); loopModeEnabled = appPrefsBox.get("isLoopModeEnabled") ?? false; shuffleModeEnabled = appPrefsBox.get("isShuffleModeEnabled") ?? false; + queueLoopModeEnabled = Hive.box("AppPrefs").get("queueLoopModeEnabled") ?? false; loudnessNormalizationEnabled = appPrefsBox.get("loudnessNormalizationEnabled") ?? false; _listenForDurationChanges(); @@ -358,6 +360,8 @@ class MyAudioHandler extends BaseAudioHandler with GetxServiceMixin { if (queue.value.length > currentIndex + 1) { return currentIndex + 1; + } else if (queueLoopModeEnabled) { + return 0; } else { return currentIndex; } @@ -597,7 +601,7 @@ class MyAudioHandler extends BaseAudioHandler with GetxServiceMixin { final currentQueue = queue.value; currentQueue.insert(currentIndex + 1, song); queue.add(currentQueue); - if(shuffleModeEnabled){ + if (shuffleModeEnabled) { shuffledQueue.insert(currentShuffleIndex + 1, song.id); } } else if (name == 'openEqualizer') { @@ -614,6 +618,8 @@ class MyAudioHandler extends BaseAudioHandler with GetxServiceMixin { final songIndex = extras!['index']; currentIndex = songIndex; mediaItem.add(queue.value[currentIndex]); + } else if (name == "toggleQueueLoopMode") { + queueLoopModeEnabled = extras!['enable']; } } diff --git a/lib/ui/home.dart b/lib/ui/home.dart index 08739695..5009c481 100644 --- a/lib/ui/home.dart +++ b/lib/ui/home.dart @@ -111,21 +111,54 @@ class Home extends StatelessWidget { .textTheme .titleLarge, ), - IconButton( - onPressed: () { - if (playerController - .isShuffleModeEnabled.isTrue) { - ScaffoldMessenger.of(context) - .showSnackBar(snackbar( - context, - "queueShufflingDeniedMsg" - .tr, - size: SanckBarSize.BIG)); - return; - } - playerController.shuffleQueue(); - }, - icon: const Icon(Icons.shuffle)) + Row( + children: [ + InkWell( + onTap: () { + playerController + .toggleQueueLoopMode(); + }, + child: Obx( + () => Container( + height: 30, + padding: + const EdgeInsets.symmetric( + horizontal: 20), + decoration: BoxDecoration( + color: playerController + .isQueueLoopModeEnabled + .isFalse + ? Colors.white24 + : Colors.white + .withOpacity(0.8), + borderRadius: + BorderRadius.circular(20), + ), + child: Center( + child: + Text("queueLoop".tr)), + ), + ), + ), + IconButton( + onPressed: () { + if (playerController + .isShuffleModeEnabled + .isTrue) { + ScaffoldMessenger.of(context) + .showSnackBar(snackbar( + context, + "queueShufflingDeniedMsg" + .tr, + size: SanckBarSize + .BIG)); + return; + } + playerController.shuffleQueue(); + }, + icon: const Icon(Icons.shuffle)), + ], + ) ], ), )), diff --git a/lib/ui/player/components/mini_player.dart b/lib/ui/player/components/mini_player.dart index c35e675c..89d6bcf1 100644 --- a/lib/ui/player/components/mini_player.dart +++ b/lib/ui/player/components/mini_player.dart @@ -5,6 +5,7 @@ import 'package:hive/hive.dart'; import 'package:ionicons/ionicons.dart'; import 'package:widget_marquee/widget_marquee.dart'; +import '/utils/helper.dart'; import '/ui/widgets/lyrics_dialog.dart'; import '/ui/widgets/song_info_dialog.dart'; import '/ui/player/player_controller.dart'; @@ -246,9 +247,12 @@ class MiniPlayer extends StatelessWidget { child: Obx(() { final isLastSong = playerController.currentQueue.isEmpty || - (playerController - .isShuffleModeEnabled - .isFalse && + (!(playerController + .isShuffleModeEnabled + .isTrue || + playerController + .isQueueLoopModeEnabled + .isTrue) && (playerController .currentQueue.last.id == playerController.currentSong diff --git a/lib/ui/player/components/player_control.dart b/lib/ui/player/components/player_control.dart index 036e50db..3a4bd682 100644 --- a/lib/ui/player/components/player_control.dart +++ b/lib/ui/player/components/player_control.dart @@ -177,7 +177,8 @@ class PlayerControlWidget extends StatelessWidget { Widget _nextButton(PlayerController playerController, BuildContext context) { return Obx(() { final isLastSong = playerController.currentQueue.isEmpty || - (playerController.isShuffleModeEnabled.isFalse && + (!(playerController.isShuffleModeEnabled.isTrue || + playerController.isQueueLoopModeEnabled.isTrue) && (playerController.currentQueue.last.id == playerController.currentSong.value?.id)); return IconButton( diff --git a/lib/ui/player/player.dart b/lib/ui/player/player.dart index 6761683c..837cd300 100644 --- a/lib/ui/player/player.dart +++ b/lib/ui/player/player.dart @@ -1,9 +1,11 @@ +import 'dart:ui'; + import 'package:get/get.dart'; import 'package:flutter/material.dart'; -import 'package:harmonymusic/ui/player/components/gesture_player.dart'; -import 'package:harmonymusic/ui/player/components/standard_player.dart'; -import 'package:harmonymusic/ui/screens/Settings/settings_screen_controller.dart'; +import '/ui/player/components/gesture_player.dart'; +import '/ui/player/components/standard_player.dart'; +import '/ui/screens/Settings/settings_screen_controller.dart'; import '../../utils/helper.dart'; import '../widgets/snackbar.dart'; import '../widgets/up_next_queue.dart'; @@ -65,31 +67,91 @@ class Player extends StatelessWidget { onReorderEnd: onReorderEnd, onReorderStart: onReorderStart, ), - Positioned( - bottom: 60, - right: 15, - child: SizedBox( - height: 60, - width: 60, - child: FittedBox( - child: FloatingActionButton( - focusElevation: 0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(14))), - elevation: 0, - onPressed: () { - if (playerController - .isShuffleModeEnabled.isTrue) { - ScaffoldMessenger.of(context) - .showSnackBar(snackbar(context, - "queueShufflingDeniedMsg".tr, - size: SanckBarSize.BIG)); - return; - } - playerController.shuffleQueue(); - }, - child: const Icon(Icons.shuffle))))), + Align( + alignment: Alignment.bottomCenter, + child: ClipRRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + padding: const EdgeInsets.only( + bottom: 10, left: 10, right: 10), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + blurRadius: 5, color: Colors.black54) + ], + color: Theme.of(context) + .primaryColor + .withOpacity(0.5)), + height: 60 + Get.mediaQuery.padding.bottom, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + //queue loop button and queue shuffle button + Obx( + () => Text( + "${playerController.currentQueue.length} ${"songs".tr}", + style: Theme.of(context) + .textTheme + .titleSmall! + .copyWith( + color: Theme.of(context) + .textTheme + .titleMedium! + .color), + ), + ), + InkWell( + onTap: () { + if (playerController + .isShuffleModeEnabled.isTrue) { + ScaffoldMessenger.of(context) + .showSnackBar(snackbar(context, + "queueShufflingDeniedMsg".tr, + size: SanckBarSize.BIG)); + return; + } + playerController.shuffleQueue(); + }, + child: Container( + height: 30, + padding: const EdgeInsets.symmetric( + horizontal: 20), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.8), + borderRadius: BorderRadius.circular(20), + ), + child: + Center(child: Text("shuffleQueue".tr)), + ), + ), + InkWell( + onTap: () { + playerController.toggleQueueLoopMode(); + }, + child: Obx( + () => Container( + height: 30, + padding: const EdgeInsets.symmetric( + horizontal: 20), + decoration: BoxDecoration( + color: playerController + .isQueueLoopModeEnabled.isFalse + ? Colors.white24 + : Colors.white.withOpacity(0.8), + borderRadius: BorderRadius.circular(20), + ), + child: + Center(child: Text("queueLoop".tr)), + ), + ), + ), + ], + ), + ), + ), + ), + ), ], ); }, diff --git a/lib/ui/player/player_controller.dart b/lib/ui/player/player_controller.dart index f19d6f46..c16c03a6 100644 --- a/lib/ui/player/player_controller.dart +++ b/lib/ui/player/player_controller.dart @@ -51,6 +51,7 @@ class PlayerController extends GetxController final currentSongIndex = (0).obs; final isFirstSong = true; final isLastSong = true; + final isQueueLoopModeEnabled = false.obs; final isLoopModeEnabled = false.obs; final isShuffleModeEnabled = false.obs; final currentSong = Rxn(); @@ -103,6 +104,9 @@ class PlayerController extends GetxController Hive.box("AppPrefs").get("isLoopModeEnabled") ?? false; isShuffleModeEnabled.value = Hive.box("appPrefs").get("isShuffleModeEnabled") ?? false; + isQueueLoopModeEnabled.value = + Hive.box("AppPrefs").get("queueLoopModeEnabled") ?? false; + if (GetPlatform.isDesktop) { setVolume(Hive.box("AppPrefs").get("volume") ?? 100); } @@ -319,6 +323,13 @@ class PlayerController extends GetxController _playerPanelCheck(); await _audioHandler .customAction("setSourceNPlay", {'mediaItem': mediaItem}); + + // disable queue loop mode when radio is started + if (radio && + isQueueLoopModeEnabled.isTrue && + isShuffleModeEnabled.isFalse) { + toggleQueueLoopMode(); + } } Future playPlayListSong(List mediaItems, int index) async { @@ -362,7 +373,7 @@ class PlayerController extends GetxController ///enqueueSong append a song to current queue ///if current queue is empty, push the song into Queue and play that song Future enqueueSong(MediaItem mediaItem) async { - if (currentQueue.isEmpty) { + if (currentQueue.isEmpty) { await playPlayListSong([mediaItem], 0); return; } @@ -452,6 +463,13 @@ class PlayerController extends GetxController : _audioHandler.setShuffleMode(AudioServiceShuffleMode.all); isShuffleModeEnabled.value = !shuffleModeEnabled; await Hive.box("AppPrefs").put("isShuffleModeEnabled", !shuffleModeEnabled); + // restrict queue loop mode when shuffle mode is enabled + if (isShuffleModeEnabled.isTrue && isQueueLoopModeEnabled.isFalse) { + isQueueLoopModeEnabled.value = true; + } else if (isShuffleModeEnabled.isFalse) { + isQueueLoopModeEnabled.value = + Hive.box("AppPrefs").get("queueLoopModeEnabled", defaultValue: false); + } } void onReorder(int oldIndex, int newIndex) { @@ -521,6 +539,30 @@ class PlayerController extends GetxController .put("isLoopModeEnabled", isLoopModeEnabled.value); } + Future toggleQueueLoopMode({bool showMessage = true}) async { + if (isShuffleModeEnabled.isTrue && isQueueLoopModeEnabled.isTrue) { + if (!showMessage) return; + ScaffoldMessenger.of(Get.context!).showSnackBar(snackbar( + Get.context!, "queueLoopNotDisMsg1".tr, + size: SanckBarSize.BIG, duration: const Duration(seconds: 2))); + return; + } + + if (isRadioModeOn && isQueueLoopModeEnabled.isFalse) { + if (!showMessage) return; + ScaffoldMessenger.of(Get.context!).showSnackBar(snackbar( + Get.context!, "queueLoopNotDisMsg2".tr, + size: SanckBarSize.BIG, duration: const Duration(seconds: 2))); + return; + } + + isQueueLoopModeEnabled.value = !isQueueLoopModeEnabled.value; + await _audioHandler.customAction( + "toggleQueueLoopMode", {"enable": isQueueLoopModeEnabled.value}); + await Hive.box("AppPrefs") + .put("queueLoopModeEnabled", isQueueLoopModeEnabled.value); + } + Future setVolume(int value) async { _audioHandler.customAction("setVolume", {"value": value}); volume.value = value; diff --git a/lib/ui/widgets/up_next_queue.dart b/lib/ui/widgets/up_next_queue.dart index 02f1d529..f2bc78e0 100644 --- a/lib/ui/widgets/up_next_queue.dart +++ b/lib/ui/widgets/up_next_queue.dart @@ -40,7 +40,8 @@ class UpNextQueue extends StatelessWidget { itemCount: playerController.currentQueue.length, padding: EdgeInsets.only( top: isQueueInSlidePanel ? 55 : 0, - bottom: Get.mediaQuery.padding.bottom), + bottom: + isQueueInSlidePanel ? 80 : 0 + Get.mediaQuery.padding.bottom), physics: const AlwaysScrollableScrollPhysics(), itemBuilder: (context, index) { final homeScaffoldContext = diff --git a/localization/en.json b/localization/en.json index 2ccbf32e..8698cb1b 100644 --- a/localization/en.json +++ b/localization/en.json @@ -49,6 +49,10 @@ "queuerearrangingDeniedMsg": "Queue can't be rearranged when shuffle mode is enabled", "songNotPlayable": "Song is not playable due to server restriction!", "upNext": "Up Next", + "shuffleQueue": "Shuffle Queue", + "queueLoop": "Queue loop", + "queueLoopNotDisMsg1": "Queue loop mode cannot be disabled when shuffle mode is enabled.", + "queueLoopNotDisMsg2": "Queue loop mode cannot be enabled in radio mode.", "removeFromLib": "Remove from Library Songs", "sleepTimer": "Sleep Timer", "add5Minutes": "Add 5 minutes",