From df16ebbd42b387d10d72c486174b87e0d7d38329 Mon Sep 17 00:00:00 2001 From: Komodo <45665554+Komodo5197@users.noreply.github.com> Date: Sat, 30 Mar 2024 02:51:02 -0400 Subject: [PATCH] Song menu tweaks. --- lib/components/AlbumScreen/song_menu.dart | 679 ++++++++++++---------- 1 file changed, 371 insertions(+), 308 deletions(-) diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index 6bc5bed6d..6c184bbc8 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -9,6 +9,7 @@ import 'package:finamp/screens/blurred_player_screen_background.dart'; import 'package:finamp/services/music_player_background_task.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.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'; @@ -136,15 +137,15 @@ class _SongMenuState extends State { /// Sets the item's favourite on the Jellyfin server. Future toggleFavorite() async { try { - final isOffline = FinampSettingsHelper.finampSettings.isOffline; if (isOffline) { Vibrate.feedback(FeedbackType.error); - GlobalSnackbar.message((context) => AppLocalizations.of(context)!.notAvailableInOfflineMode); + GlobalSnackbar.message((context) => + AppLocalizations.of(context)!.notAvailableInOfflineMode); return; } - + final currentTrack = _queueService.getCurrentTrack(); if (isBaseItemInQueueItem(widget.item, currentTrack)) { setFavourite(currentTrack!, context); @@ -176,7 +177,7 @@ class _SongMenuState extends State { widget.item.userData!.isFavorite = !widget.item.userData!.isFavorite; }); Vibrate.feedback(FeedbackType.error); - errorSnackbar(e, context); + GlobalSnackbar.error(e); } } @@ -197,8 +198,8 @@ class _SongMenuState extends State { return Stack(children: [ DraggableScrollableSheet( snap: true, - snapSizes: widget.showPlaybackControls ? const [0.6] : const [0.45], - initialChildSize: widget.showPlaybackControls ? 0.6 : 0.45, + initialChildSize: 0.9, + maxChildSize: 0.9, minChildSize: 0.3, expand: false, builder: (context, scrollController) { @@ -242,7 +243,8 @@ class _SongMenuState extends State { pinned: true, ), if (widget.showPlaybackControls) - StreamBuilder( + SongMenuMask( + child: StreamBuilder( stream: Rx.combineLatest2( _queueService.getPlaybackOrderStream(), _queueService.getLoopModeStream(), @@ -367,58 +369,68 @@ class _SongMenuState extends State { ], ); }, - ), - SliverPadding( - padding: const EdgeInsets.only(left: 8.0), - sliver: SliverList( - delegate: SliverChildListDelegate([ - Visibility( - visible: !widget.isOffline, - child: ListTile( - leading: Icon( - Icons.playlist_add, - color: iconColor, + )), + SongMenuMask( + child: SliverPadding( + padding: const EdgeInsets.only(left: 8.0), + sliver: SliverList( + delegate: SliverChildListDelegate([ + Visibility( + visible: !widget.isOffline, + child: ListTile( + leading: Icon( + Icons.playlist_add, + color: iconColor, + ), + title: Text(AppLocalizations.of(context)! + .addToPlaylistTitle), + enabled: !widget.isOffline, + onTap: () { + Navigator.pop(context); // close menu + Navigator.of(context).pushNamed( + AddToPlaylistScreen.routeName, + arguments: widget.item.id); + }, ), - title: Text(AppLocalizations.of(context)! - .addToPlaylistTitle), - enabled: !widget.isOffline, - onTap: () { - Navigator.pop(context); // close menu - Navigator.of(context).pushNamed( - AddToPlaylistScreen.routeName, - arguments: widget.item.id); - }, ), - ), - ListTile( - enabled: !widget.isOffline, - leading: widget.item.userData!.isFavorite - ? Icon( - Icons.favorite, - color: widget.isOffline ? iconColor.withOpacity(0.3) : iconColor, - ) - : Icon( - Icons.favorite_border, - color: widget.isOffline ? iconColor.withOpacity(0.3) : iconColor, - ), - title: Text(widget.item.userData!.isFavorite - ? AppLocalizations.of(context)!.removeFavourite - : AppLocalizations.of(context)!.addFavourite), - onTap: () async { - await toggleFavorite(); - if (mounted) Navigator.pop(context); - }, - ), - Visibility( - visible: _queueService.getQueue().nextUp.isNotEmpty, - child: ListTile( + Visibility( + visible: _queueService.getQueue().nextUp.isNotEmpty, + child: ListTile( + leading: Icon( + TablerIcons.corner_right_down, + color: iconColor, + ), + title: + Text(AppLocalizations.of(context)!.playNext), + onTap: () async { + await _queueService.addNext( + items: [widget.item], + source: QueueItemSource( + type: QueueItemSourceType.nextUp, + name: const QueueItemSourceName( + type: + QueueItemSourceNameType.nextUp), + id: widget.item.id)); + + if (!mounted) return; + + GlobalSnackbar.message( + (context) => AppLocalizations.of(context)! + .confirmPlayNext("track"), + isConfirmation: true); + Navigator.pop(context); + }, + ), + ), + ListTile( leading: Icon( - TablerIcons.corner_right_down, + TablerIcons.corner_right_down_double, color: iconColor, ), - title: Text(AppLocalizations.of(context)!.playNext), + title: + Text(AppLocalizations.of(context)!.addToNextUp), onTap: () async { - await _queueService.addNext( + await _queueService.addToNextUp( items: [widget.item], source: QueueItemSource( type: QueueItemSourceType.nextUp, @@ -428,286 +440,301 @@ class _SongMenuState extends State { if (!mounted) return; - GlobalSnackbar.message((context) => - AppLocalizations.of(context)!.confirmPlayNext("track"), isConfirmation: true); + GlobalSnackbar.message( + (context) => AppLocalizations.of(context)! + .confirmAddToNextUp("track"), + isConfirmation: true); Navigator.pop(context); }, ), - ), - ListTile( - leading: Icon( - TablerIcons.corner_right_down_double, - color: iconColor, - ), - title: - Text(AppLocalizations.of(context)!.addToNextUp), - onTap: () async { - await _queueService.addToNextUp( - items: [widget.item], - source: QueueItemSource( - type: QueueItemSourceType.nextUp, - name: const QueueItemSourceName( - type: QueueItemSourceNameType.nextUp), - id: widget.item.id)); - - if (!mounted) return; - - GlobalSnackbar.message((context) => - AppLocalizations.of(context)!.confirmAddToNextUp("track"), isConfirmation: true); - Navigator.pop(context); - }, - ), - ListTile( - leading: Icon( - TablerIcons.playlist, - color: iconColor, - ), - title: Text(AppLocalizations.of(context)!.addToQueue), - onTap: () async { - await _queueService.addToQueue( - items: [widget.item], - source: QueueItemSource( - type: QueueItemSourceType.queue, - name: const QueueItemSourceName( - type: QueueItemSourceNameType.queue), - id: widget.item.id)); - - if (!mounted) return; - - GlobalSnackbar.message((context) => - AppLocalizations.of(context)!.addedToQueue, isConfirmation: true); - Navigator.pop(context); - }, - ), - Visibility( - visible: widget.isInPlaylist && widget.parentItem != null && !widget.isOffline, - child: ListTile( + ListTile( leading: Icon( - Icons.playlist_remove, + TablerIcons.playlist, color: iconColor, ), - title: Text(AppLocalizations.of(context)! - .removeFromPlaylistTitle), - enabled: widget.isInPlaylist && widget.parentItem != null && !widget.isOffline, + title: + Text(AppLocalizations.of(context)!.addToQueue), onTap: () async { - try { - await _jellyfinApiHelper - .removeItemsFromPlaylist( - playlistId: widget.parentItem!.id, - entryIds: [ - widget.item.playlistItemId! - ]); + await _queueService.addToQueue( + items: [widget.item], + source: QueueItemSource( + type: QueueItemSourceType.queue, + name: const QueueItemSourceName( + type: QueueItemSourceNameType.queue), + id: widget.item.id)); - if (!mounted) return; + if (!mounted) return; - await _jellyfinApiHelper.getItems( - parentItem: await _jellyfinApiHelper - .getItemById(widget.item.parentId!), - sortBy: - "ParentIndexNumber,IndexNumber,SortName", - includeItemTypes: "Audio", - ); + GlobalSnackbar.message( + (context) => AppLocalizations.of(context)! + .addedToQueue, + isConfirmation: true); + Navigator.pop(context); + }, + ), + Visibility( + visible: widget.isInPlaylist && + widget.parentItem != null && + !widget.isOffline, + child: ListTile( + leading: Icon( + Icons.playlist_remove, + color: iconColor, + ), + title: Text(AppLocalizations.of(context)! + .removeFromPlaylistTitle), + enabled: widget.isInPlaylist && + widget.parentItem != null && + !widget.isOffline, + onTap: () async { + try { + await _jellyfinApiHelper + .removeItemsFromPlaylist( + playlistId: widget.parentItem!.id, + entryIds: [ + widget.item.playlistItemId! + ]); - if (!mounted) return; + if (!mounted) return; - if (widget.onRemoveFromList != null) - widget.onRemoveFromList!(); + await _jellyfinApiHelper.getItems( + parentItem: await _jellyfinApiHelper + .getItemById(widget.item.parentId!), + sortBy: + "ParentIndexNumber,IndexNumber,SortName", + includeItemTypes: "Audio", + ); - GlobalSnackbar.message((context) => - AppLocalizations.of(context)!.removedFromPlaylist, isConfirmation: true); - Navigator.pop(context); - } catch (e) { - GlobalSnackbar.error(e); - } - }, - ), - ), - Visibility( - visible: !widget.isOffline, - child: ListTile( - leading: Icon( - Icons.explore, - color: iconColor, - ), - title: - Text(AppLocalizations.of(context)!.instantMix), - enabled: !widget.isOffline, - onTap: () async { - await _audioServiceHelper - .startInstantMixForItem(widget.item); + if (!mounted) return; - if (!mounted) return; + if (widget.onRemoveFromList != null) + widget.onRemoveFromList!(); - GlobalSnackbar.message((context) => - AppLocalizations.of(context)!.startingInstantMix, isConfirmation: true); - Navigator.pop(context); - }, + GlobalSnackbar.message( + (context) => AppLocalizations.of(context)! + .removedFromPlaylist, + isConfirmation: true); + Navigator.pop(context); + } catch (e) { + GlobalSnackbar.error(e); + } + }, + ), ), - ), - Visibility( - visible: widget.canGoToAlbum, - child: ListTile( - leading: Icon( - Icons.album, - color: iconColor, + Visibility( + visible: !widget.isOffline, + child: ListTile( + leading: Icon( + Icons.explore, + color: iconColor, + ), + title: Text( + AppLocalizations.of(context)!.instantMix), + enabled: !widget.isOffline, + onTap: () async { + await _audioServiceHelper + .startInstantMixForItem(widget.item); + + if (!mounted) return; + + GlobalSnackbar.message( + (context) => AppLocalizations.of(context)! + .startingInstantMix, + isConfirmation: true); + Navigator.pop(context); + }, ), - title: - Text(AppLocalizations.of(context)!.goToAlbum), - enabled: widget.canGoToAlbum, - onTap: () async { - late BaseItemDto album; - try { - if (FinampSettingsHelper - .finampSettings.isOffline) { - final downloadsService = - GetIt.instance(); - album = - (await downloadsService.getCollectionInfo( - id: widget.item.albumId!))! - .baseItem!; - } else { - album = await _jellyfinApiHelper - .getItemById(widget.item.albumId!); + ), + Visibility( + visible: isDownloadRequired, + // TODO add some sort of disabled state with tooltip saying to delete the parent + // Need to do on other delete buttons too + // Do we want to try showing lock on right clicks? + // Currently only download or delete are shown. + child: ListTile( + leading: Icon( + Icons.delete_outlined, + color: iconColor, + ), + title: Text( + AppLocalizations.of(context)!.deleteItem), + enabled: !widget.isOffline && isDownloadRequired, + onTap: () async { + var item = DownloadStub.fromItem( + type: DownloadItemType.song, + item: widget.item); + unawaited(downloadsService.deleteDownload( + stub: item)); + if (mounted) { + Navigator.pop(context); } - } catch (e) { - GlobalSnackbar.error(e); - return; - } - if (mounted) { - Navigator.pop(context); - Navigator.of(context).pushNamed( - AlbumScreen.routeName, - arguments: album); - } - }, + }, + ), ), - ), - Visibility( - visible: widget.canGoToArtist, - child: ListTile( - leading: Icon( - Icons.person, - color: iconColor, + Visibility( + visible: !widget.isOffline && !isDownloadRequired, + child: ListTile( + leading: Icon( + Icons.file_download_outlined, + color: iconColor, + ), + title: Text( + AppLocalizations.of(context)!.downloadItem), + enabled: !widget.isOffline && !isDownloadRequired, + onTap: () async { + var item = DownloadStub.fromItem( + type: DownloadItemType.song, + item: widget.item); + await DownloadDialog.show(context, item, null); + if (mounted) { + Navigator.pop(context); + } + }, ), - title: - Text(AppLocalizations.of(context)!.goToArtist), - enabled: widget.canGoToArtist, + ), + ListTile( + enabled: !widget.isOffline, + leading: widget.item.userData!.isFavorite + ? Icon( + Icons.favorite, + color: widget.isOffline + ? iconColor.withOpacity(0.3) + : iconColor, + ) + : Icon( + Icons.favorite_border, + color: widget.isOffline + ? iconColor.withOpacity(0.3) + : iconColor, + ), + title: Text(widget.item.userData!.isFavorite + ? AppLocalizations.of(context)!.removeFavourite + : AppLocalizations.of(context)!.addFavourite), onTap: () async { - late BaseItemDto artist; - try { - if (FinampSettingsHelper - .finampSettings.isOffline) { - final downloadsService = - GetIt.instance(); - artist = - (await downloadsService.getCollectionInfo( - id: widget - .item.artistItems!.first.id))! - .baseItem!; - } else { - artist = await _jellyfinApiHelper.getItemById( - widget.item.artistItems!.first.id); - } - } catch (e) { - GlobalSnackbar.error(e); - return; - } - if (mounted) { - Navigator.pop(context); - Navigator.of(context).pushNamed( - ArtistScreen.routeName, - arguments: artist); - } + await toggleFavorite(); + if (mounted) Navigator.pop(context); }, ), - ), - Visibility( - visible: widget.canGoToGenre, - child: ListTile( - leading: Icon( - Icons.category_outlined, - color: iconColor, - ), - title: - Text(AppLocalizations.of(context)!.goToGenre), - enabled: widget.canGoToGenre, - onTap: () async { - late BaseItemDto genre; - try { - if (FinampSettingsHelper - .finampSettings.isOffline) { - final downloadsService = - GetIt.instance(); - genre = - (await downloadsService.getCollectionInfo( - id: widget - .item.genreItems!.first.id))! - .baseItem!; - } else { - genre = await _jellyfinApiHelper.getItemById( - widget.item.genreItems!.first.id); + Visibility( + visible: widget.canGoToAlbum, + child: ListTile( + leading: Icon( + Icons.album, + color: iconColor, + ), + title: + Text(AppLocalizations.of(context)!.goToAlbum), + enabled: widget.canGoToAlbum, + onTap: () async { + late BaseItemDto album; + try { + if (FinampSettingsHelper + .finampSettings.isOffline) { + final downloadsService = + GetIt.instance(); + album = (await downloadsService + .getCollectionInfo( + id: widget.item.albumId!))! + .baseItem!; + } else { + album = await _jellyfinApiHelper + .getItemById(widget.item.albumId!); + } + } catch (e) { + GlobalSnackbar.error(e); + return; } - } catch (e) { - GlobalSnackbar.error(e); - return; - } - if (mounted) { - Navigator.pop(context); - Navigator.of(context).pushNamed( - ArtistScreen.routeName, - arguments: genre); - } - }, + if (mounted) { + Navigator.pop(context); + Navigator.of(context).pushNamed( + AlbumScreen.routeName, + arguments: album); + } + }, + ), ), - ), - Visibility( - visible: isDownloadRequired, - // TODO add some sort of disabled state with tooltip saying to delete the parent - // Need to do on other delete buttons too - // Do we want to try showing lock on right clicks? - // Currently only download or delete are shown. - child: ListTile( - leading: Icon( - Icons.delete_outlined, - color: iconColor, + Visibility( + visible: widget.canGoToArtist, + child: ListTile( + leading: Icon( + Icons.person, + color: iconColor, + ), + title: Text( + AppLocalizations.of(context)!.goToArtist), + enabled: widget.canGoToArtist, + onTap: () async { + late BaseItemDto artist; + try { + if (FinampSettingsHelper + .finampSettings.isOffline) { + final downloadsService = + GetIt.instance(); + artist = (await downloadsService + .getCollectionInfo( + id: widget.item.artistItems! + .first.id))! + .baseItem!; + } else { + artist = + await _jellyfinApiHelper.getItemById( + widget.item.artistItems!.first.id); + } + } catch (e) { + GlobalSnackbar.error(e); + return; + } + if (mounted) { + Navigator.pop(context); + Navigator.of(context).pushNamed( + ArtistScreen.routeName, + arguments: artist); + } + }, ), - title: - Text(AppLocalizations.of(context)!.deleteItem), - enabled: !widget.isOffline && isDownloadRequired, - onTap: () async { - var item = DownloadStub.fromItem( - type: DownloadItemType.song, - item: widget.item); - unawaited( - downloadsService.deleteDownload(stub: item)); - if (mounted) { - Navigator.pop(context); - } - }, ), - ), - Visibility( - visible: !widget.isOffline && !isDownloadRequired, - child: ListTile( - leading: Icon( - Icons.file_download_outlined, - color: iconColor, + Visibility( + visible: widget.canGoToGenre, + child: ListTile( + leading: Icon( + Icons.category_outlined, + color: iconColor, + ), + title: + Text(AppLocalizations.of(context)!.goToGenre), + enabled: widget.canGoToGenre, + onTap: () async { + late BaseItemDto genre; + try { + if (FinampSettingsHelper + .finampSettings.isOffline) { + final downloadsService = + GetIt.instance(); + genre = (await downloadsService + .getCollectionInfo( + id: widget.item.genreItems! + .first.id))! + .baseItem!; + } else { + genre = + await _jellyfinApiHelper.getItemById( + widget.item.genreItems!.first.id); + } + } catch (e) { + GlobalSnackbar.error(e); + return; + } + if (mounted) { + Navigator.pop(context); + Navigator.of(context).pushNamed( + ArtistScreen.routeName, + arguments: genre); + } + }, ), - title: Text( - AppLocalizations.of(context)!.downloadItem), - enabled: !widget.isOffline && !isDownloadRequired, - onTap: () async { - var item = DownloadStub.fromItem( - type: DownloadItemType.song, - item: widget.item); - await DownloadDialog.show(context, item, null); - if (mounted) { - Navigator.pop(context); - } - }, ), - ), - ]), + ]), + ), ), ) ], @@ -748,7 +775,7 @@ class SongMenuSliverAppBar extends SliverPersistentHeaderDelegate { double get maxExtent => 150; @override - double get minExtent => 100; + double get minExtent => 150; @override bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => @@ -773,8 +800,6 @@ class _SongInfo extends ConsumerStatefulWidget { } class _SongInfoState extends ConsumerState<_SongInfo> { - final _queueService = GetIt.instance(); - VoidCallback? onDispose; bool waitingForTheme = false; @@ -979,3 +1004,41 @@ class PlaybackAction extends StatelessWidget { ); } } + +class SongMenuMask extends SingleChildRenderObjectWidget { + const SongMenuMask({ + super.key, + super.child, + }); + + @override + RenderSongMenuMask createRenderObject(BuildContext context) { + return RenderSongMenuMask(); + } +} + +class RenderSongMenuMask extends RenderProxySliver { + @override + ShaderMaskLayer? get layer => super.layer as ShaderMaskLayer?; + + @override + bool get alwaysNeedsCompositing => child != null; + + @override + void paint(PaintingContext context, Offset offset) { + if (child != null) { + layer ??= ShaderMaskLayer( + shader: const LinearGradient(colors: [ + Color.fromARGB(0, 255, 255, 255), + Color.fromARGB(255, 255, 255, 255) + ], begin: Alignment.topCenter, end: Alignment.bottomCenter) + .createShader(const Rect.fromLTWH(0, 135, 0, 10)), + blendMode: BlendMode.modulate, + maskRect: const Rect.fromLTWH(0, 0, 99999, 150)); + + context.pushLayer(layer!, super.paint, offset); + } else { + layer = null; + } + } +}