#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.nio.charset.StandardCharsets
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
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):
|
// set `KEY_XMP_SUBJECTS` from these fields (by precedence):
|
||||||
// - ME / XMP / dc:subject
|
// - ME / XMP / dc:subject
|
||||||
// - ME / IPTC / keywords
|
// - 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) {
|
private fun getCatalogMetadata(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val mimeType = call.argument<String>("mimeType")
|
val mimeType = call.argument<String>("mimeType")
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
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)) {
|
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
||||||
val xmpMeta = dir.xmpMeta
|
val xmpMeta = dir.xmpMeta
|
||||||
try {
|
try {
|
||||||
if (xmpMeta.doesPropertyExist(XMP.DC_SCHEMA_NS, XMP.SUBJECT_PROP_NAME)) {
|
if (xmpMeta.doesPropertyExist(XMP.DC_SCHEMA_NS, XMP.DC_SUBJECT_PROP_NAME)) {
|
||||||
val count = xmpMeta.countArrayItems(XMP.DC_SCHEMA_NS, XMP.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.SUBJECT_PROP_NAME, it).value }
|
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)
|
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)) {
|
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)) {
|
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)) {
|
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
|
||||||
xmpMeta.getSafeDateMillis(XMP.PHOTOSHOP_SCHEMA_NS, XMP.PS_DATE_CREATED_PROP_NAME) { metadataMap[KEY_DATE_MILLIS] = it }
|
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)
|
// identification of panorama (aka photo sphere)
|
||||||
if (xmpMeta.isPanorama()) {
|
if (xmpMeta.isPanorama()) {
|
||||||
flags = flags or MASK_IS_360
|
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_LONGITUDE = "longitude"
|
||||||
private const val KEY_XMP_SUBJECTS = "xmpSubjects"
|
private const val KEY_XMP_SUBJECTS = "xmpSubjects"
|
||||||
private const val KEY_XMP_TITLE_DESCRIPTION = "xmpTitleDescription"
|
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_ANIMATED = 1 shl 0
|
||||||
private const val MASK_IS_FLIPPED = 1 shl 1
|
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_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"
|
||||||
|
|
|
@ -14,7 +14,9 @@ object XMP {
|
||||||
|
|
||||||
// standard namespaces
|
// standard namespaces
|
||||||
// cf com.adobe.internal.xmp.XMPConst
|
// 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 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 PHOTOSHOP_SCHEMA_NS = "http://ns.adobe.com/photoshop/1.0/"
|
||||||
const val XMP_SCHEMA_NS = "http://ns.adobe.com/xap/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/"
|
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/"
|
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/"
|
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 ACDSEE_RATING_PROP_NAME = "acdsee:rating"
|
||||||
const val TITLE_PROP_NAME = "dc:title"
|
const val DC_DESCRIPTION_PROP_NAME = "dc:description"
|
||||||
const val 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 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 GENERIC_LANG = ""
|
||||||
private const val SPECIFIC_LANG = "en-US"
|
private const val SPECIFIC_LANG = "en-US"
|
||||||
|
|
|
@ -519,6 +519,7 @@
|
||||||
"settingsSectionThumbnails": "Thumbnails",
|
"settingsSectionThumbnails": "Thumbnails",
|
||||||
"settingsThumbnailShowLocationIcon": "Show location icon",
|
"settingsThumbnailShowLocationIcon": "Show location icon",
|
||||||
"settingsThumbnailShowMotionPhotoIcon": "Show motion photo icon",
|
"settingsThumbnailShowMotionPhotoIcon": "Show motion photo icon",
|
||||||
|
"settingsThumbnailShowRatingIcon": "Show rating icon",
|
||||||
"settingsThumbnailShowRawIcon": "Show raw icon",
|
"settingsThumbnailShowRawIcon": "Show raw icon",
|
||||||
"settingsThumbnailShowVideoDuration": "Show video duration",
|
"settingsThumbnailShowVideoDuration": "Show video duration",
|
||||||
|
|
||||||
|
|
|
@ -361,6 +361,8 @@ class AvesEntry {
|
||||||
return _bestDate;
|
return _bestDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int? get rating => _catalogMetadata?.rating;
|
||||||
|
|
||||||
int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees;
|
int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees;
|
||||||
|
|
||||||
set rotationDegrees(int rotationDegrees) {
|
set rotationDegrees(int rotationDegrees) {
|
||||||
|
|
|
@ -5,7 +5,7 @@ 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? rotationDegrees;
|
int? rating, rotationDegrees;
|
||||||
final String? mimeType, xmpSubjects, xmpTitleDescription;
|
final String? mimeType, xmpSubjects, xmpTitleDescription;
|
||||||
double? latitude, longitude;
|
double? latitude, longitude;
|
||||||
Address? address;
|
Address? address;
|
||||||
|
@ -31,6 +31,7 @@ class CatalogMetadata {
|
||||||
this.xmpTitleDescription,
|
this.xmpTitleDescription,
|
||||||
double? latitude,
|
double? latitude,
|
||||||
double? longitude,
|
double? longitude,
|
||||||
|
this.rating,
|
||||||
}) {
|
}) {
|
||||||
// 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}),
|
||||||
|
@ -67,6 +68,7 @@ class CatalogMetadata {
|
||||||
xmpTitleDescription: xmpTitleDescription,
|
xmpTitleDescription: xmpTitleDescription,
|
||||||
latitude: latitude,
|
latitude: latitude,
|
||||||
longitude: longitude,
|
longitude: longitude,
|
||||||
|
rating: rating,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +89,8 @@ 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'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,8 +104,9 @@ class CatalogMetadata {
|
||||||
'xmpTitleDescription': xmpTitleDescription,
|
'xmpTitleDescription': xmpTitleDescription,
|
||||||
'latitude': latitude,
|
'latitude': latitude,
|
||||||
'longitude': longitude,
|
'longitude': longitude,
|
||||||
|
'rating': rating,
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@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'
|
', xmpTitleDescription TEXT'
|
||||||
', latitude REAL'
|
', latitude REAL'
|
||||||
', longitude REAL'
|
', longitude REAL'
|
||||||
|
', rating INTEGER'
|
||||||
')');
|
')');
|
||||||
await db.execute('CREATE TABLE $addressTable('
|
await db.execute('CREATE TABLE $addressTable('
|
||||||
'contentId INTEGER PRIMARY KEY'
|
'contentId INTEGER PRIMARY KEY'
|
||||||
|
@ -168,7 +169,7 @@ class SqfliteMetadataDb implements MetadataDb {
|
||||||
')');
|
')');
|
||||||
},
|
},
|
||||||
onUpgrade: MetadataDbUpgrader.upgradeDb,
|
onUpgrade: MetadataDbUpgrader.upgradeDb,
|
||||||
version: 5,
|
version: 6,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,9 @@ class MetadataDbUpgrader {
|
||||||
case 4:
|
case 4:
|
||||||
await _upgradeFrom4(db);
|
await _upgradeFrom4(db);
|
||||||
break;
|
break;
|
||||||
|
case 5:
|
||||||
|
await _upgradeFrom5(db);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
oldVersion++;
|
oldVersion++;
|
||||||
}
|
}
|
||||||
|
@ -121,4 +124,9 @@ class MetadataDbUpgrader {
|
||||||
', resumeTimeMillis INTEGER'
|
', 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 showThumbnailLocation = true;
|
||||||
static const showThumbnailMotionPhoto = true;
|
static const showThumbnailMotionPhoto = true;
|
||||||
|
static const showThumbnailRating = true;
|
||||||
static const showThumbnailRaw = true;
|
static const showThumbnailRaw = true;
|
||||||
static const showThumbnailVideoDuration = true;
|
static const showThumbnailVideoDuration = true;
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,7 @@ class Settings extends ChangeNotifier {
|
||||||
static const collectionSelectionQuickActionsKey = 'collection_selection_quick_actions';
|
static const collectionSelectionQuickActionsKey = 'collection_selection_quick_actions';
|
||||||
static const showThumbnailLocationKey = 'show_thumbnail_location';
|
static const showThumbnailLocationKey = 'show_thumbnail_location';
|
||||||
static const showThumbnailMotionPhotoKey = 'show_thumbnail_motion_photo';
|
static const showThumbnailMotionPhotoKey = 'show_thumbnail_motion_photo';
|
||||||
|
static const showThumbnailRatingKey = 'show_thumbnail_rating';
|
||||||
static const showThumbnailRawKey = 'show_thumbnail_raw';
|
static const showThumbnailRawKey = 'show_thumbnail_raw';
|
||||||
static const showThumbnailVideoDurationKey = 'show_thumbnail_video_duration';
|
static const showThumbnailVideoDurationKey = 'show_thumbnail_video_duration';
|
||||||
|
|
||||||
|
@ -310,6 +311,10 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
set showThumbnailMotionPhoto(bool newValue) => setAndNotify(showThumbnailMotionPhotoKey, newValue);
|
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);
|
bool get showThumbnailRaw => getBoolOrDefault(showThumbnailRawKey, SettingsDefaults.showThumbnailRaw);
|
||||||
|
|
||||||
set showThumbnailRaw(bool newValue) => setAndNotify(showThumbnailRawKey, newValue);
|
set showThumbnailRaw(bool newValue) => setAndNotify(showThumbnailRawKey, newValue);
|
||||||
|
@ -619,6 +624,7 @@ class Settings extends ChangeNotifier {
|
||||||
case mustBackTwiceToExitKey:
|
case mustBackTwiceToExitKey:
|
||||||
case showThumbnailLocationKey:
|
case showThumbnailLocationKey:
|
||||||
case showThumbnailMotionPhotoKey:
|
case showThumbnailMotionPhotoKey:
|
||||||
|
case showThumbnailRatingKey:
|
||||||
case showThumbnailRawKey:
|
case showThumbnailRawKey:
|
||||||
case showThumbnailVideoDurationKey:
|
case showThumbnailVideoDurationKey:
|
||||||
case showOverlayOnOpeningKey:
|
case showOverlayOnOpeningKey:
|
||||||
|
|
|
@ -66,6 +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)
|
||||||
// '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)
|
||||||
|
|
|
@ -111,8 +111,9 @@ class AIcons {
|
||||||
static const IconData geo = Icons.language_outlined;
|
static const IconData geo = Icons.language_outlined;
|
||||||
static const IconData motionPhoto = Icons.motion_photos_on_outlined;
|
static const IconData motionPhoto = Icons.motion_photos_on_outlined;
|
||||||
static const IconData multiPage = Icons.burst_mode_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 threeSixty = Icons.threesixty_outlined;
|
||||||
|
static const IconData videoThumb = Icons.play_circle_outline;
|
||||||
static const IconData selected = Icons.check_circle_outline;
|
static const IconData selected = Icons.check_circle_outline;
|
||||||
static const IconData unselected = Icons.radio_button_unchecked;
|
static const IconData unselected = Icons.radio_button_unchecked;
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ class GridTheme extends StatelessWidget {
|
||||||
highlightBorderWidth: highlightBorderWidth,
|
highlightBorderWidth: highlightBorderWidth,
|
||||||
showLocation: showLocation ?? settings.showThumbnailLocation,
|
showLocation: showLocation ?? settings.showThumbnailLocation,
|
||||||
showMotionPhoto: settings.showThumbnailMotionPhoto,
|
showMotionPhoto: settings.showThumbnailMotionPhoto,
|
||||||
|
showRating: settings.showThumbnailRating,
|
||||||
showRaw: settings.showThumbnailRaw,
|
showRaw: settings.showThumbnailRaw,
|
||||||
showVideoDuration: settings.showThumbnailVideoDuration,
|
showVideoDuration: settings.showThumbnailVideoDuration,
|
||||||
);
|
);
|
||||||
|
@ -41,7 +42,7 @@ class GridTheme extends StatelessWidget {
|
||||||
|
|
||||||
class GridThemeData {
|
class GridThemeData {
|
||||||
final double iconSize, fontSize, highlightBorderWidth;
|
final double iconSize, fontSize, highlightBorderWidth;
|
||||||
final bool showLocation, showMotionPhoto, showRaw, showVideoDuration;
|
final bool showLocation, showMotionPhoto, showRating, showRaw, showVideoDuration;
|
||||||
|
|
||||||
const GridThemeData({
|
const GridThemeData({
|
||||||
required this.iconSize,
|
required this.iconSize,
|
||||||
|
@ -49,6 +50,7 @@ class GridThemeData {
|
||||||
required this.highlightBorderWidth,
|
required this.highlightBorderWidth,
|
||||||
required this.showLocation,
|
required this.showLocation,
|
||||||
required this.showMotionPhoto,
|
required this.showMotionPhoto,
|
||||||
|
required this.showRating,
|
||||||
required this.showRaw,
|
required this.showRaw,
|
||||||
required this.showVideoDuration,
|
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 {
|
class OverlayIcon extends StatelessWidget {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String? text;
|
final String? text;
|
||||||
|
|
|
@ -21,12 +21,11 @@ class ThumbnailEntryOverlay extends StatelessWidget {
|
||||||
final children = [
|
final children = [
|
||||||
if (entry.hasGps && context.select<GridThemeData, bool>((t) => t.showLocation)) const GpsIcon(),
|
if (entry.hasGps && context.select<GridThemeData, bool>((t) => t.showLocation)) const GpsIcon(),
|
||||||
if (entry.isVideo)
|
if (entry.isVideo)
|
||||||
VideoIcon(
|
VideoIcon(entry: entry)
|
||||||
entry: entry,
|
|
||||||
)
|
|
||||||
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.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(),
|
||||||
|
|
|
@ -20,11 +20,6 @@ class ThumbnailsSection extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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);
|
final iconSize = IconTheme.of(context).size! * MediaQuery.textScaleFactorOf(context);
|
||||||
double opacityFor(bool enabled) => enabled ? 1 : .2;
|
double opacityFor(bool enabled) => enabled ? 1 : .2;
|
||||||
|
|
||||||
|
@ -38,14 +33,16 @@ class ThumbnailsSection extends StatelessWidget {
|
||||||
showHighlight: false,
|
showHighlight: false,
|
||||||
children: [
|
children: [
|
||||||
const CollectionActionsTile(),
|
const CollectionActionsTile(),
|
||||||
SwitchListTile(
|
Selector<Settings, bool>(
|
||||||
value: currentShowThumbnailLocation,
|
selector: (context, s) => s.showThumbnailLocation,
|
||||||
|
builder: (context, current, child) => SwitchListTile(
|
||||||
|
value: current,
|
||||||
onChanged: (v) => settings.showThumbnailLocation = v,
|
onChanged: (v) => settings.showThumbnailLocation = v,
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: Text(context.l10n.settingsThumbnailShowLocationIcon)),
|
Expanded(child: Text(context.l10n.settingsThumbnailShowLocationIcon)),
|
||||||
AnimatedOpacity(
|
AnimatedOpacity(
|
||||||
opacity: opacityFor(currentShowThumbnailLocation),
|
opacity: opacityFor(current),
|
||||||
duration: Durations.toggleableTransitionAnimation,
|
duration: Durations.toggleableTransitionAnimation,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
AIcons.location,
|
AIcons.location,
|
||||||
|
@ -55,14 +52,17 @@ class ThumbnailsSection extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
),
|
||||||
value: currentShowThumbnailMotionPhoto,
|
Selector<Settings, bool>(
|
||||||
|
selector: (context, s) => s.showThumbnailMotionPhoto,
|
||||||
|
builder: (context, current, child) => SwitchListTile(
|
||||||
|
value: current,
|
||||||
onChanged: (v) => settings.showThumbnailMotionPhoto = v,
|
onChanged: (v) => settings.showThumbnailMotionPhoto = v,
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: Text(context.l10n.settingsThumbnailShowMotionPhotoIcon)),
|
Expanded(child: Text(context.l10n.settingsThumbnailShowMotionPhotoIcon)),
|
||||||
AnimatedOpacity(
|
AnimatedOpacity(
|
||||||
opacity: opacityFor(currentShowThumbnailMotionPhoto),
|
opacity: opacityFor(current),
|
||||||
duration: Durations.toggleableTransitionAnimation,
|
duration: Durations.toggleableTransitionAnimation,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - MotionPhotoIcon.scale) / 2),
|
padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - MotionPhotoIcon.scale) / 2),
|
||||||
|
@ -75,14 +75,37 @@ class ThumbnailsSection extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
),
|
||||||
value: currentShowThumbnailRaw,
|
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,
|
onChanged: (v) => settings.showThumbnailRaw = v,
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: Text(context.l10n.settingsThumbnailShowRawIcon)),
|
Expanded(child: Text(context.l10n.settingsThumbnailShowRawIcon)),
|
||||||
AnimatedOpacity(
|
AnimatedOpacity(
|
||||||
opacity: opacityFor(currentShowThumbnailRaw),
|
opacity: opacityFor(current),
|
||||||
duration: Durations.toggleableTransitionAnimation,
|
duration: Durations.toggleableTransitionAnimation,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
AIcons.raw,
|
AIcons.raw,
|
||||||
|
@ -92,11 +115,15 @@ class ThumbnailsSection extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
),
|
||||||
value: currentShowThumbnailVideoDuration,
|
Selector<Settings, bool>(
|
||||||
|
selector: (context, s) => s.showThumbnailVideoDuration,
|
||||||
|
builder: (context, current, child) => SwitchListTile(
|
||||||
|
value: current,
|
||||||
onChanged: (v) => settings.showThumbnailVideoDuration = v,
|
onChanged: (v) => settings.showThumbnailVideoDuration = v,
|
||||||
title: Text(context.l10n.settingsThumbnailShowVideoDuration),
|
title: Text(context.l10n.settingsThumbnailShowVideoDuration),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,6 +123,7 @@ class _DbTabState extends State<DbTab> {
|
||||||
'longitude': '${data.longitude}',
|
'longitude': '${data.longitude}',
|
||||||
'xmpSubjects': data.xmpSubjects ?? '',
|
'xmpSubjects': data.xmpSubjects ?? '',
|
||||||
'xmpTitleDescription': data.xmpTitleDescription ?? '',
|
'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/utils/file_utils.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_filter_chip.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/action/entry_info_action_delegate.dart';
|
||||||
|
import 'package:aves/widgets/viewer/info/common.dart';
|
||||||
import 'package:aves/widgets/viewer/info/owner.dart';
|
import 'package:aves/widgets/viewer/info/owner.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -74,15 +74,32 @@ class BasicSection extends StatelessWidget {
|
||||||
if (path != null) l10n.viewerInfoLabelPath: path,
|
if (path != null) l10n.viewerInfoLabelPath: path,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
OwnerProp(
|
OwnerProp(entry: entry),
|
||||||
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;
|
||||||
|
|
|
@ -4,7 +4,16 @@
|
||||||
"editEntryDateDialogSourceCustomDate",
|
"editEntryDateDialogSourceCustomDate",
|
||||||
"editEntryDateDialogSourceTitle",
|
"editEntryDateDialogSourceTitle",
|
||||||
"editEntryDateDialogSourceFileModifiedDate",
|
"editEntryDateDialogSourceFileModifiedDate",
|
||||||
"editEntryDateDialogTargetFieldsHeader"
|
"editEntryDateDialogTargetFieldsHeader",
|
||||||
|
"settingsThumbnailShowRatingIcon"
|
||||||
|
],
|
||||||
|
|
||||||
|
"fr": [
|
||||||
|
"settingsThumbnailShowRatingIcon"
|
||||||
|
],
|
||||||
|
|
||||||
|
"ko": [
|
||||||
|
"settingsThumbnailShowRatingIcon"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
|
@ -12,6 +21,7 @@
|
||||||
"editEntryDateDialogSourceCustomDate",
|
"editEntryDateDialogSourceCustomDate",
|
||||||
"editEntryDateDialogSourceTitle",
|
"editEntryDateDialogSourceTitle",
|
||||||
"editEntryDateDialogSourceFileModifiedDate",
|
"editEntryDateDialogSourceFileModifiedDate",
|
||||||
"editEntryDateDialogTargetFieldsHeader"
|
"editEntryDateDialogTargetFieldsHeader",
|
||||||
|
"settingsThumbnailShowRatingIcon"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue