qr: export to gif and png

This commit is contained in:
ardocrat 2024-05-27 16:53:18 +03:00
parent d1d968f165
commit 00fd12cd2a
14 changed files with 614 additions and 166 deletions

277
Cargo.lock generated
View file

@ -63,7 +63,7 @@ dependencies = [
"futures-lite 1.13.0", "futures-lite 1.13.0",
"once_cell", "once_cell",
"serde", "serde",
"zbus", "zbus 3.15.2",
] ]
[[package]] [[package]]
@ -540,6 +540,24 @@ dependencies = [
"libloading 0.7.4", "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]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.5.1" version = "0.5.1"
@ -550,6 +568,18 @@ dependencies = [
"futures-core", "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]] [[package]]
name = "async-channel" name = "async-channel"
version = "2.3.1" version = "2.3.1"
@ -603,6 +633,17 @@ dependencies = [
"futures-lite 1.13.0", "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]] [[package]]
name = "async-io" name = "async-io"
version = "1.13.0" version = "1.13.0"
@ -674,6 +715,17 @@ dependencies = [
"url", "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]] [[package]]
name = "async-once-cell" name = "async-once-cell"
version = "0.5.3" version = "0.5.3"
@ -697,6 +749,26 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "async-recursion" name = "async-recursion"
version = "1.1.1" version = "1.1.1"
@ -812,9 +884,9 @@ dependencies = [
"enumflags2", "enumflags2",
"serde", "serde",
"static_assertions", "static_assertions",
"zbus", "zbus 3.15.2",
"zbus_names", "zbus_names 2.6.1",
"zvariant", "zvariant 3.15.2",
] ]
[[package]] [[package]]
@ -826,7 +898,7 @@ dependencies = [
"atspi-common", "atspi-common",
"atspi-proxies", "atspi-proxies",
"futures-lite 1.13.0", "futures-lite 1.13.0",
"zbus", "zbus 3.15.2",
] ]
[[package]] [[package]]
@ -837,7 +909,7 @@ checksum = "6495661273703e7a229356dcbe8c8f38223d697aacfaf0e13590a9ac9977bb52"
dependencies = [ dependencies = [
"atspi-common", "atspi-common",
"serde", "serde",
"zbus", "zbus 3.15.2",
] ]
[[package]] [[package]]
@ -2624,6 +2696,12 @@ dependencies = [
"cfg-if 1.0.0", "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]] [[package]]
name = "enum-map" name = "enum-map"
version = "2.7.3" version = "2.7.3"
@ -3618,6 +3696,7 @@ dependencies = [
"env_logger 0.11.3", "env_logger 0.11.3",
"fs-mistrust", "fs-mistrust",
"futures 0.3.30", "futures 0.3.30",
"gif",
"grin_api", "grin_api",
"grin_chain", "grin_chain",
"grin_config", "grin_config",
@ -3643,6 +3722,7 @@ dependencies = [
"parking_lot 0.12.3", "parking_lot 0.12.3",
"qrcodegen", "qrcodegen",
"rand 0.8.5", "rand 0.8.5",
"rfd",
"rqrr", "rqrr",
"rust-i18n", "rust-i18n",
"serde", "serde",
@ -5678,6 +5758,19 @@ dependencies = [
"memoffset 0.7.1", "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]] [[package]]
name = "nodrop" name = "nodrop"
version = "0.1.14" version = "0.1.14"
@ -5979,6 +6072,17 @@ dependencies = [
"objc_exception", "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]] [[package]]
name = "objc-sys" name = "objc-sys"
version = "0.2.0-beta.2" version = "0.2.0-beta.2"
@ -6129,6 +6233,15 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.32.2" version = "0.32.2"
@ -7327,6 +7440,29 @@ dependencies = [
"subtle", "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]] [[package]]
name = "rgb" name = "rgb"
version = "0.8.37" version = "0.8.37"
@ -10168,8 +10304,15 @@ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna",
"percent-encoding", "percent-encoding",
"serde",
] ]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]] [[package]]
name = "usvg" name = "usvg"
version = "0.37.0" version = "0.37.0"
@ -11281,12 +11424,12 @@ version = "3.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6"
dependencies = [ dependencies = [
"async-broadcast", "async-broadcast 0.5.1",
"async-executor", "async-executor",
"async-fs", "async-fs 1.6.0",
"async-io 1.13.0", "async-io 1.13.0",
"async-lock 2.8.0", "async-lock 2.8.0",
"async-process", "async-process 1.8.1",
"async-recursion", "async-recursion",
"async-task", "async-task",
"async-trait", "async-trait",
@ -11299,7 +11442,7 @@ dependencies = [
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"hex", "hex",
"nix", "nix 0.26.4",
"once_cell", "once_cell",
"ordered-stream", "ordered-stream",
"rand 0.8.5", "rand 0.8.5",
@ -11311,9 +11454,47 @@ dependencies = [
"uds_windows", "uds_windows",
"winapi 0.3.9", "winapi 0.3.9",
"xdg-home", "xdg-home",
"zbus_macros", "zbus_macros 3.15.2",
"zbus_names", "zbus_names 2.6.1",
"zvariant", "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]] [[package]]
@ -11327,7 +11508,20 @@ dependencies = [
"quote 1.0.36", "quote 1.0.36",
"regex", "regex",
"syn 1.0.109", "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]] [[package]]
@ -11338,7 +11532,18 @@ checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d"
dependencies = [ dependencies = [
"serde", "serde",
"static_assertions", "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]] [[package]]
@ -11455,7 +11660,21 @@ dependencies = [
"libc", "libc",
"serde", "serde",
"static_assertions", "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]] [[package]]
@ -11468,7 +11687,20 @@ dependencies = [
"proc-macro2 1.0.84", "proc-macro2 1.0.84",
"quote 1.0.36", "quote 1.0.36",
"syn 1.0.109", "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]] [[package]]
@ -11481,3 +11713,14 @@ dependencies = [
"quote 1.0.36", "quote 1.0.36",
"syn 1.0.109", "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",
]

View file

@ -58,6 +58,7 @@ image = "0.25.1"
rqrr = "0.7.1" rqrr = "0.7.1"
qrcodegen = "1.8.0" qrcodegen = "1.8.0"
ur = "0.4.1" ur = "0.4.1"
gif = "0.13.1"
## tor ## tor
arti-client = { version = "0.18.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] } 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" } winit = { version = "0.29.15" }
eframe = { version = "0.27.2", features = ["wgpu"] } eframe = { version = "0.27.2", features = ["wgpu"] }
arboard = "3.2.0" arboard = "3.2.0"
rfd = "0.14.1"
# camera # camera
nokhwa = { git = "https://github.com/l1npengtul/nokhwa", branch = "0.10", features = ["input-native", "output-threaded"] } nokhwa = { git = "https://github.com/l1npengtul/nokhwa", branch = "0.10", features = ["input-native", "output-threaded"] }

View file

@ -21,6 +21,7 @@ scan_qr: Scan QR code
repeat: Repeat repeat: Repeat
scan_result: Scan result scan_result: Scan result
back: Back back: Back
share: Share
wallets: wallets:
await_conf_amount: Awaiting confirmation await_conf_amount: Awaiting confirmation
await_fin_amount: Awaiting finalization await_fin_amount: Awaiting finalization

View file

@ -21,6 +21,7 @@ scan_qr: Сканирование QR-кода
repeat: Повторить repeat: Повторить
scan_result: Результат сканирования scan_result: Результат сканирования
back: Назад back: Назад
share: Поделиться
wallets: wallets:
await_conf_amount: Ожидает подтверждения await_conf_amount: Ожидает подтверждения
await_fin_amount: Ожидает завершения await_fin_amount: Ожидает завершения

View file

@ -21,6 +21,7 @@ scan_qr: QR kod tara
repeat: Tekrar repeat: Tekrar
scan_result: Tarama sonucu scan_result: Tarama sonucu
back: Geri back: Geri
share: Paylasmak
wallets: wallets:
await_conf_amount: Onay bekleniyor await_conf_amount: Onay bekleniyor
await_fin_amount: Tamamlanma bekleniyor await_fin_amount: Tamamlanma bekleniyor

View file

@ -114,6 +114,10 @@ impl PlatformCallbacks for Android {
fn switch_camera(&self) { fn switch_camera(&self) {
self.call_java_method("switchCamera", "()V", &[]).unwrap(); self.call_java_method("switchCamera", "()V", &[]).unwrap();
} }
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
Ok(())
}
} }
lazy_static! { lazy_static! {

View file

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::fs::File;
use std::io:: Write;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
@ -20,6 +22,7 @@ use std::thread;
use nokhwa::Camera; use nokhwa::Camera;
use nokhwa::pixel_format::RgbFormat; use nokhwa::pixel_format::RgbFormat;
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType}; use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
use rfd::FileDialog;
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
@ -124,6 +127,19 @@ impl PlatformCallbacks for Desktop {
fn switch_camera(&self) { fn switch_camera(&self) {
return; return;
} }
fn share_data(&self, name: String, data: Vec<u8>) -> 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! { lazy_static! {

View file

@ -31,4 +31,5 @@ pub trait PlatformCallbacks {
fn camera_image(&self) -> Option<(Vec<u8>, u32)>; fn camera_image(&self) -> Option<(Vec<u8>, u32)>;
fn can_switch_camera(&self) -> bool; fn can_switch_camera(&self) -> bool;
fn switch_camera(&self); fn switch_camera(&self);
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
} }

View file

@ -12,15 +12,24 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::fs::{File, read};
use std::io::Cursor;
use std::mem::size_of;
use std::sync::Arc; use std::sync::Arc;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::thread; use std::thread;
use egui::{SizeHint, TextureHandle, TextureOptions}; use egui::{SizeHint, TextureHandle, TextureOptions};
use egui::load::SizedTexture; use egui::load::SizedTexture;
use egui_extras::image::load_svg_bytes_with_size; 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 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; use crate::gui::views::View;
/// QR code image from text. /// QR code image from text.
@ -38,10 +47,12 @@ pub struct QrCodeContent {
/// Texture handle to show image when created. /// Texture handle to show image when created.
texture_handle: Option<TextureHandle>, texture_handle: Option<TextureHandle>,
/// QR code image creation progress and result. /// QR code view data state.
qr_creation_state: Arc<RwLock<QrCreationState>>, qr_image_state: Arc<RwLock<QrImageState>>,
} }
const DEFAULT_QR_SIZE: u32 = 380;
impl QrCodeContent { impl QrCodeContent {
pub fn new(text: String, animated: bool) -> Self { pub fn new(text: String, animated: bool) -> Self {
Self { Self {
@ -50,108 +61,199 @@ impl QrCodeContent {
animated_index: None, animated_index: None,
animation_time: None, animation_time: None,
texture_handle: 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. /// 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 { if self.animated {
// Create animated QR code image if not created. // Show animated QR code.
if !self.has_image() { self.animated_ui(ui, text, cb);
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();
}
} else { } else {
// Create vector QR code image if not created. // Show static QR code.
if !self.has_image() { self.static_ui(ui, text, cb);
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));
}
} }
} }
/// Check if QR code is creating. /// Draw animated QR code content.
fn creating(&self) -> bool { fn animated_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) {
let r_create = self.qr_creation_state.read(); if !self.has_image() {
r_create.creating 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. /// Create multiple vector QR code images at separate thread.
fn create_svg_list(&self, text: String) { 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 || { thread::spawn(move || {
let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap(); let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
let mut data = Vec::with_capacity(encoder.fragment_count()); let mut data = Vec::with_capacity(encoder.fragment_count());
@ -162,29 +264,29 @@ impl QrCodeContent {
data.push(svg.into_bytes()); data.push(svg.into_bytes());
} }
} }
let mut w_create = qr_creation_state.write(); let mut w_state = qr_state.write();
if !data.is_empty() { 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. /// Check if image was created.
fn has_image(&self) -> bool { fn has_image(&self) -> bool {
let r_create = self.qr_creation_state.read(); let r_state = self.qr_image_state.read();
r_create.svg.is_some() || r_create.svg_list.is_some() r_state.svg.is_some() || r_state.svg_list.is_some()
} }
/// Create vector QR code image at separate thread. /// Create vector QR code image at separate thread.
fn create_svg(&self, text: String) { 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 || { thread::spawn(move || {
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) { if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
let svg = Self::qr_to_svg(qr, 0); let svg = Self::qr_to_svg(qr, 0);
let mut w_create = qr_creation_state.write(); let mut w_state = qr_state.write();
w_create.creating = false; w_state.loading = false;
w_create.svg = Some(svg.into_bytes()); w_state.svg = Some(svg.into_bytes());
} }
}); });
} }
@ -214,9 +316,91 @@ impl QrCodeContent {
result 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<Vec<u8>> {
if size >= 2usize.pow((size_of::<usize>() * 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<u8> = 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. /// Reset QR code image content state to default.
pub fn clear_state(&mut self) { pub fn clear_state(&mut self) {
let mut w_create = self.qr_creation_state.write(); let mut w_create = self.qr_image_state.write();
*w_create = QrCreationState::default(); *w_create = QrImageState::default();
} }
} }

View file

@ -175,11 +175,11 @@ impl QrScanResult {
} }
} }
/// QR code scan state. /// QR code scanning state.
pub struct QrScanState { pub struct QrScanState {
// Flag to check if image is processing to find QR code. /// Flag to check if image is processing to find QR code.
pub(crate) image_processing: bool, pub image_processing: bool,
// Found QR code content. /// Processed QR code result.
pub qr_scan_result: Option<QrScanResult> pub qr_scan_result: Option<QrScanResult>
} }
@ -192,20 +192,31 @@ impl Default for QrScanState {
} }
} }
/// QR code image creation state. /// QR code image data state.
pub struct QrCreationState { pub struct QrImageState {
// Flag to check if QR code image is creating. /// Flag to check if QR code image is loading.
pub creating: bool, pub loading: bool,
// Vector image data. /// Flag to check if QR code image is exporting.
pub exporting: bool,
/// Created GIF data from animated QR code.
pub gif_data: Option<Vec<u8>>,
/// Flag to check if GIF is creating.
pub gif_creating: bool,
/// Vector image data.
pub svg: Option<Vec<u8>>, pub svg: Option<Vec<u8>>,
// Multiple vector image data. /// Multiple vector image data for animated QR code.
pub svg_list: Option<Vec<Vec<u8>>> pub svg_list: Option<Vec<Vec<u8>>>
} }
impl Default for QrCreationState { impl Default for QrImageState {
fn default() -> Self { fn default() -> Self {
Self { Self {
creating: false, loading: false,
exporting: false,
gif_data: None,
gif_creating: false,
svg: None, svg: None,
svg_list: None, svg_list: None,
} }

View file

@ -403,6 +403,8 @@ impl WalletContent {
}); });
}); });
ui.add_space(6.0); 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() { } else if let Some(result) = self.camera_content.qr_scan_result() {
cb.stop_camera(); cb.stop_camera();
self.camera_content.clear_state(); self.camera_content.clear_state();

View file

@ -224,7 +224,7 @@ impl WalletMessages {
} }
QR_SLATEPACK_MESSAGE_MODAL => { QR_SLATEPACK_MESSAGE_MODAL => {
Modal::ui(ui.ctx(), |ui, 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() { if text.is_empty() {
self.request_qr = false; self.request_qr = false;
} }
self.request_qr_content.ui(ui, text.clone()); self.request_qr_content.ui(ui, text.clone(), cb);
ui.add_space(6.0);
// Show QR code text.
View::ellipsize_text(ui, text, 16.0, Colors::INACTIVE_TEXT);
ui.add_space(6.0); ui.add_space(6.0);
// Show button to close modal. // Show button to close modal.
@ -853,7 +849,7 @@ impl WalletMessages {
} }
/// Draw QR code Slatepack message image [`Modal`] content. /// 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); ui.add_space(6.0);
// Setup title for Slatepack message. // Setup title for Slatepack message.
@ -871,11 +867,7 @@ impl WalletMessages {
// Draw QR code content. // Draw QR code content.
let text = self.qr_message_text.clone().unwrap(); let text = self.qr_message_text.clone().unwrap();
self.qr_message_content.ui(ui, text.clone()); self.qr_message_content.ui(ui, text.clone(), cb);
ui.add_space(6.0);
// Show message text.
View::ellipsize_text(ui, text, 16.0, Colors::INACTIVE_TEXT);
ui.add_space(6.0); ui.add_space(6.0);
ui.vertical_centered_justified(|ui| { ui.vertical_centered_justified(|ui| {
@ -884,7 +876,7 @@ impl WalletMessages {
self.qr_message_content.clear_state(); self.qr_message_content.clear_state();
self.response_edit.clear(); self.response_edit.clear();
self.message_slate = None; self.message_slate = None;
modal.close(); m.close();
}); });
}); });
ui.add_space(6.0); ui.add_space(6.0);

View file

@ -187,7 +187,7 @@ impl WalletTransport {
} }
QR_ADDRESS_MODAL => { QR_ADDRESS_MODAL => {
Modal::ui(ui.ctx(), |ui, 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. /// 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); ui.add_space(6.0);
// Draw QR code content. // Draw QR code content.
let text = self.qr_address_content.text.clone(); let text = self.qr_address_content.text.clone();
self.qr_address_content.ui(ui, text.clone()); self.qr_address_content.ui(ui, text.clone(), cb);
ui.add_space(6.0); ui.add_space(10.0);
// Show address.
View::ellipsize_text(ui, text, 16.0, Colors::GRAY);
ui.add_space(6.0);
ui.vertical_centered_justified(|ui| { ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::WHITE, || { View::button(ui, t!("close"), Colors::WHITE, || {
self.qr_address_content.clear_state(); self.qr_address_content.clear_state();
modal.close(); m.close();
}); });
ui.add_space(6.0); ui.add_space(6.0);
}); });

View file

@ -122,7 +122,7 @@ impl WalletTab for WalletTransactions {
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
let data = wallet.get_data().unwrap(); 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, fn txs_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &mut Wallet, wallet: &mut Wallet,
data: &WalletData, data: &WalletData) {
cb: &dyn PlatformCallbacks) {
let amount_conf = data.info.amount_awaiting_confirmation; let amount_conf = data.info.amount_awaiting_confirmation;
let amount_fin = data.info.amount_awaiting_finalization; let amount_fin = data.info.amount_awaiting_finalization;
let amount_locked = data.info.amount_locked; let amount_locked = data.info.amount_locked;
@ -224,7 +223,7 @@ impl WalletTransactions {
// Show transaction item. // Show transaction item.
let tx = data.txs.get(index).unwrap(); let tx = data.txs.get(index).unwrap();
let rounding = View::item_rounding(index, data.txs.len(), false); 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, extra_padding: bool,
can_show_info: bool, can_show_info: bool,
data: &WalletData, data: &WalletData,
wallet: &mut Wallet, wallet: &mut Wallet) {
cb: &dyn PlatformCallbacks) {
// Setup layout size. // Setup layout size.
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
if extra_padding { if extra_padding {
@ -541,7 +539,7 @@ impl WalletTransactions {
// Show transaction amount status and time. // Show transaction amount status and time.
let rounding = View::item_rounding(0, 2, false); 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. // Show transaction ID info.
if let Some(id) = tx.data.tx_slate_id { if let Some(id) = tx.data.tx_slate_id {
@ -773,11 +771,7 @@ impl WalletTransactions {
self.tx_info_show_qr = false; self.tx_info_show_qr = false;
} else { } else {
// Draw QR code content. // Draw QR code content.
self.tx_info_qr_code_content.ui(ui, text.clone()); self.tx_info_qr_code_content.ui(ui, text.clone(), cb);
ui.add_space(6.0);
// Show QR code text.
View::ellipsize_text(ui, text, 16.0, Colors::INACTIVE_TEXT);
return; return;
} }
} }