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
|
||||
|
||||
- mosaic layout
|
||||
- reverse filters to filter out/in
|
||||
- Albums: group by content type
|
||||
- Stats: top albums
|
||||
- Stats: open full top listings
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -41,6 +41,8 @@
|
|||
"chipActionGoToAlbumPage": "Afficher dans Albums",
|
||||
"chipActionGoToCountryPage": "Afficher dans Pays",
|
||||
"chipActionGoToTagPage": "Afficher dans Libellés",
|
||||
"chipActionFilterOut": "Exclure",
|
||||
"chipActionFilterIn": "Inclure",
|
||||
"chipActionHide": "Masquer",
|
||||
"chipActionPin": "Épingler",
|
||||
"chipActionUnpin": "Retirer",
|
||||
|
@ -173,6 +175,9 @@
|
|||
"wallpaperTargetLock": "Écran de verrouillage",
|
||||
"wallpaperTargetHomeLock": "Écrans accueil et verrouillage",
|
||||
|
||||
"widgetOpenPageHome": "Ouvrir la page d’accueil",
|
||||
"widgetOpenPageViewer": "Ouvrir la visionneuse",
|
||||
|
||||
"albumTierNew": "Nouveaux",
|
||||
"albumTierPinned": "Épinglés",
|
||||
"albumTierSpecial": "Standards",
|
||||
|
@ -403,9 +408,12 @@
|
|||
"sortOrderSmallestFirst": "Moins larges d’abord",
|
||||
|
||||
"albumGroupTier": "par importance",
|
||||
"albumGroupType": "par type",
|
||||
"albumGroupVolume": "par volume de stockage",
|
||||
"albumGroupNone": "ne pas grouper",
|
||||
|
||||
"albumMimeTypeMixed": "Mixte",
|
||||
|
||||
"albumPickPageTitleCopy": "Copie",
|
||||
"albumPickPageTitleExport": "Export",
|
||||
"albumPickPageTitleMove": "Déplacement",
|
||||
|
@ -614,6 +622,7 @@
|
|||
|
||||
"settingsWidgetPageTitle": "Cadre photo",
|
||||
"settingsWidgetShowOutline": "Contours",
|
||||
"settingsWidgetOpenPage": "Quand vous appuyez sur le widget",
|
||||
|
||||
"settingsCollectionTile": "Collection",
|
||||
|
||||
|
@ -622,6 +631,7 @@
|
|||
"statsTopCountriesSectionTitle": "Top pays",
|
||||
"statsTopPlacesSectionTitle": "Top lieux",
|
||||
"statsTopTagsSectionTitle": "Top libellés",
|
||||
"statsTopAlbumsSectionTitle": "Top albums",
|
||||
|
||||
"viewerOpenPanoramaButtonLabel": "OUVRIR LE PANORAMA",
|
||||
"viewerSetWallpaperButtonLabel": "APPLIQUER",
|
||||
|
|
|
@ -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": "설정",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -42,16 +42,11 @@ 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? fromJson(String jsonString) {
|
||||
if (jsonString.isEmpty) return null;
|
||||
|
||||
try {
|
||||
final jsonMap = jsonDecode(jsonString);
|
||||
if (jsonMap is Map<String, dynamic>) {
|
||||
static CollectionFilter? _fromMap(Map<String, dynamic> jsonMap) {
|
||||
final type = jsonMap['type'];
|
||||
switch (type) {
|
||||
case AlbumFilter.type:
|
||||
|
@ -61,7 +56,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
case DateFilter.type:
|
||||
return DateFilter.fromMap(jsonMap);
|
||||
case FavouriteFilter.type:
|
||||
return FavouriteFilter.instance;
|
||||
return FavouriteFilter.fromMap(jsonMap);
|
||||
case LocationFilter.type:
|
||||
return LocationFilter.fromMap(jsonMap);
|
||||
case MimeFilter.type:
|
||||
|
@ -75,14 +70,24 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
case RatingFilter.type:
|
||||
return RatingFilter.fromMap(jsonMap);
|
||||
case RecentlyAddedFilter.type:
|
||||
return RecentlyAddedFilter.instance;
|
||||
return RecentlyAddedFilter.fromMap(jsonMap);
|
||||
case TagFilter.type:
|
||||
return TagFilter.fromMap(jsonMap);
|
||||
case TypeFilter.type:
|
||||
return TypeFilter.fromMap(jsonMap);
|
||||
case TrashFilter.type:
|
||||
return TrashFilter.instance;
|
||||
return TrashFilter.fromMap(jsonMap);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static CollectionFilter? fromJson(String jsonString) {
|
||||
if (jsonString.isEmpty) return null;
|
||||
|
||||
try {
|
||||
final jsonMap = jsonDecode(jsonString);
|
||||
if (jsonMap is Map<String, dynamic>) {
|
||||
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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,11 +169,17 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
bottom: Column(
|
||||
children: [
|
||||
if (showFilterBar)
|
||||
FilterBar(
|
||||
NotificationListener<ReverseFilterNotification>(
|
||||
onNotification: (notification) {
|
||||
collection.addFilter(notification.reversedFilter);
|
||||
return true;
|
||||
},
|
||||
child: FilterBar(
|
||||
filters: visibleFilters,
|
||||
removable: removableFilters,
|
||||
onTap: removableFilters ? collection.removeFilter : null,
|
||||
),
|
||||
),
|
||||
if (queryEnabled)
|
||||
EntryQueryBar(
|
||||
queryNotifier: context.select<Query, ValueNotifier<String>>((query) => query.queryNotifier),
|
||||
|
|
|
@ -64,7 +64,7 @@ class _FilterBarState extends State<FilterBar> {
|
|||
),
|
||||
);
|
||||
}
|
||||
: (context, animation) => _buildChip(filter),
|
||||
: (context, animation) => const SizedBox(),
|
||||
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/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(
|
||||
...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: action.getText(context), icon: action.getIcon()),
|
||||
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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
if (notification is FilterSelectedNotification) {
|
||||
_goToCollection(notification.filter);
|
||||
} else if (notification is ReverseFilterNotification) {
|
||||
_goToCollection(notification.reversedFilter);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
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/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,6 +66,11 @@ class CollectionSearchDelegate extends AvesSearchDelegate {
|
|||
final upQuery = query.trim().toUpperCase();
|
||||
bool containQuery(String s) => s.toUpperCase().contains(upQuery);
|
||||
return SafeArea(
|
||||
child: NotificationListener<ReverseFilterNotification>(
|
||||
onNotification: (notification) {
|
||||
_select(context, notification.reversedFilter);
|
||||
return true;
|
||||
},
|
||||
child: ValueListenableBuilder<String?>(
|
||||
valueListenable: _expandedSectionNotifier,
|
||||
builder: (context, expandedSection, child) {
|
||||
|
@ -109,8 +115,11 @@ class CollectionSearchDelegate extends AvesSearchDelegate {
|
|||
_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(),
|
||||
);
|
||||
|
|
|
@ -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,7 +187,12 @@ class _StatsPageState extends State<StatsPage> {
|
|||
);
|
||||
final showRatings = _entryCountPerRating.entries.any((kv) => kv.key != 0 && kv.value > 0);
|
||||
final source = widget.source;
|
||||
child = AnimationLimiter(
|
||||
child = NotificationListener<ReverseFilterNotification>(
|
||||
onNotification: (notification) {
|
||||
_onFilterSelection(context, notification.reversedFilter);
|
||||
return true;
|
||||
},
|
||||
child: AnimationLimiter(
|
||||
child: ListView(
|
||||
children: AnimationConfiguration.toStaggeredList(
|
||||
duration: durations.staggeredAnimation,
|
||||
|
@ -212,6 +219,7 @@ class _StatsPageState extends State<StatsPage> {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
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),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
|
|
@ -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,7 +231,12 @@ class _InfoPageContentState extends State<_InfoPageContent> {
|
|||
metadataNotifier: _metadataNotifier,
|
||||
);
|
||||
|
||||
return CustomScrollView(
|
||||
return NotificationListener<ReverseFilterNotification>(
|
||||
onNotification: (notification) {
|
||||
_onFilter(notification.reversedFilter);
|
||||
return true;
|
||||
},
|
||||
child: CustomScrollView(
|
||||
controller: widget.scrollController,
|
||||
slivers: [
|
||||
InfoAppBar(
|
||||
|
@ -249,6 +255,7 @@ class _InfoPageContentState extends State<_InfoPageContent> {
|
|||
),
|
||||
const BottomPaddingSliver(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue