From ef778639808aff5cd7433583d29f401e8f99c892 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 18 May 2023 02:29:58 +0200 Subject: [PATCH 001/130] replaced AudioServiceHelper queue with QueueService for unshuffled albums - Implemented new QueueService for handling most of the queueing logic - Connected QueueService to MusicPlayerBackgroundTask for accessing the just_audio and audio_service queues - Forwarded playback events from just_audio to QueueService to keep track of current item - added new PlaybackList class for storing queue items + list meta info - display PlaybackList info on player screen --- ...bum_screen_content_flexible_space_bar.dart | 15 +- lib/main.dart | 6 + lib/models/finamp_models.dart | 66 +++++ lib/screens/player_screen.dart | 18 +- lib/services/audio_service_helper.dart | 2 +- .../music_player_background_task.dart | 34 +++ lib/services/queue_service.dart | 273 ++++++++++++++++++ 7 files changed, 406 insertions(+), 8 deletions(-) create mode 100644 lib/services/queue_service.dart diff --git a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart index e5e4cf012..9d8bf2d97 100644 --- a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart +++ b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart @@ -1,5 +1,7 @@ import 'dart:math'; +import 'package:finamp/models/finamp_models.dart'; +import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; @@ -23,6 +25,8 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { Widget build(BuildContext context) { AudioServiceHelper audioServiceHelper = GetIt.instance(); + QueueService queueService = + GetIt.instance(); return FlexibleSpaceBar( background: SafeArea( @@ -57,9 +61,14 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { Expanded( child: ElevatedButton.icon( onPressed: () => - audioServiceHelper.replaceQueueWithItem( - itemList: items, - ), + // audioServiceHelper.replaceQueueWithItem( + queueService.startPlayback( + PlaybackList.create( + items: items, + type: PlaybackListType.album, + name: album.name ?? "Somewhere" + ) + ), icon: const Icon(Icons.play_arrow), label: Text(AppLocalizations.of(context)!.playButtonLabel), diff --git a/lib/main.dart b/lib/main.dart index 7e027fa1f..13b3c9ce6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:audio_service/audio_service.dart'; import 'package:audio_session/audio_session.dart'; import 'package:finamp/services/finamp_settings_helper.dart'; import 'package:finamp/services/finamp_user_helper.dart'; +import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; @@ -63,6 +64,7 @@ void main() async { await _setupDownloader(); await _setupDownloadsHelper(); await _setupAudioServiceHelper(); + await _setupQueueService(); } catch (e) { hasFailed = true; runApp(FinampErrorApp( @@ -91,6 +93,10 @@ void _setupJellyfinApiData() { GetIt.instance.registerSingleton(JellyfinApiHelper()); } +Future _setupQueueService() async { + GetIt.instance.registerSingleton(QueueService()); +} + Future _setupDownloadsHelper() async { GetIt.instance.registerSingleton(DownloadsHelper()); } diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 178f40624..cc1f85586 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -554,3 +554,69 @@ class DownloadedImage { downloadLocationId: downloadLocationId, ); } + +enum PlaybackListType { + + album(name: "Album"), + playlist(name: "Playlist"), + mix(name: "Instant Mix"), + favorites(name: "Your Likes"), + list(name: "All Songs"), + filteredList(name: "Songs"), + genre(name: "Genre"), + artist(name: "Artist"), + downloads(name: ""), + unknown(name: ""); + + const PlaybackListType({ + required this.name, + }); + + final String name; +} + +class PlaybackListInfo { + PlaybackListInfo({ + required this.type, + required this.name, + this.duration = 0, + }); + + @HiveField(0) + PlaybackListType type; + + @HiveField(1) + String name; + + @HiveField(2) + int duration; +} + +class PlaybackList { + PlaybackList({ + required this.items, + required this.info + }); + + @HiveField(0) + List items; + + @HiveField(1) + PlaybackListInfo info; + + static PlaybackList create({ + required List items, + required PlaybackListType type, + required String name, + int? duration, + }) => + PlaybackList( + items: items, + info: PlaybackListInfo( + type: type, + name: name, + duration: duration ?? 0, + ) + ); + +} diff --git a/lib/screens/player_screen.dart b/lib/screens/player_screen.dart index 2b8987c8b..33ae0552e 100644 --- a/lib/screens/player_screen.dart +++ b/lib/screens/player_screen.dart @@ -10,6 +10,10 @@ import '../components/PlayerScreen/song_info.dart'; import '../components/finamp_app_bar_button.dart'; import '../services/current_album_image_provider.dart'; import '../services/finamp_settings_helper.dart'; +import 'package:finamp/services/queue_service.dart'; +import 'package:get_it/get_it.dart'; +import '../models/finamp_models.dart'; + const _toolbarHeight = 75.0; @@ -20,6 +24,12 @@ class PlayerScreen extends StatelessWidget { @override Widget build(BuildContext context) { + + QueueService queueService = + GetIt.instance(); + + PlaybackListInfo playbackListInfo = queueService.getPlaybackListInfo(); + return SimpleGestureDetector( onVerticalSwipe: (direction) { if (!FinampSettingsHelper.finampSettings.disableGesture && @@ -78,7 +88,7 @@ class PlayerScreen extends StatelessWidget { child: Column( children: [ Text( - "Playing From", + "Playing From ${playbackListInfo.type.name}", style: TextStyle( fontSize: 12, fontWeight: FontWeight.w300, @@ -86,9 +96,9 @@ class PlayerScreen extends StatelessWidget { ), ), const Padding(padding: EdgeInsets.symmetric(vertical: 2)), - const Text( - "Somewhere", - style: TextStyle( + Text( + playbackListInfo.name, + style: const TextStyle( fontSize: 16, color: Colors.white, ), diff --git a/lib/services/audio_service_helper.dart b/lib/services/audio_service_helper.dart index 989ee97a3..2a59fa0ae 100644 --- a/lib/services/audio_service_helper.dart +++ b/lib/services/audio_service_helper.dart @@ -21,7 +21,7 @@ class AudioServiceHelper { /// Replaces the queue with the given list of items. If startAtIndex is specified, Any items below it /// will be ignored. This is used for when the user taps in the middle of an album to start from that point. Future replaceQueueWithItem({ - required List itemList, + required List itemList, //TODO create a custom type for item lists that can also hold the name of the list, etc. int initialIndex = 0, bool shuffle = false, }) async { diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index 8d6227dc3..225a40e55 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -36,6 +36,8 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { final _jellyfinApiHelper = GetIt.instance(); final _finampUserHelper = GetIt.instance(); + final _playbackEventStreamController = StreamController(); + /// Set when shuffle mode is changed. If true, [onUpdateQueue] will create a /// shuffled [ConcatenatingAudioSource]. bool shuffleNextQueue = false; @@ -69,6 +71,8 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { _player.playbackEventStream.listen((event) async { playbackState.add(_transformEvent(event)); + _playbackEventStreamController.add(event); + if (playbackState.valueOrNull != null && playbackState.valueOrNull?.processingState != AudioProcessingState.idle && @@ -90,6 +94,7 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { _player.currentIndexStream.listen((event) async { if (event == null) return; + _audioServiceBackgroundTaskLogger.info("index event received, new index: $event"); final currentItem = _getQueueItem(event); mediaItem.add(currentItem); @@ -126,6 +131,32 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { (_) => playbackState.add(_transformEvent(_player.playbackEvent))); _player.loopModeStream.listen( (_) => playbackState.add(_transformEvent(_player.playbackEvent))); + + } + + Stream getPlaybackEventStream() { + return _playbackEventStreamController.stream; + } + + Future initializeAudioSource(ConcatenatingAudioSource source) async { + + _queueAudioSource = source; + + try { + await _player.setAudioSource( + _queueAudioSource, + initialIndex: nextInitialIndex, + ); + } on PlayerException catch (e) { + _audioServiceBackgroundTaskLogger + .severe("Player error code ${e.code}: ${e.message}"); + } on PlayerInterruptedException catch (e) { + _audioServiceBackgroundTaskLogger + .warning("Player interrupted: ${e.message}"); + } catch (e) { + _audioServiceBackgroundTaskLogger + .severe("Player error ${e.toString()}"); + } } @override @@ -209,6 +240,9 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { @override Future updateQueue(List newQueue) async { + + _audioServiceBackgroundTaskLogger.severe("UPDATING QUEUE in music player background task, this shouldn't be happening!"); + try { // Convert the MediaItems to AudioSources List audioSources = []; diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart new file mode 100644 index 000000000..191c8d9f0 --- /dev/null +++ b/lib/services/queue_service.dart @@ -0,0 +1,273 @@ +import 'dart:io'; +import 'package:just_audio/just_audio.dart'; +import 'package:audio_service/audio_service.dart'; +import 'package:get_it/get_it.dart'; +import 'package:logging/logging.dart'; +import 'package:uuid/uuid.dart'; + +import 'finamp_user_helper.dart'; +import 'jellyfin_api_helper.dart'; +import 'finamp_settings_helper.dart'; +import 'downloads_helper.dart'; +import '../models/finamp_models.dart'; +import '../models/jellyfin_models.dart'; +import 'music_player_background_task.dart'; + +/// A track queueing service for Finamp. +class QueueService { + final _jellyfinApiHelper = GetIt.instance(); + final _downloadsHelper = GetIt.instance(); + final _audioHandler = GetIt.instance(); + final _finampUserHelper = GetIt.instance(); + final _queueServiceLogger = Logger("QueueService"); + + // internal state + + // the audio source used by the player. The first X items of all internal queues are merged together into this source, so that all player features, like gapless playback, are supported + ConcatenatingAudioSource _queueAudioSource = ConcatenatingAudioSource( + children: [], + useLazyPreparation: true, + ); + final int _audioSourceItemCount = 1 + 1 + 5; + + List _queue = []; // contains all regular queue items + int _currentQueueIndex = 0; + List _nextUpQueue = []; // a temporary queue that gets appended to if the user taps "next up" + List _queueHistory = []; // contains **all** items that have been played, including "next up" + PlaybackList _initialQueue = PlaybackList.create(items: [], type: PlaybackListType.unknown, name: "Somewhere"); // contains the original queue that was set when the queue was last fully replaced using `replaceQueueWithItems`. This is used to repeat the original queue once the end has been reached, **excluding** "next up" items. + + QueueService() { + + _audioHandler.getPlaybackEventStream().listen((event) async { + + _currentQueueIndex = event.currentIndex ?? 0; + + }); + + } + + Future startPlayback(PlaybackList list) async { + + _initialQueue = list; // save original PlaybackList for looping/restarting and meta info + _replaceWithItems(itemList: list.items); + _queueServiceLogger.info("Started playing PlaybackList '${list.info.name}' (${list.info.type})"); + + } + + /// Replaces the queue with the given list of items. If startAtIndex is specified, Any items below it + /// will be ignored. This is used for when the user taps in the middle of an album to start from that point. + Future _replaceWithItems({ + required List + itemList, //TODO create a custom type for item lists that can also hold the name of the list, etc. + int initialIndex = 0, + bool shuffle = false, + }) async { + try { + if (initialIndex > itemList.length) { + return Future.error( + "initialIndex is bigger than the itemList! ($initialIndex > ${itemList.length})"); + } + + _queue.clear(); // empty queue + + for (BaseItemDto item in itemList) { + try { + _queue.add(await _generateMediaItem(item)); + } catch (e) { + _queueServiceLogger.severe(e); + } + } + + // start playing first item in queue + _currentQueueIndex = 0; + _audioHandler.setNextInitialIndex(_currentQueueIndex); + + _queueAudioSource.clear(); + + for (final mediaItem in _queue) { + _queueAudioSource.add(await _mediaItemToAudioSource(mediaItem)); + } + + //TODO implement shuffle + + _audioHandler.initializeAudioSource(_queueAudioSource); + + _audioHandler.queue.add(_queue); + + _audioHandler.mediaItem.add(_queue[_currentQueueIndex]); + _audioHandler.play(); + + _audioHandler.nextInitialIndex = null; + + + } catch (e) { + _queueServiceLogger.severe(e); + return Future.error(e); + } + } + + Future addItemAtEnd(BaseItemDto item) async { + try { + // If the queue is empty (like when the app is first launched), run the + // replace queue function instead so that the song gets played + if ((_audioHandler.queue.valueOrNull?.length ?? 0) == 0) { + await _replaceWithItems(itemList: [item]); + return; + } + + final itemMediaItem = await _generateMediaItem(item); + await _audioHandler.addQueueItem(itemMediaItem); + } catch (e) { + _queueServiceLogger.severe(e); + return Future.error(e); + } + } + + Future addItemAtNext(BaseItemDto item) async { + try { + // If the queue is empty (like when the app is first launched), run the + // replace queue function instead so that the song gets played + if ((_queueAudioSource.length ?? 0) == 0) { + await _replaceWithItems(itemList: [item]); + return; + } + + final itemMediaItem = await _generateMediaItem(item); + await _audioHandler.addQueueItem(itemMediaItem); + + + + } catch (e) { + _queueServiceLogger.severe(e); + return Future.error(e); + } + } + + PlaybackListInfo getPlaybackListInfo() { + return _initialQueue.info; + } + + Future _generateMediaItem(BaseItemDto item) async { + const uuid = Uuid(); + + final downloadedSong = _downloadsHelper.getDownloadedSong(item.id); + final isDownloaded = downloadedSong == null + ? false + : await _downloadsHelper.verifyDownloadedSong(downloadedSong); + + return MediaItem( + id: uuid.v4(), + album: item.album ?? "Unknown Album", + artist: item.artists?.join(", ") ?? item.albumArtist, + artUri: _downloadsHelper.getDownloadedImage(item)?.file.uri ?? + _jellyfinApiHelper.getImageUrl(item: item), + title: item.name ?? "Unknown Name", + extras: { + // "parentId": item.parentId, + // "itemId": item.id, + "itemJson": item.toJson(), + "shouldTranscode": FinampSettingsHelper.finampSettings.shouldTranscode, + "downloadedSongJson": isDownloaded + ? (_downloadsHelper.getDownloadedSong(item.id))!.toJson() + : null, + "isOffline": FinampSettingsHelper.finampSettings.isOffline, + // TODO: Maybe add transcoding bitrate here? + }, + // Jellyfin returns microseconds * 10 for some reason + duration: Duration( + microseconds: + (item.runTimeTicks == null ? 0 : item.runTimeTicks! ~/ 10), + ), + ); + } + + /// Syncs the list of MediaItems (_queue) with the internal queue of the player. + /// Called by onAddQueueItem and onUpdateQueue. + Future _mediaItemToAudioSource(MediaItem mediaItem) async { + if (mediaItem.extras!["downloadedSongJson"] == null) { + // If DownloadedSong wasn't passed, we assume that the item is not + // downloaded. + + // If offline, we throw an error so that we don't accidentally stream from + // the internet. See the big comment in _songUri() to see why this was + // passed in extras. + if (mediaItem.extras!["isOffline"]) { + return Future.error( + "Offline mode enabled but downloaded song not found."); + } else { + if (mediaItem.extras!["shouldTranscode"] == true) { + return HlsAudioSource(await _songUri(mediaItem), tag: mediaItem); + } else { + return AudioSource.uri(await _songUri(mediaItem), tag: mediaItem); + } + } + } else { + // We have to deserialise this because Dart is stupid and can't handle + // sending classes through isolates. + final downloadedSong = + DownloadedSong.fromJson(mediaItem.extras!["downloadedSongJson"]); + + // Path verification and stuff is done in AudioServiceHelper, so this path + // should be valid. + final downloadUri = Uri.file(downloadedSong.file.path); + return AudioSource.uri(downloadUri, tag: mediaItem); + } + } + + Future _songUri(MediaItem mediaItem) async { + // We need the platform to be Android or iOS to get device info + assert(Platform.isAndroid || Platform.isIOS, + "_songUri() only supports Android and iOS"); + + // When creating the MediaItem (usually in AudioServiceHelper), we specify + // whether or not to transcode. We used to pull from FinampSettings here, + // but since audio_service runs in an isolate (or at least, it does until + // 0.18), the value would be wrong if changed while a song was playing since + // Hive is bad at multi-isolate stuff. + + final parsedBaseUrl = Uri.parse(_finampUserHelper.currentUser!.baseUrl); + + List builtPath = List.from(parsedBaseUrl.pathSegments); + + Map queryParameters = + Map.from(parsedBaseUrl.queryParameters); + + // We include the user token as a query parameter because just_audio used to + // have issues with headers in HLS, and this solution still works fine + queryParameters["ApiKey"] = _finampUserHelper.currentUser!.accessToken; + + if (mediaItem.extras!["shouldTranscode"]) { + builtPath.addAll([ + "Audio", + mediaItem.extras!["itemJson"]["Id"], + "main.m3u8", + ]); + + queryParameters.addAll({ + "audioCodec": "aac", + // Ideally we'd use 48kHz when the source is, realistically it doesn't + // matter too much + "audioSampleRate": "44100", + "maxAudioBitDepth": "16", + "audioBitRate": + FinampSettingsHelper.finampSettings.transcodeBitrate.toString(), + }); + } else { + builtPath.addAll([ + "Items", + mediaItem.extras!["itemJson"]["Id"], + "File", + ]); + } + + return Uri( + host: parsedBaseUrl.host, + port: parsedBaseUrl.port, + scheme: parsedBaseUrl.scheme, + userInfo: parsedBaseUrl.userInfo, + pathSegments: builtPath, + queryParameters: queryParameters, + ); + } + +} From 4be2568a9a0bfcfbd9e41660ad860376edd58073 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 18 May 2023 22:01:12 +0200 Subject: [PATCH 002/130] proper queue implementation - Refactored `QueueService` with the proper state variables and functions - Linear & shuffled playback now working (for starting a new list, toggling doesn't work yet) - Adding to queue works - Manually skipping back and forth works - Skips initiated by the player also update the internal queues of the QueueService --- ...bum_screen_content_flexible_space_bar.dart | 41 ++- .../AlbumScreen/song_list_tile.dart | 4 + .../PlayerScreen/player_buttons.dart | 8 +- .../player_screen_appbar_title.dart | 63 ++++ lib/components/now_playing_bar.dart | 6 +- lib/models/finamp_models.dart | 66 ++-- lib/screens/player_screen.dart | 63 +--- .../music_player_background_task.dart | 6 +- lib/services/queue_service.dart | 291 +++++++++++++++--- 9 files changed, 389 insertions(+), 159 deletions(-) create mode 100644 lib/components/PlayerScreen/player_screen_appbar_title.dart diff --git a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart index 9d8bf2d97..b61052c9a 100644 --- a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart +++ b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart @@ -28,6 +28,30 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { QueueService queueService = GetIt.instance(); + void _playAlbum() { + queueService.playbackOrder = PlaybackOrder.linear; + queueService.startPlayback( + items: items, + source: QueueItemSource( + type: QueueItemType.album, + name: album.name ?? "Somewhere", + id: album.id, + ) + ); + } + + void _shuffleAlbum() { + queueService.playbackOrder = PlaybackOrder.shuffled; + queueService.startPlayback( + items: items, + source: QueueItemSource( + type: QueueItemType.album, + name: album.name ?? "Somewhere", + id: album.id, + ) + ); + } + return FlexibleSpaceBar( background: SafeArea( child: Align( @@ -60,15 +84,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { child: Row(children: [ Expanded( child: ElevatedButton.icon( - onPressed: () => - // audioServiceHelper.replaceQueueWithItem( - queueService.startPlayback( - PlaybackList.create( - items: items, - type: PlaybackListType.album, - name: album.name ?? "Somewhere" - ) - ), + onPressed: () => _playAlbum(), icon: const Icon(Icons.play_arrow), label: Text(AppLocalizations.of(context)!.playButtonLabel), @@ -77,12 +93,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), Expanded( child: ElevatedButton.icon( - onPressed: () => - audioServiceHelper.replaceQueueWithItem( - itemList: items, - shuffle: true, - initialIndex: Random().nextInt(items.length), - ), + onPressed: () => _shuffleAlbum(), icon: const Icon(Icons.shuffle), label: Text( AppLocalizations.of(context)!.shuffleButtonLabel), diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index 610fdf7f6..f7cdffdc8 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -1,4 +1,6 @@ import 'package:audio_service/audio_service.dart'; +import 'package:finamp/models/finamp_models.dart'; +import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; @@ -68,6 +70,7 @@ class SongListTile extends StatefulWidget { class _SongListTileState extends State { final _audioServiceHelper = GetIt.instance(); + final _queueService = GetIt.instance(); final _audioHandler = GetIt.instance(); final _jellyfinApiHelper = GetIt.instance(); @@ -292,6 +295,7 @@ class _SongListTileState extends State { switch (selection) { case SongListTileMenuItems.addToQueue: await _audioServiceHelper.addQueueItem(widget.item); + await _queueService.addToQueue(widget.item, QueueItemSource(type: QueueItemType.unknown, name: "Queue", id: widget.parentId!)); if (!mounted) return; diff --git a/lib/components/PlayerScreen/player_buttons.dart b/lib/components/PlayerScreen/player_buttons.dart index 5fc9d8c11..b69cde1be 100644 --- a/lib/components/PlayerScreen/player_buttons.dart +++ b/lib/components/PlayerScreen/player_buttons.dart @@ -1,6 +1,7 @@ import 'package:audio_service/audio_service.dart'; import 'package:finamp/components/PlayerScreen/player_buttons_more.dart'; import 'package:finamp/components/PlayerScreen/player_buttons_repeating.dart'; +import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; @@ -18,6 +19,7 @@ class PlayerButtons extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final audioHandler = GetIt.instance(); + final queueService = GetIt.instance(); return IconTheme( data: IconThemeData( @@ -44,9 +46,7 @@ class PlayerButtons extends ConsumerWidget { PlayerButtonsRepeating(), IconButton( icon: const Icon(TablerIcons.player_skip_back), - onPressed: playbackState != null - ? () async => await audioHandler.skipToPrevious() - : null, + onPressed: () async => queueService.previousTrack() ), _RoundedIconButton( width: 75, @@ -69,7 +69,7 @@ class PlayerButtons extends ConsumerWidget { IconButton( icon: const Icon(TablerIcons.player_skip_forward), onPressed: playbackState != null - ? () async => audioHandler.skipToNext() + ? () async => queueService.nextTrack() : null, ), FavoriteButton(item: item), diff --git a/lib/components/PlayerScreen/player_screen_appbar_title.dart b/lib/components/PlayerScreen/player_screen_appbar_title.dart new file mode 100644 index 000000000..a9d773363 --- /dev/null +++ b/lib/components/PlayerScreen/player_screen_appbar_title.dart @@ -0,0 +1,63 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:get_it/get_it.dart'; +import 'package:audio_service/audio_service.dart'; +import 'package:palette_generator/palette_generator.dart'; + +import '../../models/jellyfin_models.dart' as jellyfin_models; +import '../../models/finamp_models.dart'; +import 'package:finamp/services/queue_service.dart'; +import '../../to_contrast.dart'; + +class PlayerScreenAppBarTitle extends StatefulWidget { + + const PlayerScreenAppBarTitle({Key? key}) : super(key: key); + + @override + State createState() => _PlayerScreenAppBarTitleState(); +} + +class _PlayerScreenAppBarTitleState extends State { + final QueueService _queueService = GetIt.instance(); + + @override + Widget build(BuildContext context) { + + final currentTrackStream = _queueService.getCurrentTrackStream(); + + return StreamBuilder( + stream: currentTrackStream, + initialData: _queueService.getCurrentTrack(), + builder: (context, snapshot) { + final queueItem = snapshot.data!; + + return Baseline( + baselineType: TextBaseline.alphabetic, + baseline: 0, + child: Column( + children: [ + Text( + "Playing From ${queueItem.source.type.name}", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w300, + color: Colors.white.withOpacity(0.7), + ), + ), + const Padding(padding: EdgeInsets.symmetric(vertical: 2)), + Text( + queueItem.source.name, + style: const TextStyle( + fontSize: 16, + color: Colors.white, + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index 07fa73deb..1086b2f76 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -1,4 +1,5 @@ import 'package:audio_service/audio_service.dart'; +import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:simple_gesture_detector/simple_gesture_detector.dart'; @@ -24,6 +25,7 @@ class NowPlayingBar extends StatelessWidget { final color = Theme.of(context).bottomNavigationBarTheme.backgroundColor; final audioHandler = GetIt.instance(); + final queueService = GetIt.instance(); return SimpleGestureDetector( onVerticalSwipe: (direction) { @@ -62,9 +64,9 @@ class NowPlayingBar extends StatelessWidget { direction: FinampSettingsHelper.finampSettings.disableGesture ? DismissDirection.none : DismissDirection.horizontal, confirmDismiss: (direction) async { if (direction == DismissDirection.endToStart) { - audioHandler.skipToNext(); + queueService.nextTrack(); } else { - audioHandler.skipToPrevious(); + queueService.previousTrack(); } return false; }, diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index cc1f85586..c79dbc3ee 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -7,6 +7,7 @@ import 'package:hive/hive.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:uuid/uuid.dart'; import 'package:path/path.dart' as path_helper; +import 'package:audio_service/audio_service.dart'; import '../services/finamp_settings_helper.dart'; import 'jellyfin_models.dart'; @@ -555,7 +556,7 @@ class DownloadedImage { ); } -enum PlaybackListType { +enum QueueItemType { album(name: "Album"), playlist(name: "Playlist"), @@ -565,58 +566,69 @@ enum PlaybackListType { filteredList(name: "Songs"), genre(name: "Genre"), artist(name: "Artist"), + upNext(name: ""), + formerUpNext(name: "Track added to Up Next"), downloads(name: ""), unknown(name: ""); - const PlaybackListType({ + const QueueItemType({ required this.name, }); final String name; } -class PlaybackListInfo { - PlaybackListInfo({ +class QueueItemSource { + QueueItemSource({ required this.type, required this.name, - this.duration = 0, + required this.id, }); @HiveField(0) - PlaybackListType type; + QueueItemType type; @HiveField(1) String name; @HiveField(2) - int duration; + String id; + } -class PlaybackList { - PlaybackList({ +class QueueItem { + QueueItem({ + required this.item, + required this.source, + }); + + @HiveField(0) + MediaItem item; + + @HiveField(1) + QueueItemSource source; + +} + +class QueueOrder { + + QueueOrder({ required this.items, - required this.info + required this.linearOrder, + required this.shuffledOrder, }); @HiveField(0) - List items; + List items; + /// The linear order of the items in the queue. Used when shuffle is disabled. + /// The integers at index x contains the index of the item within [items] at queue position x. @HiveField(1) - PlaybackListInfo info; + List linearOrder; + + /// The shuffled order of the items in the queue. Used when shuffle is enabled. + /// The integers at index x contains the index of the item within [items] at queue position x. + @HiveField(2) + List shuffledOrder; - static PlaybackList create({ - required List items, - required PlaybackListType type, - required String name, - int? duration, - }) => - PlaybackList( - items: items, - info: PlaybackListInfo( - type: type, - name: name, - duration: duration ?? 0, - ) - ); - } diff --git a/lib/screens/player_screen.dart b/lib/screens/player_screen.dart index 33ae0552e..027f702f6 100644 --- a/lib/screens/player_screen.dart +++ b/lib/screens/player_screen.dart @@ -1,13 +1,14 @@ import 'dart:ui'; +import 'package:finamp/components/PlayerScreen/player_screen_appbar_title.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:octo_image/octo_image.dart'; import 'package:simple_gesture_detector/simple_gesture_detector.dart'; import '../components/PlayerScreen/control_area.dart'; -import '../components/PlayerScreen/song_info.dart'; import '../components/finamp_app_bar_button.dart'; +import '../components/PlayerScreen/song_info.dart'; import '../services/current_album_image_provider.dart'; import '../services/finamp_settings_helper.dart'; import 'package:finamp/services/queue_service.dart'; @@ -25,11 +26,6 @@ class PlayerScreen extends StatelessWidget { @override Widget build(BuildContext context) { - QueueService queueService = - GetIt.instance(); - - PlaybackListInfo playbackListInfo = queueService.getPlaybackListInfo(); - return SimpleGestureDetector( onVerticalSwipe: (direction) { if (!FinampSettingsHelper.finampSettings.disableGesture && @@ -52,60 +48,7 @@ class PlayerScreen extends StatelessWidget { centerTitle: true, leadingWidth: 48 + 24, toolbarHeight: _toolbarHeight, - // actions: const [ - // SleepTimerButton(), - // AddToPlaylistButton(), - // ], - // title: Baseline( - // baselineType: TextBaseline.alphabetic, - // baseline: 0, - // child: Text.rich( - // textAlign: TextAlign.center, - // TextSpan( - // style: GoogleFonts.montserrat(), - // children: [ - // TextSpan( - // text: "Playing From\n", - // style: TextStyle( - // fontSize: 12, - // color: Colors.white.withOpacity(0.7), - // height: 3), - // ), - // const TextSpan( - // text: "Your Likes", - // style: TextStyle( - // fontSize: 16, - // color: Colors.white, - // ), - // ) - // ], - // ), - // ), - // ), - title: Baseline( - baselineType: TextBaseline.alphabetic, - baseline: 0, - child: Column( - children: [ - Text( - "Playing From ${playbackListInfo.type.name}", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w300, - color: Colors.white.withOpacity(0.7), - ), - ), - const Padding(padding: EdgeInsets.symmetric(vertical: 2)), - Text( - playbackListInfo.name, - style: const TextStyle( - fontSize: 16, - color: Colors.white, - ), - ), - ], - ), - ), + title: const PlayerScreenAppBarTitle(), leading: FinampAppBarButton( onPressed: () => Navigator.of(context).pop(), ), diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index 225a40e55..5ad159e3c 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -302,10 +302,14 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } } + int getPlayPositionInSeconds() { + return _player.position.inSeconds; + } + @override Future skipToPrevious() async { try { - if (!_player.hasPrevious || _player.position.inSeconds >= 5) { + if (!_player.hasPrevious) { await _player.seek(Duration.zero, index: _player.currentIndex); } else { await _player.seek(Duration.zero, index: _player.previousIndex); diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 191c8d9f0..9a810d0c3 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:just_audio/just_audio.dart'; import 'package:audio_service/audio_service.dart'; @@ -10,9 +11,12 @@ import 'jellyfin_api_helper.dart'; import 'finamp_settings_helper.dart'; import 'downloads_helper.dart'; import '../models/finamp_models.dart'; -import '../models/jellyfin_models.dart'; +import '../models/jellyfin_models.dart' as jellyfin_models; import 'music_player_background_task.dart'; +enum PlaybackOrder { shuffled, linear } +enum LoopMode { none, loopOne, loopAll } + /// A track queueing service for Finamp. class QueueService { final _jellyfinApiHelper = GetIt.instance(); @@ -23,44 +27,163 @@ class QueueService { // internal state + List _queuePreviousTracks = []; // contains **all** items that have been played, including "next up" + QueueItem? _currentTrack; // the currently playing track + List _queueNextUp = []; // a temporary queue that gets appended to if the user taps "next up" + List _queue = []; // contains all regular queue items + QueueOrder _order = QueueOrder(items: [], linearOrder: [], shuffledOrder: []); // contains all items that were at some point added to the regular queue, as well as their order when shuffle is enabled and disabled. This is used to loop the original queue once the end has been reached and "loop all" is enabled, **excluding** "next up" items and keeping the playback order. + + // public state + + PlaybackOrder playbackOrder = PlaybackOrder.linear; + LoopMode loopMode = LoopMode.none; + + final _currentTrackStream = StreamController.broadcast(); + + // external queue state + // the audio source used by the player. The first X items of all internal queues are merged together into this source, so that all player features, like gapless playback, are supported ConcatenatingAudioSource _queueAudioSource = ConcatenatingAudioSource( children: [], useLazyPreparation: true, ); - final int _audioSourceItemCount = 1 + 1 + 5; - - List _queue = []; // contains all regular queue items - int _currentQueueIndex = 0; - List _nextUpQueue = []; // a temporary queue that gets appended to if the user taps "next up" - List _queueHistory = []; // contains **all** items that have been played, including "next up" - PlaybackList _initialQueue = PlaybackList.create(items: [], type: PlaybackListType.unknown, name: "Somewhere"); // contains the original queue that was set when the queue was last fully replaced using `replaceQueueWithItems`. This is used to repeat the original queue once the end has been reached, **excluding** "next up" items. + int _queueAudioSourceIndex = 0; QueueService() { _audioHandler.getPlaybackEventStream().listen((event) async { - _currentQueueIndex = event.currentIndex ?? 0; + int indexDifference = (event.currentIndex ?? 0) - _queueAudioSourceIndex; + + _queueServiceLogger.finer("Play queue index changed, difference: $indexDifference"); + + if (indexDifference == 0) { + return; + } else if (indexDifference.abs() == 1) { + // user skipped ahead/back + if (indexDifference > 0) { + await _applyNextTrack(); + } else if (indexDifference < 0) { + await _applyPreviousTrack(); + } + } else if (indexDifference.abs() > 1) { + // user skipped ahead/back by more than one track + //TODO implement + _queueServiceLogger.severe("Skipping ahead/back by more than one track not handled yet"); + return; + + } + + _queueAudioSourceIndex = event.currentIndex ?? 0; }); } - Future startPlayback(PlaybackList list) async { + Future _applyNextTrack() async { + //TODO handle "Next Up" queue + + // update internal queues + + if (_queue.isEmpty && loopMode == LoopMode.none) { + _queueServiceLogger.info("Cannot skip ahead, no tracks in queue"); + return false; + } else if (_queue.isEmpty && loopMode == LoopMode.loopOne) { + + _queueServiceLogger.info("Looping current track: '${_currentTrack!.item.title}'"); + _queuePreviousTracks.add(_currentTrack!); + _queue.insert(0, _currentTrack!); + _currentTrack = _queue.removeAt(0); + _currentTrackStream.add(_currentTrack!); + _queueAudioSource.insert(0, await _mediaItemToAudioSource(_currentTrack!.item)); + + return true; + } else if (_queue.isEmpty && loopMode == LoopMode.loopAll) { + //TODO implement + _queueServiceLogger.severe("'Loop all' not implemented yet"); + return false; + } + + _queuePreviousTracks.add(_currentTrack!); + _currentTrack = _queue.removeAt(0); + _currentTrackStream.add(_currentTrack!); + + return true; + + } + + Future _applyPreviousTrack() async { + //TODO handle "Next Up" queue + + // update internal queues + + if (_queuePreviousTracks.isEmpty) { + _queueServiceLogger.info("Cannot skip back, no previous tracks in queue"); + return false; + } + + _queue.insert(0, _currentTrack!); + _currentTrack = _queuePreviousTracks.removeLast(); + _currentTrackStream.add(_currentTrack!); + + return true; + + } + + void nextTrack() async { + //TODO make _audioHandler._player call this function instead of skipping ahead itself + + if (await _applyNextTrack()) { + + // update external queues + _audioHandler.skipToNext(); + _queueAudioSourceIndex++; + + _queueServiceLogger.info("Skipped ahead to next track: '${_currentTrack!.item.title}'"); + + } + + } + + void previousTrack() async { + //TODO handle "Next Up" queue + // update internal queues + + if (_audioHandler.getPlayPositionInSeconds() > 5) { + _audioHandler.seek(const Duration(seconds: 0)); + return; + } + + if (await _applyPreviousTrack()) { + + // update external queues + _audioHandler.skipToPrevious(); + _queueAudioSourceIndex--; + + _queueServiceLogger.info("Skipped back to previous track: '${_currentTrack!.item.title}'"); + + } + + } + + Future startPlayback({ + required List items, + required QueueItemSource source + }) async { - _initialQueue = list; // save original PlaybackList for looping/restarting and meta info - _replaceWithItems(itemList: list.items); - _queueServiceLogger.info("Started playing PlaybackList '${list.info.name}' (${list.info.type})"); + // _initialQueue = list; // save original PlaybackList for looping/restarting and meta info + _replaceWholeQueue(itemList: items, source: source); + _queueServiceLogger.info("Started playing '${source.name}' (${source.type})"); } /// Replaces the queue with the given list of items. If startAtIndex is specified, Any items below it /// will be ignored. This is used for when the user taps in the middle of an album to start from that point. - Future _replaceWithItems({ - required List + Future _replaceWholeQueue({ + required List itemList, //TODO create a custom type for item lists that can also hold the name of the list, etc. + required QueueItemSource source, int initialIndex = 0, - bool shuffle = false, }) async { try { if (initialIndex > itemList.length) { @@ -70,84 +193,152 @@ class QueueService { _queue.clear(); // empty queue - for (BaseItemDto item in itemList) { + List newItems = []; + List newLinearOrder = []; + List newShuffledOrder; + for (int i = 0; i < itemList.length; i++) { + jellyfin_models.BaseItemDto item = itemList[i]; try { - _queue.add(await _generateMediaItem(item)); + MediaItem mediaItem = await _generateMediaItem(item); + newItems.add(QueueItem( + item: mediaItem, + source: source, + )); + newLinearOrder.add(i); } catch (e) { _queueServiceLogger.severe(e); } } + newShuffledOrder = List.from(newLinearOrder)..shuffle(); + + _order = QueueOrder( + items: newItems, + linearOrder: newLinearOrder, + shuffledOrder: newShuffledOrder, + ); + + _queueServiceLogger.fine("Order items length: ${_order.items.length}"); + + // log linear order and shuffled order + String linearOrderString = ""; + for (int itemIndex in _order.linearOrder) { + linearOrderString += "${newItems[itemIndex].item.title}, "; + } + linearOrderString = linearOrderString.substring(0, linearOrderString.length - 2); // remove last ", " + String shuffledOrderString = ""; + for (int itemIndex in _order.shuffledOrder) { + shuffledOrderString += "${newItems[itemIndex].item.title}, "; + } + shuffledOrderString = shuffledOrderString.substring(0, shuffledOrderString.length - 2); // remove last ", " + + _queueServiceLogger.finer("Linear order [${_order.linearOrder.length}]: $linearOrderString"); + _queueServiceLogger.finer("Shuffled order [${_order.shuffledOrder.length}]: $shuffledOrderString"); + + // add items to queue + for (int itemIndex in (playbackOrder == PlaybackOrder.linear ? _order.linearOrder : _order.shuffledOrder)) { + _queue.add(_order.items[itemIndex]); + } + + _queueServiceLogger.fine("Queue length: ${_queue.length}"); + + _currentTrack = _queue.removeAt(0); + _currentTrackStream.add(_currentTrack!); + _queueServiceLogger.info("Current track: '${_currentTrack!.item.title}'"); + // start playing first item in queue - _currentQueueIndex = 0; - _audioHandler.setNextInitialIndex(_currentQueueIndex); + _queueAudioSourceIndex = 0; + _audioHandler.setNextInitialIndex(_queueAudioSourceIndex); _queueAudioSource.clear(); + _queueAudioSource.add(await _mediaItemToAudioSource(_currentTrack!.item)); - for (final mediaItem in _queue) { - _queueAudioSource.add(await _mediaItemToAudioSource(mediaItem)); + for (final queueItem in _queue) { + _queueAudioSource.add(await _mediaItemToAudioSource(queueItem.item)); } - //TODO implement shuffle - _audioHandler.initializeAudioSource(_queueAudioSource); - _audioHandler.queue.add(_queue); + _audioHandler.queue.add(_queue.map((e) => e.item).toList()); - _audioHandler.mediaItem.add(_queue[_currentQueueIndex]); + _audioHandler.mediaItem.add(_currentTrack!.item); _audioHandler.play(); _audioHandler.nextInitialIndex = null; - } catch (e) { _queueServiceLogger.severe(e); return Future.error(e); } } - Future addItemAtEnd(BaseItemDto item) async { + Future addToQueue(jellyfin_models.BaseItemDto item, QueueItemSource source) async { try { - // If the queue is empty (like when the app is first launched), run the - // replace queue function instead so that the song gets played - if ((_audioHandler.queue.valueOrNull?.length ?? 0) == 0) { - await _replaceWithItems(itemList: [item]); - return; - } + QueueItem queueItem = QueueItem( + item: await _generateMediaItem(item), + source: source, + ); + + _order.items.add(queueItem); + _order.linearOrder.add(_order.items.length - 1); + _order.shuffledOrder.add(_order.items.length - 1); //TODO maybe the item should be shuffled into the queue? depends on user preference + + _queue.add(queueItem); - final itemMediaItem = await _generateMediaItem(item); - await _audioHandler.addQueueItem(itemMediaItem); + _queueServiceLogger.fine("Added '${queueItem.item.title}' to queue from '${source.name}' (${source.type})"); + } catch (e) { _queueServiceLogger.severe(e); return Future.error(e); } } - Future addItemAtNext(BaseItemDto item) async { + Future addNext(jellyfin_models.BaseItemDto item) async { try { - // If the queue is empty (like when the app is first launched), run the - // replace queue function instead so that the song gets played - if ((_queueAudioSource.length ?? 0) == 0) { - await _replaceWithItems(itemList: [item]); - return; - } + QueueItem queueItem = QueueItem( + item: await _generateMediaItem(item), + source: QueueItemSource(id: "up-next", name: "Up Next", type: QueueItemType.upNext), + ); - final itemMediaItem = await _generateMediaItem(item); - await _audioHandler.addQueueItem(itemMediaItem); + // don't add to _order, because it wasn't added to the regular queue + _queueNextUp.insert(0, queueItem); + + _queueServiceLogger.fine("Prepended '${queueItem.item.title}' to Next Up"); + + } catch (e) { + _queueServiceLogger.severe(e); + return Future.error(e); + } + } + + Future addToNextUp(jellyfin_models.BaseItemDto item) async { + try { + QueueItem queueItem = QueueItem( + item: await _generateMediaItem(item), + source: QueueItemSource(id: "up-next", name: "Up Next", type: QueueItemType.upNext), + ); + + // don't add to _order, because it wasn't added to the regular queue + + _queueNextUp.add(queueItem); + + _queueServiceLogger.fine("Prepended '${queueItem.item.title}' to Next Up"); - - } catch (e) { _queueServiceLogger.severe(e); return Future.error(e); } } - PlaybackListInfo getPlaybackListInfo() { - return _initialQueue.info; + Stream getCurrentTrackStream() { + return _currentTrackStream.stream.asBroadcastStream(); + } + + QueueItem getCurrentTrack() { + return _currentTrack!; } - Future _generateMediaItem(BaseItemDto item) async { + Future _generateMediaItem(jellyfin_models.BaseItemDto item) async { const uuid = Uuid(); final downloadedSong = _downloadsHelper.getDownloadedSong(item.id); From c51955b05d1d5241c50294c6d4f20300fe95ff84 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 18 May 2023 23:30:02 +0200 Subject: [PATCH 003/130] looping a single track working --- .../player_buttons_repeating.dart | 65 ++++++++------- .../music_player_background_task.dart | 1 + lib/services/queue_service.dart | 80 +++++++++++++------ 3 files changed, 96 insertions(+), 50 deletions(-) diff --git a/lib/components/PlayerScreen/player_buttons_repeating.dart b/lib/components/PlayerScreen/player_buttons_repeating.dart index a1088c24a..ed3e2228e 100644 --- a/lib/components/PlayerScreen/player_buttons_repeating.dart +++ b/lib/components/PlayerScreen/player_buttons_repeating.dart @@ -1,6 +1,7 @@ import 'package:audio_service/audio_service.dart'; import 'package:finamp/services/media_state_stream.dart'; import 'package:finamp/services/music_player_background_task.dart'; +import 'package:finamp/services/queue_service.dart'; import 'package:finamp/services/player_screen_theme_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -9,6 +10,7 @@ import 'package:get_it/get_it.dart'; class PlayerButtonsRepeating extends ConsumerWidget { final audioHandler = GetIt.instance(); + final queueService = GetIt.instance(); PlayerButtonsRepeating({ Key? key, @@ -24,32 +26,41 @@ class PlayerButtonsRepeating extends ConsumerWidget { : Colors.white), ), child: StreamBuilder( - stream: mediaStateStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - final mediaState = snapshot.data; - final playbackState = mediaState?.playbackState; + stream: queueService.getCurrentTrackStream(), + builder: (BuildContext context, snapshot) { + final queueItem = snapshot.data; return IconButton( - onPressed: playbackState != null - ? () async { - // Cyles from none -> all -> one - if (playbackState!.repeatMode == - AudioServiceRepeatMode.none) { - await audioHandler - .setRepeatMode(AudioServiceRepeatMode.all); - } else if (playbackState!.repeatMode == - AudioServiceRepeatMode.all) { - await audioHandler - .setRepeatMode(AudioServiceRepeatMode.one); - } else { - await audioHandler - .setRepeatMode(AudioServiceRepeatMode.none); - } - } - : null, + onPressed: () async { + // Cyles from none -> all -> one + // if (playbackState!.repeatMode == + // AudioServiceRepeatMode.none) { + // await audioHandler + // .setRepeatMode(AudioServiceRepeatMode.all); + // } else if (playbackState!.repeatMode == + // AudioServiceRepeatMode.all) { + // await audioHandler + // .setRepeatMode(AudioServiceRepeatMode.one); + // } else { + // await audioHandler + // .setRepeatMode(AudioServiceRepeatMode.none); + // } + switch (queueService.loopMode) { + case LoopMode.none: + queueService.loopMode = LoopMode.all; + break; + case LoopMode.all: + queueService.loopMode = LoopMode.one; + break; + case LoopMode.one: + queueService.loopMode = LoopMode.none; + break; + default: + queueService.loopMode = LoopMode.none; + break; + } + }, icon: _getRepeatingIcon( - playbackState == null - ? AudioServiceRepeatMode.none - : playbackState!.repeatMode, + queueService.loopMode, Theme.of(context).colorScheme.secondary, )); }), @@ -57,10 +68,10 @@ class PlayerButtonsRepeating extends ConsumerWidget { } Widget _getRepeatingIcon( - AudioServiceRepeatMode repeatMode, Color iconColour) { - if (repeatMode == AudioServiceRepeatMode.all) { + LoopMode loopMode, Color iconColour) { + if (loopMode == LoopMode.all) { return const Icon(TablerIcons.repeat); - } else if (repeatMode == AudioServiceRepeatMode.one) { + } else if (loopMode == LoopMode.one) { return const Icon(TablerIcons.repeat_once); } else { return const Icon(TablerIcons.repeat_off); diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index 5ad159e3c..749751273 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -29,6 +29,7 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { preferredForwardBufferDuration: FinampSettingsHelper.finampSettings.bufferDuration, )), + ); ConcatenatingAudioSource _queueAudioSource = ConcatenatingAudioSource(children: []); diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 9a810d0c3..9e57f1962 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -15,7 +15,7 @@ import '../models/jellyfin_models.dart' as jellyfin_models; import 'music_player_background_task.dart'; enum PlaybackOrder { shuffled, linear } -enum LoopMode { none, loopOne, loopAll } +enum LoopMode { none, one, all } /// A track queueing service for Finamp. class QueueService { @@ -33,10 +33,8 @@ class QueueService { List _queue = []; // contains all regular queue items QueueOrder _order = QueueOrder(items: [], linearOrder: [], shuffledOrder: []); // contains all items that were at some point added to the regular queue, as well as their order when shuffle is enabled and disabled. This is used to loop the original queue once the end has been reached and "loop all" is enabled, **excluding** "next up" items and keeping the playback order. - // public state - - PlaybackOrder playbackOrder = PlaybackOrder.linear; - LoopMode loopMode = LoopMode.none; + PlaybackOrder _playbackOrder = PlaybackOrder.linear; + LoopMode _loopMode = LoopMode.none; final _currentTrackStream = StreamController.broadcast(); @@ -58,16 +56,17 @@ class QueueService { _queueServiceLogger.finer("Play queue index changed, difference: $indexDifference"); if (indexDifference == 0) { + //TODO figure out a way to detect looped tracks (loopMode == LoopMode.one) to add them to the playback history return; } else if (indexDifference.abs() == 1) { - // user skipped ahead/back + // player skipped ahead/back if (indexDifference > 0) { - await _applyNextTrack(); + await _applyNextTrack(eventFromPlayer: true); } else if (indexDifference < 0) { - await _applyPreviousTrack(); + await _applyPreviousTrack(eventFromPlayer: true); } } else if (indexDifference.abs() > 1) { - // user skipped ahead/back by more than one track + // player skipped ahead/back by more than one track //TODO implement _queueServiceLogger.severe("Skipping ahead/back by more than one track not handled yet"); return; @@ -80,25 +79,27 @@ class QueueService { } - Future _applyNextTrack() async { + Future _applyNextTrack({bool eventFromPlayer = false}) async { //TODO handle "Next Up" queue // update internal queues - if (_queue.isEmpty && loopMode == LoopMode.none) { + _queueServiceLogger.finer("Loop mode: $loopMode"); + + if (loopMode == LoopMode.one) { + _queueServiceLogger.finer("Looping current track: '${_currentTrack!.item.title}'"); + + //TODO update playback history + + if (eventFromPlayer) { + return false; // player already skipped + } + + return true; // perform the skip + } if (_queue.isEmpty && loopMode == LoopMode.none) { _queueServiceLogger.info("Cannot skip ahead, no tracks in queue"); return false; - } else if (_queue.isEmpty && loopMode == LoopMode.loopOne) { - - _queueServiceLogger.info("Looping current track: '${_currentTrack!.item.title}'"); - _queuePreviousTracks.add(_currentTrack!); - _queue.insert(0, _currentTrack!); - _currentTrack = _queue.removeAt(0); - _currentTrackStream.add(_currentTrack!); - _queueAudioSource.insert(0, await _mediaItemToAudioSource(_currentTrack!.item)); - - return true; - } else if (_queue.isEmpty && loopMode == LoopMode.loopAll) { + } else if (_queue.isEmpty && loopMode == LoopMode.all) { //TODO implement _queueServiceLogger.severe("'Loop all' not implemented yet"); return false; @@ -112,11 +113,21 @@ class QueueService { } - Future _applyPreviousTrack() async { + Future _applyPreviousTrack({bool eventFromPlayer = false}) async { //TODO handle "Next Up" queue // update internal queues + if (loopMode == LoopMode.one) { + _queueServiceLogger.finer("Looping current track: '${_currentTrack!.item.title}'"); + + if (eventFromPlayer) { + return false; // player already skipped + } + + return true; // perform the skip + } + if (_queuePreviousTracks.isEmpty) { _queueServiceLogger.info("Cannot skip back, no previous tracks in queue"); return false; @@ -338,6 +349,29 @@ class QueueService { return _currentTrack!; } + void set loopMode(LoopMode mode) { + _loopMode = mode; + _currentTrackStream.add(_currentTrack ?? QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown))); + + if (mode == LoopMode.one) { + _audioHandler.setRepeatMode(AudioServiceRepeatMode.one); // without the repeat mode, we cannot prevent the player from skipping to the next track + } else { + _audioHandler.setRepeatMode(AudioServiceRepeatMode.none); + } + + } + + LoopMode get loopMode => _loopMode; + + void set playbackOrder(PlaybackOrder order) { + _playbackOrder = order; + _currentTrackStream.add(_currentTrack ?? QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown))); + + //TODO update queue accordingly and generate new shuffled order if necessary + } + + PlaybackOrder get playbackOrder => _playbackOrder; + Future _generateMediaItem(jellyfin_models.BaseItemDto item) async { const uuid = Uuid(); From 1936f8d57b3f8de594904f5c0d9017fbf488b0f1 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 19 May 2023 20:54:35 +0200 Subject: [PATCH 004/130] use new queue for `shuffleAll()` --- lib/main.dart | 10 +++---- lib/models/finamp_models.dart | 2 +- lib/services/audio_service_helper.dart | 36 ++++++++++++++++++-------- lib/services/queue_service.dart | 3 ++- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 13b3c9ce6..813f8cdb5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -63,8 +63,7 @@ void main() async { _setupJellyfinApiData(); await _setupDownloader(); await _setupDownloadsHelper(); - await _setupAudioServiceHelper(); - await _setupQueueService(); + await _setupPlaybackServices(); } catch (e) { hasFailed = true; runApp(FinampErrorApp( @@ -93,10 +92,6 @@ void _setupJellyfinApiData() { GetIt.instance.registerSingleton(JellyfinApiHelper()); } -Future _setupQueueService() async { - GetIt.instance.registerSingleton(QueueService()); -} - Future _setupDownloadsHelper() async { GetIt.instance.registerSingleton(DownloadsHelper()); } @@ -184,7 +179,7 @@ Future setupHive() async { if (themeModeBox.isEmpty) ThemeModeHelper.setThemeMode(ThemeMode.system); } -Future _setupAudioServiceHelper() async { +Future _setupPlaybackServices() async { final session = await AudioSession.instance; session.configure(const AudioSessionConfiguration.music()); @@ -202,6 +197,7 @@ Future _setupAudioServiceHelper() async { // () async => ); GetIt.instance.registerSingleton(audioHandler); + GetIt.instance.registerSingleton(QueueService()); GetIt.instance.registerSingleton(AudioServiceHelper()); } diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index c79dbc3ee..f8b751b66 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -562,7 +562,7 @@ enum QueueItemType { playlist(name: "Playlist"), mix(name: "Instant Mix"), favorites(name: "Your Likes"), - list(name: "All Songs"), + songs(name: "All Songs"), filteredList(name: "Songs"), genre(name: "Genre"), artist(name: "Artist"), diff --git a/lib/services/audio_service_helper.dart b/lib/services/audio_service_helper.dart index 2a59fa0ae..ab8576975 100644 --- a/lib/services/audio_service_helper.dart +++ b/lib/services/audio_service_helper.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:audio_service/audio_service.dart'; import 'package:get_it/get_it.dart'; import 'package:logging/logging.dart'; @@ -7,21 +9,24 @@ import 'finamp_user_helper.dart'; import 'jellyfin_api_helper.dart'; import 'finamp_settings_helper.dart'; import 'downloads_helper.dart'; -import '../models/jellyfin_models.dart'; +import '../models/finamp_models.dart'; +import '../models/jellyfin_models.dart' as jellyfin_models; import 'music_player_background_task.dart'; +import 'queue_service.dart'; /// Just some functions to make talking to AudioService a bit neater. class AudioServiceHelper { final _jellyfinApiHelper = GetIt.instance(); final _downloadsHelper = GetIt.instance(); final _audioHandler = GetIt.instance(); + final _queueService = GetIt.instance(); final _finampUserHelper = GetIt.instance(); final audioServiceHelperLogger = Logger("AudioServiceHelper"); /// Replaces the queue with the given list of items. If startAtIndex is specified, Any items below it /// will be ignored. This is used for when the user taps in the middle of an album to start from that point. Future replaceQueueWithItem({ - required List itemList, //TODO create a custom type for item lists that can also hold the name of the list, etc. + required List itemList, //TODO create a custom type for item lists that can also hold the name of the list, etc. int initialIndex = 0, bool shuffle = false, }) async { @@ -32,7 +37,7 @@ class AudioServiceHelper { } List queue = []; - for (BaseItemDto item in itemList) { + for (jellyfin_models.BaseItemDto item in itemList) { try { queue.add(await _generateMediaItem(item)); } catch (e) { @@ -64,7 +69,7 @@ class AudioServiceHelper { } } - Future addQueueItem(BaseItemDto item) async { + Future addQueueItem(jellyfin_models.BaseItemDto item) async { try { // If the queue is empty (like when the app is first launched), run the // replace queue function instead so that the song gets played @@ -83,7 +88,7 @@ class AudioServiceHelper { /// Shuffles every song in the user's current view. Future shuffleAll(bool isFavourite) async { - List? items; + List? items; if (FinampSettingsHelper.finampSettings.isOffline) { // If offline, get a shuffled list of songs from _downloadsHelper. @@ -110,13 +115,22 @@ class AudioServiceHelper { } if (items != null) { - await replaceQueueWithItem(itemList: items, shuffle: true); + // await replaceQueueWithItem(itemList: items, shuffle: true); + _queueService.playbackOrder = PlaybackOrder.shuffled; + await _queueService.startPlayback( + items: items, + source: QueueItemSource( + type: isFavourite ? QueueItemType.favorites : QueueItemType.songs, + name: "Shuffle All", + id: "shuffleAll", + ) + ); } } /// Start instant mix from item. - Future startInstantMixForItem(BaseItemDto item) async { - List? items; + Future startInstantMixForItem(jellyfin_models.BaseItemDto item) async { + List? items; try { items = await _jellyfinApiHelper.getInstantMix(item); @@ -131,7 +145,7 @@ class AudioServiceHelper { /// Start instant mix from a selection of artists. Future startInstantMixForArtists(List artistIds) async { - List? items; + List? items; try { items = await _jellyfinApiHelper.getArtistMix(artistIds); @@ -146,7 +160,7 @@ class AudioServiceHelper { /// Start instant mix from a selection of albums. Future startInstantMixForAlbums(List albumIds) async { - List? items; + List? items; try { items = await _jellyfinApiHelper.getAlbumMix(albumIds); @@ -159,7 +173,7 @@ class AudioServiceHelper { } } - Future _generateMediaItem(BaseItemDto item) async { + Future _generateMediaItem(jellyfin_models.BaseItemDto item) async { const uuid = Uuid(); final downloadedSong = _downloadsHelper.getDownloadedSong(item.id); diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 9e57f1962..9d5bf9742 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -63,6 +63,7 @@ class QueueService { if (indexDifference > 0) { await _applyNextTrack(eventFromPlayer: true); } else if (indexDifference < 0) { + //TODO properly handle rewinding instead of skipping back await _applyPreviousTrack(eventFromPlayer: true); } } else if (indexDifference.abs() > 1) { @@ -160,7 +161,7 @@ class QueueService { //TODO handle "Next Up" queue // update internal queues - if (_audioHandler.getPlayPositionInSeconds() > 5) { + if (_audioHandler.getPlayPositionInSeconds() > 5 || _queuePreviousTracks.isEmpty) { _audioHandler.seek(const Duration(seconds: 0)); return; } From b22fb96a27b6d37982496cf904d59723db29e605 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 20 May 2023 00:49:02 +0200 Subject: [PATCH 005/130] implemented 'loop all' for linear and shuffled mode - there's still a bug if the player skips to the next track by itself: if 'loop all' was enabled previously but then disabled, the player will still loop once - for more info see TODO at queue_service.dart:445 --- lib/services/queue_service.dart | 100 ++++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 10 deletions(-) diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 9d5bf9742..6e5750aeb 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -70,7 +70,7 @@ class QueueService { // player skipped ahead/back by more than one track //TODO implement _queueServiceLogger.severe("Skipping ahead/back by more than one track not handled yet"); - return; + // return; } @@ -97,12 +97,16 @@ class QueueService { } return true; // perform the skip - } if (_queue.isEmpty && loopMode == LoopMode.none) { + } if ( + (_queue.length + _queueNextUp.length == 1) // handle player skipping to next track by itself + && loopMode == LoopMode.all + ) { + + await applyLoopQueue(distanceFromEndOfQueue: 1); + + } else if (_queue.isEmpty && loopMode == LoopMode.none) { _queueServiceLogger.info("Cannot skip ahead, no tracks in queue"); - return false; - } else if (_queue.isEmpty && loopMode == LoopMode.all) { - //TODO implement - _queueServiceLogger.severe("'Loop all' not implemented yet"); + //TODO show snackbar return false; } @@ -110,6 +114,12 @@ class QueueService { _currentTrack = _queue.removeAt(0); _currentTrackStream.add(_currentTrack!); + String queueString = ""; + for (QueueItem queueItem in _queue) { + queueString += "${queueItem.item.title}, "; + } + _queueServiceLogger.finer("Queue after skipping [${_queue.length}]: $queueString"); + return true; } @@ -237,12 +247,10 @@ class QueueService { for (int itemIndex in _order.linearOrder) { linearOrderString += "${newItems[itemIndex].item.title}, "; } - linearOrderString = linearOrderString.substring(0, linearOrderString.length - 2); // remove last ", " String shuffledOrderString = ""; for (int itemIndex in _order.shuffledOrder) { shuffledOrderString += "${newItems[itemIndex].item.title}, "; } - shuffledOrderString = shuffledOrderString.substring(0, shuffledOrderString.length - 2); // remove last ", " _queueServiceLogger.finer("Linear order [${_order.linearOrder.length}]: $linearOrderString"); _queueServiceLogger.finer("Shuffled order [${_order.shuffledOrder.length}]: $shuffledOrderString"); @@ -350,7 +358,7 @@ class QueueService { return _currentTrack!; } - void set loopMode(LoopMode mode) { + set loopMode(LoopMode mode) { _loopMode = mode; _currentTrackStream.add(_currentTrack ?? QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown))); @@ -358,13 +366,40 @@ class QueueService { _audioHandler.setRepeatMode(AudioServiceRepeatMode.one); // without the repeat mode, we cannot prevent the player from skipping to the next track } else { _audioHandler.setRepeatMode(AudioServiceRepeatMode.none); + + // handle enabling loop all when the queue is empty + if (mode == LoopMode.all && (_queue.length + _queueNextUp.length == 0)) { + applyLoopQueue(); + } else if (mode != LoopMode.all) { + // find current track in `_order` and set the queue to the items after it + int currentTrackIndex = _order.items.indexOf(_currentTrack!); + int currentTrackOrderIndex = (_playbackOrder == PlaybackOrder.linear ? _order.linearOrder : _order.shuffledOrder).indexWhere((trackIndex) => trackIndex == currentTrackIndex); + // use indices of current playback order to get the items after the current track + List itemsAfterCurrentTrack = (_playbackOrder == PlaybackOrder.linear ? _order.linearOrder : _order.shuffledOrder).sublist(currentTrackOrderIndex+1); + // add items to queue + _queue.clear(); + for (int itemIndex in itemsAfterCurrentTrack) { + _queue.add(_order.items[itemIndex]); + } + + // log queue + String queueString = ""; + for (QueueItem queueItem in _queue) { + queueString += "${queueItem.item.title}, "; + } + _queueServiceLogger.finer("Queue after disabling 'loop all' [${_queue.length}]: $queueString"); + + // update external queues + pushQueueToExternalQueues(); + + } } } LoopMode get loopMode => _loopMode; - void set playbackOrder(PlaybackOrder order) { + set playbackOrder(PlaybackOrder order) { _playbackOrder = order; _currentTrackStream.add(_currentTrack ?? QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown))); @@ -373,6 +408,51 @@ class QueueService { PlaybackOrder get playbackOrder => _playbackOrder; + Future applyLoopQueue({ distanceFromEndOfQueue = 0 }) async { + + _queueServiceLogger.fine("Looping queue using `_order`"); + + // log current queue + String queueString = ""; + for (QueueItem queueItem in _queue) { + queueString += "${queueItem.item.title}, "; + } + _queueServiceLogger.finer("Current queue [${_queue.length}]: $queueString"); + + // add items to queue + for (int itemIndex in (_playbackOrder == PlaybackOrder.linear ? _order.linearOrder : _order.shuffledOrder)) { + _queue.add(_order.items[itemIndex]); + } + + // log looped queue + queueString = ""; + for (QueueItem queueItem in _queue) { + queueString += "${queueItem.item.title}, "; + } + _queueServiceLogger.finer("Current queue [${_queue.length}]: $queueString"); + + // update external queues + // _queueAudioSource.removeRange(1 + distanceFromEndOfQueue, _queueAudioSource.length+1); // clear all but the current track (not sure if this is necessary, queueAudioSource should be empty anyway) + await pushQueueToExternalQueues(skipFirst: distanceFromEndOfQueue); + + _queueServiceLogger.info("Looped queue, added ${_order.items.length} items"); + + } + + Future pushQueueToExternalQueues({ skipFirst = 0 }) async { + _audioHandler.queue.add(_queue.sublist(skipFirst).map((e) => e.item).toList()); + + //TODO handle queue still looping after disabling 'loop all' if looping has already been prepared before and skip is performed by the player (e.g. through notification) + // and figure out why this doesn't work + // for (int i = 1; i < _queueAudioSource.length; i++) { + // _queueAudioSource.removeAt(1); + // } + // _queueAudioSource.removeRange(1, _queueAudioSource.length+1); // clear all but the current track + for (final queueItem in _queue.sublist(skipFirst)) { + _queueAudioSource.add(await _mediaItemToAudioSource(queueItem.item)); + } + } + Future _generateMediaItem(jellyfin_models.BaseItemDto item) async { const uuid = Uuid(); From 1f0afd07811a9924202825e8e7b2101fbd63f4db Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 21 May 2023 15:40:07 +0200 Subject: [PATCH 006/130] working looping (linear & shuffled) --- .../AlbumScreen/song_list_tile.dart | 5 +- .../PlayerScreen/player_buttons.dart | 6 +- lib/components/now_playing_bar.dart | 6 +- lib/services/jellyfin_api.dart | 2 +- .../music_player_background_task.dart | 34 +++- lib/services/queue_service.dart | 156 +++++++++++------- 6 files changed, 140 insertions(+), 69 deletions(-) diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index f7cdffdc8..8e5d2415e 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -294,7 +294,7 @@ class _SongListTileState extends State { switch (selection) { case SongListTileMenuItems.addToQueue: - await _audioServiceHelper.addQueueItem(widget.item); + // await _audioServiceHelper.addQueueItem(widget.item); await _queueService.addToQueue(widget.item, QueueItemSource(type: QueueItemType.unknown, name: "Queue", id: widget.parentId!)); if (!mounted) return; @@ -422,7 +422,8 @@ class _SongListTileState extends State { ), ), confirmDismiss: (direction) async { - await _audioServiceHelper.addQueueItem(widget.item); + // await _audioServiceHelper.addQueueItem(widget.item); + await _queueService.addToQueue(widget.item, QueueItemSource(type: QueueItemType.unknown, name: "Queue", id: widget.parentId!)); if (!mounted) return false; diff --git a/lib/components/PlayerScreen/player_buttons.dart b/lib/components/PlayerScreen/player_buttons.dart index b69cde1be..053340e0f 100644 --- a/lib/components/PlayerScreen/player_buttons.dart +++ b/lib/components/PlayerScreen/player_buttons.dart @@ -46,7 +46,8 @@ class PlayerButtons extends ConsumerWidget { PlayerButtonsRepeating(), IconButton( icon: const Icon(TablerIcons.player_skip_back), - onPressed: () async => queueService.previousTrack() + // onPressed: () async => queueService.previousTrack() + onPressed: () async => audioHandler.skipToPrevious() ), _RoundedIconButton( width: 75, @@ -69,7 +70,8 @@ class PlayerButtons extends ConsumerWidget { IconButton( icon: const Icon(TablerIcons.player_skip_forward), onPressed: playbackState != null - ? () async => queueService.nextTrack() + // ? () async => queueService.nextTrack() + ? () async => audioHandler.skipToNext() : null, ), FavoriteButton(item: item), diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index 1086b2f76..5ed79a79f 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -64,9 +64,11 @@ class NowPlayingBar extends StatelessWidget { direction: FinampSettingsHelper.finampSettings.disableGesture ? DismissDirection.none : DismissDirection.horizontal, confirmDismiss: (direction) async { if (direction == DismissDirection.endToStart) { - queueService.nextTrack(); + // queueService.nextTrack(); + audioHandler.skipToNext(); } else { - queueService.previousTrack(); + // queueService.previousTrack(); + audioHandler.skipToPrevious(); } return false; }, diff --git a/lib/services/jellyfin_api.dart b/lib/services/jellyfin_api.dart index fca6fee19..09a67cc70 100644 --- a/lib/services/jellyfin_api.dart +++ b/lib/services/jellyfin_api.dart @@ -413,7 +413,7 @@ abstract class JellyfinApi extends ChopperService { // return request.copyWith( // headers: {"X-Emby-Authentication": await getAuthHeader()}); // }, - HttpLoggingInterceptor(), + HttpLoggingInterceptor(level: Level.none), ], ); diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index 749751273..efe1b12f4 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -61,6 +61,9 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { Duration _sleepTimerDuration = Duration.zero; final ValueNotifier _sleepTimer = ValueNotifier(null); + Future Function()? _queueCallbackNextTrack; + Future Function()? _queueCallbackPreviousTrack; + List? get shuffleIndices => _player.shuffleIndices; ValueListenable get sleepTimer => _sleepTimer; @@ -135,6 +138,14 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } + void setQueueCallbacks({ + required Future Function() nextTrackCallback, + required Future Function() previousTrackCallback + }) { + _queueCallbackNextTrack = nextTrackCallback; + _queueCallbackPreviousTrack = previousTrackCallback; + } + Stream getPlaybackEventStream() { return _playbackEventStreamController.stream; } @@ -309,11 +320,21 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { @override Future skipToPrevious() async { + + bool doSkip = true; + try { + + if (_queueCallbackPreviousTrack != null) { + doSkip = await _queueCallbackPreviousTrack!(); + } + if (!_player.hasPrevious) { await _player.seek(Duration.zero, index: _player.currentIndex); } else { - await _player.seek(Duration.zero, index: _player.previousIndex); + if (doSkip) { + await _player.seek(Duration.zero, index: _player.previousIndex); + } } } catch (e) { _audioServiceBackgroundTaskLogger.severe(e); @@ -323,8 +344,17 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { @override Future skipToNext() async { + + bool doSkip = true; + try { - await _player.seekToNext(); + + if (_queueCallbackNextTrack != null) { + doSkip = await _queueCallbackNextTrack!(); + } + if (doSkip) { + await _player.seekToNext(); + } } catch (e) { _audioServiceBackgroundTaskLogger.severe(e); return Future.error(e); diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 6e5750aeb..ebc956a64 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -61,10 +61,10 @@ class QueueService { } else if (indexDifference.abs() == 1) { // player skipped ahead/back if (indexDifference > 0) { - await _applyNextTrack(eventFromPlayer: true); + // await _applyNextTrack(eventFromPlayer: true); } else if (indexDifference < 0) { //TODO properly handle rewinding instead of skipping back - await _applyPreviousTrack(eventFromPlayer: true); + // await _applyPreviousTrack(eventFromPlayer: true); } } else if (indexDifference.abs() > 1) { // player skipped ahead/back by more than one track @@ -77,6 +77,9 @@ class QueueService { _queueAudioSourceIndex = event.currentIndex ?? 0; }); + + // register callbacks + _audioHandler.setQueueCallbacks(nextTrackCallback: _applyNextTrack, previousTrackCallback: _applyPreviousTrack); } @@ -85,9 +88,12 @@ class QueueService { // update internal queues + bool addCurrentTrackToPreviousTracks = true; + _queueServiceLogger.finer("Loop mode: $loopMode"); if (loopMode == LoopMode.one) { + _audioHandler.seek(Duration.zero); _queueServiceLogger.finer("Looping current track: '${_currentTrack!.item.title}'"); //TODO update playback history @@ -96,13 +102,14 @@ class QueueService { return false; // player already skipped } - return true; // perform the skip + return false; // perform the skip } if ( - (_queue.length + _queueNextUp.length == 1) // handle player skipping to next track by itself + (_queue.length + _queueNextUp.length == 0) && loopMode == LoopMode.all ) { - await applyLoopQueue(distanceFromEndOfQueue: 1); + await _applyLoopQueue(); + addCurrentTrackToPreviousTracks = false; } else if (_queue.isEmpty && loopMode == LoopMode.none) { _queueServiceLogger.info("Cannot skip ahead, no tracks in queue"); @@ -110,15 +117,13 @@ class QueueService { return false; } - _queuePreviousTracks.add(_currentTrack!); + if (addCurrentTrackToPreviousTracks) { + _queuePreviousTracks.add(_currentTrack!); + } _currentTrack = _queue.removeAt(0); _currentTrackStream.add(_currentTrack!); - String queueString = ""; - for (QueueItem queueItem in _queue) { - queueString += "${queueItem.item.title}, "; - } - _queueServiceLogger.finer("Queue after skipping [${_queue.length}]: $queueString"); + _logQueues(message: "after skipping forward"); return true; @@ -141,6 +146,7 @@ class QueueService { if (_queuePreviousTracks.isEmpty) { _queueServiceLogger.info("Cannot skip back, no previous tracks in queue"); + _audioHandler.seek(Duration.zero); return false; } @@ -148,45 +154,47 @@ class QueueService { _currentTrack = _queuePreviousTracks.removeLast(); _currentTrackStream.add(_currentTrack!); + _logQueues(message: "after skipping backwards"); + return true; } - void nextTrack() async { - //TODO make _audioHandler._player call this function instead of skipping ahead itself + // Future nextTrack() async { + // //TODO make _audioHandler._player call this function instead of skipping ahead itself - if (await _applyNextTrack()) { + // if (await _applyNextTrack()) { - // update external queues - _audioHandler.skipToNext(); - _queueAudioSourceIndex++; + // // update external queues + // _audioHandler.skipToNext(); + // _queueAudioSourceIndex++; - _queueServiceLogger.info("Skipped ahead to next track: '${_currentTrack!.item.title}'"); + // _queueServiceLogger.info("Skipped ahead to next track: '${_currentTrack!.item.title}'"); - } + // } - } + // } - void previousTrack() async { - //TODO handle "Next Up" queue - // update internal queues + // Future previousTrack() async { + // //TODO handle "Next Up" queue + // // update internal queues - if (_audioHandler.getPlayPositionInSeconds() > 5 || _queuePreviousTracks.isEmpty) { - _audioHandler.seek(const Duration(seconds: 0)); - return; - } + // if (_audioHandler.getPlayPositionInSeconds() > 5 || _queuePreviousTracks.isEmpty) { + // _audioHandler.seek(const Duration(seconds: 0)); + // return; + // } - if (await _applyPreviousTrack()) { + // if (await _applyPreviousTrack()) { - // update external queues - _audioHandler.skipToPrevious(); - _queueAudioSourceIndex--; + // // update external queues + // _audioHandler.skipToPrevious(); + // _queueAudioSourceIndex--; - _queueServiceLogger.info("Skipped back to previous track: '${_currentTrack!.item.title}'"); + // _queueServiceLogger.info("Skipped back to previous track: '${_currentTrack!.item.title}'"); - } + // } - } + // } Future startPlayback({ required List items, @@ -214,6 +222,7 @@ class QueueService { } _queue.clear(); // empty queue + _queuePreviousTracks.clear(); List newItems = []; List newLinearOrder = []; @@ -260,21 +269,21 @@ class QueueService { _queue.add(_order.items[itemIndex]); } - _queueServiceLogger.fine("Queue length: ${_queue.length}"); - _currentTrack = _queue.removeAt(0); _currentTrackStream.add(_currentTrack!); _queueServiceLogger.info("Current track: '${_currentTrack!.item.title}'"); + _logQueues(message: "after replacing whole queue"); + // start playing first item in queue _queueAudioSourceIndex = 0; _audioHandler.setNextInitialIndex(_queueAudioSourceIndex); _queueAudioSource.clear(); - _queueAudioSource.add(await _mediaItemToAudioSource(_currentTrack!.item)); + await _queueAudioSource.add(await _mediaItemToAudioSource(_currentTrack!.item)); for (final queueItem in _queue) { - _queueAudioSource.add(await _mediaItemToAudioSource(queueItem.item)); + await _queueAudioSource.add(await _mediaItemToAudioSource(queueItem.item)); } _audioHandler.initializeAudioSource(_queueAudioSource); @@ -306,6 +315,11 @@ class QueueService { _queue.add(queueItem); _queueServiceLogger.fine("Added '${queueItem.item.title}' to queue from '${source.name}' (${source.type})"); + + //TODO if 'loop all' is enabled, update external queues + if (_loopMode == LoopMode.all && _queue.length == 0) { + await pushQueueToExternalQueues(skipFirst: _queue.length-1); + } } catch (e) { _queueServiceLogger.severe(e); @@ -363,13 +377,13 @@ class QueueService { _currentTrackStream.add(_currentTrack ?? QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown))); if (mode == LoopMode.one) { - _audioHandler.setRepeatMode(AudioServiceRepeatMode.one); // without the repeat mode, we cannot prevent the player from skipping to the next track + // _audioHandler.setRepeatMode(AudioServiceRepeatMode.one); // without the repeat mode, we cannot prevent the player from skipping to the next track } else { - _audioHandler.setRepeatMode(AudioServiceRepeatMode.none); + // _audioHandler.setRepeatMode(AudioServiceRepeatMode.none); // handle enabling loop all when the queue is empty if (mode == LoopMode.all && (_queue.length + _queueNextUp.length == 0)) { - applyLoopQueue(); + _applyLoopQueue(); } else if (mode != LoopMode.all) { // find current track in `_order` and set the queue to the items after it int currentTrackIndex = _order.items.indexOf(_currentTrack!); @@ -382,12 +396,7 @@ class QueueService { _queue.add(_order.items[itemIndex]); } - // log queue - String queueString = ""; - for (QueueItem queueItem in _queue) { - queueString += "${queueItem.item.title}, "; - } - _queueServiceLogger.finer("Queue after disabling 'loop all' [${_queue.length}]: $queueString"); + _logQueues(message: "after looping"); // update external queues pushQueueToExternalQueues(); @@ -408,28 +417,24 @@ class QueueService { PlaybackOrder get playbackOrder => _playbackOrder; - Future applyLoopQueue({ distanceFromEndOfQueue = 0 }) async { + Future _applyLoopQueue({ distanceFromEndOfQueue = 0 }) async { + + //TODO handle skipping backwards when 'loop all' is enabled (add named parameter, add tracks from `_order` to `_queuePreviousTracks`, clear `_queue`) _queueServiceLogger.fine("Looping queue using `_order`"); // log current queue - String queueString = ""; - for (QueueItem queueItem in _queue) { - queueString += "${queueItem.item.title}, "; - } - _queueServiceLogger.finer("Current queue [${_queue.length}]: $queueString"); + _logQueues(message: "before looping"); // add items to queue for (int itemIndex in (_playbackOrder == PlaybackOrder.linear ? _order.linearOrder : _order.shuffledOrder)) { _queue.add(_order.items[itemIndex]); } + _queuePreviousTracks.clear(); + // log looped queue - queueString = ""; - for (QueueItem queueItem in _queue) { - queueString += "${queueItem.item.title}, "; - } - _queueServiceLogger.finer("Current queue [${_queue.length}]: $queueString"); + _logQueues(message: "after looping"); // update external queues // _queueAudioSource.removeRange(1 + distanceFromEndOfQueue, _queueAudioSource.length+1); // clear all but the current track (not sure if this is necessary, queueAudioSource should be empty anyway) @@ -440,6 +445,7 @@ class QueueService { } Future pushQueueToExternalQueues({ skipFirst = 0 }) async { + _audioHandler.queue.add(_queue.sublist(skipFirst).map((e) => e.item).toList()); //TODO handle queue still looping after disabling 'loop all' if looping has already been prepared before and skip is performed by the player (e.g. through notification) @@ -447,12 +453,42 @@ class QueueService { // for (int i = 1; i < _queueAudioSource.length; i++) { // _queueAudioSource.removeAt(1); // } - // _queueAudioSource.removeRange(1, _queueAudioSource.length+1); // clear all but the current track + _queueAudioSource.removeRange(_queueAudioSourceIndex+1, _queueAudioSource.length); // clear all but the current track for (final queueItem in _queue.sublist(skipFirst)) { - _queueAudioSource.add(await _mediaItemToAudioSource(queueItem.item)); + await _queueAudioSource.add(await _mediaItemToAudioSource(queueItem.item)); } } + void _logQueues({ String message = "" }) { + + // generate string for `_queue` + String queueString = ""; + for (QueueItem queueItem in _queuePreviousTracks) { + queueString += "${queueItem.item.title}, "; + } + queueString += "[${_currentTrack?.item.title}], "; + for (QueueItem queueItem in _queue) { + queueString += "${queueItem.item.title}, "; + } + + // generate string for `_queueAudioSource` + // String queueAudioSourceString = ""; + // queueAudioSourceString += "[${_queueAudioSource.sequence.first.toString()}], "; + // for (AudioSource queueItem in _queueAudioSource.sequence.sublist(1)) { + // queueAudioSourceString += "${queueItem.toString()}, "; + // } + + + // log queues + _queueServiceLogger.finer( + "Queue $message [${_queuePreviousTracks.length}-1-${_queue.length}]: $queueString" + ); + // _queueServiceLogger.finer( + // "Audio Source Queue $message [${_queue.length}]: $queueAudioSourceString" + // ) + + } + Future _generateMediaItem(jellyfin_models.BaseItemDto item) async { const uuid = Uuid(); From a8d52ea0c7461c0c6594d83eadcdbae6cca78cd2 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 21 May 2023 17:03:21 +0200 Subject: [PATCH 007/130] 'loop all' working when skipping backwards - needed to replace the concatenating audio source, because the player's index cannot be modified externally - this results in backwards loops not being gapless, oh well... - also fixed skipping backwards when 'loop one' is enabled (resets current track and adds entry to history) --- lib/services/queue_service.dart | 107 ++++++++++++++++++++++++++------ 1 file changed, 89 insertions(+), 18 deletions(-) diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index ebc956a64..47f06480f 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -132,25 +132,40 @@ class QueueService { Future _applyPreviousTrack({bool eventFromPlayer = false}) async { //TODO handle "Next Up" queue + bool addCurrentTrackToQueue = true; + // update internal queues if (loopMode == LoopMode.one) { + + _audioHandler.seek(Duration.zero); _queueServiceLogger.finer("Looping current track: '${_currentTrack!.item.title}'"); + //TODO update playback history if (eventFromPlayer) { return false; // player already skipped } - return true; // perform the skip + return false; // perform the skip } if (_queuePreviousTracks.isEmpty) { - _queueServiceLogger.info("Cannot skip back, no previous tracks in queue"); - _audioHandler.seek(Duration.zero); - return false; + + if (loopMode == LoopMode.all) { + + await _applyLoopQueue(skippingBackwards: true); + addCurrentTrackToQueue = false; + + } else { + _queueServiceLogger.info("Cannot skip back, no previous tracks in queue"); + _audioHandler.seek(Duration.zero); + return false; + } } - _queue.insert(0, _currentTrack!); + if (addCurrentTrackToQueue) { + _queue.insert(0, _currentTrack!); + } _currentTrack = _queuePreviousTracks.removeLast(); _currentTrackStream.add(_currentTrack!); @@ -286,7 +301,7 @@ class QueueService { await _queueAudioSource.add(await _mediaItemToAudioSource(queueItem.item)); } - _audioHandler.initializeAudioSource(_queueAudioSource); + await _audioHandler.initializeAudioSource(_queueAudioSource); _audioHandler.queue.add(_queue.map((e) => e.item).toList()); @@ -417,34 +432,53 @@ class QueueService { PlaybackOrder get playbackOrder => _playbackOrder; - Future _applyLoopQueue({ distanceFromEndOfQueue = 0 }) async { + Future _applyLoopQueue({ + distanceFromEndOfQueue = 0, + skippingBackwards = false, + }) async { //TODO handle skipping backwards when 'loop all' is enabled (add named parameter, add tracks from `_order` to `_queuePreviousTracks`, clear `_queue`) - _queueServiceLogger.fine("Looping queue using `_order`"); + _queueServiceLogger.fine("Looping queue for ${skippingBackwards ? "skipping backwards" : "skipping forward"} using `_order`"); // log current queue _logQueues(message: "before looping"); - // add items to queue - for (int itemIndex in (_playbackOrder == PlaybackOrder.linear ? _order.linearOrder : _order.shuffledOrder)) { - _queue.add(_order.items[itemIndex]); - } + if (skippingBackwards) { + + // add items to previous tracks + for (int itemIndex in (_playbackOrder == PlaybackOrder.linear ? _order.linearOrder : _order.shuffledOrder)) { + _queuePreviousTracks.add(_order.items[itemIndex]); + } + + _queue.clear(); + + } else { - _queuePreviousTracks.clear(); + // add items to queue + for (int itemIndex in (_playbackOrder == PlaybackOrder.linear ? _order.linearOrder : _order.shuffledOrder)) { + _queue.add(_order.items[itemIndex]); + } + + _queuePreviousTracks.clear(); + + } // log looped queue _logQueues(message: "after looping"); // update external queues // _queueAudioSource.removeRange(1 + distanceFromEndOfQueue, _queueAudioSource.length+1); // clear all but the current track (not sure if this is necessary, queueAudioSource should be empty anyway) - await pushQueueToExternalQueues(skipFirst: distanceFromEndOfQueue); + await pushQueueToExternalQueues(skipFirst: distanceFromEndOfQueue, skippingBackwards: skippingBackwards); _queueServiceLogger.info("Looped queue, added ${_order.items.length} items"); } - Future pushQueueToExternalQueues({ skipFirst = 0 }) async { + Future pushQueueToExternalQueues({ + skipFirst = 0, + skippingBackwards = false, + }) async { _audioHandler.queue.add(_queue.sublist(skipFirst).map((e) => e.item).toList()); @@ -453,9 +487,46 @@ class QueueService { // for (int i = 1; i < _queueAudioSource.length; i++) { // _queueAudioSource.removeAt(1); // } - _queueAudioSource.removeRange(_queueAudioSourceIndex+1, _queueAudioSource.length); // clear all but the current track - for (final queueItem in _queue.sublist(skipFirst)) { - await _queueAudioSource.add(await _mediaItemToAudioSource(queueItem.item)); + + if (skippingBackwards) { + + // start playing first item in queue + ConcatenatingAudioSource newQueueAudioSource = ConcatenatingAudioSource( + children: [], + useLazyPreparation: true, + ); + int newQueueAudioSourceIndex = 0; + + // _queueServiceLogger.finer("`_queuePreviousTracks.length`: ${_queuePreviousTracks.length}"); + // _queueServiceLogger.finer("`_queue.length`: ${_queue.length}"); + for (final queueItem in _queuePreviousTracks) { + await newQueueAudioSource.add(await _mediaItemToAudioSource(queueItem.item)); + newQueueAudioSourceIndex++; + } + await newQueueAudioSource.add(await _mediaItemToAudioSource(_currentTrack!.item)); + // _queueServiceLogger.finer("`newQueueAudioSourceIndex`: ${newQueueAudioSourceIndex}"); + _audioHandler.setNextInitialIndex(newQueueAudioSourceIndex); + for (final queueItem in _queue) { + await newQueueAudioSource.add(await _mediaItemToAudioSource(queueItem.item)); + } + + _queueServiceLogger.finer("Replacing `_queueAudioSource` due to looping backwards..."); + await _audioHandler.initializeAudioSource(newQueueAudioSource); + _queueAudioSource = newQueueAudioSource; + + // _audioHandler.play(); + + _audioHandler.nextInitialIndex = null; + + } else { + + _queueAudioSource.removeRange(_queueAudioSourceIndex+1, _queueAudioSource.length); // clear queue after the current track + + // add items to queue + for (final queueItem in _queue.sublist(skipFirst)) { + await _queueAudioSource.add(await _mediaItemToAudioSource(queueItem.item)); + } + } } From c80c778091d039599beac7bbb13b93e38f7a5812 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 21 May 2023 22:01:11 +0200 Subject: [PATCH 008/130] simplify external queue updates - properly remove, prepend, and append AudioSources from/to the ConcatenatingAudioSource --- lib/services/queue_service.dart | 82 ++++++++++----------------------- 1 file changed, 24 insertions(+), 58 deletions(-) diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 47f06480f..90d335cf8 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -325,15 +325,14 @@ class QueueService { _order.items.add(queueItem); _order.linearOrder.add(_order.items.length - 1); - _order.shuffledOrder.add(_order.items.length - 1); //TODO maybe the item should be shuffled into the queue? depends on user preference + _order.shuffledOrder.add(_order.items.length - 1); //TODO maybe the item should be shuffled into the queue instead of placed at the end? depends on user preference _queue.add(queueItem); _queueServiceLogger.fine("Added '${queueItem.item.title}' to queue from '${source.name}' (${source.type})"); - //TODO if 'loop all' is enabled, update external queues if (_loopMode == LoopMode.all && _queue.length == 0) { - await pushQueueToExternalQueues(skipFirst: _queue.length-1); + await pushQueueToExternalQueues(); } } catch (e) { @@ -389,7 +388,7 @@ class QueueService { set loopMode(LoopMode mode) { _loopMode = mode; - _currentTrackStream.add(_currentTrack ?? QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown))); + _currentTrackStream.add(_currentTrack ?? QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown))); if (mode == LoopMode.one) { // _audioHandler.setRepeatMode(AudioServiceRepeatMode.one); // without the repeat mode, we cannot prevent the player from skipping to the next track @@ -425,7 +424,7 @@ class QueueService { set playbackOrder(PlaybackOrder order) { _playbackOrder = order; - _currentTrackStream.add(_currentTrack ?? QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown))); + _currentTrackStream.add(_currentTrack ?? QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown))); //TODO update queue accordingly and generate new shuffled order if necessary } @@ -433,12 +432,9 @@ class QueueService { PlaybackOrder get playbackOrder => _playbackOrder; Future _applyLoopQueue({ - distanceFromEndOfQueue = 0, skippingBackwards = false, }) async { - //TODO handle skipping backwards when 'loop all' is enabled (add named parameter, add tracks from `_order` to `_queuePreviousTracks`, clear `_queue`) - _queueServiceLogger.fine("Looping queue for ${skippingBackwards ? "skipping backwards" : "skipping forward"} using `_order`"); // log current queue @@ -468,66 +464,36 @@ class QueueService { _logQueues(message: "after looping"); // update external queues - // _queueAudioSource.removeRange(1 + distanceFromEndOfQueue, _queueAudioSource.length+1); // clear all but the current track (not sure if this is necessary, queueAudioSource should be empty anyway) - await pushQueueToExternalQueues(skipFirst: distanceFromEndOfQueue, skippingBackwards: skippingBackwards); + await pushQueueToExternalQueues(); _queueServiceLogger.info("Looped queue, added ${_order.items.length} items"); } - Future pushQueueToExternalQueues({ - skipFirst = 0, - skippingBackwards = false, - }) async { - - _audioHandler.queue.add(_queue.sublist(skipFirst).map((e) => e.item).toList()); - - //TODO handle queue still looping after disabling 'loop all' if looping has already been prepared before and skip is performed by the player (e.g. through notification) - // and figure out why this doesn't work - // for (int i = 1; i < _queueAudioSource.length; i++) { - // _queueAudioSource.removeAt(1); - // } - - if (skippingBackwards) { - - // start playing first item in queue - ConcatenatingAudioSource newQueueAudioSource = ConcatenatingAudioSource( - children: [], - useLazyPreparation: true, - ); - int newQueueAudioSourceIndex = 0; + Future pushQueueToExternalQueues() async { - // _queueServiceLogger.finer("`_queuePreviousTracks.length`: ${_queuePreviousTracks.length}"); - // _queueServiceLogger.finer("`_queue.length`: ${_queue.length}"); - for (final queueItem in _queuePreviousTracks) { - await newQueueAudioSource.add(await _mediaItemToAudioSource(queueItem.item)); - newQueueAudioSourceIndex++; - } - await newQueueAudioSource.add(await _mediaItemToAudioSource(_currentTrack!.item)); - // _queueServiceLogger.finer("`newQueueAudioSourceIndex`: ${newQueueAudioSourceIndex}"); - _audioHandler.setNextInitialIndex(newQueueAudioSourceIndex); - for (final queueItem in _queue) { - await newQueueAudioSource.add(await _mediaItemToAudioSource(queueItem.item)); - } + _audioHandler.queue.add(_queue.map((e) => e.item).toList()); - _queueServiceLogger.finer("Replacing `_queueAudioSource` due to looping backwards..."); - await _audioHandler.initializeAudioSource(newQueueAudioSource); - _queueAudioSource = newQueueAudioSource; + // clear queue after the current track + // do this first so that the current track index stays the same + _queueAudioSource.removeRange(_queueAudioSourceIndex+1, _queueAudioSource.length); + // clear queue before the current track + _queueAudioSource.removeRange(0, _queueAudioSourceIndex); - // _audioHandler.play(); - - _audioHandler.nextInitialIndex = null; - - } else { - - _queueAudioSource.removeRange(_queueAudioSourceIndex+1, _queueAudioSource.length); // clear queue after the current track + // add items to queue + List previousAudioSources = []; + for (QueueItem queueItem in _queuePreviousTracks) { + previousAudioSources.add(await _mediaItemToAudioSource(queueItem.item)); + } + await _queueAudioSource.insertAll(0, previousAudioSources); - // add items to queue - for (final queueItem in _queue.sublist(skipFirst)) { - await _queueAudioSource.add(await _mediaItemToAudioSource(queueItem.item)); - } - + // add items to queue + List nextAudioSources = []; + for (QueueItem queueItem in _queue) { + nextAudioSources.add(await _mediaItemToAudioSource(queueItem.item)); } + await _queueAudioSource.addAll(nextAudioSources); + } void _logQueues({ String message = "" }) { From b7eea9bcb1b9d5cedbaef73767745b476b99da86 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 21 May 2023 22:46:09 +0200 Subject: [PATCH 009/130] working shuffle toggle --- .../PlayerScreen/player_buttons_shuffle.dart | 37 ++++---- lib/services/queue_service.dart | 92 ++++++++++++++++++- 2 files changed, 111 insertions(+), 18 deletions(-) diff --git a/lib/components/PlayerScreen/player_buttons_shuffle.dart b/lib/components/PlayerScreen/player_buttons_shuffle.dart index 004bf1845..f4a8eeacc 100644 --- a/lib/components/PlayerScreen/player_buttons_shuffle.dart +++ b/lib/components/PlayerScreen/player_buttons_shuffle.dart @@ -2,6 +2,7 @@ import 'package:audio_service/audio_service.dart'; import 'package:finamp/services/media_state_stream.dart'; import 'package:finamp/services/music_player_background_task.dart'; import 'package:finamp/services/player_screen_theme_provider.dart'; +import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; @@ -9,6 +10,7 @@ import 'package:get_it/get_it.dart'; class PlayerButtonsShuffle extends ConsumerWidget { final audioHandler = GetIt.instance(); + final _queueService = GetIt.instance(); PlayerButtonsShuffle({Key? key}) : super(key: key); @@ -22,25 +24,28 @@ class PlayerButtonsShuffle extends ConsumerWidget { : Colors.white), ), child: StreamBuilder( - stream: mediaStateStream, + stream: _queueService.getCurrentTrackStream(), builder: (BuildContext context, AsyncSnapshot snapshot) { - final mediaState = snapshot.data; - final playbackState = mediaState?.playbackState; + final currentTrack = snapshot.data; + // final playbackState = mediaState?.playbackState; return IconButton( - onPressed: playbackState != null - ? () async { - if (playbackState!.shuffleMode == - AudioServiceShuffleMode.all) { - await audioHandler - .setShuffleMode(AudioServiceShuffleMode.none); - } else { - await audioHandler - .setShuffleMode(AudioServiceShuffleMode.all); - } - } - : null, + // onPressed: playbackState != null + // ? () async { + // if (playbackState!.shuffleMode == + // AudioServiceShuffleMode.all) { + // await audioHandler + // .setShuffleMode(AudioServiceShuffleMode.none); + // } else { + // await audioHandler + // .setShuffleMode(AudioServiceShuffleMode.all); + // } + // } + // : null, + onPressed: () async { + _queueService.playbackOrder = _queueService.playbackOrder == PlaybackOrder.shuffled ? PlaybackOrder.linear : PlaybackOrder.shuffled; + }, icon: Icon( - (playbackState?.shuffleMode == AudioServiceShuffleMode.all + (_queueService.playbackOrder == PlaybackOrder.shuffled ? TablerIcons.arrows_shuffle : TablerIcons.arrows_right), ), diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 90d335cf8..f75fedc23 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -424,9 +424,13 @@ class QueueService { set playbackOrder(PlaybackOrder order) { _playbackOrder = order; - _currentTrackStream.add(_currentTrack ?? QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown))); + // _currentTrackStream.add(_currentTrack ?? QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown))); + + // update queue accordingly and generate new shuffled order if necessary + if (_currentTrack != null) { + _applyUpdatePlaybackOrder(); + } - //TODO update queue accordingly and generate new shuffled order if necessary } PlaybackOrder get playbackOrder => _playbackOrder; @@ -470,6 +474,90 @@ class QueueService { } + Future _applyUpdatePlaybackOrder() async { + + _logQueues(message: "before playback order change to ${_playbackOrder.name}"); + + // find current track in `_order` + int currentTrackIndex = _currentTrack != null ? _order.items.indexOf(_currentTrack!) : 0; + int currentTrackOrderIndex = 0; + + + List itemsBeforeCurrentTrack = []; + List itemsAfterCurrentTrack = []; + + if (_playbackOrder == PlaybackOrder.shuffled) { + + // calculate new shuffled order where the currentTrack has index 0 + _order.shuffledOrder = List.from(_order.linearOrder)..shuffle(); + + currentTrackOrderIndex = _order.shuffledOrder.indexWhere((trackIndex) => trackIndex == currentTrackIndex); + + String shuffleOrderString = ""; + for (int index in _order.shuffledOrder) { + shuffleOrderString += "$index, "; + } + _queueServiceLogger.finer("unmodified new shuffle order: $shuffleOrderString"); + + // swap the current track to index 0 + int indexOfCurrentFirstTrackInShuffleOrder = _order.shuffledOrder[0]; + _order.shuffledOrder[0] = currentTrackIndex; + _order.shuffledOrder[currentTrackOrderIndex] = indexOfCurrentFirstTrackInShuffleOrder; + + _queueServiceLogger.finer("indexOfCurrentFirstTrackInShuffleOrder: ${indexOfCurrentFirstTrackInShuffleOrder}"); + _queueServiceLogger.finer("current track first in shuffled order: ${currentTrackIndex == _order.shuffledOrder[0]}"); + + String swappedShuffleOrderString = ""; + for (int index in _order.shuffledOrder) { + swappedShuffleOrderString += "$index, "; + } + _queueServiceLogger.finer("swapped new shuffle order: $swappedShuffleOrderString"); + + // use indices of current playback order to get the items after the current track + // first item is always the current track, so skip it + itemsAfterCurrentTrack = _order.shuffledOrder.sublist(1); + String sublistString = ""; + for (int index in itemsAfterCurrentTrack) { + sublistString += "$index, "; + } + _queueServiceLogger.finer("item indices after current track: $sublistString"); + + } else { + + currentTrackOrderIndex = _order.linearOrder.indexWhere((trackIndex) => trackIndex == currentTrackIndex); + + // set the queue to the items after the current track and previousTracks to items before the current track + // use indices of current playback order to get the items before the current track + itemsBeforeCurrentTrack = _order.linearOrder.sublist(0, currentTrackOrderIndex); + + // use indices of current playback order to get the items after the current track + itemsAfterCurrentTrack = _order.linearOrder.sublist(currentTrackOrderIndex+1); + + } + + // add items to previous tracks + _queuePreviousTracks.clear(); + for (int itemIndex in itemsBeforeCurrentTrack) { + // if (itemIndex != currentTrackIndex) { + _queuePreviousTracks.add(_order.items[itemIndex]); + // } + } + // add items to queue + _queue.clear(); + for (int itemIndex in itemsAfterCurrentTrack) { + // if (itemIndex != currentTrackIndex) { + _queue.add(_order.items[itemIndex]); + // } + } + + _logQueues(message: "after playback order change to ${_playbackOrder.name}"); + + await pushQueueToExternalQueues(); + + _currentTrackStream.add(_currentTrack!); + + } + Future pushQueueToExternalQueues() async { _audioHandler.queue.add(_queue.map((e) => e.item).toList()); From 90b9d4d16347d941fe21907783b4a279354fbadc Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Mon, 22 May 2023 00:54:00 +0200 Subject: [PATCH 010/130] stream queue to queue list - convert Streams to BehaviorSubjects - show current track and queue (needs fixing) --- lib/components/PlayerScreen/queue_list.dart | 244 ++++++++++++++------ lib/models/finamp_models.dart | 19 ++ lib/services/queue_service.dart | 108 ++++++++- 3 files changed, 290 insertions(+), 81 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 8857dff51..1be0ba9fb 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -1,4 +1,5 @@ import 'package:audio_service/audio_service.dart'; +import 'package:finamp/models/finamp_models.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; @@ -6,19 +7,22 @@ import 'package:rxdart/rxdart.dart'; import '../../services/finamp_settings_helper.dart'; import '../album_image.dart'; -import '../../models/jellyfin_models.dart'; +import '../../models/jellyfin_models.dart' as jellyfin_models; import '../../services/process_artist.dart'; import '../../services/media_state_stream.dart'; import '../../services/music_player_background_task.dart'; +import '../../services/queue_service.dart'; class _QueueListStreamState { _QueueListStreamState( this.queue, this.mediaState, + this.queueInfo, ); final List? queue; final MediaState mediaState; + final QueueInfo queueInfo; } class QueueList extends StatefulWidget { @@ -32,83 +36,185 @@ class QueueList extends StatefulWidget { class _QueueListState extends State { final _audioHandler = GetIt.instance(); - List? _queue; + final _queueService = GetIt.instance(); + List? _previousTracks; + QueueItem? _currentTrack; + List? _queue; @override Widget build(BuildContext context) { return StreamBuilder<_QueueListStreamState>( // stream: AudioService.queueStream, - stream: Rx.combineLatest2?, MediaState, - _QueueListStreamState>(_audioHandler.queue, mediaStateStream, - (a, b) => _QueueListStreamState(a, b)), + stream: Rx.combineLatest3?, MediaState, QueueInfo, + _QueueListStreamState>(_audioHandler.queue, mediaStateStream, _queueService.getQueueStream(), + (a, b, c) => _QueueListStreamState(a, b, c)), + // stream: _queueService.getQueueStream(), builder: (context, snapshot) { + if (snapshot.hasData) { - _queue ??= snapshot.data!.queue; - return PrimaryScrollController( - controller: widget.scrollController, - child: Padding( - padding: const EdgeInsets.only(top: 24), - child: ReorderableListView.builder( - itemCount: snapshot.data!.queue?.length ?? 0, - onReorder: (oldIndex, newIndex) async { - setState(() { - // _queue?.insert(newIndex, _queue![oldIndex]); - // _queue?.removeAt(oldIndex); - int? smallerThanNewIndex; - if (oldIndex < newIndex) { - // When we're moving an item backwards, we need to reduce - // newIndex by 1 to account for there being a new item added - // before newIndex. - smallerThanNewIndex = newIndex - 1; - } - final item = _queue?.removeAt(oldIndex); - _queue?.insert(smallerThanNewIndex ?? newIndex, item!); - }); - await _audioHandler.reorderQueue(oldIndex, newIndex); - }, - itemBuilder: (context, index) { - final actualIndex = - _audioHandler.playbackState.valueOrNull?.shuffleMode == - AudioServiceShuffleMode.all - ? _audioHandler.shuffleIndices![index] - : index; - return Dismissible( - key: ValueKey(snapshot.data!.queue![actualIndex].id), - direction: - FinampSettingsHelper.finampSettings.disableGesture - ? DismissDirection.none - : DismissDirection.horizontal, - onDismissed: (direction) async { - await _audioHandler.removeQueueItemAt(actualIndex); - }, - child: ListTile( - leading: AlbumImage( - item: snapshot.data!.queue?[actualIndex] - .extras?["itemJson"] == - null - ? null - : BaseItemDto.fromJson(snapshot.data! - .queue?[actualIndex].extras?["itemJson"]), + + _previousTracks ??= snapshot.data!.queueInfo.previousTracks; + _currentTrack ??= snapshot.data!.queueInfo.currentTrack; + _queue ??= snapshot.data!.queueInfo.queue; + + return Stack( + children: [ + PrimaryScrollController( + controller: widget.scrollController, + child: Padding( + padding: const EdgeInsets.only(top: 24), + child: ReorderableListView.builder( + itemCount: _previousTracks?.length ?? 0, + onReorder: (oldIndex, newIndex) async { + setState(() { + // _previousTracks?.insert(newIndex, _previousTracks![oldIndex]); + // _previousTracks?.removeAt(oldIndex); + int? smallerThanNewIndex; + if (oldIndex < newIndex) { + // When we're moving an item backwards, we need to reduce + // newIndex by 1 to account for there being a new item added + // before newIndex. + smallerThanNewIndex = newIndex - 1; + } + final item = _previousTracks?.removeAt(oldIndex); + _previousTracks?.insert(smallerThanNewIndex ?? newIndex, item!); + }); + await _audioHandler.reorderQueue(oldIndex, newIndex); + }, + itemBuilder: (context, index) { + final actualIndex = + _audioHandler.playbackState.valueOrNull?.shuffleMode == + AudioServiceShuffleMode.all + ? _audioHandler.shuffleIndices![index] + : index; + return Dismissible( + key: ValueKey(_previousTracks![actualIndex].item.id), + direction: + FinampSettingsHelper.finampSettings.disableGesture + ? DismissDirection.none + : DismissDirection.horizontal, + onDismissed: (direction) async { + await _audioHandler.removeQueueItemAt(actualIndex); + }, + child: ListTile( + leading: AlbumImage( + item: _previousTracks?[actualIndex].item + .extras?["itemJson"] == + null + ? null + : jellyfin_models.BaseItemDto.fromJson(_previousTracks?[actualIndex].item.extras?["itemJson"]), + ), + title: Text( + _previousTracks?[actualIndex].item.title ?? + AppLocalizations.of(context)!.unknownName, + style: _currentTrack == + _previousTracks?[actualIndex] + ? TextStyle( + color: + Theme.of(context).colorScheme.secondary) + : null), + subtitle: Text(processArtist( + _previousTracks?[actualIndex].item.artist, + context)), + onTap: () async => + await _audioHandler.skipToIndex(actualIndex), ), - title: Text( - snapshot.data!.queue?[actualIndex].title ?? - AppLocalizations.of(context)!.unknownName, - style: snapshot.data!.mediaState.mediaItem == - snapshot.data!.queue?[actualIndex] - ? TextStyle( - color: - Theme.of(context).colorScheme.secondary) - : null), - subtitle: Text(processArtist( - snapshot.data!.queue?[actualIndex].artist, - context)), - onTap: () async => - await _audioHandler.skipToIndex(actualIndex), - ), - ); - }, + ); + }, + ), + ) + ), + const Padding(padding: EdgeInsets.symmetric(vertical: 20)), + ListTile( + leading: AlbumImage( + item: _currentTrack!.item + .extras?["itemJson"] == + null + ? null + : jellyfin_models.BaseItemDto.fromJson(_currentTrack!.item.extras?["itemJson"]), + ), + title: Text( + _currentTrack!.item.title ?? + AppLocalizations.of(context)!.unknownName, + style: TextStyle( + color: + Theme.of(context).colorScheme.secondary) ), - )); + subtitle: Text(processArtist( + _currentTrack!.item.artist, + context)), + onTap: () async => + snapshot.data!.mediaState.playbackState.playing ? await _audioHandler.pause() : await _audioHandler.play(), + ), + const Padding(padding: EdgeInsets.symmetric(vertical: 20)), + PrimaryScrollController( + controller: widget.scrollController, + child: Padding( + padding: const EdgeInsets.only(top: 24), + child: ReorderableListView.builder( + itemCount: _queue?.length ?? 0, + onReorder: (oldIndex, newIndex) async { + setState(() { + // _queue?.insert(newIndex, _queue![oldIndex]); + // _queue?.removeAt(oldIndex); + int? smallerThanNewIndex; + if (oldIndex < newIndex) { + // When we're moving an item backwards, we need to reduce + // newIndex by 1 to account for there being a new item added + // before newIndex. + smallerThanNewIndex = newIndex - 1; + } + final item = _queue?.removeAt(oldIndex); + _queue?.insert(smallerThanNewIndex ?? newIndex, item!); + }); + await _audioHandler.reorderQueue(oldIndex, newIndex); + }, + itemBuilder: (context, index) { + final actualIndex = + _audioHandler.playbackState.valueOrNull?.shuffleMode == + AudioServiceShuffleMode.all + ? _audioHandler.shuffleIndices![index] + : index; + return Dismissible( + key: ValueKey(_queue![actualIndex].item.id), + direction: + FinampSettingsHelper.finampSettings.disableGesture + ? DismissDirection.none + : DismissDirection.horizontal, + onDismissed: (direction) async { + await _audioHandler.removeQueueItemAt(actualIndex); + }, + child: ListTile( + leading: AlbumImage( + item: _queue?[actualIndex].item + .extras?["itemJson"] == + null + ? null + : jellyfin_models.BaseItemDto.fromJson(_queue?[actualIndex].item.extras?["itemJson"]), + ), + title: Text( + _queue?[actualIndex].item.title ?? + AppLocalizations.of(context)!.unknownName, + style: _currentTrack == + _queue?[actualIndex] + ? TextStyle( + color: + Theme.of(context).colorScheme.secondary) + : null), + subtitle: Text(processArtist( + _queue?[actualIndex].item.artist, + context)), + onTap: () async => + await _audioHandler.skipToIndex(actualIndex), + ), + ); + }, + ) + ) + ), + ], + ); + } else { return const Center( child: CircularProgressIndicator.adaptive(), diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index f8b751b66..7eb4a1937 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -632,3 +632,22 @@ class QueueOrder { List shuffledOrder; } + +class QueueInfo { + + QueueInfo({ + required this.previousTracks, + required this.currentTrack, + required this.queue, + }); + + @HiveField(0) + List previousTracks; + + @HiveField(1) + QueueItem currentTrack; + + @HiveField(2) + List queue; + +} diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index f75fedc23..650fb5b77 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -4,6 +4,7 @@ import 'package:just_audio/just_audio.dart'; import 'package:audio_service/audio_service.dart'; import 'package:get_it/get_it.dart'; import 'package:logging/logging.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:uuid/uuid.dart'; import 'finamp_user_helper.dart'; @@ -36,7 +37,14 @@ class QueueService { PlaybackOrder _playbackOrder = PlaybackOrder.linear; LoopMode _loopMode = LoopMode.none; - final _currentTrackStream = StreamController.broadcast(); + final _currentTrackStream = BehaviorSubject.seeded( + QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown)) + ); + final _queueStream = BehaviorSubject.seeded(QueueInfo( + previousTracks: [], + currentTrack: QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown)), + queue: [], + )); // external queue state @@ -125,6 +133,8 @@ class QueueService { _logQueues(message: "after skipping forward"); + // await pushQueueToExternalQueues(); + return true; } @@ -171,6 +181,54 @@ class QueueService { _logQueues(message: "after skipping backwards"); + // await pushQueueToExternalQueues(); + + return true; + + } + + Future _applySkipToTrackByOffset(int offset) async { + + return true; + //TODO handle "Next Up" queue + + // update internal queues + + bool addCurrentTrackToPreviousTracks = true; + + _queueServiceLogger.finer("Loop mode: $loopMode"); + + if (loopMode == LoopMode.one) { + _audioHandler.seek(Duration.zero); + _queueServiceLogger.finer("Looping current track: '${_currentTrack!.item.title}'"); + + //TODO update playback history + + return false; // perform the skip + } if ( + (_queue.length + _queueNextUp.length == 0) + && loopMode == LoopMode.all + ) { + + await _applyLoopQueue(); + addCurrentTrackToPreviousTracks = false; + + } else if (_queue.isEmpty && loopMode == LoopMode.none) { + _queueServiceLogger.info("Cannot skip ahead, no tracks in queue"); + //TODO show snackbar + return false; + } + + if (addCurrentTrackToPreviousTracks) { + _queuePreviousTracks.add(_currentTrack!); + } + _currentTrack = _queue.removeAt(0); + _currentTrackStream.add(_currentTrack!); + + _logQueues(message: "after skipping forward"); + + // await pushQueueToExternalQueues(); + return true; } @@ -305,6 +363,12 @@ class QueueService { _audioHandler.queue.add(_queue.map((e) => e.item).toList()); + _queueStream.add(QueueInfo( + previousTracks: _queuePreviousTracks, + currentTrack: _currentTrack!, + queue: _queue, + )); + _audioHandler.mediaItem.add(_currentTrack!.item); _audioHandler.play(); @@ -378,8 +442,22 @@ class QueueService { } } - Stream getCurrentTrackStream() { - return _currentTrackStream.stream.asBroadcastStream(); + QueueInfo getQueue() { + + return QueueInfo( + previousTracks: _queuePreviousTracks, + currentTrack: _currentTrack!, + queue: _queue, + ); + + } + + BehaviorSubject getQueueStream() { + return _queueStream; + } + + BehaviorSubject getCurrentTrackStream() { + return _currentTrackStream; } QueueItem getCurrentTrack() { @@ -554,19 +632,25 @@ class QueueService { await pushQueueToExternalQueues(); - _currentTrackStream.add(_currentTrack!); - } Future pushQueueToExternalQueues() async { - _audioHandler.queue.add(_queue.map((e) => e.item).toList()); - - // clear queue after the current track - // do this first so that the current track index stays the same - _queueAudioSource.removeRange(_queueAudioSourceIndex+1, _queueAudioSource.length); - // clear queue before the current track - _queueAudioSource.removeRange(0, _queueAudioSourceIndex); + _audioHandler.queue.add(_queuePreviousTracks.followedBy([_currentTrack!]).followedBy(_queue).map((e) => e.item).toList()); + _currentTrackStream.add(_currentTrack!); + _queueStream.add(QueueInfo( + previousTracks: _queuePreviousTracks, + currentTrack: _currentTrack!, + queue: _queue, + )); + + if (_queueAudioSource.length > 1) { + // clear queue after the current track + // do this first so that the current track index stays the same + _queueAudioSource.removeRange(_queueAudioSourceIndex+1, _queueAudioSource.length); + // clear queue before the current track + _queueAudioSource.removeRange(0, _queueAudioSourceIndex); + } // add items to queue List previousAudioSources = []; From 272e28bbeef1ad46f05859aef340f25253a2a4f0 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Mon, 22 May 2023 01:15:27 +0200 Subject: [PATCH 011/130] use new queue for instant mixes --- lib/models/finamp_models.dart | 4 +++- lib/services/audio_service_helper.dart | 30 +++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 7eb4a1937..3cfe82244 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -560,7 +560,9 @@ enum QueueItemType { album(name: "Album"), playlist(name: "Playlist"), - mix(name: "Instant Mix"), + itemMix(name: "Song Mix"), + artistMix(name: "Artist Mix"), + albumMix(name: "Album Mix"), favorites(name: "Your Likes"), songs(name: "All Songs"), filteredList(name: "Songs"), diff --git a/lib/services/audio_service_helper.dart b/lib/services/audio_service_helper.dart index ab8576975..8b5f94ac5 100644 --- a/lib/services/audio_service_helper.dart +++ b/lib/services/audio_service_helper.dart @@ -135,7 +135,15 @@ class AudioServiceHelper { try { items = await _jellyfinApiHelper.getInstantMix(item); if (items != null) { - await replaceQueueWithItem(itemList: items, shuffle: false); + // await replaceQueueWithItem(itemList: items, shuffle: false); + await _queueService.startPlayback( + items: items, + source: QueueItemSource( + type: QueueItemType.itemMix, + name: item.name != null ? "${item.name} - Mix" : "", + id: item.id + ) + ); } } catch (e) { audioServiceHelperLogger.severe(e); @@ -150,7 +158,15 @@ class AudioServiceHelper { try { items = await _jellyfinApiHelper.getArtistMix(artistIds); if (items != null) { - await replaceQueueWithItem(itemList: items, shuffle: false); + // await replaceQueueWithItem(itemList: items, shuffle: false); + await _queueService.startPlayback( + items: items, + source: QueueItemSource( + type: QueueItemType.artistMix, + name: artistIds.first, + id: artistIds.first, + ) + ); } } catch (e) { audioServiceHelperLogger.severe(e); @@ -165,7 +181,15 @@ class AudioServiceHelper { try { items = await _jellyfinApiHelper.getAlbumMix(albumIds); if (items != null) { - await replaceQueueWithItem(itemList: items, shuffle: false); + // await replaceQueueWithItem(itemList: items, shuffle: false); + await _queueService.startPlayback( + items: items, + source: QueueItemSource( + type: QueueItemType.albumMix, + name: albumIds.first, + id: albumIds.first, + ) + ); } } catch (e) { audioServiceHelperLogger.severe(e); From 03f4f24cd0cac40d09a95b2e6f7a2cc64f93cd59 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Mon, 22 May 2023 01:17:54 +0200 Subject: [PATCH 012/130] add sleep timer to player screen menu --- .../PlayerScreen/player_buttons_more.dart | 9 ++++- .../PlayerScreen/sleep_timer_button.dart | 37 ++++++++++--------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/lib/components/PlayerScreen/player_buttons_more.dart b/lib/components/PlayerScreen/player_buttons_more.dart index 06b35f18d..41d0c510b 100644 --- a/lib/components/PlayerScreen/player_buttons_more.dart +++ b/lib/components/PlayerScreen/player_buttons_more.dart @@ -1,4 +1,5 @@ import 'package:audio_service/audio_service.dart'; +import 'package:finamp/components/PlayerScreen/sleep_timer_button.dart'; import 'package:finamp/models/jellyfin_models.dart'; import 'package:finamp/screens/add_to_playlist_screen.dart'; import 'package:finamp/services/music_player_background_task.dart'; @@ -9,7 +10,7 @@ import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; -enum PlayerButtonsMoreItems { shuffle, repeat, addToPlaylist } +enum PlayerButtonsMoreItems { shuffle, repeat, addToPlaylist, sleepTimer } class PlayerButtonsMore extends ConsumerWidget { final audioHandler = GetIt.instance(); @@ -60,7 +61,11 @@ class PlayerButtonsMore extends ConsumerWidget { title: Text(AppLocalizations.of(context)! .addToPlaylistTooltip)); } - })) + })), + const PopupMenuItem( + value: PlayerButtonsMoreItems.sleepTimer, + child: SleepTimerButton(), + ), ], ), ); diff --git a/lib/components/PlayerScreen/sleep_timer_button.dart b/lib/components/PlayerScreen/sleep_timer_button.dart index a2178014a..fefd82dcd 100644 --- a/lib/components/PlayerScreen/sleep_timer_button.dart +++ b/lib/components/PlayerScreen/sleep_timer_button.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get_it/get_it.dart'; import '../../services/music_player_background_task.dart'; @@ -18,24 +19,24 @@ class SleepTimerButton extends StatelessWidget { return ValueListenableBuilder( valueListenable: audioHandler.sleepTimer, builder: (context, value, child) { - return IconButton( - icon: value == null - ? const Icon(Icons.mode_night_outlined) - : const Icon(Icons.mode_night), - tooltip: AppLocalizations.of(context)!.sleepTimerTooltip, - onPressed: () async { - if (value != null) { - showDialog( - context: context, - builder: (context) => const SleepTimerCancelDialog(), - ); - } else { - await showDialog( - context: context, - builder: (context) => const SleepTimerDialog(), - ); - } - }); + return ListTile( + leading: const Icon(TablerIcons.hourglass_high), + onTap: () async { + if (value != null) { + showDialog( + context: context, + builder: (context) => const SleepTimerCancelDialog(), + ); + } else { + await showDialog( + context: context, + builder: (context) => const SleepTimerDialog(), + ); + } + }, + title: Text(AppLocalizations.of(context)! + .sleepTimerTooltip) + ); }, ); } From b7e8fdf94374456462e9065f727ddee2be139b32 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 23 May 2023 00:49:29 +0200 Subject: [PATCH 013/130] reimplement queue list bottom sheet with CustomScrollView - allows efficient and flexible combination of multiple lists and elements into a single scrolling view --- lib/components/PlayerScreen/queue_list.dart | 300 ++++++++++---------- lib/services/queue_service.dart | 21 +- 2 files changed, 162 insertions(+), 159 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 1be0ba9fb..97a9b9af0 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -2,6 +2,7 @@ import 'package:audio_service/audio_service.dart'; import 'package:finamp/models/finamp_models.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get_it/get_it.dart'; import 'package:rxdart/rxdart.dart'; @@ -57,160 +58,158 @@ class _QueueListState extends State { _currentTrack ??= snapshot.data!.queueInfo.currentTrack; _queue ??= snapshot.data!.queueInfo.queue; - return Stack( - children: [ - PrimaryScrollController( - controller: widget.scrollController, - child: Padding( - padding: const EdgeInsets.only(top: 24), - child: ReorderableListView.builder( - itemCount: _previousTracks?.length ?? 0, - onReorder: (oldIndex, newIndex) async { - setState(() { - // _previousTracks?.insert(newIndex, _previousTracks![oldIndex]); - // _previousTracks?.removeAt(oldIndex); - int? smallerThanNewIndex; - if (oldIndex < newIndex) { - // When we're moving an item backwards, we need to reduce - // newIndex by 1 to account for there being a new item added - // before newIndex. - smallerThanNewIndex = newIndex - 1; - } - final item = _previousTracks?.removeAt(oldIndex); - _previousTracks?.insert(smallerThanNewIndex ?? newIndex, item!); - }); - await _audioHandler.reorderQueue(oldIndex, newIndex); + return CustomScrollView( + controller: widget.scrollController, + slivers: [ + // const SliverPadding(padding: EdgeInsets.only(top: 0)), + SliverReorderableList( + itemCount: _previousTracks?.length ?? 0, + onReorder: (oldIndex, newIndex) async { + setState(() { + // _previousTracks?.insert(newIndex, _previousTracks![oldIndex]); + // _previousTracks?.removeAt(oldIndex); + int? smallerThanNewIndex; + if (oldIndex < newIndex) { + // When we're moving an item backwards, we need to reduce + // newIndex by 1 to account for there being a new item added + // before newIndex. + smallerThanNewIndex = newIndex - 1; + } + final item = _previousTracks?.removeAt(oldIndex); + _previousTracks?.insert(smallerThanNewIndex ?? newIndex, item!); + }); + await _audioHandler.reorderQueue(oldIndex, newIndex); + }, + itemBuilder: (context, index) { + final actualIndex = + _audioHandler.playbackState.valueOrNull?.shuffleMode == + AudioServiceShuffleMode.all + ? _audioHandler.shuffleIndices![index] + : index; + return Dismissible( + key: ValueKey(_previousTracks![actualIndex].item.id), + direction: + FinampSettingsHelper.finampSettings.disableGesture + ? DismissDirection.none + : DismissDirection.horizontal, + onDismissed: (direction) async { + await _audioHandler.removeQueueItemAt(actualIndex); }, - itemBuilder: (context, index) { - final actualIndex = - _audioHandler.playbackState.valueOrNull?.shuffleMode == - AudioServiceShuffleMode.all - ? _audioHandler.shuffleIndices![index] - : index; - return Dismissible( - key: ValueKey(_previousTracks![actualIndex].item.id), - direction: - FinampSettingsHelper.finampSettings.disableGesture - ? DismissDirection.none - : DismissDirection.horizontal, - onDismissed: (direction) async { - await _audioHandler.removeQueueItemAt(actualIndex); - }, - child: ListTile( - leading: AlbumImage( - item: _previousTracks?[actualIndex].item - .extras?["itemJson"] == - null - ? null - : jellyfin_models.BaseItemDto.fromJson(_previousTracks?[actualIndex].item.extras?["itemJson"]), - ), - title: Text( - _previousTracks?[actualIndex].item.title ?? - AppLocalizations.of(context)!.unknownName, - style: _currentTrack == - _previousTracks?[actualIndex] - ? TextStyle( - color: - Theme.of(context).colorScheme.secondary) - : null), - subtitle: Text(processArtist( - _previousTracks?[actualIndex].item.artist, - context)), - onTap: () async => - await _audioHandler.skipToIndex(actualIndex), - ), - ); - }, - ), - ) + child: ListTile( + leading: AlbumImage( + item: _previousTracks?[actualIndex].item + .extras?["itemJson"] == + null + ? null + : jellyfin_models.BaseItemDto.fromJson(_previousTracks?[actualIndex].item.extras?["itemJson"]), + ), + title: Text( + _previousTracks?[actualIndex].item.title ?? + AppLocalizations.of(context)!.unknownName, + style: _currentTrack == + _previousTracks?[actualIndex] + ? TextStyle( + color: + Theme.of(context).colorScheme.secondary) + : null), + subtitle: Text(processArtist( + _previousTracks?[actualIndex].item.artist, + context)), + onTap: () async => + await _audioHandler.skipToIndex(actualIndex), + ), + ); + }, ), - const Padding(padding: EdgeInsets.symmetric(vertical: 20)), - ListTile( - leading: AlbumImage( - item: _currentTrack!.item - .extras?["itemJson"] == - null - ? null - : jellyfin_models.BaseItemDto.fromJson(_currentTrack!.item.extras?["itemJson"]), - ), - title: Text( - _currentTrack!.item.title ?? - AppLocalizations.of(context)!.unknownName, - style: TextStyle( - color: - Theme.of(context).colorScheme.secondary) + SliverAppBar( + pinned: true, + collapsedHeight: 70.0, + expandedHeight: 70.0, + leading: const Padding( + padding: EdgeInsets.zero, ), - subtitle: Text(processArtist( - _currentTrack!.item.artist, - context)), - onTap: () async => - snapshot.data!.mediaState.playbackState.playing ? await _audioHandler.pause() : await _audioHandler.play(), + flexibleSpace: Flexible( + child: ListTile( + leading: AlbumImage( + item: _currentTrack!.item + .extras?["itemJson"] == + null + ? null + : jellyfin_models.BaseItemDto.fromJson(_currentTrack!.item.extras?["itemJson"]), + ), + title: Text( + _currentTrack!.item.title ?? + AppLocalizations.of(context)!.unknownName, + style: TextStyle( + color: + Theme.of(context).colorScheme.secondary) + ), + subtitle: Text(processArtist( + _currentTrack!.item.artist, + context)), + onTap: () async => + snapshot.data!.mediaState.playbackState.playing ? await _audioHandler.pause() : await _audioHandler.play(), + ), + ) ), - const Padding(padding: EdgeInsets.symmetric(vertical: 20)), - PrimaryScrollController( - controller: widget.scrollController, - child: Padding( - padding: const EdgeInsets.only(top: 24), - child: ReorderableListView.builder( - itemCount: _queue?.length ?? 0, - onReorder: (oldIndex, newIndex) async { - setState(() { - // _queue?.insert(newIndex, _queue![oldIndex]); - // _queue?.removeAt(oldIndex); - int? smallerThanNewIndex; - if (oldIndex < newIndex) { - // When we're moving an item backwards, we need to reduce - // newIndex by 1 to account for there being a new item added - // before newIndex. - smallerThanNewIndex = newIndex - 1; - } - final item = _queue?.removeAt(oldIndex); - _queue?.insert(smallerThanNewIndex ?? newIndex, item!); - }); - await _audioHandler.reorderQueue(oldIndex, newIndex); + SliverReorderableList( + itemCount: _queue?.length ?? 0, + onReorder: (oldIndex, newIndex) async { + setState(() { + // _queue?.insert(newIndex, _queue![oldIndex]); + // _queue?.removeAt(oldIndex); + int? smallerThanNewIndex; + if (oldIndex < newIndex) { + // When we're moving an item backwards, we need to reduce + // newIndex by 1 to account for there being a new item added + // before newIndex. + smallerThanNewIndex = newIndex - 1; + } + final item = _queue?.removeAt(oldIndex); + _queue?.insert(smallerThanNewIndex ?? newIndex, item!); + }); + await _audioHandler.reorderQueue(oldIndex, newIndex); + }, + itemBuilder: (context, index) { + final actualIndex = + _audioHandler.playbackState.valueOrNull?.shuffleMode == + AudioServiceShuffleMode.all + ? _audioHandler.shuffleIndices![index] + : index; + return Dismissible( + key: ValueKey(_queue![actualIndex].item.id), + direction: + FinampSettingsHelper.finampSettings.disableGesture + ? DismissDirection.none + : DismissDirection.horizontal, + onDismissed: (direction) async { + await _audioHandler.removeQueueItemAt(actualIndex); }, - itemBuilder: (context, index) { - final actualIndex = - _audioHandler.playbackState.valueOrNull?.shuffleMode == - AudioServiceShuffleMode.all - ? _audioHandler.shuffleIndices![index] - : index; - return Dismissible( - key: ValueKey(_queue![actualIndex].item.id), - direction: - FinampSettingsHelper.finampSettings.disableGesture - ? DismissDirection.none - : DismissDirection.horizontal, - onDismissed: (direction) async { - await _audioHandler.removeQueueItemAt(actualIndex); - }, - child: ListTile( - leading: AlbumImage( - item: _queue?[actualIndex].item - .extras?["itemJson"] == - null - ? null - : jellyfin_models.BaseItemDto.fromJson(_queue?[actualIndex].item.extras?["itemJson"]), - ), - title: Text( - _queue?[actualIndex].item.title ?? - AppLocalizations.of(context)!.unknownName, - style: _currentTrack == - _queue?[actualIndex] - ? TextStyle( - color: - Theme.of(context).colorScheme.secondary) - : null), - subtitle: Text(processArtist( - _queue?[actualIndex].item.artist, - context)), - onTap: () async => - await _audioHandler.skipToIndex(actualIndex), - ), - ); - }, - ) - ) + child: ListTile( + leading: AlbumImage( + item: _queue?[actualIndex].item + .extras?["itemJson"] == + null + ? null + : jellyfin_models.BaseItemDto.fromJson(_queue?[actualIndex].item.extras?["itemJson"]), + ), + title: Text( + _queue?[actualIndex].item.title ?? + AppLocalizations.of(context)!.unknownName, + style: _currentTrack == + _queue?[actualIndex] + ? TextStyle( + color: + Theme.of(context).colorScheme.secondary) + : null), + subtitle: Text(processArtist( + _queue?[actualIndex].item.artist, + context)), + onTap: () async => + await _audioHandler.skipToIndex(actualIndex), + ), + ); + }, ), ], ); @@ -227,6 +226,9 @@ class _QueueListState extends State { Future showQueueBottomSheet(BuildContext context) { return showModalBottomSheet( + showDragHandle: true, + useSafeArea: true, + enableDrag: true, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24.0)), diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 650fb5b77..75442f6d4 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -133,6 +133,8 @@ class QueueService { _logQueues(message: "after skipping forward"); + _queueStream.add(getQueue()); + // await pushQueueToExternalQueues(); return true; @@ -172,6 +174,11 @@ class QueueService { return false; } } + + if (_audioHandler.getPlayPositionInSeconds() > 5) { + _audioHandler.seek(Duration.zero); + return false; + } if (addCurrentTrackToQueue) { _queue.insert(0, _currentTrack!); @@ -181,6 +188,8 @@ class QueueService { _logQueues(message: "after skipping backwards"); + _queueStream.add(getQueue()); + // await pushQueueToExternalQueues(); return true; @@ -363,11 +372,7 @@ class QueueService { _audioHandler.queue.add(_queue.map((e) => e.item).toList()); - _queueStream.add(QueueInfo( - previousTracks: _queuePreviousTracks, - currentTrack: _currentTrack!, - queue: _queue, - )); + _queueStream.add(getQueue()); _audioHandler.mediaItem.add(_currentTrack!.item); _audioHandler.play(); @@ -638,11 +643,7 @@ class QueueService { _audioHandler.queue.add(_queuePreviousTracks.followedBy([_currentTrack!]).followedBy(_queue).map((e) => e.item).toList()); _currentTrackStream.add(_currentTrack!); - _queueStream.add(QueueInfo( - previousTracks: _queuePreviousTracks, - currentTrack: _currentTrack!, - queue: _queue, - )); + _queueStream.add(getQueue()); if (_queueAudioSource.length > 1) { // clear queue after the current track From 731fb9e64880baa7b33d658e326a9ba077f39d09 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 23 May 2023 01:44:34 +0200 Subject: [PATCH 014/130] working track selection in queue (skipByOffset) - can skip to track in both recent track and the regular queue by tapping on them --- lib/components/PlayerScreen/queue_list.dart | 9 ++-- .../music_player_background_task.dart | 22 +++++++- lib/services/queue_service.dart | 52 ++++++++++--------- 3 files changed, 55 insertions(+), 28 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 97a9b9af0..ca2f586be 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -62,6 +62,7 @@ class _QueueListState extends State { controller: widget.scrollController, slivers: [ // const SliverPadding(padding: EdgeInsets.only(top: 0)), + // Previous Tracks SliverReorderableList( itemCount: _previousTracks?.length ?? 0, onReorder: (oldIndex, newIndex) async { @@ -116,11 +117,12 @@ class _QueueListState extends State { _previousTracks?[actualIndex].item.artist, context)), onTap: () async => - await _audioHandler.skipToIndex(actualIndex), + await _audioHandler.skipByOffset(-((_previousTracks?.length ?? 0) - index)), ), ); }, ), + // Current Track SliverAppBar( pinned: true, collapsedHeight: 70.0, @@ -138,7 +140,7 @@ class _QueueListState extends State { : jellyfin_models.BaseItemDto.fromJson(_currentTrack!.item.extras?["itemJson"]), ), title: Text( - _currentTrack!.item.title ?? + _currentTrack?.item.title ?? AppLocalizations.of(context)!.unknownName, style: TextStyle( color: @@ -152,6 +154,7 @@ class _QueueListState extends State { ), ) ), + // Queue SliverReorderableList( itemCount: _queue?.length ?? 0, onReorder: (oldIndex, newIndex) async { @@ -206,7 +209,7 @@ class _QueueListState extends State { _queue?[actualIndex].item.artist, context)), onTap: () async => - await _audioHandler.skipToIndex(actualIndex), + await _audioHandler.skipByOffset(index+1), ), ); }, diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index efe1b12f4..c48939a66 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -63,6 +63,7 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { Future Function()? _queueCallbackNextTrack; Future Function()? _queueCallbackPreviousTrack; + Future Function(int)? _queueCallbackSkipToIndexCallback; List? get shuffleIndices => _player.shuffleIndices; @@ -140,10 +141,12 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { void setQueueCallbacks({ required Future Function() nextTrackCallback, - required Future Function() previousTrackCallback + required Future Function() previousTrackCallback, + required Future Function(int) skipToIndexCallback }) { _queueCallbackNextTrack = nextTrackCallback; _queueCallbackPreviousTrack = previousTrackCallback; + _queueCallbackSkipToIndexCallback = skipToIndexCallback; } Stream getPlaybackEventStream() { @@ -370,6 +373,23 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } } + Future skipByOffset(int offset) async { + + _audioServiceBackgroundTaskLogger.fine("skipping by offset: $offset"); + + try { + + await _player.seek(Duration.zero, index: (_player.currentIndex ?? 0) + offset); + + if (_queueCallbackSkipToIndexCallback != null) { + await _queueCallbackSkipToIndexCallback!(offset); + } + } catch (e) { + _audioServiceBackgroundTaskLogger.severe(e); + return Future.error(e); + } + } + @override Future seek(Duration position) async { try { diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 75442f6d4..a7b3fae67 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -87,7 +87,11 @@ class QueueService { }); // register callbacks - _audioHandler.setQueueCallbacks(nextTrackCallback: _applyNextTrack, previousTrackCallback: _applyPreviousTrack); + _audioHandler.setQueueCallbacks( + nextTrackCallback: _applyNextTrack, + previousTrackCallback: _applyPreviousTrack, + skipToIndexCallback: _applySkipToTrackByOffset, + ); } @@ -198,32 +202,20 @@ class QueueService { Future _applySkipToTrackByOffset(int offset) async { - return true; //TODO handle "Next Up" queue - // update internal queues bool addCurrentTrackToPreviousTracks = true; - _queueServiceLogger.finer("Loop mode: $loopMode"); + _logQueues(message: "before skipping by offset $offset"); - if (loopMode == LoopMode.one) { - _audioHandler.seek(Duration.zero); - _queueServiceLogger.finer("Looping current track: '${_currentTrack!.item.title}'"); - - //TODO update playback history - - return false; // perform the skip - } if ( - (_queue.length + _queueNextUp.length == 0) - && loopMode == LoopMode.all + if (offset == 0) { + return false; + } else if ( + (offset > 0 && _queue.length < offset) || + (offset < 0 && _queuePreviousTracks.length < offset) ) { - - await _applyLoopQueue(); - addCurrentTrackToPreviousTracks = false; - - } else if (_queue.isEmpty && loopMode == LoopMode.none) { - _queueServiceLogger.info("Cannot skip ahead, no tracks in queue"); + _queueServiceLogger.info("Cannot skip to offset $offset, not enough tracks in queue"); //TODO show snackbar return false; } @@ -231,12 +223,24 @@ class QueueService { if (addCurrentTrackToPreviousTracks) { _queuePreviousTracks.add(_currentTrack!); } - _currentTrack = _queue.removeAt(0); - _currentTrackStream.add(_currentTrack!); - _logQueues(message: "after skipping forward"); + if (offset > 0) { + _queuePreviousTracks.addAll(_queue.sublist(0, offset-1)); + _queue.removeRange(0, offset-1); - // await pushQueueToExternalQueues(); + _currentTrack = _queue.removeAt(0); + _currentTrackStream.add(_currentTrack!); + } else { + _queue.insertAll(0, _queuePreviousTracks.sublist(_queuePreviousTracks.length + offset, _queuePreviousTracks.length)); + _queuePreviousTracks.removeRange(_queuePreviousTracks.length + offset, _queuePreviousTracks.length); + + _currentTrack = _queuePreviousTracks.removeLast(); + _currentTrackStream.add(_currentTrack!); + } + + _logQueues(message: "after skipping by offset $offset"); + + await pushQueueToExternalQueues(); return true; From f30f94c1db37e0d862014fcf409d48aa3fe25825 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Wed, 24 May 2023 21:51:27 +0200 Subject: [PATCH 015/130] scroll to current track when opening queue panel --- lib/components/PlayerScreen/queue_list.dart | 54 ++++++++++++--------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index ca2f586be..9ff12f8ca 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -16,12 +16,10 @@ import '../../services/queue_service.dart'; class _QueueListStreamState { _QueueListStreamState( - this.queue, this.mediaState, this.queueInfo, ); - final List? queue; final MediaState mediaState; final QueueInfo queueInfo; } @@ -46,9 +44,9 @@ class _QueueListState extends State { Widget build(BuildContext context) { return StreamBuilder<_QueueListStreamState>( // stream: AudioService.queueStream, - stream: Rx.combineLatest3?, MediaState, QueueInfo, - _QueueListStreamState>(_audioHandler.queue, mediaStateStream, _queueService.getQueueStream(), - (a, b, c) => _QueueListStreamState(a, b, c)), + stream: Rx.combineLatest2(mediaStateStream, _queueService.getQueueStream(), + (a, b) => _QueueListStreamState(a, b)), // stream: _queueService.getQueueStream(), builder: (context, snapshot) { @@ -58,6 +56,25 @@ class _QueueListState extends State { _currentTrack ??= snapshot.data!.queueInfo.currentTrack; _queue ??= snapshot.data!.queueInfo.queue; + final GlobalKey currentTrackKey = GlobalKey(debugLabel: "currentTrack"); + + void scrollToCurrentTrack() { + widget.scrollController.animateTo(((_previousTracks?.length ?? 0) * 60 + 20).toDouble(), + duration: const Duration(milliseconds: 200), + curve: Curves.linear + ); + // final targetContext = currentTrackKey.currentContext; + // if (targetContext != null) { + // Scrollable.ensureVisible(targetContext!, + // duration: const Duration(milliseconds: 200), + // curve: Curves.linear + // ); + // } + } + // scroll to current track after sheet has been opened + WidgetsBinding.instance + .addPostFrameCallback((_) => scrollToCurrentTrack()); + return CustomScrollView( controller: widget.scrollController, slivers: [ @@ -82,19 +99,16 @@ class _QueueListState extends State { await _audioHandler.reorderQueue(oldIndex, newIndex); }, itemBuilder: (context, index) { - final actualIndex = - _audioHandler.playbackState.valueOrNull?.shuffleMode == - AudioServiceShuffleMode.all - ? _audioHandler.shuffleIndices![index] - : index; + final actualIndex = index; return Dismissible( - key: ValueKey(_previousTracks![actualIndex].item.id), + key: ValueKey(_previousTracks![actualIndex].item.id + actualIndex.toString()), direction: FinampSettingsHelper.finampSettings.disableGesture ? DismissDirection.none : DismissDirection.horizontal, onDismissed: (direction) async { - await _audioHandler.removeQueueItemAt(actualIndex); + //TODO + // await _audioHandler.removeQueueItemAt(actualIndex); }, child: ListTile( leading: AlbumImage( @@ -124,14 +138,14 @@ class _QueueListState extends State { ), // Current Track SliverAppBar( + key: currentTrackKey, pinned: true, collapsedHeight: 70.0, expandedHeight: 70.0, leading: const Padding( padding: EdgeInsets.zero, ), - flexibleSpace: Flexible( - child: ListTile( + flexibleSpace: ListTile( leading: AlbumImage( item: _currentTrack!.item .extras?["itemJson"] == @@ -151,7 +165,6 @@ class _QueueListState extends State { context)), onTap: () async => snapshot.data!.mediaState.playbackState.playing ? await _audioHandler.pause() : await _audioHandler.play(), - ), ) ), // Queue @@ -174,19 +187,16 @@ class _QueueListState extends State { await _audioHandler.reorderQueue(oldIndex, newIndex); }, itemBuilder: (context, index) { - final actualIndex = - _audioHandler.playbackState.valueOrNull?.shuffleMode == - AudioServiceShuffleMode.all - ? _audioHandler.shuffleIndices![index] - : index; + final actualIndex = index; return Dismissible( - key: ValueKey(_queue![actualIndex].item.id), + key: ValueKey(_queue![actualIndex].item.id + actualIndex.toString()), direction: FinampSettingsHelper.finampSettings.disableGesture ? DismissDirection.none : DismissDirection.horizontal, onDismissed: (direction) async { - await _audioHandler.removeQueueItemAt(actualIndex); + //TODO + // await _audioHandler.removeQueueItemAt(actualIndex); }, child: ListTile( leading: AlbumImage( From 81c4f144a9a5411930f32c4f0cfe239355a3fbf2 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Wed, 24 May 2023 23:20:11 +0200 Subject: [PATCH 016/130] bugfixes --- lib/components/PlayerScreen/queue_list.dart | 2 +- lib/services/queue_service.dart | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 9ff12f8ca..6695b9695 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -131,7 +131,7 @@ class _QueueListState extends State { _previousTracks?[actualIndex].item.artist, context)), onTap: () async => - await _audioHandler.skipByOffset(-((_previousTracks?.length ?? 0) - index)), + await _audioHandler.skipByOffset(-((_previousTracks?.length ?? 0) - index + 1)), ), ); }, diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index a7b3fae67..455a42fbc 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -122,6 +122,7 @@ class QueueService { await _applyLoopQueue(); addCurrentTrackToPreviousTracks = false; + // _queueAudioSourceIndex = -1; // so that incrementing the index below will set it to 0 } else if (_queue.isEmpty && loopMode == LoopMode.none) { _queueServiceLogger.info("Cannot skip ahead, no tracks in queue"); @@ -132,6 +133,9 @@ class QueueService { if (addCurrentTrackToPreviousTracks) { _queuePreviousTracks.add(_currentTrack!); } + + await pushQueueToExternalQueues(); + _currentTrack = _queue.removeAt(0); _currentTrackStream.add(_currentTrack!); @@ -139,7 +143,7 @@ class QueueService { _queueStream.add(getQueue()); - // await pushQueueToExternalQueues(); + _queueAudioSourceIndex++; // increment external queue index so we can detect that the change has already been handled once return true; @@ -187,9 +191,14 @@ class QueueService { if (addCurrentTrackToQueue) { _queue.insert(0, _currentTrack!); } + + await pushQueueToExternalQueues(); + _currentTrack = _queuePreviousTracks.removeLast(); _currentTrackStream.add(_currentTrack!); + // _queueAudioSourceIndex--; // increment external queue index so we can detect that the change has already been handled once + _logQueues(message: "after skipping backwards"); _queueStream.add(getQueue()); @@ -365,7 +374,7 @@ class QueueService { _queueAudioSourceIndex = 0; _audioHandler.setNextInitialIndex(_queueAudioSourceIndex); - _queueAudioSource.clear(); + _queueAudioSource = ConcatenatingAudioSource(children: []); await _queueAudioSource.add(await _mediaItemToAudioSource(_currentTrack!.item)); for (final queueItem in _queue) { @@ -484,7 +493,7 @@ class QueueService { // handle enabling loop all when the queue is empty if (mode == LoopMode.all && (_queue.length + _queueNextUp.length == 0)) { - _applyLoopQueue(); + // _applyLoopQueue(); } else if (mode != LoopMode.all) { // find current track in `_order` and set the queue to the items after it int currentTrackIndex = _order.items.indexOf(_currentTrack!); @@ -500,7 +509,7 @@ class QueueService { _logQueues(message: "after looping"); // update external queues - pushQueueToExternalQueues(); + // pushQueueToExternalQueues(); } } @@ -555,7 +564,7 @@ class QueueService { _logQueues(message: "after looping"); // update external queues - await pushQueueToExternalQueues(); + // await pushQueueToExternalQueues(); _queueServiceLogger.info("Looped queue, added ${_order.items.length} items"); @@ -659,7 +668,7 @@ class QueueService { // add items to queue List previousAudioSources = []; - for (QueueItem queueItem in _queuePreviousTracks) { + for (QueueItem queueItem in _queuePreviousTracks.toList()) { previousAudioSources.add(await _mediaItemToAudioSource(queueItem.item)); } await _queueAudioSource.insertAll(0, previousAudioSources); From eddbc5eaf3a821d573351b808bec21683ebc627b Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 25 May 2023 00:11:43 +0200 Subject: [PATCH 017/130] small improvements --- lib/components/PlayerScreen/queue_list.dart | 2 +- lib/services/queue_service.dart | 40 +++++++++++++++++---- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 6695b9695..9ff12f8ca 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -131,7 +131,7 @@ class _QueueListState extends State { _previousTracks?[actualIndex].item.artist, context)), onTap: () async => - await _audioHandler.skipByOffset(-((_previousTracks?.length ?? 0) - index + 1)), + await _audioHandler.skipByOffset(-((_previousTracks?.length ?? 0) - index)), ), ); }, diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 455a42fbc..16f4aee6b 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -84,6 +84,30 @@ class QueueService { _queueAudioSourceIndex = event.currentIndex ?? 0; + // MediaItem currentItem = (_queueAudioSource.sequence[_queueAudioSourceIndex].tag as MediaItem); + // if (currentItem.id != _currentTrack?.item.id) { + // // look through previous tracks and queue to find currentItem + // int offset = 0; + // for (int i = 0; i < _queuePreviousTracks.length; i++) { + // if (currentItem.id == _queuePreviousTracks[i].item.id) { + // offset = - (_queuePreviousTracks.length - i); + // break; + // } + // } + // for (int i = 0; i < _queue.length; i++) { + // if (currentItem.id == _queue[i].item.id) { + // offset = i + 1; + // break; + // } + // } + + // _queueServiceLogger.finer("Offset to current track was $offset"); + + // await _applySkipToTrackByOffset(offset, updateExternalQueues: false); + // } else { + // _queueServiceLogger.finer("Current track is correct"); + // } + }); // register callbacks @@ -130,12 +154,11 @@ class QueueService { return false; } + await pushQueueToExternalQueues(); if (addCurrentTrackToPreviousTracks) { _queuePreviousTracks.add(_currentTrack!); } - await pushQueueToExternalQueues(); - _currentTrack = _queue.removeAt(0); _currentTrackStream.add(_currentTrack!); @@ -143,7 +166,7 @@ class QueueService { _queueStream.add(getQueue()); - _queueAudioSourceIndex++; // increment external queue index so we can detect that the change has already been handled once + // _queueAudioSourceIndex++; // increment external queue index so we can detect that the change has already been handled once return true; @@ -188,11 +211,10 @@ class QueueService { return false; } + await pushQueueToExternalQueues(); if (addCurrentTrackToQueue) { _queue.insert(0, _currentTrack!); } - - await pushQueueToExternalQueues(); _currentTrack = _queuePreviousTracks.removeLast(); _currentTrackStream.add(_currentTrack!); @@ -209,7 +231,9 @@ class QueueService { } - Future _applySkipToTrackByOffset(int offset) async { + Future _applySkipToTrackByOffset(int offset, { + bool updateExternalQueues = true, + }) async { //TODO handle "Next Up" queue @@ -249,7 +273,9 @@ class QueueService { _logQueues(message: "after skipping by offset $offset"); - await pushQueueToExternalQueues(); + if (updateExternalQueues) { + await pushQueueToExternalQueues(); + } return true; From 43cb0e2023ffc67f380ee229f55c80bb4567dc64 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 25 May 2023 00:23:30 +0200 Subject: [PATCH 018/130] tried combining current track detection with proper skipping in queue - some inconsistencies still happen, but it mostly works --- lib/services/queue_service.dart | 48 +++++++++++++++++---------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 16f4aee6b..15fa33204 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -84,29 +84,29 @@ class QueueService { _queueAudioSourceIndex = event.currentIndex ?? 0; - // MediaItem currentItem = (_queueAudioSource.sequence[_queueAudioSourceIndex].tag as MediaItem); - // if (currentItem.id != _currentTrack?.item.id) { - // // look through previous tracks and queue to find currentItem - // int offset = 0; - // for (int i = 0; i < _queuePreviousTracks.length; i++) { - // if (currentItem.id == _queuePreviousTracks[i].item.id) { - // offset = - (_queuePreviousTracks.length - i); - // break; - // } - // } - // for (int i = 0; i < _queue.length; i++) { - // if (currentItem.id == _queue[i].item.id) { - // offset = i + 1; - // break; - // } - // } - - // _queueServiceLogger.finer("Offset to current track was $offset"); - - // await _applySkipToTrackByOffset(offset, updateExternalQueues: false); - // } else { - // _queueServiceLogger.finer("Current track is correct"); - // } + MediaItem currentItem = (_queueAudioSource.sequence[_queueAudioSourceIndex].tag as MediaItem); + if (currentItem.id != _currentTrack?.item.id) { + // look through previous tracks and queue to find currentItem + int offset = 0; + for (int i = 0; i < _queuePreviousTracks.length; i++) { + if (currentItem.id == _queuePreviousTracks[i].item.id) { + offset = - (_queuePreviousTracks.length - i); + break; + } + } + for (int i = 0; i < _queue.length; i++) { + if (currentItem.id == _queue[i].item.id) { + offset = i + 1; + break; + } + } + + _queueServiceLogger.finer("Offset to current track was $offset"); + + await _applySkipToTrackByOffset(offset, updateExternalQueues: false); + } else { + _queueServiceLogger.finer("Current track is correct"); + } }); @@ -277,6 +277,8 @@ class QueueService { await pushQueueToExternalQueues(); } + _queueAudioSourceIndex = _queueAudioSourceIndex + offset; + return true; } From 6c8cdbbaf9634209b53b8821f6410a5811835da2 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 25 May 2023 10:20:58 +0200 Subject: [PATCH 019/130] update queue stream on auto advance --- lib/services/queue_service.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 15fa33204..b15cb199d 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -275,6 +275,10 @@ class QueueService { if (updateExternalQueues) { await pushQueueToExternalQueues(); + } else { + _audioHandler.queue.add(_queuePreviousTracks.followedBy([_currentTrack!]).followedBy(_queue).map((e) => e.item).toList()); + _currentTrackStream.add(_currentTrack!); + _queueStream.add(getQueue()); } _queueAudioSourceIndex = _queueAudioSourceIndex + offset; From 36aa49dd574be03ee936693a451bee56bee234f2 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 25 May 2023 22:38:22 +0200 Subject: [PATCH 020/130] still a mess but somewhat usable --- ...bum_screen_content_flexible_space_bar.dart | 4 +- .../AlbumScreen/song_list_tile.dart | 4 +- lib/models/finamp_models.dart | 25 +- lib/services/audio_service_helper.dart | 8 +- .../music_player_background_task.dart | 10 +- lib/services/queue_service.dart | 343 ++++++++++++------ 6 files changed, 260 insertions(+), 134 deletions(-) diff --git a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart index b61052c9a..0bc70baca 100644 --- a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart +++ b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart @@ -33,7 +33,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { queueService.startPlayback( items: items, source: QueueItemSource( - type: QueueItemType.album, + type: QueueItemSourceType.album, name: album.name ?? "Somewhere", id: album.id, ) @@ -45,7 +45,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { queueService.startPlayback( items: items, source: QueueItemSource( - type: QueueItemType.album, + type: QueueItemSourceType.album, name: album.name ?? "Somewhere", id: album.id, ) diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index 8e5d2415e..c3b289f7d 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -295,7 +295,7 @@ class _SongListTileState extends State { switch (selection) { case SongListTileMenuItems.addToQueue: // await _audioServiceHelper.addQueueItem(widget.item); - await _queueService.addToQueue(widget.item, QueueItemSource(type: QueueItemType.unknown, name: "Queue", id: widget.parentId!)); + await _queueService.addToQueue(widget.item, QueueItemSource(type: QueueItemSourceType.unknown, name: "Queue", id: widget.parentId!)); if (!mounted) return; @@ -423,7 +423,7 @@ class _SongListTileState extends State { ), confirmDismiss: (direction) async { // await _audioServiceHelper.addQueueItem(widget.item); - await _queueService.addToQueue(widget.item, QueueItemSource(type: QueueItemType.unknown, name: "Queue", id: widget.parentId!)); + await _queueService.addToQueue(widget.item, QueueItemSource(type: QueueItemSourceType.unknown, name: "Queue", id: widget.parentId!)); if (!mounted) return false; diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 3cfe82244..e6004c7f9 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -556,7 +556,7 @@ class DownloadedImage { ); } -enum QueueItemType { +enum QueueItemSourceType { album(name: "Album"), playlist(name: "Playlist"), @@ -568,18 +568,27 @@ enum QueueItemType { filteredList(name: "Songs"), genre(name: "Genre"), artist(name: "Artist"), - upNext(name: ""), + nextUp(name: ""), formerUpNext(name: "Track added to Up Next"), downloads(name: ""), unknown(name: ""); - const QueueItemType({ + const QueueItemSourceType({ required this.name, }); final String name; } +enum QueueItemQueueType { + + previousTracks, + currentTrack, + nextUp, + queue; + +} + class QueueItemSource { QueueItemSource({ required this.type, @@ -588,7 +597,7 @@ class QueueItemSource { }); @HiveField(0) - QueueItemType type; + QueueItemSourceType type; @HiveField(1) String name; @@ -602,6 +611,7 @@ class QueueItem { QueueItem({ required this.item, required this.source, + this.type = QueueItemQueueType.queue, }); @HiveField(0) @@ -610,6 +620,9 @@ class QueueItem { @HiveField(1) QueueItemSource source; + @HiveField(2) + QueueItemQueueType type; + } class QueueOrder { @@ -640,6 +653,7 @@ class QueueInfo { QueueInfo({ required this.previousTracks, required this.currentTrack, + required this.nextUp, required this.queue, }); @@ -650,6 +664,9 @@ class QueueInfo { QueueItem currentTrack; @HiveField(2) + List nextUp; + + @HiveField(3) List queue; } diff --git a/lib/services/audio_service_helper.dart b/lib/services/audio_service_helper.dart index 8b5f94ac5..1b699ebb2 100644 --- a/lib/services/audio_service_helper.dart +++ b/lib/services/audio_service_helper.dart @@ -120,7 +120,7 @@ class AudioServiceHelper { await _queueService.startPlayback( items: items, source: QueueItemSource( - type: isFavourite ? QueueItemType.favorites : QueueItemType.songs, + type: isFavourite ? QueueItemSourceType.favorites : QueueItemSourceType.songs, name: "Shuffle All", id: "shuffleAll", ) @@ -139,7 +139,7 @@ class AudioServiceHelper { await _queueService.startPlayback( items: items, source: QueueItemSource( - type: QueueItemType.itemMix, + type: QueueItemSourceType.itemMix, name: item.name != null ? "${item.name} - Mix" : "", id: item.id ) @@ -162,7 +162,7 @@ class AudioServiceHelper { await _queueService.startPlayback( items: items, source: QueueItemSource( - type: QueueItemType.artistMix, + type: QueueItemSourceType.artistMix, name: artistIds.first, id: artistIds.first, ) @@ -185,7 +185,7 @@ class AudioServiceHelper { await _queueService.startPlayback( items: items, source: QueueItemSource( - type: QueueItemType.albumMix, + type: QueueItemSourceType.albumMix, name: albumIds.first, id: albumIds.first, ) diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index c48939a66..9fe7edac8 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -10,7 +10,7 @@ import 'package:just_audio/just_audio.dart'; import 'package:logging/logging.dart'; import '../models/finamp_models.dart'; -import '../models/jellyfin_models.dart'; +import '../models/jellyfin_models.dart' as jellyfin_models; import 'finamp_settings_helper.dart'; import 'finamp_user_helper.dart'; import 'jellyfin_api_helper.dart'; @@ -459,7 +459,7 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { /// Generates PlaybackProgressInfo from current player info. Returns null if /// _queue is empty. If an item is not supplied, the current queue index will /// be used. - PlaybackProgressInfo? generatePlaybackProgressInfo({ + jellyfin_models.PlaybackProgressInfo? generatePlaybackProgressInfo({ MediaItem? item, required bool includeNowPlayingQueue, }) { @@ -470,7 +470,7 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } try { - return PlaybackProgressInfo( + return jellyfin_models.PlaybackProgressInfo( itemId: item?.extras?["itemJson"]["Id"] ?? _getQueueItem(_player.currentIndex ?? 0).extras!["itemJson"]["Id"], isPaused: !_player.playing, @@ -591,11 +591,11 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } MediaItem _getQueueItem(int index) { - return _queueAudioSource.sequence[index].tag as MediaItem; + return (_queueAudioSource.sequence[index].tag as QueueItem).item; } List _queueFromSource() { - return _queueAudioSource.sequence.map((e) => e.tag as MediaItem).toList(); + return _queueAudioSource.sequence.map((e) => (e.tag as QueueItem).item).toList(); } /// Syncs the list of MediaItems (_queue) with the internal queue of the player. diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 15fa33204..8f3c45e86 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:just_audio/just_audio.dart'; import 'package:audio_service/audio_service.dart'; import 'package:get_it/get_it.dart'; @@ -38,12 +39,13 @@ class QueueService { LoopMode _loopMode = LoopMode.none; final _currentTrackStream = BehaviorSubject.seeded( - QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown)) + QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)) ); final _queueStream = BehaviorSubject.seeded(QueueInfo( previousTracks: [], - currentTrack: QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown)), + currentTrack: QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)), queue: [], + nextUp: [], )); // external queue state @@ -57,66 +59,95 @@ class QueueService { QueueService() { + _queueAudioSource = ConcatenatingAudioSource( + children: [], + useLazyPreparation: true, + shuffleOrder: NextUpShuffleOrder(queueService: this), + ); + _audioHandler.getPlaybackEventStream().listen((event) async { int indexDifference = (event.currentIndex ?? 0) - _queueAudioSourceIndex; _queueServiceLogger.finer("Play queue index changed, difference: $indexDifference"); - if (indexDifference == 0) { - //TODO figure out a way to detect looped tracks (loopMode == LoopMode.one) to add them to the playback history - return; - } else if (indexDifference.abs() == 1) { - // player skipped ahead/back - if (indexDifference > 0) { - // await _applyNextTrack(eventFromPlayer: true); - } else if (indexDifference < 0) { - //TODO properly handle rewinding instead of skipping back - // await _applyPreviousTrack(eventFromPlayer: true); - } - } else if (indexDifference.abs() > 1) { - // player skipped ahead/back by more than one track - //TODO implement - _queueServiceLogger.severe("Skipping ahead/back by more than one track not handled yet"); - // return; - + _queueAudioSourceIndex = event.currentIndex ?? 0; + + _queueFromConcatenatingAudioSource(indexDifference); + + }); + + // register callbacks + // _audioHandler.setQueueCallbacks( + // nextTrackCallback: _applyNextTrack, + // previousTrackCallback: _applyPreviousTrack, + // skipToIndexCallback: _applySkipToTrackByOffset, + // ); + + } + + void _queueFromConcatenatingAudioSource(int indexDifference) { + + //TODO handle shuffleIndices + List allTracks = _queueAudioSource.sequence.map((e) => e.tag as QueueItem).toList(); + + _queuePreviousTracks.clear(); + _queueNextUp.clear(); + _queue.clear(); + + // split the queue by old type + for (int i = 0; i < allTracks.length; i++) { + switch (allTracks[i].type) { + case QueueItemQueueType.previousTracks: + _queuePreviousTracks.add(allTracks[i]); + break; + case QueueItemQueueType.currentTrack: + _currentTrack = allTracks[i]; + break; + case QueueItemQueueType.nextUp: + _queueNextUp.add(allTracks[i]); + break; + default: + // queue + _queue.add(allTracks[i]); } + } - _queueAudioSourceIndex = event.currentIndex ?? 0; + // update types + if (indexDifference > 0) { - MediaItem currentItem = (_queueAudioSource.sequence[_queueAudioSourceIndex].tag as MediaItem); - if (currentItem.id != _currentTrack?.item.id) { - // look through previous tracks and queue to find currentItem - int offset = 0; - for (int i = 0; i < _queuePreviousTracks.length; i++) { - if (currentItem.id == _queuePreviousTracks[i].item.id) { - offset = - (_queuePreviousTracks.length - i); - break; - } - } - for (int i = 0; i < _queue.length; i++) { - if (currentItem.id == _queue[i].item.id) { - offset = i + 1; - break; - } - } + _currentTrack?.type = QueueItemQueueType.previousTracks; + for (int i = 0; i < indexDifference-1; i++) { + _queue[i].type = QueueItemQueueType.previousTracks; + } + _queue[indexDifference-1].type = QueueItemQueueType.currentTrack; - _queueServiceLogger.finer("Offset to current track was $offset"); + _currentTrack = _queue[indexDifference-1]; + _queuePreviousTracks.addAll(_queue.sublist(0, indexDifference)); + _queue.removeRange(0, indexDifference); - await _applySkipToTrackByOffset(offset, updateExternalQueues: false); - } else { - _queueServiceLogger.finer("Current track is correct"); + } else if (indexDifference < 0) { + + _currentTrack?.type = QueueItemQueueType.queue; + for (int i = _queuePreviousTracks.length-1; i > _queuePreviousTracks.length+indexDifference; i--) { + _queuePreviousTracks[i].type = QueueItemQueueType.queue; } + _queue[_queue.length + indexDifference].type = QueueItemQueueType.currentTrack; - }); + _currentTrack = _queue[_queue.length + indexDifference]; + _queue.insertAll(0, _queuePreviousTracks.sublist(_queuePreviousTracks.length + indexDifference)); + _queuePreviousTracks.removeRange(_queuePreviousTracks.length + indexDifference, _queuePreviousTracks.length); + } + + _queueStream.add(getQueue()); + if (_currentTrack != null) { + _currentTrackStream.add(_currentTrack!); + _audioHandler.mediaItem.add(_currentTrack!.item); + _audioHandler.queue.add(_queuePreviousTracks.followedBy([_currentTrack!]).followedBy(_queue).map((e) => e.item).toList()); + } + + _logQueues(message: "(current)"); - // register callbacks - _audioHandler.setQueueCallbacks( - nextTrackCallback: _applyNextTrack, - previousTrackCallback: _applyPreviousTrack, - skipToIndexCallback: _applySkipToTrackByOffset, - ); - } Future _applyNextTrack({bool eventFromPlayer = false}) async { @@ -346,6 +377,7 @@ class QueueService { _queue.clear(); // empty queue _queuePreviousTracks.clear(); + _queueNextUp.clear(); List newItems = []; List newLinearOrder = []; @@ -357,6 +389,7 @@ class QueueService { newItems.add(QueueItem( item: mediaItem, source: source, + type: i == 0 ? QueueItemQueueType.currentTrack : QueueItemQueueType.queue, )); newLinearOrder.add(i); } catch (e) { @@ -374,48 +407,23 @@ class QueueService { _queueServiceLogger.fine("Order items length: ${_order.items.length}"); - // log linear order and shuffled order - String linearOrderString = ""; - for (int itemIndex in _order.linearOrder) { - linearOrderString += "${newItems[itemIndex].item.title}, "; - } - String shuffledOrderString = ""; - for (int itemIndex in _order.shuffledOrder) { - shuffledOrderString += "${newItems[itemIndex].item.title}, "; - } - - _queueServiceLogger.finer("Linear order [${_order.linearOrder.length}]: $linearOrderString"); - _queueServiceLogger.finer("Shuffled order [${_order.shuffledOrder.length}]: $shuffledOrderString"); - - // add items to queue - for (int itemIndex in (playbackOrder == PlaybackOrder.linear ? _order.linearOrder : _order.shuffledOrder)) { - _queue.add(_order.items[itemIndex]); - } - - _currentTrack = _queue.removeAt(0); - _currentTrackStream.add(_currentTrack!); - _queueServiceLogger.info("Current track: '${_currentTrack!.item.title}'"); - - _logQueues(message: "after replacing whole queue"); // start playing first item in queue _queueAudioSourceIndex = 0; _audioHandler.setNextInitialIndex(_queueAudioSourceIndex); _queueAudioSource = ConcatenatingAudioSource(children: []); - await _queueAudioSource.add(await _mediaItemToAudioSource(_currentTrack!.item)); - for (final queueItem in _queue) { - await _queueAudioSource.add(await _mediaItemToAudioSource(queueItem.item)); + for (final queueItem in newItems) { + await _queueAudioSource.add(await _queueItemToAudioSource(queueItem)); } await _audioHandler.initializeAudioSource(_queueAudioSource); _audioHandler.queue.add(_queue.map((e) => e.item).toList()); - _queueStream.add(getQueue()); + // _queueStream.add(getQueue()); - _audioHandler.mediaItem.add(_currentTrack!.item); _audioHandler.play(); _audioHandler.nextInitialIndex = null; @@ -431,19 +439,20 @@ class QueueService { QueueItem queueItem = QueueItem( item: await _generateMediaItem(item), source: source, + type: QueueItemQueueType.queue, ); - _order.items.add(queueItem); - _order.linearOrder.add(_order.items.length - 1); - _order.shuffledOrder.add(_order.items.length - 1); //TODO maybe the item should be shuffled into the queue instead of placed at the end? depends on user preference + // _order.items.add(queueItem); + // _order.linearOrder.add(_order.items.length - 1); + // _order.shuffledOrder.add(_order.items.length - 1); //TODO maybe the item should be shuffled into the queue instead of placed at the end? depends on user preference - _queue.add(queueItem); + _queueAudioSource.add(await _queueItemToAudioSource(queueItem)); _queueServiceLogger.fine("Added '${queueItem.item.title}' to queue from '${source.name}' (${source.type})"); - if (_loopMode == LoopMode.all && _queue.length == 0) { - await pushQueueToExternalQueues(); - } + // if (_loopMode == LoopMode.all && _queue.length == 0) { + // await pushQueueToExternalQueues(); + // } } catch (e) { _queueServiceLogger.severe(e); @@ -455,11 +464,12 @@ class QueueService { try { QueueItem queueItem = QueueItem( item: await _generateMediaItem(item), - source: QueueItemSource(id: "up-next", name: "Up Next", type: QueueItemType.upNext), + source: QueueItemSource(id: "up-next", name: "Up Next", type: QueueItemSourceType.nextUp), + type: QueueItemQueueType.nextUp, ); // don't add to _order, because it wasn't added to the regular queue - _queueNextUp.insert(0, queueItem); + _queueAudioSource.insert(_queueAudioSourceIndex+1, await _queueItemToAudioSource(queueItem)); _queueServiceLogger.fine("Prepended '${queueItem.item.title}' to Next Up"); @@ -473,14 +483,18 @@ class QueueService { try { QueueItem queueItem = QueueItem( item: await _generateMediaItem(item), - source: QueueItemSource(id: "up-next", name: "Up Next", type: QueueItemType.upNext), + source: QueueItemSource(id: "up-next", name: "Up Next", type: QueueItemSourceType.nextUp), + type: QueueItemQueueType.nextUp, ); // don't add to _order, because it wasn't added to the regular queue - _queueNextUp.add(queueItem); + _queueFromConcatenatingAudioSource(0); // update internal queues + int offset = _queueNextUp.length; - _queueServiceLogger.fine("Prepended '${queueItem.item.title}' to Next Up"); + _queueAudioSource.insert(_queueAudioSourceIndex+1+offset, await _queueItemToAudioSource(queueItem)); + + _queueServiceLogger.fine("Appended '${queueItem.item.title}' to Next Up"); } catch (e) { _queueServiceLogger.severe(e); @@ -492,8 +506,12 @@ class QueueService { return QueueInfo( previousTracks: _queuePreviousTracks, - currentTrack: _currentTrack!, + currentTrack: _currentTrack ?? QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)), queue: _queue, + // nextUp: _queueNextUp, + nextUp: [ + QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)), + ], ); } @@ -512,36 +530,16 @@ class QueueService { set loopMode(LoopMode mode) { _loopMode = mode; - _currentTrackStream.add(_currentTrack ?? QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown))); + _currentTrackStream.add(_currentTrack ?? QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown))); if (mode == LoopMode.one) { - // _audioHandler.setRepeatMode(AudioServiceRepeatMode.one); // without the repeat mode, we cannot prevent the player from skipping to the next track + _audioHandler.setRepeatMode(AudioServiceRepeatMode.one); + } else if (mode == LoopMode.all) { + _audioHandler.setRepeatMode(AudioServiceRepeatMode.all); } else { - // _audioHandler.setRepeatMode(AudioServiceRepeatMode.none); - - // handle enabling loop all when the queue is empty - if (mode == LoopMode.all && (_queue.length + _queueNextUp.length == 0)) { - // _applyLoopQueue(); - } else if (mode != LoopMode.all) { - // find current track in `_order` and set the queue to the items after it - int currentTrackIndex = _order.items.indexOf(_currentTrack!); - int currentTrackOrderIndex = (_playbackOrder == PlaybackOrder.linear ? _order.linearOrder : _order.shuffledOrder).indexWhere((trackIndex) => trackIndex == currentTrackIndex); - // use indices of current playback order to get the items after the current track - List itemsAfterCurrentTrack = (_playbackOrder == PlaybackOrder.linear ? _order.linearOrder : _order.shuffledOrder).sublist(currentTrackOrderIndex+1); - // add items to queue - _queue.clear(); - for (int itemIndex in itemsAfterCurrentTrack) { - _queue.add(_order.items[itemIndex]); - } - - _logQueues(message: "after looping"); - - // update external queues - // pushQueueToExternalQueues(); - - } + _audioHandler.setRepeatMode(AudioServiceRepeatMode.none); } - + } LoopMode get loopMode => _loopMode; @@ -551,9 +549,12 @@ class QueueService { // _currentTrackStream.add(_currentTrack ?? QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown))); // update queue accordingly and generate new shuffled order if necessary - if (_currentTrack != null) { - _applyUpdatePlaybackOrder(); + if (_playbackOrder == PlaybackOrder.shuffled) { + _audioHandler.setShuffleMode(AudioServiceShuffleMode.all); + } else { + _audioHandler.setShuffleMode(AudioServiceShuffleMode.none); } + _queueFromConcatenatingAudioSource(0); // update queue } @@ -774,6 +775,39 @@ class QueueService { ); } + /// Syncs the list of MediaItems (_queue) with the internal queue of the player. + /// Called by onAddQueueItem and onUpdateQueue. + Future _queueItemToAudioSource(QueueItem queueItem) async { + if (queueItem.item.extras!["downloadedSongJson"] == null) { + // If DownloadedSong wasn't passed, we assume that the item is not + // downloaded. + + // If offline, we throw an error so that we don't accidentally stream from + // the internet. See the big comment in _songUri() to see why this was + // passed in extras. + if (queueItem.item.extras!["isOffline"]) { + return Future.error( + "Offline mode enabled but downloaded song not found."); + } else { + if (queueItem.item.extras!["shouldTranscode"] == true) { + return HlsAudioSource(await _songUri(queueItem.item), tag: queueItem); + } else { + return AudioSource.uri(await _songUri(queueItem.item), tag: queueItem); + } + } + } else { + // We have to deserialise this because Dart is stupid and can't handle + // sending classes through isolates. + final downloadedSong = + DownloadedSong.fromJson(queueItem.item.extras!["downloadedSongJson"]); + + // Path verification and stuff is done in AudioServiceHelper, so this path + // should be valid. + final downloadUri = Uri.file(downloadedSong.file.path); + return AudioSource.uri(downloadUri, tag: queueItem); + } + } + /// Syncs the list of MediaItems (_queue) with the internal queue of the player. /// Called by onAddQueueItem and onUpdateQueue. Future _mediaItemToAudioSource(MediaItem mediaItem) async { @@ -864,3 +898,78 @@ class QueueService { } } + +class NextUpShuffleOrder extends ShuffleOrder { + + final Random _random; + final QueueService? _queueService; + @override + final indices = []; + + NextUpShuffleOrder({Random? random, QueueService? queueService}) : _random = random ?? Random(), _queueService = queueService; + + @override + void shuffle({int? initialIndex}) { + assert(initialIndex == null || indices.contains(initialIndex)); + if (indices.length <= 1) return; + indices.shuffle(_random); + if (initialIndex == null) return; + + int nextUpLength = 0; + if (_queueService == null) { + QueueInfo queueInfo = _queueService!.getQueue(); + nextUpLength = queueInfo.nextUp.length; + } + + const initialPos = 0; + final swapPos = indices.indexOf(initialIndex); + // Swap the indices at initialPos and swapPos. + final swapIndex = indices[initialPos]; + indices[initialPos] = initialIndex; + indices[swapPos] = swapIndex; + + // swap all Next Up items to the front + for (int i = 0; i < nextUpLength; i++) { + final swapIndex = indices.indexOf(initialPos + i); + indices[i] = indices[initialPos + i]; + indices[initialPos + i] = swapIndex; + } + + } + + @override + void insert(int index, int count) { + // // Offset indices after insertion point. + // for (var i = 0; i < indices.length; i++) { + // if (indices[i] >= index) { + // indices[i] += count; + // } + // } + // // Insert new indices at random positions after currentIndex. + // final newIndices = List.generate(count, (i) => index + i); + // for (var newIndex in newIndices) { + // final insertionIndex = _random.nextInt(indices.length + 1); + // indices.insert(insertionIndex, newIndex); + // } + } + + @override + void removeRange(int start, int end) { + final count = end - start; + // Remove old indices. + final oldIndices = List.generate(count, (i) => start + i).toSet(); + indices.removeWhere(oldIndices.contains); + // Offset indices after deletion point. + for (var i = 0; i < indices.length; i++) { + if (indices[i] >= end) { + indices[i] -= count; + } + } + } + + @override + void clear() { + indices.clear(); + } + +} From 554cb4364c70c59833c3214e1f999406c4551f9f Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 2 Jun 2023 21:27:47 +0200 Subject: [PATCH 021/130] just_audio-based shuffling with Next Up support almost working - only thing missing is disabling shuffling and keeping the right index --- lib/models/finamp_models.dart | 2 +- .../music_player_background_task.dart | 21 +- lib/services/queue_service.dart | 498 ++---------------- 3 files changed, 58 insertions(+), 463 deletions(-) diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index e6004c7f9..da9084053 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -569,7 +569,7 @@ enum QueueItemSourceType { genre(name: "Genre"), artist(name: "Artist"), nextUp(name: ""), - formerUpNext(name: "Track added to Up Next"), + formerNextUp(name: "Track added to Up Next"), downloads(name: ""), unknown(name: ""); diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index 9fe7edac8..5545d1c4d 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -39,10 +39,6 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { final _playbackEventStreamController = StreamController(); - /// Set when shuffle mode is changed. If true, [onUpdateQueue] will create a - /// shuffled [ConcatenatingAudioSource]. - bool shuffleNextQueue = false; - /// Set when creating a new queue. Will be used to set the first index in a /// new queue. int? nextInitialIndex; @@ -309,7 +305,6 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } } - shuffleNextQueue = false; nextInitialIndex = null; } catch (e) { _audioServiceBackgroundTaskLogger.severe(e); @@ -348,16 +343,9 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { @override Future skipToNext() async { - bool doSkip = true; - try { - - if (_queueCallbackNextTrack != null) { - doSkip = await _queueCallbackNextTrack!(); - } - if (doSkip) { - await _player.seekToNext(); - } + await _player.seekToNext(); + _audioServiceBackgroundTaskLogger.finer("_player.nextIndex: ${_player.nextIndex}"); } catch (e) { _audioServiceBackgroundTaskLogger.severe(e); return Future.error(e); @@ -405,12 +393,11 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { try { switch (shuffleMode) { case AudioServiceShuffleMode.all: + await _player.shuffle(); await _player.setShuffleModeEnabled(true); - shuffleNextQueue = true; break; case AudioServiceShuffleMode.none: await _player.setShuffleModeEnabled(false); - shuffleNextQueue = false; break; default: return Future.error( @@ -598,6 +585,8 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { return _queueAudioSource.sequence.map((e) => (e.tag as QueueItem).item).toList(); } + List? get effectiveSequence => _player.sequenceState?.effectiveSequence; + /// Syncs the list of MediaItems (_queue) with the internal queue of the player. /// Called by onAddQueueItem and onUpdateQueue. Future _mediaItemToAudioSource(MediaItem mediaItem) async { diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 8f3c45e86..582c2a698 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -89,7 +89,8 @@ class QueueService { void _queueFromConcatenatingAudioSource(int indexDifference) { //TODO handle shuffleIndices - List allTracks = _queueAudioSource.sequence.map((e) => e.tag as QueueItem).toList(); + List allTracks = _audioHandler.effectiveSequence?.map((e) => e.tag as QueueItem).toList() ?? []; + int adjustedQueueIndex = _queueAudioSource.shuffleIndices.length > 0 ? _queueAudioSource.shuffleIndices[_queueAudioSourceIndex] : 0; _queuePreviousTracks.clear(); _queueNextUp.clear(); @@ -97,46 +98,25 @@ class QueueService { // split the queue by old type for (int i = 0; i < allTracks.length; i++) { - switch (allTracks[i].type) { - case QueueItemQueueType.previousTracks: - _queuePreviousTracks.add(allTracks[i]); - break; - case QueueItemQueueType.currentTrack: - _currentTrack = allTracks[i]; - break; - case QueueItemQueueType.nextUp: + + if (i < adjustedQueueIndex) { + _queuePreviousTracks.add(allTracks[i]); + if (_queuePreviousTracks.last.type == QueueItemQueueType.nextUp) { + _queuePreviousTracks.last.source.type = QueueItemSourceType.formerNextUp; + } + _queuePreviousTracks.last.type = QueueItemQueueType.previousTracks; + } else if (i == adjustedQueueIndex) { + _currentTrack = allTracks[i]; + _currentTrack!.type = QueueItemQueueType.currentTrack; + } else { + if (allTracks[i].type == QueueItemQueueType.nextUp) { _queueNextUp.add(allTracks[i]); - break; - default: - // queue + } else { _queue.add(allTracks[i]); + _queue.last.type = QueueItemQueueType.queue; + } } - } - - // update types - if (indexDifference > 0) { - - _currentTrack?.type = QueueItemQueueType.previousTracks; - for (int i = 0; i < indexDifference-1; i++) { - _queue[i].type = QueueItemQueueType.previousTracks; - } - _queue[indexDifference-1].type = QueueItemQueueType.currentTrack; - - _currentTrack = _queue[indexDifference-1]; - _queuePreviousTracks.addAll(_queue.sublist(0, indexDifference)); - _queue.removeRange(0, indexDifference); - - } else if (indexDifference < 0) { - - _currentTrack?.type = QueueItemQueueType.queue; - for (int i = _queuePreviousTracks.length-1; i > _queuePreviousTracks.length+indexDifference; i--) { - _queuePreviousTracks[i].type = QueueItemQueueType.queue; - } - _queue[_queue.length + indexDifference].type = QueueItemQueueType.currentTrack; - _currentTrack = _queue[_queue.length + indexDifference]; - _queue.insertAll(0, _queuePreviousTracks.sublist(_queuePreviousTracks.length + indexDifference)); - _queuePreviousTracks.removeRange(_queuePreviousTracks.length + indexDifference, _queuePreviousTracks.length); } _queueStream.add(getQueue()); @@ -150,206 +130,6 @@ class QueueService { } - Future _applyNextTrack({bool eventFromPlayer = false}) async { - //TODO handle "Next Up" queue - - // update internal queues - - bool addCurrentTrackToPreviousTracks = true; - - _queueServiceLogger.finer("Loop mode: $loopMode"); - - if (loopMode == LoopMode.one) { - _audioHandler.seek(Duration.zero); - _queueServiceLogger.finer("Looping current track: '${_currentTrack!.item.title}'"); - - //TODO update playback history - - if (eventFromPlayer) { - return false; // player already skipped - } - - return false; // perform the skip - } if ( - (_queue.length + _queueNextUp.length == 0) - && loopMode == LoopMode.all - ) { - - await _applyLoopQueue(); - addCurrentTrackToPreviousTracks = false; - // _queueAudioSourceIndex = -1; // so that incrementing the index below will set it to 0 - - } else if (_queue.isEmpty && loopMode == LoopMode.none) { - _queueServiceLogger.info("Cannot skip ahead, no tracks in queue"); - //TODO show snackbar - return false; - } - - await pushQueueToExternalQueues(); - if (addCurrentTrackToPreviousTracks) { - _queuePreviousTracks.add(_currentTrack!); - } - - _currentTrack = _queue.removeAt(0); - _currentTrackStream.add(_currentTrack!); - - _logQueues(message: "after skipping forward"); - - _queueStream.add(getQueue()); - - // _queueAudioSourceIndex++; // increment external queue index so we can detect that the change has already been handled once - - return true; - - } - - Future _applyPreviousTrack({bool eventFromPlayer = false}) async { - //TODO handle "Next Up" queue - - bool addCurrentTrackToQueue = true; - - // update internal queues - - if (loopMode == LoopMode.one) { - - _audioHandler.seek(Duration.zero); - _queueServiceLogger.finer("Looping current track: '${_currentTrack!.item.title}'"); - //TODO update playback history - - if (eventFromPlayer) { - return false; // player already skipped - } - - return false; // perform the skip - } - - if (_queuePreviousTracks.isEmpty) { - - if (loopMode == LoopMode.all) { - - await _applyLoopQueue(skippingBackwards: true); - addCurrentTrackToQueue = false; - - } else { - _queueServiceLogger.info("Cannot skip back, no previous tracks in queue"); - _audioHandler.seek(Duration.zero); - return false; - } - } - - if (_audioHandler.getPlayPositionInSeconds() > 5) { - _audioHandler.seek(Duration.zero); - return false; - } - - await pushQueueToExternalQueues(); - if (addCurrentTrackToQueue) { - _queue.insert(0, _currentTrack!); - } - - _currentTrack = _queuePreviousTracks.removeLast(); - _currentTrackStream.add(_currentTrack!); - - // _queueAudioSourceIndex--; // increment external queue index so we can detect that the change has already been handled once - - _logQueues(message: "after skipping backwards"); - - _queueStream.add(getQueue()); - - // await pushQueueToExternalQueues(); - - return true; - - } - - Future _applySkipToTrackByOffset(int offset, { - bool updateExternalQueues = true, - }) async { - - //TODO handle "Next Up" queue - - - bool addCurrentTrackToPreviousTracks = true; - - _logQueues(message: "before skipping by offset $offset"); - - if (offset == 0) { - return false; - } else if ( - (offset > 0 && _queue.length < offset) || - (offset < 0 && _queuePreviousTracks.length < offset) - ) { - _queueServiceLogger.info("Cannot skip to offset $offset, not enough tracks in queue"); - //TODO show snackbar - return false; - } - - if (addCurrentTrackToPreviousTracks) { - _queuePreviousTracks.add(_currentTrack!); - } - - if (offset > 0) { - _queuePreviousTracks.addAll(_queue.sublist(0, offset-1)); - _queue.removeRange(0, offset-1); - - _currentTrack = _queue.removeAt(0); - _currentTrackStream.add(_currentTrack!); - } else { - _queue.insertAll(0, _queuePreviousTracks.sublist(_queuePreviousTracks.length + offset, _queuePreviousTracks.length)); - _queuePreviousTracks.removeRange(_queuePreviousTracks.length + offset, _queuePreviousTracks.length); - - _currentTrack = _queuePreviousTracks.removeLast(); - _currentTrackStream.add(_currentTrack!); - } - - _logQueues(message: "after skipping by offset $offset"); - - if (updateExternalQueues) { - await pushQueueToExternalQueues(); - } - - _queueAudioSourceIndex = _queueAudioSourceIndex + offset; - - return true; - - } - - // Future nextTrack() async { - // //TODO make _audioHandler._player call this function instead of skipping ahead itself - - // if (await _applyNextTrack()) { - - // // update external queues - // _audioHandler.skipToNext(); - // _queueAudioSourceIndex++; - - // _queueServiceLogger.info("Skipped ahead to next track: '${_currentTrack!.item.title}'"); - - // } - - // } - - // Future previousTrack() async { - // //TODO handle "Next Up" queue - // // update internal queues - - // if (_audioHandler.getPlayPositionInSeconds() > 5 || _queuePreviousTracks.isEmpty) { - // _audioHandler.seek(const Duration(seconds: 0)); - // return; - // } - - // if (await _applyPreviousTrack()) { - - // // update external queues - // _audioHandler.skipToPrevious(); - // _queueAudioSourceIndex--; - - // _queueServiceLogger.info("Skipped back to previous track: '${_currentTrack!.item.title}'"); - - // } - - // } - Future startPlayback({ required List items, required QueueItemSource source @@ -412,7 +192,7 @@ class QueueService { _queueAudioSourceIndex = 0; _audioHandler.setNextInitialIndex(_queueAudioSourceIndex); - _queueAudioSource = ConcatenatingAudioSource(children: []); + _queueAudioSource.clear(); for (final queueItem in newItems) { await _queueAudioSource.add(await _queueItemToAudioSource(queueItem)); @@ -450,10 +230,6 @@ class QueueService { _queueServiceLogger.fine("Added '${queueItem.item.title}' to queue from '${source.name}' (${source.type})"); - // if (_loopMode == LoopMode.all && _queue.length == 0) { - // await pushQueueToExternalQueues(); - // } - } catch (e) { _queueServiceLogger.severe(e); return Future.error(e); @@ -550,166 +326,16 @@ class QueueService { // update queue accordingly and generate new shuffled order if necessary if (_playbackOrder == PlaybackOrder.shuffled) { - _audioHandler.setShuffleMode(AudioServiceShuffleMode.all); + _audioHandler.setShuffleMode(AudioServiceShuffleMode.all).then((value) => _queueFromConcatenatingAudioSource(0)); } else { - _audioHandler.setShuffleMode(AudioServiceShuffleMode.none); + _audioHandler.setShuffleMode(AudioServiceShuffleMode.none).then((value) => _queueFromConcatenatingAudioSource(0)); } - _queueFromConcatenatingAudioSource(0); // update queue } PlaybackOrder get playbackOrder => _playbackOrder; - Future _applyLoopQueue({ - skippingBackwards = false, - }) async { - - _queueServiceLogger.fine("Looping queue for ${skippingBackwards ? "skipping backwards" : "skipping forward"} using `_order`"); - - // log current queue - _logQueues(message: "before looping"); - - if (skippingBackwards) { - - // add items to previous tracks - for (int itemIndex in (_playbackOrder == PlaybackOrder.linear ? _order.linearOrder : _order.shuffledOrder)) { - _queuePreviousTracks.add(_order.items[itemIndex]); - } - - _queue.clear(); - - } else { - - // add items to queue - for (int itemIndex in (_playbackOrder == PlaybackOrder.linear ? _order.linearOrder : _order.shuffledOrder)) { - _queue.add(_order.items[itemIndex]); - } - - _queuePreviousTracks.clear(); - - } - - // log looped queue - _logQueues(message: "after looping"); - - // update external queues - // await pushQueueToExternalQueues(); - - _queueServiceLogger.info("Looped queue, added ${_order.items.length} items"); - - } - - Future _applyUpdatePlaybackOrder() async { - - _logQueues(message: "before playback order change to ${_playbackOrder.name}"); - - // find current track in `_order` - int currentTrackIndex = _currentTrack != null ? _order.items.indexOf(_currentTrack!) : 0; - int currentTrackOrderIndex = 0; - - - List itemsBeforeCurrentTrack = []; - List itemsAfterCurrentTrack = []; - - if (_playbackOrder == PlaybackOrder.shuffled) { - - // calculate new shuffled order where the currentTrack has index 0 - _order.shuffledOrder = List.from(_order.linearOrder)..shuffle(); - - currentTrackOrderIndex = _order.shuffledOrder.indexWhere((trackIndex) => trackIndex == currentTrackIndex); - - String shuffleOrderString = ""; - for (int index in _order.shuffledOrder) { - shuffleOrderString += "$index, "; - } - _queueServiceLogger.finer("unmodified new shuffle order: $shuffleOrderString"); - - // swap the current track to index 0 - int indexOfCurrentFirstTrackInShuffleOrder = _order.shuffledOrder[0]; - _order.shuffledOrder[0] = currentTrackIndex; - _order.shuffledOrder[currentTrackOrderIndex] = indexOfCurrentFirstTrackInShuffleOrder; - - _queueServiceLogger.finer("indexOfCurrentFirstTrackInShuffleOrder: ${indexOfCurrentFirstTrackInShuffleOrder}"); - _queueServiceLogger.finer("current track first in shuffled order: ${currentTrackIndex == _order.shuffledOrder[0]}"); - - String swappedShuffleOrderString = ""; - for (int index in _order.shuffledOrder) { - swappedShuffleOrderString += "$index, "; - } - _queueServiceLogger.finer("swapped new shuffle order: $swappedShuffleOrderString"); - - // use indices of current playback order to get the items after the current track - // first item is always the current track, so skip it - itemsAfterCurrentTrack = _order.shuffledOrder.sublist(1); - String sublistString = ""; - for (int index in itemsAfterCurrentTrack) { - sublistString += "$index, "; - } - _queueServiceLogger.finer("item indices after current track: $sublistString"); - - } else { - - currentTrackOrderIndex = _order.linearOrder.indexWhere((trackIndex) => trackIndex == currentTrackIndex); - - // set the queue to the items after the current track and previousTracks to items before the current track - // use indices of current playback order to get the items before the current track - itemsBeforeCurrentTrack = _order.linearOrder.sublist(0, currentTrackOrderIndex); - - // use indices of current playback order to get the items after the current track - itemsAfterCurrentTrack = _order.linearOrder.sublist(currentTrackOrderIndex+1); - - } - - // add items to previous tracks - _queuePreviousTracks.clear(); - for (int itemIndex in itemsBeforeCurrentTrack) { - // if (itemIndex != currentTrackIndex) { - _queuePreviousTracks.add(_order.items[itemIndex]); - // } - } - // add items to queue - _queue.clear(); - for (int itemIndex in itemsAfterCurrentTrack) { - // if (itemIndex != currentTrackIndex) { - _queue.add(_order.items[itemIndex]); - // } - } - - _logQueues(message: "after playback order change to ${_playbackOrder.name}"); - - await pushQueueToExternalQueues(); - - } - - Future pushQueueToExternalQueues() async { - - _audioHandler.queue.add(_queuePreviousTracks.followedBy([_currentTrack!]).followedBy(_queue).map((e) => e.item).toList()); - _currentTrackStream.add(_currentTrack!); - _queueStream.add(getQueue()); - - if (_queueAudioSource.length > 1) { - // clear queue after the current track - // do this first so that the current track index stays the same - _queueAudioSource.removeRange(_queueAudioSourceIndex+1, _queueAudioSource.length); - // clear queue before the current track - _queueAudioSource.removeRange(0, _queueAudioSourceIndex); - } - - // add items to queue - List previousAudioSources = []; - for (QueueItem queueItem in _queuePreviousTracks.toList()) { - previousAudioSources.add(await _mediaItemToAudioSource(queueItem.item)); - } - await _queueAudioSource.insertAll(0, previousAudioSources); - - // add items to queue - List nextAudioSources = []; - for (QueueItem queueItem in _queue) { - nextAudioSources.add(await _mediaItemToAudioSource(queueItem.item)); - } - await _queueAudioSource.addAll(nextAudioSources); - - } + Logger get queueServiceLogger => _queueServiceLogger; void _logQueues({ String message = "" }) { @@ -808,39 +434,6 @@ class QueueService { } } - /// Syncs the list of MediaItems (_queue) with the internal queue of the player. - /// Called by onAddQueueItem and onUpdateQueue. - Future _mediaItemToAudioSource(MediaItem mediaItem) async { - if (mediaItem.extras!["downloadedSongJson"] == null) { - // If DownloadedSong wasn't passed, we assume that the item is not - // downloaded. - - // If offline, we throw an error so that we don't accidentally stream from - // the internet. See the big comment in _songUri() to see why this was - // passed in extras. - if (mediaItem.extras!["isOffline"]) { - return Future.error( - "Offline mode enabled but downloaded song not found."); - } else { - if (mediaItem.extras!["shouldTranscode"] == true) { - return HlsAudioSource(await _songUri(mediaItem), tag: mediaItem); - } else { - return AudioSource.uri(await _songUri(mediaItem), tag: mediaItem); - } - } - } else { - // We have to deserialise this because Dart is stupid and can't handle - // sending classes through isolates. - final downloadedSong = - DownloadedSong.fromJson(mediaItem.extras!["downloadedSongJson"]); - - // Path verification and stuff is done in AudioServiceHelper, so this path - // should be valid. - final downloadUri = Uri.file(downloadedSong.file.path); - return AudioSource.uri(downloadUri, tag: mediaItem); - } - } - Future _songUri(MediaItem mediaItem) async { // We need the platform to be Android or iOS to get device info assert(Platform.isAndroid || Platform.isIOS, @@ -904,19 +497,31 @@ class NextUpShuffleOrder extends ShuffleOrder { final Random _random; final QueueService? _queueService; @override - final indices = []; + List indices = []; NextUpShuffleOrder({Random? random, QueueService? queueService}) : _random = random ?? Random(), _queueService = queueService; @override void shuffle({int? initialIndex}) { assert(initialIndex == null || indices.contains(initialIndex)); + indices.clear(); + QueueInfo queueInfo = _queueService!.getQueue(); + indices = List.generate(queueInfo.previousTracks.length + queueInfo.nextUp.length + queueInfo.queue.length, (i) => i); if (indices.length <= 1) return; indices.shuffle(_random); if (initialIndex == null) return; + _queueService!.queueServiceLogger.finer("initialIndex: $initialIndex"); + + // log indices + String indicesString = ""; + for (int index in indices) { + indicesString += "$index, "; + } + _queueService!.queueServiceLogger.finer("Shuffled indices: $indicesString"); + int nextUpLength = 0; - if (_queueService == null) { + if (_queueService != null) { QueueInfo queueInfo = _queueService!.getQueue(); nextUpLength = queueInfo.nextUp.length; } @@ -929,28 +534,29 @@ class NextUpShuffleOrder extends ShuffleOrder { indices[swapPos] = swapIndex; // swap all Next Up items to the front - for (int i = 0; i < nextUpLength; i++) { - final swapIndex = indices.indexOf(initialPos + i); - indices[i] = indices[initialPos + i]; - indices[initialPos + i] = swapIndex; + for (int i = 1; i <= nextUpLength; i++) { + final swapPos = indices.indexOf(initialIndex + i); + final swapIndex = indices[initialPos + i]; + indices[initialPos + i] = initialIndex + i; + indices[swapPos] = swapIndex; } + // log indices + indicesString = ""; + for (int index in indices) { + indicesString += "$index, "; + } + _queueService!.queueServiceLogger.finer("Shuffled indices (swapped): $indicesString"); + + } @override void insert(int index, int count) { - // // Offset indices after insertion point. - // for (var i = 0; i < indices.length; i++) { - // if (indices[i] >= index) { - // indices[i] += count; - // } - // } - // // Insert new indices at random positions after currentIndex. - // final newIndices = List.generate(count, (i) => index + i); - // for (var newIndex in newIndices) { - // final insertionIndex = _random.nextInt(indices.length + 1); - // indices.insert(insertionIndex, newIndex); - // } + // Offset indices after insertion point. + for (var i = 0; i < count; i++) { + indices.add(indices.length); + } } @override From f4d2a8215b8a88dc16176bd8006ca205ee9e4fde Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 2 Jun 2023 23:48:58 +0200 Subject: [PATCH 022/130] fully working shuffle mode - fixed adjustedQueueIndex being using the track index instead of the array index - fixed adjustedQueueIndex being based on shuffledIndices when shuffle disabled --- lib/services/queue_service.dart | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 582c2a698..e1075aafd 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -67,13 +67,14 @@ class QueueService { _audioHandler.getPlaybackEventStream().listen((event) async { - int indexDifference = (event.currentIndex ?? 0) - _queueAudioSourceIndex; + // int indexDifference = (event.currentIndex ?? 0) - _queueAudioSourceIndex; - _queueServiceLogger.finer("Play queue index changed, difference: $indexDifference"); + // _queueServiceLogger.finer("Play queue index changed, difference: $indexDifference"); _queueAudioSourceIndex = event.currentIndex ?? 0; + _queueServiceLogger.finer("Play queue index changed, new index: $_queueAudioSourceIndex"); - _queueFromConcatenatingAudioSource(indexDifference); + _queueFromConcatenatingAudioSource(); }); @@ -86,11 +87,11 @@ class QueueService { } - void _queueFromConcatenatingAudioSource(int indexDifference) { + void _queueFromConcatenatingAudioSource() { //TODO handle shuffleIndices List allTracks = _audioHandler.effectiveSequence?.map((e) => e.tag as QueueItem).toList() ?? []; - int adjustedQueueIndex = _queueAudioSource.shuffleIndices.length > 0 ? _queueAudioSource.shuffleIndices[_queueAudioSourceIndex] : 0; + int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; _queuePreviousTracks.clear(); _queueNextUp.clear(); @@ -265,7 +266,7 @@ class QueueService { // don't add to _order, because it wasn't added to the regular queue - _queueFromConcatenatingAudioSource(0); // update internal queues + _queueFromConcatenatingAudioSource(); // update internal queues int offset = _queueNextUp.length; _queueAudioSource.insert(_queueAudioSourceIndex+1+offset, await _queueItemToAudioSource(queueItem)); @@ -326,9 +327,9 @@ class QueueService { // update queue accordingly and generate new shuffled order if necessary if (_playbackOrder == PlaybackOrder.shuffled) { - _audioHandler.setShuffleMode(AudioServiceShuffleMode.all).then((value) => _queueFromConcatenatingAudioSource(0)); + _audioHandler.setShuffleMode(AudioServiceShuffleMode.all).then((value) => _queueFromConcatenatingAudioSource()); } else { - _audioHandler.setShuffleMode(AudioServiceShuffleMode.none).then((value) => _queueFromConcatenatingAudioSource(0)); + _audioHandler.setShuffleMode(AudioServiceShuffleMode.none).then((value) => _queueFromConcatenatingAudioSource()); } } From 32aac3757bab78788ac543f140357ff0c1de5fc2 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 2 Jun 2023 23:52:55 +0200 Subject: [PATCH 023/130] disable fake Next Up queue --- lib/services/queue_service.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index e1075aafd..ea14db4e3 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -285,10 +285,10 @@ class QueueService { previousTracks: _queuePreviousTracks, currentTrack: _currentTrack ?? QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)), queue: _queue, - // nextUp: _queueNextUp, - nextUp: [ - QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)), - ], + nextUp: _queueNextUp, + // nextUp: [ + // QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)), + // ], ); } @@ -512,14 +512,14 @@ class NextUpShuffleOrder extends ShuffleOrder { indices.shuffle(_random); if (initialIndex == null) return; - _queueService!.queueServiceLogger.finer("initialIndex: $initialIndex"); + _queueService!.queueServiceLogger.finest("initialIndex: $initialIndex"); // log indices String indicesString = ""; for (int index in indices) { indicesString += "$index, "; } - _queueService!.queueServiceLogger.finer("Shuffled indices: $indicesString"); + _queueService!.queueServiceLogger.finest("Shuffled indices: $indicesString"); int nextUpLength = 0; if (_queueService != null) { @@ -547,7 +547,7 @@ class NextUpShuffleOrder extends ShuffleOrder { for (int index in indices) { indicesString += "$index, "; } - _queueService!.queueServiceLogger.finer("Shuffled indices (swapped): $indicesString"); + _queueService!.queueServiceLogger.finest("Shuffled indices (swapped): $indicesString"); } From 02daccb266239adb2a42de48b3157a40bedce6a5 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 3 Jun 2023 00:04:06 +0200 Subject: [PATCH 024/130] fix shuffle indices calculation --- lib/services/queue_service.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index ea14db4e3..3f1047bfd 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -307,7 +307,7 @@ class QueueService { set loopMode(LoopMode mode) { _loopMode = mode; - _currentTrackStream.add(_currentTrack ?? QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown))); + // _currentTrackStream.add(_currentTrack ?? QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown))); if (mode == LoopMode.one) { _audioHandler.setRepeatMode(AudioServiceRepeatMode.one); @@ -507,7 +507,7 @@ class NextUpShuffleOrder extends ShuffleOrder { assert(initialIndex == null || indices.contains(initialIndex)); indices.clear(); QueueInfo queueInfo = _queueService!.getQueue(); - indices = List.generate(queueInfo.previousTracks.length + queueInfo.nextUp.length + queueInfo.queue.length, (i) => i); + indices = List.generate(queueInfo.previousTracks.length + 1 + queueInfo.nextUp.length + queueInfo.queue.length, (i) => i); if (indices.length <= 1) return; indices.shuffle(_random); if (initialIndex == null) return; @@ -523,11 +523,10 @@ class NextUpShuffleOrder extends ShuffleOrder { int nextUpLength = 0; if (_queueService != null) { - QueueInfo queueInfo = _queueService!.getQueue(); nextUpLength = queueInfo.nextUp.length; } - const initialPos = 0; + const initialPos = 0; // current item will always be at the front final swapPos = indices.indexOf(initialIndex); // Swap the indices at initialPos and swapPos. final swapIndex = indices[initialPos]; From 15b495e619abdd032cbdcb3a3df89a683f06837d Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 3 Jun 2023 00:35:51 +0200 Subject: [PATCH 025/130] fix shuffle always starting on the first index --- lib/services/queue_service.dart | 54 +++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 3f1047bfd..47535a2d2 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -137,7 +137,7 @@ class QueueService { }) async { // _initialQueue = list; // save original PlaybackList for looping/restarting and meta info - _replaceWholeQueue(itemList: items, source: source); + await _replaceWholeQueue(itemList: items, source: source); _queueServiceLogger.info("Started playing '${source.name}' (${source.type})"); } @@ -178,7 +178,22 @@ class QueueService { } } - newShuffledOrder = List.from(newLinearOrder)..shuffle(); + await _audioHandler.stop(); + _queueAudioSource.clear(); + + List audioSources = []; + + for (final queueItem in newItems) { + audioSources.add(await _queueItemToAudioSource(queueItem)); + } + + await _queueAudioSource.addAll(audioSources); + // set first item in queue + _queueAudioSourceIndex = 0; + _audioHandler.setNextInitialIndex(_queueAudioSource.shuffleIndices[_queueAudioSourceIndex]); + await _audioHandler.initializeAudioSource(_queueAudioSource); + + newShuffledOrder = List.from(_queueAudioSource.shuffleIndices); _order = QueueOrder( items: newItems, @@ -188,24 +203,11 @@ class QueueService { _queueServiceLogger.fine("Order items length: ${_order.items.length}"); - - // start playing first item in queue - _queueAudioSourceIndex = 0; - _audioHandler.setNextInitialIndex(_queueAudioSourceIndex); - - _queueAudioSource.clear(); - - for (final queueItem in newItems) { - await _queueAudioSource.add(await _queueItemToAudioSource(queueItem)); - } - - await _audioHandler.initializeAudioSource(_queueAudioSource); - _audioHandler.queue.add(_queue.map((e) => e.item).toList()); // _queueStream.add(getQueue()); - _audioHandler.play(); + await _audioHandler.play(); _audioHandler.nextInitialIndex = null; @@ -553,10 +555,30 @@ class NextUpShuffleOrder extends ShuffleOrder { @override void insert(int index, int count) { + + int indicesOriginalLength = indices.length; // Offset indices after insertion point. for (var i = 0; i < count; i++) { indices.add(indices.length); } + + if (indicesOriginalLength == 0) { + _queueService!.queueServiceLogger.finest("count (before fixing first index): $count"); + // log indices + String indicesString = ""; + for (int index in indices) { + indicesString += "$index, "; + } + _queueService!.queueServiceLogger.finest("Shuffled indices (before fixing first index): $indicesString"); + indices = indices..shuffle(); + // log indices + indicesString = ""; + for (int index in indices) { + indicesString += "$index, "; + } + _queueService!.queueServiceLogger.finest("Shuffled indices (after fixing first index): $indicesString"); + } + } @override From 0ebb4ae6c02b49ed7e205659c902911a7d3dfd9b Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 3 Jun 2023 01:15:32 +0200 Subject: [PATCH 026/130] fix playback always being shuffled, try to not always start shuffle on first item --- .../player_screen_appbar_title.dart | 1 - lib/components/PlayerScreen/queue_list.dart | 2 +- lib/models/finamp_models.dart | 2 +- .../music_player_background_task.dart | 8 +- lib/services/queue_service.dart | 77 +++++++++++-------- 5 files changed, 50 insertions(+), 40 deletions(-) diff --git a/lib/components/PlayerScreen/player_screen_appbar_title.dart b/lib/components/PlayerScreen/player_screen_appbar_title.dart index a9d773363..29c31c6e6 100644 --- a/lib/components/PlayerScreen/player_screen_appbar_title.dart +++ b/lib/components/PlayerScreen/player_screen_appbar_title.dart @@ -9,7 +9,6 @@ import 'package:palette_generator/palette_generator.dart'; import '../../models/jellyfin_models.dart' as jellyfin_models; import '../../models/finamp_models.dart'; import 'package:finamp/services/queue_service.dart'; -import '../../to_contrast.dart'; class PlayerScreenAppBarTitle extends StatefulWidget { diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 9ff12f8ca..e255e0092 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -53,7 +53,7 @@ class _QueueListState extends State { if (snapshot.hasData) { _previousTracks ??= snapshot.data!.queueInfo.previousTracks; - _currentTrack ??= snapshot.data!.queueInfo.currentTrack; + _currentTrack = snapshot.data!.queueInfo.currentTrack ?? QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)); _queue ??= snapshot.data!.queueInfo.queue; final GlobalKey currentTrackKey = GlobalKey(debugLabel: "currentTrack"); diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index da9084053..261a0326b 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -661,7 +661,7 @@ class QueueInfo { List previousTracks; @HiveField(1) - QueueItem currentTrack; + QueueItem? currentTrack; @HiveField(2) List nextUp; diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index 5545d1c4d..be2c4dd57 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -459,14 +459,14 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { try { return jellyfin_models.PlaybackProgressInfo( itemId: item?.extras?["itemJson"]["Id"] ?? - _getQueueItem(_player.currentIndex ?? 0).extras!["itemJson"]["Id"], + _getQueueItem(_player.currentIndex ?? 0)?.extras?["itemJson"]?["Id"], isPaused: !_player.playing, isMuted: _player.volume == 0, positionTicks: _player.position.inMicroseconds * 10, repeatMode: _jellyfinRepeatMode(_player.loopMode), playMethod: item?.extras!["shouldTranscode"] ?? _getQueueItem(_player.currentIndex ?? 0) - .extras!["shouldTranscode"] + ?.extras?["shouldTranscode"] ? "Transcode" : "DirectPlay", // We don't send the queue since it seems useless and it can cause @@ -577,8 +577,8 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } } - MediaItem _getQueueItem(int index) { - return (_queueAudioSource.sequence[index].tag as QueueItem).item; + MediaItem? _getQueueItem(int index) { + return _queueAudioSource.sequence.isNotEmpty ? (_queueAudioSource.sequence[index].tag as QueueItem).item : null; } List _queueFromSource() { diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 47535a2d2..639fc8c51 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -159,6 +159,7 @@ class QueueService { _queue.clear(); // empty queue _queuePreviousTracks.clear(); _queueNextUp.clear(); + _currentTrack = null; List newItems = []; List newLinearOrder = []; @@ -188,11 +189,14 @@ class QueueService { } await _queueAudioSource.addAll(audioSources); + // set first item in queue _queueAudioSourceIndex = 0; _audioHandler.setNextInitialIndex(_queueAudioSource.shuffleIndices[_queueAudioSourceIndex]); await _audioHandler.initializeAudioSource(_queueAudioSource); + playbackOrder = _playbackOrder; // re-trigger playback order setter to update queue + newShuffledOrder = List.from(_queueAudioSource.shuffleIndices); _order = QueueOrder( @@ -285,7 +289,7 @@ class QueueService { return QueueInfo( previousTracks: _queuePreviousTracks, - currentTrack: _currentTrack ?? QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)), + currentTrack: _currentTrack, queue: _queue, nextUp: _queueNextUp, // nextUp: [ @@ -325,6 +329,7 @@ class QueueService { set playbackOrder(PlaybackOrder order) { _playbackOrder = order; + _queueServiceLogger.fine("Playback order set to $order"); // _currentTrackStream.add(_currentTrack ?? QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown))); // update queue accordingly and generate new shuffled order if necessary @@ -522,25 +527,31 @@ class NextUpShuffleOrder extends ShuffleOrder { indicesString += "$index, "; } _queueService!.queueServiceLogger.finest("Shuffled indices: $indicesString"); + _queueService!.queueServiceLogger.finest("Current Track: ${queueInfo.currentTrack}"); - int nextUpLength = 0; - if (_queueService != null) { - nextUpLength = queueInfo.nextUp.length; - } + // check if something is already playing, if not we also want to shuffle the first item (don't swap) + if (queueInfo.currentTrack != null) { - const initialPos = 0; // current item will always be at the front - final swapPos = indices.indexOf(initialIndex); - // Swap the indices at initialPos and swapPos. - final swapIndex = indices[initialPos]; - indices[initialPos] = initialIndex; - indices[swapPos] = swapIndex; - - // swap all Next Up items to the front - for (int i = 1; i <= nextUpLength; i++) { - final swapPos = indices.indexOf(initialIndex + i); - final swapIndex = indices[initialPos + i]; - indices[initialPos + i] = initialIndex + i; + int nextUpLength = 0; + if (_queueService != null) { + nextUpLength = queueInfo.nextUp.length; + } + + const initialPos = 0; // current item will always be at the front + final swapPos = indices.indexOf(initialIndex); + // Swap the indices at initialPos and swapPos. + final swapIndex = indices[initialPos]; + indices[initialPos] = initialIndex; indices[swapPos] = swapIndex; + + // swap all Next Up items to the front + for (int i = 1; i <= nextUpLength; i++) { + final swapPos = indices.indexOf(initialIndex + i); + final swapIndex = indices[initialPos + i]; + indices[initialPos + i] = initialIndex + i; + indices[swapPos] = swapIndex; + } + } // log indices @@ -562,22 +573,22 @@ class NextUpShuffleOrder extends ShuffleOrder { indices.add(indices.length); } - if (indicesOriginalLength == 0) { - _queueService!.queueServiceLogger.finest("count (before fixing first index): $count"); - // log indices - String indicesString = ""; - for (int index in indices) { - indicesString += "$index, "; - } - _queueService!.queueServiceLogger.finest("Shuffled indices (before fixing first index): $indicesString"); - indices = indices..shuffle(); - // log indices - indicesString = ""; - for (int index in indices) { - indicesString += "$index, "; - } - _queueService!.queueServiceLogger.finest("Shuffled indices (after fixing first index): $indicesString"); - } + // if (indicesOriginalLength == 0) { + // _queueService!.queueServiceLogger.finest("count (before fixing first index): $count"); + // // log indices + // String indicesString = ""; + // for (int index in indices) { + // indicesString += "$index, "; + // } + // _queueService!.queueServiceLogger.finest("Shuffled indices (before fixing first index): $indicesString"); + // indices = indices..shuffle(); + // // log indices + // indicesString = ""; + // for (int index in indices) { + // indicesString += "$index, "; + // } + // _queueService!.queueServiceLogger.finest("Shuffled indices (after fixing first index): $indicesString"); + // } } From fca95c72c365df3ffde7a863fae32e0adf15a74a Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 3 Jun 2023 01:23:10 +0200 Subject: [PATCH 027/130] fix skipByOffset() when shuffle is enabled - allows skipping using the QueueList again --- lib/services/music_player_background_task.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index be2c4dd57..2997b25f8 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -367,11 +367,9 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { try { - await _player.seek(Duration.zero, index: (_player.currentIndex ?? 0) + offset); + await _player.seek(Duration.zero, index: + _player.shuffleModeEnabled ? _queueAudioSource.shuffleIndices[_queueAudioSource.shuffleIndices.indexOf((_player.currentIndex ?? 0)) + offset] : (_player.currentIndex ?? 0) + offset); - if (_queueCallbackSkipToIndexCallback != null) { - await _queueCallbackSkipToIndexCallback!(offset); - } } catch (e) { _audioServiceBackgroundTaskLogger.severe(e); return Future.error(e); From d9bb4697611d04ecd6203b07a772e95053f5ebb4 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Mon, 5 Jun 2023 15:52:20 +0200 Subject: [PATCH 028/130] implementing moving and removing queue items --- lib/components/PlayerScreen/queue_list.dart | 178 +++++++++++--------- lib/services/queue_service.dart | 25 +++ 2 files changed, 122 insertions(+), 81 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index e255e0092..5909f3bd1 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -83,56 +83,64 @@ class _QueueListState extends State { SliverReorderableList( itemCount: _previousTracks?.length ?? 0, onReorder: (oldIndex, newIndex) async { - setState(() { - // _previousTracks?.insert(newIndex, _previousTracks![oldIndex]); - // _previousTracks?.removeAt(oldIndex); - int? smallerThanNewIndex; - if (oldIndex < newIndex) { - // When we're moving an item backwards, we need to reduce - // newIndex by 1 to account for there being a new item added - // before newIndex. - smallerThanNewIndex = newIndex - 1; - } - final item = _previousTracks?.removeAt(oldIndex); - _previousTracks?.insert(smallerThanNewIndex ?? newIndex, item!); - }); - await _audioHandler.reorderQueue(oldIndex, newIndex); + final oldOffset = -((_previousTracks?.length ?? 0) - oldIndex); + final newOffset = -((_previousTracks?.length ?? 0) - newIndex); + // setState(() { + // // _previousTracks?.insert(newIndex, _previousTracks![oldIndex]); + // // _previousTracks?.removeAt(oldIndex); + // int? smallerThanNewIndex; + // if (oldIndex < newIndex) { + // // When we're moving an item backwards, we need to reduce + // // newIndex by 1 to account for there being a new item added + // // before newIndex. + // smallerThanNewIndex = newIndex - 1; + // } + // final item = _previousTracks?.removeAt(oldIndex); + // _previousTracks?.insert(smallerThanNewIndex ?? newIndex, item!); + // }); + await _queueService.reorderByOffset(oldOffset, newOffset); }, itemBuilder: (context, index) { final actualIndex = index; - return Dismissible( - key: ValueKey(_previousTracks![actualIndex].item.id + actualIndex.toString()), - direction: - FinampSettingsHelper.finampSettings.disableGesture - ? DismissDirection.none - : DismissDirection.horizontal, - onDismissed: (direction) async { - //TODO - // await _audioHandler.removeQueueItemAt(actualIndex); - }, - child: ListTile( - leading: AlbumImage( - item: _previousTracks?[actualIndex].item - .extras?["itemJson"] == - null - ? null - : jellyfin_models.BaseItemDto.fromJson(_previousTracks?[actualIndex].item.extras?["itemJson"]), - ), - title: Text( - _previousTracks?[actualIndex].item.title ?? - AppLocalizations.of(context)!.unknownName, - style: _currentTrack == - _previousTracks?[actualIndex] - ? TextStyle( - color: - Theme.of(context).colorScheme.secondary) - : null), - subtitle: Text(processArtist( - _previousTracks?[actualIndex].item.artist, - context)), - onTap: () async => - await _audioHandler.skipByOffset(-((_previousTracks?.length ?? 0) - index)), - ), + final indexOffset = -((_previousTracks?.length ?? 0) - index); + return ReorderableDelayedDragStartListener( + index: index, + key: ValueKey("${_previousTracks![actualIndex].item.id}$actualIndex-drag"), + child: Dismissible( + key: ValueKey(_previousTracks![actualIndex].item.id + actualIndex.toString()), + direction: + FinampSettingsHelper.finampSettings.disableGesture + ? DismissDirection.none + : DismissDirection.horizontal, + onDismissed: (direction) async { + await _queueService.removeAtOffset(indexOffset); + }, + child: Card( + child: ListTile( + leading: AlbumImage( + item: _previousTracks?[actualIndex].item + .extras?["itemJson"] == + null + ? null + : jellyfin_models.BaseItemDto.fromJson(_previousTracks?[actualIndex].item.extras?["itemJson"]), + ), + title: Text( + _previousTracks?[actualIndex].item.title ?? + AppLocalizations.of(context)!.unknownName, + style: _currentTrack == + _previousTracks?[actualIndex] + ? TextStyle( + color: + Theme.of(context).colorScheme.secondary) + : null), + subtitle: Text(processArtist( + _previousTracks?[actualIndex].item.artist, + context)), + onTap: () async => + await _queueService.skipByOffset(indexOffset), + ), + ) + ) ); }, ), @@ -171,6 +179,8 @@ class _QueueListState extends State { SliverReorderableList( itemCount: _queue?.length ?? 0, onReorder: (oldIndex, newIndex) async { + final oldOffset = oldIndex + 1; + final newOffset = newIndex + 1; setState(() { // _queue?.insert(newIndex, _queue![oldIndex]); // _queue?.removeAt(oldIndex); @@ -184,43 +194,49 @@ class _QueueListState extends State { final item = _queue?.removeAt(oldIndex); _queue?.insert(smallerThanNewIndex ?? newIndex, item!); }); - await _audioHandler.reorderQueue(oldIndex, newIndex); + await _queueService.reorderByOffset(oldOffset, newOffset); }, itemBuilder: (context, index) { final actualIndex = index; - return Dismissible( - key: ValueKey(_queue![actualIndex].item.id + actualIndex.toString()), - direction: - FinampSettingsHelper.finampSettings.disableGesture - ? DismissDirection.none - : DismissDirection.horizontal, - onDismissed: (direction) async { - //TODO - // await _audioHandler.removeQueueItemAt(actualIndex); - }, - child: ListTile( - leading: AlbumImage( - item: _queue?[actualIndex].item - .extras?["itemJson"] == - null - ? null - : jellyfin_models.BaseItemDto.fromJson(_queue?[actualIndex].item.extras?["itemJson"]), - ), - title: Text( - _queue?[actualIndex].item.title ?? - AppLocalizations.of(context)!.unknownName, - style: _currentTrack == - _queue?[actualIndex] - ? TextStyle( - color: - Theme.of(context).colorScheme.secondary) - : null), - subtitle: Text(processArtist( - _queue?[actualIndex].item.artist, - context)), - onTap: () async => - await _audioHandler.skipByOffset(index+1), - ), + final indexOffset = index + 1; + return ReorderableDelayedDragStartListener( + key: ValueKey("${_queue![actualIndex].item.id}$actualIndex-drag"), + index: index, + child: Dismissible( + key: ValueKey(_queue![actualIndex].item.id + actualIndex.toString()), + direction: + FinampSettingsHelper.finampSettings.disableGesture + ? DismissDirection.none + : DismissDirection.horizontal, + onDismissed: (direction) async { + await _queueService.removeAtOffset(indexOffset); + }, + child: Card( + child: ListTile( + leading: AlbumImage( + item: _queue?[actualIndex].item + .extras?["itemJson"] == + null + ? null + : jellyfin_models.BaseItemDto.fromJson(_queue?[actualIndex].item.extras?["itemJson"]), + ), + title: Text( + _queue?[actualIndex].item.title ?? + AppLocalizations.of(context)!.unknownName, + style: _currentTrack == + _queue?[actualIndex] + ? TextStyle( + color: + Theme.of(context).colorScheme.secondary) + : null), + subtitle: Text(processArtist( + _queue?[actualIndex].item.artist, + context)), + onTap: () async => + await _queueService.skipByOffset(indexOffset), + ), + ) + ) ); }, ), diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 639fc8c51..4696973cc 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -285,6 +285,31 @@ class QueueService { } } + Future skipByOffset(int offset) async { + + await _audioHandler.skipByOffset(offset); + + } + + Future removeAtOffset(int offset) async { + + final index = _playbackOrder == PlaybackOrder.shuffled ? _queueAudioSource.shuffleIndices[_queueAudioSource.shuffleIndices.indexOf((_queueAudioSourceIndex)) + offset] : (_queueAudioSourceIndex) + offset; + + await _audioHandler.removeQueueItemAt(index); + _queueFromConcatenatingAudioSource(); + + } + + Future reorderByOffset(int oldOffset, int newOffset) async { + + final oldIndex = _playbackOrder == PlaybackOrder.shuffled ? _queueAudioSource.shuffleIndices[_queueAudioSource.shuffleIndices.indexOf((_queueAudioSourceIndex)) + oldOffset] : (_queueAudioSourceIndex) + oldOffset; + final newIndex = _playbackOrder == PlaybackOrder.shuffled ? _queueAudioSource.shuffleIndices[_queueAudioSource.shuffleIndices.indexOf((_queueAudioSourceIndex)) + newOffset] : (_queueAudioSourceIndex) + newOffset; + + await _audioHandler.reorderQueue(oldIndex, newIndex); + _queueFromConcatenatingAudioSource(); + + } + QueueInfo getQueue() { return QueueInfo( From 33df18a2aedec2384c48bfad98eb07dc9c5112b0 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Wed, 7 Jun 2023 23:38:27 +0200 Subject: [PATCH 029/130] improved queue list - proper reordering support (between lists!) - current track is fixed - lists can't be dragged anymore - previous tracks collapsed by default --- lib/components/PlayerScreen/queue_list.dart | 371 ++++++++++++-------- lib/services/queue_service.dart | 2 + pubspec.lock | 8 + pubspec.yaml | 1 + 4 files changed, 231 insertions(+), 151 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 5909f3bd1..3f97b5a1e 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -1,8 +1,10 @@ import 'package:audio_service/audio_service.dart'; +import 'package:drag_and_drop_lists/drag_and_drop_list_interface.dart'; import 'package:finamp/models/finamp_models.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; +import 'package:drag_and_drop_lists/drag_and_drop_lists.dart'; import 'package:get_it/get_it.dart'; import 'package:rxdart/rxdart.dart'; @@ -40,6 +42,40 @@ class _QueueListState extends State { QueueItem? _currentTrack; List? _queue; + late List _contents; + + @override + void initState() { + super.initState(); + + _contents = [ + DragAndDropListExpansion( + listKey: const ObjectKey(0), + title: const Text("Previous Tracks"), + leading: const Icon(TablerIcons.history), + disableTopAndBottomBorders: true, + canDrag: false, + children: [], + ), + DragAndDropList( + header: const ListTile( + leading: Icon(TablerIcons.music), + title: Text("Current Track"), + ), + canDrag: false, + children: [], + ), + DragAndDropList( + header: const ListTile( + leading: Icon(TablerIcons.layout_list), + title: Text("Queue"), + ), + canDrag: false, + children: [], + ), + ]; + } + @override Widget build(BuildContext context) { return StreamBuilder<_QueueListStreamState>( @@ -72,173 +108,165 @@ class _QueueListState extends State { // } } // scroll to current track after sheet has been opened - WidgetsBinding.instance - .addPostFrameCallback((_) => scrollToCurrentTrack()); + //TODO fix this + // WidgetsBinding.instance + // .addPostFrameCallback((_) => scrollToCurrentTrack()); - return CustomScrollView( - controller: widget.scrollController, - slivers: [ - // const SliverPadding(padding: EdgeInsets.only(top: 0)), - // Previous Tracks - SliverReorderableList( - itemCount: _previousTracks?.length ?? 0, - onReorder: (oldIndex, newIndex) async { - final oldOffset = -((_previousTracks?.length ?? 0) - oldIndex); - final newOffset = -((_previousTracks?.length ?? 0) - newIndex); - // setState(() { - // // _previousTracks?.insert(newIndex, _previousTracks![oldIndex]); - // // _previousTracks?.removeAt(oldIndex); - // int? smallerThanNewIndex; - // if (oldIndex < newIndex) { - // // When we're moving an item backwards, we need to reduce - // // newIndex by 1 to account for there being a new item added - // // before newIndex. - // smallerThanNewIndex = newIndex - 1; - // } - // final item = _previousTracks?.removeAt(oldIndex); - // _previousTracks?.insert(smallerThanNewIndex ?? newIndex, item!); - // }); - await _queueService.reorderByOffset(oldOffset, newOffset); - }, - itemBuilder: (context, index) { - final actualIndex = index; - final indexOffset = -((_previousTracks?.length ?? 0) - index); - return ReorderableDelayedDragStartListener( - index: index, - key: ValueKey("${_previousTracks![actualIndex].item.id}$actualIndex-drag"), - child: Dismissible( - key: ValueKey(_previousTracks![actualIndex].item.id + actualIndex.toString()), - direction: - FinampSettingsHelper.finampSettings.disableGesture - ? DismissDirection.none - : DismissDirection.horizontal, - onDismissed: (direction) async { - await _queueService.removeAtOffset(indexOffset); - }, - child: Card( - child: ListTile( - leading: AlbumImage( - item: _previousTracks?[actualIndex].item - .extras?["itemJson"] == - null + _contents = [ + //TODO save this as a variable so that a pseudo footer can be added that will call `toggleExpanded()` on the list + DragAndDropListExpansion( + listKey: const ObjectKey(0), + title: const Text("Previous Tracks"), + // subtitle: Text('Subtitle ${innerList.name}'), + // trailing: Text("Previous Tracks"), + leading: const Icon(TablerIcons.history), + disableTopAndBottomBorders: true, + canDrag: false, + children: _previousTracks?.asMap().entries.map((e) { + final index = e.key; + final item = e.value; + final actualIndex = index; + final indexOffset = -((_previousTracks?.length ?? 0) - index); + + return DragAndDropItem(child: Card( + child: ListTile( + leading: AlbumImage( + item: item.item + .extras?["itemJson"] == null + ? null + : jellyfin_models.BaseItemDto.fromJson(item.item.extras?["itemJson"]), + ), + title: Text( + item.item.title ?? AppLocalizations.of(context)!.unknownName, + style: _currentTrack == item + ? TextStyle( + color: + Theme.of(context).colorScheme.secondary) + : null), + subtitle: Text(processArtist( + item.item.artist, + context)), + onTap: () async => + await _queueService.skipByOffset(indexOffset), + ), + )); + }).toList() ?? [], + ), + DragAndDropList( + header: const ListTile( + leading: Icon(TablerIcons.music), + title: Text("Current Track"), + ), + canDrag: false, + children: [ + DragAndDropItem( + canDrag: false, + child: Card( + key: currentTrackKey, + child: ListTile( + leading: AlbumImage( + item: _currentTrack!.item + .extras?["itemJson"] == null ? null - : jellyfin_models.BaseItemDto.fromJson(_previousTracks?[actualIndex].item.extras?["itemJson"]), - ), - title: Text( - _previousTracks?[actualIndex].item.title ?? - AppLocalizations.of(context)!.unknownName, - style: _currentTrack == - _previousTracks?[actualIndex] + : jellyfin_models.BaseItemDto.fromJson(_currentTrack!.item.extras?["itemJson"]), + ), + title: Text( + _currentTrack!.item.title ?? AppLocalizations.of(context)!.unknownName, + style: _currentTrack == _currentTrack! ? TextStyle( color: Theme.of(context).colorScheme.secondary) : null), - subtitle: Text(processArtist( - _previousTracks?[actualIndex].item.artist, - context)), - onTap: () async => - await _queueService.skipByOffset(indexOffset), - ), - ) - ) - ); - }, + subtitle: Text(processArtist( + _currentTrack!.item.artist, + context)), + onTap: () async => + snapshot.data!.mediaState.playbackState.playing ? await _audioHandler.pause() : await _audioHandler.play(), + ), + ) + ) + ] + ), + DragAndDropList( + header: const ListTile( + leading: Icon(TablerIcons.layout_list), + title: Text("Queue"), ), - // Current Track - SliverAppBar( - key: currentTrackKey, - pinned: true, - collapsedHeight: 70.0, - expandedHeight: 70.0, - leading: const Padding( - padding: EdgeInsets.zero, - ), - flexibleSpace: ListTile( + canDrag: false, + children: _queue?.asMap().entries.map((e) { + final index = e.key; + final item = e.value; + // final actualIndex = index; + // final indexOffset = -((_previousTracks?.length ?? 0) - index); + final actualIndex = index; + final indexOffset = index + 1; + + return DragAndDropItem(child: Card( + child: ListTile( leading: AlbumImage( - item: _currentTrack!.item - .extras?["itemJson"] == - null - ? null - : jellyfin_models.BaseItemDto.fromJson(_currentTrack!.item.extras?["itemJson"]), + item: item.item + .extras?["itemJson"] == null + ? null + : jellyfin_models.BaseItemDto.fromJson(item.item.extras?["itemJson"]), ), title: Text( - _currentTrack?.item.title ?? - AppLocalizations.of(context)!.unknownName, - style: TextStyle( + item.item.title ?? AppLocalizations.of(context)!.unknownName, + style: _currentTrack == item + ? TextStyle( color: Theme.of(context).colorScheme.secondary) - ), + : null), subtitle: Text(processArtist( - _currentTrack!.item.artist, + item.item.artist, context)), onTap: () async => - snapshot.data!.mediaState.playbackState.playing ? await _audioHandler.pause() : await _audioHandler.play(), - ) - ), - // Queue - SliverReorderableList( - itemCount: _queue?.length ?? 0, - onReorder: (oldIndex, newIndex) async { - final oldOffset = oldIndex + 1; - final newOffset = newIndex + 1; - setState(() { - // _queue?.insert(newIndex, _queue![oldIndex]); - // _queue?.removeAt(oldIndex); - int? smallerThanNewIndex; - if (oldIndex < newIndex) { - // When we're moving an item backwards, we need to reduce - // newIndex by 1 to account for there being a new item added - // before newIndex. - smallerThanNewIndex = newIndex - 1; - } - final item = _queue?.removeAt(oldIndex); - _queue?.insert(smallerThanNewIndex ?? newIndex, item!); - }); - await _queueService.reorderByOffset(oldOffset, newOffset); - }, - itemBuilder: (context, index) { - final actualIndex = index; - final indexOffset = index + 1; - return ReorderableDelayedDragStartListener( - key: ValueKey("${_queue![actualIndex].item.id}$actualIndex-drag"), - index: index, - child: Dismissible( - key: ValueKey(_queue![actualIndex].item.id + actualIndex.toString()), - direction: - FinampSettingsHelper.finampSettings.disableGesture - ? DismissDirection.none - : DismissDirection.horizontal, - onDismissed: (direction) async { - await _queueService.removeAtOffset(indexOffset); - }, - child: Card( - child: ListTile( - leading: AlbumImage( - item: _queue?[actualIndex].item - .extras?["itemJson"] == - null - ? null - : jellyfin_models.BaseItemDto.fromJson(_queue?[actualIndex].item.extras?["itemJson"]), - ), - title: Text( - _queue?[actualIndex].item.title ?? - AppLocalizations.of(context)!.unknownName, - style: _currentTrack == - _queue?[actualIndex] - ? TextStyle( - color: - Theme.of(context).colorScheme.secondary) - : null), - subtitle: Text(processArtist( - _queue?[actualIndex].item.artist, - context)), - onTap: () async => - await _queueService.skipByOffset(indexOffset), - ), - ) - ) - ); + await _queueService.skipByOffset(indexOffset), + ), + )); + }).toList() ?? [], + ), + ]; + + return CustomScrollView( + controller: widget.scrollController, + slivers: [ + // const SliverPadding(padding: EdgeInsets.only(top: 0)), + DragAndDropLists( + children: _contents, + onItemReorder: _onItemReorder, + onListReorder: _onListReorder, + itemOnWillAccept: (draggingItem, targetItem) { + if (targetItem.child.key == currentTrackKey) { + return false; + } + return true; }, + itemDragOnLongPress: true, + sliverList: true, + scrollController: widget.scrollController, + itemDragHandle: const DragHandle( + child: Padding( + padding: EdgeInsets.only(right: 15), + child: Icon( + TablerIcons.menu, + color: Colors.grey, + ), + ), + ), + // mandatory, not actually needed because lists can't be dragged + listGhost: Padding( + padding: const EdgeInsets.symmetric(vertical: 30.0), + child: Center( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 30.0, horizontal: 100.0), + decoration: BoxDecoration( + border: Border.all(), + borderRadius: BorderRadius.circular(7.0), + ), + child: const Icon(Icons.add_box), + ), + ), + ), ), ], ); @@ -251,6 +279,47 @@ class _QueueListState extends State { }, ); } + + _onItemReorder(int oldItemIndex, int oldListIndex, int newItemIndex, int newListIndex) { + + setState(() { + var movedItem = _contents[oldListIndex].children!.removeAt(oldItemIndex); + _contents[newListIndex].children!.insert(newItemIndex, movedItem); + }); + + int oldOffset = 0; + int newOffset = 0; + + if (oldListIndex == newListIndex) { + if (oldListIndex == 0) { + // previous tracks + oldOffset = -((_previousTracks?.length ?? 0) - oldItemIndex); + newOffset = -((_previousTracks?.length ?? 0) - newItemIndex); + } else if (oldListIndex == _contents.length - 1) { + // queue + oldOffset = oldItemIndex + 1; + newOffset = newItemIndex + 1; + } + } else { + if (oldListIndex == 0) { + // previous tracks to queue + oldOffset = -((_previousTracks?.length ?? 0) - oldItemIndex); + newOffset = newItemIndex + 1; + } else if (oldListIndex == _contents.length - 1) { + // queue to previous tracks + oldOffset = oldItemIndex + 1; + newOffset = -((_previousTracks?.length ?? 0) - newItemIndex); + } + } + + _queueService.reorderByOffset(oldOffset, newOffset); + + } + + _onListReorder(int oldListIndex, int newListIndex) { + return false; + } + } Future showQueueBottomSheet(BuildContext context) { diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 4696973cc..7b7995373 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -302,6 +302,8 @@ class QueueService { Future reorderByOffset(int oldOffset, int newOffset) async { + _queueServiceLogger.fine("Reordering queue item at offset $oldOffset to offset $newOffset"); + final oldIndex = _playbackOrder == PlaybackOrder.shuffled ? _queueAudioSource.shuffleIndices[_queueAudioSource.shuffleIndices.indexOf((_queueAudioSourceIndex)) + oldOffset] : (_queueAudioSourceIndex) + oldOffset; final newIndex = _playbackOrder == PlaybackOrder.shuffled ? _queueAudioSource.shuffleIndices[_queueAudioSource.shuffleIndices.indexOf((_queueAudioSourceIndex)) + newOffset] : (_queueAudioSourceIndex) + newOffset; diff --git a/pubspec.lock b/pubspec.lock index 1deb72d79..9e288f7b2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -289,6 +289,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + drag_and_drop_lists: + dependency: "direct main" + description: + name: drag_and_drop_lists + sha256: "9a14595f9880be7953f23578aef88c15cc9b367eeb39dbc1f1bd6af52f70872b" + url: "https://pub.dev" + source: hosted + version: "0.3.3" equatable: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e7f776ce2..6f60f8cb1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,7 @@ dependencies: package_info_plus: ^3.1.0 octo_image: ^1.0.2 share_plus: ^6.3.2 + drag_and_drop_lists: ^0.3.3 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. From c10ac61dd7a891c4381b1dbd71bc1e67f6383813 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 9 Jun 2023 10:17:03 +0200 Subject: [PATCH 030/130] working next up logic - add songs to next up (prepend and append) - reorder songs - show correct source --- .../AlbumScreen/song_list_tile.dart | 39 +++++++ lib/components/PlayerScreen/queue_list.dart | 100 ++++++++++++++---- lib/screens/player_screen.dart | 36 +++---- lib/services/queue_service.dart | 23 ++-- 4 files changed, 152 insertions(+), 46 deletions(-) diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index c3b289f7d..2d2a4ad5e 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -3,6 +3,7 @@ import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get_it/get_it.dart'; import '../../models/jellyfin_models.dart'; @@ -22,6 +23,8 @@ import 'downloaded_indicator.dart'; enum SongListTileMenuItems { addToQueue, + playNext, + addToNextUp, replaceQueueWithItem, addToPlaylist, removeFromPlaylist, @@ -225,6 +228,20 @@ class _SongListTileState extends State { title: Text(AppLocalizations.of(context)!.addToQueue), ), ), + PopupMenuItem( + value: SongListTileMenuItems.playNext, + child: ListTile( + leading: const Icon(TablerIcons.hourglass_low), + title: Text("Play next"), + ), + ), + PopupMenuItem( + value: SongListTileMenuItems.addToNextUp, + child: ListTile( + leading: const Icon(TablerIcons.hourglass_high), + title: Text("Add to Next Up"), + ), + ), PopupMenuItem( value: SongListTileMenuItems.replaceQueueWithItem, child: ListTile( @@ -304,6 +321,28 @@ class _SongListTileState extends State { )); break; + case SongListTileMenuItems.playNext: + // await _audioServiceHelper.addQueueItem(widget.item); + await _queueService.addNext(widget.item); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text("Track will play next"), + )); + break; + + case SongListTileMenuItems.addToNextUp: + // await _audioServiceHelper.addQueueItem(widget.item); + await _queueService.addToNextUp(widget.item); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text("Added track to Next Up"), + )); + break; + case SongListTileMenuItems.replaceQueueWithItem: await _audioServiceHelper .replaceQueueWithItem(itemList: [widget.item]); diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 3f97b5a1e..5fe30c1e5 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -40,6 +40,7 @@ class _QueueListState extends State { final _queueService = GetIt.instance(); List? _previousTracks; QueueItem? _currentTrack; + List? _nextUp; List? _queue; late List _contents; @@ -65,6 +66,14 @@ class _QueueListState extends State { canDrag: false, children: [], ), + DragAndDropList( + header: const ListTile( + leading: Icon(TablerIcons.layout_list), + title: Text("Next Up"), + ), + canDrag: false, + children: [], + ), DragAndDropList( header: const ListTile( leading: Icon(TablerIcons.layout_list), @@ -90,6 +99,7 @@ class _QueueListState extends State { _previousTracks ??= snapshot.data!.queueInfo.previousTracks; _currentTrack = snapshot.data!.queueInfo.currentTrack ?? QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)); + _nextUp ??= snapshot.data!.queueInfo.nextUp; _queue ??= snapshot.data!.queueInfo.queue; final GlobalKey currentTrackKey = GlobalKey(debugLabel: "currentTrack"); @@ -187,6 +197,45 @@ class _QueueListState extends State { ) ] ), + if (_nextUp!.isNotEmpty) + DragAndDropList( + header: const ListTile( + leading: Icon(TablerIcons.layout_list), + title: Text("Next Up"), + ), + canDrag: false, + children: _nextUp?.asMap().entries.map((e) { + final index = e.key; + final item = e.value; + // final actualIndex = index; + // final indexOffset = -((_previousTracks?.length ?? 0) - index); + final actualIndex = index; + final indexOffset = index + 1; + + return DragAndDropItem(child: Card( + child: ListTile( + leading: AlbumImage( + item: item.item + .extras?["itemJson"] == null + ? null + : jellyfin_models.BaseItemDto.fromJson(item.item.extras?["itemJson"]), + ), + title: Text( + item.item.title ?? AppLocalizations.of(context)!.unknownName, + style: _currentTrack == item + ? TextStyle( + color: + Theme.of(context).colorScheme.secondary) + : null), + subtitle: Text(processArtist( + item.item.artist, + context)), + onTap: () async => + await _queueService.skipByOffset(indexOffset), + ), + )); + }).toList() ?? [], + ), DragAndDropList( header: const ListTile( leading: Icon(TablerIcons.layout_list), @@ -198,8 +247,8 @@ class _QueueListState extends State { final item = e.value; // final actualIndex = index; // final indexOffset = -((_previousTracks?.length ?? 0) - index); - final actualIndex = index; - final indexOffset = index + 1; + final actualIndex = index + _nextUp!.length; + final indexOffset = index + _nextUp!.length + 1; return DragAndDropItem(child: Card( child: ListTile( @@ -236,6 +285,7 @@ class _QueueListState extends State { onItemReorder: _onItemReorder, onListReorder: _onListReorder, itemOnWillAccept: (draggingItem, targetItem) { + //TODO this isn't working properly if (targetItem.child.key == currentTrackKey) { return false; } @@ -290,27 +340,35 @@ class _QueueListState extends State { int oldOffset = 0; int newOffset = 0; - if (oldListIndex == newListIndex) { - if (oldListIndex == 0) { - // previous tracks - oldOffset = -((_previousTracks?.length ?? 0) - oldItemIndex); - newOffset = -((_previousTracks?.length ?? 0) - newItemIndex); - } else if (oldListIndex == _contents.length - 1) { - // queue - oldOffset = oldItemIndex + 1; - newOffset = newItemIndex + 1; - } + // old index + if (oldListIndex == 0) { + // previous tracks + oldOffset = -((_previousTracks?.length ?? 0) - oldItemIndex); + } else if (oldListIndex == 2) { + // next up + oldOffset = oldItemIndex + 1; + } else if (oldListIndex == _contents.length - 1) { + // queue + oldOffset = oldItemIndex + _nextUp!.length + 1; + } + + // new index + if (newListIndex == 0) { + // previous tracks + newOffset = -((_previousTracks?.length ?? 0) - newItemIndex); + } else if ( + newListIndex == 2 && + oldListIndex == 2 // tracks can't be moved *to* next up, only *within* next up or *out of* next up + ) { + // next up + newOffset = newItemIndex + 1; + } else if (newListIndex == _contents.length -1) { + // queue + newOffset = newItemIndex + _nextUp!.length + 1; } else { - if (oldListIndex == 0) { - // previous tracks to queue - oldOffset = -((_previousTracks?.length ?? 0) - oldItemIndex); - newOffset = newItemIndex + 1; - } else if (oldListIndex == _contents.length - 1) { - // queue to previous tracks - oldOffset = oldItemIndex + 1; - newOffset = -((_previousTracks?.length ?? 0) - newItemIndex); - } + newOffset = oldOffset; } + _queueService.reorderByOffset(oldOffset, newOffset); diff --git a/lib/screens/player_screen.dart b/lib/screens/player_screen.dart index 782937665..f342b4105 100644 --- a/lib/screens/player_screen.dart +++ b/lib/screens/player_screen.dart @@ -30,25 +30,25 @@ class PlayerScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final imageTheme = ref.watch(playerScreenThemeProvider); - return SimpleGestureDetector( - onVerticalSwipe: (direction) { - if (!FinampSettingsHelper.finampSettings.disableGesture) { - if (direction == SwipeDirection.down) { - Navigator.of(context).pop(); - } else if (direction == SwipeDirection.up) { - showQueueBottomSheet(context); - } - } - }, - child: Theme( - data: ThemeData( - fontFamily: "LexendDeca", - colorScheme: imageTheme, - brightness: Theme.of(context).brightness, - iconTheme: Theme.of(context).iconTheme.copyWith( - color: imageTheme?.primary, - ), + return Theme( + data: ThemeData( + fontFamily: "LexendDeca", + colorScheme: imageTheme, + brightness: Theme.of(context).brightness, + iconTheme: Theme.of(context).iconTheme.copyWith( + color: imageTheme?.primary, ), + ), + child: SimpleGestureDetector( + onVerticalSwipe: (direction) { + if (!FinampSettingsHelper.finampSettings.disableGesture) { + if (direction == SwipeDirection.down) { + Navigator.of(context).pop(); + } else if (direction == SwipeDirection.up) { + showQueueBottomSheet(context); + } + } + }, child: Scaffold( appBar: AppBar( backgroundColor: Colors.transparent, diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 7b7995373..c0002de75 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -102,8 +102,9 @@ class QueueService { if (i < adjustedQueueIndex) { _queuePreviousTracks.add(allTracks[i]); - if (_queuePreviousTracks.last.type == QueueItemQueueType.nextUp) { - _queuePreviousTracks.last.source.type = QueueItemSourceType.formerNextUp; + _queueServiceLogger.finer("Last type: ${_queuePreviousTracks.last.type}"); + if (_queuePreviousTracks.last.source.type == QueueItemSourceType.nextUp) { + _queuePreviousTracks.last.source = QueueItemSource(type: QueueItemSourceType.formerNextUp, name: "Tracks added via Next Up", id: "former-next-up"); } _queuePreviousTracks.last.type = QueueItemQueueType.previousTracks; } else if (i == adjustedQueueIndex) { @@ -233,10 +234,12 @@ class QueueService { // _order.linearOrder.add(_order.items.length - 1); // _order.shuffledOrder.add(_order.items.length - 1); //TODO maybe the item should be shuffled into the queue instead of placed at the end? depends on user preference - _queueAudioSource.add(await _queueItemToAudioSource(queueItem)); + await _queueAudioSource.add(await _queueItemToAudioSource(queueItem)); _queueServiceLogger.fine("Added '${queueItem.item.title}' to queue from '${source.name}' (${source.type})"); + _queueFromConcatenatingAudioSource(); // update internal queues + } catch (e) { _queueServiceLogger.severe(e); return Future.error(e); @@ -247,15 +250,17 @@ class QueueService { try { QueueItem queueItem = QueueItem( item: await _generateMediaItem(item), - source: QueueItemSource(id: "up-next", name: "Up Next", type: QueueItemSourceType.nextUp), + source: QueueItemSource(id: "next-up", name: "Next Up", type: QueueItemSourceType.nextUp), type: QueueItemQueueType.nextUp, ); // don't add to _order, because it wasn't added to the regular queue - _queueAudioSource.insert(_queueAudioSourceIndex+1, await _queueItemToAudioSource(queueItem)); + await _queueAudioSource.insert(_queueAudioSourceIndex+1, await _queueItemToAudioSource(queueItem)); _queueServiceLogger.fine("Prepended '${queueItem.item.title}' to Next Up"); + _queueFromConcatenatingAudioSource(); // update internal queues + } catch (e) { _queueServiceLogger.severe(e); return Future.error(e); @@ -266,7 +271,7 @@ class QueueService { try { QueueItem queueItem = QueueItem( item: await _generateMediaItem(item), - source: QueueItemSource(id: "up-next", name: "Up Next", type: QueueItemSourceType.nextUp), + source: QueueItemSource(id: "next-up", name: "Next Up", type: QueueItemSourceType.nextUp), type: QueueItemQueueType.nextUp, ); @@ -275,10 +280,12 @@ class QueueService { _queueFromConcatenatingAudioSource(); // update internal queues int offset = _queueNextUp.length; - _queueAudioSource.insert(_queueAudioSourceIndex+1+offset, await _queueItemToAudioSource(queueItem)); + await _queueAudioSource.insert(_queueAudioSourceIndex+1+offset, await _queueItemToAudioSource(queueItem)); _queueServiceLogger.fine("Appended '${queueItem.item.title}' to Next Up"); + _queueFromConcatenatingAudioSource(); // update internal queues + } catch (e) { _queueServiceLogger.severe(e); return Future.error(e); @@ -302,6 +309,8 @@ class QueueService { Future reorderByOffset(int oldOffset, int newOffset) async { + //TODO reordering while shuffle is active doesn't work and causes tracks to be completely shuffled again + _queueServiceLogger.fine("Reordering queue item at offset $oldOffset to offset $newOffset"); final oldIndex = _playbackOrder == PlaybackOrder.shuffled ? _queueAudioSource.shuffleIndices[_queueAudioSource.shuffleIndices.indexOf((_queueAudioSourceIndex)) + oldOffset] : (_queueAudioSourceIndex) + oldOffset; From 1cf3c4726d15cdcc29f3b189e514a9e754f91326 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 9 Jun 2023 21:55:41 +0200 Subject: [PATCH 031/130] fix reorder if shuffle is active --- lib/services/queue_service.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index c0002de75..a0d22b9b3 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -313,8 +313,9 @@ class QueueService { _queueServiceLogger.fine("Reordering queue item at offset $oldOffset to offset $newOffset"); - final oldIndex = _playbackOrder == PlaybackOrder.shuffled ? _queueAudioSource.shuffleIndices[_queueAudioSource.shuffleIndices.indexOf((_queueAudioSourceIndex)) + oldOffset] : (_queueAudioSourceIndex) + oldOffset; - final newIndex = _playbackOrder == PlaybackOrder.shuffled ? _queueAudioSource.shuffleIndices[_queueAudioSource.shuffleIndices.indexOf((_queueAudioSourceIndex)) + newOffset] : (_queueAudioSourceIndex) + newOffset; + //!!! the player will automatically change the shuffle indices of the ConcatenatingAudioSource if shuffle is enabled, so we need to use the regular track index here + final oldIndex = _queueAudioSourceIndex + oldOffset; + final newIndex = _queueAudioSourceIndex + newOffset; await _audioHandler.reorderQueue(oldIndex, newIndex); _queueFromConcatenatingAudioSource(); From a2ad87b1bc294e3f870412d0f9bc76477958a679 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 9 Jun 2023 22:42:44 +0200 Subject: [PATCH 032/130] fix reordering issues --- lib/components/PlayerScreen/queue_list.dart | 16 ++++++++-------- lib/components/favourite_button.dart | 4 ++-- lib/models/finamp_models.dart | 2 +- lib/services/music_player_background_task.dart | 9 ++------- lib/services/queue_service.dart | 11 +++++++---- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 5fe30c1e5..0c0c523b8 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -330,12 +330,7 @@ class _QueueListState extends State { ); } - _onItemReorder(int oldItemIndex, int oldListIndex, int newItemIndex, int newListIndex) { - - setState(() { - var movedItem = _contents[oldListIndex].children!.removeAt(oldItemIndex); - _contents[newListIndex].children!.insert(newItemIndex, movedItem); - }); + _onItemReorder(int oldItemIndex, int oldListIndex, int newItemIndex, int newListIndex) async { int oldOffset = 0; int newOffset = 0; @@ -369,8 +364,13 @@ class _QueueListState extends State { newOffset = oldOffset; } - - _queueService.reorderByOffset(oldOffset, newOffset); + if (oldOffset != newOffset) { + // setState(() { + // var movedItem = _contents[oldListIndex].children!.removeAt(oldItemIndex); + // _contents[newListIndex].children!.insert(newItemIndex, movedItem); + // }); + await _queueService.reorderByOffset(oldOffset, newOffset); + } } diff --git a/lib/components/favourite_button.dart b/lib/components/favourite_button.dart index 194b68384..847a049f0 100644 --- a/lib/components/favourite_button.dart +++ b/lib/components/favourite_button.dart @@ -35,7 +35,7 @@ class _FavoriteButtonState extends State { if (widget.onlyIfFav) { return Icon( isFav ? Icons.favorite : null, - color: Colors.red, + color: Theme.of(context).colorScheme.secondary, size: 24.0, semanticLabel: AppLocalizations.of(context)!.favourite, ); @@ -43,7 +43,7 @@ class _FavoriteButtonState extends State { return IconButton( icon: Icon( isFav ? Icons.favorite : Icons.favorite_outline, - color: isFav ? Colors.red : null, + color: isFav ? Theme.of(context).colorScheme.secondary : null, size: 24.0, ), tooltip: AppLocalizations.of(context)!.favourite, diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 261a0326b..5be475aaf 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -569,7 +569,7 @@ enum QueueItemSourceType { genre(name: "Genre"), artist(name: "Artist"), nextUp(name: ""), - formerNextUp(name: "Track added to Up Next"), + formerNextUp(name: ""), downloads(name: ""), unknown(name: ""); diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index 2997b25f8..94622e267 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -491,14 +491,9 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } Future reorderQueue(int oldIndex, int newIndex) async { - // When we're moving an item forwards, we need to reduce newIndex by 1 - // to account for the current item being removed before re-insertion. - if (oldIndex < newIndex) { - newIndex -= 1; - } await _queueAudioSource.move(oldIndex, newIndex); - queue.add(_queueFromSource()); - _audioServiceBackgroundTaskLogger.log(Level.INFO, "Published queue"); + // queue.add(_queueFromSource()); + // _audioServiceBackgroundTaskLogger.log(Level.INFO, "Published queue"); } /// Sets the sleep timer with the given [duration]. diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index a0d22b9b3..73f6880b5 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -309,8 +309,6 @@ class QueueService { Future reorderByOffset(int oldOffset, int newOffset) async { - //TODO reordering while shuffle is active doesn't work and causes tracks to be completely shuffled again - _queueServiceLogger.fine("Reordering queue item at offset $oldOffset to offset $newOffset"); //!!! the player will automatically change the shuffle indices of the ConcatenatingAudioSource if shuffle is enabled, so we need to use the regular track index here @@ -389,7 +387,12 @@ class QueueService { for (QueueItem queueItem in _queuePreviousTracks) { queueString += "${queueItem.item.title}, "; } - queueString += "[${_currentTrack?.item.title}], "; + queueString += "[[${_currentTrack?.item.title}]], "; + queueString += "{"; + for (QueueItem queueItem in _queueNextUp) { + queueString += "${queueItem.item.title}, "; + } + queueString += "} "; for (QueueItem queueItem in _queue) { queueString += "${queueItem.item.title}, "; } @@ -404,7 +407,7 @@ class QueueService { // log queues _queueServiceLogger.finer( - "Queue $message [${_queuePreviousTracks.length}-1-${_queue.length}]: $queueString" + "Queue $message [${_queuePreviousTracks.length}-1-${_queueNextUp.length}-${_queue.length}]: $queueString" ); // _queueServiceLogger.finer( // "Audio Source Queue $message [${_queue.length}]: $queueAudioSourceString" From bc1b75b63e1d521393546f285143816d94440699 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 10 Jun 2023 00:37:43 +0200 Subject: [PATCH 033/130] fix adding to Next Up when shuffle is enabled --- lib/components/PlayerScreen/queue_list.dart | 155 ++++++++++++++++++-- lib/models/finamp_models.dart | 12 +- lib/services/queue_service.dart | 52 ++++--- 3 files changed, 181 insertions(+), 38 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 0c0c523b8..893e82aff 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -42,6 +42,7 @@ class _QueueListState extends State { QueueItem? _currentTrack; List? _nextUp; List? _queue; + QueueItemSource? _source; late List _contents; @@ -101,6 +102,7 @@ class _QueueListState extends State { _currentTrack = snapshot.data!.queueInfo.currentTrack ?? QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)); _nextUp ??= snapshot.data!.queueInfo.nextUp; _queue ??= snapshot.data!.queueInfo.queue; + _source ??= snapshot.data!.queueInfo.source; final GlobalKey currentTrackKey = GlobalKey(debugLabel: "currentTrack"); @@ -139,8 +141,21 @@ class _QueueListState extends State { final indexOffset = -((_previousTracks?.length ?? 0) - index); return DragAndDropItem(child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), child: ListTile( + visualDensity: VisualDensity.compact, + minVerticalPadding: 0.0, + contentPadding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0), + tileColor: _currentTrack == item + ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) + : null, leading: AlbumImage( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(7.0), + bottomLeft: Radius.circular(7.0), + ), item: item.item .extras?["itemJson"] == null ? null @@ -149,16 +164,44 @@ class _QueueListState extends State { title: Text( item.item.title ?? AppLocalizations.of(context)!.unknownName, style: _currentTrack == item - ? TextStyle( - color: - Theme.of(context).colorScheme.secondary) - : null), + ? TextStyle( + color: + Theme.of(context).colorScheme.secondary) + : null), subtitle: Text(processArtist( item.item.artist, context)), + trailing: Container( + alignment: Alignment.centerRight, + margin: const EdgeInsets.only(right: 32.0), + width: 95.0, + height: 50.0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${item.item.duration?.inMinutes.toString().padLeft(2, '0')}:${((item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + textAlign: TextAlign.end, + style: TextStyle( + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + // IconButton( + // icon: const Icon(TablerIcons.dots_vertical), + // iconSize: 28.0, + // onPressed: () async => {}, + // ), + IconButton( + icon: const Icon(TablerIcons.x), + iconSize: 28.0, + onPressed: () async => await _queueService.removeAtOffset(indexOffset), + ), + ], + ), + ), onTap: () async => await _queueService.skipByOffset(indexOffset), - ), + ) )); }).toList() ?? [], ), @@ -213,8 +256,21 @@ class _QueueListState extends State { final indexOffset = index + 1; return DragAndDropItem(child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), child: ListTile( + visualDensity: VisualDensity.compact, + minVerticalPadding: 0.0, + contentPadding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0), + tileColor: _currentTrack == item + ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) + : null, leading: AlbumImage( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(7.0), + bottomLeft: Radius.circular(7.0), + ), item: item.item .extras?["itemJson"] == null ? null @@ -230,16 +286,45 @@ class _QueueListState extends State { subtitle: Text(processArtist( item.item.artist, context)), + trailing: Container( + alignment: Alignment.centerRight, + margin: const EdgeInsets.only(right: 32.0), + width: 95.0, + height: 50.0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${item.item.duration?.inMinutes.toString().padLeft(2, '0')}:${((item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + textAlign: TextAlign.end, + style: TextStyle( + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + // IconButton( + // icon: const Icon(TablerIcons.dots_vertical), + // iconSize: 28.0, + // onPressed: () async => {}, + // ), + IconButton( + icon: const Icon(TablerIcons.x), + iconSize: 28.0, + onPressed: () async => await _queueService.removeAtOffset(indexOffset), + ), + ], + ), + ), onTap: () async => await _queueService.skipByOffset(indexOffset), - ), + ) )); }).toList() ?? [], ), DragAndDropList( - header: const ListTile( - leading: Icon(TablerIcons.layout_list), - title: Text("Queue"), + contentsWhenEmpty: const Text("Queue is empty"), + header: ListTile( + leading: const Icon(TablerIcons.layout_list), + title: Text(_source?.name != null ? "Playing from ${_source?.name}" : "Queue"), ), canDrag: false, children: _queue?.asMap().entries.map((e) { @@ -251,8 +336,21 @@ class _QueueListState extends State { final indexOffset = index + _nextUp!.length + 1; return DragAndDropItem(child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), child: ListTile( + visualDensity: VisualDensity.compact, + minVerticalPadding: 0.0, + contentPadding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0), + tileColor: _currentTrack == item + ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) + : null, leading: AlbumImage( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(7.0), + bottomLeft: Radius.circular(7.0), + ), item: item.item .extras?["itemJson"] == null ? null @@ -268,6 +366,34 @@ class _QueueListState extends State { subtitle: Text(processArtist( item.item.artist, context)), + trailing: Container( + alignment: Alignment.centerRight, + margin: const EdgeInsets.only(right: 32.0), + width: 95.0, + height: 50.0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${item.item.duration?.inMinutes.toString().padLeft(2, '0')}:${((item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + textAlign: TextAlign.end, + style: TextStyle( + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + // IconButton( + // icon: const Icon(TablerIcons.dots_vertical), + // iconSize: 28.0, + // onPressed: () async => {}, + // ), + IconButton( + icon: const Icon(TablerIcons.x), + iconSize: 28.0, + onPressed: () async => await _queueService.removeAtOffset(indexOffset), + ), + ], + ), + ), onTap: () async => await _queueService.skipByOffset(indexOffset), ), @@ -281,6 +407,8 @@ class _QueueListState extends State { slivers: [ // const SliverPadding(padding: EdgeInsets.only(top: 0)), DragAndDropLists( + listPadding: const EdgeInsets.only(top: 0.0), + children: _contents, onItemReorder: _onItemReorder, onListReorder: _onListReorder, @@ -294,12 +422,13 @@ class _QueueListState extends State { itemDragOnLongPress: true, sliverList: true, scrollController: widget.scrollController, - itemDragHandle: const DragHandle( + itemDragHandle: DragHandle( child: Padding( - padding: EdgeInsets.only(right: 15), + padding: const EdgeInsets.only(right: 20.0), child: Icon( - TablerIcons.menu, - color: Colors.grey, + TablerIcons.grip_horizontal, + color: IconTheme.of(context).color, + size: 28.0, ), ), ), diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 5be475aaf..3c5eae9e0 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -629,6 +629,7 @@ class QueueOrder { QueueOrder({ required this.items, + required this.originalSource, required this.linearOrder, required this.shuffledOrder, }); @@ -636,14 +637,17 @@ class QueueOrder { @HiveField(0) List items; + @HiveField(1) + QueueItemSource originalSource; + /// The linear order of the items in the queue. Used when shuffle is disabled. /// The integers at index x contains the index of the item within [items] at queue position x. - @HiveField(1) + @HiveField(2) List linearOrder; /// The shuffled order of the items in the queue. Used when shuffle is enabled. /// The integers at index x contains the index of the item within [items] at queue position x. - @HiveField(2) + @HiveField(3) List shuffledOrder; } @@ -655,6 +659,7 @@ class QueueInfo { required this.currentTrack, required this.nextUp, required this.queue, + required this.source, }); @HiveField(0) @@ -669,4 +674,7 @@ class QueueInfo { @HiveField(3) List queue; + @HiveField(4) + QueueItemSource source; + } diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 73f6880b5..ea3cb2db6 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -33,7 +33,7 @@ class QueueService { QueueItem? _currentTrack; // the currently playing track List _queueNextUp = []; // a temporary queue that gets appended to if the user taps "next up" List _queue = []; // contains all regular queue items - QueueOrder _order = QueueOrder(items: [], linearOrder: [], shuffledOrder: []); // contains all items that were at some point added to the regular queue, as well as their order when shuffle is enabled and disabled. This is used to loop the original queue once the end has been reached and "loop all" is enabled, **excluding** "next up" items and keeping the playback order. + QueueOrder _order = QueueOrder(items: [], originalSource: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown), linearOrder: [], shuffledOrder: []); // contains all items that were at some point added to the regular queue, as well as their order when shuffle is enabled and disabled. This is used to loop the original queue once the end has been reached and "loop all" is enabled, **excluding** "next up" items and keeping the playback order. PlaybackOrder _playbackOrder = PlaybackOrder.linear; LoopMode _loopMode = LoopMode.none; @@ -46,6 +46,7 @@ class QueueService { currentTrack: QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)), queue: [], nextUp: [], + source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown), )); // external queue state @@ -102,7 +103,6 @@ class QueueService { if (i < adjustedQueueIndex) { _queuePreviousTracks.add(allTracks[i]); - _queueServiceLogger.finer("Last type: ${_queuePreviousTracks.last.type}"); if (_queuePreviousTracks.last.source.type == QueueItemSourceType.nextUp) { _queuePreviousTracks.last.source = QueueItemSource(type: QueueItemSourceType.formerNextUp, name: "Tracks added via Next Up", id: "former-next-up"); } @@ -112,7 +112,17 @@ class QueueService { _currentTrack!.type = QueueItemQueueType.currentTrack; } else { if (allTracks[i].type == QueueItemQueueType.nextUp) { - _queueNextUp.add(allTracks[i]); + //TODO this *should* mark items from Next Up as formerNextUp when skipping backwards before Next Up is played, but it doesn't work for some reason + if ( + i == adjustedQueueIndex+1 || + i == adjustedQueueIndex+1 + _queueNextUp.length + ) { + _queueNextUp.add(allTracks[i]); + } else { + _queue.add(allTracks[i]); + _queue.last.type = QueueItemQueueType.queue; + _queuePreviousTracks.last.source = QueueItemSource(type: QueueItemSourceType.formerNextUp, name: "Tracks added via Next Up", id: "former-next-up"); + } } else { _queue.add(allTracks[i]); _queue.last.type = QueueItemQueueType.queue; @@ -202,6 +212,7 @@ class QueueService { _order = QueueOrder( items: newItems, + originalSource: source, linearOrder: newLinearOrder, shuffledOrder: newShuffledOrder, ); @@ -282,7 +293,7 @@ class QueueService { await _queueAudioSource.insert(_queueAudioSourceIndex+1+offset, await _queueItemToAudioSource(queueItem)); - _queueServiceLogger.fine("Appended '${queueItem.item.title}' to Next Up"); + _queueServiceLogger.fine("Appended '${queueItem.item.title}' to Next Up (index ${_queueAudioSourceIndex+1+offset})"); _queueFromConcatenatingAudioSource(); // update internal queues @@ -327,6 +338,7 @@ class QueueService { currentTrack: _currentTrack, queue: _queue, nextUp: _queueNextUp, + source: _order.originalSource, // nextUp: [ // QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)), // ], @@ -607,28 +619,22 @@ class NextUpShuffleOrder extends ShuffleOrder { @override void insert(int index, int count) { - int indicesOriginalLength = indices.length; // Offset indices after insertion point. - for (var i = 0; i < count; i++) { - indices.add(indices.length); + for (var i = 0; i < indices.length; i++) { + if (indices[i] >= index) { + indices[i] += count; + } } - - // if (indicesOriginalLength == 0) { - // _queueService!.queueServiceLogger.finest("count (before fixing first index): $count"); - // // log indices - // String indicesString = ""; - // for (int index in indices) { - // indicesString += "$index, "; - // } - // _queueService!.queueServiceLogger.finest("Shuffled indices (before fixing first index): $indicesString"); - // indices = indices..shuffle(); - // // log indices - // indicesString = ""; - // for (int index in indices) { - // indicesString += "$index, "; - // } - // _queueService!.queueServiceLogger.finest("Shuffled indices (after fixing first index): $indicesString"); + // // Insert new indices at random positions after currentIndex. + // final newIndices = List.generate(count, (i) => index + i); + // for (var newIndex in newIndices) { + // final insertionIndex = _random.nextInt(indices.length + 1); + // indices.insert(insertionIndex, newIndex); // } + + // Insert new indices at the specified position. + final newIndices = List.generate(count, (i) => index + i); + indices.insertAll(index, newIndices); } From 4248aae64e599595eb56d73a4b52d7a2a43adfcb Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 10 Jun 2023 00:40:58 +0200 Subject: [PATCH 034/130] small fixes --- lib/components/favourite_button.dart | 2 +- lib/models/finamp_models.dart | 2 +- lib/services/audio_service_helper.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/components/favourite_button.dart b/lib/components/favourite_button.dart index 847a049f0..1e20a41eb 100644 --- a/lib/components/favourite_button.dart +++ b/lib/components/favourite_button.dart @@ -35,7 +35,7 @@ class _FavoriteButtonState extends State { if (widget.onlyIfFav) { return Icon( isFav ? Icons.favorite : null, - color: Theme.of(context).colorScheme.secondary, + color: Colors.red, size: 24.0, semanticLabel: AppLocalizations.of(context)!.favourite, ); diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 3c5eae9e0..8ba262d2c 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -563,7 +563,7 @@ enum QueueItemSourceType { itemMix(name: "Song Mix"), artistMix(name: "Artist Mix"), albumMix(name: "Album Mix"), - favorites(name: "Your Likes"), + favorites(name: ""), songs(name: "All Songs"), filteredList(name: "Songs"), genre(name: "Genre"), diff --git a/lib/services/audio_service_helper.dart b/lib/services/audio_service_helper.dart index 1b699ebb2..ecafee8e9 100644 --- a/lib/services/audio_service_helper.dart +++ b/lib/services/audio_service_helper.dart @@ -121,7 +121,7 @@ class AudioServiceHelper { items: items, source: QueueItemSource( type: isFavourite ? QueueItemSourceType.favorites : QueueItemSourceType.songs, - name: "Shuffle All", + name: isFavourite ? "Your Likes" : "Shuffle All", id: "shuffleAll", ) ); From b59b21d62b1ad3c8abe66f63211a61d31bf64a80 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 10 Jun 2023 12:21:58 +0200 Subject: [PATCH 035/130] added playback history (service) - added new screen showing the history (click a track starts an instant mix) - added new service class for managing history and playback reporting --- .../playback_history_list.dart | 113 ++++++++++ lib/main.dart | 4 + lib/models/finamp_models.dart | 18 ++ lib/screens/music_screen.dart | 8 + lib/screens/playback_history_screen.dart | 33 +++ .../music_player_background_task.dart | 28 +-- lib/services/playback_history_service.dart | 193 ++++++++++++++++++ lib/services/queue_service.dart | 7 +- 8 files changed, 390 insertions(+), 14 deletions(-) create mode 100644 lib/components/PlaybackHistoryScreen/playback_history_list.dart create mode 100644 lib/screens/playback_history_screen.dart create mode 100644 lib/services/playback_history_service.dart diff --git a/lib/components/PlaybackHistoryScreen/playback_history_list.dart b/lib/components/PlaybackHistoryScreen/playback_history_list.dart new file mode 100644 index 000000000..0a6b0b707 --- /dev/null +++ b/lib/components/PlaybackHistoryScreen/playback_history_list.dart @@ -0,0 +1,113 @@ +import 'package:finamp/models/finamp_models.dart'; +import 'package:finamp/services/audio_service_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:get_it/get_it.dart'; + +import '../../services/playback_history_service.dart'; +import '../../models/jellyfin_models.dart' as jellyfin_models; +import '../album_image.dart'; +import '../../services/process_artist.dart'; + +class PlaybackHistoryList extends StatelessWidget { + const PlaybackHistoryList({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final playbackHistoryService = GetIt.instance(); + final audioServiceHelper = GetIt.instance(); + + List? history; + + return Scrollbar( + child: StreamBuilder>( + stream: playbackHistoryService.historyStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + + history = snapshot.data; + + return ListView.builder( + itemCount: history!.length, + reverse: true, + padding: const EdgeInsets.only(bottom: 160.0), + itemBuilder: (context, index) { + // return ListTile( + // title: Text(history![index].item.item.title), + // subtitle: Text(history![index].item.item.artist!), + // ); + return Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + child: ListTile( + visualDensity: VisualDensity.compact, + minVerticalPadding: 0.0, + contentPadding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0), + leading: AlbumImage( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(7.0), + bottomLeft: Radius.circular(7.0), + ), + item: history![index].item.item + .extras?["itemJson"] == null + ? null + : jellyfin_models.BaseItemDto.fromJson(history![index].item.item.extras?["itemJson"]), + ), + title: Text( + history![index].item.item.title, + ), + subtitle: Text(processArtist( + history![index].item.item.artist, + context)), + trailing: Container( + alignment: Alignment.centerRight, + margin: const EdgeInsets.only(right: 0.0), + width: 95.0, + height: 50.0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${history![index].item.item.duration?.inMinutes.toString().padLeft(2, '0')}:${((history![index].item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + textAlign: TextAlign.end, + style: TextStyle( + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + IconButton( + icon: const Icon(TablerIcons.dots_vertical), + iconSize: 24.0, + onPressed: () async => {}, + ), + ], + ), + ), + onTap: () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.startingInstantMix), + )); + + audioServiceHelper.startInstantMixForItem(jellyfin_models.BaseItemDto.fromJson(history![index].item.item.extras?["itemJson"])).catchError((e) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.anErrorHasOccured), + )); + }); + + } + // await _queueService.skipByOffset(indexOffset), + ) + ); + }, + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 813f8cdb5..fabb745de 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,8 +4,10 @@ import 'dart:ui'; import 'package:audio_service/audio_service.dart'; import 'package:audio_session/audio_session.dart'; +import 'package:finamp/screens/playback_history_screen.dart'; import 'package:finamp/services/finamp_settings_helper.dart'; import 'package:finamp/services/finamp_user_helper.dart'; +import 'package:finamp/services/playback_history_service.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -198,6 +200,7 @@ Future _setupPlaybackServices() async { GetIt.instance.registerSingleton(audioHandler); GetIt.instance.registerSingleton(QueueService()); + GetIt.instance.registerSingleton(PlaybackHistoryService()); GetIt.instance.registerSingleton(AudioServiceHelper()); } @@ -300,6 +303,7 @@ class Finamp extends StatelessWidget { const DownloadsScreen(), DownloadsErrorScreen.routeName: (context) => const DownloadsErrorScreen(), + PlaybackHistoryScreen.routeName: (context) => const PlaybackHistoryScreen(), LogsScreen.routeName: (context) => const LogsScreen(), SettingsScreen.routeName: (context) => const SettingsScreen(), diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 8ba262d2c..678b021b7 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -678,3 +678,21 @@ class QueueInfo { QueueItemSource source; } + +class HistoryItem { + HistoryItem({ + required this.item, + required this.startTime, + this.endTime, + }); + + @HiveField(0) + QueueItem item; + + @HiveField(1) + DateTime startTime; + + @HiveField(2) + DateTime? endTime; + +} diff --git a/lib/screens/music_screen.dart b/lib/screens/music_screen.dart index 0743558c4..1c6629792 100644 --- a/lib/screens/music_screen.dart +++ b/lib/screens/music_screen.dart @@ -1,5 +1,7 @@ +import 'package:finamp/screens/playback_history_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; import 'package:logging/logging.dart'; @@ -231,6 +233,12 @@ class _MusicScreenState extends State ) ] : [ + IconButton( + icon: const Icon(TablerIcons.clock), + onPressed: () => Navigator.of(context) + .pushNamed(PlaybackHistoryScreen.routeName), + tooltip: "Playback History", + ), SortOrderButton( tabs.elementAt(_tabController!.index), ), diff --git a/lib/screens/playback_history_screen.dart b/lib/screens/playback_history_screen.dart new file mode 100644 index 000000000..77ac97c44 --- /dev/null +++ b/lib/screens/playback_history_screen.dart @@ -0,0 +1,33 @@ +import 'package:finamp/components/PlaybackHistoryScreen/playback_history_list.dart'; +import 'package:finamp/components/now_playing_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../components/finamp_app_bar_button.dart'; + + +class PlaybackHistoryScreen extends StatelessWidget { + const PlaybackHistoryScreen({Key? key}) : super(key: key); + + static const routeName = "/playbackhistory"; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + elevation: 0.0, + backgroundColor: Colors.transparent, + title: Text("Playback History"), + leading: FinampAppBarButton( + onPressed: () => Navigator.pop(context), + ), + ), + body: const Padding( + padding: EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0, bottom: 0.0), + child: PlaybackHistoryList(), + ), + bottomNavigationBar: const NowPlayingBar(), +); + } +} diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index 94622e267..6486c6070 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import 'package:just_audio/just_audio.dart'; import 'package:logging/logging.dart'; +import 'package:rxdart/rxdart.dart'; import '../models/finamp_models.dart'; import '../models/jellyfin_models.dart' as jellyfin_models; @@ -37,7 +38,7 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { final _jellyfinApiHelper = GetIt.instance(); final _finampUserHelper = GetIt.instance(); - final _playbackEventStreamController = StreamController(); + final _playbackEventStreamController = BehaviorSubject(); /// Set when creating a new queue. Will be used to set the first index in a /// new queue. @@ -74,15 +75,15 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { _playbackEventStreamController.add(event); - if (playbackState.valueOrNull != null && - playbackState.valueOrNull?.processingState != - AudioProcessingState.idle && - playbackState.valueOrNull?.processingState != - AudioProcessingState.completed && - !FinampSettingsHelper.finampSettings.isOffline && - !_isStopping) { - await _updatePlaybackProgress(); - } + // if (playbackState.valueOrNull != null && + // playbackState.valueOrNull?.processingState != + // AudioProcessingState.idle && + // playbackState.valueOrNull?.processingState != + // AudioProcessingState.completed && + // !FinampSettingsHelper.finampSettings.isOffline && + // !_isStopping) { + // await _updatePlaybackProgress(); + // } }); // Special processing for state transitions. @@ -145,8 +146,8 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { _queueCallbackSkipToIndexCallback = skipToIndexCallback; } - Stream getPlaybackEventStream() { - return _playbackEventStreamController.stream; + BehaviorSubject getPlaybackEventStream() { + return _playbackEventStreamController; } Future initializeAudioSource(ConcatenatingAudioSource source) async { @@ -579,6 +580,9 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } List? get effectiveSequence => _player.sequenceState?.effectiveSequence; + double get volume => _player.volume; + bool get paused => !_player.playing; + Duration get playbackPosition => _player.position; /// Syncs the list of MediaItems (_queue) with the internal queue of the player. /// Called by onAddQueueItem and onUpdateQueue. diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart new file mode 100644 index 000000000..53dd848f1 --- /dev/null +++ b/lib/services/playback_history_service.dart @@ -0,0 +1,193 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; +import 'package:audio_service/audio_service.dart'; +import 'package:finamp/services/music_player_background_task.dart'; +import 'package:finamp/services/queue_service.dart'; +import 'package:get_it/get_it.dart'; +import 'package:logging/logging.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:uuid/uuid.dart'; + +import 'finamp_user_helper.dart'; +import 'jellyfin_api_helper.dart'; +import 'finamp_settings_helper.dart'; +import '../models/finamp_models.dart'; +import '../models/jellyfin_models.dart' as jellyfin_models; + +/// A track queueing service for Finamp. +class PlaybackHistoryService { + final _jellyfinApiHelper = GetIt.instance(); + final _finampUserHelper = GetIt.instance(); + final _audioService = GetIt.instance(); + final _queueService = GetIt.instance(); + final _playbackHistoryServiceLogger = Logger("PlaybackHistoryService"); + + // internal state + + List _history = []; // contains **all** items that have been played, including "next up" + HistoryItem? _currentTrack; // the currently playing track + + final _historyStream = BehaviorSubject>.seeded( + List.empty(growable: true), + ); + + PlaybackHistoryService() { + + _queueService.getCurrentTrackStream().listen((currentTrack) { + updateCurrentTrack(currentTrack); + }); + + _audioService.getPlaybackEventStream().listen((event) { + _updatePlaybackProgress(); + if (_audioService.paused) { + _playbackHistoryServiceLogger.info("Playback paused."); + } else { + _playbackHistoryServiceLogger.info("Playback resumed."); + } + }); + + } + + get history => _history; + BehaviorSubject> get historyStream => _historyStream; + + //TODO handle events that don't change the current track (e.g. pause, seek, etc.) + + void updateCurrentTrack(QueueItem? currentTrack) { + + bool playbackStarted = false; + bool playbackStopped = false; + + if (currentTrack == _currentTrack?.item || currentTrack?.item.id == "") { + // current track hasn't changed + return; + } + + // update end time of previous track + if (_currentTrack != null) { + _currentTrack!.endTime = DateTime.now(); + } else { + playbackStarted = true; + } + + if (currentTrack != null) { + _currentTrack = HistoryItem( + item: currentTrack, + startTime: DateTime.now(), + ); + _history.add(_currentTrack!); // current track is always the last item in the history + } else { + playbackStopped = true; + } + + _historyStream.add(_history); + + _updatePlaybackProgress( + playbackStarted: playbackStarted, + playbackStopped: playbackStopped, + ); + } + + Future _updatePlaybackProgress({ + bool playbackStarted = false, + bool playbackPaused = false, + bool playbackStopped = false, + }) async { + try { + + final playbackInfo = generatePlaybackProgressInfo(); + if (playbackInfo != null) { + if (playbackStarted) { + await _reportPlaybackStarted(); + } else if (playbackStopped) { + await _reportPlaybackStopped(); + } else { + await _jellyfinApiHelper.updatePlaybackProgress(playbackInfo); + } + } + } catch (e) { + _playbackHistoryServiceLogger.severe(e); + return Future.error(e); + } + } + + Future _reportPlaybackStopped() async { + + final playbackInfo = generatePlaybackProgressInfo(); + if (playbackInfo != null) { + await _jellyfinApiHelper.stopPlaybackProgress(playbackInfo); + } + + } + + Future _reportPlaybackStarted() async { + + final playbackInfo = generatePlaybackProgressInfo(); + if (playbackInfo != null) { + await _jellyfinApiHelper.reportPlaybackStart(playbackInfo); + } + + } + + /// Generates PlaybackProgressInfo from current player info. + jellyfin_models.PlaybackProgressInfo? generatePlaybackProgressInfo({ + bool includeNowPlayingQueue = false, + }) { + if (_history.isEmpty || _currentTrack == null) { + // This function relies on _history having items + return null; + } + + try { + + final itemId = _currentTrack!.item.item.extras?["itemJson"]["Id"]; + + if (itemId == null) { + _playbackHistoryServiceLogger.warning( + "Current track item ID is null, cannot generate playback progress info.", + ); + return null; + } + + return jellyfin_models.PlaybackProgressInfo( + itemId: _currentTrack!.item.item.extras?["itemJson"]["Id"], + isPaused: _audioService.paused, + isMuted: _audioService.volume == 0.0, + volumeLevel: _audioService.volume.round(), + positionTicks: _audioService.playbackPosition.inMicroseconds * 10, + repeatMode: _toJellyfinRepeatMode(_queueService.loopMode), + playbackStartTimeTicks: _currentTrack!.startTime.millisecondsSinceEpoch * 1000 * 10, + playMethod: _currentTrack!.item.item.extras!["shouldTranscode"] + ? "Transcode" + : "DirectPlay", + // We don't send the queue since it seems useless and it can cause + // issues with large queues. + // https://github.com/jmshrv/finamp/issues/387 + nowPlayingQueue: includeNowPlayingQueue + ? _queueService.getQueue().nextUp.followedBy(_queueService.getQueue().queue) + .map( + (e) => jellyfin_models.QueueItem( + id: e.item.extras!["itemJson"]["Id"], + playlistItemId: e.item.id + ), + ).toList() + : null, + ); + } catch (e) { + _playbackHistoryServiceLogger.severe(e); + rethrow; + } + } + + String _toJellyfinRepeatMode(LoopMode loopMode) { + switch (loopMode) { + case LoopMode.all: + return "RepeatAll"; + case LoopMode.one: + return "RepeatOne"; + case LoopMode.none: + return "RepeatNone"; + } + } +} diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index ea3cb2db6..4a5ba2f2a 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'package:finamp/services/playback_history_service.dart'; import 'package:just_audio/just_audio.dart'; import 'package:audio_service/audio_service.dart'; import 'package:get_it/get_it.dart'; @@ -38,7 +39,7 @@ class QueueService { PlaybackOrder _playbackOrder = PlaybackOrder.linear; LoopMode _loopMode = LoopMode.none; - final _currentTrackStream = BehaviorSubject.seeded( + final _currentTrackStream = BehaviorSubject.seeded( QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)) ); final _queueStream = BehaviorSubject.seeded(QueueInfo( @@ -136,6 +137,8 @@ class QueueService { _currentTrackStream.add(_currentTrack!); _audioHandler.mediaItem.add(_currentTrack!.item); _audioHandler.queue.add(_queuePreviousTracks.followedBy([_currentTrack!]).followedBy(_queue).map((e) => e.item).toList()); + + _currentTrackStream.add(_currentTrack); } _logQueues(message: "(current)"); @@ -350,7 +353,7 @@ class QueueService { return _queueStream; } - BehaviorSubject getCurrentTrackStream() { + BehaviorSubject getCurrentTrackStream() { return _currentTrackStream; } From f93b23446f910c69c72328cddc00a6636f1cd7a2 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 10 Jun 2023 12:25:57 +0200 Subject: [PATCH 036/130] re-add missing packages --- pubspec.lock | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pubspec.lock b/pubspec.lock index 1deb72d79..9e288f7b2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -289,6 +289,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + drag_and_drop_lists: + dependency: "direct main" + description: + name: drag_and_drop_lists + sha256: "9a14595f9880be7953f23578aef88c15cc9b367eeb39dbc1f1bd6af52f70872b" + url: "https://pub.dev" + source: hosted + version: "0.3.3" equatable: dependency: transitive description: From 7774d66ecfa9a7f17c67253b439a96ac85041f2a Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 10 Jun 2023 19:59:17 +0200 Subject: [PATCH 037/130] improve next up + shuffle behavior - fixes adding tracks to next up if the current track isn't the first track in the queue - if the track was added to Next Up while shuffle was disabled, next up will stay in place when shuffle is toggled on and off - if shuffle was enabled while adding the track, toggling shuffle off will clear Next Up --- lib/services/queue_service.dart | 37 ++++++++++++++------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 4a5ba2f2a..7e6c7479a 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -268,8 +268,12 @@ class QueueService { type: QueueItemQueueType.nextUp, ); + //TODO doesn't work when adding while shuffled and then *disabling* shuffle + // don't add to _order, because it wasn't added to the regular queue - await _queueAudioSource.insert(_queueAudioSourceIndex+1, await _queueItemToAudioSource(queueItem)); + int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; + + await _queueAudioSource.insert(adjustedQueueIndex+1, await _queueItemToAudioSource(queueItem)); _queueServiceLogger.fine("Prepended '${queueItem.item.title}' to Next Up"); @@ -292,9 +296,10 @@ class QueueService { // don't add to _order, because it wasn't added to the regular queue _queueFromConcatenatingAudioSource(); // update internal queues + int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; int offset = _queueNextUp.length; - await _queueAudioSource.insert(_queueAudioSourceIndex+1+offset, await _queueItemToAudioSource(queueItem)); + await _queueAudioSource.insert(adjustedQueueIndex+1+offset, await _queueItemToAudioSource(queueItem)); _queueServiceLogger.fine("Appended '${queueItem.item.title}' to Next Up (index ${_queueAudioSourceIndex+1+offset})"); @@ -568,6 +573,7 @@ class NextUpShuffleOrder extends ShuffleOrder { void shuffle({int? initialIndex}) { assert(initialIndex == null || indices.contains(initialIndex)); indices.clear(); + _queueService!._queueFromConcatenatingAudioSource(); QueueInfo queueInfo = _queueService!.getQueue(); indices = List.generate(queueInfo.previousTracks.length + 1 + queueInfo.nextUp.length + queueInfo.queue.length, (i) => i); if (indices.length <= 1) return; @@ -593,19 +599,15 @@ class NextUpShuffleOrder extends ShuffleOrder { } const initialPos = 0; // current item will always be at the front - final swapPos = indices.indexOf(initialIndex); - // Swap the indices at initialPos and swapPos. - final swapIndex = indices[initialPos]; - indices[initialPos] = initialIndex; - indices[swapPos] = swapIndex; - - // swap all Next Up items to the front - for (int i = 1; i <= nextUpLength; i++) { - final swapPos = indices.indexOf(initialIndex + i); - final swapIndex = indices[initialPos + i]; - indices[initialPos + i] = initialIndex + i; - indices[swapPos] = swapIndex; + + // move current track and next up tracks to the front, pushing all other tracks back while keeping their order + // remove current track and next up tracks from indices and save them in a separate list + List currentTrackIndices = []; + for (int i = 0; i < 1 + nextUpLength; i++) { + currentTrackIndices.add(indices.removeAt(indices.indexOf(initialIndex + i))); } + // insert current track and next up tracks at the front + indices.insertAll(initialPos, currentTrackIndices); } @@ -616,7 +618,6 @@ class NextUpShuffleOrder extends ShuffleOrder { } _queueService!.queueServiceLogger.finest("Shuffled indices (swapped): $indicesString"); - } @override @@ -628,12 +629,6 @@ class NextUpShuffleOrder extends ShuffleOrder { indices[i] += count; } } - // // Insert new indices at random positions after currentIndex. - // final newIndices = List.generate(count, (i) => index + i); - // for (var newIndex in newIndices) { - // final insertionIndex = _random.nextInt(indices.length + 1); - // indices.insert(insertionIndex, newIndex); - // } // Insert new indices at the specified position. final newIndices = List.generate(count, (i) => index + i); From 015402138b3fad2f0fa9e0e64b8c113ded077415 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 11 Jun 2023 11:52:49 +0200 Subject: [PATCH 038/130] adding tracks to Next Up or queue fully working now - adding tracks to Next Up works when shuffled - adding tracks to (end of) queue working when shuffled - Next Up and end of queue stay in place when turning off shuffle - toggling shuffle doesn't affect Next Up but will shuffle end of queue (only when turning shuffle *on*) - adding to Next Up and end of queue also works when shuffle is disabled --- .../AlbumScreen/song_list_tile.dart | 2 +- lib/services/queue_service.dart | 37 ++++++++++++++++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index 2d2a4ad5e..eb0aa69ad 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -312,7 +312,7 @@ class _SongListTileState extends State { switch (selection) { case SongListTileMenuItems.addToQueue: // await _audioServiceHelper.addQueueItem(widget.item); - await _queueService.addToQueue(widget.item, QueueItemSource(type: QueueItemSourceType.unknown, name: "Queue", id: widget.parentId!)); + await _queueService.addToQueue(widget.item, QueueItemSource(type: QueueItemSourceType.unknown, name: "Queue", id: widget.parentId ?? "unknown")); if (!mounted) return; diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 7e6c7479a..f37fa1761 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -271,9 +271,9 @@ class QueueService { //TODO doesn't work when adding while shuffled and then *disabling* shuffle // don't add to _order, because it wasn't added to the regular queue - int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; + // int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; - await _queueAudioSource.insert(adjustedQueueIndex+1, await _queueItemToAudioSource(queueItem)); + await _queueAudioSource.insert(_queueAudioSourceIndex+1, await _queueItemToAudioSource(queueItem)); _queueServiceLogger.fine("Prepended '${queueItem.item.title}' to Next Up"); @@ -296,10 +296,10 @@ class QueueService { // don't add to _order, because it wasn't added to the regular queue _queueFromConcatenatingAudioSource(); // update internal queues - int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; + // int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; int offset = _queueNextUp.length; - await _queueAudioSource.insert(adjustedQueueIndex+1+offset, await _queueItemToAudioSource(queueItem)); + await _queueAudioSource.insert(_queueAudioSourceIndex+1+offset, await _queueItemToAudioSource(queueItem)); _queueServiceLogger.fine("Appended '${queueItem.item.title}' to Next Up (index ${_queueAudioSourceIndex+1+offset})"); @@ -620,9 +620,36 @@ class NextUpShuffleOrder extends ShuffleOrder { } + /// `index` is the linear index of the item in the ConcatenatingAudioSource @override void insert(int index, int count) { + int insertionPoint = index; + int linearIndexOfPreviousItem = index - 1; + + // _queueService!._queueFromConcatenatingAudioSource(); + // QueueInfo queueInfo = _queueService!.getQueue(); + + // // log indices + // String indicesString = ""; + // for (int index in indices) { + // indicesString += "$index, "; + // } + // _queueService!.queueServiceLogger.finest("Shuffled indices: $indicesString"); + // _queueService!.queueServiceLogger.finest("Current Track: ${queueInfo.currentTrack}"); + + if (index >= indices.length) { + // handle appending to the queue + insertionPoint = indices.length; + } else { + // handle adding to Next Up + int shuffledIndexOfPreviousItem = indices.indexOf(linearIndexOfPreviousItem); + if (shuffledIndexOfPreviousItem != -1) { + insertionPoint = shuffledIndexOfPreviousItem + 1; + } + _queueService!.queueServiceLogger.finest("Inserting $count items at index $index (shuffled indices insertion point: $insertionPoint) (index of previous item: $shuffledIndexOfPreviousItem)"); + } + // Offset indices after insertion point. for (var i = 0; i < indices.length; i++) { if (indices[i] >= index) { @@ -632,7 +659,7 @@ class NextUpShuffleOrder extends ShuffleOrder { // Insert new indices at the specified position. final newIndices = List.generate(count, (i) => index + i); - indices.insertAll(index, newIndices); + indices.insertAll(insertionPoint, newIndices); } From cc4a44be894591f36a0b3f6507ff1aed223c49b7 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 23 Jul 2023 20:55:09 +0200 Subject: [PATCH 039/130] initial try at another custom implementation --- lib/components/PlayerScreen/queue_list.dart | 887 +++++++++++------- .../PlayerScreen/queue_list_item.dart | 175 ++++ 2 files changed, 706 insertions(+), 356 deletions(-) create mode 100644 lib/components/PlayerScreen/queue_list_item.dart diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 893e82aff..a9ceab0e6 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -15,6 +15,7 @@ import '../../services/process_artist.dart'; import '../../services/media_state_stream.dart'; import '../../services/music_player_background_task.dart'; import '../../services/queue_service.dart'; +import 'queue_list_item.dart'; class _QueueListStreamState { _QueueListStreamState( @@ -44,44 +45,42 @@ class _QueueListState extends State { List? _queue; QueueItemSource? _source; - late List _contents; + late List _contents; @override void initState() { super.initState(); - _contents = [ - DragAndDropListExpansion( - listKey: const ObjectKey(0), - title: const Text("Previous Tracks"), - leading: const Icon(TablerIcons.history), - disableTopAndBottomBorders: true, - canDrag: false, - children: [], + _contents = [ + // const SliverPadding(padding: EdgeInsets.only(top: 0)), + // Previous Tracks + SliverList.list( + children: const [], ), - DragAndDropList( - header: const ListTile( - leading: Icon(TablerIcons.music), - title: Text("Current Track"), + // Current Track + SliverAppBar( + pinned: true, + collapsedHeight: 70.0, + expandedHeight: 70.0, + leading: const Padding( + padding: EdgeInsets.zero, ), - canDrag: false, - children: [], + flexibleSpace: ListTile( + leading: const AlbumImage( + item: null, + ), + title: Text( + "Unknown song"), + subtitle: Text("Unknown artist"), + onTap: () {} + ) ), - DragAndDropList( - header: const ListTile( - leading: Icon(TablerIcons.layout_list), - title: Text("Next Up"), - ), - canDrag: false, - children: [], + SliverPersistentHeader( + delegate: SectionHeaderDelegate("Queue") ), - DragAndDropList( - header: const ListTile( - leading: Icon(TablerIcons.layout_list), - title: Text("Queue"), - ), - canDrag: false, - children: [], + // Queue + SliverList.list( + children: const [], ), ]; } @@ -90,27 +89,35 @@ class _QueueListState extends State { Widget build(BuildContext context) { return StreamBuilder<_QueueListStreamState>( // stream: AudioService.queueStream, - stream: Rx.combineLatest2(mediaStateStream, _queueService.getQueueStream(), + stream: Rx.combineLatest2( + mediaStateStream, + _queueService.getQueueStream(), (a, b) => _QueueListStreamState(a, b)), // stream: _queueService.getQueueStream(), builder: (context, snapshot) { - if (snapshot.hasData) { - _previousTracks ??= snapshot.data!.queueInfo.previousTracks; - _currentTrack = snapshot.data!.queueInfo.currentTrack ?? QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)); + _currentTrack = snapshot.data!.queueInfo.currentTrack ?? + QueueItem( + item: const MediaItem( + id: "", + title: "No track playing", + album: "No album", + artist: "No artist"), + source: QueueItemSource( + id: "", name: "", type: QueueItemSourceType.unknown)); _nextUp ??= snapshot.data!.queueInfo.nextUp; _queue ??= snapshot.data!.queueInfo.queue; _source ??= snapshot.data!.queueInfo.source; - final GlobalKey currentTrackKey = GlobalKey(debugLabel: "currentTrack"); + final GlobalKey currentTrackKey = + GlobalKey(debugLabel: "currentTrack"); void scrollToCurrentTrack() { - widget.scrollController.animateTo(((_previousTracks?.length ?? 0) * 60 + 20).toDouble(), - duration: const Duration(milliseconds: 200), - curve: Curves.linear - ); + widget.scrollController.animateTo( + ((_previousTracks?.length ?? 0) * 60 + 20).toDouble(), + duration: const Duration(milliseconds: 200), + curve: Curves.linear); // final targetContext = currentTrackKey.currentContext; // if (targetContext != null) { // Scrollable.ensureVisible(targetContext!, @@ -124,332 +131,460 @@ class _QueueListState extends State { // WidgetsBinding.instance // .addPostFrameCallback((_) => scrollToCurrentTrack()); - _contents = [ - //TODO save this as a variable so that a pseudo footer can be added that will call `toggleExpanded()` on the list - DragAndDropListExpansion( - listKey: const ObjectKey(0), - title: const Text("Previous Tracks"), - // subtitle: Text('Subtitle ${innerList.name}'), - // trailing: Text("Previous Tracks"), - leading: const Icon(TablerIcons.history), - disableTopAndBottomBorders: true, - canDrag: false, - children: _previousTracks?.asMap().entries.map((e) { - final index = e.key; - final item = e.value; + _contents = [ + // const SliverPadding(padding: EdgeInsets.only(top: 0)), + // Previous Tracks + SliverPersistentHeader( + delegate: SectionHeaderDelegate("Previous Tracks"), + ), + SliverList.builder( + itemCount: _previousTracks?.length ?? 0, + // onReorder: (oldIndex, newIndex) async { + // final oldOffset = -((_previousTracks?.length ?? 0) - oldIndex); + // final newOffset = -((_previousTracks?.length ?? 0) - newIndex); + // // setState(() { + // // // _previousTracks?.insert(newIndex, _previousTracks![oldIndex]); + // // // _previousTracks?.removeAt(oldIndex); + // // int? smallerThanNewIndex; + // // if (oldIndex < newIndex) { + // // // When we're moving an item backwards, we need to reduce + // // // newIndex by 1 to account for there being a new item added + // // // before newIndex. + // // smallerThanNewIndex = newIndex - 1; + // // } + // // final item = _previousTracks?.removeAt(oldIndex); + // // _previousTracks?.insert(smallerThanNewIndex ?? newIndex, item!); + // // }); + // await _queueService.reorderByOffset(oldOffset, newOffset); + // }, + itemBuilder: (context, index) { + final item = _previousTracks![index]; final actualIndex = index; final indexOffset = -((_previousTracks?.length ?? 0) - index); + return Card( + key: ValueKey("${_queue![actualIndex].item.id}$actualIndex"), + child: DragTarget( + builder: ( + BuildContext context, + List candidateData, + List rejectedData, + ) { + return QueueListItem( + item: item, + actualIndex: actualIndex, + indexOffset: indexOffset, + subqueue: _previousTracks!, + isCurrentTrack: _currentTrack == item, + ); + }, + onWillAccept: (data) { + return true; + }, + onAccept: (data) async { + + int oldOffset = 0; + int newOffset = 0; + + int oldListIndex = 0; + const newListIndex = 0; + int oldItemIndex = -1; + int newItemIndex = index; + + // lookup current index of the item belonging to the data + if (_nextUp != null && _nextUp!.isNotEmpty) { + oldItemIndex = _nextUp!.indexOf(data); + oldListIndex = 1; + } + if (oldItemIndex == -1) { + oldItemIndex = _nextUp!.length + _queue!.indexOf(data); + oldListIndex = 2; + } + if (oldItemIndex == -1) { + oldItemIndex = _previousTracks!.indexOf(data); + oldListIndex = 0; + } + + // old index + if (oldListIndex == 0) { + // previous tracks + oldOffset = -((_previousTracks?.length ?? 0) - oldItemIndex); + } else if (oldListIndex == 1) { + // next up + oldOffset = oldItemIndex + 1; + } else if (oldListIndex == 2) { + // queue + oldOffset = oldItemIndex + _nextUp!.length + 1; + } + + // new index + if (newListIndex == 0) { + // previous tracks + newOffset = -((_previousTracks?.length ?? 0) - newItemIndex); + } else if ( + newListIndex == 1 && + oldListIndex == 2 // tracks can't be moved *to* next up, only *within* next up or *out of* next up + ) { + // next up + newOffset = newItemIndex + 1; + } else if (newListIndex == 2) { + // queue + newOffset = newItemIndex + _nextUp!.length + 1; + } else { + newOffset = oldOffset; + } + + if (oldOffset != newOffset) { + // setState(() { + // var movedItem = _contents[oldListIndex].children!.removeAt(oldItemIndex); + // _contents[newListIndex].children!.insert(newItemIndex, movedItem); + // }); + await _queueService.reorderByOffset(oldOffset, newOffset); + } + + // setState(() { + // _queue?.insert(index, data); + // }); + }, + ) + ); + }, + ), + SliverList.list( + children: [ + DragTarget( + builder: ( + BuildContext context, + List candidateData, + List rejectedData, + ) { + return Container(height: 60.0); + }, + onWillAccept: (data) { + return true; + }, + onAccept: (data) async { + int oldOffset = 0; + int newOffset = 0; + + int oldListIndex = 0; + const newListIndex = 2; + int oldItemIndex = -1; + int newItemIndex = 0; + + // lookup current index of the item belonging to the data + if (_nextUp != null && _nextUp!.isNotEmpty) { + oldItemIndex = _nextUp!.indexOf(data); + oldListIndex = 1; + } + if (oldItemIndex == -1) { + oldItemIndex = _nextUp!.length + _queue!.indexOf(data); + oldListIndex = 2; + } + if (oldItemIndex == -1) { + oldItemIndex = _previousTracks!.indexOf(data); + oldListIndex = 0; + } + + // old index + if (oldListIndex == 0) { + // previous tracks + oldOffset = -((_previousTracks?.length ?? 0) - oldItemIndex); + } else if (oldListIndex == 1) { + // next up + oldOffset = oldItemIndex + 1; + } else if (oldListIndex == 2) { + // queue + oldOffset = oldItemIndex + _nextUp!.length + 1; + } - return DragAndDropItem(child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: ListTile( - visualDensity: VisualDensity.compact, - minVerticalPadding: 0.0, - contentPadding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0), - tileColor: _currentTrack == item - ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) - : null, - leading: AlbumImage( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(7.0), - bottomLeft: Radius.circular(7.0), - ), - item: item.item - .extras?["itemJson"] == null - ? null - : jellyfin_models.BaseItemDto.fromJson(item.item.extras?["itemJson"]), - ), - title: Text( - item.item.title ?? AppLocalizations.of(context)!.unknownName, - style: _currentTrack == item + // new index + if (newListIndex == 0) { + // previous tracks + newOffset = -((_previousTracks?.length ?? 0) - newItemIndex); + } else if ( + newListIndex == 1 && + oldListIndex == 2 // tracks can't be moved *to* next up, only *within* next up or *out of* next up + ) { + // next up + newOffset = newItemIndex + 1; + } else if (newListIndex == 2) { + // queue + newOffset = newItemIndex + _nextUp!.length + 1; + } else { + newOffset = oldOffset; + } + + if (oldOffset != newOffset) { + // setState(() { + // var movedItem = _contents[oldListIndex].children!.removeAt(oldItemIndex); + // _contents[newListIndex].children!.insert(newItemIndex, movedItem); + // }); + await _queueService.reorderByOffset(oldOffset, newOffset); + } + // setState(() { + // _queue?.insert(index, data); + // }); + }, + )] + ), + // Current Track + SliverPersistentHeader( + delegate: SectionHeaderDelegate("Current Track"), + ), + SliverAppBar( + key: currentTrackKey, + pinned: true, + collapsedHeight: 70.0, + expandedHeight: 70.0, + leading: const Padding( + padding: EdgeInsets.zero, + ), + flexibleSpace: ListTile( + leading: AlbumImage( + item: _currentTrack!.item + .extras?["itemJson"] == null + ? null + : jellyfin_models.BaseItemDto.fromJson(_currentTrack!.item.extras?["itemJson"]), + ), + title: Text( + _currentTrack!.item.title ?? AppLocalizations.of(context)!.unknownName, + style: _currentTrack == _currentTrack! ? TextStyle( color: Theme.of(context).colorScheme.secondary) : null), - subtitle: Text(processArtist( - item.item.artist, - context)), - trailing: Container( - alignment: Alignment.centerRight, - margin: const EdgeInsets.only(right: 32.0), - width: 95.0, - height: 50.0, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "${item.item.duration?.inMinutes.toString().padLeft(2, '0')}:${((item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - textAlign: TextAlign.end, - style: TextStyle( - color: Theme.of(context).textTheme.bodySmall?.color, - ), - ), - // IconButton( - // icon: const Icon(TablerIcons.dots_vertical), - // iconSize: 28.0, - // onPressed: () async => {}, - // ), - IconButton( - icon: const Icon(TablerIcons.x), - iconSize: 28.0, - onPressed: () async => await _queueService.removeAtOffset(indexOffset), - ), - ], - ), - ), - onTap: () async => - await _queueService.skipByOffset(indexOffset), - ) - )); - }).toList() ?? [], + subtitle: Text(processArtist( + _currentTrack!.item.artist, + context)), + onTap: () async => + snapshot.data!.mediaState.playbackState.playing ? await _audioHandler.pause() : await _audioHandler.play(), + ), ), - DragAndDropList( - header: const ListTile( - leading: Icon(TablerIcons.music), - title: Text("Current Track"), + SliverPersistentHeader( + delegate: SectionHeaderDelegate( + _source?.name != null + ? "Playing from ${_source?.name}" + : "Queue", + true, ), - canDrag: false, + ), + SliverList.list( children: [ - DragAndDropItem( - canDrag: false, - child: Card( - key: currentTrackKey, - child: ListTile( - leading: AlbumImage( - item: _currentTrack!.item - .extras?["itemJson"] == null - ? null - : jellyfin_models.BaseItemDto.fromJson(_currentTrack!.item.extras?["itemJson"]), - ), - title: Text( - _currentTrack!.item.title ?? AppLocalizations.of(context)!.unknownName, - style: _currentTrack == _currentTrack! - ? TextStyle( - color: - Theme.of(context).colorScheme.secondary) - : null), - subtitle: Text(processArtist( - _currentTrack!.item.artist, - context)), - onTap: () async => - snapshot.data!.mediaState.playbackState.playing ? await _audioHandler.pause() : await _audioHandler.play(), - ), - ) - ) - ] + DragTarget( + builder: ( + BuildContext context, + List candidateData, + List rejectedData, + ) { + return Container(height: 20.0); + }, + onWillAccept: (data) { + return true; + }, + onAccept: (data) async { + int oldOffset = 0; + int newOffset = 0; + + int oldListIndex = 0; + const newListIndex = 2; + int oldItemIndex = -1; + int newItemIndex = 0; + + // lookup current index of the item belonging to the data + if (_nextUp != null && _nextUp!.isNotEmpty) { + oldItemIndex = _nextUp!.indexOf(data); + oldListIndex = 1; + } + if (oldItemIndex == -1) { + oldItemIndex = _nextUp!.length + _queue!.indexOf(data); + oldListIndex = 2; + } + if (oldItemIndex == -1) { + oldItemIndex = _previousTracks!.indexOf(data); + oldListIndex = 0; + } + + // old index + if (oldListIndex == 0) { + // previous tracks + oldOffset = -((_previousTracks?.length ?? 0) - oldItemIndex); + } else if (oldListIndex == 1) { + // next up + oldOffset = oldItemIndex + 1; + } else if (oldListIndex == 2) { + // queue + oldOffset = oldItemIndex + _nextUp!.length + 1; + } + + // new index + if (newListIndex == 0) { + // previous tracks + newOffset = -((_previousTracks?.length ?? 0) - newItemIndex) + 1; + } else if ( + newListIndex == 1 && + oldListIndex == 2 // tracks can't be moved *to* next up, only *within* next up or *out of* next up + ) { + // next up + newOffset = newItemIndex; + } else if (newListIndex == 2) { + // queue + newOffset = newItemIndex + _nextUp!.length; + } else { + newOffset = oldOffset; + } + + if (oldOffset != newOffset) { + // setState(() { + // var movedItem = _contents[oldListIndex].children!.removeAt(oldItemIndex); + // _contents[newListIndex].children!.insert(newItemIndex, movedItem); + // }); + await _queueService.reorderByOffset(oldOffset, newOffset); + } + // setState(() { + // _queue?.insert(index, data); + // }); + }, + )] ), - if (_nextUp!.isNotEmpty) - DragAndDropList( - header: const ListTile( - leading: Icon(TablerIcons.layout_list), - title: Text("Next Up"), - ), - canDrag: false, - children: _nextUp?.asMap().entries.map((e) { - final index = e.key; - final item = e.value; - // final actualIndex = index; - // final indexOffset = -((_previousTracks?.length ?? 0) - index); - final actualIndex = index; - final indexOffset = index + 1; - - return DragAndDropItem(child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: ListTile( - visualDensity: VisualDensity.compact, - minVerticalPadding: 0.0, - contentPadding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0), - tileColor: _currentTrack == item - ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) - : null, - leading: AlbumImage( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(7.0), - bottomLeft: Radius.circular(7.0), - ), - item: item.item - .extras?["itemJson"] == null - ? null - : jellyfin_models.BaseItemDto.fromJson(item.item.extras?["itemJson"]), - ), - title: Text( - item.item.title ?? AppLocalizations.of(context)!.unknownName, - style: _currentTrack == item - ? TextStyle( - color: - Theme.of(context).colorScheme.secondary) - : null), - subtitle: Text(processArtist( - item.item.artist, - context)), - trailing: Container( - alignment: Alignment.centerRight, - margin: const EdgeInsets.only(right: 32.0), - width: 95.0, - height: 50.0, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "${item.item.duration?.inMinutes.toString().padLeft(2, '0')}:${((item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - textAlign: TextAlign.end, - style: TextStyle( - color: Theme.of(context).textTheme.bodySmall?.color, + // Queue + SliverList.builder( + itemCount: _queue?.length ?? 0, + // onReorder: (oldIndex, newIndex) async { + // final oldOffset = oldIndex + 1; + // final newOffset = newIndex + 1; + // setState(() { + // // _queue?.insert(newIndex, _queue![oldIndex]); + // // _queue?.removeAt(oldIndex); + // int? smallerThanNewIndex; + // if (oldIndex < newIndex) { + // // When we're moving an item backwards, we need to reduce + // // newIndex by 1 to account for there being a new item added + // // before newIndex. + // smallerThanNewIndex = newIndex - 1; + // } + // final item = _queue?.removeAt(oldIndex); + // _queue?.insert(smallerThanNewIndex ?? newIndex, item!); + // }); + // await _queueService.reorderByOffset(oldOffset, newOffset); + // }, + itemBuilder: (context, index) { + final item = _queue![index]; + final actualIndex = index; + final indexOffset = index + 1; + return Card( + key: ValueKey("${_queue![actualIndex].item.id}$actualIndex"), + child: DragTarget( + builder: ( + BuildContext context, + List candidateData, + List rejectedData, + ) { + if (candidateData.isNotEmpty && candidateData.first != null && candidateData.first != item) { + return Column( + children: [ + QueueListItemGhost( + item: candidateData.first!, + isCurrentTrack: _currentTrack == item, + ), + QueueListItem( + item: item, + actualIndex: actualIndex, + indexOffset: indexOffset, + subqueue: _queue!, + isCurrentTrack: _currentTrack == item, ), - ), - // IconButton( - // icon: const Icon(TablerIcons.dots_vertical), - // iconSize: 28.0, - // onPressed: () async => {}, - // ), - IconButton( - icon: const Icon(TablerIcons.x), - iconSize: 28.0, - onPressed: () async => await _queueService.removeAtOffset(indexOffset), - ), - ], - ), - ), - onTap: () async => - await _queueService.skipByOffset(indexOffset), + ], + ); + } else { + return QueueListItem( + item: item, + actualIndex: actualIndex, + indexOffset: indexOffset, + subqueue: _queue!, + isCurrentTrack: _currentTrack == item, + ); + } + }, + onWillAccept: (data) { + return true; + }, + onAccept: (data) async { + int oldOffset = 0; + int newOffset = 0; + + int oldListIndex = 0; + const newListIndex = 2; + int oldItemIndex = -1; + int newItemIndex = index; + + // lookup current index of the item belonging to the data + if (_nextUp != null && _nextUp!.isNotEmpty) { + oldItemIndex = _nextUp!.indexOf(data); + oldListIndex = 1; + } + if (oldItemIndex == -1) { + oldItemIndex = _nextUp!.length + _queue!.indexOf(data); + oldListIndex = 2; + } + if (oldItemIndex == -1) { + oldItemIndex = _previousTracks!.indexOf(data); + oldListIndex = 0; + } + if (oldItemIndex == -1) { + // item probably became current track + return; + } + + // old index + if (oldListIndex == 0) { + // previous tracks + oldOffset = -((_previousTracks?.length ?? 0) - oldItemIndex); + } else if (oldListIndex == 1) { + // next up + oldOffset = oldItemIndex + 1; + } else if (oldListIndex == 2) { + // queue + oldOffset = oldItemIndex + _nextUp!.length + 1; + } + + // new index + if (newListIndex == 0) { + // previous tracks + newOffset = -((_previousTracks?.length ?? 0) - newItemIndex) + 1; + } else if ( + newListIndex == 1 && + oldListIndex == 2 // tracks can't be moved *to* next up, only *within* next up or *out of* next up + ) { + // next up + newOffset = newItemIndex + 1; + } else if (newListIndex == 2) { + // queue + newOffset = newItemIndex + _nextUp!.length + 1; + } else { + newOffset = oldOffset; + } + + if (oldOffset != newOffset) { + // setState(() { + // var movedItem = _contents[oldListIndex].children!.removeAt(oldItemIndex); + // _contents[newListIndex].children!.insert(newItemIndex, movedItem); + // }); + await _queueService.reorderByOffset(oldOffset, newOffset); + } + // setState(() { + // _queue?.insert(index, data); + // }); + }, ) - )); - }).toList() ?? [], - ), - DragAndDropList( - contentsWhenEmpty: const Text("Queue is empty"), - header: ListTile( - leading: const Icon(TablerIcons.layout_list), - title: Text(_source?.name != null ? "Playing from ${_source?.name}" : "Queue"), - ), - canDrag: false, - children: _queue?.asMap().entries.map((e) { - final index = e.key; - final item = e.value; - // final actualIndex = index; - // final indexOffset = -((_previousTracks?.length ?? 0) - index); - final actualIndex = index + _nextUp!.length; - final indexOffset = index + _nextUp!.length + 1; - - return DragAndDropItem(child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: ListTile( - visualDensity: VisualDensity.compact, - minVerticalPadding: 0.0, - contentPadding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0), - tileColor: _currentTrack == item - ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) - : null, - leading: AlbumImage( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(7.0), - bottomLeft: Radius.circular(7.0), - ), - item: item.item - .extras?["itemJson"] == null - ? null - : jellyfin_models.BaseItemDto.fromJson(item.item.extras?["itemJson"]), - ), - title: Text( - item.item.title ?? AppLocalizations.of(context)!.unknownName, - style: _currentTrack == item - ? TextStyle( - color: - Theme.of(context).colorScheme.secondary) - : null), - subtitle: Text(processArtist( - item.item.artist, - context)), - trailing: Container( - alignment: Alignment.centerRight, - margin: const EdgeInsets.only(right: 32.0), - width: 95.0, - height: 50.0, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "${item.item.duration?.inMinutes.toString().padLeft(2, '0')}:${((item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - textAlign: TextAlign.end, - style: TextStyle( - color: Theme.of(context).textTheme.bodySmall?.color, - ), - ), - // IconButton( - // icon: const Icon(TablerIcons.dots_vertical), - // iconSize: 28.0, - // onPressed: () async => {}, - // ), - IconButton( - icon: const Icon(TablerIcons.x), - iconSize: 28.0, - onPressed: () async => await _queueService.removeAtOffset(indexOffset), - ), - ], - ), - ), - onTap: () async => - await _queueService.skipByOffset(indexOffset), - ), - )); - }).toList() ?? [], + ); + }, ), ]; return CustomScrollView( controller: widget.scrollController, - slivers: [ - // const SliverPadding(padding: EdgeInsets.only(top: 0)), - DragAndDropLists( - listPadding: const EdgeInsets.only(top: 0.0), - - children: _contents, - onItemReorder: _onItemReorder, - onListReorder: _onListReorder, - itemOnWillAccept: (draggingItem, targetItem) { - //TODO this isn't working properly - if (targetItem.child.key == currentTrackKey) { - return false; - } - return true; - }, - itemDragOnLongPress: true, - sliverList: true, - scrollController: widget.scrollController, - itemDragHandle: DragHandle( - child: Padding( - padding: const EdgeInsets.only(right: 20.0), - child: Icon( - TablerIcons.grip_horizontal, - color: IconTheme.of(context).color, - size: 28.0, - ), - ), - ), - // mandatory, not actually needed because lists can't be dragged - listGhost: Padding( - padding: const EdgeInsets.symmetric(vertical: 30.0), - child: Center( - child: Container( - padding: const EdgeInsets.symmetric(vertical: 30.0, horizontal: 100.0), - decoration: BoxDecoration( - border: Border.all(), - borderRadius: BorderRadius.circular(7.0), - ), - child: const Icon(Icons.add_box), - ), - ), - ), - ), - ], + slivers: _contents, ); - } else { return const Center( child: CircularProgressIndicator.adaptive(), @@ -459,8 +594,8 @@ class _QueueListState extends State { ); } - _onItemReorder(int oldItemIndex, int oldListIndex, int newItemIndex, int newListIndex) async { - + _onItemReorder(int oldItemIndex, int oldListIndex, int newItemIndex, + int newListIndex) async { int oldOffset = 0; int newOffset = 0; @@ -480,19 +615,19 @@ class _QueueListState extends State { if (newListIndex == 0) { // previous tracks newOffset = -((_previousTracks?.length ?? 0) - newItemIndex); - } else if ( - newListIndex == 2 && - oldListIndex == 2 // tracks can't be moved *to* next up, only *within* next up or *out of* next up - ) { + } else if (newListIndex == 2 && + oldListIndex == + 2 // tracks can't be moved *to* next up, only *within* next up or *out of* next up + ) { // next up newOffset = newItemIndex + 1; - } else if (newListIndex == _contents.length -1) { + } else if (newListIndex == _contents.length - 1) { // queue newOffset = newItemIndex + _nextUp!.length + 1; } else { newOffset = oldOffset; } - + if (oldOffset != newOffset) { // setState(() { // var movedItem = _contents[oldListIndex].children!.removeAt(oldItemIndex); @@ -500,13 +635,11 @@ class _QueueListState extends State { // }); await _queueService.reorderByOffset(oldOffset, newOffset); } - } _onListReorder(int oldListIndex, int newListIndex) { return false; } - } Future showQueueBottomSheet(BuildContext context) { @@ -531,3 +664,45 @@ Future showQueueBottomSheet(BuildContext context) { }, ); } + +class SectionHeaderDelegate extends SliverPersistentHeaderDelegate { + final String title; + final bool controls; + final double height; + + SectionHeaderDelegate(this.title, [this.controls = false, this.height = 50]); + + @override + Widget build(context, double shrinkOffset, bool overlapsContent) { + return Flex( + direction: Axis.horizontal, + children: [ + Flexible( + child: Text(title), + ), + if (controls) + Row( + children: [ + IconButton( + icon: const Icon(TablerIcons.arrows_shuffle), + onPressed: () {}, + ), + IconButton( + icon: const Icon(TablerIcons.repeat), + onPressed: () {}, + ), + ], + ) + ], + ); + } + + @override + double get maxExtent => height; + + @override + double get minExtent => height; + + @override + bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => false; +} diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart new file mode 100644 index 000000000..725dd0b96 --- /dev/null +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -0,0 +1,175 @@ +import 'package:finamp/components/album_image.dart'; +import 'package:finamp/services/music_player_background_task.dart'; +import 'package:finamp/services/process_artist.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:finamp/models/finamp_models.dart'; +import 'package:finamp/models/jellyfin_models.dart' as jellyfin_models; +import 'package:finamp/services/queue_service.dart'; +import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; +import 'package:get_it/get_it.dart'; + +class QueueListItem extends StatefulWidget { + + late QueueItem item; + late int actualIndex; + late int indexOffset; + late List subqueue; + late bool isCurrentTrack; + + QueueListItem({ + Key? key, + required this.item, + required this.actualIndex, + required this.indexOffset, + required this.subqueue, + this.isCurrentTrack = false, + }) : super(key: key); + @override + State createState() => _QueueListItemState(); +} + +class _QueueListItemState extends State { + + final _audioHandler = GetIt.instance(); + final _queueService = GetIt.instance(); + + @override + Widget build(BuildContext context) { + return ListTile( + visualDensity: VisualDensity.compact, + minVerticalPadding: 0.0, + contentPadding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0), + tileColor: widget.isCurrentTrack + ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) + : null, + leading: AlbumImage( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(7.0), + bottomLeft: Radius.circular(7.0), + ), + item: widget.item.item + .extras?["itemJson"] == null + ? null + : jellyfin_models.BaseItemDto.fromJson(widget.item.item.extras?["itemJson"]), + ), + title: Text( + widget.item.item.title ?? AppLocalizations.of(context)!.unknownName, + style: this.widget.isCurrentTrack + ? TextStyle( + color: + Theme.of(context).colorScheme.secondary) + : null), + subtitle: Text(processArtist( + widget.item.item.artist, + context)), + trailing: Container( + alignment: Alignment.centerRight, + margin: const EdgeInsets.only(right: 32.0), + width: 115.0, + height: 50.0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${widget.item.item.duration?.inMinutes.toString().padLeft(2, '0')}:${((widget.item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + textAlign: TextAlign.end, + style: TextStyle( + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + // IconButton( + // icon: const Icon(TablerIcons.dots_vertical), + // iconSize: 28.0, + // onPressed: () async => {}, + // ), + IconButton( + icon: const Icon(TablerIcons.x), + iconSize: 28.0, + onPressed: () async => await _queueService.removeAtOffset(widget.indexOffset), + ), + Draggable( + data: widget.item, + axis: Axis.vertical, + dragAnchorStrategy: (draggable, context, position) => Offset(MediaQuery.of(context).size.width - 62.0, 0.0), + // feedback: QueueListItemGhost( + // item: widget.item, + // isCurrentTrack: widget.isCurrentTrack, + // ), + feedback: Container(), + childWhenDragging: Container(), + // key: ValueKey("${_queue![actualIndex].item.id}$actualIndex-drag"), + child: Icon( + TablerIcons.grip_horizontal, + color: IconTheme.of(context).color, + size: 28.0, + ), + ), + ], + ), + ), + onTap: () async => + await _queueService.skipByOffset(widget.indexOffset), + ); + } +} + +class QueueListItemGhost extends StatelessWidget { + + final QueueItem item; + final bool isCurrentTrack; + + const QueueListItemGhost({ + Key? key, + required this.item, + this.isCurrentTrack = false, + }) : super(key: key); + @override + Widget build(BuildContext context) { + return SizedBox( + width: 400.0, + // height: 90.0, + child: Card( + elevation: 8.0, + margin: EdgeInsets.zero, + child: ListTile( + visualDensity: VisualDensity.compact, + minVerticalPadding: 0.0, + contentPadding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0), + tileColor: this.isCurrentTrack + ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) + : null, + leading: AlbumImage( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(7.0), + bottomLeft: Radius.circular(7.0), + ), + item: this.item.item + .extras?["itemJson"] == null + ? null + : jellyfin_models.BaseItemDto.fromJson(this.item.item.extras?["itemJson"]), + ), + title: Text( + this.item.item.title ?? AppLocalizations.of(context)!.unknownName, + style: this.isCurrentTrack + ? TextStyle( + color: + Theme.of(context).colorScheme.secondary) + : null), + subtitle: Text(processArtist( + this.item.item.artist, + context)), + trailing: Container( + alignment: Alignment.centerRight, + margin: const EdgeInsets.only(right: 32.0), + width: 115.0, + height: 50.0, + ), + onTap: () async => {}, + ), + ), + ); + } + +} + From 12e4c77cd9b10ac15ec210aeaa791792e495f2a0 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 23 Jul 2023 22:43:21 +0200 Subject: [PATCH 040/130] theoretically working implementation using `flutter_reorderable_list` --- lib/components/PlayerScreen/queue_list.dart | 537 ++++-------------- .../PlayerScreen/queue_list_item.dart | 32 +- lib/models/finamp_models.dart | 11 +- pubspec.lock | 8 + pubspec.yaml | 1 + 5 files changed, 155 insertions(+), 434 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index a9ceab0e6..769d7feb8 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -1,12 +1,13 @@ import 'package:audio_service/audio_service.dart'; import 'package:drag_and_drop_lists/drag_and_drop_list_interface.dart'; import 'package:finamp/models/finamp_models.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide ReorderableList; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:drag_and_drop_lists/drag_and_drop_lists.dart'; import 'package:get_it/get_it.dart'; import 'package:rxdart/rxdart.dart'; +import 'package:flutter_reorderable_list/flutter_reorderable_list.dart'; import '../../services/finamp_settings_helper.dart'; import '../album_image.dart'; @@ -47,6 +48,9 @@ class _QueueListState extends State { late List _contents; + int indexBeforeDrag = -1; + int indexAfterDrag = -1; + @override void initState() { super.initState(); @@ -85,6 +89,44 @@ class _QueueListState extends State { ]; } + int _offsetOfKey(Key key) { + int oldOffset = 0; + + int oldListIndex = -1; + int oldItemIndex = -1; + + // lookup current index of the item belonging to the data + if (_nextUp != null && _nextUp!.isNotEmpty) { + oldItemIndex = _nextUp!.indexWhere((e) => ValueKey(e.id) == key); + oldListIndex = 1; + } + if (oldItemIndex == -1) { + oldItemIndex = _nextUp!.length + _queue!.indexWhere((e) => ValueKey(e.id) == key); + oldListIndex = 2; + } + if (oldItemIndex == -1) { + oldItemIndex = _previousTracks!.indexWhere((e) => ValueKey(e.id) == key); + oldListIndex = 0; + } + print("oldItemIndex: $oldItemIndex"); + print("oldListIndex: $oldListIndex"); + + // old index + if (oldListIndex == 0) { + // previous tracks + oldOffset = -((_previousTracks?.length ?? 0) - oldItemIndex); + } else if (oldListIndex == 1) { + // next up + oldOffset = oldItemIndex + 1; + } else if (oldListIndex == 2) { + // queue + oldOffset = oldItemIndex + _nextUp!.length + 1; + } + + return oldOffset; + + } + @override Widget build(BuildContext context) { return StreamBuilder<_QueueListStreamState>( @@ -139,35 +181,16 @@ class _QueueListState extends State { ), SliverList.builder( itemCount: _previousTracks?.length ?? 0, - // onReorder: (oldIndex, newIndex) async { - // final oldOffset = -((_previousTracks?.length ?? 0) - oldIndex); - // final newOffset = -((_previousTracks?.length ?? 0) - newIndex); - // // setState(() { - // // // _previousTracks?.insert(newIndex, _previousTracks![oldIndex]); - // // // _previousTracks?.removeAt(oldIndex); - // // int? smallerThanNewIndex; - // // if (oldIndex < newIndex) { - // // // When we're moving an item backwards, we need to reduce - // // // newIndex by 1 to account for there being a new item added - // // // before newIndex. - // // smallerThanNewIndex = newIndex - 1; - // // } - // // final item = _previousTracks?.removeAt(oldIndex); - // // _previousTracks?.insert(smallerThanNewIndex ?? newIndex, item!); - // // }); - // await _queueService.reorderByOffset(oldOffset, newOffset); - // }, itemBuilder: (context, index) { final item = _previousTracks![index]; final actualIndex = index; final indexOffset = -((_previousTracks?.length ?? 0) - index); - return Card( - key: ValueKey("${_queue![actualIndex].item.id}$actualIndex"), - child: DragTarget( - builder: ( + return ReorderableItem( + // key: ValueKey("${_queue![actualIndex].item.id}$actualIndex"), + key: ValueKey(_previousTracks![actualIndex].id), + childBuilder: ( BuildContext context, - List candidateData, - List rejectedData, + ReorderableItemState state ) { return QueueListItem( item: item, @@ -176,157 +199,10 @@ class _QueueListState extends State { subqueue: _previousTracks!, isCurrentTrack: _currentTrack == item, ); - }, - onWillAccept: (data) { - return true; - }, - onAccept: (data) async { - - int oldOffset = 0; - int newOffset = 0; - - int oldListIndex = 0; - const newListIndex = 0; - int oldItemIndex = -1; - int newItemIndex = index; - - // lookup current index of the item belonging to the data - if (_nextUp != null && _nextUp!.isNotEmpty) { - oldItemIndex = _nextUp!.indexOf(data); - oldListIndex = 1; - } - if (oldItemIndex == -1) { - oldItemIndex = _nextUp!.length + _queue!.indexOf(data); - oldListIndex = 2; - } - if (oldItemIndex == -1) { - oldItemIndex = _previousTracks!.indexOf(data); - oldListIndex = 0; - } - - // old index - if (oldListIndex == 0) { - // previous tracks - oldOffset = -((_previousTracks?.length ?? 0) - oldItemIndex); - } else if (oldListIndex == 1) { - // next up - oldOffset = oldItemIndex + 1; - } else if (oldListIndex == 2) { - // queue - oldOffset = oldItemIndex + _nextUp!.length + 1; - } - - // new index - if (newListIndex == 0) { - // previous tracks - newOffset = -((_previousTracks?.length ?? 0) - newItemIndex); - } else if ( - newListIndex == 1 && - oldListIndex == 2 // tracks can't be moved *to* next up, only *within* next up or *out of* next up - ) { - // next up - newOffset = newItemIndex + 1; - } else if (newListIndex == 2) { - // queue - newOffset = newItemIndex + _nextUp!.length + 1; - } else { - newOffset = oldOffset; - } - - if (oldOffset != newOffset) { - // setState(() { - // var movedItem = _contents[oldListIndex].children!.removeAt(oldItemIndex); - // _contents[newListIndex].children!.insert(newItemIndex, movedItem); - // }); - await _queueService.reorderByOffset(oldOffset, newOffset); - } - - // setState(() { - // _queue?.insert(index, data); - // }); - }, - ) + } ); }, ), - SliverList.list( - children: [ - DragTarget( - builder: ( - BuildContext context, - List candidateData, - List rejectedData, - ) { - return Container(height: 60.0); - }, - onWillAccept: (data) { - return true; - }, - onAccept: (data) async { - int oldOffset = 0; - int newOffset = 0; - - int oldListIndex = 0; - const newListIndex = 2; - int oldItemIndex = -1; - int newItemIndex = 0; - - // lookup current index of the item belonging to the data - if (_nextUp != null && _nextUp!.isNotEmpty) { - oldItemIndex = _nextUp!.indexOf(data); - oldListIndex = 1; - } - if (oldItemIndex == -1) { - oldItemIndex = _nextUp!.length + _queue!.indexOf(data); - oldListIndex = 2; - } - if (oldItemIndex == -1) { - oldItemIndex = _previousTracks!.indexOf(data); - oldListIndex = 0; - } - - // old index - if (oldListIndex == 0) { - // previous tracks - oldOffset = -((_previousTracks?.length ?? 0) - oldItemIndex); - } else if (oldListIndex == 1) { - // next up - oldOffset = oldItemIndex + 1; - } else if (oldListIndex == 2) { - // queue - oldOffset = oldItemIndex + _nextUp!.length + 1; - } - - // new index - if (newListIndex == 0) { - // previous tracks - newOffset = -((_previousTracks?.length ?? 0) - newItemIndex); - } else if ( - newListIndex == 1 && - oldListIndex == 2 // tracks can't be moved *to* next up, only *within* next up or *out of* next up - ) { - // next up - newOffset = newItemIndex + 1; - } else if (newListIndex == 2) { - // queue - newOffset = newItemIndex + _nextUp!.length + 1; - } else { - newOffset = oldOffset; - } - - if (oldOffset != newOffset) { - // setState(() { - // var movedItem = _contents[oldListIndex].children!.removeAt(oldItemIndex); - // _contents[newListIndex].children!.insert(newItemIndex, movedItem); - // }); - await _queueService.reorderByOffset(oldOffset, newOffset); - } - // setState(() { - // _queue?.insert(index, data); - // }); - }, - )] - ), // Current Track SliverPersistentHeader( delegate: SectionHeaderDelegate("Current Track"), @@ -368,222 +244,91 @@ class _QueueListState extends State { true, ), ), - SliverList.list( - children: [ - DragTarget( - builder: ( - BuildContext context, - List candidateData, - List rejectedData, - ) { - return Container(height: 20.0); - }, - onWillAccept: (data) { - return true; - }, - onAccept: (data) async { - int oldOffset = 0; - int newOffset = 0; - - int oldListIndex = 0; - const newListIndex = 2; - int oldItemIndex = -1; - int newItemIndex = 0; - - // lookup current index of the item belonging to the data - if (_nextUp != null && _nextUp!.isNotEmpty) { - oldItemIndex = _nextUp!.indexOf(data); - oldListIndex = 1; - } - if (oldItemIndex == -1) { - oldItemIndex = _nextUp!.length + _queue!.indexOf(data); - oldListIndex = 2; - } - if (oldItemIndex == -1) { - oldItemIndex = _previousTracks!.indexOf(data); - oldListIndex = 0; - } - - // old index - if (oldListIndex == 0) { - // previous tracks - oldOffset = -((_previousTracks?.length ?? 0) - oldItemIndex); - } else if (oldListIndex == 1) { - // next up - oldOffset = oldItemIndex + 1; - } else if (oldListIndex == 2) { - // queue - oldOffset = oldItemIndex + _nextUp!.length + 1; - } - - // new index - if (newListIndex == 0) { - // previous tracks - newOffset = -((_previousTracks?.length ?? 0) - newItemIndex) + 1; - } else if ( - newListIndex == 1 && - oldListIndex == 2 // tracks can't be moved *to* next up, only *within* next up or *out of* next up - ) { - // next up - newOffset = newItemIndex; - } else if (newListIndex == 2) { - // queue - newOffset = newItemIndex + _nextUp!.length; - } else { - newOffset = oldOffset; - } - - if (oldOffset != newOffset) { - // setState(() { - // var movedItem = _contents[oldListIndex].children!.removeAt(oldItemIndex); - // _contents[newListIndex].children!.insert(newItemIndex, movedItem); - // }); - await _queueService.reorderByOffset(oldOffset, newOffset); - } - // setState(() { - // _queue?.insert(index, data); - // }); - }, - )] - ), // Queue SliverList.builder( itemCount: _queue?.length ?? 0, - // onReorder: (oldIndex, newIndex) async { - // final oldOffset = oldIndex + 1; - // final newOffset = newIndex + 1; - // setState(() { - // // _queue?.insert(newIndex, _queue![oldIndex]); - // // _queue?.removeAt(oldIndex); - // int? smallerThanNewIndex; - // if (oldIndex < newIndex) { - // // When we're moving an item backwards, we need to reduce - // // newIndex by 1 to account for there being a new item added - // // before newIndex. - // smallerThanNewIndex = newIndex - 1; - // } - // final item = _queue?.removeAt(oldIndex); - // _queue?.insert(smallerThanNewIndex ?? newIndex, item!); - // }); - // await _queueService.reorderByOffset(oldOffset, newOffset); - // }, itemBuilder: (context, index) { final item = _queue![index]; final actualIndex = index; final indexOffset = index + 1; - return Card( - key: ValueKey("${_queue![actualIndex].item.id}$actualIndex"), - child: DragTarget( - builder: ( + return ReorderableItem( + key: ValueKey(_queue![actualIndex].id), + childBuilder: ( BuildContext context, - List candidateData, - List rejectedData, + ReorderableItemState state ) { - if (candidateData.isNotEmpty && candidateData.first != null && candidateData.first != item) { - return Column( - children: [ - QueueListItemGhost( - item: candidateData.first!, - isCurrentTrack: _currentTrack == item, - ), - QueueListItem( - item: item, - actualIndex: actualIndex, - indexOffset: indexOffset, - subqueue: _queue!, - isCurrentTrack: _currentTrack == item, - ), - ], - ); - } else { - return QueueListItem( - item: item, - actualIndex: actualIndex, - indexOffset: indexOffset, - subqueue: _queue!, - isCurrentTrack: _currentTrack == item, + // if (candidateData.isNotEmpty && candidateData.first != null && candidateData.first != item) { + // return Column( + // children: [ + // QueueListItemGhost( + // item: candidateData.first!, + // isCurrentTrack: _currentTrack == item, + // ), + // QueueListItem( + // item: item, + // actualIndex: actualIndex, + // indexOffset: indexOffset, + // subqueue: _queue!, + // isCurrentTrack: _currentTrack == item, + // ), + // ], + // ); + // } else { + return Container( + child: SafeArea( + top: false, + bottom: false, + child: Opacity( + // hide content for placeholder + opacity: state == ReorderableItemState.placeholder ? 0.0 : 1.0, + child: IntrinsicHeight( + child: QueueListItem( + item: item, + actualIndex: actualIndex, + indexOffset: indexOffset, + subqueue: _queue!, + isCurrentTrack: _currentTrack == item, + ), + ), + ), + ), ); - } + // } }, - onWillAccept: (data) { - return true; - }, - onAccept: (data) async { - int oldOffset = 0; - int newOffset = 0; - - int oldListIndex = 0; - const newListIndex = 2; - int oldItemIndex = -1; - int newItemIndex = index; - - // lookup current index of the item belonging to the data - if (_nextUp != null && _nextUp!.isNotEmpty) { - oldItemIndex = _nextUp!.indexOf(data); - oldListIndex = 1; - } - if (oldItemIndex == -1) { - oldItemIndex = _nextUp!.length + _queue!.indexOf(data); - oldListIndex = 2; - } - if (oldItemIndex == -1) { - oldItemIndex = _previousTracks!.indexOf(data); - oldListIndex = 0; - } - if (oldItemIndex == -1) { - // item probably became current track - return; - } - - // old index - if (oldListIndex == 0) { - // previous tracks - oldOffset = -((_previousTracks?.length ?? 0) - oldItemIndex); - } else if (oldListIndex == 1) { - // next up - oldOffset = oldItemIndex + 1; - } else if (oldListIndex == 2) { - // queue - oldOffset = oldItemIndex + _nextUp!.length + 1; - } - - // new index - if (newListIndex == 0) { - // previous tracks - newOffset = -((_previousTracks?.length ?? 0) - newItemIndex) + 1; - } else if ( - newListIndex == 1 && - oldListIndex == 2 // tracks can't be moved *to* next up, only *within* next up or *out of* next up - ) { - // next up - newOffset = newItemIndex + 1; - } else if (newListIndex == 2) { - // queue - newOffset = newItemIndex + _nextUp!.length + 1; - } else { - newOffset = oldOffset; - } - - if (oldOffset != newOffset) { - // setState(() { - // var movedItem = _contents[oldListIndex].children!.removeAt(oldItemIndex); - // _contents[newListIndex].children!.insert(newItemIndex, movedItem); - // }); - await _queueService.reorderByOffset(oldOffset, newOffset); - } - // setState(() { - // _queue?.insert(index, data); - // }); - }, - ) ); }, ), ]; - return CustomScrollView( - controller: widget.scrollController, - slivers: _contents, + // return CustomScrollView( + // controller: widget.scrollController, + // slivers: _contents, + // ); + return ReorderableList( + onReorder: (Key draggedItem, Key newPosition) { + int draggingIndex = _offsetOfKey(draggedItem); + int newPositionIndex = _offsetOfKey(newPosition); + indexBeforeDrag = draggingIndex; + indexAfterDrag = newPositionIndex; + setState(() { + print("$draggingIndex -> $newPositionIndex"); + //FIXME this is a slow operation, ideally we should just swap the items in the list + // problem with that is that we're using a streambuilder, so we can't just change the list + _queueService.reorderByOffset(indexBeforeDrag, indexAfterDrag); + // _queue!.removeAt(draggingIndex); + // _queue!.insert(newPositionIndex, draggedQueueItem); + }); + return true; + }, + // onReorderDone: (Key item) { + // // int newPositionIndex = _indexOfKey(item); + // // debugPrint("Reordering finished for ${draggedItem.title}}"); + // _queueService.reorderByOffset(indexBeforeDrag, indexAfterDrag); + // }, + child: CustomScrollView( + controller: widget.scrollController, + slivers: _contents, + ), ); } else { return const Center( @@ -594,52 +339,6 @@ class _QueueListState extends State { ); } - _onItemReorder(int oldItemIndex, int oldListIndex, int newItemIndex, - int newListIndex) async { - int oldOffset = 0; - int newOffset = 0; - - // old index - if (oldListIndex == 0) { - // previous tracks - oldOffset = -((_previousTracks?.length ?? 0) - oldItemIndex); - } else if (oldListIndex == 2) { - // next up - oldOffset = oldItemIndex + 1; - } else if (oldListIndex == _contents.length - 1) { - // queue - oldOffset = oldItemIndex + _nextUp!.length + 1; - } - - // new index - if (newListIndex == 0) { - // previous tracks - newOffset = -((_previousTracks?.length ?? 0) - newItemIndex); - } else if (newListIndex == 2 && - oldListIndex == - 2 // tracks can't be moved *to* next up, only *within* next up or *out of* next up - ) { - // next up - newOffset = newItemIndex + 1; - } else if (newListIndex == _contents.length - 1) { - // queue - newOffset = newItemIndex + _nextUp!.length + 1; - } else { - newOffset = oldOffset; - } - - if (oldOffset != newOffset) { - // setState(() { - // var movedItem = _contents[oldListIndex].children!.removeAt(oldItemIndex); - // _contents[newListIndex].children!.insert(newItemIndex, movedItem); - // }); - await _queueService.reorderByOffset(oldOffset, newOffset); - } - } - - _onListReorder(int oldListIndex, int newListIndex) { - return false; - } } Future showQueueBottomSheet(BuildContext context) { diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index 725dd0b96..dac2d6831 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -1,13 +1,14 @@ import 'package:finamp/components/album_image.dart'; import 'package:finamp/services/music_player_background_task.dart'; import 'package:finamp/services/process_artist.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide ReorderableList; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/models/jellyfin_models.dart' as jellyfin_models; import 'package:finamp/services/queue_service.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get_it/get_it.dart'; +import 'package:flutter_reorderable_list/flutter_reorderable_list.dart'; class QueueListItem extends StatefulWidget { @@ -88,17 +89,24 @@ class _QueueListItemState extends State { iconSize: 28.0, onPressed: () async => await _queueService.removeAtOffset(widget.indexOffset), ), - Draggable( - data: widget.item, - axis: Axis.vertical, - dragAnchorStrategy: (draggable, context, position) => Offset(MediaQuery.of(context).size.width - 62.0, 0.0), - // feedback: QueueListItemGhost( - // item: widget.item, - // isCurrentTrack: widget.isCurrentTrack, - // ), - feedback: Container(), - childWhenDragging: Container(), - // key: ValueKey("${_queue![actualIndex].item.id}$actualIndex-drag"), + // Draggable( + // data: widget.item, + // axis: Axis.vertical, + // dragAnchorStrategy: (draggable, context, position) => Offset(MediaQuery.of(context).size.width - 62.0, 0.0), + // // feedback: QueueListItemGhost( + // // item: widget.item, + // // isCurrentTrack: widget.isCurrentTrack, + // // ), + // feedback: Container(), + // childWhenDragging: Container(), + // // key: ValueKey("${_queue![actualIndex].item.id}$actualIndex-drag"), + // child: Icon( + // TablerIcons.grip_horizontal, + // color: IconTheme.of(context).color, + // size: 28.0, + // ), + // ), + ReorderableListener( child: Icon( TablerIcons.grip_horizontal, color: IconTheme.of(context).color, diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 678b021b7..d5577cd1f 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -612,15 +612,20 @@ class QueueItem { required this.item, required this.source, this.type = QueueItemQueueType.queue, - }); + }) { + id = const Uuid().v4(); + } @HiveField(0) - MediaItem item; + late String id; @HiveField(1) - QueueItemSource source; + MediaItem item; @HiveField(2) + QueueItemSource source; + + @HiveField(3) QueueItemQueueType type; } diff --git a/pubspec.lock b/pubspec.lock index 9e288f7b2..c490f8870 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -412,6 +412,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.9" + flutter_reorderable_list: + dependency: "direct main" + description: + name: flutter_reorderable_list + sha256: "0400ef34fa00b7cac69f71efc92d7e49727f425bc1080180ebe70bf47618afe0" + url: "https://pub.dev" + source: hosted + version: "1.3.1" flutter_riverpod: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 6f60f8cb1..ea2815ad0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -78,6 +78,7 @@ dependencies: git: url: https://github.com/lamarios/locale_names.git ref: cea057c220f4ee7e09e8f1fc7036110245770948 + flutter_reorderable_list: ^1.3.1 dev_dependencies: flutter_test: From 92ed02daedebd1365be2b43bb05a8c78db74965f Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Mon, 24 Jul 2023 17:56:54 +0200 Subject: [PATCH 041/130] update internal list during reorder instead of going through QueueService --- lib/components/PlayerScreen/queue_list.dart | 68 +++++++++++++++++---- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 769d7feb8..fb55ca3cb 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -48,8 +48,8 @@ class _QueueListState extends State { late List _contents; - int indexBeforeDrag = -1; - int indexAfterDrag = -1; + int offsetBeforeDrag = -1; + int offsetAfterDrag = -1; @override void initState() { @@ -306,18 +306,62 @@ class _QueueListState extends State { // ); return ReorderableList( onReorder: (Key draggedItem, Key newPosition) { - int draggingIndex = _offsetOfKey(draggedItem); - int newPositionIndex = _offsetOfKey(newPosition); - indexBeforeDrag = draggingIndex; - indexAfterDrag = newPositionIndex; + int draggingOffset = _offsetOfKey(draggedItem); + int newPositionOffset = _offsetOfKey(newPosition); + offsetBeforeDrag = draggingOffset; + offsetAfterDrag = newPositionOffset; + print("$draggingOffset -> $newPositionOffset"); + // setState(() { + // //FIXME this is a slow operation, ideally we should just swap the items in the list + // // problem with that is that we're using a streambuilder, so we can't just change the list + // _queueService.reorderByOffset(indexBeforeDrag, indexAfterDrag); + // }); + + int indexBeforeDrag = 0; + int indexAfterDrag = 0; + List? listToUseBefore = _queue; + List? listToUseAfter = _queue; + + if (offsetBeforeDrag > 0) { + if (_nextUp!.length > 0 && offsetBeforeDrag <= _nextUp!.length) { + indexBeforeDrag = offsetBeforeDrag - 1; + listToUseBefore = _nextUp; + } else { + indexBeforeDrag = offsetBeforeDrag - _nextUp!.length - 1; + listToUseBefore = _queue; + } + } else if (offsetBeforeDrag < 0) { + indexBeforeDrag = _previousTracks!.length + offsetBeforeDrag; + listToUseBefore = _previousTracks; + } else { + // the current track can't be reordered + return false; + } + + if (offsetAfterDrag > 0) { + if (_nextUp!.length > 0 && offsetAfterDrag <= _nextUp!.length) { + indexAfterDrag = offsetAfterDrag - 1; + listToUseAfter = _nextUp; + } else { + indexAfterDrag = offsetAfterDrag - _nextUp!.length - 1; + listToUseAfter = _queue; + } + } else if (offsetAfterDrag < 0) { + indexAfterDrag = _previousTracks!.length + offsetAfterDrag; + listToUseAfter = _previousTracks; + } else { + // the current track can't be reordered + return false; + } + + print("indexBeforeDrag: $indexBeforeDrag"); + print("indexAfterDrag: $indexAfterDrag"); + setState(() { - print("$draggingIndex -> $newPositionIndex"); - //FIXME this is a slow operation, ideally we should just swap the items in the list - // problem with that is that we're using a streambuilder, so we can't just change the list - _queueService.reorderByOffset(indexBeforeDrag, indexAfterDrag); - // _queue!.removeAt(draggingIndex); - // _queue!.insert(newPositionIndex, draggedQueueItem); + final draggedQueueItem = listToUseBefore!.removeAt(indexBeforeDrag); + listToUseAfter!.insert(indexAfterDrag, draggedQueueItem); }); + return true; }, // onReorderDone: (Key item) { From 91ebd37b044738334d19131829a48127a98a8276 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Mon, 24 Jul 2023 20:12:19 +0200 Subject: [PATCH 042/130] attempted to separate queue list into separate widgets with independent streambuilders --- lib/components/PlayerScreen/queue_list.dart | 518 ++++++++++++-------- lib/services/queue_service.dart | 89 +++- 2 files changed, 400 insertions(+), 207 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index fb55ca3cb..26e3cec54 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -55,6 +55,14 @@ class _QueueListState extends State { void initState() { super.initState(); + _queueService.getQueueStream().listen((queueInfo) { + _previousTracks = queueInfo.previousTracks; + _currentTrack = queueInfo.currentTrack; + _nextUp = queueInfo.nextUp; + _queue = queueInfo.queue; + _source = queueInfo.source; + }); + _contents = [ // const SliverPadding(padding: EdgeInsets.only(top: 0)), // Previous Tracks @@ -129,49 +137,6 @@ class _QueueListState extends State { @override Widget build(BuildContext context) { - return StreamBuilder<_QueueListStreamState>( - // stream: AudioService.queueStream, - stream: Rx.combineLatest2( - mediaStateStream, - _queueService.getQueueStream(), - (a, b) => _QueueListStreamState(a, b)), - // stream: _queueService.getQueueStream(), - builder: (context, snapshot) { - if (snapshot.hasData) { - _previousTracks ??= snapshot.data!.queueInfo.previousTracks; - _currentTrack = snapshot.data!.queueInfo.currentTrack ?? - QueueItem( - item: const MediaItem( - id: "", - title: "No track playing", - album: "No album", - artist: "No artist"), - source: QueueItemSource( - id: "", name: "", type: QueueItemSourceType.unknown)); - _nextUp ??= snapshot.data!.queueInfo.nextUp; - _queue ??= snapshot.data!.queueInfo.queue; - _source ??= snapshot.data!.queueInfo.source; - - final GlobalKey currentTrackKey = - GlobalKey(debugLabel: "currentTrack"); - - void scrollToCurrentTrack() { - widget.scrollController.animateTo( - ((_previousTracks?.length ?? 0) * 60 + 20).toDouble(), - duration: const Duration(milliseconds: 200), - curve: Curves.linear); - // final targetContext = currentTrackKey.currentContext; - // if (targetContext != null) { - // Scrollable.ensureVisible(targetContext!, - // duration: const Duration(milliseconds: 200), - // curve: Curves.linear - // ); - // } - } - // scroll to current track after sheet has been opened - //TODO fix this - // WidgetsBinding.instance - // .addPostFrameCallback((_) => scrollToCurrentTrack()); _contents = [ // const SliverPadding(padding: EdgeInsets.only(top: 0)), @@ -179,63 +144,64 @@ class _QueueListState extends State { SliverPersistentHeader( delegate: SectionHeaderDelegate("Previous Tracks"), ), - SliverList.builder( - itemCount: _previousTracks?.length ?? 0, - itemBuilder: (context, index) { - final item = _previousTracks![index]; - final actualIndex = index; - final indexOffset = -((_previousTracks?.length ?? 0) - index); - return ReorderableItem( - // key: ValueKey("${_queue![actualIndex].item.id}$actualIndex"), - key: ValueKey(_previousTracks![actualIndex].id), - childBuilder: ( - BuildContext context, - ReorderableItemState state - ) { - return QueueListItem( - item: item, - actualIndex: actualIndex, - indexOffset: indexOffset, - subqueue: _previousTracks!, - isCurrentTrack: _currentTrack == item, - ); - } - ); - }, - ), - // Current Track - SliverPersistentHeader( - delegate: SectionHeaderDelegate("Current Track"), - ), - SliverAppBar( - key: currentTrackKey, - pinned: true, - collapsedHeight: 70.0, - expandedHeight: 70.0, - leading: const Padding( - padding: EdgeInsets.zero, - ), - flexibleSpace: ListTile( - leading: AlbumImage( - item: _currentTrack!.item - .extras?["itemJson"] == null - ? null - : jellyfin_models.BaseItemDto.fromJson(_currentTrack!.item.extras?["itemJson"]), - ), - title: Text( - _currentTrack!.item.title ?? AppLocalizations.of(context)!.unknownName, - style: _currentTrack == _currentTrack! - ? TextStyle( - color: - Theme.of(context).colorScheme.secondary) - : null), - subtitle: Text(processArtist( - _currentTrack!.item.artist, - context)), - onTap: () async => - snapshot.data!.mediaState.playbackState.playing ? await _audioHandler.pause() : await _audioHandler.play(), - ), - ), + // SliverList.builder( + // itemCount: _previousTracks?.length ?? 0, + // itemBuilder: (context, index) { + // final item = _previousTracks![index]; + // final actualIndex = index; + // final indexOffset = -((_previousTracks?.length ?? 0) - index); + // return ReorderableItem( + // // key: ValueKey("${_queue![actualIndex].item.id}$actualIndex"), + // key: ValueKey(_previousTracks![actualIndex].id), + // childBuilder: ( + // BuildContext context, + // ReorderableItemState state + // ) { + // return QueueListItem( + // item: item, + // actualIndex: actualIndex, + // indexOffset: indexOffset, + // subqueue: _previousTracks!, + // isCurrentTrack: _currentTrack == item, + // ); + // } + // ); + // }, + // ), + const PreviousTracksList(), + // // Current Track + // SliverPersistentHeader( + // delegate: SectionHeaderDelegate("Current Track"), + // ), + // SliverAppBar( + // key: currentTrackKey, + // pinned: true, + // collapsedHeight: 70.0, + // expandedHeight: 70.0, + // leading: const Padding( + // padding: EdgeInsets.zero, + // ), + // flexibleSpace: ListTile( + // leading: AlbumImage( + // item: _currentTrack!.item + // .extras?["itemJson"] == null + // ? null + // : jellyfin_models.BaseItemDto.fromJson(_currentTrack!.item.extras?["itemJson"]), + // ), + // title: Text( + // _currentTrack!.item.title ?? AppLocalizations.of(context)!.unknownName, + // style: _currentTrack == _currentTrack! + // ? TextStyle( + // color: + // Theme.of(context).colorScheme.secondary) + // : null), + // subtitle: Text(processArtist( + // _currentTrack!.item.artist, + // context)), + // onTap: () async => + // snapshot.data!.mediaState.playbackState.playing ? await _audioHandler.pause() : await _audioHandler.play(), + // ), + // ), SliverPersistentHeader( delegate: SectionHeaderDelegate( _source?.name != null @@ -245,59 +211,60 @@ class _QueueListState extends State { ), ), // Queue - SliverList.builder( - itemCount: _queue?.length ?? 0, - itemBuilder: (context, index) { - final item = _queue![index]; - final actualIndex = index; - final indexOffset = index + 1; - return ReorderableItem( - key: ValueKey(_queue![actualIndex].id), - childBuilder: ( - BuildContext context, - ReorderableItemState state - ) { - // if (candidateData.isNotEmpty && candidateData.first != null && candidateData.first != item) { - // return Column( - // children: [ - // QueueListItemGhost( - // item: candidateData.first!, - // isCurrentTrack: _currentTrack == item, - // ), - // QueueListItem( - // item: item, - // actualIndex: actualIndex, - // indexOffset: indexOffset, - // subqueue: _queue!, - // isCurrentTrack: _currentTrack == item, - // ), - // ], - // ); - // } else { - return Container( - child: SafeArea( - top: false, - bottom: false, - child: Opacity( - // hide content for placeholder - opacity: state == ReorderableItemState.placeholder ? 0.0 : 1.0, - child: IntrinsicHeight( - child: QueueListItem( - item: item, - actualIndex: actualIndex, - indexOffset: indexOffset, - subqueue: _queue!, - isCurrentTrack: _currentTrack == item, - ), - ), - ), - ), - ); - // } - }, - ); - }, - ), + // SliverList.builder( + // itemCount: _queue?.length ?? 0, + // itemBuilder: (context, index) { + // final item = _queue![index]; + // final actualIndex = index; + // final indexOffset = index + 1; + // return ReorderableItem( + // key: ValueKey(_queue![actualIndex].id), + // childBuilder: ( + // BuildContext context, + // ReorderableItemState state + // ) { + // // if (candidateData.isNotEmpty && candidateData.first != null && candidateData.first != item) { + // // return Column( + // // children: [ + // // QueueListItemGhost( + // // item: candidateData.first!, + // // isCurrentTrack: _currentTrack == item, + // // ), + // // QueueListItem( + // // item: item, + // // actualIndex: actualIndex, + // // indexOffset: indexOffset, + // // subqueue: _queue!, + // // isCurrentTrack: _currentTrack == item, + // // ), + // // ], + // // ); + // // } else { + // return Container( + // child: SafeArea( + // top: false, + // bottom: false, + // child: Opacity( + // // hide content for placeholder + // opacity: state == ReorderableItemState.placeholder ? 0.0 : 1.0, + // child: IntrinsicHeight( + // child: QueueListItem( + // item: item, + // actualIndex: actualIndex, + // indexOffset: indexOffset, + // subqueue: _queue!, + // isCurrentTrack: _currentTrack == item, + // ), + // ), + // ), + // ), + // ); + // // } + // }, + // ); + // }, + // ), + const QueueTracksList(), ]; // return CustomScrollView( @@ -311,56 +278,60 @@ class _QueueListState extends State { offsetBeforeDrag = draggingOffset; offsetAfterDrag = newPositionOffset; print("$draggingOffset -> $newPositionOffset"); - // setState(() { - // //FIXME this is a slow operation, ideally we should just swap the items in the list - // // problem with that is that we're using a streambuilder, so we can't just change the list - // _queueService.reorderByOffset(indexBeforeDrag, indexAfterDrag); - // }); - - int indexBeforeDrag = 0; - int indexAfterDrag = 0; - List? listToUseBefore = _queue; - List? listToUseAfter = _queue; - - if (offsetBeforeDrag > 0) { - if (_nextUp!.length > 0 && offsetBeforeDrag <= _nextUp!.length) { - indexBeforeDrag = offsetBeforeDrag - 1; - listToUseBefore = _nextUp; - } else { - indexBeforeDrag = offsetBeforeDrag - _nextUp!.length - 1; - listToUseBefore = _queue; - } - } else if (offsetBeforeDrag < 0) { - indexBeforeDrag = _previousTracks!.length + offsetBeforeDrag; - listToUseBefore = _previousTracks; - } else { - // the current track can't be reordered - return false; - } - - if (offsetAfterDrag > 0) { - if (_nextUp!.length > 0 && offsetAfterDrag <= _nextUp!.length) { - indexAfterDrag = offsetAfterDrag - 1; - listToUseAfter = _nextUp; - } else { - indexAfterDrag = offsetAfterDrag - _nextUp!.length - 1; - listToUseAfter = _queue; - } - } else if (offsetAfterDrag < 0) { - indexAfterDrag = _previousTracks!.length + offsetAfterDrag; - listToUseAfter = _previousTracks; + if (mounted) { + setState(() { + //FIXME this is a slow operation, ideally we should just swap the items in the list + // problem with that is that we're using a streambuilder, so we can't just change the list + _queueService.reorderByOffset(offsetBeforeDrag, offsetAfterDrag); + }); } else { - // the current track can't be reordered - return false; + print("NOT MOUNTED"); } - print("indexBeforeDrag: $indexBeforeDrag"); - print("indexAfterDrag: $indexAfterDrag"); + // int indexBeforeDrag = 0; + // int indexAfterDrag = 0; + // List? listToUseBefore = _queue; + // List? listToUseAfter = _queue; + + // if (offsetBeforeDrag > 0) { + // if (_nextUp!.length > 0 && offsetBeforeDrag <= _nextUp!.length) { + // indexBeforeDrag = offsetBeforeDrag - 1; + // listToUseBefore = _nextUp; + // } else { + // indexBeforeDrag = offsetBeforeDrag - _nextUp!.length - 1; + // listToUseBefore = _queue; + // } + // } else if (offsetBeforeDrag < 0) { + // indexBeforeDrag = _previousTracks!.length + offsetBeforeDrag; + // listToUseBefore = _previousTracks; + // } else { + // // the current track can't be reordered + // return false; + // } + + // if (offsetAfterDrag > 0) { + // if (_nextUp!.length > 0 && offsetAfterDrag <= _nextUp!.length) { + // indexAfterDrag = offsetAfterDrag - 1; + // listToUseAfter = _nextUp; + // } else { + // indexAfterDrag = offsetAfterDrag - _nextUp!.length - 1; + // listToUseAfter = _queue; + // } + // } else if (offsetAfterDrag < 0) { + // indexAfterDrag = _previousTracks!.length + offsetAfterDrag; + // listToUseAfter = _previousTracks; + // } else { + // // the current track can't be reordered + // return false; + // } + + // print("indexBeforeDrag: $indexBeforeDrag"); + // print("indexAfterDrag: $indexAfterDrag"); - setState(() { - final draggedQueueItem = listToUseBefore!.removeAt(indexBeforeDrag); - listToUseAfter!.insert(indexAfterDrag, draggedQueueItem); - }); + // setState(() { + // final draggedQueueItem = listToUseBefore!.removeAt(indexBeforeDrag); + // listToUseAfter!.insert(indexAfterDrag, draggedQueueItem); + // }); return true; }, @@ -374,13 +345,6 @@ class _QueueListState extends State { slivers: _contents, ), ); - } else { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); - } - }, - ); } } @@ -408,6 +372,148 @@ Future showQueueBottomSheet(BuildContext context) { ); } +class PreviousTracksList extends StatefulWidget { + + const PreviousTracksList({ + Key? key, + }) : super(key: key); + + @override + State createState() => _PreviousTracksListState(); +} + +class _PreviousTracksListState extends State { + + final _queueService = GetIt.instance(); + List? _previousTracks; + + @override + Widget build(context) { + return StreamBuilder>( + // stream: AudioService.queueStream, + // stream: Rx.combineLatest2( + // mediaStateStream, + // _queueService.getQueueStream(), + // (a, b) => _QueueListStreamState(a, b)), + stream: _queueService.getPreviousTracksStream(), + builder: (context, snapshot) { + + if (snapshot.hasData) { + + _previousTracks ??= snapshot.data!; + + return SliverList.builder( + itemCount: _previousTracks?.length ?? 0, + itemBuilder: (context, index) { + final item = _previousTracks![index]; + final actualIndex = index; + final indexOffset = -((_previousTracks?.length ?? 0) - index); + return ReorderableItem( + // key: ValueKey("${_queue![actualIndex].item.id}$actualIndex"), + key: ValueKey(_previousTracks![actualIndex].id), + childBuilder: ( + BuildContext context, + ReorderableItemState state + ) { + return SafeArea( + top: false, + bottom: false, + child: Opacity( + // hide content for placeholder + opacity: state == ReorderableItemState.placeholder ? 0.0 : 1.0, + child: IntrinsicHeight( + child: QueueListItem( + item: item, + actualIndex: actualIndex, + indexOffset: indexOffset, + subqueue: _previousTracks!, + isCurrentTrack: false, + ), + ), + ), + ); + } + ); + }, + ); + + } else { + return SliverList(delegate: SliverChildListDelegate([])); + } + }, + ); + } +} + +class QueueTracksList extends StatefulWidget { + + const QueueTracksList({ + Key? key, + }) : super(key: key); + + @override + State createState() => _QueueTracksListState(); +} + +class _QueueTracksListState extends State { + + final _queueService = GetIt.instance(); + List? _queue; + + @override + Widget build(context) { + return StreamBuilder>( + // stream: AudioService.queueStream, + stream: _queueService.getQueueTracksStream(), + // stream: _queueService.getQueueStream(), + builder: (context, snapshot) { + + if (snapshot.hasData) { + _queue ??= snapshot.data!; + + return SliverList.builder( + itemCount: _queue?.length ?? 0, + itemBuilder: (context, index) { + final item = _queue![index]; + final actualIndex = index; + final indexOffset = -((_queue?.length ?? 0) - index); + return ReorderableItem( + // key: ValueKey("${_queue![actualIndex].item.id}$actualIndex"), + key: ValueKey(_queue![actualIndex].id), + childBuilder: ( + BuildContext context, + ReorderableItemState state + ) { + return SafeArea( + top: false, + bottom: false, + child: Opacity( + // hide content for placeholder + opacity: state == ReorderableItemState.placeholder ? 0.0 : 1.0, + child: IntrinsicHeight( + child: QueueListItem( + item: item, + actualIndex: actualIndex, + indexOffset: indexOffset, + subqueue: _queue!, + isCurrentTrack: false, + ), + ), + ), + ); + } + ); + }, + ); + + } else { + return SliverList(delegate: SliverChildListDelegate([])); + } + }, + ); + } +} + class SectionHeaderDelegate extends SliverPersistentHeaderDelegate { final String title; final bool controls; diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index f37fa1761..712a8b2c5 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -49,6 +49,15 @@ class QueueService { nextUp: [], source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown), )); + final _previousTracksStream = BehaviorSubject>.seeded( + List.empty(growable: true), + ); + final _queueTracksStream = BehaviorSubject>.seeded( + List.empty(growable: true), + ); + final _nextUpTracksStream = BehaviorSubject>.seeded( + List.empty(growable: true), + ); // external queue state @@ -95,6 +104,10 @@ class QueueService { List allTracks = _audioHandler.effectiveSequence?.map((e) => e.tag as QueueItem).toList() ?? []; int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; + List _oldQueue = List.from(_queue); + List _oldQueuePreviousTracks = List.from(_queuePreviousTracks); + List _oldQueueNextUp = List.from(_queueNextUp); + _queuePreviousTracks.clear(); _queueNextUp.clear(); _queue.clear(); @@ -132,7 +145,69 @@ class QueueService { } - _queueStream.add(getQueue()); + + // final oldQueueInfo = _queueStream.value; + if (_queue.isEmpty) { + _queueServiceLogger.finer("New queue info is empty"); + } + if (_oldQueue.isEmpty) { + _queueServiceLogger.finer("Old queue info is empty"); + } + bool isQueueChanged = false; + for (int i = 0; i < _queuePreviousTracks.length; i++) { + if (_oldQueuePreviousTracks.length > i) { + if (_queuePreviousTracks[i].id != _oldQueuePreviousTracks[i].id) { + isQueueChanged = true; + break; + } + } else { + isQueueChanged = true; + break; + } + } + if (isQueueChanged) { + _queueServiceLogger.finer("Previous tracks changed"); + _previousTracksStream.add(_queuePreviousTracks); + } + + isQueueChanged = false; + for (int i = 0; i < _queueNextUp.length; i++) { + if (_oldQueueNextUp.length > i) { + if (_queueNextUp[i].id != _oldQueueNextUp[i].id) { + isQueueChanged = true; + break; + } + } else { + isQueueChanged = true; + break; + } + } + if (isQueueChanged) { + _queueServiceLogger.finer("Next Up changed"); + _nextUpTracksStream.add(_queueNextUp); + } + + isQueueChanged = false; + for (int i = 0; i < _queue.length; i++) { + // _queueServiceLogger.finer("i: $i"); + // _queueServiceLogger.finer("condition: ${oldQueueInfo.queue.length > i}"); + if (_oldQueue.length > i) { + if (_queue[i].id != _oldQueue[i].id) { + isQueueChanged = true; + break; + } + } else { + isQueueChanged = true; + break; + } + } + if (isQueueChanged) { + _queueServiceLogger.finer("Queue changed"); + _queueTracksStream.add(_queue); + } + + final newQueueInfo = getQueue(); + _queueStream.add(newQueueInfo); if (_currentTrack != null) { _currentTrackStream.add(_currentTrack!); _audioHandler.mediaItem.add(_currentTrack!.item); @@ -358,6 +433,18 @@ class QueueService { return _queueStream; } + BehaviorSubject> getPreviousTracksStream() { + return _previousTracksStream; + } + + BehaviorSubject> getQueueTracksStream() { + return _queueTracksStream; + } + + BehaviorSubject> getNextUpTracksStream() { + return _nextUpTracksStream; + } + BehaviorSubject getCurrentTrackStream() { return _currentTrackStream; } From 2e02350127b85240eb23482633228d5383802915 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 30 Jul 2023 17:39:27 +0200 Subject: [PATCH 043/130] use visibility instead of opacity --- lib/components/PlayerScreen/queue_list.dart | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index fb55ca3cb..130352227 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -274,13 +274,12 @@ class _QueueListState extends State { // ], // ); // } else { - return Container( - child: SafeArea( - top: false, - bottom: false, - child: Opacity( + return Visibility( // hide content for placeholder - opacity: state == ReorderableItemState.placeholder ? 0.0 : 1.0, + visible: state != ReorderableItemState.placeholder, + maintainSize: true, + maintainAnimation: true, + maintainState: true, child: IntrinsicHeight( child: QueueListItem( item: item, @@ -290,8 +289,6 @@ class _QueueListState extends State { isCurrentTrack: _currentTrack == item, ), ), - ), - ), ); // } }, From d36eb6af17f35762c3177114f185c7e40d2fd71d Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 30 Jul 2023 22:48:06 +0200 Subject: [PATCH 044/130] huge queue redesign progress - implemented separate widgets for different queues - reordering only possible within queues - updated design to be closer to the mockup --- lib/components/PlayerScreen/queue_list.dart | 882 ++++++++++-------- .../PlayerScreen/queue_list_item.dart | 212 ++--- .../music_player_background_task.dart | 8 + lib/services/queue_service.dart | 2 +- 4 files changed, 593 insertions(+), 511 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 26e3cec54..845a36b4a 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -1,13 +1,12 @@ import 'package:audio_service/audio_service.dart'; import 'package:drag_and_drop_lists/drag_and_drop_list_interface.dart'; import 'package:finamp/models/finamp_models.dart'; -import 'package:flutter/material.dart' hide ReorderableList; +import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:drag_and_drop_lists/drag_and_drop_lists.dart'; import 'package:get_it/get_it.dart'; import 'package:rxdart/rxdart.dart'; -import 'package:flutter_reorderable_list/flutter_reorderable_list.dart'; import '../../services/finamp_settings_helper.dart'; import '../album_image.dart'; @@ -21,10 +20,12 @@ import 'queue_list_item.dart'; class _QueueListStreamState { _QueueListStreamState( this.mediaState, + this.playbackState, this.queueInfo, ); final MediaState mediaState; + final PlaybackState playbackState; final QueueInfo queueInfo; } @@ -38,28 +39,17 @@ class QueueList extends StatefulWidget { } class _QueueListState extends State { - final _audioHandler = GetIt.instance(); final _queueService = GetIt.instance(); - List? _previousTracks; - QueueItem? _currentTrack; - List? _nextUp; - List? _queue; + QueueItemSource? _source; late List _contents; - int offsetBeforeDrag = -1; - int offsetAfterDrag = -1; - @override void initState() { super.initState(); _queueService.getQueueStream().listen((queueInfo) { - _previousTracks = queueInfo.previousTracks; - _currentTrack = queueInfo.currentTrack; - _nextUp = queueInfo.nextUp; - _queue = queueInfo.queue; _source = queueInfo.source; }); @@ -71,25 +61,21 @@ class _QueueListState extends State { ), // Current Track SliverAppBar( - pinned: true, - collapsedHeight: 70.0, - expandedHeight: 70.0, - leading: const Padding( - padding: EdgeInsets.zero, - ), - flexibleSpace: ListTile( - leading: const AlbumImage( - item: null, + pinned: true, + collapsedHeight: 70.0, + expandedHeight: 70.0, + leading: const Padding( + padding: EdgeInsets.zero, ), - title: Text( - "Unknown song"), - subtitle: Text("Unknown artist"), - onTap: () {} - ) - ), + flexibleSpace: ListTile( + leading: const AlbumImage( + item: null, + ), + title: Text("Unknown song"), + subtitle: Text("Unknown artist"), + onTap: () {})), SliverPersistentHeader( - delegate: SectionHeaderDelegate("Queue") - ), + delegate: SectionHeaderDelegate(title: const Text("Queue"))), // Queue SliverList.list( children: const [], @@ -97,261 +83,51 @@ class _QueueListState extends State { ]; } - int _offsetOfKey(Key key) { - int oldOffset = 0; - - int oldListIndex = -1; - int oldItemIndex = -1; - - // lookup current index of the item belonging to the data - if (_nextUp != null && _nextUp!.isNotEmpty) { - oldItemIndex = _nextUp!.indexWhere((e) => ValueKey(e.id) == key); - oldListIndex = 1; - } - if (oldItemIndex == -1) { - oldItemIndex = _nextUp!.length + _queue!.indexWhere((e) => ValueKey(e.id) == key); - oldListIndex = 2; - } - if (oldItemIndex == -1) { - oldItemIndex = _previousTracks!.indexWhere((e) => ValueKey(e.id) == key); - oldListIndex = 0; - } - print("oldItemIndex: $oldItemIndex"); - print("oldListIndex: $oldListIndex"); - - // old index - if (oldListIndex == 0) { - // previous tracks - oldOffset = -((_previousTracks?.length ?? 0) - oldItemIndex); - } else if (oldListIndex == 1) { - // next up - oldOffset = oldItemIndex + 1; - } else if (oldListIndex == 2) { - // queue - oldOffset = oldItemIndex + _nextUp!.length + 1; - } - - return oldOffset; - - } - @override Widget build(BuildContext context) { - - _contents = [ - // const SliverPadding(padding: EdgeInsets.only(top: 0)), - // Previous Tracks - SliverPersistentHeader( - delegate: SectionHeaderDelegate("Previous Tracks"), - ), - // SliverList.builder( - // itemCount: _previousTracks?.length ?? 0, - // itemBuilder: (context, index) { - // final item = _previousTracks![index]; - // final actualIndex = index; - // final indexOffset = -((_previousTracks?.length ?? 0) - index); - // return ReorderableItem( - // // key: ValueKey("${_queue![actualIndex].item.id}$actualIndex"), - // key: ValueKey(_previousTracks![actualIndex].id), - // childBuilder: ( - // BuildContext context, - // ReorderableItemState state - // ) { - // return QueueListItem( - // item: item, - // actualIndex: actualIndex, - // indexOffset: indexOffset, - // subqueue: _previousTracks!, - // isCurrentTrack: _currentTrack == item, - // ); - // } - // ); - // }, - // ), - const PreviousTracksList(), - // // Current Track - // SliverPersistentHeader( - // delegate: SectionHeaderDelegate("Current Track"), - // ), - // SliverAppBar( - // key: currentTrackKey, - // pinned: true, - // collapsedHeight: 70.0, - // expandedHeight: 70.0, - // leading: const Padding( - // padding: EdgeInsets.zero, - // ), - // flexibleSpace: ListTile( - // leading: AlbumImage( - // item: _currentTrack!.item - // .extras?["itemJson"] == null - // ? null - // : jellyfin_models.BaseItemDto.fromJson(_currentTrack!.item.extras?["itemJson"]), - // ), - // title: Text( - // _currentTrack!.item.title ?? AppLocalizations.of(context)!.unknownName, - // style: _currentTrack == _currentTrack! - // ? TextStyle( - // color: - // Theme.of(context).colorScheme.secondary) - // : null), - // subtitle: Text(processArtist( - // _currentTrack!.item.artist, - // context)), - // onTap: () async => - // snapshot.data!.mediaState.playbackState.playing ? await _audioHandler.pause() : await _audioHandler.play(), - // ), - // ), - SliverPersistentHeader( - delegate: SectionHeaderDelegate( - _source?.name != null - ? "Playing from ${_source?.name}" - : "Queue", - true, - ), + _contents = [ + // const SliverPadding(padding: EdgeInsets.only(top: 0)), + // Previous Tracks + const PreviousTracksList(), + SliverPadding( + padding: const EdgeInsets.only(bottom: 12.0, top: 8.0), + sliver: SliverPersistentHeader( + delegate: SectionHeaderDelegate( + title: const Text("Recently Played"), height: 30.0), + ), + ), + CurrentTrack(), + NextUpTracksList(), + SliverPadding( + padding: const EdgeInsets.only(top: 20.0, bottom: 6.0), + sliver: SliverPersistentHeader( + delegate: SectionHeaderDelegate( + title: Row( + children: [ + const Text("Playing from "), + Text(_source?.name ?? "Unknown", + style: const TextStyle(fontWeight: FontWeight.w500)), + ], ), - // Queue - // SliverList.builder( - // itemCount: _queue?.length ?? 0, - // itemBuilder: (context, index) { - // final item = _queue![index]; - // final actualIndex = index; - // final indexOffset = index + 1; - // return ReorderableItem( - // key: ValueKey(_queue![actualIndex].id), - // childBuilder: ( - // BuildContext context, - // ReorderableItemState state - // ) { - // // if (candidateData.isNotEmpty && candidateData.first != null && candidateData.first != item) { - // // return Column( - // // children: [ - // // QueueListItemGhost( - // // item: candidateData.first!, - // // isCurrentTrack: _currentTrack == item, - // // ), - // // QueueListItem( - // // item: item, - // // actualIndex: actualIndex, - // // indexOffset: indexOffset, - // // subqueue: _queue!, - // // isCurrentTrack: _currentTrack == item, - // // ), - // // ], - // // ); - // // } else { - // return Container( - // child: SafeArea( - // top: false, - // bottom: false, - // child: Opacity( - // // hide content for placeholder - // opacity: state == ReorderableItemState.placeholder ? 0.0 : 1.0, - // child: IntrinsicHeight( - // child: QueueListItem( - // item: item, - // actualIndex: actualIndex, - // indexOffset: indexOffset, - // subqueue: _queue!, - // isCurrentTrack: _currentTrack == item, - // ), - // ), - // ), - // ), - // ); - // // } - // }, - // ); - // }, - // ), - const QueueTracksList(), - ]; - - // return CustomScrollView( - // controller: widget.scrollController, - // slivers: _contents, - // ); - return ReorderableList( - onReorder: (Key draggedItem, Key newPosition) { - int draggingOffset = _offsetOfKey(draggedItem); - int newPositionOffset = _offsetOfKey(newPosition); - offsetBeforeDrag = draggingOffset; - offsetAfterDrag = newPositionOffset; - print("$draggingOffset -> $newPositionOffset"); - if (mounted) { - setState(() { - //FIXME this is a slow operation, ideally we should just swap the items in the list - // problem with that is that we're using a streambuilder, so we can't just change the list - _queueService.reorderByOffset(offsetBeforeDrag, offsetAfterDrag); - }); - } else { - print("NOT MOUNTED"); - } + // _source != null ? "Playing from ${_source?.name}" : "Queue", + controls: true, + ), + ), + ), + // Queue + const QueueTracksList(), + ]; - // int indexBeforeDrag = 0; - // int indexAfterDrag = 0; - // List? listToUseBefore = _queue; - // List? listToUseAfter = _queue; - - // if (offsetBeforeDrag > 0) { - // if (_nextUp!.length > 0 && offsetBeforeDrag <= _nextUp!.length) { - // indexBeforeDrag = offsetBeforeDrag - 1; - // listToUseBefore = _nextUp; - // } else { - // indexBeforeDrag = offsetBeforeDrag - _nextUp!.length - 1; - // listToUseBefore = _queue; - // } - // } else if (offsetBeforeDrag < 0) { - // indexBeforeDrag = _previousTracks!.length + offsetBeforeDrag; - // listToUseBefore = _previousTracks; - // } else { - // // the current track can't be reordered - // return false; - // } - - // if (offsetAfterDrag > 0) { - // if (_nextUp!.length > 0 && offsetAfterDrag <= _nextUp!.length) { - // indexAfterDrag = offsetAfterDrag - 1; - // listToUseAfter = _nextUp; - // } else { - // indexAfterDrag = offsetAfterDrag - _nextUp!.length - 1; - // listToUseAfter = _queue; - // } - // } else if (offsetAfterDrag < 0) { - // indexAfterDrag = _previousTracks!.length + offsetAfterDrag; - // listToUseAfter = _previousTracks; - // } else { - // // the current track can't be reordered - // return false; - // } - - // print("indexBeforeDrag: $indexBeforeDrag"); - // print("indexAfterDrag: $indexAfterDrag"); - - // setState(() { - // final draggedQueueItem = listToUseBefore!.removeAt(indexBeforeDrag); - // listToUseAfter!.insert(indexAfterDrag, draggedQueueItem); - // }); - - return true; - }, - // onReorderDone: (Key item) { - // // int newPositionIndex = _indexOfKey(item); - // // debugPrint("Reordering finished for ${draggedItem.title}}"); - // _queueService.reorderByOffset(indexBeforeDrag, indexAfterDrag); - // }, - child: CustomScrollView( - controller: widget.scrollController, - slivers: _contents, - ), - ); + return CustomScrollView( + controller: widget.scrollController, + slivers: _contents, + ); } - } Future showQueueBottomSheet(BuildContext context) { return showModalBottomSheet( - showDragHandle: true, + // showDragHandle: true, useSafeArea: true, enableDrag: true, isScrollControlled: true, @@ -363,9 +139,37 @@ Future showQueueBottomSheet(BuildContext context) { return DraggableScrollableSheet( expand: false, builder: (context, scrollController) { - return QueueList( - scrollController: scrollController, + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 10), + Container( + width: 40, + height: 3.5, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.5), + ), + ), + const SizedBox(height: 10), + const Text("Queue", + style: TextStyle( + color: Colors.white, + fontFamily: 'Lexend Deca', + fontSize: 18, + fontWeight: FontWeight.w300)), + const SizedBox(height: 20), + Expanded( + child: QueueList( + scrollController: scrollController, + ), + ), + ], ); + // ) + // return QueueList( + // scrollController: scrollController, + // ); }, ); }, @@ -373,7 +177,6 @@ Future showQueueBottomSheet(BuildContext context) { } class PreviousTracksList extends StatefulWidget { - const PreviousTracksList({ Key? key, }) : super(key: key); @@ -383,7 +186,6 @@ class PreviousTracksList extends StatefulWidget { } class _PreviousTracksListState extends State { - final _queueService = GetIt.instance(); List? _previousTracks; @@ -397,46 +199,114 @@ class _PreviousTracksListState extends State { // (a, b) => _QueueListStreamState(a, b)), stream: _queueService.getPreviousTracksStream(), builder: (context, snapshot) { - if (snapshot.hasData) { - _previousTracks ??= snapshot.data!; - return SliverList.builder( - itemCount: _previousTracks?.length ?? 0, - itemBuilder: (context, index) { - final item = _previousTracks![index]; - final actualIndex = index; - final indexOffset = -((_previousTracks?.length ?? 0) - index); - return ReorderableItem( - // key: ValueKey("${_queue![actualIndex].item.id}$actualIndex"), - key: ValueKey(_previousTracks![actualIndex].id), - childBuilder: ( - BuildContext context, - ReorderableItemState state - ) { - return SafeArea( - top: false, - bottom: false, - child: Opacity( - // hide content for placeholder - opacity: state == ReorderableItemState.placeholder ? 0.0 : 1.0, - child: IntrinsicHeight( - child: QueueListItem( - item: item, - actualIndex: actualIndex, - indexOffset: indexOffset, - subqueue: _previousTracks!, - isCurrentTrack: false, - ), - ), - ), - ); - } - ); - }, - ); + return SliverReorderableList( + onReorder: (oldIndex, newIndex) { + int draggingOffset = -(_previousTracks!.length - oldIndex); + int newPositionOffset = -(_previousTracks!.length - newIndex); + print("$draggingOffset -> $newPositionOffset"); + if (mounted) { + setState(() { + // temporarily update internal queue + QueueItem tmp = _previousTracks!.removeAt(oldIndex); + _previousTracks!.insert( + newIndex < oldIndex ? newIndex : newIndex - 1, tmp); + // update external queue to commit changes, results in a rebuild + _queueService.reorderByOffset( + draggingOffset, newPositionOffset); + }); + } + }, + itemCount: _previousTracks?.length ?? 0, + itemBuilder: (context, index) { + final item = _previousTracks![index]; + final actualIndex = index; + final indexOffset = -((_previousTracks?.length ?? 0) - index); + return QueueListItem( + key: ValueKey(_previousTracks![actualIndex].id), + item: item, + listIndex: index, + actualIndex: actualIndex, + indexOffset: indexOffset, + subqueue: _previousTracks!, + onTap: () async { + await _queueService.skipByOffset(indexOffset); + }, + isCurrentTrack: false, + ); + }, + ); + } else { + return SliverList(delegate: SliverChildListDelegate([])); + } + }, + ); + } +} + +class NextUpTracksList extends StatefulWidget { + const NextUpTracksList({ + Key? key, + }) : super(key: key); + + @override + State createState() => _NextUpTracksListState(); +} +class _NextUpTracksListState extends State { + final _queueService = GetIt.instance(); + List? _nextUp; + + @override + Widget build(context) { + return StreamBuilder( + // stream: AudioService.queueStream, + stream: _queueService.getQueueStream(), + // stream: _queueService.getQueueStream(), + builder: (context, snapshot) { + if (snapshot.hasData) { + _nextUp ??= snapshot.data!.nextUp; + + return SliverPadding( + padding: const EdgeInsets.only(top: 20.0, left: 8.0, right: 8.0), + sliver: SliverReorderableList( + onReorder: (oldIndex, newIndex) { + int draggingOffset = oldIndex + 1; + int newPositionOffset = newIndex + 1; + print("$draggingOffset -> $newPositionOffset"); + if (mounted) { + setState(() { + // temporarily update internal queue + QueueItem tmp = _nextUp!.removeAt(oldIndex); + _nextUp!.insert( + newIndex < oldIndex ? newIndex : newIndex - 1, tmp); + // update external queue to commit changes, results in a rebuild + _queueService.reorderByOffset( + draggingOffset, newPositionOffset); + }); + } + }, + itemCount: _nextUp?.length ?? 0, + itemBuilder: (context, index) { + final item = _nextUp![index]; + final actualIndex = index; + final indexOffset = index + 1; + return QueueListItem( + key: ValueKey(_nextUp![actualIndex].id), + item: item, + listIndex: index, + actualIndex: actualIndex, + indexOffset: indexOffset, + subqueue: _nextUp!, + onTap: () async { + await _queueService.skipByOffset(indexOffset); + }, + isCurrentTrack: false, + ); + }, + )); } else { return SliverList(delegate: SliverChildListDelegate([])); } @@ -446,7 +316,6 @@ class _PreviousTracksListState extends State { } class QueueTracksList extends StatefulWidget { - const QueueTracksList({ Key? key, }) : super(key: key); @@ -456,56 +325,307 @@ class QueueTracksList extends StatefulWidget { } class _QueueTracksListState extends State { - final _queueService = GetIt.instance(); List? _queue; + List? _nextUp; @override Widget build(context) { - return StreamBuilder>( + return StreamBuilder( // stream: AudioService.queueStream, - stream: _queueService.getQueueTracksStream(), + stream: _queueService.getQueueStream(), // stream: _queueService.getQueueStream(), builder: (context, snapshot) { + if (snapshot.hasData) { + _queue ??= snapshot.data!.queue; + _nextUp ??= snapshot.data!.nextUp; + return SliverReorderableList( + onReorder: (oldIndex, newIndex) { + int draggingOffset = oldIndex + (_nextUp?.length ?? 0) + 1; + int newPositionOffset = newIndex + (_nextUp?.length ?? 0) + 1; + print("$draggingOffset -> $newPositionOffset"); + if (mounted) { + setState(() { + // temporarily update internal queue + QueueItem tmp = _queue!.removeAt(oldIndex); + _queue!.insert( + newIndex < oldIndex ? newIndex : newIndex - 1, tmp); + // update external queue to commit changes, results in a rebuild + _queueService.reorderByOffset( + draggingOffset, newPositionOffset); + }); + } + }, + itemCount: _queue?.length ?? 0, + itemBuilder: (context, index) { + final item = _queue![index]; + final actualIndex = index; + final indexOffset = index + 1; + return QueueListItem( + key: ValueKey(_queue![actualIndex].id), + item: item, + listIndex: index, + actualIndex: actualIndex, + indexOffset: indexOffset, + subqueue: _queue!, + onTap: () async { + await _queueService.skipByOffset(indexOffset); + }, + isCurrentTrack: false, + ); + }, + ); + } else { + return SliverList(delegate: SliverChildListDelegate([])); + } + }, + ); + } +} + +class CurrentTrack extends StatelessWidget { + const CurrentTrack({ + Key? key, + }) : super(key: key); + + @override + Widget build(context) { + final queueService = GetIt.instance(); + final audioHandler = GetIt.instance(); + + QueueItem? currentTrack; + MediaState? mediaState; + PlaybackState? playbackState; + + return StreamBuilder<_QueueListStreamState>( + stream: Rx.combineLatest3( + mediaStateStream, + audioHandler.playbackState, + queueService.getQueueStream(), + (a, b, c) => _QueueListStreamState(a, b, c)), + builder: (context, snapshot) { if (snapshot.hasData) { - _queue ??= snapshot.data!; - - return SliverList.builder( - itemCount: _queue?.length ?? 0, - itemBuilder: (context, index) { - final item = _queue![index]; - final actualIndex = index; - final indexOffset = -((_queue?.length ?? 0) - index); - return ReorderableItem( - // key: ValueKey("${_queue![actualIndex].item.id}$actualIndex"), - key: ValueKey(_queue![actualIndex].id), - childBuilder: ( - BuildContext context, - ReorderableItemState state - ) { - return SafeArea( - top: false, - bottom: false, - child: Opacity( - // hide content for placeholder - opacity: state == ReorderableItemState.placeholder ? 0.0 : 1.0, - child: IntrinsicHeight( - child: QueueListItem( - item: item, - actualIndex: actualIndex, - indexOffset: indexOffset, - subqueue: _queue!, - isCurrentTrack: false, + currentTrack ??= snapshot.data!.queueInfo.currentTrack; + mediaState ??= snapshot.data!.mediaState; + playbackState ??= snapshot.data!.playbackState; + + return SliverAppBar( + // key: currentTrackKey, + pinned: true, + collapsedHeight: 70.0, + expandedHeight: 70.0, + elevation: 10.0, + leading: const Padding( + padding: EdgeInsets.zero, + ), + flexibleSpace: Container( + // width: 328, + height: 70.0, + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Container( + decoration: const ShapeDecoration( + color: Color.fromRGBO(188, 136, 86, 0.20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Stack( + alignment: Alignment.center, + children: [ + AlbumImage( + item: currentTrack!.item.extras?["itemJson"] == null + ? null + : jellyfin_models.BaseItemDto.fromJson( + currentTrack!.item.extras?["itemJson"]), + ), + Container( + width: 70, + height: 70, + decoration: const ShapeDecoration( + shape: Border(), + color: Color.fromRGBO(0, 0, 0, 0.25), + ), + child: IconButton( + onPressed: () { + audioHandler.togglePlayback(); + }, + icon: playbackState!.playing + ? const Icon( + TablerIcons.player_pause, + size: 32, + ) + : const Icon( + TablerIcons.player_play, + size: 32, + ), + )), + ], + ), + Expanded( + child: Stack( + children: [ + Positioned( + left: 0, + top: 0, + child: Container( + width: 320 * + (playbackState!.position.inSeconds / + (mediaState?.mediaItem?.duration ?? + const Duration(seconds: 0)) + .inSeconds), + height: 70.0, + decoration: const ShapeDecoration( + color: Color.fromRGBO(188, 136, 86, 0.75), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + ), ), ), ), - ); - } - ); - }, - ); - + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: 70, + width: 150, + padding: + const EdgeInsets.only(left: 12, right: 4), + // child: Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + currentTrack?.item.title ?? 'Unknown', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w500, + overflow: TextOverflow.ellipsis), + ), + const SizedBox(height: 4), + Text( + processArtist( + currentTrack!.item.artist, context), + style: TextStyle( + color: Colors.white.withOpacity(0.85), + fontSize: 13, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w300, + overflow: TextOverflow.ellipsis), + ), + ], + ), + // ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + // '0:00', + playbackState!.position.inHours > 1.0 + ? audioHandler.playbackPosition + .toString() + .split('.')[0] + : audioHandler.playbackPosition + .toString() + .substring(2, 7), + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 12, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w300, + ), + ), + const SizedBox(width: 2), + Text( + '/', + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 12, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w300, + ), + ), + const SizedBox(width: 2), + Text( + // '3:44', + (mediaState?.mediaItem?.duration + ?.inHours ?? + const Duration(seconds: 0) + .inHours) > + 1.0 + ? (mediaState + ?.mediaItem?.duration ?? + const Duration(seconds: 0)) + .toString() + .split('.')[0] + : (mediaState + ?.mediaItem?.duration ?? + const Duration(seconds: 0)) + .toString() + .substring(2, 7), + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 12, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w300, + ), + ), + ], + ), + IconButton( + icon: const Icon( + TablerIcons.heart, + size: 32, + color: Colors.white, + weight: 1.5, + ), + onPressed: () {}, + ), + IconButton( + icon: const Icon( + TablerIcons.dots_vertical, + size: 32, + color: Colors.white, + weight: 1.5, + ), + onPressed: () {}, + ), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); } else { return SliverList(delegate: SliverChildListDelegate([])); } @@ -515,34 +635,42 @@ class _QueueTracksListState extends State { } class SectionHeaderDelegate extends SliverPersistentHeaderDelegate { - final String title; + final Widget title; final bool controls; final double height; - SectionHeaderDelegate(this.title, [this.controls = false, this.height = 50]); + SectionHeaderDelegate({ + required this.title, + this.controls = false, + this.height = 30.0, + }); @override Widget build(context, double shrinkOffset, bool overlapsContent) { - return Flex( - direction: Axis.horizontal, - children: [ - Flexible( - child: Text(title), - ), - if (controls) - Row( - children: [ - IconButton( - icon: const Icon(TablerIcons.arrows_shuffle), - onPressed: () {}, - ), - IconButton( - icon: const Icon(TablerIcons.repeat), - onPressed: () {}, - ), - ], - ) - ], + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 14.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Flex( + direction: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + title, + ])), + if (controls) + IconButton( + icon: const Icon(TablerIcons.arrows_shuffle), + onPressed: () {}, + ), + if (controls) + IconButton( + icon: const Icon(TablerIcons.repeat), + onPressed: () {}, + ), + ], + ), ); } diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index dac2d6831..2013a7488 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -8,22 +8,24 @@ import 'package:finamp/models/jellyfin_models.dart' as jellyfin_models; import 'package:finamp/services/queue_service.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get_it/get_it.dart'; -import 'package:flutter_reorderable_list/flutter_reorderable_list.dart'; class QueueListItem extends StatefulWidget { - late QueueItem item; + late int listIndex; late int actualIndex; late int indexOffset; late List subqueue; late bool isCurrentTrack; - + late void Function() onTap; + QueueListItem({ Key? key, required this.item, + required this.listIndex, required this.actualIndex, required this.indexOffset, required this.subqueue, + required this.onTap, this.isCurrentTrack = false, }) : super(key: key); @override @@ -31,153 +33,97 @@ class QueueListItem extends StatefulWidget { } class _QueueListItemState extends State { - final _audioHandler = GetIt.instance(); final _queueService = GetIt.instance(); - - @override - Widget build(BuildContext context) { - return ListTile( - visualDensity: VisualDensity.compact, - minVerticalPadding: 0.0, - contentPadding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0), - tileColor: widget.isCurrentTrack - ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) - : null, - leading: AlbumImage( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(7.0), - bottomLeft: Radius.circular(7.0), - ), - item: widget.item.item - .extras?["itemJson"] == null - ? null - : jellyfin_models.BaseItemDto.fromJson(widget.item.item.extras?["itemJson"]), - ), - title: Text( - widget.item.item.title ?? AppLocalizations.of(context)!.unknownName, - style: this.widget.isCurrentTrack - ? TextStyle( - color: - Theme.of(context).colorScheme.secondary) - : null), - subtitle: Text(processArtist( - widget.item.item.artist, - context)), - trailing: Container( - alignment: Alignment.centerRight, - margin: const EdgeInsets.only(right: 32.0), - width: 115.0, - height: 50.0, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "${widget.item.item.duration?.inMinutes.toString().padLeft(2, '0')}:${((widget.item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - textAlign: TextAlign.end, - style: TextStyle( - color: Theme.of(context).textTheme.bodySmall?.color, - ), - ), - // IconButton( - // icon: const Icon(TablerIcons.dots_vertical), - // iconSize: 28.0, - // onPressed: () async => {}, - // ), - IconButton( - icon: const Icon(TablerIcons.x), - iconSize: 28.0, - onPressed: () async => await _queueService.removeAtOffset(widget.indexOffset), - ), - // Draggable( - // data: widget.item, - // axis: Axis.vertical, - // dragAnchorStrategy: (draggable, context, position) => Offset(MediaQuery.of(context).size.width - 62.0, 0.0), - // // feedback: QueueListItemGhost( - // // item: widget.item, - // // isCurrentTrack: widget.isCurrentTrack, - // // ), - // feedback: Container(), - // childWhenDragging: Container(), - // // key: ValueKey("${_queue![actualIndex].item.id}$actualIndex-drag"), - // child: Icon( - // TablerIcons.grip_horizontal, - // color: IconTheme.of(context).color, - // size: 28.0, - // ), - // ), - ReorderableListener( - child: Icon( - TablerIcons.grip_horizontal, - color: IconTheme.of(context).color, - size: 28.0, - ), - ), - ], - ), - ), - onTap: () async => - await _queueService.skipByOffset(widget.indexOffset), - ); - } -} - -class QueueListItemGhost extends StatelessWidget { - final QueueItem item; - final bool isCurrentTrack; - - const QueueListItemGhost({ - Key? key, - required this.item, - this.isCurrentTrack = false, - }) : super(key: key); @override Widget build(BuildContext context) { - return SizedBox( - width: 400.0, - // height: 90.0, - child: Card( - elevation: 8.0, - margin: EdgeInsets.zero, + return Card( + color: const Color.fromRGBO(255, 255, 255, 0.05), + elevation: 0, + margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 5.0), + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), child: ListTile( visualDensity: VisualDensity.compact, minVerticalPadding: 0.0, - contentPadding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0), - tileColor: this.isCurrentTrack + contentPadding: + const EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0), + tileColor: widget.isCurrentTrack ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) : null, leading: AlbumImage( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(7.0), - bottomLeft: Radius.circular(7.0), - ), - item: this.item.item - .extras?["itemJson"] == null - ? null - : jellyfin_models.BaseItemDto.fromJson(this.item.item.extras?["itemJson"]), + item: widget.item.item.extras?["itemJson"] == null + ? null + : jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]), ), - title: Text( - this.item.item.title ?? AppLocalizations.of(context)!.unknownName, - style: this.isCurrentTrack + title: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text( + widget.item.item.title ?? + AppLocalizations.of(context)!.unknownName, + style: this.widget.isCurrentTrack ? TextStyle( - color: - Theme.of(context).colorScheme.secondary) - : null), - subtitle: Text(processArtist( - this.item.item.artist, - context)), + color: Theme.of(context).colorScheme.secondary, + fontSize: 16, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w400, + overflow: TextOverflow.ellipsis) + : null, + overflow: TextOverflow.ellipsis, + ), + ), + subtitle: Text( + processArtist(widget.item.item.artist, context), + style: const TextStyle( + color: Colors.white70, + fontSize: 13, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w300, + overflow: TextOverflow.ellipsis), + overflow: TextOverflow.ellipsis, + ), trailing: Container( alignment: Alignment.centerRight, - margin: const EdgeInsets.only(right: 32.0), + margin: const EdgeInsets.only(right: 8.0), width: 115.0, height: 50.0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${widget.item.item.duration?.inMinutes.toString().padLeft(2, '0')}:${((widget.item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + textAlign: TextAlign.end, + style: TextStyle( + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + IconButton( + icon: const Icon( + TablerIcons.x, + color: Colors.white, + weight: 1.5, + ), + iconSize: 28.0, + onPressed: () async => + await _queueService.removeAtOffset(widget.indexOffset), + ), + ReorderableDragStartListener( + index: widget.listIndex, + child: const Icon( + TablerIcons.grip_horizontal, + color: Colors.white, + size: 28.0, + weight: 1.5, + ), + ), + ], + ), ), - onTap: () async => {}, - ), - ), - ); + onTap: widget.onTap, + )); } - } - diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index 6486c6070..0ea294eec 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -190,6 +190,14 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { @override Future pause() => _player.pause(); + Future togglePlayback() { + if (_player.playing) { + return pause(); + } else { + return play(); + } + } + @override Future stop() async { try { diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 712a8b2c5..0f989e00b 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -407,7 +407,7 @@ class QueueService { //!!! the player will automatically change the shuffle indices of the ConcatenatingAudioSource if shuffle is enabled, so we need to use the regular track index here final oldIndex = _queueAudioSourceIndex + oldOffset; - final newIndex = _queueAudioSourceIndex + newOffset; + final newIndex = oldOffset < newOffset ? _queueAudioSourceIndex + newOffset - 1 : _queueAudioSourceIndex + newOffset; await _audioHandler.reorderQueue(oldIndex, newIndex); _queueFromConcatenatingAudioSource(); From 0befb2c7cdac08a9ae8a4237ab7e045a60bf75f0 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Mon, 31 Jul 2023 23:24:43 +0200 Subject: [PATCH 045/130] fix widgets in queue list not updating --- lib/components/PlayerScreen/queue_list.dart | 42 +++++++++++++-------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 845a36b4a..692fb62ab 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -20,12 +20,12 @@ import 'queue_list_item.dart'; class _QueueListStreamState { _QueueListStreamState( this.mediaState, - this.playbackState, + this.playbackPosition, this.queueInfo, ); final MediaState mediaState; - final PlaybackState playbackState; + final Duration playbackPosition; final QueueInfo queueInfo; } @@ -61,6 +61,7 @@ class _QueueListState extends State { ), // Current Track SliverAppBar( + key: UniqueKey(), pinned: true, collapsedHeight: 70.0, expandedHeight: 70.0, @@ -96,7 +97,9 @@ class _QueueListState extends State { title: const Text("Recently Played"), height: 30.0), ), ), - CurrentTrack(), + CurrentTrack( + key: UniqueKey(), + ), NextUpTracksList(), SliverPadding( padding: const EdgeInsets.only(top: 20.0, bottom: 6.0), @@ -396,20 +399,21 @@ class CurrentTrack extends StatelessWidget { QueueItem? currentTrack; MediaState? mediaState; - PlaybackState? playbackState; + Duration? playbackPosition; return StreamBuilder<_QueueListStreamState>( - stream: Rx.combineLatest3( mediaStateStream, - audioHandler.playbackState, + AudioService.position + .startWith(audioHandler.playbackState.value.position), queueService.getQueueStream(), (a, b, c) => _QueueListStreamState(a, b, c)), builder: (context, snapshot) { if (snapshot.hasData) { - currentTrack ??= snapshot.data!.queueInfo.currentTrack; - mediaState ??= snapshot.data!.mediaState; - playbackState ??= snapshot.data!.playbackState; + currentTrack = snapshot.data!.queueInfo.currentTrack; + mediaState = snapshot.data!.mediaState; + playbackPosition = snapshot.data!.playbackPosition; return SliverAppBar( // key: currentTrackKey, @@ -447,6 +451,10 @@ class CurrentTrack extends StatelessWidget { ? null : jellyfin_models.BaseItemDto.fromJson( currentTrack!.item.extras?["itemJson"]), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + bottomLeft: Radius.circular(8), + ), ), Container( width: 70, @@ -459,7 +467,7 @@ class CurrentTrack extends StatelessWidget { onPressed: () { audioHandler.togglePlayback(); }, - icon: playbackState!.playing + icon: mediaState!.playbackState.playing ? const Icon( TablerIcons.player_pause, size: 32, @@ -477,12 +485,13 @@ class CurrentTrack extends StatelessWidget { Positioned( left: 0, top: 0, + // child: RepaintBoundary( child: Container( width: 320 * - (playbackState!.position.inSeconds / + (playbackPosition!.inMilliseconds / (mediaState?.mediaItem?.duration ?? const Duration(seconds: 0)) - .inSeconds), + .inMilliseconds), height: 70.0, decoration: const ShapeDecoration( color: Color.fromRGBO(188, 136, 86, 0.75), @@ -494,6 +503,7 @@ class CurrentTrack extends StatelessWidget { ), ), ), + // ), ), Row( mainAxisSize: MainAxisSize.max, @@ -501,7 +511,7 @@ class CurrentTrack extends StatelessWidget { children: [ Container( height: 70, - width: 150, + width: 130, padding: const EdgeInsets.only(left: 12, right: 4), // child: Expanded( @@ -545,11 +555,11 @@ class CurrentTrack extends StatelessWidget { children: [ Text( // '0:00', - playbackState!.position.inHours > 1.0 - ? audioHandler.playbackPosition + playbackPosition!.inHours > 1.0 + ? playbackPosition .toString() .split('.')[0] - : audioHandler.playbackPosition + : playbackPosition .toString() .substring(2, 7), style: TextStyle( From 7903cff63ab9912d06bcca37843dd301dc2f8cb3 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Mon, 31 Jul 2023 23:30:56 +0200 Subject: [PATCH 046/130] remove unneeded dependency --- pubspec.lock | 8 -------- pubspec.yaml | 1 - 2 files changed, 9 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index c490f8870..9e288f7b2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -412,14 +412,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.9" - flutter_reorderable_list: - dependency: "direct main" - description: - name: flutter_reorderable_list - sha256: "0400ef34fa00b7cac69f71efc92d7e49727f425bc1080180ebe70bf47618afe0" - url: "https://pub.dev" - source: hosted - version: "1.3.1" flutter_riverpod: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index ea2815ad0..6f60f8cb1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -78,7 +78,6 @@ dependencies: git: url: https://github.com/lamarios/locale_names.git ref: cea057c220f4ee7e09e8f1fc7036110245770948 - flutter_reorderable_list: ^1.3.1 dev_dependencies: flutter_test: From f3350ca43efd271530cd6bb5ff78acf73c407d22 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 10 Aug 2023 20:26:42 +0200 Subject: [PATCH 047/130] disable queue drag handles when shuffle is active --- lib/components/PlayerScreen/queue_list.dart | 2 ++ .../PlayerScreen/queue_list_item.dart | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 692fb62ab..3d291dd4d 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -234,6 +234,7 @@ class _PreviousTracksListState extends State { actualIndex: actualIndex, indexOffset: indexOffset, subqueue: _previousTracks!, + allowReorder: _queueService.playbackOrder == PlaybackOrder.linear, onTap: () async { await _queueService.skipByOffset(indexOffset); }, @@ -372,6 +373,7 @@ class _QueueTracksListState extends State { actualIndex: actualIndex, indexOffset: indexOffset, subqueue: _queue!, + allowReorder: _queueService.playbackOrder == PlaybackOrder.linear, onTap: () async { await _queueService.skipByOffset(indexOffset); }, diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index 2013a7488..1e8a2f5b2 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -16,6 +16,7 @@ class QueueListItem extends StatefulWidget { late int indexOffset; late List subqueue; late bool isCurrentTrack; + late bool allowReorder; late void Function() onTap; QueueListItem({ @@ -26,6 +27,7 @@ class QueueListItem extends StatefulWidget { required this.indexOffset, required this.subqueue, required this.onTap, + this.allowReorder = true, this.isCurrentTrack = false, }) : super(key: key); @override @@ -111,15 +113,16 @@ class _QueueListItemState extends State { onPressed: () async => await _queueService.removeAtOffset(widget.indexOffset), ), - ReorderableDragStartListener( - index: widget.listIndex, - child: const Icon( - TablerIcons.grip_horizontal, - color: Colors.white, - size: 28.0, - weight: 1.5, + if (widget.allowReorder) + ReorderableDragStartListener( + index: widget.listIndex, + child: const Icon( + TablerIcons.grip_horizontal, + color: Colors.white, + size: 28.0, + weight: 1.5, + ), ), - ), ], ), ), From 53d862fc0f6908e9bcbbb855d4abdf6894b9069a Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 10 Aug 2023 21:56:21 +0200 Subject: [PATCH 048/130] improve scroll behavior --- lib/components/PlayerScreen/queue_list.dart | 103 ++++++++++++++------ 1 file changed, 74 insertions(+), 29 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 3d291dd4d..9a009b512 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -36,6 +36,15 @@ class QueueList extends StatefulWidget { @override State createState() => _QueueListState(); + + void scrollDown() { + scrollController.animateTo( + scrollController.position.maxScrollExtent, + duration: Duration(seconds: 2), + curve: Curves.fastOutSlowIn, + ); + } + } class _QueueListState extends State { @@ -45,6 +54,8 @@ class _QueueListState extends State { late List _contents; + GlobalKey queueListKey = GlobalKey(); + @override void initState() { super.initState(); @@ -61,7 +72,6 @@ class _QueueListState extends State { ), // Current Track SliverAppBar( - key: UniqueKey(), pinned: true, collapsedHeight: 70.0, expandedHeight: 70.0, @@ -79,9 +89,33 @@ class _QueueListState extends State { delegate: SectionHeaderDelegate(title: const Text("Queue"))), // Queue SliverList.list( + key: queueListKey, children: const [], ), ]; + + // call function after 2 seconds + Future.delayed(const Duration(milliseconds: 50), () { + // setState(() {}); + // widget.scrollDown(); + scrollToCurrentTrack(); + }); + + } + + void scrollToCurrentTrack() { + + // dynamic box = currentTrackKey.currentContext!.findRenderObject(); + // Offset position = box; //this is global position + // double y = position.dy; + + // widget.scrollController.animateTo( + // y, + // // scrollController.position.maxScrollExtent, + // duration: Duration(seconds: 2), + // curve: Curves.fastOutSlowIn, + // ); + Scrollable.ensureVisible(queueListKey.currentContext!); } @override @@ -97,11 +131,10 @@ class _QueueListState extends State { title: const Text("Recently Played"), height: 30.0), ), ), - CurrentTrack( - key: UniqueKey(), - ), + CurrentTrack(), NextUpTracksList(), SliverPadding( + key: queueListKey, padding: const EdgeInsets.only(top: 20.0, bottom: 6.0), sliver: SliverPersistentHeader( delegate: SectionHeaderDelegate( @@ -129,6 +162,7 @@ class _QueueListState extends State { } Future showQueueBottomSheet(BuildContext context) { + return showModalBottomSheet( // showDragHandle: true, useSafeArea: true, @@ -140,34 +174,45 @@ Future showQueueBottomSheet(BuildContext context) { context: context, builder: (context) { return DraggableScrollableSheet( + snap: false, + snapAnimationDuration: const Duration(milliseconds: 200), + initialChildSize: 0.9, + maxChildSize: 0.9, + minChildSize: 0.9, expand: false, builder: (context, scrollController) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 10), - Container( - width: 40, - height: 3.5, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(3.5), + return Scaffold( + body: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 10), + Container( + width: 40, + height: 3.5, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.5), + ), ), - ), - const SizedBox(height: 10), - const Text("Queue", - style: TextStyle( - color: Colors.white, - fontFamily: 'Lexend Deca', - fontSize: 18, - fontWeight: FontWeight.w300)), - const SizedBox(height: 20), - Expanded( - child: QueueList( - scrollController: scrollController, + const SizedBox(height: 10), + const Text("Queue", + style: TextStyle( + color: Colors.white, + fontFamily: 'Lexend Deca', + fontSize: 18, + fontWeight: FontWeight.w300)), + const SizedBox(height: 20), + Expanded( + child: QueueList( + scrollController: scrollController, + ), ), - ), - ], + ], + ), + // floatingActionButton: FloatingActionButton.small( + // onPressed: _scrollDown, + // child: Icon(Icons.arrow_downward), + // ), ); // ) // return QueueList( @@ -188,7 +233,7 @@ class PreviousTracksList extends StatefulWidget { State createState() => _PreviousTracksListState(); } -class _PreviousTracksListState extends State { +class _PreviousTracksListState extends State with TickerProviderStateMixin { final _queueService = GetIt.instance(); List? _previousTracks; From 84946ee71fb57fa4783d9fee4a396af74845f766 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 10 Aug 2023 21:59:27 +0200 Subject: [PATCH 049/130] allow dismissing bottom sheet by scrolling up --- lib/components/PlayerScreen/queue_list.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 9a009b512..2170a7577 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -178,7 +178,6 @@ Future showQueueBottomSheet(BuildContext context) { snapAnimationDuration: const Duration(milliseconds: 200), initialChildSize: 0.9, maxChildSize: 0.9, - minChildSize: 0.9, expand: false, builder: (context, scrollController) { return Scaffold( From 867d57d6b56c89b3b1825ea71a0e076bd6854d6e Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 10 Aug 2023 21:59:41 +0200 Subject: [PATCH 050/130] properly update previous tracks on track change --- lib/components/PlayerScreen/queue_list.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 2170a7577..fbf8e33e8 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -238,16 +238,16 @@ class _PreviousTracksListState extends State with TickerProv @override Widget build(context) { - return StreamBuilder>( + return StreamBuilder( // stream: AudioService.queueStream, // stream: Rx.combineLatest2( // mediaStateStream, // _queueService.getQueueStream(), // (a, b) => _QueueListStreamState(a, b)), - stream: _queueService.getPreviousTracksStream(), + stream: _queueService.getQueueStream(), builder: (context, snapshot) { if (snapshot.hasData) { - _previousTracks ??= snapshot.data!; + _previousTracks ??= snapshot.data!.previousTracks; return SliverReorderableList( onReorder: (oldIndex, newIndex) { From a5ba1d012d3e71032f2c2caf51f03ee6f6aed4bc Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 10 Aug 2023 23:04:00 +0200 Subject: [PATCH 051/130] some fixes --- lib/components/PlayerScreen/queue_list.dart | 151 ++++++++++++++------ lib/services/queue_service.dart | 111 ++++---------- 2 files changed, 137 insertions(+), 125 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index fbf8e33e8..92c707033 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -54,7 +54,7 @@ class _QueueListState extends State { late List _contents; - GlobalKey queueListKey = GlobalKey(); + GlobalKey nextUpHeaderKey = GlobalKey(); @override void initState() { @@ -72,24 +72,26 @@ class _QueueListState extends State { ), // Current Track SliverAppBar( - pinned: true, - collapsedHeight: 70.0, - expandedHeight: 70.0, - leading: const Padding( - padding: EdgeInsets.zero, - ), - flexibleSpace: ListTile( - leading: const AlbumImage( - item: null, - ), - title: Text("Unknown song"), - subtitle: Text("Unknown artist"), - onTap: () {})), + key: UniqueKey(), + pinned: true, + collapsedHeight: 70.0, + expandedHeight: 70.0, + leading: const Padding( + padding: EdgeInsets.zero, + ), + flexibleSpace: ListTile( + leading: const AlbumImage( + item: null, + ), + title: Text("Unknown song"), + subtitle: Text("Unknown artist"), + onTap: () {}) + ), SliverPersistentHeader( delegate: SectionHeaderDelegate(title: const Text("Queue"))), // Queue SliverList.list( - key: queueListKey, + key: nextUpHeaderKey, children: const [], ), ]; @@ -115,7 +117,11 @@ class _QueueListState extends State { // duration: Duration(seconds: 2), // curve: Curves.fastOutSlowIn, // ); - Scrollable.ensureVisible(queueListKey.currentContext!); + Scrollable.ensureVisible( + nextUpHeaderKey.currentContext!, + // duration: const Duration(milliseconds: 200), + // curve: Curves.decelerate, + ); } @override @@ -131,10 +137,21 @@ class _QueueListState extends State { title: const Text("Recently Played"), height: 30.0), ), ), - CurrentTrack(), + CurrentTrack( + key: UniqueKey(), + ), + // next up + //TODO don't show this if next up is empty + SliverPadding( + key: nextUpHeaderKey, + padding: const EdgeInsets.only(top: 20.0, bottom: 0.0), + sliver: SliverPersistentHeader( + delegate: SectionHeaderDelegate( + title: const Text("Next Up"), height: 30.0),// _source != null ? "Playing from ${_source?.name}" : "Queue", + ), + ), NextUpTracksList(), SliverPadding( - key: queueListKey, padding: const EdgeInsets.only(top: 20.0, bottom: 6.0), sliver: SliverPersistentHeader( delegate: SectionHeaderDelegate( @@ -318,7 +335,7 @@ class _NextUpTracksListState extends State { _nextUp ??= snapshot.data!.nextUp; return SliverPadding( - padding: const EdgeInsets.only(top: 20.0, left: 8.0, right: 8.0), + padding: const EdgeInsets.only(top: 0.0, left: 8.0, right: 8.0), sliver: SliverReorderableList( onReorder: (oldIndex, newIndex) { int draggingOffset = oldIndex + 1; @@ -470,7 +487,9 @@ class CurrentTrack extends StatelessWidget { leading: const Padding( padding: EdgeInsets.zero, ), + backgroundColor: const Color.fromRGBO(0, 0, 0, 0.0), flexibleSpace: Container( + color: const Color.fromRGBO(0, 0, 0, 1.0), // width: 328, height: 70.0, padding: const EdgeInsets.symmetric(horizontal: 12), @@ -690,6 +709,15 @@ class CurrentTrack extends StatelessWidget { } } +class PlaybackBehaviorInfo { + + final PlaybackOrder order; + final LoopMode loop; + + PlaybackBehaviorInfo(this.order, this.loop); + +} + class SectionHeaderDelegate extends SliverPersistentHeaderDelegate { final Widget title; final bool controls; @@ -703,30 +731,67 @@ class SectionHeaderDelegate extends SliverPersistentHeaderDelegate { @override Widget build(context, double shrinkOffset, bool overlapsContent) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 14.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Flex( - direction: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - title, - ])), - if (controls) - IconButton( - icon: const Icon(TablerIcons.arrows_shuffle), - onPressed: () {}, - ), - if (controls) - IconButton( - icon: const Icon(TablerIcons.repeat), - onPressed: () {}, - ), - ], - ), + + final _queueService = GetIt.instance(); + + return StreamBuilder( + stream: Rx.combineLatest2(_queueService.getPlaybackOrderStream(), _queueService.getLoopModeStream(), (a, b) => PlaybackBehaviorInfo(a, b)), + builder:(context, snapshot) { + + PlaybackBehaviorInfo? info = snapshot.data as PlaybackBehaviorInfo?; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 14.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Flex( + direction: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + title, + ])), + if (controls) + IconButton( + icon: info?.order == PlaybackOrder.shuffled ? ( + const Icon( + TablerIcons.arrows_shuffle, + ) + ) : ( + const Icon( + TablerIcons.arrows_right, + ) + ), + color: info?.order == PlaybackOrder.shuffled ? Colors.orange : Colors.white, + onPressed: () => { + _queueService.togglePlaybackOrder(), //TODO scroll back to current track after toggling + } + ), + if (controls) + IconButton( + icon: info?.loop != LoopMode.none ? ( + info?.loop == LoopMode.one ? ( + const Icon( + TablerIcons.repeat_once, + ) + ) : ( + const Icon( + TablerIcons.repeat, + ) + ) + ) : ( + const Icon( + TablerIcons.repeat_off, + ) + ), + color: info?.loop != LoopMode.none ? Colors.orange : Colors.white, + onPressed: () => _queueService.toggleLoopMode(), + ), + ], + ), + ); + }, ); } diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 0f989e00b..f4d1386e1 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -49,15 +49,9 @@ class QueueService { nextUp: [], source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown), )); - final _previousTracksStream = BehaviorSubject>.seeded( - List.empty(growable: true), - ); - final _queueTracksStream = BehaviorSubject>.seeded( - List.empty(growable: true), - ); - final _nextUpTracksStream = BehaviorSubject>.seeded( - List.empty(growable: true), - ); + + final _playbackOrderStream = BehaviorSubject.seeded(PlaybackOrder.linear); + final _loopModeStream = BehaviorSubject.seeded(LoopMode.none); // external queue state @@ -104,10 +98,6 @@ class QueueService { List allTracks = _audioHandler.effectiveSequence?.map((e) => e.tag as QueueItem).toList() ?? []; int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; - List _oldQueue = List.from(_queue); - List _oldQueuePreviousTracks = List.from(_queuePreviousTracks); - List _oldQueueNextUp = List.from(_queueNextUp); - _queuePreviousTracks.clear(); _queueNextUp.clear(); _queue.clear(); @@ -145,67 +135,6 @@ class QueueService { } - - // final oldQueueInfo = _queueStream.value; - if (_queue.isEmpty) { - _queueServiceLogger.finer("New queue info is empty"); - } - if (_oldQueue.isEmpty) { - _queueServiceLogger.finer("Old queue info is empty"); - } - bool isQueueChanged = false; - for (int i = 0; i < _queuePreviousTracks.length; i++) { - if (_oldQueuePreviousTracks.length > i) { - if (_queuePreviousTracks[i].id != _oldQueuePreviousTracks[i].id) { - isQueueChanged = true; - break; - } - } else { - isQueueChanged = true; - break; - } - } - if (isQueueChanged) { - _queueServiceLogger.finer("Previous tracks changed"); - _previousTracksStream.add(_queuePreviousTracks); - } - - isQueueChanged = false; - for (int i = 0; i < _queueNextUp.length; i++) { - if (_oldQueueNextUp.length > i) { - if (_queueNextUp[i].id != _oldQueueNextUp[i].id) { - isQueueChanged = true; - break; - } - } else { - isQueueChanged = true; - break; - } - } - if (isQueueChanged) { - _queueServiceLogger.finer("Next Up changed"); - _nextUpTracksStream.add(_queueNextUp); - } - - isQueueChanged = false; - for (int i = 0; i < _queue.length; i++) { - // _queueServiceLogger.finer("i: $i"); - // _queueServiceLogger.finer("condition: ${oldQueueInfo.queue.length > i}"); - if (_oldQueue.length > i) { - if (_queue[i].id != _oldQueue[i].id) { - isQueueChanged = true; - break; - } - } else { - isQueueChanged = true; - break; - } - } - if (isQueueChanged) { - _queueServiceLogger.finer("Queue changed"); - _queueTracksStream.add(_queue); - } - final newQueueInfo = getQueue(); _queueStream.add(newQueueInfo); if (_currentTrack != null) { @@ -433,16 +362,12 @@ class QueueService { return _queueStream; } - BehaviorSubject> getPreviousTracksStream() { - return _previousTracksStream; - } - - BehaviorSubject> getQueueTracksStream() { - return _queueTracksStream; + BehaviorSubject getPlaybackOrderStream() { + return _playbackOrderStream; } - BehaviorSubject> getNextUpTracksStream() { - return _nextUpTracksStream; + BehaviorSubject getLoopModeStream() { + return _loopModeStream; } BehaviorSubject getCurrentTrackStream() { @@ -457,6 +382,8 @@ class QueueService { _loopMode = mode; // _currentTrackStream.add(_currentTrack ?? QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown))); + _loopModeStream.add(mode); + if (mode == LoopMode.one) { _audioHandler.setRepeatMode(AudioServiceRepeatMode.one); } else if (mode == LoopMode.all) { @@ -474,6 +401,8 @@ class QueueService { _queueServiceLogger.fine("Playback order set to $order"); // _currentTrackStream.add(_currentTrack ?? QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown))); + _playbackOrderStream.add(order); + // update queue accordingly and generate new shuffled order if necessary if (_playbackOrder == PlaybackOrder.shuffled) { _audioHandler.setShuffleMode(AudioServiceShuffleMode.all).then((value) => _queueFromConcatenatingAudioSource()); @@ -485,6 +414,24 @@ class QueueService { PlaybackOrder get playbackOrder => _playbackOrder; + void togglePlaybackOrder() { + if (_playbackOrder == PlaybackOrder.shuffled) { + playbackOrder = PlaybackOrder.linear; + } else { + playbackOrder = PlaybackOrder.shuffled; + } + } + + void toggleLoopMode() { + if (_loopMode == LoopMode.all) { + loopMode = LoopMode.one; + } else if (_loopMode == LoopMode.one) { + loopMode = LoopMode.none; + } else { + loopMode = LoopMode.all; + } + } + Logger get queueServiceLogger => _queueServiceLogger; void _logQueues({ String message = "" }) { From 001cb95ec9aca83ec736fbe227458d8b5a50ab67 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 11 Aug 2023 22:59:57 +0200 Subject: [PATCH 052/130] design, scroll and reactivity improvements --- lib/components/PlayerScreen/queue_list.dart | 282 +++++++++++------- .../PlayerScreen/queue_list_item.dart | 28 +- 2 files changed, 192 insertions(+), 118 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 92c707033..450ec98ce 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -1,14 +1,10 @@ import 'package:audio_service/audio_service.dart'; -import 'package:drag_and_drop_lists/drag_and_drop_list_interface.dart'; import 'package:finamp/models/finamp_models.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; -import 'package:drag_and_drop_lists/drag_and_drop_lists.dart'; import 'package:get_it/get_it.dart'; import 'package:rxdart/rxdart.dart'; -import '../../services/finamp_settings_helper.dart'; import '../album_image.dart'; import '../../models/jellyfin_models.dart' as jellyfin_models; import '../../services/process_artist.dart'; @@ -30,9 +26,12 @@ class _QueueListStreamState { } class QueueList extends StatefulWidget { - const QueueList({Key? key, required this.scrollController}) : super(key: key); + const QueueList( + {Key? key, required this.scrollController, required this.nextUpHeaderKey}) + : super(key: key); final ScrollController scrollController; + final GlobalKey nextUpHeaderKey; @override State createState() => _QueueListState(); @@ -44,7 +43,23 @@ class QueueList extends StatefulWidget { curve: Curves.fastOutSlowIn, ); } +} +void scrollToKey({ + required GlobalKey key, + Duration? duration, +}) { + if (duration == null) { + Scrollable.ensureVisible( + key.currentContext!, + ); + } else { + Scrollable.ensureVisible( + key.currentContext!, + duration: duration, + curve: Curves.easeOut, + ); + } } class _QueueListState extends State { @@ -54,8 +69,6 @@ class _QueueListState extends State { late List _contents; - GlobalKey nextUpHeaderKey = GlobalKey(); - @override void initState() { super.initState(); @@ -64,6 +77,8 @@ class _QueueListState extends State { _source = queueInfo.source; }); + _source = _queueService.getQueue().source; + _contents = [ // const SliverPadding(padding: EdgeInsets.only(top: 0)), // Previous Tracks @@ -72,26 +87,28 @@ class _QueueListState extends State { ), // Current Track SliverAppBar( - key: UniqueKey(), - pinned: true, - collapsedHeight: 70.0, - expandedHeight: 70.0, - leading: const Padding( - padding: EdgeInsets.zero, - ), - flexibleSpace: ListTile( - leading: const AlbumImage( - item: null, - ), - title: Text("Unknown song"), - subtitle: Text("Unknown artist"), - onTap: () {}) - ), + key: UniqueKey(), + pinned: true, + collapsedHeight: 70.0, + expandedHeight: 70.0, + leading: const Padding( + padding: EdgeInsets.zero, + ), + flexibleSpace: ListTile( + leading: const AlbumImage( + item: null, + ), + title: Text("Unknown song"), + subtitle: Text("Unknown artist"), + onTap: () {})), SliverPersistentHeader( - delegate: SectionHeaderDelegate(title: const Text("Queue"))), + delegate: SectionHeaderDelegate( + title: const Text("Queue"), + nextUpHeaderKey: widget.nextUpHeaderKey, + )), // Queue SliverList.list( - key: nextUpHeaderKey, + key: widget.nextUpHeaderKey, children: const [], ), ]; @@ -102,11 +119,9 @@ class _QueueListState extends State { // widget.scrollDown(); scrollToCurrentTrack(); }); - } void scrollToCurrentTrack() { - // dynamic box = currentTrackKey.currentContext!.findRenderObject(); // Offset position = box; //this is global position // double y = position.dy; @@ -117,11 +132,13 @@ class _QueueListState extends State { // duration: Duration(seconds: 2), // curve: Curves.fastOutSlowIn, // ); - Scrollable.ensureVisible( - nextUpHeaderKey.currentContext!, - // duration: const Duration(milliseconds: 200), - // curve: Curves.decelerate, - ); + if (widget.nextUpHeaderKey.currentContext != null) { + Scrollable.ensureVisible( + widget.nextUpHeaderKey.currentContext!, + // duration: const Duration(milliseconds: 200), + // curve: Curves.decelerate, + ); + } } @override @@ -134,25 +151,42 @@ class _QueueListState extends State { padding: const EdgeInsets.only(bottom: 12.0, top: 8.0), sliver: SliverPersistentHeader( delegate: SectionHeaderDelegate( - title: const Text("Recently Played"), height: 30.0), + title: const Text("Recently Played"), + height: 30.0, + nextUpHeaderKey: widget.nextUpHeaderKey, + ), ), ), CurrentTrack( key: UniqueKey(), ), // next up - //TODO don't show this if next up is empty - SliverPadding( - key: nextUpHeaderKey, - padding: const EdgeInsets.only(top: 20.0, bottom: 0.0), - sliver: SliverPersistentHeader( - delegate: SectionHeaderDelegate( - title: const Text("Next Up"), height: 30.0),// _source != null ? "Playing from ${_source?.name}" : "Queue", - ), + SliverToBoxAdapter( + key: widget.nextUpHeaderKey, + ), + StreamBuilder( + stream: _queueService.getQueueStream(), + builder: (context, snapshot) { + if (snapshot.data != null && snapshot.data!.nextUp.isNotEmpty) { + return SliverPadding( + // key: widget.nextUpHeaderKey, + padding: const EdgeInsets.only(top: 20.0, bottom: 0.0), + sliver: SliverPersistentHeader( + delegate: SectionHeaderDelegate( + title: const Text("Next Up"), + height: 30.0, + nextUpHeaderKey: widget.nextUpHeaderKey, + ), // _source != null ? "Playing from ${_source?.name}" : "Queue", + ), + ); + } else { + return const SliverToBoxAdapter(); + } + }, ), - NextUpTracksList(), + const NextUpTracksList(), SliverPadding( - padding: const EdgeInsets.only(top: 20.0, bottom: 6.0), + padding: const EdgeInsets.only(top: 20.0, bottom: 0.0), sliver: SliverPersistentHeader( delegate: SectionHeaderDelegate( title: Row( @@ -164,6 +198,7 @@ class _QueueListState extends State { ), // _source != null ? "Playing from ${_source?.name}" : "Queue", controls: true, + nextUpHeaderKey: widget.nextUpHeaderKey, ), ), ), @@ -179,7 +214,8 @@ class _QueueListState extends State { } Future showQueueBottomSheet(BuildContext context) { - + GlobalKey nextUpHeaderKey = GlobalKey(); + return showModalBottomSheet( // showDragHandle: true, useSafeArea: true, @@ -193,8 +229,8 @@ Future showQueueBottomSheet(BuildContext context) { return DraggableScrollableSheet( snap: false, snapAnimationDuration: const Duration(milliseconds: 200), - initialChildSize: 0.9, - maxChildSize: 0.9, + initialChildSize: 0.92, + maxChildSize: 0.92, expand: false, builder: (context, scrollController) { return Scaffold( @@ -221,14 +257,26 @@ Future showQueueBottomSheet(BuildContext context) { Expanded( child: QueueList( scrollController: scrollController, + nextUpHeaderKey: nextUpHeaderKey, ), ), ], ), - // floatingActionButton: FloatingActionButton.small( - // onPressed: _scrollDown, - // child: Icon(Icons.arrow_downward), - // ), + //TODO fade this out if the key is visible + floatingActionButton: FloatingActionButton( + onPressed: () => scrollToKey( + key: nextUpHeaderKey, + duration: const Duration(milliseconds: 500)), + backgroundColor: const Color.fromRGBO(188, 136, 86, 0.60), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0))), + child: const Padding( + padding: EdgeInsets.only(bottom: 4.0), + child: Icon( + TablerIcons.focus_2, + size: 28.0, + ), + )), ); // ) // return QueueList( @@ -249,7 +297,8 @@ class PreviousTracksList extends StatefulWidget { State createState() => _PreviousTracksListState(); } -class _PreviousTracksListState extends State with TickerProviderStateMixin { +class _PreviousTracksListState extends State + with TickerProviderStateMixin { final _queueService = GetIt.instance(); List? _previousTracks; @@ -295,7 +344,8 @@ class _PreviousTracksListState extends State with TickerProv actualIndex: actualIndex, indexOffset: indexOffset, subqueue: _previousTracks!, - allowReorder: _queueService.playbackOrder == PlaybackOrder.linear, + allowReorder: + _queueService.playbackOrder == PlaybackOrder.linear, onTap: () async { await _queueService.skipByOffset(indexOffset); }, @@ -434,7 +484,8 @@ class _QueueTracksListState extends State { actualIndex: actualIndex, indexOffset: indexOffset, subqueue: _queue!, - allowReorder: _queueService.playbackOrder == PlaybackOrder.linear, + allowReorder: + _queueService.playbackOrder == PlaybackOrder.linear, onTap: () async { await _queueService.skipByOffset(indexOffset); }, @@ -620,16 +671,12 @@ class CurrentTrack extends StatelessWidget { children: [ Text( // '0:00', - playbackPosition!.inHours > 1.0 - ? playbackPosition - .toString() - .split('.')[0] - : playbackPosition - .toString() - .substring(2, 7), + playbackPosition!.inHours >= 1.0 + ? "${playbackPosition?.inHours.toString()}:${((playbackPosition?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" + : "${playbackPosition?.inMinutes.toString()}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", style: TextStyle( color: Colors.white.withOpacity(0.8), - fontSize: 12, + fontSize: 14, fontFamily: 'Lexend Deca', fontWeight: FontWeight.w300, ), @@ -639,9 +686,9 @@ class CurrentTrack extends StatelessWidget { '/', style: TextStyle( color: Colors.white.withOpacity(0.8), - fontSize: 12, + fontSize: 14, fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w300, + fontWeight: FontWeight.w400, ), ), const SizedBox(width: 2), @@ -649,38 +696,34 @@ class CurrentTrack extends StatelessWidget { // '3:44', (mediaState?.mediaItem?.duration ?.inHours ?? - const Duration(seconds: 0) - .inHours) > + 0.0) >= 1.0 - ? (mediaState - ?.mediaItem?.duration ?? - const Duration(seconds: 0)) - .toString() - .split('.')[0] - : (mediaState - ?.mediaItem?.duration ?? - const Duration(seconds: 0)) - .toString() - .substring(2, 7), + ? "${mediaState?.mediaItem?.duration?.inHours.toString()}:${((mediaState?.mediaItem?.duration?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((mediaState?.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" + : "${mediaState?.mediaItem?.duration?.inMinutes.toString()}:${((mediaState?.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", style: TextStyle( color: Colors.white.withOpacity(0.8), - fontSize: 12, + fontSize: 14, fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w300, + fontWeight: FontWeight.w400, ), ), ], ), IconButton( + padding: const EdgeInsets.only(left: 8.0), + // visualDensity: VisualDensity.compact, icon: const Icon( TablerIcons.heart, size: 32, color: Colors.white, - weight: 1.5, + weight: + 1.5, //TODO weight not working, stroke is too thick for most icons ), onPressed: () {}, ), IconButton( + padding: const EdgeInsets.all(0.0), + // visualDensity: VisualDensity.compact, icon: const Icon( TablerIcons.dots_vertical, size: 32, @@ -710,36 +753,37 @@ class CurrentTrack extends StatelessWidget { } class PlaybackBehaviorInfo { - final PlaybackOrder order; final LoopMode loop; PlaybackBehaviorInfo(this.order, this.loop); - } class SectionHeaderDelegate extends SliverPersistentHeaderDelegate { final Widget title; final bool controls; final double height; + final GlobalKey nextUpHeaderKey; SectionHeaderDelegate({ required this.title, + required this.nextUpHeaderKey, this.controls = false, this.height = 30.0, }); @override Widget build(context, double shrinkOffset, bool overlapsContent) { - final _queueService = GetIt.instance(); - - return StreamBuilder( - stream: Rx.combineLatest2(_queueService.getPlaybackOrderStream(), _queueService.getLoopModeStream(), (a, b) => PlaybackBehaviorInfo(a, b)), - builder:(context, snapshot) { + return StreamBuilder( + stream: Rx.combineLatest2( + _queueService.getPlaybackOrderStream(), + _queueService.getLoopModeStream(), + (a, b) => PlaybackBehaviorInfo(a, b)), + builder: (context, snapshot) { PlaybackBehaviorInfo? info = snapshot.data as PlaybackBehaviorInfo?; - + return Padding( padding: const EdgeInsets.symmetric(horizontal: 14.0), child: Row( @@ -754,38 +798,46 @@ class SectionHeaderDelegate extends SliverPersistentHeaderDelegate { ])), if (controls) IconButton( - icon: info?.order == PlaybackOrder.shuffled ? ( - const Icon( - TablerIcons.arrows_shuffle, - ) - ) : ( - const Icon( - TablerIcons.arrows_right, - ) - ), - color: info?.order == PlaybackOrder.shuffled ? Colors.orange : Colors.white, - onPressed: () => { - _queueService.togglePlaybackOrder(), //TODO scroll back to current track after toggling - } - ), + padding: const EdgeInsets.only(bottom: 2.0), + iconSize: 28.0, + icon: info?.order == PlaybackOrder.shuffled + ? (const Icon( + TablerIcons.arrows_shuffle, + )) + : (const Icon( + TablerIcons.arrows_right, + )), + color: info?.order == PlaybackOrder.shuffled + ? Colors.orange + : Colors.white, + onPressed: () { + _queueService.togglePlaybackOrder(); + //TODO why is the current track scrolled out of view **after** the queue is updated? + Future.delayed( + const Duration(milliseconds: 300), + () => scrollToKey( + key: nextUpHeaderKey, + duration: const Duration(milliseconds: 500))); + // scrollToKey(key: nextUpHeaderKey, duration: const Duration(milliseconds: 1000)); + }), if (controls) IconButton( - icon: info?.loop != LoopMode.none ? ( - info?.loop == LoopMode.one ? ( - const Icon( - TablerIcons.repeat_once, - ) - ) : ( - const Icon( - TablerIcons.repeat, - ) - ) - ) : ( - const Icon( - TablerIcons.repeat_off, - ) - ), - color: info?.loop != LoopMode.none ? Colors.orange : Colors.white, + padding: const EdgeInsets.only(bottom: 2.0), + iconSize: 28.0, + icon: info?.loop != LoopMode.none + ? (info?.loop == LoopMode.one + ? (const Icon( + TablerIcons.repeat_once, + )) + : (const Icon( + TablerIcons.repeat, + ))) + : (const Icon( + TablerIcons.repeat_off, + )), + color: info?.loop != LoopMode.none + ? Colors.orange + : Colors.white, onPressed: () => _queueService.toggleLoopMode(), ), ], diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index 1e8a2f5b2..086959ab6 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -1,4 +1,12 @@ +import 'package:finamp/components/AlbumScreen/song_list_tile.dart'; import 'package:finamp/components/album_image.dart'; +import 'package:finamp/components/error_snackbar.dart'; +import 'package:finamp/screens/add_to_playlist_screen.dart'; +import 'package:finamp/screens/album_screen.dart'; +import 'package:finamp/services/audio_service_helper.dart'; +import 'package:finamp/services/downloads_helper.dart'; +import 'package:finamp/services/finamp_settings_helper.dart'; +import 'package:finamp/services/jellyfin_api_helper.dart'; import 'package:finamp/services/music_player_background_task.dart'; import 'package:finamp/services/process_artist.dart'; import 'package:flutter/material.dart' hide ReorderableList; @@ -36,7 +44,9 @@ class QueueListItem extends StatefulWidget { class _QueueListItemState extends State { final _audioHandler = GetIt.instance(); + final _audioServiceHelper = GetIt.instance(); final _queueService = GetIt.instance(); + final _jellyfinApiHelper = GetIt.instance(); @override Widget build(BuildContext context) { @@ -91,25 +101,37 @@ class _QueueListItemState extends State { trailing: Container( alignment: Alignment.centerRight, margin: const EdgeInsets.only(right: 8.0), - width: 115.0, + width: widget.allowReorder ? 145.0 : 115.0, height: 50.0, child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( - "${widget.item.item.duration?.inMinutes.toString().padLeft(2, '0')}:${((widget.item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + "${widget.item.item.duration?.inMinutes.toString()}:${((widget.item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", textAlign: TextAlign.end, style: TextStyle( color: Theme.of(context).textTheme.bodySmall?.color, ), ), IconButton( + padding: const EdgeInsets.all(0.0), + visualDensity: VisualDensity.compact, + icon: const Icon( + TablerIcons.dots_vertical, + color: Colors.white, + weight: 1.5, + ), + iconSize: 24.0, + ), + IconButton( + padding: const EdgeInsets.only(right: 14.0), + visualDensity: VisualDensity.compact, icon: const Icon( TablerIcons.x, color: Colors.white, weight: 1.5, ), - iconSize: 28.0, + iconSize: 24.0, onPressed: () async => await _queueService.removeAtOffset(widget.indexOffset), ), From 2f90d202e008b8d2b400e8c23885ca7b80988f74 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 11 Aug 2023 23:20:46 +0200 Subject: [PATCH 053/130] added song menus when tapping three dots icon --- lib/components/PlayerScreen/queue_list.dart | 251 +++++++++++++++- .../PlayerScreen/queue_list_item.dart | 267 ++++++++++++++++++ 2 files changed, 510 insertions(+), 8 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 450ec98ce..ff28995bf 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -1,5 +1,14 @@ import 'package:audio_service/audio_service.dart'; +import 'package:finamp/components/AlbumScreen/song_list_tile.dart'; +import 'package:finamp/components/error_snackbar.dart'; import 'package:finamp/models/finamp_models.dart'; +import 'package:finamp/screens/add_to_playlist_screen.dart'; +import 'package:finamp/screens/album_screen.dart'; +import 'package:finamp/services/audio_service_helper.dart'; +import 'package:finamp/services/downloads_helper.dart'; +import 'package:finamp/services/finamp_settings_helper.dart'; +import 'package:finamp/services/jellyfin_api_helper.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get_it/get_it.dart'; @@ -501,16 +510,32 @@ class _QueueTracksListState extends State { } } -class CurrentTrack extends StatelessWidget { +class CurrentTrack extends StatefulWidget { const CurrentTrack({ Key? key, }) : super(key: key); @override - Widget build(context) { - final queueService = GetIt.instance(); - final audioHandler = GetIt.instance(); + State createState() => _CurrentTrackState(); +} + +class _CurrentTrackState extends State { + late QueueService _queueService; + late MusicPlayerBackgroundTask _audioHandler; + late AudioServiceHelper _audioServiceHelper; + late JellyfinApiHelper _jellyfinApiHelper; + + @override + void initState() { + super.initState(); + _queueService = GetIt.instance(); + _audioHandler = GetIt.instance(); + _audioServiceHelper = GetIt.instance(); + _jellyfinApiHelper = GetIt.instance(); + } + @override + Widget build(context) { QueueItem? currentTrack; MediaState? mediaState; Duration? playbackPosition; @@ -520,8 +545,8 @@ class CurrentTrack extends StatelessWidget { _QueueListStreamState>( mediaStateStream, AudioService.position - .startWith(audioHandler.playbackState.value.position), - queueService.getQueueStream(), + .startWith(_audioHandler.playbackState.value.position), + _queueService.getQueueStream(), (a, b, c) => _QueueListStreamState(a, b, c)), builder: (context, snapshot) { if (snapshot.hasData) { @@ -581,7 +606,7 @@ class CurrentTrack extends StatelessWidget { ), child: IconButton( onPressed: () { - audioHandler.togglePlayback(); + _audioHandler.togglePlayback(); }, icon: mediaState!.playbackState.playing ? const Icon( @@ -730,7 +755,8 @@ class CurrentTrack extends StatelessWidget { color: Colors.white, weight: 1.5, ), - onPressed: () {}, + onPressed: () => + showSongMenu(currentTrack!), ), ], ), @@ -750,6 +776,202 @@ class CurrentTrack extends StatelessWidget { }, ); } + + void showSongMenu(QueueItem currentTrack) async { + final item = jellyfin_models.BaseItemDto.fromJson( + currentTrack?.item.extras?["itemJson"]); + + final canGoToAlbum = _isAlbumDownloadedIfOffline(item.parentId); + + // Some options are disabled in offline mode + final isOffline = FinampSettingsHelper.finampSettings.isOffline; + + final selection = await showMenu( + context: context, + position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width - 50.0, + MediaQuery.of(context).size.height - 50.0, 0.0, 0.0), + items: [ + PopupMenuItem( + value: SongListTileMenuItems.addToQueue, + child: ListTile( + leading: const Icon(Icons.queue_music), + title: Text(AppLocalizations.of(context)!.addToQueue), + ), + ), + PopupMenuItem( + value: SongListTileMenuItems.playNext, + child: ListTile( + leading: const Icon(TablerIcons.hourglass_low), + title: Text("Play next"), + ), + ), + PopupMenuItem( + value: SongListTileMenuItems.addToNextUp, + child: ListTile( + leading: const Icon(TablerIcons.hourglass_high), + title: Text("Add to Next Up"), + ), + ), + PopupMenuItem( + enabled: !isOffline, + value: SongListTileMenuItems.addToPlaylist, + child: ListTile( + leading: const Icon(Icons.playlist_add), + title: Text(AppLocalizations.of(context)!.addToPlaylistTitle), + enabled: !isOffline, + ), + ), + PopupMenuItem( + enabled: !isOffline, + value: SongListTileMenuItems.instantMix, + child: ListTile( + leading: const Icon(Icons.explore), + title: Text(AppLocalizations.of(context)!.instantMix), + enabled: !isOffline, + ), + ), + PopupMenuItem( + enabled: canGoToAlbum, + value: SongListTileMenuItems.goToAlbum, + child: ListTile( + leading: const Icon(Icons.album), + title: Text(AppLocalizations.of(context)!.goToAlbum), + enabled: canGoToAlbum, + ), + ), + item.userData!.isFavorite + ? PopupMenuItem( + value: SongListTileMenuItems.removeFavourite, + child: ListTile( + leading: const Icon(Icons.favorite_border), + title: Text(AppLocalizations.of(context)!.removeFavourite), + ), + ) + : PopupMenuItem( + value: SongListTileMenuItems.addFavourite, + child: ListTile( + leading: const Icon(Icons.favorite), + title: Text(AppLocalizations.of(context)!.addFavourite), + ), + ), + ], + ); + + if (!mounted) return; + + switch (selection) { + case SongListTileMenuItems.addToQueue: + // await _audioServiceHelper.addQueueItem(item); + await _queueService.addToQueue( + item, + QueueItemSource( + type: QueueItemSourceType.unknown, + name: "Queue", + id: currentTrack.source.id ?? "unknown")); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.addedToQueue), + )); + break; + + case SongListTileMenuItems.playNext: + // await _audioServiceHelper.addQueueItem(item); + await _queueService.addNext(item); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text("Track will play next"), + )); + break; + + case SongListTileMenuItems.addToNextUp: + // await _audioServiceHelper.addQueueItem(item); + await _queueService.addToNextUp(item); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text("Added track to Next Up"), + )); + break; + + case SongListTileMenuItems.addToPlaylist: + Navigator.of(context) + .pushNamed(AddToPlaylistScreen.routeName, arguments: item.id); + break; + + case SongListTileMenuItems.instantMix: + await _audioServiceHelper.startInstantMixForItem(item); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.startingInstantMix), + )); + break; + case SongListTileMenuItems.goToAlbum: + late jellyfin_models.BaseItemDto album; + if (FinampSettingsHelper.finampSettings.isOffline) { + // If offline, load the album's BaseItemDto from DownloadHelper. + final downloadsHelper = GetIt.instance(); + + // downloadedParent won't be null here since the menu item already + // checks if the DownloadedParent exists. + album = downloadsHelper.getDownloadedParent(item.parentId!)!.item; + } else { + // If online, get the album's BaseItemDto from the server. + try { + album = await _jellyfinApiHelper.getItemById(item.parentId!); + } catch (e) { + errorSnackbar(e, context); + break; + } + } + + if (!mounted) return; + + Navigator.of(context) + .pushNamed(AlbumScreen.routeName, arguments: album); + break; + case SongListTileMenuItems.addFavourite: + case SongListTileMenuItems.removeFavourite: + await setFavourite(item); + break; + case null: + break; + } + } + + Future setFavourite(jellyfin_models.BaseItemDto item) async { + try { + // We switch the widget state before actually doing the request to + // make the app feel faster (without, there is a delay from the + // user adding the favourite and the icon showing) + setState(() { + item.userData!.isFavorite = !item.userData!.isFavorite; + }); + + // Since we flipped the favourite state already, we can use the flipped + // state to decide which API call to make + final newUserData = item.userData!.isFavorite + ? await _jellyfinApiHelper.addFavourite(item.id) + : await _jellyfinApiHelper.removeFavourite(item.id); + + if (!mounted) return; + + setState(() { + item.userData = newUserData; + }); + } catch (e) { + setState(() { + item.userData!.isFavorite = !item.userData!.isFavorite; + }); + errorSnackbar(e, context); + } + } } class PlaybackBehaviorInfo { @@ -856,3 +1078,16 @@ class SectionHeaderDelegate extends SliverPersistentHeaderDelegate { @override bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => false; } + +/// If offline, check if an album is downloaded. Always returns true if online. +/// Returns false if albumId is null. +bool _isAlbumDownloadedIfOffline(String? albumId) { + if (albumId == null) { + return false; + } else if (FinampSettingsHelper.finampSettings.isOffline) { + final downloadsHelper = GetIt.instance(); + return downloadsHelper.isAlbumDownloaded(albumId); + } else { + return true; + } +} diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index 086959ab6..58c0bcf68 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -122,6 +122,7 @@ class _QueueListItemState extends State { weight: 1.5, ), iconSize: 24.0, + onPressed: () => showSongMenu(), ), IconButton( padding: const EdgeInsets.only(right: 14.0), @@ -151,4 +152,270 @@ class _QueueListItemState extends State { onTap: widget.onTap, )); } + + void showSongMenu() async { + final canGoToAlbum = _isAlbumDownloadedIfOffline( + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]) + .parentId); + + // Some options are disabled in offline mode + final isOffline = + FinampSettingsHelper.finampSettings.isOffline; + + final selection = await showMenu( + context: context, + position: RelativeRect.fromLTRB( + MediaQuery.of(context).size.width - 50.0, + MediaQuery.of(context).size.height - 50.0, + 0.0, + 0.0), + items: [ + PopupMenuItem( + value: SongListTileMenuItems.addToQueue, + child: ListTile( + leading: const Icon(Icons.queue_music), + title: + Text(AppLocalizations.of(context)!.addToQueue), + ), + ), + PopupMenuItem( + value: SongListTileMenuItems.playNext, + child: ListTile( + leading: const Icon(TablerIcons.hourglass_low), + title: Text("Play next"), + ), + ), + PopupMenuItem( + value: SongListTileMenuItems.addToNextUp, + child: ListTile( + leading: const Icon(TablerIcons.hourglass_high), + title: Text("Add to Next Up"), + ), + ), + PopupMenuItem( + enabled: !isOffline, + value: SongListTileMenuItems.addToPlaylist, + child: ListTile( + leading: const Icon(Icons.playlist_add), + title: Text(AppLocalizations.of(context)! + .addToPlaylistTitle), + enabled: !isOffline, + ), + ), + PopupMenuItem( + enabled: !isOffline, + value: SongListTileMenuItems.instantMix, + child: ListTile( + leading: const Icon(Icons.explore), + title: + Text(AppLocalizations.of(context)!.instantMix), + enabled: !isOffline, + ), + ), + PopupMenuItem( + enabled: canGoToAlbum, + value: SongListTileMenuItems.goToAlbum, + child: ListTile( + leading: const Icon(Icons.album), + title: + Text(AppLocalizations.of(context)!.goToAlbum), + enabled: canGoToAlbum, + ), + ), + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]) + .userData! + .isFavorite + ? PopupMenuItem( + value: SongListTileMenuItems.removeFavourite, + child: ListTile( + leading: const Icon(Icons.favorite_border), + title: Text(AppLocalizations.of(context)! + .removeFavourite), + ), + ) + : PopupMenuItem( + value: SongListTileMenuItems.addFavourite, + child: ListTile( + leading: const Icon(Icons.favorite), + title: Text(AppLocalizations.of(context)! + .addFavourite), + ), + ), + ], + ); + + if (!mounted) return; + + switch (selection) { + case SongListTileMenuItems.addToQueue: + // await _audioServiceHelper.addQueueItem(jellyfin_models.BaseItemDto.fromJson(widget.item.item.extras?["itemJson"])); + await _queueService.addToQueue( + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]), + QueueItemSource( + type: QueueItemSourceType.unknown, + name: "Queue", + id: widget.item.source.id ?? "unknown")); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: + Text(AppLocalizations.of(context)!.addedToQueue), + )); + break; + + case SongListTileMenuItems.playNext: + // await _audioServiceHelper.addQueueItem(jellyfin_models.BaseItemDto.fromJson(widget.item.item.extras?["itemJson"])); + await _queueService.addNext( + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"])); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text("Track will play next"), + )); + break; + + case SongListTileMenuItems.addToNextUp: + // await _audioServiceHelper.addQueueItem(jellyfin_models.BaseItemDto.fromJson(widget.item.item.extras?["itemJson"])); + await _queueService.addToNextUp( + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"])); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text("Added track to Next Up"), + )); + break; + + case SongListTileMenuItems.addToPlaylist: + Navigator.of(context).pushNamed( + AddToPlaylistScreen.routeName, + arguments: jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]) + .id); + break; + + case SongListTileMenuItems.instantMix: + await _audioServiceHelper.startInstantMixForItem( + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"])); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + AppLocalizations.of(context)!.startingInstantMix), + )); + break; + case SongListTileMenuItems.goToAlbum: + late jellyfin_models.BaseItemDto album; + if (FinampSettingsHelper.finampSettings.isOffline) { + // If offline, load the album's BaseItemDto from DownloadHelper. + final downloadsHelper = + GetIt.instance(); + + // downloadedParent won't be null here since the menu item already + // checks if the DownloadedParent exists. + album = downloadsHelper + .getDownloadedParent( + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]) + .parentId!)! + .item; + } else { + // If online, get the album's BaseItemDto from the server. + try { + album = await _jellyfinApiHelper.getItemById( + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]) + .parentId!); + } catch (e) { + errorSnackbar(e, context); + break; + } + } + + if (!mounted) return; + + Navigator.of(context) + .pushNamed(AlbumScreen.routeName, arguments: album); + break; + case SongListTileMenuItems.addFavourite: + case SongListTileMenuItems.removeFavourite: + await setFavourite(); + break; + case null: + break; + } + } + + Future setFavourite() async { + try { + // We switch the widget state before actually doing the request to + // make the app feel faster (without, there is a delay from the + // user adding the favourite and the icon showing) + setState(() { + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]) + .userData! + .isFavorite = !jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]) + .userData! + .isFavorite; + }); + + // Since we flipped the favourite state already, we can use the flipped + // state to decide which API call to make + final newUserData = jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]) + .userData! + .isFavorite + ? await _jellyfinApiHelper.addFavourite( + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]) + .id) + : await _jellyfinApiHelper.removeFavourite( + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]) + .id); + + if (!mounted) return; + + setState(() { + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]) + .userData = newUserData; + }); + } catch (e) { + setState(() { + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]) + .userData! + .isFavorite = !jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]) + .userData! + .isFavorite; + }); + errorSnackbar(e, context); + } + } +} + +/// If offline, check if an album is downloaded. Always returns true if online. +/// Returns false if albumId is null. +bool _isAlbumDownloadedIfOffline(String? albumId) { + if (albumId == null) { + return false; + } else if (FinampSettingsHelper.finampSettings.isOffline) { + final downloadsHelper = GetIt.instance(); + return downloadsHelper.isAlbumDownloaded(albumId); + } else { + return true; + } } From 44ec70720d290cfc2433b88056eb8e4589c130d5 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 11 Aug 2023 23:30:38 +0200 Subject: [PATCH 054/130] minor improvements --- lib/components/AlbumScreen/song_list_tile.dart | 18 ++++++++++++------ lib/services/queue_service.dart | 2 ++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index eb0aa69ad..115b32e84 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -184,10 +184,15 @@ class _SongListTileState extends State { onlyIfFav: true, ), onTap: () { - _audioServiceHelper.replaceQueueWithItem( - itemList: widget.children ?? [widget.item], - initialIndex: widget.index ?? 0, - ); + //TODO go through queue service once it supports specifying the index + if (widget.children != null) { + _audioServiceHelper.replaceQueueWithItem( + itemList: widget.children ?? [widget.item], + initialIndex: widget.index ?? 0, + ); + } else { + _audioServiceHelper.startInstantMixForItem(widget.item); + } }, ); @@ -344,8 +349,9 @@ class _SongListTileState extends State { break; case SongListTileMenuItems.replaceQueueWithItem: - await _audioServiceHelper - .replaceQueueWithItem(itemList: [widget.item]); + // await _audioServiceHelper + // .replaceQueueWithItem(itemList: [widget.item]); + await _queueService.startPlayback(items: [widget.item], source: QueueItemSource(type: QueueItemSourceType.unknown, name: "Queue", id: widget.parentId ?? "unknown")); if (!mounted) return; diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index f4d1386e1..7543fcb55 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -154,6 +154,8 @@ class QueueService { required QueueItemSource source }) async { + // TODO support starting playback from a specific item (index) in the list + // _initialQueue = list; // save original PlaybackList for looping/restarting and meta info await _replaceWholeQueue(itemList: items, source: source); _queueServiceLogger.info("Started playing '${source.name}' (${source.type})"); From e42f300a7065b54b1c0d141b7fd033773ee339eb Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 11 Aug 2023 23:34:51 +0200 Subject: [PATCH 055/130] don't use lazyPreparation to (hopefully) support pre-loading --- lib/services/queue_service.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 7543fcb55..5bf4138cb 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -58,7 +58,6 @@ class QueueService { // the audio source used by the player. The first X items of all internal queues are merged together into this source, so that all player features, like gapless playback, are supported ConcatenatingAudioSource _queueAudioSource = ConcatenatingAudioSource( children: [], - useLazyPreparation: true, ); int _queueAudioSourceIndex = 0; @@ -66,7 +65,6 @@ class QueueService { _queueAudioSource = ConcatenatingAudioSource( children: [], - useLazyPreparation: true, shuffleOrder: NextUpShuffleOrder(queueService: this), ); From 233739cfff5f78657c8bb6e174ab66441e07fa49 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 12 Aug 2023 00:01:26 +0200 Subject: [PATCH 056/130] enable favorite icon in current track widget --- lib/components/PlayerScreen/queue_list.dart | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index ff28995bf..b5ebdb63d 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -737,14 +737,24 @@ class _CurrentTrackState extends State { IconButton( padding: const EdgeInsets.only(left: 8.0), // visualDensity: VisualDensity.compact, - icon: const Icon( + icon: jellyfin_models.BaseItemDto.fromJson(currentTrack!.item.extras?["itemJson"]).userData!.isFavorite ? const Icon( + TablerIcons.heart, + size: 32, + color: Color.fromRGBO(188, 136, 86, 1.0), + weight: + 1.5, //TODO weight not working, stroke is too thick for most icons + ) : const Icon( TablerIcons.heart, size: 32, color: Colors.white, weight: 1.5, //TODO weight not working, stroke is too thick for most icons ), - onPressed: () {}, + onPressed: () => { + setState(() { + setFavourite(jellyfin_models.BaseItemDto.fromJson(currentTrack!.item.extras?["itemJson"])); + }) + }, ), IconButton( padding: const EdgeInsets.all(0.0), From 36610e961fdc44b9a66d219d4ac47a78881a3cd3 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Wed, 16 Aug 2023 13:11:55 +0200 Subject: [PATCH 057/130] make recent tracks toggleable --- lib/components/PlayerScreen/queue_list.dart | 42 +++++++++++++-------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index b5ebdb63d..329a53ed6 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -77,6 +77,7 @@ class _QueueListState extends State { QueueItemSource? _source; late List _contents; + bool isRecentTracksExpanded = false; @override void initState() { @@ -122,12 +123,6 @@ class _QueueListState extends State { ), ]; - // call function after 2 seconds - Future.delayed(const Duration(milliseconds: 50), () { - // setState(() {}); - // widget.scrollDown(); - scrollToCurrentTrack(); - }); } void scrollToCurrentTrack() { @@ -153,18 +148,33 @@ class _QueueListState extends State { @override Widget build(BuildContext context) { _contents = [ - // const SliverPadding(padding: EdgeInsets.only(top: 0)), // Previous Tracks - const PreviousTracksList(), - SliverPadding( - padding: const EdgeInsets.only(bottom: 12.0, top: 8.0), - sliver: SliverPersistentHeader( - delegate: SectionHeaderDelegate( - title: const Text("Recently Played"), - height: 30.0, - nextUpHeaderKey: widget.nextUpHeaderKey, + if (isRecentTracksExpanded) + const PreviousTracksList() + , + SliverToBoxAdapter( + child: GestureDetector( + onTap:() => setState(() => isRecentTracksExpanded = !isRecentTracksExpanded), + child: Padding( + padding: const EdgeInsets.only(left: 14.0, right: 14.0, bottom: 12.0, top: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.only(top: 2.0), + child: Text("Recently Played"), + ), + const SizedBox(width: 4.0), + Icon( + isRecentTracksExpanded ? TablerIcons.chevron_up : TablerIcons.chevron_down, + size: 28.0, + color: Colors.white, + ), + ], + ), ), - ), + ) ), CurrentTrack( key: UniqueKey(), From 0947a14f25a45a70da08ab833731cf105a560640 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Wed, 16 Aug 2023 16:26:32 +0200 Subject: [PATCH 058/130] fix shuffle always starting on first index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit what is *wrong* with just_audio?! shuffling the playlist on load? 👀 --- lib/services/queue_service.dart | 55 +++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 5bf4138cb..bfbd55eab 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:finamp/services/playback_history_service.dart'; +import 'package:flutter/foundation.dart'; import 'package:just_audio/just_audio.dart'; import 'package:audio_service/audio_service.dart'; import 'package:get_it/get_it.dart'; @@ -59,13 +60,15 @@ class QueueService { ConcatenatingAudioSource _queueAudioSource = ConcatenatingAudioSource( children: [], ); + late ShuffleOrder _shuffleOrder; int _queueAudioSourceIndex = 0; QueueService() { + _shuffleOrder = NextUpShuffleOrder(queueService: this); _queueAudioSource = ConcatenatingAudioSource( children: [], - shuffleOrder: NextUpShuffleOrder(queueService: this), + shuffleOrder: _shuffleOrder, ); _audioHandler.getPlaybackEventStream().listen((event) async { @@ -149,13 +152,14 @@ class QueueService { Future startPlayback({ required List items, - required QueueItemSource source + required QueueItemSource source, + int startingIndex = 0, }) async { // TODO support starting playback from a specific item (index) in the list // _initialQueue = list; // save original PlaybackList for looping/restarting and meta info - await _replaceWholeQueue(itemList: items, source: source); + await _replaceWholeQueue(itemList: items, source: source, initialIndex: startingIndex); _queueServiceLogger.info("Started playing '${source.name}' (${source.type})"); } @@ -207,14 +211,16 @@ class QueueService { } await _queueAudioSource.addAll(audioSources); + _shuffleOrder.shuffle(); // shuffle without providing an index to make sure shuffle doesn't always start at the first index // set first item in queue _queueAudioSourceIndex = 0; - _audioHandler.setNextInitialIndex(_queueAudioSource.shuffleIndices[_queueAudioSourceIndex]); + if (_playbackOrder == PlaybackOrder.shuffled) { + _queueAudioSourceIndex = _queueAudioSource.shuffleIndices[0]; + } + _audioHandler.setNextInitialIndex(_queueAudioSourceIndex); await _audioHandler.initializeAudioSource(_queueAudioSource); - playbackOrder = _playbackOrder; // re-trigger playback order setter to update queue - newShuffledOrder = List.from(_queueAudioSource.shuffleIndices); _order = QueueOrder( @@ -606,13 +612,19 @@ class NextUpShuffleOrder extends ShuffleOrder { @override void shuffle({int? initialIndex}) { assert(initialIndex == null || indices.contains(initialIndex)); + + if (initialIndex == null) { + // will only be called manually, when replacing the whole queue + indices.shuffle(_random); + return; + } + indices.clear(); _queueService!._queueFromConcatenatingAudioSource(); QueueInfo queueInfo = _queueService!.getQueue(); indices = List.generate(queueInfo.previousTracks.length + 1 + queueInfo.nextUp.length + queueInfo.queue.length, (i) => i); if (indices.length <= 1) return; indices.shuffle(_random); - if (initialIndex == null) return; _queueService!.queueServiceLogger.finest("initialIndex: $initialIndex"); @@ -624,26 +636,21 @@ class NextUpShuffleOrder extends ShuffleOrder { _queueService!.queueServiceLogger.finest("Shuffled indices: $indicesString"); _queueService!.queueServiceLogger.finest("Current Track: ${queueInfo.currentTrack}"); - // check if something is already playing, if not we also want to shuffle the first item (don't swap) - if (queueInfo.currentTrack != null) { - - int nextUpLength = 0; - if (_queueService != null) { - nextUpLength = queueInfo.nextUp.length; - } - - const initialPos = 0; // current item will always be at the front + int nextUpLength = 0; + if (_queueService != null) { + nextUpLength = queueInfo.nextUp.length; + } - // move current track and next up tracks to the front, pushing all other tracks back while keeping their order - // remove current track and next up tracks from indices and save them in a separate list - List currentTrackIndices = []; - for (int i = 0; i < 1 + nextUpLength; i++) { - currentTrackIndices.add(indices.removeAt(indices.indexOf(initialIndex + i))); - } - // insert current track and next up tracks at the front - indices.insertAll(initialPos, currentTrackIndices); + const initialPos = 0; // current item will always be at the front + // move current track and next up tracks to the front, pushing all other tracks back while keeping their order + // remove current track and next up tracks from indices and save them in a separate list + List currentTrackIndices = []; + for (int i = 0; i < 1 + nextUpLength; i++) { + currentTrackIndices.add(indices.removeAt(indices.indexOf(initialIndex + i))); } + // insert current track and next up tracks at the front + indices.insertAll(initialPos, currentTrackIndices); // log indices indicesString = ""; From 9d244e60039a927aead6a94115cb909c94d7de5e Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Wed, 16 Aug 2023 16:43:06 +0200 Subject: [PATCH 059/130] support starting album/playlist from specific index --- lib/components/AlbumScreen/song_list_tile.dart | 14 ++++++++++---- lib/services/queue_service.dart | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index 115b32e84..490520b6b 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -184,11 +184,17 @@ class _SongListTileState extends State { onlyIfFav: true, ), onTap: () { - //TODO go through queue service once it supports specifying the index if (widget.children != null) { - _audioServiceHelper.replaceQueueWithItem( - itemList: widget.children ?? [widget.item], - initialIndex: widget.index ?? 0, + // start linear playback of album from the given index + _queueService.playbackOrder = PlaybackOrder.linear; + _queueService.startPlayback( + items: widget.children!, + source: QueueItemSource( + type: QueueItemSourceType.album, + name: widget.item.album ?? "Somewhere", + id: widget.parentId ?? "", + ), + startingIndex: widget.index ?? 0, ); } else { _audioServiceHelper.startInstantMixForItem(widget.item); diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index bfbd55eab..59faa4001 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -214,9 +214,9 @@ class QueueService { _shuffleOrder.shuffle(); // shuffle without providing an index to make sure shuffle doesn't always start at the first index // set first item in queue - _queueAudioSourceIndex = 0; + _queueAudioSourceIndex = initialIndex; if (_playbackOrder == PlaybackOrder.shuffled) { - _queueAudioSourceIndex = _queueAudioSource.shuffleIndices[0]; + _queueAudioSourceIndex = _queueAudioSource.shuffleIndices[initialIndex]; } _audioHandler.setNextInitialIndex(_queueAudioSourceIndex); await _audioHandler.initializeAudioSource(_queueAudioSource); From fa31e35f8488bc423be07a1c2a17b94f10786675 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Wed, 16 Aug 2023 16:54:23 +0200 Subject: [PATCH 060/130] fix setting album and playlist name as queue source --- .../AlbumScreen/album_screen_content.dart | 4 +++- ...bum_screen_content_flexible_space_bar.dart | 22 ++++++++++--------- .../AlbumScreen/song_list_tile.dart | 6 +++-- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/components/AlbumScreen/album_screen_content.dart b/lib/components/AlbumScreen/album_screen_content.dart index fd96755df..7e155128b 100644 --- a/lib/components/AlbumScreen/album_screen_content.dart +++ b/lib/components/AlbumScreen/album_screen_content.dart @@ -69,7 +69,8 @@ class _AlbumScreenContentState extends State { expandedHeight: kToolbarHeight + 125 + 64, pinned: true, flexibleSpace: AlbumScreenContentFlexibleSpaceBar( - album: widget.parent, + parentItem: widget.parent, + isPlaylist: widget.parent.type == "Playlist", items: widget.children, ), actions: [ @@ -172,6 +173,7 @@ class _SongsSliverListState extends State { children: widget.childrenForQueue, index: index + indexOffset, parentId: widget.parent.id, + parentName: widget.parent.name, onDelete: () { final item = removeItem(); if (widget.onDelete != null) { diff --git a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart index 0bc70baca..6952955c3 100644 --- a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart +++ b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart @@ -14,11 +14,13 @@ import 'item_info.dart'; class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { const AlbumScreenContentFlexibleSpaceBar({ Key? key, - required this.album, + required this.parentItem, + required this.isPlaylist, required this.items, }) : super(key: key); - final BaseItemDto album; + final BaseItemDto parentItem; + final bool isPlaylist; final List items; @override @@ -33,9 +35,9 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { queueService.startPlayback( items: items, source: QueueItemSource( - type: QueueItemSourceType.album, - name: album.name ?? "Somewhere", - id: album.id, + type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, + name: parentItem.name ?? "Somewhere", + id: parentItem.id, ) ); } @@ -45,9 +47,9 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { queueService.startPlayback( items: items, source: QueueItemSource( - type: QueueItemSourceType.album, - name: album.name ?? "Somewhere", - id: album.id, + type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, + name: parentItem.name ?? "Somewhere", + id: parentItem.id, ) ); } @@ -65,7 +67,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { children: [ SizedBox( height: 125, - child: AlbumImage(item: album), + child: AlbumImage(item: parentItem), ), const Padding( padding: EdgeInsets.symmetric(horizontal: 4), @@ -73,7 +75,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { Expanded( flex: 2, child: ItemInfo( - item: album, + item: parentItem, itemSongs: items.length, ), ) diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index 490520b6b..382240c8c 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -49,6 +49,7 @@ class SongListTile extends StatefulWidget { /// song in an album. this.index, this.parentId, + this.parentName, this.isSong = false, this.showArtists = true, this.onDelete, @@ -63,6 +64,7 @@ class SongListTile extends StatefulWidget { final int? index; final bool isSong; final String? parentId; + final String? parentName; final bool showArtists; final VoidCallback? onDelete; final bool isInPlaylist; @@ -190,8 +192,8 @@ class _SongListTileState extends State { _queueService.startPlayback( items: widget.children!, source: QueueItemSource( - type: QueueItemSourceType.album, - name: widget.item.album ?? "Somewhere", + type: widget.isInPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, + name: (widget.isInPlaylist ? widget.parentName : widget.item.album) ?? "Somewhere", id: widget.parentId ?? "", ), startingIndex: widget.index ?? 0, From 4e9e566ec4bc12d2f3c2ccaecec0c00418a58a8a Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 22 Aug 2023 14:15:42 +0200 Subject: [PATCH 061/130] use long press & swipe instead of menu and remove buttons --- lib/components/PlayerScreen/queue_list.dart | 16 +- .../PlayerScreen/queue_list_item.dart | 278 +++++++++--------- 2 files changed, 159 insertions(+), 135 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 329a53ed6..96c4f3ab1 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -154,7 +154,12 @@ class _QueueListState extends State { , SliverToBoxAdapter( child: GestureDetector( - onTap:() => setState(() => isRecentTracksExpanded = !isRecentTracksExpanded), + onTap:() { + setState(() => isRecentTracksExpanded = !isRecentTracksExpanded); + if (!isRecentTracksExpanded) { + Future.delayed(const Duration(milliseconds: 200), () => scrollToCurrentTrack()); + } + }, child: Padding( padding: const EdgeInsets.only(left: 14.0, right: 14.0, bottom: 12.0, top: 8.0), child: Row( @@ -351,6 +356,9 @@ class _PreviousTracksListState extends State }); } }, + onReorderStart: (p0) { + Feedback.forLongPress(context); + }, itemCount: _previousTracks?.length ?? 0, itemBuilder: (context, index) { final item = _previousTracks![index]; @@ -422,6 +430,9 @@ class _NextUpTracksListState extends State { }); } }, + onReorderStart: (p0) { + Feedback.forLongPress(context); + }, itemCount: _nextUp?.length ?? 0, itemBuilder: (context, index) { final item = _nextUp![index]; @@ -491,6 +502,9 @@ class _QueueTracksListState extends State { }); } }, + onReorderStart: (p0) { + Feedback.forLongPress(context); + }, itemCount: _queue?.length ?? 0, itemBuilder: (context, index) { final item = _queue![index]; diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index 58c0bcf68..fec6c6940 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -50,133 +50,155 @@ class _QueueListItemState extends State { @override Widget build(BuildContext context) { - return Card( - color: const Color.fromRGBO(255, 255, 255, 0.05), - elevation: 0, - margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 5.0), - clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - child: ListTile( - visualDensity: VisualDensity.compact, - minVerticalPadding: 0.0, - contentPadding: - const EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0), - tileColor: widget.isCurrentTrack - ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) - : null, - leading: AlbumImage( - item: widget.item.item.extras?["itemJson"] == null - ? null - : jellyfin_models.BaseItemDto.fromJson( - widget.item.item.extras?["itemJson"]), - ), - title: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Text( - widget.item.item.title ?? - AppLocalizations.of(context)!.unknownName, - style: this.widget.isCurrentTrack - ? TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontSize: 16, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w400, - overflow: TextOverflow.ellipsis) - : null, - overflow: TextOverflow.ellipsis, - ), - ), - subtitle: Text( - processArtist(widget.item.item.artist, context), - style: const TextStyle( - color: Colors.white70, - fontSize: 13, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w300, - overflow: TextOverflow.ellipsis), - overflow: TextOverflow.ellipsis, - ), - trailing: Container( - alignment: Alignment.centerRight, - margin: const EdgeInsets.only(right: 8.0), - width: widget.allowReorder ? 145.0 : 115.0, - height: 50.0, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "${widget.item.item.duration?.inMinutes.toString()}:${((widget.item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - textAlign: TextAlign.end, - style: TextStyle( - color: Theme.of(context).textTheme.bodySmall?.color, - ), + return Dismissible( + key: Key(widget.item.id), + onDismissed: (direction) async { + await _queueService.removeAtOffset(widget.indexOffset); + }, + child: GestureDetector( + onLongPressStart: (details) => showSongMenu(details), + child: Card( + color: const Color.fromRGBO(255, 255, 255, 0.05), + elevation: 0, + margin: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 5.0), + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + child: ListTile( + visualDensity: VisualDensity.compact, + minVerticalPadding: 0.0, + contentPadding: + const EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0), + tileColor: widget.isCurrentTrack + ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) + : null, + leading: AlbumImage( + item: widget.item.item.extras?["itemJson"] == null + ? null + : jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]), ), - IconButton( - padding: const EdgeInsets.all(0.0), - visualDensity: VisualDensity.compact, - icon: const Icon( - TablerIcons.dots_vertical, - color: Colors.white, - weight: 1.5, + title: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text( + widget.item.item.title ?? + AppLocalizations.of(context)!.unknownName, + style: this.widget.isCurrentTrack + ? TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontSize: 16, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w400, + overflow: TextOverflow.ellipsis) + : null, + overflow: TextOverflow.ellipsis, ), - iconSize: 24.0, - onPressed: () => showSongMenu(), ), - IconButton( - padding: const EdgeInsets.only(right: 14.0), - visualDensity: VisualDensity.compact, - icon: const Icon( - TablerIcons.x, - color: Colors.white, - weight: 1.5, - ), - iconSize: 24.0, - onPressed: () async => - await _queueService.removeAtOffset(widget.indexOffset), + subtitle: Text( + processArtist(widget.item.item.artist, context), + style: const TextStyle( + color: Colors.white70, + fontSize: 13, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w300, + overflow: TextOverflow.ellipsis), + overflow: TextOverflow.ellipsis, ), - if (widget.allowReorder) - ReorderableDragStartListener( - index: widget.listIndex, - child: const Icon( - TablerIcons.grip_horizontal, - color: Colors.white, - size: 28.0, - weight: 1.5, - ), + trailing: Container( + alignment: Alignment.centerRight, + margin: const EdgeInsets.only(right: 8.0), + padding: const EdgeInsets.only(right: 6.0), + // width: widget.allowReorder ? 145.0 : 115.0, + width: widget.allowReorder ? 68.0 : 35.0, + height: 50.0, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "${widget.item.item.duration?.inMinutes.toString()}:${((widget.item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + textAlign: TextAlign.end, + style: TextStyle( + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + // IconButton( + // padding: const EdgeInsets.all(0.0), + // visualDensity: VisualDensity.compact, + // icon: const Icon( + // TablerIcons.dots_vertical, + // color: Colors.white, + // weight: 1.5, + // ), + // iconSize: 24.0, + // onPressed: () => showSongMenu(), + // ), + // IconButton( + // padding: const EdgeInsets.only(right: 14.0), + // visualDensity: VisualDensity.compact, + // icon: const Icon( + // TablerIcons.x, + // color: Colors.white, + // weight: 1.5, + // ), + // iconSize: 24.0, + // onPressed: () async => + // await _queueService.removeAtOffset(widget.indexOffset), + // ), + if (widget.allowReorder) + ReorderableDragStartListener( + index: widget.listIndex, + child: Padding( + padding: EdgeInsets.only(bottom: 5.0, left: 6.0), + child: const Icon( + TablerIcons.grip_horizontal, + color: Colors.white, + size: 28.0, + weight: 1.5, + ), + ), + ), + ], ), - ], - ), - ), - onTap: widget.onTap, - )); + ), + onTap: widget.onTap, + ))), + ); } - void showSongMenu() async { + void showSongMenu(LongPressStartDetails? details) async { final canGoToAlbum = _isAlbumDownloadedIfOffline( jellyfin_models.BaseItemDto.fromJson( widget.item.item.extras?["itemJson"]) .parentId); // Some options are disabled in offline mode - final isOffline = - FinampSettingsHelper.finampSettings.isOffline; + final isOffline = FinampSettingsHelper.finampSettings.isOffline; + + final screenSize = MediaQuery.of(context).size; + + Feedback.forLongPress(context); final selection = await showMenu( context: context, - position: RelativeRect.fromLTRB( - MediaQuery.of(context).size.width - 50.0, - MediaQuery.of(context).size.height - 50.0, - 0.0, - 0.0), + position: details != null + ? RelativeRect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + screenSize.width - details.globalPosition.dx, + screenSize.height - details.globalPosition.dy, + ) + : RelativeRect.fromLTRB(MediaQuery.of(context).size.width - 50.0, + MediaQuery.of(context).size.height - 50.0, 0.0, 0.0), items: [ PopupMenuItem( value: SongListTileMenuItems.addToQueue, child: ListTile( leading: const Icon(Icons.queue_music), - title: - Text(AppLocalizations.of(context)!.addToQueue), + title: Text(AppLocalizations.of(context)!.addToQueue), ), ), PopupMenuItem( @@ -198,8 +220,7 @@ class _QueueListItemState extends State { value: SongListTileMenuItems.addToPlaylist, child: ListTile( leading: const Icon(Icons.playlist_add), - title: Text(AppLocalizations.of(context)! - .addToPlaylistTitle), + title: Text(AppLocalizations.of(context)!.addToPlaylistTitle), enabled: !isOffline, ), ), @@ -208,8 +229,7 @@ class _QueueListItemState extends State { value: SongListTileMenuItems.instantMix, child: ListTile( leading: const Icon(Icons.explore), - title: - Text(AppLocalizations.of(context)!.instantMix), + title: Text(AppLocalizations.of(context)!.instantMix), enabled: !isOffline, ), ), @@ -218,8 +238,7 @@ class _QueueListItemState extends State { value: SongListTileMenuItems.goToAlbum, child: ListTile( leading: const Icon(Icons.album), - title: - Text(AppLocalizations.of(context)!.goToAlbum), + title: Text(AppLocalizations.of(context)!.goToAlbum), enabled: canGoToAlbum, ), ), @@ -231,16 +250,14 @@ class _QueueListItemState extends State { value: SongListTileMenuItems.removeFavourite, child: ListTile( leading: const Icon(Icons.favorite_border), - title: Text(AppLocalizations.of(context)! - .removeFavourite), + title: Text(AppLocalizations.of(context)!.removeFavourite), ), ) : PopupMenuItem( value: SongListTileMenuItems.addFavourite, child: ListTile( leading: const Icon(Icons.favorite), - title: Text(AppLocalizations.of(context)! - .addFavourite), + title: Text(AppLocalizations.of(context)!.addFavourite), ), ), ], @@ -262,16 +279,14 @@ class _QueueListItemState extends State { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: - Text(AppLocalizations.of(context)!.addedToQueue), + content: Text(AppLocalizations.of(context)!.addedToQueue), )); break; case SongListTileMenuItems.playNext: // await _audioServiceHelper.addQueueItem(jellyfin_models.BaseItemDto.fromJson(widget.item.item.extras?["itemJson"])); - await _queueService.addNext( - jellyfin_models.BaseItemDto.fromJson( - widget.item.item.extras?["itemJson"])); + await _queueService.addNext(jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"])); if (!mounted) return; @@ -282,9 +297,8 @@ class _QueueListItemState extends State { case SongListTileMenuItems.addToNextUp: // await _audioServiceHelper.addQueueItem(jellyfin_models.BaseItemDto.fromJson(widget.item.item.extras?["itemJson"])); - await _queueService.addToNextUp( - jellyfin_models.BaseItemDto.fromJson( - widget.item.item.extras?["itemJson"])); + await _queueService.addToNextUp(jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"])); if (!mounted) return; @@ -294,8 +308,7 @@ class _QueueListItemState extends State { break; case SongListTileMenuItems.addToPlaylist: - Navigator.of(context).pushNamed( - AddToPlaylistScreen.routeName, + Navigator.of(context).pushNamed(AddToPlaylistScreen.routeName, arguments: jellyfin_models.BaseItemDto.fromJson( widget.item.item.extras?["itemJson"]) .id); @@ -309,24 +322,21 @@ class _QueueListItemState extends State { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - AppLocalizations.of(context)!.startingInstantMix), + content: Text(AppLocalizations.of(context)!.startingInstantMix), )); break; case SongListTileMenuItems.goToAlbum: late jellyfin_models.BaseItemDto album; if (FinampSettingsHelper.finampSettings.isOffline) { // If offline, load the album's BaseItemDto from DownloadHelper. - final downloadsHelper = - GetIt.instance(); + final downloadsHelper = GetIt.instance(); // downloadedParent won't be null here since the menu item already // checks if the DownloadedParent exists. album = downloadsHelper - .getDownloadedParent( - jellyfin_models.BaseItemDto.fromJson( - widget.item.item.extras?["itemJson"]) - .parentId!)! + .getDownloadedParent(jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]) + .parentId!)! .item; } else { // If online, get the album's BaseItemDto from the server. From 835e6f360d58e1e2838e23c91c58af248ea1a2b4 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 22 Aug 2023 14:36:08 +0200 Subject: [PATCH 062/130] round top corners of bottom sheet --- lib/components/PlayerScreen/queue_list.dart | 1 + lib/components/PlayerScreen/queue_list_item.dart | 3 +++ 2 files changed, 4 insertions(+) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 96c4f3ab1..589ae8c77 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -248,6 +248,7 @@ Future showQueueBottomSheet(BuildContext context) { shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24.0)), ), + clipBehavior: Clip.antiAlias, context: context, builder: (context) { return DraggableScrollableSheet( diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index fec6c6940..4d378d9ca 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -54,6 +54,9 @@ class _QueueListItemState extends State { key: Key(widget.item.id), onDismissed: (direction) async { await _queueService.removeAtOffset(widget.indexOffset); + setState(() { + widget.subqueue.removeAt(widget.listIndex); + }); }, child: GestureDetector( onLongPressStart: (details) => showSongMenu(details), From 77b482785bcba5298359c14237543517dcedc352 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 22 Aug 2023 14:52:28 +0200 Subject: [PATCH 063/130] fix dismissing and wrong index offsets --- lib/components/PlayerScreen/queue_list.dart | 2 +- lib/components/PlayerScreen/queue_list_item.dart | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 589ae8c77..5e02a4ea0 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -510,7 +510,7 @@ class _QueueTracksListState extends State { itemBuilder: (context, index) { final item = _queue![index]; final actualIndex = index; - final indexOffset = index + 1; + final indexOffset = index + _nextUp!.length + 1; return QueueListItem( key: ValueKey(_queue![actualIndex].id), item: item, diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index 4d378d9ca..2ebbf3236 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -52,10 +52,10 @@ class _QueueListItemState extends State { Widget build(BuildContext context) { return Dismissible( key: Key(widget.item.id), - onDismissed: (direction) async { - await _queueService.removeAtOffset(widget.indexOffset); - setState(() { - widget.subqueue.removeAt(widget.listIndex); + onDismissed: (direction) { + setState(() async { + await _queueService.removeAtOffset(widget.indexOffset); + // widget.subqueue.removeAt(widget.listIndex); }); }, child: GestureDetector( @@ -113,8 +113,8 @@ class _QueueListItemState extends State { alignment: Alignment.centerRight, margin: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 6.0), - // width: widget.allowReorder ? 145.0 : 115.0, - width: widget.allowReorder ? 68.0 : 35.0, + width: widget.allowReorder ? 145.0 : 115.0, + // width: widget.allowReorder ? 68.0 : 35.0, height: 50.0, child: Row( mainAxisSize: MainAxisSize.min, From 1e08f1b79675d8137377cad5ffe5078ddefdde9f Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 17 Sep 2023 12:47:49 +0200 Subject: [PATCH 064/130] navigate to queue source when tapping source string - doesn't work for all sources yet - artist/album mixes will navigate to the first artists/album --- ...bum_screen_content_flexible_space_bar.dart | 2 + .../AlbumScreen/song_list_tile.dart | 1 + lib/components/MusicScreen/album_item.dart | 4 +- .../MusicScreen/album_item_list_tile.dart | 2 +- .../MusicScreen/artist_item_list_tile.dart | 6 +- .../player_screen_appbar_title.dart | 97 +++++++++++++++---- lib/models/finamp_models.dart | 4 + lib/screens/music_screen.dart | 16 ++- lib/services/audio_service_helper.dart | 21 ++-- lib/services/jellyfin_api_helper.dart | 24 +++-- 10 files changed, 133 insertions(+), 44 deletions(-) diff --git a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart index 6952955c3..3bf2c80b9 100644 --- a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart +++ b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart @@ -38,6 +38,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, name: parentItem.name ?? "Somewhere", id: parentItem.id, + item: parentItem, ) ); } @@ -50,6 +51,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, name: parentItem.name ?? "Somewhere", id: parentItem.id, + item: parentItem, ) ); } diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index 382240c8c..c064eaa6d 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -195,6 +195,7 @@ class _SongListTileState extends State { type: widget.isInPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, name: (widget.isInPlaylist ? widget.parentName : widget.item.album) ?? "Somewhere", id: widget.parentId ?? "", + item: widget.item, ), startingIndex: widget.index ?? 0, ); diff --git a/lib/components/MusicScreen/album_item.dart b/lib/components/MusicScreen/album_item.dart index b98843017..70a70ac27 100644 --- a/lib/components/MusicScreen/album_item.dart +++ b/lib/components/MusicScreen/album_item.dart @@ -126,7 +126,7 @@ class _AlbumItemState extends State { title: Text(AppLocalizations.of(context)!.addFavourite), ), ), - jellyfinApiHelper.selectedMixAlbumIds.contains(mutableAlbum.id) + jellyfinApiHelper.selectedMixAlbums.contains(mutableAlbum.id) ? PopupMenuItem<_AlbumListTileMenuItems>( value: _AlbumListTileMenuItems.removeFromMixList, child: ListTile( @@ -191,7 +191,7 @@ class _AlbumItemState extends State { break; case _AlbumListTileMenuItems.removeFromMixList: try { - jellyfinApiHelper.removeAlbumFromBuilderList(mutableAlbum); + jellyfinApiHelper.removeAlbumFromMixBuilderList(mutableAlbum); setState(() {}); } catch (e) { errorSnackbar(e, context); diff --git a/lib/components/MusicScreen/album_item_list_tile.dart b/lib/components/MusicScreen/album_item_list_tile.dart index 68fe579ac..304a6db3c 100644 --- a/lib/components/MusicScreen/album_item_list_tile.dart +++ b/lib/components/MusicScreen/album_item_list_tile.dart @@ -36,7 +36,7 @@ class AlbumItemListTile extends StatelessWidget { overflow: TextOverflow.ellipsis, ), subtitle: subtitle == null ? null : Text(subtitle), - trailing: jellyfinApiHelper.selectedMixAlbumIds.contains(item.id) + trailing: jellyfinApiHelper.selectedMixAlbums.contains(item.id) ? const Icon(Icons.explore) : null, ); diff --git a/lib/components/MusicScreen/artist_item_list_tile.dart b/lib/components/MusicScreen/artist_item_list_tile.dart index 6acf943a1..6bb2fa230 100644 --- a/lib/components/MusicScreen/artist_item_list_tile.dart +++ b/lib/components/MusicScreen/artist_item_list_tile.dart @@ -51,7 +51,7 @@ class _ArtistListTileState extends State { ), subtitle: null, trailing: - _jellyfinApiHelper.selectedMixArtistsIds.contains(mutableItem.id) + _jellyfinApiHelper.selectedMixArtists.contains(mutableItem.id) ? const Icon(Icons.explore) : null, ); @@ -86,7 +86,7 @@ class _ArtistListTileState extends State { title: Text("Add Favourite"), ), ), - _jellyfinApiHelper.selectedMixArtistsIds.contains(mutableItem.id) + _jellyfinApiHelper.selectedMixArtists.contains(mutableItem.id) ? PopupMenuItem( enabled: !isOffline, value: ArtistListTileMenuItems.removeFromMixList, @@ -153,7 +153,7 @@ class _ArtistListTileState extends State { break; case ArtistListTileMenuItems.removeFromMixList: try { - _jellyfinApiHelper.removeArtistFromBuilderList(mutableItem); + _jellyfinApiHelper.removeArtistFromMixBuilderList(mutableItem); setState(() {}); } catch (e) { errorSnackbar(e, context); diff --git a/lib/components/PlayerScreen/player_screen_appbar_title.dart b/lib/components/PlayerScreen/player_screen_appbar_title.dart index 29c31c6e6..705e59f96 100644 --- a/lib/components/PlayerScreen/player_screen_appbar_title.dart +++ b/lib/components/PlayerScreen/player_screen_appbar_title.dart @@ -1,3 +1,7 @@ +import 'package:finamp/screens/album_screen.dart'; +import 'package:finamp/screens/artist_screen.dart'; +import 'package:finamp/screens/music_screen.dart'; +import 'package:finamp/services/finamp_settings_helper.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -35,28 +39,87 @@ class _PlayerScreenAppBarTitleState extends State { return Baseline( baselineType: TextBaseline.alphabetic, baseline: 0, - child: Column( - children: [ - Text( - "Playing From ${queueItem.source.type.name}", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w300, - color: Colors.white.withOpacity(0.7), + child: GestureDetector( + onTap: () => navigateToSource(context, queueItem.source), + child: Column( + children: [ + Text( + "Playing From ${queueItem.source.type.name}", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w300, + color: Colors.white.withOpacity(0.7), + ), ), - ), - const Padding(padding: EdgeInsets.symmetric(vertical: 2)), - Text( - queueItem.source.name, - style: const TextStyle( - fontSize: 16, - color: Colors.white, + const Padding(padding: EdgeInsets.symmetric(vertical: 2)), + Text( + queueItem.source.name, + style: const TextStyle( + fontSize: 16, + color: Colors.white, + ), ), - ), - ], + ], + ), ), ); }, ); } } + +void navigateToSource(BuildContext context, QueueItemSource source) async { + + switch (source.type) { + case QueueItemSourceType.album: + Navigator.of(context).pushNamed(AlbumScreen.routeName, arguments: source.item); + break; + case QueueItemSourceType.artist: + Navigator.of(context).pushNamed(ArtistScreen.routeName, arguments: source.item); + break; + case QueueItemSourceType.genre: + Navigator.of(context).pushNamed(ArtistScreen.routeName, arguments: source.item); + break; + case QueueItemSourceType.playlist: + Navigator.of(context).pushNamed(AlbumScreen.routeName, arguments: source.item); + break; + case QueueItemSourceType.albumMix: + Navigator.of(context).pushNamed(AlbumScreen.routeName, arguments: source.item); + break; + case QueueItemSourceType.artistMix: + Navigator.of(context).pushNamed(ArtistScreen.routeName, arguments: source.item); + break; + case QueueItemSourceType.songs: + Navigator.of(context).pushNamed(MusicScreen.routeName, arguments: FinampSettingsHelper.finampSettings.showTabs.entries + .where((element) => element.value == true) + .map((e) => e.key) + .toList().indexOf(TabContentType.songs) + ); + break; + case QueueItemSourceType.nextUp: + break; + case QueueItemSourceType.formerNextUp: + break; + case QueueItemSourceType.unknown: + break; + case QueueItemSourceType.favorites: + case QueueItemSourceType.itemMix: + case QueueItemSourceType.filteredList: + case QueueItemSourceType.downloads: + default: + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Not implemented yet."), + // action: SnackBarAction( + // label: "OPEN", + // onPressed: () { + // Navigator.of(context).pushNamed( + // "/music/albumscreen", + // arguments: snapshot.data![index]); + // }, + // ), + ), + ); + } + +} diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index d5577cd1f..7c5d86020 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -594,6 +594,7 @@ class QueueItemSource { required this.type, required this.name, required this.id, + this.item, }); @HiveField(0) @@ -605,6 +606,9 @@ class QueueItemSource { @HiveField(2) String id; + @HiveField(3) + BaseItemDto? item; + } class QueueItem { diff --git a/lib/screens/music_screen.dart b/lib/screens/music_screen.dart index 1c6629792..4312f50a7 100644 --- a/lib/screens/music_screen.dart +++ b/lib/screens/music_screen.dart @@ -78,6 +78,7 @@ class _MusicScreenState extends State .where((element) => element.value) .length, vsync: this, + initialIndex: ModalRoute.of(context)?.settings.arguments as int? ?? 0, ); _tabController!.addListener(_tabIndexCallback); @@ -86,7 +87,6 @@ class _MusicScreenState extends State @override void initState() { super.initState(); - _buildTabController(); } @override @@ -121,13 +121,14 @@ class _MusicScreenState extends State tooltip: AppLocalizations.of(context)!.startMix, onPressed: () async { try { - if (_jellyfinApiHelper.selectedMixArtistsIds.isEmpty) { + if (_jellyfinApiHelper.selectedMixArtists.isEmpty) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( AppLocalizations.of(context)!.startMixNoSongsArtist))); } else { await _audioServiceHelper.startInstantMixForArtists( - _jellyfinApiHelper.selectedMixArtistsIds); + _jellyfinApiHelper.selectedMixArtists); + _jellyfinApiHelper.clearArtistMixBuilderList(); } } catch (e) { errorSnackbar(e, context); @@ -140,13 +141,13 @@ class _MusicScreenState extends State tooltip: AppLocalizations.of(context)!.startMix, onPressed: () async { try { - if (_jellyfinApiHelper.selectedMixAlbumIds.isEmpty) { + if (_jellyfinApiHelper.selectedMixAlbums.isEmpty) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( AppLocalizations.of(context)!.startMixNoSongsAlbum))); } else { await _audioServiceHelper.startInstantMixForAlbums( - _jellyfinApiHelper.selectedMixAlbumIds); + _jellyfinApiHelper.selectedMixAlbums); } } catch (e) { errorSnackbar(e, context); @@ -160,6 +161,11 @@ class _MusicScreenState extends State @override Widget build(BuildContext context) { + + if (_tabController == null) { + _buildTabController(); + } + return ValueListenableBuilder>( valueListenable: _finampUserHelper.finampUsersListenable, builder: (context, value, _) { diff --git a/lib/services/audio_service_helper.dart b/lib/services/audio_service_helper.dart index ecafee8e9..ca731beab 100644 --- a/lib/services/audio_service_helper.dart +++ b/lib/services/audio_service_helper.dart @@ -1,6 +1,7 @@ import 'dart:collection'; import 'package:audio_service/audio_service.dart'; +import 'package:finamp/models/jellyfin_models.dart'; import 'package:get_it/get_it.dart'; import 'package:logging/logging.dart'; import 'package:uuid/uuid.dart'; @@ -152,21 +153,23 @@ class AudioServiceHelper { } /// Start instant mix from a selection of artists. - Future startInstantMixForArtists(List artistIds) async { + Future startInstantMixForArtists(List artists) async { List? items; try { - items = await _jellyfinApiHelper.getArtistMix(artistIds); + items = await _jellyfinApiHelper.getArtistMix(artists.map((e) => e.id).toList()); if (items != null) { // await replaceQueueWithItem(itemList: items, shuffle: false); await _queueService.startPlayback( items: items, source: QueueItemSource( type: QueueItemSourceType.artistMix, - name: artistIds.first, - id: artistIds.first, + name: artists.map((e) => e.name).join(" & "), + id: artists.first.id, + item: artists.first, ) ); + _jellyfinApiHelper.clearArtistMixBuilderList(); } } catch (e) { audioServiceHelperLogger.severe(e); @@ -175,21 +178,23 @@ class AudioServiceHelper { } /// Start instant mix from a selection of albums. - Future startInstantMixForAlbums(List albumIds) async { + Future startInstantMixForAlbums(List albums) async { List? items; try { - items = await _jellyfinApiHelper.getAlbumMix(albumIds); + items = await _jellyfinApiHelper.getAlbumMix(albums.map((e) => e.id).toList()); if (items != null) { // await replaceQueueWithItem(itemList: items, shuffle: false); await _queueService.startPlayback( items: items, source: QueueItemSource( type: QueueItemSourceType.albumMix, - name: albumIds.first, - id: albumIds.first, + name: albums.map((e) => e.name).join(" & "), + id: albums.first.id, + item: albums.first, ) ); + _jellyfinApiHelper.clearAlbumMixBuilderList(); } } catch (e) { audioServiceHelperLogger.severe(e); diff --git a/lib/services/jellyfin_api_helper.dart b/lib/services/jellyfin_api_helper.dart index e55504c99..d2a774b00 100644 --- a/lib/services/jellyfin_api_helper.dart +++ b/lib/services/jellyfin_api_helper.dart @@ -12,10 +12,10 @@ class JellyfinApiHelper { final _jellyfinApiHelperLogger = Logger("JellyfinApiHelper"); // Stores the ids of the artists that the user selected to mix - List selectedMixArtistsIds = []; + List selectedMixArtists = []; // Stores the ids of albums that the user selected to mix - List selectedMixAlbumIds = []; + List selectedMixAlbums = []; Uri? baseUrlTemp; @@ -349,19 +349,27 @@ class JellyfinApiHelper { } void addArtistToMixBuilderList(BaseItemDto item) { - selectedMixArtistsIds.add(item.id); + selectedMixArtists.add(item); } - void removeArtistFromBuilderList(BaseItemDto item) { - selectedMixArtistsIds.remove(item.id); + void removeArtistFromMixBuilderList(BaseItemDto item) { + selectedMixArtists.remove(item); + } + + void clearArtistMixBuilderList() { + selectedMixArtists.clear(); } void addAlbumToMixBuilderList(BaseItemDto item) { - selectedMixAlbumIds.add(item.id); + selectedMixAlbums.add(item); + } + + void removeAlbumFromMixBuilderList(BaseItemDto item) { + selectedMixAlbums.remove(item); } - void removeAlbumFromBuilderList(BaseItemDto item) { - selectedMixAlbumIds.remove(item.id); + void clearAlbumMixBuilderList() { + selectedMixAlbums.clear(); } Future?> getArtistMix(List artistIds) async { From 821606e9b55392d023f46cf935c28bf6f8118903 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Mon, 18 Sep 2023 14:11:32 +0200 Subject: [PATCH 065/130] ability to play albums/playlists next / add to next up --- .../add_to_playlist_list.dart | 1 + .../AlbumScreen/album_screen_content.dart | 4 +- ...bum_screen_content_flexible_space_bar.dart | 92 ++++++++++++---- .../AlbumScreen/song_list_tile.dart | 17 +-- lib/components/MusicScreen/album_item.dart | 102 ++++++++++++++++++ .../MusicScreen/music_screen_tab_view.dart | 4 + lib/components/PlayerScreen/queue_list.dart | 4 +- .../PlayerScreen/queue_list_item.dart | 8 +- lib/services/queue_service.dart | 54 ++++++---- 9 files changed, 229 insertions(+), 57 deletions(-) diff --git a/lib/components/AddToPlaylistScreen/add_to_playlist_list.dart b/lib/components/AddToPlaylistScreen/add_to_playlist_list.dart index 242669405..38a9df820 100644 --- a/lib/components/AddToPlaylistScreen/add_to_playlist_list.dart +++ b/lib/components/AddToPlaylistScreen/add_to_playlist_list.dart @@ -45,6 +45,7 @@ class _AddToPlaylistListState extends State { return AlbumItem( album: snapshot.data![index], parentType: snapshot.data![index].type, + isPlaylist: true, onTap: () async { try { await jellyfinApiHelper.addItemstoPlaylist( diff --git a/lib/components/AlbumScreen/album_screen_content.dart b/lib/components/AlbumScreen/album_screen_content.dart index 7e155128b..880afa9bd 100644 --- a/lib/components/AlbumScreen/album_screen_content.dart +++ b/lib/components/AlbumScreen/album_screen_content.dart @@ -62,11 +62,11 @@ class _AlbumScreenContentState extends State { SliverAppBar( title: Text(widget.parent.name ?? AppLocalizations.of(context)!.unknownName), - // 125 + 64 is the total height of the widget we use as a + // 125 + 116 is the total height of the widget we use as a // FlexibleSpaceBar. We add the toolbar height since the widget // should appear below the appbar. // TODO: This height is affected by platform density. - expandedHeight: kToolbarHeight + 125 + 64, + expandedHeight: kToolbarHeight + 125 + 116, pinned: true, flexibleSpace: AlbumScreenContentFlexibleSpaceBar( parentItem: widget.parent, diff --git a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart index 3bf2c80b9..77dedf9f8 100644 --- a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart +++ b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart @@ -30,7 +30,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { QueueService queueService = GetIt.instance(); - void _playAlbum() { + void playAlbum() { queueService.playbackOrder = PlaybackOrder.linear; queueService.startPlayback( items: items, @@ -43,7 +43,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { ); } - void _shuffleAlbum() { + void shuffleAlbum() { queueService.playbackOrder = PlaybackOrder.shuffled; queueService.startPlayback( items: items, @@ -56,6 +56,32 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { ); } + void addAlbumToNextUp() { + queueService.playbackOrder = PlaybackOrder.linear; + queueService.addToNextUp( + items: items, + source: QueueItemSource( + type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, + name: parentItem.name ?? "Somewhere", + id: parentItem.id, + item: parentItem, + ) + ); + } + + void addAlbumNext() { + queueService.playbackOrder = PlaybackOrder.linear; + queueService.addNext( + items: items, + source: QueueItemSource( + type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, + name: parentItem.name ?? "Somewhere", + id: parentItem.id, + item: parentItem, + ) + ); + } + return FlexibleSpaceBar( background: SafeArea( child: Align( @@ -85,25 +111,47 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { ), Padding( padding: const EdgeInsets.symmetric(vertical: 8), - child: Row(children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: () => _playAlbum(), - icon: const Icon(Icons.play_arrow), - label: - Text(AppLocalizations.of(context)!.playButtonLabel), - ), - ), - const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), - Expanded( - child: ElevatedButton.icon( - onPressed: () => _shuffleAlbum(), - icon: const Icon(Icons.shuffle), - label: Text( - AppLocalizations.of(context)!.shuffleButtonLabel), - ), - ), - ]), + child: Column( + children: [ + Row(children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () => playAlbum(), + icon: const Icon(Icons.play_arrow), + label: + Text(AppLocalizations.of(context)!.playButtonLabel), + ), + ), + const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), + Expanded( + child: ElevatedButton.icon( + onPressed: () => shuffleAlbum(), + icon: const Icon(Icons.shuffle), + label: Text( + AppLocalizations.of(context)!.shuffleButtonLabel), + ), + ), + ]), + Row(children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () => addAlbumNext(), + icon: const Icon(Icons.hourglass_bottom), + label: + Text("Play Next"), + ), + ), + const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), + Expanded( + child: ElevatedButton.icon( + onPressed: () => addAlbumToNextUp(), + icon: const Icon(Icons.hourglass_top), + label: Text("Add to Next Up"), + ), + ), + ]), + ], + ), ) ], ), @@ -111,5 +159,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { ), ), ); + } + } diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index c064eaa6d..4b7b7654c 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -242,13 +242,14 @@ class _SongListTileState extends State { title: Text(AppLocalizations.of(context)!.addToQueue), ), ), - PopupMenuItem( - value: SongListTileMenuItems.playNext, - child: ListTile( - leading: const Icon(TablerIcons.hourglass_low), - title: Text("Play next"), + if (_queueService.getQueue().nextUp.isNotEmpty) + PopupMenuItem( + value: SongListTileMenuItems.playNext, + child: ListTile( + leading: const Icon(TablerIcons.hourglass_low), + title: Text("Play next"), + ), ), - ), PopupMenuItem( value: SongListTileMenuItems.addToNextUp, child: ListTile( @@ -337,7 +338,7 @@ class _SongListTileState extends State { case SongListTileMenuItems.playNext: // await _audioServiceHelper.addQueueItem(widget.item); - await _queueService.addNext(widget.item); + await _queueService.addNext(items: [widget.item]); if (!mounted) return; @@ -348,7 +349,7 @@ class _SongListTileState extends State { case SongListTileMenuItems.addToNextUp: // await _audioServiceHelper.addQueueItem(widget.item); - await _queueService.addToNextUp(widget.item); + await _queueService.addToNextUp(items: [widget.item]); if (!mounted) return; diff --git a/lib/components/MusicScreen/album_item.dart b/lib/components/MusicScreen/album_item.dart index 70a70ac27..a0863ba9c 100644 --- a/lib/components/MusicScreen/album_item.dart +++ b/lib/components/MusicScreen/album_item.dart @@ -1,5 +1,7 @@ import 'package:finamp/components/MusicScreen/album_item_list_tile.dart'; +import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/services/finamp_settings_helper.dart'; +import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; @@ -16,6 +18,8 @@ enum _AlbumListTileMenuItems { removeFavourite, addToMixList, removeFromMixList, + playNext, + addToNextUp, } /// This widget is kind of a shell around AlbumItemCard and AlbumItemListTile. @@ -26,6 +30,7 @@ class AlbumItem extends StatefulWidget { const AlbumItem({ Key? key, required this.album, + required this.isPlaylist, this.parentType, this.onTap, this.isGrid = false, @@ -40,6 +45,9 @@ class AlbumItem extends StatefulWidget { /// like artists. final String? parentType; + /// Used to differentiate between albums and playlists, since they use the same internal logic and widgets + final bool isPlaylist; + /// A custom onTap can be provided to override the default value, which is to /// open the item's album/artist screen. final void Function()? onTap; @@ -60,6 +68,9 @@ class AlbumItem extends StatefulWidget { class _AlbumItemState extends State { late BaseItemDto mutableAlbum; + QueueService get _queueService => + GetIt.instance(); + late Function() onTap; @override @@ -142,6 +153,23 @@ class _AlbumItemState extends State { title: Text(AppLocalizations.of(context)!.addToMix), ), ), + if (_queueService.getQueue().nextUp.isNotEmpty) + PopupMenuItem<_AlbumListTileMenuItems>( + value: _AlbumListTileMenuItems.playNext, + child: ListTile( + leading: const Icon(Icons.hourglass_bottom), + title: + Text("Play Next"), + ), + ), + PopupMenuItem<_AlbumListTileMenuItems>( + value: _AlbumListTileMenuItems.addToNextUp, + child: ListTile( + leading: const Icon(Icons.hourglass_top), + title: + Text("Add to Next Up"), + ), + ), ], ); @@ -197,6 +225,80 @@ class _AlbumItemState extends State { errorSnackbar(e, context); } break; + case _AlbumListTileMenuItems.playNext: + try { + List? albumTracks = await jellyfinApiHelper.getItems( + isGenres: false, + parentItem: mutableAlbum, + ); + + if (albumTracks == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), + ), + ); + return; + } + + _queueService.addNext( + items: albumTracks, + source: QueueItemSource( + type: QueueItemSourceType.album, + name: mutableAlbum.name ?? "Somewhere", + id: mutableAlbum.id, + item: mutableAlbum, + ) + ); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("${widget.isPlaylist ? "Playlist" : "Album"} will play next."), + ), + ); + + setState(() {}); + } catch (e) { + errorSnackbar(e, context); + } + break; + case _AlbumListTileMenuItems.addToNextUp: + try { + List? albumTracks = await jellyfinApiHelper.getItems( + isGenres: false, + parentItem: mutableAlbum, + ); + + if (albumTracks == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), + ), + ); + return; + } + + _queueService.addToNextUp( + items: albumTracks, + source: QueueItemSource( + type: QueueItemSourceType.album, + name: mutableAlbum.name ?? "Somewhere", + id: mutableAlbum.id, + item: mutableAlbum, + ) + ); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Added ${widget.isPlaylist ? "playlist" : "album"} to Next Up."), + ), + ); + + setState(() {}); + } catch (e) { + errorSnackbar(e, context); + } + break; case null: break; } diff --git a/lib/components/MusicScreen/music_screen_tab_view.dart b/lib/components/MusicScreen/music_screen_tab_view.dart index b4ccab862..1c8975c8b 100644 --- a/lib/components/MusicScreen/music_screen_tab_view.dart +++ b/lib/components/MusicScreen/music_screen_tab_view.dart @@ -306,6 +306,7 @@ class _MusicScreenTabViewState extends State return AlbumItem( album: offlineSortedItems![index], parentType: _getParentType(), + isPlaylist: widget.tabContentType == TabContentType.playlists, ); } }, @@ -334,6 +335,7 @@ class _MusicScreenTabViewState extends State return AlbumItem( album: offlineSortedItems![index], parentType: _getParentType(), + isPlaylist: widget.tabContentType == TabContentType.playlists, isGrid: true, gridAddSettingsListener: false, ); @@ -383,6 +385,7 @@ class _MusicScreenTabViewState extends State return AlbumItem( album: item, parentType: _getParentType(), + isPlaylist: widget.tabContentType == TabContentType.playlists, ); } }, @@ -414,6 +417,7 @@ class _MusicScreenTabViewState extends State return AlbumItem( album: item, parentType: _getParentType(), + isPlaylist: widget.tabContentType == TabContentType.playlists, isGrid: true, gridAddSettingsListener: false, ); diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 5e02a4ea0..0a51c94ed 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -913,7 +913,7 @@ class _CurrentTrackState extends State { case SongListTileMenuItems.playNext: // await _audioServiceHelper.addQueueItem(item); - await _queueService.addNext(item); + await _queueService.addNext(items: [item]); if (!mounted) return; @@ -924,7 +924,7 @@ class _CurrentTrackState extends State { case SongListTileMenuItems.addToNextUp: // await _audioServiceHelper.addQueueItem(item); - await _queueService.addToNextUp(item); + await _queueService.addToNextUp(items: [item]); if (!mounted) return; diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index 2ebbf3236..24a46a2c8 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -288,8 +288,8 @@ class _QueueListItemState extends State { case SongListTileMenuItems.playNext: // await _audioServiceHelper.addQueueItem(jellyfin_models.BaseItemDto.fromJson(widget.item.item.extras?["itemJson"])); - await _queueService.addNext(jellyfin_models.BaseItemDto.fromJson( - widget.item.item.extras?["itemJson"])); + await _queueService.addNext(items: [jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"])]); if (!mounted) return; @@ -300,8 +300,8 @@ class _QueueListItemState extends State { case SongListTileMenuItems.addToNextUp: // await _audioServiceHelper.addQueueItem(jellyfin_models.BaseItemDto.fromJson(widget.item.item.extras?["itemJson"])); - await _queueService.addToNextUp(jellyfin_models.BaseItemDto.fromJson( - widget.item.item.extras?["itemJson"])); + await _queueService.addToNextUp(items: [jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"])]); if (!mounted) return; diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 59faa4001..22b841c94 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -270,22 +270,28 @@ class QueueService { } } - Future addNext(jellyfin_models.BaseItemDto item) async { + Future addNext({ + required List items, + QueueItemSource? source, + }) async { try { - QueueItem queueItem = QueueItem( - item: await _generateMediaItem(item), - source: QueueItemSource(id: "next-up", name: "Next Up", type: QueueItemSourceType.nextUp), - type: QueueItemQueueType.nextUp, - ); - - //TODO doesn't work when adding while shuffled and then *disabling* shuffle + List queueItems = []; + for (final item in items) { + queueItems.add(QueueItem( + item: await _generateMediaItem(item), + source: source ?? QueueItemSource(id: "next-up", name: "Next Up", type: QueueItemSourceType.nextUp), + type: QueueItemQueueType.nextUp, + )); + } + // don't add to _order, because it wasn't added to the regular queue // int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; - await _queueAudioSource.insert(_queueAudioSourceIndex+1, await _queueItemToAudioSource(queueItem)); - - _queueServiceLogger.fine("Prepended '${queueItem.item.title}' to Next Up"); + for (final queueItem in queueItems.reversed) { + await _queueAudioSource.insert(_queueAudioSourceIndex+1, await _queueItemToAudioSource(queueItem)); + _queueServiceLogger.fine("Appended '${queueItem.item.title}' to Next Up (index ${_queueAudioSourceIndex+1})"); + } _queueFromConcatenatingAudioSource(); // update internal queues @@ -295,13 +301,19 @@ class QueueService { } } - Future addToNextUp(jellyfin_models.BaseItemDto item) async { + Future addToNextUp({ + required List items, + QueueItemSource? source, + }) async { try { - QueueItem queueItem = QueueItem( - item: await _generateMediaItem(item), - source: QueueItemSource(id: "next-up", name: "Next Up", type: QueueItemSourceType.nextUp), - type: QueueItemQueueType.nextUp, - ); + List queueItems = []; + for (final item in items) { + queueItems.add(QueueItem( + item: await _generateMediaItem(item), + source: source ?? QueueItemSource(id: "next-up", name: "Next Up", type: QueueItemSourceType.nextUp), + type: QueueItemQueueType.nextUp, + )); + } // don't add to _order, because it wasn't added to the regular queue @@ -309,9 +321,11 @@ class QueueService { // int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; int offset = _queueNextUp.length; - await _queueAudioSource.insert(_queueAudioSourceIndex+1+offset, await _queueItemToAudioSource(queueItem)); - - _queueServiceLogger.fine("Appended '${queueItem.item.title}' to Next Up (index ${_queueAudioSourceIndex+1+offset})"); + for (final queueItem in queueItems) { + await _queueAudioSource.insert(_queueAudioSourceIndex+1+offset, await _queueItemToAudioSource(queueItem)); + _queueServiceLogger.fine("Appended '${queueItem.item.title}' to Next Up (index ${_queueAudioSourceIndex+1+offset})"); + offset++; + } _queueFromConcatenatingAudioSource(); // update internal queues From 5b527540830b9378070d68f19cce6975a5f8edb8 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Mon, 18 Sep 2023 14:27:57 +0200 Subject: [PATCH 066/130] shuffle next / to next up --- .../AlbumScreen/album_screen_content.dart | 4 +- ...bum_screen_content_flexible_space_bar.dart | 71 ++++++++++++++ lib/components/MusicScreen/album_item.dart | 95 +++++++++++++++++++ 3 files changed, 168 insertions(+), 2 deletions(-) diff --git a/lib/components/AlbumScreen/album_screen_content.dart b/lib/components/AlbumScreen/album_screen_content.dart index 880afa9bd..21a351b11 100644 --- a/lib/components/AlbumScreen/album_screen_content.dart +++ b/lib/components/AlbumScreen/album_screen_content.dart @@ -62,11 +62,11 @@ class _AlbumScreenContentState extends State { SliverAppBar( title: Text(widget.parent.name ?? AppLocalizations.of(context)!.unknownName), - // 125 + 116 is the total height of the widget we use as a + // 125 + 168 is the total height of the widget we use as a // FlexibleSpaceBar. We add the toolbar height since the widget // should appear below the appbar. // TODO: This height is affected by platform density. - expandedHeight: kToolbarHeight + 125 + 116, + expandedHeight: kToolbarHeight + 125 + 168, pinned: true, flexibleSpace: AlbumScreenContentFlexibleSpaceBar( parentItem: widget.parent, diff --git a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart index 77dedf9f8..da558b5c0 100644 --- a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart +++ b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart @@ -67,6 +67,12 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { item: parentItem, ) ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Added to Next Up"), + ), + ); + } void addAlbumNext() { @@ -80,6 +86,53 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { item: parentItem, ) ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Will play next"), + ), + ); + } + + void shuffleAlbumToNextUp() { + // linear order is used in this case since we don't want to affect the rest of the queue + queueService.playbackOrder = PlaybackOrder.linear; + List clonedItems = List.from(items); + clonedItems.shuffle(); + queueService.addToNextUp( + items: clonedItems, + source: QueueItemSource( + type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, + name: parentItem.name ?? "Somewhere", + id: parentItem.id, + item: parentItem, + ) + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Shuffled to Next Up"), + ), + ); + } + + void shuffleAlbumNext() { + // linear order is used in this case since we don't want to affect the rest of the queue + queueService.playbackOrder = PlaybackOrder.linear; + List clonedItems = List.from(items); + clonedItems.shuffle(); + queueService.addNext( + items: clonedItems, + source: QueueItemSource( + type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, + name: parentItem.name ?? "Somewhere", + id: parentItem.id, + item: parentItem, + ) + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Will shuffle next"), + ), + ); } return FlexibleSpaceBar( @@ -150,6 +203,24 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { ), ), ]), + Row(children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () => shuffleAlbumNext(), + icon: const Icon(Icons.hourglass_bottom), + label: + Text("Shuffle Next"), + ), + ), + const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), + Expanded( + child: ElevatedButton.icon( + onPressed: () => shuffleAlbumToNextUp(), + icon: const Icon(Icons.hourglass_top), + label: Text("Shuffle to Next Up"), + ), + ), + ]), ], ), ) diff --git a/lib/components/MusicScreen/album_item.dart b/lib/components/MusicScreen/album_item.dart index a0863ba9c..97f888eb1 100644 --- a/lib/components/MusicScreen/album_item.dart +++ b/lib/components/MusicScreen/album_item.dart @@ -20,6 +20,8 @@ enum _AlbumListTileMenuItems { removeFromMixList, playNext, addToNextUp, + shuffleNext, + shuffleToNextUp, } /// This widget is kind of a shell around AlbumItemCard and AlbumItemListTile. @@ -170,6 +172,23 @@ class _AlbumItemState extends State { Text("Add to Next Up"), ), ), + if (_queueService.getQueue().nextUp.isNotEmpty) + PopupMenuItem<_AlbumListTileMenuItems>( + value: _AlbumListTileMenuItems.shuffleNext, + child: ListTile( + leading: const Icon(Icons.hourglass_bottom), + title: + Text("Shuffle Next"), + ), + ), + PopupMenuItem<_AlbumListTileMenuItems>( + value: _AlbumListTileMenuItems.shuffleToNextUp, + child: ListTile( + leading: const Icon(Icons.hourglass_top), + title: + Text("Shuffle to Next Up"), + ), + ), ], ); @@ -299,6 +318,82 @@ class _AlbumItemState extends State { errorSnackbar(e, context); } break; + case _AlbumListTileMenuItems.shuffleNext: + try { + List? albumTracks = await jellyfinApiHelper.getItems( + isGenres: false, + parentItem: mutableAlbum, + sortOrder: "Random", + ); + + if (albumTracks == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), + ), + ); + return; + } + + _queueService.addNext( + items: albumTracks, + source: QueueItemSource( + type: QueueItemSourceType.album, + name: mutableAlbum.name ?? "Somewhere", + id: mutableAlbum.id, + item: mutableAlbum, + ) + ); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("${widget.isPlaylist ? "Playlist" : "Album"} will shuffle next."), + ), + ); + + setState(() {}); + } catch (e) { + errorSnackbar(e, context); + } + break; + case _AlbumListTileMenuItems.shuffleToNextUp: + try { + List? albumTracks = await jellyfinApiHelper.getItems( + isGenres: false, + parentItem: mutableAlbum, + sortOrder: "Random", + ); + + if (albumTracks == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), + ), + ); + return; + } + + _queueService.addToNextUp( + items: albumTracks, + source: QueueItemSource( + type: QueueItemSourceType.album, + name: mutableAlbum.name ?? "Somewhere", + id: mutableAlbum.id, + item: mutableAlbum, + ) + ); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Shuffled ${widget.isPlaylist ? "playlist" : "album"} to Next Up."), + ), + ); + + setState(() {}); + } catch (e) { + errorSnackbar(e, context); + } + break; case null: break; } From cc6a05580789cb2cf596787485425f2a26e9c6fd Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Mon, 18 Sep 2023 17:14:13 +0200 Subject: [PATCH 067/130] dynamic colors, design and layout improvements --- lib/components/PlayerScreen/queue_list.dart | 277 +++++++++++------- .../PlayerScreen/queue_list_item.dart | 85 ++++-- .../blurred_player_screen_background.dart | 44 +++ lib/screens/player_screen.dart | 113 +++---- pubspec.lock | 8 +- 5 files changed, 328 insertions(+), 199 deletions(-) create mode 100644 lib/screens/blurred_player_screen_background.dart diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 0a51c94ed..ca054f138 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -4,12 +4,15 @@ import 'package:finamp/components/error_snackbar.dart'; import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/screens/add_to_playlist_screen.dart'; import 'package:finamp/screens/album_screen.dart'; +import 'package:finamp/screens/blurred_player_screen_background.dart'; import 'package:finamp/services/audio_service_helper.dart'; import 'package:finamp/services/downloads_helper.dart'; import 'package:finamp/services/finamp_settings_helper.dart'; import 'package:finamp/services/jellyfin_api_helper.dart'; +import 'package:finamp/services/player_screen_theme_provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get_it/get_it.dart'; import 'package:rxdart/rxdart.dart'; @@ -110,7 +113,9 @@ class _QueueListState extends State { ), title: Text("Unknown song"), subtitle: Text("Unknown artist"), - onTap: () {})), + onTap: () {}), + + ), SliverPersistentHeader( delegate: SectionHeaderDelegate( title: const Text("Queue"), @@ -251,63 +256,88 @@ Future showQueueBottomSheet(BuildContext context) { clipBehavior: Clip.antiAlias, context: context, builder: (context) { - return DraggableScrollableSheet( - snap: false, - snapAnimationDuration: const Duration(milliseconds: 200), - initialChildSize: 0.92, - maxChildSize: 0.92, - expand: false, - builder: (context, scrollController) { - return Scaffold( - body: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 10), - Container( - width: 40, - height: 3.5, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(3.5), - ), - ), - const SizedBox(height: 10), - const Text("Queue", - style: TextStyle( - color: Colors.white, - fontFamily: 'Lexend Deca', - fontSize: 18, - fontWeight: FontWeight.w300)), - const SizedBox(height: 20), - Expanded( - child: QueueList( - scrollController: scrollController, - nextUpHeaderKey: nextUpHeaderKey, - ), - ), - ], + return Consumer( + builder: (BuildContext context, WidgetRef ref, Widget? child) { + + final imageTheme = ref.watch(playerScreenThemeProvider); + + return Theme( + data: ThemeData( + fontFamily: "LexendDeca", + colorScheme: imageTheme, + brightness: Theme.of(context).brightness, + iconTheme: Theme.of(context).iconTheme.copyWith( + color: imageTheme?.primary, + ), ), - //TODO fade this out if the key is visible - floatingActionButton: FloatingActionButton( - onPressed: () => scrollToKey( - key: nextUpHeaderKey, - duration: const Duration(milliseconds: 500)), - backgroundColor: const Color.fromRGBO(188, 136, 86, 0.60), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(16.0))), - child: const Padding( - padding: EdgeInsets.only(bottom: 4.0), - child: Icon( - TablerIcons.focus_2, - size: 28.0, + child: DraggableScrollableSheet( + snap: false, + snapAnimationDuration: const Duration(milliseconds: 200), + initialChildSize: 0.92, + // maxChildSize: 0.92, + expand: false, + builder: (context, scrollController) { + return Scaffold( + body: Stack( + children: [ + if (FinampSettingsHelper + .finampSettings.showCoverAsPlayerBackground) + const BlurredPlayerScreenBackground(), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 10), + Container( + width: 40, + height: 3.5, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.5), + ), + ), + const SizedBox(height: 10), + const Text("Queue", + style: TextStyle( + color: Colors.white, + fontFamily: 'Lexend Deca', + fontSize: 18, + fontWeight: FontWeight.w300)), + const SizedBox(height: 20), + Expanded( + child: QueueList( + scrollController: scrollController, + nextUpHeaderKey: nextUpHeaderKey, + ), + ), + ], + ), + ], ), - )), + //TODO fade this out if the key is visible + floatingActionButton: FloatingActionButton( + onPressed: () => scrollToKey( + key: nextUpHeaderKey, + duration: const Duration(milliseconds: 500)), + backgroundColor: IconTheme.of(context).color!.withOpacity(0.70), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0))), + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Icon( + TablerIcons.focus_2, + size: 28.0, + color: Colors.white.withOpacity(0.85), + ), + )), + ); + // ) + // return QueueList( + // scrollController: scrollController, + // ); + }, + ), ); - // ) - // return QueueList( - // scrollController: scrollController, - // ); - }, + } ); }, ); @@ -590,14 +620,14 @@ class _CurrentTrackState extends State { ), backgroundColor: const Color.fromRGBO(0, 0, 0, 0.0), flexibleSpace: Container( - color: const Color.fromRGBO(0, 0, 0, 1.0), // width: 328, height: 70.0, padding: const EdgeInsets.symmetric(horizontal: 12), child: Container( - decoration: const ShapeDecoration( - color: Color.fromRGBO(188, 136, 86, 0.20), - shape: RoundedRectangleBorder( + decoration: ShapeDecoration( + // color: Color.fromRGBO(188, 136, 86, 0.20), + color: Color.alphaBlend(IconTheme.of(context).color!.withOpacity(0.20), Colors.black), + shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topRight: Radius.circular(8), bottomRight: Radius.circular(8), @@ -627,7 +657,7 @@ class _CurrentTrackState extends State { height: 70, decoration: const ShapeDecoration( shape: Border(), - color: Color.fromRGBO(0, 0, 0, 0.25), + color: Color.fromRGBO(0, 0, 0, 0.3), ), child: IconButton( onPressed: () { @@ -642,6 +672,7 @@ class _CurrentTrackState extends State { TablerIcons.player_play, size: 32, ), + color: Color.fromRGBO(255, 255, 255, 1.0), )), ], ), @@ -653,15 +684,16 @@ class _CurrentTrackState extends State { top: 0, // child: RepaintBoundary( child: Container( - width: 320 * + width: 298 * (playbackPosition!.inMilliseconds / (mediaState?.mediaItem?.duration ?? const Duration(seconds: 0)) .inMilliseconds), height: 70.0, - decoration: const ShapeDecoration( - color: Color.fromRGBO(188, 136, 86, 0.75), - shape: RoundedRectangleBorder( + decoration: ShapeDecoration( + // color: Color.fromRGBO(188, 136, 86, 0.75), + color: IconTheme.of(context).color!.withOpacity(0.75), + shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topRight: Radius.circular(8), bottomRight: Radius.circular(8), @@ -677,7 +709,7 @@ class _CurrentTrackState extends State { children: [ Container( height: 70, - width: 130, + width: 222, padding: const EdgeInsets.only(left: 12, right: 4), // child: Expanded( @@ -696,30 +728,21 @@ class _CurrentTrackState extends State { overflow: TextOverflow.ellipsis), ), const SizedBox(height: 4), - Text( - processArtist( - currentTrack!.item.artist, context), - style: TextStyle( - color: Colors.white.withOpacity(0.85), - fontSize: 13, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w300, - overflow: TextOverflow.ellipsis), - ), - ], - ), - // ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + processArtist( + currentTrack!.item.artist, context), + style: TextStyle( + color: Colors.white.withOpacity(0.85), + fontSize: 13, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w300, + overflow: TextOverflow.ellipsis), + ), + Row(children: [ + Text( // '0:00', playbackPosition!.inHours >= 1.0 ? "${playbackPosition?.inHours.toString()}:${((playbackPosition?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" @@ -728,7 +751,7 @@ class _CurrentTrackState extends State { color: Colors.white.withOpacity(0.8), fontSize: 14, fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w300, + fontWeight: FontWeight.w400, ), ), const SizedBox(width: 2), @@ -757,20 +780,75 @@ class _CurrentTrackState extends State { fontWeight: FontWeight.w400, ), ), + ],) + ], + ) + ], + ), + // ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + // Text( + // // '0:00', + // playbackPosition!.inHours >= 1.0 + // ? "${playbackPosition?.inHours.toString()}:${((playbackPosition?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" + // : "${playbackPosition?.inMinutes.toString()}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + // style: TextStyle( + // color: Colors.white.withOpacity(0.8), + // fontSize: 14, + // fontFamily: 'Lexend Deca', + // fontWeight: FontWeight.w400, + // ), + // ), + // const SizedBox(width: 2), + // Text( + // '/', + // style: TextStyle( + // color: Colors.white.withOpacity(0.8), + // fontSize: 14, + // fontFamily: 'Lexend Deca', + // fontWeight: FontWeight.w400, + // ), + // ), + // const SizedBox(width: 2), + // Text( + // // '3:44', + // (mediaState?.mediaItem?.duration + // ?.inHours ?? + // 0.0) >= + // 1.0 + // ? "${mediaState?.mediaItem?.duration?.inHours.toString()}:${((mediaState?.mediaItem?.duration?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((mediaState?.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" + // : "${mediaState?.mediaItem?.duration?.inMinutes.toString()}:${((mediaState?.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + // style: TextStyle( + // color: Colors.white.withOpacity(0.8), + // fontSize: 14, + // fontFamily: 'Lexend Deca', + // fontWeight: FontWeight.w400, + // ), + // ), ], ), IconButton( - padding: const EdgeInsets.only(left: 8.0), - // visualDensity: VisualDensity.compact, + iconSize: 16, + visualDensity: const VisualDensity(horizontal: -4), icon: jellyfin_models.BaseItemDto.fromJson(currentTrack!.item.extras?["itemJson"]).userData!.isFavorite ? const Icon( TablerIcons.heart, - size: 32, - color: Color.fromRGBO(188, 136, 86, 1.0), + size: 28, + color: Colors.white, + fill: 1.0, weight: 1.5, //TODO weight not working, stroke is too thick for most icons ) : const Icon( TablerIcons.heart, - size: 32, + size: 28, color: Colors.white, weight: 1.5, //TODO weight not working, stroke is too thick for most icons @@ -782,11 +860,12 @@ class _CurrentTrackState extends State { }, ), IconButton( - padding: const EdgeInsets.all(0.0), + iconSize: 28, + visualDensity: const VisualDensity(horizontal: -4), // visualDensity: VisualDensity.compact, icon: const Icon( TablerIcons.dots_vertical, - size: 32, + size: 28, color: Colors.white, weight: 1.5, ), @@ -1065,7 +1144,7 @@ class SectionHeaderDelegate extends SliverPersistentHeaderDelegate { TablerIcons.arrows_right, )), color: info?.order == PlaybackOrder.shuffled - ? Colors.orange + ? IconTheme.of(context).color! : Colors.white, onPressed: () { _queueService.togglePlaybackOrder(); @@ -1093,7 +1172,7 @@ class SectionHeaderDelegate extends SliverPersistentHeaderDelegate { TablerIcons.repeat_off, )), color: info?.loop != LoopMode.none - ? Colors.orange + ? IconTheme.of(context).color! : Colors.white, onPressed: () => _queueService.toggleLoopMode(), ), diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index 24a46a2c8..5fc557419 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -70,8 +70,9 @@ class _QueueListItemState extends State { borderRadius: BorderRadius.circular(8.0), ), child: ListTile( - visualDensity: VisualDensity.compact, + visualDensity: VisualDensity.standard, minVerticalPadding: 0.0, + horizontalTitleGap: 10.0, contentPadding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0), tileColor: widget.isCurrentTrack @@ -83,38 +84,68 @@ class _QueueListItemState extends State { : jellyfin_models.BaseItemDto.fromJson( widget.item.item.extras?["itemJson"]), ), - title: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Text( - widget.item.item.title ?? - AppLocalizations.of(context)!.unknownName, - style: this.widget.isCurrentTrack - ? TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontSize: 16, + // leading: Container( + // height: 60.0, + // width: 60.0, + // color: Colors.white, + // ), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(0.0), + child: Text( + widget.item.item.title ?? + AppLocalizations.of(context)!.unknownName, + style: this.widget.isCurrentTrack + ? TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontSize: 16, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w400, + overflow: TextOverflow.ellipsis) + : null, + overflow: TextOverflow.ellipsis, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 6.0), + child: Text( + processArtist(widget.item.item.artist, context), + style: const TextStyle( + color: Colors.white70, + fontSize: 13, fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w400, - overflow: TextOverflow.ellipsis) - : null, - overflow: TextOverflow.ellipsis, - ), - ), - subtitle: Text( - processArtist(widget.item.item.artist, context), - style: const TextStyle( - color: Colors.white70, - fontSize: 13, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w300, - overflow: TextOverflow.ellipsis), - overflow: TextOverflow.ellipsis, + fontWeight: FontWeight.w300, + overflow: TextOverflow.ellipsis), + overflow: TextOverflow.ellipsis, + ), + ), + ], ), + // subtitle: Container( + // alignment: Alignment.centerLeft, + // height: 40.5, // has to be above a certain value to get rid of vertical padding + // child: Padding( + // padding: const EdgeInsets.only(bottom: 2.0), + // child: Text( + // processArtist(widget.item.item.artist, context), + // style: const TextStyle( + // color: Colors.white70, + // fontSize: 13, + // fontFamily: 'Lexend Deca', + // fontWeight: FontWeight.w300, + // overflow: TextOverflow.ellipsis), + // overflow: TextOverflow.ellipsis, + // ), + // ), + // ), trailing: Container( alignment: Alignment.centerRight, margin: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 6.0), - width: widget.allowReorder ? 145.0 : 115.0, - // width: widget.allowReorder ? 68.0 : 35.0, + // width: widget.allowReorder ? 145.0 : 115.0, + width: widget.allowReorder ? 70.0 : 35.0, height: 50.0, child: Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/screens/blurred_player_screen_background.dart b/lib/screens/blurred_player_screen_background.dart new file mode 100644 index 000000000..747199710 --- /dev/null +++ b/lib/screens/blurred_player_screen_background.dart @@ -0,0 +1,44 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:octo_image/octo_image.dart'; + +import '../services/current_album_image_provider.dart'; + +/// Same as [_PlayerScreenAlbumImage], but with a BlurHash instead. We also +/// filter the BlurHash so that it works as a background image. +class BlurredPlayerScreenBackground extends ConsumerWidget { + const BlurredPlayerScreenBackground({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final imageProvider = ref.watch(currentAlbumImageProvider); + + return ClipRect( + child: imageProvider == null + ? const SizedBox.shrink() + : OctoImage( + image: imageProvider, + fit: BoxFit.cover, + placeholderBuilder: (_) => const SizedBox.shrink(), + errorBuilder: (_, __, ___) => const SizedBox.shrink(), + imageBuilder: (context, child) => ColorFiltered( + colorFilter: ColorFilter.mode( + Theme.of(context).brightness == Brightness.dark + ? Colors.black.withOpacity(0.75) + : Colors.white.withOpacity(0.50), + BlendMode.srcOver), + child: ImageFiltered( + imageFilter: ImageFilter.blur( + sigmaX: 85, + sigmaY: 85, + tileMode: TileMode.mirror, + ), + child: SizedBox.expand(child: child), + ), + ), + ), + ); + } +} diff --git a/lib/screens/player_screen.dart b/lib/screens/player_screen.dart index f342b4105..e95cf8e71 100644 --- a/lib/screens/player_screen.dart +++ b/lib/screens/player_screen.dart @@ -18,6 +18,7 @@ import 'package:get_it/get_it.dart'; import '../models/finamp_models.dart'; import '../services/player_screen_theme_provider.dart'; +import 'blurred_player_screen_background.dart'; const _toolbarHeight = 75.0; @@ -39,83 +40,57 @@ class PlayerScreen extends ConsumerWidget { color: imageTheme?.primary, ), ), - child: SimpleGestureDetector( - onVerticalSwipe: (direction) { - if (!FinampSettingsHelper.finampSettings.disableGesture) { - if (direction == SwipeDirection.down) { - Navigator.of(context).pop(); - } else if (direction == SwipeDirection.up) { - showQueueBottomSheet(context); - } - } - }, - child: Scaffold( - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - centerTitle: true, - leadingWidth: 48 + 24, - toolbarHeight: _toolbarHeight, - title: const PlayerScreenAppBarTitle(), - leading: FinampAppBarButton( - onPressed: () => Navigator.of(context).pop(), - ), - ), - // Required for sleep timer input - resizeToAvoidBottomInset: false, extendBodyBehindAppBar: true, - body: Stack( - children: [ - if (FinampSettingsHelper - .finampSettings.showCoverAsPlayerBackground) - const _BlurredPlayerScreenBackground(), - const SafeArea( - minimum: EdgeInsets.only(top: _toolbarHeight), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [SongInfo(), ControlArea(), QueueButton()], - ), - ), - ], - ), - ), - ), + child: _PlayerScreenContent(), ); } } -/// Same as [_PlayerScreenAlbumImage], but with a BlurHash instead. We also -/// filter the BlurHash so that it works as a background image. -class _BlurredPlayerScreenBackground extends ConsumerWidget { - const _BlurredPlayerScreenBackground({Key? key}) : super(key: key); +class _PlayerScreenContent extends StatelessWidget { + const _PlayerScreenContent({ + super.key, + }); @override - Widget build(BuildContext context, WidgetRef ref) { - final imageProvider = ref.watch(currentAlbumImageProvider); - - return ClipRect( - child: imageProvider == null - ? const SizedBox.shrink() - : OctoImage( - image: imageProvider, - fit: BoxFit.cover, - placeholderBuilder: (_) => const SizedBox.shrink(), - errorBuilder: (_, __, ___) => const SizedBox.shrink(), - imageBuilder: (context, child) => ColorFiltered( - colorFilter: ColorFilter.mode( - Theme.of(context).brightness == Brightness.dark - ? Colors.black.withOpacity(0.75) - : Colors.white.withOpacity(0.50), - BlendMode.srcOver), - child: ImageFiltered( - imageFilter: ImageFilter.blur( - sigmaX: 85, - sigmaY: 85, - tileMode: TileMode.mirror, - ), - child: SizedBox.expand(child: child), - ), + Widget build(BuildContext context) { + return SimpleGestureDetector( + onVerticalSwipe: (direction) { + if (!FinampSettingsHelper.finampSettings.disableGesture) { + if (direction == SwipeDirection.down) { + Navigator.of(context).pop(); + } else if (direction == SwipeDirection.up) { + showQueueBottomSheet(context); + } + } + }, + child: Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + centerTitle: true, + leadingWidth: 48 + 24, + toolbarHeight: _toolbarHeight, + title: const PlayerScreenAppBarTitle(), + leading: FinampAppBarButton( + onPressed: () => Navigator.of(context).pop(), + ), + ), + // Required for sleep timer input + resizeToAvoidBottomInset: false, extendBodyBehindAppBar: true, + body: Stack( + children: [ + if (FinampSettingsHelper + .finampSettings.showCoverAsPlayerBackground) + const BlurredPlayerScreenBackground(), + const SafeArea( + minimum: EdgeInsets.only(top: _toolbarHeight), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [SongInfo(), ControlArea(), QueueButton()], ), ), + ], + ), + ), ); } } diff --git a/pubspec.lock b/pubspec.lock index 9e288f7b2..5bcca4135 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -416,10 +416,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: b83ac5827baadefd331ea1d85110f34645827ea234ccabf53a655f41901a9bf4 + sha256: "1bd39b04f1bcd217a969589777ca6bd642d116e3e5de65c3e6a8e8bdd8b178ec" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.4.0" flutter_sticky_header: dependency: "direct main" description: @@ -892,10 +892,10 @@ packages: dependency: transitive description: name: riverpod - sha256: "80e48bebc83010d5e67a11c9514af6b44bbac1ec77b4333c8ea65dbc79e2d8ef" + sha256: a600120d6f213a9922860eea1abc32597436edd5b2c4e73b91410f8c2af67d22 url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.4.0" rxdart: dependency: "direct main" description: From 6b72110929922099d62a7e83df300ed9994dec1a Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Wed, 20 Sep 2023 13:52:36 +0200 Subject: [PATCH 068/130] try to fix build errors --- pubspec.lock | 332 ++++++++++++++++++++++++--------------------------- pubspec.yaml | 1 - 2 files changed, 154 insertions(+), 179 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 5bcca4135..a2fb3df9f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: a36ec4843dc30ea6bf652bf25e3448db6c5e8bcf4aa55f063a5d1dad216d8214 + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a url: "https://pub.dev" source: hosted - version: "58.0.0" + version: "61.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: cc4242565347e98424ce9945c819c192ec0838cb9d1f6aa4a97cc96becbc5b27 + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 url: "https://pub.dev" source: hosted - version: "5.10.0" + version: "5.13.0" android_id: dependency: "direct main" description: @@ -29,18 +29,18 @@ packages: dependency: transitive description: name: archive - sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" + sha256: e0902a06f0e00414e4e3438a084580161279f137aeb862274710f29ec10cf01e url: "https://pub.dev" source: hosted - version: "3.3.7" + version: "3.3.9" args: dependency: transitive description: name: args - sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.2" async: dependency: transitive description: @@ -53,18 +53,18 @@ packages: dependency: "direct main" description: name: audio_service - sha256: "7e86d7ce23caad605199f7b25e548fe7b618fb0c150fa0585f47a910fe7e7a67" + sha256: a4d989f1225ea9621898d60f23236dcbfc04876fa316086c23c5c4af075dbac4 url: "https://pub.dev" source: hosted - version: "0.18.9" + version: "0.18.12" audio_service_platform_interface: dependency: transitive description: name: audio_service_platform_interface - sha256: "2c3a1d52803931e836b9693547a71c0c3585ad54219d2214219ed5cfcc3c1af4" + sha256: "8431a455dac9916cc9ee6f7da5620a666436345c906ad2ebb7fa41d18b3c1bf4" url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.1.1" audio_service_web: dependency: transitive description: @@ -77,10 +77,10 @@ packages: dependency: "direct main" description: name: audio_session - sha256: e4acc4e9eaa32436dfc5d7aed7f0a370f2d7bb27ee27de30d6c4f220c2a05c73 + sha256: "8a2bc5e30520e18f3fb0e366793d78057fb64cd5287862c76af0c8771f2a52ad" url: "https://pub.dev" source: hosted - version: "0.1.13" + version: "0.1.16" auto_size_text: dependency: "direct main" description: @@ -101,10 +101,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -117,34 +117,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "4.0.0" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 + sha256: d912852cce27c9e80a93603db721c267716894462e7033165178b91138587972 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.6" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" url: "https://pub.dev" source: hosted - version: "7.2.7" + version: "7.2.10" built_collection: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: built_value - sha256: "31b7c748fd4b9adf8d25d72a4c4a59ef119f12876cf414f94f8af5131d5fa2b0" + sha256: ff627b645b28fb8bdb69e645f910c2458fd6b65f6585c3a53e0626024897dedf url: "https://pub.dev" source: hosted - version: "8.4.4" + version: "8.6.2" characters: dependency: transitive description: @@ -173,26 +173,26 @@ packages: dependency: transitive description: name: checked_yaml - sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" chopper: dependency: "direct main" description: name: chopper - sha256: b2645618fa760df06d7609c96b092d7b3e7a8f23639d34269f62f45a5edabc7d + sha256: "23aac2db54f6a7854ed8984fba4b33222e53123c71a0ccf01d2b1087a1b5aab7" url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.1.4" chopper_generator: dependency: "direct dev" description: name: chopper_generator - sha256: "62360a1a3536645aaee030dd7719089838a478682867d46dbcf30a3c0e5305b4" + sha256: "09d512f1acb086cf4101bef02bdb23ee7a8eb55a9935f53af866736606c57523" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.0.3" cli_util: dependency: transitive description: @@ -221,10 +221,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + sha256: "315a598c7fbe77f22de1c9da7cfd6fd21816312f16ffa124453b4fc679e540f1" url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.6.0" collection: dependency: transitive description: @@ -245,42 +245,42 @@ packages: dependency: transitive description: name: cross_file - sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb url: "https://pub.dev" source: hosted - version: "0.3.3+4" + version: "0.3.3+5" crypto: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" dart_style: dependency: transitive description: name: dart_style - sha256: "6d691edde054969f0e0f26abb1b30834b5138b963793e56f69d3a9a4435e6352" + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" device_info_plus: dependency: "direct main" description: name: device_info_plus - sha256: "435383ca05f212760b0a70426b5a90354fe6bd65992b3a5e27ab6ede74c02f5c" + sha256: f52ab3b76b36ede4d135aab80194df8925b553686f0fa12226b4e2d658e45903 url: "https://pub.dev" source: hosted - version: "8.2.0" + version: "8.2.2" device_info_plus_platform_interface: dependency: transitive description: @@ -289,14 +289,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" - drag_and_drop_lists: - dependency: "direct main" - description: - name: drag_and_drop_lists - sha256: "9a14595f9880be7953f23578aef88c15cc9b367eeb39dbc1f1bd6af52f70872b" - url: "https://pub.dev" - source: hosted - version: "0.3.3" equatable: dependency: transitive description: @@ -317,10 +309,10 @@ packages: dependency: transitive description: name: ffi - sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" file: dependency: transitive description: @@ -370,10 +362,10 @@ packages: dependency: transitive description: name: flutter_cache_manager - sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3" + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.3.1" flutter_downloader: dependency: "direct main" description: @@ -395,10 +387,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" flutter_localizations: dependency: "direct main" description: flutter @@ -408,10 +400,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: c224ac897bed083dabf11f238dd11a239809b446740be0c2044608c50029ffdf + sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.16" flutter_riverpod: dependency: "direct main" description: @@ -459,26 +451,26 @@ packages: dependency: "direct main" description: name: get_it - sha256: f9982979e3d2f286a957c04d2c3a98f55b0f0a06ffd6c5c4abbb96f06937f463 + sha256: f79870884de16d689cf9a7d15eedf31ed61d750e813c538a6efb92660fea83c3 url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.6.4" glob: dependency: transitive description: name: glob - sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" graphs: dependency: transitive description: name: graphs - sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.1" hive: dependency: "direct main" description: @@ -499,18 +491,18 @@ packages: dependency: "direct dev" description: name: hive_generator - sha256: "65998cc4d2cd9680a3d9709d893d2f6bb15e6c1f92626c3f1fa650b4b3281521" + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" http: dependency: transitive description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "0.13.6" http_multi_server: dependency: transitive description: @@ -531,10 +523,10 @@ packages: dependency: transitive description: name: image - sha256: "483a389d6ccb292b570c31b3a193779b1b0178e7eb571986d9a49904b6861227" + sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf url: "https://pub.dev" source: hosted - version: "4.0.15" + version: "4.0.17" infinite_scroll_pagination: dependency: "direct main" description: @@ -571,50 +563,50 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.8.1" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: dadc08bd61f72559f938dd08ec20dbfec6c709bba83515085ea943d2078d187a + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 url: "https://pub.dev" source: hosted - version: "6.6.1" + version: "6.7.1" just_audio: dependency: "direct main" description: name: just_audio - sha256: "7e6d31508dacd01a066e3889caf6282e5f1eb60707c230203b21a83af5c55586" + sha256: "5ed0cd723e17dfd8cd4b0253726221e67f6546841ea4553635cf895061fc335b" url: "https://pub.dev" source: hosted - version: "0.9.32" + version: "0.9.35" just_audio_platform_interface: dependency: transitive description: name: just_audio_platform_interface - sha256: eff112d5138bea3ba544b6338b1e0537a32b5e1425e4d0dc38f732771cda7c84 + sha256: d8409da198bbc59426cd45d4c92fca522a2ec269b576ce29459d6d6fcaeb44df url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.2.1" just_audio_web: dependency: transitive description: name: just_audio_web - sha256: "89d8db6f19f3821bb6bf908c4bfb846079afb2ab575b783d781a6bf119e3abaf" + sha256: ff62f733f437b25a0ff590f0e295fa5441dcb465f1edbdb33b3dea264705bc13 url: "https://pub.dev" source: hosted - version: "0.4.7" + version: "0.4.8" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.1" locale_names: dependency: "direct main" description: @@ -628,10 +620,10 @@ packages: dependency: "direct main" description: name: logging - sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" matcher: dependency: transitive description: @@ -692,10 +684,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: cbff87676c352d97116af6dbea05aa28c4d65eb0f6d5677a520c11a69ca9a24d + sha256: "10259b111176fba5c505b102e3a5b022b51dd97e30522e906d6922c745584745" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" package_info_plus_platform_interface: dependency: transitive description: @@ -708,10 +700,10 @@ packages: dependency: "direct main" description: name: palette_generator - sha256: "0e3cd6974e10b1434dcf4cf779efddb80e2696585e273a2dbede6af52f94568d" + sha256: eb7082b4b97487ebc65b3ad3f6f0b7489b96e76840381ed0e06a46fe7ffd4068 url: "https://pub.dev" source: hosted - version: "0.3.3+2" + version: "0.3.3+3" path: dependency: "direct main" description: @@ -724,122 +716,114 @@ packages: dependency: "direct main" description: name: path_provider - sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4 + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.0.14" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: da97262be945a72270513700a92b39dd2f4a54dad55d061687e2e37a6390366a + sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1" url: "https://pub.dev" source: hosted - version: "2.0.25" + version: "2.2.0" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183 + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.10" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.1.5" - pedantic: - dependency: transitive - description: - name: pedantic - sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" - url: "https://pub.dev" - source: hosted - version: "1.11.1" + version: "2.2.1" permission_handler: dependency: "direct main" description: name: permission_handler - sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8" + sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5 url: "https://pub.dev" source: hosted - version: "10.2.0" + version: "10.4.5" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2" + sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47" url: "https://pub.dev" source: hosted - version: "10.2.0" + version: "10.3.6" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: ee96ac32f5a8e6f80756e25b25b9f8e535816c8e6665a96b6d70681f8c4f7e85 + sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" url: "https://pub.dev" source: hosted - version: "9.0.8" + version: "9.1.4" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: "68abbc472002b5e6dfce47fe9898c6b7d8328d58b5d2524f75e277c07a97eb84" + sha256: f2343e9fa9c22ae4fd92d4732755bfe452214e7189afcc097380950cf567b4b2 url: "https://pub.dev" source: hosted - version: "3.9.0" + version: "3.11.5" permission_handler_windows: dependency: transitive description: name: permission_handler_windows - sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b + sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "0.1.3" petitparser: dependency: transitive description: name: petitparser - sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.4.0" platform: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.6" pointycastle: dependency: transitive description: @@ -856,14 +840,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - process: - dependency: transitive - description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" - url: "https://pub.dev" - source: hosted - version: "4.2.4" provider: dependency: "direct main" description: @@ -876,18 +852,18 @@ packages: dependency: transitive description: name: pub_semver - sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: ec85d7d55339d85f44ec2b682a82fea340071e8978257e5a43e69f79e98ef50c + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.3" riverpod: dependency: transitive description: @@ -908,34 +884,34 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "692261968a494e47323dcc8bc66d8d52e81bc27cb4b808e4e8d7e8079d4cc01a" + sha256: b1f15232d41e9701ab2f04181f21610c36c83a12ae426b79b4bd011c567934b1 url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.4" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "0c6e61471bd71b04a138b8b588fa388e66d8b005e6f2deda63371c5c505a0981" + sha256: "357412af4178d8e11d14f41723f80f12caea54cf0d5cd29af9dcdab85d58aea7" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.3.0" shelf: dependency: transitive description: name: shelf - sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" simple_gesture_detector: dependency: "direct main" description: @@ -953,26 +929,26 @@ packages: dependency: transitive description: name: sliver_tools - sha256: ccdc502098a8bfa07b3ec582c282620031481300035584e1bb3aca296a505e8c + sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6 url: "https://pub.dev" source: hosted - version: "0.2.10" + version: "0.2.12" source_gen: dependency: transitive description: name: source_gen - sha256: c2bea18c95cfa0276a366270afaa2850b09b4a76db95d546f3d003dcc7011298 + sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 url: "https://pub.dev" source: hosted - version: "1.2.7" + version: "1.4.0" source_helper: dependency: transitive description: name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.4" source_span: dependency: transitive description: @@ -985,18 +961,18 @@ packages: dependency: transitive description: name: sqflite - sha256: e7dfb6482d5d02b661d0b2399efa72b98909e5aa7b8336e1fb37e226264ade00 + sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" url: "https://pub.dev" source: hosted - version: "2.2.7" + version: "2.3.0" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "220831bf0bd5333ff2445eee35ec131553b804e6b5d47a4a37ca6f5eb66e282c" + sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.5.0" stack_trace: dependency: transitive description: @@ -1009,10 +985,10 @@ packages: dependency: transitive description: name: state_notifier - sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289" + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb url: "https://pub.dev" source: hosted - version: "0.7.2+1" + version: "1.0.0" stream_channel: dependency: transitive description: @@ -1073,42 +1049,42 @@ packages: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "206fb8334a700ef7754d6a9ed119e7349bc830448098f21a69bf1b4ed038cabc" + sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.6" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.5" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" + sha256: ba140138558fcc3eead51a1c42e92a9fb074a1b1149ed3c73e66035b2ccd94f2 url: "https://pub.dev" source: hosted - version: "2.0.16" + version: "2.0.19" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: a83ba3607a507758669cfafb03f9de09bf6e6280c14d9b9cb18f013e406dcacd + sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" uuid: dependency: "direct main" description: @@ -1137,10 +1113,10 @@ packages: dependency: transitive description: name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" web_socket_channel: dependency: transitive description: @@ -1161,26 +1137,26 @@ packages: dependency: transitive description: name: xdg_directories - sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.3" xml: dependency: transitive description: name: xml - sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.0" yaml: dependency: transitive description: name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" sdks: - dart: ">=3.0.0-0 <4.0.0" - flutter: ">=3.3.0" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.7.0" diff --git a/pubspec.yaml b/pubspec.yaml index 6f60f8cb1..e7f776ce2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,7 +54,6 @@ dependencies: package_info_plus: ^3.1.0 octo_image: ^1.0.2 share_plus: ^6.3.2 - drag_and_drop_lists: ^0.3.3 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. From 04773fbde3d0455b5ec54466a54b2591e7f7a521 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Wed, 20 Sep 2023 21:04:35 +0200 Subject: [PATCH 069/130] improved playback history screen - still needs a proper design and improved implementation --- .../playback_history_list.dart | 134 +++--- .../playback_history_list_tile.dart | 397 ++++++++++++++++++ lib/services/playback_history_service.dart | 63 +++ 3 files changed, 519 insertions(+), 75 deletions(-) create mode 100644 lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart diff --git a/lib/components/PlaybackHistoryScreen/playback_history_list.dart b/lib/components/PlaybackHistoryScreen/playback_history_list.dart index 0a6b0b707..39b488f1a 100644 --- a/lib/components/PlaybackHistoryScreen/playback_history_list.dart +++ b/lib/components/PlaybackHistoryScreen/playback_history_list.dart @@ -1,14 +1,12 @@ import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/services/audio_service_helper.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; import '../../services/playback_history_service.dart'; import '../../models/jellyfin_models.dart' as jellyfin_models; -import '../album_image.dart'; -import '../../services/process_artist.dart'; +import 'playback_history_list_tile.dart'; class PlaybackHistoryList extends StatelessWidget { const PlaybackHistoryList({Key? key}) : super(key: key); @@ -19,95 +17,81 @@ class PlaybackHistoryList extends StatelessWidget { final audioServiceHelper = GetIt.instance(); List? history; + List>> groupedHistory; - return Scrollbar( - child: StreamBuilder>( + return StreamBuilder>( stream: playbackHistoryService.historyStream, builder: (context, snapshot) { if (snapshot.hasData) { history = snapshot.data; - - return ListView.builder( - itemCount: history!.length, - reverse: true, - padding: const EdgeInsets.only(bottom: 160.0), - itemBuilder: (context, index) { - // return ListTile( - // title: Text(history![index].item.item.title), - // subtitle: Text(history![index].item.item.artist!), - // ); - return Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: ListTile( - visualDensity: VisualDensity.compact, - minVerticalPadding: 0.0, - contentPadding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0), - leading: AlbumImage( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(7.0), - bottomLeft: Radius.circular(7.0), - ), - item: history![index].item.item - .extras?["itemJson"] == null - ? null - : jellyfin_models.BaseItemDto.fromJson(history![index].item.item.extras?["itemJson"]), - ), - title: Text( - history![index].item.item.title, - ), - subtitle: Text(processArtist( - history![index].item.item.artist, - context)), - trailing: Container( - alignment: Alignment.centerRight, - margin: const EdgeInsets.only(right: 0.0), - width: 95.0, - height: 50.0, - child: Row( - mainAxisSize: MainAxisSize.min, + // groupedHistory = playbackHistoryService.getHistoryGroupedByDate(); + groupedHistory = playbackHistoryService.getHistoryGroupedByHour(); + + print(groupedHistory); + + return CustomScrollView( + // use nested SliverList.builder()s to show history items grouped by date + slivers: groupedHistory.map((group) { + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + + final actualIndex = group.value.length - index - 1; + + final historyItem = Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + child: PlaybackHistoryListTile( + actualIndex: actualIndex, + item: group.value[actualIndex], + audioServiceHelper: audioServiceHelper, + onTap: () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.startingInstantMix), + )); + + audioServiceHelper.startInstantMixForItem(jellyfin_models.BaseItemDto.fromJson(group.value[actualIndex].item.item.extras?["itemJson"])).catchError((e) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.anErrorHasOccured), + )); + }); + }, + ), + ); + + return index == 0 ? + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "${history![index].item.item.duration?.inMinutes.toString().padLeft(2, '0')}:${((history![index].item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - textAlign: TextAlign.end, - style: TextStyle( - color: Theme.of(context).textTheme.bodySmall?.color, + Padding( + padding: const EdgeInsets.only(left: 16.0, top: 8.0, bottom: 4.0), + child: Text( + "${group.key.hour % 12} ${group.key.hour >= 12 ? "pm" : "am"}", + style: const TextStyle( + fontSize: 16.0, + ), ), ), - IconButton( - icon: const Icon(TablerIcons.dots_vertical), - iconSize: 24.0, - onPressed: () async => {}, - ), + historyItem, ], - ), - ), - onTap: () { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.startingInstantMix), - )); - - audioServiceHelper.startInstantMixForItem(jellyfin_models.BaseItemDto.fromJson(history![index].item.item.extras?["itemJson"])).catchError((e) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.anErrorHasOccured), - )); - }); - - } - // await _queueService.skipByOffset(indexOffset), - ) + ) + : historyItem; + }, + childCount: group.value.length, + ), ); - }, + }).toList(), ); } else { return const Center( child: CircularProgressIndicator(), ); } - }, - ), + } + ); } + } diff --git a/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart b/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart new file mode 100644 index 000000000..b6c3c5400 --- /dev/null +++ b/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart @@ -0,0 +1,397 @@ +import 'package:finamp/models/finamp_models.dart'; +import 'package:finamp/services/audio_service_helper.dart'; +import 'package:finamp/services/jellyfin_api_helper.dart'; +import 'package:finamp/services/queue_service.dart'; +import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:get_it/get_it.dart'; + +import '../../models/jellyfin_models.dart' as jellyfin_models; +import '../album_image.dart'; +import '../../services/process_artist.dart'; + +import 'package:finamp/components/AlbumScreen/song_list_tile.dart'; +import 'package:finamp/components/error_snackbar.dart'; +import 'package:finamp/screens/add_to_playlist_screen.dart'; +import 'package:finamp/screens/album_screen.dart'; +import 'package:finamp/services/downloads_helper.dart'; +import 'package:finamp/services/finamp_settings_helper.dart'; +import 'package:flutter/material.dart' hide ReorderableList; + +class PlaybackHistoryListTile extends StatefulWidget { + PlaybackHistoryListTile({ + super.key, + required this.actualIndex, + required this.item, + required this.audioServiceHelper, + required this.onTap, + }); + + final int actualIndex; + final HistoryItem item; + final AudioServiceHelper audioServiceHelper; + late void Function() onTap; + + final _queueService = GetIt.instance(); + final _audioServiceHelper = GetIt.instance(); + final _jellyfinApiHelper = GetIt.instance(); + + @override + State createState() => _PlaybackHistoryListTileState(); +} + +class _PlaybackHistoryListTileState extends State { + @override + Widget build(BuildContext context) { + return GestureDetector( + onLongPressStart: (details) => showSongMenu(details), + child: Card( + margin: EdgeInsets.all(0.0), + elevation: 0, + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + child: ListTile( + visualDensity: VisualDensity.standard, + minVerticalPadding: 0.0, + horizontalTitleGap: 10.0, + contentPadding: + const EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0), + leading: AlbumImage( + item: widget.item.item.item.extras?["itemJson"] == null + ? null + : jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]), + ), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(0.0), + child: Text( + widget.item.item.item.title ?? + AppLocalizations.of(context)!.unknownName, + overflow: TextOverflow.ellipsis, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 6.0), + child: Text( + processArtist(widget.item.item.item.artist, context), + style: const TextStyle( + color: Colors.white70, + fontSize: 13, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w300, + overflow: TextOverflow.ellipsis), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + // subtitle: Container( + // alignment: Alignment.centerLeft, + // height: 40.5, // has to be above a certain value to get rid of vertical padding + // child: Padding( + // padding: const EdgeInsets.only(bottom: 2.0), + // child: Text( + // processArtist(widget.item.item.item.artist, context), + // style: const TextStyle( + // color: Colors.white70, + // fontSize: 13, + // fontFamily: 'Lexend Deca', + // fontWeight: FontWeight.w300, + // overflow: TextOverflow.ellipsis), + // overflow: TextOverflow.ellipsis, + // ), + // ), + // ), + trailing: Container( + alignment: Alignment.centerRight, + margin: const EdgeInsets.only(right: 8.0), + padding: const EdgeInsets.only(right: 6.0), + // width: widget.allowReorder ? 145.0 : 115.0, + width: 35.0, + height: 50.0, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "${widget.item.item.item.duration?.inMinutes.toString()}:${((widget.item.item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + textAlign: TextAlign.end, + style: TextStyle( + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + ], + ), + ), + onTap: widget.onTap, + ))); + + } + + void showSongMenu(LongPressStartDetails? details) async { + final canGoToAlbum = _isAlbumDownloadedIfOffline( + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]) + .parentId); + + // Some options are disabled in offline mode + final isOffline = FinampSettingsHelper.finampSettings.isOffline; + + final screenSize = MediaQuery.of(context).size; + + Feedback.forLongPress(context); + + final selection = await showMenu( + context: context, + position: details != null + ? RelativeRect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + screenSize.width - details.globalPosition.dx, + screenSize.height - details.globalPosition.dy, + ) + : RelativeRect.fromLTRB(MediaQuery.of(context).size.width - 50.0, + MediaQuery.of(context).size.height - 50.0, 0.0, 0.0), + items: [ + PopupMenuItem( + value: SongListTileMenuItems.addToQueue, + child: ListTile( + leading: const Icon(Icons.queue_music), + title: Text(AppLocalizations.of(context)!.addToQueue), + ), + ), + PopupMenuItem( + value: SongListTileMenuItems.playNext, + child: ListTile( + leading: const Icon(TablerIcons.hourglass_low), + title: Text("Play next"), + ), + ), + PopupMenuItem( + value: SongListTileMenuItems.addToNextUp, + child: ListTile( + leading: const Icon(TablerIcons.hourglass_high), + title: Text("Add to Next Up"), + ), + ), + PopupMenuItem( + enabled: !isOffline, + value: SongListTileMenuItems.addToPlaylist, + child: ListTile( + leading: const Icon(Icons.playlist_add), + title: Text(AppLocalizations.of(context)!.addToPlaylistTitle), + enabled: !isOffline, + ), + ), + PopupMenuItem( + enabled: !isOffline, + value: SongListTileMenuItems.instantMix, + child: ListTile( + leading: const Icon(Icons.explore), + title: Text(AppLocalizations.of(context)!.instantMix), + enabled: !isOffline, + ), + ), + PopupMenuItem( + enabled: canGoToAlbum, + value: SongListTileMenuItems.goToAlbum, + child: ListTile( + leading: const Icon(Icons.album), + title: Text(AppLocalizations.of(context)!.goToAlbum), + enabled: canGoToAlbum, + ), + ), + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]) + .userData! + .isFavorite + ? PopupMenuItem( + value: SongListTileMenuItems.removeFavourite, + child: ListTile( + leading: const Icon(Icons.favorite_border), + title: Text(AppLocalizations.of(context)!.removeFavourite), + ), + ) + : PopupMenuItem( + value: SongListTileMenuItems.addFavourite, + child: ListTile( + leading: const Icon(Icons.favorite), + title: Text(AppLocalizations.of(context)!.addFavourite), + ), + ), + ], + ); + + if (!mounted) return; + + switch (selection) { + case SongListTileMenuItems.addToQueue: + // await _audioServiceHelper.addQueueItem(jellyfin_models.BaseItemDto.fromJson(widget.item.item.item.extras?["itemJson"])); + await widget._queueService.addToQueue( + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]), + QueueItemSource( + type: QueueItemSourceType.unknown, + name: "Queue", + id: widget.item.item.source.id ?? "unknown")); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.addedToQueue), + )); + break; + + case SongListTileMenuItems.playNext: + // await _audioServiceHelper.addQueueItem(jellyfin_models.BaseItemDto.fromJson(widget.item.item.item.extras?["itemJson"])); + await widget._queueService.addNext(items: [jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"])]); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text("Track will play next"), + )); + break; + + case SongListTileMenuItems.addToNextUp: + // await _audioServiceHelper.addQueueItem(jellyfin_models.BaseItemDto.fromJson(widget.item.item.item.extras?["itemJson"])); + await widget._queueService.addToNextUp(items: [jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"])]); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text("Added track to Next Up"), + )); + break; + + case SongListTileMenuItems.addToPlaylist: + Navigator.of(context).pushNamed(AddToPlaylistScreen.routeName, + arguments: jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]) + .id); + break; + + case SongListTileMenuItems.instantMix: + await widget._audioServiceHelper.startInstantMixForItem( + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"])); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.startingInstantMix), + )); + break; + case SongListTileMenuItems.goToAlbum: + late jellyfin_models.BaseItemDto album; + if (FinampSettingsHelper.finampSettings.isOffline) { + // If offline, load the album's BaseItemDto from DownloadHelper. + final downloadsHelper = GetIt.instance(); + + // downloadedParent won't be null here since the menu item already + // checks if the DownloadedParent exists. + album = downloadsHelper + .getDownloadedParent(jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]) + .parentId!)! + .item; + } else { + // If online, get the album's BaseItemDto from the server. + try { + album = await widget._jellyfinApiHelper.getItemById( + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]) + .parentId!); + } catch (e) { + errorSnackbar(e, context); + break; + } + } + + if (!mounted) return; + + Navigator.of(context) + .pushNamed(AlbumScreen.routeName, arguments: album); + break; + case SongListTileMenuItems.addFavourite: + case SongListTileMenuItems.removeFavourite: + await setFavourite(); + break; + case null: + break; + } + } + + Future setFavourite() async { + try { + // We switch the widget state before actually doing the request to + // make the app feel faster (without, there is a delay from the + // user adding the favourite and the icon showing) + setState(() { + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]) + .userData! + .isFavorite = !jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]) + .userData! + .isFavorite; + }); + + // Since we flipped the favourite state already, we can use the flipped + // state to decide which API call to make + final newUserData = jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]) + .userData! + .isFavorite + ? await widget._jellyfinApiHelper.addFavourite( + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]) + .id) + : await widget._jellyfinApiHelper.removeFavourite( + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]) + .id); + + if (!mounted) return; + + setState(() { + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]) + .userData = newUserData; + }); + } catch (e) { + setState(() { + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]) + .userData! + .isFavorite = !jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]) + .userData! + .isFavorite; + }); + errorSnackbar(e, context); + } + } +} + + +/// If offline, check if an album is downloaded. Always returns true if online. +/// Returns false if albumId is null. +bool _isAlbumDownloadedIfOffline(String? albumId) { + if (albumId == null) { + return false; + } else if (FinampSettingsHelper.finampSettings.isOffline) { + final downloadsHelper = GetIt.instance(); + return downloadsHelper.isAlbumDownloaded(albumId); + } else { + return true; + } +} diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart index 53dd848f1..252ff36f4 100644 --- a/lib/services/playback_history_service.dart +++ b/lib/services/playback_history_service.dart @@ -52,6 +52,69 @@ class PlaybackHistoryService { get history => _history; BehaviorSubject> get historyStream => _historyStream; + /// method that converts history into a list grouped by date + List>> getHistoryGroupedByDate() { + final groupedHistory = >>[]; + + final groupedHistoryMap = >{}; + + _history.forEach((element) { + final date = DateTime( + element.startTime.year, + element.startTime.month, + element.startTime.day, + ); + + if (groupedHistoryMap.containsKey(date)) { + groupedHistoryMap[date]!.add(element); + } else { + groupedHistoryMap[date] = [element]; + } + }); + + groupedHistoryMap.forEach((key, value) { + groupedHistory.add(MapEntry(key, value)); + }); + + // sort by date (most recent first) + groupedHistory.sort((a, b) => b.key.compareTo(a.key)); + + return groupedHistory; + } + + /// method that converts history into a list grouped by minute + List>> getHistoryGroupedByHour() { + final groupedHistory = >>[]; + + final groupedHistoryMap = >{}; + + _history.forEach((element) { + final date = DateTime( + element.startTime.year, + element.startTime.month, + element.startTime.day, + element.startTime.hour, + ); + + if (groupedHistoryMap.containsKey(date)) { + groupedHistoryMap[date]!.add(element); + } else { + groupedHistoryMap[date] = [element]; + } + }); + + groupedHistoryMap.forEach((key, value) { + groupedHistory.add(MapEntry(key, value)); + }); + + // sort by minute (most recent first) + groupedHistory.sort((a, b) => b.key.compareTo(a.key)); + + return groupedHistory; + } + + + //TODO handle events that don't change the current track (e.g. pause, seek, etc.) void updateCurrentTrack(QueueItem? currentTrack) { From 6ef5f0028889b357b23c37122aa544c1d164b66d Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 21 Sep 2023 14:46:26 +0200 Subject: [PATCH 070/130] improved BaseItemDto favorite state handling - try to update the actual QueueItem wherever possible to be robust against rebuilds - added a callback to `FavouriteButton` to support updating e.g. the QueueItems in response to a toggle - the favorite buttons on the player screen and in the queue list are now synced --- lib/components/PlayerScreen/queue_list.dart | 71 +++++++++++-------- lib/components/PlayerScreen/song_info.dart | 53 +++++++------- .../PlayerScreen/song_name_content.dart | 23 ++++-- lib/components/favourite_button.dart | 19 +++-- lib/services/queue_service.dart | 17 +++++ 5 files changed, 117 insertions(+), 66 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index ca054f138..3d4e87201 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -651,6 +651,13 @@ class _CurrentTrackState extends State { topLeft: Radius.circular(8), bottomLeft: Radius.circular(8), ), + itemsToPrecache: _queueService.getNextXTracksInQueue(3).map((e) { + final item = e.item.extras?["itemJson"] != null + ? jellyfin_models.BaseItemDto.fromJson( + e.item.extras!["itemJson"] as Map) + : null; + return item!; + }).toList(), ), Container( width: 70, @@ -836,28 +843,31 @@ class _CurrentTrackState extends State { // ), ], ), - IconButton( - iconSize: 16, - visualDensity: const VisualDensity(horizontal: -4), - icon: jellyfin_models.BaseItemDto.fromJson(currentTrack!.item.extras?["itemJson"]).userData!.isFavorite ? const Icon( - TablerIcons.heart, - size: 28, - color: Colors.white, - fill: 1.0, - weight: - 1.5, //TODO weight not working, stroke is too thick for most icons - ) : const Icon( - TablerIcons.heart, - size: 28, - color: Colors.white, - weight: - 1.5, //TODO weight not working, stroke is too thick for most icons + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: IconButton( + iconSize: 16, + visualDensity: const VisualDensity(horizontal: -4), + icon: jellyfin_models.BaseItemDto.fromJson(currentTrack!.item.extras?["itemJson"]).userData!.isFavorite ? Icon( + Icons.favorite, + size: 28, + color: IconTheme.of(context).color!, + fill: 1.0, + weight: + 1.5, //TODO weight not working, stroke is too thick for most icons + ) : const Icon( + Icons.favorite_outline, + size: 28, + color: Colors.white, + weight: + 1.5, //TODO weight not working, stroke is too thick for most icons + ), + onPressed: () => { + setState(() { + setFavourite(currentTrack!); + }) + }, ), - onPressed: () => { - setState(() { - setFavourite(jellyfin_models.BaseItemDto.fromJson(currentTrack!.item.extras?["itemJson"])); - }) - }, ), IconButton( iconSize: 28, @@ -1052,18 +1062,20 @@ class _CurrentTrackState extends State { break; case SongListTileMenuItems.addFavourite: case SongListTileMenuItems.removeFavourite: - await setFavourite(item); + await setFavourite(currentTrack); break; case null: break; } } - Future setFavourite(jellyfin_models.BaseItemDto item) async { + Future setFavourite(QueueItem track) async { try { // We switch the widget state before actually doing the request to // make the app feel faster (without, there is a delay from the // user adding the favourite and the icon showing) + jellyfin_models.BaseItemDto item = jellyfin_models.BaseItemDto.fromJson(track.item.extras!["itemJson"]); + setState(() { item.userData!.isFavorite = !item.userData!.isFavorite; }); @@ -1074,15 +1086,18 @@ class _CurrentTrackState extends State { ? await _jellyfinApiHelper.addFavourite(item.id) : await _jellyfinApiHelper.removeFavourite(item.id); - if (!mounted) return; + item.userData = newUserData; + + if (!mounted) return; setState(() { - item.userData = newUserData; + //!!! update the QueueItem with the new BaseItemDto, then trigger a rebuild of the widget with the current snapshot (**which includes the modified QueueItem**) + track.item.extras!["itemJson"] = item.toJson(); }); + + _queueService.refreshQueueStream(); + } catch (e) { - setState(() { - item.userData!.isFavorite = !item.userData!.isFavorite; - }); errorSnackbar(e, context); } } diff --git a/lib/components/PlayerScreen/song_info.dart b/lib/components/PlayerScreen/song_info.dart index d408ddb52..4c0a28fe2 100644 --- a/lib/components/PlayerScreen/song_info.dart +++ b/lib/components/PlayerScreen/song_info.dart @@ -1,6 +1,8 @@ import 'dart:math'; import 'package:audio_service/audio_service.dart'; +import 'package:finamp/models/finamp_models.dart'; +import 'package:finamp/services/queue_service.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -9,7 +11,7 @@ import 'package:get_it/get_it.dart'; import 'package:palette_generator/palette_generator.dart'; import '../../generate_material_color.dart'; -import '../../models/jellyfin_models.dart'; +import '../../models/jellyfin_models.dart' as jellyfin_models; import '../../screens/artist_screen.dart'; import '../../services/current_album_image_provider.dart'; import '../../services/finamp_settings_helper.dart'; @@ -32,22 +34,18 @@ class SongInfo extends StatefulWidget { class _SongInfoState extends State { final audioHandler = GetIt.instance(); final jellyfinApiHelper = GetIt.instance(); + final queueService = GetIt.instance(); @override Widget build(BuildContext context) { - return StreamBuilder( - stream: audioHandler.mediaItem, - initialData: MediaItem( - id: "", - title: AppLocalizations.of(context)!.noItem, - album: AppLocalizations.of(context)!.noAlbum, - artist: AppLocalizations.of(context)!.noArtist, - ), + return StreamBuilder( + stream: queueService.getQueueStream(), builder: (context, snapshot) { - final mediaItem = snapshot.data!; + final currentTrack = snapshot.data!.currentTrack!; + final mediaItem = currentTrack.item; final songBaseItemDto = (mediaItem.extras?.containsKey("itemJson") ?? false) - ? BaseItemDto.fromJson(mediaItem.extras!["itemJson"]) + ? jellyfin_models.BaseItemDto.fromJson(mediaItem.extras!["itemJson"]) : null; List separatedArtistTextSpans = []; @@ -98,11 +96,10 @@ class _SongInfoState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _PlayerScreenAlbumImage(item: songBaseItemDto), + _PlayerScreenAlbumImage(queueItem: currentTrack!), const Padding(padding: EdgeInsets.symmetric(vertical: 6)), SongNameContent( - songBaseItemDto: songBaseItemDto, - mediaItem: mediaItem, + currentTrack: currentTrack, separatedArtistTextSpans: separatedArtistTextSpans, secondaryTextColour: secondaryTextColour, ) @@ -114,16 +111,22 @@ class _SongInfoState extends State { } class _PlayerScreenAlbumImage extends ConsumerWidget { - const _PlayerScreenAlbumImage({ + _PlayerScreenAlbumImage({ Key? key, - required this.item, + required this.queueItem, }) : super(key: key); - final BaseItemDto? item; + final QueueItem queueItem; @override Widget build(BuildContext context, WidgetRef ref) { final audioHandler = GetIt.instance(); + final queueService = GetIt.instance(); + + final item = queueItem.item.extras?["itemJson"] != null + ? jellyfin_models.BaseItemDto.fromJson( + queueItem.item.extras!["itemJson"] as Map) + : null; return Container( decoration: BoxDecoration( @@ -146,16 +149,16 @@ class _PlayerScreenAlbumImage extends ConsumerWidget { padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 40), child: AlbumImage( item: item, - // Here we awkwardly get the next 3 queue items so that we + // Here we get the next 3 queue items so that we // can precache them (so that the image is already loaded // when the next song comes on). - itemsToPrecache: audioHandler.queue.value - .sublist(min( - (audioHandler.playbackState.value.queueIndex ?? 0) + 1, - audioHandler.queue.value.length)) - .take(3) - .map((e) => BaseItemDto.fromJson(e.extras!["itemJson"])) - .toList(), + itemsToPrecache: queueService.getNextXTracksInQueue(3).map((e) { + final item = e.item.extras?["itemJson"] != null + ? jellyfin_models.BaseItemDto.fromJson( + e.item.extras!["itemJson"] as Map) + : null; + return item!; + }).toList(), // We need a post frame callback because otherwise this // widget rebuilds on the same frame imageProviderCallback: (imageProvider) => diff --git a/lib/components/PlayerScreen/song_name_content.dart b/lib/components/PlayerScreen/song_name_content.dart index 6e30a139c..ae4143c71 100644 --- a/lib/components/PlayerScreen/song_name_content.dart +++ b/lib/components/PlayerScreen/song_name_content.dart @@ -1,6 +1,7 @@ import 'package:audio_service/audio_service.dart'; import 'package:finamp/components/PlayerScreen/player_buttons_more.dart'; -import 'package:finamp/models/jellyfin_models.dart'; +import 'package:finamp/models/finamp_models.dart'; +import 'package:finamp/models/jellyfin_models.dart' as jellyfin_models; import 'package:flutter/material.dart'; import '../favourite_button.dart'; @@ -10,18 +11,20 @@ import 'artist_chip.dart'; class SongNameContent extends StatelessWidget { const SongNameContent( {Key? key, - required this.songBaseItemDto, - required this.mediaItem, + required this.currentTrack, required this.separatedArtistTextSpans, required this.secondaryTextColour}) : super(key: key); - final BaseItemDto? songBaseItemDto; - final MediaItem mediaItem; + final QueueItem currentTrack; final List separatedArtistTextSpans; final Color? secondaryTextColour; @override Widget build(BuildContext context) { + + final jellyfin_models.BaseItemDto? songBaseItemDto = currentTrack.item.extras!["itemJson"] != null + ? jellyfin_models.BaseItemDto.fromJson(currentTrack.item.extras!["itemJson"]) : null; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -29,7 +32,7 @@ class SongNameContent extends StatelessWidget { child: Padding( padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), child: Text( - mediaItem.title, + currentTrack.item.title, textAlign: TextAlign.center, style: const TextStyle( fontSize: 20, @@ -75,7 +78,13 @@ class SongNameContent extends StatelessWidget { ), ], ), - FavoriteButton(item: songBaseItemDto), + FavoriteButton( + item: songBaseItemDto, + onToggle: (isFavorite) { + songBaseItemDto!.userData!.isFavorite = isFavorite; + currentTrack.item.extras!["itemJson"] = songBaseItemDto.toJson(); + }, + ), ], ), ), diff --git a/lib/components/favourite_button.dart b/lib/components/favourite_button.dart index 1e20a41eb..abaed8384 100644 --- a/lib/components/favourite_button.dart +++ b/lib/components/favourite_button.dart @@ -7,14 +7,16 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; class FavoriteButton extends StatefulWidget { - const FavoriteButton( - {Key? key, - required this.item, - this.onlyIfFav = false, - this.inPlayer = false}) - : super(key: key); + const FavoriteButton({ + Key? key, + required this.item, + this.onToggle, + this.onlyIfFav = false, + this.inPlayer = false, + }) : super(key: key); final BaseItemDto? item; + final void Function(bool isFavorite)? onToggle; final bool onlyIfFav; final bool inPlayer; @@ -64,6 +66,11 @@ class _FavoriteButtonState extends State { widget.item!.toJson(); } }); + + if (widget.onToggle != null) { + widget.onToggle!(widget.item!.userData!.isFavorite); + } + } catch (e) { errorSnackbar(e, context); } diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 22b841c94..5ced2b2c1 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -382,6 +382,23 @@ class QueueService { return _queueStream; } + void refreshQueueStream() { + _queueStream.add(getQueue()); + } + + /// returns the next [amount] QueueItems from Next Up and the regular queue + List getNextXTracksInQueue(int amount) { + List nextTracks = []; + if (_queueNextUp.isNotEmpty) { + nextTracks.addAll(_queueNextUp.sublist(0, min(amount, _queueNextUp.length))); + amount -= _queueNextUp.length; + } + if (_queue.isNotEmpty && amount > 0) { + nextTracks.addAll(_queue.sublist(0, min(amount, _queue.length))); + } + return nextTracks; + } + BehaviorSubject getPlaybackOrderStream() { return _playbackOrderStream; } From 3efde88e0c539e48c29edb7d811ff344ca861ed1 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 21 Sep 2023 17:50:25 +0200 Subject: [PATCH 071/130] added localized strings --- ...bum_screen_content_flexible_space_bar.dart | 29 +++-- .../AlbumScreen/song_list_tile.dart | 20 ++- lib/components/MusicScreen/album_item.dart | 58 +++++---- .../MusicScreen/artist_item_list_tile.dart | 17 +-- .../playback_history_list_tile.dart | 15 +-- .../player_screen_appbar_title.dart | 7 +- lib/components/PlayerScreen/queue_list.dart | 87 +++---------- .../PlayerScreen/queue_list_item.dart | 18 ++- lib/l10n/app_en.arb | 104 +++++++++++++++- lib/models/finamp_models.dart | 64 ++++++++-- lib/screens/playback_history_screen.dart | 2 +- lib/services/audio_service_helper.dart | 114 ++---------------- lib/services/queue_service.dart | 31 +++-- 13 files changed, 279 insertions(+), 287 deletions(-) diff --git a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart index da558b5c0..12073866e 100644 --- a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart +++ b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart @@ -25,7 +25,6 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { @override Widget build(BuildContext context) { - AudioServiceHelper audioServiceHelper = GetIt.instance(); QueueService queueService = GetIt.instance(); @@ -36,7 +35,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { items: items, source: QueueItemSource( type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, - name: parentItem.name ?? "Somewhere", + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, ) @@ -49,7 +48,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { items: items, source: QueueItemSource( type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, - name: parentItem.name ?? "Somewhere", + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, ) @@ -62,14 +61,14 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { items: items, source: QueueItemSource( type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, - name: parentItem.name ?? "Somewhere", + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, ) ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text("Added to Next Up"), + content: Text(AppLocalizations.of(context)!.confirmAddToNextUp("album")), ), ); @@ -81,14 +80,14 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { items: items, source: QueueItemSource( type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, - name: parentItem.name ?? "Somewhere", + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, ) ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text("Will play next"), + content: Text(AppLocalizations.of(context)!.confirmPlayNext("album")), ), ); } @@ -102,14 +101,14 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { items: clonedItems, source: QueueItemSource( type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, - name: parentItem.name ?? "Somewhere", + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, ) ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text("Shuffled to Next Up"), + content: Text(AppLocalizations.of(context)!.confirmShuffleToNextUp), ), ); } @@ -123,14 +122,14 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { items: clonedItems, source: QueueItemSource( type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, - name: parentItem.name ?? "Somewhere", + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, ) ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text("Will shuffle next"), + content: Text(AppLocalizations.of(context)!.confirmShuffleNext), ), ); } @@ -191,7 +190,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { onPressed: () => addAlbumNext(), icon: const Icon(Icons.hourglass_bottom), label: - Text("Play Next"), + Text(AppLocalizations.of(context)!.playNext), ), ), const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), @@ -199,7 +198,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { child: ElevatedButton.icon( onPressed: () => addAlbumToNextUp(), icon: const Icon(Icons.hourglass_top), - label: Text("Add to Next Up"), + label: Text(AppLocalizations.of(context)!.addToNextUp), ), ), ]), @@ -209,7 +208,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { onPressed: () => shuffleAlbumNext(), icon: const Icon(Icons.hourglass_bottom), label: - Text("Shuffle Next"), + Text(AppLocalizations.of(context)!.shuffleNext), ), ), const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), @@ -217,7 +216,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { child: ElevatedButton.icon( onPressed: () => shuffleAlbumToNextUp(), icon: const Icon(Icons.hourglass_top), - label: Text("Shuffle to Next Up"), + label: Text(AppLocalizations.of(context)!.shuffleToNextUp), ), ), ]), diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index 4b7b7654c..bce804db5 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -193,7 +193,7 @@ class _SongListTileState extends State { items: widget.children!, source: QueueItemSource( type: widget.isInPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, - name: (widget.isInPlaylist ? widget.parentName : widget.item.album) ?? "Somewhere", + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: (widget.isInPlaylist ? widget.parentName : widget.item.album) ?? AppLocalizations.of(context)!.placeholderSource), id: widget.parentId ?? "", item: widget.item, ), @@ -247,14 +247,14 @@ class _SongListTileState extends State { value: SongListTileMenuItems.playNext, child: ListTile( leading: const Icon(TablerIcons.hourglass_low), - title: Text("Play next"), + title: Text(AppLocalizations.of(context)!.playNext), ), ), PopupMenuItem( value: SongListTileMenuItems.addToNextUp, child: ListTile( leading: const Icon(TablerIcons.hourglass_high), - title: Text("Add to Next Up"), + title: Text(AppLocalizations.of(context)!.addToNextUp), ), ), PopupMenuItem( @@ -326,8 +326,7 @@ class _SongListTileState extends State { switch (selection) { case SongListTileMenuItems.addToQueue: - // await _audioServiceHelper.addQueueItem(widget.item); - await _queueService.addToQueue(widget.item, QueueItemSource(type: QueueItemSourceType.unknown, name: "Queue", id: widget.parentId ?? "unknown")); + await _queueService.addToQueue(widget.item, QueueItemSource(type: QueueItemSourceType.unknown, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: AppLocalizations.of(context)!.queue), id: widget.parentId ?? "unknown")); if (!mounted) return; @@ -337,31 +336,29 @@ class _SongListTileState extends State { break; case SongListTileMenuItems.playNext: - // await _audioServiceHelper.addQueueItem(widget.item); await _queueService.addNext(items: [widget.item]); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text("Track will play next"), + content: Text(AppLocalizations.of(context)!.confirmPlayNext("track")), )); break; case SongListTileMenuItems.addToNextUp: - // await _audioServiceHelper.addQueueItem(widget.item); await _queueService.addToNextUp(items: [widget.item]); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text("Added track to Next Up"), + content: Text(AppLocalizations.of(context)!.confirmAddToNextUp("track")), )); break; case SongListTileMenuItems.replaceQueueWithItem: // await _audioServiceHelper // .replaceQueueWithItem(itemList: [widget.item]); - await _queueService.startPlayback(items: [widget.item], source: QueueItemSource(type: QueueItemSourceType.unknown, name: "Queue", id: widget.parentId ?? "unknown")); + await _queueService.startPlayback(items: [widget.item], source: QueueItemSource(type: QueueItemSourceType.unknown, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: AppLocalizations.of(context)!.queue), id: widget.parentId ?? "unknown")); if (!mounted) return; @@ -477,8 +474,7 @@ class _SongListTileState extends State { ), ), confirmDismiss: (direction) async { - // await _audioServiceHelper.addQueueItem(widget.item); - await _queueService.addToQueue(widget.item, QueueItemSource(type: QueueItemSourceType.unknown, name: "Queue", id: widget.parentId!)); + await _queueService.addToQueue(widget.item, QueueItemSource(type: QueueItemSourceType.unknown, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: AppLocalizations.of(context)!.queue), id: widget.parentId!)); if (!mounted) return false; diff --git a/lib/components/MusicScreen/album_item.dart b/lib/components/MusicScreen/album_item.dart index 97f888eb1..9ea86e5c2 100644 --- a/lib/components/MusicScreen/album_item.dart +++ b/lib/components/MusicScreen/album_item.dart @@ -74,6 +74,8 @@ class _AlbumItemState extends State { GetIt.instance(); late Function() onTap; + late AppLocalizations local; + late ScaffoldMessengerState messenger; @override void initState() { @@ -96,6 +98,10 @@ class _AlbumItemState extends State { @override Widget build(BuildContext context) { + + local = AppLocalizations.of(context)!; + messenger = ScaffoldMessenger.of(context); + final screenSize = MediaQuery.of(context).size; return Padding( @@ -129,14 +135,14 @@ class _AlbumItemState extends State { child: ListTile( leading: const Icon(Icons.favorite_border), title: - Text(AppLocalizations.of(context)!.removeFavourite), + Text(local.removeFavourite), ), ) : PopupMenuItem<_AlbumListTileMenuItems>( value: _AlbumListTileMenuItems.addFavourite, child: ListTile( leading: const Icon(Icons.favorite), - title: Text(AppLocalizations.of(context)!.addFavourite), + title: Text(local.addFavourite), ), ), jellyfinApiHelper.selectedMixAlbums.contains(mutableAlbum.id) @@ -145,14 +151,14 @@ class _AlbumItemState extends State { child: ListTile( leading: const Icon(Icons.explore_off), title: - Text(AppLocalizations.of(context)!.removeFromMix), + Text(local.removeFromMix), ), ) : PopupMenuItem<_AlbumListTileMenuItems>( value: _AlbumListTileMenuItems.addToMixList, child: ListTile( leading: const Icon(Icons.explore), - title: Text(AppLocalizations.of(context)!.addToMix), + title: Text(local.addToMix), ), ), if (_queueService.getQueue().nextUp.isNotEmpty) @@ -161,7 +167,7 @@ class _AlbumItemState extends State { child: ListTile( leading: const Icon(Icons.hourglass_bottom), title: - Text("Play Next"), + Text(local.playNext), ), ), PopupMenuItem<_AlbumListTileMenuItems>( @@ -169,7 +175,7 @@ class _AlbumItemState extends State { child: ListTile( leading: const Icon(Icons.hourglass_top), title: - Text("Add to Next Up"), + Text(local.addToNextUp), ), ), if (_queueService.getQueue().nextUp.isNotEmpty) @@ -178,7 +184,7 @@ class _AlbumItemState extends State { child: ListTile( leading: const Icon(Icons.hourglass_bottom), title: - Text("Shuffle Next"), + Text(local.shuffleNext), ), ), PopupMenuItem<_AlbumListTileMenuItems>( @@ -186,7 +192,7 @@ class _AlbumItemState extends State { child: ListTile( leading: const Icon(Icons.hourglass_top), title: - Text("Shuffle to Next Up"), + Text(local.shuffleToNextUp), ), ), ], @@ -206,7 +212,7 @@ class _AlbumItemState extends State { mutableAlbum.userData = newUserData; }); - ScaffoldMessenger.of(context).showSnackBar( + messenger.showSnackBar( const SnackBar(content: Text("Favourite added."))); } catch (e) { errorSnackbar(e, context); @@ -222,7 +228,7 @@ class _AlbumItemState extends State { setState(() { mutableAlbum.userData = newUserData; }); - ScaffoldMessenger.of(context).showSnackBar( + messenger.showSnackBar( const SnackBar(content: Text("Favourite removed."))); } catch (e) { errorSnackbar(e, context); @@ -252,7 +258,7 @@ class _AlbumItemState extends State { ); if (albumTracks == null) { - ScaffoldMessenger.of(context).showSnackBar( + messenger.showSnackBar( SnackBar( content: Text("Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), ), @@ -264,15 +270,15 @@ class _AlbumItemState extends State { items: albumTracks, source: QueueItemSource( type: QueueItemSourceType.album, - name: mutableAlbum.name ?? "Somewhere", + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: mutableAlbum.name ?? local.placeholderSource), id: mutableAlbum.id, item: mutableAlbum, ) ); - ScaffoldMessenger.of(context).showSnackBar( + messenger.showSnackBar( SnackBar( - content: Text("${widget.isPlaylist ? "Playlist" : "Album"} will play next."), + content: Text(local.confirmPlayNext(widget.isPlaylist ? "playlist" : "album")), ), ); @@ -289,7 +295,7 @@ class _AlbumItemState extends State { ); if (albumTracks == null) { - ScaffoldMessenger.of(context).showSnackBar( + messenger.showSnackBar( SnackBar( content: Text("Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), ), @@ -301,15 +307,15 @@ class _AlbumItemState extends State { items: albumTracks, source: QueueItemSource( type: QueueItemSourceType.album, - name: mutableAlbum.name ?? "Somewhere", + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: mutableAlbum.name ?? local.placeholderSource), id: mutableAlbum.id, item: mutableAlbum, ) ); - ScaffoldMessenger.of(context).showSnackBar( + messenger.showSnackBar( SnackBar( - content: Text("Added ${widget.isPlaylist ? "playlist" : "album"} to Next Up."), + content: Text(local.confirmAddToNextUp(widget.isPlaylist ? "playlist" : "album")), ), ); @@ -327,7 +333,7 @@ class _AlbumItemState extends State { ); if (albumTracks == null) { - ScaffoldMessenger.of(context).showSnackBar( + messenger.showSnackBar( SnackBar( content: Text("Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), ), @@ -339,15 +345,15 @@ class _AlbumItemState extends State { items: albumTracks, source: QueueItemSource( type: QueueItemSourceType.album, - name: mutableAlbum.name ?? "Somewhere", + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: mutableAlbum.name ?? local.placeholderSource), id: mutableAlbum.id, item: mutableAlbum, ) ); - ScaffoldMessenger.of(context).showSnackBar( + messenger.showSnackBar( SnackBar( - content: Text("${widget.isPlaylist ? "Playlist" : "Album"} will shuffle next."), + content: Text(local.confirmPlayNext(widget.isPlaylist ? "playlist" : "album")), ), ); @@ -365,7 +371,7 @@ class _AlbumItemState extends State { ); if (albumTracks == null) { - ScaffoldMessenger.of(context).showSnackBar( + messenger.showSnackBar( SnackBar( content: Text("Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), ), @@ -377,15 +383,15 @@ class _AlbumItemState extends State { items: albumTracks, source: QueueItemSource( type: QueueItemSourceType.album, - name: mutableAlbum.name ?? "Somewhere", + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: mutableAlbum.name ?? local.placeholderSource), id: mutableAlbum.id, item: mutableAlbum, ) ); - ScaffoldMessenger.of(context).showSnackBar( + messenger.showSnackBar( SnackBar( - content: Text("Shuffled ${widget.isPlaylist ? "playlist" : "album"} to Next Up."), + content: Text(local.confirmShuffleToNextUp), ), ); diff --git a/lib/components/MusicScreen/artist_item_list_tile.dart b/lib/components/MusicScreen/artist_item_list_tile.dart index 6bb2fa230..cb59f8b54 100644 --- a/lib/components/MusicScreen/artist_item_list_tile.dart +++ b/lib/components/MusicScreen/artist_item_list_tile.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../models/jellyfin_models.dart'; import '../../screens/artist_screen.dart'; @@ -72,18 +73,18 @@ class _ArtistListTileState extends State { ), items: [ mutableItem.userData!.isFavorite - ? const PopupMenuItem( + ? PopupMenuItem( value: ArtistListTileMenuItems.removeFromFavourite, child: ListTile( - leading: Icon(Icons.favorite_border), - title: Text("Remove Favourite"), + leading: const Icon(Icons.favorite_border), + title: Text(AppLocalizations.of(context)!.removeFavourite), ), ) - : const PopupMenuItem( + : PopupMenuItem( value: ArtistListTileMenuItems.addToFavourite, child: ListTile( - leading: Icon(Icons.favorite), - title: Text("Add Favourite"), + leading: const Icon(Icons.favorite), + title: Text(AppLocalizations.of(context)!.addFavourite), ), ), _jellyfinApiHelper.selectedMixArtists.contains(mutableItem.id) @@ -92,7 +93,7 @@ class _ArtistListTileState extends State { value: ArtistListTileMenuItems.removeFromMixList, child: ListTile( leading: const Icon(Icons.explore_off), - title: const Text("Remove From Mix"), + title: Text(AppLocalizations.of(context)!.removeFromMix), enabled: isOffline ? false : true, ), ) @@ -101,7 +102,7 @@ class _ArtistListTileState extends State { enabled: !isOffline, child: ListTile( leading: const Icon(Icons.explore), - title: const Text("Add To Mix"), + title: Text(AppLocalizations.of(context)!.addToMix), enabled: !isOffline, ), ), diff --git a/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart b/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart index b6c3c5400..2fb2fa296 100644 --- a/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart +++ b/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart @@ -170,14 +170,14 @@ class _PlaybackHistoryListTileState extends State { value: SongListTileMenuItems.playNext, child: ListTile( leading: const Icon(TablerIcons.hourglass_low), - title: Text("Play next"), + title: Text(AppLocalizations.of(context)!.playNext), ), ), PopupMenuItem( value: SongListTileMenuItems.addToNextUp, child: ListTile( leading: const Icon(TablerIcons.hourglass_high), - title: Text("Add to Next Up"), + title: Text(AppLocalizations.of(context)!.addToNextUp), ), ), PopupMenuItem( @@ -232,14 +232,13 @@ class _PlaybackHistoryListTileState extends State { switch (selection) { case SongListTileMenuItems.addToQueue: - // await _audioServiceHelper.addQueueItem(jellyfin_models.BaseItemDto.fromJson(widget.item.item.item.extras?["itemJson"])); await widget._queueService.addToQueue( jellyfin_models.BaseItemDto.fromJson( widget.item.item.item.extras?["itemJson"]), QueueItemSource( type: QueueItemSourceType.unknown, - name: "Queue", - id: widget.item.item.source.id ?? "unknown")); + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: AppLocalizations.of(context)!.queue), + id: widget.item.item.source.id)); if (!mounted) return; @@ -249,26 +248,24 @@ class _PlaybackHistoryListTileState extends State { break; case SongListTileMenuItems.playNext: - // await _audioServiceHelper.addQueueItem(jellyfin_models.BaseItemDto.fromJson(widget.item.item.item.extras?["itemJson"])); await widget._queueService.addNext(items: [jellyfin_models.BaseItemDto.fromJson( widget.item.item.item.extras?["itemJson"])]); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text("Track will play next"), + content: Text(AppLocalizations.of(context)!.confirmPlayNext("track")), )); break; case SongListTileMenuItems.addToNextUp: - // await _audioServiceHelper.addQueueItem(jellyfin_models.BaseItemDto.fromJson(widget.item.item.item.extras?["itemJson"])); await widget._queueService.addToNextUp(items: [jellyfin_models.BaseItemDto.fromJson( widget.item.item.item.extras?["itemJson"])]); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text("Added track to Next Up"), + content: Text(AppLocalizations.of(context)!.confirmAddToNextUp("track")), )); break; diff --git a/lib/components/PlayerScreen/player_screen_appbar_title.dart b/lib/components/PlayerScreen/player_screen_appbar_title.dart index 705e59f96..6ac4bd776 100644 --- a/lib/components/PlayerScreen/player_screen_appbar_title.dart +++ b/lib/components/PlayerScreen/player_screen_appbar_title.dart @@ -43,8 +43,7 @@ class _PlayerScreenAppBarTitleState extends State { onTap: () => navigateToSource(context, queueItem.source), child: Column( children: [ - Text( - "Playing From ${queueItem.source.type.name}", + Text(AppLocalizations.of(context)!.playingFromType(queueItem.source.type.name.toLowerCase()), style: TextStyle( fontSize: 12, fontWeight: FontWeight.w300, @@ -53,7 +52,7 @@ class _PlayerScreenAppBarTitleState extends State { ), const Padding(padding: EdgeInsets.symmetric(vertical: 2)), Text( - queueItem.source.name, + queueItem.source.name.getLocalized(context), style: const TextStyle( fontSize: 16, color: Colors.white, @@ -103,7 +102,7 @@ void navigateToSource(BuildContext context, QueueItemSource source) async { case QueueItemSourceType.unknown: break; case QueueItemSourceType.favorites: - case QueueItemSourceType.itemMix: + case QueueItemSourceType.songMix: case QueueItemSourceType.filteredList: case QueueItemSourceType.downloads: default: diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 3d4e87201..acd71cfaf 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -111,8 +111,8 @@ class _QueueListState extends State { leading: const AlbumImage( item: null, ), - title: Text("Unknown song"), - subtitle: Text("Unknown artist"), + title: const Text("unknown"), + subtitle: const Text("unknown"), onTap: () {}), ), @@ -171,9 +171,9 @@ class _QueueListState extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Padding( - padding: EdgeInsets.only(top: 2.0), - child: Text("Recently Played"), + Padding( + padding: const EdgeInsets.only(top: 2.0), + child: Text(AppLocalizations.of(context)!.recentlyPlayed), ), const SizedBox(width: 4.0), Icon( @@ -202,7 +202,7 @@ class _QueueListState extends State { padding: const EdgeInsets.only(top: 20.0, bottom: 0.0), sliver: SliverPersistentHeader( delegate: SectionHeaderDelegate( - title: const Text("Next Up"), + title: Text(AppLocalizations.of(context)!.nextUp), height: 30.0, nextUpHeaderKey: widget.nextUpHeaderKey, ), // _source != null ? "Playing from ${_source?.name}" : "Queue", @@ -220,8 +220,8 @@ class _QueueListState extends State { delegate: SectionHeaderDelegate( title: Row( children: [ - const Text("Playing from "), - Text(_source?.name ?? "Unknown", + Text("${AppLocalizations.of(context)!.playingFrom} "), + Text(_source?.name.getLocalized(context) ?? AppLocalizations.of(context)!.unknownName, style: const TextStyle(fontWeight: FontWeight.w500)), ], ), @@ -296,8 +296,8 @@ Future showQueueBottomSheet(BuildContext context) { ), ), const SizedBox(height: 10), - const Text("Queue", - style: TextStyle( + Text(AppLocalizations.of(context)!.queue, + style: const TextStyle( color: Colors.white, fontFamily: 'Lexend Deca', fontSize: 18, @@ -726,7 +726,7 @@ class _CurrentTrackState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - currentTrack?.item.title ?? 'Unknown', + currentTrack?.item.title ?? AppLocalizations.of(context)!.unknownName, style: const TextStyle( color: Colors.white, fontSize: 16, @@ -797,52 +797,6 @@ class _CurrentTrackState extends State { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - // Text( - // // '0:00', - // playbackPosition!.inHours >= 1.0 - // ? "${playbackPosition?.inHours.toString()}:${((playbackPosition?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" - // : "${playbackPosition?.inMinutes.toString()}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - // style: TextStyle( - // color: Colors.white.withOpacity(0.8), - // fontSize: 14, - // fontFamily: 'Lexend Deca', - // fontWeight: FontWeight.w400, - // ), - // ), - // const SizedBox(width: 2), - // Text( - // '/', - // style: TextStyle( - // color: Colors.white.withOpacity(0.8), - // fontSize: 14, - // fontFamily: 'Lexend Deca', - // fontWeight: FontWeight.w400, - // ), - // ), - // const SizedBox(width: 2), - // Text( - // // '3:44', - // (mediaState?.mediaItem?.duration - // ?.inHours ?? - // 0.0) >= - // 1.0 - // ? "${mediaState?.mediaItem?.duration?.inHours.toString()}:${((mediaState?.mediaItem?.duration?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((mediaState?.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" - // : "${mediaState?.mediaItem?.duration?.inMinutes.toString()}:${((mediaState?.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - // style: TextStyle( - // color: Colors.white.withOpacity(0.8), - // fontSize: 14, - // fontFamily: 'Lexend Deca', - // fontWeight: FontWeight.w400, - // ), - // ), - ], - ), Padding( padding: const EdgeInsets.only(top: 4.0), child: IconButton( @@ -854,13 +808,13 @@ class _CurrentTrackState extends State { color: IconTheme.of(context).color!, fill: 1.0, weight: - 1.5, //TODO weight not working, stroke is too thick for most icons + 1.5, ) : const Icon( Icons.favorite_outline, size: 28, color: Colors.white, weight: - 1.5, //TODO weight not working, stroke is too thick for most icons + 1.5, ), onPressed: () => { setState(() { @@ -926,14 +880,14 @@ class _CurrentTrackState extends State { value: SongListTileMenuItems.playNext, child: ListTile( leading: const Icon(TablerIcons.hourglass_low), - title: Text("Play next"), + title: Text(AppLocalizations.of(context)!.playNext), ), ), PopupMenuItem( value: SongListTileMenuItems.addToNextUp, child: ListTile( leading: const Icon(TablerIcons.hourglass_high), - title: Text("Add to Next Up"), + title: Text(AppLocalizations.of(context)!.addToNextUp), ), ), PopupMenuItem( @@ -985,13 +939,12 @@ class _CurrentTrackState extends State { switch (selection) { case SongListTileMenuItems.addToQueue: - // await _audioServiceHelper.addQueueItem(item); await _queueService.addToQueue( item, QueueItemSource( type: QueueItemSourceType.unknown, - name: "Queue", - id: currentTrack.source.id ?? "unknown")); + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: AppLocalizations.of(context)!.queue), + id: currentTrack.source.id)); if (!mounted) return; @@ -1001,24 +954,22 @@ class _CurrentTrackState extends State { break; case SongListTileMenuItems.playNext: - // await _audioServiceHelper.addQueueItem(item); await _queueService.addNext(items: [item]); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text("Track will play next"), + content: Text(AppLocalizations.of(context)!.confirmPlayNext("track")), )); break; case SongListTileMenuItems.addToNextUp: - // await _audioServiceHelper.addQueueItem(item); await _queueService.addToNextUp(items: [item]); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text("Added track to Next Up"), + content: Text(AppLocalizations.of(context)!.confirmAddToNextUp("track")), )); break; diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index 5fc557419..c7d969783 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -95,8 +95,7 @@ class _QueueListItemState extends State { Padding( padding: const EdgeInsets.all(0.0), child: Text( - widget.item.item.title ?? - AppLocalizations.of(context)!.unknownName, + widget.item.item.title, style: this.widget.isCurrentTrack ? TextStyle( color: Theme.of(context).colorScheme.secondary, @@ -239,14 +238,14 @@ class _QueueListItemState extends State { value: SongListTileMenuItems.playNext, child: ListTile( leading: const Icon(TablerIcons.hourglass_low), - title: Text("Play next"), + title: Text(AppLocalizations.of(context)!.playNext), ), ), PopupMenuItem( value: SongListTileMenuItems.addToNextUp, child: ListTile( leading: const Icon(TablerIcons.hourglass_high), - title: Text("Add to Next Up"), + title: Text(AppLocalizations.of(context)!.addToNextUp), ), ), PopupMenuItem( @@ -301,14 +300,13 @@ class _QueueListItemState extends State { switch (selection) { case SongListTileMenuItems.addToQueue: - // await _audioServiceHelper.addQueueItem(jellyfin_models.BaseItemDto.fromJson(widget.item.item.extras?["itemJson"])); await _queueService.addToQueue( jellyfin_models.BaseItemDto.fromJson( widget.item.item.extras?["itemJson"]), QueueItemSource( type: QueueItemSourceType.unknown, - name: "Queue", - id: widget.item.source.id ?? "unknown")); + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: AppLocalizations.of(context)!.queue), + id: widget.item.source.id)); if (!mounted) return; @@ -318,26 +316,24 @@ class _QueueListItemState extends State { break; case SongListTileMenuItems.playNext: - // await _audioServiceHelper.addQueueItem(jellyfin_models.BaseItemDto.fromJson(widget.item.item.extras?["itemJson"])); await _queueService.addNext(items: [jellyfin_models.BaseItemDto.fromJson( widget.item.item.extras?["itemJson"])]); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text("Track will play next"), + content: Text(AppLocalizations.of(context)!.confirmPlayNext("track")), )); break; case SongListTileMenuItems.addToNextUp: - // await _audioServiceHelper.addQueueItem(jellyfin_models.BaseItemDto.fromJson(widget.item.item.extras?["itemJson"])); await _queueService.addToNextUp(items: [jellyfin_models.BaseItemDto.fromJson( widget.item.item.extras?["itemJson"])]); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text("Added track to Next Up"), + content: Text(AppLocalizations.of(context)!.confirmAddToNextUp("track")), )); break; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5f88e39fc..d32aee21c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -11,7 +11,7 @@ }, "serverUrl": "Server URL", "@serverUrl": {}, - "internalExternalIpExplanation": "If you want to be able to access your Jellyfin server remotely, you need to use your external IP.\n\nIf your server is on a HTTP port (80/443), you don't have to specify a port. This will likely be the case if your server is behind a reverse proxy.", + "internalExternalIpExplanation": "If you want to be able to access your Jellyfin server remotely, you need to use your external IP.\n\nIf your server is on a HTTP port (80/443), you don''t have to specify a port. This will likely be the case if your server is behind a reverse proxy.", "@internalExternalIpExplanation": { "description": "Extra info for which IP to use for remote access, and info on whether or not the user needs to specify a port." }, @@ -296,7 +296,7 @@ "@selectDirectory": {}, "unknownError": "Unknown Error", "@unknownError": {}, - "pathReturnSlashErrorMessage": "Paths that return \"/\" can't be used", + "pathReturnSlashErrorMessage": "Paths that return \"/\" can''t be used", "@pathReturnSlashErrorMessage": {}, "directoryMustBeEmpty": "Directory must be empty", "@directoryMustBeEmpty": {}, @@ -404,6 +404,8 @@ "@noArtist": {}, "unknownArtist": "Unknown Artist", "@unknownArtist": {}, + "unknownAlbum": "Unknown Album", + "@unknownAlbum": {}, "streaming": "STREAMING", "@streaming": {}, "downloaded": "DOWNLOADED", @@ -480,5 +482,99 @@ "@bufferDuration": {}, "bufferDurationSubtitle": "How much the player should buffer, in seconds. Requires a restart.", "@bufferDurationSubtitle": {}, - "language": "Language" -} \ No newline at end of file + "language": "Language", + "@language": {}, + "recentlyPlayed": "Recently Played", + "@recentlyPlayed": { + "description": "Description in the queue panel for the list of tracks that was previously played" + }, + "nextUp": "Next Up", + "@nextUp": { + "description": "Description in the queue panel for the list of tracks were manually added to be played after the current track. This should be capitalized (if applicable) to be more recognizable throughout the UI" + }, + "playingFrom": "Playing from", + "@playingFrom": { + "description": "Prefix shown before the name of the main queue source, like the album or playlist that was used to start playback. Example: \"Playing from {My Nice Playlist}\"" + }, + "playNext": "Play next", + "@playNext": { + "description": "Used for adding a track to the \"Next Up\" queue at the first position, to play right after the current track finishes playing" + }, + "addToNextUp": "Add to Next Up", + "@addToNextUp": { + "description": "Used for adding a track to the \"Next Up\" queue at the end, to play after all previous tracks from Next Up have played " + }, + "shuffleNext": "Shuffle next", + "@shuffleNext": { + "description": "Used for shuffling a list (album, playlist, etc.) to the \"Next Up\" queue at the first position, to play right after the current track finishes playing" + }, + "shuffleToNextUp": "Shuffle to Next Up", + "@shuffleToNextUp": { + "description": "Used for shuffling a list (album, playlist, etc.) to the end of the \"Next Up\" queue, to play after all previous tracks from Next Up have played " + }, + "confirmPlayNext": "{type, select, track{Track} album{Album} artist{Artist} playlist{Playlist} other{Item}} will play next", + "@confirmPlayNext": { + "description": "A confirmation message that is shown after successfully adding a track to the front of the \"Next Up\" queue", + "placeholders": { + "type": { + "type": "String" + } + } + }, + "confirmAddToNextUp": "Added {type, select, track{track} album{album} artist{artist} playlist{playlist} other{item}} to Next Up", + "@confirmAddToNextUp": { + "description": "A confirmation message that is shown after successfully adding a track to the end of the \"Next Up\" queue", + "placeholders": { + "type": { + "type": "String" + } + } + }, + "confirmShuffleNext": "Will shuffle next", + "@confirmShuffleNext": { + "description": "A confirmation message that is shown after successfully shuffling a list (album, playlist, etc.) to the front of the \"Next Up\" queue" + }, + "confirmShuffleToNextUp": "Shuffled to Next Up", + "@confirmShuffleToNextUp": { + "description": "A confirmation message that is shown after successfully shuffling a list (album, playlist, etc.) to the end of the \"Next Up\" queue" + }, + "placeholderSource": "Somewhere", + "@placeholderSource": { + "description": "Placeholder text used when the source of the current track/queue is unknown" + }, + "playbackHistory": "Playback History", + "@playbackHistory": { + "description": "Title for the playback history screen, where the user can see a list of recently played tracks, sorted by " + }, + "yourLikes": "Your Likes", + "@yourLikes": { + "description": "Title for the queue source when the user is playing their liked tracks" + }, + "mix": "{mixSource} - Mix", + "@mix": { + "description": "Suffix added to a queue source when playing a mix. Example: \"Never Gonna Give You Up - Mix\"", + "placeholders": { + "mixSource": { + "type": "String", + "example": "Never Gonna Give You Up" + } + } + }, + "tracksFormerNextUp": "Tracks added via Next Up", + "@tracksFormerNextUp": { + "description": "Title for the queue source for tracks that were once added to the queue via the \"Next Up\" feature, but have since been played" + }, + "playingFromType": "Playing From {source, select, album{Album} playlist{Playlist} songMix{Song Mix} artistMix{Artist Mix} albumMix{Album Mix} allSongs{All Songs} filteredList{Songs} genre{Genre} artist{Artist} favorites{Favorites} other{}}", + "@playingFromType": { + "description": "Prefix shown before the type of the main queue source at the top of the player screen. Example: \"Playing From Album\"", + "placeholders": { + "source": { + "type": "String" + } + } + }, + "shuffleAllQueueSource": "Shuffle All", + "@shuffleAllQueueSource": { + "description": "Title for the queue source when the user is shuffling all tracks. Should be capitalized (if applicable) to be more recognizable throughout the UI" + } +} diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 7c5d86020..a5f2d08d1 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -558,16 +558,16 @@ class DownloadedImage { enum QueueItemSourceType { - album(name: "Album"), - playlist(name: "Playlist"), - itemMix(name: "Song Mix"), - artistMix(name: "Artist Mix"), - albumMix(name: "Album Mix"), - favorites(name: ""), - songs(name: "All Songs"), - filteredList(name: "Songs"), - genre(name: "Genre"), - artist(name: "Artist"), + album(name: "album"), + playlist(name: "playlist"), + songMix(name: "songMix"), + artistMix(name: "artistMix"), + albumMix(name: "albumMix"), + favorites(name: "favorites"), + songs(name: "songs"), + filteredList(name: "filteredList"), + genre(name: "genre"), + artist(name: "artist"), nextUp(name: ""), formerNextUp(name: ""), downloads(name: ""), @@ -601,7 +601,7 @@ class QueueItemSource { QueueItemSourceType type; @HiveField(1) - String name; + QueueItemSourceName name; @HiveField(2) String id; @@ -611,6 +611,48 @@ class QueueItemSource { } +enum QueueItemSourceNameType { + preTranslated, + yourLikes, + shuffleAll, + mix, + instantMix, + nextUp, + tracksFormerNextUp, +} + +class QueueItemSourceName { + + const QueueItemSourceName({ + required this.type, + this.pretranslatedName, + this.localizationParameter, // used if only part of the name is translated + }); + + final QueueItemSourceNameType type; + final String? pretranslatedName; + final String? localizationParameter; + + getLocalized(BuildContext context) { + switch (type) { + case QueueItemSourceNameType.preTranslated: + return pretranslatedName ?? ""; + case QueueItemSourceNameType.yourLikes: + return AppLocalizations.of(context)!.yourLikes; + case QueueItemSourceNameType.shuffleAll: + return AppLocalizations.of(context)!.shuffleAllQueueSource; + case QueueItemSourceNameType.mix: + return AppLocalizations.of(context)!.mix(localizationParameter ?? ""); + case QueueItemSourceNameType.instantMix: + return AppLocalizations.of(context)!.instantMix; + case QueueItemSourceNameType.nextUp: + return AppLocalizations.of(context)!.nextUp; + case QueueItemSourceNameType.tracksFormerNextUp: + return AppLocalizations.of(context)!.tracksFormerNextUp; + } + } +} + class QueueItem { QueueItem({ required this.item, diff --git a/lib/screens/playback_history_screen.dart b/lib/screens/playback_history_screen.dart index 77ac97c44..f759c16b2 100644 --- a/lib/screens/playback_history_screen.dart +++ b/lib/screens/playback_history_screen.dart @@ -18,7 +18,7 @@ class PlaybackHistoryScreen extends StatelessWidget { centerTitle: true, elevation: 0.0, backgroundColor: Colors.transparent, - title: Text("Playback History"), + title: Text(AppLocalizations.of(context)!.playbackHistory), leading: FinampAppBarButton( onPressed: () => Navigator.pop(context), ), diff --git a/lib/services/audio_service_helper.dart b/lib/services/audio_service_helper.dart index ca731beab..4fc783a6b 100644 --- a/lib/services/audio_service_helper.dart +++ b/lib/services/audio_service_helper.dart @@ -2,9 +2,11 @@ import 'dart:collection'; import 'package:audio_service/audio_service.dart'; import 'package:finamp/models/jellyfin_models.dart'; +import 'package:flutter/widgets.dart'; import 'package:get_it/get_it.dart'; import 'package:logging/logging.dart'; import 'package:uuid/uuid.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'finamp_user_helper.dart'; import 'jellyfin_api_helper.dart'; @@ -19,74 +21,10 @@ import 'queue_service.dart'; class AudioServiceHelper { final _jellyfinApiHelper = GetIt.instance(); final _downloadsHelper = GetIt.instance(); - final _audioHandler = GetIt.instance(); final _queueService = GetIt.instance(); final _finampUserHelper = GetIt.instance(); final audioServiceHelperLogger = Logger("AudioServiceHelper"); - /// Replaces the queue with the given list of items. If startAtIndex is specified, Any items below it - /// will be ignored. This is used for when the user taps in the middle of an album to start from that point. - Future replaceQueueWithItem({ - required List itemList, //TODO create a custom type for item lists that can also hold the name of the list, etc. - int initialIndex = 0, - bool shuffle = false, - }) async { - try { - if (initialIndex > itemList.length) { - return Future.error( - "startAtIndex is bigger than the itemList! ($initialIndex > ${itemList.length})"); - } - - List queue = []; - for (jellyfin_models.BaseItemDto item in itemList) { - try { - queue.add(await _generateMediaItem(item)); - } catch (e) { - audioServiceHelperLogger.severe(e); - } - } - - // if (!shuffle) { - // // Give the audio service our next initial index so that playback starts - // // at that index. We don't do this if shuffling because it causes the - // // queue to always start at the start (although you could argue that we - // // still should if initialIndex is not 0, but that doesn't happen - // // anywhere in this app so oh well). - _audioHandler.setNextInitialIndex(initialIndex); - // } - - await _audioHandler.updateQueue(queue); - - if (shuffle) { - await _audioHandler.setShuffleMode(AudioServiceShuffleMode.all); - } else { - await _audioHandler.setShuffleMode(AudioServiceShuffleMode.none); - } - - _audioHandler.play(); - } catch (e) { - audioServiceHelperLogger.severe(e); - return Future.error(e); - } - } - - Future addQueueItem(jellyfin_models.BaseItemDto item) async { - try { - // If the queue is empty (like when the app is first launched), run the - // replace queue function instead so that the song gets played - if ((_audioHandler.queue.valueOrNull?.length ?? 0) == 0) { - await replaceQueueWithItem(itemList: [item]); - return; - } - - final itemMediaItem = await _generateMediaItem(item); - await _audioHandler.addQueueItem(itemMediaItem); - } catch (e) { - audioServiceHelperLogger.severe(e); - return Future.error(e); - } - } - /// Shuffles every song in the user's current view. Future shuffleAll(bool isFavourite) async { List? items; @@ -122,7 +60,9 @@ class AudioServiceHelper { items: items, source: QueueItemSource( type: isFavourite ? QueueItemSourceType.favorites : QueueItemSourceType.songs, - name: isFavourite ? "Your Likes" : "Shuffle All", + name: QueueItemSourceName( + type: isFavourite ? QueueItemSourceNameType.yourLikes : QueueItemSourceNameType.shuffleAll, + ), id: "shuffleAll", ) ); @@ -140,8 +80,11 @@ class AudioServiceHelper { await _queueService.startPlayback( items: items, source: QueueItemSource( - type: QueueItemSourceType.itemMix, - name: item.name != null ? "${item.name} - Mix" : "", + type: QueueItemSourceType.songMix, + name: QueueItemSourceName( + type: item.name != null ? QueueItemSourceNameType.mix : QueueItemSourceNameType.instantMix, + localizationParameter: item.name ?? "", + ), id: item.id ) ); @@ -164,7 +107,7 @@ class AudioServiceHelper { items: items, source: QueueItemSource( type: QueueItemSourceType.artistMix, - name: artists.map((e) => e.name).join(" & "), + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: artists.map((e) => e.name).join(" & ")), id: artists.first.id, item: artists.first, ) @@ -189,7 +132,7 @@ class AudioServiceHelper { items: items, source: QueueItemSource( type: QueueItemSourceType.albumMix, - name: albums.map((e) => e.name).join(" & "), + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: albums.map((e) => e.name).join(" & ")), id: albums.first.id, item: albums.first, ) @@ -202,37 +145,4 @@ class AudioServiceHelper { } } - Future _generateMediaItem(jellyfin_models.BaseItemDto item) async { - const uuid = Uuid(); - - final downloadedSong = _downloadsHelper.getDownloadedSong(item.id); - final isDownloaded = downloadedSong == null - ? false - : await _downloadsHelper.verifyDownloadedSong(downloadedSong); - - return MediaItem( - id: uuid.v4(), - album: item.album ?? "Unknown Album", - artist: item.artists?.join(", ") ?? item.albumArtist, - artUri: _downloadsHelper.getDownloadedImage(item)?.file.uri ?? - _jellyfinApiHelper.getImageUrl(item: item), - title: item.name ?? "Unknown Name", - extras: { - // "parentId": item.parentId, - // "itemId": item.id, - "itemJson": item.toJson(), - "shouldTranscode": FinampSettingsHelper.finampSettings.shouldTranscode, - "downloadedSongJson": isDownloaded - ? (_downloadsHelper.getDownloadedSong(item.id))!.toJson() - : null, - "isOffline": FinampSettingsHelper.finampSettings.isOffline, - // TODO: Maybe add transcoding bitrate here? - }, - // Jellyfin returns microseconds * 10 for some reason - duration: Duration( - microseconds: - (item.runTimeTicks == null ? 0 : item.runTimeTicks! ~/ 10), - ), - ); - } } diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 5ced2b2c1..b4d4722d1 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; -import 'package:finamp/services/playback_history_service.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:flutter/widgets.dart'; import 'package:just_audio/just_audio.dart'; import 'package:audio_service/audio_service.dart'; import 'package:get_it/get_it.dart'; @@ -17,6 +19,7 @@ import 'downloads_helper.dart'; import '../models/finamp_models.dart'; import '../models/jellyfin_models.dart' as jellyfin_models; import 'music_player_background_task.dart'; +import 'package:finamp/services/playback_history_service.dart'; enum PlaybackOrder { shuffled, linear } enum LoopMode { none, one, all } @@ -35,20 +38,20 @@ class QueueService { QueueItem? _currentTrack; // the currently playing track List _queueNextUp = []; // a temporary queue that gets appended to if the user taps "next up" List _queue = []; // contains all regular queue items - QueueOrder _order = QueueOrder(items: [], originalSource: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown), linearOrder: [], shuffledOrder: []); // contains all items that were at some point added to the regular queue, as well as their order when shuffle is enabled and disabled. This is used to loop the original queue once the end has been reached and "loop all" is enabled, **excluding** "next up" items and keeping the playback order. + QueueOrder _order = QueueOrder(items: [], originalSource: QueueItemSource(id: "", name: const QueueItemSourceName(type: QueueItemSourceNameType.preTranslated), type: QueueItemSourceType.unknown), linearOrder: [], shuffledOrder: []); // contains all items that were at some point added to the regular queue, as well as their order when shuffle is enabled and disabled. This is used to loop the original queue once the end has been reached and "loop all" is enabled, **excluding** "next up" items and keeping the playback order. PlaybackOrder _playbackOrder = PlaybackOrder.linear; LoopMode _loopMode = LoopMode.none; final _currentTrackStream = BehaviorSubject.seeded( - QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)) + QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: const QueueItemSourceName(type: QueueItemSourceNameType.preTranslated), type: QueueItemSourceType.unknown)) ); final _queueStream = BehaviorSubject.seeded(QueueInfo( previousTracks: [], - currentTrack: QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)), + currentTrack: QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: const QueueItemSourceName(type: QueueItemSourceNameType.preTranslated), type: QueueItemSourceType.unknown)), queue: [], nextUp: [], - source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown), + source: QueueItemSource(id: "", name: const QueueItemSourceName(type: QueueItemSourceNameType.preTranslated), type: QueueItemSourceType.unknown), )); final _playbackOrderStream = BehaviorSubject.seeded(PlaybackOrder.linear); @@ -95,7 +98,6 @@ class QueueService { void _queueFromConcatenatingAudioSource() { - //TODO handle shuffleIndices List allTracks = _audioHandler.effectiveSequence?.map((e) => e.tag as QueueItem).toList() ?? []; int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; @@ -109,7 +111,7 @@ class QueueService { if (i < adjustedQueueIndex) { _queuePreviousTracks.add(allTracks[i]); if (_queuePreviousTracks.last.source.type == QueueItemSourceType.nextUp) { - _queuePreviousTracks.last.source = QueueItemSource(type: QueueItemSourceType.formerNextUp, name: "Tracks added via Next Up", id: "former-next-up"); + _queuePreviousTracks.last.source = QueueItemSource(type: QueueItemSourceType.formerNextUp, name: const QueueItemSourceName(type: QueueItemSourceNameType.tracksFormerNextUp), id: "former-next-up"); } _queuePreviousTracks.last.type = QueueItemQueueType.previousTracks; } else if (i == adjustedQueueIndex) { @@ -126,7 +128,7 @@ class QueueService { } else { _queue.add(allTracks[i]); _queue.last.type = QueueItemQueueType.queue; - _queuePreviousTracks.last.source = QueueItemSource(type: QueueItemSourceType.formerNextUp, name: "Tracks added via Next Up", id: "former-next-up"); + _queuePreviousTracks.last.source = QueueItemSource(type: QueueItemSourceType.formerNextUp, name: const QueueItemSourceName(type: QueueItemSourceNameType.tracksFormerNextUp), id: "former-next-up"); } } else { _queue.add(allTracks[i]); @@ -156,8 +158,6 @@ class QueueService { int startingIndex = 0, }) async { - // TODO support starting playback from a specific item (index) in the list - // _initialQueue = list; // save original PlaybackList for looping/restarting and meta info await _replaceWholeQueue(itemList: items, source: source, initialIndex: startingIndex); _queueServiceLogger.info("Started playing '${source.name}' (${source.type})"); @@ -168,7 +168,7 @@ class QueueService { /// will be ignored. This is used for when the user taps in the middle of an album to start from that point. Future _replaceWholeQueue({ required List - itemList, //TODO create a custom type for item lists that can also hold the name of the list, etc. + itemList, required QueueItemSource source, int initialIndex = 0, }) async { @@ -280,7 +280,7 @@ class QueueService { for (final item in items) { queueItems.add(QueueItem( item: await _generateMediaItem(item), - source: source ?? QueueItemSource(id: "next-up", name: "Next Up", type: QueueItemSourceType.nextUp), + source: source ?? QueueItemSource(id: "next-up", name: const QueueItemSourceName(type: QueueItemSourceNameType.nextUp), type: QueueItemSourceType.nextUp), type: QueueItemQueueType.nextUp, )); } @@ -310,7 +310,7 @@ class QueueService { for (final item in items) { queueItems.add(QueueItem( item: await _generateMediaItem(item), - source: source ?? QueueItemSource(id: "next-up", name: "Next Up", type: QueueItemSourceType.nextUp), + source: source ?? QueueItemSource(id: "next-up", name: const QueueItemSourceName(type: QueueItemSourceNameType.nextUp), type: QueueItemSourceType.nextUp), type: QueueItemQueueType.nextUp, )); } @@ -516,11 +516,11 @@ class QueueService { return MediaItem( id: uuid.v4(), - album: item.album ?? "Unknown Album", + album: item.album ?? "unknown", artist: item.artists?.join(", ") ?? item.albumArtist, artUri: _downloadsHelper.getDownloadedImage(item)?.file.uri ?? _jellyfinApiHelper.getImageUrl(item: item), - title: item.name ?? "Unknown Name", + title: item.name ?? "unknown", extras: { // "parentId": item.parentId, // "itemId": item.id, @@ -530,7 +530,6 @@ class QueueService { ? (_downloadsHelper.getDownloadedSong(item.id))!.toJson() : null, "isOffline": FinampSettingsHelper.finampSettings.isOffline, - // TODO: Maybe add transcoding bitrate here? }, // Jellyfin returns microseconds * 10 for some reason duration: Duration( From c047239ccf1406756d5f53017849c24a01c2918e Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 23 Sep 2023 00:10:57 +0200 Subject: [PATCH 072/130] design and interaction improvements --- lib/components/PlayerScreen/queue_list.dart | 73 +++-- .../PlayerScreen/queue_list_item.dart | 270 +++++++++--------- lib/components/PlayerScreen/song_info.dart | 1 + .../blurred_player_screen_background.dart | 13 +- 4 files changed, 201 insertions(+), 156 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index acd71cfaf..f8f18c9e4 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -38,11 +38,18 @@ class _QueueListStreamState { } class QueueList extends StatefulWidget { - const QueueList( - {Key? key, required this.scrollController, required this.nextUpHeaderKey}) + const QueueList({ + Key? key, + required this.scrollController, + required this.previousTracksHeaderKey, + required this.currentTrackKey, + required this.nextUpHeaderKey, + }) : super(key: key); final ScrollController scrollController; + final GlobalKey previousTracksHeaderKey; + final Key currentTrackKey; final GlobalKey nextUpHeaderKey; @override @@ -141,9 +148,9 @@ class _QueueListState extends State { // duration: Duration(seconds: 2), // curve: Curves.fastOutSlowIn, // ); - if (widget.nextUpHeaderKey.currentContext != null) { + if (widget.previousTracksHeaderKey.currentContext != null) { Scrollable.ensureVisible( - widget.nextUpHeaderKey.currentContext!, + widget.previousTracksHeaderKey.currentContext!, // duration: const Duration(milliseconds: 200), // curve: Curves.decelerate, ); @@ -155,15 +162,20 @@ class _QueueListState extends State { _contents = [ // Previous Tracks if (isRecentTracksExpanded) - const PreviousTracksList() + PreviousTracksList(previousTracksHeaderKey: widget.previousTracksHeaderKey) , + //TODO replace this with a SliverPersistentHeader and add an `onTap` callback to the delegate SliverToBoxAdapter( + key: widget.previousTracksHeaderKey, child: GestureDetector( onTap:() { setState(() => isRecentTracksExpanded = !isRecentTracksExpanded); if (!isRecentTracksExpanded) { Future.delayed(const Duration(milliseconds: 200), () => scrollToCurrentTrack()); } + // else { + // Future.delayed(const Duration(milliseconds: 300), () => scrollToCurrentTrack()); + // } }, child: Padding( padding: const EdgeInsets.only(left: 14.0, right: 14.0, bottom: 12.0, top: 8.0), @@ -187,13 +199,12 @@ class _QueueListState extends State { ) ), CurrentTrack( - key: UniqueKey(), + // key: UniqueKey(), + key: widget.currentTrackKey, ), // next up - SliverToBoxAdapter( - key: widget.nextUpHeaderKey, - ), StreamBuilder( + key: widget.nextUpHeaderKey, stream: _queueService.getQueueStream(), builder: (context, snapshot) { if (snapshot.data != null && snapshot.data!.nextUp.isNotEmpty) { @@ -201,6 +212,7 @@ class _QueueListState extends State { // key: widget.nextUpHeaderKey, padding: const EdgeInsets.only(top: 20.0, bottom: 0.0), sliver: SliverPersistentHeader( + pinned: false, //TODO use https://stackoverflow.com/a/69372976 to only ever have one of the headers pinned delegate: SectionHeaderDelegate( title: Text(AppLocalizations.of(context)!.nextUp), height: 30.0, @@ -213,10 +225,11 @@ class _QueueListState extends State { } }, ), - const NextUpTracksList(), + NextUpTracksList(previousTracksHeaderKey: widget.previousTracksHeaderKey), SliverPadding( padding: const EdgeInsets.only(top: 20.0, bottom: 0.0), sliver: SliverPersistentHeader( + pinned: true, delegate: SectionHeaderDelegate( title: Row( children: [ @@ -232,7 +245,7 @@ class _QueueListState extends State { ), ), // Queue - const QueueTracksList(), + QueueTracksList(previousTracksHeaderKey: widget.previousTracksHeaderKey), ]; return CustomScrollView( @@ -243,6 +256,8 @@ class _QueueListState extends State { } Future showQueueBottomSheet(BuildContext context) { + GlobalKey previousTracksHeaderKey = GlobalKey(); + Key currentTrackKey = UniqueKey(); GlobalKey nextUpHeaderKey = GlobalKey(); return showModalBottomSheet( @@ -282,7 +297,7 @@ Future showQueueBottomSheet(BuildContext context) { children: [ if (FinampSettingsHelper .finampSettings.showCoverAsPlayerBackground) - const BlurredPlayerScreenBackground(), + BlurredPlayerScreenBackground(brightnessFactor: Theme.of(context).brightness == Brightness.dark ? 1.0 : 1.0), Column( mainAxisSize: MainAxisSize.min, children: [ @@ -306,6 +321,8 @@ Future showQueueBottomSheet(BuildContext context) { Expanded( child: QueueList( scrollController: scrollController, + previousTracksHeaderKey: previousTracksHeaderKey, + currentTrackKey: currentTrackKey, nextUpHeaderKey: nextUpHeaderKey, ), ), @@ -316,7 +333,7 @@ Future showQueueBottomSheet(BuildContext context) { //TODO fade this out if the key is visible floatingActionButton: FloatingActionButton( onPressed: () => scrollToKey( - key: nextUpHeaderKey, + key: previousTracksHeaderKey, duration: const Duration(milliseconds: 500)), backgroundColor: IconTheme.of(context).color!.withOpacity(0.70), shape: const RoundedRectangleBorder( @@ -344,8 +361,12 @@ Future showQueueBottomSheet(BuildContext context) { } class PreviousTracksList extends StatefulWidget { + + final GlobalKey previousTracksHeaderKey; + const PreviousTracksList({ Key? key, + required this.previousTracksHeaderKey, }) : super(key: key); @override @@ -406,8 +427,10 @@ class _PreviousTracksListState extends State _queueService.playbackOrder == PlaybackOrder.linear, onTap: () async { await _queueService.skipByOffset(indexOffset); + scrollToKey(key: widget.previousTracksHeaderKey, duration: const Duration(milliseconds: 500)); }, isCurrentTrack: false, + isPreviousTrack: true, ); }, ); @@ -420,8 +443,12 @@ class _PreviousTracksListState extends State } class NextUpTracksList extends StatefulWidget { + + final GlobalKey previousTracksHeaderKey; + const NextUpTracksList({ Key? key, + required this.previousTracksHeaderKey, }) : super(key: key); @override @@ -478,6 +505,7 @@ class _NextUpTracksListState extends State { subqueue: _nextUp!, onTap: () async { await _queueService.skipByOffset(indexOffset); + scrollToKey(key: widget.previousTracksHeaderKey, duration: const Duration(milliseconds: 500)); }, isCurrentTrack: false, ); @@ -492,8 +520,12 @@ class _NextUpTracksListState extends State { } class QueueTracksList extends StatefulWidget { + + final GlobalKey previousTracksHeaderKey; + const QueueTracksList({ Key? key, + required this.previousTracksHeaderKey, }) : super(key: key); @override @@ -552,6 +584,7 @@ class _QueueTracksListState extends State { _queueService.playbackOrder == PlaybackOrder.linear, onTap: () async { await _queueService.skipByOffset(indexOffset); + scrollToKey(key: widget.previousTracksHeaderKey, duration: const Duration(milliseconds: 500)); }, isCurrentTrack: false, ); @@ -620,18 +653,15 @@ class _CurrentTrackState extends State { ), backgroundColor: const Color.fromRGBO(0, 0, 0, 0.0), flexibleSpace: Container( - // width: 328, + // width: 58, height: 70.0, padding: const EdgeInsets.symmetric(horizontal: 12), child: Container( + clipBehavior: Clip.antiAlias, decoration: ShapeDecoration( - // color: Color.fromRGBO(188, 136, 86, 0.20), - color: Color.alphaBlend(IconTheme.of(context).color!.withOpacity(0.20), Colors.black), + color: Color.alphaBlend(IconTheme.of(context).color!.withOpacity(0.35), Colors.black), shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular(8), - bottomRight: Radius.circular(8), - ), + borderRadius: BorderRadius.all(Radius.circular(8.0)), ), ), child: Row( @@ -1086,7 +1116,8 @@ class SectionHeaderDelegate extends SliverPersistentHeaderDelegate { builder: (context, snapshot) { PlaybackBehaviorInfo? info = snapshot.data as PlaybackBehaviorInfo?; - return Padding( + return Container( + // color: Colors.black.withOpacity(0.5), padding: const EdgeInsets.symmetric(horizontal: 14.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index c7d969783..7f84ee512 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -24,6 +24,7 @@ class QueueListItem extends StatefulWidget { late int indexOffset; late List subqueue; late bool isCurrentTrack; + late bool isPreviousTrack; late bool allowReorder; late void Function() onTap; @@ -37,6 +38,7 @@ class QueueListItem extends StatefulWidget { required this.onTap, this.allowReorder = true, this.isCurrentTrack = false, + this.isPreviousTrack = false, }) : super(key: key); @override State createState() => _QueueListItemState(); @@ -60,145 +62,149 @@ class _QueueListItemState extends State { }, child: GestureDetector( onLongPressStart: (details) => showSongMenu(details), - child: Card( - color: const Color.fromRGBO(255, 255, 255, 0.05), - elevation: 0, - margin: - const EdgeInsets.symmetric(horizontal: 12.0, vertical: 5.0), - clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - child: ListTile( - visualDensity: VisualDensity.standard, - minVerticalPadding: 0.0, - horizontalTitleGap: 10.0, - contentPadding: - const EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0), - tileColor: widget.isCurrentTrack - ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) - : null, - leading: AlbumImage( - item: widget.item.item.extras?["itemJson"] == null - ? null - : jellyfin_models.BaseItemDto.fromJson( - widget.item.item.extras?["itemJson"]), + child: Opacity( + opacity: widget.isPreviousTrack ? 0.8 : 1.0, + child: Card( + color: const Color.fromRGBO(255, 255, 255, 0.075), + elevation: 0, + margin: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 5.0), + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), ), - // leading: Container( - // height: 60.0, - // width: 60.0, - // color: Colors.white, - // ), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(0.0), - child: Text( - widget.item.item.title, - style: this.widget.isCurrentTrack - ? TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontSize: 16, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w400, - overflow: TextOverflow.ellipsis) - : null, - overflow: TextOverflow.ellipsis, - ), - ), - Padding( - padding: const EdgeInsets.only(top: 6.0), - child: Text( - processArtist(widget.item.item.artist, context), - style: const TextStyle( - color: Colors.white70, - fontSize: 13, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w300, - overflow: TextOverflow.ellipsis), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - // subtitle: Container( - // alignment: Alignment.centerLeft, - // height: 40.5, // has to be above a certain value to get rid of vertical padding - // child: Padding( - // padding: const EdgeInsets.only(bottom: 2.0), - // child: Text( - // processArtist(widget.item.item.artist, context), - // style: const TextStyle( - // color: Colors.white70, - // fontSize: 13, - // fontFamily: 'Lexend Deca', - // fontWeight: FontWeight.w300, - // overflow: TextOverflow.ellipsis), - // overflow: TextOverflow.ellipsis, - // ), - // ), - // ), - trailing: Container( - alignment: Alignment.centerRight, - margin: const EdgeInsets.only(right: 8.0), - padding: const EdgeInsets.only(right: 6.0), - // width: widget.allowReorder ? 145.0 : 115.0, - width: widget.allowReorder ? 70.0 : 35.0, - height: 50.0, - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, + child: ListTile( + visualDensity: VisualDensity.standard, + minVerticalPadding: 0.0, + horizontalTitleGap: 10.0, + contentPadding: + const EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0), + tileColor: widget.isCurrentTrack + ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) + : null, + leading: AlbumImage( + item: widget.item.item.extras?["itemJson"] == null + ? null + : jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]), + borderRadius: BorderRadius.zero, + ), + // leading: Container( + // height: 60.0, + // width: 60.0, + // color: Colors.white, + // ), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "${widget.item.item.duration?.inMinutes.toString()}:${((widget.item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - textAlign: TextAlign.end, - style: TextStyle( - color: Theme.of(context).textTheme.bodySmall?.color, + Padding( + padding: const EdgeInsets.all(0.0), + child: Text( + widget.item.item.title, + style: this.widget.isCurrentTrack + ? TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontSize: 16, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w400, + overflow: TextOverflow.ellipsis) + : null, + overflow: TextOverflow.ellipsis, ), ), - // IconButton( - // padding: const EdgeInsets.all(0.0), - // visualDensity: VisualDensity.compact, - // icon: const Icon( - // TablerIcons.dots_vertical, - // color: Colors.white, - // weight: 1.5, - // ), - // iconSize: 24.0, - // onPressed: () => showSongMenu(), - // ), - // IconButton( - // padding: const EdgeInsets.only(right: 14.0), - // visualDensity: VisualDensity.compact, - // icon: const Icon( - // TablerIcons.x, - // color: Colors.white, - // weight: 1.5, - // ), - // iconSize: 24.0, - // onPressed: () async => - // await _queueService.removeAtOffset(widget.indexOffset), - // ), - if (widget.allowReorder) - ReorderableDragStartListener( - index: widget.listIndex, - child: Padding( - padding: EdgeInsets.only(bottom: 5.0, left: 6.0), - child: const Icon( - TablerIcons.grip_horizontal, - color: Colors.white, - size: 28.0, - weight: 1.5, - ), - ), + Padding( + padding: const EdgeInsets.only(top: 6.0), + child: Text( + processArtist(widget.item.item.artist, context), + style: const TextStyle( + color: Colors.white70, + fontSize: 13, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w300, + overflow: TextOverflow.ellipsis), + overflow: TextOverflow.ellipsis, ), + ), ], ), - ), - onTap: widget.onTap, - ))), + // subtitle: Container( + // alignment: Alignment.centerLeft, + // height: 40.5, // has to be above a certain value to get rid of vertical padding + // child: Padding( + // padding: const EdgeInsets.only(bottom: 2.0), + // child: Text( + // processArtist(widget.item.item.artist, context), + // style: const TextStyle( + // color: Colors.white70, + // fontSize: 13, + // fontFamily: 'Lexend Deca', + // fontWeight: FontWeight.w300, + // overflow: TextOverflow.ellipsis), + // overflow: TextOverflow.ellipsis, + // ), + // ), + // ), + trailing: Container( + alignment: Alignment.centerRight, + margin: const EdgeInsets.only(right: 8.0), + padding: const EdgeInsets.only(right: 6.0), + // width: widget.allowReorder ? 145.0 : 115.0, + width: widget.allowReorder ? 70.0 : 35.0, + height: 50.0, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "${widget.item.item.duration?.inMinutes.toString()}:${((widget.item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + textAlign: TextAlign.end, + style: TextStyle( + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + // IconButton( + // padding: const EdgeInsets.all(0.0), + // visualDensity: VisualDensity.compact, + // icon: const Icon( + // TablerIcons.dots_vertical, + // color: Colors.white, + // weight: 1.5, + // ), + // iconSize: 24.0, + // onPressed: () => showSongMenu(), + // ), + // IconButton( + // padding: const EdgeInsets.only(right: 14.0), + // visualDensity: VisualDensity.compact, + // icon: const Icon( + // TablerIcons.x, + // color: Colors.white, + // weight: 1.5, + // ), + // iconSize: 24.0, + // onPressed: () async => + // await _queueService.removeAtOffset(widget.indexOffset), + // ), + if (widget.allowReorder) + ReorderableDragStartListener( + index: widget.listIndex, + child: Padding( + padding: EdgeInsets.only(bottom: 5.0, left: 6.0), + child: const Icon( + TablerIcons.grip_horizontal, + color: Colors.white, + size: 28.0, + weight: 1.5, + ), + ), + ), + ], + ), + ), + onTap: widget.onTap, + )), + )), ); } diff --git a/lib/components/PlayerScreen/song_info.dart b/lib/components/PlayerScreen/song_info.dart index 4c0a28fe2..067df28a1 100644 --- a/lib/components/PlayerScreen/song_info.dart +++ b/lib/components/PlayerScreen/song_info.dart @@ -197,6 +197,7 @@ class _PlayerScreenAlbumImage extends ConsumerWidget { accentColor: newColour, brightness: theme.brightness, ); + } }), ), diff --git a/lib/screens/blurred_player_screen_background.dart b/lib/screens/blurred_player_screen_background.dart index 747199710..65427f622 100644 --- a/lib/screens/blurred_player_screen_background.dart +++ b/lib/screens/blurred_player_screen_background.dart @@ -9,7 +9,14 @@ import '../services/current_album_image_provider.dart'; /// Same as [_PlayerScreenAlbumImage], but with a BlurHash instead. We also /// filter the BlurHash so that it works as a background image. class BlurredPlayerScreenBackground extends ConsumerWidget { - const BlurredPlayerScreenBackground({Key? key}) : super(key: key); + + /// should never be less than 1.0 + final double brightnessFactor; + + const BlurredPlayerScreenBackground({ + Key? key, + this.brightnessFactor = 1.0, + }) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { @@ -26,8 +33,8 @@ class BlurredPlayerScreenBackground extends ConsumerWidget { imageBuilder: (context, child) => ColorFiltered( colorFilter: ColorFilter.mode( Theme.of(context).brightness == Brightness.dark - ? Colors.black.withOpacity(0.75) - : Colors.white.withOpacity(0.50), + ? Colors.black.withOpacity(0.65 / brightnessFactor) + : Colors.white.withOpacity(0.50 / brightnessFactor), BlendMode.srcOver), child: ImageFiltered( imageFilter: ImageFilter.blur( From e9b4c6b53fc81da842555137812613f37133b13e Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 23 Sep 2023 00:37:11 +0200 Subject: [PATCH 073/130] added haptic feedback to QueueList --- android/app/src/debug/AndroidManifest.xml | 1 + android/app/src/main/AndroidManifest.xml | 1 + android/app/src/profile/AndroidManifest.xml | 1 + lib/components/PlayerScreen/queue_list.dart | 36 ++++++++++++++----- .../PlayerScreen/queue_list_item.dart | 6 ++-- lib/components/PlayerScreen/song_info.dart | 8 +++++ .../blurred_player_screen_background.dart | 2 +- pubspec.lock | 8 +++++ pubspec.yaml | 1 + 9 files changed, 53 insertions(+), 11 deletions(-) diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 276a25e8a..fc6064434 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -3,4 +3,5 @@ to allow setting breakpoints, to provide hot reload, etc. --> + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index fd4bfe7b4..003a2116b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + + diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index f8f18c9e4..72a16657e 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -16,6 +16,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get_it/get_it.dart'; import 'package:rxdart/rxdart.dart'; +import 'package:flutter_vibrate/flutter_vibrate.dart'; import '../album_image.dart'; import '../../models/jellyfin_models.dart' as jellyfin_models; @@ -169,6 +170,7 @@ class _QueueListState extends State { key: widget.previousTracksHeaderKey, child: GestureDetector( onTap:() { + Vibrate.feedback(FeedbackType.selection); setState(() => isRecentTracksExpanded = !isRecentTracksExpanded); if (!isRecentTracksExpanded) { Future.delayed(const Duration(milliseconds: 200), () => scrollToCurrentTrack()); @@ -260,6 +262,8 @@ Future showQueueBottomSheet(BuildContext context) { Key currentTrackKey = UniqueKey(); GlobalKey nextUpHeaderKey = GlobalKey(); + Vibrate.feedback(FeedbackType.impact); + return showModalBottomSheet( // showDragHandle: true, useSafeArea: true, @@ -332,9 +336,12 @@ Future showQueueBottomSheet(BuildContext context) { ), //TODO fade this out if the key is visible floatingActionButton: FloatingActionButton( - onPressed: () => scrollToKey( + onPressed: () { + Vibrate.feedback(FeedbackType.impact); + scrollToKey( key: previousTracksHeaderKey, - duration: const Duration(milliseconds: 500)), + duration: const Duration(milliseconds: 500)); + }, backgroundColor: IconTheme.of(context).color!.withOpacity(0.70), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(16.0))), @@ -397,6 +404,7 @@ class _PreviousTracksListState extends State int newPositionOffset = -(_previousTracks!.length - newIndex); print("$draggingOffset -> $newPositionOffset"); if (mounted) { + Vibrate.feedback(FeedbackType.impact); setState(() { // temporarily update internal queue QueueItem tmp = _previousTracks!.removeAt(oldIndex); @@ -409,7 +417,8 @@ class _PreviousTracksListState extends State } }, onReorderStart: (p0) { - Feedback.forLongPress(context); + // Feedback.forLongPress(context); + Vibrate.feedback(FeedbackType.selection); }, itemCount: _previousTracks?.length ?? 0, itemBuilder: (context, index) { @@ -426,6 +435,7 @@ class _PreviousTracksListState extends State allowReorder: _queueService.playbackOrder == PlaybackOrder.linear, onTap: () async { + Vibrate.feedback(FeedbackType.selection); await _queueService.skipByOffset(indexOffset); scrollToKey(key: widget.previousTracksHeaderKey, duration: const Duration(milliseconds: 500)); }, @@ -477,6 +487,7 @@ class _NextUpTracksListState extends State { int newPositionOffset = newIndex + 1; print("$draggingOffset -> $newPositionOffset"); if (mounted) { + Vibrate.feedback(FeedbackType.impact); setState(() { // temporarily update internal queue QueueItem tmp = _nextUp!.removeAt(oldIndex); @@ -489,7 +500,7 @@ class _NextUpTracksListState extends State { } }, onReorderStart: (p0) { - Feedback.forLongPress(context); + Vibrate.feedback(FeedbackType.selection); }, itemCount: _nextUp?.length ?? 0, itemBuilder: (context, index) { @@ -504,6 +515,7 @@ class _NextUpTracksListState extends State { indexOffset: indexOffset, subqueue: _nextUp!, onTap: () async { + Vibrate.feedback(FeedbackType.selection); await _queueService.skipByOffset(indexOffset); scrollToKey(key: widget.previousTracksHeaderKey, duration: const Duration(milliseconds: 500)); }, @@ -554,6 +566,7 @@ class _QueueTracksListState extends State { int newPositionOffset = newIndex + (_nextUp?.length ?? 0) + 1; print("$draggingOffset -> $newPositionOffset"); if (mounted) { + Vibrate.feedback(FeedbackType.impact); setState(() { // temporarily update internal queue QueueItem tmp = _queue!.removeAt(oldIndex); @@ -566,7 +579,7 @@ class _QueueTracksListState extends State { } }, onReorderStart: (p0) { - Feedback.forLongPress(context); + Vibrate.feedback(FeedbackType.selection); }, itemCount: _queue?.length ?? 0, itemBuilder: (context, index) { @@ -583,6 +596,7 @@ class _QueueTracksListState extends State { allowReorder: _queueService.playbackOrder == PlaybackOrder.linear, onTap: () async { + Vibrate.feedback(FeedbackType.selection); await _queueService.skipByOffset(indexOffset); scrollToKey(key: widget.previousTracksHeaderKey, duration: const Duration(milliseconds: 500)); }, @@ -698,6 +712,7 @@ class _CurrentTrackState extends State { ), child: IconButton( onPressed: () { + Vibrate.feedback(FeedbackType.success); _audioHandler.togglePlayback(); }, icon: mediaState!.playbackState.playing @@ -846,10 +861,11 @@ class _CurrentTrackState extends State { weight: 1.5, ), - onPressed: () => { + onPressed: () { + Vibrate.feedback(FeedbackType.success); setState(() { setFavourite(currentTrack!); - }) + }); }, ), ), @@ -1145,6 +1161,7 @@ class SectionHeaderDelegate extends SliverPersistentHeaderDelegate { : Colors.white, onPressed: () { _queueService.togglePlaybackOrder(); + Vibrate.feedback(FeedbackType.success); //TODO why is the current track scrolled out of view **after** the queue is updated? Future.delayed( const Duration(milliseconds: 300), @@ -1171,7 +1188,10 @@ class SectionHeaderDelegate extends SliverPersistentHeaderDelegate { color: info?.loop != LoopMode.none ? IconTheme.of(context).color! : Colors.white, - onPressed: () => _queueService.toggleLoopMode(), + onPressed: () { + _queueService.toggleLoopMode(); + Vibrate.feedback(FeedbackType.success); + } ), ], ), diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index 7f84ee512..84adbce0c 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -15,6 +15,7 @@ import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/models/jellyfin_models.dart' as jellyfin_models; import 'package:finamp/services/queue_service.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; +import 'package:flutter_vibrate/flutter_vibrate.dart'; import 'package:get_it/get_it.dart'; class QueueListItem extends StatefulWidget { @@ -55,10 +56,11 @@ class _QueueListItemState extends State { return Dismissible( key: Key(widget.item.id), onDismissed: (direction) { - setState(() async { - await _queueService.removeAtOffset(widget.indexOffset); + setState(() { + _queueService.removeAtOffset(widget.indexOffset); // widget.subqueue.removeAt(widget.listIndex); }); + Vibrate.feedback(FeedbackType.impact); }, child: GestureDetector( onLongPressStart: (details) => showSongMenu(details), diff --git a/lib/components/PlayerScreen/song_info.dart b/lib/components/PlayerScreen/song_info.dart index 067df28a1..a50c7be73 100644 --- a/lib/components/PlayerScreen/song_info.dart +++ b/lib/components/PlayerScreen/song_info.dart @@ -41,6 +41,14 @@ class _SongInfoState extends State { return StreamBuilder( stream: queueService.getQueueStream(), builder: (context, snapshot) { + + if (!snapshot.hasData) { + // show loading indicator + return const Center( + child: CircularProgressIndicator(), + ); + } + final currentTrack = snapshot.data!.currentTrack!; final mediaItem = currentTrack.item; final songBaseItemDto = diff --git a/lib/screens/blurred_player_screen_background.dart b/lib/screens/blurred_player_screen_background.dart index 65427f622..ec10e1555 100644 --- a/lib/screens/blurred_player_screen_background.dart +++ b/lib/screens/blurred_player_screen_background.dart @@ -33,7 +33,7 @@ class BlurredPlayerScreenBackground extends ConsumerWidget { imageBuilder: (context, child) => ColorFiltered( colorFilter: ColorFilter.mode( Theme.of(context).brightness == Brightness.dark - ? Colors.black.withOpacity(0.65 / brightnessFactor) + ? Colors.black.withOpacity(0.675 / brightnessFactor) : Colors.white.withOpacity(0.50 / brightnessFactor), BlendMode.srcOver), child: ImageFiltered( diff --git a/pubspec.lock b/pubspec.lock index 368c21374..194934fdc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -434,6 +434,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_vibrate: + dependency: "direct main" + description: + name: flutter_vibrate + sha256: "9cc9b32cf52c90dd34c1cf396ed40010b2c74e69adbb0ff16005afa900971ad8" + url: "https://pub.dev" + source: hosted + version: "1.3.0" flutter_web_plugins: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 78f8725b1..2b50a1a33 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -82,6 +82,7 @@ dependencies: git: url: https://github.com/lamarios/locale_names.git ref: cea057c220f4ee7e09e8f1fc7036110245770948 + flutter_vibrate: ^1.3.0 dev_dependencies: flutter_test: From 9db72f5249e4e78cd09e786288c2786dfc275d26 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 23 Sep 2023 01:18:34 +0200 Subject: [PATCH 074/130] smaller design tweaks to tie in the color theme - changes aren't limited to the queue list --- lib/components/PlayerScreen/album_chip.dart | 7 +- lib/components/PlayerScreen/artist_chip.dart | 11 +- .../PlayerScreen/player_buttons.dart | 1 + .../player_screen_appbar_title.dart | 2 + lib/components/PlayerScreen/queue_list.dart | 10 +- .../PlayerScreen/song_name_content.dart | 2 + lib/components/finamp_app_bar_button.dart | 8 +- lib/components/now_playing_bar.dart | 260 +++++++++--------- lib/screens/player_screen.dart | 2 +- 9 files changed, 168 insertions(+), 135 deletions(-) diff --git a/lib/components/PlayerScreen/album_chip.dart b/lib/components/PlayerScreen/album_chip.dart index f70925fd3..a9434c8ac 100644 --- a/lib/components/PlayerScreen/album_chip.dart +++ b/lib/components/PlayerScreen/album_chip.dart @@ -13,9 +13,11 @@ class AlbumChip extends StatelessWidget { const AlbumChip({ Key? key, this.item, + this.color, }) : super(key: key); final BaseItemDto? item; + final Color? color; @override Widget build(BuildContext context) { @@ -23,7 +25,7 @@ class AlbumChip extends StatelessWidget { return Container( constraints: const BoxConstraints(minWidth: 10, maxWidth: 200), - child: _AlbumChipContent(item: item!)); + child: _AlbumChipContent(item: item!, color: color)); } } @@ -46,15 +48,18 @@ class _AlbumChipContent extends StatelessWidget { const _AlbumChipContent({ Key? key, required this.item, + required this.color, }) : super(key: key); final BaseItemDto item; + final Color? color; @override Widget build(BuildContext context) { final jellyfinApiHelper = GetIt.instance(); return Material( + color: color ?? Colors.white.withOpacity(0.1), borderRadius: _borderRadius, child: InkWell( borderRadius: _borderRadius, diff --git a/lib/components/PlayerScreen/artist_chip.dart b/lib/components/PlayerScreen/artist_chip.dart index d128e7b8a..9a729b378 100644 --- a/lib/components/PlayerScreen/artist_chip.dart +++ b/lib/components/PlayerScreen/artist_chip.dart @@ -19,10 +19,12 @@ const _textStyle = TextStyle( class ArtistChip extends StatefulWidget { const ArtistChip({ Key? key, + this.color, this.item, }) : super(key: key); final BaseItemDto? item; + final Color? color; @override State createState() => _ArtistChipState(); @@ -65,8 +67,10 @@ class _ArtistChipState extends State { return FutureBuilder( future: _artistChipFuture, - builder: (context, snapshot) => - _ArtistChipContent(item: snapshot.data ?? widget.item!), + builder: (context, snapshot) { + final color = widget.color ?? _defaultColour; + return _ArtistChipContent(item: snapshot.data ?? widget.item!, color: color); + } ); } } @@ -90,9 +94,11 @@ class _ArtistChipContent extends StatelessWidget { const _ArtistChipContent({ Key? key, required this.item, + required this.color, }) : super(key: key); final BaseItemDto item; + final Color color; @override Widget build(BuildContext context) { @@ -103,6 +109,7 @@ class _ArtistChipContent extends StatelessWidget { return SizedBox( height: 24, child: Material( + color: color, borderRadius: _borderRadius, child: InkWell( // Offline artists aren't implemented and we shouldn't click through diff --git a/lib/components/PlayerScreen/player_buttons.dart b/lib/components/PlayerScreen/player_buttons.dart index 8668831f1..238c66573 100644 --- a/lib/components/PlayerScreen/player_buttons.dart +++ b/lib/components/PlayerScreen/player_buttons.dart @@ -37,6 +37,7 @@ class PlayerButtons extends StatelessWidget { _RoundedIconButton( width: 75, height: 75, + borderRadius: BorderRadius.circular(24), onTap: playbackState != null ? () async { if (playbackState.playing) { diff --git a/lib/components/PlayerScreen/player_screen_appbar_title.dart b/lib/components/PlayerScreen/player_screen_appbar_title.dart index 6ac4bd776..f2d1bdfa5 100644 --- a/lib/components/PlayerScreen/player_screen_appbar_title.dart +++ b/lib/components/PlayerScreen/player_screen_appbar_title.dart @@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_vibrate/flutter_vibrate.dart'; import 'package:get_it/get_it.dart'; import 'package:audio_service/audio_service.dart'; import 'package:palette_generator/palette_generator.dart'; @@ -106,6 +107,7 @@ void navigateToSource(BuildContext context, QueueItemSource source) async { case QueueItemSourceType.filteredList: case QueueItemSourceType.downloads: default: + Vibrate.feedback(FeedbackType.warning); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Not implemented yet."), diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 72a16657e..c21a7f9d3 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -656,6 +656,11 @@ class _CurrentTrackState extends State { mediaState = snapshot.data!.mediaState; playbackPosition = snapshot.data!.playbackPosition; + jellyfin_models.BaseItemDto? baseItem = currentTrack!.item.extras?["itemJson"] == null + ? null + : jellyfin_models.BaseItemDto.fromJson( + currentTrack!.item.extras?["itemJson"]); + return SliverAppBar( // key: currentTrackKey, pinned: true, @@ -687,10 +692,7 @@ class _CurrentTrackState extends State { alignment: Alignment.center, children: [ AlbumImage( - item: currentTrack!.item.extras?["itemJson"] == null - ? null - : jellyfin_models.BaseItemDto.fromJson( - currentTrack!.item.extras?["itemJson"]), + item: baseItem, borderRadius: const BorderRadius.only( topLeft: Radius.circular(8), bottomLeft: Radius.circular(8), diff --git a/lib/components/PlayerScreen/song_name_content.dart b/lib/components/PlayerScreen/song_name_content.dart index ae4143c71..0a6e6a081 100644 --- a/lib/components/PlayerScreen/song_name_content.dart +++ b/lib/components/PlayerScreen/song_name_content.dart @@ -56,6 +56,7 @@ class SongNameContent extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 4), child: ArtistChip( item: songBaseItemDto, + color: IconTheme.of(context).color!.withOpacity(0.1), key: songBaseItemDto?.albumArtist == null ? null // We have to add -artist and -album to the keys because otherwise @@ -71,6 +72,7 @@ class SongNameContent extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 4), child: AlbumChip( item: songBaseItemDto, + color: IconTheme.of(context).color!.withOpacity(0.1), key: songBaseItemDto?.album == null ? null : ValueKey("${songBaseItemDto!.album}-album"), diff --git a/lib/components/finamp_app_bar_button.dart b/lib/components/finamp_app_bar_button.dart index 830ba9a2c..1b9c4ca5a 100644 --- a/lib/components/finamp_app_bar_button.dart +++ b/lib/components/finamp_app_bar_button.dart @@ -17,10 +17,10 @@ class FinampAppBarButton extends StatelessWidget { child: Container( width: kMinInteractiveDimension - 12, height: kMinInteractiveDimension - 12, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.15), - shape: BoxShape.circle, - ), + // decoration: BoxDecoration( + // color: IconTheme.of(context).color?.withOpacity(0.1) ?? Colors.white.withOpacity(0.15), + // shape: BoxShape.circle, + // ), child: IconButton( onPressed: onPressed, tooltip: MaterialLocalizations.of(context).backButtonTooltip, diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index 5ed79a79f..6f344804d 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -1,6 +1,8 @@ import 'package:audio_service/audio_service.dart'; +import 'package:finamp/services/player_screen_theme_provider.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; import 'package:simple_gesture_detector/simple_gesture_detector.dart'; @@ -13,155 +15,167 @@ import '../services/music_player_background_task.dart'; import '../screens/player_screen.dart'; import 'PlayerScreen/progress_slider.dart'; -class NowPlayingBar extends StatelessWidget { +class NowPlayingBar extends ConsumerWidget { const NowPlayingBar({ Key? key, }) : super(key: key); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { // BottomNavBar's default elevation is 8 (https://api.flutter.dev/flutter/material/BottomNavigationBar/elevation.html) + final imageTheme = ref.watch(playerScreenThemeProvider); + const elevation = 8.0; - final color = Theme.of(context).bottomNavigationBarTheme.backgroundColor; + // final color = Theme.of(context).bottomNavigationBarTheme.backgroundColor; final audioHandler = GetIt.instance(); final queueService = GetIt.instance(); - return SimpleGestureDetector( - onVerticalSwipe: (direction) { - if (direction == SwipeDirection.up) { - Navigator.of(context).pushNamed(PlayerScreen.routeName); - } - }, - child: StreamBuilder( - stream: mediaStateStream, - builder: (context, snapshot) { - if (snapshot.hasData) { - final playing = snapshot.data!.playbackState.playing; - - // If we have a media item and the player hasn't finished, show - // the now playing bar. - if (snapshot.data!.mediaItem != null) { - final item = BaseItemDto.fromJson( - snapshot.data!.mediaItem!.extras!["itemJson"]); - - return Material( - color: color, - elevation: elevation, - child: SafeArea( - child: SizedBox( - width: MediaQuery.of(context).size.width, - child: Stack( - children: [ - const ProgressSlider( - allowSeeking: false, - showBuffer: false, - showDuration: false, - showPlaceholder: false, - ), - Dismissible( - key: const Key("NowPlayingBar"), - direction: FinampSettingsHelper.finampSettings.disableGesture ? DismissDirection.none : DismissDirection.horizontal, - confirmDismiss: (direction) async { - if (direction == DismissDirection.endToStart) { - // queueService.nextTrack(); - audioHandler.skipToNext(); - } else { - // queueService.previousTrack(); - audioHandler.skipToPrevious(); - } - return false; - }, - background: const Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - AspectRatio( - aspectRatio: 1, - child: FittedBox( - fit: BoxFit.fitHeight, - child: Padding( - padding: - EdgeInsets.symmetric(vertical: 8.0), - child: Icon(Icons.skip_previous), + return Theme( + data: ThemeData( + fontFamily: "LexendDeca", + colorScheme: imageTheme, + brightness: Theme.of(context).brightness, + iconTheme: Theme.of(context).iconTheme.copyWith( + color: imageTheme?.primary, + ), + ), + child: SimpleGestureDetector( + onVerticalSwipe: (direction) { + if (direction == SwipeDirection.up) { + Navigator.of(context).pushNamed(PlayerScreen.routeName); + } + }, + child: StreamBuilder( + stream: mediaStateStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + final playing = snapshot.data!.playbackState.playing; + + // If we have a media item and the player hasn't finished, show + // the now playing bar. + if (snapshot.data!.mediaItem != null) { + final item = BaseItemDto.fromJson( + snapshot.data!.mediaItem!.extras!["itemJson"]); + + return Material( + color: IconTheme.of(context).color!.withOpacity(0.1), + elevation: elevation, + child: SafeArea( + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: Stack( + children: [ + const ProgressSlider( + allowSeeking: false, + showBuffer: false, + showDuration: false, + showPlaceholder: false, + ), + Dismissible( + key: const Key("NowPlayingBar"), + direction: FinampSettingsHelper.finampSettings.disableGesture ? DismissDirection.none : DismissDirection.horizontal, + confirmDismiss: (direction) async { + if (direction == DismissDirection.endToStart) { + // queueService.nextTrack(); + audioHandler.skipToNext(); + } else { + // queueService.previousTrack(); + audioHandler.skipToPrevious(); + } + return false; + }, + background: const Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AspectRatio( + aspectRatio: 1, + child: FittedBox( + fit: BoxFit.fitHeight, + child: Padding( + padding: + EdgeInsets.symmetric(vertical: 8.0), + child: Icon(Icons.skip_previous), + ), ), ), - ), - AspectRatio( - aspectRatio: 1, - child: FittedBox( - fit: BoxFit.fitHeight, - child: Padding( - padding: - EdgeInsets.symmetric(vertical: 8.0), - child: Icon(Icons.skip_next), + AspectRatio( + aspectRatio: 1, + child: FittedBox( + fit: BoxFit.fitHeight, + child: Padding( + padding: + EdgeInsets.symmetric(vertical: 8.0), + child: Icon(Icons.skip_next), + ), ), ), - ), - ], + ], + ), ), - ), - child: ListTile( - onTap: () => Navigator.of(context) - .pushNamed(PlayerScreen.routeName), - leading: AlbumImage(item: item), - title: Text( - snapshot.data!.mediaItem!.title, - softWrap: false, - maxLines: 1, - overflow: TextOverflow.fade, - ), - subtitle: Text( - processArtist( - snapshot.data!.mediaItem!.artist, context), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (snapshot - .data!.playbackState.processingState != - AudioProcessingState.idle) - IconButton( - // We have a key here because otherwise the - // InkWell moves over to the play/pause button - key: const ValueKey("StopButton"), - icon: const Icon(Icons.stop), - onPressed: () => audioHandler.stop(), - ), - playing - ? IconButton( - icon: const Icon(Icons.pause), - onPressed: () => audioHandler.pause(), - ) - : IconButton( - icon: const Icon(Icons.play_arrow), - onPressed: () => audioHandler.play(), - ), - ], + child: ListTile( + onTap: () => Navigator.of(context) + .pushNamed(PlayerScreen.routeName), + leading: AlbumImage(item: item), + title: Text( + snapshot.data!.mediaItem!.title, + softWrap: false, + maxLines: 1, + overflow: TextOverflow.fade, + ), + subtitle: Text( + processArtist( + snapshot.data!.mediaItem!.artist, context), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (snapshot + .data!.playbackState.processingState != + AudioProcessingState.idle) + IconButton( + // We have a key here because otherwise the + // InkWell moves over to the play/pause button + key: const ValueKey("StopButton"), + icon: const Icon(Icons.stop), + onPressed: () => audioHandler.stop(), + ), + playing + ? IconButton( + icon: const Icon(Icons.pause), + onPressed: () => audioHandler.pause(), + ) + : IconButton( + icon: const Icon(Icons.play_arrow), + onPressed: () => audioHandler.play(), + ), + ], + ), ), ), - ), - ], + ], + ), ), ), - ), - ); + ); + } else { + return const SizedBox( + width: 0, + height: 0, + ); + } } else { return const SizedBox( width: 0, height: 0, ); } - } else { - return const SizedBox( - width: 0, - height: 0, - ); - } - }, + }, + ), ), ); } diff --git a/lib/screens/player_screen.dart b/lib/screens/player_screen.dart index e95cf8e71..e614bebc2 100644 --- a/lib/screens/player_screen.dart +++ b/lib/screens/player_screen.dart @@ -40,7 +40,7 @@ class PlayerScreen extends ConsumerWidget { color: imageTheme?.primary, ), ), - child: _PlayerScreenContent(), + child: const _PlayerScreenContent(), ); } } From 0f39cf926db49350cd6c6c55649bcf62fe1a2142 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 24 Sep 2023 16:06:57 +0200 Subject: [PATCH 075/130] fix dismissing tracks from queue, adjust progress slider color --- lib/components/PlayerScreen/progress_slider.dart | 9 +++++++++ lib/components/PlayerScreen/queue_list_item.dart | 8 +++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/components/PlayerScreen/progress_slider.dart b/lib/components/PlayerScreen/progress_slider.dart index 6debf5634..07926eb99 100644 --- a/lib/components/PlayerScreen/progress_slider.dart +++ b/lib/components/PlayerScreen/progress_slider.dart @@ -136,6 +136,15 @@ class _BufferSlider extends StatelessWidget { thumbShape: HiddenThumbComponentShape(), trackShape: BufferTrackShape(), trackHeight: 4.0, + inactiveTrackColor: IconTheme.of(context).color!.withOpacity(0.35), + // thumbColor: Colors.white, + // overlayColor: Colors.white, + activeTrackColor: IconTheme.of(context).color!.withOpacity(0.6), + // disabledThumbColor: Colors.white, + // activeTickMarkColor: Colors.white, + // valueIndicatorColor: Colors.white, + // inactiveTickMarkColor: Colors.white, + // disabledActiveTrackColor: Colors.white, ), child: ExcludeSemantics( child: Slider( diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index 84adbce0c..cb32a4a7a 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -55,12 +55,10 @@ class _QueueListItemState extends State { Widget build(BuildContext context) { return Dismissible( key: Key(widget.item.id), - onDismissed: (direction) { - setState(() { - _queueService.removeAtOffset(widget.indexOffset); - // widget.subqueue.removeAt(widget.listIndex); - }); + onDismissed: (direction) async { Vibrate.feedback(FeedbackType.impact); + await _queueService.removeAtOffset(widget.indexOffset); + setState(() {}); }, child: GestureDetector( onLongPressStart: (details) => showSongMenu(details), From cfe54321a78dcfd0cc13dfd3a87b764cea6c0154 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 24 Sep 2023 16:50:25 +0200 Subject: [PATCH 076/130] add button for clearing next up --- lib/components/PlayerScreen/queue_list.dart | 80 ++++++++++++++++++--- lib/services/queue_service.dart | 14 ++++ 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index c21a7f9d3..c66e0c59d 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -125,7 +125,7 @@ class _QueueListState extends State { ), SliverPersistentHeader( - delegate: SectionHeaderDelegate( + delegate: QueueSectionHeader( title: const Text("Queue"), nextUpHeaderKey: widget.nextUpHeaderKey, )), @@ -215,9 +215,8 @@ class _QueueListState extends State { padding: const EdgeInsets.only(top: 20.0, bottom: 0.0), sliver: SliverPersistentHeader( pinned: false, //TODO use https://stackoverflow.com/a/69372976 to only ever have one of the headers pinned - delegate: SectionHeaderDelegate( - title: Text(AppLocalizations.of(context)!.nextUp), - height: 30.0, + delegate: NextUpSectionHeader( + controls: true, nextUpHeaderKey: widget.nextUpHeaderKey, ), // _source != null ? "Playing from ${_source?.name}" : "Queue", ), @@ -232,7 +231,7 @@ class _QueueListState extends State { padding: const EdgeInsets.only(top: 20.0, bottom: 0.0), sliver: SliverPersistentHeader( pinned: true, - delegate: SectionHeaderDelegate( + delegate: QueueSectionHeader( title: Row( children: [ Text("${AppLocalizations.of(context)!.playingFrom} "), @@ -1109,13 +1108,13 @@ class PlaybackBehaviorInfo { PlaybackBehaviorInfo(this.order, this.loop); } -class SectionHeaderDelegate extends SliverPersistentHeaderDelegate { +class QueueSectionHeader extends SliverPersistentHeaderDelegate { final Widget title; final bool controls; final double height; final GlobalKey nextUpHeaderKey; - SectionHeaderDelegate({ + QueueSectionHeader({ required this.title, required this.nextUpHeaderKey, this.controls = false, @@ -1132,7 +1131,7 @@ class SectionHeaderDelegate extends SliverPersistentHeaderDelegate { _queueService.getLoopModeStream(), (a, b) => PlaybackBehaviorInfo(a, b)), builder: (context, snapshot) { - PlaybackBehaviorInfo? info = snapshot.data as PlaybackBehaviorInfo?; + PlaybackBehaviorInfo? info = snapshot.data; return Container( // color: Colors.black.withOpacity(0.5), @@ -1212,6 +1211,71 @@ class SectionHeaderDelegate extends SliverPersistentHeaderDelegate { bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => false; } +class NextUpSectionHeader extends SliverPersistentHeaderDelegate { + final bool controls; + final double height; + final GlobalKey nextUpHeaderKey; + + NextUpSectionHeader({ + required this.nextUpHeaderKey, + this.controls = false, + this.height = 30.0, + }); + + @override + Widget build(context, double shrinkOffset, bool overlapsContent) { + final _queueService = GetIt.instance(); + + return Container( + // color: Colors.black.withOpacity(0.5), + padding: const EdgeInsets.symmetric(horizontal: 14.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Flex( + direction: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(AppLocalizations.of(context)!.nextUp), + ])), + if (controls) + GestureDetector( + onTap: () { + _queueService.clearNextUp(); + Vibrate.feedback(FeedbackType.success); + }, + child: const Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.only(top: 4.0), + child: Text("Clear Next Up"), + ), + Icon( + TablerIcons.x, + color: Colors.white, + size: 32.0, + ), + ], + ), + ), + ], + ), + ); + } + + @override + double get maxExtent => height; + + @override + double get minExtent => height; + + @override + bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => false; +} + /// If offline, check if an album is downloaded. Always returns true if online. /// Returns false if albumId is null. bool _isAlbumDownloadedIfOffline(String? albumId) { diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index b4d4722d1..4782cb8f0 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -363,6 +363,20 @@ class QueueService { } + Future clearNextUp() async { + + // _queueFromConcatenatingAudioSource(); // update internal queues + + // remove all items from Next Up + if (_queueNextUp.isNotEmpty) { + await _queueAudioSource.removeRange(_queueAudioSourceIndex+1, _queueAudioSourceIndex+1+_queueNextUp.length); + _queueNextUp.clear(); + } + + _queueFromConcatenatingAudioSource(); // update internal queues + + } + QueueInfo getQueue() { return QueueInfo( From 4a5693c003659c03cfe7e34e6b72d1c78f8ef6b5 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 24 Sep 2023 20:05:03 +0200 Subject: [PATCH 077/130] =?UTF-8?q?fixes=20&=20improvements=20=E2=84=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - design tweaks - tried to limit flickering when changing queue - working with flutter 3.13.5, dart 3.1.2 --- .../PlayerScreen/player_buttons_more.dart | 3 +- lib/components/PlayerScreen/queue_list.dart | 132 ++++++++++++------ .../PlayerScreen/queue_list_item.dart | 42 +++--- pubspec.lock | 34 +++-- 4 files changed, 131 insertions(+), 80 deletions(-) diff --git a/lib/components/PlayerScreen/player_buttons_more.dart b/lib/components/PlayerScreen/player_buttons_more.dart index a4c637e53..f227ee39b 100644 --- a/lib/components/PlayerScreen/player_buttons_more.dart +++ b/lib/components/PlayerScreen/player_buttons_more.dart @@ -24,8 +24,9 @@ class PlayerButtonsMore extends StatelessWidget { Radius.circular(15), ), ), - icon: const Icon( + icon: Icon( TablerIcons.menu_2, + color: IconTheme.of(context).color!, ), itemBuilder: (BuildContext context) => >[ diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index c66e0c59d..b97529822 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -29,12 +29,10 @@ import 'queue_list_item.dart'; class _QueueListStreamState { _QueueListStreamState( this.mediaState, - this.playbackPosition, this.queueInfo, ); final MediaState mediaState; - final Duration playbackPosition; final QueueInfo queueInfo; } @@ -59,7 +57,7 @@ class QueueList extends StatefulWidget { void scrollDown() { scrollController.animateTo( scrollController.position.maxScrollExtent, - duration: Duration(seconds: 2), + duration: const Duration(seconds: 2), curve: Curves.fastOutSlowIn, ); } @@ -398,6 +396,7 @@ class _PreviousTracksListState extends State _previousTracks ??= snapshot.data!.previousTracks; return SliverReorderableList( + autoScrollerVelocityScalar: 20.0, onReorder: (oldIndex, newIndex) { int draggingOffset = -(_previousTracks!.length - oldIndex); int newPositionOffset = -(_previousTracks!.length - newIndex); @@ -419,13 +418,21 @@ class _PreviousTracksListState extends State // Feedback.forLongPress(context); Vibrate.feedback(FeedbackType.selection); }, + findChildIndexCallback: (Key key) { + key = key as GlobalObjectKey; + final ValueKey valueKey = key.value as ValueKey; + // search from the back as this is probably more efficient for previous tracks + final index = _previousTracks!.lastIndexWhere((item) => item.id == valueKey.value); + if (index == -1) return null; + return index; + }, itemCount: _previousTracks?.length ?? 0, itemBuilder: (context, index) { final item = _previousTracks![index]; final actualIndex = index; final indexOffset = -((_previousTracks?.length ?? 0) - index); return QueueListItem( - key: ValueKey(_previousTracks![actualIndex].id), + key: ValueKey(item.id), item: item, listIndex: index, actualIndex: actualIndex, @@ -481,10 +488,10 @@ class _NextUpTracksListState extends State { return SliverPadding( padding: const EdgeInsets.only(top: 0.0, left: 8.0, right: 8.0), sliver: SliverReorderableList( + autoScrollerVelocityScalar: 20.0, onReorder: (oldIndex, newIndex) { int draggingOffset = oldIndex + 1; int newPositionOffset = newIndex + 1; - print("$draggingOffset -> $newPositionOffset"); if (mounted) { Vibrate.feedback(FeedbackType.impact); setState(() { @@ -501,13 +508,20 @@ class _NextUpTracksListState extends State { onReorderStart: (p0) { Vibrate.feedback(FeedbackType.selection); }, + findChildIndexCallback: (Key key) { + key = key as GlobalObjectKey; + final ValueKey valueKey = key.value as ValueKey; + final index = _nextUp!.indexWhere((item) => item.id == valueKey.value); + if (index == -1) return null; + return index; + }, itemCount: _nextUp?.length ?? 0, itemBuilder: (context, index) { final item = _nextUp![index]; final actualIndex = index; final indexOffset = index + 1; return QueueListItem( - key: ValueKey(_nextUp![actualIndex].id), + key: ValueKey(item.id), item: item, listIndex: index, actualIndex: actualIndex, @@ -560,20 +574,21 @@ class _QueueTracksListState extends State { _nextUp ??= snapshot.data!.nextUp; return SliverReorderableList( + autoScrollerVelocityScalar: 20.0, onReorder: (oldIndex, newIndex) { int draggingOffset = oldIndex + (_nextUp?.length ?? 0) + 1; int newPositionOffset = newIndex + (_nextUp?.length ?? 0) + 1; print("$draggingOffset -> $newPositionOffset"); if (mounted) { + // update external queue to commit changes, but don't await it + _queueService.reorderByOffset( + draggingOffset, newPositionOffset); Vibrate.feedback(FeedbackType.impact); setState(() { // temporarily update internal queue QueueItem tmp = _queue!.removeAt(oldIndex); _queue!.insert( newIndex < oldIndex ? newIndex : newIndex - 1, tmp); - // update external queue to commit changes, results in a rebuild - _queueService.reorderByOffset( - draggingOffset, newPositionOffset); }); } }, @@ -581,12 +596,20 @@ class _QueueTracksListState extends State { Vibrate.feedback(FeedbackType.selection); }, itemCount: _queue?.length ?? 0, + findChildIndexCallback: (Key key) { + key = key as GlobalObjectKey; + final ValueKey valueKey = key.value as ValueKey; + final index = _queue!.indexWhere((item) => item.id == valueKey.value); + if (index == -1) return null; + return index; + }, itemBuilder: (context, index) { final item = _queue![index]; final actualIndex = index; final indexOffset = index + _nextUp!.length + 1; + return QueueListItem( - key: ValueKey(_queue![actualIndex].id), + key: ValueKey(item.id), item: item, listIndex: index, actualIndex: actualIndex, @@ -642,18 +665,16 @@ class _CurrentTrackState extends State { Duration? playbackPosition; return StreamBuilder<_QueueListStreamState>( - stream: Rx.combineLatest3( mediaStateStream, - AudioService.position - .startWith(_audioHandler.playbackState.value.position), _queueService.getQueueStream(), - (a, b, c) => _QueueListStreamState(a, b, c)), + (a, b) => _QueueListStreamState(a, b)), builder: (context, snapshot) { if (snapshot.hasData) { currentTrack = snapshot.data!.queueInfo.currentTrack; mediaState = snapshot.data!.mediaState; - playbackPosition = snapshot.data!.playbackPosition; + // playbackPosition = snapshot.data!.playbackPosition; jellyfin_models.BaseItemDto? baseItem = currentTrack!.item.extras?["itemJson"] == null ? null @@ -725,7 +746,7 @@ class _CurrentTrackState extends State { TablerIcons.player_play, size: 32, ), - color: Color.fromRGBO(255, 255, 255, 1.0), + color: Colors.white, )), ], ), @@ -736,25 +757,34 @@ class _CurrentTrackState extends State { left: 0, top: 0, // child: RepaintBoundary( - child: Container( - width: 298 * - (playbackPosition!.inMilliseconds / - (mediaState?.mediaItem?.duration ?? - const Duration(seconds: 0)) - .inMilliseconds), - height: 70.0, - decoration: ShapeDecoration( - // color: Color.fromRGBO(188, 136, 86, 0.75), - color: IconTheme.of(context).color!.withOpacity(0.75), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular(8), - bottomRight: Radius.circular(8), - ), - ), - ), + child: StreamBuilder( + stream: AudioService.position + .startWith(_audioHandler.playbackState.value.position), + builder: (context, snapshot) { + if (snapshot.hasData) { + playbackPosition = snapshot.data; + return Container( + width: 298 * + (playbackPosition!.inMilliseconds / + (mediaState?.mediaItem?.duration ?? + const Duration(seconds: 0)) + .inMilliseconds), + height: 70.0, + decoration: ShapeDecoration( + color: IconTheme.of(context).color!.withOpacity(0.75), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + ), + ), + ); + } else { + return Container(); + } + } ), - // ), ), Row( mainAxisSize: MainAxisSize.max, @@ -795,18 +825,32 @@ class _CurrentTrackState extends State { overflow: TextOverflow.ellipsis), ), Row(children: [ - Text( - // '0:00', - playbackPosition!.inHours >= 1.0 - ? "${playbackPosition?.inHours.toString()}:${((playbackPosition?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" - : "${playbackPosition?.inMinutes.toString()}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - style: TextStyle( + StreamBuilder( + stream: AudioService.position + .startWith(_audioHandler.playbackState.value.position), + builder: (context, snapshot) { + final TextStyle style = TextStyle( color: Colors.white.withOpacity(0.8), fontSize: 14, fontFamily: 'Lexend Deca', fontWeight: FontWeight.w400, - ), - ), + ); + if (snapshot.hasData) { + playbackPosition = snapshot.data; + return Text( + // '0:00', + playbackPosition!.inHours >= 1.0 + ? "${playbackPosition?.inHours.toString()}:${((playbackPosition?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" + : "${playbackPosition?.inMinutes.toString()}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + style: style, + ); + } else { + return Text( + "0:00", + style: style, + ); + } + }), const SizedBox(width: 2), Text( '/', @@ -835,7 +879,7 @@ class _CurrentTrackState extends State { ), ],) ], - ) + ), ], ), // ), @@ -904,7 +948,7 @@ class _CurrentTrackState extends State { void showSongMenu(QueueItem currentTrack) async { final item = jellyfin_models.BaseItemDto.fromJson( - currentTrack?.item.extras?["itemJson"]); + currentTrack.item.extras?["itemJson"]); final canGoToAlbum = _isAlbumDownloadedIfOffline(item.parentId); diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index cb32a4a7a..ceb5c1e6d 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -7,7 +7,6 @@ import 'package:finamp/services/audio_service_helper.dart'; import 'package:finamp/services/downloads_helper.dart'; import 'package:finamp/services/finamp_settings_helper.dart'; import 'package:finamp/services/jellyfin_api_helper.dart'; -import 'package:finamp/services/music_player_background_task.dart'; import 'package:finamp/services/process_artist.dart'; import 'package:flutter/material.dart' hide ReorderableList; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -19,17 +18,17 @@ import 'package:flutter_vibrate/flutter_vibrate.dart'; import 'package:get_it/get_it.dart'; class QueueListItem extends StatefulWidget { - late QueueItem item; - late int listIndex; - late int actualIndex; - late int indexOffset; - late List subqueue; - late bool isCurrentTrack; - late bool isPreviousTrack; - late bool allowReorder; - late void Function() onTap; - - QueueListItem({ + final QueueItem item; + final int listIndex; + final int actualIndex; + final int indexOffset; + final List subqueue; + final bool isCurrentTrack; + final bool isPreviousTrack; + final bool allowReorder; + final void Function() onTap; + + const QueueListItem({ Key? key, required this.item, required this.listIndex, @@ -45,14 +44,18 @@ class QueueListItem extends StatefulWidget { State createState() => _QueueListItemState(); } -class _QueueListItemState extends State { - final _audioHandler = GetIt.instance(); +class _QueueListItemState extends State with AutomaticKeepAliveClientMixin { final _audioServiceHelper = GetIt.instance(); final _queueService = GetIt.instance(); final _jellyfinApiHelper = GetIt.instance(); + @override + bool get wantKeepAlive => true; + @override Widget build(BuildContext context) { + super.build(context); + return Dismissible( key: Key(widget.item.id), onDismissed: (direction) async { @@ -89,11 +92,6 @@ class _QueueListItemState extends State { widget.item.item.extras?["itemJson"]), borderRadius: BorderRadius.zero, ), - // leading: Container( - // height: 60.0, - // width: 60.0, - // color: Colors.white, - // ), title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -149,7 +147,7 @@ class _QueueListItemState extends State { margin: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 6.0), // width: widget.allowReorder ? 145.0 : 115.0, - width: widget.allowReorder ? 70.0 : 35.0, + width: widget.allowReorder ? 70.0 : 40.0, height: 50.0, child: Row( mainAxisSize: MainAxisSize.min, @@ -189,9 +187,9 @@ class _QueueListItemState extends State { if (widget.allowReorder) ReorderableDragStartListener( index: widget.listIndex, - child: Padding( + child: const Padding( padding: EdgeInsets.only(bottom: 5.0, left: 6.0), - child: const Icon( + child: Icon( TablerIcons.grip_horizontal, color: Colors.white, size: 28.0, diff --git a/pubspec.lock b/pubspec.lock index 194934fdc..edb47cf23 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -229,10 +229,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" convert: dependency: transitive description: @@ -547,10 +547,10 @@ packages: dependency: "direct main" description: name: intl - sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.18.0" + version: "0.18.1" io: dependency: transitive description: @@ -636,18 +636,18 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: @@ -962,10 +962,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" sqflite: dependency: transitive description: @@ -1042,10 +1042,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" timing: dependency: transitive description: @@ -1126,6 +1126,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -1167,5 +1175,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.0 <4.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=3.7.0" From 2ef5510192d05b94dfc77c0eb8d8f45d969293f0 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Mon, 25 Sep 2023 12:28:13 +0200 Subject: [PATCH 078/130] formatting --- .../AlbumScreen/song_list_tile.dart | 20 - .../playback_history_list.dart | 62 +- lib/components/PlayerScreen/queue_list.dart | 643 +++++++++--------- .../PlayerScreen/queue_list_item.dart | 29 +- lib/models/finamp_models.dart | 11 - lib/screens/playback_history_screen.dart | 3 +- lib/screens/player_screen.dart | 7 +- lib/services/queue_service.dart | 264 ++++--- 8 files changed, 552 insertions(+), 487 deletions(-) diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index bce804db5..1e2b65a72 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -25,7 +25,6 @@ enum SongListTileMenuItems { addToQueue, playNext, addToNextUp, - replaceQueueWithItem, addToPlaylist, removeFromPlaylist, instantMix, @@ -257,13 +256,6 @@ class _SongListTileState extends State { title: Text(AppLocalizations.of(context)!.addToNextUp), ), ), - PopupMenuItem( - value: SongListTileMenuItems.replaceQueueWithItem, - child: ListTile( - leading: const Icon(Icons.play_circle), - title: Text(AppLocalizations.of(context)!.replaceQueue), - ), - ), widget.isInPlaylist ? PopupMenuItem( enabled: !isOffline, @@ -355,18 +347,6 @@ class _SongListTileState extends State { )); break; - case SongListTileMenuItems.replaceQueueWithItem: - // await _audioServiceHelper - // .replaceQueueWithItem(itemList: [widget.item]); - await _queueService.startPlayback(items: [widget.item], source: QueueItemSource(type: QueueItemSourceType.unknown, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: AppLocalizations.of(context)!.queue), id: widget.parentId ?? "unknown")); - - if (!mounted) return; - - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.queueReplaced), - )); - break; - case SongListTileMenuItems.addToPlaylist: Navigator.of(context).pushNamed(AddToPlaylistScreen.routeName, arguments: widget.item.id); diff --git a/lib/components/PlaybackHistoryScreen/playback_history_list.dart b/lib/components/PlaybackHistoryScreen/playback_history_list.dart index 39b488f1a..f3286c92a 100644 --- a/lib/components/PlaybackHistoryScreen/playback_history_list.dart +++ b/lib/components/PlaybackHistoryScreen/playback_history_list.dart @@ -23,7 +23,6 @@ class PlaybackHistoryList extends StatelessWidget { stream: playbackHistoryService.historyStream, builder: (context, snapshot) { if (snapshot.hasData) { - history = snapshot.data; // groupedHistory = playbackHistoryService.getHistoryGroupedByDate(); groupedHistory = playbackHistoryService.getHistoryGroupedByHour(); @@ -36,7 +35,6 @@ class PlaybackHistoryList extends StatelessWidget { return SliverList( delegate: SliverChildBuilderDelegate( (context, index) { - final actualIndex = group.value.length - index - 1; final historyItem = Card( @@ -49,35 +47,46 @@ class PlaybackHistoryList extends StatelessWidget { audioServiceHelper: audioServiceHelper, onTap: () { ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.startingInstantMix), + content: Text(AppLocalizations.of(context)! + .startingInstantMix), )); - audioServiceHelper.startInstantMixForItem(jellyfin_models.BaseItemDto.fromJson(group.value[actualIndex].item.item.extras?["itemJson"])).catchError((e) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.anErrorHasOccured), + audioServiceHelper + .startInstantMixForItem( + jellyfin_models.BaseItemDto.fromJson(group + .value[actualIndex] + .item + .item + .extras?["itemJson"])) + .catchError((e) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)! + .anErrorHasOccured), )); }); }, ), ); - - return index == 0 ? - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 16.0, top: 8.0, bottom: 4.0), - child: Text( - "${group.key.hour % 12} ${group.key.hour >= 12 ? "pm" : "am"}", - style: const TextStyle( - fontSize: 16.0, - ), - ), - ), - historyItem, - ], - ) - : historyItem; + + return index == 0 + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 16.0, top: 8.0, bottom: 4.0), + child: Text( + "${group.key.hour % 12} ${group.key.hour >= 12 ? "pm" : "am"}", + style: const TextStyle( + fontSize: 16.0, + ), + ), + ), + historyItem, + ], + ) + : historyItem; }, childCount: group.value.length, ), @@ -89,9 +98,6 @@ class PlaybackHistoryList extends StatelessWidget { child: CircularProgressIndicator(), ); } - } - - ); + }); } - } diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index b97529822..3cf8ee93a 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -43,8 +43,7 @@ class QueueList extends StatefulWidget { required this.previousTracksHeaderKey, required this.currentTrackKey, required this.nextUpHeaderKey, - }) - : super(key: key); + }) : super(key: key); final ScrollController scrollController; final GlobalKey previousTracksHeaderKey; @@ -106,21 +105,20 @@ class _QueueListState extends State { ), // Current Track SliverAppBar( - key: UniqueKey(), - pinned: true, - collapsedHeight: 70.0, - expandedHeight: 70.0, - leading: const Padding( - padding: EdgeInsets.zero, - ), - flexibleSpace: ListTile( - leading: const AlbumImage( - item: null, - ), - title: const Text("unknown"), - subtitle: const Text("unknown"), - onTap: () {}), - + key: UniqueKey(), + pinned: true, + collapsedHeight: 70.0, + expandedHeight: 70.0, + leading: const Padding( + padding: EdgeInsets.zero, + ), + flexibleSpace: ListTile( + leading: const AlbumImage( + item: null, + ), + title: const Text("unknown"), + subtitle: const Text("unknown"), + onTap: () {}), ), SliverPersistentHeader( delegate: QueueSectionHeader( @@ -133,20 +131,9 @@ class _QueueListState extends State { children: const [], ), ]; - } void scrollToCurrentTrack() { - // dynamic box = currentTrackKey.currentContext!.findRenderObject(); - // Offset position = box; //this is global position - // double y = position.dy; - - // widget.scrollController.animateTo( - // y, - // // scrollController.position.maxScrollExtent, - // duration: Duration(seconds: 2), - // curve: Curves.fastOutSlowIn, - // ); if (widget.previousTracksHeaderKey.currentContext != null) { Scrollable.ensureVisible( widget.previousTracksHeaderKey.currentContext!, @@ -161,43 +148,46 @@ class _QueueListState extends State { _contents = [ // Previous Tracks if (isRecentTracksExpanded) - PreviousTracksList(previousTracksHeaderKey: widget.previousTracksHeaderKey) - , + PreviousTracksList( + previousTracksHeaderKey: widget.previousTracksHeaderKey), //TODO replace this with a SliverPersistentHeader and add an `onTap` callback to the delegate SliverToBoxAdapter( - key: widget.previousTracksHeaderKey, - child: GestureDetector( - onTap:() { - Vibrate.feedback(FeedbackType.selection); - setState(() => isRecentTracksExpanded = !isRecentTracksExpanded); - if (!isRecentTracksExpanded) { - Future.delayed(const Duration(milliseconds: 200), () => scrollToCurrentTrack()); - } - // else { - // Future.delayed(const Duration(milliseconds: 300), () => scrollToCurrentTrack()); - // } - }, - child: Padding( - padding: const EdgeInsets.only(left: 14.0, right: 14.0, bottom: 12.0, top: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(top: 2.0), - child: Text(AppLocalizations.of(context)!.recentlyPlayed), - ), - const SizedBox(width: 4.0), - Icon( - isRecentTracksExpanded ? TablerIcons.chevron_up : TablerIcons.chevron_down, - size: 28.0, - color: Colors.white, - ), - ], + key: widget.previousTracksHeaderKey, + child: GestureDetector( + onTap: () { + Vibrate.feedback(FeedbackType.selection); + setState(() => isRecentTracksExpanded = !isRecentTracksExpanded); + if (!isRecentTracksExpanded) { + Future.delayed(const Duration(milliseconds: 200), + () => scrollToCurrentTrack()); + } + // else { + // Future.delayed(const Duration(milliseconds: 300), () => scrollToCurrentTrack()); + // } + }, + child: Padding( + padding: const EdgeInsets.only( + left: 14.0, right: 14.0, bottom: 12.0, top: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 2.0), + child: Text(AppLocalizations.of(context)!.recentlyPlayed), + ), + const SizedBox(width: 4.0), + Icon( + isRecentTracksExpanded + ? TablerIcons.chevron_up + : TablerIcons.chevron_down, + size: 28.0, + color: Colors.white, + ), + ], + ), ), - ), - ) - ), + )), CurrentTrack( // key: UniqueKey(), key: widget.currentTrackKey, @@ -212,7 +202,8 @@ class _QueueListState extends State { // key: widget.nextUpHeaderKey, padding: const EdgeInsets.only(top: 20.0, bottom: 0.0), sliver: SliverPersistentHeader( - pinned: false, //TODO use https://stackoverflow.com/a/69372976 to only ever have one of the headers pinned + pinned: + false, //TODO use https://stackoverflow.com/a/69372976 to only ever have one of the headers pinned delegate: NextUpSectionHeader( controls: true, nextUpHeaderKey: widget.nextUpHeaderKey, @@ -233,7 +224,9 @@ class _QueueListState extends State { title: Row( children: [ Text("${AppLocalizations.of(context)!.playingFrom} "), - Text(_source?.name.getLocalized(context) ?? AppLocalizations.of(context)!.unknownName, + Text( + _source?.name.getLocalized(context) ?? + AppLocalizations.of(context)!.unknownName, style: const TextStyle(fontWeight: FontWeight.w500)), ], ), @@ -273,101 +266,103 @@ Future showQueueBottomSheet(BuildContext context) { context: context, builder: (context) { return Consumer( - builder: (BuildContext context, WidgetRef ref, Widget? child) { - - final imageTheme = ref.watch(playerScreenThemeProvider); - - return Theme( - data: ThemeData( - fontFamily: "LexendDeca", - colorScheme: imageTheme, - brightness: Theme.of(context).brightness, - iconTheme: Theme.of(context).iconTheme.copyWith( - color: imageTheme?.primary, - ), - ), - child: DraggableScrollableSheet( - snap: false, - snapAnimationDuration: const Duration(milliseconds: 200), - initialChildSize: 0.92, - // maxChildSize: 0.92, - expand: false, - builder: (context, scrollController) { - return Scaffold( - body: Stack( - children: [ - if (FinampSettingsHelper + builder: (BuildContext context, WidgetRef ref, Widget? child) { + final imageTheme = ref.watch(playerScreenThemeProvider); + + return Theme( + data: ThemeData( + fontFamily: "LexendDeca", + colorScheme: imageTheme, + brightness: Theme.of(context).brightness, + iconTheme: Theme.of(context).iconTheme.copyWith( + color: imageTheme?.primary, + ), + ), + child: DraggableScrollableSheet( + snap: false, + snapAnimationDuration: const Duration(milliseconds: 200), + initialChildSize: 0.92, + // maxChildSize: 0.92, + expand: false, + builder: (context, scrollController) { + return Scaffold( + body: Stack( + children: [ + if (FinampSettingsHelper .finampSettings.showCoverAsPlayerBackground) - BlurredPlayerScreenBackground(brightnessFactor: Theme.of(context).brightness == Brightness.dark ? 1.0 : 1.0), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 10), - Container( - width: 40, - height: 3.5, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(3.5), - ), + BlurredPlayerScreenBackground( + brightnessFactor: + Theme.of(context).brightness == Brightness.dark + ? 1.0 + : 1.0), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 10), + Container( + width: 40, + height: 3.5, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.5), ), - const SizedBox(height: 10), - Text(AppLocalizations.of(context)!.queue, - style: const TextStyle( - color: Colors.white, - fontFamily: 'Lexend Deca', - fontSize: 18, - fontWeight: FontWeight.w300)), - const SizedBox(height: 20), - Expanded( - child: QueueList( - scrollController: scrollController, - previousTracksHeaderKey: previousTracksHeaderKey, - currentTrackKey: currentTrackKey, - nextUpHeaderKey: nextUpHeaderKey, - ), + ), + const SizedBox(height: 10), + Text(AppLocalizations.of(context)!.queue, + style: const TextStyle( + color: Colors.white, + fontFamily: 'Lexend Deca', + fontSize: 18, + fontWeight: FontWeight.w300)), + const SizedBox(height: 20), + Expanded( + child: QueueList( + scrollController: scrollController, + previousTracksHeaderKey: previousTracksHeaderKey, + currentTrackKey: currentTrackKey, + nextUpHeaderKey: nextUpHeaderKey, ), - ], - ), - ], - ), - //TODO fade this out if the key is visible - floatingActionButton: FloatingActionButton( - onPressed: () { - Vibrate.feedback(FeedbackType.impact); - scrollToKey( + ), + ], + ), + ], + ), + //TODO fade this out if the key is visible + floatingActionButton: FloatingActionButton( + onPressed: () { + Vibrate.feedback(FeedbackType.impact); + scrollToKey( key: previousTracksHeaderKey, duration: const Duration(milliseconds: 500)); - }, - backgroundColor: IconTheme.of(context).color!.withOpacity(0.70), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(16.0))), - child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Icon( - TablerIcons.focus_2, - size: 28.0, - color: Colors.white.withOpacity(0.85), - ), - )), - ); - // ) - // return QueueList( - // scrollController: scrollController, - // ); - }, - ), - ); - } - ); + }, + backgroundColor: + IconTheme.of(context).color!.withOpacity(0.70), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0))), + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Icon( + TablerIcons.focus_2, + size: 28.0, + color: Colors.white.withOpacity(0.85), + ), + )), + ); + // ) + // return QueueList( + // scrollController: scrollController, + // ); + }, + ), + ); + }); }, ); } class PreviousTracksList extends StatefulWidget { - final GlobalKey previousTracksHeaderKey; - + const PreviousTracksList({ Key? key, required this.previousTracksHeaderKey, @@ -385,11 +380,6 @@ class _PreviousTracksListState extends State @override Widget build(context) { return StreamBuilder( - // stream: AudioService.queueStream, - // stream: Rx.combineLatest2( - // mediaStateStream, - // _queueService.getQueueStream(), - // (a, b) => _QueueListStreamState(a, b)), stream: _queueService.getQueueStream(), builder: (context, snapshot) { if (snapshot.hasData) { @@ -415,14 +405,14 @@ class _PreviousTracksListState extends State } }, onReorderStart: (p0) { - // Feedback.forLongPress(context); Vibrate.feedback(FeedbackType.selection); }, findChildIndexCallback: (Key key) { key = key as GlobalObjectKey; final ValueKey valueKey = key.value as ValueKey; // search from the back as this is probably more efficient for previous tracks - final index = _previousTracks!.lastIndexWhere((item) => item.id == valueKey.value); + final index = _previousTracks! + .lastIndexWhere((item) => item.id == valueKey.value); if (index == -1) return null; return index; }, @@ -443,7 +433,9 @@ class _PreviousTracksListState extends State onTap: () async { Vibrate.feedback(FeedbackType.selection); await _queueService.skipByOffset(indexOffset); - scrollToKey(key: widget.previousTracksHeaderKey, duration: const Duration(milliseconds: 500)); + scrollToKey( + key: widget.previousTracksHeaderKey, + duration: const Duration(milliseconds: 500)); }, isCurrentTrack: false, isPreviousTrack: true, @@ -459,9 +451,8 @@ class _PreviousTracksListState extends State } class NextUpTracksList extends StatefulWidget { - final GlobalKey previousTracksHeaderKey; - + const NextUpTracksList({ Key? key, required this.previousTracksHeaderKey, @@ -478,9 +469,7 @@ class _NextUpTracksListState extends State { @override Widget build(context) { return StreamBuilder( - // stream: AudioService.queueStream, stream: _queueService.getQueueStream(), - // stream: _queueService.getQueueStream(), builder: (context, snapshot) { if (snapshot.hasData) { _nextUp ??= snapshot.data!.nextUp; @@ -510,8 +499,10 @@ class _NextUpTracksListState extends State { }, findChildIndexCallback: (Key key) { key = key as GlobalObjectKey; - final ValueKey valueKey = key.value as ValueKey; - final index = _nextUp!.indexWhere((item) => item.id == valueKey.value); + final ValueKey valueKey = + key.value as ValueKey; + final index = + _nextUp!.indexWhere((item) => item.id == valueKey.value); if (index == -1) return null; return index; }, @@ -530,7 +521,9 @@ class _NextUpTracksListState extends State { onTap: () async { Vibrate.feedback(FeedbackType.selection); await _queueService.skipByOffset(indexOffset); - scrollToKey(key: widget.previousTracksHeaderKey, duration: const Duration(milliseconds: 500)); + scrollToKey( + key: widget.previousTracksHeaderKey, + duration: const Duration(milliseconds: 500)); }, isCurrentTrack: false, ); @@ -545,9 +538,8 @@ class _NextUpTracksListState extends State { } class QueueTracksList extends StatefulWidget { - final GlobalKey previousTracksHeaderKey; - + const QueueTracksList({ Key? key, required this.previousTracksHeaderKey, @@ -565,9 +557,7 @@ class _QueueTracksListState extends State { @override Widget build(context) { return StreamBuilder( - // stream: AudioService.queueStream, stream: _queueService.getQueueStream(), - // stream: _queueService.getQueueStream(), builder: (context, snapshot) { if (snapshot.hasData) { _queue ??= snapshot.data!.queue; @@ -599,7 +589,8 @@ class _QueueTracksListState extends State { findChildIndexCallback: (Key key) { key = key as GlobalObjectKey; final ValueKey valueKey = key.value as ValueKey; - final index = _queue!.indexWhere((item) => item.id == valueKey.value); + final index = + _queue!.indexWhere((item) => item.id == valueKey.value); if (index == -1) return null; return index; }, @@ -620,7 +611,9 @@ class _QueueTracksListState extends State { onTap: () async { Vibrate.feedback(FeedbackType.selection); await _queueService.skipByOffset(indexOffset); - scrollToKey(key: widget.previousTracksHeaderKey, duration: const Duration(milliseconds: 500)); + scrollToKey( + key: widget.previousTracksHeaderKey, + duration: const Duration(milliseconds: 500)); }, isCurrentTrack: false, ); @@ -665,8 +658,7 @@ class _CurrentTrackState extends State { Duration? playbackPosition; return StreamBuilder<_QueueListStreamState>( - stream: Rx.combineLatest2( + stream: Rx.combineLatest2( mediaStateStream, _queueService.getQueueStream(), (a, b) => _QueueListStreamState(a, b)), @@ -674,15 +666,14 @@ class _CurrentTrackState extends State { if (snapshot.hasData) { currentTrack = snapshot.data!.queueInfo.currentTrack; mediaState = snapshot.data!.mediaState; - // playbackPosition = snapshot.data!.playbackPosition; - jellyfin_models.BaseItemDto? baseItem = currentTrack!.item.extras?["itemJson"] == null - ? null - : jellyfin_models.BaseItemDto.fromJson( - currentTrack!.item.extras?["itemJson"]); + jellyfin_models.BaseItemDto? baseItem = + currentTrack!.item.extras?["itemJson"] == null + ? null + : jellyfin_models.BaseItemDto.fromJson( + currentTrack!.item.extras?["itemJson"]); return SliverAppBar( - // key: currentTrackKey, pinned: true, collapsedHeight: 70.0, expandedHeight: 70.0, @@ -698,7 +689,9 @@ class _CurrentTrackState extends State { child: Container( clipBehavior: Clip.antiAlias, decoration: ShapeDecoration( - color: Color.alphaBlend(IconTheme.of(context).color!.withOpacity(0.35), Colors.black), + color: Color.alphaBlend( + IconTheme.of(context).color!.withOpacity(0.35), + Colors.black), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8.0)), ), @@ -717,10 +710,12 @@ class _CurrentTrackState extends State { topLeft: Radius.circular(8), bottomLeft: Radius.circular(8), ), - itemsToPrecache: _queueService.getNextXTracksInQueue(3).map((e) { + itemsToPrecache: + _queueService.getNextXTracksInQueue(3).map((e) { final item = e.item.extras?["itemJson"] != null ? jellyfin_models.BaseItemDto.fromJson( - e.item.extras!["itemJson"] as Map) + e.item.extras!["itemJson"] + as Map) : null; return item!; }).toList(), @@ -756,35 +751,37 @@ class _CurrentTrackState extends State { Positioned( left: 0, top: 0, - // child: RepaintBoundary( child: StreamBuilder( - stream: AudioService.position - .startWith(_audioHandler.playbackState.value.position), - builder: (context, snapshot) { - if (snapshot.hasData) { - playbackPosition = snapshot.data; - return Container( - width: 298 * - (playbackPosition!.inMilliseconds / - (mediaState?.mediaItem?.duration ?? - const Duration(seconds: 0)) - .inMilliseconds), - height: 70.0, - decoration: ShapeDecoration( - color: IconTheme.of(context).color!.withOpacity(0.75), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular(8), - bottomRight: Radius.circular(8), + stream: AudioService.position.startWith( + _audioHandler.playbackState.value.position), + builder: (context, snapshot) { + if (snapshot.hasData) { + playbackPosition = snapshot.data; + return Container( + width: 298 * + (playbackPosition!.inMilliseconds / + (mediaState?.mediaItem + ?.duration ?? + const Duration( + seconds: 0)) + .inMilliseconds), + height: 70.0, + decoration: ShapeDecoration( + color: IconTheme.of(context) + .color! + .withOpacity(0.75), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(8), + bottomRight: Radius.circular(8), + ), ), ), - ), - ); - } else { - return Container(); - } - } - ), + ); + } else { + return Container(); + } + }), ), Row( mainAxisSize: MainAxisSize.max, @@ -795,14 +792,15 @@ class _CurrentTrackState extends State { width: 222, padding: const EdgeInsets.only(left: 12, right: 4), - // child: Expanded( child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - currentTrack?.item.title ?? AppLocalizations.of(context)!.unknownName, + currentTrack?.item.title ?? + AppLocalizations.of(context)! + .unknownName, style: const TextStyle( color: Colors.white, fontSize: 16, @@ -812,77 +810,91 @@ class _CurrentTrackState extends State { ), const SizedBox(height: 4), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ Text( processArtist( - currentTrack!.item.artist, context), + currentTrack!.item.artist, + context), style: TextStyle( - color: Colors.white.withOpacity(0.85), + color: Colors.white + .withOpacity(0.85), fontSize: 13, fontFamily: 'Lexend Deca', fontWeight: FontWeight.w300, overflow: TextOverflow.ellipsis), ), - Row(children: [ - StreamBuilder( - stream: AudioService.position - .startWith(_audioHandler.playbackState.value.position), - builder: (context, snapshot) { - final TextStyle style = TextStyle( - color: Colors.white.withOpacity(0.8), - fontSize: 14, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w400, - ); - if (snapshot.hasData) { - playbackPosition = snapshot.data; - return Text( - // '0:00', - playbackPosition!.inHours >= 1.0 - ? "${playbackPosition?.inHours.toString()}:${((playbackPosition?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" - : "${playbackPosition?.inMinutes.toString()}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - style: style, - ); - } else { - return Text( - "0:00", - style: style, - ); - } - }), - const SizedBox(width: 2), - Text( - '/', - style: TextStyle( - color: Colors.white.withOpacity(0.8), - fontSize: 14, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w400, - ), - ), - const SizedBox(width: 2), - Text( - // '3:44', - (mediaState?.mediaItem?.duration - ?.inHours ?? - 0.0) >= - 1.0 - ? "${mediaState?.mediaItem?.duration?.inHours.toString()}:${((mediaState?.mediaItem?.duration?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((mediaState?.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" - : "${mediaState?.mediaItem?.duration?.inMinutes.toString()}:${((mediaState?.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - style: TextStyle( - color: Colors.white.withOpacity(0.8), - fontSize: 14, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w400, - ), - ), - ],) + Row( + children: [ + StreamBuilder( + stream: AudioService.position + .startWith(_audioHandler + .playbackState + .value + .position), + builder: (context, snapshot) { + final TextStyle style = + TextStyle( + color: Colors.white + .withOpacity(0.8), + fontSize: 14, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w400, + ); + if (snapshot.hasData) { + playbackPosition = + snapshot.data; + return Text( + // '0:00', + playbackPosition! + .inHours >= + 1.0 + ? "${playbackPosition?.inHours.toString()}:${((playbackPosition?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" + : "${playbackPosition?.inMinutes.toString()}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + style: style, + ); + } else { + return Text( + "0:00", + style: style, + ); + } + }), + const SizedBox(width: 2), + Text( + '/', + style: TextStyle( + color: Colors.white + .withOpacity(0.8), + fontSize: 14, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(width: 2), + Text( + // '3:44', + (mediaState?.mediaItem?.duration + ?.inHours ?? + 0.0) >= + 1.0 + ? "${mediaState?.mediaItem?.duration?.inHours.toString()}:${((mediaState?.mediaItem?.duration?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((mediaState?.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" + : "${mediaState?.mediaItem?.duration?.inMinutes.toString()}:${((mediaState?.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + style: TextStyle( + color: Colors.white + .withOpacity(0.8), + fontSize: 14, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w400, + ), + ), + ], + ) ], ), ], ), - // ), ), Row( mainAxisAlignment: MainAxisAlignment.end, @@ -891,21 +903,28 @@ class _CurrentTrackState extends State { padding: const EdgeInsets.only(top: 4.0), child: IconButton( iconSize: 16, - visualDensity: const VisualDensity(horizontal: -4), - icon: jellyfin_models.BaseItemDto.fromJson(currentTrack!.item.extras?["itemJson"]).userData!.isFavorite ? Icon( - Icons.favorite, - size: 28, - color: IconTheme.of(context).color!, - fill: 1.0, - weight: - 1.5, - ) : const Icon( - Icons.favorite_outline, - size: 28, - color: Colors.white, - weight: - 1.5, - ), + visualDensity: + const VisualDensity(horizontal: -4), + icon: + jellyfin_models.BaseItemDto.fromJson( + currentTrack!.item + .extras?["itemJson"]) + .userData! + .isFavorite + ? Icon( + Icons.favorite, + size: 28, + color: IconTheme.of(context) + .color!, + fill: 1.0, + weight: 1.5, + ) + : const Icon( + Icons.favorite_outline, + size: 28, + color: Colors.white, + weight: 1.5, + ), onPressed: () { Vibrate.feedback(FeedbackType.success); setState(() { @@ -916,7 +935,8 @@ class _CurrentTrackState extends State { ), IconButton( iconSize: 28, - visualDensity: const VisualDensity(horizontal: -4), + visualDensity: + const VisualDensity(horizontal: -4), // visualDensity: VisualDensity.compact, icon: const Icon( TablerIcons.dots_vertical, @@ -1034,7 +1054,9 @@ class _CurrentTrackState extends State { item, QueueItemSource( type: QueueItemSourceType.unknown, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: AppLocalizations.of(context)!.queue), + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: AppLocalizations.of(context)!.queue), id: currentTrack.source.id)); if (!mounted) return; @@ -1060,7 +1082,8 @@ class _CurrentTrackState extends State { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.confirmAddToNextUp("track")), + content: + Text(AppLocalizations.of(context)!.confirmAddToNextUp("track")), )); break; @@ -1116,8 +1139,9 @@ class _CurrentTrackState extends State { // We switch the widget state before actually doing the request to // make the app feel faster (without, there is a delay from the // user adding the favourite and the icon showing) - jellyfin_models.BaseItemDto item = jellyfin_models.BaseItemDto.fromJson(track.item.extras!["itemJson"]); - + jellyfin_models.BaseItemDto item = + jellyfin_models.BaseItemDto.fromJson(track.item.extras!["itemJson"]); + setState(() { item.userData!.isFavorite = !item.userData!.isFavorite; }); @@ -1128,7 +1152,6 @@ class _CurrentTrackState extends State { ? await _jellyfinApiHelper.addFavourite(item.id) : await _jellyfinApiHelper.removeFavourite(item.id); - item.userData = newUserData; if (!mounted) return; @@ -1138,7 +1161,6 @@ class _CurrentTrackState extends State { }); _queueService.refreshQueueStream(); - } catch (e) { errorSnackbar(e, context); } @@ -1217,27 +1239,26 @@ class QueueSectionHeader extends SliverPersistentHeaderDelegate { }), if (controls) IconButton( - padding: const EdgeInsets.only(bottom: 2.0), - iconSize: 28.0, - icon: info?.loop != LoopMode.none - ? (info?.loop == LoopMode.one - ? (const Icon( - TablerIcons.repeat_once, - )) - : (const Icon( - TablerIcons.repeat, - ))) - : (const Icon( - TablerIcons.repeat_off, - )), - color: info?.loop != LoopMode.none - ? IconTheme.of(context).color! - : Colors.white, - onPressed: () { - _queueService.toggleLoopMode(); - Vibrate.feedback(FeedbackType.success); - } - ), + padding: const EdgeInsets.only(bottom: 2.0), + iconSize: 28.0, + icon: info?.loop != LoopMode.none + ? (info?.loop == LoopMode.one + ? (const Icon( + TablerIcons.repeat_once, + )) + : (const Icon( + TablerIcons.repeat, + ))) + : (const Icon( + TablerIcons.repeat_off, + )), + color: info?.loop != LoopMode.none + ? IconTheme.of(context).color! + : Colors.white, + onPressed: () { + _queueService.toggleLoopMode(); + Vibrate.feedback(FeedbackType.success); + }), ], ), ); @@ -1286,8 +1307,8 @@ class NextUpSectionHeader extends SliverPersistentHeaderDelegate { if (controls) GestureDetector( onTap: () { - _queueService.clearNextUp(); - Vibrate.feedback(FeedbackType.success); + _queueService.clearNextUp(); + Vibrate.feedback(FeedbackType.success); }, child: const Row( mainAxisAlignment: MainAxisAlignment.end, diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index ceb5c1e6d..f855b9101 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -44,7 +44,8 @@ class QueueListItem extends StatefulWidget { State createState() => _QueueListItemState(); } -class _QueueListItemState extends State with AutomaticKeepAliveClientMixin { +class _QueueListItemState extends State + with AutomaticKeepAliveClientMixin { final _audioServiceHelper = GetIt.instance(); final _queueService = GetIt.instance(); final _jellyfinApiHelper = GetIt.instance(); @@ -80,8 +81,8 @@ class _QueueListItemState extends State with AutomaticKeepAliveCl visualDensity: VisualDensity.standard, minVerticalPadding: 0.0, horizontalTitleGap: 10.0, - contentPadding: - const EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0), + contentPadding: const EdgeInsets.symmetric( + vertical: 0.0, horizontal: 0.0), tileColor: widget.isCurrentTrack ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) : null, @@ -101,7 +102,8 @@ class _QueueListItemState extends State with AutomaticKeepAliveCl widget.item.item.title, style: this.widget.isCurrentTrack ? TextStyle( - color: Theme.of(context).colorScheme.secondary, + color: + Theme.of(context).colorScheme.secondary, fontSize: 16, fontFamily: 'Lexend Deca', fontWeight: FontWeight.w400, @@ -309,7 +311,9 @@ class _QueueListItemState extends State with AutomaticKeepAliveCl widget.item.item.extras?["itemJson"]), QueueItemSource( type: QueueItemSourceType.unknown, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: AppLocalizations.of(context)!.queue), + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: AppLocalizations.of(context)!.queue), id: widget.item.source.id)); if (!mounted) return; @@ -320,8 +324,10 @@ class _QueueListItemState extends State with AutomaticKeepAliveCl break; case SongListTileMenuItems.playNext: - await _queueService.addNext(items: [jellyfin_models.BaseItemDto.fromJson( - widget.item.item.extras?["itemJson"])]); + await _queueService.addNext(items: [ + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]) + ]); if (!mounted) return; @@ -331,13 +337,16 @@ class _QueueListItemState extends State with AutomaticKeepAliveCl break; case SongListTileMenuItems.addToNextUp: - await _queueService.addToNextUp(items: [jellyfin_models.BaseItemDto.fromJson( - widget.item.item.extras?["itemJson"])]); + await _queueService.addToNextUp(items: [ + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]) + ]); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.confirmAddToNextUp("track")), + content: + Text(AppLocalizations.of(context)!.confirmAddToNextUp("track")), )); break; diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index a5f2d08d1..96ec669f5 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -557,7 +557,6 @@ class DownloadedImage { } enum QueueItemSourceType { - album(name: "album"), playlist(name: "playlist"), songMix(name: "songMix"), @@ -581,12 +580,10 @@ enum QueueItemSourceType { } enum QueueItemQueueType { - previousTracks, currentTrack, nextUp, queue; - } class QueueItemSource { @@ -608,7 +605,6 @@ class QueueItemSource { @HiveField(3) BaseItemDto? item; - } enum QueueItemSourceNameType { @@ -622,7 +618,6 @@ enum QueueItemSourceNameType { } class QueueItemSourceName { - const QueueItemSourceName({ required this.type, this.pretranslatedName, @@ -673,11 +668,9 @@ class QueueItem { @HiveField(3) QueueItemQueueType type; - } class QueueOrder { - QueueOrder({ required this.items, required this.originalSource, @@ -700,11 +693,9 @@ class QueueOrder { /// The integers at index x contains the index of the item within [items] at queue position x. @HiveField(3) List shuffledOrder; - } class QueueInfo { - QueueInfo({ required this.previousTracks, required this.currentTrack, @@ -727,7 +718,6 @@ class QueueInfo { @HiveField(4) QueueItemSource source; - } class HistoryItem { @@ -745,5 +735,4 @@ class HistoryItem { @HiveField(2) DateTime? endTime; - } diff --git a/lib/screens/playback_history_screen.dart b/lib/screens/playback_history_screen.dart index f759c16b2..3c9d9b7af 100644 --- a/lib/screens/playback_history_screen.dart +++ b/lib/screens/playback_history_screen.dart @@ -5,7 +5,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../components/finamp_app_bar_button.dart'; - class PlaybackHistoryScreen extends StatelessWidget { const PlaybackHistoryScreen({Key? key}) : super(key: key); @@ -28,6 +27,6 @@ class PlaybackHistoryScreen extends StatelessWidget { child: PlaybackHistoryList(), ), bottomNavigationBar: const NowPlayingBar(), -); + ); } } diff --git a/lib/screens/player_screen.dart b/lib/screens/player_screen.dart index e614bebc2..bb4cec5b2 100644 --- a/lib/screens/player_screen.dart +++ b/lib/screens/player_screen.dart @@ -37,8 +37,8 @@ class PlayerScreen extends ConsumerWidget { colorScheme: imageTheme, brightness: Theme.of(context).brightness, iconTheme: Theme.of(context).iconTheme.copyWith( - color: imageTheme?.primary, - ), + color: imageTheme?.primary, + ), ), child: const _PlayerScreenContent(), ); @@ -78,8 +78,7 @@ class _PlayerScreenContent extends StatelessWidget { resizeToAvoidBottomInset: false, extendBodyBehindAppBar: true, body: Stack( children: [ - if (FinampSettingsHelper - .finampSettings.showCoverAsPlayerBackground) + if (FinampSettingsHelper.finampSettings.showCoverAsPlayerBackground) const BlurredPlayerScreenBackground(), const SafeArea( minimum: EdgeInsets.only(top: _toolbarHeight), diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 4782cb8f0..1045a4c43 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -22,6 +22,7 @@ import 'music_player_background_task.dart'; import 'package:finamp/services/playback_history_service.dart'; enum PlaybackOrder { shuffled, linear } + enum LoopMode { none, one, all } /// A track queueing service for Finamp. @@ -34,27 +35,60 @@ class QueueService { // internal state - List _queuePreviousTracks = []; // contains **all** items that have been played, including "next up" + List _queuePreviousTracks = + []; // contains **all** items that have been played, including "next up" QueueItem? _currentTrack; // the currently playing track - List _queueNextUp = []; // a temporary queue that gets appended to if the user taps "next up" + List _queueNextUp = + []; // a temporary queue that gets appended to if the user taps "next up" List _queue = []; // contains all regular queue items - QueueOrder _order = QueueOrder(items: [], originalSource: QueueItemSource(id: "", name: const QueueItemSourceName(type: QueueItemSourceNameType.preTranslated), type: QueueItemSourceType.unknown), linearOrder: [], shuffledOrder: []); // contains all items that were at some point added to the regular queue, as well as their order when shuffle is enabled and disabled. This is used to loop the original queue once the end has been reached and "loop all" is enabled, **excluding** "next up" items and keeping the playback order. + QueueOrder _order = QueueOrder( + items: [], + originalSource: QueueItemSource( + id: "", + name: const QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated), + type: QueueItemSourceType.unknown), + linearOrder: [], + shuffledOrder: []); // contains all items that were at some point added to the regular queue, as well as their order when shuffle is enabled and disabled. This is used to loop the original queue once the end has been reached and "loop all" is enabled, **excluding** "next up" items and keeping the playback order. PlaybackOrder _playbackOrder = PlaybackOrder.linear; LoopMode _loopMode = LoopMode.none; - final _currentTrackStream = BehaviorSubject.seeded( - QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: const QueueItemSourceName(type: QueueItemSourceNameType.preTranslated), type: QueueItemSourceType.unknown)) - ); + final _currentTrackStream = BehaviorSubject.seeded(QueueItem( + item: const MediaItem( + id: "", + title: "No track playing", + album: "No album", + artist: "No artist"), + source: QueueItemSource( + id: "", + name: const QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated), + type: QueueItemSourceType.unknown))); final _queueStream = BehaviorSubject.seeded(QueueInfo( previousTracks: [], - currentTrack: QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: const QueueItemSourceName(type: QueueItemSourceNameType.preTranslated), type: QueueItemSourceType.unknown)), + currentTrack: QueueItem( + item: const MediaItem( + id: "", + title: "No track playing", + album: "No album", + artist: "No artist"), + source: QueueItemSource( + id: "", + name: const QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated), + type: QueueItemSourceType.unknown)), queue: [], nextUp: [], - source: QueueItemSource(id: "", name: const QueueItemSourceName(type: QueueItemSourceNameType.preTranslated), type: QueueItemSourceType.unknown), - )); - - final _playbackOrderStream = BehaviorSubject.seeded(PlaybackOrder.linear); + source: QueueItemSource( + id: "", + name: const QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated), + type: QueueItemSourceType.unknown), + )); + + final _playbackOrderStream = + BehaviorSubject.seeded(PlaybackOrder.linear); final _loopModeStream = BehaviorSubject.seeded(LoopMode.none); // external queue state @@ -67,7 +101,6 @@ class QueueService { int _queueAudioSourceIndex = 0; QueueService() { - _shuffleOrder = NextUpShuffleOrder(queueService: this); _queueAudioSource = ConcatenatingAudioSource( children: [], @@ -75,16 +108,15 @@ class QueueService { ); _audioHandler.getPlaybackEventStream().listen((event) async { - // int indexDifference = (event.currentIndex ?? 0) - _queueAudioSourceIndex; // _queueServiceLogger.finer("Play queue index changed, difference: $indexDifference"); _queueAudioSourceIndex = event.currentIndex ?? 0; - _queueServiceLogger.finer("Play queue index changed, new index: $_queueAudioSourceIndex"); - - _queueFromConcatenatingAudioSource(); + _queueServiceLogger.finer( + "Play queue index changed, new index: $_queueAudioSourceIndex"); + _queueFromConcatenatingAudioSource(); }); // register callbacks @@ -93,13 +125,17 @@ class QueueService { // previousTrackCallback: _applyPreviousTrack, // skipToIndexCallback: _applySkipToTrackByOffset, // ); - } void _queueFromConcatenatingAudioSource() { - - List allTracks = _audioHandler.effectiveSequence?.map((e) => e.tag as QueueItem).toList() ?? []; - int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; + List allTracks = _audioHandler.effectiveSequence + ?.map((e) => e.tag as QueueItem) + .toList() ?? + []; + int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && + _queueAudioSource.shuffleIndices.isNotEmpty) + ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) + : _queueAudioSourceIndex; _queuePreviousTracks.clear(); _queueNextUp.clear(); @@ -107,11 +143,15 @@ class QueueService { // split the queue by old type for (int i = 0; i < allTracks.length; i++) { - if (i < adjustedQueueIndex) { _queuePreviousTracks.add(allTracks[i]); - if (_queuePreviousTracks.last.source.type == QueueItemSourceType.nextUp) { - _queuePreviousTracks.last.source = QueueItemSource(type: QueueItemSourceType.formerNextUp, name: const QueueItemSourceName(type: QueueItemSourceNameType.tracksFormerNextUp), id: "former-next-up"); + if (_queuePreviousTracks.last.source.type == + QueueItemSourceType.nextUp) { + _queuePreviousTracks.last.source = QueueItemSource( + type: QueueItemSourceType.formerNextUp, + name: const QueueItemSourceName( + type: QueueItemSourceNameType.tracksFormerNextUp), + id: "former-next-up"); } _queuePreviousTracks.last.type = QueueItemQueueType.previousTracks; } else if (i == adjustedQueueIndex) { @@ -120,22 +160,23 @@ class QueueService { } else { if (allTracks[i].type == QueueItemQueueType.nextUp) { //TODO this *should* mark items from Next Up as formerNextUp when skipping backwards before Next Up is played, but it doesn't work for some reason - if ( - i == adjustedQueueIndex+1 || - i == adjustedQueueIndex+1 + _queueNextUp.length - ) { + if (i == adjustedQueueIndex + 1 || + i == adjustedQueueIndex + 1 + _queueNextUp.length) { _queueNextUp.add(allTracks[i]); } else { _queue.add(allTracks[i]); _queue.last.type = QueueItemQueueType.queue; - _queuePreviousTracks.last.source = QueueItemSource(type: QueueItemSourceType.formerNextUp, name: const QueueItemSourceName(type: QueueItemSourceNameType.tracksFormerNextUp), id: "former-next-up"); + _queuePreviousTracks.last.source = QueueItemSource( + type: QueueItemSourceType.formerNextUp, + name: const QueueItemSourceName( + type: QueueItemSourceNameType.tracksFormerNextUp), + id: "former-next-up"); } } else { _queue.add(allTracks[i]); _queue.last.type = QueueItemQueueType.queue; } } - } final newQueueInfo = getQueue(); @@ -143,13 +184,16 @@ class QueueService { if (_currentTrack != null) { _currentTrackStream.add(_currentTrack!); _audioHandler.mediaItem.add(_currentTrack!.item); - _audioHandler.queue.add(_queuePreviousTracks.followedBy([_currentTrack!]).followedBy(_queue).map((e) => e.item).toList()); + _audioHandler.queue.add(_queuePreviousTracks + .followedBy([_currentTrack!]) + .followedBy(_queue) + .map((e) => e.item) + .toList()); _currentTrackStream.add(_currentTrack); } _logQueues(message: "(current)"); - } Future startPlayback({ @@ -157,18 +201,17 @@ class QueueService { required QueueItemSource source, int startingIndex = 0, }) async { - // _initialQueue = list; // save original PlaybackList for looping/restarting and meta info - await _replaceWholeQueue(itemList: items, source: source, initialIndex: startingIndex); - _queueServiceLogger.info("Started playing '${source.name}' (${source.type})"); - + await _replaceWholeQueue( + itemList: items, source: source, initialIndex: startingIndex); + _queueServiceLogger + .info("Started playing '${source.name}' (${source.type})"); } /// Replaces the queue with the given list of items. If startAtIndex is specified, Any items below it /// will be ignored. This is used for when the user taps in the middle of an album to start from that point. Future _replaceWholeQueue({ - required List - itemList, + required List itemList, required QueueItemSource source, int initialIndex = 0, }) async { @@ -193,7 +236,9 @@ class QueueService { newItems.add(QueueItem( item: mediaItem, source: source, - type: i == 0 ? QueueItemQueueType.currentTrack : QueueItemQueueType.queue, + type: i == 0 + ? QueueItemQueueType.currentTrack + : QueueItemQueueType.queue, )); newLinearOrder.add(i); } catch (e) { @@ -211,8 +256,9 @@ class QueueService { } await _queueAudioSource.addAll(audioSources); - _shuffleOrder.shuffle(); // shuffle without providing an index to make sure shuffle doesn't always start at the first index - + _shuffleOrder + .shuffle(); // shuffle without providing an index to make sure shuffle doesn't always start at the first index + // set first item in queue _queueAudioSourceIndex = initialIndex; if (_playbackOrder == PlaybackOrder.shuffled) { @@ -239,14 +285,14 @@ class QueueService { await _audioHandler.play(); _audioHandler.nextInitialIndex = null; - } catch (e) { _queueServiceLogger.severe(e); return Future.error(e); } } - Future addToQueue(jellyfin_models.BaseItemDto item, QueueItemSource source) async { + Future addToQueue( + jellyfin_models.BaseItemDto item, QueueItemSource source) async { try { QueueItem queueItem = QueueItem( item: await _generateMediaItem(item), @@ -260,10 +306,10 @@ class QueueService { await _queueAudioSource.add(await _queueItemToAudioSource(queueItem)); - _queueServiceLogger.fine("Added '${queueItem.item.title}' to queue from '${source.name}' (${source.type})"); + _queueServiceLogger.fine( + "Added '${queueItem.item.title}' to queue from '${source.name}' (${source.type})"); _queueFromConcatenatingAudioSource(); // update internal queues - } catch (e) { _queueServiceLogger.severe(e); return Future.error(e); @@ -275,30 +321,35 @@ class QueueService { QueueItemSource? source, }) async { try { - List queueItems = []; for (final item in items) { queueItems.add(QueueItem( item: await _generateMediaItem(item), - source: source ?? QueueItemSource(id: "next-up", name: const QueueItemSourceName(type: QueueItemSourceNameType.nextUp), type: QueueItemSourceType.nextUp), + source: source ?? + QueueItemSource( + id: "next-up", + name: const QueueItemSourceName( + type: QueueItemSourceNameType.nextUp), + type: QueueItemSourceType.nextUp), type: QueueItemQueueType.nextUp, )); } - + // don't add to _order, because it wasn't added to the regular queue // int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; for (final queueItem in queueItems.reversed) { - await _queueAudioSource.insert(_queueAudioSourceIndex+1, await _queueItemToAudioSource(queueItem)); - _queueServiceLogger.fine("Appended '${queueItem.item.title}' to Next Up (index ${_queueAudioSourceIndex+1})"); + await _queueAudioSource.insert(_queueAudioSourceIndex + 1, + await _queueItemToAudioSource(queueItem)); + _queueServiceLogger.fine( + "Appended '${queueItem.item.title}' to Next Up (index ${_queueAudioSourceIndex + 1})"); } _queueFromConcatenatingAudioSource(); // update internal queues - } catch (e) { _queueServiceLogger.severe(e); return Future.error(e); - } + } } Future addToNextUp({ @@ -310,7 +361,12 @@ class QueueService { for (final item in items) { queueItems.add(QueueItem( item: await _generateMediaItem(item), - source: source ?? QueueItemSource(id: "next-up", name: const QueueItemSourceName(type: QueueItemSourceNameType.nextUp), type: QueueItemSourceType.nextUp), + source: source ?? + QueueItemSource( + id: "next-up", + name: const QueueItemSourceName( + type: QueueItemSourceNameType.nextUp), + type: QueueItemSourceType.nextUp), type: QueueItemQueueType.nextUp, )); } @@ -322,13 +378,14 @@ class QueueService { int offset = _queueNextUp.length; for (final queueItem in queueItems) { - await _queueAudioSource.insert(_queueAudioSourceIndex+1+offset, await _queueItemToAudioSource(queueItem)); - _queueServiceLogger.fine("Appended '${queueItem.item.title}' to Next Up (index ${_queueAudioSourceIndex+1+offset})"); + await _queueAudioSource.insert(_queueAudioSourceIndex + 1 + offset, + await _queueItemToAudioSource(queueItem)); + _queueServiceLogger.fine( + "Appended '${queueItem.item.title}' to Next Up (index ${_queueAudioSourceIndex + 1 + offset})"); offset++; } _queueFromConcatenatingAudioSource(); // update internal queues - } catch (e) { _queueServiceLogger.severe(e); return Future.error(e); @@ -336,49 +393,48 @@ class QueueService { } Future skipByOffset(int offset) async { - await _audioHandler.skipByOffset(offset); - - } + } Future removeAtOffset(int offset) async { - - final index = _playbackOrder == PlaybackOrder.shuffled ? _queueAudioSource.shuffleIndices[_queueAudioSource.shuffleIndices.indexOf((_queueAudioSourceIndex)) + offset] : (_queueAudioSourceIndex) + offset; + final index = _playbackOrder == PlaybackOrder.shuffled + ? _queueAudioSource.shuffleIndices[ + _queueAudioSource.shuffleIndices.indexOf((_queueAudioSourceIndex)) + + offset] + : (_queueAudioSourceIndex) + offset; await _audioHandler.removeQueueItemAt(index); _queueFromConcatenatingAudioSource(); - - } + } Future reorderByOffset(int oldOffset, int newOffset) async { - - _queueServiceLogger.fine("Reordering queue item at offset $oldOffset to offset $newOffset"); + _queueServiceLogger.fine( + "Reordering queue item at offset $oldOffset to offset $newOffset"); //!!! the player will automatically change the shuffle indices of the ConcatenatingAudioSource if shuffle is enabled, so we need to use the regular track index here final oldIndex = _queueAudioSourceIndex + oldOffset; - final newIndex = oldOffset < newOffset ? _queueAudioSourceIndex + newOffset - 1 : _queueAudioSourceIndex + newOffset; + final newIndex = oldOffset < newOffset + ? _queueAudioSourceIndex + newOffset - 1 + : _queueAudioSourceIndex + newOffset; await _audioHandler.reorderQueue(oldIndex, newIndex); _queueFromConcatenatingAudioSource(); - } Future clearNextUp() async { - // _queueFromConcatenatingAudioSource(); // update internal queues - + // remove all items from Next Up if (_queueNextUp.isNotEmpty) { - await _queueAudioSource.removeRange(_queueAudioSourceIndex+1, _queueAudioSourceIndex+1+_queueNextUp.length); + await _queueAudioSource.removeRange(_queueAudioSourceIndex + 1, + _queueAudioSourceIndex + 1 + _queueNextUp.length); _queueNextUp.clear(); } _queueFromConcatenatingAudioSource(); // update internal queues - } QueueInfo getQueue() { - return QueueInfo( previousTracks: _queuePreviousTracks, currentTrack: _currentTrack, @@ -389,7 +445,6 @@ class QueueService { // QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)), // ], ); - } BehaviorSubject getQueueStream() { @@ -404,7 +459,8 @@ class QueueService { List getNextXTracksInQueue(int amount) { List nextTracks = []; if (_queueNextUp.isNotEmpty) { - nextTracks.addAll(_queueNextUp.sublist(0, min(amount, _queueNextUp.length))); + nextTracks + .addAll(_queueNextUp.sublist(0, min(amount, _queueNextUp.length))); amount -= _queueNextUp.length; } if (_queue.isNotEmpty && amount > 0) { @@ -442,7 +498,6 @@ class QueueService { } else { _audioHandler.setRepeatMode(AudioServiceRepeatMode.none); } - } LoopMode get loopMode => _loopMode; @@ -456,11 +511,14 @@ class QueueService { // update queue accordingly and generate new shuffled order if necessary if (_playbackOrder == PlaybackOrder.shuffled) { - _audioHandler.setShuffleMode(AudioServiceShuffleMode.all).then((value) => _queueFromConcatenatingAudioSource()); + _audioHandler + .setShuffleMode(AudioServiceShuffleMode.all) + .then((value) => _queueFromConcatenatingAudioSource()); } else { - _audioHandler.setShuffleMode(AudioServiceShuffleMode.none).then((value) => _queueFromConcatenatingAudioSource()); + _audioHandler + .setShuffleMode(AudioServiceShuffleMode.none) + .then((value) => _queueFromConcatenatingAudioSource()); } - } PlaybackOrder get playbackOrder => _playbackOrder; @@ -485,8 +543,7 @@ class QueueService { Logger get queueServiceLogger => _queueServiceLogger; - void _logQueues({ String message = "" }) { - + void _logQueues({String message = ""}) { // generate string for `_queue` String queueString = ""; for (QueueItem queueItem in _queuePreviousTracks) { @@ -509,15 +566,12 @@ class QueueService { // queueAudioSourceString += "${queueItem.toString()}, "; // } - // log queues _queueServiceLogger.finer( - "Queue $message [${_queuePreviousTracks.length}-1-${_queueNextUp.length}-${_queue.length}]: $queueString" - ); + "Queue $message [${_queuePreviousTracks.length}-1-${_queueNextUp.length}-${_queue.length}]: $queueString"); // _queueServiceLogger.finer( // "Audio Source Queue $message [${_queue.length}]: $queueAudioSourceString" // ) - } Future _generateMediaItem(jellyfin_models.BaseItemDto item) async { @@ -570,7 +624,8 @@ class QueueService { if (queueItem.item.extras!["shouldTranscode"] == true) { return HlsAudioSource(await _songUri(queueItem.item), tag: queueItem); } else { - return AudioSource.uri(await _songUri(queueItem.item), tag: queueItem); + return AudioSource.uri(await _songUri(queueItem.item), + tag: queueItem); } } } else { @@ -585,7 +640,7 @@ class QueueService { return AudioSource.uri(downloadUri, tag: queueItem); } } - + Future _songUri(MediaItem mediaItem) async { // We need the platform to be Android or iOS to get device info assert(Platform.isAndroid || Platform.isIOS, @@ -641,17 +696,17 @@ class QueueService { queryParameters: queryParameters, ); } - } class NextUpShuffleOrder extends ShuffleOrder { - final Random _random; - final QueueService? _queueService; + final QueueService? _queueService; @override List indices = []; - NextUpShuffleOrder({Random? random, QueueService? queueService}) : _random = random ?? Random(), _queueService = queueService; + NextUpShuffleOrder({Random? random, QueueService? queueService}) + : _random = random ?? Random(), + _queueService = queueService; @override void shuffle({int? initialIndex}) { @@ -662,11 +717,16 @@ class NextUpShuffleOrder extends ShuffleOrder { indices.shuffle(_random); return; } - + indices.clear(); _queueService!._queueFromConcatenatingAudioSource(); QueueInfo queueInfo = _queueService!.getQueue(); - indices = List.generate(queueInfo.previousTracks.length + 1 + queueInfo.nextUp.length + queueInfo.queue.length, (i) => i); + indices = List.generate( + queueInfo.previousTracks.length + + 1 + + queueInfo.nextUp.length + + queueInfo.queue.length, + (i) => i); if (indices.length <= 1) return; indices.shuffle(_random); @@ -677,8 +737,10 @@ class NextUpShuffleOrder extends ShuffleOrder { for (int index in indices) { indicesString += "$index, "; } - _queueService!.queueServiceLogger.finest("Shuffled indices: $indicesString"); - _queueService!.queueServiceLogger.finest("Current Track: ${queueInfo.currentTrack}"); + _queueService!.queueServiceLogger + .finest("Shuffled indices: $indicesString"); + _queueService!.queueServiceLogger + .finest("Current Track: ${queueInfo.currentTrack}"); int nextUpLength = 0; if (_queueService != null) { @@ -691,7 +753,8 @@ class NextUpShuffleOrder extends ShuffleOrder { // remove current track and next up tracks from indices and save them in a separate list List currentTrackIndices = []; for (int i = 0; i < 1 + nextUpLength; i++) { - currentTrackIndices.add(indices.removeAt(indices.indexOf(initialIndex + i))); + currentTrackIndices + .add(indices.removeAt(indices.indexOf(initialIndex + i))); } // insert current track and next up tracks at the front indices.insertAll(initialPos, currentTrackIndices); @@ -701,14 +764,13 @@ class NextUpShuffleOrder extends ShuffleOrder { for (int index in indices) { indicesString += "$index, "; } - _queueService!.queueServiceLogger.finest("Shuffled indices (swapped): $indicesString"); - + _queueService!.queueServiceLogger + .finest("Shuffled indices (swapped): $indicesString"); } /// `index` is the linear index of the item in the ConcatenatingAudioSource @override void insert(int index, int count) { - int insertionPoint = index; int linearIndexOfPreviousItem = index - 1; @@ -728,11 +790,13 @@ class NextUpShuffleOrder extends ShuffleOrder { insertionPoint = indices.length; } else { // handle adding to Next Up - int shuffledIndexOfPreviousItem = indices.indexOf(linearIndexOfPreviousItem); + int shuffledIndexOfPreviousItem = + indices.indexOf(linearIndexOfPreviousItem); if (shuffledIndexOfPreviousItem != -1) { insertionPoint = shuffledIndexOfPreviousItem + 1; } - _queueService!.queueServiceLogger.finest("Inserting $count items at index $index (shuffled indices insertion point: $insertionPoint) (index of previous item: $shuffledIndexOfPreviousItem)"); + _queueService!.queueServiceLogger.finest( + "Inserting $count items at index $index (shuffled indices insertion point: $insertionPoint) (index of previous item: $shuffledIndexOfPreviousItem)"); } // Offset indices after insertion point. @@ -745,7 +809,6 @@ class NextUpShuffleOrder extends ShuffleOrder { // Insert new indices at the specified position. final newIndices = List.generate(count, (i) => index + i); indices.insertAll(insertionPoint, newIndices); - } @override @@ -766,5 +829,4 @@ class NextUpShuffleOrder extends ShuffleOrder { void clear() { indices.clear(); } - } From c65d04d469ff237d1c229ce1f6a9a1361675ac99 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Mon, 25 Sep 2023 19:07:50 +0200 Subject: [PATCH 079/130] small design tweaks --- lib/components/PlayerScreen/queue_list.dart | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 3cf8ee93a..e253b2749 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -259,6 +259,7 @@ Future showQueueBottomSheet(BuildContext context) { useSafeArea: true, enableDrag: true, isScrollControlled: true, + routeSettings: const RouteSettings(name: "/queue"), //TODO register this globally somehow? shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24.0)), ), @@ -693,7 +694,7 @@ class _CurrentTrackState extends State { IconTheme.of(context).color!.withOpacity(0.35), Colors.black), shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(8.0)), + borderRadius: BorderRadius.all(Radius.circular(12.0)), ), ), child: Row( @@ -706,10 +707,7 @@ class _CurrentTrackState extends State { children: [ AlbumImage( item: baseItem, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(8), - bottomLeft: Radius.circular(8), - ), + borderRadius: BorderRadius.zero, itemsToPrecache: _queueService.getNextXTracksInQueue(3).map((e) { final item = e.item.extras?["itemJson"] != null @@ -772,8 +770,8 @@ class _CurrentTrackState extends State { .withOpacity(0.75), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( - topRight: Radius.circular(8), - bottomRight: Radius.circular(8), + topRight: Radius.circular(12), + bottomRight: Radius.circular(12), ), ), ), From ce5eee2d425ac7395baf93904c23b9b2d9f70f5f Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 26 Sep 2023 18:42:13 +0200 Subject: [PATCH 080/130] started to improve playback reporting - more robust with less race conditions - can detect looping a single track --- .../music_player_background_task.dart | 145 +-------- lib/services/playback_history_service.dart | 277 ++++++++++++++---- lib/services/queue_service.dart | 63 ++-- 3 files changed, 277 insertions(+), 208 deletions(-) diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index 0ea294eec..d73e84b60 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -44,9 +44,6 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { /// new queue. int? nextInitialIndex; - /// The item that was previously played. Used for reporting playback status. - MediaItem? _previousItem; - /// Set to true when we're stopping the audio service. Used to avoid playback /// progress reporting. bool _isStopping = false; @@ -74,7 +71,8 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { playbackState.add(_transformEvent(event)); _playbackEventStreamController.add(event); - + + //TODO handle this is playback_history_service.dart // if (playbackState.valueOrNull != null && // playbackState.valueOrNull?.processingState != // AudioProcessingState.idle && @@ -93,41 +91,6 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } }); - _player.currentIndexStream.listen((event) async { - if (event == null) return; - - _audioServiceBackgroundTaskLogger.info("index event received, new index: $event"); - final currentItem = _getQueueItem(event); - mediaItem.add(currentItem); - - if (!FinampSettingsHelper.finampSettings.isOffline) { - final jellyfinApiHelper = GetIt.instance(); - - if (_previousItem != null) { - final playbackData = generatePlaybackProgressInfo( - item: _previousItem, - includeNowPlayingQueue: true, - ); - - if (playbackData != null) { - await jellyfinApiHelper.stopPlaybackProgress(playbackData); - } - } - - final playbackData = generatePlaybackProgressInfo( - item: currentItem, - includeNowPlayingQueue: true, - ); - - if (playbackData != null) { - await jellyfinApiHelper.reportPlaybackStart(playbackData); - } - - // Set item for next index update - _previousItem = currentItem; - } - }); - // PlaybackEvent doesn't include shuffle/loops so we listen for changes here _player.shuffleModeEnabledStream.listen( (_) => playbackState.add(_transformEvent(_player.playbackEvent))); @@ -205,23 +168,14 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { _isStopping = true; - // Clear the previous item. - _previousItem = null; - - // Tell Jellyfin we're no longer playing audio if we're online - if (!FinampSettingsHelper.finampSettings.isOffline) { - final playbackInfo = - generatePlaybackProgressInfo(includeNowPlayingQueue: false); - if (playbackInfo != null) { - await _jellyfinApiHelper.stopPlaybackProgress(playbackInfo); - } - } - // Stop playing audio. await _player.stop(); - // Seek to the start of the first item in the queue - await _player.seek(Duration.zero, index: 0); + mediaItem.add(null); + playbackState.add(playbackState.value.copyWith(processingState: AudioProcessingState.completed)); + + // // Seek to the start of the first item in the queue + // await _player.seek(Duration.zero, index: 0); _sleepTimerIsSet = false; _sleepTimerDuration = Duration.zero; @@ -233,11 +187,11 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { // await _player.dispose(); // await _eventSubscription?.cancel(); - // // It is important to wait for this state to be broadcast before we shut - // // down the task. If we don't, the background task will be destroyed before - // // the message gets sent to the UI. + // It is important to wait for this state to be broadcast before we shut + // down the task. If we don't, the background task will be destroyed before + // the message gets sent to the UI. // await _broadcastState(); - // // Shut down this background task + // Shut down this background task // await super.stop(); _isStopping = false; @@ -450,51 +404,6 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } } - /// Generates PlaybackProgressInfo from current player info. Returns null if - /// _queue is empty. If an item is not supplied, the current queue index will - /// be used. - jellyfin_models.PlaybackProgressInfo? generatePlaybackProgressInfo({ - MediaItem? item, - required bool includeNowPlayingQueue, - }) { - if (_queueAudioSource.length == 0 && item == null) { - // This function relies on _queue having items, so we return null if it's - // empty to avoid more errors. - return null; - } - - try { - return jellyfin_models.PlaybackProgressInfo( - itemId: item?.extras?["itemJson"]["Id"] ?? - _getQueueItem(_player.currentIndex ?? 0)?.extras?["itemJson"]?["Id"], - isPaused: !_player.playing, - isMuted: _player.volume == 0, - positionTicks: _player.position.inMicroseconds * 10, - repeatMode: _jellyfinRepeatMode(_player.loopMode), - playMethod: item?.extras!["shouldTranscode"] ?? - _getQueueItem(_player.currentIndex ?? 0) - ?.extras?["shouldTranscode"] - ? "Transcode" - : "DirectPlay", - // We don't send the queue since it seems useless and it can cause - // issues with large queues. - // https://github.com/jmshrv/finamp/issues/387 - - // nowPlayingQueue: includeNowPlayingQueue - // ? _queueFromSource() - // .map( - // (e) => QueueItem( - // id: e.extras!["itemJson"]["Id"], playlistItemId: e.id), - // ) - // .toList() - // : null, - ); - } catch (e) { - _audioServiceBackgroundTaskLogger.severe(e); - rethrow; - } - } - void setNextInitialIndex(int index) { nextInitialIndex = index; } @@ -564,23 +473,8 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { ); } - Future _updatePlaybackProgress() async { - try { - JellyfinApiHelper jellyfinApiHelper = GetIt.instance(); - - final playbackInfo = - generatePlaybackProgressInfo(includeNowPlayingQueue: false); - if (playbackInfo != null) { - await jellyfinApiHelper.updatePlaybackProgress(playbackInfo); - } - } catch (e) { - _audioServiceBackgroundTaskLogger.severe(e); - return Future.error(e); - } - } - - MediaItem? _getQueueItem(int index) { - return _queueAudioSource.sequence.isNotEmpty ? (_queueAudioSource.sequence[index].tag as QueueItem).item : null; + MediaItem _getQueueItem(int index) { + return _queueAudioSource.sequence[index].tag as MediaItem; } List _queueFromSource() { @@ -613,7 +507,7 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } } } else { - // We have to deserialise this because Dart is stupid and can't handle + // We have to deserialize this because Dart is stupid and can't handle // sending classes through isolates. final downloadedSong = DownloadedSong.fromJson(mediaItem.extras!["downloadedSongJson"]); @@ -687,17 +581,6 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } } -String _jellyfinRepeatMode(LoopMode loopMode) { - switch (loopMode) { - case LoopMode.all: - return "RepeatAll"; - case LoopMode.one: - return "RepeatOne"; - case LoopMode.off: - return "RepeatNone"; - } -} - AudioServiceRepeatMode _audioServiceRepeatMode(LoopMode loopMode) { switch (loopMode) { case LoopMode.off: diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart index 252ff36f4..20c67c92a 100644 --- a/lib/services/playback_history_service.dart +++ b/lib/services/playback_history_service.dart @@ -18,16 +18,19 @@ import '../models/jellyfin_models.dart' as jellyfin_models; /// A track queueing service for Finamp. class PlaybackHistoryService { final _jellyfinApiHelper = GetIt.instance(); - final _finampUserHelper = GetIt.instance(); final _audioService = GetIt.instance(); final _queueService = GetIt.instance(); final _playbackHistoryServiceLogger = Logger("PlaybackHistoryService"); // internal state - List _history = []; // contains **all** items that have been played, including "next up" + final List _history = []; // contains **all** items that have been played, including "next up" HistoryItem? _currentTrack; // the currently playing track + PlaybackState? _previousPlaybackState; + final bool _reportQueueToServer = true; + DateTime _lastPositionUpdate = DateTime.now(); + final _historyStream = BehaviorSubject>.seeded( List.empty(growable: true), ); @@ -36,16 +39,81 @@ class PlaybackHistoryService { _queueService.getCurrentTrackStream().listen((currentTrack) { updateCurrentTrack(currentTrack); + + if (currentTrack == null) { + _reportPlaybackStopped(); + } }); - _audioService.getPlaybackEventStream().listen((event) { - _updatePlaybackProgress(); - if (_audioService.paused) { - _playbackHistoryServiceLogger.info("Playback paused."); - } else { - _playbackHistoryServiceLogger.info("Playback resumed."); + _audioService.playbackState.listen((event) { + + final prevState = _previousPlaybackState; + final prevItem = _currentTrack?.item; + final currentState = event; + final currentIndex = currentState.queueIndex; + + //TODO check if this is a race condition + final currentItem = _queueService.getCurrentTrack(); + + if (currentIndex != null && currentItem != null) { + + // handle events that don't change the current track (e.g. loop, pause, seek, etc.) + + // differences in queue index or item id are considered track changes + if (currentItem.id != prevItem?.id || (_reportQueueToServer && currentIndex != prevState?.queueIndex)) { + _playbackHistoryServiceLogger.fine("Reporting track change event from ${prevItem?.item.title} to ${currentItem.item.title}"); + onTrackChanged(currentItem, currentState, prevItem, prevState); + } + // handle play/pause events + else if (currentState.playing != prevState?.playing) { + _playbackHistoryServiceLogger.fine("Reporting play/pause event for ${currentItem.item.title}"); + onPlaybackStateChanged(currentItem, currentState); + } + // handle seeking (changes updateTime (= last abnormal position change)) + else if (currentState.playing && currentState.updateTime != prevState?.updateTime && currentState.bufferedPosition == prevState?.bufferedPosition) { + + // detect looping a single track + if ( + // same track + prevItem?.id == currentItem.id && + // last position was close to the end of the track + (prevState?.position.inMilliseconds ?? 0) >= ((prevItem?.item.duration?.inMilliseconds ?? 0) - 1000 * 10) && + // current position is close to the beginning of the track + currentState.position.inMilliseconds <= 1000 * 10 + ) { + onTrackChanged(currentItem, currentState, prevItem, prevState); + return; + } + + // rate limit updates (only send update after no changes for 5 seconds) + Future.delayed(const Duration(seconds: 5, milliseconds: 500), () { + if (_lastPositionUpdate.add(const Duration(seconds: 5)).isBefore(DateTime.now())) { + _playbackHistoryServiceLogger.fine("Reporting seek event for ${currentItem.item.title}"); + onPlaybackStateChanged(currentItem, currentState); + } + _lastPositionUpdate = DateTime.now(); + }); + + } + // maybe handle toggling shuffle when sending the queue? would result in duplicate entries in the activity log, so maybe it's not desirable + // same for updating the queue / next up + + //TODO fix stop button not sending a playback state change event + } + + _previousPlaybackState = event; }); + + //TODO Tell Jellyfin we're not / no longer playing audio on startup + // if (!FinampSettingsHelper.finampSettings.isOffline) { + //FIXME why is an ID required? which ID should we use? an empty string doesn't work... + // final playbackInfo = generatePlaybackProgressInfoFromState(const MediaItem(id: "", title: ""), _audioService.playbackState.valueOrNull ?? PlaybackState()); + // if (playbackInfo != null) { + // _playbackHistoryServiceLogger.info("Stopping playback progress after startup"); + // _jellyfinApiHelper.stopPlaybackProgress(playbackInfo); + // } + // } } @@ -113,88 +181,173 @@ class PlaybackHistoryService { return groupedHistory; } - - - //TODO handle events that don't change the current track (e.g. pause, seek, etc.) - void updateCurrentTrack(QueueItem? currentTrack) { - bool playbackStarted = false; - bool playbackStopped = false; - - if (currentTrack == _currentTrack?.item || currentTrack?.item.id == "") { + if (currentTrack == null || currentTrack == _currentTrack?.item || currentTrack.item.id == "" || currentTrack.id == _currentTrack?.item.id) { // current track hasn't changed return; } - // update end time of previous track + // if there is a **previous** track if (_currentTrack != null) { + // update end time of previous track _currentTrack!.endTime = DateTime.now(); - } else { - playbackStarted = true; } - if (currentTrack != null) { - _currentTrack = HistoryItem( - item: currentTrack, - startTime: DateTime.now(), + // if there is a **current** track + _currentTrack = HistoryItem( + item: currentTrack, + startTime: DateTime.now(), + ); + _history.add(_currentTrack!); // current track is always the last item in the history + + _historyStream.add(_history); + + } + + //TODO separate starting a track and finishing a track and rely on the information provided by the queue service + /// Report track changes to the Jellyfin Server if the user is not offline. + Future onTrackChanged( + QueueItem currentItem, + PlaybackState currentState, + QueueItem? previousItem, + PlaybackState? previousState, + ) async { + if (FinampSettingsHelper.finampSettings.isOffline) { + return; + } + + if (previousItem != null && + previousState != null && + // don't submit stop events for idle tracks (at position 0 and not playing) + (previousState.playing || + previousState.updatePosition != Duration.zero)) { + final playbackData = generatePlaybackProgressInfoFromState( + previousItem.item, + previousState, ); - _history.add(_currentTrack!); // current track is always the last item in the history - } else { - playbackStopped = true; + + if (playbackData != null) { + _playbackHistoryServiceLogger.info("Stopping playback progress for ${previousItem.item.title}"); + await _jellyfinApiHelper.stopPlaybackProgress(playbackData); + } } - _historyStream.add(_history); + // prevent reporting the same track twice if playback hasn't started yet + if (!currentState.playing) { + return; + } - _updatePlaybackProgress( - playbackStarted: playbackStarted, - playbackStopped: playbackStopped, + final playbackData = generatePlaybackProgressInfoFromState( + currentItem.item, + currentState, ); + + if (playbackData != null) { + _playbackHistoryServiceLogger.info("Starting playback progress for ${currentItem.item.title}"); + await _jellyfinApiHelper.reportPlaybackStart(playbackData); + } } - Future _updatePlaybackProgress({ - bool playbackStarted = false, - bool playbackPaused = false, - bool playbackStopped = false, - }) async { - try { + /// Report track changes to the Jellyfin Server if the user is not offline. + Future onPlaybackStateChanged( + QueueItem currentItem, + PlaybackState currentState, + ) async { + if (FinampSettingsHelper.finampSettings.isOffline) { + return; + } - final playbackInfo = generatePlaybackProgressInfo(); - if (playbackInfo != null) { - if (playbackStarted) { - await _reportPlaybackStarted(); - } else if (playbackStopped) { - await _reportPlaybackStopped(); - } else { - await _jellyfinApiHelper.updatePlaybackProgress(playbackInfo); - } - } - } catch (e) { - _playbackHistoryServiceLogger.severe(e); - return Future.error(e); + final playbackData = generatePlaybackProgressInfoFromState( + currentItem.item, + currentState, + ); + + if (playbackData != null) { + _playbackHistoryServiceLogger.info("Starting playback progress for ${currentItem.item.title}"); + await _jellyfinApiHelper.reportPlaybackStart(playbackData); } } + /// Generates PlaybackProgressInfo for the supplied item and playback state. + jellyfin_models.PlaybackProgressInfo? generatePlaybackProgressInfoFromState( + MediaItem item, + PlaybackState state, + ) { + final duration = item.duration; + return generatePlaybackProgressInfo( + item, + isPaused: !state.playing, + // always consider as unmuted + isMuted: false, + // ensure the (extrapolated) position doesn't exceed the duration + playerPosition: duration != null && state.position > duration + ? duration + : state.position, + repeatMode: _jellyfinRepeatModeFromRepeatMode(state.repeatMode), + includeNowPlayingQueue: _reportQueueToServer, + ); + } + Future _reportPlaybackStopped() async { - final playbackInfo = generatePlaybackProgressInfo(); + final playbackInfo = generateGenericPlaybackProgressInfo(); if (playbackInfo != null) { await _jellyfinApiHelper.stopPlaybackProgress(playbackInfo); } } - Future _reportPlaybackStarted() async { + // Future _reportPlaybackStarted() async { - final playbackInfo = generatePlaybackProgressInfo(); - if (playbackInfo != null) { - await _jellyfinApiHelper.reportPlaybackStart(playbackInfo); - } + // final playbackInfo = generatePlaybackProgressInfo(); + // if (playbackInfo != null) { + // await _jellyfinApiHelper.reportPlaybackStart(playbackInfo); + // } + // } + + /// Generates PlaybackProgressInfo for the supplied item and player info. + jellyfin_models.PlaybackProgressInfo? generatePlaybackProgressInfo( + MediaItem item, { + required bool isPaused, + required bool isMuted, + required Duration playerPosition, + required String repeatMode, + required bool includeNowPlayingQueue, + }) { + try { + + List? nowPlayingQueue; + if (includeNowPlayingQueue) { + nowPlayingQueue = _queueService.getNextXTracksInQueue(30) + .map((e) => jellyfin_models.QueueItem( + id: e.item.id, + playlistItemId: e.source.id, + )) + .toList(); + } + + return jellyfin_models.PlaybackProgressInfo( + itemId: item.extras?["itemJson"]["Id"] ?? "", + isPaused: isPaused, + isMuted: isMuted, + positionTicks: playerPosition.inMicroseconds * 10, + repeatMode: repeatMode, + playMethod: item.extras?["shouldTranscode"] ?? false + ? "Transcode" + : "DirectPlay", + nowPlayingQueue: nowPlayingQueue, + ); + } catch (e) { + _playbackHistoryServiceLogger.severe(e); + return null; + // rethrow; + } } /// Generates PlaybackProgressInfo from current player info. - jellyfin_models.PlaybackProgressInfo? generatePlaybackProgressInfo({ + jellyfin_models.PlaybackProgressInfo? generateGenericPlaybackProgressInfo({ bool includeNowPlayingQueue = false, }) { if (_history.isEmpty || _currentTrack == null) { @@ -243,6 +396,18 @@ class PlaybackHistoryService { } } + String _jellyfinRepeatModeFromRepeatMode(AudioServiceRepeatMode repeatMode) { + switch (repeatMode) { + case AudioServiceRepeatMode.none: + return "RepeatNone"; + case AudioServiceRepeatMode.one: + return "RepeatOne"; + case AudioServiceRepeatMode.all: + case AudioServiceRepeatMode.group: + return "RepeatAll"; + } + } + String _toJellyfinRepeatMode(LoopMode loopMode) { switch (loopMode) { case LoopMode.all: diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 1045a4c43..a4cb90a03 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -32,7 +32,6 @@ class QueueService { final _audioHandler = GetIt.instance(); final _finampUserHelper = GetIt.instance(); final _queueServiceLogger = Logger("QueueService"); - // internal state List _queuePreviousTracks = @@ -101,18 +100,22 @@ class QueueService { int _queueAudioSourceIndex = 0; QueueService() { + + // _queueServiceLogger.level = Level.OFF; + _shuffleOrder = NextUpShuffleOrder(queueService: this); _queueAudioSource = ConcatenatingAudioSource( children: [], shuffleOrder: _shuffleOrder, ); - _audioHandler.getPlaybackEventStream().listen((event) async { + _audioHandler.playbackState.listen((event) async { + // int indexDifference = (event.currentIndex ?? 0) - _queueAudioSourceIndex; // _queueServiceLogger.finer("Play queue index changed, difference: $indexDifference"); - _queueAudioSourceIndex = event.currentIndex ?? 0; + _queueAudioSourceIndex = event.queueIndex ?? 0; _queueServiceLogger.finer( "Play queue index changed, new index: $_queueAudioSourceIndex"); @@ -179,19 +182,21 @@ class QueueService { } } + if (allTracks.isEmpty) { + _queueServiceLogger.fine("Queue is empty"); + _currentTrack = null; + return; + } + final newQueueInfo = getQueue(); _queueStream.add(newQueueInfo); - if (_currentTrack != null) { - _currentTrackStream.add(_currentTrack!); - _audioHandler.mediaItem.add(_currentTrack!.item); - _audioHandler.queue.add(_queuePreviousTracks - .followedBy([_currentTrack!]) - .followedBy(_queue) - .map((e) => e.item) - .toList()); - - _currentTrackStream.add(_currentTrack); - } + _currentTrackStream.add(_currentTrack); + _audioHandler.mediaItem.add(_currentTrack?.item); + _audioHandler.queue.add(_queuePreviousTracks + .followedBy([_currentTrack!]) + .followedBy(_queue) + .map((e) => e.item) + .toList()); _logQueues(message: "(current)"); } @@ -202,6 +207,9 @@ class QueueService { int startingIndex = 0, }) async { // _initialQueue = list; // save original PlaybackList for looping/restarting and meta info + if (playbackOrder == PlaybackOrder.shuffled) { + items.shuffle(); + } await _replaceWholeQueue( itemList: items, source: source, initialIndex: startingIndex); _queueServiceLogger @@ -256,8 +264,8 @@ class QueueService { } await _queueAudioSource.addAll(audioSources); - _shuffleOrder - .shuffle(); // shuffle without providing an index to make sure shuffle doesn't always start at the first index + // _shuffleOrder + // .shuffle(); // shuffle without providing an index to make sure shuffle doesn't always start at the first index // set first item in queue _queueAudioSourceIndex = initialIndex; @@ -278,9 +286,8 @@ class QueueService { _queueServiceLogger.fine("Order items length: ${_order.items.length}"); - _audioHandler.queue.add(_queue.map((e) => e.item).toList()); - // _queueStream.add(getQueue()); + _queueFromConcatenatingAudioSource(); await _audioHandler.play(); @@ -291,6 +298,19 @@ class QueueService { } } + Future stopPlayback() async { + + queueServiceLogger.info("Stopping playback"); + + await _audioHandler.stop(); + + _queueAudioSource.clear(); + + _queueFromConcatenatingAudioSource(); + + return; + } + Future addToQueue( jellyfin_models.BaseItemDto item, QueueItemSource source) async { try { @@ -455,7 +475,8 @@ class QueueService { _queueStream.add(getQueue()); } - /// returns the next [amount] QueueItems from Next Up and the regular queue + /// Returns the next [amount] QueueItems from Next Up and the regular queue. + /// The length of the returned list may be less than [amount] if there are not enough items in the queue List getNextXTracksInQueue(int amount) { List nextTracks = []; if (_queueNextUp.isNotEmpty) { @@ -481,8 +502,8 @@ class QueueService { return _currentTrackStream; } - QueueItem getCurrentTrack() { - return _currentTrack!; + QueueItem? getCurrentTrack() { + return _currentTrack; } set loopMode(LoopMode mode) { From 8378c895436da8ba086b84bda30712a9c5f9575d Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 28 Sep 2023 15:43:06 +0200 Subject: [PATCH 081/130] fix rewind instead of skip back position < 5s --- lib/services/music_player_background_task.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index d73e84b60..f88a96942 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -288,6 +288,8 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { if (_queueCallbackPreviousTrack != null) { doSkip = await _queueCallbackPreviousTrack!(); + } else { + doSkip = _player.position.inSeconds < 5; } if (!_player.hasPrevious) { @@ -295,6 +297,8 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } else { if (doSkip) { await _player.seek(Duration.zero, index: _player.previousIndex); + } else { + await _player.seek(Duration.zero); } } } catch (e) { From 78c61a359335aeb42c0401420eca05de30fc6da3 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 28 Sep 2023 16:43:04 +0200 Subject: [PATCH 082/130] design/layout fixes --- lib/components/PlayerScreen/artist_chip.dart | 2 +- lib/components/PlayerScreen/queue_list.dart | 133 ++++++++++-------- .../PlayerScreen/queue_list_item.dart | 2 +- 3 files changed, 75 insertions(+), 62 deletions(-) diff --git a/lib/components/PlayerScreen/artist_chip.dart b/lib/components/PlayerScreen/artist_chip.dart index 9a729b378..44f9497dd 100644 --- a/lib/components/PlayerScreen/artist_chip.dart +++ b/lib/components/PlayerScreen/artist_chip.dart @@ -41,7 +41,7 @@ class _ArtistChipState extends State { void initState() { super.initState(); - if (widget.item != null) { + if (widget.item != null && widget.item!.albumArtists?.isNotEmpty == true) { final albumArtistId = widget.item!.albumArtists?.first.id; if (albumArtistId != null) { diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index e253b2749..c73ad0fa3 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -122,7 +122,7 @@ class _QueueListState extends State { ), SliverPersistentHeader( delegate: QueueSectionHeader( - title: const Text("Queue"), + title: const Flexible(child: Text("Queue", overflow: TextOverflow.ellipsis)), nextUpHeaderKey: widget.nextUpHeaderKey, )), // Queue @@ -224,10 +224,14 @@ class _QueueListState extends State { title: Row( children: [ Text("${AppLocalizations.of(context)!.playingFrom} "), - Text( - _source?.name.getLocalized(context) ?? - AppLocalizations.of(context)!.unknownName, - style: const TextStyle(fontWeight: FontWeight.w500)), + Flexible( + child: Text( + _source?.name.getLocalized(context) ?? + AppLocalizations.of(context)!.unknownName, + style: const TextStyle(fontWeight: FontWeight.w500), + overflow: TextOverflow.ellipsis, + ), + ), ], ), // _source != null ? "Playing from ${_source?.name}" : "Queue", @@ -686,7 +690,7 @@ class _CurrentTrackState extends State { flexibleSpace: Container( // width: 58, height: 70.0, - padding: const EdgeInsets.symmetric(horizontal: 12), + padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Container( clipBehavior: Clip.antiAlias, decoration: ShapeDecoration( @@ -1187,76 +1191,85 @@ class QueueSectionHeader extends SliverPersistentHeaderDelegate { @override Widget build(context, double shrinkOffset, bool overlapsContent) { - final _queueService = GetIt.instance(); + final queueService = GetIt.instance(); return StreamBuilder( stream: Rx.combineLatest2( - _queueService.getPlaybackOrderStream(), - _queueService.getLoopModeStream(), + queueService.getPlaybackOrderStream(), + queueService.getLoopModeStream(), (a, b) => PlaybackBehaviorInfo(a, b)), builder: (context, snapshot) { PlaybackBehaviorInfo? info = snapshot.data; - return Container( - // color: Colors.black.withOpacity(0.5), + return Padding( padding: const EdgeInsets.symmetric(horizontal: 14.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - child: Flex( - direction: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - title, - ])), + child: title + ), if (controls) - IconButton( - padding: const EdgeInsets.only(bottom: 2.0), - iconSize: 28.0, - icon: info?.order == PlaybackOrder.shuffled - ? (const Icon( - TablerIcons.arrows_shuffle, - )) - : (const Icon( - TablerIcons.arrows_right, - )), - color: info?.order == PlaybackOrder.shuffled - ? IconTheme.of(context).color! - : Colors.white, - onPressed: () { - _queueService.togglePlaybackOrder(); - Vibrate.feedback(FeedbackType.success); - //TODO why is the current track scrolled out of view **after** the queue is updated? - Future.delayed( - const Duration(milliseconds: 300), - () => scrollToKey( - key: nextUpHeaderKey, - duration: const Duration(milliseconds: 500))); - // scrollToKey(key: nextUpHeaderKey, duration: const Duration(milliseconds: 1000)); + Row( + children: [ + IconButton( + padding: const EdgeInsets.only(bottom: 2.0), + iconSize: 28.0, + icon: info?.order == PlaybackOrder.shuffled + ? (const Icon( + TablerIcons.arrows_shuffle, + )) + : (const Icon( + TablerIcons.arrows_right, + )), + color: info?.order == PlaybackOrder.shuffled + ? IconTheme.of(context).color! + : Colors.white, + onPressed: () { + queueService.togglePlaybackOrder(); + Vibrate.feedback(FeedbackType.success); + //TODO why is the current track scrolled out of view **after** the queue is updated? + Future.delayed( + const Duration(milliseconds: 300), + () => scrollToKey( + key: nextUpHeaderKey, + duration: const Duration(milliseconds: 500))); + // scrollToKey(key: nextUpHeaderKey, duration: const Duration(milliseconds: 1000)); }), - if (controls) - IconButton( - padding: const EdgeInsets.only(bottom: 2.0), - iconSize: 28.0, - icon: info?.loop != LoopMode.none - ? (info?.loop == LoopMode.one - ? (const Icon( - TablerIcons.repeat_once, - )) + IconButton( + padding: const EdgeInsets.only(bottom: 2.0), + iconSize: 28.0, + icon: info?.loop != LoopMode.none + ? (info?.loop == LoopMode.one + ? (const Icon( + TablerIcons.repeat_once, + )) + : (const Icon( + TablerIcons.repeat, + ))) : (const Icon( - TablerIcons.repeat, - ))) - : (const Icon( - TablerIcons.repeat_off, - )), - color: info?.loop != LoopMode.none - ? IconTheme.of(context).color! - : Colors.white, - onPressed: () { - _queueService.toggleLoopMode(); - Vibrate.feedback(FeedbackType.success); + TablerIcons.repeat_off, + )), + color: info?.loop != LoopMode.none + ? IconTheme.of(context).color! + : Colors.white, + onPressed: () { + queueService.toggleLoopMode(); + Vibrate.feedback(FeedbackType.success); }), + ], + ) + // Expanded( + // child: Flex( + // direction: Axis.horizontal, + // crossAxisAlignment: CrossAxisAlignment.center, + // clipBehavior: Clip.hardEdge, + // children: [ + // , + // ])), + + // ) ], ), ); diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index f855b9101..2e60cdc28 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -72,7 +72,7 @@ class _QueueListItemState extends State color: const Color.fromRGBO(255, 255, 255, 0.075), elevation: 0, margin: - const EdgeInsets.symmetric(horizontal: 12.0, vertical: 5.0), + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 5.0), clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.0), From dd7012d03a5c453747e8d3884411b86660e3e188 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 28 Sep 2023 17:02:35 +0200 Subject: [PATCH 083/130] fix playback reporting when manually skipping --- lib/services/playback_history_service.dart | 32 +++++++++++++--------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart index 20c67c92a..bf63958b5 100644 --- a/lib/services/playback_history_service.dart +++ b/lib/services/playback_history_service.dart @@ -62,7 +62,7 @@ class PlaybackHistoryService { // differences in queue index or item id are considered track changes if (currentItem.id != prevItem?.id || (_reportQueueToServer && currentIndex != prevState?.queueIndex)) { _playbackHistoryServiceLogger.fine("Reporting track change event from ${prevItem?.item.title} to ${currentItem.item.title}"); - onTrackChanged(currentItem, currentState, prevItem, prevState); + onTrackChanged(currentItem, currentState, prevItem, prevState, currentIndex > (prevState?.queueIndex ?? 0)); } // handle play/pause events else if (currentState.playing != prevState?.playing) { @@ -81,13 +81,16 @@ class PlaybackHistoryService { // current position is close to the beginning of the track currentState.position.inMilliseconds <= 1000 * 10 ) { - onTrackChanged(currentItem, currentState, prevItem, prevState); + onTrackChanged(currentItem, currentState, prevItem, prevState, true); return; } - // rate limit updates (only send update after no changes for 5 seconds) - Future.delayed(const Duration(seconds: 5, milliseconds: 500), () { - if (_lastPositionUpdate.add(const Duration(seconds: 5)).isBefore(DateTime.now())) { + // rate limit updates (only send update after no changes for 3 seconds) and if the track is still the same + Future.delayed(const Duration(seconds: 3, milliseconds: 500), () { + if ( + _lastPositionUpdate.add(const Duration(seconds: 3)).isBefore(DateTime.now()) && + currentItem.id == _queueService.getCurrentTrack()?.id + ) { _playbackHistoryServiceLogger.fine("Reporting seek event for ${currentItem.item.title}"); onPlaybackStateChanged(currentItem, currentState); } @@ -212,25 +215,23 @@ class PlaybackHistoryService { PlaybackState currentState, QueueItem? previousItem, PlaybackState? previousState, + bool skippingForward, ) async { if (FinampSettingsHelper.finampSettings.isOffline) { return; } + jellyfin_models.PlaybackProgressInfo? previousTrackPlaybackData; if (previousItem != null && previousState != null && // don't submit stop events for idle tracks (at position 0 and not playing) (previousState.playing || previousState.updatePosition != Duration.zero)) { - final playbackData = generatePlaybackProgressInfoFromState( + previousTrackPlaybackData = generatePlaybackProgressInfoFromState( previousItem.item, previousState, ); - if (playbackData != null) { - _playbackHistoryServiceLogger.info("Stopping playback progress for ${previousItem.item.title}"); - await _jellyfinApiHelper.stopPlaybackProgress(playbackData); - } } // prevent reporting the same track twice if playback hasn't started yet @@ -238,14 +239,19 @@ class PlaybackHistoryService { return; } - final playbackData = generatePlaybackProgressInfoFromState( + final newTrackplaybackData = generatePlaybackProgressInfoFromState( currentItem.item, currentState, ); - if (playbackData != null) { + //!!! always submit a "start" **AFTER** a "stop" to that Jellyfin knows there's still something playing + if (previousTrackPlaybackData != null) { + _playbackHistoryServiceLogger.info("Stopping playback progress for ${previousItem?.item.title}"); + await _jellyfinApiHelper.stopPlaybackProgress(previousTrackPlaybackData); + } + if (newTrackplaybackData != null) { _playbackHistoryServiceLogger.info("Starting playback progress for ${currentItem.item.title}"); - await _jellyfinApiHelper.reportPlaybackStart(playbackData); + await _jellyfinApiHelper.reportPlaybackStart(newTrackplaybackData); } } From 36df8358073eee38dbb5c0d9b84078822fe641ef Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 28 Sep 2023 17:04:28 +0200 Subject: [PATCH 084/130] update dependencies --- pubspec.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 2b50a1a33..befd74c90 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,9 +29,9 @@ dependencies: json_annotation: ^4.8.0 chopper: ^6.1.1 get_it: ^7.2.0 - just_audio: ^0.9.32 - audio_service: ^0.18.9 - audio_session: ^0.1.13 + just_audio: ^0.9.35 + audio_service: ^0.18.12 + audio_session: ^0.1.16 rxdart: ^0.27.7 simple_gesture_detector: ^0.2.0 flutter_downloader: From 860d8b12b4bfcb1f3314cf728f5f5daf02a664f2 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 28 Sep 2023 17:06:44 +0200 Subject: [PATCH 085/130] fix (temporary) button layout on album screen --- ...lbum_screen_content_flexible_space_bar.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart index 12073866e..c5e986439 100644 --- a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart +++ b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart @@ -194,15 +194,6 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { ), ), const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), - Expanded( - child: ElevatedButton.icon( - onPressed: () => addAlbumToNextUp(), - icon: const Icon(Icons.hourglass_top), - label: Text(AppLocalizations.of(context)!.addToNextUp), - ), - ), - ]), - Row(children: [ Expanded( child: ElevatedButton.icon( onPressed: () => shuffleAlbumNext(), @@ -211,6 +202,15 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { Text(AppLocalizations.of(context)!.shuffleNext), ), ), + ]), + Row(children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () => addAlbumToNextUp(), + icon: const Icon(Icons.hourglass_top), + label: Text(AppLocalizations.of(context)!.addToNextUp), + ), + ), const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), Expanded( child: ElevatedButton.icon( From 6421ecb5cef9b83a3075029ff1abb03a00aac4aa Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 28 Sep 2023 17:27:30 +0200 Subject: [PATCH 086/130] replace non-functional stop button with like button --- lib/components/now_playing_bar.dart | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index 6f344804d..b41eb1065 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -1,4 +1,5 @@ import 'package:audio_service/audio_service.dart'; +import 'package:finamp/components/favourite_button.dart'; import 'package:finamp/services/player_screen_theme_provider.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; @@ -134,15 +135,12 @@ class NowPlayingBar extends ConsumerWidget { trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - if (snapshot - .data!.playbackState.processingState != - AudioProcessingState.idle) - IconButton( - // We have a key here because otherwise the - // InkWell moves over to the play/pause button - key: const ValueKey("StopButton"), - icon: const Icon(Icons.stop), - onPressed: () => audioHandler.stop(), + FavoriteButton( + item: item, + onToggle: (isFavorite) { + item.userData!.isFavorite = isFavorite; + snapshot.data!.mediaItem?.extras!["itemJson"] = item.toJson(); + }, ), playing ? IconButton( From 635f949729cef5902a35c1275a7d9f7c58ef5c5a Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 28 Sep 2023 18:40:21 +0200 Subject: [PATCH 087/130] improve playback history page --- .../playback_history_list.dart | 16 ++- lib/main.dart | 6 ++ lib/screens/playback_history_screen.dart | 4 +- lib/services/playback_history_service.dart | 97 +++++++++++-------- 4 files changed, 78 insertions(+), 45 deletions(-) diff --git a/lib/components/PlaybackHistoryScreen/playback_history_list.dart b/lib/components/PlaybackHistoryScreen/playback_history_list.dart index f3286c92a..409f1e2c6 100644 --- a/lib/components/PlaybackHistoryScreen/playback_history_list.dart +++ b/lib/components/PlaybackHistoryScreen/playback_history_list.dart @@ -1,8 +1,11 @@ import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/services/audio_service_helper.dart'; +import 'package:finamp/services/locale_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:intl/date_symbol_data_local.dart'; import 'package:get_it/get_it.dart'; +import 'package:intl/intl.dart'; import '../../services/playback_history_service.dart'; import '../../models/jellyfin_models.dart' as jellyfin_models; @@ -25,7 +28,8 @@ class PlaybackHistoryList extends StatelessWidget { if (snapshot.hasData) { history = snapshot.data; // groupedHistory = playbackHistoryService.getHistoryGroupedByDate(); - groupedHistory = playbackHistoryService.getHistoryGroupedByHour(); + // groupedHistory = playbackHistoryService.getHistoryGroupedByHour(); + groupedHistory = playbackHistoryService.getHistoryGroupedByDateOrHourDynamic(); print(groupedHistory); @@ -69,6 +73,11 @@ class PlaybackHistoryList extends StatelessWidget { ), ); + final now = DateTime.now(); + final String localeString = (LocaleHelper.locale != null) ? ((LocaleHelper.locale?.countryCode != null) ? + "${LocaleHelper.locale?.languageCode.toLowerCase()}_${LocaleHelper.locale?.countryCode?.toUpperCase()}" : LocaleHelper.locale.toString()) : + "en_US"; + return index == 0 ? Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -77,7 +86,10 @@ class PlaybackHistoryList extends StatelessWidget { padding: const EdgeInsets.only( left: 16.0, top: 8.0, bottom: 4.0), child: Text( - "${group.key.hour % 12} ${group.key.hour >= 12 ? "pm" : "am"}", + (group.key.year == now.year && group.key.month == now.month && group.key.day == now.day ) ? + DateFormat.j(localeString).format(group.key) : + DateFormat.MMMMd(localeString).format(group.key) + , style: const TextStyle( fontSize: 16.0, ), diff --git a/lib/main.dart b/lib/main.dart index fabb745de..4f8d9ed49 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,6 +14,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/date_symbol_data_local.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; import 'package:hive_flutter/hive_flutter.dart'; @@ -86,6 +87,11 @@ void main() async { SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle(statusBarBrightness: Brightness.dark)); + final String localeString = (LocaleHelper.locale != null) ? ((LocaleHelper.locale?.countryCode != null) ? + "${LocaleHelper.locale?.languageCode.toLowerCase()}_${LocaleHelper.locale?.countryCode?.toUpperCase()}" : LocaleHelper.locale.toString()) : + "en_US"; + initializeDateFormatting(localeString, null); + runApp(const Finamp()); } } diff --git a/lib/screens/playback_history_screen.dart b/lib/screens/playback_history_screen.dart index 3c9d9b7af..e5a5d896c 100644 --- a/lib/screens/playback_history_screen.dart +++ b/lib/screens/playback_history_screen.dart @@ -16,10 +16,12 @@ class PlaybackHistoryScreen extends StatelessWidget { appBar: AppBar( centerTitle: true, elevation: 0.0, + leadingWidth: 48 + 24, + toolbarHeight: 75.0, backgroundColor: Colors.transparent, title: Text(AppLocalizations.of(context)!.playbackHistory), leading: FinampAppBarButton( - onPressed: () => Navigator.pop(context), + onPressed: () => Navigator.of(context).pop(), ), ), body: const Padding( diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart index bf63958b5..effbda046 100644 --- a/lib/services/playback_history_service.dart +++ b/lib/services/playback_history_service.dart @@ -52,18 +52,17 @@ class PlaybackHistoryService { final currentState = event; final currentIndex = currentState.queueIndex; - //TODO check if this is a race condition final currentItem = _queueService.getCurrentTrack(); if (currentIndex != null && currentItem != null) { - // handle events that don't change the current track (e.g. loop, pause, seek, etc.) - // differences in queue index or item id are considered track changes if (currentItem.id != prevItem?.id || (_reportQueueToServer && currentIndex != prevState?.queueIndex)) { _playbackHistoryServiceLogger.fine("Reporting track change event from ${prevItem?.item.title} to ${currentItem.item.title}"); onTrackChanged(currentItem, currentState, prevItem, prevState, currentIndex > (prevState?.queueIndex ?? 0)); } + // handle events that don't change the current track (e.g. loop, pause, seek, etc.) + // handle play/pause events else if (currentState.playing != prevState?.playing) { _playbackHistoryServiceLogger.fine("Reporting play/pause event for ${currentItem.item.title}"); @@ -124,55 +123,74 @@ class PlaybackHistoryService { BehaviorSubject> get historyStream => _historyStream; /// method that converts history into a list grouped by date - List>> getHistoryGroupedByDate() { - final groupedHistory = >>[]; - - final groupedHistoryMap = >{}; - - _history.forEach((element) { - final date = DateTime( + List>> getHistoryGroupedByDateOrHourDynamic() { + byDateGroupingConstructor(HistoryItem element) { + final now = DateTime.now(); + if (now.year == element.startTime.year && now.month == element.startTime.month && now.day == element.startTime.day) { + // group by hour + return DateTime( + element.startTime.year, + element.startTime.month, + element.startTime.day, + element.startTime.hour, + ); + } + // group by date + return DateTime( element.startTime.year, element.startTime.month, element.startTime.day, ); - if (groupedHistoryMap.containsKey(date)) { - groupedHistoryMap[date]!.add(element); - } else { - groupedHistoryMap[date] = [element]; - } - }); + } - groupedHistoryMap.forEach((key, value) { - groupedHistory.add(MapEntry(key, value)); - }); + return getHistoryGrouped(byDateGroupingConstructor); + } - // sort by date (most recent first) - groupedHistory.sort((a, b) => b.key.compareTo(a.key)); + /// method that converts history into a list grouped by date + List>> getHistoryGroupedByDate() { + byDateGroupingConstructor(HistoryItem element) { + return DateTime( + element.startTime.year, + element.startTime.month, + element.startTime.day, + ); - return groupedHistory; + } + + return getHistoryGrouped(byDateGroupingConstructor); } - /// method that converts history into a list grouped by minute + /// method that converts history into a list grouped by hour List>> getHistoryGroupedByHour() { - final groupedHistory = >>[]; - - final groupedHistoryMap = >{}; - - _history.forEach((element) { - final date = DateTime( + byHourGroupingConstructor(HistoryItem element) { + return DateTime( element.startTime.year, element.startTime.month, element.startTime.day, element.startTime.hour, ); + } + + return getHistoryGrouped(byHourGroupingConstructor); + } + + /// method that converts history into a list grouped by a custom date constructor controlling the granularity of the grouping + List>> getHistoryGrouped(DateTime Function (HistoryItem) dateTimeConstructor) { + final groupedHistory = >>[]; + + final groupedHistoryMap = >{}; + + for (var element in _history) { + final date = dateTimeConstructor(element); + if (groupedHistoryMap.containsKey(date)) { groupedHistoryMap[date]!.add(element); } else { groupedHistoryMap[date] = [element]; } - }); + } groupedHistoryMap.forEach((key, value) { groupedHistory.add(MapEntry(key, value)); @@ -208,7 +226,6 @@ class PlaybackHistoryService { } - //TODO separate starting a track and finishing a track and rely on the information provided by the queue service /// Report track changes to the Jellyfin Server if the user is not offline. Future onTrackChanged( QueueItem currentItem, @@ -270,8 +287,13 @@ class PlaybackHistoryService { ); if (playbackData != null) { - _playbackHistoryServiceLogger.info("Starting playback progress for ${currentItem.item.title}"); - await _jellyfinApiHelper.reportPlaybackStart(playbackData); + if (![AudioProcessingState.completed, AudioProcessingState.idle].contains(currentState.processingState)) { + _playbackHistoryServiceLogger.info("Starting playback progress for ${currentItem.item.title}"); + await _jellyfinApiHelper.reportPlaybackStart(playbackData); + } else { + _playbackHistoryServiceLogger.info("Stopping playback progress for ${currentItem.item.title}"); + await _jellyfinApiHelper.stopPlaybackProgress(playbackData); + } } } @@ -304,15 +326,6 @@ class PlaybackHistoryService { } - // Future _reportPlaybackStarted() async { - - // final playbackInfo = generatePlaybackProgressInfo(); - // if (playbackInfo != null) { - // await _jellyfinApiHelper.reportPlaybackStart(playbackInfo); - // } - - // } - /// Generates PlaybackProgressInfo for the supplied item and player info. jellyfin_models.PlaybackProgressInfo? generatePlaybackProgressInfo( MediaItem item, { From 5e85c258ec770c4f8225a0e53d85f7de8df5c51c Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 28 Sep 2023 21:06:19 +0200 Subject: [PATCH 088/130] re-enable chopper logs --- lib/services/jellyfin_api.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/services/jellyfin_api.dart b/lib/services/jellyfin_api.dart index 09a67cc70..07c028cb1 100644 --- a/lib/services/jellyfin_api.dart +++ b/lib/services/jellyfin_api.dart @@ -311,7 +311,7 @@ abstract class JellyfinApi extends ChopperService { /// "RefreshState" "ChannelImage" "EnableMediaSourceDisplay" "Width" /// "Height" "ExtraIds" "LocalTrailerCount" "IsHD" "SpecialFeatureCount" @Query("Fields") String? fields = defaultFields, - + /// Optional. Filter based on a search term. @Query("SearchTerm") String? searchTerm, @@ -359,6 +359,9 @@ abstract class JellyfinApi extends ChopperService { Future logout(); static JellyfinApi create() { + + final chopperHttpLogLevel = Level.body; //TODO allow changing the log level in settings (and a debug config file?) + final client = ChopperClient( // The first part of the URL is now here services: [ @@ -413,7 +416,7 @@ abstract class JellyfinApi extends ChopperService { // return request.copyWith( // headers: {"X-Emby-Authentication": await getAuthHeader()}); // }, - HttpLoggingInterceptor(level: Level.none), + HttpLoggingInterceptor(level: chopperHttpLogLevel), ], ); From 644669523a32a15b4e899415ffc0d058d5140a86 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 28 Sep 2023 21:06:50 +0200 Subject: [PATCH 089/130] clean up, less verbose logging --- lib/components/PlayerScreen/queue_list.dart | 10 +-- .../PlayerScreen/queue_list_item.dart | 46 +------------ .../music_player_background_task.dart | 10 --- lib/services/playback_history_service.dart | 2 - lib/services/queue_service.dart | 65 ++++++++----------- 5 files changed, 35 insertions(+), 98 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index c73ad0fa3..05173b74e 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -202,8 +202,7 @@ class _QueueListState extends State { // key: widget.nextUpHeaderKey, padding: const EdgeInsets.only(top: 20.0, bottom: 0.0), sliver: SliverPersistentHeader( - pinned: - false, //TODO use https://stackoverflow.com/a/69372976 to only ever have one of the headers pinned + pinned: false, //TODO use https://stackoverflow.com/a/69372976 to only ever have one of the headers pinned delegate: NextUpSectionHeader( controls: true, nextUpHeaderKey: widget.nextUpHeaderKey, @@ -234,7 +233,6 @@ class _QueueListState extends State { ), ], ), - // _source != null ? "Playing from ${_source?.name}" : "Queue", controls: true, nextUpHeaderKey: widget.nextUpHeaderKey, ), @@ -242,10 +240,14 @@ class _QueueListState extends State { ), // Queue QueueTracksList(previousTracksHeaderKey: widget.previousTracksHeaderKey), + const SliverPadding( + padding: EdgeInsets.only(bottom: 80.0, top: 40.0), + ) ]; return CustomScrollView( controller: widget.scrollController, + physics: const BouncingScrollPhysics(), slivers: _contents, ); } @@ -332,7 +334,7 @@ Future showQueueBottomSheet(BuildContext context) { ), ], ), - //TODO fade this out if the key is visible + //TODO fade this out if the current track is visible floatingActionButton: FloatingActionButton( onPressed: () { Vibrate.feedback(FeedbackType.impact); diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index 2e60cdc28..0cc976697 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -100,7 +100,7 @@ class _QueueListItemState extends State padding: const EdgeInsets.all(0.0), child: Text( widget.item.item.title, - style: this.widget.isCurrentTrack + style: widget.isCurrentTrack ? TextStyle( color: Theme.of(context).colorScheme.secondary, @@ -127,30 +127,11 @@ class _QueueListItemState extends State ), ], ), - // subtitle: Container( - // alignment: Alignment.centerLeft, - // height: 40.5, // has to be above a certain value to get rid of vertical padding - // child: Padding( - // padding: const EdgeInsets.only(bottom: 2.0), - // child: Text( - // processArtist(widget.item.item.artist, context), - // style: const TextStyle( - // color: Colors.white70, - // fontSize: 13, - // fontFamily: 'Lexend Deca', - // fontWeight: FontWeight.w300, - // overflow: TextOverflow.ellipsis), - // overflow: TextOverflow.ellipsis, - // ), - // ), - // ), trailing: Container( alignment: Alignment.centerRight, margin: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 6.0), - // width: widget.allowReorder ? 145.0 : 115.0, - width: widget.allowReorder ? 70.0 : 40.0, - height: 50.0, + width: widget.allowReorder ? 72.0 : 40.0, child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, @@ -163,29 +144,6 @@ class _QueueListItemState extends State color: Theme.of(context).textTheme.bodySmall?.color, ), ), - // IconButton( - // padding: const EdgeInsets.all(0.0), - // visualDensity: VisualDensity.compact, - // icon: const Icon( - // TablerIcons.dots_vertical, - // color: Colors.white, - // weight: 1.5, - // ), - // iconSize: 24.0, - // onPressed: () => showSongMenu(), - // ), - // IconButton( - // padding: const EdgeInsets.only(right: 14.0), - // visualDensity: VisualDensity.compact, - // icon: const Icon( - // TablerIcons.x, - // color: Colors.white, - // weight: 1.5, - // ), - // iconSize: 24.0, - // onPressed: () async => - // await _queueService.removeAtOffset(widget.indexOffset), - // ), if (widget.allowReorder) ReorderableDragStartListener( index: widget.listIndex, diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index f88a96942..45e09847d 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -72,16 +72,6 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { _playbackEventStreamController.add(event); - //TODO handle this is playback_history_service.dart - // if (playbackState.valueOrNull != null && - // playbackState.valueOrNull?.processingState != - // AudioProcessingState.idle && - // playbackState.valueOrNull?.processingState != - // AudioProcessingState.completed && - // !FinampSettingsHelper.finampSettings.isOffline && - // !_isStopping) { - // await _updatePlaybackProgress(); - // } }); // Special processing for state transitions. diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart index effbda046..e560b6cf5 100644 --- a/lib/services/playback_history_service.dart +++ b/lib/services/playback_history_service.dart @@ -100,8 +100,6 @@ class PlaybackHistoryService { // maybe handle toggling shuffle when sending the queue? would result in duplicate entries in the activity log, so maybe it's not desirable // same for updating the queue / next up - //TODO fix stop button not sending a playback state change event - } _previousPlaybackState = event; diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index a4cb90a03..62fbbc324 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -1,10 +1,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter/widgets.dart'; import 'package:just_audio/just_audio.dart'; import 'package:audio_service/audio_service.dart'; import 'package:get_it/get_it.dart'; @@ -12,14 +9,13 @@ import 'package:logging/logging.dart'; import 'package:rxdart/rxdart.dart'; import 'package:uuid/uuid.dart'; +import 'package:finamp/models/finamp_models.dart'; +import 'package:finamp/models/jellyfin_models.dart' as jellyfin_models; import 'finamp_user_helper.dart'; import 'jellyfin_api_helper.dart'; import 'finamp_settings_helper.dart'; import 'downloads_helper.dart'; -import '../models/finamp_models.dart'; -import '../models/jellyfin_models.dart' as jellyfin_models; import 'music_player_background_task.dart'; -import 'package:finamp/services/playback_history_service.dart'; enum PlaybackOrder { shuffled, linear } @@ -34,12 +30,12 @@ class QueueService { final _queueServiceLogger = Logger("QueueService"); // internal state - List _queuePreviousTracks = + final List _queuePreviousTracks = []; // contains **all** items that have been played, including "next up" QueueItem? _currentTrack; // the currently playing track - List _queueNextUp = + final List _queueNextUp = []; // a temporary queue that gets appended to if the user taps "next up" - List _queue = []; // contains all regular queue items + final List _queue = []; // contains all regular queue items QueueOrder _order = QueueOrder( items: [], originalSource: QueueItemSource( @@ -100,9 +96,8 @@ class QueueService { int _queueAudioSourceIndex = 0; QueueService() { - // _queueServiceLogger.level = Level.OFF; - + _shuffleOrder = NextUpShuffleOrder(queueService: this); _queueAudioSource = ConcatenatingAudioSource( children: [], @@ -110,16 +105,16 @@ class QueueService { ); _audioHandler.playbackState.listen((event) async { - // int indexDifference = (event.currentIndex ?? 0) - _queueAudioSourceIndex; - // _queueServiceLogger.finer("Play queue index changed, difference: $indexDifference"); - + final previousIndex = _queueAudioSourceIndex; _queueAudioSourceIndex = event.queueIndex ?? 0; - _queueServiceLogger.finer( - "Play queue index changed, new index: $_queueAudioSourceIndex"); - _queueFromConcatenatingAudioSource(); + if (previousIndex != _queueAudioSourceIndex) { + _queueServiceLogger.finer( + "Play queue index changed, new index: $_queueAudioSourceIndex"); + _queueFromConcatenatingAudioSource(); + } }); // register callbacks @@ -140,6 +135,11 @@ class QueueService { ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; + final previousTrack = _currentTrack; + final previousTracksPreviousLength = _queuePreviousTracks.length; + final nextUpPreviousLength = _queueNextUp.length; + final queuePreviousLength = _queue.length; + _queuePreviousTracks.clear(); _queueNextUp.clear(); _queue.clear(); @@ -198,7 +198,15 @@ class QueueService { .map((e) => e.item) .toList()); - _logQueues(message: "(current)"); + // only log queue if there's a change + if ( + previousTrack?.id != _currentTrack?.id || + previousTracksPreviousLength != _queuePreviousTracks.length || + nextUpPreviousLength != _queueNextUp.length || + queuePreviousLength != _queue.length + ) { + _logQueues(message: "(current)"); + } } Future startPlayback({ @@ -299,7 +307,6 @@ class QueueService { } Future stopPlayback() async { - queueServiceLogger.info("Stopping playback"); await _audioHandler.stop(); @@ -320,10 +327,6 @@ class QueueService { type: QueueItemQueueType.queue, ); - // _order.items.add(queueItem); - // _order.linearOrder.add(_order.items.length - 1); - // _order.shuffledOrder.add(_order.items.length - 1); //TODO maybe the item should be shuffled into the queue instead of placed at the end? depends on user preference - await _queueAudioSource.add(await _queueItemToAudioSource(queueItem)); _queueServiceLogger.fine( @@ -355,9 +358,6 @@ class QueueService { )); } - // don't add to _order, because it wasn't added to the regular queue - // int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; - for (final queueItem in queueItems.reversed) { await _queueAudioSource.insert(_queueAudioSourceIndex + 1, await _queueItemToAudioSource(queueItem)); @@ -391,10 +391,7 @@ class QueueService { )); } - // don't add to _order, because it wasn't added to the regular queue - _queueFromConcatenatingAudioSource(); // update internal queues - // int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; int offset = _queueNextUp.length; for (final queueItem in queueItems) { @@ -442,7 +439,6 @@ class QueueService { } Future clearNextUp() async { - // _queueFromConcatenatingAudioSource(); // update internal queues // remove all items from Next Up if (_queueNextUp.isNotEmpty) { @@ -461,9 +457,6 @@ class QueueService { queue: _queue, nextUp: _queueNextUp, source: _order.originalSource, - // nextUp: [ - // QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown)), - // ], ); } @@ -475,7 +468,7 @@ class QueueService { _queueStream.add(getQueue()); } - /// Returns the next [amount] QueueItems from Next Up and the regular queue. + /// Returns the next [amount] QueueItems from Next Up and the regular queue. /// The length of the returned list may be less than [amount] if there are not enough items in the queue List getNextXTracksInQueue(int amount) { List nextTracks = []; @@ -508,7 +501,6 @@ class QueueService { set loopMode(LoopMode mode) { _loopMode = mode; - // _currentTrackStream.add(_currentTrack ?? QueueItem(item: const MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemSourceType.unknown))); _loopModeStream.add(mode); @@ -526,7 +518,6 @@ class QueueService { set playbackOrder(PlaybackOrder order) { _playbackOrder = order; _queueServiceLogger.fine("Playback order set to $order"); - // _currentTrackStream.add(_currentTrack ?? QueueItem(item: MediaItem(id: "", title: "No track playing", album: "No album", artist: "No artist"), source: QueueItemSource(id: "", name: "", type: QueueItemType.unknown))); _playbackOrderStream.add(order); @@ -611,8 +602,6 @@ class QueueService { _jellyfinApiHelper.getImageUrl(item: item), title: item.name ?? "unknown", extras: { - // "parentId": item.parentId, - // "itemId": item.id, "itemJson": item.toJson(), "shouldTranscode": FinampSettingsHelper.finampSettings.shouldTranscode, "downloadedSongJson": isDownloaded From 2b711ab2de5da3cdca636b6204a2c0ddf9c9ddce Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 28 Sep 2023 21:18:26 +0200 Subject: [PATCH 090/130] increase buffer duration to compensate for spotty connections - has worked really well in my testing, with no pauses at all when losing reception for a few minutes - this could increase data usage if the Next Up queue is used a lot (frequent changes to next tracks) - @jmshrv definitely add a disclaimer to the release notes! --- lib/models/finamp_models.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 96ec669f5..16e102482 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -53,7 +53,7 @@ const _sleepTimerSeconds = 1800; // 30 Minutes const _showCoverAsPlayerBackground = true; const _hideSongArtistsIfSameAsAlbumArtists = true; const _disableGesture = false; -const _bufferDurationSeconds = 50; +const _bufferDurationSeconds = 600; @HiveType(typeId: 28) class FinampSettings { From e32b9a8ec84431e1d99ea67a2f57a71cc1dc474b Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 29 Sep 2023 21:21:11 +0200 Subject: [PATCH 091/130] remove dead code --- lib/components/PlayerScreen/queue_list.dart | 2 +- lib/components/now_playing_bar.dart | 2 - .../music_player_background_task.dart | 286 ++---------------- lib/services/queue_service.dart | 5 +- 4 files changed, 24 insertions(+), 271 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 05173b74e..4b5401412 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -482,7 +482,7 @@ class _NextUpTracksListState extends State { _nextUp ??= snapshot.data!.nextUp; return SliverPadding( - padding: const EdgeInsets.only(top: 0.0, left: 8.0, right: 8.0), + padding: const EdgeInsets.only(top: 0.0, left: 4.0, right: 4.0), sliver: SliverReorderableList( autoScrollerVelocityScalar: 20.0, onReorder: (oldIndex, newIndex) { diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index b41eb1065..840a47f39 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -78,10 +78,8 @@ class NowPlayingBar extends ConsumerWidget { direction: FinampSettingsHelper.finampSettings.disableGesture ? DismissDirection.none : DismissDirection.horizontal, confirmDismiss: (direction) async { if (direction == DismissDirection.endToStart) { - // queueService.nextTrack(); audioHandler.skipToNext(); } else { - // queueService.previousTrack(); audioHandler.skipToPrevious(); } return false; diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index 45e09847d..f2f0da984 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -1,20 +1,11 @@ import 'dart:async'; -import 'dart:io'; -import 'package:android_id/android_id.dart'; import 'package:audio_service/audio_service.dart'; -import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; -import 'package:get_it/get_it.dart'; import 'package:just_audio/just_audio.dart'; import 'package:logging/logging.dart'; -import 'package:rxdart/rxdart.dart'; -import '../models/finamp_models.dart'; -import '../models/jellyfin_models.dart' as jellyfin_models; import 'finamp_settings_helper.dart'; -import 'finamp_user_helper.dart'; -import 'jellyfin_api_helper.dart'; /// This provider handles the currently playing music so that multiple widgets /// can control music. @@ -30,24 +21,15 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { preferredForwardBufferDuration: FinampSettingsHelper.finampSettings.bufferDuration, )), - ); ConcatenatingAudioSource _queueAudioSource = ConcatenatingAudioSource(children: []); final _audioServiceBackgroundTaskLogger = Logger("MusicPlayerBackgroundTask"); - final _jellyfinApiHelper = GetIt.instance(); - final _finampUserHelper = GetIt.instance(); - - final _playbackEventStreamController = BehaviorSubject(); /// Set when creating a new queue. Will be used to set the first index in a /// new queue. int? nextInitialIndex; - /// Set to true when we're stopping the audio service. Used to avoid playback - /// progress reporting. - bool _isStopping = false; - /// Holds the current sleep timer, if any. This is a ValueNotifier so that /// widgets like SleepTimerButton can update when the sleep timer is/isn't /// null. @@ -55,9 +37,7 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { Duration _sleepTimerDuration = Duration.zero; final ValueNotifier _sleepTimer = ValueNotifier(null); - Future Function()? _queueCallbackNextTrack; Future Function()? _queueCallbackPreviousTrack; - Future Function(int)? _queueCallbackSkipToIndexCallback; List? get shuffleIndices => _player.shuffleIndices; @@ -69,9 +49,6 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { // Propagate all events from the audio player to AudioService clients. _player.playbackEventStream.listen((event) async { playbackState.add(_transformEvent(event)); - - _playbackEventStreamController.add(event); - }); // Special processing for state transitions. @@ -86,25 +63,16 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { (_) => playbackState.add(_transformEvent(_player.playbackEvent))); _player.loopModeStream.listen( (_) => playbackState.add(_transformEvent(_player.playbackEvent))); - } + /// this could be useful for updating queue state from this player class, but isn't used right now due to limitations with just_audio void setQueueCallbacks({ - required Future Function() nextTrackCallback, required Future Function() previousTrackCallback, - required Future Function(int) skipToIndexCallback }) { - _queueCallbackNextTrack = nextTrackCallback; _queueCallbackPreviousTrack = previousTrackCallback; - _queueCallbackSkipToIndexCallback = skipToIndexCallback; - } - - BehaviorSubject getPlaybackEventStream() { - return _playbackEventStreamController; } Future initializeAudioSource(ConcatenatingAudioSource source) async { - _queueAudioSource = source; try { @@ -119,8 +87,7 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { _audioServiceBackgroundTaskLogger .warning("Player interrupted: ${e.message}"); } catch (e) { - _audioServiceBackgroundTaskLogger - .severe("Player error ${e.toString()}"); + _audioServiceBackgroundTaskLogger.severe("Player error ${e.toString()}"); } } @@ -156,16 +123,15 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { try { _audioServiceBackgroundTaskLogger.info("Stopping audio service"); - _isStopping = true; - // Stop playing audio. await _player.stop(); mediaItem.add(null); - playbackState.add(playbackState.value.copyWith(processingState: AudioProcessingState.completed)); + playbackState.add(playbackState.value + .copyWith(processingState: AudioProcessingState.completed)); - // // Seek to the start of the first item in the queue - // await _player.seek(Duration.zero, index: 0); + // // Seek to the start of the current item + await _player.seek(Duration.zero); _sleepTimerIsSet = false; _sleepTimerDuration = Duration.zero; @@ -174,91 +140,6 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { _sleepTimer.value = null; await super.stop(); - - // await _player.dispose(); - // await _eventSubscription?.cancel(); - // It is important to wait for this state to be broadcast before we shut - // down the task. If we don't, the background task will be destroyed before - // the message gets sent to the UI. - // await _broadcastState(); - // Shut down this background task - // await super.stop(); - - _isStopping = false; - } catch (e) { - _audioServiceBackgroundTaskLogger.severe(e); - return Future.error(e); - } - } - - @override - Future addQueueItem(MediaItem mediaItem) async { - try { - await _queueAudioSource.add(await _mediaItemToAudioSource(mediaItem)); - queue.add(_queueFromSource()); - } catch (e) { - _audioServiceBackgroundTaskLogger.severe(e); - return Future.error(e); - } - } - - @override - Future updateQueue(List newQueue) async { - - _audioServiceBackgroundTaskLogger.severe("UPDATING QUEUE in music player background task, this shouldn't be happening!"); - - try { - // Convert the MediaItems to AudioSources - List audioSources = []; - for (final mediaItem in newQueue) { - audioSources.add(await _mediaItemToAudioSource(mediaItem)); - } - - // Create a new ConcatenatingAudioSource with the new queue. - _queueAudioSource = ConcatenatingAudioSource( - children: audioSources, - ); - - try { - await _player.setAudioSource( - _queueAudioSource, - initialIndex: nextInitialIndex, - ); - } on PlayerException catch (e) { - _audioServiceBackgroundTaskLogger - .severe("Player error code ${e.code}: ${e.message}"); - } on PlayerInterruptedException catch (e) { - _audioServiceBackgroundTaskLogger - .warning("Player interrupted: ${e.message}"); - } catch (e) { - _audioServiceBackgroundTaskLogger - .severe("Player error ${e.toString()}"); - } - queue.add(_queueFromSource()); - - // Sets the media item for the new queue. This will be whatever is - // currently playing from the new queue (for example, the first song in - // an album). If the player is shuffling, set the index to the player's - // current index. Otherwise, set it to nextInitialIndex. nextInitialIndex - // is much more stable than the current index as we know the value is set - // when running this function. - if (_player.shuffleModeEnabled) { - if (_player.currentIndex == null) { - _audioServiceBackgroundTaskLogger.severe( - "_player.currentIndex is null during onUpdateQueue, not setting new media item"); - } else { - mediaItem.add(_getQueueItem(_player.currentIndex!)); - } - } else { - if (nextInitialIndex == null) { - _audioServiceBackgroundTaskLogger.severe( - "nextInitialIndex is null during onUpdateQueue, not setting new media item"); - } else { - mediaItem.add(_getQueueItem(nextInitialIndex!)); - } - } - - nextInitialIndex = null; } catch (e) { _audioServiceBackgroundTaskLogger.severe(e); return Future.error(e); @@ -271,17 +152,15 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { @override Future skipToPrevious() async { - bool doSkip = true; - - try { + try { if (_queueCallbackPreviousTrack != null) { doSkip = await _queueCallbackPreviousTrack!(); } else { doSkip = _player.position.inSeconds < 5; } - + if (!_player.hasPrevious) { await _player.seek(Duration.zero, index: _player.currentIndex); } else { @@ -299,19 +178,10 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { @override Future skipToNext() async { - try { await _player.seekToNext(); - _audioServiceBackgroundTaskLogger.finer("_player.nextIndex: ${_player.nextIndex}"); - } catch (e) { - _audioServiceBackgroundTaskLogger.severe(e); - return Future.error(e); - } - } - - Future skipToIndex(int index) async { - try { - await _player.seek(Duration.zero, index: index); + _audioServiceBackgroundTaskLogger + .finer("_player.nextIndex: ${_player.nextIndex}"); } catch (e) { _audioServiceBackgroundTaskLogger.severe(e); return Future.error(e); @@ -319,14 +189,16 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } Future skipByOffset(int offset) async { - _audioServiceBackgroundTaskLogger.fine("skipping by offset: $offset"); - - try { - - await _player.seek(Duration.zero, index: - _player.shuffleModeEnabled ? _queueAudioSource.shuffleIndices[_queueAudioSource.shuffleIndices.indexOf((_player.currentIndex ?? 0)) + offset] : (_player.currentIndex ?? 0) + offset); + try { + await _player.seek(Duration.zero, + index: _player.shuffleModeEnabled + ? _queueAudioSource.shuffleIndices[_queueAudioSource + .shuffleIndices + .indexOf((_player.currentIndex ?? 0)) + + offset] + : (_player.currentIndex ?? 0) + offset); } catch (e) { _audioServiceBackgroundTaskLogger.severe(e); return Future.error(e); @@ -387,27 +259,10 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } } - @override - Future removeQueueItemAt(int index) async { - try { - await _queueAudioSource.removeAt(index); - queue.add(_queueFromSource()); - } catch (e) { - _audioServiceBackgroundTaskLogger.severe(e); - return Future.error(e); - } - } - void setNextInitialIndex(int index) { nextInitialIndex = index; } - Future reorderQueue(int oldIndex, int newIndex) async { - await _queueAudioSource.move(oldIndex, newIndex); - // queue.add(_queueFromSource()); - // _audioServiceBackgroundTaskLogger.log(Level.INFO, "Published queue"); - } - /// Sets the sleep timer with the given [duration]. Timer setSleepTimer(Duration duration) { _sleepTimerIsSet = true; @@ -467,112 +322,11 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { ); } - MediaItem _getQueueItem(int index) { - return _queueAudioSource.sequence[index].tag as MediaItem; - } - - List _queueFromSource() { - return _queueAudioSource.sequence.map((e) => (e.tag as QueueItem).item).toList(); - } - - List? get effectiveSequence => _player.sequenceState?.effectiveSequence; + List? get effectiveSequence => + _player.sequenceState?.effectiveSequence; double get volume => _player.volume; bool get paused => !_player.playing; Duration get playbackPosition => _player.position; - - /// Syncs the list of MediaItems (_queue) with the internal queue of the player. - /// Called by onAddQueueItem and onUpdateQueue. - Future _mediaItemToAudioSource(MediaItem mediaItem) async { - if (mediaItem.extras!["downloadedSongJson"] == null) { - // If DownloadedSong wasn't passed, we assume that the item is not - // downloaded. - - // If offline, we throw an error so that we don't accidentally stream from - // the internet. See the big comment in _songUri() to see why this was - // passed in extras. - if (mediaItem.extras!["isOffline"]) { - return Future.error( - "Offline mode enabled but downloaded song not found."); - } else { - if (mediaItem.extras!["shouldTranscode"] == true) { - return HlsAudioSource(await _songUri(mediaItem), tag: mediaItem); - } else { - return AudioSource.uri(await _songUri(mediaItem), tag: mediaItem); - } - } - } else { - // We have to deserialize this because Dart is stupid and can't handle - // sending classes through isolates. - final downloadedSong = - DownloadedSong.fromJson(mediaItem.extras!["downloadedSongJson"]); - - // Path verification and stuff is done in AudioServiceHelper, so this path - // should be valid. - final downloadUri = Uri.file(downloadedSong.file.path); - return AudioSource.uri(downloadUri, tag: mediaItem); - } - } - - Future _songUri(MediaItem mediaItem) async { - // We need the platform to be Android or iOS to get device info - assert(Platform.isAndroid || Platform.isIOS, - "_songUri() only supports Android and iOS"); - - // When creating the MediaItem (usually in AudioServiceHelper), we specify - // whether or not to transcode. We used to pull from FinampSettings here, - // but since audio_service runs in an isolate (or at least, it does until - // 0.18), the value would be wrong if changed while a song was playing since - // Hive is bad at multi-isolate stuff. - - final androidId = - Platform.isAndroid ? await const AndroidId().getId() : null; - final iosDeviceInfo = - Platform.isIOS ? await DeviceInfoPlugin().iosInfo : null; - - final parsedBaseUrl = Uri.parse(_finampUserHelper.currentUser!.baseUrl); - - List builtPath = List.from(parsedBaseUrl.pathSegments); - - Map queryParameters = - Map.from(parsedBaseUrl.queryParameters); - - // We include the user token as a query parameter because just_audio used to - // have issues with headers in HLS, and this solution still works fine - queryParameters["ApiKey"] = _finampUserHelper.currentUser!.accessToken; - - if (mediaItem.extras!["shouldTranscode"]) { - builtPath.addAll([ - "Audio", - mediaItem.extras!["itemJson"]["Id"], - "main.m3u8", - ]); - - queryParameters.addAll({ - "audioCodec": "aac", - // Ideally we'd use 48kHz when the source is, realistically it doesn't - // matter too much - "audioSampleRate": "44100", - "maxAudioBitDepth": "16", - "audioBitRate": - FinampSettingsHelper.finampSettings.transcodeBitrate.toString(), - }); - } else { - builtPath.addAll([ - "Items", - mediaItem.extras!["itemJson"]["Id"], - "File", - ]); - } - - return Uri( - host: parsedBaseUrl.host, - port: parsedBaseUrl.port, - scheme: parsedBaseUrl.scheme, - userInfo: parsedBaseUrl.userInfo, - pathSegments: builtPath, - queryParameters: queryParameters, - ); - } } AudioServiceRepeatMode _audioServiceRepeatMode(LoopMode loopMode) { diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 62fbbc324..cc7abce61 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -420,7 +420,8 @@ class QueueService { offset] : (_queueAudioSourceIndex) + offset; - await _audioHandler.removeQueueItemAt(index); + await _queueAudioSource.removeAt(index); + // await _audioHandler.removeQueueItemAt(index); _queueFromConcatenatingAudioSource(); } @@ -434,7 +435,7 @@ class QueueService { ? _queueAudioSourceIndex + newOffset - 1 : _queueAudioSourceIndex + newOffset; - await _audioHandler.reorderQueue(oldIndex, newIndex); + await _queueAudioSource.move(oldIndex, newIndex); _queueFromConcatenatingAudioSource(); } From c9f1902ae52fcb7a26e864c5a540ef5fcb0261d7 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 30 Sep 2023 14:43:06 +0200 Subject: [PATCH 092/130] use animated theme, refactor recent tracks toggle --- lib/components/PlayerScreen/queue_list.dart | 148 ++++++++++++------ .../PlayerScreen/queue_list_item.dart | 2 +- lib/components/now_playing_bar.dart | 3 +- lib/l10n/app_en.arb | 8 +- lib/screens/player_screen.dart | 3 +- lib/services/playback_history_service.dart | 1 + 6 files changed, 113 insertions(+), 52 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 4b5401412..e87c7b8f1 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -85,7 +85,7 @@ class _QueueListState extends State { QueueItemSource? _source; late List _contents; - bool isRecentTracksExpanded = false; + BehaviorSubject isRecentTracksExpanded = BehaviorSubject.seeded(false); @override void initState() { @@ -147,47 +147,25 @@ class _QueueListState extends State { Widget build(BuildContext context) { _contents = [ // Previous Tracks - if (isRecentTracksExpanded) - PreviousTracksList( - previousTracksHeaderKey: widget.previousTracksHeaderKey), - //TODO replace this with a SliverPersistentHeader and add an `onTap` callback to the delegate - SliverToBoxAdapter( - key: widget.previousTracksHeaderKey, - child: GestureDetector( - onTap: () { - Vibrate.feedback(FeedbackType.selection); - setState(() => isRecentTracksExpanded = !isRecentTracksExpanded); - if (!isRecentTracksExpanded) { - Future.delayed(const Duration(milliseconds: 200), - () => scrollToCurrentTrack()); - } - // else { - // Future.delayed(const Duration(milliseconds: 300), () => scrollToCurrentTrack()); - // } - }, - child: Padding( - padding: const EdgeInsets.only( - left: 14.0, right: 14.0, bottom: 12.0, top: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(top: 2.0), - child: Text(AppLocalizations.of(context)!.recentlyPlayed), - ), - const SizedBox(width: 4.0), - Icon( - isRecentTracksExpanded - ? TablerIcons.chevron_up - : TablerIcons.chevron_down, - size: 28.0, - color: Colors.white, - ), - ], - ), - ), - )), + StreamBuilder( + stream: isRecentTracksExpanded, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!) { + return PreviousTracksList( + previousTracksHeaderKey: widget.previousTracksHeaderKey); + } else { + return const SliverToBoxAdapter(); + } + } + ), + SliverPersistentHeader( + key: widget.previousTracksHeaderKey, + delegate: PreviousTracksSectionHeader( + isRecentTracksExpanded: isRecentTracksExpanded, + previousTracksHeaderKey: widget.previousTracksHeaderKey, + onTap: () => isRecentTracksExpanded.add(!isRecentTracksExpanded.value), + ) + ), CurrentTrack( // key: UniqueKey(), key: widget.currentTrackKey, @@ -276,7 +254,8 @@ Future showQueueBottomSheet(BuildContext context) { builder: (BuildContext context, WidgetRef ref, Widget? child) { final imageTheme = ref.watch(playerScreenThemeProvider); - return Theme( + return AnimatedTheme( + duration: const Duration(milliseconds: 750), data: ThemeData( fontFamily: "LexendDeca", colorScheme: imageTheme, @@ -1323,15 +1302,15 @@ class NextUpSectionHeader extends SliverPersistentHeaderDelegate { _queueService.clearNextUp(); Vibrate.feedback(FeedbackType.success); }, - child: const Row( + child: Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( - padding: EdgeInsets.only(top: 4.0), - child: Text("Clear Next Up"), + padding: const EdgeInsets.only(top: 4.0), + child: Text(AppLocalizations.of(context)!.clearNextUp), ), - Icon( + const Icon( TablerIcons.x, color: Colors.white, size: 32.0, @@ -1354,6 +1333,81 @@ class NextUpSectionHeader extends SliverPersistentHeaderDelegate { bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => false; } +class PreviousTracksSectionHeader extends SliverPersistentHeaderDelegate { + // final bool controls; + final double height; + final VoidCallback? onTap; + final GlobalKey previousTracksHeaderKey; + final BehaviorSubject isRecentTracksExpanded; + + PreviousTracksSectionHeader({ + required this.previousTracksHeaderKey, + required this.isRecentTracksExpanded, + // this.controls = false, + this.onTap, + this.height = 50.0, + }); + + @override + Widget build(context, double shrinkOffset, bool overlapsContent) { + + return Padding( + // color: Colors.black.withOpacity(0.5), + padding: const EdgeInsets.only(left: 14.0, right: 14.0, bottom: 12.0, top: 8.0), + child: GestureDetector( + onTap: () { + try { + if (onTap != null) { + onTap!(); + Vibrate.feedback(FeedbackType.selection); + } + } catch (e) { + Vibrate.feedback(FeedbackType.error); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text(AppLocalizations.of(context)!.previousTracks), + ), + const SizedBox(width: 4.0), + StreamBuilder( + stream: isRecentTracksExpanded, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!) { + return const Icon( + TablerIcons.chevron_up, + size: 28.0, + color: Colors.white, + ); + } else { + return const Icon( + TablerIcons.chevron_down, + size: 28.0, + color: Colors.white, + ); + } + } + ), + ], + ), + ), + ); + } + + @override + double get maxExtent => height; + + @override + double get minExtent => height; + + @override + bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => false; +} + /// If offline, check if an album is downloaded. Always returns true if online. /// Returns false if albumId is null. bool _isAlbumDownloadedIfOffline(String? albumId) { diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index 0cc976697..8145c5c98 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -131,7 +131,7 @@ class _QueueListItemState extends State alignment: Alignment.centerRight, margin: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 6.0), - width: widget.allowReorder ? 72.0 : 40.0, + width: widget.allowReorder ? 72.0 : 42.0, //TODO make this responsive child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index 840a47f39..ee596fcce 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -32,7 +32,8 @@ class NowPlayingBar extends ConsumerWidget { final audioHandler = GetIt.instance(); final queueService = GetIt.instance(); - return Theme( + return AnimatedTheme( + duration: const Duration(milliseconds: 750), data: ThemeData( fontFamily: "LexendDeca", colorScheme: imageTheme, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d32aee21c..bbf098123 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -484,14 +484,18 @@ "@bufferDurationSubtitle": {}, "language": "Language", "@language": {}, - "recentlyPlayed": "Recently Played", - "@recentlyPlayed": { + "previousTracks": "Recently Played", + "@previousTracks": { "description": "Description in the queue panel for the list of tracks that was previously played" }, "nextUp": "Next Up", "@nextUp": { "description": "Description in the queue panel for the list of tracks were manually added to be played after the current track. This should be capitalized (if applicable) to be more recognizable throughout the UI" }, + "clearNextUp": "Clear Next Up", + "@clearNextUp": { + "description": "Label for the action that deletes all tracks added to Next Up" + }, "playingFrom": "Playing from", "@playingFrom": { "description": "Prefix shown before the name of the main queue source, like the album or playlist that was used to start playback. Example: \"Playing from {My Nice Playlist}\"" diff --git a/lib/screens/player_screen.dart b/lib/screens/player_screen.dart index bb4cec5b2..1d6090c7a 100644 --- a/lib/screens/player_screen.dart +++ b/lib/screens/player_screen.dart @@ -31,7 +31,8 @@ class PlayerScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final imageTheme = ref.watch(playerScreenThemeProvider); - return Theme( + return AnimatedTheme( + duration: const Duration(milliseconds: 750), data: ThemeData( fontFamily: "LexendDeca", colorScheme: imageTheme, diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart index e560b6cf5..cc13dfe4b 100644 --- a/lib/services/playback_history_service.dart +++ b/lib/services/playback_history_service.dart @@ -263,6 +263,7 @@ class PlaybackHistoryService { if (previousTrackPlaybackData != null) { _playbackHistoryServiceLogger.info("Stopping playback progress for ${previousItem?.item.title}"); await _jellyfinApiHelper.stopPlaybackProgress(previousTrackPlaybackData); + //TODO also mark the track as played in the user data: https://api.jellyfin.org/openapi/api.html#tag/Playstate/operation/MarkPlayedItem } if (newTrackplaybackData != null) { _playbackHistoryServiceLogger.info("Starting playback progress for ${currentItem.item.title}"); From 1c596ad6fdf7f4dd9cf089051ce664147df55e83 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Mon, 2 Oct 2023 15:56:21 +0200 Subject: [PATCH 093/130] fix instant mixes, small queue service API changes --- ...album_screen_content_flexible_space_bar.dart | 14 +++++--------- lib/components/AlbumScreen/song_list_tile.dart | 2 +- lib/services/audio_service_helper.dart | 17 ++++++++--------- lib/services/queue_service.dart | 8 +++++++- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart index c5e986439..aa0f0bc96 100644 --- a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart +++ b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart @@ -30,7 +30,6 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { GetIt.instance(); void playAlbum() { - queueService.playbackOrder = PlaybackOrder.linear; queueService.startPlayback( items: items, source: QueueItemSource( @@ -38,12 +37,12 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, - ) + ), + order: PlaybackOrder.linear, ); } void shuffleAlbum() { - queueService.playbackOrder = PlaybackOrder.shuffled; queueService.startPlayback( items: items, source: QueueItemSource( @@ -51,12 +50,12 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, - ) + ), + order: PlaybackOrder.shuffled, ); } void addAlbumToNextUp() { - queueService.playbackOrder = PlaybackOrder.linear; queueService.addToNextUp( items: items, source: QueueItemSource( @@ -64,7 +63,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, - ) + ), ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -75,7 +74,6 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { } void addAlbumNext() { - queueService.playbackOrder = PlaybackOrder.linear; queueService.addNext( items: items, source: QueueItemSource( @@ -94,7 +92,6 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { void shuffleAlbumToNextUp() { // linear order is used in this case since we don't want to affect the rest of the queue - queueService.playbackOrder = PlaybackOrder.linear; List clonedItems = List.from(items); clonedItems.shuffle(); queueService.addToNextUp( @@ -115,7 +112,6 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { void shuffleAlbumNext() { // linear order is used in this case since we don't want to affect the rest of the queue - queueService.playbackOrder = PlaybackOrder.linear; List clonedItems = List.from(items); clonedItems.shuffle(); queueService.addNext( diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index 1e2b65a72..b5b55fbcc 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -187,7 +187,6 @@ class _SongListTileState extends State { onTap: () { if (widget.children != null) { // start linear playback of album from the given index - _queueService.playbackOrder = PlaybackOrder.linear; _queueService.startPlayback( items: widget.children!, source: QueueItemSource( @@ -196,6 +195,7 @@ class _SongListTileState extends State { id: widget.parentId ?? "", item: widget.item, ), + order: PlaybackOrder.linear, startingIndex: widget.index ?? 0, ); } else { diff --git a/lib/services/audio_service_helper.dart b/lib/services/audio_service_helper.dart index 4fc783a6b..3d5a67be5 100644 --- a/lib/services/audio_service_helper.dart +++ b/lib/services/audio_service_helper.dart @@ -54,8 +54,6 @@ class AudioServiceHelper { } if (items != null) { - // await replaceQueueWithItem(itemList: items, shuffle: true); - _queueService.playbackOrder = PlaybackOrder.shuffled; await _queueService.startPlayback( items: items, source: QueueItemSource( @@ -64,7 +62,8 @@ class AudioServiceHelper { type: isFavourite ? QueueItemSourceNameType.yourLikes : QueueItemSourceNameType.shuffleAll, ), id: "shuffleAll", - ) + ), + order: PlaybackOrder.shuffled, ); } } @@ -76,7 +75,6 @@ class AudioServiceHelper { try { items = await _jellyfinApiHelper.getInstantMix(item); if (items != null) { - // await replaceQueueWithItem(itemList: items, shuffle: false); await _queueService.startPlayback( items: items, source: QueueItemSource( @@ -86,7 +84,8 @@ class AudioServiceHelper { localizationParameter: item.name ?? "", ), id: item.id - ) + ), + order: PlaybackOrder.linear, // instant mixes should have their order determined by the server, especially to make sure the first item is the one that the mix is based off of ); } } catch (e) { @@ -102,7 +101,6 @@ class AudioServiceHelper { try { items = await _jellyfinApiHelper.getArtistMix(artists.map((e) => e.id).toList()); if (items != null) { - // await replaceQueueWithItem(itemList: items, shuffle: false); await _queueService.startPlayback( items: items, source: QueueItemSource( @@ -110,7 +108,8 @@ class AudioServiceHelper { name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: artists.map((e) => e.name).join(" & ")), id: artists.first.id, item: artists.first, - ) + ), + order: PlaybackOrder.linear, // instant mixes should have their order determined by the server, especially to make sure the first item is the one that the mix is based off of ); _jellyfinApiHelper.clearArtistMixBuilderList(); } @@ -127,7 +126,6 @@ class AudioServiceHelper { try { items = await _jellyfinApiHelper.getAlbumMix(albums.map((e) => e.id).toList()); if (items != null) { - // await replaceQueueWithItem(itemList: items, shuffle: false); await _queueService.startPlayback( items: items, source: QueueItemSource( @@ -135,7 +133,8 @@ class AudioServiceHelper { name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: albums.map((e) => e.name).join(" & ")), id: albums.first.id, item: albums.first, - ) + ), + order: PlaybackOrder.linear, // instant mixes should have their order determined by the server, especially to make sure the first item is the one that the mix is based off of ); _jellyfinApiHelper.clearAlbumMixBuilderList(); } diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index cc7abce61..84a76d56e 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -212,10 +212,16 @@ class QueueService { Future startPlayback({ required List items, required QueueItemSource source, + PlaybackOrder? order, int startingIndex = 0, }) async { // _initialQueue = list; // save original PlaybackList for looping/restarting and meta info - if (playbackOrder == PlaybackOrder.shuffled) { + + if (order != null) { + playbackOrder = order; + } + + if (_playbackOrder == PlaybackOrder.shuffled) { items.shuffle(); } await _replaceWholeQueue( From 678b4ad0a41681928dbb96478d27ce2243fc69eb Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Mon, 2 Oct 2023 20:16:39 +0200 Subject: [PATCH 094/130] small adjustments --- lib/components/PlayerScreen/queue_list.dart | 2 +- lib/components/now_playing_bar.dart | 2 +- lib/screens/player_screen.dart | 2 +- .../music_player_background_task.dart | 26 ++++++++++++------- lib/services/queue_service.dart | 6 ++++- 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index e87c7b8f1..fc3646f26 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -255,7 +255,7 @@ Future showQueueBottomSheet(BuildContext context) { final imageTheme = ref.watch(playerScreenThemeProvider); return AnimatedTheme( - duration: const Duration(milliseconds: 750), + duration: const Duration(milliseconds: 500), data: ThemeData( fontFamily: "LexendDeca", colorScheme: imageTheme, diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index ee596fcce..e5ef4c6d1 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -33,7 +33,7 @@ class NowPlayingBar extends ConsumerWidget { final queueService = GetIt.instance(); return AnimatedTheme( - duration: const Duration(milliseconds: 750), + duration: const Duration(milliseconds: 500), data: ThemeData( fontFamily: "LexendDeca", colorScheme: imageTheme, diff --git a/lib/screens/player_screen.dart b/lib/screens/player_screen.dart index 1d6090c7a..6cd1e00e0 100644 --- a/lib/screens/player_screen.dart +++ b/lib/screens/player_screen.dart @@ -32,7 +32,7 @@ class PlayerScreen extends ConsumerWidget { final imageTheme = ref.watch(playerScreenThemeProvider); return AnimatedTheme( - duration: const Duration(milliseconds: 750), + duration: const Duration(milliseconds: 500), data: ThemeData( fontFamily: "LexendDeca", colorScheme: imageTheme, diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index f2f0da984..629382942 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -59,10 +59,18 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { }); // PlaybackEvent doesn't include shuffle/loops so we listen for changes here - _player.shuffleModeEnabledStream.listen( - (_) => playbackState.add(_transformEvent(_player.playbackEvent))); - _player.loopModeStream.listen( - (_) => playbackState.add(_transformEvent(_player.playbackEvent))); + _player.shuffleModeEnabledStream.listen((_) { + final event = _transformEvent(_player.playbackEvent); + playbackState.add(event); + _audioServiceBackgroundTaskLogger.info( + "Shuffle mode changed to ${event.shuffleMode} (${_player.shuffleModeEnabled})."); + }); + _player.loopModeStream.listen((_) { + final event = _transformEvent(_player.playbackEvent); + playbackState.add(event); + _audioServiceBackgroundTaskLogger.info( + "Loop mode changed to ${event.repeatMode} (${_player.loopMode})."); + }); } /// this could be useful for updating queue state from this player class, but isn't used right now due to limitations with just_audio @@ -162,10 +170,10 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } if (!_player.hasPrevious) { - await _player.seek(Duration.zero, index: _player.currentIndex); + await _player.seek(Duration.zero); } else { if (doSkip) { - await _player.seek(Duration.zero, index: _player.previousIndex); + await _player.seekToPrevious(); } else { await _player.seek(Duration.zero); } @@ -220,7 +228,7 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { try { switch (shuffleMode) { case AudioServiceShuffleMode.all: - await _player.shuffle(); + // await _player.shuffle(); await _player.setShuffleModeEnabled(true); break; case AudioServiceShuffleMode.none: @@ -228,7 +236,7 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { break; default: return Future.error( - "Unsupported AudioServiceRepeatMode! Recieved ${shuffleMode.toString()}, requires all or none."); + "Unsupported AudioServiceRepeatMode! Received ${shuffleMode.toString()}, requires all or none."); } } catch (e) { _audioServiceBackgroundTaskLogger.severe(e); @@ -251,7 +259,7 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { break; default: return Future.error( - "Unsupported AudioServiceRepeatMode! Recieved ${repeatMode.toString()}, requires all, none, or one."); + "Unsupported AudioServiceRepeatMode! Received ${repeatMode.toString()}, requires all, none, or one."); } } catch (e) { _audioServiceBackgroundTaskLogger.severe(e); diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 84a76d56e..f865eb3ec 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -111,8 +111,12 @@ class QueueService { _queueAudioSourceIndex = event.queueIndex ?? 0; if (previousIndex != _queueAudioSourceIndex) { + int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && + _queueAudioSource.shuffleIndices.isNotEmpty) + ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) + : _queueAudioSourceIndex; _queueServiceLogger.finer( - "Play queue index changed, new index: $_queueAudioSourceIndex"); + "Play queue index changed, new index: $adjustedQueueIndex (actual index: $_queueAudioSourceIndex)"); _queueFromConcatenatingAudioSource(); } }); From 9fdbb52e3543e24d13c51a70b270290cdcc8ebdf Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 3 Oct 2023 13:21:30 +0200 Subject: [PATCH 095/130] made queue list current track progress slider responsive --- lib/components/PlayerScreen/progress_slider.dart | 3 ++- lib/components/PlayerScreen/queue_list.dart | 15 ++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/components/PlayerScreen/progress_slider.dart b/lib/components/PlayerScreen/progress_slider.dart index 07926eb99..c83c89a10 100644 --- a/lib/components/PlayerScreen/progress_slider.dart +++ b/lib/components/PlayerScreen/progress_slider.dart @@ -46,7 +46,7 @@ class _ProgressSliderState extends State { // RepaintBoundary to avoid more areas being repainted than necessary child: SliderTheme( data: SliderThemeData( - trackHeight: 2.0, + trackHeight: 4.0, trackShape: CustomTrackShape(), ), child: RepaintBoundary( @@ -243,6 +243,7 @@ class __PlaybackProgressSliderState // ? _sliderThemeData.copyWith( ? SliderTheme.of(context).copyWith( inactiveTrackColor: Colors.transparent, + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8), ) // ) // : _sliderThemeData.copyWith( diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index fc3646f26..725e2a78d 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -659,6 +659,9 @@ class _CurrentTrackState extends State { : jellyfin_models.BaseItemDto.fromJson( currentTrack!.item.extras?["itemJson"]); + final horizontalPadding = 8.0; + final albumImageSize = 70.0; + return SliverAppBar( pinned: true, collapsedHeight: 70.0, @@ -670,8 +673,8 @@ class _CurrentTrackState extends State { backgroundColor: const Color.fromRGBO(0, 0, 0, 0.0), flexibleSpace: Container( // width: 58, - height: 70.0, - padding: const EdgeInsets.symmetric(horizontal: 8.0), + height: albumImageSize, + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), child: Container( clipBehavior: Clip.antiAlias, decoration: ShapeDecoration( @@ -704,8 +707,8 @@ class _CurrentTrackState extends State { }).toList(), ), Container( - width: 70, - height: 70, + width: albumImageSize, + height: albumImageSize, decoration: const ShapeDecoration( shape: Border(), color: Color.fromRGBO(0, 0, 0, 0.3), @@ -740,8 +743,10 @@ class _CurrentTrackState extends State { builder: (context, snapshot) { if (snapshot.hasData) { playbackPosition = snapshot.data; + final screenSize = MediaQuery.of(context).size; return Container( - width: 298 * + // rather hacky workaround, using LayoutBuilder would be nice but I couldn't get it to work... + width: (screenSize.width - 2*horizontalPadding - albumImageSize) * (playbackPosition!.inMilliseconds / (mediaState?.mediaItem ?.duration ?? From a2405099007150c550803ee35a8a325465a6f66c Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 3 Oct 2023 13:59:14 +0200 Subject: [PATCH 096/130] fix next up source change --- lib/components/PlayerScreen/queue_list.dart | 3 +-- lib/services/playback_history_service.dart | 3 +-- lib/services/queue_service.dart | 25 ++++++++++++++++----- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 725e2a78d..257bea33f 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -243,7 +243,7 @@ Future showQueueBottomSheet(BuildContext context) { useSafeArea: true, enableDrag: true, isScrollControlled: true, - routeSettings: const RouteSettings(name: "/queue"), //TODO register this globally somehow? + routeSettings: const RouteSettings(name: "/queue"), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24.0)), ), @@ -1215,7 +1215,6 @@ class QueueSectionHeader extends SliverPersistentHeaderDelegate { onPressed: () { queueService.togglePlaybackOrder(); Vibrate.feedback(FeedbackType.success); - //TODO why is the current track scrolled out of view **after** the queue is updated? Future.delayed( const Duration(milliseconds: 300), () => scrollToKey( diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart index cc13dfe4b..43d52ab8f 100644 --- a/lib/services/playback_history_service.dart +++ b/lib/services/playback_history_service.dart @@ -105,9 +105,8 @@ class PlaybackHistoryService { _previousPlaybackState = event; }); - //TODO Tell Jellyfin we're not / no longer playing audio on startup + //TODO Tell Jellyfin we're not / no longer playing audio on startup - doesn't currently work because an item ID is required, and we don't have one (yet) // if (!FinampSettingsHelper.finampSettings.isOffline) { - //FIXME why is an ID required? which ID should we use? an empty string doesn't work... // final playbackInfo = generatePlaybackProgressInfoFromState(const MediaItem(id: "", title: ""), _audioService.playbackState.valueOrNull ?? PlaybackState()); // if (playbackInfo != null) { // _playbackHistoryServiceLogger.info("Stopping playback progress after startup"); diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index f865eb3ec..e86d1deb1 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -148,6 +148,8 @@ class QueueService { _queueNextUp.clear(); _queue.clear(); + bool canHaveNextUp = true; + // split the queue by old type for (int i = 0; i < allTracks.length; i++) { if (i < adjustedQueueIndex) { @@ -165,15 +167,25 @@ class QueueService { _currentTrack = allTracks[i]; _currentTrack!.type = QueueItemQueueType.currentTrack; } else { - if (allTracks[i].type == QueueItemQueueType.nextUp) { - //TODO this *should* mark items from Next Up as formerNextUp when skipping backwards before Next Up is played, but it doesn't work for some reason - if (i == adjustedQueueIndex + 1 || - i == adjustedQueueIndex + 1 + _queueNextUp.length) { + if (allTracks[i].type == QueueItemQueueType.currentTrack && allTracks[i].source.type == QueueItemSourceType.nextUp) { + _queue.add(allTracks[i]); + _queue.last.type = QueueItemQueueType.queue; + _queue.last.source = QueueItemSource( + type: QueueItemSourceType.formerNextUp, + name: const QueueItemSourceName(type: QueueItemSourceNameType.tracksFormerNextUp), + id: "former-next-up" + ); + canHaveNextUp = false; + } + else if (allTracks[i].type == QueueItemQueueType.nextUp) { + if (canHaveNextUp) { _queueNextUp.add(allTracks[i]); - } else { + _queueNextUp.last.type = QueueItemQueueType.nextUp; + } + else { _queue.add(allTracks[i]); _queue.last.type = QueueItemQueueType.queue; - _queuePreviousTracks.last.source = QueueItemSource( + _queue.last.source = QueueItemSource( type: QueueItemSourceType.formerNextUp, name: const QueueItemSourceName( type: QueueItemSourceNameType.tracksFormerNextUp), @@ -182,6 +194,7 @@ class QueueService { } else { _queue.add(allTracks[i]); _queue.last.type = QueueItemQueueType.queue; + canHaveNextUp = false; } } } From c436ae20902336a71df35827e4ef4c01272e88d3 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 3 Oct 2023 15:28:46 +0200 Subject: [PATCH 097/130] fix loop one skip behavior - manual skips will switch the track, reaching the end of the track will loop it --- lib/services/music_player_background_task.dart | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index 629382942..8a8f0bb6d 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -173,7 +173,12 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { await _player.seek(Duration.zero); } else { if (doSkip) { - await _player.seekToPrevious(); + if (_player.loopMode == LoopMode.one) { + // if the user manually skips to the previous track, they probably want to actually skip to the previous track + await skipByOffset(-1); //!!! don't use _player.previousIndex here, because that adjusts based on loop mode + } else { + await _player.seekToPrevious(); + } } else { await _player.seek(Duration.zero); } @@ -187,7 +192,12 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { @override Future skipToNext() async { try { - await _player.seekToNext(); + if (_player.loopMode == LoopMode.one && _player.hasNext) { + // if the user manually skips to the next track, they probably want to actually skip to the next track + await skipByOffset(1); //!!! don't use _player.nextIndex here, because that adjusts based on loop mode + } else { + await _player.seekToNext(); + } _audioServiceBackgroundTaskLogger .finer("_player.nextIndex: ${_player.nextIndex}"); } catch (e) { From 61683c63afaa4436d9ac672ab15e5f93deb9aea5 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 3 Oct 2023 16:18:40 +0200 Subject: [PATCH 098/130] persist FinampLoopMode across restarts - required some changes to types to add Hive support and prevent name conflicts --- ...bum_screen_content_flexible_space_bar.dart | 4 +- .../AlbumScreen/song_list_tile.dart | 2 +- .../playback_history_list.dart | 6 +- .../playback_history_list_tile.dart | 2 +- .../player_buttons_repeating.dart | 21 +- .../PlayerScreen/player_buttons_shuffle.dart | 5 +- .../player_screen_appbar_title.dart | 4 +- lib/components/PlayerScreen/queue_list.dart | 48 +- .../PlayerScreen/queue_list_item.dart | 4 +- lib/components/PlayerScreen/song_info.dart | 4 +- .../PlayerScreen/song_name_content.dart | 2 +- lib/main.dart | 1 + lib/models/finamp_models.dart | 123 ++-- lib/models/finamp_models.g.dart | 561 +++++++++++++++++- lib/services/audio_service_helper.dart | 8 +- lib/services/finamp_settings_helper.dart | 10 + lib/services/playback_history_service.dart | 44 +- lib/services/queue_service.dart | 127 ++-- 18 files changed, 802 insertions(+), 174 deletions(-) diff --git a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart index aa0f0bc96..91b5dd343 100644 --- a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart +++ b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart @@ -38,7 +38,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { id: parentItem.id, item: parentItem, ), - order: PlaybackOrder.linear, + order: FinampPlaybackOrder.linear, ); } @@ -51,7 +51,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { id: parentItem.id, item: parentItem, ), - order: PlaybackOrder.shuffled, + order: FinampPlaybackOrder.shuffled, ); } diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index b5b55fbcc..cb9bb9e9d 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -195,7 +195,7 @@ class _SongListTileState extends State { id: widget.parentId ?? "", item: widget.item, ), - order: PlaybackOrder.linear, + order: FinampPlaybackOrder.linear, startingIndex: widget.index ?? 0, ); } else { diff --git a/lib/components/PlaybackHistoryScreen/playback_history_list.dart b/lib/components/PlaybackHistoryScreen/playback_history_list.dart index 409f1e2c6..ee036e7fb 100644 --- a/lib/components/PlaybackHistoryScreen/playback_history_list.dart +++ b/lib/components/PlaybackHistoryScreen/playback_history_list.dart @@ -19,10 +19,10 @@ class PlaybackHistoryList extends StatelessWidget { final playbackHistoryService = GetIt.instance(); final audioServiceHelper = GetIt.instance(); - List? history; - List>> groupedHistory; + List? history; + List>> groupedHistory; - return StreamBuilder>( + return StreamBuilder>( stream: playbackHistoryService.historyStream, builder: (context, snapshot) { if (snapshot.hasData) { diff --git a/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart b/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart index 2fb2fa296..84c9db088 100644 --- a/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart +++ b/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart @@ -28,7 +28,7 @@ class PlaybackHistoryListTile extends StatefulWidget { }); final int actualIndex; - final HistoryItem item; + final FinampHistoryItem item; final AudioServiceHelper audioServiceHelper; late void Function() onTap; diff --git a/lib/components/PlayerScreen/player_buttons_repeating.dart b/lib/components/PlayerScreen/player_buttons_repeating.dart index 8d302f73c..153c61c41 100644 --- a/lib/components/PlayerScreen/player_buttons_repeating.dart +++ b/lib/components/PlayerScreen/player_buttons_repeating.dart @@ -1,4 +1,5 @@ import 'package:audio_service/audio_service.dart'; +import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/services/media_state_stream.dart'; import 'package:finamp/services/music_player_background_task.dart'; import 'package:finamp/services/queue_service.dart'; @@ -24,17 +25,17 @@ class PlayerButtonsRepeating extends StatelessWidget { onPressed: () async { // Cycles from none -> all -> one switch (queueService.loopMode) { - case LoopMode.none: - queueService.loopMode = LoopMode.all; + case FinampLoopMode.none: + queueService.loopMode = FinampLoopMode.all; break; - case LoopMode.all: - queueService.loopMode = LoopMode.one; + case FinampLoopMode.all: + queueService.loopMode = FinampLoopMode.one; break; - case LoopMode.one: - queueService.loopMode = LoopMode.none; + case FinampLoopMode.one: + queueService.loopMode = FinampLoopMode.none; break; default: - queueService.loopMode = LoopMode.none; + queueService.loopMode = FinampLoopMode.none; break; } }, @@ -46,10 +47,10 @@ class PlayerButtonsRepeating extends StatelessWidget { } Widget _getRepeatingIcon( - LoopMode loopMode, Color iconColour) { - if (loopMode == LoopMode.all) { + FinampLoopMode loopMode, Color iconColour) { + if (loopMode == FinampLoopMode.all) { return const Icon(TablerIcons.repeat); - } else if (loopMode == LoopMode.one) { + } else if (loopMode == FinampLoopMode.one) { return const Icon(TablerIcons.repeat_once); } else { return const Icon(TablerIcons.repeat_off); diff --git a/lib/components/PlayerScreen/player_buttons_shuffle.dart b/lib/components/PlayerScreen/player_buttons_shuffle.dart index 54c841ed6..bd2ac4f95 100644 --- a/lib/components/PlayerScreen/player_buttons_shuffle.dart +++ b/lib/components/PlayerScreen/player_buttons_shuffle.dart @@ -1,4 +1,5 @@ import 'package:audio_service/audio_service.dart'; +import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/services/media_state_stream.dart'; import 'package:finamp/services/music_player_background_task.dart'; import 'package:finamp/services/queue_service.dart'; @@ -19,10 +20,10 @@ class PlayerButtonsShuffle extends StatelessWidget { builder: (BuildContext context, AsyncSnapshot snapshot) { return IconButton( onPressed: () async { - _queueService.playbackOrder = _queueService.playbackOrder == PlaybackOrder.shuffled ? PlaybackOrder.linear : PlaybackOrder.shuffled; + _queueService.playbackOrder = _queueService.playbackOrder == FinampPlaybackOrder.shuffled ? FinampPlaybackOrder.linear : FinampPlaybackOrder.shuffled; }, icon: Icon( - (_queueService.playbackOrder == PlaybackOrder.shuffled + (_queueService.playbackOrder == FinampPlaybackOrder.shuffled ? TablerIcons.arrows_shuffle : TablerIcons.arrows_right), ), diff --git a/lib/components/PlayerScreen/player_screen_appbar_title.dart b/lib/components/PlayerScreen/player_screen_appbar_title.dart index f2d1bdfa5..18874e56e 100644 --- a/lib/components/PlayerScreen/player_screen_appbar_title.dart +++ b/lib/components/PlayerScreen/player_screen_appbar_title.dart @@ -31,7 +31,7 @@ class _PlayerScreenAppBarTitleState extends State { final currentTrackStream = _queueService.getCurrentTrackStream(); - return StreamBuilder( + return StreamBuilder( stream: currentTrackStream, initialData: _queueService.getCurrentTrack(), builder: (context, snapshot) { @@ -44,7 +44,7 @@ class _PlayerScreenAppBarTitleState extends State { onTap: () => navigateToSource(context, queueItem.source), child: Column( children: [ - Text(AppLocalizations.of(context)!.playingFromType(queueItem.source.type.name.toLowerCase()), + Text(AppLocalizations.of(context)!.playingFromType(queueItem.source.type.toString()), style: TextStyle( fontSize: 12, fontWeight: FontWeight.w300, diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 257bea33f..7da953d36 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -33,7 +33,7 @@ class _QueueListStreamState { ); final MediaState mediaState; - final QueueInfo queueInfo; + final FinampQueueInfo queueInfo; } class QueueList extends StatefulWidget { @@ -361,11 +361,11 @@ class PreviousTracksList extends StatefulWidget { class _PreviousTracksListState extends State with TickerProviderStateMixin { final _queueService = GetIt.instance(); - List? _previousTracks; + List? _previousTracks; @override Widget build(context) { - return StreamBuilder( + return StreamBuilder( stream: _queueService.getQueueStream(), builder: (context, snapshot) { if (snapshot.hasData) { @@ -381,7 +381,7 @@ class _PreviousTracksListState extends State Vibrate.feedback(FeedbackType.impact); setState(() { // temporarily update internal queue - QueueItem tmp = _previousTracks!.removeAt(oldIndex); + FinampQueueItem tmp = _previousTracks!.removeAt(oldIndex); _previousTracks!.insert( newIndex < oldIndex ? newIndex : newIndex - 1, tmp); // update external queue to commit changes, results in a rebuild @@ -415,7 +415,7 @@ class _PreviousTracksListState extends State indexOffset: indexOffset, subqueue: _previousTracks!, allowReorder: - _queueService.playbackOrder == PlaybackOrder.linear, + _queueService.playbackOrder == FinampPlaybackOrder.linear, onTap: () async { Vibrate.feedback(FeedbackType.selection); await _queueService.skipByOffset(indexOffset); @@ -450,11 +450,11 @@ class NextUpTracksList extends StatefulWidget { class _NextUpTracksListState extends State { final _queueService = GetIt.instance(); - List? _nextUp; + List? _nextUp; @override Widget build(context) { - return StreamBuilder( + return StreamBuilder( stream: _queueService.getQueueStream(), builder: (context, snapshot) { if (snapshot.hasData) { @@ -471,7 +471,7 @@ class _NextUpTracksListState extends State { Vibrate.feedback(FeedbackType.impact); setState(() { // temporarily update internal queue - QueueItem tmp = _nextUp!.removeAt(oldIndex); + FinampQueueItem tmp = _nextUp!.removeAt(oldIndex); _nextUp!.insert( newIndex < oldIndex ? newIndex : newIndex - 1, tmp); // update external queue to commit changes, results in a rebuild @@ -537,12 +537,12 @@ class QueueTracksList extends StatefulWidget { class _QueueTracksListState extends State { final _queueService = GetIt.instance(); - List? _queue; - List? _nextUp; + List? _queue; + List? _nextUp; @override Widget build(context) { - return StreamBuilder( + return StreamBuilder( stream: _queueService.getQueueStream(), builder: (context, snapshot) { if (snapshot.hasData) { @@ -562,7 +562,7 @@ class _QueueTracksListState extends State { Vibrate.feedback(FeedbackType.impact); setState(() { // temporarily update internal queue - QueueItem tmp = _queue!.removeAt(oldIndex); + FinampQueueItem tmp = _queue!.removeAt(oldIndex); _queue!.insert( newIndex < oldIndex ? newIndex : newIndex - 1, tmp); }); @@ -593,7 +593,7 @@ class _QueueTracksListState extends State { indexOffset: indexOffset, subqueue: _queue!, allowReorder: - _queueService.playbackOrder == PlaybackOrder.linear, + _queueService.playbackOrder == FinampPlaybackOrder.linear, onTap: () async { Vibrate.feedback(FeedbackType.selection); await _queueService.skipByOffset(indexOffset); @@ -639,12 +639,12 @@ class _CurrentTrackState extends State { @override Widget build(context) { - QueueItem? currentTrack; + FinampQueueItem? currentTrack; MediaState? mediaState; Duration? playbackPosition; return StreamBuilder<_QueueListStreamState>( - stream: Rx.combineLatest2( + stream: Rx.combineLatest2( mediaStateStream, _queueService.getQueueStream(), (a, b) => _QueueListStreamState(a, b)), @@ -954,7 +954,7 @@ class _CurrentTrackState extends State { ); } - void showSongMenu(QueueItem currentTrack) async { + void showSongMenu(FinampQueueItem currentTrack) async { final item = jellyfin_models.BaseItemDto.fromJson( currentTrack.item.extras?["itemJson"]); @@ -1122,7 +1122,7 @@ class _CurrentTrackState extends State { } } - Future setFavourite(QueueItem track) async { + Future setFavourite(FinampQueueItem track) async { try { // We switch the widget state before actually doing the request to // make the app feel faster (without, there is a delay from the @@ -1156,8 +1156,8 @@ class _CurrentTrackState extends State { } class PlaybackBehaviorInfo { - final PlaybackOrder order; - final LoopMode loop; + final FinampPlaybackOrder order; + final FinampLoopMode loop; PlaybackBehaviorInfo(this.order, this.loop); } @@ -1202,14 +1202,14 @@ class QueueSectionHeader extends SliverPersistentHeaderDelegate { IconButton( padding: const EdgeInsets.only(bottom: 2.0), iconSize: 28.0, - icon: info?.order == PlaybackOrder.shuffled + icon: info?.order == FinampPlaybackOrder.shuffled ? (const Icon( TablerIcons.arrows_shuffle, )) : (const Icon( TablerIcons.arrows_right, )), - color: info?.order == PlaybackOrder.shuffled + color: info?.order == FinampPlaybackOrder.shuffled ? IconTheme.of(context).color! : Colors.white, onPressed: () { @@ -1225,8 +1225,8 @@ class QueueSectionHeader extends SliverPersistentHeaderDelegate { IconButton( padding: const EdgeInsets.only(bottom: 2.0), iconSize: 28.0, - icon: info?.loop != LoopMode.none - ? (info?.loop == LoopMode.one + icon: info?.loop != FinampLoopMode.none + ? (info?.loop == FinampLoopMode.one ? (const Icon( TablerIcons.repeat_once, )) @@ -1236,7 +1236,7 @@ class QueueSectionHeader extends SliverPersistentHeaderDelegate { : (const Icon( TablerIcons.repeat_off, )), - color: info?.loop != LoopMode.none + color: info?.loop != FinampLoopMode.none ? IconTheme.of(context).color! : Colors.white, onPressed: () { diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index 8145c5c98..d7ac510cd 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -18,11 +18,11 @@ import 'package:flutter_vibrate/flutter_vibrate.dart'; import 'package:get_it/get_it.dart'; class QueueListItem extends StatefulWidget { - final QueueItem item; + final FinampQueueItem item; final int listIndex; final int actualIndex; final int indexOffset; - final List subqueue; + final List subqueue; final bool isCurrentTrack; final bool isPreviousTrack; final bool allowReorder; diff --git a/lib/components/PlayerScreen/song_info.dart b/lib/components/PlayerScreen/song_info.dart index a50c7be73..edb0bfb99 100644 --- a/lib/components/PlayerScreen/song_info.dart +++ b/lib/components/PlayerScreen/song_info.dart @@ -38,7 +38,7 @@ class _SongInfoState extends State { @override Widget build(BuildContext context) { - return StreamBuilder( + return StreamBuilder( stream: queueService.getQueueStream(), builder: (context, snapshot) { @@ -124,7 +124,7 @@ class _PlayerScreenAlbumImage extends ConsumerWidget { required this.queueItem, }) : super(key: key); - final QueueItem queueItem; + final FinampQueueItem queueItem; @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/lib/components/PlayerScreen/song_name_content.dart b/lib/components/PlayerScreen/song_name_content.dart index 0a6e6a081..4f5c17fd2 100644 --- a/lib/components/PlayerScreen/song_name_content.dart +++ b/lib/components/PlayerScreen/song_name_content.dart @@ -15,7 +15,7 @@ class SongNameContent extends StatelessWidget { required this.separatedArtistTextSpans, required this.secondaryTextColour}) : super(key: key); - final QueueItem currentTrack; + final FinampQueueItem currentTrack; final List separatedArtistTextSpans; final Color? secondaryTextColour; diff --git a/lib/main.dart b/lib/main.dart index 4f8d9ed49..43b416e6b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -163,6 +163,7 @@ Future setupHive() async { Hive.registerAdapter(DownloadedImageAdapter()); Hive.registerAdapter(ThemeModeAdapter()); Hive.registerAdapter(LocaleAdapter()); + Hive.registerAdapter(FinampLoopModeAdapter()); await Future.wait([ Hive.openBox("DownloadedParents"), Hive.openBox("DownloadedItems"), diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 16e102482..bc32c4811 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -54,6 +55,7 @@ const _showCoverAsPlayerBackground = true; const _hideSongArtistsIfSameAsAlbumArtists = true; const _disableGesture = false; const _bufferDurationSeconds = 600; +const _defaultLoopMode = FinampLoopMode.all; @HiveType(typeId: 28) class FinampSettings { @@ -85,6 +87,7 @@ class FinampSettings { this.bufferDurationSeconds = _bufferDurationSeconds, required this.tabSortBy, required this.tabSortOrder, + this.loopMode = _defaultLoopMode, }); @HiveField(0) @@ -168,6 +171,9 @@ class FinampSettings { @HiveField(21, defaultValue: {}) Map tabSortOrder; + @HiveField(22, defaultValue: _defaultLoopMode) + FinampLoopMode loopMode; + static Future create() async { final internalSongDir = await getInternalSongDir(); final downloadLocation = DownloadLocation.create( @@ -556,36 +562,69 @@ class DownloadedImage { ); } -enum QueueItemSourceType { - album(name: "album"), - playlist(name: "playlist"), - songMix(name: "songMix"), - artistMix(name: "artistMix"), - albumMix(name: "albumMix"), - favorites(name: "favorites"), - songs(name: "songs"), - filteredList(name: "filteredList"), - genre(name: "genre"), - artist(name: "artist"), - nextUp(name: ""), - formerNextUp(name: ""), - downloads(name: ""), - unknown(name: ""); - - const QueueItemSourceType({ - required this.name, - }); +@HiveType(typeId: 50) +enum FinampPlaybackOrder { + @HiveField(0) + shuffled, + @HiveField(1) + linear; +} - final String name; +@HiveType(typeId: 51) +enum FinampLoopMode { + @HiveField(0) + none, + @HiveField(1) + one, + @HiveField(2) + all; } +@HiveType(typeId: 52) +enum QueueItemSourceType { + @HiveField(0) + album, + @HiveField(1) + playlist, + @HiveField(2) + songMix, + @HiveField(3) + artistMix, + @HiveField(4) + albumMix, + @HiveField(5) + favorites, + @HiveField(6) + songs, + @HiveField(7) + filteredList, + @HiveField(8) + genre, + @HiveField(9) + artist, + @HiveField(10) + nextUp, + @HiveField(11) + formerNextUp, + @HiveField(12) + downloads, + @HiveField(13) + unknown; +} + +@HiveType(typeId: 53) enum QueueItemQueueType { + @HiveField(0) previousTracks, + @HiveField(1) currentTrack, + @HiveField(2) nextUp, + @HiveField(3) queue; } +@HiveType(typeId: 54) class QueueItemSource { QueueItemSource({ required this.type, @@ -607,16 +646,25 @@ class QueueItemSource { BaseItemDto? item; } +@HiveType(typeId: 55) enum QueueItemSourceNameType { + @HiveField(0) preTranslated, + @HiveField(1) yourLikes, + @HiveField(2) shuffleAll, + @HiveField(3) mix, + @HiveField(4) instantMix, + @HiveField(5) nextUp, + @HiveField(6) tracksFormerNextUp, } +@HiveType(typeId: 56) class QueueItemSourceName { const QueueItemSourceName({ required this.type, @@ -624,8 +672,11 @@ class QueueItemSourceName { this.localizationParameter, // used if only part of the name is translated }); + @HiveField(0) final QueueItemSourceNameType type; + @HiveField(1) final String? pretranslatedName; + @HiveField(2) final String? localizationParameter; getLocalized(BuildContext context) { @@ -648,8 +699,9 @@ class QueueItemSourceName { } } -class QueueItem { - QueueItem({ +@HiveType(typeId: 57) +class FinampQueueItem { + FinampQueueItem({ required this.item, required this.source, this.type = QueueItemQueueType.queue, @@ -670,8 +722,9 @@ class QueueItem { QueueItemQueueType type; } -class QueueOrder { - QueueOrder({ +@HiveType(typeId: 58) +class FinampQueueOrder { + FinampQueueOrder({ required this.items, required this.originalSource, required this.linearOrder, @@ -679,7 +732,7 @@ class QueueOrder { }); @HiveField(0) - List items; + List items; @HiveField(1) QueueItemSource originalSource; @@ -695,8 +748,9 @@ class QueueOrder { List shuffledOrder; } -class QueueInfo { - QueueInfo({ +@HiveType(typeId: 59) +class FinampQueueInfo { + FinampQueueInfo({ required this.previousTracks, required this.currentTrack, required this.nextUp, @@ -705,30 +759,31 @@ class QueueInfo { }); @HiveField(0) - List previousTracks; + List previousTracks; @HiveField(1) - QueueItem? currentTrack; + FinampQueueItem? currentTrack; @HiveField(2) - List nextUp; + List nextUp; @HiveField(3) - List queue; + List queue; @HiveField(4) QueueItemSource source; } -class HistoryItem { - HistoryItem({ +@HiveType(typeId: 60) +class FinampHistoryItem { + FinampHistoryItem({ required this.item, required this.startTime, this.endTime, }); @HiveField(0) - QueueItem item; + FinampQueueItem item; @HiveField(1) DateTime startTime; diff --git a/lib/models/finamp_models.g.dart b/lib/models/finamp_models.g.dart index a9935509e..db62efe93 100644 --- a/lib/models/finamp_models.g.dart +++ b/lib/models/finamp_models.g.dart @@ -92,20 +92,23 @@ class FinampSettingsAdapter extends TypeAdapter { fields[16] == null ? true : fields[16] as bool, hideSongArtistsIfSameAsAlbumArtists: fields[17] == null ? true : fields[17] as bool, - bufferDurationSeconds: fields[18] == null ? 50 : fields[18] as int, + bufferDurationSeconds: fields[18] == null ? 600 : fields[18] as int, tabSortBy: fields[20] == null ? {} : (fields[20] as Map).cast(), tabSortOrder: fields[21] == null ? {} : (fields[21] as Map).cast(), + loopMode: fields[22] == null + ? FinampLoopMode.all + : fields[22] as FinampLoopMode, )..disableGesture = fields[19] == null ? false : fields[19] as bool; } @override void write(BinaryWriter writer, FinampSettings obj) { writer - ..writeByte(22) + ..writeByte(23) ..writeByte(0) ..write(obj.isOffline) ..writeByte(1) @@ -149,7 +152,9 @@ class FinampSettingsAdapter extends TypeAdapter { ..writeByte(20) ..write(obj.tabSortBy) ..writeByte(21) - ..write(obj.tabSortOrder); + ..write(obj.tabSortOrder) + ..writeByte(22) + ..write(obj.loopMode); } @override @@ -353,6 +358,260 @@ class DownloadedImageAdapter extends TypeAdapter { typeId == other.typeId; } +class QueueItemSourceAdapter extends TypeAdapter { + @override + final int typeId = 54; + + @override + QueueItemSource read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return QueueItemSource( + type: fields[0] as QueueItemSourceType, + name: fields[1] as QueueItemSourceName, + id: fields[2] as String, + item: fields[3] as BaseItemDto?, + ); + } + + @override + void write(BinaryWriter writer, QueueItemSource obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.type) + ..writeByte(1) + ..write(obj.name) + ..writeByte(2) + ..write(obj.id) + ..writeByte(3) + ..write(obj.item); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is QueueItemSourceAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class QueueItemSourceNameAdapter extends TypeAdapter { + @override + final int typeId = 56; + + @override + QueueItemSourceName read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return QueueItemSourceName( + type: fields[0] as QueueItemSourceNameType, + pretranslatedName: fields[1] as String?, + localizationParameter: fields[2] as String?, + ); + } + + @override + void write(BinaryWriter writer, QueueItemSourceName obj) { + writer + ..writeByte(3) + ..writeByte(0) + ..write(obj.type) + ..writeByte(1) + ..write(obj.pretranslatedName) + ..writeByte(2) + ..write(obj.localizationParameter); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is QueueItemSourceNameAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class FinampQueueItemAdapter extends TypeAdapter { + @override + final int typeId = 57; + + @override + FinampQueueItem read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return FinampQueueItem( + item: fields[1] as MediaItem, + source: fields[2] as QueueItemSource, + type: fields[3] as QueueItemQueueType, + )..id = fields[0] as String; + } + + @override + void write(BinaryWriter writer, FinampQueueItem obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.item) + ..writeByte(2) + ..write(obj.source) + ..writeByte(3) + ..write(obj.type); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FinampQueueItemAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class FinampQueueOrderAdapter extends TypeAdapter { + @override + final int typeId = 58; + + @override + FinampQueueOrder read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return FinampQueueOrder( + items: (fields[0] as List).cast(), + originalSource: fields[1] as QueueItemSource, + linearOrder: (fields[2] as List).cast(), + shuffledOrder: (fields[3] as List).cast(), + ); + } + + @override + void write(BinaryWriter writer, FinampQueueOrder obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.items) + ..writeByte(1) + ..write(obj.originalSource) + ..writeByte(2) + ..write(obj.linearOrder) + ..writeByte(3) + ..write(obj.shuffledOrder); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FinampQueueOrderAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class FinampQueueInfoAdapter extends TypeAdapter { + @override + final int typeId = 59; + + @override + FinampQueueInfo read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return FinampQueueInfo( + previousTracks: (fields[0] as List).cast(), + currentTrack: fields[1] as FinampQueueItem?, + nextUp: (fields[2] as List).cast(), + queue: (fields[3] as List).cast(), + source: fields[4] as QueueItemSource, + ); + } + + @override + void write(BinaryWriter writer, FinampQueueInfo obj) { + writer + ..writeByte(5) + ..writeByte(0) + ..write(obj.previousTracks) + ..writeByte(1) + ..write(obj.currentTrack) + ..writeByte(2) + ..write(obj.nextUp) + ..writeByte(3) + ..write(obj.queue) + ..writeByte(4) + ..write(obj.source); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FinampQueueInfoAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class FinampHistoryItemAdapter extends TypeAdapter { + @override + final int typeId = 60; + + @override + FinampHistoryItem read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return FinampHistoryItem( + item: fields[0] as FinampQueueItem, + startTime: fields[1] as DateTime, + endTime: fields[2] as DateTime?, + ); + } + + @override + void write(BinaryWriter writer, FinampHistoryItem obj) { + writer + ..writeByte(3) + ..writeByte(0) + ..write(obj.item) + ..writeByte(1) + ..write(obj.startTime) + ..writeByte(2) + ..write(obj.endTime); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FinampHistoryItemAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + class TabContentTypeAdapter extends TypeAdapter { @override final int typeId = 36; @@ -446,6 +705,302 @@ class ContentViewTypeAdapter extends TypeAdapter { typeId == other.typeId; } +class FinampPlaybackOrderAdapter extends TypeAdapter { + @override + final int typeId = 50; + + @override + FinampPlaybackOrder read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return FinampPlaybackOrder.shuffled; + case 1: + return FinampPlaybackOrder.linear; + default: + return FinampPlaybackOrder.shuffled; + } + } + + @override + void write(BinaryWriter writer, FinampPlaybackOrder obj) { + switch (obj) { + case FinampPlaybackOrder.shuffled: + writer.writeByte(0); + break; + case FinampPlaybackOrder.linear: + writer.writeByte(1); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FinampPlaybackOrderAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class FinampLoopModeAdapter extends TypeAdapter { + @override + final int typeId = 51; + + @override + FinampLoopMode read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return FinampLoopMode.none; + case 1: + return FinampLoopMode.one; + case 2: + return FinampLoopMode.all; + default: + return FinampLoopMode.none; + } + } + + @override + void write(BinaryWriter writer, FinampLoopMode obj) { + switch (obj) { + case FinampLoopMode.none: + writer.writeByte(0); + break; + case FinampLoopMode.one: + writer.writeByte(1); + break; + case FinampLoopMode.all: + writer.writeByte(2); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FinampLoopModeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class QueueItemSourceTypeAdapter extends TypeAdapter { + @override + final int typeId = 52; + + @override + QueueItemSourceType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return QueueItemSourceType.album; + case 1: + return QueueItemSourceType.playlist; + case 2: + return QueueItemSourceType.songMix; + case 3: + return QueueItemSourceType.artistMix; + case 4: + return QueueItemSourceType.albumMix; + case 5: + return QueueItemSourceType.favorites; + case 6: + return QueueItemSourceType.songs; + case 7: + return QueueItemSourceType.filteredList; + case 8: + return QueueItemSourceType.genre; + case 9: + return QueueItemSourceType.artist; + case 10: + return QueueItemSourceType.nextUp; + case 11: + return QueueItemSourceType.formerNextUp; + case 12: + return QueueItemSourceType.downloads; + case 13: + return QueueItemSourceType.unknown; + default: + return QueueItemSourceType.album; + } + } + + @override + void write(BinaryWriter writer, QueueItemSourceType obj) { + switch (obj) { + case QueueItemSourceType.album: + writer.writeByte(0); + break; + case QueueItemSourceType.playlist: + writer.writeByte(1); + break; + case QueueItemSourceType.songMix: + writer.writeByte(2); + break; + case QueueItemSourceType.artistMix: + writer.writeByte(3); + break; + case QueueItemSourceType.albumMix: + writer.writeByte(4); + break; + case QueueItemSourceType.favorites: + writer.writeByte(5); + break; + case QueueItemSourceType.songs: + writer.writeByte(6); + break; + case QueueItemSourceType.filteredList: + writer.writeByte(7); + break; + case QueueItemSourceType.genre: + writer.writeByte(8); + break; + case QueueItemSourceType.artist: + writer.writeByte(9); + break; + case QueueItemSourceType.nextUp: + writer.writeByte(10); + break; + case QueueItemSourceType.formerNextUp: + writer.writeByte(11); + break; + case QueueItemSourceType.downloads: + writer.writeByte(12); + break; + case QueueItemSourceType.unknown: + writer.writeByte(13); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is QueueItemSourceTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class QueueItemQueueTypeAdapter extends TypeAdapter { + @override + final int typeId = 53; + + @override + QueueItemQueueType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return QueueItemQueueType.previousTracks; + case 1: + return QueueItemQueueType.currentTrack; + case 2: + return QueueItemQueueType.nextUp; + case 3: + return QueueItemQueueType.queue; + default: + return QueueItemQueueType.previousTracks; + } + } + + @override + void write(BinaryWriter writer, QueueItemQueueType obj) { + switch (obj) { + case QueueItemQueueType.previousTracks: + writer.writeByte(0); + break; + case QueueItemQueueType.currentTrack: + writer.writeByte(1); + break; + case QueueItemQueueType.nextUp: + writer.writeByte(2); + break; + case QueueItemQueueType.queue: + writer.writeByte(3); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is QueueItemQueueTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class QueueItemSourceNameTypeAdapter + extends TypeAdapter { + @override + final int typeId = 55; + + @override + QueueItemSourceNameType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return QueueItemSourceNameType.preTranslated; + case 1: + return QueueItemSourceNameType.yourLikes; + case 2: + return QueueItemSourceNameType.shuffleAll; + case 3: + return QueueItemSourceNameType.mix; + case 4: + return QueueItemSourceNameType.instantMix; + case 5: + return QueueItemSourceNameType.nextUp; + case 6: + return QueueItemSourceNameType.tracksFormerNextUp; + default: + return QueueItemSourceNameType.preTranslated; + } + } + + @override + void write(BinaryWriter writer, QueueItemSourceNameType obj) { + switch (obj) { + case QueueItemSourceNameType.preTranslated: + writer.writeByte(0); + break; + case QueueItemSourceNameType.yourLikes: + writer.writeByte(1); + break; + case QueueItemSourceNameType.shuffleAll: + writer.writeByte(2); + break; + case QueueItemSourceNameType.mix: + writer.writeByte(3); + break; + case QueueItemSourceNameType.instantMix: + writer.writeByte(4); + break; + case QueueItemSourceNameType.nextUp: + writer.writeByte(5); + break; + case QueueItemSourceNameType.tracksFormerNextUp: + writer.writeByte(6); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is QueueItemSourceNameTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** diff --git a/lib/services/audio_service_helper.dart b/lib/services/audio_service_helper.dart index 3d5a67be5..7a6c4b0bd 100644 --- a/lib/services/audio_service_helper.dart +++ b/lib/services/audio_service_helper.dart @@ -63,7 +63,7 @@ class AudioServiceHelper { ), id: "shuffleAll", ), - order: PlaybackOrder.shuffled, + order: FinampPlaybackOrder.shuffled, ); } } @@ -85,7 +85,7 @@ class AudioServiceHelper { ), id: item.id ), - order: PlaybackOrder.linear, // instant mixes should have their order determined by the server, especially to make sure the first item is the one that the mix is based off of + order: FinampPlaybackOrder.linear, // instant mixes should have their order determined by the server, especially to make sure the first item is the one that the mix is based off of ); } } catch (e) { @@ -109,7 +109,7 @@ class AudioServiceHelper { id: artists.first.id, item: artists.first, ), - order: PlaybackOrder.linear, // instant mixes should have their order determined by the server, especially to make sure the first item is the one that the mix is based off of + order: FinampPlaybackOrder.linear, // instant mixes should have their order determined by the server, especially to make sure the first item is the one that the mix is based off of ); _jellyfinApiHelper.clearArtistMixBuilderList(); } @@ -134,7 +134,7 @@ class AudioServiceHelper { id: albums.first.id, item: albums.first, ), - order: PlaybackOrder.linear, // instant mixes should have their order determined by the server, especially to make sure the first item is the one that the mix is based off of + order: FinampPlaybackOrder.linear, // instant mixes should have their order determined by the server, especially to make sure the first item is the one that the mix is based off of ); _jellyfinApiHelper.clearAlbumMixBuilderList(); } diff --git a/lib/services/finamp_settings_helper.dart b/lib/services/finamp_settings_helper.dart index 696a70dbc..b7488227d 100644 --- a/lib/services/finamp_settings_helper.dart +++ b/lib/services/finamp_settings_helper.dart @@ -1,3 +1,4 @@ +import 'package:finamp/services/queue_service.dart'; import 'package:flutter/foundation.dart'; import 'package:hive_flutter/hive_flutter.dart'; @@ -189,4 +190,13 @@ class FinampSettingsHelper { Hive.box("FinampSettings") .put("FinampSettings", finampSettingsTemp); } + + /// Set the loopMode property + static void setLoopMode(FinampLoopMode loopMode) { + FinampSettings finampSettingsTemp = finampSettings; + finampSettingsTemp.loopMode = loopMode; + print("SETTING LOOP MODE TO ${finampSettingsTemp.loopMode}"); + Hive.box("FinampSettings") + .put("FinampSettings", finampSettingsTemp); + } } diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart index 43d52ab8f..7b8c968e3 100644 --- a/lib/services/playback_history_service.dart +++ b/lib/services/playback_history_service.dart @@ -24,14 +24,14 @@ class PlaybackHistoryService { // internal state - final List _history = []; // contains **all** items that have been played, including "next up" - HistoryItem? _currentTrack; // the currently playing track + final List _history = []; // contains **all** items that have been played, including "next up" + FinampHistoryItem? _currentTrack; // the currently playing track PlaybackState? _previousPlaybackState; final bool _reportQueueToServer = true; DateTime _lastPositionUpdate = DateTime.now(); - final _historyStream = BehaviorSubject>.seeded( + final _historyStream = BehaviorSubject>.seeded( List.empty(growable: true), ); @@ -117,11 +117,11 @@ class PlaybackHistoryService { } get history => _history; - BehaviorSubject> get historyStream => _historyStream; + BehaviorSubject> get historyStream => _historyStream; /// method that converts history into a list grouped by date - List>> getHistoryGroupedByDateOrHourDynamic() { - byDateGroupingConstructor(HistoryItem element) { + List>> getHistoryGroupedByDateOrHourDynamic() { + byDateGroupingConstructor(FinampHistoryItem element) { final now = DateTime.now(); if (now.year == element.startTime.year && now.month == element.startTime.month && now.day == element.startTime.day) { // group by hour @@ -145,8 +145,8 @@ class PlaybackHistoryService { } /// method that converts history into a list grouped by date - List>> getHistoryGroupedByDate() { - byDateGroupingConstructor(HistoryItem element) { + List>> getHistoryGroupedByDate() { + byDateGroupingConstructor(FinampHistoryItem element) { return DateTime( element.startTime.year, element.startTime.month, @@ -159,8 +159,8 @@ class PlaybackHistoryService { } /// method that converts history into a list grouped by hour - List>> getHistoryGroupedByHour() { - byHourGroupingConstructor(HistoryItem element) { + List>> getHistoryGroupedByHour() { + byHourGroupingConstructor(FinampHistoryItem element) { return DateTime( element.startTime.year, element.startTime.month, @@ -174,10 +174,10 @@ class PlaybackHistoryService { } /// method that converts history into a list grouped by a custom date constructor controlling the granularity of the grouping - List>> getHistoryGrouped(DateTime Function (HistoryItem) dateTimeConstructor) { - final groupedHistory = >>[]; + List>> getHistoryGrouped(DateTime Function (FinampHistoryItem) dateTimeConstructor) { + final groupedHistory = >>[]; - final groupedHistoryMap = >{}; + final groupedHistoryMap = >{}; for (var element in _history) { final date = dateTimeConstructor(element); @@ -199,7 +199,7 @@ class PlaybackHistoryService { return groupedHistory; } - void updateCurrentTrack(QueueItem? currentTrack) { + void updateCurrentTrack(FinampQueueItem? currentTrack) { if (currentTrack == null || currentTrack == _currentTrack?.item || currentTrack.item.id == "" || currentTrack.id == _currentTrack?.item.id) { // current track hasn't changed @@ -213,7 +213,7 @@ class PlaybackHistoryService { } // if there is a **current** track - _currentTrack = HistoryItem( + _currentTrack = FinampHistoryItem( item: currentTrack, startTime: DateTime.now(), ); @@ -225,9 +225,9 @@ class PlaybackHistoryService { /// Report track changes to the Jellyfin Server if the user is not offline. Future onTrackChanged( - QueueItem currentItem, + FinampQueueItem currentItem, PlaybackState currentState, - QueueItem? previousItem, + FinampQueueItem? previousItem, PlaybackState? previousState, bool skippingForward, ) async { @@ -272,7 +272,7 @@ class PlaybackHistoryService { /// Report track changes to the Jellyfin Server if the user is not offline. Future onPlaybackStateChanged( - QueueItem currentItem, + FinampQueueItem currentItem, PlaybackState currentState, ) async { if (FinampSettingsHelper.finampSettings.isOffline) { @@ -425,13 +425,13 @@ class PlaybackHistoryService { } } - String _toJellyfinRepeatMode(LoopMode loopMode) { + String _toJellyfinRepeatMode(FinampLoopMode loopMode) { switch (loopMode) { - case LoopMode.all: + case FinampLoopMode.all: return "RepeatAll"; - case LoopMode.one: + case FinampLoopMode.one: return "RepeatOne"; - case LoopMode.none: + case FinampLoopMode.none: return "RepeatNone"; } } diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index e86d1deb1..6657d2d15 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -17,10 +17,6 @@ import 'finamp_settings_helper.dart'; import 'downloads_helper.dart'; import 'music_player_background_task.dart'; -enum PlaybackOrder { shuffled, linear } - -enum LoopMode { none, one, all } - /// A track queueing service for Finamp. class QueueService { final _jellyfinApiHelper = GetIt.instance(); @@ -30,13 +26,13 @@ class QueueService { final _queueServiceLogger = Logger("QueueService"); // internal state - final List _queuePreviousTracks = + final List _queuePreviousTracks = []; // contains **all** items that have been played, including "next up" - QueueItem? _currentTrack; // the currently playing track - final List _queueNextUp = + FinampQueueItem? _currentTrack; // the currently playing track + final List _queueNextUp = []; // a temporary queue that gets appended to if the user taps "next up" - final List _queue = []; // contains all regular queue items - QueueOrder _order = QueueOrder( + final List _queue = []; // contains all regular queue items + FinampQueueOrder _order = FinampQueueOrder( items: [], originalSource: QueueItemSource( id: "", @@ -46,10 +42,10 @@ class QueueService { linearOrder: [], shuffledOrder: []); // contains all items that were at some point added to the regular queue, as well as their order when shuffle is enabled and disabled. This is used to loop the original queue once the end has been reached and "loop all" is enabled, **excluding** "next up" items and keeping the playback order. - PlaybackOrder _playbackOrder = PlaybackOrder.linear; - LoopMode _loopMode = LoopMode.none; + FinampPlaybackOrder _playbackOrder = FinampPlaybackOrder.linear; + FinampLoopMode _loopMode = FinampLoopMode.none; - final _currentTrackStream = BehaviorSubject.seeded(QueueItem( + final _currentTrackStream = BehaviorSubject.seeded(FinampQueueItem( item: const MediaItem( id: "", title: "No track playing", @@ -60,9 +56,9 @@ class QueueService { name: const QueueItemSourceName( type: QueueItemSourceNameType.preTranslated), type: QueueItemSourceType.unknown))); - final _queueStream = BehaviorSubject.seeded(QueueInfo( + final _queueStream = BehaviorSubject.seeded(FinampQueueInfo( previousTracks: [], - currentTrack: QueueItem( + currentTrack: FinampQueueItem( item: const MediaItem( id: "", title: "No track playing", @@ -83,8 +79,8 @@ class QueueService { )); final _playbackOrderStream = - BehaviorSubject.seeded(PlaybackOrder.linear); - final _loopModeStream = BehaviorSubject.seeded(LoopMode.none); + BehaviorSubject.seeded(FinampPlaybackOrder.linear); + final _loopModeStream = BehaviorSubject.seeded(FinampLoopMode.none); // external queue state @@ -98,6 +94,11 @@ class QueueService { QueueService() { // _queueServiceLogger.level = Level.OFF; + final finampSettings = FinampSettingsHelper.finampSettings; + + loopMode = finampSettings.loopMode; + _queueServiceLogger.info("Restored loop mode to $loopMode from settings"); + _shuffleOrder = NextUpShuffleOrder(queueService: this); _queueAudioSource = ConcatenatingAudioSource( children: [], @@ -111,7 +112,7 @@ class QueueService { _queueAudioSourceIndex = event.queueIndex ?? 0; if (previousIndex != _queueAudioSourceIndex) { - int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && + int adjustedQueueIndex = (playbackOrder == FinampPlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; @@ -130,11 +131,11 @@ class QueueService { } void _queueFromConcatenatingAudioSource() { - List allTracks = _audioHandler.effectiveSequence - ?.map((e) => e.tag as QueueItem) + List allTracks = _audioHandler.effectiveSequence + ?.map((e) => e.tag as FinampQueueItem) .toList() ?? []; - int adjustedQueueIndex = (playbackOrder == PlaybackOrder.shuffled && + int adjustedQueueIndex = (playbackOrder == FinampPlaybackOrder.shuffled && _queueAudioSource.shuffleIndices.isNotEmpty) ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) : _queueAudioSourceIndex; @@ -229,7 +230,7 @@ class QueueService { Future startPlayback({ required List items, required QueueItemSource source, - PlaybackOrder? order, + FinampPlaybackOrder? order, int startingIndex = 0, }) async { // _initialQueue = list; // save original PlaybackList for looping/restarting and meta info @@ -238,7 +239,7 @@ class QueueService { playbackOrder = order; } - if (_playbackOrder == PlaybackOrder.shuffled) { + if (_playbackOrder == FinampPlaybackOrder.shuffled) { items.shuffle(); } await _replaceWholeQueue( @@ -265,14 +266,14 @@ class QueueService { _queueNextUp.clear(); _currentTrack = null; - List newItems = []; + List newItems = []; List newLinearOrder = []; List newShuffledOrder; for (int i = 0; i < itemList.length; i++) { jellyfin_models.BaseItemDto item = itemList[i]; try { MediaItem mediaItem = await _generateMediaItem(item); - newItems.add(QueueItem( + newItems.add(FinampQueueItem( item: mediaItem, source: source, type: i == 0 @@ -300,7 +301,7 @@ class QueueService { // set first item in queue _queueAudioSourceIndex = initialIndex; - if (_playbackOrder == PlaybackOrder.shuffled) { + if (_playbackOrder == FinampPlaybackOrder.shuffled) { _queueAudioSourceIndex = _queueAudioSource.shuffleIndices[initialIndex]; } _audioHandler.setNextInitialIndex(_queueAudioSourceIndex); @@ -308,7 +309,7 @@ class QueueService { newShuffledOrder = List.from(_queueAudioSource.shuffleIndices); - _order = QueueOrder( + _order = FinampQueueOrder( items: newItems, originalSource: source, linearOrder: newLinearOrder, @@ -344,7 +345,7 @@ class QueueService { Future addToQueue( jellyfin_models.BaseItemDto item, QueueItemSource source) async { try { - QueueItem queueItem = QueueItem( + FinampQueueItem queueItem = FinampQueueItem( item: await _generateMediaItem(item), source: source, type: QueueItemQueueType.queue, @@ -367,9 +368,9 @@ class QueueService { QueueItemSource? source, }) async { try { - List queueItems = []; + List queueItems = []; for (final item in items) { - queueItems.add(QueueItem( + queueItems.add(FinampQueueItem( item: await _generateMediaItem(item), source: source ?? QueueItemSource( @@ -400,9 +401,9 @@ class QueueService { QueueItemSource? source, }) async { try { - List queueItems = []; + List queueItems = []; for (final item in items) { - queueItems.add(QueueItem( + queueItems.add(FinampQueueItem( item: await _generateMediaItem(item), source: source ?? QueueItemSource( @@ -437,7 +438,7 @@ class QueueService { } Future removeAtOffset(int offset) async { - final index = _playbackOrder == PlaybackOrder.shuffled + final index = _playbackOrder == FinampPlaybackOrder.shuffled ? _queueAudioSource.shuffleIndices[ _queueAudioSource.shuffleIndices.indexOf((_queueAudioSourceIndex)) + offset] @@ -474,8 +475,8 @@ class QueueService { _queueFromConcatenatingAudioSource(); // update internal queues } - QueueInfo getQueue() { - return QueueInfo( + FinampQueueInfo getQueue() { + return FinampQueueInfo( previousTracks: _queuePreviousTracks, currentTrack: _currentTrack, queue: _queue, @@ -484,7 +485,7 @@ class QueueService { ); } - BehaviorSubject getQueueStream() { + BehaviorSubject getQueueStream() { return _queueStream; } @@ -494,8 +495,8 @@ class QueueService { /// Returns the next [amount] QueueItems from Next Up and the regular queue. /// The length of the returned list may be less than [amount] if there are not enough items in the queue - List getNextXTracksInQueue(int amount) { - List nextTracks = []; + List getNextXTracksInQueue(int amount) { + List nextTracks = []; if (_queueNextUp.isNotEmpty) { nextTracks .addAll(_queueNextUp.sublist(0, min(amount, _queueNextUp.length))); @@ -507,46 +508,50 @@ class QueueService { return nextTracks; } - BehaviorSubject getPlaybackOrderStream() { + BehaviorSubject getPlaybackOrderStream() { return _playbackOrderStream; } - BehaviorSubject getLoopModeStream() { + BehaviorSubject getLoopModeStream() { return _loopModeStream; } - BehaviorSubject getCurrentTrackStream() { + BehaviorSubject getCurrentTrackStream() { return _currentTrackStream; } - QueueItem? getCurrentTrack() { + FinampQueueItem? getCurrentTrack() { return _currentTrack; } - set loopMode(LoopMode mode) { + set loopMode(FinampLoopMode mode) { _loopMode = mode; _loopModeStream.add(mode); - if (mode == LoopMode.one) { + if (mode == FinampLoopMode.one) { _audioHandler.setRepeatMode(AudioServiceRepeatMode.one); - } else if (mode == LoopMode.all) { + } else if (mode == FinampLoopMode.all) { _audioHandler.setRepeatMode(AudioServiceRepeatMode.all); } else { _audioHandler.setRepeatMode(AudioServiceRepeatMode.none); } + + FinampSettingsHelper.setLoopMode(loopMode); + _queueServiceLogger.fine("Loop mode set to ${FinampSettingsHelper.finampSettings.loopMode}"); + } - LoopMode get loopMode => _loopMode; + FinampLoopMode get loopMode => _loopMode; - set playbackOrder(PlaybackOrder order) { + set playbackOrder(FinampPlaybackOrder order) { _playbackOrder = order; _queueServiceLogger.fine("Playback order set to $order"); _playbackOrderStream.add(order); // update queue accordingly and generate new shuffled order if necessary - if (_playbackOrder == PlaybackOrder.shuffled) { + if (_playbackOrder == FinampPlaybackOrder.shuffled) { _audioHandler .setShuffleMode(AudioServiceShuffleMode.all) .then((value) => _queueFromConcatenatingAudioSource()); @@ -557,23 +562,23 @@ class QueueService { } } - PlaybackOrder get playbackOrder => _playbackOrder; + FinampPlaybackOrder get playbackOrder => _playbackOrder; void togglePlaybackOrder() { - if (_playbackOrder == PlaybackOrder.shuffled) { - playbackOrder = PlaybackOrder.linear; + if (_playbackOrder == FinampPlaybackOrder.shuffled) { + playbackOrder = FinampPlaybackOrder.linear; } else { - playbackOrder = PlaybackOrder.shuffled; + playbackOrder = FinampPlaybackOrder.shuffled; } } void toggleLoopMode() { - if (_loopMode == LoopMode.all) { - loopMode = LoopMode.one; - } else if (_loopMode == LoopMode.one) { - loopMode = LoopMode.none; + if (_loopMode == FinampLoopMode.all) { + loopMode = FinampLoopMode.one; + } else if (_loopMode == FinampLoopMode.one) { + loopMode = FinampLoopMode.none; } else { - loopMode = LoopMode.all; + loopMode = FinampLoopMode.all; } } @@ -582,16 +587,16 @@ class QueueService { void _logQueues({String message = ""}) { // generate string for `_queue` String queueString = ""; - for (QueueItem queueItem in _queuePreviousTracks) { + for (FinampQueueItem queueItem in _queuePreviousTracks) { queueString += "${queueItem.item.title}, "; } queueString += "[[${_currentTrack?.item.title}]], "; queueString += "{"; - for (QueueItem queueItem in _queueNextUp) { + for (FinampQueueItem queueItem in _queueNextUp) { queueString += "${queueItem.item.title}, "; } queueString += "} "; - for (QueueItem queueItem in _queue) { + for (FinampQueueItem queueItem in _queue) { queueString += "${queueItem.item.title}, "; } @@ -643,7 +648,7 @@ class QueueService { /// Syncs the list of MediaItems (_queue) with the internal queue of the player. /// Called by onAddQueueItem and onUpdateQueue. - Future _queueItemToAudioSource(QueueItem queueItem) async { + Future _queueItemToAudioSource(FinampQueueItem queueItem) async { if (queueItem.item.extras!["downloadedSongJson"] == null) { // If DownloadedSong wasn't passed, we assume that the item is not // downloaded. @@ -754,7 +759,7 @@ class NextUpShuffleOrder extends ShuffleOrder { indices.clear(); _queueService!._queueFromConcatenatingAudioSource(); - QueueInfo queueInfo = _queueService!.getQueue(); + FinampQueueInfo queueInfo = _queueService!.getQueue(); indices = List.generate( queueInfo.previousTracks.length + 1 + From 0c3aa16e217f41848bb2fe6a776bcff78a08f705 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 12 Oct 2023 20:13:08 +0200 Subject: [PATCH 099/130] add new queue sources & some fixes - fixed playlists being added to Next Up as "albums" - fixed sorting when adding lists to Next Up from the long-press menu --- ...bum_screen_content_flexible_space_bar.dart | 8 +++--- lib/components/MusicScreen/album_item.dart | 26 ++++++++++++------- .../player_screen_appbar_title.dart | 5 +++- lib/l10n/app_en.arb | 2 +- lib/models/finamp_models.dart | 12 ++++++--- lib/models/finamp_models.g.dart | 4 +-- lib/services/audio_service_helper.dart | 2 +- lib/services/queue_service.dart | 5 ++-- 8 files changed, 39 insertions(+), 25 deletions(-) diff --git a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart index 91b5dd343..db1cb978f 100644 --- a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart +++ b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart @@ -59,7 +59,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { queueService.addToNextUp( items: items, source: QueueItemSource( - type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, + type: isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, @@ -77,7 +77,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { queueService.addNext( items: items, source: QueueItemSource( - type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, + type: isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, @@ -97,7 +97,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { queueService.addToNextUp( items: clonedItems, source: QueueItemSource( - type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, + type: isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, @@ -117,7 +117,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { queueService.addNext( items: clonedItems, source: QueueItemSource( - type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, + type: isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, diff --git a/lib/components/MusicScreen/album_item.dart b/lib/components/MusicScreen/album_item.dart index 9ea86e5c2..23dbcc547 100644 --- a/lib/components/MusicScreen/album_item.dart +++ b/lib/components/MusicScreen/album_item.dart @@ -253,8 +253,10 @@ class _AlbumItemState extends State { case _AlbumListTileMenuItems.playNext: try { List? albumTracks = await jellyfinApiHelper.getItems( - isGenres: false, parentItem: mutableAlbum, + isGenres: false, + sortBy: "ParentIndexNumber,IndexNumber,SortName", + includeItemTypes: "Audio", ); if (albumTracks == null) { @@ -269,7 +271,7 @@ class _AlbumItemState extends State { _queueService.addNext( items: albumTracks, source: QueueItemSource( - type: QueueItemSourceType.album, + type: widget.isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: mutableAlbum.name ?? local.placeholderSource), id: mutableAlbum.id, item: mutableAlbum, @@ -290,8 +292,10 @@ class _AlbumItemState extends State { case _AlbumListTileMenuItems.addToNextUp: try { List? albumTracks = await jellyfinApiHelper.getItems( - isGenres: false, parentItem: mutableAlbum, + isGenres: false, + sortBy: "ParentIndexNumber,IndexNumber,SortName", + includeItemTypes: "Audio", ); if (albumTracks == null) { @@ -306,7 +310,7 @@ class _AlbumItemState extends State { _queueService.addToNextUp( items: albumTracks, source: QueueItemSource( - type: QueueItemSourceType.album, + type: widget.isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: mutableAlbum.name ?? local.placeholderSource), id: mutableAlbum.id, item: mutableAlbum, @@ -327,9 +331,10 @@ class _AlbumItemState extends State { case _AlbumListTileMenuItems.shuffleNext: try { List? albumTracks = await jellyfinApiHelper.getItems( - isGenres: false, parentItem: mutableAlbum, - sortOrder: "Random", + isGenres: false, + sortBy: "Random", + includeItemTypes: "Audio", ); if (albumTracks == null) { @@ -344,7 +349,7 @@ class _AlbumItemState extends State { _queueService.addNext( items: albumTracks, source: QueueItemSource( - type: QueueItemSourceType.album, + type: widget.isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: mutableAlbum.name ?? local.placeholderSource), id: mutableAlbum.id, item: mutableAlbum, @@ -365,9 +370,10 @@ class _AlbumItemState extends State { case _AlbumListTileMenuItems.shuffleToNextUp: try { List? albumTracks = await jellyfinApiHelper.getItems( - isGenres: false, parentItem: mutableAlbum, - sortOrder: "Random", + isGenres: false, + sortBy: "Random", + includeItemTypes: "Audio", ); if (albumTracks == null) { @@ -382,7 +388,7 @@ class _AlbumItemState extends State { _queueService.addToNextUp( items: albumTracks, source: QueueItemSource( - type: QueueItemSourceType.album, + type: widget.isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: mutableAlbum.name ?? local.placeholderSource), id: mutableAlbum.id, item: mutableAlbum, diff --git a/lib/components/PlayerScreen/player_screen_appbar_title.dart b/lib/components/PlayerScreen/player_screen_appbar_title.dart index 18874e56e..babdf7417 100644 --- a/lib/components/PlayerScreen/player_screen_appbar_title.dart +++ b/lib/components/PlayerScreen/player_screen_appbar_title.dart @@ -72,15 +72,18 @@ void navigateToSource(BuildContext context, QueueItemSource source) async { switch (source.type) { case QueueItemSourceType.album: + case QueueItemSourceType.nextUpAlbum: Navigator.of(context).pushNamed(AlbumScreen.routeName, arguments: source.item); break; case QueueItemSourceType.artist: + case QueueItemSourceType.nextUpArtist: Navigator.of(context).pushNamed(ArtistScreen.routeName, arguments: source.item); break; case QueueItemSourceType.genre: Navigator.of(context).pushNamed(ArtistScreen.routeName, arguments: source.item); break; case QueueItemSourceType.playlist: + case QueueItemSourceType.nextUpPlaylist: Navigator.of(context).pushNamed(AlbumScreen.routeName, arguments: source.item); break; case QueueItemSourceType.albumMix: @@ -89,7 +92,7 @@ void navigateToSource(BuildContext context, QueueItemSource source) async { case QueueItemSourceType.artistMix: Navigator.of(context).pushNamed(ArtistScreen.routeName, arguments: source.item); break; - case QueueItemSourceType.songs: + case QueueItemSourceType.allSongs: Navigator.of(context).pushNamed(MusicScreen.routeName, arguments: FinampSettingsHelper.finampSettings.showTabs.entries .where((element) => element.value == true) .map((e) => e.key) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bbf098123..771a14608 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -568,7 +568,7 @@ "@tracksFormerNextUp": { "description": "Title for the queue source for tracks that were once added to the queue via the \"Next Up\" feature, but have since been played" }, - "playingFromType": "Playing From {source, select, album{Album} playlist{Playlist} songMix{Song Mix} artistMix{Artist Mix} albumMix{Album Mix} allSongs{All Songs} filteredList{Songs} genre{Genre} artist{Artist} favorites{Favorites} other{}}", + "playingFromType": "Playing From {source, select, album{Album} playlist{Playlist} songMix{Song Mix} artistMix{Artist Mix} albumMix{Album Mix} favorites{Favorites} allSongs{All Songs} filteredList{Songs} genre{Genre} artist{Artist} nextUpAlbum{Album in Next Up} nextUpPlaylist{Playlist in Next Up} nextUpArtist{Artist in Next Up} other{}}", "@playingFromType": { "description": "Prefix shown before the type of the main queue source at the top of the player screen. Example: \"Playing From Album\"", "placeholders": { diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index bc32c4811..2d29203ee 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -595,7 +595,7 @@ enum QueueItemSourceType { @HiveField(5) favorites, @HiveField(6) - songs, + allSongs, @HiveField(7) filteredList, @HiveField(8) @@ -605,10 +605,16 @@ enum QueueItemSourceType { @HiveField(10) nextUp, @HiveField(11) - formerNextUp, + nextUpAlbum, @HiveField(12) - downloads, + nextUpPlaylist, @HiveField(13) + nextUpArtist, + @HiveField(14) + formerNextUp, + @HiveField(15) + downloads, + @HiveField(16) unknown; } diff --git a/lib/models/finamp_models.g.dart b/lib/models/finamp_models.g.dart index db62efe93..4859314b5 100644 --- a/lib/models/finamp_models.g.dart +++ b/lib/models/finamp_models.g.dart @@ -808,7 +808,7 @@ class QueueItemSourceTypeAdapter extends TypeAdapter { case 5: return QueueItemSourceType.favorites; case 6: - return QueueItemSourceType.songs; + return QueueItemSourceType.allSongs; case 7: return QueueItemSourceType.filteredList; case 8: @@ -849,7 +849,7 @@ class QueueItemSourceTypeAdapter extends TypeAdapter { case QueueItemSourceType.favorites: writer.writeByte(5); break; - case QueueItemSourceType.songs: + case QueueItemSourceType.allSongs: writer.writeByte(6); break; case QueueItemSourceType.filteredList: diff --git a/lib/services/audio_service_helper.dart b/lib/services/audio_service_helper.dart index 7a6c4b0bd..06eeb060a 100644 --- a/lib/services/audio_service_helper.dart +++ b/lib/services/audio_service_helper.dart @@ -57,7 +57,7 @@ class AudioServiceHelper { await _queueService.startPlayback( items: items, source: QueueItemSource( - type: isFavourite ? QueueItemSourceType.favorites : QueueItemSourceType.songs, + type: isFavourite ? QueueItemSourceType.favorites : QueueItemSourceType.allSongs, name: QueueItemSourceName( type: isFavourite ? QueueItemSourceNameType.yourLikes : QueueItemSourceNameType.shuffleAll, ), diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 6657d2d15..2469724cc 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -155,8 +155,7 @@ class QueueService { for (int i = 0; i < allTracks.length; i++) { if (i < adjustedQueueIndex) { _queuePreviousTracks.add(allTracks[i]); - if (_queuePreviousTracks.last.source.type == - QueueItemSourceType.nextUp) { + if ([QueueItemSourceType.nextUp, QueueItemSourceType.nextUpAlbum, QueueItemSourceType.nextUpPlaylist, QueueItemSourceType.nextUpArtist].contains(_queuePreviousTracks.last.source.type)) { _queuePreviousTracks.last.source = QueueItemSource( type: QueueItemSourceType.formerNextUp, name: const QueueItemSourceName( @@ -168,7 +167,7 @@ class QueueService { _currentTrack = allTracks[i]; _currentTrack!.type = QueueItemQueueType.currentTrack; } else { - if (allTracks[i].type == QueueItemQueueType.currentTrack && allTracks[i].source.type == QueueItemSourceType.nextUp) { + if (allTracks[i].type == QueueItemQueueType.currentTrack && [QueueItemSourceType.nextUp, QueueItemSourceType.nextUpAlbum, QueueItemSourceType.nextUpPlaylist, QueueItemSourceType.nextUpArtist].contains(allTracks[i].source.type)) { _queue.add(allTracks[i]); _queue.last.type = QueueItemQueueType.queue; _queue.last.source = QueueItemSource( From db953ee9c10d10ad6a8a09ba6a91027bb4770e66 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 12 Oct 2023 20:16:41 +0200 Subject: [PATCH 100/130] reword "recently played" to "previous tracks" - this just works better with looping queues where after the loop the previous tracks are empty again - "recently played" is better suited for the playback history screen --- lib/l10n/app_en.arb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 771a14608..68d0bd579 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -484,9 +484,9 @@ "@bufferDurationSubtitle": {}, "language": "Language", "@language": {}, - "previousTracks": "Recently Played", + "previousTracks": "Previous Tracks", "@previousTracks": { - "description": "Description in the queue panel for the list of tracks that was previously played" + "description": "Description in the queue panel for the list of tracks that come before the current track in the queue. The tracks might not actually have been played (e.g. if the user skipped ahead to a specific track)." }, "nextUp": "Next Up", "@nextUp": { From 4124b65196da51c36875884f8c85f25300f33c45 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 12 Oct 2023 20:19:12 +0200 Subject: [PATCH 101/130] fix text thickness of "playing from" label --- lib/components/PlayerScreen/queue_list.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 7da953d36..0062ea78b 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -200,7 +200,10 @@ class _QueueListState extends State { delegate: QueueSectionHeader( title: Row( children: [ - Text("${AppLocalizations.of(context)!.playingFrom} "), + Text( + "${AppLocalizations.of(context)!.playingFrom} ", + style: const TextStyle(fontWeight: FontWeight.w300), + ), Flexible( child: Text( _source?.name.getLocalized(context) ?? From 148a69a12e5e09028bebbab7ef9f4ff52e3282f9 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 12 Oct 2023 20:21:34 +0200 Subject: [PATCH 102/130] hopefully work around Impeller issues with SliderThemes --- lib/components/PlayerScreen/progress_slider.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/components/PlayerScreen/progress_slider.dart b/lib/components/PlayerScreen/progress_slider.dart index c83c89a10..f7c08cdcb 100644 --- a/lib/components/PlayerScreen/progress_slider.dart +++ b/lib/components/PlayerScreen/progress_slider.dart @@ -249,9 +249,9 @@ class __PlaybackProgressSliderState // : _sliderThemeData.copyWith( : SliderTheme.of(context).copyWith( inactiveTrackColor: Colors.transparent, - thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 0), + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 0.1), // gets rid of both horizontal and vertical padding - overlayShape: const RoundSliderOverlayShape(overlayRadius: 0), + overlayShape: const RoundSliderOverlayShape(overlayRadius: 0.1), trackShape: const RectangularSliderTrackShape(), // rectangular shape is thinner than round trackHeight: 4.0, From 4f5967406e27d6f58ef42d15c0af8ceb70d684f3 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 15 Oct 2023 21:22:40 +0200 Subject: [PATCH 103/130] add looping tracks to local playback history --- lib/services/playback_history_service.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart index 7b8c968e3..5434def75 100644 --- a/lib/services/playback_history_service.dart +++ b/lib/services/playback_history_service.dart @@ -80,6 +80,7 @@ class PlaybackHistoryService { // current position is close to the beginning of the track currentState.position.inMilliseconds <= 1000 * 10 ) { + updateCurrentTrack(currentItem); onTrackChanged(currentItem, currentState, prevItem, prevState, true); return; } From 19b8ee8d8edd0edbb71ea1f8503c386c9de46322 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 15 Oct 2023 21:28:00 +0200 Subject: [PATCH 104/130] increase `maxBufferDuration` to reduce request frequency --- lib/services/music_player_background_task.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index 8a8f0bb6d..998e0dd8a 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -14,7 +14,7 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { audioLoadConfiguration: AudioLoadConfiguration( androidLoadControl: AndroidLoadControl( minBufferDuration: FinampSettingsHelper.finampSettings.bufferDuration, - maxBufferDuration: FinampSettingsHelper.finampSettings.bufferDuration, + maxBufferDuration: FinampSettingsHelper.finampSettings.bufferDuration * 1.5, // allows the player to fetch a bit more data in exchange for reduced request frequency prioritizeTimeOverSizeThresholds: true, ), darwinLoadControl: DarwinLoadControl( From c811900e0eff02837a0a5b39a7c740e4bf1cea06 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 15 Oct 2023 21:33:31 +0200 Subject: [PATCH 105/130] add track to playback history when rewinding --- lib/services/playback_history_service.dart | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart index 5434def75..433e487ef 100644 --- a/lib/services/playback_history_service.dart +++ b/lib/services/playback_history_service.dart @@ -71,18 +71,24 @@ class PlaybackHistoryService { // handle seeking (changes updateTime (= last abnormal position change)) else if (currentState.playing && currentState.updateTime != prevState?.updateTime && currentState.bufferedPosition == prevState?.bufferedPosition) { - // detect looping a single track + // detect rewinding & looping a single track if ( // same track prevItem?.id == currentItem.id && - // last position was close to the end of the track - (prevState?.position.inMilliseconds ?? 0) >= ((prevItem?.item.duration?.inMilliseconds ?? 0) - 1000 * 10) && // current position is close to the beginning of the track currentState.position.inMilliseconds <= 1000 * 10 ) { - updateCurrentTrack(currentItem); - onTrackChanged(currentItem, currentState, prevItem, prevState, true); - return; + if ((prevState?.position.inMilliseconds ?? 0) >= ((prevItem?.item.duration?.inMilliseconds ?? 0) - 1000 * 10)) { + // looping a single track + // last position was close to the end of the track + updateCurrentTrack(currentItem); // add to playback history + onTrackChanged(currentItem, currentState, prevItem, prevState, true); + return; // don't report seek event + } else { + // rewinding + updateCurrentTrack(currentItem); // add to playback history + // don't return, report seek event + } } // rate limit updates (only send update after no changes for 3 seconds) and if the track is still the same From 60907b4014285c875baad831c7249b507068f07f Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 15 Oct 2023 21:46:32 +0200 Subject: [PATCH 106/130] Navigate to source when clicking "Playing from" --- .../player_screen_appbar_title.dart | 70 +------------------ lib/components/PlayerScreen/queue_list.dart | 14 +++- .../PlayerScreen/queue_source_helper.dart | 67 ++++++++++++++++++ 3 files changed, 82 insertions(+), 69 deletions(-) create mode 100644 lib/components/PlayerScreen/queue_source_helper.dart diff --git a/lib/components/PlayerScreen/player_screen_appbar_title.dart b/lib/components/PlayerScreen/player_screen_appbar_title.dart index babdf7417..6cc9b9cda 100644 --- a/lib/components/PlayerScreen/player_screen_appbar_title.dart +++ b/lib/components/PlayerScreen/player_screen_appbar_title.dart @@ -1,20 +1,14 @@ -import 'package:finamp/screens/album_screen.dart'; -import 'package:finamp/screens/artist_screen.dart'; -import 'package:finamp/screens/music_screen.dart'; -import 'package:finamp/services/finamp_settings_helper.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_vibrate/flutter_vibrate.dart'; import 'package:get_it/get_it.dart'; -import 'package:audio_service/audio_service.dart'; -import 'package:palette_generator/palette_generator.dart'; -import '../../models/jellyfin_models.dart' as jellyfin_models; import '../../models/finamp_models.dart'; import 'package:finamp/services/queue_service.dart'; +import 'queue_source_helper.dart'; + class PlayerScreenAppBarTitle extends StatefulWidget { const PlayerScreenAppBarTitle({Key? key}) : super(key: key); @@ -67,63 +61,3 @@ class _PlayerScreenAppBarTitleState extends State { ); } } - -void navigateToSource(BuildContext context, QueueItemSource source) async { - - switch (source.type) { - case QueueItemSourceType.album: - case QueueItemSourceType.nextUpAlbum: - Navigator.of(context).pushNamed(AlbumScreen.routeName, arguments: source.item); - break; - case QueueItemSourceType.artist: - case QueueItemSourceType.nextUpArtist: - Navigator.of(context).pushNamed(ArtistScreen.routeName, arguments: source.item); - break; - case QueueItemSourceType.genre: - Navigator.of(context).pushNamed(ArtistScreen.routeName, arguments: source.item); - break; - case QueueItemSourceType.playlist: - case QueueItemSourceType.nextUpPlaylist: - Navigator.of(context).pushNamed(AlbumScreen.routeName, arguments: source.item); - break; - case QueueItemSourceType.albumMix: - Navigator.of(context).pushNamed(AlbumScreen.routeName, arguments: source.item); - break; - case QueueItemSourceType.artistMix: - Navigator.of(context).pushNamed(ArtistScreen.routeName, arguments: source.item); - break; - case QueueItemSourceType.allSongs: - Navigator.of(context).pushNamed(MusicScreen.routeName, arguments: FinampSettingsHelper.finampSettings.showTabs.entries - .where((element) => element.value == true) - .map((e) => e.key) - .toList().indexOf(TabContentType.songs) - ); - break; - case QueueItemSourceType.nextUp: - break; - case QueueItemSourceType.formerNextUp: - break; - case QueueItemSourceType.unknown: - break; - case QueueItemSourceType.favorites: - case QueueItemSourceType.songMix: - case QueueItemSourceType.filteredList: - case QueueItemSourceType.downloads: - default: - Vibrate.feedback(FeedbackType.warning); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Not implemented yet."), - // action: SnackBarAction( - // label: "OPEN", - // onPressed: () { - // Navigator.of(context).pushNamed( - // "/music/albumscreen", - // arguments: snapshot.data![index]); - // }, - // ), - ), - ); - } - -} diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 0062ea78b..94f3fff3b 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -25,6 +25,7 @@ import '../../services/media_state_stream.dart'; import '../../services/music_player_background_task.dart'; import '../../services/queue_service.dart'; import 'queue_list_item.dart'; +import 'queue_source_helper.dart'; class _QueueListStreamState { _QueueListStreamState( @@ -122,6 +123,7 @@ class _QueueListState extends State { ), SliverPersistentHeader( delegate: QueueSectionHeader( + source: _source, title: const Flexible(child: Text("Queue", overflow: TextOverflow.ellipsis)), nextUpHeaderKey: widget.nextUpHeaderKey, )), @@ -198,6 +200,7 @@ class _QueueListState extends State { sliver: SliverPersistentHeader( pinned: true, delegate: QueueSectionHeader( + source: _source, title: Row( children: [ Text( @@ -1167,12 +1170,14 @@ class PlaybackBehaviorInfo { class QueueSectionHeader extends SliverPersistentHeaderDelegate { final Widget title; + final QueueItemSource? source; final bool controls; final double height; final GlobalKey nextUpHeaderKey; QueueSectionHeader({ required this.title, + required this.source, required this.nextUpHeaderKey, this.controls = false, this.height = 30.0, @@ -1197,7 +1202,14 @@ class QueueSectionHeader extends SliverPersistentHeaderDelegate { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - child: title + child: GestureDetector( + child: title, + onTap: () { + if (source != null) { + navigateToSource(context, source!); + } + } + ), ), if (controls) Row( diff --git a/lib/components/PlayerScreen/queue_source_helper.dart b/lib/components/PlayerScreen/queue_source_helper.dart new file mode 100644 index 000000000..3fdfaadb3 --- /dev/null +++ b/lib/components/PlayerScreen/queue_source_helper.dart @@ -0,0 +1,67 @@ +import 'package:finamp/models/finamp_models.dart'; +import 'package:finamp/screens/album_screen.dart'; +import 'package:finamp/screens/artist_screen.dart'; +import 'package:finamp/screens/music_screen.dart'; +import 'package:finamp/services/finamp_settings_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_vibrate/flutter_vibrate.dart'; + +void navigateToSource(BuildContext context, QueueItemSource source) async { + + switch (source.type) { + case QueueItemSourceType.album: + case QueueItemSourceType.nextUpAlbum: + Navigator.of(context).pushNamed(AlbumScreen.routeName, arguments: source.item); + break; + case QueueItemSourceType.artist: + case QueueItemSourceType.nextUpArtist: + Navigator.of(context).pushNamed(ArtistScreen.routeName, arguments: source.item); + break; + case QueueItemSourceType.genre: + Navigator.of(context).pushNamed(ArtistScreen.routeName, arguments: source.item); + break; + case QueueItemSourceType.playlist: + case QueueItemSourceType.nextUpPlaylist: + Navigator.of(context).pushNamed(AlbumScreen.routeName, arguments: source.item); + break; + case QueueItemSourceType.albumMix: + Navigator.of(context).pushNamed(AlbumScreen.routeName, arguments: source.item); + break; + case QueueItemSourceType.artistMix: + Navigator.of(context).pushNamed(ArtistScreen.routeName, arguments: source.item); + break; + case QueueItemSourceType.allSongs: + Navigator.of(context).pushNamed(MusicScreen.routeName, arguments: FinampSettingsHelper.finampSettings.showTabs.entries + .where((element) => element.value == true) + .map((e) => e.key) + .toList().indexOf(TabContentType.songs) + ); + break; + case QueueItemSourceType.nextUp: + break; + case QueueItemSourceType.formerNextUp: + break; + case QueueItemSourceType.unknown: + break; + case QueueItemSourceType.favorites: + case QueueItemSourceType.songMix: + case QueueItemSourceType.filteredList: + case QueueItemSourceType.downloads: + default: + Vibrate.feedback(FeedbackType.warning); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Not implemented yet."), + // action: SnackBarAction( + // label: "OPEN", + // onPressed: () { + // Navigator.of(context).pushNamed( + // "/music/albumscreen", + // arguments: snapshot.data![index]); + // }, + // ), + ), + ); + } + +} From ab41bab717018acd5df84195a4652b2760f23b0f Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 21 Oct 2023 00:29:56 +0200 Subject: [PATCH 107/130] add buttons for adding albums/playlists to queue --- .../AlbumScreen/album_screen_content.dart | 4 +- ...bum_screen_content_flexible_space_bar.dart | 65 +++++++++++- .../AlbumScreen/song_list_tile.dart | 4 +- lib/components/MusicScreen/album_item.dart | 98 ++++++++++++++++++- .../playback_history_list_tile.dart | 6 +- lib/components/PlayerScreen/queue_list.dart | 4 +- .../PlayerScreen/queue_list_item.dart | 6 +- lib/l10n/app_en.arb | 21 +++- lib/services/queue_service.dart | 30 ++++-- 9 files changed, 211 insertions(+), 27 deletions(-) diff --git a/lib/components/AlbumScreen/album_screen_content.dart b/lib/components/AlbumScreen/album_screen_content.dart index 21a351b11..446a9b21a 100644 --- a/lib/components/AlbumScreen/album_screen_content.dart +++ b/lib/components/AlbumScreen/album_screen_content.dart @@ -62,11 +62,11 @@ class _AlbumScreenContentState extends State { SliverAppBar( title: Text(widget.parent.name ?? AppLocalizations.of(context)!.unknownName), - // 125 + 168 is the total height of the widget we use as a + // 125 + 186 is the total height of the widget we use as a // FlexibleSpaceBar. We add the toolbar height since the widget // should appear below the appbar. // TODO: This height is affected by platform density. - expandedHeight: kToolbarHeight + 125 + 168, + expandedHeight: kToolbarHeight + 125 + 186, pinned: true, flexibleSpace: AlbumScreenContentFlexibleSpaceBar( parentItem: widget.parent, diff --git a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart index db1cb978f..99f658540 100644 --- a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart +++ b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart @@ -67,7 +67,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(AppLocalizations.of(context)!.confirmAddToNextUp("album")), + content: Text(AppLocalizations.of(context)!.confirmAddToNextUp(isPlaylist ? "playlist" : "album")), ), ); @@ -85,7 +85,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(AppLocalizations.of(context)!.confirmPlayNext("album")), + content: Text(AppLocalizations.of(context)!.confirmPlayNext(isPlaylist ? "playlist" : "album")), ), ); } @@ -130,6 +130,44 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { ); } + void addAlbumToQueue() { + queueService.addToQueue( + items: items, + source: QueueItemSource( + type: isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), + id: parentItem.id, + item: parentItem, + ), + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.confirmAddToQueue(isPlaylist ? "playlist" : "album")), + ), + ); + + } + + void shuffleAlbumToQueue() { + // linear order is used in this case since we don't want to affect the rest of the queue + List clonedItems = List.from(items); + clonedItems.shuffle(); + queueService.addToQueue( + items: clonedItems, + source: QueueItemSource( + type: isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), + id: parentItem.id, + item: parentItem, + ) + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.confirmShuffleToQueue), + ), + ); + } + return FlexibleSpaceBar( background: SafeArea( child: Align( @@ -183,6 +221,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { Row(children: [ Expanded( child: ElevatedButton.icon( + style: const ButtonStyle(visualDensity: VisualDensity.compact), onPressed: () => addAlbumNext(), icon: const Icon(Icons.hourglass_bottom), label: @@ -192,6 +231,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), Expanded( child: ElevatedButton.icon( + style: const ButtonStyle(visualDensity: VisualDensity.compact), onPressed: () => shuffleAlbumNext(), icon: const Icon(Icons.hourglass_bottom), label: @@ -202,6 +242,7 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { Row(children: [ Expanded( child: ElevatedButton.icon( + style: const ButtonStyle(visualDensity: VisualDensity.compact), onPressed: () => addAlbumToNextUp(), icon: const Icon(Icons.hourglass_top), label: Text(AppLocalizations.of(context)!.addToNextUp), @@ -210,12 +251,32 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), Expanded( child: ElevatedButton.icon( + style: const ButtonStyle(visualDensity: VisualDensity.compact), onPressed: () => shuffleAlbumToNextUp(), icon: const Icon(Icons.hourglass_top), label: Text(AppLocalizations.of(context)!.shuffleToNextUp), ), ), ]), + Row(children: [ + Expanded( + child: ElevatedButton.icon( + style: const ButtonStyle(visualDensity: VisualDensity.compact), + onPressed: () => addAlbumToQueue(), + icon: const Icon(Icons.queue_music), + label: Text(AppLocalizations.of(context)!.addToQueue), + ), + ), + const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), + Expanded( + child: ElevatedButton.icon( + style: const ButtonStyle(visualDensity: VisualDensity.compact), + onPressed: () => shuffleAlbumToQueue(), + icon: const Icon(Icons.queue_music), + label: Text(AppLocalizations.of(context)!.shuffleToQueue), + ), + ), + ]), ], ), ) diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index cb9bb9e9d..433ce4f57 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -318,7 +318,7 @@ class _SongListTileState extends State { switch (selection) { case SongListTileMenuItems.addToQueue: - await _queueService.addToQueue(widget.item, QueueItemSource(type: QueueItemSourceType.unknown, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: AppLocalizations.of(context)!.queue), id: widget.parentId ?? "unknown")); + await _queueService.addToQueue(items: [widget.item], source: QueueItemSource(type: QueueItemSourceType.unknown, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: AppLocalizations.of(context)!.queue), id: widget.parentId ?? "unknown")); if (!mounted) return; @@ -454,7 +454,7 @@ class _SongListTileState extends State { ), ), confirmDismiss: (direction) async { - await _queueService.addToQueue(widget.item, QueueItemSource(type: QueueItemSourceType.unknown, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: AppLocalizations.of(context)!.queue), id: widget.parentId!)); + await _queueService.addToQueue(items: [widget.item], source: QueueItemSource(type: QueueItemSourceType.unknown, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: AppLocalizations.of(context)!.queue), id: widget.parentId!)); if (!mounted) return false; diff --git a/lib/components/MusicScreen/album_item.dart b/lib/components/MusicScreen/album_item.dart index 23dbcc547..89b6d1950 100644 --- a/lib/components/MusicScreen/album_item.dart +++ b/lib/components/MusicScreen/album_item.dart @@ -22,6 +22,8 @@ enum _AlbumListTileMenuItems { addToNextUp, shuffleNext, shuffleToNextUp, + addToQueue, + shuffleToQueue, } /// This widget is kind of a shell around AlbumItemCard and AlbumItemListTile. @@ -195,6 +197,22 @@ class _AlbumItemState extends State { Text(local.shuffleToNextUp), ), ), + PopupMenuItem<_AlbumListTileMenuItems>( + value: _AlbumListTileMenuItems.addToQueue, + child: ListTile( + leading: const Icon(Icons.queue_music), + title: + Text(local.addToQueue), + ), + ), + PopupMenuItem<_AlbumListTileMenuItems>( + value: _AlbumListTileMenuItems.shuffleToQueue, + child: ListTile( + leading: const Icon(Icons.queue_music), + title: + Text(local.shuffleToQueue), + ), + ), ], ); @@ -372,7 +390,7 @@ class _AlbumItemState extends State { List? albumTracks = await jellyfinApiHelper.getItems( parentItem: mutableAlbum, isGenres: false, - sortBy: "Random", + sortBy: "Random", //TODO this isn't working anymore with Jellyfin 10.9 (unstable) includeItemTypes: "Audio", ); @@ -406,6 +424,84 @@ class _AlbumItemState extends State { errorSnackbar(e, context); } break; + case _AlbumListTileMenuItems.addToQueue: + try { + List? albumTracks = await jellyfinApiHelper.getItems( + parentItem: mutableAlbum, + isGenres: false, + sortBy: "ParentIndexNumber,IndexNumber,SortName", + includeItemTypes: "Audio", + ); + + if (albumTracks == null) { + messenger.showSnackBar( + SnackBar( + content: Text("Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), + ), + ); + return; + } + + _queueService.addToQueue( + items: albumTracks, + source: QueueItemSource( + type: widget.isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: mutableAlbum.name ?? local.placeholderSource), + id: mutableAlbum.id, + item: mutableAlbum, + ) + ); + + messenger.showSnackBar( + SnackBar( + content: Text(local.confirmAddToQueue(widget.isPlaylist ? "playlist" : "album")), + ), + ); + + setState(() {}); + } catch (e) { + errorSnackbar(e, context); + } + break; + case _AlbumListTileMenuItems.shuffleToQueue: + try { + List? albumTracks = await jellyfinApiHelper.getItems( + parentItem: mutableAlbum, + isGenres: false, + sortBy: "Random", + includeItemTypes: "Audio", + ); + + if (albumTracks == null) { + messenger.showSnackBar( + SnackBar( + content: Text("Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), + ), + ); + return; + } + + _queueService.addToQueue( + items: albumTracks, + source: QueueItemSource( + type: widget.isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, + name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: mutableAlbum.name ?? local.placeholderSource), + id: mutableAlbum.id, + item: mutableAlbum, + ) + ); + + messenger.showSnackBar( + SnackBar( + content: Text(local.confirmAddToQueue(widget.isPlaylist ? "playlist" : "album")), + ), + ); + + setState(() {}); + } catch (e) { + errorSnackbar(e, context); + } + break; case null: break; } diff --git a/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart b/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart index 84c9db088..036b3bad7 100644 --- a/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart +++ b/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart @@ -233,9 +233,9 @@ class _PlaybackHistoryListTileState extends State { switch (selection) { case SongListTileMenuItems.addToQueue: await widget._queueService.addToQueue( - jellyfin_models.BaseItemDto.fromJson( - widget.item.item.item.extras?["itemJson"]), - QueueItemSource( + items: [jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"])], + source: QueueItemSource( type: QueueItemSourceType.unknown, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: AppLocalizations.of(context)!.queue), id: widget.item.item.source.id)); diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 94f3fff3b..2b7bdbf00 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -1045,8 +1045,8 @@ class _CurrentTrackState extends State { switch (selection) { case SongListTileMenuItems.addToQueue: await _queueService.addToQueue( - item, - QueueItemSource( + items: [item], + source: QueueItemSource( type: QueueItemSourceType.unknown, name: QueueItemSourceName( type: QueueItemSourceNameType.preTranslated, diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index d7ac510cd..3060a03f1 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -265,9 +265,9 @@ class _QueueListItemState extends State switch (selection) { case SongListTileMenuItems.addToQueue: await _queueService.addToQueue( - jellyfin_models.BaseItemDto.fromJson( - widget.item.item.extras?["itemJson"]), - QueueItemSource( + items: [jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"])], + source: QueueItemSource( type: QueueItemSourceType.unknown, name: QueueItemSourceName( type: QueueItemSourceNameType.preTranslated, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 68d0bd579..7bcc46c61 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -506,7 +506,7 @@ }, "addToNextUp": "Add to Next Up", "@addToNextUp": { - "description": "Used for adding a track to the \"Next Up\" queue at the end, to play after all previous tracks from Next Up have played " + "description": "Used for adding a track to the \"Next Up\" queue at the end, to play after all prior tracks from Next Up have played " }, "shuffleNext": "Shuffle next", "@shuffleNext": { @@ -514,7 +514,11 @@ }, "shuffleToNextUp": "Shuffle to Next Up", "@shuffleToNextUp": { - "description": "Used for shuffling a list (album, playlist, etc.) to the end of the \"Next Up\" queue, to play after all previous tracks from Next Up have played " + "description": "Used for shuffling a list (album, playlist, etc.) to the end of the \"Next Up\" queue, to play after all prior tracks from Next Up have played " + }, + "shuffleToQueue": "Shuffle to queue", + "@shuffleToQueue": { + "description": "Used for shuffling a list (album, playlist, etc.) to the end of the regular queue, to play after all prior tracks from the queue have played " }, "confirmPlayNext": "{type, select, track{Track} album{Album} artist{Artist} playlist{Playlist} other{Item}} will play next", "@confirmPlayNext": { @@ -534,6 +538,15 @@ } } }, + "confirmAddToQueue": "Added {type, select, track{track} album{album} artist{artist} playlist{playlist} other{item}} to queue", + "@confirmAddToQueue": { + "description": "A confirmation message that is shown after successfully adding a track to the end of the regular queue", + "placeholders": { + "type": { + "type": "String" + } + } + }, "confirmShuffleNext": "Will shuffle next", "@confirmShuffleNext": { "description": "A confirmation message that is shown after successfully shuffling a list (album, playlist, etc.) to the front of the \"Next Up\" queue" @@ -542,6 +555,10 @@ "@confirmShuffleToNextUp": { "description": "A confirmation message that is shown after successfully shuffling a list (album, playlist, etc.) to the end of the \"Next Up\" queue" }, + "confirmShuffleToQueue": "Shuffled to queue", + "@confirmShuffleToQueue": { + "description": "A confirmation message that is shown after successfully shuffling a list (album, playlist, etc.) to the end of the regular queue" + }, "placeholderSource": "Somewhere", "@placeholderSource": { "description": "Placeholder text used when the source of the current track/queue is unknown" diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 2469724cc..10945bac8 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -341,19 +341,29 @@ class QueueService { return; } - Future addToQueue( - jellyfin_models.BaseItemDto item, QueueItemSource source) async { + Future addToQueue({ + required List items, + QueueItemSource? source, + }) async { try { - FinampQueueItem queueItem = FinampQueueItem( - item: await _generateMediaItem(item), - source: source, - type: QueueItemQueueType.queue, - ); + List queueItems = []; + for (final item in items) { - await _queueAudioSource.add(await _queueItemToAudioSource(queueItem)); + queueItems.add(FinampQueueItem( + item: await _generateMediaItem(item), + source: source ?? _order.originalSource, + type: QueueItemQueueType.queue, + )); - _queueServiceLogger.fine( - "Added '${queueItem.item.title}' to queue from '${source.name}' (${source.type})"); + } + + List audioSources = []; + for (final item in queueItems) { + audioSources.add(await _queueItemToAudioSource(item)); + _queueServiceLogger.fine( + "Added '${item.item.title}' to queue from '${source?.name}' (${source?.type})"); + } + await _queueAudioSource.addAll(audioSources); _queueFromConcatenatingAudioSource(); // update internal queues } catch (e) { From 32dba484836ef890fe0e5f63ec6776d43988a015 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 21 Oct 2023 17:31:10 +0200 Subject: [PATCH 108/130] add scrollbar to queue panel --- lib/components/PlayerScreen/queue_list.dart | 22 +++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 2b7bdbf00..6d40ad7f3 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -229,10 +229,24 @@ class _QueueListState extends State { ) ]; - return CustomScrollView( - controller: widget.scrollController, - physics: const BouncingScrollPhysics(), - slivers: _contents, + return ScrollbarTheme( + data: ScrollbarThemeData( + thumbColor: MaterialStateProperty.all(Theme.of(context).colorScheme.primary.withOpacity(0.7)), + trackColor: MaterialStateProperty.all(Theme.of(context).colorScheme.primary.withOpacity(0.2)), + radius: const Radius.circular(6.0), + thickness: MaterialStateProperty.all(12.0), + // thumbVisibility: MaterialStateProperty.all(true), + trackVisibility: MaterialStateProperty.all(false) + ), + child: Scrollbar( + controller: widget.scrollController, + interactive: true, + child: CustomScrollView( + controller: widget.scrollController, + physics: const BouncingScrollPhysics(), + slivers: _contents, + ), + ), ); } } From 5710e28cc74b4a1dbc3b1997c9d43b78978f9e90 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 21 Oct 2023 18:12:44 +0200 Subject: [PATCH 109/130] don't shuffle twice --- lib/services/queue_service.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 10945bac8..ac0397f5d 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -238,9 +238,6 @@ class QueueService { playbackOrder = order; } - if (_playbackOrder == FinampPlaybackOrder.shuffled) { - items.shuffle(); - } await _replaceWholeQueue( itemList: items, source: source, initialIndex: startingIndex); _queueServiceLogger From c39c4161e65073ff67b9822add2a8ed68440e8dd Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 24 Oct 2023 23:02:03 +0200 Subject: [PATCH 110/130] fix shuffle indices being wrong until updating queue - re-shuffling will now also yield a new order than before --- lib/services/finamp_settings_helper.dart | 1 - .../music_player_background_task.dart | 10 +++++- lib/services/queue_service.dart | 36 ++++++++++++++----- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/lib/services/finamp_settings_helper.dart b/lib/services/finamp_settings_helper.dart index b7488227d..b403b4985 100644 --- a/lib/services/finamp_settings_helper.dart +++ b/lib/services/finamp_settings_helper.dart @@ -195,7 +195,6 @@ class FinampSettingsHelper { static void setLoopMode(FinampLoopMode loopMode) { FinampSettings finampSettingsTemp = finampSettings; finampSettingsTemp.loopMode = loopMode; - print("SETTING LOOP MODE TO ${finampSettingsTemp.loopMode}"); Hive.box("FinampSettings") .put("FinampSettings", finampSettingsTemp); } diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index 998e0dd8a..ec6f4b25f 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -233,12 +233,20 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } } + Future shuffle() async { + try { + await _player.shuffle(); + } catch (e) { + _audioServiceBackgroundTaskLogger.severe(e); + return Future.error(e); + } + } + @override Future setShuffleMode(AudioServiceShuffleMode shuffleMode) async { try { switch (shuffleMode) { case AudioServiceShuffleMode.all: - // await _player.shuffle(); await _player.setShuffleModeEnabled(true); break; case AudioServiceShuffleMode.none: diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index ac0397f5d..9c0089a42 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -234,12 +234,8 @@ class QueueService { }) async { // _initialQueue = list; // save original PlaybackList for looping/restarting and meta info - if (order != null) { - playbackOrder = order; - } - await _replaceWholeQueue( - itemList: items, source: source, initialIndex: startingIndex); + itemList: items, source: source, order: order, initialIndex: startingIndex); _queueServiceLogger .info("Started playing '${source.name}' (${source.type})"); } @@ -249,6 +245,7 @@ class QueueService { Future _replaceWholeQueue({ required List itemList, required QueueItemSource source, + FinampPlaybackOrder? order, int initialIndex = 0, }) async { try { @@ -314,6 +311,11 @@ class QueueService { _queueServiceLogger.fine("Order items length: ${_order.items.length}"); + // set playback order to trigger shuffle if necessary (fixes indices being wrong when starting with shuffle enabled) + if (order != null) { + playbackOrder = order; + } + // _queueStream.add(getQueue()); _queueFromConcatenatingAudioSource(); @@ -558,13 +560,15 @@ class QueueService { // update queue accordingly and generate new shuffled order if necessary if (_playbackOrder == FinampPlaybackOrder.shuffled) { - _audioHandler - .setShuffleMode(AudioServiceShuffleMode.all) - .then((value) => _queueFromConcatenatingAudioSource()); + _audioHandler.shuffle().then((_) => + _audioHandler + .setShuffleMode(AudioServiceShuffleMode.all) + .then((_) => _queueFromConcatenatingAudioSource()) + ); } else { _audioHandler .setShuffleMode(AudioServiceShuffleMode.none) - .then((value) => _queueFromConcatenatingAudioSource()); + .then((_) => _queueFromConcatenatingAudioSource()); } } @@ -858,6 +862,13 @@ class NextUpShuffleOrder extends ShuffleOrder { @override void removeRange(int start, int end) { + // log indices + String indicesString = ""; + for (int index in indices) { + indicesString += "$index, "; + } + _queueService!.queueServiceLogger + .finest("Shuffled indices before removing: $indicesString"); final count = end - start; // Remove old indices. final oldIndices = List.generate(count, (i) => start + i).toSet(); @@ -868,6 +879,13 @@ class NextUpShuffleOrder extends ShuffleOrder { indices[i] -= count; } } + // log indices + indicesString = ""; + for (int index in indices) { + indicesString += "$index, "; + } + _queueService!.queueServiceLogger + .finest("Shuffled indices after removing: $indicesString"); } @override From 7cb1abafe61c14e2817fd28663206e05d453b832 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Wed, 25 Oct 2023 00:09:46 +0200 Subject: [PATCH 111/130] improved way of fixing shuffle always starting on first track --- lib/services/playback_history_service.dart | 11 +++++++++++ lib/services/queue_service.dart | 17 ++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart index 433e487ef..9a80cd849 100644 --- a/lib/services/playback_history_service.dart +++ b/lib/services/playback_history_service.dart @@ -59,6 +59,7 @@ class PlaybackHistoryService { // differences in queue index or item id are considered track changes if (currentItem.id != prevItem?.id || (_reportQueueToServer && currentIndex != prevState?.queueIndex)) { _playbackHistoryServiceLogger.fine("Reporting track change event from ${prevItem?.item.title} to ${currentItem.item.title}"); + //TODO handle reporting track changes based on history changes, as that is more reliable onTrackChanged(currentItem, currentState, prevItem, prevState, currentIndex > (prevState?.queueIndex ?? 0)); } // handle events that don't change the current track (e.g. loop, pause, seek, etc.) @@ -82,6 +83,7 @@ class PlaybackHistoryService { // looping a single track // last position was close to the end of the track updateCurrentTrack(currentItem); // add to playback history + //TODO handle reporting track changes based on history changes, as that is more reliable onTrackChanged(currentItem, currentState, prevItem, prevState, true); return; // don't report seek event } else { @@ -213,10 +215,19 @@ class PlaybackHistoryService { return; } + int previousTrackTotalPlayTimeInMilliseconds = 0; // if there is a **previous** track if (_currentTrack != null) { // update end time of previous track _currentTrack!.endTime = DateTime.now(); + previousTrackTotalPlayTimeInMilliseconds = _currentTrack!.endTime!.difference(_currentTrack!.startTime).inMilliseconds; + } + + if (previousTrackTotalPlayTimeInMilliseconds < 1000) { + // replace history item with current track + if (_history.isNotEmpty) { + _history.removeLast(); + } } // if there is a **current** track diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 9c0089a42..ffb2cfd67 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -230,10 +230,18 @@ class QueueService { required List items, required QueueItemSource source, FinampPlaybackOrder? order, - int startingIndex = 0, + int? startingIndex, }) async { // _initialQueue = list; // save original PlaybackList for looping/restarting and meta info + if (startingIndex == null) { + if (order == FinampPlaybackOrder.shuffled) { + startingIndex = Random().nextInt(items.length); + } else { + startingIndex = 0; + } + } + await _replaceWholeQueue( itemList: items, source: source, order: order, initialIndex: startingIndex); _queueServiceLogger @@ -245,8 +253,8 @@ class QueueService { Future _replaceWholeQueue({ required List itemList, required QueueItemSource source, + required int initialIndex, FinampPlaybackOrder? order, - int initialIndex = 0, }) async { try { if (initialIndex > itemList.length) { @@ -289,8 +297,6 @@ class QueueService { } await _queueAudioSource.addAll(audioSources); - // _shuffleOrder - // .shuffle(); // shuffle without providing an index to make sure shuffle doesn't always start at the first index // set first item in queue _queueAudioSourceIndex = initialIndex; @@ -310,8 +316,9 @@ class QueueService { ); _queueServiceLogger.fine("Order items length: ${_order.items.length}"); - + // set playback order to trigger shuffle if necessary (fixes indices being wrong when starting with shuffle enabled) + if (order != null) { playbackOrder = order; } From 0fc9e9b1e03dec26053e1765fb20d4853b6942a0 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Wed, 25 Oct 2023 23:25:39 +0200 Subject: [PATCH 112/130] fix light mode contrast calculation --- lib/at_contrast.dart | 25 +++++++++++----------- lib/components/PlayerScreen/song_info.dart | 20 +++++++++++------ 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/lib/at_contrast.dart b/lib/at_contrast.dart index 3b0686475..8737ebc9a 100644 --- a/lib/at_contrast.dart +++ b/lib/at_contrast.dart @@ -24,25 +24,24 @@ extension AtContrast on Color { double minLightness = 0.0; double maxLightness = 1.0; - double diff = contrast - targetContrast; + double diff = contrast.abs() - targetContrast.abs(); int steps = 0; + int maxSteps = 25; - while (diff.abs() > _tolerance) { + // If diff is negative, we need more contrast. + while (diff < -_tolerance && steps < maxSteps) { steps++; - print("$steps $diff"); - // If diff is negative, we need more contrast. Otherwise, we need less + print("contrast: $steps $diff"); if (diff.isNegative) { - minLightness = hslColor.lightness; + if (lighter) { + minLightness = hslColor.lightness; + } else { + maxLightness = hslColor.lightness; + } - final lightDiff = maxLightness - hslColor.lightness; + final lightDiff = lighter ? maxLightness - minLightness : minLightness - maxLightness; hslColor = hslColor.withLightness(hslColor.lightness + lightDiff / 2); - } else { - maxLightness = hslColor.lightness; - - final lightDiff = hslColor.lightness - minLightness; - - hslColor = hslColor.withLightness(hslColor.lightness - lightDiff / 2); } contrast = contrastRatio( @@ -50,7 +49,7 @@ extension AtContrast on Color { backgroundLuminance, ); - diff = (contrast - targetContrast); + diff = (contrast.abs() - targetContrast.abs()); } _atContrastLogger.info("Calculated contrast in $steps steps"); diff --git a/lib/components/PlayerScreen/song_info.dart b/lib/components/PlayerScreen/song_info.dart index edb0bfb99..14cb85dbc 100644 --- a/lib/components/PlayerScreen/song_info.dart +++ b/lib/components/PlayerScreen/song_info.dart @@ -188,21 +188,29 @@ class _PlayerScreenAlbumImage extends ConsumerWidget { final paletteGenerator = await PaletteGenerator.fromImageProvider(imageProvider); - final accent = paletteGenerator.dominantColor!.color; + Color accent = paletteGenerator.dominantColor!.color; final lighter = theme.brightness == Brightness.dark; final background = Color.alphaBlend( lighter - ? Colors.black.withOpacity(0.75) - : Colors.white.withOpacity(0.5), + ? Colors.black.withOpacity(0.675) + : Colors.white.withOpacity(0.675), accent); - final newColour = accent.atContrast(4.5, background, lighter); + // increase saturation if the accent colour is too dark + if (!lighter) { + final hsv = HSVColor.fromColor(accent); + final newSaturation = min(1.0, hsv.saturation * 2); + final adjustedHsv = hsv.withSaturation(newSaturation); + accent = adjustedHsv.toColor(); + } + + accent = accent.atContrast(3.5, background, lighter); ref.read(playerScreenThemeProvider.notifier).state = ColorScheme.fromSwatch( - primarySwatch: generateMaterialColor(newColour), - accentColor: newColour, + primarySwatch: generateMaterialColor(accent), + accentColor: accent, brightness: theme.brightness, ); From 8116198321cf42ad06347f6e5ee2d8dc26177dfe Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Wed, 25 Oct 2023 23:25:57 +0200 Subject: [PATCH 113/130] light mode color adjustments --- lib/components/PlayerScreen/finamp_back_button_icon.dart | 8 ++++++-- .../PlayerScreen/player_screen_appbar_title.dart | 6 +++--- lib/components/PlayerScreen/queue_list.dart | 2 +- lib/components/finamp_app_bar_button.dart | 2 +- lib/screens/blurred_player_screen_background.dart | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/components/PlayerScreen/finamp_back_button_icon.dart b/lib/components/PlayerScreen/finamp_back_button_icon.dart index 942b12ddf..b92ea02c4 100644 --- a/lib/components/PlayerScreen/finamp_back_button_icon.dart +++ b/lib/components/PlayerScreen/finamp_back_button_icon.dart @@ -12,12 +12,16 @@ class FinampBackButtonIcon extends StatelessWidget { Widget build(BuildContext context) { return CustomPaint( size: Size(size, size), - painter: RPSCustomPainter(), + painter: RPSCustomPainter(context), ); } } class RPSCustomPainter extends CustomPainter { + + BuildContext context; + RPSCustomPainter(this.context); + @override void paint(Canvas canvas, Size size) { Path path_0 = Path(); @@ -28,7 +32,7 @@ class RPSCustomPainter extends CustomPainter { Paint paint0Stroke = Paint() ..style = PaintingStyle.stroke ..strokeWidth = size.width * 0.08333333; - paint0Stroke.color = Colors.white.withOpacity(1.0); + paint0Stroke.color = Theme.of(context).iconTheme.color ?? Colors.white; paint0Stroke.strokeCap = StrokeCap.round; paint0Stroke.strokeJoin = StrokeJoin.round; canvas.drawPath(path_0, paint0Stroke); diff --git a/lib/components/PlayerScreen/player_screen_appbar_title.dart b/lib/components/PlayerScreen/player_screen_appbar_title.dart index 6cc9b9cda..00c7df645 100644 --- a/lib/components/PlayerScreen/player_screen_appbar_title.dart +++ b/lib/components/PlayerScreen/player_screen_appbar_title.dart @@ -42,15 +42,15 @@ class _PlayerScreenAppBarTitleState extends State { style: TextStyle( fontSize: 12, fontWeight: FontWeight.w300, - color: Colors.white.withOpacity(0.7), + color: Theme.of(context).brightness == Brightness.dark ? Colors.white.withOpacity(0.7) : Colors.black.withOpacity(0.8), ), ), const Padding(padding: EdgeInsets.symmetric(vertical: 2)), Text( queueItem.source.name.getLocalized(context), - style: const TextStyle( + style: TextStyle( fontSize: 16, - color: Colors.white, + color: Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black.withOpacity(0.9), ), ), ], diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 6d40ad7f3..9a8816d28 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -300,7 +300,7 @@ Future showQueueBottomSheet(BuildContext context) { brightnessFactor: Theme.of(context).brightness == Brightness.dark ? 1.0 - : 1.0), + : 2.0), Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/components/finamp_app_bar_button.dart b/lib/components/finamp_app_bar_button.dart index 1b9c4ca5a..ec9ec1a6d 100644 --- a/lib/components/finamp_app_bar_button.dart +++ b/lib/components/finamp_app_bar_button.dart @@ -24,7 +24,7 @@ class FinampAppBarButton extends StatelessWidget { child: IconButton( onPressed: onPressed, tooltip: MaterialLocalizations.of(context).backButtonTooltip, - icon: const FinampBackButtonIcon(), + icon: FinampBackButtonIcon(), // Needed because otherwise the splash goes over the container // It may be like a pixel over now but I've spent way too long on this diff --git a/lib/screens/blurred_player_screen_background.dart b/lib/screens/blurred_player_screen_background.dart index ec10e1555..524a4dba2 100644 --- a/lib/screens/blurred_player_screen_background.dart +++ b/lib/screens/blurred_player_screen_background.dart @@ -34,7 +34,7 @@ class BlurredPlayerScreenBackground extends ConsumerWidget { colorFilter: ColorFilter.mode( Theme.of(context).brightness == Brightness.dark ? Colors.black.withOpacity(0.675 / brightnessFactor) - : Colors.white.withOpacity(0.50 / brightnessFactor), + : Colors.white.withOpacity(0.675 / brightnessFactor), BlendMode.srcOver), child: ImageFiltered( imageFilter: ImageFilter.blur( From 7034e6c14e87b181f7483969e8c6cdf3b8c125b1 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Wed, 25 Oct 2023 23:35:07 +0200 Subject: [PATCH 114/130] move accent color generation to now playing bar --- lib/components/PlayerScreen/song_info.dart | 49 ------------------ lib/components/now_playing_bar.dart | 58 +++++++++++++++++++++- 2 files changed, 57 insertions(+), 50 deletions(-) diff --git a/lib/components/PlayerScreen/song_info.dart b/lib/components/PlayerScreen/song_info.dart index 14cb85dbc..489ff8c96 100644 --- a/lib/components/PlayerScreen/song_info.dart +++ b/lib/components/PlayerScreen/song_info.dart @@ -167,55 +167,6 @@ class _PlayerScreenAlbumImage extends ConsumerWidget { : null; return item!; }).toList(), - // We need a post frame callback because otherwise this - // widget rebuilds on the same frame - imageProviderCallback: (imageProvider) => - WidgetsBinding.instance.addPostFrameCallback((_) async { - // Don't do anything if the image from the callback is the same as - // the current provider's image. This is probably needed because of - // addPostFrameCallback shenanigans - if (imageProvider != null && - ref.read(currentAlbumImageProvider.notifier).state == - imageProvider) { - return; - } - - ref.read(currentAlbumImageProvider.notifier).state = imageProvider; - - if (imageProvider != null) { - final theme = Theme.of(context); - - final paletteGenerator = - await PaletteGenerator.fromImageProvider(imageProvider); - - Color accent = paletteGenerator.dominantColor!.color; - - final lighter = theme.brightness == Brightness.dark; - final background = Color.alphaBlend( - lighter - ? Colors.black.withOpacity(0.675) - : Colors.white.withOpacity(0.675), - accent); - - // increase saturation if the accent colour is too dark - if (!lighter) { - final hsv = HSVColor.fromColor(accent); - final newSaturation = min(1.0, hsv.saturation * 2); - final adjustedHsv = hsv.withSaturation(newSaturation); - accent = adjustedHsv.toColor(); - } - - accent = accent.atContrast(3.5, background, lighter); - - ref.read(playerScreenThemeProvider.notifier).state = - ColorScheme.fromSwatch( - primarySwatch: generateMaterialColor(accent), - accentColor: accent, - brightness: theme.brightness, - ); - - } - }), ), ), ); diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index e5ef4c6d1..88adb1fb0 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -1,10 +1,16 @@ +import 'dart:math'; + import 'package:audio_service/audio_service.dart'; +import 'package:finamp/at_contrast.dart'; import 'package:finamp/components/favourite_button.dart'; +import 'package:finamp/generate_material_color.dart'; +import 'package:finamp/services/current_album_image_provider.dart'; import 'package:finamp/services/player_screen_theme_provider.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; +import 'package:palette_generator/palette_generator.dart'; import 'package:simple_gesture_detector/simple_gesture_detector.dart'; import '../services/finamp_settings_helper.dart'; @@ -118,7 +124,57 @@ class NowPlayingBar extends ConsumerWidget { child: ListTile( onTap: () => Navigator.of(context) .pushNamed(PlayerScreen.routeName), - leading: AlbumImage(item: item), + leading: AlbumImage(item: item, + // We need a post frame callback because otherwise this + // widget rebuilds on the same frame + imageProviderCallback: (imageProvider) => + WidgetsBinding.instance.addPostFrameCallback((_) async { + // Don't do anything if the image from the callback is the same as + // the current provider's image. This is probably needed because of + // addPostFrameCallback shenanigans + if (imageProvider != null && + ref.read(currentAlbumImageProvider.notifier).state == + imageProvider) { + return; + } + + ref.read(currentAlbumImageProvider.notifier).state = imageProvider; + + if (imageProvider != null) { + final theme = Theme.of(context); + + final paletteGenerator = + await PaletteGenerator.fromImageProvider(imageProvider); + + Color accent = paletteGenerator.dominantColor!.color; + + final lighter = theme.brightness == Brightness.dark; + final background = Color.alphaBlend( + lighter + ? Colors.black.withOpacity(0.675) + : Colors.white.withOpacity(0.675), + accent); + + // increase saturation if the accent colour is too dark + if (!lighter) { + final hsv = HSVColor.fromColor(accent); + final newSaturation = min(1.0, hsv.saturation * 2); + final adjustedHsv = hsv.withSaturation(newSaturation); + accent = adjustedHsv.toColor(); + } + + accent = accent.atContrast(3.5, background, lighter); + + ref.read(playerScreenThemeProvider.notifier).state = + ColorScheme.fromSwatch( + primarySwatch: generateMaterialColor(accent), + accentColor: accent, + brightness: theme.brightness, + ); + + } + }), + ), title: Text( snapshot.data!.mediaItem!.title, softWrap: false, From 9fb3be4c37d4ed49a44a80fd18356ea035cf53b4 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 29 Oct 2023 20:04:12 +0100 Subject: [PATCH 115/130] refactor now_playing_bar.dart, fix errors when changing theme - also move the accent color generation back into the player screen (for now) to fix an infinite loop --- lib/components/PlayerScreen/queue_list.dart | 16 +- lib/components/PlayerScreen/song_info.dart | 51 ++- lib/components/album_image.dart | 1 + lib/components/now_playing_bar.dart | 302 +++++++++--------- lib/main.dart | 1 - .../blurred_player_screen_background.dart | 2 +- lib/screens/player_screen.dart | 5 +- lib/services/queue_service.dart | 36 +-- 8 files changed, 209 insertions(+), 205 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 9a8816d28..217fdaed9 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -34,7 +34,7 @@ class _QueueListStreamState { ); final MediaState mediaState; - final FinampQueueInfo queueInfo; + final FinampQueueInfo? queueInfo; } class QueueList extends StatefulWidget { @@ -93,7 +93,7 @@ class _QueueListState extends State { super.initState(); _queueService.getQueueStream().listen((queueInfo) { - _source = queueInfo.source; + _source = queueInfo?.source; }); _source = _queueService.getQueue().source; @@ -300,7 +300,7 @@ Future showQueueBottomSheet(BuildContext context) { brightnessFactor: Theme.of(context).brightness == Brightness.dark ? 1.0 - : 2.0), + : 1.0), Column( mainAxisSize: MainAxisSize.min, children: [ @@ -385,7 +385,7 @@ class _PreviousTracksListState extends State @override Widget build(context) { - return StreamBuilder( + return StreamBuilder( stream: _queueService.getQueueStream(), builder: (context, snapshot) { if (snapshot.hasData) { @@ -474,7 +474,7 @@ class _NextUpTracksListState extends State { @override Widget build(context) { - return StreamBuilder( + return StreamBuilder( stream: _queueService.getQueueStream(), builder: (context, snapshot) { if (snapshot.hasData) { @@ -562,7 +562,7 @@ class _QueueTracksListState extends State { @override Widget build(context) { - return StreamBuilder( + return StreamBuilder( stream: _queueService.getQueueStream(), builder: (context, snapshot) { if (snapshot.hasData) { @@ -664,13 +664,13 @@ class _CurrentTrackState extends State { Duration? playbackPosition; return StreamBuilder<_QueueListStreamState>( - stream: Rx.combineLatest2( + stream: Rx.combineLatest2( mediaStateStream, _queueService.getQueueStream(), (a, b) => _QueueListStreamState(a, b)), builder: (context, snapshot) { if (snapshot.hasData) { - currentTrack = snapshot.data!.queueInfo.currentTrack; + currentTrack = snapshot.data!.queueInfo?.currentTrack; mediaState = snapshot.data!.mediaState; jellyfin_models.BaseItemDto? baseItem = diff --git a/lib/components/PlayerScreen/song_info.dart b/lib/components/PlayerScreen/song_info.dart index 489ff8c96..482db6a42 100644 --- a/lib/components/PlayerScreen/song_info.dart +++ b/lib/components/PlayerScreen/song_info.dart @@ -38,7 +38,7 @@ class _SongInfoState extends State { @override Widget build(BuildContext context) { - return StreamBuilder( + return StreamBuilder( stream: queueService.getQueueStream(), builder: (context, snapshot) { @@ -167,6 +167,55 @@ class _PlayerScreenAlbumImage extends ConsumerWidget { : null; return item!; }).toList(), + // We need a post frame callback because otherwise this + // widget rebuilds on the same frame + imageProviderCallback: (imageProvider) => WidgetsBinding.instance.addPostFrameCallback((_) async { + // Don't do anything if the image from the callback is the same as + // the current provider's image. This is probably needed because of + // addPostFrameCallback shenanigans + if (imageProvider != null && + ref.read(currentAlbumImageProvider.notifier).state == + imageProvider) { + return; + } + + ref.read(currentAlbumImageProvider.notifier).state = imageProvider; + + if (imageProvider != null) { + final theme = Theme.of(context); + + final paletteGenerator = + await PaletteGenerator.fromImageProvider(imageProvider); + + Color accent = paletteGenerator.dominantColor!.color; + + final lighter = theme.brightness == Brightness.dark; + + // increase saturation + if (!lighter) { + final hsv = HSVColor.fromColor(accent); + final newSaturation = min(1.0, hsv.saturation * 2); + final adjustedHsv = hsv.withSaturation(newSaturation); + accent = adjustedHsv.toColor(); + } + + final background = Color.alphaBlend( + lighter + ? Colors.black.withOpacity(0.675) + : Colors.white.withOpacity(0.675), + accent); + + accent = accent.atContrast(4.5, background, lighter); + + ref.read(playerScreenThemeProvider.notifier).state = + ColorScheme.fromSwatch( + primarySwatch: generateMaterialColor(accent), + accentColor: accent, + brightness: theme.brightness, + ); + + } + }), ), ), ); diff --git a/lib/components/album_image.dart b/lib/components/album_image.dart index 09280d1ce..e84d8c117 100644 --- a/lib/components/album_image.dart +++ b/lib/components/album_image.dart @@ -1,3 +1,4 @@ +import 'package:finamp/services/current_album_image_provider.dart'; import 'package:flutter/material.dart'; import 'package:octo_image/octo_image.dart'; diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index 88adb1fb0..132795e9b 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -4,9 +4,11 @@ import 'package:audio_service/audio_service.dart'; import 'package:finamp/at_contrast.dart'; import 'package:finamp/components/favourite_button.dart'; import 'package:finamp/generate_material_color.dart'; +import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/services/current_album_image_provider.dart'; import 'package:finamp/services/player_screen_theme_provider.dart'; import 'package:finamp/services/queue_service.dart'; +import 'package:finamp/services/theme_mode_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; @@ -16,7 +18,7 @@ import 'package:simple_gesture_detector/simple_gesture_detector.dart'; import '../services/finamp_settings_helper.dart'; import '../services/media_state_stream.dart'; import 'album_image.dart'; -import '../models/jellyfin_models.dart'; +import '../models/jellyfin_models.dart' as jellyfin_models; import '../services/process_artist.dart'; import '../services/music_player_background_task.dart'; import '../screens/player_screen.dart'; @@ -42,8 +44,9 @@ class NowPlayingBar extends ConsumerWidget { duration: const Duration(milliseconds: 500), data: ThemeData( fontFamily: "LexendDeca", - colorScheme: imageTheme, - brightness: Theme.of(context).brightness, + colorScheme: imageTheme?.copyWith( + brightness: Theme.of(context).brightness, + ), iconTheme: Theme.of(context).iconTheme.copyWith( color: imageTheme?.primary, ), @@ -54,180 +57,161 @@ class NowPlayingBar extends ConsumerWidget { Navigator.of(context).pushNamed(PlayerScreen.routeName); } }, - child: StreamBuilder( - stream: mediaStateStream, + child: StreamBuilder( + stream: queueService.getQueueStream(), builder: (context, snapshot) { - if (snapshot.hasData) { - final playing = snapshot.data!.playbackState.playing; - - // If we have a media item and the player hasn't finished, show - // the now playing bar. - if (snapshot.data!.mediaItem != null) { - final item = BaseItemDto.fromJson( - snapshot.data!.mediaItem!.extras!["itemJson"]); - - return Material( - color: IconTheme.of(context).color!.withOpacity(0.1), - elevation: elevation, - child: SafeArea( - child: SizedBox( - width: MediaQuery.of(context).size.width, - child: Stack( - children: [ - const ProgressSlider( - allowSeeking: false, - showBuffer: false, - showDuration: false, - showPlaceholder: false, - ), - Dismissible( - key: const Key("NowPlayingBar"), - direction: FinampSettingsHelper.finampSettings.disableGesture ? DismissDirection.none : DismissDirection.horizontal, - confirmDismiss: (direction) async { - if (direction == DismissDirection.endToStart) { - audioHandler.skipToNext(); - } else { - audioHandler.skipToPrevious(); - } - return false; - }, - background: const Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - AspectRatio( - aspectRatio: 1, - child: FittedBox( - fit: BoxFit.fitHeight, - child: Padding( - padding: - EdgeInsets.symmetric(vertical: 8.0), - child: Icon(Icons.skip_previous), - ), + + print("BUILDING"); + + if (snapshot.hasData && snapshot.data!.currentTrack != null) { + final currentTrack = snapshot.data!.currentTrack!; + final currentTrackBaseItem = currentTrack.item.extras?["itemJson"] != null + ? jellyfin_models.BaseItemDto.fromJson( + currentTrack.item.extras!["itemJson"] + as Map) + : null; + return Material( + color: Theme.of(context).brightness == Brightness.dark ? IconTheme.of(context).color!.withOpacity(0.1) : Theme.of(context).cardColor, + elevation: elevation, + child: SafeArea( + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: Stack( + children: [ + const ProgressSlider( + allowSeeking: false, + showBuffer: false, + showDuration: false, + showPlaceholder: false, + ), + Dismissible( + key: const Key("NowPlayingBar"), + direction: FinampSettingsHelper.finampSettings.disableGesture ? DismissDirection.none : DismissDirection.horizontal, + confirmDismiss: (direction) async { + if (direction == DismissDirection.endToStart) { + audioHandler.skipToNext(); + } else { + audioHandler.skipToPrevious(); + } + return false; + }, + background: const Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AspectRatio( + aspectRatio: 1, + child: FittedBox( + fit: BoxFit.fitHeight, + child: Padding( + padding: + EdgeInsets.symmetric(vertical: 8.0), + child: Icon(Icons.skip_previous), ), ), - AspectRatio( - aspectRatio: 1, - child: FittedBox( - fit: BoxFit.fitHeight, - child: Padding( - padding: - EdgeInsets.symmetric(vertical: 8.0), - child: Icon(Icons.skip_next), - ), + ), + AspectRatio( + aspectRatio: 1, + child: FittedBox( + fit: BoxFit.fitHeight, + child: Padding( + padding: + EdgeInsets.symmetric(vertical: 8.0), + child: Icon(Icons.skip_next), ), ), - ], - ), + ), + ], ), - child: ListTile( - onTap: () => Navigator.of(context) - .pushNamed(PlayerScreen.routeName), - leading: AlbumImage(item: item, - // We need a post frame callback because otherwise this - // widget rebuilds on the same frame - imageProviderCallback: (imageProvider) => - WidgetsBinding.instance.addPostFrameCallback((_) async { - // Don't do anything if the image from the callback is the same as - // the current provider's image. This is probably needed because of - // addPostFrameCallback shenanigans - if (imageProvider != null && - ref.read(currentAlbumImageProvider.notifier).state == - imageProvider) { - return; - } - - ref.read(currentAlbumImageProvider.notifier).state = imageProvider; - - if (imageProvider != null) { - final theme = Theme.of(context); - - final paletteGenerator = - await PaletteGenerator.fromImageProvider(imageProvider); - - Color accent = paletteGenerator.dominantColor!.color; - - final lighter = theme.brightness == Brightness.dark; - final background = Color.alphaBlend( - lighter - ? Colors.black.withOpacity(0.675) - : Colors.white.withOpacity(0.675), - accent); - - // increase saturation if the accent colour is too dark - if (!lighter) { - final hsv = HSVColor.fromColor(accent); - final newSaturation = min(1.0, hsv.saturation * 2); - final adjustedHsv = hsv.withSaturation(newSaturation); - accent = adjustedHsv.toColor(); - } - - accent = accent.atContrast(3.5, background, lighter); - - ref.read(playerScreenThemeProvider.notifier).state = - ColorScheme.fromSwatch( - primarySwatch: generateMaterialColor(accent), - accentColor: accent, - brightness: theme.brightness, + ), + child: ListTile( + onTap: () => Navigator.of(context) + .pushNamed(PlayerScreen.routeName), + leading: AlbumImage( + item: currentTrackBaseItem, + itemsToPrecache: + queueService.getNextXTracksInQueue(3).map((e) { + final item = e.item.extras?["itemJson"] != null + ? jellyfin_models.BaseItemDto.fromJson( + e.item.extras!["itemJson"] + as Map) + : null; + return item!; + }).toList(), + ), + title: Text( + currentTrack.item.title, + softWrap: false, + maxLines: 1, + overflow: TextOverflow.fade, + ), + subtitle: Text( + processArtist( + currentTrack.item.artist, context), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: StreamBuilder( + stream: mediaStateStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + final playing = snapshot.data!.playbackState.playing; + + // If we have a media item and the player hasn't finished, show + // the now playing bar. + if (snapshot.data!.mediaItem != null) { + final item = jellyfin_models.BaseItemDto.fromJson( + snapshot.data!.mediaItem!.extras!["itemJson"]); + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + FavoriteButton( + item: item, + onToggle: (isFavorite) { + item.userData!.isFavorite = isFavorite; + snapshot.data!.mediaItem?.extras!["itemJson"] = item.toJson(); + }, + ), + playing + ? IconButton( + icon: const Icon(Icons.pause), + onPressed: () => audioHandler.pause(), + ) + : IconButton( + icon: const Icon(Icons.play_arrow), + onPressed: () => audioHandler.play(), + ), + ], + ); + } else { + return const SizedBox( + width: 0, + height: 0, ); - } - }), - ), - title: Text( - snapshot.data!.mediaItem!.title, - softWrap: false, - maxLines: 1, - overflow: TextOverflow.fade, - ), - subtitle: Text( - processArtist( - snapshot.data!.mediaItem!.artist, context), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FavoriteButton( - item: item, - onToggle: (isFavorite) { - item.userData!.isFavorite = isFavorite; - snapshot.data!.mediaItem?.extras!["itemJson"] = item.toJson(); - }, - ), - playing - ? IconButton( - icon: const Icon(Icons.pause), - onPressed: () => audioHandler.pause(), - ) - : IconButton( - icon: const Icon(Icons.play_arrow), - onPressed: () => audioHandler.play(), - ), - ], - ), + } else { + return const SizedBox( + width: 0, + height: 0, + ); + } + }, ), ), - ], - ), + ), + ], ), ), - ); - } else { - return const SizedBox( - width: 0, - height: 0, - ); - } + ), + ); } else { return const SizedBox( width: 0, height: 0, ); } - }, + } ), ), ); diff --git a/lib/main.dart b/lib/main.dart index 43b416e6b..9bdac3637 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -347,7 +347,6 @@ class Finamp extends StatelessWidget { ), ), darkTheme: ThemeData( - brightness: Brightness.dark, scaffoldBackgroundColor: backgroundColor, appBarTheme: const AppBarTheme( color: raisedDarkColor, diff --git a/lib/screens/blurred_player_screen_background.dart b/lib/screens/blurred_player_screen_background.dart index 524a4dba2..a036fefdf 100644 --- a/lib/screens/blurred_player_screen_background.dart +++ b/lib/screens/blurred_player_screen_background.dart @@ -34,7 +34,7 @@ class BlurredPlayerScreenBackground extends ConsumerWidget { colorFilter: ColorFilter.mode( Theme.of(context).brightness == Brightness.dark ? Colors.black.withOpacity(0.675 / brightnessFactor) - : Colors.white.withOpacity(0.675 / brightnessFactor), + : Colors.white.withOpacity(0.5 / brightnessFactor), BlendMode.srcOver), child: ImageFiltered( imageFilter: ImageFilter.blur( diff --git a/lib/screens/player_screen.dart b/lib/screens/player_screen.dart index 6cd1e00e0..ae5e1e06c 100644 --- a/lib/screens/player_screen.dart +++ b/lib/screens/player_screen.dart @@ -35,8 +35,9 @@ class PlayerScreen extends ConsumerWidget { duration: const Duration(milliseconds: 500), data: ThemeData( fontFamily: "LexendDeca", - colorScheme: imageTheme, - brightness: Theme.of(context).brightness, + colorScheme: imageTheme?.copyWith( + brightness: Theme.of(context).brightness, + ), iconTheme: Theme.of(context).iconTheme.copyWith( color: imageTheme?.primary, ), diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index ffb2cfd67..11fbcfa0c 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -45,38 +45,8 @@ class QueueService { FinampPlaybackOrder _playbackOrder = FinampPlaybackOrder.linear; FinampLoopMode _loopMode = FinampLoopMode.none; - final _currentTrackStream = BehaviorSubject.seeded(FinampQueueItem( - item: const MediaItem( - id: "", - title: "No track playing", - album: "No album", - artist: "No artist"), - source: QueueItemSource( - id: "", - name: const QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated), - type: QueueItemSourceType.unknown))); - final _queueStream = BehaviorSubject.seeded(FinampQueueInfo( - previousTracks: [], - currentTrack: FinampQueueItem( - item: const MediaItem( - id: "", - title: "No track playing", - album: "No album", - artist: "No artist"), - source: QueueItemSource( - id: "", - name: const QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated), - type: QueueItemSourceType.unknown)), - queue: [], - nextUp: [], - source: QueueItemSource( - id: "", - name: const QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated), - type: QueueItemSourceType.unknown), - )); + final _currentTrackStream = BehaviorSubject.seeded(null); + final _queueStream = BehaviorSubject.seeded(null); final _playbackOrderStream = BehaviorSubject.seeded(FinampPlaybackOrder.linear); @@ -500,7 +470,7 @@ class QueueService { ); } - BehaviorSubject getQueueStream() { + BehaviorSubject getQueueStream() { return _queueStream; } From 0ba857c70318558c0d911d53688c73a33ec08f53 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 29 Oct 2023 20:52:18 +0100 Subject: [PATCH 116/130] updated now playing bar design --- lib/components/PlayerScreen/queue_list.dart | 211 +++++----- lib/components/now_playing_bar.dart | 415 ++++++++++++++------ 2 files changed, 392 insertions(+), 234 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 217fdaed9..ff4c776d2 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -679,8 +679,8 @@ class _CurrentTrackState extends State { : jellyfin_models.BaseItemDto.fromJson( currentTrack!.item.extras?["itemJson"]); - final horizontalPadding = 8.0; - final albumImageSize = 70.0; + const horizontalPadding = 8.0; + const albumImageSize = 70.0; return SliverAppBar( pinned: true, @@ -795,113 +795,114 @@ class _CurrentTrackState extends State { mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Container( - height: 70, - width: 222, - padding: - const EdgeInsets.only(left: 12, right: 4), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - currentTrack?.item.title ?? - AppLocalizations.of(context)! - .unknownName, - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w500, - overflow: TextOverflow.ellipsis), - ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - processArtist( - currentTrack!.item.artist, - context), - style: TextStyle( - color: Colors.white - .withOpacity(0.85), - fontSize: 13, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w300, - overflow: TextOverflow.ellipsis), - ), - Row( - children: [ - StreamBuilder( - stream: AudioService.position - .startWith(_audioHandler - .playbackState - .value - .position), - builder: (context, snapshot) { - final TextStyle style = - TextStyle( - color: Colors.white - .withOpacity(0.8), - fontSize: 14, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w400, - ); - if (snapshot.hasData) { - playbackPosition = - snapshot.data; - return Text( - // '0:00', - playbackPosition! - .inHours >= - 1.0 - ? "${playbackPosition?.inHours.toString()}:${((playbackPosition?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" - : "${playbackPosition?.inMinutes.toString()}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - style: style, - ); - } else { - return Text( - "0:00", - style: style, - ); - } - }), - const SizedBox(width: 2), - Text( - '/', - style: TextStyle( + Expanded( + child: Container( + height: albumImageSize, + padding: + const EdgeInsets.only(left: 12, right: 4), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + currentTrack?.item.title ?? + AppLocalizations.of(context)! + .unknownName, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w500, + overflow: TextOverflow.ellipsis), + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + processArtist( + currentTrack!.item.artist, + context), + style: TextStyle( color: Colors.white - .withOpacity(0.8), - fontSize: 14, + .withOpacity(0.85), + fontSize: 13, fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w300, + overflow: TextOverflow.ellipsis), + ), + Row( + children: [ + StreamBuilder( + stream: AudioService.position + .startWith(_audioHandler + .playbackState + .value + .position), + builder: (context, snapshot) { + final TextStyle style = + TextStyle( + color: Colors.white + .withOpacity(0.8), + fontSize: 14, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w400, + ); + if (snapshot.hasData) { + playbackPosition = + snapshot.data; + return Text( + // '0:00', + playbackPosition! + .inHours >= + 1.0 + ? "${playbackPosition?.inHours.toString()}:${((playbackPosition?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" + : "${playbackPosition?.inMinutes.toString()}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + style: style, + ); + } else { + return Text( + "0:00", + style: style, + ); + } + }), + const SizedBox(width: 2), + Text( + '/', + style: TextStyle( + color: Colors.white + .withOpacity(0.8), + fontSize: 14, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w400, + ), ), - ), - const SizedBox(width: 2), - Text( - // '3:44', - (mediaState?.mediaItem?.duration - ?.inHours ?? - 0.0) >= - 1.0 - ? "${mediaState?.mediaItem?.duration?.inHours.toString()}:${((mediaState?.mediaItem?.duration?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((mediaState?.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" - : "${mediaState?.mediaItem?.duration?.inMinutes.toString()}:${((mediaState?.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - style: TextStyle( - color: Colors.white - .withOpacity(0.8), - fontSize: 14, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w400, + const SizedBox(width: 2), + Text( + // '3:44', + (mediaState?.mediaItem?.duration + ?.inHours ?? + 0.0) >= + 1.0 + ? "${mediaState?.mediaItem?.duration?.inHours.toString()}:${((mediaState?.mediaItem?.duration?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((mediaState?.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" + : "${mediaState?.mediaItem?.duration?.inMinutes.toString()}:${((mediaState?.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + style: TextStyle( + color: Colors.white + .withOpacity(0.8), + fontSize: 14, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w400, + ), ), - ), - ], - ) - ], - ), - ], + ], + ) + ], + ), + ], + ), ), ), Row( diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index 132795e9b..c8589db38 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -7,13 +7,17 @@ import 'package:finamp/generate_material_color.dart'; import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/services/current_album_image_provider.dart'; import 'package:finamp/services/player_screen_theme_provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:finamp/services/theme_mode_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get_it/get_it.dart'; import 'package:palette_generator/palette_generator.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:simple_gesture_detector/simple_gesture_detector.dart'; +import 'package:flutter_vibrate/flutter_vibrate.dart'; import '../services/finamp_settings_helper.dart'; import '../services/media_state_stream.dart'; @@ -35,11 +39,15 @@ class NowPlayingBar extends ConsumerWidget { final imageTheme = ref.watch(playerScreenThemeProvider); const elevation = 8.0; + const horizontalPadding = 8.0; + const albumImageSize = 70.0; // final color = Theme.of(context).bottomNavigationBarTheme.backgroundColor; final audioHandler = GetIt.instance(); final queueService = GetIt.instance(); + Duration? playbackPosition; + return AnimatedTheme( duration: const Duration(milliseconds: 500), data: ThemeData( @@ -57,12 +65,12 @@ class NowPlayingBar extends ConsumerWidget { Navigator.of(context).pushNamed(PlayerScreen.routeName); } }, + onTap: () => Navigator.of(context) + .pushNamed(PlayerScreen.routeName), child: StreamBuilder( stream: queueService.getQueueStream(), builder: (context, snapshot) { - print("BUILDING"); - if (snapshot.hasData && snapshot.data!.currentTrack != null) { final currentTrack = snapshot.data!.currentTrack!; final currentTrackBaseItem = currentTrack.item.extras?["itemJson"] != null @@ -70,137 +78,286 @@ class NowPlayingBar extends ConsumerWidget { currentTrack.item.extras!["itemJson"] as Map) : null; - return Material( - color: Theme.of(context).brightness == Brightness.dark ? IconTheme.of(context).color!.withOpacity(0.1) : Theme.of(context).cardColor, - elevation: elevation, - child: SafeArea( - child: SizedBox( - width: MediaQuery.of(context).size.width, - child: Stack( - children: [ - const ProgressSlider( - allowSeeking: false, - showBuffer: false, - showDuration: false, - showPlaceholder: false, - ), - Dismissible( - key: const Key("NowPlayingBar"), - direction: FinampSettingsHelper.finampSettings.disableGesture ? DismissDirection.none : DismissDirection.horizontal, - confirmDismiss: (direction) async { - if (direction == DismissDirection.endToStart) { - audioHandler.skipToNext(); - } else { - audioHandler.skipToPrevious(); - } - return false; - }, - background: const Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - AspectRatio( - aspectRatio: 1, - child: FittedBox( - fit: BoxFit.fitHeight, - child: Padding( - padding: - EdgeInsets.symmetric(vertical: 8.0), - child: Icon(Icons.skip_previous), - ), - ), - ), - AspectRatio( - aspectRatio: 1, - child: FittedBox( - fit: BoxFit.fitHeight, - child: Padding( - padding: - EdgeInsets.symmetric(vertical: 8.0), - child: Icon(Icons.skip_next), + return Padding( + padding: const EdgeInsets.only(left: 12.0, bottom: 12.0, right: 12.0), + child: Material( + shadowColor: Theme.of(context).colorScheme.secondary.withOpacity(0.2), + borderRadius: BorderRadius.circular(12.0), + clipBehavior: Clip.antiAlias, + color: Theme.of(context).brightness == Brightness.dark ? IconTheme.of(context).color!.withOpacity(0.1) : Theme.of(context).cardColor, + elevation: elevation, + child: SafeArea( + child: Dismissible( + key: const Key("NowPlayingBar"), + direction: FinampSettingsHelper.finampSettings.disableGesture ? DismissDirection.none : DismissDirection.horizontal, + confirmDismiss: (direction) async { + if (direction == DismissDirection.endToStart) { + audioHandler.skipToNext(); + } else { + audioHandler.skipToPrevious(); + } + return false; + }, + child: StreamBuilder( + stream: mediaStateStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + final playing = snapshot.data!.playbackState.playing; + final mediaState = snapshot.data!; + // If we have a media item and the player hasn't finished, show + // the now playing bar. + if (snapshot.data!.mediaItem != null) { + //TODO move into separate component and share with queue list + return Container( + width: MediaQuery.of(context).size.width, + height: albumImageSize, + padding: EdgeInsets.zero, + child: Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: Color.alphaBlend( + IconTheme.of(context).color!.withOpacity(0.35), + Colors.black), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), ), ), - ), - ], - ), - ), - child: ListTile( - onTap: () => Navigator.of(context) - .pushNamed(PlayerScreen.routeName), - leading: AlbumImage( - item: currentTrackBaseItem, - itemsToPrecache: - queueService.getNextXTracksInQueue(3).map((e) { - final item = e.item.extras?["itemJson"] != null - ? jellyfin_models.BaseItemDto.fromJson( - e.item.extras!["itemJson"] - as Map) - : null; - return item!; - }).toList(), - ), - title: Text( - currentTrack.item.title, - softWrap: false, - maxLines: 1, - overflow: TextOverflow.fade, - ), - subtitle: Text( - processArtist( - currentTrack.item.artist, context), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - trailing: StreamBuilder( - stream: mediaStateStream, - builder: (context, snapshot) { - if (snapshot.hasData) { - final playing = snapshot.data!.playbackState.playing; - - // If we have a media item and the player hasn't finished, show - // the now playing bar. - if (snapshot.data!.mediaItem != null) { - final item = jellyfin_models.BaseItemDto.fromJson( - snapshot.data!.mediaItem!.extras!["itemJson"]); - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - FavoriteButton( - item: item, - onToggle: (isFavorite) { - item.userData!.isFavorite = isFavorite; - snapshot.data!.mediaItem?.extras!["itemJson"] = item.toJson(); - }, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Stack( + alignment: Alignment.center, + children: [ + AlbumImage( + item: currentTrackBaseItem, + borderRadius: BorderRadius.zero, + itemsToPrecache: + queueService.getNextXTracksInQueue(3).map((e) { + final item = e.item.extras?["itemJson"] != null + ? jellyfin_models.BaseItemDto.fromJson( + e.item.extras!["itemJson"] + as Map) + : null; + return item!; + }).toList(), ), - playing - ? IconButton( - icon: const Icon(Icons.pause), - onPressed: () => audioHandler.pause(), - ) - : IconButton( - icon: const Icon(Icons.play_arrow), - onPressed: () => audioHandler.play(), + Container( + width: albumImageSize, + height: albumImageSize, + decoration: const ShapeDecoration( + shape: Border(), + color: Color.fromRGBO(0, 0, 0, 0.3), ), - ], - ); - } else { - return const SizedBox( - width: 0, - height: 0, - ); - } - } else { - return const SizedBox( - width: 0, - height: 0, - ); - } - }, - ), - ), - ), - ], + child: IconButton( + onPressed: () { + Vibrate.feedback(FeedbackType.success); + audioHandler.togglePlayback(); + }, + icon: mediaState!.playbackState.playing + ? const Icon( + TablerIcons.player_pause, + size: 32, + ) + : const Icon( + TablerIcons.player_play, + size: 32, + ), + color: Colors.white, + )), + ], + ), + Expanded( + child: Stack( + children: [ + Positioned( + left: 0, + top: 0, + child: StreamBuilder( + stream: AudioService.position.startWith( + audioHandler.playbackState.value.position), + builder: (context, snapshot) { + if (snapshot.hasData) { + playbackPosition = snapshot.data; + final screenSize = MediaQuery.of(context).size; + return Container( + // rather hacky workaround, using LayoutBuilder would be nice but I couldn't get it to work... + width: (screenSize.width - 2*horizontalPadding - albumImageSize) * + (playbackPosition!.inMilliseconds / + (mediaState.mediaItem + ?.duration ?? + const Duration( + seconds: 0)) + .inMilliseconds), + height: 70.0, + decoration: ShapeDecoration( + color: IconTheme.of(context) + .color! + .withOpacity(0.75), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + ), + ); + } else { + return Container(); + } + }), + ), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Container( + height: albumImageSize, + padding: + const EdgeInsets.only(left: 12, right: 4), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + currentTrack.item.title, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w500, + overflow: TextOverflow.ellipsis), + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + processArtist( + currentTrack!.item.artist, + context), + style: TextStyle( + color: Colors.white + .withOpacity(0.85), + fontSize: 13, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w300, + overflow: TextOverflow.ellipsis), + ), + Row( + children: [ + StreamBuilder( + stream: AudioService.position + .startWith(audioHandler + .playbackState + .value + .position), + builder: (context, snapshot) { + final TextStyle style = + TextStyle( + color: Colors.white + .withOpacity(0.8), + fontSize: 14, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w400, + ); + if (snapshot.hasData) { + playbackPosition = + snapshot.data; + return Text( + // '0:00', + playbackPosition! + .inHours >= + 1.0 + ? "${playbackPosition?.inHours.toString()}:${((playbackPosition?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" + : "${playbackPosition?.inMinutes.toString()}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + style: style, + ); + } else { + return Text( + "0:00", + style: style, + ); + } + }), + const SizedBox(width: 2), + Text( + '/', + style: TextStyle( + color: Colors.white + .withOpacity(0.8), + fontSize: 14, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(width: 2), + Text( + // '3:44', + (mediaState.mediaItem?.duration + ?.inHours ?? + 0.0) >= + 1.0 + ? "${mediaState.mediaItem?.duration?.inHours.toString()}:${((mediaState.mediaItem?.duration?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((mediaState.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" + : "${mediaState.mediaItem?.duration?.inMinutes.toString()}:${((mediaState.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + style: TextStyle( + color: Colors.white + .withOpacity(0.8), + fontSize: 14, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w400, + ), + ), + ], + ) + ], + ), + ], + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only(top: 4.0, right: 4.0), + child: FavoriteButton( + item: currentTrackBaseItem, + onToggle: (isFavorite) { + currentTrackBaseItem!.userData!.isFavorite = isFavorite; + snapshot.data!.mediaItem?.extras!["itemJson"] = currentTrackBaseItem.toJson(); + }, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } else { + return const SizedBox( + width: 0, + height: 0, + ); + } + } else { + return const SizedBox( + width: 0, + height: 0, + ); + } + }, + ), ), ), ), From 00e47aeca335fb260c5b659b48a336e132ea4831 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 17 Nov 2023 21:46:32 +0100 Subject: [PATCH 117/130] added favorite button to playback history items - makes it easier to "like" a song that you just listened to --- .../playback_history_list_tile.dart | 200 ++++++++++-------- lib/components/now_playing_bar.dart | 1 + 2 files changed, 107 insertions(+), 94 deletions(-) diff --git a/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart b/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart index 036b3bad7..449c550b1 100644 --- a/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart +++ b/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart @@ -1,3 +1,4 @@ +import 'package:finamp/components/favourite_button.dart'; import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/services/audio_service_helper.dart'; import 'package:finamp/services/jellyfin_api_helper.dart'; @@ -37,101 +38,104 @@ class PlaybackHistoryListTile extends StatefulWidget { final _jellyfinApiHelper = GetIt.instance(); @override - State createState() => _PlaybackHistoryListTileState(); + State createState() => + _PlaybackHistoryListTileState(); } class _PlaybackHistoryListTileState extends State { @override Widget build(BuildContext context) { + + final baseItem = jellyfin_models.BaseItemDto.fromJson(widget.item.item.item.extras?["itemJson"]); + return GestureDetector( - onLongPressStart: (details) => showSongMenu(details), - child: Card( - margin: EdgeInsets.all(0.0), - elevation: 0, - clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), + onLongPressStart: (details) => showSongMenu(details), + child: Card( + margin: EdgeInsets.all(0.0), + elevation: 0, + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + child: ListTile( + visualDensity: VisualDensity.standard, + minVerticalPadding: 0.0, + horizontalTitleGap: 10.0, + contentPadding: + const EdgeInsets.only(right: 4.0), + leading: AlbumImage( + item: widget.item.item.item.extras?["itemJson"] == null + ? null + : baseItem, ), - child: ListTile( - visualDensity: VisualDensity.standard, - minVerticalPadding: 0.0, - horizontalTitleGap: 10.0, - contentPadding: - const EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0), - leading: AlbumImage( - item: widget.item.item.item.extras?["itemJson"] == null - ? null - : jellyfin_models.BaseItemDto.fromJson( - widget.item.item.item.extras?["itemJson"]), - ), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(0.0), - child: Text( - widget.item.item.item.title ?? - AppLocalizations.of(context)!.unknownName, - overflow: TextOverflow.ellipsis, - ), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(0.0), + child: Text( + widget.item.item.item.title ?? + AppLocalizations.of(context)!.unknownName, + overflow: TextOverflow.ellipsis, ), - Padding( - padding: const EdgeInsets.only(top: 6.0), - child: Text( - processArtist(widget.item.item.item.artist, context), - style: const TextStyle( - color: Colors.white70, - fontSize: 13, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w300, - overflow: TextOverflow.ellipsis), - overflow: TextOverflow.ellipsis, - ), + ), + Padding( + padding: const EdgeInsets.only(top: 6.0), + child: Text( + processArtist(widget.item.item.item.artist, context), + style: const TextStyle( + color: Colors.white70, + fontSize: 13, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w300, + overflow: TextOverflow.ellipsis), + overflow: TextOverflow.ellipsis, ), - ], - ), - // subtitle: Container( - // alignment: Alignment.centerLeft, - // height: 40.5, // has to be above a certain value to get rid of vertical padding - // child: Padding( - // padding: const EdgeInsets.only(bottom: 2.0), - // child: Text( - // processArtist(widget.item.item.item.artist, context), - // style: const TextStyle( - // color: Colors.white70, - // fontSize: 13, - // fontFamily: 'Lexend Deca', - // fontWeight: FontWeight.w300, - // overflow: TextOverflow.ellipsis), - // overflow: TextOverflow.ellipsis, - // ), - // ), - // ), - trailing: Container( - alignment: Alignment.centerRight, - margin: const EdgeInsets.only(right: 8.0), - padding: const EdgeInsets.only(right: 6.0), - // width: widget.allowReorder ? 145.0 : 115.0, - width: 35.0, - height: 50.0, - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "${widget.item.item.item.duration?.inMinutes.toString()}:${((widget.item.item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - textAlign: TextAlign.end, - style: TextStyle( - color: Theme.of(context).textTheme.bodySmall?.color, - ), - ), - ], ), - ), - onTap: widget.onTap, - ))); - + ], + ), + // subtitle: Container( + // alignment: Alignment.centerLeft, + // height: 40.5, // has to be above a certain value to get rid of vertical padding + // child: Padding( + // padding: const EdgeInsets.only(bottom: 2.0), + // child: Text( + // processArtist(widget.item.item.item.artist, context), + // style: const TextStyle( + // color: Colors.white70, + // fontSize: 13, + // fontFamily: 'Lexend Deca', + // fontWeight: FontWeight.w300, + // overflow: TextOverflow.ellipsis), + // overflow: TextOverflow.ellipsis, + // ), + // ), + // ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "${widget.item.item.item.duration?.inMinutes.toString()}:${((widget.item.item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + textAlign: TextAlign.end, + style: TextStyle( + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + FavoriteButton( + item: baseItem, + onToggle: (isFavorite) => setState(() { + if (baseItem.userData != null) { + baseItem.userData!.isFavorite = isFavorite; + widget.item.item.item.extras?["itemJson"] = baseItem.toJson(); + } + }), + ) + ], + ), + onTap: widget.onTap, + ))); } void showSongMenu(LongPressStartDetails? details) async { @@ -233,11 +237,15 @@ class _PlaybackHistoryListTileState extends State { switch (selection) { case SongListTileMenuItems.addToQueue: await widget._queueService.addToQueue( - items: [jellyfin_models.BaseItemDto.fromJson( - widget.item.item.item.extras?["itemJson"])], + items: [ + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]) + ], source: QueueItemSource( type: QueueItemSourceType.unknown, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: AppLocalizations.of(context)!.queue), + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: AppLocalizations.of(context)!.queue), id: widget.item.item.source.id)); if (!mounted) return; @@ -248,8 +256,10 @@ class _PlaybackHistoryListTileState extends State { break; case SongListTileMenuItems.playNext: - await widget._queueService.addNext(items: [jellyfin_models.BaseItemDto.fromJson( - widget.item.item.item.extras?["itemJson"])]); + await widget._queueService.addNext(items: [ + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]) + ]); if (!mounted) return; @@ -259,13 +269,16 @@ class _PlaybackHistoryListTileState extends State { break; case SongListTileMenuItems.addToNextUp: - await widget._queueService.addToNextUp(items: [jellyfin_models.BaseItemDto.fromJson( - widget.item.item.item.extras?["itemJson"])]); + await widget._queueService.addToNextUp(items: [ + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]) + ]); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.confirmAddToNextUp("track")), + content: + Text(AppLocalizations.of(context)!.confirmAddToNextUp("track")), )); break; @@ -379,7 +392,6 @@ class _PlaybackHistoryListTileState extends State { } } - /// If offline, check if an album is downloaded. Always returns true if online. /// Returns false if albumId is null. bool _isAlbumDownloadedIfOffline(String? albumId) { diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index c8589db38..3c7b9d876 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -87,6 +87,7 @@ class NowPlayingBar extends ConsumerWidget { color: Theme.of(context).brightness == Brightness.dark ? IconTheme.of(context).color!.withOpacity(0.1) : Theme.of(context).cardColor, elevation: elevation, child: SafeArea( + //TODO use a PageView instead of a Dismissible, and only wrap dynamic items (not the buttons) child: Dismissible( key: const Key("NowPlayingBar"), direction: FinampSettingsHelper.finampSettings.disableGesture ? DismissDirection.none : DismissDirection.horizontal, From a2b6c6375786bdb00b9f7a79e1143af466c76b4c Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 17 Nov 2023 23:01:22 +0100 Subject: [PATCH 118/130] show artist chips for all track artists --- lib/components/PlayerScreen/artist_chip.dart | 84 +++++++++++-------- .../PlayerScreen/song_name_content.dart | 4 +- 2 files changed, 50 insertions(+), 38 deletions(-) diff --git a/lib/components/PlayerScreen/artist_chip.dart b/lib/components/PlayerScreen/artist_chip.dart index 44f9497dd..6ca564588 100644 --- a/lib/components/PlayerScreen/artist_chip.dart +++ b/lib/components/PlayerScreen/artist_chip.dart @@ -16,14 +16,49 @@ const _textStyle = TextStyle( overflow: TextOverflow.fade, ); +class ArtistChips extends StatelessWidget { + const ArtistChips({ + Key? key, + this.color, + this.baseItem, + }) : super(key: key); + + final BaseItemDto? baseItem; + final Color? color; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(baseItem?.artistItems?.length ?? 0, (index) { + + final currentArtist = baseItem!.artistItems![index]; + + return Padding( + padding: const EdgeInsets.only(right: 4.0), + child: ArtistChip( + color: color, + artist: BaseItemDto( + id: currentArtist.id, + name: currentArtist.name, + type: "MusicArtist", + ), + + ), + ); + }), + ); + } +} + class ArtistChip extends StatefulWidget { const ArtistChip({ Key? key, this.color, - this.item, + this.artist, }) : super(key: key); - final BaseItemDto? item; + final BaseItemDto? artist; final Color? color; @override @@ -41,55 +76,32 @@ class _ArtistChipState extends State { void initState() { super.initState(); - if (widget.item != null && widget.item!.albumArtists?.isNotEmpty == true) { - final albumArtistId = widget.item!.albumArtists?.first.id; + if (widget.artist != null) { + final albumArtistId = widget.artist!.id; - if (albumArtistId != null) { // This is a terrible hack but since offline artists aren't yet - // implemented it's kind of needed. When offline, we make a fake item - // with the required amount of data to show an artist chip. - _artistChipFuture = FinampSettingsHelper.finampSettings.isOffline - ? Future.sync( - () => BaseItemDto( - id: widget.item!.id, - name: widget.item!.albumArtist, - type: "MusicArtist", - ), - ) - : _jellyfinApiHelper.getItemById(albumArtistId); - } + // implemented it's kind of needed. When offline, we make a fake item + // with the required amount of data to show an artist chip. + _artistChipFuture = FinampSettingsHelper.finampSettings.isOffline + ? Future.sync( + () => widget.artist! + ) + : _jellyfinApiHelper.getItemById(albumArtistId); } } @override Widget build(BuildContext context) { - if (_artistChipFuture == null) return const _EmptyArtistChip(); - return FutureBuilder( future: _artistChipFuture, builder: (context, snapshot) { final color = widget.color ?? _defaultColour; - return _ArtistChipContent(item: snapshot.data ?? widget.item!, color: color); + return _ArtistChipContent(item: snapshot.data ?? widget.artist!, color: color); } ); } } -class _EmptyArtistChip extends StatelessWidget { - const _EmptyArtistChip({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: _height, - width: 72, - child: Material( - borderRadius: _borderRadius, - ), - ); - } -} - class _ArtistChipContent extends StatelessWidget { const _ArtistChipContent({ Key? key, @@ -104,7 +116,7 @@ class _ArtistChipContent extends StatelessWidget { Widget build(BuildContext context) { // We do this so that we can pass the song item here to show an actual value // instead of empty - final name = item.isArtist ? item.name : item.albumArtist; + final name = item.isArtist ? item.name : (item.artists?.first ?? item.albumArtist); return SizedBox( height: 24, diff --git a/lib/components/PlayerScreen/song_name_content.dart b/lib/components/PlayerScreen/song_name_content.dart index 4f5c17fd2..fc02d34bf 100644 --- a/lib/components/PlayerScreen/song_name_content.dart +++ b/lib/components/PlayerScreen/song_name_content.dart @@ -54,8 +54,8 @@ class SongNameContent extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 4), - child: ArtistChip( - item: songBaseItemDto, + child: ArtistChips( + baseItem: songBaseItemDto, color: IconTheme.of(context).color!.withOpacity(0.1), key: songBaseItemDto?.albumArtist == null ? null From 6450daf3eb1bccce8e03adc3defcb162c303e616 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 17 Nov 2023 23:01:45 +0100 Subject: [PATCH 119/130] improve text wrapping and reduce layout shifts on player screen --- lib/components/PlayerScreen/control_area.dart | 4 +-- .../PlayerScreen/progress_slider.dart | 10 +++++-- lib/components/PlayerScreen/song_info.dart | 27 ++++++++++--------- .../PlayerScreen/song_name_content.dart | 24 ++++++++++------- 4 files changed, 38 insertions(+), 27 deletions(-) diff --git a/lib/components/PlayerScreen/control_area.dart b/lib/components/PlayerScreen/control_area.dart index a57840da2..09d7fce18 100644 --- a/lib/components/PlayerScreen/control_area.dart +++ b/lib/components/PlayerScreen/control_area.dart @@ -9,7 +9,7 @@ class ControlArea extends StatelessWidget { @override Widget build(BuildContext context) { return const Padding( - padding: EdgeInsets.symmetric(horizontal: 20), + padding: EdgeInsets.only(left: 20.0, right: 20.0, bottom: 8.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -17,7 +17,7 @@ class ControlArea extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 16), child: ProgressSlider(), ), - Padding(padding: EdgeInsets.symmetric(vertical: 4)), + Padding(padding: EdgeInsets.symmetric(vertical: 2)), PlayerButtons(), ], ), diff --git a/lib/components/PlayerScreen/progress_slider.dart b/lib/components/PlayerScreen/progress_slider.dart index f7c08cdcb..17a1994d4 100644 --- a/lib/components/PlayerScreen/progress_slider.dart +++ b/lib/components/PlayerScreen/progress_slider.dart @@ -194,14 +194,20 @@ class _ProgressSliderDuration extends StatelessWidget { style: Theme.of(context) .textTheme .bodyMedium - ?.copyWith(color: Theme.of(context).textTheme.bodySmall?.color), + ?.copyWith( + color: Theme.of(context).textTheme.bodySmall?.color, + height: 0.5, // reduce line height + ), ), Text( printDuration(itemDuration), style: Theme.of(context) .textTheme .bodyMedium - ?.copyWith(color: Theme.of(context).textTheme.bodySmall?.color), + ?.copyWith( + color: Theme.of(context).textTheme.bodySmall?.color, + height: 0.5, // reduce line height + ), ), ], ); diff --git a/lib/components/PlayerScreen/song_info.dart b/lib/components/PlayerScreen/song_info.dart index 482db6a42..d45aee117 100644 --- a/lib/components/PlayerScreen/song_info.dart +++ b/lib/components/PlayerScreen/song_info.dart @@ -101,17 +101,19 @@ class _SongInfoState extends State { separatedArtistTextSpans.removeLast(); } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _PlayerScreenAlbumImage(queueItem: currentTrack!), - const Padding(padding: EdgeInsets.symmetric(vertical: 6)), - SongNameContent( - currentTrack: currentTrack, - separatedArtistTextSpans: separatedArtistTextSpans, - secondaryTextColour: secondaryTextColour, - ) - ], + return Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _PlayerScreenAlbumImage(queueItem: currentTrack), + SongNameContent( + currentTrack: currentTrack, + separatedArtistTextSpans: separatedArtistTextSpans, + secondaryTextColour: secondaryTextColour, + ) + ], + ), ); }, ); @@ -128,7 +130,6 @@ class _PlayerScreenAlbumImage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final audioHandler = GetIt.instance(); final queueService = GetIt.instance(); final item = queueItem.item.extras?["itemJson"] != null @@ -148,7 +149,7 @@ class _PlayerScreenAlbumImage extends ConsumerWidget { ), alignment: Alignment.center, constraints: const BoxConstraints( - maxHeight: 300, + maxHeight: 320, // maxWidth: 300, // minHeight: 300, // minWidth: 300, diff --git a/lib/components/PlayerScreen/song_name_content.dart b/lib/components/PlayerScreen/song_name_content.dart index fc02d34bf..8f90a7c88 100644 --- a/lib/components/PlayerScreen/song_name_content.dart +++ b/lib/components/PlayerScreen/song_name_content.dart @@ -30,17 +30,21 @@ class SongNameContent extends StatelessWidget { children: [ Center( child: Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), - child: Text( - currentTrack.item.title, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 20, - height: 24 / 20, + padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 4.0, bottom: 4.0), + child: Container( + height: 48.0, + alignment: Alignment.center, + child: Text( + currentTrack.item.title, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 20, + // height: 24 / 20, + ), + overflow: TextOverflow.ellipsis, + softWrap: true, + maxLines: 2, ), - overflow: TextOverflow.fade, - softWrap: true, - maxLines: 2, ), ), ), From dd7e8cf93a243c5da01620dc391e5659c0ea6020 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 18 Nov 2023 23:54:58 +0100 Subject: [PATCH 120/130] prevent overflow for long/many artists --- lib/components/PlayerScreen/album_chip.dart | 4 +- lib/components/PlayerScreen/artist_chip.dart | 94 +++++++++++-------- .../PlayerScreen/song_name_content.dart | 39 ++++---- 3 files changed, 75 insertions(+), 62 deletions(-) diff --git a/lib/components/PlayerScreen/album_chip.dart b/lib/components/PlayerScreen/album_chip.dart index a9434c8ac..d3bea7671 100644 --- a/lib/components/PlayerScreen/album_chip.dart +++ b/lib/components/PlayerScreen/album_chip.dart @@ -24,7 +24,7 @@ class AlbumChip extends StatelessWidget { if (item == null) return const _EmptyAlbumChip(); return Container( - constraints: const BoxConstraints(minWidth: 10, maxWidth: 200), + constraints: const BoxConstraints(minWidth: 10), child: _AlbumChipContent(item: item!, color: color)); } } @@ -73,7 +73,7 @@ class _AlbumChipContent extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), child: Text( item.album ?? AppLocalizations.of(context)!.noAlbum, - overflow: TextOverflow.fade, + overflow: TextOverflow.ellipsis, softWrap: false, ), ), diff --git a/lib/components/PlayerScreen/artist_chip.dart b/lib/components/PlayerScreen/artist_chip.dart index 6ca564588..b487b5c77 100644 --- a/lib/components/PlayerScreen/artist_chip.dart +++ b/lib/components/PlayerScreen/artist_chip.dart @@ -28,25 +28,31 @@ class ArtistChips extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate(baseItem?.artistItems?.length ?? 0, (index) { - - final currentArtist = baseItem!.artistItems![index]; - - return Padding( - padding: const EdgeInsets.only(right: 4.0), - child: ArtistChip( - color: color, - artist: BaseItemDto( - id: currentArtist.id, - name: currentArtist.name, - type: "MusicArtist", - ), - + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Flexible( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Wrap( + spacing: 4.0, + runSpacing: 4.0, + children: List.generate(baseItem?.artistItems?.length ?? 0, (index) { + + final currentArtist = baseItem!.artistItems![index]; + + return ArtistChip( + color: color, + artist: BaseItemDto( + id: currentArtist.id, + name: currentArtist.name, + type: "MusicArtist", + ), + + ); + }), ), - ); - }), + ), + ), ); } } @@ -131,29 +137,39 @@ class _ArtistChipContent extends StatelessWidget { : () => Navigator.of(context) .popAndPushNamed(ArtistScreen.routeName, arguments: item), borderRadius: _borderRadius, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (item.isArtist && item.imageId != null) - AlbumImage( - item: item, - borderRadius: const BorderRadius.only( - topLeft: _radius, - bottomLeft: _radius, + child: Flexible( + fit: FlexFit.loose, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (item.isArtist && item.imageId != null) + AlbumImage( + item: item, + borderRadius: const BorderRadius.only( + topLeft: _radius, + bottomLeft: _radius, + ), ), - ), - Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: Text( - name ?? AppLocalizations.of(context)!.unknownArtist, - style: _textStyle, - softWrap: false, - overflow: TextOverflow.fade, + Flexible( + fit: FlexFit.loose, + flex: 1, + child: Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 220), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Text( + name ?? AppLocalizations.of(context)!.unknownArtist, + style: _textStyle, + softWrap: false, + overflow: TextOverflow.ellipsis, + ), + ), + ), ), - ), - ) - ], + ) + ], + ), ), ), ), diff --git a/lib/components/PlayerScreen/song_name_content.dart b/lib/components/PlayerScreen/song_name_content.dart index 8f90a7c88..915c75cfd 100644 --- a/lib/components/PlayerScreen/song_name_content.dart +++ b/lib/components/PlayerScreen/song_name_content.dart @@ -30,7 +30,7 @@ class SongNameContent extends StatelessWidget { children: [ Center( child: Padding( - padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 4.0, bottom: 4.0), + padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 4.0, bottom: 0.0), child: Container( height: 48.0, alignment: Alignment.center, @@ -50,14 +50,14 @@ class SongNameContent extends StatelessWidget { ), Padding( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, children: [ - PlayerButtonsMore(), - Column( + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 4), + PlayerButtonsMore(), + Flexible( child: ArtistChips( baseItem: songBaseItemDto, color: IconTheme.of(context).color!.withOpacity(0.1), @@ -72,24 +72,21 @@ class SongNameContent extends StatelessWidget { : ValueKey("${songBaseItemDto!.albumArtist}-artist"), ), ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: AlbumChip( - item: songBaseItemDto, - color: IconTheme.of(context).color!.withOpacity(0.1), - key: songBaseItemDto?.album == null - ? null - : ValueKey("${songBaseItemDto!.album}-album"), - ), + FavoriteButton( + item: songBaseItemDto, + onToggle: (isFavorite) { + songBaseItemDto!.userData!.isFavorite = isFavorite; + currentTrack.item.extras!["itemJson"] = songBaseItemDto.toJson(); + }, ), ], ), - FavoriteButton( + AlbumChip( item: songBaseItemDto, - onToggle: (isFavorite) { - songBaseItemDto!.userData!.isFavorite = isFavorite; - currentTrack.item.extras!["itemJson"] = songBaseItemDto.toJson(); - }, + color: IconTheme.of(context).color!.withOpacity(0.1), + key: songBaseItemDto?.album == null + ? null + : ValueKey("${songBaseItemDto!.album}-album"), ), ], ), From d95f0412f8334ab551ee1915ef859df77471d4fb Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 19 Nov 2023 00:14:07 +0100 Subject: [PATCH 121/130] fix artist overflow in queue list --- lib/components/PlayerScreen/queue_list.dart | 24 +++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index ff4c776d2..7c83188ff 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -821,17 +821,19 @@ class _CurrentTrackState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - processArtist( - currentTrack!.item.artist, - context), - style: TextStyle( - color: Colors.white - .withOpacity(0.85), - fontSize: 13, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w300, - overflow: TextOverflow.ellipsis), + Expanded( + child: Text( + processArtist( + currentTrack!.item.artist, + context), + style: TextStyle( + color: Colors.white + .withOpacity(0.85), + fontSize: 13, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w300, + overflow: TextOverflow.ellipsis), + ), ), Row( children: [ From 7ee2b9720a408d0ecce07bb6b8aba665a0bac3f4 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 19 Nov 2023 00:21:44 +0100 Subject: [PATCH 122/130] fix overflow in player screen appbar title (source info) --- .../player_screen_appbar_title.dart | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/lib/components/PlayerScreen/player_screen_appbar_title.dart b/lib/components/PlayerScreen/player_screen_appbar_title.dart index 00c7df645..cf818a81b 100644 --- a/lib/components/PlayerScreen/player_screen_appbar_title.dart +++ b/lib/components/PlayerScreen/player_screen_appbar_title.dart @@ -31,29 +31,34 @@ class _PlayerScreenAppBarTitleState extends State { builder: (context, snapshot) { final queueItem = snapshot.data!; - return Baseline( - baselineType: TextBaseline.alphabetic, - baseline: 0, - child: GestureDetector( - onTap: () => navigateToSource(context, queueItem.source), - child: Column( - children: [ - Text(AppLocalizations.of(context)!.playingFromType(queueItem.source.type.toString()), - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w300, - color: Theme.of(context).brightness == Brightness.dark ? Colors.white.withOpacity(0.7) : Colors.black.withOpacity(0.8), + return Container( + constraints: const BoxConstraints(maxWidth: 235), + child: Baseline( + baselineType: TextBaseline.alphabetic, + baseline: 0, + child: GestureDetector( + onTap: () => navigateToSource(context, queueItem.source), + child: Column( + children: [ + Text(AppLocalizations.of(context)!.playingFromType(queueItem.source.type.toString()), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w300, + color: Theme.of(context).brightness == Brightness.dark ? Colors.white.withOpacity(0.7) : Colors.black.withOpacity(0.8), + ), + overflow: TextOverflow.ellipsis, ), - ), - const Padding(padding: EdgeInsets.symmetric(vertical: 2)), - Text( - queueItem.source.name.getLocalized(context), - style: TextStyle( - fontSize: 16, - color: Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black.withOpacity(0.9), + const Padding(padding: EdgeInsets.symmetric(vertical: 2)), + Text( + queueItem.source.name.getLocalized(context), + style: TextStyle( + fontSize: 16, + color: Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black.withOpacity(0.9), + ), + maxLines: 2, ), - ), - ], + ], + ), ), ), ); From cb91d6fbb61cf2f586ab38346a294e817427cdfa Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 19 Nov 2023 00:35:19 +0100 Subject: [PATCH 123/130] group recent playbacks by minute in history --- .../playback_history_list.dart | 10 +++++++--- lib/services/playback_history_service.dart | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/components/PlaybackHistoryScreen/playback_history_list.dart b/lib/components/PlaybackHistoryScreen/playback_history_list.dart index ee036e7fb..7866b2b48 100644 --- a/lib/components/PlaybackHistoryScreen/playback_history_list.dart +++ b/lib/components/PlaybackHistoryScreen/playback_history_list.dart @@ -29,7 +29,7 @@ class PlaybackHistoryList extends StatelessWidget { history = snapshot.data; // groupedHistory = playbackHistoryService.getHistoryGroupedByDate(); // groupedHistory = playbackHistoryService.getHistoryGroupedByHour(); - groupedHistory = playbackHistoryService.getHistoryGroupedByDateOrHourDynamic(); + groupedHistory = playbackHistoryService.getHistoryGroupedDynamically(); print(groupedHistory); @@ -86,8 +86,12 @@ class PlaybackHistoryList extends StatelessWidget { padding: const EdgeInsets.only( left: 16.0, top: 8.0, bottom: 4.0), child: Text( - (group.key.year == now.year && group.key.month == now.month && group.key.day == now.day ) ? - DateFormat.j(localeString).format(group.key) : + (group.key.year == now.year && group.key.month == now.month && group.key.day == now.day) ? + ( + group.key.hour == now.hour ? + DateFormat.jm(localeString).format(group.key) : + DateFormat.j(localeString).format(group.key) + ) : DateFormat.MMMMd(localeString).format(group.key) , style: const TextStyle( diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart index 9a80cd849..ce2f9ba81 100644 --- a/lib/services/playback_history_service.dart +++ b/lib/services/playback_history_service.dart @@ -129,10 +129,20 @@ class PlaybackHistoryService { BehaviorSubject> get historyStream => _historyStream; /// method that converts history into a list grouped by date - List>> getHistoryGroupedByDateOrHourDynamic() { + List>> getHistoryGroupedDynamically() { byDateGroupingConstructor(FinampHistoryItem element) { final now = DateTime.now(); - if (now.year == element.startTime.year && now.month == element.startTime.month && now.day == element.startTime.day) { + if (now.year == element.startTime.year && now.month == element.startTime.month && now.day == element.startTime.day && now.hour == element.startTime.hour) { + // group by minute + return DateTime( + element.startTime.year, + element.startTime.month, + element.startTime.day, + element.startTime.hour, + element.startTime.minute, + ); + } + else if (now.year == element.startTime.year && now.month == element.startTime.month && now.day == element.startTime.day) { // group by hour return DateTime( element.startTime.year, From c063cb91ab90d4542f5956eda022ee0a892439a2 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 19 Nov 2023 00:35:59 +0100 Subject: [PATCH 124/130] fix metastable first item when starting playback for the first time --- lib/services/queue_service.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 11fbcfa0c..a515157ee 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -259,6 +259,7 @@ class QueueService { await _audioHandler.stop(); _queueAudioSource.clear(); + await _audioHandler.initializeAudioSource(_queueAudioSource); List audioSources = []; @@ -274,7 +275,7 @@ class QueueService { _queueAudioSourceIndex = _queueAudioSource.shuffleIndices[initialIndex]; } _audioHandler.setNextInitialIndex(_queueAudioSourceIndex); - await _audioHandler.initializeAudioSource(_queueAudioSource); + // await _audioHandler.initializeAudioSource(_queueAudioSource); newShuffledOrder = List.from(_queueAudioSource.shuffleIndices); From 81f2e53e9f3340d2231a08bbff369b28fa97c47f Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 19 Nov 2023 00:54:28 +0100 Subject: [PATCH 125/130] fix layout issues --- lib/components/PlayerScreen/artist_chip.dart | 93 +++++++++----------- lib/components/PlayerScreen/song_info.dart | 24 +++-- 2 files changed, 53 insertions(+), 64 deletions(-) diff --git a/lib/components/PlayerScreen/artist_chip.dart b/lib/components/PlayerScreen/artist_chip.dart index b487b5c77..a7e2c819d 100644 --- a/lib/components/PlayerScreen/artist_chip.dart +++ b/lib/components/PlayerScreen/artist_chip.dart @@ -30,27 +30,25 @@ class ArtistChips extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), - child: Flexible( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Wrap( - spacing: 4.0, - runSpacing: 4.0, - children: List.generate(baseItem?.artistItems?.length ?? 0, (index) { - - final currentArtist = baseItem!.artistItems![index]; - - return ArtistChip( - color: color, - artist: BaseItemDto( - id: currentArtist.id, - name: currentArtist.name, - type: "MusicArtist", - ), - - ); - }), - ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Wrap( + spacing: 4.0, + runSpacing: 4.0, + children: List.generate(baseItem?.artistItems?.length ?? 0, (index) { + + final currentArtist = baseItem!.artistItems![index]; + + return ArtistChip( + color: color, + artist: BaseItemDto( + id: currentArtist.id, + name: currentArtist.name, + type: "MusicArtist", + ), + + ); + }), ), ), ); @@ -137,39 +135,32 @@ class _ArtistChipContent extends StatelessWidget { : () => Navigator.of(context) .popAndPushNamed(ArtistScreen.routeName, arguments: item), borderRadius: _borderRadius, - child: Flexible( - fit: FlexFit.loose, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (item.isArtist && item.imageId != null) - AlbumImage( - item: item, - borderRadius: const BorderRadius.only( - topLeft: _radius, - bottomLeft: _radius, - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (item.isArtist && item.imageId != null) + AlbumImage( + item: item, + borderRadius: const BorderRadius.only( + topLeft: _radius, + bottomLeft: _radius, ), - Flexible( - fit: FlexFit.loose, - flex: 1, - child: Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 220), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: Text( - name ?? AppLocalizations.of(context)!.unknownArtist, - style: _textStyle, - softWrap: false, - overflow: TextOverflow.ellipsis, - ), - ), + ), + Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 220), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Text( + name ?? AppLocalizations.of(context)!.unknownArtist, + style: _textStyle, + softWrap: false, + overflow: TextOverflow.ellipsis, ), ), - ) - ], - ), + ), + ) + ], ), ), ), diff --git a/lib/components/PlayerScreen/song_info.dart b/lib/components/PlayerScreen/song_info.dart index d45aee117..ab7831a16 100644 --- a/lib/components/PlayerScreen/song_info.dart +++ b/lib/components/PlayerScreen/song_info.dart @@ -101,19 +101,17 @@ class _SongInfoState extends State { separatedArtistTextSpans.removeLast(); } - return Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _PlayerScreenAlbumImage(queueItem: currentTrack), - SongNameContent( - currentTrack: currentTrack, - separatedArtistTextSpans: separatedArtistTextSpans, - secondaryTextColour: secondaryTextColour, - ) - ], - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _PlayerScreenAlbumImage(queueItem: currentTrack), + SongNameContent( + currentTrack: currentTrack, + separatedArtistTextSpans: separatedArtistTextSpans, + secondaryTextColour: secondaryTextColour, + ) + ], ); }, ); From fdeb78180792dbffbc02d4a27e2ec52c70daf6b0 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 19 Nov 2023 01:25:13 +0100 Subject: [PATCH 126/130] light theme contrast fixes --- .../playback_history_list_tile.dart | 4 ++-- lib/components/PlayerScreen/queue_list.dart | 20 +++++++++---------- .../PlayerScreen/queue_list_item.dart | 4 ++-- lib/components/now_playing_bar.dart | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart b/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart index 449c550b1..df99e1ec9 100644 --- a/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart +++ b/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart @@ -83,8 +83,8 @@ class _PlaybackHistoryListTileState extends State { padding: const EdgeInsets.only(top: 6.0), child: Text( processArtist(widget.item.item.item.artist, context), - style: const TextStyle( - color: Colors.white70, + style: TextStyle( + color: Theme.of(context).textTheme.bodyMedium!.color!, fontSize: 13, fontFamily: 'Lexend Deca', fontWeight: FontWeight.w300, diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 7c83188ff..a91faf069 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -309,14 +309,14 @@ Future showQueueBottomSheet(BuildContext context) { width: 40, height: 3.5, decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).textTheme.bodySmall!.color!, borderRadius: BorderRadius.circular(3.5), ), ), const SizedBox(height: 10), Text(AppLocalizations.of(context)!.queue, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: Theme.of(context).textTheme.bodyLarge!.color!, fontFamily: 'Lexend Deca', fontSize: 18, fontWeight: FontWeight.w300)), @@ -694,13 +694,13 @@ class _CurrentTrackState extends State { flexibleSpace: Container( // width: 58, height: albumImageSize, - padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + padding: const EdgeInsets.symmetric(horizontal: horizontalPadding), child: Container( clipBehavior: Clip.antiAlias, decoration: ShapeDecoration( color: Color.alphaBlend( - IconTheme.of(context).color!.withOpacity(0.35), - Colors.black), + Theme.of(context).brightness == Brightness.dark ? IconTheme.of(context).color!.withOpacity(0.35) : IconTheme.of(context).color!.withOpacity(0.5), + Theme.of(context).brightness == Brightness.dark ? Colors.black : Colors.white), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(12.0)), ), @@ -1414,16 +1414,16 @@ class PreviousTracksSectionHeader extends SliverPersistentHeaderDelegate { stream: isRecentTracksExpanded, builder: (context, snapshot) { if (snapshot.hasData && snapshot.data!) { - return const Icon( + return Icon( TablerIcons.chevron_up, size: 28.0, - color: Colors.white, + color: Theme.of(context).iconTheme.color!, ); } else { - return const Icon( + return Icon( TablerIcons.chevron_down, size: 28.0, - color: Colors.white, + color: Theme.of(context).iconTheme.color!, ); } } diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index 3060a03f1..002ee07d0 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -116,8 +116,8 @@ class _QueueListItemState extends State padding: const EdgeInsets.only(top: 6.0), child: Text( processArtist(widget.item.item.artist, context), - style: const TextStyle( - color: Colors.white70, + style: TextStyle( + color: Theme.of(context).textTheme.bodyMedium!.color!, fontSize: 13, fontFamily: 'Lexend Deca', fontWeight: FontWeight.w300, diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index 3c7b9d876..1b044d529 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -117,8 +117,8 @@ class NowPlayingBar extends ConsumerWidget { clipBehavior: Clip.antiAlias, decoration: ShapeDecoration( color: Color.alphaBlend( - IconTheme.of(context).color!.withOpacity(0.35), - Colors.black), + Theme.of(context).brightness == Brightness.dark ? IconTheme.of(context).color!.withOpacity(0.35) : IconTheme.of(context).color!.withOpacity(0.5), + Theme.of(context).brightness == Brightness.dark ? Colors.black : Colors.white), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(12.0)), ), From 258f183315bb4fd1f8b17bfb2dc1c533c974ea99 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 19 Nov 2023 01:35:35 +0100 Subject: [PATCH 127/130] format changed files --- lib/at_contrast.dart | 3 +- ...bum_screen_content_flexible_space_bar.dart | 221 +++--- .../AlbumScreen/song_list_tile.dart | 36 +- lib/components/MusicScreen/album_item.dart | 228 +++--- .../MusicScreen/artist_item_list_tile.dart | 13 +- .../MusicScreen/music_screen_tab_view.dart | 12 +- .../playback_history_list.dart | 29 +- .../playback_history_list_tile.dart | 10 +- lib/components/PlayerScreen/artist_chip.dart | 25 +- .../PlayerScreen/finamp_back_button_icon.dart | 3 +- .../PlayerScreen/player_buttons_more.dart | 54 +- .../player_buttons_repeating.dart | 43 +- .../PlayerScreen/player_buttons_shuffle.dart | 5 +- .../player_screen_appbar_title.dart | 17 +- .../PlayerScreen/progress_slider.dart | 10 +- lib/components/PlayerScreen/queue_list.dart | 204 ++--- .../PlayerScreen/queue_list_item.dart | 15 +- .../PlayerScreen/queue_source_helper.dart | 31 +- .../PlayerScreen/sleep_timer_button.dart | 32 +- lib/components/PlayerScreen/song_info.dart | 26 +- .../PlayerScreen/song_name_content.dart | 14 +- lib/components/favourite_button.dart | 1 - lib/components/now_playing_bar.dart | 694 +++++++++++------- lib/main.dart | 11 +- .../blurred_player_screen_background.dart | 3 +- lib/screens/music_screen.dart | 5 +- lib/services/audio_service_helper.dart | 49 +- lib/services/jellyfin_api.dart | 6 +- .../music_player_background_task.dart | 10 +- lib/services/playback_history_service.dart | 144 ++-- lib/services/queue_service.dart | 77 +- 31 files changed, 1166 insertions(+), 865 deletions(-) diff --git a/lib/at_contrast.dart b/lib/at_contrast.dart index 8737ebc9a..6f98007a6 100644 --- a/lib/at_contrast.dart +++ b/lib/at_contrast.dart @@ -39,7 +39,8 @@ extension AtContrast on Color { maxLightness = hslColor.lightness; } - final lightDiff = lighter ? maxLightness - minLightness : minLightness - maxLightness; + final lightDiff = + lighter ? maxLightness - minLightness : minLightness - maxLightness; hslColor = hslColor.withLightness(hslColor.lightness + lightDiff / 2); } diff --git a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart index 99f658540..fc55a7e57 100644 --- a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart +++ b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart @@ -25,67 +25,86 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { @override Widget build(BuildContext context) { - GetIt.instance(); - QueueService queueService = - GetIt.instance(); + GetIt.instance(); + QueueService queueService = GetIt.instance(); void playAlbum() { queueService.startPlayback( - items: items, - source: QueueItemSource( - type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), - id: parentItem.id, - item: parentItem, - ), - order: FinampPlaybackOrder.linear, + items: items, + source: QueueItemSource( + type: isPlaylist + ? QueueItemSourceType.playlist + : QueueItemSourceType.album, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: parentItem.name ?? + AppLocalizations.of(context)!.placeholderSource), + id: parentItem.id, + item: parentItem, + ), + order: FinampPlaybackOrder.linear, ); } void shuffleAlbum() { queueService.startPlayback( - items: items, - source: QueueItemSource( - type: isPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), - id: parentItem.id, - item: parentItem, - ), - order: FinampPlaybackOrder.shuffled, + items: items, + source: QueueItemSource( + type: isPlaylist + ? QueueItemSourceType.playlist + : QueueItemSourceType.album, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: parentItem.name ?? + AppLocalizations.of(context)!.placeholderSource), + id: parentItem.id, + item: parentItem, + ), + order: FinampPlaybackOrder.shuffled, ); } void addAlbumToNextUp() { queueService.addToNextUp( - items: items, - source: QueueItemSource( - type: isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), - id: parentItem.id, - item: parentItem, - ), + items: items, + source: QueueItemSource( + type: isPlaylist + ? QueueItemSourceType.nextUpPlaylist + : QueueItemSourceType.nextUpAlbum, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: parentItem.name ?? + AppLocalizations.of(context)!.placeholderSource), + id: parentItem.id, + item: parentItem, + ), ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(AppLocalizations.of(context)!.confirmAddToNextUp(isPlaylist ? "playlist" : "album")), + content: Text(AppLocalizations.of(context)! + .confirmAddToNextUp(isPlaylist ? "playlist" : "album")), ), ); - } void addAlbumNext() { queueService.addNext( items: items, source: QueueItemSource( - type: isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), + type: isPlaylist + ? QueueItemSourceType.nextUpPlaylist + : QueueItemSourceType.nextUpAlbum, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: parentItem.name ?? + AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, - ) - ); + )); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(AppLocalizations.of(context)!.confirmPlayNext(isPlaylist ? "playlist" : "album")), + content: Text(AppLocalizations.of(context)! + .confirmPlayNext(isPlaylist ? "playlist" : "album")), ), ); } @@ -97,12 +116,16 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { queueService.addToNextUp( items: clonedItems, source: QueueItemSource( - type: isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), + type: isPlaylist + ? QueueItemSourceType.nextUpPlaylist + : QueueItemSourceType.nextUpAlbum, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: parentItem.name ?? + AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, - ) - ); + )); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(AppLocalizations.of(context)!.confirmShuffleToNextUp), @@ -117,12 +140,16 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { queueService.addNext( items: clonedItems, source: QueueItemSource( - type: isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), + type: isPlaylist + ? QueueItemSourceType.nextUpPlaylist + : QueueItemSourceType.nextUpAlbum, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: parentItem.name ?? + AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, - ) - ); + )); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(AppLocalizations.of(context)!.confirmShuffleNext), @@ -132,20 +159,25 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { void addAlbumToQueue() { queueService.addToQueue( - items: items, - source: QueueItemSource( - type: isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), - id: parentItem.id, - item: parentItem, - ), + items: items, + source: QueueItemSource( + type: isPlaylist + ? QueueItemSourceType.nextUpPlaylist + : QueueItemSourceType.nextUpAlbum, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: parentItem.name ?? + AppLocalizations.of(context)!.placeholderSource), + id: parentItem.id, + item: parentItem, + ), ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(AppLocalizations.of(context)!.confirmAddToQueue(isPlaylist ? "playlist" : "album")), + content: Text(AppLocalizations.of(context)! + .confirmAddToQueue(isPlaylist ? "playlist" : "album")), ), ); - } void shuffleAlbumToQueue() { @@ -155,12 +187,16 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { queueService.addToQueue( items: clonedItems, source: QueueItemSource( - type: isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: parentItem.name ?? AppLocalizations.of(context)!.placeholderSource), + type: isPlaylist + ? QueueItemSourceType.nextUpPlaylist + : QueueItemSourceType.nextUpAlbum, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: parentItem.name ?? + AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, - ) - ); + )); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(AppLocalizations.of(context)!.confirmShuffleToQueue), @@ -202,78 +238,91 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { Row(children: [ Expanded( child: ElevatedButton.icon( - onPressed: () => playAlbum(), - icon: const Icon(Icons.play_arrow), - label: - Text(AppLocalizations.of(context)!.playButtonLabel), - ), + onPressed: () => playAlbum(), + icon: const Icon(Icons.play_arrow), + label: Text( + AppLocalizations.of(context)!.playButtonLabel), + ), ), - const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8)), Expanded( child: ElevatedButton.icon( onPressed: () => shuffleAlbum(), icon: const Icon(Icons.shuffle), - label: Text( - AppLocalizations.of(context)!.shuffleButtonLabel), + label: Text(AppLocalizations.of(context)! + .shuffleButtonLabel), ), ), ]), Row(children: [ Expanded( child: ElevatedButton.icon( - style: const ButtonStyle(visualDensity: VisualDensity.compact), - onPressed: () => addAlbumNext(), - icon: const Icon(Icons.hourglass_bottom), - label: - Text(AppLocalizations.of(context)!.playNext), - ), + style: const ButtonStyle( + visualDensity: VisualDensity.compact), + onPressed: () => addAlbumNext(), + icon: const Icon(Icons.hourglass_bottom), + label: Text(AppLocalizations.of(context)!.playNext), + ), ), - const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8)), Expanded( child: ElevatedButton.icon( - style: const ButtonStyle(visualDensity: VisualDensity.compact), - onPressed: () => shuffleAlbumNext(), - icon: const Icon(Icons.hourglass_bottom), - label: - Text(AppLocalizations.of(context)!.shuffleNext), - ), + style: const ButtonStyle( + visualDensity: VisualDensity.compact), + onPressed: () => shuffleAlbumNext(), + icon: const Icon(Icons.hourglass_bottom), + label: + Text(AppLocalizations.of(context)!.shuffleNext), + ), ), ]), Row(children: [ Expanded( child: ElevatedButton.icon( - style: const ButtonStyle(visualDensity: VisualDensity.compact), + style: const ButtonStyle( + visualDensity: VisualDensity.compact), onPressed: () => addAlbumToNextUp(), icon: const Icon(Icons.hourglass_top), - label: Text(AppLocalizations.of(context)!.addToNextUp), + label: + Text(AppLocalizations.of(context)!.addToNextUp), ), ), - const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8)), Expanded( child: ElevatedButton.icon( - style: const ButtonStyle(visualDensity: VisualDensity.compact), + style: const ButtonStyle( + visualDensity: VisualDensity.compact), onPressed: () => shuffleAlbumToNextUp(), icon: const Icon(Icons.hourglass_top), - label: Text(AppLocalizations.of(context)!.shuffleToNextUp), + label: Text( + AppLocalizations.of(context)!.shuffleToNextUp), ), ), ]), Row(children: [ Expanded( child: ElevatedButton.icon( - style: const ButtonStyle(visualDensity: VisualDensity.compact), + style: const ButtonStyle( + visualDensity: VisualDensity.compact), onPressed: () => addAlbumToQueue(), icon: const Icon(Icons.queue_music), - label: Text(AppLocalizations.of(context)!.addToQueue), + label: + Text(AppLocalizations.of(context)!.addToQueue), ), ), - const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8)), Expanded( child: ElevatedButton.icon( - style: const ButtonStyle(visualDensity: VisualDensity.compact), + style: const ButtonStyle( + visualDensity: VisualDensity.compact), onPressed: () => shuffleAlbumToQueue(), icon: const Icon(Icons.queue_music), - label: Text(AppLocalizations.of(context)!.shuffleToQueue), + label: Text( + AppLocalizations.of(context)!.shuffleToQueue), ), ), ]), @@ -286,7 +335,5 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { ), ), ); - } - } diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index 433ce4f57..cfd2655d0 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -190,8 +190,15 @@ class _SongListTileState extends State { _queueService.startPlayback( items: widget.children!, source: QueueItemSource( - type: widget.isInPlaylist ? QueueItemSourceType.playlist : QueueItemSourceType.album, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: (widget.isInPlaylist ? widget.parentName : widget.item.album) ?? AppLocalizations.of(context)!.placeholderSource), + type: widget.isInPlaylist + ? QueueItemSourceType.playlist + : QueueItemSourceType.album, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: (widget.isInPlaylist + ? widget.parentName + : widget.item.album) ?? + AppLocalizations.of(context)!.placeholderSource), id: widget.parentId ?? "", item: widget.item, ), @@ -318,7 +325,14 @@ class _SongListTileState extends State { switch (selection) { case SongListTileMenuItems.addToQueue: - await _queueService.addToQueue(items: [widget.item], source: QueueItemSource(type: QueueItemSourceType.unknown, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: AppLocalizations.of(context)!.queue), id: widget.parentId ?? "unknown")); + await _queueService.addToQueue( + items: [widget.item], + source: QueueItemSource( + type: QueueItemSourceType.unknown, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: AppLocalizations.of(context)!.queue), + id: widget.parentId ?? "unknown")); if (!mounted) return; @@ -333,7 +347,8 @@ class _SongListTileState extends State { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.confirmPlayNext("track")), + content: + Text(AppLocalizations.of(context)!.confirmPlayNext("track")), )); break; @@ -343,7 +358,8 @@ class _SongListTileState extends State { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.confirmAddToNextUp("track")), + content: Text( + AppLocalizations.of(context)!.confirmAddToNextUp("track")), )); break; @@ -454,7 +470,15 @@ class _SongListTileState extends State { ), ), confirmDismiss: (direction) async { - await _queueService.addToQueue(items: [widget.item], source: QueueItemSource(type: QueueItemSourceType.unknown, name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: AppLocalizations.of(context)!.queue), id: widget.parentId!)); + await _queueService.addToQueue( + items: [widget.item], + source: QueueItemSource( + type: QueueItemSourceType.unknown, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: + AppLocalizations.of(context)!.queue), + id: widget.parentId!)); if (!mounted) return false; diff --git a/lib/components/MusicScreen/album_item.dart b/lib/components/MusicScreen/album_item.dart index 89b6d1950..7a0c79fd7 100644 --- a/lib/components/MusicScreen/album_item.dart +++ b/lib/components/MusicScreen/album_item.dart @@ -72,8 +72,7 @@ class AlbumItem extends StatefulWidget { class _AlbumItemState extends State { late BaseItemDto mutableAlbum; - QueueService get _queueService => - GetIt.instance(); + QueueService get _queueService => GetIt.instance(); late Function() onTap; late AppLocalizations local; @@ -100,10 +99,9 @@ class _AlbumItemState extends State { @override Widget build(BuildContext context) { - local = AppLocalizations.of(context)!; messenger = ScaffoldMessenger.of(context); - + final screenSize = MediaQuery.of(context).size; return Padding( @@ -136,8 +134,7 @@ class _AlbumItemState extends State { value: _AlbumListTileMenuItems.removeFavourite, child: ListTile( leading: const Icon(Icons.favorite_border), - title: - Text(local.removeFavourite), + title: Text(local.removeFavourite), ), ) : PopupMenuItem<_AlbumListTileMenuItems>( @@ -152,8 +149,7 @@ class _AlbumItemState extends State { value: _AlbumListTileMenuItems.removeFromMixList, child: ListTile( leading: const Icon(Icons.explore_off), - title: - Text(local.removeFromMix), + title: Text(local.removeFromMix), ), ) : PopupMenuItem<_AlbumListTileMenuItems>( @@ -168,16 +164,14 @@ class _AlbumItemState extends State { value: _AlbumListTileMenuItems.playNext, child: ListTile( leading: const Icon(Icons.hourglass_bottom), - title: - Text(local.playNext), + title: Text(local.playNext), ), ), PopupMenuItem<_AlbumListTileMenuItems>( value: _AlbumListTileMenuItems.addToNextUp, child: ListTile( leading: const Icon(Icons.hourglass_top), - title: - Text(local.addToNextUp), + title: Text(local.addToNextUp), ), ), if (_queueService.getQueue().nextUp.isNotEmpty) @@ -185,32 +179,28 @@ class _AlbumItemState extends State { value: _AlbumListTileMenuItems.shuffleNext, child: ListTile( leading: const Icon(Icons.hourglass_bottom), - title: - Text(local.shuffleNext), + title: Text(local.shuffleNext), ), ), PopupMenuItem<_AlbumListTileMenuItems>( value: _AlbumListTileMenuItems.shuffleToNextUp, child: ListTile( leading: const Icon(Icons.hourglass_top), - title: - Text(local.shuffleToNextUp), + title: Text(local.shuffleToNextUp), ), ), PopupMenuItem<_AlbumListTileMenuItems>( value: _AlbumListTileMenuItems.addToQueue, child: ListTile( leading: const Icon(Icons.queue_music), - title: - Text(local.addToQueue), + title: Text(local.addToQueue), ), ), PopupMenuItem<_AlbumListTileMenuItems>( value: _AlbumListTileMenuItems.shuffleToQueue, child: ListTile( leading: const Icon(Icons.queue_music), - title: - Text(local.shuffleToQueue), + title: Text(local.shuffleToQueue), ), ), ], @@ -270,7 +260,8 @@ class _AlbumItemState extends State { break; case _AlbumListTileMenuItems.playNext: try { - List? albumTracks = await jellyfinApiHelper.getItems( + List? albumTracks = + await jellyfinApiHelper.getItems( parentItem: mutableAlbum, isGenres: false, sortBy: "ParentIndexNumber,IndexNumber,SortName", @@ -280,28 +271,34 @@ class _AlbumItemState extends State { if (albumTracks == null) { messenger.showSnackBar( SnackBar( - content: Text("Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), + content: Text( + "Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), ), ); return; - } + } _queueService.addNext( - items: albumTracks, - source: QueueItemSource( - type: widget.isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: mutableAlbum.name ?? local.placeholderSource), - id: mutableAlbum.id, - item: mutableAlbum, - ) - ); + items: albumTracks, + source: QueueItemSource( + type: widget.isPlaylist + ? QueueItemSourceType.nextUpPlaylist + : QueueItemSourceType.nextUpAlbum, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: + mutableAlbum.name ?? local.placeholderSource), + id: mutableAlbum.id, + item: mutableAlbum, + )); messenger.showSnackBar( SnackBar( - content: Text(local.confirmPlayNext(widget.isPlaylist ? "playlist" : "album")), + content: Text(local.confirmPlayNext( + widget.isPlaylist ? "playlist" : "album")), ), ); - + setState(() {}); } catch (e) { errorSnackbar(e, context); @@ -309,7 +306,8 @@ class _AlbumItemState extends State { break; case _AlbumListTileMenuItems.addToNextUp: try { - List? albumTracks = await jellyfinApiHelper.getItems( + List? albumTracks = + await jellyfinApiHelper.getItems( parentItem: mutableAlbum, isGenres: false, sortBy: "ParentIndexNumber,IndexNumber,SortName", @@ -319,28 +317,34 @@ class _AlbumItemState extends State { if (albumTracks == null) { messenger.showSnackBar( SnackBar( - content: Text("Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), + content: Text( + "Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), ), ); return; - } + } _queueService.addToNextUp( - items: albumTracks, - source: QueueItemSource( - type: widget.isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: mutableAlbum.name ?? local.placeholderSource), - id: mutableAlbum.id, - item: mutableAlbum, - ) - ); + items: albumTracks, + source: QueueItemSource( + type: widget.isPlaylist + ? QueueItemSourceType.nextUpPlaylist + : QueueItemSourceType.nextUpAlbum, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: + mutableAlbum.name ?? local.placeholderSource), + id: mutableAlbum.id, + item: mutableAlbum, + )); messenger.showSnackBar( SnackBar( - content: Text(local.confirmAddToNextUp(widget.isPlaylist ? "playlist" : "album")), + content: Text(local.confirmAddToNextUp( + widget.isPlaylist ? "playlist" : "album")), ), ); - + setState(() {}); } catch (e) { errorSnackbar(e, context); @@ -348,7 +352,8 @@ class _AlbumItemState extends State { break; case _AlbumListTileMenuItems.shuffleNext: try { - List? albumTracks = await jellyfinApiHelper.getItems( + List? albumTracks = + await jellyfinApiHelper.getItems( parentItem: mutableAlbum, isGenres: false, sortBy: "Random", @@ -358,28 +363,34 @@ class _AlbumItemState extends State { if (albumTracks == null) { messenger.showSnackBar( SnackBar( - content: Text("Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), + content: Text( + "Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), ), ); return; - } + } _queueService.addNext( - items: albumTracks, - source: QueueItemSource( - type: widget.isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: mutableAlbum.name ?? local.placeholderSource), - id: mutableAlbum.id, - item: mutableAlbum, - ) - ); + items: albumTracks, + source: QueueItemSource( + type: widget.isPlaylist + ? QueueItemSourceType.nextUpPlaylist + : QueueItemSourceType.nextUpAlbum, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: + mutableAlbum.name ?? local.placeholderSource), + id: mutableAlbum.id, + item: mutableAlbum, + )); messenger.showSnackBar( SnackBar( - content: Text(local.confirmPlayNext(widget.isPlaylist ? "playlist" : "album")), + content: Text(local.confirmPlayNext( + widget.isPlaylist ? "playlist" : "album")), ), ); - + setState(() {}); } catch (e) { errorSnackbar(e, context); @@ -387,38 +398,45 @@ class _AlbumItemState extends State { break; case _AlbumListTileMenuItems.shuffleToNextUp: try { - List? albumTracks = await jellyfinApiHelper.getItems( + List? albumTracks = + await jellyfinApiHelper.getItems( parentItem: mutableAlbum, isGenres: false, - sortBy: "Random", //TODO this isn't working anymore with Jellyfin 10.9 (unstable) + sortBy: + "Random", //TODO this isn't working anymore with Jellyfin 10.9 (unstable) includeItemTypes: "Audio", ); if (albumTracks == null) { messenger.showSnackBar( SnackBar( - content: Text("Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), + content: Text( + "Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), ), ); return; - } + } _queueService.addToNextUp( - items: albumTracks, - source: QueueItemSource( - type: widget.isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: mutableAlbum.name ?? local.placeholderSource), - id: mutableAlbum.id, - item: mutableAlbum, - ) - ); + items: albumTracks, + source: QueueItemSource( + type: widget.isPlaylist + ? QueueItemSourceType.nextUpPlaylist + : QueueItemSourceType.nextUpAlbum, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: + mutableAlbum.name ?? local.placeholderSource), + id: mutableAlbum.id, + item: mutableAlbum, + )); messenger.showSnackBar( SnackBar( content: Text(local.confirmShuffleToNextUp), ), ); - + setState(() {}); } catch (e) { errorSnackbar(e, context); @@ -426,7 +444,8 @@ class _AlbumItemState extends State { break; case _AlbumListTileMenuItems.addToQueue: try { - List? albumTracks = await jellyfinApiHelper.getItems( + List? albumTracks = + await jellyfinApiHelper.getItems( parentItem: mutableAlbum, isGenres: false, sortBy: "ParentIndexNumber,IndexNumber,SortName", @@ -436,28 +455,34 @@ class _AlbumItemState extends State { if (albumTracks == null) { messenger.showSnackBar( SnackBar( - content: Text("Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), + content: Text( + "Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), ), ); return; - } + } _queueService.addToQueue( - items: albumTracks, - source: QueueItemSource( - type: widget.isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: mutableAlbum.name ?? local.placeholderSource), - id: mutableAlbum.id, - item: mutableAlbum, - ) - ); + items: albumTracks, + source: QueueItemSource( + type: widget.isPlaylist + ? QueueItemSourceType.nextUpPlaylist + : QueueItemSourceType.nextUpAlbum, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: + mutableAlbum.name ?? local.placeholderSource), + id: mutableAlbum.id, + item: mutableAlbum, + )); messenger.showSnackBar( SnackBar( - content: Text(local.confirmAddToQueue(widget.isPlaylist ? "playlist" : "album")), + content: Text(local.confirmAddToQueue( + widget.isPlaylist ? "playlist" : "album")), ), ); - + setState(() {}); } catch (e) { errorSnackbar(e, context); @@ -465,7 +490,8 @@ class _AlbumItemState extends State { break; case _AlbumListTileMenuItems.shuffleToQueue: try { - List? albumTracks = await jellyfinApiHelper.getItems( + List? albumTracks = + await jellyfinApiHelper.getItems( parentItem: mutableAlbum, isGenres: false, sortBy: "Random", @@ -475,28 +501,34 @@ class _AlbumItemState extends State { if (albumTracks == null) { messenger.showSnackBar( SnackBar( - content: Text("Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), + content: Text( + "Couldn't load ${widget.isPlaylist ? "playlist" : "album"}."), ), ); return; - } + } _queueService.addToQueue( - items: albumTracks, - source: QueueItemSource( - type: widget.isPlaylist ? QueueItemSourceType.nextUpPlaylist : QueueItemSourceType.nextUpAlbum, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: mutableAlbum.name ?? local.placeholderSource), - id: mutableAlbum.id, - item: mutableAlbum, - ) - ); + items: albumTracks, + source: QueueItemSource( + type: widget.isPlaylist + ? QueueItemSourceType.nextUpPlaylist + : QueueItemSourceType.nextUpAlbum, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: + mutableAlbum.name ?? local.placeholderSource), + id: mutableAlbum.id, + item: mutableAlbum, + )); messenger.showSnackBar( SnackBar( - content: Text(local.confirmAddToQueue(widget.isPlaylist ? "playlist" : "album")), + content: Text(local.confirmAddToQueue( + widget.isPlaylist ? "playlist" : "album")), ), ); - + setState(() {}); } catch (e) { errorSnackbar(e, context); diff --git a/lib/components/MusicScreen/artist_item_list_tile.dart b/lib/components/MusicScreen/artist_item_list_tile.dart index cb59f8b54..69e2ced84 100644 --- a/lib/components/MusicScreen/artist_item_list_tile.dart +++ b/lib/components/MusicScreen/artist_item_list_tile.dart @@ -51,10 +51,9 @@ class _ArtistListTileState extends State { overflow: TextOverflow.ellipsis, ), subtitle: null, - trailing: - _jellyfinApiHelper.selectedMixArtists.contains(mutableItem.id) - ? const Icon(Icons.explore) - : null, + trailing: _jellyfinApiHelper.selectedMixArtists.contains(mutableItem.id) + ? const Icon(Icons.explore) + : null, ); return GestureDetector( @@ -77,7 +76,8 @@ class _ArtistListTileState extends State { value: ArtistListTileMenuItems.removeFromFavourite, child: ListTile( leading: const Icon(Icons.favorite_border), - title: Text(AppLocalizations.of(context)!.removeFavourite), + title: + Text(AppLocalizations.of(context)!.removeFavourite), ), ) : PopupMenuItem( @@ -93,7 +93,8 @@ class _ArtistListTileState extends State { value: ArtistListTileMenuItems.removeFromMixList, child: ListTile( leading: const Icon(Icons.explore_off), - title: Text(AppLocalizations.of(context)!.removeFromMix), + title: + Text(AppLocalizations.of(context)!.removeFromMix), enabled: isOffline ? false : true, ), ) diff --git a/lib/components/MusicScreen/music_screen_tab_view.dart b/lib/components/MusicScreen/music_screen_tab_view.dart index 1c8975c8b..c6639542a 100644 --- a/lib/components/MusicScreen/music_screen_tab_view.dart +++ b/lib/components/MusicScreen/music_screen_tab_view.dart @@ -306,7 +306,8 @@ class _MusicScreenTabViewState extends State return AlbumItem( album: offlineSortedItems![index], parentType: _getParentType(), - isPlaylist: widget.tabContentType == TabContentType.playlists, + isPlaylist: widget.tabContentType == + TabContentType.playlists, ); } }, @@ -335,7 +336,8 @@ class _MusicScreenTabViewState extends State return AlbumItem( album: offlineSortedItems![index], parentType: _getParentType(), - isPlaylist: widget.tabContentType == TabContentType.playlists, + isPlaylist: widget.tabContentType == + TabContentType.playlists, isGrid: true, gridAddSettingsListener: false, ); @@ -385,7 +387,8 @@ class _MusicScreenTabViewState extends State return AlbumItem( album: item, parentType: _getParentType(), - isPlaylist: widget.tabContentType == TabContentType.playlists, + isPlaylist: widget.tabContentType == + TabContentType.playlists, ); } }, @@ -417,7 +420,8 @@ class _MusicScreenTabViewState extends State return AlbumItem( album: item, parentType: _getParentType(), - isPlaylist: widget.tabContentType == TabContentType.playlists, + isPlaylist: widget.tabContentType == + TabContentType.playlists, isGrid: true, gridAddSettingsListener: false, ); diff --git a/lib/components/PlaybackHistoryScreen/playback_history_list.dart b/lib/components/PlaybackHistoryScreen/playback_history_list.dart index 7866b2b48..3d1e980a7 100644 --- a/lib/components/PlaybackHistoryScreen/playback_history_list.dart +++ b/lib/components/PlaybackHistoryScreen/playback_history_list.dart @@ -29,7 +29,8 @@ class PlaybackHistoryList extends StatelessWidget { history = snapshot.data; // groupedHistory = playbackHistoryService.getHistoryGroupedByDate(); // groupedHistory = playbackHistoryService.getHistoryGroupedByHour(); - groupedHistory = playbackHistoryService.getHistoryGroupedDynamically(); + groupedHistory = + playbackHistoryService.getHistoryGroupedDynamically(); print(groupedHistory); @@ -74,9 +75,11 @@ class PlaybackHistoryList extends StatelessWidget { ); final now = DateTime.now(); - final String localeString = (LocaleHelper.locale != null) ? ((LocaleHelper.locale?.countryCode != null) ? - "${LocaleHelper.locale?.languageCode.toLowerCase()}_${LocaleHelper.locale?.countryCode?.toUpperCase()}" : LocaleHelper.locale.toString()) : - "en_US"; + final String localeString = (LocaleHelper.locale != null) + ? ((LocaleHelper.locale?.countryCode != null) + ? "${LocaleHelper.locale?.languageCode.toLowerCase()}_${LocaleHelper.locale?.countryCode?.toUpperCase()}" + : LocaleHelper.locale.toString()) + : "en_US"; return index == 0 ? Column( @@ -86,14 +89,16 @@ class PlaybackHistoryList extends StatelessWidget { padding: const EdgeInsets.only( left: 16.0, top: 8.0, bottom: 4.0), child: Text( - (group.key.year == now.year && group.key.month == now.month && group.key.day == now.day) ? - ( - group.key.hour == now.hour ? - DateFormat.jm(localeString).format(group.key) : - DateFormat.j(localeString).format(group.key) - ) : - DateFormat.MMMMd(localeString).format(group.key) - , + (group.key.year == now.year && + group.key.month == now.month && + group.key.day == now.day) + ? (group.key.hour == now.hour + ? DateFormat.jm(localeString) + .format(group.key) + : DateFormat.j(localeString) + .format(group.key)) + : DateFormat.MMMMd(localeString) + .format(group.key), style: const TextStyle( fontSize: 16.0, ), diff --git a/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart b/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart index df99e1ec9..76e854d78 100644 --- a/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart +++ b/lib/components/PlaybackHistoryScreen/playback_history_list_tile.dart @@ -45,9 +45,9 @@ class PlaybackHistoryListTile extends StatefulWidget { class _PlaybackHistoryListTileState extends State { @override Widget build(BuildContext context) { + final baseItem = jellyfin_models.BaseItemDto.fromJson( + widget.item.item.item.extras?["itemJson"]); - final baseItem = jellyfin_models.BaseItemDto.fromJson(widget.item.item.item.extras?["itemJson"]); - return GestureDetector( onLongPressStart: (details) => showSongMenu(details), child: Card( @@ -61,8 +61,7 @@ class _PlaybackHistoryListTileState extends State { visualDensity: VisualDensity.standard, minVerticalPadding: 0.0, horizontalTitleGap: 10.0, - contentPadding: - const EdgeInsets.only(right: 4.0), + contentPadding: const EdgeInsets.only(right: 4.0), leading: AlbumImage( item: widget.item.item.item.extras?["itemJson"] == null ? null @@ -128,7 +127,8 @@ class _PlaybackHistoryListTileState extends State { onToggle: (isFavorite) => setState(() { if (baseItem.userData != null) { baseItem.userData!.isFavorite = isFavorite; - widget.item.item.item.extras?["itemJson"] = baseItem.toJson(); + widget.item.item.item.extras?["itemJson"] = + baseItem.toJson(); } }), ) diff --git a/lib/components/PlayerScreen/artist_chip.dart b/lib/components/PlayerScreen/artist_chip.dart index a7e2c819d..0a6504477 100644 --- a/lib/components/PlayerScreen/artist_chip.dart +++ b/lib/components/PlayerScreen/artist_chip.dart @@ -36,9 +36,8 @@ class ArtistChips extends StatelessWidget { spacing: 4.0, runSpacing: 4.0, children: List.generate(baseItem?.artistItems?.length ?? 0, (index) { - final currentArtist = baseItem!.artistItems![index]; - + return ArtistChip( color: color, artist: BaseItemDto( @@ -46,7 +45,6 @@ class ArtistChips extends StatelessWidget { name: currentArtist.name, type: "MusicArtist", ), - ); }), ), @@ -83,13 +81,11 @@ class _ArtistChipState extends State { if (widget.artist != null) { final albumArtistId = widget.artist!.id; - // This is a terrible hack but since offline artists aren't yet + // This is a terrible hack but since offline artists aren't yet // implemented it's kind of needed. When offline, we make a fake item // with the required amount of data to show an artist chip. _artistChipFuture = FinampSettingsHelper.finampSettings.isOffline - ? Future.sync( - () => widget.artist! - ) + ? Future.sync(() => widget.artist!) : _jellyfinApiHelper.getItemById(albumArtistId); } } @@ -97,12 +93,12 @@ class _ArtistChipState extends State { @override Widget build(BuildContext context) { return FutureBuilder( - future: _artistChipFuture, - builder: (context, snapshot) { - final color = widget.color ?? _defaultColour; - return _ArtistChipContent(item: snapshot.data ?? widget.artist!, color: color); - } - ); + future: _artistChipFuture, + builder: (context, snapshot) { + final color = widget.color ?? _defaultColour; + return _ArtistChipContent( + item: snapshot.data ?? widget.artist!, color: color); + }); } } @@ -120,7 +116,8 @@ class _ArtistChipContent extends StatelessWidget { Widget build(BuildContext context) { // We do this so that we can pass the song item here to show an actual value // instead of empty - final name = item.isArtist ? item.name : (item.artists?.first ?? item.albumArtist); + final name = + item.isArtist ? item.name : (item.artists?.first ?? item.albumArtist); return SizedBox( height: 24, diff --git a/lib/components/PlayerScreen/finamp_back_button_icon.dart b/lib/components/PlayerScreen/finamp_back_button_icon.dart index b92ea02c4..60d370769 100644 --- a/lib/components/PlayerScreen/finamp_back_button_icon.dart +++ b/lib/components/PlayerScreen/finamp_back_button_icon.dart @@ -18,10 +18,9 @@ class FinampBackButtonIcon extends StatelessWidget { } class RPSCustomPainter extends CustomPainter { - BuildContext context; RPSCustomPainter(this.context); - + @override void paint(Canvas canvas, Size size) { Path path_0 = Path(); diff --git a/lib/components/PlayerScreen/player_buttons_more.dart b/lib/components/PlayerScreen/player_buttons_more.dart index f227ee39b..415e37af5 100644 --- a/lib/components/PlayerScreen/player_buttons_more.dart +++ b/lib/components/PlayerScreen/player_buttons_more.dart @@ -29,36 +29,34 @@ class PlayerButtonsMore extends StatelessWidget { color: IconTheme.of(context).color!, ), itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( + >[ + PopupMenuItem( value: PlayerButtonsMoreItems.addToPlaylist, child: StreamBuilder( - stream: audioHandler.mediaItem, - builder: (context, snapshot) { - if (snapshot.hasData) { - return ListTile( - leading: const Icon(TablerIcons.playlist_add), - onTap: () => Navigator.of(context).pushReplacementNamed( - AddToPlaylistScreen.routeName, - arguments: BaseItemDto.fromJson( - snapshot.data!.extras!["itemJson"]) - .id), - title: Text(AppLocalizations.of(context)! - .addToPlaylistTooltip)); - } else { - return ListTile( - leading: const Icon(TablerIcons.playlist_add), - onTap: () {}, - title: Text(AppLocalizations.of(context)! - .addToPlaylistTooltip)); - } - } - ) - ), - const PopupMenuItem( - value: PlayerButtonsMoreItems.sleepTimer, - child: SleepTimerButton(), - ), + stream: audioHandler.mediaItem, + builder: (context, snapshot) { + if (snapshot.hasData) { + return ListTile( + leading: const Icon(TablerIcons.playlist_add), + onTap: () => Navigator.of(context).pushReplacementNamed( + AddToPlaylistScreen.routeName, + arguments: BaseItemDto.fromJson( + snapshot.data!.extras!["itemJson"]) + .id), + title: Text(AppLocalizations.of(context)! + .addToPlaylistTooltip)); + } else { + return ListTile( + leading: const Icon(TablerIcons.playlist_add), + onTap: () {}, + title: Text(AppLocalizations.of(context)! + .addToPlaylistTooltip)); + } + })), + const PopupMenuItem( + value: PlayerButtonsMoreItems.sleepTimer, + child: SleepTimerButton(), + ), ], ); } diff --git a/lib/components/PlayerScreen/player_buttons_repeating.dart b/lib/components/PlayerScreen/player_buttons_repeating.dart index 153c61c41..85d389da0 100644 --- a/lib/components/PlayerScreen/player_buttons_repeating.dart +++ b/lib/components/PlayerScreen/player_buttons_repeating.dart @@ -23,31 +23,30 @@ class PlayerButtonsRepeating extends StatelessWidget { builder: (BuildContext context, AsyncSnapshot snapshot) { return IconButton( onPressed: () async { - // Cycles from none -> all -> one - switch (queueService.loopMode) { - case FinampLoopMode.none: - queueService.loopMode = FinampLoopMode.all; - break; - case FinampLoopMode.all: - queueService.loopMode = FinampLoopMode.one; - break; - case FinampLoopMode.one: - queueService.loopMode = FinampLoopMode.none; - break; - default: - queueService.loopMode = FinampLoopMode.none; - break; - } - }, - icon: _getRepeatingIcon( - queueService.loopMode, - Theme.of(context).colorScheme.secondary, - )); + // Cycles from none -> all -> one + switch (queueService.loopMode) { + case FinampLoopMode.none: + queueService.loopMode = FinampLoopMode.all; + break; + case FinampLoopMode.all: + queueService.loopMode = FinampLoopMode.one; + break; + case FinampLoopMode.one: + queueService.loopMode = FinampLoopMode.none; + break; + default: + queueService.loopMode = FinampLoopMode.none; + break; + } + }, + icon: _getRepeatingIcon( + queueService.loopMode, + Theme.of(context).colorScheme.secondary, + )); }); } - Widget _getRepeatingIcon( - FinampLoopMode loopMode, Color iconColour) { + Widget _getRepeatingIcon(FinampLoopMode loopMode, Color iconColour) { if (loopMode == FinampLoopMode.all) { return const Icon(TablerIcons.repeat); } else if (loopMode == FinampLoopMode.one) { diff --git a/lib/components/PlayerScreen/player_buttons_shuffle.dart b/lib/components/PlayerScreen/player_buttons_shuffle.dart index bd2ac4f95..afddb0f71 100644 --- a/lib/components/PlayerScreen/player_buttons_shuffle.dart +++ b/lib/components/PlayerScreen/player_buttons_shuffle.dart @@ -20,7 +20,10 @@ class PlayerButtonsShuffle extends StatelessWidget { builder: (BuildContext context, AsyncSnapshot snapshot) { return IconButton( onPressed: () async { - _queueService.playbackOrder = _queueService.playbackOrder == FinampPlaybackOrder.shuffled ? FinampPlaybackOrder.linear : FinampPlaybackOrder.shuffled; + _queueService.playbackOrder = + _queueService.playbackOrder == FinampPlaybackOrder.shuffled + ? FinampPlaybackOrder.linear + : FinampPlaybackOrder.shuffled; }, icon: Icon( (_queueService.playbackOrder == FinampPlaybackOrder.shuffled diff --git a/lib/components/PlayerScreen/player_screen_appbar_title.dart b/lib/components/PlayerScreen/player_screen_appbar_title.dart index cf818a81b..5e14ffd36 100644 --- a/lib/components/PlayerScreen/player_screen_appbar_title.dart +++ b/lib/components/PlayerScreen/player_screen_appbar_title.dart @@ -10,11 +10,11 @@ import 'package:finamp/services/queue_service.dart'; import 'queue_source_helper.dart'; class PlayerScreenAppBarTitle extends StatefulWidget { - const PlayerScreenAppBarTitle({Key? key}) : super(key: key); @override - State createState() => _PlayerScreenAppBarTitleState(); + State createState() => + _PlayerScreenAppBarTitleState(); } class _PlayerScreenAppBarTitleState extends State { @@ -22,7 +22,6 @@ class _PlayerScreenAppBarTitleState extends State { @override Widget build(BuildContext context) { - final currentTrackStream = _queueService.getCurrentTrackStream(); return StreamBuilder( @@ -40,11 +39,15 @@ class _PlayerScreenAppBarTitleState extends State { onTap: () => navigateToSource(context, queueItem.source), child: Column( children: [ - Text(AppLocalizations.of(context)!.playingFromType(queueItem.source.type.toString()), + Text( + AppLocalizations.of(context)! + .playingFromType(queueItem.source.type.toString()), style: TextStyle( fontSize: 12, fontWeight: FontWeight.w300, - color: Theme.of(context).brightness == Brightness.dark ? Colors.white.withOpacity(0.7) : Colors.black.withOpacity(0.8), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.7) + : Colors.black.withOpacity(0.8), ), overflow: TextOverflow.ellipsis, ), @@ -53,7 +56,9 @@ class _PlayerScreenAppBarTitleState extends State { queueItem.source.name.getLocalized(context), style: TextStyle( fontSize: 16, - color: Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black.withOpacity(0.9), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black.withOpacity(0.9), ), maxLines: 2, ), diff --git a/lib/components/PlayerScreen/progress_slider.dart b/lib/components/PlayerScreen/progress_slider.dart index 17a1994d4..232506bb0 100644 --- a/lib/components/PlayerScreen/progress_slider.dart +++ b/lib/components/PlayerScreen/progress_slider.dart @@ -191,20 +191,14 @@ class _ProgressSliderDuration extends StatelessWidget { printDuration( Duration(microseconds: position.inMicroseconds), ), - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith( + style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).textTheme.bodySmall?.color, height: 0.5, // reduce line height ), ), Text( printDuration(itemDuration), - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith( + style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).textTheme.bodySmall?.color, height: 0.5, // reduce line height ), diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index a91faf069..e35b69f98 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -123,8 +123,9 @@ class _QueueListState extends State { ), SliverPersistentHeader( delegate: QueueSectionHeader( - source: _source, - title: const Flexible(child: Text("Queue", overflow: TextOverflow.ellipsis)), + source: _source, + title: const Flexible( + child: Text("Queue", overflow: TextOverflow.ellipsis)), nextUpHeaderKey: widget.nextUpHeaderKey, )), // Queue @@ -150,24 +151,23 @@ class _QueueListState extends State { _contents = [ // Previous Tracks StreamBuilder( - stream: isRecentTracksExpanded, - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data!) { - return PreviousTracksList( - previousTracksHeaderKey: widget.previousTracksHeaderKey); - } else { - return const SliverToBoxAdapter(); - } - } - ), + stream: isRecentTracksExpanded, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!) { + return PreviousTracksList( + previousTracksHeaderKey: widget.previousTracksHeaderKey); + } else { + return const SliverToBoxAdapter(); + } + }), SliverPersistentHeader( - key: widget.previousTracksHeaderKey, - delegate: PreviousTracksSectionHeader( - isRecentTracksExpanded: isRecentTracksExpanded, - previousTracksHeaderKey: widget.previousTracksHeaderKey, - onTap: () => isRecentTracksExpanded.add(!isRecentTracksExpanded.value), - ) - ), + key: widget.previousTracksHeaderKey, + delegate: PreviousTracksSectionHeader( + isRecentTracksExpanded: isRecentTracksExpanded, + previousTracksHeaderKey: widget.previousTracksHeaderKey, + onTap: () => + isRecentTracksExpanded.add(!isRecentTracksExpanded.value), + )), CurrentTrack( // key: UniqueKey(), key: widget.currentTrackKey, @@ -182,7 +182,8 @@ class _QueueListState extends State { // key: widget.nextUpHeaderKey, padding: const EdgeInsets.only(top: 20.0, bottom: 0.0), sliver: SliverPersistentHeader( - pinned: false, //TODO use https://stackoverflow.com/a/69372976 to only ever have one of the headers pinned + pinned: + false, //TODO use https://stackoverflow.com/a/69372976 to only ever have one of the headers pinned delegate: NextUpSectionHeader( controls: true, nextUpHeaderKey: widget.nextUpHeaderKey, @@ -209,11 +210,11 @@ class _QueueListState extends State { ), Flexible( child: Text( - _source?.name.getLocalized(context) ?? - AppLocalizations.of(context)!.unknownName, - style: const TextStyle(fontWeight: FontWeight.w500), - overflow: TextOverflow.ellipsis, - ), + _source?.name.getLocalized(context) ?? + AppLocalizations.of(context)!.unknownName, + style: const TextStyle(fontWeight: FontWeight.w500), + overflow: TextOverflow.ellipsis, + ), ), ], ), @@ -231,13 +232,14 @@ class _QueueListState extends State { return ScrollbarTheme( data: ScrollbarThemeData( - thumbColor: MaterialStateProperty.all(Theme.of(context).colorScheme.primary.withOpacity(0.7)), - trackColor: MaterialStateProperty.all(Theme.of(context).colorScheme.primary.withOpacity(0.2)), - radius: const Radius.circular(6.0), - thickness: MaterialStateProperty.all(12.0), - // thumbVisibility: MaterialStateProperty.all(true), - trackVisibility: MaterialStateProperty.all(false) - ), + thumbColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.primary.withOpacity(0.7)), + trackColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.primary.withOpacity(0.2)), + radius: const Radius.circular(6.0), + thickness: MaterialStateProperty.all(12.0), + // thumbVisibility: MaterialStateProperty.all(true), + trackVisibility: MaterialStateProperty.all(false)), child: Scrollbar( controller: widget.scrollController, interactive: true, @@ -309,14 +311,18 @@ Future showQueueBottomSheet(BuildContext context) { width: 40, height: 3.5, decoration: BoxDecoration( - color: Theme.of(context).textTheme.bodySmall!.color!, + color: + Theme.of(context).textTheme.bodySmall!.color!, borderRadius: BorderRadius.circular(3.5), ), ), const SizedBox(height: 10), Text(AppLocalizations.of(context)!.queue, style: TextStyle( - color: Theme.of(context).textTheme.bodyLarge!.color!, + color: Theme.of(context) + .textTheme + .bodyLarge! + .color!, fontFamily: 'Lexend Deca', fontSize: 18, fontWeight: FontWeight.w300)), @@ -664,7 +670,8 @@ class _CurrentTrackState extends State { Duration? playbackPosition; return StreamBuilder<_QueueListStreamState>( - stream: Rx.combineLatest2( + stream: Rx.combineLatest2( mediaStateStream, _queueService.getQueueStream(), (a, b) => _QueueListStreamState(a, b)), @@ -694,13 +701,18 @@ class _CurrentTrackState extends State { flexibleSpace: Container( // width: 58, height: albumImageSize, - padding: const EdgeInsets.symmetric(horizontal: horizontalPadding), + padding: + const EdgeInsets.symmetric(horizontal: horizontalPadding), child: Container( clipBehavior: Clip.antiAlias, decoration: ShapeDecoration( color: Color.alphaBlend( - Theme.of(context).brightness == Brightness.dark ? IconTheme.of(context).color!.withOpacity(0.35) : IconTheme.of(context).color!.withOpacity(0.5), - Theme.of(context).brightness == Brightness.dark ? Colors.black : Colors.white), + Theme.of(context).brightness == Brightness.dark + ? IconTheme.of(context).color!.withOpacity(0.35) + : IconTheme.of(context).color!.withOpacity(0.5), + Theme.of(context).brightness == Brightness.dark + ? Colors.black + : Colors.white), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(12.0)), ), @@ -763,10 +775,13 @@ class _CurrentTrackState extends State { builder: (context, snapshot) { if (snapshot.hasData) { playbackPosition = snapshot.data; - final screenSize = MediaQuery.of(context).size; + final screenSize = + MediaQuery.of(context).size; return Container( // rather hacky workaround, using LayoutBuilder would be nice but I couldn't get it to work... - width: (screenSize.width - 2*horizontalPadding - albumImageSize) * + width: (screenSize.width - + 2 * horizontalPadding - + albumImageSize) * (playbackPosition!.inMilliseconds / (mediaState?.mediaItem ?.duration ?? @@ -803,7 +818,8 @@ class _CurrentTrackState extends State { child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( currentTrack?.item.title ?? @@ -832,7 +848,8 @@ class _CurrentTrackState extends State { fontSize: 13, fontFamily: 'Lexend Deca', fontWeight: FontWeight.w300, - overflow: TextOverflow.ellipsis), + overflow: + TextOverflow.ellipsis), ), ), Row( @@ -850,7 +867,8 @@ class _CurrentTrackState extends State { .withOpacity(0.8), fontSize: 14, fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w400, + fontWeight: + FontWeight.w400, ); if (snapshot.hasData) { playbackPosition = @@ -1220,40 +1238,39 @@ class QueueSectionHeader extends SliverPersistentHeaderDelegate { children: [ Expanded( child: GestureDetector( - child: title, - onTap: () { - if (source != null) { - navigateToSource(context, source!); - } - } - ), + child: title, + onTap: () { + if (source != null) { + navigateToSource(context, source!); + } + }), ), if (controls) Row( children: [ IconButton( - padding: const EdgeInsets.only(bottom: 2.0), - iconSize: 28.0, - icon: info?.order == FinampPlaybackOrder.shuffled - ? (const Icon( - TablerIcons.arrows_shuffle, - )) - : (const Icon( - TablerIcons.arrows_right, - )), - color: info?.order == FinampPlaybackOrder.shuffled - ? IconTheme.of(context).color! - : Colors.white, - onPressed: () { - queueService.togglePlaybackOrder(); - Vibrate.feedback(FeedbackType.success); - Future.delayed( - const Duration(milliseconds: 300), - () => scrollToKey( - key: nextUpHeaderKey, - duration: const Duration(milliseconds: 500))); - // scrollToKey(key: nextUpHeaderKey, duration: const Duration(milliseconds: 1000)); - }), + padding: const EdgeInsets.only(bottom: 2.0), + iconSize: 28.0, + icon: info?.order == FinampPlaybackOrder.shuffled + ? (const Icon( + TablerIcons.arrows_shuffle, + )) + : (const Icon( + TablerIcons.arrows_right, + )), + color: info?.order == FinampPlaybackOrder.shuffled + ? IconTheme.of(context).color! + : Colors.white, + onPressed: () { + queueService.togglePlaybackOrder(); + Vibrate.feedback(FeedbackType.success); + Future.delayed( + const Duration(milliseconds: 300), + () => scrollToKey( + key: nextUpHeaderKey, + duration: const Duration(milliseconds: 500))); + // scrollToKey(key: nextUpHeaderKey, duration: const Duration(milliseconds: 1000)); + }), IconButton( padding: const EdgeInsets.only(bottom: 2.0), iconSize: 28.0, @@ -1274,7 +1291,7 @@ class QueueSectionHeader extends SliverPersistentHeaderDelegate { onPressed: () { queueService.toggleLoopMode(); Vibrate.feedback(FeedbackType.success); - }), + }), ], ) // Expanded( @@ -1285,8 +1302,8 @@ class QueueSectionHeader extends SliverPersistentHeaderDelegate { // children: [ // , // ])), - - // ) + + // ) ], ), ); @@ -1386,10 +1403,10 @@ class PreviousTracksSectionHeader extends SliverPersistentHeaderDelegate { @override Widget build(context, double shrinkOffset, bool overlapsContent) { - return Padding( // color: Colors.black.withOpacity(0.5), - padding: const EdgeInsets.only(left: 14.0, right: 14.0, bottom: 12.0, top: 8.0), + padding: const EdgeInsets.only( + left: 14.0, right: 14.0, bottom: 12.0, top: 8.0), child: GestureDetector( onTap: () { try { @@ -1411,23 +1428,22 @@ class PreviousTracksSectionHeader extends SliverPersistentHeaderDelegate { ), const SizedBox(width: 4.0), StreamBuilder( - stream: isRecentTracksExpanded, - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data!) { - return Icon( - TablerIcons.chevron_up, - size: 28.0, - color: Theme.of(context).iconTheme.color!, - ); - } else { - return Icon( - TablerIcons.chevron_down, - size: 28.0, - color: Theme.of(context).iconTheme.color!, - ); - } - } - ), + stream: isRecentTracksExpanded, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!) { + return Icon( + TablerIcons.chevron_up, + size: 28.0, + color: Theme.of(context).iconTheme.color!, + ); + } else { + return Icon( + TablerIcons.chevron_down, + size: 28.0, + color: Theme.of(context).iconTheme.color!, + ); + } + }), ], ), ), diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index 002ee07d0..32639e69f 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -117,7 +117,10 @@ class _QueueListItemState extends State child: Text( processArtist(widget.item.item.artist, context), style: TextStyle( - color: Theme.of(context).textTheme.bodyMedium!.color!, + color: Theme.of(context) + .textTheme + .bodyMedium! + .color!, fontSize: 13, fontFamily: 'Lexend Deca', fontWeight: FontWeight.w300, @@ -131,7 +134,9 @@ class _QueueListItemState extends State alignment: Alignment.centerRight, margin: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 6.0), - width: widget.allowReorder ? 72.0 : 42.0, //TODO make this responsive + width: widget.allowReorder + ? 72.0 + : 42.0, //TODO make this responsive child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, @@ -265,8 +270,10 @@ class _QueueListItemState extends State switch (selection) { case SongListTileMenuItems.addToQueue: await _queueService.addToQueue( - items: [jellyfin_models.BaseItemDto.fromJson( - widget.item.item.extras?["itemJson"])], + items: [ + jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]) + ], source: QueueItemSource( type: QueueItemSourceType.unknown, name: QueueItemSourceName( diff --git a/lib/components/PlayerScreen/queue_source_helper.dart b/lib/components/PlayerScreen/queue_source_helper.dart index 3fdfaadb3..026716466 100644 --- a/lib/components/PlayerScreen/queue_source_helper.dart +++ b/lib/components/PlayerScreen/queue_source_helper.dart @@ -7,35 +7,41 @@ import 'package:flutter/material.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart'; void navigateToSource(BuildContext context, QueueItemSource source) async { - switch (source.type) { case QueueItemSourceType.album: case QueueItemSourceType.nextUpAlbum: - Navigator.of(context).pushNamed(AlbumScreen.routeName, arguments: source.item); + Navigator.of(context) + .pushNamed(AlbumScreen.routeName, arguments: source.item); break; case QueueItemSourceType.artist: case QueueItemSourceType.nextUpArtist: - Navigator.of(context).pushNamed(ArtistScreen.routeName, arguments: source.item); + Navigator.of(context) + .pushNamed(ArtistScreen.routeName, arguments: source.item); break; case QueueItemSourceType.genre: - Navigator.of(context).pushNamed(ArtistScreen.routeName, arguments: source.item); + Navigator.of(context) + .pushNamed(ArtistScreen.routeName, arguments: source.item); break; case QueueItemSourceType.playlist: case QueueItemSourceType.nextUpPlaylist: - Navigator.of(context).pushNamed(AlbumScreen.routeName, arguments: source.item); + Navigator.of(context) + .pushNamed(AlbumScreen.routeName, arguments: source.item); break; case QueueItemSourceType.albumMix: - Navigator.of(context).pushNamed(AlbumScreen.routeName, arguments: source.item); + Navigator.of(context) + .pushNamed(AlbumScreen.routeName, arguments: source.item); break; case QueueItemSourceType.artistMix: - Navigator.of(context).pushNamed(ArtistScreen.routeName, arguments: source.item); + Navigator.of(context) + .pushNamed(ArtistScreen.routeName, arguments: source.item); break; case QueueItemSourceType.allSongs: - Navigator.of(context).pushNamed(MusicScreen.routeName, arguments: FinampSettingsHelper.finampSettings.showTabs.entries - .where((element) => element.value == true) - .map((e) => e.key) - .toList().indexOf(TabContentType.songs) - ); + Navigator.of(context).pushNamed(MusicScreen.routeName, + arguments: FinampSettingsHelper.finampSettings.showTabs.entries + .where((element) => element.value == true) + .map((e) => e.key) + .toList() + .indexOf(TabContentType.songs)); break; case QueueItemSourceType.nextUp: break; @@ -63,5 +69,4 @@ void navigateToSource(BuildContext context, QueueItemSource source) async { ), ); } - } diff --git a/lib/components/PlayerScreen/sleep_timer_button.dart b/lib/components/PlayerScreen/sleep_timer_button.dart index fefd82dcd..d14911be8 100644 --- a/lib/components/PlayerScreen/sleep_timer_button.dart +++ b/lib/components/PlayerScreen/sleep_timer_button.dart @@ -20,23 +20,21 @@ class SleepTimerButton extends StatelessWidget { valueListenable: audioHandler.sleepTimer, builder: (context, value, child) { return ListTile( - leading: const Icon(TablerIcons.hourglass_high), - onTap: () async { - if (value != null) { - showDialog( - context: context, - builder: (context) => const SleepTimerCancelDialog(), - ); - } else { - await showDialog( - context: context, - builder: (context) => const SleepTimerDialog(), - ); - } - }, - title: Text(AppLocalizations.of(context)! - .sleepTimerTooltip) - ); + leading: const Icon(TablerIcons.hourglass_high), + onTap: () async { + if (value != null) { + showDialog( + context: context, + builder: (context) => const SleepTimerCancelDialog(), + ); + } else { + await showDialog( + context: context, + builder: (context) => const SleepTimerDialog(), + ); + } + }, + title: Text(AppLocalizations.of(context)!.sleepTimerTooltip)); }, ); } diff --git a/lib/components/PlayerScreen/song_info.dart b/lib/components/PlayerScreen/song_info.dart index ab7831a16..514779fc7 100644 --- a/lib/components/PlayerScreen/song_info.dart +++ b/lib/components/PlayerScreen/song_info.dart @@ -41,19 +41,19 @@ class _SongInfoState extends State { return StreamBuilder( stream: queueService.getQueueStream(), builder: (context, snapshot) { - if (!snapshot.hasData) { // show loading indicator return const Center( child: CircularProgressIndicator(), ); } - + final currentTrack = snapshot.data!.currentTrack!; final mediaItem = currentTrack.item; final songBaseItemDto = (mediaItem.extras?.containsKey("itemJson") ?? false) - ? jellyfin_models.BaseItemDto.fromJson(mediaItem.extras!["itemJson"]) + ? jellyfin_models.BaseItemDto.fromJson( + mediaItem.extras!["itemJson"]) : null; List separatedArtistTextSpans = []; @@ -168,7 +168,8 @@ class _PlayerScreenAlbumImage extends ConsumerWidget { }).toList(), // We need a post frame callback because otherwise this // widget rebuilds on the same frame - imageProviderCallback: (imageProvider) => WidgetsBinding.instance.addPostFrameCallback((_) async { + imageProviderCallback: (imageProvider) => + WidgetsBinding.instance.addPostFrameCallback((_) async { // Don't do anything if the image from the callback is the same as // the current provider's image. This is probably needed because of // addPostFrameCallback shenanigans @@ -177,19 +178,19 @@ class _PlayerScreenAlbumImage extends ConsumerWidget { imageProvider) { return; } - + ref.read(currentAlbumImageProvider.notifier).state = imageProvider; - + if (imageProvider != null) { final theme = Theme.of(context); - + final paletteGenerator = await PaletteGenerator.fromImageProvider(imageProvider); - + Color accent = paletteGenerator.dominantColor!.color; - + final lighter = theme.brightness == Brightness.dark; - + // increase saturation if (!lighter) { final hsv = HSVColor.fromColor(accent); @@ -203,16 +204,15 @@ class _PlayerScreenAlbumImage extends ConsumerWidget { ? Colors.black.withOpacity(0.675) : Colors.white.withOpacity(0.675), accent); - + accent = accent.atContrast(4.5, background, lighter); - + ref.read(playerScreenThemeProvider.notifier).state = ColorScheme.fromSwatch( primarySwatch: generateMaterialColor(accent), accentColor: accent, brightness: theme.brightness, ); - } }), ), diff --git a/lib/components/PlayerScreen/song_name_content.dart b/lib/components/PlayerScreen/song_name_content.dart index 915c75cfd..72e6a8df8 100644 --- a/lib/components/PlayerScreen/song_name_content.dart +++ b/lib/components/PlayerScreen/song_name_content.dart @@ -21,16 +21,19 @@ class SongNameContent extends StatelessWidget { @override Widget build(BuildContext context) { + final jellyfin_models.BaseItemDto? songBaseItemDto = + currentTrack.item.extras!["itemJson"] != null + ? jellyfin_models.BaseItemDto.fromJson( + currentTrack.item.extras!["itemJson"]) + : null; - final jellyfin_models.BaseItemDto? songBaseItemDto = currentTrack.item.extras!["itemJson"] != null - ? jellyfin_models.BaseItemDto.fromJson(currentTrack.item.extras!["itemJson"]) : null; - return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Padding( - padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 4.0, bottom: 0.0), + padding: const EdgeInsets.only( + left: 10.0, right: 10.0, top: 4.0, bottom: 0.0), child: Container( height: 48.0, alignment: Alignment.center, @@ -76,7 +79,8 @@ class SongNameContent extends StatelessWidget { item: songBaseItemDto, onToggle: (isFavorite) { songBaseItemDto!.userData!.isFavorite = isFavorite; - currentTrack.item.extras!["itemJson"] = songBaseItemDto.toJson(); + currentTrack.item.extras!["itemJson"] = + songBaseItemDto.toJson(); }, ), ], diff --git a/lib/components/favourite_button.dart b/lib/components/favourite_button.dart index abaed8384..3f31cdbc8 100644 --- a/lib/components/favourite_button.dart +++ b/lib/components/favourite_button.dart @@ -70,7 +70,6 @@ class _FavoriteButtonState extends State { if (widget.onToggle != null) { widget.onToggle!(widget.item!.userData!.isFavorite); } - } catch (e) { errorSnackbar(e, context); } diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index 1b044d529..6441bc6f1 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -56,8 +56,8 @@ class NowPlayingBar extends ConsumerWidget { brightness: Theme.of(context).brightness, ), iconTheme: Theme.of(context).iconTheme.copyWith( - color: imageTheme?.primary, - ), + color: imageTheme?.primary, + ), ), child: SimpleGestureDetector( onVerticalSwipe: (direction) { @@ -65,312 +65,436 @@ class NowPlayingBar extends ConsumerWidget { Navigator.of(context).pushNamed(PlayerScreen.routeName); } }, - onTap: () => Navigator.of(context) - .pushNamed(PlayerScreen.routeName), + onTap: () => Navigator.of(context).pushNamed(PlayerScreen.routeName), child: StreamBuilder( - stream: queueService.getQueueStream(), - builder: (context, snapshot) { - - if (snapshot.hasData && snapshot.data!.currentTrack != null) { - final currentTrack = snapshot.data!.currentTrack!; - final currentTrackBaseItem = currentTrack.item.extras?["itemJson"] != null - ? jellyfin_models.BaseItemDto.fromJson( - currentTrack.item.extras!["itemJson"] - as Map) - : null; - return Padding( - padding: const EdgeInsets.only(left: 12.0, bottom: 12.0, right: 12.0), - child: Material( - shadowColor: Theme.of(context).colorScheme.secondary.withOpacity(0.2), - borderRadius: BorderRadius.circular(12.0), - clipBehavior: Clip.antiAlias, - color: Theme.of(context).brightness == Brightness.dark ? IconTheme.of(context).color!.withOpacity(0.1) : Theme.of(context).cardColor, - elevation: elevation, - child: SafeArea( - //TODO use a PageView instead of a Dismissible, and only wrap dynamic items (not the buttons) - child: Dismissible( - key: const Key("NowPlayingBar"), - direction: FinampSettingsHelper.finampSettings.disableGesture ? DismissDirection.none : DismissDirection.horizontal, - confirmDismiss: (direction) async { - if (direction == DismissDirection.endToStart) { - audioHandler.skipToNext(); - } else { - audioHandler.skipToPrevious(); - } - return false; - }, - child: StreamBuilder( - stream: mediaStateStream, - builder: (context, snapshot) { - if (snapshot.hasData) { - final playing = snapshot.data!.playbackState.playing; - final mediaState = snapshot.data!; - // If we have a media item and the player hasn't finished, show - // the now playing bar. - if (snapshot.data!.mediaItem != null) { - //TODO move into separate component and share with queue list - return Container( - width: MediaQuery.of(context).size.width, - height: albumImageSize, - padding: EdgeInsets.zero, - child: Container( - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: Color.alphaBlend( - Theme.of(context).brightness == Brightness.dark ? IconTheme.of(context).color!.withOpacity(0.35) : IconTheme.of(context).color!.withOpacity(0.5), - Theme.of(context).brightness == Brightness.dark ? Colors.black : Colors.white), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(12.0)), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Stack( - alignment: Alignment.center, - children: [ - AlbumImage( - item: currentTrackBaseItem, - borderRadius: BorderRadius.zero, - itemsToPrecache: - queueService.getNextXTracksInQueue(3).map((e) { - final item = e.item.extras?["itemJson"] != null - ? jellyfin_models.BaseItemDto.fromJson( - e.item.extras!["itemJson"] - as Map) - : null; - return item!; - }).toList(), - ), - Container( - width: albumImageSize, - height: albumImageSize, - decoration: const ShapeDecoration( - shape: Border(), - color: Color.fromRGBO(0, 0, 0, 0.3), - ), - child: IconButton( - onPressed: () { - Vibrate.feedback(FeedbackType.success); - audioHandler.togglePlayback(); - }, - icon: mediaState!.playbackState.playing - ? const Icon( - TablerIcons.player_pause, - size: 32, - ) - : const Icon( - TablerIcons.player_play, - size: 32, - ), - color: Colors.white, - )), - ], + stream: queueService.getQueueStream(), + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!.currentTrack != null) { + final currentTrack = snapshot.data!.currentTrack!; + final currentTrackBaseItem = + currentTrack.item.extras?["itemJson"] != null + ? jellyfin_models.BaseItemDto.fromJson(currentTrack + .item.extras!["itemJson"] as Map) + : null; + return Padding( + padding: const EdgeInsets.only( + left: 12.0, bottom: 12.0, right: 12.0), + child: Material( + shadowColor: Theme.of(context) + .colorScheme + .secondary + .withOpacity(0.2), + borderRadius: BorderRadius.circular(12.0), + clipBehavior: Clip.antiAlias, + color: Theme.of(context).brightness == Brightness.dark + ? IconTheme.of(context).color!.withOpacity(0.1) + : Theme.of(context).cardColor, + elevation: elevation, + child: SafeArea( + //TODO use a PageView instead of a Dismissible, and only wrap dynamic items (not the buttons) + child: Dismissible( + key: const Key("NowPlayingBar"), + direction: + FinampSettingsHelper.finampSettings.disableGesture + ? DismissDirection.none + : DismissDirection.horizontal, + confirmDismiss: (direction) async { + if (direction == DismissDirection.endToStart) { + audioHandler.skipToNext(); + } else { + audioHandler.skipToPrevious(); + } + return false; + }, + child: StreamBuilder( + stream: mediaStateStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + final playing = + snapshot.data!.playbackState.playing; + final mediaState = snapshot.data!; + // If we have a media item and the player hasn't finished, show + // the now playing bar. + if (snapshot.data!.mediaItem != null) { + //TODO move into separate component and share with queue list + return Container( + width: MediaQuery.of(context).size.width, + height: albumImageSize, + padding: EdgeInsets.zero, + child: Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: Color.alphaBlend( + Theme.of(context).brightness == + Brightness.dark + ? IconTheme.of(context) + .color! + .withOpacity(0.35) + : IconTheme.of(context) + .color! + .withOpacity(0.5), + Theme.of(context).brightness == + Brightness.dark + ? Colors.black + : Colors.white), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(12.0)), ), - Expanded( - child: Stack( + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: + MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Stack( + alignment: Alignment.center, children: [ - Positioned( - left: 0, - top: 0, - child: StreamBuilder( - stream: AudioService.position.startWith( - audioHandler.playbackState.value.position), - builder: (context, snapshot) { - if (snapshot.hasData) { - playbackPosition = snapshot.data; - final screenSize = MediaQuery.of(context).size; - return Container( - // rather hacky workaround, using LayoutBuilder would be nice but I couldn't get it to work... - width: (screenSize.width - 2*horizontalPadding - albumImageSize) * - (playbackPosition!.inMilliseconds / - (mediaState.mediaItem - ?.duration ?? - const Duration( - seconds: 0)) - .inMilliseconds), - height: 70.0, - decoration: ShapeDecoration( - color: IconTheme.of(context) - .color! - .withOpacity(0.75), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular(12), - bottomRight: Radius.circular(12), - ), - ), - ), - ); - } else { - return Container(); - } - }), + AlbumImage( + item: currentTrackBaseItem, + borderRadius: BorderRadius.zero, + itemsToPrecache: queueService + .getNextXTracksInQueue(3) + .map((e) { + final item = e.item.extras?[ + "itemJson"] != + null + ? jellyfin_models + .BaseItemDto.fromJson(e + .item + .extras!["itemJson"] + as Map) + : null; + return item!; + }).toList(), ), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Container( - height: albumImageSize, - padding: - const EdgeInsets.only(left: 12, right: 4), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - currentTrack.item.title, - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w500, - overflow: TextOverflow.ellipsis), + Container( + width: albumImageSize, + height: albumImageSize, + decoration: + const ShapeDecoration( + shape: Border(), + color: Color.fromRGBO( + 0, 0, 0, 0.3), + ), + child: IconButton( + onPressed: () { + Vibrate.feedback( + FeedbackType.success); + audioHandler + .togglePlayback(); + }, + icon: mediaState! + .playbackState.playing + ? const Icon( + TablerIcons + .player_pause, + size: 32, + ) + : const Icon( + TablerIcons + .player_play, + size: 32, ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - processArtist( - currentTrack!.item.artist, - context), - style: TextStyle( - color: Colors.white - .withOpacity(0.85), - fontSize: 13, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w300, - overflow: TextOverflow.ellipsis), + color: Colors.white, + )), + ], + ), + Expanded( + child: Stack( + children: [ + Positioned( + left: 0, + top: 0, + child: StreamBuilder( + stream: AudioService + .position + .startWith(audioHandler + .playbackState + .value + .position), + builder: + (context, snapshot) { + if (snapshot.hasData) { + playbackPosition = + snapshot.data; + final screenSize = + MediaQuery.of( + context) + .size; + return Container( + // rather hacky workaround, using LayoutBuilder would be nice but I couldn't get it to work... + width: (screenSize + .width - + 2 * + horizontalPadding - + albumImageSize) * + (playbackPosition! + .inMilliseconds / + (mediaState.mediaItem + ?.duration ?? + const Duration( + seconds: 0)) + .inMilliseconds), + height: 70.0, + decoration: + ShapeDecoration( + color: IconTheme.of( + context) + .color! + .withOpacity( + 0.75), + shape: + const RoundedRectangleBorder( + borderRadius: + BorderRadius + .only( + topRight: Radius + .circular( + 12), + bottomRight: + Radius + .circular( + 12), + ), ), - Row( - children: [ - StreamBuilder( - stream: AudioService.position - .startWith(audioHandler - .playbackState - .value - .position), - builder: (context, snapshot) { - final TextStyle style = - TextStyle( - color: Colors.white - .withOpacity(0.8), - fontSize: 14, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w400, - ); - if (snapshot.hasData) { - playbackPosition = - snapshot.data; - return Text( - // '0:00', - playbackPosition! - .inHours >= - 1.0 - ? "${playbackPosition?.inHours.toString()}:${((playbackPosition?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" - : "${playbackPosition?.inMinutes.toString()}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - style: style, - ); - } else { - return Text( - "0:00", - style: style, + ), + ); + } else { + return Container(); + } + }), + ), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Expanded( + child: Container( + height: albumImageSize, + padding: + const EdgeInsets.only( + left: 12, + right: 4), + child: Column( + mainAxisSize: + MainAxisSize.min, + mainAxisAlignment: + MainAxisAlignment + .center, + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + currentTrack + .item.title, + style: const TextStyle( + color: Colors + .white, + fontSize: 16, + fontFamily: + 'Lexend Deca', + fontWeight: + FontWeight + .w500, + overflow: + TextOverflow + .ellipsis), + ), + const SizedBox( + height: 4), + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + processArtist( + currentTrack! + .item + .artist, + context), + style: TextStyle( + color: Colors + .white + .withOpacity( + 0.85), + fontSize: + 13, + fontFamily: + 'Lexend Deca', + fontWeight: + FontWeight + .w300, + overflow: + TextOverflow + .ellipsis), + ), + Row( + children: [ + StreamBuilder< + Duration>( + stream: AudioService.position.startWith(audioHandler + .playbackState + .value + .position), + builder: + (context, + snapshot) { + final TextStyle + style = + TextStyle( + color: Colors + .white + .withOpacity(0.8), + fontSize: + 14, + fontFamily: + 'Lexend Deca', + fontWeight: + FontWeight.w400, ); - } - }), - const SizedBox(width: 2), - Text( - '/', - style: TextStyle( - color: Colors.white - .withOpacity(0.8), - fontSize: 14, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w400, + if (snapshot + .hasData) { + playbackPosition = + snapshot.data; + return Text( + // '0:00', + playbackPosition!.inHours >= 1.0 + ? "${playbackPosition?.inHours.toString()}:${((playbackPosition?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" + : "${playbackPosition?.inMinutes.toString()}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + style: + style, + ); + } else { + return Text( + "0:00", + style: + style, + ); + } + }), + const SizedBox( + width: 2), + Text( + '/', + style: + TextStyle( + color: Colors + .white + .withOpacity( + 0.8), + fontSize: + 14, + fontFamily: + 'Lexend Deca', + fontWeight: + FontWeight + .w400, + ), ), - ), - const SizedBox(width: 2), - Text( - // '3:44', - (mediaState.mediaItem?.duration - ?.inHours ?? - 0.0) >= - 1.0 - ? "${mediaState.mediaItem?.duration?.inHours.toString()}:${((mediaState.mediaItem?.duration?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((mediaState.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" - : "${mediaState.mediaItem?.duration?.inMinutes.toString()}:${((mediaState.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - style: TextStyle( - color: Colors.white - .withOpacity(0.8), - fontSize: 14, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w400, + const SizedBox( + width: 2), + Text( + // '3:44', + (mediaState.mediaItem?.duration?.inHours ?? + 0.0) >= + 1.0 + ? "${mediaState.mediaItem?.duration?.inHours.toString()}:${((mediaState.mediaItem?.duration?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((mediaState.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" + : "${mediaState.mediaItem?.duration?.inMinutes.toString()}:${((mediaState.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + style: + TextStyle( + color: Colors + .white + .withOpacity( + 0.8), + fontSize: + 14, + fontFamily: + 'Lexend Deca', + fontWeight: + FontWeight + .w400, + ), ), - ), - ], - ) - ], - ), - ], + ], + ) + ], + ), + ], + ), ), ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.only(top: 4.0, right: 4.0), - child: FavoriteButton( - item: currentTrackBaseItem, - onToggle: (isFavorite) { - currentTrackBaseItem!.userData!.isFavorite = isFavorite; - snapshot.data!.mediaItem?.extras!["itemJson"] = currentTrackBaseItem.toJson(); - }, + Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + Padding( + padding: + const EdgeInsets + .only( + top: 4.0, + right: 4.0), + child: FavoriteButton( + item: + currentTrackBaseItem, + onToggle: + (isFavorite) { + currentTrackBaseItem! + .userData! + .isFavorite = + isFavorite; + snapshot + .data! + .mediaItem + ?.extras![ + "itemJson"] = + currentTrackBaseItem + .toJson(); + }, + ), ), - ), - ], - ), - ], - ), - ], + ], + ), + ], + ), + ], + ), ), - ), - ], + ], + ), ), - ), - ); + ); + } else { + return const SizedBox( + width: 0, + height: 0, + ); + } } else { return const SizedBox( width: 0, height: 0, ); } - } else { - return const SizedBox( - width: 0, - height: 0, - ); - } - }, + }, + ), ), ), ), - ), - ); - } else { - return const SizedBox( - width: 0, - height: 0, - ); - } - } - ), + ); + } else { + return const SizedBox( + width: 0, + height: 0, + ); + } + }), ), ); } diff --git a/lib/main.dart b/lib/main.dart index 9bdac3637..6fa7602b3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -87,9 +87,11 @@ void main() async { SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle(statusBarBrightness: Brightness.dark)); - final String localeString = (LocaleHelper.locale != null) ? ((LocaleHelper.locale?.countryCode != null) ? - "${LocaleHelper.locale?.languageCode.toLowerCase()}_${LocaleHelper.locale?.countryCode?.toUpperCase()}" : LocaleHelper.locale.toString()) : - "en_US"; + final String localeString = (LocaleHelper.locale != null) + ? ((LocaleHelper.locale?.countryCode != null) + ? "${LocaleHelper.locale?.languageCode.toLowerCase()}_${LocaleHelper.locale?.countryCode?.toUpperCase()}" + : LocaleHelper.locale.toString()) + : "en_US"; initializeDateFormatting(localeString, null); runApp(const Finamp()); @@ -310,7 +312,8 @@ class Finamp extends StatelessWidget { const DownloadsScreen(), DownloadsErrorScreen.routeName: (context) => const DownloadsErrorScreen(), - PlaybackHistoryScreen.routeName: (context) => const PlaybackHistoryScreen(), + PlaybackHistoryScreen.routeName: (context) => + const PlaybackHistoryScreen(), LogsScreen.routeName: (context) => const LogsScreen(), SettingsScreen.routeName: (context) => const SettingsScreen(), diff --git a/lib/screens/blurred_player_screen_background.dart b/lib/screens/blurred_player_screen_background.dart index a036fefdf..be39b6035 100644 --- a/lib/screens/blurred_player_screen_background.dart +++ b/lib/screens/blurred_player_screen_background.dart @@ -9,10 +9,9 @@ import '../services/current_album_image_provider.dart'; /// Same as [_PlayerScreenAlbumImage], but with a BlurHash instead. We also /// filter the BlurHash so that it works as a background image. class BlurredPlayerScreenBackground extends ConsumerWidget { - /// should never be less than 1.0 final double brightnessFactor; - + const BlurredPlayerScreenBackground({ Key? key, this.brightnessFactor = 1.0, diff --git a/lib/screens/music_screen.dart b/lib/screens/music_screen.dart index 4312f50a7..53da5c6a9 100644 --- a/lib/screens/music_screen.dart +++ b/lib/screens/music_screen.dart @@ -161,11 +161,10 @@ class _MusicScreenState extends State @override Widget build(BuildContext context) { - if (_tabController == null) { _buildTabController(); } - + return ValueListenableBuilder>( valueListenable: _finampUserHelper.finampUsersListenable, builder: (context, value, _) { @@ -242,7 +241,7 @@ class _MusicScreenState extends State IconButton( icon: const Icon(TablerIcons.clock), onPressed: () => Navigator.of(context) - .pushNamed(PlaybackHistoryScreen.routeName), + .pushNamed(PlaybackHistoryScreen.routeName), tooltip: "Playback History", ), SortOrderButton( diff --git a/lib/services/audio_service_helper.dart b/lib/services/audio_service_helper.dart index 06eeb060a..a8e847ebd 100644 --- a/lib/services/audio_service_helper.dart +++ b/lib/services/audio_service_helper.dart @@ -55,11 +55,15 @@ class AudioServiceHelper { if (items != null) { await _queueService.startPlayback( - items: items, + items: items, source: QueueItemSource( - type: isFavourite ? QueueItemSourceType.favorites : QueueItemSourceType.allSongs, + type: isFavourite + ? QueueItemSourceType.favorites + : QueueItemSourceType.allSongs, name: QueueItemSourceName( - type: isFavourite ? QueueItemSourceNameType.yourLikes : QueueItemSourceNameType.shuffleAll, + type: isFavourite + ? QueueItemSourceNameType.yourLikes + : QueueItemSourceNameType.shuffleAll, ), id: "shuffleAll", ), @@ -78,14 +82,16 @@ class AudioServiceHelper { await _queueService.startPlayback( items: items, source: QueueItemSource( - type: QueueItemSourceType.songMix, - name: QueueItemSourceName( - type: item.name != null ? QueueItemSourceNameType.mix : QueueItemSourceNameType.instantMix, - localizationParameter: item.name ?? "", - ), - id: item.id - ), - order: FinampPlaybackOrder.linear, // instant mixes should have their order determined by the server, especially to make sure the first item is the one that the mix is based off of + type: QueueItemSourceType.songMix, + name: QueueItemSourceName( + type: item.name != null + ? QueueItemSourceNameType.mix + : QueueItemSourceNameType.instantMix, + localizationParameter: item.name ?? "", + ), + id: item.id), + order: FinampPlaybackOrder + .linear, // instant mixes should have their order determined by the server, especially to make sure the first item is the one that the mix is based off of ); } } catch (e) { @@ -99,17 +105,21 @@ class AudioServiceHelper { List? items; try { - items = await _jellyfinApiHelper.getArtistMix(artists.map((e) => e.id).toList()); + items = await _jellyfinApiHelper + .getArtistMix(artists.map((e) => e.id).toList()); if (items != null) { await _queueService.startPlayback( items: items, source: QueueItemSource( type: QueueItemSourceType.artistMix, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: artists.map((e) => e.name).join(" & ")), + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: artists.map((e) => e.name).join(" & ")), id: artists.first.id, item: artists.first, ), - order: FinampPlaybackOrder.linear, // instant mixes should have their order determined by the server, especially to make sure the first item is the one that the mix is based off of + order: FinampPlaybackOrder + .linear, // instant mixes should have their order determined by the server, especially to make sure the first item is the one that the mix is based off of ); _jellyfinApiHelper.clearArtistMixBuilderList(); } @@ -124,17 +134,21 @@ class AudioServiceHelper { List? items; try { - items = await _jellyfinApiHelper.getAlbumMix(albums.map((e) => e.id).toList()); + items = await _jellyfinApiHelper + .getAlbumMix(albums.map((e) => e.id).toList()); if (items != null) { await _queueService.startPlayback( items: items, source: QueueItemSource( type: QueueItemSourceType.albumMix, - name: QueueItemSourceName(type: QueueItemSourceNameType.preTranslated, pretranslatedName: albums.map((e) => e.name).join(" & ")), + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: albums.map((e) => e.name).join(" & ")), id: albums.first.id, item: albums.first, ), - order: FinampPlaybackOrder.linear, // instant mixes should have their order determined by the server, especially to make sure the first item is the one that the mix is based off of + order: FinampPlaybackOrder + .linear, // instant mixes should have their order determined by the server, especially to make sure the first item is the one that the mix is based off of ); _jellyfinApiHelper.clearAlbumMixBuilderList(); } @@ -143,5 +157,4 @@ class AudioServiceHelper { return Future.error(e); } } - } diff --git a/lib/services/jellyfin_api.dart b/lib/services/jellyfin_api.dart index 07c028cb1..e3f841ece 100644 --- a/lib/services/jellyfin_api.dart +++ b/lib/services/jellyfin_api.dart @@ -311,7 +311,7 @@ abstract class JellyfinApi extends ChopperService { /// "RefreshState" "ChannelImage" "EnableMediaSourceDisplay" "Width" /// "Height" "ExtraIds" "LocalTrailerCount" "IsHD" "SpecialFeatureCount" @Query("Fields") String? fields = defaultFields, - + /// Optional. Filter based on a search term. @Query("SearchTerm") String? searchTerm, @@ -359,9 +359,9 @@ abstract class JellyfinApi extends ChopperService { Future logout(); static JellyfinApi create() { + final chopperHttpLogLevel = Level + .body; //TODO allow changing the log level in settings (and a debug config file?) - final chopperHttpLogLevel = Level.body; //TODO allow changing the log level in settings (and a debug config file?) - final client = ChopperClient( // The first part of the URL is now here services: [ diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index ec6f4b25f..de60a51a4 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -14,7 +14,9 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { audioLoadConfiguration: AudioLoadConfiguration( androidLoadControl: AndroidLoadControl( minBufferDuration: FinampSettingsHelper.finampSettings.bufferDuration, - maxBufferDuration: FinampSettingsHelper.finampSettings.bufferDuration * 1.5, // allows the player to fetch a bit more data in exchange for reduced request frequency + maxBufferDuration: FinampSettingsHelper + .finampSettings.bufferDuration * + 1.5, // allows the player to fetch a bit more data in exchange for reduced request frequency prioritizeTimeOverSizeThresholds: true, ), darwinLoadControl: DarwinLoadControl( @@ -175,7 +177,8 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { if (doSkip) { if (_player.loopMode == LoopMode.one) { // if the user manually skips to the previous track, they probably want to actually skip to the previous track - await skipByOffset(-1); //!!! don't use _player.previousIndex here, because that adjusts based on loop mode + await skipByOffset( + -1); //!!! don't use _player.previousIndex here, because that adjusts based on loop mode } else { await _player.seekToPrevious(); } @@ -194,7 +197,8 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { try { if (_player.loopMode == LoopMode.one && _player.hasNext) { // if the user manually skips to the next track, they probably want to actually skip to the next track - await skipByOffset(1); //!!! don't use _player.nextIndex here, because that adjusts based on loop mode + await skipByOffset( + 1); //!!! don't use _player.nextIndex here, because that adjusts based on loop mode } else { await _player.seekToNext(); } diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart index ce2f9ba81..e11754dc1 100644 --- a/lib/services/playback_history_service.dart +++ b/lib/services/playback_history_service.dart @@ -24,7 +24,8 @@ class PlaybackHistoryService { // internal state - final List _history = []; // contains **all** items that have been played, including "next up" + final List _history = + []; // contains **all** items that have been played, including "next up" FinampHistoryItem? _currentTrack; // the currently playing track PlaybackState? _previousPlaybackState; @@ -33,10 +34,9 @@ class PlaybackHistoryService { final _historyStream = BehaviorSubject>.seeded( List.empty(growable: true), - ); + ); PlaybackHistoryService() { - _queueService.getCurrentTrackStream().listen((currentTrack) { updateCurrentTrack(currentTrack); @@ -46,7 +46,6 @@ class PlaybackHistoryService { }); _audioService.playbackState.listen((event) { - final prevState = _previousPlaybackState; final prevItem = _currentTrack?.item; final currentState = event; @@ -55,36 +54,41 @@ class PlaybackHistoryService { final currentItem = _queueService.getCurrentTrack(); if (currentIndex != null && currentItem != null) { - // differences in queue index or item id are considered track changes - if (currentItem.id != prevItem?.id || (_reportQueueToServer && currentIndex != prevState?.queueIndex)) { - _playbackHistoryServiceLogger.fine("Reporting track change event from ${prevItem?.item.title} to ${currentItem.item.title}"); + if (currentItem.id != prevItem?.id || + (_reportQueueToServer && currentIndex != prevState?.queueIndex)) { + _playbackHistoryServiceLogger.fine( + "Reporting track change event from ${prevItem?.item.title} to ${currentItem.item.title}"); //TODO handle reporting track changes based on history changes, as that is more reliable - onTrackChanged(currentItem, currentState, prevItem, prevState, currentIndex > (prevState?.queueIndex ?? 0)); + onTrackChanged(currentItem, currentState, prevItem, prevState, + currentIndex > (prevState?.queueIndex ?? 0)); } // handle events that don't change the current track (e.g. loop, pause, seek, etc.) // handle play/pause events else if (currentState.playing != prevState?.playing) { - _playbackHistoryServiceLogger.fine("Reporting play/pause event for ${currentItem.item.title}"); + _playbackHistoryServiceLogger + .fine("Reporting play/pause event for ${currentItem.item.title}"); onPlaybackStateChanged(currentItem, currentState); } // handle seeking (changes updateTime (= last abnormal position change)) - else if (currentState.playing && currentState.updateTime != prevState?.updateTime && currentState.bufferedPosition == prevState?.bufferedPosition) { - + else if (currentState.playing && + currentState.updateTime != prevState?.updateTime && + currentState.bufferedPosition == prevState?.bufferedPosition) { // detect rewinding & looping a single track if ( - // same track - prevItem?.id == currentItem.id && - // current position is close to the beginning of the track - currentState.position.inMilliseconds <= 1000 * 10 - ) { - if ((prevState?.position.inMilliseconds ?? 0) >= ((prevItem?.item.duration?.inMilliseconds ?? 0) - 1000 * 10)) { + // same track + prevItem?.id == currentItem.id && + // current position is close to the beginning of the track + currentState.position.inMilliseconds <= 1000 * 10) { + if ((prevState?.position.inMilliseconds ?? 0) >= + ((prevItem?.item.duration?.inMilliseconds ?? 0) - 1000 * 10)) { // looping a single track // last position was close to the end of the track updateCurrentTrack(currentItem); // add to playback history //TODO handle reporting track changes based on history changes, as that is more reliable - onTrackChanged(currentItem, currentState, prevItem, prevState, true); + onTrackChanged( + currentItem, currentState, prevItem, prevState, true); return; // don't report seek event } else { // rewinding @@ -95,20 +99,19 @@ class PlaybackHistoryService { // rate limit updates (only send update after no changes for 3 seconds) and if the track is still the same Future.delayed(const Duration(seconds: 3, milliseconds: 500), () { - if ( - _lastPositionUpdate.add(const Duration(seconds: 3)).isBefore(DateTime.now()) && - currentItem.id == _queueService.getCurrentTrack()?.id - ) { - _playbackHistoryServiceLogger.fine("Reporting seek event for ${currentItem.item.title}"); + if (_lastPositionUpdate + .add(const Duration(seconds: 3)) + .isBefore(DateTime.now()) && + currentItem.id == _queueService.getCurrentTrack()?.id) { + _playbackHistoryServiceLogger + .fine("Reporting seek event for ${currentItem.item.title}"); onPlaybackStateChanged(currentItem, currentState); } _lastPositionUpdate = DateTime.now(); }); - } // maybe handle toggling shuffle when sending the queue? would result in duplicate entries in the activity log, so maybe it's not desirable // same for updating the queue / next up - } _previousPlaybackState = event; @@ -116,23 +119,26 @@ class PlaybackHistoryService { //TODO Tell Jellyfin we're not / no longer playing audio on startup - doesn't currently work because an item ID is required, and we don't have one (yet) // if (!FinampSettingsHelper.finampSettings.isOffline) { - // final playbackInfo = generatePlaybackProgressInfoFromState(const MediaItem(id: "", title: ""), _audioService.playbackState.valueOrNull ?? PlaybackState()); - // if (playbackInfo != null) { - // _playbackHistoryServiceLogger.info("Stopping playback progress after startup"); - // _jellyfinApiHelper.stopPlaybackProgress(playbackInfo); - // } + // final playbackInfo = generatePlaybackProgressInfoFromState(const MediaItem(id: "", title: ""), _audioService.playbackState.valueOrNull ?? PlaybackState()); + // if (playbackInfo != null) { + // _playbackHistoryServiceLogger.info("Stopping playback progress after startup"); + // _jellyfinApiHelper.stopPlaybackProgress(playbackInfo); + // } // } - } get history => _history; BehaviorSubject> get historyStream => _historyStream; /// method that converts history into a list grouped by date - List>> getHistoryGroupedDynamically() { + List>> + getHistoryGroupedDynamically() { byDateGroupingConstructor(FinampHistoryItem element) { final now = DateTime.now(); - if (now.year == element.startTime.year && now.month == element.startTime.month && now.day == element.startTime.day && now.hour == element.startTime.hour) { + if (now.year == element.startTime.year && + now.month == element.startTime.month && + now.day == element.startTime.day && + now.hour == element.startTime.hour) { // group by minute return DateTime( element.startTime.year, @@ -141,8 +147,9 @@ class PlaybackHistoryService { element.startTime.hour, element.startTime.minute, ); - } - else if (now.year == element.startTime.year && now.month == element.startTime.month && now.day == element.startTime.day) { + } else if (now.year == element.startTime.year && + now.month == element.startTime.month && + now.day == element.startTime.day) { // group by hour return DateTime( element.startTime.year, @@ -157,7 +164,6 @@ class PlaybackHistoryService { element.startTime.month, element.startTime.day, ); - } return getHistoryGrouped(byDateGroupingConstructor); @@ -171,7 +177,6 @@ class PlaybackHistoryService { element.startTime.month, element.startTime.day, ); - } return getHistoryGrouped(byDateGroupingConstructor); @@ -186,14 +191,14 @@ class PlaybackHistoryService { element.startTime.day, element.startTime.hour, ); - } return getHistoryGrouped(byHourGroupingConstructor); } /// method that converts history into a list grouped by a custom date constructor controlling the granularity of the grouping - List>> getHistoryGrouped(DateTime Function (FinampHistoryItem) dateTimeConstructor) { + List>> getHistoryGrouped( + DateTime Function(FinampHistoryItem) dateTimeConstructor) { final groupedHistory = >>[]; final groupedHistoryMap = >{}; @@ -219,8 +224,10 @@ class PlaybackHistoryService { } void updateCurrentTrack(FinampQueueItem? currentTrack) { - - if (currentTrack == null || currentTrack == _currentTrack?.item || currentTrack.item.id == "" || currentTrack.id == _currentTrack?.item.id) { + if (currentTrack == null || + currentTrack == _currentTrack?.item || + currentTrack.item.id == "" || + currentTrack.id == _currentTrack?.item.id) { // current track hasn't changed return; } @@ -230,7 +237,9 @@ class PlaybackHistoryService { if (_currentTrack != null) { // update end time of previous track _currentTrack!.endTime = DateTime.now(); - previousTrackTotalPlayTimeInMilliseconds = _currentTrack!.endTime!.difference(_currentTrack!.startTime).inMilliseconds; + previousTrackTotalPlayTimeInMilliseconds = _currentTrack!.endTime! + .difference(_currentTrack!.startTime) + .inMilliseconds; } if (previousTrackTotalPlayTimeInMilliseconds < 1000) { @@ -245,10 +254,10 @@ class PlaybackHistoryService { item: currentTrack, startTime: DateTime.now(), ); - _history.add(_currentTrack!); // current track is always the last item in the history + _history.add( + _currentTrack!); // current track is always the last item in the history _historyStream.add(_history); - } /// Report track changes to the Jellyfin Server if the user is not offline. @@ -273,7 +282,6 @@ class PlaybackHistoryService { previousItem.item, previousState, ); - } // prevent reporting the same track twice if playback hasn't started yet @@ -288,12 +296,14 @@ class PlaybackHistoryService { //!!! always submit a "start" **AFTER** a "stop" to that Jellyfin knows there's still something playing if (previousTrackPlaybackData != null) { - _playbackHistoryServiceLogger.info("Stopping playback progress for ${previousItem?.item.title}"); + _playbackHistoryServiceLogger + .info("Stopping playback progress for ${previousItem?.item.title}"); await _jellyfinApiHelper.stopPlaybackProgress(previousTrackPlaybackData); //TODO also mark the track as played in the user data: https://api.jellyfin.org/openapi/api.html#tag/Playstate/operation/MarkPlayedItem } if (newTrackplaybackData != null) { - _playbackHistoryServiceLogger.info("Starting playback progress for ${currentItem.item.title}"); + _playbackHistoryServiceLogger + .info("Starting playback progress for ${currentItem.item.title}"); await _jellyfinApiHelper.reportPlaybackStart(newTrackplaybackData); } } @@ -313,11 +323,14 @@ class PlaybackHistoryService { ); if (playbackData != null) { - if (![AudioProcessingState.completed, AudioProcessingState.idle].contains(currentState.processingState)) { - _playbackHistoryServiceLogger.info("Starting playback progress for ${currentItem.item.title}"); + if (![AudioProcessingState.completed, AudioProcessingState.idle] + .contains(currentState.processingState)) { + _playbackHistoryServiceLogger + .info("Starting playback progress for ${currentItem.item.title}"); await _jellyfinApiHelper.reportPlaybackStart(playbackData); } else { - _playbackHistoryServiceLogger.info("Stopping playback progress for ${currentItem.item.title}"); + _playbackHistoryServiceLogger + .info("Stopping playback progress for ${currentItem.item.title}"); await _jellyfinApiHelper.stopPlaybackProgress(playbackData); } } @@ -344,12 +357,10 @@ class PlaybackHistoryService { } Future _reportPlaybackStopped() async { - final playbackInfo = generateGenericPlaybackProgressInfo(); if (playbackInfo != null) { await _jellyfinApiHelper.stopPlaybackProgress(playbackInfo); } - } /// Generates PlaybackProgressInfo for the supplied item and player info. @@ -362,17 +373,17 @@ class PlaybackHistoryService { required bool includeNowPlayingQueue, }) { try { - List? nowPlayingQueue; if (includeNowPlayingQueue) { - nowPlayingQueue = _queueService.getNextXTracksInQueue(30) + nowPlayingQueue = _queueService + .getNextXTracksInQueue(30) .map((e) => jellyfin_models.QueueItem( - id: e.item.id, - playlistItemId: e.source.id, - )) + id: e.item.id, + playlistItemId: e.source.id, + )) .toList(); } - + return jellyfin_models.PlaybackProgressInfo( itemId: item.extras?["itemJson"]["Id"] ?? "", isPaused: isPaused, @@ -401,7 +412,6 @@ class PlaybackHistoryService { } try { - final itemId = _currentTrack!.item.item.extras?["itemJson"]["Id"]; if (itemId == null) { @@ -410,7 +420,7 @@ class PlaybackHistoryService { ); return null; } - + return jellyfin_models.PlaybackProgressInfo( itemId: _currentTrack!.item.item.extras?["itemJson"]["Id"], isPaused: _audioService.paused, @@ -418,7 +428,8 @@ class PlaybackHistoryService { volumeLevel: _audioService.volume.round(), positionTicks: _audioService.playbackPosition.inMicroseconds * 10, repeatMode: _toJellyfinRepeatMode(_queueService.loopMode), - playbackStartTimeTicks: _currentTrack!.startTime.millisecondsSinceEpoch * 1000 * 10, + playbackStartTimeTicks: + _currentTrack!.startTime.millisecondsSinceEpoch * 1000 * 10, playMethod: _currentTrack!.item.item.extras!["shouldTranscode"] ? "Transcode" : "DirectPlay", @@ -426,13 +437,16 @@ class PlaybackHistoryService { // issues with large queues. // https://github.com/jmshrv/finamp/issues/387 nowPlayingQueue: includeNowPlayingQueue - ? _queueService.getQueue().nextUp.followedBy(_queueService.getQueue().queue) + ? _queueService + .getQueue() + .nextUp + .followedBy(_queueService.getQueue().queue) .map( (e) => jellyfin_models.QueueItem( id: e.item.extras!["itemJson"]["Id"], - playlistItemId: e.item.id - ), - ).toList() + playlistItemId: e.item.id), + ) + .toList() : null, ); } catch (e) { diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index a515157ee..1e26d0eb7 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -50,7 +50,8 @@ class QueueService { final _playbackOrderStream = BehaviorSubject.seeded(FinampPlaybackOrder.linear); - final _loopModeStream = BehaviorSubject.seeded(FinampLoopMode.none); + final _loopModeStream = + BehaviorSubject.seeded(FinampLoopMode.none); // external queue state @@ -82,10 +83,11 @@ class QueueService { _queueAudioSourceIndex = event.queueIndex ?? 0; if (previousIndex != _queueAudioSourceIndex) { - int adjustedQueueIndex = (playbackOrder == FinampPlaybackOrder.shuffled && - _queueAudioSource.shuffleIndices.isNotEmpty) - ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) - : _queueAudioSourceIndex; + int adjustedQueueIndex = (playbackOrder == + FinampPlaybackOrder.shuffled && + _queueAudioSource.shuffleIndices.isNotEmpty) + ? _queueAudioSource.shuffleIndices.indexOf(_queueAudioSourceIndex) + : _queueAudioSourceIndex; _queueServiceLogger.finer( "Play queue index changed, new index: $adjustedQueueIndex (actual index: $_queueAudioSourceIndex)"); _queueFromConcatenatingAudioSource(); @@ -125,7 +127,12 @@ class QueueService { for (int i = 0; i < allTracks.length; i++) { if (i < adjustedQueueIndex) { _queuePreviousTracks.add(allTracks[i]); - if ([QueueItemSourceType.nextUp, QueueItemSourceType.nextUpAlbum, QueueItemSourceType.nextUpPlaylist, QueueItemSourceType.nextUpArtist].contains(_queuePreviousTracks.last.source.type)) { + if ([ + QueueItemSourceType.nextUp, + QueueItemSourceType.nextUpAlbum, + QueueItemSourceType.nextUpPlaylist, + QueueItemSourceType.nextUpArtist + ].contains(_queuePreviousTracks.last.source.type)) { _queuePreviousTracks.last.source = QueueItemSource( type: QueueItemSourceType.formerNextUp, name: const QueueItemSourceName( @@ -137,22 +144,26 @@ class QueueService { _currentTrack = allTracks[i]; _currentTrack!.type = QueueItemQueueType.currentTrack; } else { - if (allTracks[i].type == QueueItemQueueType.currentTrack && [QueueItemSourceType.nextUp, QueueItemSourceType.nextUpAlbum, QueueItemSourceType.nextUpPlaylist, QueueItemSourceType.nextUpArtist].contains(allTracks[i].source.type)) { + if (allTracks[i].type == QueueItemQueueType.currentTrack && + [ + QueueItemSourceType.nextUp, + QueueItemSourceType.nextUpAlbum, + QueueItemSourceType.nextUpPlaylist, + QueueItemSourceType.nextUpArtist + ].contains(allTracks[i].source.type)) { _queue.add(allTracks[i]); _queue.last.type = QueueItemQueueType.queue; _queue.last.source = QueueItemSource( - type: QueueItemSourceType.formerNextUp, - name: const QueueItemSourceName(type: QueueItemSourceNameType.tracksFormerNextUp), - id: "former-next-up" - ); + type: QueueItemSourceType.formerNextUp, + name: const QueueItemSourceName( + type: QueueItemSourceNameType.tracksFormerNextUp), + id: "former-next-up"); canHaveNextUp = false; - } - else if (allTracks[i].type == QueueItemQueueType.nextUp) { + } else if (allTracks[i].type == QueueItemQueueType.nextUp) { if (canHaveNextUp) { _queueNextUp.add(allTracks[i]); _queueNextUp.last.type = QueueItemQueueType.nextUp; - } - else { + } else { _queue.add(allTracks[i]); _queue.last.type = QueueItemQueueType.queue; _queue.last.source = QueueItemSource( @@ -186,12 +197,10 @@ class QueueService { .toList()); // only log queue if there's a change - if ( - previousTrack?.id != _currentTrack?.id || - previousTracksPreviousLength != _queuePreviousTracks.length || - nextUpPreviousLength != _queueNextUp.length || - queuePreviousLength != _queue.length - ) { + if (previousTrack?.id != _currentTrack?.id || + previousTracksPreviousLength != _queuePreviousTracks.length || + nextUpPreviousLength != _queueNextUp.length || + queuePreviousLength != _queue.length) { _logQueues(message: "(current)"); } } @@ -213,7 +222,10 @@ class QueueService { } await _replaceWholeQueue( - itemList: items, source: source, order: order, initialIndex: startingIndex); + itemList: items, + source: source, + order: order, + initialIndex: startingIndex); _queueServiceLogger .info("Started playing '${source.name}' (${source.type})"); } @@ -287,7 +299,7 @@ class QueueService { ); _queueServiceLogger.fine("Order items length: ${_order.items.length}"); - + // set playback order to trigger shuffle if necessary (fixes indices being wrong when starting with shuffle enabled) if (order != null) { @@ -319,19 +331,17 @@ class QueueService { } Future addToQueue({ - required List items, - QueueItemSource? source, + required List items, + QueueItemSource? source, }) async { try { List queueItems = []; for (final item in items) { - queueItems.add(FinampQueueItem( item: await _generateMediaItem(item), source: source ?? _order.originalSource, type: QueueItemQueueType.queue, )); - } List audioSources = []; @@ -450,7 +460,6 @@ class QueueService { } Future clearNextUp() async { - // remove all items from Next Up if (_queueNextUp.isNotEmpty) { await _queueAudioSource.removeRange(_queueAudioSourceIndex + 1, @@ -524,8 +533,8 @@ class QueueService { } FinampSettingsHelper.setLoopMode(loopMode); - _queueServiceLogger.fine("Loop mode set to ${FinampSettingsHelper.finampSettings.loopMode}"); - + _queueServiceLogger.fine( + "Loop mode set to ${FinampSettingsHelper.finampSettings.loopMode}"); } FinampLoopMode get loopMode => _loopMode; @@ -538,11 +547,9 @@ class QueueService { // update queue accordingly and generate new shuffled order if necessary if (_playbackOrder == FinampPlaybackOrder.shuffled) { - _audioHandler.shuffle().then((_) => - _audioHandler - .setShuffleMode(AudioServiceShuffleMode.all) - .then((_) => _queueFromConcatenatingAudioSource()) - ); + _audioHandler.shuffle().then((_) => _audioHandler + .setShuffleMode(AudioServiceShuffleMode.all) + .then((_) => _queueFromConcatenatingAudioSource())); } else { _audioHandler .setShuffleMode(AudioServiceShuffleMode.none) From c99817bc663caf50553509923b11310ae8b872c4 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 19 Nov 2023 02:36:44 +0100 Subject: [PATCH 128/130] fix overflow on now playing bar --- lib/components/now_playing_bar.dart | 44 +++++++++++++++-------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index 6441bc6f1..cbd3801c2 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -313,27 +313,29 @@ class NowPlayingBar extends ConsumerWidget { MainAxisAlignment .spaceBetween, children: [ - Text( - processArtist( - currentTrack! - .item - .artist, - context), - style: TextStyle( - color: Colors - .white - .withOpacity( - 0.85), - fontSize: - 13, - fontFamily: - 'Lexend Deca', - fontWeight: - FontWeight - .w300, - overflow: - TextOverflow - .ellipsis), + Expanded( + child: Text( + processArtist( + currentTrack! + .item + .artist, + context), + style: TextStyle( + color: Colors + .white + .withOpacity( + 0.85), + fontSize: + 13, + fontFamily: + 'Lexend Deca', + fontWeight: + FontWeight + .w300, + overflow: + TextOverflow + .ellipsis), + ), ), Row( children: [ From ba78f9214fa0d2243e910fdd309b9c6244fb4c80 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 19 Nov 2023 02:39:53 +0100 Subject: [PATCH 129/130] go back to metastable to get initial index to work - why is this so hard? :(( --- .../music_player_background_task.dart | 13 ++++++++++ lib/services/playback_history_service.dart | 17 ++++++++----- lib/services/queue_service.dart | 25 +++++++++---------- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index de60a51a4..83578c92c 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -227,6 +227,19 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } } + Future skipToIndex(int index) async { + _audioServiceBackgroundTaskLogger.fine("skipping to index: $index"); + + try { + await _player.seek(Duration.zero, index: _player.shuffleModeEnabled + ? _queueAudioSource.shuffleIndices[index] + : index); + } catch (e) { + _audioServiceBackgroundTaskLogger.severe(e); + return Future.error(e); + } + } + @override Future seek(Duration position) async { try { diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart index e11754dc1..8a67cb602 100644 --- a/lib/services/playback_history_service.dart +++ b/lib/services/playback_history_service.dart @@ -85,14 +85,14 @@ class PlaybackHistoryService { ((prevItem?.item.duration?.inMilliseconds ?? 0) - 1000 * 10)) { // looping a single track // last position was close to the end of the track - updateCurrentTrack(currentItem); // add to playback history + updateCurrentTrack(currentItem, forceNewTrack: true); // add to playback history //TODO handle reporting track changes based on history changes, as that is more reliable onTrackChanged( currentItem, currentState, prevItem, prevState, true); return; // don't report seek event } else { // rewinding - updateCurrentTrack(currentItem); // add to playback history + updateCurrentTrack(currentItem, forceNewTrack: true); // add to playback history // don't return, report seek event } } @@ -223,11 +223,16 @@ class PlaybackHistoryService { return groupedHistory; } - void updateCurrentTrack(FinampQueueItem? currentTrack) { + void updateCurrentTrack(FinampQueueItem? currentTrack, { + bool forceNewTrack = false, + }) { if (currentTrack == null || - currentTrack == _currentTrack?.item || - currentTrack.item.id == "" || - currentTrack.id == _currentTrack?.item.id) { + !forceNewTrack && ( + currentTrack == _currentTrack?.item || + currentTrack.item.id == "" || + currentTrack.id == _currentTrack?.item.id + ) + ) { // current track hasn't changed return; } diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 1e26d0eb7..b32f69513 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -271,7 +271,7 @@ class QueueService { await _audioHandler.stop(); _queueAudioSource.clear(); - await _audioHandler.initializeAudioSource(_queueAudioSource); + // await _audioHandler.initializeAudioSource(_queueAudioSource); List audioSources = []; @@ -281,14 +281,6 @@ class QueueService { await _queueAudioSource.addAll(audioSources); - // set first item in queue - _queueAudioSourceIndex = initialIndex; - if (_playbackOrder == FinampPlaybackOrder.shuffled) { - _queueAudioSourceIndex = _queueAudioSource.shuffleIndices[initialIndex]; - } - _audioHandler.setNextInitialIndex(_queueAudioSourceIndex); - // await _audioHandler.initializeAudioSource(_queueAudioSource); - newShuffledOrder = List.from(_queueAudioSource.shuffleIndices); _order = FinampQueueOrder( @@ -297,15 +289,22 @@ class QueueService { linearOrder: newLinearOrder, shuffledOrder: newShuffledOrder, ); - - _queueServiceLogger.fine("Order items length: ${_order.items.length}"); - // set playback order to trigger shuffle if necessary (fixes indices being wrong when starting with shuffle enabled) - if (order != null) { playbackOrder = order; } + // set first item in queue + _queueAudioSourceIndex = initialIndex; + if (_playbackOrder == FinampPlaybackOrder.shuffled) { + _queueAudioSourceIndex = _queueAudioSource.shuffleIndices[initialIndex]; + } + _audioHandler.setNextInitialIndex(_queueAudioSourceIndex); + await _audioHandler.initializeAudioSource(_queueAudioSource); + // _audioHandler.skipToIndex(_queueAudioSourceIndex); + + _queueServiceLogger.fine("Order items length: ${_order.items.length}"); + // _queueStream.add(getQueue()); _queueFromConcatenatingAudioSource(); From ce6ed6b5e9136bb67dc77f412c4637bd6c907b5f Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sun, 19 Nov 2023 12:47:21 +0100 Subject: [PATCH 130/130] fix shuffled queue not being played in order --- lib/services/queue_service.dart | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index b32f69513..65d4fc9ce 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -281,6 +281,14 @@ class QueueService { await _queueAudioSource.addAll(audioSources); + // set first item in queue + _queueAudioSourceIndex = initialIndex; + if (_playbackOrder == FinampPlaybackOrder.shuffled) { + _queueAudioSourceIndex = _queueAudioSource.shuffleIndices[initialIndex]; + } + _audioHandler.setNextInitialIndex(_queueAudioSourceIndex); + await _audioHandler.initializeAudioSource(_queueAudioSource); + newShuffledOrder = List.from(_queueAudioSource.shuffleIndices); _order = FinampQueueOrder( @@ -289,22 +297,15 @@ class QueueService { linearOrder: newLinearOrder, shuffledOrder: newShuffledOrder, ); + + _queueServiceLogger.fine("Order items length: ${_order.items.length}"); + // set playback order to trigger shuffle if necessary (fixes indices being wrong when starting with shuffle enabled) + if (order != null) { playbackOrder = order; } - // set first item in queue - _queueAudioSourceIndex = initialIndex; - if (_playbackOrder == FinampPlaybackOrder.shuffled) { - _queueAudioSourceIndex = _queueAudioSource.shuffleIndices[initialIndex]; - } - _audioHandler.setNextInitialIndex(_queueAudioSourceIndex); - await _audioHandler.initializeAudioSource(_queueAudioSource); - // _audioHandler.skipToIndex(_queueAudioSourceIndex); - - _queueServiceLogger.fine("Order items length: ${_order.items.length}"); - // _queueStream.add(getQueue()); _queueFromConcatenatingAudioSource();