#143 rating: sort/group/filter

This commit is contained in:
Thibault Deckers 2021-12-29 18:27:32 +09:00
parent 039983b8f7
commit 713ef3d782
24 changed files with 203 additions and 58 deletions

View file

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

View file

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

View file

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

View file

@ -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": "시스템",

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,
}) { }) {

View file

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

View 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),
);
}
}

View file

@ -316,7 +316,6 @@ class _ScaleOverlayState extends State<_ScaleOverlay> {
colors: const [ colors: const [
Colors.black, Colors.black,
Colors.black54, Colors.black54,
// Colors.amber,
], ],
), ),
) )

View file

@ -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(),

View file

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

View file

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

View file

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

View file

@ -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"
] ]
} }