Committed by
GitHub
Merge branch 'master' into pause_function
Showing
56 changed files
with
916 additions
and
632 deletions
| 1 | -version: 2 | ||
| 2 | -updates: | ||
| 3 | - - package-ecosystem: "github-actions" | ||
| 4 | - directory: "/" | ||
| 5 | - schedule: | ||
| 6 | - interval: "weekly" | ||
| 7 | - reviewers: | ||
| 8 | - - "juliansteenbakker" | ||
| 9 | - commit-message: | ||
| 10 | - prefix: "chore" | ||
| 11 | - include: "scope" | ||
| 12 | - - package-ecosystem: gradle | ||
| 13 | - directory: "/android" | ||
| 14 | - schedule: | ||
| 15 | - interval: "weekly" | ||
| 16 | - reviewers: | ||
| 17 | - - "juliansteenbakker" | ||
| 18 | - commit-message: | ||
| 19 | - prefix: "chore" | ||
| 20 | - include: "scope" | ||
| 21 | - - package-ecosystem: gradle | ||
| 22 | - directory: "/example/android" | ||
| 23 | - schedule: | ||
| 24 | - interval: "weekly" | ||
| 25 | - reviewers: | ||
| 26 | - - "juliansteenbakker" | ||
| 27 | - commit-message: | ||
| 28 | - prefix: "chore" | ||
| 29 | - include: "scope" | ||
| 30 | - - package-ecosystem: "pub" | ||
| 31 | - directory: "/" | ||
| 32 | - schedule: | ||
| 33 | - interval: "weekly" | ||
| 34 | - commit-message: | ||
| 35 | - prefix: "chore" | ||
| 36 | - include: "scope" | ||
| 37 | - reviewers: | ||
| 38 | - - "juliansteenbakker" | 1 | +# version: 2 |
| 2 | +# updates: | ||
| 3 | +# - package-ecosystem: "github-actions" | ||
| 4 | +# directory: "/" | ||
| 5 | +# schedule: | ||
| 6 | +# interval: "weekly" | ||
| 7 | +# reviewers: | ||
| 8 | +# - "juliansteenbakker" | ||
| 9 | +# commit-message: | ||
| 10 | +# prefix: "chore" | ||
| 11 | +# include: "scope" | ||
| 12 | +# - package-ecosystem: gradle | ||
| 13 | +# directory: "/android" | ||
| 14 | +# schedule: | ||
| 15 | +# interval: "weekly" | ||
| 16 | +# reviewers: | ||
| 17 | +# - "juliansteenbakker" | ||
| 18 | +# commit-message: | ||
| 19 | +# prefix: "chore" | ||
| 20 | +# include: "scope" | ||
| 21 | +# # - package-ecosystem: gradle | ||
| 22 | +# # directory: "/example/android" | ||
| 23 | +# # schedule: | ||
| 24 | +# # interval: "weekly" | ||
| 25 | +# # reviewers: | ||
| 26 | +# # - "juliansteenbakker" | ||
| 27 | +# # commit-message: | ||
| 28 | +# # prefix: "chore" | ||
| 29 | +# # include: "scope" | ||
| 30 | +# - package-ecosystem: "pub" | ||
| 31 | +# directory: "/" | ||
| 32 | +# schedule: | ||
| 33 | +# interval: "weekly" | ||
| 34 | +# commit-message: | ||
| 35 | +# prefix: "chore" | ||
| 36 | +# include: "scope" | ||
| 37 | +# reviewers: | ||
| 38 | +# - "juliansteenbakker" |
| @@ -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.0.2 | 10 | + - uses: GoogleCloudPlatform/release-please-action@v4.1.0 |
| 11 | with: | 11 | with: |
| 12 | token: ${{ secrets.GITHUB_TOKEN }} | 12 | token: ${{ secrets.GITHUB_TOKEN }} |
| 13 | release-type: simple | 13 | release-type: simple |
| 1 | -## 5.0.0-beta.2 | 1 | +## 5.1.1 |
| 2 | +* This release fixes an issue with automatic starts in the examples. | ||
| 3 | + | ||
| 4 | +## 5.1.0 | ||
| 5 | +This updates reverts a few breaking changes made in v5.0.0 in order to keep things simple. | ||
| 6 | + | ||
| 7 | +* The `onDetect` method has been reinstated in the `MobileScanner` widget, but is nullable. You can | ||
| 8 | +still listen to `MobileScannerController.barcodes` directly by passing null to this parameter. | ||
| 9 | +* The `autoStart` attribute has been reinstated in the `MobileScannerController` and defaults to true. However, if you want | ||
| 10 | +to control which camera is used on start, or you want to manage the lifecycle yourself, you should set | ||
| 11 | +autoStart to false and manually call `MobileScannerController.start({CameraFacing? cameraDirection})`. | ||
| 12 | +* The `controller` is no longer required in the `MobileScanner` widget. However if provided, the user should take care | ||
| 13 | +of disposing it. | ||
| 14 | +* [Android] Revert Gradle 8 back to Gradle 7, to be inline with most Flutter plugins and prevent build issues. | ||
| 15 | +* [Android] Revert Kotlin back from 1.9 to 1.7 to be inline with most Flutter plugins. Special 1.9 functionality | ||
| 16 | +has been refactored to be compatible with 1.7. | ||
| 17 | + | ||
| 18 | + | ||
| 19 | +## 5.0.2 | ||
| 20 | +Bugs fixed: | ||
| 21 | +* Fixed a crash when the controller is disposed while it is still starting. [#1036](https://github.com/juliansteenbakker/mobile_scanner/pull/1036) (thanks @EArminjon !) | ||
| 22 | +* Fixed an issue that causes the initial torch state to be out of sync. | ||
| 23 | + | ||
| 24 | +Improvements: | ||
| 25 | +* Updated the lifeycle code sample to handle not-initialized controllers. | ||
| 26 | + | ||
| 27 | +## 5.0.1 | ||
| 28 | +Improvements: | ||
| 29 | +* Adjusted the platform checks to use the defaultTargetPlatform API, so that tests can use the correct platform overrides. | ||
| 2 | 30 | ||
| 31 | +## 5.0.0 | ||
| 32 | +This major release contains all the changes from the 5.0.0 beta releases, along with the following changes: | ||
| 33 | + | ||
| 34 | +Improvements: | ||
| 35 | +- [Android] Remove the Kotlin Standard Library from the dependencies, as it is automatically included in Kotlin 1.4+ | ||
| 36 | + | ||
| 37 | +## 5.0.0-beta.3 | ||
| 3 | **BREAKING CHANGES:** | 38 | **BREAKING CHANGES:** |
| 4 | 39 | ||
| 5 | -* Flutter 3.16.0 is now required. | 40 | +* Flutter 3.19.0 is now required. |
| 41 | +* [iOS] iOS 12.0 is now the minimum supported iOS version. | ||
| 42 | +* [iOS] Adds a Privacy Manifest. | ||
| 43 | + | ||
| 44 | +Bugs fixed: | ||
| 45 | +* Fixed an issue where the camera preview and barcode scanner did not work the second time on web. | ||
| 46 | + | ||
| 47 | +Improvements: | ||
| 48 | +* [web] Migrates to extension types. (thanks @koji-1009 !) | ||
| 6 | 49 | ||
| 50 | +## 5.0.0-beta.2 | ||
| 7 | Bugs fixed: | 51 | Bugs fixed: |
| 8 | * Fixed an issue where the scan window was not updated when its size was changed. (thanks @navaronbracke !) | 52 | * Fixed an issue where the scan window was not updated when its size was changed. (thanks @navaronbracke !) |
| 9 | 53 | ||
| 10 | ## 5.0.0-beta.1 | 54 | ## 5.0.0-beta.1 |
| 11 | - | ||
| 12 | **BREAKING CHANGES:** | 55 | **BREAKING CHANGES:** |
| 13 | 56 | ||
| 14 | * The `width` and `height` of `BarcodeCapture` have been removed, in favor of `size`. | 57 | * The `width` and `height` of `BarcodeCapture` have been removed, in favor of `size`. |
| @@ -39,7 +82,6 @@ Bugs fixed: | @@ -39,7 +82,6 @@ Bugs fixed: | ||
| 39 | * [iOS] Fixed a crash with a nil capture session when starting the camera. (thanks @navaronbracke !) | 82 | * [iOS] Fixed a crash with a nil capture session when starting the camera. (thanks @navaronbracke !) |
| 40 | 83 | ||
| 41 | ## 4.0.0 | 84 | ## 4.0.0 |
| 42 | - | ||
| 43 | **BREAKING CHANGES:** | 85 | **BREAKING CHANGES:** |
| 44 | 86 | ||
| 45 | * [Android] compileSdk has been upgraded to version 34. | 87 | * [Android] compileSdk has been upgraded to version 34. |
| @@ -7,6 +7,28 @@ | @@ -7,6 +7,28 @@ | ||
| 7 | 7 | ||
| 8 | A universal scanner for Flutter based on MLKit. Uses CameraX on Android and AVFoundation on iOS. | 8 | A universal scanner for Flutter based on MLKit. Uses CameraX on Android and AVFoundation on iOS. |
| 9 | 9 | ||
| 10 | +## Breaking Changes v5.0.0 | ||
| 11 | +Version 5.0.0 brings some breaking changes. However, some are reverted in version 5.1.0. Please see the list below for all breaking changes, and Changelog.md for a more detailed list. | ||
| 12 | + | ||
| 13 | +* ~~The `autoStart` attribute has been removed from the `MobileScannerController`. The controller should be manually started on-demand.~~ (Reverted in version 5.1.0) | ||
| 14 | +* ~~A controller is now required for the `MobileScanner` widget.~~ (Reverted in version 5.1.0) | ||
| 15 | +* ~~The `onDetect` method has been removed from the `MobileScanner` widget. Instead, listen to `MobileScannerController.barcodes` directly.~~ (Reverted in version 5.1.0) | ||
| 16 | +* The `width` and `height` of `BarcodeCapture` have been removed, in favor of `size`. | ||
| 17 | +* The `raw` attribute is now `Object?` instead of `dynamic`, so that it participates in type promotion. | ||
| 18 | +* The `MobileScannerArguments` class has been removed from the public API, as it is an internal type. | ||
| 19 | +* The `cameraFacingOverride` named argument for the `start()` method has been renamed to `cameraDirection`. | ||
| 20 | +* The `analyzeImage` function now correctly returns a `BarcodeCapture?` instead of a boolean. | ||
| 21 | +* The `formats` attribute of the `MobileScannerController` is now non-null. | ||
| 22 | +* The `MobileScannerState` enum has been renamed to `MobileScannerAuthorizationState`. | ||
| 23 | +* The various `ValueNotifier`s for the camera state have been removed. Use the `value` of the `MobileScannerController` instead. | ||
| 24 | +* The `hasTorch` getter has been removed. Instead, use the torch state of the controller's value. | ||
| 25 | +* The `TorchState` enum now provides a new value for unavailable flashlights. | ||
| 26 | +* The `onPermissionSet`, `onStart` and `onScannerStarted` methods have been removed from the `MobileScanner` widget. Instead, await `MobileScannerController.start()`. | ||
| 27 | +* The `startDelay` has been removed from the `MobileScanner` widget. Instead, use a delay between manual starts of one or more controllers. | ||
| 28 | +* The `overlay` widget of the `MobileScanner` has been replaced by a new property, `overlayBuilder`, which provides the constraints for the overlay. | ||
| 29 | +* The torch can no longer be toggled on the web, as this is only available for image tracks and not video tracks. As a result the torch state for the web will always be `TorchState.unavailable`. | ||
| 30 | +* The zoom scale can no longer be modified on the web, as this is only available for image tracks and not video tracks. As a result, the zoom scale will always be `1.0`. | ||
| 31 | + | ||
| 10 | ## Features Supported | 32 | ## Features Supported |
| 11 | 33 | ||
| 12 | See the example app for detailed implementation information. | 34 | See the example app for detailed implementation information. |
| @@ -103,7 +125,11 @@ class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver { | @@ -103,7 +125,11 @@ class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver { | ||
| 103 | 125 | ||
| 104 | @override | 126 | @override |
| 105 | void didChangeAppLifecycleState(AppLifecycleState state) { | 127 | void didChangeAppLifecycleState(AppLifecycleState state) { |
| 106 | - super.didChangeAppLifecycleState(state); | 128 | + // 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. | ||
| 130 | + if (!controller.value.isInitialized) { | ||
| 131 | + return; | ||
| 132 | + } | ||
| 107 | 133 | ||
| 108 | switch (state) { | 134 | switch (state) { |
| 109 | case AppLifecycleState.detached: | 135 | case AppLifecycleState.detached: |
| @@ -165,5 +191,5 @@ Future<void> dispose() async { | @@ -165,5 +191,5 @@ Future<void> dispose() async { | ||
| 165 | 191 | ||
| 166 | To display the camera preview, pass the controller to a `MobileScanner` widget. | 192 | To display the camera preview, pass the controller to a `MobileScanner` widget. |
| 167 | 193 | ||
| 168 | -See the examples for runnable examples of various usages, | 194 | +See the [examples](example/README.md) for runnable examples of various usages, |
| 169 | such as the basic usage, applying a scan window, or retrieving images from the barcodes. | 195 | such as the basic usage, applying a scan window, or retrieving images from the barcodes. |
| @@ -9,7 +9,7 @@ buildscript { | @@ -9,7 +9,7 @@ buildscript { | ||
| 9 | } | 9 | } |
| 10 | 10 | ||
| 11 | dependencies { | 11 | dependencies { |
| 12 | - classpath 'com.android.tools.build:gradle:8.2.2' | 12 | + classpath 'com.android.tools.build:gradle:8.3.2' |
| 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" | 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" |
| 14 | } | 14 | } |
| 15 | } | 15 | } |
| @@ -32,12 +32,12 @@ android { | @@ -32,12 +32,12 @@ android { | ||
| 32 | compileSdk 34 | 32 | compileSdk 34 |
| 33 | 33 | ||
| 34 | compileOptions { | 34 | compileOptions { |
| 35 | - sourceCompatibility JavaVersion.VERSION_17 | ||
| 36 | - targetCompatibility JavaVersion.VERSION_17 | 35 | + sourceCompatibility JavaVersion.VERSION_1_8 |
| 36 | + targetCompatibility JavaVersion.VERSION_1_8 | ||
| 37 | } | 37 | } |
| 38 | 38 | ||
| 39 | kotlinOptions { | 39 | kotlinOptions { |
| 40 | - jvmTarget = '17' | 40 | + jvmTarget = '1.8' |
| 41 | } | 41 | } |
| 42 | 42 | ||
| 43 | sourceSets { | 43 | sourceSets { |
| @@ -64,8 +64,6 @@ android { | @@ -64,8 +64,6 @@ android { | ||
| 64 | } | 64 | } |
| 65 | 65 | ||
| 66 | dependencies { | 66 | dependencies { |
| 67 | - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" | ||
| 68 | - | ||
| 69 | def useUnbundled = project.findProperty('dev.steenbakker.mobile_scanner.useUnbundled') ?: false | 67 | def useUnbundled = project.findProperty('dev.steenbakker.mobile_scanner.useUnbundled') ?: false |
| 70 | if (useUnbundled.toBoolean()) { | 68 | if (useUnbundled.toBoolean()) { |
| 71 | // Dynamically downloaded model via Google Play Services | 69 | // Dynamically downloaded model via Google Play Services |
| @@ -75,9 +73,13 @@ dependencies { | @@ -75,9 +73,13 @@ dependencies { | ||
| 75 | implementation 'com.google.mlkit:barcode-scanning:17.2.0' | 73 | implementation 'com.google.mlkit:barcode-scanning:17.2.0' |
| 76 | } | 74 | } |
| 77 | 75 | ||
| 78 | - implementation 'androidx.camera:camera-camera2:1.3.1' | ||
| 79 | - implementation 'androidx.camera:camera-lifecycle:1.3.1' | 76 | + // org.jetbrains.kotlin:kotlin-bom artifact purpose is to align kotlin stdlib and related code versions. |
| 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")) | ||
| 79 | + | ||
| 80 | + implementation 'androidx.camera:camera-lifecycle:1.3.3' | ||
| 81 | + implementation 'androidx.camera:camera-camera2:1.3.3' | ||
| 80 | 82 | ||
| 81 | testImplementation 'org.jetbrains.kotlin:kotlin-test' | 83 | testImplementation 'org.jetbrains.kotlin:kotlin-test' |
| 82 | - testImplementation 'org.mockito:mockito-core:5.10.0' | 84 | + testImplementation 'org.mockito:mockito-core:5.12.0' |
| 83 | } | 85 | } |
| @@ -19,6 +19,8 @@ import androidx.camera.core.ExperimentalGetImage | @@ -19,6 +19,8 @@ import androidx.camera.core.ExperimentalGetImage | ||
| 19 | import androidx.camera.core.ImageAnalysis | 19 | import androidx.camera.core.ImageAnalysis |
| 20 | import androidx.camera.core.ImageProxy | 20 | import androidx.camera.core.ImageProxy |
| 21 | import androidx.camera.core.Preview | 21 | import androidx.camera.core.Preview |
| 22 | +import androidx.camera.core.TorchState | ||
| 23 | +import androidx.camera.core.resolutionselector.AspectRatioStrategy | ||
| 22 | import androidx.camera.core.resolutionselector.ResolutionSelector | 24 | import androidx.camera.core.resolutionselector.ResolutionSelector |
| 23 | import androidx.camera.core.resolutionselector.ResolutionStrategy | 25 | import androidx.camera.core.resolutionselector.ResolutionStrategy |
| 24 | import androidx.camera.lifecycle.ProcessCameraProvider | 26 | import androidx.camera.lifecycle.ProcessCameraProvider |
| @@ -367,11 +369,22 @@ class MobileScanner( | @@ -367,11 +369,22 @@ class MobileScanner( | ||
| 367 | val height = resolution.height.toDouble() | 369 | val height = resolution.height.toDouble() |
| 368 | val portrait = (camera?.cameraInfo?.sensorRotationDegrees ?: 0) % 180 == 0 | 370 | val portrait = (camera?.cameraInfo?.sensorRotationDegrees ?: 0) % 180 == 0 |
| 369 | 371 | ||
| 372 | + // Start with 'unavailable' torch state. | ||
| 373 | + var currentTorchState: Int = -1 | ||
| 374 | + | ||
| 375 | + camera?.cameraInfo?.let { | ||
| 376 | + if (!it.hasFlashUnit()) { | ||
| 377 | + return@let | ||
| 378 | + } | ||
| 379 | + | ||
| 380 | + currentTorchState = it.torchState.value ?: -1 | ||
| 381 | + } | ||
| 382 | + | ||
| 370 | mobileScannerStartedCallback( | 383 | mobileScannerStartedCallback( |
| 371 | MobileScannerStartParameters( | 384 | MobileScannerStartParameters( |
| 372 | if (portrait) width else height, | 385 | if (portrait) width else height, |
| 373 | if (portrait) height else width, | 386 | if (portrait) height else width, |
| 374 | - camera?.cameraInfo?.hasFlashUnit() ?: false, | 387 | + currentTorchState, |
| 375 | textureEntry!!.id(), | 388 | textureEntry!!.id(), |
| 376 | numberOfCameras ?: 0 | 389 | numberOfCameras ?: 0 |
| 377 | ) | 390 | ) |
| @@ -433,13 +446,16 @@ class MobileScanner( | @@ -433,13 +446,16 @@ class MobileScanner( | ||
| 433 | /** | 446 | /** |
| 434 | * Toggles the flash light on or off. | 447 | * Toggles the flash light on or off. |
| 435 | */ | 448 | */ |
| 436 | - fun toggleTorch(enableTorch: Boolean) { | ||
| 437 | - if (camera == null) { | ||
| 438 | - return | ||
| 439 | - } | 449 | + fun toggleTorch() { |
| 450 | + camera?.let { | ||
| 451 | + if (!it.cameraInfo.hasFlashUnit()) { | ||
| 452 | + return@let | ||
| 453 | + } | ||
| 440 | 454 | ||
| 441 | - if (camera?.cameraInfo?.hasFlashUnit() == true) { | ||
| 442 | - camera?.cameraControl?.enableTorch(enableTorch) | 455 | + when(it.cameraInfo.torchState.value) { |
| 456 | + TorchState.OFF -> it.cameraControl.enableTorch(true) | ||
| 457 | + TorchState.ON -> it.cameraControl.enableTorch(false) | ||
| 458 | + } | ||
| 443 | } | 459 | } |
| 444 | } | 460 | } |
| 445 | 461 |
| @@ -74,6 +74,7 @@ class MobileScannerHandler( | @@ -74,6 +74,7 @@ class MobileScannerHandler( | ||
| 74 | private var mobileScanner: MobileScanner? = null | 74 | private var mobileScanner: MobileScanner? = null |
| 75 | 75 | ||
| 76 | private val torchStateCallback: TorchStateCallback = {state: Int -> | 76 | private val torchStateCallback: TorchStateCallback = {state: Int -> |
| 77 | + // Off = 0, On = 1 | ||
| 77 | barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state)) | 78 | barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state)) |
| 78 | } | 79 | } |
| 79 | 80 | ||
| @@ -121,9 +122,9 @@ class MobileScannerHandler( | @@ -121,9 +122,9 @@ class MobileScannerHandler( | ||
| 121 | } | 122 | } |
| 122 | }) | 123 | }) |
| 123 | "start" -> start(call, result) | 124 | "start" -> start(call, result) |
| 124 | - "torch" -> toggleTorch(call, result) | ||
| 125 | "pause" -> pause(result) | 125 | "pause" -> pause(result) |
| 126 | "stop" -> stop(result) | 126 | "stop" -> stop(result) |
| 127 | + "toggleTorch" -> toggleTorch(result) | ||
| 127 | "analyzeImage" -> analyzeImage(call, result) | 128 | "analyzeImage" -> analyzeImage(call, result) |
| 128 | "setScale" -> setScale(call, result) | 129 | "setScale" -> setScale(call, result) |
| 129 | "resetScale" -> resetScale(result) | 130 | "resetScale" -> resetScale(result) |
| @@ -168,7 +169,8 @@ class MobileScannerHandler( | @@ -168,7 +169,8 @@ class MobileScannerHandler( | ||
| 168 | val position = | 169 | val position = |
| 169 | if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA | 170 | if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA |
| 170 | 171 | ||
| 171 | - val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed} | 172 | + val detectionSpeed: DetectionSpeed = if (speed == 0) DetectionSpeed.NO_DUPLICATES |
| 173 | + else if (speed ==1) DetectionSpeed.NORMAL else DetectionSpeed.UNRESTRICTED | ||
| 172 | 174 | ||
| 173 | mobileScanner!!.start( | 175 | mobileScanner!!.start( |
| 174 | barcodeScannerOptions, | 176 | barcodeScannerOptions, |
| @@ -183,7 +185,7 @@ class MobileScannerHandler( | @@ -183,7 +185,7 @@ class MobileScannerHandler( | ||
| 183 | result.success(mapOf( | 185 | result.success(mapOf( |
| 184 | "textureId" to it.id, | 186 | "textureId" to it.id, |
| 185 | "size" to mapOf("width" to it.width, "height" to it.height), | 187 | "size" to mapOf("width" to it.width, "height" to it.height), |
| 186 | - "torchable" to it.hasFlashUnit, | 188 | + "currentTorchState" to it.currentTorchState, |
| 187 | "numberOfCameras" to it.numberOfCameras | 189 | "numberOfCameras" to it.numberOfCameras |
| 188 | )) | 190 | )) |
| 189 | } | 191 | } |
| @@ -256,8 +258,8 @@ class MobileScannerHandler( | @@ -256,8 +258,8 @@ class MobileScannerHandler( | ||
| 256 | mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback) | 258 | mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback) |
| 257 | } | 259 | } |
| 258 | 260 | ||
| 259 | - private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) { | ||
| 260 | - mobileScanner!!.toggleTorch(call.arguments == 1) | 261 | + private fun toggleTorch(result: MethodChannel.Result) { |
| 262 | + mobileScanner?.toggleTorch() | ||
| 261 | result.success(null) | 263 | result.success(null) |
| 262 | } | 264 | } |
| 263 | 265 |
| @@ -3,7 +3,7 @@ package dev.steenbakker.mobile_scanner.objects | @@ -3,7 +3,7 @@ package dev.steenbakker.mobile_scanner.objects | ||
| 3 | class MobileScannerStartParameters( | 3 | class MobileScannerStartParameters( |
| 4 | val width: Double = 0.0, | 4 | val width: Double = 0.0, |
| 5 | val height: Double, | 5 | val height: Double, |
| 6 | - val hasFlashUnit: Boolean, | 6 | + val currentTorchState: Int, |
| 7 | val id: Long, | 7 | val id: Long, |
| 8 | val numberOfCameras: Int | 8 | val numberOfCameras: Int |
| 9 | ) | 9 | ) |
| @@ -2,15 +2,66 @@ | @@ -2,15 +2,66 @@ | ||
| 2 | 2 | ||
| 3 | Demonstrates how to use the mobile_scanner plugin. | 3 | Demonstrates how to use the mobile_scanner plugin. |
| 4 | 4 | ||
| 5 | -## Getting Started | 5 | +## Run Examples |
| 6 | 6 | ||
| 7 | -This project is a starting point for a Flutter application. | 7 | +1. `git clone https://github.com/juliansteenbakker/mobile_scanner.git` |
| 8 | +2. `cd mobile_scanner/examples/lib` | ||
| 9 | +3. `flutter pub get` | ||
| 10 | +4. `flutter run` | ||
| 8 | 11 | ||
| 9 | -A few resources to get you started if this is your first Flutter project: | 12 | +## Examples Overview |
| 10 | 13 | ||
| 11 | -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) | ||
| 12 | -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) | 14 | +### With Controller |
| 13 | 15 | ||
| 14 | -For help getting started with Flutter development, view the | ||
| 15 | -[online documentation](https://docs.flutter.dev/), which offers tutorials, | ||
| 16 | -samples, guidance on mobile development, and a full API reference. | 16 | +Scanner widget with control buttons overlay. Shows first detected barcode. |
| 17 | +(See ListView example for detecting and displaying multiple barcodes at the same time.) | ||
| 18 | + | ||
| 19 | +* Displays Flashlight, SwitchCamera and Start/Stop buttons. | ||
| 20 | +* Uses `MobileScannerController` to start/stop, toggle flashlight, switch camera. | ||
| 21 | +* Displays Gallery button to use images as source for analysis. | ||
| 22 | +* Handles changes in AppLifecycleState. | ||
| 23 | + | ||
| 24 | +### With ListView | ||
| 25 | + | ||
| 26 | +Scanner widget with control buttons overlay. Shows all barcodes detected at the same time in a ListView. | ||
| 27 | + | ||
| 28 | +* Displays Flashlight, SwitchCamera and Start/Stop buttons. | ||
| 29 | +* Uses `MobileScannerController` to start/stop, toggle flashlight, switch camera. | ||
| 30 | +* Displays Gallery button to use images as source for analysis. | ||
| 31 | + | ||
| 32 | +### With Zoom Slider | ||
| 33 | + | ||
| 34 | +Scanner widget with control buttons and zoom slider overlay. Shows first detected barcode. | ||
| 35 | + | ||
| 36 | +* Displays Flashlight, SwitchCamera and Start/Stop buttons and zoom slider. | ||
| 37 | +* Uses `MobileScannerController` to start/stop, toggle flashlight, switch camera, set zoom scale. | ||
| 38 | +* Displays Gallery button to use images as source for analysis. | ||
| 39 | + | ||
| 40 | +### With Controller (returning image) | ||
| 41 | + | ||
| 42 | +Scanner widget with control buttons overlay. Shows the first detected barcode and the captured image. | ||
| 43 | + | ||
| 44 | +* Displays Flashlight, SwitchCamera and Start/Stop buttons. | ||
| 45 | +* Uses `MobileScannerController` to start/stop, toggle flashlight, switch camera. | ||
| 46 | +* Displays captured image that contains the detected barcode. | ||
| 47 | + | ||
| 48 | +### With Page View | ||
| 49 | + | ||
| 50 | +Scanner widget in one of many pages that can be swiped horizontally. Starts and stops scanner depending on page visibility. | ||
| 51 | + | ||
| 52 | +* Shows first detected barcode. | ||
| 53 | + | ||
| 54 | +### With Scan Window | ||
| 55 | + | ||
| 56 | +Scanner widget with scan window overlay. Barcodes are only detected inside the scan window. | ||
| 57 | + | ||
| 58 | +* Draws scan window - a half-transparent overlay with a cut out middle part. | ||
| 59 | +* Draws bounding box around (first) detected barcode. (not working on every device) | ||
| 60 | + | ||
| 61 | +### With Overlay | ||
| 62 | + | ||
| 63 | +Scanner widget with scan window overlay. Barcodes are only detected inside the scan window. | ||
| 64 | + | ||
| 65 | +* Draws scan window - a half-transparent overlay with a cut out middle part that has a border with rounded corners. | ||
| 66 | +* Displays Flashlight, SwitchCamera buttons. | ||
| 67 | +* Uses `MobileScannerController` to toggle flashlight, switch camera. |
| @@ -25,16 +25,14 @@ if (flutterVersionName == null) { | @@ -25,16 +25,14 @@ if (flutterVersionName == null) { | ||
| 25 | android { | 25 | android { |
| 26 | namespace "dev.steenbakker.mobile_scanner_example" | 26 | namespace "dev.steenbakker.mobile_scanner_example" |
| 27 | compileSdk 34 | 27 | compileSdk 34 |
| 28 | - ndkVersion "25.1.8937393" | ||
| 29 | -// ndkVersion flutter.ndkVersion | ||
| 30 | 28 | ||
| 31 | compileOptions { | 29 | compileOptions { |
| 32 | - sourceCompatibility JavaVersion.VERSION_17 | ||
| 33 | - targetCompatibility JavaVersion.VERSION_17 | 30 | + sourceCompatibility JavaVersion.VERSION_1_8 |
| 31 | + targetCompatibility JavaVersion.VERSION_1_8 | ||
| 34 | } | 32 | } |
| 35 | 33 | ||
| 36 | kotlinOptions { | 34 | kotlinOptions { |
| 37 | - jvmTarget = '17' | 35 | + jvmTarget = '1.8' |
| 38 | } | 36 | } |
| 39 | 37 | ||
| 40 | sourceSets { | 38 | sourceSets { |
| @@ -62,7 +60,3 @@ android { | @@ -62,7 +60,3 @@ android { | ||
| 62 | flutter { | 60 | flutter { |
| 63 | source '../..' | 61 | source '../..' |
| 64 | } | 62 | } |
| 65 | - | ||
| 66 | -dependencies { | ||
| 67 | - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" | ||
| 68 | -} |
| 1 | -buildscript { | ||
| 2 | - ext.kotlin_version = '1.9.22' | ||
| 3 | - repositories { | ||
| 4 | - google() | ||
| 5 | - mavenCentral() | ||
| 6 | - } | ||
| 7 | - | ||
| 8 | - dependencies { | ||
| 9 | - classpath 'com.android.tools.build:gradle:8.2.2' | ||
| 10 | - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" | ||
| 11 | - } | ||
| 12 | -} | ||
| 13 | - | ||
| 14 | allprojects { | 1 | allprojects { |
| 15 | repositories { | 2 | repositories { |
| 16 | google() | 3 | google() |
| 1 | +#Thu May 02 10:24:49 CEST 2024 | ||
| 1 | distributionBase=GRADLE_USER_HOME | 2 | distributionBase=GRADLE_USER_HOME |
| 2 | distributionPath=wrapper/dists | 3 | distributionPath=wrapper/dists |
| 3 | -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip | 4 | +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip |
| 4 | zipStoreBase=GRADLE_USER_HOME | 5 | zipStoreBase=GRADLE_USER_HOME |
| 5 | zipStorePath=wrapper/dists | 6 | zipStorePath=wrapper/dists |
| @@ -5,12 +5,21 @@ pluginManagement { | @@ -5,12 +5,21 @@ pluginManagement { | ||
| 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") | 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") |
| 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" | 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" |
| 7 | return flutterSdkPath | 7 | return flutterSdkPath |
| 8 | - } | ||
| 9 | - settings.ext.flutterSdkPath = flutterSdkPath() | 8 | + }() |
| 9 | + | ||
| 10 | + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") | ||
| 10 | 11 | ||
| 11 | - includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") | 12 | + repositories { |
| 13 | + google() | ||
| 14 | + mavenCentral() | ||
| 15 | + gradlePluginPortal() | ||
| 16 | + } | ||
| 12 | } | 17 | } |
| 13 | 18 | ||
| 14 | -include ":app" | 19 | +plugins { |
| 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 | ||
| 23 | +} | ||
| 15 | 24 | ||
| 16 | -apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" | 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>11.0</string> | 24 | + <string>12.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, '11.0' | 2 | +# platform :ios, '12.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' |
| @@ -41,7 +41,7 @@ post_install do |installer| | @@ -41,7 +41,7 @@ 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| | 43 | target.build_configurations.each do |config| |
| 44 | - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' | 44 | + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' |
| 45 | end | 45 | end |
| 46 | end | 46 | end |
| 47 | end | 47 | end |
| @@ -198,6 +198,7 @@ | @@ -198,6 +198,7 @@ | ||
| 198 | 9705A1C41CF9048500538489 /* Embed Frameworks */, | 198 | 9705A1C41CF9048500538489 /* Embed Frameworks */, |
| 199 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, | 199 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, |
| 200 | 3DBCC0215D7BED1D9A756EA3 /* [CP] Embed Pods Frameworks */, | 200 | 3DBCC0215D7BED1D9A756EA3 /* [CP] Embed Pods Frameworks */, |
| 201 | + BB0C8EA8DA81A75DE53F052F /* [CP] Copy Pods Resources */, | ||
| 201 | ); | 202 | ); |
| 202 | buildRules = ( | 203 | buildRules = ( |
| 203 | ); | 204 | ); |
| @@ -215,7 +216,7 @@ | @@ -215,7 +216,7 @@ | ||
| 215 | isa = PBXProject; | 216 | isa = PBXProject; |
| 216 | attributes = { | 217 | attributes = { |
| 217 | BuildIndependentTargetsInParallel = YES; | 218 | BuildIndependentTargetsInParallel = YES; |
| 218 | - LastUpgradeCheck = 1430; | 219 | + LastUpgradeCheck = 1510; |
| 219 | ORGANIZATIONNAME = ""; | 220 | ORGANIZATIONNAME = ""; |
| 220 | TargetAttributes = { | 221 | TargetAttributes = { |
| 221 | 331C8080294A63A400263BE5 = { | 222 | 331C8080294A63A400263BE5 = { |
| @@ -317,6 +318,23 @@ | @@ -317,6 +318,23 @@ | ||
| 317 | shellPath = /bin/sh; | 318 | shellPath = /bin/sh; |
| 318 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; | 319 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; |
| 319 | }; | 320 | }; |
| 321 | + BB0C8EA8DA81A75DE53F052F /* [CP] Copy Pods Resources */ = { | ||
| 322 | + isa = PBXShellScriptBuildPhase; | ||
| 323 | + buildActionMask = 2147483647; | ||
| 324 | + files = ( | ||
| 325 | + ); | ||
| 326 | + inputFileListPaths = ( | ||
| 327 | + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", | ||
| 328 | + ); | ||
| 329 | + name = "[CP] Copy Pods Resources"; | ||
| 330 | + outputFileListPaths = ( | ||
| 331 | + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", | ||
| 332 | + ); | ||
| 333 | + runOnlyForDeploymentPostprocessing = 0; | ||
| 334 | + shellPath = /bin/sh; | ||
| 335 | + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; | ||
| 336 | + showEnvVarsInLog = 0; | ||
| 337 | + }; | ||
| 320 | C7DE006A696F551C4E067E41 /* [CP] Check Pods Manifest.lock */ = { | 338 | C7DE006A696F551C4E067E41 /* [CP] Check Pods Manifest.lock */ = { |
| 321 | isa = PBXShellScriptBuildPhase; | 339 | isa = PBXShellScriptBuildPhase; |
| 322 | buildActionMask = 2147483647; | 340 | buildActionMask = 2147483647; |
| @@ -452,7 +470,7 @@ | @@ -452,7 +470,7 @@ | ||
| 452 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | 470 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
| 453 | GCC_WARN_UNUSED_FUNCTION = YES; | 471 | GCC_WARN_UNUSED_FUNCTION = YES; |
| 454 | GCC_WARN_UNUSED_VARIABLE = YES; | 472 | GCC_WARN_UNUSED_VARIABLE = YES; |
| 455 | - IPHONEOS_DEPLOYMENT_TARGET = 11.0; | 473 | + IPHONEOS_DEPLOYMENT_TARGET = 12.0; |
| 456 | MTL_ENABLE_DEBUG_INFO = NO; | 474 | MTL_ENABLE_DEBUG_INFO = NO; |
| 457 | SDKROOT = iphoneos; | 475 | SDKROOT = iphoneos; |
| 458 | SUPPORTED_PLATFORMS = iphoneos; | 476 | SUPPORTED_PLATFORMS = iphoneos; |
| @@ -495,7 +513,7 @@ | @@ -495,7 +513,7 @@ | ||
| 495 | CURRENT_PROJECT_VERSION = 1; | 513 | CURRENT_PROJECT_VERSION = 1; |
| 496 | GENERATE_INFOPLIST_FILE = YES; | 514 | GENERATE_INFOPLIST_FILE = YES; |
| 497 | MARKETING_VERSION = 1.0; | 515 | MARKETING_VERSION = 1.0; |
| 498 | - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile-scanner.RunnerTests; | 516 | + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner.RunnerTests"; |
| 499 | PRODUCT_NAME = "$(TARGET_NAME)"; | 517 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 500 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; | 518 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; |
| 501 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | 519 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; |
| @@ -513,7 +531,7 @@ | @@ -513,7 +531,7 @@ | ||
| 513 | CURRENT_PROJECT_VERSION = 1; | 531 | CURRENT_PROJECT_VERSION = 1; |
| 514 | GENERATE_INFOPLIST_FILE = YES; | 532 | GENERATE_INFOPLIST_FILE = YES; |
| 515 | MARKETING_VERSION = 1.0; | 533 | MARKETING_VERSION = 1.0; |
| 516 | - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile-scanner.RunnerTests; | 534 | + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner.RunnerTests"; |
| 517 | PRODUCT_NAME = "$(TARGET_NAME)"; | 535 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 518 | SWIFT_VERSION = 5.0; | 536 | SWIFT_VERSION = 5.0; |
| 519 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; | 537 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; |
| @@ -529,7 +547,7 @@ | @@ -529,7 +547,7 @@ | ||
| 529 | CURRENT_PROJECT_VERSION = 1; | 547 | CURRENT_PROJECT_VERSION = 1; |
| 530 | GENERATE_INFOPLIST_FILE = YES; | 548 | GENERATE_INFOPLIST_FILE = YES; |
| 531 | MARKETING_VERSION = 1.0; | 549 | MARKETING_VERSION = 1.0; |
| 532 | - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile-scanner.RunnerTests; | 550 | + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner.RunnerTests"; |
| 533 | PRODUCT_NAME = "$(TARGET_NAME)"; | 551 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 534 | SWIFT_VERSION = 5.0; | 552 | SWIFT_VERSION = 5.0; |
| 535 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; | 553 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; |
| @@ -583,7 +601,7 @@ | @@ -583,7 +601,7 @@ | ||
| 583 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | 601 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
| 584 | GCC_WARN_UNUSED_FUNCTION = YES; | 602 | GCC_WARN_UNUSED_FUNCTION = YES; |
| 585 | GCC_WARN_UNUSED_VARIABLE = YES; | 603 | GCC_WARN_UNUSED_VARIABLE = YES; |
| 586 | - IPHONEOS_DEPLOYMENT_TARGET = 11.0; | 604 | + IPHONEOS_DEPLOYMENT_TARGET = 12.0; |
| 587 | MTL_ENABLE_DEBUG_INFO = YES; | 605 | MTL_ENABLE_DEBUG_INFO = YES; |
| 588 | ONLY_ACTIVE_ARCH = YES; | 606 | ONLY_ACTIVE_ARCH = YES; |
| 589 | SDKROOT = iphoneos; | 607 | SDKROOT = iphoneos; |
| @@ -632,7 +650,7 @@ | @@ -632,7 +650,7 @@ | ||
| 632 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | 650 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
| 633 | GCC_WARN_UNUSED_FUNCTION = YES; | 651 | GCC_WARN_UNUSED_FUNCTION = YES; |
| 634 | GCC_WARN_UNUSED_VARIABLE = YES; | 652 | GCC_WARN_UNUSED_VARIABLE = YES; |
| 635 | - IPHONEOS_DEPLOYMENT_TARGET = 11.0; | 653 | + IPHONEOS_DEPLOYMENT_TARGET = 12.0; |
| 636 | MTL_ENABLE_DEBUG_INFO = NO; | 654 | MTL_ENABLE_DEBUG_INFO = NO; |
| 637 | SDKROOT = iphoneos; | 655 | SDKROOT = iphoneos; |
| 638 | SUPPORTED_PLATFORMS = iphoneos; | 656 | SUPPORTED_PLATFORMS = iphoneos; |
| @@ -2,6 +2,8 @@ | @@ -2,6 +2,8 @@ | ||
| 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| 3 | <plist version="1.0"> | 3 | <plist version="1.0"> |
| 4 | <dict> | 4 | <dict> |
| 5 | + <key>CADisableMinimumFrameDurationOnPhone</key> | ||
| 6 | + <true/> | ||
| 5 | <key>CFBundleDevelopmentRegion</key> | 7 | <key>CFBundleDevelopmentRegion</key> |
| 6 | <string>$(DEVELOPMENT_LANGUAGE)</string> | 8 | <string>$(DEVELOPMENT_LANGUAGE)</string> |
| 7 | <key>CFBundleDisplayName</key> | 9 | <key>CFBundleDisplayName</key> |
| @@ -28,6 +30,8 @@ | @@ -28,6 +30,8 @@ | ||
| 28 | <string>This app needs camera access to scan QR codes</string> | 30 | <string>This app needs camera access to scan QR codes</string> |
| 29 | <key>NSPhotoLibraryUsageDescription</key> | 31 | <key>NSPhotoLibraryUsageDescription</key> |
| 30 | <string>This app needs photos access to get QR code from photo library</string> | 32 | <string>This app needs photos access to get QR code from photo library</string> |
| 33 | + <key>UIApplicationSupportsIndirectInputEvents</key> | ||
| 34 | + <true/> | ||
| 31 | <key>UILaunchStoryboardName</key> | 35 | <key>UILaunchStoryboardName</key> |
| 32 | <string>LaunchScreen</string> | 36 | <string>LaunchScreen</string> |
| 33 | <key>UIMainStoryboardFile</key> | 37 | <key>UIMainStoryboardFile</key> |
| @@ -47,9 +51,5 @@ | @@ -47,9 +51,5 @@ | ||
| 47 | </array> | 51 | </array> |
| 48 | <key>UIViewControllerBasedStatusBarAppearance</key> | 52 | <key>UIViewControllerBasedStatusBarAppearance</key> |
| 49 | <false/> | 53 | <false/> |
| 50 | - <key>CADisableMinimumFrameDurationOnPhone</key> | ||
| 51 | - <true/> | ||
| 52 | - <key>UIApplicationSupportsIndirectInputEvents</key> | ||
| 53 | - <true/> | ||
| 54 | </dict> | 54 | </dict> |
| 55 | </plist> | 55 | </plist> |
| @@ -16,12 +16,9 @@ class BarcodeScannerWithController extends StatefulWidget { | @@ -16,12 +16,9 @@ class BarcodeScannerWithController extends StatefulWidget { | ||
| 16 | class _BarcodeScannerWithControllerState | 16 | class _BarcodeScannerWithControllerState |
| 17 | extends State<BarcodeScannerWithController> with WidgetsBindingObserver { | 17 | extends State<BarcodeScannerWithController> with WidgetsBindingObserver { |
| 18 | final MobileScannerController controller = MobileScannerController( | 18 | final MobileScannerController controller = MobileScannerController( |
| 19 | - torchEnabled: true, useNewCameraSelector: true, | ||
| 20 | - // formats: [BarcodeFormat.qrCode] | ||
| 21 | - // facing: CameraFacing.front, | ||
| 22 | - // detectionSpeed: DetectionSpeed.normal | ||
| 23 | - // detectionTimeoutMs: 1000, | ||
| 24 | - // returnImage: false, | 19 | + autoStart: false, |
| 20 | + torchEnabled: true, | ||
| 21 | + useNewCameraSelector: true, | ||
| 25 | ); | 22 | ); |
| 26 | 23 | ||
| 27 | Barcode? _barcode; | 24 | Barcode? _barcode; |
| @@ -63,7 +60,9 @@ class _BarcodeScannerWithControllerState | @@ -63,7 +60,9 @@ class _BarcodeScannerWithControllerState | ||
| 63 | 60 | ||
| 64 | @override | 61 | @override |
| 65 | void didChangeAppLifecycleState(AppLifecycleState state) { | 62 | void didChangeAppLifecycleState(AppLifecycleState state) { |
| 66 | - super.didChangeAppLifecycleState(state); | 63 | + if (!controller.value.isInitialized) { |
| 64 | + return; | ||
| 65 | + } | ||
| 67 | 66 | ||
| 68 | switch (state) { | 67 | switch (state) { |
| 69 | case AppLifecycleState.detached: | 68 | case AppLifecycleState.detached: |
| @@ -15,20 +15,8 @@ class BarcodeScannerListView extends StatefulWidget { | @@ -15,20 +15,8 @@ class BarcodeScannerListView extends StatefulWidget { | ||
| 15 | class _BarcodeScannerListViewState extends State<BarcodeScannerListView> { | 15 | class _BarcodeScannerListViewState extends State<BarcodeScannerListView> { |
| 16 | final MobileScannerController controller = MobileScannerController( | 16 | final MobileScannerController controller = MobileScannerController( |
| 17 | torchEnabled: true, | 17 | torchEnabled: true, |
| 18 | - // formats: [BarcodeFormat.qrCode] | ||
| 19 | - // facing: CameraFacing.front, | ||
| 20 | - // detectionSpeed: DetectionSpeed.normal | ||
| 21 | - // detectionTimeoutMs: 1000, | ||
| 22 | - // returnImage: false, | ||
| 23 | ); | 18 | ); |
| 24 | 19 | ||
| 25 | - @override | ||
| 26 | - void initState() { | ||
| 27 | - super.initState(); | ||
| 28 | - | ||
| 29 | - controller.start(); | ||
| 30 | - } | ||
| 31 | - | ||
| 32 | Widget _buildBarcodesListView() { | 20 | Widget _buildBarcodesListView() { |
| 33 | return StreamBuilder<BarcodeCapture>( | 21 | return StreamBuilder<BarcodeCapture>( |
| 34 | stream: controller.barcodes, | 22 | stream: controller.barcodes, |
| @@ -18,12 +18,6 @@ class _BarcodeScannerPageViewState extends State<BarcodeScannerPageView> { | @@ -18,12 +18,6 @@ class _BarcodeScannerPageViewState extends State<BarcodeScannerPageView> { | ||
| 18 | final PageController pageController = PageController(); | 18 | final PageController pageController = PageController(); |
| 19 | 19 | ||
| 20 | @override | 20 | @override |
| 21 | - void initState() { | ||
| 22 | - super.initState(); | ||
| 23 | - unawaited(controller.start()); | ||
| 24 | - } | ||
| 25 | - | ||
| 26 | - @override | ||
| 27 | Widget build(BuildContext context) { | 21 | Widget build(BuildContext context) { |
| 28 | return Scaffold( | 22 | return Scaffold( |
| 29 | appBar: AppBar(title: const Text('With PageView')), | 23 | appBar: AppBar(title: const Text('With PageView')), |
| @@ -18,20 +18,10 @@ class _BarcodeScannerReturningImageState | @@ -18,20 +18,10 @@ class _BarcodeScannerReturningImageState | ||
| 18 | extends State<BarcodeScannerReturningImage> { | 18 | extends State<BarcodeScannerReturningImage> { |
| 19 | final MobileScannerController controller = MobileScannerController( | 19 | final MobileScannerController controller = MobileScannerController( |
| 20 | torchEnabled: true, | 20 | torchEnabled: true, |
| 21 | - // formats: [BarcodeFormat.qrCode] | ||
| 22 | - // facing: CameraFacing.front, | ||
| 23 | - // detectionSpeed: DetectionSpeed.normal | ||
| 24 | - // detectionTimeoutMs: 1000, | ||
| 25 | returnImage: true, | 21 | returnImage: true, |
| 26 | ); | 22 | ); |
| 27 | 23 | ||
| 28 | @override | 24 | @override |
| 29 | - void initState() { | ||
| 30 | - super.initState(); | ||
| 31 | - controller.start(); | ||
| 32 | - } | ||
| 33 | - | ||
| 34 | - @override | ||
| 35 | Widget build(BuildContext context) { | 25 | Widget build(BuildContext context) { |
| 36 | return Scaffold( | 26 | return Scaffold( |
| 37 | appBar: AppBar(title: const Text('Returning image')), | 27 | appBar: AppBar(title: const Text('Returning image')), |
example/lib/barcode_scanner_simple.dart
0 → 100644
| 1 | +import 'package:flutter/material.dart'; | ||
| 2 | +import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 3 | + | ||
| 4 | +class BarcodeScannerSimple extends StatefulWidget { | ||
| 5 | + const BarcodeScannerSimple({super.key}); | ||
| 6 | + | ||
| 7 | + @override | ||
| 8 | + State<BarcodeScannerSimple> createState() => _BarcodeScannerSimpleState(); | ||
| 9 | +} | ||
| 10 | + | ||
| 11 | +class _BarcodeScannerSimpleState extends State<BarcodeScannerSimple> { | ||
| 12 | + Barcode? _barcode; | ||
| 13 | + | ||
| 14 | + Widget _buildBarcode(Barcode? value) { | ||
| 15 | + if (value == null) { | ||
| 16 | + return const Text( | ||
| 17 | + 'Scan something!', | ||
| 18 | + overflow: TextOverflow.fade, | ||
| 19 | + style: TextStyle(color: Colors.white), | ||
| 20 | + ); | ||
| 21 | + } | ||
| 22 | + | ||
| 23 | + return Text( | ||
| 24 | + value.displayValue ?? 'No display value.', | ||
| 25 | + overflow: TextOverflow.fade, | ||
| 26 | + style: const TextStyle(color: Colors.white), | ||
| 27 | + ); | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + void _handleBarcode(BarcodeCapture barcodes) { | ||
| 31 | + if (mounted) { | ||
| 32 | + setState(() { | ||
| 33 | + _barcode = barcodes.barcodes.firstOrNull; | ||
| 34 | + }); | ||
| 35 | + } | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + @override | ||
| 39 | + Widget build(BuildContext context) { | ||
| 40 | + return Scaffold( | ||
| 41 | + appBar: AppBar(title: const Text('Simple scanner')), | ||
| 42 | + backgroundColor: Colors.black, | ||
| 43 | + body: Stack( | ||
| 44 | + children: [ | ||
| 45 | + MobileScanner( | ||
| 46 | + onDetect: _handleBarcode, | ||
| 47 | + ), | ||
| 48 | + Align( | ||
| 49 | + alignment: Alignment.bottomCenter, | ||
| 50 | + child: Container( | ||
| 51 | + alignment: Alignment.bottomCenter, | ||
| 52 | + height: 100, | ||
| 53 | + color: Colors.black.withOpacity(0.4), | ||
| 54 | + child: Row( | ||
| 55 | + mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 56 | + children: [ | ||
| 57 | + Expanded(child: Center(child: _buildBarcode(_barcode))), | ||
| 58 | + ], | ||
| 59 | + ), | ||
| 60 | + ), | ||
| 61 | + ), | ||
| 62 | + ], | ||
| 63 | + ), | ||
| 64 | + ); | ||
| 65 | + } | ||
| 66 | +} |
| 1 | -import 'dart:io'; | ||
| 2 | - | ||
| 3 | import 'package:flutter/foundation.dart'; | 1 | import 'package:flutter/foundation.dart'; |
| 4 | import 'package:flutter/material.dart'; | 2 | import 'package:flutter/material.dart'; |
| 5 | import 'package:mobile_scanner/mobile_scanner.dart'; | 3 | import 'package:mobile_scanner/mobile_scanner.dart'; |
| @@ -19,13 +17,6 @@ class _BarcodeScannerWithScanWindowState | @@ -19,13 +17,6 @@ class _BarcodeScannerWithScanWindowState | ||
| 19 | extends State<BarcodeScannerWithScanWindow> { | 17 | extends State<BarcodeScannerWithScanWindow> { |
| 20 | final MobileScannerController controller = MobileScannerController(); | 18 | final MobileScannerController controller = MobileScannerController(); |
| 21 | 19 | ||
| 22 | - @override | ||
| 23 | - void initState() { | ||
| 24 | - super.initState(); | ||
| 25 | - | ||
| 26 | - controller.start(); | ||
| 27 | - } | ||
| 28 | - | ||
| 29 | Widget _buildBarcodeOverlay() { | 20 | Widget _buildBarcodeOverlay() { |
| 30 | return ValueListenableBuilder( | 21 | return ValueListenableBuilder( |
| 31 | valueListenable: controller, | 22 | valueListenable: controller, |
| @@ -204,7 +195,7 @@ class BarcodeOverlay extends CustomPainter { | @@ -204,7 +195,7 @@ class BarcodeOverlay extends CustomPainter { | ||
| 204 | final double ratioWidth; | 195 | final double ratioWidth; |
| 205 | final double ratioHeight; | 196 | final double ratioHeight; |
| 206 | 197 | ||
| 207 | - if (!kIsWeb && Platform.isIOS) { | 198 | + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS) { |
| 208 | ratioWidth = barcodeSize.width / adjustedSize.destination.width; | 199 | ratioWidth = barcodeSize.width / adjustedSize.destination.width; |
| 209 | ratioHeight = barcodeSize.height / adjustedSize.destination.height; | 200 | ratioHeight = barcodeSize.height / adjustedSize.destination.height; |
| 210 | } else { | 201 | } else { |
| @@ -22,12 +22,6 @@ class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom> { | @@ -22,12 +22,6 @@ class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom> { | ||
| 22 | 22 | ||
| 23 | double _zoomFactor = 0.0; | 23 | double _zoomFactor = 0.0; |
| 24 | 24 | ||
| 25 | - @override | ||
| 26 | - void initState() { | ||
| 27 | - super.initState(); | ||
| 28 | - controller.start(); | ||
| 29 | - } | ||
| 30 | - | ||
| 31 | Widget _buildZoomScaleSlider() { | 25 | Widget _buildZoomScaleSlider() { |
| 32 | return ValueListenableBuilder( | 26 | return ValueListenableBuilder( |
| 33 | valueListenable: controller, | 27 | valueListenable: controller, |
| @@ -3,6 +3,7 @@ import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; | @@ -3,6 +3,7 @@ import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; | ||
| 3 | import 'package:mobile_scanner_example/barcode_scanner_listview.dart'; | 3 | import 'package:mobile_scanner_example/barcode_scanner_listview.dart'; |
| 4 | import 'package:mobile_scanner_example/barcode_scanner_pageview.dart'; | 4 | import 'package:mobile_scanner_example/barcode_scanner_pageview.dart'; |
| 5 | import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart'; | 5 | import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart'; |
| 6 | +import 'package:mobile_scanner_example/barcode_scanner_simple.dart'; | ||
| 6 | import 'package:mobile_scanner_example/barcode_scanner_window.dart'; | 7 | import 'package:mobile_scanner_example/barcode_scanner_window.dart'; |
| 7 | import 'package:mobile_scanner_example/barcode_scanner_zoom.dart'; | 8 | import 'package:mobile_scanner_example/barcode_scanner_zoom.dart'; |
| 8 | import 'package:mobile_scanner_example/mobile_scanner_overlay.dart'; | 9 | import 'package:mobile_scanner_example/mobile_scanner_overlay.dart'; |
| @@ -31,6 +32,16 @@ class MyHome extends StatelessWidget { | @@ -31,6 +32,16 @@ class MyHome extends StatelessWidget { | ||
| 31 | onPressed: () { | 32 | onPressed: () { |
| 32 | Navigator.of(context).push( | 33 | Navigator.of(context).push( |
| 33 | MaterialPageRoute( | 34 | MaterialPageRoute( |
| 35 | + builder: (context) => const BarcodeScannerSimple(), | ||
| 36 | + ), | ||
| 37 | + ); | ||
| 38 | + }, | ||
| 39 | + child: const Text('MobileScanner Simple'), | ||
| 40 | + ), | ||
| 41 | + ElevatedButton( | ||
| 42 | + onPressed: () { | ||
| 43 | + Navigator.of(context).push( | ||
| 44 | + MaterialPageRoute( | ||
| 34 | builder: (context) => const BarcodeScannerListView(), | 45 | builder: (context) => const BarcodeScannerListView(), |
| 35 | ), | 46 | ), |
| 36 | ); | 47 | ); |
| @@ -16,12 +16,6 @@ class _BarcodeScannerWithOverlayState extends State<BarcodeScannerWithOverlay> { | @@ -16,12 +16,6 @@ class _BarcodeScannerWithOverlayState extends State<BarcodeScannerWithOverlay> { | ||
| 16 | ); | 16 | ); |
| 17 | 17 | ||
| 18 | @override | 18 | @override |
| 19 | - void initState() { | ||
| 20 | - super.initState(); | ||
| 21 | - controller.start(); | ||
| 22 | - } | ||
| 23 | - | ||
| 24 | - @override | ||
| 25 | Widget build(BuildContext context) { | 19 | Widget build(BuildContext context) { |
| 26 | final scanWindow = Rect.fromCenter( | 20 | final scanWindow = Rect.fromCenter( |
| 27 | center: MediaQuery.sizeOf(context).center(Offset.zero), | 21 | center: MediaQuery.sizeOf(context).center(Offset.zero), |
| @@ -138,6 +138,15 @@ class ToggleFlashlightButton extends StatelessWidget { | @@ -138,6 +138,15 @@ class ToggleFlashlightButton extends StatelessWidget { | ||
| 138 | } | 138 | } |
| 139 | 139 | ||
| 140 | switch (state.torchState) { | 140 | switch (state.torchState) { |
| 141 | + case TorchState.auto: | ||
| 142 | + return IconButton( | ||
| 143 | + color: Colors.white, | ||
| 144 | + iconSize: 32.0, | ||
| 145 | + icon: const Icon(Icons.flash_auto), | ||
| 146 | + onPressed: () async { | ||
| 147 | + await controller.toggleTorch(); | ||
| 148 | + }, | ||
| 149 | + ); | ||
| 141 | case TorchState.off: | 150 | case TorchState.off: |
| 142 | return IconButton( | 151 | return IconButton( |
| 143 | color: Colors.white, | 152 | color: Colors.white, |
| @@ -259,7 +259,7 @@ | @@ -259,7 +259,7 @@ | ||
| 259 | isa = PBXProject; | 259 | isa = PBXProject; |
| 260 | attributes = { | 260 | attributes = { |
| 261 | LastSwiftUpdateCheck = 0920; | 261 | LastSwiftUpdateCheck = 0920; |
| 262 | - LastUpgradeCheck = 1430; | 262 | + LastUpgradeCheck = 1510; |
| 263 | ORGANIZATIONNAME = ""; | 263 | ORGANIZATIONNAME = ""; |
| 264 | TargetAttributes = { | 264 | TargetAttributes = { |
| 265 | 331C80D4294CF70F00263BE5 = { | 265 | 331C80D4294CF70F00263BE5 = { |
| @@ -6,8 +6,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | @@ -6,8 +6,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | ||
| 6 | version: 0.0.1 | 6 | version: 0.0.1 |
| 7 | 7 | ||
| 8 | environment: | 8 | environment: |
| 9 | - sdk: ">=3.2.0 <4.0.0" | ||
| 10 | - flutter: ">=3.16.0" | 9 | + sdk: ">=3.3.0 <4.0.0" |
| 10 | + flutter: ">=3.19.0" | ||
| 11 | 11 | ||
| 12 | # Dependencies specify other packages that your package needs in order to work. | 12 | # Dependencies specify other packages that your package needs in order to work. |
| 13 | # To automatically upgrade your package dependencies to the latest versions | 13 | # To automatically upgrade your package dependencies to the latest versions |
| @@ -268,12 +268,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -268,12 +268,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 268 | // as they interact with the hardware camera. | 268 | // as they interact with the hardware camera. |
| 269 | if (torch) { | 269 | if (torch) { |
| 270 | DispatchQueue.main.async { | 270 | DispatchQueue.main.async { |
| 271 | - do { | ||
| 272 | - try self.toggleTorch(.on) | ||
| 273 | - } catch { | ||
| 274 | - // If the torch does not turn on, | ||
| 275 | - // continue with the capture session anyway. | ||
| 276 | - } | 271 | + self.turnTorchOn() |
| 277 | } | 272 | } |
| 278 | } | 273 | } |
| 279 | 274 | ||
| @@ -292,13 +287,12 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -292,13 +287,12 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 292 | // as this does not change the configuration of the hardware camera. | 287 | // as this does not change the configuration of the hardware camera. |
| 293 | let dimensions = CMVideoFormatDescriptionGetDimensions( | 288 | let dimensions = CMVideoFormatDescriptionGetDimensions( |
| 294 | device.activeFormat.formatDescription) | 289 | device.activeFormat.formatDescription) |
| 295 | - let hasTorch = device.hasTorch | ||
| 296 | 290 | ||
| 297 | completion( | 291 | completion( |
| 298 | MobileScannerStartParameters( | 292 | MobileScannerStartParameters( |
| 299 | width: Double(dimensions.height), | 293 | width: Double(dimensions.height), |
| 300 | height: Double(dimensions.width), | 294 | height: Double(dimensions.width), |
| 301 | - hasTorch: hasTorch, | 295 | + currentTorchState: device.hasTorch ? device.torchMode.rawValue : -1, |
| 302 | textureId: self.textureId ?? 0 | 296 | textureId: self.textureId ?? 0 |
| 303 | ) | 297 | ) |
| 304 | ) | 298 | ) |
| @@ -355,30 +349,67 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -355,30 +349,67 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 355 | textureId = nil | 349 | textureId = nil |
| 356 | } | 350 | } |
| 357 | 351 | ||
| 358 | - /// Set the torch mode. | 352 | + /// Toggle the torch. |
| 359 | /// | 353 | /// |
| 360 | /// This method should be called on the main DispatchQueue. | 354 | /// This method should be called on the main DispatchQueue. |
| 361 | - func toggleTorch(_ torch: AVCaptureDevice.TorchMode) throws { | 355 | + func toggleTorch() { |
| 362 | guard let device = self.device else { | 356 | guard let device = self.device else { |
| 363 | return | 357 | return |
| 364 | } | 358 | } |
| 365 | 359 | ||
| 366 | - if (!device.hasTorch || !device.isTorchAvailable || !device.isTorchModeSupported(torch)) { | 360 | + if (!device.hasTorch || !device.isTorchAvailable) { |
| 367 | return | 361 | return |
| 368 | } | 362 | } |
| 369 | 363 | ||
| 370 | - if (device.torchMode != torch) { | 364 | + var newTorchMode: AVCaptureDevice.TorchMode = device.torchMode |
| 365 | + | ||
| 366 | + switch(device.torchMode) { | ||
| 367 | + case AVCaptureDevice.TorchMode.auto: | ||
| 368 | + newTorchMode = device.isTorchActive ? AVCaptureDevice.TorchMode.off : AVCaptureDevice.TorchMode.on | ||
| 369 | + break; | ||
| 370 | + case AVCaptureDevice.TorchMode.off: | ||
| 371 | + newTorchMode = AVCaptureDevice.TorchMode.on | ||
| 372 | + break; | ||
| 373 | + case AVCaptureDevice.TorchMode.on: | ||
| 374 | + newTorchMode = AVCaptureDevice.TorchMode.off | ||
| 375 | + break; | ||
| 376 | + default: | ||
| 377 | + return; | ||
| 378 | + } | ||
| 379 | + | ||
| 380 | + if (!device.isTorchModeSupported(newTorchMode) || device.torchMode == newTorchMode) { | ||
| 381 | + return; | ||
| 382 | + } | ||
| 383 | + | ||
| 384 | + do { | ||
| 371 | try device.lockForConfiguration() | 385 | try device.lockForConfiguration() |
| 372 | - device.torchMode = torch | 386 | + device.torchMode = newTorchMode |
| 373 | device.unlockForConfiguration() | 387 | device.unlockForConfiguration() |
| 388 | + } catch(_) {} | ||
| 389 | + } | ||
| 390 | + | ||
| 391 | + /// Turn the torch on. | ||
| 392 | + private func turnTorchOn() { | ||
| 393 | + guard let device = self.device else { | ||
| 394 | + return | ||
| 395 | + } | ||
| 396 | + | ||
| 397 | + if (!device.hasTorch || !device.isTorchAvailable || !device.isTorchModeSupported(.on) || device.torchMode == .on) { | ||
| 398 | + return | ||
| 374 | } | 399 | } |
| 400 | + | ||
| 401 | + do { | ||
| 402 | + try device.lockForConfiguration() | ||
| 403 | + device.torchMode = .on | ||
| 404 | + device.unlockForConfiguration() | ||
| 405 | + } catch(_) {} | ||
| 375 | } | 406 | } |
| 376 | 407 | ||
| 377 | // Observer for torch state | 408 | // Observer for torch state |
| 378 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { | 409 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { |
| 379 | switch keyPath { | 410 | switch keyPath { |
| 380 | case "torchMode": | 411 | case "torchMode": |
| 381 | - // off = 0; on = 1; auto = 2 | 412 | + // Off = 0, On = 1, Auto = 2 |
| 382 | let state = change?[.newKey] as? Int | 413 | let state = change?[.newKey] as? Int |
| 383 | torchModeChangeCallback(state) | 414 | torchModeChangeCallback(state) |
| 384 | case "videoZoomFactor": | 415 | case "videoZoomFactor": |
| @@ -490,7 +521,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -490,7 +521,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 490 | struct MobileScannerStartParameters { | 521 | struct MobileScannerStartParameters { |
| 491 | var width: Double = 0.0 | 522 | var width: Double = 0.0 |
| 492 | var height: Double = 0.0 | 523 | var height: Double = 0.0 |
| 493 | - var hasTorch = false | 524 | + var currentTorchState: Int = -1 |
| 494 | var textureId: Int64 = 0 | 525 | var textureId: Int64 = 0 |
| 495 | } | 526 | } |
| 496 | } | 527 | } |
| @@ -82,8 +82,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -82,8 +82,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 82 | pause(result) | 82 | pause(result) |
| 83 | case "stop": | 83 | case "stop": |
| 84 | stop(result) | 84 | stop(result) |
| 85 | - case "torch": | ||
| 86 | - toggleTorch(call, result) | 85 | + case "toggleTorch": |
| 86 | + toggleTorch(result) | ||
| 87 | case "analyzeImage": | 87 | case "analyzeImage": |
| 88 | analyzeImage(call, result) | 88 | analyzeImage(call, result) |
| 89 | case "setScale": | 89 | case "setScale": |
| @@ -127,7 +127,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -127,7 +127,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 127 | result([ | 127 | result([ |
| 128 | "textureId": parameters.textureId, | 128 | "textureId": parameters.textureId, |
| 129 | "size": ["width": parameters.width, "height": parameters.height], | 129 | "size": ["width": parameters.width, "height": parameters.height], |
| 130 | - "torchable": parameters.hasTorch]) | 130 | + "currentTorchState": parameters.currentTorchState, |
| 131 | + ]) | ||
| 131 | } | 132 | } |
| 132 | } | 133 | } |
| 133 | } catch MobileScannerError.alreadyStarted { | 134 | } catch MobileScannerError.alreadyStarted { |
| @@ -166,13 +167,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -166,13 +167,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 166 | } | 167 | } |
| 167 | 168 | ||
| 168 | /// Toggles the torch. | 169 | /// Toggles the torch. |
| 169 | - private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 170 | - do { | ||
| 171 | - try mobileScanner.toggleTorch(call.arguments as? Int == 1 ? .on : .off) | ||
| 172 | - result(nil) | ||
| 173 | - } catch { | ||
| 174 | - result(FlutterError(code: "MobileScanner", message: error.localizedDescription, details: nil)) | ||
| 175 | - } | 170 | + private func toggleTorch(_ result: @escaping FlutterResult) { |
| 171 | + mobileScanner.toggleTorch() | ||
| 172 | + result(nil) | ||
| 176 | } | 173 | } |
| 177 | 174 | ||
| 178 | /// Sets the zoomScale. | 175 | /// Sets the zoomScale. |
| @@ -8,31 +8,6 @@ extension CVBuffer { | @@ -8,31 +8,6 @@ extension CVBuffer { | ||
| 8 | let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent) | 8 | let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent) |
| 9 | return UIImage(cgImage: cgImage!) | 9 | return UIImage(cgImage: cgImage!) |
| 10 | } | 10 | } |
| 11 | - | ||
| 12 | - var image1: UIImage { | ||
| 13 | - // Lock the base address of the pixel buffer | ||
| 14 | - CVPixelBufferLockBaseAddress(self, CVPixelBufferLockFlags.readOnly) | ||
| 15 | - // Get the number of bytes per row for the pixel buffer | ||
| 16 | - let baseAddress = CVPixelBufferGetBaseAddress(self) | ||
| 17 | - // Get the number of bytes per row for the pixel buffer | ||
| 18 | - let bytesPerRow = CVPixelBufferGetBytesPerRow(self) | ||
| 19 | - // Get the pixel buffer width and height | ||
| 20 | - let width = CVPixelBufferGetWidth(self) | ||
| 21 | - let height = CVPixelBufferGetHeight(self) | ||
| 22 | - // Create a device-dependent RGB color space | ||
| 23 | - let colorSpace = CGColorSpaceCreateDeviceRGB() | ||
| 24 | - // Create a bitmap graphics context with the sample buffer data | ||
| 25 | - var bitmapInfo = CGBitmapInfo.byteOrder32Little.rawValue | ||
| 26 | - bitmapInfo |= CGImageAlphaInfo.premultipliedFirst.rawValue & CGBitmapInfo.alphaInfoMask.rawValue | ||
| 27 | - //let bitmapInfo: UInt32 = CGBitmapInfo.alphaInfoMask.rawValue | ||
| 28 | - let context = CGContext(data: baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) | ||
| 29 | - // Create a Quartz image from the pixel data in the bitmap graphics context | ||
| 30 | - let quartzImage = context?.makeImage() | ||
| 31 | - // Unlock the pixel buffer | ||
| 32 | - CVPixelBufferUnlockBaseAddress(self, CVPixelBufferLockFlags.readOnly) | ||
| 33 | - // Create an image object from the Quartz image | ||
| 34 | - return UIImage(cgImage: quartzImage!) | ||
| 35 | - } | ||
| 36 | } | 11 | } |
| 37 | 12 | ||
| 38 | extension UIDeviceOrientation { | 13 | extension UIDeviceOrientation { |
ios/Resources/PrivacyInfo.xcprivacy
0 → 100644
| 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 | + <key>NSPrivacyAccessedAPITypes</key> | ||
| 6 | + <array/> | ||
| 7 | + <key>NSPrivacyCollectedDataTypes</key> | ||
| 8 | + <array/> | ||
| 9 | + <key>NSPrivacyTrackingDomains</key> | ||
| 10 | + <array/> | ||
| 11 | + <key>NSPrivacyTracking</key> | ||
| 12 | + <false/> | ||
| 13 | +</dict> | ||
| 14 | +</plist> |
| @@ -4,21 +4,22 @@ | @@ -4,21 +4,22 @@ | ||
| 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 = '3.5.6' | 7 | + s.version = '5.1.1' |
| 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. |
| 11 | DESC | 11 | DESC |
| 12 | - s.homepage = 'http://example.com' | 12 | + s.homepage = 'https://github.com/juliansteenbakker/mobile_scanner' |
| 13 | s.license = { :file => '../LICENSE' } | 13 | s.license = { :file => '../LICENSE' } |
| 14 | - s.author = { 'Your Company' => 'email@example.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 = 'Classes/**/*' |
| 17 | s.dependency 'Flutter' | 17 | s.dependency 'Flutter' |
| 18 | - s.dependency 'GoogleMLKit/BarcodeScanning', '~> 4.0.0' | ||
| 19 | - s.platform = :ios, '11.0' | 18 | + s.dependency 'GoogleMLKit/BarcodeScanning', '~> 6.0.0' |
| 19 | + s.platform = :ios, '12.0' | ||
| 20 | s.static_framework = true | 20 | s.static_framework = true |
| 21 | # Flutter.framework does not contain a i386 slice. | 21 | # Flutter.framework does not contain a i386 slice. |
| 22 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } | 22 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } |
| 23 | s.swift_version = '5.0' | 23 | s.swift_version = '5.0' |
| 24 | + s.resource_bundles = { 'mobile_scanner_privacy' => ['Resources/PrivacyInfo.xcprivacy'] } | ||
| 24 | end | 25 | end |
| 1 | /// The state of the flashlight. | 1 | /// The state of the flashlight. |
| 2 | enum TorchState { | 2 | enum TorchState { |
| 3 | + /// The flashlight turns on automatically in low light conditions. | ||
| 4 | + /// | ||
| 5 | + /// This is currently only supported on iOS and MacOS. | ||
| 6 | + auto(2), | ||
| 7 | + | ||
| 3 | /// The flashlight is off. | 8 | /// The flashlight is off. |
| 4 | off(0), | 9 | off(0), |
| 5 | 10 | ||
| @@ -7,18 +12,20 @@ enum TorchState { | @@ -7,18 +12,20 @@ enum TorchState { | ||
| 7 | on(1), | 12 | on(1), |
| 8 | 13 | ||
| 9 | /// The flashlight is unavailable. | 14 | /// The flashlight is unavailable. |
| 10 | - unavailable(2); | 15 | + unavailable(-1); |
| 11 | 16 | ||
| 12 | const TorchState(this.rawValue); | 17 | const TorchState(this.rawValue); |
| 13 | 18 | ||
| 14 | factory TorchState.fromRawValue(int value) { | 19 | factory TorchState.fromRawValue(int value) { |
| 15 | switch (value) { | 20 | switch (value) { |
| 21 | + case -1: | ||
| 22 | + return TorchState.unavailable; | ||
| 16 | case 0: | 23 | case 0: |
| 17 | return TorchState.off; | 24 | return TorchState.off; |
| 18 | case 1: | 25 | case 1: |
| 19 | return TorchState.on; | 26 | return TorchState.on; |
| 20 | case 2: | 27 | case 2: |
| 21 | - return TorchState.unavailable; | 28 | + return TorchState.auto; |
| 22 | default: | 29 | default: |
| 23 | throw ArgumentError.value(value, 'value', 'Invalid raw value.'); | 30 | throw ArgumentError.value(value, 'value', 'Invalid raw value.'); |
| 24 | } | 31 | } |
| 1 | import 'dart:async'; | 1 | import 'dart:async'; |
| 2 | -import 'dart:io'; | ||
| 3 | 2 | ||
| 3 | +import 'package:flutter/foundation.dart'; | ||
| 4 | import 'package:flutter/services.dart'; | 4 | import 'package:flutter/services.dart'; |
| 5 | import 'package:flutter/widgets.dart'; | 5 | import 'package:flutter/widgets.dart'; |
| 6 | import 'package:mobile_scanner/src/enums/barcode_format.dart'; | 6 | import 'package:mobile_scanner/src/enums/barcode_format.dart'; |
| @@ -55,7 +55,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -55,7 +55,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 55 | final List<Map<Object?, Object?>> barcodes = | 55 | final List<Map<Object?, Object?>> barcodes = |
| 56 | data.cast<Map<Object?, Object?>>(); | 56 | data.cast<Map<Object?, Object?>>(); |
| 57 | 57 | ||
| 58 | - if (Platform.isMacOS) { | 58 | + if (defaultTargetPlatform == TargetPlatform.macOS) { |
| 59 | return BarcodeCapture( | 59 | return BarcodeCapture( |
| 60 | raw: event, | 60 | raw: event, |
| 61 | barcodes: barcodes | 61 | barcodes: barcodes |
| @@ -71,7 +71,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -71,7 +71,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 71 | ); | 71 | ); |
| 72 | } | 72 | } |
| 73 | 73 | ||
| 74 | - if (Platform.isAndroid || Platform.isIOS) { | 74 | + if (defaultTargetPlatform == TargetPlatform.android || |
| 75 | + defaultTargetPlatform == TargetPlatform.iOS) { | ||
| 75 | final double? width = event['width'] as double?; | 76 | final double? width = event['width'] as double?; |
| 76 | final double? height = event['height'] as double?; | 77 | final double? height = event['height'] as double?; |
| 77 | 78 | ||
| @@ -95,12 +96,29 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -95,12 +96,29 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 95 | /// | 96 | /// |
| 96 | /// Throws a [MobileScannerException] if the permission is not granted. | 97 | /// Throws a [MobileScannerException] if the permission is not granted. |
| 97 | Future<void> _requestCameraPermission() async { | 98 | Future<void> _requestCameraPermission() async { |
| 98 | - final MobileScannerAuthorizationState authorizationState; | ||
| 99 | - | ||
| 100 | try { | 99 | try { |
| 101 | - authorizationState = MobileScannerAuthorizationState.fromRawValue( | 100 | + final MobileScannerAuthorizationState authorizationState = |
| 101 | + MobileScannerAuthorizationState.fromRawValue( | ||
| 102 | await methodChannel.invokeMethod<int>('state') ?? 0, | 102 | await methodChannel.invokeMethod<int>('state') ?? 0, |
| 103 | ); | 103 | ); |
| 104 | + | ||
| 105 | + switch (authorizationState) { | ||
| 106 | + // Authorization was already granted, no need to request it again. | ||
| 107 | + case MobileScannerAuthorizationState.authorized: | ||
| 108 | + return; | ||
| 109 | + // Android does not have an undetermined authorization state. | ||
| 110 | + // So if the permission was denied, request it again. | ||
| 111 | + case MobileScannerAuthorizationState.denied: | ||
| 112 | + case MobileScannerAuthorizationState.undetermined: | ||
| 113 | + final bool permissionGranted = | ||
| 114 | + await methodChannel.invokeMethod<bool>('request') ?? false; | ||
| 115 | + | ||
| 116 | + if (!permissionGranted) { | ||
| 117 | + throw const MobileScannerException( | ||
| 118 | + errorCode: MobileScannerErrorCode.permissionDenied, | ||
| 119 | + ); | ||
| 120 | + } | ||
| 121 | + } | ||
| 104 | } on PlatformException catch (error) { | 122 | } on PlatformException catch (error) { |
| 105 | // If the permission state is invalid, that is an error. | 123 | // If the permission state is invalid, that is an error. |
| 106 | throw MobileScannerException( | 124 | throw MobileScannerException( |
| @@ -112,37 +130,6 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -112,37 +130,6 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 112 | ), | 130 | ), |
| 113 | ); | 131 | ); |
| 114 | } | 132 | } |
| 115 | - | ||
| 116 | - switch (authorizationState) { | ||
| 117 | - case MobileScannerAuthorizationState.authorized: | ||
| 118 | - return; // Already authorized. | ||
| 119 | - // Android does not have an undetermined authorization state. | ||
| 120 | - // So if the permission was denied, request it again. | ||
| 121 | - case MobileScannerAuthorizationState.denied: | ||
| 122 | - case MobileScannerAuthorizationState.undetermined: | ||
| 123 | - try { | ||
| 124 | - final bool granted = | ||
| 125 | - await methodChannel.invokeMethod<bool>('request') ?? false; | ||
| 126 | - | ||
| 127 | - if (granted) { | ||
| 128 | - return; // Authorization was granted. | ||
| 129 | - } | ||
| 130 | - | ||
| 131 | - throw const MobileScannerException( | ||
| 132 | - errorCode: MobileScannerErrorCode.permissionDenied, | ||
| 133 | - ); | ||
| 134 | - } on PlatformException catch (error) { | ||
| 135 | - // If the permission state is invalid, that is an error. | ||
| 136 | - throw MobileScannerException( | ||
| 137 | - errorCode: MobileScannerErrorCode.genericError, | ||
| 138 | - errorDetails: MobileScannerErrorDetails( | ||
| 139 | - code: error.code, | ||
| 140 | - details: error.details as Object?, | ||
| 141 | - message: error.message, | ||
| 142 | - ), | ||
| 143 | - ); | ||
| 144 | - } | ||
| 145 | - } | ||
| 146 | } | 133 | } |
| 147 | 134 | ||
| 148 | @override | 135 | @override |
| @@ -192,15 +179,6 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -192,15 +179,6 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 192 | } | 179 | } |
| 193 | 180 | ||
| 194 | @override | 181 | @override |
| 195 | - Future<void> setTorchState(TorchState torchState) async { | ||
| 196 | - if (torchState == TorchState.unavailable) { | ||
| 197 | - return; | ||
| 198 | - } | ||
| 199 | - | ||
| 200 | - await methodChannel.invokeMethod<void>('torch', torchState.rawValue); | ||
| 201 | - } | ||
| 202 | - | ||
| 203 | - @override | ||
| 204 | Future<void> setZoomScale(double zoomScale) async { | 182 | Future<void> setZoomScale(double zoomScale) async { |
| 205 | await methodChannel.invokeMethod<void>('setScale', zoomScale); | 183 | await methodChannel.invokeMethod<void>('setScale', zoomScale); |
| 206 | } | 184 | } |
| @@ -260,7 +238,9 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -260,7 +238,9 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 260 | _textureId = textureId; | 238 | _textureId = textureId; |
| 261 | 239 | ||
| 262 | final int? numberOfCameras = startResult['numberOfCameras'] as int?; | 240 | final int? numberOfCameras = startResult['numberOfCameras'] as int?; |
| 263 | - final bool hasTorch = startResult['torchable'] as bool? ?? false; | 241 | + final TorchState currentTorchState = TorchState.fromRawValue( |
| 242 | + startResult['currentTorchState'] as int? ?? -1, | ||
| 243 | + ); | ||
| 264 | 244 | ||
| 265 | final Map<Object?, Object?>? sizeInfo = | 245 | final Map<Object?, Object?>? sizeInfo = |
| 266 | startResult['size'] as Map<Object?, Object?>?; | 246 | startResult['size'] as Map<Object?, Object?>?; |
| @@ -278,7 +258,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -278,7 +258,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 278 | _pausing = false; | 258 | _pausing = false; |
| 279 | 259 | ||
| 280 | return MobileScannerViewAttributes( | 260 | return MobileScannerViewAttributes( |
| 281 | - hasTorch: hasTorch, | 261 | + currentTorchMode: currentTorchState, |
| 282 | numberOfCameras: numberOfCameras, | 262 | numberOfCameras: numberOfCameras, |
| 283 | size: size, | 263 | size: size, |
| 284 | ); | 264 | ); |
| @@ -309,6 +289,11 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -309,6 +289,11 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 309 | } | 289 | } |
| 310 | 290 | ||
| 311 | @override | 291 | @override |
| 292 | + Future<void> toggleTorch() async { | ||
| 293 | + await methodChannel.invokeMethod<void>('toggleTorch'); | ||
| 294 | + } | ||
| 295 | + | ||
| 296 | + @override | ||
| 312 | Future<void> updateScanWindow(Rect? window) async { | 297 | Future<void> updateScanWindow(Rect? window) async { |
| 313 | if (_textureId == null) { | 298 | if (_textureId == null) { |
| 314 | return; | 299 | return; |
| @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; | @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; | ||
| 4 | import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; | 4 | import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; |
| 5 | import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; | 5 | import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; |
| 6 | import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart'; | 6 | import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart'; |
| 7 | +import 'package:mobile_scanner/src/objects/barcode_capture.dart'; | ||
| 7 | import 'package:mobile_scanner/src/objects/mobile_scanner_state.dart'; | 8 | import 'package:mobile_scanner/src/objects/mobile_scanner_state.dart'; |
| 8 | import 'package:mobile_scanner/src/scan_window_calculation.dart'; | 9 | import 'package:mobile_scanner/src/scan_window_calculation.dart'; |
| 9 | 10 | ||
| @@ -18,7 +19,8 @@ typedef MobileScannerErrorBuilder = Widget Function( | @@ -18,7 +19,8 @@ typedef MobileScannerErrorBuilder = Widget Function( | ||
| 18 | class MobileScanner extends StatefulWidget { | 19 | class MobileScanner extends StatefulWidget { |
| 19 | /// Create a new [MobileScanner] using the provided [controller]. | 20 | /// Create a new [MobileScanner] using the provided [controller]. |
| 20 | const MobileScanner({ | 21 | const MobileScanner({ |
| 21 | - required this.controller, | 22 | + this.controller, |
| 23 | + this.onDetect, | ||
| 22 | this.fit = BoxFit.cover, | 24 | this.fit = BoxFit.cover, |
| 23 | this.errorBuilder, | 25 | this.errorBuilder, |
| 24 | this.overlayBuilder, | 26 | this.overlayBuilder, |
| @@ -29,7 +31,11 @@ class MobileScanner extends StatefulWidget { | @@ -29,7 +31,11 @@ class MobileScanner extends StatefulWidget { | ||
| 29 | }); | 31 | }); |
| 30 | 32 | ||
| 31 | /// The controller for the camera preview. | 33 | /// The controller for the camera preview. |
| 32 | - final MobileScannerController controller; | 34 | + final MobileScannerController? controller; |
| 35 | + | ||
| 36 | + /// 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 | + final void Function(BarcodeCapture barcodes)? onDetect; | ||
| 33 | 39 | ||
| 34 | /// The error builder for the camera preview. | 40 | /// The error builder for the camera preview. |
| 35 | /// | 41 | /// |
| @@ -112,7 +118,10 @@ class MobileScanner extends StatefulWidget { | @@ -112,7 +118,10 @@ class MobileScanner extends StatefulWidget { | ||
| 112 | State<MobileScanner> createState() => _MobileScannerState(); | 118 | State<MobileScanner> createState() => _MobileScannerState(); |
| 113 | } | 119 | } |
| 114 | 120 | ||
| 115 | -class _MobileScannerState extends State<MobileScanner> { | 121 | +class _MobileScannerState extends State<MobileScanner> |
| 122 | + with WidgetsBindingObserver { | ||
| 123 | + late final controller = widget.controller ?? MobileScannerController(); | ||
| 124 | + | ||
| 116 | /// The current scan window. | 125 | /// The current scan window. |
| 117 | Rect? scanWindow; | 126 | Rect? scanWindow; |
| 118 | 127 | ||
| @@ -139,7 +148,7 @@ class _MobileScannerState extends State<MobileScanner> { | @@ -139,7 +148,7 @@ class _MobileScannerState extends State<MobileScanner> { | ||
| 139 | if (scanWindow == null) { | 148 | if (scanWindow == null) { |
| 140 | scanWindow = newScanWindow; | 149 | scanWindow = newScanWindow; |
| 141 | 150 | ||
| 142 | - unawaited(widget.controller.updateScanWindow(scanWindow)); | 151 | + unawaited(controller.updateScanWindow(scanWindow)); |
| 143 | 152 | ||
| 144 | return; | 153 | return; |
| 145 | } | 154 | } |
| @@ -154,7 +163,7 @@ class _MobileScannerState extends State<MobileScanner> { | @@ -154,7 +163,7 @@ class _MobileScannerState extends State<MobileScanner> { | ||
| 154 | if (widget.scanWindowUpdateThreshold == 0.0) { | 163 | if (widget.scanWindowUpdateThreshold == 0.0) { |
| 155 | scanWindow = newScanWindow; | 164 | scanWindow = newScanWindow; |
| 156 | 165 | ||
| 157 | - unawaited(widget.controller.updateScanWindow(scanWindow)); | 166 | + unawaited(controller.updateScanWindow(scanWindow)); |
| 158 | 167 | ||
| 159 | return; | 168 | return; |
| 160 | } | 169 | } |
| @@ -167,14 +176,14 @@ class _MobileScannerState extends State<MobileScanner> { | @@ -167,14 +176,14 @@ class _MobileScannerState extends State<MobileScanner> { | ||
| 167 | dy >= widget.scanWindowUpdateThreshold) { | 176 | dy >= widget.scanWindowUpdateThreshold) { |
| 168 | scanWindow = newScanWindow; | 177 | scanWindow = newScanWindow; |
| 169 | 178 | ||
| 170 | - unawaited(widget.controller.updateScanWindow(scanWindow)); | 179 | + unawaited(controller.updateScanWindow(scanWindow)); |
| 171 | } | 180 | } |
| 172 | } | 181 | } |
| 173 | 182 | ||
| 174 | @override | 183 | @override |
| 175 | Widget build(BuildContext context) { | 184 | Widget build(BuildContext context) { |
| 176 | return ValueListenableBuilder<MobileScannerState>( | 185 | return ValueListenableBuilder<MobileScannerState>( |
| 177 | - valueListenable: widget.controller, | 186 | + valueListenable: controller, |
| 178 | builder: (BuildContext context, MobileScannerState value, Widget? child) { | 187 | builder: (BuildContext context, MobileScannerState value, Widget? child) { |
| 179 | if (!value.isInitialized) { | 188 | if (!value.isInitialized) { |
| 180 | const Widget defaultPlaceholder = ColoredBox(color: Colors.black); | 189 | const Widget defaultPlaceholder = ColoredBox(color: Colors.black); |
| @@ -234,10 +243,62 @@ class _MobileScannerState extends State<MobileScanner> { | @@ -234,10 +243,62 @@ class _MobileScannerState extends State<MobileScanner> { | ||
| 234 | ); | 243 | ); |
| 235 | } | 244 | } |
| 236 | 245 | ||
| 246 | + StreamSubscription? _subscription; | ||
| 247 | + | ||
| 248 | + @override | ||
| 249 | + void initState() { | ||
| 250 | + if (widget.onDetect != null) { | ||
| 251 | + WidgetsBinding.instance.addObserver(this); | ||
| 252 | + _subscription = controller.barcodes.listen(widget.onDetect); | ||
| 253 | + } | ||
| 254 | + if (controller.autoStart) { | ||
| 255 | + controller.start(); | ||
| 256 | + } | ||
| 257 | + super.initState(); | ||
| 258 | + } | ||
| 259 | + | ||
| 237 | @override | 260 | @override |
| 238 | void dispose() { | 261 | void dispose() { |
| 239 | super.dispose(); | 262 | super.dispose(); |
| 263 | + | ||
| 264 | + if (_subscription != null) { | ||
| 265 | + _subscription!.cancel(); | ||
| 266 | + _subscription = null; | ||
| 267 | + } | ||
| 268 | + | ||
| 269 | + if (controller.autoStart) { | ||
| 270 | + controller.stop(); | ||
| 271 | + } | ||
| 240 | // When this widget is unmounted, reset the scan window. | 272 | // When this widget is unmounted, reset the scan window. |
| 241 | - unawaited(widget.controller.updateScanWindow(null)); | 273 | + unawaited(controller.updateScanWindow(null)); |
| 274 | + | ||
| 275 | + // Dispose default controller if not provided by user | ||
| 276 | + if (widget.controller == null) { | ||
| 277 | + controller.dispose(); | ||
| 278 | + WidgetsBinding.instance.removeObserver(this); | ||
| 279 | + } | ||
| 280 | + } | ||
| 281 | + | ||
| 282 | + @override | ||
| 283 | + void didChangeAppLifecycleState(AppLifecycleState state) { | ||
| 284 | + if (widget.controller != null) return; | ||
| 285 | + if (!controller.value.isInitialized) { | ||
| 286 | + return; | ||
| 287 | + } | ||
| 288 | + | ||
| 289 | + switch (state) { | ||
| 290 | + case AppLifecycleState.detached: | ||
| 291 | + case AppLifecycleState.hidden: | ||
| 292 | + case AppLifecycleState.paused: | ||
| 293 | + return; | ||
| 294 | + case AppLifecycleState.resumed: | ||
| 295 | + _subscription = controller.barcodes.listen(widget.onDetect); | ||
| 296 | + | ||
| 297 | + unawaited(controller.start()); | ||
| 298 | + case AppLifecycleState.inactive: | ||
| 299 | + unawaited(_subscription?.cancel()); | ||
| 300 | + _subscription = null; | ||
| 301 | + unawaited(controller.stop()); | ||
| 302 | + } | ||
| 242 | } | 303 | } |
| 243 | } | 304 | } |
| @@ -17,6 +17,7 @@ import 'package:mobile_scanner/src/objects/start_options.dart'; | @@ -17,6 +17,7 @@ import 'package:mobile_scanner/src/objects/start_options.dart'; | ||
| 17 | class MobileScannerController extends ValueNotifier<MobileScannerState> { | 17 | class MobileScannerController extends ValueNotifier<MobileScannerState> { |
| 18 | /// Construct a new [MobileScannerController] instance. | 18 | /// Construct a new [MobileScannerController] instance. |
| 19 | MobileScannerController({ | 19 | MobileScannerController({ |
| 20 | + this.autoStart = true, | ||
| 20 | this.cameraResolution, | 21 | this.cameraResolution, |
| 21 | this.detectionSpeed = DetectionSpeed.normal, | 22 | this.detectionSpeed = DetectionSpeed.normal, |
| 22 | int detectionTimeoutMs = 250, | 23 | int detectionTimeoutMs = 250, |
| @@ -47,6 +48,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -47,6 +48,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 47 | /// Currently only supported on Android. | 48 | /// Currently only supported on Android. |
| 48 | final Size? cameraResolution; | 49 | final Size? cameraResolution; |
| 49 | 50 | ||
| 51 | + /// Automatically start the scanner on initialization. | ||
| 52 | + final bool autoStart; | ||
| 53 | + | ||
| 50 | /// The detection speed for the scanner. | 54 | /// The detection speed for the scanner. |
| 51 | /// | 55 | /// |
| 52 | /// Defaults to [DetectionSpeed.normal]. | 56 | /// Defaults to [DetectionSpeed.normal]. |
| @@ -177,11 +181,16 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -177,11 +181,16 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 177 | 181 | ||
| 178 | _disposeListeners(); | 182 | _disposeListeners(); |
| 179 | 183 | ||
| 184 | + final TorchState oldTorchState = value.torchState; | ||
| 185 | + | ||
| 180 | // After the camera stopped, set the torch state to off, | 186 | // After the camera stopped, set the torch state to off, |
| 181 | // as the torch state callback is never called when the camera is stopped. | 187 | // as the torch state callback is never called when the camera is stopped. |
| 188 | + // If the device does not have a torch, do not report "off". | ||
| 182 | value = value.copyWith( | 189 | value = value.copyWith( |
| 183 | isRunning: false, | 190 | isRunning: false, |
| 184 | - torchState: TorchState.off, | 191 | + torchState: oldTorchState == TorchState.unavailable |
| 192 | + ? TorchState.unavailable | ||
| 193 | + : TorchState.off, | ||
| 185 | ); | 194 | ); |
| 186 | } | 195 | } |
| 187 | 196 | ||
| @@ -242,12 +251,14 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -242,12 +251,14 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 242 | } | 251 | } |
| 243 | 252 | ||
| 244 | /// Start scanning for barcodes. | 253 | /// Start scanning for barcodes. |
| 245 | - /// Upon calling this method, the necessary camera permission will be requested. | ||
| 246 | /// | 254 | /// |
| 247 | /// The [cameraDirection] can be used to specify the camera direction. | 255 | /// The [cameraDirection] can be used to specify the camera direction. |
| 248 | /// If this is null, this defaults to the [facing] value. | 256 | /// If this is null, this defaults to the [facing] value. |
| 249 | /// | 257 | /// |
| 250 | /// Does nothing if the camera is already running. | 258 | /// Does nothing if the camera is already running. |
| 259 | + /// Upon calling this method, the necessary camera permission will be requested. | ||
| 260 | + /// | ||
| 261 | + /// If the permission is denied on iOS, MacOS or Web, there is no way to request it again. | ||
| 251 | Future<void> start({CameraFacing? cameraDirection}) async { | 262 | Future<void> start({CameraFacing? cameraDirection}) async { |
| 252 | if (_isDisposed) { | 263 | if (_isDisposed) { |
| 253 | throw const MobileScannerException( | 264 | throw const MobileScannerException( |
| @@ -259,6 +270,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -259,6 +270,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 259 | ); | 270 | ); |
| 260 | } | 271 | } |
| 261 | 272 | ||
| 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 | + | ||
| 262 | // Do nothing if the camera is already running. | 280 | // Do nothing if the camera is already running. |
| 263 | if (value.isRunning) { | 281 | if (value.isRunning) { |
| 264 | return; | 282 | return; |
| @@ -284,16 +302,18 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -284,16 +302,18 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 284 | options, | 302 | options, |
| 285 | ); | 303 | ); |
| 286 | 304 | ||
| 287 | - value = value.copyWith( | ||
| 288 | - availableCameras: viewAttributes.numberOfCameras, | ||
| 289 | - cameraDirection: effectiveDirection, | ||
| 290 | - isInitialized: true, | ||
| 291 | - isRunning: true, | ||
| 292 | - size: viewAttributes.size, | ||
| 293 | - // If the device has a flashlight, let the platform update the torch state. | ||
| 294 | - // If it does not have one, provide the unavailable state directly. | ||
| 295 | - torchState: viewAttributes.hasTorch ? null : TorchState.unavailable, | ||
| 296 | - ); | 305 | + if (!_isDisposed) { |
| 306 | + value = value.copyWith( | ||
| 307 | + availableCameras: viewAttributes.numberOfCameras, | ||
| 308 | + cameraDirection: effectiveDirection, | ||
| 309 | + isInitialized: true, | ||
| 310 | + isRunning: true, | ||
| 311 | + size: viewAttributes.size, | ||
| 312 | + // Provide the current torch state. | ||
| 313 | + // Updates are provided by the `torchStateStream`. | ||
| 314 | + torchState: viewAttributes.currentTorchMode, | ||
| 315 | + ); | ||
| 316 | + } | ||
| 297 | } on MobileScannerException catch (error) { | 317 | } on MobileScannerException catch (error) { |
| 298 | // The initialization finished with an error. | 318 | // The initialization finished with an error. |
| 299 | // To avoid stale values, reset the output size, | 319 | // To avoid stale values, reset the output size, |
| @@ -365,6 +385,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -365,6 +385,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 365 | /// | 385 | /// |
| 366 | /// Does nothing if the device has no torch, | 386 | /// Does nothing if the device has no torch, |
| 367 | /// or if the camera is not running. | 387 | /// or if the camera is not running. |
| 388 | + /// | ||
| 389 | + /// If the current torch state is [TorchState.auto], | ||
| 390 | + /// the torch is turned on or off depending on its actual current state. | ||
| 368 | Future<void> toggleTorch() async { | 391 | Future<void> toggleTorch() async { |
| 369 | _throwIfNotInitialized(); | 392 | _throwIfNotInitialized(); |
| 370 | 393 | ||
| @@ -378,13 +401,10 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -378,13 +401,10 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 378 | return; | 401 | return; |
| 379 | } | 402 | } |
| 380 | 403 | ||
| 381 | - final TorchState newState = | ||
| 382 | - torchState == TorchState.off ? TorchState.on : TorchState.off; | ||
| 383 | - | ||
| 384 | - // Update the torch state to the new state. | 404 | + // Request the torch state to be switched to the opposite state. |
| 385 | // When the platform has updated the torch state, | 405 | // When the platform has updated the torch state, |
| 386 | // it will send an update through the torch state event stream. | 406 | // it will send an update through the torch state event stream. |
| 387 | - await MobileScannerPlatform.instance.setTorchState(newState); | 407 | + await MobileScannerPlatform.instance.toggleTorch(); |
| 388 | } | 408 | } |
| 389 | 409 | ||
| 390 | /// Update the scan window with the given [window] rectangle. | 410 | /// Update the scan window with the given [window] rectangle. |
| @@ -67,11 +67,6 @@ abstract class MobileScannerPlatform extends PlatformInterface { | @@ -67,11 +67,6 @@ abstract class MobileScannerPlatform extends PlatformInterface { | ||
| 67 | /// This is only supported on the web. | 67 | /// This is only supported on the web. |
| 68 | void setBarcodeLibraryScriptUrl(String scriptUrl) {} | 68 | void setBarcodeLibraryScriptUrl(String scriptUrl) {} |
| 69 | 69 | ||
| 70 | - /// Set the torch state of the active camera. | ||
| 71 | - Future<void> setTorchState(TorchState torchState) { | ||
| 72 | - throw UnimplementedError('setTorchState() has not been implemented.'); | ||
| 73 | - } | ||
| 74 | - | ||
| 75 | /// Set the zoom scale of the camera. | 70 | /// Set the zoom scale of the camera. |
| 76 | /// | 71 | /// |
| 77 | /// The [zoomScale] must be between `0.0` and `1.0` (both inclusive). | 72 | /// The [zoomScale] must be between `0.0` and `1.0` (both inclusive). |
| @@ -100,6 +95,10 @@ abstract class MobileScannerPlatform extends PlatformInterface { | @@ -100,6 +95,10 @@ abstract class MobileScannerPlatform extends PlatformInterface { | ||
| 100 | throw UnimplementedError('pause() has not been implemented.'); | 95 | throw UnimplementedError('pause() has not been implemented.'); |
| 101 | } | 96 | } |
| 102 | 97 | ||
| 98 | + /// Toggle the torch on the active camera on or off. | ||
| 99 | + Future<void> toggleTorch() { | ||
| 100 | + throw UnimplementedError('toggleTorch() has not been implemented.'); | ||
| 101 | + } | ||
| 103 | 102 | ||
| 104 | /// Update the scan window to the given [window] rectangle. | 103 | /// Update the scan window to the given [window] rectangle. |
| 105 | /// | 104 | /// |
| 1 | import 'dart:ui'; | 1 | import 'dart:ui'; |
| 2 | 2 | ||
| 3 | +import 'package:mobile_scanner/src/enums/torch_state.dart'; | ||
| 4 | + | ||
| 3 | /// This class defines the attributes for the mobile scanner view. | 5 | /// This class defines the attributes for the mobile scanner view. |
| 4 | class MobileScannerViewAttributes { | 6 | class MobileScannerViewAttributes { |
| 5 | const MobileScannerViewAttributes({ | 7 | const MobileScannerViewAttributes({ |
| 6 | - required this.hasTorch, | 8 | + required this.currentTorchMode, |
| 7 | this.numberOfCameras, | 9 | this.numberOfCameras, |
| 8 | required this.size, | 10 | required this.size, |
| 9 | }); | 11 | }); |
| 10 | 12 | ||
| 11 | - /// Whether the current active camera has a torch. | ||
| 12 | - final bool hasTorch; | 13 | + /// The current torch state of the active camera. |
| 14 | + final TorchState currentTorchMode; | ||
| 13 | 15 | ||
| 14 | /// The number of available cameras. | 16 | /// The number of available cameras. |
| 15 | final int? numberOfCameras; | 17 | final int? numberOfCameras; |
| @@ -38,7 +38,7 @@ class MobileScannerState { | @@ -38,7 +38,7 @@ class MobileScannerState { | ||
| 38 | /// The facing direction of the camera. | 38 | /// The facing direction of the camera. |
| 39 | final CameraFacing cameraDirection; | 39 | final CameraFacing cameraDirection; |
| 40 | 40 | ||
| 41 | - /// The error that occurred while setting up or using the canera. | 41 | + /// The error that occurred while setting up or using the camera. |
| 42 | final MobileScannerException? error; | 42 | final MobileScannerException? error; |
| 43 | 43 | ||
| 44 | /// Whether the mobile scanner has initialized successfully. | 44 | /// Whether the mobile scanner has initialized successfully. |
| @@ -62,20 +62,19 @@ abstract class BarcodeReader { | @@ -62,20 +62,19 @@ abstract class BarcodeReader { | ||
| 62 | 62 | ||
| 63 | final Completer<void> completer = Completer(); | 63 | final Completer<void> completer = Completer(); |
| 64 | 64 | ||
| 65 | - final HTMLScriptElement script = | ||
| 66 | - (document.createElement('script') as HTMLScriptElement) | ||
| 67 | - ..id = scriptId | ||
| 68 | - ..async = true | ||
| 69 | - ..defer = false | ||
| 70 | - ..type = 'application/javascript' | ||
| 71 | - ..lang = 'javascript' | ||
| 72 | - ..crossOrigin = 'anonymous' | ||
| 73 | - ..src = alternateScriptUrl ?? scriptUrl | ||
| 74 | - ..onload = (JSAny _) { | ||
| 75 | - if (!completer.isCompleted) { | ||
| 76 | - completer.complete(); | ||
| 77 | - } | ||
| 78 | - }.toJS; | 65 | + final HTMLScriptElement script = HTMLScriptElement() |
| 66 | + ..id = scriptId | ||
| 67 | + ..async = true | ||
| 68 | + ..defer = false | ||
| 69 | + ..type = 'application/javascript' | ||
| 70 | + ..lang = 'javascript' | ||
| 71 | + ..crossOrigin = 'anonymous' | ||
| 72 | + ..src = alternateScriptUrl ?? scriptUrl | ||
| 73 | + ..onload = (JSAny _) { | ||
| 74 | + if (!completer.isCompleted) { | ||
| 75 | + completer.complete(); | ||
| 76 | + } | ||
| 77 | + }.toJS; | ||
| 79 | 78 | ||
| 80 | script.onerror = (JSAny _) { | 79 | script.onerror = (JSAny _) { |
| 81 | if (!completer.isCompleted) { | 80 | if (!completer.isCompleted) { |
| @@ -9,12 +9,10 @@ import 'dart:js_interop'; | @@ -9,12 +9,10 @@ import 'dart:js_interop'; | ||
| 9 | /// | 9 | /// |
| 10 | /// Object literals can be made using [jsify]. | 10 | /// Object literals can be made using [jsify]. |
| 11 | @JS('Map') | 11 | @JS('Map') |
| 12 | -@staticInterop | ||
| 13 | -class JSMap<K extends JSAny, V extends JSAny> { | 12 | +extension type JSMap<K extends JSAny, V extends JSAny>._(JSObject _) |
| 13 | + implements JSObject { | ||
| 14 | external factory JSMap(); | 14 | external factory JSMap(); |
| 15 | -} | ||
| 16 | 15 | ||
| 17 | -extension JSMapExtension<K extends JSAny, V extends JSAny> on JSMap<K, V> { | ||
| 18 | external V? get(K key); | 16 | external V? get(K key); |
| 19 | external JSVoid set(K key, V? value); | 17 | external JSVoid set(K key, V? value); |
| 20 | } | 18 | } |
| 1 | import 'dart:js_interop'; | 1 | import 'dart:js_interop'; |
| 2 | 2 | ||
| 3 | +import 'package:mobile_scanner/src/web/media_track_extension.dart'; | ||
| 3 | import 'package:web/web.dart'; | 4 | import 'package:web/web.dart'; |
| 4 | 5 | ||
| 5 | /// This class represents a delegate that manages the constraints for a [MediaStreamTrack]. | 6 | /// This class represents a delegate that manages the constraints for a [MediaStreamTrack]. |
| @@ -9,25 +10,36 @@ final class MediaTrackConstraintsDelegate { | @@ -9,25 +10,36 @@ final class MediaTrackConstraintsDelegate { | ||
| 9 | 10 | ||
| 10 | /// Get the settings for the given [mediaStream]. | 11 | /// Get the settings for the given [mediaStream]. |
| 11 | MediaTrackSettings? getSettings(MediaStream? mediaStream) { | 12 | MediaTrackSettings? getSettings(MediaStream? mediaStream) { |
| 12 | - final List<JSAny?>? tracks = mediaStream?.getVideoTracks().toDart; | 13 | + final List<MediaStreamTrack>? tracks = mediaStream?.getVideoTracks().toDart; |
| 13 | 14 | ||
| 14 | if (tracks == null || tracks.isEmpty) { | 15 | if (tracks == null || tracks.isEmpty) { |
| 15 | return null; | 16 | return null; |
| 16 | } | 17 | } |
| 17 | 18 | ||
| 18 | - final MediaStreamTrack? track = tracks.first as MediaStreamTrack?; | 19 | + final MediaStreamTrack track = tracks.first; |
| 19 | 20 | ||
| 20 | - if (track == null) { | ||
| 21 | - return null; | 21 | + final MediaTrackCapabilities capabilities; |
| 22 | + | ||
| 23 | + if (track.getCapabilitiesNullable != null) { | ||
| 24 | + capabilities = track.getCapabilities(); | ||
| 25 | + } else { | ||
| 26 | + capabilities = MediaTrackCapabilities(); | ||
| 22 | } | 27 | } |
| 23 | 28 | ||
| 24 | final MediaTrackSettings settings = track.getSettings(); | 29 | final MediaTrackSettings settings = track.getSettings(); |
| 30 | + final JSArray<JSString>? facingModes = capabilities.facingModeNullable; | ||
| 31 | + | ||
| 32 | + if (facingModes == null || facingModes.toDart.isEmpty) { | ||
| 33 | + return MediaTrackSettings( | ||
| 34 | + width: settings.width, | ||
| 35 | + height: settings.height, | ||
| 36 | + ); | ||
| 37 | + } | ||
| 25 | 38 | ||
| 26 | return MediaTrackSettings( | 39 | return MediaTrackSettings( |
| 27 | width: settings.width, | 40 | width: settings.width, |
| 28 | height: settings.height, | 41 | height: settings.height, |
| 29 | facingMode: settings.facingMode, | 42 | facingMode: settings.facingMode, |
| 30 | - aspectRatio: settings.aspectRatio, | ||
| 31 | ); | 43 | ); |
| 32 | } | 44 | } |
| 33 | } | 45 | } |
lib/src/web/media_track_extension.dart
0 → 100644
| 1 | +import 'dart:js_interop'; | ||
| 2 | +import 'package:web/web.dart'; | ||
| 3 | + | ||
| 4 | +/// This extension provides nullable properties for [MediaStreamTrack], | ||
| 5 | +/// for cases where the properties are not supported by all browsers. | ||
| 6 | +extension NullableMediaStreamTrackCapabilities on MediaStreamTrack { | ||
| 7 | + /// The `getCapabilities` function is not supported on Firefox. | ||
| 8 | + @JS('getCapabilities') | ||
| 9 | + external JSFunction? get getCapabilitiesNullable; | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +/// This extension provides nullable properties for [MediaTrackCapabilities], | ||
| 13 | +/// for cases where the properties are not supported by all browsers. | ||
| 14 | +extension NullableMediaTrackCapabilities on MediaTrackCapabilities { | ||
| 15 | + /// The `facingMode` property is not supported on Safari. | ||
| 16 | + @JS('facingMode') | ||
| 17 | + external JSArray<JSString>? get facingModeNullable; | ||
| 18 | +} |
| @@ -15,6 +15,7 @@ import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; | @@ -15,6 +15,7 @@ import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; | ||
| 15 | import 'package:mobile_scanner/src/objects/barcode_capture.dart'; | 15 | import 'package:mobile_scanner/src/objects/barcode_capture.dart'; |
| 16 | import 'package:mobile_scanner/src/objects/start_options.dart'; | 16 | import 'package:mobile_scanner/src/objects/start_options.dart'; |
| 17 | import 'package:mobile_scanner/src/web/barcode_reader.dart'; | 17 | import 'package:mobile_scanner/src/web/barcode_reader.dart'; |
| 18 | +import 'package:mobile_scanner/src/web/media_track_extension.dart'; | ||
| 18 | import 'package:mobile_scanner/src/web/zxing/zxing_barcode_reader.dart'; | 19 | import 'package:mobile_scanner/src/web/zxing/zxing_barcode_reader.dart'; |
| 19 | import 'package:web/web.dart'; | 20 | import 'package:web/web.dart'; |
| 20 | 21 | ||
| @@ -27,7 +28,7 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -27,7 +28,7 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 27 | String? _alternateScriptUrl; | 28 | String? _alternateScriptUrl; |
| 28 | 29 | ||
| 29 | /// The internal barcode reader. | 30 | /// The internal barcode reader. |
| 30 | - final BarcodeReader _barcodeReader = ZXingBarcodeReader(); | 31 | + BarcodeReader? _barcodeReader; |
| 31 | 32 | ||
| 32 | /// The stream controller for the barcode stream. | 33 | /// The stream controller for the barcode stream. |
| 33 | final StreamController<BarcodeCapture> _barcodesController = | 34 | final StreamController<BarcodeCapture> _barcodesController = |
| @@ -37,21 +38,13 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -37,21 +38,13 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 37 | StreamSubscription<Object?>? _barcodesSubscription; | 38 | StreamSubscription<Object?>? _barcodesSubscription; |
| 38 | 39 | ||
| 39 | /// The container div element for the camera view. | 40 | /// The container div element for the camera view. |
| 40 | - /// | ||
| 41 | - /// This container element is used by the barcode reader. | ||
| 42 | - HTMLDivElement? _divElement; | 41 | + late HTMLDivElement _divElement; |
| 43 | 42 | ||
| 44 | - /// This [Completer] is used to prevent additional calls to the [start] method. | ||
| 45 | - /// | ||
| 46 | - /// To handle lifecycle changes properly, | ||
| 47 | - /// the scanner is stopped when the application is inactive, | ||
| 48 | - /// and restarted when the application gains focus. | 43 | + /// The flag that keeps track of whether a permission request is in progress. |
| 49 | /// | 44 | /// |
| 50 | - /// However, when the camera permission is requested, | ||
| 51 | - /// the application is put in the inactive state due to the permission popup gaining focus. | ||
| 52 | - /// Thus, as long as the permission status is not known, | ||
| 53 | - /// any calls to the [start] method are ignored. | ||
| 54 | - Completer<void>? _cameraPermissionCompleter; | 45 | + /// On the web, a permission request triggers a dialog, that in turn triggers a lifecycle change. |
| 46 | + /// While the permission request is in progress, any attempts at (re)starting the camera should be ignored. | ||
| 47 | + bool _permissionRequestInProgress = false; | ||
| 55 | 48 | ||
| 56 | /// The stream controller for the media track settings stream. | 49 | /// The stream controller for the media track settings stream. |
| 57 | /// | 50 | /// |
| @@ -62,18 +55,19 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -62,18 +55,19 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 62 | final StreamController<MediaTrackSettings> _settingsController = | 55 | final StreamController<MediaTrackSettings> _settingsController = |
| 63 | StreamController.broadcast(); | 56 | StreamController.broadcast(); |
| 64 | 57 | ||
| 65 | - /// The view type for the platform view factory. | ||
| 66 | - static const String _viewType = 'MobileScannerWeb'; | 58 | + /// The texture ID for the camera view. |
| 59 | + int _textureId = 1; | ||
| 60 | + | ||
| 61 | + /// The video element for the camera view. | ||
| 62 | + late HTMLVideoElement _videoElement; | ||
| 63 | + | ||
| 64 | + /// Get the view type for the platform view factory. | ||
| 65 | + String _getViewType(int textureId) => 'mobile-scanner-view-$textureId'; | ||
| 67 | 66 | ||
| 68 | static void registerWith(Registrar registrar) { | 67 | static void registerWith(Registrar registrar) { |
| 69 | MobileScannerPlatform.instance = MobileScannerWeb(); | 68 | MobileScannerPlatform.instance = MobileScannerWeb(); |
| 70 | } | 69 | } |
| 71 | 70 | ||
| 72 | - bool get _hasPendingPermissionRequest { | ||
| 73 | - return _cameraPermissionCompleter != null && | ||
| 74 | - !_cameraPermissionCompleter!.isCompleted; | ||
| 75 | - } | ||
| 76 | - | ||
| 77 | @override | 71 | @override |
| 78 | Stream<BarcodeCapture?> get barcodesStream => _barcodesController.stream; | 72 | Stream<BarcodeCapture?> get barcodesStream => _barcodesController.stream; |
| 79 | 73 | ||
| @@ -85,6 +79,33 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -85,6 +79,33 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 85 | Stream<double> get zoomScaleStateStream => | 79 | Stream<double> get zoomScaleStateStream => |
| 86 | _settingsController.stream.map((_) => 1.0); | 80 | _settingsController.stream.map((_) => 1.0); |
| 87 | 81 | ||
| 82 | + /// Create the [HTMLVideoElement] along with its parent container [HTMLDivElement]. | ||
| 83 | + HTMLVideoElement _createVideoElement(int textureId) { | ||
| 84 | + final HTMLVideoElement videoElement = HTMLVideoElement(); | ||
| 85 | + | ||
| 86 | + videoElement.style | ||
| 87 | + ..height = '100%' | ||
| 88 | + ..width = '100%' | ||
| 89 | + ..objectFit = 'cover' | ||
| 90 | + ..transformOrigin = 'center' | ||
| 91 | + ..pointerEvents = 'none'; | ||
| 92 | + | ||
| 93 | + // Attach the video element to its parent container | ||
| 94 | + // and setup the PlatformView factory for this `textureId`. | ||
| 95 | + _divElement = HTMLDivElement() | ||
| 96 | + ..style.objectFit = 'cover' | ||
| 97 | + ..style.height = '100%' | ||
| 98 | + ..style.width = '100%' | ||
| 99 | + ..append(videoElement); | ||
| 100 | + | ||
| 101 | + ui_web.platformViewRegistry.registerViewFactory( | ||
| 102 | + _getViewType(textureId), | ||
| 103 | + (_) => _divElement, | ||
| 104 | + ); | ||
| 105 | + | ||
| 106 | + return videoElement; | ||
| 107 | + } | ||
| 108 | + | ||
| 88 | void _handleMediaTrackSettingsChange(MediaTrackSettings settings) { | 109 | void _handleMediaTrackSettingsChange(MediaTrackSettings settings) { |
| 89 | if (_settingsController.isClosed) { | 110 | if (_settingsController.isClosed) { |
| 90 | return; | 111 | return; |
| @@ -93,6 +114,40 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -93,6 +114,40 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 93 | _settingsController.add(settings); | 114 | _settingsController.add(settings); |
| 94 | } | 115 | } |
| 95 | 116 | ||
| 117 | + /// Flip the [videoElement] horizontally, | ||
| 118 | + /// if the [videoStream] indicates that is facing the user. | ||
| 119 | + void _maybeFlipVideoPreview( | ||
| 120 | + HTMLVideoElement videoElement, | ||
| 121 | + MediaStream videoStream, | ||
| 122 | + ) { | ||
| 123 | + final List<MediaStreamTrack> tracks = videoStream.getVideoTracks().toDart; | ||
| 124 | + | ||
| 125 | + if (tracks.isEmpty) { | ||
| 126 | + return; | ||
| 127 | + } | ||
| 128 | + | ||
| 129 | + final MediaStreamTrack videoTrack = tracks.first; | ||
| 130 | + final MediaTrackCapabilities capabilities; | ||
| 131 | + | ||
| 132 | + if (videoTrack.getCapabilitiesNullable != null) { | ||
| 133 | + capabilities = videoTrack.getCapabilities(); | ||
| 134 | + } else { | ||
| 135 | + capabilities = MediaTrackCapabilities(); | ||
| 136 | + } | ||
| 137 | + | ||
| 138 | + final JSArray<JSString>? facingModes = capabilities.facingModeNullable; | ||
| 139 | + | ||
| 140 | + // TODO: this is an empty array on MacOS Chrome, where there is no facing mode, but one, user facing camera. | ||
| 141 | + // Facing mode is not supported by this track, do nothing. | ||
| 142 | + if (facingModes == null || facingModes.toDart.isEmpty) { | ||
| 143 | + return; | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + if (videoTrack.getSettings().facingMode == 'user') { | ||
| 147 | + videoElement.style.transform = 'scaleX(-1)'; | ||
| 148 | + } | ||
| 149 | + } | ||
| 150 | + | ||
| 96 | /// Prepare a [MediaStream] for the video output. | 151 | /// Prepare a [MediaStream] for the video output. |
| 97 | /// | 152 | /// |
| 98 | /// This method requests permission to use the camera. | 153 | /// This method requests permission to use the camera. |
| @@ -102,7 +157,7 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -102,7 +157,7 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 102 | Future<MediaStream> _prepareVideoStream( | 157 | Future<MediaStream> _prepareVideoStream( |
| 103 | CameraFacing cameraDirection, | 158 | CameraFacing cameraDirection, |
| 104 | ) async { | 159 | ) async { |
| 105 | - if ((window.navigator.mediaDevices as JSAny?).isUndefinedOrNull) { | 160 | + if (window.navigator.mediaDevices.isUndefinedOrNull) { |
| 106 | throw const MobileScannerException( | 161 | throw const MobileScannerException( |
| 107 | errorCode: MobileScannerErrorCode.unsupported, | 162 | errorCode: MobileScannerErrorCode.unsupported, |
| 108 | errorDetails: MobileScannerErrorDetails( | 163 | errorDetails: MobileScannerErrorDetails( |
| @@ -117,7 +172,7 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -117,7 +172,7 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 117 | 172 | ||
| 118 | final MediaStreamConstraints constraints; | 173 | final MediaStreamConstraints constraints; |
| 119 | 174 | ||
| 120 | - if ((capabilities as JSAny).isUndefinedOrNull || !capabilities.facingMode) { | 175 | + if (capabilities.isUndefinedOrNull || !capabilities.facingMode) { |
| 121 | constraints = MediaStreamConstraints(video: true.toJS); | 176 | constraints = MediaStreamConstraints(video: true.toJS); |
| 122 | } else { | 177 | } else { |
| 123 | final String facingMode = switch (cameraDirection) { | 178 | final String facingMode = switch (cameraDirection) { |
| @@ -126,43 +181,24 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -126,43 +181,24 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 126 | }; | 181 | }; |
| 127 | 182 | ||
| 128 | constraints = MediaStreamConstraints( | 183 | constraints = MediaStreamConstraints( |
| 129 | - video: MediaTrackConstraintSet(facingMode: facingMode.toJS) as JSAny, | 184 | + video: MediaTrackConstraintSet( |
| 185 | + facingMode: facingMode.toJS, | ||
| 186 | + ), | ||
| 130 | ); | 187 | ); |
| 131 | } | 188 | } |
| 132 | 189 | ||
| 133 | try { | 190 | try { |
| 134 | - // Retrieving the video track requests the camera permission. | ||
| 135 | - // If the completer is not null, the permission was never requested before. | ||
| 136 | - _cameraPermissionCompleter ??= Completer<void>(); | 191 | + _permissionRequestInProgress = true; |
| 137 | 192 | ||
| 138 | - final MediaStream? videoStream = await window.navigator.mediaDevices | ||
| 139 | - .getUserMedia(constraints) | ||
| 140 | - .toDart as MediaStream?; | ||
| 141 | - | ||
| 142 | - // At this point the permission is granted. | ||
| 143 | - if (!_cameraPermissionCompleter!.isCompleted) { | ||
| 144 | - _cameraPermissionCompleter!.complete(); | ||
| 145 | - } | 193 | + // Retrieving the media devices requests the camera permission. |
| 194 | + final MediaStream videoStream = | ||
| 195 | + await window.navigator.mediaDevices.getUserMedia(constraints).toDart; | ||
| 146 | 196 | ||
| 147 | - if (videoStream == null) { | ||
| 148 | - throw const MobileScannerException( | ||
| 149 | - errorCode: MobileScannerErrorCode.genericError, | ||
| 150 | - errorDetails: MobileScannerErrorDetails( | ||
| 151 | - message: | ||
| 152 | - 'Could not create a video stream from the camera with the given options. ' | ||
| 153 | - 'The browser might not support the given constraints.', | ||
| 154 | - ), | ||
| 155 | - ); | ||
| 156 | - } | 197 | + _permissionRequestInProgress = false; |
| 157 | 198 | ||
| 158 | return videoStream; | 199 | return videoStream; |
| 159 | } on DOMException catch (error, stackTrace) { | 200 | } on DOMException catch (error, stackTrace) { |
| 160 | - // At this point the permission request completed, although with an error, | ||
| 161 | - // but the error is irrelevant for the completer. | ||
| 162 | - if (!_cameraPermissionCompleter!.isCompleted) { | ||
| 163 | - _cameraPermissionCompleter!.complete(); | ||
| 164 | - } | ||
| 165 | - | 201 | + _permissionRequestInProgress = false; |
| 166 | final String errorMessage = error.toString(); | 202 | final String errorMessage = error.toString(); |
| 167 | 203 | ||
| 168 | MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError; | 204 | MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError; |
| @@ -192,11 +228,11 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -192,11 +228,11 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 192 | 228 | ||
| 193 | @override | 229 | @override |
| 194 | Widget buildCameraView() { | 230 | Widget buildCameraView() { |
| 195 | - if (!_barcodeReader.isScanning) { | ||
| 196 | - return const SizedBox(); | 231 | + if (_barcodeReader?.isScanning ?? false) { |
| 232 | + return HtmlElementView(viewType: _getViewType(_textureId)); | ||
| 197 | } | 233 | } |
| 198 | 234 | ||
| 199 | - return const HtmlElementView(viewType: _viewType); | 235 | + return const SizedBox(); |
| 200 | } | 236 | } |
| 201 | 237 | ||
| 202 | @override | 238 | @override |
| @@ -213,14 +249,6 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -213,14 +249,6 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 213 | } | 249 | } |
| 214 | 250 | ||
| 215 | @override | 251 | @override |
| 216 | - Future<void> setTorchState(TorchState torchState) { | ||
| 217 | - throw UnsupportedError( | ||
| 218 | - 'Setting the torch state is not supported for video tracks on the web.\n' | ||
| 219 | - 'See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#instance_properties_of_video_tracks', | ||
| 220 | - ); | ||
| 221 | - } | ||
| 222 | - | ||
| 223 | - @override | ||
| 224 | Future<void> setZoomScale(double zoomScale) { | 252 | Future<void> setZoomScale(double zoomScale) { |
| 225 | throw UnsupportedError( | 253 | throw UnsupportedError( |
| 226 | 'Setting the zoom scale is not supported for video tracks on the web.\n' | 254 | 'Setting the zoom scale is not supported for video tracks on the web.\n' |
| @@ -233,7 +261,7 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -233,7 +261,7 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 233 | // If the permission request has not yet completed, | 261 | // If the permission request has not yet completed, |
| 234 | // the camera view is not ready yet. | 262 | // the camera view is not ready yet. |
| 235 | // Prevent the permission popup from triggering a restart of the scanner. | 263 | // Prevent the permission popup from triggering a restart of the scanner. |
| 236 | - if (_hasPendingPermissionRequest) { | 264 | + if (_permissionRequestInProgress) { |
| 237 | throw PermissionRequestPendingException(); | 265 | throw PermissionRequestPendingException(); |
| 238 | } | 266 | } |
| 239 | 267 | ||
| @@ -242,23 +270,13 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -242,23 +270,13 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 242 | await stop(); | 270 | await stop(); |
| 243 | } | 271 | } |
| 244 | 272 | ||
| 245 | - await _barcodeReader.maybeLoadLibrary( | 273 | + _barcodeReader = ZXingBarcodeReader(); |
| 274 | + | ||
| 275 | + await _barcodeReader?.maybeLoadLibrary( | ||
| 246 | alternateScriptUrl: _alternateScriptUrl, | 276 | alternateScriptUrl: _alternateScriptUrl, |
| 247 | ); | 277 | ); |
| 248 | 278 | ||
| 249 | - // Setup the view factory & container element. | ||
| 250 | - if (_divElement == null) { | ||
| 251 | - _divElement = (document.createElement('div') as HTMLDivElement) | ||
| 252 | - ..style.width = '100%' | ||
| 253 | - ..style.height = '100%'; | ||
| 254 | - | ||
| 255 | - ui_web.platformViewRegistry.registerViewFactory( | ||
| 256 | - _viewType, | ||
| 257 | - (int id) => _divElement!, | ||
| 258 | - ); | ||
| 259 | - } | ||
| 260 | - | ||
| 261 | - if (_barcodeReader.isScanning) { | 279 | + if (_barcodeReader?.isScanning ?? false) { |
| 262 | throw const MobileScannerException( | 280 | throw const MobileScannerException( |
| 263 | errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, | 281 | errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, |
| 264 | errorDetails: MobileScannerErrorDetails( | 282 | errorDetails: MobileScannerErrorDetails( |
| @@ -280,25 +298,19 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -280,25 +298,19 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 280 | } | 298 | } |
| 281 | 299 | ||
| 282 | // Listen for changes to the media track settings. | 300 | // Listen for changes to the media track settings. |
| 283 | - _barcodeReader.setMediaTrackSettingsListener( | 301 | + _barcodeReader?.setMediaTrackSettingsListener( |
| 284 | _handleMediaTrackSettingsChange, | 302 | _handleMediaTrackSettingsChange, |
| 285 | ); | 303 | ); |
| 286 | 304 | ||
| 287 | - final HTMLVideoElement videoElement; | 305 | + _textureId += 1; // Request a new texture. |
| 288 | 306 | ||
| 289 | - // Attach the video element to the DOM, through its parent container. | ||
| 290 | - // If a video element is already present, reuse it. | ||
| 291 | - if (_divElement!.children.length == 0) { | ||
| 292 | - videoElement = document.createElement('video') as HTMLVideoElement; | 307 | + _videoElement = _createVideoElement(_textureId); |
| 293 | 308 | ||
| 294 | - _divElement!.appendChild(videoElement); | ||
| 295 | - } else { | ||
| 296 | - videoElement = _divElement!.children.item(0)! as HTMLVideoElement; | ||
| 297 | - } | 309 | + _maybeFlipVideoPreview(_videoElement, videoStream); |
| 298 | 310 | ||
| 299 | - await _barcodeReader.start( | 311 | + await _barcodeReader?.start( |
| 300 | startOptions, | 312 | startOptions, |
| 301 | - videoElement: videoElement, | 313 | + videoElement: _videoElement, |
| 302 | videoStream: videoStream, | 314 | videoStream: videoStream, |
| 303 | ); | 315 | ); |
| 304 | } catch (error, stackTrace) { | 316 | } catch (error, stackTrace) { |
| @@ -312,7 +324,7 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -312,7 +324,7 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 312 | } | 324 | } |
| 313 | 325 | ||
| 314 | try { | 326 | try { |
| 315 | - _barcodesSubscription = _barcodeReader.detectBarcodes().listen( | 327 | + _barcodesSubscription = _barcodeReader?.detectBarcodes().listen( |
| 316 | (BarcodeCapture barcode) { | 328 | (BarcodeCapture barcode) { |
| 317 | if (_barcodesController.isClosed) { | 329 | if (_barcodesController.isClosed) { |
| 318 | return; | 330 | return; |
| @@ -322,15 +334,17 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -322,15 +334,17 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 322 | }, | 334 | }, |
| 323 | ); | 335 | ); |
| 324 | 336 | ||
| 325 | - final bool hasTorch = await _barcodeReader.hasTorch(); | 337 | + final bool hasTorch = await _barcodeReader?.hasTorch() ?? false; |
| 326 | 338 | ||
| 327 | if (hasTorch && startOptions.torchEnabled) { | 339 | if (hasTorch && startOptions.torchEnabled) { |
| 328 | - await _barcodeReader.setTorchState(TorchState.on); | 340 | + await _barcodeReader?.setTorchState(TorchState.on); |
| 329 | } | 341 | } |
| 330 | 342 | ||
| 331 | return MobileScannerViewAttributes( | 343 | return MobileScannerViewAttributes( |
| 332 | - hasTorch: hasTorch, | ||
| 333 | - size: _barcodeReader.videoSize, | 344 | + // The torch of a media stream is not available for video tracks. |
| 345 | + // See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#instance_properties_of_video_tracks | ||
| 346 | + currentTorchMode: TorchState.unavailable, | ||
| 347 | + size: _barcodeReader?.videoSize ?? Size.zero, | ||
| 334 | ); | 348 | ); |
| 335 | } catch (error, stackTrace) { | 349 | } catch (error, stackTrace) { |
| 336 | throw MobileScannerException( | 350 | throw MobileScannerException( |
| @@ -352,15 +366,20 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -352,15 +366,20 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 352 | 366 | ||
| 353 | @override | 367 | @override |
| 354 | Future<void> stop() async { | 368 | Future<void> stop() async { |
| 355 | - if (_barcodesController.isClosed) { | ||
| 356 | - return; | ||
| 357 | - } | ||
| 358 | - | ||
| 359 | // Ensure the barcode scanner is stopped, by cancelling the subscription. | 369 | // Ensure the barcode scanner is stopped, by cancelling the subscription. |
| 360 | await _barcodesSubscription?.cancel(); | 370 | await _barcodesSubscription?.cancel(); |
| 361 | _barcodesSubscription = null; | 371 | _barcodesSubscription = null; |
| 362 | 372 | ||
| 363 | - await _barcodeReader.stop(); | 373 | + await _barcodeReader?.stop(); |
| 374 | + _barcodeReader = null; | ||
| 375 | + } | ||
| 376 | + | ||
| 377 | + @override | ||
| 378 | + Future<void> toggleTorch() { | ||
| 379 | + throw UnsupportedError( | ||
| 380 | + 'Setting the torch state is not supported for video tracks on the web.\n' | ||
| 381 | + 'See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#instance_properties_of_video_tracks', | ||
| 382 | + ); | ||
| 364 | } | 383 | } |
| 365 | 384 | ||
| 366 | @override | 385 | @override |
| @@ -372,31 +391,8 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -372,31 +391,8 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 372 | 391 | ||
| 373 | @override | 392 | @override |
| 374 | Future<void> dispose() async { | 393 | Future<void> dispose() async { |
| 375 | - if (_barcodesController.isClosed) { | ||
| 376 | - return; | ||
| 377 | - } | ||
| 378 | - | 394 | + // The `_barcodesController` and `_settingsController` |
| 395 | + // are not closed, as these have the same lifetime as the plugin. | ||
| 379 | await stop(); | 396 | await stop(); |
| 380 | - await _barcodesController.close(); | ||
| 381 | - await _settingsController.close(); | ||
| 382 | - | ||
| 383 | - // Finally, remove the video element from the DOM. | ||
| 384 | - try { | ||
| 385 | - final HTMLCollection? divChildren = _divElement?.children; | ||
| 386 | - | ||
| 387 | - // Since the exact element is unknown, remove all children. | ||
| 388 | - // In practice, there should only be one child, the single video element. | ||
| 389 | - if (divChildren != null && divChildren.length > 0) { | ||
| 390 | - for (int i = 0; i < divChildren.length; i++) { | ||
| 391 | - final Node? child = divChildren.item(i); | ||
| 392 | - | ||
| 393 | - if (child != null) { | ||
| 394 | - _divElement?.removeChild(child); | ||
| 395 | - } | ||
| 396 | - } | ||
| 397 | - } | ||
| 398 | - } catch (_) { | ||
| 399 | - // The video element was no longer a child of the container element. | ||
| 400 | - } | ||
| 401 | } | 397 | } |
| 402 | } | 398 | } |
| @@ -11,96 +11,68 @@ import 'package:mobile_scanner/src/web/zxing/result_point.dart'; | @@ -11,96 +11,68 @@ import 'package:mobile_scanner/src/web/zxing/result_point.dart'; | ||
| 11 | /// | 11 | /// |
| 12 | /// See also: https://github.com/zxing-js/library/blob/master/src/core/Result.ts | 12 | /// See also: https://github.com/zxing-js/library/blob/master/src/core/Result.ts |
| 13 | @JS() | 13 | @JS() |
| 14 | -@anonymous | ||
| 15 | -@staticInterop | ||
| 16 | -abstract class Result {} | ||
| 17 | - | ||
| 18 | -extension ResultExt on Result { | 14 | +extension type Result(JSObject _) implements JSObject { |
| 19 | @JS('barcodeFormat') | 15 | @JS('barcodeFormat') |
| 20 | - external JSNumber? get _barcodeFormat; | 16 | + external int? get _barcodeFormat; |
| 21 | 17 | ||
| 22 | - @JS('text') | ||
| 23 | - external JSString? get _text; | 18 | + /// Get the text of the result. |
| 19 | + external String? get text; | ||
| 24 | 20 | ||
| 25 | @JS('rawBytes') | 21 | @JS('rawBytes') |
| 26 | external JSUint8Array? get _rawBytes; | 22 | external JSUint8Array? get _rawBytes; |
| 27 | 23 | ||
| 28 | @JS('resultPoints') | 24 | @JS('resultPoints') |
| 29 | - external JSArray? get _resultPoints; | 25 | + external JSArray<ResultPoint>? get _resultPoints; |
| 30 | 26 | ||
| 31 | - @JS('timestamp') | ||
| 32 | - external JSNumber? get _timestamp; | 27 | + /// Get the timestamp of the result. |
| 28 | + external int? get timestamp; | ||
| 33 | 29 | ||
| 34 | /// Get the barcode format of the result. | 30 | /// Get the barcode format of the result. |
| 35 | /// | 31 | /// |
| 36 | /// See also https://github.com/zxing-js/library/blob/master/src/core/BarcodeFormat.ts | 32 | /// See also https://github.com/zxing-js/library/blob/master/src/core/BarcodeFormat.ts |
| 37 | BarcodeFormat get barcodeFormat { | 33 | BarcodeFormat get barcodeFormat { |
| 38 | - switch (_barcodeFormat?.toDartInt) { | ||
| 39 | - case 0: | ||
| 40 | - return BarcodeFormat.aztec; | ||
| 41 | - case 1: | ||
| 42 | - return BarcodeFormat.codabar; | ||
| 43 | - case 2: | ||
| 44 | - return BarcodeFormat.code39; | ||
| 45 | - case 3: | ||
| 46 | - return BarcodeFormat.code93; | ||
| 47 | - case 4: | ||
| 48 | - return BarcodeFormat.code128; | ||
| 49 | - case 5: | ||
| 50 | - return BarcodeFormat.dataMatrix; | ||
| 51 | - case 6: | ||
| 52 | - return BarcodeFormat.ean8; | ||
| 53 | - case 7: | ||
| 54 | - return BarcodeFormat.ean13; | ||
| 55 | - case 8: | ||
| 56 | - return BarcodeFormat.itf; | ||
| 57 | - case 9: | ||
| 58 | - // Maxicode | ||
| 59 | - return BarcodeFormat.unknown; | ||
| 60 | - case 10: | ||
| 61 | - return BarcodeFormat.pdf417; | ||
| 62 | - case 11: | ||
| 63 | - return BarcodeFormat.qrCode; | ||
| 64 | - case 12: | ||
| 65 | - // RSS 14 | ||
| 66 | - return BarcodeFormat.unknown; | ||
| 67 | - case 13: | ||
| 68 | - // RSS EXPANDED | ||
| 69 | - return BarcodeFormat.unknown; | ||
| 70 | - case 14: | ||
| 71 | - return BarcodeFormat.upcA; | ||
| 72 | - case 15: | ||
| 73 | - return BarcodeFormat.upcE; | ||
| 74 | - case 16: | ||
| 75 | - // UPC/EAN extension | ||
| 76 | - return BarcodeFormat.unknown; | ||
| 77 | - default: | ||
| 78 | - return BarcodeFormat.unknown; | ||
| 79 | - } | 34 | + return switch (_barcodeFormat) { |
| 35 | + 0 => BarcodeFormat.aztec, | ||
| 36 | + 1 => BarcodeFormat.codabar, | ||
| 37 | + 2 => BarcodeFormat.code39, | ||
| 38 | + 3 => BarcodeFormat.code93, | ||
| 39 | + 4 => BarcodeFormat.code128, | ||
| 40 | + 5 => BarcodeFormat.dataMatrix, | ||
| 41 | + 6 => BarcodeFormat.ean8, | ||
| 42 | + 7 => BarcodeFormat.ean13, | ||
| 43 | + 8 => BarcodeFormat.itf, | ||
| 44 | + // Maxicode | ||
| 45 | + 9 => BarcodeFormat.unknown, | ||
| 46 | + 10 => BarcodeFormat.pdf417, | ||
| 47 | + 11 => BarcodeFormat.qrCode, | ||
| 48 | + // RSS 14 | ||
| 49 | + 12 => BarcodeFormat.unknown, | ||
| 50 | + // RSS EXPANDED | ||
| 51 | + 13 => BarcodeFormat.unknown, | ||
| 52 | + 14 => BarcodeFormat.upcA, | ||
| 53 | + 15 => BarcodeFormat.upcE, | ||
| 54 | + // UPC/EAN extension | ||
| 55 | + 16 => BarcodeFormat.unknown, | ||
| 56 | + _ => BarcodeFormat.unknown | ||
| 57 | + }; | ||
| 80 | } | 58 | } |
| 81 | 59 | ||
| 60 | + /// Get the raw bytes of the result. | ||
| 61 | + Uint8List? get rawBytes => _rawBytes?.toDart; | ||
| 62 | + | ||
| 82 | /// Get the corner points of the result. | 63 | /// Get the corner points of the result. |
| 83 | List<Offset> get resultPoints { | 64 | List<Offset> get resultPoints { |
| 84 | - final JSArray? points = _resultPoints; | 65 | + final JSArray<ResultPoint>? points = _resultPoints; |
| 85 | 66 | ||
| 86 | if (points == null) { | 67 | if (points == null) { |
| 87 | - return []; | 68 | + return const []; |
| 88 | } | 69 | } |
| 89 | 70 | ||
| 90 | - return points.toDart.cast<ResultPoint>().map((point) { | 71 | + return points.toDart.map((point) { |
| 91 | return Offset(point.x, point.y); | 72 | return Offset(point.x, point.y); |
| 92 | }).toList(); | 73 | }).toList(); |
| 93 | } | 74 | } |
| 94 | 75 | ||
| 95 | - /// Get the raw bytes of the result. | ||
| 96 | - Uint8List? get rawBytes => _rawBytes?.toDart; | ||
| 97 | - | ||
| 98 | - /// Get the text of the result. | ||
| 99 | - String? get text => _text?.toDart; | ||
| 100 | - | ||
| 101 | - /// Get the timestamp of the result. | ||
| 102 | - int? get timestamp => _timestamp?.toDartInt; | ||
| 103 | - | ||
| 104 | /// Convert this result to a [Barcode]. | 76 | /// Convert this result to a [Barcode]. |
| 105 | Barcode get toBarcode { | 77 | Barcode get toBarcode { |
| 106 | return Barcode( | 78 | return Barcode( |
| @@ -4,20 +4,10 @@ import 'dart:js_interop'; | @@ -4,20 +4,10 @@ import 'dart:js_interop'; | ||
| 4 | /// | 4 | /// |
| 5 | /// See also: https://github.com/zxing-js/library/blob/master/src/core/ResultPoint.ts | 5 | /// See also: https://github.com/zxing-js/library/blob/master/src/core/ResultPoint.ts |
| 6 | @JS() | 6 | @JS() |
| 7 | -@anonymous | ||
| 8 | -@staticInterop | ||
| 9 | -abstract class ResultPoint {} | ||
| 10 | - | ||
| 11 | -extension ResultPointExt on ResultPoint { | ||
| 12 | - @JS('x') | ||
| 13 | - external JSNumber get _x; | ||
| 14 | - | ||
| 15 | - @JS('y') | ||
| 16 | - external JSNumber get _y; | ||
| 17 | - | 7 | +extension type ResultPoint(JSObject _) implements JSObject { |
| 18 | /// The x coordinate of the point. | 8 | /// The x coordinate of the point. |
| 19 | - double get x => _x.toDartDouble; | 9 | + external double get x; |
| 20 | 10 | ||
| 21 | /// The y coordinate of the point. | 11 | /// The y coordinate of the point. |
| 22 | - double get y => _y.toDartDouble; | 12 | + external double get y; |
| 23 | } | 13 | } |
| @@ -12,8 +12,6 @@ import 'package:mobile_scanner/src/web/zxing/result.dart'; | @@ -12,8 +12,6 @@ import 'package:mobile_scanner/src/web/zxing/result.dart'; | ||
| 12 | import 'package:mobile_scanner/src/web/zxing/zxing_browser_multi_format_reader.dart'; | 12 | import 'package:mobile_scanner/src/web/zxing/zxing_browser_multi_format_reader.dart'; |
| 13 | import 'package:web/web.dart' as web; | 13 | import 'package:web/web.dart' as web; |
| 14 | 14 | ||
| 15 | -// TODO: remove the JSAny casts once upgraded to a package:web version that restores "implements JSAny" | ||
| 16 | - | ||
| 17 | /// A barcode reader implementation that uses the ZXing library. | 15 | /// A barcode reader implementation that uses the ZXing library. |
| 18 | final class ZXingBarcodeReader extends BarcodeReader { | 16 | final class ZXingBarcodeReader extends BarcodeReader { |
| 19 | ZXingBarcodeReader(); | 17 | ZXingBarcodeReader(); |
| @@ -48,42 +46,6 @@ final class ZXingBarcodeReader extends BarcodeReader { | @@ -48,42 +46,6 @@ final class ZXingBarcodeReader extends BarcodeReader { | ||
| 48 | @override | 46 | @override |
| 49 | String get scriptUrl => 'https://unpkg.com/@zxing/library@0.19.1'; | 47 | String get scriptUrl => 'https://unpkg.com/@zxing/library@0.19.1'; |
| 50 | 48 | ||
| 51 | - /// Get the barcode format from the ZXing library, for the given [format]. | ||
| 52 | - static int getZXingBarcodeFormat(BarcodeFormat format) { | ||
| 53 | - switch (format) { | ||
| 54 | - case BarcodeFormat.aztec: | ||
| 55 | - return 0; | ||
| 56 | - case BarcodeFormat.codabar: | ||
| 57 | - return 1; | ||
| 58 | - case BarcodeFormat.code39: | ||
| 59 | - return 2; | ||
| 60 | - case BarcodeFormat.code93: | ||
| 61 | - return 3; | ||
| 62 | - case BarcodeFormat.code128: | ||
| 63 | - return 4; | ||
| 64 | - case BarcodeFormat.dataMatrix: | ||
| 65 | - return 5; | ||
| 66 | - case BarcodeFormat.ean8: | ||
| 67 | - return 6; | ||
| 68 | - case BarcodeFormat.ean13: | ||
| 69 | - return 7; | ||
| 70 | - case BarcodeFormat.itf: | ||
| 71 | - return 8; | ||
| 72 | - case BarcodeFormat.pdf417: | ||
| 73 | - return 10; | ||
| 74 | - case BarcodeFormat.qrCode: | ||
| 75 | - return 11; | ||
| 76 | - case BarcodeFormat.upcA: | ||
| 77 | - return 14; | ||
| 78 | - case BarcodeFormat.upcE: | ||
| 79 | - return 15; | ||
| 80 | - case BarcodeFormat.unknown: | ||
| 81 | - case BarcodeFormat.all: | ||
| 82 | - default: | ||
| 83 | - return -1; | ||
| 84 | - } | ||
| 85 | - } | ||
| 86 | - | ||
| 87 | JSMap? _createReaderHints(List<BarcodeFormat> formats) { | 49 | JSMap? _createReaderHints(List<BarcodeFormat> formats) { |
| 88 | if (formats.isEmpty || formats.contains(BarcodeFormat.all)) { | 50 | if (formats.isEmpty || formats.contains(BarcodeFormat.all)) { |
| 89 | return null; | 51 | return null; |
| @@ -96,8 +58,7 @@ final class ZXingBarcodeReader extends BarcodeReader { | @@ -96,8 +58,7 @@ final class ZXingBarcodeReader extends BarcodeReader { | ||
| 96 | hints.set( | 58 | hints.set( |
| 97 | 2.toJS, | 59 | 2.toJS, |
| 98 | [ | 60 | [ |
| 99 | - for (final BarcodeFormat format in formats) | ||
| 100 | - getZXingBarcodeFormat(format).toJS, | 61 | + for (final BarcodeFormat format in formats) format.toJS, |
| 101 | ].toJS, | 62 | ].toJS, |
| 102 | ); | 63 | ); |
| 103 | 64 | ||
| @@ -114,9 +75,9 @@ final class ZXingBarcodeReader extends BarcodeReader { | @@ -114,9 +75,9 @@ final class ZXingBarcodeReader extends BarcodeReader { | ||
| 114 | web.MediaStream videoStream, | 75 | web.MediaStream videoStream, |
| 115 | ) async { | 76 | ) async { |
| 116 | final JSPromise? result = _reader?.attachStreamToVideo.callAsFunction( | 77 | final JSPromise? result = _reader?.attachStreamToVideo.callAsFunction( |
| 117 | - _reader as JSAny?, | ||
| 118 | - videoStream as JSAny, | ||
| 119 | - videoElement as JSAny, | 78 | + _reader, |
| 79 | + videoStream, | ||
| 80 | + videoElement, | ||
| 120 | ) as JSPromise?; | 81 | ) as JSPromise?; |
| 121 | 82 | ||
| 122 | await result?.toDart; | 83 | await result?.toDart; |
| @@ -135,8 +96,8 @@ final class ZXingBarcodeReader extends BarcodeReader { | @@ -135,8 +96,8 @@ final class ZXingBarcodeReader extends BarcodeReader { | ||
| 135 | 96 | ||
| 136 | controller.onListen = () { | 97 | controller.onListen = () { |
| 137 | _reader?.decodeContinuously.callAsFunction( | 98 | _reader?.decodeContinuously.callAsFunction( |
| 138 | - _reader as JSAny?, | ||
| 139 | - _reader?.videoElement as JSAny?, | 99 | + _reader, |
| 100 | + _reader?.videoElement, | ||
| 140 | (Result? result, JSAny? error) { | 101 | (Result? result, JSAny? error) { |
| 141 | if (controller.isClosed || result == null) { | 102 | if (controller.isClosed || result == null) { |
| 142 | return; | 103 | return; |
| @@ -155,8 +116,8 @@ final class ZXingBarcodeReader extends BarcodeReader { | @@ -155,8 +116,8 @@ final class ZXingBarcodeReader extends BarcodeReader { | ||
| 155 | // when the stream subscription returned by this method is cancelled in `MobileScannerWeb.stop()`. | 116 | // when the stream subscription returned by this method is cancelled in `MobileScannerWeb.stop()`. |
| 156 | // This avoids both leaving the barcode scanner running and a memory leak for the stream subscription. | 117 | // This avoids both leaving the barcode scanner running and a memory leak for the stream subscription. |
| 157 | controller.onCancel = () async { | 118 | controller.onCancel = () async { |
| 158 | - _reader?.stopContinuousDecode.callAsFunction(_reader as JSAny?); | ||
| 159 | - _reader?.reset.callAsFunction(_reader as JSAny?); | 119 | + _reader?.stopContinuousDecode.callAsFunction(_reader); |
| 120 | + _reader?.reset.callAsFunction(_reader); | ||
| 160 | await controller.close(); | 121 | await controller.close(); |
| 161 | }; | 122 | }; |
| 162 | 123 | ||
| @@ -185,7 +146,7 @@ final class ZXingBarcodeReader extends BarcodeReader { | @@ -185,7 +146,7 @@ final class ZXingBarcodeReader extends BarcodeReader { | ||
| 185 | 146 | ||
| 186 | _reader = ZXingBrowserMultiFormatReader( | 147 | _reader = ZXingBrowserMultiFormatReader( |
| 187 | _createReaderHints(formats), | 148 | _createReaderHints(formats), |
| 188 | - detectionTimeoutMs.toJS, | 149 | + detectionTimeoutMs, |
| 189 | ); | 150 | ); |
| 190 | 151 | ||
| 191 | await _prepareVideoElement(videoElement, videoStream); | 152 | await _prepareVideoElement(videoElement, videoStream); |
| @@ -199,8 +160,32 @@ final class ZXingBarcodeReader extends BarcodeReader { | @@ -199,8 +160,32 @@ final class ZXingBarcodeReader extends BarcodeReader { | ||
| 199 | @override | 160 | @override |
| 200 | Future<void> stop() async { | 161 | Future<void> stop() async { |
| 201 | _onMediaTrackSettingsChanged = null; | 162 | _onMediaTrackSettingsChanged = null; |
| 202 | - _reader?.stopContinuousDecode.callAsFunction(_reader as JSAny?); | ||
| 203 | - _reader?.reset.callAsFunction(_reader as JSAny?); | 163 | + _reader?.stopContinuousDecode.callAsFunction(_reader); |
| 164 | + _reader?.reset.callAsFunction(_reader); | ||
| 204 | _reader = null; | 165 | _reader = null; |
| 205 | } | 166 | } |
| 206 | } | 167 | } |
| 168 | + | ||
| 169 | +extension on BarcodeFormat { | ||
| 170 | + /// Get the barcode format from the ZXing library. | ||
| 171 | + JSNumber get toJS { | ||
| 172 | + final int zxingFormat = switch (this) { | ||
| 173 | + BarcodeFormat.aztec => 0, | ||
| 174 | + BarcodeFormat.codabar => 1, | ||
| 175 | + BarcodeFormat.code39 => 2, | ||
| 176 | + BarcodeFormat.code93 => 3, | ||
| 177 | + BarcodeFormat.code128 => 4, | ||
| 178 | + BarcodeFormat.dataMatrix => 5, | ||
| 179 | + BarcodeFormat.ean8 => 6, | ||
| 180 | + BarcodeFormat.ean13 => 7, | ||
| 181 | + BarcodeFormat.itf => 8, | ||
| 182 | + BarcodeFormat.pdf417 => 10, | ||
| 183 | + BarcodeFormat.qrCode => 11, | ||
| 184 | + BarcodeFormat.upcA => 14, | ||
| 185 | + BarcodeFormat.upcE => 15, | ||
| 186 | + BarcodeFormat.unknown || BarcodeFormat.all || _ => -1, | ||
| 187 | + }; | ||
| 188 | + | ||
| 189 | + return zxingFormat.toJS; | ||
| 190 | + } | ||
| 191 | +} |
| @@ -7,8 +7,7 @@ import 'package:web/web.dart'; | @@ -7,8 +7,7 @@ import 'package:web/web.dart'; | ||
| 7 | /// | 7 | /// |
| 8 | /// See https://github.com/zxing-js/library/blob/master/src/browser/BrowserMultiFormatReader.ts | 8 | /// See https://github.com/zxing-js/library/blob/master/src/browser/BrowserMultiFormatReader.ts |
| 9 | @JS('ZXing.BrowserMultiFormatReader') | 9 | @JS('ZXing.BrowserMultiFormatReader') |
| 10 | -@staticInterop | ||
| 11 | -class ZXingBrowserMultiFormatReader { | 10 | +extension type ZXingBrowserMultiFormatReader._(JSObject _) implements JSObject { |
| 12 | /// Construct a new `ZXing.BrowserMultiFormatReader`. | 11 | /// Construct a new `ZXing.BrowserMultiFormatReader`. |
| 13 | /// | 12 | /// |
| 14 | /// The [hints] are the configuration options for the reader. | 13 | /// The [hints] are the configuration options for the reader. |
| @@ -17,11 +16,9 @@ class ZXingBrowserMultiFormatReader { | @@ -17,11 +16,9 @@ class ZXingBrowserMultiFormatReader { | ||
| 17 | /// See also: https://github.com/zxing-js/library/blob/master/src/core/DecodeHintType.ts | 16 | /// See also: https://github.com/zxing-js/library/blob/master/src/core/DecodeHintType.ts |
| 18 | external factory ZXingBrowserMultiFormatReader( | 17 | external factory ZXingBrowserMultiFormatReader( |
| 19 | JSMap? hints, | 18 | JSMap? hints, |
| 20 | - JSNumber? timeBetweenScansMillis, | 19 | + int timeBetweenScansMillis, |
| 21 | ); | 20 | ); |
| 22 | -} | ||
| 23 | 21 | ||
| 24 | -extension ZXingBrowserMultiFormatReaderExt on ZXingBrowserMultiFormatReader { | ||
| 25 | /// Attach a [MediaStream] to a [HTMLVideoElement]. | 22 | /// Attach a [MediaStream] to a [HTMLVideoElement]. |
| 26 | /// | 23 | /// |
| 27 | /// This function accepts a [MediaStream] and a [HTMLVideoElement] as arguments, | 24 | /// This function accepts a [MediaStream] and a [HTMLVideoElement] as arguments, |
| @@ -67,8 +67,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -67,8 +67,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 67 | requestPermission(call, result) | 67 | requestPermission(call, result) |
| 68 | case "start": | 68 | case "start": |
| 69 | start(call, result) | 69 | start(call, result) |
| 70 | - case "torch": | ||
| 71 | - toggleTorch(call, result) | 70 | + case "toggleTorch": |
| 71 | + toggleTorch(result) | ||
| 72 | case "setScale": | 72 | case "setScale": |
| 73 | setScale(call, result) | 73 | setScale(call, result) |
| 74 | case "resetScale": | 74 | case "resetScale": |
| @@ -298,12 +298,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -298,12 +298,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 298 | 298 | ||
| 299 | // Turn on the torch if requested. | 299 | // Turn on the torch if requested. |
| 300 | if (torch) { | 300 | if (torch) { |
| 301 | - do { | ||
| 302 | - try self.toggleTorchInternal(.on) | ||
| 303 | - } catch { | ||
| 304 | - // If the torch could not be turned on, | ||
| 305 | - // continue the capture session. | ||
| 306 | - } | 301 | + self.turnTorchOn() |
| 307 | } | 302 | } |
| 308 | 303 | ||
| 309 | device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil) | 304 | device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil) |
| @@ -336,17 +331,22 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -336,17 +331,22 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 336 | captureSession!.startRunning() | 331 | captureSession!.startRunning() |
| 337 | let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription) | 332 | let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription) |
| 338 | let size = ["width": Double(dimensions.width), "height": Double(dimensions.height)] | 333 | let size = ["width": Double(dimensions.width), "height": Double(dimensions.height)] |
| 339 | - let answer: [String : Any?] = ["textureId": textureId, "size": size, "torchable": device.hasTorch] | 334 | + |
| 335 | + let answer: [String : Any?] = [ | ||
| 336 | + "textureId": textureId, | ||
| 337 | + "size": size, | ||
| 338 | + "currentTorchState": device.hasTorch ? device.torchMode.rawValue : -1, | ||
| 339 | + ] | ||
| 340 | result(answer) | 340 | result(answer) |
| 341 | } | 341 | } |
| 342 | 342 | ||
| 343 | // TODO: this method should be removed when iOS and MacOS share their implementation. | 343 | // TODO: this method should be removed when iOS and MacOS share their implementation. |
| 344 | - private func toggleTorchInternal(_ torch: AVCaptureDevice.TorchMode) throws { | 344 | + private func toggleTorchInternal() { |
| 345 | guard let device = self.device else { | 345 | guard let device = self.device else { |
| 346 | return | 346 | return |
| 347 | } | 347 | } |
| 348 | 348 | ||
| 349 | - if (!device.hasTorch || !device.isTorchModeSupported(torch)) { | 349 | + if (!device.hasTorch) { |
| 350 | return | 350 | return |
| 351 | } | 351 | } |
| 352 | 352 | ||
| @@ -355,12 +355,57 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -355,12 +355,57 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 355 | return | 355 | return |
| 356 | } | 356 | } |
| 357 | } | 357 | } |
| 358 | + | ||
| 359 | + var newTorchMode: AVCaptureDevice.TorchMode = device.torchMode | ||
| 360 | + | ||
| 361 | + switch(device.torchMode) { | ||
| 362 | + case AVCaptureDevice.TorchMode.auto: | ||
| 363 | + if #available(macOS 10.15, *) { | ||
| 364 | + newTorchMode = device.isTorchActive ? AVCaptureDevice.TorchMode.off : AVCaptureDevice.TorchMode.on | ||
| 365 | + } | ||
| 366 | + break; | ||
| 367 | + case AVCaptureDevice.TorchMode.off: | ||
| 368 | + newTorchMode = AVCaptureDevice.TorchMode.on | ||
| 369 | + break; | ||
| 370 | + case AVCaptureDevice.TorchMode.on: | ||
| 371 | + newTorchMode = AVCaptureDevice.TorchMode.off | ||
| 372 | + break; | ||
| 373 | + default: | ||
| 374 | + return; | ||
| 375 | + } | ||
| 376 | + | ||
| 377 | + if (!device.isTorchModeSupported(newTorchMode) || device.torchMode == newTorchMode) { | ||
| 378 | + return; | ||
| 379 | + } | ||
| 358 | 380 | ||
| 359 | - if (device.torchMode != torch) { | 381 | + do { |
| 360 | try device.lockForConfiguration() | 382 | try device.lockForConfiguration() |
| 361 | - device.torchMode = torch | 383 | + device.torchMode = newTorchMode |
| 362 | device.unlockForConfiguration() | 384 | device.unlockForConfiguration() |
| 385 | + } catch(_) {} | ||
| 386 | + } | ||
| 387 | + | ||
| 388 | + /// Turn the torch on. | ||
| 389 | + private func turnTorchOn() { | ||
| 390 | + guard let device = self.device else { | ||
| 391 | + return | ||
| 392 | + } | ||
| 393 | + | ||
| 394 | + if (!device.hasTorch || !device.isTorchModeSupported(.on) || device.torchMode == .on) { | ||
| 395 | + return | ||
| 363 | } | 396 | } |
| 397 | + | ||
| 398 | + if #available(macOS 15.0, *) { | ||
| 399 | + if(!device.isTorchAvailable) { | ||
| 400 | + return | ||
| 401 | + } | ||
| 402 | + } | ||
| 403 | + | ||
| 404 | + do { | ||
| 405 | + try device.lockForConfiguration() | ||
| 406 | + device.torchMode = .on | ||
| 407 | + device.unlockForConfiguration() | ||
| 408 | + } catch(_) {} | ||
| 364 | } | 409 | } |
| 365 | 410 | ||
| 366 | /// Reset the zoom scale. | 411 | /// Reset the zoom scale. |
| @@ -375,15 +420,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -375,15 +420,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 375 | result(nil) | 420 | result(nil) |
| 376 | } | 421 | } |
| 377 | 422 | ||
| 378 | - private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 379 | - let requestedTorchMode: AVCaptureDevice.TorchMode = call.arguments as! Int == 1 ? .on : .off | ||
| 380 | - | ||
| 381 | - do { | ||
| 382 | - try self.toggleTorchInternal(requestedTorchMode) | ||
| 383 | - result(nil) | ||
| 384 | - } catch { | ||
| 385 | - result(FlutterError(code: "MobileScanner", message: error.localizedDescription, details: nil)) | ||
| 386 | - } | 423 | + private func toggleTorch(_ result: @escaping FlutterResult) { |
| 424 | + self.toggleTorchInternal() | ||
| 425 | + result(nil) | ||
| 387 | } | 426 | } |
| 388 | 427 | ||
| 389 | // func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | 428 | // func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
| @@ -439,7 +478,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -439,7 +478,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 439 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { | 478 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { |
| 440 | switch keyPath { | 479 | switch keyPath { |
| 441 | case "torchMode": | 480 | case "torchMode": |
| 442 | - // off = 0 on = 1 auto = 2 | 481 | + // Off = 0, On = 1, Auto = 2 |
| 443 | let state = change?[.newKey] as? Int | 482 | let state = change?[.newKey] as? Int |
| 444 | let event: [String: Any?] = ["name": "torchState", "data": state] | 483 | let event: [String: Any?] = ["name": "torchState", "data": state] |
| 445 | sink?(event) | 484 | sink?(event) |
| @@ -4,14 +4,14 @@ | @@ -4,14 +4,14 @@ | ||
| 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 = '3.5.6' | 7 | + s.version = '5.1.1' |
| 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. |
| 11 | DESC | 11 | DESC |
| 12 | - s.homepage = 'http://example.com' | 12 | + s.homepage = 'https://github.com/juliansteenbakker/mobile_scanner' |
| 13 | s.license = { :file => '../LICENSE' } | 13 | s.license = { :file => '../LICENSE' } |
| 14 | - s.author = { 'Your Company' => 'email@example.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 = 'Classes/**/*' |
| 17 | s.dependency 'FlutterMacOS' | 17 | s.dependency 'FlutterMacOS' |
| 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.0.0-beta.2 | 3 | +version: 5.1.1 |
| 4 | repository: https://github.com/juliansteenbakker/mobile_scanner | 4 | repository: https://github.com/juliansteenbakker/mobile_scanner |
| 5 | 5 | ||
| 6 | screenshots: | 6 | screenshots: |
| @@ -16,16 +16,16 @@ screenshots: | @@ -16,16 +16,16 @@ screenshots: | ||
| 16 | path: example/screenshots/overlay.png | 16 | path: example/screenshots/overlay.png |
| 17 | 17 | ||
| 18 | environment: | 18 | environment: |
| 19 | - sdk: ">=3.2.0 <4.0.0" | ||
| 20 | - flutter: ">=3.16.0" | 19 | + sdk: ">=3.3.0 <4.0.0" |
| 20 | + flutter: ">=3.19.0" | ||
| 21 | 21 | ||
| 22 | dependencies: | 22 | dependencies: |
| 23 | flutter: | 23 | flutter: |
| 24 | sdk: flutter | 24 | sdk: flutter |
| 25 | flutter_web_plugins: | 25 | flutter_web_plugins: |
| 26 | sdk: flutter | 26 | sdk: flutter |
| 27 | - plugin_platform_interface: ^2.0.2 | ||
| 28 | - web: ^0.4.0 | 27 | + plugin_platform_interface: ^2.0.2 |
| 28 | + web: ^0.5.1 | ||
| 29 | 29 | ||
| 30 | dev_dependencies: | 30 | dev_dependencies: |
| 31 | flutter_test: | 31 | flutter_test: |
| @@ -7,7 +7,8 @@ void main() { | @@ -7,7 +7,8 @@ void main() { | ||
| 7 | const values = <int, TorchState>{ | 7 | const values = <int, TorchState>{ |
| 8 | 0: TorchState.off, | 8 | 0: TorchState.off, |
| 9 | 1: TorchState.on, | 9 | 1: TorchState.on, |
| 10 | - 2: TorchState.unavailable, | 10 | + 2: TorchState.auto, |
| 11 | + -1: TorchState.unavailable, | ||
| 11 | }; | 12 | }; |
| 12 | 13 | ||
| 13 | for (final MapEntry<int, TorchState> entry in values.entries) { | 14 | for (final MapEntry<int, TorchState> entry in values.entries) { |
| @@ -18,7 +19,7 @@ void main() { | @@ -18,7 +19,7 @@ void main() { | ||
| 18 | }); | 19 | }); |
| 19 | 20 | ||
| 20 | test('invalid raw value throws argument error', () { | 21 | test('invalid raw value throws argument error', () { |
| 21 | - const int negative = -1; | 22 | + const int negative = -2; |
| 22 | const int outOfRange = 3; | 23 | const int outOfRange = 3; |
| 23 | 24 | ||
| 24 | expect(() => TorchState.fromRawValue(negative), throwsArgumentError); | 25 | expect(() => TorchState.fromRawValue(negative), throwsArgumentError); |
| @@ -27,9 +28,10 @@ void main() { | @@ -27,9 +28,10 @@ void main() { | ||
| 27 | 28 | ||
| 28 | test('can be converted to raw value', () { | 29 | test('can be converted to raw value', () { |
| 29 | const values = <TorchState, int>{ | 30 | const values = <TorchState, int>{ |
| 31 | + TorchState.unavailable: -1, | ||
| 30 | TorchState.off: 0, | 32 | TorchState.off: 0, |
| 31 | TorchState.on: 1, | 33 | TorchState.on: 1, |
| 32 | - TorchState.unavailable: 2, | 34 | + TorchState.auto: 2, |
| 33 | }; | 35 | }; |
| 34 | 36 | ||
| 35 | for (final MapEntry<TorchState, int> entry in values.entries) { | 37 | for (final MapEntry<TorchState, int> entry in values.entries) { |
-
Please register or login to post a comment