From 0aaebd1ef26a6878324fb988977f3de1192a2b70 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 4 May 2024 12:20:35 +0300 Subject: [PATCH] qr: parse scan result, slatepack address image --- Cargo.lock | 146 ++++++++++++++++++++++ Cargo.toml | 5 +- src/gui/views/camera.rs | 83 +++++++----- src/gui/views/mod.rs | 5 +- src/gui/views/qr.rs | 132 +++++++++++++++++++ src/gui/views/types.rs | 17 +++ src/gui/views/wallets/wallet/content.rs | 39 ++++-- src/gui/views/wallets/wallet/messages.rs | 33 +++-- src/gui/views/wallets/wallet/transport.rs | 78 +++++++++--- 9 files changed, 460 insertions(+), 78 deletions(-) create mode 100644 src/gui/views/qr.rs diff --git a/Cargo.lock b/Cargo.lock index 36a561f..806b365 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1000,6 +1000,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.0" @@ -2168,6 +2174,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +[[package]] +name = "data-url" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" + [[package]] name = "der" version = "0.7.9" @@ -2659,6 +2671,7 @@ dependencies = [ "image 0.24.9", "log", "mime_guess2", + "resvg", "serde", ] @@ -3114,6 +3127,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + [[package]] name = "fluent" version = "0.16.0" @@ -3775,6 +3794,7 @@ dependencies = [ "local-ip-address", "log", "openssl-sys", + "qrcodegen", "rand 0.8.5", "rqrr", "rust-i18n", @@ -4847,6 +4867,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "imagesize" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" + [[package]] name = "imgref" version = "1.10.1" @@ -5150,6 +5176,15 @@ dependencies = [ "libc", ] +[[package]] +name = "kurbo" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" +dependencies = [ + "arrayvec 0.7.4", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -6557,6 +6592,12 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project" version = "1.1.5" @@ -6892,6 +6933,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5520fbcd7da152a449261c5a533a1c7fad044e9e8aa9528cfec3f464786c7926" +[[package]] +name = "qrcodegen" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" + [[package]] name = "quick-error" version = "1.2.3" @@ -7221,6 +7268,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rctree" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" + [[package]] name = "rdrand" version = "0.4.0" @@ -7355,6 +7408,20 @@ dependencies = [ "winreg", ] +[[package]] +name = "resvg" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadccb3d99a9efb8e5e00c16fbb732cbe400db2ec7fc004697ee7d97d86cf1f4" +dependencies = [ + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", +] + [[package]] name = "retry-error" version = "0.5.2" @@ -7436,6 +7503,12 @@ dependencies = [ "libc", ] +[[package]] +name = "roxmltree" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" + [[package]] name = "rqrr" version = "0.7.1" @@ -8176,6 +8249,15 @@ dependencies = [ "time", ] +[[package]] +name = "simplecss" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" +dependencies = [ + "log", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -8368,6 +8450,9 @@ name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] [[package]] name = "strsim" @@ -8461,6 +8546,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "171758edb47aa306a78dfa4ab9aeb5167405bd4e3dc2b64e88f6a84bbe98bd63" +[[package]] +name = "svgtypes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e44e288cd960318917cbd540340968b90becc8bc81f171345d706e7a89d9d70" +dependencies = [ + "kurbo", + "siphasher", +] + [[package]] name = "syn" version = "0.15.44" @@ -8735,6 +8830,7 @@ dependencies = [ "bytemuck", "cfg-if 1.0.0", "log", + "png", "tiny-skia-path", ] @@ -10264,6 +10360,50 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "usvg" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b0a51b72ab80ca511d126b77feeeb4fb1e972764653e61feac30adc161a756" +dependencies = [ + "base64 0.21.7", + "log", + "pico-args", + "usvg-parser", + "usvg-tree", + "xmlwriter", +] + +[[package]] +name = "usvg-parser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd4e3c291f45d152929a31f0f6c819245e2921bfd01e7bd91201a9af39a2bdc" +dependencies = [ + "data-url", + "flate2", + "imagesize", + "kurbo", + "log", + "roxmltree", + "simplecss", + "siphasher", + "svgtypes", + "usvg-tree", +] + +[[package]] +name = "usvg-tree" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee3d202ebdb97a6215604b8f5b4d6ef9024efd623cf2e373a6416ba976ec7d3" +dependencies = [ + "rctree", + "strict-num", + "svgtypes", + "tiny-skia-path", +] + [[package]] name = "utf8parse" version = "0.2.1" @@ -11336,6 +11476,12 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + [[package]] name = "xxhash-rust" version = "0.8.10" diff --git a/Cargo.toml b/Cargo.toml index 1e5bd94..2b7088d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ grin_wallet_controller = "5.3.0" ## ui egui = { version = "0.27.2", default-features = false } -egui_extras = { version = "0.27.2", features = ["image"] } +egui_extras = { version = "0.27.2", features = ["image", "svg"] } rust-i18n = "2.3.1" ## other @@ -53,8 +53,9 @@ rand = "0.8.5" serde_derive = "1.0.197" serde_json = "1.0.115" tokio = { version = "1.37.0", features = ["full"] } -image = { version = "0.25.1" } +image = "0.25.1" rqrr = "0.7.1" +qrcodegen = "1.8.0" ## tor arti = { version = "1.2.0", features = ["pt-client", "static"] } diff --git a/src/gui/views/camera.rs b/src/gui/views/camera.rs index 4dd29e2..cac5545 100644 --- a/src/gui/views/camera.rs +++ b/src/gui/views/camera.rs @@ -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> } @@ -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 - = 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 + = 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 { + pub fn qr_scan_result(&self) -> Option { 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 } diff --git a/src/gui/views/mod.rs b/src/gui/views/mod.rs index d5c2153..f2ecf92 100644 --- a/src/gui/views/mod.rs +++ b/src/gui/views/mod.rs @@ -33,4 +33,7 @@ mod wallets; pub use wallets::*; mod camera; -pub use camera::*; \ No newline at end of file +pub use camera::*; + +mod qr; +pub use qr::*; \ No newline at end of file diff --git a/src/gui/views/qr.rs b/src/gui/views/qr.rs new file mode 100644 index 0000000..b601e79 --- /dev/null +++ b/src/gui/views/qr.rs @@ -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, + /// QR code image creation progress and result. + qr_creation_state: Arc> +} + +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 += "\n"; + result += "\n"; + result += &format!( + "\n", dimension); + result += "\t\n"; + result += "\t\n"; + result += "\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(); + } +} \ No newline at end of file diff --git a/src/gui/views/types.rs b/src/gui/views/types.rs index fd20f7b..b356a8d 100644 --- a/src/gui/views/types.rs +++ b/src/gui/views/types.rs @@ -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> +} + +impl Default for QrCreationState { + fn default() -> Self { + Self { + creating: false, + svg: None, + } + } } \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 5452eff..cf884d6 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -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, + qr_scan_result: Option, /// Current tab content to show. pub current_tab: Box @@ -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| { diff --git a/src/gui/views/wallets/wallet/messages.rs b/src/gui/views/wallets/wallet/messages.rs index b5c39b9..dd47053 100644 --- a/src/gui/views/wallets/wallet/messages.rs +++ b/src/gui/views/wallets/wallet/messages.rs @@ -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) -> 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; diff --git a/src/gui/views/wallets/wallet/transport.rs b/src/gui/views/wallets/wallet/transport.rs index c06341f..e9fc5c0 100644 --- a/src/gui/views/wallets/wallet/transport.rs +++ b/src/gui/views/wallets/wallet/transport.rs @@ -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) { // 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)