mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-19 00:54:33 +00:00
add file selection button
still janky, it should show snackbar messages on errors but doesn't. ffs
This commit is contained in:
parent
8bf8a90404
commit
7477922382
1 changed files with 191 additions and 89 deletions
|
@ -10,11 +10,13 @@
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:camera_linux/camera_linux.dart';
|
import 'package:camera_linux/camera_linux.dart';
|
||||||
import 'package:cw_core/monero_transaction_priority.dart';
|
import 'package:cw_core/monero_transaction_priority.dart';
|
||||||
import 'package:decimal/decimal.dart';
|
import 'package:decimal/decimal.dart';
|
||||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
@ -171,7 +173,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _scanQr() async {
|
Future<void> scanWebcam() async {
|
||||||
try {
|
try {
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -180,34 +182,19 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
||||||
walletId: widget.walletId,
|
walletId: widget.walletId,
|
||||||
onQrCodeDetected: (qrCodeData) {
|
onQrCodeDetected: (qrCodeData) {
|
||||||
try {
|
try {
|
||||||
var results = AddressUtils.parseUri(qrCodeData);
|
_processQrCodeData(qrCodeData);
|
||||||
if (results.isNotEmpty && results["scheme"] == coin.uriScheme) {
|
|
||||||
_address = (results["address"] ?? "").trim();
|
|
||||||
sendToController.text = _address!;
|
|
||||||
|
|
||||||
if (results["amount"] != null) {
|
|
||||||
final Amount amount =
|
|
||||||
Decimal.parse(results["amount"]!).toAmount(
|
|
||||||
fractionDigits: coin.fractionDigits,
|
|
||||||
);
|
|
||||||
cryptoAmountController.text =
|
|
||||||
ref.read(pAmountFormatter(coin)).format(
|
|
||||||
amount,
|
|
||||||
withUnitName: false,
|
|
||||||
);
|
|
||||||
ref.read(pSendAmount.notifier).state = amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
_setValidAddressProviders(_address);
|
|
||||||
setState(() {
|
|
||||||
_addressToggleFlag = sendToController.text.isNotEmpty;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("Error processing QR code data: $e\n$s",
|
Logging.instance.log("Error processing QR code data: $e\n$s",
|
||||||
level: LogLevel.Error);
|
level: LogLevel.Error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onSnackbar: (message) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -780,6 +767,35 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _processQrCodeData(String qrCodeData) {
|
||||||
|
try {
|
||||||
|
var results = AddressUtils.parseUri(qrCodeData);
|
||||||
|
if (results.isNotEmpty && results["scheme"] == coin.uriScheme) {
|
||||||
|
_address = (results["address"] ?? "").trim();
|
||||||
|
sendToController.text = _address!;
|
||||||
|
|
||||||
|
if (results["amount"] != null) {
|
||||||
|
final Amount amount = Decimal.parse(results["amount"]!).toAmount(
|
||||||
|
fractionDigits: coin.fractionDigits,
|
||||||
|
);
|
||||||
|
cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format(
|
||||||
|
amount,
|
||||||
|
withUnitName: false,
|
||||||
|
);
|
||||||
|
ref.read(pSendAmount.notifier).state = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setValidAddressProviders(_address);
|
||||||
|
setState(() {
|
||||||
|
_addressToggleFlag = sendToController.text.isNotEmpty;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance
|
||||||
|
.log("Error processing QR code data: $e\n$s", level: LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _setValidAddressProviders(String? address) {
|
void _setValidAddressProviders(String? address) {
|
||||||
if (isPaynymSend) {
|
if (isPaynymSend) {
|
||||||
ref.read(pValidSendToAddress.notifier).state = true;
|
ref.read(pValidSendToAddress.notifier).state = true;
|
||||||
|
@ -1565,7 +1581,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
||||||
key: const Key(
|
key: const Key(
|
||||||
"sendViewScanQrButtonKey",
|
"sendViewScanQrButtonKey",
|
||||||
),
|
),
|
||||||
onTap: _scanQr,
|
onTap: scanWebcam,
|
||||||
child: const QrCodeIcon(),
|
child: const QrCodeIcon(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -1982,10 +1998,12 @@ String formatAddress(String epicAddress) {
|
||||||
class QrCodeScannerDialog extends StatefulWidget {
|
class QrCodeScannerDialog extends StatefulWidget {
|
||||||
final String walletId;
|
final String walletId;
|
||||||
final Function(String) onQrCodeDetected;
|
final Function(String) onQrCodeDetected;
|
||||||
|
final Function(String) onSnackbar;
|
||||||
|
|
||||||
QrCodeScannerDialog({
|
QrCodeScannerDialog({
|
||||||
required this.walletId,
|
required this.walletId,
|
||||||
required this.onQrCodeDetected,
|
required this.onQrCodeDetected,
|
||||||
|
required this.onSnackbar,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -1994,10 +2012,9 @@ class QrCodeScannerDialog extends StatefulWidget {
|
||||||
|
|
||||||
class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
||||||
final _cameraLinuxPlugin = CameraLinux();
|
final _cameraLinuxPlugin = CameraLinux();
|
||||||
late String _base64Image;
|
|
||||||
bool _isCameraOpen = false;
|
bool _isCameraOpen = false;
|
||||||
Image? _image;
|
Image? _image;
|
||||||
String? _qrCodeData;
|
bool _isScanning = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -2016,27 +2033,89 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
||||||
await _cameraLinuxPlugin.initializeCamera();
|
await _cameraLinuxPlugin.initializeCamera();
|
||||||
Logging.instance
|
Logging.instance
|
||||||
.log("Camera initialized successfully", level: LogLevel.Info);
|
.log("Camera initialized successfully", level: LogLevel.Info);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isCameraOpen = true;
|
||||||
|
_isScanning = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
_captureAndScanImage();
|
_captureAndScanImage();
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance
|
||||||
.log("Failed to initialize camera: $e\n$s", level: LogLevel.Error);
|
.log("Failed to initialize camera: $e\n$s", level: LogLevel.Error);
|
||||||
|
if (mounted) {
|
||||||
|
widget.onSnackbar("Failed to initialize camera. Please try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _stopCamera() async {
|
||||||
|
try {
|
||||||
|
_cameraLinuxPlugin.stopCamera();
|
||||||
|
Logging.instance.log("Camera stopped successfully", level: LogLevel.Info);
|
||||||
|
} 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 {
|
Future<void> _captureAndScanImage() async {
|
||||||
while (_qrCodeData == null && mounted) {
|
while (_isCameraOpen && _isScanning) {
|
||||||
try {
|
try {
|
||||||
_base64Image = await _cameraLinuxPlugin.captureImage();
|
final base64Image = await _cameraLinuxPlugin.captureImage();
|
||||||
Logging.instance
|
final img.Image? image = img.decodeImage(base64Decode(base64Image));
|
||||||
.log("Image captured successfully", level: LogLevel.Info);
|
|
||||||
_isCameraOpen = true;
|
|
||||||
|
|
||||||
var image = img.decodeImage(base64Decode(_base64Image));
|
|
||||||
if (image == null) {
|
if (image == null) {
|
||||||
throw Exception("Failed to decode image");
|
Logging.instance.log("Failed to decode image", level: LogLevel.Info);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 250));
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
LuminanceSource source = RGBLuminanceSource(
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 250));
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log("Failed to capture and scan image: $e\n$s",
|
||||||
|
level: LogLevel.Error);
|
||||||
|
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.width,
|
||||||
image.height,
|
image.height,
|
||||||
image
|
image
|
||||||
|
@ -2045,45 +2124,19 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
||||||
.buffer
|
.buffer
|
||||||
.asInt32List(),
|
.asInt32List(),
|
||||||
);
|
);
|
||||||
var bitmap = BinaryBitmap(GlobalHistogramBinarizer(source));
|
final BinaryBitmap bitmap =
|
||||||
_image = Image.memory(Uint8List.fromList(img.encodePng(image)));
|
BinaryBitmap(GlobalHistogramBinarizer(source));
|
||||||
var reader = QRCodeReader();
|
|
||||||
|
|
||||||
try {
|
final QRCodeReader reader = QRCodeReader();
|
||||||
var qrDecode = reader.decode(bitmap);
|
final qrDecode = reader.decode(bitmap);
|
||||||
_qrCodeData = qrDecode.text;
|
if (qrDecode.text.isEmpty) {
|
||||||
Logging.instance.log("QR code decoded successfully: $_qrCodeData",
|
return null;
|
||||||
level: LogLevel.Info);
|
|
||||||
widget.onQrCodeDetected(_qrCodeData!);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
break; // Exit the loop once QR code is detected
|
|
||||||
} catch (e) {
|
|
||||||
Logging.instance
|
|
||||||
.log("Failed to decode QR code: $e", level: LogLevel.Error);
|
|
||||||
setState(() {
|
|
||||||
// Update the dialog with the new image.
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return qrDecode.text;
|
||||||
await Future.delayed(
|
|
||||||
const Duration(milliseconds: 1000)); // Add delay here.
|
|
||||||
} catch (e, s) {
|
|
||||||
Logging.instance.log("Failed to capture and scan image: $e\n$s",
|
|
||||||
level: LogLevel.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _stopCamera() {
|
|
||||||
try {
|
|
||||||
_cameraLinuxPlugin.stopCamera();
|
|
||||||
Logging.instance.log("Camera stopped successfully", level: LogLevel.Info);
|
|
||||||
setState(() {
|
|
||||||
_isCameraOpen = false;
|
|
||||||
});
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance
|
||||||
.log("Failed to stop camera: $e\n$s", level: LogLevel.Error);
|
.log("Failed to decode QR code: $e\n$s", level: LogLevel.Error);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2120,15 +2173,64 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: PrimaryButton(
|
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) {
|
||||||
|
widget.onSnackbar("No file selected.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final filePath = result.files.single.path!;
|
||||||
|
try {
|
||||||
|
final img.Image? image =
|
||||||
|
img.decodeImage(File(filePath).readAsBytesSync());
|
||||||
|
if (image == null) {
|
||||||
|
widget.onSnackbar(
|
||||||
|
"Failed to decode image. Please select a valid image file.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String? scanResult = await _scanImage(image);
|
||||||
|
if (scanResult != null && scanResult.isNotEmpty) {
|
||||||
|
widget.onQrCodeDetected(scanResult);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
} else {
|
||||||
|
widget.onSnackbar("No QR code found in the image.");
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log("Failed to decode image: $e\n$s",
|
||||||
|
level: LogLevel.Error);
|
||||||
|
widget.onSnackbar(
|
||||||
|
"Error processing the image. Please try again.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
// Close button.
|
||||||
|
PrimaryButton(
|
||||||
buttonHeight: ButtonHeight.l,
|
buttonHeight: ButtonHeight.l,
|
||||||
label: "Close",
|
label: "Close",
|
||||||
|
width: 272.5,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_stopCamera();
|
_stopCamera();
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue