refactor: replace PopupMenuButton widgets with AdaptivePopSheetList

This commit is contained in:
Kingkor Roy Tirtho 2023-06-12 12:50:58 +06:00
parent ddc1c5f373
commit 3b56c78d5c
4 changed files with 316 additions and 275 deletions

View File

@ -42,7 +42,7 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
this.borderRadius = const BorderRadius.all(Radius.circular(999)), this.borderRadius = const BorderRadius.all(Radius.circular(999)),
this.tooltip, this.tooltip,
}) : assert( }) : assert(
icon != null || child != null, !(icon != null && child != null),
'Either icon or child must be provided', 'Either icon or child must be provided',
); );
@ -55,21 +55,14 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
return PopupMenuButton( return PopupMenuButton(
icon: icon, icon: icon,
tooltip: tooltip, tooltip: tooltip,
child: IgnorePointer(child: child), child: child == null ? null : IgnorePointer(child: child),
itemBuilder: (context) => children itemBuilder: (context) => children
.map( .map(
(item) => PopupMenuItem( (item) => PopupMenuItem(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
child: ListTile( child: _AdaptivePopSheetListItem(
enabled: item.enabled, item: item,
onTap: () { onSelected: onSelected,
item.onTap?.call();
Navigator.pop(context);
if (item.value != null) {
onSelected?.call(item.value as T);
}
},
title: item.child,
), ),
), ),
) )
@ -90,7 +83,17 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (headings != null) ...[ if (headings != null) ...[
Container(
width: 180,
height: 6,
decoration: BoxDecoration(
color: theme.colorScheme.primary,
borderRadius: BorderRadius.circular(999),
),
),
const SizedBox(height: 8),
...headings!, ...headings!,
const SizedBox(height: 8),
Divider( Divider(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
thickness: 0.3, thickness: 0.3,
@ -99,16 +102,9 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
), ),
], ],
...children.map( ...children.map(
(item) => ListTile( (item) => _AdaptivePopSheetListItem(
onTap: () { item: item,
item.onTap?.call(); onSelected: onSelected,
Navigator.pop(context);
if (item.value != null) {
onSelected?.call(item.value as T);
}
},
enabled: item.enabled,
title: item.child,
), ),
) )
], ],
@ -144,3 +140,41 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
); );
} }
} }
class _AdaptivePopSheetListItem<T> extends StatelessWidget {
final PopSheetEntry<T> item;
final ValueChanged<T>? 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,
),
),
);
}
}

View File

@ -25,32 +25,50 @@ class SortTracksDropdown extends StatelessWidget {
PopSheetEntry( PopSheetEntry(
value: SortBy.none, value: SortBy.none,
enabled: value != SortBy.none, enabled: value != SortBy.none,
child: Text(context.l10n.none), child: ListTile(
enabled: value != SortBy.none,
title: Text(context.l10n.none),
),
), ),
PopSheetEntry( PopSheetEntry(
value: SortBy.ascending, value: SortBy.ascending,
enabled: 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( PopSheetEntry(
value: SortBy.descending, value: SortBy.descending,
enabled: 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( PopSheetEntry(
value: SortBy.dateAdded, value: SortBy.dateAdded,
enabled: 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( PopSheetEntry(
value: SortBy.artist, value: SortBy.artist,
enabled: 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( PopSheetEntry(
value: SortBy.album, value: SortBy.album,
enabled: 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: [ headings: [

View File

@ -7,15 +7,29 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/library/user_local_tracks.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/dialogs/playlist_add_track_dialog.dart';
import 'package:spotube/components/shared/heart_button.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/extensions/context.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/mutations/mutations.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 { class TrackOptions extends HookConsumerWidget {
final Track track; final Track track;
@ -80,151 +94,62 @@ class TrackOptions extends HookConsumerWidget {
playlistId ?? "", playlistId ?? "",
); );
final mediaQuery = MediaQuery.of(context); return ListTileTheme(
shape: RoundedRectangleBorder(
final createItems = useCallback( borderRadius: BorderRadius.circular(10),
(BuildContext context) { ),
if (track is LocalTrack) { child: AdaptivePopSheetList<TrackOptionValue>(
return [ onSelected: (value) async {
if (mediaQuery.isSm) ...[ switch (value) {
Text( case TrackOptionValue.delete:
track.name!, await File((track as LocalTrack).path).delete();
maxLines: 1, ref.refresh(localTracksProvider);
overflow: TextOverflow.ellipsis, break;
style: Theme.of(context).textTheme.titleMedium, case TrackOptionValue.addToQueue:
), await playback.addTrack(track);
Divider( if (context.mounted) {
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]);
scaffoldMessenger.showSnackBar( scaffoldMessenger.showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text(
context.l10n.track_will_play_next(track.name!), context.l10n.added_track_to_queue(track.name!),
), ),
), ),
); );
Navigator.pop(context); }
}, break;
leading: const Icon(SpotubeIcons.lightning), case TrackOptionValue.playNext:
title: Text(context.l10n.play_next), playback.addTracksAtFirst([track]);
), scaffoldMessenger.showSnackBar(
] else SnackBar(
ListTile( content: Text(
onTap: playlist.activeTrack?.id == track.id context.l10n.track_will_play_next(track.name!),
? null ),
: () { ),
playback.removeTrack(track.id!); );
scaffoldMessenger.showSnackBar( break;
SnackBar( case TrackOptionValue.removeFromQueue:
content: Text( playback.removeTrack(track.id!);
context.l10n.removed_track_from_queue( scaffoldMessenger.showSnackBar(
track.name!, 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), break;
title: Text(context.l10n.remove_from_queue), case TrackOptionValue.favorite:
), favorites.toggleTrackLike.mutate(favorites.isLiked);
if (favorites.me.hasData) break;
ListTile( case TrackOptionValue.addToPlaylist:
onTap: () { actionAddToPlaylist(context, track);
favorites.toggleTrackLike.mutate(favorites.isLiked); break;
Navigator.pop(context); case TrackOptionValue.removeFromPlaylist:
}, removingTrack.value = track.uri;
leading: favorites.isLiked removeTrack.mutate(track.uri!);
? const Icon( break;
SpotubeIcons.heartFilled, case TrackOptionValue.blacklist:
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: () {
if (isBlackListed) { if (isBlackListed) {
ref.read(BlackListNotifier.provider.notifier).remove( ref.read(BlackListNotifier.provider.notifier).remove(
BlacklistedElement.track(track.id!, track.name!), BlacklistedElement.track(track.id!, track.name!),
@ -234,69 +159,139 @@ class TrackOptions extends HookConsumerWidget {
BlacklistedElement.track(track.id!, track.name!), BlacklistedElement.track(track.id!, track.name!),
); );
} }
Navigator.pop(context); break;
}, case TrackOptionValue.share:
leading: const Icon(SpotubeIcons.playlistRemove), actionShare(context, track);
iconColor: !isBlackListed ? Colors.red[400] : null, break;
textColor: !isBlackListed ? Colors.red[400] : null, }
},
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( title: Text(
isBlackListed track.name!,
? context.l10n.remove_from_blacklist maxLines: 1,
: context.l10n.add_to_blacklist, overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
subtitle: Align(
alignment: Alignment.centerLeft,
child: TypeConversionUtils.artists_X_ClickableArtists(
track.artists!,
),
), ),
), ),
ListTile( ],
onTap: () { children: switch (track.runtimeType) {
actionShare(context, track); LocalTrack => [
Navigator.pop(context); PopSheetEntry(
}, value: TrackOptionValue.delete,
leading: const Icon(SpotubeIcons.share), child: ListTile(
title: Text(context.l10n.share), leading: const Icon(SpotubeIcons.trash),
) title: Text(context.l10n.delete),
];
},
[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),
), ),
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,
),
), ),
), ),
), PopSheetEntry(
useRootNavigator: true, 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();
},
); );
} }
} }

View File

@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.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/confirm_download_dialog.dart';
import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart'; import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart';
import 'package:spotube/components/shared/fallbacks/not_found.dart'; import 'package:spotube/components/shared/fallbacks/not_found.dart';
@ -138,70 +139,63 @@ class TracksTableView extends HookConsumerWidget {
.state = value; .state = value;
}, },
), ),
PopupMenuButton( AdaptivePopSheetList(
tooltip: context.l10n.more_actions, tooltip: context.l10n.more_actions,
itemBuilder: (context) { headings: [
return [ Text(
PopupMenuItem( context.l10n.more_actions,
style: tableHeadStyle,
),
],
children: [
PopSheetEntry(
enabled: selectedTracks.isNotEmpty,
value: "download",
child: ListTile(
leading: const Icon(SpotubeIcons.download),
enabled: selectedTracks.isNotEmpty, enabled: selectedTracks.isNotEmpty,
value: "download", title: Text(
child: Row( context.l10n.download_count(selectedTracks.length),
children: [
const Icon(SpotubeIcons.download),
const SizedBox(width: 5),
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, enabled: selectedTracks.isNotEmpty,
value: "add-to-playlist", title: Text(
child: Row( context.l10n
children: [ .add_count_to_playlist(selectedTracks.length),
const Icon(SpotubeIcons.playlistAdd),
const SizedBox(width: 5),
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, enabled: selectedTracks.isNotEmpty,
value: "add-to-queue", title: Text(
child: Row( context.l10n
children: [ .add_count_to_queue(selectedTracks.length),
const Icon(SpotubeIcons.queueAdd),
const SizedBox(width: 5),
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, enabled: selectedTracks.isNotEmpty,
value: "play-next", title: Text(
child: Row( context.l10n.play_count_next(selectedTracks.length),
children: [
const Icon(SpotubeIcons.lightning),
const SizedBox(width: 5),
Text(
context.l10n
.play_count_next(selectedTracks.length),
),
],
), ),
), ),
]; ),
}, ],
onSelected: (action) async { onSelected: (action) async {
switch (action) { switch (action) {
case "download": case "download":