Merge branch 'master' into pause_function
# Conflicts: # ios/Classes/MobileScanner.swift # macos/mobile_scanner/Sources/mobile_scanner/MobileScannerPlugin.swift
Showing
69 changed files
with
1223 additions
and
538 deletions
| @@ -7,7 +7,7 @@ | @@ -7,7 +7,7 @@ | ||
| 7 | release-please: | 7 | release-please: |
| 8 | runs-on: ubuntu-latest | 8 | runs-on: ubuntu-latest |
| 9 | steps: | 9 | steps: |
| 10 | - - uses: GoogleCloudPlatform/release-please-action@v4.1.0 | 10 | + - uses: GoogleCloudPlatform/release-please-action@v4.1.3 |
| 11 | with: | 11 | with: |
| 12 | token: ${{ secrets.GITHUB_TOKEN }} | 12 | token: ${{ secrets.GITHUB_TOKEN }} |
| 13 | release-type: simple | 13 | release-type: simple |
| 1 | -## NEXT | ||
| 2 | -* This release requires Flutter 3.22.0 and Dart 3.4. | 1 | +## 6.0.2 |
| 2 | + | ||
| 3 | +Bugs fixed: | ||
| 4 | +* Fixed a bug that prevented `analyzeImage` from actually accepting the configured formats. | ||
| 5 | + | ||
| 6 | +Improvements: | ||
| 7 | +* [iOS] Excluded the `arm64` architecture for Simulators, which is unsupported by MLKit 7.0.0. | ||
| 8 | + | ||
| 9 | +## 6.0.1 | ||
| 10 | + | ||
| 11 | +Bugs fixed: | ||
| 12 | +* Fixed a bug that would cause onDetect to not handle errors. | ||
| 13 | + | ||
| 14 | +Improvements: | ||
| 15 | +* [iOS] Excluded the `armv7` architecture, which is unsupported by MLKit 7.0.0. | ||
| 16 | +* Added a new `onDetectError` error handler to the `MobileScanner` widget, for use with `onDetect`. | ||
| 17 | + | ||
| 18 | +## 6.0.0 | ||
| 19 | + | ||
| 20 | +**BREAKING CHANGES:** | ||
| 21 | + | ||
| 22 | +* [iOS] iOS 15.5.0 is now the minimum supported iOS version. | ||
| 23 | +* [iOS] Updates MLKit to version 7.0.0. | ||
| 24 | +* [iOS] Updates the minimum supported XCode version to 15.3.0. | ||
| 25 | + | ||
| 26 | +Improvements: | ||
| 27 | +* [MacOS] Added the corners and size information to barcode results. | ||
| 28 | +* [MacOS] Added support for `analyzeImage`. | ||
| 29 | +* [MacOS] Added a Privacy Manifest. | ||
| 30 | +* [web] Added the size information to barcode results. | ||
| 31 | +* [web] Added the video output size information to barcode capture. | ||
| 32 | +* Added support for barcode formats to image analysis. | ||
| 33 | +* Updated the scanner to report any scanning errors that were encountered during processing. | ||
| 34 | +* Introduced a new getter `hasCameraPermission` for the `MobileScannerState`. | ||
| 35 | +* Fixed a bug in the lifecycle handling sample. Now instead of checking `isInitialized`, | ||
| 36 | +the sample recommends using `hasCameraPermission`, which also guards against camera permission errors. | ||
| 37 | +* Updated the behavior of `returnImage` to only determine if the camera output bytes should be sent. | ||
| 38 | +* Updated the behavior of `BarcodeCapture.size` to always be provided when available, regardless of `returnImage`. | ||
| 39 | + | ||
| 40 | +Bugs fixed: | ||
| 41 | +* Fixed a bug that would cause the scanner to emit an error when it was already started. Now it ignores any calls to start while it is starting. | ||
| 42 | +* [MacOS] Fixed a bug that prevented the `anaylzeImage()` sample from working properly. | ||
| 43 | + | ||
| 44 | +## 5.2.3 | ||
| 45 | + | ||
| 46 | +Deprecations: | ||
| 47 | +* The `EncryptionType.none` constant has been deprecated, as its name was misleading. Use `EncryptionType.unknown` instead. | ||
| 48 | + | ||
| 49 | +Bugs fixed: | ||
| 50 | +* Fixed `EncryptionType` throwing on invalid `SAE` encryption type. | ||
| 51 | +* [web] Removed the `controls` attribute on the video preview. | ||
| 52 | + | ||
| 53 | +Improvements: | ||
| 54 | +* All enum types for barcode data (i.e. Wifi type or email type) now return `unknown` for unrecognized values. | ||
| 55 | + | ||
| 56 | +## 5.2.2 | ||
| 57 | + | ||
| 58 | +Improvements: | ||
| 59 | +* [MacOS] Adds Swift Package Manager support. | ||
| 60 | +* [MacOS] Adds support for `returnImage`. | ||
| 61 | +* Added a new `size` property to `Barcode`, that denotes the bounding box of the barcode. | ||
| 62 | + | ||
| 63 | +Bugs fixed: | ||
| 64 | +* Fixed some documentation errors for the `size` and `image` of `BarcodeCapture`. | ||
| 65 | +* [iOS] Fixed a bug with `returnImage`. | ||
| 66 | +* [Android/iOS] Adjusted the raw barcode scan value to pass the raw event data, like on MacOS. | ||
| 67 | + | ||
| 68 | +## 5.2.1 | ||
| 69 | + | ||
| 70 | +* Updates the `package:web` dependency to use a version range. | ||
| 71 | + | ||
| 72 | +## 5.2.0 | ||
| 73 | + | ||
| 74 | +This release requires Flutter 3.22.0 and Dart 3.4. | ||
| 3 | 75 | ||
| 4 | * [Android] Fixed a leak of the barcode scanner. | 76 | * [Android] Fixed a leak of the barcode scanner. |
| 5 | * [Android] Fixed a crash when encountering invalid numbers for the scan window. | 77 | * [Android] Fixed a crash when encountering invalid numbers for the scan window. |
| @@ -39,7 +111,7 @@ Improvements: | @@ -39,7 +111,7 @@ Improvements: | ||
| 39 | This major release contains all the changes from the 5.0.0 beta releases, along with the following changes: | 111 | This major release contains all the changes from the 5.0.0 beta releases, along with the following changes: |
| 40 | 112 | ||
| 41 | Improvements: | 113 | Improvements: |
| 42 | -- [Android] Remove the Kotlin Standard Library from the dependencies, as it is automatically included in Kotlin 1.4+ | 114 | +* [Android] Remove the Kotlin Standard Library from the dependencies, as it is automatically included in Kotlin 1.4+ |
| 43 | 115 | ||
| 44 | ## 5.0.0-beta.3 | 116 | ## 5.0.0-beta.3 |
| 45 | **BREAKING CHANGES:** | 117 | **BREAKING CHANGES:** |
| @@ -35,8 +35,8 @@ See the example app for detailed implementation information. | @@ -35,8 +35,8 @@ See the example app for detailed implementation information. | ||
| 35 | 35 | ||
| 36 | | Features | Android | iOS | macOS | Web | | 36 | | Features | Android | iOS | macOS | Web | |
| 37 | |------------------------|--------------------|--------------------|----------------------|-----| | 37 | |------------------------|--------------------|--------------------|----------------------|-----| |
| 38 | -| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | | ||
| 39 | -| returnImage | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | | 38 | +| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | |
| 39 | +| returnImage | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | | ||
| 40 | | scanWindow | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | | 40 | | scanWindow | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | |
| 41 | 41 | ||
| 42 | ## Platform Support | 42 | ## Platform Support |
| @@ -60,6 +60,10 @@ dev.steenbakker.mobile_scanner.useUnbundled=true | @@ -60,6 +60,10 @@ dev.steenbakker.mobile_scanner.useUnbundled=true | ||
| 60 | ``` | 60 | ``` |
| 61 | 61 | ||
| 62 | ### iOS | 62 | ### iOS |
| 63 | + | ||
| 64 | +_iOS arm64 Simulators are currently not yet supported, until the migration to the Vision API is complete._ | ||
| 65 | +_See_ https://github.com/juliansteenbakker/mobile_scanner/issues/1225 | ||
| 66 | + | ||
| 63 | **Add the following keys to your Info.plist file, located in <project root>/ios/Runner/Info.plist:** | 67 | **Add the following keys to your Info.plist file, located in <project root>/ios/Runner/Info.plist:** |
| 64 | NSCameraUsageDescription - describe why your app needs access to the camera. This is called Privacy - Camera Usage Description in the visual editor. | 68 | NSCameraUsageDescription - describe why your app needs access to the camera. This is called Privacy - Camera Usage Description in the visual editor. |
| 65 | 69 | ||
| @@ -83,8 +87,8 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities: | @@ -83,8 +87,8 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities: | ||
| 83 | 87 | ||
| 84 | ## Web | 88 | ## Web |
| 85 | 89 | ||
| 86 | -As of version 5.0.0 adding the library to the `index.html` is no longer required, | ||
| 87 | -as the library is automatically loaded on first use. | 90 | +As of version 5.0.0 adding the barcode scanning library script to the `index.html` is no longer required, |
| 91 | +as the script is automatically loaded on first use. | ||
| 88 | 92 | ||
| 89 | ### Providing a mirror for the barcode scanning library | 93 | ### Providing a mirror for the barcode scanning library |
| 90 | 94 | ||
| @@ -127,7 +131,7 @@ class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver { | @@ -127,7 +131,7 @@ class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver { | ||
| 127 | void didChangeAppLifecycleState(AppLifecycleState state) { | 131 | void didChangeAppLifecycleState(AppLifecycleState state) { |
| 128 | // If the controller is not ready, do not try to start or stop it. | 132 | // If the controller is not ready, do not try to start or stop it. |
| 129 | // Permission dialogs can trigger lifecycle changes before the controller is ready. | 133 | // Permission dialogs can trigger lifecycle changes before the controller is ready. |
| 130 | - if (!controller.value.isInitialized) { | 134 | + if (!controller.value.hasCameraPermission) { |
| 131 | return; | 135 | return; |
| 132 | } | 136 | } |
| 133 | 137 | ||
| @@ -192,4 +196,4 @@ Future<void> dispose() async { | @@ -192,4 +196,4 @@ Future<void> dispose() async { | ||
| 192 | To display the camera preview, pass the controller to a `MobileScanner` widget. | 196 | To display the camera preview, pass the controller to a `MobileScanner` widget. |
| 193 | 197 | ||
| 194 | See the [examples](example/README.md) for runnable examples of various usages, | 198 | See the [examples](example/README.md) for runnable examples of various usages, |
| 195 | -such as the basic usage, applying a scan window, or retrieving images from the barcodes. | 199 | +such as the basic usage, applying a scan window, or retrieving images from the barcodes. |
| @@ -67,7 +67,7 @@ dependencies { | @@ -67,7 +67,7 @@ dependencies { | ||
| 67 | def useUnbundled = project.findProperty('dev.steenbakker.mobile_scanner.useUnbundled') ?: false | 67 | def useUnbundled = project.findProperty('dev.steenbakker.mobile_scanner.useUnbundled') ?: false |
| 68 | if (useUnbundled.toBoolean()) { | 68 | if (useUnbundled.toBoolean()) { |
| 69 | // Dynamically downloaded model via Google Play Services | 69 | // Dynamically downloaded model via Google Play Services |
| 70 | - implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.0' | 70 | + implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1' |
| 71 | } else { | 71 | } else { |
| 72 | // Bundled model in app | 72 | // Bundled model in app |
| 73 | implementation 'com.google.mlkit:barcode-scanning:17.2.0' | 73 | implementation 'com.google.mlkit:barcode-scanning:17.2.0' |
| @@ -77,8 +77,8 @@ dependencies { | @@ -77,8 +77,8 @@ dependencies { | ||
| 77 | // See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7 | 77 | // See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7 |
| 78 | implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22")) | 78 | implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22")) |
| 79 | 79 | ||
| 80 | - implementation 'androidx.camera:camera-lifecycle:1.3.3' | ||
| 81 | - implementation 'androidx.camera:camera-camera2:1.3.3' | 80 | + implementation 'androidx.camera:camera-lifecycle:1.3.4' |
| 81 | + implementation 'androidx.camera:camera-camera2:1.3.4' | ||
| 82 | 82 | ||
| 83 | testImplementation 'org.jetbrains.kotlin:kotlin-test' | 83 | testImplementation 'org.jetbrains.kotlin:kotlin-test' |
| 84 | testImplementation 'org.mockito:mockito-core:5.12.0' | 84 | testImplementation 'org.mockito:mockito-core:5.12.0' |
| @@ -18,6 +18,12 @@ class BarcodeHandler(binaryMessenger: BinaryMessenger) : EventChannel.StreamHand | @@ -18,6 +18,12 @@ class BarcodeHandler(binaryMessenger: BinaryMessenger) : EventChannel.StreamHand | ||
| 18 | eventChannel.setStreamHandler(this) | 18 | eventChannel.setStreamHandler(this) |
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | + fun publishError(errorCode: String, errorMessage: String, errorDetails: Any?) { | ||
| 22 | + Handler(Looper.getMainLooper()).post { | ||
| 23 | + eventSink?.error(errorCode, errorMessage, errorDetails) | ||
| 24 | + } | ||
| 25 | + } | ||
| 26 | + | ||
| 21 | fun publishEvent(event: Map<String, Any>) { | 27 | fun publishEvent(event: Map<String, Any>) { |
| 22 | Handler(Looper.getMainLooper()).post { | 28 | Handler(Looper.getMainLooper()).post { |
| 23 | eventSink?.success(event) | 29 | eventSink?.success(event) |
| @@ -123,9 +123,8 @@ class MobileScanner( | @@ -123,9 +123,8 @@ class MobileScanner( | ||
| 123 | mobileScannerCallback( | 123 | mobileScannerCallback( |
| 124 | barcodeMap, | 124 | barcodeMap, |
| 125 | null, | 125 | null, |
| 126 | - null, | ||
| 127 | - null | ||
| 128 | - ) | 126 | + mediaImage.width, |
| 127 | + mediaImage.height) | ||
| 129 | return@addOnSuccessListener | 128 | return@addOnSuccessListener |
| 130 | } | 129 | } |
| 131 | 130 |
| @@ -10,6 +10,7 @@ import androidx.camera.core.ExperimentalGetImage | @@ -10,6 +10,7 @@ import androidx.camera.core.ExperimentalGetImage | ||
| 10 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions | 10 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions |
| 11 | import dev.steenbakker.mobile_scanner.objects.BarcodeFormats | 11 | import dev.steenbakker.mobile_scanner.objects.BarcodeFormats |
| 12 | import dev.steenbakker.mobile_scanner.objects.DetectionSpeed | 12 | import dev.steenbakker.mobile_scanner.objects.DetectionSpeed |
| 13 | +import dev.steenbakker.mobile_scanner.objects.MobileScannerErrorCodes | ||
| 13 | import io.flutter.plugin.common.BinaryMessenger | 14 | import io.flutter.plugin.common.BinaryMessenger |
| 14 | import io.flutter.plugin.common.MethodCall | 15 | import io.flutter.plugin.common.MethodCall |
| 15 | import io.flutter.plugin.common.MethodChannel | 16 | import io.flutter.plugin.common.MethodChannel |
| @@ -28,7 +29,7 @@ class MobileScannerHandler( | @@ -28,7 +29,7 @@ class MobileScannerHandler( | ||
| 28 | 29 | ||
| 29 | private val analyzeImageErrorCallback: AnalyzerErrorCallback = { | 30 | private val analyzeImageErrorCallback: AnalyzerErrorCallback = { |
| 30 | Handler(Looper.getMainLooper()).post { | 31 | Handler(Looper.getMainLooper()).post { |
| 31 | - analyzerResult?.error("MobileScanner", it, null) | 32 | + analyzerResult?.error(MobileScannerErrorCodes.BARCODE_ERROR, it, null) |
| 32 | analyzerResult = null | 33 | analyzerResult = null |
| 33 | } | 34 | } |
| 34 | } | 35 | } |
| @@ -46,27 +47,21 @@ class MobileScannerHandler( | @@ -46,27 +47,21 @@ class MobileScannerHandler( | ||
| 46 | private var analyzerResult: MethodChannel.Result? = null | 47 | private var analyzerResult: MethodChannel.Result? = null |
| 47 | 48 | ||
| 48 | private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int? -> | 49 | private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int? -> |
| 49 | - if (image != null) { | ||
| 50 | - barcodeHandler.publishEvent(mapOf( | ||
| 51 | - "name" to "barcode", | ||
| 52 | - "data" to barcodes, | ||
| 53 | - "image" to image, | ||
| 54 | - "width" to width!!.toDouble(), | ||
| 55 | - "height" to height!!.toDouble() | ||
| 56 | - )) | ||
| 57 | - } else { | ||
| 58 | - barcodeHandler.publishEvent(mapOf( | ||
| 59 | - "name" to "barcode", | ||
| 60 | - "data" to barcodes | ||
| 61 | - )) | ||
| 62 | - } | 50 | + barcodeHandler.publishEvent(mapOf( |
| 51 | + "name" to "barcode", | ||
| 52 | + "data" to barcodes, | ||
| 53 | + // The image dimensions are always provided. | ||
| 54 | + // The image bytes are only non-null when `returnImage` is true. | ||
| 55 | + "image" to mapOf( | ||
| 56 | + "bytes" to image, | ||
| 57 | + "width" to width?.toDouble(), | ||
| 58 | + "height" to height?.toDouble(), | ||
| 59 | + ) | ||
| 60 | + )) | ||
| 63 | } | 61 | } |
| 64 | 62 | ||
| 65 | private val errorCallback: MobileScannerErrorCallback = {error: String -> | 63 | private val errorCallback: MobileScannerErrorCallback = {error: String -> |
| 66 | - barcodeHandler.publishEvent(mapOf( | ||
| 67 | - "name" to "error", | ||
| 68 | - "data" to error, | ||
| 69 | - )) | 64 | + barcodeHandler.publishError(MobileScannerErrorCodes.BARCODE_ERROR, error, null) |
| 70 | } | 65 | } |
| 71 | 66 | ||
| 72 | private var methodChannel: MethodChannel? = null | 67 | private var methodChannel: MethodChannel? = null |
| @@ -104,21 +99,21 @@ class MobileScannerHandler( | @@ -104,21 +99,21 @@ class MobileScannerHandler( | ||
| 104 | 99 | ||
| 105 | @ExperimentalGetImage | 100 | @ExperimentalGetImage |
| 106 | override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { | 101 | override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { |
| 107 | - if (mobileScanner == null) { | ||
| 108 | - result.error("MobileScanner", "Called ${call.method} before initializing.", null) | ||
| 109 | - return | ||
| 110 | - } | ||
| 111 | when (call.method) { | 102 | when (call.method) { |
| 112 | "state" -> result.success(permissions.hasCameraPermission(activity)) | 103 | "state" -> result.success(permissions.hasCameraPermission(activity)) |
| 113 | "request" -> permissions.requestPermission( | 104 | "request" -> permissions.requestPermission( |
| 114 | activity, | 105 | activity, |
| 115 | addPermissionListener, | 106 | addPermissionListener, |
| 116 | object: MobileScannerPermissions.ResultCallback { | 107 | object: MobileScannerPermissions.ResultCallback { |
| 117 | - override fun onResult(errorCode: String?, errorDescription: String?) { | 108 | + override fun onResult(errorCode: String?) { |
| 118 | when(errorCode) { | 109 | when(errorCode) { |
| 119 | null -> result.success(true) | 110 | null -> result.success(true) |
| 120 | - MobileScannerPermissions.CAMERA_ACCESS_DENIED -> result.success(false) | ||
| 121 | - else -> result.error(errorCode, errorDescription, null) | 111 | + MobileScannerErrorCodes.CAMERA_ACCESS_DENIED -> result.success(false) |
| 112 | + MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING -> result.error( | ||
| 113 | + MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING, | ||
| 114 | + MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE, null) | ||
| 115 | + else -> result.error( | ||
| 116 | + MobileScannerErrorCodes.GENERIC_ERROR, MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE, null) | ||
| 122 | } | 117 | } |
| 123 | } | 118 | } |
| 124 | }) | 119 | }) |
| @@ -150,28 +145,16 @@ class MobileScannerHandler( | @@ -150,28 +145,16 @@ class MobileScannerHandler( | ||
| 150 | null | 145 | null |
| 151 | } | 146 | } |
| 152 | 147 | ||
| 153 | - var barcodeScannerOptions: BarcodeScannerOptions? = null | ||
| 154 | - if (formats != null) { | ||
| 155 | - val formatsList: MutableList<Int> = mutableListOf() | ||
| 156 | - for (formatValue in formats) { | ||
| 157 | - formatsList.add(BarcodeFormats.fromRawValue(formatValue).intValue) | ||
| 158 | - } | ||
| 159 | - barcodeScannerOptions = if (formatsList.size == 1) { | ||
| 160 | - BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first()) | ||
| 161 | - .build() | ||
| 162 | - } else { | ||
| 163 | - BarcodeScannerOptions.Builder().setBarcodeFormats( | ||
| 164 | - formatsList.first(), | ||
| 165 | - *formatsList.subList(1, formatsList.size).toIntArray() | ||
| 166 | - ).build() | ||
| 167 | - } | ||
| 168 | - } | 148 | + val barcodeScannerOptions: BarcodeScannerOptions? = buildBarcodeScannerOptions(formats) |
| 169 | 149 | ||
| 170 | val position = | 150 | val position = |
| 171 | if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA | 151 | if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA |
| 172 | 152 | ||
| 173 | - val detectionSpeed: DetectionSpeed = if (speed == 0) DetectionSpeed.NO_DUPLICATES | ||
| 174 | - else if (speed ==1) DetectionSpeed.NORMAL else DetectionSpeed.UNRESTRICTED | 153 | + val detectionSpeed: DetectionSpeed = when (speed) { |
| 154 | + 0 -> DetectionSpeed.NO_DUPLICATES | ||
| 155 | + 1 -> DetectionSpeed.NORMAL | ||
| 156 | + else -> DetectionSpeed.UNRESTRICTED | ||
| 157 | + } | ||
| 175 | 158 | ||
| 176 | mobileScanner!!.start( | 159 | mobileScanner!!.start( |
| 177 | barcodeScannerOptions, | 160 | barcodeScannerOptions, |
| @@ -196,29 +179,29 @@ class MobileScannerHandler( | @@ -196,29 +179,29 @@ class MobileScannerHandler( | ||
| 196 | when (it) { | 179 | when (it) { |
| 197 | is AlreadyStarted -> { | 180 | is AlreadyStarted -> { |
| 198 | result.error( | 181 | result.error( |
| 199 | - "MobileScanner", | ||
| 200 | - "Called start() while already started", | 182 | + MobileScannerErrorCodes.ALREADY_STARTED_ERROR, |
| 183 | + MobileScannerErrorCodes.ALREADY_STARTED_ERROR_MESSAGE, | ||
| 201 | null | 184 | null |
| 202 | ) | 185 | ) |
| 203 | } | 186 | } |
| 204 | is CameraError -> { | 187 | is CameraError -> { |
| 205 | result.error( | 188 | result.error( |
| 206 | - "MobileScanner", | ||
| 207 | - "Error occurred when setting up camera!", | 189 | + MobileScannerErrorCodes.CAMERA_ERROR, |
| 190 | + MobileScannerErrorCodes.CAMERA_ERROR_MESSAGE, | ||
| 208 | null | 191 | null |
| 209 | ) | 192 | ) |
| 210 | } | 193 | } |
| 211 | is NoCamera -> { | 194 | is NoCamera -> { |
| 212 | result.error( | 195 | result.error( |
| 213 | - "MobileScanner", | ||
| 214 | - "No camera found or failed to open camera!", | 196 | + MobileScannerErrorCodes.NO_CAMERA_ERROR, |
| 197 | + MobileScannerErrorCodes.NO_CAMERA_ERROR_MESSAGE, | ||
| 215 | null | 198 | null |
| 216 | ) | 199 | ) |
| 217 | } | 200 | } |
| 218 | else -> { | 201 | else -> { |
| 219 | result.error( | 202 | result.error( |
| 220 | - "MobileScanner", | ||
| 221 | - "Unknown error occurred.", | 203 | + MobileScannerErrorCodes.GENERIC_ERROR, |
| 204 | + MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE, | ||
| 222 | null | 205 | null |
| 223 | ) | 206 | ) |
| 224 | } | 207 | } |
| @@ -254,13 +237,13 @@ class MobileScannerHandler( | @@ -254,13 +237,13 @@ class MobileScannerHandler( | ||
| 254 | 237 | ||
| 255 | private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { | 238 | private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { |
| 256 | analyzerResult = result | 239 | analyzerResult = result |
| 257 | - val uri = Uri.fromFile(File(call.arguments.toString())) | ||
| 258 | 240 | ||
| 259 | - // TODO: parse options from the method call | ||
| 260 | - // See https://github.com/juliansteenbakker/mobile_scanner/issues/1069 | 241 | + val formats: List<Int>? = call.argument<List<Int>>("formats") |
| 242 | + val filePath: String = call.argument<String>("filePath")!! | ||
| 243 | + | ||
| 261 | mobileScanner!!.analyzeImage( | 244 | mobileScanner!!.analyzeImage( |
| 262 | - uri, | ||
| 263 | - null, | 245 | + Uri.fromFile(File(filePath)), |
| 246 | + buildBarcodeScannerOptions(formats), | ||
| 264 | analyzeImageSuccessCallback, | 247 | analyzeImageSuccessCallback, |
| 265 | analyzeImageErrorCallback) | 248 | analyzeImageErrorCallback) |
| 266 | } | 249 | } |
| @@ -275,9 +258,11 @@ class MobileScannerHandler( | @@ -275,9 +258,11 @@ class MobileScannerHandler( | ||
| 275 | mobileScanner!!.setScale(call.arguments as Double) | 258 | mobileScanner!!.setScale(call.arguments as Double) |
| 276 | result.success(null) | 259 | result.success(null) |
| 277 | } catch (e: ZoomWhenStopped) { | 260 | } catch (e: ZoomWhenStopped) { |
| 278 | - result.error("MobileScanner", "Called setScale() while stopped!", null) | 261 | + result.error( |
| 262 | + MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR, MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE, null) | ||
| 279 | } catch (e: ZoomNotInRange) { | 263 | } catch (e: ZoomNotInRange) { |
| 280 | - result.error("MobileScanner", "Scale should be within 0 and 1", null) | 264 | + result.error( |
| 265 | + MobileScannerErrorCodes.GENERIC_ERROR, MobileScannerErrorCodes.INVALID_ZOOM_SCALE_ERROR_MESSAGE, null) | ||
| 281 | } | 266 | } |
| 282 | } | 267 | } |
| 283 | 268 | ||
| @@ -286,7 +271,8 @@ class MobileScannerHandler( | @@ -286,7 +271,8 @@ class MobileScannerHandler( | ||
| 286 | mobileScanner!!.resetScale() | 271 | mobileScanner!!.resetScale() |
| 287 | result.success(null) | 272 | result.success(null) |
| 288 | } catch (e: ZoomWhenStopped) { | 273 | } catch (e: ZoomWhenStopped) { |
| 289 | - result.error("MobileScanner", "Called resetScale() while stopped!", null) | 274 | + result.error( |
| 275 | + MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR, MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE, null) | ||
| 290 | } | 276 | } |
| 291 | } | 277 | } |
| 292 | 278 | ||
| @@ -295,4 +281,26 @@ class MobileScannerHandler( | @@ -295,4 +281,26 @@ class MobileScannerHandler( | ||
| 295 | 281 | ||
| 296 | result.success(null) | 282 | result.success(null) |
| 297 | } | 283 | } |
| 284 | + | ||
| 285 | + private fun buildBarcodeScannerOptions(formats: List<Int>?): BarcodeScannerOptions? { | ||
| 286 | + if (formats == null) { | ||
| 287 | + return null | ||
| 288 | + } | ||
| 289 | + | ||
| 290 | + val formatsList: MutableList<Int> = mutableListOf() | ||
| 291 | + | ||
| 292 | + for (formatValue in formats) { | ||
| 293 | + formatsList.add(BarcodeFormats.fromRawValue(formatValue).intValue) | ||
| 294 | + } | ||
| 295 | + | ||
| 296 | + if (formatsList.size == 1) { | ||
| 297 | + return BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first()) | ||
| 298 | + .build() | ||
| 299 | + } | ||
| 300 | + | ||
| 301 | + return BarcodeScannerOptions.Builder().setBarcodeFormats( | ||
| 302 | + formatsList.first(), | ||
| 303 | + *formatsList.subList(1, formatsList.size).toIntArray() | ||
| 304 | + ).build() | ||
| 305 | + } | ||
| 298 | } | 306 | } |
| @@ -5,6 +5,7 @@ import android.app.Activity | @@ -5,6 +5,7 @@ import android.app.Activity | ||
| 5 | import android.content.pm.PackageManager | 5 | import android.content.pm.PackageManager |
| 6 | import androidx.core.app.ActivityCompat | 6 | import androidx.core.app.ActivityCompat |
| 7 | import androidx.core.content.ContextCompat | 7 | import androidx.core.content.ContextCompat |
| 8 | +import dev.steenbakker.mobile_scanner.objects.MobileScannerErrorCodes | ||
| 8 | import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener | 9 | import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener |
| 9 | 10 | ||
| 10 | /** | 11 | /** |
| @@ -12,11 +13,6 @@ import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener | @@ -12,11 +13,6 @@ import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener | ||
| 12 | */ | 13 | */ |
| 13 | class MobileScannerPermissions { | 14 | class MobileScannerPermissions { |
| 14 | companion object { | 15 | companion object { |
| 15 | - const val CAMERA_ACCESS_DENIED = "CameraAccessDenied" | ||
| 16 | - const val CAMERA_ACCESS_DENIED_MESSAGE = "Camera access permission was denied." | ||
| 17 | - const val CAMERA_PERMISSIONS_REQUEST_ONGOING = "CameraPermissionsRequestOngoing" | ||
| 18 | - const val CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE = "Another request is ongoing and multiple requests cannot be handled at once." | ||
| 19 | - | ||
| 20 | /** | 16 | /** |
| 21 | * When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits. | 17 | * When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits. |
| 22 | * @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode | 18 | * @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode |
| @@ -25,7 +21,7 @@ class MobileScannerPermissions { | @@ -25,7 +21,7 @@ class MobileScannerPermissions { | ||
| 25 | } | 21 | } |
| 26 | 22 | ||
| 27 | interface ResultCallback { | 23 | interface ResultCallback { |
| 28 | - fun onResult(errorCode: String?, errorDescription: String?) | 24 | + fun onResult(errorCode: String?) |
| 29 | } | 25 | } |
| 30 | 26 | ||
| 31 | private var listener: RequestPermissionsResultListener? = null | 27 | private var listener: RequestPermissionsResultListener? = null |
| @@ -53,14 +49,13 @@ class MobileScannerPermissions { | @@ -53,14 +49,13 @@ class MobileScannerPermissions { | ||
| 53 | addPermissionListener: (RequestPermissionsResultListener) -> Unit, | 49 | addPermissionListener: (RequestPermissionsResultListener) -> Unit, |
| 54 | callback: ResultCallback) { | 50 | callback: ResultCallback) { |
| 55 | if (ongoing) { | 51 | if (ongoing) { |
| 56 | - callback.onResult( | ||
| 57 | - CAMERA_PERMISSIONS_REQUEST_ONGOING, CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE) | 52 | + callback.onResult(MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING) |
| 58 | return | 53 | return |
| 59 | } | 54 | } |
| 60 | 55 | ||
| 61 | if(hasCameraPermission(activity) == 1) { | 56 | if(hasCameraPermission(activity) == 1) { |
| 62 | // Permissions already exist. Call the callback with success. | 57 | // Permissions already exist. Call the callback with success. |
| 63 | - callback.onResult(null, null) | 58 | + callback.onResult(null) |
| 64 | return | 59 | return |
| 65 | } | 60 | } |
| 66 | 61 | ||
| @@ -68,10 +63,10 @@ class MobileScannerPermissions { | @@ -68,10 +63,10 @@ class MobileScannerPermissions { | ||
| 68 | // Keep track of the listener, so that it can be unregistered later. | 63 | // Keep track of the listener, so that it can be unregistered later. |
| 69 | listener = MobileScannerPermissionsListener( | 64 | listener = MobileScannerPermissionsListener( |
| 70 | object: ResultCallback { | 65 | object: ResultCallback { |
| 71 | - override fun onResult(errorCode: String?, errorDescription: String?) { | 66 | + override fun onResult(errorCode: String?) { |
| 72 | ongoing = false | 67 | ongoing = false |
| 73 | listener = null | 68 | listener = null |
| 74 | - callback.onResult(errorCode, errorDescription) | 69 | + callback.onResult(errorCode) |
| 75 | } | 70 | } |
| 76 | } | 71 | } |
| 77 | ) | 72 | ) |
| 1 | package dev.steenbakker.mobile_scanner | 1 | package dev.steenbakker.mobile_scanner |
| 2 | 2 | ||
| 3 | import android.content.pm.PackageManager | 3 | import android.content.pm.PackageManager |
| 4 | +import dev.steenbakker.mobile_scanner.objects.MobileScannerErrorCodes | ||
| 4 | import io.flutter.plugin.common.PluginRegistry | 5 | import io.flutter.plugin.common.PluginRegistry |
| 5 | 6 | ||
| 6 | /** | 7 | /** |
| @@ -29,11 +30,9 @@ internal class MobileScannerPermissionsListener( | @@ -29,11 +30,9 @@ internal class MobileScannerPermissionsListener( | ||
| 29 | // grantResults could be empty if the permissions request with the user is interrupted | 30 | // grantResults could be empty if the permissions request with the user is interrupted |
| 30 | // https://developer.android.com/reference/android/app/Activity#onRequestPermissionsResult(int,%20java.lang.String[],%20int[]) | 31 | // https://developer.android.com/reference/android/app/Activity#onRequestPermissionsResult(int,%20java.lang.String[],%20int[]) |
| 31 | if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) { | 32 | if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) { |
| 32 | - resultCallback.onResult( | ||
| 33 | - MobileScannerPermissions.CAMERA_ACCESS_DENIED, | ||
| 34 | - MobileScannerPermissions.CAMERA_ACCESS_DENIED_MESSAGE) | 33 | + resultCallback.onResult(MobileScannerErrorCodes.CAMERA_ACCESS_DENIED) |
| 35 | } else { | 34 | } else { |
| 36 | - resultCallback.onResult(null, null) | 35 | + resultCallback.onResult(null) |
| 37 | } | 36 | } |
| 38 | 37 | ||
| 39 | return true | 38 | return true |
| @@ -28,12 +28,22 @@ fun Image.toByteArray(): ByteArray { | @@ -28,12 +28,22 @@ fun Image.toByteArray(): ByteArray { | ||
| 28 | 28 | ||
| 29 | val Barcode.data: Map<String, Any?> | 29 | val Barcode.data: Map<String, Any?> |
| 30 | get() = mapOf( | 30 | get() = mapOf( |
| 31 | - "corners" to cornerPoints?.map { corner -> corner.data }, "format" to format, | ||
| 32 | - "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType, | ||
| 33 | - "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data, | ||
| 34 | - "driverLicense" to driverLicense?.data, "email" to email?.data, | ||
| 35 | - "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data, | ||
| 36 | - "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue | 31 | + "calendarEvent" to calendarEvent?.data, |
| 32 | + "contactInfo" to contactInfo?.data, | ||
| 33 | + "corners" to cornerPoints?.map { corner -> corner.data }, | ||
| 34 | + "displayValue" to displayValue, | ||
| 35 | + "driverLicense" to driverLicense?.data, | ||
| 36 | + "email" to email?.data, | ||
| 37 | + "format" to format, | ||
| 38 | + "geoPoint" to geoPoint?.data, | ||
| 39 | + "phone" to phone?.data, | ||
| 40 | + "rawBytes" to rawBytes, | ||
| 41 | + "rawValue" to rawValue, | ||
| 42 | + "size" to boundingBox?.size, | ||
| 43 | + "sms" to sms?.data, | ||
| 44 | + "type" to valueType, | ||
| 45 | + "url" to url?.data, | ||
| 46 | + "wifi" to wifi?.data, | ||
| 37 | ) | 47 | ) |
| 38 | 48 | ||
| 39 | private val Point.data: Map<String, Double> | 49 | private val Point.data: Map<String, Double> |
| @@ -92,4 +102,14 @@ private val Barcode.UrlBookmark.data: Map<String, Any?> | @@ -92,4 +102,14 @@ private val Barcode.UrlBookmark.data: Map<String, Any?> | ||
| 92 | get() = mapOf("title" to title, "url" to url) | 102 | get() = mapOf("title" to title, "url" to url) |
| 93 | 103 | ||
| 94 | private val Barcode.WiFi.data: Map<String, Any?> | 104 | private val Barcode.WiFi.data: Map<String, Any?> |
| 95 | - get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid) | ||
| 105 | + get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid) | ||
| 106 | + | ||
| 107 | +private val Rect.size: Map<String, Any?> | ||
| 108 | + get() { | ||
| 109 | + // Rect.isValid can't be accessed for some reason, so just do the check manually. | ||
| 110 | + if (left <= right && top <= bottom) { | ||
| 111 | + return mapOf("width" to width().toDouble(), "height" to height().toDouble()) | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + return emptyMap() | ||
| 115 | + } |
android/src/main/kotlin/dev/steenbakker/mobile_scanner/objects/MobileScannerErrorCodes.kt
0 → 100644
| 1 | +package dev.steenbakker.mobile_scanner.objects | ||
| 2 | + | ||
| 3 | +class MobileScannerErrorCodes { | ||
| 4 | + companion object { | ||
| 5 | + const val ALREADY_STARTED_ERROR = "MOBILE_SCANNER_ALREADY_STARTED_ERROR" | ||
| 6 | + const val ALREADY_STARTED_ERROR_MESSAGE = "The scanner was already started." | ||
| 7 | + // The error code 'BARCODE_ERROR' does not have an error message, | ||
| 8 | + // because it uses the error message from the underlying error. | ||
| 9 | + const val BARCODE_ERROR = "MOBILE_SCANNER_BARCODE_ERROR" | ||
| 10 | + // The error code 'CAMERA_ACCESS_DENIED' does not have an error message, | ||
| 11 | + // because it is used for a boolean result. | ||
| 12 | + const val CAMERA_ACCESS_DENIED = "MOBILE_SCANNER_CAMERA_PERMISSION_DENIED" | ||
| 13 | + const val CAMERA_ERROR = "MOBILE_SCANNER_CAMERA_ERROR" | ||
| 14 | + const val CAMERA_ERROR_MESSAGE = "An error occurred when opening the camera." | ||
| 15 | + const val CAMERA_PERMISSIONS_REQUEST_ONGOING = "MOBILE_SCANNER_CAMERA_PERMISSION_REQUEST_PENDING" | ||
| 16 | + const val CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE = "Another request is ongoing and multiple requests cannot be handled at once." | ||
| 17 | + const val GENERIC_ERROR = "MOBILE_SCANNER_GENERIC_ERROR" | ||
| 18 | + const val GENERIC_ERROR_MESSAGE = "An unknown error occurred." | ||
| 19 | + const val INVALID_ZOOM_SCALE_ERROR_MESSAGE = "The zoom scale should be between 0 and 1 (both inclusive)" | ||
| 20 | + const val NO_CAMERA_ERROR = "MOBILE_SCANNER_NO_CAMERA_ERROR" | ||
| 21 | + const val NO_CAMERA_ERROR_MESSAGE = "No cameras available." | ||
| 22 | + const val SET_SCALE_WHEN_STOPPED_ERROR = "MOBILE_SCANNER_SET_SCALE_WHEN_STOPPED_ERROR" | ||
| 23 | + const val SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE = "The zoom scale cannot be changed when the camera is stopped." | ||
| 24 | + } | ||
| 25 | +} |
| @@ -27,12 +27,12 @@ android { | @@ -27,12 +27,12 @@ android { | ||
| 27 | compileSdk 34 | 27 | compileSdk 34 |
| 28 | 28 | ||
| 29 | compileOptions { | 29 | compileOptions { |
| 30 | - sourceCompatibility JavaVersion.VERSION_1_8 | ||
| 31 | - targetCompatibility JavaVersion.VERSION_1_8 | 30 | + sourceCompatibility JavaVersion.VERSION_17 |
| 31 | + targetCompatibility JavaVersion.VERSION_17 | ||
| 32 | } | 32 | } |
| 33 | 33 | ||
| 34 | kotlinOptions { | 34 | kotlinOptions { |
| 35 | - jvmTarget = '1.8' | 35 | + jvmTarget = '17' |
| 36 | } | 36 | } |
| 37 | 37 | ||
| 38 | sourceSets { | 38 | sourceSets { |
| @@ -42,7 +42,7 @@ android { | @@ -42,7 +42,7 @@ android { | ||
| 42 | defaultConfig { | 42 | defaultConfig { |
| 43 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). | 43 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). |
| 44 | applicationId "dev.steenbakker.mobile_scanner_example" | 44 | applicationId "dev.steenbakker.mobile_scanner_example" |
| 45 | - minSdkVersion 21 | 45 | + minSdkVersion 24 |
| 46 | targetSdkVersion 34 | 46 | targetSdkVersion 34 |
| 47 | versionCode flutterVersionCode.toInteger() | 47 | versionCode flutterVersionCode.toInteger() |
| 48 | versionName flutterVersionName | 48 | versionName flutterVersionName |
| 1 | #Thu May 02 10:24:49 CEST 2024 | 1 | #Thu May 02 10:24:49 CEST 2024 |
| 2 | distributionBase=GRADLE_USER_HOME | 2 | distributionBase=GRADLE_USER_HOME |
| 3 | distributionPath=wrapper/dists | 3 | distributionPath=wrapper/dists |
| 4 | -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip | 4 | +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip |
| 5 | zipStoreBase=GRADLE_USER_HOME | 5 | zipStoreBase=GRADLE_USER_HOME |
| 6 | zipStorePath=wrapper/dists | 6 | zipStorePath=wrapper/dists |
| @@ -18,8 +18,8 @@ pluginManagement { | @@ -18,8 +18,8 @@ pluginManagement { | ||
| 18 | 18 | ||
| 19 | plugins { | 19 | plugins { |
| 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" | 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" |
| 21 | - id "com.android.application" version "7.3.0" apply false | ||
| 22 | - id "org.jetbrains.kotlin.android" version "1.7.22" apply false | 21 | + id "com.android.application" version "8.3.2" apply false |
| 22 | + id "org.jetbrains.kotlin.android" version "2.0.20" apply false | ||
| 23 | } | 23 | } |
| 24 | 24 | ||
| 25 | include ":app" | 25 | include ":app" |
| @@ -21,6 +21,6 @@ | @@ -21,6 +21,6 @@ | ||
| 21 | <key>CFBundleVersion</key> | 21 | <key>CFBundleVersion</key> |
| 22 | <string>1.0</string> | 22 | <string>1.0</string> |
| 23 | <key>MinimumOSVersion</key> | 23 | <key>MinimumOSVersion</key> |
| 24 | - <string>12.0</string> | 24 | + <string>15.5.0</string> |
| 25 | </dict> | 25 | </dict> |
| 26 | </plist> | 26 | </plist> |
| 1 | # Uncomment this line to define a global platform for your project | 1 | # Uncomment this line to define a global platform for your project |
| 2 | -# platform :ios, '12.0' | 2 | +# platform :ios, '15.5.0' |
| 3 | 3 | ||
| 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. |
| 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' | 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' |
| @@ -40,8 +40,8 @@ end | @@ -40,8 +40,8 @@ end | ||
| 40 | post_install do |installer| | 40 | post_install do |installer| |
| 41 | installer.pods_project.targets.each do |target| | 41 | installer.pods_project.targets.each do |target| |
| 42 | flutter_additional_ios_build_settings(target) | 42 | flutter_additional_ios_build_settings(target) |
| 43 | - target.build_configurations.each do |config| | ||
| 44 | - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' | ||
| 45 | - end | 43 | + target.build_configurations.each do |config| |
| 44 | + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.5.0' | ||
| 45 | + end | ||
| 46 | end | 46 | end |
| 47 | end | 47 | end |
| @@ -470,7 +470,7 @@ | @@ -470,7 +470,7 @@ | ||
| 470 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | 470 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
| 471 | GCC_WARN_UNUSED_FUNCTION = YES; | 471 | GCC_WARN_UNUSED_FUNCTION = YES; |
| 472 | GCC_WARN_UNUSED_VARIABLE = YES; | 472 | GCC_WARN_UNUSED_VARIABLE = YES; |
| 473 | - IPHONEOS_DEPLOYMENT_TARGET = 12.0; | 473 | + IPHONEOS_DEPLOYMENT_TARGET = 15.5.0; |
| 474 | MTL_ENABLE_DEBUG_INFO = NO; | 474 | MTL_ENABLE_DEBUG_INFO = NO; |
| 475 | SDKROOT = iphoneos; | 475 | SDKROOT = iphoneos; |
| 476 | SUPPORTED_PLATFORMS = iphoneos; | 476 | SUPPORTED_PLATFORMS = iphoneos; |
| @@ -488,14 +488,14 @@ | @@ -488,14 +488,14 @@ | ||
| 488 | CODE_SIGN_IDENTITY = "Apple Development"; | 488 | CODE_SIGN_IDENTITY = "Apple Development"; |
| 489 | CODE_SIGN_STYLE = Automatic; | 489 | CODE_SIGN_STYLE = Automatic; |
| 490 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; | 490 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; |
| 491 | - DEVELOPMENT_TEAM = 75Y2P2WSQQ; | 491 | + DEVELOPMENT_TEAM = ""; |
| 492 | ENABLE_BITCODE = NO; | 492 | ENABLE_BITCODE = NO; |
| 493 | INFOPLIST_FILE = Runner/Info.plist; | 493 | INFOPLIST_FILE = Runner/Info.plist; |
| 494 | LD_RUNPATH_SEARCH_PATHS = ( | 494 | LD_RUNPATH_SEARCH_PATHS = ( |
| 495 | "$(inherited)", | 495 | "$(inherited)", |
| 496 | "@executable_path/Frameworks", | 496 | "@executable_path/Frameworks", |
| 497 | ); | 497 | ); |
| 498 | - PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner"; | 498 | + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner-example"; |
| 499 | PRODUCT_NAME = "$(TARGET_NAME)"; | 499 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 500 | PROVISIONING_PROFILE_SPECIFIER = ""; | 500 | PROVISIONING_PROFILE_SPECIFIER = ""; |
| 501 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; | 501 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; |
| @@ -601,7 +601,7 @@ | @@ -601,7 +601,7 @@ | ||
| 601 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | 601 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
| 602 | GCC_WARN_UNUSED_FUNCTION = YES; | 602 | GCC_WARN_UNUSED_FUNCTION = YES; |
| 603 | GCC_WARN_UNUSED_VARIABLE = YES; | 603 | GCC_WARN_UNUSED_VARIABLE = YES; |
| 604 | - IPHONEOS_DEPLOYMENT_TARGET = 12.0; | 604 | + IPHONEOS_DEPLOYMENT_TARGET = 15.5.0; |
| 605 | MTL_ENABLE_DEBUG_INFO = YES; | 605 | MTL_ENABLE_DEBUG_INFO = YES; |
| 606 | ONLY_ACTIVE_ARCH = YES; | 606 | ONLY_ACTIVE_ARCH = YES; |
| 607 | SDKROOT = iphoneos; | 607 | SDKROOT = iphoneos; |
| @@ -650,7 +650,7 @@ | @@ -650,7 +650,7 @@ | ||
| 650 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | 650 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
| 651 | GCC_WARN_UNUSED_FUNCTION = YES; | 651 | GCC_WARN_UNUSED_FUNCTION = YES; |
| 652 | GCC_WARN_UNUSED_VARIABLE = YES; | 652 | GCC_WARN_UNUSED_VARIABLE = YES; |
| 653 | - IPHONEOS_DEPLOYMENT_TARGET = 12.0; | 653 | + IPHONEOS_DEPLOYMENT_TARGET = 15.5.0; |
| 654 | MTL_ENABLE_DEBUG_INFO = NO; | 654 | MTL_ENABLE_DEBUG_INFO = NO; |
| 655 | SDKROOT = iphoneos; | 655 | SDKROOT = iphoneos; |
| 656 | SUPPORTED_PLATFORMS = iphoneos; | 656 | SUPPORTED_PLATFORMS = iphoneos; |
| @@ -670,14 +670,14 @@ | @@ -670,14 +670,14 @@ | ||
| 670 | CODE_SIGN_IDENTITY = "Apple Development"; | 670 | CODE_SIGN_IDENTITY = "Apple Development"; |
| 671 | CODE_SIGN_STYLE = Automatic; | 671 | CODE_SIGN_STYLE = Automatic; |
| 672 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; | 672 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; |
| 673 | - DEVELOPMENT_TEAM = 75Y2P2WSQQ; | 673 | + DEVELOPMENT_TEAM = ""; |
| 674 | ENABLE_BITCODE = NO; | 674 | ENABLE_BITCODE = NO; |
| 675 | INFOPLIST_FILE = Runner/Info.plist; | 675 | INFOPLIST_FILE = Runner/Info.plist; |
| 676 | LD_RUNPATH_SEARCH_PATHS = ( | 676 | LD_RUNPATH_SEARCH_PATHS = ( |
| 677 | "$(inherited)", | 677 | "$(inherited)", |
| 678 | "@executable_path/Frameworks", | 678 | "@executable_path/Frameworks", |
| 679 | ); | 679 | ); |
| 680 | - PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner"; | 680 | + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner-example"; |
| 681 | PRODUCT_NAME = "$(TARGET_NAME)"; | 681 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 682 | PROVISIONING_PROFILE_SPECIFIER = ""; | 682 | PROVISIONING_PROFILE_SPECIFIER = ""; |
| 683 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; | 683 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; |
| @@ -696,14 +696,14 @@ | @@ -696,14 +696,14 @@ | ||
| 696 | CODE_SIGN_IDENTITY = "Apple Development"; | 696 | CODE_SIGN_IDENTITY = "Apple Development"; |
| 697 | CODE_SIGN_STYLE = Automatic; | 697 | CODE_SIGN_STYLE = Automatic; |
| 698 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; | 698 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; |
| 699 | - DEVELOPMENT_TEAM = 75Y2P2WSQQ; | 699 | + DEVELOPMENT_TEAM = ""; |
| 700 | ENABLE_BITCODE = NO; | 700 | ENABLE_BITCODE = NO; |
| 701 | INFOPLIST_FILE = Runner/Info.plist; | 701 | INFOPLIST_FILE = Runner/Info.plist; |
| 702 | LD_RUNPATH_SEARCH_PATHS = ( | 702 | LD_RUNPATH_SEARCH_PATHS = ( |
| 703 | "$(inherited)", | 703 | "$(inherited)", |
| 704 | "@executable_path/Frameworks", | 704 | "@executable_path/Frameworks", |
| 705 | ); | 705 | ); |
| 706 | - PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner"; | 706 | + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner-example"; |
| 707 | PRODUCT_NAME = "$(TARGET_NAME)"; | 707 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 708 | PROVISIONING_PROFILE_SPECIFIER = ""; | 708 | PROVISIONING_PROFILE_SPECIFIER = ""; |
| 709 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; | 709 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; |
| 1 | import UIKit | 1 | import UIKit |
| 2 | import Flutter | 2 | import Flutter |
| 3 | 3 | ||
| 4 | -@UIApplicationMain | 4 | +@main |
| 5 | @objc class AppDelegate: FlutterAppDelegate { | 5 | @objc class AppDelegate: FlutterAppDelegate { |
| 6 | override func application( | 6 | override func application( |
| 7 | _ application: UIApplication, | 7 | _ application: UIApplication, |
| 1 | +import 'package:flutter/foundation.dart'; | ||
| 2 | +import 'package:flutter/material.dart'; | ||
| 3 | +import 'package:image_picker/image_picker.dart'; | ||
| 4 | +import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 5 | + | ||
| 6 | +class BarcodeScannerAnalyzeImage extends StatefulWidget { | ||
| 7 | + const BarcodeScannerAnalyzeImage({super.key}); | ||
| 8 | + | ||
| 9 | + @override | ||
| 10 | + State<BarcodeScannerAnalyzeImage> createState() => | ||
| 11 | + _BarcodeScannerAnalyzeImageState(); | ||
| 12 | +} | ||
| 13 | + | ||
| 14 | +class _BarcodeScannerAnalyzeImageState | ||
| 15 | + extends State<BarcodeScannerAnalyzeImage> { | ||
| 16 | + final MobileScannerController _controller = MobileScannerController(); | ||
| 17 | + | ||
| 18 | + BarcodeCapture? _barcodeCapture; | ||
| 19 | + | ||
| 20 | + Future<void> _analyzeImageFromFile() async { | ||
| 21 | + try { | ||
| 22 | + final XFile? file = | ||
| 23 | + await ImagePicker().pickImage(source: ImageSource.gallery); | ||
| 24 | + | ||
| 25 | + if (!mounted) { | ||
| 26 | + return; | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + if (file == null) { | ||
| 30 | + setState(() { | ||
| 31 | + _barcodeCapture = null; | ||
| 32 | + }); | ||
| 33 | + return; | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + final BarcodeCapture? barcodeCapture = | ||
| 37 | + await _controller.analyzeImage(file.path); | ||
| 38 | + | ||
| 39 | + if (mounted) { | ||
| 40 | + setState(() { | ||
| 41 | + _barcodeCapture = barcodeCapture; | ||
| 42 | + }); | ||
| 43 | + } | ||
| 44 | + } catch (_) {} | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + @override | ||
| 48 | + Widget build(BuildContext context) { | ||
| 49 | + Widget label = const Text('Pick a file to detect barcode'); | ||
| 50 | + | ||
| 51 | + if (_barcodeCapture != null) { | ||
| 52 | + label = Text( | ||
| 53 | + _barcodeCapture?.barcodes.firstOrNull?.rawValue ?? | ||
| 54 | + 'No barcode detected', | ||
| 55 | + ); | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + return Scaffold( | ||
| 59 | + appBar: AppBar(title: const Text('Analyze image from file')), | ||
| 60 | + body: Column( | ||
| 61 | + children: [ | ||
| 62 | + Expanded( | ||
| 63 | + child: Center( | ||
| 64 | + child: ElevatedButton( | ||
| 65 | + onPressed: kIsWeb ? null : _analyzeImageFromFile, | ||
| 66 | + child: kIsWeb | ||
| 67 | + ? const Text('Analyze image is not supported on web') | ||
| 68 | + : const Text('Choose file'), | ||
| 69 | + ), | ||
| 70 | + ), | ||
| 71 | + ), | ||
| 72 | + Expanded(child: Center(child: label)), | ||
| 73 | + ], | ||
| 74 | + ), | ||
| 75 | + ); | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + @override | ||
| 79 | + void dispose() { | ||
| 80 | + _controller.dispose(); | ||
| 81 | + super.dispose(); | ||
| 82 | + } | ||
| 83 | +} |
| @@ -60,7 +60,7 @@ class _BarcodeScannerWithControllerState | @@ -60,7 +60,7 @@ class _BarcodeScannerWithControllerState | ||
| 60 | 60 | ||
| 61 | @override | 61 | @override |
| 62 | void didChangeAppLifecycleState(AppLifecycleState state) { | 62 | void didChangeAppLifecycleState(AppLifecycleState state) { |
| 63 | - if (!controller.value.isInitialized) { | 63 | + if (!controller.value.hasCameraPermission) { |
| 64 | return; | 64 | return; |
| 65 | } | 65 | } |
| 66 | 66 |
| @@ -39,16 +39,16 @@ class _BarcodeScannerWithScanWindowState | @@ -39,16 +39,16 @@ class _BarcodeScannerWithScanWindowState | ||
| 39 | final scannedBarcode = barcodeCapture.barcodes.first; | 39 | final scannedBarcode = barcodeCapture.barcodes.first; |
| 40 | 40 | ||
| 41 | // No barcode corners, or size, or no camera preview size. | 41 | // No barcode corners, or size, or no camera preview size. |
| 42 | - if (scannedBarcode.corners.isEmpty || | ||
| 43 | - value.size.isEmpty || | ||
| 44 | - barcodeCapture.size.isEmpty) { | 42 | + if (value.size.isEmpty || |
| 43 | + scannedBarcode.size.isEmpty || | ||
| 44 | + scannedBarcode.corners.isEmpty) { | ||
| 45 | return const SizedBox(); | 45 | return const SizedBox(); |
| 46 | } | 46 | } |
| 47 | 47 | ||
| 48 | return CustomPaint( | 48 | return CustomPaint( |
| 49 | painter: BarcodeOverlay( | 49 | painter: BarcodeOverlay( |
| 50 | barcodeCorners: scannedBarcode.corners, | 50 | barcodeCorners: scannedBarcode.corners, |
| 51 | - barcodeSize: barcodeCapture.size, | 51 | + barcodeSize: scannedBarcode.size, |
| 52 | boxFit: BoxFit.contain, | 52 | boxFit: BoxFit.contain, |
| 53 | cameraPreviewSize: value.size, | 53 | cameraPreviewSize: value.size, |
| 54 | ), | 54 | ), |
| @@ -131,15 +131,15 @@ class ScannerOverlay extends CustomPainter { | @@ -131,15 +131,15 @@ class ScannerOverlay extends CustomPainter { | ||
| 131 | 131 | ||
| 132 | @override | 132 | @override |
| 133 | void paint(Canvas canvas, Size size) { | 133 | void paint(Canvas canvas, Size size) { |
| 134 | - // TODO: use `Offset.zero & size` instead of Rect.largest | ||
| 135 | // we need to pass the size to the custom paint widget | 134 | // we need to pass the size to the custom paint widget |
| 136 | - final backgroundPath = Path()..addRect(Rect.largest); | 135 | + final backgroundPath = Path() |
| 136 | + ..addRect(Rect.fromLTWH(0, 0, size.width, size.height)); | ||
| 137 | final cutoutPath = Path()..addRect(scanWindow); | 137 | final cutoutPath = Path()..addRect(scanWindow); |
| 138 | 138 | ||
| 139 | final backgroundPaint = Paint() | 139 | final backgroundPaint = Paint() |
| 140 | ..color = Colors.black.withOpacity(0.5) | 140 | ..color = Colors.black.withOpacity(0.5) |
| 141 | ..style = PaintingStyle.fill | 141 | ..style = PaintingStyle.fill |
| 142 | - ..blendMode = BlendMode.dstOut; | 142 | + ..blendMode = BlendMode.dstOver; |
| 143 | 143 | ||
| 144 | final backgroundWithCutout = Path.combine( | 144 | final backgroundWithCutout = Path.combine( |
| 145 | PathOperation.difference, | 145 | PathOperation.difference, |
| 1 | import 'package:flutter/material.dart'; | 1 | import 'package:flutter/material.dart'; |
| 2 | +import 'package:mobile_scanner_example/barcode_scanner_analyze_image.dart'; | ||
| 2 | import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; | 3 | import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; |
| 3 | import 'package:mobile_scanner_example/barcode_scanner_listview.dart'; | 4 | import 'package:mobile_scanner_example/barcode_scanner_listview.dart'; |
| 4 | import 'package:mobile_scanner_example/barcode_scanner_pageview.dart'; | 5 | import 'package:mobile_scanner_example/barcode_scanner_pageview.dart'; |
| @@ -20,95 +21,75 @@ void main() { | @@ -20,95 +21,75 @@ void main() { | ||
| 20 | class MyHome extends StatelessWidget { | 21 | class MyHome extends StatelessWidget { |
| 21 | const MyHome({super.key}); | 22 | const MyHome({super.key}); |
| 22 | 23 | ||
| 24 | + Widget _buildItem(BuildContext context, String label, Widget page) { | ||
| 25 | + return Padding( | ||
| 26 | + padding: const EdgeInsets.all(8.0), | ||
| 27 | + child: Center( | ||
| 28 | + child: ElevatedButton( | ||
| 29 | + onPressed: () { | ||
| 30 | + Navigator.of(context).push( | ||
| 31 | + MaterialPageRoute( | ||
| 32 | + builder: (context) => page, | ||
| 33 | + ), | ||
| 34 | + ); | ||
| 35 | + }, | ||
| 36 | + child: Text(label), | ||
| 37 | + ), | ||
| 38 | + ), | ||
| 39 | + ); | ||
| 40 | + } | ||
| 41 | + | ||
| 23 | @override | 42 | @override |
| 24 | Widget build(BuildContext context) { | 43 | Widget build(BuildContext context) { |
| 25 | return Scaffold( | 44 | return Scaffold( |
| 26 | appBar: AppBar(title: const Text('Mobile Scanner Example')), | 45 | appBar: AppBar(title: const Text('Mobile Scanner Example')), |
| 27 | body: Center( | 46 | body: Center( |
| 28 | - child: Column( | ||
| 29 | - mainAxisAlignment: MainAxisAlignment.spaceAround, | 47 | + child: ListView( |
| 30 | children: [ | 48 | children: [ |
| 31 | - ElevatedButton( | ||
| 32 | - onPressed: () { | ||
| 33 | - Navigator.of(context).push( | ||
| 34 | - MaterialPageRoute( | ||
| 35 | - builder: (context) => const BarcodeScannerSimple(), | ||
| 36 | - ), | ||
| 37 | - ); | ||
| 38 | - }, | ||
| 39 | - child: const Text('MobileScanner Simple'), | 49 | + _buildItem( |
| 50 | + context, | ||
| 51 | + 'MobileScanner Simple', | ||
| 52 | + const BarcodeScannerSimple(), | ||
| 40 | ), | 53 | ), |
| 41 | - ElevatedButton( | ||
| 42 | - onPressed: () { | ||
| 43 | - Navigator.of(context).push( | ||
| 44 | - MaterialPageRoute( | ||
| 45 | - builder: (context) => const BarcodeScannerListView(), | ||
| 46 | - ), | ||
| 47 | - ); | ||
| 48 | - }, | ||
| 49 | - child: const Text('MobileScanner with ListView'), | 54 | + _buildItem( |
| 55 | + context, | ||
| 56 | + 'MobileScanner with ListView', | ||
| 57 | + const BarcodeScannerListView(), | ||
| 50 | ), | 58 | ), |
| 51 | - ElevatedButton( | ||
| 52 | - onPressed: () { | ||
| 53 | - Navigator.of(context).push( | ||
| 54 | - MaterialPageRoute( | ||
| 55 | - builder: (context) => const BarcodeScannerWithController(), | ||
| 56 | - ), | ||
| 57 | - ); | ||
| 58 | - }, | ||
| 59 | - child: const Text('MobileScanner with Controller'), | 59 | + _buildItem( |
| 60 | + context, | ||
| 61 | + 'MobileScanner with Controller', | ||
| 62 | + const BarcodeScannerWithController(), | ||
| 60 | ), | 63 | ), |
| 61 | - ElevatedButton( | ||
| 62 | - onPressed: () { | ||
| 63 | - Navigator.of(context).push( | ||
| 64 | - MaterialPageRoute( | ||
| 65 | - builder: (context) => const BarcodeScannerWithScanWindow(), | ||
| 66 | - ), | ||
| 67 | - ); | ||
| 68 | - }, | ||
| 69 | - child: const Text('MobileScanner with ScanWindow'), | 64 | + _buildItem( |
| 65 | + context, | ||
| 66 | + 'MobileScanner with ScanWindow', | ||
| 67 | + const BarcodeScannerWithScanWindow(), | ||
| 70 | ), | 68 | ), |
| 71 | - ElevatedButton( | ||
| 72 | - onPressed: () { | ||
| 73 | - Navigator.of(context).push( | ||
| 74 | - MaterialPageRoute( | ||
| 75 | - builder: (context) => const BarcodeScannerReturningImage(), | ||
| 76 | - ), | ||
| 77 | - ); | ||
| 78 | - }, | ||
| 79 | - child: const Text( | ||
| 80 | - 'MobileScanner with Controller (returning image)', | ||
| 81 | - ), | 69 | + _buildItem( |
| 70 | + context, | ||
| 71 | + 'MobileScanner with Controller (return image)', | ||
| 72 | + const BarcodeScannerReturningImage(), | ||
| 73 | + ), | ||
| 74 | + _buildItem( | ||
| 75 | + context, | ||
| 76 | + 'MobileScanner with zoom slider', | ||
| 77 | + const BarcodeScannerWithZoom(), | ||
| 82 | ), | 78 | ), |
| 83 | - ElevatedButton( | ||
| 84 | - onPressed: () { | ||
| 85 | - Navigator.of(context).push( | ||
| 86 | - MaterialPageRoute( | ||
| 87 | - builder: (context) => const BarcodeScannerWithZoom(), | ||
| 88 | - ), | ||
| 89 | - ); | ||
| 90 | - }, | ||
| 91 | - child: const Text('MobileScanner with zoom slider'), | 79 | + _buildItem( |
| 80 | + context, | ||
| 81 | + 'MobileScanner with PageView', | ||
| 82 | + const BarcodeScannerPageView(), | ||
| 92 | ), | 83 | ), |
| 93 | - ElevatedButton( | ||
| 94 | - onPressed: () { | ||
| 95 | - Navigator.of(context).push( | ||
| 96 | - MaterialPageRoute( | ||
| 97 | - builder: (context) => const BarcodeScannerPageView(), | ||
| 98 | - ), | ||
| 99 | - ); | ||
| 100 | - }, | ||
| 101 | - child: const Text('MobileScanner pageView'), | 84 | + _buildItem( |
| 85 | + context, | ||
| 86 | + 'MobileScanner with Overlay', | ||
| 87 | + const BarcodeScannerWithOverlay(), | ||
| 102 | ), | 88 | ), |
| 103 | - ElevatedButton( | ||
| 104 | - onPressed: () { | ||
| 105 | - Navigator.of(context).push( | ||
| 106 | - MaterialPageRoute( | ||
| 107 | - builder: (context) => BarcodeScannerWithOverlay(), | ||
| 108 | - ), | ||
| 109 | - ); | ||
| 110 | - }, | ||
| 111 | - child: const Text('MobileScanner with Overlay'), | 89 | + _buildItem( |
| 90 | + context, | ||
| 91 | + 'Analyze image from file', | ||
| 92 | + const BarcodeScannerAnalyzeImage(), | ||
| 112 | ), | 93 | ), |
| 113 | ], | 94 | ], |
| 114 | ), | 95 | ), |
| @@ -5,6 +5,8 @@ import 'package:mobile_scanner_example/scanner_button_widgets.dart'; | @@ -5,6 +5,8 @@ import 'package:mobile_scanner_example/scanner_button_widgets.dart'; | ||
| 5 | import 'package:mobile_scanner_example/scanner_error_widget.dart'; | 5 | import 'package:mobile_scanner_example/scanner_error_widget.dart'; |
| 6 | 6 | ||
| 7 | class BarcodeScannerWithOverlay extends StatefulWidget { | 7 | class BarcodeScannerWithOverlay extends StatefulWidget { |
| 8 | + const BarcodeScannerWithOverlay({super.key}); | ||
| 9 | + | ||
| 8 | @override | 10 | @override |
| 9 | _BarcodeScannerWithOverlayState createState() => | 11 | _BarcodeScannerWithOverlayState createState() => |
| 10 | _BarcodeScannerWithOverlayState(); | 12 | _BarcodeScannerWithOverlayState(); |
| @@ -100,9 +102,9 @@ class ScannerOverlay extends CustomPainter { | @@ -100,9 +102,9 @@ class ScannerOverlay extends CustomPainter { | ||
| 100 | 102 | ||
| 101 | @override | 103 | @override |
| 102 | void paint(Canvas canvas, Size size) { | 104 | void paint(Canvas canvas, Size size) { |
| 103 | - // TODO: use `Offset.zero & size` instead of Rect.largest | ||
| 104 | // we need to pass the size to the custom paint widget | 105 | // we need to pass the size to the custom paint widget |
| 105 | - final backgroundPath = Path()..addRect(Rect.largest); | 106 | + final backgroundPath = Path() |
| 107 | + ..addRect(Rect.fromLTWH(0, 0, size.width, size.height)); | ||
| 106 | 108 | ||
| 107 | final cutoutPath = Path() | 109 | final cutoutPath = Path() |
| 108 | ..addRRect( | 110 | ..addRRect( |
| @@ -118,7 +120,7 @@ class ScannerOverlay extends CustomPainter { | @@ -118,7 +120,7 @@ class ScannerOverlay extends CustomPainter { | ||
| 118 | final backgroundPaint = Paint() | 120 | final backgroundPaint = Paint() |
| 119 | ..color = Colors.black.withOpacity(0.5) | 121 | ..color = Colors.black.withOpacity(0.5) |
| 120 | ..style = PaintingStyle.fill | 122 | ..style = PaintingStyle.fill |
| 121 | - ..blendMode = BlendMode.dstOut; | 123 | + ..blendMode = BlendMode.dstOver; |
| 122 | 124 | ||
| 123 | final backgroundWithCutout = Path.combine( | 125 | final backgroundWithCutout = Path.combine( |
| 124 | PathOperation.difference, | 126 | PathOperation.difference, |
| @@ -112,6 +112,7 @@ class SwitchCameraButton extends StatelessWidget { | @@ -112,6 +112,7 @@ class SwitchCameraButton extends StatelessWidget { | ||
| 112 | } | 112 | } |
| 113 | 113 | ||
| 114 | return IconButton( | 114 | return IconButton( |
| 115 | + color: Colors.white, | ||
| 115 | iconSize: 32.0, | 116 | iconSize: 32.0, |
| 116 | icon: icon, | 117 | icon: icon, |
| 117 | onPressed: () async { | 118 | onPressed: () async { |
| @@ -166,9 +167,13 @@ class ToggleFlashlightButton extends StatelessWidget { | @@ -166,9 +167,13 @@ class ToggleFlashlightButton extends StatelessWidget { | ||
| 166 | }, | 167 | }, |
| 167 | ); | 168 | ); |
| 168 | case TorchState.unavailable: | 169 | case TorchState.unavailable: |
| 169 | - return const Icon( | ||
| 170 | - Icons.no_flash, | ||
| 171 | - color: Colors.grey, | 170 | + return const SizedBox.square( |
| 171 | + dimension: 48.0, | ||
| 172 | + child: Icon( | ||
| 173 | + Icons.no_flash, | ||
| 174 | + size: 32.0, | ||
| 175 | + color: Colors.grey, | ||
| 176 | + ), | ||
| 172 | ); | 177 | ); |
| 173 | } | 178 | } |
| 174 | }, | 179 | }, |
| 1 | import Cocoa | 1 | import Cocoa |
| 2 | import FlutterMacOS | 2 | import FlutterMacOS |
| 3 | 3 | ||
| 4 | -@NSApplicationMain | 4 | +@main |
| 5 | class AppDelegate: FlutterAppDelegate { | 5 | class AppDelegate: FlutterAppDelegate { |
| 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { | 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { |
| 7 | return true | 7 | return true |
| 8 | } | 8 | } |
| 9 | + | ||
| 10 | + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { | ||
| 11 | + return true | ||
| 12 | + } | ||
| 9 | } | 13 | } |
| @@ -21,7 +21,7 @@ | @@ -21,7 +21,7 @@ | ||
| 21 | <meta name="description" content="Demonstrates how to use the mobile_scanner plugin."> | 21 | <meta name="description" content="Demonstrates how to use the mobile_scanner plugin."> |
| 22 | 22 | ||
| 23 | <!-- iOS meta tags & icons --> | 23 | <!-- iOS meta tags & icons --> |
| 24 | - <meta name="apple-mobile-web-app-capable" content="yes"> | 24 | + <meta name="mobile-web-app-capable" content="yes"> |
| 25 | <meta name="apple-mobile-web-app-status-bar-style" content="black"> | 25 | <meta name="apple-mobile-web-app-status-bar-style" content="black"> |
| 26 | <meta name="apple-mobile-web-app-title" content="mobile_scanner_example"> | 26 | <meta name="apple-mobile-web-app-title" content="mobile_scanner_example"> |
| 27 | <link rel="apple-touch-icon" href="icons/Icon-192.png"> | 27 | <link rel="apple-touch-icon" href="icons/Icon-192.png"> |
| @@ -19,6 +19,12 @@ public class BarcodeHandler: NSObject, FlutterStreamHandler { | @@ -19,6 +19,12 @@ public class BarcodeHandler: NSObject, FlutterStreamHandler { | ||
| 19 | eventChannel.setStreamHandler(self) | 19 | eventChannel.setStreamHandler(self) |
| 20 | } | 20 | } |
| 21 | 21 | ||
| 22 | + func publishError(_ error: FlutterError) { | ||
| 23 | + DispatchQueue.main.async { | ||
| 24 | + self.eventSink?(error) | ||
| 25 | + } | ||
| 26 | + } | ||
| 27 | + | ||
| 22 | func publishEvent(_ event: [String: Any?]) { | 28 | func publishEvent(_ event: [String: Any?]) { |
| 23 | DispatchQueue.main.async { | 29 | DispatchQueue.main.async { |
| 24 | self.eventSink?(event) | 30 | self.eventSink?(event) |
| @@ -22,11 +22,8 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -22,11 +22,8 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 22 | /// The selected camera | 22 | /// The selected camera |
| 23 | var device: AVCaptureDevice! | 23 | var device: AVCaptureDevice! |
| 24 | 24 | ||
| 25 | - /// Barcode scanner for results | ||
| 26 | - var scanner = BarcodeScanner.barcodeScanner() | ||
| 27 | - | ||
| 28 | - /// Return image buffer with the Barcode event | ||
| 29 | - var returnImage: Bool = false | 25 | + /// The long lived barcode scanner for scanning barcodes from a camera preview. |
| 26 | + var scanner: BarcodeScanner? = nil | ||
| 30 | 27 | ||
| 31 | /// Default position of camera | 28 | /// Default position of camera |
| 32 | var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back | 29 | var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back |
| @@ -60,11 +57,11 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -60,11 +57,11 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 60 | private var imagesCurrentlyBeingProcessed = false | 57 | private var imagesCurrentlyBeingProcessed = false |
| 61 | 58 | ||
| 62 | public var timeoutSeconds: Double = 0 | 59 | public var timeoutSeconds: Double = 0 |
| 63 | - | 60 | + |
| 64 | private var stopped: Bool { | 61 | private var stopped: Bool { |
| 65 | return device == nil || captureSession == nil | 62 | return device == nil || captureSession == nil |
| 66 | } | 63 | } |
| 67 | - | 64 | + |
| 68 | private var paused: Bool { | 65 | private var paused: Bool { |
| 69 | return stopped && textureId != nil | 66 | return stopped && textureId != nil |
| 70 | } | 67 | } |
| @@ -134,9 +131,8 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -134,9 +131,8 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 134 | 131 | ||
| 135 | /// Gets called when a new image is added to the buffer | 132 | /// Gets called when a new image is added to the buffer |
| 136 | public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { | 133 | public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { |
| 137 | - | 134 | + |
| 138 | guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { | 135 | guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { |
| 139 | - print("Failed to get image buffer from sample buffer.") | ||
| 140 | return | 136 | return |
| 141 | } | 137 | } |
| 142 | latestBuffer = imageBuffer | 138 | latestBuffer = imageBuffer |
| @@ -159,7 +155,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -159,7 +155,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 159 | position: videoPosition | 155 | position: videoPosition |
| 160 | ) | 156 | ) |
| 161 | 157 | ||
| 162 | - scanner.process(image) { [self] barcodes, error in | 158 | + scanner?.process(image) { [self] barcodes, error in |
| 163 | imagesCurrentlyBeingProcessed = false | 159 | imagesCurrentlyBeingProcessed = false |
| 164 | 160 | ||
| 165 | if (detectionSpeed == DetectionSpeed.noDuplicates) { | 161 | if (detectionSpeed == DetectionSpeed.noDuplicates) { |
| @@ -169,7 +165,9 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -169,7 +165,9 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 169 | 165 | ||
| 170 | if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) { | 166 | if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) { |
| 171 | return | 167 | return |
| 172 | - } else if (newScannedBarcodes?.isEmpty == false) { | 168 | + } |
| 169 | + | ||
| 170 | + if (newScannedBarcodes?.isEmpty == false) { | ||
| 173 | barcodesString = newScannedBarcodes | 171 | barcodesString = newScannedBarcodes |
| 174 | } | 172 | } |
| 175 | } | 173 | } |
| @@ -180,7 +178,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -180,7 +178,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 180 | } | 178 | } |
| 181 | 179 | ||
| 182 | /// Start scanning for barcodes | 180 | /// Start scanning for barcodes |
| 183 | - func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: Bool, detectionSpeed: DetectionSpeed, completion: @escaping (MobileScannerStartParameters) -> ()) throws { | 181 | + func start(barcodeScannerOptions: BarcodeScannerOptions?, cameraPosition: AVCaptureDevice.Position, torch: Bool, detectionSpeed: DetectionSpeed, completion: @escaping (MobileScannerStartParameters) -> ()) throws { |
| 184 | self.detectionSpeed = detectionSpeed | 182 | self.detectionSpeed = detectionSpeed |
| 185 | if (device != nil || captureSession != nil) { | 183 | if (device != nil || captureSession != nil) { |
| 186 | throw MobileScannerError.alreadyStarted | 184 | throw MobileScannerError.alreadyStarted |
| @@ -303,7 +301,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -303,7 +301,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 303 | completion(MobileScannerStartParameters()) | 301 | completion(MobileScannerStartParameters()) |
| 304 | } | 302 | } |
| 305 | } | 303 | } |
| 306 | - | 304 | + |
| 307 | /// Pause scanning for barcodes | 305 | /// Pause scanning for barcodes |
| 308 | func pause() throws { | 306 | func pause() throws { |
| 309 | if (paused) { | 307 | if (paused) { |
| @@ -322,13 +320,13 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -322,13 +320,13 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 322 | releaseCamera() | 320 | releaseCamera() |
| 323 | releaseTexture() | 321 | releaseTexture() |
| 324 | } | 322 | } |
| 325 | - | 323 | + |
| 326 | private func releaseCamera() { | 324 | private func releaseCamera() { |
| 327 | - | 325 | + |
| 328 | guard let captureSession = captureSession else { | 326 | guard let captureSession = captureSession else { |
| 329 | return | 327 | return |
| 330 | } | 328 | } |
| 331 | - | 329 | + |
| 332 | captureSession.stopRunning() | 330 | captureSession.stopRunning() |
| 333 | for input in captureSession.inputs { | 331 | for input in captureSession.inputs { |
| 334 | captureSession.removeInput(input) | 332 | captureSession.removeInput(input) |
| @@ -343,10 +341,11 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -343,10 +341,11 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 343 | self.captureSession = nil | 341 | self.captureSession = nil |
| 344 | device = nil | 342 | device = nil |
| 345 | } | 343 | } |
| 346 | - | 344 | + |
| 347 | private func releaseTexture() { | 345 | private func releaseTexture() { |
| 348 | registry?.unregisterTexture(textureId) | 346 | registry?.unregisterTexture(textureId) |
| 349 | textureId = nil | 347 | textureId = nil |
| 348 | + scanner = nil | ||
| 350 | } | 349 | } |
| 351 | 350 | ||
| 352 | /// Toggle the torch. | 351 | /// Toggle the torch. |
| @@ -464,7 +463,8 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -464,7 +463,8 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 464 | } | 463 | } |
| 465 | 464 | ||
| 466 | /// Analyze a single image | 465 | /// Analyze a single image |
| 467 | - func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) { | 466 | + func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, |
| 467 | + barcodeScannerOptions: BarcodeScannerOptions?, callback: @escaping BarcodeScanningCallback) { | ||
| 468 | let image = VisionImage(image: image) | 468 | let image = VisionImage(image: image) |
| 469 | image.orientation = imageOrientation( | 469 | image.orientation = imageOrientation( |
| 470 | deviceOrientation: UIDevice.current.orientation, | 470 | deviceOrientation: UIDevice.current.orientation, |
| @@ -472,22 +472,13 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -472,22 +472,13 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 472 | position: position | 472 | position: position |
| 473 | ) | 473 | ) |
| 474 | 474 | ||
| 475 | + let scanner: BarcodeScanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner() | ||
| 476 | + | ||
| 475 | scanner.process(image, completion: callback) | 477 | scanner.process(image, completion: callback) |
| 476 | } | 478 | } |
| 477 | 479 | ||
| 478 | var barcodesString: Array<String?>? | 480 | var barcodesString: Array<String?>? |
| 479 | 481 | ||
| 480 | - // /// Convert image buffer to jpeg | ||
| 481 | - // private func ciImageToJpeg(ciImage: CIImage) -> Data { | ||
| 482 | - // | ||
| 483 | - // // let ciImage = CIImage(cvPixelBuffer: latestBuffer) | ||
| 484 | - // let context:CIContext = CIContext.init(options: nil) | ||
| 485 | - // let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)! | ||
| 486 | - // let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up) | ||
| 487 | - // | ||
| 488 | - // return uiImage.jpegData(compressionQuality: 0.8)! | ||
| 489 | - // } | ||
| 490 | - | ||
| 491 | /// Rotates images accordingly | 482 | /// Rotates images accordingly |
| 492 | func imageOrientation( | 483 | func imageOrientation( |
| 493 | deviceOrientation: UIDeviceOrientation, | 484 | deviceOrientation: UIDeviceOrientation, |
| @@ -6,6 +6,12 @@ | @@ -6,6 +6,12 @@ | ||
| 6 | // | 6 | // |
| 7 | import Foundation | 7 | import Foundation |
| 8 | 8 | ||
| 9 | +// TODO: decide if we should keep or discard this enum | ||
| 10 | +// When merging the iOS / MacOS implementations we should either keep the enum or remove it | ||
| 11 | + | ||
| 12 | +// This enum is a bit of a leftover from older parts of the iOS implementation. | ||
| 13 | +// It is used by the handler that throws these error codes, | ||
| 14 | +// while the plugin class intercepts these and converts them to `FlutterError()`s. | ||
| 9 | enum MobileScannerError: Error { | 15 | enum MobileScannerError: Error { |
| 10 | case noCamera | 16 | case noCamera |
| 11 | case alreadyStarted | 17 | case alreadyStarted |
ios/Classes/MobileScannerErrorCodes.swift
0 → 100644
| 1 | +// | ||
| 2 | +// MobileScannerErrorCodes.swift | ||
| 3 | +// mobile_scanner | ||
| 4 | +// | ||
| 5 | +// Created by Navaron Bracke on 28/05/2024. | ||
| 6 | +// | ||
| 7 | + | ||
| 8 | +import Foundation | ||
| 9 | + | ||
| 10 | +/// This struct defines the error codes and error messages for MobileScanner errors. | ||
| 11 | +/// | ||
| 12 | +/// These are used by `FlutterError` as error code and error message. | ||
| 13 | +/// | ||
| 14 | +/// This struct should not be confused with `MobileScannerError`, | ||
| 15 | +/// which is an implementation detail for the iOS implementation. | ||
| 16 | +struct MobileScannerErrorCodes { | ||
| 17 | + static let ALREADY_STARTED_ERROR = "MOBILE_SCANNER_ALREADY_STARTED_ERROR" | ||
| 18 | + static let ALREADY_STARTED_ERROR_MESSAGE = "The scanner was already started." | ||
| 19 | + // The error code 'BARCODE_ERROR' does not have an error message, | ||
| 20 | + // because it uses the error message from the undelying error. | ||
| 21 | + static let BARCODE_ERROR = "MOBILE_SCANNER_BARCODE_ERROR" | ||
| 22 | + // The error code 'CAMERA_ERROR' does not have an error message, | ||
| 23 | + // because it uses the error message from the underlying error. | ||
| 24 | + static let CAMERA_ERROR = "MOBILE_SCANNER_CAMERA_ERROR" | ||
| 25 | + static let GENERIC_ERROR = "MOBILE_SCANNER_GENERIC_ERROR" | ||
| 26 | + static let GENERIC_ERROR_MESSAGE = "An unknown error occurred." | ||
| 27 | + // This message is used with the 'GENERIC_ERROR' error code. | ||
| 28 | + static let INVALID_ZOOM_SCALE_ERROR_MESSAGE = "The zoom scale should be between 0 and 1 (both inclusive)" | ||
| 29 | + static let NO_CAMERA_ERROR = "MOBILE_SCANNER_NO_CAMERA_ERROR" | ||
| 30 | + static let NO_CAMERA_ERROR_MESSAGE = "No cameras available." | ||
| 31 | + static let SET_SCALE_WHEN_STOPPED_ERROR = "MOBILE_SCANNER_SET_SCALE_WHEN_STOPPED_ERROR" | ||
| 32 | + static let SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE = "The zoom scale cannot be changed when the camera is stopped." | ||
| 33 | +} |
| @@ -11,6 +11,10 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -11,6 +11,10 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 11 | 11 | ||
| 12 | /// The handler sends all information via an event channel back to Flutter | 12 | /// The handler sends all information via an event channel back to Flutter |
| 13 | private let barcodeHandler: BarcodeHandler | 13 | private let barcodeHandler: BarcodeHandler |
| 14 | + | ||
| 15 | + /// Whether to return the input image with the barcode event. | ||
| 16 | + /// This is static to avoid accessing `self` in the callback in the constructor. | ||
| 17 | + private static var returnImage: Bool = false | ||
| 14 | 18 | ||
| 15 | /// The points for the scan window. | 19 | /// The points for the scan window. |
| 16 | static var scanWindow: [CGFloat]? | 20 | static var scanWindow: [CGFloat]? |
| @@ -37,24 +41,47 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -37,24 +41,47 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 37 | 41 | ||
| 38 | init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) { | 42 | init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) { |
| 39 | self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in | 43 | self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in |
| 40 | - if barcodes != nil { | ||
| 41 | - let barcodesMap: [Any?] = barcodes!.compactMap { barcode in | ||
| 42 | - if (MobileScannerPlugin.scanWindow != nil) { | ||
| 43 | - if (MobileScannerPlugin.isBarcodeInScanWindow(barcode: barcode, imageSize: image.size)) { | ||
| 44 | - return barcode.data | ||
| 45 | - } else { | ||
| 46 | - return nil | ||
| 47 | - } | ||
| 48 | - } else { | ||
| 49 | - return barcode.data | ||
| 50 | - } | 44 | + if error != nil { |
| 45 | + barcodeHandler.publishError( | ||
| 46 | + FlutterError(code: MobileScannerErrorCodes.BARCODE_ERROR, | ||
| 47 | + message: error?.localizedDescription, | ||
| 48 | + details: nil)) | ||
| 49 | + return | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + if barcodes == nil { | ||
| 53 | + return | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + let barcodesMap: [Any?] = barcodes!.compactMap { barcode in | ||
| 57 | + if (MobileScannerPlugin.scanWindow == nil) { | ||
| 58 | + return barcode.data | ||
| 51 | } | 59 | } |
| 52 | - if (!barcodesMap.isEmpty) { | ||
| 53 | - barcodeHandler.publishEvent(["name": "barcode", "data": barcodesMap, "image": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!), "width": image.size.width, "height": image.size.height]) | 60 | + |
| 61 | + if (MobileScannerPlugin.isBarcodeInScanWindow(barcode: barcode, imageSize: image.size)) { | ||
| 62 | + return barcode.data | ||
| 54 | } | 63 | } |
| 55 | - } else if (error != nil){ | ||
| 56 | - barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription]) | 64 | + |
| 65 | + return nil | ||
| 57 | } | 66 | } |
| 67 | + | ||
| 68 | + if (barcodesMap.isEmpty) { | ||
| 69 | + return | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + // The image dimensions are always provided. | ||
| 73 | + // The image bytes are only non-null when `returnImage` is true. | ||
| 74 | + let imageData: [String: Any?] = [ | ||
| 75 | + "bytes": MobileScannerPlugin.returnImage ? FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!) : nil, | ||
| 76 | + "width": image.size.width, | ||
| 77 | + "height": image.size.height, | ||
| 78 | + ] | ||
| 79 | + | ||
| 80 | + barcodeHandler.publishEvent([ | ||
| 81 | + "name": "barcode", | ||
| 82 | + "data": barcodesMap, | ||
| 83 | + "image": imageData, | ||
| 84 | + ]) | ||
| 58 | }, torchModeChangeCallback: { torchState in | 85 | }, torchModeChangeCallback: { torchState in |
| 59 | barcodeHandler.publishEvent(["name": "torchState", "data": torchState]) | 86 | barcodeHandler.publishEvent(["name": "torchState", "data": torchState]) |
| 60 | }, zoomScaleChangeCallback: { zoomScale in | 87 | }, zoomScaleChangeCallback: { zoomScale in |
| @@ -106,23 +133,15 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -106,23 +133,15 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 106 | let speed: Int = (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0 | 133 | let speed: Int = (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0 |
| 107 | let timeoutMs: Int = (call.arguments as! Dictionary<String, Any?>)["timeout"] as? Int ?? 0 | 134 | let timeoutMs: Int = (call.arguments as! Dictionary<String, Any?>)["timeout"] as? Int ?? 0 |
| 108 | self.mobileScanner.timeoutSeconds = Double(timeoutMs) / Double(1000) | 135 | self.mobileScanner.timeoutSeconds = Double(timeoutMs) / Double(1000) |
| 136 | + MobileScannerPlugin.returnImage = returnImage | ||
| 109 | 137 | ||
| 110 | - let formatList = formats.map { format in return BarcodeFormat(rawValue: format)} | ||
| 111 | - var barcodeOptions: BarcodeScannerOptions? = nil | ||
| 112 | - | ||
| 113 | - if (formatList.count != 0) { | ||
| 114 | - var barcodeFormats: BarcodeFormat = [] | ||
| 115 | - for index in formats { | ||
| 116 | - barcodeFormats.insert(BarcodeFormat(rawValue: index)) | ||
| 117 | - } | ||
| 118 | - barcodeOptions = BarcodeScannerOptions(formats: barcodeFormats) | ||
| 119 | - } | 138 | + let barcodeOptions: BarcodeScannerOptions? = buildBarcodeScannerOptions(formats) |
| 120 | 139 | ||
| 121 | let position = facing == 0 ? AVCaptureDevice.Position.front : .back | 140 | let position = facing == 0 ? AVCaptureDevice.Position.front : .back |
| 122 | let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)! | 141 | let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)! |
| 123 | 142 | ||
| 124 | do { | 143 | do { |
| 125 | - try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch, detectionSpeed: detectionSpeed) { parameters in | 144 | + try mobileScanner.start(barcodeScannerOptions: barcodeOptions, cameraPosition: position, torch: torch, detectionSpeed: detectionSpeed) { parameters in |
| 126 | DispatchQueue.main.async { | 145 | DispatchQueue.main.async { |
| 127 | result([ | 146 | result([ |
| 128 | "textureId": parameters.textureId, | 147 | "textureId": parameters.textureId, |
| @@ -132,20 +151,20 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -132,20 +151,20 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 132 | } | 151 | } |
| 133 | } | 152 | } |
| 134 | } catch MobileScannerError.alreadyStarted { | 153 | } catch MobileScannerError.alreadyStarted { |
| 135 | - result(FlutterError(code: "MobileScanner", | ||
| 136 | - message: "Called start() while already started!", | 154 | + result(FlutterError(code: MobileScannerErrorCodes.ALREADY_STARTED_ERROR, |
| 155 | + message: MobileScannerErrorCodes.ALREADY_STARTED_ERROR_MESSAGE, | ||
| 137 | details: nil)) | 156 | details: nil)) |
| 138 | } catch MobileScannerError.noCamera { | 157 | } catch MobileScannerError.noCamera { |
| 139 | - result(FlutterError(code: "MobileScanner", | ||
| 140 | - message: "No camera found or failed to open camera!", | 158 | + result(FlutterError(code: MobileScannerErrorCodes.NO_CAMERA_ERROR, |
| 159 | + message: MobileScannerErrorCodes.NO_CAMERA_ERROR_MESSAGE, | ||
| 141 | details: nil)) | 160 | details: nil)) |
| 142 | } catch MobileScannerError.cameraError(let error) { | 161 | } catch MobileScannerError.cameraError(let error) { |
| 143 | - result(FlutterError(code: "MobileScanner", | ||
| 144 | - message: "Error occured when setting up camera!", | ||
| 145 | - details: error)) | 162 | + result(FlutterError(code: MobileScannerErrorCodes.CAMERA_ERROR, |
| 163 | + message: error.localizedDescription, | ||
| 164 | + details: nil)) | ||
| 146 | } catch { | 165 | } catch { |
| 147 | - result(FlutterError(code: "MobileScanner", | ||
| 148 | - message: "Unknown error occured.", | 166 | + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR, |
| 167 | + message: MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE, | ||
| 149 | details: nil)) | 168 | details: nil)) |
| 150 | } | 169 | } |
| 151 | } | 170 | } |
| @@ -176,25 +195,25 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -176,25 +195,25 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 176 | private func setScale(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | 195 | private func setScale(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
| 177 | let scale = call.arguments as? CGFloat | 196 | let scale = call.arguments as? CGFloat |
| 178 | if (scale == nil) { | 197 | if (scale == nil) { |
| 179 | - result(FlutterError(code: "MobileScanner", | ||
| 180 | - message: "You must provide a scale when calling setScale!", | ||
| 181 | - details: nil)) | 198 | + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR, |
| 199 | + message: MobileScannerErrorCodes.INVALID_ZOOM_SCALE_ERROR_MESSAGE, | ||
| 200 | + details: "The invalid zoom scale was nil.")) | ||
| 182 | return | 201 | return |
| 183 | } | 202 | } |
| 184 | do { | 203 | do { |
| 185 | try mobileScanner.setScale(scale!) | 204 | try mobileScanner.setScale(scale!) |
| 186 | result(nil) | 205 | result(nil) |
| 187 | } catch MobileScannerError.zoomWhenStopped { | 206 | } catch MobileScannerError.zoomWhenStopped { |
| 188 | - result(FlutterError(code: "MobileScanner", | ||
| 189 | - message: "Called setScale() while stopped!", | 207 | + result(FlutterError(code: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR, |
| 208 | + message: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE, | ||
| 190 | details: nil)) | 209 | details: nil)) |
| 191 | } catch MobileScannerError.zoomError(let error) { | 210 | } catch MobileScannerError.zoomError(let error) { |
| 192 | - result(FlutterError(code: "MobileScanner", | ||
| 193 | - message: "Error while zooming.", | ||
| 194 | - details: error)) | 211 | + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR, |
| 212 | + message: error.localizedDescription, | ||
| 213 | + details: nil)) | ||
| 195 | } catch { | 214 | } catch { |
| 196 | - result(FlutterError(code: "MobileScanner", | ||
| 197 | - message: "Error while zooming.", | 215 | + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR, |
| 216 | + message: MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE, | ||
| 198 | details: nil)) | 217 | details: nil)) |
| 199 | } | 218 | } |
| 200 | } | 219 | } |
| @@ -205,16 +224,16 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -205,16 +224,16 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 205 | try mobileScanner.resetScale() | 224 | try mobileScanner.resetScale() |
| 206 | result(nil) | 225 | result(nil) |
| 207 | } catch MobileScannerError.zoomWhenStopped { | 226 | } catch MobileScannerError.zoomWhenStopped { |
| 208 | - result(FlutterError(code: "MobileScanner", | ||
| 209 | - message: "Called resetScale() while stopped!", | 227 | + result(FlutterError(code: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR, |
| 228 | + message: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE, | ||
| 210 | details: nil)) | 229 | details: nil)) |
| 211 | } catch MobileScannerError.zoomError(let error) { | 230 | } catch MobileScannerError.zoomError(let error) { |
| 212 | - result(FlutterError(code: "MobileScanner", | ||
| 213 | - message: "Error while zooming.", | ||
| 214 | - details: error)) | 231 | + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR, |
| 232 | + message: error.localizedDescription, | ||
| 233 | + details: nil)) | ||
| 215 | } catch { | 234 | } catch { |
| 216 | - result(FlutterError(code: "MobileScanner", | ||
| 217 | - message: "Error while zooming.", | 235 | + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR, |
| 236 | + message: MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE, | ||
| 218 | details: nil)) | 237 | details: nil)) |
| 219 | } | 238 | } |
| 220 | } | 239 | } |
| @@ -243,19 +262,20 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -243,19 +262,20 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 243 | 262 | ||
| 244 | /// Analyzes a single image. | 263 | /// Analyzes a single image. |
| 245 | private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | 264 | private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
| 246 | - let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "") | 265 | + let formats: Array<Int> = (call.arguments as! Dictionary<String, Any?>)["formats"] as? Array ?? [] |
| 266 | + let scannerOptions: BarcodeScannerOptions? = buildBarcodeScannerOptions(formats) | ||
| 267 | + let uiImage = UIImage(contentsOfFile: (call.arguments as! Dictionary<String, Any?>)["filePath"] as? String ?? "") | ||
| 247 | 268 | ||
| 248 | if (uiImage == nil) { | 269 | if (uiImage == nil) { |
| 249 | - result(FlutterError(code: "MobileScanner", | ||
| 250 | - message: "No image found in analyzeImage!", | ||
| 251 | - details: nil)) | 270 | + result(nil) |
| 252 | return | 271 | return |
| 253 | } | 272 | } |
| 254 | 273 | ||
| 255 | - mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { barcodes, error in | 274 | + mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, |
| 275 | + barcodeScannerOptions: scannerOptions, callback: { barcodes, error in | ||
| 256 | if error != nil { | 276 | if error != nil { |
| 257 | DispatchQueue.main.async { | 277 | DispatchQueue.main.async { |
| 258 | - result(FlutterError(code: "MobileScanner", | 278 | + result(FlutterError(code: MobileScannerErrorCodes.BARCODE_ERROR, |
| 259 | message: error?.localizedDescription, | 279 | message: error?.localizedDescription, |
| 260 | details: nil)) | 280 | details: nil)) |
| 261 | } | 281 | } |
| @@ -267,13 +287,29 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -267,13 +287,29 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 267 | DispatchQueue.main.async { | 287 | DispatchQueue.main.async { |
| 268 | result(nil) | 288 | result(nil) |
| 269 | } | 289 | } |
| 270 | - } else { | ||
| 271 | - let barcodesMap: [Any?] = barcodes!.compactMap { barcode in barcode.data } | ||
| 272 | 290 | ||
| 273 | - DispatchQueue.main.async { | ||
| 274 | - result(["name": "barcode", "data": barcodesMap]) | ||
| 275 | - } | 291 | + return |
| 292 | + } | ||
| 293 | + | ||
| 294 | + let barcodesMap: [Any?] = barcodes!.compactMap { barcode in barcode.data } | ||
| 295 | + | ||
| 296 | + DispatchQueue.main.async { | ||
| 297 | + result(["name": "barcode", "data": barcodesMap]) | ||
| 276 | } | 298 | } |
| 277 | }) | 299 | }) |
| 278 | } | 300 | } |
| 301 | + | ||
| 302 | + private func buildBarcodeScannerOptions(_ formats: [Int]) -> BarcodeScannerOptions? { | ||
| 303 | + guard !formats.isEmpty else { | ||
| 304 | + return nil | ||
| 305 | + } | ||
| 306 | + | ||
| 307 | + var barcodeFormats: BarcodeFormat = [] | ||
| 308 | + | ||
| 309 | + for format in formats { | ||
| 310 | + barcodeFormats.insert(BarcodeFormat(rawValue: format)) | ||
| 311 | + } | ||
| 312 | + | ||
| 313 | + return BarcodeScannerOptions(formats: barcodeFormats) | ||
| 314 | + } | ||
| 279 | } | 315 | } |
| @@ -29,8 +29,27 @@ extension UIDeviceOrientation { | @@ -29,8 +29,27 @@ extension UIDeviceOrientation { | ||
| 29 | 29 | ||
| 30 | extension Barcode { | 30 | extension Barcode { |
| 31 | var data: [String: Any?] { | 31 | var data: [String: Any?] { |
| 32 | - let corners = cornerPoints?.map({$0.cgPointValue.data}) | ||
| 33 | - return ["corners": corners, "format": format.rawValue, "rawBytes": rawData, "rawValue": rawValue, "type": valueType.rawValue, "calendarEvent": calendarEvent?.data, "contactInfo": contactInfo?.data, "driverLicense": driverLicense?.data, "email": email?.data, "geoPoint": geoPoint?.data, "phone": phone?.data, "sms": sms?.data, "url": url?.data, "wifi": wifi?.data, "displayValue": displayValue] | 32 | + return [ |
| 33 | + "calendarEvent": calendarEvent?.data, | ||
| 34 | + "contactInfo": contactInfo?.data, | ||
| 35 | + "corners": cornerPoints?.map({$0.cgPointValue.data}), | ||
| 36 | + "displayValue": displayValue, | ||
| 37 | + "driverLicense": driverLicense?.data, | ||
| 38 | + "email": email?.data, | ||
| 39 | + "format": format.rawValue, | ||
| 40 | + "geoPoint": geoPoint?.data, | ||
| 41 | + "phone": phone?.data, | ||
| 42 | + "rawBytes": rawData, | ||
| 43 | + "rawValue": rawValue, | ||
| 44 | + "size": frame.isNull ? nil : [ | ||
| 45 | + "width": frame.width, | ||
| 46 | + "height": frame.height, | ||
| 47 | + ], | ||
| 48 | + "sms": sms?.data, | ||
| 49 | + "type": valueType.rawValue, | ||
| 50 | + "url": url?.data, | ||
| 51 | + "wifi": wifi?.data, | ||
| 52 | + ] | ||
| 34 | } | 53 | } |
| 35 | } | 54 | } |
| 36 | 55 |
| @@ -4,7 +4,7 @@ | @@ -4,7 +4,7 @@ | ||
| 4 | # | 4 | # |
| 5 | Pod::Spec.new do |s| | 5 | Pod::Spec.new do |s| |
| 6 | s.name = 'mobile_scanner' | 6 | s.name = 'mobile_scanner' |
| 7 | - s.version = '5.1.1' | 7 | + s.version = '6.0.2' |
| 8 | s.summary = 'An universal scanner for Flutter based on MLKit.' | 8 | s.summary = 'An universal scanner for Flutter based on MLKit.' |
| 9 | s.description = <<-DESC | 9 | s.description = <<-DESC |
| 10 | An universal scanner for Flutter based on MLKit. | 10 | An universal scanner for Flutter based on MLKit. |
| @@ -15,11 +15,16 @@ An universal scanner for Flutter based on MLKit. | @@ -15,11 +15,16 @@ An universal scanner for Flutter based on MLKit. | ||
| 15 | s.source = { :path => '.' } | 15 | s.source = { :path => '.' } |
| 16 | s.source_files = 'Classes/**/*' | 16 | s.source_files = 'Classes/**/*' |
| 17 | s.dependency 'Flutter' | 17 | s.dependency 'Flutter' |
| 18 | - s.dependency 'GoogleMLKit/BarcodeScanning', '~> 6.0.0' | ||
| 19 | - s.platform = :ios, '12.0' | 18 | + s.dependency 'GoogleMLKit/BarcodeScanning', '~> 7.0.0' |
| 19 | + s.platform = :ios, '15.5.0' | ||
| 20 | s.static_framework = true | 20 | s.static_framework = true |
| 21 | - # Flutter.framework does not contain a i386 slice. | ||
| 22 | - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } | 21 | + # Flutter.framework does not contain a i386 slice, and MLKit does not support armv7. |
| 22 | + s.pod_target_xcconfig = { | ||
| 23 | + 'DEFINES_MODULE' => 'YES', | ||
| 24 | + # TODO: add back arm64 (and armv7?) when switching to the Vision API. | ||
| 25 | + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386 armv7 arm64', | ||
| 26 | + 'EXCLUDED_ARCHS[sdk=iphoneos*]' => 'armv7', | ||
| 27 | + } | ||
| 23 | s.swift_version = '5.0' | 28 | s.swift_version = '5.0' |
| 24 | s.resource_bundles = { 'mobile_scanner_privacy' => ['Resources/PrivacyInfo.xcprivacy'] } | 29 | s.resource_bundles = { 'mobile_scanner_privacy' => ['Resources/PrivacyInfo.xcprivacy'] } |
| 25 | end | 30 | end |
| @@ -11,8 +11,7 @@ export 'src/enums/phone_type.dart'; | @@ -11,8 +11,7 @@ export 'src/enums/phone_type.dart'; | ||
| 11 | export 'src/enums/torch_state.dart'; | 11 | export 'src/enums/torch_state.dart'; |
| 12 | export 'src/mobile_scanner.dart'; | 12 | export 'src/mobile_scanner.dart'; |
| 13 | export 'src/mobile_scanner_controller.dart'; | 13 | export 'src/mobile_scanner_controller.dart'; |
| 14 | -export 'src/mobile_scanner_exception.dart' | ||
| 15 | - hide PermissionRequestPendingException; | 14 | +export 'src/mobile_scanner_exception.dart'; |
| 16 | export 'src/mobile_scanner_platform_interface.dart'; | 15 | export 'src/mobile_scanner_platform_interface.dart'; |
| 17 | export 'src/objects/address.dart'; | 16 | export 'src/objects/address.dart'; |
| 18 | export 'src/objects/barcode.dart'; | 17 | export 'src/objects/barcode.dart'; |
| @@ -20,7 +20,7 @@ enum AddressType { | @@ -20,7 +20,7 @@ enum AddressType { | ||
| 20 | case 2: | 20 | case 2: |
| 21 | return AddressType.home; | 21 | return AddressType.home; |
| 22 | default: | 22 | default: |
| 23 | - throw ArgumentError.value(value, 'value', 'Invalid raw value.'); | 23 | + return AddressType.unknown; |
| 24 | } | 24 | } |
| 25 | } | 25 | } |
| 26 | 26 |
| @@ -70,7 +70,7 @@ enum BarcodeType { | @@ -70,7 +70,7 @@ enum BarcodeType { | ||
| 70 | case 12: | 70 | case 12: |
| 71 | return BarcodeType.driverLicense; | 71 | return BarcodeType.driverLicense; |
| 72 | default: | 72 | default: |
| 73 | - throw ArgumentError.value(value, 'value', 'Invalid raw value.'); | 73 | + return BarcodeType.unknown; |
| 74 | } | 74 | } |
| 75 | } | 75 | } |
| 76 | 76 |
| 1 | /// Wifi encryption type constants. | 1 | /// Wifi encryption type constants. |
| 2 | enum EncryptionType { | 2 | enum EncryptionType { |
| 3 | /// Unknown encryption type. | 3 | /// Unknown encryption type. |
| 4 | - none(0), | 4 | + unknown(0), |
| 5 | 5 | ||
| 6 | /// Not encrypted. | 6 | /// Not encrypted. |
| 7 | open(1), | 7 | open(1), |
| @@ -14,10 +14,15 @@ enum EncryptionType { | @@ -14,10 +14,15 @@ enum EncryptionType { | ||
| 14 | 14 | ||
| 15 | const EncryptionType(this.rawValue); | 15 | const EncryptionType(this.rawValue); |
| 16 | 16 | ||
| 17 | + @Deprecated( | ||
| 18 | + 'EncryptionType.none is deprecated. Use EncryptionType.unknown instead.', | ||
| 19 | + ) | ||
| 20 | + static const EncryptionType none = EncryptionType.unknown; | ||
| 21 | + | ||
| 17 | factory EncryptionType.fromRawValue(int value) { | 22 | factory EncryptionType.fromRawValue(int value) { |
| 18 | switch (value) { | 23 | switch (value) { |
| 19 | case 0: | 24 | case 0: |
| 20 | - return EncryptionType.none; | 25 | + return EncryptionType.unknown; |
| 21 | case 1: | 26 | case 1: |
| 22 | return EncryptionType.open; | 27 | return EncryptionType.open; |
| 23 | case 2: | 28 | case 2: |
| @@ -25,7 +30,7 @@ enum EncryptionType { | @@ -25,7 +30,7 @@ enum EncryptionType { | ||
| 25 | case 3: | 30 | case 3: |
| 26 | return EncryptionType.wep; | 31 | return EncryptionType.wep; |
| 27 | default: | 32 | default: |
| 28 | - throw ArgumentError.value(value, 'value', 'Invalid raw value.'); | 33 | + return EncryptionType.unknown; |
| 29 | } | 34 | } |
| 30 | } | 35 | } |
| 31 | 36 |
| 1 | +import 'package:flutter/services.dart'; | ||
| 1 | import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; | 2 | import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; |
| 2 | 3 | ||
| 3 | /// This enum defines the different error codes for the mobile scanner. | 4 | /// This enum defines the different error codes for the mobile scanner. |
| @@ -24,5 +25,25 @@ enum MobileScannerErrorCode { | @@ -24,5 +25,25 @@ enum MobileScannerErrorCode { | ||
| 24 | permissionDenied, | 25 | permissionDenied, |
| 25 | 26 | ||
| 26 | /// Scanning is unsupported on the current device. | 27 | /// Scanning is unsupported on the current device. |
| 27 | - unsupported, | 28 | + unsupported; |
| 29 | + | ||
| 30 | + /// Convert the given [PlatformException.code] to a [MobileScannerErrorCode]. | ||
| 31 | + factory MobileScannerErrorCode.fromPlatformException( | ||
| 32 | + PlatformException exception, | ||
| 33 | + ) { | ||
| 34 | + // The following error code mapping should be kept in sync with their native counterparts. | ||
| 35 | + // These are located in `MobileScannerErrorCodes.kt` and `MobileScannerErrorCodes.swift`. | ||
| 36 | + return switch (exception.code) { | ||
| 37 | + // In case the scanner was already started, report the right error code. | ||
| 38 | + // If the scanner is already starting, | ||
| 39 | + // this error code is a signal to the controller to just ignore the attempt. | ||
| 40 | + 'MOBILE_SCANNER_ALREADY_STARTED_ERROR' => | ||
| 41 | + MobileScannerErrorCode.controllerAlreadyInitialized, | ||
| 42 | + // In case no cameras are available, using the scanner is not supported. | ||
| 43 | + 'MOBILE_SCANNER_NO_CAMERA_ERROR' => MobileScannerErrorCode.unsupported, | ||
| 44 | + 'MOBILE_SCANNER_CAMERA_PERMISSION_DENIED' => | ||
| 45 | + MobileScannerErrorCode.permissionDenied, | ||
| 46 | + _ => MobileScannerErrorCode.genericError, | ||
| 47 | + }; | ||
| 48 | + } | ||
| 28 | } | 49 | } |
| @@ -16,6 +16,14 @@ import 'package:mobile_scanner/src/objects/start_options.dart'; | @@ -16,6 +16,14 @@ import 'package:mobile_scanner/src/objects/start_options.dart'; | ||
| 16 | 16 | ||
| 17 | /// An implementation of [MobileScannerPlatform] that uses method channels. | 17 | /// An implementation of [MobileScannerPlatform] that uses method channels. |
| 18 | class MethodChannelMobileScanner extends MobileScannerPlatform { | 18 | class MethodChannelMobileScanner extends MobileScannerPlatform { |
| 19 | + /// The name of the barcode event that is sent when a barcode is scanned. | ||
| 20 | + @visibleForTesting | ||
| 21 | + static const String kBarcodeEventName = 'barcode'; | ||
| 22 | + | ||
| 23 | + /// The name of the error event that is sent when a barcode scan error occurs. | ||
| 24 | + @visibleForTesting | ||
| 25 | + static const String kBarcodeErrorEventName = 'MOBILE_SCANNER_BARCODE_ERROR'; | ||
| 26 | + | ||
| 19 | /// The method channel used to interact with the native platform. | 27 | /// The method channel used to interact with the native platform. |
| 20 | @visibleForTesting | 28 | @visibleForTesting |
| 21 | final methodChannel = const MethodChannel( | 29 | final methodChannel = const MethodChannel( |
| @@ -55,31 +63,19 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -55,31 +63,19 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 55 | final List<Map<Object?, Object?>> barcodes = | 63 | final List<Map<Object?, Object?>> barcodes = |
| 56 | data.cast<Map<Object?, Object?>>(); | 64 | data.cast<Map<Object?, Object?>>(); |
| 57 | 65 | ||
| 58 | - if (defaultTargetPlatform == TargetPlatform.macOS) { | ||
| 59 | - return BarcodeCapture( | ||
| 60 | - raw: event, | ||
| 61 | - barcodes: barcodes | ||
| 62 | - .map( | ||
| 63 | - (barcode) => Barcode( | ||
| 64 | - rawValue: barcode['payload'] as String?, | ||
| 65 | - format: BarcodeFormat.fromRawValue( | ||
| 66 | - barcode['symbology'] as int? ?? -1, | ||
| 67 | - ), | ||
| 68 | - ), | ||
| 69 | - ) | ||
| 70 | - .toList(), | ||
| 71 | - ); | ||
| 72 | - } | ||
| 73 | - | ||
| 74 | if (defaultTargetPlatform == TargetPlatform.android || | 66 | if (defaultTargetPlatform == TargetPlatform.android || |
| 75 | - defaultTargetPlatform == TargetPlatform.iOS) { | ||
| 76 | - final double? width = event['width'] as double?; | ||
| 77 | - final double? height = event['height'] as double?; | 67 | + defaultTargetPlatform == TargetPlatform.iOS || |
| 68 | + defaultTargetPlatform == TargetPlatform.macOS) { | ||
| 69 | + final Map<Object?, Object?>? imageData = | ||
| 70 | + event['image'] as Map<Object?, Object?>?; | ||
| 71 | + final Uint8List? image = imageData?['bytes'] as Uint8List?; | ||
| 72 | + final double? width = imageData?['width'] as double?; | ||
| 73 | + final double? height = imageData?['height'] as double?; | ||
| 78 | 74 | ||
| 79 | return BarcodeCapture( | 75 | return BarcodeCapture( |
| 80 | - raw: data, | 76 | + raw: event, |
| 81 | barcodes: barcodes.map(Barcode.fromNative).toList(), | 77 | barcodes: barcodes.map(Barcode.fromNative).toList(), |
| 82 | - image: event['image'] as Uint8List?, | 78 | + image: image, |
| 83 | size: width == null || height == null ? Size.zero : Size(width, height), | 79 | size: width == null || height == null ? Size.zero : Size(width, height), |
| 84 | ); | 80 | ); |
| 85 | } | 81 | } |
| @@ -92,6 +88,19 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -92,6 +88,19 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 92 | ); | 88 | ); |
| 93 | } | 89 | } |
| 94 | 90 | ||
| 91 | + /// Parse a [MobileScannerBarcodeException] from the given [error] and [stackTrace], and throw it. | ||
| 92 | + /// | ||
| 93 | + /// If the error is not a [PlatformException], | ||
| 94 | + /// with [kBarcodeErrorEventName] as [PlatformException.code], the error is rethrown as-is. | ||
| 95 | + Never _parseBarcodeError(Object error, StackTrace stackTrace) { | ||
| 96 | + if (error case PlatformException(:final String code, :final String? message) | ||
| 97 | + when code == kBarcodeErrorEventName) { | ||
| 98 | + throw MobileScannerBarcodeException(message); | ||
| 99 | + } | ||
| 100 | + | ||
| 101 | + Error.throwWithStackTrace(error, stackTrace); | ||
| 102 | + } | ||
| 103 | + | ||
| 95 | /// Request permission to access the camera. | 104 | /// Request permission to access the camera. |
| 96 | /// | 105 | /// |
| 97 | /// Throws a [MobileScannerException] if the permission is not granted. | 106 | /// Throws a [MobileScannerException] if the permission is not granted. |
| @@ -134,9 +143,12 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -134,9 +143,12 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 134 | 143 | ||
| 135 | @override | 144 | @override |
| 136 | Stream<BarcodeCapture?> get barcodesStream { | 145 | Stream<BarcodeCapture?> get barcodesStream { |
| 146 | + // Handle incoming barcode events. | ||
| 147 | + // The error events are transformed to `MobileScannerBarcodeException` where possible. | ||
| 137 | return eventsStream | 148 | return eventsStream |
| 138 | - .where((event) => event['name'] == 'barcode') | ||
| 139 | - .map((event) => _parseBarcode(event)); | 149 | + .where((e) => e['name'] == kBarcodeEventName) |
| 150 | + .map((event) => _parseBarcode(event)) | ||
| 151 | + .handleError(_parseBarcodeError); | ||
| 140 | } | 152 | } |
| 141 | 153 | ||
| 142 | @override | 154 | @override |
| @@ -154,14 +166,34 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -154,14 +166,34 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 154 | } | 166 | } |
| 155 | 167 | ||
| 156 | @override | 168 | @override |
| 157 | - Future<BarcodeCapture?> analyzeImage(String path) async { | ||
| 158 | - final Map<String, Object?>? result = | ||
| 159 | - await methodChannel.invokeMapMethod<String, Object?>( | ||
| 160 | - 'analyzeImage', | ||
| 161 | - path, | ||
| 162 | - ); | 169 | + Future<BarcodeCapture?> analyzeImage( |
| 170 | + String path, { | ||
| 171 | + List<BarcodeFormat> formats = const <BarcodeFormat>[], | ||
| 172 | + }) async { | ||
| 173 | + try { | ||
| 174 | + final Map<Object?, Object?>? result = | ||
| 175 | + await methodChannel.invokeMapMethod<Object?, Object?>( | ||
| 176 | + 'analyzeImage', | ||
| 177 | + { | ||
| 178 | + 'filePath': path, | ||
| 179 | + 'formats': formats.isEmpty | ||
| 180 | + ? null | ||
| 181 | + : [ | ||
| 182 | + for (final BarcodeFormat format in formats) | ||
| 183 | + if (format != BarcodeFormat.unknown) format.rawValue, | ||
| 184 | + ], | ||
| 185 | + }, | ||
| 186 | + ); | ||
| 163 | 187 | ||
| 164 | - return _parseBarcode(result); | 188 | + return _parseBarcode(result); |
| 189 | + } on PlatformException catch (error) { | ||
| 190 | + // Handle any errors from analyze image requests. | ||
| 191 | + if (error.code == kBarcodeErrorEventName) { | ||
| 192 | + throw MobileScannerBarcodeException(error.message); | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + return null; | ||
| 196 | + } | ||
| 165 | } | 197 | } |
| 166 | 198 | ||
| 167 | @override | 199 | @override |
| @@ -189,8 +221,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -189,8 +221,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 189 | throw const MobileScannerException( | 221 | throw const MobileScannerException( |
| 190 | errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, | 222 | errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, |
| 191 | errorDetails: MobileScannerErrorDetails( | 223 | errorDetails: MobileScannerErrorDetails( |
| 192 | - message: | ||
| 193 | - 'The scanner was already started. Call stop() before calling start() again.', | 224 | + message: 'The scanner was already started.', |
| 194 | ), | 225 | ), |
| 195 | ); | 226 | ); |
| 196 | } | 227 | } |
| @@ -206,7 +237,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -206,7 +237,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 206 | ); | 237 | ); |
| 207 | } on PlatformException catch (error) { | 238 | } on PlatformException catch (error) { |
| 208 | throw MobileScannerException( | 239 | throw MobileScannerException( |
| 209 | - errorCode: MobileScannerErrorCode.genericError, | 240 | + errorCode: MobileScannerErrorCode.fromPlatformException(error), |
| 210 | errorDetails: MobileScannerErrorDetails( | 241 | errorDetails: MobileScannerErrorDetails( |
| 211 | code: error.code, | 242 | code: error.code, |
| 212 | details: error.details as Object?, | 243 | details: error.details as Object?, |
| @@ -242,17 +273,13 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -242,17 +273,13 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 242 | startResult['currentTorchState'] as int? ?? -1, | 273 | startResult['currentTorchState'] as int? ?? -1, |
| 243 | ); | 274 | ); |
| 244 | 275 | ||
| 245 | - final Map<Object?, Object?>? sizeInfo = | ||
| 246 | - startResult['size'] as Map<Object?, Object?>?; | ||
| 247 | - final double? width = sizeInfo?['width'] as double?; | ||
| 248 | - final double? height = sizeInfo?['height'] as double?; | ||
| 249 | - | ||
| 250 | final Size size; | 276 | final Size size; |
| 251 | 277 | ||
| 252 | - if (width == null || height == null) { | ||
| 253 | - size = Size.zero; | ||
| 254 | - } else { | 278 | + if (startResult['size'] |
| 279 | + case {'width': final double width, 'height': final double height}) { | ||
| 255 | size = Size(width, height); | 280 | size = Size(width, height); |
| 281 | + } else { | ||
| 282 | + size = Size.zero; | ||
| 256 | } | 283 | } |
| 257 | 284 | ||
| 258 | _pausing = false; | 285 | _pausing = false; |
| @@ -21,6 +21,7 @@ class MobileScanner extends StatefulWidget { | @@ -21,6 +21,7 @@ class MobileScanner extends StatefulWidget { | ||
| 21 | const MobileScanner({ | 21 | const MobileScanner({ |
| 22 | this.controller, | 22 | this.controller, |
| 23 | this.onDetect, | 23 | this.onDetect, |
| 24 | + this.onDetectError = _onDetectErrorHandler, | ||
| 24 | this.fit = BoxFit.cover, | 25 | this.fit = BoxFit.cover, |
| 25 | this.errorBuilder, | 26 | this.errorBuilder, |
| 26 | this.overlayBuilder, | 27 | this.overlayBuilder, |
| @@ -34,9 +35,17 @@ class MobileScanner extends StatefulWidget { | @@ -34,9 +35,17 @@ class MobileScanner extends StatefulWidget { | ||
| 34 | final MobileScannerController? controller; | 35 | final MobileScannerController? controller; |
| 35 | 36 | ||
| 36 | /// The function that signals when new codes were detected by the [controller]. | 37 | /// The function that signals when new codes were detected by the [controller]. |
| 37 | - /// If null, use the controller.barcodes stream directly to capture barcodes. | 38 | + /// |
| 39 | + /// To handle both [BarcodeCapture]s and [MobileScannerBarcodeException]s, | ||
| 40 | + /// use the [MobileScannerController.barcodes] stream directly (recommended), | ||
| 41 | + /// or provide a function to [onDetectError]. | ||
| 38 | final void Function(BarcodeCapture barcodes)? onDetect; | 42 | final void Function(BarcodeCapture barcodes)? onDetect; |
| 39 | 43 | ||
| 44 | + /// The error handler equivalent for the [onDetect] function. | ||
| 45 | + /// | ||
| 46 | + /// If [onDetect] is not null, and this is null, errors are silently ignored. | ||
| 47 | + final void Function(Object error, StackTrace stackTrace) onDetectError; | ||
| 48 | + | ||
| 40 | /// The error builder for the camera preview. | 49 | /// The error builder for the camera preview. |
| 41 | /// | 50 | /// |
| 42 | /// If this is null, a black [ColoredBox], | 51 | /// If this is null, a black [ColoredBox], |
| @@ -116,6 +125,11 @@ class MobileScanner extends StatefulWidget { | @@ -116,6 +125,11 @@ class MobileScanner extends StatefulWidget { | ||
| 116 | 125 | ||
| 117 | @override | 126 | @override |
| 118 | State<MobileScanner> createState() => _MobileScannerState(); | 127 | State<MobileScanner> createState() => _MobileScannerState(); |
| 128 | + | ||
| 129 | + /// This empty function is used as the default error handler for [onDetect]. | ||
| 130 | + static void _onDetectErrorHandler(Object error, StackTrace stackTrace) { | ||
| 131 | + // Do nothing. | ||
| 132 | + } | ||
| 119 | } | 133 | } |
| 120 | 134 | ||
| 121 | class _MobileScannerState extends State<MobileScanner> | 135 | class _MobileScannerState extends State<MobileScanner> |
| @@ -249,7 +263,11 @@ class _MobileScannerState extends State<MobileScanner> | @@ -249,7 +263,11 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 249 | void initState() { | 263 | void initState() { |
| 250 | if (widget.onDetect != null) { | 264 | if (widget.onDetect != null) { |
| 251 | WidgetsBinding.instance.addObserver(this); | 265 | WidgetsBinding.instance.addObserver(this); |
| 252 | - _subscription = controller.barcodes.listen(widget.onDetect); | 266 | + _subscription = controller.barcodes.listen( |
| 267 | + widget.onDetect, | ||
| 268 | + onError: widget.onDetectError, | ||
| 269 | + cancelOnError: false, | ||
| 270 | + ); | ||
| 253 | } | 271 | } |
| 254 | if (controller.autoStart) { | 272 | if (controller.autoStart) { |
| 255 | controller.start(); | 273 | controller.start(); |
| @@ -281,8 +299,7 @@ class _MobileScannerState extends State<MobileScanner> | @@ -281,8 +299,7 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 281 | 299 | ||
| 282 | @override | 300 | @override |
| 283 | void didChangeAppLifecycleState(AppLifecycleState state) { | 301 | void didChangeAppLifecycleState(AppLifecycleState state) { |
| 284 | - if (widget.controller != null) return; | ||
| 285 | - if (!controller.value.isInitialized) { | 302 | + if (widget.controller != null || !controller.value.hasCameraPermission) { |
| 286 | return; | 303 | return; |
| 287 | } | 304 | } |
| 288 | 305 | ||
| @@ -292,7 +309,11 @@ class _MobileScannerState extends State<MobileScanner> | @@ -292,7 +309,11 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 292 | case AppLifecycleState.paused: | 309 | case AppLifecycleState.paused: |
| 293 | return; | 310 | return; |
| 294 | case AppLifecycleState.resumed: | 311 | case AppLifecycleState.resumed: |
| 295 | - _subscription = controller.barcodes.listen(widget.onDetect); | 312 | + _subscription = controller.barcodes.listen( |
| 313 | + widget.onDetect, | ||
| 314 | + onError: widget.onDetectError, | ||
| 315 | + cancelOnError: false, | ||
| 316 | + ); | ||
| 296 | 317 | ||
| 297 | unawaited(controller.start()); | 318 | unawaited(controller.start()); |
| 298 | case AppLifecycleState.inactive: | 319 | case AppLifecycleState.inactive: |
| @@ -75,12 +75,11 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -75,12 +75,11 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 75 | /// If this is empty, all supported formats are detected. | 75 | /// If this is empty, all supported formats are detected. |
| 76 | final List<BarcodeFormat> formats; | 76 | final List<BarcodeFormat> formats; |
| 77 | 77 | ||
| 78 | - /// Whether scanned barcodes should contain the image | ||
| 79 | - /// that is embedded into the barcode. | 78 | + /// Whether the [BarcodeCapture.image] bytes should be provided. |
| 80 | /// | 79 | /// |
| 81 | /// If this is false, [BarcodeCapture.image] will always be null. | 80 | /// If this is false, [BarcodeCapture.image] will always be null. |
| 82 | /// | 81 | /// |
| 83 | - /// Defaults to false, and is only supported on iOS and Android. | 82 | + /// Defaults to false, and is only supported on iOS, MacOS and Android. |
| 84 | final bool returnImage; | 83 | final bool returnImage; |
| 85 | 84 | ||
| 86 | /// Whether the flashlight should be turned on when the camera is started. | 85 | /// Whether the flashlight should be turned on when the camera is started. |
| @@ -102,6 +101,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -102,6 +101,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 102 | StreamController.broadcast(); | 101 | StreamController.broadcast(); |
| 103 | 102 | ||
| 104 | /// Get the stream of scanned barcodes. | 103 | /// Get the stream of scanned barcodes. |
| 104 | + /// | ||
| 105 | + /// If an error occurred during the detection of a barcode, | ||
| 106 | + /// a [MobileScannerBarcodeException] error is emitted to the stream. | ||
| 105 | Stream<BarcodeCapture> get barcodes => _barcodesController.stream; | 107 | Stream<BarcodeCapture> get barcodes => _barcodesController.stream; |
| 106 | 108 | ||
| 107 | StreamSubscription<BarcodeCapture?>? _barcodesSubscription; | 109 | StreamSubscription<BarcodeCapture?>? _barcodesSubscription; |
| @@ -121,14 +123,25 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -121,14 +123,25 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 121 | } | 123 | } |
| 122 | 124 | ||
| 123 | void _setupListeners() { | 125 | void _setupListeners() { |
| 124 | - _barcodesSubscription = MobileScannerPlatform.instance.barcodesStream | ||
| 125 | - .listen((BarcodeCapture? barcode) { | ||
| 126 | - if (_barcodesController.isClosed || barcode == null) { | ||
| 127 | - return; | ||
| 128 | - } | ||
| 129 | - | ||
| 130 | - _barcodesController.add(barcode); | ||
| 131 | - }); | 126 | + _barcodesSubscription = |
| 127 | + MobileScannerPlatform.instance.barcodesStream.listen( | ||
| 128 | + (BarcodeCapture? barcode) { | ||
| 129 | + if (_barcodesController.isClosed || barcode == null) { | ||
| 130 | + return; | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + _barcodesController.add(barcode); | ||
| 134 | + }, | ||
| 135 | + onError: (Object error) { | ||
| 136 | + if (_barcodesController.isClosed) { | ||
| 137 | + return; | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + _barcodesController.addError(error); | ||
| 141 | + }, | ||
| 142 | + // Errors are handled gracefully by forwarding them. | ||
| 143 | + cancelOnError: false, | ||
| 144 | + ); | ||
| 132 | 145 | ||
| 133 | _torchStateSubscription = MobileScannerPlatform.instance.torchStateStream | 146 | _torchStateSubscription = MobileScannerPlatform.instance.torchStateStream |
| 134 | .listen((TorchState torchState) { | 147 | .listen((TorchState torchState) { |
| @@ -197,12 +210,20 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -197,12 +210,20 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 197 | /// Analyze an image file. | 210 | /// Analyze an image file. |
| 198 | /// | 211 | /// |
| 199 | /// The [path] points to a file on the device. | 212 | /// The [path] points to a file on the device. |
| 213 | + /// The [formats] specify the barcode formats that should be detected in the image. | ||
| 214 | + /// If the [formats] are omitted or empty, all formats are detected. | ||
| 200 | /// | 215 | /// |
| 201 | - /// This is only supported on Android and iOS. | 216 | + /// This is only supported on Android, iOS and MacOS. |
| 202 | /// | 217 | /// |
| 203 | /// Returns the [BarcodeCapture] that was found in the image. | 218 | /// Returns the [BarcodeCapture] that was found in the image. |
| 204 | - Future<BarcodeCapture?> analyzeImage(String path) { | ||
| 205 | - return MobileScannerPlatform.instance.analyzeImage(path); | 219 | + /// |
| 220 | + /// If an error occurred during the analysis of the image, | ||
| 221 | + /// a [MobileScannerBarcodeException] error is thrown. | ||
| 222 | + Future<BarcodeCapture?> analyzeImage( | ||
| 223 | + String path, { | ||
| 224 | + List<BarcodeFormat> formats = const <BarcodeFormat>[], | ||
| 225 | + }) { | ||
| 226 | + return MobileScannerPlatform.instance.analyzeImage(path, formats: formats); | ||
| 206 | } | 227 | } |
| 207 | 228 | ||
| 208 | /// Build a camera preview widget. | 229 | /// Build a camera preview widget. |
| @@ -270,13 +291,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -270,13 +291,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 270 | ); | 291 | ); |
| 271 | } | 292 | } |
| 272 | 293 | ||
| 273 | - // Permission was denied, do nothing. | ||
| 274 | - // When the controller is stopped, | ||
| 275 | - // the error is reset so the permission can be requested again if possible. | ||
| 276 | - if (value.error?.errorCode == MobileScannerErrorCode.permissionDenied) { | ||
| 277 | - return; | ||
| 278 | - } | ||
| 279 | - | ||
| 280 | // Do nothing if the camera is already running. | 294 | // Do nothing if the camera is already running. |
| 281 | if (value.isRunning) { | 295 | if (value.isRunning) { |
| 282 | return; | 296 | return; |
| @@ -316,6 +330,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -316,6 +330,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 316 | ); | 330 | ); |
| 317 | } | 331 | } |
| 318 | } on MobileScannerException catch (error) { | 332 | } on MobileScannerException catch (error) { |
| 333 | + // If the controller is already initialized, ignore the error. | ||
| 334 | + // Starting the controller while it is already started, or in the process of starting, is redundant. | ||
| 335 | + if (error.errorCode == | ||
| 336 | + MobileScannerErrorCode.controllerAlreadyInitialized) { | ||
| 337 | + return; | ||
| 338 | + } | ||
| 339 | + | ||
| 319 | // The initialization finished with an error. | 340 | // The initialization finished with an error. |
| 320 | // To avoid stale values, reset the output size, | 341 | // To avoid stale values, reset the output size, |
| 321 | // torch state and zoom scale to the defaults. | 342 | // torch state and zoom scale to the defaults. |
| @@ -330,8 +351,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -330,8 +351,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 330 | zoomScale: 1.0, | 351 | zoomScale: 1.0, |
| 331 | ); | 352 | ); |
| 332 | } | 353 | } |
| 333 | - } on PermissionRequestPendingException catch (_) { | ||
| 334 | - // If a permission request was already pending, do nothing. | ||
| 335 | } | 354 | } |
| 336 | } | 355 | } |
| 337 | 356 |
| 1 | import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; | 1 | import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; |
| 2 | 2 | ||
| 3 | -/// This class represents an exception thrown by the mobile scanner. | 3 | +/// This class represents an exception thrown by the [MobileScannerController]. |
| 4 | class MobileScannerException implements Exception { | 4 | class MobileScannerException implements Exception { |
| 5 | const MobileScannerException({ | 5 | const MobileScannerException({ |
| 6 | required this.errorCode, | 6 | required this.errorCode, |
| @@ -16,9 +16,9 @@ class MobileScannerException implements Exception { | @@ -16,9 +16,9 @@ class MobileScannerException implements Exception { | ||
| 16 | @override | 16 | @override |
| 17 | String toString() { | 17 | String toString() { |
| 18 | if (errorDetails != null && errorDetails?.message != null) { | 18 | if (errorDetails != null && errorDetails?.message != null) { |
| 19 | - return "MobileScannerException: code ${errorCode.name}, message: ${errorDetails?.message}"; | 19 | + return 'MobileScannerException(${errorCode.name}, ${errorDetails?.message})'; |
| 20 | } | 20 | } |
| 21 | - return "MobileScannerException: ${errorCode.name}"; | 21 | + return 'MobileScannerException(${errorCode.name})'; |
| 22 | } | 22 | } |
| 23 | } | 23 | } |
| 24 | 24 | ||
| @@ -40,9 +40,21 @@ class MobileScannerErrorDetails { | @@ -40,9 +40,21 @@ class MobileScannerErrorDetails { | ||
| 40 | final String? message; | 40 | final String? message; |
| 41 | } | 41 | } |
| 42 | 42 | ||
| 43 | -/// This class represents an exception that is thrown | ||
| 44 | -/// when the scanner was (re)started while a permission request was pending. | ||
| 45 | -/// | ||
| 46 | -/// This exception type is only used internally, | ||
| 47 | -/// and is not part of the public API. | ||
| 48 | -class PermissionRequestPendingException implements Exception {} | 43 | +/// This class represents an exception thrown by the [MobileScannerController] |
| 44 | +/// when a barcode scanning error occurs when processing an input frame. | ||
| 45 | +class MobileScannerBarcodeException implements Exception { | ||
| 46 | + /// Creates a new [MobileScannerBarcodeException] with the given error message. | ||
| 47 | + const MobileScannerBarcodeException(this.message); | ||
| 48 | + | ||
| 49 | + /// The error message of the exception. | ||
| 50 | + final String? message; | ||
| 51 | + | ||
| 52 | + @override | ||
| 53 | + String toString() { | ||
| 54 | + if (message?.isNotEmpty ?? false) { | ||
| 55 | + return 'MobileScannerBarcodeException($message)'; | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + return 'MobileScannerBarcodeException(Could not detect a barcode in the input image.)'; | ||
| 59 | + } | ||
| 60 | +} |
| 1 | import 'package:flutter/widgets.dart'; | 1 | import 'package:flutter/widgets.dart'; |
| 2 | +import 'package:mobile_scanner/src/enums/barcode_format.dart'; | ||
| 2 | import 'package:mobile_scanner/src/enums/torch_state.dart'; | 3 | import 'package:mobile_scanner/src/enums/torch_state.dart'; |
| 3 | import 'package:mobile_scanner/src/method_channel/mobile_scanner_method_channel.dart'; | 4 | import 'package:mobile_scanner/src/method_channel/mobile_scanner_method_channel.dart'; |
| 4 | import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; | 5 | import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; |
| @@ -46,9 +47,15 @@ abstract class MobileScannerPlatform extends PlatformInterface { | @@ -46,9 +47,15 @@ abstract class MobileScannerPlatform extends PlatformInterface { | ||
| 46 | /// Analyze a local image file for barcodes. | 47 | /// Analyze a local image file for barcodes. |
| 47 | /// | 48 | /// |
| 48 | /// The [path] is the path to the file on disk. | 49 | /// The [path] is the path to the file on disk. |
| 50 | + /// The [formats] specify the barcode formats that should be detected. | ||
| 51 | + /// | ||
| 52 | + /// If [formats] is empty, all barcode formats will be detected. | ||
| 49 | /// | 53 | /// |
| 50 | /// Returns the barcodes that were found in the image. | 54 | /// Returns the barcodes that were found in the image. |
| 51 | - Future<BarcodeCapture?> analyzeImage(String path) { | 55 | + Future<BarcodeCapture?> analyzeImage( |
| 56 | + String path, { | ||
| 57 | + List<BarcodeFormat> formats = const <BarcodeFormat>[], | ||
| 58 | + }) { | ||
| 52 | throw UnimplementedError('analyzeImage() has not been implemented.'); | 59 | throw UnimplementedError('analyzeImage() has not been implemented.'); |
| 53 | } | 60 | } |
| 54 | 61 |
| @@ -28,6 +28,7 @@ class Barcode { | @@ -28,6 +28,7 @@ class Barcode { | ||
| 28 | this.phone, | 28 | this.phone, |
| 29 | this.rawBytes, | 29 | this.rawBytes, |
| 30 | this.rawValue, | 30 | this.rawValue, |
| 31 | + this.size = Size.zero, | ||
| 31 | this.sms, | 32 | this.sms, |
| 32 | this.type = BarcodeType.unknown, | 33 | this.type = BarcodeType.unknown, |
| 33 | this.url, | 34 | this.url, |
| @@ -38,9 +39,9 @@ class Barcode { | @@ -38,9 +39,9 @@ class Barcode { | ||
| 38 | factory Barcode.fromNative(Map<Object?, Object?> data) { | 39 | factory Barcode.fromNative(Map<Object?, Object?> data) { |
| 39 | final Map<Object?, Object?>? calendarEvent = | 40 | final Map<Object?, Object?>? calendarEvent = |
| 40 | data['calendarEvent'] as Map<Object?, Object?>?; | 41 | data['calendarEvent'] as Map<Object?, Object?>?; |
| 41 | - final List<Object?>? corners = data['corners'] as List<Object?>?; | ||
| 42 | final Map<Object?, Object?>? contactInfo = | 42 | final Map<Object?, Object?>? contactInfo = |
| 43 | data['contactInfo'] as Map<Object?, Object?>?; | 43 | data['contactInfo'] as Map<Object?, Object?>?; |
| 44 | + final List<Object?>? corners = data['corners'] as List<Object?>?; | ||
| 44 | final Map<Object?, Object?>? driverLicense = | 45 | final Map<Object?, Object?>? driverLicense = |
| 45 | data['driverLicense'] as Map<Object?, Object?>?; | 46 | data['driverLicense'] as Map<Object?, Object?>?; |
| 46 | final Map<Object?, Object?>? email = | 47 | final Map<Object?, Object?>? email = |
| @@ -50,9 +51,13 @@ class Barcode { | @@ -50,9 +51,13 @@ class Barcode { | ||
| 50 | final Map<Object?, Object?>? phone = | 51 | final Map<Object?, Object?>? phone = |
| 51 | data['phone'] as Map<Object?, Object?>?; | 52 | data['phone'] as Map<Object?, Object?>?; |
| 52 | final Map<Object?, Object?>? sms = data['sms'] as Map<Object?, Object?>?; | 53 | final Map<Object?, Object?>? sms = data['sms'] as Map<Object?, Object?>?; |
| 54 | + final Map<Object?, Object?>? size = data['size'] as Map<Object?, Object?>?; | ||
| 53 | final Map<Object?, Object?>? url = data['url'] as Map<Object?, Object?>?; | 55 | final Map<Object?, Object?>? url = data['url'] as Map<Object?, Object?>?; |
| 54 | final Map<Object?, Object?>? wifi = data['wifi'] as Map<Object?, Object?>?; | 56 | final Map<Object?, Object?>? wifi = data['wifi'] as Map<Object?, Object?>?; |
| 55 | 57 | ||
| 58 | + final double? barcodeWidth = size?['width'] as double?; | ||
| 59 | + final double? barcodeHeight = size?['height'] as double?; | ||
| 60 | + | ||
| 56 | return Barcode( | 61 | return Barcode( |
| 57 | calendarEvent: calendarEvent == null | 62 | calendarEvent: calendarEvent == null |
| 58 | ? null | 63 | ? null |
| @@ -81,6 +86,9 @@ class Barcode { | @@ -81,6 +86,9 @@ class Barcode { | ||
| 81 | phone: phone == null ? null : Phone.fromNative(phone), | 86 | phone: phone == null ? null : Phone.fromNative(phone), |
| 82 | rawBytes: data['rawBytes'] as Uint8List?, | 87 | rawBytes: data['rawBytes'] as Uint8List?, |
| 83 | rawValue: data['rawValue'] as String?, | 88 | rawValue: data['rawValue'] as String?, |
| 89 | + size: barcodeWidth == null || barcodeHeight == null | ||
| 90 | + ? Size.zero | ||
| 91 | + : Size(barcodeWidth, barcodeHeight), | ||
| 84 | sms: sms == null ? null : SMS.fromNative(sms), | 92 | sms: sms == null ? null : SMS.fromNative(sms), |
| 85 | type: BarcodeType.fromRawValue(data['type'] as int? ?? 0), | 93 | type: BarcodeType.fromRawValue(data['type'] as int? ?? 0), |
| 86 | url: url == null ? null : UrlBookmark.fromNative(url), | 94 | url: url == null ? null : UrlBookmark.fromNative(url), |
| @@ -144,6 +152,11 @@ class Barcode { | @@ -144,6 +152,11 @@ class Barcode { | ||
| 144 | /// This is null if the raw value is not available. | 152 | /// This is null if the raw value is not available. |
| 145 | final String? rawValue; | 153 | final String? rawValue; |
| 146 | 154 | ||
| 155 | + /// The normalized size of the barcode bounding box. | ||
| 156 | + /// | ||
| 157 | + /// If the bounding box is unavailable, this will be [Size.zero]. | ||
| 158 | + final Size size; | ||
| 159 | + | ||
| 147 | /// The SMS message that is embedded in the barcode. | 160 | /// The SMS message that is embedded in the barcode. |
| 148 | final SMS? sms; | 161 | final SMS? sms; |
| 149 | 162 |
| 1 | +/// @docImport 'package:mobile_scanner/src/mobile_scanner_controller.dart'; | ||
| 2 | +library; | ||
| 3 | + | ||
| 1 | import 'dart:typed_data'; | 4 | import 'dart:typed_data'; |
| 2 | import 'dart:ui'; | 5 | import 'dart:ui'; |
| 3 | 6 | ||
| @@ -16,15 +19,19 @@ class BarcodeCapture { | @@ -16,15 +19,19 @@ class BarcodeCapture { | ||
| 16 | /// The list of scanned barcodes. | 19 | /// The list of scanned barcodes. |
| 17 | final List<Barcode> barcodes; | 20 | final List<Barcode> barcodes; |
| 18 | 21 | ||
| 19 | - /// The bytes of the image that is embedded in the barcode. | 22 | + /// The input image of the barcode capture. |
| 23 | + /// | ||
| 24 | + /// This is the image that was used to detect the available [barcodes], | ||
| 25 | + /// not the image from a specific barcode. | ||
| 20 | /// | 26 | /// |
| 21 | - /// This null if [MobileScannerController.returnImage] is false, | ||
| 22 | - /// or if there is no available image. | 27 | + /// This is always null if [MobileScannerController.returnImage] is false. |
| 23 | final Uint8List? image; | 28 | final Uint8List? image; |
| 24 | 29 | ||
| 25 | - /// The raw data of the scanned barcode. | 30 | + /// The raw data of the barcode scan. |
| 31 | + /// | ||
| 32 | + /// This is the data that was used to detect the available [barcodes], the input [image] and the [size]. | ||
| 26 | final Object? raw; | 33 | final Object? raw; |
| 27 | 34 | ||
| 28 | - /// The size of the scanned barcode. | 35 | + /// The size of the camera input [image]. |
| 29 | final Size size; | 36 | final Size size; |
| 30 | } | 37 | } |
| 1 | import 'dart:ui'; | 1 | import 'dart:ui'; |
| 2 | 2 | ||
| 3 | import 'package:mobile_scanner/src/enums/camera_facing.dart'; | 3 | import 'package:mobile_scanner/src/enums/camera_facing.dart'; |
| 4 | +import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; | ||
| 4 | import 'package:mobile_scanner/src/enums/torch_state.dart'; | 5 | import 'package:mobile_scanner/src/enums/torch_state.dart'; |
| 5 | import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; | 6 | import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; |
| 6 | 7 | ||
| @@ -43,7 +44,8 @@ class MobileScannerState { | @@ -43,7 +44,8 @@ class MobileScannerState { | ||
| 43 | 44 | ||
| 44 | /// Whether the mobile scanner has initialized successfully. | 45 | /// Whether the mobile scanner has initialized successfully. |
| 45 | /// | 46 | /// |
| 46 | - /// This is `true` if the camera is ready to be used. | 47 | + /// This does not indicate that the camera permission was granted. |
| 48 | + /// To check if the camera permission was granted, use [hasCameraPermission]. | ||
| 47 | final bool isInitialized; | 49 | final bool isInitialized; |
| 48 | 50 | ||
| 49 | /// Whether the mobile scanner is currently running. | 51 | /// Whether the mobile scanner is currently running. |
| @@ -60,6 +62,12 @@ class MobileScannerState { | @@ -60,6 +62,12 @@ class MobileScannerState { | ||
| 60 | /// The current zoom scale of the camera. | 62 | /// The current zoom scale of the camera. |
| 61 | final double zoomScale; | 63 | final double zoomScale; |
| 62 | 64 | ||
| 65 | + /// Whether permission to access the camera was granted. | ||
| 66 | + bool get hasCameraPermission { | ||
| 67 | + return isInitialized && | ||
| 68 | + error?.errorCode != MobileScannerErrorCode.permissionDenied; | ||
| 69 | + } | ||
| 70 | + | ||
| 63 | /// Create a copy of this state with the given parameters. | 71 | /// Create a copy of this state with the given parameters. |
| 64 | MobileScannerState copyWith({ | 72 | MobileScannerState copyWith({ |
| 65 | int? availableCameras, | 73 | int? availableCameras, |
| @@ -5,7 +5,7 @@ import 'package:mobile_scanner/src/enums/encryption_type.dart'; | @@ -5,7 +5,7 @@ import 'package:mobile_scanner/src/enums/encryption_type.dart'; | ||
| 5 | class WiFi { | 5 | class WiFi { |
| 6 | /// Construct a new [WiFi] instance. | 6 | /// Construct a new [WiFi] instance. |
| 7 | const WiFi({ | 7 | const WiFi({ |
| 8 | - this.encryptionType = EncryptionType.none, | 8 | + this.encryptionType = EncryptionType.unknown, |
| 9 | this.ssid, | 9 | this.ssid, |
| 10 | this.password, | 10 | this.password, |
| 11 | }); | 11 | }); |
| @@ -4,6 +4,7 @@ import 'dart:ui_web' as ui_web; | @@ -4,6 +4,7 @@ import 'dart:ui_web' as ui_web; | ||
| 4 | 4 | ||
| 5 | import 'package:flutter/widgets.dart'; | 5 | import 'package:flutter/widgets.dart'; |
| 6 | import 'package:flutter_web_plugins/flutter_web_plugins.dart'; | 6 | import 'package:flutter_web_plugins/flutter_web_plugins.dart'; |
| 7 | +import 'package:mobile_scanner/src/enums/barcode_format.dart'; | ||
| 7 | import 'package:mobile_scanner/src/enums/camera_facing.dart'; | 8 | import 'package:mobile_scanner/src/enums/camera_facing.dart'; |
| 8 | import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; | 9 | import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; |
| 9 | import 'package:mobile_scanner/src/enums/torch_state.dart'; | 10 | import 'package:mobile_scanner/src/enums/torch_state.dart'; |
| @@ -38,12 +39,6 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -38,12 +39,6 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 38 | /// The container div element for the camera view. | 39 | /// The container div element for the camera view. |
| 39 | late HTMLDivElement _divElement; | 40 | late HTMLDivElement _divElement; |
| 40 | 41 | ||
| 41 | - /// The flag that keeps track of whether a permission request is in progress. | ||
| 42 | - /// | ||
| 43 | - /// On the web, a permission request triggers a dialog, that in turn triggers a lifecycle change. | ||
| 44 | - /// While the permission request is in progress, any attempts at (re)starting the camera should be ignored. | ||
| 45 | - bool _permissionRequestInProgress = false; | ||
| 46 | - | ||
| 47 | /// The stream controller for the media track settings stream. | 42 | /// The stream controller for the media track settings stream. |
| 48 | /// | 43 | /// |
| 49 | /// Currently, only the facing mode setting can be supported, | 44 | /// Currently, only the facing mode setting can be supported, |
| @@ -88,6 +83,18 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -88,6 +83,18 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 88 | ..transformOrigin = 'center' | 83 | ..transformOrigin = 'center' |
| 89 | ..pointerEvents = 'none'; | 84 | ..pointerEvents = 'none'; |
| 90 | 85 | ||
| 86 | + // Do not show the media controls, as this is a preview element. | ||
| 87 | + // Also prevent play/pause events from changing the media controls. | ||
| 88 | + videoElement.controls = false; | ||
| 89 | + | ||
| 90 | + videoElement.onplay = (JSAny _) { | ||
| 91 | + videoElement.controls = false; | ||
| 92 | + }.toJS; | ||
| 93 | + | ||
| 94 | + videoElement.onpause = (JSAny _) { | ||
| 95 | + videoElement.controls = false; | ||
| 96 | + }.toJS; | ||
| 97 | + | ||
| 91 | // Attach the video element to its parent container | 98 | // Attach the video element to its parent container |
| 92 | // and setup the PlatformView factory for this `textureId`. | 99 | // and setup the PlatformView factory for this `textureId`. |
| 93 | _divElement = HTMLDivElement() | 100 | _divElement = HTMLDivElement() |
| @@ -136,6 +143,7 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -136,6 +143,7 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 136 | final JSArray<JSString>? facingModes = capabilities.facingModeNullable; | 143 | final JSArray<JSString>? facingModes = capabilities.facingModeNullable; |
| 137 | 144 | ||
| 138 | // TODO: this is an empty array on MacOS Chrome, where there is no facing mode, but one, user facing camera. | 145 | // TODO: this is an empty array on MacOS Chrome, where there is no facing mode, but one, user facing camera. |
| 146 | + // We might be able to add a workaround, using the label of the video track. | ||
| 139 | // Facing mode is not supported by this track, do nothing. | 147 | // Facing mode is not supported by this track, do nothing. |
| 140 | if (facingModes == null || facingModes.toDart.isEmpty) { | 148 | if (facingModes == null || facingModes.toDart.isEmpty) { |
| 141 | return; | 149 | return; |
| @@ -186,17 +194,12 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -186,17 +194,12 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 186 | } | 194 | } |
| 187 | 195 | ||
| 188 | try { | 196 | try { |
| 189 | - _permissionRequestInProgress = true; | ||
| 190 | - | ||
| 191 | // Retrieving the media devices requests the camera permission. | 197 | // Retrieving the media devices requests the camera permission. |
| 192 | final MediaStream videoStream = | 198 | final MediaStream videoStream = |
| 193 | await window.navigator.mediaDevices.getUserMedia(constraints).toDart; | 199 | await window.navigator.mediaDevices.getUserMedia(constraints).toDart; |
| 194 | 200 | ||
| 195 | - _permissionRequestInProgress = false; | ||
| 196 | - | ||
| 197 | return videoStream; | 201 | return videoStream; |
| 198 | } on DOMException catch (error, stackTrace) { | 202 | } on DOMException catch (error, stackTrace) { |
| 199 | - _permissionRequestInProgress = false; | ||
| 200 | final String errorMessage = error.toString(); | 203 | final String errorMessage = error.toString(); |
| 201 | 204 | ||
| 202 | MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError; | 205 | MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError; |
| @@ -220,7 +223,10 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -220,7 +223,10 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 220 | } | 223 | } |
| 221 | 224 | ||
| 222 | @override | 225 | @override |
| 223 | - Future<BarcodeCapture?> analyzeImage(String path) { | 226 | + Future<BarcodeCapture?> analyzeImage( |
| 227 | + String path, { | ||
| 228 | + List<BarcodeFormat> formats = const <BarcodeFormat>[], | ||
| 229 | + }) { | ||
| 224 | throw UnsupportedError('analyzeImage() is not supported on the web.'); | 230 | throw UnsupportedError('analyzeImage() is not supported on the web.'); |
| 225 | } | 231 | } |
| 226 | 232 | ||
| @@ -256,11 +262,13 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -256,11 +262,13 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 256 | 262 | ||
| 257 | @override | 263 | @override |
| 258 | Future<MobileScannerViewAttributes> start(StartOptions startOptions) async { | 264 | Future<MobileScannerViewAttributes> start(StartOptions startOptions) async { |
| 259 | - // If the permission request has not yet completed, | ||
| 260 | - // the camera view is not ready yet. | ||
| 261 | - // Prevent the permission popup from triggering a restart of the scanner. | ||
| 262 | - if (_permissionRequestInProgress) { | ||
| 263 | - throw PermissionRequestPendingException(); | 265 | + if (_barcodeReader != null) { |
| 266 | + throw const MobileScannerException( | ||
| 267 | + errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, | ||
| 268 | + errorDetails: MobileScannerErrorDetails( | ||
| 269 | + message: 'The scanner was already started.', | ||
| 270 | + ), | ||
| 271 | + ); | ||
| 264 | } | 272 | } |
| 265 | 273 | ||
| 266 | // If the previous state is a pause, reset scanner. | 274 | // If the previous state is a pause, reset scanner. |
| @@ -274,16 +282,6 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -274,16 +282,6 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 274 | alternateScriptUrl: _alternateScriptUrl, | 282 | alternateScriptUrl: _alternateScriptUrl, |
| 275 | ); | 283 | ); |
| 276 | 284 | ||
| 277 | - if (_barcodeReader?.isScanning ?? false) { | ||
| 278 | - throw const MobileScannerException( | ||
| 279 | - errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, | ||
| 280 | - errorDetails: MobileScannerErrorDetails( | ||
| 281 | - message: | ||
| 282 | - 'The scanner was already started. Call stop() before calling start() again.', | ||
| 283 | - ), | ||
| 284 | - ); | ||
| 285 | - } | ||
| 286 | - | ||
| 287 | // Request camera permissions and prepare the video stream. | 285 | // Request camera permissions and prepare the video stream. |
| 288 | final MediaStream videoStream = await _prepareVideoStream( | 286 | final MediaStream videoStream = await _prepareVideoStream( |
| 289 | startOptions.cameraDirection, | 287 | startOptions.cameraDirection, |
| @@ -330,6 +328,15 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -330,6 +328,15 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 330 | 328 | ||
| 331 | _barcodesController.add(barcode); | 329 | _barcodesController.add(barcode); |
| 332 | }, | 330 | }, |
| 331 | + onError: (Object error) { | ||
| 332 | + if (_barcodesController.isClosed) { | ||
| 333 | + return; | ||
| 334 | + } | ||
| 335 | + | ||
| 336 | + _barcodesController.addError(error); | ||
| 337 | + }, | ||
| 338 | + // Errors are handled gracefully by forwarding them. | ||
| 339 | + cancelOnError: false, | ||
| 333 | ); | 340 | ); |
| 334 | 341 | ||
| 335 | final bool hasTorch = await _barcodeReader?.hasTorch() ?? false; | 342 | final bool hasTorch = await _barcodeReader?.hasTorch() ?? false; |
| @@ -75,13 +75,33 @@ extension type Result(JSObject _) implements JSObject { | @@ -75,13 +75,33 @@ extension type Result(JSObject _) implements JSObject { | ||
| 75 | 75 | ||
| 76 | /// Convert this result to a [Barcode]. | 76 | /// Convert this result to a [Barcode]. |
| 77 | Barcode get toBarcode { | 77 | Barcode get toBarcode { |
| 78 | + final List<Offset> corners = resultPoints; | ||
| 79 | + | ||
| 78 | return Barcode( | 80 | return Barcode( |
| 79 | - corners: resultPoints, | 81 | + corners: corners, |
| 80 | format: barcodeFormat, | 82 | format: barcodeFormat, |
| 81 | displayValue: text, | 83 | displayValue: text, |
| 82 | rawBytes: rawBytes, | 84 | rawBytes: rawBytes, |
| 83 | rawValue: text, | 85 | rawValue: text, |
| 86 | + size: _computeSize(corners), | ||
| 84 | type: BarcodeType.text, | 87 | type: BarcodeType.text, |
| 85 | ); | 88 | ); |
| 86 | } | 89 | } |
| 90 | + | ||
| 91 | + Size _computeSize(List<Offset> points) { | ||
| 92 | + if (points.length != 4) { | ||
| 93 | + return Size.zero; | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + final Iterable<double> xCoords = points.map((p) => p.dx); | ||
| 97 | + final Iterable<double> yCoords = points.map((p) => p.dy); | ||
| 98 | + | ||
| 99 | + // Find the minimum and maximum x and y coordinates. | ||
| 100 | + final double xMin = xCoords.reduce((a, b) => a < b ? a : b); | ||
| 101 | + final double xMax = xCoords.reduce((a, b) => a > b ? a : b); | ||
| 102 | + final double yMin = yCoords.reduce((a, b) => a < b ? a : b); | ||
| 103 | + final double yMax = yCoords.reduce((a, b) => a > b ? a : b); | ||
| 104 | + | ||
| 105 | + return Size(xMax - xMin, yMax - yMin); | ||
| 106 | + } | ||
| 87 | } | 107 | } |
| @@ -2,7 +2,9 @@ import 'dart:async'; | @@ -2,7 +2,9 @@ import 'dart:async'; | ||
| 2 | import 'dart:js_interop'; | 2 | import 'dart:js_interop'; |
| 3 | import 'dart:ui'; | 3 | import 'dart:ui'; |
| 4 | 4 | ||
| 5 | +import 'package:flutter/foundation.dart'; | ||
| 5 | import 'package:mobile_scanner/src/enums/barcode_format.dart'; | 6 | import 'package:mobile_scanner/src/enums/barcode_format.dart'; |
| 7 | +import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; | ||
| 6 | import 'package:mobile_scanner/src/objects/barcode_capture.dart'; | 8 | import 'package:mobile_scanner/src/objects/barcode_capture.dart'; |
| 7 | import 'package:mobile_scanner/src/objects/start_options.dart'; | 9 | import 'package:mobile_scanner/src/objects/start_options.dart'; |
| 8 | import 'package:mobile_scanner/src/web/barcode_reader.dart'; | 10 | import 'package:mobile_scanner/src/web/barcode_reader.dart'; |
| @@ -10,12 +12,18 @@ import 'package:mobile_scanner/src/web/javascript_map.dart'; | @@ -10,12 +12,18 @@ import 'package:mobile_scanner/src/web/javascript_map.dart'; | ||
| 10 | import 'package:mobile_scanner/src/web/media_track_constraints_delegate.dart'; | 12 | import 'package:mobile_scanner/src/web/media_track_constraints_delegate.dart'; |
| 11 | import 'package:mobile_scanner/src/web/zxing/result.dart'; | 13 | import 'package:mobile_scanner/src/web/zxing/result.dart'; |
| 12 | import 'package:mobile_scanner/src/web/zxing/zxing_browser_multi_format_reader.dart'; | 14 | import 'package:mobile_scanner/src/web/zxing/zxing_browser_multi_format_reader.dart'; |
| 15 | +import 'package:mobile_scanner/src/web/zxing/zxing_exception.dart'; | ||
| 13 | import 'package:web/web.dart' as web; | 16 | import 'package:web/web.dart' as web; |
| 14 | 17 | ||
| 15 | /// A barcode reader implementation that uses the ZXing library. | 18 | /// A barcode reader implementation that uses the ZXing library. |
| 16 | final class ZXingBarcodeReader extends BarcodeReader { | 19 | final class ZXingBarcodeReader extends BarcodeReader { |
| 17 | ZXingBarcodeReader(); | 20 | ZXingBarcodeReader(); |
| 18 | 21 | ||
| 22 | + /// ZXing reports an error with this message if the code could not be detected. | ||
| 23 | + @visibleForTesting | ||
| 24 | + static const String kNoCodeDetectedErrorMessage = | ||
| 25 | + 'No MultiFormat Readers were able to detect the code.'; | ||
| 26 | + | ||
| 19 | /// The listener for media track settings changes. | 27 | /// The listener for media track settings changes. |
| 20 | void Function(web.MediaTrackSettings)? _onMediaTrackSettingsChanged; | 28 | void Function(web.MediaTrackSettings)? _onMediaTrackSettingsChanged; |
| 21 | 29 | ||
| @@ -98,16 +106,25 @@ final class ZXingBarcodeReader extends BarcodeReader { | @@ -98,16 +106,25 @@ final class ZXingBarcodeReader extends BarcodeReader { | ||
| 98 | _reader?.decodeContinuously.callAsFunction( | 106 | _reader?.decodeContinuously.callAsFunction( |
| 99 | _reader, | 107 | _reader, |
| 100 | _reader?.videoElement, | 108 | _reader?.videoElement, |
| 101 | - (Result? result, JSAny? error) { | ||
| 102 | - if (controller.isClosed || result == null) { | 109 | + (Result? result, ZXingException? error) { |
| 110 | + if (controller.isClosed) { | ||
| 111 | + return; | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + // Skip the event if no code was detected. | ||
| 115 | + if (error != null && error.message != kNoCodeDetectedErrorMessage) { | ||
| 116 | + controller.addError(MobileScannerBarcodeException(error.message)); | ||
| 103 | return; | 117 | return; |
| 104 | } | 118 | } |
| 105 | 119 | ||
| 106 | - controller.add( | ||
| 107 | - BarcodeCapture( | ||
| 108 | - barcodes: [result.toBarcode], | ||
| 109 | - ), | ||
| 110 | - ); | 120 | + if (result != null) { |
| 121 | + controller.add( | ||
| 122 | + BarcodeCapture( | ||
| 123 | + barcodes: [result.toBarcode], | ||
| 124 | + size: videoSize, | ||
| 125 | + ), | ||
| 126 | + ); | ||
| 127 | + } | ||
| 111 | }.toJS, | 128 | }.toJS, |
| 112 | ); | 129 | ); |
| 113 | }; | 130 | }; |
| @@ -138,11 +155,10 @@ final class ZXingBarcodeReader extends BarcodeReader { | @@ -138,11 +155,10 @@ final class ZXingBarcodeReader extends BarcodeReader { | ||
| 138 | required web.MediaStream videoStream, | 155 | required web.MediaStream videoStream, |
| 139 | }) async { | 156 | }) async { |
| 140 | final int detectionTimeoutMs = options.detectionTimeoutMs; | 157 | final int detectionTimeoutMs = options.detectionTimeoutMs; |
| 141 | - final List<BarcodeFormat> formats = options.formats; | ||
| 142 | - | ||
| 143 | - if (formats.contains(BarcodeFormat.unknown)) { | ||
| 144 | - formats.removeWhere((element) => element == BarcodeFormat.unknown); | ||
| 145 | - } | 158 | + final List<BarcodeFormat> formats = [ |
| 159 | + for (final BarcodeFormat format in options.formats) | ||
| 160 | + if (format != BarcodeFormat.unknown) format, | ||
| 161 | + ]; | ||
| 146 | 162 | ||
| 147 | _reader = ZXingBrowserMultiFormatReader( | 163 | _reader = ZXingBrowserMultiFormatReader( |
| 148 | _createReaderHints(formats), | 164 | _createReaderHints(formats), |
lib/src/web/zxing/zxing_exception.dart
0 → 100644
| 1 | +import 'dart:js_interop'; | ||
| 2 | + | ||
| 3 | +/// The JS static interop class for the Result class in the ZXing library. | ||
| 4 | +/// | ||
| 5 | +/// See also: https://github.com/zxing-js/library/blob/master/src/core/Exception.ts | ||
| 6 | +@JS('ZXing.Exception') | ||
| 7 | +extension type ZXingException._(JSObject _) implements JSObject { | ||
| 8 | + /// The error message of the exception, if any. | ||
| 9 | + external String? get message; | ||
| 10 | +} |
| @@ -4,7 +4,7 @@ | @@ -4,7 +4,7 @@ | ||
| 4 | # | 4 | # |
| 5 | Pod::Spec.new do |s| | 5 | Pod::Spec.new do |s| |
| 6 | s.name = 'mobile_scanner' | 6 | s.name = 'mobile_scanner' |
| 7 | - s.version = '5.1.1' | 7 | + s.version = '6.0.2' |
| 8 | s.summary = 'An universal scanner for Flutter based on MLKit.' | 8 | s.summary = 'An universal scanner for Flutter based on MLKit.' |
| 9 | s.description = <<-DESC | 9 | s.description = <<-DESC |
| 10 | An universal scanner for Flutter based on MLKit. | 10 | An universal scanner for Flutter based on MLKit. |
| @@ -13,9 +13,10 @@ An universal scanner for Flutter based on MLKit. | @@ -13,9 +13,10 @@ An universal scanner for Flutter based on MLKit. | ||
| 13 | s.license = { :file => '../LICENSE' } | 13 | s.license = { :file => '../LICENSE' } |
| 14 | s.author = { 'Julian Steenbakker' => 'juliansteenbakker@outlook.com' } | 14 | s.author = { 'Julian Steenbakker' => 'juliansteenbakker@outlook.com' } |
| 15 | s.source = { :path => '.' } | 15 | s.source = { :path => '.' } |
| 16 | - s.source_files = 'Classes/**/*' | 16 | + s.source_files = 'mobile_scanner/Sources/mobile_scanner/**/*.swift' |
| 17 | s.dependency 'FlutterMacOS' | 17 | s.dependency 'FlutterMacOS' |
| 18 | s.platform = :osx, '10.14' | 18 | s.platform = :osx, '10.14' |
| 19 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } | 19 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } |
| 20 | s.swift_version = '5.0' | 20 | s.swift_version = '5.0' |
| 21 | + s.resource_bundles = {'mobile_scanner_macos_privacy' => ['mobile_scanner/Sources/mobile_scanner/Resources/PrivacyInfo.xcprivacy']} | ||
| 21 | end | 22 | end |
macos/mobile_scanner/Package.swift
0 → 100644
| 1 | +// swift-tools-version: 5.9 | ||
| 2 | +// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
| 3 | + | ||
| 4 | +import PackageDescription | ||
| 5 | + | ||
| 6 | +let package = Package( | ||
| 7 | + name: "mobile_scanner", | ||
| 8 | + platforms: [ | ||
| 9 | + .macOS("10.14") | ||
| 10 | + ], | ||
| 11 | + products: [ | ||
| 12 | + .library(name: "mobile-scanner", targets: ["mobile_scanner"]) | ||
| 13 | + ], | ||
| 14 | + dependencies: [], | ||
| 15 | + targets: [ | ||
| 16 | + .target( | ||
| 17 | + name: "mobile_scanner", | ||
| 18 | + dependencies: [], | ||
| 19 | + resources: [ | ||
| 20 | + .process("Resources"), | ||
| 21 | + ] | ||
| 22 | + ) | ||
| 23 | + ] | ||
| 24 | +) |
| 1 | +// | ||
| 2 | +// MobileScannerErrorCodes.swift | ||
| 3 | +// mobile_scanner | ||
| 4 | +// | ||
| 5 | +// Created by Navaron Bracke on 27/05/2024. | ||
| 6 | +// | ||
| 7 | + | ||
| 8 | +import Foundation | ||
| 9 | + | ||
| 10 | +struct MobileScannerErrorCodes { | ||
| 11 | + static let ALREADY_STARTED_ERROR = "MOBILE_SCANNER_ALREADY_STARTED_ERROR" | ||
| 12 | + static let ALREADY_STARTED_ERROR_MESSAGE = "The scanner was already started." | ||
| 13 | + // The error code 'BARCODE_ERROR' does not have an error message, | ||
| 14 | + // because it uses the error message from the undelying error. | ||
| 15 | + static let BARCODE_ERROR = "MOBILE_SCANNER_BARCODE_ERROR" | ||
| 16 | + // The error code 'CAMERA_ERROR' does not have an error message, | ||
| 17 | + // because it uses the error message from the underlying error. | ||
| 18 | + static let CAMERA_ERROR = "MOBILE_SCANNER_CAMERA_ERROR" | ||
| 19 | + static let NO_CAMERA_ERROR = "MOBILE_SCANNER_NO_CAMERA_ERROR" | ||
| 20 | + static let NO_CAMERA_ERROR_MESSAGE = "No cameras available." | ||
| 21 | +} |
| @@ -26,24 +26,26 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -26,24 +26,26 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 26 | // optional window to limit scan search | 26 | // optional window to limit scan search |
| 27 | var scanWindow: CGRect? | 27 | var scanWindow: CGRect? |
| 28 | 28 | ||
| 29 | + /// Whether to return the input image with the barcode event. | ||
| 30 | + /// This is static to avoid accessing `self` in the `VNDetectBarcodesRequest` callback. | ||
| 31 | + private static var returnImage: Bool = false | ||
| 32 | + | ||
| 29 | var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates | 33 | var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates |
| 30 | 34 | ||
| 31 | var timeoutSeconds: Double = 0 | 35 | var timeoutSeconds: Double = 0 |
| 32 | 36 | ||
| 33 | var symbologies:[VNBarcodeSymbology] = [] | 37 | var symbologies:[VNBarcodeSymbology] = [] |
| 34 | - | ||
| 35 | - // var analyzeMode: Int = 0 | ||
| 36 | - var analyzing: Bool = false | 38 | + |
| 37 | var position = AVCaptureDevice.Position.back | 39 | var position = AVCaptureDevice.Position.back |
| 38 | 40 | ||
| 39 | private var stopped: Bool { | 41 | private var stopped: Bool { |
| 40 | return device == nil || captureSession == nil | 42 | return device == nil || captureSession == nil |
| 41 | } | 43 | } |
| 42 | - | 44 | + |
| 43 | private var paused: Bool { | 45 | private var paused: Bool { |
| 44 | return stopped && textureId != nil | 46 | return stopped && textureId != nil |
| 45 | } | 47 | } |
| 46 | - | 48 | + |
| 47 | public static func register(with registrar: FlutterPluginRegistrar) { | 49 | public static func register(with registrar: FlutterPluginRegistrar) { |
| 48 | let instance = MobileScannerPlugin(registrar.textures) | 50 | let instance = MobileScannerPlugin(registrar.textures) |
| 49 | let method = FlutterMethodChannel(name: | 51 | let method = FlutterMethodChannel(name: |
| @@ -73,14 +75,14 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -73,14 +75,14 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 73 | setScale(call, result) | 75 | setScale(call, result) |
| 74 | case "resetScale": | 76 | case "resetScale": |
| 75 | resetScale(call, result) | 77 | resetScale(call, result) |
| 76 | - // case "analyze": | ||
| 77 | - // switchAnalyzeMode(call, result) | ||
| 78 | case "pause": | 78 | case "pause": |
| 79 | pause(result) | 79 | pause(result) |
| 80 | case "stop": | 80 | case "stop": |
| 81 | stop(result) | 81 | stop(result) |
| 82 | case "updateScanWindow": | 82 | case "updateScanWindow": |
| 83 | updateScanWindow(call, result) | 83 | updateScanWindow(call, result) |
| 84 | + case "analyzeImage": | ||
| 85 | + analyzeImage(call, result) | ||
| 84 | default: | 86 | default: |
| 85 | result(FlutterMethodNotImplemented) | 87 | result(FlutterMethodNotImplemented) |
| 86 | } | 88 | } |
| @@ -111,12 +113,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -111,12 +113,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 111 | 113 | ||
| 112 | // Gets called when a new image is added to the buffer | 114 | // Gets called when a new image is added to the buffer |
| 113 | public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { | 115 | public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { |
| 114 | - // Ignore invalid textureId | 116 | + // Ignore invalid texture id. |
| 115 | if textureId == nil { | 117 | if textureId == nil { |
| 116 | return | 118 | return |
| 117 | } | 119 | } |
| 118 | guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { | 120 | guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { |
| 119 | - print("Failed to get image buffer from sample buffer.") | ||
| 120 | return | 121 | return |
| 121 | } | 122 | } |
| 122 | latestBuffer = imageBuffer | 123 | latestBuffer = imageBuffer |
| @@ -128,57 +129,78 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -128,57 +129,78 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 128 | nextScanTime = currentTime + timeoutSeconds | 129 | nextScanTime = currentTime + timeoutSeconds |
| 129 | imagesCurrentlyBeingProcessed = true | 130 | imagesCurrentlyBeingProcessed = true |
| 130 | DispatchQueue.global(qos: .userInitiated).async { [weak self] in | 131 | DispatchQueue.global(qos: .userInitiated).async { [weak self] in |
| 131 | - if(self!.latestBuffer == nil){ | 132 | + if self!.latestBuffer == nil { |
| 132 | return | 133 | return |
| 133 | } | 134 | } |
| 134 | var cgImage: CGImage? | 135 | var cgImage: CGImage? |
| 135 | VTCreateCGImageFromCVPixelBuffer(self!.latestBuffer, options: nil, imageOut: &cgImage) | 136 | VTCreateCGImageFromCVPixelBuffer(self!.latestBuffer, options: nil, imageOut: &cgImage) |
| 136 | let imageRequestHandler = VNImageRequestHandler(cgImage: cgImage!) | 137 | let imageRequestHandler = VNImageRequestHandler(cgImage: cgImage!) |
| 137 | do { | 138 | do { |
| 138 | - let barcodeRequest:VNDetectBarcodesRequest = VNDetectBarcodesRequest(completionHandler: { [weak self] (request, error) in | 139 | + let barcodeRequest: VNDetectBarcodesRequest = VNDetectBarcodesRequest(completionHandler: { [weak self] (request, error) in |
| 139 | self?.imagesCurrentlyBeingProcessed = false | 140 | self?.imagesCurrentlyBeingProcessed = false |
| 140 | - if error == nil { | ||
| 141 | - if let results = request.results as? [VNBarcodeObservation] { | ||
| 142 | - for barcode in results { | ||
| 143 | - if self?.scanWindow != nil && cgImage != nil { | ||
| 144 | - let match = self?.isBarCodeInScanWindow(self!.scanWindow!, barcode, cgImage!) ?? false | ||
| 145 | - if (!match) { | ||
| 146 | - continue | ||
| 147 | - } | ||
| 148 | - } | ||
| 149 | - | ||
| 150 | - DispatchQueue.main.async { | ||
| 151 | - self?.sink?([ | ||
| 152 | - "name": "barcode", | ||
| 153 | - "data": [ | ||
| 154 | - [ | ||
| 155 | - "payload": barcode.payloadStringValue ?? "", | ||
| 156 | - "symbology": barcode.symbology.toInt ?? -1, | ||
| 157 | - ], | ||
| 158 | - ], | ||
| 159 | - ]) | ||
| 160 | - } | ||
| 161 | - // if barcodeType == "QR" { | ||
| 162 | - // let image = CIImage(image: source) | ||
| 163 | - // image?.cropping(to: barcode.boundingBox) | ||
| 164 | - // self.qrCodeDescriptor(qrCode: barcode, qrCodeImage: image!) | ||
| 165 | - // } | ||
| 166 | - } | ||
| 167 | - } | ||
| 168 | - } else { | 141 | + |
| 142 | + if error != nil { | ||
| 169 | DispatchQueue.main.async { | 143 | DispatchQueue.main.async { |
| 170 | - self?.sink?(FlutterError(code: "MobileScanner", message: error?.localizedDescription, details: nil)) | 144 | + self?.sink?(FlutterError( |
| 145 | + code: MobileScannerErrorCodes.BARCODE_ERROR, | ||
| 146 | + message: error?.localizedDescription, details: nil)) | ||
| 147 | + } | ||
| 148 | + return | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + guard let results: [VNBarcodeObservation] = request.results as? [VNBarcodeObservation] else { | ||
| 152 | + return | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + if results.isEmpty { | ||
| 156 | + return | ||
| 157 | + } | ||
| 158 | + | ||
| 159 | + let barcodes: [VNBarcodeObservation] = results.compactMap({ barcode in | ||
| 160 | + // If there is a scan window, check if the barcode is within said scan window. | ||
| 161 | + if self?.scanWindow != nil && cgImage != nil && !(self?.isBarCodeInScanWindow(self!.scanWindow!, barcode, cgImage!) ?? false) { | ||
| 162 | + return nil | ||
| 171 | } | 163 | } |
| 164 | + | ||
| 165 | + return barcode | ||
| 166 | + }) | ||
| 167 | + | ||
| 168 | + DispatchQueue.main.async { | ||
| 169 | + guard let image = cgImage else { | ||
| 170 | + self?.sink?([ | ||
| 171 | + "name": "barcode", | ||
| 172 | + "data": barcodes.map({ $0.toMap() }), | ||
| 173 | + ]) | ||
| 174 | + return | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | + // The image dimensions are always provided. | ||
| 178 | + // The image bytes are only non-null when `returnImage` is true. | ||
| 179 | + let imageData: [String: Any?] = [ | ||
| 180 | + "bytes": MobileScannerPlugin.returnImage ? FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!) : nil, | ||
| 181 | + "width": Double(image.width), | ||
| 182 | + "height": Double(image.height), | ||
| 183 | + ] | ||
| 184 | + | ||
| 185 | + self?.sink?([ | ||
| 186 | + "name": "barcode", | ||
| 187 | + "data": barcodes.map({ $0.toMap() }), | ||
| 188 | + "image": imageData, | ||
| 189 | + ]) | ||
| 172 | } | 190 | } |
| 173 | }) | 191 | }) |
| 174 | - if(self?.symbologies.isEmpty == false){ | ||
| 175 | - // add the symbologies the user wishes to support | 192 | + |
| 193 | + if self?.symbologies.isEmpty == false { | ||
| 194 | + // Add the symbologies the user wishes to support. | ||
| 176 | barcodeRequest.symbologies = self!.symbologies | 195 | barcodeRequest.symbologies = self!.symbologies |
| 177 | } | 196 | } |
| 197 | + | ||
| 178 | try imageRequestHandler.perform([barcodeRequest]) | 198 | try imageRequestHandler.perform([barcodeRequest]) |
| 179 | - } catch let e { | 199 | + } catch let error { |
| 180 | DispatchQueue.main.async { | 200 | DispatchQueue.main.async { |
| 181 | - self?.sink?(FlutterError(code: "MobileScanner", message: e.localizedDescription, details: nil)) | 201 | + self?.sink?(FlutterError( |
| 202 | + code: MobileScannerErrorCodes.BARCODE_ERROR, | ||
| 203 | + message: error.localizedDescription, details: nil)) | ||
| 182 | } | 204 | } |
| 183 | } | 205 | } |
| 184 | } | 206 | } |
| @@ -258,8 +280,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -258,8 +280,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 258 | 280 | ||
| 259 | func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | 281 | func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
| 260 | if (device != nil || captureSession != nil) { | 282 | if (device != nil || captureSession != nil) { |
| 261 | - result(FlutterError(code: "MobileScanner", | ||
| 262 | - message: "Called start() while already started!", | 283 | + result(FlutterError(code: MobileScannerErrorCodes.ALREADY_STARTED_ERROR, |
| 284 | + message: MobileScannerErrorCodes.ALREADY_STARTED_ERROR_MESSAGE, | ||
| 263 | details: nil)) | 285 | details: nil)) |
| 264 | return | 286 | return |
| 265 | } | 287 | } |
| @@ -269,12 +291,12 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -269,12 +291,12 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 269 | 291 | ||
| 270 | let argReader = MapArgumentReader(call.arguments as? [String: Any]) | 292 | let argReader = MapArgumentReader(call.arguments as? [String: Any]) |
| 271 | 293 | ||
| 272 | - // let ratio: Int = argReader.int(key: "ratio") | ||
| 273 | let torch:Bool = argReader.bool(key: "torch") ?? false | 294 | let torch:Bool = argReader.bool(key: "torch") ?? false |
| 274 | let facing:Int = argReader.int(key: "facing") ?? 1 | 295 | let facing:Int = argReader.int(key: "facing") ?? 1 |
| 275 | let speed:Int = argReader.int(key: "speed") ?? 0 | 296 | let speed:Int = argReader.int(key: "speed") ?? 0 |
| 276 | let timeoutMs:Int = argReader.int(key: "timeout") ?? 0 | 297 | let timeoutMs:Int = argReader.int(key: "timeout") ?? 0 |
| 277 | symbologies = argReader.toSymbology() | 298 | symbologies = argReader.toSymbology() |
| 299 | + MobileScannerPlugin.returnImage = argReader.bool(key: "returnImage") ?? false | ||
| 278 | 300 | ||
| 279 | timeoutSeconds = Double(timeoutMs) / 1000.0 | 301 | timeoutSeconds = Double(timeoutMs) / 1000.0 |
| 280 | detectionSpeed = DetectionSpeed(rawValue: speed)! | 302 | detectionSpeed = DetectionSpeed(rawValue: speed)! |
| @@ -290,8 +312,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -290,8 +312,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 290 | } | 312 | } |
| 291 | 313 | ||
| 292 | if (device == nil) { | 314 | if (device == nil) { |
| 293 | - result(FlutterError(code: "MobileScanner", | ||
| 294 | - message: "No camera found or failed to open camera!", | 315 | + result(FlutterError(code: MobileScannerErrorCodes.NO_CAMERA_ERROR, |
| 316 | + message: MobileScannerErrorCodes.NO_CAMERA_ERROR_MESSAGE, | ||
| 295 | details: nil)) | 317 | details: nil)) |
| 296 | return | 318 | return |
| 297 | } | 319 | } |
| @@ -309,7 +331,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -309,7 +331,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 309 | let input = try AVCaptureDeviceInput(device: device) | 331 | let input = try AVCaptureDeviceInput(device: device) |
| 310 | captureSession!.addInput(input) | 332 | captureSession!.addInput(input) |
| 311 | } catch { | 333 | } catch { |
| 312 | - result(FlutterError(code: "MobileScanner", message: error.localizedDescription, details: nil)) | 334 | + result(FlutterError( |
| 335 | + code: MobileScannerErrorCodes.CAMERA_ERROR, | ||
| 336 | + message: error.localizedDescription, details: nil)) | ||
| 313 | return | 337 | return |
| 314 | } | 338 | } |
| 315 | captureSession!.sessionPreset = AVCaptureSession.Preset.photo | 339 | captureSession!.sessionPreset = AVCaptureSession.Preset.photo |
| @@ -322,7 +346,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -322,7 +346,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 322 | videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) | 346 | videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) |
| 323 | captureSession!.addOutput(videoOutput) | 347 | captureSession!.addOutput(videoOutput) |
| 324 | for connection in videoOutput.connections { | 348 | for connection in videoOutput.connections { |
| 325 | - // connection.videoOrientation = .portrait | ||
| 326 | if position == .front && connection.isVideoMirroringSupported { | 349 | if position == .front && connection.isVideoMirroringSupported { |
| 327 | connection.isVideoMirrored = true | 350 | connection.isVideoMirrored = true |
| 328 | } | 351 | } |
| @@ -425,15 +448,10 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -425,15 +448,10 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 425 | result(nil) | 448 | result(nil) |
| 426 | } | 449 | } |
| 427 | 450 | ||
| 428 | - // func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 429 | - // analyzeMode = call.arguments as! Int | ||
| 430 | - // result(nil) | ||
| 431 | - // } | ||
| 432 | - | ||
| 433 | func pause(_ result: FlutterResult) { | 451 | func pause(_ result: FlutterResult) { |
| 434 | if (paused || stopped) { | 452 | if (paused || stopped) { |
| 435 | result(nil) | 453 | result(nil) |
| 436 | - | 454 | + |
| 437 | return | 455 | return |
| 438 | } | 456 | } |
| 439 | releaseCamera() | 457 | releaseCamera() |
| @@ -447,15 +465,15 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -447,15 +465,15 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 447 | } | 465 | } |
| 448 | releaseCamera() | 466 | releaseCamera() |
| 449 | releaseTexture() | 467 | releaseTexture() |
| 450 | - | 468 | + |
| 451 | result(nil) | 469 | result(nil) |
| 452 | } | 470 | } |
| 453 | - | 471 | + |
| 454 | private func releaseCamera() { | 472 | private func releaseCamera() { |
| 455 | guard let captureSession = captureSession else { | 473 | guard let captureSession = captureSession else { |
| 456 | return | 474 | return |
| 457 | } | 475 | } |
| 458 | - | 476 | + |
| 459 | captureSession.stopRunning() | 477 | captureSession.stopRunning() |
| 460 | for input in captureSession.inputs { | 478 | for input in captureSession.inputs { |
| 461 | captureSession.removeInput(input) | 479 | captureSession.removeInput(input) |
| @@ -464,16 +482,78 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -464,16 +482,78 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 464 | captureSession.removeOutput(output) | 482 | captureSession.removeOutput(output) |
| 465 | } | 483 | } |
| 466 | device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode)) | 484 | device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode)) |
| 485 | +// registry.unregisterTexture(textureId) TODO | ||
| 486 | + | ||
| 467 | latestBuffer = nil | 487 | latestBuffer = nil |
| 468 | self.captureSession = nil | 488 | self.captureSession = nil |
| 469 | device = nil | 489 | device = nil |
| 470 | } | 490 | } |
| 471 | - | 491 | + |
| 472 | private func releaseTexture() { | 492 | private func releaseTexture() { |
| 473 | registry.unregisterTexture(textureId) | 493 | registry.unregisterTexture(textureId) |
| 474 | textureId = nil | 494 | textureId = nil |
| 475 | } | 495 | } |
| 476 | - | 496 | + |
| 497 | + func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 498 | + let argReader = MapArgumentReader(call.arguments as? [String: Any]) | ||
| 499 | + let symbologies:[VNBarcodeSymbology] = argReader.toSymbology() | ||
| 500 | + | ||
| 501 | + guard let filePath: String = argReader.string(key: "filePath") else { | ||
| 502 | + result(nil) | ||
| 503 | + return | ||
| 504 | + } | ||
| 505 | + | ||
| 506 | + let fileUrl = URL(fileURLWithPath: filePath) | ||
| 507 | + | ||
| 508 | + guard let ciImage = CIImage(contentsOf: fileUrl) else { | ||
| 509 | + result(nil) | ||
| 510 | + return | ||
| 511 | + } | ||
| 512 | + | ||
| 513 | + let imageRequestHandler = VNImageRequestHandler(ciImage: ciImage, orientation: CGImagePropertyOrientation.up, options: [:]) | ||
| 514 | + | ||
| 515 | + do { | ||
| 516 | + let barcodeRequest: VNDetectBarcodesRequest = VNDetectBarcodesRequest( | ||
| 517 | + completionHandler: { [] (request, error) in | ||
| 518 | + | ||
| 519 | + if error != nil { | ||
| 520 | + DispatchQueue.main.async { | ||
| 521 | + result(FlutterError( | ||
| 522 | + code: MobileScannerErrorCodes.BARCODE_ERROR, | ||
| 523 | + message: error?.localizedDescription, details: nil)) | ||
| 524 | + } | ||
| 525 | + return | ||
| 526 | + } | ||
| 527 | + | ||
| 528 | + guard let barcodes: [VNBarcodeObservation] = request.results as? [VNBarcodeObservation] else { | ||
| 529 | + return | ||
| 530 | + } | ||
| 531 | + | ||
| 532 | + if barcodes.isEmpty { | ||
| 533 | + return | ||
| 534 | + } | ||
| 535 | + | ||
| 536 | + result([ | ||
| 537 | + "name": "barcode", | ||
| 538 | + "data": barcodes.map({ $0.toMap() }), | ||
| 539 | + ]) | ||
| 540 | + }) | ||
| 541 | + | ||
| 542 | + if !symbologies.isEmpty { | ||
| 543 | + // Add the symbologies the user wishes to support. | ||
| 544 | + barcodeRequest.symbologies = symbologies | ||
| 545 | + } | ||
| 546 | + | ||
| 547 | + try imageRequestHandler.perform([barcodeRequest]) | ||
| 548 | + } catch let error { | ||
| 549 | + DispatchQueue.main.async { | ||
| 550 | + result(FlutterError( | ||
| 551 | + code: MobileScannerErrorCodes.BARCODE_ERROR, | ||
| 552 | + message: error.localizedDescription, details: nil)) | ||
| 553 | + } | ||
| 554 | + } | ||
| 555 | + } | ||
| 556 | + | ||
| 477 | // Observer for torch state | 557 | // Observer for torch state |
| 478 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { | 558 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { |
| 479 | switch keyPath { | 559 | switch keyPath { |
| @@ -533,6 +613,59 @@ class MapArgumentReader { | @@ -533,6 +613,59 @@ class MapArgumentReader { | ||
| 533 | 613 | ||
| 534 | } | 614 | } |
| 535 | 615 | ||
| 616 | +extension CGImage { | ||
| 617 | + public func jpegData(compressionQuality: CGFloat) -> Data? { | ||
| 618 | + let mutableData = CFDataCreateMutable(nil, 0) | ||
| 619 | + | ||
| 620 | + let formatHint: CFString | ||
| 621 | + | ||
| 622 | + if #available(macOS 11.0, *) { | ||
| 623 | + formatHint = UTType.jpeg.identifier as CFString | ||
| 624 | + } else { | ||
| 625 | + formatHint = kUTTypeJPEG | ||
| 626 | + } | ||
| 627 | + | ||
| 628 | + guard let destination = CGImageDestinationCreateWithData(mutableData!, formatHint, 1, nil) else { | ||
| 629 | + return nil | ||
| 630 | + } | ||
| 631 | + | ||
| 632 | + let options: NSDictionary = [ | ||
| 633 | + kCGImageDestinationLossyCompressionQuality: compressionQuality, | ||
| 634 | + ] | ||
| 635 | + | ||
| 636 | + CGImageDestinationAddImage(destination, self, options) | ||
| 637 | + | ||
| 638 | + if !CGImageDestinationFinalize(destination) { | ||
| 639 | + return nil | ||
| 640 | + } | ||
| 641 | + | ||
| 642 | + return mutableData as Data? | ||
| 643 | + } | ||
| 644 | +} | ||
| 645 | + | ||
| 646 | +extension VNBarcodeObservation { | ||
| 647 | + private func distanceBetween(_ p1: CGPoint, _ p2: CGPoint) -> CGFloat { | ||
| 648 | + return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2)) | ||
| 649 | + } | ||
| 650 | + | ||
| 651 | + public func toMap() -> [String: Any?] { | ||
| 652 | + return [ | ||
| 653 | + "corners": [ | ||
| 654 | + ["x": topLeft.x, "y": topLeft.y], | ||
| 655 | + ["x": topRight.x, "y": topRight.y], | ||
| 656 | + ["x": bottomRight.x, "y": bottomRight.y], | ||
| 657 | + ["x": bottomLeft.x, "y": bottomLeft.y], | ||
| 658 | + ], | ||
| 659 | + "format": symbology.toInt ?? -1, | ||
| 660 | + "rawValue": payloadStringValue ?? "", | ||
| 661 | + "size": [ | ||
| 662 | + "width": distanceBetween(topLeft, topRight), | ||
| 663 | + "height": distanceBetween(topLeft, bottomLeft), | ||
| 664 | + ], | ||
| 665 | + ] | ||
| 666 | + } | ||
| 667 | +} | ||
| 668 | + | ||
| 536 | extension VNBarcodeSymbology { | 669 | extension VNBarcodeSymbology { |
| 537 | static func fromInt(_ mapValue:Int) -> VNBarcodeSymbology? { | 670 | static func fromInt(_ mapValue:Int) -> VNBarcodeSymbology? { |
| 538 | if #available(macOS 12.0, *) { | 671 | if #available(macOS 12.0, *) { |
| @@ -568,7 +701,7 @@ extension VNBarcodeSymbology { | @@ -568,7 +701,7 @@ extension VNBarcodeSymbology { | ||
| 568 | } | 701 | } |
| 569 | } | 702 | } |
| 570 | 703 | ||
| 571 | - var toInt:Int? { | 704 | + var toInt: Int? { |
| 572 | if #available(macOS 12.0, *) { | 705 | if #available(macOS 12.0, *) { |
| 573 | if(self == VNBarcodeSymbology.codabar){ | 706 | if(self == VNBarcodeSymbology.codabar){ |
| 574 | return 8 | 707 | return 8 |
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| 3 | +<plist version="1.0"> | ||
| 4 | +<dict> | ||
| 5 | + <!-- The key NSPrivacyAccessedAPITypes is not required for MacOS --> | ||
| 6 | + <key>NSPrivacyTrackingDomains</key> | ||
| 7 | + <array/> | ||
| 8 | + <key>NSPrivacyCollectedDataTypes</key> | ||
| 9 | + <array/> | ||
| 10 | + <key>NSPrivacyTracking</key> | ||
| 11 | + <false/> | ||
| 12 | +</dict> | ||
| 13 | +</plist> |
| 1 | name: mobile_scanner | 1 | name: mobile_scanner |
| 2 | description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS. | 2 | description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS. |
| 3 | -version: 5.1.1 | 3 | +version: 6.0.2 |
| 4 | repository: https://github.com/juliansteenbakker/mobile_scanner | 4 | repository: https://github.com/juliansteenbakker/mobile_scanner |
| 5 | 5 | ||
| 6 | screenshots: | 6 | screenshots: |
| @@ -25,7 +25,7 @@ dependencies: | @@ -25,7 +25,7 @@ dependencies: | ||
| 25 | flutter_web_plugins: | 25 | flutter_web_plugins: |
| 26 | sdk: flutter | 26 | sdk: flutter |
| 27 | plugin_platform_interface: ^2.0.2 | 27 | plugin_platform_interface: ^2.0.2 |
| 28 | - web: ^1.0.0 | 28 | + web: ">=0.5.1 <2.0.0" |
| 29 | 29 | ||
| 30 | dev_dependencies: | 30 | dev_dependencies: |
| 31 | flutter_test: | 31 | flutter_test: |
| @@ -17,12 +17,12 @@ void main() { | @@ -17,12 +17,12 @@ void main() { | ||
| 17 | } | 17 | } |
| 18 | }); | 18 | }); |
| 19 | 19 | ||
| 20 | - test('invalid raw value throws argument error', () { | 20 | + test('invalid raw value returns AddressType.unknown', () { |
| 21 | const int negative = -1; | 21 | const int negative = -1; |
| 22 | const int outOfRange = 3; | 22 | const int outOfRange = 3; |
| 23 | 23 | ||
| 24 | - expect(() => AddressType.fromRawValue(negative), throwsArgumentError); | ||
| 25 | - expect(() => AddressType.fromRawValue(outOfRange), throwsArgumentError); | 24 | + expect(AddressType.fromRawValue(negative), AddressType.unknown); |
| 25 | + expect(AddressType.fromRawValue(outOfRange), AddressType.unknown); | ||
| 26 | }); | 26 | }); |
| 27 | 27 | ||
| 28 | test('can be converted to raw value', () { | 28 | test('can be converted to raw value', () { |
| @@ -27,12 +27,12 @@ void main() { | @@ -27,12 +27,12 @@ void main() { | ||
| 27 | } | 27 | } |
| 28 | }); | 28 | }); |
| 29 | 29 | ||
| 30 | - test('invalid raw value throws argument error', () { | 30 | + test('invalid raw value returns BarcodeType.unknown', () { |
| 31 | const int negative = -1; | 31 | const int negative = -1; |
| 32 | const int outOfRange = 13; | 32 | const int outOfRange = 13; |
| 33 | 33 | ||
| 34 | - expect(() => BarcodeType.fromRawValue(negative), throwsArgumentError); | ||
| 35 | - expect(() => BarcodeType.fromRawValue(outOfRange), throwsArgumentError); | 34 | + expect(BarcodeType.fromRawValue(negative), BarcodeType.unknown); |
| 35 | + expect(BarcodeType.fromRawValue(outOfRange), BarcodeType.unknown); | ||
| 36 | }); | 36 | }); |
| 37 | 37 | ||
| 38 | test('can be converted to raw value', () { | 38 | test('can be converted to raw value', () { |
| @@ -17,12 +17,12 @@ void main() { | @@ -17,12 +17,12 @@ void main() { | ||
| 17 | } | 17 | } |
| 18 | }); | 18 | }); |
| 19 | 19 | ||
| 20 | - test('invalid raw value throws argument error', () { | 20 | + test('invalid raw value returns EmailType.unknown', () { |
| 21 | const int negative = -1; | 21 | const int negative = -1; |
| 22 | const int outOfRange = 3; | 22 | const int outOfRange = 3; |
| 23 | 23 | ||
| 24 | - expect(() => EmailType.fromRawValue(negative), throwsArgumentError); | ||
| 25 | - expect(() => EmailType.fromRawValue(outOfRange), throwsArgumentError); | 24 | + expect(EmailType.fromRawValue(negative), EmailType.unknown); |
| 25 | + expect(EmailType.fromRawValue(outOfRange), EmailType.unknown); | ||
| 26 | }); | 26 | }); |
| 27 | 27 | ||
| 28 | test('can be converted to raw value', () { | 28 | test('can be converted to raw value', () { |
| @@ -5,7 +5,7 @@ void main() { | @@ -5,7 +5,7 @@ void main() { | ||
| 5 | group('$EncryptionType tests', () { | 5 | group('$EncryptionType tests', () { |
| 6 | test('can be created from raw value', () { | 6 | test('can be created from raw value', () { |
| 7 | const values = <int, EncryptionType>{ | 7 | const values = <int, EncryptionType>{ |
| 8 | - 0: EncryptionType.none, | 8 | + 0: EncryptionType.unknown, |
| 9 | 1: EncryptionType.open, | 9 | 1: EncryptionType.open, |
| 10 | 2: EncryptionType.wpa, | 10 | 2: EncryptionType.wpa, |
| 11 | 3: EncryptionType.wep, | 11 | 3: EncryptionType.wep, |
| @@ -18,20 +18,17 @@ void main() { | @@ -18,20 +18,17 @@ void main() { | ||
| 18 | } | 18 | } |
| 19 | }); | 19 | }); |
| 20 | 20 | ||
| 21 | - test('invalid raw value throws argument error', () { | 21 | + test('invalid raw value returns EncryptionType.unknown', () { |
| 22 | const int negative = -1; | 22 | const int negative = -1; |
| 23 | const int outOfRange = 4; | 23 | const int outOfRange = 4; |
| 24 | 24 | ||
| 25 | - expect(() => EncryptionType.fromRawValue(negative), throwsArgumentError); | ||
| 26 | - expect( | ||
| 27 | - () => EncryptionType.fromRawValue(outOfRange), | ||
| 28 | - throwsArgumentError, | ||
| 29 | - ); | 25 | + expect(EncryptionType.fromRawValue(negative), EncryptionType.unknown); |
| 26 | + expect(EncryptionType.fromRawValue(outOfRange), EncryptionType.unknown); | ||
| 30 | }); | 27 | }); |
| 31 | 28 | ||
| 32 | test('can be converted to raw value', () { | 29 | test('can be converted to raw value', () { |
| 33 | const values = <EncryptionType, int>{ | 30 | const values = <EncryptionType, int>{ |
| 34 | - EncryptionType.none: 0, | 31 | + EncryptionType.unknown: 0, |
| 35 | EncryptionType.open: 1, | 32 | EncryptionType.open: 1, |
| 36 | EncryptionType.wpa: 2, | 33 | EncryptionType.wpa: 2, |
| 37 | EncryptionType.wep: 3, | 34 | EncryptionType.wep: 3, |
| @@ -19,12 +19,12 @@ void main() { | @@ -19,12 +19,12 @@ void main() { | ||
| 19 | } | 19 | } |
| 20 | }); | 20 | }); |
| 21 | 21 | ||
| 22 | - test('invalid raw value throws argument error', () { | 22 | + test('invalid raw value returns PhoneType.unknown', () { |
| 23 | const int negative = -1; | 23 | const int negative = -1; |
| 24 | const int outOfRange = 5; | 24 | const int outOfRange = 5; |
| 25 | 25 | ||
| 26 | - expect(() => PhoneType.fromRawValue(negative), throwsArgumentError); | ||
| 27 | - expect(() => PhoneType.fromRawValue(outOfRange), throwsArgumentError); | 26 | + expect(PhoneType.fromRawValue(negative), PhoneType.unknown); |
| 27 | + expect(PhoneType.fromRawValue(outOfRange), PhoneType.unknown); | ||
| 28 | }); | 28 | }); |
| 29 | 29 | ||
| 30 | test('can be converted to raw value', () { | 30 | test('can be converted to raw value', () { |
-
Please register or login to post a comment