diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d21d10c9..6cc0bdafd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ### Added - mosaic layout +- reverse filters to filter out/in - Albums: group by content type - Stats: top albums - Stats: open full top listings diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8444c25c8..7cc4d0918 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -69,6 +69,8 @@ "chipActionGoToAlbumPage": "Show in Albums", "chipActionGoToCountryPage": "Show in Countries", "chipActionGoToTagPage": "Show in Tags", + "chipActionFilterOut": "Filter out", + "chipActionFilterIn": "Filter in", "chipActionHide": "Hide", "chipActionPin": "Pin to top", "chipActionUnpin": "Unpin from top", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 3d9ee6a81..4f90b5d4f 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -41,6 +41,8 @@ "chipActionGoToAlbumPage": "Afficher dans Albums", "chipActionGoToCountryPage": "Afficher dans Pays", "chipActionGoToTagPage": "Afficher dans Libellés", + "chipActionFilterOut": "Exclure", + "chipActionFilterIn": "Inclure", "chipActionHide": "Masquer", "chipActionPin": "Épingler", "chipActionUnpin": "Retirer", @@ -173,6 +175,9 @@ "wallpaperTargetLock": "Écran de verrouillage", "wallpaperTargetHomeLock": "Écrans accueil et verrouillage", + "widgetOpenPageHome": "Ouvrir la page d’accueil", + "widgetOpenPageViewer": "Ouvrir la visionneuse", + "albumTierNew": "Nouveaux", "albumTierPinned": "Épinglés", "albumTierSpecial": "Standards", @@ -403,9 +408,12 @@ "sortOrderSmallestFirst": "Moins larges d’abord", "albumGroupTier": "par importance", + "albumGroupType": "par type", "albumGroupVolume": "par volume de stockage", "albumGroupNone": "ne pas grouper", + "albumMimeTypeMixed": "Mixte", + "albumPickPageTitleCopy": "Copie", "albumPickPageTitleExport": "Export", "albumPickPageTitleMove": "Déplacement", @@ -614,6 +622,7 @@ "settingsWidgetPageTitle": "Cadre photo", "settingsWidgetShowOutline": "Contours", + "settingsWidgetOpenPage": "Quand vous appuyez sur le widget", "settingsCollectionTile": "Collection", @@ -622,6 +631,7 @@ "statsTopCountriesSectionTitle": "Top pays", "statsTopPlacesSectionTitle": "Top lieux", "statsTopTagsSectionTitle": "Top libellés", + "statsTopAlbumsSectionTitle": "Top albums", "viewerOpenPanoramaButtonLabel": "OUVRIR LE PANORAMA", "viewerSetWallpaperButtonLabel": "APPLIQUER", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 4a1971957..2772a3673 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -41,6 +41,8 @@ "chipActionGoToAlbumPage": "앨범 페이지에서 보기", "chipActionGoToCountryPage": "국가 페이지에서 보기", "chipActionGoToTagPage": "태그 페이지에서 보기", + "chipActionFilterOut": "제외하기", + "chipActionFilterIn": "포함시키기", "chipActionHide": "숨기기", "chipActionPin": "고정", "chipActionUnpin": "고정 해제", @@ -173,6 +175,9 @@ "wallpaperTargetLock": "잠금화면", "wallpaperTargetHomeLock": "홈 및 잠금화면", + "widgetOpenPageHome": "홈 열기", + "widgetOpenPageViewer": "뷰어 열기", + "albumTierNew": "신규", "albumTierPinned": "고정", "albumTierSpecial": "기본", @@ -403,9 +408,12 @@ "sortOrderSmallestFirst": "작은 파일순", "albumGroupTier": "단계별로", + "albumGroupType": "유형별로", "albumGroupVolume": "저장공간별로", "albumGroupNone": "묶음 없음", + "albumMimeTypeMixed": "혼합", + "albumPickPageTitleCopy": "앨범으로 복사", "albumPickPageTitleExport": "앨범으로 내보내기", "albumPickPageTitleMove": "앨범으로 이동", @@ -614,6 +622,7 @@ "settingsWidgetPageTitle": "사진 액자", "settingsWidgetShowOutline": "윤곽", + "settingsWidgetOpenPage": "위젯을 탭하면", "settingsCollectionTile": "미디어", @@ -622,6 +631,7 @@ "statsTopCountriesSectionTitle": "국가 랭킹", "statsTopPlacesSectionTitle": "장소 랭킹", "statsTopTagsSectionTitle": "태그 랭킹", + "statsTopAlbumsSectionTitle": "앨범 랭킹", "viewerOpenPanoramaButtonLabel": "파노라마 열기", "viewerSetWallpaperButtonLabel": "설정", diff --git a/lib/model/actions/chip_actions.dart b/lib/model/actions/chip_actions.dart index efae273d7..28b596579 100644 --- a/lib/model/actions/chip_actions.dart +++ b/lib/model/actions/chip_actions.dart @@ -6,6 +6,7 @@ enum ChipAction { goToAlbumPage, goToCountryPage, goToTagPage, + reverse, hide, } @@ -18,6 +19,9 @@ extension ExtraChipAction on ChipAction { return context.l10n.chipActionGoToCountryPage; case ChipAction.goToTagPage: return context.l10n.chipActionGoToTagPage; + case ChipAction.reverse: + // different data depending on state + return context.l10n.chipActionFilterOut; case ChipAction.hide: return context.l10n.chipActionHide; } @@ -33,6 +37,8 @@ extension ExtraChipAction on ChipAction { return AIcons.location; case ChipAction.goToTagPage: return AIcons.tag; + case ChipAction.reverse: + return AIcons.reverse; case ChipAction.hide: return AIcons.hide; } diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index 431f52e18..f4b01222f 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -14,16 +14,20 @@ class AlbumFilter extends CoveredCollectionFilter { final String album; final String? displayName; + late final EntryFilter _test; @override - List get props => [album]; + List get props => [album, reversed]; - const AlbumFilter(this.album, this.displayName); + AlbumFilter(this.album, this.displayName, {super.reversed = false}) { + _test = (entry) => entry.directory == album; + } factory AlbumFilter.fromMap(Map json) { return AlbumFilter( json['album'], json['uniqueName'], + reversed: json['reversed'] ?? false, ); } @@ -32,10 +36,14 @@ class AlbumFilter extends CoveredCollectionFilter { 'type': type, 'album': album, 'uniqueName': displayName, + 'reversed': reversed, }; @override - EntryFilter get test => (entry) => entry.directory == album; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => true; @override String get universalLabel => displayName ?? pContext.split(album).last; diff --git a/lib/model/filters/coordinate.dart b/lib/model/filters/coordinate.dart index f08edc62e..20dff71da 100644 --- a/lib/model/filters/coordinate.dart +++ b/lib/model/filters/coordinate.dart @@ -17,16 +17,20 @@ class CoordinateFilter extends CollectionFilter { final LatLng sw; final LatLng ne; final bool minuteSecondPadding; + late final EntryFilter _test; @override - List get props => [sw, ne]; + List get props => [sw, ne, reversed]; - const CoordinateFilter(this.sw, this.ne, {this.minuteSecondPadding = false}); + CoordinateFilter(this.sw, this.ne, {this.minuteSecondPadding = false, super.reversed = false}) { + _test = (entry) => GeoUtils.contains(sw, ne, entry.latLng); + } factory CoordinateFilter.fromMap(Map json) { return CoordinateFilter( LatLng.fromJson(json['sw']), LatLng.fromJson(json['ne']), + reversed: json['reversed'] ?? false, ); } @@ -35,10 +39,11 @@ class CoordinateFilter extends CollectionFilter { 'type': type, 'sw': sw.toJson(), 'ne': ne.toJson(), + 'reversed': reversed, }; @override - EntryFilter get test => (entry) => GeoUtils.contains(sw, ne, entry.latLng); + EntryFilter get positiveTest => _test; String _formatBounds(AppLocalizations l10n, CoordinateFormat format) { String s(LatLng latLng) => format.format( @@ -50,6 +55,9 @@ class CoordinateFilter extends CollectionFilter { return '${s(ne)}\n${s(sw)}'; } + @override + bool get exclusiveProp => false; + @override String get universalLabel => _formatBounds(lookupAppLocalizations(AppLocalizations.supportedLocales.first), CoordinateFormat.decimal); diff --git a/lib/model/filters/date.dart b/lib/model/filters/date.dart index 6262bd8c5..c56876881 100644 --- a/lib/model/filters/date.dart +++ b/lib/model/filters/date.dart @@ -18,9 +18,9 @@ class DateFilter extends CollectionFilter { static final onThisDay = DateFilter(DateLevel.md, null); @override - List get props => [level, date]; + List get props => [level, date, reversed]; - DateFilter(this.level, this.date) { + DateFilter(this.level, this.date, {super.reversed = false}) { _effectiveDate = date ?? DateTime.now(); switch (level) { case DateLevel.y: @@ -56,6 +56,7 @@ class DateFilter extends CollectionFilter { return DateFilter( DateLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? DateLevel.ymd, dateString != null ? DateTime.tryParse(dateString) : null, + reversed: json['reversed'] ?? false, ); } @@ -64,15 +65,20 @@ class DateFilter extends CollectionFilter { 'type': type, 'level': level.toString(), 'date': date?.toIso8601String(), + 'reversed': reversed, }; @override - EntryFilter get test => _test; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => true; @override bool isCompatible(CollectionFilter other) { if (other is DateFilter) { - return isCompatibleLevel(level, other.level); + if (reversed != other.reversed && this == other.reverse()) return false; + return reversed || other.reversed || isCompatibleLevel(level, other.level); } else { return true; } diff --git a/lib/model/filters/favourite.dart b/lib/model/filters/favourite.dart index 35ee91b65..197fb21d1 100644 --- a/lib/model/filters/favourite.dart +++ b/lib/model/filters/favourite.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; @@ -9,20 +10,32 @@ import 'package:provider/provider.dart'; class FavouriteFilter extends CollectionFilter { static const type = 'favourite'; + static bool _test(AvesEntry entry) => entry.isFavourite; + static const instance = FavouriteFilter._private(); + static const instanceReversed = FavouriteFilter._private(reversed: true); @override - List get props => []; + List get props => [reversed]; - const FavouriteFilter._private(); + const FavouriteFilter._private({super.reversed = false}); + + factory FavouriteFilter.fromMap(Map json) { + final reversed = json['reversed'] ?? false; + return reversed ? instanceReversed : instance; + } @override Map toMap() => { 'type': type, + 'reversed': reversed, }; @override - EntryFilter get test => (entry) => entry.isFavourite; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => false; @override String get universalLabel => type; diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart index a57b0cce7..930dd2da2 100644 --- a/lib/model/filters/filters.dart +++ b/lib/model/filters/filters.dart @@ -42,9 +42,44 @@ abstract class CollectionFilter extends Equatable implements Comparable jsonMap) { + final type = jsonMap['type']; + switch (type) { + case AlbumFilter.type: + return AlbumFilter.fromMap(jsonMap); + case CoordinateFilter.type: + return CoordinateFilter.fromMap(jsonMap); + case DateFilter.type: + return DateFilter.fromMap(jsonMap); + case FavouriteFilter.type: + return FavouriteFilter.fromMap(jsonMap); + case LocationFilter.type: + return LocationFilter.fromMap(jsonMap); + case MimeFilter.type: + return MimeFilter.fromMap(jsonMap); + case MissingFilter.type: + return MissingFilter.fromMap(jsonMap); + case PathFilter.type: + return PathFilter.fromMap(jsonMap); + case QueryFilter.type: + return QueryFilter.fromMap(jsonMap); + case RatingFilter.type: + return RatingFilter.fromMap(jsonMap); + case RecentlyAddedFilter.type: + return RecentlyAddedFilter.fromMap(jsonMap); + case TagFilter.type: + return TagFilter.fromMap(jsonMap); + case TypeFilter.type: + return TypeFilter.fromMap(jsonMap); + case TrashFilter.type: + return TrashFilter.fromMap(jsonMap); + } + return null; + } static CollectionFilter? fromJson(String jsonString) { if (jsonString.isEmpty) return null; @@ -52,37 +87,7 @@ abstract class CollectionFilter extends Equatable implements Comparable) { - final type = jsonMap['type']; - switch (type) { - case AlbumFilter.type: - return AlbumFilter.fromMap(jsonMap); - case CoordinateFilter.type: - return CoordinateFilter.fromMap(jsonMap); - case DateFilter.type: - return DateFilter.fromMap(jsonMap); - case FavouriteFilter.type: - return FavouriteFilter.instance; - case LocationFilter.type: - return LocationFilter.fromMap(jsonMap); - case MimeFilter.type: - return MimeFilter.fromMap(jsonMap); - case MissingFilter.type: - return MissingFilter.fromMap(jsonMap); - case PathFilter.type: - return PathFilter.fromMap(jsonMap); - case QueryFilter.type: - return QueryFilter.fromMap(jsonMap); - case RatingFilter.type: - return RatingFilter.fromMap(jsonMap); - case RecentlyAddedFilter.type: - return RecentlyAddedFilter.instance; - case TagFilter.type: - return TagFilter.fromMap(jsonMap); - case TypeFilter.type: - return TypeFilter.fromMap(jsonMap); - case TrashFilter.type: - return TrashFilter.instance; - } + return _fromMap(jsonMap); } } catch (error, stack) { debugPrint('failed to parse filter from json=$jsonString error=$error\n$stack'); @@ -95,9 +100,21 @@ abstract class CollectionFilter extends Equatable implements Comparable jsonEncode(toMap()); - EntryFilter get test; + EntryFilter get positiveTest; - bool isCompatible(CollectionFilter other) => category != other.category; + EntryFilter get test => reversed ? (v) => !positiveTest(v) : positiveTest; + + CollectionFilter reverse() => _fromMap(toMap()..['reversed'] = !reversed)!; + + bool get exclusiveProp; + + bool isCompatible(CollectionFilter other) { + if (category != other.category) return true; + if (!reversed && !other.reversed) return !exclusiveProp; + if (reversed && other.reversed) return true; + if (this == other.reverse()) return false; + return true; + } String get universalLabel; @@ -129,7 +146,7 @@ abstract class CollectionFilter extends Equatable implements Comparable color(BuildContext context) { diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart index 9b219c806..3fad0a7a3 100644 --- a/lib/model/filters/location.dart +++ b/lib/model/filters/location.dart @@ -15,9 +15,9 @@ class LocationFilter extends CoveredCollectionFilter { late final EntryFilter _test; @override - List get props => [level, _location, _countryCode]; + List get props => [level, _location, _countryCode, reversed]; - LocationFilter(this.level, String location) { + LocationFilter(this.level, String location, {super.reversed = false}) { final split = location.split(locationSeparator); _location = split.isNotEmpty ? split[0] : location; _countryCode = split.length > 1 ? split[1] : null; @@ -35,6 +35,7 @@ class LocationFilter extends CoveredCollectionFilter { return LocationFilter( LocationLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? LocationLevel.place, json['location'], + reversed: json['reversed'] ?? false, ); } @@ -43,6 +44,7 @@ class LocationFilter extends CoveredCollectionFilter { 'type': type, 'level': level.toString(), 'location': _countryCode != null ? countryNameAndCode : _location, + 'reversed': reversed, }; String get countryNameAndCode => '$_location$locationSeparator$_countryCode'; @@ -50,7 +52,10 @@ class LocationFilter extends CoveredCollectionFilter { String? get countryCode => _countryCode; @override - EntryFilter get test => _test; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => true; @override String get universalLabel => _location; diff --git a/lib/model/filters/mime.dart b/lib/model/filters/mime.dart index 98fb3f9de..bceaadc09 100644 --- a/lib/model/filters/mime.dart +++ b/lib/model/filters/mime.dart @@ -14,17 +14,17 @@ class MimeFilter extends CollectionFilter { static const type = 'mime'; final String mime; - late final EntryFilter _test; late final String _label; late final IconData _icon; + late final EntryFilter _test; static final image = MimeFilter(MimeTypes.anyImage); static final video = MimeFilter(MimeTypes.anyVideo); @override - List get props => [mime]; + List get props => [mime, reversed]; - MimeFilter(this.mime) { + MimeFilter(this.mime, {super.reversed = false}) { IconData? icon; var lowMime = mime.toLowerCase(); if (lowMime.endsWith('/*')) { @@ -46,6 +46,7 @@ class MimeFilter extends CollectionFilter { factory MimeFilter.fromMap(Map json) { return MimeFilter( json['mime'], + reversed: json['reversed'] ?? false, ); } @@ -53,10 +54,14 @@ class MimeFilter extends CollectionFilter { Map toMap() => { 'type': type, 'mime': mime, + 'reversed': reversed, }; @override - EntryFilter get test => _test; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => true; @override String get universalLabel => _label; diff --git a/lib/model/filters/missing.dart b/lib/model/filters/missing.dart index 119355e3e..03c80646d 100644 --- a/lib/model/filters/missing.dart +++ b/lib/model/filters/missing.dart @@ -10,16 +10,16 @@ class MissingFilter extends CollectionFilter { static const _title = 'title'; final String metadataType; - late final EntryFilter _test; late final IconData _icon; + late final EntryFilter _test; static final date = MissingFilter._private(_date); static final title = MissingFilter._private(_title); @override - List get props => [metadataType]; + List get props => [metadataType, reversed]; - MissingFilter._private(this.metadataType) { + MissingFilter._private(this.metadataType, {super.reversed = false}) { switch (metadataType) { case _date: _test = (entry) => (entry.catalogMetadata?.dateMillis ?? 0) == 0; @@ -35,6 +35,7 @@ class MissingFilter extends CollectionFilter { factory MissingFilter.fromMap(Map json) { return MissingFilter._private( json['metadataType'], + reversed: json['reversed'] ?? false, ); } @@ -42,10 +43,14 @@ class MissingFilter extends CollectionFilter { Map toMap() => { 'type': type, 'metadataType': metadataType, + 'reversed': reversed, }; @override - EntryFilter get test => _test; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => false; @override String get universalLabel => metadataType; diff --git a/lib/model/filters/path.dart b/lib/model/filters/path.dart index ce1c1ead8..cb5d5308a 100644 --- a/lib/model/filters/path.dart +++ b/lib/model/filters/path.dart @@ -10,14 +10,24 @@ class PathFilter extends CollectionFilter { // without trailing separator final String _rootAlbum; - @override - List get props => [path]; + late final EntryFilter _test; - PathFilter(this.path) : _rootAlbum = path.substring(0, path.length - 1); + @override + List get props => [path, reversed]; + + PathFilter(this.path, {super.reversed = false}) : _rootAlbum = path.substring(0, path.length - 1) { + _test = (entry) { + final dir = entry.directory; + if (dir == null) return false; + // avoid string building in most cases + return dir.startsWith(_rootAlbum) && '$dir${pContext.separator}'.startsWith(path); + }; + } factory PathFilter.fromMap(Map json) { return PathFilter( json['path'], + reversed: json['reversed'] ?? false, ); } @@ -25,15 +35,14 @@ class PathFilter extends CollectionFilter { Map toMap() => { 'type': type, 'path': path, + 'reversed': reversed, }; @override - EntryFilter get test => (entry) { - final dir = entry.directory; - if (dir == null) return false; - // avoid string building in most cases - return dir.startsWith(_rootAlbum) && '$dir${pContext.separator}'.startsWith(path); - }; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => true; @override String get universalLabel => path; diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index 7785b0345..05ccc1c6d 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -18,7 +18,7 @@ class QueryFilter extends CollectionFilter { late final EntryFilter _test; @override - List get props => [query, live]; + List get props => [query, live, reversed]; static final _fieldPattern = RegExp(r'(.+)([=<>])(.+)'); static final _fileSizePattern = RegExp(r'(\d+)([KMG])?'); @@ -33,7 +33,7 @@ class QueryFilter extends CollectionFilter { static const opLower = '<'; static const opGreater = '>'; - QueryFilter(this.query, {this.colorful = true, this.live = false}) { + QueryFilter(this.query, {this.colorful = true, this.live = false, super.reversed = false}) { var upQuery = query.toUpperCase(); final test = fieldTest(upQuery); @@ -62,6 +62,7 @@ class QueryFilter extends CollectionFilter { factory QueryFilter.fromMap(Map json) { return QueryFilter( json['query'], + reversed: json['reversed'] ?? false, ); } @@ -69,13 +70,14 @@ class QueryFilter extends CollectionFilter { Map toMap() => { 'type': type, 'query': query, + 'reversed': reversed, }; @override - EntryFilter get test => _test; + EntryFilter get positiveTest => _test; @override - bool isCompatible(CollectionFilter other) => true; + bool get exclusiveProp => false; @override String get universalLabel => query; diff --git a/lib/model/filters/rating.dart b/lib/model/filters/rating.dart index ad516aa5e..ad2a44998 100644 --- a/lib/model/filters/rating.dart +++ b/lib/model/filters/rating.dart @@ -7,15 +7,19 @@ class RatingFilter extends CollectionFilter { static const type = 'rating'; final int rating; + late final EntryFilter _test; @override - List get props => [rating]; + List get props => [rating, reversed]; - const RatingFilter(this.rating); + RatingFilter(this.rating, {super.reversed = false}) { + _test = (entry) => entry.rating == rating; + } factory RatingFilter.fromMap(Map json) { return RatingFilter( json['rating'] ?? 0, + reversed: json['reversed'] ?? false, ); } @@ -23,10 +27,14 @@ class RatingFilter extends CollectionFilter { Map toMap() => { 'type': type, 'rating': rating, + 'reversed': reversed, }; @override - EntryFilter get test => (entry) => entry.rating == rating; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => true; @override String get universalLabel => '$rating'; diff --git a/lib/model/filters/recent.dart b/lib/model/filters/recent.dart index f6ffb1a0b..c1555bab0 100644 --- a/lib/model/filters/recent.dart +++ b/lib/model/filters/recent.dart @@ -6,30 +6,43 @@ import 'package:flutter/material.dart'; class RecentlyAddedFilter extends CollectionFilter { static const type = 'recently_added'; + static late EntryFilter _test; + static final instance = RecentlyAddedFilter._private(); + static final instanceReversed = RecentlyAddedFilter._private(reversed: true); static late int nowSecs; static void updateNow() { nowSecs = DateTime.now().millisecondsSinceEpoch ~/ 1000; + _test = (entry) => (nowSecs - (entry.dateAddedSecs ?? 0)) < _dayInSecs; } static const _dayInSecs = 24 * 60 * 60; @override - List get props => []; + List get props => [reversed]; - RecentlyAddedFilter._private() { + RecentlyAddedFilter._private({super.reversed = false}) { updateNow(); } + factory RecentlyAddedFilter.fromMap(Map json) { + final reversed = json['reversed'] ?? false; + return reversed ? instanceReversed : instance; + } + @override Map toMap() => { 'type': type, + 'reversed': reversed, }; @override - EntryFilter get test => (entry) => (nowSecs - (entry.dateAddedSecs ?? 0)) < _dayInSecs; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => false; @override String get universalLabel => type; diff --git a/lib/model/filters/tag.dart b/lib/model/filters/tag.dart index b72624da1..389da1f4e 100644 --- a/lib/model/filters/tag.dart +++ b/lib/model/filters/tag.dart @@ -10,20 +10,20 @@ class TagFilter extends CoveredCollectionFilter { late final EntryFilter _test; @override - List get props => [tag]; + List get props => [tag, reversed]; - TagFilter(this.tag, {bool not = false}) : super(not: not) { + TagFilter(this.tag, {super.reversed = false}) { if (tag.isEmpty) { - _test = not ? (entry) => entry.tags.isNotEmpty : (entry) => entry.tags.isEmpty; + _test = (entry) => entry.tags.isEmpty; } else { - _test = not ? (entry) => !entry.tags.contains(tag) : (entry) => entry.tags.contains(tag); + _test = (entry) => entry.tags.contains(tag); } } factory TagFilter.fromMap(Map json) { return TagFilter( json['tag'], - not: json['not'] ?? false, + reversed: json['reversed'] ?? false, ); } @@ -31,14 +31,14 @@ class TagFilter extends CoveredCollectionFilter { Map toMap() => { 'type': type, 'tag': tag, - 'not': not, + 'reversed': reversed, }; @override - EntryFilter get test => _test; + EntryFilter get positiveTest => _test; @override - bool isCompatible(CollectionFilter other) => true; + bool get exclusiveProp => false; @override String get universalLabel => tag; diff --git a/lib/model/filters/trash.dart b/lib/model/filters/trash.dart index 536318fa7..666236dc2 100644 --- a/lib/model/filters/trash.dart +++ b/lib/model/filters/trash.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -6,20 +7,32 @@ import 'package:flutter/material.dart'; class TrashFilter extends CollectionFilter { static const type = 'trash'; + static bool _test(AvesEntry entry) => entry.trashed; + static const instance = TrashFilter._private(); + static const instanceReversed = TrashFilter._private(reversed: true); @override - List get props => []; + List get props => [reversed]; - const TrashFilter._private(); + const TrashFilter._private({super.reversed = false}); + + factory TrashFilter.fromMap(Map json) { + final reversed = json['reversed'] ?? false; + return reversed ? instanceReversed : instance; + } @override Map toMap() => { 'type': type, + 'reversed': reversed, }; @override - EntryFilter get test => (entry) => entry.trashed; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => false; @override String get universalLabel => type; diff --git a/lib/model/filters/type.dart b/lib/model/filters/type.dart index bef26c27b..7a7788466 100644 --- a/lib/model/filters/type.dart +++ b/lib/model/filters/type.dart @@ -17,8 +17,8 @@ class TypeFilter extends CollectionFilter { static const _sphericalVideo = 'spherical_video'; // subset of videos final String itemType; - late final EntryFilter _test; late final IconData _icon; + late final EntryFilter _test; static final animated = TypeFilter._private(_animated); static final geotiff = TypeFilter._private(_geotiff); @@ -28,9 +28,9 @@ class TypeFilter extends CollectionFilter { static final sphericalVideo = TypeFilter._private(_sphericalVideo); @override - List get props => [itemType]; + List get props => [itemType, reversed]; - TypeFilter._private(this.itemType) { + TypeFilter._private(this.itemType, {super.reversed = false}) { switch (itemType) { case _animated: _test = (entry) => entry.isAnimated; @@ -62,6 +62,7 @@ class TypeFilter extends CollectionFilter { factory TypeFilter.fromMap(Map json) { return TypeFilter._private( json['itemType'], + reversed: json['reversed'] ?? false, ); } @@ -69,10 +70,14 @@ class TypeFilter extends CollectionFilter { Map toMap() => { 'type': type, 'itemType': itemType, + 'reversed': reversed, }; @override - EntryFilter get test => _test; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => false; @override String get universalLabel => itemType; diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index fbeec1e64..f4e48bdd8 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -128,7 +128,7 @@ class CollectionLens with ChangeNotifier { } bool get showHeaders { - bool showAlbumHeaders() => !filters.any((f) => f is AlbumFilter); + bool showAlbumHeaders() => !filters.any((v) => v is AlbumFilter && !v.reversed); switch (sortFactor) { case EntrySortFactor.date: diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index a1c6e59bf..7dd2853f1 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -105,6 +105,7 @@ class AIcons { static const IconData print = Icons.print_outlined; static const IconData refresh = Icons.refresh_outlined; static const IconData replay10 = Icons.replay_10_outlined; + static const IconData reverse = Icons.invert_colors_outlined; static const IconData skip10 = Icons.forward_10_outlined; static const IconData reset = Icons.restart_alt_outlined; static const IconData restore = Icons.restore_outlined; diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 13ee3c64b..05a3bbd15 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -29,6 +29,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_app_bar.dart'; import 'package:aves/widgets/common/search/route.dart'; import 'package:aves/widgets/dialogs/tile_view_dialog.dart'; +import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -168,10 +169,16 @@ class _CollectionAppBarState extends State with SingleTickerPr bottom: Column( children: [ if (showFilterBar) - FilterBar( - filters: visibleFilters, - removable: removableFilters, - onTap: removableFilters ? collection.removeFilter : null, + NotificationListener( + onNotification: (notification) { + collection.addFilter(notification.reversedFilter); + return true; + }, + child: FilterBar( + filters: visibleFilters, + removable: removableFilters, + onTap: removableFilters ? collection.removeFilter : null, + ), ), if (queryEnabled) EntryQueryBar( diff --git a/lib/widgets/collection/filter_bar.dart b/lib/widgets/collection/filter_bar.dart index 5170fff8e..dfbb3b873 100644 --- a/lib/widgets/collection/filter_bar.dart +++ b/lib/widgets/collection/filter_bar.dart @@ -64,7 +64,7 @@ class _FilterBarState extends State { ), ); } - : (context, animation) => _buildChip(filter), + : (context, animation) => const SizedBox(), duration: animate ? Durations.filterBarRemovalAnimation : Duration.zero, ); }); diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index 6d58cb513..7f30ff28a 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -15,6 +15,7 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/filter_bar.dart'; import 'package:aves/widgets/common/basic/menu.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:flutter/material.dart'; @@ -96,6 +97,7 @@ class AvesFilterChip extends StatefulWidget { if (filter is AlbumFilter) ChipAction.goToAlbumPage, if ((filter is LocationFilter && filter.level == LocationLevel.country)) ChipAction.goToCountryPage, if (filter is TagFilter) ChipAction.goToTagPage, + ChipAction.reverse, ChipAction.hide, ]; @@ -113,12 +115,20 @@ class AvesFilterChip extends StatefulWidget { child: Text(filter.getLabel(context)), ), const PopupMenuDivider(), - ...actions.map((action) => PopupMenuItem( - value: action, - child: MenuIconTheme( - child: MenuRow(text: action.getText(context), icon: action.getIcon()), - ), - )), + ...actions.map((action) { + late String text; + if (action == ChipAction.reverse) { + text = filter.reversed ? context.l10n.chipActionFilterIn : context.l10n.chipActionFilterOut; + } else { + text = action.getText(context); + } + return PopupMenuItem( + value: action, + child: MenuIconTheme( + child: MenuRow(text: text, icon: action.getIcon()), + ), + ); + }), ], ); if (selectedAction != null) { @@ -229,7 +239,8 @@ class _AvesFilterChipState extends State { filter.getLabel(context), style: TextStyle( fontSize: AvesFilterChip.fontSize, - decoration: filter.not ? TextDecoration.lineThrough : null, + decoration: filter.reversed ? TextDecoration.lineThrough : null, + decorationThickness: 2, ), softWrap: false, overflow: TextOverflow.fade, diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index 12751b2d3..ed0f20673 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -173,7 +173,7 @@ class _AppDebugPageState extends State { final source = context.read(); settings.changeFilterVisibility(settings.hiddenFilters, true); settings.changeFilterVisibility({ - TagFilter('aves-thumbnail', not: true), + TagFilter('aves-thumbnail', reversed: true), }, false); await favourites.clear(); await favourites.add(source.visibleEntries); diff --git a/lib/widgets/filter_grids/common/action_delegates/chip.dart b/lib/widgets/filter_grids/common/action_delegates/chip.dart index 3e86c12db..1d75b73cd 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip.dart @@ -13,9 +13,6 @@ import 'package:provider/provider.dart'; class ChipActionDelegate { void onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) { switch (action) { - case ChipAction.hide: - _hide(context, filter); - break; case ChipAction.goToAlbumPage: _goTo(context, filter, AlbumListPage.routeName, (context) => const AlbumListPage()); break; @@ -25,11 +22,34 @@ class ChipActionDelegate { case ChipAction.goToTagPage: _goTo(context, filter, TagListPage.routeName, (context) => const TagListPage()); break; + case ChipAction.reverse: + ReverseFilterNotification(filter).dispatch(context); + break; + case ChipAction.hide: + _hide(context, filter); + break; default: break; } } + void _goTo( + BuildContext context, + CollectionFilter filter, + String routeName, + WidgetBuilder pageBuilder, + ) { + context.read().set(filter); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + settings: RouteSettings(name: routeName), + builder: pageBuilder, + ), + (route) => false, + ); + } + Future _hide(BuildContext context, CollectionFilter filter) async { final confirmed = await showDialog( context: context, @@ -53,21 +73,11 @@ class ChipActionDelegate { settings.changeFilterVisibility({filter}, false); } - - void _goTo( - BuildContext context, - CollectionFilter filter, - String routeName, - WidgetBuilder pageBuilder, - ) { - context.read().set(filter); - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute( - settings: RouteSettings(name: routeName), - builder: pageBuilder, - ), - (route) => false, - ); - } +} + +@immutable +class ReverseFilterNotification extends Notification { + final CollectionFilter reversedFilter; + + ReverseFilterNotification(CollectionFilter filter) : reversedFilter = filter.reverse(); } diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index 464c7b74e..c59574814 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -21,6 +21,7 @@ import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; import 'package:aves/widgets/common/providers/map_theme_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/thumbnail/scroller.dart'; +import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/map/map_info_row.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/notifications.dart'; @@ -164,9 +165,15 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin @override Widget build(BuildContext context) { - return NotificationListener( + return NotificationListener( onNotification: (notification) { - _goToCollection(notification.filter); + if (notification is FilterSelectedNotification) { + _goToCollection(notification.filter); + } else if (notification is ReverseFilterNotification) { + _goToCollection(notification.reversedFilter); + } else { + return false; + } return true; }, child: Selector( diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index 0b23308ab..c9ed92b34 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -22,6 +22,7 @@ import 'package:aves/widgets/common/expandable_filter_row.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/search/delegate.dart'; +import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -65,52 +66,60 @@ class CollectionSearchDelegate extends AvesSearchDelegate { final upQuery = query.trim().toUpperCase(); bool containQuery(String s) => s.toUpperCase().contains(upQuery); return SafeArea( - child: ValueListenableBuilder( + child: NotificationListener( + onNotification: (notification) { + _select(context, notification.reversedFilter); + return true; + }, + child: ValueListenableBuilder( valueListenable: _expandedSectionNotifier, builder: (context, expandedSection, child) { final queryFilter = _buildQueryFilter(false); return Selector>( - selector: (context, s) => s.hiddenFilters, - builder: (context, hiddenFilters, child) { - bool notHidden(CollectionFilter filter) => !hiddenFilters.contains(filter); + selector: (context, s) => s.hiddenFilters, + builder: (context, hiddenFilters, child) { + bool notHidden(CollectionFilter filter) => !hiddenFilters.contains(filter); - final visibleTypeFilters = typeFilters.where(notHidden).toList(); - if (hiddenFilters.contains(MimeFilter.video)) { - [MimeFilter.image, TypeFilter.sphericalVideo].forEach(visibleTypeFilters.remove); - } + final visibleTypeFilters = typeFilters.where(notHidden).toList(); + if (hiddenFilters.contains(MimeFilter.video)) { + [MimeFilter.image, TypeFilter.sphericalVideo].forEach(visibleTypeFilters.remove); + } - final history = settings.searchHistory.where(notHidden).toList(); + final history = settings.searchHistory.where(notHidden).toList(); - return ListView( - padding: const EdgeInsets.only(top: 8), - children: [ + return ListView( + padding: const EdgeInsets.only(top: 8), + children: [ + _buildFilterRow( + context: context, + filters: [ + queryFilter, + ...visibleTypeFilters, + ].whereNotNull().where((f) => containQuery(f.getLabel(context))).toList(), + // usually perform hero animation only on tapped chips, + // 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, - filters: [ - queryFilter, - ...visibleTypeFilters, - ].whereNotNull().where((f) => containQuery(f.getLabel(context))).toList(), - // usually perform hero animation only on tapped chips, - // 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, + title: context.l10n.searchRecentSectionTitle, + filters: history, ), - if (upQuery.isEmpty && history.isNotEmpty) - _buildFilterRow( - context: context, - title: context.l10n.searchRecentSectionTitle, - filters: history, - ), - _buildDateFilters(context, containQuery), - _buildAlbumFilters(containQuery), - _buildCountryFilters(containQuery), - _buildPlaceFilters(containQuery), - _buildTagFilters(containQuery), - _buildRatingFilters(context, containQuery), - _buildMetadataFilters(context, containQuery), - ], - ); - }); - }), + _buildDateFilters(context, containQuery), + _buildAlbumFilters(containQuery), + _buildCountryFilters(containQuery), + _buildPlaceFilters(containQuery), + _buildTagFilters(containQuery), + _buildRatingFilters(context, containQuery), + _buildMetadataFilters(context, containQuery), + ], + ); + }, + ); + }, + ), + ), ); } @@ -219,7 +228,7 @@ class CollectionSearchDelegate extends AvesSearchDelegate { MissingFilter.date, LocationFilter(LocationLevel.place, ''), TagFilter(''), - const RatingFilter(0), + RatingFilter(0), MissingFilter.title, ].where((f) => containQuery(f.getLabel(context))).toList(), ); diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index e08c69028..f4bb72074 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -17,8 +17,10 @@ import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/stats/date/histogram.dart'; import 'package:aves/widgets/stats/filter_table.dart'; import 'package:aves/widgets/stats/mime_donut.dart'; @@ -185,31 +187,37 @@ class _StatsPageState extends State { ); final showRatings = _entryCountPerRating.entries.any((kv) => kv.key != 0 && kv.value > 0); final source = widget.source; - child = AnimationLimiter( - child: ListView( - children: AnimationConfiguration.toStaggeredList( - duration: durations.staggeredAnimation, - delay: durations.staggeredAnimationDelay * timeDilation, - childAnimationBuilder: (child) => SlideAnimation( - verticalOffset: 50.0, - child: FadeInAnimation( - child: child, + child = NotificationListener( + onNotification: (notification) { + _onFilterSelection(context, notification.reversedFilter); + return true; + }, + child: AnimationLimiter( + child: ListView( + children: AnimationConfiguration.toStaggeredList( + duration: durations.staggeredAnimation, + delay: durations.staggeredAnimationDelay * timeDilation, + childAnimationBuilder: (child) => SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: child, + ), ), + children: [ + mimeDonuts, + Histogram( + entries: entries, + animationDuration: chartAnimationDuration, + onFilterSelection: (filter) => _onFilterSelection(context, filter), + ), + locationIndicator, + ..._buildFilterSection(context, l10n.statsTopCountriesSectionTitle, _entryCountPerCountry, (v) => LocationFilter(LocationLevel.country, v)), + ..._buildFilterSection(context, l10n.statsTopPlacesSectionTitle, _entryCountPerPlace, (v) => LocationFilter(LocationLevel.place, v)), + ..._buildFilterSection(context, l10n.statsTopTagsSectionTitle, _entryCountPerTag, TagFilter.new), + ..._buildFilterSection(context, l10n.statsTopAlbumsSectionTitle, _entryCountPerAlbum, (v) => AlbumFilter(v, source.getAlbumDisplayName(context, v))), + if (showRatings) ..._buildFilterSection(context, l10n.searchRatingSectionTitle, _entryCountPerRating, RatingFilter.new, sortByCount: false, maxRowCount: null), + ], ), - children: [ - mimeDonuts, - Histogram( - entries: entries, - animationDuration: chartAnimationDuration, - onFilterSelection: (filter) => _onFilterSelection(context, filter), - ), - locationIndicator, - ..._buildFilterSection(context, l10n.statsTopCountriesSectionTitle, _entryCountPerCountry, (v) => LocationFilter(LocationLevel.country, v)), - ..._buildFilterSection(context, l10n.statsTopPlacesSectionTitle, _entryCountPerPlace, (v) => LocationFilter(LocationLevel.place, v)), - ..._buildFilterSection(context, l10n.statsTopTagsSectionTitle, _entryCountPerTag, TagFilter.new), - ..._buildFilterSection(context, l10n.statsTopAlbumsSectionTitle, _entryCountPerAlbum, (v) => AlbumFilter(v, source.getAlbumDisplayName(context, v))), - if (showRatings) ..._buildFilterSection(context, l10n.searchRatingSectionTitle, _entryCountPerRating, RatingFilter.new, sortByCount: false, maxRowCount: null), - ], ), ), ); @@ -277,6 +285,7 @@ class _StatsPageState extends State { maxRowCount: null, onFilterSelection: (filter) => _onFilterSelection(context, filter), ), + onFilterSelection: (filter) => _onFilterSelection(context, filter), ), ), ) @@ -312,7 +321,7 @@ class _StatsPageState extends State { // even when the target is a child of an `AnimatedList`. // Do not use `WidgetsBinding.instance.addPostFrameCallback`, // as it may not trigger if there is no subsequent build. - Future.delayed(const Duration(milliseconds: 100), () => Navigator.pop(context)); + Future.delayed(const Duration(milliseconds: 100), () => Navigator.popUntil(context, (route) => route.settings.name == CollectionPage.routeName)); } void _jumpToCollectionPage(BuildContext context, CollectionFilter filter) { @@ -335,11 +344,13 @@ class StatsTopPage extends StatelessWidget { final String title; final WidgetBuilder tableBuilder; + final FilterCallback onFilterSelection; const StatsTopPage({ super.key, required this.title, required this.tableBuilder, + required this.onFilterSelection, }); @override @@ -353,12 +364,18 @@ class StatsTopPage extends StatelessWidget { child: SafeArea( bottom: false, child: Builder(builder: (context) { - return SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 8) + - EdgeInsets.only( - bottom: context.select((mq) => mq.effectiveBottomPadding), - ), - child: tableBuilder(context), + return NotificationListener( + onNotification: (notification) { + onFilterSelection(notification.reversedFilter); + return true; + }, + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 8) + + EdgeInsets.only( + bottom: context.select((mq) => mq.effectiveBottomPadding), + ), + child: tableBuilder(context), + ), ); }), ), diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index f2a2a5a21..31a4e8a96 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -8,6 +8,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart'; import 'package:aves/widgets/viewer/embedded/embedded_data_opener.dart'; import 'package:aves/widgets/viewer/info/basic_section.dart'; @@ -230,25 +231,31 @@ class _InfoPageContentState extends State<_InfoPageContent> { metadataNotifier: _metadataNotifier, ); - return CustomScrollView( - controller: widget.scrollController, - slivers: [ - InfoAppBar( - entry: entry, - actionDelegate: _actionDelegate, - metadataNotifier: _metadataNotifier, - onBackPressed: widget.goToViewer, - ), - SliverPadding( - padding: horizontalPadding + const EdgeInsets.only(top: 8), - sliver: basicAndLocationSliver, - ), - SliverPadding( - padding: horizontalPadding + const EdgeInsets.only(bottom: 8), - sliver: metadataSliver, - ), - const BottomPaddingSliver(), - ], + return NotificationListener( + onNotification: (notification) { + _onFilter(notification.reversedFilter); + return true; + }, + child: CustomScrollView( + controller: widget.scrollController, + slivers: [ + InfoAppBar( + entry: entry, + actionDelegate: _actionDelegate, + metadataNotifier: _metadataNotifier, + onBackPressed: widget.goToViewer, + ), + SliverPadding( + padding: horizontalPadding + const EdgeInsets.only(top: 8), + sliver: basicAndLocationSliver, + ), + SliverPadding( + padding: horizontalPadding + const EdgeInsets.only(bottom: 8), + sliver: metadataSliver, + ), + const BottomPaddingSliver(), + ], + ), ); } diff --git a/test/model/collection_source_test.dart b/test/model/collection_source_test.dart index 9858c8114..033a3bdde 100644 --- a/test/model/collection_source_test.dart +++ b/test/model/collection_source_test.dart @@ -113,7 +113,7 @@ void main() { }); test('album/country/tag hidden on launch when their items are hidden by entry prop', () async { - settings.hiddenFilters = {const AlbumFilter(testAlbum, 'whatever')}; + settings.hiddenFilters = {AlbumFilter(testAlbum, 'whatever')}; final image1 = FakeMediaStoreService.newImage(testAlbum, 'image1'); (mediaStoreService as FakeMediaStoreService).entries = { @@ -191,7 +191,7 @@ void main() { expect(source.rawAlbums.length, 1); expect(covers.count, 0); - const albumFilter = AlbumFilter(testAlbum, 'whatever'); + final albumFilter = AlbumFilter(testAlbum, 'whatever'); expect(albumFilter.test(image1), true); expect(covers.count, 0); expect(covers.of(albumFilter), null); @@ -213,7 +213,7 @@ void main() { final source = await _initSource(); await image1.toggleFavourite(); - const albumFilter = AlbumFilter(testAlbum, 'whatever'); + final albumFilter = AlbumFilter(testAlbum, 'whatever'); await covers.set(filter: albumFilter, entryId: image1.id, packageName: null, color: null); await source.updateAfterRename( todoEntries: {image1}, @@ -257,8 +257,8 @@ void main() { expect(source.rawAlbums.contains(sourceAlbum), true); expect(source.rawAlbums.contains(destinationAlbum), false); - const sourceAlbumFilter = AlbumFilter(sourceAlbum, 'whatever'); - const destinationAlbumFilter = AlbumFilter(destinationAlbum, 'whatever'); + final sourceAlbumFilter = AlbumFilter(sourceAlbum, 'whatever'); + final destinationAlbumFilter = AlbumFilter(destinationAlbum, 'whatever'); expect(sourceAlbumFilter.test(image1), true); expect(destinationAlbumFilter.test(image1), false); @@ -308,7 +308,7 @@ void main() { final source = await _initSource(); expect(source.rawAlbums.length, 1); - const sourceAlbumFilter = AlbumFilter(sourceAlbum, 'whatever'); + final sourceAlbumFilter = AlbumFilter(sourceAlbum, 'whatever'); await covers.set(filter: sourceAlbumFilter, entryId: image1.id, packageName: null, color: null); await source.updateAfterMove( @@ -333,14 +333,14 @@ void main() { final source = await _initSource(); await image1.toggleFavourite(); - var albumFilter = const AlbumFilter(sourceAlbum, 'whatever'); + var albumFilter = AlbumFilter(sourceAlbum, 'whatever'); await covers.set(filter: albumFilter, entryId: image1.id, packageName: null, color: null); await source.renameAlbum(sourceAlbum, destinationAlbum, { image1 }, { FakeMediaStoreService.moveOpEventForMove(image1, sourceAlbum, destinationAlbum), }); - albumFilter = const AlbumFilter(destinationAlbum, 'whatever'); + albumFilter = AlbumFilter(destinationAlbum, 'whatever'); expect(favourites.count, 1); expect(image1.isFavourite, true); diff --git a/test/model/filters_test.dart b/test/model/filters_test.dart index 39e6f582f..be4627b73 100644 --- a/test/model/filters_test.dart +++ b/test/model/filters_test.dart @@ -33,7 +33,7 @@ void main() { test('Filter serialization', () { CollectionFilter? jsonRoundTrip(filter) => CollectionFilter.fromJson(filter.toJson()); - const album = AlbumFilter('path/to/album', 'album'); + final album = AlbumFilter('path/to/album', 'album'); expect(album, jsonRoundTrip(album)); final bounds = CoordinateFilter(LatLng(29.979167, 28.223615), LatLng(36.451000, 31.134167)); @@ -63,7 +63,7 @@ void main() { final query = QueryFilter('some query'); expect(query, jsonRoundTrip(query)); - const rating = RatingFilter(3); + final rating = RatingFilter(3); expect(rating, jsonRoundTrip(rating)); final recent = RecentlyAddedFilter.instance; diff --git a/untranslated.json b/untranslated.json index 541bdd91f..15a757309 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,5 +1,7 @@ { "de": [ + "chipActionFilterOut", + "chipActionFilterIn", "viewerTransitionNone", "widgetOpenPageHome", "widgetOpenPageViewer", @@ -11,6 +13,8 @@ ], "el": [ + "chipActionFilterOut", + "chipActionFilterIn", "viewerTransitionNone", "widgetOpenPageHome", "widgetOpenPageViewer", @@ -22,6 +26,8 @@ ], "es": [ + "chipActionFilterOut", + "chipActionFilterIn", "entryInfoActionEditTitleDescription", "filterNoDateLabel", "filterNoTitleLabel", @@ -48,16 +54,9 @@ "viewerInfoLabelDescription" ], - "fr": [ - "widgetOpenPageHome", - "widgetOpenPageViewer", - "albumGroupType", - "albumMimeTypeMixed", - "settingsWidgetOpenPage", - "statsTopAlbumsSectionTitle" - ], - "id": [ + "chipActionFilterOut", + "chipActionFilterIn", "viewerTransitionNone", "widgetOpenPageHome", "widgetOpenPageViewer", @@ -69,6 +68,8 @@ ], "it": [ + "chipActionFilterOut", + "chipActionFilterIn", "viewerTransitionNone", "widgetOpenPageHome", "widgetOpenPageViewer", @@ -80,6 +81,8 @@ ], "ja": [ + "chipActionFilterOut", + "chipActionFilterIn", "entryInfoActionEditTitleDescription", "filterNoDateLabel", "filterNoTitleLabel", @@ -107,16 +110,9 @@ "viewerInfoLabelDescription" ], - "ko": [ - "widgetOpenPageHome", - "widgetOpenPageViewer", - "albumGroupType", - "albumMimeTypeMixed", - "settingsWidgetOpenPage", - "statsTopAlbumsSectionTitle" - ], - "nl": [ + "chipActionFilterOut", + "chipActionFilterIn", "viewerTransitionNone", "widgetOpenPageHome", "widgetOpenPageViewer", @@ -128,6 +124,8 @@ ], "pt": [ + "chipActionFilterOut", + "chipActionFilterIn", "viewerTransitionNone", "widgetOpenPageHome", "widgetOpenPageViewer", @@ -139,6 +137,8 @@ ], "ru": [ + "chipActionFilterOut", + "chipActionFilterIn", "viewerTransitionNone", "widgetOpenPageHome", "widgetOpenPageViewer", @@ -150,6 +150,8 @@ ], "tr": [ + "chipActionFilterOut", + "chipActionFilterIn", "slideshowActionResume", "slideshowActionShowInCollection", "entryInfoActionEditTitleDescription", @@ -206,6 +208,8 @@ ], "zh": [ + "chipActionFilterOut", + "chipActionFilterIn", "viewerTransitionNone", "widgetOpenPageHome", "widgetOpenPageViewer",