diff --git a/mobile/lib/constants/aspect_ratios.dart b/mobile/lib/constants/aspect_ratios.dart new file mode 100644 index 0000000000..be0486a193 --- /dev/null +++ b/mobile/lib/constants/aspect_ratios.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class AspectRatioPreset { + final double? ratio; + final String label; + final IconData icon; + final bool iconRotated; + + AspectRatioPreset({required this.ratio, required this.label, required this.icon, this.iconRotated = false}); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is AspectRatioPreset) { + return ratio == other.ratio && label == other.label; + } + return false; + } + + @override + int get hashCode => ratio.hashCode ^ label.hashCode; +} + +final aspectRatios = { + 'Free': AspectRatioPreset(ratio: null, label: 'Free', icon: Icons.crop_free_rounded), + '1:1': AspectRatioPreset(ratio: 1.0, label: '1:1', icon: Icons.crop_square_rounded), + '16:9': AspectRatioPreset(ratio: 16 / 9, label: '16:9', icon: Icons.crop_16_9_rounded), + '3:2': AspectRatioPreset(ratio: 3 / 2, label: '3:2', icon: Icons.crop_3_2_rounded), + '7:5': AspectRatioPreset(ratio: 7 / 5, label: '7:5', icon: Icons.crop_7_5_rounded), + '9:16': AspectRatioPreset(ratio: 9 / 16, label: '9:16', icon: Icons.crop_16_9_rounded, iconRotated: true), + '2:3': AspectRatioPreset(ratio: 2 / 3, label: '2:3', icon: Icons.crop_3_2_rounded, iconRotated: true), + '5:7': AspectRatioPreset(ratio: 5 / 7, label: '5:7', icon: Icons.crop_7_5_rounded, iconRotated: true), +}; diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart index f28ee3d72d..73e40111fe 100644 --- a/mobile/lib/domain/models/asset/base_asset.model.dart +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -69,7 +69,7 @@ sealed class BaseAsset { bool get isLocalOnly => storage == AssetState.local; bool get isRemoteOnly => storage == AssetState.remote; - bool get isEditable => isImage && !isMotionPhoto && this is RemoteAsset; + bool get isEditable => false; // Overridden in subclasses AssetState get storage; diff --git a/mobile/lib/domain/models/asset/remote_asset.model.dart b/mobile/lib/domain/models/asset/remote_asset.model.dart index 43d49506e3..a8bc020b16 100644 --- a/mobile/lib/domain/models/asset/remote_asset.model.dart +++ b/mobile/lib/domain/models/asset/remote_asset.model.dart @@ -43,6 +43,9 @@ class RemoteAsset extends BaseAsset { @override String get heroTag => '${localId ?? checksum}_$id'; + @override + bool get isEditable => isImage && !isMotionPhoto && !isAnimatedImage; + @override String toString() { return '''Asset { diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index 924634ba15..198733b3c8 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -1,6 +1,5 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; @@ -117,12 +116,4 @@ class AssetService { Future> getSourceAlbums(String localAssetId, {BackupSelection? backupSelection}) { return _localAssetRepository.getSourceAlbums(localAssetId, backupSelection: backupSelection); } - - Future> getAssetEdits(String assetId) { - return _remoteAssetRepository.getAssetEdits(assetId); - } - - Future editAsset(String assetId, List edits) { - return _remoteAssetRepository.editAsset(assetId, edits); - } } diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index cb09590575..002160f549 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -4,7 +4,6 @@ import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/stack.model.dart'; import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' hide ExifInfo; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; @@ -12,7 +11,6 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift. import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; -import 'package:uuid/uuid.dart'; class RemoteAssetRepository extends DriftDatabaseRepository { final Drift _db; @@ -269,34 +267,11 @@ class RemoteAssetRepository extends DriftDatabaseRepository { return _db.managers.remoteAssetEntity.count(); } - Future> getAssetEdits(String assetId) async { + Future> getAssetEdits(String assetId) { final query = _db.assetEditEntity.select() ..where((row) => row.assetId.equals(assetId)) ..orderBy([(row) => OrderingTerm.asc(row.sequence)]); return query.map((row) => row.toDto()).get(); } - - Future editAsset(String assetId, List edits) async { - await _db.transaction(() async { - await _db.batch((batch) async { - // delete existing edits - batch.deleteWhere(_db.assetEditEntity, (row) => row.assetId.equals(assetId)); - - // insert new edits - for (var i = 0; i < edits.length; i++) { - final edit = edits[i]; - final companion = AssetEditEntityCompanion( - id: Value(const Uuid().v4()), - assetId: Value(assetId), - action: Value(edit.action), - parameters: Value(edit.parameters), - sequence: Value(i), - ); - - batch.insert(_db.assetEditEntity, companion); - } - }); - }); - } } diff --git a/mobile/lib/presentation/pages/drift_edit.page.dart b/mobile/lib/presentation/pages/drift_edit.page.dart index 136beb4292..f21e7654f6 100644 --- a/mobile/lib/presentation/pages/drift_edit.page.dart +++ b/mobile/lib/presentation/pages/drift_edit.page.dart @@ -7,7 +7,7 @@ import 'package:crop_image/crop_image.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/constants/aspect_ratios.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -20,7 +20,6 @@ import 'package:openapi/api.dart' show CropParameters, RotateParameters, MirrorP @RoutePage() class DriftEditImagePage extends ConsumerStatefulWidget { final Image image; - final BaseAsset asset; final List edits; final ExifInfo exifInfo; final Future Function(List edits) applyEdits; @@ -28,7 +27,6 @@ class DriftEditImagePage extends ConsumerStatefulWidget { const DriftEditImagePage({ super.key, required this.image, - required this.asset, required this.edits, required this.exifInfo, required this.applyEdits, @@ -38,12 +36,10 @@ class DriftEditImagePage extends ConsumerStatefulWidget { ConsumerState createState() => _DriftEditImagePageState(); } -typedef AspectRatio = ({double? ratio, String label}); - class _DriftEditImagePageState extends ConsumerState with TickerProviderStateMixin { late final CropController cropController; - Duration _rotationAnimationDuration = const Duration(milliseconds: 250); + Duration _rotationAnimationDuration = const Duration(milliseconds: 0); int _rotationAngle = 0; bool _flipHorizontal = false; @@ -56,18 +52,6 @@ class _DriftEditImagePageState extends ConsumerState with Ti bool _isApplyingEdits = false; bool _hasSheetChanges = false; late final Rect _initialCrop; - final String _selectedSegment = 'transform'; - - List aspectRatios = [ - (ratio: null, label: 'Free'), - (ratio: 1.0, label: '1:1'), - (ratio: 16.0 / 9.0, label: '16:9'), - (ratio: 3.0 / 2.0, label: '3:2'), - (ratio: 7.0 / 5.0, label: '7:5'), - (ratio: 9.0 / 16.0, label: '9:16'), - (ratio: 2.0 / 3.0, label: '2:3'), - (ratio: 5.0 / 7.0, label: '5:7'), - ]; void initEditor() { final existingCrop = widget.edits.firstWhereOrNull((edit) => edit.action == AssetEditAction.crop); @@ -83,9 +67,6 @@ class _DriftEditImagePageState extends ConsumerState with Ti cropController = CropController(defaultCrop: crop); final transform = normalizeTransformEdits(widget.edits); - - // dont animate to initial rotation - _rotationAnimationDuration = const Duration(milliseconds: 0); _rotationAngle = transform.rotation.toInt(); _flipHorizontal = transform.mirrorHorizontal; @@ -160,7 +141,7 @@ class _DriftEditImagePageState extends ConsumerState with Ti void _rotateLeft() { setState(() { - _rotationAnimationDuration = const Duration(milliseconds: 150); + _rotationAnimationDuration = const Duration(milliseconds: 250); _rotationAngle -= 90; _hasSheetChanges = true; }); @@ -168,7 +149,7 @@ class _DriftEditImagePageState extends ConsumerState with Ti void _rotateRight() { setState(() { - _rotationAnimationDuration = const Duration(milliseconds: 150); + _rotationAnimationDuration = const Duration(milliseconds: 250); _rotationAngle += 90; _hasSheetChanges = true; }); @@ -230,30 +211,29 @@ class _DriftEditImagePageState extends ConsumerState with Ti return isCropped || isRotated || isFlipped; } - Future _showDiscardChangesDialog() async { - return await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('editor_discard_edits_title'.tr()), - content: Text('editor_discard_edits_prompt'.tr()), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - style: ButtonStyle( - foregroundColor: WidgetStateProperty.all(context.themeData.colorScheme.onSurfaceVariant), - ), - child: Text('cancel'.tr()), - ), - TextButton(onPressed: () => Navigator.of(context).pop(true), child: Text('confirm'.tr())), - ], + Future _showDiscardChangesDialog() { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('editor_discard_edits_title'.tr()), + content: Text('editor_discard_edits_prompt'.tr()), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + style: ButtonStyle( + foregroundColor: WidgetStateProperty.all(context.themeData.colorScheme.onSurfaceVariant), + ), + child: Text('cancel'.tr()), ), - ) ?? - false; + TextButton(onPressed: () => Navigator.of(context).pop(true), child: Text('confirm'.tr())), + ], + ), + ); } Future _handleClose() async { if (hasUnsavedChanges) { - final shouldDiscard = await _showDiscardChangesDialog(); + final shouldDiscard = await _showDiscardChangesDialog() ?? false; if (shouldDiscard && mounted) { Navigator.of(context).pop(); } @@ -268,7 +248,7 @@ class _DriftEditImagePageState extends ConsumerState with Ti canPop: !hasUnsavedChanges, onPopInvokedWithResult: (didPop, result) async { if (didPop) return; - final shouldDiscard = await _showDiscardChangesDialog(); + final shouldDiscard = await _showDiscardChangesDialog() ?? false; if (shouldDiscard && mounted) { Navigator.of(context).pop(); } @@ -319,19 +299,10 @@ class _DriftEditImagePageState extends ConsumerState with Ti padding: const EdgeInsets.all(10), width: (_rotationAngle % 180 == 0) ? baseWidth : baseHeight, height: (_rotationAngle % 180 == 0) ? baseHeight : baseWidth, - child: FutureBuilder( - future: resolveImage(widget.image.image), - builder: (context, data) { - if (!data.hasData) { - return const Center(child: CircularProgressIndicator()); - } - - return CropImage( - controller: cropController, - image: widget.image, - gridColor: Colors.white, - ); - }, + child: CropImage( + controller: cropController, + image: widget.image, + gridColor: Colors.white, ), ), ), @@ -357,48 +328,18 @@ class _DriftEditImagePageState extends ConsumerState with Ti child: Column( mainAxisSize: MainAxisSize.min, children: [ - AnimatedCrossFade( - duration: const Duration(milliseconds: 250), - firstCurve: Curves.easeInOut, - secondCurve: Curves.easeInOut, - sizeCurve: Curves.easeInOut, - crossFadeState: _selectedSegment == 'transform' - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: _TransformControls( - onRotateLeft: _rotateLeft, - onRotateRight: _rotateRight, - onFlipHorizontal: _flipHorizontally, - onFlipVertical: _flipVertically, - onAspectRatioSelected: _applyAspectRatio, - aspectRatio: _aspectRatio, - ), - // this will never show since the segmented button is not shown yet - secondChild: const Text("Filters coming soon!"), + _TransformControls( + onRotateLeft: _rotateLeft, + onRotateRight: _rotateRight, + onFlipHorizontal: _flipHorizontally, + onFlipVertical: _flipVertically, + onAspectRatioSelected: _applyAspectRatio, + aspectRatio: _aspectRatio, ), Padding( padding: const EdgeInsets.only(bottom: 36, left: 24, right: 24), child: Row( children: [ - // SegmentedButton( - // segments: [ - // const ButtonSegment( - // value: 'transform', - // label: Text('Transform'), - // icon: Icon(Icons.transform), - // ), - // const ButtonSegment( - // value: 'filters', - // label: Text('Filters'), - // icon: Icon(Icons.color_lens), - // ), - // ], - // selected: {selectedSegment}, - // onSelectionChanged: (value) => setState(() { - // selectedSegment = value.first; - // }), - // showSelectedIcon: false, - // ), const Spacer(), ImmichTextButton( labelText: 'reset'.tr(), @@ -424,17 +365,11 @@ class _DriftEditImagePageState extends ConsumerState with Ti } class _AspectRatioButton extends StatelessWidget { - final double? currentAspectRatio; - final double? ratio; - final String label; + final AspectRatioPreset ratio; + final bool isSelected; final VoidCallback onPressed; - const _AspectRatioButton({ - required this.currentAspectRatio, - required this.ratio, - required this.label, - required this.onPressed, - }); + const _AspectRatioButton({required this.ratio, required this.isSelected, required this.onPressed}); @override Widget build(BuildContext context) { @@ -444,22 +379,12 @@ class _AspectRatioButton extends StatelessWidget { IconButton( iconSize: 36, icon: Transform.rotate( - angle: (ratio ?? 1.0) < 1.0 ? pi / 2 : 0, - child: Icon(switch (label) { - 'Free' => Icons.crop_free_rounded, - '1:1' => Icons.crop_square_rounded, - '16:9' => Icons.crop_16_9_rounded, - '3:2' => Icons.crop_3_2_rounded, - '7:5' => Icons.crop_7_5_rounded, - '9:16' => Icons.crop_16_9_rounded, - '2:3' => Icons.crop_3_2_rounded, - '5:7' => Icons.crop_7_5_rounded, - _ => Icons.crop_free_rounded, - }, color: currentAspectRatio == ratio ? context.primaryColor : context.themeData.iconTheme.color), + angle: ratio.iconRotated ? pi / 2 : 0, + child: Icon(ratio.icon, color: isSelected ? context.primaryColor : context.themeData.iconTheme.color), ), onPressed: onPressed, ), - Text(label, style: context.textTheme.displayMedium), + Text(ratio.label, style: context.textTheme.displayMedium), ], ); } @@ -473,17 +398,6 @@ class _AspectRatioSelector extends StatelessWidget { @override Widget build(BuildContext context) { - final aspectRatios = { - 'Free': null, - '1:1': 1.0, - '16:9': 16 / 9, - '3:2': 3 / 2, - '7:5': 7 / 5, - '9:16': 9 / 16, - '2:3': 2 / 3, - '5:7': 5 / 7, - }; - return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( @@ -491,10 +405,9 @@ class _AspectRatioSelector extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: _AspectRatioButton( - currentAspectRatio: currentAspectRatio, ratio: entry.value, - label: entry.key, - onPressed: () => onAspectRatioSelected(entry.value), + isSelected: currentAspectRatio == entry.value.ratio, + onPressed: () => onAspectRatioSelected(entry.value.ratio), ), ); }).toList(), diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart index 4614cdff19..8d96e04fbb 100644 --- a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart @@ -63,7 +63,7 @@ class EditImageActionButton extends ConsumerWidget { } await context.pushRoute( - DriftEditImageRoute(asset: currentAsset, image: image, edits: edits, exifInfo: exifInfo, applyEdits: editImage), + DriftEditImageRoute(image: image, edits: edits, exifInfo: exifInfo, applyEdits: editImage), ); } diff --git a/mobile/lib/presentation/widgets/images/remote_image_provider.dart b/mobile/lib/presentation/widgets/images/remote_image_provider.dart index 7f2e372ee8..41894bdf81 100644 --- a/mobile/lib/presentation/widgets/images/remote_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/remote_image_provider.dart @@ -13,10 +13,11 @@ import 'package:openapi/api.dart'; class RemoteImageProvider extends CancellableImageProvider with CancellableImageProviderMixin { final String url; + final bool edited; - RemoteImageProvider({required this.url}); + RemoteImageProvider({required this.url, this.edited = true}); - RemoteImageProvider.thumbnail({required String assetId, required String thumbhash, bool edited = true}) + RemoteImageProvider.thumbnail({required String assetId, required String thumbhash, this.edited = true}) : url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash, edited: edited); @override @@ -45,13 +46,13 @@ class RemoteImageProvider extends CancellableImageProvider bool operator ==(Object other) { if (identical(this, other)) return true; if (other is RemoteImageProvider) { - return url == other.url; + return url == other.url && edited == other.edited; } return false; } @override - int get hashCode => url.hashCode; + int get hashCode => url.hashCode ^ edited.hashCode; } class RemoteFullImageProvider extends CancellableImageProvider @@ -148,7 +149,12 @@ class RemoteFullImageProvider extends CancellableImageProvider { timeout, onTimeout: () { state.socket?.off(event, handler); - throw TimeoutException("Timeout waiting for event: $event"); + completer.completeError(TimeoutException("Timeout waiting for event: $event")); }, ); } diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index d8824456b0..35f5a2952f 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -107,23 +107,17 @@ class AssetApiRepository extends ApiRepository { return _api.updateAsset(assetId, UpdateAssetDto(rating: rating)); } - Future editAsset(String assetId, List edits) async { + Future editAsset(String assetId, List edits) { final editDtos = edits - .map((edit) { - if (edit.action == AssetEditAction.other) { - return null; - } - - return AssetEditActionItemDto(action: edit.action.toDto()!, parameters: edit.parameters); - }) - .whereType() + .where((edit) => edit.action != AssetEditAction.other) + .map((edit) => AssetEditActionItemDto(action: edit.action.toDto()!, parameters: edit.parameters)) .toList(); - await _api.editAsset(assetId, AssetEditsCreateDto(edits: editDtos)); + return _api.editAsset(assetId, AssetEditsCreateDto(edits: editDtos)); } Future removeEdits(String assetId) async { - await _api.removeAssetEdits(assetId); + return _api.removeAssetEdits(assetId); } } diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index b43ff9a514..32b67ba980 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1009,7 +1009,6 @@ class DriftEditImageRoute extends PageRouteInfo { DriftEditImageRoute({ Key? key, required Image image, - required BaseAsset asset, required List edits, required ExifInfo exifInfo, required Future Function(List) applyEdits, @@ -1019,7 +1018,6 @@ class DriftEditImageRoute extends PageRouteInfo { args: DriftEditImageRouteArgs( key: key, image: image, - asset: asset, edits: edits, exifInfo: exifInfo, applyEdits: applyEdits, @@ -1036,7 +1034,6 @@ class DriftEditImageRoute extends PageRouteInfo { return DriftEditImagePage( key: args.key, image: args.image, - asset: args.asset, edits: args.edits, exifInfo: args.exifInfo, applyEdits: args.applyEdits, @@ -1049,7 +1046,6 @@ class DriftEditImageRouteArgs { const DriftEditImageRouteArgs({ this.key, required this.image, - required this.asset, required this.edits, required this.exifInfo, required this.applyEdits, @@ -1059,8 +1055,6 @@ class DriftEditImageRouteArgs { final Image image; - final BaseAsset asset; - final List edits; final ExifInfo exifInfo; @@ -1069,7 +1063,7 @@ class DriftEditImageRouteArgs { @override String toString() { - return 'DriftEditImageRouteArgs{key: $key, image: $image, asset: $asset, edits: $edits, exifInfo: $exifInfo, applyEdits: $applyEdits}'; + return 'DriftEditImageRouteArgs{key: $key, image: $image, edits: $edits, exifInfo: $exifInfo, applyEdits: $applyEdits}'; } } diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index bdda1e6c63..1a6333215a 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -253,8 +253,6 @@ class ActionService { } else { await _assetApiRepository.editAsset(remoteId, edits); } - - await _remoteAssetRepository.editAsset(remoteId, edits); } Future _deleteLocalAssets(List localIds) async { diff --git a/mobile/lib/utils/editor.utils.dart b/mobile/lib/utils/editor.utils.dart index 08a2c2d518..edf5079b7f 100644 --- a/mobile/lib/utils/editor.utils.dart +++ b/mobile/lib/utils/editor.utils.dart @@ -1,6 +1,4 @@ -import 'dart:async'; import 'dart:math'; -import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart'; @@ -17,10 +15,10 @@ Rect convertCropParametersToRect(CropParameters parameters, int originalWidth, i } CropParameters convertRectToCropParameters(Rect rect, int originalWidth, int originalHeight) { - final x = (rect.left * originalWidth).round(); - final y = (rect.top * originalHeight).round(); - final width = (rect.width * originalWidth).round(); - final height = (rect.height * originalHeight).round(); + final x = (rect.left * originalWidth).truncate(); + final y = (rect.top * originalHeight).truncate(); + final width = (rect.width * originalWidth).truncate(); + final height = (rect.height * originalHeight).truncate(); return CropParameters( x: max(x, 0).clamp(0, originalWidth), @@ -35,12 +33,22 @@ AffineMatrix buildAffineFromEdits(List edits) { edits.map((edit) { switch (edit.action) { case AssetEditAction.rotate: - final angleInDegrees = edit.parameters["angle"] as num; + final parameters = RotateParameters.fromJson(edit.parameters); + if (parameters == null) { + throw ArgumentError("Unable to parse rotate parameters from edit: ${edit.parameters}"); + } + + final angleInDegrees = parameters.angle; final angleInRadians = angleInDegrees * pi / 180; return AffineMatrix.rotate(angleInRadians); + case AssetEditAction.mirror: - final axis = edit.parameters["axis"] as String; - return axis == "horizontal" ? AffineMatrix.flipY() : AffineMatrix.flipX(); + final parameters = MirrorParameters.fromJson(edit.parameters); + if (parameters == null) { + throw ArgumentError("Unable to parse mirror parameters from edit: ${edit.parameters}"); + } + + return parameters.axis == MirrorAxis.horizontal ? AffineMatrix.flipY() : AffineMatrix.flipX(); default: return AffineMatrix.identity(); } @@ -70,28 +78,3 @@ NormalizedTransform normalizeTransformEdits(List edits) { mirrorVertical: isCloseToZero(a) ? b == c : a == -d, ); } - -/// Helper to resolve an ImageProvider to a ui.Image -Future resolveImage(ImageProvider provider) { - final completer = Completer(); - final stream = provider.resolve(const ImageConfiguration()); - - late final ImageStreamListener listener; - listener = ImageStreamListener( - (ImageInfo info, bool sync) { - if (!completer.isCompleted) { - completer.complete(info.image); - } - stream.removeListener(listener); - }, - onError: (error, stackTrace) { - if (!completer.isCompleted) { - completer.completeError(error, stackTrace); - } - stream.removeListener(listener); - }, - ); - - stream.addListener(listener); - return completer.future; -} diff --git a/mobile/lib/utils/hooks/crop_controller_hook.dart b/mobile/lib/utils/hooks/crop_controller_hook.dart index b5bd536ecb..663bca3dbf 100644 --- a/mobile/lib/utils/hooks/crop_controller_hook.dart +++ b/mobile/lib/utils/hooks/crop_controller_hook.dart @@ -1,14 +1,8 @@ +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:crop_image/crop_image.dart'; import 'dart:ui'; // Import the dart:ui library for Rect -import 'package:crop_image/crop_image.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; - /// A hook that provides a [CropController] instance. -CropController useCropController({Rect? initialCrop, CropRotation? initialRotation}) { - return useMemoized( - () => CropController( - defaultCrop: initialCrop ?? const Rect.fromLTRB(0, 0, 1, 1), - rotation: initialRotation ?? CropRotation.up, - ), - ); +CropController useCropController() { + return useMemoized(() => CropController(defaultCrop: const Rect.fromLTRB(0, 0, 1, 1))); }