Navaron Bracke
Committed by GitHub

Merge pull request #1005 from navaronbracke/5_0_0-beta-3

feat: 5.0.0-beta 3
## 5.0.0-beta.2
## 5.0.0-beta.3
**BREAKING CHANGES:**
* Flutter 3.16.0 is now required.
* Flutter 3.19.0 is now required.
* [iOS] iOS 12.0 is now the minimum supported iOS version.
* [iOS] Adds a Privacy Manifest.
Bugs fixed:
* Fixed an issue where the camera preview and barcode scanner did not work the second time on web.
Improvements:
* [web] Migrates to extension types. (thanks @koji-1009 !)
## 5.0.0-beta.2
Bugs fixed:
* Fixed an issue where the scan window was not updated when its size was changed. (thanks @navaronbracke !)
... ...
... ... @@ -62,7 +62,3 @@ android {
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
... ...
buildscript {
ext.kotlin_version = '1.9.22'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
... ...
... ... @@ -5,12 +5,21 @@ pluginManagement {
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
include ":app"
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.2.2" apply false
id "org.jetbrains.kotlin.android" version "1.9.22" apply false
}
apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle"
include ":app"
\ No newline at end of file
... ...
... ... @@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>11.0</string>
<string>12.0</string>
</dict>
</plist>
... ...
# Uncomment this line to define a global platform for your project
# platform :ios, '11.0'
# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
... ... @@ -41,7 +41,7 @@ post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
end
end
end
... ...
... ... @@ -452,7 +452,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
... ... @@ -583,7 +583,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
... ... @@ -632,7 +632,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
... ...
... ... @@ -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.2.0 <4.0.0"
flutter: ">=3.16.0"
sdk: ">=3.3.0 <4.0.0"
flutter: ">=3.19.0"
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
... ...
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>
... ...
... ... @@ -4,21 +4,22 @@
#
Pod::Spec.new do |s|
s.name = 'mobile_scanner'
s.version = '3.5.6'
s.version = '5.0.0'
s.summary = 'An universal scanner for Flutter based on MLKit.'
s.description = <<-DESC
An universal scanner for Flutter based on MLKit.
DESC
s.homepage = 'http://example.com'
s.homepage = 'https://github.com/juliansteenbakker/mobile_scanner'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.author = { 'Julian Steenbakker' => 'juliansteenbakker@outlook.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.dependency 'GoogleMLKit/BarcodeScanning', '~> 4.0.0'
s.platform = :ios, '11.0'
s.dependency 'GoogleMLKit/BarcodeScanning', '~> 5.0.0'
s.platform = :ios, '12.0'
s.static_framework = true
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
s.resource_bundles = { 'mobile_scanner_privacy' => ['Resources/PrivacyInfo.xcprivacy'] }
end
... ...
... ... @@ -94,12 +94,29 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
///
/// Throws a [MobileScannerException] if the permission is not granted.
Future<void> _requestCameraPermission() async {
final MobileScannerAuthorizationState authorizationState;
try {
authorizationState = MobileScannerAuthorizationState.fromRawValue(
final MobileScannerAuthorizationState authorizationState =
MobileScannerAuthorizationState.fromRawValue(
await methodChannel.invokeMethod<int>('state') ?? 0,
);
switch (authorizationState) {
// Authorization was already granted, no need to request it again.
case MobileScannerAuthorizationState.authorized:
return;
// Android does not have an undetermined authorization state.
// So if the permission was denied, request it again.
case MobileScannerAuthorizationState.denied:
case MobileScannerAuthorizationState.undetermined:
final bool permissionGranted =
await methodChannel.invokeMethod<bool>('request') ?? false;
if (!permissionGranted) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.permissionDenied,
);
}
}
} on PlatformException catch (error) {
// If the permission state is invalid, that is an error.
throw MobileScannerException(
... ... @@ -111,37 +128,6 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
),
);
}
switch (authorizationState) {
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 granted =
await methodChannel.invokeMethod<bool>('request') ?? false;
if (granted) {
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
... ...
... ... @@ -223,12 +223,14 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
}
/// 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.
///
/// Does nothing if the camera is already running.
/// Upon calling this method, the necessary camera permission will be requested.
///
/// If the permission is denied on iOS, MacOS or Web, there is no way to request it again.
Future<void> start({CameraFacing? cameraDirection}) async {
if (_isDisposed) {
throw const MobileScannerException(
... ... @@ -240,6 +242,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
);
}
// Permission was denied, do nothing.
// When the controller is stopped,
// the error is reset so the permission can be requested again if possible.
if (value.error?.errorCode == MobileScannerErrorCode.permissionDenied) {
return;
}
// Do nothing if the camera is already running.
if (value.isRunning) {
return;
... ...
... ... @@ -62,20 +62,19 @@ abstract class BarcodeReader {
final Completer<void> completer = Completer();
final HTMLScriptElement script =
(document.createElement('script') as HTMLScriptElement)
..id = scriptId
..async = true
..defer = false
..type = 'application/javascript'
..lang = 'javascript'
..crossOrigin = 'anonymous'
..src = alternateScriptUrl ?? scriptUrl
..onload = (JSAny _) {
if (!completer.isCompleted) {
completer.complete();
}
}.toJS;
final HTMLScriptElement script = HTMLScriptElement()
..id = scriptId
..async = true
..defer = false
..type = 'application/javascript'
..lang = 'javascript'
..crossOrigin = 'anonymous'
..src = alternateScriptUrl ?? scriptUrl
..onload = (JSAny _) {
if (!completer.isCompleted) {
completer.complete();
}
}.toJS;
script.onerror = (JSAny _) {
if (!completer.isCompleted) {
... ...
... ... @@ -9,12 +9,10 @@ import 'dart:js_interop';
///
/// Object literals can be made using [jsify].
@JS('Map')
@staticInterop
class JSMap<K extends JSAny, V extends JSAny> {
extension type JSMap<K extends JSAny, V extends JSAny>._(JSObject _)
implements JSObject {
external factory JSMap();
}
extension JSMapExtension<K extends JSAny, V extends JSAny> on JSMap<K, V> {
external V? get(K key);
external JSVoid set(K key, V? value);
}
... ...
... ... @@ -9,25 +9,28 @@ final class MediaTrackConstraintsDelegate {
/// Get the settings for the given [mediaStream].
MediaTrackSettings? getSettings(MediaStream? mediaStream) {
final List<JSAny?>? tracks = mediaStream?.getVideoTracks().toDart;
final List<MediaStreamTrack>? tracks = mediaStream?.getVideoTracks().toDart;
if (tracks == null || tracks.isEmpty) {
return null;
}
final MediaStreamTrack? track = tracks.first as MediaStreamTrack?;
if (track == null) {
return null;
}
final MediaStreamTrack track = tracks.first;
final MediaTrackCapabilities capabilities = track.getCapabilities();
final MediaTrackSettings settings = track.getSettings();
if (capabilities.facingMode.toDart.isEmpty) {
return MediaTrackSettings(
width: settings.width,
height: settings.height,
);
}
return MediaTrackSettings(
width: settings.width,
height: settings.height,
facingMode: settings.facingMode,
aspectRatio: settings.aspectRatio,
);
}
}
... ...
... ... @@ -25,7 +25,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
String? _alternateScriptUrl;
/// The internal barcode reader.
final BarcodeReader _barcodeReader = ZXingBarcodeReader();
BarcodeReader? _barcodeReader;
/// The stream controller for the barcode stream.
final StreamController<BarcodeCapture> _barcodesController =
... ... @@ -35,21 +35,13 @@ class MobileScannerWeb extends MobileScannerPlatform {
StreamSubscription<Object?>? _barcodesSubscription;
/// The container div element for the camera view.
///
/// This container element is used by the barcode reader.
HTMLDivElement? _divElement;
late HTMLDivElement _divElement;
/// This [Completer] is used to prevent additional calls to the [start] method.
///
/// To handle lifecycle changes properly,
/// the scanner is stopped when the application is inactive,
/// and restarted when the application gains focus.
/// The flag that keeps track of whether a permission request is in progress.
///
/// However, when the camera permission is requested,
/// the application is put in the inactive state due to the permission popup gaining focus.
/// Thus, as long as the permission status is not known,
/// any calls to the [start] method are ignored.
Completer<void>? _cameraPermissionCompleter;
/// On the web, a permission request triggers a dialog, that in turn triggers a lifecycle change.
/// While the permission request is in progress, any attempts at (re)starting the camera should be ignored.
bool _permissionRequestInProgress = false;
/// The stream controller for the media track settings stream.
///
... ... @@ -60,18 +52,19 @@ class MobileScannerWeb extends MobileScannerPlatform {
final StreamController<MediaTrackSettings> _settingsController =
StreamController.broadcast();
/// The view type for the platform view factory.
static const String _viewType = 'MobileScannerWeb';
/// The texture ID for the camera view.
int _textureId = 1;
/// The video element for the camera view.
late HTMLVideoElement _videoElement;
/// Get the view type for the platform view factory.
String _getViewType(int textureId) => 'mobile-scanner-view-$textureId';
static void registerWith(Registrar registrar) {
MobileScannerPlatform.instance = MobileScannerWeb();
}
bool get _hasPendingPermissionRequest {
return _cameraPermissionCompleter != null &&
!_cameraPermissionCompleter!.isCompleted;
}
@override
Stream<BarcodeCapture?> get barcodesStream => _barcodesController.stream;
... ... @@ -83,6 +76,33 @@ class MobileScannerWeb extends MobileScannerPlatform {
Stream<double> get zoomScaleStateStream =>
_settingsController.stream.map((_) => 1.0);
/// Create the [HTMLVideoElement] along with its parent container [HTMLDivElement].
HTMLVideoElement _createVideoElement(int textureId) {
final HTMLVideoElement videoElement = HTMLVideoElement();
videoElement.style
..height = '100%'
..width = '100%'
..objectFit = 'cover'
..transformOrigin = 'center'
..pointerEvents = 'none';
// Attach the video element to its parent container
// and setup the PlatformView factory for this `textureId`.
_divElement = HTMLDivElement()
..style.objectFit = 'cover'
..style.height = '100%'
..style.width = '100%'
..append(videoElement);
ui_web.platformViewRegistry.registerViewFactory(
_getViewType(textureId),
(_) => _divElement,
);
return videoElement;
}
void _handleMediaTrackSettingsChange(MediaTrackSettings settings) {
if (_settingsController.isClosed) {
return;
... ... @@ -91,6 +111,32 @@ class MobileScannerWeb extends MobileScannerPlatform {
_settingsController.add(settings);
}
/// Flip the [videoElement] horizontally,
/// if the [videoStream] indicates that is facing the user.
void _maybeFlipVideoPreview(
HTMLVideoElement videoElement,
MediaStream videoStream,
) {
final List<MediaStreamTrack> tracks = videoStream.getVideoTracks().toDart;
if (tracks.isEmpty) {
return;
}
final MediaStreamTrack videoTrack = tracks.first;
final MediaTrackCapabilities capabilities = videoTrack.getCapabilities();
// TODO: this is empty on MacOS, where there is no facing mode, but one, user facing camera.
// Facing mode is not supported by this track, do nothing.
if (capabilities.facingMode.toDart.isEmpty) {
return;
}
if (videoTrack.getSettings().facingMode == 'user') {
videoElement.style.transform = 'scaleX(-1)';
}
}
/// Prepare a [MediaStream] for the video output.
///
/// This method requests permission to use the camera.
... ... @@ -100,7 +146,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
Future<MediaStream> _prepareVideoStream(
CameraFacing cameraDirection,
) async {
if ((window.navigator.mediaDevices as JSAny?).isUndefinedOrNull) {
if (window.navigator.mediaDevices.isUndefinedOrNull) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.unsupported,
errorDetails: MobileScannerErrorDetails(
... ... @@ -115,7 +161,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
final MediaStreamConstraints constraints;
if ((capabilities as JSAny).isUndefinedOrNull || !capabilities.facingMode) {
if (capabilities.isUndefinedOrNull || !capabilities.facingMode) {
constraints = MediaStreamConstraints(video: true.toJS);
} else {
final String facingMode = switch (cameraDirection) {
... ... @@ -124,43 +170,24 @@ class MobileScannerWeb extends MobileScannerPlatform {
};
constraints = MediaStreamConstraints(
video: MediaTrackConstraintSet(facingMode: facingMode.toJS) as JSAny,
video: MediaTrackConstraintSet(
facingMode: facingMode.toJS,
),
);
}
try {
// Retrieving the video track requests the camera permission.
// If the completer is not null, the permission was never requested before.
_cameraPermissionCompleter ??= Completer<void>();
// Retrieving the media devices requests the camera permission.
_permissionRequestInProgress = true;
final MediaStream? videoStream = await window.navigator.mediaDevices
.getUserMedia(constraints)
.toDart as MediaStream?;
final MediaStream videoStream =
await window.navigator.mediaDevices.getUserMedia(constraints).toDart;
// At this point the permission is granted.
if (!_cameraPermissionCompleter!.isCompleted) {
_cameraPermissionCompleter!.complete();
}
if (videoStream == null) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
message:
'Could not create a video stream from the camera with the given options. '
'The browser might not support the given constraints.',
),
);
}
_permissionRequestInProgress = false;
return videoStream;
} on DOMException catch (error, stackTrace) {
// At this point the permission request completed, although with an error,
// but the error is irrelevant for the completer.
if (!_cameraPermissionCompleter!.isCompleted) {
_cameraPermissionCompleter!.complete();
}
final String errorMessage = error.toString();
MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;
... ... @@ -173,6 +200,10 @@ class MobileScannerWeb extends MobileScannerPlatform {
errorCode = MobileScannerErrorCode.permissionDenied;
}
// At this point the permission request completed, although with an error,
// but the error is irrelevant.
_permissionRequestInProgress = false;
throw MobileScannerException(
errorCode: errorCode,
errorDetails: MobileScannerErrorDetails(
... ... @@ -190,11 +221,11 @@ class MobileScannerWeb extends MobileScannerPlatform {
@override
Widget buildCameraView() {
if (!_barcodeReader.isScanning) {
return const SizedBox();
if (_barcodeReader?.isScanning ?? false) {
return HtmlElementView(viewType: _getViewType(_textureId));
}
return const HtmlElementView(viewType: _viewType);
return const SizedBox();
}
@override
... ... @@ -231,27 +262,17 @@ class MobileScannerWeb extends MobileScannerPlatform {
// If the permission request has not yet completed,
// the camera view is not ready yet.
// Prevent the permission popup from triggering a restart of the scanner.
if (_hasPendingPermissionRequest) {
if (_permissionRequestInProgress) {
throw PermissionRequestPendingException();
}
await _barcodeReader.maybeLoadLibrary(
_barcodeReader = ZXingBarcodeReader();
await _barcodeReader?.maybeLoadLibrary(
alternateScriptUrl: _alternateScriptUrl,
);
// 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) {
if (_barcodeReader?.isScanning ?? false) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
errorDetails: MobileScannerErrorDetails(
... ... @@ -273,25 +294,19 @@ class MobileScannerWeb extends MobileScannerPlatform {
}
// Listen for changes to the media track settings.
_barcodeReader.setMediaTrackSettingsListener(
_barcodeReader?.setMediaTrackSettingsListener(
_handleMediaTrackSettingsChange,
);
final HTMLVideoElement videoElement;
_textureId += 1; // Request a new texture.
// Attach the video element to the DOM, through its parent container.
// If a video element is already present, reuse it.
if (_divElement!.children.length == 0) {
videoElement = document.createElement('video') as HTMLVideoElement;
_videoElement = _createVideoElement(_textureId);
_divElement!.appendChild(videoElement);
} else {
videoElement = _divElement!.children.item(0)! as HTMLVideoElement;
}
_maybeFlipVideoPreview(_videoElement, videoStream);
await _barcodeReader.start(
await _barcodeReader?.start(
startOptions,
videoElement: videoElement,
videoElement: _videoElement,
videoStream: videoStream,
);
} catch (error, stackTrace) {
... ... @@ -305,7 +320,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
}
try {
_barcodesSubscription = _barcodeReader.detectBarcodes().listen(
_barcodesSubscription = _barcodeReader?.detectBarcodes().listen(
(BarcodeCapture barcode) {
if (_barcodesController.isClosed) {
return;
... ... @@ -315,15 +330,15 @@ class MobileScannerWeb extends MobileScannerPlatform {
},
);
final bool hasTorch = await _barcodeReader.hasTorch();
final bool hasTorch = await _barcodeReader?.hasTorch() ?? false;
if (hasTorch && startOptions.torchEnabled) {
await _barcodeReader.setTorchState(TorchState.on);
await _barcodeReader?.setTorchState(TorchState.on);
}
return MobileScannerViewAttributes(
hasTorch: hasTorch,
size: _barcodeReader.videoSize,
size: _barcodeReader?.videoSize ?? Size.zero,
);
} catch (error, stackTrace) {
throw MobileScannerException(
... ... @@ -338,15 +353,12 @@ class MobileScannerWeb extends MobileScannerPlatform {
@override
Future<void> stop() async {
if (_barcodesController.isClosed) {
return;
}
// Ensure the barcode scanner is stopped, by cancelling the subscription.
await _barcodesSubscription?.cancel();
_barcodesSubscription = null;
await _barcodeReader.stop();
await _barcodeReader?.stop();
_barcodeReader = null;
}
@override
... ... @@ -358,31 +370,8 @@ class MobileScannerWeb extends MobileScannerPlatform {
@override
Future<void> dispose() async {
if (_barcodesController.isClosed) {
return;
}
// The `_barcodesController` and `_settingsController`
// are not closed, as these have the same lifetime as the plugin.
await stop();
await _barcodesController.close();
await _settingsController.close();
// Finally, remove the video element from the DOM.
try {
final HTMLCollection? divChildren = _divElement?.children;
// Since the exact element is unknown, remove all children.
// In practice, there should only be one child, the single video element.
if (divChildren != null && divChildren.length > 0) {
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.
}
}
}
... ...
... ... @@ -11,96 +11,68 @@ 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 {
extension type Result(JSObject _) implements JSObject {
@JS('barcodeFormat')
external JSNumber? get _barcodeFormat;
external int? get _barcodeFormat;
@JS('text')
external JSString? get _text;
/// Get the text of the result.
external String? get text;
@JS('rawBytes')
external JSUint8Array? get _rawBytes;
@JS('resultPoints')
external JSArray? get _resultPoints;
external JSArray<ResultPoint>? get _resultPoints;
@JS('timestamp')
external JSNumber? get _timestamp;
/// Get the timestamp of the result.
external int? 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 {
switch (_barcodeFormat?.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;
}
return switch (_barcodeFormat) {
0 => BarcodeFormat.aztec,
1 => BarcodeFormat.codabar,
2 => BarcodeFormat.code39,
3 => BarcodeFormat.code93,
4 => BarcodeFormat.code128,
5 => BarcodeFormat.dataMatrix,
6 => BarcodeFormat.ean8,
7 => BarcodeFormat.ean13,
8 => BarcodeFormat.itf,
// Maxicode
9 => BarcodeFormat.unknown,
10 => BarcodeFormat.pdf417,
11 => BarcodeFormat.qrCode,
// RSS 14
12 => BarcodeFormat.unknown,
// RSS EXPANDED
13 => BarcodeFormat.unknown,
14 => BarcodeFormat.upcA,
15 => BarcodeFormat.upcE,
// UPC/EAN extension
16 => BarcodeFormat.unknown,
_ => BarcodeFormat.unknown
};
}
/// Get the raw bytes of the result.
Uint8List? get rawBytes => _rawBytes?.toDart;
/// Get the corner points of the result.
List<Offset> get resultPoints {
final JSArray? points = _resultPoints;
final JSArray<ResultPoint>? points = _resultPoints;
if (points == null) {
return [];
return const [];
}
return points.toDart.cast<ResultPoint>().map((point) {
return points.toDart.map((point) {
return Offset(point.x, point.y);
}).toList();
}
/// Get the raw bytes of the result.
Uint8List? get rawBytes => _rawBytes?.toDart;
/// Get the text of the result.
String? get text => _text?.toDart;
/// Get the timestamp of the result.
int? get timestamp => _timestamp?.toDartInt;
/// Convert this result to a [Barcode].
Barcode get toBarcode {
return Barcode(
... ...
... ... @@ -4,20 +4,10 @@ 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 {
@JS('x')
external JSNumber get _x;
@JS('y')
external JSNumber get _y;
extension type ResultPoint(JSObject _) implements JSObject {
/// The x coordinate of the point.
double get x => _x.toDartDouble;
external double get x;
/// The y coordinate of the point.
double get y => _y.toDartDouble;
external double get y;
}
... ...
... ... @@ -12,8 +12,6 @@ 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;
// TODO: remove the JSAny casts once upgraded to a package:web version that restores "implements JSAny"
/// A barcode reader implementation that uses the ZXing library.
final class ZXingBarcodeReader extends BarcodeReader {
ZXingBarcodeReader();
... ... @@ -48,42 +46,6 @@ final class ZXingBarcodeReader extends BarcodeReader {
@override
String get scriptUrl => 'https://unpkg.com/@zxing/library@0.19.1';
/// Get the barcode format from the ZXing library, for the given [format].
static 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:
default:
return -1;
}
}
JSMap? _createReaderHints(List<BarcodeFormat> formats) {
if (formats.isEmpty || formats.contains(BarcodeFormat.all)) {
return null;
... ... @@ -96,8 +58,7 @@ final class ZXingBarcodeReader extends BarcodeReader {
hints.set(
2.toJS,
[
for (final BarcodeFormat format in formats)
getZXingBarcodeFormat(format).toJS,
for (final BarcodeFormat format in formats) format.toJS,
].toJS,
);
... ... @@ -114,9 +75,9 @@ final class ZXingBarcodeReader extends BarcodeReader {
web.MediaStream videoStream,
) async {
final JSPromise? result = _reader?.attachStreamToVideo.callAsFunction(
_reader as JSAny?,
videoStream as JSAny,
videoElement as JSAny,
_reader,
videoStream,
videoElement,
) as JSPromise?;
await result?.toDart;
... ... @@ -135,8 +96,8 @@ final class ZXingBarcodeReader extends BarcodeReader {
controller.onListen = () {
_reader?.decodeContinuously.callAsFunction(
_reader as JSAny?,
_reader?.videoElement as JSAny?,
_reader,
_reader?.videoElement,
(Result? result, JSAny? error) {
if (controller.isClosed || result == null) {
return;
... ... @@ -155,8 +116,8 @@ final class ZXingBarcodeReader extends BarcodeReader {
// 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 as JSAny?);
_reader?.reset.callAsFunction(_reader as JSAny?);
_reader?.stopContinuousDecode.callAsFunction(_reader);
_reader?.reset.callAsFunction(_reader);
await controller.close();
};
... ... @@ -185,7 +146,7 @@ final class ZXingBarcodeReader extends BarcodeReader {
_reader = ZXingBrowserMultiFormatReader(
_createReaderHints(formats),
detectionTimeoutMs.toJS,
detectionTimeoutMs,
);
await _prepareVideoElement(videoElement, videoStream);
... ... @@ -194,8 +155,32 @@ final class ZXingBarcodeReader extends BarcodeReader {
@override
Future<void> stop() async {
_onMediaTrackSettingsChanged = null;
_reader?.stopContinuousDecode.callAsFunction(_reader as JSAny?);
_reader?.reset.callAsFunction(_reader as JSAny?);
_reader?.stopContinuousDecode.callAsFunction(_reader);
_reader?.reset.callAsFunction(_reader);
_reader = null;
}
}
extension on BarcodeFormat {
/// Get the barcode format from the ZXing library.
JSNumber get toJS {
final int zxingFormat = switch (this) {
BarcodeFormat.aztec => 0,
BarcodeFormat.codabar => 1,
BarcodeFormat.code39 => 2,
BarcodeFormat.code93 => 3,
BarcodeFormat.code128 => 4,
BarcodeFormat.dataMatrix => 5,
BarcodeFormat.ean8 => 6,
BarcodeFormat.ean13 => 7,
BarcodeFormat.itf => 8,
BarcodeFormat.pdf417 => 10,
BarcodeFormat.qrCode => 11,
BarcodeFormat.upcA => 14,
BarcodeFormat.upcE => 15,
BarcodeFormat.unknown || BarcodeFormat.all || _ => -1,
};
return zxingFormat.toJS;
}
}
... ...
... ... @@ -7,8 +7,7 @@ import 'package:web/web.dart';
///
/// See https://github.com/zxing-js/library/blob/master/src/browser/BrowserMultiFormatReader.ts
@JS('ZXing.BrowserMultiFormatReader')
@staticInterop
class ZXingBrowserMultiFormatReader {
extension type ZXingBrowserMultiFormatReader._(JSObject _) implements JSObject {
/// Construct a new `ZXing.BrowserMultiFormatReader`.
///
/// The [hints] are the configuration options for the reader.
... ... @@ -17,11 +16,9 @@ class ZXingBrowserMultiFormatReader {
/// See also: https://github.com/zxing-js/library/blob/master/src/core/DecodeHintType.ts
external factory ZXingBrowserMultiFormatReader(
JSMap? hints,
JSNumber? timeBetweenScansMillis,
int timeBetweenScansMillis,
);
}
extension ZXingBrowserMultiFormatReaderExt on ZXingBrowserMultiFormatReader {
/// Attach a [MediaStream] to a [HTMLVideoElement].
///
/// This function accepts a [MediaStream] and a [HTMLVideoElement] as arguments,
... ...
... ... @@ -4,14 +4,14 @@
#
Pod::Spec.new do |s|
s.name = 'mobile_scanner'
s.version = '3.5.6'
s.version = '5.0.0'
s.summary = 'An universal scanner for Flutter based on MLKit.'
s.description = <<-DESC
An universal scanner for Flutter based on MLKit.
DESC
s.homepage = 'http://example.com'
s.homepage = 'https://github.com/juliansteenbakker/mobile_scanner'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.author = { 'Julian Steenbakker' => 'juliansteenbakker@outlook.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'FlutterMacOS'
... ...
name: mobile_scanner
description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS.
version: 5.0.0-beta.2
version: 5.0.0-beta.3
repository: https://github.com/juliansteenbakker/mobile_scanner
screenshots:
... ... @@ -16,8 +16,8 @@ screenshots:
path: example/screenshots/overlay.png
environment:
sdk: ">=3.2.0 <4.0.0"
flutter: ">=3.16.0"
sdk: ">=3.3.0 <4.0.0"
flutter: ">=3.19.0"
dependencies:
flutter:
... ... @@ -25,7 +25,7 @@ dependencies:
flutter_web_plugins:
sdk: flutter
plugin_platform_interface: ^2.0.2
web: ^0.4.0
web: ^0.5.1
dev_dependencies:
flutter_test:
... ...