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/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/favourite_toggler.dart';
|
import 'package:aves/widgets/common/favourite_toggler.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_app_bar.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/dialogs/tile_view_dialog.dart';
|
||||||
import 'package:aves/widgets/search/search_delegate.dart';
|
import 'package:aves/widgets/search/search_delegate.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -543,6 +544,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
context,
|
context,
|
||||||
SearchPageRoute(
|
SearchPageRoute(
|
||||||
delegate: CollectionSearchDelegate(
|
delegate: CollectionSearchDelegate(
|
||||||
|
searchFieldLabel: context.l10n.searchCollectionFieldHint,
|
||||||
source: collection.source,
|
source: collection.source,
|
||||||
parentCollection: collection,
|
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/permission_aware.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/size_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/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/add_shortcut_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
|
import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||||
|
@ -566,6 +567,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
context,
|
context,
|
||||||
SearchPageRoute(
|
SearchPageRoute(
|
||||||
delegate: CollectionSearchDelegate(
|
delegate: CollectionSearchDelegate(
|
||||||
|
searchFieldLabel: context.l10n.searchCollectionFieldHint,
|
||||||
source: collection.source,
|
source: collection.source,
|
||||||
parentCollection: collection,
|
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/theme/durations.dart';
|
||||||
import 'package:aves/utils/debouncer.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/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/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
class SearchPage extends StatefulWidget {
|
class SearchPage extends StatefulWidget {
|
||||||
static const routeName = '/search';
|
final AvesSearchDelegate delegate;
|
||||||
|
|
||||||
final CollectionSearchDelegate delegate;
|
|
||||||
final Animation<double> animation;
|
final Animation<double> animation;
|
||||||
|
|
||||||
const SearchPage({
|
const SearchPage({
|
||||||
|
@ -31,23 +29,40 @@ class _SearchPageState extends State<SearchPage> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
widget.delegate.queryTextController.addListener(_onQueryChanged);
|
_registerWidget(widget);
|
||||||
widget.animation.addStatusListener(_onAnimationStatusChanged);
|
widget.animation.addStatusListener(_onAnimationStatusChanged);
|
||||||
widget.delegate.currentBodyNotifier.addListener(_onSearchBodyChanged);
|
|
||||||
_focusNode.addListener(_onFocusChanged);
|
_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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
widget.delegate.queryTextController.removeListener(_onQueryChanged);
|
_unregisterWidget(widget);
|
||||||
widget.animation.removeStatusListener(_onAnimationStatusChanged);
|
widget.animation.removeStatusListener(_onAnimationStatusChanged);
|
||||||
widget.delegate.currentBodyNotifier.removeListener(_onSearchBodyChanged);
|
|
||||||
widget.delegate.focusNode = null;
|
|
||||||
_focusNode.dispose();
|
_focusNode.dispose();
|
||||||
super.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) {
|
void _onAnimationStatusChanged(AnimationStatus status) {
|
||||||
if (status != AnimationStatus.completed) {
|
if (status != AnimationStatus.completed) {
|
||||||
return;
|
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() {
|
void _onFocusChanged() {
|
||||||
if (_focusNode.hasFocus && widget.delegate.currentBody != SearchBody.suggestions) {
|
if (_focusNode.hasFocus && widget.delegate.currentBody != SearchBody.suggestions) {
|
||||||
widget.delegate.showSuggestions(context);
|
widget.delegate.showSuggestions(context);
|
||||||
|
@ -130,7 +132,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
hintText: context.l10n.searchCollectionFieldHint,
|
hintText: widget.delegate.searchFieldLabel,
|
||||||
hintStyle: theme.inputDecorationTheme.hintStyle,
|
hintStyle: theme.inputDecorationTheme.hintStyle,
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.search,
|
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/permission_aware.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/size_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/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/aves_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/filter_editors/cover_selection_dialog.dart';
|
import 'package:aves/widgets/dialogs/filter_editors/cover_selection_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
|
import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
|
||||||
|
@ -246,6 +247,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
|
||||||
context,
|
context,
|
||||||
SearchPageRoute(
|
SearchPageRoute(
|
||||||
delegate: CollectionSearchDelegate(
|
delegate: CollectionSearchDelegate(
|
||||||
|
searchFieldLabel: context.l10n.searchCollectionFieldHint,
|
||||||
source: context.read<CollectionSource>(),
|
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/basic/menu.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.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/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/filter_grids/common/action_delegates/chip_set.dart';
|
||||||
import 'package:aves/widgets/search/search_delegate.dart';
|
import 'package:aves/widgets/search/search_delegate.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -222,6 +223,7 @@ class _FilterGridAppBarState<T extends CollectionFilter> extends State<FilterGri
|
||||||
context,
|
context,
|
||||||
SearchPageRoute(
|
SearchPageRoute(
|
||||||
delegate: CollectionSearchDelegate(
|
delegate: CollectionSearchDelegate(
|
||||||
|
searchFieldLabel: context.l10n.searchCollectionFieldHint,
|
||||||
source: source,
|
source: source,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -17,9 +17,10 @@ import 'package:aves/services/viewer_service.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/widgets/collection/collection_page.dart';
|
import 'package:aves/widgets/collection/collection_page.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/routes.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/filter_grids/albums_page.dart';
|
||||||
import 'package:aves/widgets/search/search_delegate.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:aves/widgets/viewer/entry_viewer_page.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -47,7 +48,11 @@ class _HomePageState extends State<HomePage> {
|
||||||
String? _shortcutRouteName, _shortcutSearchQuery;
|
String? _shortcutRouteName, _shortcutSearchQuery;
|
||||||
Set<String>? _shortcutFilters;
|
Set<String>? _shortcutFilters;
|
||||||
|
|
||||||
static const allowedShortcutRoutes = [CollectionPage.routeName, AlbumListPage.routeName, SearchPage.routeName];
|
static const allowedShortcutRoutes = [
|
||||||
|
CollectionPage.routeName,
|
||||||
|
AlbumListPage.routeName,
|
||||||
|
CollectionSearchDelegate.pageRouteName,
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -103,7 +108,7 @@ class _HomePageState extends State<HomePage> {
|
||||||
appMode = multiple ? AppMode.pickMultipleMediaExternal : AppMode.pickSingleMediaExternal;
|
appMode = multiple ? AppMode.pickMultipleMediaExternal : AppMode.pickSingleMediaExternal;
|
||||||
break;
|
break;
|
||||||
case 'search':
|
case 'search':
|
||||||
_shortcutRouteName = SearchPage.routeName;
|
_shortcutRouteName = CollectionSearchDelegate.pageRouteName;
|
||||||
_shortcutSearchQuery = intentData['query'];
|
_shortcutSearchQuery = intentData['query'];
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -245,9 +250,10 @@ class _HomePageState extends State<HomePage> {
|
||||||
settings: const RouteSettings(name: AlbumListPage.routeName),
|
settings: const RouteSettings(name: AlbumListPage.routeName),
|
||||||
builder: (_) => const AlbumListPage(),
|
builder: (_) => const AlbumListPage(),
|
||||||
);
|
);
|
||||||
case SearchPage.routeName:
|
case CollectionSearchDelegate.pageRouteName:
|
||||||
return SearchPageRoute(
|
return SearchPageRoute(
|
||||||
delegate: CollectionSearchDelegate(
|
delegate: CollectionSearchDelegate(
|
||||||
|
searchFieldLabel: context.l10n.searchCollectionFieldHint,
|
||||||
source: source,
|
source: source,
|
||||||
canPop: false,
|
canPop: false,
|
||||||
initialQuery: _shortcutSearchQuery,
|
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/location.dart';
|
||||||
import 'package:aves/model/source/tag.dart';
|
import 'package:aves/model/source/tag.dart';
|
||||||
import 'package:aves/ref/mime_types.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/collection/collection_page.dart';
|
||||||
import 'package:aves/widgets/common/expandable_filter_row.dart';
|
import 'package:aves/widgets/common/expandable_filter_row.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_filter_chip.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:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class CollectionSearchDelegate {
|
class CollectionSearchDelegate extends AvesSearchDelegate {
|
||||||
final CollectionSource source;
|
final CollectionSource source;
|
||||||
final CollectionLens? parentCollection;
|
final CollectionLens? parentCollection;
|
||||||
final ValueNotifier<String?> _expandedSectionNotifier = ValueNotifier(null);
|
final ValueNotifier<String?> _expandedSectionNotifier = ValueNotifier(null);
|
||||||
final bool canPop;
|
|
||||||
|
|
||||||
|
static const pageRouteName = '/search';
|
||||||
static const int searchHistoryCount = 10;
|
static const int searchHistoryCount = 10;
|
||||||
static final typeFilters = [
|
static final typeFilters = [
|
||||||
FavouriteFilter.instance,
|
FavouriteFilter.instance,
|
||||||
|
@ -46,46 +44,18 @@ class CollectionSearchDelegate {
|
||||||
];
|
];
|
||||||
|
|
||||||
CollectionSearchDelegate({
|
CollectionSearchDelegate({
|
||||||
|
required super.searchFieldLabel,
|
||||||
required this.source,
|
required this.source,
|
||||||
this.parentCollection,
|
this.parentCollection,
|
||||||
this.canPop = true,
|
super.canPop,
|
||||||
String? initialQuery,
|
String? initialQuery,
|
||||||
}) {
|
}) : super(
|
||||||
|
routeName: pageRouteName,
|
||||||
|
) {
|
||||||
query = initialQuery ?? '';
|
query = initialQuery ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildLeading(BuildContext context) {
|
@override
|
||||||
// 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,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildSuggestions(BuildContext context) {
|
Widget buildSuggestions(BuildContext context) {
|
||||||
final upQuery = query.trim().toUpperCase();
|
final upQuery = query.trim().toUpperCase();
|
||||||
bool containQuery(String s) => s.toUpperCase().contains(upQuery);
|
bool containQuery(String s) => s.toUpperCase().contains(upQuery);
|
||||||
|
@ -208,6 +178,7 @@ class CollectionSearchDelegate {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Widget buildResults(BuildContext context) {
|
Widget buildResults(BuildContext context) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
// `buildResults` is called in the build phase,
|
// `buildResults` is called in the build phase,
|
||||||
|
@ -215,7 +186,7 @@ class CollectionSearchDelegate {
|
||||||
// and possibly trigger a rebuild here
|
// and possibly trigger a rebuild here
|
||||||
_select(context, _buildQueryFilter(true));
|
_select(context, _buildQueryFilter(true));
|
||||||
});
|
});
|
||||||
return const SizedBox.shrink();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
QueryFilter? _buildQueryFilter(bool colorful) {
|
QueryFilter? _buildQueryFilter(bool colorful) {
|
||||||
|
@ -225,7 +196,7 @@ class CollectionSearchDelegate {
|
||||||
|
|
||||||
void _select(BuildContext context, CollectionFilter? filter) {
|
void _select(BuildContext context, CollectionFilter? filter) {
|
||||||
if (filter == null) {
|
if (filter == null) {
|
||||||
_goBack(context);
|
goBack(context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,17 +219,12 @@ class CollectionSearchDelegate {
|
||||||
// so that hero animation target is ready in the `FilterBar`,
|
// so that hero animation target is ready in the `FilterBar`,
|
||||||
// even when the target is a child of an `AnimatedList`
|
// even when the target is a child of an `AnimatedList`
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_goBack(context);
|
goBack(context);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _goBack(BuildContext context) {
|
|
||||||
_clean();
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _jumpToCollectionPage(BuildContext context, CollectionFilter filter) {
|
void _jumpToCollectionPage(BuildContext context, CollectionFilter filter) {
|
||||||
_clean();
|
clean();
|
||||||
Navigator.pushAndRemoveUntil(
|
Navigator.pushAndRemoveUntil(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
@ -271,118 +237,4 @@ class CollectionSearchDelegate {
|
||||||
(route) => false,
|
(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/build_context.dart';
|
||||||
import 'package:aves/widgets/common/extensions/media_query.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/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/accessibility/accessibility.dart';
|
||||||
import 'package:aves/widgets/settings/app_export/items.dart';
|
import 'package:aves/widgets/settings/app_export/items.dart';
|
||||||
import 'package:aves/widgets/settings/app_export/selection_dialog.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) {
|
void _goToSearch(BuildContext context) {
|
||||||
showSearch(
|
Navigator.push(
|
||||||
context: context,
|
context,
|
||||||
delegate: SettingsSearchDelegate(
|
SearchPageRoute(
|
||||||
searchFieldLabel: context.l10n.settingsSearchFieldLabel,
|
delegate: SettingsSearchDelegate(
|
||||||
sections: sections,
|
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/empty.dart';
|
||||||
import 'package:aves/widgets/common/identity/highlight_title.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/providers/media_query_data_provider.dart';
|
||||||
|
import 'package:aves/widgets/common/search/delegate.dart';
|
||||||
import 'package:aves/widgets/settings/settings_definition.dart';
|
import 'package:aves/widgets/settings/settings_definition.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SettingsSearchDelegate extends SearchDelegate {
|
class SettingsSearchDelegate extends AvesSearchDelegate {
|
||||||
final List<SettingsSection> sections;
|
final List<SettingsSection> sections;
|
||||||
|
|
||||||
|
static const pageRouteName = '/settings/search';
|
||||||
|
|
||||||
SettingsSearchDelegate({
|
SettingsSearchDelegate({
|
||||||
required String searchFieldLabel,
|
required super.searchFieldLabel,
|
||||||
required this.sections,
|
required this.sections,
|
||||||
}) : super(
|
}) : super(
|
||||||
searchFieldLabel: searchFieldLabel,
|
routeName: pageRouteName,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildLeading(BuildContext context) {
|
Widget buildSuggestions(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();
|
|
||||||
}
|
|
||||||
|
|
||||||
final upQuery = query.toUpperCase().trim();
|
final upQuery = query.toUpperCase().trim();
|
||||||
|
if (upQuery.isEmpty) return const SizedBox();
|
||||||
|
|
||||||
bool testKey(String key) => key.toUpperCase().contains(upQuery);
|
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