#143 rating: sort/group/filter
This commit is contained in:
parent
039983b8f7
commit
713ef3d782
24 changed files with 203 additions and 58 deletions
|
@ -480,15 +480,15 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
xmpMeta.getSafeInt(XMP.XMP_SCHEMA_NS, XMP.XMP_RATING_PROP_NAME) { if (it in RATING_RANGE) metadataMap[KEY_RATING] = it }
|
xmpMeta.getSafeInt(XMP.XMP_SCHEMA_NS, XMP.XMP_RATING_PROP_NAME) { metadataMap[KEY_RATING] = it }
|
||||||
if (!metadataMap.containsKey(KEY_RATING)) {
|
if (!metadataMap.containsKey(KEY_RATING)) {
|
||||||
xmpMeta.getSafeInt(XMP.MICROSOFTPHOTO_SCHEMA_NS, XMP.MS_RATING_PROP_NAME) { percentRating ->
|
xmpMeta.getSafeInt(XMP.MICROSOFTPHOTO_SCHEMA_NS, XMP.MS_RATING_PROP_NAME) { percentRating ->
|
||||||
// values of 1,25,50,75,99% correspond to 1,2,3,4,5 stars
|
// values of 1,25,50,75,99% correspond to 1,2,3,4,5 stars
|
||||||
val standardRating = (percentRating / 25f).roundToInt() + 1
|
val standardRating = (percentRating / 25f).roundToInt() + 1
|
||||||
if (standardRating in RATING_RANGE) metadataMap[KEY_RATING] = standardRating
|
metadataMap[KEY_RATING] = standardRating
|
||||||
}
|
}
|
||||||
if (!metadataMap.containsKey(KEY_RATING)) {
|
if (!metadataMap.containsKey(KEY_RATING)) {
|
||||||
xmpMeta.getSafeInt(XMP.ACDSEE_SCHEMA_NS, XMP.ACDSEE_RATING_PROP_NAME) { if (it in RATING_RANGE) metadataMap[KEY_RATING] = it }
|
xmpMeta.getSafeInt(XMP.ACDSEE_SCHEMA_NS, XMP.ACDSEE_RATING_PROP_NAME) { metadataMap[KEY_RATING] = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -991,7 +991,6 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
private const val MASK_IS_360 = 1 shl 3
|
private const val MASK_IS_360 = 1 shl 3
|
||||||
private const val MASK_IS_MULTIPAGE = 1 shl 4
|
private const val MASK_IS_MULTIPAGE = 1 shl 4
|
||||||
private const val XMP_SUBJECTS_SEPARATOR = ";"
|
private const val XMP_SUBJECTS_SEPARATOR = ";"
|
||||||
private val RATING_RANGE = 1..5
|
|
||||||
|
|
||||||
// overlay metadata
|
// overlay metadata
|
||||||
private const val KEY_APERTURE = "aperture"
|
private const val KEY_APERTURE = "aperture"
|
||||||
|
|
|
@ -94,6 +94,8 @@
|
||||||
"filterFavouriteLabel": "Favourite",
|
"filterFavouriteLabel": "Favourite",
|
||||||
"filterLocationEmptyLabel": "Unlocated",
|
"filterLocationEmptyLabel": "Unlocated",
|
||||||
"filterTagEmptyLabel": "Untagged",
|
"filterTagEmptyLabel": "Untagged",
|
||||||
|
"filterRatingUnratedLabel": "Unrated",
|
||||||
|
"filterRatingRejectedLabel": "Rejected",
|
||||||
"filterTypeAnimatedLabel": "Animated",
|
"filterTypeAnimatedLabel": "Animated",
|
||||||
"filterTypeMotionPhotoLabel": "Motion Photo",
|
"filterTypeMotionPhotoLabel": "Motion Photo",
|
||||||
"filterTypePanoramaLabel": "Panorama",
|
"filterTypePanoramaLabel": "Panorama",
|
||||||
|
@ -381,6 +383,7 @@
|
||||||
"collectionSortDate": "By date",
|
"collectionSortDate": "By date",
|
||||||
"collectionSortSize": "By size",
|
"collectionSortSize": "By size",
|
||||||
"collectionSortName": "By album & file name",
|
"collectionSortName": "By album & file name",
|
||||||
|
"collectionSortRating": "By rating",
|
||||||
|
|
||||||
"collectionGroupAlbum": "By album",
|
"collectionGroupAlbum": "By album",
|
||||||
"collectionGroupMonth": "By month",
|
"collectionGroupMonth": "By month",
|
||||||
|
@ -494,6 +497,7 @@
|
||||||
"searchSectionCountries": "Countries",
|
"searchSectionCountries": "Countries",
|
||||||
"searchSectionPlaces": "Places",
|
"searchSectionPlaces": "Places",
|
||||||
"searchSectionTags": "Tags",
|
"searchSectionTags": "Tags",
|
||||||
|
"searchSectionRating": "Ratings",
|
||||||
|
|
||||||
"settingsPageTitle": "Settings",
|
"settingsPageTitle": "Settings",
|
||||||
"settingsSystemDefault": "System",
|
"settingsSystemDefault": "System",
|
||||||
|
|
|
@ -79,6 +79,8 @@
|
||||||
"filterFavouriteLabel": "Favori",
|
"filterFavouriteLabel": "Favori",
|
||||||
"filterLocationEmptyLabel": "Sans lieu",
|
"filterLocationEmptyLabel": "Sans lieu",
|
||||||
"filterTagEmptyLabel": "Sans libellé",
|
"filterTagEmptyLabel": "Sans libellé",
|
||||||
|
"filterRatingUnratedLabel": "Sans notation",
|
||||||
|
"filterRatingRejectedLabel": "Rejeté",
|
||||||
"filterTypeAnimatedLabel": "Animation",
|
"filterTypeAnimatedLabel": "Animation",
|
||||||
"filterTypeMotionPhotoLabel": "Photo animée",
|
"filterTypeMotionPhotoLabel": "Photo animée",
|
||||||
"filterTypePanoramaLabel": "Panorama",
|
"filterTypePanoramaLabel": "Panorama",
|
||||||
|
@ -273,6 +275,7 @@
|
||||||
"collectionSortDate": "par date",
|
"collectionSortDate": "par date",
|
||||||
"collectionSortSize": "par taille",
|
"collectionSortSize": "par taille",
|
||||||
"collectionSortName": "alphabétique",
|
"collectionSortName": "alphabétique",
|
||||||
|
"collectionSortRating": "par notation",
|
||||||
|
|
||||||
"collectionGroupAlbum": "par album",
|
"collectionGroupAlbum": "par album",
|
||||||
"collectionGroupMonth": "par mois",
|
"collectionGroupMonth": "par mois",
|
||||||
|
@ -346,6 +349,7 @@
|
||||||
"searchSectionCountries": "Pays",
|
"searchSectionCountries": "Pays",
|
||||||
"searchSectionPlaces": "Lieux",
|
"searchSectionPlaces": "Lieux",
|
||||||
"searchSectionTags": "Libellés",
|
"searchSectionTags": "Libellés",
|
||||||
|
"searchSectionRating": "Notations",
|
||||||
|
|
||||||
"settingsPageTitle": "Réglages",
|
"settingsPageTitle": "Réglages",
|
||||||
"settingsSystemDefault": "Système",
|
"settingsSystemDefault": "Système",
|
||||||
|
|
|
@ -79,6 +79,8 @@
|
||||||
"filterFavouriteLabel": "즐겨찾기",
|
"filterFavouriteLabel": "즐겨찾기",
|
||||||
"filterLocationEmptyLabel": "장소 없음",
|
"filterLocationEmptyLabel": "장소 없음",
|
||||||
"filterTagEmptyLabel": "태그 없음",
|
"filterTagEmptyLabel": "태그 없음",
|
||||||
|
"filterRatingUnratedLabel": "별점 없음",
|
||||||
|
"filterRatingRejectedLabel": "거부됨",
|
||||||
"filterTypeAnimatedLabel": "애니메이션",
|
"filterTypeAnimatedLabel": "애니메이션",
|
||||||
"filterTypeMotionPhotoLabel": "모션 포토",
|
"filterTypeMotionPhotoLabel": "모션 포토",
|
||||||
"filterTypePanoramaLabel": "파노라마",
|
"filterTypePanoramaLabel": "파노라마",
|
||||||
|
@ -273,6 +275,7 @@
|
||||||
"collectionSortDate": "날짜",
|
"collectionSortDate": "날짜",
|
||||||
"collectionSortSize": "크기",
|
"collectionSortSize": "크기",
|
||||||
"collectionSortName": "이름",
|
"collectionSortName": "이름",
|
||||||
|
"collectionSortRating": "별점",
|
||||||
|
|
||||||
"collectionGroupAlbum": "앨범별로",
|
"collectionGroupAlbum": "앨범별로",
|
||||||
"collectionGroupMonth": "월별로",
|
"collectionGroupMonth": "월별로",
|
||||||
|
@ -346,6 +349,7 @@
|
||||||
"searchSectionCountries": "국가",
|
"searchSectionCountries": "국가",
|
||||||
"searchSectionPlaces": "장소",
|
"searchSectionPlaces": "장소",
|
||||||
"searchSectionTags": "태그",
|
"searchSectionTags": "태그",
|
||||||
|
"searchSectionRating": "별점",
|
||||||
|
|
||||||
"settingsPageTitle": "설정",
|
"settingsPageTitle": "설정",
|
||||||
"settingsSystemDefault": "시스템",
|
"settingsSystemDefault": "시스템",
|
||||||
|
|
|
@ -361,7 +361,7 @@ class AvesEntry {
|
||||||
return _bestDate;
|
return _bestDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
int? get rating => _catalogMetadata?.rating;
|
int get rating => _catalogMetadata?.rating ?? 0;
|
||||||
|
|
||||||
int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees;
|
int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees;
|
||||||
|
|
||||||
|
@ -861,14 +861,6 @@ class AvesEntry {
|
||||||
return c != 0 ? c : compareAsciiUpperCase(a.extension ?? '', b.extension ?? '');
|
return c != 0 ? c : compareAsciiUpperCase(a.extension ?? '', b.extension ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// compare by:
|
|
||||||
// 1) size descending
|
|
||||||
// 2) name ascending
|
|
||||||
static int compareBySize(AvesEntry a, AvesEntry b) {
|
|
||||||
final c = (b.sizeBytes ?? 0).compareTo(a.sizeBytes ?? 0);
|
|
||||||
return c != 0 ? c : compareByName(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
static final _epoch = DateTime.fromMillisecondsSinceEpoch(0);
|
static final _epoch = DateTime.fromMillisecondsSinceEpoch(0);
|
||||||
|
|
||||||
// compare by:
|
// compare by:
|
||||||
|
@ -879,4 +871,20 @@ class AvesEntry {
|
||||||
if (c != 0) return c;
|
if (c != 0) return c;
|
||||||
return compareByName(b, a);
|
return compareByName(b, a);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compare by:
|
||||||
|
// 1) rating descending
|
||||||
|
// 2) date descending
|
||||||
|
static int compareByRating(AvesEntry a, AvesEntry b) {
|
||||||
|
final c = b.rating.compareTo(a.rating);
|
||||||
|
return c != 0 ? c : compareByDate(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare by:
|
||||||
|
// 1) size descending
|
||||||
|
// 2) date descending
|
||||||
|
static int compareBySize(AvesEntry a, AvesEntry b) {
|
||||||
|
final c = (b.sizeBytes ?? 0).compareTo(a.sizeBytes ?? 0);
|
||||||
|
return c != 0 ? c : compareByDate(a, b);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import 'package:aves/model/filters/location.dart';
|
||||||
import 'package:aves/model/filters/mime.dart';
|
import 'package:aves/model/filters/mime.dart';
|
||||||
import 'package:aves/model/filters/path.dart';
|
import 'package:aves/model/filters/path.dart';
|
||||||
import 'package:aves/model/filters/query.dart';
|
import 'package:aves/model/filters/query.dart';
|
||||||
|
import 'package:aves/model/filters/rating.dart';
|
||||||
import 'package:aves/model/filters/tag.dart';
|
import 'package:aves/model/filters/tag.dart';
|
||||||
import 'package:aves/model/filters/type.dart';
|
import 'package:aves/model/filters/type.dart';
|
||||||
import 'package:aves/utils/color_utils.dart';
|
import 'package:aves/utils/color_utils.dart';
|
||||||
|
@ -26,6 +27,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
||||||
AlbumFilter.type,
|
AlbumFilter.type,
|
||||||
LocationFilter.type,
|
LocationFilter.type,
|
||||||
CoordinateFilter.type,
|
CoordinateFilter.type,
|
||||||
|
RatingFilter.type,
|
||||||
TagFilter.type,
|
TagFilter.type,
|
||||||
PathFilter.type,
|
PathFilter.type,
|
||||||
];
|
];
|
||||||
|
@ -52,6 +54,8 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
||||||
return PathFilter.fromMap(jsonMap);
|
return PathFilter.fromMap(jsonMap);
|
||||||
case QueryFilter.type:
|
case QueryFilter.type:
|
||||||
return QueryFilter.fromMap(jsonMap);
|
return QueryFilter.fromMap(jsonMap);
|
||||||
|
case RatingFilter.type:
|
||||||
|
return RatingFilter.fromMap(jsonMap);
|
||||||
case TagFilter.type:
|
case TagFilter.type:
|
||||||
return TagFilter.fromMap(jsonMap);
|
return TagFilter.fromMap(jsonMap);
|
||||||
case TypeFilter.type:
|
case TypeFilter.type:
|
||||||
|
|
64
lib/model/filters/rating.dart
Normal file
64
lib/model/filters/rating.dart
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class RatingFilter extends CollectionFilter {
|
||||||
|
static const type = 'rating';
|
||||||
|
|
||||||
|
final int rating;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [rating];
|
||||||
|
|
||||||
|
const RatingFilter(this.rating);
|
||||||
|
|
||||||
|
RatingFilter.fromMap(Map<String, dynamic> json)
|
||||||
|
: this(
|
||||||
|
json['rating'] ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toMap() => {
|
||||||
|
'type': type,
|
||||||
|
'rating': rating,
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
EntryFilter get test => (entry) => entry.rating == rating;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get universalLabel => '$rating';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getLabel(BuildContext context) => formatRating(context, rating);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
|
||||||
|
switch (rating) {
|
||||||
|
case -1:
|
||||||
|
return Icon(AIcons.ratingRejected, size: size);
|
||||||
|
case 0:
|
||||||
|
return Icon(AIcons.ratingUnrated, size: size);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get category => type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => '$type-$rating';
|
||||||
|
|
||||||
|
static String formatRating(BuildContext context, int rating) {
|
||||||
|
switch (rating) {
|
||||||
|
case -1:
|
||||||
|
return context.l10n.filterRatingRejectedLabel;
|
||||||
|
case 0:
|
||||||
|
return context.l10n.filterRatingUnratedLabel;
|
||||||
|
default:
|
||||||
|
return '\u2B50' * rating;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,10 +5,11 @@ class CatalogMetadata {
|
||||||
final int? contentId, dateMillis;
|
final int? contentId, dateMillis;
|
||||||
final bool isAnimated, isGeotiff, is360, isMultiPage;
|
final bool isAnimated, isGeotiff, is360, isMultiPage;
|
||||||
bool isFlipped;
|
bool isFlipped;
|
||||||
int? rating, rotationDegrees;
|
int? rotationDegrees;
|
||||||
final String? mimeType, xmpSubjects, xmpTitleDescription;
|
final String? mimeType, xmpSubjects, xmpTitleDescription;
|
||||||
double? latitude, longitude;
|
double? latitude, longitude;
|
||||||
Address? address;
|
Address? address;
|
||||||
|
int rating;
|
||||||
|
|
||||||
static const double _precisionErrorTolerance = 1e-9;
|
static const double _precisionErrorTolerance = 1e-9;
|
||||||
static const _isAnimatedMask = 1 << 0;
|
static const _isAnimatedMask = 1 << 0;
|
||||||
|
@ -31,7 +32,7 @@ class CatalogMetadata {
|
||||||
this.xmpTitleDescription,
|
this.xmpTitleDescription,
|
||||||
double? latitude,
|
double? latitude,
|
||||||
double? longitude,
|
double? longitude,
|
||||||
this.rating,
|
this.rating = 0,
|
||||||
}) {
|
}) {
|
||||||
// Geocoder throws an `IllegalArgumentException` when a coordinate has a funky value like `1.7056881853375E7`
|
// Geocoder throws an `IllegalArgumentException` when a coordinate has a funky value like `1.7056881853375E7`
|
||||||
// We also exclude zero coordinates, taking into account precision errors (e.g. {5.952380952380953e-11,-2.7777777777777777e-10}),
|
// We also exclude zero coordinates, taking into account precision errors (e.g. {5.952380952380953e-11,-2.7777777777777777e-10}),
|
||||||
|
@ -89,8 +90,7 @@ class CatalogMetadata {
|
||||||
xmpTitleDescription: map['xmpTitleDescription'] ?? '',
|
xmpTitleDescription: map['xmpTitleDescription'] ?? '',
|
||||||
latitude: map['latitude'],
|
latitude: map['latitude'],
|
||||||
longitude: map['longitude'],
|
longitude: map['longitude'],
|
||||||
// `rotationDegrees` should default to `null`, not 0
|
rating: map['rating'] ?? 0,
|
||||||
rating: map['rating'],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import 'package:aves/model/filters/favourite.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/location.dart';
|
import 'package:aves/model/filters/location.dart';
|
||||||
import 'package:aves/model/filters/query.dart';
|
import 'package:aves/model/filters/query.dart';
|
||||||
|
import 'package:aves/model/filters/rating.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/events.dart';
|
import 'package:aves/model/source/events.dart';
|
||||||
|
@ -108,15 +109,27 @@ class CollectionLens with ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get showHeaders {
|
bool get showHeaders {
|
||||||
if (sortFactor == EntrySortFactor.size) return false;
|
bool showAlbumHeaders() => !filters.any((f) => f is AlbumFilter);
|
||||||
|
|
||||||
if (sortFactor == EntrySortFactor.date && sectionFactor == EntryGroupFactor.none) return false;
|
switch (sortFactor) {
|
||||||
|
case EntrySortFactor.date:
|
||||||
final albumSections = sortFactor == EntrySortFactor.name || (sortFactor == EntrySortFactor.date && sectionFactor == EntryGroupFactor.album);
|
switch (sectionFactor) {
|
||||||
final filterByAlbum = filters.any((f) => f is AlbumFilter);
|
case EntryGroupFactor.none:
|
||||||
if (albumSections && filterByAlbum) return false;
|
return false;
|
||||||
|
case EntryGroupFactor.album:
|
||||||
return true;
|
return showAlbumHeaders();
|
||||||
|
case EntryGroupFactor.month:
|
||||||
|
return true;
|
||||||
|
case EntryGroupFactor.day:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case EntrySortFactor.name:
|
||||||
|
return showAlbumHeaders();
|
||||||
|
case EntrySortFactor.rating:
|
||||||
|
return !filters.any((f) => f is RatingFilter);
|
||||||
|
case EntrySortFactor.size:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void addFilter(CollectionFilter filter) {
|
void addFilter(CollectionFilter filter) {
|
||||||
|
@ -181,12 +194,15 @@ class CollectionLens with ChangeNotifier {
|
||||||
case EntrySortFactor.date:
|
case EntrySortFactor.date:
|
||||||
_filteredSortedEntries.sort(AvesEntry.compareByDate);
|
_filteredSortedEntries.sort(AvesEntry.compareByDate);
|
||||||
break;
|
break;
|
||||||
case EntrySortFactor.size:
|
|
||||||
_filteredSortedEntries.sort(AvesEntry.compareBySize);
|
|
||||||
break;
|
|
||||||
case EntrySortFactor.name:
|
case EntrySortFactor.name:
|
||||||
_filteredSortedEntries.sort(AvesEntry.compareByName);
|
_filteredSortedEntries.sort(AvesEntry.compareByName);
|
||||||
break;
|
break;
|
||||||
|
case EntrySortFactor.rating:
|
||||||
|
_filteredSortedEntries.sort(AvesEntry.compareByRating);
|
||||||
|
break;
|
||||||
|
case EntrySortFactor.size:
|
||||||
|
_filteredSortedEntries.sort(AvesEntry.compareBySize);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,15 +226,18 @@ class CollectionLens with ChangeNotifier {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case EntrySortFactor.name:
|
||||||
|
final byAlbum = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
|
||||||
|
sections = SplayTreeMap<EntryAlbumSectionKey, List<AvesEntry>>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.directory!, b.directory!));
|
||||||
|
break;
|
||||||
|
case EntrySortFactor.rating:
|
||||||
|
sections = groupBy<AvesEntry, EntryRatingSectionKey>(_filteredSortedEntries, (entry) => EntryRatingSectionKey(entry.rating));
|
||||||
|
break;
|
||||||
case EntrySortFactor.size:
|
case EntrySortFactor.size:
|
||||||
sections = Map.fromEntries([
|
sections = Map.fromEntries([
|
||||||
MapEntry(const SectionKey(), _filteredSortedEntries),
|
MapEntry(const SectionKey(), _filteredSortedEntries),
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
case EntrySortFactor.name:
|
|
||||||
final byAlbum = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
|
|
||||||
sections = SplayTreeMap<EntryAlbumSectionKey, List<AvesEntry>>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.directory!, b.directory!));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
sections = Map.unmodifiable(sections);
|
sections = Map.unmodifiable(sections);
|
||||||
_sortedEntries = null;
|
_sortedEntries = null;
|
||||||
|
|
|
@ -4,7 +4,7 @@ enum ChipSortFactor { date, name, count }
|
||||||
|
|
||||||
enum AlbumChipGroupFactor { none, importance, volume }
|
enum AlbumChipGroupFactor { none, importance, volume }
|
||||||
|
|
||||||
enum EntrySortFactor { date, size, name }
|
enum EntrySortFactor { date, name, rating, size }
|
||||||
|
|
||||||
enum EntryGroupFactor { none, album, month, day }
|
enum EntryGroupFactor { none, album, month, day }
|
||||||
|
|
||||||
|
|
|
@ -23,3 +23,12 @@ class EntryDateSectionKey extends SectionKey with EquatableMixin {
|
||||||
|
|
||||||
const EntryDateSectionKey(this.date);
|
const EntryDateSectionKey(this.date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EntryRatingSectionKey extends SectionKey with EquatableMixin {
|
||||||
|
final int rating;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [rating];
|
||||||
|
|
||||||
|
const EntryRatingSectionKey(this.rating);
|
||||||
|
}
|
||||||
|
|
|
@ -66,7 +66,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
||||||
// 'dateMillis': date taken in milliseconds since Epoch (long)
|
// 'dateMillis': date taken in milliseconds since Epoch (long)
|
||||||
// 'isAnimated': animated gif/webp (bool)
|
// 'isAnimated': animated gif/webp (bool)
|
||||||
// 'isFlipped': flipped according to EXIF orientation (bool)
|
// 'isFlipped': flipped according to EXIF orientation (bool)
|
||||||
// 'rating': rating in [1,5] (int)
|
// 'rating': rating in [-1,5] (int)
|
||||||
// 'rotationDegrees': rotation degrees according to EXIF orientation or other metadata (int)
|
// 'rotationDegrees': rotation degrees according to EXIF orientation or other metadata (int)
|
||||||
// 'latitude': latitude (double)
|
// 'latitude': latitude (double)
|
||||||
// 'longitude': longitude (double)
|
// 'longitude': longitude (double)
|
||||||
|
|
|
@ -22,6 +22,8 @@ class AIcons {
|
||||||
static const IconData locationOff = Icons.location_off_outlined;
|
static const IconData locationOff = Icons.location_off_outlined;
|
||||||
static const IconData mainStorage = Icons.smartphone_outlined;
|
static const IconData mainStorage = Icons.smartphone_outlined;
|
||||||
static const IconData privacy = MdiIcons.shieldAccountOutline;
|
static const IconData privacy = MdiIcons.shieldAccountOutline;
|
||||||
|
static const IconData ratingRejected = MdiIcons.starRemoveOutline;
|
||||||
|
static const IconData ratingUnrated = MdiIcons.starOffOutline;
|
||||||
static const IconData raw = Icons.raw_on_outlined;
|
static const IconData raw = Icons.raw_on_outlined;
|
||||||
static const IconData shooting = Icons.camera_outlined;
|
static const IconData shooting = Icons.camera_outlined;
|
||||||
static const IconData removableStorage = Icons.sd_storage_outlined;
|
static const IconData removableStorage = Icons.sd_storage_outlined;
|
||||||
|
|
|
@ -210,7 +210,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
action,
|
action,
|
||||||
appMode: appMode,
|
appMode: appMode,
|
||||||
isSelecting: isSelecting,
|
isSelecting: isSelecting,
|
||||||
sortFactor: collection.sortFactor,
|
|
||||||
itemCount: collection.entryCount,
|
itemCount: collection.entryCount,
|
||||||
selectedItemCount: selectedItemCount,
|
selectedItemCount: selectedItemCount,
|
||||||
);
|
);
|
||||||
|
@ -448,6 +447,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
EntrySortFactor.date: l10n.collectionSortDate,
|
EntrySortFactor.date: l10n.collectionSortDate,
|
||||||
EntrySortFactor.size: l10n.collectionSortSize,
|
EntrySortFactor.size: l10n.collectionSortSize,
|
||||||
EntrySortFactor.name: l10n.collectionSortName,
|
EntrySortFactor.name: l10n.collectionSortName,
|
||||||
|
EntrySortFactor.rating: l10n.collectionSortRating,
|
||||||
},
|
},
|
||||||
groupOptions: {
|
groupOptions: {
|
||||||
EntryGroupFactor.album: l10n.collectionGroupAlbum,
|
EntryGroupFactor.album: l10n.collectionGroupAlbum,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/model/filters/rating.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/enums.dart';
|
import 'package:aves/model/source/enums.dart';
|
||||||
|
@ -47,6 +48,11 @@ class CollectionDraggableThumbLabel extends StatelessWidget {
|
||||||
if (_showAlbumName(context, entry)) _getAlbumName(context, entry),
|
if (_showAlbumName(context, entry)) _getAlbumName(context, entry),
|
||||||
if (entry.bestTitle != null) entry.bestTitle!,
|
if (entry.bestTitle != null) entry.bestTitle!,
|
||||||
];
|
];
|
||||||
|
case EntrySortFactor.rating:
|
||||||
|
return [
|
||||||
|
RatingFilter.formatRating(context, entry.rating),
|
||||||
|
DraggableThumbLabel.formatMonthThumbLabel(context, entry.bestDate),
|
||||||
|
];
|
||||||
case EntrySortFactor.size:
|
case EntrySortFactor.size:
|
||||||
return [
|
return [
|
||||||
if (entry.sizeBytes != null) formatFileSize(context.l10n.localeName, entry.sizeBytes!, round: 0),
|
if (entry.sizeBytes != null) formatFileSize(context.l10n.localeName, entry.sizeBytes!, round: 0),
|
||||||
|
|
|
@ -15,7 +15,6 @@ import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/model/source/analysis_controller.dart';
|
import 'package:aves/model/source/analysis_controller.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/enums.dart';
|
|
||||||
import 'package:aves/services/common/image_op_events.dart';
|
import 'package:aves/services/common/image_op_events.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/services/media/enums.dart';
|
import 'package:aves/services/media/enums.dart';
|
||||||
|
@ -44,7 +43,6 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
EntrySetAction action, {
|
EntrySetAction action, {
|
||||||
required AppMode appMode,
|
required AppMode appMode,
|
||||||
required bool isSelecting,
|
required bool isSelecting,
|
||||||
required EntrySortFactor sortFactor,
|
|
||||||
required int itemCount,
|
required int itemCount,
|
||||||
required int selectedItemCount,
|
required int selectedItemCount,
|
||||||
}) {
|
}) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:aves/model/source/enums.dart';
|
||||||
import 'package:aves/model/source/section_keys.dart';
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
import 'package:aves/widgets/collection/grid/headers/album.dart';
|
import 'package:aves/widgets/collection/grid/headers/album.dart';
|
||||||
import 'package:aves/widgets/collection/grid/headers/date.dart';
|
import 'package:aves/widgets/collection/grid/headers/date.dart';
|
||||||
|
import 'package:aves/widgets/collection/grid/headers/rating.dart';
|
||||||
import 'package:aves/widgets/common/grid/header.dart';
|
import 'package:aves/widgets/common/grid/header.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@ -49,6 +50,8 @@ class CollectionSectionHeader extends StatelessWidget {
|
||||||
break;
|
break;
|
||||||
case EntrySortFactor.name:
|
case EntrySortFactor.name:
|
||||||
return _buildAlbumHeader(context);
|
return _buildAlbumHeader(context);
|
||||||
|
case EntrySortFactor.rating:
|
||||||
|
return RatingSectionHeader<AvesEntry>(key: ValueKey(sectionKey), rating: (sectionKey as EntryRatingSectionKey).rating);
|
||||||
case EntrySortFactor.size:
|
case EntrySortFactor.size:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
21
lib/widgets/collection/grid/headers/rating.dart
Normal file
21
lib/widgets/collection/grid/headers/rating.dart
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import 'package:aves/model/filters/rating.dart';
|
||||||
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/header.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class RatingSectionHeader<T> extends StatelessWidget {
|
||||||
|
final int rating;
|
||||||
|
|
||||||
|
const RatingSectionHeader({
|
||||||
|
Key? key,
|
||||||
|
required this.rating,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SectionHeader<T>(
|
||||||
|
sectionKey: EntryRatingSectionKey(rating),
|
||||||
|
title: RatingFilter.formatRating(context, rating),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -316,7 +316,6 @@ class _ScaleOverlayState extends State<_ScaleOverlay> {
|
||||||
colors: const [
|
colors: const [
|
||||||
Colors.black,
|
Colors.black,
|
||||||
Colors.black54,
|
Colors.black54,
|
||||||
// Colors.amber,
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -25,7 +25,7 @@ class ThumbnailEntryOverlay extends StatelessWidget {
|
||||||
else if (entry.isAnimated)
|
else if (entry.isAnimated)
|
||||||
const AnimatedImageIcon()
|
const AnimatedImageIcon()
|
||||||
else ...[
|
else ...[
|
||||||
if (entry.rating != null && context.select<GridThemeData, bool>((t) => t.showRating)) RatingIcon(entry: entry),
|
if (entry.rating != 0 && context.select<GridThemeData, bool>((t) => t.showRating)) RatingIcon(entry: entry),
|
||||||
if (entry.isRaw && context.select<GridThemeData, bool>((t) => t.showRaw)) const RawIcon(),
|
if (entry.isRaw && context.select<GridThemeData, bool>((t) => t.showRaw)) const RawIcon(),
|
||||||
if (entry.isGeotiff) const GeotiffIcon(),
|
if (entry.isGeotiff) const GeotiffIcon(),
|
||||||
if (entry.is360) const SphericalImageIcon(),
|
if (entry.is360) const SphericalImageIcon(),
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/location.dart';
|
import 'package:aves/model/filters/location.dart';
|
||||||
import 'package:aves/model/filters/mime.dart';
|
import 'package:aves/model/filters/mime.dart';
|
||||||
import 'package:aves/model/filters/query.dart';
|
import 'package:aves/model/filters/query.dart';
|
||||||
|
import 'package:aves/model/filters/rating.dart';
|
||||||
import 'package:aves/model/filters/tag.dart';
|
import 'package:aves/model/filters/tag.dart';
|
||||||
import 'package:aves/model/filters/type.dart';
|
import 'package:aves/model/filters/type.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
@ -179,6 +180,11 @@ class CollectionSearchDelegate {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
_buildFilterRow(
|
||||||
|
context: context,
|
||||||
|
title: context.l10n.searchSectionRating,
|
||||||
|
filters: [0, -1, 5, 4, 3, 2, 1].map((rating) => RatingFilter(rating)).toList(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/favourite.dart';
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
import 'package:aves/model/filters/mime.dart';
|
import 'package:aves/model/filters/mime.dart';
|
||||||
|
import 'package:aves/model/filters/rating.dart';
|
||||||
import 'package:aves/model/filters/tag.dart';
|
import 'package:aves/model/filters/tag.dart';
|
||||||
import 'package:aves/model/filters/type.dart';
|
import 'package:aves/model/filters/type.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
|
@ -75,31 +76,12 @@ class BasicSection extends StatelessWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
OwnerProp(entry: entry),
|
OwnerProp(entry: entry),
|
||||||
_buildRatingRow(),
|
|
||||||
_buildChips(context),
|
_buildChips(context),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRatingRow() {
|
|
||||||
final rating = entry.rating;
|
|
||||||
return rating != null
|
|
||||||
? Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: Row(
|
|
||||||
children: List.generate(
|
|
||||||
5,
|
|
||||||
(i) => Icon(
|
|
||||||
Icons.star,
|
|
||||||
color: rating > i ? Colors.amber : Colors.grey[800],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildChips(BuildContext context) {
|
Widget _buildChips(BuildContext context) {
|
||||||
final tags = entry.tags.toList()..sort(compareAsciiUpperCase);
|
final tags = entry.tags.toList()..sort(compareAsciiUpperCase);
|
||||||
final album = entry.directory;
|
final album = entry.directory;
|
||||||
|
@ -113,6 +95,7 @@ class BasicSection extends StatelessWidget {
|
||||||
if (entry.isVideo && entry.is360) TypeFilter.sphericalVideo,
|
if (entry.isVideo && entry.is360) TypeFilter.sphericalVideo,
|
||||||
if (entry.isVideo && !entry.is360) MimeFilter.video,
|
if (entry.isVideo && !entry.is360) MimeFilter.video,
|
||||||
if (album != null) AlbumFilter(album, collection?.source.getAlbumDisplayName(context, album)),
|
if (album != null) AlbumFilter(album, collection?.source.getAlbumDisplayName(context, album)),
|
||||||
|
if (entry.rating != 0) RatingFilter(entry.rating),
|
||||||
...tags.map((tag) => TagFilter(tag)),
|
...tags.map((tag) => TagFilter(tag)),
|
||||||
};
|
};
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
|
|
|
@ -6,6 +6,7 @@ import 'package:aves/model/filters/location.dart';
|
||||||
import 'package:aves/model/filters/mime.dart';
|
import 'package:aves/model/filters/mime.dart';
|
||||||
import 'package:aves/model/filters/path.dart';
|
import 'package:aves/model/filters/path.dart';
|
||||||
import 'package:aves/model/filters/query.dart';
|
import 'package:aves/model/filters/query.dart';
|
||||||
|
import 'package:aves/model/filters/rating.dart';
|
||||||
import 'package:aves/model/filters/tag.dart';
|
import 'package:aves/model/filters/tag.dart';
|
||||||
import 'package:aves/model/filters/type.dart';
|
import 'package:aves/model/filters/type.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
|
@ -50,6 +51,9 @@ void main() {
|
||||||
final query = QueryFilter('some query');
|
final query = QueryFilter('some query');
|
||||||
expect(query, jsonRoundTrip(query));
|
expect(query, jsonRoundTrip(query));
|
||||||
|
|
||||||
|
const rating = RatingFilter(3);
|
||||||
|
expect(rating, jsonRoundTrip(rating));
|
||||||
|
|
||||||
final tag = TagFilter('some tag');
|
final tag = TagFilter('some tag');
|
||||||
expect(tag, jsonRoundTrip(tag));
|
expect(tag, jsonRoundTrip(tag));
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
{
|
{
|
||||||
"de": [
|
"de": [
|
||||||
|
"filterRatingUnratedLabel",
|
||||||
|
"filterRatingRejectedLabel",
|
||||||
"editEntryDateDialogSourceFieldLabel",
|
"editEntryDateDialogSourceFieldLabel",
|
||||||
"editEntryDateDialogSourceCustomDate",
|
"editEntryDateDialogSourceCustomDate",
|
||||||
"editEntryDateDialogSourceTitle",
|
"editEntryDateDialogSourceTitle",
|
||||||
"editEntryDateDialogSourceFileModifiedDate",
|
"editEntryDateDialogSourceFileModifiedDate",
|
||||||
"editEntryDateDialogTargetFieldsHeader",
|
"editEntryDateDialogTargetFieldsHeader",
|
||||||
|
"collectionSortRating",
|
||||||
|
"searchSectionRating",
|
||||||
"settingsThumbnailShowRatingIcon"
|
"settingsThumbnailShowRatingIcon"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -17,11 +21,15 @@
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
|
"filterRatingUnratedLabel",
|
||||||
|
"filterRatingRejectedLabel",
|
||||||
"editEntryDateDialogSourceFieldLabel",
|
"editEntryDateDialogSourceFieldLabel",
|
||||||
"editEntryDateDialogSourceCustomDate",
|
"editEntryDateDialogSourceCustomDate",
|
||||||
"editEntryDateDialogSourceTitle",
|
"editEntryDateDialogSourceTitle",
|
||||||
"editEntryDateDialogSourceFileModifiedDate",
|
"editEntryDateDialogSourceFileModifiedDate",
|
||||||
"editEntryDateDialogTargetFieldsHeader",
|
"editEntryDateDialogTargetFieldsHeader",
|
||||||
|
"collectionSortRating",
|
||||||
|
"searchSectionRating",
|
||||||
"settingsThumbnailShowRatingIcon"
|
"settingsThumbnailShowRatingIcon"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue