#277 albums/countries/tags: show/hide title filter

This commit is contained in:
Thibault Deckers 2022-08-04 12:34:29 +02:00
parent daedd552e2
commit 5dc6b22fb6
23 changed files with 471 additions and 354 deletions

View file

@ -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
## <a id="v1.6.11"></a>[v1.6.11] - 2022-07-26

View file

@ -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

View file

@ -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<String> _queryNotifier = ValueNotifier('');
final StreamController<bool> _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<bool> _enabledStreamController = StreamController.broadcast();
Stream<bool> get enabledStream => _enabledStreamController.stream;
final AChangeNotifier focusRequestNotifier = AChangeNotifier();
AChangeNotifier get focusRequestNotifier => _focusRequestNotifier;
final ValueNotifier<String> queryNotifier = ValueNotifier('');
ValueNotifier<String> get queryNotifier => _queryNotifier;
}

View file

@ -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<CollectionAppBar> 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<CollectionAppBar> with SingleTickerPr
}
}
List<Widget> _buildActions(Selection<AvesEntry> selection) {
List<Widget> _buildActions(BuildContext context, Selection<AvesEntry> selection) {
final isSelecting = selection.isSelecting;
final selectedItemCount = selection.selectedItems.length;
@ -328,7 +329,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
return Selector<Query?, bool>(
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<CollectionAppBar> with SingleTickerPr
late Widget child;
switch (action) {
case EntrySetAction.toggleTitleSearch:
child = _TitleSearchToggler(
child = TitleSearchToggler(
queryEnabled: context.read<Query>().enabled,
isMenuItem: true,
);
@ -556,30 +557,3 @@ class _CollectionAppBarState extends State<CollectionAppBar> 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,
);
}
}

View file

@ -186,52 +186,51 @@ class _CollectionSectionedContent extends StatefulWidget {
}
class _CollectionSectionedContentState extends State<_CollectionSectionedContent> {
final ValueNotifier<double> _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<double> 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<ValueNotifier<AppMode>, bool>((v) => v.value.canSelectMedia),
items: collection.sortedEntries,
scrollController: scrollController,
appBarHeightNotifier: appBarHeightNotifier,
appBarHeightNotifier: _appBarHeightNotifier,
child: scaler,
);
return GridItemTracker<AvesEntry>(
scrollableKey: scrollableKey,
scrollableKey: _scrollableKey,
tileLayout: tileLayout,
appBarHeightNotifier: appBarHeightNotifier,
appBarHeightNotifier: _appBarHeightNotifier,
scrollController: scrollController,
child: selector,
);

View file

@ -128,7 +128,7 @@ class _CollectionPageState extends State<CollectionPage> {
),
),
floatingActionButton: _buildFab(context, hasSelection),
drawer: AppDrawer(currentCollection: _collection),
drawer: canNavigate ? AppDrawer(currentCollection: _collection) : null,
bottomNavigationBar: showBottomNavigationBar
? AppBottomNavBar(
events: _draggableScrollBarEventStreamController.stream,

View file

@ -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,
);
}
}

View file

@ -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<double> _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<ValueNotifier<AppMode>>.value(
@ -79,24 +93,21 @@ class _AlbumPickPageState extends State<_AlbumPickPage> {
return SelectionProvider<FilterGridItem<AlbumFilter>>(
child: FilterGridPage<AlbumFilter>(
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<String> 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<StatelessWidget> _buildActions(BuildContext context) {
List<Widget> _buildActions(
BuildContext context,
AppMode appMode,
Selection<FilterGridItem<AlbumFilter>> 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<ChipSetAction>(
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<String> 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,
),
);
}
}

View file

@ -43,7 +43,7 @@ class AlbumListPage extends StatelessWidget {
return StreamBuilder<Set<CollectionFilter>?>(
// to update sections by tier
stream: covers.packageChangeStream,
builder: (context, snapshot) => FilterNavigationPage<AlbumFilter>(
builder: (context, snapshot) => FilterNavigationPage<AlbumFilter, AlbumChipSetActionDelegate>(
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<FilterGridItem<AlbumFilter>> applyQuery(BuildContext context, List<FilterGridItem<AlbumFilter>> filters, String query) {
return filters.where((item) => (item.filter.displayName ?? item.filter.album).toUpperCase().contains(query)).toList();
}
static List<FilterGridItem<AlbumFilter>> getAlbumGridItems(BuildContext context, CollectionSource source) {
final filters = source.rawAlbums.map((album) => AlbumFilter(album, source.getAlbumDisplayName(context, album))).toSet();

View file

@ -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<T extends CollectionFilter> 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<T extends CollectionFilter> 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<T extends CollectionFilter> with FeedbackMi
case ChipSetAction.search:
_goToSearch(context);
break;
case ChipSetAction.toggleTitleSearch:
context.read<Query>().toggle();
break;
case ChipSetAction.createAlbum:
break;
// browsing or selecting

View file

@ -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<T extends CollectionFilter> extends StatefulWidget {
typedef ActionsBuilder<T extends CollectionFilter, CSAD extends ChipSetActionDelegate<T>> = List<Widget> Function(
BuildContext context,
AppMode appMode,
Selection<FilterGridItem<T>> selection,
CSAD actionDelegate,
);
class FilterGridAppBar<T extends CollectionFilter, CSAD extends ChipSetActionDelegate<T>> extends StatefulWidget {
final CollectionSource source;
final String title;
final ChipSetActionDelegate<T> actionDelegate;
final CSAD actionDelegate;
final ActionsBuilder<T, CSAD>? actionsBuilder;
final bool isEmpty;
final ValueNotifier<double> appBarHeightNotifier;
const FilterGridAppBar({
super.key,
required this.source,
required this.title,
required this.actionDelegate,
this.actionsBuilder,
required this.isEmpty,
required this.appBarHeightNotifier,
});
@override
State<FilterGridAppBar<T>> createState() => _FilterGridAppBarState<T>();
State<FilterGridAppBar<T, CSAD>> createState() => _FilterGridAppBarState<T, CSAD>();
static PopupMenuItem<ChipSetAction> toMenuItem(BuildContext context, ChipSetAction action, {required bool enabled}) {
late Widget child;
switch (action) {
case ChipSetAction.toggleTitleSearch:
child = TitleSearchToggler(
queryEnabled: context.read<Query>().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<T extends CollectionFilter> extends State<FilterGridAppBar<T>> with SingleTickerProviderStateMixin {
class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetActionDelegate<T>> extends State<FilterGridAppBar<T, CSAD>> with SingleTickerProviderStateMixin {
final List<StreamSubscription> _subscriptions = [];
late AnimationController _browseToSelectAnimation;
final ValueNotifier<bool> _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<T extends CollectionFilter> extends State<FilterGri
@override
void initState() {
super.initState();
final query = context.read<Query>();
_subscriptions.add(query.enabledStream.listen((e) => _updateAppBarHeight()));
_queryFocusRequestNotifier = query.focusRequestNotifier;
_queryFocusRequestNotifier.addListener(_onQueryFocusRequest);
_browseToSelectAnimation = AnimationController(
duration: context.read<DurationsData>().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<T extends CollectionFilter> extends State<FilterGri
final selection = context.watch<Selection<FilterGridItem<T>>>();
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<Query, bool>(
selector: (context, query) => query.enabled,
builder: (context, queryEnabled, child) {
ActionsBuilder<T, CSAD> 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<T>(
queryNotifier: context.select<Query, ValueNotifier<String>>((query) => query.queryNotifier),
focusNode: _queryBarFocusNode,
)
: null,
transitionKey: isSelecting,
);
},
);
}
double get appBarContentHeight {
final hasQuery = context.read<Query>().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<T extends CollectionFilter> extends State<FilterGri
}
}
List<Widget> _buildActions(AppMode appMode, Selection<FilterGridItem<T>> selection) {
List<Widget> _buildActions(
BuildContext context,
AppMode appMode,
Selection<FilterGridItem<T>> selection,
CSAD actionDelegate,
) {
final itemCount = actionDelegate.allItems.length;
final isSelecting = selection.isSelecting;
final selectedItems = selection.selectedItems;
@ -157,7 +226,7 @@ class _FilterGridAppBarState<T extends CollectionFilter> extends State<FilterGri
);
final quickActionButtons = (isSelecting ? selectionQuickActions : browsingQuickActions).where(isVisible).map(
(action) => _toActionButton(action, enabled: canApply(action)),
(action) => _toActionButton(context, actionDelegate, action, enabled: canApply(action)),
);
return [
@ -166,13 +235,13 @@ class _FilterGridAppBarState<T extends CollectionFilter> extends State<FilterGri
child: PopupMenuButton<ChipSetAction>(
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<T extends CollectionFilter> extends State<FilterGri
];
},
onSelected: (action) async {
// remove focus, if any, to prevent the keyboard from showing up
// after the user is done with the popup menu
FocusManager.instance.primaryFocus?.unfocus();
// wait for the popup menu to hide before proceeding with the action
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
_onActionSelected(action);
_onActionSelected(context, action, actionDelegate);
},
),
),
];
}
Widget _toActionButton(ChipSetAction action, {required bool enabled}) {
return IconButton(
icon: action.getIcon(),
onPressed: enabled ? () => _onActionSelected(action) : null,
tooltip: action.getText(context),
);
}
PopupMenuItem<ChipSetAction> _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<Query?, bool>(
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<T extends CollectionFilter> extends State<FilterGri
}
}
void _onActionSelected(ChipSetAction action) {
void _onQueryFocusRequest() => _queryBarFocusNode.requestFocus();
void _updateAppBarHeight() {
widget.appBarHeightNotifier.value = AvesAppBar.appBarHeightForContentHeight(appBarContentHeight);
}
void _onActionSelected(BuildContext context, ChipSetAction action, ChipSetActionDelegate<T> actionDelegate) {
final selection = context.read<Selection<FilterGridItem<T>>>();
final selectedFilters = selection.selectedItems.map((v) => v.filter).toSet();
actionDelegate.onActionSelected(context, selectedFilters, action);

View file

@ -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<T extends CollectionFilter> = Iterable<FilterGridItem<T>> Function(Iterable<FilterGridItem<T>> filters, String query);
typedef QueryTest<T extends CollectionFilter> = List<FilterGridItem<T>> Function(BuildContext context, List<FilterGridItem<T>> filters, String query);
class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
final String? settingsRouteKey;
final Widget appBar;
final double appBarHeight;
final ValueNotifier<double> appBarHeightNotifier;
final Map<ChipSectionKey, List<FilterGridItem<T>>> sections;
final Set<T> newFilters;
final ChipSortFactor sortFactor;
final bool showHeaders, selectable;
final ValueNotifier<String> queryNotifier;
final QueryTest<T>? applyQuery;
final QueryTest<T> applyQuery;
final Widget Function() emptyBuilder;
final HeroType heroType;
final StreamController<DraggableScrollBarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
@ -58,14 +59,13 @@ class FilterGridPage<T extends CollectionFilter> 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<T extends CollectionFilter> extends StatelessWidget {
return false;
},
child: Scaffold(
body: WillPopScope(
onWillPop: () {
final selection = context.read<Selection<FilterGridItem<T>>>();
if (selection.isSelecting) {
selection.browse();
return SynchronousFuture(false);
}
return SynchronousFuture(true);
},
child: DoubleBackPopScope(
child: GestureAreaProtectorStack(
child: SafeArea(
top: false,
bottom: false,
child: Selector<MediaQueryData, double>(
selector: (context, mq) => mq.padding.top,
builder: (context, mqPaddingTop, child) {
return FilterGrid<T>(
// 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<Selection<FilterGridItem<T>>>();
if (selection.isSelecting) {
selection.browse();
return SynchronousFuture(false);
}
return SynchronousFuture(true);
},
child: DoubleBackPopScope(
child: GestureAreaProtectorStack(
child: SafeArea(
top: false,
bottom: false,
child: Selector<MediaQueryData, double>(
selector: (context, mq) => mq.padding.top,
builder: (context, mqPaddingTop, child) {
return ValueListenableBuilder<double>(
valueListenable: appBarHeightNotifier,
builder: (context, appBarHeight, child) {
return FilterGrid<T>(
// 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<T extends CollectionFilter> extends StatefulWidget {
final Set<T> newFilters;
final ChipSortFactor sortFactor;
final bool showHeaders, selectable;
final ValueNotifier<String> queryNotifier;
final QueryTest<T>? applyQuery;
final QueryTest<T> applyQuery;
final Widget Function() emptyBuilder;
final HeroType heroType;
@ -162,7 +168,6 @@ class FilterGrid<T extends CollectionFilter> 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<T extends CollectionFilter> extends State<FilterGrid<T>>
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<T extends CollectionFilter> extends StatelessWidget {
final Set<T> newFilters;
final ChipSortFactor sortFactor;
final bool showHeaders, selectable;
final ValueNotifier<String> queryNotifier;
final Widget Function() emptyBuilder;
final QueryTest<T>? applyQuery;
final QueryTest<T> applyQuery;
final HeroType heroType;
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
@ -232,7 +235,6 @@ class _FilterGridContent<T extends CollectionFilter> 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<T extends CollectionFilter> extends StatelessWidget {
Widget build(BuildContext context) {
final settingsRouteKey = context.read<TileExtentController>().settingsRouteKey;
final tileLayout = context.select<Settings, TileLayout>((s) => s.getTileLayout(settingsRouteKey));
return ValueListenableBuilder<String>(
valueListenable: queryNotifier,
builder: (context, query, child) {
Map<ChipSectionKey, List<FilterGridItem<T>>> 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<Query, bool>(
selector: (context, query) => query.enabled,
builder: (context, queryEnabled, child) {
return ValueListenableBuilder<String>(
valueListenable: context.select<Query, ValueNotifier<String>>((query) => query.queryNotifier),
builder: (context, query, child) {
Map<ChipSectionKey, List<FilterGridItem<T>>> 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<double>(
valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier),
builder: (context, thumbnailExtent, child) {
return Selector<TileExtentController, Tuple4<double, int, double, double>>(
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<DurationsData>().staggeredAnimationPageTarget;
final tileAnimationDelay = context.read<TileExtentController>().getTileAnimationDelay(target);
return Selector<MediaQueryData, double>(
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<T>(
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<double>(
valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier),
builder: (context, thumbnailExtent, child) {
return Selector<TileExtentController, Tuple4<double, int, double, double>>(
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<DurationsData>().staggeredAnimationPageTarget;
final tileAnimationDelay = context.read<TileExtentController>().getTileAnimationDelay(target);
return Selector<MediaQueryData, double>(
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<T>(
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<T>(
appBar: appBar,
appBarHeightNotifier: _appBarHeightNotifier,
visibleSections: visibleSections,
sortFactor: sortFactor,
selectable: selectable,
emptyBuilder: emptyBuilder,
bannerBuilder: _getFilterBanner,
scrollController: PrimaryScrollController.of(context)!,
tileLayout: tileLayout,
),
);
return sectionedListLayoutProvider;
},
child: _FilterSectionedContent<T>(
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<T extends CollectionFilter> extends StatelessWidget {
return empty
? SliverFillRemaining(
hasScrollBody: false,
child: Selector<MediaQueryData, double>(
selector: (context, mq) => mq.effectiveBottomPadding,
builder: (context, mqPaddingBottom, child) {
return Padding(
padding: EdgeInsets.only(bottom: mqPaddingBottom),
child: emptyBuilder(),
);
},
),
child: emptyBuilder(),
)
: SectionedListSliver<FilterGridItem<T>>();
}),

View file

@ -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<T extends CollectionFilter> extends StatelessWidget {
class FilterNavigationPage<T extends CollectionFilter, CSAD extends ChipSetActionDelegate<T>> extends StatefulWidget {
final CollectionSource source;
final String title;
final ChipSortFactor sortFactor;
final bool showHeaders;
final ChipSetActionDelegate<T> actionDelegate;
final CSAD actionDelegate;
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
final Set<T>? newFilters;
final QueryTest<T> applyQuery;
final Widget Function() emptyBuilder;
const FilterNavigationPage({
@ -30,40 +30,12 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
required this.actionDelegate,
required this.filterSections,
this.newFilters,
required this.applyQuery,
required this.emptyBuilder,
});
@override
Widget build(BuildContext context) {
return SelectionProvider<FilterGridItem<T>>(
child: Builder(
builder: (context) => FilterGridPage<T>(
appBar: FilterGridAppBar<T>(
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<SourceState>(
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<FilterNavigationPage<T, CSAD>> createState() => _FilterNavigationPageState<T, CSAD>();
static int compareFiltersByDate(FilterGridItem<CollectionFilter> a, FilterGridItem<CollectionFilter> b) {
final c = (b.entry?.bestDate ?? epoch).compareTo(a.entry?.bestDate ?? epoch);
@ -79,7 +51,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
return a.filter.compareTo(b.filter);
}
static List<FilterGridItem<T>> sort<T extends CollectionFilter>(ChipSortFactor sortFactor, CollectionSource source, Set<T> filters) {
static List<FilterGridItem<T>> sort<T extends CollectionFilter, CSAD extends ChipSetActionDelegate<T>>(ChipSortFactor sortFactor, CollectionSource source, Set<T> filters) {
List<FilterGridItem<T>> toGridItem(CollectionSource source, Set<T> filters) {
return filters
.map((filter) => FilterGridItem(
@ -107,3 +79,40 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
return allMapEntries;
}
}
class _FilterNavigationPageState<T extends CollectionFilter, CSAD extends ChipSetActionDelegate<T>> extends State<FilterNavigationPage<T, CSAD>> {
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
@override
Widget build(BuildContext context) {
return SelectionProvider<FilterGridItem<T>>(
child: Builder(
builder: (context) => FilterGridPage<T>(
appBar: FilterGridAppBar<T, CSAD>(
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<SourceState>(
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,
),
),
);
}
}

View file

@ -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<T extends CollectionFilter> extends StatelessWidget {
final ValueNotifier<String> 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<Selection<FilterGridItem<T>>, bool>(
selector: (context, selection) => !selection.isSelecting,
builder: (context, editable, child) => QueryBar(
queryNotifier: queryNotifier,
focusNode: focusNode,
hintText: context.l10n.collectionSearchTitlesHintText,
editable: editable,
),
),
);
}
}

View file

@ -35,12 +35,13 @@ class CountryListPage extends StatelessWidget {
stream: source.eventBus.on<CountriesChangedEvent>(),
builder: (context, snapshot) {
final gridItems = _getGridItems(source);
return FilterNavigationPage<LocationFilter>(
return FilterNavigationPage<LocationFilter, CountryChipSetActionDelegate>(
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<FilterGridItem<LocationFilter>> applyQuery(BuildContext context, List<FilterGridItem<LocationFilter>> filters, String query) {
return filters.where((item) => item.filter.getLabel(context).toUpperCase().contains(query)).toList();
}
List<FilterGridItem<LocationFilter>> _getGridItems(CollectionSource source) {
final filters = source.sortedCountries.map((location) => LocationFilter(LocationLevel.country, location)).toSet();

View file

@ -35,12 +35,13 @@ class TagListPage extends StatelessWidget {
stream: source.eventBus.on<TagsChangedEvent>(),
builder: (context, snapshot) {
final gridItems = _getGridItems(source);
return FilterNavigationPage<TagFilter>(
return FilterNavigationPage<TagFilter, TagChipSetActionDelegate>(
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<FilterGridItem<TagFilter>> applyQuery(BuildContext context, List<FilterGridItem<TagFilter>> filters, String query) {
return filters.where((item) => item.filter.tag.toUpperCase().contains(query)).toList();
}
List<FilterGridItem<TagFilter>> _getGridItems(CollectionSource source) {
final filters = source.sortedTags.map(TagFilter.new).toSet();

View file

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

View file

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

View file

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