Julian Steenbakker
Committed by GitHub

Merge branch 'master' into dependabot/gradle/android/androidx.camera-camera-lifecycle-1.2.1

@@ -12,4 +12,4 @@ jobs: @@ -12,4 +12,4 @@ jobs:
12 assign-author: 12 assign-author:
13 runs-on: ubuntu-latest 13 runs-on: ubuntu-latest
14 steps: 14 steps:
15 - - uses: toshimaru/auto-author-assign@v1.6.1 15 + - uses: toshimaru/auto-author-assign@v1.6.2
@@ -11,7 +11,7 @@ jobs: @@ -11,7 +11,7 @@ jobs:
11 analysis: 11 analysis:
12 runs-on: ubuntu-latest 12 runs-on: ubuntu-latest
13 steps: 13 steps:
14 - - uses: actions/checkout@v3.2.0 14 + - uses: actions/checkout@v3.3.0
15 - uses: actions/setup-java@v3.9.0 15 - uses: actions/setup-java@v3.9.0
16 with: 16 with:
17 java-version: 11 17 java-version: 11
@@ -28,7 +28,7 @@ jobs: @@ -28,7 +28,7 @@ jobs:
28 formatting: 28 formatting:
29 runs-on: ubuntu-latest 29 runs-on: ubuntu-latest
30 steps: 30 steps:
31 - - uses: actions/checkout@v3.2.0 31 + - uses: actions/checkout@v3.3.0
32 - uses: actions/setup-java@v3.9.0 32 - uses: actions/setup-java@v3.9.0
33 with: 33 with:
34 java-version: 11 34 java-version: 11
@@ -7,7 +7,7 @@ @@ -7,7 +7,7 @@
7 release-please: 7 release-please:
8 runs-on: ubuntu-latest 8 runs-on: ubuntu-latest
9 steps: 9 steps:
10 - - uses: GoogleCloudPlatform/release-please-action@v3.7.1 10 + - uses: GoogleCloudPlatform/release-please-action@v3.7.3
11 with: 11 with:
12 token: ${{ secrets.GITHUB_TOKEN }} 12 token: ${{ secrets.GITHUB_TOKEN }}
13 release-type: simple 13 release-type: simple
  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.
@@ -9,7 +9,7 @@ buildscript { @@ -9,7 +9,7 @@ buildscript {
9 } 9 }
10 10
11 dependencies { 11 dependencies {
12 - classpath 'com.android.tools.build:gradle:7.3.1' 12 + classpath 'com.android.tools.build:gradle:7.4.0'
13 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14 } 14 }
15 } 15 }
@@ -6,7 +6,7 @@ buildscript { @@ -6,7 +6,7 @@ buildscript {
6 } 6 }
7 7
8 dependencies { 8 dependencies {
9 - classpath 'com.android.tools.build:gradle:7.3.1' 9 + classpath 'com.android.tools.build:gradle:7.4.0'
10 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 10 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 } 11 }
12 } 12 }
@@ -28,19 +28,23 @@ class _BarcodeListScannerWithControllerState @@ -28,19 +28,23 @@ class _BarcodeListScannerWithControllerState
28 bool isStarted = true; 28 bool isStarted = true;
29 29
30 void _startOrStop() { 30 void _startOrStop() {
31 - if (isStarted) {  
32 - controller.stop();  
33 - } else {  
34 - controller.start().catchError((error) {  
35 - if (mounted) {  
36 - setState(() {});  
37 - } 31 + try {
  32 + if (isStarted) {
  33 + controller.stop();
  34 + } else {
  35 + controller.start();
  36 + }
  37 + setState(() {
  38 + isStarted = !isStarted;
38 }); 39 });
  40 + } on Exception catch (e) {
  41 + ScaffoldMessenger.of(context).showSnackBar(
  42 + SnackBar(
  43 + content: Text('Something went wrong! $e'),
  44 + backgroundColor: Colors.red,
  45 + ),
  46 + );
39 } 47 }
40 -  
41 - setState(() {  
42 - isStarted = !isStarted;  
43 - });  
44 } 48 }
45 49
46 @override 50 @override
@@ -121,7 +125,7 @@ class _BarcodeListScannerWithControllerState @@ -121,7 +125,7 @@ class _BarcodeListScannerWithControllerState
121 overflow: TextOverflow.fade, 125 overflow: TextOverflow.fade,
122 style: Theme.of(context) 126 style: Theme.of(context)
123 .textTheme 127 .textTheme
124 - .headline4! 128 + .headlineMedium!
125 .copyWith(color: Colors.white), 129 .copyWith(color: Colors.white),
126 ), 130 ),
127 ), 131 ),
@@ -28,19 +28,23 @@ class _BarcodeScannerWithControllerState @@ -28,19 +28,23 @@ class _BarcodeScannerWithControllerState
28 bool isStarted = true; 28 bool isStarted = true;
29 29
30 void _startOrStop() { 30 void _startOrStop() {
31 - if (isStarted) {  
32 - controller.stop();  
33 - } else {  
34 - controller.start().catchError((error) {  
35 - if (mounted) {  
36 - setState(() {});  
37 - } 31 + try {
  32 + if (isStarted) {
  33 + controller.stop();
  34 + } else {
  35 + controller.start();
  36 + }
  37 + setState(() {
  38 + isStarted = !isStarted;
38 }); 39 });
  40 + } on Exception catch (e) {
  41 + ScaffoldMessenger.of(context).showSnackBar(
  42 + SnackBar(
  43 + content: Text('Something went wrong! $e'),
  44 + backgroundColor: Colors.red,
  45 + ),
  46 + );
39 } 47 }
40 -  
41 - setState(() {  
42 - isStarted = !isStarted;  
43 - });  
44 } 48 }
45 49
46 @override 50 @override
@@ -127,7 +131,7 @@ class _BarcodeScannerWithControllerState @@ -127,7 +131,7 @@ class _BarcodeScannerWithControllerState
127 overflow: TextOverflow.fade, 131 overflow: TextOverflow.fade,
128 style: Theme.of(context) 132 style: Theme.of(context)
129 .textTheme 133 .textTheme
130 - .headline4! 134 + .headlineMedium!
131 .copyWith(color: Colors.white), 135 .copyWith(color: Colors.white),
132 ), 136 ),
133 ), 137 ),
@@ -30,19 +30,23 @@ class _BarcodeScannerReturningImageState @@ -30,19 +30,23 @@ class _BarcodeScannerReturningImageState
30 bool isStarted = true; 30 bool isStarted = true;
31 31
32 void _startOrStop() { 32 void _startOrStop() {
33 - if (isStarted) {  
34 - controller.stop();  
35 - } else {  
36 - controller.start().catchError((error) {  
37 - if (mounted) {  
38 - setState(() {});  
39 - } 33 + try {
  34 + if (isStarted) {
  35 + controller.stop();
  36 + } else {
  37 + controller.start();
  38 + }
  39 + setState(() {
  40 + isStarted = !isStarted;
40 }); 41 });
  42 + } on Exception catch (e) {
  43 + ScaffoldMessenger.of(context).showSnackBar(
  44 + SnackBar(
  45 + content: Text('Something went wrong! $e'),
  46 + backgroundColor: Colors.red,
  47 + ),
  48 + );
41 } 49 }
42 -  
43 - setState(() {  
44 - isStarted = !isStarted;  
45 - });  
46 } 50 }
47 51
48 @override 52 @override
@@ -141,7 +145,7 @@ class _BarcodeScannerReturningImageState @@ -141,7 +145,7 @@ class _BarcodeScannerReturningImageState
141 overflow: TextOverflow.fade, 145 overflow: TextOverflow.fade,
142 style: Theme.of(context) 146 style: Theme.of(context)
143 .textTheme 147 .textTheme
144 - .headline4! 148 + .headlineMedium!
145 .copyWith(color: Colors.white), 149 .copyWith(color: Colors.white),
146 ), 150 ),
147 ), 151 ),
@@ -82,7 +82,7 @@ class _BarcodeScannerWithScanWindowState @@ -82,7 +82,7 @@ class _BarcodeScannerWithScanWindowState
82 overflow: TextOverflow.fade, 82 overflow: TextOverflow.fade,
83 style: Theme.of(context) 83 style: Theme.of(context)
84 .textTheme 84 .textTheme
85 - .headline4! 85 + .headlineMedium!
86 .copyWith(color: Colors.white), 86 .copyWith(color: Colors.white),
87 ), 87 ),
88 ), 88 ),
@@ -54,7 +54,7 @@ class _BarcodeScannerWithoutControllerState @@ -54,7 +54,7 @@ class _BarcodeScannerWithoutControllerState
54 overflow: TextOverflow.fade, 54 overflow: TextOverflow.fade,
55 style: Theme.of(context) 55 style: Theme.of(context)
56 .textTheme 56 .textTheme
57 - .headline4! 57 + .headlineMedium!
58 .copyWith(color: Colors.white), 58 .copyWith(color: Colors.white),
59 ), 59 ),
60 ), 60 ),
@@ -109,7 +109,7 @@ class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom> @@ -109,7 +109,7 @@ class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom>
109 overflow: TextOverflow.fade, 109 overflow: TextOverflow.fade,
110 style: Theme.of(context) 110 style: Theme.of(context)
111 .textTheme 111 .textTheme
112 - .headline4! 112 + .headlineMedium!
113 .copyWith(color: Colors.white), 113 .copyWith(color: Colors.white),
114 ), 114 ),
115 ), 115 ),
@@ -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,