reverse filters to filter out/in

This commit is contained in:
Thibault Deckers 2022-09-27 19:15:20 +02:00
parent 92ba7b9e9f
commit 9ba9ec302e
34 changed files with 463 additions and 244 deletions

View file

@ -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

View file

@ -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",

View file

@ -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 daccueil",
"widgetOpenPageViewer": "Ouvrir la visionneuse",
"albumTierNew": "Nouveaux",
"albumTierPinned": "Épinglés",
"albumTierSpecial": "Standards",
@ -403,9 +408,12 @@
"sortOrderSmallestFirst": "Moins larges dabord",
"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",

View file

@ -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": "설정",

View file

@ -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;
}

View file

@ -14,16 +14,20 @@ class AlbumFilter extends CoveredCollectionFilter {
final String album;
final String? displayName;
late final EntryFilter _test;
@override
List<Object?> get props => [album];
List<Object?> 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<String, dynamic> 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;

View file

@ -17,16 +17,20 @@ class CoordinateFilter extends CollectionFilter {
final LatLng sw;
final LatLng ne;
final bool minuteSecondPadding;
late final EntryFilter _test;
@override
List<Object?> get props => [sw, ne];
List<Object?> 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<String, dynamic> 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);

View file

@ -18,9 +18,9 @@ class DateFilter extends CollectionFilter {
static final onThisDay = DateFilter(DateLevel.md, null);
@override
List<Object?> get props => [level, date];
List<Object?> 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;
}

View file

@ -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<Object?> get props => [];
List<Object?> get props => [reversed];
const FavouriteFilter._private();
const FavouriteFilter._private({super.reversed = false});
factory FavouriteFilter.fromMap(Map<String, dynamic> json) {
final reversed = json['reversed'] ?? false;
return reversed ? instanceReversed : instance;
}
@override
Map<String, dynamic> 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;

View file

@ -42,9 +42,44 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
PathFilter.type,
];
final bool not;
final bool reversed;
const CollectionFilter({this.not = false});
const CollectionFilter({required this.reversed});
static CollectionFilter? _fromMap(Map<String, dynamic> 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<Collecti
try {
final jsonMap = jsonDecode(jsonString);
if (jsonMap is Map<String, dynamic>) {
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<Collecti
String toJson() => 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<Collecti
@immutable
abstract class CoveredCollectionFilter extends CollectionFilter {
const CoveredCollectionFilter({bool not = false}) : super(not: not);
const CoveredCollectionFilter({required super.reversed});
@override
Future<Color> color(BuildContext context) {

View file

@ -15,9 +15,9 @@ class LocationFilter extends CoveredCollectionFilter {
late final EntryFilter _test;
@override
List<Object?> get props => [level, _location, _countryCode];
List<Object?> 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;

View file

@ -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<Object?> get props => [mime];
List<Object?> 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<String, dynamic> json) {
return MimeFilter(
json['mime'],
reversed: json['reversed'] ?? false,
);
}
@ -53,10 +54,14 @@ class MimeFilter extends CollectionFilter {
Map<String, dynamic> 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;

View file

@ -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<Object?> get props => [metadataType];
List<Object?> 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<String, dynamic> json) {
return MissingFilter._private(
json['metadataType'],
reversed: json['reversed'] ?? false,
);
}
@ -42,10 +43,14 @@ class MissingFilter extends CollectionFilter {
Map<String, dynamic> 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;

View file

@ -10,14 +10,24 @@ class PathFilter extends CollectionFilter {
// without trailing separator
final String _rootAlbum;
@override
List<Object?> get props => [path];
late final EntryFilter _test;
PathFilter(this.path) : _rootAlbum = path.substring(0, path.length - 1);
@override
List<Object?> 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<String, dynamic> json) {
return PathFilter(
json['path'],
reversed: json['reversed'] ?? false,
);
}
@ -25,15 +35,14 @@ class PathFilter extends CollectionFilter {
Map<String, dynamic> 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;

View file

@ -18,7 +18,7 @@ class QueryFilter extends CollectionFilter {
late final EntryFilter _test;
@override
List<Object?> get props => [query, live];
List<Object?> 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<String, dynamic> json) {
return QueryFilter(
json['query'],
reversed: json['reversed'] ?? false,
);
}
@ -69,13 +70,14 @@ class QueryFilter extends CollectionFilter {
Map<String, dynamic> 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;

View file

@ -7,15 +7,19 @@ class RatingFilter extends CollectionFilter {
static const type = 'rating';
final int rating;
late final EntryFilter _test;
@override
List<Object?> get props => [rating];
List<Object?> get props => [rating, reversed];
const RatingFilter(this.rating);
RatingFilter(this.rating, {super.reversed = false}) {
_test = (entry) => entry.rating == rating;
}
factory RatingFilter.fromMap(Map<String, dynamic> json) {
return RatingFilter(
json['rating'] ?? 0,
reversed: json['reversed'] ?? false,
);
}
@ -23,10 +27,14 @@ class RatingFilter extends CollectionFilter {
Map<String, dynamic> 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';

View file

@ -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<Object?> get props => [];
List<Object?> get props => [reversed];
RecentlyAddedFilter._private() {
RecentlyAddedFilter._private({super.reversed = false}) {
updateNow();
}
factory RecentlyAddedFilter.fromMap(Map<String, dynamic> json) {
final reversed = json['reversed'] ?? false;
return reversed ? instanceReversed : instance;
}
@override
Map<String, dynamic> 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;

View file

@ -10,20 +10,20 @@ class TagFilter extends CoveredCollectionFilter {
late final EntryFilter _test;
@override
List<Object?> get props => [tag];
List<Object?> 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<String, dynamic> json) {
return TagFilter(
json['tag'],
not: json['not'] ?? false,
reversed: json['reversed'] ?? false,
);
}
@ -31,14 +31,14 @@ class TagFilter extends CoveredCollectionFilter {
Map<String, dynamic> 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;

View file

@ -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<Object?> get props => [];
List<Object?> get props => [reversed];
const TrashFilter._private();
const TrashFilter._private({super.reversed = false});
factory TrashFilter.fromMap(Map<String, dynamic> json) {
final reversed = json['reversed'] ?? false;
return reversed ? instanceReversed : instance;
}
@override
Map<String, dynamic> 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;

View file

@ -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<Object?> get props => [itemType];
List<Object?> 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<String, dynamic> json) {
return TypeFilter._private(
json['itemType'],
reversed: json['reversed'] ?? false,
);
}
@ -69,10 +70,14 @@ class TypeFilter extends CollectionFilter {
Map<String, dynamic> 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;

View file

@ -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:

View file

@ -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;

View file

@ -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<CollectionAppBar> with SingleTickerPr
bottom: Column(
children: [
if (showFilterBar)
FilterBar(
filters: visibleFilters,
removable: removableFilters,
onTap: removableFilters ? collection.removeFilter : null,
NotificationListener<ReverseFilterNotification>(
onNotification: (notification) {
collection.addFilter(notification.reversedFilter);
return true;
},
child: FilterBar(
filters: visibleFilters,
removable: removableFilters,
onTap: removableFilters ? collection.removeFilter : null,
),
),
if (queryEnabled)
EntryQueryBar(

View file

@ -64,7 +64,7 @@ class _FilterBarState extends State<FilterBar> {
),
);
}
: (context, animation) => _buildChip(filter),
: (context, animation) => const SizedBox(),
duration: animate ? Durations.filterBarRemovalAnimation : Duration.zero,
);
});

View file

@ -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<AvesFilterChip> {
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,

View file

@ -173,7 +173,7 @@ class _AppDebugPageState extends State<AppDebugPage> {
final source = context.read<CollectionSource>();
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);

View file

@ -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<HighlightInfo>().set(filter);
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
settings: RouteSettings(name: routeName),
builder: pageBuilder,
),
(route) => false,
);
}
Future<void> _hide(BuildContext context, CollectionFilter filter) async {
final confirmed = await showDialog<bool>(
context: context,
@ -53,21 +73,11 @@ class ChipActionDelegate {
settings.changeFilterVisibility({filter}, false);
}
void _goTo(
BuildContext context,
CollectionFilter filter,
String routeName,
WidgetBuilder pageBuilder,
) {
context.read<HighlightInfo>().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();
}

View file

@ -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<FilterSelectedNotification>(
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<Settings, EntryMapStyle>(

View file

@ -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<String?>(
child: NotificationListener<ReverseFilterNotification>(
onNotification: (notification) {
_select(context, notification.reversedFilter);
return true;
},
child: ValueListenableBuilder<String?>(
valueListenable: _expandedSectionNotifier,
builder: (context, expandedSection, child) {
final queryFilter = _buildQueryFilter(false);
return Selector<Settings, Set<CollectionFilter>>(
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(),
);

View file

@ -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<StatsPage> {
);
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<ReverseFilterNotification>(
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<String>(context, l10n.statsTopCountriesSectionTitle, _entryCountPerCountry, (v) => LocationFilter(LocationLevel.country, v)),
..._buildFilterSection<String>(context, l10n.statsTopPlacesSectionTitle, _entryCountPerPlace, (v) => LocationFilter(LocationLevel.place, v)),
..._buildFilterSection<String>(context, l10n.statsTopTagsSectionTitle, _entryCountPerTag, TagFilter.new),
..._buildFilterSection<String>(context, l10n.statsTopAlbumsSectionTitle, _entryCountPerAlbum, (v) => AlbumFilter(v, source.getAlbumDisplayName(context, v))),
if (showRatings) ..._buildFilterSection<int>(context, l10n.searchRatingSectionTitle, _entryCountPerRating, RatingFilter.new, sortByCount: false, maxRowCount: null),
],
),
children: [
mimeDonuts,
Histogram(
entries: entries,
animationDuration: chartAnimationDuration,
onFilterSelection: (filter) => _onFilterSelection(context, filter),
),
locationIndicator,
..._buildFilterSection<String>(context, l10n.statsTopCountriesSectionTitle, _entryCountPerCountry, (v) => LocationFilter(LocationLevel.country, v)),
..._buildFilterSection<String>(context, l10n.statsTopPlacesSectionTitle, _entryCountPerPlace, (v) => LocationFilter(LocationLevel.place, v)),
..._buildFilterSection<String>(context, l10n.statsTopTagsSectionTitle, _entryCountPerTag, TagFilter.new),
..._buildFilterSection<String>(context, l10n.statsTopAlbumsSectionTitle, _entryCountPerAlbum, (v) => AlbumFilter(v, source.getAlbumDisplayName(context, v))),
if (showRatings) ..._buildFilterSection<int>(context, l10n.searchRatingSectionTitle, _entryCountPerRating, RatingFilter.new, sortByCount: false, maxRowCount: null),
],
),
),
);
@ -277,6 +285,7 @@ class _StatsPageState extends State<StatsPage> {
maxRowCount: null,
onFilterSelection: (filter) => _onFilterSelection(context, filter),
),
onFilterSelection: (filter) => _onFilterSelection(context, filter),
),
),
)
@ -312,7 +321,7 @@ class _StatsPageState extends State<StatsPage> {
// 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<MediaQueryData, double>((mq) => mq.effectiveBottomPadding),
),
child: tableBuilder(context),
return NotificationListener<ReverseFilterNotification>(
onNotification: (notification) {
onFilterSelection(notification.reversedFilter);
return true;
},
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 8) +
EdgeInsets.only(
bottom: context.select<MediaQueryData, double>((mq) => mq.effectiveBottomPadding),
),
child: tableBuilder(context),
),
);
}),
),

View file

@ -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<ReverseFilterNotification>(
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(),
],
),
);
}

View file

@ -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);

View file

@ -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;

View file

@ -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",