Julian Steenbakker
Committed by GitHub

Merge pull request #461 from p-mazhnik/pavel/web-auto

feat(web): automatically inject js libraries
  1 +## Next
  2 +
  3 +Improvements:
  4 +* [Web] Automatically inject js libraries
  5 +
1 ## 3.0.0-beta.4 6 ## 3.0.0-beta.4
2 Fixes: 7 Fixes:
3 * Fixes a permission bug on Android where denying the permission would cause an infinite loop of permission requests. 8 * Fixes a permission bug on Android where denying the permission would cause an infinite loop of permission requests.
@@ -53,13 +53,6 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities: @@ -53,13 +53,6 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities:
53 53
54 <img width="696" alt="Screenshot of XCode where Camera is checked" src="https://user-images.githubusercontent.com/24459435/193464115-d76f81d0-6355-4cb2-8bee-538e413a3ad0.png"> 54 <img width="696" alt="Screenshot of XCode where Camera is checked" src="https://user-images.githubusercontent.com/24459435/193464115-d76f81d0-6355-4cb2-8bee-538e413a3ad0.png">
55 55
56 -### Web  
57 -Add this to `web/index.html`:  
58 -  
59 -```html  
60 -<script type="text/javascript" src="https://unpkg.com/@zxing/library@0.19.1"></script>  
61 -```  
62 -  
63 ## Usage 56 ## Usage
64 57
65 Import `package:mobile_scanner/mobile_scanner.dart`, and use the widget with or without the controller. 58 Import `package:mobile_scanner/mobile_scanner.dart`, and use the widget with or without the controller.
@@ -28,8 +28,6 @@ @@ -28,8 +28,6 @@
28 28
29 <title>example</title> 29 <title>example</title>
30 <link rel="manifest" href="manifest.json"> 30 <link rel="manifest" href="manifest.json">
31 - <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>  
32 - <script type="text/javascript" src="https://unpkg.com/@zxing/library@0.19.1"></script>  
33 </head> 31 </head>
34 <body> 32 <body>
35 <!-- This script installs service_worker.js to provide PWA functionality to 33 <!-- This script installs service_worker.js to provide PWA functionality to
@@ -8,6 +8,7 @@ import 'package:mobile_scanner/mobile_scanner_web.dart'; @@ -8,6 +8,7 @@ import 'package:mobile_scanner/mobile_scanner_web.dart';
8 import 'package:mobile_scanner/src/barcode_utility.dart'; 8 import 'package:mobile_scanner/src/barcode_utility.dart';
9 import 'package:mobile_scanner/src/enums/camera_facing.dart'; 9 import 'package:mobile_scanner/src/enums/camera_facing.dart';
10 import 'package:mobile_scanner/src/objects/barcode.dart'; 10 import 'package:mobile_scanner/src/objects/barcode.dart';
  11 +import 'package:mobile_scanner/src/web/utils.dart';
11 12
12 /// This plugin is the web implementation of mobile_scanner. 13 /// This plugin is the web implementation of mobile_scanner.
13 /// It only supports QR codes. 14 /// It only supports QR codes.
@@ -25,6 +26,8 @@ class MobileScannerWebPlugin { @@ -25,6 +26,8 @@ class MobileScannerWebPlugin {
25 ); 26 );
26 final MobileScannerWebPlugin instance = MobileScannerWebPlugin(); 27 final MobileScannerWebPlugin instance = MobileScannerWebPlugin();
27 28
  29 + injectJSLibraries(barCodeReader.jsLibraries);
  30 +
28 channel.setMethodCallHandler(instance.handleMethodCall); 31 channel.setMethodCallHandler(instance.handleMethodCall);
29 event.setController(instance.controller); 32 event.setController(instance.controller);
30 } 33 }
@@ -7,6 +7,24 @@ import 'package:mobile_scanner/src/enums/camera_facing.dart'; @@ -7,6 +7,24 @@ import 'package:mobile_scanner/src/enums/camera_facing.dart';
7 import 'package:mobile_scanner/src/objects/barcode.dart'; 7 import 'package:mobile_scanner/src/objects/barcode.dart';
8 import 'package:mobile_scanner/src/web/media.dart'; 8 import 'package:mobile_scanner/src/web/media.dart';
9 9
  10 +class JsLibrary {
  11 + /// The name of global variable where library is stored.
  12 + /// Used to properly import the library if [usesRequireJs] flag is true
  13 + final String contextName;
  14 + final String url;
  15 +
  16 + /// If js code checks for 'define' variable.
  17 + /// E.g. if at the beginning you see code like
  18 + /// if (typeof define === "function" && define.amd)
  19 + final bool usesRequireJs;
  20 +
  21 + const JsLibrary({
  22 + required this.contextName,
  23 + required this.url,
  24 + required this.usesRequireJs,
  25 + });
  26 +}
  27 +
10 abstract class WebBarcodeReaderBase { 28 abstract class WebBarcodeReaderBase {
11 /// Timer used to capture frames to be analyzed 29 /// Timer used to capture frames to be analyzed
12 Duration frameInterval = const Duration(milliseconds: 200); 30 Duration frameInterval = const Duration(milliseconds: 200);
@@ -21,6 +39,9 @@ abstract class WebBarcodeReaderBase { @@ -21,6 +39,9 @@ abstract class WebBarcodeReaderBase {
21 int get videoWidth; 39 int get videoWidth;
22 int get videoHeight; 40 int get videoHeight;
23 41
  42 + /// JS libraries to be injected into html page.
  43 + List<JsLibrary> get jsLibraries;
  44 +
24 /// Starts streaming video 45 /// Starts streaming video
25 Future<void> start({ 46 Future<void> start({
26 required CameraFacing cameraFacing, 47 required CameraFacing cameraFacing,
@@ -20,11 +20,14 @@ class Code { @@ -20,11 +20,14 @@ class Code {
20 external Uint8ClampedList get binaryData; 20 external Uint8ClampedList get binaryData;
21 } 21 }
22 22
  23 +const jsqrLibrary = JsLibrary(
  24 + contextName: 'jsQR',
  25 + url: 'https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js',
  26 + usesRequireJs: true,
  27 +);
  28 +
23 /// Barcode reader that uses jsQR library. 29 /// Barcode reader that uses jsQR library.
24 /// jsQR supports only QR codes format. 30 /// jsQR supports only QR codes format.
25 -///  
26 -/// Include jsQR to your index.html file:  
27 -/// <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>  
28 class JsQrCodeReader extends WebBarcodeReaderBase 31 class JsQrCodeReader extends WebBarcodeReaderBase
29 with InternalStreamCreation, InternalTorchDetection { 32 with InternalStreamCreation, InternalTorchDetection {
30 JsQrCodeReader({required super.videoContainer}); 33 JsQrCodeReader({required super.videoContainer});
@@ -33,6 +36,9 @@ class JsQrCodeReader extends WebBarcodeReaderBase @@ -33,6 +36,9 @@ class JsQrCodeReader extends WebBarcodeReaderBase
33 bool get isStarted => localMediaStream != null; 36 bool get isStarted => localMediaStream != null;
34 37
35 @override 38 @override
  39 + List<JsLibrary> get jsLibraries => [jsqrLibrary];
  40 +
  41 + @override
36 Future<void> start({ 42 Future<void> start({
37 required CameraFacing cameraFacing, 43 required CameraFacing cameraFacing,
38 List<BarcodeFormat>? formats, 44 List<BarcodeFormat>? formats,
  1 +import 'dart:async';
  2 +import 'dart:html' as html;
  3 +import 'dart:js' show context;
  4 +
  5 +import 'package:js/js.dart';
  6 +import 'package:mobile_scanner/src/web/base.dart';
  7 +
  8 +Future<void> loadScript(JsLibrary library) async {
  9 + // ignore: avoid_dynamic_calls
  10 + if (library.usesRequireJs && context['define']?['amd'] != null) {
  11 + // see https://github.com/dart-lang/sdk/issues/33979
  12 + return loadScriptUsingRequireJS(library.contextName, library.url);
  13 + } else {
  14 + return loadScriptUsingScriptTag(library.url);
  15 + }
  16 +}
  17 +
  18 +Future<void> loadScriptUsingScriptTag(String url) {
  19 + final script = html.ScriptElement()
  20 + ..async = true
  21 + ..defer = false
  22 + ..crossOrigin = 'anonymous'
  23 + ..type = 'text/javascript'
  24 + // ignore: unsafe_html
  25 + ..src = url;
  26 +
  27 + html.document.head!.append(script);
  28 +
  29 + return script.onLoad.first;
  30 +}
  31 +
  32 +Future<void> loadScriptUsingRequireJS(String packageName, String url) {
  33 + final Completer completer = Completer();
  34 + final String eventName = '_${packageName}Loaded';
  35 +
  36 + context.callMethod(
  37 + 'addEventListener',
  38 + [eventName, allowInterop((_) => completer.complete())],
  39 + );
  40 +
  41 + final script = html.ScriptElement()
  42 + ..type = 'text/javascript'
  43 + ..async = false
  44 + ..defer = false
  45 + ..text = '''
  46 + require(["$url"], (package) => {
  47 + window.$packageName = package;
  48 + const event = new Event("$eventName");
  49 + dispatchEvent(event);
  50 + })
  51 + ''';
  52 +
  53 + html.document.head!.append(script);
  54 +
  55 + return completer.future;
  56 +}
  57 +
  58 +/// Injects JS [libraries]
  59 +///
  60 +/// Returns a [Future] that resolves when all of the `script` tags `onLoad` events trigger.
  61 +Future<void> injectJSLibraries(List<JsLibrary> libraries) {
  62 + final List<Future<void>> loading = [];
  63 +
  64 + for (final library in libraries) {
  65 + final future = loadScript(library);
  66 + loading.add(future);
  67 + }
  68 +
  69 + return Future.wait(loading);
  70 +}
@@ -168,10 +168,13 @@ extension JsZXingBrowserMultiFormatReaderExt @@ -168,10 +168,13 @@ extension JsZXingBrowserMultiFormatReaderExt
168 external MediaStream? stream; 168 external MediaStream? stream;
169 } 169 }
170 170
  171 +const zxingJsLibrary = JsLibrary(
  172 + contextName: 'ZXing',
  173 + url: 'https://unpkg.com/@zxing/library@0.19.1',
  174 + usesRequireJs: true,
  175 +);
  176 +
171 /// Barcode reader that uses zxing-js library. 177 /// Barcode reader that uses zxing-js library.
172 -///  
173 -/// Include zxing-js to your index.html file:  
174 -/// <script type="text/javascript" src="https://unpkg.com/@zxing/library@0.19.1"></script>  
175 class ZXingBarcodeReader extends WebBarcodeReaderBase 178 class ZXingBarcodeReader extends WebBarcodeReaderBase
176 with InternalStreamCreation, InternalTorchDetection { 179 with InternalStreamCreation, InternalTorchDetection {
177 JsZXingBrowserMultiFormatReader? _reader; 180 JsZXingBrowserMultiFormatReader? _reader;
@@ -182,6 +185,9 @@ class ZXingBarcodeReader extends WebBarcodeReaderBase @@ -182,6 +185,9 @@ class ZXingBarcodeReader extends WebBarcodeReaderBase
182 bool get isStarted => localMediaStream != null; 185 bool get isStarted => localMediaStream != null;
183 186
184 @override 187 @override
  188 + List<JsLibrary> get jsLibraries => [zxingJsLibrary];
  189 +
  190 + @override
185 Future<void> start({ 191 Future<void> start({
186 required CameraFacing cameraFacing, 192 required CameraFacing cameraFacing,
187 List<BarcodeFormat>? formats, 193 List<BarcodeFormat>? formats,