diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index 2c64b5985..aec589bb7 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -87,6 +87,6 @@ class AlbumFilter extends CollectionFilter { @override String toString() { - return 'AlbumFilter{album=$album}'; + return '$runtimeType#${shortHash(this)}{album=$album}'; } } diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart index c82e1573e..97160bf31 100644 --- a/lib/model/filters/location.dart +++ b/lib/model/filters/location.dart @@ -1,6 +1,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/widgets/common/icons.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; class LocationFilter extends CollectionFilter { @@ -45,7 +46,7 @@ class LocationFilter extends CollectionFilter { // as of Flutter v1.22.0-12.1.pre emoji shadows are rendered as colorful duplicates, // not filled with the shadow color as expected, so we remove them if (flag != null) return Text(flag, style: TextStyle(fontSize: size, shadows: [])); - return Icon(AIcons.location, size: size); + return Icon(_location.isEmpty ? AIcons.locationOff : AIcons.location, size: size); } @override @@ -62,7 +63,7 @@ class LocationFilter extends CollectionFilter { @override String toString() { - return 'LocationFilter{level=$level, location=$_location}'; + return '$runtimeType#${shortHash(this)}{level=$level, location=$_location}'; } // U+0041 Latin Capital letter A diff --git a/lib/model/filters/mime.dart b/lib/model/filters/mime.dart index 242843d7a..b1c642d27 100644 --- a/lib/model/filters/mime.dart +++ b/lib/model/filters/mime.dart @@ -1,6 +1,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/widgets/common/icons.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; class MimeFilter extends CollectionFilter { @@ -81,4 +82,9 @@ class MimeFilter extends CollectionFilter { @override int get hashCode => hashValues(type, mime); + + @override + String toString() { + return '$runtimeType#${shortHash(this)}{mime=$mime}'; + } } diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index 00ac31fde..4a07c2671 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -69,4 +69,9 @@ class QueryFilter extends CollectionFilter { @override int get hashCode => hashValues(type, query); + + @override + String toString() { + return '$runtimeType#${shortHash(this)}{query=$query}'; + } } diff --git a/lib/model/filters/tag.dart b/lib/model/filters/tag.dart index 7f2f546ae..30198b8f9 100644 --- a/lib/model/filters/tag.dart +++ b/lib/model/filters/tag.dart @@ -1,6 +1,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/widgets/common/icons.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; class TagFilter extends CollectionFilter { @@ -32,7 +33,7 @@ class TagFilter extends CollectionFilter { String get label => tag.isEmpty ? emptyLabel : tag; @override - Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => showGenericIcon ? Icon(AIcons.tag, size: size) : null; + Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagOff : AIcons.tag, size: size) : null; @override String get typeKey => type; @@ -48,6 +49,6 @@ class TagFilter extends CollectionFilter { @override String toString() { - return 'TagFilter{tag=$tag}'; + return '$runtimeType#${shortHash(this)}{tag=$tag}'; } } diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 9fa81b662..d9d111331 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -55,6 +55,10 @@ class Settings extends ChangeNotifier { // rendering static const svgBackgroundKey = 'svg_background'; + // search + static const saveSearchHistoryKey = 'save_search_history'; + static const searchHistoryKey = 'search_history'; + Future init() async { _prefs = await SharedPreferences.getInstance(); } @@ -179,6 +183,16 @@ class Settings extends ChangeNotifier { set svgBackground(int newValue) => setAndNotify(svgBackgroundKey, newValue); + // search + + bool get saveSearchHistory => getBoolOrDefault(saveSearchHistoryKey, true); + + set saveSearchHistory(bool newValue) => setAndNotify(saveSearchHistoryKey, newValue); + + List get searchHistory => (_prefs.getStringList(searchHistoryKey) ?? []).map(CollectionFilter.fromJson).toList(); + + set searchHistory(List newValue) => setAndNotify(searchHistoryKey, newValue.map((filter) => filter.toJson()).toList()); + // utils // `RoutePredicate` diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index a96334340..81a9b539c 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -160,6 +160,12 @@ class Constants { licenseUrl: 'https://github.com/MikeMitterer/dart-latlong/blob/master/LICENSE', sourceUrl: 'https://github.com/MikeMitterer/dart-latlong', ), + Dependency( + name: 'Material Design Icons Flutter', + license: 'MIT', + licenseUrl: 'https://github.com/ziofat/material_design_icons_flutter/blob/master/LICENSE', + sourceUrl: 'https://github.com/ziofat/material_design_icons_flutter', + ), Dependency( name: 'Overlay Support', license: 'Apache 2.0', diff --git a/lib/widgets/app_debug_page.dart b/lib/widgets/app_debug_page.dart index 362611baf..d96244588 100644 --- a/lib/widgets/app_debug_page.dart +++ b/lib/widgets/app_debug_page.dart @@ -316,6 +316,7 @@ class AppDebugPageState extends State { 'collectionTileExtent': '${settings.collectionTileExtent}', 'infoMapZoom': '${settings.infoMapZoom}', 'pinnedFilters': '${settings.pinnedFilters}', + 'searchHistory': '${settings.searchHistory}', }), ], ); diff --git a/lib/widgets/common/icons.dart b/lib/widgets/common/icons.dart index 9e30990c3..13fd1fa36 100644 --- a/lib/widgets/common/icons.dart +++ b/lib/widgets/common/icons.dart @@ -6,6 +6,7 @@ import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/image_providers/app_icon_image_provider.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; class AIcons { static const IconData allCollection = Icons.collections_outlined; @@ -20,15 +21,17 @@ class AIcons { static const IconData disc = Icons.fiber_manual_record; static const IconData error = Icons.error_outline; static const IconData location = Icons.place_outlined; + static const IconData locationOff = Icons.location_off_outlined; static const IconData raw = Icons.camera_outlined; static const IconData shooting = Icons.camera_outlined; static const IconData removableStorage = Icons.sd_storage_outlined; static const IconData settings = Icons.settings_outlined; static const IconData text = Icons.format_quote_outlined; static const IconData tag = Icons.local_offer_outlined; + static const IconData tagOff = MdiIcons.tagOffOutline; // actions - static const IconData addShortcut = Icons.bookmark_border; + static const IconData addShortcut = Icons.add_to_home_screen_outlined; static const IconData clear = Icons.clear_outlined; static const IconData collapse = Icons.expand_less_outlined; static const IconData createAlbum = Icons.add_circle_outline; diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index 55bacb750..1ba7d3503 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -25,6 +25,8 @@ class ImageSearchDelegate { final ValueNotifier expandedSectionNotifier = ValueNotifier(null); final CollectionLens parentCollection; + static const searchHistoryCount = 10; + ImageSearchDelegate({@required this.source, this.parentCollection}); ThemeData appBarTheme(BuildContext context) { @@ -67,7 +69,8 @@ class ImageSearchDelegate { child: ValueListenableBuilder( valueListenable: expandedSectionNotifier, builder: (context, expandedSection, child) { - var queryFilter = _buildQueryFilter(false); + final queryFilter = _buildQueryFilter(false); + final history = settings.searchHistory; return ListView( padding: EdgeInsets.only(top: 8), children: [ @@ -85,6 +88,12 @@ class ImageSearchDelegate { // but we also need to animate the query chip when it is selected by submitting the search query heroTypeBuilder: (filter) => filter == queryFilter ? HeroType.always : HeroType.onTap, ), + if (upQuery.isEmpty && history.isNotEmpty) + _buildFilterRow( + context: context, + title: 'Recent', + filters: history, + ), StreamBuilder( stream: source.eventBus.on(), builder: (context, snapshot) { @@ -170,6 +179,12 @@ class ImageSearchDelegate { } void _select(BuildContext context, CollectionFilter filter) { + if (settings.saveSearchHistory) { + final history = settings.searchHistory + ..remove(filter) + ..insert(0, filter); + settings.searchHistory = history.take(searchHistoryCount).toList(); + } if (parentCollection != null) { _applyToParentCollectionPage(context, filter); } else { diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index 99aa0eed3..b42f35c8a 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -40,6 +40,7 @@ class _SettingsPageState extends State { _buildDisplaySection(context), _buildThumbnailsSection(context), _buildViewerSection(context), + _buildSearchSection(context), _buildPrivacySection(context), ], ), @@ -171,6 +172,25 @@ class _SettingsPageState extends State { ); } + Widget _buildSearchSection(BuildContext context) { + return AvesExpansionTile( + title: 'Search', + expandedNotifier: _expandedNotifier, + children: [ + SwitchListTile( + value: settings.saveSearchHistory, + onChanged: (v) { + settings.saveSearchHistory = v; + if (!v) { + settings.searchHistory = []; + } + }, + title: Text('Save search history'), + ), + ], + ); + } + Widget _buildPrivacySection(BuildContext context) { return AvesExpansionTile( title: 'Privacy', diff --git a/pubspec.lock b/pubspec.lock index 17a8cd870..ab17ce9bf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -480,6 +480,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.10-nullsafety.1" + material_design_icons_flutter: + dependency: "direct main" + description: + name: material_design_icons_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.5755" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6c2cd36a4..4525a03bf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,6 +65,7 @@ dependencies: google_maps_flutter: intl: latlong: # for flutter_map + material_design_icons_flutter: overlay_support: package_info: palette_generator: