Julian Steenbakker
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;
1 <?xml version="1.0" encoding="UTF-8"?> 1 <?xml version="1.0" encoding="UTF-8"?>
2 <Scheme 2 <Scheme
3 - LastUpgradeVersion = "1430" 3 + LastUpgradeVersion = "1510"
4 version = "1.3"> 4 version = "1.3">
5 <BuildAction 5 <BuildAction
6 parallelizeBuildables = "YES" 6 parallelizeBuildables = "YES"
@@ -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')),
  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 = {
1 <?xml version="1.0" encoding="UTF-8"?> 1 <?xml version="1.0" encoding="UTF-8"?>
2 <Scheme 2 <Scheme
3 - LastUpgradeVersion = "1430" 3 + LastUpgradeVersion = "1510"
4 version = "1.3"> 4 version = "1.3">
5 <BuildAction 5 <BuildAction
6 parallelizeBuildables = "YES" 6 parallelizeBuildables = "YES"
@@ -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 {
  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 }
  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) {