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