#80 collection: live title filter

This commit is contained in:
Thibault Deckers 2021-11-04 10:44:31 +09:00
parent 2b1ae43b7b
commit f370abf811
18 changed files with 374 additions and 121 deletions

View file

@ -225,23 +225,6 @@ class MediaStoreImageProvider : ImageProvider() {
return found
}
private fun hasEntry(context: Context, contentUri: Uri): Boolean {
var found = false
val projection = arrayOf(MediaStore.MediaColumns._ID)
try {
val cursor = context.contentResolver.query(contentUri, projection, null, null, null)
if (cursor != null) {
while (cursor.moveToNext()) {
found = true
}
cursor.close()
}
} catch (e: Exception) {
Log.e(LOG_TAG, "failed to get entry at contentUri=$contentUri", e)
}
return found
}
private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType
// `uri` is a media URI, not a document URI

View file

@ -520,6 +520,10 @@
}
},
"collectionActionShowTitleSearch": "Show title filter",
"@collectionActionShowTitleSearch": {},
"collectionActionHideTitleSearch": "Hide title filter",
"@collectionActionHideTitleSearch": {},
"collectionActionAddShortcut": "Add shortcut",
"@collectionActionAddShortcut": {},
"collectionActionCopy": "Copy to album",
@ -531,6 +535,9 @@
"collectionActionEdit": "Edit",
"@collectionActionEdit": {},
"collectionSearchTitlesHintText": "Search titles",
"@collectionSearchTitlesHintText": {},
"collectionSortTitle": "Sort",
"@collectionSortTitle": {},
"collectionSortDate": "By date",

View file

@ -10,7 +10,8 @@ enum EntrySetAction {
selectAll,
selectNone,
// browsing
search,
searchCollection,
toggleTitleSearch,
addShortcut,
// browsing or selecting
map,
@ -38,7 +39,8 @@ class EntrySetActions {
];
static const browsing = [
EntrySetAction.search,
EntrySetAction.searchCollection,
EntrySetAction.toggleTitleSearch,
EntrySetAction.addShortcut,
EntrySetAction.map,
EntrySetAction.stats,
@ -71,8 +73,11 @@ extension ExtraEntrySetAction on EntrySetAction {
case EntrySetAction.selectNone:
return context.l10n.menuActionSelectNone;
// browsing
case EntrySetAction.search:
case EntrySetAction.searchCollection:
return MaterialLocalizations.of(context).searchFieldLabel;
case EntrySetAction.toggleTitleSearch:
// different data depending on toggle state
return context.l10n.collectionActionShowTitleSearch;
case EntrySetAction.addShortcut:
return context.l10n.collectionActionAddShortcut;
// browsing or selecting
@ -122,8 +127,11 @@ extension ExtraEntrySetAction on EntrySetAction {
case EntrySetAction.selectNone:
return AIcons.unselected;
// browsing
case EntrySetAction.search:
case EntrySetAction.searchCollection:
return AIcons.search;
case EntrySetAction.toggleTitleSearch:
// different data depending on toggle state
return AIcons.filter;
case EntrySetAction.addShortcut:
return AIcons.addShortcut;
// browsing or selecting

View file

@ -12,15 +12,15 @@ class QueryFilter extends CollectionFilter {
static final RegExp exactRegex = RegExp('^"(.*)"\$');
final String query;
final bool colorful;
final bool colorful, live;
late final EntryFilter _test;
@override
List<Object?> get props => [query];
List<Object?> get props => [query, live];
QueryFilter(this.query, {this.colorful = true}) {
QueryFilter(this.query, {this.colorful = true, this.live = false}) {
var upQuery = query.toUpperCase();
if (upQuery.startsWith('id:')) {
if (upQuery.startsWith('ID:')) {
final id = int.tryParse(upQuery.substring(3));
_test = (entry) => entry.contentId == id;
return;

31
lib/model/query.dart Normal file
View file

@ -0,0 +1,31 @@
import 'dart:async';
import 'package:aves/utils/change_notifier.dart';
import 'package:flutter/foundation.dart';
class Query extends ChangeNotifier {
bool _enabled = false;
bool get enabled => _enabled;
set enabled(bool value) {
_enabled = value;
_enabledStreamController.add(_enabled);
queryNotifier.value = '';
notifyListeners();
if (_enabled) {
focusRequestNotifier.notifyListeners();
}
}
void toggle() => enabled = !enabled;
final StreamController<bool> _enabledStreamController = StreamController<bool>.broadcast();
Stream<bool> get enabledStream => _enabledStreamController.stream;
final AChangeNotifier focusRequestNotifier = AChangeNotifier();
final ValueNotifier<String> queryNotifier = ValueNotifier('');
}

View file

@ -37,7 +37,7 @@ class SettingsDefaults {
static const collectionSectionFactor = EntryGroupFactor.month;
static const collectionSortFactor = EntrySortFactor.date;
static const collectionBrowsingQuickActions = [
EntrySetAction.search,
EntrySetAction.searchCollection,
];
static const collectionSelectionQuickActions = [
EntrySetAction.share,

View file

@ -7,6 +7,7 @@ import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/location.dart';
import 'package:aves/model/filters/query.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/location.dart';
@ -126,6 +127,14 @@ class CollectionLens with ChangeNotifier {
_onFilterChanged();
}
void setLiveQuery(String query) {
filters.removeWhere((v) => v is QueryFilter && v.live);
if (query.isNotEmpty) {
filters.add(QueryFilter(query, live: true));
}
_onFilterChanged();
}
void _onFilterChanged() {
_refresh();
filterChangeNotifier.notifyListeners();

View file

@ -46,6 +46,8 @@ class AIcons {
static const IconData flip = Icons.flip_outlined;
static const IconData favourite = Icons.favorite_border;
static const IconData favouriteActive = Icons.favorite;
static const IconData filter = MdiIcons.filterOutline;
static const IconData filterOff = MdiIcons.filterOffOutline;
static const IconData geoBounds = Icons.public_outlined;
static const IconData goUp = Icons.arrow_upward_outlined;
static const IconData group = Icons.group_work_outlined;

View file

@ -3,6 +3,8 @@ import 'dart:async';
import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_set_actions.dart';
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/query.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';
@ -13,6 +15,7 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.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/basic/menu.dart';
@ -40,20 +43,27 @@ class CollectionAppBar extends StatefulWidget {
}
class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerProviderStateMixin {
final List<StreamSubscription> _subscriptions = [];
final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate();
late AnimationController _browseToSelectAnimation;
late Future<bool> _canAddShortcutsLoader;
final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false);
final FocusNode _queryBarFocusNode = FocusNode();
late final Listenable _queryFocusRequestNotifier;
CollectionLens get collection => widget.collection;
CollectionSource get source => collection.source;
bool get hasFilters => collection.filters.isNotEmpty;
bool get showFilterBar => collection.filters.any((v) => !(v is QueryFilter && v.live));
@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,
@ -74,8 +84,12 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
@override
void dispose() {
_unregisterWidget(widget);
_queryFocusRequestNotifier.removeListener(_onQueryFocusRequest);
_isSelectingNotifier.removeListener(_onActivityChange);
_browseToSelectAnimation.dispose();
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
super.dispose();
}
@ -90,37 +104,53 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
@override
Widget build(BuildContext context) {
final appMode = context.watch<ValueNotifier<AppMode>>().value;
return Selector<Selection<AvesEntry>, Tuple2<bool, int>>(
selector: (context, selection) => Tuple2(selection.isSelecting, selection.selectedItems.length),
builder: (context, s, child) {
final isSelecting = s.item1;
final selectedItemCount = s.item2;
_isSelectingNotifier.value = isSelecting;
return AnimatedBuilder(
animation: collection.filterChangeNotifier,
builder: (context, child) {
final removableFilters = appMode != AppMode.pickInternal;
return FutureBuilder<bool>(
future: _canAddShortcutsLoader,
builder: (context, snapshot) {
final canAddShortcuts = snapshot.data ?? false;
return SliverAppBar(
leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null,
title: _buildAppBarTitle(isSelecting),
actions: _buildActions(
isSelecting: isSelecting,
selectedItemCount: selectedItemCount,
supportShortcuts: canAddShortcuts,
),
bottom: hasFilters
? FilterBar(
filters: collection.filters,
removable: removableFilters,
onTap: removableFilters ? collection.removeFilter : null,
)
: null,
titleSpacing: 0,
floating: true,
return FutureBuilder<bool>(
future: _canAddShortcutsLoader,
builder: (context, snapshot) {
final canAddShortcuts = snapshot.data ?? false;
return Selector<Selection<AvesEntry>, Tuple2<bool, int>>(
selector: (context, selection) => Tuple2(selection.isSelecting, selection.selectedItems.length),
builder: (context, s, child) {
final isSelecting = s.item1;
final selectedItemCount = s.item2;
_isSelectingNotifier.value = isSelecting;
return AnimatedBuilder(
animation: collection.filterChangeNotifier,
builder: (context, child) {
final removableFilters = appMode != AppMode.pickInternal;
return Selector<Query, bool>(
selector: (context, query) => query.enabled,
builder: (context, queryEnabled, child) {
return SliverAppBar(
leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null,
title: _buildAppBarTitle(isSelecting),
actions: _buildActions(
isSelecting: isSelecting,
selectedItemCount: selectedItemCount,
supportShortcuts: canAddShortcuts,
),
bottom: PreferredSize(
preferredSize: Size.fromHeight(appBarBottomHeight),
child: Column(
children: [
if (showFilterBar)
FilterBar(
filters: collection.filters.where((v) => !(v is QueryFilter && v.live)).toSet(),
removable: removableFilters,
onTap: removableFilters ? collection.removeFilter : null,
),
if (queryEnabled)
EntryQueryBar(
queryNotifier: context.select<Query, ValueNotifier<String>>((query) => query.queryNotifier),
focusNode: _queryBarFocusNode,
)
],
),
),
titleSpacing: 0,
floating: true,
);
},
);
},
);
@ -130,6 +160,11 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
);
}
double get appBarBottomHeight {
final hasQuery = context.read<Query>().enabled;
return (showFilterBar ? FilterBar.preferredHeight : .0) + (hasQuery ? EntryQueryBar.preferredHeight : .0);
}
Widget _buildAppBarLeading(bool isSelecting) {
VoidCallback? onPressed;
String? tooltip;
@ -263,20 +298,46 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
Key _getActionKey(EntrySetAction action) => Key('menu-${action.toString().substring('EntrySetAction.'.length)}');
Widget _toActionButton(EntrySetAction action, {required bool enabled}) {
return IconButton(
key: _getActionKey(action),
icon: action.getIcon(),
onPressed: enabled ? () => _onActionSelected(action) : null,
tooltip: action.getText(context),
);
final onPressed = enabled ? () => _onActionSelected(action) : null;
switch (action) {
case EntrySetAction.toggleTitleSearch:
return Selector<Query, bool>(
selector: (context, query) => query.enabled,
builder: (context, queryEnabled, child) {
return _TitleSearchToggler(
queryEnabled: queryEnabled,
onPressed: onPressed,
);
},
);
default:
return IconButton(
key: _getActionKey(action),
icon: action.getIcon(),
onPressed: onPressed,
tooltip: action.getText(context),
);
}
}
PopupMenuItem<EntrySetAction> _toMenuItem(EntrySetAction action, {required bool enabled}) {
late Widget child;
switch (action) {
case EntrySetAction.toggleTitleSearch:
child = _TitleSearchToggler(
queryEnabled: context.read<Query>().enabled,
isMenuItem: true,
);
break;
default:
child = MenuRow(text: action.getText(context), icon: action.getIcon());
break;
}
return PopupMenuItem(
key: _getActionKey(action),
value: action,
enabled: enabled,
child: MenuRow(text: action.getText(context), icon: action.getIcon()),
child: child,
);
}
@ -327,10 +388,10 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
}
void _onFilterChanged() {
widget.appBarHeightNotifier.value = kToolbarHeight + (hasFilters ? FilterBar.preferredHeight : 0);
_updateAppBarHeight();
if (hasFilters) {
final filters = collection.filters;
final filters = collection.filters;
if (filters.isNotEmpty) {
final selection = context.read<Selection<AvesEntry>>();
if (selection.isSelecting) {
final toRemove = selection.selectedItems.where((entry) => !filters.every((f) => f.test(entry))).toSet();
@ -339,6 +400,10 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
}
}
void _onQueryFocusRequest() => _queryBarFocusNode.requestFocus();
void _updateAppBarHeight() => widget.appBarHeightNotifier.value = kToolbarHeight + appBarBottomHeight;
Future<void> _onActionSelected(EntrySetAction action) async {
switch (action) {
// general
@ -358,7 +423,8 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
context.read<Selection<AvesEntry>>().clearSelection();
break;
// browsing
case EntrySetAction.search:
case EntrySetAction.searchCollection:
case EntrySetAction.toggleTitleSearch:
case EntrySetAction.addShortcut:
// browsing or selecting
case EntrySetAction.map:
@ -435,3 +501,30 @@ 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

@ -5,6 +5,7 @@ import 'package:aves/widgets/collection/collection_grid.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/behaviour/double_back_pop.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/selection_provider.dart';
import 'package:aves/widgets/drawer/app_drawer.dart';
import 'package:flutter/foundation.dart';
@ -39,25 +40,27 @@ class _CollectionPageState extends State<CollectionPage> {
return MediaQueryDataProvider(
child: Scaffold(
body: SelectionProvider<AvesEntry>(
child: Builder(
builder: (context) => WillPopScope(
onWillPop: () {
final selection = context.read<Selection<AvesEntry>>();
if (selection.isSelecting) {
selection.browse();
return SynchronousFuture(false);
}
return SynchronousFuture(true);
},
child: DoubleBackPopScope(
child: GestureAreaProtectorStack(
child: SafeArea(
bottom: false,
child: ChangeNotifierProvider<CollectionLens>.value(
value: collection,
child: const CollectionGrid(
// key is expected by test driver
key: Key('collection-grid'),
child: QueryProvider(
child: Builder(
builder: (context) => WillPopScope(
onWillPop: () {
final selection = context.read<Selection<AvesEntry>>();
if (selection.isSelecting) {
selection.browse();
return SynchronousFuture(false);
}
return SynchronousFuture(true);
},
child: DoubleBackPopScope(
child: GestureAreaProtectorStack(
child: SafeArea(
bottom: false,
child: ChangeNotifierProvider<CollectionLens>.value(
value: collection,
child: const CollectionGrid(
// key is expected by test driver
key: Key('collection-grid'),
),
),
),
),

View file

@ -8,6 +8,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/album.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/source/analysis_controller.dart';
import 'package:aves/model/source/collection_lens.dart';
@ -60,8 +61,10 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
case EntrySetAction.selectNone:
return isSelecting && selectedItemCount == itemCount;
// browsing
case EntrySetAction.search:
case EntrySetAction.searchCollection:
return appMode.canSearch && !isSelecting;
case EntrySetAction.toggleTitleSearch:
return !isSelecting;
case EntrySetAction.addShortcut:
return appMode == AppMode.main && !isSelecting && supportShortcuts;
// browsing or selecting
@ -102,7 +105,8 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
return selectedItemCount < itemCount;
case EntrySetAction.selectNone:
return hasSelection;
case EntrySetAction.search:
case EntrySetAction.searchCollection:
case EntrySetAction.toggleTitleSearch:
case EntrySetAction.addShortcut:
return true;
case EntrySetAction.map:
@ -133,9 +137,12 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
case EntrySetAction.selectNone:
break;
// browsing
case EntrySetAction.search:
case EntrySetAction.searchCollection:
_goToSearch(context);
break;
case EntrySetAction.toggleTitleSearch:
context.read<Query>().toggle();
break;
case EntrySetAction.addShortcut:
_addShortcut(context);
break;

View file

@ -3,7 +3,7 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:flutter/material.dart';
class FilterBar extends StatefulWidget implements PreferredSizeWidget {
class FilterBar extends StatefulWidget {
static const double verticalPadding = 16;
static const double preferredHeight = AvesFilterChip.minChipHeight + verticalPadding;
@ -19,9 +19,6 @@ class FilterBar extends StatefulWidget implements PreferredSizeWidget {
}) : filters = List<CollectionFilter>.from(filters)..sort(),
super(key: key);
@override
final Size preferredSize = const Size.fromHeight(preferredHeight);
@override
_FilterBarState createState() => _FilterBarState();
}

View file

@ -0,0 +1,76 @@
import 'package:aves/model/entry.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/model/source/collection_lens.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 EntryQueryBar extends StatefulWidget {
final ValueNotifier<String> queryNotifier;
final FocusNode focusNode;
static const preferredHeight = kToolbarHeight;
const EntryQueryBar({
Key? key,
required this.queryNotifier,
required this.focusNode,
}) : super(key: key);
@override
_EntryQueryBarState createState() => _EntryQueryBarState();
}
class _EntryQueryBarState extends State<EntryQueryBar> {
@override
void initState() {
super.initState();
_registerWidget(widget);
}
@override
void didUpdateWidget(covariant EntryQueryBar oldWidget) {
super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@override
void dispose() {
_unregisterWidget(widget);
super.dispose();
}
// TODO TLAD focus on text field when enabled (`autofocus` is unusable)
// TODO TLAD lose focus on navigation to viewer?
void _registerWidget(EntryQueryBar widget) {
widget.queryNotifier.addListener(_onQueryChanged);
}
void _unregisterWidget(EntryQueryBar widget) {
widget.queryNotifier.removeListener(_onQueryChanged);
}
@override
Widget build(BuildContext context) {
return Container(
height: EntryQueryBar.preferredHeight,
alignment: Alignment.topCenter,
child: Selector<Selection<AvesEntry>, bool>(
selector: (context, selection) => !selection.isSelecting,
builder: (context, editable, child) => QueryBar(
queryNotifier: widget.queryNotifier,
focusNode: widget.focusNode,
hintText: context.l10n.collectionSearchTitlesHintText,
editable: editable,
),
),
);
}
void _onQueryChanged() {
final query = widget.queryNotifier.value;
context.read<CollectionLens>().setLiveQuery(query);
}
}

View file

@ -244,7 +244,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
// when the user is not dragging the thumb
if (!_isDragInProcess) {
if (notification is ScrollUpdateNotification) {
_thumbOffsetNotifier.value = (scrollMetrics.pixels / scrollMetrics.maxScrollExtent * thumbMaxScrollExtent).clamp(thumbMinScrollExtent, thumbMaxScrollExtent);
final scrollExtent = (scrollMetrics.pixels / scrollMetrics.maxScrollExtent * thumbMaxScrollExtent);
_thumbOffsetNotifier.value = thumbMaxScrollExtent > thumbMinScrollExtent ? scrollExtent.clamp(thumbMinScrollExtent, thumbMaxScrollExtent) : thumbMinScrollExtent;
}
if (notification is ScrollUpdateNotification || notification is OverscrollNotification) {

View file

@ -7,11 +7,19 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class QueryBar extends StatefulWidget {
final ValueNotifier<String> filterNotifier;
final ValueNotifier<String> queryNotifier;
final FocusNode? focusNode;
final IconData? icon;
final String? hintText;
final bool editable;
const QueryBar({
Key? key,
required this.filterNotifier,
required this.queryNotifier,
this.focusNode,
this.icon,
this.hintText,
this.editable = true,
}) : super(key: key);
@override
@ -22,22 +30,24 @@ class _QueryBarState extends State<QueryBar> {
final Debouncer _debouncer = Debouncer(delay: Durations.searchDebounceDelay);
late TextEditingController _controller;
ValueNotifier<String> get filterNotifier => widget.filterNotifier;
ValueNotifier<String> get queryNotifier => widget.queryNotifier;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: filterNotifier.value);
_controller = TextEditingController(text: queryNotifier.value);
}
@override
Widget build(BuildContext context) {
final clearButton = IconButton(
icon: const Icon(AIcons.clear),
onPressed: () {
_controller.clear();
filterNotifier.value = '';
},
onPressed: widget.editable
? () {
_controller.clear();
queryNotifier.value = '';
}
: null,
tooltip: context.l10n.clearTooltip,
);
@ -47,16 +57,18 @@ class _QueryBarState extends State<QueryBar> {
Expanded(
child: TextField(
controller: _controller,
focusNode: widget.focusNode ?? FocusNode(),
decoration: InputDecoration(
icon: const Padding(
padding: EdgeInsetsDirectional.only(start: 16),
child: Icon(AIcons.search),
icon: Padding(
padding: const EdgeInsetsDirectional.only(start: 16),
child: Icon(widget.icon ?? AIcons.filter),
),
hintText: MaterialLocalizations.of(context).searchFieldLabel,
hintText: widget.hintText ?? MaterialLocalizations.of(context).searchFieldLabel,
hintStyle: Theme.of(context).inputDecorationTheme.hintStyle,
),
textInputAction: TextInputAction.search,
onChanged: (s) => _debouncer(() => filterNotifier.value = s),
onChanged: (s) => _debouncer(() => queryNotifier.value = s.trim()),
enabled: widget.editable,
),
),
ConstrainedBox(
@ -73,7 +85,7 @@ class _QueryBarState extends State<QueryBar> {
child: child,
),
),
child: value.text.isNotEmpty ? clearButton : const SizedBox.shrink(),
child: value.text.isNotEmpty ? clearButton : const SizedBox(),
),
),
)

View file

@ -0,0 +1,20 @@
import 'package:aves/model/query.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class QueryProvider extends StatelessWidget {
final Widget child;
const QueryProvider({
Key? key,
required this.child,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<Query>(
create: (context) => Query(),
child: child,
);
}
}

View file

@ -18,10 +18,8 @@ import 'package:aves/widgets/dialogs/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/filter_grid_page.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
@ -96,7 +94,7 @@ class AlbumPickAppBar extends StatelessWidget {
final AlbumChipSetActionDelegate actionDelegate;
final ValueNotifier<String> queryNotifier;
static const preferredHeight = kToolbarHeight + AlbumFilterBar.preferredHeight;
static const preferredHeight = kToolbarHeight + AlbumQueryBar.preferredHeight;
const AlbumPickAppBar({
Key? key,
@ -127,8 +125,8 @@ class AlbumPickAppBar extends StatelessWidget {
title: Text(title()),
source: source,
),
bottom: AlbumFilterBar(
filterNotifier: queryNotifier,
bottom: AlbumQueryBar(
queryNotifier: queryNotifier,
),
actions: [
if (moveType != null)
@ -176,14 +174,14 @@ class AlbumPickAppBar extends StatelessWidget {
}
}
class AlbumFilterBar extends StatelessWidget implements PreferredSizeWidget {
final ValueNotifier<String> filterNotifier;
class AlbumQueryBar extends StatelessWidget implements PreferredSizeWidget {
final ValueNotifier<String> queryNotifier;
static const preferredHeight = kToolbarHeight;
const AlbumFilterBar({
const AlbumQueryBar({
Key? key,
required this.filterNotifier,
required this.queryNotifier,
}) : super(key: key);
@override
@ -192,10 +190,10 @@ class AlbumFilterBar extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
return Container(
height: AlbumFilterBar.preferredHeight,
height: AlbumQueryBar.preferredHeight,
alignment: Alignment.topCenter,
child: QueryBar(
filterNotifier: filterNotifier,
queryNotifier: queryNotifier,
),
);
}

View file

@ -3,6 +3,9 @@
"unsupportedTypeDialogTitle",
"unsupportedTypeDialogMessage",
"editEntryDateDialogExtractFromTitle",
"collectionActionShowTitleSearch",
"collectionActionHideTitleSearch",
"collectionSearchTitlesHintText",
"settingsCollectionQuickActionTabBrowsing",
"settingsCollectionQuickActionTabSelecting",
"settingsCollectionBrowsingQuickActionEditorBanner",
@ -17,7 +20,10 @@
"aboutLinkPolicy",
"aboutCreditsTranslators",
"policyPageTitle",
"collectionActionShowTitleSearch",
"collectionActionHideTitleSearch",
"collectionActionEdit",
"collectionSearchTitlesHintText",
"collectionEditFailureFeedback",
"collectionEditSuccessFeedback",
"settingsCollectionQuickActionTabBrowsing",