Committed by
GitHub
Merge branch 'master' into feature/scan-window
Showing
5 changed files
with
111 additions
and
59 deletions
| @@ -77,33 +77,41 @@ class _BarcodeScannerWithControllerState | @@ -77,33 +77,41 @@ class _BarcodeScannerWithControllerState | ||
| 77 | child: Row( | 77 | child: Row( |
| 78 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, | 78 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
| 79 | children: [ | 79 | children: [ |
| 80 | - IconButton( | ||
| 81 | - color: Colors.white, | ||
| 82 | - icon: ValueListenableBuilder( | ||
| 83 | - valueListenable: controller.torchState, | ||
| 84 | - builder: (context, state, child) { | ||
| 85 | - if (state == null) { | ||
| 86 | - return const Icon( | ||
| 87 | - Icons.flash_off, | ||
| 88 | - color: Colors.grey, | ||
| 89 | - ); | ||
| 90 | - } | ||
| 91 | - switch (state as TorchState) { | ||
| 92 | - case TorchState.off: | ||
| 93 | - return const Icon( | ||
| 94 | - Icons.flash_off, | ||
| 95 | - color: Colors.grey, | ||
| 96 | - ); | ||
| 97 | - case TorchState.on: | ||
| 98 | - return const Icon( | ||
| 99 | - Icons.flash_on, | ||
| 100 | - color: Colors.yellow, | ||
| 101 | - ); | ||
| 102 | - } | ||
| 103 | - }, | ||
| 104 | - ), | ||
| 105 | - iconSize: 32.0, | ||
| 106 | - onPressed: () => controller.toggleTorch(), | 80 | + ValueListenableBuilder( |
| 81 | + valueListenable: controller.hasTorchState, | ||
| 82 | + builder: (context, state, child) { | ||
| 83 | + if (state != true) { | ||
| 84 | + return const SizedBox.shrink(); | ||
| 85 | + } | ||
| 86 | + return IconButton( | ||
| 87 | + color: Colors.white, | ||
| 88 | + icon: ValueListenableBuilder( | ||
| 89 | + valueListenable: controller.torchState, | ||
| 90 | + builder: (context, state, child) { | ||
| 91 | + if (state == null) { | ||
| 92 | + return const Icon( | ||
| 93 | + Icons.flash_off, | ||
| 94 | + color: Colors.grey, | ||
| 95 | + ); | ||
| 96 | + } | ||
| 97 | + switch (state as TorchState) { | ||
| 98 | + case TorchState.off: | ||
| 99 | + return const Icon( | ||
| 100 | + Icons.flash_off, | ||
| 101 | + color: Colors.grey, | ||
| 102 | + ); | ||
| 103 | + case TorchState.on: | ||
| 104 | + return const Icon( | ||
| 105 | + Icons.flash_on, | ||
| 106 | + color: Colors.yellow, | ||
| 107 | + ); | ||
| 108 | + } | ||
| 109 | + }, | ||
| 110 | + ), | ||
| 111 | + iconSize: 32.0, | ||
| 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 { |
-
Please register or login to post a comment