Merge pull request #965 from cypherstack/deskcam

Add desktop webcam to FROST process
This commit is contained in:
julian-CStack 2024-08-27 15:36:32 -06:00 committed by GitHub
commit 319080cbca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 501 additions and 446 deletions

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
@ -32,6 +33,7 @@ import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/desktop/desktop_app_bar.dart';
import '../../../../widgets/desktop/desktop_scaffold.dart';
import '../../../../widgets/desktop/primary_button.dart';
import '../../../../widgets/desktop/qr_code_scanner_dialog.dart';
import '../../../../widgets/frost_mascot.dart';
import '../../../../widgets/icon_widgets/clipboard_icon.dart';
import '../../../../widgets/icon_widgets/qrcode_icon.dart';
@ -207,6 +209,54 @@ class _RestoreFrostMsWalletViewState
super.dispose();
}
Future<void> scanQr() async {
try {
if (Platform.isAndroid || Platform.isIOS) {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75),
);
}
final qrResult = await BarcodeScanner.scan();
configFieldController.text = qrResult.rawContent;
setState(() {
_configEmpty = configFieldController.text.isEmpty;
});
} else {
// Platform.isLinux, Platform.isWindows, or Platform.isMacOS.
await showDialog(
context: context,
builder: (context) {
return QrCodeScannerDialog(
onQrCodeDetected: (qrCodeData) {
try {
// TODO [prio=low]: Validate QR code data.
configFieldController.text = qrCodeData;
setState(() {
_configEmpty = configFieldController.text.isEmpty;
});
} catch (e, s) {
Logging.instance.log("Error processing QR code data: $e\n$s",
level: LogLevel.Error);
}
},
);
},
);
}
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
);
}
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
@ -351,31 +401,7 @@ class _RestoreFrostMsWalletViewState
semanticsLabel:
"Scan QR Button. Opens Camera For Scanning QR Code.",
key: const Key("frConfigScanQrButtonKey"),
onTap: () async {
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75),
);
}
final qrResult = await BarcodeScanner.scan();
configFieldController.text =
qrResult.rawContent;
setState(() {
_configEmpty =
configFieldController.text.isEmpty;
});
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
);
}
},
onTap: scanQr,
child: const QrCodeIcon(),
),
],

View file

@ -9,26 +9,14 @@
*/
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:camera_linux/camera_linux.dart';
import 'package:camera_macos/camera_macos_arguments.dart';
import 'package:camera_macos/camera_macos_controller.dart';
import 'package:camera_macos/camera_macos_device.dart';
import 'package:camera_macos/camera_macos_platform_interface.dart';
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:camera_windows/camera_windows.dart';
import 'package:cw_core/monero_transaction_priority.dart';
import 'package:decimal/decimal.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:image/image.dart' as img;
import 'package:zxing2/qrcode.dart';
import '../../../../models/isar/models/contact_entry.dart';
import '../../../../models/paynym/paynym_account_lite.dart';
@ -71,6 +59,7 @@ import '../../../../widgets/desktop/desktop_dialog.dart';
import '../../../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../../../widgets/desktop/desktop_fee_dialog.dart';
import '../../../../widgets/desktop/primary_button.dart';
import '../../../../widgets/desktop/qr_code_scanner_dialog.dart';
import '../../../../widgets/desktop/secondary_button.dart';
import '../../../../widgets/dialogs/firo_exchange_address_dialog.dart';
import '../../../../widgets/fee_slider.dart';
@ -79,7 +68,6 @@ import '../../../../widgets/icon_widgets/clipboard_icon.dart';
import '../../../../widgets/icon_widgets/qrcode_icon.dart';
import '../../../../widgets/icon_widgets/x_icon.dart';
import '../../../../widgets/rounded_container.dart';
import '../../../../widgets/stack_dialog.dart';
import '../../../../widgets/stack_text_field.dart';
import '../../../../widgets/textfield_icon_button.dart';
import '../../../coin_control/desktop_coin_control_use_dialog.dart';
@ -160,7 +148,6 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
context: context,
builder: (context) {
return QrCodeScannerDialog(
walletId: widget.walletId,
onQrCodeDetected: (qrCodeData) {
try {
_processQrCodeData(qrCodeData);
@ -1968,389 +1955,3 @@ String formatAddress(String epicAddress) {
}
return epicAddress;
}
class QrCodeScannerDialog extends StatefulWidget {
final String walletId;
final Function(String) onQrCodeDetected;
QrCodeScannerDialog({
required this.walletId,
required this.onQrCodeDetected,
});
@override
_QrCodeScannerDialogState createState() => _QrCodeScannerDialogState();
}
class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
final CameraLinux? _cameraLinuxPlugin =
Platform.isLinux ? CameraLinux() : null;
final CameraWindows? _cameraWindowsPlugin =
Platform.isWindows ? CameraWindows() : null;
CameraMacOSController? _macOSController;
bool _isCameraOpen = false;
Image? _image;
bool _isScanning = false;
int _cameraId = -1;
String? _macOSDeviceId;
int _imageDelayInMs = 250;
@override
void initState() {
super.initState();
_isCameraOpen = false;
_isScanning = false;
_initializeCamera();
}
@override
void dispose() {
_stopCamera();
super.dispose();
}
Future<void> _initializeCamera() async {
try {
setState(() {
_isScanning = true; // Show the progress indicator
});
if (Platform.isLinux && _cameraLinuxPlugin != null) {
await _cameraLinuxPlugin!.initializeCamera();
Logging.instance.log("Linux Camera initialized", level: LogLevel.Info);
} else if (Platform.isWindows && _cameraWindowsPlugin != null) {
final List<CameraDescription> cameras =
await _cameraWindowsPlugin!.availableCameras();
if (cameras.isEmpty) {
throw CameraException('No cameras available', 'No cameras found.');
}
final CameraDescription camera = cameras[0]; // Could be user-selected.
_cameraId = await _cameraWindowsPlugin!.createCameraWithSettings(
camera,
const MediaSettings(
resolutionPreset: ResolutionPreset.low,
fps: 4,
videoBitrate: 200000,
enableAudio: false,
),
);
await _cameraWindowsPlugin!.initializeCamera(_cameraId);
// await _cameraWindowsPlugin!.onCameraInitialized(_cameraId).first;
// TODO [prio=low]: Make this work. ^^^
Logging.instance.log("Windows Camera initialized with ID: $_cameraId",
level: LogLevel.Info);
} else if (Platform.isMacOS) {
final List<CameraMacOSDevice> videoDevices = await CameraMacOS.instance
.listDevices(deviceType: CameraMacOSDeviceType.video);
if (videoDevices.isEmpty) {
throw Exception('No cameras available');
}
_macOSDeviceId = videoDevices.first.deviceId;
await CameraMacOS.instance
.initialize(cameraMacOSMode: CameraMacOSMode.photo);
setState(() {
_isCameraOpen = true;
});
Logging.instance.log(
"macOS Camera initialized with ID: $_macOSDeviceId",
level: LogLevel.Info);
}
if (mounted) {
setState(() {
_isCameraOpen = true;
_isScanning = true;
});
}
unawaited(_captureAndScanImage()); // Could be awaited.
} catch (e, s) {
Logging.instance
.log("Failed to initialize camera: $e\n$s", level: LogLevel.Error);
if (mounted) {
// widget.onSnackbar("Failed to initialize camera. Please try again.");
setState(() {
_isScanning = false;
});
}
}
}
Future<void> _stopCamera() async {
try {
if (Platform.isLinux && _cameraLinuxPlugin != null) {
_cameraLinuxPlugin!.stopCamera();
Logging.instance.log("Linux Camera stopped", level: LogLevel.Info);
} else if (Platform.isWindows && _cameraWindowsPlugin != null) {
// if (_cameraId >= 0) {
await _cameraWindowsPlugin!.dispose(_cameraId);
Logging.instance.log("Windows Camera stopped with ID: $_cameraId",
level: LogLevel.Info);
// } else {
// Logging.instance.log("Windows Camera ID is null. Cannot dispose.",
// level: LogLevel.Error);
// }
} else if (Platform.isMacOS) {
// if (_macOSDeviceId != null) {
await CameraMacOS.instance.stopImageStream();
Logging.instance.log("macOS Camera stopped with ID: $_macOSDeviceId",
level: LogLevel.Info);
// } else {
// Logging.instance.log("macOS Camera ID is null. Cannot stop.",
// level: LogLevel.Error);
// }
}
} catch (e, s) {
Logging.instance
.log("Failed to stop camera: $e\n$s", level: LogLevel.Error);
} finally {
if (mounted) {
setState(() {
_isScanning = false;
_isCameraOpen = false;
});
}
}
}
Future<void> _captureAndScanImage() async {
while (_isCameraOpen && _isScanning) {
try {
String? base64Image;
if (Platform.isLinux && _cameraLinuxPlugin != null) {
base64Image = await _cameraLinuxPlugin!.captureImage();
} else if (Platform.isWindows) {
final XFile xfile =
await _cameraWindowsPlugin!.takePicture(_cameraId);
final bytes = await xfile.readAsBytes();
base64Image = base64Encode(bytes);
// We could use a Uint8List to optimize for Windows and macOS.
} else if (Platform.isMacOS) {
final macOSimg = await CameraMacOS.instance.takePicture();
if (macOSimg == null) {
Logging.instance
.log("Failed to capture image", level: LogLevel.Error);
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
continue;
}
final img.Image? image = img.decodeImage(macOSimg.bytes!);
if (image == null) {
Logging.instance
.log("Failed to capture image", level: LogLevel.Error);
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
continue;
}
base64Image = base64Encode(Uint8List.fromList(img.encodePng(image)));
}
if (base64Image == null || base64Image.isEmpty) {
// Logging.instance
// .log("Failed to capture image", level: LogLevel.Error);
// Spammy.
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
continue;
}
final img.Image? image = img.decodeImage(base64Decode(base64Image));
// TODO [prio=low]: Optimize this process. Docs say:
// > WARNING Since this will check the image data against all known
// > decoders, it is much slower than using an explicit decoder
if (image == null) {
Logging.instance.log("Failed to decode image", level: LogLevel.Error);
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
continue;
}
if (mounted) {
setState(() {
_image = Image.memory(
base64Decode(base64Image!),
fit: BoxFit.cover,
);
});
}
final String? scanResult = await _scanImage(image);
if (scanResult != null && scanResult.isNotEmpty) {
widget.onQrCodeDetected(scanResult);
if (mounted) {
Navigator.of(context).pop();
}
break;
} else {
// Logging.instance.log("No QR code found in the image", level: LogLevel.Info);
// if (mounted) {
// widget.onSnackbar("No QR code found in the image.");
// }
// Spammy.
}
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
} catch (e, s) {
// Logging.instance.log("Failed to capture and scan image: $e\n$s", level: LogLevel.Error);
// Spammy.
// if (mounted) {
// widget.onSnackbar(
// "Error capturing or scanning the image. Please try again.");
// }
}
}
}
Future<String?> _scanImage(img.Image image) async {
try {
final LuminanceSource source = RGBLuminanceSource(
image.width,
image.height,
image
.convert(numChannels: 4)
.getBytes(order: img.ChannelOrder.abgr)
.buffer
.asInt32List(),
);
final BinaryBitmap bitmap =
BinaryBitmap(GlobalHistogramBinarizer(source));
final QRCodeReader reader = QRCodeReader();
final qrDecode = reader.decode(bitmap);
if (qrDecode.text.isEmpty) {
return null;
}
return qrDecode.text;
} catch (e, s) {
// Logging.instance.log("Failed to decode QR code: $e\n$s", level: LogLevel.Error);
// Spammy.
return null;
}
}
@override
Widget build(BuildContext context) {
return DesktopDialog(
maxWidth: 696,
maxHeight: 600,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left: 32),
child: Text(
"Scan QR code",
style: STextStyles.desktopH3(context),
),
),
const DesktopDialogCloseButton(),
],
),
Expanded(
child: _isCameraOpen
? _image != null
? _image!
: const Center(
child: CircularProgressIndicator(),
)
: const Center(
child:
CircularProgressIndicator(), // Show progress indicator immediately
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(child: Container()),
// "Select file" button.
SecondaryButton(
buttonHeight: ButtonHeight.l,
label: "Select file",
width: 200,
onPressed: () async {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ["png", "jpg", "jpeg"],
);
if (result == null || result.files.single.path == null) {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "Error scanning QR code",
message: "No file selected.",
),
);
return;
}
final filePath = result?.files.single.path!;
if (filePath == null) {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "Error scanning QR code",
message: "Error selecting file.",
),
);
return;
}
try {
final img.Image? image =
img.decodeImage(File(filePath!).readAsBytesSync());
if (image == null) {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "Error scanning QR code",
message: "Failed to decode image.",
),
);
return;
}
final String? scanResult = await _scanImage(image);
if (scanResult != null && scanResult.isNotEmpty) {
widget.onQrCodeDetected(scanResult);
Navigator.of(context).pop();
} else {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "Error scanning QR code",
message: "No QR code found in the image.",
),
);
}
} catch (e, s) {
Logging.instance.log("Failed to decode image: $e\n$s",
level: LogLevel.Error);
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "Error scanning QR code",
message:
"Error processing the image. Please try again.",
),
);
}
},
),
const SizedBox(width: 16),
// Close button.
PrimaryButton(
buttonHeight: ButtonHeight.l,
label: "Close",
width: 272.5,
onPressed: () {
_stopCamera();
Navigator.of(context).pop();
},
),
],
),
),
],
),
);
}
}

View file

@ -0,0 +1,401 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:camera_linux/camera_linux.dart';
import 'package:camera_macos/camera_macos_arguments.dart';
import 'package:camera_macos/camera_macos_device.dart';
import 'package:camera_macos/camera_macos_platform_interface.dart';
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:camera_windows/camera_windows.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:zxing2/qrcode.dart';
import '../../notifications/show_flush_bar.dart';
import '../../utilities/assets.dart';
import '../../utilities/logger.dart';
import '../../utilities/text_styles.dart';
import 'desktop_dialog.dart';
import 'desktop_dialog_close_button.dart';
class QrCodeScannerDialog extends StatefulWidget {
final Function(String) onQrCodeDetected;
QrCodeScannerDialog({
required this.onQrCodeDetected,
});
@override
_QrCodeScannerDialogState createState() => _QrCodeScannerDialogState();
}
class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
final CameraLinux? _cameraLinuxPlugin =
Platform.isLinux ? CameraLinux() : null;
final CameraWindows? _cameraWindowsPlugin =
Platform.isWindows ? CameraWindows() : null;
bool _isCameraOpen = false;
Image? _image;
bool _isScanning = false;
int _cameraId = -1;
String? _macOSDeviceId;
final int _imageDelayInMs = 250;
@override
void initState() {
super.initState();
_isCameraOpen = false;
_isScanning = false;
_initializeCamera();
}
@override
void dispose() {
_stopCamera();
super.dispose();
}
Future<void> _initializeCamera() async {
try {
setState(() {
_isScanning = true; // Show the progress indicator
});
if (Platform.isLinux && _cameraLinuxPlugin != null) {
await _cameraLinuxPlugin!.initializeCamera();
Logging.instance.log("Linux Camera initialized", level: LogLevel.Info);
} else if (Platform.isWindows && _cameraWindowsPlugin != null) {
final List<CameraDescription> cameras =
await _cameraWindowsPlugin!.availableCameras();
if (cameras.isEmpty) {
throw CameraException('No cameras available', 'No cameras found.');
}
final CameraDescription camera = cameras[0]; // Could be user-selected.
_cameraId = await _cameraWindowsPlugin!.createCameraWithSettings(
camera,
const MediaSettings(
resolutionPreset: ResolutionPreset.low,
fps: 4,
videoBitrate: 200000,
enableAudio: false,
),
);
await _cameraWindowsPlugin!.initializeCamera(_cameraId);
// await _cameraWindowsPlugin!.onCameraInitialized(_cameraId).first;
// TODO [prio=low]: Make this work. ^^^
Logging.instance.log("Windows Camera initialized with ID: $_cameraId",
level: LogLevel.Info);
} else if (Platform.isMacOS) {
final List<CameraMacOSDevice> videoDevices = await CameraMacOS.instance
.listDevices(deviceType: CameraMacOSDeviceType.video);
if (videoDevices.isEmpty) {
throw Exception('No cameras available');
}
_macOSDeviceId = videoDevices.first.deviceId;
await CameraMacOS.instance
.initialize(cameraMacOSMode: CameraMacOSMode.photo);
setState(() {
_isCameraOpen = true;
});
Logging.instance.log(
"macOS Camera initialized with ID: $_macOSDeviceId",
level: LogLevel.Info);
}
if (mounted) {
setState(() {
_isCameraOpen = true;
_isScanning = true;
});
}
unawaited(_captureAndScanImage()); // Could be awaited.
} catch (e, s) {
Logging.instance
.log("Failed to initialize camera: $e\n$s", level: LogLevel.Error);
if (mounted) {
// widget.onSnackbar("Failed to initialize camera. Please try again.");
setState(() {
_isScanning = false;
});
}
}
}
Future<void> _stopCamera() async {
try {
if (Platform.isLinux && _cameraLinuxPlugin != null) {
_cameraLinuxPlugin!.stopCamera();
Logging.instance.log("Linux Camera stopped", level: LogLevel.Info);
} else if (Platform.isWindows && _cameraWindowsPlugin != null) {
// if (_cameraId >= 0) {
await _cameraWindowsPlugin!.dispose(_cameraId);
Logging.instance.log("Windows Camera stopped with ID: $_cameraId",
level: LogLevel.Info);
// } else {
// Logging.instance.log("Windows Camera ID is null. Cannot dispose.",
// level: LogLevel.Error);
// }
} else if (Platform.isMacOS) {
// if (_macOSDeviceId != null) {
await CameraMacOS.instance.stopImageStream();
Logging.instance.log("macOS Camera stopped with ID: $_macOSDeviceId",
level: LogLevel.Info);
// } else {
// Logging.instance.log("macOS Camera ID is null. Cannot stop.",
// level: LogLevel.Error);
// }
}
} catch (e, s) {
Logging.instance
.log("Failed to stop camera: $e\n$s", level: LogLevel.Error);
} finally {
if (mounted) {
setState(() {
_isScanning = false;
_isCameraOpen = false;
});
}
}
}
Future<void> _captureAndScanImage() async {
while (_isCameraOpen && _isScanning) {
try {
String? base64Image;
if (Platform.isLinux && _cameraLinuxPlugin != null) {
base64Image = await _cameraLinuxPlugin!.captureImage();
} else if (Platform.isWindows) {
final XFile xfile =
await _cameraWindowsPlugin!.takePicture(_cameraId);
final bytes = await xfile.readAsBytes();
base64Image = base64Encode(bytes);
// We could use a Uint8List to optimize for Windows and macOS.
} else if (Platform.isMacOS) {
final macOSimg = await CameraMacOS.instance.takePicture();
if (macOSimg == null) {
Logging.instance
.log("Failed to capture image", level: LogLevel.Error);
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
continue;
}
final img.Image? image = img.decodeImage(macOSimg.bytes!);
if (image == null) {
Logging.instance
.log("Failed to capture image", level: LogLevel.Error);
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
continue;
}
base64Image = base64Encode(img.encodePng(image));
}
if (base64Image == null || base64Image.isEmpty) {
// Logging.instance
// .log("Failed to capture image", level: LogLevel.Error);
// Spammy.
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
continue;
}
final img.Image? image = img.decodeImage(base64Decode(base64Image));
// TODO [prio=low]: Optimize this process. Docs say:
// > WARNING Since this will check the image data against all known
// > decoders, it is much slower than using an explicit decoder
if (image == null) {
Logging.instance.log("Failed to decode image", level: LogLevel.Error);
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
continue;
}
if (mounted) {
setState(() {
_image = Image.memory(
base64Decode(base64Image!),
fit: BoxFit.cover,
);
});
}
final String? scanResult = await _scanImage(image);
if (scanResult != null && scanResult.isNotEmpty) {
widget.onQrCodeDetected(scanResult);
if (mounted) {
Navigator.of(context).pop();
}
break;
} else {
// Logging.instance.log("No QR code found in the image", level: LogLevel.Info);
// if (mounted) {
// widget.onSnackbar("No QR code found in the image.");
// }
// Spammy.
}
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
} catch (e, s) {
// Logging.instance.log("Failed to capture and scan image: $e\n$s", level: LogLevel.Error);
// Spammy.
// if (mounted) {
// widget.onSnackbar(
// "Error capturing or scanning the image. Please try again.");
// }
}
}
}
Future<String?> _scanImage(img.Image image) async {
try {
final LuminanceSource source = RGBLuminanceSource(
image.width,
image.height,
image
.convert(numChannels: 4)
.getBytes(order: img.ChannelOrder.abgr)
.buffer
.asInt32List(),
);
final BinaryBitmap bitmap =
BinaryBitmap(GlobalHistogramBinarizer(source));
final QRCodeReader reader = QRCodeReader();
final qrDecode = reader.decode(bitmap);
if (qrDecode.text.isEmpty) {
return null;
}
return qrDecode.text;
} catch (e, s) {
// Logging.instance.log("Failed to decode QR code: $e\n$s", level: LogLevel.Error);
// Spammy.
return null;
}
}
@override
Widget build(BuildContext context) {
return DesktopDialog(
maxWidth: 696,
maxHeight: 600,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left: 32),
child: Text(
"Scan QR code",
style: STextStyles.desktopH3(context),
),
),
const DesktopDialogCloseButton(),
],
),
Expanded(
child: _isCameraOpen
? _image != null
? _image!
: const Center(
child: CircularProgressIndicator(),
)
: const Center(
child:
CircularProgressIndicator(), // Show progress indicator immediately
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(child: Container()),
// "Select file" button.
SecondaryButton(
buttonHeight: ButtonHeight.l,
label: "Select file",
width: 200,
onPressed: () async {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ["png", "jpg", "jpeg"],
);
if (result == null || result.files.single.path == null) {
await showFloatingFlushBar(
type: FlushBarType.info,
message: "No file selected",
iconAsset: Assets.svg.file,
context: context,
);
return;
}
final filePath = result?.files.single.path!;
if (filePath == null) {
await showFloatingFlushBar(
type: FlushBarType.info,
message: "Error selecting file.",
iconAsset: Assets.svg.file,
context: context,
);
return;
}
try {
final img.Image? image =
img.decodeImage(File(filePath!).readAsBytesSync());
if (image == null) {
await showFloatingFlushBar(
type: FlushBarType.info,
message: "Failed to decode image.",
iconAsset: Assets.svg.file,
context: context,
);
return;
}
final String? scanResult = await _scanImage(image);
if (scanResult != null && scanResult.isNotEmpty) {
widget.onQrCodeDetected(scanResult);
Navigator.of(context).pop();
} else {
await showFloatingFlushBar(
type: FlushBarType.info,
message: "No QR code found in the image.",
iconAsset: Assets.svg.file,
context: context,
);
}
} catch (e, s) {
Logging.instance.log("Failed to decode image: $e\n$s",
level: LogLevel.Error);
await showFloatingFlushBar(
type: FlushBarType.info,
message:
"Error processing the image. Please try again.",
iconAsset: Assets.svg.file,
context: context,
);
}
},
),
const SizedBox(width: 16),
// Close button.
PrimaryButton(
buttonHeight: ButtonHeight.l,
label: "Close",
width: 272.5,
onPressed: () {
_stopCamera();
Navigator.of(context).pop();
},
),
],
),
),
],
),
);
}
}

View file

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -8,6 +10,7 @@ import '../../utilities/logger.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../conditional_parent.dart';
import '../desktop/qr_code_scanner_dialog.dart';
import '../icon_widgets/clipboard_icon.dart';
import '../icon_widgets/qrcode_icon.dart';
import '../icon_widgets/x_icon.dart';
@ -71,6 +74,50 @@ class _FrostStepFieldState extends State<FrostStepField> {
super.initState();
}
Future<void> scanQr() async {
try {
if (Platform.isAndroid || Platform.isIOS) {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75),
);
}
final qrResult = await BarcodeScanner.scan();
widget.controller.text = qrResult.rawContent;
_changed(widget.controller.text);
} else {
// Platform.isLinux, Platform.isWindows, or Platform.isMacOS.
await showDialog(
context: context,
builder: (context) {
return QrCodeScannerDialog(
onQrCodeDetected: (qrCodeData) {
try {
// TODO [prio=low]: Validate QR code data.
widget.controller.text = qrCodeData;
_changed(widget.controller.text);
} catch (e, s) {
Logging.instance.log("Error processing QR code data: $e\n$s",
level: LogLevel.Error);
}
},
);
},
);
}
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
);
}
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
@ -150,27 +197,7 @@ class _FrostStepFieldState extends State<FrostStepField> {
semanticsLabel:
"Scan QR Button. Opens Camera For Scanning QR Code.",
key: _qrKey,
onTap: () async {
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75),
);
}
final qrResult = await BarcodeScanner.scan();
widget.controller.text = qrResult.rawContent;
_changed(widget.controller.text);
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
);
}
},
onTap: scanQr,
child: const QrCodeIcon(),
),
],