#526 converter: write metadata
This commit is contained in:
parent
64ae4e7614
commit
ef6eb53eb2
15 changed files with 285 additions and 84 deletions
|
@ -6,7 +6,8 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
### Added
|
||||
|
||||
- Collection: bulk converting
|
||||
- Export: bulk converting
|
||||
- Export: write metadata when converting
|
||||
- Places: page & navigation entry
|
||||
|
||||
### Changed
|
||||
|
|
|
@ -132,8 +132,9 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
val lengthUnit = arguments["lengthUnit"] as String?
|
||||
val width = (arguments["width"] as Number?)?.toInt()
|
||||
val height = (arguments["height"] as Number?)?.toInt()
|
||||
val writeMetadata = arguments["writeMetadata"] as Boolean?
|
||||
val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?)
|
||||
if (destinationDir == null || mimeType == null || lengthUnit == null || width == null || height == null || nameConflictStrategy == null) {
|
||||
if (destinationDir == null || mimeType == null || lengthUnit == null || width == null || height == null || writeMetadata == null || nameConflictStrategy == null) {
|
||||
error("convert-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
@ -148,10 +149,21 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
|
||||
destinationDir = ensureTrailingSeparator(destinationDir)
|
||||
val entries = entryMapList.map(::AvesEntry)
|
||||
provider.convertMultiple(activity, mimeType, destinationDir, entries, lengthUnit, width, height, nameConflictStrategy, object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = success(fields)
|
||||
override fun onFailure(throwable: Throwable) = error("convert-failure", "failed to convert entries", throwable)
|
||||
})
|
||||
provider.convertMultiple(
|
||||
activity = activity,
|
||||
imageExportMimeType = mimeType,
|
||||
targetDir = destinationDir,
|
||||
entries = entries,
|
||||
lengthUnit = lengthUnit,
|
||||
width = width,
|
||||
height = height,
|
||||
writeMetadata = writeMetadata,
|
||||
nameConflictStrategy = nameConflictStrategy,
|
||||
callback = object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = success(fields)
|
||||
override fun onFailure(throwable: Throwable) = error("convert-failure", "failed to convert entries", throwable)
|
||||
},
|
||||
)
|
||||
endOfStream()
|
||||
}
|
||||
|
||||
|
@ -183,10 +195,17 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
// always use Media Store (as we move from or to it)
|
||||
val provider = MediaStoreImageProvider()
|
||||
|
||||
provider.moveMultiple(activity, copy, nameConflictStrategy, entriesByTargetDir, ::isCancelledOp, object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = success(fields)
|
||||
override fun onFailure(throwable: Throwable) = error("move-failure", "failed to move entries", throwable)
|
||||
})
|
||||
provider.moveMultiple(
|
||||
activity = activity,
|
||||
copy = copy,
|
||||
nameConflictStrategy = nameConflictStrategy,
|
||||
entriesByTargetDir = entriesByTargetDir,
|
||||
isCancelledOp = ::isCancelledOp,
|
||||
callback = object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = success(fields)
|
||||
override fun onFailure(throwable: Throwable) = error("move-failure", "failed to move entries", throwable)
|
||||
},
|
||||
)
|
||||
endOfStream()
|
||||
}
|
||||
|
||||
|
@ -218,10 +237,15 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
}
|
||||
|
||||
val entryMap = mapOf(*entryList.map { Pair(it.key, it.value) }.toTypedArray())
|
||||
provider.renameMultiple(activity, entryMap, ::isCancelledOp, object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = success(fields)
|
||||
override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message)
|
||||
})
|
||||
provider.renameMultiple(
|
||||
activity = activity,
|
||||
entriesToNewName = entryMap,
|
||||
isCancelledOp = ::isCancelledOp,
|
||||
callback = object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = success(fields)
|
||||
override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
endOfStream()
|
||||
|
|
|
@ -15,15 +15,9 @@ class AvesEntry(map: FieldMap) {
|
|||
val trashed = map["trashed"] as Boolean
|
||||
val trashPath = map["trashPath"] as String?
|
||||
|
||||
private val isRotated: Boolean
|
||||
val isRotated: Boolean
|
||||
get() = rotationDegrees % 180 == 90
|
||||
|
||||
val displayWidth: Int
|
||||
get() = if (isRotated) height else width
|
||||
|
||||
val displayHeight: Int
|
||||
get() = if (isRotated) width else height
|
||||
|
||||
companion object {
|
||||
// convenience method
|
||||
private fun toLong(o: Any?): Long? = when (o) {
|
||||
|
|
|
@ -38,9 +38,13 @@ import deckers.thibault.aves.utils.FileUtils.transferTo
|
|||
import deckers.thibault.aves.utils.MimeTypes.canEditExif
|
||||
import deckers.thibault.aves.utils.MimeTypes.canEditIptc
|
||||
import deckers.thibault.aves.utils.MimeTypes.canEditXmp
|
||||
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
|
||||
import deckers.thibault.aves.utils.MimeTypes.canReadWithPixyMeta
|
||||
import deckers.thibault.aves.utils.MimeTypes.canRemoveMetadata
|
||||
import deckers.thibault.aves.utils.MimeTypes.extensionFor
|
||||
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
||||
import pixy.meta.meta.Metadata
|
||||
import pixy.meta.meta.MetadataType
|
||||
import java.io.*
|
||||
import java.nio.channels.Channels
|
||||
import java.util.*
|
||||
|
@ -177,6 +181,7 @@ abstract class ImageProvider {
|
|||
lengthUnit: String,
|
||||
width: Int,
|
||||
height: Int,
|
||||
writeMetadata: Boolean,
|
||||
nameConflictStrategy: NameConflictStrategy,
|
||||
callback: ImageOpCallback,
|
||||
) {
|
||||
|
@ -204,7 +209,7 @@ abstract class ImageProvider {
|
|||
val sourceMimeType = entry.mimeType
|
||||
val exportMimeType = if (isVideo(sourceMimeType)) sourceMimeType else imageExportMimeType
|
||||
try {
|
||||
val newFields = exportSingle(
|
||||
val newFields = convertSingle(
|
||||
activity = activity,
|
||||
sourceEntry = entry,
|
||||
targetDir = targetDir,
|
||||
|
@ -212,19 +217,20 @@ abstract class ImageProvider {
|
|||
lengthUnit = lengthUnit,
|
||||
width = width,
|
||||
height = height,
|
||||
writeMetadata = writeMetadata,
|
||||
nameConflictStrategy = nameConflictStrategy,
|
||||
exportMimeType = exportMimeType,
|
||||
)
|
||||
result["newFields"] = newFields
|
||||
result["success"] = true
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to export to targetDir=$targetDir entry with sourcePath=$sourcePath pageId=$pageId", e)
|
||||
Log.w(LOG_TAG, "failed to convert to targetDir=$targetDir entry with sourcePath=$sourcePath pageId=$pageId", e)
|
||||
}
|
||||
callback.onSuccess(result)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun exportSingle(
|
||||
private suspend fun convertSingle(
|
||||
activity: Activity,
|
||||
sourceEntry: AvesEntry,
|
||||
targetDir: String,
|
||||
|
@ -232,6 +238,7 @@ abstract class ImageProvider {
|
|||
lengthUnit: String,
|
||||
width: Int,
|
||||
height: Int,
|
||||
writeMetadata: Boolean,
|
||||
nameConflictStrategy: NameConflictStrategy,
|
||||
exportMimeType: String,
|
||||
): FieldMap {
|
||||
|
@ -269,17 +276,11 @@ abstract class ImageProvider {
|
|||
sourceDocFile.copyTo(output)
|
||||
}
|
||||
} else {
|
||||
val targetWidthPx: Int
|
||||
val targetHeightPx: Int
|
||||
when (lengthUnit) {
|
||||
LENGTH_UNIT_PERCENT -> {
|
||||
targetWidthPx = sourceEntry.displayWidth * width / 100
|
||||
targetHeightPx = sourceEntry.displayHeight * height / 100
|
||||
}
|
||||
else -> {
|
||||
targetWidthPx = width
|
||||
targetHeightPx = height
|
||||
}
|
||||
var targetWidthPx: Int = if (sourceEntry.isRotated) height else width
|
||||
var targetHeightPx: Int = if (sourceEntry.isRotated) width else height
|
||||
if (lengthUnit == LENGTH_UNIT_PERCENT) {
|
||||
targetWidthPx = sourceEntry.width * targetWidthPx / 100
|
||||
targetHeightPx = sourceEntry.height * targetHeightPx / 100
|
||||
}
|
||||
|
||||
val model: Any = if (MimeTypes.isHeic(sourceMimeType) && pageId != null) {
|
||||
|
@ -344,13 +345,108 @@ abstract class ImageProvider {
|
|||
targetNameWithoutExtension = targetNameWithoutExtension,
|
||||
write = write,
|
||||
)
|
||||
return scanNewPath(activity, targetPath, exportMimeType)
|
||||
|
||||
val newFields = scanNewPath(activity, targetPath, exportMimeType)
|
||||
val targetUri = Uri.parse(newFields["uri"] as String)
|
||||
if (writeMetadata) {
|
||||
copyMetadata(
|
||||
context = activity,
|
||||
sourceMimeType = sourceMimeType,
|
||||
sourceUri = sourceUri,
|
||||
targetMimeType = targetMimeType,
|
||||
targetUri = targetUri,
|
||||
targetPath = targetPath,
|
||||
)
|
||||
}
|
||||
|
||||
return newFields
|
||||
} finally {
|
||||
// clearing Glide target should happen after effectively writing the bitmap
|
||||
Glide.with(activity).clear(target)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyMetadata(
|
||||
context: Context,
|
||||
sourceMimeType: String,
|
||||
sourceUri: Uri,
|
||||
targetMimeType: String,
|
||||
targetUri: Uri,
|
||||
targetPath: String,
|
||||
) {
|
||||
val editableFile = File.createTempFile("aves", null).apply {
|
||||
deleteOnExit()
|
||||
// copy original file to a temporary file for editing
|
||||
val inputStream = StorageUtils.openInputStream(context, targetUri)
|
||||
transferFrom(inputStream, File(targetPath).length())
|
||||
}
|
||||
|
||||
// copy IPTC / XMP via PixyMeta
|
||||
|
||||
var pixyIptc: pixy.meta.meta.iptc.IPTC? = null
|
||||
var pixyXmp: pixy.meta.meta.xmp.XMP? = null
|
||||
if (canReadWithPixyMeta(sourceMimeType)) {
|
||||
StorageUtils.openInputStream(context, sourceUri)?.use { input ->
|
||||
val metadata = Metadata.readMetadata(input)
|
||||
if (canEditIptc(targetMimeType)) {
|
||||
pixyIptc = metadata[MetadataType.IPTC] as pixy.meta.meta.iptc.IPTC?
|
||||
}
|
||||
if (canEditXmp(targetMimeType)) {
|
||||
pixyXmp = metadata[MetadataType.XMP] as pixy.meta.meta.xmp.XMP?
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pixyIptc != null || pixyXmp != null) {
|
||||
editableFile.outputStream().use { output ->
|
||||
if (pixyIptc != null) {
|
||||
// reopen input to read from start
|
||||
StorageUtils.openInputStream(context, targetUri)?.use { input ->
|
||||
val iptcs = pixyIptc!!.dataSets.flatMap { it.value }
|
||||
Metadata.insertIPTC(input, output, iptcs)
|
||||
}
|
||||
}
|
||||
if (pixyXmp != null) {
|
||||
// reopen input to read from start
|
||||
StorageUtils.openInputStream(context, targetUri)?.use { input ->
|
||||
val xmpString = pixyXmp!!.xmpDocString()
|
||||
val extendedXmp = if (pixyXmp!!.hasExtendedXmp()) pixyXmp!!.extendedXmpDocString() else null
|
||||
PixyMetaHelper.setXmp(input, output, xmpString, if (targetMimeType == MimeTypes.JPEG) extendedXmp else null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// copy Exif via ExifInterface
|
||||
|
||||
val exif = HashMap<String, String?>()
|
||||
val skippedTags = listOf(
|
||||
ExifInterface.TAG_IMAGE_LENGTH,
|
||||
ExifInterface.TAG_IMAGE_WIDTH,
|
||||
ExifInterface.TAG_ORIENTATION,
|
||||
)
|
||||
if (canReadWithExifInterface(sourceMimeType) && canEditExif(targetMimeType)) {
|
||||
StorageUtils.openInputStream(context, sourceUri)?.use { input ->
|
||||
ExifInterface(input).apply {
|
||||
ExifInterfaceHelper.allTags.keys.filterNot { skippedTags.contains(it) }.filter { hasAttribute(it) }.forEach { tag ->
|
||||
exif[tag] = getAttribute(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (exif.isNotEmpty()) {
|
||||
ExifInterface(editableFile).apply {
|
||||
exif.entries.forEach { (tag, value) ->
|
||||
setAttribute(tag, value)
|
||||
}
|
||||
saveAttributes()
|
||||
}
|
||||
}
|
||||
|
||||
// copy the edited temporary file back to the original
|
||||
editableFile.transferTo(outputStream(context, targetMimeType, targetUri, targetPath))
|
||||
editableFile.delete()
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun captureFrame(
|
||||
contextWrapper: ContextWrapper,
|
||||
|
|
|
@ -422,6 +422,7 @@
|
|||
"exportEntryDialogFormat": "Format:",
|
||||
"exportEntryDialogWidth": "Width",
|
||||
"exportEntryDialogHeight": "Height",
|
||||
"exportEntryDialogWriteMetadata": "Write metadata",
|
||||
|
||||
"renameEntryDialogLabel": "New name",
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@ import 'package:aves/model/filters/recent.dart';
|
|||
import 'package:aves/model/naming_pattern.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
import 'package:aves/model/source/enums/enums.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/countries_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/places_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/tags_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
@ -41,7 +41,6 @@ class SettingsDefaults {
|
|||
static const drawerPageBookmarks = [
|
||||
AlbumListPage.routeName,
|
||||
CountryListPage.routeName,
|
||||
PlaceListPage.routeName,
|
||||
TagListPage.routeName,
|
||||
];
|
||||
|
||||
|
@ -116,6 +115,11 @@ class SettingsDefaults {
|
|||
|
||||
static const tagEditorCurrentFilterSectionExpanded = true;
|
||||
|
||||
// converter
|
||||
|
||||
static const convertMimeType = MimeTypes.jpeg;
|
||||
static const convertWriteMetadata = true;
|
||||
|
||||
// rendering
|
||||
static const imageBackground = EntryBackground.white;
|
||||
|
||||
|
|
|
@ -158,6 +158,11 @@ class Settings extends ChangeNotifier {
|
|||
static const tagEditorCurrentFilterSectionExpandedKey = 'tag_editor_current_filter_section_expanded';
|
||||
static const tagEditorExpandedSectionKey = 'tag_editor_expanded_section';
|
||||
|
||||
// converter
|
||||
|
||||
static const convertMimeTypeKey = 'convert_mime_type';
|
||||
static const convertWriteMetadataKey = 'convert_write_metadata';
|
||||
|
||||
// map
|
||||
static const mapStyleKey = 'info_map_style';
|
||||
static const mapDefaultCenterKey = 'map_default_center';
|
||||
|
@ -724,6 +729,16 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
set tagEditorExpandedSection(String? newValue) => _set(tagEditorExpandedSectionKey, newValue);
|
||||
|
||||
// converter
|
||||
|
||||
String get convertMimeType => getString(convertMimeTypeKey) ?? SettingsDefaults.convertMimeType;
|
||||
|
||||
set convertMimeType(String newValue) => _set(convertMimeTypeKey, newValue);
|
||||
|
||||
bool get convertWriteMetadata => getBool(convertWriteMetadataKey) ?? SettingsDefaults.convertWriteMetadata;
|
||||
|
||||
set convertWriteMetadata(bool newValue) => _set(convertWriteMetadataKey, newValue);
|
||||
|
||||
// map
|
||||
|
||||
EntryMapStyle? get mapStyle {
|
||||
|
@ -1069,6 +1084,7 @@ class Settings extends ChangeNotifier {
|
|||
case videoGestureVerticalDragBrightnessVolumeKey:
|
||||
case subtitleShowOutlineKey:
|
||||
case tagEditorCurrentFilterSectionExpandedKey:
|
||||
case convertWriteMetadataKey:
|
||||
case saveSearchHistoryKey:
|
||||
case showPinchGestureAlternativesKey:
|
||||
case filePickerShowHiddenFilesKey:
|
||||
|
@ -1106,6 +1122,7 @@ class Settings extends ChangeNotifier {
|
|||
case subtitleTextAlignmentKey:
|
||||
case subtitleTextPositionKey:
|
||||
case tagEditorExpandedSectionKey:
|
||||
case convertMimeTypeKey:
|
||||
case mapStyleKey:
|
||||
case mapDefaultCenterKey:
|
||||
case coordinateFormatKey:
|
||||
|
|
|
@ -127,6 +127,7 @@ class PlatformMediaEditService implements MediaEditService {
|
|||
'lengthUnit': options.lengthUnit.name,
|
||||
'width': options.width,
|
||||
'height': options.height,
|
||||
'writeMetadata': options.writeMetadata,
|
||||
'destinationPath': destinationAlbum,
|
||||
'nameConflictStrategy': nameConflictStrategy.toPlatform(),
|
||||
})
|
||||
|
@ -187,14 +188,16 @@ class PlatformMediaEditService implements MediaEditService {
|
|||
@immutable
|
||||
class EntryConvertOptions extends Equatable {
|
||||
final String mimeType;
|
||||
final bool writeMetadata;
|
||||
final LengthUnit lengthUnit;
|
||||
final int width, height;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [mimeType, lengthUnit, width, height];
|
||||
List<Object?> get props => [mimeType, writeMetadata, lengthUnit, width, height];
|
||||
|
||||
const EntryConvertOptions({
|
||||
required this.mimeType,
|
||||
required this.writeMetadata,
|
||||
required this.lengthUnit,
|
||||
required this.width,
|
||||
required this.height,
|
||||
|
|
14
lib/widgets/common/fx/transitions.dart
Normal file
14
lib/widgets/common/fx/transitions.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class AvesTransitions {
|
||||
static Widget formTransitionBuilder(Widget child, Animation<double> animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,13 +1,17 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/metadata/enums/enums.dart';
|
||||
import 'package:aves/model/metadata/enums/length_unit.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/services/media/media_edit_service.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/themes.dart';
|
||||
import 'package:aves/utils/mime_utils.dart';
|
||||
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/transitions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'aves_dialog.dart';
|
||||
|
||||
|
@ -28,8 +32,8 @@ class ConvertEntryDialog extends StatefulWidget {
|
|||
class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
||||
final TextEditingController _widthController = TextEditingController(), _heightController = TextEditingController();
|
||||
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
||||
late String _mimeType;
|
||||
late bool _sameSized;
|
||||
late ValueNotifier<String> _mimeTypeNotifier;
|
||||
late bool _writeMetadata, _sameSized;
|
||||
late List<LengthUnit> _lengthUnitOptions;
|
||||
late LengthUnit _lengthUnit;
|
||||
|
||||
|
@ -45,7 +49,8 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_mimeType = MimeTypes.jpeg;
|
||||
_mimeTypeNotifier = ValueNotifier(settings.convertMimeType);
|
||||
_writeMetadata = settings.convertWriteMetadata;
|
||||
_sameSized = entries.map((entry) => entry.displaySize).toSet().length == 1;
|
||||
_lengthUnitOptions = [
|
||||
if (_sameSized) LengthUnit.px,
|
||||
|
@ -103,10 +108,10 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
|||
TextDropdownButton<String>(
|
||||
values: imageExportFormats,
|
||||
valueText: MimeUtils.displayType,
|
||||
value: _mimeType,
|
||||
value: _mimeTypeNotifier.value,
|
||||
onChanged: (selected) {
|
||||
if (selected != null) {
|
||||
setState(() => _mimeType = selected);
|
||||
setState(() => _mimeTypeNotifier.value = selected);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
@ -195,7 +200,32 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
|||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ValueListenableBuilder<String>(
|
||||
valueListenable: _mimeTypeNotifier,
|
||||
builder: (context, mimeType, child) {
|
||||
Widget child;
|
||||
if (MimeTypes.canEditExif(mimeType) || MimeTypes.canEditIptc(mimeType) || MimeTypes.canEditXmp(mimeType)) {
|
||||
child = SwitchListTile(
|
||||
value: _writeMetadata,
|
||||
onChanged: (v) => setState(() => _writeMetadata = v),
|
||||
title: Text(context.l10n.exportEntryDialogWriteMetadata),
|
||||
contentPadding: const EdgeInsetsDirectional.only(
|
||||
start: AvesDialog.defaultHorizontalContentPadding,
|
||||
end: AvesDialog.defaultHorizontalContentPadding - 8,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
child = const SizedBox(height: 16);
|
||||
}
|
||||
return AnimatedSwitcher(
|
||||
duration: context.read<DurationsData>().formTransition,
|
||||
switchInCurve: Curves.easeInOutCubic,
|
||||
switchOutCurve: Curves.easeInOutCubic,
|
||||
transitionBuilder: AvesTransitions.formTransitionBuilder,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
|
@ -209,12 +239,19 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
|||
final height = int.tryParse(_heightController.text);
|
||||
final options = (width != null && height != null)
|
||||
? EntryConvertOptions(
|
||||
mimeType: _mimeType,
|
||||
mimeType: _mimeTypeNotifier.value,
|
||||
writeMetadata: _writeMetadata,
|
||||
lengthUnit: _lengthUnit,
|
||||
width: width,
|
||||
height: height,
|
||||
)
|
||||
: null;
|
||||
|
||||
if (options != null) {
|
||||
settings.convertMimeType = options.mimeType;
|
||||
settings.convertWriteMetadata = options.writeMetadata;
|
||||
}
|
||||
|
||||
Navigator.maybeOf(context)?.pop(options);
|
||||
}
|
||||
: null,
|
||||
|
|
|
@ -13,6 +13,7 @@ import 'package:aves/utils/time_utils.dart';
|
|||
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
|
||||
import 'package:aves/widgets/common/basic/wheel.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/transitions.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/item_picker.dart';
|
||||
|
@ -111,7 +112,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
|||
duration: context.read<DurationsData>().formTransition,
|
||||
switchInCurve: Curves.easeInOutCubic,
|
||||
switchOutCurve: Curves.easeInOutCubic,
|
||||
transitionBuilder: _formTransitionBuilder,
|
||||
transitionBuilder: AvesTransitions.formTransitionBuilder,
|
||||
child: Column(
|
||||
key: ValueKey(_action),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
@ -143,15 +144,6 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _formTransitionBuilder(Widget child, Animation<double> animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSetCustomContent(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final locale = l10n.localeName;
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:aves/theme/themes.dart';
|
|||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/transitions.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/item_picker.dart';
|
||||
|
@ -114,7 +115,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
|||
duration: context.read<DurationsData>().formTransition,
|
||||
switchInCurve: Curves.easeInOutCubic,
|
||||
switchOutCurve: Curves.easeInOutCubic,
|
||||
transitionBuilder: _formTransitionBuilder,
|
||||
transitionBuilder: AvesTransitions.formTransitionBuilder,
|
||||
child: Column(
|
||||
key: ValueKey(_action),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
@ -145,15 +146,6 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _formTransitionBuilder(Widget child, Animation<double> animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildChooseOnMapContent(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:aves/theme/icons.dart';
|
|||
import 'package:aves/theme/themes.dart';
|
||||
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/transitions.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_caption.dart';
|
||||
import 'package:aves/widgets/common/identity/highlight_title.dart';
|
||||
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||
|
@ -98,14 +99,7 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
|
|||
duration: context.read<DurationsData>().formTransition,
|
||||
switchInCurve: Curves.easeInOutCubic,
|
||||
switchOutCurve: Curves.easeInOutCubic,
|
||||
transitionBuilder: (child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
transitionBuilder: AvesTransitions.formTransitionBuilder,
|
||||
child: _buildSection(
|
||||
show: canGroup,
|
||||
icon: AIcons.group,
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:aves/model/entry.dart';
|
|||
import 'package:aves/model/filters/date.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/transitions.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/stats/date/axis.dart';
|
||||
import 'package:charts_flutter/flutter.dart' as charts;
|
||||
|
@ -343,14 +344,7 @@ class _HistogramState extends State<Histogram> with AutomaticKeepAliveClientMixi
|
|||
duration: context.read<DurationsData>().formTransition,
|
||||
switchInCurve: Curves.easeInOutCubic,
|
||||
switchOutCurve: Curves.easeInOutCubic,
|
||||
transitionBuilder: (child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
transitionBuilder: AvesTransitions.formTransitionBuilder,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
|
|
|
@ -215,6 +215,7 @@
|
|||
"exportEntryDialogFormat",
|
||||
"exportEntryDialogWidth",
|
||||
"exportEntryDialogHeight",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"renameEntryDialogLabel",
|
||||
"editEntryDialogCopyFromItem",
|
||||
"editEntryDialogTargetFieldsHeader",
|
||||
|
@ -766,6 +767,7 @@
|
|||
"exportEntryDialogFormat",
|
||||
"exportEntryDialogWidth",
|
||||
"exportEntryDialogHeight",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"renameEntryDialogLabel",
|
||||
"editEntryDialogCopyFromItem",
|
||||
"editEntryDialogTargetFieldsHeader",
|
||||
|
@ -1182,6 +1184,7 @@
|
|||
"authenticateToConfigureVault",
|
||||
"authenticateToUnlockVault",
|
||||
"vaultBinUsageDialogMessage",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty",
|
||||
|
@ -1211,6 +1214,7 @@
|
|||
"authenticateToConfigureVault",
|
||||
"authenticateToUnlockVault",
|
||||
"vaultBinUsageDialogMessage",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty",
|
||||
|
@ -1220,11 +1224,16 @@
|
|||
|
||||
"el": [
|
||||
"chipActionGoToPlacePage",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"exportEntryDialogWriteMetadata"
|
||||
],
|
||||
|
||||
"eu": [
|
||||
"chipActionGoToPlacePage",
|
||||
"chipActionLock",
|
||||
|
@ -1247,6 +1256,7 @@
|
|||
"authenticateToConfigureVault",
|
||||
"authenticateToUnlockVault",
|
||||
"vaultBinUsageDialogMessage",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty",
|
||||
|
@ -1356,6 +1366,7 @@
|
|||
"renameProcessorName",
|
||||
"deleteSingleAlbumConfirmationDialogMessage",
|
||||
"deleteMultiAlbumConfirmationDialogMessage",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"renameEntryDialogLabel",
|
||||
"editEntryDialogCopyFromItem",
|
||||
"editEntryDialogTargetFieldsHeader",
|
||||
|
@ -1721,6 +1732,10 @@
|
|||
"filePickerUseThisFolder"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"exportEntryDialogWriteMetadata"
|
||||
],
|
||||
|
||||
"gl": [
|
||||
"columnCount",
|
||||
"chipActionGoToPlacePage",
|
||||
|
@ -1826,6 +1841,7 @@
|
|||
"exportEntryDialogFormat",
|
||||
"exportEntryDialogWidth",
|
||||
"exportEntryDialogHeight",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"renameEntryDialogLabel",
|
||||
"editEntryDialogCopyFromItem",
|
||||
"editEntryDialogTargetFieldsHeader",
|
||||
|
@ -2454,6 +2470,7 @@
|
|||
"exportEntryDialogFormat",
|
||||
"exportEntryDialogWidth",
|
||||
"exportEntryDialogHeight",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"renameEntryDialogLabel",
|
||||
"editEntryDialogCopyFromItem",
|
||||
"editEntryDialogTargetFieldsHeader",
|
||||
|
@ -2850,13 +2867,15 @@
|
|||
|
||||
"id": [
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent"
|
||||
"lengthUnitPercent",
|
||||
"exportEntryDialogWriteMetadata"
|
||||
],
|
||||
|
||||
"it": [
|
||||
"chipActionGoToPlacePage",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty"
|
||||
|
@ -2894,6 +2913,7 @@
|
|||
"authenticateToConfigureVault",
|
||||
"authenticateToUnlockVault",
|
||||
"vaultBinUsageDialogMessage",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"tooManyItemsErrorDialogMessage",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
|
@ -2908,6 +2928,10 @@
|
|||
"settingsWidgetDisplayedItem"
|
||||
],
|
||||
|
||||
"ko": [
|
||||
"exportEntryDialogWriteMetadata"
|
||||
],
|
||||
|
||||
"lt": [
|
||||
"columnCount",
|
||||
"chipActionGoToPlacePage",
|
||||
|
@ -2934,6 +2958,7 @@
|
|||
"authenticateToConfigureVault",
|
||||
"authenticateToUnlockVault",
|
||||
"vaultBinUsageDialogMessage",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"tooManyItemsErrorDialogMessage",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
|
@ -2969,6 +2994,7 @@
|
|||
"authenticateToConfigureVault",
|
||||
"authenticateToUnlockVault",
|
||||
"vaultBinUsageDialogMessage",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty",
|
||||
|
@ -3013,6 +3039,7 @@
|
|||
"authenticateToConfigureVault",
|
||||
"authenticateToUnlockVault",
|
||||
"vaultBinUsageDialogMessage",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"tooManyItemsErrorDialogMessage",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
|
@ -3064,6 +3091,7 @@
|
|||
"authenticateToConfigureVault",
|
||||
"authenticateToUnlockVault",
|
||||
"vaultBinUsageDialogMessage",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"editEntryDialogTargetFieldsHeader",
|
||||
"editEntryDateDialogSetCustom",
|
||||
"editEntryLocationDialogTitle",
|
||||
|
@ -3347,6 +3375,7 @@
|
|||
"chipActionGoToPlacePage",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty"
|
||||
|
@ -3375,6 +3404,7 @@
|
|||
"authenticateToConfigureVault",
|
||||
"authenticateToUnlockVault",
|
||||
"vaultBinUsageDialogMessage",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"tooManyItemsErrorDialogMessage",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
|
@ -3388,6 +3418,7 @@
|
|||
"chipActionGoToPlacePage",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty"
|
||||
|
@ -3417,6 +3448,7 @@
|
|||
"authenticateToConfigureVault",
|
||||
"authenticateToUnlockVault",
|
||||
"vaultBinUsageDialogMessage",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"tooManyItemsErrorDialogMessage",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
|
@ -3465,6 +3497,7 @@
|
|||
"vaultBinUsageDialogMessage",
|
||||
"deleteSingleAlbumConfirmationDialogMessage",
|
||||
"deleteMultiAlbumConfirmationDialogMessage",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"editEntryLocationDialogLatitude",
|
||||
"editEntryLocationDialogLongitude",
|
||||
"locationPickerUseThisLocationButton",
|
||||
|
@ -3873,6 +3906,7 @@
|
|||
"authenticateToConfigureVault",
|
||||
"authenticateToUnlockVault",
|
||||
"vaultBinUsageDialogMessage",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"editEntryDateDialogExtractFromTitle",
|
||||
"editEntryDateDialogShift",
|
||||
"removeEntryMetadataDialogTitle",
|
||||
|
@ -4217,6 +4251,7 @@
|
|||
"authenticateToConfigureVault",
|
||||
"authenticateToUnlockVault",
|
||||
"vaultBinUsageDialogMessage",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty",
|
||||
|
@ -4226,7 +4261,8 @@
|
|||
|
||||
"uk": [
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent"
|
||||
"lengthUnitPercent",
|
||||
"exportEntryDialogWriteMetadata"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
|
@ -4253,6 +4289,7 @@
|
|||
"authenticateToConfigureVault",
|
||||
"authenticateToUnlockVault",
|
||||
"vaultBinUsageDialogMessage",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"tooManyItemsErrorDialogMessage",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
|
@ -4291,6 +4328,7 @@
|
|||
"authenticateToConfigureVault",
|
||||
"authenticateToUnlockVault",
|
||||
"vaultBinUsageDialogMessage",
|
||||
"exportEntryDialogWriteMetadata",
|
||||
"tooManyItemsErrorDialogMessage",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
|
|
Loading…
Reference in a new issue