From 8a50d72e1665c25abcc7f24fddaf2d346df90dca Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 3 Nov 2023 17:23:30 +0100 Subject: [PATCH 001/161] fix type of 'raw' attribute; remove width & height; update a comment --- CHANGELOG.md | 5 +++++ lib/src/mobile_scanner_controller.dart | 9 +++++++-- lib/src/objects/barcode.dart | 4 +++- lib/src/objects/barcode_capture.dart | 25 ++++++------------------- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40082b8e9..11d1ad5fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT +BREAKING CHANGES: +* The `width` and `height` of `BarcodeCapture` have been removed, in favor of `size`. +* The `raw` attribute is now `Object?` instead of `dynamic`, so that it participates in type promotion. + ## 4.0.1 Bugs fixed: * [iOS] Fixed a crash with a nil capture session when starting the camera. (thanks @navaronbracke !) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index bfd983964..ba3153ec6 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -434,13 +434,18 @@ class MobileScannerController { final parsed = (data as List) .map((value) => Barcode.fromNative(value as Map)) .toList(); + + final double? width = event['width'] as double?; + final double? height = event['height'] as double?; + _barcodesController.add( BarcodeCapture( raw: data, barcodes: parsed, image: event['image'] as Uint8List?, - width: event['width'] as double?, - height: event['height'] as double?, + size: width == null || height == null + ? Size.zero + : Size(width, height), ), ); case 'barcodeMac': diff --git a/lib/src/objects/barcode.dart b/lib/src/objects/barcode.dart index d5c6cf70c..fba2fe665 100644 --- a/lib/src/objects/barcode.dart +++ b/lib/src/objects/barcode.dart @@ -147,7 +147,9 @@ class Barcode { /// The SMS message that is embedded in the barcode. final SMS? sms; - /// The type of the [format] of the barcode. + /// The contextual type of the [format] of the barcode. + /// + /// For example: TYPE_TEXT, TYPE_PRODUCT, TYPE_URL, etc. /// /// For types that are recognized, /// but could not be parsed correctly, [BarcodeType.text] will be returned. diff --git a/lib/src/objects/barcode_capture.dart b/lib/src/objects/barcode_capture.dart index 95f32068c..9ae7a8820 100644 --- a/lib/src/objects/barcode_capture.dart +++ b/lib/src/objects/barcode_capture.dart @@ -6,38 +6,25 @@ import 'package:mobile_scanner/src/objects/barcode.dart'; /// This class represents a scanned barcode. class BarcodeCapture { /// Create a new [BarcodeCapture] instance. - BarcodeCapture({ + const BarcodeCapture({ this.barcodes = const [], - double? height, this.image, this.raw, - double? width, - }) : size = - width == null && height == null ? Size.zero : Size(width!, height!); + this.size = Size.zero, + }); /// The list of scanned barcodes. final List barcodes; /// The bytes of the image that is embedded in the barcode. /// - /// This null if [MobileScannerController.returnImage] is false. + /// This null if [MobileScannerController.returnImage] is false, + /// or if there is no available image. final Uint8List? image; /// The raw data of the scanned barcode. - final dynamic raw; // TODO: this should be `Object?` instead of dynamic + final Object? raw; /// The size of the scanned barcode. final Size size; - - /// The width of the scanned barcode. - /// - /// Prefer using `size.width` instead, - /// as this getter will be removed in the future. - double get width => size.width; - - /// The height of the scanned barcode. - /// - /// Prefer using `size.height` instead, - /// as this getter will be removed in the future. - double get height => size.height; } From 9d7cf2dbdbb2be51bc2592e2767c0a42ddb00b6f Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 3 Nov 2023 17:26:07 +0100 Subject: [PATCH 002/161] adjust example title --- example/lib/main.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 5c0fdf6e9..c0c1a7b51 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -8,7 +8,14 @@ import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; import 'package:mobile_scanner_example/barcode_scanner_zoom.dart'; import 'package:mobile_scanner_example/mobile_scanner_overlay.dart'; -void main() => runApp(const MaterialApp(home: MyHome())); +void main() { + runApp( + const MaterialApp( + title: 'Mobile Scanner Example', + home: MyHome(), + ), + ); +} class MyHome extends StatelessWidget { const MyHome({super.key}); @@ -16,7 +23,7 @@ class MyHome extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Flutter Demo Home Page')), + appBar: AppBar(title: const Text('Mobile Scanner Example')), body: SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, From fcc66d73a6939e1198400a927818481f2a820628 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 3 Nov 2023 17:42:47 +0100 Subject: [PATCH 003/161] add platform interface stub --- lib/mobile_scanner.dart | 1 + .../mobile_scanner_method_channel.dart | 14 +++++++++++ .../mobile_scanner_platform_interface.dart | 25 +++++++++++++++++++ pubspec.yaml | 1 + 4 files changed, 41 insertions(+) create mode 100644 lib/src/method_channel/mobile_scanner_method_channel.dart create mode 100644 lib/src/mobile_scanner_platform_interface.dart diff --git a/lib/mobile_scanner.dart b/lib/mobile_scanner.dart index 311a85010..087ac5027 100644 --- a/lib/mobile_scanner.dart +++ b/lib/mobile_scanner.dart @@ -12,6 +12,7 @@ export 'src/enums/torch_state.dart'; export 'src/mobile_scanner.dart'; export 'src/mobile_scanner_controller.dart'; export 'src/mobile_scanner_exception.dart'; +export 'src/mobile_scanner_platform_interface.dart'; export 'src/objects/address.dart'; export 'src/objects/barcode.dart'; export 'src/objects/barcode_capture.dart'; diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart new file mode 100644 index 000000000..d5e5e3516 --- /dev/null +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -0,0 +1,14 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart'; + +/// An implementation of [MobileScannerPlatform] that uses method channels. +class MethodChannelMobileScanner extends MobileScannerPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('dev.steenbakker.mobile_scanner/scanner/method'); + + /// The event channel that sends back scanned barcode events. + @visibleForTesting + final eventChannel = const EventChannel('dev.steenbakker.mobile_scanner/scanner/event'); +} diff --git a/lib/src/mobile_scanner_platform_interface.dart b/lib/src/mobile_scanner_platform_interface.dart new file mode 100644 index 000000000..c9bb185da --- /dev/null +++ b/lib/src/mobile_scanner_platform_interface.dart @@ -0,0 +1,25 @@ +import 'package:mobile_scanner/src/method_channel/mobile_scanner_method_channel.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +/// The platform interface for the `mobile_scanner` plugin. +abstract class MobileScannerPlatform extends PlatformInterface { + /// Constructs a MobileScannerPlatform. + MobileScannerPlatform() : super(token: _token); + + static final Object _token = Object(); + + static MobileScannerPlatform _instance = MethodChannelMobileScanner(); + + /// The default instance of [MobileScannerPlatform] to use. + /// + /// Defaults to [MethodChannelMobileScanner]. + static MobileScannerPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [MobileScannerPlatform] when + /// they register themselves. + static set instance(MobileScannerPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 61a3816b5..f778e4a1e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: flutter_web_plugins: sdk: flutter js: ">=0.6.3 <0.8.0" + plugin_platform_interface: ^2.0.2 dev_dependencies: flutter_test: From ac9da02bc5162e254c785490458ee5e2845e48ad Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 7 Nov 2023 12:59:51 +0100 Subject: [PATCH 004/161] define the platform interface --- .../mobile_scanner_platform_interface.dart | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/lib/src/mobile_scanner_platform_interface.dart b/lib/src/mobile_scanner_platform_interface.dart index c9bb185da..d06e5cd92 100644 --- a/lib/src/mobile_scanner_platform_interface.dart +++ b/lib/src/mobile_scanner_platform_interface.dart @@ -1,4 +1,7 @@ +import 'package:flutter/widgets.dart'; +import 'package:mobile_scanner/src/enums/camera_facing.dart'; import 'package:mobile_scanner/src/method_channel/mobile_scanner_method_channel.dart'; +import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; /// The platform interface for the `mobile_scanner` plugin. @@ -22,4 +25,70 @@ abstract class MobileScannerPlatform extends PlatformInterface { PlatformInterface.verifyToken(instance, _token); _instance = instance; } + + /// Analyze a local image file for barcodes. + /// + /// Returns whether the file at the given [path] contains a barcode. + Future analyzeImage(String path) { + throw UnimplementedError('analyzeImage() has not been implemented.'); + } + + /// Build the camera view for the barcode scanner. + Widget buildCameraView(covariant MobileScannerViewAttributes attributes) { + throw UnimplementedError('buildCameraView() has not been implemented.'); + } + + /// Dispose of this [MobileScannerPlatform] instance. + void dispose() { + throw UnimplementedError('dispose() has not been implemented.'); + } + + /// Reset the zoom scale, so that the camera is fully zoomed out. + Future resetZoomScale() { + throw UnimplementedError('resetZoomScale() has not been implemented.'); + } + + /// Set the zoom scale of the camera. + /// + /// The [zoomScale] must be between `0.0` and `1.0` (both inclusive). + /// A value of `0.0` indicates that the camera is fully zoomed out, + /// while `1.0` indicates that the camera is fully zoomed in. + Future setZoomScale(double zoomScale) { + throw UnimplementedError('setZoomScale() has not been implemented.'); + } + + /// Start scanning for barcodes. + /// + /// Upon calling this method, the necessary camera permission will be requested. + /// + /// Returns an instance of [MobileScannerViewAttributes]. + Future start({CameraFacing? cameraDirection}) { + throw UnimplementedError('start() has not been implemented.'); + } + + /// Stop the camera. + Future stop() { + throw UnimplementedError('stop() has not been implemented.'); + } + + /// Switch between the front and back camera. + Future switchCamera() { + throw UnimplementedError('switchCamera() has not been implemented.'); + } + + /// Switch the torch on or off. + /// + /// Does nothing if the device has no torch. + Future toggleTorch() { + throw UnimplementedError('toggleTorch() has not been implemented.'); + } + + /// Update the scan window to the given [window] rectangle. + /// + /// Any barcodes that do not intersect with the given [window] will be ignored. + /// + /// If [window] is `null`, the scan window will be reset to the full screen. + Future updateScanWindow(Rect? window) { + throw UnimplementedError('updateScanWindow() has not been implemented.'); + } } From 5948daed13022d2e4f879c3f774bd96b28f1b0f3 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 7 Nov 2023 15:22:25 +0100 Subject: [PATCH 005/161] replace mobile_scanner_arguments with a platform agnostic equivalent --- lib/mobile_scanner.dart | 1 - ...obile_scanner_texture_view_attributes.dart | 22 ++++++++++++++ lib/src/mobile_scanner_view_attributes.dart | 10 +++++++ lib/src/objects/mobile_scanner_arguments.dart | 30 ------------------- .../mobile_scanner_html_view_attributes.dart | 21 +++++++++++++ 5 files changed, 53 insertions(+), 31 deletions(-) create mode 100644 lib/src/method_channel/mobile_scanner_texture_view_attributes.dart create mode 100644 lib/src/mobile_scanner_view_attributes.dart delete mode 100644 lib/src/objects/mobile_scanner_arguments.dart create mode 100644 lib/src/web/mobile_scanner_html_view_attributes.dart diff --git a/lib/mobile_scanner.dart b/lib/mobile_scanner.dart index 087ac5027..0c3250cd7 100644 --- a/lib/mobile_scanner.dart +++ b/lib/mobile_scanner.dart @@ -21,7 +21,6 @@ export 'src/objects/contact_info.dart'; export 'src/objects/driver_license.dart'; export 'src/objects/email.dart'; export 'src/objects/geo_point.dart'; -export 'src/objects/mobile_scanner_arguments.dart'; export 'src/objects/person_name.dart'; export 'src/objects/phone.dart'; export 'src/objects/sms.dart'; diff --git a/lib/src/method_channel/mobile_scanner_texture_view_attributes.dart b/lib/src/method_channel/mobile_scanner_texture_view_attributes.dart new file mode 100644 index 000000000..a8df71444 --- /dev/null +++ b/lib/src/method_channel/mobile_scanner_texture_view_attributes.dart @@ -0,0 +1,22 @@ +import 'package:flutter/widgets.dart'; + +import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; + +/// An implementation for [MobileScannerViewAttributes] for platforms that provide a [Texture]. +class MobileScannerTextureViewAttributes + implements MobileScannerViewAttributes { + const MobileScannerTextureViewAttributes({ + required this.textureId, + required this.hasTorch, + required this.size, + }); + + /// The id of the [Texture]. + final int textureId; + + @override + final bool hasTorch; + + @override + final Size size; +} diff --git a/lib/src/mobile_scanner_view_attributes.dart b/lib/src/mobile_scanner_view_attributes.dart new file mode 100644 index 000000000..1271d94b7 --- /dev/null +++ b/lib/src/mobile_scanner_view_attributes.dart @@ -0,0 +1,10 @@ +import 'dart:ui'; + +/// This interface defines the attributes for the mobile scanner view. +abstract class MobileScannerViewAttributes { + /// Whether the current active camera has a torch. + bool get hasTorch; + + /// The size of the camera output. + Size get size; +} diff --git a/lib/src/objects/mobile_scanner_arguments.dart b/lib/src/objects/mobile_scanner_arguments.dart deleted file mode 100644 index 2f414ace3..000000000 --- a/lib/src/objects/mobile_scanner_arguments.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; - -/// The start arguments of the scanner. -class MobileScannerArguments { - /// The output size of the camera. - /// This value can be used to draw a box in the image. - final Size size; - - /// A bool which is true if the device has a torch. - final bool hasTorch; - - /// The texture id of the capture used internally. - final int? textureId; - - /// The texture id of the capture used internally if device is web. - final String? webId; - - /// Indicates how many cameras are available. - /// - /// Currently only supported on Android. - final int? numberOfCameras; - - MobileScannerArguments({ - required this.size, - required this.hasTorch, - this.textureId, - this.webId, - this.numberOfCameras, - }); -} diff --git a/lib/src/web/mobile_scanner_html_view_attributes.dart b/lib/src/web/mobile_scanner_html_view_attributes.dart new file mode 100644 index 000000000..ce3e4afd8 --- /dev/null +++ b/lib/src/web/mobile_scanner_html_view_attributes.dart @@ -0,0 +1,21 @@ +import 'package:flutter/widgets.dart'; + +import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; + +/// An implementation for [MobileScannerViewAttributes] for the web. +class MobileScannerHtmlViewAttributes implements MobileScannerViewAttributes { + const MobileScannerHtmlViewAttributes({ + required this.htmlElementViewType, + required this.hasTorch, + required this.size, + }); + + @override + final bool hasTorch; + + /// The [HtmlElementView.viewType] for the underlying [HtmlElementView]. + final String htmlElementViewType; + + @override + final Size size; +} From 369f44301873732cea85a05a19add868c3555842 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 7 Nov 2023 15:30:50 +0100 Subject: [PATCH 006/161] move dispose to the bottom --- lib/src/mobile_scanner_platform_interface.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/mobile_scanner_platform_interface.dart b/lib/src/mobile_scanner_platform_interface.dart index d06e5cd92..276c3f2e3 100644 --- a/lib/src/mobile_scanner_platform_interface.dart +++ b/lib/src/mobile_scanner_platform_interface.dart @@ -38,11 +38,6 @@ abstract class MobileScannerPlatform extends PlatformInterface { throw UnimplementedError('buildCameraView() has not been implemented.'); } - /// Dispose of this [MobileScannerPlatform] instance. - void dispose() { - throw UnimplementedError('dispose() has not been implemented.'); - } - /// Reset the zoom scale, so that the camera is fully zoomed out. Future resetZoomScale() { throw UnimplementedError('resetZoomScale() has not been implemented.'); @@ -91,4 +86,9 @@ abstract class MobileScannerPlatform extends PlatformInterface { Future updateScanWindow(Rect? window) { throw UnimplementedError('updateScanWindow() has not been implemented.'); } + + /// Dispose of this [MobileScannerPlatform] instance. + void dispose() { + throw UnimplementedError('dispose() has not been implemented.'); + } } From 445ca4c9b8d297f10da769611a83ddb31e6bc160 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 7 Nov 2023 15:43:18 +0100 Subject: [PATCH 007/161] make the start method return a Future in the platform interface; remove the view attributes from the buildCameraView() function --- lib/src/mobile_scanner_platform_interface.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/src/mobile_scanner_platform_interface.dart b/lib/src/mobile_scanner_platform_interface.dart index 276c3f2e3..d1322502c 100644 --- a/lib/src/mobile_scanner_platform_interface.dart +++ b/lib/src/mobile_scanner_platform_interface.dart @@ -1,7 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:mobile_scanner/src/enums/camera_facing.dart'; import 'package:mobile_scanner/src/method_channel/mobile_scanner_method_channel.dart'; -import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; /// The platform interface for the `mobile_scanner` plugin. @@ -34,7 +33,7 @@ abstract class MobileScannerPlatform extends PlatformInterface { } /// Build the camera view for the barcode scanner. - Widget buildCameraView(covariant MobileScannerViewAttributes attributes) { + Widget buildCameraView() { throw UnimplementedError('buildCameraView() has not been implemented.'); } @@ -52,12 +51,12 @@ abstract class MobileScannerPlatform extends PlatformInterface { throw UnimplementedError('setZoomScale() has not been implemented.'); } - /// Start scanning for barcodes. + /// Start the barcode scanner and prepare a scanner view. /// /// Upon calling this method, the necessary camera permission will be requested. /// - /// Returns an instance of [MobileScannerViewAttributes]. - Future start({CameraFacing? cameraDirection}) { + /// The given [cameraDirection] is used as the direction for the camera that needs to be set up. + Future start(CameraFacing cameraDirection) { throw UnimplementedError('start() has not been implemented.'); } From 10e8075a89cd595218104898c4204cac57a21178 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 7 Nov 2023 15:48:16 +0100 Subject: [PATCH 008/161] make dispose return a Future --- lib/src/mobile_scanner_platform_interface.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/mobile_scanner_platform_interface.dart b/lib/src/mobile_scanner_platform_interface.dart index d1322502c..cd9e1ecda 100644 --- a/lib/src/mobile_scanner_platform_interface.dart +++ b/lib/src/mobile_scanner_platform_interface.dart @@ -87,7 +87,7 @@ abstract class MobileScannerPlatform extends PlatformInterface { } /// Dispose of this [MobileScannerPlatform] instance. - void dispose() { + Future dispose() { throw UnimplementedError('dispose() has not been implemented.'); } } From 6059e752be5bbcc8c5abfe2343cb43290c81d5d1 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 7 Nov 2023 16:14:44 +0100 Subject: [PATCH 009/161] remove switchCamera() from the platform interface; update definiton of set torch state for the platform interface --- lib/src/mobile_scanner_platform_interface.dart | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/lib/src/mobile_scanner_platform_interface.dart b/lib/src/mobile_scanner_platform_interface.dart index cd9e1ecda..825e16750 100644 --- a/lib/src/mobile_scanner_platform_interface.dart +++ b/lib/src/mobile_scanner_platform_interface.dart @@ -1,5 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:mobile_scanner/src/enums/camera_facing.dart'; +import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/method_channel/mobile_scanner_method_channel.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; @@ -42,6 +43,11 @@ abstract class MobileScannerPlatform extends PlatformInterface { throw UnimplementedError('resetZoomScale() has not been implemented.'); } + /// Set the torch state of the active camera. + Future setTorchState(TorchState torchState) { + throw UnimplementedError('setTorchState() has not been implemented.'); + } + /// Set the zoom scale of the camera. /// /// The [zoomScale] must be between `0.0` and `1.0` (both inclusive). @@ -65,18 +71,6 @@ abstract class MobileScannerPlatform extends PlatformInterface { throw UnimplementedError('stop() has not been implemented.'); } - /// Switch between the front and back camera. - Future switchCamera() { - throw UnimplementedError('switchCamera() has not been implemented.'); - } - - /// Switch the torch on or off. - /// - /// Does nothing if the device has no torch. - Future toggleTorch() { - throw UnimplementedError('toggleTorch() has not been implemented.'); - } - /// Update the scan window to the given [window] rectangle. /// /// Any barcodes that do not intersect with the given [window] will be ignored. From 740001342a7212ca624182014dea23683ace9408 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 11:26:21 +0100 Subject: [PATCH 010/161] implement mobile scanner method channel --- .../mobile_scanner_method_channel.dart | 88 ++++++++++++++++++- 1 file changed, 85 insertions(+), 3 deletions(-) diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index d5e5e3516..6fa88bc02 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -1,14 +1,96 @@ -import 'package:flutter/foundation.dart'; +import 'dart:async'; + import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:mobile_scanner/src/enums/camera_facing.dart'; +import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart'; /// An implementation of [MobileScannerPlatform] that uses method channels. class MethodChannelMobileScanner extends MobileScannerPlatform { /// The method channel used to interact with the native platform. @visibleForTesting - final methodChannel = const MethodChannel('dev.steenbakker.mobile_scanner/scanner/method'); + final methodChannel = const MethodChannel( + 'dev.steenbakker.mobile_scanner/scanner/method', + ); /// The event channel that sends back scanned barcode events. @visibleForTesting - final eventChannel = const EventChannel('dev.steenbakker.mobile_scanner/scanner/event'); + final eventChannel = const EventChannel( + 'dev.steenbakker.mobile_scanner/scanner/event', + ); + + Stream>? _eventsStream; + + Stream> get eventsStream { + _eventsStream ??= + eventChannel.receiveBroadcastStream().cast>(); + + return _eventsStream!; + } + + int? _textureId; + + @override + Stream get torchStateStream { + return eventsStream + .where((event) => event['name'] == 'torchState') + .map((event) => TorchState.fromRawValue(event['data'] as int? ?? 0)); + } + + @override + Stream get zoomScaleStateStream { + return eventsStream + .where((event) => event['name'] == 'zoomScaleState') + .map((event) => event['data'] as double? ?? 0.0); + } + + @override + Future analyzeImage(String path) async { + final bool? result = await methodChannel.invokeMethod( + 'analyzeImage', + path, + ); + + return result ?? false; + } + + @override + Widget buildCameraView() => Texture(textureId: _textureId!); + + @override + Future resetZoomScale() async { + await methodChannel.invokeMethod('resetScale'); + } + + @override + Future setTorchState(TorchState torchState) async { + await methodChannel.invokeMethod('torch', torchState.rawValue); + } + + @override + Future setZoomScale(double zoomScale) async { + await methodChannel.invokeMethod('setScale', zoomScale); + } + + @override + Future start(CameraFacing cameraDirection) {} + + @override + Future stop() async { + await methodChannel.invokeMethod('stop'); + } + + @override + Future updateScanWindow(Rect? window) async { + await methodChannel.invokeMethod( + 'updateScanWindow', + {'rect': window}, + ); + } + + @override + Future dispose() async { + await stop(); + } } From c3ea41fa6fd047732b8416b70a44e6f053f08e9b Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 11:52:04 +0100 Subject: [PATCH 011/161] let analyzeImage return the barcode directly from native --- .../mobile_scanner/MobileScanner.kt | 12 +++++------ .../mobile_scanner/MobileScannerCallbacks.kt | 3 ++- .../mobile_scanner/MobileScannerHandler.kt | 20 +++++++++++-------- ios/Classes/MobileScannerPlugin.swift | 14 ++++++------- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt index cbd1b1127..7cd091132 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt @@ -78,7 +78,7 @@ class MobileScanner( scanner.process(inputImage) .addOnSuccessListener { barcodes -> if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) { - val newScannedBarcodes = barcodes.mapNotNull({ barcode -> barcode.rawValue }).sorted() + val newScannedBarcodes = barcodes.mapNotNull { barcode -> barcode.rawValue }.sorted() if (newScannedBarcodes == lastScanned) { // New scanned is duplicate, returning return@addOnSuccessListener @@ -424,7 +424,7 @@ class MobileScanner( /** * Analyze a single image. */ - fun analyzeImage(image: Uri, analyzerCallback: AnalyzerCallback) { + fun analyzeImage(image: Uri, onSuccess: AnalyzerSuccessCallback, onError: AnalyzerErrorCallback) { val inputImage = InputImage.fromFilePath(activity, image) scanner.process(inputImage) @@ -432,15 +432,13 @@ class MobileScanner( val barcodeMap = barcodes.map { barcode -> barcode.data } if (barcodeMap.isNotEmpty()) { - analyzerCallback(barcodeMap) + onSuccess(barcodeMap) } else { - analyzerCallback(null) + onSuccess(null) } } .addOnFailureListener { e -> - mobileScannerErrorCallback( - e.localizedMessage ?: e.toString() - ) + onError(e.localizedMessage ?: e.toString()) } } diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerCallbacks.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerCallbacks.kt index f8549b3a2..5732a28ed 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerCallbacks.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerCallbacks.kt @@ -3,7 +3,8 @@ package dev.steenbakker.mobile_scanner import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters typealias MobileScannerCallback = (barcodes: List>, image: ByteArray?, width: Int?, height: Int?) -> Unit -typealias AnalyzerCallback = (barcodes: List>?) -> Unit +typealias AnalyzerErrorCallback = (message: String) -> Unit +typealias AnalyzerSuccessCallback = (barcodes: List>?) -> Unit typealias MobileScannerErrorCallback = (error: String) -> Unit typealias TorchStateCallback = (state: Int) -> Unit typealias ZoomScaleStateCallback = (zoomScale: Double) -> Unit diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt index b23a2d200..99bd171c0 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt @@ -26,16 +26,19 @@ class MobileScannerHandler( private val addPermissionListener: (RequestPermissionsResultListener) -> Unit, textureRegistry: TextureRegistry): MethodChannel.MethodCallHandler { - private val analyzerCallback: AnalyzerCallback = { barcodes: List>?-> - if (barcodes != null) { - barcodeHandler.publishEvent(mapOf( - "name" to "barcode", - "data" to barcodes - )) + private val analyzeImageErrorCallback: AnalyzerErrorCallback = { + Handler(Looper.getMainLooper()).post { + analyzerResult?.error("MobileScanner", it, null) + analyzerResult = null } + } + private val analyzeImageSuccessCallback: AnalyzerSuccessCallback = { Handler(Looper.getMainLooper()).post { - analyzerResult?.success(barcodes != null) + analyzerResult?.success(mapOf( + "name" to "barcode", + "data" to it + )) analyzerResult = null } } @@ -236,7 +239,8 @@ class MobileScannerHandler( private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { analyzerResult = result val uri = Uri.fromFile(File(call.arguments.toString())) - mobileScanner!!.analyzeImage(uri, analyzerCallback) + + mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback) } private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) { diff --git a/ios/Classes/MobileScannerPlugin.swift b/ios/Classes/MobileScannerPlugin.swift index 8fab8366e..25eb1c24a 100644 --- a/ios/Classes/MobileScannerPlugin.swift +++ b/ios/Classes/MobileScannerPlugin.swift @@ -245,12 +245,12 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { return } - mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { [self] barcodes, error in + mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { barcodes, error in if error != nil { - barcodeHandler.publishEvent(["name": "error", "message": error?.localizedDescription]) - DispatchQueue.main.async { - result(false) + result(FlutterError(code: "MobileScanner", + message: error?.localizedDescription, + details: nil)) } return @@ -258,15 +258,13 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { if (barcodes == nil || barcodes!.isEmpty) { DispatchQueue.main.async { - result(false) + result(nil) } } else { let barcodesMap: [Any?] = barcodes!.compactMap { barcode in barcode.data } - let event: [String: Any?] = ["name": "barcode", "data": barcodesMap] - barcodeHandler.publishEvent(event) DispatchQueue.main.async { - result(true) + result(["name": "barcode", "data": barcodesMap]) } } }) From d410ef09ed2ee67e422a527193ee7a5ea9303db7 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 11:58:03 +0100 Subject: [PATCH 012/161] do not send barcodes on mac as "barcodeMac" --- macos/Classes/MobileScannerPlugin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Classes/MobileScannerPlugin.swift b/macos/Classes/MobileScannerPlugin.swift index 0b478be44..896dba474 100644 --- a/macos/Classes/MobileScannerPlugin.swift +++ b/macos/Classes/MobileScannerPlugin.swift @@ -138,7 +138,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, } DispatchQueue.main.async { - self?.sink?(["name": "barcodeMac", "data" : ["payload": barcode.payloadStringValue, "symbology": barcode.symbology.toInt as Any?]] as [String : Any]) + self?.sink?(["name": "barcode", "data" : ["payload": barcode.payloadStringValue, "symbology": barcode.symbology.toInt as Any?]] as [String : Any]) } // if barcodeType == "QR" { // let image = CIImage(image: source) From c2b57f25058f0080bc2879e90da3f456a9be4d72 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 12:40:51 +0100 Subject: [PATCH 013/161] let MacOS return a list of barcodes to align with iOS/Android --- macos/Classes/MobileScannerPlugin.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/macos/Classes/MobileScannerPlugin.swift b/macos/Classes/MobileScannerPlugin.swift index 896dba474..fcc6e36c1 100644 --- a/macos/Classes/MobileScannerPlugin.swift +++ b/macos/Classes/MobileScannerPlugin.swift @@ -138,7 +138,15 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, } DispatchQueue.main.async { - self?.sink?(["name": "barcode", "data" : ["payload": barcode.payloadStringValue, "symbology": barcode.symbology.toInt as Any?]] as [String : Any]) + self?.sink?([ + "name": "barcode", + "data": [ + [ + "payload": barcode.payloadStringValue ?? "", + "symbology": barcode.symbology.toInt ?? -1, + ], + ], + ]) } // if barcodeType == "QR" { // let image = CIImage(image: source) From 6fd59da44e6444dde02d469a92537a0c04e8798b Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 12:58:17 +0100 Subject: [PATCH 014/161] refactor analyze image in the platform interface --- .../mobile_scanner_method_channel.dart | 62 ++++++++++++++++++- .../mobile_scanner_platform_interface.dart | 7 ++- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index 6fa88bc02..e073efb45 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -1,10 +1,16 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'package:mobile_scanner/src/enums/barcode_format.dart'; import 'package:mobile_scanner/src/enums/camera_facing.dart'; +import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; import 'package:mobile_scanner/src/enums/torch_state.dart'; +import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart'; +import 'package:mobile_scanner/src/objects/barcode.dart'; +import 'package:mobile_scanner/src/objects/barcode_capture.dart'; /// An implementation of [MobileScannerPlatform] that uses method channels. class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -31,6 +37,56 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { int? _textureId; + /// Parse a [BarcodeCapture] from the given [event]. + BarcodeCapture? _parseBarcode(Map? event) { + if (event == null) { + return null; + } + + final Object? data = event['data']; + + if (data == null || data is! List) { + return null; + } + + final List> barcodes = data.cast>(); + + if (Platform.isMacOS) { + return BarcodeCapture( + raw: event, + barcodes: barcodes + .map( + (barcode) => Barcode( + rawValue: barcode['payload'] as String?, + format: BarcodeFormat.fromRawValue( + barcode['symbology'] as int? ?? -1, + ), + ), + ) + .toList(), + ); + } + + if (Platform.isAndroid || Platform.isIOS) { + final double? width = event['width'] as double?; + final double? height = event['height'] as double?; + + return BarcodeCapture( + raw: data, + barcodes: barcodes.map(Barcode.fromNative).toList(), + image: event['image'] as Uint8List?, + size: width == null || height == null ? Size.zero : Size(width, height), + ); + } + + throw const MobileScannerException( + errorCode: MobileScannerErrorCode.genericError, + errorDetails: MobileScannerErrorDetails( + message: 'Only Android, iOS and macOS are supported.', + ), + ); + } + @override Stream get torchStateStream { return eventsStream @@ -46,13 +102,13 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { } @override - Future analyzeImage(String path) async { - final bool? result = await methodChannel.invokeMethod( + Future analyzeImage(String path) async { + final Map? result = await methodChannel.invokeMapMethod( 'analyzeImage', path, ); - return result ?? false; + return _parseBarcode(result); } @override diff --git a/lib/src/mobile_scanner_platform_interface.dart b/lib/src/mobile_scanner_platform_interface.dart index 825e16750..eb97de6c9 100644 --- a/lib/src/mobile_scanner_platform_interface.dart +++ b/lib/src/mobile_scanner_platform_interface.dart @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mobile_scanner/src/enums/camera_facing.dart'; import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/method_channel/mobile_scanner_method_channel.dart'; +import 'package:mobile_scanner/src/objects/barcode_capture.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; /// The platform interface for the `mobile_scanner` plugin. @@ -28,8 +29,10 @@ abstract class MobileScannerPlatform extends PlatformInterface { /// Analyze a local image file for barcodes. /// - /// Returns whether the file at the given [path] contains a barcode. - Future analyzeImage(String path) { + /// The [path] is the path to the file on disk. + /// + /// Returns the barcodes that were found in the image. + Future analyzeImage(String path) { throw UnimplementedError('analyzeImage() has not been implemented.'); } From 85055590c90a96435807062f5c4e924c5fe338d9 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 13:13:06 +0100 Subject: [PATCH 015/161] add stream getters to platform interface --- lib/src/mobile_scanner_platform_interface.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/src/mobile_scanner_platform_interface.dart b/lib/src/mobile_scanner_platform_interface.dart index eb97de6c9..3c0d7689e 100644 --- a/lib/src/mobile_scanner_platform_interface.dart +++ b/lib/src/mobile_scanner_platform_interface.dart @@ -27,6 +27,21 @@ abstract class MobileScannerPlatform extends PlatformInterface { _instance = instance; } + /// Get the stream of barcode captures. + Stream get barcodesStream { + throw UnimplementedError('barcodesStream has not been implemented.'); + } + + /// Get the stream of torch state changes. + Stream get torchStateStream { + throw UnimplementedError('torchStateStream has not been implemented.'); + } + + /// Get the stream of zoom scale changes. + Stream get zoomScaleStateStream { + throw UnimplementedError('zoomScaleStateStream has not been implemented.'); + } + /// Analyze a local image file for barcodes. /// /// The [path] is the path to the file on disk. From eaf9c70dc3d447bbb02f7d4107fb316ab8de734f Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 13:13:54 +0100 Subject: [PATCH 016/161] implement barcodes stream for native --- lib/src/method_channel/mobile_scanner_method_channel.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index e073efb45..6d45a3d63 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -87,6 +87,11 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { ); } + @override + Stream get barcodesStream { + return eventsStream.where((event) => event['name'] == 'barcode').map((event) => _parseBarcode(event)); + } + @override Stream get torchStateStream { return eventsStream From 83a8db5b3424865458806061f9f8e1bbfed7db21 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 14:13:37 +0100 Subject: [PATCH 017/161] implement request permission for native --- .../mobile_scanner_method_channel.dart | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index 6d45a3d63..5ebfc08d7 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -87,6 +87,60 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { ); } + /// Request permission to access the camera. + /// + /// Throws a [MobileScannerException] if the permission is not granted. + Future _requestCameraPermission() async { + final MobileScannerState authorizationState; + + try { + authorizationState = MobileScannerState.fromRawValue( + await methodChannel.invokeMethod('state') ?? 0, + ); + } on PlatformException catch (error) { + // If the permission state is invalid, that is an error. + throw MobileScannerException( + errorCode: MobileScannerErrorCode.genericError, + errorDetails: MobileScannerErrorDetails( + code: error.code, + details: error.details as Object?, + message: error.message, + ), + ); + } + + switch (authorizationState) { + case MobileScannerState.denied: + throw const MobileScannerException( + errorCode: MobileScannerErrorCode.permissionDenied, + ); + case MobileScannerState.authorized: + return; // Already authorized. + case MobileScannerState.undetermined: + try { + final bool permissionResult = await methodChannel.invokeMethod('request') ?? false; + + if (permissionResult) { + return; // Authorization was granted. + } + + throw const MobileScannerException( + errorCode: MobileScannerErrorCode.permissionDenied, + ); + } on PlatformException catch (error) { + // If the permission state is invalid, that is an error. + throw MobileScannerException( + errorCode: MobileScannerErrorCode.genericError, + errorDetails: MobileScannerErrorDetails( + code: error.code, + details: error.details as Object?, + message: error.message, + ), + ); + } + } + } + @override Stream get barcodesStream { return eventsStream.where((event) => event['name'] == 'barcode').map((event) => _parseBarcode(event)); From 53ba133cb2a239c34606f49dff3e906e31751826 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 14:41:32 +0100 Subject: [PATCH 018/161] revert the addition of platform specific view attributes --- ...obile_scanner_texture_view_attributes.dart | 22 ------------------- lib/src/mobile_scanner_view_attributes.dart | 13 +++++++---- .../mobile_scanner_html_view_attributes.dart | 21 ------------------ 3 files changed, 9 insertions(+), 47 deletions(-) delete mode 100644 lib/src/method_channel/mobile_scanner_texture_view_attributes.dart delete mode 100644 lib/src/web/mobile_scanner_html_view_attributes.dart diff --git a/lib/src/method_channel/mobile_scanner_texture_view_attributes.dart b/lib/src/method_channel/mobile_scanner_texture_view_attributes.dart deleted file mode 100644 index a8df71444..000000000 --- a/lib/src/method_channel/mobile_scanner_texture_view_attributes.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter/widgets.dart'; - -import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; - -/// An implementation for [MobileScannerViewAttributes] for platforms that provide a [Texture]. -class MobileScannerTextureViewAttributes - implements MobileScannerViewAttributes { - const MobileScannerTextureViewAttributes({ - required this.textureId, - required this.hasTorch, - required this.size, - }); - - /// The id of the [Texture]. - final int textureId; - - @override - final bool hasTorch; - - @override - final Size size; -} diff --git a/lib/src/mobile_scanner_view_attributes.dart b/lib/src/mobile_scanner_view_attributes.dart index 1271d94b7..d7ec62be7 100644 --- a/lib/src/mobile_scanner_view_attributes.dart +++ b/lib/src/mobile_scanner_view_attributes.dart @@ -1,10 +1,15 @@ import 'dart:ui'; -/// This interface defines the attributes for the mobile scanner view. -abstract class MobileScannerViewAttributes { +/// This class defines the attributes for the mobile scanner view. +class MobileScannerViewAttributes { + const MobileScannerViewAttributes({ + required this.hasTorch, + required this.size, + }); + /// Whether the current active camera has a torch. - bool get hasTorch; + final bool hasTorch; /// The size of the camera output. - Size get size; + final Size size; } diff --git a/lib/src/web/mobile_scanner_html_view_attributes.dart b/lib/src/web/mobile_scanner_html_view_attributes.dart deleted file mode 100644 index ce3e4afd8..000000000 --- a/lib/src/web/mobile_scanner_html_view_attributes.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/widgets.dart'; - -import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; - -/// An implementation for [MobileScannerViewAttributes] for the web. -class MobileScannerHtmlViewAttributes implements MobileScannerViewAttributes { - const MobileScannerHtmlViewAttributes({ - required this.htmlElementViewType, - required this.hasTorch, - required this.size, - }); - - @override - final bool hasTorch; - - /// The [HtmlElementView.viewType] for the underlying [HtmlElementView]. - final String htmlElementViewType; - - @override - final Size size; -} From b9b8d7c26f25390e58dd62e9d2ac89386fd4294e Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 14:43:39 +0100 Subject: [PATCH 019/161] let start return the view attributes --- lib/src/mobile_scanner_platform_interface.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/mobile_scanner_platform_interface.dart b/lib/src/mobile_scanner_platform_interface.dart index 3c0d7689e..ef5e1ee81 100644 --- a/lib/src/mobile_scanner_platform_interface.dart +++ b/lib/src/mobile_scanner_platform_interface.dart @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mobile_scanner/src/enums/camera_facing.dart'; import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/method_channel/mobile_scanner_method_channel.dart'; +import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; import 'package:mobile_scanner/src/objects/barcode_capture.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; @@ -80,7 +81,7 @@ abstract class MobileScannerPlatform extends PlatformInterface { /// Upon calling this method, the necessary camera permission will be requested. /// /// The given [cameraDirection] is used as the direction for the camera that needs to be set up. - Future start(CameraFacing cameraDirection) { + Future start(CameraFacing cameraDirection) { throw UnimplementedError('start() has not been implemented.'); } From 9dda3bac72f07337e5dc5fb4b2ca28865c927cf9 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 14:44:18 +0100 Subject: [PATCH 020/161] add null check for texture id --- .../mobile_scanner_method_channel.dart | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index 5ebfc08d7..4bb2fa7b3 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -171,7 +171,18 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { } @override - Widget buildCameraView() => Texture(textureId: _textureId!); + Widget buildCameraView() { + if (_textureId == null) { + throw const MobileScannerException( + errorCode: MobileScannerErrorCode.controllerUninitialized, + errorDetails: MobileScannerErrorDetails( + message: 'The controller was not yet initialized. Call start() before calling buildCameraView().', + ), + ); + } + + return Texture(textureId: _textureId!); + } @override Future resetZoomScale() async { From 4d4cf733525ead473cc28f7e3ca62af81e33f8d4 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 14:45:42 +0100 Subject: [PATCH 021/161] add controller already initialized error code --- lib/src/enums/mobile_scanner_error_code.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/src/enums/mobile_scanner_error_code.dart b/lib/src/enums/mobile_scanner_error_code.dart index 5e0666678..382b18f9b 100644 --- a/lib/src/enums/mobile_scanner_error_code.dart +++ b/lib/src/enums/mobile_scanner_error_code.dart @@ -1,5 +1,13 @@ +import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; + /// This enum defines the different error codes for the mobile scanner. enum MobileScannerErrorCode { + /// The controller was already started. + /// + /// The controller should be stopped using [MobileScannerController.stop], + /// before restarting it. + controllerAlreadyInitialized, + /// The controller was used /// while it was not yet initialized using [MobileScannerController.start]. controllerUninitialized, From 1feb506cb30c7c8b5177ace77e3148945c8ca1b4 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 15:00:15 +0100 Subject: [PATCH 022/161] create a new start options class --- lib/src/objects/start_options.dart | 56 ++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 lib/src/objects/start_options.dart diff --git a/lib/src/objects/start_options.dart b/lib/src/objects/start_options.dart new file mode 100644 index 000000000..71f368d69 --- /dev/null +++ b/lib/src/objects/start_options.dart @@ -0,0 +1,56 @@ +import 'dart:ui'; + +import 'package:mobile_scanner/src/enums/barcode_format.dart'; +import 'package:mobile_scanner/src/enums/camera_facing.dart'; +import 'package:mobile_scanner/src/enums/detection_speed.dart'; + +/// This class defines the different start options for the mobile scanner. +class StartOptions { + const StartOptions({ + required this.cameraDirection, + required this.cameraResolution, + required this.detectionSpeed, + required this.detectionTimeoutMs, + required this.formats, + required this.returnImage, + required this.torchEnabled, + }); + + /// The direction for the camera. + final CameraFacing cameraDirection; + + /// The desired camera resolution for the scanner. + final Size? cameraResolution; + + /// The detection speed for the scanner. + final DetectionSpeed detectionSpeed; + + /// The detection timeout for the scanner, in milliseconds. + final int detectionTimeoutMs; + + /// The barcode formats to detect. + final List formats; + + /// Whether the detected barcodes should provide their image data. + final bool returnImage; + + /// Whether the torch should be turned on when the scanner starts. + final bool torchEnabled; + + Map toMap() { + return { + if (cameraResolution != null) + 'cameraResolution': [ + cameraResolution!.width.toInt(), + cameraResolution!.height.toInt(), + ], + 'facing': cameraDirection.rawValue, + if (formats.isNotEmpty) + 'formats': formats.map((f) => f.rawValue).toList(), + 'returnImage': returnImage, + 'speed': detectionSpeed.rawValue, + 'timeout': detectionTimeoutMs, + 'torch': torchEnabled, + }; + } +} From aafa235bbc8fa72fe21de36c7d96d6ea13436895 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 15:02:56 +0100 Subject: [PATCH 023/161] replace camera direction with start options --- lib/src/mobile_scanner_platform_interface.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/mobile_scanner_platform_interface.dart b/lib/src/mobile_scanner_platform_interface.dart index ef5e1ee81..863ff0207 100644 --- a/lib/src/mobile_scanner_platform_interface.dart +++ b/lib/src/mobile_scanner_platform_interface.dart @@ -1,9 +1,9 @@ import 'package:flutter/widgets.dart'; -import 'package:mobile_scanner/src/enums/camera_facing.dart'; import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/method_channel/mobile_scanner_method_channel.dart'; import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; import 'package:mobile_scanner/src/objects/barcode_capture.dart'; +import 'package:mobile_scanner/src/objects/start_options.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; /// The platform interface for the `mobile_scanner` plugin. @@ -81,7 +81,7 @@ abstract class MobileScannerPlatform extends PlatformInterface { /// Upon calling this method, the necessary camera permission will be requested. /// /// The given [cameraDirection] is used as the direction for the camera that needs to be set up. - Future start(CameraFacing cameraDirection) { + Future start(StartOptions startOptions) { throw UnimplementedError('start() has not been implemented.'); } From 6d699aedcf745719736dcbb470571c7eb1eeb059 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 15:06:39 +0100 Subject: [PATCH 024/161] set texture id to null in stop() --- lib/src/method_channel/mobile_scanner_method_channel.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index 4bb2fa7b3..fad744e33 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -205,6 +205,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @override Future stop() async { await methodChannel.invokeMethod('stop'); + + _textureId = null; } @override From 0bf66ee36094793c57d1f19daa63064944291b00 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 15:07:06 +0100 Subject: [PATCH 025/161] implement start() for native --- .../mobile_scanner_method_channel.dart | 76 ++++++++++++++++++- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index fad744e33..3bc35ecaa 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -4,13 +4,15 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:mobile_scanner/src/enums/barcode_format.dart'; -import 'package:mobile_scanner/src/enums/camera_facing.dart'; import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; +import 'package:mobile_scanner/src/enums/mobile_scanner_state.dart'; import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart'; +import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; import 'package:mobile_scanner/src/objects/barcode.dart'; import 'package:mobile_scanner/src/objects/barcode_capture.dart'; +import 'package:mobile_scanner/src/objects/start_options.dart'; /// An implementation of [MobileScannerPlatform] that uses method channels. class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -29,8 +31,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { Stream>? _eventsStream; Stream> get eventsStream { - _eventsStream ??= - eventChannel.receiveBroadcastStream().cast>(); + _eventsStream ??= eventChannel.receiveBroadcastStream().cast>(); return _eventsStream!; } @@ -200,7 +201,74 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { } @override - Future start(CameraFacing cameraDirection) {} + Future start(StartOptions startOptions) async { + if (_textureId != null) { + throw const MobileScannerException( + errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, + errorDetails: MobileScannerErrorDetails( + message: 'The scanner was already started. Call stop() before calling start() again.', + ), + ); + } + + await _requestCameraPermission(); + + Map? startResult; + + try { + startResult = await methodChannel.invokeMapMethod( + 'start', + startOptions.toMap(), + ); + } on PlatformException catch (error) { + throw MobileScannerException( + errorCode: MobileScannerErrorCode.genericError, + errorDetails: MobileScannerErrorDetails( + code: error.code, + details: error.details as Object?, + message: error.message, + ), + ); + } + + if (startResult == null) { + throw const MobileScannerException( + errorCode: MobileScannerErrorCode.genericError, + errorDetails: MobileScannerErrorDetails( + message: 'The start method did not return a view configuration.', + ), + ); + } + + final int? textureId = startResult['textureId'] as int?; + + if (textureId == null) { + throw const MobileScannerException( + errorCode: MobileScannerErrorCode.genericError, + errorDetails: MobileScannerErrorDetails( + message: 'The start method did not return a texture id.', + ), + ); + } + + _textureId = textureId; + + final bool hasTorch = startResult['torchable'] as bool? ?? false; + + final Map? sizeInfo = startResult['size'] as Map?; + final double? width = sizeInfo?['width'] as double?; + final double? height = sizeInfo?['height'] as double?; + + final Size size; + + if (width == null || height == null) { + size = Size.zero; + } else { + size = Size(width, height); + } + + return MobileScannerViewAttributes(hasTorch: hasTorch, size: size); + } @override Future stop() async { From db0345769ca858d4ee07021fa722ddabcd338c91 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 15:17:39 +0100 Subject: [PATCH 026/161] add unavailable torch state --- lib/src/enums/torch_state.dart | 7 ++++++- test/enums/torch_state_test.dart | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/src/enums/torch_state.dart b/lib/src/enums/torch_state.dart index 8d99baa96..4a6d9b1c5 100644 --- a/lib/src/enums/torch_state.dart +++ b/lib/src/enums/torch_state.dart @@ -4,7 +4,10 @@ enum TorchState { off(0), /// The flashlight is on. - on(1); + on(1), + + /// The flashlight is unavailable. + unavailable(2); const TorchState(this.rawValue); @@ -14,6 +17,8 @@ enum TorchState { return TorchState.off; case 1: return TorchState.on; + case 2: + return TorchState.unavailable; default: throw ArgumentError.value(value, 'value', 'Invalid raw value.'); } diff --git a/test/enums/torch_state_test.dart b/test/enums/torch_state_test.dart index 10ddeda0b..ad528832e 100644 --- a/test/enums/torch_state_test.dart +++ b/test/enums/torch_state_test.dart @@ -7,6 +7,7 @@ void main() { const values = { 0: TorchState.off, 1: TorchState.on, + 2: TorchState.unavailable, }; for (final MapEntry entry in values.entries) { @@ -18,7 +19,7 @@ void main() { test('invalid raw value throws argument error', () { const int negative = -1; - const int outOfRange = 2; + const int outOfRange = 3; expect(() => TorchState.fromRawValue(negative), throwsArgumentError); expect(() => TorchState.fromRawValue(outOfRange), throwsArgumentError); @@ -28,6 +29,7 @@ void main() { const values = { TorchState.off: 0, TorchState.on: 1, + TorchState.unavailable: 2, }; for (final MapEntry entry in values.entries) { From 8c72717bd9bf52097ea1c7bff5950ef15783ea2e Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 15:18:14 +0100 Subject: [PATCH 027/161] do nothing in setTorchState if torch is not available --- lib/src/method_channel/mobile_scanner_method_channel.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index 3bc35ecaa..609045fd1 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -192,6 +192,10 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @override Future setTorchState(TorchState torchState) async { + if (torchState == TorchState.unavailable) { + return; + } + await methodChannel.invokeMethod('torch', torchState.rawValue); } From bf320d595a34d7335ba0b80c8ac364fd4fc44782 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 16:51:05 +0100 Subject: [PATCH 028/161] reimplement parts of the MobileScannerController using the platform interface --- lib/src/mobile_scanner_controller.dart | 526 +++++-------------------- 1 file changed, 97 insertions(+), 429 deletions(-) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index ba3153ec6..7cd0b4aee 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -1,7 +1,4 @@ import 'dart:async'; -import 'dart:io'; -// ignore: unnecessary_import -import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -12,53 +9,45 @@ import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; import 'package:mobile_scanner/src/enums/mobile_scanner_state.dart'; import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; -import 'package:mobile_scanner/src/objects/barcode.dart'; +import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart'; import 'package:mobile_scanner/src/objects/barcode_capture.dart'; -import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart'; -/// The [MobileScannerController] holds all the logic of this plugin, -/// where as the [MobileScanner] class is the frontend of this plugin. -class MobileScannerController { +/// The controller for the [MobileScanner] widget. +class MobileScannerController extends ValueNotifier { + /// Construct a new [MobileScannerController] instance. MobileScannerController({ - this.facing = CameraFacing.back, + this.cameraResolution, this.detectionSpeed = DetectionSpeed.normal, - this.detectionTimeoutMs = 250, - this.torchEnabled = false, - this.formats, + int detectionTimeoutMs = 250, + this.facing = CameraFacing.back, + this.formats = const [], this.returnImage = false, - @Deprecated( - 'Instead, use the result of calling `start()` to determine if permissions were granted.', - ) - this.onPermissionSet, - this.autoStart = true, - this.cameraResolution, + this.torchEnabled = false, this.useNewCameraSelector = false, - }); + }) : detectionTimeoutMs = detectionSpeed == DetectionSpeed.normal ? detectionTimeoutMs : 0, + assert(detectionTimeoutMs >= 0, 'The detection timeout must be greater than or equal to 0.'), + super(MobileScannerState.uninitialized(facing)); - /// Select which camera should be used. + /// The desired resolution for the camera. /// - /// Default: CameraFacing.back - final CameraFacing facing; - - /// Enable or disable the torch (Flash) on start + /// When this value is provided, the camera will try to match this resolution, + /// or fallback to the closest available resolution. + /// When this is null, Android defaults to a resolution of 640x480. /// - /// Default: disabled - final bool torchEnabled; - - /// Set to true if you want to return the image buffer with the Barcode event + /// Bear in mind that changing the resolution has an effect on the aspect ratio. /// - /// Only supported on iOS and Android - final bool returnImage; - - /// If provided, the scanner will only detect those specific formats - final List? formats; + /// When the camera orientation changes, + /// the resolution will be flipped to match the new dimensions of the display. + /// + /// Currently only supported on Android. + final Size? cameraResolution; - /// Sets the speed of detections. + /// The detection speed for the scanner. /// - /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices + /// Defaults to [DetectionSpeed.normal]. final DetectionSpeed detectionSpeed; - /// Sets the timeout, in milliseconds, of the scanner. + /// The detection timeout, in milliseconds, for the scanner. /// /// This timeout is ignored if the [detectionSpeed] /// is not set to [DetectionSpeed.normal]. @@ -67,444 +56,123 @@ class MobileScannerController { /// which prevents memory issues on older devices. final int detectionTimeoutMs; - /// Automatically start the mobileScanner on initialization. - final bool autoStart; + /// The facing direction for the camera. + /// + /// Defaults to the back-facing camera. + final CameraFacing facing; - /// The desired resolution for the camera. + /// The formats that the scanner should detect. /// - /// When this value is provided, the camera will try to match this resolution, - /// or fallback to the closest available resolution. - /// When this is null, Android defaults to a resolution of 640x480. + /// If this is empty, all supported formats are detected. + final List formats; + + /// Whether scanned barcodes should contain the image + /// that is embedded into the barcode. /// - /// Bear in mind that changing the resolution has an effect on the aspect ratio. + /// If this is false, [BarcodeCapture.image] will always be null. /// - /// When the camera orientation changes, - /// the resolution will be flipped to match the new dimensions of the display. + /// Defaults to false, and is only supported on iOS and Android. + final bool returnImage; + + /// Whether the flashlight should be turned on when the camera is started. /// - /// Currently only supported on Android. - final Size? cameraResolution; + /// Defaults to false. + final bool torchEnabled; - /// Use the new resolution selector. Warning: not fully tested, may produce - /// unwanted/zoomed images. + /// Use the new resolution selector. + /// + /// This feature is experimental and not fully tested yet. + /// Use caution when using this flag, + /// as the new resolution selector may produce unwanted or zoomed images. /// - /// Only supported on Android + /// Only supported on Android. final bool useNewCameraSelector; - /// Sets the barcode stream - final StreamController _barcodesController = - StreamController.broadcast(); + /// The internal barcode controller, that listens for detected barcodes. + final StreamController _barcodesController = StreamController.broadcast(); + /// Get the stream of scanned barcodes. Stream get barcodes => _barcodesController.stream; - static const MethodChannel _methodChannel = - MethodChannel('dev.steenbakker.mobile_scanner/scanner/method'); - static const EventChannel _eventChannel = - EventChannel('dev.steenbakker.mobile_scanner/scanner/event'); - - @Deprecated( - 'Instead, use the result of calling `start()` to determine if permissions were granted.', - ) - Function(bool permissionGranted)? onPermissionSet; - - /// Listen to events from the platform specific code - StreamSubscription? events; - - /// A notifier that provides several arguments about the MobileScanner - final ValueNotifier startArguments = - ValueNotifier(null); - - /// A notifier that provides the state of the Torch (Flash) - final ValueNotifier torchState = ValueNotifier(TorchState.off); - - /// A notifier that provides the state of which camera is being used - late final ValueNotifier cameraFacingState = - ValueNotifier(facing); - - /// A notifier that provides zoomScale. - final ValueNotifier zoomScaleState = ValueNotifier(0.0); - - bool isStarting = false; - - /// A notifier that provides availability of the Torch (Flash) - final ValueNotifier hasTorchState = ValueNotifier(false); - - /// Returns whether the device has a torch. + /// Analyze an image file. /// - /// Throws an error if the controller is not initialized. - bool get hasTorch { - final hasTorch = hasTorchState.value; - if (hasTorch == null) { - throw const MobileScannerException( - errorCode: MobileScannerErrorCode.controllerUninitialized, - ); - } - - return hasTorch; + /// The [path] points to a file on the device. + /// + /// This is only supported on Android and iOS. + /// + /// Returns the [BarcodeCapture] that was found in the image. + Future analyzeImage(String path) { + return MobileScannerPlatform.instance.analyzeImage(path); } - /// Set the starting arguments for the camera - Map _argumentsToMap({CameraFacing? cameraFacingOverride}) { - final Map arguments = {}; - - cameraFacingState.value = cameraFacingOverride ?? facing; - arguments['facing'] = cameraFacingState.value.rawValue; - arguments['torch'] = torchEnabled; - arguments['speed'] = detectionSpeed.rawValue; - arguments['timeout'] = detectionTimeoutMs; - arguments['returnImage'] = returnImage; - arguments['useNewCameraSelector'] = useNewCameraSelector; - - /* if (scanWindow != null) { - arguments['scanWindow'] = [ - scanWindow!.left, - scanWindow!.top, - scanWindow!.right, - scanWindow!.bottom, - ]; - } */ - - if (formats != null) { - if (kIsWeb || Platform.isIOS || Platform.isMacOS || Platform.isAndroid) { - arguments['formats'] = formats!.map((e) => e.rawValue).toList(); - } - } - - if (cameraResolution != null) { - arguments['cameraResolution'] = [ - cameraResolution!.width.toInt(), - cameraResolution!.height.toInt(), - ]; - } - - return arguments; + /// Reset the zoom scale of the camera. + Future resetZoomScale() async { + await MobileScannerPlatform.instance.resetZoomScale(); } - /// Start scanning for barcodes. - /// Upon calling this method, the necessary camera permission will be requested. + /// Set the zoom scale of the camera. /// - /// Returns an instance of [MobileScannerArguments] - /// when the scanner was successfully started. - /// Returns null if the scanner is currently starting. - /// - /// Throws a [MobileScannerException] if starting the scanner failed. - Future start({ - CameraFacing? cameraFacingOverride, - }) async { - if (isStarting) { - debugPrint("Called start() while starting."); - return null; - } - - events ??= _eventChannel - .receiveBroadcastStream() - .listen((data) => _handleEvent(data as Map)); - - isStarting = true; - - // Check authorization status - if (!kIsWeb) { - final MobileScannerState state; - - try { - state = MobileScannerState.fromRawValue( - await _methodChannel.invokeMethod('state') as int? ?? 0, - ); - } on PlatformException catch (error) { - isStarting = false; - - throw MobileScannerException( - errorCode: MobileScannerErrorCode.genericError, - errorDetails: MobileScannerErrorDetails( - code: error.code, - details: error.details as Object?, - message: error.message, - ), - ); - } - - switch (state) { - // Android does not have an undetermined permission state. - // So if the permission state is denied, just request it now. - case MobileScannerState.undetermined: - case MobileScannerState.denied: - try { - final bool granted = - await _methodChannel.invokeMethod('request') as bool? ?? false; - - if (!granted) { - isStarting = false; - throw const MobileScannerException( - errorCode: MobileScannerErrorCode.permissionDenied, - ); - } - } on PlatformException catch (error) { - isStarting = false; - throw MobileScannerException( - errorCode: MobileScannerErrorCode.genericError, - errorDetails: MobileScannerErrorDetails( - code: error.code, - details: error.details as Object?, - message: error.message, - ), - ); - } - - case MobileScannerState.authorized: - break; - } - } - - // Start the camera with arguments - Map? startResult = {}; - try { - startResult = await _methodChannel.invokeMapMethod( - 'start', - _argumentsToMap(cameraFacingOverride: cameraFacingOverride), - ); - } on PlatformException catch (error) { - MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError; - - final String? errorMessage = error.message; - - if (kIsWeb) { - if (errorMessage == null) { - errorCode = MobileScannerErrorCode.genericError; - } else if (errorMessage.contains('NotFoundError') || - errorMessage.contains('NotSupportedError')) { - errorCode = MobileScannerErrorCode.unsupported; - } else if (errorMessage.contains('NotAllowedError')) { - errorCode = MobileScannerErrorCode.permissionDenied; - } else { - errorCode = MobileScannerErrorCode.genericError; - } - } - - isStarting = false; - - throw MobileScannerException( - errorCode: errorCode, - errorDetails: MobileScannerErrorDetails( - code: error.code, - details: error.details as Object?, - message: error.message, - ), - ); - } - - if (startResult == null) { - isStarting = false; + /// The [zoomScale] must be between 0.0 and 1.0 (both inclusive). + Future setZoomScale(double zoomScale) async { + if (zoomScale < 0 || zoomScale > 1) { throw const MobileScannerException( errorCode: MobileScannerErrorCode.genericError, + errorDetails: MobileScannerErrorDetails( + message: 'The zoomScale must be between 0.0 and 1.0', + ), ); } - final hasTorch = startResult['torchable'] as bool? ?? false; - hasTorchState.value = hasTorch; - - final Size size; - - if (kIsWeb) { - size = Size( - startResult['videoWidth'] as double? ?? 0, - startResult['videoHeight'] as double? ?? 0, - ); - } else { - final Map? sizeInfo = - startResult['size'] as Map?; - - size = Size( - sizeInfo?['width'] as double? ?? 0, - sizeInfo?['height'] as double? ?? 0, - ); - } - - isStarting = false; - return startArguments.value = MobileScannerArguments( - numberOfCameras: startResult['numberOfCameras'] as int?, - size: size, - hasTorch: hasTorch, - textureId: kIsWeb ? null : startResult['textureId'] as int?, - webId: kIsWeb ? startResult['ViewID'] as String? : null, - ); + await MobileScannerPlatform.instance.setZoomScale(zoomScale); } - /// Stops the camera, but does not dispose this controller. + /// Stop the camera. + /// + /// After calling this method, the camera can be restarted using [start]. Future stop() async { - await _methodChannel.invokeMethod('stop'); + await MobileScannerPlatform.instance.stop(); // After the camera stopped, set the torch state to off, // as the torch state callback is never called when the camera is stopped. torchState.value = TorchState.off; } - /// Switches the torch on or off. - /// - /// Does nothing if the device has no torch. - /// - /// Throws if the controller was not initialized. - Future toggleTorch() async { - final hasTorch = hasTorchState.value; - - if (hasTorch == null) { - throw const MobileScannerException( - errorCode: MobileScannerErrorCode.controllerUninitialized, - ); - } + /// Switch between the front and back camera. + Future switchCamera() async { + await MobileScannerPlatform.instance.stop(); - if (!hasTorch) { - return; - } + final CameraFacing cameraDirection; - final TorchState newState = - torchState.value == TorchState.off ? TorchState.on : TorchState.off; + // TODO: update the camera facing direction state - await _methodChannel.invokeMethod('torch', newState.rawValue); + await start(cameraDirection: cameraDirection); } - /// Changes the state of the camera (front or back). + /// Switches the flashlight on or off. /// - /// Does nothing if the device has no front camera. - Future switchCamera() async { - await _methodChannel.invokeMethod('stop'); - final CameraFacing facingToUse = - cameraFacingState.value == CameraFacing.back - ? CameraFacing.front - : CameraFacing.back; - await start(cameraFacingOverride: facingToUse); - } - - /// Handles a local image file. - /// Returns true if a barcode or QR code is found. - /// Returns false if nothing is found. + /// Does nothing if the device has no torch. /// - /// [path] The path of the image on the devices - Future analyzeImage(String path) async { - events ??= _eventChannel - .receiveBroadcastStream() - .listen((data) => _handleEvent(data as Map)); - - return _methodChannel - .invokeMethod('analyzeImage', path) - .then((bool? value) => value ?? false); - } + /// Throws if the controller was not initialized. + Future toggleTorch() async { + final bool hasTorch; - /// Set the zoomScale of the camera. - /// - /// [zoomScale] must be within 0.0 and 1.0, where 1.0 is the max zoom, and 0.0 - /// is zoomed out. - Future setZoomScale(double zoomScale) async { - if (zoomScale < 0 || zoomScale > 1) { - throw const MobileScannerException( - errorCode: MobileScannerErrorCode.genericError, - errorDetails: MobileScannerErrorDetails( - message: 'The zoomScale must be between 0 and 1.', - ), - ); + if (!hasTorch) { + return; } - await _methodChannel.invokeMethod('setScale', zoomScale); - } - /// Reset the zoomScale of the camera to use standard scale 1x. - Future resetZoomScale() async { - await _methodChannel.invokeMethod('resetScale'); - } + final TorchState newState = torchState.value == TorchState.off ? TorchState.on : TorchState.off; - /// Disposes the MobileScannerController and closes all listeners. - /// - /// If you call this, you cannot use this controller object anymore. - void dispose() { - stop(); - events?.cancel(); - _barcodesController.close(); + // Update the torch state to the new state. + // When the platform has updated the torch state, + // it will send an update through the torch state event stream. + await MobileScannerPlatform.instance.setTorchState(); } - /// Handles a returning event from the platform side - void _handleEvent(Map event) { - final name = event['name']; - final data = event['data']; - - switch (name) { - case 'torchState': - final state = TorchState.values[data as int? ?? 0]; - torchState.value = state; - case 'zoomScaleState': - zoomScaleState.value = data as double? ?? 0.0; - case 'barcode': - if (data == null) return; - final parsed = (data as List) - .map((value) => Barcode.fromNative(value as Map)) - .toList(); - - final double? width = event['width'] as double?; - final double? height = event['height'] as double?; - - _barcodesController.add( - BarcodeCapture( - raw: data, - barcodes: parsed, - image: event['image'] as Uint8List?, - size: width == null || height == null - ? Size.zero - : Size(width, height), - ), - ); - case 'barcodeMac': - _barcodesController.add( - BarcodeCapture( - raw: data, - barcodes: [ - Barcode( - rawValue: (data as Map)['payload'] as String?, - format: BarcodeFormat.fromRawValue( - data['symbology'] as int? ?? -1, - ), - ), - ], - ), - ); - case 'barcodeWeb': - final barcode = data as Map?; - final corners = barcode?['corners'] as List? ?? []; - - _barcodesController.add( - BarcodeCapture( - raw: data, - barcodes: [ - if (barcode != null) - Barcode( - rawValue: barcode['rawValue'] as String?, - rawBytes: barcode['rawBytes'] as Uint8List?, - format: BarcodeFormat.fromRawValue( - barcode['format'] as int? ?? -1, - ), - corners: List.unmodifiable( - corners.cast>().map( - (Map e) { - return Offset(e['x']! as double, e['y']! as double); - }, - ), - ), - ), - ], - ), - ); - case 'error': - throw MobileScannerException( - errorCode: MobileScannerErrorCode.genericError, - errorDetails: MobileScannerErrorDetails(message: data as String?), - ); - default: - throw UnimplementedError(name as String?); - } - } - - /// updates the native ScanWindow - Future updateScanWindow(Rect? window) async { - List? data; - if (window != null) { - data = [window.left, window.top, window.right, window.bottom]; - } + @override + Future dispose() async { + await MobileScannerPlatform.instance.dispose(); + unawaited(_barcodesController.close()); - await _methodChannel.invokeMethod('updateScanWindow', {'rect': data}); + super.dispose(); } } From 8be5281e88ccdef26b6a5c84011f1baea2bfc837 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 16:57:42 +0100 Subject: [PATCH 029/161] rename mobile scanner state enum --- lib/mobile_scanner.dart | 2 +- ...> mobile_scanner_authorization_state.dart} | 12 ++--- .../mobile_scanner_method_channel.dart | 12 ++--- ...bile_scanner_authorization_state_test.dart | 50 +++++++++++++++++++ test/enums/mobile_scanner_state_test.dart | 50 ------------------- 5 files changed, 63 insertions(+), 63 deletions(-) rename lib/src/enums/{mobile_scanner_state.dart => mobile_scanner_authorization_state.dart} (58%) create mode 100644 test/enums/mobile_scanner_authorization_state_test.dart delete mode 100644 test/enums/mobile_scanner_state_test.dart diff --git a/lib/mobile_scanner.dart b/lib/mobile_scanner.dart index 0c3250cd7..41907ee14 100644 --- a/lib/mobile_scanner.dart +++ b/lib/mobile_scanner.dart @@ -5,8 +5,8 @@ export 'src/enums/camera_facing.dart'; export 'src/enums/detection_speed.dart'; export 'src/enums/email_type.dart'; export 'src/enums/encryption_type.dart'; +export 'src/enums/mobile_scanner_authorization_state.dart'; export 'src/enums/mobile_scanner_error_code.dart'; -export 'src/enums/mobile_scanner_state.dart'; export 'src/enums/phone_type.dart'; export 'src/enums/torch_state.dart'; export 'src/mobile_scanner.dart'; diff --git a/lib/src/enums/mobile_scanner_state.dart b/lib/src/enums/mobile_scanner_authorization_state.dart similarity index 58% rename from lib/src/enums/mobile_scanner_state.dart rename to lib/src/enums/mobile_scanner_authorization_state.dart index 4fe6619e9..2d50d34ee 100644 --- a/lib/src/enums/mobile_scanner_state.dart +++ b/lib/src/enums/mobile_scanner_authorization_state.dart @@ -1,5 +1,5 @@ /// The authorization state of the scanner. -enum MobileScannerState { +enum MobileScannerAuthorizationState { /// The scanner has not yet requested the required permissions. undetermined(0), @@ -9,16 +9,16 @@ enum MobileScannerState { /// The user denied the required permissions. denied(2); - const MobileScannerState(this.rawValue); + const MobileScannerAuthorizationState(this.rawValue); - factory MobileScannerState.fromRawValue(int value) { + factory MobileScannerAuthorizationState.fromRawValue(int value) { switch (value) { case 0: - return MobileScannerState.undetermined; + return MobileScannerAuthorizationState.undetermined; case 1: - return MobileScannerState.authorized; + return MobileScannerAuthorizationState.authorized; case 2: - return MobileScannerState.denied; + return MobileScannerAuthorizationState.denied; default: throw ArgumentError.value(value, 'value', 'Invalid raw value.'); } diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index 609045fd1..ca7a27145 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -4,8 +4,8 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:mobile_scanner/src/enums/barcode_format.dart'; +import 'package:mobile_scanner/src/enums/mobile_scanner_authorization_state.dart'; import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; -import 'package:mobile_scanner/src/enums/mobile_scanner_state.dart'; import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart'; @@ -92,10 +92,10 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { /// /// Throws a [MobileScannerException] if the permission is not granted. Future _requestCameraPermission() async { - final MobileScannerState authorizationState; + final MobileScannerAuthorizationState authorizationState; try { - authorizationState = MobileScannerState.fromRawValue( + authorizationState = MobileScannerAuthorizationState.fromRawValue( await methodChannel.invokeMethod('state') ?? 0, ); } on PlatformException catch (error) { @@ -111,13 +111,13 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { } switch (authorizationState) { - case MobileScannerState.denied: + case MobileScannerAuthorizationState.denied: throw const MobileScannerException( errorCode: MobileScannerErrorCode.permissionDenied, ); - case MobileScannerState.authorized: + case MobileScannerAuthorizationState.authorized: return; // Already authorized. - case MobileScannerState.undetermined: + case MobileScannerAuthorizationState.undetermined: try { final bool permissionResult = await methodChannel.invokeMethod('request') ?? false; diff --git a/test/enums/mobile_scanner_authorization_state_test.dart b/test/enums/mobile_scanner_authorization_state_test.dart new file mode 100644 index 000000000..3f868cdc4 --- /dev/null +++ b/test/enums/mobile_scanner_authorization_state_test.dart @@ -0,0 +1,50 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile_scanner/src/enums/mobile_scanner_authorization_state.dart'; + +void main() { + group('$MobileScannerAuthorizationState tests', () { + test('can be created from raw value', () { + const values = { + 0: MobileScannerAuthorizationState.undetermined, + 1: MobileScannerAuthorizationState.authorized, + 2: MobileScannerAuthorizationState.denied, + }; + + for (final MapEntry entry in values.entries) { + final MobileScannerAuthorizationState result = MobileScannerAuthorizationState.fromRawValue( + entry.key, + ); + + expect(result, entry.value); + } + }); + + test('invalid raw value throws argument error', () { + const int negative = -1; + const int outOfRange = 3; + + expect( + () => MobileScannerAuthorizationState.fromRawValue(negative), + throwsArgumentError, + ); + expect( + () => MobileScannerAuthorizationState.fromRawValue(outOfRange), + throwsArgumentError, + ); + }); + + test('can be converted to raw value', () { + const values = { + MobileScannerAuthorizationState.undetermined: 0, + MobileScannerAuthorizationState.authorized: 1, + MobileScannerAuthorizationState.denied: 2, + }; + + for (final MapEntry entry in values.entries) { + final int result = entry.key.rawValue; + + expect(result, entry.value); + } + }); + }); +} diff --git a/test/enums/mobile_scanner_state_test.dart b/test/enums/mobile_scanner_state_test.dart deleted file mode 100644 index dcf4f26bf..000000000 --- a/test/enums/mobile_scanner_state_test.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mobile_scanner/src/enums/mobile_scanner_state.dart'; - -void main() { - group('$MobileScannerState tests', () { - test('can be created from raw value', () { - const values = { - 0: MobileScannerState.undetermined, - 1: MobileScannerState.authorized, - 2: MobileScannerState.denied, - }; - - for (final MapEntry entry in values.entries) { - final MobileScannerState result = MobileScannerState.fromRawValue( - entry.key, - ); - - expect(result, entry.value); - } - }); - - test('invalid raw value throws argument error', () { - const int negative = -1; - const int outOfRange = 3; - - expect( - () => MobileScannerState.fromRawValue(negative), - throwsArgumentError, - ); - expect( - () => MobileScannerState.fromRawValue(outOfRange), - throwsArgumentError, - ); - }); - - test('can be converted to raw value', () { - const values = { - MobileScannerState.undetermined: 0, - MobileScannerState.authorized: 1, - MobileScannerState.denied: 2, - }; - - for (final MapEntry entry in values.entries) { - final int result = entry.key.rawValue; - - expect(result, entry.value); - } - }); - }); -} From 3c5a6091564f192f64f401ea8b29cd543078aac9 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 17:05:12 +0100 Subject: [PATCH 030/161] implement mobile scanner state as a model class --- lib/mobile_scanner.dart | 1 + lib/src/objects/mobile_scanner_state.dart | 65 +++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 lib/src/objects/mobile_scanner_state.dart diff --git a/lib/mobile_scanner.dart b/lib/mobile_scanner.dart index 41907ee14..347a90993 100644 --- a/lib/mobile_scanner.dart +++ b/lib/mobile_scanner.dart @@ -21,6 +21,7 @@ export 'src/objects/contact_info.dart'; export 'src/objects/driver_license.dart'; export 'src/objects/email.dart'; export 'src/objects/geo_point.dart'; +export 'src/objects/mobile_scanner_state.dart'; export 'src/objects/person_name.dart'; export 'src/objects/phone.dart'; export 'src/objects/sms.dart'; diff --git a/lib/src/objects/mobile_scanner_state.dart b/lib/src/objects/mobile_scanner_state.dart new file mode 100644 index 000000000..d24576d9e --- /dev/null +++ b/lib/src/objects/mobile_scanner_state.dart @@ -0,0 +1,65 @@ +import 'dart:ui'; + +import 'package:mobile_scanner/src/enums/camera_facing.dart'; +import 'package:mobile_scanner/src/enums/torch_state.dart'; +import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; + +/// This class represents the current state of a [MobileScannerController]. +class MobileScannerState { + /// Create a new [MobileScannerState] instance. + const MobileScannerState({ + required this.cameraDirection, + required this.isInitialized, + required this.size, + required this.torchState, + required this.zoomScale, + this.error, + }); + + /// Create a new [MobileScannerState] instance that is uninitialized. + const MobileScannerState.uninitialized(CameraFacing facing) + : this( + cameraDirection: facing, + isInitialized: false, + size: Size.zero, + torchState: TorchState.unavailable, + zoomScale: 1.0, + ); + + /// The facing direction of the camera. + final CameraFacing cameraDirection; + + /// The error that occurred while setting up or using the canera. + final MobileScannerException? error; + + /// Whether the mobile scanner has initialized successfully. + final bool isInitialized; + + /// The size of the camera output. + final Size size; + + /// The current state of the flashlight of the camera. + final TorchState torchState; + + /// The current zoom scale of the camera. + final double zoomScale; + + /// Create a copy of this state with the given parameters. + MobileScannerState copyWith({ + CameraFacing? cameraDirection, + MobileScannerException? error, + bool? isInitialized, + Size? size, + TorchState? torchState, + double? zoomScale, + }) { + return MobileScannerState( + cameraDirection: cameraDirection ?? this.cameraDirection, + error: error, + isInitialized: isInitialized ?? this.isInitialized, + size: size ?? this.size, + torchState: torchState ?? this.torchState, + zoomScale: zoomScale ?? this.zoomScale, + ); + } +} From 86e8e952e3d6d0c7b7b6562300d58748f91d0a9b Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 17:15:23 +0100 Subject: [PATCH 031/161] clamp the zoom scale --- lib/src/mobile_scanner_controller.dart | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 7cd0b4aee..4a8909d89 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -113,17 +113,13 @@ class MobileScannerController extends ValueNotifier { /// Set the zoom scale of the camera. /// /// The [zoomScale] must be between 0.0 and 1.0 (both inclusive). + /// + /// If the [zoomScale] is out of range, + /// it is adjusted to fit within the allowed range. Future setZoomScale(double zoomScale) async { - if (zoomScale < 0 || zoomScale > 1) { - throw const MobileScannerException( - errorCode: MobileScannerErrorCode.genericError, - errorDetails: MobileScannerErrorDetails( - message: 'The zoomScale must be between 0.0 and 1.0', - ), - ); - } + final double clampedZoomScale = zoomScale.clamp(0.0, 1.0); - await MobileScannerPlatform.instance.setZoomScale(zoomScale); + await MobileScannerPlatform.instance.setZoomScale(clampedZoomScale); } /// Stop the camera. From 3bb3383e541baacb54251b721fcc081406d4eee1 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 17:19:50 +0100 Subject: [PATCH 032/161] add helper to check disposal --- lib/src/mobile_scanner_controller.dart | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 4a8909d89..b9b177b42 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -94,6 +94,28 @@ class MobileScannerController extends ValueNotifier { /// Get the stream of scanned barcodes. Stream get barcodes => _barcodesController.stream; + bool _isDisposed = false; + + void _throwIfNotInitialized() { + if (!value.isInitialized) { + throw const MobileScannerException( + errorCode: MobileScannerErrorCode.controllerUninitialized, + errorDetails: MobileScannerErrorDetails( + message: 'The MobileScannerController has not been initialized.', + ), + ); + } + + if (_isDisposed) { + throw const MobileScannerException( + errorCode: MobileScannerErrorCode.controllerDisposed, + errorDetails: MobileScannerErrorDetails( + message: 'The MobileScannerController was used after it has been disposed.', + ), + ); + } + } + /// Analyze an image file. /// /// The [path] points to a file on the device. @@ -166,9 +188,14 @@ class MobileScannerController extends ValueNotifier { @override Future dispose() async { + if (_isDisposed) { + return; + } + await MobileScannerPlatform.instance.dispose(); unawaited(_barcodesController.close()); + _isDisposed = true; super.dispose(); } } From a807d6ffa6bb8d8312e42935e71fa54eb01ffc18 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 18:13:43 +0100 Subject: [PATCH 033/161] add a controller disposed error code --- lib/src/enums/mobile_scanner_error_code.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/enums/mobile_scanner_error_code.dart b/lib/src/enums/mobile_scanner_error_code.dart index 382b18f9b..0fbf3d46d 100644 --- a/lib/src/enums/mobile_scanner_error_code.dart +++ b/lib/src/enums/mobile_scanner_error_code.dart @@ -8,6 +8,9 @@ enum MobileScannerErrorCode { /// before restarting it. controllerAlreadyInitialized, + /// The controller was used after being disposed. + controllerDisposed, + /// The controller was used /// while it was not yet initialized using [MobileScannerController.start]. controllerUninitialized, From 92d400cd54cf8f1cbd89b44fc3941b75063403b4 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 18:14:02 +0100 Subject: [PATCH 034/161] do nothing in stop if texture id is null --- lib/src/method_channel/mobile_scanner_method_channel.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index ca7a27145..22e648d59 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -276,6 +276,10 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @override Future stop() async { + if (_textureId == null) { + return; + } + await methodChannel.invokeMethod('stop'); _textureId = null; From 17f2fbbc2281250dfebcca73b5a0e0c121b3b181 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 18:15:55 +0100 Subject: [PATCH 035/161] implement the start method --- lib/src/mobile_scanner_controller.dart | 83 +++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index b9b177b42..a8f5e2e79 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -6,11 +6,13 @@ import 'package:mobile_scanner/src/enums/barcode_format.dart'; import 'package:mobile_scanner/src/enums/camera_facing.dart'; import 'package:mobile_scanner/src/enums/detection_speed.dart'; import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; -import 'package:mobile_scanner/src/enums/mobile_scanner_state.dart'; import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart'; +import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; import 'package:mobile_scanner/src/objects/barcode_capture.dart'; +import 'package:mobile_scanner/src/objects/mobile_scanner_state.dart'; +import 'package:mobile_scanner/src/objects/start_options.dart'; /// The controller for the [MobileScanner] widget. class MobileScannerController extends ValueNotifier { @@ -94,8 +96,38 @@ class MobileScannerController extends ValueNotifier { /// Get the stream of scanned barcodes. Stream get barcodes => _barcodesController.stream; + StreamSubscription? _barcodesSubscription; + StreamSubscription? _torchStateSubscription; + StreamSubscription? _zoomScaleSubscription; + bool _isDisposed = false; + void _disposeListeners() { + _barcodesSubscription?.cancel(); + _torchStateSubscription?.cancel(); + _zoomScaleSubscription?.cancel(); + + _barcodesSubscription = null; + _torchStateSubscription = null; + _zoomScaleSubscription = null; + } + + void _setupListeners() { + _barcodesSubscription = MobileScannerPlatform.instance.barcodesStream.listen((BarcodeCapture? barcode) { + if (barcode != null) { + _barcodesController.add(barcode); + } + }); + + _torchStateSubscription = MobileScannerPlatform.instance.torchStateStream.listen((TorchState torchState) { + value = value.copyWith(torchState: torchState); + }); + + _zoomScaleSubscription = MobileScannerPlatform.instance.zoomScaleStateStream.listen((double zoomScale) { + value = value.copyWith(zoomScale: zoomScale); + }); + } + void _throwIfNotInitialized() { if (!value.isInitialized) { throw const MobileScannerException( @@ -144,6 +176,55 @@ class MobileScannerController extends ValueNotifier { await MobileScannerPlatform.instance.setZoomScale(clampedZoomScale); } + /// Start scanning for barcodes. + /// Upon calling this method, the necessary camera permission will be requested. + /// + /// Throws a [MobileScannerException] if starting the scanner failed. + Future start({CameraFacing? cameraDirection}) async { + if (_isDisposed) { + throw const MobileScannerException( + errorCode: MobileScannerErrorCode.controllerDisposed, + errorDetails: MobileScannerErrorDetails( + message: 'The MobileScannerController was used after it has been disposed.', + ), + ); + } + + final CameraFacing effectiveDirection = cameraDirection ?? facing; + + final StartOptions options = StartOptions( + cameraDirection: effectiveDirection, + cameraResolution: cameraResolution, + detectionSpeed: detectionSpeed, + detectionTimeoutMs: detectionTimeoutMs, + formats: formats, + returnImage: returnImage, + torchEnabled: torchEnabled, + ); + + try { + _setupListeners(); + + final MobileScannerViewAttributes viewAttributes = await MobileScannerPlatform.instance.start( + options, + ); + + value = value.copyWith( + cameraDirection: effectiveDirection, + isInitialized: true, + size: viewAttributes.size, + ); + } on MobileScannerException catch (error) { + if (!_isDisposed) { + value = value.copyWith( + cameraDirection: facing, + isInitialized: true, + error: error, + ); + } + } + } + /// Stop the camera. /// /// After calling this method, the camera can be restarted using [start]. From 03812538de9c9658a6f21889463206617e05eb55 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 18:17:20 +0100 Subject: [PATCH 036/161] fix doc --- lib/src/mobile_scanner_controller.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index a8f5e2e79..226514936 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -179,6 +179,9 @@ class MobileScannerController extends ValueNotifier { /// Start scanning for barcodes. /// Upon calling this method, the necessary camera permission will be requested. /// + /// The [cameraDirection] can be used to specify the camera direction. + /// If this is null, this defaults to the [facing] value. + /// /// Throws a [MobileScannerException] if starting the scanner failed. Future start({CameraFacing? cameraDirection}) async { if (_isDisposed) { From b64e5fa6098bb8580b4773caa9de68aee22bf3a4 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 18:18:56 +0100 Subject: [PATCH 037/161] throw in zoom scale methods if not initialized --- lib/src/mobile_scanner_controller.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 226514936..b6edfbf13 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -161,6 +161,10 @@ class MobileScannerController extends ValueNotifier { /// Reset the zoom scale of the camera. Future resetZoomScale() async { + _throwIfNotInitialized(); + + // When the platform has updated the zoom scale, + // it will send an update through the zoom scale state event stream. await MobileScannerPlatform.instance.resetZoomScale(); } @@ -171,8 +175,13 @@ class MobileScannerController extends ValueNotifier { /// If the [zoomScale] is out of range, /// it is adjusted to fit within the allowed range. Future setZoomScale(double zoomScale) async { + _throwIfNotInitialized(); + final double clampedZoomScale = zoomScale.clamp(0.0, 1.0); + // Update the zoom scale state to the new state. + // When the platform has updated the zoom scale, + // it will send an update through the zoom scale state event stream. await MobileScannerPlatform.instance.setZoomScale(clampedZoomScale); } @@ -253,8 +262,6 @@ class MobileScannerController extends ValueNotifier { /// Switches the flashlight on or off. /// /// Does nothing if the device has no torch. - /// - /// Throws if the controller was not initialized. Future toggleTorch() async { final bool hasTorch; From cd7302f9a1130b419226b2d88568f9ff55cd0b42 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 18:21:07 +0100 Subject: [PATCH 038/161] fix state update in stop(); pass camera direction in switchCamera(); fix the torch state for toggleTorch() --- lib/src/mobile_scanner_controller.dart | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index b6edfbf13..7d991df0e 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -241,40 +241,48 @@ class MobileScannerController extends ValueNotifier { /// /// After calling this method, the camera can be restarted using [start]. Future stop() async { + _disposeListeners(); + + _throwIfNotInitialized(); + await MobileScannerPlatform.instance.stop(); // After the camera stopped, set the torch state to off, // as the torch state callback is never called when the camera is stopped. - torchState.value = TorchState.off; + value = value.copyWith(torchState: TorchState.off); } /// Switch between the front and back camera. Future switchCamera() async { - await MobileScannerPlatform.instance.stop(); + _throwIfNotInitialized(); - final CameraFacing cameraDirection; + await stop(); - // TODO: update the camera facing direction state + final CameraFacing cameraDirection = value.cameraDirection; - await start(cameraDirection: cameraDirection); + await start( + cameraDirection: cameraDirection == CameraFacing.front ? CameraFacing.back : CameraFacing.front, + ); } /// Switches the flashlight on or off. /// /// Does nothing if the device has no torch. Future toggleTorch() async { - final bool hasTorch; + _throwIfNotInitialized(); + + final TorchState torchState = value.torchState; - if (!hasTorch) { + if (torchState == TorchState.unavailable) { return; } - final TorchState newState = torchState.value == TorchState.off ? TorchState.on : TorchState.off; + final TorchState newState = torchState == TorchState.off ? TorchState.on : TorchState.off; // Update the torch state to the new state. // When the platform has updated the torch state, // it will send an update through the torch state event stream. - await MobileScannerPlatform.instance.setTorchState(); + await MobileScannerPlatform.instance.setTorchState(newState); } @override From ff99c61d69056eb596d1fc0a4d8968f17947720e Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 18:28:27 +0100 Subject: [PATCH 039/161] fix torch state --- lib/src/mobile_scanner_controller.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 7d991df0e..8a48a4704 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -225,6 +225,9 @@ class MobileScannerController extends ValueNotifier { cameraDirection: effectiveDirection, isInitialized: true, size: viewAttributes.size, + // If the device has a flashlight, let the platform update the torch state. + // If it does not have one, provide the unavailable state directly. + torchState: viewAttributes.hasTorch ? null : TorchState.unavailable, ); } on MobileScannerException catch (error) { if (!_isDisposed) { From 7cb95d50288630de049a8452e6f8016ec0bc9ea8 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 20:15:34 +0100 Subject: [PATCH 040/161] fix bug with null scan window; abort if texture is null --- .../mobile_scanner/MobileScannerHandler.kt | 2 +- .../mobile_scanner_method_channel.dart | 12 +++++++++++- lib/src/mobile_scanner_controller.dart | 7 +++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt index 99bd171c0..17299ebd3 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt @@ -269,7 +269,7 @@ class MobileScannerHandler( } private fun updateScanWindow(call: MethodCall, result: MethodChannel.Result) { - mobileScanner!!.scanWindow = call.argument?>("rect") + mobileScanner?.scanWindow = call.argument?>("rect") result.success(null) } diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index 22e648d59..e846c43c5 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -287,9 +287,19 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @override Future updateScanWindow(Rect? window) async { + if (_textureId == null) { + return; + } + + List? points; + + if (window != null) { + points = [window.left, window.top, window.right, window.bottom]; + } + await methodChannel.invokeMethod( 'updateScanWindow', - {'rect': window}, + {'rect': points}, ); } diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 8a48a4704..adc0273e2 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -288,6 +288,13 @@ class MobileScannerController extends ValueNotifier { await MobileScannerPlatform.instance.setTorchState(newState); } + /// Update the scan window with the given [window] rectangle. + /// + /// If [window] is null, the scan window will be reset to the full camera preview. + Future updateScanWindow(Rect? window) async { + await MobileScannerPlatform.instance.updateScanWindow(window); + } + @override Future dispose() async { if (_isDisposed) { From c1862f8cfc041a126302724e484a050c7c623524 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 20:18:44 +0100 Subject: [PATCH 041/161] skip update scan window if disposed or not ready --- lib/src/mobile_scanner_controller.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index adc0273e2..7823e65f5 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -292,6 +292,10 @@ class MobileScannerController extends ValueNotifier { /// /// If [window] is null, the scan window will be reset to the full camera preview. Future updateScanWindow(Rect? window) async { + if (_isDisposed || !value.isInitialized) { + return; + } + await MobileScannerPlatform.instance.updateScanWindow(window); } From 19fa6a195114b6cb46bb05dc94035e5f2e462ba8 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 20:52:38 +0100 Subject: [PATCH 042/161] add build camera view method to controller --- lib/src/mobile_scanner_controller.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 7823e65f5..85c0787cb 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -1,7 +1,6 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:mobile_scanner/src/enums/barcode_format.dart'; import 'package:mobile_scanner/src/enums/camera_facing.dart'; import 'package:mobile_scanner/src/enums/detection_speed.dart'; @@ -159,6 +158,13 @@ class MobileScannerController extends ValueNotifier { return MobileScannerPlatform.instance.analyzeImage(path); } + /// Build a camera preview widget. + Widget buildCameraView() { + _throwIfNotInitialized(); + + return MobileScannerPlatform.instance.buildCameraView(); + } + /// Reset the zoom scale of the camera. Future resetZoomScale() async { _throwIfNotInitialized(); From 4c7ed61a96a41290be02e1fd19ee401df8be7039 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 21:00:42 +0100 Subject: [PATCH 043/161] refactor the MobileScanner widget to remove the lifecycle handling in the widget --- lib/src/mobile_scanner.dart | 328 +++++++++++------------------------- 1 file changed, 103 insertions(+), 225 deletions(-) diff --git a/lib/src/mobile_scanner.dart b/lib/src/mobile_scanner.dart index 4bf54e9bb..650aac19a 100644 --- a/lib/src/mobile_scanner.dart +++ b/lib/src/mobile_scanner.dart @@ -1,13 +1,10 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; -import 'package:mobile_scanner/src/objects/barcode_capture.dart'; -import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart'; +import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart'; +import 'package:mobile_scanner/src/objects/mobile_scanner_state.dart'; import 'package:mobile_scanner/src/scan_window_calculation.dart'; /// The function signature for the error builder. @@ -17,18 +14,26 @@ typedef MobileScannerErrorBuilder = Widget Function( Widget?, ); -/// The [MobileScanner] widget displays a live camera preview. +/// This widget displays a live camera preview for the barcode scanner. class MobileScanner extends StatefulWidget { - /// The controller that manages the barcode scanner. - /// - /// If this is null, the scanner will manage its own controller. - final MobileScannerController? controller; + /// Create a new [MobileScanner] using the provided [controller]. + const MobileScanner({ + required this.controller, + this.fit = BoxFit.cover, + this.errorBuilder, + this.overlayBuilder, + this.placeholderBuilder, + this.scanWindow, + super.key, + }); - /// The function that builds an error widget when the scanner - /// could not be started. + /// The controller for the camera preview. + final MobileScannerController controller; + + /// The error builder for the camera preview. /// - /// If this is null, defaults to a black [ColoredBox] - /// with a centered white [Icons.error] icon. + /// If this is null, a black [ColoredBox], + /// with a centered white [Icons.error] icon is used as error widget. final MobileScannerErrorBuilder? errorBuilder; /// The [BoxFit] for the camera preview. @@ -36,250 +41,123 @@ class MobileScanner extends StatefulWidget { /// Defaults to [BoxFit.cover]. final BoxFit fit; - /// The function that signals when new codes were detected by the [controller]. - final void Function(BarcodeCapture barcodes) onDetect; - - /// The function that signals when the barcode scanner is started. - @Deprecated('Use onScannerStarted() instead.') - final void Function(MobileScannerArguments? arguments)? onStart; - - /// The function that signals when the barcode scanner is started. - final void Function(MobileScannerArguments? arguments)? onScannerStarted; + /// The builder for the overlay above the camera preview. + /// + /// The resulting widget can be combined with the [scanWindow] rectangle + /// to create a cutout for the camera preview. + /// + /// The [BoxConstraints] for this builder + /// are the same constraints that are used to compute the effective [scanWindow]. + /// + /// The overlay is only displayed when the camera preview is visible. + final LayoutWidgetBuilder? overlayBuilder; - /// The function that builds a placeholder widget when the scanner - /// is not yet displaying its camera preview. + /// The placeholder builder for the camera preview. /// /// If this is null, a black [ColoredBox] is used as placeholder. + /// + /// The placeholder is displayed when the camera preview is being initialized. final Widget Function(BuildContext, Widget?)? placeholderBuilder; - /// if set barcodes will only be scanned if they fall within this [Rect] - /// useful for having a cut-out overlay for example. these [Rect] - /// coordinates are relative to the widget size, so by how much your - /// rectangle overlays the actual image can depend on things like the - /// [BoxFit] - final Rect? scanWindow; - - /// Only set this to true if you are starting another instance of mobile_scanner - /// right after disposing the first one, like in a PageView. + /// The scan window rectangle for the barcode scanner. /// - /// Default: false - final bool startDelay; - - /// The overlay which will be painted above the scanner when has started successful. - /// Will no be pointed when an error occurs or the scanner hasn't been started yet. - final Widget? overlay; - - /// Create a new [MobileScanner] using the provided [controller] - /// and [onBarcodeDetected] callback. - const MobileScanner({ - this.controller, - this.errorBuilder, - this.fit = BoxFit.cover, - required this.onDetect, - @Deprecated('Use onScannerStarted() instead.') this.onStart, - this.onScannerStarted, - this.placeholderBuilder, - this.scanWindow, - this.startDelay = false, - this.overlay, - super.key, - }); + /// If this is not null, the barcode scanner will only scan barcodes + /// which intersect this rectangle. + /// + /// The rectangle is relative to the layout size of the *camera preview widget*, + /// rather than the actual camera preview size, + /// since the actual widget size might not be the same as the camera preview size. + /// + /// For example, the applied [fit] has an effect on the size of the camera preview widget, + /// while the camera preview size remains the same. + final Rect? scanWindow; @override State createState() => _MobileScannerState(); } -class _MobileScannerState extends State - with WidgetsBindingObserver { - /// The subscription that listens to barcode detection. - StreamSubscription? _barcodesSubscription; - - /// The internally managed controller. - late MobileScannerController _controller; - - /// Whether the controller should resume - /// when the application comes back to the foreground. - bool _resumeFromBackground = false; - - MobileScannerException? _startException; - - Widget _buildPlaceholderOrError(BuildContext context, Widget? child) { - final error = _startException; - - if (error != null) { - return widget.errorBuilder?.call(context, error, child) ?? - const ColoredBox( - color: Colors.black, - child: Center(child: Icon(Icons.error, color: Colors.white)), - ); - } - - return widget.placeholderBuilder?.call(context, child) ?? - const ColoredBox(color: Colors.black); - } - - /// Start the given [scanner]. - Future _startScanner() async { - if (widget.startDelay) { - await Future.delayed(const Duration(seconds: 1, milliseconds: 500)); - } - - _barcodesSubscription ??= _controller.barcodes.listen( - widget.onDetect, - ); +class _MobileScannerState extends State { + /// The current scan window. + Rect? scanWindow; - if (!_controller.autoStart) { - debugPrint( - 'mobile_scanner: not starting automatically because autoStart is set to false in the controller.', + /// Recalculate the scan window based on the updated [constraints]. + void _maybeUpdateScanWindow(MobileScannerState scannerState, BoxConstraints constraints) { + if (widget.scanWindow != null && scanWindow == null) { + scanWindow = calculateScanWindowRelativeToTextureInPercentage( + widget.fit, + widget.scanWindow!, + textureSize: scannerState.size, + widgetSize: constraints.biggest, ); - return; - } - _controller.start().then((arguments) { - // ignore: deprecated_member_use_from_same_package - widget.onStart?.call(arguments); - widget.onScannerStarted?.call(arguments); - }).catchError((error) { - if (!mounted) { - return; - } - - if (error is MobileScannerException) { - _startException = error; - } else if (error is PlatformException) { - _startException = MobileScannerException( - errorCode: MobileScannerErrorCode.genericError, - errorDetails: MobileScannerErrorDetails( - code: error.code, - message: error.message, - details: error.details, - ), - ); - } else { - _startException = MobileScannerException( - errorCode: MobileScannerErrorCode.genericError, - errorDetails: MobileScannerErrorDetails( - details: error, - ), - ); - } - - setState(() {}); - }); - } - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - _controller = widget.controller ?? MobileScannerController(); - _startScanner(); + unawaited(widget.controller.updateScanWindow(scanWindow)); + } } @override - void didChangeAppLifecycleState(AppLifecycleState state) { - // App state changed before the controller was initialized. - if (_controller.isStarting) { - return; - } + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: widget.controller, + builder: (BuildContext context, MobileScannerState value, Widget? child) { + if (!value.isInitialized) { + const Widget defaultPlaceholder = ColoredBox(color: Colors.black); - switch (state) { - case AppLifecycleState.resumed: - if (_resumeFromBackground) { - _startScanner(); + return widget.placeholderBuilder?.call(context, child) ?? defaultPlaceholder; } - case AppLifecycleState.inactive: - _resumeFromBackground = true; - _controller.stop(); - default: - break; - } - } - Rect? scanWindow; + final MobileScannerException? error = value.error; - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - return ValueListenableBuilder( - valueListenable: _controller.startArguments, - builder: (context, value, child) { - if (value == null) { - return _buildPlaceholderOrError(context, child); - } + if (error != null) { + const Widget defaultError = ColoredBox( + color: Colors.black, + child: Center(child: Icon(Icons.error, color: Colors.white)), + ); - if (widget.scanWindow != null && scanWindow == null) { - scanWindow = calculateScanWindowRelativeToTextureInPercentage( - widget.fit, - widget.scanWindow!, - textureSize: value.size, - widgetSize: constraints.biggest, - ); + return widget.errorBuilder?.call(context, error, child) ?? defaultError; + } - _controller.updateScanWindow(scanWindow); - } - if (widget.overlay != null) { - return Stack( - alignment: Alignment.center, - children: [ - _scanner( - value.size, - value.webId, - value.textureId, - value.numberOfCameras, + return LayoutBuilder( + builder: (context, constraints) { + _maybeUpdateScanWindow(value, constraints); + + final Widget? overlay = widget.overlayBuilder?.call(context, constraints); + final Size cameraPreviewSize = value.size; + + final Widget scannerWidget = ClipRect( + child: SizedBox.fromSize( + size: constraints.biggest, + child: FittedBox( + fit: widget.fit, + child: SizedBox( + width: cameraPreviewSize.width, + height: cameraPreviewSize.height, + child: MobileScannerPlatform.instance.buildCameraView(), ), - widget.overlay!, - ], - ); - } else { - return _scanner( - value.size, - value.webId, - value.textureId, - value.numberOfCameras, - ); + ), + ), + ); + + if (overlay == null) { + return scannerWidget; } + + return Stack( + alignment: Alignment.center, + children: [ + scannerWidget, + overlay, + ], + ); }, ); }, ); } - Widget _scanner( - Size size, - String? webId, - int? textureId, - int? numberOfCameras, - ) { - return ClipRect( - child: LayoutBuilder( - builder: (_, constraints) { - return SizedBox.fromSize( - size: constraints.biggest, - child: FittedBox( - fit: widget.fit, - child: SizedBox( - width: size.width, - height: size.height, - child: kIsWeb - ? HtmlElementView(viewType: webId!) - : Texture(textureId: textureId!), - ), - ), - ); - }, - ), - ); - } - @override void dispose() { - _controller.updateScanWindow(null); - WidgetsBinding.instance.removeObserver(this); - _barcodesSubscription?.cancel(); - _barcodesSubscription = null; - _controller.dispose(); + // When this widget is unmounted, reset the scan window. + widget.controller.updateScanWindow(null); super.dispose(); } } From a3584378fe8673cdcefb9dffb2b9e2bd957f4c8e Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 21:09:40 +0100 Subject: [PATCH 044/161] add stub for the web implementation --- lib/mobile_scanner_web.dart | 3 --- lib/src/web/mobile_scanner_web.dart | 12 ++++++++++++ pubspec.yaml | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) delete mode 100644 lib/mobile_scanner_web.dart create mode 100644 lib/src/web/mobile_scanner_web.dart diff --git a/lib/mobile_scanner_web.dart b/lib/mobile_scanner_web.dart deleted file mode 100644 index 07a7a4e9b..000000000 --- a/lib/mobile_scanner_web.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'src/web/base.dart'; -export 'src/web/jsqr.dart'; -export 'src/web/zxing.dart'; diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart new file mode 100644 index 000000000..2d9a21523 --- /dev/null +++ b/lib/src/web/mobile_scanner_web.dart @@ -0,0 +1,12 @@ +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart'; + +/// A web implementation of the MobileScannerPlatform of the MobileScanner plugin. +class MobileScannerWeb extends MobileScannerPlatform { + /// Constructs a [MobileScannerWeb] instance. + MobileScannerWeb(); + + static void registerWith(Registrar registrar) { + MobileScannerPlatform.instance = MobileScannerWeb(); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index f778e4a1e..b21260bdd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,4 +44,4 @@ flutter: pluginClass: MobileScannerPlugin web: pluginClass: MobileScannerWebPlugin - fileName: mobile_scanner_web_plugin.dart + fileName: web/mobile_scanner_web.dart From bb808d07c06cc50270c8198400b503e7cf326ee6 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 8 Nov 2023 21:16:16 +0100 Subject: [PATCH 045/161] update changelog --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11d1ad5fd..e82943d77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ BREAKING CHANGES: * The `width` and `height` of `BarcodeCapture` have been removed, in favor of `size`. * The `raw` attribute is now `Object?` instead of `dynamic`, so that it participates in type promotion. +* The `MobileScannerArguments` class has been removed from the public API, as it is an internal type. +* The `cameraFacingOverride` named argument for the `start()` method has been renamed to `cameraDirection`. +* The `analyzeImage` function now correctly returns a `BarcodeCapture?` instead of a boolean. +* The `formats` attribute of the `MobileScannerController` is now non-null. +* The `MobileScannerState` enum has been renamed to `MobileScannerAuthorizationState`. +* The various `ValueNotifier`s for the camera state have been removed. Use the `value` of the `MobileScannerController` instead. +* The `hasTorch` getter has been removed. Instead, use the torch state of the controller's value. + The `TorchState` enum now provides a new value for unavailable flashlights. +* The `autoStart` attribute has been removed from the `MobileScannerController`. The controller should be manually started on-demand. +* A controller is now required for the `MobileScanner` widget. +* The `onPremissionSet`, `onStart` and `onScannerStarted` methods have been removed from the `MobileScanner` widget. Instead, await `MobileScannerController.start()`. +* The `startDelay` has been removed from the `MobileScanner` widget. Instead, use a delay between manual starts of one or more controllers. +* The `onDetect` method has been removed from the `MobileScanner` widget. Instead, listen to `MobileScannerController.barcodes` directly. +* The `overlay` widget of the `MobileScanner` has been replaced by a new property, `overlayBuilder`, which provides the constraints for the overlay. + +Improvements: +* The `MobileScannerController` is now a ChangeNotifier, with `MobileScannerState` as its model. ## 4.0.1 Bugs fixed: From ee05a6eefaa20162de6d374dee35107a3f57f39b Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 9 Nov 2023 09:05:28 +0100 Subject: [PATCH 046/161] add an isRunning flag to the scanner state --- lib/src/mobile_scanner_controller.dart | 7 ++++++- lib/src/objects/mobile_scanner_state.dart | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 85c0787cb..5f96be432 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -230,12 +230,14 @@ class MobileScannerController extends ValueNotifier { value = value.copyWith( cameraDirection: effectiveDirection, isInitialized: true, + isRunning: true, size: viewAttributes.size, // If the device has a flashlight, let the platform update the torch state. // If it does not have one, provide the unavailable state directly. torchState: viewAttributes.hasTorch ? null : TorchState.unavailable, ); } on MobileScannerException catch (error) { + // The initialization finished with an error. if (!_isDisposed) { value = value.copyWith( cameraDirection: facing, @@ -258,7 +260,10 @@ class MobileScannerController extends ValueNotifier { // After the camera stopped, set the torch state to off, // as the torch state callback is never called when the camera is stopped. - value = value.copyWith(torchState: TorchState.off); + value = value.copyWith( + isRunning: false, + torchState: TorchState.off, + ); } /// Switch between the front and back camera. diff --git a/lib/src/objects/mobile_scanner_state.dart b/lib/src/objects/mobile_scanner_state.dart index d24576d9e..eeaf12311 100644 --- a/lib/src/objects/mobile_scanner_state.dart +++ b/lib/src/objects/mobile_scanner_state.dart @@ -10,6 +10,7 @@ class MobileScannerState { const MobileScannerState({ required this.cameraDirection, required this.isInitialized, + required this.isRunning, required this.size, required this.torchState, required this.zoomScale, @@ -21,6 +22,7 @@ class MobileScannerState { : this( cameraDirection: facing, isInitialized: false, + isRunning: false, size: Size.zero, torchState: TorchState.unavailable, zoomScale: 1.0, @@ -33,8 +35,15 @@ class MobileScannerState { final MobileScannerException? error; /// Whether the mobile scanner has initialized successfully. + /// + /// This is `true` if the camera is ready to be used. final bool isInitialized; + /// Whether the mobile scanner is currently running. + /// + /// This is `true` if the camera is active. + final bool isRunning; + /// The size of the camera output. final Size size; @@ -49,6 +58,7 @@ class MobileScannerState { CameraFacing? cameraDirection, MobileScannerException? error, bool? isInitialized, + bool? isRunning, Size? size, TorchState? torchState, double? zoomScale, @@ -57,6 +67,7 @@ class MobileScannerState { cameraDirection: cameraDirection ?? this.cameraDirection, error: error, isInitialized: isInitialized ?? this.isInitialized, + isRunning: isRunning ?? this.isRunning, size: size ?? this.size, torchState: torchState ?? this.torchState, zoomScale: zoomScale ?? this.zoomScale, From 1c8776b243584429dc0d41feaada84b870e93feb Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 9 Nov 2023 09:58:35 +0100 Subject: [PATCH 047/161] format --- .../mobile_scanner_method_channel.dart | 25 ++++++++----- lib/src/mobile_scanner.dart | 14 +++++--- lib/src/mobile_scanner_controller.dart | 36 +++++++++++++------ ...bile_scanner_authorization_state_test.dart | 9 +++-- 4 files changed, 58 insertions(+), 26 deletions(-) diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index e846c43c5..23b8f6f47 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -31,7 +31,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { Stream>? _eventsStream; Stream> get eventsStream { - _eventsStream ??= eventChannel.receiveBroadcastStream().cast>(); + _eventsStream ??= + eventChannel.receiveBroadcastStream().cast>(); return _eventsStream!; } @@ -50,7 +51,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { return null; } - final List> barcodes = data.cast>(); + final List> barcodes = + data.cast>(); if (Platform.isMacOS) { return BarcodeCapture( @@ -119,7 +121,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { return; // Already authorized. case MobileScannerAuthorizationState.undetermined: try { - final bool permissionResult = await methodChannel.invokeMethod('request') ?? false; + final bool permissionResult = + await methodChannel.invokeMethod('request') ?? false; if (permissionResult) { return; // Authorization was granted. @@ -144,7 +147,9 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @override Stream get barcodesStream { - return eventsStream.where((event) => event['name'] == 'barcode').map((event) => _parseBarcode(event)); + return eventsStream + .where((event) => event['name'] == 'barcode') + .map((event) => _parseBarcode(event)); } @override @@ -163,7 +168,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @override Future analyzeImage(String path) async { - final Map? result = await methodChannel.invokeMapMethod( + final Map? result = + await methodChannel.invokeMapMethod( 'analyzeImage', path, ); @@ -177,7 +183,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { throw const MobileScannerException( errorCode: MobileScannerErrorCode.controllerUninitialized, errorDetails: MobileScannerErrorDetails( - message: 'The controller was not yet initialized. Call start() before calling buildCameraView().', + message: + 'The controller was not yet initialized. Call start() before calling buildCameraView().', ), ); } @@ -210,7 +217,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { throw const MobileScannerException( errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, errorDetails: MobileScannerErrorDetails( - message: 'The scanner was already started. Call stop() before calling start() again.', + message: + 'The scanner was already started. Call stop() before calling start() again.', ), ); } @@ -259,7 +267,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { final bool hasTorch = startResult['torchable'] as bool? ?? false; - final Map? sizeInfo = startResult['size'] as Map?; + final Map? sizeInfo = + startResult['size'] as Map?; final double? width = sizeInfo?['width'] as double?; final double? height = sizeInfo?['height'] as double?; diff --git a/lib/src/mobile_scanner.dart b/lib/src/mobile_scanner.dart index 650aac19a..1b7bd9463 100644 --- a/lib/src/mobile_scanner.dart +++ b/lib/src/mobile_scanner.dart @@ -81,7 +81,10 @@ class _MobileScannerState extends State { Rect? scanWindow; /// Recalculate the scan window based on the updated [constraints]. - void _maybeUpdateScanWindow(MobileScannerState scannerState, BoxConstraints constraints) { + void _maybeUpdateScanWindow( + MobileScannerState scannerState, + BoxConstraints constraints, + ) { if (widget.scanWindow != null && scanWindow == null) { scanWindow = calculateScanWindowRelativeToTextureInPercentage( widget.fit, @@ -102,7 +105,8 @@ class _MobileScannerState extends State { if (!value.isInitialized) { const Widget defaultPlaceholder = ColoredBox(color: Colors.black); - return widget.placeholderBuilder?.call(context, child) ?? defaultPlaceholder; + return widget.placeholderBuilder?.call(context, child) ?? + defaultPlaceholder; } final MobileScannerException? error = value.error; @@ -113,14 +117,16 @@ class _MobileScannerState extends State { child: Center(child: Icon(Icons.error, color: Colors.white)), ); - return widget.errorBuilder?.call(context, error, child) ?? defaultError; + return widget.errorBuilder?.call(context, error, child) ?? + defaultError; } return LayoutBuilder( builder: (context, constraints) { _maybeUpdateScanWindow(value, constraints); - final Widget? overlay = widget.overlayBuilder?.call(context, constraints); + final Widget? overlay = + widget.overlayBuilder?.call(context, constraints); final Size cameraPreviewSize = value.size; final Widget scannerWidget = ClipRect( diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 5f96be432..c808ac171 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -25,8 +25,12 @@ class MobileScannerController extends ValueNotifier { this.returnImage = false, this.torchEnabled = false, this.useNewCameraSelector = false, - }) : detectionTimeoutMs = detectionSpeed == DetectionSpeed.normal ? detectionTimeoutMs : 0, - assert(detectionTimeoutMs >= 0, 'The detection timeout must be greater than or equal to 0.'), + }) : detectionTimeoutMs = + detectionSpeed == DetectionSpeed.normal ? detectionTimeoutMs : 0, + assert( + detectionTimeoutMs >= 0, + 'The detection timeout must be greater than or equal to 0.', + ), super(MobileScannerState.uninitialized(facing)); /// The desired resolution for the camera. @@ -90,7 +94,8 @@ class MobileScannerController extends ValueNotifier { final bool useNewCameraSelector; /// The internal barcode controller, that listens for detected barcodes. - final StreamController _barcodesController = StreamController.broadcast(); + final StreamController _barcodesController = + StreamController.broadcast(); /// Get the stream of scanned barcodes. Stream get barcodes => _barcodesController.stream; @@ -112,17 +117,20 @@ class MobileScannerController extends ValueNotifier { } void _setupListeners() { - _barcodesSubscription = MobileScannerPlatform.instance.barcodesStream.listen((BarcodeCapture? barcode) { + _barcodesSubscription = MobileScannerPlatform.instance.barcodesStream + .listen((BarcodeCapture? barcode) { if (barcode != null) { _barcodesController.add(barcode); } }); - _torchStateSubscription = MobileScannerPlatform.instance.torchStateStream.listen((TorchState torchState) { + _torchStateSubscription = MobileScannerPlatform.instance.torchStateStream + .listen((TorchState torchState) { value = value.copyWith(torchState: torchState); }); - _zoomScaleSubscription = MobileScannerPlatform.instance.zoomScaleStateStream.listen((double zoomScale) { + _zoomScaleSubscription = MobileScannerPlatform.instance.zoomScaleStateStream + .listen((double zoomScale) { value = value.copyWith(zoomScale: zoomScale); }); } @@ -141,7 +149,8 @@ class MobileScannerController extends ValueNotifier { throw const MobileScannerException( errorCode: MobileScannerErrorCode.controllerDisposed, errorDetails: MobileScannerErrorDetails( - message: 'The MobileScannerController was used after it has been disposed.', + message: + 'The MobileScannerController was used after it has been disposed.', ), ); } @@ -203,7 +212,8 @@ class MobileScannerController extends ValueNotifier { throw const MobileScannerException( errorCode: MobileScannerErrorCode.controllerDisposed, errorDetails: MobileScannerErrorDetails( - message: 'The MobileScannerController was used after it has been disposed.', + message: + 'The MobileScannerController was used after it has been disposed.', ), ); } @@ -223,7 +233,8 @@ class MobileScannerController extends ValueNotifier { try { _setupListeners(); - final MobileScannerViewAttributes viewAttributes = await MobileScannerPlatform.instance.start( + final MobileScannerViewAttributes viewAttributes = + await MobileScannerPlatform.instance.start( options, ); @@ -275,7 +286,9 @@ class MobileScannerController extends ValueNotifier { final CameraFacing cameraDirection = value.cameraDirection; await start( - cameraDirection: cameraDirection == CameraFacing.front ? CameraFacing.back : CameraFacing.front, + cameraDirection: cameraDirection == CameraFacing.front + ? CameraFacing.back + : CameraFacing.front, ); } @@ -291,7 +304,8 @@ class MobileScannerController extends ValueNotifier { return; } - final TorchState newState = torchState == TorchState.off ? TorchState.on : TorchState.off; + final TorchState newState = + torchState == TorchState.off ? TorchState.on : TorchState.off; // Update the torch state to the new state. // When the platform has updated the torch state, diff --git a/test/enums/mobile_scanner_authorization_state_test.dart b/test/enums/mobile_scanner_authorization_state_test.dart index 3f868cdc4..5aba56d96 100644 --- a/test/enums/mobile_scanner_authorization_state_test.dart +++ b/test/enums/mobile_scanner_authorization_state_test.dart @@ -10,8 +10,10 @@ void main() { 2: MobileScannerAuthorizationState.denied, }; - for (final MapEntry entry in values.entries) { - final MobileScannerAuthorizationState result = MobileScannerAuthorizationState.fromRawValue( + for (final MapEntry entry + in values.entries) { + final MobileScannerAuthorizationState result = + MobileScannerAuthorizationState.fromRawValue( entry.key, ); @@ -40,7 +42,8 @@ void main() { MobileScannerAuthorizationState.denied: 2, }; - for (final MapEntry entry in values.entries) { + for (final MapEntry entry + in values.entries) { final int result = entry.key.rawValue; expect(result, entry.value); From c941a4cb2739307d207e91dcdfa49723b9788853 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 9 Nov 2023 10:13:43 +0100 Subject: [PATCH 048/161] add helper widgets for the example --- example/lib/scanner_button_widgets.dart | 164 ++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 example/lib/scanner_button_widgets.dart diff --git a/example/lib/scanner_button_widgets.dart b/example/lib/scanner_button_widgets.dart new file mode 100644 index 000000000..a8b2f2130 --- /dev/null +++ b/example/lib/scanner_button_widgets.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +class AnalyzeImageFromGalleryButton extends StatelessWidget { + const AnalyzeImageFromGalleryButton({required this.controller, super.key}); + + final MobileScannerController controller; + + @override + Widget build(BuildContext context) { + return IconButton( + color: Colors.white, + icon: const Icon(Icons.image), + iconSize: 32.0, + onPressed: () async { + final ImagePicker picker = ImagePicker(); + + final XFile? image = await picker.pickImage( + source: ImageSource.gallery, + ); + + if (image == null) { + return; + } + + final BarcodeCapture? barcodes = await controller.analyzeImage( + image.path, + ); + + if (!context.mounted) { + return; + } + + final SnackBar snackbar = barcodes != null + ? const SnackBar( + content: Text('Barcode found!'), + backgroundColor: Colors.green, + ) + : const SnackBar( + content: Text('No barcode found!'), + backgroundColor: Colors.red, + ); + + ScaffoldMessenger.of(context).showSnackBar(snackbar); + }, + ); + } +} + +class StartStopMobileScannerButton extends StatelessWidget { + const StartStopMobileScannerButton({required this.controller, super.key}); + + final MobileScannerController controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, state, child) { + if (!state.isInitialized || !state.isRunning) { + return IconButton( + color: Colors.white, + icon: const Icon(Icons.play_arrow), + iconSize: 32.0, + onPressed: () async { + await controller.start(); + }, + ); + } + + return IconButton( + color: Colors.white, + icon: const Icon(Icons.stop), + iconSize: 32.0, + onPressed: () async { + await controller.stop(); + }, + ); + }, + ); + } +} + +class SwitchCameraButton extends StatelessWidget { + const SwitchCameraButton({required this.controller, super.key}); + + final MobileScannerController controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, state, child) { + if (!state.isInitialized || !state.isRunning) { + return const SizedBox.shrink(); + } + + final Widget icon; + + switch (state.cameraDirection) { + case CameraFacing.front: + icon = const Icon(Icons.camera_front); + break; + case CameraFacing.back: + icon = const Icon(Icons.camera_rear); + break; + } + + return IconButton( + iconSize: 32.0, + icon: icon, + onPressed: () async { + await controller.switchCamera(); + }, + ); + }, + ); + } +} + +class ToggleFlashlightButton extends StatelessWidget { + const ToggleFlashlightButton({required this.controller, super.key}); + + final MobileScannerController controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, state, child) { + if (!state.isInitialized || !state.isRunning) { + return const SizedBox.shrink(); + } + + switch (state.torchState) { + case TorchState.off: + return IconButton( + color: Colors.white, + iconSize: 32.0, + icon: const Icon(Icons.flash_off), + onPressed: () async { + await controller.toggleTorch(); + }, + ); + case TorchState.on: + return IconButton( + color: Colors.white, + iconSize: 32.0, + icon: const Icon(Icons.flash_on), + onPressed: () async { + await controller.toggleTorch(); + }, + ); + case TorchState.unavailable: + return const Icon( + Icons.no_flash, + color: Colors.grey, + ); + } + }, + ); + } +} From 7975e22771b8038d033c145d7320d953fe5cc8fa Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 9 Nov 2023 10:16:28 +0100 Subject: [PATCH 049/161] clean up the zoom slider example --- example/lib/barcode_scanner_zoom.dart | 202 ++++++++++---------------- 1 file changed, 75 insertions(+), 127 deletions(-) diff --git a/example/lib/barcode_scanner_zoom.dart b/example/lib/barcode_scanner_zoom.dart index 665968b2d..92fb4025d 100644 --- a/example/lib/barcode_scanner_zoom.dart +++ b/example/lib/barcode_scanner_zoom.dart @@ -1,7 +1,9 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; -import 'package:image_picker/image_picker.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mobile_scanner_example/scanner_button_widgets.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; class BarcodeScannerWithZoom extends StatefulWidget { @@ -15,13 +17,71 @@ class _BarcodeScannerWithZoomState extends State with SingleTickerProviderStateMixin { BarcodeCapture? barcode; - MobileScannerController controller = MobileScannerController( + final MobileScannerController controller = MobileScannerController( torchEnabled: true, ); - bool isStarted = true; double _zoomFactor = 0.0; + StreamSubscription? _barcodesSubscription; + + @override + void initState() { + super.initState(); + _barcodesSubscription = controller.barcodes.listen((event) { + setState(() { + barcode = event; + }); + }); + + controller.start(); + } + + Widget _buildZoomScaleSlider() { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, state, child) { + if (!state.isInitialized || !state.isRunning) { + return const SizedBox.shrink(); + } + + final TextStyle labelStyle = Theme.of(context) + .textTheme + .headlineMedium! + .copyWith(color: Colors.white); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + children: [ + Text( + '0%', + overflow: TextOverflow.fade, + style: labelStyle, + ), + Expanded( + child: Slider( + value: _zoomFactor, + onChanged: (value) { + setState(() { + _zoomFactor = value; + controller.setZoomScale(value); + }); + }, + ), + ), + Text( + '100%', + overflow: TextOverflow.fade, + style: labelStyle, + ), + ], + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -37,11 +97,6 @@ class _BarcodeScannerWithZoomState extends State errorBuilder: (context, error, child) { return ScannerErrorWidget(error: error); }, - onDetect: (barcode) { - setState(() { - this.barcode = barcode; - }); - }, ), Align( alignment: Alignment.bottomCenter, @@ -51,81 +106,12 @@ class _BarcodeScannerWithZoomState extends State color: Colors.black.withOpacity(0.4), child: Column( children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Row( - children: [ - Text( - "0%", - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), - Expanded( - child: Slider( - max: 100, - divisions: 100, - value: _zoomFactor, - label: "${_zoomFactor.round()} %", - onChanged: (value) { - setState(() { - _zoomFactor = value; - controller.setZoomScale(value); - }); - }, - ), - ), - Text( - "100%", - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), - ], - ), - ), + _buildZoomScaleSlider(), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: controller.torchState, - builder: (context, state, child) { - switch (state) { - case TorchState.off: - return const Icon( - Icons.flash_off, - color: Colors.grey, - ); - case TorchState.on: - return const Icon( - Icons.flash_on, - color: Colors.yellow, - ); - } - }, - ), - iconSize: 32.0, - onPressed: () => controller.toggleTorch(), - ), - IconButton( - color: Colors.white, - icon: isStarted - ? const Icon(Icons.stop) - : const Icon(Icons.play_arrow), - iconSize: 32.0, - onPressed: () => setState(() { - isStarted - ? controller.stop() - : controller.start(); - isStarted = !isStarted; - }), - ), + ToggleFlashlightButton(controller: controller), + StartStopMobileScannerButton(controller: controller), Center( child: SizedBox( width: MediaQuery.of(context).size.width - 200, @@ -143,53 +129,8 @@ class _BarcodeScannerWithZoomState extends State ), ), ), - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: controller.cameraFacingState, - builder: (context, state, child) { - switch (state) { - case CameraFacing.front: - return const Icon(Icons.camera_front); - case CameraFacing.back: - return const Icon(Icons.camera_rear); - } - }, - ), - iconSize: 32.0, - onPressed: () => controller.switchCamera(), - ), - IconButton( - color: Colors.white, - icon: const Icon(Icons.image), - iconSize: 32.0, - onPressed: () async { - final ImagePicker picker = ImagePicker(); - // Pick an image - final XFile? image = await picker.pickImage( - source: ImageSource.gallery, - ); - if (image != null) { - if (await controller.analyzeImage(image.path)) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Barcode found!'), - backgroundColor: Colors.green, - ), - ); - } else { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('No barcode found!'), - backgroundColor: Colors.red, - ), - ); - } - } - }, - ), + SwitchCameraButton(controller: controller), + AnalyzeImageFromGalleryButton(controller: controller), ], ), ], @@ -202,4 +143,11 @@ class _BarcodeScannerWithZoomState extends State ), ); } + + @override + Future dispose() async { + _barcodesSubscription?.cancel(); + await controller.dispose(); + super.dispose(); + } } From e7d6b2b3a18c1cdb83d55027a344498e85cad7db Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 9 Nov 2023 10:20:56 +0100 Subject: [PATCH 050/161] fix bug with context mounted --- example/lib/barcode_scanner_zoom.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/example/lib/barcode_scanner_zoom.dart b/example/lib/barcode_scanner_zoom.dart index 92fb4025d..62194d46d 100644 --- a/example/lib/barcode_scanner_zoom.dart +++ b/example/lib/barcode_scanner_zoom.dart @@ -29,6 +29,10 @@ class _BarcodeScannerWithZoomState extends State void initState() { super.initState(); _barcodesSubscription = controller.barcodes.listen((event) { + if (!context.mounted) { + return; + } + setState(() { barcode = event; }); From 1458ae768975a8bf3478b4c4cea74bda0c820a2c Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 9 Nov 2023 10:26:11 +0100 Subject: [PATCH 051/161] fix doc --- lib/src/mobile_scanner_controller.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index c808ac171..e3b40a4c5 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -324,6 +324,9 @@ class MobileScannerController extends ValueNotifier { await MobileScannerPlatform.instance.updateScanWindow(window); } + /// Dispose the controller. + /// + /// Once the controller is disposed, it cannot be used anymore. @override Future dispose() async { if (_isDisposed) { From fc87d1a9ee210d3caa092f279654aad5fb87b77f Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 9 Nov 2023 10:27:40 +0100 Subject: [PATCH 052/161] fix the barcode scanner controller example --- example/lib/barcode_scanner_controller.dart | 143 ++++---------------- 1 file changed, 26 insertions(+), 117 deletions(-) diff --git a/example/lib/barcode_scanner_controller.dart b/example/lib/barcode_scanner_controller.dart index 8aac24bbf..7410d2de4 100644 --- a/example/lib/barcode_scanner_controller.dart +++ b/example/lib/barcode_scanner_controller.dart @@ -1,6 +1,8 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; -import 'package:image_picker/image_picker.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mobile_scanner_example/scanner_button_widgets.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; class BarcodeScannerWithController extends StatefulWidget { @@ -25,29 +27,23 @@ class _BarcodeScannerWithControllerState // returnImage: false, ); - bool isStarted = true; + StreamSubscription? _barcodesSubscription; - void _startOrStop() { - try { - if (isStarted) { - controller.stop(); - } else { - controller.start(); + @override + void initState() { + super.initState(); + _barcodesSubscription = controller.barcodes.listen((event) { + if (!context.mounted) { + return; } + setState(() { - isStarted = !isStarted; + barcode = event; }); - } on Exception catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Something went wrong! $e'), - backgroundColor: Colors.red, - ), - ); - } - } + }); - int? numberOfCameras; + controller.start(); + } @override Widget build(BuildContext context) { @@ -59,22 +55,11 @@ class _BarcodeScannerWithControllerState return Stack( children: [ MobileScanner( - onScannerStarted: (arguments) { - if (mounted && arguments?.numberOfCameras != null) { - numberOfCameras = arguments!.numberOfCameras; - setState(() {}); - } - }, controller: controller, errorBuilder: (context, error, child) { return ScannerErrorWidget(error: error); }, fit: BoxFit.contain, - onDetect: (barcode) { - setState(() { - this.barcode = barcode; - }); - }, ), Align( alignment: Alignment.bottomCenter, @@ -85,44 +70,8 @@ class _BarcodeScannerWithControllerState child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - ValueListenableBuilder( - valueListenable: controller.hasTorchState, - builder: (context, state, child) { - if (state != true) { - return const SizedBox.shrink(); - } - return IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: controller.torchState, - builder: (context, state, child) { - switch (state) { - case TorchState.off: - return const Icon( - Icons.flash_off, - color: Colors.grey, - ); - case TorchState.on: - return const Icon( - Icons.flash_on, - color: Colors.yellow, - ); - } - }, - ), - iconSize: 32.0, - onPressed: () => controller.toggleTorch(), - ); - }, - ), - IconButton( - color: Colors.white, - icon: isStarted - ? const Icon(Icons.stop) - : const Icon(Icons.play_arrow), - iconSize: 32.0, - onPressed: _startOrStop, - ), + ToggleFlashlightButton(controller: controller), + StartStopMobileScannerButton(controller: controller), Center( child: SizedBox( width: MediaQuery.of(context).size.width - 200, @@ -140,55 +89,8 @@ class _BarcodeScannerWithControllerState ), ), ), - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: controller.cameraFacingState, - builder: (context, state, child) { - switch (state) { - case CameraFacing.front: - return const Icon(Icons.camera_front); - case CameraFacing.back: - return const Icon(Icons.camera_rear); - } - }, - ), - iconSize: 32.0, - onPressed: (numberOfCameras ?? 0) < 2 - ? null - : () => controller.switchCamera(), - ), - IconButton( - color: Colors.white, - icon: const Icon(Icons.image), - iconSize: 32.0, - onPressed: () async { - final ImagePicker picker = ImagePicker(); - // Pick an image - final XFile? image = await picker.pickImage( - source: ImageSource.gallery, - ); - if (image != null) { - if (await controller.analyzeImage(image.path)) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Barcode found!'), - backgroundColor: Colors.green, - ), - ); - } else { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('No barcode found!'), - backgroundColor: Colors.red, - ), - ); - } - } - }, - ), + SwitchCameraButton(controller: controller), + AnalyzeImageFromGalleryButton(controller: controller), ], ), ), @@ -199,4 +101,11 @@ class _BarcodeScannerWithControllerState ), ); } + + @override + Future dispose() async { + _barcodesSubscription?.cancel(); + await controller.dispose(); + super.dispose(); + } } From b38d36a935e0e3832c7b457d70a8a2b1ae949dc6 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 9 Nov 2023 10:36:35 +0100 Subject: [PATCH 053/161] remove builder from the zoom slider example --- example/lib/barcode_scanner_zoom.dart | 88 +++++++++++++-------------- 1 file changed, 42 insertions(+), 46 deletions(-) diff --git a/example/lib/barcode_scanner_zoom.dart b/example/lib/barcode_scanner_zoom.dart index 62194d46d..f43493bf3 100644 --- a/example/lib/barcode_scanner_zoom.dart +++ b/example/lib/barcode_scanner_zoom.dart @@ -91,59 +91,55 @@ class _BarcodeScannerWithZoomState extends State return Scaffold( appBar: AppBar(title: const Text('With zoom slider')), backgroundColor: Colors.black, - body: Builder( - builder: (context) { - return Stack( - children: [ - MobileScanner( - controller: controller, - fit: BoxFit.contain, - errorBuilder: (context, error, child) { - return ScannerErrorWidget(error: error); - }, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - alignment: Alignment.bottomCenter, - height: 100, - color: Colors.black.withOpacity(0.4), - child: Column( + body: Stack( + children: [ + MobileScanner( + controller: controller, + fit: BoxFit.contain, + errorBuilder: (context, error, child) { + return ScannerErrorWidget(error: error); + }, + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + alignment: Alignment.bottomCenter, + height: 100, + color: Colors.black.withOpacity(0.4), + child: Column( + children: [ + _buildZoomScaleSlider(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _buildZoomScaleSlider(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ToggleFlashlightButton(controller: controller), - StartStopMobileScannerButton(controller: controller), - Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 200, - height: 50, - child: FittedBox( - child: Text( - barcode?.barcodes.first.rawValue ?? - 'Scan something!', - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), - ), + ToggleFlashlightButton(controller: controller), + StartStopMobileScannerButton(controller: controller), + Center( + child: SizedBox( + width: MediaQuery.of(context).size.width - 200, + height: 50, + child: FittedBox( + child: Text( + barcode?.barcodes.first.rawValue ?? + 'Scan something!', + overflow: TextOverflow.fade, + style: Theme.of(context) + .textTheme + .headlineMedium! + .copyWith(color: Colors.white), ), ), - SwitchCameraButton(controller: controller), - AnalyzeImageFromGalleryButton(controller: controller), - ], + ), ), + SwitchCameraButton(controller: controller), + AnalyzeImageFromGalleryButton(controller: controller), ], ), - ), + ], ), - ], - ); - }, + ), + ), + ], ), ); } From 5d7d58558bd759179cb472627fbdc0b0a2d1baf8 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 9 Nov 2023 10:38:10 +0100 Subject: [PATCH 054/161] remove obsolete sample --- .../barcode_scanner_without_controller.dart | 74 ------------------- example/lib/main.dart | 12 --- 2 files changed, 86 deletions(-) delete mode 100644 example/lib/barcode_scanner_without_controller.dart diff --git a/example/lib/barcode_scanner_without_controller.dart b/example/lib/barcode_scanner_without_controller.dart deleted file mode 100644 index fe4c84252..000000000 --- a/example/lib/barcode_scanner_without_controller.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; -import 'package:mobile_scanner_example/scanner_error_widget.dart'; - -class BarcodeScannerWithoutController extends StatefulWidget { - const BarcodeScannerWithoutController({super.key}); - - @override - State createState() => - _BarcodeScannerWithoutControllerState(); -} - -class _BarcodeScannerWithoutControllerState - extends State - with SingleTickerProviderStateMixin { - BarcodeCapture? capture; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Without controller')), - backgroundColor: Colors.black, - body: Builder( - builder: (context) { - return Stack( - children: [ - MobileScanner( - fit: BoxFit.contain, - errorBuilder: (context, error, child) { - return ScannerErrorWidget(error: error); - }, - onDetect: (capture) { - setState(() { - this.capture = capture; - }); - }, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - alignment: Alignment.bottomCenter, - height: 100, - color: Colors.black.withOpacity(0.4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 120, - height: 50, - child: FittedBox( - child: Text( - capture?.barcodes.first.rawValue ?? - 'Scan something!', - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), - ), - ), - ), - ], - ), - ), - ), - ], - ); - }, - ), - ); - } -} diff --git a/example/lib/main.dart b/example/lib/main.dart index c0c1a7b51..190b246e7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,7 +4,6 @@ import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; import 'package:mobile_scanner_example/barcode_scanner_pageview.dart'; import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart'; import 'package:mobile_scanner_example/barcode_scanner_window.dart'; -import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; import 'package:mobile_scanner_example/barcode_scanner_zoom.dart'; import 'package:mobile_scanner_example/mobile_scanner_overlay.dart'; @@ -72,17 +71,6 @@ class MyHome extends StatelessWidget { child: const Text('MobileScanner with Controller (returning image)'), ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - const BarcodeScannerWithoutController(), - ), - ); - }, - child: const Text('MobileScanner without Controller'), - ), ElevatedButton( onPressed: () { Navigator.of(context).push( From 0ac899bdc0470f5c88192733ee29a8d23d9dfb82 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 9 Nov 2023 10:40:59 +0100 Subject: [PATCH 055/161] remove the builder from the controller sample --- example/lib/barcode_scanner_controller.dart | 83 ++++++++++----------- 1 file changed, 39 insertions(+), 44 deletions(-) diff --git a/example/lib/barcode_scanner_controller.dart b/example/lib/barcode_scanner_controller.dart index 7410d2de4..71b84ebae 100644 --- a/example/lib/barcode_scanner_controller.dart +++ b/example/lib/barcode_scanner_controller.dart @@ -50,54 +50,49 @@ class _BarcodeScannerWithControllerState return Scaffold( appBar: AppBar(title: const Text('With controller')), backgroundColor: Colors.black, - body: Builder( - builder: (context) { - return Stack( - children: [ - MobileScanner( - controller: controller, - errorBuilder: (context, error, child) { - return ScannerErrorWidget(error: error); - }, - fit: BoxFit.contain, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - alignment: Alignment.bottomCenter, - height: 100, - color: Colors.black.withOpacity(0.4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ToggleFlashlightButton(controller: controller), - StartStopMobileScannerButton(controller: controller), - Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 200, - height: 50, - child: FittedBox( - child: Text( - barcode?.barcodes.first.rawValue ?? - 'Scan something!', - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), - ), + body: Stack( + children: [ + MobileScanner( + controller: controller, + errorBuilder: (context, error, child) { + return ScannerErrorWidget(error: error); + }, + fit: BoxFit.contain, + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + alignment: Alignment.bottomCenter, + height: 100, + color: Colors.black.withOpacity(0.4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ToggleFlashlightButton(controller: controller), + StartStopMobileScannerButton(controller: controller), + Center( + child: SizedBox( + width: MediaQuery.of(context).size.width - 200, + height: 50, + child: FittedBox( + child: Text( + barcode?.barcodes.first.rawValue ?? 'Scan something!', + overflow: TextOverflow.fade, + style: Theme.of(context) + .textTheme + .headlineMedium! + .copyWith(color: Colors.white), ), ), - SwitchCameraButton(controller: controller), - AnalyzeImageFromGalleryButton(controller: controller), - ], + ), ), - ), + SwitchCameraButton(controller: controller), + AnalyzeImageFromGalleryButton(controller: controller), + ], ), - ], - ); - }, + ), + ), + ], ), ); } From 87d0bf71e9522182e545e15fd4789dfe3811a7dc Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 9 Nov 2023 11:07:53 +0100 Subject: [PATCH 056/161] refactor a sample to use a list view instead --- .../lib/barcode_list_scanner_controller.dart | 192 ------------------ example/lib/barcode_scanner_listview.dart | 113 +++++++++++ example/lib/main.dart | 7 +- 3 files changed, 116 insertions(+), 196 deletions(-) delete mode 100644 example/lib/barcode_list_scanner_controller.dart create mode 100644 example/lib/barcode_scanner_listview.dart diff --git a/example/lib/barcode_list_scanner_controller.dart b/example/lib/barcode_list_scanner_controller.dart deleted file mode 100644 index e1a4f39aa..000000000 --- a/example/lib/barcode_list_scanner_controller.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; -import 'package:mobile_scanner_example/scanner_error_widget.dart'; - -class BarcodeListScannerWithController extends StatefulWidget { - const BarcodeListScannerWithController({super.key}); - - @override - State createState() => - _BarcodeListScannerWithControllerState(); -} - -class _BarcodeListScannerWithControllerState - extends State - with SingleTickerProviderStateMixin { - BarcodeCapture? barcodeCapture; - - final MobileScannerController controller = MobileScannerController( - torchEnabled: true, - // formats: [BarcodeFormat.qrCode] - // facing: CameraFacing.front, - // detectionSpeed: DetectionSpeed.normal - // detectionTimeoutMs: 1000, - // returnImage: false, - ); - - bool isStarted = true; - - void _startOrStop() { - try { - if (isStarted) { - controller.stop(); - } else { - controller.start(); - } - setState(() { - isStarted = !isStarted; - }); - } on Exception catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Something went wrong! $e'), - backgroundColor: Colors.red, - ), - ); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('With ValueListenableBuilder')), - backgroundColor: Colors.black, - body: Builder( - builder: (context) { - return Stack( - children: [ - MobileScanner( - controller: controller, - errorBuilder: (context, error, child) { - return ScannerErrorWidget(error: error); - }, - fit: BoxFit.contain, - onDetect: (barcodeCapture) { - setState(() { - this.barcodeCapture = barcodeCapture; - }); - }, - onScannerStarted: (arguments) { - // Do something with arguments. - }, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - alignment: Alignment.bottomCenter, - height: 100, - color: Colors.black.withOpacity(0.4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: controller.torchState, - builder: (context, state, child) { - switch (state) { - case TorchState.off: - return const Icon( - Icons.flash_off, - color: Colors.grey, - ); - case TorchState.on: - return const Icon( - Icons.flash_on, - color: Colors.yellow, - ); - } - }, - ), - iconSize: 32.0, - onPressed: () => controller.toggleTorch(), - ), - IconButton( - color: Colors.white, - icon: isStarted - ? const Icon(Icons.stop) - : const Icon(Icons.play_arrow), - iconSize: 32.0, - onPressed: _startOrStop, - ), - Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 200, - height: 50, - child: FittedBox( - child: Text( - '${barcodeCapture?.barcodes.map((e) => e.rawValue) ?? 'Scan something!'}', - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), - ), - ), - ), - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: controller.cameraFacingState, - builder: (context, state, child) { - switch (state) { - case CameraFacing.front: - return const Icon(Icons.camera_front); - case CameraFacing.back: - return const Icon(Icons.camera_rear); - } - }, - ), - iconSize: 32.0, - onPressed: () => controller.switchCamera(), - ), - IconButton( - color: Colors.white, - icon: const Icon(Icons.image), - iconSize: 32.0, - onPressed: () async { - final ImagePicker picker = ImagePicker(); - // Pick an image - final XFile? image = await picker.pickImage( - source: ImageSource.gallery, - ); - if (image != null) { - if (await controller.analyzeImage(image.path)) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Barcode found!'), - backgroundColor: Colors.green, - ), - ); - } else { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('No barcode found!'), - backgroundColor: Colors.red, - ), - ); - } - } - }, - ), - ], - ), - ), - ), - ], - ); - }, - ), - ); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } -} diff --git a/example/lib/barcode_scanner_listview.dart b/example/lib/barcode_scanner_listview.dart new file mode 100644 index 000000000..5c65607a7 --- /dev/null +++ b/example/lib/barcode_scanner_listview.dart @@ -0,0 +1,113 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mobile_scanner_example/scanner_button_widgets.dart'; +import 'package:mobile_scanner_example/scanner_error_widget.dart'; + +class BarcodeScannerListView extends StatefulWidget { + const BarcodeScannerListView({super.key}); + + @override + State createState() => _BarcodeScannerListViewState(); +} + +class _BarcodeScannerListViewState extends State { + final MobileScannerController controller = MobileScannerController( + torchEnabled: true, + // formats: [BarcodeFormat.qrCode] + // facing: CameraFacing.front, + // detectionSpeed: DetectionSpeed.normal + // detectionTimeoutMs: 1000, + // returnImage: false, + ); + + @override + void initState() { + super.initState(); + + controller.start(); + } + + Widget _buildBarcodesListView() { + return StreamBuilder( + stream: controller.barcodes, + builder: (context, snapshot) { + final barcodes = snapshot.data?.barcodes; + + if (barcodes == null || barcodes.isEmpty) { + return const Center( + child: Text( + 'Scan Something!', + style: TextStyle(color: Colors.white, fontSize: 20), + ), + ); + } + + return ListView.builder( + itemCount: barcodes.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + barcodes[index].rawValue ?? 'No raw value', + overflow: TextOverflow.fade, + style: const TextStyle(color: Colors.white), + ), + ); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('With ListView')), + backgroundColor: Colors.black, + body: Stack( + children: [ + MobileScanner( + controller: controller, + errorBuilder: (context, error, child) { + return ScannerErrorWidget(error: error); + }, + fit: BoxFit.contain, + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + alignment: Alignment.bottomCenter, + height: 100, + color: Colors.black.withOpacity(0.4), + child: Column( + children: [ + Expanded( + child: _buildBarcodesListView(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ToggleFlashlightButton(controller: controller), + StartStopMobileScannerButton(controller: controller), + const Spacer(), + SwitchCameraButton(controller: controller), + AnalyzeImageFromGalleryButton(controller: controller), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } + + @override + Future dispose() async { + await controller.dispose(); + super.dispose(); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 190b246e7..4ea56344d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart'; import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; +import 'package:mobile_scanner_example/barcode_scanner_listview.dart'; import 'package:mobile_scanner_example/barcode_scanner_pageview.dart'; import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart'; import 'package:mobile_scanner_example/barcode_scanner_window.dart'; @@ -33,12 +33,11 @@ class MyHome extends StatelessWidget { onPressed: () { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => - const BarcodeListScannerWithController(), + builder: (context) => const BarcodeScannerListView(), ), ); }, - child: const Text('MobileScanner with List Controller'), + child: const Text('MobileScanner with ListView'), ), ElevatedButton( onPressed: () { From 726de01d35876532e82e4ed98ba6a36fc7420735 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 9 Nov 2023 11:08:18 +0100 Subject: [PATCH 057/161] remove unused ticker providers --- example/lib/barcode_scanner_controller.dart | 3 +-- example/lib/barcode_scanner_zoom.dart | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/example/lib/barcode_scanner_controller.dart b/example/lib/barcode_scanner_controller.dart index 71b84ebae..22dc32cfb 100644 --- a/example/lib/barcode_scanner_controller.dart +++ b/example/lib/barcode_scanner_controller.dart @@ -14,8 +14,7 @@ class BarcodeScannerWithController extends StatefulWidget { } class _BarcodeScannerWithControllerState - extends State - with SingleTickerProviderStateMixin { + extends State { BarcodeCapture? barcode; final MobileScannerController controller = MobileScannerController( diff --git a/example/lib/barcode_scanner_zoom.dart b/example/lib/barcode_scanner_zoom.dart index f43493bf3..c52a602ec 100644 --- a/example/lib/barcode_scanner_zoom.dart +++ b/example/lib/barcode_scanner_zoom.dart @@ -13,8 +13,7 @@ class BarcodeScannerWithZoom extends StatefulWidget { State createState() => _BarcodeScannerWithZoomState(); } -class _BarcodeScannerWithZoomState extends State - with SingleTickerProviderStateMixin { +class _BarcodeScannerWithZoomState extends State { BarcodeCapture? barcode; final MobileScannerController controller = MobileScannerController( From 1d68f6b2051b8f19cd1352d2d5fb2349de5e660b Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 9 Nov 2023 11:44:10 +0100 Subject: [PATCH 058/161] fix the pageview sample --- example/lib/barcode_scanner_pageview.dart | 148 +++++++++++++--------- 1 file changed, 88 insertions(+), 60 deletions(-) diff --git a/example/lib/barcode_scanner_pageview.dart b/example/lib/barcode_scanner_pageview.dart index b06cf755e..f510b24dc 100644 --- a/example/lib/barcode_scanner_pageview.dart +++ b/example/lib/barcode_scanner_pageview.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; @@ -9,62 +11,15 @@ class BarcodeScannerPageView extends StatefulWidget { State createState() => _BarcodeScannerPageViewState(); } -class _BarcodeScannerPageViewState extends State - with SingleTickerProviderStateMixin { - BarcodeCapture? capture; - - Widget cameraView() { - return Builder( - builder: (context) { - return Stack( - children: [ - MobileScanner( - startDelay: true, - controller: MobileScannerController(torchEnabled: true), - fit: BoxFit.contain, - errorBuilder: (context, error, child) { - return ScannerErrorWidget(error: error); - }, - onDetect: (capture) { - setState(() { - this.capture = capture; - }); - }, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - alignment: Alignment.bottomCenter, - height: 100, - color: Colors.black.withOpacity(0.4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 120, - height: 50, - child: FittedBox( - child: Text( - capture?.barcodes.first.rawValue ?? - 'Scan something!', - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), - ), - ), - ), - ], - ), - ), - ), - ], - ); - }, - ); +class _BarcodeScannerPageViewState extends State { + final MobileScannerController scannerController = MobileScannerController(); + + final PageController pageController = PageController(); + + @override + void initState() { + super.initState(); + scannerController.start(); } @override @@ -73,13 +28,86 @@ class _BarcodeScannerPageViewState extends State appBar: AppBar(title: const Text('With PageView')), backgroundColor: Colors.black, body: PageView( + controller: pageController, + onPageChanged: (index) async { + // Stop the camera view for the current page, + // and then restart the camera for the new page. + await scannerController.stop(); + + // When switching pages, add a delay to the next start call. + // Otherwise the camera will start before the next page is displayed. + await Future.delayed(const Duration(seconds: 1, milliseconds: 500)); + + if (!mounted) { + return; + } + + scannerController.start(); + }, children: [ - cameraView(), - Container(), - cameraView(), - cameraView(), + _BarcodeScannerPage(controller: scannerController), + const SizedBox(), + _BarcodeScannerPage(controller: scannerController), + _BarcodeScannerPage(controller: scannerController), ], ), ); } + + @override + Future dispose() async { + await scannerController.dispose(); + pageController.dispose(); + super.dispose(); + } +} + +class _BarcodeScannerPage extends StatelessWidget { + const _BarcodeScannerPage({required this.controller}); + + final MobileScannerController controller; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + MobileScanner( + controller: controller, + fit: BoxFit.contain, + errorBuilder: (context, error, child) { + return ScannerErrorWidget(error: error); + }, + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + alignment: Alignment.bottomCenter, + height: 100, + color: Colors.black.withOpacity(0.4), + child: Center( + child: StreamBuilder( + stream: controller.barcodes, + builder: (context, snapshot) { + final barcodes = snapshot.data?.barcodes; + + if (barcodes == null || barcodes.isEmpty) { + return const Text( + 'Scan Something!', + style: TextStyle(color: Colors.white, fontSize: 20), + ); + } + + return Text( + barcodes.first.rawValue ?? 'No raw value', + overflow: TextOverflow.fade, + style: const TextStyle(color: Colors.white), + ); + }, + ), + ), + ), + ), + ], + ); + } } From 18bcb68b2466ee638097e1445976430d6b78f8dd Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 9 Nov 2023 11:50:48 +0100 Subject: [PATCH 059/161] let the barcode overlay take the camera preview size as argument --- example/lib/barcode_scanner_window.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/example/lib/barcode_scanner_window.dart b/example/lib/barcode_scanner_window.dart index 974ff5053..d5629884f 100644 --- a/example/lib/barcode_scanner_window.dart +++ b/example/lib/barcode_scanner_window.dart @@ -139,15 +139,15 @@ class ScannerOverlay extends CustomPainter { class BarcodeOverlay extends CustomPainter { BarcodeOverlay({ required this.barcode, - required this.arguments, required this.boxFit, + required this.cameraPreviewSize, required this.capture, }); - final BarcodeCapture capture; final Barcode barcode; - final MobileScannerArguments arguments; final BoxFit boxFit; + final Size cameraPreviewSize; + final BarcodeCapture capture; @override void paint(Canvas canvas, Size size) { @@ -155,7 +155,7 @@ class BarcodeOverlay extends CustomPainter { return; } - final adjustedSize = applyBoxFit(boxFit, arguments.size, size); + final adjustedSize = applyBoxFit(boxFit, cameraPreviewSize, size); double verticalPadding = size.height - adjustedSize.destination.height; double horizontalPadding = size.width - adjustedSize.destination.width; @@ -178,8 +178,8 @@ class BarcodeOverlay extends CustomPainter { ratioWidth = capture.size.width / adjustedSize.destination.width; ratioHeight = capture.size.height / adjustedSize.destination.height; } else { - ratioWidth = arguments.size.width / adjustedSize.destination.width; - ratioHeight = arguments.size.height / adjustedSize.destination.height; + ratioWidth = cameraPreviewSize.width / adjustedSize.destination.width; + ratioHeight = cameraPreviewSize.height / adjustedSize.destination.height; } final List adjustedOffset = []; From b551840c5e1cf2dd035e59925b9ba8a46d370c2e Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 9 Nov 2023 11:54:45 +0100 Subject: [PATCH 060/161] let the barcode overlay take the corners & capture size as arguments --- example/lib/barcode_scanner_window.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/example/lib/barcode_scanner_window.dart b/example/lib/barcode_scanner_window.dart index d5629884f..ae96b1c19 100644 --- a/example/lib/barcode_scanner_window.dart +++ b/example/lib/barcode_scanner_window.dart @@ -138,20 +138,20 @@ class ScannerOverlay extends CustomPainter { class BarcodeOverlay extends CustomPainter { BarcodeOverlay({ - required this.barcode, + required this.barcodeCorners, + required this.barcodeSize, required this.boxFit, required this.cameraPreviewSize, - required this.capture, }); - final Barcode barcode; + final List barcodeCorners; + final Size barcodeSize; final BoxFit boxFit; final Size cameraPreviewSize; - final BarcodeCapture capture; @override void paint(Canvas canvas, Size size) { - if (barcode.corners.isEmpty) { + if (barcodeCorners.isEmpty) { return; } @@ -175,15 +175,15 @@ class BarcodeOverlay extends CustomPainter { final double ratioHeight; if (!kIsWeb && Platform.isIOS) { - ratioWidth = capture.size.width / adjustedSize.destination.width; - ratioHeight = capture.size.height / adjustedSize.destination.height; + ratioWidth = barcodeSize.width / adjustedSize.destination.width; + ratioHeight = barcodeSize.height / adjustedSize.destination.height; } else { ratioWidth = cameraPreviewSize.width / adjustedSize.destination.width; ratioHeight = cameraPreviewSize.height / adjustedSize.destination.height; } final List adjustedOffset = []; - for (final offset in barcode.corners) { + for (final offset in barcodeCorners) { adjustedOffset.add( Offset( offset.dx / ratioWidth + horizontalPadding, From cb3364b6a6466e5f21ce0d63030e79d0943c72b1 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 9 Nov 2023 14:18:59 +0100 Subject: [PATCH 061/161] fix the return image sample --- .../lib/barcode_scanner_returning_image.dart | 183 ++++++++---------- 1 file changed, 82 insertions(+), 101 deletions(-) diff --git a/example/lib/barcode_scanner_returning_image.dart b/example/lib/barcode_scanner_returning_image.dart index 15c1912a4..ce1e7dfde 100644 --- a/example/lib/barcode_scanner_returning_image.dart +++ b/example/lib/barcode_scanner_returning_image.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mobile_scanner_example/scanner_button_widgets.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; class BarcodeScannerReturningImage extends StatefulWidget { @@ -13,11 +14,7 @@ class BarcodeScannerReturningImage extends StatefulWidget { } class _BarcodeScannerReturningImageState - extends State - with SingleTickerProviderStateMixin { - BarcodeCapture? barcode; - // MobileScannerArguments? arguments; - + extends State { final MobileScannerController controller = MobileScannerController( torchEnabled: true, // formats: [BarcodeFormat.qrCode] @@ -27,26 +24,10 @@ class _BarcodeScannerReturningImageState returnImage: true, ); - bool isStarted = true; - - void _startOrStop() { - try { - if (isStarted) { - controller.stop(); - } else { - controller.start(); - } - setState(() { - isStarted = !isStarted; - }); - } on Exception catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Something went wrong! $e'), - backgroundColor: Colors.red, - ), - ); - } + @override + void initState() { + super.initState(); + controller.start(); } @override @@ -57,20 +38,55 @@ class _BarcodeScannerReturningImageState child: Column( children: [ Expanded( - child: barcode?.image != null - ? Transform.rotate( - angle: 90 * pi / 180, - child: Image( - gaplessPlayback: true, - image: MemoryImage(barcode!.image!), - fit: BoxFit.contain, - ), - ) - : const Center( + child: StreamBuilder( + stream: controller.barcodes, + builder: (context, snapshot) { + final barcode = snapshot.data; + + if (barcode == null) { + return const Center( child: Text( 'Your scanned barcode will appear here!', ), - ), + ); + } + + final barcodeImage = barcode.image; + + if (barcodeImage == null) { + return const Center( + child: Text('No image for this barcode.'), + ); + } + + return Image.memory( + barcodeImage, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Text('Could not decode image bytes. $error'), + ); + }, + frameBuilder: ( + BuildContext context, + Widget child, + int? frame, + bool? wasSynchronouslyLoaded, + ) { + if (wasSynchronouslyLoaded == true || frame != null) { + return Transform.rotate( + angle: 90 * pi / 180, + child: child, + ); + } + + return const Center( + child: CircularProgressIndicator(), + ); + }, + ); + }, + ), ), Expanded( flex: 2, @@ -84,11 +100,6 @@ class _BarcodeScannerReturningImageState return ScannerErrorWidget(error: error); }, fit: BoxFit.contain, - onDetect: (barcode) { - setState(() { - this.barcode = barcode; - }); - }, ), Align( alignment: Alignment.bottomCenter, @@ -99,69 +110,39 @@ class _BarcodeScannerReturningImageState child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: controller.torchState, - builder: (context, state, child) { - switch (state) { - case TorchState.off: - return const Icon( - Icons.flash_off, - color: Colors.grey, - ); - case TorchState.on: - return const Icon( - Icons.flash_on, - color: Colors.yellow, - ); - } - }, - ), - iconSize: 32.0, - onPressed: () => controller.toggleTorch(), - ), - IconButton( - color: Colors.white, - icon: isStarted - ? const Icon(Icons.stop) - : const Icon(Icons.play_arrow), - iconSize: 32.0, - onPressed: _startOrStop, + ToggleFlashlightButton(controller: controller), + StartStopMobileScannerButton( + controller: controller, ), - Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 200, - height: 50, - child: FittedBox( - child: Text( - barcode?.barcodes.first.rawValue ?? + Expanded( + child: Center( + child: StreamBuilder( + stream: controller.barcodes, + builder: (context, snapshot) { + final barcodes = snapshot.data?.barcodes; + + if (barcodes == null || barcodes.isEmpty) { + return const Text( 'Scan something!', - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), + style: TextStyle( + color: Colors.white, + fontSize: 20, + ), + ); + } + + return Text( + barcodes.first.rawValue ?? 'No raw value', + overflow: TextOverflow.fade, + style: const TextStyle( + color: Colors.white, + ), + ); + }, ), ), ), - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: controller.cameraFacingState, - builder: (context, state, child) { - switch (state) { - case CameraFacing.front: - return const Icon(Icons.camera_front); - case CameraFacing.back: - return const Icon(Icons.camera_rear); - } - }, - ), - iconSize: 32.0, - onPressed: () => controller.switchCamera(), - ), + SwitchCameraButton(controller: controller), ], ), ), @@ -177,8 +158,8 @@ class _BarcodeScannerReturningImageState } @override - void dispose() { - controller.dispose(); + Future dispose() async { + await controller.dispose(); super.dispose(); } } From d19e94ab1911c68b3a7415e2c9bb1c3e32a6e0a9 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 9 Nov 2023 14:58:24 +0100 Subject: [PATCH 062/161] add sample for scan window in documentation --- lib/src/mobile_scanner.dart | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/lib/src/mobile_scanner.dart b/lib/src/mobile_scanner.dart index 1b7bd9463..c571fb52c 100644 --- a/lib/src/mobile_scanner.dart +++ b/lib/src/mobile_scanner.dart @@ -64,12 +64,34 @@ class MobileScanner extends StatefulWidget { /// If this is not null, the barcode scanner will only scan barcodes /// which intersect this rectangle. /// - /// The rectangle is relative to the layout size of the *camera preview widget*, - /// rather than the actual camera preview size, - /// since the actual widget size might not be the same as the camera preview size. + /// This rectangle is relative to the layout size + /// of the *camera preview widget* in the widget tree, + /// rather than the actual size of the camera preview output. + /// This is because the size of the camera preview widget + /// might not be the same as the size of the camera output. /// /// For example, the applied [fit] has an effect on the size of the camera preview widget, /// while the camera preview size remains the same. + /// + /// The following example shows a scan window that is centered, + /// fills half the height and one third of the width of the layout: + /// + /// ```dart + /// LayoutBuider( + /// builder: (BuildContext context, BoxConstraints constraints) { + /// final Size layoutSize = constraints.biggest; + /// + /// final double scanWindowWidth = layoutSize.width / 3; + /// final double scanWindowHeight = layoutSize.height / 2; + /// + /// final Rect scanWindow = Rect.fromCenter( + /// center: layoutSize.center(Offset.zero), + /// width: scanWindowWidth, + /// height: scanWindowHeight, + /// ); + /// } + /// ); + /// ``` final Rect? scanWindow; @override @@ -80,7 +102,9 @@ class _MobileScannerState extends State { /// The current scan window. Rect? scanWindow; - /// Recalculate the scan window based on the updated [constraints]. + /// Calculate the scan window based on the given [constraints]. + /// + /// If the [scanWindow] is already set, this method does nothing. void _maybeUpdateScanWindow( MobileScannerState scannerState, BoxConstraints constraints, From 68a7a49cd998b4db588279b8aca1526a781045b5 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 16 Nov 2023 12:43:21 +0100 Subject: [PATCH 063/161] update barcode scanner window sample --- example/lib/barcode_scanner_window.dart | 197 +++++++++++++++--------- 1 file changed, 126 insertions(+), 71 deletions(-) diff --git a/example/lib/barcode_scanner_window.dart b/example/lib/barcode_scanner_window.dart index ae96b1c19..5d4e042f1 100644 --- a/example/lib/barcode_scanner_window.dart +++ b/example/lib/barcode_scanner_window.dart @@ -16,16 +16,75 @@ class BarcodeScannerWithScanWindow extends StatefulWidget { class _BarcodeScannerWithScanWindowState extends State { - late MobileScannerController controller = MobileScannerController(); - Barcode? barcode; - BarcodeCapture? capture; + final MobileScannerController controller = MobileScannerController(); - Future onDetect(BarcodeCapture barcode) async { - capture = barcode; - setState(() => this.barcode = barcode.barcodes.first); + @override + void initState() { + super.initState(); + + controller.start(); } - MobileScannerArguments? arguments; + Widget _buildBarcodeOverlay() { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, value, child) { + // Not ready. + if (!value.isInitialized || !value.isRunning || value.error != null) { + return const SizedBox(); + } + + return StreamBuilder( + stream: controller.barcodes, + builder: (context, snapshot) { + final BarcodeCapture? barcodeCapture = snapshot.data; + + // No barcode. + if (barcodeCapture == null || barcodeCapture.barcodes.isEmpty) { + return const SizedBox(); + } + + final scannedBarcode = barcodeCapture.barcodes.first; + + // No barcode corners, or size, or no camera preview size. + if (scannedBarcode.corners.isEmpty || + value.size.isEmpty || + barcodeCapture.size.isEmpty) { + return const SizedBox(); + } + + return CustomPaint( + painter: BarcodeOverlay( + barcodeCorners: scannedBarcode.corners, + barcodeSize: barcodeCapture.size, + boxFit: BoxFit.contain, + cameraPreviewSize: value.size, + ), + ); + }, + ); + }, + ); + } + + Widget _buildScanWindow(Rect scanWindowRect) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, value, child) { + // Not ready. + if (!value.isInitialized || + !value.isRunning || + value.error != null || + value.size.isEmpty) { + return const SizedBox(); + } + + return CustomPaint( + painter: ScannerOverlay(scanWindowRect), + ); + }, + ); + } @override Widget build(BuildContext context) { @@ -34,77 +93,69 @@ class _BarcodeScannerWithScanWindowState width: 200, height: 200, ); + return Scaffold( appBar: AppBar(title: const Text('With Scan window')), backgroundColor: Colors.black, - body: Builder( - builder: (context) { - return Stack( - fit: StackFit.expand, - children: [ - MobileScanner( - fit: BoxFit.contain, - scanWindow: scanWindow, - controller: controller, - onScannerStarted: (arguments) { - setState(() { - this.arguments = arguments; - }); - }, - errorBuilder: (context, error, child) { - return ScannerErrorWidget(error: error); + body: Stack( + fit: StackFit.expand, + children: [ + MobileScanner( + fit: BoxFit.contain, + scanWindow: scanWindow, + controller: controller, + errorBuilder: (context, error, child) { + return ScannerErrorWidget(error: error); + }, + ), + _buildBarcodeOverlay(), + _buildScanWindow(scanWindow), + Align( + alignment: Alignment.bottomCenter, + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + height: 100, + color: Colors.black.withOpacity(0.4), + child: StreamBuilder( + stream: controller.barcodes, + builder: (context, snapshot) { + final barcodeCapture = snapshot.data; + + String displayValue = 'Scan something!'; + + if (barcodeCapture != null && + barcodeCapture.barcodes.isNotEmpty) { + final String? value = + barcodeCapture.barcodes.first.displayValue; + + if (value != null) { + displayValue = value; + } + } + + return Text( + displayValue, + overflow: TextOverflow.fade, + style: Theme.of(context) + .textTheme + .headlineMedium! + .copyWith(color: Colors.white), + ); }, - onDetect: onDetect, - ), - if (barcode != null && - barcode?.corners != null && - arguments != null) - CustomPaint( - painter: BarcodeOverlay( - barcode: barcode!, - arguments: arguments!, - boxFit: BoxFit.contain, - capture: capture!, - ), - ), - CustomPaint( - painter: ScannerOverlay(scanWindow), - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - alignment: Alignment.bottomCenter, - height: 100, - color: Colors.black.withOpacity(0.4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 120, - height: 50, - child: FittedBox( - child: Text( - barcode?.displayValue ?? 'Scan something!', - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), - ), - ), - ), - ], - ), - ), ), - ], - ); - }, + ), + ), + ], ), ); } + + @override + Future dispose() async { + await controller.dispose(); + super.dispose(); + } } class ScannerOverlay extends CustomPainter { @@ -114,6 +165,8 @@ class ScannerOverlay extends CustomPainter { @override void paint(Canvas canvas, Size size) { + // TODO: use `Offset.zero & size` instead of Rect.largest + // we need to pass the size to the custom paint widget final backgroundPath = Path()..addRect(Rect.largest); final cutoutPath = Path()..addRect(scanWindow); @@ -151,7 +204,9 @@ class BarcodeOverlay extends CustomPainter { @override void paint(Canvas canvas, Size size) { - if (barcodeCorners.isEmpty) { + if (barcodeCorners.isEmpty || + barcodeSize.isEmpty || + cameraPreviewSize.isEmpty) { return; } From 14972d97161f1cefdf361577e53bcb05859bcbb2 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 16 Nov 2023 15:47:40 +0100 Subject: [PATCH 064/161] add scanned barcode label widget --- example/lib/scanned_barcode_label.dart | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 example/lib/scanned_barcode_label.dart diff --git a/example/lib/scanned_barcode_label.dart b/example/lib/scanned_barcode_label.dart new file mode 100644 index 000000000..c7f168f6d --- /dev/null +++ b/example/lib/scanned_barcode_label.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +class ScannedBarcodeLabel extends StatelessWidget { + const ScannedBarcodeLabel({ + super.key, + required this.barcodes, + }); + + final Stream barcodes; + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: barcodes, + builder: (context, snaphot) { + final scannedBarcodes = snaphot.data?.barcodes ?? []; + + if (scannedBarcodes.isEmpty) { + return const Text( + 'Scan something!', + overflow: TextOverflow.fade, + style: TextStyle(color: Colors.white), + ); + } + + return Text( + scannedBarcodes.first.displayValue ?? 'No display value.', + overflow: TextOverflow.fade, + style: const TextStyle(color: Colors.white), + ); + }, + ); + } +} From da838a75b95507c7a481d8b2f53ba2ab9c84e450 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 16 Nov 2023 16:06:27 +0100 Subject: [PATCH 065/161] remove stream subscriptions & MediaQueryData.size usages --- example/lib/barcode_scanner_controller.dart | 33 +++--------------- example/lib/barcode_scanner_pageview.dart | 21 ++---------- .../lib/barcode_scanner_returning_image.dart | 26 ++------------ example/lib/barcode_scanner_window.dart | 29 ++-------------- example/lib/barcode_scanner_zoom.dart | 34 +++---------------- 5 files changed, 16 insertions(+), 127 deletions(-) diff --git a/example/lib/barcode_scanner_controller.dart b/example/lib/barcode_scanner_controller.dart index 22dc32cfb..afc74e84a 100644 --- a/example/lib/barcode_scanner_controller.dart +++ b/example/lib/barcode_scanner_controller.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mobile_scanner_example/scanned_barcode_label.dart'; import 'package:mobile_scanner_example/scanner_button_widgets.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; @@ -15,8 +16,6 @@ class BarcodeScannerWithController extends StatefulWidget { class _BarcodeScannerWithControllerState extends State { - BarcodeCapture? barcode; - final MobileScannerController controller = MobileScannerController( torchEnabled: true, useNewCameraSelector: true, // formats: [BarcodeFormat.qrCode] @@ -26,21 +25,9 @@ class _BarcodeScannerWithControllerState // returnImage: false, ); - StreamSubscription? _barcodesSubscription; - @override void initState() { super.initState(); - _barcodesSubscription = controller.barcodes.listen((event) { - if (!context.mounted) { - return; - } - - setState(() { - barcode = event; - }); - }); - controller.start(); } @@ -69,20 +56,9 @@ class _BarcodeScannerWithControllerState children: [ ToggleFlashlightButton(controller: controller), StartStopMobileScannerButton(controller: controller), - Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 200, - height: 50, - child: FittedBox( - child: Text( - barcode?.barcodes.first.rawValue ?? 'Scan something!', - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), - ), + Expanded( + child: Center( + child: ScannedBarcodeLabel(barcodes: controller.barcodes), ), ), SwitchCameraButton(controller: controller), @@ -98,7 +74,6 @@ class _BarcodeScannerWithControllerState @override Future dispose() async { - _barcodesSubscription?.cancel(); await controller.dispose(); super.dispose(); } diff --git a/example/lib/barcode_scanner_pageview.dart b/example/lib/barcode_scanner_pageview.dart index f510b24dc..88be31227 100644 --- a/example/lib/barcode_scanner_pageview.dart +++ b/example/lib/barcode_scanner_pageview.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mobile_scanner_example/scanned_barcode_label.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; class BarcodeScannerPageView extends StatefulWidget { @@ -85,25 +86,7 @@ class _BarcodeScannerPage extends StatelessWidget { height: 100, color: Colors.black.withOpacity(0.4), child: Center( - child: StreamBuilder( - stream: controller.barcodes, - builder: (context, snapshot) { - final barcodes = snapshot.data?.barcodes; - - if (barcodes == null || barcodes.isEmpty) { - return const Text( - 'Scan Something!', - style: TextStyle(color: Colors.white, fontSize: 20), - ); - } - - return Text( - barcodes.first.rawValue ?? 'No raw value', - overflow: TextOverflow.fade, - style: const TextStyle(color: Colors.white), - ); - }, - ), + child: ScannedBarcodeLabel(barcodes: controller.barcodes), ), ), ), diff --git a/example/lib/barcode_scanner_returning_image.dart b/example/lib/barcode_scanner_returning_image.dart index ce1e7dfde..13a00b58a 100644 --- a/example/lib/barcode_scanner_returning_image.dart +++ b/example/lib/barcode_scanner_returning_image.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mobile_scanner_example/scanned_barcode_label.dart'; import 'package:mobile_scanner_example/scanner_button_widgets.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; @@ -116,29 +117,8 @@ class _BarcodeScannerReturningImageState ), Expanded( child: Center( - child: StreamBuilder( - stream: controller.barcodes, - builder: (context, snapshot) { - final barcodes = snapshot.data?.barcodes; - - if (barcodes == null || barcodes.isEmpty) { - return const Text( - 'Scan something!', - style: TextStyle( - color: Colors.white, - fontSize: 20, - ), - ); - } - - return Text( - barcodes.first.rawValue ?? 'No raw value', - overflow: TextOverflow.fade, - style: const TextStyle( - color: Colors.white, - ), - ); - }, + child: ScannedBarcodeLabel( + barcodes: controller.barcodes, ), ), ), diff --git a/example/lib/barcode_scanner_window.dart b/example/lib/barcode_scanner_window.dart index 5d4e042f1..4dc409e2c 100644 --- a/example/lib/barcode_scanner_window.dart +++ b/example/lib/barcode_scanner_window.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mobile_scanner_example/scanned_barcode_label.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; @@ -117,33 +118,7 @@ class _BarcodeScannerWithScanWindowState padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), height: 100, color: Colors.black.withOpacity(0.4), - child: StreamBuilder( - stream: controller.barcodes, - builder: (context, snapshot) { - final barcodeCapture = snapshot.data; - - String displayValue = 'Scan something!'; - - if (barcodeCapture != null && - barcodeCapture.barcodes.isNotEmpty) { - final String? value = - barcodeCapture.barcodes.first.displayValue; - - if (value != null) { - displayValue = value; - } - } - - return Text( - displayValue, - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ); - }, - ), + child: ScannedBarcodeLabel(barcodes: controller.barcodes), ), ), ], diff --git a/example/lib/barcode_scanner_zoom.dart b/example/lib/barcode_scanner_zoom.dart index c52a602ec..6ac0980bc 100644 --- a/example/lib/barcode_scanner_zoom.dart +++ b/example/lib/barcode_scanner_zoom.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mobile_scanner_example/scanned_barcode_label.dart'; import 'package:mobile_scanner_example/scanner_button_widgets.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; @@ -14,29 +15,15 @@ class BarcodeScannerWithZoom extends StatefulWidget { } class _BarcodeScannerWithZoomState extends State { - BarcodeCapture? barcode; - final MobileScannerController controller = MobileScannerController( torchEnabled: true, ); double _zoomFactor = 0.0; - StreamSubscription? _barcodesSubscription; - @override void initState() { super.initState(); - _barcodesSubscription = controller.barcodes.listen((event) { - if (!context.mounted) { - return; - } - - setState(() { - barcode = event; - }); - }); - controller.start(); } @@ -113,20 +100,10 @@ class _BarcodeScannerWithZoomState extends State { children: [ ToggleFlashlightButton(controller: controller), StartStopMobileScannerButton(controller: controller), - Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 200, - height: 50, - child: FittedBox( - child: Text( - barcode?.barcodes.first.rawValue ?? - 'Scan something!', - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), + Expanded( + child: Center( + child: ScannedBarcodeLabel( + barcodes: controller.barcodes, ), ), ), @@ -145,7 +122,6 @@ class _BarcodeScannerWithZoomState extends State { @override Future dispose() async { - _barcodesSubscription?.cancel(); await controller.dispose(); super.dispose(); } From 9c01955c569534912cfd99a30eb3007d94881c56 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 16 Nov 2023 16:08:34 +0100 Subject: [PATCH 066/161] remove obsolete sized box --- example/lib/main.dart | 153 ++++++++++++++++++++---------------------- 1 file changed, 74 insertions(+), 79 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 4ea56344d..9af11aedd 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -23,85 +23,80 @@ class MyHome extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Mobile Scanner Example')), - body: SizedBox( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerListView(), - ), - ); - }, - child: const Text('MobileScanner with ListView'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerWithController(), - ), - ); - }, - child: const Text('MobileScanner with Controller'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerWithScanWindow(), - ), - ); - }, - child: const Text('MobileScanner with ScanWindow'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerReturningImage(), - ), - ); - }, - child: - const Text('MobileScanner with Controller (returning image)'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerWithZoom(), - ), - ); - }, - child: const Text('MobileScanner with zoom slider'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerPageView(), - ), - ); - }, - child: const Text('MobileScanner pageView'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => BarcodeScannerWithOverlay(), - ), - ); - }, - child: const Text('MobileScanner with Overlay'), - ), - ], - ), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const BarcodeScannerListView(), + ), + ); + }, + child: const Text('MobileScanner with ListView'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const BarcodeScannerWithController(), + ), + ); + }, + child: const Text('MobileScanner with Controller'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const BarcodeScannerWithScanWindow(), + ), + ); + }, + child: const Text('MobileScanner with ScanWindow'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const BarcodeScannerReturningImage(), + ), + ); + }, + child: const Text('MobileScanner with Controller (returning image)'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const BarcodeScannerWithZoom(), + ), + ); + }, + child: const Text('MobileScanner with zoom slider'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const BarcodeScannerPageView(), + ), + ); + }, + child: const Text('MobileScanner pageView'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => BarcodeScannerWithOverlay(), + ), + ); + }, + child: const Text('MobileScanner with Overlay'), + ), + ], ), ); } From ee94bb8be9db51f9152adf577ee89137623a4a55 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 16 Nov 2023 16:50:15 +0100 Subject: [PATCH 067/161] clean up the mobile_scanner_overlay sample --- example/lib/main.dart | 4 +- example/lib/mobile_scanner_overlay.dart | 219 ++++++++---------------- 2 files changed, 77 insertions(+), 146 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 9af11aedd..272a063f5 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -64,7 +64,9 @@ class MyHome extends StatelessWidget { ), ); }, - child: const Text('MobileScanner with Controller (returning image)'), + child: const Text( + 'MobileScanner with Controller (returning image)', + ), ), ElevatedButton( onPressed: () { diff --git a/example/lib/mobile_scanner_overlay.dart b/example/lib/mobile_scanner_overlay.dart index cff099b38..d2be5cca3 100644 --- a/example/lib/mobile_scanner_overlay.dart +++ b/example/lib/mobile_scanner_overlay.dart @@ -1,58 +1,23 @@ import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mobile_scanner_example/scanned_barcode_label.dart'; +import 'package:mobile_scanner_example/scanner_button_widgets.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; class BarcodeScannerWithOverlay extends StatefulWidget { @override - _BarcodeScannerWithOverlayState createState() => - _BarcodeScannerWithOverlayState(); + _BarcodeScannerWithOverlayState createState() => _BarcodeScannerWithOverlayState(); } class _BarcodeScannerWithOverlayState extends State { - String overlayText = "Please scan QR Code"; - bool camStarted = false; - final MobileScannerController controller = MobileScannerController( formats: const [BarcodeFormat.qrCode], - autoStart: false, ); @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - void startCamera() { - if (camStarted) { - return; - } - - controller.start().then((_) { - if (mounted) { - setState(() { - camStarted = true; - }); - } - }).catchError((Object error, StackTrace stackTrace) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Something went wrong! $error'), - backgroundColor: Colors.red, - ), - ); - } - }); - } - - void onBarcodeDetect(BarcodeCapture barcodeCapture) { - final barcode = barcodeCapture.barcodes.last; - setState(() { - overlayText = barcodeCapture.barcodes.last.displayValue ?? - barcode.rawValue ?? - 'Barcode has no displayable value'; - }); + void initState() { + super.initState(); + controller.start(); } @override @@ -64,119 +29,84 @@ class _BarcodeScannerWithOverlayState extends State { ); return Scaffold( + backgroundColor: Colors.black, appBar: AppBar( title: const Text('Scanner with Overlay Example app'), ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: camStarted - ? Stack( - fit: StackFit.expand, - children: [ - Center( - child: MobileScanner( - fit: BoxFit.contain, - onDetect: onBarcodeDetect, - overlay: Padding( - padding: const EdgeInsets.all(16.0), - child: Align( - alignment: Alignment.bottomCenter, - child: Opacity( - opacity: 0.7, - child: Text( - overlayText, - style: const TextStyle( - backgroundColor: Colors.black26, - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 24, - overflow: TextOverflow.ellipsis, - ), - maxLines: 1, - ), - ), - ), - ), - controller: controller, - scanWindow: scanWindow, - errorBuilder: (context, error, child) { - return ScannerErrorWidget(error: error); - }, - ), - ), - CustomPaint( - painter: ScannerOverlay(scanWindow), - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: Align( - alignment: Alignment.bottomCenter, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ValueListenableBuilder( - valueListenable: controller.torchState, - builder: (context, value, child) { - final Color iconColor; - - switch (value) { - case TorchState.off: - iconColor = Colors.black; - case TorchState.on: - iconColor = Colors.yellow; - } - - return IconButton( - onPressed: () => controller.toggleTorch(), - icon: Icon( - Icons.flashlight_on, - color: iconColor, - ), - ); - }, - ), - IconButton( - onPressed: () => controller.switchCamera(), - icon: const Icon( - Icons.cameraswitch_rounded, - color: Colors.white, - ), - ), - ], - ), - ), - ), - ], - ) - : const Center( - child: Text("Tap on Camera to activate QR Scanner"), - ), + body: Stack( + fit: StackFit.expand, + children: [ + Center( + child: MobileScanner( + fit: BoxFit.contain, + controller: controller, + scanWindow: scanWindow, + errorBuilder: (context, error, child) { + return ScannerErrorWidget(error: error); + }, + overlayBuilder: (context, constraints) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Align( + alignment: Alignment.bottomCenter, + child: ScannedBarcodeLabel(barcodes: controller.barcodes), + ), + ); + }, ), - ], - ), - ), - floatingActionButton: camStarted - ? null - : FloatingActionButton( - onPressed: startCamera, - child: const Icon(Icons.camera_alt), + ), + ValueListenableBuilder( + valueListenable: controller, + builder: (context, value, child) { + if (!value.isInitialized || !value.isRunning || value.error != null) { + return const SizedBox(); + } + + return CustomPaint( + painter: ScannerOverlay(scanWindow: scanWindow), + ); + }, + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ToggleFlashlightButton(controller: controller), + SwitchCameraButton(controller: controller), + ], + ), ), + ), + ], + ), ); } + + @override + Future dispose() async { + await controller.dispose(); + super.dispose(); + } } class ScannerOverlay extends CustomPainter { - ScannerOverlay(this.scanWindow); + const ScannerOverlay({ + required this.scanWindow, + this.borderRadius = 12.0, + }); final Rect scanWindow; - final double borderRadius = 12.0; + final double borderRadius; @override void paint(Canvas canvas, Size size) { + // TODO: use `Offset.zero & size` instead of Rect.largest + // we need to pass the size to the custom paint widget final backgroundPath = Path()..addRect(Rect.largest); + final cutoutPath = Path() ..addRRect( RRect.fromRectAndCorners( @@ -199,14 +129,11 @@ class ScannerOverlay extends CustomPainter { cutoutPath, ); - // Create a Paint object for the white border final borderPaint = Paint() ..color = Colors.white ..style = PaintingStyle.stroke - ..strokeWidth = 4.0; // Adjust the border width as needed + ..strokeWidth = 4.0; - // Calculate the border rectangle with rounded corners -// Adjust the radius as needed final borderRect = RRect.fromRectAndCorners( scanWindow, topLeft: Radius.circular(borderRadius), @@ -215,13 +142,15 @@ class ScannerOverlay extends CustomPainter { bottomRight: Radius.circular(borderRadius), ); - // Draw the white border + // First, draw the background, + // with a cutout area that is a bit larger than the scan window. + // Finally, draw the scan window itself. canvas.drawPath(backgroundWithCutout, backgroundPaint); canvas.drawRRect(borderRect, borderPaint); } @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { - return false; + bool shouldRepaint(ScannerOverlay oldDelegate) { + return scanWindow != oldDelegate.scanWindow || borderRadius != oldDelegate.borderRadius; } } From 5b60e607b5a020515635bdb882b6a25b86d3107f Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 16 Nov 2023 17:00:01 +0100 Subject: [PATCH 068/161] depend on package:web --- example/pubspec.yaml | 4 ++-- lib/src/web/media.dart | 42 ------------------------------------------ pubspec.yaml | 9 +++++---- 3 files changed, 7 insertions(+), 48 deletions(-) delete mode 100644 lib/src/web/media.dart diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 1faac68b9..f2824b01c 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -6,8 +6,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 0.0.1 environment: - sdk: ">=3.1.0 <4.0.0" - flutter: ">=3.13.0" + sdk: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions diff --git a/lib/src/web/media.dart b/lib/src/web/media.dart deleted file mode 100644 index c4e82f83b..000000000 --- a/lib/src/web/media.dart +++ /dev/null @@ -1,42 +0,0 @@ -// // This is here because dart doesn't seem to support this properly -// // https://stackoverflow.com/questions/61161135/adding-support-for-navigator-mediadevices-getusermedia-to-dart - -@JS('navigator.mediaDevices') -library media_devices; - -import 'package:js/js.dart'; - -@JS('getUserMedia') -external Future getUserMedia(UserMediaOptions constraints); - -@JS() -@anonymous -class UserMediaOptions { - external VideoOptions get video; - - external factory UserMediaOptions({VideoOptions? video}); -} - -@JS() -@anonymous -class VideoOptions { - external String get facingMode; - // external DeviceIdOptions get deviceId; - external Map get width; - external Map get height; - - external factory VideoOptions({ - String? facingMode, - DeviceIdOptions? deviceId, - Map? width, - Map? height, - }); -} - -@JS() -@anonymous -class DeviceIdOptions { - external String get exact; - - external factory DeviceIdOptions({String? exact}); -} diff --git a/pubspec.yaml b/pubspec.yaml index b21260bdd..344751306 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,16 +16,17 @@ screenshots: path: example/screenshots/overlay.png environment: - sdk: ">=3.1.0 <4.0.0" - flutter: ">=3.13.0" + sdk: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" dependencies: flutter: sdk: flutter flutter_web_plugins: sdk: flutter - js: ">=0.6.3 <0.8.0" - plugin_platform_interface: ^2.0.2 + js: ^0.6.3 + plugin_platform_interface: ^2.0.2 + web: ^0.3.0 dev_dependencies: flutter_test: From 4cbdce8e74d38b20d3420aaa90995d8eebcbe360 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 17 Nov 2023 11:20:52 +0100 Subject: [PATCH 069/161] remove the obsolete jsQr implementation --- lib/src/web/jsqr.dart | 104 ------------------------------------------ 1 file changed, 104 deletions(-) delete mode 100644 lib/src/web/jsqr.dart diff --git a/lib/src/web/jsqr.dart b/lib/src/web/jsqr.dart deleted file mode 100644 index 7e24a9c28..000000000 --- a/lib/src/web/jsqr.dart +++ /dev/null @@ -1,104 +0,0 @@ -@JS() -library jsqr; - -import 'dart:async'; -import 'dart:html'; -import 'dart:typed_data'; - -import 'package:js/js.dart'; -import 'package:mobile_scanner/src/enums/barcode_format.dart'; -import 'package:mobile_scanner/src/enums/camera_facing.dart'; -import 'package:mobile_scanner/src/objects/barcode.dart'; -import 'package:mobile_scanner/src/web/base.dart'; - -@JS('jsQR') -external Code? jsQR(dynamic data, int? width, int? height); - -@JS() -class Code { - external String get data; - - external Uint8ClampedList get binaryData; -} - -/// Barcode reader that uses jsQR library. -/// jsQR supports only QR codes format. -class JsQrCodeReader extends WebBarcodeReaderBase - with InternalStreamCreation, InternalTorchDetection { - JsQrCodeReader({required super.videoContainer}); - - @override - bool get isStarted => localMediaStream != null; - - @override - Future start({ - required CameraFacing cameraFacing, - List? formats, - Duration? detectionTimeout, - }) async { - videoContainer.children = [video]; - - if (detectionTimeout != null) { - frameInterval = detectionTimeout; - } - - final stream = await initMediaStream(cameraFacing); - - prepareVideoElement(video); - if (stream != null) { - await attachStreamToVideo(stream, video); - } - } - - @override - void prepareVideoElement(VideoElement videoSource) { - // required to tell iOS safari we don't want fullscreen - videoSource.setAttribute('playsinline', 'true'); - } - - @override - Future attachStreamToVideo( - MediaStream stream, - VideoElement videoSource, - ) async { - localMediaStream = stream; - videoSource.srcObject = stream; - await videoSource.play(); - } - - @override - Stream detectBarcodeContinuously() async* { - yield* Stream.periodic(frameInterval, (_) { - return _captureFrame(video); - }).asyncMap((event) async { - final code = await event; - if (code == null) { - return null; - } - return Barcode( - rawValue: code.data, - rawBytes: Uint8List.fromList(code.binaryData), - format: BarcodeFormat.qrCode, - ); - }); - } - - @override - Future stopDetectBarcodeContinuously() async { - return; - } - - /// Captures a frame and analyzes it for QR codes - Future _captureFrame(VideoElement video) async { - if (localMediaStream == null) return null; - final canvas = - CanvasElement(width: video.videoWidth, height: video.videoHeight); - final ctx = canvas.context2D; - - ctx.drawImage(video, 0, 0); - final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!); - - final code = jsQR(imgData.data, canvas.width, canvas.height); - return code; - } -} From 87bd6bd210e5212af7e41c5daa3b89621300f08f Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 17 Nov 2023 13:26:22 +0100 Subject: [PATCH 070/161] update the result point js interop definition --- lib/src/web/zxing/result_point.dart | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 lib/src/web/zxing/result_point.dart diff --git a/lib/src/web/zxing/result_point.dart b/lib/src/web/zxing/result_point.dart new file mode 100644 index 000000000..d6d779c08 --- /dev/null +++ b/lib/src/web/zxing/result_point.dart @@ -0,0 +1,28 @@ +import 'dart:js_interop'; + +/// The JS static interop class for the Result class in the ZXing library. +/// +/// See also: https://github.com/zxing-js/library/blob/master/src/core/ResultPoint.ts +@JS() +@staticInterop +abstract class Result {} + +extension ResultPointExt on Result { + external JSFunction getX; + + external JSFunction getY; + + /// The x coordinate of the point. + double get x { + final JSNumber? x = getX.callAsFunction() as JSNumber?; + + return x?.toDartDouble ?? 0; + } + + /// The y coordinate of the point. + double get y { + final JSNumber? y = getY.callAsFunction() as JSNumber?; + + return y?.toDartDouble ?? 0; + } +} From 6b50ca9823fd1df016c71e76f0549232289a6239 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 17 Nov 2023 13:27:30 +0100 Subject: [PATCH 071/161] update the result js interop definition --- lib/src/web/zxing/result.dart | 77 +++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 lib/src/web/zxing/result.dart diff --git a/lib/src/web/zxing/result.dart b/lib/src/web/zxing/result.dart new file mode 100644 index 000000000..755888557 --- /dev/null +++ b/lib/src/web/zxing/result.dart @@ -0,0 +1,77 @@ +import 'dart:js_interop'; + +import 'package:mobile_scanner/src/enums/barcode_format.dart'; + +/// The JS static interop class for the Result class in the ZXing library. +/// +/// See also: https://github.com/zxing-js/library/blob/master/src/core/Result.ts +@JS() +@staticInterop +abstract class Result {} + +extension ResultExt on Result { + /// Get the barcode format. + /// + /// See also https://github.com/zxing-js/library/blob/master/src/core/BarcodeFormat.ts + external JSFunction getBarcodeFormat; + + /// Get the raw bytes of the result. + external JSFunction getRawBytes; + + /// Get the points of the result. + external JSFunction getResultPoints; + + /// Get the text of the result. + external JSFunction getText; + + /// Get the timestamp of the result. + external JSFunction getTimestamp; + + /// Get the barcode format of the result. + BarcodeFormat get barcodeFormat { + final JSNumber? format = getBarcodeFormat.callAsFunction() as JSNumber?; + + switch (format?.toDartInt) { + case 0: + return BarcodeFormat.aztec; + case 1: + return BarcodeFormat.codabar; + case 2: + return BarcodeFormat.code39; + case 3: + return BarcodeFormat.code93; + case 4: + return BarcodeFormat.code128; + case 5: + return BarcodeFormat.dataMatrix; + case 6: + return BarcodeFormat.ean8; + case 7: + return BarcodeFormat.ean13; + case 8: + return BarcodeFormat.itf; + case 9: + // Maxicode + return BarcodeFormat.unknown; + case 10: + return BarcodeFormat.pdf417; + case 11: + return BarcodeFormat.qrCode; + case 12: + // RSS 14 + return BarcodeFormat.unknown; + case 13: + // RSS EXPANDED + return BarcodeFormat.unknown; + case 14: + return BarcodeFormat.upcA; + case 15: + return BarcodeFormat.upcE; + case 16: + // UPC/EAN extension + return BarcodeFormat.unknown; + default: + return BarcodeFormat.unknown; + } + } +} From a64bc9ae37aae59c9f56158f33b4df9518a1a05b Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 17 Nov 2023 13:35:21 +0100 Subject: [PATCH 072/161] fix typo --- lib/src/web/zxing/result_point.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/web/zxing/result_point.dart b/lib/src/web/zxing/result_point.dart index d6d779c08..3c5237965 100644 --- a/lib/src/web/zxing/result_point.dart +++ b/lib/src/web/zxing/result_point.dart @@ -5,9 +5,9 @@ import 'dart:js_interop'; /// See also: https://github.com/zxing-js/library/blob/master/src/core/ResultPoint.ts @JS() @staticInterop -abstract class Result {} +abstract class ResultPoint {} -extension ResultPointExt on Result { +extension ResultPointExt on ResultPoint { external JSFunction getX; external JSFunction getY; From 3e4ab2d829b17ec7374a6884dd69156af016c194 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 17 Nov 2023 13:39:07 +0100 Subject: [PATCH 073/161] add extra getters for Result --- lib/src/web/zxing/result.dart | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/lib/src/web/zxing/result.dart b/lib/src/web/zxing/result.dart index 755888557..608f9121e 100644 --- a/lib/src/web/zxing/result.dart +++ b/lib/src/web/zxing/result.dart @@ -1,6 +1,9 @@ import 'dart:js_interop'; +import 'dart:typed_data'; +import 'dart:ui'; import 'package:mobile_scanner/src/enums/barcode_format.dart'; +import 'package:mobile_scanner/src/web/zxing/result_point.dart'; /// The JS static interop class for the Result class in the ZXing library. /// @@ -74,4 +77,35 @@ extension ResultExt on Result { return BarcodeFormat.unknown; } } + + List get resultPoints { + final JSArray? resultPoints = getResultPoints.callAsFunction() as JSArray?; + + if (resultPoints == null) { + return []; + } + + return resultPoints.toDart.cast().map((point) { + return Offset(point.x, point.y); + }).toList(); + } + + /// Get the raw bytes of the result. + Uint8List? get rawBytes { + final JSUint8Array? rawBytes = getRawBytes.callAsFunction() as JSUint8Array?; + + return rawBytes?.toDart; + } + + /// Get the text of the result. + String? get text { + return getText.callAsFunction() as String?; + } + + /// Get the timestamp of the result. + int? get timestamp { + final JSNumber? timestamp = getTimestamp.callAsFunction() as JSNumber?; + + return timestamp?.toDartInt; + } } From d6644baaff81f63a299a6994e97bd2fae489eacc Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 21 Nov 2023 09:17:37 +0100 Subject: [PATCH 074/161] add mention of Flutter 3.16.0 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e82943d77..00d054af3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## NEXT + BREAKING CHANGES: + +* Flutter 3.16.0 is now required. * The `width` and `height` of `BarcodeCapture` have been removed, in favor of `size`. * The `raw` attribute is now `Object?` instead of `dynamic`, so that it participates in type promotion. * The `MobileScannerArguments` class has been removed from the public API, as it is an internal type. From 0f4754905138f629fd630c9fa01fb64ef9b89e85 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 22 Nov 2023 12:19:45 +0100 Subject: [PATCH 075/161] add base for the new barcode reader --- lib/src/web/barcode_reader.dart | 78 +++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 lib/src/web/barcode_reader.dart diff --git a/lib/src/web/barcode_reader.dart b/lib/src/web/barcode_reader.dart new file mode 100644 index 000000000..7b6604166 --- /dev/null +++ b/lib/src/web/barcode_reader.dart @@ -0,0 +1,78 @@ +import 'dart:async'; +import 'dart:js_interop'; + +import 'package:js/js.dart'; +import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; +import 'package:mobile_scanner/src/enums/torch_state.dart'; +import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; +import 'package:web/web.dart'; + +/// This class represents the base interface for a barcode reader implementation. +abstract class BarcodeReader { + const BarcodeReader(); + + /// The id for the script tag that loads the barcode library. + /// + /// If a script tag with this id already exists, + /// the library will not be loaded again. + String get scriptId => 'mobile-scanner-barcode-reader'; + + /// The script url for the barcode library. + String get scriptUrl; + + /// Load the barcode reader library. + /// + /// Does nothing if the library is already loaded. + Future maybeLoadLibrary() async { + // Script already exists. + if (document.querySelector('script#$scriptId') != null) { + return; + } + + final Completer completer = Completer(); + + final HTMLScriptElement script = (document.createElement('script') as HTMLScriptElement) + ..id = scriptId + ..async = true + ..defer = false + ..type = 'application/javascript' + ..lang = 'javascript' + ..crossOrigin = 'anonymous' + ..src = scriptUrl + ..onload = allowInterop((JSAny _) { + if (!completer.isCompleted) { + completer.complete(); + } + }).toJS; + + script.onerror = allowInterop((JSAny _) { + if (!completer.isCompleted) { + // Remove the script if it did not load. + document.head!.removeChild(script); + + completer.completeError( + const MobileScannerException( + errorCode: MobileScannerErrorCode.genericError, + errorDetails: MobileScannerErrorDetails( + message: 'Could not load the BarcodeReader script due to a network error.', + ), + ), + ); + } + }).toJS; + + document.head!.appendChild(script); + + await completer.future; + } + + /// Set the flashlight state for the device. + Future setTorchState(TorchState torchState) { + throw UnimplementedError('setTorchState() has not been implemented.'); + } + + /// Stop the barcode reader and dispose of the video stream. + Future stop() { + throw UnimplementedError('stop() has not been implemented.'); + } +} From 60d93c709679df223030629658dac24901c37302 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 22 Nov 2023 12:25:50 +0100 Subject: [PATCH 076/161] fix doc --- lib/src/web/zxing/result.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/web/zxing/result.dart b/lib/src/web/zxing/result.dart index 608f9121e..bcc89fa39 100644 --- a/lib/src/web/zxing/result.dart +++ b/lib/src/web/zxing/result.dart @@ -78,6 +78,7 @@ extension ResultExt on Result { } } + /// Get the corner points of the result. List get resultPoints { final JSArray? resultPoints = getResultPoints.callAsFunction() as JSArray?; From 167b83887920456c0f6e9de13dbcbab76dc0d7d4 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 22 Nov 2023 12:27:50 +0100 Subject: [PATCH 077/161] add stub for ZXing barcode reader --- lib/src/web/zxing/zxing_barcode_reader.dart | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 lib/src/web/zxing/zxing_barcode_reader.dart diff --git a/lib/src/web/zxing/zxing_barcode_reader.dart b/lib/src/web/zxing/zxing_barcode_reader.dart new file mode 100644 index 000000000..dd73cff10 --- /dev/null +++ b/lib/src/web/zxing/zxing_barcode_reader.dart @@ -0,0 +1,49 @@ +import 'package:mobile_scanner/src/enums/barcode_format.dart'; +import 'package:mobile_scanner/src/web/barcode_reader.dart'; +import 'package:mobile_scanner/src/web/zxing/zxing_browser_multi_format_reader.dart'; + +/// A barcode reader implementation that uses the ZXing library. +final class ZXingBarcodeReader extends BarcodeReader { + ZXingBarcodeReader(); + + /// The internal barcode reader. + ZXingBrowserMultiFormatReader? _reader; + + @override + String get scriptUrl => 'https://unpkg.com/@zxing/library@0.19.1'; + + /// Get the barcode format from the ZXing library, for the given [format]. + int getZXingBarcodeFormat(BarcodeFormat format) { + switch (format) { + case BarcodeFormat.aztec: + return 0; + case BarcodeFormat.codabar: + return 1; + case BarcodeFormat.code39: + return 2; + case BarcodeFormat.code93: + return 3; + case BarcodeFormat.code128: + return 4; + case BarcodeFormat.dataMatrix: + return 5; + case BarcodeFormat.ean8: + return 6; + case BarcodeFormat.ean13: + return 7; + case BarcodeFormat.itf: + return 8; + case BarcodeFormat.pdf417: + return 10; + case BarcodeFormat.qrCode: + return 11; + case BarcodeFormat.upcA: + return 14; + case BarcodeFormat.upcE: + return 15; + case BarcodeFormat.unknown: + case BarcodeFormat.all: + return -1; + } + } +} From 5a2d63cb46273bcc1e75ac6251506adbc52842f0 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Mon, 27 Nov 2023 17:03:12 +0100 Subject: [PATCH 078/161] apply Android permission fix again --- .../method_channel/mobile_scanner_method_channel.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index 23b8f6f47..73657d9a5 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -113,18 +113,17 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { } switch (authorizationState) { - case MobileScannerAuthorizationState.denied: - throw const MobileScannerException( - errorCode: MobileScannerErrorCode.permissionDenied, - ); case MobileScannerAuthorizationState.authorized: return; // Already authorized. + // Android does not have an undetermined authorization state. + // So if the permission was denied, request it again. + case MobileScannerAuthorizationState.denied: case MobileScannerAuthorizationState.undetermined: try { - final bool permissionResult = + final bool granted = await methodChannel.invokeMethod('request') ?? false; - if (permissionResult) { + if (granted) { return; // Authorization was granted. } From 1c9e0f5b23865583a481598f4adefb29d566cacf Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Mon, 27 Nov 2023 17:08:13 +0100 Subject: [PATCH 079/161] add a stub for the start method --- lib/src/web/barcode_reader.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/src/web/barcode_reader.dart b/lib/src/web/barcode_reader.dart index 7b6604166..398468ae1 100644 --- a/lib/src/web/barcode_reader.dart +++ b/lib/src/web/barcode_reader.dart @@ -5,6 +5,7 @@ import 'package:js/js.dart'; import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; +import 'package:mobile_scanner/src/objects/start_options.dart'; import 'package:web/web.dart'; /// This class represents the base interface for a barcode reader implementation. @@ -71,6 +72,14 @@ abstract class BarcodeReader { throw UnimplementedError('setTorchState() has not been implemented.'); } + /// Start the barcode reader and initialize the video stream. + /// + /// The [options] are used to configure the barcode reader. + /// The [containerElement] will become the parent of the video output element. + Future start(StartOptions options, {required HTMLElement containerElement}) { + throw UnimplementedError('start() has not been implemented.'); + } + /// Stop the barcode reader and dispose of the video stream. Future stop() { throw UnimplementedError('stop() has not been implemented.'); From 4756a6d33e94c1df7b206859745ec2b5f72b7391 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Mon, 27 Nov 2023 17:29:55 +0100 Subject: [PATCH 080/161] update the zxing JS interop definition --- .../zxing_browser_multi_format_reader.dart | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 lib/src/web/zxing/zxing_browser_multi_format_reader.dart diff --git a/lib/src/web/zxing/zxing_browser_multi_format_reader.dart b/lib/src/web/zxing/zxing_browser_multi_format_reader.dart new file mode 100644 index 000000000..97d175fd9 --- /dev/null +++ b/lib/src/web/zxing/zxing_browser_multi_format_reader.dart @@ -0,0 +1,72 @@ +import 'dart:js_interop'; + +import 'package:js/js.dart'; +import 'package:web/web.dart'; + +/// The JS interop class for the ZXing BrowserMultiFormatReader. +/// +/// See https://github.com/zxing-js/library/blob/master/src/browser/BrowserMultiFormatReader.ts +@JS('ZXing.BrowserMultiFormatReader') +@staticInterop +class ZXingBrowserMultiFormatReader { + /// Construct a new ZXingBrowserMultiFormatReader. + /// + /// The [hints] are the configuration options for the reader. + /// The [timeBetweenScansMillis] is the allowed time between scans in milliseconds. + /// + /// See also: https://github.com/zxing-js/library/blob/master/src/core/DecodeHintType.ts + external factory ZXingBrowserMultiFormatReader( + JSAny? hints, + int? timeBetweenScansMillis, + ); +} + +extension ZXingBrowserMultiFormatReaderExt on ZXingBrowserMultiFormatReader { + /// Set the source for the [HTMLVideoElement] that acts as input for the barcode reader. + /// + /// This function takes a [HTMLVideoElement], and a [MediaStream] as arguments, + /// and returns no result. + /// + /// See https://github.com/zxing-js/library/blob/master/src/browser/BrowserCodeReader.ts#L1182 + external JSFunction addVideoSource; + + /// Continuously decode barcodes from a [HTMLVideoElement]. + /// + /// When a barcode is found, a callback function is called with the result. + /// The callback function receives a [Result] and an exception object as arguments. + /// + /// See also: https://github.com/zxing-js/library/blob/master/src/browser/DecodeContinuouslyCallback.ts + external JSFunction decodeContinuously; + + /// Whether the video stream is currently playing. + /// + /// This function takes a [HTMLVideoElement] as argument, + /// and returns a [bool]. + /// + /// See https://github.com/zxing-js/library/blob/master/src/browser/BrowserCodeReader.ts#L458 + external JSFunction isVideoPlaying; + + /// Prepare the video element for the barcode reader. + /// + /// This function takes a [HTMLVideoElement] as argument, + /// and returns the same [HTMLVideoElement], + /// after it was prepared for the barcode reader. + /// + /// See https://github.com/zxing-js/library/blob/master/src/browser/BrowserCodeReader.ts#L802 + external JSFunction prepareVideoElement; + + /// Reset the barcode reader to it's initial state, + /// and stop any ongoing barcode decoding. + /// + /// This function takes no arguments and returns no result. + /// + /// See https://github.com/zxing-js/library/blob/master/src/browser/BrowserCodeReader.ts#L1104 + external JSFunction reset; + + /// Stop decoding barcodes. + /// + /// This function takes no arguments and returns no result. + /// + /// See https://github.com/zxing-js/library/blob/master/src/browser/BrowserCodeReader.ts#L396 + external JSFunction stopContinuousDecode; +} From df6b0af9a96185db873b88cd6e9021d130ae3589 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Mon, 27 Nov 2023 18:42:24 +0100 Subject: [PATCH 081/161] optimize a function typing --- .../zxing_browser_multi_format_reader.dart | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/lib/src/web/zxing/zxing_browser_multi_format_reader.dart b/lib/src/web/zxing/zxing_browser_multi_format_reader.dart index 97d175fd9..18cafbcd7 100644 --- a/lib/src/web/zxing/zxing_browser_multi_format_reader.dart +++ b/lib/src/web/zxing/zxing_browser_multi_format_reader.dart @@ -1,6 +1,5 @@ import 'dart:js_interop'; -import 'package:js/js.dart'; import 'package:web/web.dart'; /// The JS interop class for the ZXing BrowserMultiFormatReader. @@ -22,13 +21,13 @@ class ZXingBrowserMultiFormatReader { } extension ZXingBrowserMultiFormatReaderExt on ZXingBrowserMultiFormatReader { - /// Set the source for the [HTMLVideoElement] that acts as input for the barcode reader. + /// Attach a [MediaStream] to a [HTMLVideoElement]. /// - /// This function takes a [HTMLVideoElement], and a [MediaStream] as arguments, - /// and returns no result. + /// This function accepts a [MediaStream] and a [HTMLVideoElement] as arguments, + /// and returns a [JSPromise]. /// - /// See https://github.com/zxing-js/library/blob/master/src/browser/BrowserCodeReader.ts#L1182 - external JSFunction addVideoSource; + /// See https://github.com/zxing-js/library/blob/master/src/browser/BrowserCodeReader.ts#L406 + external JSFunction attachStreamToVideo; /// Continuously decode barcodes from a [HTMLVideoElement]. /// @@ -46,15 +45,6 @@ extension ZXingBrowserMultiFormatReaderExt on ZXingBrowserMultiFormatReader { /// See https://github.com/zxing-js/library/blob/master/src/browser/BrowserCodeReader.ts#L458 external JSFunction isVideoPlaying; - /// Prepare the video element for the barcode reader. - /// - /// This function takes a [HTMLVideoElement] as argument, - /// and returns the same [HTMLVideoElement], - /// after it was prepared for the barcode reader. - /// - /// See https://github.com/zxing-js/library/blob/master/src/browser/BrowserCodeReader.ts#L802 - external JSFunction prepareVideoElement; - /// Reset the barcode reader to it's initial state, /// and stop any ongoing barcode decoding. /// From 48b432d59240ad7d08dde83651365c8ffbcae5c5 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 5 Dec 2023 13:47:14 +0100 Subject: [PATCH 082/161] fix name in pubspec --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 344751306..12cbe94cc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,5 +44,5 @@ flutter: macos: pluginClass: MobileScannerPlugin web: - pluginClass: MobileScannerWebPlugin + pluginClass: MobileScannerWeb fileName: web/mobile_scanner_web.dart From 831adf004137d945afed08223d99619a2b0af51c Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 5 Dec 2023 13:58:41 +0100 Subject: [PATCH 083/161] remove the old web plugin --- lib/mobile_scanner_web_plugin.dart | 192 ----------------------------- 1 file changed, 192 deletions(-) delete mode 100644 lib/mobile_scanner_web_plugin.dart diff --git a/lib/mobile_scanner_web_plugin.dart b/lib/mobile_scanner_web_plugin.dart deleted file mode 100644 index 288891aa9..000000000 --- a/lib/mobile_scanner_web_plugin.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:async'; -import 'dart:html' as html; -import 'dart:ui_web' as ui; - -import 'package:flutter/services.dart'; -import 'package:flutter_web_plugins/flutter_web_plugins.dart'; -import 'package:mobile_scanner/mobile_scanner_web.dart'; -import 'package:mobile_scanner/src/enums/barcode_format.dart'; -import 'package:mobile_scanner/src/enums/camera_facing.dart'; - -/// This plugin is the web implementation of mobile_scanner. -/// It only supports QR codes. -class MobileScannerWebPlugin { - static void registerWith(Registrar registrar) { - final PluginEventChannel event = PluginEventChannel( - 'dev.steenbakker.mobile_scanner/scanner/event', - const StandardMethodCodec(), - registrar, - ); - final MethodChannel channel = MethodChannel( - 'dev.steenbakker.mobile_scanner/scanner/method', - const StandardMethodCodec(), - registrar, - ); - final MobileScannerWebPlugin instance = MobileScannerWebPlugin(); - - channel.setMethodCallHandler(instance.handleMethodCall); - event.setController(instance.controller); - } - - // Controller to send events back to the framework - StreamController controller = StreamController.broadcast(); - - // ID of the video feed - String viewID = 'WebScanner-${DateTime.now().millisecondsSinceEpoch}'; - - static final html.DivElement vidDiv = html.DivElement(); - - /// Represents barcode reader library. - /// Change this property if you want to use a custom implementation. - /// - /// Example of using the jsQR library: - /// void main() { - /// if (kIsWeb) { - /// MobileScannerWebPlugin.barCodeReader = - /// JsQrCodeReader(videoContainer: MobileScannerWebPlugin.vidDiv); - /// } - /// runApp(const MaterialApp(home: MyHome())); - /// } - static WebBarcodeReaderBase barCodeReader = - ZXingBarcodeReader(videoContainer: vidDiv); - StreamSubscription? _barCodeStreamSubscription; - - /// Handle incomming messages - Future handleMethodCall(MethodCall call) async { - switch (call.method) { - case 'start': - return _start(call.arguments as Map); - case 'torch': - return _torch(call.arguments); - case 'stop': - return cancel(); - case 'updateScanWindow': - return Future.value(); - default: - throw PlatformException( - code: 'Unimplemented', - details: "The mobile_scanner plugin for web doesn't implement " - "the method '${call.method}'", - ); - } - } - - /// Can enable or disable the flash if available - Future _torch(arguments) async { - barCodeReader.toggleTorch(enabled: arguments == 1); - } - - /// Starts the video stream and the scanner - Future _start(Map arguments) async { - var cameraFacing = CameraFacing.front; - if (arguments.containsKey('facing')) { - cameraFacing = CameraFacing.values[arguments['facing'] as int]; - } - - ui.platformViewRegistry.registerViewFactory( - viewID, - (int id) { - return vidDiv - ..style.width = '100%' - ..style.height = '100%'; - }, - ); - - // Check if stream is running - if (barCodeReader.isStarted) { - final hasTorch = await barCodeReader.hasTorch(); - return { - 'ViewID': viewID, - 'videoWidth': barCodeReader.videoWidth, - 'videoHeight': barCodeReader.videoHeight, - 'torchable': hasTorch, - }; - } - try { - List? formats; - if (arguments.containsKey('formats')) { - formats = (arguments['formats'] as List) - .cast() - .map(BarcodeFormat.fromRawValue) - .toList(); - } - - final Duration? detectionTimeout; - if (arguments.containsKey('timeout')) { - detectionTimeout = Duration(milliseconds: arguments['timeout'] as int); - } else { - detectionTimeout = null; - } - - await barCodeReader.start( - cameraFacing: cameraFacing, - formats: formats, - detectionTimeout: detectionTimeout, - ); - - _barCodeStreamSubscription = - barCodeReader.detectBarcodeContinuously().listen((code) { - if (code != null) { - controller.add({ - 'name': 'barcodeWeb', - 'data': { - 'rawValue': code.rawValue, - 'rawBytes': code.rawBytes, - 'format': code.format.rawValue, - 'displayValue': code.displayValue, - 'type': code.type.rawValue, - if (code.corners.isNotEmpty) - 'corners': code.corners - .map( - (Offset c) => {'x': c.dx, 'y': c.dy}, - ) - .toList(), - }, - }); - } - }); - - final hasTorch = await barCodeReader.hasTorch(); - final bool? enableTorch = arguments['torch'] as bool?; - - if (hasTorch && enableTorch != null) { - await barCodeReader.toggleTorch(enabled: enableTorch); - } - - return { - 'ViewID': viewID, - 'videoWidth': barCodeReader.videoWidth, - 'videoHeight': barCodeReader.videoHeight, - 'torchable': hasTorch, - }; - } catch (e, stackTrace) { - throw PlatformException( - code: 'MobileScannerWeb', - message: '$e', - details: stackTrace.toString(), - ); - } - } - - /// Check if any camera's are available - static Future cameraAvailable() async { - final sources = - await html.window.navigator.mediaDevices!.enumerateDevices(); - for (final e in sources) { - // TODO: - // ignore: avoid_dynamic_calls - if (e.kind == 'videoinput') { - return true; - } - } - return false; - } - - /// Stops the video feed and analyzer - Future cancel() async { - await barCodeReader.stop(); - await barCodeReader.stopDetectBarcodeContinuously(); - await _barCodeStreamSubscription?.cancel(); - _barCodeStreamSubscription = null; - } -} From 50438b2a52cb248a701ff0dbeaf1f8393e1bafe3 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 5 Dec 2023 14:00:02 +0100 Subject: [PATCH 084/161] add detect method to barcode reader interface --- lib/src/web/barcode_reader.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/src/web/barcode_reader.dart b/lib/src/web/barcode_reader.dart index 398468ae1..5e4ea2874 100644 --- a/lib/src/web/barcode_reader.dart +++ b/lib/src/web/barcode_reader.dart @@ -5,6 +5,7 @@ import 'package:js/js.dart'; import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; +import 'package:mobile_scanner/src/objects/barcode_capture.dart'; import 'package:mobile_scanner/src/objects/start_options.dart'; import 'package:web/web.dart'; @@ -21,6 +22,13 @@ abstract class BarcodeReader { /// The script url for the barcode library. String get scriptUrl; + /// Start detecting barcodes. + /// + /// The returned stream will emit a [BarcodeCapture] for each detected barcode. + Stream detectBarcodes() { + throw UnimplementedError('detectBarcodes() has not been implemented.'); + } + /// Load the barcode reader library. /// /// Does nothing if the library is already loaded. From 2d3e70a7dfdefedda2fa12558ec2935e3f3b3a8d Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 5 Dec 2023 14:09:15 +0100 Subject: [PATCH 085/161] add isScanning to the barcode scanner web interface & remove hasTorch --- lib/src/web/barcode_reader.dart | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/src/web/barcode_reader.dart b/lib/src/web/barcode_reader.dart index 5e4ea2874..e50853f73 100644 --- a/lib/src/web/barcode_reader.dart +++ b/lib/src/web/barcode_reader.dart @@ -3,7 +3,6 @@ import 'dart:js_interop'; import 'package:js/js.dart'; import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; -import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; import 'package:mobile_scanner/src/objects/barcode_capture.dart'; import 'package:mobile_scanner/src/objects/start_options.dart'; @@ -13,6 +12,9 @@ import 'package:web/web.dart'; abstract class BarcodeReader { const BarcodeReader(); + /// Whether the scanner is currently scanning for barcodes. + bool get isScanning; + /// The id for the script tag that loads the barcode library. /// /// If a script tag with this id already exists, @@ -75,11 +77,6 @@ abstract class BarcodeReader { await completer.future; } - /// Set the flashlight state for the device. - Future setTorchState(TorchState torchState) { - throw UnimplementedError('setTorchState() has not been implemented.'); - } - /// Start the barcode reader and initialize the video stream. /// /// The [options] are used to configure the barcode reader. From 097fd4f55e9f5db64071634386755c270730583c Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 5 Dec 2023 14:31:27 +0100 Subject: [PATCH 086/161] remove the old zxing bindings --- lib/src/web/zxing.dart | 285 ----------------------------------------- 1 file changed, 285 deletions(-) delete mode 100644 lib/src/web/zxing.dart diff --git a/lib/src/web/zxing.dart b/lib/src/web/zxing.dart deleted file mode 100644 index c1bad0d18..000000000 --- a/lib/src/web/zxing.dart +++ /dev/null @@ -1,285 +0,0 @@ -import 'dart:async'; -import 'dart:html'; -import 'dart:typed_data'; -import 'dart:ui'; - -import 'package:js/js.dart'; -import 'package:mobile_scanner/src/enums/barcode_format.dart'; -import 'package:mobile_scanner/src/enums/camera_facing.dart'; -import 'package:mobile_scanner/src/objects/barcode.dart'; -import 'package:mobile_scanner/src/web/base.dart'; - -@JS('ZXing.BrowserMultiFormatReader') -@staticInterop -class JsZXingBrowserMultiFormatReader { - /// https://github.com/zxing-js/library/blob/1e9ccb3b6b28d75b9eef866dba196d8937eb4449/src/browser/BrowserMultiFormatReader.ts#L11 - external factory JsZXingBrowserMultiFormatReader( - dynamic hints, - int timeBetweenScansMillis, - ); -} - -@JS() -@anonymous -abstract class ResultPoint { - /// The x coordinate of the point. - external double get x; - - /// The y coordinate of the point. - external double get y; -} - -@JS() -@anonymous -abstract class Result { - /// raw text encoded by the barcode - external String get text; - - /// Returns raw bytes encoded by the barcode, if applicable, otherwise null - external Uint8ClampedList? get rawBytes; - - /// Representing the format of the barcode that was decoded - external int? format; - - /// Returns the result points of the barcode. These points represent the corners of the barcode. - external List get resultPoints; -} - -extension ResultExt on Result { - Barcode toBarcode() { - final corners = resultPoints - .cast() - .map((ResultPoint rp) => Offset(rp.x, rp.y)) - .toList(); - - final rawBytes = this.rawBytes; - return Barcode( - rawValue: text, - rawBytes: rawBytes != null ? Uint8List.fromList(rawBytes) : null, - format: barcodeFormat, - corners: corners, - ); - } - - /// https://github.com/zxing-js/library/blob/1e9ccb3b6b28d75b9eef866dba196d8937eb4449/src/core/BarcodeFormat.ts#L28 - BarcodeFormat get barcodeFormat { - switch (format) { - case 0: - return BarcodeFormat.aztec; - case 1: - return BarcodeFormat.codebar; - case 2: - return BarcodeFormat.code39; - case 3: - return BarcodeFormat.code93; - case 4: - return BarcodeFormat.code128; - case 5: - return BarcodeFormat.dataMatrix; - case 6: - return BarcodeFormat.ean8; - case 7: - return BarcodeFormat.ean13; - case 8: - return BarcodeFormat.itf; - // case 9: - // return BarcodeFormat.maxicode; - case 10: - return BarcodeFormat.pdf417; - case 11: - return BarcodeFormat.qrCode; - // case 12: - // return BarcodeFormat.rss14; - // case 13: - // return BarcodeFormat.rssExp; - case 14: - return BarcodeFormat.upcA; - case 15: - return BarcodeFormat.upcE; - default: - return BarcodeFormat.unknown; - } - } -} - -extension ZXingBarcodeFormat on BarcodeFormat { - int get zxingBarcodeFormat { - switch (this) { - case BarcodeFormat.aztec: - return 0; - case BarcodeFormat.codabar: - return 1; - case BarcodeFormat.code39: - return 2; - case BarcodeFormat.code93: - return 3; - case BarcodeFormat.code128: - return 4; - case BarcodeFormat.dataMatrix: - return 5; - case BarcodeFormat.ean8: - return 6; - case BarcodeFormat.ean13: - return 7; - case BarcodeFormat.itf: - return 8; - case BarcodeFormat.pdf417: - return 10; - case BarcodeFormat.qrCode: - return 11; - case BarcodeFormat.upcA: - return 14; - case BarcodeFormat.upcE: - return 15; - case BarcodeFormat.unknown: - case BarcodeFormat.all: - default: - return -1; - } - } -} - -typedef BarcodeDetectionCallback = void Function( - Result? result, - dynamic error, -); - -extension JsZXingBrowserMultiFormatReaderExt - on JsZXingBrowserMultiFormatReader { - external Promise decodeFromVideoElementContinuously( - VideoElement source, - BarcodeDetectionCallback callbackFn, - ); - - /// Continuously decodes from video input - external void decodeContinuously( - VideoElement element, - BarcodeDetectionCallback callbackFn, - ); - - external Promise decodeFromStream( - MediaStream stream, - VideoElement videoSource, - BarcodeDetectionCallback callbackFn, - ); - - external Promise decodeFromConstraints( - dynamic constraints, - VideoElement videoSource, - BarcodeDetectionCallback callbackFn, - ); - - external void stopContinuousDecode(); - - external VideoElement prepareVideoElement(VideoElement videoSource); - - /// Defines what the [videoElement] src will be. - external void addVideoSource( - VideoElement videoElement, - MediaStream stream, - ); - - external bool isVideoPlaying(VideoElement video); - - external void reset(); - - /// The HTML video element, used to display the camera stream. - external VideoElement? videoElement; - - /// The stream output from camera. - external MediaStream? stream; -} - -/// Barcode reader that uses zxing-js library. -class ZXingBarcodeReader extends WebBarcodeReaderBase - with InternalStreamCreation, InternalTorchDetection { - JsZXingBrowserMultiFormatReader? _reader; - - ZXingBarcodeReader({required super.videoContainer}); - - @override - bool get isStarted => localMediaStream != null; - - @override - Future start({ - required CameraFacing cameraFacing, - List? formats, - Duration? detectionTimeout, - }) async { - final JsMap? hints; - if (formats != null && !formats.contains(BarcodeFormat.all)) { - hints = JsMap(); - final zxingFormats = - formats.map((e) => e.zxingBarcodeFormat).where((e) => e > 0).toList(); - // set hint DecodeHintType.POSSIBLE_FORMATS - // https://github.com/zxing-js/library/blob/1e9ccb3b6b28d75b9eef866dba196d8937eb4449/src/core/DecodeHintType.ts#L28 - hints.set(2, zxingFormats); - } else { - hints = null; - } - if (detectionTimeout != null) { - frameInterval = detectionTimeout; - } - _reader = JsZXingBrowserMultiFormatReader( - hints, - frameInterval.inMilliseconds, - ); - videoContainer.children = [video]; - - final stream = await initMediaStream(cameraFacing); - - prepareVideoElement(video); - if (stream != null) { - await attachStreamToVideo(stream, video); - } - } - - @override - void prepareVideoElement(VideoElement videoSource) { - _reader?.prepareVideoElement(videoSource); - } - - @override - Future attachStreamToVideo( - MediaStream stream, - VideoElement videoSource, - ) async { - _reader?.addVideoSource(videoSource, stream); - _reader?.videoElement = videoSource; - _reader?.stream = stream; - localMediaStream = stream; - await videoSource.play(); - } - - StreamController? controller; - - @override - Future stopDetectBarcodeContinuously() async { - _reader?.stopContinuousDecode(); - controller?.close(); - controller = null; - } - - @override - Stream detectBarcodeContinuously() { - controller ??= StreamController(); - controller!.onListen = () async { - _reader?.decodeContinuously( - video, - allowInterop((result, error) { - if (result != null) { - controller?.add(result.toBarcode()); - } - }), - ); - }; - controller!.onCancel = () => stopDetectBarcodeContinuously(); - return controller!.stream; - } - - @override - Future stop() async { - _reader?.reset(); - super.stop(); - } -} From 3ef2188c9e2fb702e59c10093533dce50800a63a Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 5 Dec 2023 16:16:59 +0100 Subject: [PATCH 087/161] reimplement mobile_scanner web with platform interface --- lib/src/web/mobile_scanner_web.dart | 161 ++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index 2d9a21523..25ec63bc1 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -1,12 +1,173 @@ +import 'dart:async'; +import 'dart:ui_web' as ui_web; + +import 'package:flutter/widgets.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; +import 'package:mobile_scanner/src/enums/torch_state.dart'; +import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart'; +import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; +import 'package:mobile_scanner/src/objects/barcode_capture.dart'; +import 'package:mobile_scanner/src/objects/start_options.dart'; +import 'package:mobile_scanner/src/web/barcode_reader.dart'; +import 'package:mobile_scanner/src/web/zxing/zxing_barcode_reader.dart'; +import 'package:web/web.dart'; /// A web implementation of the MobileScannerPlatform of the MobileScanner plugin. class MobileScannerWeb extends MobileScannerPlatform { /// Constructs a [MobileScannerWeb] instance. MobileScannerWeb(); + /// The internal barcode reader. + final BarcodeReader _barcodeReader = ZXingBarcodeReader(); + + /// The stream controller for the barcode stream. + final StreamController _barcodesController = StreamController.broadcast(); + + /// The subscription for the barcode stream. + StreamSubscription? _barcodesSubscription; + + /// The container div element for the camera view. + /// + /// This container element is used by the barcode reader. + HTMLDivElement? _divElement; + + /// The view type for the platform view factory. + final String _viewType = 'MobileScannerWeb'; + static void registerWith(Registrar registrar) { MobileScannerPlatform.instance = MobileScannerWeb(); } + + @override + Widget buildCameraView() { + if (!_barcodeReader.isScanning) { + throw const MobileScannerException( + errorCode: MobileScannerErrorCode.controllerUninitialized, + errorDetails: MobileScannerErrorDetails( + message: 'The controller was not yet initialized. Call start() before calling buildCameraView().', + ), + ); + } + + return HtmlElementView(viewType: _viewType); + } + + @override + Future start(StartOptions startOptions) async { + await _barcodeReader.maybeLoadLibrary(); + + // Setup the view factory & container element. + if (_divElement == null) { + _divElement = (document.createElement('div') as HTMLDivElement) + ..style.width = '100%' + ..style.height = '100%'; + + ui_web.platformViewRegistry.registerViewFactory( + _viewType, + (int id) => _divElement, + ); + } + + if (_barcodeReader.isScanning) { + throw const MobileScannerException( + errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, + errorDetails: MobileScannerErrorDetails( + message: 'The scanner was already started. Call stop() before calling start() again.', + ), + ); + } + + try { + await _barcodeReader.start( + startOptions, + containerElement: _divElement!, + ); + } catch (error, stackTrace) { + final String errorMessage = error.toString(); + + MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError; + + if (error is DOMException) { + if (errorMessage.contains('NotFoundError') || errorMessage.contains('NotSupportedError')) { + errorCode = MobileScannerErrorCode.unsupported; + } else if (errorMessage.contains('NotAllowedError')) { + errorCode = MobileScannerErrorCode.permissionDenied; + } + } + + throw MobileScannerException( + errorCode: errorCode, + errorDetails: MobileScannerErrorDetails( + message: errorMessage, + details: stackTrace.toString(), + ), + ); + } + + try { + _barcodesSubscription = _barcodeReader.detectBarcodes().listen((BarcodeCapture barcode) { + if (_barcodesController.isClosed) { + return; + } + + _barcodesController.add(barcode); + }); + + final bool hasTorch = _barcodeReader.hasTorch; + + if (hasTorch && startOptions.torchEnabled) { + await _barcodeReader.setTorchState(TorchState.on).catchError((_) { + // If the torch could not be turned on, continue the camera session anyway. + }); + } + + return MobileScannerViewAttributes( + hasTorch: hasTorch, + size: _barcodeReader.outputSize, + ); + } catch (error, stackTrace) { + throw MobileScannerException( + errorCode: MobileScannerErrorCode.genericError, + errorDetails: MobileScannerErrorDetails( + message: error.toString(), + details: stackTrace.toString(), + ), + ); + } + } + + @override + Future stop() async { + if (_barcodesController.isClosed) { + return; + } + + _barcodesSubscription?.cancel(); + _barcodesSubscription = null; + + // Clear the existing barcodes. + _barcodesController.add(const BarcodeCapture()); + + await _barcodeReader.stop(); + } + + @override + Future updateScanWindow(Rect? window) { + // A scan window is not supported on the web, + // because the scanner does not expose size information for the barcodes. + return Future.value(); + } + + @override + Future dispose() async { + if (_barcodesController.isClosed) { + return; + } + + await stop(); + await _barcodeReader.dispose(); + await _barcodesController.close(); + } } From f05f880f4122aef81b1e5f45e0bee987ee99637e Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 5 Dec 2023 16:23:02 +0100 Subject: [PATCH 088/161] remove obsolete breaks --- example/lib/scanner_button_widgets.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/example/lib/scanner_button_widgets.dart b/example/lib/scanner_button_widgets.dart index a8b2f2130..61a729c6f 100644 --- a/example/lib/scanner_button_widgets.dart +++ b/example/lib/scanner_button_widgets.dart @@ -101,10 +101,8 @@ class SwitchCameraButton extends StatelessWidget { switch (state.cameraDirection) { case CameraFacing.front: icon = const Icon(Icons.camera_front); - break; case CameraFacing.back: icon = const Icon(Icons.camera_rear); - break; } return IconButton( From 2ef4ccdff761f8e98a9cce1c573824e2424e4612 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 5 Dec 2023 16:30:48 +0100 Subject: [PATCH 089/161] emit running false before actually stopping the camera --- lib/src/mobile_scanner_controller.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index e3b40a4c5..4f8e93387 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -267,14 +267,14 @@ class MobileScannerController extends ValueNotifier { _throwIfNotInitialized(); - await MobileScannerPlatform.instance.stop(); - // After the camera stopped, set the torch state to off, // as the torch state callback is never called when the camera is stopped. value = value.copyWith( isRunning: false, torchState: TorchState.off, ); + + await MobileScannerPlatform.instance.stop(); } /// Switch between the front and back camera. From 8be461cdf4c9d66705ba16d790c09ba2db8f4b7c Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 5 Dec 2023 16:34:28 +0100 Subject: [PATCH 090/161] format barcode reader base --- lib/src/web/barcode_reader.dart | 35 +++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/lib/src/web/barcode_reader.dart b/lib/src/web/barcode_reader.dart index e50853f73..f30864dc7 100644 --- a/lib/src/web/barcode_reader.dart +++ b/lib/src/web/barcode_reader.dart @@ -42,19 +42,20 @@ abstract class BarcodeReader { final Completer completer = Completer(); - final HTMLScriptElement script = (document.createElement('script') as HTMLScriptElement) - ..id = scriptId - ..async = true - ..defer = false - ..type = 'application/javascript' - ..lang = 'javascript' - ..crossOrigin = 'anonymous' - ..src = scriptUrl - ..onload = allowInterop((JSAny _) { - if (!completer.isCompleted) { - completer.complete(); - } - }).toJS; + final HTMLScriptElement script = + (document.createElement('script') as HTMLScriptElement) + ..id = scriptId + ..async = true + ..defer = false + ..type = 'application/javascript' + ..lang = 'javascript' + ..crossOrigin = 'anonymous' + ..src = scriptUrl + ..onload = allowInterop((JSAny _) { + if (!completer.isCompleted) { + completer.complete(); + } + }).toJS; script.onerror = allowInterop((JSAny _) { if (!completer.isCompleted) { @@ -65,7 +66,8 @@ abstract class BarcodeReader { const MobileScannerException( errorCode: MobileScannerErrorCode.genericError, errorDetails: MobileScannerErrorDetails( - message: 'Could not load the BarcodeReader script due to a network error.', + message: + 'Could not load the BarcodeReader script due to a network error.', ), ), ); @@ -81,7 +83,10 @@ abstract class BarcodeReader { /// /// The [options] are used to configure the barcode reader. /// The [containerElement] will become the parent of the video output element. - Future start(StartOptions options, {required HTMLElement containerElement}) { + Future start( + StartOptions options, { + required HTMLElement containerElement, + }) { throw UnimplementedError('start() has not been implemented.'); } From c11832e50b9a8507d6452cf4b2a5039193ed8b9d Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 7 Dec 2023 08:31:40 +0100 Subject: [PATCH 091/161] add a toBarcode getter to the rsult JS interop class --- lib/src/web/zxing/result.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/src/web/zxing/result.dart b/lib/src/web/zxing/result.dart index bcc89fa39..bb3300e63 100644 --- a/lib/src/web/zxing/result.dart +++ b/lib/src/web/zxing/result.dart @@ -3,6 +3,8 @@ import 'dart:typed_data'; import 'dart:ui'; import 'package:mobile_scanner/src/enums/barcode_format.dart'; +import 'package:mobile_scanner/src/enums/barcode_type.dart'; +import 'package:mobile_scanner/src/objects/barcode.dart'; import 'package:mobile_scanner/src/web/zxing/result_point.dart'; /// The JS static interop class for the Result class in the ZXing library. @@ -109,4 +111,16 @@ extension ResultExt on Result { return timestamp?.toDartInt; } + + /// Convert this result to a [Barcode]. + Barcode get toBarcode { + return Barcode( + corners: resultPoints, + format: barcodeFormat, + displayValue: text, + rawBytes: rawBytes, + rawValue: text, + type: BarcodeType.text, + ); + } } From d6be818ea7f32e3f737b58ed9970057e0958b79f Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 7 Dec 2023 10:49:15 +0100 Subject: [PATCH 092/161] add video size getter to barcode reader --- lib/src/web/barcode_reader.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/web/barcode_reader.dart b/lib/src/web/barcode_reader.dart index f30864dc7..47bb95d55 100644 --- a/lib/src/web/barcode_reader.dart +++ b/lib/src/web/barcode_reader.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:js_interop'; +import 'dart:ui'; import 'package:js/js.dart'; import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; @@ -15,6 +16,9 @@ abstract class BarcodeReader { /// Whether the scanner is currently scanning for barcodes. bool get isScanning; + /// Get the size of the output of the video stream. + Size get videoSize; + /// The id for the script tag that loads the barcode library. /// /// If a script tag with this id already exists, From 0be282eac3d4495a7aa13a353af49f81074a365b Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 7 Dec 2023 10:50:15 +0100 Subject: [PATCH 093/161] make the barcode reader getters throw if not implemented --- lib/src/web/barcode_reader.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/src/web/barcode_reader.dart b/lib/src/web/barcode_reader.dart index 47bb95d55..ccbda769b 100644 --- a/lib/src/web/barcode_reader.dart +++ b/lib/src/web/barcode_reader.dart @@ -14,10 +14,14 @@ abstract class BarcodeReader { const BarcodeReader(); /// Whether the scanner is currently scanning for barcodes. - bool get isScanning; + bool get isScanning { + throw UnimplementedError('isScanning has not been implemented.'); + } /// Get the size of the output of the video stream. - Size get videoSize; + Size get videoSize { + throw UnimplementedError('videoSize has not been implemented.'); + } /// The id for the script tag that loads the barcode library. /// @@ -26,7 +30,9 @@ abstract class BarcodeReader { String get scriptId => 'mobile-scanner-barcode-reader'; /// The script url for the barcode library. - String get scriptUrl; + String get scriptUrl { + throw UnimplementedError('scriptUrl has not been implemented.'); + } /// Start detecting barcodes. /// From d2245cfbca96a1e2120c20d3d9e156a91b736410 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 7 Dec 2023 10:53:24 +0100 Subject: [PATCH 094/161] remove unused method invocation; use videoSize getter --- lib/src/web/mobile_scanner_web.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index 25ec63bc1..fdb2eb2a7 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -125,7 +125,7 @@ class MobileScannerWeb extends MobileScannerPlatform { return MobileScannerViewAttributes( hasTorch: hasTorch, - size: _barcodeReader.outputSize, + size: _barcodeReader.videoSize, ); } catch (error, stackTrace) { throw MobileScannerException( @@ -167,7 +167,6 @@ class MobileScannerWeb extends MobileScannerPlatform { } await stop(); - await _barcodeReader.dispose(); await _barcodesController.close(); } } From dbeb4bd4356cbc90b13bb19e338cd0f57b872c66 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 7 Dec 2023 11:17:30 +0100 Subject: [PATCH 095/161] reimplement ZXing barcode reader --- lib/src/web/zxing/zxing_barcode_reader.dart | 142 +++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/lib/src/web/zxing/zxing_barcode_reader.dart b/lib/src/web/zxing/zxing_barcode_reader.dart index dd73cff10..518d173f4 100644 --- a/lib/src/web/zxing/zxing_barcode_reader.dart +++ b/lib/src/web/zxing/zxing_barcode_reader.dart @@ -1,6 +1,15 @@ +import 'dart:async'; +import 'dart:js_interop'; + +import 'package:js/js.dart'; import 'package:mobile_scanner/src/enums/barcode_format.dart'; +import 'package:mobile_scanner/src/enums/camera_facing.dart'; +import 'package:mobile_scanner/src/objects/barcode_capture.dart'; +import 'package:mobile_scanner/src/objects/start_options.dart'; import 'package:mobile_scanner/src/web/barcode_reader.dart'; +import 'package:mobile_scanner/src/web/zxing/result.dart'; import 'package:mobile_scanner/src/web/zxing/zxing_browser_multi_format_reader.dart'; +import 'package:web/web.dart' as web; /// A barcode reader implementation that uses the ZXing library. final class ZXingBarcodeReader extends BarcodeReader { @@ -9,11 +18,14 @@ final class ZXingBarcodeReader extends BarcodeReader { /// The internal barcode reader. ZXingBrowserMultiFormatReader? _reader; + @override + bool get isScanning => _reader?.stream != null; + @override String get scriptUrl => 'https://unpkg.com/@zxing/library@0.19.1'; /// Get the barcode format from the ZXing library, for the given [format]. - int getZXingBarcodeFormat(BarcodeFormat format) { + static int getZXingBarcodeFormat(BarcodeFormat format) { switch (format) { case BarcodeFormat.aztec: return 0; @@ -46,4 +58,132 @@ final class ZXingBarcodeReader extends BarcodeReader { return -1; } } + + /// Prepare the [web.MediaStream] for the barcode reader video input. + /// + /// This method requests permission to use the camera. + Future _prepareMediaStream( + CameraFacing cameraDirection, + ) async { + if (web.window.navigator.mediaDevices.isUndefinedOrNull) { + return null; + } + + final capabilities = web.window.navigator.mediaDevices.getSupportedConstraints(); + + final web.MediaStreamConstraints constraints; + + if (capabilities.isUndefinedOrNull || !capabilities.facingMode) { + constraints = web.MediaStreamConstraints(video: true.toJS); + } else { + final String facingMode = switch (cameraDirection) { + CameraFacing.back => 'environment', + CameraFacing.front => 'user', + }; + + constraints = web.MediaStreamConstraints( + video: web.MediaTrackConstraintSet( + facingMode: facingMode.toJS, + ), + ); + } + + final JSAny? mediaStream = await web.window.navigator.mediaDevices.getUserMedia(constraints).toDart; + + return mediaStream as web.MediaStream?; + } + + /// Prepare the video element for the barcode reader. + /// + /// The given [videoElement] is attached to the DOM, by attaching it to the [containerElement]. + /// The camera video output is then attached to both the barcode reader (to detect barcodes), + /// and the video element (to display the camera output). + Future _prepareVideoElement( + web.HTMLVideoElement videoElement, { + required CameraFacing cameraDirection, + required web.HTMLElement containerElement, + }) async { + // Attach the video element to the DOM, through its parent container. + containerElement.appendChild(videoElement); + + // Set up the camera output stream. + // This will request permission to use the camera. + final web.MediaStream? stream = await _prepareMediaStream(cameraDirection); + + if (stream != null) { + final JSPromise? result = _reader?.attachStreamToVideo.callAsFunction(null, stream, videoElement) as JSPromise?; + + await result?.toDart; + } + } + + @override + Stream detectBarcodes() { + final controller = StreamController(); + + controller.onListen = () { + _reader?.decodeContinuously.callAsFunction( + null, + _reader?.videoElement, + allowInterop((result, error) { + if (!controller.isClosed && result != null) { + final barcode = (result as Result).toBarcode; + + controller.add( + BarcodeCapture( + barcodes: [barcode], + ), + ); + } + }).toJS, + ); + }; + + controller.onCancel = () async { + _reader?.stopContinuousDecode.callAsFunction(); + _reader?.reset.callAsFunction(); + await controller.close(); + }; + + return controller.stream; + } + + @override + Future start(StartOptions options, {required web.HTMLElement containerElement}) async { + final int detectionTimeoutMs = options.detectionTimeoutMs; + final List formats = options.formats; + + if (formats.contains(BarcodeFormat.unknown)) { + formats.removeWhere((element) => element == BarcodeFormat.unknown); + } + + final Map? hints; + + if (formats.isNotEmpty && !formats.contains(BarcodeFormat.all)) { + // Set the formats hint. + // See https://github.com/zxing-js/library/blob/master/src/core/DecodeHintType.ts#L45 + hints = { + 2: formats.map(getZXingBarcodeFormat).toList(), + }; + } else { + hints = null; + } + + _reader = ZXingBrowserMultiFormatReader(hints.jsify(), detectionTimeoutMs); + + final web.HTMLVideoElement videoElement = web.document.createElement('video') as web.HTMLVideoElement; + + await _prepareVideoElement( + videoElement, + cameraDirection: options.cameraDirection, + containerElement: containerElement, + ); + } + + @override + Future stop() async { + _reader?.stopContinuousDecode.callAsFunction(); + _reader?.reset.callAsFunction(); + _reader = null; + } } From 1fd8e0e491464cebe1660315d999d721a73c897b Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 7 Dec 2023 11:17:56 +0100 Subject: [PATCH 096/161] add stream & video element getters to JS interop definition --- lib/src/web/zxing/zxing_browser_multi_format_reader.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/web/zxing/zxing_browser_multi_format_reader.dart b/lib/src/web/zxing/zxing_browser_multi_format_reader.dart index 18cafbcd7..a86cdf271 100644 --- a/lib/src/web/zxing/zxing_browser_multi_format_reader.dart +++ b/lib/src/web/zxing/zxing_browser_multi_format_reader.dart @@ -59,4 +59,10 @@ extension ZXingBrowserMultiFormatReaderExt on ZXingBrowserMultiFormatReader { /// /// See https://github.com/zxing-js/library/blob/master/src/browser/BrowserCodeReader.ts#L396 external JSFunction stopContinuousDecode; + + /// Get the current MediaStream of the barcode reader. + external MediaStream? get stream; + + /// Get the current HTMLVideoElement of the barcode reader. + external HTMLVideoElement? get videoElement; } From bf7bfdeecc01b47833a65c7d2ea35f5c94d5d41d Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 7 Dec 2023 11:18:26 +0100 Subject: [PATCH 097/161] remove the old base.dart file --- lib/src/web/base.dart | 199 ------------------------------------------ 1 file changed, 199 deletions(-) delete mode 100644 lib/src/web/base.dart diff --git a/lib/src/web/base.dart b/lib/src/web/base.dart deleted file mode 100644 index 593cccd56..000000000 --- a/lib/src/web/base.dart +++ /dev/null @@ -1,199 +0,0 @@ -import 'dart:html' as html; - -import 'package:flutter/material.dart'; -import 'package:js/js.dart'; -import 'package:js/js_util.dart'; -import 'package:mobile_scanner/src/enums/barcode_format.dart'; -import 'package:mobile_scanner/src/enums/camera_facing.dart'; -import 'package:mobile_scanner/src/objects/barcode.dart'; -import 'package:mobile_scanner/src/web/media.dart'; - -class JsLibrary { - /// The name of global variable where library is stored. - /// Used to properly import the library if [usesRequireJs] flag is true - final String contextName; - final String url; - - /// If js code checks for 'define' variable. - /// E.g. if at the beginning you see code like - /// if (typeof define === "function" && define.amd) - final bool usesRequireJs; - - const JsLibrary({ - required this.contextName, - required this.url, - required this.usesRequireJs, - }); -} - -abstract class WebBarcodeReaderBase { - /// Timer used to capture frames to be analyzed - Duration frameInterval = const Duration(milliseconds: 200); - final html.DivElement videoContainer; - - WebBarcodeReaderBase({ - required this.videoContainer, - }); - - bool get isStarted; - - int get videoWidth; - int get videoHeight; - - /// Starts streaming video - Future start({ - required CameraFacing cameraFacing, - List? formats, - Duration? detectionTimeout, - }); - - /// Starts scanning QR codes or barcodes - Stream detectBarcodeContinuously(); - - /// Stops scanning QR codes or barcodes - Future stopDetectBarcodeContinuously(); - - /// Stops streaming video - Future stop(); - - /// Can enable or disable the flash if available - Future toggleTorch({required bool enabled}); - - /// Determine whether device has flash - Future hasTorch(); -} - -mixin InternalStreamCreation on WebBarcodeReaderBase { - /// The video stream. - /// Will be initialized later to see which camera needs to be used. - html.MediaStream? localMediaStream; - final html.VideoElement video = html.VideoElement(); - - @override - int get videoWidth => video.videoWidth; - @override - int get videoHeight => video.videoHeight; - - Future initMediaStream(CameraFacing cameraFacing) async { - // Check if browser supports multiple camera's and set if supported - final Map? capabilities = - html.window.navigator.mediaDevices?.getSupportedConstraints(); - final Map constraints; - if (capabilities != null && capabilities['facingMode'] as bool) { - constraints = { - 'video': VideoOptions( - facingMode: - cameraFacing == CameraFacing.front ? 'user' : 'environment', - ), - }; - } else { - constraints = {'video': true}; - } - final stream = - await html.window.navigator.mediaDevices?.getUserMedia(constraints); - return stream; - } - - void prepareVideoElement(html.VideoElement videoSource); - - Future attachStreamToVideo( - html.MediaStream stream, - html.VideoElement videoSource, - ); - - @override - Future stop() async { - try { - // Stop the camera stream - localMediaStream?.getTracks().forEach((track) { - if (track.readyState == 'live') { - track.stop(); - } - }); - } catch (e) { - debugPrint('Failed to stop stream: $e'); - } - video.srcObject = null; - localMediaStream = null; - videoContainer.children = []; - } -} - -/// Mixin for libraries that don't have built-in torch support -mixin InternalTorchDetection on InternalStreamCreation { - Future> getSupportedTorchStates() async { - try { - final track = localMediaStream?.getVideoTracks(); - if (track != null) { - final imageCapture = ImageCapture(track.first); - final photoCapabilities = await promiseToFuture( - imageCapture.getPhotoCapabilities(), - ); - - return photoCapabilities.fillLightMode; - } - } catch (e) { - // ImageCapture is not supported by some browsers: - // https://developer.mozilla.org/en-US/docs/Web/API/ImageCapture#browser_compatibility - } - return []; - } - - @override - Future hasTorch() async { - return (await getSupportedTorchStates()).isNotEmpty; - } - - @override - Future toggleTorch({required bool enabled}) async { - final hasTorch = await this.hasTorch(); - if (hasTorch) { - final track = localMediaStream?.getVideoTracks(); - await track?.first.applyConstraints({ - 'advanced': [ - {'torch': enabled}, - ], - }); - } - } -} - -@JS('Promise') -@staticInterop -class Promise {} - -@JS() -@anonymous -@staticInterop -class PhotoCapabilities {} - -extension PhotoCapabilitiesExtension on PhotoCapabilities { - @JS('fillLightMode') - external List? get _fillLightMode; - - /// Returns an array of available fill light options. Options include auto, off, or flash. - List get fillLightMode => - _fillLightMode?.cast() ?? []; -} - -@JS('ImageCapture') -@staticInterop -class ImageCapture { - /// MediaStreamTrack - external factory ImageCapture(dynamic track); -} - -extension ImageCaptureExt on ImageCapture { - external Promise getPhotoCapabilities(); -} - -@JS('Map') -@staticInterop -class JsMap { - external factory JsMap(); -} - -extension JsMapExt on JsMap { - external void set(dynamic key, dynamic value); - external dynamic get(dynamic key); -} From c4012192cda06713fb29176081dc2660bf3e1bb6 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 7 Dec 2023 14:15:38 +0100 Subject: [PATCH 098/161] reimplement flashlight delegate & use media capabilities which is more supported --- lib/src/web/flashlight_delegate.dart | 78 ++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 lib/src/web/flashlight_delegate.dart diff --git a/lib/src/web/flashlight_delegate.dart b/lib/src/web/flashlight_delegate.dart new file mode 100644 index 000000000..be07edcd3 --- /dev/null +++ b/lib/src/web/flashlight_delegate.dart @@ -0,0 +1,78 @@ +import 'dart:js_interop'; + +import 'package:mobile_scanner/src/enums/torch_state.dart'; +import 'package:web/web.dart'; + +/// This class represents a flashlight delegate for the web platform. +/// +/// It provides an interface to query and update the flashlight state of a [MediaStream]. +final class FlashlightDelegate { + /// Returns a list of supported flashlight modes for the given [mediaStream]. + /// + /// The [TorchState.off] mode is always supported, regardless of the return value. + Future> getSupportedFlashlightModes(MediaStream? mediaStream) async { + if (mediaStream == null) { + return []; + } + + final List tracks = mediaStream.getVideoTracks().toDart; + + if (tracks.isEmpty) { + return []; + } + + final MediaStreamTrack? track = tracks.first as MediaStreamTrack?; + + if (track == null) { + return []; + } + + try { + final MediaTrackCapabilities capabilities = track.getCapabilities(); + + return [ + if (capabilities.torch) TorchState.on, + ]; + } catch (_) { + // Firefox does not support getCapabilities() yet. + // https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/getCapabilities#browser_compatibility + + return []; + } + } + + /// Returns whether the given [mediaStream] has a flashlight. + Future hasFlashlight(MediaStream? mediaStream) async { + return (await getSupportedFlashlightModes(mediaStream)).isNotEmpty; + } + + /// Set the flashlight state of the given [mediaStream] to the given [value]. + Future setFlashlightState(MediaStream? mediaStream, TorchState value) async { + if (mediaStream == null) { + return; + } + + if (await hasFlashlight(mediaStream)) { + final List tracks = mediaStream.getVideoTracks().toDart; + + if (tracks.isEmpty) { + return; + } + + final bool flashlightEnabled = switch (value) { + TorchState.on => true, + TorchState.off || TorchState.unavailable => false, + }; + + final MediaStreamTrack? track = tracks.first as MediaStreamTrack?; + + final MediaTrackConstraints constraints = MediaTrackConstraints( + advanced: [ + {'torch': flashlightEnabled}.jsify(), + ].toJS, + ); + + await track?.applyConstraints(constraints).toDart; + } + } +} From 2750e1acb6c1b71b376576eaa05be6169c2f28ab Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 7 Dec 2023 14:16:15 +0100 Subject: [PATCH 099/161] implement size getter --- lib/src/web/zxing/zxing_barcode_reader.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/src/web/zxing/zxing_barcode_reader.dart b/lib/src/web/zxing/zxing_barcode_reader.dart index 518d173f4..992aa4ab7 100644 --- a/lib/src/web/zxing/zxing_barcode_reader.dart +++ b/lib/src/web/zxing/zxing_barcode_reader.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:js_interop'; +import 'dart:ui'; import 'package:js/js.dart'; import 'package:mobile_scanner/src/enums/barcode_format.dart'; @@ -21,6 +22,20 @@ final class ZXingBarcodeReader extends BarcodeReader { @override bool get isScanning => _reader?.stream != null; + @override + Size get videoSize { + final web.HTMLVideoElement? videoElement = _reader?.videoElement; + + if (videoElement == null) { + return Size.zero; + } + + return Size( + videoElement.videoWidth.toDouble(), + videoElement.videoHeight.toDouble(), + ); + } + @override String get scriptUrl => 'https://unpkg.com/@zxing/library@0.19.1'; From 019526a3eb7406d880eb7e8656de24118ca73db1 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 7 Dec 2023 15:36:32 +0100 Subject: [PATCH 100/161] add flashlight delegate to barcode reader base --- lib/src/web/barcode_reader.dart | 4 ++++ lib/src/web/flashlight_delegate.dart | 3 +++ 2 files changed, 7 insertions(+) diff --git a/lib/src/web/barcode_reader.dart b/lib/src/web/barcode_reader.dart index ccbda769b..be682362b 100644 --- a/lib/src/web/barcode_reader.dart +++ b/lib/src/web/barcode_reader.dart @@ -7,12 +7,16 @@ import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; import 'package:mobile_scanner/src/objects/barcode_capture.dart'; import 'package:mobile_scanner/src/objects/start_options.dart'; +import 'package:mobile_scanner/src/web/flashlight_delegate.dart'; import 'package:web/web.dart'; /// This class represents the base interface for a barcode reader implementation. abstract class BarcodeReader { const BarcodeReader(); + /// Get the flashlight delegate for the barcode reader. + FlashlightDelegate get flashlightDelegate => const FlashlightDelegate(); + /// Whether the scanner is currently scanning for barcodes. bool get isScanning { throw UnimplementedError('isScanning has not been implemented.'); diff --git a/lib/src/web/flashlight_delegate.dart b/lib/src/web/flashlight_delegate.dart index be07edcd3..041e64074 100644 --- a/lib/src/web/flashlight_delegate.dart +++ b/lib/src/web/flashlight_delegate.dart @@ -7,6 +7,9 @@ import 'package:web/web.dart'; /// /// It provides an interface to query and update the flashlight state of a [MediaStream]. final class FlashlightDelegate { + /// Constructs a [FlashlightDelegate] instance. + const FlashlightDelegate(); + /// Returns a list of supported flashlight modes for the given [mediaStream]. /// /// The [TorchState.off] mode is always supported, regardless of the return value. From 26c6ba80ef7d2cde313b0d45790b9c47a1887040 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 7 Dec 2023 16:09:23 +0100 Subject: [PATCH 101/161] add hasTorch & setTorchState to the barcode reader interface instead --- lib/src/web/barcode_reader.dart | 15 +++++++++++---- lib/src/web/mobile_scanner_web.dart | 6 ++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/src/web/barcode_reader.dart b/lib/src/web/barcode_reader.dart index be682362b..7915eb25c 100644 --- a/lib/src/web/barcode_reader.dart +++ b/lib/src/web/barcode_reader.dart @@ -4,19 +4,16 @@ import 'dart:ui'; import 'package:js/js.dart'; import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; +import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; import 'package:mobile_scanner/src/objects/barcode_capture.dart'; import 'package:mobile_scanner/src/objects/start_options.dart'; -import 'package:mobile_scanner/src/web/flashlight_delegate.dart'; import 'package:web/web.dart'; /// This class represents the base interface for a barcode reader implementation. abstract class BarcodeReader { const BarcodeReader(); - /// Get the flashlight delegate for the barcode reader. - FlashlightDelegate get flashlightDelegate => const FlashlightDelegate(); - /// Whether the scanner is currently scanning for barcodes. bool get isScanning { throw UnimplementedError('isScanning has not been implemented.'); @@ -45,6 +42,11 @@ abstract class BarcodeReader { throw UnimplementedError('detectBarcodes() has not been implemented.'); } + /// Check whether the active camera has a flashlight. + Future hasTorch() { + throw UnimplementedError('hasTorch() has not been implemented.'); + } + /// Load the barcode reader library. /// /// Does nothing if the library is already loaded. @@ -93,6 +95,11 @@ abstract class BarcodeReader { await completer.future; } + /// Set the torch state for the active camera to the given [value]. + Future setTorchState(TorchState value) { + throw UnimplementedError('setTorchState() has not been implemented.'); + } + /// Start the barcode reader and initialize the video stream. /// /// The [options] are used to configure the barcode reader. diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index fdb2eb2a7..023618f48 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -115,12 +115,10 @@ class MobileScannerWeb extends MobileScannerPlatform { _barcodesController.add(barcode); }); - final bool hasTorch = _barcodeReader.hasTorch; + final bool hasTorch = await _barcodeReader.hasTorch(); if (hasTorch && startOptions.torchEnabled) { - await _barcodeReader.setTorchState(TorchState.on).catchError((_) { - // If the torch could not be turned on, continue the camera session anyway. - }); + await _barcodeReader.setTorchState(TorchState.on); } return MobileScannerViewAttributes( From e1ffa1f04095b6102fbf4def641d22f1fe3990c4 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 7 Dec 2023 16:33:12 +0100 Subject: [PATCH 102/161] implement torch state in ZXing barcode reader --- lib/src/web/zxing/zxing_barcode_reader.dart | 33 +++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/lib/src/web/zxing/zxing_barcode_reader.dart b/lib/src/web/zxing/zxing_barcode_reader.dart index 992aa4ab7..abed64531 100644 --- a/lib/src/web/zxing/zxing_barcode_reader.dart +++ b/lib/src/web/zxing/zxing_barcode_reader.dart @@ -5,9 +5,11 @@ import 'dart:ui'; import 'package:js/js.dart'; import 'package:mobile_scanner/src/enums/barcode_format.dart'; import 'package:mobile_scanner/src/enums/camera_facing.dart'; +import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/objects/barcode_capture.dart'; import 'package:mobile_scanner/src/objects/start_options.dart'; import 'package:mobile_scanner/src/web/barcode_reader.dart'; +import 'package:mobile_scanner/src/web/flashlight_delegate.dart'; import 'package:mobile_scanner/src/web/zxing/result.dart'; import 'package:mobile_scanner/src/web/zxing/zxing_browser_multi_format_reader.dart'; import 'package:web/web.dart' as web; @@ -16,6 +18,9 @@ import 'package:web/web.dart' as web; final class ZXingBarcodeReader extends BarcodeReader { ZXingBarcodeReader(); + /// The internal flashlight delegate. + final FlashlightDelegate _flashlightDelegate = const FlashlightDelegate(); + /// The internal barcode reader. ZXingBrowserMultiFormatReader? _reader; @@ -163,6 +168,34 @@ final class ZXingBarcodeReader extends BarcodeReader { return controller.stream; } + @override + Future hasTorch() { + final web.MediaStream? mediaStream = _reader?.stream; + + if (mediaStream == null) { + return Future.value(false); + } + + return _flashlightDelegate.hasFlashlight(mediaStream); + } + + @override + Future setTorchState(TorchState value) { + switch (value) { + case TorchState.unavailable: + return Future.value(); + case TorchState.off: + case TorchState.on: + final web.MediaStream? mediaStream = _reader?.stream; + + if (mediaStream == null) { + return Future.value(); + } + + return _flashlightDelegate.setFlashlightState(mediaStream, value); + } + } + @override Future start(StartOptions options, {required web.HTMLElement containerElement}) async { final int detectionTimeoutMs = options.detectionTimeoutMs; From 7521a64c1dd241b5658a1f8e93bc5eecc7d7cadf Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 29 Dec 2023 10:57:54 +0100 Subject: [PATCH 103/161] clear barocdes on the web before restarting; forward barcode stream on web --- lib/src/web/mobile_scanner_web.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index 023618f48..7a880efcb 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -40,6 +40,9 @@ class MobileScannerWeb extends MobileScannerPlatform { MobileScannerPlatform.instance = MobileScannerWeb(); } + @override + Stream get barcodesStream => _barcodesController.stream; + @override Widget buildCameraView() { if (!_barcodeReader.isScanning) { @@ -80,6 +83,9 @@ class MobileScannerWeb extends MobileScannerPlatform { } try { + // Clear the existing barcodes. + _barcodesController.add(const BarcodeCapture()); + await _barcodeReader.start( startOptions, containerElement: _divElement!, @@ -145,9 +151,6 @@ class MobileScannerWeb extends MobileScannerPlatform { _barcodesSubscription?.cancel(); _barcodesSubscription = null; - // Clear the existing barcodes. - _barcodesController.add(const BarcodeCapture()); - await _barcodeReader.stop(); } From 2270939511d30338e75739fee3b2cca3935fae3c Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 29 Dec 2023 10:59:35 +0100 Subject: [PATCH 104/161] add default case --- lib/src/web/zxing/zxing_barcode_reader.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/web/zxing/zxing_barcode_reader.dart b/lib/src/web/zxing/zxing_barcode_reader.dart index abed64531..dd3cc366a 100644 --- a/lib/src/web/zxing/zxing_barcode_reader.dart +++ b/lib/src/web/zxing/zxing_barcode_reader.dart @@ -75,6 +75,7 @@ final class ZXingBarcodeReader extends BarcodeReader { return 15; case BarcodeFormat.unknown: case BarcodeFormat.all: + default: return -1; } } From 66f88993bf8f62c491342967834c080478869a26 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 29 Dec 2023 11:46:31 +0100 Subject: [PATCH 105/161] cherry pick available cameras into new implementation --- .../method_channel/mobile_scanner_method_channel.dart | 7 ++++++- lib/src/mobile_scanner_controller.dart | 1 + lib/src/mobile_scanner_view_attributes.dart | 4 ++++ lib/src/objects/mobile_scanner_state.dart | 9 +++++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index 73657d9a5..f71e996f9 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -264,6 +264,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { _textureId = textureId; + final int? numberOfCameras = startResult['numberOfCameras'] as int?; final bool hasTorch = startResult['torchable'] as bool? ?? false; final Map? sizeInfo = @@ -279,7 +280,11 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { size = Size(width, height); } - return MobileScannerViewAttributes(hasTorch: hasTorch, size: size); + return MobileScannerViewAttributes( + hasTorch: hasTorch, + numberOfCameras: numberOfCameras, + size: size, + ); } @override diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 4f8e93387..eda0424f4 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -239,6 +239,7 @@ class MobileScannerController extends ValueNotifier { ); value = value.copyWith( + availableCameras: viewAttributes.numberOfCameras, cameraDirection: effectiveDirection, isInitialized: true, isRunning: true, diff --git a/lib/src/mobile_scanner_view_attributes.dart b/lib/src/mobile_scanner_view_attributes.dart index d7ec62be7..1e5d72b1a 100644 --- a/lib/src/mobile_scanner_view_attributes.dart +++ b/lib/src/mobile_scanner_view_attributes.dart @@ -4,12 +4,16 @@ import 'dart:ui'; class MobileScannerViewAttributes { const MobileScannerViewAttributes({ required this.hasTorch, + this.numberOfCameras, required this.size, }); /// Whether the current active camera has a torch. final bool hasTorch; + /// The number of available cameras. + final int? numberOfCameras; + /// The size of the camera output. final Size size; } diff --git a/lib/src/objects/mobile_scanner_state.dart b/lib/src/objects/mobile_scanner_state.dart index eeaf12311..c373bec39 100644 --- a/lib/src/objects/mobile_scanner_state.dart +++ b/lib/src/objects/mobile_scanner_state.dart @@ -8,6 +8,7 @@ import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; class MobileScannerState { /// Create a new [MobileScannerState] instance. const MobileScannerState({ + required this.availableCameras, required this.cameraDirection, required this.isInitialized, required this.isRunning, @@ -20,6 +21,7 @@ class MobileScannerState { /// Create a new [MobileScannerState] instance that is uninitialized. const MobileScannerState.uninitialized(CameraFacing facing) : this( + availableCameras: null, cameraDirection: facing, isInitialized: false, isRunning: false, @@ -28,6 +30,11 @@ class MobileScannerState { zoomScale: 1.0, ); + /// The number of available cameras. + /// + /// This is null if the number of cameras is unknown. + final int? availableCameras; + /// The facing direction of the camera. final CameraFacing cameraDirection; @@ -55,6 +62,7 @@ class MobileScannerState { /// Create a copy of this state with the given parameters. MobileScannerState copyWith({ + int? availableCameras, CameraFacing? cameraDirection, MobileScannerException? error, bool? isInitialized, @@ -64,6 +72,7 @@ class MobileScannerState { double? zoomScale, }) { return MobileScannerState( + availableCameras: availableCameras ?? this.availableCameras, cameraDirection: cameraDirection ?? this.cameraDirection, error: error, isInitialized: isInitialized ?? this.isInitialized, From 7fc83a3c3ee20536d13807ee7d0b3b511be7590c Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 29 Dec 2023 12:02:28 +0100 Subject: [PATCH 106/161] add camera count check in switch camera button --- example/lib/scanner_button_widgets.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/example/lib/scanner_button_widgets.dart b/example/lib/scanner_button_widgets.dart index 61a729c6f..427441cdc 100644 --- a/example/lib/scanner_button_widgets.dart +++ b/example/lib/scanner_button_widgets.dart @@ -96,6 +96,12 @@ class SwitchCameraButton extends StatelessWidget { return const SizedBox.shrink(); } + final int? availableCameras = state.availableCameras; + + if (availableCameras != null && availableCameras < 2) { + return const SizedBox.shrink(); + } + final Widget icon; switch (state.cameraDirection) { From eeacd392ee1b5f48884966a8c4a9749bc356150b Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 29 Dec 2023 12:28:15 +0100 Subject: [PATCH 107/161] add comment for barcode reader cancel on web --- lib/src/web/mobile_scanner_web.dart | 3 ++- lib/src/web/zxing/zxing_barcode_reader.dart | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index 7a880efcb..97b2a9b6c 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -148,7 +148,8 @@ class MobileScannerWeb extends MobileScannerPlatform { return; } - _barcodesSubscription?.cancel(); + // Ensure the barcode scanner is stopped, by cancelling the subscription. + await _barcodesSubscription?.cancel(); _barcodesSubscription = null; await _barcodeReader.stop(); diff --git a/lib/src/web/zxing/zxing_barcode_reader.dart b/lib/src/web/zxing/zxing_barcode_reader.dart index dd3cc366a..46a5bd1bf 100644 --- a/lib/src/web/zxing/zxing_barcode_reader.dart +++ b/lib/src/web/zxing/zxing_barcode_reader.dart @@ -160,6 +160,9 @@ final class ZXingBarcodeReader extends BarcodeReader { ); }; + // The onCancel() method of the controller is called + // when the stream subscription returned by this method is cancelled in `MobileScannerWeb.stop()`. + // This avoids both leaving the barcode scanner running and a memory leak for the stream subscription. controller.onCancel = () async { _reader?.stopContinuousDecode.callAsFunction(); _reader?.reset.callAsFunction(); From 289a72ce0176093d0542188ece240b795c73ee76 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 29 Dec 2023 12:57:55 +0100 Subject: [PATCH 108/161] fix typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00d054af3..c793fb1e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ BREAKING CHANGES: The `TorchState` enum now provides a new value for unavailable flashlights. * The `autoStart` attribute has been removed from the `MobileScannerController`. The controller should be manually started on-demand. * A controller is now required for the `MobileScanner` widget. -* The `onPremissionSet`, `onStart` and `onScannerStarted` methods have been removed from the `MobileScanner` widget. Instead, await `MobileScannerController.start()`. +* The `onPermissionSet`, `onStart` and `onScannerStarted` methods have been removed from the `MobileScanner` widget. Instead, await `MobileScannerController.start()`. * The `startDelay` has been removed from the `MobileScanner` widget. Instead, use a delay between manual starts of one or more controllers. * The `onDetect` method has been removed from the `MobileScanner` widget. Instead, listen to `MobileScannerController.barcodes` directly. * The `overlay` widget of the `MobileScanner` has been replaced by a new property, `overlayBuilder`, which provides the constraints for the overlay. From 3b7ef82a00a40c3249fa4ba732d543a8de7c0de1 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 29 Dec 2023 13:06:02 +0100 Subject: [PATCH 109/161] rename flashlight delegate --- ...gate.dart => media_track_constraints_delegate.dart} | 10 ++++------ lib/src/web/zxing/zxing_barcode_reader.dart | 6 +++--- 2 files changed, 7 insertions(+), 9 deletions(-) rename lib/src/web/{flashlight_delegate.dart => media_track_constraints_delegate.dart} (88%) diff --git a/lib/src/web/flashlight_delegate.dart b/lib/src/web/media_track_constraints_delegate.dart similarity index 88% rename from lib/src/web/flashlight_delegate.dart rename to lib/src/web/media_track_constraints_delegate.dart index 041e64074..96495454e 100644 --- a/lib/src/web/flashlight_delegate.dart +++ b/lib/src/web/media_track_constraints_delegate.dart @@ -3,12 +3,10 @@ import 'dart:js_interop'; import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:web/web.dart'; -/// This class represents a flashlight delegate for the web platform. -/// -/// It provides an interface to query and update the flashlight state of a [MediaStream]. -final class FlashlightDelegate { - /// Constructs a [FlashlightDelegate] instance. - const FlashlightDelegate(); +/// This class represents a delegate that manages the constraints for a [MediaStreamTrack]. +final class MediaTrackConstraintsDelegate { + /// Constructs a [MediaTrackConstraintsDelegate] instance. + const MediaTrackConstraintsDelegate(); /// Returns a list of supported flashlight modes for the given [mediaStream]. /// diff --git a/lib/src/web/zxing/zxing_barcode_reader.dart b/lib/src/web/zxing/zxing_barcode_reader.dart index 46a5bd1bf..7085c58e1 100644 --- a/lib/src/web/zxing/zxing_barcode_reader.dart +++ b/lib/src/web/zxing/zxing_barcode_reader.dart @@ -9,7 +9,7 @@ import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/objects/barcode_capture.dart'; import 'package:mobile_scanner/src/objects/start_options.dart'; import 'package:mobile_scanner/src/web/barcode_reader.dart'; -import 'package:mobile_scanner/src/web/flashlight_delegate.dart'; +import 'package:mobile_scanner/src/web/media_track_constraints_delegate.dart'; import 'package:mobile_scanner/src/web/zxing/result.dart'; import 'package:mobile_scanner/src/web/zxing/zxing_browser_multi_format_reader.dart'; import 'package:web/web.dart' as web; @@ -18,8 +18,8 @@ import 'package:web/web.dart' as web; final class ZXingBarcodeReader extends BarcodeReader { ZXingBarcodeReader(); - /// The internal flashlight delegate. - final FlashlightDelegate _flashlightDelegate = const FlashlightDelegate(); + /// The internal media stream track constraints delegate. + final MediaTrackConstraintsDelegate _flashlightDelegate = const MediaTrackConstraintsDelegate(); /// The internal barcode reader. ZXingBrowserMultiFormatReader? _reader; From 5568383d21dfd4ead9dc2755cbb625929f57d240 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 29 Dec 2023 13:13:47 +0100 Subject: [PATCH 110/161] add get constraints method --- lib/src/web/media_track_constraints_delegate.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/src/web/media_track_constraints_delegate.dart b/lib/src/web/media_track_constraints_delegate.dart index 96495454e..ab6aa325b 100644 --- a/lib/src/web/media_track_constraints_delegate.dart +++ b/lib/src/web/media_track_constraints_delegate.dart @@ -8,6 +8,19 @@ final class MediaTrackConstraintsDelegate { /// Constructs a [MediaTrackConstraintsDelegate] instance. const MediaTrackConstraintsDelegate(); + /// Get the constraints for the given [mediaStream]. + MediaTrackConstraints? getConstraints(MediaStream? mediaStream) { + final List tracks = mediaStream?.getVideoTracks().toDart ?? const []; + + if (tracks.isEmpty) { + return null; + } + + final MediaStreamTrack? track = tracks.first as MediaStreamTrack?; + + return track?.getConstraints(); + } + /// Returns a list of supported flashlight modes for the given [mediaStream]. /// /// The [TorchState.off] mode is always supported, regardless of the return value. From 51c6e2775edcb433f700a62b87af36c01f1cabd0 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 29 Dec 2023 13:29:52 +0100 Subject: [PATCH 111/161] manage media stream constraints listener --- lib/src/web/barcode_reader.dart | 7 +++++++ lib/src/web/mobile_scanner_web.dart | 15 +++++++++++++++ lib/src/web/zxing/zxing_barcode_reader.dart | 6 +++--- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/src/web/barcode_reader.dart b/lib/src/web/barcode_reader.dart index 7915eb25c..98aa344ff 100644 --- a/lib/src/web/barcode_reader.dart +++ b/lib/src/web/barcode_reader.dart @@ -95,6 +95,13 @@ abstract class BarcodeReader { await completer.future; } + /// Set a listener for the media stream constraints. + void setMediaTrackConstraintsListener(void Function(MediaTrackConstraints) listener) { + throw UnimplementedError( + 'setMediaTrackConstraintsListener() has not been implemented.', + ); + } + /// Set the torch state for the active camera to the given [value]. Future setTorchState(TorchState value) { throw UnimplementedError('setTorchState() has not been implemented.'); diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index 97b2a9b6c..99f33e4dd 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -33,6 +33,9 @@ class MobileScannerWeb extends MobileScannerPlatform { /// This container element is used by the barcode reader. HTMLDivElement? _divElement; + /// The stream controller for the media track constraints stream. + final StreamController _constraintsController = StreamController.broadcast(); + /// The view type for the platform view factory. final String _viewType = 'MobileScannerWeb'; @@ -43,6 +46,14 @@ class MobileScannerWeb extends MobileScannerPlatform { @override Stream get barcodesStream => _barcodesController.stream; + void _handleMediaTrackConstraintsChange(MediaTrackConstraints constraints) { + if (_constraintsController.isClosed) { + return; + } + + _constraintsController.add(constraints); + } + @override Widget buildCameraView() { if (!_barcodeReader.isScanning) { @@ -86,6 +97,9 @@ class MobileScannerWeb extends MobileScannerPlatform { // Clear the existing barcodes. _barcodesController.add(const BarcodeCapture()); + // Listen for changes to the media track constraints. + _barcodeReader.setMediaTrackConstraintsListener(_handleMediaTrackConstraintsChange); + await _barcodeReader.start( startOptions, containerElement: _divElement!, @@ -170,5 +184,6 @@ class MobileScannerWeb extends MobileScannerPlatform { await stop(); await _barcodesController.close(); + await _constraintsController.close(); } } diff --git a/lib/src/web/zxing/zxing_barcode_reader.dart b/lib/src/web/zxing/zxing_barcode_reader.dart index 7085c58e1..6ece97836 100644 --- a/lib/src/web/zxing/zxing_barcode_reader.dart +++ b/lib/src/web/zxing/zxing_barcode_reader.dart @@ -19,7 +19,7 @@ final class ZXingBarcodeReader extends BarcodeReader { ZXingBarcodeReader(); /// The internal media stream track constraints delegate. - final MediaTrackConstraintsDelegate _flashlightDelegate = const MediaTrackConstraintsDelegate(); + final MediaTrackConstraintsDelegate _mediaTrackConstraintsDelegate = const MediaTrackConstraintsDelegate(); /// The internal barcode reader. ZXingBrowserMultiFormatReader? _reader; @@ -180,7 +180,7 @@ final class ZXingBarcodeReader extends BarcodeReader { return Future.value(false); } - return _flashlightDelegate.hasFlashlight(mediaStream); + return _mediaTrackConstraintsDelegate.hasFlashlight(mediaStream); } @override @@ -196,7 +196,7 @@ final class ZXingBarcodeReader extends BarcodeReader { return Future.value(); } - return _flashlightDelegate.setFlashlightState(mediaStream, value); + return _mediaTrackConstraintsDelegate.setFlashlightState(mediaStream, value); } } From c31beb80605bce6dc63afcdc3394be5747a4ab9d Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 29 Dec 2023 13:39:05 +0100 Subject: [PATCH 112/161] manage constraints listener for ZXing --- lib/src/web/zxing/zxing_barcode_reader.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/src/web/zxing/zxing_barcode_reader.dart b/lib/src/web/zxing/zxing_barcode_reader.dart index 6ece97836..ee8913995 100644 --- a/lib/src/web/zxing/zxing_barcode_reader.dart +++ b/lib/src/web/zxing/zxing_barcode_reader.dart @@ -18,6 +18,9 @@ import 'package:web/web.dart' as web; final class ZXingBarcodeReader extends BarcodeReader { ZXingBarcodeReader(); + /// The listener for media track constraints changes. + void Function(web.MediaTrackConstraints)? _onMediaTrackConstraintsChanged; + /// The internal media stream track constraints delegate. final MediaTrackConstraintsDelegate _mediaTrackConstraintsDelegate = const MediaTrackConstraintsDelegate(); @@ -183,6 +186,11 @@ final class ZXingBarcodeReader extends BarcodeReader { return _mediaTrackConstraintsDelegate.hasFlashlight(mediaStream); } + @override + void setMediaTrackConstraintsListener(void Function(web.MediaTrackConstraints) listener) { + _onMediaTrackConstraintsChanged ??= listener; + } + @override Future setTorchState(TorchState value) { switch (value) { @@ -234,6 +242,7 @@ final class ZXingBarcodeReader extends BarcodeReader { @override Future stop() async { + _onMediaTrackConstraintsChanged = null; _reader?.stopContinuousDecode.callAsFunction(); _reader?.reset.callAsFunction(); _reader = null; From 15d3557b7b6bb807164992b928fafc6cf3dcc522 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 29 Dec 2023 13:46:16 +0100 Subject: [PATCH 113/161] handle media track constraints updates --- lib/src/web/zxing/zxing_barcode_reader.dart | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/src/web/zxing/zxing_barcode_reader.dart b/lib/src/web/zxing/zxing_barcode_reader.dart index ee8913995..6db937509 100644 --- a/lib/src/web/zxing/zxing_barcode_reader.dart +++ b/lib/src/web/zxing/zxing_barcode_reader.dart @@ -138,6 +138,12 @@ final class ZXingBarcodeReader extends BarcodeReader { final JSPromise? result = _reader?.attachStreamToVideo.callAsFunction(null, stream, videoElement) as JSPromise?; await result?.toDart; + + final web.MediaTrackConstraints? constraints = _mediaTrackConstraintsDelegate.getConstraints(stream); + + if (constraints != null) { + _onMediaTrackConstraintsChanged?.call(constraints); + } } } @@ -192,7 +198,7 @@ final class ZXingBarcodeReader extends BarcodeReader { } @override - Future setTorchState(TorchState value) { + Future setTorchState(TorchState value) async { switch (value) { case TorchState.unavailable: return Future.value(); @@ -204,7 +210,13 @@ final class ZXingBarcodeReader extends BarcodeReader { return Future.value(); } - return _mediaTrackConstraintsDelegate.setFlashlightState(mediaStream, value); + await _mediaTrackConstraintsDelegate.setFlashlightState(mediaStream, value); + + final web.MediaTrackConstraints? constraints = _mediaTrackConstraintsDelegate.getConstraints(mediaStream); + + if (constraints != null) { + _onMediaTrackConstraintsChanged?.call(constraints); + } } } From 9fa1d014356756f0ef0f75d328ffea808e5621ef Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 29 Dec 2023 14:15:26 +0100 Subject: [PATCH 114/161] remove the video element from the DOM when disposing the controller --- lib/src/web/mobile_scanner_web.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index 99f33e4dd..1866f0710 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -185,5 +185,22 @@ class MobileScannerWeb extends MobileScannerPlatform { await stop(); await _barcodesController.close(); await _constraintsController.close(); + + // Finally, remove the video element from the DOM. + try { + final HTMLCollection? divChildren = _divElement?.children; + + if (divChildren != null) { + for (int i = 0; i < divChildren.length; i++) { + final Node? child = divChildren.item(i); + + if (child != null) { + _divElement?.removeChild(child); + } + } + } + } catch (_) { + // The video element was no longer a child of the container element. + } } } From b99652db930f1ce0139405c15211996fa958e9ab Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 29 Dec 2023 15:07:03 +0100 Subject: [PATCH 115/161] use MediaTrackSettings instead of constraints --- lib/src/web/barcode_reader.dart | 4 ++-- .../web/media_track_constraints_delegate.dart | 6 ++--- lib/src/web/mobile_scanner_web.dart | 16 +++++++------- lib/src/web/zxing/zxing_barcode_reader.dart | 22 +++++++++---------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/src/web/barcode_reader.dart b/lib/src/web/barcode_reader.dart index 98aa344ff..c0c62f9cb 100644 --- a/lib/src/web/barcode_reader.dart +++ b/lib/src/web/barcode_reader.dart @@ -95,8 +95,8 @@ abstract class BarcodeReader { await completer.future; } - /// Set a listener for the media stream constraints. - void setMediaTrackConstraintsListener(void Function(MediaTrackConstraints) listener) { + /// Set a listener for the media stream settings. + void setMediaTrackSettingsListener(void Function(MediaTrackSettings) listener) { throw UnimplementedError( 'setMediaTrackConstraintsListener() has not been implemented.', ); diff --git a/lib/src/web/media_track_constraints_delegate.dart b/lib/src/web/media_track_constraints_delegate.dart index ab6aa325b..dfa22dfa5 100644 --- a/lib/src/web/media_track_constraints_delegate.dart +++ b/lib/src/web/media_track_constraints_delegate.dart @@ -8,8 +8,8 @@ final class MediaTrackConstraintsDelegate { /// Constructs a [MediaTrackConstraintsDelegate] instance. const MediaTrackConstraintsDelegate(); - /// Get the constraints for the given [mediaStream]. - MediaTrackConstraints? getConstraints(MediaStream? mediaStream) { + /// Get the settings for the given [mediaStream]. + MediaTrackSettings? getSettings(MediaStream? mediaStream) { final List tracks = mediaStream?.getVideoTracks().toDart ?? const []; if (tracks.isEmpty) { @@ -18,7 +18,7 @@ final class MediaTrackConstraintsDelegate { final MediaStreamTrack? track = tracks.first as MediaStreamTrack?; - return track?.getConstraints(); + return track?.getSettings(); } /// Returns a list of supported flashlight modes for the given [mediaStream]. diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index 1866f0710..25795d278 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -33,8 +33,8 @@ class MobileScannerWeb extends MobileScannerPlatform { /// This container element is used by the barcode reader. HTMLDivElement? _divElement; - /// The stream controller for the media track constraints stream. - final StreamController _constraintsController = StreamController.broadcast(); + /// The stream controller for the media track settings stream. + final StreamController _settingsController = StreamController.broadcast(); /// The view type for the platform view factory. final String _viewType = 'MobileScannerWeb'; @@ -46,12 +46,12 @@ class MobileScannerWeb extends MobileScannerPlatform { @override Stream get barcodesStream => _barcodesController.stream; - void _handleMediaTrackConstraintsChange(MediaTrackConstraints constraints) { - if (_constraintsController.isClosed) { + void _handleMediaTrackSettingsChange(MediaTrackSettings settings) { + if (_settingsController.isClosed) { return; } - _constraintsController.add(constraints); + _settingsController.add(settings); } @override @@ -97,8 +97,8 @@ class MobileScannerWeb extends MobileScannerPlatform { // Clear the existing barcodes. _barcodesController.add(const BarcodeCapture()); - // Listen for changes to the media track constraints. - _barcodeReader.setMediaTrackConstraintsListener(_handleMediaTrackConstraintsChange); + // Listen for changes to the media track settings. + _barcodeReader.setMediaTrackSettingsListener(_handleMediaTrackSettingsChange); await _barcodeReader.start( startOptions, @@ -184,7 +184,7 @@ class MobileScannerWeb extends MobileScannerPlatform { await stop(); await _barcodesController.close(); - await _constraintsController.close(); + await _settingsController.close(); // Finally, remove the video element from the DOM. try { diff --git a/lib/src/web/zxing/zxing_barcode_reader.dart b/lib/src/web/zxing/zxing_barcode_reader.dart index 6db937509..9f92c92a6 100644 --- a/lib/src/web/zxing/zxing_barcode_reader.dart +++ b/lib/src/web/zxing/zxing_barcode_reader.dart @@ -18,8 +18,8 @@ import 'package:web/web.dart' as web; final class ZXingBarcodeReader extends BarcodeReader { ZXingBarcodeReader(); - /// The listener for media track constraints changes. - void Function(web.MediaTrackConstraints)? _onMediaTrackConstraintsChanged; + /// The listener for media track settings changes. + void Function(web.MediaTrackSettings)? _onMediaTrackSettingsChanged; /// The internal media stream track constraints delegate. final MediaTrackConstraintsDelegate _mediaTrackConstraintsDelegate = const MediaTrackConstraintsDelegate(); @@ -139,10 +139,10 @@ final class ZXingBarcodeReader extends BarcodeReader { await result?.toDart; - final web.MediaTrackConstraints? constraints = _mediaTrackConstraintsDelegate.getConstraints(stream); + final web.MediaTrackSettings? settings = _mediaTrackConstraintsDelegate.getSettings(stream); - if (constraints != null) { - _onMediaTrackConstraintsChanged?.call(constraints); + if (settings != null) { + _onMediaTrackSettingsChanged?.call(settings); } } } @@ -193,8 +193,8 @@ final class ZXingBarcodeReader extends BarcodeReader { } @override - void setMediaTrackConstraintsListener(void Function(web.MediaTrackConstraints) listener) { - _onMediaTrackConstraintsChanged ??= listener; + void setMediaTrackSettingsListener(void Function(web.MediaTrackSettings) listener) { + _onMediaTrackSettingsChanged ??= listener; } @override @@ -212,10 +212,10 @@ final class ZXingBarcodeReader extends BarcodeReader { await _mediaTrackConstraintsDelegate.setFlashlightState(mediaStream, value); - final web.MediaTrackConstraints? constraints = _mediaTrackConstraintsDelegate.getConstraints(mediaStream); + final web.MediaTrackSettings? settings = _mediaTrackConstraintsDelegate.getSettings(mediaStream); - if (constraints != null) { - _onMediaTrackConstraintsChanged?.call(constraints); + if (settings != null) { + _onMediaTrackSettingsChanged?.call(settings); } } } @@ -254,7 +254,7 @@ final class ZXingBarcodeReader extends BarcodeReader { @override Future stop() async { - _onMediaTrackConstraintsChanged = null; + _onMediaTrackSettingsChanged = null; _reader?.stopContinuousDecode.callAsFunction(); _reader?.reset.callAsFunction(); _reader = null; From d28930d162ae4fe96ba4ea6194f576f17b583e22 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 29 Dec 2023 15:14:38 +0100 Subject: [PATCH 116/161] forward updates for the zoom & torch on web --- lib/src/web/mobile_scanner_web.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index 25795d278..6b1a0a74e 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -46,6 +46,16 @@ class MobileScannerWeb extends MobileScannerPlatform { @override Stream get barcodesStream => _barcodesController.stream; + @override + Stream get torchStateStream => _settingsController.stream.map( + (settings) => settings.torch ? TorchState.on : TorchState.off, + ); + + @override + Stream get zoomScaleStateStream => _settingsController.stream.map( + (settings) => settings.zoom.toDouble(), + ); + void _handleMediaTrackSettingsChange(MediaTrackSettings settings) { if (_settingsController.isClosed) { return; From a51f9f6309310128b4dfb9c244fca5332aa0fe6f Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 29 Dec 2023 15:23:58 +0100 Subject: [PATCH 117/161] format --- lib/src/web/barcode_reader.dart | 4 ++- .../web/media_track_constraints_delegate.dart | 13 ++++--- lib/src/web/mobile_scanner_web.dart | 33 ++++++++++------- lib/src/web/zxing/result.dart | 3 +- lib/src/web/zxing/zxing_barcode_reader.dart | 36 +++++++++++++------ 5 files changed, 61 insertions(+), 28 deletions(-) diff --git a/lib/src/web/barcode_reader.dart b/lib/src/web/barcode_reader.dart index c0c62f9cb..9505592ed 100644 --- a/lib/src/web/barcode_reader.dart +++ b/lib/src/web/barcode_reader.dart @@ -96,7 +96,9 @@ abstract class BarcodeReader { } /// Set a listener for the media stream settings. - void setMediaTrackSettingsListener(void Function(MediaTrackSettings) listener) { + void setMediaTrackSettingsListener( + void Function(MediaTrackSettings) listener, + ) { throw UnimplementedError( 'setMediaTrackConstraintsListener() has not been implemented.', ); diff --git a/lib/src/web/media_track_constraints_delegate.dart b/lib/src/web/media_track_constraints_delegate.dart index dfa22dfa5..4b4e36ad3 100644 --- a/lib/src/web/media_track_constraints_delegate.dart +++ b/lib/src/web/media_track_constraints_delegate.dart @@ -10,9 +10,9 @@ final class MediaTrackConstraintsDelegate { /// Get the settings for the given [mediaStream]. MediaTrackSettings? getSettings(MediaStream? mediaStream) { - final List tracks = mediaStream?.getVideoTracks().toDart ?? const []; + final List? tracks = mediaStream?.getVideoTracks().toDart; - if (tracks.isEmpty) { + if (tracks == null || tracks.isEmpty) { return null; } @@ -24,7 +24,9 @@ final class MediaTrackConstraintsDelegate { /// Returns a list of supported flashlight modes for the given [mediaStream]. /// /// The [TorchState.off] mode is always supported, regardless of the return value. - Future> getSupportedFlashlightModes(MediaStream? mediaStream) async { + Future> getSupportedFlashlightModes( + MediaStream? mediaStream, + ) async { if (mediaStream == null) { return []; } @@ -61,7 +63,10 @@ final class MediaTrackConstraintsDelegate { } /// Set the flashlight state of the given [mediaStream] to the given [value]. - Future setFlashlightState(MediaStream? mediaStream, TorchState value) async { + Future setFlashlightState( + MediaStream? mediaStream, + TorchState value, + ) async { if (mediaStream == null) { return; } diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index 6b1a0a74e..49ecb9df3 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -23,7 +23,8 @@ class MobileScannerWeb extends MobileScannerPlatform { final BarcodeReader _barcodeReader = ZXingBarcodeReader(); /// The stream controller for the barcode stream. - final StreamController _barcodesController = StreamController.broadcast(); + final StreamController _barcodesController = + StreamController.broadcast(); /// The subscription for the barcode stream. StreamSubscription? _barcodesSubscription; @@ -34,7 +35,8 @@ class MobileScannerWeb extends MobileScannerPlatform { HTMLDivElement? _divElement; /// The stream controller for the media track settings stream. - final StreamController _settingsController = StreamController.broadcast(); + final StreamController _settingsController = + StreamController.broadcast(); /// The view type for the platform view factory. final String _viewType = 'MobileScannerWeb'; @@ -70,7 +72,8 @@ class MobileScannerWeb extends MobileScannerPlatform { throw const MobileScannerException( errorCode: MobileScannerErrorCode.controllerUninitialized, errorDetails: MobileScannerErrorDetails( - message: 'The controller was not yet initialized. Call start() before calling buildCameraView().', + message: + 'The controller was not yet initialized. Call start() before calling buildCameraView().', ), ); } @@ -98,7 +101,8 @@ class MobileScannerWeb extends MobileScannerPlatform { throw const MobileScannerException( errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, errorDetails: MobileScannerErrorDetails( - message: 'The scanner was already started. Call stop() before calling start() again.', + message: + 'The scanner was already started. Call stop() before calling start() again.', ), ); } @@ -108,7 +112,9 @@ class MobileScannerWeb extends MobileScannerPlatform { _barcodesController.add(const BarcodeCapture()); // Listen for changes to the media track settings. - _barcodeReader.setMediaTrackSettingsListener(_handleMediaTrackSettingsChange); + _barcodeReader.setMediaTrackSettingsListener( + _handleMediaTrackSettingsChange, + ); await _barcodeReader.start( startOptions, @@ -120,7 +126,8 @@ class MobileScannerWeb extends MobileScannerPlatform { MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError; if (error is DOMException) { - if (errorMessage.contains('NotFoundError') || errorMessage.contains('NotSupportedError')) { + if (errorMessage.contains('NotFoundError') || + errorMessage.contains('NotSupportedError')) { errorCode = MobileScannerErrorCode.unsupported; } else if (errorMessage.contains('NotAllowedError')) { errorCode = MobileScannerErrorCode.permissionDenied; @@ -137,13 +144,15 @@ class MobileScannerWeb extends MobileScannerPlatform { } try { - _barcodesSubscription = _barcodeReader.detectBarcodes().listen((BarcodeCapture barcode) { - if (_barcodesController.isClosed) { - return; - } + _barcodesSubscription = _barcodeReader.detectBarcodes().listen( + (BarcodeCapture barcode) { + if (_barcodesController.isClosed) { + return; + } - _barcodesController.add(barcode); - }); + _barcodesController.add(barcode); + }, + ); final bool hasTorch = await _barcodeReader.hasTorch(); diff --git a/lib/src/web/zxing/result.dart b/lib/src/web/zxing/result.dart index bb3300e63..fd164f042 100644 --- a/lib/src/web/zxing/result.dart +++ b/lib/src/web/zxing/result.dart @@ -95,7 +95,8 @@ extension ResultExt on Result { /// Get the raw bytes of the result. Uint8List? get rawBytes { - final JSUint8Array? rawBytes = getRawBytes.callAsFunction() as JSUint8Array?; + final JSUint8Array? rawBytes = + getRawBytes.callAsFunction() as JSUint8Array?; return rawBytes?.toDart; } diff --git a/lib/src/web/zxing/zxing_barcode_reader.dart b/lib/src/web/zxing/zxing_barcode_reader.dart index 9f92c92a6..845814de6 100644 --- a/lib/src/web/zxing/zxing_barcode_reader.dart +++ b/lib/src/web/zxing/zxing_barcode_reader.dart @@ -22,7 +22,8 @@ final class ZXingBarcodeReader extends BarcodeReader { void Function(web.MediaTrackSettings)? _onMediaTrackSettingsChanged; /// The internal media stream track constraints delegate. - final MediaTrackConstraintsDelegate _mediaTrackConstraintsDelegate = const MediaTrackConstraintsDelegate(); + final MediaTrackConstraintsDelegate _mediaTrackConstraintsDelegate = + const MediaTrackConstraintsDelegate(); /// The internal barcode reader. ZXingBrowserMultiFormatReader? _reader; @@ -93,7 +94,8 @@ final class ZXingBarcodeReader extends BarcodeReader { return null; } - final capabilities = web.window.navigator.mediaDevices.getSupportedConstraints(); + final capabilities = + web.window.navigator.mediaDevices.getSupportedConstraints(); final web.MediaStreamConstraints constraints; @@ -112,7 +114,9 @@ final class ZXingBarcodeReader extends BarcodeReader { ); } - final JSAny? mediaStream = await web.window.navigator.mediaDevices.getUserMedia(constraints).toDart; + final JSAny? mediaStream = await web.window.navigator.mediaDevices + .getUserMedia(constraints) + .toDart; return mediaStream as web.MediaStream?; } @@ -135,11 +139,13 @@ final class ZXingBarcodeReader extends BarcodeReader { final web.MediaStream? stream = await _prepareMediaStream(cameraDirection); if (stream != null) { - final JSPromise? result = _reader?.attachStreamToVideo.callAsFunction(null, stream, videoElement) as JSPromise?; + final JSPromise? result = _reader?.attachStreamToVideo + .callAsFunction(null, stream, videoElement) as JSPromise?; await result?.toDart; - final web.MediaTrackSettings? settings = _mediaTrackConstraintsDelegate.getSettings(stream); + final web.MediaTrackSettings? settings = + _mediaTrackConstraintsDelegate.getSettings(stream); if (settings != null) { _onMediaTrackSettingsChanged?.call(settings); @@ -193,7 +199,9 @@ final class ZXingBarcodeReader extends BarcodeReader { } @override - void setMediaTrackSettingsListener(void Function(web.MediaTrackSettings) listener) { + void setMediaTrackSettingsListener( + void Function(web.MediaTrackSettings) listener, + ) { _onMediaTrackSettingsChanged ??= listener; } @@ -210,9 +218,13 @@ final class ZXingBarcodeReader extends BarcodeReader { return Future.value(); } - await _mediaTrackConstraintsDelegate.setFlashlightState(mediaStream, value); + await _mediaTrackConstraintsDelegate.setFlashlightState( + mediaStream, + value, + ); - final web.MediaTrackSettings? settings = _mediaTrackConstraintsDelegate.getSettings(mediaStream); + final web.MediaTrackSettings? settings = + _mediaTrackConstraintsDelegate.getSettings(mediaStream); if (settings != null) { _onMediaTrackSettingsChanged?.call(settings); @@ -221,7 +233,10 @@ final class ZXingBarcodeReader extends BarcodeReader { } @override - Future start(StartOptions options, {required web.HTMLElement containerElement}) async { + Future start( + StartOptions options, { + required web.HTMLElement containerElement, + }) async { final int detectionTimeoutMs = options.detectionTimeoutMs; final List formats = options.formats; @@ -243,7 +258,8 @@ final class ZXingBarcodeReader extends BarcodeReader { _reader = ZXingBrowserMultiFormatReader(hints.jsify(), detectionTimeoutMs); - final web.HTMLVideoElement videoElement = web.document.createElement('video') as web.HTMLVideoElement; + final web.HTMLVideoElement videoElement = + web.document.createElement('video') as web.HTMLVideoElement; await _prepareVideoElement( videoElement, From bb6c05a99392fe5e58406403503d4a69a3095546 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 2 Jan 2024 11:15:49 +0100 Subject: [PATCH 118/161] fix comment --- lib/src/mobile_scanner_controller.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index eda0424f4..63fa70b88 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -205,8 +205,6 @@ class MobileScannerController extends ValueNotifier { /// /// The [cameraDirection] can be used to specify the camera direction. /// If this is null, this defaults to the [facing] value. - /// - /// Throws a [MobileScannerException] if starting the scanner failed. Future start({CameraFacing? cameraDirection}) async { if (_isDisposed) { throw const MobileScannerException( From 169728a0fb4d76d079cef4b91bd88895e79e3c0e Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 2 Jan 2024 14:12:28 +0100 Subject: [PATCH 119/161] ensure no stale values are present when initialization fails --- lib/src/mobile_scanner_controller.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 63fa70b88..0f7410752 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -248,11 +248,17 @@ class MobileScannerController extends ValueNotifier { ); } on MobileScannerException catch (error) { // The initialization finished with an error. + // To avoid stale values, reset the output size, + // torch state and zoom scale to the defaults. if (!_isDisposed) { value = value.copyWith( cameraDirection: facing, isInitialized: true, + isRunning: false, error: error, + size: Size.zero, + torchState: TorchState.unavailable, + zoomScale: 1.0, ); } } From f2f16e9a8ce32f50aff061ba81105a51e3fc77c9 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 2 Jan 2024 14:18:55 +0100 Subject: [PATCH 120/161] do nothing in start/stop if already started/stopped --- lib/src/mobile_scanner_controller.dart | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 0f7410752..2041c1f08 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -205,6 +205,8 @@ class MobileScannerController extends ValueNotifier { /// /// The [cameraDirection] can be used to specify the camera direction. /// If this is null, this defaults to the [facing] value. + /// + /// Does nothing if the camera is already running. Future start({CameraFacing? cameraDirection}) async { if (_isDisposed) { throw const MobileScannerException( @@ -216,6 +218,11 @@ class MobileScannerController extends ValueNotifier { ); } + // Do nothing if the camera is already running. + if (value.isRunning) { + return; + } + final CameraFacing effectiveDirection = cameraDirection ?? facing; final StartOptions options = StartOptions( @@ -267,11 +274,18 @@ class MobileScannerController extends ValueNotifier { /// Stop the camera. /// /// After calling this method, the camera can be restarted using [start]. + /// + /// Does nothing if the camera is already stopped. Future stop() async { - _disposeListeners(); - _throwIfNotInitialized(); + // Do nothing if already stopped. + if (!value.isRunning) { + return; + } + + _disposeListeners(); + // After the camera stopped, set the torch state to off, // as the torch state callback is never called when the camera is stopped. value = value.copyWith( From f47d16e67e62f792474f11e1bf3a2e3c3a300042 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 2 Jan 2024 14:24:55 +0100 Subject: [PATCH 121/161] don't update the zoom scale or torch state if the camer is not running --- lib/src/mobile_scanner_controller.dart | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 2041c1f08..c0d3b9b39 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -175,9 +175,15 @@ class MobileScannerController extends ValueNotifier { } /// Reset the zoom scale of the camera. + /// + /// Does nothing if the camera is not running. Future resetZoomScale() async { _throwIfNotInitialized(); + if (!value.isRunning) { + return; + } + // When the platform has updated the zoom scale, // it will send an update through the zoom scale state event stream. await MobileScannerPlatform.instance.resetZoomScale(); @@ -189,9 +195,15 @@ class MobileScannerController extends ValueNotifier { /// /// If the [zoomScale] is out of range, /// it is adjusted to fit within the allowed range. + /// + /// Does nothing if the camera is not running. Future setZoomScale(double zoomScale) async { _throwIfNotInitialized(); + if (!value.isRunning) { + return; + } + final double clampedZoomScale = zoomScale.clamp(0.0, 1.0); // Update the zoom scale state to the new state. @@ -313,10 +325,15 @@ class MobileScannerController extends ValueNotifier { /// Switches the flashlight on or off. /// - /// Does nothing if the device has no torch. + /// Does nothing if the device has no torch, + /// or if the camera is not running. Future toggleTorch() async { _throwIfNotInitialized(); + if (!value.isRunning) { + return; + } + final TorchState torchState = value.torchState; if (torchState == TorchState.unavailable) { From 6c46a25f449277a3bbb3e6c6f71cac327afb05a2 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 3 Jan 2024 17:11:05 +0100 Subject: [PATCH 122/161] fix compilation error on web; don't depend on package:js anymore for dart2wasm --- lib/src/web/barcode_reader.dart | 2 +- lib/src/web/zxing/result.dart | 52 ++++++++------------- lib/src/web/zxing/result_point.dart | 19 +++----- lib/src/web/zxing/zxing_barcode_reader.dart | 8 ++-- pubspec.yaml | 3 +- 5 files changed, 32 insertions(+), 52 deletions(-) diff --git a/lib/src/web/barcode_reader.dart b/lib/src/web/barcode_reader.dart index 9505592ed..02db28ad8 100644 --- a/lib/src/web/barcode_reader.dart +++ b/lib/src/web/barcode_reader.dart @@ -1,8 +1,8 @@ import 'dart:async'; +import 'dart:js'; import 'dart:js_interop'; import 'dart:ui'; -import 'package:js/js.dart'; import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; diff --git a/lib/src/web/zxing/result.dart b/lib/src/web/zxing/result.dart index fd164f042..67a60ee98 100644 --- a/lib/src/web/zxing/result.dart +++ b/lib/src/web/zxing/result.dart @@ -11,32 +11,31 @@ import 'package:mobile_scanner/src/web/zxing/result_point.dart'; /// /// See also: https://github.com/zxing-js/library/blob/master/src/core/Result.ts @JS() +@anonymous @staticInterop abstract class Result {} extension ResultExt on Result { - /// Get the barcode format. - /// - /// See also https://github.com/zxing-js/library/blob/master/src/core/BarcodeFormat.ts - external JSFunction getBarcodeFormat; + @JS('barcodeFormat') + external JSNumber? get _barcodeFormat; - /// Get the raw bytes of the result. - external JSFunction getRawBytes; + @JS('text') + external JSString? get _text; - /// Get the points of the result. - external JSFunction getResultPoints; + @JS('rawBytes') + external JSUint8Array? get _rawBytes; - /// Get the text of the result. - external JSFunction getText; + @JS('resultPoints') + external JSArray? get _resultPoints; - /// Get the timestamp of the result. - external JSFunction getTimestamp; + @JS('timestamp') + external JSNumber? get _timestamp; /// Get the barcode format of the result. + /// + /// See also https://github.com/zxing-js/library/blob/master/src/core/BarcodeFormat.ts BarcodeFormat get barcodeFormat { - final JSNumber? format = getBarcodeFormat.callAsFunction() as JSNumber?; - - switch (format?.toDartInt) { + switch (_barcodeFormat?.toDartInt) { case 0: return BarcodeFormat.aztec; case 1: @@ -82,36 +81,25 @@ extension ResultExt on Result { /// Get the corner points of the result. List get resultPoints { - final JSArray? resultPoints = getResultPoints.callAsFunction() as JSArray?; + final JSArray? points = _resultPoints; - if (resultPoints == null) { + if (points == null) { return []; } - return resultPoints.toDart.cast().map((point) { + return points.toDart.cast().map((point) { return Offset(point.x, point.y); }).toList(); } /// Get the raw bytes of the result. - Uint8List? get rawBytes { - final JSUint8Array? rawBytes = - getRawBytes.callAsFunction() as JSUint8Array?; - - return rawBytes?.toDart; - } + Uint8List? get rawBytes => _rawBytes?.toDart; /// Get the text of the result. - String? get text { - return getText.callAsFunction() as String?; - } + String? get text => _text?.toDart; /// Get the timestamp of the result. - int? get timestamp { - final JSNumber? timestamp = getTimestamp.callAsFunction() as JSNumber?; - - return timestamp?.toDartInt; - } + int? get timestamp => _timestamp?.toDartInt; /// Convert this result to a [Barcode]. Barcode get toBarcode { diff --git a/lib/src/web/zxing/result_point.dart b/lib/src/web/zxing/result_point.dart index 3c5237965..fa143e5b0 100644 --- a/lib/src/web/zxing/result_point.dart +++ b/lib/src/web/zxing/result_point.dart @@ -4,25 +4,20 @@ import 'dart:js_interop'; /// /// See also: https://github.com/zxing-js/library/blob/master/src/core/ResultPoint.ts @JS() +@anonymous @staticInterop abstract class ResultPoint {} extension ResultPointExt on ResultPoint { - external JSFunction getX; + @JS('x') + external JSNumber get _x; - external JSFunction getY; + @JS('y') + external JSNumber get _y; /// The x coordinate of the point. - double get x { - final JSNumber? x = getX.callAsFunction() as JSNumber?; - - return x?.toDartDouble ?? 0; - } + double get x => _x.toDartDouble; /// The y coordinate of the point. - double get y { - final JSNumber? y = getY.callAsFunction() as JSNumber?; - - return y?.toDartDouble ?? 0; - } + double get y => _y.toDartDouble; } diff --git a/lib/src/web/zxing/zxing_barcode_reader.dart b/lib/src/web/zxing/zxing_barcode_reader.dart index 845814de6..28f5c6b97 100644 --- a/lib/src/web/zxing/zxing_barcode_reader.dart +++ b/lib/src/web/zxing/zxing_barcode_reader.dart @@ -1,8 +1,8 @@ import 'dart:async'; +import 'dart:js'; import 'dart:js_interop'; import 'dart:ui'; -import 'package:js/js.dart'; import 'package:mobile_scanner/src/enums/barcode_format.dart'; import 'package:mobile_scanner/src/enums/camera_facing.dart'; import 'package:mobile_scanner/src/enums/torch_state.dart'; @@ -161,13 +161,11 @@ final class ZXingBarcodeReader extends BarcodeReader { _reader?.decodeContinuously.callAsFunction( null, _reader?.videoElement, - allowInterop((result, error) { + allowInterop((Result? result, JSAny? error) { if (!controller.isClosed && result != null) { - final barcode = (result as Result).toBarcode; - controller.add( BarcodeCapture( - barcodes: [barcode], + barcodes: [result.toBarcode], ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index 12cbe94cc..a025c99e9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,6 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - js: ^0.6.3 plugin_platform_interface: ^2.0.2 web: ^0.3.0 @@ -45,4 +44,4 @@ flutter: pluginClass: MobileScannerPlugin web: pluginClass: MobileScannerWeb - fileName: web/mobile_scanner_web.dart + fileName: src/web/mobile_scanner_web.dart From a08dc6d8958e86d6f7c261fa715f6b3a84eda8c2 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 3 Jan 2024 17:13:27 +0100 Subject: [PATCH 123/161] fix small align issue in example app --- example/lib/main.dart | 152 +++++++++++++++++++++--------------------- 1 file changed, 77 insertions(+), 75 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 272a063f5..082e3395e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -23,82 +23,84 @@ class MyHome extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Mobile Scanner Example')), - body: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerListView(), - ), - ); - }, - child: const Text('MobileScanner with ListView'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerWithController(), - ), - ); - }, - child: const Text('MobileScanner with Controller'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerWithScanWindow(), - ), - ); - }, - child: const Text('MobileScanner with ScanWindow'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerReturningImage(), - ), - ); - }, - child: const Text( - 'MobileScanner with Controller (returning image)', + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const BarcodeScannerListView(), + ), + ); + }, + child: const Text('MobileScanner with ListView'), ), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerWithZoom(), - ), - ); - }, - child: const Text('MobileScanner with zoom slider'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerPageView(), - ), - ); - }, - child: const Text('MobileScanner pageView'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => BarcodeScannerWithOverlay(), - ), - ); - }, - child: const Text('MobileScanner with Overlay'), - ), - ], + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const BarcodeScannerWithController(), + ), + ); + }, + child: const Text('MobileScanner with Controller'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const BarcodeScannerWithScanWindow(), + ), + ); + }, + child: const Text('MobileScanner with ScanWindow'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const BarcodeScannerReturningImage(), + ), + ); + }, + child: const Text( + 'MobileScanner with Controller (returning image)', + ), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const BarcodeScannerWithZoom(), + ), + ); + }, + child: const Text('MobileScanner with zoom slider'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const BarcodeScannerPageView(), + ), + ); + }, + child: const Text('MobileScanner pageView'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => BarcodeScannerWithOverlay(), + ), + ); + }, + child: const Text('MobileScanner with Overlay'), + ), + ], + ), ), ); } From c9f02febc3b675c6718c70100460731edf1e730b Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 3 Jan 2024 21:28:54 +0100 Subject: [PATCH 124/161] fix type --- lib/src/web/mobile_scanner_web.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index 49ecb9df3..f87b38bd5 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -93,7 +93,7 @@ class MobileScannerWeb extends MobileScannerPlatform { ui_web.platformViewRegistry.registerViewFactory( _viewType, - (int id) => _divElement, + (int id) => _divElement!, ); } From 6ec6f531fed3a42e4b33335cd5f0d30d97f77e4e Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 3 Jan 2024 21:35:47 +0100 Subject: [PATCH 125/161] fix async dispose() implementations --- example/lib/barcode_scanner_controller.dart | 2 +- example/lib/barcode_scanner_listview.dart | 2 +- example/lib/barcode_scanner_pageview.dart | 16 ++++++++-------- example/lib/barcode_scanner_returning_image.dart | 2 +- example/lib/barcode_scanner_window.dart | 2 +- example/lib/barcode_scanner_zoom.dart | 2 +- example/lib/mobile_scanner_overlay.dart | 2 +- .../mobile_scanner_method_channel.dart | 4 ++-- lib/src/mobile_scanner_controller.dart | 6 +++--- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/example/lib/barcode_scanner_controller.dart b/example/lib/barcode_scanner_controller.dart index afc74e84a..66ee3583b 100644 --- a/example/lib/barcode_scanner_controller.dart +++ b/example/lib/barcode_scanner_controller.dart @@ -74,7 +74,7 @@ class _BarcodeScannerWithControllerState @override Future dispose() async { - await controller.dispose(); super.dispose(); + await controller.dispose(); } } diff --git a/example/lib/barcode_scanner_listview.dart b/example/lib/barcode_scanner_listview.dart index 5c65607a7..ca0de42ba 100644 --- a/example/lib/barcode_scanner_listview.dart +++ b/example/lib/barcode_scanner_listview.dart @@ -107,7 +107,7 @@ class _BarcodeScannerListViewState extends State { @override Future dispose() async { - await controller.dispose(); super.dispose(); + await controller.dispose(); } } diff --git a/example/lib/barcode_scanner_pageview.dart b/example/lib/barcode_scanner_pageview.dart index 88be31227..c81865b15 100644 --- a/example/lib/barcode_scanner_pageview.dart +++ b/example/lib/barcode_scanner_pageview.dart @@ -13,14 +13,14 @@ class BarcodeScannerPageView extends StatefulWidget { } class _BarcodeScannerPageViewState extends State { - final MobileScannerController scannerController = MobileScannerController(); + final MobileScannerController controller = MobileScannerController(); final PageController pageController = PageController(); @override void initState() { super.initState(); - scannerController.start(); + unawaited(controller.start()); } @override @@ -33,7 +33,7 @@ class _BarcodeScannerPageViewState extends State { onPageChanged: (index) async { // Stop the camera view for the current page, // and then restart the camera for the new page. - await scannerController.stop(); + await controller.stop(); // When switching pages, add a delay to the next start call. // Otherwise the camera will start before the next page is displayed. @@ -43,13 +43,13 @@ class _BarcodeScannerPageViewState extends State { return; } - scannerController.start(); + unawaited(controller.start()); }, children: [ - _BarcodeScannerPage(controller: scannerController), + _BarcodeScannerPage(controller: controller), const SizedBox(), - _BarcodeScannerPage(controller: scannerController), - _BarcodeScannerPage(controller: scannerController), + _BarcodeScannerPage(controller: controller), + _BarcodeScannerPage(controller: controller), ], ), ); @@ -57,9 +57,9 @@ class _BarcodeScannerPageViewState extends State { @override Future dispose() async { - await scannerController.dispose(); pageController.dispose(); super.dispose(); + await controller.dispose(); } } diff --git a/example/lib/barcode_scanner_returning_image.dart b/example/lib/barcode_scanner_returning_image.dart index 13a00b58a..5f2943c0e 100644 --- a/example/lib/barcode_scanner_returning_image.dart +++ b/example/lib/barcode_scanner_returning_image.dart @@ -139,7 +139,7 @@ class _BarcodeScannerReturningImageState @override Future dispose() async { - await controller.dispose(); super.dispose(); + await controller.dispose(); } } diff --git a/example/lib/barcode_scanner_window.dart b/example/lib/barcode_scanner_window.dart index 4dc409e2c..71f0f5318 100644 --- a/example/lib/barcode_scanner_window.dart +++ b/example/lib/barcode_scanner_window.dart @@ -128,8 +128,8 @@ class _BarcodeScannerWithScanWindowState @override Future dispose() async { - await controller.dispose(); super.dispose(); + await controller.dispose(); } } diff --git a/example/lib/barcode_scanner_zoom.dart b/example/lib/barcode_scanner_zoom.dart index 6ac0980bc..40a0fdfee 100644 --- a/example/lib/barcode_scanner_zoom.dart +++ b/example/lib/barcode_scanner_zoom.dart @@ -122,7 +122,7 @@ class _BarcodeScannerWithZoomState extends State { @override Future dispose() async { - await controller.dispose(); super.dispose(); + await controller.dispose(); } } diff --git a/example/lib/mobile_scanner_overlay.dart b/example/lib/mobile_scanner_overlay.dart index d2be5cca3..771198672 100644 --- a/example/lib/mobile_scanner_overlay.dart +++ b/example/lib/mobile_scanner_overlay.dart @@ -87,8 +87,8 @@ class _BarcodeScannerWithOverlayState extends State { @override Future dispose() async { - await controller.dispose(); super.dispose(); + await controller.dispose(); } } diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index f71e996f9..544a62ef3 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -293,9 +293,9 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { return; } - await methodChannel.invokeMethod('stop'); - _textureId = null; + + await methodChannel.invokeMethod('stop'); } @override diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index c0d3b9b39..019b54d1b 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -369,10 +369,10 @@ class MobileScannerController extends ValueNotifier { return; } - await MobileScannerPlatform.instance.dispose(); - unawaited(_barcodesController.close()); - _isDisposed = true; + unawaited(_barcodesController.close()); super.dispose(); + + await MobileScannerPlatform.instance.dispose(); } } From 6e39b502511a0596bb3684f24f2d6fe99112ed9d Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 4 Jan 2024 08:56:51 +0100 Subject: [PATCH 126/161] use a sized box instead of an error --- lib/src/method_channel/mobile_scanner_method_channel.dart | 8 +------- lib/src/web/mobile_scanner_web.dart | 8 +------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index 544a62ef3..cd0174024 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -179,13 +179,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @override Widget buildCameraView() { if (_textureId == null) { - throw const MobileScannerException( - errorCode: MobileScannerErrorCode.controllerUninitialized, - errorDetails: MobileScannerErrorDetails( - message: - 'The controller was not yet initialized. Call start() before calling buildCameraView().', - ), - ); + return const SizedBox(); } return Texture(textureId: _textureId!); diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index f87b38bd5..fe100699c 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -69,13 +69,7 @@ class MobileScannerWeb extends MobileScannerPlatform { @override Widget buildCameraView() { if (!_barcodeReader.isScanning) { - throw const MobileScannerException( - errorCode: MobileScannerErrorCode.controllerUninitialized, - errorDetails: MobileScannerErrorDetails( - message: - 'The controller was not yet initialized. Call start() before calling buildCameraView().', - ), - ); + return const SizedBox(); } return HtmlElementView(viewType: _viewType); From d697e2d5f4f3e8effb670f51243506d532f70210 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Sat, 6 Jan 2024 19:40:20 +0100 Subject: [PATCH 127/161] fix doc --- lib/src/web/zxing/zxing_browser_multi_format_reader.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/web/zxing/zxing_browser_multi_format_reader.dart b/lib/src/web/zxing/zxing_browser_multi_format_reader.dart index a86cdf271..b70b61d7a 100644 --- a/lib/src/web/zxing/zxing_browser_multi_format_reader.dart +++ b/lib/src/web/zxing/zxing_browser_multi_format_reader.dart @@ -8,7 +8,7 @@ import 'package:web/web.dart'; @JS('ZXing.BrowserMultiFormatReader') @staticInterop class ZXingBrowserMultiFormatReader { - /// Construct a new ZXingBrowserMultiFormatReader. + /// Construct a new `ZXing.BrowserMultiFormatReader`. /// /// The [hints] are the configuration options for the reader. /// The [timeBetweenScansMillis] is the allowed time between scans in milliseconds. From 7e11556b4c89f6d5027015f6fb5f1d5a95d55dc9 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Sat, 6 Jan 2024 19:40:44 +0100 Subject: [PATCH 128/161] add JS interop stub for the JS Map class --- lib/src/web/javascript_map.dart | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 lib/src/web/javascript_map.dart diff --git a/lib/src/web/javascript_map.dart b/lib/src/web/javascript_map.dart new file mode 100644 index 000000000..f4a6f692e --- /dev/null +++ b/lib/src/web/javascript_map.dart @@ -0,0 +1,20 @@ +import 'dart:js_interop'; + +/// A static interop stub for the `Map` class. +/// +/// This stub is here because the `js_types` from the Dart SDK do not yet provide a `Map` equivalent: +/// https://github.com/dart-lang/sdk/issues/54365 +/// +/// See also: https://github.com/dart-lang/sdk/issues/54365#issuecomment-1856995463 +/// +/// Object literals can be made using [jsify]. +@JS('Map') +@staticInterop +class JsMap implements JSAny { + external factory JsMap(); +} + +extension JsMapExtension on JsMap { + external V? get(K key); + external JSVoid set(K key, V? value); +} From 0bcca09bca64831c0c93298c3490ad9539f27554 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Sat, 6 Jan 2024 19:45:41 +0100 Subject: [PATCH 129/161] adjust web barcode scanner constructor to only accept JS types; move hints creation to separate method --- lib/src/web/zxing/zxing_barcode_reader.dart | 38 ++++++++++++------- .../zxing_browser_multi_format_reader.dart | 5 ++- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/lib/src/web/zxing/zxing_barcode_reader.dart b/lib/src/web/zxing/zxing_barcode_reader.dart index 28f5c6b97..02998f594 100644 --- a/lib/src/web/zxing/zxing_barcode_reader.dart +++ b/lib/src/web/zxing/zxing_barcode_reader.dart @@ -9,6 +9,7 @@ import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/objects/barcode_capture.dart'; import 'package:mobile_scanner/src/objects/start_options.dart'; import 'package:mobile_scanner/src/web/barcode_reader.dart'; +import 'package:mobile_scanner/src/web/javascript_map.dart'; import 'package:mobile_scanner/src/web/media_track_constraints_delegate.dart'; import 'package:mobile_scanner/src/web/zxing/result.dart'; import 'package:mobile_scanner/src/web/zxing/zxing_browser_multi_format_reader.dart'; @@ -84,6 +85,26 @@ final class ZXingBarcodeReader extends BarcodeReader { } } + JsMap? _createReaderHints(List formats) { + if (formats.isEmpty || formats.contains(BarcodeFormat.all)) { + return null; + } + + final JsMap hints = JsMap(); + + // Set the formats hint. + // See https://github.com/zxing-js/library/blob/master/src/core/DecodeHintType.ts#L45 + hints.set( + 2.toJS, + [ + for (final BarcodeFormat format in formats) + getZXingBarcodeFormat(format).toJS, + ].toJS, + ); + + return hints; + } + /// Prepare the [web.MediaStream] for the barcode reader video input. /// /// This method requests permission to use the camera. @@ -242,19 +263,10 @@ final class ZXingBarcodeReader extends BarcodeReader { formats.removeWhere((element) => element == BarcodeFormat.unknown); } - final Map? hints; - - if (formats.isNotEmpty && !formats.contains(BarcodeFormat.all)) { - // Set the formats hint. - // See https://github.com/zxing-js/library/blob/master/src/core/DecodeHintType.ts#L45 - hints = { - 2: formats.map(getZXingBarcodeFormat).toList(), - }; - } else { - hints = null; - } - - _reader = ZXingBrowserMultiFormatReader(hints.jsify(), detectionTimeoutMs); + _reader = ZXingBrowserMultiFormatReader( + _createReaderHints(formats), + detectionTimeoutMs.toJS, + ); final web.HTMLVideoElement videoElement = web.document.createElement('video') as web.HTMLVideoElement; diff --git a/lib/src/web/zxing/zxing_browser_multi_format_reader.dart b/lib/src/web/zxing/zxing_browser_multi_format_reader.dart index b70b61d7a..0e2699065 100644 --- a/lib/src/web/zxing/zxing_browser_multi_format_reader.dart +++ b/lib/src/web/zxing/zxing_browser_multi_format_reader.dart @@ -1,5 +1,6 @@ import 'dart:js_interop'; +import 'package:mobile_scanner/src/web/javascript_map.dart'; import 'package:web/web.dart'; /// The JS interop class for the ZXing BrowserMultiFormatReader. @@ -15,8 +16,8 @@ class ZXingBrowserMultiFormatReader { /// /// See also: https://github.com/zxing-js/library/blob/master/src/core/DecodeHintType.ts external factory ZXingBrowserMultiFormatReader( - JSAny? hints, - int? timeBetweenScansMillis, + JsMap? hints, + JSNumber? timeBetweenScansMillis, ); } From 035590ac8e318b84f8db0140e3acc40a8ee8cfcf Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Sat, 6 Jan 2024 19:49:01 +0100 Subject: [PATCH 130/161] guard against disposed controller during state updates --- lib/src/mobile_scanner_controller.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 019b54d1b..7bd24575a 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -119,18 +119,26 @@ class MobileScannerController extends ValueNotifier { void _setupListeners() { _barcodesSubscription = MobileScannerPlatform.instance.barcodesStream .listen((BarcodeCapture? barcode) { - if (barcode != null) { + if (!_barcodesController.isClosed && barcode != null) { _barcodesController.add(barcode); } }); _torchStateSubscription = MobileScannerPlatform.instance.torchStateStream .listen((TorchState torchState) { + if (_isDisposed) { + return; + } + value = value.copyWith(torchState: torchState); }); _zoomScaleSubscription = MobileScannerPlatform.instance.zoomScaleStateStream .listen((double zoomScale) { + if (_isDisposed) { + return; + } + value = value.copyWith(zoomScale: zoomScale); }); } From 1822f2cf8c8630e463ca9f895d2cee8debb3d748 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Mon, 8 Jan 2024 11:36:04 +0100 Subject: [PATCH 131/161] ensure the MobileScanner widget calls `super.dispose()` first --- lib/src/mobile_scanner.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/mobile_scanner.dart b/lib/src/mobile_scanner.dart index c571fb52c..c3d59d498 100644 --- a/lib/src/mobile_scanner.dart +++ b/lib/src/mobile_scanner.dart @@ -186,8 +186,8 @@ class _MobileScannerState extends State { @override void dispose() { - // When this widget is unmounted, reset the scan window. - widget.controller.updateScanWindow(null); super.dispose(); + // When this widget is unmounted, reset the scan window. + unawaited(widget.controller.updateScanWindow(null)); } } From 3cdeeb38ee5fc235c9613783dc48b3a28f98dc21 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Mon, 8 Jan 2024 12:16:05 +0100 Subject: [PATCH 132/161] add basic listening and lifecycle handling to first example --- example/lib/barcode_scanner_controller.dart | 65 ++++++++++++++++++--- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/example/lib/barcode_scanner_controller.dart b/example/lib/barcode_scanner_controller.dart index 66ee3583b..6eff999d4 100644 --- a/example/lib/barcode_scanner_controller.dart +++ b/example/lib/barcode_scanner_controller.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; -import 'package:mobile_scanner_example/scanned_barcode_label.dart'; import 'package:mobile_scanner_example/scanner_button_widgets.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; @@ -15,7 +14,7 @@ class BarcodeScannerWithController extends StatefulWidget { } class _BarcodeScannerWithControllerState - extends State { + extends State with WidgetsBindingObserver { final MobileScannerController controller = MobileScannerController( torchEnabled: true, useNewCameraSelector: true, // formats: [BarcodeFormat.qrCode] @@ -25,10 +24,61 @@ class _BarcodeScannerWithControllerState // returnImage: false, ); + Barcode? _barcode; + StreamSubscription? _subscription; + + Widget _buildBarcode(Barcode? value) { + if (value == null) { + return const Text( + 'Scan something!', + overflow: TextOverflow.fade, + style: TextStyle(color: Colors.white), + ); + } + + return Text( + value.displayValue ?? 'No display value.', + overflow: TextOverflow.fade, + style: const TextStyle(color: Colors.white), + ); + } + + void _handleBarcode(BarcodeCapture barcodes) { + if (mounted) { + setState(() { + _barcode = barcodes.barcodes.firstOrNull; + }); + } + } + @override void initState() { super.initState(); - controller.start(); + WidgetsBinding.instance.addObserver(this); + + _subscription = controller.barcodes.listen(_handleBarcode); + + unawaited(controller.start()); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + + switch (state) { + case AppLifecycleState.detached: + case AppLifecycleState.hidden: + case AppLifecycleState.paused: + return; + case AppLifecycleState.resumed: + _subscription ??= controller.barcodes.listen(_handleBarcode); + + unawaited(controller.start()); + case AppLifecycleState.inactive: + unawaited(_subscription?.cancel()); + _subscription = null; + unawaited(controller.stop()); + } } @override @@ -56,11 +106,7 @@ class _BarcodeScannerWithControllerState children: [ ToggleFlashlightButton(controller: controller), StartStopMobileScannerButton(controller: controller), - Expanded( - child: Center( - child: ScannedBarcodeLabel(barcodes: controller.barcodes), - ), - ), + Expanded(child: Center(child: _buildBarcode(_barcode))), SwitchCameraButton(controller: controller), AnalyzeImageFromGalleryButton(controller: controller), ], @@ -74,6 +120,9 @@ class _BarcodeScannerWithControllerState @override Future dispose() async { + WidgetsBinding.instance.removeObserver(this); + unawaited(_subscription?.cancel()); + _subscription = null; super.dispose(); await controller.dispose(); } From 6f421d5f1cadc27f6373d97caa3c3cd61de22d47 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Mon, 8 Jan 2024 12:17:31 +0100 Subject: [PATCH 133/161] fix sizeOf usage --- example/lib/barcode_scanner_controller.dart | 2 +- example/lib/barcode_scanner_window.dart | 2 +- example/lib/mobile_scanner_overlay.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/lib/barcode_scanner_controller.dart b/example/lib/barcode_scanner_controller.dart index 6eff999d4..126297bd0 100644 --- a/example/lib/barcode_scanner_controller.dart +++ b/example/lib/barcode_scanner_controller.dart @@ -71,7 +71,7 @@ class _BarcodeScannerWithControllerState case AppLifecycleState.paused: return; case AppLifecycleState.resumed: - _subscription ??= controller.barcodes.listen(_handleBarcode); + _subscription = controller.barcodes.listen(_handleBarcode); unawaited(controller.start()); case AppLifecycleState.inactive: diff --git a/example/lib/barcode_scanner_window.dart b/example/lib/barcode_scanner_window.dart index 71f0f5318..6cca48d63 100644 --- a/example/lib/barcode_scanner_window.dart +++ b/example/lib/barcode_scanner_window.dart @@ -90,7 +90,7 @@ class _BarcodeScannerWithScanWindowState @override Widget build(BuildContext context) { final scanWindow = Rect.fromCenter( - center: MediaQuery.of(context).size.center(Offset.zero), + center: MediaQuery.sizeOf(context).center(Offset.zero), width: 200, height: 200, ); diff --git a/example/lib/mobile_scanner_overlay.dart b/example/lib/mobile_scanner_overlay.dart index 771198672..f021e4772 100644 --- a/example/lib/mobile_scanner_overlay.dart +++ b/example/lib/mobile_scanner_overlay.dart @@ -23,7 +23,7 @@ class _BarcodeScannerWithOverlayState extends State { @override Widget build(BuildContext context) { final scanWindow = Rect.fromCenter( - center: MediaQuery.of(context).size.center(Offset.zero), + center: MediaQuery.sizeOf(context).center(Offset.zero), width: 200, height: 200, ); From 73fc318cf1c9394d189d048739cdc837e9d43b5e Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Mon, 8 Jan 2024 12:47:11 +0100 Subject: [PATCH 134/161] update the readme --- README.md | 250 +++++++++++++++++------------------------------------- 1 file changed, 78 insertions(+), 172 deletions(-) diff --git a/README.md b/README.md index bfb33dc22..d4f4c8a14 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,15 @@ A universal scanner for Flutter based on MLKit. Uses CameraX on Android and AVFoundation on iOS. - ## Features Supported See the example app for detailed implementation information. -| Features | Android | iOS | macOS | Web | -|------------------------|--------------------|--------------------|-------|-----| -| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | -| returnImage | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | -| scanWindow | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | -| barcodeOverlay | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | +| Features | Android | iOS | macOS | Web | +|------------------------|--------------------|--------------------|----------------------|-----| +| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | +| returnImage | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | +| scanWindow | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | ## Platform Support @@ -26,6 +24,7 @@ See the example app for detailed implementation information. | ✔ | ✔ | ✔ | ✔ | :x: | :x: | ## Platform specific setup + ### Android This package uses by default the **bundled version** of MLKit Barcode-scanning for Android. This version is immediately available to the device. But it will increase the size of the app by approximately 3 to 10 MB. @@ -61,194 +60,101 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities: Screenshot of XCode where Camera is checked ## Web -This package uses ZXing on web to read barcodes so it needs to be included in `index.html` as script. + +Include the `ZXing` library in the `` of your `index.html` as a script. + ```html - + + + + + ``` ## Usage -Import `package:mobile_scanner/mobile_scanner.dart`, and use the widget with or without the controller. +Import the package with `package:mobile_scanner/mobile_scanner.dart`. -If you don't provide a controller, you can't control functions like the torch(flash) or switching camera. - -If you don't set `detectionSpeed` to `DetectionSpeed.noDuplicates`, you can get multiple scans in a very short time, causing things like pop() to fire lots of times. - -Example without controller: +Create a new `MobileScannerController` controller, using the required options. +Provide a `StreamSubscription` for the barcode events. ```dart -import 'package:mobile_scanner/mobile_scanner.dart'; +final MobileScannerController controller = MobileScannerController( + // required options for the scanner +); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Mobile Scanner')), - body: MobileScanner( - // fit: BoxFit.contain, - onDetect: (capture) { - final List barcodes = capture.barcodes; - final Uint8List? image = capture.image; - for (final barcode in barcodes) { - debugPrint('Barcode found! ${barcode.rawValue}'); - } - }, - ), - ); - } +StreamSubscription? _subscription; ``` -Example with controller and initial values: +Ensure that your `State` class mixes in `WidgetsBindingObserver`, to handle lifecyle changes: ```dart -import 'package:mobile_scanner/mobile_scanner.dart'; +class MyState extends State with WidgetsBindingObserver { + // ... @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Mobile Scanner')), - body: MobileScanner( - // fit: BoxFit.contain, - controller: MobileScannerController( - detectionSpeed: DetectionSpeed.normal, - facing: CameraFacing.front, - torchEnabled: true, - ), - onDetect: (capture) { - final List barcodes = capture.barcodes; - final Uint8List? image = capture.image; - for (final barcode in barcodes) { - debugPrint('Barcode found! ${barcode.rawValue}'); - } - }, - ), - ); + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + + switch (state) { + case AppLifecycleState.detached: + case AppLifecycleState.hidden: + case AppLifecycleState.paused: + return; + case AppLifecycleState.resumed: + // Restart the scanner when the app is resumed. + // Don't forget to resume listening to the barcode events. + _subscription = controller.barcodes.listen(_handleBarcode); + + unawaited(controller.start()); + case AppLifecycleState.inactive: + // Stop the scanner when the app is paused. + // Also stop the barcode events subscription. + unawaited(_subscription?.cancel()); + _subscription = null; + unawaited(controller.stop()); + } } + + // ... +} ``` -Example with controller and torch & camera controls: +Then, start the scanner in `void initState()`: ```dart -import 'package:mobile_scanner/mobile_scanner.dart'; - - MobileScannerController cameraController = MobileScannerController(); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Mobile Scanner'), - actions: [ - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: cameraController.torchState, - builder: (context, state, child) { - switch (state as TorchState) { - case TorchState.off: - return const Icon(Icons.flash_off, color: Colors.grey); - case TorchState.on: - return const Icon(Icons.flash_on, color: Colors.yellow); - } - }, - ), - iconSize: 32.0, - onPressed: () => cameraController.toggleTorch(), - ), - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: cameraController.cameraFacingState, - builder: (context, state, child) { - switch (state as CameraFacing) { - case CameraFacing.front: - return const Icon(Icons.camera_front); - case CameraFacing.back: - return const Icon(Icons.camera_rear); - } - }, - ), - iconSize: 32.0, - onPressed: () => cameraController.switchCamera(), - ), - ], - ), - body: MobileScanner( - // fit: BoxFit.contain, - controller: cameraController, - onDetect: (capture) { - final List barcodes = capture.barcodes; - final Uint8List? image = capture.image; - for (final barcode in barcodes) { - debugPrint('Barcode found! ${barcode.rawValue}'); - } - }, - ), - ); - } +@override +void initState() { + super.initState(); + // Start listening to lifecycle changes. + WidgetsBinding.instance.addObserver(this); + + // Start listening to the barcode events. + _subscription = controller.barcodes.listen(_handleBarcode); + + // Finally, start the scanner itself. + unawaited(controller.start()); +} ``` -Example with controller and returning images +Finally, dispose of the the `MobileScannerController` when you are done with it. ```dart -import 'package:mobile_scanner/mobile_scanner.dart'; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Mobile Scanner')), - body: MobileScanner( - fit: BoxFit.contain, - controller: MobileScannerController( - // facing: CameraFacing.back, - // torchEnabled: false, - returnImage: true, - ), - onDetect: (capture) { - final List barcodes = capture.barcodes; - final Uint8List? image = capture.image; - for (final barcode in barcodes) { - debugPrint('Barcode found! ${barcode.rawValue}'); - } - if (image != null) { - showDialog( - context: context, - builder: (context) => - Image(image: MemoryImage(image)), - ); - Future.delayed(const Duration(seconds: 5), () { - Navigator.pop(context); - }); - } - }, - ), - ); - } +@override +Future dispose() async { + // Stop listening to lifecycle changes. + WidgetsBinding.instance.removeObserver(this); + // Stop listening to the barcode events. + unawaited(_subscription?.cancel()); + _subscription = null; + // Dispose the widget itself. + super.dispose(); + // Finally, dispose of the controller. + await controller.dispose(); +} ``` -### BarcodeCapture - -The onDetect function returns a BarcodeCapture objects which contains the following items. - -| Property name | Type | Description | -|---------------|---------------|-----------------------------------| -| barcodes | List | A list with scanned barcodes. | -| image | Uint8List? | If enabled, an image of the scan. | - -You can use the following properties of the Barcode object. - -| Property name | Type | Description | -|---------------|----------------|-------------------------------------| -| format | BarcodeFormat | | -| rawBytes | Uint8List? | binary scan result | -| rawValue | String? | Value if barcode is in UTF-8 format | -| displayValue | String? | | -| type | BarcodeType | | -| calendarEvent | CalendarEvent? | | -| contactInfo | ContactInfo? | | -| driverLicense | DriverLicense? | | -| email | Email? | | -| geoPoint | GeoPoint? | | -| phone | Phone? | | -| sms | SMS? | | -| url | UrlBookmark? | | -| wifi | WiFi? | WiFi Access-Point details | +To display the camera preview, pass the controller to a `MobileScanner` widget. + +See the examples for runnable examples of various usages, +such as the basic usage, applying a scan window, or retrieving images from the barcodes. From db57d85b430c6b8a8ceacfd9f31a52f296bf2843 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Mon, 8 Jan 2024 12:47:43 +0100 Subject: [PATCH 135/161] move ZXing to the HEAD tag --- example/web/index.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/example/web/index.html b/example/web/index.html index bc900fb63..8d6b56db0 100644 --- a/example/web/index.html +++ b/example/web/index.html @@ -38,9 +38,11 @@ + + + - - +If a different mirror is needed to load the barcode scanning library, +the source URL can be set beforehand. + +```dart +import 'package:flutter/foundation.dart'; + +final String scriptUrl = // ... + +if (kIsWeb) { + MobileScannerPlatform.instance.setBarcodeLibraryScriptUrl(scriptUrl); +} ``` ## Usage diff --git a/example/web/index.html b/example/web/index.html index 8d6b56db0..5beb6ff8e 100644 --- a/example/web/index.html +++ b/example/web/index.html @@ -38,9 +38,6 @@ - - -