diff --git a/CHANGELOG.md b/CHANGELOG.md
index c89c1ae4f..e73d2d6fa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
### Added
- Viewer: optional gesture to show previous/next item
+- Albums / Countries / Tags: live title filter
## [v1.6.11] - 2022-07-26
diff --git a/lib/model/actions/chip_set_actions.dart b/lib/model/actions/chip_set_actions.dart
index 43fb11ecf..a1e919801 100644
--- a/lib/model/actions/chip_set_actions.dart
+++ b/lib/model/actions/chip_set_actions.dart
@@ -10,6 +10,7 @@ enum ChipSetAction {
selectNone,
// browsing
search,
+ toggleTitleSearch,
createAlbum,
// browsing or selecting
map,
@@ -35,6 +36,7 @@ class ChipSetActions {
static const browsing = [
ChipSetAction.search,
+ ChipSetAction.toggleTitleSearch,
ChipSetAction.createAlbum,
ChipSetAction.map,
ChipSetAction.slideshow,
@@ -69,6 +71,9 @@ extension ExtraChipSetAction on ChipSetAction {
// browsing
case ChipSetAction.search:
return MaterialLocalizations.of(context).searchFieldLabel;
+ case ChipSetAction.toggleTitleSearch:
+ // different data depending on toggle state
+ return context.l10n.collectionActionShowTitleSearch;
case ChipSetAction.createAlbum:
return context.l10n.chipActionCreateAlbum;
// browsing or selecting
@@ -111,6 +116,9 @@ extension ExtraChipSetAction on ChipSetAction {
// browsing
case ChipSetAction.search:
return AIcons.search;
+ case ChipSetAction.toggleTitleSearch:
+ // different data depending on toggle state
+ return AIcons.filter;
case ChipSetAction.createAlbum:
return AIcons.add;
// browsing or selecting
diff --git a/lib/model/query.dart b/lib/model/query.dart
index 1078a71fc..df48d2206 100644
--- a/lib/model/query.dart
+++ b/lib/model/query.dart
@@ -4,6 +4,10 @@ import 'package:aves/utils/change_notifier.dart';
import 'package:flutter/foundation.dart';
class Query extends ChangeNotifier {
+ final AChangeNotifier _focusRequestNotifier = AChangeNotifier();
+ final ValueNotifier _queryNotifier = ValueNotifier('');
+ final StreamController _enabledStreamController = StreamController.broadcast();
+
Query({required String? initialValue}) {
if (initialValue != null && initialValue.isNotEmpty) {
_enabled = true;
@@ -28,11 +32,9 @@ class Query extends ChangeNotifier {
void toggle() => enabled = !enabled;
- final StreamController _enabledStreamController = StreamController.broadcast();
-
Stream get enabledStream => _enabledStreamController.stream;
- final AChangeNotifier focusRequestNotifier = AChangeNotifier();
+ AChangeNotifier get focusRequestNotifier => _focusRequestNotifier;
- final ValueNotifier queryNotifier = ValueNotifier('');
+ ValueNotifier get queryNotifier => _queryNotifier;
}
diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart
index bd14c2eda..53cb7715c 100644
--- a/lib/widgets/collection/app_bar.dart
+++ b/lib/widgets/collection/app_bar.dart
@@ -19,11 +19,12 @@ import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
import 'package:aves/widgets/collection/filter_bar.dart';
import 'package:aves/widgets/collection/query_bar.dart';
-import 'package:aves/widgets/common/app_bar_subtitle.dart';
-import 'package:aves/widgets/common/app_bar_title.dart';
+import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart';
+import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
+import 'package:aves/widgets/common/app_bar/favourite_toggler.dart';
+import 'package:aves/widgets/common/app_bar/title_search_toggler.dart';
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';
@@ -142,7 +143,7 @@ class _CollectionAppBarState extends State with SingleTickerPr
isSelecting: isSelecting,
),
title: _buildAppBarTitle(isSelecting),
- actions: _buildActions(selection),
+ actions: _buildActions(context, selection),
bottom: Column(
children: [
if (showFilterBar)
@@ -234,7 +235,7 @@ class _CollectionAppBarState extends State with SingleTickerPr
}
}
- List _buildActions(Selection selection) {
+ List _buildActions(BuildContext context, Selection selection) {
final isSelecting = selection.isSelecting;
final selectedItemCount = selection.selectedItems.length;
@@ -328,7 +329,7 @@ class _CollectionAppBarState extends State with SingleTickerPr
return Selector(
selector: (context, query) => query?.enabled ?? false,
builder: (context, queryEnabled, child) {
- return _TitleSearchToggler(
+ return TitleSearchToggler(
queryEnabled: queryEnabled,
onPressed: onPressed,
);
@@ -353,7 +354,7 @@ class _CollectionAppBarState extends State with SingleTickerPr
late Widget child;
switch (action) {
case EntrySetAction.toggleTitleSearch:
- child = _TitleSearchToggler(
+ child = TitleSearchToggler(
queryEnabled: context.read().enabled,
isMenuItem: true,
);
@@ -556,30 +557,3 @@ class _CollectionAppBarState extends State with SingleTickerPr
);
}
}
-
-class _TitleSearchToggler extends StatelessWidget {
- final bool queryEnabled, isMenuItem;
- final VoidCallback? onPressed;
-
- const _TitleSearchToggler({
- required this.queryEnabled,
- this.isMenuItem = false,
- this.onPressed,
- });
-
- @override
- Widget build(BuildContext context) {
- final icon = Icon(queryEnabled ? AIcons.filterOff : AIcons.filter);
- final text = queryEnabled ? context.l10n.collectionActionHideTitleSearch : context.l10n.collectionActionShowTitleSearch;
- return isMenuItem
- ? MenuRow(
- text: text,
- icon: icon,
- )
- : IconButton(
- icon: icon,
- onPressed: onPressed,
- tooltip: text,
- );
- }
-}
diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart
index 6c9d116e9..1e262055e 100644
--- a/lib/widgets/collection/collection_grid.dart
+++ b/lib/widgets/collection/collection_grid.dart
@@ -186,52 +186,51 @@ class _CollectionSectionedContent extends StatefulWidget {
}
class _CollectionSectionedContentState extends State<_CollectionSectionedContent> {
+ final ValueNotifier _appBarHeightNotifier = ValueNotifier(0);
+ final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable');
+
CollectionLens get collection => widget.collection;
TileLayout get tileLayout => widget.tileLayout;
ScrollController get scrollController => widget.scrollController;
- final ValueNotifier appBarHeightNotifier = ValueNotifier(0);
-
- final GlobalKey scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable');
-
@override
Widget build(BuildContext context) {
final scrollView = AnimationLimiter(
child: _CollectionScrollView(
- scrollableKey: scrollableKey,
+ scrollableKey: _scrollableKey,
collection: collection,
appBar: CollectionAppBar(
- appBarHeightNotifier: appBarHeightNotifier,
+ appBarHeightNotifier: _appBarHeightNotifier,
collection: collection,
),
- appBarHeightNotifier: appBarHeightNotifier,
+ appBarHeightNotifier: _appBarHeightNotifier,
isScrollingNotifier: widget.isScrollingNotifier,
scrollController: scrollController,
),
);
final scaler = _CollectionScaler(
- scrollableKey: scrollableKey,
- appBarHeightNotifier: appBarHeightNotifier,
+ scrollableKey: _scrollableKey,
+ appBarHeightNotifier: _appBarHeightNotifier,
tileLayout: tileLayout,
child: scrollView,
);
final selector = GridSelectionGestureDetector(
- scrollableKey: scrollableKey,
+ scrollableKey: _scrollableKey,
selectable: context.select, bool>((v) => v.value.canSelectMedia),
items: collection.sortedEntries,
scrollController: scrollController,
- appBarHeightNotifier: appBarHeightNotifier,
+ appBarHeightNotifier: _appBarHeightNotifier,
child: scaler,
);
return GridItemTracker(
- scrollableKey: scrollableKey,
+ scrollableKey: _scrollableKey,
tileLayout: tileLayout,
- appBarHeightNotifier: appBarHeightNotifier,
+ appBarHeightNotifier: _appBarHeightNotifier,
scrollController: scrollController,
child: selector,
);
diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart
index fda86253b..210acd11f 100644
--- a/lib/widgets/collection/collection_page.dart
+++ b/lib/widgets/collection/collection_page.dart
@@ -128,7 +128,7 @@ class _CollectionPageState extends State {
),
),
floatingActionButton: _buildFab(context, hasSelection),
- drawer: AppDrawer(currentCollection: _collection),
+ drawer: canNavigate ? AppDrawer(currentCollection: _collection) : null,
bottomNavigationBar: showBottomNavigationBar
? AppBottomNavBar(
events: _draggableScrollBarEventStreamController.stream,
diff --git a/lib/widgets/common/app_bar_subtitle.dart b/lib/widgets/common/app_bar/app_bar_subtitle.dart
similarity index 100%
rename from lib/widgets/common/app_bar_subtitle.dart
rename to lib/widgets/common/app_bar/app_bar_subtitle.dart
diff --git a/lib/widgets/common/app_bar_title.dart b/lib/widgets/common/app_bar/app_bar_title.dart
similarity index 100%
rename from lib/widgets/common/app_bar_title.dart
rename to lib/widgets/common/app_bar/app_bar_title.dart
diff --git a/lib/widgets/common/favourite_toggler.dart b/lib/widgets/common/app_bar/favourite_toggler.dart
similarity index 100%
rename from lib/widgets/common/favourite_toggler.dart
rename to lib/widgets/common/app_bar/favourite_toggler.dart
diff --git a/lib/widgets/common/sliver_app_bar_title.dart b/lib/widgets/common/app_bar/sliver_app_bar_title.dart
similarity index 100%
rename from lib/widgets/common/sliver_app_bar_title.dart
rename to lib/widgets/common/app_bar/sliver_app_bar_title.dart
diff --git a/lib/widgets/common/app_bar/title_search_toggler.dart b/lib/widgets/common/app_bar/title_search_toggler.dart
new file mode 100644
index 000000000..52e7c7bff
--- /dev/null
+++ b/lib/widgets/common/app_bar/title_search_toggler.dart
@@ -0,0 +1,32 @@
+import 'package:aves/theme/icons.dart';
+import 'package:aves/widgets/common/basic/menu.dart';
+import 'package:aves/widgets/common/extensions/build_context.dart';
+import 'package:flutter/material.dart';
+
+class TitleSearchToggler extends StatelessWidget {
+ final bool queryEnabled, isMenuItem;
+ final VoidCallback? onPressed;
+
+ const TitleSearchToggler({
+ super.key,
+ required this.queryEnabled,
+ this.isMenuItem = false,
+ this.onPressed,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final icon = Icon(queryEnabled ? AIcons.filterOff : AIcons.filter);
+ final text = queryEnabled ? context.l10n.collectionActionHideTitleSearch : context.l10n.collectionActionShowTitleSearch;
+ return isMenuItem
+ ? MenuRow(
+ text: text,
+ icon: icon,
+ )
+ : IconButton(
+ icon: icon,
+ onPressed: onPressed,
+ tooltip: text,
+ );
+ }
+}
diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart
index 928954be5..a2f040eda 100644
--- a/lib/widgets/filter_grids/album_pick.dart
+++ b/lib/widgets/filter_grids/album_pick.dart
@@ -3,23 +3,22 @@ import 'package:aves/model/actions/chip_set_actions.dart';
import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
+import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/album.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
-import 'package:aves/widgets/common/app_bar_subtitle.dart';
import 'package:aves/widgets/common/basic/menu.dart';
-import 'package:aves/widgets/common/basic/query_bar.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_filter_chip.dart';
import 'package:aves/widgets/common/identity/empty.dart';
import 'package:aves/widgets/common/providers/selection_provider.dart';
import 'package:aves/widgets/dialogs/filter_editors/create_album_dialog.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/album_set.dart';
+import 'package:aves/widgets/filter_grids/common/app_bar.dart';
import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
@@ -61,10 +60,25 @@ class _AlbumPickPage extends StatefulWidget {
}
class _AlbumPickPageState extends State<_AlbumPickPage> {
- final _queryNotifier = ValueNotifier('');
+ final ValueNotifier _appBarHeightNotifier = ValueNotifier(0);
CollectionSource get source => widget.source;
+ String get title {
+ switch (widget.moveType) {
+ case MoveType.copy:
+ return context.l10n.albumPickPageTitleCopy;
+ case MoveType.move:
+ return context.l10n.albumPickPageTitleMove;
+ case MoveType.export:
+ return context.l10n.albumPickPageTitleExport;
+ case MoveType.toBin:
+ case MoveType.fromBin:
+ case null:
+ return context.l10n.albumPickPageTitlePick;
+ }
+ }
+
@override
Widget build(BuildContext context) {
return ListenableProvider>.value(
@@ -79,24 +93,21 @@ class _AlbumPickPageState extends State<_AlbumPickPage> {
return SelectionProvider>(
child: FilterGridPage(
settingsRouteKey: AlbumListPage.routeName,
- appBar: _AlbumPickAppBar(
+ appBar: FilterGridAppBar(
source: source,
- moveType: widget.moveType,
+ title: title,
actionDelegate: AlbumChipSetActionDelegate(gridItems),
- queryNotifier: _queryNotifier,
+ actionsBuilder: _buildActions,
+ isEmpty: false,
+ appBarHeightNotifier: _appBarHeightNotifier,
),
- appBarHeight: AvesAppBar.appBarHeightForContentHeight(_AlbumPickAppBar.contentHeight),
+ appBarHeightNotifier: _appBarHeightNotifier,
sections: AlbumListPage.groupToSections(context, source, gridItems),
newFilters: source.getNewAlbumFilters(context),
sortFactor: settings.albumSortFactor,
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
selectable: false,
- queryNotifier: _queryNotifier,
- applyQuery: (filters, query) {
- if (query.isEmpty) return filters;
- query = query.toUpperCase();
- return filters.where((item) => (item.filter.displayName ?? item.filter.album).toUpperCase().contains(query)).toList();
- },
+ applyQuery: AlbumListPage.applyQuery,
emptyBuilder: () => EmptyContent(
icon: AIcons.album,
text: context.l10n.albumEmpty,
@@ -110,57 +121,15 @@ class _AlbumPickPageState extends State<_AlbumPickPage> {
),
);
}
-}
-class _AlbumPickAppBar extends StatelessWidget {
- final CollectionSource source;
- final MoveType? moveType;
- final AlbumChipSetActionDelegate actionDelegate;
- final ValueNotifier queryNotifier;
-
- static const contentHeight = kToolbarHeight + _AlbumQueryBar.preferredHeight;
-
- const _AlbumPickAppBar({
- required this.source,
- required this.moveType,
- required this.actionDelegate,
- required this.queryNotifier,
- });
-
- @override
- Widget build(BuildContext context) {
- String title() {
- switch (moveType) {
- case MoveType.copy:
- return context.l10n.albumPickPageTitleCopy;
- case MoveType.move:
- return context.l10n.albumPickPageTitleMove;
- case MoveType.export:
- return context.l10n.albumPickPageTitleExport;
- case MoveType.toBin:
- case MoveType.fromBin:
- case null:
- return context.l10n.albumPickPageTitlePick;
- }
- }
-
- return AvesAppBar(
- contentHeight: contentHeight,
- leading: const BackButton(),
- title: SourceStateAwareAppBarTitle(
- title: Text(title()),
- source: source,
- ),
- actions: _buildActions(context),
- bottom: _AlbumQueryBar(
- queryNotifier: queryNotifier,
- ),
- );
- }
-
- List _buildActions(BuildContext context) {
+ List _buildActions(
+ BuildContext context,
+ AppMode appMode,
+ Selection> selection,
+ AlbumChipSetActionDelegate actionDelegate,
+ ) {
return [
- if (moveType != null)
+ if (widget.moveType != null)
IconButton(
icon: const Icon(AIcons.add),
onPressed: () async {
@@ -180,10 +149,9 @@ class _AlbumPickAppBar extends StatelessWidget {
child: PopupMenuButton(
itemBuilder: (context) {
return [
- PopupMenuItem(
- value: ChipSetAction.configureView,
- child: MenuRow(text: context.l10n.menuActionConfigureView, icon: const Icon(AIcons.view)),
- ),
+ FilterGridAppBar.toMenuItem(context, ChipSetAction.configureView, enabled: true),
+ const PopupMenuDivider(),
+ FilterGridAppBar.toMenuItem(context, ChipSetAction.toggleTitleSearch, enabled: true),
];
},
onSelected: (action) async {
@@ -200,27 +168,3 @@ class _AlbumPickAppBar extends StatelessWidget {
];
}
}
-
-class _AlbumQueryBar extends StatelessWidget implements PreferredSizeWidget {
- final ValueNotifier queryNotifier;
-
- static const preferredHeight = kToolbarHeight;
-
- const _AlbumQueryBar({
- required this.queryNotifier,
- });
-
- @override
- Size get preferredSize => const Size.fromHeight(preferredHeight);
-
- @override
- Widget build(BuildContext context) {
- return Container(
- height: _AlbumQueryBar.preferredHeight,
- alignment: Alignment.topCenter,
- child: QueryBar(
- queryNotifier: queryNotifier,
- ),
- );
- }
-}
diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart
index fec6279e6..fb5a47786 100644
--- a/lib/widgets/filter_grids/albums_page.dart
+++ b/lib/widgets/filter_grids/albums_page.dart
@@ -43,7 +43,7 @@ class AlbumListPage extends StatelessWidget {
return StreamBuilder?>(
// to update sections by tier
stream: covers.packageChangeStream,
- builder: (context, snapshot) => FilterNavigationPage(
+ builder: (context, snapshot) => FilterNavigationPage(
source: source,
title: context.l10n.albumPageTitle,
sortFactor: settings.albumSortFactor,
@@ -51,6 +51,7 @@ class AlbumListPage extends StatelessWidget {
actionDelegate: AlbumChipSetActionDelegate(gridItems),
filterSections: groupToSections(context, source, gridItems),
newFilters: source.getNewAlbumFilters(context),
+ applyQuery: applyQuery,
emptyBuilder: () => EmptyContent(
icon: AIcons.album,
text: context.l10n.albumEmpty,
@@ -67,6 +68,10 @@ class AlbumListPage extends StatelessWidget {
// common with album selection page to move/copy entries
+ static List> applyQuery(BuildContext context, List> filters, String query) {
+ return filters.where((item) => (item.filter.displayName ?? item.filter.album).toUpperCase().contains(query)).toList();
+ }
+
static List> getAlbumGridItems(BuildContext context, CollectionSource source) {
final filters = source.rawAlbums.map((album) => AlbumFilter(album, source.getAlbumDisplayName(context, album))).toSet();
diff --git a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart
index a9ef0e0c0..060559a3c 100644
--- a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart
+++ b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart
@@ -4,6 +4,7 @@ import 'package:aves/model/covers.dart';
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
+import 'package:aves/model/query.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
@@ -62,6 +63,8 @@ abstract class ChipSetActionDelegate with FeedbackMi
// browsing
case ChipSetAction.search:
return appMode.canNavigate && !isSelecting;
+ case ChipSetAction.toggleTitleSearch:
+ return !isSelecting;
case ChipSetAction.createAlbum:
return false;
// browsing or selecting
@@ -104,6 +107,7 @@ abstract class ChipSetActionDelegate with FeedbackMi
case ChipSetAction.selectNone:
// browsing
case ChipSetAction.search:
+ case ChipSetAction.toggleTitleSearch:
case ChipSetAction.createAlbum:
return true;
// browsing or selecting
@@ -143,6 +147,9 @@ abstract class ChipSetActionDelegate with FeedbackMi
case ChipSetAction.search:
_goToSearch(context);
break;
+ case ChipSetAction.toggleTitleSearch:
+ context.read().toggle();
+ break;
case ChipSetAction.createAlbum:
break;
// browsing or selecting
diff --git a/lib/widgets/filter_grids/common/app_bar.dart b/lib/widgets/filter_grids/common/app_bar.dart
index 25bb1e244..1beed106f 100644
--- a/lib/widgets/filter_grids/common/app_bar.dart
+++ b/lib/widgets/filter_grids/common/app_bar.dart
@@ -1,47 +1,85 @@
+import 'dart:async';
+
import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/chip_set_actions.dart';
import 'package:aves/model/filters/filters.dart';
+import 'package:aves/model/query.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/theme/durations.dart';
-import 'package:aves/widgets/common/app_bar_subtitle.dart';
-import 'package:aves/widgets/common/app_bar_title.dart';
+import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart';
+import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
+import 'package:aves/widgets/common/app_bar/title_search_toggler.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/filter_grids/common/query_bar.dart';
import 'package:aves/widgets/search/search_delegate.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
-class FilterGridAppBar extends StatefulWidget {
+typedef ActionsBuilder> = List Function(
+ BuildContext context,
+ AppMode appMode,
+ Selection> selection,
+ CSAD actionDelegate,
+);
+
+class FilterGridAppBar> extends StatefulWidget {
final CollectionSource source;
final String title;
- final ChipSetActionDelegate actionDelegate;
+ final CSAD actionDelegate;
+ final ActionsBuilder? actionsBuilder;
final bool isEmpty;
+ final ValueNotifier appBarHeightNotifier;
const FilterGridAppBar({
super.key,
required this.source,
required this.title,
required this.actionDelegate,
+ this.actionsBuilder,
required this.isEmpty,
+ required this.appBarHeightNotifier,
});
@override
- State> createState() => _FilterGridAppBarState();
+ State> createState() => _FilterGridAppBarState();
+
+ static PopupMenuItem toMenuItem(BuildContext context, ChipSetAction action, {required bool enabled}) {
+ late Widget child;
+ switch (action) {
+ case ChipSetAction.toggleTitleSearch:
+ child = TitleSearchToggler(
+ queryEnabled: context.read().enabled,
+ isMenuItem: true,
+ );
+ break;
+ default:
+ child = MenuRow(text: action.getText(context), icon: action.getIcon());
+ break;
+ }
+
+ return PopupMenuItem(
+ value: action,
+ enabled: enabled,
+ child: child,
+ );
+ }
}
-class _FilterGridAppBarState extends State> with SingleTickerProviderStateMixin {
+class _FilterGridAppBarState> extends State> with SingleTickerProviderStateMixin {
+ final List _subscriptions = [];
late AnimationController _browseToSelectAnimation;
final ValueNotifier _isSelectingNotifier = ValueNotifier(false);
+ final FocusNode _queryBarFocusNode = FocusNode();
+ late final Listenable _queryFocusRequestNotifier;
CollectionSource get source => widget.source;
- ChipSetActionDelegate get actionDelegate => widget.actionDelegate;
-
static const browsingQuickActions = [
ChipSetAction.search,
];
@@ -54,17 +92,26 @@ class _FilterGridAppBarState extends State();
+ _subscriptions.add(query.enabledStream.listen((e) => _updateAppBarHeight()));
+ _queryFocusRequestNotifier = query.focusRequestNotifier;
+ _queryFocusRequestNotifier.addListener(_onQueryFocusRequest);
_browseToSelectAnimation = AnimationController(
duration: context.read().iconAnimation,
vsync: this,
);
_isSelectingNotifier.addListener(_onActivityChange);
+ WidgetsBinding.instance.addPostFrameCallback((_) => _updateAppBarHeight());
}
@override
void dispose() {
+ _queryFocusRequestNotifier.removeListener(_onQueryFocusRequest);
_isSelectingNotifier.removeListener(_onActivityChange);
_browseToSelectAnimation.dispose();
+ _subscriptions
+ ..forEach((sub) => sub.cancel())
+ ..clear();
super.dispose();
}
@@ -74,18 +121,35 @@ class _FilterGridAppBarState extends State>>();
final isSelecting = selection.isSelecting;
_isSelectingNotifier.value = isSelecting;
- return AvesAppBar(
- contentHeight: kToolbarHeight,
- leading: _buildAppBarLeading(
- hasDrawer: appMode.canNavigate,
- isSelecting: isSelecting,
- ),
- title: _buildAppBarTitle(isSelecting),
- actions: _buildActions(appMode, selection),
- transitionKey: isSelecting,
+ return Selector(
+ selector: (context, query) => query.enabled,
+ builder: (context, queryEnabled, child) {
+ ActionsBuilder actionsBuilder = widget.actionsBuilder ?? _buildActions;
+ return AvesAppBar(
+ contentHeight: appBarContentHeight,
+ leading: _buildAppBarLeading(
+ hasDrawer: appMode.canNavigate,
+ isSelecting: isSelecting,
+ ),
+ title: _buildAppBarTitle(isSelecting),
+ actions: actionsBuilder(context, appMode, selection, widget.actionDelegate),
+ bottom: queryEnabled
+ ? FilterQueryBar(
+ queryNotifier: context.select>((query) => query.queryNotifier),
+ focusNode: _queryBarFocusNode,
+ )
+ : null,
+ transitionKey: isSelecting,
+ );
+ },
);
}
+ double get appBarContentHeight {
+ final hasQuery = context.read().enabled;
+ return kToolbarHeight + (hasQuery ? FilterQueryBar.preferredHeight : .0);
+ }
+
Widget _buildAppBarLeading({required bool hasDrawer, required bool isSelecting}) {
if (!hasDrawer) {
return const CloseButton();
@@ -136,7 +200,12 @@ class _FilterGridAppBarState extends State _buildActions(AppMode appMode, Selection> selection) {
+ List _buildActions(
+ BuildContext context,
+ AppMode appMode,
+ Selection> selection,
+ CSAD actionDelegate,
+ ) {
final itemCount = actionDelegate.allItems.length;
final isSelecting = selection.isSelecting;
final selectedItems = selection.selectedItems;
@@ -157,7 +226,7 @@ class _FilterGridAppBarState extends State _toActionButton(action, enabled: canApply(action)),
+ (action) => _toActionButton(context, actionDelegate, action, enabled: canApply(action)),
);
return [
@@ -166,13 +235,13 @@ class _FilterGridAppBarState extends State(
itemBuilder: (context) {
final generalMenuItems = ChipSetActions.general.where(isVisible).map(
- (action) => _toMenuItem(action, enabled: canApply(action)),
+ (action) => FilterGridAppBar.toMenuItem(context, action, enabled: canApply(action)),
);
final browsingMenuActions = ChipSetActions.browsing.where((v) => !browsingQuickActions.contains(v));
final selectionMenuActions = ChipSetActions.selection.where((v) => !selectionQuickActions.contains(v));
final contextualMenuItems = (isSelecting ? selectionMenuActions : browsingMenuActions).where(isVisible).map(
- (action) => _toMenuItem(action, enabled: canApply(action)),
+ (action) => FilterGridAppBar.toMenuItem(context, action, enabled: canApply(action)),
);
return [
@@ -184,29 +253,45 @@ class _FilterGridAppBarState extends State _onActionSelected(action) : null,
- tooltip: action.getText(context),
- );
- }
-
- PopupMenuItem _toMenuItem(ChipSetAction action, {required bool enabled}) {
- return PopupMenuItem(
- value: action,
- enabled: enabled,
- child: MenuRow(text: action.getText(context), icon: action.getIcon()),
- );
+ Widget _toActionButton(
+ BuildContext context,
+ CSAD actionDelegate,
+ ChipSetAction action, {
+ required bool enabled,
+ }) {
+ final onPressed = enabled ? () => _onActionSelected(context, action, actionDelegate) : null;
+ switch (action) {
+ case ChipSetAction.toggleTitleSearch:
+ // `Query` may not be available during hero
+ return Selector(
+ selector: (context, query) => query?.enabled ?? false,
+ builder: (context, queryEnabled, child) {
+ return TitleSearchToggler(
+ queryEnabled: queryEnabled,
+ onPressed: onPressed,
+ );
+ },
+ );
+ default:
+ return IconButton(
+ icon: action.getIcon(),
+ onPressed: onPressed,
+ tooltip: action.getText(context),
+ );
+ }
}
void _onActivityChange() {
@@ -217,7 +302,13 @@ class _FilterGridAppBarState extends State _queryBarFocusNode.requestFocus();
+
+ void _updateAppBarHeight() {
+ widget.appBarHeightNotifier.value = AvesAppBar.appBarHeightForContentHeight(appBarContentHeight);
+ }
+
+ void _onActionSelected(BuildContext context, ChipSetAction action, ChipSetActionDelegate actionDelegate) {
final selection = context.read>>();
final selectedFilters = selection.selectedItems.map((v) => v.filter).toSet();
actionDelegate.onActionSelected(context, selectedFilters, action);
diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart
index 448cd0167..ab4c9a984 100644
--- a/lib/widgets/filter_grids/common/filter_grid_page.dart
+++ b/lib/widgets/filter_grids/common/filter_grid_page.dart
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:aves/app_mode.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/highlight.dart';
+import 'package:aves/model/query.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/enums.dart';
@@ -21,6 +22,7 @@ import 'package:aves/widgets/common/grid/theme.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
+import 'package:aves/widgets/common/providers/query_provider.dart';
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart';
import 'package:aves/widgets/common/tile_extent_controller.dart';
import 'package:aves/widgets/filter_grids/common/covered_filter_chip.dart';
@@ -38,18 +40,17 @@ import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
-typedef QueryTest = Iterable> Function(Iterable> filters, String query);
+typedef QueryTest = List> Function(BuildContext context, List> filters, String query);
class FilterGridPage extends StatelessWidget {
final String? settingsRouteKey;
final Widget appBar;
- final double appBarHeight;
+ final ValueNotifier appBarHeightNotifier;
final Map>> sections;
final Set newFilters;
final ChipSortFactor sortFactor;
final bool showHeaders, selectable;
- final ValueNotifier queryNotifier;
- final QueryTest? applyQuery;
+ final QueryTest applyQuery;
final Widget Function() emptyBuilder;
final HeroType heroType;
final StreamController _draggableScrollBarEventStreamController = StreamController.broadcast();
@@ -58,14 +59,13 @@ class FilterGridPage extends StatelessWidget {
super.key,
this.settingsRouteKey,
required this.appBar,
- required this.appBarHeight,
+ required this.appBarHeightNotifier,
required this.sections,
required this.newFilters,
required this.sortFactor,
required this.showHeaders,
required this.selectable,
- required this.queryNotifier,
- this.applyQuery,
+ required this.applyQuery,
required this.emptyBuilder,
required this.heroType,
});
@@ -84,46 +84,53 @@ class FilterGridPage extends StatelessWidget {
return false;
},
child: Scaffold(
- body: WillPopScope(
- onWillPop: () {
- final selection = context.read>>();
- if (selection.isSelecting) {
- selection.browse();
- return SynchronousFuture(false);
- }
- return SynchronousFuture(true);
- },
- child: DoubleBackPopScope(
- child: GestureAreaProtectorStack(
- child: SafeArea(
- top: false,
- bottom: false,
- child: Selector(
- selector: (context, mq) => mq.padding.top,
- builder: (context, mqPaddingTop, child) {
- return FilterGrid(
- // key is expected by test driver
- key: const Key('filter-grid'),
- settingsRouteKey: settingsRouteKey,
- appBar: appBar,
- appBarHeight: mqPaddingTop + appBarHeight,
- sections: sections,
- newFilters: newFilters,
- sortFactor: sortFactor,
- showHeaders: showHeaders,
- selectable: selectable,
- queryNotifier: queryNotifier,
- applyQuery: applyQuery,
- emptyBuilder: emptyBuilder,
- heroType: heroType,
- );
- },
+ body: QueryProvider(
+ initialQuery: null,
+ child: WillPopScope(
+ onWillPop: () {
+ final selection = context.read>>();
+ if (selection.isSelecting) {
+ selection.browse();
+ return SynchronousFuture(false);
+ }
+ return SynchronousFuture(true);
+ },
+ child: DoubleBackPopScope(
+ child: GestureAreaProtectorStack(
+ child: SafeArea(
+ top: false,
+ bottom: false,
+ child: Selector(
+ selector: (context, mq) => mq.padding.top,
+ builder: (context, mqPaddingTop, child) {
+ return ValueListenableBuilder(
+ valueListenable: appBarHeightNotifier,
+ builder: (context, appBarHeight, child) {
+ return FilterGrid(
+ // key is expected by test driver
+ key: const Key('filter-grid'),
+ settingsRouteKey: settingsRouteKey,
+ appBar: appBar,
+ appBarHeight: mqPaddingTop + appBarHeight,
+ sections: sections,
+ newFilters: newFilters,
+ sortFactor: sortFactor,
+ showHeaders: showHeaders,
+ selectable: selectable,
+ applyQuery: applyQuery,
+ emptyBuilder: emptyBuilder,
+ heroType: heroType,
+ );
+ },
+ );
+ },
+ ),
),
),
),
),
),
- drawer: const AppDrawer(),
+ drawer: canNavigate ? const AppDrawer() : null,
bottomNavigationBar: showBottomNavigationBar
? AppBottomNavBar(
events: _draggableScrollBarEventStreamController.stream,
@@ -147,8 +154,7 @@ class FilterGrid extends StatefulWidget {
final Set newFilters;
final ChipSortFactor sortFactor;
final bool showHeaders, selectable;
- final ValueNotifier queryNotifier;
- final QueryTest? applyQuery;
+ final QueryTest applyQuery;
final Widget Function() emptyBuilder;
final HeroType heroType;
@@ -162,7 +168,6 @@ class FilterGrid extends StatefulWidget {
required this.sortFactor,
required this.showHeaders,
required this.selectable,
- required this.queryNotifier,
required this.applyQuery,
required this.emptyBuilder,
required this.heroType,
@@ -201,7 +206,6 @@ class _FilterGridState extends State>
sortFactor: widget.sortFactor,
showHeaders: widget.showHeaders,
selectable: widget.selectable,
- queryNotifier: widget.queryNotifier,
applyQuery: widget.applyQuery,
emptyBuilder: widget.emptyBuilder,
heroType: widget.heroType,
@@ -216,9 +220,8 @@ class _FilterGridContent extends StatelessWidget {
final Set newFilters;
final ChipSortFactor sortFactor;
final bool showHeaders, selectable;
- final ValueNotifier queryNotifier;
final Widget Function() emptyBuilder;
- final QueryTest? applyQuery;
+ final QueryTest applyQuery;
final HeroType heroType;
final ValueNotifier _appBarHeightNotifier = ValueNotifier(0);
@@ -232,7 +235,6 @@ class _FilterGridContent extends StatelessWidget {
required this.sortFactor,
required this.showHeaders,
required this.selectable,
- required this.queryNotifier,
required this.applyQuery,
required this.emptyBuilder,
required this.heroType,
@@ -244,92 +246,97 @@ class _FilterGridContent extends StatelessWidget {
Widget build(BuildContext context) {
final settingsRouteKey = context.read().settingsRouteKey;
final tileLayout = context.select((s) => s.getTileLayout(settingsRouteKey));
- return ValueListenableBuilder(
- valueListenable: queryNotifier,
- builder: (context, query, child) {
- Map>> visibleSections;
- if (applyQuery == null) {
- visibleSections = sections;
- } else {
- visibleSections = {};
- sections.forEach((sectionKey, sectionFilters) {
- final visibleFilters = applyQuery!(sectionFilters, query);
- if (visibleFilters.isNotEmpty) {
- visibleSections[sectionKey] = visibleFilters.toList();
+ return Selector(
+ selector: (context, query) => query.enabled,
+ builder: (context, queryEnabled, child) {
+ return ValueListenableBuilder(
+ valueListenable: context.select>((query) => query.queryNotifier),
+ builder: (context, query, child) {
+ Map>> visibleSections;
+ if (queryEnabled && query.isNotEmpty) {
+ visibleSections = {};
+ sections.forEach((sectionKey, sectionFilters) {
+ final visibleFilters = applyQuery(context, sectionFilters, query.toUpperCase());
+ if (visibleFilters.isNotEmpty) {
+ visibleSections[sectionKey] = visibleFilters;
+ }
+ });
+ } else {
+ visibleSections = sections;
}
- });
- }
- final sectionedListLayoutProvider = ValueListenableBuilder(
- valueListenable: context.select>((controller) => controller.extentNotifier),
- builder: (context, thumbnailExtent, child) {
- return Selector>(
- selector: (context, c) => Tuple4(c.viewportSize.width, c.columnCount, c.spacing, c.horizontalPadding),
- builder: (context, c, child) {
- final scrollableWidth = c.item1;
- final columnCount = c.item2;
- final tileSpacing = c.item3;
- final horizontalPadding = c.item4;
- // do not listen for animation delay change
- final target = context.read().staggeredAnimationPageTarget;
- final tileAnimationDelay = context.read().getTileAnimationDelay(target);
- return Selector(
- selector: (context, mq) => mq.textScaleFactor,
- builder: (context, textScaleFactor, child) {
- final tileHeight = CoveredFilterChip.tileHeight(
- extent: thumbnailExtent,
- textScaleFactor: textScaleFactor,
- showText: tileLayout == TileLayout.grid,
- );
- return GridTheme(
- extent: thumbnailExtent,
- child: FilterListDetailsTheme(
- extent: thumbnailExtent,
- child: SectionedFilterListLayoutProvider(
- sections: visibleSections,
- showHeaders: showHeaders,
- tileLayout: tileLayout,
- scrollableWidth: scrollableWidth,
- columnCount: columnCount,
- spacing: tileSpacing,
- horizontalPadding: horizontalPadding,
- tileWidth: thumbnailExtent,
- tileHeight: tileHeight,
- tileBuilder: (gridItem) {
- return InteractiveFilterTile(
- gridItem: gridItem,
- chipExtent: thumbnailExtent,
- thumbnailExtent: thumbnailExtent,
+ final sectionedListLayoutProvider = ValueListenableBuilder(
+ valueListenable: context.select>((controller) => controller.extentNotifier),
+ builder: (context, thumbnailExtent, child) {
+ return Selector>(
+ selector: (context, c) => Tuple4(c.viewportSize.width, c.columnCount, c.spacing, c.horizontalPadding),
+ builder: (context, c, child) {
+ final scrollableWidth = c.item1;
+ final columnCount = c.item2;
+ final tileSpacing = c.item3;
+ final horizontalPadding = c.item4;
+ // do not listen for animation delay change
+ final target = context.read().staggeredAnimationPageTarget;
+ final tileAnimationDelay = context.read().getTileAnimationDelay(target);
+ return Selector(
+ selector: (context, mq) => mq.textScaleFactor,
+ builder: (context, textScaleFactor, child) {
+ final tileHeight = CoveredFilterChip.tileHeight(
+ extent: thumbnailExtent,
+ textScaleFactor: textScaleFactor,
+ showText: tileLayout == TileLayout.grid,
+ );
+ return GridTheme(
+ extent: thumbnailExtent,
+ child: FilterListDetailsTheme(
+ extent: thumbnailExtent,
+ child: SectionedFilterListLayoutProvider(
+ sections: visibleSections,
+ showHeaders: showHeaders,
tileLayout: tileLayout,
- banner: _getFilterBanner(context, gridItem.filter),
- heroType: heroType,
- );
- },
- tileAnimationDelay: tileAnimationDelay,
- child: child!,
- ),
- ),
+ scrollableWidth: scrollableWidth,
+ columnCount: columnCount,
+ spacing: tileSpacing,
+ horizontalPadding: horizontalPadding,
+ tileWidth: thumbnailExtent,
+ tileHeight: tileHeight,
+ tileBuilder: (gridItem) {
+ return InteractiveFilterTile(
+ gridItem: gridItem,
+ chipExtent: thumbnailExtent,
+ thumbnailExtent: thumbnailExtent,
+ tileLayout: tileLayout,
+ banner: _getFilterBanner(context, gridItem.filter),
+ heroType: heroType,
+ );
+ },
+ tileAnimationDelay: tileAnimationDelay,
+ child: child!,
+ ),
+ ),
+ );
+ },
+ child: child,
);
},
child: child,
);
},
- child: child,
+ child: _FilterSectionedContent(
+ appBar: appBar,
+ appBarHeightNotifier: _appBarHeightNotifier,
+ visibleSections: visibleSections,
+ sortFactor: sortFactor,
+ selectable: selectable,
+ emptyBuilder: emptyBuilder,
+ bannerBuilder: _getFilterBanner,
+ scrollController: PrimaryScrollController.of(context)!,
+ tileLayout: tileLayout,
+ ),
);
+ return sectionedListLayoutProvider;
},
- child: _FilterSectionedContent(
- appBar: appBar,
- appBarHeightNotifier: _appBarHeightNotifier,
- visibleSections: visibleSections,
- sortFactor: sortFactor,
- selectable: selectable,
- emptyBuilder: emptyBuilder,
- bannerBuilder: _getFilterBanner,
- scrollController: PrimaryScrollController.of(context)!,
- tileLayout: tileLayout,
- ),
);
- return sectionedListLayoutProvider;
},
);
}
@@ -571,15 +578,7 @@ class _FilterScrollView extends StatelessWidget {
return empty
? SliverFillRemaining(
hasScrollBody: false,
- child: Selector(
- selector: (context, mq) => mq.effectiveBottomPadding,
- builder: (context, mqPaddingBottom, child) {
- return Padding(
- padding: EdgeInsets.only(bottom: mqPaddingBottom),
- child: emptyBuilder(),
- );
- },
- ),
+ child: emptyBuilder(),
)
: SectionedListSliver>();
}),
diff --git a/lib/widgets/filter_grids/common/filter_nav_page.dart b/lib/widgets/filter_grids/common/filter_nav_page.dart
index e06f92500..7c68a3b89 100644
--- a/lib/widgets/filter_grids/common/filter_nav_page.dart
+++ b/lib/widgets/filter_grids/common/filter_nav_page.dart
@@ -2,7 +2,6 @@ import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/utils/time_utils.dart';
-import 'package:aves/widgets/common/identity/aves_app_bar.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/providers/selection_provider.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
@@ -11,14 +10,15 @@ import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart';
import 'package:aves/widgets/filter_grids/common/section_keys.dart';
import 'package:flutter/material.dart';
-class FilterNavigationPage extends StatelessWidget {
+class FilterNavigationPage> extends StatefulWidget {
final CollectionSource source;
final String title;
final ChipSortFactor sortFactor;
final bool showHeaders;
- final ChipSetActionDelegate actionDelegate;
+ final CSAD actionDelegate;
final Map>> filterSections;
final Set? newFilters;
+ final QueryTest applyQuery;
final Widget Function() emptyBuilder;
const FilterNavigationPage({
@@ -30,40 +30,12 @@ class FilterNavigationPage extends StatelessWidget {
required this.actionDelegate,
required this.filterSections,
this.newFilters,
+ required this.applyQuery,
required this.emptyBuilder,
});
@override
- Widget build(BuildContext context) {
- return SelectionProvider>(
- child: Builder(
- builder: (context) => FilterGridPage(
- appBar: FilterGridAppBar(
- source: source,
- title: title,
- actionDelegate: actionDelegate,
- isEmpty: filterSections.isEmpty,
- ),
- appBarHeight: AvesAppBar.appBarHeightForContentHeight(kToolbarHeight),
- sections: filterSections,
- newFilters: newFilters ?? {},
- sortFactor: sortFactor,
- showHeaders: showHeaders,
- selectable: true,
- queryNotifier: ValueNotifier(''),
- emptyBuilder: () => ValueListenableBuilder(
- valueListenable: source.stateNotifier,
- builder: (context, sourceState, child) {
- return sourceState != SourceState.loading ? emptyBuilder() : const SizedBox();
- },
- ),
- // do not always enable hero, otherwise unwanted hero gets triggered
- // when using `Show in [...]` action from a chip in the Collection filter bar
- heroType: HeroType.onTap,
- ),
- ),
- );
- }
+ State> createState() => _FilterNavigationPageState();
static int compareFiltersByDate(FilterGridItem a, FilterGridItem b) {
final c = (b.entry?.bestDate ?? epoch).compareTo(a.entry?.bestDate ?? epoch);
@@ -79,7 +51,7 @@ class FilterNavigationPage extends StatelessWidget {
return a.filter.compareTo(b.filter);
}
- static List> sort(ChipSortFactor sortFactor, CollectionSource source, Set filters) {
+ static List> sort>(ChipSortFactor sortFactor, CollectionSource source, Set filters) {
List> toGridItem(CollectionSource source, Set filters) {
return filters
.map((filter) => FilterGridItem(
@@ -107,3 +79,40 @@ class FilterNavigationPage extends StatelessWidget {
return allMapEntries;
}
}
+
+class _FilterNavigationPageState> extends State> {
+ final ValueNotifier _appBarHeightNotifier = ValueNotifier(0);
+
+ @override
+ Widget build(BuildContext context) {
+ return SelectionProvider>(
+ child: Builder(
+ builder: (context) => FilterGridPage(
+ appBar: FilterGridAppBar(
+ source: widget.source,
+ title: widget.title,
+ actionDelegate: widget.actionDelegate,
+ isEmpty: widget.filterSections.isEmpty,
+ appBarHeightNotifier: _appBarHeightNotifier,
+ ),
+ appBarHeightNotifier: _appBarHeightNotifier,
+ sections: widget.filterSections,
+ newFilters: widget.newFilters ?? {},
+ sortFactor: widget.sortFactor,
+ showHeaders: widget.showHeaders,
+ selectable: true,
+ applyQuery: widget.applyQuery,
+ emptyBuilder: () => ValueListenableBuilder(
+ valueListenable: widget.source.stateNotifier,
+ builder: (context, sourceState, child) {
+ return sourceState != SourceState.loading ? widget.emptyBuilder() : const SizedBox();
+ },
+ ),
+ // do not always enable hero, otherwise unwanted hero gets triggered
+ // when using `Show in [...]` action from a chip in the Collection filter bar
+ heroType: HeroType.onTap,
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/filter_grids/common/query_bar.dart b/lib/widgets/filter_grids/common/query_bar.dart
new file mode 100644
index 000000000..6ecffa88c
--- /dev/null
+++ b/lib/widgets/filter_grids/common/query_bar.dart
@@ -0,0 +1,36 @@
+import 'package:aves/model/filters/filters.dart';
+import 'package:aves/model/selection.dart';
+import 'package:aves/widgets/common/basic/query_bar.dart';
+import 'package:aves/widgets/common/extensions/build_context.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+class FilterQueryBar extends StatelessWidget {
+ final ValueNotifier queryNotifier;
+ final FocusNode focusNode;
+
+ static const preferredHeight = kToolbarHeight;
+
+ const FilterQueryBar({
+ super.key,
+ required this.queryNotifier,
+ required this.focusNode,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ height: FilterQueryBar.preferredHeight,
+ alignment: Alignment.topCenter,
+ child: Selector>, bool>(
+ selector: (context, selection) => !selection.isSelecting,
+ builder: (context, editable, child) => QueryBar(
+ queryNotifier: queryNotifier,
+ focusNode: focusNode,
+ hintText: context.l10n.collectionSearchTitlesHintText,
+ editable: editable,
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/filter_grids/countries_page.dart b/lib/widgets/filter_grids/countries_page.dart
index a3da3c50b..f33f824b0 100644
--- a/lib/widgets/filter_grids/countries_page.dart
+++ b/lib/widgets/filter_grids/countries_page.dart
@@ -35,12 +35,13 @@ class CountryListPage extends StatelessWidget {
stream: source.eventBus.on(),
builder: (context, snapshot) {
final gridItems = _getGridItems(source);
- return FilterNavigationPage(
+ return FilterNavigationPage(
source: source,
title: context.l10n.countryPageTitle,
sortFactor: settings.countrySortFactor,
actionDelegate: CountryChipSetActionDelegate(gridItems),
filterSections: _groupToSections(gridItems),
+ applyQuery: applyQuery,
emptyBuilder: () => EmptyContent(
icon: AIcons.location,
text: context.l10n.countryEmpty,
@@ -52,6 +53,10 @@ class CountryListPage extends StatelessWidget {
);
}
+ List> applyQuery(BuildContext context, List> filters, String query) {
+ return filters.where((item) => item.filter.getLabel(context).toUpperCase().contains(query)).toList();
+ }
+
List> _getGridItems(CollectionSource source) {
final filters = source.sortedCountries.map((location) => LocationFilter(LocationLevel.country, location)).toSet();
diff --git a/lib/widgets/filter_grids/tags_page.dart b/lib/widgets/filter_grids/tags_page.dart
index 89091a2ff..5eae618ac 100644
--- a/lib/widgets/filter_grids/tags_page.dart
+++ b/lib/widgets/filter_grids/tags_page.dart
@@ -35,12 +35,13 @@ class TagListPage extends StatelessWidget {
stream: source.eventBus.on(),
builder: (context, snapshot) {
final gridItems = _getGridItems(source);
- return FilterNavigationPage(
+ return FilterNavigationPage(
source: source,
title: context.l10n.tagPageTitle,
sortFactor: settings.tagSortFactor,
actionDelegate: TagChipSetActionDelegate(gridItems),
filterSections: _groupToSections(gridItems),
+ applyQuery: applyQuery,
emptyBuilder: () => EmptyContent(
icon: AIcons.tag,
text: context.l10n.tagEmpty,
@@ -52,6 +53,10 @@ class TagListPage extends StatelessWidget {
);
}
+ List> applyQuery(BuildContext context, List> filters, String query) {
+ return filters.where((item) => item.filter.tag.toUpperCase().contains(query)).toList();
+ }
+
List> _getGridItems(CollectionSource source) {
final filters = source.sortedTags.map(TagFilter.new).toSet();
diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart
index 37cc46737..d3f2df1a2 100644
--- a/lib/widgets/settings/settings_page.dart
+++ b/lib/widgets/settings/settings_page.dart
@@ -9,7 +9,7 @@ import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
-import 'package:aves/widgets/common/app_bar_title.dart';
+import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
diff --git a/lib/widgets/viewer/info/info_app_bar.dart b/lib/widgets/viewer/info/info_app_bar.dart
index 7d3fb8ea0..3290cf091 100644
--- a/lib/widgets/viewer/info/info_app_bar.dart
+++ b/lib/widgets/viewer/info/info_app_bar.dart
@@ -2,10 +2,10 @@ import 'package:aves/model/actions/entry_info_actions.dart';
import 'package:aves/model/entry.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
-import 'package:aves/widgets/common/app_bar_title.dart';
+import 'package:aves/widgets/common/app_bar/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/sliver_app_bar_title.dart';
+import 'package:aves/widgets/common/app_bar/sliver_app_bar_title.dart';
import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart';
import 'package:aves/widgets/viewer/info/info_search.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart';
diff --git a/lib/widgets/viewer/overlay/viewer_buttons.dart b/lib/widgets/viewer/overlay/viewer_buttons.dart
index a66a71f82..aa0767ce6 100644
--- a/lib/widgets/viewer/overlay/viewer_buttons.dart
+++ b/lib/widgets/viewer/overlay/viewer_buttons.dart
@@ -7,7 +7,7 @@ import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/basic/popup_menu_button.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
-import 'package:aves/widgets/common/favourite_toggler.dart';
+import 'package:aves/widgets/common/app_bar/favourite_toggler.dart';
import 'package:aves/widgets/viewer/action/entry_action_delegate.dart';
import 'package:aves/widgets/viewer/multipage/conductor.dart';
import 'package:aves/widgets/viewer/notifications.dart';