#587 conversion quality setting

This commit is contained in:
Thibault Deckers 2023-04-29 11:13:40 +02:00
parent ebf549e7ac
commit 50b4006352
10 changed files with 130 additions and 14 deletions

View file

@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
- Collection: support for Sony predictive capture as burst
- Video: option to never/always resume playback
- Display: option to set maximum brightness on all pages
- Export: set quality when converting to JPEG/WEBP
- Hungarian translation (thanks György Viktor, byPety)
### Changed

View file

@ -1,10 +1,10 @@
package deckers.thibault.aves.channel.streams
import android.app.Activity
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.fragment.app.FragmentActivity
import deckers.thibault.aves.channel.calls.MediaEditHandler.Companion.cancelledOps
import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.FieldMap
@ -23,7 +23,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.util.*
class ImageOpStreamHandler(private val activity: Activity, private val arguments: Any?) : EventChannel.StreamHandler {
class ImageOpStreamHandler(private val activity: FragmentActivity, private val arguments: Any?) : EventChannel.StreamHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private lateinit var eventSink: EventSink
private lateinit var handler: Handler
@ -129,12 +129,13 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
var destinationDir = arguments["destinationPath"] as String?
val mimeType = arguments["mimeType"] as String?
val quality = (arguments["quality"] as Number?)?.toInt()
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 || writeMetadata == null || nameConflictStrategy == null) {
if (destinationDir == null || mimeType == null || quality == null || lengthUnit == null || width == null || height == null || writeMetadata == null || nameConflictStrategy == null) {
error("convert-args", "missing arguments", null)
return
}
@ -154,6 +155,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
imageExportMimeType = mimeType,
targetDir = destinationDir,
entries = entries,
quality = quality,
lengthUnit = lengthUnit,
width = width,
height = height,

View file

@ -11,6 +11,7 @@ import android.os.Binder
import android.os.Build
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import androidx.fragment.app.FragmentActivity
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
@ -175,10 +176,11 @@ abstract class ImageProvider {
}
suspend fun convertMultiple(
activity: Activity,
activity: FragmentActivity,
imageExportMimeType: String,
targetDir: String,
entries: List<AvesEntry>,
quality: Int,
lengthUnit: String,
width: Int,
height: Int,
@ -215,6 +217,7 @@ abstract class ImageProvider {
sourceEntry = entry,
targetDir = targetDir,
targetDirDocFile = targetDirDocFile,
quality = quality,
lengthUnit = lengthUnit,
width = width,
height = height,
@ -232,10 +235,11 @@ abstract class ImageProvider {
}
private suspend fun convertSingle(
activity: Activity,
activity: FragmentActivity,
sourceEntry: AvesEntry,
targetDir: String,
targetDirDocFile: DocumentFileCompat?,
quality: Int,
lengthUnit: String,
width: Int,
height: Int,
@ -273,7 +277,6 @@ abstract class ImageProvider {
targetMimeType = sourceMimeType
write = { output ->
val sourceDocFile = DocumentFileCompat.fromSingleUri(activity, sourceUri)
@Suppress("BlockingMethodInNonBlockingContext")
sourceDocFile.copyTo(output)
}
} else {
@ -317,7 +320,6 @@ abstract class ImageProvider {
if (exportMimeType == MimeTypes.BMP) {
BmpWriter.writeRGB24(bitmap, output)
} else {
val quality = 100
val format = when (exportMimeType) {
MimeTypes.JPEG -> Bitmap.CompressFormat.JPEG
MimeTypes.PNG -> Bitmap.CompressFormat.PNG

View file

@ -437,6 +437,7 @@
"exportEntryDialogFormat": "Format:",
"exportEntryDialogWidth": "Width",
"exportEntryDialogHeight": "Height",
"exportEntryDialogQuality": "Quality",
"exportEntryDialogWriteMetadata": "Write metadata",
"renameEntryDialogLabel": "New name",

View file

@ -116,6 +116,7 @@ class SettingsDefaults {
// converter
static const convertMimeType = MimeTypes.jpeg;
static const convertQuality = 95;
static const convertWriteMetadata = true;
// rendering

View file

@ -165,6 +165,7 @@ class Settings extends ChangeNotifier {
// converter
static const convertMimeTypeKey = 'convert_mime_type';
static const convertQualityKey = 'convert_quality';
static const convertWriteMetadataKey = 'convert_write_metadata';
// map
@ -768,6 +769,10 @@ class Settings extends ChangeNotifier {
set convertMimeType(String newValue) => _set(convertMimeTypeKey, newValue);
int get convertQuality => getInt(convertQualityKey) ?? SettingsDefaults.convertQuality;
set convertQuality(int newValue) => _set(convertQualityKey, newValue);
bool get convertWriteMetadata => getBool(convertWriteMetadataKey) ?? SettingsDefaults.convertWriteMetadata;
set convertWriteMetadata(bool newValue) => _set(convertWriteMetadataKey, newValue);
@ -1062,6 +1067,7 @@ class Settings extends ChangeNotifier {
switch (key) {
case subtitleTextColorKey:
case subtitleBackgroundColorKey:
case convertQualityKey:
case screenSaverIntervalKey:
case slideshowIntervalKey:
if (newValue is int) {

View file

@ -129,6 +129,7 @@ class PlatformMediaEditService implements MediaEditService {
'op': 'convert',
'entries': entries.map((entry) => entry.toPlatformEntryMap()).toList(),
'mimeType': options.mimeType,
'quality': options.quality,
'lengthUnit': options.lengthUnit.name,
'width': options.width,
'height': options.height,
@ -195,10 +196,10 @@ class EntryConvertOptions extends Equatable {
final String mimeType;
final bool writeMetadata;
final LengthUnit lengthUnit;
final int width, height;
final int width, height, quality;
@override
List<Object?> get props => [mimeType, writeMetadata, lengthUnit, width, height];
List<Object?> get props => [mimeType, writeMetadata, lengthUnit, width, height, quality];
const EntryConvertOptions({
required this.mimeType,
@ -206,5 +207,6 @@ class EntryConvertOptions extends Equatable {
required this.lengthUnit,
required this.width,
required this.height,
required this.quality,
});
}

View file

@ -7,6 +7,8 @@ class SliderListTile extends StatelessWidget {
final double min;
final double max;
final int? divisions;
final EdgeInsetsGeometry titlePadding;
final Widget Function(BuildContext context, double value)? titleTrailing;
const SliderListTile({
super.key,
@ -16,6 +18,8 @@ class SliderListTile extends StatelessWidget {
this.min = 0.0,
this.max = 1.0,
this.divisions,
this.titlePadding = const EdgeInsetsDirectional.only(start: 16),
this.titleTrailing,
});
@override
@ -36,8 +40,14 @@ class SliderListTile extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsetsDirectional.only(start: 16),
child: Text(title),
padding: titlePadding,
child: Row(
children: [
Text(title),
const Spacer(),
if (titleTrailing != null) titleTrailing!(context, value),
],
),
),
Padding(
// match `SwitchListTile.contentPadding`

View file

@ -8,6 +8,8 @@ import 'package:aves/theme/text.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/utils/mime_utils.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/basic/list_tiles/slider.dart';
import 'package:aves/widgets/common/basic/text/change_highlight.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';
@ -35,6 +37,7 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
final TextEditingController _widthController = TextEditingController(), _heightController = TextEditingController();
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
late ValueNotifier<String> _mimeTypeNotifier;
late int _quality;
late bool _writeMetadata, _sameSized;
late List<LengthUnit> _lengthUnitOptions;
late LengthUnit _lengthUnit;
@ -48,10 +51,16 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
MimeTypes.webp,
];
static const qualityFormats = [
MimeTypes.jpeg,
MimeTypes.webp,
];
@override
void initState() {
super.initState();
_mimeTypeNotifier = ValueNotifier(settings.convertMimeType);
_quality = settings.convertQuality;
_writeMetadata = settings.convertWriteMetadata;
_sameSized = entries.map((entry) => entry.displaySize).toSet().length == 1;
_lengthUnitOptions = [
@ -88,6 +97,9 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
Widget build(BuildContext context) {
final l10n = context.l10n;
const contentHorizontalPadding = EdgeInsets.symmetric(horizontal: AvesDialog.defaultHorizontalContentPadding);
final theme = Theme.of(context);
final trailingStyle = TextStyle(color: theme.textTheme.bodySmall!.color);
final trailingChangeShadowColor = theme.colorScheme.onPrimary;
// used by the drop down to match input decoration
final textFieldDecorationBorder = Border(
@ -202,6 +214,51 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
],
),
),
ValueListenableBuilder<String>(
valueListenable: _mimeTypeNotifier,
builder: (context, mimeType, child) {
Widget child;
if (qualityFormats.contains(mimeType)) {
child = SliderListTile(
value: _quality.toDouble(),
onChanged: (v) => setState(() => _quality = v.round()),
min: 0,
max: 100,
title: context.l10n.exportEntryDialogQuality,
titlePadding: contentHorizontalPadding,
titleTrailing: (context, value) => ChangeHighlightText(
'${value.round()}',
style: trailingStyle.copyWith(
shadows: [
Shadow(
color: trailingChangeShadowColor.withOpacity(0),
blurRadius: 0,
)
],
),
changedStyle: trailingStyle.copyWith(
shadows: [
Shadow(
color: trailingChangeShadowColor,
blurRadius: 3,
)
],
),
duration: context.read<DurationsData>().formTextStyleTransition,
),
);
} else {
child = const SizedBox();
}
return AnimatedSwitcher(
duration: context.read<DurationsData>().formTransition,
switchInCurve: Curves.easeInOutCubic,
switchOutCurve: Curves.easeInOutCubic,
transitionBuilder: AvesTransitions.formTransitionBuilder,
child: child,
);
},
),
ValueListenableBuilder<String>(
valueListenable: _mimeTypeNotifier,
builder: (context, mimeType, child) {
@ -246,11 +303,13 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
lengthUnit: _lengthUnit,
width: width,
height: height,
quality: _quality,
)
: null;
if (options != null) {
settings.convertMimeType = options.mimeType;
settings.convertQuality = options.quality;
settings.convertWriteMetadata = options.writeMetadata;
}

View file

@ -226,6 +226,7 @@
"exportEntryDialogFormat",
"exportEntryDialogWidth",
"exportEntryDialogHeight",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"renameEntryDialogLabel",
"editEntryDialogCopyFromItem",
@ -804,6 +805,7 @@
"exportEntryDialogFormat",
"exportEntryDialogWidth",
"exportEntryDialogHeight",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"renameEntryDialogLabel",
"editEntryDialogCopyFromItem",
@ -1219,6 +1221,7 @@
"maxBrightnessAlways",
"videoResumptionModeNever",
"videoResumptionModeAlways",
"exportEntryDialogQuality",
"settingsAskEverytime",
"settingsVideoPlaybackTile",
"settingsVideoPlaybackPageTitle",
@ -1232,6 +1235,7 @@
"maxBrightnessAlways",
"videoResumptionModeNever",
"videoResumptionModeAlways",
"exportEntryDialogQuality",
"settingsAskEverytime",
"settingsVideoPlaybackTile",
"settingsVideoPlaybackPageTitle",
@ -1245,6 +1249,7 @@
"maxBrightnessAlways",
"videoResumptionModeNever",
"videoResumptionModeAlways",
"exportEntryDialogQuality",
"settingsAskEverytime",
"settingsVideoPlaybackTile",
"settingsVideoPlaybackPageTitle",
@ -1257,7 +1262,8 @@
"maxBrightnessNever",
"maxBrightnessAlways",
"videoResumptionModeNever",
"videoResumptionModeAlways"
"videoResumptionModeAlways",
"exportEntryDialogQuality"
],
"eu": [
@ -1265,6 +1271,7 @@
"maxBrightnessAlways",
"videoResumptionModeNever",
"videoResumptionModeAlways",
"exportEntryDialogQuality",
"settingsAskEverytime",
"settingsVideoPlaybackTile",
"settingsVideoPlaybackPageTitle",
@ -1386,6 +1393,7 @@
"renameProcessorName",
"deleteSingleAlbumConfirmationDialogMessage",
"deleteMultiAlbumConfirmationDialogMessage",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"renameEntryDialogLabel",
"editEntryDialogCopyFromItem",
@ -1771,7 +1779,8 @@
"maxBrightnessNever",
"maxBrightnessAlways",
"videoResumptionModeNever",
"videoResumptionModeAlways"
"videoResumptionModeAlways",
"exportEntryDialogQuality"
],
"gl": [
@ -1890,6 +1899,7 @@
"exportEntryDialogFormat",
"exportEntryDialogWidth",
"exportEntryDialogHeight",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"renameEntryDialogLabel",
"editEntryDialogCopyFromItem",
@ -2545,6 +2555,7 @@
"exportEntryDialogFormat",
"exportEntryDialogWidth",
"exportEntryDialogHeight",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"renameEntryDialogLabel",
"editEntryDialogCopyFromItem",
@ -3180,6 +3191,7 @@
"exportEntryDialogFormat",
"exportEntryDialogWidth",
"exportEntryDialogHeight",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"renameEntryDialogLabel",
"editEntryDialogCopyFromItem",
@ -3595,6 +3607,7 @@
"maxBrightnessAlways",
"videoResumptionModeNever",
"videoResumptionModeAlways",
"exportEntryDialogQuality",
"settingsAskEverytime",
"settingsVideoPlaybackTile",
"settingsVideoPlaybackPageTitle",
@ -3608,6 +3621,7 @@
"maxBrightnessAlways",
"videoResumptionModeNever",
"videoResumptionModeAlways",
"exportEntryDialogQuality",
"settingsAskEverytime",
"settingsVideoPlaybackTile",
"settingsVideoPlaybackPageTitle",
@ -3621,6 +3635,7 @@
"maxBrightnessAlways",
"videoResumptionModeNever",
"videoResumptionModeAlways",
"exportEntryDialogQuality",
"settingsAskEverytime",
"settingsCollectionBurstPatternsTile",
"settingsVideoPlaybackTile",
@ -3646,6 +3661,7 @@
"videoResumptionModeNever",
"videoResumptionModeAlways",
"vaultBinUsageDialogMessage",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"stateEmpty",
"placeEmpty",
@ -3675,7 +3691,8 @@
"maxBrightnessNever",
"maxBrightnessAlways",
"videoResumptionModeNever",
"videoResumptionModeAlways"
"videoResumptionModeAlways",
"exportEntryDialogQuality"
],
"lt": [
@ -3715,6 +3732,7 @@
"authenticateToConfigureVault",
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"tooManyItemsErrorDialogMessage",
"drawerPlacePage",
@ -3756,6 +3774,7 @@
"videoResumptionModeAlways",
"patternDialogEnter",
"patternDialogConfirm",
"exportEntryDialogQuality",
"statePageTitle",
"stateEmpty",
"searchStatesSectionTitle",
@ -3818,6 +3837,7 @@
"authenticateToConfigureVault",
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"tooManyItemsErrorDialogMessage",
"drawerPlacePage",
@ -3896,6 +3916,7 @@
"authenticateToConfigureVault",
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"editEntryDialogTargetFieldsHeader",
"editEntryDateDialogSetCustom",
@ -4414,6 +4435,7 @@
"exportEntryDialogFormat",
"exportEntryDialogWidth",
"exportEntryDialogHeight",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"editEntryDialogCopyFromItem",
"editEntryDialogTargetFieldsHeader",
@ -4785,6 +4807,7 @@
"maxBrightnessAlways",
"videoResumptionModeNever",
"videoResumptionModeAlways",
"exportEntryDialogQuality",
"settingsAskEverytime",
"settingsVideoPlaybackTile",
"settingsVideoPlaybackPageTitle",
@ -4798,6 +4821,7 @@
"maxBrightnessAlways",
"videoResumptionModeNever",
"videoResumptionModeAlways",
"exportEntryDialogQuality",
"settingsAskEverytime",
"settingsVideoPlaybackTile",
"settingsVideoPlaybackPageTitle",
@ -4811,6 +4835,7 @@
"maxBrightnessAlways",
"videoResumptionModeNever",
"videoResumptionModeAlways",
"exportEntryDialogQuality",
"settingsAskEverytime",
"settingsVideoPlaybackTile",
"settingsVideoPlaybackPageTitle",
@ -4824,6 +4849,7 @@
"maxBrightnessAlways",
"videoResumptionModeNever",
"videoResumptionModeAlways",
"exportEntryDialogQuality",
"statePageTitle",
"stateEmpty",
"searchStatesSectionTitle",
@ -4887,6 +4913,7 @@
"vaultBinUsageDialogMessage",
"deleteSingleAlbumConfirmationDialogMessage",
"deleteMultiAlbumConfirmationDialogMessage",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"editEntryLocationDialogLatitude",
"editEntryLocationDialogLongitude",
@ -5322,6 +5349,7 @@
"authenticateToConfigureVault",
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"editEntryDateDialogExtractFromTitle",
"editEntryDateDialogShift",
@ -5693,6 +5721,7 @@
"authenticateToConfigureVault",
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"drawerPlacePage",
"statePageTitle",
@ -5721,6 +5750,7 @@
"maxBrightnessAlways",
"videoResumptionModeNever",
"videoResumptionModeAlways",
"exportEntryDialogQuality",
"settingsAskEverytime",
"settingsVideoPlaybackTile",
"settingsVideoPlaybackPageTitle",
@ -5760,6 +5790,7 @@
"authenticateToConfigureVault",
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"tooManyItemsErrorDialogMessage",
"drawerPlacePage",
@ -5825,6 +5856,7 @@
"authenticateToConfigureVault",
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"tooManyItemsErrorDialogMessage",
"drawerPlacePage",