support scanning from image data

This commit is contained in:
Adam Velebil 2023-10-10 13:49:01 +02:00
parent 4ffda92872
commit 3b766fb28f
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
17 changed files with 292 additions and 69 deletions

View File

@ -36,7 +36,9 @@ import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.google.zxing.* import com.google.zxing.BinaryBitmap
import com.google.zxing.NotFoundException
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.common.HybridBinarizer import com.google.zxing.common.HybridBinarizer
import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
@ -290,11 +292,6 @@ internal class QRScannerView(
) : ImageAnalysis.Analyzer { ) : ImageAnalysis.Analyzer {
var analysisPaused = false var analysisPaused = false
val multiFormatReader = MultiFormatReader().also {
it.setHints(mapOf(DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE)))
}
var analyzedImagesCount = 0 var analyzedImagesCount = 0
private fun ByteBuffer.toByteArray(lastRowPadding: Int): ByteArray { private fun ByteBuffer.toByteArray(lastRowPadding: Int): ByteArray {
@ -392,14 +389,14 @@ internal class QRScannerView(
fullSize fullSize
} }
val result: com.google.zxing.Result = multiFormatReader.decode(bitmapToProcess) val result = QrCodeScanner.scan(bitmapToProcess)
if (analysisPaused) { if (analysisPaused) {
return return
} }
analysisPaused = true // pause analysisPaused = true // pause
Log.v(TAG, "Analysis result: ${result.text}") Log.v(TAG, "Analysis result: $result")
listener.invoke(Result.success(result.text)) listener.invoke(Result.success(result))
} catch (_: NotFoundException) { } catch (_: NotFoundException) {
if (analyzedImagesCount == 0) { if (analyzedImagesCount == 0) {
Log.v(TAG, " No QR code found (NotFoundException)") Log.v(TAG, " No QR code found (NotFoundException)")

View File

@ -16,6 +16,12 @@
package com.yubico.authenticator.flutter_plugins.qrscanner_zxing package com.yubico.authenticator.flutter_plugins.qrscanner_zxing
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import com.google.zxing.BinaryBitmap
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.common.HybridBinarizer
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
@ -24,6 +30,7 @@ import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry import io.flutter.plugin.common.PluginRegistry
import java.lang.Exception
class PermissionsResultRegistrar { class PermissionsResultRegistrar {
@ -65,10 +72,48 @@ class QRScannerZxingPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
} }
override fun onMethodCall(call: MethodCall, result: Result) { override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "getPlatformVersion") { when (call.method) {
result.success("Android ${android.os.Build.VERSION.RELEASE}") "getPlatformVersion" -> {
} else { result.success("Android ${android.os.Build.VERSION.RELEASE}")
result.notImplemented() }
"scanBitmap" -> {
val imageFileData = call.argument<ByteArray>("bitmap")
var bitmap: Bitmap? = null
try {
imageFileData?.let { byteArray ->
Log.i(TAG, "Received ${byteArray.size} bytes")
val options = BitmapFactory.Options()
options.inSampleSize = 4
bitmap = BitmapFactory.decodeByteArray(imageFileData, 0, byteArray.size, options)
bitmap?.let {
val intArray = IntArray(it.allocationByteCount)
it.getPixels(intArray, 0, it.width, 0, 0, it.width, it.height)
val luminanceSource =
RGBLuminanceSource(it.rowBytes, it.height, intArray)
val binaryBitmap = BinaryBitmap(HybridBinarizer(luminanceSource))
val scanResult = QrCodeScanner.scan(binaryBitmap)
Log.i(TAG, "Scan result: $scanResult")
result.success(scanResult)
return
}
}
} catch (e: Exception) {
Log.e(TAG, "Failure decoding data: $e")
result.error("Failed to decode", e.message, e)
return
} finally {
bitmap?.let {
it.recycle()
bitmap = null
}
}
Log.e(TAG, "Failure decoding data: Invalid image format ")
result.error("Failed to decode", "Invalid image format", null)
}
else -> {
result.notImplemented()
}
} }
} }
@ -97,4 +142,8 @@ class QRScannerZxingPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
): Boolean { ): Boolean {
return registrar.onResult(requestCode, permissions, grantResults) return registrar.onResult(requestCode, permissions, grantResults)
} }
companion object {
const val TAG = "QRScannerZxPlugin"
}
} }

View File

@ -0,0 +1,18 @@
package com.yubico.authenticator.flutter_plugins.qrscanner_zxing
import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap
import com.google.zxing.DecodeHintType
import com.google.zxing.MultiFormatReader
object QrCodeScanner {
private val qrCodeScanner = MultiFormatReader().also {
it.setHints(mapOf(DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE)))
}
fun scan(binaryBitmap: BinaryBitmap) : String {
val result: com.google.zxing.Result = qrCodeScanner.decode(binaryBitmap)
return result.text
}
}

View File

@ -26,9 +26,11 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion 33 compileSdk 34
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
namespace 'com.yubico.authenticator.flutter_plugins.qrscanner_zxing_example'
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
@ -48,7 +50,7 @@ android {
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 34
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }
@ -60,6 +62,10 @@ android {
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }
} }
buildFeatures {
buildConfig true
}
} }
flutter { flutter {

View File

@ -1,12 +1,12 @@
buildscript { buildscript {
ext.kotlin_version = '1.7.21' ext.kotlin_version = '1.9.10'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.1.3' classpath 'com.android.tools.build:gradle:8.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }
@ -26,6 +26,6 @@ subprojects {
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
} }
task clean(type: Delete) { tasks.register('clean', Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip

View File

@ -14,7 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:qrscanner_zxing/qrscanner_zxing_method_channel.dart';
import 'package:qrscanner_zxing/qrscanner_zxing_view.dart'; import 'package:qrscanner_zxing/qrscanner_zxing_view.dart';
import 'cutout_overlay.dart'; import 'cutout_overlay.dart';
@ -64,6 +66,32 @@ class AppHomePage extends StatelessWidget {
)); ));
}, },
child: const Text("Open QR Scanner")), child: const Text("Open QR Scanner")),
ElevatedButton(
onPressed: () async {
final result = await FilePicker.platform.pickFiles(
allowedExtensions: ['png', 'jpg'],
type: FileType.custom,
allowMultiple: false,
lockParentWindow: true,
withData: true,
dialogTitle: 'Select file with QR code');
if (result == null || !result.isSinglePick) {
// no result
return;
}
final bytes = result.files.first.bytes;
if (bytes != null) {
var channel = MethodChannelQRScannerZxing();
var value = await channel.scanBitmap(bytes);
debugPrint(value);
} else {
// no files selected
}
},
child: const Text("Scan from file")),
], ],
), ),
), ),

View File

@ -37,10 +37,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.1" version: "1.17.2"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@ -57,6 +57,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030
url: "https://pub.dev"
source: hosted
version: "5.5.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -70,19 +86,24 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c
url: "https://pub.dev"
source: hosted
version: "2.0.16"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
js: flutter_web_plugins:
dependency: transitive dependency: transitive
description: description: flutter
name: js source: sdk
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 version: "0.0.0"
url: "https://pub.dev"
source: hosted
version: "0.6.7"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@ -95,18 +116,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.15" version: "0.12.16"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0" version: "0.5.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -127,10 +148,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: plugin_platform_interface name: plugin_platform_interface
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.6"
qrscanner_zxing: qrscanner_zxing:
dependency: "direct main" dependency: "direct main"
description: description:
@ -147,10 +168,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.10.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -187,10 +208,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.1" version: "0.6.0"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -199,6 +220,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
win32:
dependency: transitive
description:
name: win32
sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3"
url: "https://pub.dev"
source: hosted
version: "5.0.9"
sdks: sdks:
dart: ">=3.0.0-0 <4.0.0" dart: ">=3.1.0-185.0.dev <4.0.0"
flutter: ">=2.5.0" flutter: ">=3.7.0"

View File

@ -4,7 +4,7 @@ description: Demonstrates how to use the qrscanner_zxing plugin.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: 'none' # Remove this line if you wish to publish to pub.dev
environment: environment:
sdk: ">=2.17.0-266.1.beta <3.0.0" sdk: '>=3.0.0 <4.0.0'
dependencies: dependencies:
flutter: flutter:
@ -14,6 +14,7 @@ dependencies:
path: ../ path: ../
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
file_picker: ^5.3.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -14,10 +14,16 @@
* limitations under the License. * limitations under the License.
*/ */
import 'dart:typed_data';
import 'qrscanner_zxing_platform_interface.dart'; import 'qrscanner_zxing_platform_interface.dart';
class QRScannerZxing { class QRScannerZxing {
Future<String?> getPlatformVersion() { Future<String?> getPlatformVersion() {
return QRScannerZxingPlatform.instance.getPlatformVersion(); return QRScannerZxingPlatform.instance.getPlatformVersion();
} }
Future<String?> scanBitmap(Uint8List bitmap) {
return QRScannerZxingPlatform.instance.scanBitmap(bitmap);
}
} }

View File

@ -31,4 +31,11 @@ class MethodChannelQRScannerZxing extends QRScannerZxingPlatform {
await methodChannel.invokeMethod<String>('getPlatformVersion'); await methodChannel.invokeMethod<String>('getPlatformVersion');
return version; return version;
} }
@override
Future<String?> scanBitmap(Uint8List bitmap) async {
final version = await methodChannel
.invokeMethod<String>('scanBitmap', {'bitmap': bitmap});
return version;
}
} }

View File

@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import 'dart:typed_data';
import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'qrscanner_zxing_method_channel.dart'; import 'qrscanner_zxing_method_channel.dart';
@ -42,4 +44,8 @@ abstract class QRScannerZxingPlatform extends PlatformInterface {
Future<String?> getPlatformVersion() { Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.'); throw UnimplementedError('platformVersion() has not been implemented.');
} }
Future<String?> scanBitmap(Uint8List bitmap) {
throw UnimplementedError('platformVersion() has not been implemented.');
}
} }

View File

@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:qrscanner_zxing/qrscanner_zxing.dart'; import 'package:qrscanner_zxing/qrscanner_zxing.dart';
@ -25,6 +27,10 @@ class MockQRScannerZxingPlatform
implements QRScannerZxingPlatform { implements QRScannerZxingPlatform {
@override @override
Future<String?> getPlatformVersion() => Future.value('42'); Future<String?> getPlatformVersion() => Future.value('42');
@override
Future<String?> scanBitmap(Uint8List bitmap) =>
Future.value(bitmap.length.toString());
} }
void main() { void main() {
@ -42,4 +48,12 @@ void main() {
expect(await qrscannerZxingPlugin.getPlatformVersion(), '42'); expect(await qrscannerZxingPlugin.getPlatformVersion(), '42');
}); });
test('scanBitmap', () async {
QRScannerZxing qrscannerZxingPlugin = QRScannerZxing();
MockQRScannerZxingPlatform fakePlatform = MockQRScannerZxingPlatform();
QRScannerZxingPlatform.instance = fakePlatform;
expect(await qrscannerZxingPlugin.scanBitmap(Uint8List(10)), '10');
});
} }

View File

@ -22,6 +22,7 @@ const _prefix = 'android.keys';
const okButton = Key('$_prefix.ok'); const okButton = Key('$_prefix.ok');
const manualEntryButton = Key('$_prefix.manual_entry'); const manualEntryButton = Key('$_prefix.manual_entry');
const readFromImage = Key('$_prefix.read_image_file');
const nfcBypassTouchSetting = Key('$_prefix.nfc_bypass_touch'); const nfcBypassTouchSetting = Key('$_prefix.nfc_bypass_touch');
const nfcSilenceSoundsSettings = Key('$_prefix.nfc_silence_sounds'); const nfcSilenceSoundsSettings = Key('$_prefix.nfc_silence_sounds');

View File

@ -14,39 +14,50 @@
* limitations under the License. * limitations under the License.
*/ */
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:yubico_authenticator/app/state.dart'; import 'package:yubico_authenticator/app/state.dart';
import 'package:yubico_authenticator/exception/cancellation_exception.dart'; import 'package:yubico_authenticator/exception/cancellation_exception.dart';
import 'package:yubico_authenticator/theme.dart'; import 'package:yubico_authenticator/theme.dart';
import 'package:qrscanner_zxing/qrscanner_zxing_method_channel.dart';
import 'qr_scanner_view.dart'; import 'qr_scanner_view.dart';
class AndroidQrScanner implements QrScanner { class AndroidQrScanner implements QrScanner {
final WithContext _withContext; final WithContext _withContext;
AndroidQrScanner(this._withContext); AndroidQrScanner(this._withContext);
@override @override
Future<String?> scanQr([String? _]) async { Future<String?> scanQr([String? imageData]) async {
var scannedCode = await _withContext( if (imageData == null) {
(context) async => await Navigator.of(context).push(PageRouteBuilder( var scannedCode = await _withContext(
pageBuilder: (_, __, ___) => (context) async =>
Theme(data: AppTheme.darkTheme, child: const QrScannerView()), await Navigator.of(context).push(PageRouteBuilder(
settings: const RouteSettings(name: 'android_qr_scanner_view'), pageBuilder: (_, __, ___) =>
transitionDuration: const Duration(seconds: 0), Theme(data: AppTheme.darkTheme, child: const QrScannerView()),
reverseTransitionDuration: const Duration(seconds: 0), settings: const RouteSettings(name: 'android_qr_scanner_view'),
))); transitionDuration: const Duration(seconds: 0),
if (scannedCode == null) { reverseTransitionDuration: const Duration(seconds: 0),
// user has cancelled the scan )));
throw CancellationException(); if (scannedCode == null) {
// user has cancelled the scan
throw CancellationException();
}
if (scannedCode == '') {
return null;
}
return scannedCode;
} else {
var zxingChannel = MethodChannelQRScannerZxing();
return await zxingChannel.scanBitmap(base64Decode(imageData));
} }
if (scannedCode == '') {
return null;
}
return scannedCode;
} }
} }
QrScanner? Function(dynamic) androidQrScannerProvider(hasCamera) { QrScanner? Function(dynamic) androidQrScannerProvider(hasCamera) {
return (ref) => return (ref) =>
hasCamera ? AndroidQrScanner(ref.watch(withContextProvider)) : null; hasCamera ? AndroidQrScanner(ref.watch(withContextProvider)) : null;
} }

View File

@ -14,13 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
import 'dart:convert';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:yubico_authenticator/app/state.dart';
import '../keys.dart' as keys; import '../keys.dart' as keys;
import 'qr_scanner_scan_status.dart'; import 'qr_scanner_scan_status.dart';
class QRScannerUI extends StatelessWidget { class QRScannerUI extends ConsumerWidget {
final ScanStatus status; final ScanStatus status;
final Size screenSize; final Size screenSize;
final GlobalKey overlayWidgetKey; final GlobalKey overlayWidgetKey;
@ -32,7 +37,7 @@ class QRScannerUI extends StatelessWidget {
required this.overlayWidgetKey}); required this.overlayWidgetKey});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
return Stack( return Stack(
@ -73,15 +78,51 @@ class QRScannerUI extends StatelessWidget {
textScaleFactor: 0.7, textScaleFactor: 0.7,
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
), ),
OutlinedButton( Row(
onPressed: () { mainAxisAlignment: MainAxisAlignment.spaceEvenly,
Navigator.of(context).pop(''); children: [
}, OutlinedButton(
key: keys.manualEntryButton, onPressed: () {
child: Text( Navigator.of(context).pop('');
l10n.s_enter_manually, },
style: const TextStyle(color: Colors.white), key: keys.manualEntryButton,
)), child: Text(
l10n.s_enter_manually,
style: const TextStyle(color: Colors.white),
)),
OutlinedButton(
onPressed: () async {
Navigator.of(context).pop('');
final result = await FilePicker.platform.pickFiles(
allowedExtensions: ['png', 'jpg'],
type: FileType.custom,
allowMultiple: false,
lockParentWindow: true,
dialogTitle: 'Select file with QR code');
if (result != null && result.files.isNotEmpty) {
final fileWithCode = result.files.first;
final bytes = fileWithCode.bytes;
if (bytes == null || bytes.isEmpty) {
//err return
return;
}
if (bytes.length > 3 * 1024 * 1024) {
// too big file
return;
}
final scanner = ref.read(qrScannerProvider);
if (scanner != null) {
await scanner.scanQr(base64UrlEncode(bytes));
}
}
},
key: keys.readFromImage,
child: Text(
l10n.s_read_from_image,
style: const TextStyle(color: Colors.white),
)),
],
),
], ],
), ),
const SizedBox(height: 8) const SizedBox(height: 8)

View File

@ -516,6 +516,7 @@
"q_want_to_scan": "Would like to scan?", "q_want_to_scan": "Would like to scan?",
"q_no_qr": "No QR code?", "q_no_qr": "No QR code?",
"s_enter_manually": "Enter manually", "s_enter_manually": "Enter manually",
"s_read_from_image": "Provide image file",
"@_factory_reset": {}, "@_factory_reset": {},
"s_reset": "Reset", "s_reset": "Reset",