refactor: expandable filter field on genre and user local tracks page

This commit is contained in:
Kingkor Roy Tirtho 2023-06-18 12:49:38 +06:00
parent dce1b88694
commit 20274b1c65
4 changed files with 170 additions and 87 deletions

View File

@ -15,7 +15,7 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/compact_search.dart';
import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart';
import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
import 'package:spotube/components/shared/track_table/track_tile.dart';
@ -160,7 +160,10 @@ class UserLocalTracks extends HookConsumerWidget {
playlist.containsTracks(trackSnapshot.value ?? []);
final isMounted = useIsMounted();
final searchText = useState<String>("");
final searchController = useTextEditingController();
useValueListenable(searchController);
final searchFocus = useFocusNode();
final isFiltering = useState(false);
useAsyncEffect(
() async {
@ -175,11 +178,6 @@ class UserLocalTracks extends HookConsumerWidget {
[],
);
final searchbar = CompactSearch(
onChanged: (value) => searchText.value = value,
placeholder: context.l10n.search_local_tracks,
);
return Column(
children: [
Padding(
@ -213,7 +211,10 @@ class UserLocalTracks extends HookConsumerWidget {
),
),
const Spacer(),
searchbar,
ExpandableSearchButton(
isFiltering: isFiltering,
searchFocus: searchFocus,
),
const SizedBox(width: 10),
SortTracksDropdown(
value: sortBy.value,
@ -231,6 +232,11 @@ class UserLocalTracks extends HookConsumerWidget {
],
),
),
ExpandableSearchField(
searchController: searchController,
searchFocus: searchFocus,
isFiltering: isFiltering,
),
trackSnapshot.when(
data: (tracks) {
final sortedTracks = useMemoized(() {
@ -238,14 +244,14 @@ class UserLocalTracks extends HookConsumerWidget {
}, [sortBy.value, tracks]);
final filteredTracks = useMemoized(() {
if (searchText.value.isEmpty) {
if (searchController.text.isEmpty) {
return sortedTracks;
}
return sortedTracks
.map((e) => (
weightedRatio(
"${e.name} - ${TypeConversionUtils.artists_X_String<Artist>(e.artists ?? [])}",
searchText.value,
searchController.text,
),
e,
))
@ -257,7 +263,7 @@ class UserLocalTracks extends HookConsumerWidget {
.map((e) => e.$2)
.toList()
.toList();
}, [searchText.value, sortedTracks]);
}, [searchController.text, sortedTracks]);
return Expanded(
child: RefreshIndicator(

View File

@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/context.dart';
class ExpandableSearchField extends StatelessWidget {
final ValueNotifier<bool> isFiltering;
final TextEditingController searchController;
final FocusNode searchFocus;
const ExpandableSearchField({
Key? key,
required this.isFiltering,
required this.searchController,
required this.searchFocus,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: isFiltering.value ? 1 : 0,
child: AnimatedSize(
duration: const Duration(milliseconds: 200),
child: SizedBox(
height: isFiltering.value ? 50 : 0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: CallbackShortcuts(
bindings: {
LogicalKeySet(LogicalKeyboardKey.escape): () {
isFiltering.value = false;
searchController.clear();
searchFocus.unfocus();
}
},
child: TextField(
focusNode: searchFocus,
controller: searchController,
decoration: InputDecoration(
hintText: context.l10n.search_tracks,
isDense: true,
prefixIcon: const Icon(SpotubeIcons.search),
),
),
),
),
),
),
);
}
}
class ExpandableSearchButton extends StatelessWidget {
final ValueNotifier<bool> isFiltering;
final FocusNode searchFocus;
final Widget icon;
final ValueChanged<bool>? onPressed;
const ExpandableSearchButton({
Key? key,
required this.isFiltering,
required this.searchFocus,
this.icon = const Icon(SpotubeIcons.filter),
this.onPressed,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return IconButton(
icon: icon,
style: IconButton.styleFrom(
backgroundColor:
isFiltering.value ? theme.colorScheme.secondaryContainer : null,
foregroundColor: isFiltering.value ? theme.colorScheme.secondary : null,
minimumSize: const Size(25, 25),
),
onPressed: () {
isFiltering.value = !isFiltering.value;
if (isFiltering.value) {
searchFocus.requestFocus();
} else {
searchFocus.unfocus();
}
onPressed?.call(isFiltering.value);
},
);
}
}

View File

@ -10,6 +10,7 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
import 'package:spotube/components/shared/dialogs/confirm_download_dialog.dart';
import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart';
import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
import 'package:spotube/components/shared/fallbacks/not_found.dart';
import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
import 'package:spotube/components/shared/track_table/track_tile.dart';
@ -168,26 +169,12 @@ class TracksTableView extends HookConsumerWidget {
.state = value;
},
),
IconButton(
tooltip: context.l10n.filter_playlists,
icon: const Icon(SpotubeIcons.filter),
style: IconButton.styleFrom(
foregroundColor: isFiltering.value
? theme.colorScheme.secondary
: null,
backgroundColor: isFiltering.value
? theme.colorScheme.secondaryContainer
: null,
minimumSize: const Size(22, 22),
),
onPressed: () {
isFiltering.value = !isFiltering.value;
ExpandableSearchButton(
isFiltering: isFiltering,
searchFocus: searchFocus,
onPressed: (value) {
if (isFiltering.value) {
onFiltering?.call();
searchFocus.requestFocus();
} else {
searchController.clear();
searchFocus.unfocus();
}
},
),
@ -302,36 +289,10 @@ class TracksTableView extends HookConsumerWidget {
],
);
}),
AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: isFiltering.value ? 1 : 0,
child: AnimatedSize(
duration: const Duration(milliseconds: 200),
child: SizedBox(
height: isFiltering.value ? 50 : 0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: CallbackShortcuts(
bindings: {
LogicalKeySet(LogicalKeyboardKey.escape): () {
isFiltering.value = false;
searchController.clear();
searchFocus.unfocus();
}
},
child: TextField(
focusNode: searchFocus,
controller: searchController,
decoration: InputDecoration(
hintText: context.l10n.search_tracks,
isDense: true,
prefixIcon: const Icon(SpotubeIcons.search),
),
),
),
),
),
),
ExpandableSearchField(
isFiltering: isFiltering,
searchController: searchController,
searchFocus: searchFocus,
),
...sortedTracks.mapIndexed((i, track) {
return TrackTile(

View File

@ -1,11 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/genre/category_card.dart';
import 'package:spotube/components/shared/compact_search.dart';
import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
import 'package:spotube/components/shared/shimmers/shimmer_categories.dart';
import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/extensions/context.dart';
@ -18,15 +21,21 @@ class GenrePage extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final scrollController = useScrollController();
final recommendationMarket = ref.watch(
userPreferencesProvider.select((s) => s.recommendationMarket),
);
final categoriesQuery = useQueries.category.list(ref, recommendationMarket);
final isFiltering = useState(false);
final isMounted = useIsMounted();
final searchText = useState("");
final searchController = useTextEditingController();
final searchFocus = useFocusNode();
useValueListenable(searchController);
final categories = useMemoized(
() {
final categories = categoriesQuery.pages
@ -34,12 +43,12 @@ class GenrePage extends HookConsumerWidget {
(page) => page.items ?? const Iterable.empty(),
)
.toList();
if (searchText.value.isEmpty) {
if (searchController.text.isEmpty) {
return categories;
}
return categories
.map((e) => (
weightedRatio(e.name!, searchText.value),
weightedRatio(e.name!, searchController.text),
e,
))
.sorted((a, b) => b.$1.compareTo(a.$1))
@ -47,14 +56,7 @@ class GenrePage extends HookConsumerWidget {
.map((e) => e.$2)
.toList();
},
[categoriesQuery.pages, searchText.value],
);
final searchbar = CompactSearch(
onChanged: (value) {
searchText.value = value;
},
placeholder: context.l10n.genre_categories_filter,
[categoriesQuery.pages, searchController.text],
);
final list = RefreshIndicator(
@ -68,22 +70,32 @@ class GenrePage extends HookConsumerWidget {
}
},
controller: scrollController,
child: ListView.builder(
controller: scrollController,
itemCount: categories.length,
shrinkWrap: true,
itemBuilder: (context, index) {
return AnimatedCrossFade(
crossFadeState: searchText.value.isEmpty &&
index == categories.length - 1 &&
categoriesQuery.hasNextPage
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(milliseconds: 300),
firstChild: const ShimmerCategories(),
secondChild: CategoryCard(categories[index]),
);
},
child: Column(
children: [
ExpandableSearchField(
isFiltering: isFiltering,
searchController: searchController,
searchFocus: searchFocus,
),
Expanded(
child: ListView.builder(
controller: scrollController,
itemCount: categories.length,
itemBuilder: (context, index) {
return AnimatedCrossFade(
crossFadeState: searchController.text.isEmpty &&
index == categories.length - 1 &&
categoriesQuery.hasNextPage
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(milliseconds: 300),
firstChild: const ShimmerCategories(),
secondChild: CategoryCard(categories[index]),
);
},
),
),
],
),
),
);
@ -94,7 +106,20 @@ class GenrePage extends HookConsumerWidget {
Positioned(
top: 0,
right: 10,
child: searchbar,
child: ExpandableSearchButton(
isFiltering: isFiltering,
searchFocus: searchFocus,
icon: const Icon(SpotubeIcons.search),
onPressed: (value) {
if (isFiltering.value) {
scrollController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
),
),
],
);