#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
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
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 collectionSortFactor = EntrySortFactor.date;
|
||||
static const collectionBrowsingQuickActions = [
|
||||
EntrySetAction.search,
|
||||
EntrySetAction.searchCollection,
|
||||
];
|
||||
static const collectionSelectionQuickActions = [
|
||||
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/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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
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
|
||||
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) {
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
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/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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue