From a96b85e269d67b1313d9c73d22557cd693599319 Mon Sep 17 00:00:00 2001 From: Victor Carreras <34163765+vicajilau@users.noreply.github.com> Date: Tue, 2 Dec 2025 08:20:16 +0100 Subject: [PATCH 1/3] feat: Major update to version 2.0.0 with improved exception handling and code organization - Reorganized library structure and updated version in pubspec.yaml - Introduced custom exception CunningDocumentScannerException for better error handling - Enhanced getPictures method to throw specific permission denied exceptions - Added comprehensive unit tests for custom exceptions - Removed deprecated ios_options.dart and separated related functionality into new files --- CHANGELOG.md | 17 +++ example/pubspec.lock | 2 +- lib/cunning_document_scanner.dart | 53 +------ lib/src/cunning_document_scanner.dart | 49 ++++++ lib/src/exceptions.dart | 30 ++++ lib/src/ios_image_format.dart | 8 + .../ios_scanner_options.dart} | 9 +- pubspec.yaml | 4 +- test/cunning_document_scanner_test.dart | 18 ++- test/src/exceptions_test.dart | 140 ++++++++++++++++++ 10 files changed, 267 insertions(+), 63 deletions(-) create mode 100644 lib/src/cunning_document_scanner.dart create mode 100644 lib/src/exceptions.dart create mode 100644 lib/src/ios_image_format.dart rename lib/{ios_options.dart => src/ios_scanner_options.dart} (83%) create mode 100644 test/src/exceptions_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 856cff1..19f7172 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## 2.0.0 +### Breaking Changes +* Reorganized library structure: all implementation files moved to `lib/src/` directory. +* Renamed `ios_options.dart` to `ios_scanner_options.dart` for better clarity. +* Separated `IosImageFormat` enum into its own file (`ios_image_format.dart`). + +### Improvements +* Added custom exception `CunningDocumentScannerException` with specific error codes. +* Replaced generic `Exception` with `CunningDocumentScannerException.permissionDenied()` for better error handling. +* Improved code organization with barrel exports - users only need a single import. +* Added comprehensive unit tests for custom exceptions. +* Enhanced equality operators for `CunningDocumentScannerException`. + +### Migration Guide +* No changes required for users - the public API remains the same with `import 'package:cunning_document_scanner/cunning_document_scanner.dart';` +* If catching exceptions, update catch blocks to use `CunningDocumentScannerException` instead of generic `Exception`. + ## 1.4.0 ### General * Bumped `permission_handler` to `12.0.1`. diff --git a/example/pubspec.lock b/example/pubspec.lock index 2b99f67..0e5ddab 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -47,7 +47,7 @@ packages: path: ".." relative: true source: path - version: "1.3.1" + version: "2.0.0" cupertino_icons: dependency: "direct main" description: diff --git a/lib/cunning_document_scanner.dart b/lib/cunning_document_scanner.dart index 107d7f8..213325d 100644 --- a/lib/cunning_document_scanner.dart +++ b/lib/cunning_document_scanner.dart @@ -1,49 +1,6 @@ -import 'dart:async'; +// Cunning Document Scanner - A simple document scanning library for Flutter. -import 'package:flutter/services.dart'; -import 'package:permission_handler/permission_handler.dart'; - -import 'ios_options.dart'; - -export 'ios_options.dart'; - -/// A class that provides a simple way to scan documents. -class CunningDocumentScanner { - /// The method channel used to interact with the native platform. - static const MethodChannel _channel = - MethodChannel('cunning_document_scanner'); - - /// Starts the document scanning process. - /// - /// This method will open the camera and allow the user to scan documents. - /// - /// [noOfPages] is the maximum number of pages that can be scanned. - /// [isGalleryImportAllowed] is a flag that allows the user to import images from the gallery. - /// [iosScannerOptions] is a set of options for the iOS scanner. - /// - /// Returns a list of paths to the scanned images, or null if the user cancels the operation. - static Future?> getPictures({ - int noOfPages = 100, - bool isGalleryImportAllowed = false, - IosScannerOptions? iosScannerOptions, - }) async { - Map statuses = await [ - Permission.camera, - ].request(); - if (statuses.containsValue(PermissionStatus.denied) || - statuses.containsValue(PermissionStatus.permanentlyDenied)) { - throw Exception("Permission not granted"); - } - - final List? pictures = await _channel.invokeMethod('getPictures', { - 'noOfPages': noOfPages, - 'isGalleryImportAllowed': isGalleryImportAllowed, - if (iosScannerOptions != null) - 'iosScannerOptions': { - 'imageFormat': iosScannerOptions.imageFormat.name, - 'jpgCompressionQuality': iosScannerOptions.jpgCompressionQuality, - } - }); - return pictures?.map((e) => e as String).toList(); - } -} +export 'src/cunning_document_scanner.dart'; +export 'src/exceptions.dart'; +export 'src/ios_image_format.dart'; +export 'src/ios_scanner_options.dart'; diff --git a/lib/src/cunning_document_scanner.dart b/lib/src/cunning_document_scanner.dart new file mode 100644 index 0000000..31ea74b --- /dev/null +++ b/lib/src/cunning_document_scanner.dart @@ -0,0 +1,49 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import 'exceptions.dart'; +import 'ios_scanner_options.dart'; + +/// A class that provides a simple way to scan documents. +class CunningDocumentScanner { + /// The method channel used to interact with the native platform. + static const MethodChannel _channel = + MethodChannel('cunning_document_scanner'); + + /// Starts the document scanning process. + /// + /// This method will open the camera and allow the user to scan documents. + /// + /// [noOfPages] is the maximum number of pages that can be scanned. + /// [isGalleryImportAllowed] is a flag that allows the user to import images from the gallery. + /// [iosScannerOptions] is a set of options for the iOS scanner. + /// + /// Returns a list of paths to the scanned images, or null if the user cancels the operation. + static Future?> getPictures({ + int noOfPages = 100, + bool isGalleryImportAllowed = false, + IosScannerOptions? iosScannerOptions, + }) async { + Map statuses = await [ + Permission.camera, + ].request(); + if (statuses.containsValue(PermissionStatus.denied) || + statuses.containsValue(PermissionStatus.permanentlyDenied)) { + throw const CunningDocumentScannerException.permissionDenied( + 'Camera permission not granted'); + } + + final List? pictures = await _channel.invokeMethod('getPictures', { + 'noOfPages': noOfPages, + 'isGalleryImportAllowed': isGalleryImportAllowed, + if (iosScannerOptions != null) + 'iosScannerOptions': { + 'imageFormat': iosScannerOptions.imageFormat.name, + 'jpgCompressionQuality': iosScannerOptions.jpgCompressionQuality, + } + }); + return pictures?.map((e) => e as String).toList(); + } +} diff --git a/lib/src/exceptions.dart b/lib/src/exceptions.dart new file mode 100644 index 0000000..b8a7e15 --- /dev/null +++ b/lib/src/exceptions.dart @@ -0,0 +1,30 @@ +/// Custom exceptions thrown by `CunningDocumentScanner`. +class CunningDocumentScannerException implements Exception { + /// A short message describing the error. + final String message; + + /// Optional code to categorize errors (e.g. 'permission_denied'). + final String? code; + + const CunningDocumentScannerException(this.message, {this.code}); + + /// Named constructor for permission errors. + const CunningDocumentScannerException.permissionDenied([ + String message = 'Permission not granted', + ]) : this(message, code: 'permission_denied'); + + @override + String toString() => + 'CunningDocumentScannerException(${code ?? 'error'}): $message'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CunningDocumentScannerException && + runtimeType == other.runtimeType && + message == other.message && + code == other.code; + + @override + int get hashCode => message.hashCode ^ code.hashCode; +} diff --git a/lib/src/ios_image_format.dart b/lib/src/ios_image_format.dart new file mode 100644 index 0000000..6d97d2d --- /dev/null +++ b/lib/src/ios_image_format.dart @@ -0,0 +1,8 @@ +/// Enumerates the different output image formats are supported. +enum IosImageFormat { + /// Indicates the output image should be formatted as JPEG image. + jpg, + + /// Indicates the output image should be formatted as PNG image. + png, +} diff --git a/lib/ios_options.dart b/lib/src/ios_scanner_options.dart similarity index 83% rename from lib/ios_options.dart rename to lib/src/ios_scanner_options.dart index 6d4d74f..ecce6fe 100644 --- a/lib/ios_options.dart +++ b/lib/src/ios_scanner_options.dart @@ -1,11 +1,4 @@ -/// Enumerates the different output image formats are supported. -enum IosImageFormat { - /// Indicates the output image should be formatted as JPEG image. - jpg, - - /// Indicates the output image should be formatted as PNG image. - png, -} +import 'ios_image_format.dart'; /// Different options that modify the behavior of the document scanner on iOS. /// diff --git a/pubspec.yaml b/pubspec.yaml index 60081dd..2263c84 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: cunning_document_scanner description: A document scanner plugin for flutter. Scan and crop automatically on iOS and Android. -version: 1.4.0 +version: 2.0.0 homepage: https://cunning.biz repository: https://github.com/jachzen/cunning_document_scanner @@ -19,7 +19,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ">=3.0.1 <7.0.0" - mockito: ^5.4.5 + mockito: ^5.6.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/cunning_document_scanner_test.dart b/test/cunning_document_scanner_test.dart index c00e874..9910977 100644 --- a/test/cunning_document_scanner_test.dart +++ b/test/cunning_document_scanner_test.dart @@ -13,8 +13,13 @@ void main() { }); test('getPictures with denied permission', () async { - expect(() async => await CunningDocumentScanner.getPictures(), - throwsA(isA())); + expect( + () async => await CunningDocumentScanner.getPictures(), + throwsA(isA() + .having((e) => e.code, 'code', 'permission_denied') + .having( + (e) => e.message, 'message', 'Camera permission not granted')), + ); }); }); @@ -25,8 +30,13 @@ void main() { }); test('getPictures with permanently denied permission', () async { - expect(() async => await CunningDocumentScanner.getPictures(), - throwsA(isA())); + expect( + () async => await CunningDocumentScanner.getPictures(), + throwsA(isA() + .having((e) => e.code, 'code', 'permission_denied') + .having( + (e) => e.message, 'message', 'Camera permission not granted')), + ); }); }); diff --git a/test/src/exceptions_test.dart b/test/src/exceptions_test.dart new file mode 100644 index 0000000..f5cb446 --- /dev/null +++ b/test/src/exceptions_test.dart @@ -0,0 +1,140 @@ +import 'package:cunning_document_scanner/cunning_document_scanner.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('CunningDocumentScannerException', () { + test('creates exception with message and code', () { + const exception = CunningDocumentScannerException( + 'Test error', + code: 'test_code', + ); + + expect(exception.message, 'Test error'); + expect(exception.code, 'test_code'); + }); + + test('creates exception with message only', () { + const exception = CunningDocumentScannerException('Test error'); + + expect(exception.message, 'Test error'); + expect(exception.code, isNull); + }); + + test('toString returns formatted error message with code', () { + const exception = CunningDocumentScannerException( + 'Test error', + code: 'test_code', + ); + + expect( + exception.toString(), + 'CunningDocumentScannerException(test_code): Test error', + ); + }); + + test('toString returns formatted error message without code', () { + const exception = CunningDocumentScannerException('Test error'); + + expect( + exception.toString(), + 'CunningDocumentScannerException(error): Test error', + ); + }); + + group('permissionDenied constructor', () { + test('creates exception with default message', () { + const exception = CunningDocumentScannerException.permissionDenied(); + + expect(exception.message, 'Permission not granted'); + expect(exception.code, 'permission_denied'); + }); + + test('creates exception with custom message', () { + const exception = CunningDocumentScannerException.permissionDenied( + 'Camera permission denied', + ); + + expect(exception.message, 'Camera permission denied'); + expect(exception.code, 'permission_denied'); + }); + + test('toString returns formatted permission error', () { + const exception = CunningDocumentScannerException.permissionDenied( + 'Camera permission denied', + ); + + expect( + exception.toString(), + 'CunningDocumentScannerException(permission_denied): Camera permission denied', + ); + }); + }); + + group('equality', () { + test('two exceptions with same message and code are equal', () { + const exception1 = CunningDocumentScannerException( + 'Test error', + code: 'test_code', + ); + const exception2 = CunningDocumentScannerException( + 'Test error', + code: 'test_code', + ); + + expect(exception1, equals(exception2)); + expect(exception1.hashCode, equals(exception2.hashCode)); + }); + + test('two exceptions with different messages are not equal', () { + const exception1 = CunningDocumentScannerException( + 'Test error 1', + code: 'test_code', + ); + const exception2 = CunningDocumentScannerException( + 'Test error 2', + code: 'test_code', + ); + + expect(exception1, isNot(equals(exception2))); + }); + + test('two exceptions with different codes are not equal', () { + const exception1 = CunningDocumentScannerException( + 'Test error', + code: 'code1', + ); + const exception2 = CunningDocumentScannerException( + 'Test error', + code: 'code2', + ); + + expect(exception1, isNot(equals(exception2))); + }); + + test('exception equals itself', () { + const exception = CunningDocumentScannerException( + 'Test error', + code: 'test_code', + ); + + expect(exception, equals(exception)); + }); + }); + + group('implements Exception', () { + test('can be caught as Exception', () { + expect( + () => throw const CunningDocumentScannerException('Test error'), + throwsA(isA()), + ); + }); + + test('can be caught as CunningDocumentScannerException', () { + expect( + () => throw const CunningDocumentScannerException('Test error'), + throwsA(isA()), + ); + }); + }); + }); +} From 04bcda0904c2f3f63bef16dbd5aa3617af1406c0 Mon Sep 17 00:00:00 2001 From: Victor Carreras <34163765+vicajilau@users.noreply.github.com> Date: Tue, 2 Dec 2025 08:45:00 +0100 Subject: [PATCH 2/3] feat: Add launch configuration for Dart in VSCode --- .vscode/launch.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c3be203 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "example", + "cwd": "example", + "request": "launch", + "type": "dart" + } + ] +} \ No newline at end of file From e84a4caad1487d0dbb9c3ee758c2620a9436caac Mon Sep 17 00:00:00 2001 From: Victor Carreras <34163765+vicajilau@users.noreply.github.com> Date: Tue, 2 Dec 2025 08:49:00 +0100 Subject: [PATCH 3/3] refactor: Improve code organization and formatting in main.dart --- example/lib/main.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index da90eec..be717f8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,8 @@ -import 'dart:io'; -import 'package:flutter/material.dart'; import 'dart:async'; +import 'dart:io'; + import 'package:cunning_document_scanner/cunning_document_scanner.dart'; +import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); @@ -49,10 +50,11 @@ class _MyAppState extends State { List pictures; try { pictures = await CunningDocumentScanner.getPictures( + isGalleryImportAllowed: true, iosScannerOptions: IosScannerOptions( - imageFormat: IosImageFormat.jpg, - jpgCompressionQuality: 0.5, - )) ?? + imageFormat: IosImageFormat.jpg, + jpgCompressionQuality: 0.5, + )) ?? []; if (!mounted) return; setState(() {