瀏覽代碼

Added web support

flutter-beta
TheOneWithTheBraid 5 年之前
父節點
當前提交
37bd70a82a
共有 9 個檔案被更改,包括 455 行新增0 行删除
  1. 二進制
      example/web/favicon.png
  2. 二進制
      example/web/icons/Icon-192.png
  3. 二進制
      example/web/icons/Icon-512.png
  4. +46
    -0
      example/web/index.html
  5. +23
    -0
      example/web/manifest.json
  6. +5
    -0
      lib/src/web/flutter_qr_stub.dart
  7. +334
    -0
      lib/src/web/flutter_qr_web.dart
  8. +12
    -0
      lib/src/web/jsqr.dart
  9. +35
    -0
      lib/src/web/media.dart

二進制
example/web/favicon.png 查看文件

Before After
Width: 16  |  Height: 16  |  Size: 917 B

二進制
example/web/icons/Icon-192.png 查看文件

Before After
Width: 192  |  Height: 192  |  Size: 5.2 KiB

二進制
example/web/icons/Icon-512.png 查看文件

Before After
Width: 512  |  Height: 512  |  Size: 8.1 KiB

+ 46
- 0
example/web/index.html 查看文件

@@ -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 查看文件

@@ -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"
}
]
}

+ 5
- 0
lib/src/web/flutter_qr_stub.dart 查看文件

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

+ 334
- 0
lib/src/web/flutter_qr_web.dart 查看文件

@@ -0,0 +1,334 @@
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;
html.MediaRecorder? _mediaRecorder;

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;

final _canvasKey = GlobalKey();

double _scaleFactor = 1;

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

Future<String?> _captureImage() 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);
var dataUrl = canvas.toDataUrl('image/jpeg', 0.9);
return dataUrl;
}

@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 查看文件

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

+ 35
- 0
lib/src/web/media.dart 查看文件

@@ -0,0 +1,35 @@
// 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});
}

Loading…
取消
儲存