From 3b56c78d5ccd2a8fc23b27179e731a9dd908818f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 12 Jun 2023 12:50:58 +0600 Subject: [PATCH] refactor: replace PopupMenuButton widgets with AdaptivePopSheetList --- .../adaptive/adaptive_pop_sheet_list.dart | 78 +++- .../shared/sort_tracks_dropdown.dart | 30 +- .../shared/track_table/track_options.dart | 387 +++++++++--------- .../shared/track_table/tracks_table_view.dart | 96 ++--- 4 files changed, 316 insertions(+), 275 deletions(-) diff --git a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart index 9bd180fc..9a9e9630 100644 --- a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart +++ b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart @@ -42,7 +42,7 @@ class AdaptivePopSheetList extends StatelessWidget { this.borderRadius = const BorderRadius.all(Radius.circular(999)), this.tooltip, }) : assert( - icon != null || child != null, + !(icon != null && child != null), 'Either icon or child must be provided', ); @@ -55,21 +55,14 @@ class AdaptivePopSheetList extends StatelessWidget { return PopupMenuButton( icon: icon, tooltip: tooltip, - child: IgnorePointer(child: child), + child: child == null ? null : IgnorePointer(child: child), itemBuilder: (context) => children .map( (item) => PopupMenuItem( padding: EdgeInsets.zero, - child: ListTile( - enabled: item.enabled, - onTap: () { - item.onTap?.call(); - Navigator.pop(context); - if (item.value != null) { - onSelected?.call(item.value as T); - } - }, - title: item.child, + child: _AdaptivePopSheetListItem( + item: item, + onSelected: onSelected, ), ), ) @@ -90,7 +83,17 @@ class AdaptivePopSheetList extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ if (headings != null) ...[ + Container( + width: 180, + height: 6, + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(999), + ), + ), + const SizedBox(height: 8), ...headings!, + const SizedBox(height: 8), Divider( color: theme.colorScheme.primary, thickness: 0.3, @@ -99,16 +102,9 @@ class AdaptivePopSheetList extends StatelessWidget { ), ], ...children.map( - (item) => ListTile( - onTap: () { - item.onTap?.call(); - Navigator.pop(context); - if (item.value != null) { - onSelected?.call(item.value as T); - } - }, - enabled: item.enabled, - title: item.child, + (item) => _AdaptivePopSheetListItem( + item: item, + onSelected: onSelected, ), ) ], @@ -144,3 +140,41 @@ class AdaptivePopSheetList extends StatelessWidget { ); } } + +class _AdaptivePopSheetListItem extends StatelessWidget { + final PopSheetEntry item; + final ValueChanged? onSelected; + const _AdaptivePopSheetListItem({ + super.key, + required this.item, + this.onSelected, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return InkWell( + borderRadius: BorderRadius.circular(10), + onTap: !item.enabled + ? null + : () { + item.onTap?.call(); + Navigator.pop(context); + if (item.value != null) { + onSelected?.call(item.value as T); + } + }, + child: DefaultTextStyle( + style: TextStyle( + color: item.enabled + ? theme.textTheme.bodyMedium!.color + : theme.textTheme.bodyMedium!.color!.withOpacity(0.5), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: item.child, + ), + ), + ); + } +} diff --git a/lib/components/shared/sort_tracks_dropdown.dart b/lib/components/shared/sort_tracks_dropdown.dart index e3ca166c..be43f27e 100644 --- a/lib/components/shared/sort_tracks_dropdown.dart +++ b/lib/components/shared/sort_tracks_dropdown.dart @@ -25,32 +25,50 @@ class SortTracksDropdown extends StatelessWidget { PopSheetEntry( value: SortBy.none, enabled: value != SortBy.none, - child: Text(context.l10n.none), + child: ListTile( + enabled: value != SortBy.none, + title: Text(context.l10n.none), + ), ), PopSheetEntry( value: SortBy.ascending, enabled: value != SortBy.ascending, - child: Text(context.l10n.sort_a_z), + child: ListTile( + enabled: value != SortBy.ascending, + title: Text(context.l10n.sort_a_z), + ), ), PopSheetEntry( value: SortBy.descending, enabled: value != SortBy.descending, - child: Text(context.l10n.sort_z_a), + child: ListTile( + enabled: value != SortBy.descending, + title: Text(context.l10n.sort_z_a), + ), ), PopSheetEntry( value: SortBy.dateAdded, enabled: value != SortBy.dateAdded, - child: Text(context.l10n.sort_date), + child: ListTile( + enabled: value != SortBy.dateAdded, + title: Text(context.l10n.sort_date), + ), ), PopSheetEntry( value: SortBy.artist, enabled: value != SortBy.artist, - child: Text(context.l10n.sort_artist), + child: ListTile( + enabled: value != SortBy.artist, + title: Text(context.l10n.sort_artist), + ), ), PopSheetEntry( value: SortBy.album, enabled: value != SortBy.album, - child: Text(context.l10n.sort_album), + child: ListTile( + enabled: value != SortBy.album, + title: Text(context.l10n.sort_album), + ), ), ], headings: [ diff --git a/lib/components/shared/track_table/track_options.dart b/lib/components/shared/track_table/track_options.dart index 42340e5f..6e31f840 100644 --- a/lib/components/shared/track_table/track_options.dart +++ b/lib/components/shared/track_table/track_options.dart @@ -7,15 +7,29 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/library/user_local_tracks.dart'; +import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart'; import 'package:spotube/components/shared/heart_button.dart'; -import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/mutations/mutations.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; + +enum TrackOptionValue { + share, + addToPlaylist, + addToQueue, + removeFromPlaylist, + removeFromQueue, + blacklist, + delete, + playNext, + favorite, +} class TrackOptions extends HookConsumerWidget { final Track track; @@ -80,151 +94,62 @@ class TrackOptions extends HookConsumerWidget { playlistId ?? "", ); - final mediaQuery = MediaQuery.of(context); - - final createItems = useCallback( - (BuildContext context) { - if (track is LocalTrack) { - return [ - if (mediaQuery.isSm) ...[ - Text( - track.name!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium, - ), - Divider( - color: Theme.of(context).colorScheme.primary, - thickness: 0.2, - indent: 16, - endIndent: 16, - ), - ], - ListTile( - onTap: () async { - await File((track as LocalTrack).path).delete(); - ref.refresh(localTracksProvider); - if (context.mounted) Navigator.pop(context); - }, - leading: const Icon(SpotubeIcons.trash), - title: Text(context.l10n.delete), - ) - ]; - } - - return [ - if (mediaQuery.isSm) ...[ - Text( - track.name!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium, - ), - Divider( - color: Theme.of(context).colorScheme.primary, - thickness: 0.2, - indent: 16, - endIndent: 16, - ), - ], - if (!playlist.containsTrack(track)) ...[ - ListTile( - onTap: () async { - await playback.addTrack(track); - if (context.mounted) { - scaffoldMessenger.showSnackBar( - SnackBar( - content: Text( - context.l10n.added_track_to_queue(track.name!), - ), - ), - ); - Navigator.pop(context); - } - }, - leading: const Icon(SpotubeIcons.queueAdd), - title: Text(context.l10n.add_to_queue), - ), - ListTile( - onTap: () { - playback.addTracksAtFirst([track]); + return ListTileTheme( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: AdaptivePopSheetList( + onSelected: (value) async { + switch (value) { + case TrackOptionValue.delete: + await File((track as LocalTrack).path).delete(); + ref.refresh(localTracksProvider); + break; + case TrackOptionValue.addToQueue: + await playback.addTrack(track); + if (context.mounted) { scaffoldMessenger.showSnackBar( SnackBar( content: Text( - context.l10n.track_will_play_next(track.name!), + context.l10n.added_track_to_queue(track.name!), ), ), ); - Navigator.pop(context); - }, - leading: const Icon(SpotubeIcons.lightning), - title: Text(context.l10n.play_next), - ), - ] else - ListTile( - onTap: playlist.activeTrack?.id == track.id - ? null - : () { - playback.removeTrack(track.id!); - scaffoldMessenger.showSnackBar( - SnackBar( - content: Text( - context.l10n.removed_track_from_queue( - track.name!, - ), - ), - ), - ); - Navigator.pop(context); - }, - enabled: playlist.activeTrack?.id != track.id, - leading: const Icon(SpotubeIcons.queueRemove), - title: Text(context.l10n.remove_from_queue), - ), - if (favorites.me.hasData) - ListTile( - onTap: () { - favorites.toggleTrackLike.mutate(favorites.isLiked); - Navigator.pop(context); - }, - leading: favorites.isLiked - ? const Icon( - SpotubeIcons.heartFilled, - color: Colors.pink, - ) - : const Icon(SpotubeIcons.heart), - title: Text( - favorites.isLiked - ? context.l10n.remove_from_favorites - : context.l10n.save_as_favorite, - ), - ), - if (auth != null) - ListTile( - onTap: () { - actionAddToPlaylist(context, track); - Navigator.pop(context); - }, - leading: const Icon(SpotubeIcons.playlistAdd), - title: Text(context.l10n.add_to_playlist), - ), - if (userPlaylist && auth != null) - ListTile( - onTap: () { - removingTrack.value = track.uri; - removeTrack.mutate(track.uri!); - Navigator.pop(context); - }, - leading: (removeTrack.isMutating || !removeTrack.hasData) && - removingTrack.value == track.uri - ? const Center( - child: CircularProgressIndicator(), - ) - : const Icon(SpotubeIcons.removeFilled), - title: Text(context.l10n.remove_from_playlist), - ), - ListTile( - onTap: () { + } + break; + case TrackOptionValue.playNext: + playback.addTracksAtFirst([track]); + scaffoldMessenger.showSnackBar( + SnackBar( + content: Text( + context.l10n.track_will_play_next(track.name!), + ), + ), + ); + break; + case TrackOptionValue.removeFromQueue: + playback.removeTrack(track.id!); + scaffoldMessenger.showSnackBar( + SnackBar( + content: Text( + context.l10n.removed_track_from_queue( + track.name!, + ), + ), + ), + ); + break; + case TrackOptionValue.favorite: + favorites.toggleTrackLike.mutate(favorites.isLiked); + break; + case TrackOptionValue.addToPlaylist: + actionAddToPlaylist(context, track); + break; + case TrackOptionValue.removeFromPlaylist: + removingTrack.value = track.uri; + removeTrack.mutate(track.uri!); + break; + case TrackOptionValue.blacklist: if (isBlackListed) { ref.read(BlackListNotifier.provider.notifier).remove( BlacklistedElement.track(track.id!, track.name!), @@ -234,69 +159,139 @@ class TrackOptions extends HookConsumerWidget { BlacklistedElement.track(track.id!, track.name!), ); } - Navigator.pop(context); - }, - leading: const Icon(SpotubeIcons.playlistRemove), - iconColor: !isBlackListed ? Colors.red[400] : null, - textColor: !isBlackListed ? Colors.red[400] : null, + break; + case TrackOptionValue.share: + actionShare(context, track); + break; + } + }, + icon: const Icon(SpotubeIcons.moreHorizontal), + headings: [ + ListTile( + dense: true, + leading: AspectRatio( + aspectRatio: 1, + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: UniversalImage( + path: TypeConversionUtils.image_X_UrlString( + track.album!.images, + placeholder: ImagePlaceholder.albumArt), + fit: BoxFit.cover, + ), + ), + ), title: Text( - isBlackListed - ? context.l10n.remove_from_blacklist - : context.l10n.add_to_blacklist, + track.name!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: Align( + alignment: Alignment.centerLeft, + child: TypeConversionUtils.artists_X_ClickableArtists( + track.artists!, + ), ), ), - ListTile( - onTap: () { - actionShare(context, track); - Navigator.pop(context); - }, - leading: const Icon(SpotubeIcons.share), - title: Text(context.l10n.share), - ) - ]; - }, - [track, playlist, favorites, auth, isBlackListed, mediaQuery], - ); - - if (mediaQuery.isSm) { - return IconButton( - icon: const Icon(SpotubeIcons.moreHorizontal), - onPressed: () { - showModalBottomSheet( - context: context, - builder: (context) => Padding( - padding: const EdgeInsets.all(10.0), - child: ListTileTheme( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + ], + children: switch (track.runtimeType) { + LocalTrack => [ + PopSheetEntry( + value: TrackOptionValue.delete, + child: ListTile( + leading: const Icon(SpotubeIcons.trash), + title: Text(context.l10n.delete), ), - horizontalTitleGap: 5, - child: Column( - mainAxisSize: MainAxisSize.min, - children: createItems(context), + ) + ], + _ => [ + if (!playlist.containsTrack(track)) ...[ + PopSheetEntry( + value: TrackOptionValue.addToQueue, + child: ListTile( + leading: const Icon(SpotubeIcons.queueAdd), + title: Text(context.l10n.add_to_queue), + ), + ), + PopSheetEntry( + value: TrackOptionValue.playNext, + child: ListTile( + leading: const Icon(SpotubeIcons.lightning), + title: Text(context.l10n.play_next), + ), + ), + ] else + PopSheetEntry( + value: TrackOptionValue.removeFromQueue, + enabled: playlist.activeTrack?.id != track.id, + child: ListTile( + enabled: playlist.activeTrack?.id != track.id, + leading: const Icon(SpotubeIcons.queueRemove), + title: Text(context.l10n.remove_from_queue), + ), + ), + if (favorites.me.hasData) + PopSheetEntry( + value: TrackOptionValue.favorite, + child: ListTile( + leading: favorites.isLiked + ? const Icon( + SpotubeIcons.heartFilled, + color: Colors.pink, + ) + : const Icon(SpotubeIcons.heart), + title: Text( + favorites.isLiked + ? context.l10n.remove_from_favorites + : context.l10n.save_as_favorite, + ), + ), + ), + if (auth != null) + PopSheetEntry( + value: TrackOptionValue.addToPlaylist, + child: ListTile( + leading: const Icon(SpotubeIcons.playlistAdd), + title: Text(context.l10n.add_to_playlist), + ), + ), + if (userPlaylist && auth != null) + PopSheetEntry( + value: TrackOptionValue.removeFromPlaylist, + child: ListTile( + leading: (removeTrack.isMutating || !removeTrack.hasData) && + removingTrack.value == track.uri + ? const Center( + child: CircularProgressIndicator(), + ) + : const Icon(SpotubeIcons.removeFilled), + title: Text(context.l10n.remove_from_playlist), + ), + ), + PopSheetEntry( + value: TrackOptionValue.blacklist, + child: ListTile( + leading: const Icon(SpotubeIcons.playlistRemove), + iconColor: !isBlackListed ? Colors.red[400] : null, + textColor: !isBlackListed ? Colors.red[400] : null, + title: Text( + isBlackListed + ? context.l10n.remove_from_blacklist + : context.l10n.add_to_blacklist, + ), ), ), - ), - useRootNavigator: true, - ); + PopSheetEntry( + value: TrackOptionValue.share, + child: ListTile( + leading: const Icon(SpotubeIcons.share), + title: Text(context.l10n.share), + ), + ) + ] }, - ); - } - - return PopupMenuButton( - icon: const Icon(SpotubeIcons.moreHorizontal), - position: PopupMenuPosition.under, - tooltip: context.l10n.more_actions, - itemBuilder: (context) { - return createItems(context) - .map( - (e) => PopupMenuItem( - padding: EdgeInsets.zero, - child: e, - ), - ) - .toList(); - }, + ), ); } } diff --git a/lib/components/shared/track_table/tracks_table_view.dart b/lib/components/shared/track_table/tracks_table_view.dart index ddaea209..9ac8034c 100644 --- a/lib/components/shared/track_table/tracks_table_view.dart +++ b/lib/components/shared/track_table/tracks_table_view.dart @@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/shared/dialogs/confirm_download_dialog.dart'; import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart'; import 'package:spotube/components/shared/fallbacks/not_found.dart'; @@ -138,70 +139,63 @@ class TracksTableView extends HookConsumerWidget { .state = value; }, ), - PopupMenuButton( + AdaptivePopSheetList( tooltip: context.l10n.more_actions, - itemBuilder: (context) { - return [ - PopupMenuItem( + headings: [ + Text( + context.l10n.more_actions, + style: tableHeadStyle, + ), + ], + children: [ + PopSheetEntry( + enabled: selectedTracks.isNotEmpty, + value: "download", + child: ListTile( + leading: const Icon(SpotubeIcons.download), enabled: selectedTracks.isNotEmpty, - value: "download", - child: Row( - children: [ - const Icon(SpotubeIcons.download), - const SizedBox(width: 5), - Text( - context.l10n - .download_count(selectedTracks.length), - ), - ], + title: Text( + context.l10n.download_count(selectedTracks.length), ), ), - if (!userPlaylist) - PopupMenuItem( + ), + if (!userPlaylist) + PopSheetEntry( + enabled: selectedTracks.isNotEmpty, + value: "add-to-playlist", + child: ListTile( + leading: const Icon(SpotubeIcons.playlistAdd), enabled: selectedTracks.isNotEmpty, - value: "add-to-playlist", - child: Row( - children: [ - const Icon(SpotubeIcons.playlistAdd), - const SizedBox(width: 5), - Text( - context.l10n.add_count_to_playlist( - selectedTracks.length, - ), - ), - ], + title: Text( + context.l10n + .add_count_to_playlist(selectedTracks.length), ), ), - PopupMenuItem( + ), + PopSheetEntry( + enabled: selectedTracks.isNotEmpty, + value: "add-to-queue", + child: ListTile( + leading: const Icon(SpotubeIcons.queueAdd), enabled: selectedTracks.isNotEmpty, - value: "add-to-queue", - child: Row( - children: [ - const Icon(SpotubeIcons.queueAdd), - const SizedBox(width: 5), - Text( - context.l10n - .add_count_to_queue(selectedTracks.length), - ), - ], + title: Text( + context.l10n + .add_count_to_queue(selectedTracks.length), ), ), - PopupMenuItem( + ), + PopSheetEntry( + enabled: selectedTracks.isNotEmpty, + value: "play-next", + child: ListTile( + leading: const Icon(SpotubeIcons.lightning), enabled: selectedTracks.isNotEmpty, - value: "play-next", - child: Row( - children: [ - const Icon(SpotubeIcons.lightning), - const SizedBox(width: 5), - Text( - context.l10n - .play_count_next(selectedTracks.length), - ), - ], + title: Text( + context.l10n.play_count_next(selectedTracks.length), ), ), - ]; - }, + ), + ], onSelected: (action) async { switch (action) { case "download":