#80 collection: live title filter
This commit is contained in:
parent
2b1ae43b7b
commit
f370abf811
18 changed files with 374 additions and 121 deletions
|
@ -225,23 +225,6 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
return found
|
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
|
private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType
|
||||||
|
|
||||||
// `uri` is a media URI, not a document URI
|
// `uri` is a media URI, not a document URI
|
||||||
|
|
|
@ -520,6 +520,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"collectionActionShowTitleSearch": "Show title filter",
|
||||||
|
"@collectionActionShowTitleSearch": {},
|
||||||
|
"collectionActionHideTitleSearch": "Hide title filter",
|
||||||
|
"@collectionActionHideTitleSearch": {},
|
||||||
"collectionActionAddShortcut": "Add shortcut",
|
"collectionActionAddShortcut": "Add shortcut",
|
||||||
"@collectionActionAddShortcut": {},
|
"@collectionActionAddShortcut": {},
|
||||||
"collectionActionCopy": "Copy to album",
|
"collectionActionCopy": "Copy to album",
|
||||||
|
@ -531,6 +535,9 @@
|
||||||
"collectionActionEdit": "Edit",
|
"collectionActionEdit": "Edit",
|
||||||
"@collectionActionEdit": {},
|
"@collectionActionEdit": {},
|
||||||
|
|
||||||
|
"collectionSearchTitlesHintText": "Search titles",
|
||||||
|
"@collectionSearchTitlesHintText": {},
|
||||||
|
|
||||||
"collectionSortTitle": "Sort",
|
"collectionSortTitle": "Sort",
|
||||||
"@collectionSortTitle": {},
|
"@collectionSortTitle": {},
|
||||||
"collectionSortDate": "By date",
|
"collectionSortDate": "By date",
|
||||||
|
|
|
@ -10,7 +10,8 @@ enum EntrySetAction {
|
||||||
selectAll,
|
selectAll,
|
||||||
selectNone,
|
selectNone,
|
||||||
// browsing
|
// browsing
|
||||||
search,
|
searchCollection,
|
||||||
|
toggleTitleSearch,
|
||||||
addShortcut,
|
addShortcut,
|
||||||
// browsing or selecting
|
// browsing or selecting
|
||||||
map,
|
map,
|
||||||
|
@ -38,7 +39,8 @@ class EntrySetActions {
|
||||||
];
|
];
|
||||||
|
|
||||||
static const browsing = [
|
static const browsing = [
|
||||||
EntrySetAction.search,
|
EntrySetAction.searchCollection,
|
||||||
|
EntrySetAction.toggleTitleSearch,
|
||||||
EntrySetAction.addShortcut,
|
EntrySetAction.addShortcut,
|
||||||
EntrySetAction.map,
|
EntrySetAction.map,
|
||||||
EntrySetAction.stats,
|
EntrySetAction.stats,
|
||||||
|
@ -71,8 +73,11 @@ extension ExtraEntrySetAction on EntrySetAction {
|
||||||
case EntrySetAction.selectNone:
|
case EntrySetAction.selectNone:
|
||||||
return context.l10n.menuActionSelectNone;
|
return context.l10n.menuActionSelectNone;
|
||||||
// browsing
|
// browsing
|
||||||
case EntrySetAction.search:
|
case EntrySetAction.searchCollection:
|
||||||
return MaterialLocalizations.of(context).searchFieldLabel;
|
return MaterialLocalizations.of(context).searchFieldLabel;
|
||||||
|
case EntrySetAction.toggleTitleSearch:
|
||||||
|
// different data depending on toggle state
|
||||||
|
return context.l10n.collectionActionShowTitleSearch;
|
||||||
case EntrySetAction.addShortcut:
|
case EntrySetAction.addShortcut:
|
||||||
return context.l10n.collectionActionAddShortcut;
|
return context.l10n.collectionActionAddShortcut;
|
||||||
// browsing or selecting
|
// browsing or selecting
|
||||||
|
@ -122,8 +127,11 @@ extension ExtraEntrySetAction on EntrySetAction {
|
||||||
case EntrySetAction.selectNone:
|
case EntrySetAction.selectNone:
|
||||||
return AIcons.unselected;
|
return AIcons.unselected;
|
||||||
// browsing
|
// browsing
|
||||||
case EntrySetAction.search:
|
case EntrySetAction.searchCollection:
|
||||||
return AIcons.search;
|
return AIcons.search;
|
||||||
|
case EntrySetAction.toggleTitleSearch:
|
||||||
|
// different data depending on toggle state
|
||||||
|
return AIcons.filter;
|
||||||
case EntrySetAction.addShortcut:
|
case EntrySetAction.addShortcut:
|
||||||
return AIcons.addShortcut;
|
return AIcons.addShortcut;
|
||||||
// browsing or selecting
|
// browsing or selecting
|
||||||
|
|
|
@ -12,15 +12,15 @@ class QueryFilter extends CollectionFilter {
|
||||||
static final RegExp exactRegex = RegExp('^"(.*)"\$');
|
static final RegExp exactRegex = RegExp('^"(.*)"\$');
|
||||||
|
|
||||||
final String query;
|
final String query;
|
||||||
final bool colorful;
|
final bool colorful, live;
|
||||||
late final EntryFilter _test;
|
late final EntryFilter _test;
|
||||||
|
|
||||||
@override
|
@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();
|
var upQuery = query.toUpperCase();
|
||||||
if (upQuery.startsWith('id:')) {
|
if (upQuery.startsWith('ID:')) {
|
||||||
final id = int.tryParse(upQuery.substring(3));
|
final id = int.tryParse(upQuery.substring(3));
|
||||||
_test = (entry) => entry.contentId == id;
|
_test = (entry) => entry.contentId == id;
|
||||||
return;
|
return;
|
||||||
|
|
31
lib/model/query.dart
Normal file
31
lib/model/query.dart
Normal 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('');
|
||||||
|
}
|
|
@ -37,7 +37,7 @@ class SettingsDefaults {
|
||||||
static const collectionSectionFactor = EntryGroupFactor.month;
|
static const collectionSectionFactor = EntryGroupFactor.month;
|
||||||
static const collectionSortFactor = EntrySortFactor.date;
|
static const collectionSortFactor = EntrySortFactor.date;
|
||||||
static const collectionBrowsingQuickActions = [
|
static const collectionBrowsingQuickActions = [
|
||||||
EntrySetAction.search,
|
EntrySetAction.searchCollection,
|
||||||
];
|
];
|
||||||
static const collectionSelectionQuickActions = [
|
static const collectionSelectionQuickActions = [
|
||||||
EntrySetAction.share,
|
EntrySetAction.share,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/favourite.dart';
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/location.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/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/location.dart';
|
import 'package:aves/model/source/location.dart';
|
||||||
|
@ -126,6 +127,14 @@ class CollectionLens with ChangeNotifier {
|
||||||
_onFilterChanged();
|
_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() {
|
void _onFilterChanged() {
|
||||||
_refresh();
|
_refresh();
|
||||||
filterChangeNotifier.notifyListeners();
|
filterChangeNotifier.notifyListeners();
|
||||||
|
|
|
@ -46,6 +46,8 @@ class AIcons {
|
||||||
static const IconData flip = Icons.flip_outlined;
|
static const IconData flip = Icons.flip_outlined;
|
||||||
static const IconData favourite = Icons.favorite_border;
|
static const IconData favourite = Icons.favorite_border;
|
||||||
static const IconData favouriteActive = Icons.favorite;
|
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 geoBounds = Icons.public_outlined;
|
||||||
static const IconData goUp = Icons.arrow_upward_outlined;
|
static const IconData goUp = Icons.arrow_upward_outlined;
|
||||||
static const IconData group = Icons.group_work_outlined;
|
static const IconData group = Icons.group_work_outlined;
|
||||||
|
|
|
@ -3,6 +3,8 @@ import 'dart:async';
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/actions/entry_set_actions.dart';
|
import 'package:aves/model/actions/entry_set_actions.dart';
|
||||||
import 'package:aves/model/entry.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/selection.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.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/theme/icons.dart';
|
||||||
import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
|
import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
|
||||||
import 'package:aves/widgets/collection/filter_bar.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_subtitle.dart';
|
||||||
import 'package:aves/widgets/common/app_bar_title.dart';
|
import 'package:aves/widgets/common/app_bar_title.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/menu.dart';
|
||||||
|
@ -40,20 +43,27 @@ class CollectionAppBar extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerProviderStateMixin {
|
class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerProviderStateMixin {
|
||||||
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate();
|
final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate();
|
||||||
late AnimationController _browseToSelectAnimation;
|
late AnimationController _browseToSelectAnimation;
|
||||||
late Future<bool> _canAddShortcutsLoader;
|
late Future<bool> _canAddShortcutsLoader;
|
||||||
final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false);
|
final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false);
|
||||||
|
final FocusNode _queryBarFocusNode = FocusNode();
|
||||||
|
late final Listenable _queryFocusRequestNotifier;
|
||||||
|
|
||||||
CollectionLens get collection => widget.collection;
|
CollectionLens get collection => widget.collection;
|
||||||
|
|
||||||
CollectionSource get source => collection.source;
|
CollectionSource get source => collection.source;
|
||||||
|
|
||||||
bool get hasFilters => collection.filters.isNotEmpty;
|
bool get showFilterBar => collection.filters.any((v) => !(v is QueryFilter && v.live));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
final query = context.read<Query>();
|
||||||
|
_subscriptions.add(query.enabledStream.listen((e) => _updateAppBarHeight()));
|
||||||
|
_queryFocusRequestNotifier = query.focusRequestNotifier;
|
||||||
|
_queryFocusRequestNotifier.addListener(_onQueryFocusRequest);
|
||||||
_browseToSelectAnimation = AnimationController(
|
_browseToSelectAnimation = AnimationController(
|
||||||
duration: context.read<DurationsData>().iconAnimation,
|
duration: context.read<DurationsData>().iconAnimation,
|
||||||
vsync: this,
|
vsync: this,
|
||||||
|
@ -74,8 +84,12 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_unregisterWidget(widget);
|
_unregisterWidget(widget);
|
||||||
|
_queryFocusRequestNotifier.removeListener(_onQueryFocusRequest);
|
||||||
_isSelectingNotifier.removeListener(_onActivityChange);
|
_isSelectingNotifier.removeListener(_onActivityChange);
|
||||||
_browseToSelectAnimation.dispose();
|
_browseToSelectAnimation.dispose();
|
||||||
|
_subscriptions
|
||||||
|
..forEach((sub) => sub.cancel())
|
||||||
|
..clear();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,37 +104,53 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||||
return Selector<Selection<AvesEntry>, Tuple2<bool, int>>(
|
return FutureBuilder<bool>(
|
||||||
selector: (context, selection) => Tuple2(selection.isSelecting, selection.selectedItems.length),
|
future: _canAddShortcutsLoader,
|
||||||
builder: (context, s, child) {
|
builder: (context, snapshot) {
|
||||||
final isSelecting = s.item1;
|
final canAddShortcuts = snapshot.data ?? false;
|
||||||
final selectedItemCount = s.item2;
|
return Selector<Selection<AvesEntry>, Tuple2<bool, int>>(
|
||||||
_isSelectingNotifier.value = isSelecting;
|
selector: (context, selection) => Tuple2(selection.isSelecting, selection.selectedItems.length),
|
||||||
return AnimatedBuilder(
|
builder: (context, s, child) {
|
||||||
animation: collection.filterChangeNotifier,
|
final isSelecting = s.item1;
|
||||||
builder: (context, child) {
|
final selectedItemCount = s.item2;
|
||||||
final removableFilters = appMode != AppMode.pickInternal;
|
_isSelectingNotifier.value = isSelecting;
|
||||||
return FutureBuilder<bool>(
|
return AnimatedBuilder(
|
||||||
future: _canAddShortcutsLoader,
|
animation: collection.filterChangeNotifier,
|
||||||
builder: (context, snapshot) {
|
builder: (context, child) {
|
||||||
final canAddShortcuts = snapshot.data ?? false;
|
final removableFilters = appMode != AppMode.pickInternal;
|
||||||
return SliverAppBar(
|
return Selector<Query, bool>(
|
||||||
leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null,
|
selector: (context, query) => query.enabled,
|
||||||
title: _buildAppBarTitle(isSelecting),
|
builder: (context, queryEnabled, child) {
|
||||||
actions: _buildActions(
|
return SliverAppBar(
|
||||||
isSelecting: isSelecting,
|
leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null,
|
||||||
selectedItemCount: selectedItemCount,
|
title: _buildAppBarTitle(isSelecting),
|
||||||
supportShortcuts: canAddShortcuts,
|
actions: _buildActions(
|
||||||
),
|
isSelecting: isSelecting,
|
||||||
bottom: hasFilters
|
selectedItemCount: selectedItemCount,
|
||||||
? FilterBar(
|
supportShortcuts: canAddShortcuts,
|
||||||
filters: collection.filters,
|
),
|
||||||
removable: removableFilters,
|
bottom: PreferredSize(
|
||||||
onTap: removableFilters ? collection.removeFilter : null,
|
preferredSize: Size.fromHeight(appBarBottomHeight),
|
||||||
)
|
child: Column(
|
||||||
: null,
|
children: [
|
||||||
titleSpacing: 0,
|
if (showFilterBar)
|
||||||
floating: true,
|
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) {
|
Widget _buildAppBarLeading(bool isSelecting) {
|
||||||
VoidCallback? onPressed;
|
VoidCallback? onPressed;
|
||||||
String? tooltip;
|
String? tooltip;
|
||||||
|
@ -263,20 +298,46 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
Key _getActionKey(EntrySetAction action) => Key('menu-${action.toString().substring('EntrySetAction.'.length)}');
|
Key _getActionKey(EntrySetAction action) => Key('menu-${action.toString().substring('EntrySetAction.'.length)}');
|
||||||
|
|
||||||
Widget _toActionButton(EntrySetAction action, {required bool enabled}) {
|
Widget _toActionButton(EntrySetAction action, {required bool enabled}) {
|
||||||
return IconButton(
|
final onPressed = enabled ? () => _onActionSelected(action) : null;
|
||||||
key: _getActionKey(action),
|
switch (action) {
|
||||||
icon: action.getIcon(),
|
case EntrySetAction.toggleTitleSearch:
|
||||||
onPressed: enabled ? () => _onActionSelected(action) : null,
|
return Selector<Query, bool>(
|
||||||
tooltip: action.getText(context),
|
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}) {
|
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(
|
return PopupMenuItem(
|
||||||
key: _getActionKey(action),
|
key: _getActionKey(action),
|
||||||
value: action,
|
value: action,
|
||||||
enabled: enabled,
|
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() {
|
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>>();
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
if (selection.isSelecting) {
|
if (selection.isSelecting) {
|
||||||
final toRemove = selection.selectedItems.where((entry) => !filters.every((f) => f.test(entry))).toSet();
|
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 {
|
Future<void> _onActionSelected(EntrySetAction action) async {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
// general
|
// general
|
||||||
|
@ -358,7 +423,8 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
context.read<Selection<AvesEntry>>().clearSelection();
|
context.read<Selection<AvesEntry>>().clearSelection();
|
||||||
break;
|
break;
|
||||||
// browsing
|
// browsing
|
||||||
case EntrySetAction.search:
|
case EntrySetAction.searchCollection:
|
||||||
|
case EntrySetAction.toggleTitleSearch:
|
||||||
case EntrySetAction.addShortcut:
|
case EntrySetAction.addShortcut:
|
||||||
// browsing or selecting
|
// browsing or selecting
|
||||||
case EntrySetAction.map:
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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/basic/insets.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/double_back_pop.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/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/common/providers/selection_provider.dart';
|
||||||
import 'package:aves/widgets/drawer/app_drawer.dart';
|
import 'package:aves/widgets/drawer/app_drawer.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
@ -39,25 +40,27 @@ class _CollectionPageState extends State<CollectionPage> {
|
||||||
return MediaQueryDataProvider(
|
return MediaQueryDataProvider(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: SelectionProvider<AvesEntry>(
|
body: SelectionProvider<AvesEntry>(
|
||||||
child: Builder(
|
child: QueryProvider(
|
||||||
builder: (context) => WillPopScope(
|
child: Builder(
|
||||||
onWillPop: () {
|
builder: (context) => WillPopScope(
|
||||||
final selection = context.read<Selection<AvesEntry>>();
|
onWillPop: () {
|
||||||
if (selection.isSelecting) {
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
selection.browse();
|
if (selection.isSelecting) {
|
||||||
return SynchronousFuture(false);
|
selection.browse();
|
||||||
}
|
return SynchronousFuture(false);
|
||||||
return SynchronousFuture(true);
|
}
|
||||||
},
|
return SynchronousFuture(true);
|
||||||
child: DoubleBackPopScope(
|
},
|
||||||
child: GestureAreaProtectorStack(
|
child: DoubleBackPopScope(
|
||||||
child: SafeArea(
|
child: GestureAreaProtectorStack(
|
||||||
bottom: false,
|
child: SafeArea(
|
||||||
child: ChangeNotifierProvider<CollectionLens>.value(
|
bottom: false,
|
||||||
value: collection,
|
child: ChangeNotifierProvider<CollectionLens>.value(
|
||||||
child: const CollectionGrid(
|
value: collection,
|
||||||
// key is expected by test driver
|
child: const CollectionGrid(
|
||||||
key: Key('collection-grid'),
|
// key is expected by test driver
|
||||||
|
key: Key('collection-grid'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -8,6 +8,7 @@ import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.dart';
|
||||||
|
import 'package:aves/model/query.dart';
|
||||||
import 'package:aves/model/selection.dart';
|
import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/model/source/analysis_controller.dart';
|
import 'package:aves/model/source/analysis_controller.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
|
@ -60,8 +61,10 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
case EntrySetAction.selectNone:
|
case EntrySetAction.selectNone:
|
||||||
return isSelecting && selectedItemCount == itemCount;
|
return isSelecting && selectedItemCount == itemCount;
|
||||||
// browsing
|
// browsing
|
||||||
case EntrySetAction.search:
|
case EntrySetAction.searchCollection:
|
||||||
return appMode.canSearch && !isSelecting;
|
return appMode.canSearch && !isSelecting;
|
||||||
|
case EntrySetAction.toggleTitleSearch:
|
||||||
|
return !isSelecting;
|
||||||
case EntrySetAction.addShortcut:
|
case EntrySetAction.addShortcut:
|
||||||
return appMode == AppMode.main && !isSelecting && supportShortcuts;
|
return appMode == AppMode.main && !isSelecting && supportShortcuts;
|
||||||
// browsing or selecting
|
// browsing or selecting
|
||||||
|
@ -102,7 +105,8 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
return selectedItemCount < itemCount;
|
return selectedItemCount < itemCount;
|
||||||
case EntrySetAction.selectNone:
|
case EntrySetAction.selectNone:
|
||||||
return hasSelection;
|
return hasSelection;
|
||||||
case EntrySetAction.search:
|
case EntrySetAction.searchCollection:
|
||||||
|
case EntrySetAction.toggleTitleSearch:
|
||||||
case EntrySetAction.addShortcut:
|
case EntrySetAction.addShortcut:
|
||||||
return true;
|
return true;
|
||||||
case EntrySetAction.map:
|
case EntrySetAction.map:
|
||||||
|
@ -133,9 +137,12 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
case EntrySetAction.selectNone:
|
case EntrySetAction.selectNone:
|
||||||
break;
|
break;
|
||||||
// browsing
|
// browsing
|
||||||
case EntrySetAction.search:
|
case EntrySetAction.searchCollection:
|
||||||
_goToSearch(context);
|
_goToSearch(context);
|
||||||
break;
|
break;
|
||||||
|
case EntrySetAction.toggleTitleSearch:
|
||||||
|
context.read<Query>().toggle();
|
||||||
|
break;
|
||||||
case EntrySetAction.addShortcut:
|
case EntrySetAction.addShortcut:
|
||||||
_addShortcut(context);
|
_addShortcut(context);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class FilterBar extends StatefulWidget implements PreferredSizeWidget {
|
class FilterBar extends StatefulWidget {
|
||||||
static const double verticalPadding = 16;
|
static const double verticalPadding = 16;
|
||||||
static const double preferredHeight = AvesFilterChip.minChipHeight + verticalPadding;
|
static const double preferredHeight = AvesFilterChip.minChipHeight + verticalPadding;
|
||||||
|
|
||||||
|
@ -19,9 +19,6 @@ class FilterBar extends StatefulWidget implements PreferredSizeWidget {
|
||||||
}) : filters = List<CollectionFilter>.from(filters)..sort(),
|
}) : filters = List<CollectionFilter>.from(filters)..sort(),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@override
|
|
||||||
final Size preferredSize = const Size.fromHeight(preferredHeight);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_FilterBarState createState() => _FilterBarState();
|
_FilterBarState createState() => _FilterBarState();
|
||||||
}
|
}
|
||||||
|
|
76
lib/widgets/collection/query_bar.dart
Normal file
76
lib/widgets/collection/query_bar.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -244,7 +244,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
||||||
// when the user is not dragging the thumb
|
// when the user is not dragging the thumb
|
||||||
if (!_isDragInProcess) {
|
if (!_isDragInProcess) {
|
||||||
if (notification is ScrollUpdateNotification) {
|
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) {
|
if (notification is ScrollUpdateNotification || notification is OverscrollNotification) {
|
||||||
|
|
|
@ -7,11 +7,19 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class QueryBar extends StatefulWidget {
|
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({
|
const QueryBar({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.filterNotifier,
|
required this.queryNotifier,
|
||||||
|
this.focusNode,
|
||||||
|
this.icon,
|
||||||
|
this.hintText,
|
||||||
|
this.editable = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -22,22 +30,24 @@ class _QueryBarState extends State<QueryBar> {
|
||||||
final Debouncer _debouncer = Debouncer(delay: Durations.searchDebounceDelay);
|
final Debouncer _debouncer = Debouncer(delay: Durations.searchDebounceDelay);
|
||||||
late TextEditingController _controller;
|
late TextEditingController _controller;
|
||||||
|
|
||||||
ValueNotifier<String> get filterNotifier => widget.filterNotifier;
|
ValueNotifier<String> get queryNotifier => widget.queryNotifier;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = TextEditingController(text: filterNotifier.value);
|
_controller = TextEditingController(text: queryNotifier.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final clearButton = IconButton(
|
final clearButton = IconButton(
|
||||||
icon: const Icon(AIcons.clear),
|
icon: const Icon(AIcons.clear),
|
||||||
onPressed: () {
|
onPressed: widget.editable
|
||||||
_controller.clear();
|
? () {
|
||||||
filterNotifier.value = '';
|
_controller.clear();
|
||||||
},
|
queryNotifier.value = '';
|
||||||
|
}
|
||||||
|
: null,
|
||||||
tooltip: context.l10n.clearTooltip,
|
tooltip: context.l10n.clearTooltip,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -47,16 +57,18 @@ class _QueryBarState extends State<QueryBar> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
|
focusNode: widget.focusNode ?? FocusNode(),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
icon: const Padding(
|
icon: Padding(
|
||||||
padding: EdgeInsetsDirectional.only(start: 16),
|
padding: const EdgeInsetsDirectional.only(start: 16),
|
||||||
child: Icon(AIcons.search),
|
child: Icon(widget.icon ?? AIcons.filter),
|
||||||
),
|
),
|
||||||
hintText: MaterialLocalizations.of(context).searchFieldLabel,
|
hintText: widget.hintText ?? MaterialLocalizations.of(context).searchFieldLabel,
|
||||||
hintStyle: Theme.of(context).inputDecorationTheme.hintStyle,
|
hintStyle: Theme.of(context).inputDecorationTheme.hintStyle,
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.search,
|
textInputAction: TextInputAction.search,
|
||||||
onChanged: (s) => _debouncer(() => filterNotifier.value = s),
|
onChanged: (s) => _debouncer(() => queryNotifier.value = s.trim()),
|
||||||
|
enabled: widget.editable,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
|
@ -73,7 +85,7 @@ class _QueryBarState extends State<QueryBar> {
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: value.text.isNotEmpty ? clearButton : const SizedBox.shrink(),
|
child: value.text.isNotEmpty ? clearButton : const SizedBox(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
20
lib/widgets/common/providers/query_provider.dart
Normal file
20
lib/widgets/common/providers/query_provider.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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/albums_page.dart';
|
||||||
import 'package:aves/widgets/filter_grids/common/action_delegates/album_set.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:aves/widgets/filter_grids/common/filter_grid_page.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
@ -96,7 +94,7 @@ class AlbumPickAppBar extends StatelessWidget {
|
||||||
final AlbumChipSetActionDelegate actionDelegate;
|
final AlbumChipSetActionDelegate actionDelegate;
|
||||||
final ValueNotifier<String> queryNotifier;
|
final ValueNotifier<String> queryNotifier;
|
||||||
|
|
||||||
static const preferredHeight = kToolbarHeight + AlbumFilterBar.preferredHeight;
|
static const preferredHeight = kToolbarHeight + AlbumQueryBar.preferredHeight;
|
||||||
|
|
||||||
const AlbumPickAppBar({
|
const AlbumPickAppBar({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
@ -127,8 +125,8 @@ class AlbumPickAppBar extends StatelessWidget {
|
||||||
title: Text(title()),
|
title: Text(title()),
|
||||||
source: source,
|
source: source,
|
||||||
),
|
),
|
||||||
bottom: AlbumFilterBar(
|
bottom: AlbumQueryBar(
|
||||||
filterNotifier: queryNotifier,
|
queryNotifier: queryNotifier,
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (moveType != null)
|
if (moveType != null)
|
||||||
|
@ -176,14 +174,14 @@ class AlbumPickAppBar extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AlbumFilterBar extends StatelessWidget implements PreferredSizeWidget {
|
class AlbumQueryBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
final ValueNotifier<String> filterNotifier;
|
final ValueNotifier<String> queryNotifier;
|
||||||
|
|
||||||
static const preferredHeight = kToolbarHeight;
|
static const preferredHeight = kToolbarHeight;
|
||||||
|
|
||||||
const AlbumFilterBar({
|
const AlbumQueryBar({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.filterNotifier,
|
required this.queryNotifier,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -192,10 +190,10 @@ class AlbumFilterBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
height: AlbumFilterBar.preferredHeight,
|
height: AlbumQueryBar.preferredHeight,
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: QueryBar(
|
child: QueryBar(
|
||||||
filterNotifier: filterNotifier,
|
queryNotifier: queryNotifier,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
"unsupportedTypeDialogTitle",
|
"unsupportedTypeDialogTitle",
|
||||||
"unsupportedTypeDialogMessage",
|
"unsupportedTypeDialogMessage",
|
||||||
"editEntryDateDialogExtractFromTitle",
|
"editEntryDateDialogExtractFromTitle",
|
||||||
|
"collectionActionShowTitleSearch",
|
||||||
|
"collectionActionHideTitleSearch",
|
||||||
|
"collectionSearchTitlesHintText",
|
||||||
"settingsCollectionQuickActionTabBrowsing",
|
"settingsCollectionQuickActionTabBrowsing",
|
||||||
"settingsCollectionQuickActionTabSelecting",
|
"settingsCollectionQuickActionTabSelecting",
|
||||||
"settingsCollectionBrowsingQuickActionEditorBanner",
|
"settingsCollectionBrowsingQuickActionEditorBanner",
|
||||||
|
@ -17,7 +20,10 @@
|
||||||
"aboutLinkPolicy",
|
"aboutLinkPolicy",
|
||||||
"aboutCreditsTranslators",
|
"aboutCreditsTranslators",
|
||||||
"policyPageTitle",
|
"policyPageTitle",
|
||||||
|
"collectionActionShowTitleSearch",
|
||||||
|
"collectionActionHideTitleSearch",
|
||||||
"collectionActionEdit",
|
"collectionActionEdit",
|
||||||
|
"collectionSearchTitlesHintText",
|
||||||
"collectionEditFailureFeedback",
|
"collectionEditFailureFeedback",
|
||||||
"collectionEditSuccessFeedback",
|
"collectionEditSuccessFeedback",
|
||||||
"settingsCollectionQuickActionTabBrowsing",
|
"settingsCollectionQuickActionTabBrowsing",
|
||||||
|
|
Loading…
Reference in a new issue