#301 info: edit description
This commit is contained in:
parent
1d4d0307d7
commit
c015c71fa9
33 changed files with 342 additions and 70 deletions
|
@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
|
|||
- Viewer: optional gesture to show previous/next item
|
||||
- Albums / Countries / Tags: live title filter
|
||||
- option to hide confirmation message after moving items to the bin
|
||||
- Collection / Info: edit description via Exif / IPTC / XMP
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
@ -106,6 +106,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
"hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentResolverProp) }
|
||||
"getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentResolverProp) }
|
||||
"getDate" -> ioScope.launch { safe(call, result, ::getDate) }
|
||||
"getDescription" -> ioScope.launch { safe(call, result, ::getDescription) }
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
@ -409,9 +410,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
// - XMP / photoshop:DateCreated
|
||||
// - PNG / TIME / LAST_MODIFICATION_TIME
|
||||
// - Video / METADATA_KEY_DATE
|
||||
// set `KEY_XMP_TITLE_DESCRIPTION` from these fields (by precedence):
|
||||
// set `KEY_XMP_TITLE` from this field:
|
||||
// - XMP / dc:title
|
||||
// - XMP / dc:description
|
||||
// set `KEY_XMP_SUBJECTS` from these fields (by precedence):
|
||||
// - XMP / dc:subject
|
||||
// - IPTC / keywords
|
||||
|
@ -514,10 +514,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
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.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.DC_DESCRIPTION_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] = it }
|
||||
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
|
||||
xmpMeta.getSafeDateMillis(XMP.XMP_SCHEMA_NS, XMP.XMP_CREATE_DATE_PROP_NAME) { metadataMap[KEY_DATE_MILLIS] = it }
|
||||
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
|
||||
|
@ -1052,6 +1049,58 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
result.success(dateMillis)
|
||||
}
|
||||
|
||||
// return description from these fields (by precedence):
|
||||
// - XMP / dc:description
|
||||
// - IPTC / caption-abstract
|
||||
// - Exif / ImageDescription
|
||||
private fun getDescription(call: MethodCall, result: MethodChannel.Result) {
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
if (mimeType == null || uri == null) {
|
||||
result.error("getDescription-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
var description: String? = null
|
||||
if (canReadWithMetadataExtractor(mimeType)) {
|
||||
try {
|
||||
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
||||
val metadata = MetadataExtractorHelper.safeRead(input)
|
||||
|
||||
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
||||
val xmpMeta = dir.xmpMeta
|
||||
try {
|
||||
if (xmpMeta.doesPropertyExist(XMP.DC_SCHEMA_NS, XMP.DC_DESCRIPTION_PROP_NAME)) {
|
||||
xmpMeta.getSafeLocalizedText(XMP.DC_SCHEMA_NS, XMP.DC_DESCRIPTION_PROP_NAME) { description = it }
|
||||
}
|
||||
} catch (e: XMPException) {
|
||||
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
|
||||
}
|
||||
}
|
||||
if (description == null) {
|
||||
for (dir in metadata.getDirectoriesOfType(IptcDirectory::class.java)) {
|
||||
dir.getSafeString(IptcDirectory.TAG_CAPTION) { description = it }
|
||||
}
|
||||
}
|
||||
if (description == null) {
|
||||
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
|
||||
dir.getSafeString(ExifIFD0Directory.TAG_IMAGE_DESCRIPTION) { description = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
|
||||
} catch (e: AssertionError) {
|
||||
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
|
||||
}
|
||||
}
|
||||
|
||||
result.success(description)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<MetadataFetchHandler>()
|
||||
const val CHANNEL = "deckers.thibault/aves/metadata_fetch"
|
||||
|
@ -1100,7 +1149,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
private const val KEY_LATITUDE = "latitude"
|
||||
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_XMP_TITLE = "xmpTitleDescription"
|
||||
private const val KEY_RATING = "rating"
|
||||
|
||||
private const val MASK_IS_ANIMATED = 1 shl 0
|
||||
|
|
|
@ -28,8 +28,8 @@ 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 DC_DESCRIPTION_PROP_NAME = "dc:description"
|
||||
const val DC_SUBJECT_PROP_NAME = "dc:subject"
|
||||
const val DC_DESCRIPTION_PROP_NAME = "dc:description"
|
||||
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"
|
||||
|
|
|
@ -238,6 +238,8 @@
|
|||
|
||||
"renameEntryDialogLabel": "Neuer Name",
|
||||
|
||||
"editEntryDialogTargetFieldsHeader": "Zu ändernde Felder",
|
||||
|
||||
"editEntryDateDialogTitle": "Datum & Uhrzeit",
|
||||
"editEntryDateDialogSetCustom": "Datum einstellen",
|
||||
"editEntryDateDialogCopyField": "Von anderem Datum kopieren",
|
||||
|
@ -245,7 +247,6 @@
|
|||
"editEntryDateDialogExtractFromTitle": "Auszug aus dem Titel",
|
||||
"editEntryDateDialogShift": "Verschieben",
|
||||
"editEntryDateDialogSourceFileModifiedDate": "Änderungsdatum der Datei",
|
||||
"editEntryDateDialogTargetFieldsHeader": "Zu ändernde Felder",
|
||||
"editEntryDateDialogHours": "Stunden",
|
||||
"editEntryDateDialogMinutes": "Minuten",
|
||||
|
||||
|
|
|
@ -115,6 +115,7 @@
|
|||
|
||||
"entryInfoActionEditDate": "Edit date & time",
|
||||
"entryInfoActionEditLocation": "Edit location",
|
||||
"entryInfoActionEditDescription": "Edit description",
|
||||
"entryInfoActionEditRating": "Edit rating",
|
||||
"entryInfoActionEditTags": "Edit tags",
|
||||
"entryInfoActionRemoveMetadata": "Remove metadata",
|
||||
|
@ -368,6 +369,8 @@
|
|||
|
||||
"renameEntryDialogLabel": "New name",
|
||||
|
||||
"editEntryDialogTargetFieldsHeader": "Fields to modify",
|
||||
|
||||
"editEntryDateDialogTitle": "Date & Time",
|
||||
"editEntryDateDialogSetCustom": "Set custom date",
|
||||
"editEntryDateDialogCopyField": "Copy from other date",
|
||||
|
@ -375,7 +378,6 @@
|
|||
"editEntryDateDialogExtractFromTitle": "Extract from title",
|
||||
"editEntryDateDialogShift": "Shift",
|
||||
"editEntryDateDialogSourceFileModifiedDate": "File modified date",
|
||||
"editEntryDateDialogTargetFieldsHeader": "Fields to modify",
|
||||
"editEntryDateDialogHours": "Hours",
|
||||
"editEntryDateDialogMinutes": "Minutes",
|
||||
|
||||
|
@ -386,6 +388,8 @@
|
|||
|
||||
"locationPickerUseThisLocationButton": "Use this location",
|
||||
|
||||
"editEntryDescriptionDialogTitle": "Description",
|
||||
|
||||
"editEntryRatingDialogTitle": "Rating",
|
||||
|
||||
"removeEntryMetadataDialogTitle": "Metadata Removal",
|
||||
|
|
|
@ -238,6 +238,8 @@
|
|||
|
||||
"renameEntryDialogLabel": "Renombrar",
|
||||
|
||||
"editEntryDialogTargetFieldsHeader": "Campos a modificar",
|
||||
|
||||
"editEntryDateDialogTitle": "Fecha y hora",
|
||||
"editEntryDateDialogSetCustom": "Establecer fecha personalizada",
|
||||
"editEntryDateDialogCopyField": "Copiar de otra fecha",
|
||||
|
@ -245,7 +247,6 @@
|
|||
"editEntryDateDialogExtractFromTitle": "Extraer del título",
|
||||
"editEntryDateDialogShift": "Cambiar",
|
||||
"editEntryDateDialogSourceFileModifiedDate": "Fecha de modificación del archivo",
|
||||
"editEntryDateDialogTargetFieldsHeader": "Campos a modificar",
|
||||
"editEntryDateDialogHours": "Horas",
|
||||
"editEntryDateDialogMinutes": "Minutos",
|
||||
|
||||
|
|
|
@ -238,6 +238,8 @@
|
|||
|
||||
"renameEntryDialogLabel": "Nouveau nom",
|
||||
|
||||
"editEntryDialogTargetFieldsHeader": "Champs à modifier",
|
||||
|
||||
"editEntryDateDialogTitle": "Date & Heure",
|
||||
"editEntryDateDialogSetCustom": "Régler une date personnalisée",
|
||||
"editEntryDateDialogCopyField": "Copier d’une autre date",
|
||||
|
@ -245,7 +247,6 @@
|
|||
"editEntryDateDialogExtractFromTitle": "Extraire du titre",
|
||||
"editEntryDateDialogShift": "Décaler",
|
||||
"editEntryDateDialogSourceFileModifiedDate": "Date de modification du fichier",
|
||||
"editEntryDateDialogTargetFieldsHeader": "Champs à modifier",
|
||||
"editEntryDateDialogHours": "Heures",
|
||||
"editEntryDateDialogMinutes": "Minutes",
|
||||
|
||||
|
|
|
@ -238,6 +238,8 @@
|
|||
|
||||
"renameEntryDialogLabel": "Nama baru",
|
||||
|
||||
"editEntryDialogTargetFieldsHeader": "Bidang untuk dimodifikasikan",
|
||||
|
||||
"editEntryDateDialogTitle": "Tanggal & Waktu",
|
||||
"editEntryDateDialogSetCustom": "Atur tanggal khusus",
|
||||
"editEntryDateDialogCopyField": "Salin dari tanggal lain",
|
||||
|
@ -245,7 +247,6 @@
|
|||
"editEntryDateDialogExtractFromTitle": "Ekstrak dari judul",
|
||||
"editEntryDateDialogShift": "Geser",
|
||||
"editEntryDateDialogSourceFileModifiedDate": "Tanggal modifikasi file",
|
||||
"editEntryDateDialogTargetFieldsHeader": "Bidang untuk dimodifikasikan",
|
||||
"editEntryDateDialogHours": "Jam",
|
||||
"editEntryDateDialogMinutes": "Menit",
|
||||
|
||||
|
|
|
@ -238,6 +238,8 @@
|
|||
|
||||
"renameEntryDialogLabel": "Nuovo nome",
|
||||
|
||||
"editEntryDialogTargetFieldsHeader": "Campi da modificare",
|
||||
|
||||
"editEntryDateDialogTitle": "Data e ora",
|
||||
"editEntryDateDialogSetCustom": "Imposta data personalizzata",
|
||||
"editEntryDateDialogCopyField": "Copia da un’altra data",
|
||||
|
@ -245,7 +247,6 @@
|
|||
"editEntryDateDialogExtractFromTitle": "Estrai dal titolo",
|
||||
"editEntryDateDialogShift": "Turno",
|
||||
"editEntryDateDialogSourceFileModifiedDate": "Data di modifica del file",
|
||||
"editEntryDateDialogTargetFieldsHeader": "Campi da modificare",
|
||||
"editEntryDateDialogHours": "Ore",
|
||||
"editEntryDateDialogMinutes": "Minuti",
|
||||
|
||||
|
|
|
@ -238,6 +238,8 @@
|
|||
|
||||
"renameEntryDialogLabel": "新しい名前",
|
||||
|
||||
"editEntryDialogTargetFieldsHeader": "更新するフィールド",
|
||||
|
||||
"editEntryDateDialogTitle": "日時",
|
||||
"editEntryDateDialogSetCustom": "日を設定する",
|
||||
"editEntryDateDialogCopyField": "他の日からコピーする",
|
||||
|
@ -245,7 +247,6 @@
|
|||
"editEntryDateDialogExtractFromTitle": "タイトルから抽出する",
|
||||
"editEntryDateDialogShift": "シフト",
|
||||
"editEntryDateDialogSourceFileModifiedDate": "ファイル更新日",
|
||||
"editEntryDateDialogTargetFieldsHeader": "更新するフィールド",
|
||||
"editEntryDateDialogHours": "時",
|
||||
"editEntryDateDialogMinutes": "分",
|
||||
|
||||
|
|
|
@ -238,6 +238,8 @@
|
|||
|
||||
"renameEntryDialogLabel": "이름",
|
||||
|
||||
"editEntryDialogTargetFieldsHeader": "수정할 필드",
|
||||
|
||||
"editEntryDateDialogTitle": "날짜 및 시간",
|
||||
"editEntryDateDialogSetCustom": "지정 날짜로 편집",
|
||||
"editEntryDateDialogCopyField": "다른 날짜에서 지정",
|
||||
|
@ -245,7 +247,6 @@
|
|||
"editEntryDateDialogExtractFromTitle": "제목에서 추출",
|
||||
"editEntryDateDialogShift": "시간 이동",
|
||||
"editEntryDateDialogSourceFileModifiedDate": "파일 수정한 날짜",
|
||||
"editEntryDateDialogTargetFieldsHeader": "수정할 필드",
|
||||
"editEntryDateDialogHours": "시간",
|
||||
"editEntryDateDialogMinutes": "분",
|
||||
|
||||
|
|
|
@ -238,6 +238,8 @@
|
|||
|
||||
"renameEntryDialogLabel": "Novo nome",
|
||||
|
||||
"editEntryDialogTargetFieldsHeader": "Campos para modificar",
|
||||
|
||||
"editEntryDateDialogTitle": "Data e hora",
|
||||
"editEntryDateDialogSetCustom": "Definir data personalizada",
|
||||
"editEntryDateDialogCopyField": "Copiar de outra data",
|
||||
|
@ -245,7 +247,6 @@
|
|||
"editEntryDateDialogExtractFromTitle": "Extrair do título",
|
||||
"editEntryDateDialogShift": "Mudança",
|
||||
"editEntryDateDialogSourceFileModifiedDate": "Data de modificação do arquivo",
|
||||
"editEntryDateDialogTargetFieldsHeader": "Campos para modificar",
|
||||
"editEntryDateDialogHours": "Horas",
|
||||
"editEntryDateDialogMinutes": "Minutos",
|
||||
|
||||
|
|
|
@ -237,6 +237,8 @@
|
|||
|
||||
"renameEntryDialogLabel": "Новое название",
|
||||
|
||||
"editEntryDialogTargetFieldsHeader": "Поля для изменения",
|
||||
|
||||
"editEntryDateDialogTitle": "Дата и время",
|
||||
"editEntryDateDialogSetCustom": "Установить дату",
|
||||
"editEntryDateDialogCopyField": "Копировать с другой даты",
|
||||
|
@ -244,7 +246,6 @@
|
|||
"editEntryDateDialogExtractFromTitle": "Извлечь из названия",
|
||||
"editEntryDateDialogShift": "Сдвиг",
|
||||
"editEntryDateDialogSourceFileModifiedDate": "Дата изменения файла",
|
||||
"editEntryDateDialogTargetFieldsHeader": "Поля для изменения",
|
||||
"editEntryDateDialogHours": "Часов",
|
||||
"editEntryDateDialogMinutes": "Минут",
|
||||
|
||||
|
|
|
@ -230,6 +230,8 @@
|
|||
|
||||
"renameEntryDialogLabel": "Yeni ad",
|
||||
|
||||
"editEntryDialogTargetFieldsHeader": "Değiştirilecek alanlar",
|
||||
|
||||
"editEntryDateDialogTitle": "Tarih ve Saat",
|
||||
"editEntryDateDialogSetCustom": "Özel tarih ayarla",
|
||||
"editEntryDateDialogCopyField": "Başka bir tarihten kopyala",
|
||||
|
@ -237,7 +239,6 @@
|
|||
"editEntryDateDialogExtractFromTitle": "Başlıktan ayıkla",
|
||||
"editEntryDateDialogShift": "Değişim",
|
||||
"editEntryDateDialogSourceFileModifiedDate": "Dosya değiştirilme tarihi",
|
||||
"editEntryDateDialogTargetFieldsHeader": "Değiştirilecek alanlar",
|
||||
"editEntryDateDialogHours": "Saat",
|
||||
"editEntryDateDialogMinutes": "Dakika",
|
||||
|
||||
|
|
|
@ -238,6 +238,8 @@
|
|||
|
||||
"renameEntryDialogLabel": "新名称",
|
||||
|
||||
"editEntryDialogTargetFieldsHeader": "待修改的字段",
|
||||
|
||||
"editEntryDateDialogTitle": "日期和时间",
|
||||
"editEntryDateDialogSetCustom": "设置自定义日期",
|
||||
"editEntryDateDialogCopyField": "复制自其他日期",
|
||||
|
@ -245,7 +247,6 @@
|
|||
"editEntryDateDialogExtractFromTitle": "从标题提取",
|
||||
"editEntryDateDialogShift": "转移",
|
||||
"editEntryDateDialogSourceFileModifiedDate": "文件修改日期",
|
||||
"editEntryDateDialogTargetFieldsHeader": "待修改的字段",
|
||||
"editEntryDateDialogHours": "时",
|
||||
"editEntryDateDialogMinutes": "分",
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ enum EntryInfoAction {
|
|||
// general
|
||||
editDate,
|
||||
editLocation,
|
||||
editDescription,
|
||||
editRating,
|
||||
editTags,
|
||||
removeMetadata,
|
||||
|
@ -23,6 +24,7 @@ class EntryInfoActions {
|
|||
static const common = [
|
||||
EntryInfoAction.editDate,
|
||||
EntryInfoAction.editLocation,
|
||||
EntryInfoAction.editDescription,
|
||||
EntryInfoAction.editRating,
|
||||
EntryInfoAction.editTags,
|
||||
EntryInfoAction.removeMetadata,
|
||||
|
@ -43,6 +45,8 @@ extension ExtraEntryInfoAction on EntryInfoAction {
|
|||
return context.l10n.entryInfoActionEditDate;
|
||||
case EntryInfoAction.editLocation:
|
||||
return context.l10n.entryInfoActionEditLocation;
|
||||
case EntryInfoAction.editDescription:
|
||||
return context.l10n.entryInfoActionEditDescription;
|
||||
case EntryInfoAction.editRating:
|
||||
return context.l10n.entryInfoActionEditRating;
|
||||
case EntryInfoAction.editTags:
|
||||
|
@ -84,6 +88,8 @@ extension ExtraEntryInfoAction on EntryInfoAction {
|
|||
return AIcons.date;
|
||||
case EntryInfoAction.editLocation:
|
||||
return AIcons.location;
|
||||
case EntryInfoAction.editDescription:
|
||||
return AIcons.description;
|
||||
case EntryInfoAction.editRating:
|
||||
return AIcons.editRating;
|
||||
case EntryInfoAction.editTags:
|
||||
|
|
|
@ -31,6 +31,7 @@ enum EntrySetAction {
|
|||
flip,
|
||||
editDate,
|
||||
editLocation,
|
||||
editDescription,
|
||||
editRating,
|
||||
editTags,
|
||||
removeMetadata,
|
||||
|
@ -99,6 +100,7 @@ class EntrySetActions {
|
|||
static const edit = [
|
||||
EntrySetAction.editDate,
|
||||
EntrySetAction.editLocation,
|
||||
EntrySetAction.editDescription,
|
||||
EntrySetAction.editRating,
|
||||
EntrySetAction.editTags,
|
||||
EntrySetAction.removeMetadata,
|
||||
|
@ -162,6 +164,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
|||
return context.l10n.entryInfoActionEditDate;
|
||||
case EntrySetAction.editLocation:
|
||||
return context.l10n.entryInfoActionEditLocation;
|
||||
case EntrySetAction.editDescription:
|
||||
return context.l10n.entryInfoActionEditDescription;
|
||||
case EntrySetAction.editRating:
|
||||
return context.l10n.entryInfoActionEditRating;
|
||||
case EntrySetAction.editTags:
|
||||
|
@ -229,6 +233,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
|||
return AIcons.date;
|
||||
case EntrySetAction.editLocation:
|
||||
return AIcons.location;
|
||||
case EntrySetAction.editDescription:
|
||||
return AIcons.description;
|
||||
case EntrySetAction.editRating:
|
||||
return AIcons.editRating;
|
||||
case EntrySetAction.editTags:
|
||||
|
|
|
@ -257,6 +257,8 @@ class AvesEntry {
|
|||
|
||||
bool get canEditLocation => canEdit && canEditExif;
|
||||
|
||||
bool get canEditDescription => canEdit && (canEditExif || canEditXmp);
|
||||
|
||||
bool get canEditRating => canEdit && canEditXmp;
|
||||
|
||||
bool get canEditTags => canEdit && canEditXmp;
|
||||
|
|
|
@ -140,6 +140,54 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
|||
return _changeOrientation(() => metadataEditService.flip(this));
|
||||
}
|
||||
|
||||
// write:
|
||||
// - Exif / ImageDescription
|
||||
// - IPTC / caption-abstract, if IPTC exists
|
||||
// - XMP / dc:description
|
||||
Future<Set<EntryDataType>> editDescription(String? description) async {
|
||||
final Set<EntryDataType> dataTypes = {};
|
||||
final Map<MetadataType, dynamic> metadata = {};
|
||||
|
||||
final missingDate = await _missingDateCheckAndExifEdit(dataTypes);
|
||||
|
||||
if (canEditExif) {
|
||||
metadata[MetadataType.exif] = {MetadataField.exifImageDescription.exifInterfaceTag!: description};
|
||||
}
|
||||
|
||||
if (canEditIptc) {
|
||||
final iptc = await metadataFetchService.getIptc(this);
|
||||
if (iptc != null) {
|
||||
editIptcValues(iptc, IPTC.applicationRecord, IPTC.captionAbstractTag, {if (description != null) description});
|
||||
metadata[MetadataType.iptc] = iptc;
|
||||
}
|
||||
}
|
||||
|
||||
if (canEditXmp) {
|
||||
metadata[MetadataType.xmp] = await _editXmp((descriptions) {
|
||||
final modified = XMP.setAttribute(
|
||||
descriptions,
|
||||
XMP.dcDescription,
|
||||
description,
|
||||
namespace: Namespaces.dc,
|
||||
strat: XmpEditStrategy.always,
|
||||
);
|
||||
if (modified && missingDate != null) {
|
||||
editCreateDateXmp(descriptions, missingDate);
|
||||
}
|
||||
return modified;
|
||||
});
|
||||
}
|
||||
|
||||
final newFields = await metadataEditService.editMetadata(this, metadata);
|
||||
if (newFields.isNotEmpty) {
|
||||
dataTypes.addAll({
|
||||
EntryDataType.basic,
|
||||
});
|
||||
}
|
||||
|
||||
return dataTypes;
|
||||
}
|
||||
|
||||
// write:
|
||||
// - IPTC / keywords, if IPTC exists
|
||||
// - XMP / dc:subject
|
||||
|
@ -152,7 +200,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
|||
if (canEditIptc) {
|
||||
final iptc = await metadataFetchService.getIptc(this);
|
||||
if (iptc != null) {
|
||||
editTagsIptc(iptc, tags);
|
||||
editIptcValues(iptc, IPTC.applicationRecord, IPTC.keywordsTag, tags);
|
||||
metadata[MetadataType.iptc] = iptc;
|
||||
}
|
||||
}
|
||||
|
@ -245,9 +293,18 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
|||
};
|
||||
}
|
||||
|
||||
static void editIptcValues(List<Map<String, dynamic>> iptc, int record, int tag, Set<String> values) {
|
||||
iptc.removeWhere((v) => v['record'] == record && v['tag'] == tag);
|
||||
iptc.add({
|
||||
'record': record,
|
||||
'tag': tag,
|
||||
'values': values.map((v) => utf8.encode(v)).toList(),
|
||||
});
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static void editCreateDateXmp(List<XmlNode> descriptions, DateTime? date) {
|
||||
XMP.setAttribute(
|
||||
static bool editCreateDateXmp(List<XmlNode> descriptions, DateTime? date) {
|
||||
return XMP.setAttribute(
|
||||
descriptions,
|
||||
XMP.xmpCreateDate,
|
||||
date != null ? XMP.toXmpDate(date) : null,
|
||||
|
@ -256,16 +313,6 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
|||
);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static void editTagsIptc(List<Map<String, dynamic>> iptc, Set<String> tags) {
|
||||
iptc.removeWhere((v) => v['record'] == IPTC.applicationRecord && v['tag'] == IPTC.keywordsTag);
|
||||
iptc.add({
|
||||
'record': IPTC.applicationRecord,
|
||||
'tag': IPTC.keywordsTag,
|
||||
'values': tags.map((v) => utf8.encode(v)).toList(),
|
||||
});
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static bool editTagsXmp(List<XmlNode> descriptions, Set<String> tags) {
|
||||
return XMP.setStringBag(
|
||||
|
@ -366,7 +413,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
|||
}
|
||||
|
||||
Future<DateModifier?> _applyDateModifierToEntry(DateModifier modifier) async {
|
||||
Set<MetadataField> mainMetadataDate() => {canEditExif ? MetadataField.exifDateOriginal : MetadataField.xmpCreateDate};
|
||||
Set<MetadataField> mainMetadataDate() => {canEditExif ? MetadataField.exifDateOriginal : MetadataField.xmpXmpCreateDate};
|
||||
|
||||
switch (modifier.action) {
|
||||
case DateEditAction.copyField:
|
||||
|
|
|
@ -6,12 +6,12 @@ import 'package:flutter/widgets.dart';
|
|||
|
||||
@immutable
|
||||
class DateModifier extends Equatable {
|
||||
static const writableDateFields = [
|
||||
static const writableFields = [
|
||||
MetadataField.exifDate,
|
||||
MetadataField.exifDateOriginal,
|
||||
MetadataField.exifDateDigitized,
|
||||
MetadataField.exifGpsDatestamp,
|
||||
MetadataField.xmpCreateDate,
|
||||
MetadataField.xmpXmpCreateDate,
|
||||
];
|
||||
|
||||
final DateEditAction action;
|
||||
|
|
|
@ -36,7 +36,8 @@ enum MetadataField {
|
|||
exifGpsTrack,
|
||||
exifGpsTrackRef,
|
||||
exifGpsVersionId,
|
||||
xmpCreateDate,
|
||||
exifImageDescription,
|
||||
xmpXmpCreateDate,
|
||||
}
|
||||
|
||||
class MetadataFields {
|
||||
|
@ -114,8 +115,9 @@ extension ExtraMetadataField on MetadataField {
|
|||
case MetadataField.exifGpsTrack:
|
||||
case MetadataField.exifGpsTrackRef:
|
||||
case MetadataField.exifGpsVersionId:
|
||||
case MetadataField.exifImageDescription:
|
||||
return MetadataType.exif;
|
||||
case MetadataField.xmpCreateDate:
|
||||
case MetadataField.xmpXmpCreateDate:
|
||||
return MetadataType.xmp;
|
||||
}
|
||||
}
|
||||
|
@ -192,8 +194,27 @@ extension ExtraMetadataField on MetadataField {
|
|||
return 'GPSTrackRef';
|
||||
case MetadataField.exifGpsVersionId:
|
||||
return 'GPSVersionID';
|
||||
case MetadataField.xmpCreateDate:
|
||||
case MetadataField.exifImageDescription:
|
||||
return 'ImageDescription';
|
||||
case MetadataField.xmpXmpCreateDate:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String get title {
|
||||
switch (this) {
|
||||
case MetadataField.exifDate:
|
||||
return 'Exif date';
|
||||
case MetadataField.exifDateOriginal:
|
||||
return 'Exif original date';
|
||||
case MetadataField.exifDateDigitized:
|
||||
return 'Exif digitized date';
|
||||
case MetadataField.exifGpsDatestamp:
|
||||
return 'Exif GPS date';
|
||||
case MetadataField.xmpXmpCreateDate:
|
||||
return 'XMP xmp:CreateDate';
|
||||
default:
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,4 +3,5 @@ class IPTC {
|
|||
|
||||
// ApplicationRecord tags
|
||||
static const int keywordsTag = 25;
|
||||
static const int captionAbstractTag = 120;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,8 @@ abstract class MetadataFetchService {
|
|||
Future<String?> getContentResolverProp(AvesEntry entry, String prop);
|
||||
|
||||
Future<DateTime?> getDate(AvesEntry entry, MetadataField field);
|
||||
|
||||
Future<String?> getDescription(AvesEntry entry);
|
||||
}
|
||||
|
||||
class PlatformMetadataFetchService implements MetadataFetchService {
|
||||
|
@ -75,7 +77,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
|||
// 'latitude': latitude (double)
|
||||
// 'longitude': longitude (double)
|
||||
// 'xmpSubjects': ';' separated XMP subjects (string)
|
||||
// 'xmpTitleDescription': XMP title or XMP description (string)
|
||||
// 'xmpTitleDescription': XMP title (string)
|
||||
final result = await _platform.invokeMethod('getCatalogMetadata', <String, dynamic>{
|
||||
'mimeType': entry.mimeType,
|
||||
'uri': entry.uri,
|
||||
|
@ -268,4 +270,20 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getDescription(AvesEntry entry) async {
|
||||
try {
|
||||
return await _platform.invokeMethod('getDescription', <String, dynamic>{
|
||||
'mimeType': entry.mimeType,
|
||||
'uri': entry.uri,
|
||||
'sizeBytes': entry.sizeBytes,
|
||||
});
|
||||
} on PlatformException catch (e, stack) {
|
||||
if (!entry.isMissingAtPath) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ class AIcons {
|
|||
static const IconData checked = Icons.done_outlined;
|
||||
static const IconData counter = Icons.plus_one_outlined;
|
||||
static const IconData date = Icons.calendar_today_outlined;
|
||||
static const IconData description = Icons.description_outlined;
|
||||
static const IconData disc = Icons.fiber_manual_record;
|
||||
static const IconData display = Icons.light_mode_outlined;
|
||||
static const IconData error = Icons.error_outline;
|
||||
|
|
|
@ -35,6 +35,7 @@ class XMP {
|
|||
static const rdfRoot = 'RDF';
|
||||
static const rdfDescription = 'Description';
|
||||
static const containerDirectory = 'Directory';
|
||||
static const dcDescription = 'description';
|
||||
static const dcSubject = 'subject';
|
||||
static const msPhotoRating = 'Rating';
|
||||
static const xmpRating = 'Rating';
|
||||
|
|
|
@ -496,6 +496,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
case EntrySetAction.flip:
|
||||
case EntrySetAction.editDate:
|
||||
case EntrySetAction.editLocation:
|
||||
case EntrySetAction.editDescription:
|
||||
case EntrySetAction.editRating:
|
||||
case EntrySetAction.editTags:
|
||||
case EntrySetAction.removeMetadata:
|
||||
|
|
|
@ -92,6 +92,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
case EntrySetAction.flip:
|
||||
case EntrySetAction.editDate:
|
||||
case EntrySetAction.editLocation:
|
||||
case EntrySetAction.editDescription:
|
||||
case EntrySetAction.editRating:
|
||||
case EntrySetAction.editTags:
|
||||
case EntrySetAction.removeMetadata:
|
||||
|
@ -143,6 +144,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
case EntrySetAction.flip:
|
||||
case EntrySetAction.editDate:
|
||||
case EntrySetAction.editLocation:
|
||||
case EntrySetAction.editDescription:
|
||||
case EntrySetAction.editRating:
|
||||
case EntrySetAction.editTags:
|
||||
case EntrySetAction.removeMetadata:
|
||||
|
@ -219,6 +221,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
case EntrySetAction.editLocation:
|
||||
_editLocation(context);
|
||||
break;
|
||||
case EntrySetAction.editDescription:
|
||||
_editDescription(context);
|
||||
break;
|
||||
case EntrySetAction.editRating:
|
||||
_editRating(context);
|
||||
break;
|
||||
|
@ -490,6 +495,16 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
await _edit(context, entries, (entry) => entry.editLocation(location));
|
||||
}
|
||||
|
||||
Future<void> _editDescription(BuildContext context) async {
|
||||
final entries = await _getEditableTargetItems(context, canEdit: (entry) => entry.canEditDescription);
|
||||
if (entries == null || entries.isEmpty) return;
|
||||
|
||||
final description = await selectDescription(context, entries);
|
||||
if (description == null) return;
|
||||
|
||||
await _edit(context, entries, (entry) => entry.editDescription(description));
|
||||
}
|
||||
|
||||
Future<void> _editRating(BuildContext context) async {
|
||||
final entries = await _getEditableTargetItems(context, canEdit: (entry) => entry.canEditRating);
|
||||
if (entries == null || entries.isEmpty) return;
|
||||
|
|
|
@ -3,9 +3,11 @@ import 'package:aves/model/metadata/date_modifier.dart';
|
|||
import 'package:aves/model/metadata/enums.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/entry_editors/edit_date_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/entry_editors/edit_description_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/entry_editors/edit_location_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/entry_editors/edit_rating_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/entry_editors/edit_tags_dialog.dart';
|
||||
|
@ -17,39 +19,49 @@ mixin EntryEditorMixin {
|
|||
Future<DateModifier?> selectDateModifier(BuildContext context, Set<AvesEntry> entries, CollectionLens? collection) async {
|
||||
if (entries.isEmpty) return null;
|
||||
|
||||
final modifier = await showDialog<DateModifier>(
|
||||
return showDialog<DateModifier>(
|
||||
context: context,
|
||||
builder: (context) => EditEntryDateDialog(
|
||||
entry: entries.first,
|
||||
collection: collection,
|
||||
),
|
||||
);
|
||||
return modifier;
|
||||
}
|
||||
|
||||
Future<LatLng?> selectLocation(BuildContext context, Set<AvesEntry> entries, CollectionLens? collection) async {
|
||||
if (entries.isEmpty) return null;
|
||||
|
||||
final location = await showDialog<LatLng>(
|
||||
return showDialog<LatLng>(
|
||||
context: context,
|
||||
builder: (context) => EditEntryLocationDialog(
|
||||
entry: entries.first,
|
||||
collection: collection,
|
||||
),
|
||||
);
|
||||
return location;
|
||||
}
|
||||
|
||||
Future<String?> selectDescription(BuildContext context, Set<AvesEntry> entries) async {
|
||||
if (entries.isEmpty) return null;
|
||||
|
||||
final initialDescription = await metadataFetchService.getDescription(entries.first) ?? '';
|
||||
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => EditEntryDescriptionDialog(
|
||||
initialDescription: initialDescription,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int?> selectRating(BuildContext context, Set<AvesEntry> entries) async {
|
||||
if (entries.isEmpty) return null;
|
||||
|
||||
final rating = await showDialog<int>(
|
||||
return showDialog<int>(
|
||||
context: context,
|
||||
builder: (context) => EditEntryRatingDialog(
|
||||
entry: entries.first,
|
||||
),
|
||||
);
|
||||
return rating;
|
||||
}
|
||||
|
||||
Future<Map<AvesEntry, Set<String>>?> selectTags(BuildContext context, Set<AvesEntry> entries) async {
|
||||
|
|
|
@ -38,7 +38,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
|||
late ValueNotifier<int> _shiftHour, _shiftMinute;
|
||||
late ValueNotifier<String> _shiftSign;
|
||||
bool _showOptions = false;
|
||||
final Set<MetadataField> _fields = {...DateModifier.writableDateFields};
|
||||
final Set<MetadataField> _fields = {...DateModifier.writableFields};
|
||||
|
||||
DateTime get copyItemDate => _copyItemSource.bestDate ?? DateTime.now();
|
||||
|
||||
|
@ -276,14 +276,14 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
|||
children: [
|
||||
ExpansionPanel(
|
||||
headerBuilder: (context, isExpanded) => ListTile(
|
||||
title: Text(context.l10n.editEntryDateDialogTargetFieldsHeader),
|
||||
title: Text(context.l10n.editEntryDialogTargetFieldsHeader),
|
||||
),
|
||||
body: Column(
|
||||
children: DateModifier.writableDateFields
|
||||
children: DateModifier.writableFields
|
||||
.map((field) => SwitchListTile(
|
||||
value: _fields.contains(field),
|
||||
onChanged: (selected) => setState(() => selected ? _fields.add(field) : _fields.remove(field)),
|
||||
title: Text(_fieldTitle(field)),
|
||||
title: Text(field.title),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
|
@ -330,23 +330,6 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
|||
}
|
||||
}
|
||||
|
||||
String _fieldTitle(MetadataField field) {
|
||||
switch (field) {
|
||||
case MetadataField.exifDate:
|
||||
return 'Exif date';
|
||||
case MetadataField.exifDateOriginal:
|
||||
return 'Exif original date';
|
||||
case MetadataField.exifDateDigitized:
|
||||
return 'Exif digitized date';
|
||||
case MetadataField.exifGpsDatestamp:
|
||||
return 'Exif GPS date';
|
||||
case MetadataField.xmpCreateDate:
|
||||
return 'XMP xmp:CreateDate';
|
||||
default:
|
||||
return field.name;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _editDate() async {
|
||||
final _date = await showDatePicker(
|
||||
context: context,
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EditEntryDescriptionDialog extends StatefulWidget {
|
||||
final String initialDescription;
|
||||
|
||||
const EditEntryDescriptionDialog({
|
||||
super.key,
|
||||
required this.initialDescription,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EditEntryDescriptionDialog> createState() => _EditEntryDescriptionDialogState();
|
||||
}
|
||||
|
||||
class _EditEntryDescriptionDialogState extends State<EditEntryDescriptionDialog> {
|
||||
late final TextEditingController _textController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textController = TextEditingController(text: widget.initialDescription);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Builder(builder: (context) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
return AvesDialog(
|
||||
title: l10n.editEntryDescriptionDialogTitle,
|
||||
content: TextField(
|
||||
controller: _textController,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: null,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _submit(context),
|
||||
child: Text(l10n.applyButtonLabel),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
void _submit(BuildContext context) => Navigator.pop(context, _textController.text);
|
||||
}
|
|
@ -79,7 +79,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
|||
controller: _latitudeController,
|
||||
focusNode: _latitudeFocusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.editEntryLocationDialogLatitude,
|
||||
labelText: l10n.editEntryLocationDialogLatitude,
|
||||
hintText: coordinateFormatter.format(Constants.pointNemo.latitude),
|
||||
),
|
||||
onChanged: (_) => _validate(),
|
||||
|
@ -88,7 +88,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
|||
controller: _longitudeController,
|
||||
focusNode: _longitudeFocusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.editEntryLocationDialogLongitude,
|
||||
labelText: l10n.editEntryLocationDialogLongitude,
|
||||
hintText: coordinateFormatter.format(Constants.pointNemo.longitude),
|
||||
),
|
||||
onChanged: (_) => _validate(),
|
||||
|
@ -131,7 +131,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
|||
builder: (context, isValid, child) {
|
||||
return TextButton(
|
||||
onPressed: isValid ? () => _submit(context) : null,
|
||||
child: Text(context.l10n.applyButtonLabel),
|
||||
child: Text(l10n.applyButtonLabel),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
@ -35,6 +35,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
|
|||
// general
|
||||
case EntryInfoAction.editDate:
|
||||
case EntryInfoAction.editLocation:
|
||||
case EntryInfoAction.editDescription:
|
||||
case EntryInfoAction.editRating:
|
||||
case EntryInfoAction.editTags:
|
||||
case EntryInfoAction.removeMetadata:
|
||||
|
@ -59,6 +60,8 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
|
|||
return entry.canEditDate;
|
||||
case EntryInfoAction.editLocation:
|
||||
return entry.canEditLocation;
|
||||
case EntryInfoAction.editDescription:
|
||||
return entry.canEditDescription;
|
||||
case EntryInfoAction.editRating:
|
||||
return entry.canEditRating;
|
||||
case EntryInfoAction.editTags:
|
||||
|
@ -89,6 +92,9 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
|
|||
case EntryInfoAction.editLocation:
|
||||
await _editLocation(context);
|
||||
break;
|
||||
case EntryInfoAction.editDescription:
|
||||
await _editDescription(context);
|
||||
break;
|
||||
case EntryInfoAction.editRating:
|
||||
await _editRating(context);
|
||||
break;
|
||||
|
@ -131,6 +137,13 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
|
|||
await edit(context, () => entry.editLocation(location));
|
||||
}
|
||||
|
||||
Future<void> _editDescription(BuildContext context) async {
|
||||
final description = await selectDescription(context, {entry});
|
||||
if (description == null) return;
|
||||
|
||||
await edit(context, () => entry.editDescription(description));
|
||||
}
|
||||
|
||||
Future<void> _editRating(BuildContext context) async {
|
||||
final rating = await selectRating(context, {entry});
|
||||
if (rating == null) return;
|
||||
|
|
|
@ -1,43 +1,61 @@
|
|||
{
|
||||
"de": [
|
||||
"entryInfoActionEditDescription",
|
||||
"editEntryDescriptionDialogTitle",
|
||||
"settingsConfirmationAfterMoveToBinItems",
|
||||
"settingsViewerGestureSideTapNext"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"entryInfoActionEditDescription",
|
||||
"editEntryDescriptionDialogTitle",
|
||||
"settingsConfirmationAfterMoveToBinItems"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"entryInfoActionEditDescription",
|
||||
"editEntryDescriptionDialogTitle",
|
||||
"settingsViewerGestureSideTapNext"
|
||||
],
|
||||
|
||||
"id": [
|
||||
"entryInfoActionEditDescription",
|
||||
"editEntryDescriptionDialogTitle",
|
||||
"settingsConfirmationAfterMoveToBinItems",
|
||||
"settingsViewerGestureSideTapNext"
|
||||
],
|
||||
|
||||
"it": [
|
||||
"entryInfoActionEditDescription",
|
||||
"editEntryDescriptionDialogTitle",
|
||||
"settingsConfirmationAfterMoveToBinItems",
|
||||
"settingsViewerGestureSideTapNext"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
"entryInfoActionEditDescription",
|
||||
"editEntryDescriptionDialogTitle",
|
||||
"settingsConfirmationAfterMoveToBinItems",
|
||||
"settingsViewerGestureSideTapNext"
|
||||
],
|
||||
|
||||
"ko": [
|
||||
"entryInfoActionEditDescription",
|
||||
"editEntryDescriptionDialogTitle",
|
||||
"settingsViewerGestureSideTapNext"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"entryInfoActionEditDescription",
|
||||
"editEntryDescriptionDialogTitle",
|
||||
"settingsConfirmationAfterMoveToBinItems",
|
||||
"settingsViewerGestureSideTapNext"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
"entryInfoActionEditDescription",
|
||||
"filterOnThisDayLabel",
|
||||
"editEntryDescriptionDialogTitle",
|
||||
"settingsConfirmationAfterMoveToBinItems",
|
||||
"settingsViewerGestureSideTapNext",
|
||||
"settingsSlideshowFillScreen",
|
||||
|
@ -49,6 +67,7 @@
|
|||
"tr": [
|
||||
"slideshowActionResume",
|
||||
"slideshowActionShowInCollection",
|
||||
"entryInfoActionEditDescription",
|
||||
"filterOnThisDayLabel",
|
||||
"slideshowVideoPlaybackSkip",
|
||||
"slideshowVideoPlaybackMuted",
|
||||
|
@ -60,6 +79,7 @@
|
|||
"wallpaperTargetHome",
|
||||
"wallpaperTargetLock",
|
||||
"wallpaperTargetHomeLock",
|
||||
"editEntryDescriptionDialogTitle",
|
||||
"menuActionSlideshow",
|
||||
"settingsConfirmationAfterMoveToBinItems",
|
||||
"settingsViewerGestureSideTapNext",
|
||||
|
@ -81,6 +101,8 @@
|
|||
],
|
||||
|
||||
"zh": [
|
||||
"entryInfoActionEditDescription",
|
||||
"editEntryDescriptionDialogTitle",
|
||||
"settingsConfirmationAfterMoveToBinItems",
|
||||
"settingsViewerGestureSideTapNext"
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue