refactor: library page filter fields and update home sections

This commit is contained in:
Kingkor Roy Tirtho 2024-12-21 18:23:45 +06:00
parent f80ea32de4
commit 2925dd6748
19 changed files with 313 additions and 377 deletions

View File

@ -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(),
);
}
}

View File

@ -1,8 +1,8 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
@ -37,7 +37,6 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData(:textTheme) = Theme.of(context);
final scrollController = useScrollController(); final scrollController = useScrollController();
final height = useBreakpointValue<double>( final height = useBreakpointValue<double>(
xs: 226, xs: 226,
@ -56,7 +55,7 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
DefaultTextStyle( DefaultTextStyle(
style: textTheme.titleMedium!, style: context.theme.typography.h4,
child: title, child: title,
), ),
if (titleTrailing != null) titleTrailing!, if (titleTrailing != null) titleTrailing!,

View File

@ -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<Widget> 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);
}

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart' hide Page;
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/collections/spotube_icons.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
@ -40,9 +40,9 @@ class HomePageFeedSection extends HookConsumerWidget {
onFetchMore: () {}, onFetchMore: () {},
titleTrailing: Directionality( titleTrailing: Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: TextButton.icon( child: Button.link(
label: Text(context.l10n.browse_more), leading: const Icon(SpotubeIcons.angleRight),
icon: const Icon(SpotubeIcons.angleRight), child: Text(context.l10n.browse_more),
onPressed: () => ServiceUtils.pushNamed( onPressed: () => ServiceUtils.pushNamed(
context, context,
HomeFeedSectionPage.name, HomeFeedSectionPage.name,

View File

@ -1,8 +1,9 @@
import 'dart:ui'; import 'dart:ui';
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:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/modules/home/sections/friends/friend_item.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), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
context.l10n.friends, context.l10n.friends,
style: Theme.of(context).textTheme.titleMedium, style: context.theme.typography.h4,
), ),
), ),
), ),

View File

@ -1,8 +1,8 @@
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/collections/spotube_icons.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/models/spotify_friends.dart'; import 'package:spotube/models/spotify_friends.dart';
@ -20,27 +20,15 @@ class FriendItem extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final ThemeData(
textTheme: textTheme,
colorScheme: colorScheme,
) = Theme.of(context);
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
return Container( return Card(
padding: const EdgeInsets.all(8), 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( child: Row(
children: [ children: [
CircleAvatar( Avatar(
backgroundImage: UniversalImage.imageProvider( initials: Avatar.getInitials(friend.user.name),
provider: UniversalImage.imageProvider(
friend.user.imageUrl, friend.user.imageUrl,
), ),
), ),
@ -50,11 +38,10 @@ class FriendItem extends HookConsumerWidget {
children: [ children: [
Text( Text(
friend.user.name, friend.user.name,
style: textTheme.bodyLarge, style: context.theme.typography.bold,
), ),
RichText( RichText(
text: TextSpan( text: TextSpan(
style: textTheme.bodySmall,
children: [ children: [
TextSpan( TextSpan(
text: friend.track.name, text: friend.track.name,

View File

@ -1,10 +1,10 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
@ -22,7 +22,6 @@ class HomeGenresSection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final categoriesQuery = ref.watch(categoriesProvider); final categoriesQuery = ref.watch(categoriesProvider);
@ -46,21 +45,18 @@ class HomeGenresSection extends HookConsumerWidget {
children: [ children: [
Text( Text(
context.l10n.genres, context.l10n.genres,
style: textTheme.headlineSmall, style: context.theme.typography.h4,
), ),
Directionality( Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: TextButton.icon( child: Button.link(
onPressed: () { onPressed: () {
context.pushNamed(GenrePage.name); context.pushNamed(GenrePage.name);
}, },
icon: const Icon(SpotubeIcons.angleRight), leading: const Icon(SpotubeIcons.angleRight),
label: Text( child: Text(
context.l10n.browse_all, context.l10n.browse_all,
style: textTheme.bodyMedium?.copyWith( ).muted(),
color: colorScheme.secondary,
),
),
), ),
), ),
], ],
@ -96,12 +92,12 @@ class HomeGenresSection extends HookConsumerWidget {
final text = gradient.colors final text = gradient.colors
.take(2) .take(2)
.any((c) => c.computeLuminance() > 0.5) .any((c) => c.computeLuminance() > 0.5)
? Colors.grey[900] ? Colors.gray[900]
: Colors.white; : Colors.white;
return ( return (
gradient: LinearGradient( gradient: LinearGradient(
colors: gradient.colors colors: gradient.colors
.map((c) => c.withOpacity(0.8)) .map((c) => c.withAlpha((0.8 * 255).ceil()))
.toList(), .toList(),
), ),
textColor: text textColor: text
@ -110,40 +106,42 @@ class HomeGenresSection extends HookConsumerWidget {
[], [],
); );
return InkWell( return MouseRegion(
onTap: () { cursor: SystemMouseCursors.click,
context.pushNamed( child: GestureDetector(
GenrePlaylistsPage.name, onTap: () {
pathParameters: { context.pushNamed(
"categoryId": category.id!, GenrePlaylistsPage.name,
}, pathParameters: {
extra: category, "categoryId": category.id!,
); },
}, extra: category,
borderRadius: BorderRadius.circular(8), );
child: Ink( },
decoration: BoxDecoration( child: Container(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: UniversalImage.imageProvider(
category.icons!.first.url!,
),
fit: BoxFit.cover,
),
),
child: Ink(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(8),
color: colorScheme.surfaceContainerHighest, image: DecorationImage(
gradient: categoriesQuery.isLoading ? null : gradient, image: UniversalImage.imageProvider(
category.icons!.first.url!,
),
fit: BoxFit.cover,
),
), ),
padding: const EdgeInsets.symmetric(horizontal: 16), child: Container(
child: Align( decoration: BoxDecoration(
alignment: Alignment.centerLeft, borderRadius: BorderRadius.circular(5),
child: Text( color: context.theme.colorScheme.muted,
category.name!, gradient:
style: textTheme.titleMedium categoriesQuery.isLoading ? null : gradient,
?.copyWith(color: textColor), ),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
category.name!,
style: context.theme.typography.large,
),
), ),
), ),
), ),

View File

@ -1,5 +1,5 @@
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart' hide Page;
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';

View File

@ -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:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
@ -52,7 +52,7 @@ class UserAlbums extends HookConsumerWidget {
return SafeArea( return SafeArea(
child: Scaffold( child: Scaffold(
body: RefreshIndicator( child: RefreshTrigger(
onRefresh: () async { onRefresh: () async {
ref.invalidate(favoriteAlbumsProvider); ref.invalidate(favoriteAlbumsProvider);
}, },
@ -62,13 +62,17 @@ class UserAlbums extends HookConsumerWidget {
controller: controller, controller: controller,
slivers: [ slivers: [
SliverAppBar( SliverAppBar(
backgroundColor: Theme.of(context).colorScheme.background,
floating: true, floating: true,
flexibleSpace: Padding( flexibleSpace: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: SearchBar( child: SizedBox(
onChanged: (value) => searchText.value = value, height: 48,
leading: const Icon(SpotubeIcons.filter), child: TextField(
hintText: context.l10n.filter_albums, onChanged: (value) => searchText.value = value,
leading: const Icon(SpotubeIcons.filter),
placeholder: Text(context.l10n.filter_artist),
),
), ),
), ),
), ),

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
@ -54,7 +54,7 @@ class UserArtists extends HookConsumerWidget {
return SafeArea( return SafeArea(
child: Scaffold( child: Scaffold(
body: RefreshIndicator( child: RefreshTrigger(
onRefresh: () async { onRefresh: () async {
ref.invalidate(followedArtistsProvider); ref.invalidate(followedArtistsProvider);
}, },
@ -66,11 +66,15 @@ class UserArtists extends HookConsumerWidget {
controller: controller, controller: controller,
slivers: [ slivers: [
SliverAppBar( SliverAppBar(
backgroundColor: Theme.of(context).colorScheme.background,
floating: true, floating: true,
flexibleSpace: SearchBar( flexibleSpace: SizedBox(
onChanged: (value) => searchText.value = value, height: 48,
leading: const Icon(SpotubeIcons.filter), child: TextField(
hintText: context.l10n.filter_artist, onChanged: (value) => searchText.value = value,
leading: const Icon(SpotubeIcons.filter),
placeholder: Text(context.l10n.filter_artist),
),
), ),
), ),
const SliverGap(10), const SliverGap(10),

View File

@ -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:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.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:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
@ -79,7 +80,7 @@ class UserPlaylists extends HookConsumerWidget {
return const AnonymousFallback(); return const AnonymousFallback();
} }
return RefreshIndicator( return RefreshTrigger(
onRefresh: () async { onRefresh: () async {
ref.invalidate(favoritePlaylistsProvider); ref.invalidate(favoritePlaylistsProvider);
}, },
@ -91,11 +92,13 @@ class UserPlaylists extends HookConsumerWidget {
slivers: [ slivers: [
SliverAppBar( SliverAppBar(
floating: true, floating: true,
flexibleSpace: Padding( backgroundColor: context.theme.colorScheme.background,
flexibleSpace: Container(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
child: SearchBar( height: 48,
child: TextField(
onChanged: (value) => searchText.value = value, onChanged: (value) => searchText.value = value,
hintText: context.l10n.filter_playlists, placeholder: Text(context.l10n.filter_playlists),
leading: const Icon(SpotubeIcons.filter), leading: const Icon(SpotubeIcons.filter),
), ),
), ),
@ -107,12 +110,14 @@ class UserPlaylists extends HookConsumerWidget {
const Gap(10), const Gap(10),
const PlaylistCreateDialogButton(), const PlaylistCreateDialogButton(),
const Gap(10), const Gap(10),
ElevatedButton.icon( Button.primary(
icon: const Icon(SpotubeIcons.magic), leading: const Icon(SpotubeIcons.magic),
label: Text(context.l10n.generate_playlist), child: Text(context.l10n.generate_playlist),
onPressed: () { onPressed: () {
ServiceUtils.pushNamed( ServiceUtils.pushNamed(
context, PlaylistGeneratorPage.name); context,
PlaylistGeneratorPage.name,
);
}, },
), ),
const Gap(10), const Gap(10),

View File

@ -104,7 +104,7 @@ class Sidebar extends HookConsumerWidget {
index: selectedIndex, index: selectedIndex,
onSelected: (index) { onSelected: (index) {
final tile = sidebarTileList[index]; final tile = sidebarTileList[index];
ServiceUtils.pushNamed(context, tile.name); context.goNamed(tile.name);
}, },
children: navigationButtons, children: navigationButtons,
) )
@ -113,13 +113,13 @@ class Sidebar extends HookConsumerWidget {
index: selectedIndex, index: selectedIndex,
onSelected: (index) { onSelected: (index) {
final tile = sidebarTileList[index]; final tile = sidebarTileList[index];
ServiceUtils.pushNamed(context, tile.name); context.goNamed(tile.name);
}, },
children: navigationButtons, children: navigationButtons,
), ),
), ),
const SidebarFooter(), const SidebarFooter(),
const Gap(130) if (mediaQuery.lgAndUp) const Gap(130) else const Gap(65),
], ],
), ),
const VerticalDivider(), const VerticalDivider(),

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; 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:spotube/components/themed_button_tab_bar.dart';
import 'package:spotube/modules/stats/top/albums.dart'; import 'package:spotube/modules/stats/top/albums.dart';
import 'package:spotube/modules/stats/top/artists.dart'; import 'package:spotube/modules/stats/top/artists.dart';
import 'package:spotube/modules/stats/top/tracks.dart'; import 'package:spotube/modules/stats/top/tracks.dart';
@ -23,7 +22,7 @@ class StatsPageTopSection extends HookConsumerWidget {
slivers: [ slivers: [
SliverAppBar( SliverAppBar(
floating: true, floating: true,
flexibleSpace: ThemedButtonsTabBar( flexibleSpace: TabBar(
controller: tabController, controller: tabController,
tabs: [ tabs: [
Tab( Tab(

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
@ -34,18 +35,22 @@ class HomePage extends HookConsumerWidget {
return SafeArea( return SafeArea(
bottom: false, bottom: false,
child: Scaffold( child: Scaffold(
appBar: kIsMobile || kIsMacOS ? null : const TitleBar(), headers: [
body: CustomScrollView( if (kIsWindows || kIsLinux) const TitleBar(),
],
child: CustomScrollView(
controller: controller, controller: controller,
slivers: [ slivers: [
if (mediaQuery.smAndDown || layoutMode == LayoutMode.compact) if (mediaQuery.smAndDown || layoutMode == LayoutMode.compact)
SliverAppBar( SliverAppBar(
floating: true, floating: true,
title: Assets.spotubeLogoPng.image(height: 45), title: Assets.spotubeLogoPng.image(height: 45),
backgroundColor: context.theme.colorScheme.background,
foregroundColor: context.theme.colorScheme.foreground,
actions: [ actions: [
const ConnectDeviceButton(), const ConnectDeviceButton(),
const Gap(10), const Gap(10),
IconButton( IconButton.ghost(
icon: const Icon(SpotubeIcons.settings, size: 20), icon: const Icon(SpotubeIcons.settings, size: 20),
onPressed: () { onPressed: () {
ServiceUtils.pushNamed(context, SettingsPage.name); ServiceUtils.pushNamed(context, SettingsPage.name);

View File

@ -1,14 +1,12 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/collections/spotube_icons.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/components/image/universal_image.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/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
@ -39,6 +37,7 @@ class LyricsPage extends HookConsumerWidget {
final palette = usePaletteColor(albumArt, ref); final palette = usePaletteColor(albumArt, ref);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final route = ModalRoute.of(context); final route = ModalRoute.of(context);
final selectedIndex = useState(0);
final resetStatusBar = useCustomStatusBarColor( final resetStatusBar = useCustomStatusBarColor(
palette.color, palette.color,
@ -46,134 +45,134 @@ class LyricsPage extends HookConsumerWidget {
noSetBGColor: true, noSetBGColor: true,
); );
PreferredSizeWidget tabbar = ThemedButtonsTabBar( Widget tabbar = Padding(
tabs: [ padding: const EdgeInsets.all(10),
Tab(text: " ${context.l10n.synced} "), child: Opacity(
Tab(text: " ${context.l10n.plain} "), opacity: 0.8,
], child: Tabs(
index: selectedIndex.value,
onChanged: (index) => selectedIndex.value = index,
tabs: [
Text(context.l10n.synced),
Text(context.l10n.plain),
],
),
),
); );
tabbar = PreferredSize( tabbar = Row(
preferredSize: tabbar.preferredSize, children: [
child: Row( tabbar,
children: [ const Spacer(),
tabbar, Consumer(
const Spacer(), builder: (context, ref, child) {
Consumer( final playback = ref.watch(audioPlayerProvider);
builder: (context, ref, child) { final lyric = ref.watch(syncedLyricsProvider(playback.activeTrack));
final playback = ref.watch(audioPlayerProvider); final providerName = lyric.asData?.value.provider;
final lyric =
ref.watch(syncedLyricsProvider(playback.activeTrack));
final providerName = lyric.asData?.value.provider;
if (providerName == null) { if (providerName == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return Align( return Align(
alignment: Alignment.bottomRight, alignment: Alignment.bottomRight,
child: Text(context.l10n.powered_by_provider(providerName)), child: Text(context.l10n.powered_by_provider(providerName)),
); );
}, },
), ),
const Gap(5), const Gap(5),
], ],
),
); );
if (isModal) { if (isModal) {
return PopScope( return PopScope(
canPop: true, canPop: true,
onPopInvokedWithResult: (_, __) => resetStatusBar(), onPopInvokedWithResult: (_, __) => resetStatusBar(),
child: DefaultTabController( child: SafeArea(
length: 2, child: BackdropFilter(
child: SafeArea( filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: BackdropFilter( child: Container(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), clipBehavior: Clip.hardEdge,
child: Container( decoration: BoxDecoration(
clipBehavior: Clip.hardEdge, color: Theme.of(context).colorScheme.background.withOpacity(.4),
decoration: BoxDecoration( borderRadius: const BorderRadius.only(
color: Theme.of(context).colorScheme.surface.withOpacity(.4), topLeft: Radius.circular(10),
borderRadius: const BorderRadius.only( topRight: Radius.circular(10),
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
),
), ),
child: Column( ),
children: [ child: Column(
const SizedBox(height: 5), children: [
Container( const SizedBox(height: 5),
height: 7, Container(
width: 150, height: 7,
decoration: BoxDecoration( width: 150,
color: palette.titleTextColor, decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10), color: palette.titleTextColor,
), borderRadius: BorderRadius.circular(10),
), ),
AppBar( ),
leadingWidth: double.infinity, AppBar(
leading: tabbar, leading: [tabbar],
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
automaticallyImplyLeading: false, trailing: [
actions: [ IconButton.ghost(
IconButton( icon: const Icon(SpotubeIcons.minimize),
icon: const Icon(SpotubeIcons.minimize), onPressed: () => Navigator.of(context).pop(),
onPressed: () => Navigator.of(context).pop(), ),
), const SizedBox(width: 5),
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( return SafeArea(
length: 2, bottom: mediaQuery.mdAndUp,
child: SafeArea( child: Scaffold(
bottom: mediaQuery.mdAndUp, floatingHeader: true,
child: Scaffold( headers: [
extendBodyBehindAppBar: true, !kIsMacOS
appBar: !kIsMacOS
? TitleBar( ? TitleBar(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
title: tabbar, title: tabbar,
) )
: tabbar, : tabbar
body: Container( ],
clipBehavior: Clip.hardEdge, child: Container(
decoration: BoxDecoration( clipBehavior: Clip.hardEdge,
image: DecorationImage( decoration: BoxDecoration(
image: UniversalImage.imageProvider(albumArt), image: DecorationImage(
fit: BoxFit.cover, image: UniversalImage.imageProvider(albumArt),
), fit: BoxFit.cover,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(10),
),
), ),
margin: const EdgeInsets.only(bottom: 10), borderRadius: const BorderRadius.only(
child: BackdropFilter( bottomLeft: Radius.circular(10),
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), ),
child: ColoredBox( ),
color: palette.color.withOpacity(.7), margin: const EdgeInsets.only(bottom: 10),
child: SafeArea( child: BackdropFilter(
child: TabBarView( filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
children: [ child: ColoredBox(
SyncedLyrics(palette: palette, isModal: isModal), color: palette.color.withOpacity(.7),
PlainLyrics(palette: palette, isModal: isModal), child: SafeArea(
], child: IndexedStack(
), index: selectedIndex.value,
children: [
SyncedLyrics(palette: palette, isModal: isModal),
PlainLyrics(palette: palette, isModal: isModal),
],
), ),
), ),
), ),

View File

@ -1,21 +1,17 @@
import 'dart:async'; import 'package:flutter/services.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
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:spotify/spotify.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/collections/spotube_icons.dart';
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.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/albums.dart';
import 'package:spotube/pages/search/sections/artists.dart'; import 'package:spotube/pages/search/sections/artists.dart';
import 'package:spotube/pages/search/sections/playlists.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/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
class SearchPage extends HookConsumerWidget { class SearchPage extends HookConsumerWidget {
@ -36,6 +31,7 @@ class SearchPage extends HookConsumerWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final searchTerm = ref.watch(searchTermStateProvider); final searchTerm = ref.watch(searchTermStateProvider);
final controller = useSearchController(); final controller = useSearchController();
final focusNode = useFocusNode();
final auth = ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
final mediaQuery = MediaQuery.of(context); 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( return SafeArea(
bottom: false, bottom: false,
child: Scaffold( child: Scaffold(
appBar: kIsDesktop && !kIsMacOS headers: [
? const TitleBar(automaticallyImplyLeading: true) if (kIsWindows || kIsLinux)
: null, const TitleBar(automaticallyImplyLeading: true)
body: auth.asData?.value == null ],
child: auth.asData?.value == null
? const AnonymousFallback() ? const AnonymousFallback()
: Column( : Column(
children: [ children: [
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
if ((kIsMobile || kIsMacOS) && context.canPop())
const BackButton()
else
const Gap(20),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.all(20),
right: 20, child: ListenableBuilder(
top: 20, listenable: controller,
bottom: 20, builder: (context, _) {
), final suggestions = controller.text.isEmpty
child: SearchAnchor( ? KVStoreService.recentSearches
searchController: controller, : KVStoreService.recentSearches
viewBuilder: (_) => HookBuilder(builder: (context) { .where(
final searchController = (s) =>
useListenable(controller); weightedRatio(
final update = useForceUpdate(); s.toLowerCase(),
final suggestions = searchController.text.isEmpty controller.text.toLowerCase(),
? KVStoreService.recentSearches ) >
: KVStoreService.recentSearches 50,
.where( )
(s) => .toList();
weightedRatio(
s.toLowerCase(),
searchController.text
.toLowerCase(),
) >
50,
)
.toList();
return ListView.builder( return KeyboardListener(
itemCount: suggestions.length, focusNode: focusNode,
itemBuilder: (context, index) { autofocus: true,
final suggestion = suggestions[index]; onKeyEvent: (value) {
final isEnter = value.logicalKey ==
LogicalKeyboardKey.enter;
return ListTile( if (isEnter) {
leading: const Icon(SpotubeIcons.history), onSubmitted(controller.text);
title: Text(suggestion), focusNode.unfocus();
trailing: IconButton( }
icon: const Icon(SpotubeIcons.trash), },
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: () { onPressed: () {
KVStoreService.setRecentSearches( controller.clear();
KVStoreService.recentSearches
.where((s) => s != suggestion)
.toList(),
);
update();
}, },
), ),
onTap: () { onAcceptSuggestion: (index) {
controller.closeView(suggestion); controller.text =
KVStoreService.recentSearches[index];
ref ref
.read( .read(searchTermStateProvider
searchTermStateProvider.notifier) .notifier)
.state = suggestion; .state =
KVStoreService.recentSearches[index];
}, },
); onChanged: (value) {},
}, onSubmitted: onSubmitted,
); ),
}), );
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(),
);
},
),
), ),
), ),
], ],
@ -211,15 +182,15 @@ class SearchPage extends HookConsumerWidget {
Icon( Icon(
SpotubeIcons.web, SpotubeIcons.web,
size: 120, size: 120,
color: theme.colorScheme.onSurface color: theme.colorScheme.foreground
.withOpacity(0.7), .withOpacity(0.7),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Text( Text(
context.l10n.search_to_get_results, context.l10n.search_to_get_results,
style: theme.textTheme.titleLarge?.copyWith( style: theme.typography.h3.copyWith(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: theme.colorScheme.onSurface color: theme.colorScheme.foreground
.withOpacity(0.5), .withOpacity(0.5),
), ),
), ),
@ -245,7 +216,7 @@ class SearchPage extends HookConsumerWidget {
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: theme.colorScheme.onSurface color: theme.colorScheme.foreground
.withOpacity(0.7), .withOpacity(0.7),
), ),
), ),