mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
refactor: expandable filter field on genre and user local tracks page
This commit is contained in:
parent
dce1b88694
commit
20274b1c65
@ -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(
|
||||
|
@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user