From c9e256f161e94644bc2448e9e4a3acb92f4b7eb3 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Mon, 11 Nov 2024 11:03:32 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Redesign=20track=20list=20tile=20(#?= =?UTF-8?q?829)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * started redesigning track list tile * improved list tile dismissible behavior and design * highlight current track, including mini visualizer - also removed old list tile code * use current accent color to highlight playing track - hopefully this doesn't have a huge performance impact... * track tile contrast and layout tweaks * use track list tile on playback history * use AddToPlaylistButton instead of FavoriteButton * fix color of favorite/playlist button on track list tile * adjust font size of artist span * simplify track list tile widgets to improve performance - also gets rid of duplicate gesture detectors and fixes incorrect menu themes * merge upstream * only show tile background for current track - also wait with applying the theme until accent color is available * tile layout adjustments * show album, tweak text styles * fix visual bugs * fix wrong cover and theme for AddToPlaylistButton * port new list tile to queue list - some edge cases might still be missing, and refactoring to using constructors instead of separate classed would be good * refactor track list tiles and fix theme issues * show list indices, hide covers, show & hide extra info where appropriate * show artist by default * fix auto-generated files * use new track list tile for Next Up and previous tracks, remove old queue list tile * added setting for optionally showing album covers on album screen * only apply cover on album screen setting to album screen list tiles * accessibility improvements * remove mini visualizer, tint title - the visualizer doesn't fit in well and isn't needed to mark the current track anymore - the fully-saturated accent color looked a bit strange * remove mini visualizer dependencies * don't block for dismiss gestures * fix covers hidden on non-album screens * remove `hideSongArtistsIfSameAsAlbumArtists` setting - doesn't work well with new track list tile - other apps also show the artist always (Spotify, YT Music) * use `colorThemeProvider` for tile accent color - uses Finamp's blue as a fallback until the correct accent color has been calculated - avoids pulsing/flashing effect on theme change * increase track title highlight * add download & lyrics indicators - also adjusted download indicator icons and size * get rid of unneeded widget and AnimatedTheme * improve title contrast in dark mode, fix opacity * fix title characters getting cut off * disable dismissible on tab view * re-enable dismissing queue items * fix tile color on queue * fix fallbacks for indices and artists * try to fix fade effect on artist overflow * use SliverFixedExtentList on album screen * fix -1 index * don't highlight some track list tiles * improve alphabet list padding --- .../add_to_playlist_button.dart | 8 +- .../AlbumScreen/album_screen_content.dart | 30 +- .../AlbumScreen/downloaded_indicator.dart | 15 +- .../AlbumScreen/song_list_tile.dart | 443 --------- .../AlbumScreen/track_list_tile.dart | 871 ++++++++++++++++++ ...sts_if_same_as_album_artists_selector.dart | 31 - .../MusicScreen/alphabet_item_list.dart | 9 +- .../MusicScreen/music_screen_tab_view.dart | 5 +- .../playback_history_list.dart | 38 +- lib/components/PlayerScreen/queue_list.dart | 61 +- .../PlayerScreen/queue_list_item.dart | 220 ----- lib/l10n/app_en.arb | 40 +- lib/main.dart | 3 + lib/models/finamp_models.dart | 16 +- lib/models/finamp_models.g.dart | 19 +- lib/screens/album_settings_screen.dart | 57 ++ lib/screens/layout_settings_screen.dart | 9 +- lib/screens/playback_history_screen.dart | 6 +- lib/services/finamp_settings_helper.dart | 9 - pubspec.lock | 8 - pubspec.yaml | 1 - 21 files changed, 1068 insertions(+), 831 deletions(-) delete mode 100644 lib/components/AlbumScreen/song_list_tile.dart create mode 100644 lib/components/AlbumScreen/track_list_tile.dart delete mode 100644 lib/components/LayoutSettingsScreen/hide_song_artists_if_same_as_album_artists_selector.dart delete mode 100644 lib/components/PlayerScreen/queue_list_item.dart create mode 100644 lib/screens/album_settings_screen.dart diff --git a/lib/components/AddToPlaylistScreen/add_to_playlist_button.dart b/lib/components/AddToPlaylistScreen/add_to_playlist_button.dart index e50178ab0..c5f5adba0 100644 --- a/lib/components/AddToPlaylistScreen/add_to_playlist_button.dart +++ b/lib/components/AddToPlaylistScreen/add_to_playlist_button.dart @@ -5,11 +5,13 @@ import 'package:finamp/models/jellyfin_models.dart'; import 'package:finamp/services/favorite_provider.dart'; import 'package:finamp/services/feedback_helper.dart'; import 'package:finamp/services/finamp_settings_helper.dart'; +import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart'; +import 'package:get_it/get_it.dart'; import 'playlist_actions_menu.dart'; @@ -35,6 +37,8 @@ class AddToPlaylistButton extends ConsumerStatefulWidget { } class _AddToPlaylistButtonState extends ConsumerState { + final _queueService = GetIt.instance(); + @override Widget build(BuildContext context) { if (widget.item == null) { @@ -74,12 +78,14 @@ class _AddToPlaylistButtonState extends ConsumerState { } bool inPlaylist = queueItemInPlaylist(widget.queueItem); + final currentTrack = _queueService.getCurrentTrack()?.baseItem; await showPlaylistActionsMenu( context: context, item: widget.item!, parentPlaylist: inPlaylist ? widget.queueItem!.source.item : null, - usePlayerTheme: true, + usePlayerTheme: widget.item?.blurHash != null && + widget.item?.blurHash == currentTrack?.blurHash, ); }), ), diff --git a/lib/components/AlbumScreen/album_screen_content.dart b/lib/components/AlbumScreen/album_screen_content.dart index bee56f487..b751b7bdf 100644 --- a/lib/components/AlbumScreen/album_screen_content.dart +++ b/lib/components/AlbumScreen/album_screen_content.dart @@ -10,7 +10,7 @@ import '../../services/finamp_settings_helper.dart'; import 'album_screen_content_flexible_space_bar.dart'; import 'download_button.dart'; import 'playlist_name_edit_button.dart'; -import 'song_list_tile.dart'; +import 'track_list_tile.dart'; typedef BaseItemDtoCallback = void Function(BaseItemDto item); @@ -175,7 +175,10 @@ class _SongsSliverListState extends State { ), ); } - return SliverList( + return SliverFixedExtentList( + itemExtent: TrackListItemTile.defaultTileHeight + + TrackListItemTile.defaultTitleGap, + // return SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { // When user selects song from disc other than first, index number is @@ -197,10 +200,13 @@ class _SongsSliverListState extends State { return item; } - return SongListTile( + return TrackListTile( item: item, children: widget.childrenForQueue, index: indexOffset, + showIndex: item.albumId == widget.parent.id, + showCover: item.albumId != widget.parent.id || + FinampSettingsHelper.finampSettings.showCoversOnAlbumScreen, parentItem: widget.parent, onRemoveFromList: () { final item = removeItem(); @@ -210,24 +216,6 @@ class _SongsSliverListState extends State { }, isInPlaylist: widget.parent.type == "Playlist", isOnArtistScreen: widget.isOnArtistScreen, - // show artists except for this one scenario - showArtists: !( - // we're on album screen - widget.parent.type == "MusicAlbum" - // "hide song artists if they're the same as album artists" == true - && - FinampSettingsHelper - .finampSettings.hideSongArtistsIfSameAsAlbumArtists - // song artists == album artists - && - setEquals( - widget.parent.albumArtists - ?.map((e) => e.name) - .toSet(), - item.artists?.toSet())) - // hide song artist if on the artist screen - && - widget.parent.type != "MusicArtist", showPlayCount: widget.showPlayCount, ); }, diff --git a/lib/components/AlbumScreen/downloaded_indicator.dart b/lib/components/AlbumScreen/downloaded_indicator.dart index 94e819dcf..8cd3639c1 100644 --- a/lib/components/AlbumScreen/downloaded_indicator.dart +++ b/lib/components/AlbumScreen/downloaded_indicator.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get_it/get_it.dart'; import '../../models/finamp_models.dart'; @@ -30,22 +31,26 @@ class DownloadedIndicator extends ConsumerWidget { case DownloadItemState.downloading: case DownloadItemState.needsRedownload: return Icon( - Icons.download_outlined, - color: Colors.grey.withOpacity(0.5), + TablerIcons.cloud_download, + color: Theme.of(context) + .textTheme + .bodyMedium! + .color + ?.withOpacity(0.75), size: size, ); case DownloadItemState.failed: case DownloadItemState.syncFailed: return Icon( - Icons.error, + TablerIcons.download_off, color: Colors.red, size: size, ); case DownloadItemState.complete: case DownloadItemState.needsRedownloadComplete: return Icon( - Icons.download, - color: Theme.of(context).colorScheme.secondary, + TablerIcons.device_sd_card, + color: Theme.of(context).textTheme.bodyMedium!.color, size: size, ); } diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart deleted file mode 100644 index 8d2eb95fa..000000000 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ /dev/null @@ -1,443 +0,0 @@ -import 'dart:async'; - -import 'package:audio_service/audio_service.dart'; -import 'package:collection/collection.dart'; -import 'package:finamp/components/AlbumScreen/song_menu.dart'; -import 'package:finamp/components/MusicScreen/music_screen_tab_view.dart'; -import 'package:finamp/components/global_snackbar.dart'; -import 'package:finamp/models/finamp_models.dart'; -import 'package:finamp/models/jellyfin_models.dart' as jellyfin_models; -import 'package:finamp/services/finamp_user_helper.dart'; -import 'package:finamp/services/queue_service.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; -import 'package:get_it/get_it.dart'; -import 'package:mini_music_visualizer/mini_music_visualizer.dart'; - -import '../../services/audio_service_helper.dart'; -import '../../services/downloads_service.dart'; -import '../../services/finamp_settings_helper.dart'; -import '../../services/music_player_background_task.dart'; -import '../../services/process_artist.dart'; -import '../../services/theme_provider.dart'; -import '../album_image.dart'; -import '../favourite_button.dart'; -import '../print_duration.dart'; -import 'downloaded_indicator.dart'; - -enum SongListTileMenuItems { - addToQueue, - playNext, - addToNextUp, - addToPlaylist, - removeFromPlaylist, - instantMix, - goToAlbum, - addFavourite, - removeFavourite, - download, - delete, -} - -class SongListTile extends ConsumerStatefulWidget { - const SongListTile({ - super.key, - required this.item, - - /// Children that are related to this list tile, such as the other songs in - /// the album. This is used to give the audio service all the songs for the - /// item. If null, only this song will be given to the audio service. - this.children, - - /// Index of the song in whatever parent this widget is in. Used to start - /// the audio service at a certain index, such as when selecting the middle - /// song in an album. Will be -1 if we are offline and the song is not downloaded. - this.index, - this.parentItem, - - /// Whether we are in the songs tab, as opposed to a playlist/album - this.isSong = false, - this.showArtists = true, - this.onRemoveFromList, - this.showPlayCount = false, - - /// Whether this widget is being displayed in a playlist. If true, will show - /// the remove from playlist button. - this.isInPlaylist = false, - this.isOnArtistScreen = false, - this.isShownInSearch = false, - }); - - final jellyfin_models.BaseItemDto item; - final Future>? children; - final Future? index; - final bool isSong; - final jellyfin_models.BaseItemDto? parentItem; - final bool showArtists; - final VoidCallback? onRemoveFromList; - final bool showPlayCount; - final bool isInPlaylist; - final bool isOnArtistScreen; - final bool isShownInSearch; - - @override - ConsumerState createState() => _SongListTileState(); -} - -class _SongListTileState extends ConsumerState - with SingleTickerProviderStateMixin { - final _audioServiceHelper = GetIt.instance(); - final _queueService = GetIt.instance(); - final _audioHandler = GetIt.instance(); - - FinampTheme? _menuTheme; - - @override - void dispose() { - _menuTheme?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - bool playable; - if (FinampSettingsHelper.finampSettings.isOffline) { - playable = ref.watch(GetIt.instance() - .stateProvider(DownloadStub.fromItem( - type: DownloadItemType.song, item: widget.item)) - .select((value) => value.value?.isComplete ?? false)); - } else { - playable = true; - } - - final listTile = StreamBuilder( - stream: _audioHandler.mediaItem, - builder: (context, snapshot) { - // I think past me did this check directly from the JSON for - // performance. It works for now, apologies if you're debugging it - // years in the future. - final isCurrentlyPlaying = - snapshot.data?.extras?["itemJson"]["Id"] == widget.item.id && - snapshot.data?.extras?["itemJson"]["AlbumId"] == - widget.parentItem?.id; - - return ListTile( - leading: AlbumImage( - item: widget.item, - disabled: !playable, - themeCallback: (x) => _menuTheme ??= x, - ), - title: Opacity( - opacity: playable ? 1.0 : 0.5, - child: RichText( - text: TextSpan( - children: [ - // third condition checks if the item is viewed from its album (instead of e.g. a playlist) - // same horrible check as in canGoToAlbum in GestureDetector below - if (widget.item.indexNumber != null && - !widget.isSong && - widget.item.albumId == widget.parentItem?.id) - TextSpan( - text: "${widget.item.indexNumber}. ", - style: TextStyle( - color: Theme.of(context).disabledColor)), - TextSpan( - text: widget.item.name ?? - AppLocalizations.of(context)!.unknownName, - style: TextStyle( - color: isCurrentlyPlaying - ? Theme.of(context).colorScheme.secondary - : null, - ), - ), - ], - style: Theme.of(context).textTheme.titleMedium, - ), - ), - ), - subtitle: Opacity( - opacity: playable ? 1.0 : 0.5, - child: Text.rich( - TextSpan( - children: [ - WidgetSpan( - child: Transform.translate( - offset: const Offset(-3, 0), - child: DownloadedIndicator( - item: DownloadStub.fromItem( - item: widget.item, type: DownloadItemType.song), - size: Theme.of(context) - .textTheme - .bodyMedium! - .fontSize! + - 3, - ), - ), - alignment: PlaceholderAlignment.top, - ), - if (widget.item.hasLyrics ?? false) - WidgetSpan( - child: Transform.translate( - offset: const Offset(-2.5, 0), - child: Icon( - TablerIcons.microphone_2, - size: Theme.of(context) - .textTheme - .bodyMedium! - .fontSize! + - 2, - )), - alignment: PlaceholderAlignment.top, - ), - TextSpan( - text: printDuration(widget.item.runTimeTicksDuration()), - style: TextStyle( - color: Theme.of(context) - .textTheme - .bodyMedium - ?.color - ?.withOpacity(0.7)), - ), - if (widget.showArtists) - TextSpan( - text: - " · ${processArtist(widget.item.artists?.join(", ") ?? widget.item.albumArtist, context)}", - style: - TextStyle(color: Theme.of(context).disabledColor), - ), - if (widget.showPlayCount) - TextSpan( - text: - " · ${AppLocalizations.of(context)!.playCountValue(widget.item.userData?.playCount ?? 0)}", - style: - TextStyle(color: Theme.of(context).disabledColor), - ), - ], - ), - overflow: TextOverflow.ellipsis, - ), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (isCurrentlyPlaying) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: MiniMusicVisualizer( - color: Theme.of(context).colorScheme.secondary, - width: 4, - height: 15, - ), - ), - FavoriteButton( - item: widget.item, - onlyIfFav: true, - ), - ], - ), - // This must be in ListTile instead of parent GestureDetecter to - // enable hover color changes - onTap: () async { - if (!playable) return; - var children = await widget.children; - if (children != null) { - // start linear playback of album from the given index - await _queueService.startPlayback( - items: children, - startingIndex: await widget.index, - order: FinampPlaybackOrder.linear, - source: QueueItemSource( - type: widget.isInPlaylist - ? QueueItemSourceType.playlist - : widget.isOnArtistScreen - ? QueueItemSourceType.artist - : QueueItemSourceType.album, - name: QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated, - pretranslatedName: ((widget.isInPlaylist || - widget.isOnArtistScreen) - ? widget.parentItem?.name - : widget.item.album) ?? - AppLocalizations.of(context)!.placeholderSource), - 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, - ), - ); - } else { - // TODO put in a real offline songs implementation - if (FinampSettingsHelper.finampSettings.isOffline) { - final settings = FinampSettingsHelper.finampSettings; - final downloadsService = GetIt.instance(); - final finampUserHelper = GetIt.instance(); - - // get all downloaded songs in order - List offlineItems; - // If we're on the songs tab, just get all of the downloaded items - offlineItems = await downloadsService.getAllSongs( - // nameFilter: widget.searchTerm, - viewFilter: finampUserHelper.currentUser?.currentView?.id, - nullableViewFilters: - settings.showDownloadsWithUnknownLibrary, - onlyFavorites: settings.onlyShowFavourite && - settings.trackOfflineFavorites, - ); - - var items = offlineItems - .map((e) => e.baseItem) - .whereNotNull() - .toList(); - - 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, - source: QueueItemSource( - name: QueueItemSourceName( - type: widget.item.name != null - ? QueueItemSourceNameType.mix - : QueueItemSourceNameType.instantMix, - localizationParameter: widget.item.name ?? "", - ), - type: QueueItemSourceType.allSongs, - id: widget.item.id, - ), - ); - } else { - if (FinampSettingsHelper - .finampSettings.startInstantMixForIndividualTracks) { - await _audioServiceHelper - .startInstantMixForItem(widget.item); - } else { - await _queueService.startPlayback( - items: [widget.item], - source: QueueItemSource( - name: QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated, - pretranslatedName: widget.item.name), - type: QueueItemSourceType.song, - id: widget.item.id, - ), - ); - } - } - } - }, - ); - }); - - void menuCallback() async { - if (playable) { - unawaited(Feedback.forLongPress(context)); - await showModalSongMenu( - context: context, - item: widget.item, - isInPlaylist: widget.isInPlaylist, - parentItem: widget.parentItem, - onRemoveFromList: widget.onRemoveFromList, - themeProvider: _menuTheme, - confirmPlaylistRemoval: false, - ); - } - } - - return GestureDetector( - onTapDown: (_) { - _menuTheme?.calculate(Theme.of(context).brightness); - }, - onLongPressStart: (details) => menuCallback(), - onSecondaryTapDown: (details) => menuCallback(), - child: (widget.isSong || !playable) - ? listTile - : Dismissible( - key: Key(widget.index.toString()), - direction: FinampSettingsHelper.finampSettings.disableGesture - ? DismissDirection.none - : DismissDirection.horizontal, - dismissThresholds: const { - DismissDirection.startToEnd: 0.65, - DismissDirection.endToStart: 0.65 - }, - background: Container( - color: Theme.of(context).colorScheme.secondaryContainer, - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Icon( - TablerIcons.playlist, - color: - Theme.of(context).colorScheme.onSecondaryContainer, - size: 40, - ), - Icon( - TablerIcons.playlist, - color: - Theme.of(context).colorScheme.onSecondaryContainer, - size: 40, - ) - ], - ), - ), - ), - confirmDismiss: (direction) async { - if (FinampSettingsHelper.finampSettings.swipeInsertQueueNext) { - await _queueService.addToNextUp( - items: [widget.item], - source: QueueItemSource( - type: QueueItemSourceType.nextUp, - name: QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated, - pretranslatedName: - AppLocalizations.of(context)!.queue), - id: widget.parentItem?.id ?? "", - item: widget.parentItem, - )); - } else { - await _queueService.addToQueue( - items: [widget.item], - source: QueueItemSource( - type: QueueItemSourceType.queue, - name: QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated, - pretranslatedName: - AppLocalizations.of(context)!.queue), - id: widget.parentItem?.id ?? "", - item: widget.parentItem, - )); - } - - if (!mounted) return false; - - GlobalSnackbar.message( - (scaffold) => - FinampSettingsHelper.finampSettings.swipeInsertQueueNext - ? AppLocalizations.of(scaffold)! - .confirmAddToNextUp("track") - : AppLocalizations.of(scaffold)! - .confirmAddToQueue("track"), - isConfirmation: true, - ); - - return false; - }, - child: listTile, - ), - ); - } -} diff --git a/lib/components/AlbumScreen/track_list_tile.dart b/lib/components/AlbumScreen/track_list_tile.dart new file mode 100644 index 000000000..e2148e791 --- /dev/null +++ b/lib/components/AlbumScreen/track_list_tile.dart @@ -0,0 +1,871 @@ +import 'dart:async'; + +import 'package:audio_service/audio_service.dart'; +import 'package:collection/collection.dart'; +import 'package:finamp/components/AlbumScreen/song_menu.dart'; +import 'package:finamp/components/MusicScreen/music_screen_tab_view.dart'; +import 'package:finamp/components/AddToPlaylistScreen/add_to_playlist_button.dart'; +import 'package:finamp/components/global_snackbar.dart'; +import 'package:finamp/models/finamp_models.dart'; +import 'package:finamp/models/jellyfin_models.dart' as jellyfin_models; +import 'package:finamp/services/feedback_helper.dart'; +import 'package:finamp/services/finamp_user_helper.dart'; +import 'package:finamp/services/queue_service.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; +import 'package:flutter_vibrate/flutter_vibrate.dart'; +import 'package:get_it/get_it.dart'; + +import '../../services/audio_service_helper.dart'; +import '../../services/downloads_service.dart'; +import '../../services/finamp_settings_helper.dart'; +import '../../services/music_player_background_task.dart'; +import '../../services/process_artist.dart'; +import '../../services/theme_provider.dart'; +import '../album_image.dart'; +import '../favourite_button.dart'; +import '../print_duration.dart'; +import 'downloaded_indicator.dart'; + +enum TrackListTileMenuItems { + addToQueue, + playNext, + addToNextUp, + addToPlaylist, + removeFromPlaylist, + instantMix, + goToAlbum, + addFavourite, + removeFavourite, + download, + delete, +} + +class TrackListTile extends StatelessWidget { + const TrackListTile({ + super.key, + required this.item, + + /// Children that are related to this list tile, such as the other songs in + /// the album. This is used to give the audio service all the songs for the + /// item. If null, only this song will be given to the audio service. + this.children, + + /// Index of the song in whatever parent this widget is in. Used to start + /// the audio service at a certain index, such as when selecting the middle + /// song in an album. Will be -1 if we are offline and the song is not downloaded. + this.index, + this.parentItem, + + // if leading index number should be shown + this.showIndex = false, + // if leading album cover should be shown + this.showCover = true, + + /// Whether we are in the songs tab, as opposed to a playlist/album + this.isSong = false, + this.onRemoveFromList, + this.showPlayCount = false, + + /// Whether this widget is being displayed in a playlist. If true, will show + /// the remove from playlist button. + this.isInPlaylist = false, + this.isOnArtistScreen = false, + this.isShownInSearch = false, + this.allowDismiss = true, + this.highlightCurrentTrack = true, + }); + + final jellyfin_models.BaseItemDto item; + final Future>? children; + final Future? index; + final bool showIndex; + final bool showCover; + final bool isSong; + final jellyfin_models.BaseItemDto? parentItem; + final VoidCallback? onRemoveFromList; + final bool showPlayCount; + final bool isInPlaylist; + final bool isOnArtistScreen; + final bool isShownInSearch; + final bool allowDismiss; + final bool highlightCurrentTrack; + + @override + Widget build(BuildContext context) { + trackListTileOnTap(bool playable) async { + final queueService = GetIt.instance(); + final audioServiceHelper = GetIt.instance(); + + if (!playable) return; + if (children != null) { + // start linear playback of album from the given index + await queueService.startPlayback( + items: await children!, + startingIndex: await index, + order: FinampPlaybackOrder.linear, + source: QueueItemSource( + type: isInPlaylist + ? QueueItemSourceType.playlist + : isOnArtistScreen + ? QueueItemSourceType.artist + : QueueItemSourceType.album, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: ((isInPlaylist || isOnArtistScreen) + ? parentItem?.name + : item.album) ?? + AppLocalizations.of(context)!.placeholderSource), + id: parentItem?.id ?? "", + item: parentItem, + // we're playing from an album, so we should use the album's normalization gain. + contextNormalizationGain: (isInPlaylist || isOnArtistScreen) + ? null + : parentItem?.normalizationGain, + ), + ); + } else { + // TODO put in a real offline songs implementation + if (FinampSettingsHelper.finampSettings.isOffline) { + final settings = FinampSettingsHelper.finampSettings; + final downloadsService = GetIt.instance(); + final finampUserHelper = GetIt.instance(); + + // get all downloaded songs in order + List offlineItems; + // If we're on the songs tab, just get all of the downloaded items + offlineItems = await downloadsService.getAllSongs( + // nameFilter: widget.searchTerm, + viewFilter: finampUserHelper.currentUser?.currentView?.id, + nullableViewFilters: settings.showDownloadsWithUnknownLibrary, + onlyFavorites: + settings.onlyShowFavourite && settings.trackOfflineFavorites, + ); + + var items = + offlineItems.map((e) => e.baseItem).whereNotNull().toList(); + + items = sortItems(items, settings.tabSortBy[TabContentType.songs], + settings.tabSortOrder[TabContentType.songs]); + + await queueService.startPlayback( + items: items, + startingIndex: isShownInSearch + ? items.indexWhere((element) => element.id == item.id) + : await index, + source: QueueItemSource( + name: QueueItemSourceName( + type: item.name != null + ? QueueItemSourceNameType.mix + : QueueItemSourceNameType.instantMix, + localizationParameter: item.name ?? "", + ), + type: QueueItemSourceType.allSongs, + id: item.id, + ), + ); + } else { + if (FinampSettingsHelper + .finampSettings.startInstantMixForIndividualTracks) { + await audioServiceHelper.startInstantMixForItem(item); + } else { + await queueService.startPlayback( + items: [item], + source: QueueItemSource( + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: item.name), + type: QueueItemSourceType.song, + id: item.id, + ), + ); + } + } + } + } + + Future trackListTileConfirmDismiss(DismissDirection direction) async { + final queueService = GetIt.instance(); + if (FinampSettingsHelper.finampSettings.swipeInsertQueueNext) { + unawaited(queueService.addToNextUp( + items: [item], + source: QueueItemSource( + type: QueueItemSourceType.nextUp, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: AppLocalizations.of(context)!.queue), + id: parentItem?.id ?? "", + item: parentItem, + ))); + } else { + unawaited(queueService.addToQueue( + items: [item], + source: QueueItemSource( + type: QueueItemSourceType.queue, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: AppLocalizations.of(context)!.queue), + id: parentItem?.id ?? "", + item: parentItem, + ))); + } + + GlobalSnackbar.message( + (scaffold) => FinampSettingsHelper.finampSettings.swipeInsertQueueNext + ? AppLocalizations.of(scaffold)!.confirmAddToNextUp("track") + : AppLocalizations.of(scaffold)!.confirmAddToQueue("track"), + isConfirmation: true, + ); + + return false; + } + + final dismissBackground = Container( + // color: Theme.of(context).colorScheme.secondaryContainer, + padding: const EdgeInsets.only(left: 12.0, right: 12.0, top: 8.0), + alignment: Alignment.centerLeft, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + TablerIcons.playlist, + color: Theme.of(context).colorScheme.secondary, + size: 40, + ), + const SizedBox(width: 4.0), + Text( + FinampSettingsHelper.finampSettings.swipeInsertQueueNext + ? AppLocalizations.of(context)!.addToNextUp + : AppLocalizations.of(context)!.addToQueue, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + FinampSettingsHelper.finampSettings.swipeInsertQueueNext + ? AppLocalizations.of(context)!.addToNextUp + : AppLocalizations.of(context)!.addToQueue, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 4.0), + Icon( + TablerIcons.playlist, + color: Theme.of(context).colorScheme.secondary, + size: 40, + ), + ], + ), + ], + ), + ); + + return TrackListItem( + baseItem: item, + parentItem: parentItem, + listIndex: index, + actualIndex: item.indexNumber, + showIndex: showIndex, + showCover: showCover, + showArtists: parentItem?.isArtist != true, + showPlayCount: showPlayCount, + isInPlaylist: isInPlaylist, + allowReorder: false, + allowDismiss: allowDismiss, + highlightCurrentTrack: highlightCurrentTrack, + onRemoveFromList: onRemoveFromList, + onTap: trackListTileOnTap, + confirmDismiss: trackListTileConfirmDismiss, + dismissBackground: dismissBackground, + ); + } +} + +class QueueListTile extends StatelessWidget { + final jellyfin_models.BaseItemDto item; + final jellyfin_models.BaseItemDto? parentItem; + final Future? listIndex; + final int actualIndex; + final int indexOffset; + final bool isCurrentTrack; + final bool isInPlaylist; + final bool allowReorder; + final bool allowDismiss; + final bool highlightCurrentTrack; + + final void Function(bool playable) onTap; + final VoidCallback? onRemoveFromList; + final void Function(FinampTheme)? themeCallback; + + const QueueListTile({ + super.key, + required this.item, + required this.listIndex, + required this.actualIndex, + required this.indexOffset, + required this.onTap, + required this.isCurrentTrack, + required this.isInPlaylist, + required this.allowReorder, + this.allowDismiss = true, + this.highlightCurrentTrack = false, + this.parentItem, + this.onRemoveFromList, + this.themeCallback, + }); + + @override + Widget build(BuildContext context) { + Future queueListTileConfirmDismiss(direction) async { + final queueService = GetIt.instance(); + FeedbackHelper.feedback(FeedbackType.impact); + unawaited(queueService.removeAtOffset(indexOffset)); + return true; + } + + return TrackListItem( + baseItem: item, + parentItem: parentItem, + listIndex: listIndex, + actualIndex: item.indexNumber, + isInPlaylist: isInPlaylist, + allowReorder: allowReorder, + allowDismiss: allowDismiss, + highlightCurrentTrack: highlightCurrentTrack, + onRemoveFromList: onRemoveFromList, + // This must be in ListTile instead of parent GestureDetecter to + // enable hover color changes + onTap: onTap, + confirmDismiss: queueListTileConfirmDismiss, + ); + } +} + +class TrackListItem extends ConsumerStatefulWidget { + final jellyfin_models.BaseItemDto baseItem; + final jellyfin_models.BaseItemDto? parentItem; + final Future? listIndex; + final int? actualIndex; + final bool showIndex; + final bool showCover; + final bool showArtists; + final bool showPlayCount; + final bool isInPlaylist; + final bool allowReorder; + final bool allowDismiss; + final bool highlightCurrentTrack; + final Widget dismissBackground; + + final void Function(bool playable) onTap; + final Future Function(DismissDirection direction) confirmDismiss; + final VoidCallback? onRemoveFromList; + + const TrackListItem( + {super.key, + required this.baseItem, + required this.listIndex, + required this.actualIndex, + required this.onTap, + required this.confirmDismiss, + this.parentItem, + this.isInPlaylist = false, + this.allowReorder = false, + this.allowDismiss = true, + this.showIndex = false, + this.showCover = true, + this.showArtists = true, + this.showPlayCount = false, + this.highlightCurrentTrack = true, + this.onRemoveFromList, + this.dismissBackground = const SizedBox.shrink()}); + + @override + ConsumerState createState() => TrackListItemState(); +} + +class TrackListItemState extends ConsumerState + with SingleTickerProviderStateMixin { + final _audioHandler = GetIt.instance(); + + FinampTheme? _menuTheme; + + @override + void dispose() { + _menuTheme?.dispose(); + super.dispose(); + } + + @override + Widget build( + BuildContext context, + ) { + bool playable; + if (FinampSettingsHelper.finampSettings.isOffline) { + playable = ref.watch(GetIt.instance() + .stateProvider(DownloadStub.fromItem( + type: DownloadItemType.song, item: widget.baseItem)) + .select((value) => value.value?.isComplete ?? false)); + } else { + playable = true; + } + + final bool showAlbum = widget.baseItem.albumId != widget.parentItem?.id; + + void menuCallback() async { + if (playable) { + FeedbackHelper.feedback(FeedbackType.selection); + await showModalSongMenu( + context: context, + item: widget.baseItem, + isInPlaylist: widget.isInPlaylist, + parentItem: widget.parentItem, + onRemoveFromList: widget.onRemoveFromList, + themeProvider: _menuTheme, + confirmPlaylistRemoval: false, + ); + } + } + + final listItem = StreamBuilder( + stream: _audioHandler.mediaItem, + builder: (context, snapshot) { + // I think past me did this check directly from the JSON for + // performance. It works for now, apologies if you're debugging it + // years in the future. + final isCurrentlyPlaying = + snapshot.data?.extras?["itemJson"]["Id"] == widget.baseItem.id; + + return Opacity( + opacity: playable ? 1.0 : 0.5, + child: Card( + color: Colors.transparent, + elevation: 0, + margin: const EdgeInsets.only(left: 10.0, right: 10.0, top: 10.0), + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + child: isCurrentlyPlaying && widget.highlightCurrentTrack + ? ProviderScope( + overrides: [ + themeDataProvider.overrideWith((ref) { + return ref.watch(playerScreenThemeDataProvider) ?? + FinampTheme.defaultTheme(); + }) + ], + child: Consumer( + builder: (BuildContext context, WidgetRef ref, + Widget? child) { + final imageTheme = + ref.watch(playerScreenThemeProvider); + return AnimatedTheme( + duration: const Duration(milliseconds: 500), + data: ThemeData( + // colorScheme: imageTheme, + // brightness: Theme.of(context).brightness, + colorScheme: imageTheme.copyWith( + surfaceContainer: ref + .watch(colorThemeProvider) + .primary + .withOpacity( + Theme.of(context).brightness == + Brightness.dark + ? 0.35 + : 0.3) + ), + textTheme: Theme.of(context).textTheme.copyWith( + bodyLarge: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith( + color: Color.alphaBlend( + (ref + .watch( + colorThemeProvider) + .secondary + .withOpacity(Theme.of( + context) + .brightness == + Brightness + .light + ? 0.5 + : 0.1)), + Theme.of(context) + .textTheme + .bodyLarge + ?.color ?? + (Theme.of(context) + .brightness == + Brightness.light + ? Colors.black + : Colors.white)) + ), + ), + iconTheme: Theme.of(context).iconTheme.copyWith( + color: imageTheme.primary, + ), + ), + child: TrackListItemTile( + baseItem: widget.baseItem, + listIndex: widget.listIndex, + actualIndex: widget.actualIndex, + showIndex: widget.showIndex, + showCover: widget.showCover, + showArtists: widget.showArtists, + showAlbum: showAlbum, + showPlayCount: widget.showPlayCount, + themeCallback: (x) => _menuTheme = x, + isCurrentTrack: isCurrentlyPlaying, + highlightCurrentTrack: + widget.highlightCurrentTrack, + allowReorder: widget.allowReorder, + onTap: () => widget.onTap(playable)), + ); + }, + ), + ) + : TrackListItemTile( + baseItem: widget.baseItem, + listIndex: widget.listIndex, + actualIndex: widget.actualIndex, + showIndex: widget.showIndex, + showCover: widget.showCover, + showArtists: widget.showArtists, + showAlbum: showAlbum, + showPlayCount: widget.showPlayCount, + themeCallback: (x) => _menuTheme = x, + isCurrentTrack: isCurrentlyPlaying, + highlightCurrentTrack: widget.highlightCurrentTrack, + allowReorder: widget.allowReorder, + onTap: () => widget.onTap(playable)), + ), + ); + }); + + return GestureDetector( + onTapDown: (_) { + _menuTheme?.calculate(Theme.of(context).brightness); + }, + onLongPressStart: (details) => menuCallback(), + onSecondaryTapDown: (details) => menuCallback(), + child: !playable + ? listItem + : Dismissible( + key: Key(widget.listIndex.toString()), + direction: FinampSettingsHelper.finampSettings.disableGesture || + !widget.allowDismiss + ? DismissDirection.none + : DismissDirection.horizontal, + dismissThresholds: const { + DismissDirection.startToEnd: 0.65, + DismissDirection.endToStart: 0.65 + }, + // no background, dismissing really dismisses here + confirmDismiss: widget.confirmDismiss, + background: widget.dismissBackground, + child: listItem, + ), + ); + } +} + +class TrackListItemTile extends StatelessWidget { + const TrackListItemTile({ + super.key, + required this.baseItem, + required this.themeCallback, + required this.isCurrentTrack, + required this.allowReorder, + required this.onTap, + required this.actualIndex, + this.listIndex, + this.showIndex = false, + this.showCover = true, + this.showArtists = true, + this.showAlbum = true, + this.showPlayCount = false, + this.highlightCurrentTrack = true, + }); + + final jellyfin_models.BaseItemDto baseItem; + final void Function(FinampTheme theme)? themeCallback; + final bool isCurrentTrack; + final bool allowReorder; + final Future? listIndex; + final int? actualIndex; + final bool showIndex; + final bool showCover; + final bool showArtists; + final bool showAlbum; + final bool showPlayCount; + final bool highlightCurrentTrack; + final void Function() onTap; + + static const double defaultTileHeight = 60.0; + static const double defaultTitleGap = 10.0; + + @override + Widget build(BuildContext context) { + + final highlightTrack = isCurrentTrack && highlightCurrentTrack; + + final bool secondRowNeeded = showArtists || showAlbum || showPlayCount; + + final durationLabelFullHours = + (baseItem.runTimeTicksDuration()?.inHours ?? 0); + final durationLabelFullMinutes = + (baseItem.runTimeTicksDuration()?.inMinutes ?? 0) % 60; + final durationLabelSeconds = + (baseItem.runTimeTicksDuration()?.inSeconds ?? 0) % 60; + final durationLabelString = + "${durationLabelFullHours > 0 ? "$durationLabelFullHours ${AppLocalizations.of(context)!.hours} " : ""}${durationLabelFullMinutes > 0 ? "$durationLabelFullMinutes ${AppLocalizations.of(context)!.minutes} " : ""}$durationLabelSeconds ${AppLocalizations.of(context)!.seconds}"; + + final artistsString = (baseItem.artists?.isNotEmpty ?? false) + ? baseItem.artists?.join(", ") + : baseItem.albumArtist ?? AppLocalizations.of(context)!.unknownArtist; + + return ListTileTheme( + tileColor: highlightTrack + ? Theme.of(context).colorScheme.surfaceContainer + : Colors.transparent, + child: ListTile( + visualDensity: const VisualDensity( + horizontal: 0.0, + vertical: 0.5, + ), + minVerticalPadding: 0.0, + horizontalTitleGap: defaultTitleGap, + contentPadding: + const EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0), + // tileColor: Theme.of(context).colorScheme.primary.withOpacity(0.5), + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (showIndex && actualIndex != null) + Padding( + padding: showCover + ? const EdgeInsets.only(left: 2.0, right: 6.0) + : const EdgeInsets.only(left: 6.0, right: 0.0), + child: SizedBox.fromSize( + size: const Size(22.0, defaultTileHeight), + child: Center( + child: Text( + actualIndex.toString(), + textAlign: TextAlign.end, + style: TextStyle( + color: Theme.of(context).textTheme.bodyMedium?.color, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ))), + ), + if (showCover) + AlbumImage( + item: baseItem, + borderRadius: highlightTrack + ? BorderRadius.zero + : BorderRadius.circular(8.0), + themeCallback: themeCallback, + ), + ], + ), + title: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: defaultTileHeight, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + fit: FlexFit.loose, + flex: 3, + child: Text( + baseItem.name ?? AppLocalizations.of(context)!.unknownName, + style: TextStyle( + color: Theme.of(context).textTheme.bodyLarge!.color, + fontSize: 15.5, + fontWeight: FontWeight.w500, + height: 1.1), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ), + Flexible( + fit: FlexFit.loose, + flex: 2, + child: Text.rich( + overflow: TextOverflow.clip, + softWrap: false, + maxLines: 1, + TextSpan(children: [ + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(right: 2.0), + child: Transform.translate( + offset: const Offset(-1.5, 2.5), + child: DownloadedIndicator( + item: DownloadStub.fromItem( + item: baseItem, type: DownloadItemType.song), + size: Theme.of(context) + .textTheme + .bodyMedium! + .fontSize! + + 1, + ), + ), + ), + alignment: PlaceholderAlignment.top, + ), + if (baseItem.hasLyrics ?? false) + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(right: 2.0), + child: Transform.translate( + offset: const Offset(-1.5, 2.5), + child: Icon( + TablerIcons.microphone_2, + size: Theme.of(context) + .textTheme + .bodyMedium! + .fontSize! + + 1, + )), + ), + alignment: PlaceholderAlignment.top, + ), + if (showArtists) + TextSpan( + text: artistsString, + style: TextStyle( + color: Theme.of(context) + .textTheme + .bodyMedium! + .color! + .withOpacity(0.75), + fontSize: 13, + fontWeight: FontWeight.w400, + overflow: TextOverflow.ellipsis), + ), + if (!secondRowNeeded) + // show the artist anyway if nothing else is shown + TextSpan( + text: baseItem.artists?.join(", ") ?? + baseItem.albumArtist ?? + AppLocalizations.of(context)!.unknownArtist, + style: TextStyle( + color: Theme.of(context) + .textTheme + .bodyMedium! + .color! + .withOpacity(0.6), + fontSize: 13, + fontWeight: FontWeight.w300, + ), + ), + if (showArtists) + const WidgetSpan(child: SizedBox(width: 10.0)), + if (showAlbum) + TextSpan( + text: baseItem.album, + style: TextStyle( + color: Theme.of(context) + .textTheme + .bodyMedium! + .color! + .withOpacity(0.6), + fontSize: 13, + fontWeight: FontWeight.w300, + ), + ), + if (showAlbum) + const WidgetSpan(child: SizedBox(width: 10.0)), + if (showPlayCount) + TextSpan( + text: AppLocalizations.of(context)! + .playCountValue(baseItem.userData?.playCount ?? 0), + style: TextStyle( + color: Theme.of(context) + .textTheme + .bodyMedium! + .color! + .withOpacity(0.6), + fontSize: 13, + fontWeight: FontWeight.w300, + ), + ), + ]), + ), + ), + ], + ), + ), + trailing: Container( + margin: const EdgeInsets.only(right: 0.0), + padding: const EdgeInsets.only(right: 4.0), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + printDuration(baseItem.runTimeTicksDuration(), + leadingZeroes: false), + semanticsLabel: durationLabelString, + textAlign: TextAlign.end, + style: TextStyle( + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + Semantics( + excludeSemantics: true, + child: AddToPlaylistButton( + item: baseItem, + size: 24, + visualDensity: const VisualDensity( + horizontal: -4, + ), + ), + ), + if (allowReorder) + FutureBuilder( + future: listIndex, + builder: (context, snapshot) { + return ReorderableDragStartListener( + index: snapshot.data ?? + 0, // will briefly use 0 as index, but should resolve quickly enough for user not to notice + child: Padding( + padding: const EdgeInsets.only(left: 6.0), + child: Icon( + TablerIcons.grip_horizontal, + color: + Theme.of(context).textTheme.bodyMedium?.color ?? + Colors.white, + size: 28.0, + weight: 1.5, + ), + ), + ); + }), + ], + ), + ), + onTap: onTap, + ), + ); + } +} diff --git a/lib/components/LayoutSettingsScreen/hide_song_artists_if_same_as_album_artists_selector.dart b/lib/components/LayoutSettingsScreen/hide_song_artists_if_same_as_album_artists_selector.dart deleted file mode 100644 index d7db1825b..000000000 --- a/lib/components/LayoutSettingsScreen/hide_song_artists_if_same_as_album_artists_selector.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:hive/hive.dart'; - -import '../../models/finamp_models.dart'; -import '../../services/finamp_settings_helper.dart'; - -class HideSongArtistsIfSameAsAlbumArtistsSelector extends StatelessWidget { - const HideSongArtistsIfSameAsAlbumArtistsSelector({Key? key}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder>( - valueListenable: FinampSettingsHelper.finampSettingsListener, - builder: (_, box, __) { - return SwitchListTile.adaptive( - title: Text(AppLocalizations.of(context)! - .hideSongArtistsIfSameAsAlbumArtists), - subtitle: Text(AppLocalizations.of(context)! - .hideSongArtistsIfSameAsAlbumArtistsSubtitle), - value: FinampSettingsHelper - .finampSettings.hideSongArtistsIfSameAsAlbumArtists, - onChanged: (value) => - FinampSettingsHelper.setHideSongArtistsIfSameAsAlbumArtists( - value), - ); - }, - ); - } -} diff --git a/lib/components/MusicScreen/alphabet_item_list.dart b/lib/components/MusicScreen/alphabet_item_list.dart index f7da9d606..5cbf5c770 100644 --- a/lib/components/MusicScreen/alphabet_item_list.dart +++ b/lib/components/MusicScreen/alphabet_item_list.dart @@ -93,8 +93,7 @@ class _AlphabetListState extends State { children: List.generate( alphabet.length, (x) => Container( - padding: - const EdgeInsets.symmetric(horizontal: 10, vertical: 0), + padding: const EdgeInsets.only(right: 6.0), height: _letterHeight, child: FittedBox( child: Text( @@ -126,10 +125,8 @@ class _AlphabetListState extends State { // Disable default scrollbar ScrollConfiguration( behavior: const FinampScrollBehavior(scrollbars: false), - child: MediaQuery( - data: mediaQuery.copyWith( - padding: mediaQuery.padding.copyWith( - right: mediaQuery.padding.right + _letterHeight)), + child: Padding( + padding: const EdgeInsets.only(right: 22.0), child: widget.child, )), if (_currentSelected != null && _displayPreview) diff --git a/lib/components/MusicScreen/music_screen_tab_view.dart b/lib/components/MusicScreen/music_screen_tab_view.dart index dffe0cd29..c7014568f 100644 --- a/lib/components/MusicScreen/music_screen_tab_view.dart +++ b/lib/components/MusicScreen/music_screen_tab_view.dart @@ -19,7 +19,7 @@ import '../../models/jellyfin_models.dart'; import '../../services/downloads_service.dart'; import '../../services/finamp_settings_helper.dart'; import '../../services/jellyfin_api_helper.dart'; -import '../AlbumScreen/song_list_tile.dart'; +import '../AlbumScreen/track_list_tile.dart'; import '../first_page_progress_indicator.dart'; import '../global_snackbar.dart'; import '../new_page_progress_indicator.dart'; @@ -374,12 +374,13 @@ class _MusicScreenTabViewState extends State controller: controller, index: index, child: widget.tabContentType == TabContentType.songs - ? SongListTile( + ? TrackListTile( key: ValueKey(item.id), item: item, isSong: true, index: Future.value(index), isShownInSearch: widget.searchTerm != null, + allowDismiss: false, ) : AlbumItem( key: ValueKey(item.id), diff --git a/lib/components/PlaybackHistoryScreen/playback_history_list.dart b/lib/components/PlaybackHistoryScreen/playback_history_list.dart index cc0dd3792..cdeb48d9c 100644 --- a/lib/components/PlaybackHistoryScreen/playback_history_list.dart +++ b/lib/components/PlaybackHistoryScreen/playback_history_list.dart @@ -1,3 +1,5 @@ +import 'package:finamp/components/AddToPlaylistScreen/add_to_playlist_list.dart'; +import 'package:finamp/components/AlbumScreen/track_list_tile.dart'; import 'package:finamp/components/global_snackbar.dart'; import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/services/audio_service_helper.dart'; @@ -32,39 +34,19 @@ class PlaybackHistoryList extends StatelessWidget { return CustomScrollView( // use nested SliverList.builder()s to show history items grouped by date - slivers: groupedHistory.map((group) { + slivers: groupedHistory.indexed.map((indexedGroup) { + final groupIndex = indexedGroup.$1; + final group = indexedGroup.$2; return SliverList( delegate: SliverChildBuilderDelegate( (context, index) { final actualIndex = group.value.length - index - 1; - final historyItem = Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: PlaybackHistoryListTile( - actualIndex: actualIndex, - item: group.value[actualIndex], - audioServiceHelper: audioServiceHelper, - onTap: () { - GlobalSnackbar.message( - (scaffold) => AppLocalizations.of(context)! - .startingInstantMix, - isConfirmation: true, - ); - - audioServiceHelper - .startInstantMixForItem( - jellyfin_models.BaseItemDto.fromJson(group - .value[actualIndex] - .item - .item - .extras?["itemJson"])) - .catchError((e) { - GlobalSnackbar.error(e); - }); - }, - ), + final historyItem = TrackListTile( + index: Future.value(actualIndex), + item: group.value[actualIndex].item.baseItem!, + highlightCurrentTrack: groupIndex == 0 && + index == 0, // only highlight first track ); final now = DateTime.now(); diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 8deb199c0..1a6d4e1dd 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -1,9 +1,11 @@ import 'dart:async'; import 'package:audio_service/audio_service.dart'; +import 'package:finamp/components/AlbumScreen/track_list_tile.dart'; import 'package:finamp/components/AlbumScreen/song_menu.dart'; import 'package:finamp/components/Buttons/simple_button.dart'; import 'package:finamp/components/AddToPlaylistScreen/add_to_playlist_button.dart'; +import 'package:finamp/components/print_duration.dart'; import 'package:finamp/main.dart'; import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/screens/blurred_player_screen_background.dart'; @@ -28,7 +30,6 @@ import '../../services/process_artist.dart'; import '../../services/queue_service.dart'; import '../album_image.dart'; import '../themed_bottom_sheet.dart'; -import 'queue_list_item.dart'; import 'queue_source_helper.dart'; class _QueueListStreamState { @@ -465,16 +466,17 @@ class _PreviousTracksListState extends State final item = _previousTracks![index]; final actualIndex = index; final indexOffset = -((_previousTracks?.length ?? 0) - index); - return QueueListItem( + return QueueListTile( key: ValueKey(item.id), - item: item, - listIndex: index, + item: item.baseItem!, + listIndex: Future.value(index), actualIndex: actualIndex, indexOffset: indexOffset, - subqueue: _previousTracks!, + isInPlaylist: queueItemInPlaylist(item), + parentItem: item.source.item, allowReorder: _queueService.playbackOrder == FinampPlaybackOrder.linear, - onTap: () async { + onTap: (bool playable) async { FeedbackHelper.feedback(FeedbackType.selection); await _queueService.skipByOffset(indexOffset); scrollToKey( @@ -482,7 +484,6 @@ class _PreviousTracksListState extends State duration: const Duration(milliseconds: 500)); }, isCurrentTrack: false, - isPreviousTrack: true, ); }, ); @@ -521,7 +522,7 @@ class _NextUpTracksListState extends State { _nextUp ??= snapshot.data!.nextUp; return SliverPadding( - padding: const EdgeInsets.only(top: 0.0, left: 4.0, right: 4.0), + padding: const EdgeInsets.only(top: 0.0, left: 8.0, right: 8.0), sliver: SliverReorderableList( autoScrollerVelocityScalar: 20.0, onReorder: (oldIndex, newIndex) { @@ -557,14 +558,17 @@ class _NextUpTracksListState extends State { final item = _nextUp![index]; final actualIndex = index; final indexOffset = index + 1; - return QueueListItem( + return QueueListTile( key: ValueKey(item.id), - item: item, - listIndex: index, + item: item.baseItem!, + listIndex: Future.value(index), actualIndex: actualIndex, indexOffset: indexOffset, - subqueue: _nextUp!, - onTap: () async { + isInPlaylist: queueItemInPlaylist(item), + parentItem: item.source.item, + allowReorder: _queueService.playbackOrder == + FinampPlaybackOrder.linear, + onTap: (bool playable) async { FeedbackHelper.feedback(FeedbackType.selection); await _queueService.skipByOffset(indexOffset); scrollToKey( @@ -648,16 +652,17 @@ class _QueueTracksListState extends State { final actualIndex = index; final indexOffset = index + _nextUp!.length + 1; - return QueueListItem( + return QueueListTile( key: ValueKey(item.id), - item: item, - listIndex: index, + item: item.baseItem!, + listIndex: Future.value(index), actualIndex: actualIndex, indexOffset: indexOffset, - subqueue: _queue!, + isInPlaylist: queueItemInPlaylist(item), + parentItem: item.source.item, allowReorder: _queueService.playbackOrder == FinampPlaybackOrder.linear, - onTap: () async { + onTap: (bool playable) async { FeedbackHelper.feedback(FeedbackType.selection); await _queueService.skipByOffset(indexOffset); scrollToKey( @@ -1049,17 +1054,23 @@ class QueueSectionHeader extends StatelessWidget { builder: (context, snapshot) { if (snapshot.hasData) { var remaining = snapshot.data!.remainingDuration; - var remainText = AppLocalizations.of(context)! - .remainingDuration( - remaining.inHours.toString(), - (remaining.inMinutes % 60) - .toString() - .padLeft(2, '0')); + var remainText = printDuration(remaining, + leadingZeroes: false); + final remainingLabelFullHours = + (remaining.inHours); + final remainingLabelFullMinutes = + (remaining.inMinutes) % 60; + final remainingLabelSeconds = + (remaining.inSeconds) % 60; + final remainingLabelString = + "${remainingLabelFullHours > 0 ? "$remainingLabelFullHours ${AppLocalizations.of(context)!.hours} " : ""}${remainingLabelFullMinutes > 0 ? "$remainingLabelFullMinutes ${AppLocalizations.of(context)!.minutes} " : ""}$remainingLabelSeconds ${AppLocalizations.of(context)!.seconds}"; return Padding( padding: const EdgeInsets.only( top: 4.0, right: 8.0), child: Text( - "${snapshot.data!.currentTrackIndex} / ${snapshot.data!.trackCount} ($remainText)")); + "${snapshot.data!.currentTrackIndex} / ${snapshot.data!.trackCount} (${AppLocalizations.of(context)!.remainingDuration(remainText)})", + semanticsLabel: + "${AppLocalizations.of(context)!.trackCountTooltip(snapshot.data!.currentTrackIndex, snapshot.data!.trackCount)} (${AppLocalizations.of(context)!.remainingDuration(remainingLabelString)})")); } return const SizedBox.shrink(); }), diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart deleted file mode 100644 index 8140c065a..000000000 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'package:finamp/components/AlbumScreen/song_menu.dart'; -import 'package:finamp/components/PlayerScreen/queue_source_helper.dart'; -import 'package:finamp/components/album_image.dart'; -import 'package:finamp/models/finamp_models.dart'; -import 'package:finamp/models/jellyfin_models.dart' as jellyfin_models; -import 'package:finamp/services/feedback_helper.dart'; -import 'package:finamp/services/finamp_settings_helper.dart'; -import 'package:finamp/services/process_artist.dart'; -import 'package:finamp/services/queue_service.dart'; -import 'package:flutter/material.dart' hide ReorderableList; -import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; -import 'package:flutter_vibrate/flutter_vibrate.dart'; -import 'package:get_it/get_it.dart'; - -import '../../services/theme_provider.dart'; - -class QueueListItem extends StatefulWidget { - final FinampQueueItem item; - final int listIndex; - final int actualIndex; - final int indexOffset; - final List subqueue; - final bool isCurrentTrack; - final bool isPreviousTrack; - final bool allowReorder; - final void Function() onTap; - - const QueueListItem({ - super.key, - required this.item, - required this.listIndex, - required this.actualIndex, - required this.indexOffset, - required this.subqueue, - required this.onTap, - this.allowReorder = true, - this.isCurrentTrack = false, - this.isPreviousTrack = false, - }); - @override - State createState() => _QueueListItemState(); -} - -class _QueueListItemState extends State - with AutomaticKeepAliveClientMixin { - final _queueService = GetIt.instance(); - - @override - bool get wantKeepAlive => true; - - FinampTheme? _menuTheme; - - @override - void dispose() { - _menuTheme?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - super.build(context); - - jellyfin_models.BaseItemDto baseItem = jellyfin_models.BaseItemDto.fromJson( - widget.item.item.extras?["itemJson"]); - - final cardBackground = Theme.of(context).brightness == Brightness.dark - ? const Color.fromRGBO(255, 255, 255, 0.075) - : const Color.fromRGBO(255, 255, 255, 0.125); - - void menuCallback() { - var currentTrack = jellyfin_models.BaseItemDto.fromJson( - _queueService.getCurrentTrack()?.item.extras?["itemJson"]); - showModalSongMenu( - context: context, - item: baseItem, - usePlayerTheme: widget.item.baseItem?.blurHash != null && - widget.item.baseItem?.blurHash == currentTrack.blurHash, - themeProvider: _menuTheme, - isInPlaylist: queueItemInPlaylist(widget.item), - parentItem: widget.item.source.item, - confirmPlaylistRemoval: true, - ); - } - - return Dismissible( - key: Key(widget.item.id), - direction: FinampSettingsHelper.finampSettings.disableGesture - ? DismissDirection.none - : DismissDirection.horizontal, - onDismissed: (direction) async { - FeedbackHelper.feedback(FeedbackType.impact); - await _queueService.removeAtOffset(widget.indexOffset); - setState(() {}); - }, - child: GestureDetector( - onTapDown: (_) { - _menuTheme?.calculate(Theme.of(context).brightness); - }, - onLongPressStart: (details) => menuCallback(), - onSecondaryTapDown: (details) => menuCallback(), - child: Opacity( - opacity: widget.isPreviousTrack ? 0.8 : 1.0, - child: Card( - color: cardBackground, - elevation: 0, - margin: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 5.0), - clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - child: ListTile( - visualDensity: VisualDensity.standard, - minVerticalPadding: 0.0, - horizontalTitleGap: 10.0, - contentPadding: const EdgeInsets.symmetric( - vertical: 0.0, horizontal: 0.0), - tileColor: widget.isCurrentTrack - ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) - : null, - leading: AlbumImage( - item: widget.item.item.extras?["itemJson"] == null - ? null - : jellyfin_models.BaseItemDto.fromJson( - widget.item.item.extras?["itemJson"]), - borderRadius: BorderRadius.zero, - themeCallback: (x) => _menuTheme = x, - ), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(0.0), - child: Text( - widget.item.item.title, - style: widget.isCurrentTrack - ? TextStyle( - color: - Theme.of(context).colorScheme.secondary, - fontSize: 16, - fontWeight: FontWeight.w400, - overflow: TextOverflow.ellipsis) - : null, - overflow: TextOverflow.ellipsis, - ), - ), - Padding( - padding: const EdgeInsets.only(top: 6.0), - child: Text( - processArtist(widget.item.item.artist, context), - style: TextStyle( - color: Theme.of(context) - .textTheme - .bodyMedium! - .color!, - fontSize: 13, - fontWeight: FontWeight.w300, - overflow: TextOverflow.ellipsis), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - trailing: Container( - margin: const EdgeInsets.only(right: 8.0), - padding: const EdgeInsets.only(right: 6.0), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "${widget.item.item.duration?.inMinutes.toString()}:${((widget.item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - textAlign: TextAlign.end, - style: TextStyle( - color: Theme.of(context).textTheme.bodySmall?.color, - ), - ), - if (FinampSettingsHelper.finampSettings.disableGesture) - IconButton( - padding: const EdgeInsets.only(left: 6.0), - visualDensity: VisualDensity.compact, - icon: const Icon( - TablerIcons.x, - color: Colors.white, - weight: 1.5, - ), - iconSize: 24.0, - onPressed: () async { - FeedbackHelper.feedback(FeedbackType.light); - await _queueService - .removeAtOffset(widget.indexOffset); - }, - ), - if (widget.allowReorder) - ReorderableDragStartListener( - index: widget.listIndex, - child: Padding( - padding: const EdgeInsets.only(left: 6.0), - child: Icon( - TablerIcons.grip_horizontal, - color: Theme.of(context) - .textTheme - .bodyMedium - ?.color ?? - Colors.white, - size: 28.0, - weight: 1.5, - ), - ), - ), - ], - ), - ), - onTap: widget.onTap, - )), - )), - ); - } -} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ebd8b1c7b..2173aedf6 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -635,6 +635,20 @@ "@timeFractionTooltip": { "description": "Tooltip and accessibility label for the track progress. {currentTime} is the current position within the track, as a translated string like '2 minutes 40 seconds', and {totalTime} is the total duration of the track, also a translated string." }, + "trackCountTooltip": "Track {currentTrackIndex} of {totalTrackCount}", + "@trackCountTooltip": { + "description": "Tooltip and accessibility label for the queue progress. {currentTrackIndex} and {totalTrackCount} are both numbers", + "placeholders": { + "currentTrackIndex": { + "type": "int", + "example": "10" + }, + "totalTrackCount": { + "type": "int", + "example": "13" + } + } + }, "invalidNumber": "Invalid Number", "@invalidNumber": {}, "sleepTimerTooltip": "Sleep timer", @@ -1557,17 +1571,15 @@ } } }, - "remainingDuration": "{hoursRemaining, select, 0{} other{{hoursRemaining}h }}{minutesRemaining}m remaining", + "remainingDuration": "{duration} remaining", "@remainingDuration": { + "description": "Displays duration of unplayed tracks. {duration} is a pre-formatted string.", "placeholders": { - "hoursRemaining": { - "type": "String" - }, - "minutesRemaining": { - "type": "String" + "duration": { + "type": "String", + "example": "36:23" } - }, - "description": "Displays duration of unplayed tracks" + } }, "removeFromPlaylistConfirm": "Remove", "removeFromPlaylistCancel": "Cancel", @@ -1774,6 +1786,18 @@ "@showFeatureChipsToggleSubtitle": { "description": "Subtitle for the setting that controls if the feature chips showing advanced track info are shown on the player screen" }, + "albumScreen": "Album Screen", + "@albumScreen": { + "description": "Name for the view/screen that shows albums" + }, + "showCoversOnAlbumScreenTitle": "Show Album Covers For Tracks", + "@showCoversOnAlbumScreenTitle": { + "description": "Title for the setting that controls if album covers are shown for each track separately on the album screen" + }, + "showCoversOnAlbumScreenSubtitle": "Show album covers for each track separately on the album screen.", + "@showCoversOnAlbumScreenSubtitle": { + "description": "Subtitle for the setting that controls if album covers are shown for each track separately on the album screen" + }, "emptyTopTracksList": "You haven't listened to any track by this artist yet.", "@emptyTopTracksList": { "description": "Message shown as a placeholder when the top tracks list for an artist is empty" diff --git a/lib/main.dart b/lib/main.dart index 38aefc954..85ff783f8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:audio_session/audio_session.dart'; import 'package:background_downloader/background_downloader.dart'; import 'package:finamp/color_schemes.g.dart'; import 'package:finamp/gen/assets.gen.dart'; +import 'package:finamp/screens/album_settings_screen.dart'; import 'package:finamp/screens/downloads_settings_screen.dart'; import 'package:finamp/screens/interaction_settings_screen.dart'; import 'package:finamp/screens/login_screen.dart'; @@ -524,6 +525,8 @@ class _FinampState extends ConsumerState with WindowListener { const LyricsSettingsScreen(), LanguageSelectionScreen.routeName: (context) => const LanguageSelectionScreen(), + AlbumSettingsScreen.routeName: (context) => + const AlbumSettingsScreen(), }, initialRoute: SplashScreen.routeName, navigatorObservers: [ diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index f5068b6fe..c9a6b0c34 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -80,7 +80,6 @@ const _showTextOnGridView = true; const _sleepTimerSeconds = 1800; // 30 Minutes const _useCoverAsBackground = true; const _playerScreenCoverMinimumPadding = 1.5; -const _hideSongArtistsIfSameAsAlbumArtists = true; const _showArtistsTopSongs = true; const _disableGesture = false; const _showFastScroller = true; @@ -128,6 +127,7 @@ const _featureChipsConfigurationDefault = FinampFeatureChipType.size, FinampFeatureChipType.normalizationGain, ]); +const _showCoversOnAlbumScreenDefault = false; @HiveType(typeId: 28) class FinampSettings { @@ -160,8 +160,6 @@ class FinampSettings { required this.downloadLocationsMap, this.useCoverAsBackground = _useCoverAsBackground, this.playerScreenCoverMinimumPadding = _playerScreenCoverMinimumPadding, - this.hideSongArtistsIfSameAsAlbumArtists = - _hideSongArtistsIfSameAsAlbumArtists, this.showArtistsTopSongs = _showArtistsTopSongs, this.bufferDurationSeconds = _bufferDurationSeconds, required this.tabSortBy, @@ -212,9 +210,10 @@ class FinampSettings { _showSeekControlsOnMediaNotificationDefault, this.keepScreenOnOption = _keepScreenOnOption, this.keepScreenOnWhilePluggedIn = _keepScreenOnWhilePluggedIn, + this.featureChipsConfiguration = _featureChipsConfigurationDefault, + this.showCoversOnAlbumScreen = _showCoversOnAlbumScreenDefault, this.hasDownloadedPlaylistInfo = _hasDownloadedPlaylistInfoDefault, - this.transcodingSegmentContainer = _defaultTranscodingSegmentContainer, - this.featureChipsConfiguration = _featureChipsConfigurationDefault}); + this.transcodingSegmentContainer = _defaultTranscodingSegmentContainer}); @HiveField(0, defaultValue: _isOfflineDefault) bool isOffline; @@ -281,10 +280,6 @@ class FinampSettings { @HiveField(16, defaultValue: _useCoverAsBackground) bool useCoverAsBackground = _useCoverAsBackground; - @HiveField(17, defaultValue: _hideSongArtistsIfSameAsAlbumArtists) - bool hideSongArtistsIfSameAsAlbumArtists = - _hideSongArtistsIfSameAsAlbumArtists; - @HiveField(18, defaultValue: _bufferDurationSeconds) int bufferDurationSeconds; @@ -460,6 +455,9 @@ class FinampSettings { @HiveField(76, defaultValue: _featureChipsConfigurationDefault) FinampFeatureChipsConfiguration featureChipsConfiguration; + @HiveField(77, defaultValue: _showCoversOnAlbumScreenDefault) + bool showCoversOnAlbumScreen; + static Future create() async { final downloadLocation = await DownloadLocation.create( name: "Internal Storage", diff --git a/lib/models/finamp_models.g.dart b/lib/models/finamp_models.g.dart index 15d28fa16..ba10b5632 100644 --- a/lib/models/finamp_models.g.dart +++ b/lib/models/finamp_models.g.dart @@ -101,8 +101,6 @@ class FinampSettingsAdapter extends TypeAdapter { useCoverAsBackground: fields[16] == null ? true : fields[16] as bool, playerScreenCoverMinimumPadding: fields[48] == null ? 1.5 : fields[48] as double, - hideSongArtistsIfSameAsAlbumArtists: - fields[17] == null ? true : fields[17] as bool, showArtistsTopSongs: fields[54] == null ? true : fields[54] as bool, bufferDurationSeconds: fields[18] == null ? 600 : fields[18] as int, tabSortBy: fields[20] == null @@ -185,11 +183,6 @@ class FinampSettingsAdapter extends TypeAdapter { : fields[72] as KeepScreenOnOption, keepScreenOnWhilePluggedIn: fields[73] == null ? true : fields[73] as bool, - hasDownloadedPlaylistInfo: - fields[74] == null ? false : fields[74] as bool, - transcodingSegmentContainer: fields[75] == null - ? FinampSegmentContainer.fragmentedMp4 - : fields[75] as FinampSegmentContainer, featureChipsConfiguration: fields[76] == null ? const FinampFeatureChipsConfiguration(enabled: true, features: [ FinampFeatureChipType.playCount, @@ -203,6 +196,12 @@ class FinampSettingsAdapter extends TypeAdapter { FinampFeatureChipType.normalizationGain ]) : fields[76] as FinampFeatureChipsConfiguration, + showCoversOnAlbumScreen: fields[77] == null ? false : fields[77] as bool, + hasDownloadedPlaylistInfo: + fields[74] == null ? false : fields[74] as bool, + transcodingSegmentContainer: fields[75] == null + ? FinampSegmentContainer.fragmentedMp4 + : fields[75] as FinampSegmentContainer, ) ..disableGesture = fields[19] == null ? false : fields[19] as bool ..showFastScroller = fields[25] == null ? true : fields[25] as bool @@ -247,8 +246,6 @@ class FinampSettingsAdapter extends TypeAdapter { ..write(obj.downloadLocationsMap) ..writeByte(16) ..write(obj.useCoverAsBackground) - ..writeByte(17) - ..write(obj.hideSongArtistsIfSameAsAlbumArtists) ..writeByte(18) ..write(obj.bufferDurationSeconds) ..writeByte(19) @@ -362,7 +359,9 @@ class FinampSettingsAdapter extends TypeAdapter { ..writeByte(75) ..write(obj.transcodingSegmentContainer) ..writeByte(76) - ..write(obj.featureChipsConfiguration); + ..write(obj.featureChipsConfiguration) + ..writeByte(77) + ..write(obj.showCoversOnAlbumScreen); } @override diff --git a/lib/screens/album_settings_screen.dart b/lib/screens/album_settings_screen.dart new file mode 100644 index 000000000..c55593ad9 --- /dev/null +++ b/lib/screens/album_settings_screen.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:hive/hive.dart'; + +import '../models/finamp_models.dart'; +import '../services/finamp_settings_helper.dart'; + +class AlbumSettingsScreen extends StatelessWidget { + const AlbumSettingsScreen({super.key}); + + static const routeName = "/settings/layout/album"; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.albumScreen), + ), + body: ListView( + children: const [ + ShowCoversOnAlbumScreenToggle(), + ], + ), + ); + } +} + +class ShowCoversOnAlbumScreenToggle extends StatelessWidget { + const ShowCoversOnAlbumScreenToggle({super.key}); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder>( + valueListenable: FinampSettingsHelper.finampSettingsListener, + builder: (context, box, child) { + bool? showCoversOnAlbumScreen = + box.get("FinampSettings")?.showCoversOnAlbumScreen; + + return SwitchListTile.adaptive( + title: + Text(AppLocalizations.of(context)!.showCoversOnAlbumScreenTitle), + subtitle: Text( + AppLocalizations.of(context)!.showCoversOnAlbumScreenSubtitle), + value: showCoversOnAlbumScreen ?? false, + onChanged: showCoversOnAlbumScreen == null + ? null + : (value) { + FinampSettings finampSettingsTemp = + box.get("FinampSettings")!; + finampSettingsTemp.showCoversOnAlbumScreen = value; + box.put("FinampSettings", finampSettingsTemp); + }, + ); + }, + ); + } +} diff --git a/lib/screens/layout_settings_screen.dart b/lib/screens/layout_settings_screen.dart index 398e60eb9..b1f2a1ba6 100644 --- a/lib/screens/layout_settings_screen.dart +++ b/lib/screens/layout_settings_screen.dart @@ -1,3 +1,4 @@ +import 'package:finamp/screens/album_settings_screen.dart'; import 'package:finamp/screens/customization_settings_screen.dart'; import 'package:finamp/components/LayoutSettingsScreen/show_artists_top_songs.dart'; import 'package:finamp/screens/player_settings_screen.dart'; @@ -9,7 +10,6 @@ import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import '../components/LayoutSettingsScreen/content_grid_view_cross_axis_count_list_tile.dart'; import '../components/LayoutSettingsScreen/content_view_type_dropdown_list_tile.dart'; -import '../components/LayoutSettingsScreen/hide_song_artists_if_same_as_album_artists_selector.dart'; import '../components/LayoutSettingsScreen/show_artist_chip_image_toggle.dart'; import '../components/LayoutSettingsScreen/show_text_on_grid_view_selector.dart'; import '../components/LayoutSettingsScreen/theme_selector.dart'; @@ -53,6 +53,12 @@ class LayoutSettingsScreen extends StatelessWidget { onTap: () => Navigator.of(context) .pushNamed(LyricsSettingsScreen.routeName), ), + ListTile( + leading: const Icon(TablerIcons.disc), + title: Text(AppLocalizations.of(context)!.albumScreen), + onTap: () => Navigator.of(context) + .pushNamed(AlbumSettingsScreen.routeName), + ), ListTile( leading: const Icon(Icons.tab), title: Text(AppLocalizations.of(context)!.tabs), @@ -73,7 +79,6 @@ class LayoutSettingsScreen extends StatelessWidget { const ShowArtistChipImageToggle(), const ShowArtistsTopSongsSelector(), const AllowSplitScreenSwitch(), - const HideSongArtistsIfSameAsAlbumArtistsSelector(), const ShowProgressOnNowPlayingBarToggle(), ], ), diff --git a/lib/screens/playback_history_screen.dart b/lib/screens/playback_history_screen.dart index 05659bd32..8cebcbeba 100644 --- a/lib/screens/playback_history_screen.dart +++ b/lib/screens/playback_history_screen.dart @@ -3,6 +3,8 @@ import 'package:finamp/components/PlaybackHistoryScreen/share_offline_listens_bu import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../components/now_playing_bar.dart'; + class PlaybackHistoryScreen extends StatelessWidget { const PlaybackHistoryScreen({Key? key}) : super(key: key); @@ -23,10 +25,10 @@ class PlaybackHistoryScreen extends StatelessWidget { ], ), body: const Padding( - padding: EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0, bottom: 0.0), + padding: EdgeInsets.only(left: 0.0, right: 0.0, top: 16.0, bottom: 0.0), child: PlaybackHistoryList(), ), - //bottomNavigationBar: const NowPlayingBar(), + bottomNavigationBar: const NowPlayingBar(), ); } } diff --git a/lib/services/finamp_settings_helper.dart b/lib/services/finamp_settings_helper.dart index e860d4f6b..d0da4c15c 100644 --- a/lib/services/finamp_settings_helper.dart +++ b/lib/services/finamp_settings_helper.dart @@ -216,15 +216,6 @@ class FinampSettingsHelper { .put("FinampSettings", finampSettingsTemp); } - static void setHideSongArtistsIfSameAsAlbumArtists( - bool hideSongArtistsIfSameAsAlbumArtists) { - FinampSettings finampSettingsTemp = finampSettings; - finampSettingsTemp.hideSongArtistsIfSameAsAlbumArtists = - hideSongArtistsIfSameAsAlbumArtists; - Hive.box("FinampSettings") - .put("FinampSettings", finampSettingsTemp); - } - static void setShowArtistsTopSongs(bool showArtistsTopSongs) { FinampSettings finampSettingsTemp = finampSettings; finampSettingsTemp.showArtistsTopSongs = showArtistsTopSongs; diff --git a/pubspec.lock b/pubspec.lock index 3f668f3d0..7e66c8afc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -964,14 +964,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" - mini_music_visualizer: - dependency: "direct main" - description: - name: mini_music_visualizer - sha256: "779a957424ce9a09cc00989a8cf9b7541ec22316d9781a43e701afa6acacf274" - url: "https://pub.dev" - source: hosted - version: "1.1.4" msix: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index b7b6eeaad..28e91391f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,7 +90,6 @@ dependencies: riverpod_annotation: ^2.6.1 locale_names: ^1.1.1 flutter_vibrate: ^1.3.0 - mini_music_visualizer: ^1.0.2 flutter_cache_manager: ^3.4.1 # fix not showing elipses when maxLines==1 balanced_text: