Julian Steenbakker
Committed by GitHub

Merge branch 'master' into dependabot/gradle/android/androidx.camera-camera-lifecycle-1.3.2

1 -## NEXT 1 +## 5.0.0-beta.3
  2 +
  3 +**BREAKING CHANGES:**
  4 +
  5 +* Flutter 3.19.0 is now required.
  6 +* [iOS] iOS 12.0 is now the minimum supported iOS version.
  7 +* [iOS] Adds a Privacy Manifest.
  8 +
  9 +Bugs fixed:
  10 +* Fixed an issue where the camera preview and barcode scanner did not work the second time on web.
  11 +
  12 +Improvements:
  13 +* [web] Migrates to extension types. (thanks @koji-1009 !)
  14 +
  15 +## 5.0.0-beta.2
2 16
3 Bugs fixed: 17 Bugs fixed:
4 * Fixed an issue where the scan window was not updated when its size was changed. (thanks @navaronbracke !) 18 * Fixed an issue where the scan window was not updated when its size was changed. (thanks @navaronbracke !)
@@ -7,7 +21,6 @@ Bugs fixed: @@ -7,7 +21,6 @@ Bugs fixed:
7 21
8 **BREAKING CHANGES:** 22 **BREAKING CHANGES:**
9 23
10 -* Flutter 3.19.0 is now required.  
11 * The `width` and `height` of `BarcodeCapture` have been removed, in favor of `size`. 24 * The `width` and `height` of `BarcodeCapture` have been removed, in favor of `size`.
12 * The `raw` attribute is now `Object?` instead of `dynamic`, so that it participates in type promotion. 25 * The `raw` attribute is now `Object?` instead of `dynamic`, so that it participates in type promotion.
13 * The `MobileScannerArguments` class has been removed from the public API, as it is an internal type. 26 * The `MobileScannerArguments` class has been removed from the public API, as it is an internal type.
@@ -165,5 +165,5 @@ Future<void> dispose() async { @@ -165,5 +165,5 @@ Future<void> dispose() async {
165 165
166 To display the camera preview, pass the controller to a `MobileScanner` widget. 166 To display the camera preview, pass the controller to a `MobileScanner` widget.
167 167
168 -See the examples for runnable examples of various usages, 168 +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. 169 such as the basic usage, applying a scan window, or retrieving images from the barcodes.
@@ -75,9 +75,9 @@ dependencies { @@ -75,9 +75,9 @@ dependencies {
75 implementation 'com.google.mlkit:barcode-scanning:17.2.0' 75 implementation 'com.google.mlkit:barcode-scanning:17.2.0'
76 } 76 }
77 77
78 - implementation 'androidx.camera:camera-camera2:1.3.1'  
79 implementation 'androidx.camera:camera-lifecycle:1.3.2' 78 implementation 'androidx.camera:camera-lifecycle:1.3.2'
  79 + implementation 'androidx.camera:camera-camera2:1.3.2'
80 80
81 testImplementation 'org.jetbrains.kotlin:kotlin-test' 81 testImplementation 'org.jetbrains.kotlin:kotlin-test'
82 - testImplementation 'org.mockito:mockito-core:5.10.0' 82 + testImplementation 'org.mockito:mockito-core:5.11.0'
83 } 83 }
@@ -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.
@@ -62,7 +62,3 @@ android { @@ -62,7 +62,3 @@ android {
62 flutter { 62 flutter {
63 source '../..' 63 source '../..'
64 } 64 }
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()
@@ -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 "8.2.2" apply false
  22 + id "org.jetbrains.kotlin.android" version "1.9.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
@@ -452,7 +452,7 @@ @@ -452,7 +452,7 @@
452 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 452 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
453 GCC_WARN_UNUSED_FUNCTION = YES; 453 GCC_WARN_UNUSED_FUNCTION = YES;
454 GCC_WARN_UNUSED_VARIABLE = YES; 454 GCC_WARN_UNUSED_VARIABLE = YES;
455 - IPHONEOS_DEPLOYMENT_TARGET = 11.0; 455 + IPHONEOS_DEPLOYMENT_TARGET = 12.0;
456 MTL_ENABLE_DEBUG_INFO = NO; 456 MTL_ENABLE_DEBUG_INFO = NO;
457 SDKROOT = iphoneos; 457 SDKROOT = iphoneos;
458 SUPPORTED_PLATFORMS = iphoneos; 458 SUPPORTED_PLATFORMS = iphoneos;
@@ -583,7 +583,7 @@ @@ -583,7 +583,7 @@
583 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 583 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
584 GCC_WARN_UNUSED_FUNCTION = YES; 584 GCC_WARN_UNUSED_FUNCTION = YES;
585 GCC_WARN_UNUSED_VARIABLE = YES; 585 GCC_WARN_UNUSED_VARIABLE = YES;
586 - IPHONEOS_DEPLOYMENT_TARGET = 11.0; 586 + IPHONEOS_DEPLOYMENT_TARGET = 12.0;
587 MTL_ENABLE_DEBUG_INFO = YES; 587 MTL_ENABLE_DEBUG_INFO = YES;
588 ONLY_ACTIVE_ARCH = YES; 588 ONLY_ACTIVE_ARCH = YES;
589 SDKROOT = iphoneos; 589 SDKROOT = iphoneos;
@@ -632,7 +632,7 @@ @@ -632,7 +632,7 @@
632 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 632 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
633 GCC_WARN_UNUSED_FUNCTION = YES; 633 GCC_WARN_UNUSED_FUNCTION = YES;
634 GCC_WARN_UNUSED_VARIABLE = YES; 634 GCC_WARN_UNUSED_VARIABLE = YES;
635 - IPHONEOS_DEPLOYMENT_TARGET = 11.0; 635 + IPHONEOS_DEPLOYMENT_TARGET = 12.0;
636 MTL_ENABLE_DEBUG_INFO = NO; 636 MTL_ENABLE_DEBUG_INFO = NO;
637 SDKROOT = iphoneos; 637 SDKROOT = iphoneos;
638 SUPPORTED_PLATFORMS = iphoneos; 638 SUPPORTED_PLATFORMS = iphoneos;
@@ -13,8 +13,8 @@ class ScannedBarcodeLabel extends StatelessWidget { @@ -13,8 +13,8 @@ class ScannedBarcodeLabel extends StatelessWidget {
13 Widget build(BuildContext context) { 13 Widget build(BuildContext context) {
14 return StreamBuilder( 14 return StreamBuilder(
15 stream: barcodes, 15 stream: barcodes,
16 - builder: (context, snaphot) {  
17 - final scannedBarcodes = snaphot.data?.barcodes ?? []; 16 + builder: (context, snapshot) {
  17 + final scannedBarcodes = snapshot.data?.barcodes ?? [];
18 18
19 if (scannedBarcodes.isEmpty) { 19 if (scannedBarcodes.isEmpty) {
20 return const Text( 20 return const Text(
  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.0.0'
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', '~> 5.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
@@ -94,12 +94,29 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -94,12 +94,29 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
94 /// 94 ///
95 /// Throws a [MobileScannerException] if the permission is not granted. 95 /// Throws a [MobileScannerException] if the permission is not granted.
96 Future<void> _requestCameraPermission() async { 96 Future<void> _requestCameraPermission() async {
97 - final MobileScannerAuthorizationState authorizationState;  
98 -  
99 try { 97 try {
100 - authorizationState = MobileScannerAuthorizationState.fromRawValue( 98 + final MobileScannerAuthorizationState authorizationState =
  99 + MobileScannerAuthorizationState.fromRawValue(
101 await methodChannel.invokeMethod<int>('state') ?? 0, 100 await methodChannel.invokeMethod<int>('state') ?? 0,
102 ); 101 );
  102 +
  103 + switch (authorizationState) {
  104 + // Authorization was already granted, no need to request it again.
  105 + case MobileScannerAuthorizationState.authorized:
  106 + return;
  107 + // Android does not have an undetermined authorization state.
  108 + // So if the permission was denied, request it again.
  109 + case MobileScannerAuthorizationState.denied:
  110 + case MobileScannerAuthorizationState.undetermined:
  111 + final bool permissionGranted =
  112 + await methodChannel.invokeMethod<bool>('request') ?? false;
  113 +
  114 + if (!permissionGranted) {
  115 + throw const MobileScannerException(
  116 + errorCode: MobileScannerErrorCode.permissionDenied,
  117 + );
  118 + }
  119 + }
103 } on PlatformException catch (error) { 120 } on PlatformException catch (error) {
104 // If the permission state is invalid, that is an error. 121 // If the permission state is invalid, that is an error.
105 throw MobileScannerException( 122 throw MobileScannerException(
@@ -111,37 +128,6 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -111,37 +128,6 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
111 ), 128 ),
112 ); 129 );
113 } 130 }
114 -  
115 - switch (authorizationState) {  
116 - case MobileScannerAuthorizationState.authorized:  
117 - return; // Already authorized.  
118 - // Android does not have an undetermined authorization state.  
119 - // So if the permission was denied, request it again.  
120 - case MobileScannerAuthorizationState.denied:  
121 - case MobileScannerAuthorizationState.undetermined:  
122 - try {  
123 - final bool granted =  
124 - await methodChannel.invokeMethod<bool>('request') ?? false;  
125 -  
126 - if (granted) {  
127 - return; // Authorization was granted.  
128 - }  
129 -  
130 - throw const MobileScannerException(  
131 - errorCode: MobileScannerErrorCode.permissionDenied,  
132 - );  
133 - } on PlatformException catch (error) {  
134 - // If the permission state is invalid, that is an error.  
135 - throw MobileScannerException(  
136 - errorCode: MobileScannerErrorCode.genericError,  
137 - errorDetails: MobileScannerErrorDetails(  
138 - code: error.code,  
139 - details: error.details as Object?,  
140 - message: error.message,  
141 - ),  
142 - );  
143 - }  
144 - }  
145 } 131 }
146 132
147 @override 133 @override
@@ -223,12 +223,14 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -223,12 +223,14 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
223 } 223 }
224 224
225 /// Start scanning for barcodes. 225 /// Start scanning for barcodes.
226 - /// Upon calling this method, the necessary camera permission will be requested.  
227 /// 226 ///
228 /// The [cameraDirection] can be used to specify the camera direction. 227 /// The [cameraDirection] can be used to specify the camera direction.
229 /// If this is null, this defaults to the [facing] value. 228 /// If this is null, this defaults to the [facing] value.
230 /// 229 ///
231 /// Does nothing if the camera is already running. 230 /// Does nothing if the camera is already running.
  231 + /// Upon calling this method, the necessary camera permission will be requested.
  232 + ///
  233 + /// If the permission is denied on iOS, MacOS or Web, there is no way to request it again.
232 Future<void> start({CameraFacing? cameraDirection}) async { 234 Future<void> start({CameraFacing? cameraDirection}) async {
233 if (_isDisposed) { 235 if (_isDisposed) {
234 throw const MobileScannerException( 236 throw const MobileScannerException(
@@ -240,6 +242,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -240,6 +242,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
240 ); 242 );
241 } 243 }
242 244
  245 + // Permission was denied, do nothing.
  246 + // When the controller is stopped,
  247 + // the error is reset so the permission can be requested again if possible.
  248 + if (value.error?.errorCode == MobileScannerErrorCode.permissionDenied) {
  249 + return;
  250 + }
  251 +
243 // Do nothing if the camera is already running. 252 // Do nothing if the camera is already running.
244 if (value.isRunning) { 253 if (value.isRunning) {
245 return; 254 return;
@@ -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 }
@@ -9,25 +9,28 @@ final class MediaTrackConstraintsDelegate { @@ -9,25 +9,28 @@ final class MediaTrackConstraintsDelegate {
9 9
10 /// Get the settings for the given [mediaStream]. 10 /// Get the settings for the given [mediaStream].
11 MediaTrackSettings? getSettings(MediaStream? mediaStream) { 11 MediaTrackSettings? getSettings(MediaStream? mediaStream) {
12 - final List<JSAny?>? tracks = mediaStream?.getVideoTracks().toDart; 12 + final List<MediaStreamTrack>? tracks = mediaStream?.getVideoTracks().toDart;
13 13
14 if (tracks == null || tracks.isEmpty) { 14 if (tracks == null || tracks.isEmpty) {
15 return null; 15 return null;
16 } 16 }
17 17
18 - final MediaStreamTrack? track = tracks.first as MediaStreamTrack?;  
19 -  
20 - if (track == null) {  
21 - return null;  
22 - } 18 + final MediaStreamTrack track = tracks.first;
23 19
  20 + final MediaTrackCapabilities capabilities = track.getCapabilities();
24 final MediaTrackSettings settings = track.getSettings(); 21 final MediaTrackSettings settings = track.getSettings();
25 22
  23 + if (capabilities.facingMode.toDart.isEmpty) {
  24 + return MediaTrackSettings(
  25 + width: settings.width,
  26 + height: settings.height,
  27 + );
  28 + }
  29 +
26 return MediaTrackSettings( 30 return MediaTrackSettings(
27 width: settings.width, 31 width: settings.width,
28 height: settings.height, 32 height: settings.height,
29 facingMode: settings.facingMode, 33 facingMode: settings.facingMode,
30 - aspectRatio: settings.aspectRatio,  
31 ); 34 );
32 } 35 }
33 } 36 }
@@ -25,7 +25,7 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -25,7 +25,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
25 String? _alternateScriptUrl; 25 String? _alternateScriptUrl;
26 26
27 /// The internal barcode reader. 27 /// The internal barcode reader.
28 - final BarcodeReader _barcodeReader = ZXingBarcodeReader(); 28 + BarcodeReader? _barcodeReader;
29 29
30 /// The stream controller for the barcode stream. 30 /// The stream controller for the barcode stream.
31 final StreamController<BarcodeCapture> _barcodesController = 31 final StreamController<BarcodeCapture> _barcodesController =
@@ -35,21 +35,13 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -35,21 +35,13 @@ class MobileScannerWeb extends MobileScannerPlatform {
35 StreamSubscription<Object?>? _barcodesSubscription; 35 StreamSubscription<Object?>? _barcodesSubscription;
36 36
37 /// The container div element for the camera view. 37 /// The container div element for the camera view.
38 - ///  
39 - /// This container element is used by the barcode reader.  
40 - HTMLDivElement? _divElement; 38 + late HTMLDivElement _divElement;
41 39
42 - /// This [Completer] is used to prevent additional calls to the [start] method.  
43 - ///  
44 - /// To handle lifecycle changes properly,  
45 - /// the scanner is stopped when the application is inactive,  
46 - /// and restarted when the application gains focus. 40 + /// The flag that keeps track of whether a permission request is in progress.
47 /// 41 ///
48 - /// However, when the camera permission is requested,  
49 - /// the application is put in the inactive state due to the permission popup gaining focus.  
50 - /// Thus, as long as the permission status is not known,  
51 - /// any calls to the [start] method are ignored.  
52 - Completer<void>? _cameraPermissionCompleter; 42 + /// On the web, a permission request triggers a dialog, that in turn triggers a lifecycle change.
  43 + /// While the permission request is in progress, any attempts at (re)starting the camera should be ignored.
  44 + bool _permissionRequestInProgress = false;
53 45
54 /// The stream controller for the media track settings stream. 46 /// The stream controller for the media track settings stream.
55 /// 47 ///
@@ -60,18 +52,19 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -60,18 +52,19 @@ class MobileScannerWeb extends MobileScannerPlatform {
60 final StreamController<MediaTrackSettings> _settingsController = 52 final StreamController<MediaTrackSettings> _settingsController =
61 StreamController.broadcast(); 53 StreamController.broadcast();
62 54
63 - /// The view type for the platform view factory.  
64 - static const String _viewType = 'MobileScannerWeb'; 55 + /// The texture ID for the camera view.
  56 + int _textureId = 1;
  57 +
  58 + /// The video element for the camera view.
  59 + late HTMLVideoElement _videoElement;
  60 +
  61 + /// Get the view type for the platform view factory.
  62 + String _getViewType(int textureId) => 'mobile-scanner-view-$textureId';
65 63
66 static void registerWith(Registrar registrar) { 64 static void registerWith(Registrar registrar) {
67 MobileScannerPlatform.instance = MobileScannerWeb(); 65 MobileScannerPlatform.instance = MobileScannerWeb();
68 } 66 }
69 67
70 - bool get _hasPendingPermissionRequest {  
71 - return _cameraPermissionCompleter != null &&  
72 - !_cameraPermissionCompleter!.isCompleted;  
73 - }  
74 -  
75 @override 68 @override
76 Stream<BarcodeCapture?> get barcodesStream => _barcodesController.stream; 69 Stream<BarcodeCapture?> get barcodesStream => _barcodesController.stream;
77 70
@@ -83,6 +76,33 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -83,6 +76,33 @@ class MobileScannerWeb extends MobileScannerPlatform {
83 Stream<double> get zoomScaleStateStream => 76 Stream<double> get zoomScaleStateStream =>
84 _settingsController.stream.map((_) => 1.0); 77 _settingsController.stream.map((_) => 1.0);
85 78
  79 + /// Create the [HTMLVideoElement] along with its parent container [HTMLDivElement].
  80 + HTMLVideoElement _createVideoElement(int textureId) {
  81 + final HTMLVideoElement videoElement = HTMLVideoElement();
  82 +
  83 + videoElement.style
  84 + ..height = '100%'
  85 + ..width = '100%'
  86 + ..objectFit = 'cover'
  87 + ..transformOrigin = 'center'
  88 + ..pointerEvents = 'none';
  89 +
  90 + // Attach the video element to its parent container
  91 + // and setup the PlatformView factory for this `textureId`.
  92 + _divElement = HTMLDivElement()
  93 + ..style.objectFit = 'cover'
  94 + ..style.height = '100%'
  95 + ..style.width = '100%'
  96 + ..append(videoElement);
  97 +
  98 + ui_web.platformViewRegistry.registerViewFactory(
  99 + _getViewType(textureId),
  100 + (_) => _divElement,
  101 + );
  102 +
  103 + return videoElement;
  104 + }
  105 +
86 void _handleMediaTrackSettingsChange(MediaTrackSettings settings) { 106 void _handleMediaTrackSettingsChange(MediaTrackSettings settings) {
87 if (_settingsController.isClosed) { 107 if (_settingsController.isClosed) {
88 return; 108 return;
@@ -91,6 +111,32 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -91,6 +111,32 @@ class MobileScannerWeb extends MobileScannerPlatform {
91 _settingsController.add(settings); 111 _settingsController.add(settings);
92 } 112 }
93 113
  114 + /// Flip the [videoElement] horizontally,
  115 + /// if the [videoStream] indicates that is facing the user.
  116 + void _maybeFlipVideoPreview(
  117 + HTMLVideoElement videoElement,
  118 + MediaStream videoStream,
  119 + ) {
  120 + final List<MediaStreamTrack> tracks = videoStream.getVideoTracks().toDart;
  121 +
  122 + if (tracks.isEmpty) {
  123 + return;
  124 + }
  125 +
  126 + final MediaStreamTrack videoTrack = tracks.first;
  127 + final MediaTrackCapabilities capabilities = videoTrack.getCapabilities();
  128 +
  129 + // TODO: this is empty on MacOS, where there is no facing mode, but one, user facing camera.
  130 + // Facing mode is not supported by this track, do nothing.
  131 + if (capabilities.facingMode.toDart.isEmpty) {
  132 + return;
  133 + }
  134 +
  135 + if (videoTrack.getSettings().facingMode == 'user') {
  136 + videoElement.style.transform = 'scaleX(-1)';
  137 + }
  138 + }
  139 +
94 /// Prepare a [MediaStream] for the video output. 140 /// Prepare a [MediaStream] for the video output.
95 /// 141 ///
96 /// This method requests permission to use the camera. 142 /// This method requests permission to use the camera.
@@ -100,7 +146,7 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -100,7 +146,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
100 Future<MediaStream> _prepareVideoStream( 146 Future<MediaStream> _prepareVideoStream(
101 CameraFacing cameraDirection, 147 CameraFacing cameraDirection,
102 ) async { 148 ) async {
103 - if ((window.navigator.mediaDevices as JSAny?).isUndefinedOrNull) { 149 + if (window.navigator.mediaDevices.isUndefinedOrNull) {
104 throw const MobileScannerException( 150 throw const MobileScannerException(
105 errorCode: MobileScannerErrorCode.unsupported, 151 errorCode: MobileScannerErrorCode.unsupported,
106 errorDetails: MobileScannerErrorDetails( 152 errorDetails: MobileScannerErrorDetails(
@@ -115,7 +161,7 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -115,7 +161,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
115 161
116 final MediaStreamConstraints constraints; 162 final MediaStreamConstraints constraints;
117 163
118 - if ((capabilities as JSAny).isUndefinedOrNull || !capabilities.facingMode) { 164 + if (capabilities.isUndefinedOrNull || !capabilities.facingMode) {
119 constraints = MediaStreamConstraints(video: true.toJS); 165 constraints = MediaStreamConstraints(video: true.toJS);
120 } else { 166 } else {
121 final String facingMode = switch (cameraDirection) { 167 final String facingMode = switch (cameraDirection) {
@@ -124,43 +170,24 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -124,43 +170,24 @@ class MobileScannerWeb extends MobileScannerPlatform {
124 }; 170 };
125 171
126 constraints = MediaStreamConstraints( 172 constraints = MediaStreamConstraints(
127 - video: MediaTrackConstraintSet(facingMode: facingMode.toJS) as JSAny, 173 + video: MediaTrackConstraintSet(
  174 + facingMode: facingMode.toJS,
  175 + ),
128 ); 176 );
129 } 177 }
130 178
131 try { 179 try {
132 - // Retrieving the video track requests the camera permission.  
133 - // If the completer is not null, the permission was never requested before.  
134 - _cameraPermissionCompleter ??= Completer<void>(); 180 + // Retrieving the media devices requests the camera permission.
  181 + _permissionRequestInProgress = true;
135 182
136 - final MediaStream? videoStream = await window.navigator.mediaDevices  
137 - .getUserMedia(constraints)  
138 - .toDart as MediaStream?; 183 + final MediaStream videoStream =
  184 + await window.navigator.mediaDevices.getUserMedia(constraints).toDart;
139 185
140 // At this point the permission is granted. 186 // At this point the permission is granted.
141 - if (!_cameraPermissionCompleter!.isCompleted) {  
142 - _cameraPermissionCompleter!.complete();  
143 - }  
144 -  
145 - if (videoStream == null) {  
146 - throw const MobileScannerException(  
147 - errorCode: MobileScannerErrorCode.genericError,  
148 - errorDetails: MobileScannerErrorDetails(  
149 - message:  
150 - 'Could not create a video stream from the camera with the given options. '  
151 - 'The browser might not support the given constraints.',  
152 - ),  
153 - );  
154 - } 187 + _permissionRequestInProgress = false;
155 188
156 return videoStream; 189 return videoStream;
157 } on DOMException catch (error, stackTrace) { 190 } on DOMException catch (error, stackTrace) {
158 - // At this point the permission request completed, although with an error,  
159 - // but the error is irrelevant for the completer.  
160 - if (!_cameraPermissionCompleter!.isCompleted) {  
161 - _cameraPermissionCompleter!.complete();  
162 - }  
163 -  
164 final String errorMessage = error.toString(); 191 final String errorMessage = error.toString();
165 192
166 MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError; 193 MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;
@@ -173,6 +200,10 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -173,6 +200,10 @@ class MobileScannerWeb extends MobileScannerPlatform {
173 errorCode = MobileScannerErrorCode.permissionDenied; 200 errorCode = MobileScannerErrorCode.permissionDenied;
174 } 201 }
175 202
  203 + // At this point the permission request completed, although with an error,
  204 + // but the error is irrelevant.
  205 + _permissionRequestInProgress = false;
  206 +
176 throw MobileScannerException( 207 throw MobileScannerException(
177 errorCode: errorCode, 208 errorCode: errorCode,
178 errorDetails: MobileScannerErrorDetails( 209 errorDetails: MobileScannerErrorDetails(
@@ -190,11 +221,11 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -190,11 +221,11 @@ class MobileScannerWeb extends MobileScannerPlatform {
190 221
191 @override 222 @override
192 Widget buildCameraView() { 223 Widget buildCameraView() {
193 - if (!_barcodeReader.isScanning) {  
194 - return const SizedBox(); 224 + if (_barcodeReader?.isScanning ?? false) {
  225 + return HtmlElementView(viewType: _getViewType(_textureId));
195 } 226 }
196 227
197 - return const HtmlElementView(viewType: _viewType); 228 + return const SizedBox();
198 } 229 }
199 230
200 @override 231 @override
@@ -231,27 +262,17 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -231,27 +262,17 @@ class MobileScannerWeb extends MobileScannerPlatform {
231 // If the permission request has not yet completed, 262 // If the permission request has not yet completed,
232 // the camera view is not ready yet. 263 // the camera view is not ready yet.
233 // Prevent the permission popup from triggering a restart of the scanner. 264 // Prevent the permission popup from triggering a restart of the scanner.
234 - if (_hasPendingPermissionRequest) { 265 + if (_permissionRequestInProgress) {
235 throw PermissionRequestPendingException(); 266 throw PermissionRequestPendingException();
236 } 267 }
237 268
238 - await _barcodeReader.maybeLoadLibrary( 269 + _barcodeReader = ZXingBarcodeReader();
  270 +
  271 + await _barcodeReader?.maybeLoadLibrary(
239 alternateScriptUrl: _alternateScriptUrl, 272 alternateScriptUrl: _alternateScriptUrl,
240 ); 273 );
241 274
242 - // Setup the view factory & container element.  
243 - if (_divElement == null) {  
244 - _divElement = (document.createElement('div') as HTMLDivElement)  
245 - ..style.width = '100%'  
246 - ..style.height = '100%';  
247 -  
248 - ui_web.platformViewRegistry.registerViewFactory(  
249 - _viewType,  
250 - (int id) => _divElement!,  
251 - );  
252 - }  
253 -  
254 - if (_barcodeReader.isScanning) { 275 + if (_barcodeReader?.isScanning ?? false) {
255 throw const MobileScannerException( 276 throw const MobileScannerException(
256 errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, 277 errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
257 errorDetails: MobileScannerErrorDetails( 278 errorDetails: MobileScannerErrorDetails(
@@ -273,25 +294,19 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -273,25 +294,19 @@ class MobileScannerWeb extends MobileScannerPlatform {
273 } 294 }
274 295
275 // Listen for changes to the media track settings. 296 // Listen for changes to the media track settings.
276 - _barcodeReader.setMediaTrackSettingsListener( 297 + _barcodeReader?.setMediaTrackSettingsListener(
277 _handleMediaTrackSettingsChange, 298 _handleMediaTrackSettingsChange,
278 ); 299 );
279 300
280 - final HTMLVideoElement videoElement; 301 + _textureId += 1; // Request a new texture.
281 302
282 - // Attach the video element to the DOM, through its parent container.  
283 - // If a video element is already present, reuse it.  
284 - if (_divElement!.children.length == 0) {  
285 - videoElement = document.createElement('video') as HTMLVideoElement; 303 + _videoElement = _createVideoElement(_textureId);
286 304
287 - _divElement!.appendChild(videoElement);  
288 - } else {  
289 - videoElement = _divElement!.children.item(0)! as HTMLVideoElement;  
290 - } 305 + _maybeFlipVideoPreview(_videoElement, videoStream);
291 306
292 - await _barcodeReader.start( 307 + await _barcodeReader?.start(
293 startOptions, 308 startOptions,
294 - videoElement: videoElement, 309 + videoElement: _videoElement,
295 videoStream: videoStream, 310 videoStream: videoStream,
296 ); 311 );
297 } catch (error, stackTrace) { 312 } catch (error, stackTrace) {
@@ -305,7 +320,7 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -305,7 +320,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
305 } 320 }
306 321
307 try { 322 try {
308 - _barcodesSubscription = _barcodeReader.detectBarcodes().listen( 323 + _barcodesSubscription = _barcodeReader?.detectBarcodes().listen(
309 (BarcodeCapture barcode) { 324 (BarcodeCapture barcode) {
310 if (_barcodesController.isClosed) { 325 if (_barcodesController.isClosed) {
311 return; 326 return;
@@ -315,15 +330,15 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -315,15 +330,15 @@ class MobileScannerWeb extends MobileScannerPlatform {
315 }, 330 },
316 ); 331 );
317 332
318 - final bool hasTorch = await _barcodeReader.hasTorch(); 333 + final bool hasTorch = await _barcodeReader?.hasTorch() ?? false;
319 334
320 if (hasTorch && startOptions.torchEnabled) { 335 if (hasTorch && startOptions.torchEnabled) {
321 - await _barcodeReader.setTorchState(TorchState.on); 336 + await _barcodeReader?.setTorchState(TorchState.on);
322 } 337 }
323 338
324 return MobileScannerViewAttributes( 339 return MobileScannerViewAttributes(
325 hasTorch: hasTorch, 340 hasTorch: hasTorch,
326 - size: _barcodeReader.videoSize, 341 + size: _barcodeReader?.videoSize ?? Size.zero,
327 ); 342 );
328 } catch (error, stackTrace) { 343 } catch (error, stackTrace) {
329 throw MobileScannerException( 344 throw MobileScannerException(
@@ -338,15 +353,12 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -338,15 +353,12 @@ class MobileScannerWeb extends MobileScannerPlatform {
338 353
339 @override 354 @override
340 Future<void> stop() async { 355 Future<void> stop() async {
341 - if (_barcodesController.isClosed) {  
342 - return;  
343 - }  
344 -  
345 // Ensure the barcode scanner is stopped, by cancelling the subscription. 356 // Ensure the barcode scanner is stopped, by cancelling the subscription.
346 await _barcodesSubscription?.cancel(); 357 await _barcodesSubscription?.cancel();
347 _barcodesSubscription = null; 358 _barcodesSubscription = null;
348 359
349 - await _barcodeReader.stop(); 360 + await _barcodeReader?.stop();
  361 + _barcodeReader = null;
350 } 362 }
351 363
352 @override 364 @override
@@ -358,31 +370,8 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -358,31 +370,8 @@ class MobileScannerWeb extends MobileScannerPlatform {
358 370
359 @override 371 @override
360 Future<void> dispose() async { 372 Future<void> dispose() async {
361 - if (_barcodesController.isClosed) {  
362 - return;  
363 - }  
364 - 373 + // The `_barcodesController` and `_settingsController`
  374 + // are not closed, as these have the same lifetime as the plugin.
365 await stop(); 375 await stop();
366 - await _barcodesController.close();  
367 - await _settingsController.close();  
368 -  
369 - // Finally, remove the video element from the DOM.  
370 - try {  
371 - final HTMLCollection? divChildren = _divElement?.children;  
372 -  
373 - // Since the exact element is unknown, remove all children.  
374 - // In practice, there should only be one child, the single video element.  
375 - if (divChildren != null && divChildren.length > 0) {  
376 - for (int i = 0; i < divChildren.length; i++) {  
377 - final Node? child = divChildren.item(i);  
378 -  
379 - if (child != null) {  
380 - _divElement?.removeChild(child);  
381 - }  
382 - }  
383 - }  
384 - } catch (_) {  
385 - // The video element was no longer a child of the container element.  
386 - }  
387 } 376 }
388 } 377 }
@@ -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);
@@ -194,8 +155,32 @@ final class ZXingBarcodeReader extends BarcodeReader { @@ -194,8 +155,32 @@ final class ZXingBarcodeReader extends BarcodeReader {
194 @override 155 @override
195 Future<void> stop() async { 156 Future<void> stop() async {
196 _onMediaTrackSettingsChanged = null; 157 _onMediaTrackSettingsChanged = null;
197 - _reader?.stopContinuousDecode.callAsFunction(_reader as JSAny?);  
198 - _reader?.reset.callAsFunction(_reader as JSAny?); 158 + _reader?.stopContinuousDecode.callAsFunction(_reader);
  159 + _reader?.reset.callAsFunction(_reader);
199 _reader = null; 160 _reader = null;
200 } 161 }
201 } 162 }
  163 +
  164 +extension on BarcodeFormat {
  165 + /// Get the barcode format from the ZXing library.
  166 + JSNumber get toJS {
  167 + final int zxingFormat = switch (this) {
  168 + BarcodeFormat.aztec => 0,
  169 + BarcodeFormat.codabar => 1,
  170 + BarcodeFormat.code39 => 2,
  171 + BarcodeFormat.code93 => 3,
  172 + BarcodeFormat.code128 => 4,
  173 + BarcodeFormat.dataMatrix => 5,
  174 + BarcodeFormat.ean8 => 6,
  175 + BarcodeFormat.ean13 => 7,
  176 + BarcodeFormat.itf => 8,
  177 + BarcodeFormat.pdf417 => 10,
  178 + BarcodeFormat.qrCode => 11,
  179 + BarcodeFormat.upcA => 14,
  180 + BarcodeFormat.upcE => 15,
  181 + BarcodeFormat.unknown || BarcodeFormat.all || _ => -1,
  182 + };
  183 +
  184 + return zxingFormat.toJS;
  185 + }
  186 +}
@@ -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,
@@ -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.0.0'
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.1 3 +version: 5.0.0-beta.3
4 repository: https://github.com/juliansteenbakker/mobile_scanner 4 repository: https://github.com/juliansteenbakker/mobile_scanner
5 5
6 screenshots: 6 screenshots:
@@ -25,7 +25,7 @@ dependencies: @@ -25,7 +25,7 @@ dependencies:
25 flutter_web_plugins: 25 flutter_web_plugins:
26 sdk: flutter 26 sdk: flutter
27 plugin_platform_interface: ^2.0.2 27 plugin_platform_interface: ^2.0.2
28 - web: ^0.5.0 28 + web: ^0.5.1
29 29
30 dev_dependencies: 30 dev_dependencies:
31 flutter_test: 31 flutter_test: