#143 rating: cataloguing, thumbnail overlay, info stars

This commit is contained in:
Thibault Deckers 2021-12-29 15:28:07 +09:00
parent 23c13c21f8
commit 039983b8f7
18 changed files with 209 additions and 79 deletions

View file

@ -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<String>("mimeType")
val uri = call.argument<String>("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"

View file

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

View file

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

View file

@ -361,6 +361,8 @@ class AvesEntry {
return _bestDate;
}
int? get rating => _catalogMetadata?.rating;
int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees;
set rotationDegrees(int rotationDegrees) {

View file

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

View file

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

View file

@ -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<void> _upgradeFrom5(Database db) async {
debugPrint('upgrading DB from v5');
await db.execute('ALTER TABLE $metadataTable ADD COLUMN rating INTEGER;');
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,12 +21,11 @@ class ThumbnailEntryOverlay extends StatelessWidget {
final children = [
if (entry.hasGps && context.select<GridThemeData, bool>((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<GridThemeData, bool>((t) => t.showRating)) RatingIcon(entry: entry),
if (entry.isRaw && context.select<GridThemeData, bool>((t) => t.showRaw)) const RawIcon(),
if (entry.isGeotiff) const GeotiffIcon(),
if (entry.is360) const SphericalImageIcon(),

View file

@ -20,11 +20,6 @@ class ThumbnailsSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final currentShowThumbnailLocation = context.select<Settings, bool>((s) => s.showThumbnailLocation);
final currentShowThumbnailMotionPhoto = context.select<Settings, bool>((s) => s.showThumbnailMotionPhoto);
final currentShowThumbnailRaw = context.select<Settings, bool>((s) => s.showThumbnailRaw);
final currentShowThumbnailVideoDuration = context.select<Settings, bool>((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<Settings, bool>(
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<Settings, bool>(
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<Settings, bool>(
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<Settings, bool>(
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<Settings, bool>(
selector: (context, s) => s.showThumbnailVideoDuration,
builder: (context, current, child) => SwitchListTile(
value: current,
onChanged: (v) => settings.showThumbnailVideoDuration = v,
title: Text(context.l10n.settingsThumbnailShowVideoDuration),
),
),
],
);

View file

@ -123,6 +123,7 @@ class _DbTabState extends State<DbTab> {
'longitude': '${data.longitude}',
'xmpSubjects': data.xmpSubjects ?? '',
'xmpTitleDescription': data.xmpTitleDescription ?? '',
'rating': '${data.rating}',
},
),
],

View file

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

View file

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