Explorar el Código

Merge pull request #306 from TheOneWithTheBraid/master

Added web support
flutter-beta
Julian Steenbakker hace 5 años
committed by GitHub
padre
commit
d936a93f0d
No se encontró ninguna clave conocida en la base de datos para esta firma ID de clave GPG: 4AEE18F83AFDEB23
Se han modificado 12 ficheros con 490 adiciones y 24 borrados
  1. +14
    -0
      README.md
  2. BIN
      example/web/favicon.png
  3. BIN
      example/web/icons/Icon-192.png
  4. BIN
      example/web/icons/Icon-512.png
  5. +46
    -0
      example/web/index.html
  6. +23
    -0
      example/web/manifest.json
  7. +33
    -24
      lib/src/qr_code_scanner.dart
  8. +5
    -0
      lib/src/web/flutter_qr_stub.dart
  9. +315
    -0
      lib/src/web/flutter_qr_web.dart
  10. +12
    -0
      lib/src/web/jsqr.dart
  11. +36
    -0
      lib/src/web/media.dart
  12. +6
    -0
      pubspec.yaml

+ 14
- 0
README.md Ver fichero

@@ -132,6 +132,20 @@ In order to use this plugin, add the following to your Info.plist file:
<string>This app needs camera access to scan QR codes</string>
```

## Web Integration

Add this to `web/index.html`:

```html
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.3.1/dist/jsQR.min.js"></script>
```

Please note: on web, only QR codes are supported. Other barcodes and 2D codes cannot be scanned.

Web support is in very earlt stage. Features such as flash, pause or resume are not implemented. Moreover, the camera
preview does not respect the surrounding constraints. This is not at last due to Flutter's early state of platform views
on web.

## Flip Camera (Back/Front)
The default camera is the back camera.
```dart


BIN
example/web/favicon.png Ver fichero

Antes Después
Anchura: 16  |  Altura: 16  |  Tamaño: 917 B

BIN
example/web/icons/Icon-192.png Ver fichero

Antes Después
Anchura: 192  |  Altura: 192  |  Tamaño: 5.2 KiB

BIN
example/web/icons/Icon-512.png Ver fichero

Antes Después
Anchura: 512  |  Altura: 512  |  Tamaño: 8.1 KiB

+ 46
- 0
example/web/index.html Ver fichero

@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.

The path provided below has to start and end with a slash "/" in order for
it to work correctly.

Fore more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
-->
<base href="/">

<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">

<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="example">
<link rel="apple-touch-icon" href="icons/Icon-192.png">

<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>

<title>example</title>
<link rel="manifest" href="manifest.json">
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.3.1/dist/jsQR.min.js"></script>
</head>
<body>
<!-- This script installs service_worker.js to provide PWA functionality to
application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('flutter-first-frame', function () {
navigator.serviceWorker.register('flutter_service_worker.js');
});
}
</script>
<script src="main.dart.js" type="application/javascript"></script>
</body>
</html>

+ 23
- 0
example/web/manifest.json Ver fichero

@@ -0,0 +1,23 @@
{
"name": "example",
"short_name": "example",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

+ 33
- 24
lib/src/qr_code_scanner.dart Ver fichero

@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -12,6 +11,9 @@ 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);
@@ -111,28 +113,35 @@ class _QRViewState extends State<QRView> {

Widget _getPlatformQrView() {
Widget _platformQrView;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
_platformQrView = AndroidView(
viewType: 'net.touchcapture.qr.flutterqr/qrview',
onPlatformViewCreated: _onPlatformViewCreated,
creationParams:
_QrCameraSettings(cameraFacing: widget.cameraFacing).toMap(),
creationParamsCodec: StandardMessageCodec(),
);
break;
case TargetPlatform.iOS:
_platformQrView = UiKitView(
viewType: 'net.touchcapture.qr.flutterqr/qrview',
onPlatformViewCreated: _onPlatformViewCreated,
creationParams:
_QrCameraSettings(cameraFacing: widget.cameraFacing).toMap(),
creationParamsCodec: StandardMessageCodec(),
);
break;
default:
throw UnsupportedError(
"Trying to use the default qrview implementation for $defaultTargetPlatform but there isn't a default one");
if (kIsWeb) {
_platformQrView = createWebQrView(
onPlatformViewCreated: widget.onQRViewCreated,
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: StandardMessageCodec(),
);
break;
case TargetPlatform.iOS:
_platformQrView = UiKitView(
viewType: 'net.touchcapture.qr.flutterqr/qrview',
onPlatformViewCreated: _onPlatformViewCreated,
creationParams:
_QrCameraSettings(cameraFacing: widget.cameraFacing).toMap(),
creationParamsCodec: StandardMessageCodec(),
);
break;
default:
throw UnsupportedError(
"Trying to use the default qrview implementation for $defaultTargetPlatform but there isn't a default one");
}
}
return _platformQrView;
}
@@ -309,7 +318,7 @@ class QRViewController {

/// Stops the camera and disposes the barcode stream.
void dispose() {
if (Platform.isIOS) stopCamera();
if (defaultTargetPlatform == TargetPlatform.iOS) stopCamera();
_scanUpdateController.close();
}



+ 5
- 0
lib/src/web/flutter_qr_stub.dart Ver fichero

@@ -0,0 +1,5 @@
import 'package:flutter/widgets.dart';
import 'package:qr_code_scanner/src/types/camera.dart';

Widget createWebQrView({onPlatformViewCreated, CameraFacing? cameraFacing}) =>
throw UnimplementedError();

+ 315
- 0
lib/src/web/flutter_qr_web.dart Ver fichero

@@ -0,0 +1,315 @@
import 'dart:async';
import 'dart:core';
import 'dart:html' as html;
import 'dart:js_util';
import 'dart:ui' as ui;

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

import '../../qr_code_scanner.dart';
import '../qr_code_scanner.dart';
import '../types/camera.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 CameraFacing? cameraFacing;

const WebQrView(
{Key? key,
required this.onPlatformViewCreated,
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 = Size(0, 0);
Timer? timer;
String? code;
String? _errorMsg;
var video;
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(Duration(milliseconds: 500), () {
start();
});
}

Future start() async {
await _makeCall();
_frameIntervall?.cancel();
_frameIntervall = Timer.periodic(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:
var stream = await promiseToFuture(getUserMedia(constraints));
_localStream = stream;
video.srcObject = _localStream;
video.setAttribute('playsinline',
'true'); // required to tell iOS safari we don't want fullscreen
if (_controller == null) {
_controller = QRViewControllerWeb(this);
widget.onPlatformViewCreated(_controller!);
}
await video.play();
} catch (e) {
cancel();
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);
});
}

final code = jsQR(imgData.data, canvas.width, canvas.height);
// ignore: unnecessary_null_comparison
if (code != null) {
_scanUpdateController
.add(Barcode(code.data, BarcodeFormat.qrcode, code.data.codeUnits));
}
}

@override
Widget build(BuildContext context) {
if (_errorMsg != null) {
return Center(child: Text(_errorMsg!));
}
if (_localStream == null) {
return 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;
}
}

Widget createWebQrView({onPlatformViewCreated, CameraFacing? cameraFacing}) =>
WebQrView(
onPlatformViewCreated: onPlatformViewCreated,
cameraFacing: cameraFacing,
);

+ 12
- 0
lib/src/web/jsqr.dart Ver fichero

@@ -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
- 0
lib/src/web/media.dart Ver fichero

@@ -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});
}

+ 6
- 0
pubspec.yaml Ver fichero

@@ -11,6 +11,8 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_web_plugins:
sdk: flutter

dev_dependencies:
pedantic: ^1.11.0
@@ -23,3 +25,7 @@ flutter:
pluginClass: FlutterQrPlugin
ios:
pluginClass: FlutterQrPlugin

# web:
# pluginClass: FlutterQrPlugin
# fileName: flutter_qr_web.dart

Cargando…
Cancelar
Guardar