#143 rating: cataloguing, thumbnail overlay, info stars
This commit is contained in:
parent
23c13c21f8
commit
039983b8f7
18 changed files with 209 additions and 79 deletions
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
||||
|
|
|
@ -361,6 +361,8 @@ class AvesEntry {
|
|||
return _bestDate;
|
||||
}
|
||||
|
||||
int? get rating => _catalogMetadata?.rating;
|
||||
|
||||
int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees;
|
||||
|
||||
set rotationDegrees(int rotationDegrees) {
|
||||
|
|
|
@ -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}';
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -123,6 +123,7 @@ class _DbTabState extends State<DbTab> {
|
|||
'longitude': '${data.longitude}',
|
||||
'xmpSubjects': data.xmpSubjects ?? '',
|
||||
'xmpTitleDescription': data.xmpTitleDescription ?? '',
|
||||
'rating': '${data.rating}',
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue