From c753e4f7a2205c4a5ea14a24077986f16296c7c8 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 1 Sep 2022 11:52:15 +0200 Subject: [PATCH] info: edit title --- CHANGELOG.md | 4 + .../channel/calls/MetadataFetchHandler.kt | 12 ++- lib/l10n/app_de.arb | 4 +- lib/l10n/app_en.arb | 5 +- lib/l10n/app_fr.arb | 5 +- lib/l10n/app_it.arb | 4 +- lib/l10n/app_ko.arb | 5 +- lib/l10n/app_nl.arb | 4 +- lib/l10n/app_pt.arb | 4 +- lib/l10n/app_zh.arb | 4 +- lib/model/actions/entry_info_actions.dart | 10 +- lib/model/actions/entry_set_actions.dart | 10 +- lib/model/entry.dart | 2 +- lib/model/entry_metadata_edition.dart | 50 +++++++--- lib/ref/iptc.dart | 1 + lib/utils/xmp_utils.dart | 1 + lib/widgets/collection/app_bar.dart | 2 +- .../collection/entry_set_action_delegate.dart | 18 ++-- .../common/action_mixins/entry_editor.dart | 12 ++- .../common/basic/labeled_checkbox.dart | 55 +++++++++++ .../edit_description_dialog.dart | 93 +++++++++++++++---- .../action/entry_info_action_delegate.dart | 18 ++-- lib/widgets/viewer/info/info_search.dart | 2 +- untranslated.json | 50 +++++++--- 24 files changed, 267 insertions(+), 108 deletions(-) create mode 100644 lib/widgets/common/basic/labeled_checkbox.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 13ef06817..5589fc335 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Collection / Info: edit title via IPTC / XMP + ### Changed - upgraded Flutter to stable v3.3.0 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt index 6314e9c2a..7df4f8f25 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt @@ -427,8 +427,9 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { // - XMP / photoshop:DateCreated // - PNG / TIME / LAST_MODIFICATION_TIME // - Video / METADATA_KEY_DATE - // set `KEY_XMP_TITLE` from this field: + // set `KEY_XMP_TITLE` from these fields (by precedence): // - XMP / dc:title + // - IPTC / object-name // set `KEY_XMP_SUBJECTS` from these fields (by precedence): // - XMP / dc:subject // - IPTC / keywords @@ -567,9 +568,14 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { metadata.getDirectoriesOfType(XmpDirectory::class.java).map { it.xmpMeta }.forEach(::processXmp) // XMP fallback to IPTC - if (!metadataMap.containsKey(KEY_XMP_SUBJECTS)) { + if (!metadataMap.containsKey(KEY_XMP_TITLE) || !metadataMap.containsKey(KEY_XMP_SUBJECTS)) { for (dir in metadata.getDirectoriesOfType(IptcDirectory::class.java)) { - dir.keywords?.let { metadataMap[KEY_XMP_SUBJECTS] = it.joinToString(XMP_SUBJECTS_SEPARATOR) } + if (!metadataMap.containsKey(KEY_XMP_TITLE)) { + dir.getSafeString(IptcDirectory.TAG_OBJECT_NAME) { metadataMap[KEY_XMP_TITLE] = it } + } + if (!metadataMap.containsKey(KEY_XMP_SUBJECTS)) { + dir.keywords?.let { metadataMap[KEY_XMP_SUBJECTS] = it.joinToString(XMP_SUBJECTS_SEPARATOR) } + } } } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e965f4c6f..27b965155 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -87,7 +87,6 @@ "entryInfoActionEditDate": "Datum & Uhrzeit bearbeiten", "entryInfoActionEditLocation": "Standort bearbeiten", - "entryInfoActionEditDescription": "Beschreibung bearbeiten", "entryInfoActionEditRating": "Bewertung bearbeiten", "entryInfoActionEditTags": "Tags bearbeiten", "entryInfoActionRemoveMetadata": "Metadaten entfernen", @@ -259,8 +258,6 @@ "locationPickerUseThisLocationButton": "Diesen Standort verwenden", - "editEntryDescriptionDialogTitle": "Beschreibung", - "editEntryRatingDialogTitle": "Bewertung", "removeEntryMetadataDialogTitle": "Entfernung von Metadaten", @@ -614,6 +611,7 @@ "viewerInfoBackToViewerTooltip": "Zurück zum Betrachter", "viewerInfoUnknown": "Unbekannt", + "viewerInfoLabelDescription": "Beschreibung", "viewerInfoLabelTitle": "Titel", "viewerInfoLabelDate": "Datum", "viewerInfoLabelResolution": "Auflösung", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0028aeeca..069f80bc9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -115,7 +115,7 @@ "entryInfoActionEditDate": "Edit date & time", "entryInfoActionEditLocation": "Edit location", - "entryInfoActionEditDescription": "Edit description", + "entryInfoActionEditTitleDescription": "Edit title & description", "entryInfoActionEditRating": "Edit rating", "entryInfoActionEditTags": "Edit tags", "entryInfoActionRemoveMetadata": "Remove metadata", @@ -389,8 +389,6 @@ "locationPickerUseThisLocationButton": "Use this location", - "editEntryDescriptionDialogTitle": "Description", - "editEntryRatingDialogTitle": "Rating", "removeEntryMetadataDialogTitle": "Metadata Removal", @@ -799,6 +797,7 @@ "viewerInfoBackToViewerTooltip": "Back to viewer", "viewerInfoUnknown": "unknown", + "viewerInfoLabelDescription": "Description", "viewerInfoLabelTitle": "Title", "viewerInfoLabelDate": "Date", "viewerInfoLabelResolution": "Resolution", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 5a91e964c..c7cc5811a 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -87,7 +87,7 @@ "entryInfoActionEditDate": "Modifier la date", "entryInfoActionEditLocation": "Modifier le lieu", - "entryInfoActionEditDescription": "Modifier la description", + "entryInfoActionEditTitleDescription": "Modifier titre et description", "entryInfoActionEditRating": "Modifier la notation", "entryInfoActionEditTags": "Modifier les libellés", "entryInfoActionRemoveMetadata": "Retirer les métadonnées", @@ -259,8 +259,6 @@ "locationPickerUseThisLocationButton": "Utiliser ce lieu", - "editEntryDescriptionDialogTitle": "Description", - "editEntryRatingDialogTitle": "Notation", "removeEntryMetadataDialogTitle": "Retrait de métadonnées", @@ -614,6 +612,7 @@ "viewerInfoBackToViewerTooltip": "Retour à la visionneuse", "viewerInfoUnknown": "inconnu", + "viewerInfoLabelDescription": "Description", "viewerInfoLabelTitle": "Titre", "viewerInfoLabelDate": "Date", "viewerInfoLabelResolution": "Résolution", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 3d24c1639..af8d35589 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -87,7 +87,6 @@ "entryInfoActionEditDate": "Modifica data e ora", "entryInfoActionEditLocation": "Modifica posizione", - "entryInfoActionEditDescription": "Modifica descrizione", "entryInfoActionEditRating": "Modifica valutazione", "entryInfoActionEditTags": "Modifica etichetta", "entryInfoActionRemoveMetadata": "Rimuovi metadati", @@ -259,8 +258,6 @@ "locationPickerUseThisLocationButton": "Usa questa posizione", - "editEntryDescriptionDialogTitle": "Descrizione", - "editEntryRatingDialogTitle": "Valutazione", "removeEntryMetadataDialogTitle": "Rimozione dei metadati", @@ -614,6 +611,7 @@ "viewerInfoBackToViewerTooltip": "Torna alla visualizzazione", "viewerInfoUnknown": "sconosciuto", + "viewerInfoLabelDescription": "Descrizione", "viewerInfoLabelTitle": "Titolo", "viewerInfoLabelDate": "Data", "viewerInfoLabelResolution": "Risoluzione", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 1d555672f..46c93eda8 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -87,7 +87,7 @@ "entryInfoActionEditDate": "날짜 및 시간 수정", "entryInfoActionEditLocation": "위치 수정", - "entryInfoActionEditDescription": "설명 수정", + "entryInfoActionEditTitleDescription": "제목 및 설명 수정", "entryInfoActionEditRating": "별점 수정", "entryInfoActionEditTags": "태그 수정", "entryInfoActionRemoveMetadata": "메타데이터 삭제", @@ -259,8 +259,6 @@ "locationPickerUseThisLocationButton": "이 위치 사용", - "editEntryDescriptionDialogTitle": "설명", - "editEntryRatingDialogTitle": "별점", "removeEntryMetadataDialogTitle": "메타데이터 삭제", @@ -614,6 +612,7 @@ "viewerInfoBackToViewerTooltip": "뷰어로", "viewerInfoUnknown": "알 수 없음", + "viewerInfoLabelDescription": "설명", "viewerInfoLabelTitle": "제목", "viewerInfoLabelDate": "날짜", "viewerInfoLabelResolution": "해상도", diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 6274340e6..fd9b37604 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -87,7 +87,6 @@ "entryInfoActionEditDate": "Bewerk Datum & Tijd", "entryInfoActionEditLocation": "Bewerk Locatie", - "entryInfoActionEditDescription": "Omschrijving wijzigen", "entryInfoActionEditRating": "Bewerk waardering", "entryInfoActionEditTags": "Bewerk labels", "entryInfoActionRemoveMetadata": "Verwijder metadata", @@ -259,8 +258,6 @@ "locationPickerUseThisLocationButton": "Gebruik deze locatie", - "editEntryDescriptionDialogTitle": "Omschrijving", - "editEntryRatingDialogTitle": "Beoordeling", "removeEntryMetadataDialogTitle": "Verwijderen metadata", @@ -614,6 +611,7 @@ "viewerInfoBackToViewerTooltip": "Terug naar viewer", "viewerInfoUnknown": "onbekendd", + "viewerInfoLabelDescription": "Omschrijving", "viewerInfoLabelTitle": "Titel", "viewerInfoLabelDate": "Datum", "viewerInfoLabelResolution": "Resolutie", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index e8f129ec7..b7d3047fc 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -87,7 +87,6 @@ "entryInfoActionEditDate": "Editar data e hora", "entryInfoActionEditLocation": "Editar localização", - "entryInfoActionEditDescription": "Editar descrição", "entryInfoActionEditRating": "Editar classificação", "entryInfoActionEditTags": "Editar etiquetas", "entryInfoActionRemoveMetadata": "Remover metadados", @@ -259,8 +258,6 @@ "locationPickerUseThisLocationButton": "Usar essa localização", - "editEntryDescriptionDialogTitle": "Descrição", - "editEntryRatingDialogTitle": "Avaliação", "removeEntryMetadataDialogTitle": "Remoção de metadados", @@ -614,6 +611,7 @@ "viewerInfoBackToViewerTooltip": "Voltar ao visualizador", "viewerInfoUnknown": "desconhecido", + "viewerInfoLabelDescription": "Descrição", "viewerInfoLabelTitle": "Título", "viewerInfoLabelDate": "Data", "viewerInfoLabelResolution": "Resolução", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 2e6ebfd63..ab1f3db74 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -87,7 +87,6 @@ "entryInfoActionEditDate": "编辑日期和时间", "entryInfoActionEditLocation": "编辑位置", - "entryInfoActionEditDescription": "编辑备注", "entryInfoActionEditRating": "修改评分", "entryInfoActionEditTags": "编辑标签", "entryInfoActionRemoveMetadata": "移除元数据", @@ -259,8 +258,6 @@ "locationPickerUseThisLocationButton": "使用此位置", - "editEntryDescriptionDialogTitle": "备注", - "editEntryRatingDialogTitle": "评分", "removeEntryMetadataDialogTitle": "元数据移除工具", @@ -614,6 +611,7 @@ "viewerInfoBackToViewerTooltip": "返回查看器", "viewerInfoUnknown": "未知", + "viewerInfoLabelDescription": "备注", "viewerInfoLabelTitle": "标题", "viewerInfoLabelDate": "日期", "viewerInfoLabelResolution": "分辨率", diff --git a/lib/model/actions/entry_info_actions.dart b/lib/model/actions/entry_info_actions.dart index eca523345..d1b6a67b7 100644 --- a/lib/model/actions/entry_info_actions.dart +++ b/lib/model/actions/entry_info_actions.dart @@ -7,7 +7,7 @@ enum EntryInfoAction { // general editDate, editLocation, - editDescription, + editTitleDescription, editRating, editTags, removeMetadata, @@ -24,7 +24,7 @@ class EntryInfoActions { static const common = [ EntryInfoAction.editDate, EntryInfoAction.editLocation, - EntryInfoAction.editDescription, + EntryInfoAction.editTitleDescription, EntryInfoAction.editRating, EntryInfoAction.editTags, EntryInfoAction.removeMetadata, @@ -45,8 +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.editTitleDescription: + return context.l10n.entryInfoActionEditTitleDescription; case EntryInfoAction.editRating: return context.l10n.entryInfoActionEditRating; case EntryInfoAction.editTags: @@ -88,7 +88,7 @@ extension ExtraEntryInfoAction on EntryInfoAction { return AIcons.date; case EntryInfoAction.editLocation: return AIcons.location; - case EntryInfoAction.editDescription: + case EntryInfoAction.editTitleDescription: return AIcons.description; case EntryInfoAction.editRating: return AIcons.editRating; diff --git a/lib/model/actions/entry_set_actions.dart b/lib/model/actions/entry_set_actions.dart index f6f25d39d..0efb48962 100644 --- a/lib/model/actions/entry_set_actions.dart +++ b/lib/model/actions/entry_set_actions.dart @@ -31,7 +31,7 @@ enum EntrySetAction { flip, editDate, editLocation, - editDescription, + editTitleDescription, editRating, editTags, removeMetadata, @@ -100,7 +100,7 @@ class EntrySetActions { static const edit = [ EntrySetAction.editDate, EntrySetAction.editLocation, - EntrySetAction.editDescription, + EntrySetAction.editTitleDescription, EntrySetAction.editRating, EntrySetAction.editTags, EntrySetAction.removeMetadata, @@ -164,8 +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.editTitleDescription: + return context.l10n.entryInfoActionEditTitleDescription; case EntrySetAction.editRating: return context.l10n.entryInfoActionEditRating; case EntrySetAction.editTags: @@ -233,7 +233,7 @@ extension ExtraEntrySetAction on EntrySetAction { return AIcons.date; case EntrySetAction.editLocation: return AIcons.location; - case EntrySetAction.editDescription: + case EntrySetAction.editTitleDescription: return AIcons.description; case EntrySetAction.editRating: return AIcons.editRating; diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 1a70bf464..6589e4362 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -279,7 +279,7 @@ class AvesEntry { bool get canEditLocation => canEdit && canEditExif; - bool get canEditDescription => canEdit && (canEditExif || canEditXmp); + bool get canEditTitleDescription => canEdit && canEditXmp; bool get canEditRating => canEdit && canEditXmp; diff --git a/lib/model/entry_metadata_edition.dart b/lib/model/entry_metadata_edition.dart index f6fdc4cc1..1659b20bb 100644 --- a/lib/model/entry_metadata_edition.dart +++ b/lib/model/entry_metadata_edition.dart @@ -140,37 +140,62 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { return _changeOrientation(() => metadataEditService.flip(this)); } - // write: + // write title: + // - IPTC / object-name, if IPTC exists + // - XMP / dc:title + // write description: // - Exif / ImageDescription // - IPTC / caption-abstract, if IPTC exists // - XMP / dc:description - Future> editDescription(String? description) async { + Future> editTitleDescription(Map fields) async { final Set dataTypes = {}; final Map metadata = {}; final missingDate = await _missingDateCheckAndExifEdit(dataTypes); - if (canEditExif) { + final editTitle = fields.keys.contains(DescriptionField.title); + final editDescription = fields.keys.contains(DescriptionField.description); + final title = fields[DescriptionField.title]; + final description = fields[DescriptionField.description]; + + if (canEditExif && editDescription) { 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}); + if (editTitle) { + editIptcValues(iptc, IPTC.applicationRecord, IPTC.objectName, {if (title != null) title}); + } + if (editDescription) { + 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, - ); + var modified = false; + if (editTitle) { + modified |= XMP.setAttribute( + descriptions, + XMP.dcTitle, + title, + namespace: Namespaces.dc, + strat: XmpEditStrategy.always, + ); + } + if (editDescription) { + modified |= XMP.setAttribute( + descriptions, + XMP.dcDescription, + description, + namespace: Namespaces.dc, + strat: XmpEditStrategy.always, + ); + } if (modified && missingDate != null) { editCreateDateXmp(descriptions, missingDate); } @@ -182,6 +207,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { if (newFields.isNotEmpty) { dataTypes.addAll({ EntryDataType.basic, + EntryDataType.catalog, }); } @@ -467,3 +493,5 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { }; } } + +enum DescriptionField { title, description } diff --git a/lib/ref/iptc.dart b/lib/ref/iptc.dart index 74efd75e7..5ef584d94 100644 --- a/lib/ref/iptc.dart +++ b/lib/ref/iptc.dart @@ -2,6 +2,7 @@ class IPTC { static const int applicationRecord = 2; // ApplicationRecord tags + static const int objectName = 5; static const int keywordsTag = 25; static const int captionAbstractTag = 120; } diff --git a/lib/utils/xmp_utils.dart b/lib/utils/xmp_utils.dart index eb5853533..49860cda7 100644 --- a/lib/utils/xmp_utils.dart +++ b/lib/utils/xmp_utils.dart @@ -153,6 +153,7 @@ class XMP { static const containerDirectory = 'Directory'; static const dcDescription = 'description'; static const dcSubject = 'subject'; + static const dcTitle = 'title'; static const msPhotoRating = 'Rating'; static const xmpRating = 'Rating'; diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 2bb0ffe44..55d142445 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -496,7 +496,7 @@ class _CollectionAppBarState extends State with SingleTickerPr case EntrySetAction.flip: case EntrySetAction.editDate: case EntrySetAction.editLocation: - case EntrySetAction.editDescription: + case EntrySetAction.editTitleDescription: case EntrySetAction.editRating: case EntrySetAction.editTags: case EntrySetAction.removeMetadata: diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index f8a9bb9cd..19e1ff4f9 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -92,7 +92,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware case EntrySetAction.flip: case EntrySetAction.editDate: case EntrySetAction.editLocation: - case EntrySetAction.editDescription: + case EntrySetAction.editTitleDescription: case EntrySetAction.editRating: case EntrySetAction.editTags: case EntrySetAction.removeMetadata: @@ -144,7 +144,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware case EntrySetAction.flip: case EntrySetAction.editDate: case EntrySetAction.editLocation: - case EntrySetAction.editDescription: + case EntrySetAction.editTitleDescription: case EntrySetAction.editRating: case EntrySetAction.editTags: case EntrySetAction.removeMetadata: @@ -221,8 +221,8 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware case EntrySetAction.editLocation: _editLocation(context); break; - case EntrySetAction.editDescription: - _editDescription(context); + case EntrySetAction.editTitleDescription: + _editTitleDescription(context); break; case EntrySetAction.editRating: _editRating(context); @@ -495,14 +495,14 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware await _edit(context, entries, (entry) => entry.editLocation(location)); } - Future _editDescription(BuildContext context) async { - final entries = await _getEditableTargetItems(context, canEdit: (entry) => entry.canEditDescription); + Future _editTitleDescription(BuildContext context) async { + final entries = await _getEditableTargetItems(context, canEdit: (entry) => entry.canEditTitleDescription); if (entries == null || entries.isEmpty) return; - final description = await selectDescription(context, entries); - if (description == null) return; + final modifier = await selectTitleDescriptionModifier(context, entries); + if (modifier == null) return; - await _edit(context, entries, (entry) => entry.editDescription(description)); + await _edit(context, entries, (entry) => entry.editTitleDescription(modifier)); } Future _editRating(BuildContext context) async { diff --git a/lib/widgets/common/action_mixins/entry_editor.dart b/lib/widgets/common/action_mixins/entry_editor.dart index a70aac16f..9d0ea84ad 100644 --- a/lib/widgets/common/action_mixins/entry_editor.dart +++ b/lib/widgets/common/action_mixins/entry_editor.dart @@ -1,4 +1,5 @@ import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry_metadata_edition.dart'; import 'package:aves/model/metadata/date_modifier.dart'; import 'package:aves/model/metadata/enums.dart'; import 'package:aves/model/source/collection_lens.dart'; @@ -40,14 +41,17 @@ mixin EntryEditorMixin { ); } - Future selectDescription(BuildContext context, Set entries) async { + Future?> selectTitleDescriptionModifier(BuildContext context, Set entries) async { if (entries.isEmpty) return null; - final initialDescription = await metadataFetchService.getDescription(entries.first) ?? ''; + final entry = entries.first; + final initialTitle = entry.catalogMetadata?.xmpTitle ?? ''; + final initialDescription = await metadataFetchService.getDescription(entry) ?? ''; - return showDialog( + return showDialog>( context: context, - builder: (context) => EditEntryDescriptionDialog( + builder: (context) => EditEntryTitleDescriptionDialog( + initialTitle: initialTitle, initialDescription: initialDescription, ), ); diff --git a/lib/widgets/common/basic/labeled_checkbox.dart b/lib/widgets/common/basic/labeled_checkbox.dart new file mode 100644 index 000000000..286b89247 --- /dev/null +++ b/lib/widgets/common/basic/labeled_checkbox.dart @@ -0,0 +1,55 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class LabeledCheckbox extends StatefulWidget { + final bool value; + final ValueChanged onChanged; + final String text; + + const LabeledCheckbox({ + Key? key, + required this.value, + required this.onChanged, + required this.text, + }) : super(key: key); + + @override + State createState() => _LabeledCheckboxState(); +} + +class _LabeledCheckboxState extends State { + late TapGestureRecognizer _tapRecognizer; + + @override + void initState() { + super.initState(); + _tapRecognizer = TapGestureRecognizer()..onTap = () => widget.onChanged(!widget.value); + } + + @override + void dispose() { + _tapRecognizer.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Checkbox( + value: widget.value, + onChanged: widget.onChanged, + ), + ), + TextSpan( + text: widget.text, + recognizer: _tapRecognizer, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/dialogs/entry_editors/edit_description_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_description_dialog.dart index ac43a2b2c..7e9c67473 100644 --- a/lib/widgets/dialogs/entry_editors/edit_description_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_description_dialog.dart @@ -1,52 +1,56 @@ +import 'package:aves/model/entry_metadata_edition.dart'; +import 'package:aves/widgets/common/basic/labeled_checkbox.dart'; 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; +class EditEntryTitleDescriptionDialog extends StatefulWidget { + final String initialTitle, initialDescription; - const EditEntryDescriptionDialog({ + const EditEntryTitleDescriptionDialog({ super.key, + required this.initialTitle, required this.initialDescription, }); @override - State createState() => _EditEntryDescriptionDialogState(); + State createState() => _EditEntryTitleDescriptionDialogState(); } -class _EditEntryDescriptionDialogState extends State { - late final TextEditingController _textController; +class _EditEntryTitleDescriptionDialogState extends State { + final Set fields = { + DescriptionField.title, + DescriptionField.description, + }; + late final TextEditingController _titleTextController, _descriptionTextController; @override void initState() { super.initState(); - _textController = TextEditingController(text: widget.initialDescription); + _titleTextController = TextEditingController(text: widget.initialTitle); + _descriptionTextController = 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, - ), + scrollableContent: [ + const SizedBox(height: 8), + ..._buildFieldEditor(DescriptionField.title), + ..._buildFieldEditor(DescriptionField.description), + const SizedBox(height: 8), + ], actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(MaterialLocalizations.of(context).cancelButtonLabel), ), TextButton( - onPressed: () => _submit(context), - child: Text(l10n.applyButtonLabel), + onPressed: fields.isEmpty ? null : () => _submit(context), + child: Text(context.l10n.applyButtonLabel), ), ], ); @@ -54,5 +58,54 @@ class _EditEntryDescriptionDialogState extends State ); } - void _submit(BuildContext context) => Navigator.pop(context, _textController.text); + List _buildFieldEditor(DescriptionField field) { + final editing = fields.contains(field); + return [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: LabeledCheckbox( + value: editing, + onChanged: (v) => setState(() => editing ? fields.remove(field) : fields.add(field)), + text: _fieldName(field), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: TextField( + controller: _fieldController(field), + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + maxLines: null, + enabled: editing, + ), + ), + ]; + } + + TextEditingController _fieldController(DescriptionField field) { + switch (field) { + case DescriptionField.title: + return _titleTextController; + case DescriptionField.description: + return _descriptionTextController; + } + } + + String _fieldName(DescriptionField field) { + switch (field) { + case DescriptionField.title: + return context.l10n.viewerInfoLabelTitle; + case DescriptionField.description: + return context.l10n.viewerInfoLabelDescription; + } + } + + void _submit(BuildContext context) { + final modifier = Map.fromEntries(fields.map((field) { + final text = _fieldController(field).text; + return MapEntry(field, text.isEmpty ? null : text); + })); + return Navigator.pop>(context, modifier); + } } diff --git a/lib/widgets/viewer/action/entry_info_action_delegate.dart b/lib/widgets/viewer/action/entry_info_action_delegate.dart index a73bc78e6..d44c97eca 100644 --- a/lib/widgets/viewer/action/entry_info_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_info_action_delegate.dart @@ -35,7 +35,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi // general case EntryInfoAction.editDate: case EntryInfoAction.editLocation: - case EntryInfoAction.editDescription: + case EntryInfoAction.editTitleDescription: case EntryInfoAction.editRating: case EntryInfoAction.editTags: case EntryInfoAction.removeMetadata: @@ -60,8 +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.editTitleDescription: + return entry.canEditTitleDescription; case EntryInfoAction.editRating: return entry.canEditRating; case EntryInfoAction.editTags: @@ -92,8 +92,8 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi case EntryInfoAction.editLocation: await _editLocation(context); break; - case EntryInfoAction.editDescription: - await _editDescription(context); + case EntryInfoAction.editTitleDescription: + await _editTitleDescription(context); break; case EntryInfoAction.editRating: await _editRating(context); @@ -137,11 +137,11 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi await edit(context, () => entry.editLocation(location)); } - Future _editDescription(BuildContext context) async { - final description = await selectDescription(context, {entry}); - if (description == null) return; + Future _editTitleDescription(BuildContext context) async { + final modifier = await selectTitleDescriptionModifier(context, {entry}); + if (modifier == null) return; - await edit(context, () => entry.editDescription(description)); + await edit(context, () => entry.editTitleDescription(modifier)); } Future _editRating(BuildContext context) async { diff --git a/lib/widgets/viewer/info/info_search.dart b/lib/widgets/viewer/info/info_search.dart index 6dbb9200a..bd2018b54 100644 --- a/lib/widgets/viewer/info/info_search.dart +++ b/lib/widgets/viewer/info/info_search.dart @@ -55,7 +55,7 @@ class InfoSearchDelegate extends SearchDelegate { final l10n = context.l10n; final suggestions = { l10n.viewerInfoSearchSuggestionDate: 'date or time or when -timer -uptime -exposure -timeline -verbatim', - l10n.viewerInfoSearchSuggestionDescription: 'abstract or description or comment or textual or title -line', + l10n.viewerInfoSearchSuggestionDescription: 'description or title or comment or textual or abstract or object name -line', l10n.viewerInfoSearchSuggestionDimensions: 'width or height or dimension or framesize or imagelength', l10n.viewerInfoSearchSuggestionResolution: 'resolution', l10n.viewerInfoSearchSuggestionRights: 'rights or copyright or attribution or license or artist or creator or by-line or credit -tool', diff --git a/untranslated.json b/untranslated.json index b1efafdb6..74b24cb1e 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,43 +1,59 @@ { + "de": [ + "entryInfoActionEditTitleDescription" + ], + "es": [ - "entryInfoActionEditDescription", + "entryInfoActionEditTitleDescription", "filterRecentlyAddedLabel", - "editEntryDescriptionDialogTitle", - "settingsConfirmationAfterMoveToBinItems" + "settingsConfirmationAfterMoveToBinItems", + "viewerInfoLabelDescription" ], "id": [ - "entryInfoActionEditDescription", + "entryInfoActionEditTitleDescription", "filterRecentlyAddedLabel", - "editEntryDescriptionDialogTitle", "settingsConfirmationAfterMoveToBinItems", - "settingsViewerGestureSideTapNext" + "settingsViewerGestureSideTapNext", + "viewerInfoLabelDescription" + ], + + "it": [ + "entryInfoActionEditTitleDescription" ], "ja": [ - "entryInfoActionEditDescription", + "entryInfoActionEditTitleDescription", "filterRecentlyAddedLabel", - "editEntryDescriptionDialogTitle", "settingsConfirmationAfterMoveToBinItems", - "settingsViewerGestureSideTapNext" + "settingsViewerGestureSideTapNext", + "viewerInfoLabelDescription" + ], + + "nl": [ + "entryInfoActionEditTitleDescription" + ], + + "pt": [ + "entryInfoActionEditTitleDescription" ], "ru": [ - "entryInfoActionEditDescription", + "entryInfoActionEditTitleDescription", "filterOnThisDayLabel", "filterRecentlyAddedLabel", - "editEntryDescriptionDialogTitle", "settingsConfirmationAfterMoveToBinItems", "settingsViewerGestureSideTapNext", "settingsSlideshowFillScreen", "settingsScreenSaverPageTitle", - "settingsWidgetShowOutline" + "settingsWidgetShowOutline", + "viewerInfoLabelDescription" ], "tr": [ "slideshowActionResume", "slideshowActionShowInCollection", - "entryInfoActionEditDescription", + "entryInfoActionEditTitleDescription", "filterOnThisDayLabel", "filterRecentlyAddedLabel", "slideshowVideoPlaybackSkip", @@ -50,7 +66,6 @@ "wallpaperTargetHome", "wallpaperTargetLock", "wallpaperTargetHomeLock", - "editEntryDescriptionDialogTitle", "menuActionSlideshow", "settingsConfirmationAfterMoveToBinItems", "settingsViewerGestureSideTapNext", @@ -67,6 +82,11 @@ "settingsSlideshowVideoPlaybackTitle", "settingsScreenSaverPageTitle", "settingsWidgetShowOutline", - "viewerSetWallpaperButtonLabel" + "viewerSetWallpaperButtonLabel", + "viewerInfoLabelDescription" + ], + + "zh": [ + "entryInfoActionEditTitleDescription" ] }