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';