info: edit title
This commit is contained in:
parent
c2cc81fd1d
commit
c753e4f7a2
24 changed files with 267 additions and 108 deletions
|
@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## <a id="unreleased"></a>[Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Collection / Info: edit title via IPTC / XMP
|
||||
|
||||
### Changed
|
||||
|
||||
- upgraded Flutter to stable v3.3.0
|
||||
|
|
|
@ -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,11 +568,16 @@ 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)) {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (mimeType) {
|
||||
MimeTypes.PNG -> {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "해상도",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "分辨率",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<Set<EntryDataType>> editDescription(String? description) async {
|
||||
Future<Set<EntryDataType>> editTitleDescription(Map<DescriptionField, String?> fields) async {
|
||||
final Set<EntryDataType> dataTypes = {};
|
||||
final Map<MetadataType, dynamic> 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) {
|
||||
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(
|
||||
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 }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -496,7 +496,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> 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:
|
||||
|
|
|
@ -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<void> _editDescription(BuildContext context) async {
|
||||
final entries = await _getEditableTargetItems(context, canEdit: (entry) => entry.canEditDescription);
|
||||
Future<void> _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<void> _editRating(BuildContext context) async {
|
||||
|
|
|
@ -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<String?> selectDescription(BuildContext context, Set<AvesEntry> entries) async {
|
||||
Future<Map<DescriptionField, String?>?> selectTitleDescriptionModifier(BuildContext context, Set<AvesEntry> 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<String>(
|
||||
return showDialog<Map<DescriptionField, String?>>(
|
||||
context: context,
|
||||
builder: (context) => EditEntryDescriptionDialog(
|
||||
builder: (context) => EditEntryTitleDescriptionDialog(
|
||||
initialTitle: initialTitle,
|
||||
initialDescription: initialDescription,
|
||||
),
|
||||
);
|
||||
|
|
55
lib/widgets/common/basic/labeled_checkbox.dart
Normal file
55
lib/widgets/common/basic/labeled_checkbox.dart
Normal file
|
@ -0,0 +1,55 @@
|
|||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LabeledCheckbox extends StatefulWidget {
|
||||
final bool value;
|
||||
final ValueChanged<bool?> onChanged;
|
||||
final String text;
|
||||
|
||||
const LabeledCheckbox({
|
||||
Key? key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
required this.text,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<LabeledCheckbox> createState() => _LabeledCheckboxState();
|
||||
}
|
||||
|
||||
class _LabeledCheckboxState extends State<LabeledCheckbox> {
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<EditEntryDescriptionDialog> createState() => _EditEntryDescriptionDialogState();
|
||||
State<EditEntryTitleDescriptionDialog> createState() => _EditEntryTitleDescriptionDialogState();
|
||||
}
|
||||
|
||||
class _EditEntryDescriptionDialogState extends State<EditEntryDescriptionDialog> {
|
||||
late final TextEditingController _textController;
|
||||
class _EditEntryTitleDescriptionDialogState extends State<EditEntryTitleDescriptionDialog> {
|
||||
final Set<DescriptionField> 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<EditEntryDescriptionDialog>
|
|||
);
|
||||
}
|
||||
|
||||
void _submit(BuildContext context) => Navigator.pop(context, _textController.text);
|
||||
List<Widget> _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<Map<DescriptionField, String?>>(context, modifier);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<void> _editDescription(BuildContext context) async {
|
||||
final description = await selectDescription(context, {entry});
|
||||
if (description == null) return;
|
||||
Future<void> _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<void> _editRating(BuildContext context) async {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue