大幅変更&環境バージョンアップ

This commit is contained in:
2024-08-22 14:35:09 +09:00
parent 56e9861c7a
commit dc58dc0584
446 changed files with 29645 additions and 8315 deletions

View File

@ -0,0 +1,7 @@
export 'src/qr_code_scanner.dart';
export 'src/qr_scanner_overlay_shape.dart';
export 'src/types/barcode.dart';
export 'src/types/barcode_format.dart';
export 'src/types/camera.dart';
export 'src/types/camera_exception.dart';
export 'src/types/features.dart';

View File

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
class LifecycleEventHandler extends WidgetsBindingObserver {
LifecycleEventHandler({
required this.resumeCallBack,
});
late final AsyncCallback resumeCallBack;
@override
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
switch (state) {
case AppLifecycleState.resumed:
await resumeCallBack();
break;
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
}
}
}

View File

@ -0,0 +1,373 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'lifecycle_event_handler.dart';
import 'qr_scanner_overlay_shape.dart';
import 'types/barcode.dart';
import 'types/barcode_format.dart';
import 'types/camera.dart';
import 'types/camera_exception.dart';
import 'types/features.dart';
import 'web/flutter_qr_stub.dart'
// ignore: uri_does_not_exist
if (dart.library.html) 'web/flutter_qr_web.dart';
typedef QRViewCreatedCallback = void Function(QRViewController);
typedef PermissionSetCallback = void Function(QRViewController, bool);
/// The [QRView] is the view where the camera
/// and the barcode scanner gets displayed.
class QRView extends StatefulWidget {
const QRView({
required Key key,
required this.onQRViewCreated,
this.overlay,
this.overlayMargin = EdgeInsets.zero,
this.cameraFacing = CameraFacing.back,
this.onPermissionSet,
this.formatsAllowed = const <BarcodeFormat>[],
}) : super(key: key);
/// [onQRViewCreated] gets called when the view is created
final QRViewCreatedCallback onQRViewCreated;
/// Use [overlay] to provide an overlay for the view.
/// This can be used to create a certain scan area.
final QrScannerOverlayShape? overlay;
/// Use [overlayMargin] to provide a margin to [overlay]
final EdgeInsetsGeometry overlayMargin;
/// Set which camera to use on startup.
///
/// [cameraFacing] can either be CameraFacing.front or CameraFacing.back.
/// Defaults to CameraFacing.back
final CameraFacing cameraFacing;
/// Calls the provided [onPermissionSet] callback when the permission is set.
final PermissionSetCallback? onPermissionSet;
/// Use [formatsAllowed] to specify which formats needs to be scanned.
final List<BarcodeFormat> formatsAllowed;
@override
State<StatefulWidget> createState() => _QRViewState();
}
class _QRViewState extends State<QRView> {
late MethodChannel _channel;
late LifecycleEventHandler _observer;
@override
void initState() {
super.initState();
_observer = LifecycleEventHandler(resumeCallBack: updateDimensions);
WidgetsBinding.instance.addObserver(_observer);
}
@override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: onNotification,
child: SizeChangedLayoutNotifier(
child: (widget.overlay != null)
? _getPlatformQrViewWithOverlay()
: _getPlatformQrView(),
),
);
}
@override
void dispose() {
super.dispose();
WidgetsBinding.instance.removeObserver(_observer);
}
Future<void> updateDimensions() async {
await QRViewController.updateDimensions(
widget.key as GlobalKey<State<StatefulWidget>>, _channel,
overlay: widget.overlay);
}
bool onNotification(notification) {
updateDimensions();
return false;
}
Widget _getPlatformQrViewWithOverlay() {
return Stack(
children: [
_getPlatformQrView(),
Padding(
padding: widget.overlayMargin,
child: Container(
decoration: ShapeDecoration(
shape: widget.overlay!,
),
),
)
],
);
}
Widget _getPlatformQrView() {
Widget _platformQrView;
if (kIsWeb) {
_platformQrView = createWebQrView(
onPlatformViewCreated: widget.onQRViewCreated,
onPermissionSet: widget.onPermissionSet,
cameraFacing: widget.cameraFacing,
);
} else {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
_platformQrView = AndroidView(
viewType: 'net.touchcapture.qr.flutterqr/qrview',
onPlatformViewCreated: _onPlatformViewCreated,
creationParams:
_QrCameraSettings(cameraFacing: widget.cameraFacing).toMap(),
creationParamsCodec: const StandardMessageCodec(),
);
break;
case TargetPlatform.iOS:
_platformQrView = UiKitView(
viewType: 'net.touchcapture.qr.flutterqr/qrview',
onPlatformViewCreated: _onPlatformViewCreated,
creationParams:
_QrCameraSettings(cameraFacing: widget.cameraFacing).toMap(),
creationParamsCodec: const StandardMessageCodec(),
);
break;
default:
throw UnsupportedError(
"Trying to use the default qrview implementation for $defaultTargetPlatform but there isn't a default one");
}
}
return _platformQrView;
}
void _onPlatformViewCreated(int id) {
_channel = MethodChannel('net.touchcapture.qr.flutterqr/qrview_$id');
// Start scan after creation of the view
final controller = QRViewController._(
_channel,
widget.key as GlobalKey<State<StatefulWidget>>?,
widget.onPermissionSet,
widget.cameraFacing)
.._startScan(widget.key as GlobalKey<State<StatefulWidget>>,
widget.overlay, widget.formatsAllowed);
// Initialize the controller for controlling the QRView
widget.onQRViewCreated(controller);
}
}
class _QrCameraSettings {
_QrCameraSettings({
this.cameraFacing = CameraFacing.unknown,
});
final CameraFacing cameraFacing;
Map<String, dynamic> toMap() {
return <String, dynamic>{
'cameraFacing': cameraFacing.index,
};
}
}
class QRViewController {
QRViewController._(MethodChannel channel, GlobalKey? qrKey,
PermissionSetCallback? onPermissionSet, CameraFacing cameraFacing)
: _channel = channel,
_cameraFacing = cameraFacing {
_channel.setMethodCallHandler((call) async {
switch (call.method) {
case 'onRecognizeQR':
if (call.arguments != null) {
final args = call.arguments as Map;
final code = args['code'] as String?;
final rawType = args['type'] as String;
// Raw bytes are only supported by Android.
final rawBytes = args['rawBytes'] as List<int>?;
final format = BarcodeTypesExtension.fromString(rawType);
if (format != BarcodeFormat.unknown) {
final barcode = Barcode(code, format, rawBytes);
_scanUpdateController.sink.add(barcode);
} else {
throw Exception('Unexpected barcode type $rawType');
}
}
break;
case 'onPermissionSet':
if (call.arguments != null && call.arguments is bool) {
_hasPermissions = call.arguments;
if (onPermissionSet != null) {
onPermissionSet(this, _hasPermissions);
}
}
break;
}
});
}
final MethodChannel _channel;
final CameraFacing _cameraFacing;
final StreamController<Barcode> _scanUpdateController =
StreamController<Barcode>();
Stream<Barcode> get scannedDataStream => _scanUpdateController.stream;
bool _hasPermissions = false;
bool get hasPermissions => _hasPermissions;
/// Starts the barcode scanner
Future<void> _startScan(GlobalKey key, QrScannerOverlayShape? overlay,
List<BarcodeFormat>? barcodeFormats) async {
// We need to update the dimension before the scan is started.
try {
await QRViewController.updateDimensions(key, _channel, overlay: overlay);
return await _channel.invokeMethod(
'startScan', barcodeFormats?.map((e) => e.asInt()).toList() ?? []);
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}
/// Gets information about which camera is active.
Future<CameraFacing> getCameraInfo() async {
try {
var cameraFacing = await _channel.invokeMethod('getCameraInfo') as int;
if (cameraFacing == -1) return _cameraFacing;
return CameraFacing
.values[await _channel.invokeMethod('getCameraInfo') as int];
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}
/// Flips the camera between available modes
Future<CameraFacing> flipCamera() async {
try {
return CameraFacing
.values[await _channel.invokeMethod('flipCamera') as int];
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}
/// Get flashlight status
Future<bool?> getFlashStatus() async {
try {
return await _channel.invokeMethod('getFlashInfo');
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}
/// Toggles the flashlight between available modes
Future<void> toggleFlash() async {
try {
await _channel.invokeMethod('toggleFlash') as bool?;
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}
/// Pauses the camera and barcode scanning
Future<void> pauseCamera() async {
try {
await _channel.invokeMethod('pauseCamera');
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}
/// Stops barcode scanning and the camera
Future<void> stopCamera() async {
try {
await _channel.invokeMethod('stopCamera');
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}
/// Resumes barcode scanning
Future<void> resumeCamera() async {
try {
await _channel.invokeMethod('resumeCamera');
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}
/// Returns which features are available on device.
Future<SystemFeatures> getSystemFeatures() async {
try {
var features =
await _channel.invokeMapMethod<String, dynamic>('getSystemFeatures');
if (features != null) {
return SystemFeatures.fromJson(features);
}
throw CameraException('Error', 'Could not get system features');
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}
/// Stops the camera and disposes the barcode stream.
void dispose() {
if (defaultTargetPlatform == TargetPlatform.iOS) stopCamera();
_scanUpdateController.close();
}
/// Updates the view dimensions for iOS.
static Future<bool> updateDimensions(GlobalKey key, MethodChannel channel,
{QrScannerOverlayShape? overlay}) async {
if (defaultTargetPlatform == TargetPlatform.iOS) {
// Add small delay to ensure the render box is loaded
await Future.delayed(const Duration(milliseconds: 300));
if (key.currentContext == null) return false;
final renderBox = key.currentContext!.findRenderObject() as RenderBox;
try {
await channel.invokeMethod('setDimensions', {
'width': renderBox.size.width,
'height': renderBox.size.height,
'scanAreaWidth': overlay?.cutOutWidth ?? 0,
'scanAreaHeight': overlay?.cutOutHeight ?? 0,
'scanAreaOffset': overlay?.cutOutBottomOffset ?? 0
});
return true;
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
} else if (defaultTargetPlatform == TargetPlatform.android) {
if (overlay == null) {
return false;
}
await channel.invokeMethod('changeScanArea', {
'scanAreaWidth': overlay.cutOutWidth,
'scanAreaHeight': overlay.cutOutHeight,
'cutOutBottomOffset': overlay.cutOutBottomOffset
});
return true;
}
return false;
}
//Starts/Stops invert scanning.
Future<void> scanInvert(bool isScanInvert) async {
if (defaultTargetPlatform == TargetPlatform.android) {
try {
await _channel
.invokeMethod('invertScan', {"isInvertScan": isScanInvert});
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}
}
}

View File

@ -0,0 +1,183 @@
import 'dart:math';
import 'package:flutter/material.dart';
class QrScannerOverlayShape extends ShapeBorder {
QrScannerOverlayShape({
this.borderColor = Colors.red,
this.borderWidth = 3.0,
this.overlayColor = const Color.fromRGBO(0, 0, 0, 80),
this.borderRadius = 0,
this.borderLength = 40,
double? cutOutSize,
double? cutOutWidth,
double? cutOutHeight,
this.cutOutBottomOffset = 0,
}) : cutOutWidth = cutOutWidth ?? cutOutSize ?? 250,
cutOutHeight = cutOutHeight ?? cutOutSize ?? 250 {
assert(
borderLength <=
min(this.cutOutWidth, this.cutOutHeight) / 2 + borderWidth * 2,
"Border can't be larger than ${min(this.cutOutWidth, this.cutOutHeight) / 2 + borderWidth * 2}",
);
assert(
(cutOutWidth == null && cutOutHeight == null) ||
(cutOutSize == null && cutOutWidth != null && cutOutHeight != null),
'Use only cutOutWidth and cutOutHeight or only cutOutSize');
}
final Color borderColor;
final double borderWidth;
final Color overlayColor;
final double borderRadius;
final double borderLength;
final double cutOutWidth;
final double cutOutHeight;
final double cutOutBottomOffset;
@override
EdgeInsetsGeometry get dimensions => const EdgeInsets.all(10);
@override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
return Path()
..fillType = PathFillType.evenOdd
..addPath(getOuterPath(rect), Offset.zero);
}
@override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
Path _getLeftTopPath(Rect rect) {
return Path()
..moveTo(rect.left, rect.bottom)
..lineTo(rect.left, rect.top)
..lineTo(rect.right, rect.top);
}
return _getLeftTopPath(rect)
..lineTo(
rect.right,
rect.bottom,
)
..lineTo(
rect.left,
rect.bottom,
)
..lineTo(
rect.left,
rect.top,
);
}
@override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
final width = rect.width;
final borderWidthSize = width / 2;
final height = rect.height;
final borderOffset = borderWidth / 2;
final _borderLength =
borderLength > min(cutOutHeight, cutOutHeight) / 2 + borderWidth * 2
? borderWidthSize / 2
: borderLength;
final _cutOutWidth =
cutOutWidth < width ? cutOutWidth : width - borderOffset;
final _cutOutHeight =
cutOutHeight < height ? cutOutHeight : height - borderOffset;
final backgroundPaint = Paint()
..color = overlayColor
..style = PaintingStyle.fill;
final borderPaint = Paint()
..color = borderColor
..style = PaintingStyle.stroke
..strokeWidth = borderWidth;
final boxPaint = Paint()
..color = borderColor
..style = PaintingStyle.fill
..blendMode = BlendMode.dstOut;
final cutOutRect = Rect.fromLTWH(
rect.left + width / 2 - _cutOutWidth / 2 + borderOffset,
-cutOutBottomOffset +
rect.top +
height / 2 -
_cutOutHeight / 2 +
borderOffset,
_cutOutWidth - borderOffset * 2,
_cutOutHeight - borderOffset * 2,
);
canvas
..saveLayer(
rect,
backgroundPaint,
)
..drawRect(
rect,
backgroundPaint,
)
// Draw top right corner
..drawRRect(
RRect.fromLTRBAndCorners(
cutOutRect.right - _borderLength,
cutOutRect.top,
cutOutRect.right,
cutOutRect.top + _borderLength,
topRight: Radius.circular(borderRadius),
),
borderPaint,
)
// Draw top left corner
..drawRRect(
RRect.fromLTRBAndCorners(
cutOutRect.left,
cutOutRect.top,
cutOutRect.left + _borderLength,
cutOutRect.top + _borderLength,
topLeft: Radius.circular(borderRadius),
),
borderPaint,
)
// Draw bottom right corner
..drawRRect(
RRect.fromLTRBAndCorners(
cutOutRect.right - _borderLength,
cutOutRect.bottom - _borderLength,
cutOutRect.right,
cutOutRect.bottom,
bottomRight: Radius.circular(borderRadius),
),
borderPaint,
)
// Draw bottom left corner
..drawRRect(
RRect.fromLTRBAndCorners(
cutOutRect.left,
cutOutRect.bottom - _borderLength,
cutOutRect.left + _borderLength,
cutOutRect.bottom,
bottomLeft: Radius.circular(borderRadius),
),
borderPaint,
)
..drawRRect(
RRect.fromRectAndRadius(
cutOutRect,
Radius.circular(borderRadius),
),
boxPaint,
)
..restore();
}
@override
ShapeBorder scale(double t) {
return QrScannerOverlayShape(
borderColor: borderColor,
borderWidth: borderWidth,
overlayColor: overlayColor,
);
}
}

View File

@ -0,0 +1,16 @@
import 'barcode_format.dart';
/// The [Barcode] object holds information about the barcode or qr code.
///
/// [code] is the string-content of the barcode.
/// [format] displays which type the code is.
/// Only for Android and iOS, [rawBytes] gives a list of bytes of the result.
class Barcode {
Barcode(this.code, this.format, this.rawBytes);
final String? code;
final BarcodeFormat format;
/// Raw bytes are only supported by Android and iOS.
final List<int>? rawBytes;
}

View File

@ -0,0 +1,148 @@
enum BarcodeFormat {
/// Aztec 2D barcode format.
aztec,
/// CODABAR 1D format.
/// Not supported in iOS
codabar,
/// Code 39 1D format.
code39,
/// Code 93 1D format.
code93,
/// Code 128 1D format.
code128,
/// Data Matrix 2D barcode format.
dataMatrix,
/// EAN-8 1D format.
ean8,
/// EAN-13 1D format.
ean13,
/// ITF (Interleaved Two of Five) 1D format.
itf,
/// MaxiCode 2D barcode format.
/// Not supported in iOS.
maxicode,
/// PDF417 format.
pdf417,
/// QR Code 2D barcode format.
qrcode,
/// RSS 14
/// Not supported in iOS.
rss14,
/// RSS EXPANDED
/// Not supported in iOS.
rssExpanded,
/// UPC-A 1D format.
/// Same as ean-13 on iOS.
upcA,
/// UPC-E 1D format.
upcE,
/// UPC/EAN extension format. Not a stand-alone format.
upcEanExtension,
/// Unknown
unknown
}
extension BarcodeTypesExtension on BarcodeFormat {
int asInt() {
return index;
}
static BarcodeFormat fromString(String format) {
switch (format) {
case 'AZTEC':
return BarcodeFormat.aztec;
case 'CODABAR':
return BarcodeFormat.codabar;
case 'CODE_39':
return BarcodeFormat.code39;
case 'CODE_93':
return BarcodeFormat.code93;
case 'CODE_128':
return BarcodeFormat.code128;
case 'DATA_MATRIX':
return BarcodeFormat.dataMatrix;
case 'EAN_8':
return BarcodeFormat.ean8;
case 'EAN_13':
return BarcodeFormat.ean13;
case 'ITF':
return BarcodeFormat.itf;
case 'MAXICODE':
return BarcodeFormat.maxicode;
case 'PDF_417':
return BarcodeFormat.pdf417;
case 'QR_CODE':
return BarcodeFormat.qrcode;
case 'RSS14':
return BarcodeFormat.rss14;
case 'RSS_EXPANDED':
return BarcodeFormat.rssExpanded;
case 'UPC_A':
return BarcodeFormat.upcA;
case 'UPC_E':
return BarcodeFormat.upcE;
case 'UPC_EAN_EXTENSION':
return BarcodeFormat.upcEanExtension;
default:
return BarcodeFormat.unknown;
}
}
String get formatName {
switch (this) {
case BarcodeFormat.aztec:
return 'AZTEC';
case BarcodeFormat.codabar:
return 'CODABAR';
case BarcodeFormat.code39:
return 'CODE_39';
case BarcodeFormat.code93:
return 'CODE_93';
case BarcodeFormat.code128:
return 'CODE_128';
case BarcodeFormat.dataMatrix:
return 'DATA_MATRIX';
case BarcodeFormat.ean8:
return 'EAN_8';
case BarcodeFormat.ean13:
return 'EAN_13';
case BarcodeFormat.itf:
return 'ITF';
case BarcodeFormat.maxicode:
return 'MAXICODE';
case BarcodeFormat.pdf417:
return 'PDF_417';
case BarcodeFormat.qrcode:
return 'QR_CODE';
case BarcodeFormat.rss14:
return 'RSS14';
case BarcodeFormat.rssExpanded:
return 'RSS_EXPANDED';
case BarcodeFormat.upcA:
return 'UPC_A';
case BarcodeFormat.upcE:
return 'UPC_E';
case BarcodeFormat.upcEanExtension:
return 'UPC_EAN_EXTENSION';
default:
return 'UNKNOWN';
}
}
}

View File

@ -0,0 +1,10 @@
enum CameraFacing {
/// Shows back facing camera.
back,
/// Shows front facing camera.
front,
/// Unknown camera
unknown
}

View File

@ -0,0 +1,14 @@
/// This is thrown when the plugin reports an error.
class CameraException implements Exception {
/// Creates a new camera exception with the given error code and description.
CameraException(this.code, this.description);
/// Error code.
String code;
/// Textual description of the error.
String? description;
@override
String toString() => 'CameraException($code, $description)';
}

View File

@ -0,0 +1,13 @@
class SystemFeatures {
SystemFeatures(this.hasFlash, this.hasBackCamera, this.hasFrontCamera);
factory SystemFeatures.fromJson(Map<String, dynamic> features) =>
SystemFeatures(
features['hasFlash'] ?? false,
features['hasBackCamera'] ?? false,
features['hasFrontCamera'] ?? false);
final bool hasFlash;
final bool hasFrontCamera;
final bool hasBackCamera;
}

View File

@ -0,0 +1,6 @@
import 'package:flutter/material.dart';
import 'package:qr_code_scanner/src/types/camera.dart';
Widget createWebQrView(
{onPlatformViewCreated, onPermissionSet, CameraFacing? cameraFacing}) =>
const SizedBox();

View File

@ -0,0 +1,334 @@
// ignore_for_file: avoid_web_libraries_in_flutter
import 'dart:async';
import 'dart:core';
import 'dart:html' as html;
import 'dart:js_util';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import '../../qr_code_scanner.dart';
import 'jsqr.dart';
import 'media.dart';
/// Even though it has been highly modified, the origial implementation has been
/// adopted from https://github.com:treeder/jsqr_flutter
///
/// Copyright 2020 @treeder
/// Copyright 2021 The one with the braid
class WebQrView extends StatefulWidget {
final QRViewCreatedCallback onPlatformViewCreated;
final PermissionSetCallback? onPermissionSet;
final CameraFacing? cameraFacing;
const WebQrView(
{Key? key,
required this.onPlatformViewCreated,
this.onPermissionSet,
this.cameraFacing = CameraFacing.front})
: super(key: key);
@override
_WebQrViewState createState() => _WebQrViewState();
static html.DivElement vidDiv =
html.DivElement(); // need a global for the registerViewFactory
static Future<bool> cameraAvailable() async {
final sources =
await html.window.navigator.mediaDevices!.enumerateDevices();
// List<String> vidIds = [];
var hasCam = false;
for (final e in sources) {
if (e.kind == 'videoinput') {
// vidIds.add(e['deviceId']);
hasCam = true;
}
}
return hasCam;
}
}
class _WebQrViewState extends State<WebQrView> {
html.MediaStream? _localStream;
// html.CanvasElement canvas;
// html.CanvasRenderingContext2D ctx;
bool _currentlyProcessing = false;
QRViewControllerWeb? _controller;
late Size _size = const Size(0, 0);
Timer? timer;
String? code;
String? _errorMsg;
html.VideoElement video = html.VideoElement();
String viewID = 'QRVIEW-' + DateTime.now().millisecondsSinceEpoch.toString();
final StreamController<Barcode> _scanUpdateController =
StreamController<Barcode>();
late CameraFacing facing;
Timer? _frameIntervall;
@override
void initState() {
super.initState();
facing = widget.cameraFacing ?? CameraFacing.front;
// video = html.VideoElement();
WebQrView.vidDiv.children = [video];
// ignore: UNDEFINED_PREFIXED_NAME
ui.platformViewRegistry
.registerViewFactory(viewID, (int id) => WebQrView.vidDiv);
// giving JavaScipt some time to process the DOM changes
Timer(const Duration(milliseconds: 500), () {
start();
});
}
Future start() async {
await _makeCall();
_frameIntervall?.cancel();
_frameIntervall =
Timer.periodic(const Duration(milliseconds: 200), (timer) {
_captureFrame2();
});
}
void cancel() {
if (timer != null) {
timer!.cancel();
timer = null;
}
if (_currentlyProcessing) {
_stopStream();
}
}
@override
void dispose() {
cancel();
super.dispose();
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> _makeCall() async {
if (_localStream != null) {
return;
}
try {
var constraints = UserMediaOptions(
video: VideoOptions(
facingMode: (facing == CameraFacing.front ? 'user' : 'environment'),
));
// dart style, not working properly:
// var stream =
// await html.window.navigator.mediaDevices.getUserMedia(constraints);
// straight JS:
if (_controller == null) {
_controller = QRViewControllerWeb(this);
widget.onPlatformViewCreated(_controller!);
}
var stream = await promiseToFuture(getUserMedia(constraints));
widget.onPermissionSet?.call(_controller!, true);
_localStream = stream;
video.srcObject = _localStream;
video.setAttribute('playsinline',
'true'); // required to tell iOS safari we don't want fullscreen
await video.play();
} catch (e) {
cancel();
if (e.toString().contains("NotAllowedError")) {
widget.onPermissionSet?.call(_controller!, false);
}
setState(() {
_errorMsg = e.toString();
});
return;
}
if (!mounted) return;
setState(() {
_currentlyProcessing = true;
});
}
Future<void> _stopStream() async {
try {
// await _localStream.dispose();
_localStream!.getTracks().forEach((track) {
if (track.readyState == 'live') {
track.stop();
}
});
// video.stop();
video.srcObject = null;
_localStream = null;
// _localRenderer.srcObject = null;
// ignore: empty_catches
} catch (e) {}
}
Future<dynamic> _captureFrame2() async {
if (_localStream == null) {
return null;
}
final canvas =
html.CanvasElement(width: video.videoWidth, height: video.videoHeight);
final ctx = canvas.context2D;
// canvas.width = video.videoWidth;
// canvas.height = video.videoHeight;
ctx.drawImage(video, 0, 0);
final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!);
final size =
Size(canvas.width?.toDouble() ?? 0, canvas.height?.toDouble() ?? 0);
if (size != _size) {
setState(() {
_setCanvasSize(size);
});
}
try {
final code = jsQR(imgData.data, canvas.width, canvas.height);
// ignore: unnecessary_null_comparison
if (code != null && code.data != null) {
_scanUpdateController
.add(Barcode(code.data, BarcodeFormat.qrcode, code.data.codeUnits));
}
} on NoSuchMethodError {
// Do nothing, this exception occurs continously in web release when no
// code is found.
// NoSuchMethodError: method not found: 'get$data' on null
}
}
@override
Widget build(BuildContext context) {
if (_errorMsg != null) {
return Center(child: Text(_errorMsg!));
}
if (_localStream == null) {
return const Center(child: CircularProgressIndicator());
}
return LayoutBuilder(
builder: (context, constraints) {
var zoom = 1.0;
if (_size.height != 0) zoom = constraints.maxHeight / _size.height;
if (_size.width != 0) {
final horizontalZoom = constraints.maxWidth / _size.width;
if (horizontalZoom > zoom) {
zoom = horizontalZoom;
}
}
return SizedBox(
width: constraints.maxWidth,
height: constraints.maxHeight,
child: Center(
child: SizedBox.fromSize(
size: _size,
child: Transform.scale(
alignment: Alignment.center,
scale: zoom,
child: HtmlElementView(viewType: viewID),
),
),
),
);
},
);
}
void _setCanvasSize(ui.Size size) {
setState(() {
_size = size;
});
}
}
class QRViewControllerWeb implements QRViewController {
final _WebQrViewState _state;
QRViewControllerWeb(this._state);
@override
void dispose() => _state.cancel();
@override
Future<CameraFacing> flipCamera() async {
// TODO: improve error handling
_state.facing = _state.facing == CameraFacing.front
? CameraFacing.back
: CameraFacing.front;
await _state.start();
return _state.facing;
}
@override
Future<CameraFacing> getCameraInfo() async {
return _state.facing;
}
@override
Future<bool?> getFlashStatus() async {
// TODO: flash is simply not supported by JavaScipt. To avoid issuing applications, we always return it to be off.
return false;
}
@override
Future<SystemFeatures> getSystemFeatures() {
// TODO: implement getSystemFeatures
throw UnimplementedError();
}
@override
// TODO: implement hasPermissions. Blocking: WebQrView.cameraAvailable() returns a Future<bool> whereas a bool is required
bool get hasPermissions => throw UnimplementedError();
@override
Future<void> pauseCamera() {
// TODO: implement pauseCamera
throw UnimplementedError();
}
@override
Future<void> resumeCamera() {
// TODO: implement resumeCamera
throw UnimplementedError();
}
@override
Stream<Barcode> get scannedDataStream => _state._scanUpdateController.stream;
@override
Future<void> stopCamera() {
// TODO: implement stopCamera
throw UnimplementedError();
}
@override
Future<void> toggleFlash() async {
// TODO: flash is simply not supported by JavaScipt
return;
}
@override
Future<void> scanInvert(bool isScanInvert) {
// TODO: implement scanInvert
throw UnimplementedError();
}
}
Widget createWebQrView(
{onPlatformViewCreated, onPermissionSet, CameraFacing? cameraFacing}) =>
WebQrView(
onPlatformViewCreated: onPlatformViewCreated,
onPermissionSet: onPermissionSet,
cameraFacing: cameraFacing,
);

View File

@ -0,0 +1,12 @@
@JS()
library jsqr;
import 'package:js/js.dart';
@JS('jsQR')
external Code jsQR(var data, int? width, int? height);
@JS()
class Code {
external String get data;
}

View File

@ -0,0 +1,36 @@
// 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<dynamic> 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 factory VideoOptions(
{String? facingMode, DeviceIdOptions? deviceId});
}
@JS()
@anonymous
class DeviceIdOptions {
external String get exact;
external factory DeviceIdOptions({String? exact});
}