p-mazhnik

Merge branch 'master' into pavel/web-format

# Conflicts:
#	lib/src/web/base.dart
@@ -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,
@@ -84,11 +84,12 @@ class MobileScannerWebPlugin { @@ -84,11 +84,12 @@ class MobileScannerWebPlugin {
84 84
85 // Check if stream is running 85 // Check if stream is running
86 if (barCodeReader.isStarted) { 86 if (barCodeReader.isStarted) {
  87 + final hasTorch = await barCodeReader.hasTorch();
87 return { 88 return {
88 'ViewID': viewID, 89 'ViewID': viewID,
89 'videoWidth': barCodeReader.videoWidth, 90 'videoWidth': barCodeReader.videoWidth,
90 'videoHeight': barCodeReader.videoHeight, 91 'videoHeight': barCodeReader.videoHeight,
91 - 'torchable': barCodeReader.hasTorch, 92 + 'torchable': hasTorch,
92 }; 93 };
93 } 94 }
94 try { 95 try {
@@ -124,12 +125,17 @@ class MobileScannerWebPlugin { @@ -124,12 +125,17 @@ class MobileScannerWebPlugin {
124 }); 125 });
125 } 126 }
126 }); 127 });
  128 + final hasTorch = await barCodeReader.hasTorch();
  129 +
  130 + if (hasTorch && arguments.containsKey('torch')) {
  131 + barCodeReader.toggleTorch(enabled: arguments['torch'] as bool);
  132 + }
127 133
128 return { 134 return {
129 'ViewID': viewID, 135 'ViewID': viewID,
130 'videoWidth': barCodeReader.videoWidth, 136 'videoWidth': barCodeReader.videoWidth,
131 'videoHeight': barCodeReader.videoHeight, 137 'videoHeight': barCodeReader.videoHeight,
132 - 'torchable': barCodeReader.hasTorch, 138 + 'torchable': hasTorch,
133 }; 139 };
134 } catch (e) { 140 } catch (e) {
135 throw PlatformException(code: 'MobileScannerWeb', message: '$e'); 141 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
@@ -210,8 +212,9 @@ class MobileScannerController { @@ -210,8 +212,9 @@ class MobileScannerController {
210 ); 212 );
211 } 213 }
212 214
213 - _hasTorch = startResult['torchable'] as bool? ?? false;  
214 - if (_hasTorch! && torchEnabled) { 215 + final hasTorch = startResult['torchable'] as bool? ?? false;
  216 + hasTorchState.value = hasTorch;
  217 + if (hasTorch && torchEnabled) {
215 torchState.value = TorchState.on; 218 torchState.value = TorchState.on;
216 } 219 }
217 220
@@ -223,7 +226,7 @@ class MobileScannerController { @@ -223,7 +226,7 @@ class MobileScannerController {
223 startResult['videoHeight'] as double? ?? 0, 226 startResult['videoHeight'] as double? ?? 0,
224 ) 227 )
225 : toSize(startResult['size'] as Map? ?? {}), 228 : toSize(startResult['size'] as Map? ?? {}),
226 - hasTorch: _hasTorch!, 229 + hasTorch: hasTorch,
227 textureId: kIsWeb ? null : startResult['textureId'] as int?, 230 textureId: kIsWeb ? null : startResult['textureId'] as int?,
228 webId: kIsWeb ? startResult['ViewID'] as String? : null, 231 webId: kIsWeb ? startResult['ViewID'] as String? : null,
229 ); 232 );
@@ -244,7 +247,7 @@ class MobileScannerController { @@ -244,7 +247,7 @@ class MobileScannerController {
244 /// 247 ///
245 /// Throws if the controller was not initialized. 248 /// Throws if the controller was not initialized.
246 Future<void> toggleTorch() async { 249 Future<void> toggleTorch() async {
247 - final hasTorch = _hasTorch; 250 + final hasTorch = hasTorchState.value;
248 251
249 if (hasTorch == null) { 252 if (hasTorch == null) {
250 throw const MobileScannerException( 253 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'; 4 import 'package:js/js.dart';
  5 +import 'package:js/js_util.dart';
5 import 'package:mobile_scanner/src/enums/camera_facing.dart'; 6 import 'package:mobile_scanner/src/enums/camera_facing.dart';
6 import 'package:mobile_scanner/src/objects/barcode.dart'; 7 import 'package:mobile_scanner/src/objects/barcode.dart';
7 import 'package:mobile_scanner/src/web/media.dart'; 8 import 'package:mobile_scanner/src/web/media.dart';
@@ -9,7 +10,7 @@ import 'package:mobile_scanner/src/web/media.dart'; @@ -9,7 +10,7 @@ import 'package:mobile_scanner/src/web/media.dart';
9 abstract class WebBarcodeReaderBase { 10 abstract class WebBarcodeReaderBase {
10 /// Timer used to capture frames to be analyzed 11 /// Timer used to capture frames to be analyzed
11 Duration frameInterval = const Duration(milliseconds: 200); 12 Duration frameInterval = const Duration(milliseconds: 200);
12 - final DivElement videoContainer; 13 + final html.DivElement videoContainer;
13 14
14 WebBarcodeReaderBase({ 15 WebBarcodeReaderBase({
15 required this.videoContainer, 16 required this.videoContainer,
@@ -37,24 +38,24 @@ abstract class WebBarcodeReaderBase { @@ -37,24 +38,24 @@ abstract class WebBarcodeReaderBase {
37 Future<void> toggleTorch({required bool enabled}); 38 Future<void> toggleTorch({required bool enabled});
38 39
39 /// Determine whether device has flash 40 /// Determine whether device has flash
40 - bool get hasTorch; 41 + Future<bool> hasTorch();
41 } 42 }
42 43
43 mixin InternalStreamCreation on WebBarcodeReaderBase { 44 mixin InternalStreamCreation on WebBarcodeReaderBase {
44 /// The video stream. 45 /// The video stream.
45 /// Will be initialized later to see which camera needs to be used. 46 /// Will be initialized later to see which camera needs to be used.
46 - MediaStream? localMediaStream;  
47 - final VideoElement video = VideoElement(); 47 + html.MediaStream? localMediaStream;
  48 + final html.VideoElement video = html.VideoElement();
48 49
49 @override 50 @override
50 int get videoWidth => video.videoWidth; 51 int get videoWidth => video.videoWidth;
51 @override 52 @override
52 int get videoHeight => video.videoHeight; 53 int get videoHeight => video.videoHeight;
53 54
54 - Future<MediaStream?> initMediaStream(CameraFacing cameraFacing) async { 55 + Future<html.MediaStream?> initMediaStream(CameraFacing cameraFacing) async {
55 // Check if browser supports multiple camera's and set if supported 56 // Check if browser supports multiple camera's and set if supported
56 final Map? capabilities = 57 final Map? capabilities =
57 - window.navigator.mediaDevices?.getSupportedConstraints(); 58 + html.window.navigator.mediaDevices?.getSupportedConstraints();
58 final Map<String, dynamic> constraints; 59 final Map<String, dynamic> constraints;
59 if (capabilities != null && capabilities['facingMode'] as bool) { 60 if (capabilities != null && capabilities['facingMode'] as bool) {
60 constraints = { 61 constraints = {
@@ -67,15 +68,15 @@ mixin InternalStreamCreation on WebBarcodeReaderBase { @@ -67,15 +68,15 @@ mixin InternalStreamCreation on WebBarcodeReaderBase {
67 constraints = {'video': true}; 68 constraints = {'video': true};
68 } 69 }
69 final stream = 70 final stream =
70 - await window.navigator.mediaDevices?.getUserMedia(constraints); 71 + await html.window.navigator.mediaDevices?.getUserMedia(constraints);
71 return stream; 72 return stream;
72 } 73 }
73 74
74 - void prepareVideoElement(VideoElement videoSource); 75 + void prepareVideoElement(html.VideoElement videoSource);
75 76
76 Future<void> attachStreamToVideo( 77 Future<void> attachStreamToVideo(
77 - MediaStream stream,  
78 - VideoElement videoSource, 78 + html.MediaStream stream,
  79 + html.VideoElement videoSource,
79 ); 80 );
80 81
81 @override 82 @override
@@ -98,19 +99,34 @@ mixin InternalStreamCreation on WebBarcodeReaderBase { @@ -98,19 +99,34 @@ mixin InternalStreamCreation on WebBarcodeReaderBase {
98 99
99 /// Mixin for libraries that don't have built-in torch support 100 /// Mixin for libraries that don't have built-in torch support
100 mixin InternalTorchDetection on InternalStreamCreation { 101 mixin InternalTorchDetection on InternalStreamCreation {
  102 + Future<List<String>> getSupportedTorchStates() async {
  103 + try {
  104 + final track = localMediaStream?.getVideoTracks();
  105 + if (track != null) {
  106 + final imageCapture = ImageCapture(track.first);
  107 + final photoCapabilities = await promiseToFuture<PhotoCapabilities>(
  108 + imageCapture.getPhotoCapabilities(),
  109 + );
  110 + final fillLightMode = photoCapabilities.fillLightMode;
  111 + if (fillLightMode != null) {
  112 + return fillLightMode;
  113 + }
  114 + }
  115 + } catch (e) {
  116 + // ImageCapture is not supported by some browsers:
  117 + // https://developer.mozilla.org/en-US/docs/Web/API/ImageCapture#browser_compatibility
  118 + }
  119 + return [];
  120 + }
  121 +
101 @override 122 @override
102 - bool get hasTorch {  
103 - // TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533  
104 - // final track = _localStream?.getVideoTracks();  
105 - // if (track != null) {  
106 - // final imageCapture = html.ImageCapture(track.first);  
107 - // final photoCapabilities = await imageCapture.getPhotoCapabilities();  
108 - // }  
109 - return false; 123 + Future<bool> hasTorch() async {
  124 + return (await getSupportedTorchStates()).isNotEmpty;
110 } 125 }
111 126
112 @override 127 @override
113 Future<void> toggleTorch({required bool enabled}) async { 128 Future<void> toggleTorch({required bool enabled}) async {
  129 + final hasTorch = await this.hasTorch();
114 if (hasTorch) { 130 if (hasTorch) {
115 final track = localMediaStream?.getVideoTracks(); 131 final track = localMediaStream?.getVideoTracks();
116 await track?.first.applyConstraints({ 132 await track?.first.applyConstraints({
@@ -122,6 +138,28 @@ mixin InternalTorchDetection on InternalStreamCreation { @@ -122,6 +138,28 @@ mixin InternalTorchDetection on InternalStreamCreation {
122 } 138 }
123 } 139 }
124 140
  141 +@JS('Promise')
  142 +@staticInterop
  143 +class Promise<T> {}
  144 +
  145 +@JS()
  146 +@anonymous
  147 +class PhotoCapabilities {
  148 + /// Returns an array of available fill light options. Options include auto, off, or flash.
  149 + external List<String>? get fillLightMode;
  150 +}
  151 +
  152 +@JS('ImageCapture')
  153 +@staticInterop
  154 +class ImageCapture {
  155 + /// MediaStreamTrack
  156 + external factory ImageCapture(dynamic track);
  157 +}
  158 +
  159 +extension ImageCaptureExt on ImageCapture {
  160 + external Promise<PhotoCapabilities> getPhotoCapabilities();
  161 +}
  162 +
125 @JS('Map') 163 @JS('Map')
126 @staticInterop 164 @staticInterop
127 class JsMap { 165 class JsMap {
@@ -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 {