mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
refactor: library page filter fields and update home sections
This commit is contained in:
parent
f80ea32de4
commit
2925dd6748
14
lib/components/button/back_button.dart
Normal file
14
lib/components/button/back_button.dart
Normal 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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!,
|
||||||
|
@ -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);
|
|
||||||
}
|
|
@ -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';
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
|
@ -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,7 +106,9 @@ class HomeGenresSection extends HookConsumerWidget {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
return InkWell(
|
return MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushNamed(
|
context.pushNamed(
|
||||||
GenrePlaylistsPage.name,
|
GenrePlaylistsPage.name,
|
||||||
@ -120,8 +118,7 @@ class HomeGenresSection extends HookConsumerWidget {
|
|||||||
extra: category,
|
extra: category,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(8),
|
child: Container(
|
||||||
child: Ink(
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
@ -131,19 +128,20 @@ class HomeGenresSection extends HookConsumerWidget {
|
|||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Ink(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
color: colorScheme.surfaceContainerHighest,
|
color: context.theme.colorScheme.muted,
|
||||||
gradient: categoriesQuery.isLoading ? null : gradient,
|
gradient:
|
||||||
|
categoriesQuery.isLoading ? null : gradient,
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text(
|
child: Text(
|
||||||
category.name!,
|
category.name!,
|
||||||
style: textTheme.titleMedium
|
style: context.theme.typography.large,
|
||||||
?.copyWith(color: textColor),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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(
|
||||||
|
height: 48,
|
||||||
|
child: TextField(
|
||||||
onChanged: (value) => searchText.value = value,
|
onChanged: (value) => searchText.value = value,
|
||||||
leading: const Icon(SpotubeIcons.filter),
|
leading: const Icon(SpotubeIcons.filter),
|
||||||
hintText: context.l10n.filter_albums,
|
placeholder: Text(context.l10n.filter_artist),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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(
|
||||||
|
height: 48,
|
||||||
|
child: TextField(
|
||||||
onChanged: (value) => searchText.value = value,
|
onChanged: (value) => searchText.value = value,
|
||||||
leading: const Icon(SpotubeIcons.filter),
|
leading: const Icon(SpotubeIcons.filter),
|
||||||
hintText: context.l10n.filter_artist,
|
placeholder: Text(context.l10n.filter_artist),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SliverGap(10),
|
const SliverGap(10),
|
||||||
|
@ -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),
|
||||||
|
@ -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(),
|
||||||
|
@ -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(
|
||||||
|
@ -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);
|
||||||
|
@ -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,24 +45,29 @@ class LyricsPage extends HookConsumerWidget {
|
|||||||
noSetBGColor: true,
|
noSetBGColor: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
PreferredSizeWidget tabbar = ThemedButtonsTabBar(
|
Widget tabbar = Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.8,
|
||||||
|
child: Tabs(
|
||||||
|
index: selectedIndex.value,
|
||||||
|
onChanged: (index) => selectedIndex.value = index,
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(text: " ${context.l10n.synced} "),
|
Text(context.l10n.synced),
|
||||||
Tab(text: " ${context.l10n.plain} "),
|
Text(context.l10n.plain),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
tabbar = PreferredSize(
|
tabbar = Row(
|
||||||
preferredSize: tabbar.preferredSize,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
children: [
|
||||||
tabbar,
|
tabbar,
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final playback = ref.watch(audioPlayerProvider);
|
final playback = ref.watch(audioPlayerProvider);
|
||||||
final lyric =
|
final lyric = ref.watch(syncedLyricsProvider(playback.activeTrack));
|
||||||
ref.watch(syncedLyricsProvider(playback.activeTrack));
|
|
||||||
final providerName = lyric.asData?.value.provider;
|
final providerName = lyric.asData?.value.provider;
|
||||||
|
|
||||||
if (providerName == null) {
|
if (providerName == null) {
|
||||||
@ -78,22 +82,19 @@ class LyricsPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const Gap(5),
|
const Gap(5),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isModal) {
|
if (isModal) {
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: true,
|
canPop: true,
|
||||||
onPopInvokedWithResult: (_, __) => resetStatusBar(),
|
onPopInvokedWithResult: (_, __) => resetStatusBar(),
|
||||||
child: DefaultTabController(
|
|
||||||
length: 2,
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: BackdropFilter(
|
child: BackdropFilter(
|
||||||
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
|
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
|
||||||
child: Container(
|
child: Container(
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.surface.withOpacity(.4),
|
color: Theme.of(context).colorScheme.background.withOpacity(.4),
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topLeft: Radius.circular(10),
|
topLeft: Radius.circular(10),
|
||||||
topRight: Radius.circular(10),
|
topRight: Radius.circular(10),
|
||||||
@ -111,12 +112,10 @@ class LyricsPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
AppBar(
|
AppBar(
|
||||||
leadingWidth: double.infinity,
|
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(),
|
||||||
),
|
),
|
||||||
@ -124,7 +123,8 @@ class LyricsPage extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TabBarView(
|
child: IndexedStack(
|
||||||
|
index: selectedIndex.value,
|
||||||
children: [
|
children: [
|
||||||
SyncedLyrics(palette: palette, isModal: isModal),
|
SyncedLyrics(palette: palette, isModal: isModal),
|
||||||
PlainLyrics(palette: palette, isModal: isModal),
|
PlainLyrics(palette: palette, isModal: isModal),
|
||||||
@ -136,22 +136,21 @@ class LyricsPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return DefaultTabController(
|
return SafeArea(
|
||||||
length: 2,
|
|
||||||
child: SafeArea(
|
|
||||||
bottom: mediaQuery.mdAndUp,
|
bottom: mediaQuery.mdAndUp,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
extendBodyBehindAppBar: true,
|
floatingHeader: true,
|
||||||
appBar: !kIsMacOS
|
headers: [
|
||||||
|
!kIsMacOS
|
||||||
? TitleBar(
|
? TitleBar(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
title: tabbar,
|
title: tabbar,
|
||||||
)
|
)
|
||||||
: tabbar,
|
: tabbar
|
||||||
body: Container(
|
],
|
||||||
|
child: Container(
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
@ -168,7 +167,8 @@ class LyricsPage extends HookConsumerWidget {
|
|||||||
child: ColoredBox(
|
child: ColoredBox(
|
||||||
color: palette.color.withOpacity(.7),
|
color: palette.color.withOpacity(.7),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: TabBarView(
|
child: IndexedStack(
|
||||||
|
index: selectedIndex.value,
|
||||||
children: [
|
children: [
|
||||||
SyncedLyrics(palette: palette, isModal: isModal),
|
SyncedLyrics(palette: palette, isModal: isModal),
|
||||||
PlainLyrics(palette: palette, isModal: isModal),
|
PlainLyrics(palette: palette, isModal: isModal),
|
||||||
@ -179,7 +179,6 @@ class LyricsPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,91 +80,8 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return SafeArea(
|
void onSubmitted(String value) {
|
||||||
bottom: false,
|
ref.read(searchTermStateProvider.notifier).state = value;
|
||||||
child: Scaffold(
|
|
||||||
appBar: kIsDesktop && !kIsMacOS
|
|
||||||
? const TitleBar(automaticallyImplyLeading: true)
|
|
||||||
: null,
|
|
||||||
body: 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();
|
|
||||||
|
|
||||||
return ListView.builder(
|
|
||||||
itemCount: suggestions.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final suggestion = suggestions[index];
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
leading: const Icon(SpotubeIcons.history),
|
|
||||||
title: Text(suggestion),
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: const Icon(SpotubeIcons.trash),
|
|
||||||
onPressed: () {
|
|
||||||
KVStoreService.setRecentSearches(
|
|
||||||
KVStoreService.recentSearches
|
|
||||||
.where((s) => s != suggestion)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
update();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
controller.closeView(suggestion);
|
|
||||||
ref
|
|
||||||
.read(
|
|
||||||
searchTermStateProvider.notifier)
|
|
||||||
.state = suggestion;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
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) {
|
if (value.trim().isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -178,23 +91,81 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
...KVStoreService.recentSearches,
|
...KVStoreService.recentSearches,
|
||||||
}.toList(),
|
}.toList(),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: Scaffold(
|
||||||
|
headers: [
|
||||||
|
if (kIsWindows || kIsLinux)
|
||||||
|
const TitleBar(automaticallyImplyLeading: true)
|
||||||
|
],
|
||||||
|
child: auth.asData?.value == null
|
||||||
|
? const AnonymousFallback()
|
||||||
|
: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
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 KeyboardListener(
|
||||||
|
focusNode: focusNode,
|
||||||
|
autofocus: true,
|
||||||
|
onKeyEvent: (value) {
|
||||||
|
final isEnter = value.logicalKey ==
|
||||||
|
LogicalKeyboardKey.enter;
|
||||||
|
|
||||||
|
if (isEnter) {
|
||||||
|
onSubmitted(controller.text);
|
||||||
|
focusNode.unfocus();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
child: AutoComplete(
|
||||||
},
|
autofocus: true,
|
||||||
builder: (context, controller) {
|
|
||||||
return SearchBar(
|
|
||||||
autoFocus: queries.none((s) =>
|
|
||||||
s.asData?.value != null &&
|
|
||||||
!s.hasError) &&
|
|
||||||
!kIsMobile,
|
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
suggestions: suggestions,
|
||||||
leading: const Icon(SpotubeIcons.search),
|
leading: const Icon(SpotubeIcons.search),
|
||||||
hintText: "${context.l10n.search}...",
|
textInputAction: TextInputAction.search,
|
||||||
onTap: controller.openView,
|
placeholder: Text(context.l10n.search),
|
||||||
onChanged: (_) => controller.openView(),
|
trailing: IconButton.ghost(
|
||||||
);
|
size: ButtonSize.small,
|
||||||
|
icon: const Icon(SpotubeIcons.close),
|
||||||
|
onPressed: () {
|
||||||
|
controller.clear();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
onAcceptSuggestion: (index) {
|
||||||
|
controller.text =
|
||||||
|
KVStoreService.recentSearches[index];
|
||||||
|
ref
|
||||||
|
.read(searchTermStateProvider
|
||||||
|
.notifier)
|
||||||
|
.state =
|
||||||
|
KVStoreService.recentSearches[index];
|
||||||
|
},
|
||||||
|
onChanged: (value) {},
|
||||||
|
onSubmitted: onSubmitted,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Loading…
Reference in New Issue
Block a user