From 039983b8f78ceac1557be7372138f237a3508b50 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 29 Dec 2021 15:28:07 +0900 Subject: [PATCH] #143 rating: cataloguing, thumbnail overlay, info stars --- .../channel/calls/MetadataFetchHandler.kt | 31 +++- .../deckers/thibault/aves/metadata/XMP.kt | 13 +- lib/l10n/app_en.arb | 1 + lib/model/entry.dart | 2 + lib/model/metadata/catalog.dart | 9 +- lib/model/metadata_db.dart | 3 +- lib/model/metadata_db_upgrade.dart | 8 + lib/model/settings/defaults.dart | 1 + lib/model/settings/settings.dart | 6 + .../metadata/metadata_fetch_service.dart | 1 + lib/theme/icons.dart | 3 +- lib/widgets/common/grid/theme.dart | 4 +- lib/widgets/common/identity/aves_icons.dart | 24 +++ lib/widgets/common/thumbnail/overlay.dart | 5 +- .../settings/thumbnails/thumbnails.dart | 137 +++++++++++------- lib/widgets/viewer/debug/db.dart | 1 + lib/widgets/viewer/info/basic_section.dart | 25 +++- untranslated.json | 14 +- 18 files changed, 209 insertions(+), 79 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt index 2bdb8919b..81d30936f 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt @@ -78,6 +78,7 @@ import java.nio.charset.Charset import java.nio.charset.StandardCharsets import java.text.ParseException import java.util.* +import kotlin.math.roundToInt import kotlin.math.roundToLong class MetadataFetchHandler(private val context: Context) : MethodCallHandler { @@ -374,6 +375,10 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { // set `KEY_XMP_SUBJECTS` from these fields (by precedence): // - ME / XMP / dc:subject // - ME / IPTC / keywords + // set `KEY_RATING` from these fields (by precedence): + // - ME / XMP / xmp:Rating + // - ME / XMP / MicrosoftPhoto:Rating + // - ME / XMP / acdsee:rating private fun getCatalogMetadata(call: MethodCall, result: MethodChannel.Result) { val mimeType = call.argument("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } @@ -459,22 +464,34 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) { val xmpMeta = dir.xmpMeta try { - if (xmpMeta.doesPropertyExist(XMP.DC_SCHEMA_NS, XMP.SUBJECT_PROP_NAME)) { - val count = xmpMeta.countArrayItems(XMP.DC_SCHEMA_NS, XMP.SUBJECT_PROP_NAME) - val values = (1 until count + 1).map { xmpMeta.getArrayItem(XMP.DC_SCHEMA_NS, XMP.SUBJECT_PROP_NAME, it).value } + if (xmpMeta.doesPropertyExist(XMP.DC_SCHEMA_NS, XMP.DC_SUBJECT_PROP_NAME)) { + val count = xmpMeta.countArrayItems(XMP.DC_SCHEMA_NS, XMP.DC_SUBJECT_PROP_NAME) + val values = (1 until count + 1).map { xmpMeta.getArrayItem(XMP.DC_SCHEMA_NS, XMP.DC_SUBJECT_PROP_NAME, it).value } metadataMap[KEY_XMP_SUBJECTS] = values.joinToString(XMP_SUBJECTS_SEPARATOR) } - xmpMeta.getSafeLocalizedText(XMP.DC_SCHEMA_NS, XMP.TITLE_PROP_NAME, acceptBlank = false) { metadataMap[KEY_XMP_TITLE_DESCRIPTION] = it } + xmpMeta.getSafeLocalizedText(XMP.DC_SCHEMA_NS, XMP.DC_TITLE_PROP_NAME, acceptBlank = false) { metadataMap[KEY_XMP_TITLE_DESCRIPTION] = it } if (!metadataMap.containsKey(KEY_XMP_TITLE_DESCRIPTION)) { - xmpMeta.getSafeLocalizedText(XMP.DC_SCHEMA_NS, XMP.DESCRIPTION_PROP_NAME, acceptBlank = false) { metadataMap[KEY_XMP_TITLE_DESCRIPTION] = it } + xmpMeta.getSafeLocalizedText(XMP.DC_SCHEMA_NS, XMP.DC_DESCRIPTION_PROP_NAME, acceptBlank = false) { metadataMap[KEY_XMP_TITLE_DESCRIPTION] = it } } if (!metadataMap.containsKey(KEY_DATE_MILLIS)) { - xmpMeta.getSafeDateMillis(XMP.XMP_SCHEMA_NS, XMP.CREATE_DATE_PROP_NAME) { metadataMap[KEY_DATE_MILLIS] = it } + xmpMeta.getSafeDateMillis(XMP.XMP_SCHEMA_NS, XMP.XMP_CREATE_DATE_PROP_NAME) { metadataMap[KEY_DATE_MILLIS] = it } if (!metadataMap.containsKey(KEY_DATE_MILLIS)) { xmpMeta.getSafeDateMillis(XMP.PHOTOSHOP_SCHEMA_NS, XMP.PS_DATE_CREATED_PROP_NAME) { metadataMap[KEY_DATE_MILLIS] = it } } } + xmpMeta.getSafeInt(XMP.XMP_SCHEMA_NS, XMP.XMP_RATING_PROP_NAME) { if (it in RATING_RANGE) metadataMap[KEY_RATING] = it } + if (!metadataMap.containsKey(KEY_RATING)) { + 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 + val standardRating = (percentRating / 25f).roundToInt() + 1 + if (standardRating in RATING_RANGE) metadataMap[KEY_RATING] = standardRating + } + 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 } + } + } + // identification of panorama (aka photo sphere) if (xmpMeta.isPanorama()) { flags = flags or MASK_IS_360 @@ -966,6 +983,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { private const val KEY_LONGITUDE = "longitude" private const val KEY_XMP_SUBJECTS = "xmpSubjects" private const val KEY_XMP_TITLE_DESCRIPTION = "xmpTitleDescription" + private const val KEY_RATING = "rating" private const val MASK_IS_ANIMATED = 1 shl 0 private const val MASK_IS_FLIPPED = 1 shl 1 @@ -973,6 +991,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { private const val MASK_IS_360 = 1 shl 3 private const val MASK_IS_MULTIPAGE = 1 shl 4 private const val XMP_SUBJECTS_SEPARATOR = ";" + private val RATING_RANGE = 1..5 // overlay metadata private const val KEY_APERTURE = "aperture" diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt index 72c38f447..97f099fe4 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt @@ -14,7 +14,9 @@ object XMP { // standard namespaces // cf com.adobe.internal.xmp.XMPConst + const val ACDSEE_SCHEMA_NS = "http://ns.acdsee.com/iptc/1.0/" const val DC_SCHEMA_NS = "http://purl.org/dc/elements/1.1/" + const val MICROSOFTPHOTO_SCHEMA_NS = "http://ns.microsoft.com/photo/1.0/" const val PHOTOSHOP_SCHEMA_NS = "http://ns.adobe.com/photoshop/1.0/" const val XMP_SCHEMA_NS = "http://ns.adobe.com/xap/1.0/" private const val XMP_GIMG_SCHEMA_NS = "http://ns.adobe.com/xap/1.0/g/img/" @@ -27,11 +29,14 @@ object XMP { const val CONTAINER_SCHEMA_NS = "http://ns.google.com/photos/1.0/container/" private const val CONTAINER_ITEM_SCHEMA_NS = "http://ns.google.com/photos/1.0/container/item/" - const val SUBJECT_PROP_NAME = "dc:subject" - const val TITLE_PROP_NAME = "dc:title" - const val DESCRIPTION_PROP_NAME = "dc:description" + const val ACDSEE_RATING_PROP_NAME = "acdsee:rating" + const val DC_DESCRIPTION_PROP_NAME = "dc:description" + const val DC_SUBJECT_PROP_NAME = "dc:subject" + const val DC_TITLE_PROP_NAME = "dc:title" + const val MS_RATING_PROP_NAME = "MicrosoftPhoto:Rating" const val PS_DATE_CREATED_PROP_NAME = "photoshop:DateCreated" - const val CREATE_DATE_PROP_NAME = "xmp:CreateDate" + const val XMP_CREATE_DATE_PROP_NAME = "xmp:CreateDate" + const val XMP_RATING_PROP_NAME = "xmp:Rating" private const val GENERIC_LANG = "" private const val SPECIFIC_LANG = "en-US" diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 25f6bf07e..fa91e031e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -519,6 +519,7 @@ "settingsSectionThumbnails": "Thumbnails", "settingsThumbnailShowLocationIcon": "Show location icon", "settingsThumbnailShowMotionPhotoIcon": "Show motion photo icon", + "settingsThumbnailShowRatingIcon": "Show rating icon", "settingsThumbnailShowRawIcon": "Show raw icon", "settingsThumbnailShowVideoDuration": "Show video duration", diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 93ef1d4f5..9d0925459 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -361,6 +361,8 @@ class AvesEntry { return _bestDate; } + int? get rating => _catalogMetadata?.rating; + int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees; set rotationDegrees(int rotationDegrees) { diff --git a/lib/model/metadata/catalog.dart b/lib/model/metadata/catalog.dart index 1c532673e..3f204ea72 100644 --- a/lib/model/metadata/catalog.dart +++ b/lib/model/metadata/catalog.dart @@ -5,7 +5,7 @@ class CatalogMetadata { final int? contentId, dateMillis; final bool isAnimated, isGeotiff, is360, isMultiPage; bool isFlipped; - int? rotationDegrees; + int? rating, rotationDegrees; final String? mimeType, xmpSubjects, xmpTitleDescription; double? latitude, longitude; Address? address; @@ -31,6 +31,7 @@ class CatalogMetadata { this.xmpTitleDescription, double? latitude, double? longitude, + this.rating, }) { // 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}), @@ -67,6 +68,7 @@ class CatalogMetadata { xmpTitleDescription: xmpTitleDescription, latitude: latitude, longitude: longitude, + rating: rating, ); } @@ -87,6 +89,8 @@ class CatalogMetadata { xmpTitleDescription: map['xmpTitleDescription'] ?? '', latitude: map['latitude'], longitude: map['longitude'], + // `rotationDegrees` should default to `null`, not 0 + rating: map['rating'], ); } @@ -100,8 +104,9 @@ class CatalogMetadata { 'xmpTitleDescription': xmpTitleDescription, 'latitude': latitude, 'longitude': longitude, + 'rating': rating, }; @override - String toString() => '$runtimeType#${shortHash(this)}{contentId=$contentId, mimeType=$mimeType, dateMillis=$dateMillis, isAnimated=$isAnimated, isFlipped=$isFlipped, isGeotiff=$isGeotiff, is360=$is360, isMultiPage=$isMultiPage, rotationDegrees=$rotationDegrees, latitude=$latitude, longitude=$longitude, xmpSubjects=$xmpSubjects, xmpTitleDescription=$xmpTitleDescription}'; + String toString() => '$runtimeType#${shortHash(this)}{contentId=$contentId, mimeType=$mimeType, dateMillis=$dateMillis, isAnimated=$isAnimated, isFlipped=$isFlipped, isGeotiff=$isGeotiff, is360=$is360, isMultiPage=$isMultiPage, rotationDegrees=$rotationDegrees, xmpSubjects=$xmpSubjects, xmpTitleDescription=$xmpTitleDescription, latitude=$latitude, longitude=$longitude, rating=$rating}'; } diff --git a/lib/model/metadata_db.dart b/lib/model/metadata_db.dart index bb0a63201..ca9f09623 100644 --- a/lib/model/metadata_db.dart +++ b/lib/model/metadata_db.dart @@ -145,6 +145,7 @@ class SqfliteMetadataDb implements MetadataDb { ', xmpTitleDescription TEXT' ', latitude REAL' ', longitude REAL' + ', rating INTEGER' ')'); await db.execute('CREATE TABLE $addressTable(' 'contentId INTEGER PRIMARY KEY' @@ -168,7 +169,7 @@ class SqfliteMetadataDb implements MetadataDb { ')'); }, onUpgrade: MetadataDbUpgrader.upgradeDb, - version: 5, + version: 6, ); } diff --git a/lib/model/metadata_db_upgrade.dart b/lib/model/metadata_db_upgrade.dart index 578934b8d..d69e7857c 100644 --- a/lib/model/metadata_db_upgrade.dart +++ b/lib/model/metadata_db_upgrade.dart @@ -25,6 +25,9 @@ class MetadataDbUpgrader { case 4: await _upgradeFrom4(db); break; + case 5: + await _upgradeFrom5(db); + break; } oldVersion++; } @@ -121,4 +124,9 @@ class MetadataDbUpgrader { ', resumeTimeMillis INTEGER' ')'); } + + static Future _upgradeFrom5(Database db) async { + debugPrint('upgrading DB from v5'); + await db.execute('ALTER TABLE $metadataTable ADD COLUMN rating INTEGER;'); + } } diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index 96ff24077..cfd155b42 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -45,6 +45,7 @@ class SettingsDefaults { ]; static const showThumbnailLocation = true; static const showThumbnailMotionPhoto = true; + static const showThumbnailRating = true; static const showThumbnailRaw = true; static const showThumbnailVideoDuration = true; diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index f76a97765..3abbdea7a 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -63,6 +63,7 @@ class Settings extends ChangeNotifier { static const collectionSelectionQuickActionsKey = 'collection_selection_quick_actions'; static const showThumbnailLocationKey = 'show_thumbnail_location'; static const showThumbnailMotionPhotoKey = 'show_thumbnail_motion_photo'; + static const showThumbnailRatingKey = 'show_thumbnail_rating'; static const showThumbnailRawKey = 'show_thumbnail_raw'; static const showThumbnailVideoDurationKey = 'show_thumbnail_video_duration'; @@ -310,6 +311,10 @@ class Settings extends ChangeNotifier { set showThumbnailMotionPhoto(bool newValue) => setAndNotify(showThumbnailMotionPhotoKey, newValue); + bool get showThumbnailRating => getBoolOrDefault(showThumbnailRatingKey, SettingsDefaults.showThumbnailRating); + + set showThumbnailRating(bool newValue) => setAndNotify(showThumbnailRatingKey, newValue); + bool get showThumbnailRaw => getBoolOrDefault(showThumbnailRawKey, SettingsDefaults.showThumbnailRaw); set showThumbnailRaw(bool newValue) => setAndNotify(showThumbnailRawKey, newValue); @@ -619,6 +624,7 @@ class Settings extends ChangeNotifier { case mustBackTwiceToExitKey: case showThumbnailLocationKey: case showThumbnailMotionPhotoKey: + case showThumbnailRatingKey: case showThumbnailRawKey: case showThumbnailVideoDurationKey: case showOverlayOnOpeningKey: diff --git a/lib/services/metadata/metadata_fetch_service.dart b/lib/services/metadata/metadata_fetch_service.dart index 39ebc5fbd..574269843 100644 --- a/lib/services/metadata/metadata_fetch_service.dart +++ b/lib/services/metadata/metadata_fetch_service.dart @@ -66,6 +66,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { // 'dateMillis': date taken in milliseconds since Epoch (long) // 'isAnimated': animated gif/webp (bool) // 'isFlipped': flipped according to EXIF orientation (bool) + // 'rating': rating in [1,5] (int) // 'rotationDegrees': rotation degrees according to EXIF orientation or other metadata (int) // 'latitude': latitude (double) // 'longitude': longitude (double) diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 7bfb34635..87c5380db 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -111,8 +111,9 @@ class AIcons { static const IconData geo = Icons.language_outlined; static const IconData motionPhoto = Icons.motion_photos_on_outlined; static const IconData multiPage = Icons.burst_mode_outlined; - static const IconData videoThumb = Icons.play_circle_outline; + static const IconData rating = Icons.star_border_outlined; static const IconData threeSixty = Icons.threesixty_outlined; + static const IconData videoThumb = Icons.play_circle_outline; static const IconData selected = Icons.check_circle_outline; static const IconData unselected = Icons.radio_button_unchecked; diff --git a/lib/widgets/common/grid/theme.dart b/lib/widgets/common/grid/theme.dart index 3c4fda8d5..442754db6 100644 --- a/lib/widgets/common/grid/theme.dart +++ b/lib/widgets/common/grid/theme.dart @@ -30,6 +30,7 @@ class GridTheme extends StatelessWidget { highlightBorderWidth: highlightBorderWidth, showLocation: showLocation ?? settings.showThumbnailLocation, showMotionPhoto: settings.showThumbnailMotionPhoto, + showRating: settings.showThumbnailRating, showRaw: settings.showThumbnailRaw, showVideoDuration: settings.showThumbnailVideoDuration, ); @@ -41,7 +42,7 @@ class GridTheme extends StatelessWidget { class GridThemeData { final double iconSize, fontSize, highlightBorderWidth; - final bool showLocation, showMotionPhoto, showRaw, showVideoDuration; + final bool showLocation, showMotionPhoto, showRating, showRaw, showVideoDuration; const GridThemeData({ required this.iconSize, @@ -49,6 +50,7 @@ class GridThemeData { required this.highlightBorderWidth, required this.showLocation, required this.showMotionPhoto, + required this.showRating, required this.showRaw, required this.showVideoDuration, }); diff --git a/lib/widgets/common/identity/aves_icons.dart b/lib/widgets/common/identity/aves_icons.dart index a64941ac1..a2f85c220 100644 --- a/lib/widgets/common/identity/aves_icons.dart +++ b/lib/widgets/common/identity/aves_icons.dart @@ -139,6 +139,30 @@ class MultiPageIcon extends StatelessWidget { } } +class RatingIcon extends StatelessWidget { + final AvesEntry entry; + + const RatingIcon({ + Key? key, + required this.entry, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final gridTheme = context.watch(); + return DefaultTextStyle( + style: TextStyle( + color: Colors.grey.shade200, + fontSize: gridTheme.fontSize, + ), + child: OverlayIcon( + icon: AIcons.rating, + text: '${entry.rating}', + ), + ); + } +} + class OverlayIcon extends StatelessWidget { final IconData icon; final String? text; diff --git a/lib/widgets/common/thumbnail/overlay.dart b/lib/widgets/common/thumbnail/overlay.dart index 4e72d1c0e..cd0c9de0d 100644 --- a/lib/widgets/common/thumbnail/overlay.dart +++ b/lib/widgets/common/thumbnail/overlay.dart @@ -21,12 +21,11 @@ class ThumbnailEntryOverlay extends StatelessWidget { final children = [ if (entry.hasGps && context.select((t) => t.showLocation)) const GpsIcon(), if (entry.isVideo) - VideoIcon( - entry: entry, - ) + VideoIcon(entry: entry) else if (entry.isAnimated) const AnimatedImageIcon() else ...[ + if (entry.rating != null && context.select((t) => t.showRating)) RatingIcon(entry: entry), if (entry.isRaw && context.select((t) => t.showRaw)) const RawIcon(), if (entry.isGeotiff) const GeotiffIcon(), if (entry.is360) const SphericalImageIcon(), diff --git a/lib/widgets/settings/thumbnails/thumbnails.dart b/lib/widgets/settings/thumbnails/thumbnails.dart index e2e167a76..c10c87717 100644 --- a/lib/widgets/settings/thumbnails/thumbnails.dart +++ b/lib/widgets/settings/thumbnails/thumbnails.dart @@ -20,11 +20,6 @@ class ThumbnailsSection extends StatelessWidget { @override Widget build(BuildContext context) { - final currentShowThumbnailLocation = context.select((s) => s.showThumbnailLocation); - final currentShowThumbnailMotionPhoto = context.select((s) => s.showThumbnailMotionPhoto); - final currentShowThumbnailRaw = context.select((s) => s.showThumbnailRaw); - final currentShowThumbnailVideoDuration = context.select((s) => s.showThumbnailVideoDuration); - final iconSize = IconTheme.of(context).size! * MediaQuery.textScaleFactorOf(context); double opacityFor(bool enabled) => enabled ? 1 : .2; @@ -38,64 +33,96 @@ class ThumbnailsSection extends StatelessWidget { showHighlight: false, children: [ const CollectionActionsTile(), - SwitchListTile( - value: currentShowThumbnailLocation, - onChanged: (v) => settings.showThumbnailLocation = v, - title: Row( - children: [ - Expanded(child: Text(context.l10n.settingsThumbnailShowLocationIcon)), - AnimatedOpacity( - opacity: opacityFor(currentShowThumbnailLocation), - duration: Durations.toggleableTransitionAnimation, - child: Icon( - AIcons.location, - size: iconSize, - ), - ), - ], - ), - ), - SwitchListTile( - value: currentShowThumbnailMotionPhoto, - onChanged: (v) => settings.showThumbnailMotionPhoto = v, - title: Row( - children: [ - Expanded(child: Text(context.l10n.settingsThumbnailShowMotionPhotoIcon)), - AnimatedOpacity( - opacity: opacityFor(currentShowThumbnailMotionPhoto), - duration: Durations.toggleableTransitionAnimation, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - MotionPhotoIcon.scale) / 2), + Selector( + selector: (context, s) => s.showThumbnailLocation, + builder: (context, current, child) => SwitchListTile( + value: current, + onChanged: (v) => settings.showThumbnailLocation = v, + title: Row( + children: [ + Expanded(child: Text(context.l10n.settingsThumbnailShowLocationIcon)), + AnimatedOpacity( + opacity: opacityFor(current), + duration: Durations.toggleableTransitionAnimation, child: Icon( - AIcons.motionPhoto, - size: iconSize * MotionPhotoIcon.scale, + AIcons.location, + size: iconSize, ), ), - ), - ], + ], + ), ), ), - SwitchListTile( - value: currentShowThumbnailRaw, - onChanged: (v) => settings.showThumbnailRaw = v, - title: Row( - children: [ - Expanded(child: Text(context.l10n.settingsThumbnailShowRawIcon)), - AnimatedOpacity( - opacity: opacityFor(currentShowThumbnailRaw), - duration: Durations.toggleableTransitionAnimation, - child: Icon( - AIcons.raw, - size: iconSize, + Selector( + selector: (context, s) => s.showThumbnailMotionPhoto, + builder: (context, current, child) => SwitchListTile( + value: current, + onChanged: (v) => settings.showThumbnailMotionPhoto = v, + title: Row( + children: [ + Expanded(child: Text(context.l10n.settingsThumbnailShowMotionPhotoIcon)), + AnimatedOpacity( + opacity: opacityFor(current), + duration: Durations.toggleableTransitionAnimation, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - MotionPhotoIcon.scale) / 2), + child: Icon( + AIcons.motionPhoto, + size: iconSize * MotionPhotoIcon.scale, + ), + ), ), - ), - ], + ], + ), ), ), - SwitchListTile( - value: currentShowThumbnailVideoDuration, - onChanged: (v) => settings.showThumbnailVideoDuration = v, - title: Text(context.l10n.settingsThumbnailShowVideoDuration), + Selector( + selector: (context, s) => s.showThumbnailRating, + builder: (context, current, child) => SwitchListTile( + value: current, + onChanged: (v) => settings.showThumbnailRating = v, + title: Row( + children: [ + Expanded(child: Text(context.l10n.settingsThumbnailShowRatingIcon)), + AnimatedOpacity( + opacity: opacityFor(current), + duration: Durations.toggleableTransitionAnimation, + child: Icon( + AIcons.rating, + size: iconSize, + ), + ), + ], + ), + ), + ), + Selector( + selector: (context, s) => s.showThumbnailRaw, + builder: (context, current, child) => SwitchListTile( + value: current, + onChanged: (v) => settings.showThumbnailRaw = v, + title: Row( + children: [ + Expanded(child: Text(context.l10n.settingsThumbnailShowRawIcon)), + AnimatedOpacity( + opacity: opacityFor(current), + duration: Durations.toggleableTransitionAnimation, + child: Icon( + AIcons.raw, + size: iconSize, + ), + ), + ], + ), + ), + ), + Selector( + selector: (context, s) => s.showThumbnailVideoDuration, + builder: (context, current, child) => SwitchListTile( + value: current, + onChanged: (v) => settings.showThumbnailVideoDuration = v, + title: Text(context.l10n.settingsThumbnailShowVideoDuration), + ), ), ], ); diff --git a/lib/widgets/viewer/debug/db.dart b/lib/widgets/viewer/debug/db.dart index 11279a2f3..ecc490ee5 100644 --- a/lib/widgets/viewer/debug/db.dart +++ b/lib/widgets/viewer/debug/db.dart @@ -123,6 +123,7 @@ class _DbTabState extends State { 'longitude': '${data.longitude}', 'xmpSubjects': data.xmpSubjects ?? '', 'xmpTitleDescription': data.xmpTitleDescription ?? '', + 'rating': '${data.rating}', }, ), ], diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index c550e6863..b53ea7bfa 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -12,8 +12,8 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; -import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart'; +import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/owner.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -74,15 +74,32 @@ class BasicSection extends StatelessWidget { if (path != null) l10n.viewerInfoLabelPath: path, }, ), - OwnerProp( - entry: entry, - ), + OwnerProp(entry: entry), + _buildRatingRow(), _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) { final tags = entry.tags.toList()..sort(compareAsciiUpperCase); final album = entry.directory; diff --git a/untranslated.json b/untranslated.json index 347926e60..723499108 100644 --- a/untranslated.json +++ b/untranslated.json @@ -4,7 +4,16 @@ "editEntryDateDialogSourceCustomDate", "editEntryDateDialogSourceTitle", "editEntryDateDialogSourceFileModifiedDate", - "editEntryDateDialogTargetFieldsHeader" + "editEntryDateDialogTargetFieldsHeader", + "settingsThumbnailShowRatingIcon" + ], + + "fr": [ + "settingsThumbnailShowRatingIcon" + ], + + "ko": [ + "settingsThumbnailShowRatingIcon" ], "ru": [ @@ -12,6 +21,7 @@ "editEntryDateDialogSourceCustomDate", "editEntryDateDialogSourceTitle", "editEntryDateDialogSourceFileModifiedDate", - "editEntryDateDialogTargetFieldsHeader" + "editEntryDateDialogTargetFieldsHeader", + "settingsThumbnailShowRatingIcon" ] }