大幅変更&環境バージョンアップ
This commit is contained in:
7
plugin/qr_code_scanner/lib/qr_code_scanner.dart
Normal file
7
plugin/qr_code_scanner/lib/qr_code_scanner.dart
Normal 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';
|
||||
22
plugin/qr_code_scanner/lib/src/lifecycle_event_handler.dart
Normal file
22
plugin/qr_code_scanner/lib/src/lifecycle_event_handler.dart
Normal 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:
|
||||
}
|
||||
}
|
||||
}
|
||||
373
plugin/qr_code_scanner/lib/src/qr_code_scanner.dart
Normal file
373
plugin/qr_code_scanner/lib/src/qr_code_scanner.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
183
plugin/qr_code_scanner/lib/src/qr_scanner_overlay_shape.dart
Normal file
183
plugin/qr_code_scanner/lib/src/qr_scanner_overlay_shape.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
16
plugin/qr_code_scanner/lib/src/types/barcode.dart
Normal file
16
plugin/qr_code_scanner/lib/src/types/barcode.dart
Normal 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;
|
||||
}
|
||||
148
plugin/qr_code_scanner/lib/src/types/barcode_format.dart
Normal file
148
plugin/qr_code_scanner/lib/src/types/barcode_format.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
10
plugin/qr_code_scanner/lib/src/types/camera.dart
Normal file
10
plugin/qr_code_scanner/lib/src/types/camera.dart
Normal file
@ -0,0 +1,10 @@
|
||||
enum CameraFacing {
|
||||
/// Shows back facing camera.
|
||||
back,
|
||||
|
||||
/// Shows front facing camera.
|
||||
front,
|
||||
|
||||
/// Unknown camera
|
||||
unknown
|
||||
}
|
||||
14
plugin/qr_code_scanner/lib/src/types/camera_exception.dart
Normal file
14
plugin/qr_code_scanner/lib/src/types/camera_exception.dart
Normal 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)';
|
||||
}
|
||||
13
plugin/qr_code_scanner/lib/src/types/features.dart
Normal file
13
plugin/qr_code_scanner/lib/src/types/features.dart
Normal 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;
|
||||
}
|
||||
6
plugin/qr_code_scanner/lib/src/web/flutter_qr_stub.dart
Normal file
6
plugin/qr_code_scanner/lib/src/web/flutter_qr_stub.dart
Normal 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();
|
||||
334
plugin/qr_code_scanner/lib/src/web/flutter_qr_web.dart
Normal file
334
plugin/qr_code_scanner/lib/src/web/flutter_qr_web.dart
Normal 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,
|
||||
);
|
||||
12
plugin/qr_code_scanner/lib/src/web/jsqr.dart
Normal file
12
plugin/qr_code_scanner/lib/src/web/jsqr.dart
Normal 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;
|
||||
}
|
||||
36
plugin/qr_code_scanner/lib/src/web/media.dart
Normal file
36
plugin/qr_code_scanner/lib/src/web/media.dart
Normal 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});
|
||||
}
|
||||
Reference in New Issue
Block a user