diff --git a/lib/components/button/back_button.dart b/lib/components/button/back_button.dart new file mode 100644 index 00000000..784f8e6b --- /dev/null +++ b/lib/components/button/back_button.dart @@ -0,0 +1,14 @@ +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:spotube/collections/spotube_icons.dart'; + +class BackButton extends StatelessWidget { + const BackButton({super.key}); + + @override + Widget build(BuildContext context) { + return IconButton.ghost( + icon: const Icon(SpotubeIcons.angleLeft), + onPressed: () => Navigator.of(context).pop(), + ); + } +} diff --git a/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart b/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart index 1093fff0..f41e0709 100644 --- a/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart +++ b/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart @@ -1,8 +1,8 @@ import 'dart:ui'; -import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:gap/gap.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/fake.dart'; @@ -37,7 +37,6 @@ class HorizontalPlaybuttonCardView extends HookWidget { @override Widget build(BuildContext context) { - final ThemeData(:textTheme) = Theme.of(context); final scrollController = useScrollController(); final height = useBreakpointValue( xs: 226, @@ -56,7 +55,7 @@ class HorizontalPlaybuttonCardView extends HookWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ DefaultTextStyle( - style: textTheme.titleMedium!, + style: context.theme.typography.h4, child: title, ), if (titleTrailing != null) titleTrailing!, diff --git a/lib/components/themed_button_tab_bar.dart b/lib/components/themed_button_tab_bar.dart deleted file mode 100644 index c245e5f4..00000000 --- a/lib/components/themed_button_tab_bar.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:buttons_tabbar/buttons_tabbar.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:spotube/hooks/utils/use_brightness_value.dart'; - -class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget { - final List tabs; - final TabController? controller; - const ThemedButtonsTabBar({super.key, required this.tabs, this.controller}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final bgColor = useBrightnessValue( - theme.colorScheme.primaryContainer, - Color.lerp(theme.colorScheme.primary, Colors.black, 0.7)!, - ); - - return Padding( - padding: const EdgeInsets.only( - top: 8, - bottom: 8, - ), - child: ButtonsTabBar( - controller: controller, - radius: 100, - decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.circular(15), - ), - labelStyle: theme.textTheme.labelLarge?.copyWith( - color: theme.colorScheme.primary, - fontWeight: FontWeight.bold, - ), - borderWidth: 0, - unselectedDecoration: BoxDecoration( - color: theme.colorScheme.surface, - borderRadius: BorderRadius.circular(15), - ), - unselectedLabelStyle: theme.textTheme.labelLarge?.copyWith( - color: theme.colorScheme.primary, - ), - tabs: tabs, - ), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(50); -} diff --git a/lib/modules/home/sections/featured.dart b/lib/modules/home/sections/featured.dart index 4f30c342..9ccc8908 100644 --- a/lib/modules/home/sections/featured.dart +++ b/lib/modules/home/sections/featured.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart' hide Page; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; diff --git a/lib/modules/home/sections/feed.dart b/lib/modules/home/sections/feed.dart index 8685fe19..bce2ea5b 100644 --- a/lib/modules/home/sections/feed.dart +++ b/lib/modules/home/sections/feed.dart @@ -1,5 +1,5 @@ -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'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/extensions/context.dart'; @@ -40,9 +40,9 @@ class HomePageFeedSection extends HookConsumerWidget { onFetchMore: () {}, titleTrailing: Directionality( textDirection: TextDirection.rtl, - child: TextButton.icon( - label: Text(context.l10n.browse_more), - icon: const Icon(SpotubeIcons.angleRight), + child: Button.link( + leading: const Icon(SpotubeIcons.angleRight), + child: Text(context.l10n.browse_more), onPressed: () => ServiceUtils.pushNamed( context, HomeFeedSectionPage.name, diff --git a/lib/modules/home/sections/friends.dart b/lib/modules/home/sections/friends.dart index 6f59c209..00f4a86a 100644 --- a/lib/modules/home/sections/friends.dart +++ b/lib/modules/home/sections/friends.dart @@ -1,8 +1,9 @@ import 'dart:ui'; -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:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/modules/home/sections/friends/friend_item.dart'; @@ -75,7 +76,7 @@ class HomePageFriendsSection extends HookConsumerWidget { padding: const EdgeInsets.all(8.0), child: Text( context.l10n.friends, - style: Theme.of(context).textTheme.titleMedium, + style: context.theme.typography.h4, ), ), ), diff --git a/lib/modules/home/sections/friends/friend_item.dart b/lib/modules/home/sections/friends/friend_item.dart index 773a4a8c..42ec2909 100644 --- a/lib/modules/home/sections/friends/friend_item.dart +++ b/lib/modules/home/sections/friends/friend_item.dart @@ -1,8 +1,8 @@ import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +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/components/image/universal_image.dart'; import 'package:spotube/models/spotify_friends.dart'; @@ -20,27 +20,15 @@ class FriendItem extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final ThemeData( - textTheme: textTheme, - colorScheme: colorScheme, - ) = Theme.of(context); - final spotify = ref.watch(spotifyProvider); - return Container( + return Card( padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest.withOpacity(0.3), - borderRadius: BorderRadius.circular(15), - ), - constraints: const BoxConstraints( - minWidth: 300, - ), - height: 80, child: Row( children: [ - CircleAvatar( - backgroundImage: UniversalImage.imageProvider( + Avatar( + initials: Avatar.getInitials(friend.user.name), + provider: UniversalImage.imageProvider( friend.user.imageUrl, ), ), @@ -50,11 +38,10 @@ class FriendItem extends HookConsumerWidget { children: [ Text( friend.user.name, - style: textTheme.bodyLarge, + style: context.theme.typography.bold, ), RichText( text: TextSpan( - style: textTheme.bodySmall, children: [ TextSpan( text: friend.track.name, diff --git a/lib/modules/home/sections/genres.dart b/lib/modules/home/sections/genres.dart index 5f2dfa5e..574f3294 100644 --- a/lib/modules/home/sections/genres.dart +++ b/lib/modules/home/sections/genres.dart @@ -1,10 +1,10 @@ import 'dart:math'; -import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/fake.dart'; @@ -22,7 +22,6 @@ class HomeGenresSection extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final ThemeData(:textTheme, :colorScheme) = Theme.of(context); final mediaQuery = MediaQuery.of(context); final categoriesQuery = ref.watch(categoriesProvider); @@ -46,21 +45,18 @@ class HomeGenresSection extends HookConsumerWidget { children: [ Text( context.l10n.genres, - style: textTheme.headlineSmall, + style: context.theme.typography.h4, ), Directionality( textDirection: TextDirection.rtl, - child: TextButton.icon( + child: Button.link( onPressed: () { context.pushNamed(GenrePage.name); }, - icon: const Icon(SpotubeIcons.angleRight), - label: Text( + leading: const Icon(SpotubeIcons.angleRight), + child: Text( context.l10n.browse_all, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.secondary, - ), - ), + ).muted(), ), ), ], @@ -96,12 +92,12 @@ class HomeGenresSection extends HookConsumerWidget { final text = gradient.colors .take(2) .any((c) => c.computeLuminance() > 0.5) - ? Colors.grey[900] + ? Colors.gray[900] : Colors.white; return ( gradient: LinearGradient( colors: gradient.colors - .map((c) => c.withOpacity(0.8)) + .map((c) => c.withAlpha((0.8 * 255).ceil())) .toList(), ), textColor: text @@ -110,40 +106,42 @@ class HomeGenresSection extends HookConsumerWidget { [], ); - return InkWell( - onTap: () { - context.pushNamed( - GenrePlaylistsPage.name, - pathParameters: { - "categoryId": category.id!, - }, - extra: category, - ); - }, - borderRadius: BorderRadius.circular(8), - child: Ink( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - image: DecorationImage( - image: UniversalImage.imageProvider( - category.icons!.first.url!, - ), - fit: BoxFit.cover, - ), - ), - child: Ink( + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + context.pushNamed( + GenrePlaylistsPage.name, + pathParameters: { + "categoryId": category.id!, + }, + extra: category, + ); + }, + child: Container( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: colorScheme.surfaceContainerHighest, - gradient: categoriesQuery.isLoading ? null : gradient, + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: UniversalImage.imageProvider( + category.icons!.first.url!, + ), + fit: BoxFit.cover, + ), ), - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - category.name!, - style: textTheme.titleMedium - ?.copyWith(color: textColor), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: context.theme.colorScheme.muted, + gradient: + categoriesQuery.isLoading ? null : gradient, + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + category.name!, + style: context.theme.typography.large, + ), ), ), ), diff --git a/lib/modules/home/sections/made_for_user.dart b/lib/modules/home/sections/made_for_user.dart index 1b9854d3..4fd025d5 100644 --- a/lib/modules/home/sections/made_for_user.dart +++ b/lib/modules/home/sections/made_for_user.dart @@ -1,5 +1,5 @@ -import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/provider/spotify/spotify.dart'; diff --git a/lib/modules/home/sections/new_releases.dart b/lib/modules/home/sections/new_releases.dart index e2b32741..2ebbbee0 100644 --- a/lib/modules/home/sections/new_releases.dart +++ b/lib/modules/home/sections/new_releases.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart' hide Page; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/modules/home/sections/recent.dart b/lib/modules/home/sections/recent.dart index 43c0459d..5420ad55 100644 --- a/lib/modules/home/sections/recent.dart +++ b/lib/modules/home/sections/recent.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; diff --git a/lib/modules/library/user_albums.dart b/lib/modules/library/user_albums.dart index 37fca7c0..4a22bbea 100644 --- a/lib/modules/library/user_albums.dart +++ b/lib/modules/library/user_albums.dart @@ -1,8 +1,8 @@ -import 'package:flutter/material.dart' hide Image; +import 'package:shadcn_flutter/shadcn_flutter.dart' hide Image; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:collection/collection.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; -import 'package:gap/gap.dart'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/fake.dart'; @@ -52,7 +52,7 @@ class UserAlbums extends HookConsumerWidget { return SafeArea( child: Scaffold( - body: RefreshIndicator( + child: RefreshTrigger( onRefresh: () async { ref.invalidate(favoriteAlbumsProvider); }, @@ -62,13 +62,17 @@ class UserAlbums extends HookConsumerWidget { controller: controller, slivers: [ SliverAppBar( + backgroundColor: Theme.of(context).colorScheme.background, floating: true, flexibleSpace: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: SearchBar( - onChanged: (value) => searchText.value = value, - leading: const Icon(SpotubeIcons.filter), - hintText: context.l10n.filter_albums, + child: SizedBox( + height: 48, + child: TextField( + onChanged: (value) => searchText.value = value, + leading: const Icon(SpotubeIcons.filter), + placeholder: Text(context.l10n.filter_artist), + ), ), ), ), diff --git a/lib/modules/library/user_artists.dart b/lib/modules/library/user_artists.dart index 7968d91c..83a321fc 100644 --- a/lib/modules/library/user_artists.dart +++ b/lib/modules/library/user_artists.dart @@ -1,9 +1,9 @@ -import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:collection/collection.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; -import 'package:gap/gap.dart'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/fake.dart'; @@ -54,7 +54,7 @@ class UserArtists extends HookConsumerWidget { return SafeArea( child: Scaffold( - body: RefreshIndicator( + child: RefreshTrigger( onRefresh: () async { ref.invalidate(followedArtistsProvider); }, @@ -66,11 +66,15 @@ class UserArtists extends HookConsumerWidget { controller: controller, slivers: [ SliverAppBar( + backgroundColor: Theme.of(context).colorScheme.background, floating: true, - flexibleSpace: SearchBar( - onChanged: (value) => searchText.value = value, - leading: const Icon(SpotubeIcons.filter), - hintText: context.l10n.filter_artist, + flexibleSpace: SizedBox( + height: 48, + child: TextField( + onChanged: (value) => searchText.value = value, + leading: const Icon(SpotubeIcons.filter), + placeholder: Text(context.l10n.filter_artist), + ), ), ), const SliverGap(10), diff --git a/lib/modules/library/user_playlists.dart b/lib/modules/library/user_playlists.dart index 577f9655..50595298 100644 --- a/lib/modules/library/user_playlists.dart +++ b/lib/modules/library/user_playlists.dart @@ -1,9 +1,10 @@ -import 'package:flutter/material.dart' hide Image; +import 'package:flutter/material.dart' show kToolbarHeight; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:collection/collection.dart'; -import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart' hide Image; +import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; @@ -79,7 +80,7 @@ class UserPlaylists extends HookConsumerWidget { return const AnonymousFallback(); } - return RefreshIndicator( + return RefreshTrigger( onRefresh: () async { ref.invalidate(favoritePlaylistsProvider); }, @@ -91,11 +92,13 @@ class UserPlaylists extends HookConsumerWidget { slivers: [ SliverAppBar( floating: true, - flexibleSpace: Padding( + backgroundColor: context.theme.colorScheme.background, + flexibleSpace: Container( padding: const EdgeInsets.symmetric(horizontal: 8), - child: SearchBar( + height: 48, + child: TextField( onChanged: (value) => searchText.value = value, - hintText: context.l10n.filter_playlists, + placeholder: Text(context.l10n.filter_playlists), leading: const Icon(SpotubeIcons.filter), ), ), @@ -107,12 +110,14 @@ class UserPlaylists extends HookConsumerWidget { const Gap(10), const PlaylistCreateDialogButton(), const Gap(10), - ElevatedButton.icon( - icon: const Icon(SpotubeIcons.magic), - label: Text(context.l10n.generate_playlist), + Button.primary( + leading: const Icon(SpotubeIcons.magic), + child: Text(context.l10n.generate_playlist), onPressed: () { ServiceUtils.pushNamed( - context, PlaylistGeneratorPage.name); + context, + PlaylistGeneratorPage.name, + ); }, ), const Gap(10), diff --git a/lib/modules/root/sidebar.dart b/lib/modules/root/sidebar.dart index f045c23d..1afa85c5 100644 --- a/lib/modules/root/sidebar.dart +++ b/lib/modules/root/sidebar.dart @@ -104,7 +104,7 @@ class Sidebar extends HookConsumerWidget { index: selectedIndex, onSelected: (index) { final tile = sidebarTileList[index]; - ServiceUtils.pushNamed(context, tile.name); + context.goNamed(tile.name); }, children: navigationButtons, ) @@ -113,13 +113,13 @@ class Sidebar extends HookConsumerWidget { index: selectedIndex, onSelected: (index) { final tile = sidebarTileList[index]; - ServiceUtils.pushNamed(context, tile.name); + context.goNamed(tile.name); }, children: navigationButtons, ), ), const SidebarFooter(), - const Gap(130) + if (mediaQuery.lgAndUp) const Gap(130) else const Gap(65), ], ), const VerticalDivider(), diff --git a/lib/modules/stats/top/top.dart b/lib/modules/stats/top/top.dart index 643064aa..5c2cfbf9 100644 --- a/lib/modules/stats/top/top.dart +++ b/lib/modules/stats/top/top.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/components/themed_button_tab_bar.dart'; import 'package:spotube/modules/stats/top/albums.dart'; import 'package:spotube/modules/stats/top/artists.dart'; import 'package:spotube/modules/stats/top/tracks.dart'; @@ -23,7 +22,7 @@ class StatsPageTopSection extends HookConsumerWidget { slivers: [ SliverAppBar( floating: true, - flexibleSpace: ThemedButtonsTabBar( + flexibleSpace: TabBar( controller: tabController, tabs: [ Tab( diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index eede8dfd..2dfbc0f3 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -1,7 +1,8 @@ -import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:gap/gap.dart'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/models/database/database.dart'; @@ -34,18 +35,22 @@ class HomePage extends HookConsumerWidget { return SafeArea( bottom: false, child: Scaffold( - appBar: kIsMobile || kIsMacOS ? null : const TitleBar(), - body: CustomScrollView( + headers: [ + if (kIsWindows || kIsLinux) const TitleBar(), + ], + child: CustomScrollView( controller: controller, slivers: [ if (mediaQuery.smAndDown || layoutMode == LayoutMode.compact) SliverAppBar( floating: true, title: Assets.spotubeLogoPng.image(height: 45), + backgroundColor: context.theme.colorScheme.background, + foregroundColor: context.theme.colorScheme.foreground, actions: [ const ConnectDeviceButton(), const Gap(10), - IconButton( + IconButton.ghost( icon: const Icon(SpotubeIcons.settings, size: 20), onPressed: () { ServiceUtils.pushNamed(context, SettingsPage.name); diff --git a/lib/pages/lyrics/lyrics.dart b/lib/pages/lyrics/lyrics.dart index 48005f6e..17e552e6 100644 --- a/lib/pages/lyrics/lyrics.dart +++ b/lib/pages/lyrics/lyrics.dart @@ -1,14 +1,12 @@ import 'dart:ui'; -import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:gap/gap.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/components/titlebar/titlebar.dart'; import 'package:spotube/components/image/universal_image.dart'; -import 'package:spotube/components/themed_button_tab_bar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; @@ -39,6 +37,7 @@ class LyricsPage extends HookConsumerWidget { final palette = usePaletteColor(albumArt, ref); final mediaQuery = MediaQuery.of(context); final route = ModalRoute.of(context); + final selectedIndex = useState(0); final resetStatusBar = useCustomStatusBarColor( palette.color, @@ -46,134 +45,134 @@ class LyricsPage extends HookConsumerWidget { noSetBGColor: true, ); - PreferredSizeWidget tabbar = ThemedButtonsTabBar( - tabs: [ - Tab(text: " ${context.l10n.synced} "), - Tab(text: " ${context.l10n.plain} "), - ], + Widget tabbar = Padding( + padding: const EdgeInsets.all(10), + child: Opacity( + opacity: 0.8, + child: Tabs( + index: selectedIndex.value, + onChanged: (index) => selectedIndex.value = index, + tabs: [ + Text(context.l10n.synced), + Text(context.l10n.plain), + ], + ), + ), ); - tabbar = PreferredSize( - preferredSize: tabbar.preferredSize, - child: Row( - children: [ - tabbar, - const Spacer(), - Consumer( - builder: (context, ref, child) { - final playback = ref.watch(audioPlayerProvider); - final lyric = - ref.watch(syncedLyricsProvider(playback.activeTrack)); - final providerName = lyric.asData?.value.provider; + tabbar = Row( + children: [ + tabbar, + const Spacer(), + Consumer( + builder: (context, ref, child) { + final playback = ref.watch(audioPlayerProvider); + final lyric = ref.watch(syncedLyricsProvider(playback.activeTrack)); + final providerName = lyric.asData?.value.provider; - if (providerName == null) { - return const SizedBox.shrink(); - } + if (providerName == null) { + return const SizedBox.shrink(); + } - return Align( - alignment: Alignment.bottomRight, - child: Text(context.l10n.powered_by_provider(providerName)), - ); - }, - ), - const Gap(5), - ], - ), + return Align( + alignment: Alignment.bottomRight, + child: Text(context.l10n.powered_by_provider(providerName)), + ); + }, + ), + const Gap(5), + ], ); if (isModal) { return PopScope( canPop: true, onPopInvokedWithResult: (_, __) => resetStatusBar(), - child: DefaultTabController( - length: 2, - child: SafeArea( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), - child: Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface.withOpacity(.4), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(10), - topRight: Radius.circular(10), - ), + child: SafeArea( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background.withOpacity(.4), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10), ), - child: Column( - children: [ - const SizedBox(height: 5), - Container( - height: 7, - width: 150, - decoration: BoxDecoration( - color: palette.titleTextColor, - borderRadius: BorderRadius.circular(10), - ), + ), + child: Column( + children: [ + const SizedBox(height: 5), + Container( + height: 7, + width: 150, + decoration: BoxDecoration( + color: palette.titleTextColor, + borderRadius: BorderRadius.circular(10), ), - AppBar( - leadingWidth: double.infinity, - leading: tabbar, - backgroundColor: Colors.transparent, - automaticallyImplyLeading: false, - actions: [ - IconButton( - icon: const Icon(SpotubeIcons.minimize), - onPressed: () => Navigator.of(context).pop(), - ), - const SizedBox(width: 5), + ), + AppBar( + leading: [tabbar], + backgroundColor: Colors.transparent, + trailing: [ + IconButton.ghost( + icon: const Icon(SpotubeIcons.minimize), + onPressed: () => Navigator.of(context).pop(), + ), + const SizedBox(width: 5), + ], + ), + Expanded( + child: IndexedStack( + index: selectedIndex.value, + children: [ + SyncedLyrics(palette: palette, isModal: isModal), + PlainLyrics(palette: palette, isModal: isModal), ], ), - Expanded( - child: TabBarView( - children: [ - SyncedLyrics(palette: palette, isModal: isModal), - PlainLyrics(palette: palette, isModal: isModal), - ], - ), - ), - ], - ), + ), + ], ), ), ), ), ); } - return DefaultTabController( - length: 2, - child: SafeArea( - bottom: mediaQuery.mdAndUp, - child: Scaffold( - extendBodyBehindAppBar: true, - appBar: !kIsMacOS + return SafeArea( + bottom: mediaQuery.mdAndUp, + child: Scaffold( + floatingHeader: true, + headers: [ + !kIsMacOS ? TitleBar( backgroundColor: Colors.transparent, title: tabbar, ) - : tabbar, - body: Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - image: DecorationImage( - image: UniversalImage.imageProvider(albumArt), - fit: BoxFit.cover, - ), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(10), - ), + : tabbar + ], + child: Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + image: DecorationImage( + image: UniversalImage.imageProvider(albumArt), + fit: BoxFit.cover, ), - margin: const EdgeInsets.only(bottom: 10), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), - child: ColoredBox( - color: palette.color.withOpacity(.7), - child: SafeArea( - child: TabBarView( - children: [ - SyncedLyrics(palette: palette, isModal: isModal), - PlainLyrics(palette: palette, isModal: isModal), - ], - ), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(10), + ), + ), + margin: const EdgeInsets.only(bottom: 10), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: ColoredBox( + color: palette.color.withOpacity(.7), + child: SafeArea( + child: IndexedStack( + index: selectedIndex.value, + children: [ + SyncedLyrics(palette: palette, isModal: isModal), + PlainLyrics(palette: palette, isModal: isModal), + ], ), ), ), diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index c413df68..ba5cce83 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -1,21 +1,17 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart' hide Page; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fuzzywuzzy/fuzzywuzzy.dart'; -import 'package:gap/gap.dart'; -import 'package:go_router/go_router.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:flutter/services.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotify/spotify.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:fuzzywuzzy/fuzzywuzzy.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/utils/use_force_update.dart'; import 'package:spotube/pages/search/sections/albums.dart'; import 'package:spotube/pages/search/sections/artists.dart'; import 'package:spotube/pages/search/sections/playlists.dart'; @@ -23,7 +19,6 @@ import 'package:spotube/pages/search/sections/tracks.dart'; import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; - import 'package:spotube/utils/platform.dart'; class SearchPage extends HookConsumerWidget { @@ -36,6 +31,7 @@ class SearchPage extends HookConsumerWidget { final theme = Theme.of(context); final searchTerm = ref.watch(searchTermStateProvider); final controller = useSearchController(); + final focusNode = useFocusNode(); final auth = ref.watch(authenticationProvider); final mediaQuery = MediaQuery.of(context); @@ -84,117 +80,92 @@ class SearchPage extends HookConsumerWidget { }, ); + void onSubmitted(String value) { + ref.read(searchTermStateProvider.notifier).state = value; + if (value.trim().isEmpty) { + return; + } + KVStoreService.setRecentSearches( + { + value, + ...KVStoreService.recentSearches, + }.toList(), + ); + } + return SafeArea( bottom: false, child: Scaffold( - appBar: kIsDesktop && !kIsMacOS - ? const TitleBar(automaticallyImplyLeading: true) - : null, - body: auth.asData?.value == null + headers: [ + if (kIsWindows || kIsLinux) + const TitleBar(automaticallyImplyLeading: true) + ], + child: auth.asData?.value == null ? const AnonymousFallback() : Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - if ((kIsMobile || kIsMacOS) && context.canPop()) - const BackButton() - else - const Gap(20), Expanded( child: Padding( - padding: const EdgeInsets.only( - right: 20, - top: 20, - bottom: 20, - ), - child: SearchAnchor( - searchController: controller, - viewBuilder: (_) => HookBuilder(builder: (context) { - final searchController = - useListenable(controller); - final update = useForceUpdate(); - final suggestions = searchController.text.isEmpty - ? KVStoreService.recentSearches - : KVStoreService.recentSearches - .where( - (s) => - weightedRatio( - s.toLowerCase(), - searchController.text - .toLowerCase(), - ) > - 50, - ) - .toList(); + padding: const EdgeInsets.all(20), + child: ListenableBuilder( + listenable: controller, + builder: (context, _) { + final suggestions = controller.text.isEmpty + ? KVStoreService.recentSearches + : KVStoreService.recentSearches + .where( + (s) => + weightedRatio( + s.toLowerCase(), + controller.text.toLowerCase(), + ) > + 50, + ) + .toList(); - return ListView.builder( - itemCount: suggestions.length, - itemBuilder: (context, index) { - final suggestion = suggestions[index]; + return KeyboardListener( + focusNode: focusNode, + autofocus: true, + onKeyEvent: (value) { + final isEnter = value.logicalKey == + LogicalKeyboardKey.enter; - return ListTile( - leading: const Icon(SpotubeIcons.history), - title: Text(suggestion), - trailing: IconButton( - icon: const Icon(SpotubeIcons.trash), + if (isEnter) { + onSubmitted(controller.text); + focusNode.unfocus(); + } + }, + child: AutoComplete( + autofocus: true, + controller: controller, + suggestions: suggestions, + leading: const Icon(SpotubeIcons.search), + textInputAction: TextInputAction.search, + placeholder: Text(context.l10n.search), + trailing: IconButton.ghost( + size: ButtonSize.small, + icon: const Icon(SpotubeIcons.close), onPressed: () { - KVStoreService.setRecentSearches( - KVStoreService.recentSearches - .where((s) => s != suggestion) - .toList(), - ); - update(); + controller.clear(); }, ), - onTap: () { - controller.closeView(suggestion); + onAcceptSuggestion: (index) { + controller.text = + KVStoreService.recentSearches[index]; ref - .read( - searchTermStateProvider.notifier) - .state = suggestion; + .read(searchTermStateProvider + .notifier) + .state = + KVStoreService.recentSearches[index]; }, - ); - }, - ); - }), - suggestionsBuilder: (context, controller) { - return []; - }, - viewOnSubmitted: (value) async { - controller.closeView(value); - Timer( - const Duration(milliseconds: 50), - () { - ref - .read(searchTermStateProvider.notifier) - .state = value; - if (value.trim().isEmpty) { - return; - } - KVStoreService.setRecentSearches( - { - value, - ...KVStoreService.recentSearches, - }.toList(), - ); - }, - ); - }, - builder: (context, controller) { - return SearchBar( - autoFocus: queries.none((s) => - s.asData?.value != null && - !s.hasError) && - !kIsMobile, - controller: controller, - leading: const Icon(SpotubeIcons.search), - hintText: "${context.l10n.search}...", - onTap: controller.openView, - onChanged: (_) => controller.openView(), - ); - }, - ), + onChanged: (value) {}, + onSubmitted: onSubmitted, + ), + ); + }), ), ), ], @@ -211,15 +182,15 @@ class SearchPage extends HookConsumerWidget { Icon( SpotubeIcons.web, size: 120, - color: theme.colorScheme.onSurface + color: theme.colorScheme.foreground .withOpacity(0.7), ), const SizedBox(height: 20), Text( context.l10n.search_to_get_results, - style: theme.textTheme.titleLarge?.copyWith( + style: theme.typography.h3.copyWith( fontWeight: FontWeight.w900, - color: theme.colorScheme.onSurface + color: theme.colorScheme.foreground .withOpacity(0.5), ), ), @@ -245,7 +216,7 @@ class SearchPage extends HookConsumerWidget { style: TextStyle( fontSize: 20, fontWeight: FontWeight.w900, - color: theme.colorScheme.onSurface + color: theme.colorScheme.foreground .withOpacity(0.7), ), ),