settings live search
This commit is contained in:
parent
2174497567
commit
2c917becea
11 changed files with 258 additions and 235 deletions
|
@ -25,6 +25,7 @@ import 'package:aves/widgets/common/basic/menu.dart';
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/favourite_toggler.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
|
||||
import 'package:aves/widgets/common/search/route.dart';
|
||||
import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
|
||||
import 'package:aves/widgets/search/search_delegate.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -543,6 +544,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
context,
|
||||
SearchPageRoute(
|
||||
delegate: CollectionSearchDelegate(
|
||||
searchFieldLabel: context.l10n.searchCollectionFieldHint,
|
||||
source: collection.source,
|
||||
parentCollection: collection,
|
||||
),
|
||||
|
|
|
@ -27,6 +27,7 @@ import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
|||
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/size_aware.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/search/route.dart';
|
||||
import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
|
@ -566,6 +567,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
context,
|
||||
SearchPageRoute(
|
||||
delegate: CollectionSearchDelegate(
|
||||
searchFieldLabel: context.l10n.searchCollectionFieldHint,
|
||||
source: collection.source,
|
||||
parentCollection: collection,
|
||||
),
|
||||
|
|
105
lib/widgets/common/search/delegate.dart
Normal file
105
lib/widgets/common/search/delegate.dart
Normal file
|
@ -0,0 +1,105 @@
|
|||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/search/route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
abstract class AvesSearchDelegate extends SearchDelegate {
|
||||
final String routeName;
|
||||
final bool canPop;
|
||||
|
||||
AvesSearchDelegate({
|
||||
required this.routeName,
|
||||
this.canPop = true,
|
||||
String? initialQuery,
|
||||
required super.searchFieldLabel,
|
||||
}) {
|
||||
query = initialQuery ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildLeading(BuildContext context) {
|
||||
// use a property instead of checking `Navigator.canPop(context)`
|
||||
// because the navigator state changes as soon as we press back
|
||||
// so the leading may mistakenly switch to the close button
|
||||
return canPop
|
||||
? IconButton(
|
||||
icon: AnimatedIcon(
|
||||
icon: AnimatedIcons.menu_arrow,
|
||||
progress: transitionAnimation,
|
||||
),
|
||||
onPressed: () => goBack(context),
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
)
|
||||
: const CloseButton(
|
||||
onPressed: SystemNavigator.pop,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget> buildActions(BuildContext context) {
|
||||
return [
|
||||
if (query.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(AIcons.clear),
|
||||
onPressed: () {
|
||||
query = '';
|
||||
showSuggestions(context);
|
||||
},
|
||||
tooltip: context.l10n.clearTooltip,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void goBack(BuildContext context) {
|
||||
clean();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
void clean() {
|
||||
currentBody = null;
|
||||
focusNode?.unfocus();
|
||||
}
|
||||
|
||||
// adapted from Flutter `SearchDelegate` in `/material/search.dart`
|
||||
|
||||
@override
|
||||
void showResults(BuildContext context) {
|
||||
focusNode?.unfocus();
|
||||
currentBody = SearchBody.results;
|
||||
}
|
||||
|
||||
@override
|
||||
void showSuggestions(BuildContext context) {
|
||||
assert(focusNode != null, '_focusNode must be set by route before showSuggestions is called.');
|
||||
focusNode!.requestFocus();
|
||||
currentBody = SearchBody.suggestions;
|
||||
}
|
||||
|
||||
@override
|
||||
Animation<double> get transitionAnimation => proxyAnimation;
|
||||
|
||||
FocusNode? focusNode;
|
||||
|
||||
final TextEditingController queryTextController = TextEditingController();
|
||||
|
||||
final ProxyAnimation proxyAnimation = ProxyAnimation(kAlwaysDismissedAnimation);
|
||||
|
||||
@override
|
||||
String get query => queryTextController.text;
|
||||
|
||||
@override
|
||||
set query(String value) {
|
||||
queryTextController.text = value;
|
||||
}
|
||||
|
||||
final ValueNotifier<SearchBody?> currentBodyNotifier = ValueNotifier(null);
|
||||
|
||||
SearchBody? get currentBody => currentBodyNotifier.value;
|
||||
|
||||
set currentBody(SearchBody? value) {
|
||||
currentBodyNotifier.value = value;
|
||||
}
|
||||
|
||||
SearchPageRoute? route;
|
||||
}
|
|
@ -2,16 +2,14 @@ import 'dart:ui';
|
|||
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/utils/debouncer.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
|
||||
import 'package:aves/widgets/search/search_delegate.dart';
|
||||
import 'package:aves/widgets/common/search/delegate.dart';
|
||||
import 'package:aves/widgets/common/search/route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
class SearchPage extends StatefulWidget {
|
||||
static const routeName = '/search';
|
||||
|
||||
final CollectionSearchDelegate delegate;
|
||||
final AvesSearchDelegate delegate;
|
||||
final Animation<double> animation;
|
||||
|
||||
const SearchPage({
|
||||
|
@ -31,23 +29,40 @@ class _SearchPageState extends State<SearchPage> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.delegate.queryTextController.addListener(_onQueryChanged);
|
||||
_registerWidget(widget);
|
||||
widget.animation.addStatusListener(_onAnimationStatusChanged);
|
||||
widget.delegate.currentBodyNotifier.addListener(_onSearchBodyChanged);
|
||||
_focusNode.addListener(_onFocusChanged);
|
||||
widget.delegate.focusNode = _focusNode;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant SearchPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.delegate != oldWidget.delegate) {
|
||||
_unregisterWidget(oldWidget);
|
||||
_registerWidget(widget);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.delegate.queryTextController.removeListener(_onQueryChanged);
|
||||
_unregisterWidget(widget);
|
||||
widget.animation.removeStatusListener(_onAnimationStatusChanged);
|
||||
widget.delegate.currentBodyNotifier.removeListener(_onSearchBodyChanged);
|
||||
widget.delegate.focusNode = null;
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _registerWidget(SearchPage widget) {
|
||||
widget.delegate.queryTextController.addListener(_onQueryChanged);
|
||||
widget.delegate.currentBodyNotifier.addListener(_onSearchBodyChanged);
|
||||
widget.delegate.focusNode = _focusNode;
|
||||
}
|
||||
|
||||
void _unregisterWidget(SearchPage widget) {
|
||||
widget.delegate.queryTextController.removeListener(_onQueryChanged);
|
||||
widget.delegate.currentBodyNotifier.removeListener(_onSearchBodyChanged);
|
||||
widget.delegate.focusNode = null;
|
||||
}
|
||||
|
||||
void _onAnimationStatusChanged(AnimationStatus status) {
|
||||
if (status != AnimationStatus.completed) {
|
||||
return;
|
||||
|
@ -59,19 +74,6 @@ class _SearchPageState extends State<SearchPage> {
|
|||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant SearchPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.delegate != oldWidget.delegate) {
|
||||
oldWidget.delegate.queryTextController.removeListener(_onQueryChanged);
|
||||
widget.delegate.queryTextController.addListener(_onQueryChanged);
|
||||
oldWidget.delegate.currentBodyNotifier.removeListener(_onSearchBodyChanged);
|
||||
widget.delegate.currentBodyNotifier.addListener(_onSearchBodyChanged);
|
||||
oldWidget.delegate.focusNode = null;
|
||||
widget.delegate.focusNode = _focusNode;
|
||||
}
|
||||
}
|
||||
|
||||
void _onFocusChanged() {
|
||||
if (_focusNode.hasFocus && widget.delegate.currentBody != SearchBody.suggestions) {
|
||||
widget.delegate.showSuggestions(context);
|
||||
|
@ -130,7 +132,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||
focusNode: _focusNode,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: context.l10n.searchCollectionFieldHint,
|
||||
hintText: widget.delegate.searchFieldLabel,
|
||||
hintStyle: theme.inputDecorationTheme.hintStyle,
|
||||
),
|
||||
textInputAction: TextInputAction.search,
|
75
lib/widgets/common/search/route.dart
Normal file
75
lib/widgets/common/search/route.dart
Normal file
|
@ -0,0 +1,75 @@
|
|||
import 'package:aves/widgets/common/search/delegate.dart';
|
||||
import 'package:aves/widgets/common/search/page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// adapted from Flutter `_SearchBody` in `/material/search.dart`
|
||||
enum SearchBody { suggestions, results }
|
||||
|
||||
// adapted from Flutter `_SearchPageRoute` in `/material/search.dart`
|
||||
class SearchPageRoute<T> extends PageRoute<T> {
|
||||
SearchPageRoute({
|
||||
required this.delegate,
|
||||
}) : super(settings: RouteSettings(name: delegate.routeName)) {
|
||||
assert(
|
||||
delegate.route == null,
|
||||
'The ${delegate.runtimeType} instance is currently used by another active '
|
||||
'search. Please close that search by calling close() on the SearchDelegate '
|
||||
'before openening another search with the same delegate instance.',
|
||||
);
|
||||
delegate.route = this;
|
||||
}
|
||||
|
||||
final AvesSearchDelegate delegate;
|
||||
|
||||
@override
|
||||
Color? get barrierColor => null;
|
||||
|
||||
@override
|
||||
String? get barrierLabel => null;
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 300);
|
||||
|
||||
@override
|
||||
bool get maintainState => false;
|
||||
|
||||
@override
|
||||
Widget buildTransitions(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Animation<double> createAnimation() {
|
||||
final animation = super.createAnimation();
|
||||
delegate.proxyAnimation.parent = animation;
|
||||
return animation;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPage(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SearchPage(
|
||||
delegate: delegate,
|
||||
animation: animation,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didComplete(T? result) {
|
||||
super.didComplete(result);
|
||||
assert(delegate.route == this);
|
||||
delegate.route = null;
|
||||
delegate.currentBody = null;
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
|||
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/size_aware.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/search/route.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/filter_editors/cover_selection_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
|
||||
|
@ -246,6 +247,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
|
|||
context,
|
||||
SearchPageRoute(
|
||||
delegate: CollectionSearchDelegate(
|
||||
searchFieldLabel: context.l10n.searchCollectionFieldHint,
|
||||
source: context.read<CollectionSource>(),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:aves/widgets/common/app_bar_title.dart';
|
|||
import 'package:aves/widgets/common/basic/menu.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
|
||||
import 'package:aves/widgets/common/search/route.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
|
||||
import 'package:aves/widgets/search/search_delegate.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -222,6 +223,7 @@ class _FilterGridAppBarState<T extends CollectionFilter> extends State<FilterGri
|
|||
context,
|
||||
SearchPageRoute(
|
||||
delegate: CollectionSearchDelegate(
|
||||
searchFieldLabel: context.l10n.searchCollectionFieldHint,
|
||||
source: source,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -17,9 +17,10 @@ import 'package:aves/services/viewer_service.dart';
|
|||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/behaviour/routes.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/search/route.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/search/search_delegate.dart';
|
||||
import 'package:aves/widgets/search/search_page.dart';
|
||||
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -47,7 +48,11 @@ class _HomePageState extends State<HomePage> {
|
|||
String? _shortcutRouteName, _shortcutSearchQuery;
|
||||
Set<String>? _shortcutFilters;
|
||||
|
||||
static const allowedShortcutRoutes = [CollectionPage.routeName, AlbumListPage.routeName, SearchPage.routeName];
|
||||
static const allowedShortcutRoutes = [
|
||||
CollectionPage.routeName,
|
||||
AlbumListPage.routeName,
|
||||
CollectionSearchDelegate.pageRouteName,
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -103,7 +108,7 @@ class _HomePageState extends State<HomePage> {
|
|||
appMode = multiple ? AppMode.pickMultipleMediaExternal : AppMode.pickSingleMediaExternal;
|
||||
break;
|
||||
case 'search':
|
||||
_shortcutRouteName = SearchPage.routeName;
|
||||
_shortcutRouteName = CollectionSearchDelegate.pageRouteName;
|
||||
_shortcutSearchQuery = intentData['query'];
|
||||
break;
|
||||
default:
|
||||
|
@ -245,9 +250,10 @@ class _HomePageState extends State<HomePage> {
|
|||
settings: const RouteSettings(name: AlbumListPage.routeName),
|
||||
builder: (_) => const AlbumListPage(),
|
||||
);
|
||||
case SearchPage.routeName:
|
||||
case CollectionSearchDelegate.pageRouteName:
|
||||
return SearchPageRoute(
|
||||
delegate: CollectionSearchDelegate(
|
||||
searchFieldLabel: context.l10n.searchCollectionFieldHint,
|
||||
source: source,
|
||||
canPop: false,
|
||||
initialQuery: _shortcutSearchQuery,
|
||||
|
|
|
@ -14,23 +14,21 @@ import 'package:aves/model/source/collection_source.dart';
|
|||
import 'package:aves/model/source/location.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/expandable_filter_row.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/search/search_page.dart';
|
||||
import 'package:aves/widgets/common/search/delegate.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class CollectionSearchDelegate {
|
||||
class CollectionSearchDelegate extends AvesSearchDelegate {
|
||||
final CollectionSource source;
|
||||
final CollectionLens? parentCollection;
|
||||
final ValueNotifier<String?> _expandedSectionNotifier = ValueNotifier(null);
|
||||
final bool canPop;
|
||||
|
||||
static const pageRouteName = '/search';
|
||||
static const int searchHistoryCount = 10;
|
||||
static final typeFilters = [
|
||||
FavouriteFilter.instance,
|
||||
|
@ -46,46 +44,18 @@ class CollectionSearchDelegate {
|
|||
];
|
||||
|
||||
CollectionSearchDelegate({
|
||||
required super.searchFieldLabel,
|
||||
required this.source,
|
||||
this.parentCollection,
|
||||
this.canPop = true,
|
||||
super.canPop,
|
||||
String? initialQuery,
|
||||
}) {
|
||||
}) : super(
|
||||
routeName: pageRouteName,
|
||||
) {
|
||||
query = initialQuery ?? '';
|
||||
}
|
||||
|
||||
Widget buildLeading(BuildContext context) {
|
||||
// use a property instead of checking `Navigator.canPop(context)`
|
||||
// because the navigator state changes as soon as we press back
|
||||
// so the leading may mistakenly switch to the close button
|
||||
return canPop
|
||||
? IconButton(
|
||||
icon: AnimatedIcon(
|
||||
icon: AnimatedIcons.menu_arrow,
|
||||
progress: transitionAnimation,
|
||||
),
|
||||
onPressed: () => _goBack(context),
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
)
|
||||
: const CloseButton(
|
||||
onPressed: SystemNavigator.pop,
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> buildActions(BuildContext context) {
|
||||
return [
|
||||
if (query.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(AIcons.clear),
|
||||
onPressed: () {
|
||||
query = '';
|
||||
showSuggestions(context);
|
||||
},
|
||||
tooltip: context.l10n.clearTooltip,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildSuggestions(BuildContext context) {
|
||||
final upQuery = query.trim().toUpperCase();
|
||||
bool containQuery(String s) => s.toUpperCase().contains(upQuery);
|
||||
|
@ -208,6 +178,7 @@ class CollectionSearchDelegate {
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// `buildResults` is called in the build phase,
|
||||
|
@ -215,7 +186,7 @@ class CollectionSearchDelegate {
|
|||
// and possibly trigger a rebuild here
|
||||
_select(context, _buildQueryFilter(true));
|
||||
});
|
||||
return const SizedBox.shrink();
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
QueryFilter? _buildQueryFilter(bool colorful) {
|
||||
|
@ -225,7 +196,7 @@ class CollectionSearchDelegate {
|
|||
|
||||
void _select(BuildContext context, CollectionFilter? filter) {
|
||||
if (filter == null) {
|
||||
_goBack(context);
|
||||
goBack(context);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -248,17 +219,12 @@ class CollectionSearchDelegate {
|
|||
// so that hero animation target is ready in the `FilterBar`,
|
||||
// even when the target is a child of an `AnimatedList`
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_goBack(context);
|
||||
goBack(context);
|
||||
});
|
||||
}
|
||||
|
||||
void _goBack(BuildContext context) {
|
||||
_clean();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
void _jumpToCollectionPage(BuildContext context, CollectionFilter filter) {
|
||||
_clean();
|
||||
clean();
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
@ -271,118 +237,4 @@ class CollectionSearchDelegate {
|
|||
(route) => false,
|
||||
);
|
||||
}
|
||||
|
||||
void _clean() {
|
||||
currentBody = null;
|
||||
focusNode?.unfocus();
|
||||
}
|
||||
|
||||
// adapted from Flutter `SearchDelegate` in `/material/search.dart`
|
||||
|
||||
void showResults(BuildContext context) {
|
||||
focusNode?.unfocus();
|
||||
currentBody = SearchBody.results;
|
||||
}
|
||||
|
||||
void showSuggestions(BuildContext context) {
|
||||
assert(focusNode != null, '_focusNode must be set by route before showSuggestions is called.');
|
||||
focusNode!.requestFocus();
|
||||
currentBody = SearchBody.suggestions;
|
||||
}
|
||||
|
||||
Animation<double> get transitionAnimation => proxyAnimation;
|
||||
|
||||
FocusNode? focusNode;
|
||||
|
||||
final TextEditingController queryTextController = TextEditingController();
|
||||
|
||||
final ProxyAnimation proxyAnimation = ProxyAnimation(kAlwaysDismissedAnimation);
|
||||
|
||||
String get query => queryTextController.text;
|
||||
|
||||
set query(String value) {
|
||||
queryTextController.text = value;
|
||||
}
|
||||
|
||||
final ValueNotifier<SearchBody?> currentBodyNotifier = ValueNotifier(null);
|
||||
|
||||
SearchBody? get currentBody => currentBodyNotifier.value;
|
||||
|
||||
set currentBody(SearchBody? value) {
|
||||
currentBodyNotifier.value = value;
|
||||
}
|
||||
|
||||
SearchPageRoute? route;
|
||||
}
|
||||
|
||||
// adapted from Flutter `_SearchBody` in `/material/search.dart`
|
||||
enum SearchBody { suggestions, results }
|
||||
|
||||
// adapted from Flutter `_SearchPageRoute` in `/material/search.dart`
|
||||
class SearchPageRoute<T> extends PageRoute<T> {
|
||||
SearchPageRoute({
|
||||
required this.delegate,
|
||||
}) : super(settings: const RouteSettings(name: SearchPage.routeName)) {
|
||||
assert(
|
||||
delegate.route == null,
|
||||
'The ${delegate.runtimeType} instance is currently used by another active '
|
||||
'search. Please close that search by calling close() on the SearchDelegate '
|
||||
'before openening another search with the same delegate instance.',
|
||||
);
|
||||
delegate.route = this;
|
||||
}
|
||||
|
||||
final CollectionSearchDelegate delegate;
|
||||
|
||||
@override
|
||||
Color? get barrierColor => null;
|
||||
|
||||
@override
|
||||
String? get barrierLabel => null;
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 300);
|
||||
|
||||
@override
|
||||
bool get maintainState => false;
|
||||
|
||||
@override
|
||||
Widget buildTransitions(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Animation<double> createAnimation() {
|
||||
final animation = super.createAnimation();
|
||||
delegate.proxyAnimation.parent = animation;
|
||||
return animation;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPage(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SearchPage(
|
||||
delegate: delegate,
|
||||
animation: animation,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didComplete(T? result) {
|
||||
super.didComplete(result);
|
||||
assert(delegate.route == this);
|
||||
delegate.route = null;
|
||||
delegate.currentBody = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import 'package:aves/widgets/common/basic/menu.dart';
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/search/route.dart';
|
||||
import 'package:aves/widgets/settings/accessibility/accessibility.dart';
|
||||
import 'package:aves/widgets/settings/app_export/items.dart';
|
||||
import 'package:aves/widgets/settings/app_export/selection_dialog.dart';
|
||||
|
@ -221,11 +222,13 @@ class _SettingsPageState extends State<SettingsPage> with FeedbackMixin {
|
|||
}
|
||||
|
||||
void _goToSearch(BuildContext context) {
|
||||
showSearch(
|
||||
context: context,
|
||||
delegate: SettingsSearchDelegate(
|
||||
searchFieldLabel: context.l10n.settingsSearchFieldLabel,
|
||||
sections: sections,
|
||||
Navigator.push(
|
||||
context,
|
||||
SearchPageRoute(
|
||||
delegate: SettingsSearchDelegate(
|
||||
searchFieldLabel: context.l10n.settingsSearchFieldLabel,
|
||||
sections: sections,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,58 +3,27 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
import 'package:aves/widgets/common/identity/highlight_title.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/search/delegate.dart';
|
||||
import 'package:aves/widgets/settings/settings_definition.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsSearchDelegate extends SearchDelegate {
|
||||
class SettingsSearchDelegate extends AvesSearchDelegate {
|
||||
final List<SettingsSection> sections;
|
||||
|
||||
static const pageRouteName = '/settings/search';
|
||||
|
||||
SettingsSearchDelegate({
|
||||
required String searchFieldLabel,
|
||||
required super.searchFieldLabel,
|
||||
required this.sections,
|
||||
}) : super(
|
||||
searchFieldLabel: searchFieldLabel,
|
||||
routeName: pageRouteName,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget buildLeading(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: AnimatedIcon(
|
||||
icon: AnimatedIcons.menu_arrow,
|
||||
progress: transitionAnimation,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget> buildActions(BuildContext context) {
|
||||
return [
|
||||
if (query.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(AIcons.clear),
|
||||
onPressed: () {
|
||||
query = '';
|
||||
showSuggestions(context);
|
||||
},
|
||||
tooltip: context.l10n.clearTooltip,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildSuggestions(BuildContext context) => const SizedBox();
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) {
|
||||
if (query.isEmpty) {
|
||||
showSuggestions(context);
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
Widget buildSuggestions(BuildContext context) {
|
||||
final upQuery = query.toUpperCase().trim();
|
||||
if (upQuery.isEmpty) return const SizedBox();
|
||||
|
||||
bool testKey(String key) => key.toUpperCase().contains(upQuery);
|
||||
|
||||
|
@ -109,4 +78,7 @@ class SettingsSearchDelegate extends SearchDelegate {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) => buildSuggestions(context);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue