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,56 +94,18 @@ 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) {
return [
if (mediaQuery.isSm) ...[
Text(
track.name!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
), ),
Divider( child: AdaptivePopSheetList<TrackOptionValue>(
color: Theme.of(context).colorScheme.primary, onSelected: (value) async {
thickness: 0.2, switch (value) {
indent: 16, case TrackOptionValue.delete:
endIndent: 16,
),
],
ListTile(
onTap: () async {
await File((track as LocalTrack).path).delete(); await File((track as LocalTrack).path).delete();
ref.refresh(localTracksProvider); ref.refresh(localTracksProvider);
if (context.mounted) Navigator.pop(context); break;
}, case TrackOptionValue.addToQueue:
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); await playback.addTrack(track);
if (context.mounted) { if (context.mounted) {
scaffoldMessenger.showSnackBar( scaffoldMessenger.showSnackBar(
@ -139,14 +115,9 @@ class TrackOptions extends HookConsumerWidget {
), ),
), ),
); );
Navigator.pop(context);
} }
}, break;
leading: const Icon(SpotubeIcons.queueAdd), case TrackOptionValue.playNext:
title: Text(context.l10n.add_to_queue),
),
ListTile(
onTap: () {
playback.addTracksAtFirst([track]); playback.addTracksAtFirst([track]);
scaffoldMessenger.showSnackBar( scaffoldMessenger.showSnackBar(
SnackBar( SnackBar(
@ -155,16 +126,8 @@ class TrackOptions extends HookConsumerWidget {
), ),
), ),
); );
Navigator.pop(context); break;
}, case TrackOptionValue.removeFromQueue:
leading: const Icon(SpotubeIcons.lightning),
title: Text(context.l10n.play_next),
),
] else
ListTile(
onTap: playlist.activeTrack?.id == track.id
? null
: () {
playback.removeTrack(track.id!); playback.removeTrack(track.id!);
scaffoldMessenger.showSnackBar( scaffoldMessenger.showSnackBar(
SnackBar( SnackBar(
@ -175,18 +138,103 @@ class TrackOptions extends HookConsumerWidget {
), ),
), ),
); );
Navigator.pop(context); 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!),
);
} else {
ref.read(BlackListNotifier.provider.notifier).add(
BlacklistedElement.track(track.id!, track.name!),
);
}
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(
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!,
),
),
),
],
children: switch (track.runtimeType) {
LocalTrack => [
PopSheetEntry(
value: TrackOptionValue.delete,
child: ListTile(
leading: const Icon(SpotubeIcons.trash),
title: Text(context.l10n.delete),
),
)
],
_ => [
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, enabled: playlist.activeTrack?.id != track.id,
leading: const Icon(SpotubeIcons.queueRemove), leading: const Icon(SpotubeIcons.queueRemove),
title: Text(context.l10n.remove_from_queue), title: Text(context.l10n.remove_from_queue),
), ),
),
if (favorites.me.hasData) if (favorites.me.hasData)
ListTile( PopSheetEntry(
onTap: () { value: TrackOptionValue.favorite,
favorites.toggleTrackLike.mutate(favorites.isLiked); child: ListTile(
Navigator.pop(context);
},
leading: favorites.isLiked leading: favorites.isLiked
? const Icon( ? const Icon(
SpotubeIcons.heartFilled, SpotubeIcons.heartFilled,
@ -199,22 +247,19 @@ class TrackOptions extends HookConsumerWidget {
: context.l10n.save_as_favorite, : context.l10n.save_as_favorite,
), ),
), ),
),
if (auth != null) if (auth != null)
ListTile( PopSheetEntry(
onTap: () { value: TrackOptionValue.addToPlaylist,
actionAddToPlaylist(context, track); child: ListTile(
Navigator.pop(context);
},
leading: const Icon(SpotubeIcons.playlistAdd), leading: const Icon(SpotubeIcons.playlistAdd),
title: Text(context.l10n.add_to_playlist), title: Text(context.l10n.add_to_playlist),
), ),
),
if (userPlaylist && auth != null) if (userPlaylist && auth != null)
ListTile( PopSheetEntry(
onTap: () { value: TrackOptionValue.removeFromPlaylist,
removingTrack.value = track.uri; child: ListTile(
removeTrack.mutate(track.uri!);
Navigator.pop(context);
},
leading: (removeTrack.isMutating || !removeTrack.hasData) && leading: (removeTrack.isMutating || !removeTrack.hasData) &&
removingTrack.value == track.uri removingTrack.value == track.uri
? const Center( ? const Center(
@ -223,19 +268,10 @@ class TrackOptions extends HookConsumerWidget {
: const Icon(SpotubeIcons.removeFilled), : const Icon(SpotubeIcons.removeFilled),
title: Text(context.l10n.remove_from_playlist), title: Text(context.l10n.remove_from_playlist),
), ),
ListTile( ),
onTap: () { PopSheetEntry(
if (isBlackListed) { value: TrackOptionValue.blacklist,
ref.read(BlackListNotifier.provider.notifier).remove( child: ListTile(
BlacklistedElement.track(track.id!, track.name!),
);
} else {
ref.read(BlackListNotifier.provider.notifier).add(
BlacklistedElement.track(track.id!, track.name!),
);
}
Navigator.pop(context);
},
leading: const Icon(SpotubeIcons.playlistRemove), leading: const Icon(SpotubeIcons.playlistRemove),
iconColor: !isBlackListed ? Colors.red[400] : null, iconColor: !isBlackListed ? Colors.red[400] : null,
textColor: !isBlackListed ? Colors.red[400] : null, textColor: !isBlackListed ? Colors.red[400] : null,
@ -245,58 +281,17 @@ class TrackOptions extends HookConsumerWidget {
: context.l10n.add_to_blacklist, : context.l10n.add_to_blacklist,
), ),
), ),
ListTile( ),
onTap: () { PopSheetEntry(
actionShare(context, track); value: TrackOptionValue.share,
Navigator.pop(context); child: ListTile(
},
leading: const Icon(SpotubeIcons.share), leading: const Icon(SpotubeIcons.share),
title: Text(context.l10n.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),
),
horizontalTitleGap: 5,
child: Column(
mainAxisSize: MainAxisSize.min,
children: createItems(context),
),
),
),
useRootNavigator: true,
);
},
);
}
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 [
PopupMenuItem(
enabled: selectedTracks.isNotEmpty,
value: "download",
child: Row(
children: [
const Icon(SpotubeIcons.download),
const SizedBox(width: 5),
Text( Text(
context.l10n context.l10n.more_actions,
.download_count(selectedTracks.length), style: tableHeadStyle,
), ),
], ],
children: [
PopSheetEntry(
enabled: selectedTracks.isNotEmpty,
value: "download",
child: ListTile(
leading: const Icon(SpotubeIcons.download),
enabled: selectedTracks.isNotEmpty,
title: Text(
context.l10n.download_count(selectedTracks.length),
),
), ),
), ),
if (!userPlaylist) if (!userPlaylist)
PopupMenuItem( PopSheetEntry(
enabled: selectedTracks.isNotEmpty, enabled: selectedTracks.isNotEmpty,
value: "add-to-playlist", value: "add-to-playlist",
child: Row( child: ListTile(
children: [ leading: const Icon(SpotubeIcons.playlistAdd),
const Icon(SpotubeIcons.playlistAdd), enabled: selectedTracks.isNotEmpty,
const SizedBox(width: 5), title: Text(
Text( context.l10n
context.l10n.add_count_to_playlist( .add_count_to_playlist(selectedTracks.length),
selectedTracks.length,
), ),
), ),
],
), ),
), PopSheetEntry(
PopupMenuItem(
enabled: selectedTracks.isNotEmpty, enabled: selectedTracks.isNotEmpty,
value: "add-to-queue", value: "add-to-queue",
child: Row( child: ListTile(
children: [ leading: const Icon(SpotubeIcons.queueAdd),
const Icon(SpotubeIcons.queueAdd), enabled: selectedTracks.isNotEmpty,
const SizedBox(width: 5), title: Text(
Text(
context.l10n context.l10n
.add_count_to_queue(selectedTracks.length), .add_count_to_queue(selectedTracks.length),
), ),
],
), ),
), ),
PopupMenuItem( PopSheetEntry(
enabled: selectedTracks.isNotEmpty, enabled: selectedTracks.isNotEmpty,
value: "play-next", value: "play-next",
child: Row( child: ListTile(
children: [ leading: const Icon(SpotubeIcons.lightning),
const Icon(SpotubeIcons.lightning), enabled: selectedTracks.isNotEmpty,
const SizedBox(width: 5), title: Text(
Text( context.l10n.play_count_next(selectedTracks.length),
context.l10n ),
.play_count_next(selectedTracks.length), ),
), ),
], ],
),
),
];
},
onSelected: (action) async { onSelected: (action) async {
switch (action) { switch (action) {
case "download": case "download":