diff --git a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart new file mode 100644 index 000000000..9bd180fc2 --- /dev/null +++ b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/extensions/constrains.dart'; + +class PopSheetEntry { + final T? value; + final VoidCallback? onTap; + final Widget child; + final bool enabled; + + const PopSheetEntry({ + required this.child, + this.value, + this.onTap, + this.enabled = true, + }); +} + +/// An adaptive widget that shows a [PopupMenuButton] when screen size is above +/// or equal to 640px +/// In smaller screen, a [IconButton] with a [showModalBottomSheet] is shown +class AdaptivePopSheetList extends StatelessWidget { + final List> children; + final Widget? icon; + final Widget? child; + final bool useRootNavigator; + + final List? headings; + final String? tooltip; + final ValueChanged? onSelected; + + final BorderRadius borderRadius; + + const AdaptivePopSheetList({ + super.key, + required this.children, + this.icon, + this.child, + this.useRootNavigator = true, + this.headings, + this.onSelected, + this.borderRadius = const BorderRadius.all(Radius.circular(999)), + this.tooltip, + }) : assert( + icon != null || child != null, + 'Either icon or child must be provided', + ); + + @override + Widget build(BuildContext context) { + final mediaQuery = MediaQuery.of(context); + final theme = Theme.of(context); + + if (mediaQuery.mdAndUp) { + return PopupMenuButton( + icon: icon, + tooltip: tooltip, + child: 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, + ), + ), + ) + .toList(), + ); + } + + void showSheet() { + showModalBottomSheet( + context: context, + useRootNavigator: useRootNavigator, + builder: (context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: DefaultTextStyle( + style: theme.textTheme.titleMedium!, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (headings != null) ...[ + ...headings!, + Divider( + color: theme.colorScheme.primary, + thickness: 0.3, + endIndent: 16, + indent: 16, + ), + ], + ...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, + ), + ) + ], + ), + ), + ); + }, + ); + } + + if (child != null) { + return Tooltip( + message: tooltip ?? '', + child: InkWell( + onTap: showSheet, + borderRadius: borderRadius, + child: IgnorePointer(child: child), + ), + ); + } + + return IconButton( + icon: icon ?? const Icon(SpotubeIcons.moreVertical), + tooltip: tooltip, + style: theme.iconButtonTheme.style?.copyWith( + shape: MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: borderRadius, + ), + ), + ), + onPressed: showSheet, + ); + } +} diff --git a/lib/components/shared/sort_tracks_dropdown.dart b/lib/components/shared/sort_tracks_dropdown.dart index a73bf8ed3..e3ca166c7 100644 --- a/lib/components/shared/sort_tracks_dropdown.dart +++ b/lib/components/shared/sort_tracks_dropdown.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.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/extensions/context.dart'; class SortTracksDropdown extends StatelessWidget { @@ -15,44 +16,62 @@ class SortTracksDropdown extends StatelessWidget { @override Widget build(BuildContext context) { - return PopupMenuButton( - itemBuilder: (context) { - return [ - PopupMenuItem( + return ListTileTheme( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: AdaptivePopSheetList( + children: [ + PopSheetEntry( value: SortBy.none, enabled: value != SortBy.none, child: Text(context.l10n.none), ), - PopupMenuItem( + PopSheetEntry( value: SortBy.ascending, enabled: value != SortBy.ascending, child: Text(context.l10n.sort_a_z), ), - PopupMenuItem( + PopSheetEntry( value: SortBy.descending, enabled: value != SortBy.descending, child: Text(context.l10n.sort_z_a), ), - PopupMenuItem( + PopSheetEntry( value: SortBy.dateAdded, enabled: value != SortBy.dateAdded, child: Text(context.l10n.sort_date), ), - PopupMenuItem( + PopSheetEntry( value: SortBy.artist, enabled: value != SortBy.artist, child: Text(context.l10n.sort_artist), ), - PopupMenuItem( + PopSheetEntry( value: SortBy.album, enabled: value != SortBy.album, child: Text(context.l10n.sort_album), ), - ]; - }, - onSelected: onChanged, - tooltip: context.l10n.sort_tracks, - icon: const Icon(SpotubeIcons.sort), + ], + headings: [ + Text(context.l10n.sort_tracks), + ], + onSelected: onChanged, + tooltip: context.l10n.sort_tracks, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: DefaultTextStyle( + style: Theme.of(context).textTheme.titleSmall!, + child: Row( + children: [ + const Icon(SpotubeIcons.sort), + const SizedBox(width: 8), + Text(context.l10n.sort_tracks), + ], + ), + ), + ), + ), ); } }