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 '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<T> extends HookWidget {
@override
Widget build(BuildContext context) {
final ThemeData(:textTheme) = Theme.of(context);
final scrollController = useScrollController();
final height = useBreakpointValue<double>(
xs: 226,
@ -56,7 +55,7 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
DefaultTextStyle(
style: textTheme.titleMedium!,
style: context.theme.typography.h4,
child: title,
),
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: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';

View File

@ -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,

View File

@ -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,
),
),
),

View File

@ -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,

View File

@ -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,
),
),
),
),

View File

@ -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';

View File

@ -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';

View File

@ -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';

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: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),
),
),
),
),

View File

@ -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),

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: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),

View File

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

View File

@ -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(

View File

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

View File

@ -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),
],
),
),
),

View File

@ -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),
),
),