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.tooltip,
}) : assert(
icon != null || child != null,
!(icon != null && child != null),
'Either icon or child must be provided',
);
@ -55,21 +55,14 @@ class AdaptivePopSheetList<T> 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<T> 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<T> 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<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(
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: [

View File

@ -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,56 +94,18 @@ 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,
return ListTileTheme(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
Divider(
color: Theme.of(context).colorScheme.primary,
thickness: 0.2,
indent: 16,
endIndent: 16,
),
],
ListTile(
onTap: () async {
child: AdaptivePopSheetList<TrackOptionValue>(
onSelected: (value) async {
switch (value) {
case TrackOptionValue.delete:
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 {
break;
case TrackOptionValue.addToQueue:
await playback.addTrack(track);
if (context.mounted) {
scaffoldMessenger.showSnackBar(
@ -139,14 +115,9 @@ class TrackOptions extends HookConsumerWidget {
),
),
);
Navigator.pop(context);
}
},
leading: const Icon(SpotubeIcons.queueAdd),
title: Text(context.l10n.add_to_queue),
),
ListTile(
onTap: () {
break;
case TrackOptionValue.playNext:
playback.addTracksAtFirst([track]);
scaffoldMessenger.showSnackBar(
SnackBar(
@ -155,16 +126,8 @@ class TrackOptions extends HookConsumerWidget {
),
),
);
Navigator.pop(context);
},
leading: const Icon(SpotubeIcons.lightning),
title: Text(context.l10n.play_next),
),
] else
ListTile(
onTap: playlist.activeTrack?.id == track.id
? null
: () {
break;
case TrackOptionValue.removeFromQueue:
playback.removeTrack(track.id!);
scaffoldMessenger.showSnackBar(
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,
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);
},
PopSheetEntry(
value: TrackOptionValue.favorite,
child: ListTile(
leading: favorites.isLiked
? const Icon(
SpotubeIcons.heartFilled,
@ -199,22 +247,19 @@ class TrackOptions extends HookConsumerWidget {
: context.l10n.save_as_favorite,
),
),
),
if (auth != null)
ListTile(
onTap: () {
actionAddToPlaylist(context, track);
Navigator.pop(context);
},
PopSheetEntry(
value: TrackOptionValue.addToPlaylist,
child: ListTile(
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);
},
PopSheetEntry(
value: TrackOptionValue.removeFromPlaylist,
child: ListTile(
leading: (removeTrack.isMutating || !removeTrack.hasData) &&
removingTrack.value == track.uri
? const Center(
@ -223,19 +268,10 @@ class TrackOptions extends HookConsumerWidget {
: const Icon(SpotubeIcons.removeFilled),
title: Text(context.l10n.remove_from_playlist),
),
ListTile(
onTap: () {
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!),
);
}
Navigator.pop(context);
},
),
PopSheetEntry(
value: TrackOptionValue.blacklist,
child: ListTile(
leading: const Icon(SpotubeIcons.playlistRemove),
iconColor: !isBlackListed ? Colors.red[400] : null,
textColor: !isBlackListed ? Colors.red[400] : null,
@ -245,58 +281,17 @@ class TrackOptions extends HookConsumerWidget {
: context.l10n.add_to_blacklist,
),
),
ListTile(
onTap: () {
actionShare(context, track);
Navigator.pop(context);
},
),
PopSheetEntry(
value: TrackOptionValue.share,
child: ListTile(
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),
),
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: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(
enabled: selectedTracks.isNotEmpty,
value: "download",
child: Row(
children: [
const Icon(SpotubeIcons.download),
const SizedBox(width: 5),
headings: [
Text(
context.l10n
.download_count(selectedTracks.length),
context.l10n.more_actions,
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)
PopupMenuItem(
PopSheetEntry(
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,
child: ListTile(
leading: const Icon(SpotubeIcons.playlistAdd),
enabled: selectedTracks.isNotEmpty,
title: Text(
context.l10n
.add_count_to_playlist(selectedTracks.length),
),
),
],
),
),
PopupMenuItem(
PopSheetEntry(
enabled: selectedTracks.isNotEmpty,
value: "add-to-queue",
child: Row(
children: [
const Icon(SpotubeIcons.queueAdd),
const SizedBox(width: 5),
Text(
child: ListTile(
leading: const Icon(SpotubeIcons.queueAdd),
enabled: selectedTracks.isNotEmpty,
title: Text(
context.l10n
.add_count_to_queue(selectedTracks.length),
),
],
),
),
PopupMenuItem(
PopSheetEntry(
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),
child: ListTile(
leading: const Icon(SpotubeIcons.lightning),
enabled: selectedTracks.isNotEmpty,
title: Text(
context.l10n.play_count_next(selectedTracks.length),
),
),
),
],
),
),
];
},
onSelected: (action) async {
switch (action) {
case "download":