From 5a33802d4f0d782c1d588c24faf8aa0c2e9e66e1 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 4 May 2024 14:06:38 +0200 Subject: [PATCH] run `dart format` --- lib/color_schemes.g.dart | 2 +- .../add_to_playlist_list.dart | 5 +- .../new_playlist_dialog.dart | 7 +- ...bum_screen_content_flexible_space_bar.dart | 73 ++-- .../AlbumScreen/download_dialog.dart | 8 +- .../AlbumScreen/song_list_tile.dart | 34 +- lib/components/AlbumScreen/song_menu.dart | 373 +++++++++--------- ...ist_screen_content_flexible_space_bar.dart | 69 ++-- ...yback_session_update_frequency_editor.dart | 49 ++- .../report_queue_to_server_toggle.dart | 4 +- lib/components/Buttons/cta_large.dart | 20 +- lib/components/Buttons/cta_medium.dart | 12 +- lib/components/Buttons/simple_button.dart | 27 +- .../FastScrollSelector.dart | 2 +- ...control_visibility_dropdown_list_tile.dart | 10 +- ...r_screen_minimum_cover_padding_editor.dart | 13 +- .../use_cover_as_background_toggle.dart | 10 +- .../login_server_selection_page.dart | 88 +++-- .../LoginScreen/login_splash_page.dart | 12 +- .../login_user_selection_page.dart | 120 +++--- .../LogsScreen/copy_logs_button.dart | 4 +- lib/components/LogsScreen/log_tile.dart | 2 +- lib/components/MusicScreen/album_item.dart | 79 ++-- .../MusicScreen/album_item_list_tile.dart | 76 ++-- .../MusicScreen/alphabet_item_list.dart | 45 +-- .../MusicScreen/music_screen_tab_view.dart | 17 +- .../MusicScreen/sort_order_button.dart | 2 +- .../playback_history_list.dart | 2 +- .../PlayerScreen/feature_chips.dart | 29 +- .../player_screen_album_image.dart | 6 +- lib/components/PlayerScreen/queue_list.dart | 6 +- .../PlayerScreen/sleep_timer_dialog.dart | 6 +- .../PlayerScreen/song_name_content.dart | 6 +- .../SettingsScreen/logout_list_tile.dart | 4 +- .../bitrate_selector.dart | 6 +- ...me_normalization_ios_base_gain_editor.dart | 13 +- .../volume_normalization_mode_selector.dart | 49 ++- .../volume_normalization_switch.dart | 10 +- lib/components/album_list_tile.dart | 36 +- lib/components/artist_text_spans.dart | 42 +- .../confirmation_prompt_dialog.dart | 7 +- lib/components/favourite_button.dart | 69 ++-- lib/components/finamp_app_bar_button.dart | 6 +- lib/components/global_snackbar.dart | 10 +- lib/components/now_playing_bar.dart | 3 +- lib/main.dart | 7 +- lib/models/finamp_models.dart | 16 +- lib/models/jellyfin_models.dart | 3 - .../customization_settings_screen.dart | 6 +- lib/screens/layout_settings_screen.dart | 7 +- lib/screens/lyrics_screen.dart | 336 ++++++++-------- lib/screens/music_screen.dart | 4 +- lib/screens/player_screen.dart | 85 ++-- lib/screens/settings_screen.dart | 4 +- lib/screens/tabs_settings_screen.dart | 4 +- .../volume_normalization_settings_screen.dart | 3 +- lib/services/album_image_provider.dart | 3 +- lib/services/censored_log.dart | 8 +- .../current_album_image_provider.dart | 2 +- .../current_track_metadata_provider.dart | 14 +- lib/services/downloads_service.dart | 31 +- lib/services/downloads_service_backend.dart | 22 +- lib/services/feedback_helper.dart | 2 +- lib/services/finamp_settings_helper.dart | 21 +- lib/services/generate_subtitle.dart | 10 +- lib/services/jellyfin_api.dart | 4 +- lib/services/jellyfin_api_helper.dart | 3 +- lib/services/metadata_provider.dart | 114 +++--- .../music_player_background_task.dart | 35 +- lib/services/offline_listen_helper.dart | 3 - lib/services/playback_history_service.dart | 78 ++-- lib/services/progress_state_stream.dart | 3 +- lib/services/queue_service.dart | 13 +- 73 files changed, 1304 insertions(+), 1010 deletions(-) diff --git a/lib/color_schemes.g.dart b/lib/color_schemes.g.dart index d16641529..1a7096733 100644 --- a/lib/color_schemes.g.dart +++ b/lib/color_schemes.g.dart @@ -81,4 +81,4 @@ const darkColorScheme = ColorScheme( surfaceTint: Color(0xFF7BD0FF), outlineVariant: Color(0xFF41484D), scrim: Color(0xFF000000), -); \ No newline at end of file +); diff --git a/lib/components/AddToPlaylistScreen/add_to_playlist_list.dart b/lib/components/AddToPlaylistScreen/add_to_playlist_list.dart index f501c7d60..27a4ed3de 100644 --- a/lib/components/AddToPlaylistScreen/add_to_playlist_list.dart +++ b/lib/components/AddToPlaylistScreen/add_to_playlist_list.dart @@ -66,7 +66,10 @@ class _AddToPlaylistListState extends State { keepSlow: true)); if (!mounted) return; - GlobalSnackbar.message((scaffold) => AppLocalizations.of(context)!.confirmAddedToPlaylist, isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(context)! + .confirmAddedToPlaylist, + isConfirmation: true); Navigator.pop(context); } catch (e) { errorSnackbar(e, context); diff --git a/lib/components/AddToPlaylistScreen/new_playlist_dialog.dart b/lib/components/AddToPlaylistScreen/new_playlist_dialog.dart index 33b276a58..b7e769b0c 100644 --- a/lib/components/AddToPlaylistScreen/new_playlist_dialog.dart +++ b/lib/components/AddToPlaylistScreen/new_playlist_dialog.dart @@ -73,7 +73,8 @@ class _NewPlaylistDialogState extends State { _formKey.currentState!.save(); try { - final newPlaylistResponse = await _jellyfinApiHelper.createNewPlaylist(NewPlaylist( + final newPlaylistResponse = + await _jellyfinApiHelper.createNewPlaylist(NewPlaylist( name: _name, ids: [widget.itemToAdd], userId: _finampUserHelper.currentUser!.id, @@ -89,8 +90,7 @@ class _NewPlaylistDialogState extends State { // resync all playlists, so the new playlist automatically gets downloaded if all playlists should be downloaded - final downloadsService = - GetIt.instance(); + final downloadsService = GetIt.instance(); unawaited(downloadsService.resync( DownloadStub.fromId( id: "All Playlists", @@ -99,7 +99,6 @@ class _NewPlaylistDialogState extends State { .finampCollectionNames("allPlaylists")), null, keepSlow: true)); - } catch (e) { errorSnackbar(e, context); setState(() { 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 7dacbdfcf..849a5e4c5 100644 --- a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart +++ b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart @@ -60,7 +60,8 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, - contextNormalizationGain: isPlaylist ? null : parentItem.normalizationGain, + contextNormalizationGain: + isPlaylist ? null : parentItem.normalizationGain, ), order: FinampPlaybackOrder.linear, ); @@ -79,7 +80,8 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, - contextNormalizationGain: isPlaylist ? null : parentItem.normalizationGain, + contextNormalizationGain: + isPlaylist ? null : parentItem.normalizationGain, ), order: FinampPlaybackOrder.shuffled, ); @@ -98,11 +100,14 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, - contextNormalizationGain: isPlaylist ? null : parentItem.normalizationGain, + contextNormalizationGain: + isPlaylist ? null : parentItem.normalizationGain, ), ); - GlobalSnackbar.message((scaffold) => AppLocalizations.of(scaffold)! - .confirmAddToNextUp(isPlaylist ? "playlist" : "album"), isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)! + .confirmAddToNextUp(isPlaylist ? "playlist" : "album"), + isConfirmation: true); } void addAlbumNext() { @@ -118,10 +123,13 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, - contextNormalizationGain: isPlaylist ? null : parentItem.normalizationGain, + contextNormalizationGain: + isPlaylist ? null : parentItem.normalizationGain, )); - GlobalSnackbar.message((scaffold) => AppLocalizations.of(scaffold)! - .confirmPlayNext(isPlaylist ? "playlist" : "album"), isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)! + .confirmPlayNext(isPlaylist ? "playlist" : "album"), + isConfirmation: true); } void shuffleAlbumToNextUp() { @@ -140,10 +148,12 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, - contextNormalizationGain: isPlaylist ? null : parentItem.normalizationGain, + contextNormalizationGain: + isPlaylist ? null : parentItem.normalizationGain, )); - GlobalSnackbar.message((scaffold) => AppLocalizations.of(scaffold)! - .confirmShuffleToNextUp, isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)!.confirmShuffleToNextUp, + isConfirmation: true); } void shuffleAlbumNext() { @@ -162,10 +172,12 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { AppLocalizations.of(context)!.placeholderSource), id: parentItem.id, item: parentItem, - contextNormalizationGain: isPlaylist ? null : parentItem.normalizationGain, + contextNormalizationGain: + isPlaylist ? null : parentItem.normalizationGain, )); - GlobalSnackbar.message((scaffold) => AppLocalizations.of(scaffold)! - .confirmShuffleNext, isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)!.confirmShuffleNext, + isConfirmation: true); } void addAlbumToQueue() { @@ -183,8 +195,10 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { item: parentItem, ), ); - GlobalSnackbar.message((scaffold) => AppLocalizations.of(scaffold)! - .confirmAddToQueue(isPlaylist ? "playlist" : "album"), isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)! + .confirmAddToQueue(isPlaylist ? "playlist" : "album"), + isConfirmation: true); } void shuffleAlbumToQueue() { @@ -204,8 +218,9 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { id: parentItem.id, item: parentItem, )); - GlobalSnackbar.message((scaffold) => AppLocalizations.of(scaffold)! - .confirmShuffleToQueue, isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)!.confirmShuffleToQueue, + isConfirmation: true); } return FlexibleSpaceBar( @@ -252,13 +267,14 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { icon: TablerIcons.player_play, onPressed: () => playAlbum(), // set the minimum width as 25% of the screen width, - minWidth: MediaQuery.of(context).size.width * 0.25, + minWidth: + MediaQuery.of(context).size.width * 0.25, ), PopupMenuButton( enableFeedback: true, icon: const Icon(TablerIcons.dots_vertical), - onOpened: () => - FeedbackHelper.feedback(FeedbackType.light), + onOpened: () => FeedbackHelper.feedback( + FeedbackType.light), itemBuilder: (context) { final queueService = GetIt.instance(); @@ -280,8 +296,8 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { PopupMenuItem( value: AlbumMenuItems.addToNextUp, child: ListTile( - leading: - const Icon(TablerIcons.corner_right_down_double), + leading: const Icon(TablerIcons + .corner_right_down_double), title: Text( AppLocalizations.of(context)! .addToNextUp), @@ -336,13 +352,14 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { icon: TablerIcons.arrows_shuffle, onPressed: () => shuffleAlbum(), // set the minimum width as 25% of the screen width, - minWidth: MediaQuery.of(context).size.width * 0.25, + minWidth: + MediaQuery.of(context).size.width * 0.25, ), PopupMenuButton( enableFeedback: true, icon: const Icon(TablerIcons.dots_vertical), - onOpened: () => - FeedbackHelper.feedback(FeedbackType.light), + onOpened: () => FeedbackHelper.feedback( + FeedbackType.light), itemBuilder: (context) { final queueService = GetIt.instance(); @@ -364,8 +381,8 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { PopupMenuItem( value: AlbumMenuItems.shuffleToNextUp, child: ListTile( - leading: - const Icon(TablerIcons.corner_right_down_double), + leading: const Icon(TablerIcons + .corner_right_down_double), title: Text( AppLocalizations.of(context)! .shuffleToNextUp), diff --git a/lib/components/AlbumScreen/download_dialog.dart b/lib/components/AlbumScreen/download_dialog.dart index ca122d5e7..23c94b858 100644 --- a/lib/components/AlbumScreen/download_dialog.dart +++ b/lib/components/AlbumScreen/download_dialog.dart @@ -60,8 +60,9 @@ class DownloadDialog extends StatefulWidget { : DownloadProfile(transcodeCodec: FinampTranscodingCodec.original); profile.downloadLocationId = FinampSettingsHelper.finampSettings.internalSongDir.id; - GlobalSnackbar.message((scaffold) => - AppLocalizations.of(scaffold)!.confirmDownloadStarted, isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)!.confirmDownloadStarted, + isConfirmation: true); unawaited(downloadsService .addDownload(stub: item, viewId: viewId!, transcodeProfile: profile) // TODO only show the enqueued confirmation if the enqueuing took longer than ~10 seconds @@ -75,7 +76,8 @@ class DownloadDialog extends StatefulWidget { children = await jellyfinApiHelper.getItems( parentItem: item.baseItem!, includeItemTypes: "Audio", - fields: "${jellyfinApiHelper.defaultFields},MediaSources,MediaStreams"); + fields: + "${jellyfinApiHelper.defaultFields},MediaSources,MediaStreams"); } if (!context.mounted) return; await showDialog( diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index 7db15b1fe..fde9635db 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -251,16 +251,15 @@ class _SongListTileState extends ConsumerState id: widget.parentItem?.id ?? "", item: widget.parentItem, // we're playing from an album, so we should use the album's normalization gain. - contextNormalizationGain: (widget.isInPlaylist || - widget.isOnArtistScreen) - ? null - : widget.parentItem?.normalizationGain, + contextNormalizationGain: + (widget.isInPlaylist || widget.isOnArtistScreen) + ? null + : widget.parentItem?.normalizationGain, ), ); } else { // TODO put in a real offline songs implementation if (FinampSettingsHelper.finampSettings.isOffline) { - final settings = FinampSettingsHelper.finampSettings; final downloadService = GetIt.instance(); final finampUserHelper = GetIt.instance(); @@ -273,13 +272,17 @@ class _SongListTileState extends ConsumerState viewFilter: finampUserHelper.currentUser?.currentView?.id, nullableViewFilters: settings.showDownloadsWithUnknownLibrary); - var items = offlineItems.map((e) => e.baseItem).whereNotNull().toList(); + var items = + offlineItems.map((e) => e.baseItem).whereNotNull().toList(); - items = sortItems(items, settings.tabSortBy[TabContentType.songs], settings.tabSortOrder[TabContentType.songs]); + items = sortItems(items, settings.tabSortBy[TabContentType.songs], + settings.tabSortOrder[TabContentType.songs]); await _queueService.startPlayback( items: items, - startingIndex: widget.isShownInSearch ? items.indexWhere((element) => element.id == widget.item.id) : await widget.index, + startingIndex: widget.isShownInSearch + ? items.indexWhere((element) => element.id == widget.item.id) + : await widget.index, source: QueueItemSource( name: QueueItemSourceName( type: QueueItemSourceNameType.preTranslated, @@ -301,7 +304,10 @@ class _SongListTileState extends ConsumerState direction: FinampSettingsHelper.finampSettings.disableGesture ? DismissDirection.none : DismissDirection.horizontal, - dismissThresholds: const {DismissDirection.startToEnd: 0.5, DismissDirection.endToStart: 0.5}, + dismissThresholds: const { + DismissDirection.startToEnd: 0.5, + DismissDirection.endToStart: 0.5 + }, background: Container( color: Theme.of(context).colorScheme.secondaryContainer, alignment: Alignment.centerLeft, @@ -312,16 +318,14 @@ class _SongListTileState extends ConsumerState children: [ Icon( TablerIcons.playlist, - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, + color: + Theme.of(context).colorScheme.onSecondaryContainer, size: 40, ), Icon( TablerIcons.playlist, - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, + color: + Theme.of(context).colorScheme.onSecondaryContainer, size: 40, ) ], diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index d8f7503d9..022bbc224 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -91,9 +91,8 @@ Future showModalSongMenu({ } class SongMenu extends ConsumerStatefulWidget { - static const routeName = "/song-menu"; - + const SongMenu({ super.key, required this.item, @@ -212,19 +211,21 @@ class _SongMenuState extends ConsumerState { Vibrate.feedback(FeedbackType.selection); } - Future shouldShowSpeedControls(double currentSpeed, MetadataProvider? metadata) async { - if (currentSpeed != 1.0 || - FinampSettingsHelper.finampSettings.playbackSpeedVisibility == - PlaybackSpeedVisibility.visible) { - return true; - } - - if (FinampSettingsHelper.finampSettings.playbackSpeedVisibility == PlaybackSpeedVisibility.automatic) { - return metadata?.qualifiesForPlaybackSpeedControl ?? false; - } + Future shouldShowSpeedControls( + double currentSpeed, MetadataProvider? metadata) async { + if (currentSpeed != 1.0 || + FinampSettingsHelper.finampSettings.playbackSpeedVisibility == + PlaybackSpeedVisibility.visible) { + return true; + } - return false; -} + if (FinampSettingsHelper.finampSettings.playbackSpeedVisibility == + PlaybackSpeedVisibility.automatic) { + return metadata?.qualifiesForPlaybackSpeedControl ?? false; + } + + return false; + } void scrollToExtent( DraggableScrollableController scrollController, double? percentage) { @@ -244,7 +245,6 @@ class _SongMenuState extends ConsumerState { @override Widget build(BuildContext context) { return Consumer(builder: (context, ref, child) { - final iconColor = _imageTheme?.primary ?? Theme.of(context).iconTheme.color ?? Colors.white; @@ -281,7 +281,7 @@ class _SongMenuState extends ConsumerState { : 1.0), CustomScrollView( controller: scrollController, - slivers: [ + slivers: [ SliverPersistentHeader( delegate: SongMenuSliverAppBar( item: widget.item, @@ -309,178 +309,187 @@ class _SongMenuState extends ConsumerState { ), if (widget.showPlaybackControls) SongMenuMask( - child: StreamBuilder( - stream: Rx.combineLatest3( - _queueService.getPlaybackOrderStream(), - _queueService.getLoopModeStream(), - _queueService.getPlaybackSpeedStream(), - (a, b, c) => PlaybackBehaviorInfo(a, b, c)), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const SliverToBoxAdapter(); - } - - final playbackBehavior = snapshot.data!; - const playbackOrderIcons = { - FinampPlaybackOrder.linear: TablerIcons.arrows_right, - FinampPlaybackOrder.shuffled: - TablerIcons.arrows_shuffle, - }; - final playbackOrderTooltips = { - FinampPlaybackOrder.linear: - AppLocalizations.of(context) - ?.playbackOrderLinearButtonLabel ?? - "Playing in order", - FinampPlaybackOrder.shuffled: - AppLocalizations.of(context) - ?.playbackOrderShuffledButtonLabel ?? - "Shuffling", - }; - const loopModeIcons = { - FinampLoopMode.none: TablerIcons.repeat, - FinampLoopMode.one: TablerIcons.repeat_once, - FinampLoopMode.all: TablerIcons.repeat, - }; - final loopModeTooltips = { - FinampLoopMode.none: AppLocalizations.of(context) - ?.loopModeNoneButtonLabel ?? - "Looping off", - FinampLoopMode.one: AppLocalizations.of(context) - ?.loopModeOneButtonLabel ?? - "Looping this song", - FinampLoopMode.all: AppLocalizations.of(context) - ?.loopModeAllButtonLabel ?? - "Looping all", - }; - - var sliverArray = [ - PlaybackAction( - icon: playbackOrderIcons[playbackBehavior.order]!, - onPressed: () async { - _queueService.togglePlaybackOrder(); - }, - tooltip: - playbackOrderTooltips[playbackBehavior.order]!, - iconColor: playbackBehavior.order == - FinampPlaybackOrder.shuffled - ? iconColor - : Theme.of(context) - .textTheme - .bodyMedium - ?.color ?? - Colors.white, - ), - ValueListenableBuilder( - valueListenable: _audioHandler.sleepTimer, - builder: (context, timerValue, child) { - final remainingMinutes = - (_audioHandler.sleepTimerRemaining.inSeconds / - 60.0) - .ceil(); - return PlaybackAction( - icon: timerValue != null - ? TablerIcons.hourglass_high - : TablerIcons.hourglass_empty, - onPressed: () async { - if (timerValue != null) { - await showDialog( - context: context, - builder: (context) => - const SleepTimerCancelDialog(), - ); - } else { - await showDialog( - context: context, - builder: (context) => - const SleepTimerDialog(), + child: StreamBuilder( + stream: Rx.combineLatest3( + _queueService.getPlaybackOrderStream(), + _queueService.getLoopModeStream(), + _queueService.getPlaybackSpeedStream(), + (a, b, c) => PlaybackBehaviorInfo(a, b, c)), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const SliverToBoxAdapter(); + } + + final playbackBehavior = snapshot.data!; + const playbackOrderIcons = { + FinampPlaybackOrder.linear: + TablerIcons.arrows_right, + FinampPlaybackOrder.shuffled: + TablerIcons.arrows_shuffle, + }; + final playbackOrderTooltips = { + FinampPlaybackOrder.linear: + AppLocalizations.of(context) + ?.playbackOrderLinearButtonLabel ?? + "Playing in order", + FinampPlaybackOrder + .shuffled: AppLocalizations.of(context) + ?.playbackOrderShuffledButtonLabel ?? + "Shuffling", + }; + const loopModeIcons = { + FinampLoopMode.none: TablerIcons.repeat, + FinampLoopMode.one: TablerIcons.repeat_once, + FinampLoopMode.all: TablerIcons.repeat, + }; + final loopModeTooltips = { + FinampLoopMode.none: + AppLocalizations.of(context) + ?.loopModeNoneButtonLabel ?? + "Looping off", + FinampLoopMode.one: AppLocalizations.of(context) + ?.loopModeOneButtonLabel ?? + "Looping this song", + FinampLoopMode.all: AppLocalizations.of(context) + ?.loopModeAllButtonLabel ?? + "Looping all", + }; + + var sliverArray = [ + PlaybackAction( + icon: playbackOrderIcons[ + playbackBehavior.order]!, + onPressed: () async { + _queueService.togglePlaybackOrder(); + }, + tooltip: playbackOrderTooltips[ + playbackBehavior.order]!, + iconColor: playbackBehavior.order == + FinampPlaybackOrder.shuffled + ? iconColor + : Theme.of(context) + .textTheme + .bodyMedium + ?.color ?? + Colors.white, + ), + ValueListenableBuilder( + valueListenable: _audioHandler.sleepTimer, + builder: (context, timerValue, child) { + final remainingMinutes = (_audioHandler + .sleepTimerRemaining.inSeconds / + 60.0) + .ceil(); + return PlaybackAction( + icon: timerValue != null + ? TablerIcons.hourglass_high + : TablerIcons.hourglass_empty, + onPressed: () async { + if (timerValue != null) { + await showDialog( + context: context, + builder: (context) => + const SleepTimerCancelDialog(), + ); + } else { + await showDialog( + context: context, + builder: (context) => + const SleepTimerDialog(), + ); + } + }, + tooltip: timerValue != null + ? AppLocalizations.of(context) + ?.sleepTimerRemainingTime( + remainingMinutes) ?? + "Sleeping in $remainingMinutes minutes" + : AppLocalizations.of(context)! + .sleepTimerTooltip, + iconColor: timerValue != null + ? iconColor + : Theme.of(context) + .textTheme + .bodyMedium + ?.color ?? + Colors.white, ); - } + }, + ), + // [Playback speed widget will be added here if conditions are met] + PlaybackAction( + icon: loopModeIcons[playbackBehavior.loop]!, + onPressed: () async { + _queueService.toggleLoopMode(); + }, + tooltip: + loopModeTooltips[playbackBehavior.loop]!, + iconColor: playbackBehavior.loop == + FinampLoopMode.none + ? Theme.of(context) + .textTheme + .bodyMedium + ?.color ?? + Colors.white + : iconColor, + ), + ]; + + final speedWidget = PlaybackAction( + icon: TablerIcons.brand_speedtest, + onPressed: () { + toggleSpeedMenu(); }, - tooltip: timerValue != null - ? AppLocalizations.of(context) - ?.sleepTimerRemainingTime( - remainingMinutes) ?? - "Sleeping in $remainingMinutes minutes" - : AppLocalizations.of(context)! - .sleepTimerTooltip, - iconColor: timerValue != null - ? iconColor - : Theme.of(context) - .textTheme - .bodyMedium - ?.color ?? - Colors.white, - ); - }, - ), - // [Playback speed widget will be added here if conditions are met] - PlaybackAction( - icon: loopModeIcons[playbackBehavior.loop]!, - onPressed: () async { - _queueService.toggleLoopMode(); - }, - tooltip: loopModeTooltips[playbackBehavior.loop]!, - iconColor: - playbackBehavior.loop == FinampLoopMode.none + tooltip: AppLocalizations.of(context)! + .playbackSpeedButtonLabel( + playbackBehavior.speed), + iconColor: playbackBehavior.speed == 1.0 ? Theme.of(context) .textTheme .bodyMedium ?.color ?? Colors.white : iconColor, - ), - ]; + ); - final speedWidget = PlaybackAction( - icon: TablerIcons.brand_speedtest, - onPressed: () { - toggleSpeedMenu(); - }, - tooltip: AppLocalizations.of(context)!.playbackSpeedButtonLabel(playbackBehavior.speed), - iconColor: playbackBehavior.speed == 1.0 - ? Theme.of(context).textTheme.bodyMedium?.color ?? - Colors.white - : iconColor, - ); - - if (speedWidgetWasVisible) { - sliverArray.insertAll(2, [speedWidget]); - return SliverCrossAxisGroup( - slivers: sliverArray, - ); - } - return FutureBuilder( - future: - shouldShowSpeedControls(playbackBehavior.speed, metadata.value), - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.data == true) { - speedWidgetWasVisible = true; + if (speedWidgetWasVisible) { sliverArray.insertAll(2, [speedWidget]); + return SliverCrossAxisGroup( + slivers: sliverArray, + ); } - return SliverCrossAxisGroup( - slivers: sliverArray, - ); - }); - }, - ), + return FutureBuilder( + future: shouldShowSpeedControls( + playbackBehavior.speed, metadata.value), + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.data == true) { + speedWidgetWasVisible = true; + sliverArray.insertAll(2, [speedWidget]); + } + return SliverCrossAxisGroup( + slivers: sliverArray, + ); + }); + }, + ), ), - SliverToBoxAdapter( - child: AnimatedSwitcher( - duration: songMenuDefaultAnimationDuration, - switchInCurve: songMenuDefaultInCurve, - switchOutCurve: songMenuDefaultOutCurve, - transitionBuilder: (child, animation) { - return SizeTransition( - sizeFactor: animation, child: child); - }, - child: showSpeedMenu - ? SpeedMenu(iconColor: iconColor) - : null, - ), - ), - SongMenuMask( + SliverToBoxAdapter( + child: AnimatedSwitcher( + duration: songMenuDefaultAnimationDuration, + switchInCurve: songMenuDefaultInCurve, + switchOutCurve: songMenuDefaultOutCurve, + transitionBuilder: (child, animation) { + return SizeTransition( + sizeFactor: animation, child: child); + }, + child: showSpeedMenu + ? SpeedMenu(iconColor: iconColor) + : null, + ), + ), + SongMenuMask( child: SliverPadding( padding: const EdgeInsets.only(left: 8.0), sliver: SliverList( @@ -495,9 +504,9 @@ class _SongMenuState extends ConsumerState { }, ); }), - ]); - }); -} + ]); + }); + } List _menuEntries(BuildContext context, Color iconColor) { final downloadsService = GetIt.instance(); @@ -597,9 +606,8 @@ class _SongMenuState extends ConsumerState { source: QueueItemSource( type: QueueItemSourceType.queue, name: QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated, - pretranslatedName: - AppLocalizations.of(context)!.queue), + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: AppLocalizations.of(context)!.queue), id: widget.item.id)); if (!context.mounted) return; @@ -859,7 +867,6 @@ class _SongMenuState extends ConsumerState { ), ]; } - } class SongMenuSliverAppBar extends SliverPersistentHeaderDelegate { diff --git a/lib/components/ArtistScreen/artist_screen_content_flexible_space_bar.dart b/lib/components/ArtistScreen/artist_screen_content_flexible_space_bar.dart index c8df8ae3a..0346647f1 100644 --- a/lib/components/ArtistScreen/artist_screen_content_flexible_space_bar.dart +++ b/lib/components/ArtistScreen/artist_screen_content_flexible_space_bar.dart @@ -99,8 +99,9 @@ class ArtistScreenContentFlexibleSpaceBar extends StatelessWidget { item: parentItem, ), ); - GlobalSnackbar.message((scaffold) => AppLocalizations.of(scaffold)! - .confirmShuffleNext, isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)!.confirmShuffleNext, + isConfirmation: true); } void shuffleAllFromArtistToNextUp(List items) { @@ -119,8 +120,9 @@ class ArtistScreenContentFlexibleSpaceBar extends StatelessWidget { item: parentItem, ), ); - GlobalSnackbar.message((scaffold) => AppLocalizations.of(scaffold)! - .confirmShuffleToNextUp, isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)!.confirmShuffleToNextUp, + isConfirmation: true); } void shuffleAllFromArtistToQueue(List items) { @@ -138,8 +140,9 @@ class ArtistScreenContentFlexibleSpaceBar extends StatelessWidget { item: parentItem, ), ); - GlobalSnackbar.message((scaffold) => AppLocalizations.of(scaffold)! - .confirmShuffleToQueue, isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)!.confirmShuffleToQueue, + isConfirmation: true); } void addArtistNext(List items) { @@ -156,8 +159,10 @@ class ArtistScreenContentFlexibleSpaceBar extends StatelessWidget { id: parentItem.id, item: parentItem, )); - GlobalSnackbar.message((scaffold) => AppLocalizations.of(scaffold)! - .confirmPlayNext(isGenre ? "genre" : "artist"), isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)! + .confirmPlayNext(isGenre ? "genre" : "artist"), + isConfirmation: true); } void addArtistToNextUp(List items) { @@ -174,8 +179,10 @@ class ArtistScreenContentFlexibleSpaceBar extends StatelessWidget { id: parentItem.id, item: parentItem, )); - GlobalSnackbar.message((scaffold) => AppLocalizations.of(scaffold)! - .confirmAddToNextUp(isGenre ? "genre" : "artist"), isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)! + .confirmAddToNextUp(isGenre ? "genre" : "artist"), + isConfirmation: true); } void addArtistToQueue(List items) { @@ -192,8 +199,10 @@ class ArtistScreenContentFlexibleSpaceBar extends StatelessWidget { id: parentItem.id, item: parentItem, )); - GlobalSnackbar.message((scaffold) => AppLocalizations.of(scaffold)! - .confirmAddToQueue(isGenre ? "genre" : "artist"), isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)! + .confirmAddToQueue(isGenre ? "genre" : "artist"), + isConfirmation: true); } void shuffleAlbumsFromArtist(List items) { @@ -240,8 +249,8 @@ class ArtistScreenContentFlexibleSpaceBar extends StatelessWidget { item: parentItem, ), ); - GlobalSnackbar.message((scaffold) => AppLocalizations.of(scaffold)! - .confirmShuffleNext, + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)!.confirmShuffleNext, isConfirmation: true); } @@ -266,8 +275,8 @@ class ArtistScreenContentFlexibleSpaceBar extends StatelessWidget { item: parentItem, ), ); - GlobalSnackbar.message((scaffold) => AppLocalizations.of(scaffold)! - .confirmShuffleToNextUp, + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)!.confirmShuffleToNextUp, isConfirmation: true); } @@ -291,8 +300,8 @@ class ArtistScreenContentFlexibleSpaceBar extends StatelessWidget { item: parentItem, ), ); - GlobalSnackbar.message((scaffold) => AppLocalizations.of(scaffold)! - .confirmShuffleToQueue, + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)!.confirmShuffleToQueue, isConfirmation: true); } @@ -347,13 +356,15 @@ class ArtistScreenContentFlexibleSpaceBar extends StatelessWidget { onPressed: () => allSongs.then((items) => playAllFromArtist(items ?? [])), // set the minimum width as 25% of the screen width, - minWidth: MediaQuery.of(context).size.width * 0.25, + minWidth: + MediaQuery.of(context).size.width * + 0.25, ), PopupMenuButton( enableFeedback: true, // icon: const Icon(TablerIcons.dots_vertical), - onOpened: () => - FeedbackHelper.feedback(FeedbackType.light), + onOpened: () => FeedbackHelper.feedback( + FeedbackType.light), itemBuilder: (context) { final queueService = GetIt.instance(); @@ -365,8 +376,8 @@ class ArtistScreenContentFlexibleSpaceBar extends StatelessWidget { PopupMenuItem( value: ArtistMenuItems.playNext, child: ListTile( - leading: const Icon( - TablerIcons.corner_right_down), + leading: const Icon(TablerIcons + .corner_right_down), title: Text( AppLocalizations.of(context)! .playNext), @@ -375,8 +386,8 @@ class ArtistScreenContentFlexibleSpaceBar extends StatelessWidget { PopupMenuItem( value: ArtistMenuItems.addToNextUp, child: ListTile( - leading: - const Icon(TablerIcons.corner_right_down_double), + leading: const Icon(TablerIcons + .corner_right_down_double), title: Text( AppLocalizations.of(context)! .addToNextUp), @@ -464,13 +475,15 @@ class ArtistScreenContentFlexibleSpaceBar extends StatelessWidget { onPressed: () => allSongs.then((items) => shuffleAllFromArtist(items ?? [])), // set the minimum width as 25% of the screen width, - minWidth: MediaQuery.of(context).size.width * 0.25, + minWidth: + MediaQuery.of(context).size.width * + 0.25, ), PopupMenuButton( enableFeedback: true, // icon: const Icon(TablerIcons.dots_vertical), - onOpened: () => - FeedbackHelper.feedback(FeedbackType.light), + onOpened: () => FeedbackHelper.feedback( + FeedbackType.light), itemBuilder: (context) { final queueService = GetIt.instance(); diff --git a/lib/components/AudioServiceSettingsScreen/periodic_playback_session_update_frequency_editor.dart b/lib/components/AudioServiceSettingsScreen/periodic_playback_session_update_frequency_editor.dart index 8be297bcd..bb270d526 100644 --- a/lib/components/AudioServiceSettingsScreen/periodic_playback_session_update_frequency_editor.dart +++ b/lib/components/AudioServiceSettingsScreen/periodic_playback_session_update_frequency_editor.dart @@ -5,7 +5,8 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../services/finamp_settings_helper.dart'; class PeriodicPlaybackSessionUpdateFrequencyEditor extends StatefulWidget { - const PeriodicPlaybackSessionUpdateFrequencyEditor({Key? key}) : super(key: key); + const PeriodicPlaybackSessionUpdateFrequencyEditor({Key? key}) + : super(key: key); @override State createState() => @@ -15,18 +16,21 @@ class PeriodicPlaybackSessionUpdateFrequencyEditor extends StatefulWidget { class _PeriodicPlaybackSessionUpdateFrequencyEditorState extends State { final _controller = TextEditingController( - text: - FinampSettingsHelper.finampSettings.periodicPlaybackSessionUpdateFrequencySeconds.toString()); + text: FinampSettingsHelper + .finampSettings.periodicPlaybackSessionUpdateFrequencySeconds + .toString()); @override Widget build(BuildContext context) { return ListTile( - title: Text(AppLocalizations.of(context)!.periodicPlaybackSessionUpdateFrequency), + title: Text( + AppLocalizations.of(context)!.periodicPlaybackSessionUpdateFrequency), subtitle: RichText( text: TextSpan( children: [ TextSpan( - text: AppLocalizations.of(context)!.periodicPlaybackSessionUpdateFrequencySubtitle, + text: AppLocalizations.of(context)! + .periodicPlaybackSessionUpdateFrequencySubtitle, style: Theme.of(context).textTheme.bodyMedium, ), const TextSpan(text: "\n"), @@ -39,20 +43,24 @@ class _PeriodicPlaybackSessionUpdateFrequencyEditorState ), recognizer: TapGestureRecognizer() ..onTap = () { - showGeneralDialog(context: context, pageBuilder: (context, anim1, anim2) { - return AlertDialog( - title: Text(AppLocalizations.of(context)!.periodicPlaybackSessionUpdateFrequency), - content: Text(AppLocalizations.of(context)!.periodicPlaybackSessionUpdateFrequencyDetails), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(AppLocalizations.of(context)!.close), - ), - ], - ); - }); + showGeneralDialog( + context: context, + pageBuilder: (context, anim1, anim2) { + return AlertDialog( + title: Text(AppLocalizations.of(context)! + .periodicPlaybackSessionUpdateFrequency), + content: Text(AppLocalizations.of(context)! + .periodicPlaybackSessionUpdateFrequencyDetails), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(AppLocalizations.of(context)!.close), + ), + ], + ); + }); }, ), ], @@ -68,7 +76,8 @@ class _PeriodicPlaybackSessionUpdateFrequencyEditorState final valueInt = int.tryParse(value); if (valueInt != null) { - FinampSettingsHelper.setPeriodicPlaybackSessionUpdateFrequencySeconds(valueInt); + FinampSettingsHelper + .setPeriodicPlaybackSessionUpdateFrequencySeconds(valueInt); } }, ), diff --git a/lib/components/AudioServiceSettingsScreen/report_queue_to_server_toggle.dart b/lib/components/AudioServiceSettingsScreen/report_queue_to_server_toggle.dart index f023eae8d..af8d55cc4 100644 --- a/lib/components/AudioServiceSettingsScreen/report_queue_to_server_toggle.dart +++ b/lib/components/AudioServiceSettingsScreen/report_queue_to_server_toggle.dart @@ -15,8 +15,8 @@ class ReportQueueToServerToggle extends StatelessWidget { builder: (_, box, __) { return SwitchListTile.adaptive( title: Text(AppLocalizations.of(context)!.reportQueueToServer), - subtitle: Text( - AppLocalizations.of(context)!.reportQueueToServerSubtitle), + subtitle: + Text(AppLocalizations.of(context)!.reportQueueToServerSubtitle), value: FinampSettingsHelper.finampSettings.reportQueueToServer, onChanged: (value) => FinampSettingsHelper.setReportQueueToServer(value), diff --git a/lib/components/Buttons/cta_large.dart b/lib/components/Buttons/cta_large.dart index 0afbfca96..72f533d89 100644 --- a/lib/components/Buttons/cta_large.dart +++ b/lib/components/Buttons/cta_large.dart @@ -1,5 +1,3 @@ - - import 'package:finamp/color_schemes.g.dart'; import 'package:finamp/services/feedback_helper.dart'; import 'package:flutter/material.dart'; @@ -10,7 +8,11 @@ class CTALarge extends StatelessWidget { final IconData icon; final void Function() onPressed; - const CTALarge({super.key, required this.text, required this.icon, required this.onPressed}); + const CTALarge( + {super.key, + required this.text, + required this.icon, + required this.onPressed}); @override Widget build(BuildContext context) { @@ -29,7 +31,9 @@ class CTALarge extends StatelessWidget { const EdgeInsets.symmetric(horizontal: 24, vertical: 20), ), backgroundColor: MaterialStateProperty.all( - Theme.of(context).brightness == Brightness.dark ? jellyfinBlueColor.withOpacity(0.3) : jellyfinBlueColor, + Theme.of(context).brightness == Brightness.dark + ? jellyfinBlueColor.withOpacity(0.3) + : jellyfinBlueColor, ), ), child: Wrap( @@ -38,10 +42,14 @@ class CTALarge extends StatelessWidget { Icon( icon, size: 28, - color: Theme.of(context).brightness == Brightness.dark ? jellyfinBlueColor : Colors.white, + color: Theme.of(context).brightness == Brightness.dark + ? jellyfinBlueColor + : Colors.white, weight: 1.5, ), - const SizedBox(width: 12,), + const SizedBox( + width: 12, + ), Text( text, style: const TextStyle( diff --git a/lib/components/Buttons/cta_medium.dart b/lib/components/Buttons/cta_medium.dart index 7aa3f135b..e07a76725 100644 --- a/lib/components/Buttons/cta_medium.dart +++ b/lib/components/Buttons/cta_medium.dart @@ -18,12 +18,11 @@ class CTAMedium extends StatelessWidget { @override Widget build(BuildContext context) { - final screenSize = MediaQuery.of(context).size; final minWidth = this.minWidth ?? screenSize.width * 0.25; final paddingHorizontal = screenSize.width * 0.015; final paddingVertical = screenSize.height * 0.015; - + return ElevatedButton( onPressed: () { FeedbackHelper.feedback(FeedbackType.selection); @@ -36,7 +35,11 @@ class CTAMedium extends StatelessWidget { ), ), padding: MaterialStateProperty.all( - EdgeInsets.only(left: 8 + paddingHorizontal, right: 8, top: paddingVertical, bottom: paddingVertical), + EdgeInsets.only( + left: 8 + paddingHorizontal, + right: 8, + top: paddingVertical, + bottom: paddingVertical), ), backgroundColor: MaterialStateProperty.all( Theme.of(context).brightness == Brightness.dark @@ -47,7 +50,8 @@ class CTAMedium extends StatelessWidget { child: Container( constraints: BoxConstraints(minWidth: minWidth + paddingHorizontal), padding: EdgeInsets.only( - right: paddingHorizontal), // this is to center the content when a minWidth is set + right: + paddingHorizontal), // this is to center the content when a minWidth is set alignment: Alignment.center, child: Wrap( crossAxisAlignment: WrapCrossAlignment.center, diff --git a/lib/components/Buttons/simple_button.dart b/lib/components/Buttons/simple_button.dart index 6aaba1618..a8bb6b86d 100644 --- a/lib/components/Buttons/simple_button.dart +++ b/lib/components/Buttons/simple_button.dart @@ -14,15 +14,24 @@ class SimpleButton extends StatelessWidget { final Color? iconColor; final void Function() onPressed; final bool disabled; + /// fades the button out, while keeping it enabled /// used for representing state while also allowing interaction that can yield more information about the state (e.g. lyrics button) final bool inactive; - const SimpleButton({super.key, required this.text, required this.icon, required this.onPressed, this.iconPosition = IconPosition.start, this.iconSize = 20.0, this.iconColor, this.disabled = false, this.inactive = false}); + const SimpleButton( + {super.key, + required this.text, + required this.icon, + required this.onPressed, + this.iconPosition = IconPosition.start, + this.iconSize = 20.0, + this.iconColor, + this.disabled = false, + this.inactive = false}); @override Widget build(BuildContext context) { - final contents = [ Icon( icon, @@ -30,17 +39,21 @@ class SimpleButton extends StatelessWidget { color: (disabled || inactive) ? iconColor?.withOpacity(0.5) : iconColor, weight: 1.5, ), - const SizedBox(width: 6,), + const SizedBox( + width: 6, + ), Text( text, style: TextStyle( - color: (disabled || inactive) ? Theme.of(context).disabledColor : Theme.of(context).textTheme.bodyMedium!.color!, + color: (disabled || inactive) + ? Theme.of(context).disabledColor + : Theme.of(context).textTheme.bodyMedium!.color!, fontSize: 14, fontWeight: FontWeight.normal, ), ), ]; - + return Tooltip( message: disabled ? "$text (Disabled)" : text, child: TextButton( @@ -61,7 +74,9 @@ class SimpleButton extends StatelessWidget { ), child: Wrap( crossAxisAlignment: WrapCrossAlignment.center, - children: iconPosition == IconPosition.start ? contents : contents.reversed.toList(), + children: iconPosition == IconPosition.start + ? contents + : contents.reversed.toList(), ), ), ); diff --git a/lib/components/InteractionSettingsScreen/FastScrollSelector.dart b/lib/components/InteractionSettingsScreen/FastScrollSelector.dart index 3979206a4..a4a835d5a 100644 --- a/lib/components/InteractionSettingsScreen/FastScrollSelector.dart +++ b/lib/components/InteractionSettingsScreen/FastScrollSelector.dart @@ -21,4 +21,4 @@ class FastScrollSelector extends StatelessWidget { }, ); } -} \ No newline at end of file +} diff --git a/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart b/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart index 10130d804..afce715d6 100644 --- a/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart +++ b/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart @@ -44,7 +44,15 @@ class PlaybackSpeedControlVisibilityDropdownListTile extends StatelessWidget { title: Text(AppLocalizations.of(context)! .playbackSpeedControlSetting), content: Text(AppLocalizations.of(context)! - .playbackSpeedControlSettingDescription(MetadataProvider.speedControlLongTrackDuration.inMinutes, MetadataProvider.speedControlLongAlbumDuration.inHours, MetadataProvider.speedControlGenres.join(", "))), + .playbackSpeedControlSettingDescription( + MetadataProvider + .speedControlLongTrackDuration + .inMinutes, + MetadataProvider + .speedControlLongAlbumDuration + .inHours, + MetadataProvider.speedControlGenres + .join(", "))), actions: [ TextButton( onPressed: () { diff --git a/lib/components/LayoutSettingsScreen/player_screen_minimum_cover_padding_editor.dart b/lib/components/LayoutSettingsScreen/player_screen_minimum_cover_padding_editor.dart index 70ad250d6..56d9c0c84 100644 --- a/lib/components/LayoutSettingsScreen/player_screen_minimum_cover_padding_editor.dart +++ b/lib/components/LayoutSettingsScreen/player_screen_minimum_cover_padding_editor.dart @@ -14,14 +14,16 @@ class PlayerScreenMinimumCoverPaddingEditor extends StatefulWidget { class _PlayerScreenMinimumCoverPaddingEditorState extends State { final _controller = TextEditingController( - text: - FinampSettingsHelper.finampSettings.playerScreenCoverMinimumPadding.toString()); + text: FinampSettingsHelper.finampSettings.playerScreenCoverMinimumPadding + .toString()); @override Widget build(BuildContext context) { return ListTile( - title: Text(AppLocalizations.of(context)!.playerScreenMinimumCoverPaddingEditorTitle), - subtitle: Text(AppLocalizations.of(context)!.playerScreenMinimumCoverPaddingEditorSubtitle), + title: Text(AppLocalizations.of(context)! + .playerScreenMinimumCoverPaddingEditorTitle), + subtitle: Text(AppLocalizations.of(context)! + .playerScreenMinimumCoverPaddingEditorSubtitle), trailing: SizedBox( width: 50 * MediaQuery.of(context).textScaleFactor, child: TextField( @@ -32,7 +34,8 @@ class _PlayerScreenMinimumCoverPaddingEditorState final valueDouble = double.tryParse(value); if (valueDouble != null) { - FinampSettingsHelper.setPlayerScreenCoverMinimumPadding(valueDouble); + FinampSettingsHelper.setPlayerScreenCoverMinimumPadding( + valueDouble); } }, ), diff --git a/lib/components/LayoutSettingsScreen/use_cover_as_background_toggle.dart b/lib/components/LayoutSettingsScreen/use_cover_as_background_toggle.dart index 9ac61e4c2..16a09d093 100644 --- a/lib/components/LayoutSettingsScreen/use_cover_as_background_toggle.dart +++ b/lib/components/LayoutSettingsScreen/use_cover_as_background_toggle.dart @@ -14,12 +14,10 @@ class UseCoverAsBackgroundToggle extends StatelessWidget { valueListenable: FinampSettingsHelper.finampSettingsListener, builder: (_, box, __) { return SwitchListTile.adaptive( - title: - Text(AppLocalizations.of(context)!.useCoverAsBackground), - subtitle: Text(AppLocalizations.of(context)! - .useCoverAsBackgroundSubtitle), - value: - FinampSettingsHelper.finampSettings.useCoverAsBackground, + title: Text(AppLocalizations.of(context)!.useCoverAsBackground), + subtitle: + Text(AppLocalizations.of(context)!.useCoverAsBackgroundSubtitle), + value: FinampSettingsHelper.finampSettings.useCoverAsBackground, onChanged: (value) => FinampSettingsHelper.setUseCoverAsBackground(value), ); diff --git a/lib/components/LoginScreen/login_server_selection_page.dart b/lib/components/LoginScreen/login_server_selection_page.dart index d42d59178..b17b921c7 100644 --- a/lib/components/LoginScreen/login_server_selection_page.dart +++ b/lib/components/LoginScreen/login_server_selection_page.dart @@ -87,7 +87,8 @@ class _LoginServerSelectionPageState extends State { ), ), ), - Text(AppLocalizations.of(context)!.loginFlowServerSelectionHeading, + Text( + AppLocalizations.of(context)!.loginFlowServerSelectionHeading, style: Theme.of(context).textTheme.headlineMedium), Padding( padding: const EdgeInsets.only(top: 20.0, bottom: 12.0), @@ -106,39 +107,40 @@ class _LoginServerSelectionPageState extends State { _buildServerUrlInput(context), ConstrainedBox( constraints: const BoxConstraints(minHeight: 95.0), - child: widget.serverState.baseUrlToTest != null && widget.serverState.manualServer == null ? - Padding( - padding: const EdgeInsets.only(top: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Padding( - padding: EdgeInsets.all(4.0), - child: CircularProgressIndicator(), + child: widget.serverState.baseUrlToTest != null && + widget.serverState.manualServer == null + ? Padding( + padding: const EdgeInsets.only(top: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.all(4.0), + child: CircularProgressIndicator(), + ), + const SizedBox(width: 8.0), + Text( + AppLocalizations.of(context)!.connectingToServer, + style: Theme.of(context).textTheme.bodySmall, + ), + ], ), - const SizedBox(width: 8.0), - Text( - AppLocalizations.of(context)!.connectingToServer, - style: Theme.of(context).textTheme.bodySmall, + ) + : Visibility( + visible: widget.serverState.manualServer != null, + child: Padding( + padding: const EdgeInsets.only(top: 12.0), + child: JellyfinServerSelectionWidget( + baseUrl: widget.serverState.baseUrl, + serverInfo: widget.serverState.manualServer, + onPressed: () { + widget.onServerSelected?.call( + widget.serverState.manualServer!, + widget.serverState.baseUrl!); + }, + ), ), - ], - ), - ) : - Visibility( - visible: widget.serverState.manualServer != null, - child: Padding( - padding: const EdgeInsets.only(top: 12.0), - child: JellyfinServerSelectionWidget( - baseUrl: widget.serverState.baseUrl, - serverInfo: widget.serverState.manualServer, - onPressed: () { - widget.onServerSelected?.call( - widget.serverState.manualServer!, - widget.serverState.baseUrl!); - }, ), - ), - ), ), Padding( padding: const EdgeInsets.only(top: 20.0, bottom: 16.0), @@ -155,7 +157,6 @@ class _LoginServerSelectionPageState extends State { clipBehavior: Clip.antiAlias, itemCount: widget.serverState.discoveredServers.length + 1, itemBuilder: (context, index) { - if (index < widget.serverState.discoveredServers.length) { // get key and value final entry = widget.serverState.discoveredServers.entries @@ -181,20 +182,23 @@ class _LoginServerSelectionPageState extends State { children: [ const Padding( padding: EdgeInsets.all(4.0), - child: SizedBox(height: 20.0, width: 20.0, child: CircularProgressIndicator( - strokeWidth: 2.0, - )), + child: SizedBox( + height: 20.0, + width: 20.0, + child: CircularProgressIndicator( + strokeWidth: 2.0, + )), ), const SizedBox(width: 8.0), Text( - AppLocalizations.of(context)!.loginFlowLocalNetworkServersScanningForServers, + AppLocalizations.of(context)! + .loginFlowLocalNetworkServersScanningForServers, style: Theme.of(context).textTheme.bodySmall, ), ], ), ); } - }, ), ), @@ -261,7 +265,8 @@ class _LoginServerSelectionPageState extends State { autocorrect: false, keyboardType: TextInputType.url, autofillHints: const [AutofillHints.url], - decoration: inputFieldDecoration(AppLocalizations.of(context)!.serverUrlHint), + decoration: inputFieldDecoration( + AppLocalizations.of(context)!.serverUrlHint), textInputAction: TextInputAction.next, onEditingComplete: () => node.nextFocus(), onChanged: (value) async { @@ -284,7 +289,6 @@ class _LoginServerSelectionPageState extends State { ), ); } - } class JellyfinServerSelectionWidget extends StatelessWidget { @@ -354,7 +358,8 @@ class JellyfinServerSelectionWidget extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), ), onPressed: onPressed, child: buildContent(), @@ -364,7 +369,8 @@ class JellyfinServerSelectionWidget extends StatelessWidget { color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(10), ), - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), child: buildContent(), ); } diff --git a/lib/components/LoginScreen/login_splash_page.dart b/lib/components/LoginScreen/login_splash_page.dart index 55c603a72..5ae0491b4 100644 --- a/lib/components/LoginScreen/login_splash_page.dart +++ b/lib/components/LoginScreen/login_splash_page.dart @@ -33,15 +33,17 @@ class LoginSplashPage extends StatelessWidget { ), RichText( text: TextSpan( - text: "${AppLocalizations.of(context)!.loginFlowWelcomeHeading} ", + text: + "${AppLocalizations.of(context)!.loginFlowWelcomeHeading} ", style: Theme.of(context).textTheme.headlineMedium, children: [ TextSpan( text: AppLocalizations.of(context)!.finamp, - style: Theme.of(context).textTheme.headlineMedium!.copyWith( - // color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.w600, - ), + style: + Theme.of(context).textTheme.headlineMedium!.copyWith( + // color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.w600, + ), ), ], ), diff --git a/lib/components/LoginScreen/login_user_selection_page.dart b/lib/components/LoginScreen/login_user_selection_page.dart index 1a146bb5a..b74be569f 100644 --- a/lib/components/LoginScreen/login_user_selection_page.dart +++ b/lib/components/LoginScreen/login_user_selection_page.dart @@ -71,7 +71,9 @@ class _LoginUserSelectionPageState extends State { height: 75, ), ), - Text(AppLocalizations.of(context)!.loginFlowAccountSelectionHeading, + Text( + AppLocalizations.of(context)! + .loginFlowAccountSelectionHeading, style: Theme.of(context).textTheme.headlineMedium, textAlign: TextAlign.center), Padding( @@ -99,31 +101,35 @@ class _LoginUserSelectionPageState extends State { future: jellyfinApiHelper.initiateQuickConnect(), builder: (context, snapshot) { if (snapshot.hasData) { - widget.connectionState.quickConnectState = snapshot.data; + widget.connectionState.quickConnectState = + snapshot.data; widget.connectionState.isConnected = true; _quickConnectLogger.info( "Quick connect state: ${widget.connectionState.quickConnectState.toString()}"); waitForQuickConnect(); - _quickConnectLogger.info("Waiting for quick connect..."); + _quickConnectLogger + .info("Waiting for quick connect..."); return QuickConnectSection( connectionState: widget.connectionState, onAuthenticated: widget.onAuthenticated, ); } else { widget.connectionState.isConnected = false; - return QuickConnectSection(connectionState: widget.connectionState, onAuthenticated: widget.onAuthenticated); + return QuickConnectSection( + connectionState: widget.connectionState, + onAuthenticated: widget.onAuthenticated); } }, ); } else { - _quickConnectLogger - .severe("Quick connect not available!"); + _quickConnectLogger.severe("Quick connect not available!"); widget.connectionState.quickConnectState = null; widget.connectionState.isConnected = true; return Padding( padding: const EdgeInsets.only(top: 16.0, bottom: 12.0), child: Text( - AppLocalizations.of(context)!.loginFlowQuickConnectDisabled, + AppLocalizations.of(context)! + .loginFlowQuickConnectDisabled, textAlign: TextAlign.center, ), ); @@ -131,7 +137,8 @@ class _LoginUserSelectionPageState extends State { }, ), Padding( - padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 0, bottom: 8.0), + padding: const EdgeInsets.only( + left: 8.0, right: 8.0, top: 0, bottom: 8.0), child: Text( AppLocalizations.of(context)!.loginFlowSelectAUser, textAlign: TextAlign.center, @@ -142,7 +149,7 @@ class _LoginUserSelectionPageState extends State { builder: (context, snapshot) { if (snapshot.hasData) { final users = snapshot.data!.users; - + return Wrap( alignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.start, @@ -201,54 +208,61 @@ class QuickConnectSection extends StatelessWidget { @override Widget build(BuildContext context) { - return connectionState!.quickConnectState != null ? - Column( - children: [ - Text( - AppLocalizations.of(context)!.loginFlowQuickConnectPrompt, - textAlign: TextAlign.center, - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: SelectableText( - connectionState.quickConnectState?.code ?? "", - style: Theme.of(context).textTheme.displaySmall!.copyWith( - fontFamily: "monospace", - letterSpacing: 5.0, - ), - semanticsLabel: connectionState!.quickConnectState?.code?.split("").join(" "), - textAlign: TextAlign.center, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( - AppLocalizations.of(context)! - .loginFlowQuickConnectInstructions, - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w300, - color: Theme.of(context).textTheme.bodySmall!.color!.withOpacity(0.9), + return connectionState!.quickConnectState != null + ? Column( + children: [ + Text( + AppLocalizations.of(context)!.loginFlowQuickConnectPrompt, + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: SelectableText( + connectionState.quickConnectState?.code ?? "", + style: Theme.of(context).textTheme.displaySmall!.copyWith( + fontFamily: "monospace", + letterSpacing: 5.0, + ), + semanticsLabel: connectionState!.quickConnectState?.code + ?.split("") + .join(" "), + textAlign: TextAlign.center, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + AppLocalizations.of(context)! + .loginFlowQuickConnectInstructions, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontWeight: FontWeight.w300, + color: Theme.of(context) + .textTheme + .bodySmall! + .color! + .withOpacity(0.9), + ), + textAlign: TextAlign.center, + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), + child: Text( + "- ${AppLocalizations.of(context)!.orDivider} -", + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + ) + ], + ) + : Padding( + padding: const EdgeInsets.only(top: 16.0, bottom: 12.0), child: Text( - "- ${AppLocalizations.of(context)!.orDivider} -", - style: Theme.of(context).textTheme.bodyLarge, + AppLocalizations.of(context)!.loginFlowQuickConnectDisabled, textAlign: TextAlign.center, ), - ) - ], - ) : - Padding( - padding: const EdgeInsets.only(top: 16.0, bottom: 12.0), - child: Text( - AppLocalizations.of(context)!.loginFlowQuickConnectDisabled, - textAlign: TextAlign.center, - ), - ); + ); } } diff --git a/lib/components/LogsScreen/copy_logs_button.dart b/lib/components/LogsScreen/copy_logs_button.dart index f58b04a05..606df757b 100644 --- a/lib/components/LogsScreen/copy_logs_button.dart +++ b/lib/components/LogsScreen/copy_logs_button.dart @@ -24,7 +24,9 @@ class _CopyLogsButtonState extends State { if (!mounted) return; - GlobalSnackbar.message((scaffold) => AppLocalizations.of(context)!.logsCopied, isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(context)!.logsCopied, + isConfirmation: true); }, ); } diff --git a/lib/components/LogsScreen/log_tile.dart b/lib/components/LogsScreen/log_tile.dart index 36f4e12a2..37cc8cd03 100644 --- a/lib/components/LogsScreen/log_tile.dart +++ b/lib/components/LogsScreen/log_tile.dart @@ -178,4 +178,4 @@ class _LogMessageContent extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/components/MusicScreen/album_item.dart b/lib/components/MusicScreen/album_item.dart index 5cf0af9c6..a3cc7ef21 100644 --- a/lib/components/MusicScreen/album_item.dart +++ b/lib/components/MusicScreen/album_item.dart @@ -183,8 +183,9 @@ class _AlbumItemState extends State { .map((e) => e.id) .contains(mutableAlbum.id) ? PopupMenuItem<_AlbumListTileMenuItems>( - enabled: !isOffline && ["MusicAlbum", "MusicArtist", "MusicGenre"] - .contains(mutableAlbum.type), + enabled: !isOffline && + ["MusicAlbum", "MusicArtist", "MusicGenre"] + .contains(mutableAlbum.type), value: _AlbumListTileMenuItems.removeFromMixList, child: ListTile( enabled: !isOffline, @@ -193,8 +194,9 @@ class _AlbumItemState extends State { ), ) : PopupMenuItem<_AlbumListTileMenuItems>( - enabled: !isOffline && ["MusicAlbum", "MusicArtist", "MusicGenre"] - .contains(mutableAlbum.type), + enabled: !isOffline && + ["MusicAlbum", "MusicArtist", "MusicGenre"] + .contains(mutableAlbum.type), value: _AlbumListTileMenuItems.addToMixList, child: ListTile( enabled: !isOffline, @@ -290,8 +292,10 @@ class _AlbumItemState extends State { mutableAlbum.userData = newUserData; }); - GlobalSnackbar.message((scaffold) => - AppLocalizations.of(scaffold)!.confirmFavoriteAdded, isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => + AppLocalizations.of(scaffold)!.confirmFavoriteAdded, + isConfirmation: true); } catch (e) { GlobalSnackbar.error(e); } @@ -306,8 +310,10 @@ class _AlbumItemState extends State { setState(() { mutableAlbum.userData = newUserData; }); - GlobalSnackbar.message((scaffold) => - AppLocalizations.of(scaffold)!.confirmFavoriteRemoved, isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => + AppLocalizations.of(scaffold)!.confirmFavoriteRemoved, + isConfirmation: true); } catch (e) { GlobalSnackbar.error(e); } @@ -329,7 +335,8 @@ class _AlbumItemState extends State { case _AlbumListTileMenuItems.removeFromMixList: try { if (mutableAlbum.type == "MusicArtist") { - jellyfinApiHelper.removeArtistFromMixBuilderList(mutableAlbum); + jellyfinApiHelper + .removeArtistFromMixBuilderList(mutableAlbum); } else if (mutableAlbum.type == "MusicAlbum") { jellyfinApiHelper.removeAlbumFromMixBuilderList(mutableAlbum); } @@ -372,12 +379,13 @@ class _AlbumItemState extends State { item: mutableAlbum, contextNormalizationGain: widget.isPlaylist ? null - : mutableAlbum - .normalizationGain, + : mutableAlbum.normalizationGain, )); - GlobalSnackbar.message((scaffold) => - AppLocalizations.of(scaffold)!.confirmPlayNext(itemType), isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)! + .confirmPlayNext(itemType), + isConfirmation: true); setState(() {}); } catch (e) { @@ -418,12 +426,13 @@ class _AlbumItemState extends State { item: mutableAlbum, contextNormalizationGain: widget.isPlaylist ? null - : mutableAlbum - .normalizationGain, + : mutableAlbum.normalizationGain, )); - GlobalSnackbar.message((scaffold) => - AppLocalizations.of(scaffold)!.confirmAddToNextUp(itemType), isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)! + .confirmAddToNextUp(itemType), + isConfirmation: true); setState(() {}); } catch (e) { @@ -465,12 +474,13 @@ class _AlbumItemState extends State { item: mutableAlbum, contextNormalizationGain: widget.isPlaylist ? null - : mutableAlbum - .normalizationGain, + : mutableAlbum.normalizationGain, )); - GlobalSnackbar.message((scaffold) => - AppLocalizations.of(scaffold)!.confirmPlayNext(itemType), isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)! + .confirmPlayNext(itemType), + isConfirmation: true); setState(() {}); } catch (e) { @@ -512,12 +522,13 @@ class _AlbumItemState extends State { item: mutableAlbum, contextNormalizationGain: widget.isPlaylist ? null - : mutableAlbum - .normalizationGain, + : mutableAlbum.normalizationGain, )); - GlobalSnackbar.message((scaffold) => - AppLocalizations.of(scaffold)!.confirmShuffleToNextUp, isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => + AppLocalizations.of(scaffold)!.confirmShuffleToNextUp, + isConfirmation: true); setState(() {}); } catch (e) { @@ -558,12 +569,13 @@ class _AlbumItemState extends State { item: mutableAlbum, contextNormalizationGain: widget.isPlaylist ? null - : mutableAlbum - .normalizationGain, + : mutableAlbum.normalizationGain, )); - GlobalSnackbar.message((scaffold) => - AppLocalizations.of(scaffold)!.confirmAddToQueue(itemType), isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)! + .confirmAddToQueue(itemType), + isConfirmation: true); setState(() {}); } catch (e) { @@ -605,12 +617,13 @@ class _AlbumItemState extends State { item: mutableAlbum, contextNormalizationGain: widget.isPlaylist ? null - : mutableAlbum - .normalizationGain, + : mutableAlbum.normalizationGain, )); - GlobalSnackbar.message((scaffold) => - AppLocalizations.of(scaffold)!.confirmShuffleToQueue, isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => + AppLocalizations.of(scaffold)!.confirmShuffleToQueue, + isConfirmation: true); setState(() {}); } catch (e) { diff --git a/lib/components/MusicScreen/album_item_list_tile.dart b/lib/components/MusicScreen/album_item_list_tile.dart index 4f7ac68af..06523b262 100644 --- a/lib/components/MusicScreen/album_item_list_tile.dart +++ b/lib/components/MusicScreen/album_item_list_tile.dart @@ -30,42 +30,48 @@ class AlbumItemListTile extends StatelessWidget { final subtitle = generateSubtitle(item, parentType, context); return ListTile( - // This widget is used on the add to playlist screen, so we allow a custom - // onTap to be passed as an argument. - onTap: onTap, - leading: AlbumImage(item: item), - title: Text( - item.name ?? AppLocalizations.of(context)!.unknownName, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text.rich( - TextSpan(children: [ - WidgetSpan( - child: Transform.translate( - offset: const Offset(-3, 0), - child: DownloadedIndicator( - item: DownloadStub.fromItem( - item: item, type: DownloadItemType.collection), - size: Theme.of(context).textTheme.bodyMedium!.fontSize! + 3, + // This widget is used on the add to playlist screen, so we allow a custom + // onTap to be passed as an argument. + onTap: onTap, + leading: AlbumImage(item: item), + title: Text( + item.name ?? AppLocalizations.of(context)!.unknownName, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text.rich( + TextSpan(children: [ + WidgetSpan( + child: Transform.translate( + offset: const Offset(-3, 0), + child: DownloadedIndicator( + item: DownloadStub.fromItem( + item: item, type: DownloadItemType.collection), + size: Theme.of(context).textTheme.bodyMedium!.fontSize! + 3, + ), ), + alignment: PlaceholderAlignment.top, ), - alignment: PlaceholderAlignment.top, - ), - if (subtitle != null) - TextSpan( - text: subtitle, - style: TextStyle(color: Theme.of(context).disabledColor)) - ]), - overflow: TextOverflow.ellipsis, - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if ((item.type == "MusicArtist" ? jellyfinApiHelper.selectedMixArtists : jellyfinApiHelper.selectedMixAlbums).contains(item)) - const Icon(Icons.explore), - FavoriteButton(item: item, onlyIfFav: true,) - ],) - ); + if (subtitle != null) + TextSpan( + text: subtitle, + style: TextStyle(color: Theme.of(context).disabledColor)) + ]), + overflow: TextOverflow.ellipsis, + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if ((item.type == "MusicArtist" + ? jellyfinApiHelper.selectedMixArtists + : jellyfinApiHelper.selectedMixAlbums) + .contains(item)) + const Icon(Icons.explore), + FavoriteButton( + item: item, + onlyIfFav: true, + ) + ], + )); } } diff --git a/lib/components/MusicScreen/alphabet_item_list.dart b/lib/components/MusicScreen/alphabet_item_list.dart index e4169ca60..b7f406997 100644 --- a/lib/components/MusicScreen/alphabet_item_list.dart +++ b/lib/components/MusicScreen/alphabet_item_list.dart @@ -58,19 +58,16 @@ class _AlphabetListState extends State { @override Widget build(BuildContext context) { - final alphabetList = Container( margin: EdgeInsets.only( - bottom: - MediaQuery.paddingOf(context).bottom + _bottomPadding / 2, + bottom: MediaQuery.paddingOf(context).bottom + _bottomPadding / 2, ), decoration: FinampSettingsHelper.finampSettings.contentViewType == ContentViewType.grid ? BoxDecoration( borderRadius: BorderRadius.circular(12.0), - color: Theme.of(context) - .scaffoldBackgroundColor - .withOpacity(0.75), + color: + Theme.of(context).scaffoldBackgroundColor.withOpacity(0.75), ) : null, padding: EdgeInsets.only( @@ -80,10 +77,8 @@ class _AlphabetListState extends State { child: LayoutBuilder(builder: (context, constraints) { _letterHeight = constraints.maxHeight / alphabet.length; return Listener( - onPointerDown: (x) => - updateSelected(x.localPosition, Drag.start), - onPointerMove: (x) => - updateSelected(x.localPosition, Drag.update), + onPointerDown: (x) => updateSelected(x.localPosition, Drag.start), + onPointerMove: (x) => updateSelected(x.localPosition, Drag.update), onPointerUp: (x) => updateSelected(x.localPosition, Drag.end), behavior: HitTestBehavior.opaque, child: Column( @@ -91,8 +86,8 @@ class _AlphabetListState extends State { children: List.generate( alphabet.length, (x) => Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 0), + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 0), height: _letterHeight, child: FittedBox( child: Text( @@ -104,7 +99,7 @@ class _AlphabetListState extends State { ); }), ); - + // This gestureDetector blocks the horizontal drag to switch tabs gesture // while dragging on alphabet, but still allows all gestures on main content. // I don't know why this works, there's weird interactions between the listener, @@ -129,17 +124,19 @@ class _AlphabetListState extends State { style: const TextStyle(fontSize: 120))), ), ), - Directionality.of(context) == TextDirection.rtl ? Positioned( - left: 0, - top: -10, - bottom: -10, - child: alphabetList, - ) : Positioned( - right: 0, - top: -10, - bottom: -10, - child: alphabetList, - ), + Directionality.of(context) == TextDirection.rtl + ? Positioned( + left: 0, + top: -10, + bottom: -10, + child: alphabetList, + ) + : Positioned( + right: 0, + top: -10, + bottom: -10, + child: alphabetList, + ), ], ), ); diff --git a/lib/components/MusicScreen/music_screen_tab_view.dart b/lib/components/MusicScreen/music_screen_tab_view.dart index 89f979a7b..8d4abdb3a 100644 --- a/lib/components/MusicScreen/music_screen_tab_view.dart +++ b/lib/components/MusicScreen/music_screen_tab_view.dart @@ -201,10 +201,13 @@ class _MusicScreenTabViewState extends State } //TODO use binary search to improve performance for already loaded pages - bool reversed = FinampSettingsHelper.finampSettings.tabSortOrder[widget.tabContentType] == SortOrder.descending; + bool reversed = FinampSettingsHelper + .finampSettings.tabSortOrder[widget.tabContentType] == + SortOrder.descending; for (var i = 0; i < _pagingController.itemList!.length; i++) { - int itemCodePoint = - _pagingController.itemList![i].nameForSorting!.toLowerCase().codeUnitAt(0); + int itemCodePoint = _pagingController.itemList![i].nameForSorting! + .toLowerCase() + .codeUnitAt(0); if (itemCodePoint <= maxCodePoint) { final comparisonResult = itemCodePoint - codePointToScrollTo; if (comparisonResult == 0) { @@ -212,14 +215,15 @@ class _MusicScreenTabViewState extends State await controller.scrollToIndex(i, duration: _getAnimationDurationForOffsetToIndex(i), preferPosition: AutoScrollPosition.begin); - + letterToSearch = null; return; } else if (reversed ? comparisonResult < 0 : comparisonResult > 0) { // If the letter is before the current item, there was no previous match (letter doesn't seem to exist in library) // scroll to the previous item instead timer?.cancel(); - await controller.scrollToIndex((i - 1).clamp(0, (_pagingController.itemList?.length ?? 1) - 1), + await controller.scrollToIndex( + (i - 1).clamp(0, (_pagingController.itemList?.length ?? 1) - 1), // duration: scrollDuration, duration: _getAnimationDurationForOffsetToIndex(i), preferPosition: AutoScrollPosition.middle); @@ -251,7 +255,8 @@ class _MusicScreenTabViewState extends State final medianIndex = renderedIndices.elementAt(renderedIndices.length ~/ 2); final duration = Duration( - milliseconds: ((medianIndex - index).abs() / 50 * 300).clamp(200, 7500).round(), + milliseconds: + ((medianIndex - index).abs() / 50 * 300).clamp(200, 7500).round(), ); return duration; } diff --git a/lib/components/MusicScreen/sort_order_button.dart b/lib/components/MusicScreen/sort_order_button.dart index 09d8cab71..cf54c8c35 100644 --- a/lib/components/MusicScreen/sort_order_button.dart +++ b/lib/components/MusicScreen/sort_order_button.dart @@ -25,7 +25,7 @@ class SortOrderButton extends StatelessWidget { : const Icon(Icons.arrow_upward), onPressed: () { if (finampSettings.getSortOrder(tabType) == SortOrder.ascending) { - FinampSettingsHelper.setSortOrder(tabType,SortOrder.descending); + FinampSettingsHelper.setSortOrder(tabType, SortOrder.descending); } else { FinampSettingsHelper.setSortOrder(tabType, SortOrder.ascending); } diff --git a/lib/components/PlaybackHistoryScreen/playback_history_list.dart b/lib/components/PlaybackHistoryScreen/playback_history_list.dart index f2b97b494..b5259cea9 100644 --- a/lib/components/PlaybackHistoryScreen/playback_history_list.dart +++ b/lib/components/PlaybackHistoryScreen/playback_history_list.dart @@ -66,7 +66,7 @@ class PlaybackHistoryList extends StatelessWidget { .item .extras?["itemJson"])) .catchError((e) { - GlobalSnackbar.error(e); + GlobalSnackbar.error(e); }); }, ), diff --git a/lib/components/PlayerScreen/feature_chips.dart b/lib/components/PlayerScreen/feature_chips.dart index afa6d83a3..8fe3ea4c2 100644 --- a/lib/components/PlayerScreen/feature_chips.dart +++ b/lib/components/PlayerScreen/feature_chips.dart @@ -32,11 +32,16 @@ class FeatureState { final MetadataProvider? metadata; bool get isDownloaded => metadata?.isDownloaded ?? false; - bool get isTranscoding => !isDownloaded && (currentTrack?.item.extras?["shouldTranscode"] ?? false); - String get container => isTranscoding ? "aac" : metadata?.mediaSourceInfo.container ?? ""; + bool get isTranscoding => + !isDownloaded && (currentTrack?.item.extras?["shouldTranscode"] ?? false); + String get container => + isTranscoding ? "aac" : metadata?.mediaSourceInfo.container ?? ""; int? get size => isTranscoding ? null : metadata?.mediaSourceInfo.size; - MediaStream? get audioStream => metadata?.mediaSourceInfo.mediaStreams.firstWhereOrNull((stream) => stream.type == "Audio"); - int? get bitrate => isTranscoding ? settings.transcodeBitrate : audioStream?.bitRate ?? metadata?.mediaSourceInfo.bitrate; + MediaStream? get audioStream => metadata?.mediaSourceInfo.mediaStreams + .firstWhereOrNull((stream) => stream.type == "Audio"); + int? get bitrate => isTranscoding + ? settings.transcodeBitrate + : audioStream?.bitRate ?? metadata?.mediaSourceInfo.bitrate; int? get sampleRate => audioStream?.sampleRate; int? get bitDepth => audioStream?.bitDepth; @@ -48,7 +53,8 @@ class FeatureState { if (queueService.playbackSpeed != 1.0) { features.add( FeatureProperties( - text: AppLocalizations.of(context)!.playbackSpeedFeatureText(queueService.playbackSpeed), + text: AppLocalizations.of(context)! + .playbackSpeedFeatureText(queueService.playbackSpeed), ), ); } @@ -100,11 +106,11 @@ class FeatureState { } if (metadata?.mediaSourceInfo != null) { - if (bitrate != null) { features.add( FeatureProperties( - text: "${container.toUpperCase()} @ ${AppLocalizations.of(context)!.kiloBitsPerSecondLabel(bitrate! ~/ 1000)}", + text: + "${container.toUpperCase()} @ ${AppLocalizations.of(context)!.kiloBitsPerSecondLabel(bitrate! ~/ 1000)}", ), ); } @@ -120,7 +126,8 @@ class FeatureState { if (sampleRate != null) { features.add( FeatureProperties( - text: AppLocalizations.of(context)!.numberAsKiloHertz(sampleRate! / 1000.0), + text: AppLocalizations.of(context)! + .numberAsKiloHertz(sampleRate! / 1000.0), ), ); } @@ -135,11 +142,13 @@ class FeatureState { } if (FinampSettingsHelper.finampSettings.volumeNormalizationActive) { - double? effectiveGainChange = getEffectiveGainChange(currentTrack!.item, currentTrack!.baseItem); + double? effectiveGainChange = + getEffectiveGainChange(currentTrack!.item, currentTrack!.baseItem); if (effectiveGainChange != null) { features.add( FeatureProperties( - text: AppLocalizations.of(context)!.numberAsDecibel(double.parse(effectiveGainChange.toStringAsFixed(1))), + text: AppLocalizations.of(context)!.numberAsDecibel( + double.parse(effectiveGainChange.toStringAsFixed(1))), ), ); } diff --git a/lib/components/PlayerScreen/player_screen_album_image.dart b/lib/components/PlayerScreen/player_screen_album_image.dart index 0ab97eee3..a73a5244a 100644 --- a/lib/components/PlayerScreen/player_screen_album_image.dart +++ b/lib/components/PlayerScreen/player_screen_album_image.dart @@ -38,14 +38,12 @@ class PlayerScreenAlbumImage extends StatelessWidget { onHorizontalSwipe: (direction) { final queueService = GetIt.instance(); if (direction == SwipeDirection.left) { - if (!FinampSettingsHelper - .finampSettings.disableGesture) { + if (!FinampSettingsHelper.finampSettings.disableGesture) { queueService.skipByOffset(1); FeedbackHelper.feedback(FeedbackType.selection); } } else if (direction == SwipeDirection.right) { - if (!FinampSettingsHelper - .finampSettings.disableGesture) { + if (!FinampSettingsHelper.finampSettings.disableGesture) { queueService.skipByOffset(-1); FeedbackHelper.feedback(FeedbackType.selection); } diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index d44f8d7df..b51929e08 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -389,7 +389,9 @@ class JumpToCurrentButtonState extends State { shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(16.0))), icon: Icon( - _jumpToCurrentTrackDirection < 0 ? TablerIcons.arrow_bar_to_up : TablerIcons.arrow_bar_to_down, + _jumpToCurrentTrackDirection < 0 + ? TablerIcons.arrow_bar_to_up + : TablerIcons.arrow_bar_to_down, size: 28.0, color: Colors.white.withOpacity(0.9), ), @@ -401,7 +403,7 @@ class JumpToCurrentButtonState extends State { fontWeight: FontWeight.w500, ), ), - ) + ) : const SizedBox.shrink(); } } diff --git a/lib/components/PlayerScreen/sleep_timer_dialog.dart b/lib/components/PlayerScreen/sleep_timer_dialog.dart index 1e94adffe..fbdc19078 100644 --- a/lib/components/PlayerScreen/sleep_timer_dialog.dart +++ b/lib/components/PlayerScreen/sleep_timer_dialog.dart @@ -33,7 +33,8 @@ class _SleepTimerDialogState extends State { Expanded( child: TextFormField( controller: _textController, - keyboardType: const TextInputType.numberWithOptions(decimal: true), + keyboardType: + const TextInputType.numberWithOptions(decimal: true), textAlign: TextAlign.center, decoration: InputDecoration( labelText: AppLocalizations.of(context)!.minutes), @@ -51,7 +52,8 @@ class _SleepTimerDialogState extends State { final valueDouble = double.parse(value!); final durationInSeconds = (valueDouble * 60).round(); - _audioHandler.setSleepTimer(Duration(seconds: durationInSeconds)); + _audioHandler + .setSleepTimer(Duration(seconds: durationInSeconds)); FinampSettingsHelper.setSleepTimerSeconds(durationInSeconds); }, ), diff --git a/lib/components/PlayerScreen/song_name_content.dart b/lib/components/PlayerScreen/song_name_content.dart index f2315c30f..d91776af5 100644 --- a/lib/components/PlayerScreen/song_name_content.dart +++ b/lib/components/PlayerScreen/song_name_content.dart @@ -30,15 +30,15 @@ class SongNameContent extends StatelessWidget { child: CircularProgressIndicator(), ); } - + final currentTrack = snapshot.data!.currentTrack!; - + final jellyfin_models.BaseItemDto? songBaseItemDto = currentTrack.item.extras!["itemJson"] != null ? jellyfin_models.BaseItemDto.fromJson( currentTrack.item.extras!["itemJson"]) : null; - + return LayoutBuilder(builder: (context, constraints) { double padding = ((constraints.maxWidth - 260) / 4).clamp(0, 20); return Padding( diff --git a/lib/components/SettingsScreen/logout_list_tile.dart b/lib/components/SettingsScreen/logout_list_tile.dart index cd4501e3e..cf0f6c8dc 100644 --- a/lib/components/SettingsScreen/logout_list_tile.dart +++ b/lib/components/SettingsScreen/logout_list_tile.dart @@ -64,7 +64,9 @@ class _LogoutListTileState extends State { // We don't want audio to be playing after we log out. // We check if the audio service is running on iOS because // stop() never completes if the service is not running. - if (!Platform.isIOS || (audioHandler.playbackState.valueOrNull?.playing ?? false)) { + if (!Platform.isIOS || + (audioHandler.playbackState.valueOrNull?.playing ?? + false)) { await queueService.stopPlayback(); } diff --git a/lib/components/TranscodingSettingsScreen/bitrate_selector.dart b/lib/components/TranscodingSettingsScreen/bitrate_selector.dart index 6113b73e0..64dcdd366 100644 --- a/lib/components/TranscodingSettingsScreen/bitrate_selector.dart +++ b/lib/components/TranscodingSettingsScreen/bitrate_selector.dart @@ -31,14 +31,16 @@ class BitrateSelector extends StatelessWidget { max: 320, value: finampSettings.transcodeBitrate / 1000, divisions: 8, - label: AppLocalizations.of(context)!.kiloBitsPerSecondLabel(finampSettings.transcodeBitrate ~/ 1000), + label: AppLocalizations.of(context)!.kiloBitsPerSecondLabel( + finampSettings.transcodeBitrate ~/ 1000), onChanged: (value) { FinampSettingsHelper.setTranscodeBitrate( (value * 1000).toInt()); }, ), Text( - AppLocalizations.of(context)!.kiloBitsPerSecondLabel(finampSettings.transcodeBitrate ~/ 1000), + AppLocalizations.of(context)!.kiloBitsPerSecondLabel( + finampSettings.transcodeBitrate ~/ 1000), style: Theme.of(context).textTheme.titleLarge, ) ], diff --git a/lib/components/VolumeNormalizationSettingsScreen/volume_normalization_ios_base_gain_editor.dart b/lib/components/VolumeNormalizationSettingsScreen/volume_normalization_ios_base_gain_editor.dart index fe40f582a..0d2d29310 100644 --- a/lib/components/VolumeNormalizationSettingsScreen/volume_normalization_ios_base_gain_editor.dart +++ b/lib/components/VolumeNormalizationSettingsScreen/volume_normalization_ios_base_gain_editor.dart @@ -14,14 +14,16 @@ class VolumeNormalizationIOSBaseGainEditor extends StatefulWidget { class _VolumeNormalizationIOSBaseGainEditorState extends State { final _controller = TextEditingController( - text: - FinampSettingsHelper.finampSettings.volumeNormalizationIOSBaseGain.toString()); + text: FinampSettingsHelper.finampSettings.volumeNormalizationIOSBaseGain + .toString()); @override Widget build(BuildContext context) { return ListTile( - title: Text(AppLocalizations.of(context)!.volumeNormalizationIOSBaseGainEditorTitle), - subtitle: Text(AppLocalizations.of(context)!.volumeNormalizationIOSBaseGainEditorSubtitle), + title: Text(AppLocalizations.of(context)! + .volumeNormalizationIOSBaseGainEditorTitle), + subtitle: Text(AppLocalizations.of(context)! + .volumeNormalizationIOSBaseGainEditorSubtitle), trailing: SizedBox( width: 50 * MediaQuery.of(context).textScaleFactor, child: TextField( @@ -32,7 +34,8 @@ class _VolumeNormalizationIOSBaseGainEditorState final valueDouble = double.tryParse(value); if (valueDouble != null) { - FinampSettingsHelper.setVolumeNormalizationIOSBaseGain(valueDouble); + FinampSettingsHelper.setVolumeNormalizationIOSBaseGain( + valueDouble); } }, ), diff --git a/lib/components/VolumeNormalizationSettingsScreen/volume_normalization_mode_selector.dart b/lib/components/VolumeNormalizationSettingsScreen/volume_normalization_mode_selector.dart index eb5df26a3..d0ce5b0ac 100644 --- a/lib/components/VolumeNormalizationSettingsScreen/volume_normalization_mode_selector.dart +++ b/lib/components/VolumeNormalizationSettingsScreen/volume_normalization_mode_selector.dart @@ -32,14 +32,17 @@ class VolumeNormalizationModeSelector extends StatelessWidget { return ValueListenableBuilder>( valueListenable: FinampSettingsHelper.finampSettingsListener, builder: (_, box, __) { - VolumeNormalizationMode? volumeNormalizationMode = box.get("FinampSettings")?.volumeNormalizationMode; + VolumeNormalizationMode? volumeNormalizationMode = + box.get("FinampSettings")?.volumeNormalizationMode; return ListTile( - title: Text(AppLocalizations.of(context)!.volumeNormalizationModeSelectorTitle), + title: Text(AppLocalizations.of(context)! + .volumeNormalizationModeSelectorTitle), subtitle: RichText( text: TextSpan( children: [ TextSpan( - text: AppLocalizations.of(context)!.volumeNormalizationModeSelectorSubtitle, + text: AppLocalizations.of(context)! + .volumeNormalizationModeSelectorSubtitle, style: Theme.of(context).textTheme.bodyMedium, ), const TextSpan(text: "\n"), @@ -52,20 +55,25 @@ class VolumeNormalizationModeSelector extends StatelessWidget { ), recognizer: TapGestureRecognizer() ..onTap = () { - showGeneralDialog(context: context, pageBuilder: (context, anim1, anim2) { - return AlertDialog( - title: Text(AppLocalizations.of(context)!.volumeNormalizationModeSelectorTitle), - content: Text(AppLocalizations.of(context)!.volumeNormalizationModeSelectorDescription), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(AppLocalizations.of(context)!.close), - ), - ], - ); - }); + showGeneralDialog( + context: context, + pageBuilder: (context, anim1, anim2) { + return AlertDialog( + title: Text(AppLocalizations.of(context)! + .volumeNormalizationModeSelectorTitle), + content: Text(AppLocalizations.of(context)! + .volumeNormalizationModeSelectorDescription), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: + Text(AppLocalizations.of(context)!.close), + ), + ], + ); + }); }, ), ], @@ -81,10 +89,9 @@ class VolumeNormalizationModeSelector extends StatelessWidget { .toList(), onChanged: (value) { if (value != null) { - FinampSettings finampSettingsTemp = - box.get("FinampSettings")!; - finampSettingsTemp.volumeNormalizationMode = value; - box.put("FinampSettings", finampSettingsTemp); + FinampSettings finampSettingsTemp = box.get("FinampSettings")!; + finampSettingsTemp.volumeNormalizationMode = value; + box.put("FinampSettings", finampSettingsTemp); } }, ), diff --git a/lib/components/VolumeNormalizationSettingsScreen/volume_normalization_switch.dart b/lib/components/VolumeNormalizationSettingsScreen/volume_normalization_switch.dart index bee283eed..477520ef6 100644 --- a/lib/components/VolumeNormalizationSettingsScreen/volume_normalization_switch.dart +++ b/lib/components/VolumeNormalizationSettingsScreen/volume_normalization_switch.dart @@ -13,12 +13,14 @@ class VolumeNormalizationSwitch extends StatelessWidget { return ValueListenableBuilder>( valueListenable: FinampSettingsHelper.finampSettingsListener, builder: (context, box, child) { - bool? volumeNormalizationActive = box.get("FinampSettings")?.volumeNormalizationActive; + bool? volumeNormalizationActive = + box.get("FinampSettings")?.volumeNormalizationActive; return SwitchListTile.adaptive( - title: Text(AppLocalizations.of(context)!.volumeNormalizationSwitchTitle), - subtitle: - Text(AppLocalizations.of(context)!.volumeNormalizationSwitchSubtitle), + title: Text( + AppLocalizations.of(context)!.volumeNormalizationSwitchTitle), + subtitle: Text( + AppLocalizations.of(context)!.volumeNormalizationSwitchSubtitle), value: volumeNormalizationActive ?? false, onChanged: volumeNormalizationActive == null ? null diff --git a/lib/components/album_list_tile.dart b/lib/components/album_list_tile.dart index 1efe3d1b6..fff5abbef 100644 --- a/lib/components/album_list_tile.dart +++ b/lib/components/album_list_tile.dart @@ -319,8 +319,10 @@ class _AlbumListTileState extends State { await _queueService.addNext( items: albumTracks, source: queueSource); - GlobalSnackbar.message((scaffold) => - AppLocalizations.of(scaffold)!.confirmPlayNext("album"), isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => + AppLocalizations.of(scaffold)!.confirmPlayNext("album"), + isConfirmation: true); setState(() {}); } catch (e) { @@ -345,8 +347,10 @@ class _AlbumListTileState extends State { await _queueService.addToNextUp( items: albumTracks, source: queueSource); - GlobalSnackbar.message((scaffold) => - AppLocalizations.of(scaffold)!.confirmAddToNextUp("album"), isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)! + .confirmAddToNextUp("album"), + isConfirmation: true); setState(() {}); } catch (e) { @@ -371,8 +375,10 @@ class _AlbumListTileState extends State { await _queueService.addNext( items: albumTracks, source: queueSource); - GlobalSnackbar.message((scaffold) => - AppLocalizations.of(scaffold)!.confirmPlayNext("album"), isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => + AppLocalizations.of(scaffold)!.confirmPlayNext("album"), + isConfirmation: true); setState(() {}); } catch (e) { @@ -398,8 +404,10 @@ class _AlbumListTileState extends State { await _queueService.addToNextUp( items: albumTracks, source: queueSource); - GlobalSnackbar.message((scaffold) => - AppLocalizations.of(scaffold)!.confirmShuffleToNextUp, isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => + AppLocalizations.of(scaffold)!.confirmShuffleToNextUp, + isConfirmation: true); setState(() {}); } catch (e) { @@ -424,8 +432,10 @@ class _AlbumListTileState extends State { await _queueService.addToQueue( items: albumTracks, source: queueSource); - GlobalSnackbar.message((scaffold) => - AppLocalizations.of(scaffold)!.confirmAddToQueue("album"), isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)! + .confirmAddToQueue("album"), + isConfirmation: true); setState(() {}); } catch (e) { @@ -450,8 +460,10 @@ class _AlbumListTileState extends State { await _queueService.addToQueue( items: albumTracks, source: queueSource); - GlobalSnackbar.message((scaffold) => - AppLocalizations.of(scaffold)!.confirmAddToQueue("album"), isConfirmation: true); + GlobalSnackbar.message( + (scaffold) => AppLocalizations.of(scaffold)! + .confirmAddToQueue("album"), + isConfirmation: true); setState(() {}); } catch (e) { diff --git a/lib/components/artist_text_spans.dart b/lib/components/artist_text_spans.dart index 58b757dc8..eb7e62bad 100644 --- a/lib/components/artist_text_spans.dart +++ b/lib/components/artist_text_spans.dart @@ -8,17 +8,12 @@ import '../../services/jellyfin_api_helper.dart'; import '../screens/artist_screen.dart'; List getArtistsTextSpans( - BaseItemDto item, - BuildContext context, - bool popRoutes - ) { + BaseItemDto item, BuildContext context, bool popRoutes) { final jellyfinApiHelper = GetIt.instance(); List separatedArtistTextSpans = []; List? artists = - item.type == "MusicAlbum" - ? item.albumArtists - : item.artistItems; + item.type == "MusicAlbum" ? item.albumArtists : item.artistItems; if (artists?.isEmpty ?? true) { separatedArtistTextSpans = [ @@ -29,25 +24,22 @@ List getArtistsTextSpans( ]; } else { artists - ?.map((e) => TextSpan( - text: e.name, - style: Theme.of(context).textTheme.bodyMedium, - recognizer: TapGestureRecognizer() - ..onTap = () { - // Offline artists aren't implemented yet so we return if offline - if (FinampSettingsHelper.finampSettings.isOffline) return; + ?.map((e) => TextSpan( + text: e.name, + style: Theme.of(context).textTheme.bodyMedium, + recognizer: TapGestureRecognizer() + ..onTap = () { + // Offline artists aren't implemented yet so we return if offline + if (FinampSettingsHelper.finampSettings.isOffline) return; - jellyfinApiHelper.getItemById(e.id).then((artist) => - popRoutes - ? Navigator.of(context).popAndPushNamed( - ArtistScreen.routeName, - arguments: artist) - : Navigator.of(context).pushNamed( - ArtistScreen.routeName, - arguments: artist) - ); - })) - .forEach((artistTextSpan) { + jellyfinApiHelper.getItemById(e.id).then((artist) => popRoutes + ? Navigator.of(context).popAndPushNamed( + ArtistScreen.routeName, + arguments: artist) + : Navigator.of(context) + .pushNamed(ArtistScreen.routeName, arguments: artist)); + })) + .forEach((artistTextSpan) { separatedArtistTextSpans.add(artistTextSpan); separatedArtistTextSpans.add(TextSpan( text: ", ", diff --git a/lib/components/confirmation_prompt_dialog.dart b/lib/components/confirmation_prompt_dialog.dart index 4c241af8a..1bc530912 100644 --- a/lib/components/confirmation_prompt_dialog.dart +++ b/lib/components/confirmation_prompt_dialog.dart @@ -20,7 +20,6 @@ class ConfirmationPromptDialog extends AlertDialog { final void Function()? onConfirmed; final void Function()? onAborted; - @override Widget build(BuildContext context) { return AlertDialog( @@ -41,7 +40,8 @@ class ConfirmationPromptDialog extends AlertDialog { maxWidth: 150.0, ), child: TextButton( - child: Text(abortButtonText, + child: Text( + abortButtonText, textAlign: TextAlign.center, ), onPressed: () { @@ -55,7 +55,8 @@ class ConfirmationPromptDialog extends AlertDialog { maxWidth: 150.0, ), child: TextButton( - child: Text(confirmButtonText, + child: Text( + confirmButtonText, textAlign: TextAlign.center, softWrap: true, ), diff --git a/lib/components/favourite_button.dart b/lib/components/favourite_button.dart index b274a32e5..b21e2db13 100644 --- a/lib/components/favourite_button.dart +++ b/lib/components/favourite_button.dart @@ -40,7 +40,7 @@ class _FavoriteButtonState extends State { final jellyfinApiHelper = GetIt.instance(); final isOffline = FinampSettingsHelper.finampSettings.isOffline; - + if (widget.item == null) { return const SizedBox.shrink(); } @@ -64,42 +64,45 @@ class _FavoriteButtonState extends State { size: widget.size ?? 24.0, ), color: widget.color ?? IconTheme.of(context).color, - disabledColor: (widget.color ?? IconTheme.of(context).color)!.withOpacity(0.3), + disabledColor: + (widget.color ?? IconTheme.of(context).color)!.withOpacity(0.3), visualDensity: widget.visualDensity ?? VisualDensity.compact, tooltip: AppLocalizations.of(context)!.favourite, - onPressed: isOffline ? null : () async { + onPressed: isOffline + ? null + : () async { + if (isOffline) { + FeedbackHelper.feedback(FeedbackType.error); + GlobalSnackbar.message((context) => + AppLocalizations.of(context)!.notAvailableInOfflineMode); + return; + } - if (isOffline) { - FeedbackHelper.feedback(FeedbackType.error); - GlobalSnackbar.message((context) => AppLocalizations.of(context)!.notAvailableInOfflineMode); - return; - } - - try { - UserItemDataDto? newUserData; - if (isFav) { - newUserData = - await jellyfinApiHelper.removeFavourite(widget.item!.id); - } else { - newUserData = - await jellyfinApiHelper.addFavourite(widget.item!.id); - } - setState(() { - widget.item!.userData = newUserData; - if (widget.inPlayer) { - audioHandler.mediaItem.valueOrNull!.extras!['itemJson'] = - widget.item!.toJson(); - } - }); + try { + UserItemDataDto? newUserData; + if (isFav) { + newUserData = await jellyfinApiHelper + .removeFavourite(widget.item!.id); + } else { + newUserData = + await jellyfinApiHelper.addFavourite(widget.item!.id); + } + setState(() { + widget.item!.userData = newUserData; + if (widget.inPlayer) { + audioHandler.mediaItem.valueOrNull!.extras!['itemJson'] = + widget.item!.toJson(); + } + }); - if (widget.onToggle != null) { - FeedbackHelper.feedback(FeedbackType.success); - widget.onToggle!(widget.item!.userData!.isFavorite); - } - } catch (e) { - GlobalSnackbar.error(e); - } - }, + if (widget.onToggle != null) { + FeedbackHelper.feedback(FeedbackType.success); + widget.onToggle!(widget.item!.userData!.isFavorite); + } + } catch (e) { + GlobalSnackbar.error(e); + } + }, ); } } diff --git a/lib/components/finamp_app_bar_button.dart b/lib/components/finamp_app_bar_button.dart index 0630a741e..7a778e320 100644 --- a/lib/components/finamp_app_bar_button.dart +++ b/lib/components/finamp_app_bar_button.dart @@ -9,12 +9,12 @@ class FinampAppBarButton extends StatelessWidget { }); final VoidCallback? onPressed; + /// The direction in which the screen will slide when the button is pressed. final AxisDirection dismissDirection; @override Widget build(BuildContext context) { - IconData getIcon() { switch (dismissDirection) { case AxisDirection.down: @@ -29,7 +29,7 @@ class FinampAppBarButton extends StatelessWidget { return TablerIcons.chevron_down; } } - + return Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), child: IconButton( @@ -41,7 +41,7 @@ class FinampAppBarButton extends StatelessWidget { weight: 2.0, ), // 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 // button by now. splashRadius: Material.defaultSplashRadius - 8, diff --git a/lib/components/global_snackbar.dart b/lib/components/global_snackbar.dart index 3357bb702..4e2417d2f 100644 --- a/lib/components/global_snackbar.dart +++ b/lib/components/global_snackbar.dart @@ -62,17 +62,21 @@ class GlobalSnackbar { } /// Show a localized message to the user using the global context - static void message(String Function(BuildContext scaffold) message, { + static void message( + String Function(BuildContext scaffold) message, { bool isConfirmation = false, }) => _enqueue(() => _message(message, isConfirmation)); - static void _message(String Function(BuildContext scaffold) message, bool isConfirmation) { + static void _message( + String Function(BuildContext scaffold) message, bool isConfirmation) { var text = message(materialAppNavigatorKey.currentContext!); _logger.info("Displaying message: $text"); materialAppScaffoldKey.currentState!.showSnackBar( SnackBar( content: Text(text), - duration: isConfirmation ? const Duration(milliseconds: 1500) : const Duration(seconds: 4), + duration: isConfirmation + ? const Duration(milliseconds: 1500) + : const Duration(seconds: 4), ), ); } diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index e3d3857f0..c751ec6ea 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -510,7 +510,8 @@ class NowPlayingBar extends ConsumerWidget { var imageTheme = ref.watch(playerScreenThemeProvider(Theme.of(context).brightness)); - ref.listen(currentTrackMetadataProvider, (metadataOrNull, metadata) {}); // keep provider alive + ref.listen(currentTrackMetadataProvider, + (metadataOrNull, metadata) {}); // keep provider alive return Hero( tag: "nowplaying", diff --git a/lib/main.dart b/lib/main.dart index 7f2117646..4111501a7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -221,7 +221,12 @@ Future setupHive() async { final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( - [DownloadItemSchema, IsarTaskDataSchema, FinampUserSchema, DownloadedLyricsSchema], + [ + DownloadItemSchema, + IsarTaskDataSchema, + FinampUserSchema, + DownloadedLyricsSchema + ], directory: dir.path, name: isarDatabaseName, ); diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 249a29632..4f39cd28c 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -116,7 +116,8 @@ class FinampSettings { this.sortOrder = SortOrder.ascending, this.songShuffleItemCount = _songShuffleItemCountDefault, this.volumeNormalizationActive = _volumeNormalizationActiveDefault, - this.volumeNormalizationIOSBaseGain = _volumeNormalizationIOSBaseGainDefault, + this.volumeNormalizationIOSBaseGain = + _volumeNormalizationIOSBaseGainDefault, this.volumeNormalizationMode = _volumeNormalizationModeDefault, this.contentViewType = _contentViewType, this.playbackSpeedVisibility = _playbackSpeedVisibility, @@ -1023,10 +1024,8 @@ class DownloadItem extends DownloadStub { item.childCount ??= baseItem?.childCount; } assert(item == null || - ((item.mediaSources == null || - item.mediaSources!.isNotEmpty) && - (item.mediaStreams == null || - item.mediaStreams!.isNotEmpty))); + ((item.mediaSources == null || item.mediaSources!.isNotEmpty) && + (item.mediaStreams == null || item.mediaStreams!.isNotEmpty))); var orderedChildren = orderedChildItems?.map((e) => e.isarId).toList(); if (viewId == null || viewId == this.viewId) { if (item == null || baseItem!.mostlyEqual(item)) { @@ -1756,7 +1755,7 @@ enum TranscodeDownloadsSetting { /// TODO @collection class DownloadedLyrics { - DownloadedLyrics({ + DownloadedLyrics({ required this.jsonItem, required this.isarId, }); @@ -1766,8 +1765,8 @@ class DownloadedLyrics { required int isarId, }) { return DownloadedLyrics( - isarId: isarId, - jsonItem: jsonEncode(item.toJson()), + isarId: isarId, + jsonItem: jsonEncode(item.toJson()), ); } @@ -1826,5 +1825,4 @@ enum PlaybackSpeedVisibility { return AppLocalizations.of(context)!.hidden; } } - } diff --git a/lib/models/jellyfin_models.dart b/lib/models/jellyfin_models.dart index 8a9395562..7a3624661 100644 --- a/lib/models/jellyfin_models.dart +++ b/lib/models/jellyfin_models.dart @@ -3846,7 +3846,6 @@ class LyricMetadata { factory LyricMetadata.fromJson(Map json) => _$LyricMetadataFromJson(json); Map toJson() => _$LyricMetadataToJson(this); - } /// Lyric model. @@ -3873,7 +3872,6 @@ class LyricLine { factory LyricLine.fromJson(Map json) => _$LyricLineFromJson(json); Map toJson() => _$LyricLineToJson(this); - } /// LyricResponse model. @@ -3900,5 +3898,4 @@ class LyricDto { factory LyricDto.fromJson(Map json) => _$LyricDtoFromJson(json); Map toJson() => _$LyricDtoToJson(this); - } diff --git a/lib/screens/customization_settings_screen.dart b/lib/screens/customization_settings_screen.dart index 1bb3db00b..e1b02d9a3 100644 --- a/lib/screens/customization_settings_screen.dart +++ b/lib/screens/customization_settings_screen.dart @@ -11,10 +11,12 @@ class CustomizationSettingsScreen extends StatefulWidget { static const routeName = "/settings/customization"; @override - State createState() => _CustomizationSettingsScreenState(); + State createState() => + _CustomizationSettingsScreenState(); } -class _CustomizationSettingsScreenState extends State { +class _CustomizationSettingsScreenState + extends State { @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/screens/layout_settings_screen.dart b/lib/screens/layout_settings_screen.dart index f159c703e..152e4c8e9 100644 --- a/lib/screens/layout_settings_screen.dart +++ b/lib/screens/layout_settings_screen.dart @@ -29,9 +29,10 @@ class LayoutSettingsScreen extends StatelessWidget { children: [ ListTile( leading: const Icon(TablerIcons.sparkles), - title: Text(AppLocalizations.of(context)!.customizationSettingsTitle), - onTap: () => - Navigator.of(context).pushNamed(CustomizationSettingsScreen.routeName), + title: + Text(AppLocalizations.of(context)!.customizationSettingsTitle), + onTap: () => Navigator.of(context) + .pushNamed(CustomizationSettingsScreen.routeName), ), ListTile( leading: const Icon(Icons.play_circle_outline), diff --git a/lib/screens/lyrics_screen.dart b/lib/screens/lyrics_screen.dart index 5a4110d16..76d5ef067 100644 --- a/lib/screens/lyrics_screen.dart +++ b/lib/screens/lyrics_screen.dart @@ -63,10 +63,8 @@ class _LyricsScreenContent extends StatefulWidget { } class _LyricsScreenContentState extends State<_LyricsScreenContent> { - @override Widget build(BuildContext context) { - double toolbarHeight = 53; int maxLines = 2; @@ -76,7 +74,8 @@ class _LyricsScreenContentState extends State<_LyricsScreenContent> { appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, - scrolledUnderElevation: 0.0, // disable tint/shadow when content is scrolled under the app bar + scrolledUnderElevation: + 0.0, // disable tint/shadow when content is scrolled under the app bar centerTitle: true, toolbarHeight: toolbarHeight, title: PlayerScreenAppBarTitle( @@ -109,19 +108,21 @@ class _LyricsScreenContentState extends State<_LyricsScreenContent> { minimum: EdgeInsets.only(top: toolbarHeight), child: LayoutBuilder(builder: (context, constraints) { if (MediaQuery.of(context).orientation == Orientation.landscape) { - controller.updateLayoutLandscape(Size(constraints.maxWidth, constraints.maxHeight)); + controller.updateLayoutLandscape( + Size(constraints.maxWidth, constraints.maxHeight)); return SimpleGestureDetector( - onHorizontalSwipe: (direction) { - if (direction == SwipeDirection.right) { - if (!FinampSettingsHelper.finampSettings.disableGesture) { - Navigator.of(context).pop(); + onHorizontalSwipe: (direction) { + if (direction == SwipeDirection.right) { + if (!FinampSettingsHelper + .finampSettings.disableGesture) { + Navigator.of(context).pop(); + } } - } - }, - child: const LyricsView() - ); + }, + child: const LyricsView()); } else { - controller.updateLayoutPortrait(Size(constraints.maxWidth, constraints.maxHeight)); + controller.updateLayoutPortrait( + Size(constraints.maxWidth, constraints.maxHeight)); return SimpleGestureDetector( onHorizontalSwipe: (direction) { if (direction == SwipeDirection.right) { @@ -137,24 +138,24 @@ class _LyricsScreenContentState extends State<_LyricsScreenContent> { child: LyricsView(), ), SimpleGestureDetector( - onVerticalSwipe: (direction) { - if (direction == SwipeDirection.up) { - // This should never actually be called until widget finishes build and controller is initialized - if (!FinampSettingsHelper.finampSettings.disableGesture) { - showQueueBottomSheet(context); + onVerticalSwipe: (direction) { + if (direction == SwipeDirection.up) { + // This should never actually be called until widget finishes build and controller is initialized + if (!FinampSettingsHelper + .finampSettings.disableGesture) { + showQueueBottomSheet(context); + } } - } - }, - child: Column( - children: [ - SongNameContent(controller), - ControlArea(controller), - const SizedBox( - height: 12, - ) - ], - ) - ) + }, + child: Column( + children: [ + SongNameContent(controller), + ControlArea(controller), + const SizedBox( + height: 12, + ) + ], + )) ], ), ); @@ -168,15 +169,14 @@ class _LyricsScreenContentState extends State<_LyricsScreenContent> { } class LyricsView extends ConsumerStatefulWidget { - const LyricsView({super.key}); @override _LyricsViewState createState() => _LyricsViewState(); } -class _LyricsViewState extends ConsumerState with WidgetsBindingObserver { - +class _LyricsViewState extends ConsumerState + with WidgetsBindingObserver { late AutoScrollController autoScrollController; StreamSubscription? progressStateStreamSubscription; Duration? currentPosition; @@ -191,10 +191,10 @@ class _LyricsViewState extends ConsumerState with WidgetsBindingObse void initState() { WidgetsBinding.instance!.addObserver(this); autoScrollController = AutoScrollController( - suggestedRowHeight: 72, - viewportBoundaryGetter: () => - Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom), - axis: Axis.vertical); + suggestedRowHeight: 72, + viewportBoundaryGetter: () => + Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom), + axis: Axis.vertical); super.initState(); } @@ -219,42 +219,40 @@ class _LyricsViewState extends ConsumerState with WidgetsBindingObse final _audioHandler = GetIt.instance(); //!!! use unwrapPrevious() to prevent getting previous values. If we don't have the lyrics for the current song yet, we want to show the loading state, and not the lyrics for the previous track - final isSynchronizedLyrics = metadata.valueOrNull?.lyrics?.lyrics?.first.start != null; + final isSynchronizedLyrics = + metadata.valueOrNull?.lyrics?.lyrics?.first.start != null; Widget getEmptyState({ required String message, required IconData icon, }) { return Center( - child: LayoutBuilder( - builder: (context, constraints) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - ConstrainedBox( + child: LayoutBuilder(builder: (context, constraints) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ConstrainedBox( constraints: BoxConstraints( maxHeight: constraints.maxHeight - 180, ), - child: const PlayerScreenAlbumImage() - ), - const SizedBox(height: 24), - Icon( - icon, - size: 32, + child: const PlayerScreenAlbumImage()), + const SizedBox(height: 24), + Icon( + icon, + size: 32, + color: Theme.of(context).textTheme.headlineMedium!.color, + ), + const SizedBox(height: 12), + Text( + message, + style: TextStyle( color: Theme.of(context).textTheme.headlineMedium!.color, + fontSize: 16, ), - const SizedBox(height: 12), - Text( - message, - style: TextStyle( - color: Theme.of(context).textTheme.headlineMedium!.color, - fontSize: 16, - ), - ), - ], - ); - } - ), + ), + ], + ); + }), ); } @@ -263,7 +261,10 @@ class _LyricsViewState extends ConsumerState with WidgetsBindingObse message: "Loading lyrics...", icon: TablerIcons.microphone_2, ); - } else if (metadata.value == null || metadata.value!.hasLyrics && metadata.value!.lyrics == null && !metadata.isLoading) { + } else if (metadata.value == null || + metadata.value!.hasLyrics && + metadata.value!.lyrics == null && + !metadata.isLoading) { return getEmptyState( message: "Couldn't load lyrics!", icon: TablerIcons.microphone_2_off, @@ -274,9 +275,9 @@ class _LyricsViewState extends ConsumerState with WidgetsBindingObse icon: TablerIcons.microphone_2_off, ); } else { - progressStateStreamSubscription?.cancel(); - progressStateStreamSubscription = progressStateStream.listen((state) async { + progressStateStreamSubscription = + progressStateStream.listen((state) async { currentPosition = state.position; if (!isSynchronizedLyrics || !_isInForeground) { @@ -288,7 +289,8 @@ class _LyricsViewState extends ConsumerState with WidgetsBindingObse for (int i = 0; i < metadata.value!.lyrics!.lyrics!.length; i++) { closestLineIndex = i; final line = metadata.value!.lyrics!.lyrics![i]; - if ((line.start ?? 0) ~/ 10 > (currentPosition?.inMicroseconds ?? 0)) { + if ((line.start ?? 0) ~/ 10 > + (currentPosition?.inMicroseconds ?? 0)) { closestLineIndex = i - 1; break; } @@ -311,121 +313,127 @@ class _LyricsViewState extends ConsumerState with WidgetsBindingObse ); } else { unawaited(autoScrollController.scrollToIndex( - clampedIndex.clamp(0, metadata.value!.lyrics!.lyrics!.length - 1), + clampedIndex.clamp( + 0, metadata.value!.lyrics!.lyrics!.length - 1), preferPosition: AutoScrollPosition.middle, duration: const Duration(milliseconds: 500), )); } - } previousLineIndex = currentLineIndex; } }); - return LayoutBuilder( - builder: (context, constraints) { - - return Padding( - padding: const EdgeInsets.only(left: 20.0, right: 12.0), - child: Stack( - children: [ - Listener( - onPointerMove: (event) { - if (isSynchronizedLyrics && event.delta.dy.abs() > 2 * event.delta.dx.abs()) { - setState(() { - isAutoScrollEnabled = false; - }); - } - }, - child: LyricsListMask( - child: ListView.builder( - controller: autoScrollController, - itemCount: metadata.value!.lyrics!.lyrics?.length ?? 0, - itemBuilder: (context, index) { - final line = metadata.value!.lyrics!.lyrics![index]; - final nextLine = index < metadata.value!.lyrics!.lyrics!.length - 1 ? metadata.value!.lyrics!.lyrics![index + 1] : null; - - final isCurrentLine = (currentPosition?.inMicroseconds ?? 0) >= (line.start ?? 0) ~/ 10 && - (nextLine == null || (currentPosition?.inMicroseconds ?? 0) < (nextLine.start ?? 0) ~/ 10 ); - - return Column( - children: [ - if (index == 0) - AutoScrollTag( - key: const ValueKey(-1), - controller: autoScrollController, - index: -1, - child: SizedBox( - height: constraints.maxHeight * 0.65, - child: Center( - child: SizedBox( - height: constraints.maxHeight * 0.55, - child: const PlayerScreenAlbumImage() - ) - ), - ), - ), + return LayoutBuilder(builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.only(left: 20.0, right: 12.0), + child: Stack( + children: [ + Listener( + onPointerMove: (event) { + if (isSynchronizedLyrics && + event.delta.dy.abs() > 2 * event.delta.dx.abs()) { + setState(() { + isAutoScrollEnabled = false; + }); + } + }, + child: LyricsListMask( + child: ListView.builder( + controller: autoScrollController, + itemCount: metadata.value!.lyrics!.lyrics?.length ?? 0, + itemBuilder: (context, index) { + final line = metadata.value!.lyrics!.lyrics![index]; + final nextLine = + index < metadata.value!.lyrics!.lyrics!.length - 1 + ? metadata.value!.lyrics!.lyrics![index + 1] + : null; + + final isCurrentLine = + (currentPosition?.inMicroseconds ?? 0) >= + (line.start ?? 0) ~/ 10 && + (nextLine == null || + (currentPosition?.inMicroseconds ?? 0) < + (nextLine.start ?? 0) ~/ 10); + + return Column( + children: [ + if (index == 0) AutoScrollTag( - key: ValueKey(index), + key: const ValueKey(-1), controller: autoScrollController, - index: index, - child: _LyricLine( + index: -1, + child: SizedBox( + height: constraints.maxHeight * 0.65, + child: Center( + child: SizedBox( + height: constraints.maxHeight * 0.55, + child: const PlayerScreenAlbumImage())), + ), + ), + AutoScrollTag( + key: ValueKey(index), + controller: autoScrollController, + index: index, + child: _LyricLine( line: line, isCurrentLine: isCurrentLine, onTap: () async { // Seek to the start of the line - await _audioHandler.seek(Duration(microseconds: (line.start ?? 0) ~/ 10)); + await _audioHandler.seek(Duration( + microseconds: (line.start ?? 0) ~/ 10)); setState(() { isAutoScrollEnabled = true; }); if (previousLineIndex != null) { - unawaited(autoScrollController.scrollToIndex( + unawaited( + autoScrollController.scrollToIndex( previousLineIndex!, preferPosition: AutoScrollPosition.middle, - duration: const Duration(milliseconds: 500), + duration: + const Duration(milliseconds: 500), )); } - } - ), - ), - if (index == metadata.value!.lyrics!.lyrics!.length - 1) - SizedBox(height: constraints.maxHeight * 0.2), - ], - ); - }, - ), + }), + ), + if (index == + metadata.value!.lyrics!.lyrics!.length - 1) + SizedBox(height: constraints.maxHeight * 0.2), + ], + ); + }, ), ), - if (isSynchronizedLyrics) - Positioned( - bottom: 24, - right: 0, - child: EnableAutoScrollButton(autoScrollEnabled: isAutoScrollEnabled, onEnableAutoScroll: () { - setState(() { - isAutoScrollEnabled = true; - }); - if (previousLineIndex != null) { - unawaited(autoScrollController.scrollToIndex( - previousLineIndex!, - preferPosition: AutoScrollPosition.middle, - duration: const Duration(milliseconds: 500), - )); - } - FeedbackHelper.feedback(FeedbackType.impact); - }), - ), - ], - ), - ); - } - ); + ), + if (isSynchronizedLyrics) + Positioned( + bottom: 24, + right: 0, + child: EnableAutoScrollButton( + autoScrollEnabled: isAutoScrollEnabled, + onEnableAutoScroll: () { + setState(() { + isAutoScrollEnabled = true; + }); + if (previousLineIndex != null) { + unawaited(autoScrollController.scrollToIndex( + previousLineIndex!, + preferPosition: AutoScrollPosition.middle, + duration: const Duration(milliseconds: 500), + )); + } + FeedbackHelper.feedback(FeedbackType.impact); + }), + ), + ], + ), + ); + }); } - } } class _LyricLine extends StatelessWidget { - final LyricLine line; final bool isCurrentLine; final VoidCallback? onTap; @@ -438,10 +446,9 @@ class _LyricLine extends StatelessWidget { @override Widget build(BuildContext context) { - final lowlightLine = !isCurrentLine && line.start != null; final isSynchronized = line.start != null; - + return GestureDetector( onTap: isSynchronized ? onTap : null, child: Padding( @@ -453,7 +460,9 @@ class _LyricLine extends StatelessWidget { Text( "${Duration(microseconds: (line.start ?? 0) ~/ 10).inMinutes}:${(Duration(microseconds: (line.start ?? 0) ~/ 10).inSeconds % 60).toString().padLeft(2, '0')}", style: TextStyle( - color: lowlightLine ? Colors.grey : Theme.of(context).textTheme.bodyLarge!.color, + color: lowlightLine + ? Colors.grey + : Theme.of(context).textTheme.bodyLarge!.color, fontSize: 16, height: 1.75, ), @@ -465,9 +474,15 @@ class _LyricLine extends StatelessWidget { line.text ?? "", textAlign: TextAlign.start, style: TextStyle( - color: lowlightLine ? Colors.grey : Theme.of(context).textTheme.bodyLarge!.color, - fontWeight: lowlightLine || !isSynchronized ? FontWeight.normal : FontWeight.w500, - letterSpacing: lowlightLine || !isSynchronized ? 0.05 : -0.045, // keep text width consistent across the different weights + color: lowlightLine + ? Colors.grey + : Theme.of(context).textTheme.bodyLarge!.color, + fontWeight: lowlightLine || !isSynchronized + ? FontWeight.normal + : FontWeight.w500, + letterSpacing: lowlightLine || !isSynchronized + ? 0.05 + : -0.045, // keep text width consistent across the different weights fontSize: isSynchronized ? 26 : 20, height: 1.25, ), @@ -517,15 +532,14 @@ class LyricsListMask extends StatelessWidget { child: child, ); } - } class EnableAutoScrollButton extends StatelessWidget { - final bool autoScrollEnabled; final VoidCallback? onEnableAutoScroll; - const EnableAutoScrollButton({super.key, required this.autoScrollEnabled, this.onEnableAutoScroll}); + const EnableAutoScrollButton( + {super.key, required this.autoScrollEnabled, this.onEnableAutoScroll}); @override Widget build(BuildContext context) { @@ -551,7 +565,7 @@ class EnableAutoScrollButton extends StatelessWidget { fontWeight: FontWeight.w500, ), ), - ) + ) : const SizedBox.shrink(); } } diff --git a/lib/screens/music_screen.dart b/lib/screens/music_screen.dart index b23525638..7bb0d0a81 100644 --- a/lib/screens/music_screen.dart +++ b/lib/screens/music_screen.dart @@ -97,8 +97,8 @@ class _MusicScreenState extends ConsumerState super.dispose(); } - FloatingActionButton? getFloatingActionButton(List sortedTabs) { - + FloatingActionButton? getFloatingActionButton( + List sortedTabs) { // Show the floating action button only on the albums, artists, generes and songs tab. if (_tabController!.index == sortedTabs.indexOf(TabContentType.songs)) { return FloatingActionButton( diff --git a/lib/screens/player_screen.dart b/lib/screens/player_screen.dart index 4cfabf353..2f9326802 100644 --- a/lib/screens/player_screen.dart +++ b/lib/screens/player_screen.dart @@ -89,7 +89,9 @@ class _PlayerScreenContent extends ConsumerWidget { final metadata = ref.watch(currentTrackMetadataProvider).unwrapPrevious(); final isLyricsLoading = metadata.isLoading || metadata.isRefreshing; - final isLyricsAvailable = (metadata.valueOrNull?.hasLyrics ?? false) && (metadata.valueOrNull?.lyrics != null || metadata.isLoading) && !metadata.hasError; + final isLyricsAvailable = (metadata.valueOrNull?.hasLyrics ?? false) && + (metadata.valueOrNull?.lyrics != null || metadata.isLoading) && + !metadata.hasError; return SimpleGestureDetector( onVerticalSwipe: (direction) { @@ -107,9 +109,11 @@ class _PlayerScreenContent extends ConsumerWidget { }, onHorizontalSwipe: (direction) { if (direction == SwipeDirection.left && isLyricsAvailable) { - if (!FinampSettingsHelper - .finampSettings.disableGesture) { - Navigator.of(context).push(_buildSlideRouteTransition(this, const LyricsScreen(), routeSettings: const RouteSettings(name: LyricsScreen.routeName))); + if (!FinampSettingsHelper.finampSettings.disableGesture) { + Navigator.of(context).push(_buildSlideRouteTransition( + this, const LyricsScreen(), + routeSettings: + const RouteSettings(name: LyricsScreen.routeName))); } } }, @@ -117,7 +121,8 @@ class _PlayerScreenContent extends ConsumerWidget { appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, - scrolledUnderElevation: 0.0, // disable tint/shadow when content is scrolled under the app bar + scrolledUnderElevation: + 0.0, // disable tint/shadow when content is scrolled under the app bar centerTitle: true, toolbarHeight: toolbarHeight, title: PlayerScreenAppBarTitle( @@ -204,7 +209,9 @@ class _PlayerScreenContent extends ConsumerWidget { SongNameContent(controller), ControlArea(controller), if (controller.shouldShow(PlayerHideable.queueButton)) - _buildBottomActions(context, controller, + _buildBottomActions( + context, + controller, isLyricsLoading: isLyricsLoading, isLyricsAvailable: isLyricsAvailable, ), @@ -223,7 +230,9 @@ class _PlayerScreenContent extends ConsumerWidget { ); } - PageRouteBuilder _buildSlideRouteTransition(Widget sourceWidget, Widget targetWidget, { + PageRouteBuilder _buildSlideRouteTransition( + Widget sourceWidget, + Widget targetWidget, { RouteSettings? routeSettings, }) { return PageRouteBuilder( @@ -259,11 +268,12 @@ class _PlayerScreenContent extends ConsumerWidget { ); } - Widget _buildBottomActions(BuildContext context, PlayerHideableController controller, { + Widget _buildBottomActions( + BuildContext context, + PlayerHideableController controller, { bool isLyricsLoading = true, bool isLyricsAvailable = false, }) { - IconData getLyricsIcon() { if (!isLyricsLoading && !isLyricsAvailable) { return TablerIcons.microphone_2_off; @@ -275,36 +285,39 @@ class _PlayerScreenContent extends ConsumerWidget { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Spacer(flex: 1,), + const Spacer( + flex: 1, + ), Expanded( - flex: 16, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SizedBox( - width: 80, - // child: Text("Output") - child: SizedBox.shrink(), - ), - const SizedBox( - width: 80, - child: QueueButton() - ), - SizedBox( - width: 80, - child: SimpleButton( - inactive: !isLyricsAvailable, - text: "Lyrics", - icon: getLyricsIcon(), - onPressed: () { - Navigator.of(context).push(_buildSlideRouteTransition(this, const LyricsScreen(), routeSettings: const RouteSettings(name: LyricsScreen.routeName))); - }, + flex: 16, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox( + width: 80, + // child: Text("Output") + child: SizedBox.shrink(), ), - ), - ], - ) + const SizedBox(width: 80, child: QueueButton()), + SizedBox( + width: 80, + child: SimpleButton( + inactive: !isLyricsAvailable, + text: "Lyrics", + icon: getLyricsIcon(), + onPressed: () { + Navigator.of(context).push(_buildSlideRouteTransition( + this, const LyricsScreen(), + routeSettings: const RouteSettings( + name: LyricsScreen.routeName))); + }, + ), + ), + ], + )), + const Spacer( + flex: 1, ), - const Spacer(flex: 1,), ], ); } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 1e8465fae..c88d37b28 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -66,8 +66,8 @@ class SettingsScreen extends StatelessWidget { ), ListTile( leading: const Icon(Icons.equalizer_rounded), - title: - Text(AppLocalizations.of(context)!.volumeNormalizationSettingsTitle), + title: Text(AppLocalizations.of(context)! + .volumeNormalizationSettingsTitle), onTap: () => Navigator.of(context) .pushNamed(VolumeNormalizationSettingsScreen.routeName), ), diff --git a/lib/screens/tabs_settings_screen.dart b/lib/screens/tabs_settings_screen.dart index a0bb1d0b1..7787588de 100644 --- a/lib/screens/tabs_settings_screen.dart +++ b/lib/screens/tabs_settings_screen.dart @@ -55,14 +55,14 @@ class _TabsSettingsScreenState extends State { newIndex -= 1; } - var currentTabOrder = FinampSettingsHelper.finampSettings.tabOrder; + var currentTabOrder = + FinampSettingsHelper.finampSettings.tabOrder; // move all values below newIndex down by one final oldTab = currentTabOrder[oldIndex]; currentTabOrder.removeAt(oldIndex); currentTabOrder.insert(newIndex, oldTab); FinampSettingsHelper.setTabOrder(currentTabOrder); - }); }, ), diff --git a/lib/screens/volume_normalization_settings_screen.dart b/lib/screens/volume_normalization_settings_screen.dart index 552491b55..badc0148c 100644 --- a/lib/screens/volume_normalization_settings_screen.dart +++ b/lib/screens/volume_normalization_settings_screen.dart @@ -16,7 +16,8 @@ class VolumeNormalizationSettingsScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.volumeNormalizationSettingsTitle), + title: Text( + AppLocalizations.of(context)!.volumeNormalizationSettingsTitle), ), body: Scrollbar( child: ListView( diff --git a/lib/services/album_image_provider.dart b/lib/services/album_image_provider.dart index 60b79cfa0..4f820a405 100644 --- a/lib/services/album_image_provider.dart +++ b/lib/services/album_image_provider.dart @@ -50,7 +50,8 @@ final AutoDisposeFutureProviderFamily try { downloadedImage = await isardownloader.getImageDownload(item: request.item); } catch (e) { - albumImageProviderLogger.warning("Couldn't get the offline image for track '${request.item.name}' because it's missing a blurhash"); + albumImageProviderLogger.warning( + "Couldn't get the offline image for track '${request.item.name}' because it's missing a blurhash"); } if (downloadedImage?.file == null) { diff --git a/lib/services/censored_log.dart b/lib/services/censored_log.dart index 639b2cc0e..c95d65870 100644 --- a/lib/services/censored_log.dart +++ b/lib/services/censored_log.dart @@ -26,15 +26,17 @@ extension CensoredMessage on LogRecord { if (jellyfinApiHelper.baseUrlTemp != null) { // Replace the temporary base URL with BASEURL final tempUriMatcher = jellyfinApiHelper.baseUrlTemp!; - tempUriMatcher.replace(scheme: ""); // don't replace the scheme, it might be important for diagnosing issues + tempUriMatcher.replace( + scheme: + ""); // don't replace the scheme, it might be important for diagnosing issues workingLogString = workingLogString.replaceAll( CaseInsensitivePattern(tempUriMatcher.toString()), "TEMP_BASEURL"); // remove anything between the quotes in "Failed host lookup: ''" workingLogString = workingLogString.replaceAll( - CaseInsensitivePattern(tempUriMatcher.host.toString()), "TEMP_HOST"); + CaseInsensitivePattern(tempUriMatcher.host.toString()), + "TEMP_HOST"); } - } // If userHelper is not initialized, calling code cannot have used baseurl/token diff --git a/lib/services/current_album_image_provider.dart b/lib/services/current_album_image_provider.dart index f72252779..aebc2c571 100644 --- a/lib/services/current_album_image_provider.dart +++ b/lib/services/current_album_image_provider.dart @@ -17,7 +17,7 @@ final currentAlbumImageProvider = FutureProvider((ref) async { ImageStreamListener? listener; // Set up onDispose function before crossing async boundary ref.onDispose(() { - if(stream!=null&&listener!=null){ + if (stream != null && listener != null) { stream?.removeListener(listener!); } }); diff --git a/lib/services/current_track_metadata_provider.dart b/lib/services/current_track_metadata_provider.dart index a7ef59fbe..691297eac 100644 --- a/lib/services/current_track_metadata_provider.dart +++ b/lib/services/current_track_metadata_provider.dart @@ -18,7 +18,13 @@ final currentTrackMetadataProvider = BaseItemDto? base = itemToPrecache.baseItem; if (base != null) { // only fetch lyrics for the current track - final request = MetadataRequest(item: base, queueItem: itemToPrecache, includeLyrics: true, checkIfSpeedControlNeeded: FinampSettingsHelper.finampSettings.playbackSpeedVisibility == PlaybackSpeedVisibility.automatic); + final request = MetadataRequest( + item: base, + queueItem: itemToPrecache, + includeLyrics: true, + checkIfSpeedControlNeeded: + FinampSettingsHelper.finampSettings.playbackSpeedVisibility == + PlaybackSpeedVisibility.automatic); unawaited(ref.watch(metadataProvider(request).future)); } } @@ -29,12 +35,14 @@ final currentTrackMetadataProvider = item: currentTrack!.baseItem!, queueItem: currentTrack, includeLyrics: true, - checkIfSpeedControlNeeded: FinampSettingsHelper.finampSettings.playbackSpeedVisibility == PlaybackSpeedVisibility.automatic, + checkIfSpeedControlNeeded: + FinampSettingsHelper.finampSettings.playbackSpeedVisibility == + PlaybackSpeedVisibility.automatic, ); return ref.watch(metadataProvider(request)); } return const AsyncValue.data(null); }); - + final currentSongProvider = StreamProvider( (_) => GetIt.instance().getCurrentTrackStream()); diff --git a/lib/services/downloads_service.dart b/lib/services/downloads_service.dart index 9093ad70c..3ea35a2e6 100644 --- a/lib/services/downloads_service.dart +++ b/lib/services/downloads_service.dart @@ -688,16 +688,23 @@ class DownloadsService { downloadCounts[repairStepTrackingName] = 4; final Map idsWithLyrics = HashMap(); var allItems = _isar.downloadItems - .where() - .stateNotEqualTo(DownloadItemState.notDownloaded) - .filter() - .typeEqualTo(DownloadItemType.song) - .findAllSync(); - final JellyfinApiHelper _jellyfinApiData = GetIt.instance(); + .where() + .stateNotEqualTo(DownloadItemState.notDownloaded) + .filter() + .typeEqualTo(DownloadItemType.song) + .findAllSync(); + final JellyfinApiHelper _jellyfinApiData = + GetIt.instance(); for (var item in allItems) { - if (item.baseItem?.mediaStreams?.any((stream) => stream.type == "Lyric") ?? false) { + if (item.baseItem?.mediaStreams + ?.any((stream) => stream.type == "Lyric") ?? + false) { // check if lyrics are already downloaded - if (_isar.downloadedLyrics.where().isarIdEqualTo(item.isarId).countSync() > 0) { + if (_isar.downloadedLyrics + .where() + .isarIdEqualTo(item.isarId) + .countSync() > + 0) { continue; } idsWithLyrics[item.isarId] = null; @@ -715,7 +722,8 @@ class DownloadsService { for (var id in idsWithLyrics.keys) { var canonItem = _isar.downloadItems.getSync(id); if (canonItem != null && idsWithLyrics[id] != null) { - final lyricsItem = DownloadedLyrics.fromItem(isarId: canonItem.isarId, item: idsWithLyrics[id]!); + final lyricsItem = DownloadedLyrics.fromItem( + isarId: canonItem.isarId, item: idsWithLyrics[id]!); _isar.downloadedLyrics.putSync(lyricsItem); } } @@ -1371,11 +1379,12 @@ class DownloadsService { return _getDownloadByID( blurHash ?? item!.blurHash ?? item!.imageId!, DownloadItemType.image); } - + /// Get DownloadedLyrics by the corresponding track's BaseItemDto. Future getLyricsDownload( {required BaseItemDto baseItem}) async { - var item = _isar.downloadedLyrics.getSync(DownloadStub.getHash(baseItem.id, DownloadItemType.song)); + var item = _isar.downloadedLyrics + .getSync(DownloadStub.getHash(baseItem.id, DownloadItemType.song)); return item; } diff --git a/lib/services/downloads_service_backend.dart b/lib/services/downloads_service_backend.dart index 5365774b4..94feb80c0 100644 --- a/lib/services/downloads_service_backend.dart +++ b/lib/services/downloads_service_backend.dart @@ -691,7 +691,7 @@ class DownloadsDeleteService { _downloadsService.updateItemState( transactionItem, DownloadItemState.notDownloaded); } - + if (item.type == DownloadItemType.song) { // delete corresponding lyrics if they exist, using the same ID if (_isar.downloadedLyrics.deleteSync(item.isarId)) { @@ -1011,11 +1011,17 @@ class DownloadsSyncService { } } - final requiredAttributes = [parent.baseItem?.sortName, parent.baseItem?.childCount, parent.baseItem?.mediaSources, parent.baseItem?.mediaStreams]; + final requiredAttributes = [ + parent.baseItem?.sortName, + parent.baseItem?.childCount, + parent.baseItem?.mediaSources, + parent.baseItem?.mediaStreams + ]; //If we aren't quicksyncing, fetch the latest BaseItemDto to copy into Isar if (parent.type.requiresItem && (!FinampSettingsHelper.finampSettings.preferQuickSyncs || - _downloadsService.forceFullSync || requiredAttributes.any((element) => element == null) )) { + _downloadsService.forceFullSync || + requiredAttributes.any((element) => element == null))) { newBaseItem ??= (await _getCollectionInfo(parent.baseItem!.id, parent.type, true)) ?.baseItem; @@ -1219,8 +1225,8 @@ class DownloadsSyncService { } _metadataCache[id] = itemFetch.future; item = await _jellyfinApiData - .getItemByIdBatched( - id, "${_jellyfinApiData.defaultFields},childCount,sortName,MediaSources,MediaStreams") + .getItemByIdBatched(id, + "${_jellyfinApiData.defaultFields},childCount,sortName,MediaSources,MediaStreams") .then((value) => value == null ? null : DownloadStub.fromItem(item: value, type: type)); @@ -1252,7 +1258,8 @@ class DownloadsSyncService { case BaseItemDtoType.playlist || BaseItemDtoType.album: childType = DownloadItemType.song; childFilter = BaseItemDtoType.song; - fields = "${_jellyfinApiData.defaultFields},MediaSources,MediaStreams,SortName"; + fields = + "${_jellyfinApiData.defaultFields},MediaSources,MediaStreams,SortName"; sortOrder = "ParentIndexNumber,IndexNumber,SortName"; case BaseItemDtoType.artist || BaseItemDtoType.genre || @@ -1492,7 +1499,8 @@ class DownloadsSyncService { _downloadsService.updateItemState(canonItem, DownloadItemState.enqueued, alwaysPut: true); if (lyrics != null) { - final lyricsItem = DownloadedLyrics.fromItem(isarId: canonItem.isarId, item: lyrics); + final lyricsItem = + DownloadedLyrics.fromItem(isarId: canonItem.isarId, item: lyrics); _isar.downloadedLyrics.putSync(lyricsItem); } } diff --git a/lib/services/feedback_helper.dart b/lib/services/feedback_helper.dart index 9b7f0996f..8475c3536 100644 --- a/lib/services/feedback_helper.dart +++ b/lib/services/feedback_helper.dart @@ -7,4 +7,4 @@ class FeedbackHelper { Vibrate.feedback(feedbackType); } } -} \ No newline at end of file +} diff --git a/lib/services/finamp_settings_helper.dart b/lib/services/finamp_settings_helper.dart index f95268158..b516961cc 100644 --- a/lib/services/finamp_settings_helper.dart +++ b/lib/services/finamp_settings_helper.dart @@ -129,14 +129,17 @@ class FinampSettingsHelper { .put("FinampSettings", finampSettingsTemp); } - static void setVolumeNormalizationIOSBaseGain(double volumeNormalizationIOSBaseGain) { + static void setVolumeNormalizationIOSBaseGain( + double volumeNormalizationIOSBaseGain) { FinampSettings finampSettingsTemp = finampSettings; - finampSettingsTemp.volumeNormalizationIOSBaseGain = volumeNormalizationIOSBaseGain; + finampSettingsTemp.volumeNormalizationIOSBaseGain = + volumeNormalizationIOSBaseGain; Hive.box("FinampSettings") .put("FinampSettings", finampSettingsTemp); } - static void setVolumeNormalizationMode(VolumeNormalizationMode volumeNormalizationMode) { + static void setVolumeNormalizationMode( + VolumeNormalizationMode volumeNormalizationMode) { FinampSettings finampSettingsTemp = finampSettings; finampSettingsTemp.volumeNormalizationMode = volumeNormalizationMode; Hive.box("FinampSettings") @@ -197,13 +200,13 @@ class FinampSettingsHelper { static void setUseCoverAsBackground(bool useCoverAsBackground) { FinampSettings finampSettingsTemp = finampSettings; - finampSettingsTemp.useCoverAsBackground = - useCoverAsBackground; + finampSettingsTemp.useCoverAsBackground = useCoverAsBackground; Hive.box("FinampSettings") .put("FinampSettings", finampSettingsTemp); } - static void setPlayerScreenCoverMinimumPadding(double playerScreenCoverMinimumPadding) { + static void setPlayerScreenCoverMinimumPadding( + double playerScreenCoverMinimumPadding) { FinampSettings finampSettingsTemp = finampSettings; finampSettingsTemp.playerScreenCoverMinimumPadding = playerScreenCoverMinimumPadding; @@ -346,9 +349,11 @@ class FinampSettingsHelper { .put("FinampSettings", finampSettingsTemp); } - static void setPeriodicPlaybackSessionUpdateFrequencySeconds(int periodicPlaybackSessionUpdateFrequencySeconds) { + static void setPeriodicPlaybackSessionUpdateFrequencySeconds( + int periodicPlaybackSessionUpdateFrequencySeconds) { FinampSettings finampSettingsTemp = finampSettings; - finampSettingsTemp.periodicPlaybackSessionUpdateFrequencySeconds = periodicPlaybackSessionUpdateFrequencySeconds; + finampSettingsTemp.periodicPlaybackSessionUpdateFrequencySeconds = + periodicPlaybackSessionUpdateFrequencySeconds; Hive.box("FinampSettings") .put("FinampSettings", finampSettingsTemp); } diff --git a/lib/services/generate_subtitle.dart b/lib/services/generate_subtitle.dart index f3763fa36..451484e23 100644 --- a/lib/services/generate_subtitle.dart +++ b/lib/services/generate_subtitle.dart @@ -7,7 +7,6 @@ import 'process_artist.dart'; /// Creates the subtitle text used on AlbumItemListTile and AlbumItemCard String? generateSubtitle( BaseItemDto item, String? parentType, BuildContext context) { - // If the parentType is MusicArtist, this is being called by an AlbumListTile in an AlbumView of an artist. if (parentType == "MusicArtist") { return item.productionYearString; @@ -15,7 +14,14 @@ String? generateSubtitle( switch (item.type) { case "MusicAlbum": - return item.albumArtists != null && item.albumArtists!.isNotEmpty && (item.albumArtists!.length > 1 || item.albumArtists?.first.name != item.albumArtist) ? item.albumArtists?.map((e) => processArtist(e.name, context)).join(", ") : processArtist(item.albumArtist, context); + return item.albumArtists != null && + item.albumArtists!.isNotEmpty && + (item.albumArtists!.length > 1 || + item.albumArtists?.first.name != item.albumArtist) + ? item.albumArtists + ?.map((e) => processArtist(e.name, context)) + .join(", ") + : processArtist(item.albumArtist, context); case "Playlist": return AppLocalizations.of(context)!.songCount(item.childCount!); // case "MusicGenre": diff --git a/lib/services/jellyfin_api.dart b/lib/services/jellyfin_api.dart index 44242dc1d..292173ba2 100644 --- a/lib/services/jellyfin_api.dart +++ b/lib/services/jellyfin_api.dart @@ -120,7 +120,6 @@ abstract class JellyfinApi extends ChopperService { /// Optional. If specified, results will be filtered to include only those /// containing the specified genre id. @Query("GenreIds") String? genreIds, - @Query("ids") String? ids, /// When searching within folders, this determines whether or not the search @@ -461,7 +460,8 @@ abstract class JellyfinApi extends ChopperService { final client = ChopperClient( client: http.IOClient(HttpClient() ..connectionTimeout = const Duration( - seconds: 8) // if we don't get a response by then, it's probably not worth it to wait any longer. this prevents the server connection test from taking too long + seconds: + 8) // if we don't get a response by then, it's probably not worth it to wait any longer. this prevents the server connection test from taking too long ), // The first part of the URL is now here services: [ diff --git a/lib/services/jellyfin_api_helper.dart b/lib/services/jellyfin_api_helper.dart index e90d642dd..d8e2e8456 100644 --- a/lib/services/jellyfin_api_helper.dart +++ b/lib/services/jellyfin_api_helper.dart @@ -772,7 +772,8 @@ class JellyfinApiHelper { // Jellyfin). Once https://github.com/jellyfin/jellyfin/pull/9192 lands, // we could use M4A/AAC. - assert(transcodingProfile.codec.container != null, "Missing container for codec while trying to download transcoded track!"); + assert(transcodingProfile.codec.container != null, + "Missing container for codec while trying to download transcoded track!"); queryParameters.addAll({ "transcodingContainer": transcodingProfile.codec.container!, diff --git a/lib/services/metadata_provider.dart b/lib/services/metadata_provider.dart index 05e74fc49..1e592e204 100644 --- a/lib/services/metadata_provider.dart +++ b/lib/services/metadata_provider.dart @@ -36,11 +36,11 @@ class MetadataRequest { } @override - int get hashCode => Object.hash(item.id, queueItem?.id, includeLyrics, checkIfSpeedControlNeeded); + int get hashCode => Object.hash( + item.id, queueItem?.id, includeLyrics, checkIfSpeedControlNeeded); } class MetadataProvider { - static const speedControlGenres = ["audiobook", "podcast", "speech"]; static const speedControlLongTrackDuration = Duration(minutes: 15); static const speedControlLongAlbumDuration = Duration(hours: 3); @@ -57,30 +57,37 @@ class MetadataProvider { this.qualifiesForPlaybackSpeedControl = false, }); - bool get hasLyrics => mediaSourceInfo.mediaStreams.any((e) => e.type == "Lyric"); - + bool get hasLyrics => + mediaSourceInfo.mediaStreams.any((e) => e.type == "Lyric"); } final AutoDisposeFutureProviderFamily - metadataProvider = FutureProvider.autoDispose.family((ref, request) async { - - unawaited(ref.watch(FinampSettingsHelper.finampSettingsProvider.selectAsync((settings) => settings?.isOffline))); // watch settings to trigger re-fetching metadata when offline mode changes + metadataProvider = FutureProvider.autoDispose + .family((ref, request) async { + unawaited(ref.watch(FinampSettingsHelper.finampSettingsProvider.selectAsync( + (settings) => settings + ?.isOffline))); // watch settings to trigger re-fetching metadata when offline mode changes final jellyfinApiHelper = GetIt.instance(); final downloadsService = GetIt.instance(); - metadataProviderLogger.fine("Fetching metadata for '${request.item.name}' (${request.item.id})"); + metadataProviderLogger.fine( + "Fetching metadata for '${request.item.name}' (${request.item.id})"); MediaSourceInfo? playbackInfo; MediaSourceInfo? localPlaybackInfo; - + final downloadStub = await downloadsService.getSongInfo(id: request.item.id); if (downloadStub != null) { - final downloadItem = await ref.watch(downloadsService.itemProvider(downloadStub).future); + final downloadItem = + await ref.watch(downloadsService.itemProvider(downloadStub).future); if (downloadItem != null && downloadItem.state.isComplete) { - metadataProviderLogger.fine("Got offline metadata for '${request.item.name}'"); + metadataProviderLogger + .fine("Got offline metadata for '${request.item.name}'"); var profile = downloadItem.fileTranscodingProfile; - var codec = profile?.codec == FinampTranscodingCodec.original ? downloadItem.baseItem?.mediaSources?.first.container : profile?.codec.name ?? FinampTranscodingCodec.original.name; + var codec = profile?.codec == FinampTranscodingCodec.original + ? downloadItem.baseItem?.mediaSources?.first.container + : profile?.codec.name ?? FinampTranscodingCodec.original.name; localPlaybackInfo = MediaSourceInfo( id: downloadItem.baseItem!.id, protocol: "File", @@ -95,13 +102,14 @@ final AutoDisposeFutureProviderFamily requiresLooping: false, supportsProbing: false, mediaStreams: downloadItem.baseItem!.mediaStreams?.map((mediaStream) { - // if we transcoded the file, we need to update the original bitrate - // the bitrate of the MediaSource(Info) includes all streams, so we prefer the media stream bitrate and hence update it here - if (mediaStream.type == "Audio") { - mediaStream.bitRate = profile?.stereoBitrate; - } - return mediaStream; - }).toList() ?? [], + // if we transcoded the file, we need to update the original bitrate + // the bitrate of the MediaSource(Info) includes all streams, so we prefer the media stream bitrate and hence update it here + if (mediaStream.type == "Audio") { + mediaStream.bitRate = profile?.stereoBitrate; + } + return mediaStream; + }).toList() ?? + [], readAtNativeFramerate: false, ignoreDts: false, ignoreIndex: false, @@ -113,18 +121,22 @@ final AutoDisposeFutureProviderFamily ); } } - + //!!! only use offline metadata if the app is in offline mode // Finamp should always use the server metadata when online, if possible if (FinampSettingsHelper.finampSettings.isOffline) { playbackInfo = localPlaybackInfo; } else { // fetch from server in online mode - metadataProviderLogger.fine("Fetching metadata for '${request.item.name}' (${request.item.id}) from server due to missing attributes"); + metadataProviderLogger.fine( + "Fetching metadata for '${request.item.name}' (${request.item.id}) from server due to missing attributes"); try { - playbackInfo = (await jellyfinApiHelper.getPlaybackInfo(request.item.id))?.first; + playbackInfo = + (await jellyfinApiHelper.getPlaybackInfo(request.item.id))?.first; } catch (e) { - metadataProviderLogger.severe("Failed to fetch metadata for '${request.item.name}' (${request.item.id})", e); + metadataProviderLogger.severe( + "Failed to fetch metadata for '${request.item.name}' (${request.item.id})", + e); return null; } @@ -138,67 +150,79 @@ final AutoDisposeFutureProviderFamily } if (playbackInfo == null) { - metadataProviderLogger.warning("Couldn't load metadata for '${request.item.name}' (${request.item.id})"); + metadataProviderLogger.warning( + "Couldn't load metadata for '${request.item.name}' (${request.item.id})"); return null; } - final metadata = MetadataProvider(mediaSourceInfo: playbackInfo, isDownloaded: localPlaybackInfo != null); + final metadata = MetadataProvider( + mediaSourceInfo: playbackInfo, isDownloaded: localPlaybackInfo != null); // check if item qualifies for having playback speed control available if (request.checkIfSpeedControlNeeded) { - for (final genre in request.item.genres ?? []) { if (MetadataProvider.speedControlGenres.contains(genre.toLowerCase())) { metadata.qualifiesForPlaybackSpeedControl = true; break; } } - if (!metadata.qualifiesForPlaybackSpeedControl && metadata.mediaSourceInfo.runTimeTicks! > MetadataProvider.speedControlLongTrackDuration.inMicroseconds * 10) { + if (!metadata.qualifiesForPlaybackSpeedControl && + metadata.mediaSourceInfo.runTimeTicks! > + MetadataProvider.speedControlLongTrackDuration.inMicroseconds * + 10) { // we might want playback speed control for long tracks (like podcasts or audiobook chapters) metadata.qualifiesForPlaybackSpeedControl = true; } else { // check if "album" is long enough to qualify for playback speed control try { - final parent = await jellyfinApiHelper.getItemById(request.item.parentId!); - if (parent.runTimeTicks! > MetadataProvider.speedControlLongAlbumDuration.inMicroseconds * 10) { + final parent = + await jellyfinApiHelper.getItemById(request.item.parentId!); + if (parent.runTimeTicks! > + MetadataProvider.speedControlLongAlbumDuration.inMicroseconds * + 10) { metadata.qualifiesForPlaybackSpeedControl = true; } - } catch(e) { - metadataProviderLogger.warning("Failed to check if '${request.item.name}' (${request.item.id}) qualifies for playback speed controls", e); + } catch (e) { + metadataProviderLogger.warning( + "Failed to check if '${request.item.name}' (${request.item.id}) qualifies for playback speed controls", + e); } } - } if (request.includeLyrics && metadata.hasLyrics) { - //!!! only use offline metadata if the app is in offline mode // Finamp should always use the server metadata when online, if possible if (FinampSettingsHelper.finampSettings.isOffline) { DownloadedLyrics? downloadedLyrics; - downloadedLyrics = await downloadsService.getLyricsDownload(baseItem: request.item); - if (downloadedLyrics != null) { - metadata.lyrics = downloadedLyrics.lyricDto; - metadataProviderLogger.fine("Got offline lyrics for '${request.item.name}'"); - } - else { - metadataProviderLogger.fine("No offline lyrics for '${request.item.name}'"); - } + downloadedLyrics = + await downloadsService.getLyricsDownload(baseItem: request.item); + if (downloadedLyrics != null) { + metadata.lyrics = downloadedLyrics.lyricDto; + metadataProviderLogger + .fine("Got offline lyrics for '${request.item.name}'"); + } else { + metadataProviderLogger + .fine("No offline lyrics for '${request.item.name}'"); + } } else { - metadataProviderLogger.fine("Fetching lyrics for '${request.item.name}' (${request.item.id})"); + metadataProviderLogger.fine( + "Fetching lyrics for '${request.item.name}' (${request.item.id})"); try { final lyrics = await jellyfinApiHelper.getLyrics( itemId: request.item.id, ); metadata.lyrics = lyrics; } catch (e) { - metadataProviderLogger.warning("Failed to fetch lyrics for '${request.item.name}' (${request.item.id}). Metadata might be stale", e); + metadataProviderLogger.warning( + "Failed to fetch lyrics for '${request.item.name}' (${request.item.id}). Metadata might be stale", + e); } } } - metadataProviderLogger.fine("Fetched metadata for '${request.item.name}' (${request.item.id}): ${metadata.lyrics} ${metadata.hasLyrics}"); + metadataProviderLogger.fine( + "Fetched metadata for '${request.item.name}' (${request.item.id}): ${metadata.lyrics} ${metadata.hasLyrics}"); return metadata; - }); diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index d02972407..04fcf0e85 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -82,13 +82,15 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { audioPipeline: _audioPipeline, ); - _loudnessEnhancerEffect - ?.setEnabled(FinampSettingsHelper.finampSettings.volumeNormalizationActive); + _loudnessEnhancerEffect?.setEnabled( + FinampSettingsHelper.finampSettings.volumeNormalizationActive); _loudnessEnhancerEffect?.setTargetGain(0.0 / 10.0); //!!! always divide by 10, the just_audio implementation has a bug so it expects a value in Bel and not Decibel (remove once https://github.com/ryanheise/just_audio/pull/1092/commits/436b3274d0233818a061ecc1c0856a630329c4e6 is merged) // calculate base volume gain for iOS as a linear factor, because just_audio doesn't yet support AudioEffect on iOS - iosBaseVolumeGainFactor = pow(10.0, - FinampSettingsHelper.finampSettings.volumeNormalizationIOSBaseGain / 20.0) + iosBaseVolumeGainFactor = pow( + 10.0, + FinampSettingsHelper.finampSettings.volumeNormalizationIOSBaseGain / + 20.0) as double; // https://sound.stackexchange.com/questions/38722/convert-db-value-to-linear-scale if (!Platform.isAndroid) { _volumeNormalizationLogger.info( @@ -102,8 +104,11 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { FinampSettingsHelper.finampSettingsListener.addListener(() { // update replay gain settings every time settings are changed - iosBaseVolumeGainFactor = pow(10.0, - FinampSettingsHelper.finampSettings.volumeNormalizationIOSBaseGain / 20.0) + iosBaseVolumeGainFactor = pow( + 10.0, + FinampSettingsHelper + .finampSettings.volumeNormalizationIOSBaseGain / + 20.0) as double; // https://sound.stackexchange.com/questions/38722/convert-db-value-to-linear-scale if (FinampSettingsHelper.finampSettings.volumeNormalizationActive) { _loudnessEnhancerEffect?.setEnabled(true); @@ -414,13 +419,13 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { final baseItem = jellyfin_models.BaseItemDto.fromJson( currentTrack.extras?["itemJson"]); - double? effectiveGainChange = getEffectiveGainChange(currentTrack, baseItem); + double? effectiveGainChange = + getEffectiveGainChange(currentTrack, baseItem); _volumeNormalizationLogger.info( "normalization gain for '${baseItem.name}': $effectiveGainChange (track gain change: ${baseItem.normalizationGain})"); if (effectiveGainChange != null) { - _volumeNormalizationLogger.info( - "Gain change: $effectiveGainChange"); + _volumeNormalizationLogger.info("Gain change: $effectiveGainChange"); if (Platform.isAndroid) { _loudnessEnhancerEffect?.setTargetGain(effectiveGainChange / 10.0); //!!! always divide by 10, the just_audio implementation has a bug so it expects a value in Bel and not Decibel (remove once https://github.com/ryanheise/just_audio/pull/1092/commits/436b3274d0233818a061ecc1c0856a630329c4e6 is merged) @@ -524,15 +529,17 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { Duration get playbackPosition => _player.position; } -double? getEffectiveGainChange(MediaItem currentTrack, jellyfin_models.BaseItemDto? item) { - final baseItem = item ?? jellyfin_models.BaseItemDto.fromJson( - currentTrack.extras?["itemJson"]); +double? getEffectiveGainChange( + MediaItem currentTrack, jellyfin_models.BaseItemDto? item) { + final baseItem = item ?? + jellyfin_models.BaseItemDto.fromJson(currentTrack.extras?["itemJson"]); double? effectiveGainChange; switch (FinampSettingsHelper.finampSettings.volumeNormalizationMode) { case VolumeNormalizationMode.hybrid: - // case VolumeNormalizationMode.albumBased: // we use the context normalization gain for album-based because we don't have the album item here + // case VolumeNormalizationMode.albumBased: // we use the context normalization gain for album-based because we don't have the album item here // use context normalization gain if available, otherwise use track normalization gain - effectiveGainChange = currentTrack.extras?["contextNormalizationGain"] ?? baseItem.normalizationGain; + effectiveGainChange = currentTrack.extras?["contextNormalizationGain"] ?? + baseItem.normalizationGain; break; case VolumeNormalizationMode.trackBased: // only ever use track normalization gain diff --git a/lib/services/offline_listen_helper.dart b/lib/services/offline_listen_helper.dart index 551b84295..1bf0ebce3 100644 --- a/lib/services/offline_listen_helper.dart +++ b/lib/services/offline_listen_helper.dart @@ -82,12 +82,9 @@ class OfflineListenLogHelper { /// Share the offline listens log file Future shareOfflineListens() async { - final file = await _logFile; final xFile = XFile(file.path, mimeType: "application/json"); await Share.shareXFiles([xFile]); - } - } diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart index 67e79432b..a88d83db7 100644 --- a/lib/services/playback_history_service.dart +++ b/lib/services/playback_history_service.dart @@ -30,8 +30,10 @@ class PlaybackHistoryService { DateTime _lastPositionUpdate = DateTime.now(); bool _wasOfflineBefore = false; - FinampQueueItem? _lastReportedTrackStarted; // used to check if playback has already reported as "started" at some point for the current track - FinampQueueItem? _lastReportedTrackStopped; // used to prevent reporting a track as stopped multiple times + FinampQueueItem? + _lastReportedTrackStarted; // used to check if playback has already reported as "started" at some point for the current track + FinampQueueItem? + _lastReportedTrackStopped; // used to prevent reporting a track as stopped multiple times final _historyStream = BehaviorSubject>.seeded( List.empty(growable: true), @@ -44,9 +46,12 @@ class PlaybackHistoryService { PlaybackHistoryService() { _queueService.getCurrentTrackStream().listen((currentTrack) { - if (_audioService.playbackState.valueOrNull?.processingState != AudioProcessingState.completed) { + if (_audioService.playbackState.valueOrNull?.processingState != + AudioProcessingState.completed) { updateCurrentTrack(currentTrack); - } else if (_audioService.playbackState.valueOrNull?.processingState == AudioProcessingState.completed || currentTrack == null) { + } else if (_audioService.playbackState.valueOrNull?.processingState == + AudioProcessingState.completed || + currentTrack == null) { _playbackHistoryServiceLogger.info("Handling playback stop event"); _reportPlaybackStopped(); // stop periodic background updates if playback has ended @@ -54,7 +59,7 @@ class PlaybackHistoryService { } }); - FinampSettingsHelper.finampSettingsListener.addListener(() { + FinampSettingsHelper.finampSettingsListener.addListener(() { final isOffline = FinampSettingsHelper.finampSettings.isOffline; if (!isOffline && _wasOfflineBefore) { _updatePlaybackInfo(); @@ -71,14 +76,15 @@ 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) { if (currentState.playing != prevState?.playing) { // add to playback history if playback was stopped before updateCurrentTrack(currentItem, forceNewTrack: true); } - if (currentState.processingState != AudioProcessingState.completed && (currentState.queueIndex != prevState?.queueIndex || currentState.position != prevState?.position)) { + if (currentState.processingState != AudioProcessingState.completed && + (currentState.queueIndex != prevState?.queueIndex || + currentState.position != prevState?.position)) { _playbackHistoryServiceLogger.fine( "Handling 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 @@ -95,13 +101,16 @@ class PlaybackHistoryService { onPlaybackStateChanged(currentItem, currentState, prevState); } // handle seeking (changes updateTime (= last abnormal position change)) - else if (currentState.playing && prevState != null && + else if (currentState.playing && + prevState != null && // comparing the updateTime timestamps directly is unreliable, as they might just have different microsecond values // instead, compare the difference in milliseconds, with a small margin of error - (currentState.updateTime.millisecondsSinceEpoch - prevState.updateTime.millisecondsSinceEpoch).abs() > 1500) { - + (currentState.updateTime.millisecondsSinceEpoch - + prevState.updateTime.millisecondsSinceEpoch) + .abs() > + 1500) { bool isSeekEvent = true; - + // detect rewinding & looping a single track if ( // same track @@ -112,14 +121,16 @@ 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, forceNewTrack: true); // 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); isSeekEvent = false; // don't report seek event } else { // rewinding - updateCurrentTrack(currentItem, forceNewTrack: true); // add to playback history + updateCurrentTrack(currentItem, + forceNewTrack: true); // add to playback history // don't return, report seek event isSeekEvent = true; } @@ -166,13 +177,14 @@ class PlaybackHistoryService { BehaviorSubject> get historyStream => _historyStream; void _resetPeriodicUpdates() { - _periodicUpdateTimer?.cancel(); // remove old timer - _periodicUpdateTimer = Timer.periodic(Duration(seconds: FinampSettingsHelper.finampSettings.periodicPlaybackSessionUpdateFrequencySeconds), (timer) { + _periodicUpdateTimer = Timer.periodic( + Duration( + seconds: FinampSettingsHelper.finampSettings + .periodicPlaybackSessionUpdateFrequencySeconds), (timer) { _reportPeriodicSessionStatus(); }); - } /// method that converts history into a list grouped by date @@ -268,17 +280,16 @@ class PlaybackHistoryService { return groupedHistory; } - void updateCurrentTrack(FinampQueueItem? currentTrack, { + void updateCurrentTrack( + FinampQueueItem? currentTrack, { bool forceNewTrack = false, }) { if (currentTrack == null || - !forceNewTrack && ( - currentTrack == _currentTrack?.item || - currentTrack.item.id == "" || - currentTrack.id == _currentTrack?.item.id || - !_audioService.playbackState.value.playing - ) - ) { + !forceNewTrack && + (currentTrack == _currentTrack?.item || + currentTrack.item.id == "" || + currentTrack.id == _currentTrack?.item.id || + !_audioService.playbackState.value.playing)) { // current track hasn't changed return; } @@ -357,7 +368,8 @@ class PlaybackHistoryService { _resetPeriodicUpdates(); // delay next periodic update to avoid race conditions with old data //!!! allow reporting the same track here to skipping after looping a single track is reported correctly _lastReportedTrackStopped = previousItem; - await _jellyfinApiHelper.stopPlaybackProgress(previousTrackPlaybackData); + 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 } catch (e) { _playbackHistoryServiceLogger.warning(e); @@ -434,7 +446,8 @@ class PlaybackHistoryService { playerPosition: duration != null && state.position > duration ? duration : state.position, - includeNowPlayingQueue: FinampSettingsHelper.finampSettings.reportQueueToServer, + includeNowPlayingQueue: + FinampSettingsHelper.finampSettings.reportQueueToServer, ); } @@ -460,7 +473,8 @@ class PlaybackHistoryService { } } - Future _updatePlaybackInfo({ jellyfin_models.PlaybackProgressInfo? playbackData }) async { + Future _updatePlaybackInfo( + {jellyfin_models.PlaybackProgressInfo? playbackData}) async { if (FinampSettingsHelper.finampSettings.isOffline) { return; } @@ -495,7 +509,6 @@ class PlaybackHistoryService { required bool includeNowPlayingQueue, }) { try { - return jellyfin_models.PlaybackProgressInfo( itemId: item.item.extras?["itemJson"]["Id"] ?? "", playSessionId: _queueService.getQueue().id, @@ -509,7 +522,8 @@ class PlaybackHistoryService { playMethod: item.item.extras?["shouldTranscode"] ?? false ? "Transcode" : "DirectPlay", - nowPlayingQueue: getQueueToReport(includeNowPlayingQueue: includeNowPlayingQueue), + nowPlayingQueue: + getQueueToReport(includeNowPlayingQueue: includeNowPlayingQueue), playlistItemId: item.id, ); } catch (e) { @@ -561,8 +575,10 @@ class PlaybackHistoryService { } } - List? getQueueToReport({ bool? includeNowPlayingQueue }) { - if ((includeNowPlayingQueue ?? false) && FinampSettingsHelper.finampSettings.reportQueueToServer) { + List? getQueueToReport( + {bool? includeNowPlayingQueue}) { + if ((includeNowPlayingQueue ?? false) && + FinampSettingsHelper.finampSettings.reportQueueToServer) { final queue = _queueService .peekQueue(next: _maxQueueLengthToReport) .map((e) => jellyfin_models.QueueItem( diff --git a/lib/services/progress_state_stream.dart b/lib/services/progress_state_stream.dart index a10284a6b..0e20da450 100644 --- a/lib/services/progress_state_stream.dart +++ b/lib/services/progress_state_stream.dart @@ -19,7 +19,8 @@ Stream get progressStateStream { return Rx.combineLatest3( audioHandler.mediaItem, audioHandler.playbackState, - AudioService.position.startWith(audioHandler.playbackState.value.position), + AudioService.position + .startWith(audioHandler.playbackState.value.position), (mediaItem, playbackState, position) => ProgressState(mediaItem, playbackState, position)); } diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 3397d9f21..bed075863 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -551,7 +551,8 @@ class QueueService { List queueItems = []; for (final item in items) { queueItems.add(FinampQueueItem( - item: await _generateMediaItem(item, source?.contextNormalizationGain), + item: + await _generateMediaItem(item, source?.contextNormalizationGain), source: source ?? _order.originalSource, type: QueueItemQueueType.queue, )); @@ -583,7 +584,8 @@ class QueueService { List queueItems = []; for (final item in items) { queueItems.add(FinampQueueItem( - item: await _generateMediaItem(item, source?.contextNormalizationGain), + item: + await _generateMediaItem(item, source?.contextNormalizationGain), source: source ?? QueueItemSource( id: "next-up", @@ -623,7 +625,8 @@ class QueueService { List queueItems = []; for (final item in items) { queueItems.add(FinampQueueItem( - item: await _generateMediaItem(item, source?.contextNormalizationGain), + item: + await _generateMediaItem(item, source?.contextNormalizationGain), source: source ?? QueueItemSource( id: "next-up", @@ -887,8 +890,8 @@ class QueueService { /// [contextNormalizationGain] is the normalization gain of the context that the song is being played in, e.g. the album /// Should only be used when the tracks within that context come from the same source, e.g. the same album (or maybe artist?). Usually makes no sense for playlists. - Future _generateMediaItem( - jellyfin_models.BaseItemDto item, double? contextNormalizationGain) async { + Future _generateMediaItem(jellyfin_models.BaseItemDto item, + double? contextNormalizationGain) async { const uuid = Uuid(); final downloadedSong = await _isarDownloader.getSongDownload(item: item);