Committed by
GitHub
Merge pull request #49 from juliansteenbakker/web
Feature: web support
Showing
16 changed files
with
364 additions
and
24 deletions
example/web/favicon.png
0 → 100644
917 Bytes
example/web/icons/Icon-192.png
0 → 100644
5.17 KB
example/web/icons/Icon-512.png
0 → 100644
8.06 KB
example/web/index.html
0 → 100644
| 1 | +<!DOCTYPE html> | ||
| 2 | +<html> | ||
| 3 | +<head> | ||
| 4 | + <!-- | ||
| 5 | + If you are serving your web app in a path other than the root, change the | ||
| 6 | + href value below to reflect the base path you are serving from. | ||
| 7 | + | ||
| 8 | + The path provided below has to start and end with a slash "/" in order for | ||
| 9 | + it to work correctly. | ||
| 10 | + | ||
| 11 | + Fore more details: | ||
| 12 | + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base | ||
| 13 | + --> | ||
| 14 | + <base href="/"> | ||
| 15 | + | ||
| 16 | + <meta charset="UTF-8"> | ||
| 17 | + <meta content="IE=Edge" http-equiv="X-UA-Compatible"> | ||
| 18 | + <meta name="description" content="A new Flutter project."> | ||
| 19 | + | ||
| 20 | + <!-- iOS meta tags & icons --> | ||
| 21 | + <meta name="apple-mobile-web-app-capable" content="yes"> | ||
| 22 | + <meta name="apple-mobile-web-app-status-bar-style" content="black"> | ||
| 23 | + <meta name="apple-mobile-web-app-title" content="example"> | ||
| 24 | + <link rel="apple-touch-icon" href="icons/Icon-192.png"> | ||
| 25 | + | ||
| 26 | + <!-- Favicon --> | ||
| 27 | + <link rel="icon" type="image/png" href="favicon.png"/> | ||
| 28 | + | ||
| 29 | + <title>example</title> | ||
| 30 | + <link rel="manifest" href="manifest.json"> | ||
| 31 | +<!-- <script src="https://cdn.jsdelivr.net/npm/qr-scanner@1.4.1/qr-scanner.min.js"></script>--> | ||
| 32 | + <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script> | ||
| 33 | +</head> | ||
| 34 | +<body> | ||
| 35 | + <!-- This script installs service_worker.js to provide PWA functionality to | ||
| 36 | + application. For more information, see: | ||
| 37 | + https://developers.google.com/web/fundamentals/primers/service-workers --> | ||
| 38 | + <script> | ||
| 39 | + if ('serviceWorker' in navigator) { | ||
| 40 | + window.addEventListener('flutter-first-frame', function () { | ||
| 41 | + navigator.serviceWorker.register('flutter_service_worker.js'); | ||
| 42 | + }); | ||
| 43 | + } | ||
| 44 | + </script> | ||
| 45 | + <script src="main.dart.js" type="application/javascript"></script> | ||
| 46 | +</body> | ||
| 47 | +</html> |
example/web/manifest.json
0 → 100644
| 1 | +{ | ||
| 2 | + "name": "Mobile Scanner Example", | ||
| 3 | + "short_name": "mobile_scanner_example", | ||
| 4 | + "start_url": ".", | ||
| 5 | + "display": "standalone", | ||
| 6 | + "background_color": "#0175C2", | ||
| 7 | + "theme_color": "#0175C2", | ||
| 8 | + "description": "A barcode and qr code scanner example.", | ||
| 9 | + "orientation": "portrait-primary", | ||
| 10 | + "prefer_related_applications": false, | ||
| 11 | + "icons": [ | ||
| 12 | + { | ||
| 13 | + "src": "icons/Icon-192.png", | ||
| 14 | + "sizes": "192x192", | ||
| 15 | + "type": "image/png" | ||
| 16 | + }, | ||
| 17 | + { | ||
| 18 | + "src": "icons/Icon-512.png", | ||
| 19 | + "sizes": "512x512", | ||
| 20 | + "type": "image/png" | ||
| 21 | + } | ||
| 22 | + ] | ||
| 23 | +} |
lib/mobile_scanner_web_plugin.dart
0 → 100644
| 1 | +import 'dart:async'; | ||
| 2 | + | ||
| 3 | +import 'package:flutter/material.dart'; | ||
| 4 | +import 'package:flutter/services.dart'; | ||
| 5 | +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; | ||
| 6 | +import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 7 | +import 'package:mobile_scanner/src/web/jsqr.dart'; | ||
| 8 | +import 'dart:html' as html; | ||
| 9 | +import 'dart:ui' as ui; | ||
| 10 | + | ||
| 11 | +import 'package:mobile_scanner/src/web/media.dart'; | ||
| 12 | + | ||
| 13 | +/// This plugin is the web implementation of mobile_scanner. | ||
| 14 | +/// It only supports QR codes. | ||
| 15 | +class MobileScannerWebPlugin { | ||
| 16 | + static void registerWith(Registrar registrar) { | ||
| 17 | + PluginEventChannel event = PluginEventChannel( | ||
| 18 | + 'dev.steenbakker.mobile_scanner/scanner/event', | ||
| 19 | + const StandardMethodCodec(), | ||
| 20 | + registrar); | ||
| 21 | + MethodChannel channel = MethodChannel( | ||
| 22 | + 'dev.steenbakker.mobile_scanner/scanner/method', | ||
| 23 | + const StandardMethodCodec(), | ||
| 24 | + registrar); | ||
| 25 | + final MobileScannerWebPlugin instance = MobileScannerWebPlugin(); | ||
| 26 | + WidgetsFlutterBinding.ensureInitialized(); | ||
| 27 | + | ||
| 28 | + channel.setMethodCallHandler(instance.handleMethodCall); | ||
| 29 | + event.setController(instance.controller); | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + // Controller to send events back to the framework | ||
| 33 | + StreamController controller = StreamController(); | ||
| 34 | + | ||
| 35 | + // The video stream. Will be initialized later to see which camera needs to be used. | ||
| 36 | + html.MediaStream? _localStream; | ||
| 37 | + html.VideoElement video = html.VideoElement(); | ||
| 38 | + | ||
| 39 | + // ID of the video feed | ||
| 40 | + String viewID = 'WebScanner-${DateTime.now().millisecondsSinceEpoch}'; | ||
| 41 | + | ||
| 42 | + // Determine wether device has flas | ||
| 43 | + bool hasFlash = false; | ||
| 44 | + | ||
| 45 | + // Timer used to capture frames to be analyzed | ||
| 46 | + Timer? _frameInterval; | ||
| 47 | + | ||
| 48 | + html.DivElement vidDiv = html.DivElement(); | ||
| 49 | + | ||
| 50 | + /// Handle incomming messages | ||
| 51 | + Future<dynamic> handleMethodCall(MethodCall call) async { | ||
| 52 | + switch (call.method) { | ||
| 53 | + case 'start': | ||
| 54 | + return await _start(call.arguments); | ||
| 55 | + case 'torch': | ||
| 56 | + return await _torch(call.arguments); | ||
| 57 | + case 'stop': | ||
| 58 | + return await cancel(); | ||
| 59 | + default: | ||
| 60 | + throw PlatformException( | ||
| 61 | + code: 'Unimplemented', | ||
| 62 | + details: "The mobile_scanner plugin for web doesn't implement " | ||
| 63 | + "the method '${call.method}'"); | ||
| 64 | + } | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + /// Can enable or disable the flash if available | ||
| 68 | + Future<void> _torch(arguments) async { | ||
| 69 | + if (hasFlash) { | ||
| 70 | + final track = _localStream?.getVideoTracks(); | ||
| 71 | + await track!.first.applyConstraints({ | ||
| 72 | + 'advanced': {'torch': arguments == 1} | ||
| 73 | + }); | ||
| 74 | + } else { | ||
| 75 | + controller.addError('Device has no flash'); | ||
| 76 | + } | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + /// Starts the video stream and the scanner | ||
| 80 | + Future<Map> _start(arguments) async { | ||
| 81 | + vidDiv.children = [video]; | ||
| 82 | + | ||
| 83 | + final CameraFacing cameraFacing = | ||
| 84 | + arguments['cameraFacing'] ?? CameraFacing.front; | ||
| 85 | + | ||
| 86 | + // See https://github.com/flutter/flutter/issues/41563 | ||
| 87 | + // ignore: UNDEFINED_PREFIXED_NAME | ||
| 88 | + ui.platformViewRegistry.registerViewFactory( | ||
| 89 | + viewID, | ||
| 90 | + (int id) => vidDiv | ||
| 91 | + ..style.width = '100%' | ||
| 92 | + ..style.height = '100%'); | ||
| 93 | + | ||
| 94 | + // Check if stream is running | ||
| 95 | + if (_localStream != null) { | ||
| 96 | + return { | ||
| 97 | + 'ViewID': viewID, | ||
| 98 | + 'videoWidth': video.videoWidth, | ||
| 99 | + 'videoHeight': video.videoHeight | ||
| 100 | + }; | ||
| 101 | + } | ||
| 102 | + | ||
| 103 | + try { | ||
| 104 | + // Check if browser supports multiple camera's and set if supported | ||
| 105 | + Map? capabilities = | ||
| 106 | + html.window.navigator.mediaDevices?.getSupportedConstraints(); | ||
| 107 | + if (capabilities != null && capabilities['facingMode']) { | ||
| 108 | + UserMediaOptions constraints = UserMediaOptions( | ||
| 109 | + video: VideoOptions( | ||
| 110 | + facingMode: | ||
| 111 | + (cameraFacing == CameraFacing.front ? 'user' : 'environment'), | ||
| 112 | + width: {'ideal': 4096}, | ||
| 113 | + height: {'ideal': 2160}, | ||
| 114 | + )); | ||
| 115 | + | ||
| 116 | + _localStream = | ||
| 117 | + await html.window.navigator.getUserMedia(video: constraints); | ||
| 118 | + } else { | ||
| 119 | + _localStream = await html.window.navigator.getUserMedia(video: true); | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + video.srcObject = _localStream; | ||
| 123 | + | ||
| 124 | + // TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533 | ||
| 125 | + // final track = _localStream?.getVideoTracks(); | ||
| 126 | + // if (track != null) { | ||
| 127 | + // final imageCapture = html.ImageCapture(track.first); | ||
| 128 | + // final photoCapabilities = await imageCapture.getPhotoCapabilities(); | ||
| 129 | + // } | ||
| 130 | + | ||
| 131 | + // required to tell iOS safari we don't want fullscreen | ||
| 132 | + video.setAttribute('playsinline', 'true'); | ||
| 133 | + | ||
| 134 | + await video.play(); | ||
| 135 | + | ||
| 136 | + // Then capture a frame to be analyzed every 200 miliseconds | ||
| 137 | + _frameInterval = | ||
| 138 | + Timer.periodic(const Duration(milliseconds: 200), (timer) { | ||
| 139 | + _captureFrame(); | ||
| 140 | + }); | ||
| 141 | + | ||
| 142 | + return { | ||
| 143 | + 'ViewID': viewID, | ||
| 144 | + 'videoWidth': video.videoWidth, | ||
| 145 | + 'videoHeight': video.videoHeight, | ||
| 146 | + 'torchable': hasFlash | ||
| 147 | + }; | ||
| 148 | + } catch (e) { | ||
| 149 | + throw PlatformException(code: 'MobileScannerWeb', message: e.toString()); | ||
| 150 | + } | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | + /// Check if any camera's are available | ||
| 154 | + static Future<bool> cameraAvailable() async { | ||
| 155 | + final sources = | ||
| 156 | + await html.window.navigator.mediaDevices!.enumerateDevices(); | ||
| 157 | + for (final e in sources) { | ||
| 158 | + if (e.kind == 'videoinput') { | ||
| 159 | + return true; | ||
| 160 | + } | ||
| 161 | + } | ||
| 162 | + return false; | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + /// Stops the video feed and analyzer | ||
| 166 | + Future<void> cancel() async { | ||
| 167 | + try { | ||
| 168 | + // Stop the camera stream | ||
| 169 | + _localStream!.getTracks().forEach((track) { | ||
| 170 | + if (track.readyState == 'live') { | ||
| 171 | + track.stop(); | ||
| 172 | + } | ||
| 173 | + }); | ||
| 174 | + } catch (e) { | ||
| 175 | + debugPrint('Failed to stop stream: $e'); | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + video.srcObject = null; | ||
| 179 | + _localStream = null; | ||
| 180 | + _frameInterval?.cancel(); | ||
| 181 | + _frameInterval = null; | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + /// Captures a frame and analyzes it for QR codes | ||
| 185 | + Future<dynamic> _captureFrame() async { | ||
| 186 | + if (_localStream == null) return null; | ||
| 187 | + final canvas = | ||
| 188 | + html.CanvasElement(width: video.videoWidth, height: video.videoHeight); | ||
| 189 | + final ctx = canvas.context2D; | ||
| 190 | + | ||
| 191 | + ctx.drawImage(video, 0, 0); | ||
| 192 | + final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!); | ||
| 193 | + | ||
| 194 | + final code = jsQR(imgData.data, canvas.width, canvas.height); | ||
| 195 | + if (code != null) { | ||
| 196 | + controller.add({'name': 'barcodeWeb', 'data': code.data}); | ||
| 197 | + } | ||
| 198 | + } | ||
| 199 | +} |
| 1 | +import 'package:flutter/foundation.dart'; | ||
| 1 | import 'package:flutter/material.dart'; | 2 | import 'package:flutter/material.dart'; |
| 2 | import 'package:mobile_scanner/mobile_scanner.dart'; | 3 | import 'package:mobile_scanner/mobile_scanner.dart'; |
| 3 | 4 | ||
| @@ -12,7 +13,7 @@ class MobileScanner extends StatefulWidget { | @@ -12,7 +13,7 @@ class MobileScanner extends StatefulWidget { | ||
| 12 | /// | 13 | /// |
| 13 | /// [barcode] The barcode object with all information about the scanned code. | 14 | /// [barcode] The barcode object with all information about the scanned code. |
| 14 | /// [args] Information about the state of the MobileScanner widget | 15 | /// [args] Information about the state of the MobileScanner widget |
| 15 | - final Function(Barcode barcode, MobileScannerArguments args)? onDetect; | 16 | + final Function(Barcode barcode, MobileScannerArguments? args)? onDetect; |
| 16 | 17 | ||
| 17 | /// TODO: Function that gets called when the Widget is initialized. Can be usefull | 18 | /// TODO: Function that gets called when the Widget is initialized. Can be usefull |
| 18 | /// to check wether the device has a torch(flash) or not. | 19 | /// to check wether the device has a torch(flash) or not. |
| @@ -78,7 +79,9 @@ class _MobileScannerState extends State<MobileScanner> | @@ -78,7 +79,9 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 78 | child: SizedBox( | 79 | child: SizedBox( |
| 79 | width: value.size.width, | 80 | width: value.size.width, |
| 80 | height: value.size.height, | 81 | height: value.size.height, |
| 81 | - child: Texture(textureId: value.textureId), | 82 | + child: kIsWeb |
| 83 | + ? HtmlElementView(viewType: value.webId!) | ||
| 84 | + : Texture(textureId: value.textureId!), | ||
| 82 | ), | 85 | ), |
| 83 | ), | 86 | ), |
| 84 | ), | 87 | ), |
| @@ -3,14 +3,16 @@ import 'package:flutter/material.dart'; | @@ -3,14 +3,16 @@ import 'package:flutter/material.dart'; | ||
| 3 | /// Camera args for [CameraView]. | 3 | /// Camera args for [CameraView]. |
| 4 | class MobileScannerArguments { | 4 | class MobileScannerArguments { |
| 5 | /// The texture id. | 5 | /// The texture id. |
| 6 | - final int textureId; | 6 | + final int? textureId; |
| 7 | 7 | ||
| 8 | /// Size of the texture. | 8 | /// Size of the texture. |
| 9 | final Size size; | 9 | final Size size; |
| 10 | 10 | ||
| 11 | final bool hasTorch; | 11 | final bool hasTorch; |
| 12 | 12 | ||
| 13 | + final String? webId; | ||
| 14 | + | ||
| 13 | /// Create a [MobileScannerArguments]. | 15 | /// Create a [MobileScannerArguments]. |
| 14 | MobileScannerArguments( | 16 | MobileScannerArguments( |
| 15 | - {required this.textureId, required this.size, required this.hasTorch}); | 17 | + {this.textureId, required this.size, required this.hasTorch, this.webId}); |
| 16 | } | 18 | } |
| @@ -2,6 +2,7 @@ import 'dart:async'; | @@ -2,6 +2,7 @@ import 'dart:async'; | ||
| 2 | import 'dart:io'; | 2 | import 'dart:io'; |
| 3 | 3 | ||
| 4 | import 'package:flutter/cupertino.dart'; | 4 | import 'package:flutter/cupertino.dart'; |
| 5 | +import 'package:flutter/foundation.dart'; | ||
| 5 | import 'package:flutter/services.dart'; | 6 | import 'package:flutter/services.dart'; |
| 6 | import 'package:mobile_scanner/mobile_scanner.dart'; | 7 | import 'package:mobile_scanner/mobile_scanner.dart'; |
| 7 | 8 | ||
| @@ -97,6 +98,9 @@ class MobileScannerController { | @@ -97,6 +98,9 @@ class MobileScannerController { | ||
| 97 | case 'barcodeMac': | 98 | case 'barcodeMac': |
| 98 | barcodesController.add(Barcode(rawValue: data['payload'])); | 99 | barcodesController.add(Barcode(rawValue: data['payload'])); |
| 99 | break; | 100 | break; |
| 101 | + case 'barcodeWeb': | ||
| 102 | + barcodesController.add(Barcode(rawValue: data)); | ||
| 103 | + break; | ||
| 100 | default: | 104 | default: |
| 101 | throw UnimplementedError(); | 105 | throw UnimplementedError(); |
| 102 | } | 106 | } |
| @@ -124,13 +128,15 @@ class MobileScannerController { | @@ -124,13 +128,15 @@ class MobileScannerController { | ||
| 124 | // setAnalyzeMode(AnalyzeMode.barcode.index); | 128 | // setAnalyzeMode(AnalyzeMode.barcode.index); |
| 125 | 129 | ||
| 126 | // Check authorization status | 130 | // Check authorization status |
| 131 | + if (!kIsWeb) { | ||
| 127 | MobileScannerState state = | 132 | MobileScannerState state = |
| 128 | MobileScannerState.values[await methodChannel.invokeMethod('state')]; | 133 | MobileScannerState.values[await methodChannel.invokeMethod('state')]; |
| 129 | switch (state) { | 134 | switch (state) { |
| 130 | case MobileScannerState.undetermined: | 135 | case MobileScannerState.undetermined: |
| 131 | final bool result = await methodChannel.invokeMethod('request'); | 136 | final bool result = await methodChannel.invokeMethod('request'); |
| 132 | - state = | ||
| 133 | - result ? MobileScannerState.authorized : MobileScannerState.denied; | 137 | + state = result |
| 138 | + ? MobileScannerState.authorized | ||
| 139 | + : MobileScannerState.denied; | ||
| 134 | break; | 140 | break; |
| 135 | case MobileScannerState.denied: | 141 | case MobileScannerState.denied: |
| 136 | isStarting = false; | 142 | isStarting = false; |
| @@ -138,6 +144,7 @@ class MobileScannerController { | @@ -138,6 +144,7 @@ class MobileScannerController { | ||
| 138 | case MobileScannerState.authorized: | 144 | case MobileScannerState.authorized: |
| 139 | break; | 145 | break; |
| 140 | } | 146 | } |
| 147 | + } | ||
| 141 | 148 | ||
| 142 | cameraFacingState.value = facing; | 149 | cameraFacingState.value = facing; |
| 143 | 150 | ||
| @@ -173,10 +180,19 @@ class MobileScannerController { | @@ -173,10 +180,19 @@ class MobileScannerController { | ||
| 173 | } | 180 | } |
| 174 | 181 | ||
| 175 | hasTorch = startResult['torchable']; | 182 | hasTorch = startResult['torchable']; |
| 183 | + | ||
| 184 | + if (kIsWeb) { | ||
| 185 | + args.value = MobileScannerArguments( | ||
| 186 | + webId: startResult['ViewID'], | ||
| 187 | + size: Size(startResult['videoWidth'], startResult['videoHeight']), | ||
| 188 | + hasTorch: hasTorch); | ||
| 189 | + } else { | ||
| 176 | args.value = MobileScannerArguments( | 190 | args.value = MobileScannerArguments( |
| 177 | textureId: startResult['textureId'], | 191 | textureId: startResult['textureId'], |
| 178 | size: toSize(startResult['size']), | 192 | size: toSize(startResult['size']), |
| 179 | hasTorch: hasTorch); | 193 | hasTorch: hasTorch); |
| 194 | + } | ||
| 195 | + | ||
| 180 | isStarting = false; | 196 | isStarting = false; |
| 181 | } | 197 | } |
| 182 | 198 |
lib/src/web/jsqr.dart
0 → 100644
lib/src/web/media.dart
0 → 100644
| 1 | +// // This is here because dart doesn't seem to support this properly | ||
| 2 | +// // https://stackoverflow.com/questions/61161135/adding-support-for-navigator-mediadevices-getusermedia-to-dart | ||
| 3 | + | ||
| 4 | +@JS('navigator.mediaDevices') | ||
| 5 | +library media_devices; | ||
| 6 | + | ||
| 7 | +import 'package:js/js.dart'; | ||
| 8 | + | ||
| 9 | +@JS('getUserMedia') | ||
| 10 | +external Future<dynamic> getUserMedia(UserMediaOptions constraints); | ||
| 11 | + | ||
| 12 | +@JS() | ||
| 13 | +@anonymous | ||
| 14 | +class UserMediaOptions { | ||
| 15 | + external VideoOptions get video; | ||
| 16 | + | ||
| 17 | + external factory UserMediaOptions({VideoOptions? video}); | ||
| 18 | +} | ||
| 19 | + | ||
| 20 | +@JS() | ||
| 21 | +@anonymous | ||
| 22 | +class VideoOptions { | ||
| 23 | + external String get facingMode; | ||
| 24 | + // external DeviceIdOptions get deviceId; | ||
| 25 | + external Map get width; | ||
| 26 | + external Map get height; | ||
| 27 | + | ||
| 28 | + external factory VideoOptions( | ||
| 29 | + {String? facingMode, DeviceIdOptions? deviceId, Map? width, Map? height}); | ||
| 30 | +} | ||
| 31 | + | ||
| 32 | +@JS() | ||
| 33 | +@anonymous | ||
| 34 | +class DeviceIdOptions { | ||
| 35 | + external String get exact; | ||
| 36 | + | ||
| 37 | + external factory DeviceIdOptions({String? exact}); | ||
| 38 | +} |
lib/src/web/qr_scanner.dart
0 → 100644
| @@ -8,8 +8,11 @@ environment: | @@ -8,8 +8,11 @@ environment: | ||
| 8 | flutter: ">=1.10.0" | 8 | flutter: ">=1.10.0" |
| 9 | 9 | ||
| 10 | dependencies: | 10 | dependencies: |
| 11 | + js: ^0.6.3 | ||
| 11 | flutter: | 12 | flutter: |
| 12 | sdk: flutter | 13 | sdk: flutter |
| 14 | + flutter_web_plugins: | ||
| 15 | + sdk: flutter | ||
| 13 | 16 | ||
| 14 | dev_dependencies: | 17 | dev_dependencies: |
| 15 | flutter_test: | 18 | flutter_test: |
| @@ -26,3 +29,6 @@ flutter: | @@ -26,3 +29,6 @@ flutter: | ||
| 26 | pluginClass: MobileScannerPlugin | 29 | pluginClass: MobileScannerPlugin |
| 27 | macos: | 30 | macos: |
| 28 | pluginClass: MobileScannerPlugin | 31 | pluginClass: MobileScannerPlugin |
| 32 | + web: | ||
| 33 | + pluginClass: MobileScannerWebPlugin | ||
| 34 | + fileName: mobile_scanner_web_plugin.dart |
-
Please register or login to post a comment