qr: parse scan result, slatepack address image

This commit is contained in:
ardocrat
2024-05-04 12:20:35 +03:00
parent 7a79b88e68
commit 0aaebd1ef2
9 changed files with 460 additions and 78 deletions
+53 -30
View File
@@ -13,20 +13,22 @@
// limitations under the License.
use std::sync::{Arc, RwLock};
use std::thread;
use eframe::emath::Align;
use egui::load::SizedTexture;
use egui::{Layout, Pos2, Rect, TextureOptions, Widget};
use grin_wallet_libwallet::SlatepackAddress;
use image::{DynamicImage, EncodableLayout, ImageFormat};
use crate::gui::Colors;
use crate::gui::icons::CAMERA_ROTATE;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::QrScanState;
use crate::gui::views::types::{QrScanResult, QrScanState};
use crate::gui::views::View;
/// Camera scanner content.
/// Camera QR code scanner.
pub struct CameraContent {
// QR code scanning progress and result.
/// QR code scanning progress and result.
qr_scan_state: Arc<RwLock<QrScanState>>
}
@@ -126,7 +128,7 @@ impl CameraContent {
/// Check if image is processing to find QR code.
fn image_processing(&self) -> bool {
let mut r_scan = self.qr_scan_state.read().unwrap();
let r_scan = self.qr_scan_state.read().unwrap();
r_scan.image_processing
}
@@ -142,39 +144,60 @@ impl CameraContent {
w_scan.image_processing = true;
}
// Launch scanner at separate thread.
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
// Prepare image data.
let img = data.to_luma8();
let mut img: rqrr::PreparedImage<image::GrayImage>
= rqrr::PreparedImage::prepare(img);
// Scan and save results.
let grids = img.detect_grids();
for g in grids {
if let Ok((meta, text)) = g.decode() {
println!("12345 ecc: {}, text: {}", meta.ecc_level, text.clone());
if !text.trim().is_empty() {
let mut w_scan = self.qr_scan_state.write().unwrap();
w_scan.qr_scan_result = Some(text);
let data = data.clone();
let qr_scan_state = self.qr_scan_state.clone();
thread::spawn(move || {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
// Prepare image data.
let img = data.to_luma8();
let mut img: rqrr::PreparedImage<image::GrayImage>
= rqrr::PreparedImage::prepare(img);
// Scan and save results.
let grids = img.detect_grids();
for g in grids {
if let Ok((_, text)) = g.decode() {
let text = text.trim();
if !text.is_empty() {
let result = Self::parse_scan_result(text);
let mut w_scan = qr_scan_state.write().unwrap();
w_scan.qr_scan_result = Some(result);
}
}
}
}
// Setup scanning flag.
{
let mut w_scan = self.qr_scan_state.write().unwrap();
w_scan.image_processing = false;
}
});
// Setup scanning flag.
{
let mut w_scan = qr_scan_state.write().unwrap();
w_scan.image_processing = false;
}
});
});
}
fn parse_scan_result(text: &str) -> QrScanResult {
// Check if string starts with Grin address prefix.
if text.starts_with("tgrin") || text.starts_with("grin") {
if SlatepackAddress::try_from(text).is_ok() {
return QrScanResult::Address(text.to_string())
}
}
// Check if string contains Slatepack message prefix and postfix.
if text.starts_with("BEGINSLATEPACK.") && text.ends_with("ENDSLATEPACK.") {
return QrScanResult::Slatepack(text.to_string())
}
QrScanResult::Text(text.to_string())
}
/// Get QR code scan result.
pub fn qr_scan_result(&self) -> Option<String> {
pub fn qr_scan_result(&self) -> Option<QrScanResult> {
let r_scan = self.qr_scan_state.read().unwrap();
if r_scan.qr_scan_result.is_some() {
return Some(r_scan.qr_scan_result.as_ref().unwrap().clone());
return Some(r_scan.qr_scan_result.clone().unwrap());
}
None
}
+4 -1
View File
@@ -33,4 +33,7 @@ mod wallets;
pub use wallets::*;
mod camera;
pub use camera::*;
pub use camera::*;
mod qr;
pub use qr::*;
+132
View File
@@ -0,0 +1,132 @@
// Copyright 2023 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::{Arc, RwLock};
use std::thread;
use egui::{SizeHint, TextureHandle, TextureOptions};
use egui::load::SizedTexture;
use egui_extras::image::{load_svg_bytes, load_svg_bytes_with_size};
use crate::gui::views::types::QrCreationState;
use crate::gui::views::View;
/// QR code image from text.
pub struct QrCodeContent {
/// Text to create QR code.
pub(crate) text: String,
/// Texture handle to show image when created.
texture_handle: Option<TextureHandle>,
/// QR code image creation progress and result.
qr_creation_state: Arc<RwLock<QrCreationState>>
}
impl QrCodeContent {
pub fn new(text: String) -> Self {
Self {
text,
texture_handle: None,
qr_creation_state: Arc::new(RwLock::new(QrCreationState::default())),
}
}
/// Draw QR code.
pub fn ui(&mut self, ui: &mut egui::Ui, text: String) {
// Get saved QR code image or load new one.
if !self.has_image() {
ui.add_space(38.0);
View::small_loading_spinner(ui);
ui.add_space(38.0);
// Create image from text if not loading.
self.create_image(text);
} else {
// Create image from SVG data.
let r_create = self.qr_creation_state.read().unwrap();
let svg = r_create.svg.as_ref().unwrap();
let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32);
let color_img = load_svg_bytes_with_size(svg, Some(size)).unwrap();
// Create image texture.
let texture_handle = ui.ctx().load_texture("qr_code",
color_img.clone(),
TextureOptions::default());
self.texture_handle = Some(texture_handle.clone());
let img_size = egui::emath::vec2(color_img.width() as f32,
color_img.height() as f32);
let sized_img = SizedTexture::new(texture_handle.id(), img_size);
// Add image to content.
ui.add(egui::Image::from_texture(sized_img)
.max_height(ui.available_width())
.fit_to_original_size(1.0));
}
}
/// Check if image is creating.
fn creating(&self) -> bool {
let r_create = self.qr_creation_state.read().unwrap();
r_create.creating
}
/// Check if image was created.
fn has_image(&self) -> bool {
let r_create = self.qr_creation_state.read().unwrap();
r_create.svg.is_some()
}
/// Create QR code image at separate thread.
fn create_image(&self, text: String) {
let qr_creation_state = self.qr_creation_state.clone();
if !self.creating() {
thread::spawn(move || {
let qr = qrcodegen::QrCode::encode_text(text.as_str(),
qrcodegen::QrCodeEcc::Medium).unwrap();
let svg = Self::qr_to_svg(qr, 0);
let mut w_create = qr_creation_state.write().unwrap();
w_create.creating = false;
w_create.svg = Some(svg.into_bytes());
});
}
}
/// Convert QR code to SVG string.
fn qr_to_svg(qr: qrcodegen::QrCode, border: i32) -> String {
let mut result = String::new();
let dimension = qr.size().checked_add(border.checked_mul(2).unwrap()).unwrap();
result += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
result += "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n";
result += &format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {0} {0}\" stroke=\"none\">\n", dimension);
result += "\t<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\n";
result += "\t<path d=\"";
for y in 0 .. qr.size() {
for x in 0 .. qr.size() {
if qr.get_module(x, y) {
if x != 0 || y != 0 {
result += " ";
}
result += &format!("M{},{}h1v1h-1z", x + border, y + border);
}
}
}
result += "\" fill=\"#000000\"/>\n";
result += "</svg>\n";
result
}
/// Reset QR code image content state to default.
pub fn clear_state(&mut self) {
let mut w_create = self.qr_creation_state.write().unwrap();
*w_create = QrCreationState::default();
}
}
+17
View File
@@ -166,4 +166,21 @@ impl Default for QrScanState {
qr_scan_result: None,
}
}
}
/// QR code image creation state.
pub struct QrCreationState {
// Flag to check if QR code image is creating.
pub creating: bool,
// Found QR code content.
pub svg: Option<Vec<u8>>
}
impl Default for QrCreationState {
fn default() -> Self {
Self {
creating: false,
svg: None,
}
}
}
+31 -8
View File
@@ -22,7 +22,7 @@ use crate::gui::Colors;
use crate::gui::icons::{BRIDGE, CHAT_CIRCLE_TEXT, CHECK, CHECK_FAT, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, PATH, POWER, REPEAT, SCAN, USERS_THREE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, Root, View};
use crate::gui::views::types::{ModalPosition, TextEditOptions};
use crate::gui::views::types::{ModalPosition, QrScanResult, TextEditOptions};
use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport, WalletSettings};
use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType};
use crate::node::Node;
@@ -44,7 +44,7 @@ pub struct WalletContent {
/// Camera content for QR scan [`Modal`].
camera_content: CameraContent,
/// QR code scan result
qr_scan_result: Option<String>,
qr_scan_result: Option<QrScanResult>,
/// Current tab content to show.
pub current_tab: Box<dyn WalletTab>
@@ -367,15 +367,37 @@ impl WalletContent {
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
if let Some(result) = &self.qr_scan_result {
let result_text = match result {
QrScanResult::Slatepack(t) => t,
QrScanResult::Address(t) => t,
QrScanResult::Text(t) => t
};
ui.vertical_centered(|ui| {
ui.label(RichText::new(result).size(16.0).color(Colors::INACTIVE_TEXT));
ui.label(RichText::new(result_text).size(16.0).color(Colors::INACTIVE_TEXT));
});
} else if let Some(result) = self.camera_content.qr_scan_result() {
///TODO: parse result and show
cb.stop_camera();
self.camera_content.clear_state();
println!("result: {}", result);
self.qr_scan_result = Some(result);
match &result {
QrScanResult::Slatepack(message) => {
let mut messages =
WalletMessages::new(wallet.can_use_dandelion(), Some(message.clone()));
messages.parse_message(wallet);
self.current_tab = Box::new(messages);
modal.close();
}
QrScanResult::Address(receiver) => {
if wallet.get_data().unwrap().info.amount_currently_spendable > 0 {
let addr = wallet.slatepack_address().unwrap();
let mut transport = WalletTransport::new(addr.clone());
transport.show_send_tor_modal(cb, Some(receiver.clone()));
self.current_tab = Box::new(transport);
}
}
QrScanResult::Text(_) => {
self.qr_scan_result = Some(result);
}
}
} else {
self.camera_content.ui(ui, cb);
}
@@ -409,13 +431,14 @@ impl WalletContent {
let is_messages = current_type == WalletTabType::Messages;
View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, || {
self.current_tab = Box::new(
WalletMessages::new(wallet.can_use_dandelion())
WalletMessages::new(wallet.can_use_dandelion(), None)
);
});
});
columns[2].vertical_centered_justified(|ui| {
View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, || {
self.current_tab = Box::new(WalletTransport::default());
let addr = wallet.slatepack_address().unwrap();
self.current_tab = Box::new(WalletTransport::new(addr));
});
});
columns[3].vertical_centered_justified(|ui| {
+16 -17
View File
@@ -77,22 +77,6 @@ pub struct WalletMessages {
/// Identifier for amount input [`Modal`].
const AMOUNT_MODAL: &'static str = "amount_modal";
impl WalletMessages {
pub fn new(dandelion: bool) -> Self {
Self {
send_request: false,
message_edit: "".to_string(),
message_slate: None,
message_error: None,
response_edit: "".to_string(),
dandelion,
amount_edit: "".to_string(),
request_edit: "".to_string(),
request_error: None,
}
}
}
impl WalletTab for WalletMessages {
fn get_type(&self) -> WalletTabType {
WalletTabType::Messages
@@ -140,6 +124,21 @@ impl WalletTab for WalletMessages {
}
impl WalletMessages {
/// Create new content instance, put message into input if provided.
pub fn new(dandelion: bool, message: Option<String>) -> Self {
Self {
send_request: false,
message_edit: message.unwrap_or("".to_string()),
message_slate: None,
message_error: None,
response_edit: "".to_string(),
dandelion,
amount_edit: "".to_string(),
request_edit: "".to_string(),
request_error: None,
}
}
/// Draw manual wallet transaction interaction content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
@@ -452,7 +451,7 @@ impl WalletMessages {
}
/// Parse message input into [`Slate`] updating slate and response input.
fn parse_message(&mut self, wallet: &mut Wallet) {
pub fn parse_message(&mut self, wallet: &mut Wallet) {
self.message_error = None;
if self.message_edit.is_empty() {
return;
+58 -20
View File
@@ -22,7 +22,7 @@ use grin_wallet_libwallet::SlatepackAddress;
use crate::gui::Colors;
use crate::gui::icons::{CHECK_CIRCLE, COMPUTER_TOWER, COPY, DOTS_THREE_CIRCLE, EXPORT, GEAR_SIX, GLOBE_SIMPLE, POWER, QR_CODE, WARNING_CIRCLE, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Root, View};
use crate::gui::views::{Modal, QrCodeContent, Root, View};
use crate::gui::views::types::{ModalPosition, TextEditOptions};
use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::WalletContent;
@@ -46,20 +46,9 @@ pub struct WalletTransport {
address_error: bool,
/// Flag to check if [`Modal`] was just opened to focus on first field.
modal_just_opened: bool,
}
impl Default for WalletTransport {
fn default() -> Self {
Self {
tor_sending: Arc::new(RwLock::new(false)),
tor_send_error: Arc::new(RwLock::new(false)),
tor_success: Arc::new(RwLock::new(false)),
amount_edit: "".to_string(),
address_edit: "".to_string(),
address_error: false,
modal_just_opened: false,
}
}
/// QR code address image [`Modal`] content.
qr_code_content: QrCodeContent,
}
impl WalletTab for WalletTransport {
@@ -114,7 +103,24 @@ const SEND_TOR_MODAL: &'static str = "send_tor_modal";
/// Identifier for [`Modal`] to setup Tor service.
const TOR_SETTINGS_MODAL: &'static str = "tor_settings_modal";
/// Identifier for [`Modal`] to show QR code address image.
const QR_ADDRESS_MODAL: &'static str = "qr_address_modal";
impl WalletTransport {
/// Create new content instance from provided Slatepack address text.
pub fn new(addr: String) -> Self {
Self {
tor_sending: Arc::new(RwLock::new(false)),
tor_send_error: Arc::new(RwLock::new(false)),
tor_success: Arc::new(RwLock::new(false)),
amount_edit: "".to_string(),
address_edit: "".to_string(),
address_error: false,
modal_just_opened: false,
qr_code_content: QrCodeContent::new(addr),
}
}
/// Draw wallet transport content.
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
ui.add_space(3.0);
@@ -146,6 +152,11 @@ impl WalletTransport {
self.tor_settings_modal_ui(ui, wallet, modal);
});
}
QR_ADDRESS_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.qr_address_modal_ui(ui, modal);
});
}
_ => {}
}
}
@@ -186,7 +197,7 @@ impl WalletTransport {
// Draw button to setup Tor transport.
let button_rounding = View::item_rounding(0, 2, true);
View::item_button(ui, button_rounding, GEAR_SIX, None, || {
// Show modal.
// Show Tor settings modal.
Modal::new(TOR_SETTINGS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("transport.tor_settings"))
@@ -273,7 +284,7 @@ impl WalletTransport {
}
/// Draw Tor send content.
fn tor_receive_ui(&self,
fn tor_receive_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
data: &WalletData,
@@ -304,7 +315,12 @@ impl WalletTransport {
View::item_rounding(1, 2, true)
};
View::item_button(ui, button_rounding, QR_CODE, None, || {
//TODO: qr for address
// Show QR code image address modal.
self.qr_code_content.clear_state();
Modal::new(QR_ADDRESS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_mining.address"))
.show();
});
// Show button to enable/disable Tor listener for current wallet.
@@ -338,6 +354,28 @@ impl WalletTransport {
});
}
/// Draw QR code image address [`Modal`] content.
fn qr_address_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) {
ui.add_space(6.0);
// Draw QR code content.
let text = self.qr_code_content.text.clone();
self.qr_code_content.ui(ui, text.clone());
ui.add_space(6.0);
// Show address.
View::ellipsize_text(ui, text, 15.0, Colors::TEXT);
ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::WHITE, || {
self.qr_code_content.clear_state();
modal.close();
});
ui.add_space(6.0);
});
}
/// Draw Tor receive content.
fn tor_send_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Setup layout size.
@@ -355,14 +393,14 @@ impl WalletTransport {
// Draw button to open sending modal.
let send_text = format!("{} {}", EXPORT, t!("wallets.send"));
View::button(ui, send_text, Colors::WHITE, || {
self.show_send_tor_modal(cb);
self.show_send_tor_modal(cb, None);
});
});
});
}
/// Show [`Modal`] to send over Tor.
fn show_send_tor_modal(&mut self, cb: &dyn PlatformCallbacks) {
pub fn show_send_tor_modal(&mut self, cb: &dyn PlatformCallbacks, address: Option<String>) {
// Setup modal values.
let mut w_send_err = self.tor_send_error.write().unwrap();
*w_send_err = false;
@@ -372,7 +410,7 @@ impl WalletTransport {
*w_success = false;
self.modal_just_opened = true;
self.amount_edit = "".to_string();
self.address_edit = "".to_string();
self.address_edit = address.unwrap_or("".to_string());
self.address_error = false;
// Show modal.
Modal::new(SEND_TOR_MODAL)