refactor: extend list item for PopSheetEntry for better interactivity

This commit is contained in:
Kingkor Roy Tirtho 2023-06-18 22:13:06 +06:00
parent b4713e377a
commit 0620b62023
8 changed files with 175 additions and 158 deletions

View File

@ -86,4 +86,5 @@ abstract class SpotubeIcons {
static const volumeMedium = FeatherIcons.volume1; static const volumeMedium = FeatherIcons.volume1;
static const volumeLow = FeatherIcons.volume; static const volumeLow = FeatherIcons.volume;
static const volumeMute = FeatherIcons.volumeX; static const volumeMute = FeatherIcons.volumeX;
static const timer = FeatherIcons.clock;
} }

View File

@ -3,10 +3,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart' hide Offset;
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/player/player_queue.dart'; import 'package:spotube/components/player/player_queue.dart';
import 'package:spotube/components/player/sibling_tracks_sheet.dart'; import 'package:spotube/components/player/sibling_tracks_sheet.dart';
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
import 'package:spotube/components/shared/heart_button.dart'; import 'package:spotube/components/shared/heart_button.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';
@ -127,6 +128,35 @@ class PlayerActions extends HookConsumerWidget {
), ),
if (playlist.activeTrack != null && !isLocalTrack && auth != null) if (playlist.activeTrack != null && !isLocalTrack && auth != null)
TrackHeartButton(track: playlist.activeTrack!), TrackHeartButton(track: playlist.activeTrack!),
AdaptivePopSheetList(
offset: const Offset(0, -50 * 5),
headings: [
Text(context.l10n.sleep_timer),
],
icon: const Icon(SpotubeIcons.timer),
children: [
PopSheetEntry(
value: const Duration(minutes: 15),
title: Text(context.l10n.mins(15)),
),
PopSheetEntry(
value: const Duration(minutes: 30),
title: Text(context.l10n.mins(30)),
),
PopSheetEntry(
value: const Duration(hours: 1),
title: Text(context.l10n.hour(1)),
),
PopSheetEntry(
value: const Duration(hours: 2),
title: Text(context.l10n.hours(2)),
),
PopSheetEntry(
value: Duration.zero,
title: Text(context.l10n.cancel),
),
],
),
...(extraActions ?? []) ...(extraActions ?? [])
], ],
); );

View File

@ -2,17 +2,47 @@ import 'package:flutter/material.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
class PopSheetEntry<T> { _emptyCB() {}
final T? value;
final VoidCallback? onTap;
final Widget child;
final bool enabled;
class PopSheetEntry<T> extends ListTile {
final T? value;
const PopSheetEntry({ const PopSheetEntry({
required this.child,
this.value, this.value,
this.onTap, super.key,
this.enabled = true, super.leading,
super.title,
super.subtitle,
super.trailing,
super.isThreeLine = false,
super.dense,
super.visualDensity,
super.shape,
super.style,
super.selectedColor,
super.iconColor,
super.textColor,
super.titleTextStyle,
super.subtitleTextStyle,
super.leadingAndTrailingTextStyle,
super.contentPadding,
super.enabled = true,
super.onTap = _emptyCB,
super.onLongPress,
super.onFocusChange,
super.mouseCursor,
super.selected = false,
super.focusColor,
super.hoverColor,
super.splashColor,
super.focusNode,
super.autofocus = false,
super.tileColor,
super.selectedTileColor,
super.enableFeedback,
super.horizontalTitleGap,
super.minVerticalPadding,
super.minLeadingWidth,
super.titleAlignment,
}); });
} }
@ -30,6 +60,7 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
final ValueChanged<T>? onSelected; final ValueChanged<T>? onSelected;
final BorderRadius borderRadius; final BorderRadius borderRadius;
final Offset offset;
const AdaptivePopSheetList({ const AdaptivePopSheetList({
super.key, super.key,
@ -41,6 +72,7 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
this.onSelected, this.onSelected,
this.borderRadius = const BorderRadius.all(Radius.circular(999)), this.borderRadius = const BorderRadius.all(Radius.circular(999)),
this.tooltip, this.tooltip,
this.offset = Offset.zero,
}) : 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,11 +87,13 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
return PopupMenuButton( return PopupMenuButton(
icon: icon, icon: icon,
tooltip: tooltip, tooltip: tooltip,
offset: offset,
child: child == null ? null : 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,
enabled: false,
child: _AdaptivePopSheetListItem( child: _AdaptivePopSheetListItem(
item: item, item: item,
onSelected: onSelected, onSelected: onSelected,
@ -151,8 +185,11 @@ class _AdaptivePopSheetListItem<T> extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(10), borderRadius: (theme.listTileTheme.shape as RoundedRectangleBorder?)
?.borderRadius as BorderRadius? ??
const BorderRadius.all(Radius.circular(10)),
onTap: !item.enabled onTap: !item.enabled
? null ? null
: () { : () {
@ -162,16 +199,9 @@ class _AdaptivePopSheetListItem<T> extends StatelessWidget {
onSelected?.call(item.value as T); 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( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
child: item.child, child: IgnorePointer(child: item),
),
), ),
); );
} }

View File

@ -25,60 +25,39 @@ class SortTracksDropdown extends StatelessWidget {
children: [ children: [
PopSheetEntry( PopSheetEntry(
value: SortBy.none, value: SortBy.none,
enabled: value != SortBy.none,
child: ListTile(
enabled: value != SortBy.none, enabled: value != SortBy.none,
title: Text(context.l10n.none), title: Text(context.l10n.none),
), ),
),
PopSheetEntry( PopSheetEntry(
value: SortBy.ascending, value: SortBy.ascending,
enabled: value != SortBy.ascending,
child: ListTile(
enabled: value != SortBy.ascending, enabled: value != SortBy.ascending,
title: Text(context.l10n.sort_a_z), title: Text(context.l10n.sort_a_z),
), ),
),
PopSheetEntry( PopSheetEntry(
value: SortBy.descending, value: SortBy.descending,
enabled: value != SortBy.descending,
child: ListTile(
enabled: value != SortBy.descending, enabled: value != SortBy.descending,
title: Text(context.l10n.sort_z_a), title: Text(context.l10n.sort_z_a),
), ),
),
PopSheetEntry( PopSheetEntry(
value: SortBy.newest, value: SortBy.newest,
enabled: value != SortBy.newest,
child: ListTile(
enabled: value != SortBy.newest, enabled: value != SortBy.newest,
title: Text(context.l10n.sort_newest), title: Text(context.l10n.sort_newest),
), ),
),
PopSheetEntry( PopSheetEntry(
value: SortBy.oldest, value: SortBy.oldest,
enabled: value != SortBy.oldest,
child: ListTile(
enabled: value != SortBy.oldest, enabled: value != SortBy.oldest,
title: Text(context.l10n.sort_oldest), title: Text(context.l10n.sort_oldest),
), ),
),
PopSheetEntry( PopSheetEntry(
value: SortBy.artist, value: SortBy.artist,
enabled: value != SortBy.artist,
child: ListTile(
enabled: value != SortBy.artist, enabled: value != SortBy.artist,
title: Text(context.l10n.sort_artist), title: Text(context.l10n.sort_artist),
), ),
),
PopSheetEntry( PopSheetEntry(
value: SortBy.album, value: SortBy.album,
enabled: value != SortBy.album,
child: ListTile(
enabled: value != SortBy.album, enabled: value != SortBy.album,
title: Text(context.l10n.sort_album), title: Text(context.l10n.sort_album),
), ),
),
], ],
headings: [ headings: [
Text(context.l10n.sort_tracks), Text(context.l10n.sort_tracks),

View File

@ -207,42 +207,32 @@ class TrackOptions extends HookConsumerWidget {
LocalTrack => [ LocalTrack => [
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.delete, value: TrackOptionValue.delete,
child: ListTile(
leading: const Icon(SpotubeIcons.trash), leading: const Icon(SpotubeIcons.trash),
title: Text(context.l10n.delete), title: Text(context.l10n.delete),
),
) )
], ],
_ => [ _ => [
if (!playlist.containsTrack(track)) ...[ if (!playlist.containsTrack(track)) ...[
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.addToQueue, value: TrackOptionValue.addToQueue,
child: ListTile(
leading: const Icon(SpotubeIcons.queueAdd), leading: const Icon(SpotubeIcons.queueAdd),
title: Text(context.l10n.add_to_queue), title: Text(context.l10n.add_to_queue),
), ),
),
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.playNext, value: TrackOptionValue.playNext,
child: ListTile(
leading: const Icon(SpotubeIcons.lightning), leading: const Icon(SpotubeIcons.lightning),
title: Text(context.l10n.play_next), title: Text(context.l10n.play_next),
), ),
),
] else ] else
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.removeFromQueue, 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)
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.favorite, value: TrackOptionValue.favorite,
child: ListTile(
leading: favorites.isLiked leading: favorites.isLiked
? const Icon( ? const Icon(
SpotubeIcons.heartFilled, SpotubeIcons.heartFilled,
@ -255,19 +245,15 @@ class TrackOptions extends HookConsumerWidget {
: context.l10n.save_as_favorite, : context.l10n.save_as_favorite,
), ),
), ),
),
if (auth != null) if (auth != null)
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.addToPlaylist, value: TrackOptionValue.addToPlaylist,
child: ListTile(
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)
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.removeFromPlaylist, value: TrackOptionValue.removeFromPlaylist,
child: ListTile(
leading: (removeTrack.isMutating || !removeTrack.hasData) && leading: (removeTrack.isMutating || !removeTrack.hasData) &&
removingTrack.value == track.uri removingTrack.value == track.uri
? const Center( ? const Center(
@ -276,10 +262,8 @@ 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),
), ),
),
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.blacklist, value: TrackOptionValue.blacklist,
child: ListTile(
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,
@ -289,21 +273,16 @@ class TrackOptions extends HookConsumerWidget {
: context.l10n.add_to_blacklist, : context.l10n.add_to_blacklist,
), ),
), ),
),
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.share, value: TrackOptionValue.share,
child: ListTile(
leading: const Icon(SpotubeIcons.share), leading: const Icon(SpotubeIcons.share),
title: Text(context.l10n.share), title: Text(context.l10n.share),
), ),
),
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.details, value: TrackOptionValue.details,
child: ListTile(
leading: const Icon(SpotubeIcons.info), leading: const Icon(SpotubeIcons.info),
title: Text(context.l10n.details), title: Text(context.l10n.details),
), ),
),
] ]
}, },
), ),

View File

@ -185,55 +185,6 @@ class TracksTableView extends HookConsumerWidget {
style: tableHeadStyle, 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)
PopSheetEntry(
enabled: selectedTracks.isNotEmpty,
value: "add-to-playlist",
child: ListTile(
leading: const Icon(SpotubeIcons.playlistAdd),
enabled: selectedTracks.isNotEmpty,
title: Text(
context.l10n
.add_count_to_playlist(selectedTracks.length),
),
),
),
PopSheetEntry(
enabled: selectedTracks.isNotEmpty,
value: "add-to-queue",
child: ListTile(
leading: const Icon(SpotubeIcons.queueAdd),
enabled: selectedTracks.isNotEmpty,
title: Text(
context.l10n
.add_count_to_queue(selectedTracks.length),
),
),
),
PopSheetEntry(
enabled: selectedTracks.isNotEmpty,
value: "play-next",
child: ListTile(
leading: const Icon(SpotubeIcons.lightning),
enabled: selectedTracks.isNotEmpty,
title: Text(
context.l10n.play_count_next(selectedTracks.length),
),
),
),
],
onSelected: (action) async { onSelected: (action) async {
switch (action) { switch (action) {
case "download": case "download":
@ -283,6 +234,43 @@ class TracksTableView extends HookConsumerWidget {
} }
}, },
icon: const Icon(SpotubeIcons.moreVertical), icon: const Icon(SpotubeIcons.moreVertical),
children: [
PopSheetEntry(
value: "download",
leading: const Icon(SpotubeIcons.download),
enabled: selectedTracks.isNotEmpty,
title: Text(
context.l10n.download_count(selectedTracks.length),
),
),
if (!userPlaylist)
PopSheetEntry(
value: "add-to-playlist",
leading: const Icon(SpotubeIcons.playlistAdd),
enabled: selectedTracks.isNotEmpty,
title: Text(
context.l10n
.add_count_to_playlist(selectedTracks.length),
),
),
PopSheetEntry(
enabled: selectedTracks.isNotEmpty,
value: "add-to-queue",
leading: const Icon(SpotubeIcons.queueAdd),
title: Text(
context.l10n
.add_count_to_queue(selectedTracks.length),
),
),
PopSheetEntry(
enabled: selectedTracks.isNotEmpty,
value: "play-next",
leading: const Icon(SpotubeIcons.lightning),
title: Text(
context.l10n.play_count_next(selectedTracks.length),
),
),
],
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
], ],

View File

@ -239,5 +239,9 @@
"streamUrl": "Stream URL", "streamUrl": "Stream URL",
"stop": "Stop", "stop": "Stop",
"sort_newest": "Sort by newest added", "sort_newest": "Sort by newest added",
"sort_oldest": "Sort by oldest added" "sort_oldest": "Sort by oldest added",
"sleep_timer": "Sleep Timer",
"mins": "{minutes} Minutes",
"hours": "{hours} Hours",
"hour": "{hours} Hour"
} }

View File

@ -197,6 +197,9 @@ class PlayerView extends HookConsumerWidget {
label: Text(context.l10n.details), label: Text(context.l10n.details),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: bodyTextColor, foregroundColor: bodyTextColor,
side: BorderSide(
color: bodyTextColor ?? Colors.white,
),
), ),
onPressed: currentTrack == null onPressed: currentTrack == null
? null ? null
@ -218,6 +221,9 @@ class PlayerView extends HookConsumerWidget {
icon: const Icon(SpotubeIcons.music), icon: const Icon(SpotubeIcons.music),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: bodyTextColor, foregroundColor: bodyTextColor,
side: BorderSide(
color: bodyTextColor ?? Colors.white,
),
), ),
onPressed: () { onPressed: () {
showModalBottomSheet( showModalBottomSheet(
@ -257,7 +263,7 @@ class PlayerView extends HookConsumerWidget {
overlayColor: titleTextColor?.withOpacity(0.2), overlayColor: titleTextColor?.withOpacity(0.2),
trackHeight: 2, trackHeight: 2,
thumbShape: const RoundSliderThumbShape( thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 6, enabledThumbRadius: 8,
), ),
), ),
child: const Padding( child: const Padding(