From 00fd12cd2ad3b4375d2ac647040bc39aaf3d789c Mon Sep 17 00:00:00 2001 From: ardocrat Date: Mon, 27 May 2024 16:53:18 +0300 Subject: [PATCH] qr: export to gif and png --- Cargo.lock | 277 ++++++++++++++- Cargo.toml | 2 + locales/en.yml | 1 + locales/ru.yml | 1 + locales/tr.yml | 1 + src/gui/platform/android/mod.rs | 4 + src/gui/platform/desktop/mod.rs | 16 + src/gui/platform/mod.rs | 1 + src/gui/views/qr.rs | 390 ++++++++++++++++------ src/gui/views/types.rs | 35 +- src/gui/views/wallets/wallet/content.rs | 2 + src/gui/views/wallets/wallet/messages.rs | 18 +- src/gui/views/wallets/wallet/transport.rs | 14 +- src/gui/views/wallets/wallet/txs.rs | 18 +- 14 files changed, 614 insertions(+), 166 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30930d8..88c91b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,7 +63,7 @@ dependencies = [ "futures-lite 1.13.0", "once_cell", "serde", - "zbus", + "zbus 3.15.2", ] [[package]] @@ -540,6 +540,24 @@ dependencies = [ "libloading 0.7.4", ] +[[package]] +name = "ashpd" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093" +dependencies = [ + "async-fs 2.1.2", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.8.5", + "serde", + "serde_repr", + "url", + "zbus 4.2.2", +] + [[package]] name = "async-broadcast" version = "0.5.1" @@ -550,6 +568,18 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-broadcast" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb" +dependencies = [ + "event-listener 5.3.0", + "event-listener-strategy 0.5.2", + "futures-core", + "pin-project-lite 0.2.14", +] + [[package]] name = "async-channel" version = "2.3.1" @@ -603,6 +633,17 @@ dependencies = [ "futures-lite 1.13.0", ] +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock 3.3.0", + "blocking", + "futures-lite 2.3.0", +] + [[package]] name = "async-io" version = "1.13.0" @@ -674,6 +715,17 @@ dependencies = [ "url", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io 2.3.2", + "blocking", + "futures-lite 2.3.0", +] + [[package]] name = "async-once-cell" version = "0.5.3" @@ -697,6 +749,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "async-process" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a53fc6301894e04a92cb2584fedde80cb25ba8e02d9dc39d4a87d036e22f397d" +dependencies = [ + "async-channel", + "async-io 2.3.2", + "async-lock 3.3.0", + "async-signal", + "async-task", + "blocking", + "cfg-if 1.0.0", + "event-listener 5.3.0", + "futures-lite 2.3.0", + "rustix 0.38.34", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "async-recursion" version = "1.1.1" @@ -812,9 +884,9 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zbus", - "zbus_names", - "zvariant", + "zbus 3.15.2", + "zbus_names 2.6.1", + "zvariant 3.15.2", ] [[package]] @@ -826,7 +898,7 @@ dependencies = [ "atspi-common", "atspi-proxies", "futures-lite 1.13.0", - "zbus", + "zbus 3.15.2", ] [[package]] @@ -837,7 +909,7 @@ checksum = "6495661273703e7a229356dcbe8c8f38223d697aacfaf0e13590a9ac9977bb52" dependencies = [ "atspi-common", "serde", - "zbus", + "zbus 3.15.2", ] [[package]] @@ -2624,6 +2696,12 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + [[package]] name = "enum-map" version = "2.7.3" @@ -3618,6 +3696,7 @@ dependencies = [ "env_logger 0.11.3", "fs-mistrust", "futures 0.3.30", + "gif", "grin_api", "grin_chain", "grin_config", @@ -3643,6 +3722,7 @@ dependencies = [ "parking_lot 0.12.3", "qrcodegen", "rand 0.8.5", + "rfd", "rqrr", "rust-i18n", "serde", @@ -5678,6 +5758,19 @@ dependencies = [ "memoffset 0.7.1", ] +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.5.0", + "cfg-if 1.0.0", + "cfg_aliases", + "libc", + "memoffset 0.9.1", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -5979,6 +6072,17 @@ dependencies = [ "objc_exception", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc-sys" version = "0.2.0-beta.2" @@ -6129,6 +6233,15 @@ dependencies = [ "cc", ] +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "object" version = "0.32.2" @@ -7327,6 +7440,29 @@ dependencies = [ "subtle", ] +[[package]] +name = "rfd" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a73a7337fc24366edfca76ec521f51877b114e42dab584008209cca6719251" +dependencies = [ + "ashpd", + "block", + "dispatch", + "js-sys", + "log", + "objc", + "objc-foundation", + "objc_id", + "pollster", + "raw-window-handle 0.6.2", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.48.0", +] + [[package]] name = "rgb" version = "0.8.37" @@ -10168,8 +10304,15 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "usvg" version = "0.37.0" @@ -11281,12 +11424,12 @@ version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" dependencies = [ - "async-broadcast", + "async-broadcast 0.5.1", "async-executor", - "async-fs", + "async-fs 1.6.0", "async-io 1.13.0", "async-lock 2.8.0", - "async-process", + "async-process 1.8.1", "async-recursion", "async-task", "async-trait", @@ -11299,7 +11442,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix", + "nix 0.26.4", "once_cell", "ordered-stream", "rand 0.8.5", @@ -11311,9 +11454,47 @@ dependencies = [ "uds_windows", "winapi 0.3.9", "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 3.15.2", + "zbus_names 2.6.1", + "zvariant 3.15.2", +] + +[[package]] +name = "zbus" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989c3977a7aafa97b12b9a35d21cdcff9b0d2289762b14683f45d66b1ba6c48f" +dependencies = [ + "async-broadcast 0.7.0", + "async-executor", + "async-fs 2.1.2", + "async-io 2.3.2", + "async-lock 3.3.0", + "async-process 2.2.2", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener 5.3.0", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.28.0", + "ordered-stream", + "rand 0.8.5", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros 4.2.2", + "zbus_names 3.0.0", + "zvariant 4.1.1", ] [[package]] @@ -11327,7 +11508,20 @@ dependencies = [ "quote 1.0.36", "regex", "syn 1.0.109", - "zvariant_utils", + "zvariant_utils 1.0.1", +] + +[[package]] +name = "zbus_macros" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe9de53245dcf426b7be226a4217dd5e339080e5d46e64a02d6e5dcbf90fca1" +dependencies = [ + "proc-macro-crate 3.1.0", + "proc-macro2 1.0.84", + "quote 1.0.36", + "syn 2.0.66", + "zvariant_utils 2.0.0", ] [[package]] @@ -11338,7 +11532,18 @@ checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" dependencies = [ "serde", "static_assertions", - "zvariant", + "zvariant 3.15.2", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant 4.1.1", ] [[package]] @@ -11455,7 +11660,21 @@ dependencies = [ "libc", "serde", "static_assertions", - "zvariant_derive", + "zvariant_derive 3.15.2", +] + +[[package]] +name = "zvariant" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa6d31a02fbfb602bfde791de7fedeb9c2c18115b3d00f3a36e489f46ffbbc7" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "zvariant_derive 4.1.1", ] [[package]] @@ -11468,7 +11687,20 @@ dependencies = [ "proc-macro2 1.0.84", "quote 1.0.36", "syn 1.0.109", - "zvariant_utils", + "zvariant_utils 1.0.1", +] + +[[package]] +name = "zvariant_derive" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "642bf1b6b6d527988b3e8193d20969d53700a36eac734d21ae6639db168701c8" +dependencies = [ + "proc-macro-crate 3.1.0", + "proc-macro2 1.0.84", + "quote 1.0.36", + "syn 2.0.66", + "zvariant_utils 2.0.0", ] [[package]] @@ -11481,3 +11713,14 @@ dependencies = [ "quote 1.0.36", "syn 1.0.109", ] + +[[package]] +name = "zvariant_utils" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc242db087efc22bd9ade7aa7809e4ba828132edc312871584a6b4391bdf8786" +dependencies = [ + "proc-macro2 1.0.84", + "quote 1.0.36", + "syn 2.0.66", +] diff --git a/Cargo.toml b/Cargo.toml index d2a543b..e8c3c5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ image = "0.25.1" rqrr = "0.7.1" qrcodegen = "1.8.0" ur = "0.4.1" +gif = "0.13.1" ## tor arti-client = { version = "0.18.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] } @@ -92,6 +93,7 @@ env_logger = "0.11.3" winit = { version = "0.29.15" } eframe = { version = "0.27.2", features = ["wgpu"] } arboard = "3.2.0" +rfd = "0.14.1" # camera nokhwa = { git = "https://github.com/l1npengtul/nokhwa", branch = "0.10", features = ["input-native", "output-threaded"] } diff --git a/locales/en.yml b/locales/en.yml index 976e601..8670fe7 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -21,6 +21,7 @@ scan_qr: Scan QR code repeat: Repeat scan_result: Scan result back: Back +share: Share wallets: await_conf_amount: Awaiting confirmation await_fin_amount: Awaiting finalization diff --git a/locales/ru.yml b/locales/ru.yml index 09ad7ec..68de74d 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -21,6 +21,7 @@ scan_qr: Сканирование QR-кода repeat: Повторить scan_result: Результат сканирования back: Назад +share: Поделиться wallets: await_conf_amount: Ожидает подтверждения await_fin_amount: Ожидает завершения diff --git a/locales/tr.yml b/locales/tr.yml index 55f28e8..00cfbcc 100644 --- a/locales/tr.yml +++ b/locales/tr.yml @@ -21,6 +21,7 @@ scan_qr: QR kod tara repeat: Tekrar scan_result: Tarama sonucu back: Geri +share: Paylasmak wallets: await_conf_amount: Onay bekleniyor await_fin_amount: Tamamlanma bekleniyor diff --git a/src/gui/platform/android/mod.rs b/src/gui/platform/android/mod.rs index 7907418..0159302 100644 --- a/src/gui/platform/android/mod.rs +++ b/src/gui/platform/android/mod.rs @@ -114,6 +114,10 @@ impl PlatformCallbacks for Android { fn switch_camera(&self) { self.call_java_method("switchCamera", "()V", &[]).unwrap(); } + + fn share_data(&self, name: String, data: Vec) -> Result<(), std::io::Error> { + Ok(()) + } } lazy_static! { diff --git a/src/gui/platform/desktop/mod.rs b/src/gui/platform/desktop/mod.rs index ecf4e4d..f5dc3df 100644 --- a/src/gui/platform/desktop/mod.rs +++ b/src/gui/platform/desktop/mod.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fs::File; +use std::io:: Write; use lazy_static::lazy_static; use parking_lot::RwLock; use std::sync::atomic::{AtomicBool, Ordering}; @@ -20,6 +22,7 @@ use std::thread; use nokhwa::Camera; use nokhwa::pixel_format::RgbFormat; use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType}; +use rfd::FileDialog; use crate::gui::platform::PlatformCallbacks; @@ -124,6 +127,19 @@ impl PlatformCallbacks for Desktop { fn switch_camera(&self) { return; } + + fn share_data(&self, name: String, data: Vec) -> Result<(), std::io::Error> { + let folder = FileDialog::new() + .set_directory(dirs::home_dir().unwrap()) + .set_file_name(name.clone()) + .save_file(); + if let Some(folder) = folder { + let mut image = File::create(folder)?; + image.write_all(data.as_slice())?; + image.sync_all()?; + } + Ok(()) + } } lazy_static! { diff --git a/src/gui/platform/mod.rs b/src/gui/platform/mod.rs index 253fb50..d8bf62a 100644 --- a/src/gui/platform/mod.rs +++ b/src/gui/platform/mod.rs @@ -31,4 +31,5 @@ pub trait PlatformCallbacks { fn camera_image(&self) -> Option<(Vec, u32)>; fn can_switch_camera(&self) -> bool; fn switch_camera(&self); + fn share_data(&self, name: String, data: Vec) -> Result<(), std::io::Error>; } \ No newline at end of file diff --git a/src/gui/views/qr.rs b/src/gui/views/qr.rs index 7d85adf..f587192 100644 --- a/src/gui/views/qr.rs +++ b/src/gui/views/qr.rs @@ -12,15 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fs::{File, read}; +use std::io::Cursor; +use std::mem::size_of; use std::sync::Arc; use parking_lot::RwLock; use std::thread; use egui::{SizeHint, TextureHandle, TextureOptions}; use egui::load::SizedTexture; use egui_extras::image::load_svg_bytes_with_size; +use image::codecs::jpeg::JpegEncoder; +use image::{ColorType, EncodableLayout, ExtendedColorType, ImageBuffer, ImageEncoder, Rgb, RgbImage}; +use image::codecs::png::{CompressionType, FilterType, PngDecoder, PngEncoder}; use qrcodegen::QrCode; +use crate::gui::Colors; +use crate::gui::icons::IMAGES_SQUARE; +use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::types::QrCreationState; +use crate::gui::views::types::QrImageState; use crate::gui::views::View; /// QR code image from text. @@ -38,10 +47,12 @@ pub struct QrCodeContent { /// Texture handle to show image when created. texture_handle: Option, - /// QR code image creation progress and result. - qr_creation_state: Arc>, + /// QR code view data state. + qr_image_state: Arc>, } +const DEFAULT_QR_SIZE: u32 = 380; + impl QrCodeContent { pub fn new(text: String, animated: bool) -> Self { Self { @@ -50,108 +61,199 @@ impl QrCodeContent { animated_index: None, animation_time: None, texture_handle: None, - qr_creation_state: Arc::new(RwLock::new(QrCreationState::default())), + qr_image_state: Arc::new(RwLock::new(QrImageState::default())), } } /// Draw QR code. - pub fn ui(&mut self, ui: &mut egui::Ui, text: String) { + pub fn ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { if self.animated { - // Create animated QR code image if not created. - if !self.has_image() { - let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; - ui.vertical_centered(|ui| { - ui.add_space(space); - View::big_loading_spinner(ui); - ui.add_space(space); - }); - - // Create multiple vector images from text if not creating. - if !self.creating() { - self.create_svg_list(text); - } - } else { - let svg_list = { - let r_create = self.qr_creation_state.read(); - r_create.svg_list.clone().unwrap() - }; - - // Setup animated index. - let now = chrono::Utc::now().timestamp_millis(); - if now - *self.animation_time.get_or_insert(now) > 100 { - if let Some(i) = self.animated_index { - self.animated_index = Some(i + 1); - } - if *self.animated_index.get_or_insert(0) == svg_list.len() { - self.animated_index = Some(0); - } - self.animation_time = Some(now); - } - - let svg = svg_list[self.animated_index.unwrap_or(0)].clone(); - - // Create images from SVG data. - let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32); - let color_img = load_svg_bytes_with_size(svg.as_slice(), 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)); - ui.ctx().request_repaint(); - } + // Show animated QR code. + self.animated_ui(ui, text, cb); } else { - // Create vector QR code image if not created. - if !self.has_image() { - let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; - ui.vertical_centered(|ui| { - ui.add_space(space); - View::big_loading_spinner(ui); - ui.add_space(space); - }); - - // Create vector image from text if not creating. - if !self.creating() { - self.create_svg(text); - } - } else { - // Create image from SVG data. - let r_create = self.qr_creation_state.read(); - 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)); - } + // Show static QR code. + self.static_ui(ui, text, cb); } } - /// Check if QR code is creating. - fn creating(&self) -> bool { - let r_create = self.qr_creation_state.read(); - r_create.creating + /// Draw animated QR code content. + fn animated_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { + if !self.has_image() { + let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; + ui.vertical_centered(|ui| { + ui.add_space(space); + View::big_loading_spinner(ui); + ui.add_space(space); + }); + + // Create multiple vector images from text if not creating. + if !self.loading() { + self.create_svg_list(text); + } + } else { + let svg_list = { + let r_create = self.qr_image_state.read(); + r_create.svg_list.clone().unwrap() + }; + + // Setup animated index. + let now = chrono::Utc::now().timestamp_millis(); + if now - *self.animation_time.get_or_insert(now) > 100 { + if let Some(i) = self.animated_index { + self.animated_index = Some(i + 1); + } + if *self.animated_index.get_or_insert(0) == svg_list.len() { + self.animated_index = Some(0); + } + self.animation_time = Some(now); + } + + let svg = svg_list[self.animated_index.unwrap_or(0)].clone(); + + // Create images from SVG data. + let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32); + let color_img = load_svg_bytes_with_size(svg.as_slice(), 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)); + ui.add_space(6.0); + + // Show QR code text. + View::ellipsize_text(ui, text.clone(), 16.0, Colors::INACTIVE_TEXT); + ui.add_space(6.0); + + ui.vertical_centered(|ui| { + let sharing = { + let r_state = self.qr_image_state.read(); + r_state.exporting || r_state.gif_creating + }; + if !sharing { + // Show button to share QR. + let share_text = format!("{} {}", IMAGES_SQUARE, t!("share")); + View::button(ui, share_text, Colors::GOLD, || { + { + let mut w_state = self.qr_image_state.write(); + w_state.exporting = true; + } + // Create GIF to export. + self.create_qr_gif(text, DEFAULT_QR_SIZE as usize); + }); + ui.add_space(2.0); + } else { + ui.vertical_centered(|ui| { + ui.add_space(6.0); + View::small_loading_spinner(ui); + ui.add_space(10.0); + }); + } + + // Check if GIF was created to share. + let has_gif = { + let r_state = self.qr_image_state.read(); + r_state.gif_data.is_some() + }; + if has_gif { + let data = { + let r_state = self.qr_image_state.read(); + r_state.gif_data.clone().unwrap() + }; + let name = format!("{}.gif", chrono::Utc::now().timestamp()); + cb.share_data(name, data).unwrap_or_default(); + // Clear GIF data and exporting flag. + { + let mut w_state = self.qr_image_state.write(); + w_state.gif_data = None; + w_state.exporting = false; + } + } + }); + + ui.ctx().request_repaint(); + } + } + + /// Draw static QR code content. + fn static_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { + if !self.has_image() { + let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; + ui.vertical_centered(|ui| { + ui.add_space(space); + View::big_loading_spinner(ui); + ui.add_space(space); + }); + + // Create vector image from text if not creating. + if !self.loading() { + self.create_svg(text); + } + } else { + // Create image from SVG data. + let svg = { + let r_state = self.qr_image_state.read(); + r_state.svg.clone().unwrap() + }; + let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32); + let color_img = load_svg_bytes_with_size(svg.as_slice(), 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)); + ui.add_space(6.0); + + // Show QR code text. + View::ellipsize_text(ui, text.clone(), 16.0, Colors::INACTIVE_TEXT); + ui.add_space(6.0); + + // Show button to share QR. + ui.vertical_centered(|ui| { + let share_text = format!("{} {}", IMAGES_SQUARE, t!("share")); + View::button(ui, share_text, Colors::GOLD, || { + if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) { + if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) { + let mut png = vec![]; + let png_enc = PngEncoder::new_with_quality(&mut png, + CompressionType::Best, + FilterType::NoFilter); + if let Ok(()) = png_enc.write_image(data.as_slice(), + DEFAULT_QR_SIZE, + DEFAULT_QR_SIZE, + ExtendedColorType::L8) { + let name = format!("{}.png", chrono::Utc::now().timestamp()); + cb.share_data(name, png).unwrap_or_default(); + } + } + } + }); + }); + } + } + + /// Check if QR code is loading. + fn loading(&self) -> bool { + let r_state = self.qr_image_state.read(); + r_state.loading } /// Create multiple vector QR code images at separate thread. fn create_svg_list(&self, text: String) { - let qr_creation_state = self.qr_creation_state.clone(); + let qr_state = self.qr_image_state.clone(); thread::spawn(move || { let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap(); let mut data = Vec::with_capacity(encoder.fragment_count()); @@ -162,29 +264,29 @@ impl QrCodeContent { data.push(svg.into_bytes()); } } - let mut w_create = qr_creation_state.write(); + let mut w_state = qr_state.write(); if !data.is_empty() { - w_create.svg_list = Some(data); + w_state.svg_list = Some(data); } - w_create.creating = false; + w_state.loading = false; }); } /// Check if image was created. fn has_image(&self) -> bool { - let r_create = self.qr_creation_state.read(); - r_create.svg.is_some() || r_create.svg_list.is_some() + let r_state = self.qr_image_state.read(); + r_state.svg.is_some() || r_state.svg_list.is_some() } /// Create vector QR code image at separate thread. fn create_svg(&self, text: String) { - let qr_creation_state = self.qr_creation_state.clone(); + let qr_state = self.qr_image_state.clone(); thread::spawn(move || { if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) { let svg = Self::qr_to_svg(qr, 0); - let mut w_create = qr_creation_state.write(); - w_create.creating = false; - w_create.svg = Some(svg.into_bytes()); + let mut w_state = qr_state.write(); + w_state.loading = false; + w_state.svg = Some(svg.into_bytes()); } }); } @@ -214,9 +316,91 @@ impl QrCodeContent { result } + /// Create GIF image at separate thread. + fn create_qr_gif(&self, text: String, size: usize) { + { + let mut w_state = self.qr_image_state.write(); + w_state.gif_creating = true; + + } + let qr_state = self.qr_image_state.clone(); + thread::spawn(move || { + // Setup GIF image encoder. + let mut gif = vec![]; + if let Ok(mut gif_enc) = gif::Encoder::new(&mut gif, + size as u16, + size as u16, + &[]) { + gif_enc.set_repeat(gif::Repeat::Infinite).unwrap(); + // Generate QR codes from text. + let mut ur_enc = ur::Encoder::bytes(text.as_bytes(), 100).unwrap(); + for _ in 0..ur_enc.fragment_count() { + let ur = ur_enc.next_part().unwrap(); + if let Ok(qr) = QrCode::encode_text(ur.as_str(), qrcodegen::QrCodeEcc::Low) { + // Create an image from QR data and write it to encoder. + if let Some(image) = Self::qr_to_image_data(qr, size) { + let mut frame = gif::Frame::from_indexed_pixels(size as u16, + size as u16, + image.as_slice(), None); + frame.palette = Some(vec![]); + // Write an image to GIF encoder. + if let Ok(_) = gif_enc.write_frame(&frame) { + continue; + } + } + // Exit on error. + let mut w_state = qr_state.write(); + w_state.gif_creating = false; + return; + } + } + } + // Setup GIF image data. + let mut w_state = qr_state.write(); + if !gif.is_empty() { + w_state.gif_data = Some(gif); + } + w_state.gif_creating = false; + }); + } + + /// Convert QR code to image data. + fn qr_to_image_data(qr: QrCode, size: usize) -> Option> { + if size >= 2usize.pow((size_of::() * 4) as u32) { + return None; + } + let margin_size = 1; + let s = qr.size(); + let data_length = s as usize; + let data_length_with_margin = data_length + 2 * margin_size; + let point_size = size / data_length_with_margin; + if point_size == 0 { + return None; + } + let margin = (size - (point_size * data_length)) / 2; + let length = size * size; + let mut img_raw: Vec = vec![255u8; length]; + for i in 0..s { + for j in 0..s { + if qr.get_module(i, j) { + let x = i as usize * point_size + margin; + let y = j as usize * point_size + margin; + + for j in y..(y + point_size) { + let offset = j * size; + for i in x..(x + point_size) { + img_raw[offset + i] = 0; + } + } + } + } + } + Some(img_raw) + } + /// Reset QR code image content state to default. pub fn clear_state(&mut self) { - let mut w_create = self.qr_creation_state.write(); - *w_create = QrCreationState::default(); + let mut w_create = self.qr_image_state.write(); + *w_create = QrImageState::default(); } } \ No newline at end of file diff --git a/src/gui/views/types.rs b/src/gui/views/types.rs index 21f93b0..710b3d5 100644 --- a/src/gui/views/types.rs +++ b/src/gui/views/types.rs @@ -175,11 +175,11 @@ impl QrScanResult { } } -/// QR code scan state. +/// QR code scanning state. pub struct QrScanState { - // Flag to check if image is processing to find QR code. - pub(crate) image_processing: bool, - // Found QR code content. + /// Flag to check if image is processing to find QR code. + pub image_processing: bool, + /// Processed QR code result. pub qr_scan_result: Option } @@ -192,20 +192,31 @@ impl Default for QrScanState { } } -/// QR code image creation state. -pub struct QrCreationState { - // Flag to check if QR code image is creating. - pub creating: bool, - // Vector image data. +/// QR code image data state. +pub struct QrImageState { + /// Flag to check if QR code image is loading. + pub loading: bool, + /// Flag to check if QR code image is exporting. + pub exporting: bool, + + /// Created GIF data from animated QR code. + pub gif_data: Option>, + /// Flag to check if GIF is creating. + pub gif_creating: bool, + + /// Vector image data. pub svg: Option>, - // Multiple vector image data. + /// Multiple vector image data for animated QR code. pub svg_list: Option>> } -impl Default for QrCreationState { +impl Default for QrImageState { fn default() -> Self { Self { - creating: false, + loading: false, + exporting: false, + gif_data: None, + gif_creating: false, svg: None, svg_list: None, } diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 1e5d9d2..6dd9b33 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -403,6 +403,8 @@ impl WalletContent { }); }); ui.add_space(6.0); + View::horizontal_line(ui, Colors::ITEM_STROKE); + ui.add_space(6.0); } else if let Some(result) = self.camera_content.qr_scan_result() { cb.stop_camera(); self.camera_content.clear_state(); diff --git a/src/gui/views/wallets/wallet/messages.rs b/src/gui/views/wallets/wallet/messages.rs index 5f800b8..8b419af 100644 --- a/src/gui/views/wallets/wallet/messages.rs +++ b/src/gui/views/wallets/wallet/messages.rs @@ -224,7 +224,7 @@ impl WalletMessages { } QR_SLATEPACK_MESSAGE_MODAL => { Modal::ui(ui.ctx(), |ui, modal| { - self.qr_message_modal_ui(ui, modal); + self.qr_message_modal_ui(ui, modal, cb); }); } _ => {} @@ -483,11 +483,7 @@ impl WalletMessages { if text.is_empty() { self.request_qr = false; } - self.request_qr_content.ui(ui, text.clone()); - ui.add_space(6.0); - - // Show QR code text. - View::ellipsize_text(ui, text, 16.0, Colors::INACTIVE_TEXT); + self.request_qr_content.ui(ui, text.clone(), cb); ui.add_space(6.0); // Show button to close modal. @@ -853,7 +849,7 @@ impl WalletMessages { } /// Draw QR code Slatepack message image [`Modal`] content. - fn qr_message_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) { + fn qr_message_modal_ui(&mut self, ui: &mut egui::Ui, m: &Modal, cb: &dyn PlatformCallbacks) { ui.add_space(6.0); // Setup title for Slatepack message. @@ -871,11 +867,7 @@ impl WalletMessages { // Draw QR code content. let text = self.qr_message_text.clone().unwrap(); - self.qr_message_content.ui(ui, text.clone()); - ui.add_space(6.0); - - // Show message text. - View::ellipsize_text(ui, text, 16.0, Colors::INACTIVE_TEXT); + self.qr_message_content.ui(ui, text.clone(), cb); ui.add_space(6.0); ui.vertical_centered_justified(|ui| { @@ -884,7 +876,7 @@ impl WalletMessages { self.qr_message_content.clear_state(); self.response_edit.clear(); self.message_slate = None; - modal.close(); + m.close(); }); }); ui.add_space(6.0); diff --git a/src/gui/views/wallets/wallet/transport.rs b/src/gui/views/wallets/wallet/transport.rs index a7b210c..2ad749a 100644 --- a/src/gui/views/wallets/wallet/transport.rs +++ b/src/gui/views/wallets/wallet/transport.rs @@ -187,7 +187,7 @@ impl WalletTransport { } QR_ADDRESS_MODAL => { Modal::ui(ui.ctx(), |ui, modal| { - self.qr_address_modal_ui(ui, modal); + self.qr_address_modal_ui(ui, modal, cb); }); } _ => {} @@ -584,22 +584,18 @@ impl WalletTransport { } /// Draw QR code image address [`Modal`] content. - fn qr_address_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) { + fn qr_address_modal_ui(&mut self, ui: &mut egui::Ui, m: &Modal, cb: &dyn PlatformCallbacks) { ui.add_space(6.0); // Draw QR code content. let text = self.qr_address_content.text.clone(); - self.qr_address_content.ui(ui, text.clone()); - ui.add_space(6.0); - - // Show address. - View::ellipsize_text(ui, text, 16.0, Colors::GRAY); - ui.add_space(6.0); + self.qr_address_content.ui(ui, text.clone(), cb); + ui.add_space(10.0); ui.vertical_centered_justified(|ui| { View::button(ui, t!("close"), Colors::WHITE, || { self.qr_address_content.clear_state(); - modal.close(); + m.close(); }); ui.add_space(6.0); }); diff --git a/src/gui/views/wallets/wallet/txs.rs b/src/gui/views/wallets/wallet/txs.rs index 8514f02..ef1bc12 100644 --- a/src/gui/views/wallets/wallet/txs.rs +++ b/src/gui/views/wallets/wallet/txs.rs @@ -122,7 +122,7 @@ impl WalletTab for WalletTransactions { .show_inside(ui, |ui| { ui.vertical_centered(|ui| { let data = wallet.get_data().unwrap(); - self.txs_ui(ui, wallet, &data, cb); + self.txs_ui(ui, wallet, &data); }); }); } @@ -142,8 +142,7 @@ impl WalletTransactions { fn txs_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, - data: &WalletData, - cb: &dyn PlatformCallbacks) { + data: &WalletData) { let amount_conf = data.info.amount_awaiting_confirmation; let amount_fin = data.info.amount_awaiting_finalization; let amount_locked = data.info.amount_locked; @@ -224,7 +223,7 @@ impl WalletTransactions { // Show transaction item. let tx = data.txs.get(index).unwrap(); let rounding = View::item_rounding(index, data.txs.len(), false); - self.tx_item_ui(ui, tx, rounding, padding, true, &data, wallet, cb); + self.tx_item_ui(ui, tx, rounding, padding, true, &data, wallet); } }); }) @@ -291,8 +290,7 @@ impl WalletTransactions { extra_padding: bool, can_show_info: bool, data: &WalletData, - wallet: &mut Wallet, - cb: &dyn PlatformCallbacks) { + wallet: &mut Wallet) { // Setup layout size. let mut rect = ui.available_rect_before_wrap(); if extra_padding { @@ -541,7 +539,7 @@ impl WalletTransactions { // Show transaction amount status and time. let rounding = View::item_rounding(0, 2, false); - self.tx_item_ui(ui, tx, rounding, false, false, &data, wallet, cb); + self.tx_item_ui(ui, tx, rounding, false, false, &data, wallet); // Show transaction ID info. if let Some(id) = tx.data.tx_slate_id { @@ -773,11 +771,7 @@ impl WalletTransactions { self.tx_info_show_qr = false; } else { // Draw QR code content. - self.tx_info_qr_code_content.ui(ui, text.clone()); - ui.add_space(6.0); - - // Show QR code text. - View::ellipsize_text(ui, text, 16.0, Colors::INACTIVE_TEXT); + self.tx_info_qr_code_content.ui(ui, text.clone(), cb); return; } }