import 'package:aves/model/app/support.dart'; import 'package:aves/model/entry/entry.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/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'; import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'aves_dialog.dart'; class ConvertEntryDialog extends StatefulWidget { static const routeName = '/dialog/convert_entry'; final Set entries; const ConvertEntryDialog({ super.key, required this.entries, }); @override State createState() => _ConvertEntryDialogState(); } class _ConvertEntryDialogState extends State { final TextEditingController _widthController = TextEditingController(), _heightController = TextEditingController(); final ValueNotifier _isValidNotifier = ValueNotifier(false); late ValueNotifier _mimeTypeNotifier; late int _quality; late bool _writeMetadata, _sameSized; late List _lengthUnitOptions; late LengthUnit _lengthUnit; Set get entries => widget.entries; static const imageExportFormats = [ MimeTypes.bmp, MimeTypes.jpeg, MimeTypes.png, 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 = [ if (_sameSized) LengthUnit.px, LengthUnit.percent, ]; _lengthUnit = _lengthUnitOptions.first; _initDimensions(); _validate(); } void _initDimensions() { switch (_lengthUnit) { case LengthUnit.px: final displaySize = entries.first.displaySize; _widthController.text = '${displaySize.width.round()}'; _heightController.text = '${displaySize.height.round()}'; case LengthUnit.percent: _widthController.text = '100'; _heightController.text = '100'; } } @override void dispose() { _widthController.dispose(); _heightController.dispose(); _isValidNotifier.dispose(); _mimeTypeNotifier.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = context.l10n; const contentHorizontalPadding = EdgeInsets.symmetric(horizontal: AvesDialog.defaultHorizontalContentPadding); final colorScheme = Theme.of(context).colorScheme; final trailingStyle = TextStyle(color: colorScheme.onSurfaceVariant); final trailingChangeShadowColor = colorScheme.onBackground; // used by the drop down to match input decoration final textFieldDecorationBorder = Border( bottom: BorderSide( color: colorScheme.onSurface.withOpacity(0.38), width: 1.0, ), ); return AvesDialog( scrollableContent: [ const SizedBox(height: 16), Padding( padding: contentHorizontalPadding, child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(l10n.exportEntryDialogFormat), const SizedBox(width: AvesDialog.controlCaptionPadding), TextDropdownButton( values: imageExportFormats, valueText: MimeUtils.displayType, value: _mimeTypeNotifier.value, onChanged: (selected) { if (selected != null) { setState(() => _mimeTypeNotifier.value = selected); } }, ), ], ), ), Padding( padding: contentHorizontalPadding, child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Expanded( child: TextField( controller: _widthController, decoration: InputDecoration(labelText: l10n.exportEntryDialogWidth), keyboardType: TextInputType.number, onChanged: (value) { final width = int.tryParse(value); if (width != null) { switch (_lengthUnit) { case LengthUnit.px: _heightController.text = '${(width / entries.first.displayAspectRatio).round()}'; case LengthUnit.percent: _heightController.text = '$width'; } } else { _heightController.text = ''; } _validate(); }, ), ), const SizedBox(width: 8), const Text(AText.resolutionSeparator), const SizedBox(width: 8), Expanded( child: TextField( controller: _heightController, decoration: InputDecoration(labelText: l10n.exportEntryDialogHeight), keyboardType: TextInputType.number, onChanged: (value) { final height = int.tryParse(value); if (height != null) { switch (_lengthUnit) { case LengthUnit.px: _widthController.text = '${(height * entries.first.displayAspectRatio).round()}'; case LengthUnit.percent: _widthController.text = '$height'; } } else { _widthController.text = ''; } _validate(); }, ), ), const SizedBox(width: 16), TextDropdownButton( values: _lengthUnitOptions, valueText: (v) => v.getText(context), value: _lengthUnit, onChanged: _lengthUnitOptions.length > 1 ? (v) { if (v != null && _lengthUnit != v) { _lengthUnit = v; _initDimensions(); _validate(); setState(() {}); } } : null, underline: Container( height: 1.0, decoration: BoxDecoration( border: textFieldDecorationBorder, ), ), itemHeight: 60, dropdownColor: Themes.thirdLayerColor(context), ), ], ), ), ValueListenableBuilder( 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().formTextStyleTransition, ), ); } else { child = const SizedBox(); } return AnimatedSwitcher( duration: context.read().formTransition, switchInCurve: Curves.easeInOutCubic, switchOutCurve: Curves.easeInOutCubic, transitionBuilder: AvesTransitions.formTransitionBuilder, child: child, ); }, ), ValueListenableBuilder( valueListenable: _mimeTypeNotifier, builder: (context, mimeType, child) { Widget child; if (AppSupport.canEditExif(mimeType) || AppSupport.canEditIptc(mimeType) || AppSupport.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().formTransition, switchInCurve: Curves.easeInOutCubic, switchOutCurve: Curves.easeInOutCubic, transitionBuilder: AvesTransitions.formTransitionBuilder, child: child, ); }, ), ], actions: [ const CancelButton(), ValueListenableBuilder( valueListenable: _isValidNotifier, builder: (context, isValid, child) { return TextButton( onPressed: isValid ? () { final width = int.tryParse(_widthController.text); final height = int.tryParse(_heightController.text); final options = (width != null && height != null) ? EntryConvertOptions( mimeType: _mimeTypeNotifier.value, writeMetadata: _writeMetadata, lengthUnit: _lengthUnit, width: width, height: height, quality: _quality, ) : null; if (options != null) { settings.convertMimeType = options.mimeType; settings.convertQuality = options.quality; settings.convertWriteMetadata = options.writeMetadata; } Navigator.maybeOf(context)?.pop(options); } : null, child: Text(l10n.applyButtonLabel), ); }, ), ], ); } Future _validate() async { final width = int.tryParse(_widthController.text); final height = int.tryParse(_heightController.text); _isValidNotifier.value = (width ?? 0) > 0 && (height ?? 0) > 0; } }