Julian Steenbakker
Committed by GitHub

Merge branch 'master' into feature/scan-window

@@ -77,7 +77,13 @@ class _BarcodeScannerWithControllerState @@ -77,7 +77,13 @@ class _BarcodeScannerWithControllerState
77 child: Row( 77 child: Row(
78 mainAxisAlignment: MainAxisAlignment.spaceEvenly, 78 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
79 children: [ 79 children: [
80 - IconButton( 80 + ValueListenableBuilder(
  81 + valueListenable: controller.hasTorchState,
  82 + builder: (context, state, child) {
  83 + if (state != true) {
  84 + return const SizedBox.shrink();
  85 + }
  86 + return IconButton(
81 color: Colors.white, 87 color: Colors.white,
82 icon: ValueListenableBuilder( 88 icon: ValueListenableBuilder(
83 valueListenable: controller.torchState, 89 valueListenable: controller.torchState,
@@ -104,6 +110,8 @@ class _BarcodeScannerWithControllerState @@ -104,6 +110,8 @@ class _BarcodeScannerWithControllerState
104 ), 110 ),
105 iconSize: 32.0, 111 iconSize: 32.0,
106 onPressed: () => controller.toggleTorch(), 112 onPressed: () => controller.toggleTorch(),
  113 + );
  114 + },
107 ), 115 ),
108 IconButton( 116 IconButton(
109 color: Colors.white, 117 color: Colors.white,
@@ -82,11 +82,12 @@ class MobileScannerWebPlugin { @@ -82,11 +82,12 @@ class MobileScannerWebPlugin {
82 82
83 // Check if stream is running 83 // Check if stream is running
84 if (barCodeReader.isStarted) { 84 if (barCodeReader.isStarted) {
  85 + final hasTorch = await barCodeReader.hasTorch();
85 return { 86 return {
86 'ViewID': viewID, 87 'ViewID': viewID,
87 'videoWidth': barCodeReader.videoWidth, 88 'videoWidth': barCodeReader.videoWidth,
88 'videoHeight': barCodeReader.videoHeight, 89 'videoHeight': barCodeReader.videoHeight,
89 - 'torchable': barCodeReader.hasTorch, 90 + 'torchable': hasTorch,
90 }; 91 };
91 } 92 }
92 try { 93 try {
@@ -106,12 +107,17 @@ class MobileScannerWebPlugin { @@ -106,12 +107,17 @@ class MobileScannerWebPlugin {
106 }); 107 });
107 } 108 }
108 }); 109 });
  110 + final hasTorch = await barCodeReader.hasTorch();
  111 +
  112 + if (hasTorch && arguments.containsKey('torch')) {
  113 + barCodeReader.toggleTorch(enabled: arguments['torch'] as bool);
  114 + }
109 115
110 return { 116 return {
111 'ViewID': viewID, 117 'ViewID': viewID,
112 'videoWidth': barCodeReader.videoWidth, 118 'videoWidth': barCodeReader.videoWidth,
113 'videoHeight': barCodeReader.videoHeight, 119 'videoHeight': barCodeReader.videoHeight,
114 - 'torchable': barCodeReader.hasTorch, 120 + 'torchable': hasTorch,
115 }; 121 };
116 } catch (e) { 122 } catch (e) {
117 throw PlatformException(code: 'MobileScannerWeb', message: '$e'); 123 throw PlatformException(code: 'MobileScannerWeb', message: '$e');
@@ -99,19 +99,21 @@ class MobileScannerController { @@ -99,19 +99,21 @@ class MobileScannerController {
99 99
100 bool isStarting = false; 100 bool isStarting = false;
101 101
102 - bool? _hasTorch; 102 + /// A notifier that provides availability of the Torch (Flash)
  103 + final ValueNotifier<bool?> hasTorchState = ValueNotifier(false);
103 104
104 /// Returns whether the device has a torch. 105 /// Returns whether the device has a torch.
105 /// 106 ///
106 /// Throws an error if the controller is not initialized. 107 /// Throws an error if the controller is not initialized.
107 bool get hasTorch { 108 bool get hasTorch {
108 - if (_hasTorch == null) { 109 + final hasTorch = hasTorchState.value;
  110 + if (hasTorch == null) {
109 throw const MobileScannerException( 111 throw const MobileScannerException(
110 errorCode: MobileScannerErrorCode.controllerUninitialized, 112 errorCode: MobileScannerErrorCode.controllerUninitialized,
111 ); 113 );
112 } 114 }
113 115
114 - return _hasTorch!; 116 + return hasTorch;
115 } 117 }
116 118
117 /// Set the starting arguments for the camera 119 /// Set the starting arguments for the camera
@@ -219,8 +221,9 @@ class MobileScannerController { @@ -219,8 +221,9 @@ class MobileScannerController {
219 ); 221 );
220 } 222 }
221 223
222 - _hasTorch = startResult['torchable'] as bool? ?? false;  
223 - if (_hasTorch! && torchEnabled) { 224 + final hasTorch = startResult['torchable'] as bool? ?? false;
  225 + hasTorchState.value = hasTorch;
  226 + if (hasTorch && torchEnabled) {
224 torchState.value = TorchState.on; 227 torchState.value = TorchState.on;
225 } 228 }
226 229
@@ -232,7 +235,7 @@ class MobileScannerController { @@ -232,7 +235,7 @@ class MobileScannerController {
232 startResult['videoHeight'] as double? ?? 0, 235 startResult['videoHeight'] as double? ?? 0,
233 ) 236 )
234 : toSize(startResult['size'] as Map? ?? {}), 237 : toSize(startResult['size'] as Map? ?? {}),
235 - hasTorch: _hasTorch!, 238 + hasTorch: hasTorch,
236 textureId: kIsWeb ? null : startResult['textureId'] as int?, 239 textureId: kIsWeb ? null : startResult['textureId'] as int?,
237 webId: kIsWeb ? startResult['ViewID'] as String? : null, 240 webId: kIsWeb ? startResult['ViewID'] as String? : null,
238 ); 241 );
@@ -253,7 +256,7 @@ class MobileScannerController { @@ -253,7 +256,7 @@ class MobileScannerController {
253 /// 256 ///
254 /// Throws if the controller was not initialized. 257 /// Throws if the controller was not initialized.
255 Future<void> toggleTorch() async { 258 Future<void> toggleTorch() async {
256 - final hasTorch = _hasTorch; 259 + final hasTorch = hasTorchState.value;
257 260
258 if (hasTorch == null) { 261 if (hasTorch == null) {
259 throw const MobileScannerException( 262 throw const MobileScannerException(
1 -import 'dart:html'; 1 +import 'dart:html' as html;
2 2
3 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
  4 +import 'package:js/js.dart';
  5 +import 'package:js/js_util.dart';
4 import 'package:mobile_scanner/src/enums/camera_facing.dart'; 6 import 'package:mobile_scanner/src/enums/camera_facing.dart';
5 import 'package:mobile_scanner/src/objects/barcode.dart'; 7 import 'package:mobile_scanner/src/objects/barcode.dart';
6 import 'package:mobile_scanner/src/web/media.dart'; 8 import 'package:mobile_scanner/src/web/media.dart';
@@ -8,7 +10,7 @@ import 'package:mobile_scanner/src/web/media.dart'; @@ -8,7 +10,7 @@ import 'package:mobile_scanner/src/web/media.dart';
8 abstract class WebBarcodeReaderBase { 10 abstract class WebBarcodeReaderBase {
9 /// Timer used to capture frames to be analyzed 11 /// Timer used to capture frames to be analyzed
10 final Duration frameInterval; 12 final Duration frameInterval;
11 - final DivElement videoContainer; 13 + final html.DivElement videoContainer;
12 14
13 const WebBarcodeReaderBase({ 15 const WebBarcodeReaderBase({
14 required this.videoContainer, 16 required this.videoContainer,
@@ -35,24 +37,24 @@ abstract class WebBarcodeReaderBase { @@ -35,24 +37,24 @@ abstract class WebBarcodeReaderBase {
35 Future<void> toggleTorch({required bool enabled}); 37 Future<void> toggleTorch({required bool enabled});
36 38
37 /// Determine whether device has flash 39 /// Determine whether device has flash
38 - bool get hasTorch; 40 + Future<bool> hasTorch();
39 } 41 }
40 42
41 mixin InternalStreamCreation on WebBarcodeReaderBase { 43 mixin InternalStreamCreation on WebBarcodeReaderBase {
42 /// The video stream. 44 /// The video stream.
43 /// Will be initialized later to see which camera needs to be used. 45 /// Will be initialized later to see which camera needs to be used.
44 - MediaStream? localMediaStream;  
45 - final VideoElement video = VideoElement(); 46 + html.MediaStream? localMediaStream;
  47 + final html.VideoElement video = html.VideoElement();
46 48
47 @override 49 @override
48 int get videoWidth => video.videoWidth; 50 int get videoWidth => video.videoWidth;
49 @override 51 @override
50 int get videoHeight => video.videoHeight; 52 int get videoHeight => video.videoHeight;
51 53
52 - Future<MediaStream?> initMediaStream(CameraFacing cameraFacing) async { 54 + Future<html.MediaStream?> initMediaStream(CameraFacing cameraFacing) async {
53 // Check if browser supports multiple camera's and set if supported 55 // Check if browser supports multiple camera's and set if supported
54 final Map? capabilities = 56 final Map? capabilities =
55 - window.navigator.mediaDevices?.getSupportedConstraints(); 57 + html.window.navigator.mediaDevices?.getSupportedConstraints();
56 final Map<String, dynamic> constraints; 58 final Map<String, dynamic> constraints;
57 if (capabilities != null && capabilities['facingMode'] as bool) { 59 if (capabilities != null && capabilities['facingMode'] as bool) {
58 constraints = { 60 constraints = {
@@ -65,15 +67,15 @@ mixin InternalStreamCreation on WebBarcodeReaderBase { @@ -65,15 +67,15 @@ mixin InternalStreamCreation on WebBarcodeReaderBase {
65 constraints = {'video': true}; 67 constraints = {'video': true};
66 } 68 }
67 final stream = 69 final stream =
68 - await window.navigator.mediaDevices?.getUserMedia(constraints); 70 + await html.window.navigator.mediaDevices?.getUserMedia(constraints);
69 return stream; 71 return stream;
70 } 72 }
71 73
72 - void prepareVideoElement(VideoElement videoSource); 74 + void prepareVideoElement(html.VideoElement videoSource);
73 75
74 Future<void> attachStreamToVideo( 76 Future<void> attachStreamToVideo(
75 - MediaStream stream,  
76 - VideoElement videoSource, 77 + html.MediaStream stream,
  78 + html.VideoElement videoSource,
77 ); 79 );
78 80
79 @override 81 @override
@@ -96,19 +98,34 @@ mixin InternalStreamCreation on WebBarcodeReaderBase { @@ -96,19 +98,34 @@ mixin InternalStreamCreation on WebBarcodeReaderBase {
96 98
97 /// Mixin for libraries that don't have built-in torch support 99 /// Mixin for libraries that don't have built-in torch support
98 mixin InternalTorchDetection on InternalStreamCreation { 100 mixin InternalTorchDetection on InternalStreamCreation {
  101 + Future<List<String>> getSupportedTorchStates() async {
  102 + try {
  103 + final track = localMediaStream?.getVideoTracks();
  104 + if (track != null) {
  105 + final imageCapture = ImageCapture(track.first);
  106 + final photoCapabilities = await promiseToFuture<PhotoCapabilities>(
  107 + imageCapture.getPhotoCapabilities(),
  108 + );
  109 + final fillLightMode = photoCapabilities.fillLightMode;
  110 + if (fillLightMode != null) {
  111 + return fillLightMode;
  112 + }
  113 + }
  114 + } catch (e) {
  115 + // ImageCapture is not supported by some browsers:
  116 + // https://developer.mozilla.org/en-US/docs/Web/API/ImageCapture#browser_compatibility
  117 + }
  118 + return [];
  119 + }
  120 +
99 @override 121 @override
100 - bool get hasTorch {  
101 - // TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533  
102 - // final track = _localStream?.getVideoTracks();  
103 - // if (track != null) {  
104 - // final imageCapture = html.ImageCapture(track.first);  
105 - // final photoCapabilities = await imageCapture.getPhotoCapabilities();  
106 - // }  
107 - return false; 122 + Future<bool> hasTorch() async {
  123 + return (await getSupportedTorchStates()).isNotEmpty;
108 } 124 }
109 125
110 @override 126 @override
111 Future<void> toggleTorch({required bool enabled}) async { 127 Future<void> toggleTorch({required bool enabled}) async {
  128 + final hasTorch = await this.hasTorch();
112 if (hasTorch) { 129 if (hasTorch) {
113 final track = localMediaStream?.getVideoTracks(); 130 final track = localMediaStream?.getVideoTracks();
114 await track?.first.applyConstraints({ 131 await track?.first.applyConstraints({
@@ -119,3 +136,25 @@ mixin InternalTorchDetection on InternalStreamCreation { @@ -119,3 +136,25 @@ mixin InternalTorchDetection on InternalStreamCreation {
119 } 136 }
120 } 137 }
121 } 138 }
  139 +
  140 +@JS('Promise')
  141 +@staticInterop
  142 +class Promise<T> {}
  143 +
  144 +@JS()
  145 +@anonymous
  146 +class PhotoCapabilities {
  147 + /// Returns an array of available fill light options. Options include auto, off, or flash.
  148 + external List<String>? get fillLightMode;
  149 +}
  150 +
  151 +@JS('ImageCapture')
  152 +@staticInterop
  153 +class ImageCapture {
  154 + /// MediaStreamTrack
  155 + external factory ImageCapture(dynamic track);
  156 +}
  157 +
  158 +extension ImageCaptureExt on ImageCapture {
  159 + external Promise<PhotoCapabilities> getPhotoCapabilities();
  160 +}
@@ -7,10 +7,6 @@ import 'package:mobile_scanner/src/enums/camera_facing.dart'; @@ -7,10 +7,6 @@ import 'package:mobile_scanner/src/enums/camera_facing.dart';
7 import 'package:mobile_scanner/src/objects/barcode.dart'; 7 import 'package:mobile_scanner/src/objects/barcode.dart';
8 import 'package:mobile_scanner/src/web/base.dart'; 8 import 'package:mobile_scanner/src/web/base.dart';
9 9
10 -@JS('Promise')  
11 -@staticInterop  
12 -class Promise<T> {}  
13 -  
14 @JS('ZXing.BrowserMultiFormatReader') 10 @JS('ZXing.BrowserMultiFormatReader')
15 @staticInterop 11 @staticInterop
16 class JsZXingBrowserMultiFormatReader { 12 class JsZXingBrowserMultiFormatReader {