From 04190f2ddaa644c511edce7e55c0d4e749b6de70 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 21 Dec 2024 12:42:51 +0600 Subject: [PATCH] refactor: use DropdownMenu for adaptive pop sheet list, shadcn widgets for bottom player and player controls and actions --- .../adaptive/adaptive_pop_sheet_list.dart | 284 +++++++----------- lib/components/sort_tracks_dropdown.dart | 32 +- lib/components/track_tile/track_options.dart | 85 +++--- .../sections/body/track_view_options.dart | 16 +- lib/main.dart | 6 +- lib/modules/player/player_actions.dart | 132 ++++---- lib/modules/player/player_controls.dart | 240 +++++++-------- lib/modules/player/volume_slider.dart | 20 +- lib/modules/root/bottom_player.dart | 103 +++---- 9 files changed, 438 insertions(+), 480 deletions(-) diff --git a/lib/components/adaptive/adaptive_pop_sheet_list.dart b/lib/components/adaptive/adaptive_pop_sheet_list.dart index 97dc6132..5345199e 100644 --- a/lib/components/adaptive/adaptive_pop_sheet_list.dart +++ b/lib/components/adaptive/adaptive_pop_sheet_list.dart @@ -1,56 +1,34 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' show ListTile, showModalBottomSheet; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/extensions/constrains.dart'; -_emptyCB() {} - -class PopSheetEntry extends ListTile { +class AdaptiveMenuButton extends MenuButton { final T? value; - const PopSheetEntry({ - this.value, + const AdaptiveMenuButton({ super.key, - super.leading, - super.title, - super.subtitle, + this.value, + required super.child, + super.subMenu, + super.onPressed, 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.leading, 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, - }); + super.autoClose = true, + super.popoverController, + }) : assert( + value != null || onPressed != null, + 'Either value or onPressed must be provided', + ); } /// 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 List> children; final Widget? icon; final Widget? child; final bool useRootNavigator; @@ -59,7 +37,6 @@ class AdaptivePopSheetList extends StatelessWidget { final String? tooltip; final ValueChanged? onSelected; - final BorderRadius borderRadius; final Offset offset; const AdaptivePopSheetList({ @@ -70,7 +47,6 @@ class AdaptivePopSheetList extends StatelessWidget { this.useRootNavigator = true, this.headings, this.onSelected, - this.borderRadius = const BorderRadius.all(Radius.circular(999)), this.tooltip, this.offset = Offset.zero, }) : assert( @@ -78,158 +54,128 @@ class AdaptivePopSheetList extends StatelessWidget { 'Either icon or child must be provided', ); - Future showPopupMenu(BuildContext context, RelativeRect position) { + Future showDropdownMenu(BuildContext context, Offset position) async { final mediaQuery = MediaQuery.of(context); + final childrenModified = children.map((s) { + if (s.onPressed == null) { + return MenuButton( + key: s.key, + autoClose: s.autoClose, + enabled: s.enabled, + leading: s.leading, + focusNode: s.focusNode, + onPressed: (context) { + if (s.value != null) { + onSelected?.call(s.value as T); + } + }, + popoverController: s.popoverController, + subMenu: s.subMenu, + trailing: s.trailing, + child: s.child, + ); + } + return s; + }).toList(); - return showMenu( + if (mediaQuery.mdAndUp) { + await showDropdown( + context: context, + rootOverlay: useRootNavigator, + // heightConstraint: PopoverConstraint.anchorFixedSize, + // constraints: BoxConstraints( + // maxHeight: mediaQuery.size.height * 0.6, + // ), + position: position, + builder: (context) { + return DropdownMenu( + children: childrenModified, + ); + }, + ).future; + return; + } + + showModalBottomSheet( context: context, - useRootNavigator: useRootNavigator, - constraints: BoxConstraints( - maxHeight: mediaQuery.size.height * 0.6, + enableDrag: true, + showDragHandle: true, + useRootNavigator: true, + shape: RoundedRectangleBorder( + borderRadius: context.theme.borderRadiusMd, ), - position: position, - items: children - .map( - (item) => PopupMenuItem( - padding: EdgeInsets.zero, - enabled: false, - child: _AdaptivePopSheetListItem( - item: item, - onSelected: onSelected, - ), - ), - ) - .toList(), + backgroundColor: context.theme.colorScheme.card, + builder: (context) { + return ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: childrenModified.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final data = childrenModified[index]; + + return ListTile( + dense: true, + leading: data.leading, + title: data.child, + enabled: data.enabled, + trailing: data.trailing, + focusNode: data.focusNode, + onTap: () { + data.onPressed?.call(context); + if (data.autoClose) { + Navigator.of(context).pop(); + } + }, + ); + }, + ); + }, ); } @override Widget build(BuildContext context) { final mediaQuery = MediaQuery.of(context); - final theme = Theme.of(context); if (mediaQuery.mdAndUp) { - return PopupMenuButton( - icon: icon, - tooltip: tooltip, - offset: offset, - child: child == null ? null : IgnorePointer(child: child), - itemBuilder: (context) => children - .map( - (item) => PopupMenuItem( - padding: EdgeInsets.zero, - enabled: false, - child: _AdaptivePopSheetListItem( - item: item, - onSelected: onSelected, - ), + return Tooltip( + tooltip: Text(tooltip ?? ''), + child: IconButton.ghost( + icon: icon ?? const Icon(SpotubeIcons.moreVertical), + onPressed: () { + final renderBox = context.findRenderObject() as RenderBox; + final position = RelativeRect.fromRect( + Rect.fromPoints( + renderBox.localToGlobal(Offset.zero, + ancestor: context.findRenderObject()), + renderBox.localToGlobal(renderBox.size.bottomRight(Offset.zero), + ancestor: context.findRenderObject()), ), - ) - .toList(), - ); - } - - void showSheet() { - showModalBottomSheet( - context: context, - useRootNavigator: useRootNavigator, - isScrollControlled: true, - showDragHandle: true, - constraints: BoxConstraints( - maxHeight: mediaQuery.size.height * 0.6, + Offset.zero & mediaQuery.size, + ); + final offset = Offset(position.left, position.top); + showDropdownMenu(context, offset); + }, ), - builder: (context) { - return Padding( - padding: const EdgeInsets.all(8.0).copyWith(top: 0), - child: DefaultTextStyle( - style: theme.textTheme.titleMedium!, - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (headings != null) ...[ - ...headings!, - const SizedBox(height: 8), - Divider( - color: theme.colorScheme.primary, - thickness: 0.3, - endIndent: 16, - indent: 16, - ), - ], - ...children.map( - (item) => _AdaptivePopSheetListItem( - item: item, - onSelected: onSelected, - ), - ) - ], - ), - ), - ), - ); - }, ); } if (child != null) { return Tooltip( - message: tooltip ?? '', - child: InkWell( - onTap: showSheet, - borderRadius: borderRadius, + tooltip: Text(tooltip ?? ''), + child: Button( + onPressed: () => showDropdownMenu(context, Offset.zero), + style: const ButtonStyle.ghost(), child: IgnorePointer(child: child), ), ); } - return IconButton( - icon: icon ?? const Icon(SpotubeIcons.moreVertical), - tooltip: tooltip, - style: theme.iconButtonTheme.style?.copyWith( - shape: WidgetStatePropertyAll( - RoundedRectangleBorder( - borderRadius: borderRadius, - ), - ), - ), - onPressed: showSheet, - ); - } -} - -class _AdaptivePopSheetListItem extends StatelessWidget { - final PopSheetEntry item; - final ValueChanged? onSelected; - const _AdaptivePopSheetListItem({ - super.key, - required this.item, - this.onSelected, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return InkWell( - borderRadius: (theme.listTileTheme.shape as RoundedRectangleBorder?) - ?.borderRadius as BorderRadius? ?? - const BorderRadius.all(Radius.circular(10)), - onTap: !item.enabled - ? null - : () { - item.onTap?.call(); - if (item.value != null) { - Navigator.pop(context); - onSelected?.call(item.value as T); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: IconTheme.merge( - data: const IconThemeData(opacity: 1), - child: IgnorePointer(child: item), - ), + return Tooltip( + tooltip: Text(tooltip ?? ''), + child: IconButton.ghost( + icon: icon ?? const Icon(SpotubeIcons.moreVertical), + onPressed: () => showDropdownMenu(context, Offset.zero), ), ); } diff --git a/lib/components/sort_tracks_dropdown.dart b/lib/components/sort_tracks_dropdown.dart index 16727013..4f65e738 100644 --- a/lib/components/sort_tracks_dropdown.dart +++ b/lib/components/sort_tracks_dropdown.dart @@ -23,45 +23,45 @@ class SortTracksDropdown extends StatelessWidget { ), child: AdaptivePopSheetList( children: [ - PopSheetEntry( + AdaptiveMenuButton( value: SortBy.none, enabled: value != SortBy.none, - title: Text(context.l10n.none), + child: Text(context.l10n.none), ), - PopSheetEntry( + AdaptiveMenuButton( value: SortBy.ascending, enabled: value != SortBy.ascending, - title: Text(context.l10n.sort_a_z), + child: Text(context.l10n.sort_a_z), ), - PopSheetEntry( + AdaptiveMenuButton( value: SortBy.descending, enabled: value != SortBy.descending, - title: Text(context.l10n.sort_z_a), + child: Text(context.l10n.sort_z_a), ), - PopSheetEntry( + AdaptiveMenuButton( value: SortBy.newest, enabled: value != SortBy.newest, - title: Text(context.l10n.sort_newest), + child: Text(context.l10n.sort_newest), ), - PopSheetEntry( + AdaptiveMenuButton( value: SortBy.oldest, enabled: value != SortBy.oldest, - title: Text(context.l10n.sort_oldest), + child: Text(context.l10n.sort_oldest), ), - PopSheetEntry( + AdaptiveMenuButton( value: SortBy.duration, enabled: value != SortBy.duration, - title: Text(context.l10n.sort_duration), + child: Text(context.l10n.sort_duration), ), - PopSheetEntry( + AdaptiveMenuButton( value: SortBy.artist, enabled: value != SortBy.artist, - title: Text(context.l10n.sort_artist), + child: Text(context.l10n.sort_artist), ), - PopSheetEntry( + AdaptiveMenuButton( value: SortBy.album, enabled: value != SortBy.album, - title: Text(context.l10n.sort_album), + child: Text(context.l10n.sort_album), ), ], headings: [ diff --git a/lib/components/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart index d2cb92cf..28a85fd1 100644 --- a/lib/components/track_tile/track_options.dart +++ b/lib/components/track_tile/track_options.dart @@ -5,7 +5,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotify/spotify.dart'; +import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; +import 'package:spotify/spotify.dart' hide Offset; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart'; @@ -332,38 +333,46 @@ class TrackOptions extends HookConsumerWidget { ], children: [ if (isLocalTrack) - PopSheetEntry( + AdaptiveMenuButton( value: TrackOptionValue.delete, leading: const Icon(SpotubeIcons.trash), - title: Text(context.l10n.delete), + child: Text(context.l10n.delete), ), if (mediaQuery.smAndDown && !isLocalTrack) - PopSheetEntry( + AdaptiveMenuButton( value: TrackOptionValue.album, leading: const Icon(SpotubeIcons.album), - title: Text(context.l10n.go_to_album), - subtitle: Text(track.album!.name!), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(context.l10n.go_to_album), + Text( + track.album!.name!, + style: context.theme.typography.xSmall, + ), + ], + ), ), if (!playlist.containsTrack(track)) ...[ - PopSheetEntry( + AdaptiveMenuButton( value: TrackOptionValue.addToQueue, leading: const Icon(SpotubeIcons.queueAdd), - title: Text(context.l10n.add_to_queue), + child: Text(context.l10n.add_to_queue), ), - PopSheetEntry( + AdaptiveMenuButton( value: TrackOptionValue.playNext, leading: const Icon(SpotubeIcons.lightning), - title: Text(context.l10n.play_next), + child: Text(context.l10n.play_next), ), ] else - PopSheetEntry( + AdaptiveMenuButton( value: TrackOptionValue.removeFromQueue, enabled: playlist.activeTrack?.id != track.id, leading: const Icon(SpotubeIcons.queueRemove), - title: Text(context.l10n.remove_from_queue), + child: Text(context.l10n.remove_from_queue), ), if (me.asData?.value != null && !isLocalTrack) - PopSheetEntry( + AdaptiveMenuButton( value: TrackOptionValue.favorite, leading: favorites.isLiked ? const Icon( @@ -371,32 +380,32 @@ class TrackOptions extends HookConsumerWidget { color: Colors.pink, ) : const Icon(SpotubeIcons.heart), - title: Text( + child: Text( favorites.isLiked ? context.l10n.remove_from_favorites : context.l10n.save_as_favorite, ), ), if (auth.asData?.value != null && !isLocalTrack) ...[ - PopSheetEntry( + AdaptiveMenuButton( value: TrackOptionValue.startRadio, leading: const Icon(SpotubeIcons.radio), - title: Text(context.l10n.start_a_radio), + child: Text(context.l10n.start_a_radio), ), - PopSheetEntry( + AdaptiveMenuButton( value: TrackOptionValue.addToPlaylist, leading: const Icon(SpotubeIcons.playlistAdd), - title: Text(context.l10n.add_to_playlist), + child: Text(context.l10n.add_to_playlist), ), ], if (userPlaylist && auth.asData?.value != null && !isLocalTrack) - PopSheetEntry( + AdaptiveMenuButton( value: TrackOptionValue.removeFromPlaylist, leading: const Icon(SpotubeIcons.removeFilled), - title: Text(context.l10n.remove_from_playlist), + child: Text(context.l10n.remove_from_playlist), ), if (!isLocalTrack) - PopSheetEntry( + AdaptiveMenuButton( value: TrackOptionValue.download, enabled: !isInQueue, leading: isInQueue @@ -407,48 +416,56 @@ class TrackOptions extends HookConsumerWidget { ); }) : const Icon(SpotubeIcons.download), - title: Text(context.l10n.download_track), + child: Text(context.l10n.download_track), ), if (!isLocalTrack) - PopSheetEntry( + AdaptiveMenuButton( value: TrackOptionValue.blacklist, - leading: const Icon(SpotubeIcons.playlistRemove), - iconColor: isBlackListed != true ? Colors.red[400] : null, - textColor: isBlackListed != true ? Colors.red[400] : null, - title: Text( + leading: Icon( + SpotubeIcons.playlistRemove, + color: isBlackListed != true ? Colors.red[400] : null, + ), + child: Text( isBlackListed == true ? context.l10n.remove_from_blacklist : context.l10n.add_to_blacklist, + style: TextStyle( + color: isBlackListed != true ? Colors.red[400] : null, + ), ), ), if (!isLocalTrack) - PopSheetEntry( + AdaptiveMenuButton( value: TrackOptionValue.share, leading: const Icon(SpotubeIcons.share), - title: Text(context.l10n.share), + child: Text(context.l10n.share), ), if (!isLocalTrack) - PopSheetEntry( + AdaptiveMenuButton( value: TrackOptionValue.songlink, leading: Assets.logos.songlinkTransparent.image( width: 22, height: 22, color: colorScheme.onSurface.withOpacity(0.5), ), - title: Text(context.l10n.song_link), + child: Text(context.l10n.song_link), ), if (!isLocalTrack) - PopSheetEntry( + AdaptiveMenuButton( value: TrackOptionValue.details, leading: const Icon(SpotubeIcons.info), - title: Text(context.l10n.details), + child: Text(context.l10n.details), ), ], ); //! This is the most ANTI pattern I've ever done, but it works showMenuCbRef?.value = (relativeRect) { - adaptivePopSheetList.showPopupMenu(context, relativeRect); + final offsetFromRect = Offset( + relativeRect.left, + relativeRect.top, + ); + adaptivePopSheetList.showDropdownMenu(context, offsetFromRect); }; return ListTileTheme( diff --git a/lib/components/tracks_view/sections/body/track_view_options.dart b/lib/components/tracks_view/sections/body/track_view_options.dart index 23198aec..7114d713 100644 --- a/lib/components/tracks_view/sections/body/track_view_options.dart +++ b/lib/components/tracks_view/sections/body/track_view_options.dart @@ -102,35 +102,35 @@ class TrackViewBodyOptions extends HookConsumerWidget { }, icon: const Icon(SpotubeIcons.moreVertical), children: [ - PopSheetEntry( + AdaptiveMenuButton( value: "download", leading: const Icon(SpotubeIcons.download), enabled: selectedTracks.isNotEmpty, - title: Text( + child: Text( context.l10n.download_count(selectedTracks.length), ), ), - PopSheetEntry( + AdaptiveMenuButton( value: "add-to-playlist", leading: const Icon(SpotubeIcons.playlistAdd), enabled: selectedTracks.isNotEmpty, - title: Text( + child: Text( context.l10n.add_count_to_playlist(selectedTracks.length), ), ), - PopSheetEntry( + AdaptiveMenuButton( enabled: selectedTracks.isNotEmpty, value: "add-to-queue", leading: const Icon(SpotubeIcons.queueAdd), - title: Text( + child: Text( context.l10n.add_count_to_queue(selectedTracks.length), ), ), - PopSheetEntry( + AdaptiveMenuButton( enabled: selectedTracks.isNotEmpty, value: "play-next", leading: const Icon(SpotubeIcons.lightning), - title: Text( + child: Text( context.l10n.play_count_next(selectedTracks.length), ), ), diff --git a/lib/main.dart b/lib/main.dart index 3ed4314b..cd9acec2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -219,12 +219,16 @@ class Spotube extends HookConsumerWidget { theme: ThemeData( radius: .5, iconTheme: const IconThemeProperties(), - colorScheme: ColorSchemes.lightNeutral(), + colorScheme: ColorSchemes.lightBlue(), + surfaceOpacity: .9, + surfaceBlur: 10, ), darkTheme: ThemeData( radius: .5, iconTheme: const IconThemeProperties(), colorScheme: ColorSchemes.darkNeutral(), + surfaceOpacity: .9, + surfaceBlur: 10, ), themeMode: themeMode, shortcuts: { diff --git a/lib/modules/player/player_actions.dart b/lib/modules/player/player_actions.dart index a47c992d..7db65c23 100644 --- a/lib/modules/player/player_actions.dart +++ b/lib/modules/player/player_actions.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/player/sibling_tracks_sheet.dart'; @@ -76,38 +76,37 @@ class PlayerActions extends HookConsumerWidget { mainAxisAlignment: mainAxisAlignment, children: [ if (showQueue) - IconButton( - icon: const Icon(SpotubeIcons.queue), - tooltip: context.l10n.queue, - onPressed: playlist.activeTrack != null - ? () { - Scaffold.of(context).openEndDrawer(); - } - : null, + Tooltip( + tooltip: Text(context.l10n.queue), + child: IconButton.ghost( + icon: const Icon(SpotubeIcons.queue), + enabled: playlist.activeTrack != null, + onPressed: () { + // Scaffold.of(context).openEndDrawer(); + }, + ), ), if (!isLocalTrack) - IconButton( - icon: const Icon(SpotubeIcons.alternativeRoute), - tooltip: context.l10n.alternative_track_sources, - onPressed: playlist.activeTrack != null - ? () { - showModalBottomSheet( - context: context, - isDismissible: true, - enableDrag: true, - isScrollControlled: true, - backgroundColor: Colors.black12, - barrierColor: Colors.black12, - elevation: 0, - shape: RoundedRectangleBorder( + Tooltip( + tooltip: Text(context.l10n.alternative_track_sources), + child: IconButton.ghost( + icon: const Icon(SpotubeIcons.alternativeRoute), + onPressed: playlist.activeTrack != null + ? () { + openDrawer( + context: context, + position: OverlayPosition.bottom, + barrierDismissible: true, + draggable: true, + barrierColor: Colors.black.withValues(alpha: .2), borderRadius: BorderRadius.circular(10), - ), - builder: (context) { - return SiblingTracksSheet(floating: floatingQueue); - }, - ); - } - : null, + builder: (context) { + return SiblingTracksSheet(floating: floatingQueue); + }, + ); + } + : null, + ), ), if (!kIsWeb && !isLocalTrack) if (isInQueue) @@ -115,24 +114,26 @@ class PlayerActions extends HookConsumerWidget { height: 20, width: 20, child: CircularProgressIndicator( - strokeWidth: 2, + size: 2, ), ) else - IconButton( - tooltip: context.l10n.download_track, - icon: Icon( - isDownloaded ? SpotubeIcons.done : SpotubeIcons.download, + Tooltip( + tooltip: Text(context.l10n.download_track), + child: IconButton.ghost( + icon: Icon( + isDownloaded ? SpotubeIcons.done : SpotubeIcons.download, + ), + onPressed: playlist.activeTrack != null + ? () => downloader.addToQueue(playlist.activeTrack!) + : null, ), - onPressed: playlist.activeTrack != null - ? () => downloader.addToQueue(playlist.activeTrack!) - : null, ), if (playlist.activeTrack != null && !isLocalTrack && auth.asData?.value != null) TrackHeartButton(track: playlist.activeTrack!), - AdaptivePopSheetList( + AdaptivePopSheetList( offset: Offset(0, -50 * (sleepTimerEntries.values.length + 2)), headings: [ Text(context.l10n.sleep_timer), @@ -150,24 +151,40 @@ class PlayerActions extends HookConsumerWidget { }, children: [ for (final entry in sleepTimerEntries.entries) - PopSheetEntry( + AdaptiveMenuButton( value: entry.value, enabled: sleepTimer != entry.value, - title: Text(entry.key), + child: Text(entry.key), ), - PopSheetEntry( - title: Text( - customHoursEnabled - ? context.l10n.custom_hours - : sleepTimer.format(abbreviated: true), - ), - // only enabled when there's no preset timers selected + AdaptiveMenuButton( enabled: customHoursEnabled, - onTap: () async { + onPressed: (context) async { final currentTime = TimeOfDay.now(); - final time = await showTimePicker( + final time = await showDialog( context: context, - initialTime: currentTime, + builder: (context) => HookBuilder(builder: (context) { + final timeRef = useRef(null); + return AlertDialog( + title: Text( + ShadcnLocalizations.of(context).placeholderTimePicker, + ), + content: TimePickerDialog( + use24HourFormat: false, + initialValue: TimeOfDay.fromDateTime( + DateTime.now().add(sleepTimer ?? Duration.zero), + ), + onChanged: (value) => timeRef.value = value, + ), + actions: [ + Button.primary( + onPressed: () { + Navigator.of(context).pop(timeRef.value); + }, + child: Text(context.l10n.save), + ), + ], + ); + }), ); if (time != null) { @@ -179,12 +196,19 @@ class PlayerActions extends HookConsumerWidget { ); } }, + child: Text( + customHoursEnabled + ? context.l10n.custom_hours + : sleepTimer.format(abbreviated: true), + ), ), - PopSheetEntry( + AdaptiveMenuButton( value: Duration.zero, enabled: sleepTimer != Duration.zero && sleepTimer != null, - textColor: Colors.green, - title: Text(context.l10n.cancel), + child: Text( + context.l10n.cancel, + style: const TextStyle(color: Colors.green), + ), ), ], ), diff --git a/lib/modules/player/player_controls.dart b/lib/modules/player/player_controls.dart index 12288a3d..0b3f5c2b 100644 --- a/lib/modules/player/player_controls.dart +++ b/lib/modules/player/player_controls.dart @@ -1,9 +1,9 @@ -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:media_kit/media_kit.dart'; import 'package:palette_generator/palette_generator.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart' hide ThemeData; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/intents.dart'; @@ -47,44 +47,6 @@ class PlayerControls extends HookConsumerWidget { useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; final theme = Theme.of(context); - final isDominantColorDark = ThemeData.estimateBrightnessForColor( - palette?.dominantColor?.color ?? theme.colorScheme.primary, - ) == - Brightness.dark; - - final dominantColor = isDominantColorDark - ? palette?.mutedColor ?? palette?.dominantColor - : palette?.dominantColor; - - final sliderColor = - palette?.dominantColor?.titleTextColor ?? theme.colorScheme.primary; - - final buttonStyle = IconButton.styleFrom( - backgroundColor: dominantColor?.color.withOpacity(0.2) ?? - theme.colorScheme.surface.withOpacity(0.4), - minimumSize: const Size(28, 28), - ); - - final activeButtonStyle = IconButton.styleFrom( - backgroundColor: - dominantColor?.titleTextColor ?? theme.colorScheme.primaryContainer, - foregroundColor: - dominantColor?.color ?? theme.colorScheme.onPrimaryContainer, - minimumSize: const Size(28, 28), - ); - - final accentColor = palette?.lightVibrantColor ?? - palette?.darkVibrantColor ?? - dominantColor; - - final resumePauseStyle = IconButton.styleFrom( - backgroundColor: accentColor?.color ?? theme.colorScheme.primary, - foregroundColor: - accentColor?.titleTextColor ?? theme.colorScheme.onPrimary, - padding: EdgeInsets.all(compact ? 10 : 12), - iconSize: compact ? 18 : 24, - ); - return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { @@ -122,45 +84,41 @@ class PlayerControls extends HookConsumerWidget { return Column( children: [ Tooltip( - message: context.l10n.slide_to_seek, + tooltip: Text(context.l10n.slide_to_seek), child: Slider( - // cannot divide by zero - // there's an edge case for value being bigger - // than total duration. Keeping it resolved - value: progress.value.toDouble(), - secondaryTrackValue: bufferProgress, + value: + SliderValue.single(progress.value.toDouble()), onChanged: isFetchingActiveTrack ? null : (v) { - progress.value = v; + progress.value = v.value; }, onChangeEnd: (value) async { await audioPlayer.seek( Duration( - seconds: (value * duration.inSeconds).toInt(), + seconds: (value.value * duration.inSeconds) + .toInt(), ), ); }, - activeColor: sliderColor, - secondaryActiveColor: sliderColor.withOpacity(0.2), - inactiveColor: sliderColor.withOpacity(0.15), ), ), Padding( padding: const EdgeInsets.symmetric( horizontal: 8.0, ), - child: DefaultTextStyle( - style: theme.textTheme.bodySmall!.copyWith( - color: palette?.dominantColor?.bodyTextColor, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(position.toHumanReadableString()), - Text(duration.toHumanReadableString()), - ], - ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + position.toHumanReadableString(), + style: theme.typography.xSmall, + ), + Text( + duration.toHumanReadableString(), + style: theme.typography.xSmall, + ), + ], ), ), ], @@ -173,92 +131,104 @@ class PlayerControls extends HookConsumerWidget { Consumer(builder: (context, ref, _) { final shuffled = ref .watch(audioPlayerProvider.select((s) => s.shuffled)); - return IconButton( - tooltip: shuffled - ? context.l10n.unshuffle_playlist - : context.l10n.shuffle_playlist, - icon: const Icon(SpotubeIcons.shuffle), - style: shuffled ? activeButtonStyle : buttonStyle, - onPressed: isFetchingActiveTrack - ? null - : () { - if (shuffled) { - audioPlayer.setShuffle(false); - } else { - audioPlayer.setShuffle(true); - } - }, + return Tooltip( + tooltip: Text( + shuffled + ? context.l10n.unshuffle_playlist + : context.l10n.shuffle_playlist, + ), + child: IconButton( + icon: const Icon(SpotubeIcons.shuffle), + variance: shuffled + ? ButtonVariance.secondary + : ButtonVariance.ghost, + onPressed: isFetchingActiveTrack + ? null + : () { + if (shuffled) { + audioPlayer.setShuffle(false); + } else { + audioPlayer.setShuffle(true); + } + }, + ), ); }), - IconButton( - tooltip: context.l10n.previous_track, - icon: const Icon(SpotubeIcons.skipBack), - style: buttonStyle, - onPressed: isFetchingActiveTrack - ? null - : audioPlayer.skipToPrevious, + Tooltip( + tooltip: Text(context.l10n.previous_track), + child: IconButton.ghost( + enabled: !isFetchingActiveTrack, + icon: const Icon(SpotubeIcons.skipBack), + onPressed: audioPlayer.skipToPrevious, + ), ), - IconButton( - tooltip: playing - ? context.l10n.pause_playback - : context.l10n.resume_playback, - icon: isFetchingActiveTrack - ? SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - color: accentColor?.titleTextColor ?? - theme.colorScheme.onPrimary, + Tooltip( + tooltip: Text( + playing + ? context.l10n.pause_playback + : context.l10n.resume_playback, + ), + child: IconButton.primary( + shape: ButtonShape.circle, + icon: isFetchingActiveTrack + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(), + ) + : Icon( + playing ? SpotubeIcons.pause : SpotubeIcons.play, ), - ) - : Icon( - playing ? SpotubeIcons.pause : SpotubeIcons.play, - ), - style: resumePauseStyle, - onPressed: isFetchingActiveTrack - ? null - : Actions.handler( - context, - PlayPauseIntent(ref), - ), + onPressed: isFetchingActiveTrack + ? null + : Actions.handler( + context, + PlayPauseIntent(ref), + ), + ), ), - IconButton( - tooltip: context.l10n.next_track, - icon: const Icon(SpotubeIcons.skipForward), - style: buttonStyle, - onPressed: - isFetchingActiveTrack ? null : audioPlayer.skipToNext, + Tooltip( + tooltip: Text(context.l10n.next_track), + child: IconButton.ghost( + icon: const Icon(SpotubeIcons.skipForward), + onPressed: + isFetchingActiveTrack ? null : audioPlayer.skipToNext, + ), ), Consumer(builder: (context, ref, _) { final loopMode = ref .watch(audioPlayerProvider.select((s) => s.loopMode)); - return IconButton( - tooltip: loopMode == PlaylistMode.single - ? context.l10n.loop_track - : loopMode == PlaylistMode.loop - ? context.l10n.repeat_playlist - : null, - icon: Icon( + return Tooltip( + tooltip: Text( loopMode == PlaylistMode.single - ? SpotubeIcons.repeatOne - : SpotubeIcons.repeat, + ? context.l10n.loop_track + : loopMode == PlaylistMode.loop + ? context.l10n.repeat_playlist + : "", + ), + child: IconButton( + icon: Icon( + loopMode == PlaylistMode.single + ? SpotubeIcons.repeatOne + : SpotubeIcons.repeat, + ), + variance: loopMode == PlaylistMode.single || + loopMode == PlaylistMode.loop + ? ButtonVariance.secondary + : ButtonVariance.ghost, + onPressed: isFetchingActiveTrack + ? null + : () async { + await audioPlayer.setLoopMode( + switch (loopMode) { + PlaylistMode.loop => PlaylistMode.single, + PlaylistMode.single => PlaylistMode.none, + PlaylistMode.none => PlaylistMode.loop, + }, + ); + }, ), - style: loopMode == PlaylistMode.single || - loopMode == PlaylistMode.loop - ? activeButtonStyle - : buttonStyle, - onPressed: isFetchingActiveTrack - ? null - : () async { - await audioPlayer.setLoopMode( - switch (loopMode) { - PlaylistMode.loop => PlaylistMode.single, - PlaylistMode.single => PlaylistMode.none, - PlaylistMode.none => PlaylistMode.loop, - }, - ); - }, ); }), ], diff --git a/lib/modules/player/volume_slider.dart b/lib/modules/player/volume_slider.dart index 8483143b..515f1fbc 100644 --- a/lib/modules/player/volume_slider.dart +++ b/lib/modules/player/volume_slider.dart @@ -1,6 +1,7 @@ import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotube/collections/spotube_icons.dart'; class VolumeSlider extends HookConsumerWidget { @@ -30,17 +31,11 @@ class VolumeSlider extends HookConsumerWidget { } } }, - child: SliderTheme( - data: const SliderThemeData( - showValueIndicator: ShowValueIndicator.always, - ), - child: Slider( - min: 0, - max: 1, - label: (value * 100).toStringAsFixed(0), - value: value, - onChanged: onChanged, - ), + child: Slider( + min: 0, + max: 1, + value: SliderValue.single(value), + onChanged: (v) => onChanged(v.value), ), ); return Row( @@ -48,6 +43,7 @@ class VolumeSlider extends HookConsumerWidget { !fullWidth ? MainAxisAlignment.center : MainAxisAlignment.start, children: [ IconButton( + variance: ButtonVariance.ghost, icon: Icon( value == 0 ? SpotubeIcons.volumeMute diff --git a/lib/modules/root/bottom_player.dart b/lib/modules/root/bottom_player.dart index a2f45449..f435eefb 100644 --- a/lib/modules/root/bottom_player.dart +++ b/lib/modules/root/bottom_player.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; @@ -16,7 +17,6 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/hooks/utils/use_brightness_value.dart'; -import 'package:flutter/material.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -46,7 +46,7 @@ class BottomPlayer extends HookConsumerWidget { ); final theme = Theme.of(context); - final bg = theme.colorScheme.surfaceContainerHighest; + final bg = theme.colorScheme.background; final bgColor = useBrightnessValue( Color.lerp(bg, Colors.white, 0.7), @@ -64,31 +64,30 @@ class BottomPlayer extends HookConsumerWidget { child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), child: DecoratedBox( - decoration: BoxDecoration(color: bgColor?.withOpacity(0.8)), - child: Material( - type: MaterialType.transparency, - textStyle: theme.textTheme.bodyMedium!, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: PlayerTrackDetails(track: playlist.activeTrack), + decoration: BoxDecoration(color: bgColor?.withValues(alpha: .8)), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: PlayerTrackDetails(track: playlist.activeTrack), + ), + // controls + const Flexible( + flex: 3, + child: Padding( + padding: EdgeInsets.only(top: 5), + child: PlayerControls(), ), - // controls - const Flexible( - flex: 3, - child: Padding( - padding: EdgeInsets.only(top: 5), - child: PlayerControls(), - ), - ), - // add to saved tracks - Column( - children: [ - PlayerActions( - extraActions: [ - IconButton( - tooltip: context.l10n.mini_player, + ), + // add to saved tracks + Column( + children: [ + PlayerActions( + extraActions: [ + Tooltip( + tooltip: Text(context.l10n.mini_player), + child: IconButton( + variance: ButtonVariance.ghost, icon: const Icon(SpotubeIcons.miniPlayer), onPressed: () async { if (!kIsDesktop) return; @@ -107,35 +106,37 @@ class BottomPlayer extends HookConsumerWidget { await Future.delayed( const Duration(milliseconds: 100), () async { - GoRouter.of(context).go( - '/mini-player', - extra: prevSize, - ); + if (context.mounted) { + context.go( + '/mini-player', + extra: prevSize, + ); + } }, ); }, ), - ], - ), - Container( - height: 40, - constraints: const BoxConstraints(maxWidth: 250), - padding: const EdgeInsets.only(right: 10), - child: Consumer(builder: (context, ref, _) { - final volume = ref.watch(volumeProvider); - return VolumeSlider( - fullWidth: true, - value: volume, - onChanged: (value) { - ref.read(volumeProvider.notifier).setVolume(value); - }, - ); - }), - ) - ], - ), - ], - ), + ), + ], + ), + Container( + height: 40, + constraints: const BoxConstraints(maxWidth: 250), + padding: const EdgeInsets.only(right: 10), + child: Consumer(builder: (context, ref, _) { + final volume = ref.watch(volumeProvider); + return VolumeSlider( + fullWidth: true, + value: volume, + onChanged: (value) { + ref.read(volumeProvider.notifier).setVolume(value); + }, + ); + }), + ) + ], + ), + ], ), ), ),