#883 bulk converting motion photos to still images
This commit is contained in:
parent
87cfae1e9a
commit
978c22dc50
12 changed files with 328 additions and 227 deletions
|
@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
- Collection: stack RAW and JPEG with same file names
|
||||
- Collection: ask to rename/replace/skip when converting items with name conflict
|
||||
- Export: bulk converting motion photos to still images
|
||||
|
||||
## <a id="v1.11.3"></a>[v1.11.3] - 2024-06-17
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:collection';
|
|||
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||
import 'package:aves/model/entry/extensions/props.dart';
|
||||
import 'package:aves/model/entry/sort.dart';
|
||||
import 'package:aves/model/favourites.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
|
@ -13,7 +14,6 @@ import 'package:aves/model/filters/mime.dart';
|
|||
import 'package:aves/model/filters/query.dart';
|
||||
import 'package:aves/model/filters/rating.dart';
|
||||
import 'package:aves/model/filters/trash.dart';
|
||||
import 'package:aves/model/filters/type.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/events.dart';
|
||||
|
@ -224,7 +224,7 @@ class CollectionLens with ChangeNotifier {
|
|||
}
|
||||
|
||||
void _stackDevelopedRaws() {
|
||||
final allRawEntries = _filteredSortedEntries.where(TypeFilter.raw.test).toSet();
|
||||
final allRawEntries = _filteredSortedEntries.where((entry) => entry.isRaw).toSet();
|
||||
if (allRawEntries.isNotEmpty) {
|
||||
final allDevelopedEntries = _filteredSortedEntries.where(MimeFilter(MimeTypes.jpeg).test).toSet();
|
||||
final rawEntriesByDir = groupBy<AvesEntry, String?>(allRawEntries, (entry) => entry.directory);
|
||||
|
|
|
@ -193,15 +193,17 @@ class PlatformMediaEditService implements MediaEditService {
|
|||
|
||||
@immutable
|
||||
class EntryConvertOptions extends Equatable {
|
||||
final EntryConvertAction action;
|
||||
final String mimeType;
|
||||
final bool writeMetadata;
|
||||
final LengthUnit lengthUnit;
|
||||
final int width, height, quality;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [mimeType, writeMetadata, lengthUnit, width, height, quality];
|
||||
List<Object?> get props => [action, mimeType, writeMetadata, lengthUnit, width, height, quality];
|
||||
|
||||
const EntryConvertOptions({
|
||||
required this.action,
|
||||
required this.mimeType,
|
||||
required this.writeMetadata,
|
||||
required this.lengthUnit,
|
||||
|
|
|
@ -5,45 +5,46 @@ import 'package:flutter/material.dart';
|
|||
|
||||
extension ExtraEntrySetActionView on EntrySetAction {
|
||||
String getText(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return switch (this) {
|
||||
// general
|
||||
EntrySetAction.configureView => context.l10n.menuActionConfigureView,
|
||||
EntrySetAction.select => context.l10n.menuActionSelect,
|
||||
EntrySetAction.selectAll => context.l10n.menuActionSelectAll,
|
||||
EntrySetAction.selectNone => context.l10n.menuActionSelectNone,
|
||||
EntrySetAction.configureView => l10n.menuActionConfigureView,
|
||||
EntrySetAction.select => l10n.menuActionSelect,
|
||||
EntrySetAction.selectAll => l10n.menuActionSelectAll,
|
||||
EntrySetAction.selectNone => l10n.menuActionSelectNone,
|
||||
// browsing
|
||||
EntrySetAction.searchCollection => MaterialLocalizations.of(context).searchFieldLabel,
|
||||
EntrySetAction.toggleTitleSearch =>
|
||||
// different data depending on toggle state
|
||||
context.l10n.collectionActionShowTitleSearch,
|
||||
EntrySetAction.addShortcut => context.l10n.collectionActionAddShortcut,
|
||||
EntrySetAction.setHome => context.l10n.collectionActionSetHome,
|
||||
EntrySetAction.emptyBin => context.l10n.collectionActionEmptyBin,
|
||||
l10n.collectionActionShowTitleSearch,
|
||||
EntrySetAction.addShortcut => l10n.collectionActionAddShortcut,
|
||||
EntrySetAction.setHome => l10n.collectionActionSetHome,
|
||||
EntrySetAction.emptyBin => l10n.collectionActionEmptyBin,
|
||||
// browsing or selecting
|
||||
EntrySetAction.map => context.l10n.menuActionMap,
|
||||
EntrySetAction.slideshow => context.l10n.menuActionSlideshow,
|
||||
EntrySetAction.stats => context.l10n.menuActionStats,
|
||||
EntrySetAction.rescan => context.l10n.collectionActionRescan,
|
||||
EntrySetAction.map => l10n.menuActionMap,
|
||||
EntrySetAction.slideshow => l10n.menuActionSlideshow,
|
||||
EntrySetAction.stats => l10n.menuActionStats,
|
||||
EntrySetAction.rescan => l10n.collectionActionRescan,
|
||||
// selecting
|
||||
EntrySetAction.share => context.l10n.entryActionShare,
|
||||
EntrySetAction.delete => context.l10n.entryActionDelete,
|
||||
EntrySetAction.restore => context.l10n.entryActionRestore,
|
||||
EntrySetAction.copy => context.l10n.collectionActionCopy,
|
||||
EntrySetAction.move => context.l10n.collectionActionMove,
|
||||
EntrySetAction.rename => context.l10n.entryActionRename,
|
||||
EntrySetAction.convert => context.l10n.entryActionConvert,
|
||||
EntrySetAction.share => l10n.entryActionShare,
|
||||
EntrySetAction.delete => l10n.entryActionDelete,
|
||||
EntrySetAction.restore => l10n.entryActionRestore,
|
||||
EntrySetAction.copy => l10n.collectionActionCopy,
|
||||
EntrySetAction.move => l10n.collectionActionMove,
|
||||
EntrySetAction.rename => l10n.entryActionRename,
|
||||
EntrySetAction.convert => l10n.entryActionConvert,
|
||||
EntrySetAction.toggleFavourite =>
|
||||
// different data depending on toggle state
|
||||
context.l10n.entryActionAddFavourite,
|
||||
EntrySetAction.rotateCCW => context.l10n.entryActionRotateCCW,
|
||||
EntrySetAction.rotateCW => context.l10n.entryActionRotateCW,
|
||||
EntrySetAction.flip => context.l10n.entryActionFlip,
|
||||
EntrySetAction.editDate => context.l10n.entryInfoActionEditDate,
|
||||
EntrySetAction.editLocation => context.l10n.entryInfoActionEditLocation,
|
||||
EntrySetAction.editTitleDescription => context.l10n.entryInfoActionEditTitleDescription,
|
||||
EntrySetAction.editRating => context.l10n.entryInfoActionEditRating,
|
||||
EntrySetAction.editTags => context.l10n.entryInfoActionEditTags,
|
||||
EntrySetAction.removeMetadata => context.l10n.entryInfoActionRemoveMetadata,
|
||||
l10n.entryActionAddFavourite,
|
||||
EntrySetAction.rotateCCW => l10n.entryActionRotateCCW,
|
||||
EntrySetAction.rotateCW => l10n.entryActionRotateCW,
|
||||
EntrySetAction.flip => l10n.entryActionFlip,
|
||||
EntrySetAction.editDate => l10n.entryInfoActionEditDate,
|
||||
EntrySetAction.editLocation => l10n.entryInfoActionEditLocation,
|
||||
EntrySetAction.editTitleDescription => l10n.entryInfoActionEditTitleDescription,
|
||||
EntrySetAction.editRating => l10n.entryInfoActionEditRating,
|
||||
EntrySetAction.editTags => l10n.entryInfoActionEditTags,
|
||||
EntrySetAction.removeMetadata => l10n.entryInfoActionRemoveMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
21
lib/view/src/metadata/convert_action.dart
Normal file
21
lib/view/src/metadata/convert_action.dart
Normal file
|
@ -0,0 +1,21 @@
|
|||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves_model/aves_model.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
extension ExtraEntryConvertActionView on EntryConvertAction {
|
||||
String getText(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return switch (this) {
|
||||
EntryConvertAction.convert => l10n.entryActionConvert,
|
||||
EntryConvertAction.convertMotionPhotoToStillImage => l10n.entryActionConvertMotionPhotoToStillImage,
|
||||
};
|
||||
}
|
||||
|
||||
IconData getIconData() {
|
||||
return switch (this) {
|
||||
EntryConvertAction.convert => AIcons.convert,
|
||||
EntryConvertAction.convertMotionPhotoToStillImage => AIcons.convertToStillImage,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import 'package:aves/model/device.dart';
|
|||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/favourites.dart';
|
||||
import 'package:aves/model/entry/extensions/metadata_edition.dart';
|
||||
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||
import 'package:aves/model/entry/extensions/props.dart';
|
||||
import 'package:aves/model/favourites.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
|
@ -20,6 +21,7 @@ import 'package:aves/model/vaults/vaults.dart';
|
|||
import 'package:aves/services/app_service.dart';
|
||||
import 'package:aves/services/common/image_op_events.dart';
|
||||
import 'package:aves/services/common/services.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/collection_utils.dart';
|
||||
|
@ -34,6 +36,7 @@ import 'package:aves/widgets/common/search/route.dart';
|
|||
import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/convert_entry_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/entry_editors/rename_entry_set_page.dart';
|
||||
import 'package:aves/widgets/dialogs/pick_dialogs/location_pick_page.dart';
|
||||
import 'package:aves/widgets/map/map_page.dart';
|
||||
|
@ -366,9 +369,23 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
_browse(context);
|
||||
}
|
||||
|
||||
void _convert(BuildContext context) {
|
||||
Future<void> _convert(BuildContext context) async {
|
||||
final entries = _getTargetItems(context);
|
||||
convert(context, entries);
|
||||
|
||||
final options = await showDialog<EntryConvertOptions>(
|
||||
context: context,
|
||||
builder: (context) => ConvertEntryDialog(entries: entries),
|
||||
routeSettings: const RouteSettings(name: ConvertEntryDialog.routeName),
|
||||
);
|
||||
if (options == null) return;
|
||||
|
||||
switch (options.action) {
|
||||
case EntryConvertAction.convert:
|
||||
await doExport(context, entries, options);
|
||||
case EntryConvertAction.convertMotionPhotoToStillImage:
|
||||
final todoItems = entries.where((entry) => entry.isMotionPhoto).toSet();
|
||||
await _edit(context, todoItems, (entry) => entry.removeTrailerVideo());
|
||||
}
|
||||
|
||||
_browse(context);
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
|||
import 'package:aves/widgets/common/action_mixins/size_aware.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/convert_entry_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart';
|
||||
import 'package:aves/widgets/dialogs/selection_dialogs/single_selection.dart';
|
||||
import 'package:aves/widgets/viewer/controls/notifications.dart';
|
||||
|
@ -38,14 +37,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
|
||||
mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
||||
Future<void> convert(BuildContext context, Set<AvesEntry> targetEntries) async {
|
||||
final options = await showDialog<EntryConvertOptions>(
|
||||
context: context,
|
||||
builder: (context) => ConvertEntryDialog(entries: targetEntries),
|
||||
routeSettings: const RouteSettings(name: ConvertEntryDialog.routeName),
|
||||
);
|
||||
if (options == null) return;
|
||||
|
||||
Future<void> doExport(BuildContext context, Set<AvesEntry> targetEntries, EntryConvertOptions options) async {
|
||||
final destinationAlbum = await pickAlbum(context: context, moveType: MoveType.export);
|
||||
if (destinationAlbum == null) return;
|
||||
if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:aves/model/app/support.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/services/media/media_edit_service.dart';
|
||||
|
@ -7,6 +8,7 @@ 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/src/metadata/convert_action.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';
|
||||
|
@ -34,6 +36,8 @@ class ConvertEntryDialog extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
||||
late List<EntryConvertAction> _actionOptions;
|
||||
EntryConvertAction _action = EntryConvertAction.convert;
|
||||
final TextEditingController _widthController = TextEditingController(), _heightController = TextEditingController();
|
||||
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
||||
late ValueNotifier<String> _mimeTypeNotifier;
|
||||
|
@ -44,14 +48,16 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
|||
|
||||
Set<AvesEntry> get entries => widget.entries;
|
||||
|
||||
static const imageExportFormats = [
|
||||
EdgeInsets get contentHorizontalPadding => const EdgeInsets.symmetric(horizontal: AvesDialog.defaultHorizontalContentPadding);
|
||||
|
||||
static const _imageExportFormats = [
|
||||
MimeTypes.bmp,
|
||||
MimeTypes.jpeg,
|
||||
MimeTypes.png,
|
||||
MimeTypes.webp,
|
||||
];
|
||||
|
||||
static const qualityFormats = [
|
||||
static const _qualityFormats = [
|
||||
MimeTypes.jpeg,
|
||||
MimeTypes.webp,
|
||||
];
|
||||
|
@ -59,6 +65,10 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_actionOptions = [
|
||||
EntryConvertAction.convert,
|
||||
if (entries.any((entry) => entry.isMotionPhoto)) EntryConvertAction.convertMotionPhotoToStillImage,
|
||||
];
|
||||
_mimeTypeNotifier = ValueNotifier(settings.convertMimeType);
|
||||
_quality = settings.convertQuality;
|
||||
_writeMetadata = settings.convertWriteMetadata;
|
||||
|
@ -95,192 +105,41 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
|||
|
||||
@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.onSurface;
|
||||
|
||||
// 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(
|
||||
if (_actionOptions.length > 1)
|
||||
Padding(
|
||||
padding: contentHorizontalPadding,
|
||||
child: TextDropdownButton<EntryConvertAction>(
|
||||
values: _actionOptions,
|
||||
valueText: (v) => v.getText(context),
|
||||
valueIcon: (v) => v.getIconData(),
|
||||
value: _action,
|
||||
onChanged: (v) {
|
||||
_action = v!;
|
||||
_validate();
|
||||
setState(() {});
|
||||
},
|
||||
isExpanded: true,
|
||||
dropdownColor: Themes.thirdLayerColor(context),
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: context.read<DurationsData>().formTransition,
|
||||
switchInCurve: Curves.easeInOutCubic,
|
||||
switchOutCurve: Curves.easeInOutCubic,
|
||||
transitionBuilder: AvesTransitions.formTransitionBuilder,
|
||||
child: Column(
|
||||
key: ValueKey(_action),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(l10n.exportEntryDialogFormat),
|
||||
const SizedBox(width: AvesDialog.controlCaptionPadding),
|
||||
TextDropdownButton<String>(
|
||||
values: imageExportFormats,
|
||||
valueText: MimeUtils.displayType,
|
||||
value: _mimeTypeNotifier.value,
|
||||
onChanged: (selected) {
|
||||
if (selected != null) {
|
||||
setState(() => _mimeTypeNotifier.value = selected);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (_action == EntryConvertAction.convert) ..._buildConvertContent(context),
|
||||
if (_action == EntryConvertAction.convertMotionPhotoToStillImage) const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
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<LengthUnit>(
|
||||
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<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) {
|
||||
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<DurationsData>().formTransition,
|
||||
switchInCurve: Curves.easeInOutCubic,
|
||||
switchOutCurve: Curves.easeInOutCubic,
|
||||
transitionBuilder: AvesTransitions.formTransitionBuilder,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
|
@ -294,6 +153,7 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
|||
final height = int.tryParse(_heightController.text);
|
||||
final options = (width != null && height != null)
|
||||
? EntryConvertOptions(
|
||||
action: _action,
|
||||
mimeType: _mimeTypeNotifier.value,
|
||||
writeMetadata: _writeMetadata,
|
||||
lengthUnit: _lengthUnit,
|
||||
|
@ -312,7 +172,7 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
|||
Navigator.maybeOf(context)?.pop(options);
|
||||
}
|
||||
: null,
|
||||
child: Text(l10n.applyButtonLabel),
|
||||
child: Text(context.l10n.applyButtonLabel),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -320,6 +180,193 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
|||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildConvertContent(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final trailingStyle = TextStyle(color: colorScheme.onSurfaceVariant);
|
||||
final trailingChangeShadowColor = colorScheme.onSurface;
|
||||
|
||||
// used by the drop down to match input decoration
|
||||
final textFieldDecorationBorder = Border(
|
||||
bottom: BorderSide(
|
||||
color: colorScheme.onSurface.withOpacity(0.38),
|
||||
width: 1.0,
|
||||
),
|
||||
);
|
||||
|
||||
return [
|
||||
Padding(
|
||||
padding: contentHorizontalPadding,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(l10n.exportEntryDialogFormat),
|
||||
const SizedBox(width: AvesDialog.controlCaptionPadding),
|
||||
TextDropdownButton<String>(
|
||||
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<LengthUnit>(
|
||||
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<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) {
|
||||
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<DurationsData>().formTransition,
|
||||
switchInCurve: Curves.easeInOutCubic,
|
||||
switchOutCurve: Curves.easeInOutCubic,
|
||||
transitionBuilder: AvesTransitions.formTransitionBuilder,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _validate() async {
|
||||
final width = int.tryParse(_widthController.text);
|
||||
final height = int.tryParse(_heightController.text);
|
||||
|
|
|
@ -15,6 +15,7 @@ import 'package:aves/model/source/collection_lens.dart';
|
|||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/vaults/vaults.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/services/media/media_edit_service.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/entry_storage.dart';
|
||||
|
@ -26,6 +27,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/convert_entry_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/entry_editors/rename_entry_dialog.dart';
|
||||
import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart';
|
||||
import 'package:aves/widgets/viewer/action/printer.dart';
|
||||
|
@ -198,7 +200,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
case EntryAction.restore:
|
||||
_move(context, targetEntry, moveType: MoveType.fromBin);
|
||||
case EntryAction.convert:
|
||||
convert(context, {targetEntry});
|
||||
_convert(context, targetEntry);
|
||||
case EntryAction.print:
|
||||
EntryPrinter(targetEntry).print(context);
|
||||
case EntryAction.rename:
|
||||
|
@ -444,6 +446,22 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
entries: {targetEntry},
|
||||
);
|
||||
|
||||
Future<void> _convert(BuildContext context, AvesEntry targetEntry) async {
|
||||
final options = await showDialog<EntryConvertOptions>(
|
||||
context: context,
|
||||
builder: (context) => ConvertEntryDialog(entries: {targetEntry}),
|
||||
routeSettings: const RouteSettings(name: ConvertEntryDialog.routeName),
|
||||
);
|
||||
if (options == null) return;
|
||||
|
||||
switch (options.action) {
|
||||
case EntryConvertAction.convert:
|
||||
await doExport(context, {targetEntry}, options);
|
||||
case EntryConvertAction.convertMotionPhotoToStillImage:
|
||||
await _metadataActionDelegate.onActionSelected(context, targetEntry, collection, EntryAction.convertMotionPhotoToStillImage);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _rename(BuildContext context, AvesEntry targetEntry) async {
|
||||
final newName = await showDialog<String>(
|
||||
context: context,
|
||||
|
|
|
@ -92,7 +92,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
|
|||
}
|
||||
}
|
||||
|
||||
void onActionSelected(BuildContext context, AvesEntry targetEntry, CollectionLens? collection, EntryAction action) async {
|
||||
Future<void> onActionSelected(BuildContext context, AvesEntry targetEntry, CollectionLens? collection, EntryAction action) async {
|
||||
await reportService.log('$action');
|
||||
_eventStreamController.add(ActionStartedEvent(action));
|
||||
switch (action) {
|
||||
|
|
|
@ -94,7 +94,7 @@ class InfoAppBar extends StatelessWidget {
|
|||
onSelected: (action) async {
|
||||
// wait for the popup menu to hide before proceeding with the action
|
||||
await Future.delayed(animations.popUpAnimationDelay * timeDilation);
|
||||
actionDelegate.onActionSelected(context, entry, collection, action);
|
||||
await actionDelegate.onActionSelected(context, entry, collection, action);
|
||||
},
|
||||
popUpAnimationStyle: animations.popUpAnimationStyle,
|
||||
),
|
||||
|
|
|
@ -15,6 +15,8 @@ enum DateFieldSource {
|
|||
exifGpsDate,
|
||||
}
|
||||
|
||||
enum EntryConvertAction { convert, convertMotionPhotoToStillImage }
|
||||
|
||||
enum LengthUnit { px, percent }
|
||||
|
||||
enum LocationEditAction {
|
||||
|
|
Loading…
Reference in a new issue