Skip to content

Commit

Permalink
breaking change - ability to get errors while cropping an image (#145)
Browse files Browse the repository at this point in the history
* bump `image` dependency

* breaking change - ability to get errors while cropping an image

* ++ example

* ++ test
  • Loading branch information
justprodev authored Dec 11, 2024
1 parent 20c3dd8 commit 40c1961
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 45 deletions.
22 changes: 17 additions & 5 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,23 @@ class _CropSampleState extends State<CropSample> {
willUpdateScale: (newScale) => newScale < 5,
controller: _cropController,
image: _imageDataList[_currentImage],
onCropped: (croppedData) {
setState(() {
_croppedData = croppedData;
_isCropping = false;
});
onCropped: (result) {
switch (result) {
case CropSuccess():
_croppedData = result.croppedImage;
case CropFailure():
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Error'),
content: Text('Failed to crop image: ${result.error}'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('OK')),
],
),
);
}
setState(() => _isCropping = false);
},
withCircleUi: _isCircleUi,
onStatusChanged: (status) => setState(() {
Expand Down
30 changes: 18 additions & 12 deletions lib/src/widget/crop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class Crop extends StatelessWidget {
final Uint8List image;

/// callback when cropping completed
final ValueChanged<Uint8List> onCropped;
final ValueChanged<CropResult> onCropped;

/// fixed aspect ratio of cropping rect.
/// null, by default, means no fixed aspect ratio.
Expand Down Expand Up @@ -221,7 +221,7 @@ class Crop extends StatelessWidget {

class _CropEditor extends StatefulWidget {
final Uint8List image;
final ValueChanged<Uint8List> onCropped;
final ValueChanged<CropResult> onCropped;
final double? aspectRatio;
final double? initialSize;
final CroppingRectBuilder? initialRectBuilder;
Expand Down Expand Up @@ -480,16 +480,22 @@ class _CropEditorState extends State<_CropEditor> {
widget.onStatusChanged?.call(CropStatus.cropping);

// use compute() not to block UI update
final cropResult = await compute(
_cropFunc,
[
widget.imageCropper,
_parsedImageDetail!.image,
_readyState.rectToCrop,
withCircleShape,
_detectedFormat,
],
);
late CropResult cropResult;
try {
final image = await compute(
_cropFunc,
[
widget.imageCropper,
_parsedImageDetail!.image,
_readyState.rectToCrop,
withCircleShape,
_detectedFormat,
],
);
cropResult = CropSuccess(image);
} catch(e, trace) {
cropResult = CropFailure(e, trace);
}

widget.onCropped(cropResult);
widget.onStatusChanged?.call(CropStatus.ready);
Expand Down
18 changes: 18 additions & 0 deletions lib/src/widget/crop_result.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Created by [email protected] on 04.03.2024.

import 'dart:typed_data';

sealed class CropResult {
const CropResult();
}

class CropSuccess extends CropResult {
const CropSuccess(this.croppedImage);
final Uint8List croppedImage;
}

class CropFailure extends CropResult {
const CropFailure(this.error, [this.stackTrace]);
final Object error;
final StackTrace? stackTrace;
}
1 change: 1 addition & 0 deletions lib/src/widget/widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export 'crop.dart';
export 'controller.dart';
export 'dot_control.dart';
export 'edge_alignment.dart';
export 'crop_result.dart';
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ dependencies:
flutter:
sdk: flutter

image: ^4.2.0
image: ^4.3.0

dev_dependencies:
flutter_test:
Expand Down
90 changes: 63 additions & 27 deletions test/widget/crop_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,34 +31,70 @@ void main() {
},
);

testWidgets(
'onCropped is called after calling CropController.crop()',
(tester) async {
// to ensure callback is called
final completer = Completer<void>();

final controller = CropController();
final widget = withMaterial(
Crop(
image: testImage,
onCropped: (value) => completer.complete(),
controller: controller,
),
);

await tester.runAsync(() async {
await tester.pumpWidget(widget);
// wait for parsing image
await Future.delayed(const Duration(seconds: 2));

controller.crop();
// wait for cropping image
await Future.delayed(const Duration(seconds: 2));
});
group('onCropped', () {
testWidgets(
'onCropped is called after calling CropController.crop()',
(tester) async {
// to ensure callback is called
final completer = Completer<void>();

final controller = CropController();
final widget = withMaterial(
Crop(
image: testImage,
onCropped: (value) => completer.complete(),
controller: controller,
),
);

await tester.runAsync(() async {
await tester.pumpWidget(widget);
// wait for parsing image
await Future.delayed(const Duration(seconds: 2));

controller.crop();
// wait for cropping image
await Future.delayed(const Duration(seconds: 2));
});

expect(completer.isCompleted, isTrue);
},
);

testWidgets(
'onCropped returns an error if cropping fails',
(tester) async {
// to ensure callback is called
bool hasError = false;

final controller = CropController();
final widget = withMaterial(
Crop(
image: testImage,
onCropped: (value) {
hasError = value is CropFailure;
},
controller: controller,
imageCropper: FailureCropper(),
),
);

await tester.runAsync(() async {
await tester.pumpWidget(widget);
// wait for parsing image
await Future.delayed(const Duration(seconds: 2));

controller.crop();

// wait for cropping image
await Future.delayed(const Duration(seconds: 2));
});

expect(hasError, isTrue);
},
);
});

expect(completer.isCompleted, isTrue);
},
);

testWidgets(
'status is changing with null -> .ready -> .cropping -> .ready',
Expand Down
21 changes: 21 additions & 0 deletions test/widget/helper.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import 'dart:async';

import 'dart:typed_data';

import 'package:crop_your_image/src/logic/cropper/image_cropper.dart';
import 'package:crop_your_image/src/logic/format_detector/format.dart';
import 'package:crop_your_image/src/logic/shape.dart';
import 'package:flutter/material.dart';

Widget withMaterial(Widget widget) {
Expand All @@ -9,3 +16,17 @@ Widget withMaterial(Widget widget) {
),
);
}

/// [ImageCropper] that always fails
class FailureCropper extends ImageCropper {
@override
Future<Uint8List> call({
required dynamic original,
required Offset topLeft,
required Offset bottomRight,
ImageFormat outputFormat = ImageFormat.jpeg,
ImageShape shape = ImageShape.rectangle,
}) async {
throw Error();
}
}

0 comments on commit 40c1961

Please sign in to comment.