From 4aeda9c9dc6111f3c5dabcd0bfe669e344f50f74 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Thu, 21 May 2026 00:56:28 +0300 Subject: [PATCH] build: v0.3.6, format code --- Cargo.lock | 13 +- Cargo.toml | 6 +- android/app/build.gradle | 2 +- macos/Grim.app/Contents/Info.plist | 2 +- src/gui/app.rs | 728 +-- src/gui/colors.rs | 293 +- src/gui/mod.rs | 3 +- src/gui/platform/android/mod.rs | 345 +- src/gui/platform/desktop/mod.rs | 533 ++- src/gui/platform/mod.rs | 34 +- src/gui/views/camera.rs | 782 +-- src/gui/views/content.rs | 531 ++- src/gui/views/file_pick.rs | 305 +- src/gui/views/input/edit.rs | 740 +-- src/gui/views/input/keyboard.rs | 938 ++-- src/gui/views/input/mod.rs | 2 +- src/gui/views/input/types.rs | 36 +- src/gui/views/mod.rs | 4 +- src/gui/views/modal.rs | 620 +-- src/gui/views/network/connections.rs | 613 +-- src/gui/views/network/content.rs | 773 +-- src/gui/views/network/metrics.rs | 297 +- src/gui/views/network/mining.rs | 464 +- src/gui/views/network/mod.rs | 2 +- src/gui/views/network/modals/ext_conn.rs | 432 +- src/gui/views/network/modals/mod.rs | 2 +- src/gui/views/network/modals/share_conn.rs | 62 +- src/gui/views/network/node.rs | 382 +- src/gui/views/network/settings.rs | 454 +- src/gui/views/network/setup/dandelion.rs | 767 +-- src/gui/views/network/setup/mod.rs | 2 +- src/gui/views/network/setup/node.rs | 1210 ++--- src/gui/views/network/setup/p2p.rs | 1412 +++--- src/gui/views/network/setup/pool.rs | 929 ++-- src/gui/views/network/setup/stratum.rs | 894 ++-- src/gui/views/network/types.rs | 40 +- src/gui/views/pull_to_refresh.rs | 591 +-- src/gui/views/qr.rs | 883 ++-- src/gui/views/scan.rs | 219 +- src/gui/views/settings/content.rs | 100 +- src/gui/views/settings/interface.rs | 211 +- src/gui/views/settings/mod.rs | 2 +- src/gui/views/settings/network.rs | 500 +- src/gui/views/settings/tor.rs | 1134 ++--- src/gui/views/title_panel.rs | 194 +- src/gui/views/types.rs | 177 +- src/gui/views/views.rs | 1441 +++--- src/gui/views/wallets/content.rs | 1452 +++--- src/gui/views/wallets/creation/content.rs | 643 +-- src/gui/views/wallets/creation/mnemonic.rs | 515 +- src/gui/views/wallets/creation/mod.rs | 2 +- src/gui/views/wallets/creation/types.rs | 30 +- src/gui/views/wallets/mod.rs | 4 +- src/gui/views/wallets/modals/add.rs | 158 +- src/gui/views/wallets/modals/changelog.rs | 198 +- src/gui/views/wallets/modals/list.rs | 262 +- src/gui/views/wallets/modals/mod.rs | 2 +- src/gui/views/wallets/modals/open.rs | 167 +- src/gui/views/wallets/modals/settings.rs | 279 +- .../views/wallets/wallet/account/content.rs | 709 +-- .../views/wallets/wallet/account/create.rs | 147 +- src/gui/views/wallets/wallet/account/mod.rs | 2 +- src/gui/views/wallets/wallet/content.rs | 1135 +++-- src/gui/views/wallets/wallet/message.rs | 414 +- src/gui/views/wallets/wallet/mod.rs | 6 +- src/gui/views/wallets/wallet/proof.rs | 409 +- .../views/wallets/wallet/request/invoice.rs | 194 +- src/gui/views/wallets/wallet/request/mod.rs | 2 +- src/gui/views/wallets/wallet/request/send.rs | 573 +-- .../views/wallets/wallet/settings/common.rs | 933 ++-- .../wallets/wallet/settings/connection.rs | 258 +- .../views/wallets/wallet/settings/content.rs | 69 +- src/gui/views/wallets/wallet/settings/mod.rs | 2 +- .../views/wallets/wallet/settings/recovery.rs | 497 +- .../views/wallets/wallet/transport/content.rs | 369 +- src/gui/views/wallets/wallet/transport/mod.rs | 2 +- .../wallets/wallet/transport/settings.rs | 90 +- src/gui/views/wallets/wallet/txs/content.rs | 1333 +++--- src/gui/views/wallets/wallet/txs/mod.rs | 2 +- src/gui/views/wallets/wallet/txs/tx.rs | 704 +-- src/gui/views/wallets/wallet/types.rs | 101 +- src/http/client.rs | 116 +- src/http/mod.rs | 2 +- src/http/release.rs | 246 +- src/lib.rs | 394 +- src/logger.rs | 188 +- src/main.rs | 351 +- src/node/config.rs | 1938 ++++---- src/node/mine_block.rs | 382 +- src/node/mod.rs | 4 +- src/node/node.rs | 1341 +++--- src/node/stratum.rs | 1557 +++--- src/node/types.rs | 20 +- src/settings/config.rs | 645 +-- src/settings/mod.rs | 2 +- src/settings/settings.rs | 298 +- src/tor/config.rs | 362 +- src/tor/http.rs | 260 +- src/tor/mod.rs | 2 +- src/tor/tor.rs | 1374 +++--- src/tor/types.rs | 249 +- src/wallet/config.rs | 436 +- src/wallet/connections/config.rs | 155 +- src/wallet/connections/external.rs | 251 +- src/wallet/connections/mod.rs | 2 +- src/wallet/list.rs | 180 +- src/wallet/mnemonic.rs | 410 +- src/wallet/mod.rs | 2 +- src/wallet/seed.rs | 126 +- src/wallet/store.rs | 154 +- src/wallet/types.rs | 723 +-- src/wallet/utils.rs | 18 +- src/wallet/wallet.rs | 4236 +++++++++-------- wix/main.wxs | 4 +- 114 files changed, 24876 insertions(+), 23319 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ae35090..cc9a9ebe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3900,7 +3900,7 @@ dependencies = [ [[package]] name = "grim" -version = "0.3.5" +version = "0.3.6" dependencies = [ "android-activity", "android_logger", @@ -5690,6 +5690,7 @@ version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" dependencies = [ + "cc", "pkg-config", "vcpkg", ] @@ -6945,6 +6946,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-src" +version = "300.6.0+3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.111" @@ -6953,6 +6963,7 @@ checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] diff --git a/Cargo.toml b/Cargo.toml index 0b0a4f84..eadfba9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "grim" -version = "0.3.5" +version = "0.3.6" authors = ["Ardocrat "] description = "Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere." license = "Apache-2.0" @@ -92,8 +92,8 @@ uuid = { version = "0.8.2", features = ["v4"] } num-bigint = "0.4.6" ## tor -arti-client = { version = "0.42.0", features = ["pt-client", "onion-service-service", "onion-service-client"] } -tor-rtcompat = "0.42.0" +arti-client = { version = "0.42.0", features = ["static", "pt-client", "onion-service-service", "onion-service-client"] } +tor-rtcompat = { version = "0.42.0", features = ["static"] } tor-config = "0.42.0" fs-mistrust = "0.14.1" tor-hsservice = "0.42.0" diff --git a/android/app/build.gradle b/android/app/build.gradle index 9899b051..9a226614 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -12,7 +12,7 @@ android { minSdk 24 targetSdk 36 versionCode 5 - versionName "0.3.5" + versionName "0.3.6" } lint { diff --git a/macos/Grim.app/Contents/Info.plist b/macos/Grim.app/Contents/Info.plist index ea2990a1..775d964e 100644 --- a/macos/Grim.app/Contents/Info.plist +++ b/macos/Grim.app/Contents/Info.plist @@ -21,7 +21,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.3.5 + 0.3.6 CFBundleSupportedPlatforms MacOSX diff --git a/src/gui/app.rs b/src/gui/app.rs index 1ac8af1e..a8382b16 100755 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -12,407 +12,419 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::atomic::{AtomicBool, Ordering}; use egui::epaint::RectShape; -use egui::{Align, Context, CornerRadius, CursorIcon, LayerId, Layout, Modifiers, Order, ResizeDirection, Stroke, StrokeKind, UiBuilder, ViewportCommand}; +use egui::{ + Align, Context, CornerRadius, CursorIcon, LayerId, Layout, Modifiers, Order, ResizeDirection, + Stroke, StrokeKind, UiBuilder, ViewportCommand, +}; use lazy_static::lazy_static; +use std::sync::atomic::{AtomicBool, Ordering}; +use crate::AppConfig; +use crate::gui::Colors; use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::ContentContainer; use crate::gui::views::{Content, KeyboardContent, Modal, TitlePanel, View}; -use crate::gui::Colors; -use crate::AppConfig; lazy_static! { - /// State to check if platform Back button was pressed. - static ref BACK_BUTTON_PRESSED: AtomicBool = AtomicBool::new(false); + /// State to check if platform Back button was pressed. + static ref BACK_BUTTON_PRESSED: AtomicBool = AtomicBool::new(false); } /// Implements ui entry point and contains platform-specific callbacks. pub struct App { - /// Handles platform-specific functionality. - pub platform: Platform, + /// Handles platform-specific functionality. + pub platform: Platform, - /// Main content. - content: Content, + /// Main content. + content: Content, - /// Last window resize direction. - resize_direction: Option, - /// Flag to check if it's first draw. - first_draw: bool + /// Last window resize direction. + resize_direction: Option, + /// Flag to check if it's first draw. + first_draw: bool, } impl App { - pub fn new(platform: Platform) -> Self { - Self { - platform, - content: Content::default(), - resize_direction: None, - first_draw: true - } - } + pub fn new(platform: Platform) -> Self { + Self { + platform, + content: Content::default(), + resize_direction: None, + first_draw: true, + } + } - /// Called of first content draw. - fn on_first_draw(&mut self, ctx: &Context) { - // Set platform context. - if View::is_desktop() { - self.platform.set_context(ctx); - } - // Setup visuals. - crate::setup_fonts(ctx); - crate::setup_visuals(ctx); - } + /// Called of first content draw. + fn on_first_draw(&mut self, ctx: &Context) { + // Set platform context. + if View::is_desktop() { + self.platform.set_context(ctx); + } + // Setup visuals. + crate::setup_fonts(ctx); + crate::setup_visuals(ctx); + } - /// Draw application content. - pub fn ui(&mut self, ctx: &Context) { - if self.first_draw { - self.on_first_draw(ctx); - self.first_draw = false; - } + /// Draw application content. + pub fn ui(&mut self, ctx: &Context) { + if self.first_draw { + self.on_first_draw(ctx); + self.first_draw = false; + } - // Handle Esc keyboard key event and platform Back button key event. - let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed); - if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape) || - i.consume_key(Modifiers::NONE, egui::Key::BrowserBack)) { - // Pass event to content. - self.content.on_back(ctx, &self.platform); - if back_pressed { - BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed); - } - // Request repaint to update previous content. - ctx.request_repaint(); - } + // Handle Esc keyboard key event and platform Back button key event. + let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed); + if back_pressed + || ctx.input_mut(|i| { + i.consume_key(Modifiers::NONE, egui::Key::Escape) + || i.consume_key(Modifiers::NONE, egui::Key::BrowserBack) + }) { + // Pass event to content. + self.content.on_back(ctx, &self.platform); + if back_pressed { + BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed); + } + // Request repaint to update previous content. + ctx.request_repaint(); + } - // Handle Close event on desktop. - if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) { - if !self.content.exit_allowed { - ctx.send_viewport_cmd(ViewportCommand::CancelClose); - Content::show_exit_modal(); - } else { - let (w, h) = View::window_size(ctx); - AppConfig::save_window_size(w, h); - ctx.input(|i| { - if let Some(rect) = i.viewport().outer_rect { - AppConfig::save_window_pos(rect.left(), rect.top()); - } - }); - } - } + // Handle Close event on desktop. + if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) { + if !self.content.exit_allowed { + ctx.send_viewport_cmd(ViewportCommand::CancelClose); + Content::show_exit_modal(); + } else { + let (w, h) = View::window_size(ctx); + AppConfig::save_window_size(w, h); + ctx.input(|i| { + if let Some(rect) = i.viewport().outer_rect { + AppConfig::save_window_pos(rect.left(), rect.top()); + } + }); + } + } - // Show main content. - egui::CentralPanel::default() - .frame(egui::Frame { - ..Default::default() - }) - .show(ctx, |ui| { - if View::is_desktop() { - let is_fullscreen = ui.ctx().input(|i| { - i.viewport().fullscreen.unwrap_or(false) - }); - let os = egui::os::OperatingSystem::from_target_os(); - match os { - egui::os::OperatingSystem::Mac => { - self.window_title_ui(ui, is_fullscreen); - ui.add_space(-1.0); - Self::title_panel_bg(ui, true); - self.content.ui(ui, &self.platform); - } - egui::os::OperatingSystem::Windows => { - Self::title_panel_bg(ui, false); - self.content.ui(ui, &self.platform); - } - _ => { - self.custom_frame_ui(ui, is_fullscreen); - } - } - } else { - Self::title_panel_bg(ui, false); - self.content.ui(ui, &self.platform); - } - }); + // Show main content. + egui::CentralPanel::default() + .frame(egui::Frame { + ..Default::default() + }) + .show(ctx, |ui| { + if View::is_desktop() { + let is_fullscreen = + ui.ctx().input(|i| i.viewport().fullscreen.unwrap_or(false)); + let os = egui::os::OperatingSystem::from_target_os(); + match os { + egui::os::OperatingSystem::Mac => { + self.window_title_ui(ui, is_fullscreen); + ui.add_space(-1.0); + Self::title_panel_bg(ui, true); + self.content.ui(ui, &self.platform); + } + egui::os::OperatingSystem::Windows => { + Self::title_panel_bg(ui, false); + self.content.ui(ui, &self.platform); + } + _ => { + self.custom_frame_ui(ui, is_fullscreen); + } + } + } else { + Self::title_panel_bg(ui, false); + self.content.ui(ui, &self.platform); + } + }); - // Check if desktop window was focused after requested attention. - if self.platform.user_attention_required() && - ctx.input(|i| i.viewport().focused.unwrap_or(true)) { - self.platform.clear_user_attention(); - } + // Check if desktop window was focused after requested attention. + if self.platform.user_attention_required() + && ctx.input(|i| i.viewport().focused.unwrap_or(true)) + { + self.platform.clear_user_attention(); + } - // Show modal or keyboard window above opened Modal. - if Modal::opened().is_some() { - ctx.move_to_top(LayerId::new(Order::Middle, egui::Id::new(Modal::WINDOW_ID))); - let keyboard_showing = if let Some(l) = ctx.top_layer_id() { - l.id == egui::Id::new(KeyboardContent::WINDOW_ID) - } else { - false - }; - if keyboard_showing { - ctx.move_to_top( - LayerId::new(Order::Middle, egui::Id::new(KeyboardContent::WINDOW_ID)) - ); - } - } - // Reset keyboard state for newly opened modal. - if Modal::first_draw() { - KeyboardContent::reset_window_state(); - } - } + // Show modal or keyboard window above opened Modal. + if Modal::opened().is_some() { + ctx.move_to_top(LayerId::new(Order::Middle, egui::Id::new(Modal::WINDOW_ID))); + let keyboard_showing = if let Some(l) = ctx.top_layer_id() { + l.id == egui::Id::new(KeyboardContent::WINDOW_ID) + } else { + false + }; + if keyboard_showing { + ctx.move_to_top(LayerId::new( + Order::Middle, + egui::Id::new(KeyboardContent::WINDOW_ID), + )); + } + } + // Reset keyboard state for newly opened modal. + if Modal::first_draw() { + KeyboardContent::reset_window_state(); + } + } - /// Draw custom desktop window frame content. - fn custom_frame_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) { - let content_bg_rect = { - let mut r = ui.max_rect(); - if !is_fullscreen { - r = r.shrink(Content::WINDOW_FRAME_MARGIN); - } - r.min.y += Content::WINDOW_TITLE_HEIGHT + TitlePanel::HEIGHT; - r - }; - let content_bg = RectShape::new(content_bg_rect, - CornerRadius::ZERO, - Colors::fill_lite(), - View::default_stroke(), - StrokeKind::Outside); - // Draw content background. - ui.painter().add(content_bg); + /// Draw custom desktop window frame content. + fn custom_frame_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) { + let content_bg_rect = { + let mut r = ui.max_rect(); + if !is_fullscreen { + r = r.shrink(Content::WINDOW_FRAME_MARGIN); + } + r.min.y += Content::WINDOW_TITLE_HEIGHT + TitlePanel::HEIGHT; + r + }; + let content_bg = RectShape::new( + content_bg_rect, + CornerRadius::ZERO, + Colors::fill_lite(), + View::default_stroke(), + StrokeKind::Outside, + ); + // Draw content background. + ui.painter().add(content_bg); - let mut content_rect = ui.max_rect(); - if !is_fullscreen { - content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN); - } - // Draw window content. - ui.scope_builder(UiBuilder::new().max_rect(content_rect), |ui| { - // Draw window title. - self.window_title_ui(ui, is_fullscreen); - ui.add_space(-1.0); + let mut content_rect = ui.max_rect(); + if !is_fullscreen { + content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN); + } + // Draw window content. + ui.scope_builder(UiBuilder::new().max_rect(content_rect), |ui| { + // Draw window title. + self.window_title_ui(ui, is_fullscreen); + ui.add_space(-1.0); - // Draw title panel background. - Self::title_panel_bg(ui, true); + // Draw title panel background. + Self::title_panel_bg(ui, true); - let content_rect = { - let mut rect = ui.max_rect(); - rect.min.y += Content::WINDOW_TITLE_HEIGHT; - rect - }; - let mut content_ui = ui.new_child(UiBuilder::new() - .max_rect(content_rect) - .layout(*ui.layout())); - // Draw main content. - self.content.ui(&mut content_ui, &self.platform); - }); + let content_rect = { + let mut rect = ui.max_rect(); + rect.min.y += Content::WINDOW_TITLE_HEIGHT; + rect + }; + let mut content_ui = + ui.new_child(UiBuilder::new().max_rect(content_rect).layout(*ui.layout())); + // Draw main content. + self.content.ui(&mut content_ui, &self.platform); + }); - // Setup resize areas. - if !is_fullscreen { - self.resize_area_ui(ui, ResizeDirection::North); - self.resize_area_ui(ui, ResizeDirection::East); - self.resize_area_ui(ui, ResizeDirection::South); - self.resize_area_ui(ui, ResizeDirection::West); - self.resize_area_ui(ui, ResizeDirection::NorthWest); - self.resize_area_ui(ui, ResizeDirection::NorthEast); - self.resize_area_ui(ui, ResizeDirection::SouthEast); - self.resize_area_ui(ui, ResizeDirection::SouthWest); - } - } + // Setup resize areas. + if !is_fullscreen { + self.resize_area_ui(ui, ResizeDirection::North); + self.resize_area_ui(ui, ResizeDirection::East); + self.resize_area_ui(ui, ResizeDirection::South); + self.resize_area_ui(ui, ResizeDirection::West); + self.resize_area_ui(ui, ResizeDirection::NorthWest); + self.resize_area_ui(ui, ResizeDirection::NorthEast); + self.resize_area_ui(ui, ResizeDirection::SouthEast); + self.resize_area_ui(ui, ResizeDirection::SouthWest); + } + } - /// Draw title panel background. - fn title_panel_bg(ui: &mut egui::Ui, window_title: bool) { - let title_rect = { - let mut rect = ui.max_rect(); - if window_title { - rect.min.y += Content::WINDOW_TITLE_HEIGHT - 0.5; - } - rect.max.y = rect.min.y + View::get_top_inset() + TitlePanel::HEIGHT; - rect - }; - let title_bg = RectShape::filled(title_rect, CornerRadius::ZERO, Colors::yellow()); - ui.painter().add(title_bg); - } + /// Draw title panel background. + fn title_panel_bg(ui: &mut egui::Ui, window_title: bool) { + let title_rect = { + let mut rect = ui.max_rect(); + if window_title { + rect.min.y += Content::WINDOW_TITLE_HEIGHT - 0.5; + } + rect.max.y = rect.min.y + View::get_top_inset() + TitlePanel::HEIGHT; + rect + }; + let title_bg = RectShape::filled(title_rect, CornerRadius::ZERO, Colors::yellow()); + ui.painter().add(title_bg); + } - /// Draw custom window title content. - fn window_title_ui(&self, ui: &mut egui::Ui, is_fullscreen: bool) { - let title_rect = { - let mut rect = ui.max_rect(); - rect.max.y = rect.min.y + Content::WINDOW_TITLE_HEIGHT; - rect - }; + /// Draw custom window title content. + fn window_title_ui(&self, ui: &mut egui::Ui, is_fullscreen: bool) { + let title_rect = { + let mut rect = ui.max_rect(); + rect.max.y = rect.min.y + Content::WINDOW_TITLE_HEIGHT; + rect + }; - let title_bg_rect = { - let mut r = title_rect.clone(); - r.max.y += TitlePanel::HEIGHT - 1.0; - r - }; - let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac; - let window_title_bg = RectShape::new(title_bg_rect, if is_fullscreen || is_mac { - CornerRadius::ZERO - } else { - CornerRadius { - nw: 8.0 as u8, - ne: 8.0 as u8, - sw: 0.0 as u8, - se: 0.0 as u8, - } - }, Colors::yellow_dark(), Stroke::new(1.0, Colors::STROKE), StrokeKind::Outside); - // Draw title background. - ui.painter().add(window_title_bg); + let title_bg_rect = { + let mut r = title_rect.clone(); + r.max.y += TitlePanel::HEIGHT - 1.0; + r + }; + let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac; + let window_title_bg = RectShape::new( + title_bg_rect, + if is_fullscreen || is_mac { + CornerRadius::ZERO + } else { + CornerRadius { + nw: 8.0 as u8, + ne: 8.0 as u8, + sw: 0.0 as u8, + se: 0.0 as u8, + } + }, + Colors::yellow_dark(), + Stroke::new(1.0, Colors::STROKE), + StrokeKind::Outside, + ); + // Draw title background. + ui.painter().add(window_title_bg); - let painter = ui.painter(); + let painter = ui.painter(); - let interact_rect = { - let mut rect = title_rect.clone(); - rect.max.x -= 128.0; - rect.min.x += 85.0; - if !is_fullscreen { - rect.min.y += Content::WINDOW_FRAME_MARGIN; - } - rect - }; - let title_resp = ui.interact( - interact_rect, - egui::Id::new("window_title"), - egui::Sense::drag(), - ); - // Interact with the window title (drag to move window): - if !is_fullscreen && title_resp.drag_started_by(egui::PointerButton::Primary) { - ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag); - } + let interact_rect = { + let mut rect = title_rect.clone(); + rect.max.x -= 128.0; + rect.min.x += 85.0; + if !is_fullscreen { + rect.min.y += Content::WINDOW_FRAME_MARGIN; + } + rect + }; + let title_resp = ui.interact( + interact_rect, + egui::Id::new("window_title"), + egui::Sense::drag(), + ); + // Interact with the window title (drag to move window): + if !is_fullscreen && title_resp.drag_started_by(egui::PointerButton::Primary) { + ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag); + } - // Paint the title. - let title_text = format!("Grim {} ツ", crate::VERSION); - painter.text( - title_rect.center(), - egui::Align2::CENTER_CENTER, - title_text, - egui::FontId::proportional(15.0), - Colors::title(true), - ); + // Paint the title. + let title_text = format!("Grim {} ツ", crate::VERSION); + painter.text( + title_rect.center(), + egui::Align2::CENTER_CENTER, + title_text, + egui::FontId::proportional(15.0), + Colors::title(true), + ); - ui.scope_builder(UiBuilder::new().max_rect(title_rect), |ui| { - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - // Draw button to close window. - View::title_button_small(ui, X, |_| { - if Modal::opened().is_none() || Modal::opened_closeable() { - Content::show_exit_modal(); - } - }); + ui.scope_builder(UiBuilder::new().max_rect(title_rect), |ui| { + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + // Draw button to close window. + View::title_button_small(ui, X, |_| { + if Modal::opened().is_none() || Modal::opened_closeable() { + Content::show_exit_modal(); + } + }); - // Draw fullscreen button. - let fullscreen_icon = if is_fullscreen { - ARROWS_IN - } else { - ARROWS_OUT - }; - View::title_button_small(ui, fullscreen_icon, |ui| { - ui.ctx().send_viewport_cmd(ViewportCommand::Fullscreen(!is_fullscreen)); - }); + // Draw fullscreen button. + let fullscreen_icon = if is_fullscreen { ARROWS_IN } else { ARROWS_OUT }; + View::title_button_small(ui, fullscreen_icon, |ui| { + ui.ctx() + .send_viewport_cmd(ViewportCommand::Fullscreen(!is_fullscreen)); + }); - // Draw button to minimize window. - View::title_button_small(ui, CARET_DOWN, |ui| { - ui.ctx().send_viewport_cmd(ViewportCommand::Minimized(true)); - }); + // Draw button to minimize window. + View::title_button_small(ui, CARET_DOWN, |ui| { + ui.ctx().send_viewport_cmd(ViewportCommand::Minimized(true)); + }); - // Draw application icon. - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - // Draw button to minimize window. - let use_dark = AppConfig::dark_theme().unwrap_or(false); - let theme_icon = if use_dark { - SUN - } else { - MOON - }; - View::title_button_small(ui, theme_icon, |ui| { - AppConfig::set_dark_theme(!use_dark); - crate::setup_visuals(ui.ctx()); - }); - }); - }); - }); - } + // Draw application icon. + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout( + layout_size, + Layout::left_to_right(Align::Center), + |ui| { + // Draw button to minimize window. + let use_dark = AppConfig::dark_theme().unwrap_or(false); + let theme_icon = if use_dark { SUN } else { MOON }; + View::title_button_small(ui, theme_icon, |ui| { + AppConfig::set_dark_theme(!use_dark); + crate::setup_visuals(ui.ctx()); + }); + }, + ); + }); + }); + } - /// Setup window resize area. - fn resize_area_ui(&mut self, ui: &egui::Ui, direction: ResizeDirection) { - let mut rect = ui.max_rect(); + /// Setup window resize area. + fn resize_area_ui(&mut self, ui: &egui::Ui, direction: ResizeDirection) { + let mut rect = ui.max_rect(); - // Setup area id, cursor and area rect based on direction. - let (id, cursor, rect) = match direction { - ResizeDirection::North => ("n", CursorIcon::ResizeNorth, { - rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0; - rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN; - rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0; - rect - }), - ResizeDirection::East => ("e", CursorIcon::ResizeEast, { - rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0; - rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN; - rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0; - rect - }), - ResizeDirection::South => ("s", CursorIcon::ResizeSouth, { - rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0; - rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN; - rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0; - rect - }), - ResizeDirection::West => ("w", CursorIcon::ResizeWest, { - rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0; - rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN; - rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0; - rect - }), - ResizeDirection::NorthWest => ("nw", CursorIcon::ResizeNorthWest, { - rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN * 2.0; - rect.max.x = rect.max.y + Content::WINDOW_FRAME_MARGIN * 2.0; - rect - }), - ResizeDirection::NorthEast => ("ne", CursorIcon::ResizeNorthEast, { - rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0; - rect.max.y = Content::WINDOW_FRAME_MARGIN * 2.0; - rect - }), - ResizeDirection::SouthEast => ("se", CursorIcon::ResizeSouthEast, { - rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0; - rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0; - rect - }), - ResizeDirection::SouthWest => ("sw", CursorIcon::ResizeSouthWest, { - rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0; - rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN * 2.0; - rect - }), - }; + // Setup area id, cursor and area rect based on direction. + let (id, cursor, rect) = match direction { + ResizeDirection::North => ("n", CursorIcon::ResizeNorth, { + rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0; + rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN; + rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0; + rect + }), + ResizeDirection::East => ("e", CursorIcon::ResizeEast, { + rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0; + rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN; + rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0; + rect + }), + ResizeDirection::South => ("s", CursorIcon::ResizeSouth, { + rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0; + rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN; + rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0; + rect + }), + ResizeDirection::West => ("w", CursorIcon::ResizeWest, { + rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0; + rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN; + rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0; + rect + }), + ResizeDirection::NorthWest => ("nw", CursorIcon::ResizeNorthWest, { + rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN * 2.0; + rect.max.x = rect.max.y + Content::WINDOW_FRAME_MARGIN * 2.0; + rect + }), + ResizeDirection::NorthEast => ("ne", CursorIcon::ResizeNorthEast, { + rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0; + rect.max.y = Content::WINDOW_FRAME_MARGIN * 2.0; + rect + }), + ResizeDirection::SouthEast => ("se", CursorIcon::ResizeSouthEast, { + rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0; + rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0; + rect + }), + ResizeDirection::SouthWest => ("sw", CursorIcon::ResizeSouthWest, { + rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0; + rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN * 2.0; + rect + }), + }; - // Setup resize area. - let id = egui::Id::new("window_resize").with(id); - let sense = egui::Sense::drag(); - let area_resp = ui.interact(rect, id, sense).on_hover_cursor(cursor); - if area_resp.dragged() { - if self.resize_direction.is_none() { - self.resize_direction = Some(direction.clone()); - ui.ctx().send_viewport_cmd(ViewportCommand::BeginResize(direction)); - } - } - if area_resp.drag_stopped() { - self.resize_direction = None; - } - } + // Setup resize area. + let id = egui::Id::new("window_resize").with(id); + let sense = egui::Sense::drag(); + let area_resp = ui.interact(rect, id, sense).on_hover_cursor(cursor); + if area_resp.dragged() { + if self.resize_direction.is_none() { + self.resize_direction = Some(direction.clone()); + ui.ctx() + .send_viewport_cmd(ViewportCommand::BeginResize(direction)); + } + } + if area_resp.drag_stopped() { + self.resize_direction = None; + } + } } /// To draw with egui`s eframe (for wgpu, glow backends and wasm target). impl eframe::App for App { - fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { - ctx.plugin_or_default::(); - self.ui(ctx); - } + fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { + ctx.plugin_or_default::(); + self.ui(ctx); + } - fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] { - let os = egui::os::OperatingSystem::from_target_os(); - let is_win = os == egui::os::OperatingSystem::Windows; - let is_mac = os == egui::os::OperatingSystem::Mac; - if !View::is_desktop() || is_win || is_mac { - return Colors::fill_lite().to_normalized_gamma_f32(); - } - Colors::TRANSPARENT.to_normalized_gamma_f32() - } + fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] { + let os = egui::os::OperatingSystem::from_target_os(); + let is_win = os == egui::os::OperatingSystem::Windows; + let is_mac = os == egui::os::OperatingSystem::Mac; + if !View::is_desktop() || is_win || is_mac { + return Colors::fill_lite().to_normalized_gamma_f32(); + } + Colors::TRANSPARENT.to_normalized_gamma_f32() + } } #[allow(dead_code)] @@ -421,9 +433,9 @@ impl eframe::App for App { #[unsafe(no_mangle)] /// Handle Back key code event from Android. pub extern "C" fn Java_mw_gri_android_MainActivity_onBack( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - _activity: jni::objects::JObject, + _env: jni::JNIEnv, + _class: jni::objects::JObject, + _activity: jni::objects::JObject, ) { - BACK_BUTTON_PRESSED.store(true, Ordering::Relaxed); -} \ No newline at end of file + BACK_BUTTON_PRESSED.store(true, Ordering::Relaxed); +} diff --git a/src/gui/colors.rs b/src/gui/colors.rs index 10147988..95d60734 100644 --- a/src/gui/colors.rs +++ b/src/gui/colors.rs @@ -38,8 +38,11 @@ const RED: Color32 = Color32::from_rgb(0x8B, 0, 0); const RED_DARK: Color32 = Color32::from_rgb((0x8B as f32 * 1.3 + 0.5) as u8, 50, 30); const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4); -const BLUE_DARK: Color32 = - Color32::from_rgb(0, (0x66 as f32 * 1.3 + 0.5) as u8, (0xE4 as f32 * 1.3 + 0.5) as u8); +const BLUE_DARK: Color32 = Color32::from_rgb( + 0, + (0x66 as f32 * 1.3 + 0.5) as u8, + (0xE4 as f32 * 1.3 + 0.5) as u8, +); const FILL: Color32 = Color32::from_gray(244); const FILL_DARK: Color32 = Color32::from_gray(26); @@ -80,187 +83,155 @@ const ITEM_HOVER_DARK: Color32 = Color32::from_gray(48); /// Check if dark theme should be used. fn use_dark() -> bool { - AppConfig::dark_theme().unwrap_or(false) + AppConfig::dark_theme().unwrap_or(false) } impl Colors { - pub const FILL_DEEP: Color32 = Color32::from_gray(238); - pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0); - pub const STROKE: Color32 = Color32::from_gray(200); + pub const FILL_DEEP: Color32 = Color32::from_gray(238); + pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0); + pub const STROKE: Color32 = Color32::from_gray(200); - pub fn white_or_black(black_in_white: bool) -> Color32 { - if use_dark() { - if black_in_white { - WHITE - } else { - BLACK - } - } else { - if black_in_white { - BLACK - } else { - WHITE - } - } - } + pub fn white_or_black(black_in_white: bool) -> Color32 { + if use_dark() { + if black_in_white { WHITE } else { BLACK } + } else { + if black_in_white { BLACK } else { WHITE } + } + } - pub fn semi_transparent() -> Color32 { - if use_dark() { - DARK_SEMI_TRANSPARENT - } else { - SEMI_TRANSPARENT - } - } + pub fn semi_transparent() -> Color32 { + if use_dark() { + DARK_SEMI_TRANSPARENT + } else { + SEMI_TRANSPARENT + } + } - pub fn gold() -> Color32 { - if use_dark() { - GOLD.gamma_multiply(0.9) - } else { - GOLD - } - } + pub fn gold() -> Color32 { + if use_dark() { + GOLD.gamma_multiply(0.9) + } else { + GOLD + } + } - pub fn gold_dark() -> Color32 { - if use_dark() { - GOLD_DARK.gamma_multiply(0.9) - } else { - GOLD_DARK - } - } + pub fn gold_dark() -> Color32 { + if use_dark() { + GOLD_DARK.gamma_multiply(0.9) + } else { + GOLD_DARK + } + } - pub fn yellow() -> Color32 { - YELLOW - } + pub fn yellow() -> Color32 { + YELLOW + } - pub fn yellow_dark() -> Color32 { - YELLOW_DARK - } + pub fn yellow_dark() -> Color32 { + YELLOW_DARK + } - pub fn green() -> Color32 { - if use_dark() { - GREEN_DARK - } else { - GREEN - } - } + pub fn green() -> Color32 { + if use_dark() { GREEN_DARK } else { GREEN } + } - pub fn red() -> Color32 { - if use_dark() { - RED_DARK - } else { - RED - } - } + pub fn red() -> Color32 { + if use_dark() { RED_DARK } else { RED } + } - pub fn blue() -> Color32 { - if use_dark() { - BLUE_DARK - } else { - BLUE - } - } + pub fn blue() -> Color32 { + if use_dark() { BLUE_DARK } else { BLUE } + } - pub fn fill() -> Color32 { - if use_dark() { - FILL_DARK - } else { - FILL - } - } + pub fn fill() -> Color32 { + if use_dark() { FILL_DARK } else { FILL } + } - pub fn fill_deep() -> Color32 { - if use_dark() { - FILL_DEEP_DARK - } else { - Self::FILL_DEEP - } - } + pub fn fill_deep() -> Color32 { + if use_dark() { + FILL_DEEP_DARK + } else { + Self::FILL_DEEP + } + } - pub fn fill_lite() -> Color32 { - if use_dark() { - FILL_LITE_DARK - } else { - FILL_LITE - } - } + pub fn fill_lite() -> Color32 { + if use_dark() { + FILL_LITE_DARK + } else { + FILL_LITE + } + } - pub fn checkbox() -> Color32 { - if use_dark() { - CHECKBOX_DARK - } else { - CHECKBOX - } - } + pub fn checkbox() -> Color32 { + if use_dark() { CHECKBOX_DARK } else { CHECKBOX } + } - pub fn text(always_light: bool) -> Color32 { - if use_dark() && !always_light { - TEXT_DARK - } else { - TEXT - } - } + pub fn text(always_light: bool) -> Color32 { + if use_dark() && !always_light { + TEXT_DARK + } else { + TEXT + } + } - pub fn text_button() -> Color32 { - if use_dark() { - TEXT_BUTTON_DARK - } else { - TEXT_BUTTON - } - } + pub fn text_button() -> Color32 { + if use_dark() { + TEXT_BUTTON_DARK + } else { + TEXT_BUTTON + } + } - pub fn title(always_light: bool) -> Color32 { - if use_dark() && !always_light { - TITLE_DARK - } else { - TITLE - } - } + pub fn title(always_light: bool) -> Color32 { + if use_dark() && !always_light { + TITLE_DARK + } else { + TITLE + } + } - pub fn gray() -> Color32 { - if use_dark() { - GRAY_DARK - } else { - GRAY - } - } + pub fn gray() -> Color32 { + if use_dark() { GRAY_DARK } else { GRAY } + } - pub fn stroke() -> Color32 { - if use_dark() { - STROKE_DARK - } else { - Self::STROKE - } - } + pub fn stroke() -> Color32 { + if use_dark() { + STROKE_DARK + } else { + Self::STROKE + } + } - pub fn inactive_text() -> Color32 { - if use_dark() { - INACTIVE_TEXT_DARK - } else { - INACTIVE_TEXT - } - } + pub fn inactive_text() -> Color32 { + if use_dark() { + INACTIVE_TEXT_DARK + } else { + INACTIVE_TEXT + } + } - pub fn item_button_text() -> Color32 { - if use_dark() { - ITEM_BUTTON_DARK - } else { - ITEM_BUTTON - } - } + pub fn item_button_text() -> Color32 { + if use_dark() { + ITEM_BUTTON_DARK + } else { + ITEM_BUTTON + } + } - pub fn item_stroke() -> Color32 { - if use_dark() { - ITEM_STROKE_DARK - } else { - ITEM_STROKE - } - } + pub fn item_stroke() -> Color32 { + if use_dark() { + ITEM_STROKE_DARK + } else { + ITEM_STROKE + } + } - pub fn item_hover() -> Color32 { - if use_dark() { - ITEM_HOVER_DARK - } else { - ITEM_HOVER - } - } -} \ No newline at end of file + pub fn item_hover() -> Color32 { + if use_dark() { + ITEM_HOVER_DARK + } else { + ITEM_HOVER + } + } +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs index cf6c7a80..7ddf7a5d 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. - mod app; pub use app::App; mod colors; pub use colors::Colors; +pub mod icons; pub mod platform; pub mod views; -pub mod icons; \ No newline at end of file diff --git a/src/gui/platform/android/mod.rs b/src/gui/platform/android/mod.rs index e275ec25..7592b9b4 100644 --- a/src/gui/platform/android/mod.rs +++ b/src/gui/platform/android/mod.rs @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +use lazy_static::lazy_static; +use parking_lot::RwLock; use std::env; use std::ffi::OsString; use std::fs::File; use std::io::Write; use std::path::PathBuf; -use lazy_static::lazy_static; use std::sync::Arc; -use parking_lot::RwLock; use jni::JNIEnv; use jni::objects::{JByteArray, JObject, JString, JValue}; @@ -30,210 +30,217 @@ use crate::gui::platform::PlatformCallbacks; /// Android platform implementation. #[derive(Clone)] pub struct Android { - /// Android related state. - android_app: AndroidApp, + /// Android related state. + android_app: AndroidApp, - /// Context to repaint content and handle viewport commands. - ctx: Arc>>, + /// Context to repaint content and handle viewport commands. + ctx: Arc>>, } impl Android { - /// Create new Android platform instance from provided [`AndroidApp`]. - pub fn new(app: AndroidApp) -> Self { - Self { - android_app: app, - ctx: Arc::new(RwLock::new(None)), - } - } + /// Create new Android platform instance from provided [`AndroidApp`]. + pub fn new(app: AndroidApp) -> Self { + Self { + android_app: app, + ctx: Arc::new(RwLock::new(None)), + } + } - /// Call Android Activity method with JNI. - pub fn call_java_method(&self, name: &str, s: &str, a: &[JValue]) -> Option { - let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); - let mut env = vm.attach_current_thread().unwrap(); - let activity = unsafe { - JObject::from_raw(self.android_app.activity_as_ptr() as jni::sys::jobject) - }; - if let Ok(result) = env.call_method(activity, name, s, a) { - return Some(result.as_jni().clone()); - } - None - } + /// Call Android Activity method with JNI. + pub fn call_java_method(&self, name: &str, s: &str, a: &[JValue]) -> Option { + let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); + let mut env = vm.attach_current_thread().unwrap(); + let activity = + unsafe { JObject::from_raw(self.android_app.activity_as_ptr() as jni::sys::jobject) }; + if let Ok(result) = env.call_method(activity, name, s, a) { + return Some(result.as_jni().clone()); + } + None + } } impl PlatformCallbacks for Android { - fn set_context(&mut self, ctx: &egui::Context) { - let mut w_ctx = self.ctx.write(); - *w_ctx = Some(ctx.clone()); - } + fn set_context(&mut self, ctx: &egui::Context) { + let mut w_ctx = self.ctx.write(); + *w_ctx = Some(ctx.clone()); + } - fn exit(&self) { - let _ = self.call_java_method("exit", "()V", &[]); - } + fn exit(&self) { + let _ = self.call_java_method("exit", "()V", &[]); + } - fn copy_string_to_buffer(&self, data: String) { - let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); - let env = vm.attach_current_thread().unwrap(); - let arg_value = env.new_string(data).unwrap(); - let _ = self.call_java_method("copyText", - "(Ljava/lang/String;)V", - &[JValue::Object(&JObject::from(arg_value))]); - } + fn copy_string_to_buffer(&self, data: String) { + let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); + let env = vm.attach_current_thread().unwrap(); + let arg_value = env.new_string(data).unwrap(); + let _ = self.call_java_method( + "copyText", + "(Ljava/lang/String;)V", + &[JValue::Object(&JObject::from(arg_value))], + ); + } - fn get_string_from_buffer(&self) -> String { - let result = self.call_java_method("pasteText", "()Ljava/lang/String;", &[]).unwrap(); - let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); - let mut env = vm.attach_current_thread().unwrap(); - let j_object: jni::sys::jobject = unsafe { result.l }; - let paste_data: String = unsafe { - env.get_string(JString::from(JObject::from_raw(j_object)).as_ref()).unwrap().into() - }; - paste_data - } + fn get_string_from_buffer(&self) -> String { + let result = self + .call_java_method("pasteText", "()Ljava/lang/String;", &[]) + .unwrap(); + let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); + let mut env = vm.attach_current_thread().unwrap(); + let j_object: jni::sys::jobject = unsafe { result.l }; + let paste_data: String = unsafe { + env.get_string(JString::from(JObject::from_raw(j_object)).as_ref()) + .unwrap() + .into() + }; + paste_data + } - fn start_camera(&self) { - // Clear image. - let mut w_image = LAST_CAMERA_IMAGE.write(); - *w_image = None; - // Start camera. - let _ = self.call_java_method("startCamera", "()V", &[]); - } + fn start_camera(&self) { + // Clear image. + let mut w_image = LAST_CAMERA_IMAGE.write(); + *w_image = None; + // Start camera. + let _ = self.call_java_method("startCamera", "()V", &[]); + } - fn stop_camera(&self) { - // Stop camera. - let _ = self.call_java_method("stopCamera", "()V", &[]); - // Clear image. - let mut w_image = LAST_CAMERA_IMAGE.write(); - *w_image = None; - } + fn stop_camera(&self) { + // Stop camera. + let _ = self.call_java_method("stopCamera", "()V", &[]); + // Clear image. + let mut w_image = LAST_CAMERA_IMAGE.write(); + *w_image = None; + } - fn camera_image(&self) -> Option<(Vec, u32)> { - let r_image = LAST_CAMERA_IMAGE.read(); - if r_image.is_some() { - return Some(r_image.clone().unwrap()); - } - None - } + fn camera_image(&self) -> Option<(Vec, u32)> { + let r_image = LAST_CAMERA_IMAGE.read(); + if r_image.is_some() { + return Some(r_image.clone().unwrap()); + } + None + } - fn can_switch_camera(&self) -> bool { - if let Some(res) = self.call_java_method("camerasAmount", "()I", &[]) { - let amount = unsafe { res.i }; - return amount > 1; - } - false - } + fn can_switch_camera(&self) -> bool { + if let Some(res) = self.call_java_method("camerasAmount", "()I", &[]) { + let amount = unsafe { res.i }; + return amount > 1; + } + false + } - fn switch_camera(&self) { - let _ = self.call_java_method("switchCamera", "()V", &[]); - } + fn switch_camera(&self) { + let _ = self.call_java_method("switchCamera", "()V", &[]); + } - fn share_data(&self, name: String, data: Vec) -> Result<(), std::io::Error> { - let default_cache = OsString::from(dirs::cache_dir().unwrap()); - let mut file = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache)); - // File path for Android provider. - file.push("share"); - if !file.exists() { - std::fs::create_dir(file.clone())?; - } - file.push(name); - if file.exists() { - std::fs::remove_file(file.clone())?; - } - let mut image = File::create_new(file.clone())?; - image.write_all(data.as_slice())?; - image.sync_all()?; - // Call share modal at system. - let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); - let env = vm.attach_current_thread().unwrap(); - let arg_value = env.new_string(file.to_str().unwrap()).unwrap(); - let _ = self.call_java_method("shareData", - "(Ljava/lang/String;)V", - &[JValue::Object(&JObject::from(arg_value))]); - Ok(()) - } + fn share_data(&self, name: String, data: Vec) -> Result<(), std::io::Error> { + let default_cache = OsString::from(dirs::cache_dir().unwrap()); + let mut file = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache)); + // File path for Android provider. + file.push("share"); + if !file.exists() { + std::fs::create_dir(file.clone())?; + } + file.push(name); + if file.exists() { + std::fs::remove_file(file.clone())?; + } + let mut image = File::create_new(file.clone())?; + image.write_all(data.as_slice())?; + image.sync_all()?; + // Call share modal at system. + let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); + let env = vm.attach_current_thread().unwrap(); + let arg_value = env.new_string(file.to_str().unwrap()).unwrap(); + let _ = self.call_java_method( + "shareData", + "(Ljava/lang/String;)V", + &[JValue::Object(&JObject::from(arg_value))], + ); + Ok(()) + } - fn pick_file(&self) -> Option { - // Clear previous result. - let mut w_path = PICKED_FILE_PATH.write(); - *w_path = None; - // Launch file picker. - let _ = self.call_java_method("pickFile", "()V", &[]); - // Return empty string to identify async pick. - Some("".to_string()) - } + fn pick_file(&self) -> Option { + // Clear previous result. + let mut w_path = PICKED_FILE_PATH.write(); + *w_path = None; + // Launch file picker. + let _ = self.call_java_method("pickFile", "()V", &[]); + // Return empty string to identify async pick. + Some("".to_string()) + } - fn pick_folder(&self) -> Option { - // Clear previous result. - let mut w_path = PICKED_FILE_PATH.write(); - *w_path = None; - // Launch file picker. - let _ = self.call_java_method("pickFolder", "()V", &[]); - // Return empty string to identify async pick. - Some("".to_string()) - } + fn pick_folder(&self) -> Option { + // Clear previous result. + let mut w_path = PICKED_FILE_PATH.write(); + *w_path = None; + // Launch file picker. + let _ = self.call_java_method("pickFolder", "()V", &[]); + // Return empty string to identify async pick. + Some("".to_string()) + } - fn picked_file(&self) -> Option { - let has_file = { - let r_path = PICKED_FILE_PATH.read(); - r_path.is_some() - }; - if has_file { - let mut w_path = PICKED_FILE_PATH.write(); - let path = Some(w_path.clone().unwrap()); - *w_path = None; - return path - } - None - } + fn picked_file(&self) -> Option { + let has_file = { + let r_path = PICKED_FILE_PATH.read(); + r_path.is_some() + }; + if has_file { + let mut w_path = PICKED_FILE_PATH.write(); + let path = Some(w_path.clone().unwrap()); + *w_path = None; + return path; + } + None + } - fn request_user_attention(&self) {} + fn request_user_attention(&self) {} - fn user_attention_required(&self) -> bool { - false - } + fn user_attention_required(&self) -> bool { + false + } - fn clear_user_attention(&self) {} + fn clear_user_attention(&self) {} } lazy_static! { - /// Last image data from camera. - static ref LAST_CAMERA_IMAGE: Arc, u32)>>> = Arc::new(RwLock::new(None)); - /// Picked file path. - static ref PICKED_FILE_PATH: Arc>> = Arc::new(RwLock::new(None)); + /// Last image data from camera. + static ref LAST_CAMERA_IMAGE: Arc, u32)>>> = Arc::new(RwLock::new(None)); + /// Picked file path. + static ref PICKED_FILE_PATH: Arc>> = Arc::new(RwLock::new(None)); } /// Callback from Java code with last entered character from soft keyboard. #[allow(non_snake_case)] #[unsafe(no_mangle)] pub extern "C" fn Java_mw_gri_android_MainActivity_onCameraImage( - env: JNIEnv, - _class: JObject, - buff: jni::sys::jbyteArray, - rotation: jni::sys::jint, + env: JNIEnv, + _class: JObject, + buff: jni::sys::jbyteArray, + rotation: jni::sys::jint, ) { - let arr = unsafe { JByteArray::from_raw(buff) }; - let image : Vec = env.convert_byte_array(arr).unwrap(); - let mut w_image = LAST_CAMERA_IMAGE.write(); - *w_image = Some((image, rotation as u32)); + let arr = unsafe { JByteArray::from_raw(buff) }; + let image: Vec = env.convert_byte_array(arr).unwrap(); + let mut w_image = LAST_CAMERA_IMAGE.write(); + *w_image = Some((image, rotation as u32)); } /// Callback from Java code with picked file path. #[allow(non_snake_case)] #[unsafe(no_mangle)] pub extern "C" fn Java_mw_gri_android_MainActivity_onFilePick( - _env: JNIEnv, - _class: JObject, - char: jni::sys::jstring + _env: JNIEnv, + _class: JObject, + char: jni::sys::jstring, ) { - use std::ops::Add; - unsafe { - let j_obj = JString::from_raw(char); - let j_str = _env.get_string_unchecked(j_obj.as_ref()).unwrap(); - match j_str.to_str() { - Ok(str) => { - let mut w_path = PICKED_FILE_PATH.write(); - *w_path = Some(w_path.clone().unwrap_or("".to_string()).add(str)); - } - Err(_) => {} - } - } -} \ No newline at end of file + use std::ops::Add; + unsafe { + let j_obj = JString::from_raw(char); + let j_str = _env.get_string_unchecked(j_obj.as_ref()).unwrap(); + match j_str.to_str() { + Ok(str) => { + let mut w_path = PICKED_FILE_PATH.write(); + *w_path = Some(w_path.clone().unwrap_or("".to_string()).add(str)); + } + Err(_) => {} + } + } +} diff --git a/src/gui/platform/desktop/mod.rs b/src/gui/platform/desktop/mod.rs index 8c9e7daf..9bb1b7ab 100644 --- a/src/gui/platform/desktop/mod.rs +++ b/src/gui/platform/desktop/mod.rs @@ -12,309 +12,316 @@ // See the License for the specific language governing permissions and // limitations under the License. +use egui::{UserAttentionType, ViewportCommand, WindowLevel}; +use lazy_static::lazy_static; +use parking_lot::RwLock; +use rfd::FileDialog; use std::fs::File; use std::io::Write; -use std::thread; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; -use parking_lot::RwLock; -use lazy_static::lazy_static; -use egui::{UserAttentionType, ViewportCommand, WindowLevel}; -use rfd::FileDialog; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::thread; use crate::gui::platform::PlatformCallbacks; /// Desktop platform related actions. #[derive(Clone)] pub struct Desktop { - /// Context to repaint content and handle viewport commands. - ctx: Arc>>, + /// Context to repaint content and handle viewport commands. + ctx: Arc>>, - /// Cameras amount. - cameras_amount: Arc, - /// Camera index. - camera_index: Arc, - /// Flag to check if camera stop is needed. - stop_camera: Arc, + /// Cameras amount. + cameras_amount: Arc, + /// Camera index. + camera_index: Arc, + /// Flag to check if camera stop is needed. + stop_camera: Arc, - /// Flag to check if attention required after window focusing. - attention_required: Arc, + /// Flag to check if attention required after window focusing. + attention_required: Arc, } impl Desktop { - pub fn new() -> Self { - Self { - ctx: Arc::new(RwLock::new(None)), - cameras_amount: Arc::new(AtomicUsize::new(0)), - camera_index: Arc::new(AtomicUsize::new(0)), - stop_camera: Arc::new(AtomicBool::new(false)), - attention_required: Arc::new(AtomicBool::new(false)), - } - } + pub fn new() -> Self { + Self { + ctx: Arc::new(RwLock::new(None)), + cameras_amount: Arc::new(AtomicUsize::new(0)), + camera_index: Arc::new(AtomicUsize::new(0)), + stop_camera: Arc::new(AtomicBool::new(false)), + attention_required: Arc::new(AtomicBool::new(false)), + } + } - // #[allow(dead_code)] - #[cfg(not(target_os = "macos"))] - fn start_camera_capture(cameras_amount: Arc, - camera_index: Arc, - stop_camera: Arc) { - use nokhwa::Camera; - use nokhwa::pixel_format::RgbFormat; - use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType}; - use nokhwa::utils::ApiBackend; + // #[allow(dead_code)] + #[cfg(not(target_os = "macos"))] + fn start_camera_capture( + cameras_amount: Arc, + camera_index: Arc, + stop_camera: Arc, + ) { + use nokhwa::Camera; + use nokhwa::pixel_format::RgbFormat; + use nokhwa::utils::ApiBackend; + use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType}; - let devices = nokhwa::query(ApiBackend::Auto).unwrap(); - cameras_amount.store(devices.len(), Ordering::Relaxed); - let index = camera_index.load(Ordering::Relaxed); - if devices.is_empty() || index >= devices.len() { - return; - } + let devices = nokhwa::query(ApiBackend::Auto).unwrap(); + cameras_amount.store(devices.len(), Ordering::Relaxed); + let index = camera_index.load(Ordering::Relaxed); + if devices.is_empty() || index >= devices.len() { + return; + } - thread::spawn(move || { - let index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32); - let requested = RequestedFormat::new::( - RequestedFormatType::AbsoluteHighestFrameRate - ); - // Create and open camera. - if let Ok(mut camera) = Camera::new(index, requested) { - if let Ok(_) = camera.open_stream() { - loop { - // Stop if camera was stopped. - if stop_camera.load(Ordering::Relaxed) { - stop_camera.store(false, Ordering::Relaxed); - // Clear image. - let mut w_image = LAST_CAMERA_IMAGE.write(); - *w_image = None; - break; - } - // Get a frame. - if let Ok(frame) = camera.frame() { - // Save image. - let mut w_image = LAST_CAMERA_IMAGE.write(); - *w_image = Some((frame.buffer().to_vec(), 0)); - } else { - // Clear image. - let mut w_image = LAST_CAMERA_IMAGE.write(); - *w_image = None; - break; - } - } - camera.stop_stream().unwrap(); - }; - } - }); - } + thread::spawn(move || { + let index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32); + let requested = + RequestedFormat::new::(RequestedFormatType::AbsoluteHighestFrameRate); + // Create and open camera. + if let Ok(mut camera) = Camera::new(index, requested) { + if let Ok(_) = camera.open_stream() { + loop { + // Stop if camera was stopped. + if stop_camera.load(Ordering::Relaxed) { + stop_camera.store(false, Ordering::Relaxed); + // Clear image. + let mut w_image = LAST_CAMERA_IMAGE.write(); + *w_image = None; + break; + } + // Get a frame. + if let Ok(frame) = camera.frame() { + // Save image. + let mut w_image = LAST_CAMERA_IMAGE.write(); + *w_image = Some((frame.buffer().to_vec(), 0)); + } else { + // Clear image. + let mut w_image = LAST_CAMERA_IMAGE.write(); + *w_image = None; + break; + } + } + camera.stop_stream().unwrap(); + }; + } + }); + } - #[allow(dead_code)] - #[cfg(target_os = "macos")] - fn start_camera_capture(cameras_amount: Arc, - camera_index: Arc, - stop_camera: Arc) { - use nokhwa::nokhwa_initialize; - use nokhwa::pixel_format::RgbFormat; - use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType}; - use nokhwa::utils::ApiBackend; - use nokhwa::query; - use nokhwa::CallbackCamera; + #[allow(dead_code)] + #[cfg(target_os = "macos")] + fn start_camera_capture( + cameras_amount: Arc, + camera_index: Arc, + stop_camera: Arc, + ) { + use nokhwa::CallbackCamera; + use nokhwa::nokhwa_initialize; + use nokhwa::pixel_format::RgbFormat; + use nokhwa::query; + use nokhwa::utils::ApiBackend; + use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType}; - // Ask permission to open camera. - nokhwa_initialize(|_| {}); + // Ask permission to open camera. + nokhwa_initialize(|_| {}); - thread::spawn(move || { - let cameras = query(ApiBackend::Auto).unwrap(); - cameras_amount.store(cameras.len(), Ordering::Relaxed); - let index = camera_index.load(Ordering::Relaxed); - if cameras.is_empty() || index >= cameras.len() { - return; - } - // Start camera. - let camera_index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32); - let camera_callback = CallbackCamera::new( - camera_index, - RequestedFormat::new::(RequestedFormatType::AbsoluteHighestFrameRate), - |_| {} - ); - if let Ok(mut cb) = camera_callback { - if cb.open_stream().is_ok() { - loop { - // Stop if camera was stopped. - if stop_camera.load(Ordering::Relaxed) { - stop_camera.store(false, Ordering::Relaxed); - // Clear image. - let mut w_image = LAST_CAMERA_IMAGE.write(); - *w_image = None; - break; - } - // Get image from camera. - if let Ok(frame) = cb.poll_frame() { - let image = frame.decode_image::().unwrap(); - let mut bytes: Vec = Vec::new(); - let format = image::ImageFormat::Jpeg; - // Convert image to Jpeg format. - image.write_to(&mut std::io::Cursor::new(&mut bytes), format).unwrap(); - let mut w_image = LAST_CAMERA_IMAGE.write(); - *w_image = Some((bytes, 0)); - } else { - // Clear image. - let mut w_image = LAST_CAMERA_IMAGE.write(); - *w_image = None; - break; - } - } - } - } - }); - } + thread::spawn(move || { + let cameras = query(ApiBackend::Auto).unwrap(); + cameras_amount.store(cameras.len(), Ordering::Relaxed); + let index = camera_index.load(Ordering::Relaxed); + if cameras.is_empty() || index >= cameras.len() { + return; + } + // Start camera. + let camera_index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32); + let camera_callback = CallbackCamera::new( + camera_index, + RequestedFormat::new::(RequestedFormatType::AbsoluteHighestFrameRate), + |_| {}, + ); + if let Ok(mut cb) = camera_callback { + if cb.open_stream().is_ok() { + loop { + // Stop if camera was stopped. + if stop_camera.load(Ordering::Relaxed) { + stop_camera.store(false, Ordering::Relaxed); + // Clear image. + let mut w_image = LAST_CAMERA_IMAGE.write(); + *w_image = None; + break; + } + // Get image from camera. + if let Ok(frame) = cb.poll_frame() { + let image = frame.decode_image::().unwrap(); + let mut bytes: Vec = Vec::new(); + let format = image::ImageFormat::Jpeg; + // Convert image to Jpeg format. + image + .write_to(&mut std::io::Cursor::new(&mut bytes), format) + .unwrap(); + let mut w_image = LAST_CAMERA_IMAGE.write(); + *w_image = Some((bytes, 0)); + } else { + // Clear image. + let mut w_image = LAST_CAMERA_IMAGE.write(); + *w_image = None; + break; + } + } + } + } + }); + } } impl PlatformCallbacks for Desktop { - fn set_context(&mut self, ctx: &egui::Context) { - let mut w_ctx = self.ctx.write(); - *w_ctx = Some(ctx.clone()); - } + fn set_context(&mut self, ctx: &egui::Context) { + let mut w_ctx = self.ctx.write(); + *w_ctx = Some(ctx.clone()); + } - fn exit(&self) { - let r_ctx = self.ctx.read(); - if r_ctx.is_some() { - let ctx = r_ctx.as_ref().unwrap(); - ctx.send_viewport_cmd(ViewportCommand::Close); - } - } + fn exit(&self) { + let r_ctx = self.ctx.read(); + if r_ctx.is_some() { + let ctx = r_ctx.as_ref().unwrap(); + ctx.send_viewport_cmd(ViewportCommand::Close); + } + } - fn copy_string_to_buffer(&self, data: String) { - let mut clipboard = arboard::Clipboard::new().unwrap(); - clipboard.set_text(data).unwrap(); - } + fn copy_string_to_buffer(&self, data: String) { + let mut clipboard = arboard::Clipboard::new().unwrap(); + clipboard.set_text(data).unwrap(); + } - fn get_string_from_buffer(&self) -> String { - let mut clipboard = arboard::Clipboard::new().unwrap(); - clipboard.get_text().unwrap_or("".to_string()) - } + fn get_string_from_buffer(&self) -> String { + let mut clipboard = arboard::Clipboard::new().unwrap(); + clipboard.get_text().unwrap_or("".to_string()) + } - fn start_camera(&self) { - // Clear image. - { - let mut w_image = LAST_CAMERA_IMAGE.write(); - *w_image = None; - } - // Setup stop camera flag. - let stop_camera = self.stop_camera.clone(); - stop_camera.store(false, Ordering::Relaxed); + fn start_camera(&self) { + // Clear image. + { + let mut w_image = LAST_CAMERA_IMAGE.write(); + *w_image = None; + } + // Setup stop camera flag. + let stop_camera = self.stop_camera.clone(); + stop_camera.store(false, Ordering::Relaxed); - Self::start_camera_capture(self.cameras_amount.clone(), - self.camera_index.clone(), - stop_camera); - } + Self::start_camera_capture( + self.cameras_amount.clone(), + self.camera_index.clone(), + stop_camera, + ); + } - fn stop_camera(&self) { - // Stop camera. - self.stop_camera.store(true, Ordering::Relaxed); - } + fn stop_camera(&self) { + // Stop camera. + self.stop_camera.store(true, Ordering::Relaxed); + } - fn camera_image(&self) -> Option<(Vec, u32)> { - let r_image = LAST_CAMERA_IMAGE.read(); - if r_image.is_some() { - return r_image.clone(); - } - None - } + fn camera_image(&self) -> Option<(Vec, u32)> { + let r_image = LAST_CAMERA_IMAGE.read(); + if r_image.is_some() { + return r_image.clone(); + } + None + } - fn can_switch_camera(&self) -> bool { - let amount = self.cameras_amount.load(Ordering::Relaxed); - amount > 1 - } + fn can_switch_camera(&self) -> bool { + let amount = self.cameras_amount.load(Ordering::Relaxed); + amount > 1 + } - fn switch_camera(&self) { - self.stop_camera(); - let index = self.camera_index.load(Ordering::Relaxed); - let amount = self.cameras_amount.load(Ordering::Relaxed); - if index == amount - 1 { - self.camera_index.store(0, Ordering::Relaxed); - } else { - self.camera_index.store(index + 1, Ordering::Relaxed); - } - self.start_camera(); - } + fn switch_camera(&self) { + self.stop_camera(); + let index = self.camera_index.load(Ordering::Relaxed); + let amount = self.cameras_amount.load(Ordering::Relaxed); + if index == amount - 1 { + self.camera_index.store(0, Ordering::Relaxed); + } else { + self.camera_index.store(index + 1, Ordering::Relaxed); + } + self.start_camera(); + } - fn share_data(&self, name: String, data: Vec) -> Result<(), std::io::Error> { - let folder = FileDialog::new() - .set_title(t!("share")) - .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(()) - } + fn share_data(&self, name: String, data: Vec) -> Result<(), std::io::Error> { + let folder = FileDialog::new() + .set_title(t!("share")) + .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(()) + } - fn pick_file(&self) -> Option { - let file = FileDialog::new() - .set_title(t!("choose_file")) - .set_directory(dirs::home_dir().unwrap()) - .pick_file(); - if let Some(file) = file { - return Some(file.to_str().unwrap_or_default().to_string()); - } - None - } + fn pick_file(&self) -> Option { + let file = FileDialog::new() + .set_title(t!("choose_file")) + .set_directory(dirs::home_dir().unwrap()) + .pick_file(); + if let Some(file) = file { + return Some(file.to_str().unwrap_or_default().to_string()); + } + None + } - fn pick_folder(&self) -> Option { - let file = FileDialog::new() - .set_title(t!("choose_folder")) - .set_directory(dirs::home_dir().unwrap()) - .pick_folder(); - if let Some(file) = file { - return Some(file.to_str().unwrap_or_default().to_string()); - } - None - } + fn pick_folder(&self) -> Option { + let file = FileDialog::new() + .set_title(t!("choose_folder")) + .set_directory(dirs::home_dir().unwrap()) + .pick_folder(); + if let Some(file) = file { + return Some(file.to_str().unwrap_or_default().to_string()); + } + None + } - fn picked_file(&self) -> Option { - None - } + fn picked_file(&self) -> Option { + None + } - fn request_user_attention(&self) { - let r_ctx = self.ctx.read(); - if r_ctx.is_some() { - let ctx = r_ctx.as_ref().unwrap(); - // Request attention on taskbar. - ctx.send_viewport_cmd( - ViewportCommand::RequestUserAttention(UserAttentionType::Informational) - ); - // Un-minimize window. - if ctx.input(|i| i.viewport().minimized.unwrap_or(false)) { - ctx.send_viewport_cmd(ViewportCommand::Minimized(false)); - } - // Focus to window. - if !ctx.input(|i| i.viewport().focused.unwrap_or(false)) { - ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::AlwaysOnTop)); - ctx.send_viewport_cmd(ViewportCommand::Focus); - } - ctx.request_repaint(); - } - self.attention_required.store(true, Ordering::Relaxed); - } + fn request_user_attention(&self) { + let r_ctx = self.ctx.read(); + if r_ctx.is_some() { + let ctx = r_ctx.as_ref().unwrap(); + // Request attention on taskbar. + ctx.send_viewport_cmd(ViewportCommand::RequestUserAttention( + UserAttentionType::Informational, + )); + // Un-minimize window. + if ctx.input(|i| i.viewport().minimized.unwrap_or(false)) { + ctx.send_viewport_cmd(ViewportCommand::Minimized(false)); + } + // Focus to window. + if !ctx.input(|i| i.viewport().focused.unwrap_or(false)) { + ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::AlwaysOnTop)); + ctx.send_viewport_cmd(ViewportCommand::Focus); + } + ctx.request_repaint(); + } + self.attention_required.store(true, Ordering::Relaxed); + } - fn user_attention_required(&self) -> bool { - self.attention_required.load(Ordering::Relaxed) - } + fn user_attention_required(&self) -> bool { + self.attention_required.load(Ordering::Relaxed) + } - fn clear_user_attention(&self) { - let r_ctx = self.ctx.read(); - if r_ctx.is_some() { - let ctx = r_ctx.as_ref().unwrap(); - ctx.send_viewport_cmd( - ViewportCommand::RequestUserAttention(UserAttentionType::Reset) - ); - ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::Normal)); - } - self.attention_required.store(false, Ordering::Relaxed); - } + fn clear_user_attention(&self) { + let r_ctx = self.ctx.read(); + if r_ctx.is_some() { + let ctx = r_ctx.as_ref().unwrap(); + ctx.send_viewport_cmd(ViewportCommand::RequestUserAttention( + UserAttentionType::Reset, + )); + ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::Normal)); + } + self.attention_required.store(false, Ordering::Relaxed); + } } lazy_static! { - /// Last captured image from started camera. - static ref LAST_CAMERA_IMAGE: Arc, u32)>>> = Arc::new(RwLock::new(None)); + /// Last captured image from started camera. + static ref LAST_CAMERA_IMAGE: Arc, u32)>>> = Arc::new(RwLock::new(None)); } diff --git a/src/gui/platform/mod.rs b/src/gui/platform/mod.rs index 249d2040..1cbd520e 100644 --- a/src/gui/platform/mod.rs +++ b/src/gui/platform/mod.rs @@ -22,20 +22,20 @@ pub mod platform; pub mod platform; pub trait PlatformCallbacks { - fn set_context(&mut self, ctx: &egui::Context); - fn exit(&self); - fn copy_string_to_buffer(&self, data: String); - fn get_string_from_buffer(&self) -> String; - fn start_camera(&self); - fn stop_camera(&self); - 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>; - fn pick_file(&self) -> Option; - fn pick_folder(&self) -> Option; - fn picked_file(&self) -> Option; - fn request_user_attention(&self); - fn user_attention_required(&self) -> bool; - fn clear_user_attention(&self); -} \ No newline at end of file + fn set_context(&mut self, ctx: &egui::Context); + fn exit(&self); + fn copy_string_to_buffer(&self, data: String); + fn get_string_from_buffer(&self) -> String; + fn start_camera(&self); + fn stop_camera(&self); + 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>; + fn pick_file(&self) -> Option; + fn pick_folder(&self) -> Option; + fn picked_file(&self) -> Option; + fn request_user_attention(&self); + fn user_attention_required(&self) -> bool; + fn clear_user_attention(&self); +} diff --git a/src/gui/views/camera.rs b/src/gui/views/camera.rs index 7496ca58..49ef2808 100644 --- a/src/gui/views/camera.rs +++ b/src/gui/views/camera.rs @@ -22,425 +22,429 @@ use parking_lot::RwLock; use std::sync::Arc; use std::thread; +use crate::gui::Colors; use crate::gui::icons::CAMERA_ROTATE; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::types::{QrScanResult, QrScanState}; use crate::gui::views::View; -use crate::gui::Colors; -use crate::wallet::types::PhraseSize; +use crate::gui::views::types::{QrScanResult, QrScanState}; use crate::wallet::WalletUtils; +use crate::wallet::types::PhraseSize; /// Camera QR code scanner. pub struct CameraContent { - /// QR code scanning progress and result. - qr_scan_state: Arc>, - /// Uniform Resources URIs collected from QR code scanning. - ur_data: Arc, usize)>>> + /// QR code scanning progress and result. + qr_scan_state: Arc>, + /// Uniform Resources URIs collected from QR code scanning. + ur_data: Arc, usize)>>>, } impl Default for CameraContent { - fn default() -> Self { - Self { - qr_scan_state: Arc::new(RwLock::new(QrScanState::default())), - ur_data: Arc::new(RwLock::new(None)) - } - } + fn default() -> Self { + Self { + qr_scan_state: Arc::new(RwLock::new(QrScanState::default())), + ur_data: Arc::new(RwLock::new(None)), + } + } } impl CameraContent { - /// Draw camera content. - pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - let rect = if let Some(img_data) = cb.camera_image() { - if let Ok(img) = image::load_from_memory(&*img_data.0) { - // Process image to find QR code. - self.scan_qr(&img); + /// Draw camera content. + pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + let rect = if let Some(img_data) = cb.camera_image() { + if let Ok(img) = image::load_from_memory(&*img_data.0) { + // Process image to find QR code. + self.scan_qr(&img); - // Draw image. - let img_rect = self.image_ui(ui, img, img_data.1); + // Draw image. + let img_rect = self.image_ui(ui, img, img_data.1); - img_rect - } else { - self.loading_ui(ui) - } - } else { - self.loading_ui(ui) - }; + img_rect + } else { + self.loading_ui(ui) + } + } else { + self.loading_ui(ui) + }; - // Show UR scan progress. - self.ur_progress_ui(ui, &rect); + // Show UR scan progress. + self.ur_progress_ui(ui, &rect); - // Show button to switch cameras. - if cb.can_switch_camera() { - let r = { - let mut r = rect.clone(); - r.min.y = r.max.y - 52.0; - r.min.x = r.max.x - 52.0; - r - }; - ui.scope_builder(UiBuilder::new().max_rect(r), |ui| { - let rotate_img = CAMERA_ROTATE.to_string(); - View::button(ui, rotate_img, Colors::white_or_black(false), || { - cb.switch_camera(); - }); - }); - } - ui.add_space(6.0); - ui.ctx().request_repaint(); - } + // Show button to switch cameras. + if cb.can_switch_camera() { + let r = { + let mut r = rect.clone(); + r.min.y = r.max.y - 52.0; + r.min.x = r.max.x - 52.0; + r + }; + ui.scope_builder(UiBuilder::new().max_rect(r), |ui| { + let rotate_img = CAMERA_ROTATE.to_string(); + View::button(ui, rotate_img, Colors::white_or_black(false), || { + cb.switch_camera(); + }); + }); + } + ui.add_space(6.0); + ui.ctx().request_repaint(); + } - /// Draw camera image. - fn image_ui(&mut self, ui: &mut egui::Ui, mut img: DynamicImage, rotation: u32) -> Rect { - // Setup image rotation. - img = match rotation { - 90 => img.rotate90(), - 180 => img.rotate180(), - 270 => img.rotate270(), - _ => img - }; - if View::is_desktop() { - img = img.fliph(); - } - // Convert to ColorImage. - let color_img = match &img { - DynamicImage::ImageRgb8(image) => { - egui::ColorImage::from_rgb( - [image.width() as usize, image.height() as usize], - image.as_bytes(), - ) - }, - other => { - let image = other.to_rgba8(); - egui::ColorImage::from_rgba_unmultiplied( - [image.width() as usize, image.height() as usize], - image.as_bytes(), - ) - }, - }; - // Create image texture. - let texture = ui.ctx().load_texture("camera_image", - color_img.clone(), - TextureOptions::default()); - let img_size = egui::emath::vec2(color_img.width() as f32, - color_img.height() as f32); - let sized_img = SizedTexture::new(texture.id(), img_size); - egui::Image::from_texture(sized_img) - // Setup to crop image at square. - .uv(Rect::from([ - if img_size.y > img_size.x { - Pos2::new(0.0, 1.0 - (img_size.x / img_size.y)) - } else { - Pos2::new(1.0 - (img_size.y / img_size.x), 0.0) - }, - Pos2::new(1.0, 1.0) - ])) - .max_height(ui.available_width()) - .maintain_aspect_ratio(false) - .shrink_to_fit() - .ui(ui).rect - } + /// Draw camera image. + fn image_ui(&mut self, ui: &mut egui::Ui, mut img: DynamicImage, rotation: u32) -> Rect { + // Setup image rotation. + img = match rotation { + 90 => img.rotate90(), + 180 => img.rotate180(), + 270 => img.rotate270(), + _ => img, + }; + if View::is_desktop() { + img = img.fliph(); + } + // Convert to ColorImage. + let color_img = match &img { + DynamicImage::ImageRgb8(image) => egui::ColorImage::from_rgb( + [image.width() as usize, image.height() as usize], + image.as_bytes(), + ), + other => { + let image = other.to_rgba8(); + egui::ColorImage::from_rgba_unmultiplied( + [image.width() as usize, image.height() as usize], + image.as_bytes(), + ) + } + }; + // Create image texture. + let texture = + ui.ctx() + .load_texture("camera_image", color_img.clone(), TextureOptions::default()); + let img_size = egui::emath::vec2(color_img.width() as f32, color_img.height() as f32); + let sized_img = SizedTexture::new(texture.id(), img_size); + egui::Image::from_texture(sized_img) + // Setup to crop image at square. + .uv(Rect::from([ + if img_size.y > img_size.x { + Pos2::new(0.0, 1.0 - (img_size.x / img_size.y)) + } else { + Pos2::new(1.0 - (img_size.y / img_size.x), 0.0) + }, + Pos2::new(1.0, 1.0), + ])) + .max_height(ui.available_width()) + .maintain_aspect_ratio(false) + .shrink_to_fit() + .ui(ui) + .rect + } - /// Draw animated QR code scanning progress. - fn ur_progress_ui(&self, ui: &mut egui::Ui, rect: &Rect) { - let show_ur_progress = { - self.ur_data.as_ref().read().is_some() - }; - if show_ur_progress { - ui.scope_builder(UiBuilder::new().max_rect(rect.clone()), |ui| { - ui.centered_and_justified(|ui| { - ui.label(RichText::new(format!("{}%", self.ur_progress())) - .size(32.0) - .color(Colors::gold_dark())); - }); - }); - } - } + /// Draw animated QR code scanning progress. + fn ur_progress_ui(&self, ui: &mut egui::Ui, rect: &Rect) { + let show_ur_progress = { self.ur_data.as_ref().read().is_some() }; + if show_ur_progress { + ui.scope_builder(UiBuilder::new().max_rect(rect.clone()), |ui| { + ui.centered_and_justified(|ui| { + ui.label( + RichText::new(format!("{}%", self.ur_progress())) + .size(32.0) + .color(Colors::gold_dark()), + ); + }); + }); + } + } - /// Draw camera loading progress content. - fn loading_ui(&self, ui: &mut egui::Ui) -> Rect { - 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); - }).response.rect - } + /// Draw camera loading progress content. + fn loading_ui(&self, ui: &mut egui::Ui) -> Rect { + 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); + }) + .response + .rect + } - /// Check if image is processing to find QR code. - fn image_processing(&self) -> bool { - let r_scan = self.qr_scan_state.read(); - r_scan.image_processing - } + /// Check if image is processing to find QR code. + fn image_processing(&self) -> bool { + let r_scan = self.qr_scan_state.read(); + r_scan.image_processing + } - /// Get UR scanning progress in percents. - fn ur_progress(&self) -> i32 { - // Setup data. - let r_data = self.ur_data.read(); - let (data, total) = r_data.clone().unwrap_or((vec![], 0)); - if data.is_empty() { - return 0; - } - // Calculate progress. - let mut complete = 0; - for i in &data { - if !i.is_empty() { - complete += 1; - } - } - (100 * complete / total) as i32 - } + /// Get UR scanning progress in percents. + fn ur_progress(&self) -> i32 { + // Setup data. + let r_data = self.ur_data.read(); + let (data, total) = r_data.clone().unwrap_or((vec![], 0)); + if data.is_empty() { + return 0; + } + // Calculate progress. + let mut complete = 0; + for i in &data { + if !i.is_empty() { + complete += 1; + } + } + (100 * complete / total) as i32 + } - /// Parse QR code from provided image data. - fn scan_qr(&self, image_data: &DynamicImage) { - // Do not scan when another image is processing. - if self.image_processing() { - return; - } - // Setup scanning flag. - { - let mut w_scan = self.qr_scan_state.write(); - w_scan.image_processing = true; - } + /// Parse QR code from provided image data. + fn scan_qr(&self, image_data: &DynamicImage) { + // Do not scan when another image is processing. + if self.image_processing() { + return; + } + // Setup scanning flag. + { + let mut w_scan = self.qr_scan_state.write(); + w_scan.image_processing = true; + } - let image_data = image_data.clone(); - let qr_scan_state = self.qr_scan_state.clone(); - let ur_data = self.ur_data.clone(); + let image_data = image_data.clone(); + let qr_scan_state = self.qr_scan_state.clone(); + let ur_data = self.ur_data.clone(); - let on_scan = async move { - // Prepare image data. - let img = image_data.to_luma8(); - let mut img: rqrr::PreparedImage = rqrr::PreparedImage::prepare(img); - // Scan and save results. - let grids = img.detect_grids(); - if let Some(g) = grids.get(0) { - let mut qr_data = vec![]; - if let Ok(_) = g.decode_to(&mut qr_data) { - // Setup scanned data into text. - let text = String::from_utf8(qr_data.clone()).unwrap_or("".to_string()); - // Setup current text. - let cur_text = { - let r_scan = qr_scan_state.read(); - let text = if let Some(res) = r_scan.qr_scan_result.clone() { - res.text() - } else { - "".to_string() - }; - text - }; - // Parse non-empty data if parsed text is different from saved. - if !qr_data.is_empty() && (cur_text.is_empty() || text != cur_text) { - let res = Self::parse_qr_code(qr_data); - match res { - QrScanResult::URPart(uri, index, total) => { - // Setup current UR data. - let mut cur_data = { - let r_data = ur_data.read(); - let mut cur_data = vec!["".to_string(); total]; - if let Some((d, _)) = r_data.clone() { - cur_data = d; - } - cur_data - }; - if !cur_data.contains(&uri) { - // Save part of UR data. - { - cur_data.insert(index, uri); - let mut w_data = ur_data.write(); - *w_data = Some((cur_data.clone(), total)); - } - // Setup UR decoder. - let mut decoder = ur::Decoder::default(); - for m in cur_data { - if !m.is_empty() { - if let Ok(_) = decoder.receive(m.as_str()) { - continue; - } else { - break; - } - } - } - // Check if UR data is complete. - if decoder.complete() { - if let Ok(data) = decoder.message() { - // Parse complete data. - let res = Self::parse_qr_code(data.unwrap_or(vec![])); - // Clean UR data. - let mut w_data = ur_data.write(); - *w_data = None; - // Save scan result. - let mut w_scan = qr_scan_state.write(); - w_scan.qr_scan_result = Some(res); - return; - } - } - } - } - _ => { - // Clean UR data. - let mut w_data = ur_data.write(); - *w_data = None; - // Save scan result. - let mut w_scan = qr_scan_state.write(); - w_scan.qr_scan_result = Some(res); - return; - } - } - } - } - } - // Reset scanning flag to process again. - { - let mut w_scan = qr_scan_state.write(); - w_scan.image_processing = false; - } - }; + let on_scan = async move { + // Prepare image data. + let img = image_data.to_luma8(); + let mut img: rqrr::PreparedImage = rqrr::PreparedImage::prepare(img); + // Scan and save results. + let grids = img.detect_grids(); + if let Some(g) = grids.get(0) { + let mut qr_data = vec![]; + if let Ok(_) = g.decode_to(&mut qr_data) { + // Setup scanned data into text. + let text = String::from_utf8(qr_data.clone()).unwrap_or("".to_string()); + // Setup current text. + let cur_text = { + let r_scan = qr_scan_state.read(); + let text = if let Some(res) = r_scan.qr_scan_result.clone() { + res.text() + } else { + "".to_string() + }; + text + }; + // Parse non-empty data if parsed text is different from saved. + if !qr_data.is_empty() && (cur_text.is_empty() || text != cur_text) { + let res = Self::parse_qr_code(qr_data); + match res { + QrScanResult::URPart(uri, index, total) => { + // Setup current UR data. + let mut cur_data = { + let r_data = ur_data.read(); + let mut cur_data = vec!["".to_string(); total]; + if let Some((d, _)) = r_data.clone() { + cur_data = d; + } + cur_data + }; + if !cur_data.contains(&uri) { + // Save part of UR data. + { + cur_data.insert(index, uri); + let mut w_data = ur_data.write(); + *w_data = Some((cur_data.clone(), total)); + } + // Setup UR decoder. + let mut decoder = ur::Decoder::default(); + for m in cur_data { + if !m.is_empty() { + if let Ok(_) = decoder.receive(m.as_str()) { + continue; + } else { + break; + } + } + } + // Check if UR data is complete. + if decoder.complete() { + if let Ok(data) = decoder.message() { + // Parse complete data. + let res = Self::parse_qr_code(data.unwrap_or(vec![])); + // Clean UR data. + let mut w_data = ur_data.write(); + *w_data = None; + // Save scan result. + let mut w_scan = qr_scan_state.write(); + w_scan.qr_scan_result = Some(res); + return; + } + } + } + } + _ => { + // Clean UR data. + let mut w_data = ur_data.write(); + *w_data = None; + // Save scan result. + let mut w_scan = qr_scan_state.write(); + w_scan.qr_scan_result = Some(res); + return; + } + } + } + } + } + // Reset scanning flag to process again. + { + let mut w_scan = qr_scan_state.write(); + w_scan.image_processing = false; + } + }; - // Launch scanner at separate thread. - thread::spawn(move || { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(on_scan); - }); - } + // Launch scanner at separate thread. + thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(on_scan); + }); + } - /// Parse QR code scan result. - fn parse_qr_code(data: Vec) -> QrScanResult { - // Check if string starts with Grin address prefix. - let text_string = String::from_utf8(data.clone()).unwrap_or("".to_string()); - let text = text_string.trim(); - if text.starts_with("tgrin") || text.starts_with("grin") { - if SlatepackAddress::try_from(text).is_ok() { - return QrScanResult::Address(ZeroingString::from(text)); - } - } + /// Parse QR code scan result. + fn parse_qr_code(data: Vec) -> QrScanResult { + // Check if string starts with Grin address prefix. + let text_string = String::from_utf8(data.clone()).unwrap_or("".to_string()); + let text = text_string.trim(); + if text.starts_with("tgrin") || text.starts_with("grin") { + if SlatepackAddress::try_from(text).is_ok() { + return QrScanResult::Address(ZeroingString::from(text)); + } + } - // Check if string contains Slatepack message prefix and postfix. - if text.starts_with("BEGINSLATEPACK.") && text.ends_with("ENDSLATEPACK.") { - return QrScanResult::Slatepack(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()); + } - // Check Uniform Resource data. - // https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md - if text.starts_with("ur:bytes/") { - let split = text.split("/").collect::>(); - if let Some(index_total) = split.get(1) { - if let Some((index, total)) = index_total.split_once("-") { - let index = index.parse::(); - let total = total.parse::(); - if index.is_ok() && total.is_ok() { - let index = index.unwrap() - 1; - let total = total.unwrap(); - return QrScanResult::URPart(text_string, index, total); - } - } - } - } + // Check Uniform Resource data. + // https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md + if text.starts_with("ur:bytes/") { + let split = text.split("/").collect::>(); + if let Some(index_total) = split.get(1) { + if let Some((index, total)) = index_total.split_once("-") { + let index = index.parse::(); + let total = total.parse::(); + if index.is_ok() && total.is_ok() { + let index = index.unwrap() - 1; + let total = total.unwrap(); + return QrScanResult::URPart(text_string, index, total); + } + } + } + } - // Check Compact SeedQR format. - // https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#compactseedqr-specification - if data.len() <= 32 && 16 <= data.len() && data.len() % 4 == 0 { - // Setup words amount. - let total_bits = data.len() * 8; - let checksum_bits = total_bits / 32; - let total_words = (total_bits + checksum_bits) / 11; - // Setup entropy. - let mut entropy = data.clone(); - WalletUtils::setup_checksum(&mut entropy); - // Setup bits. - let mut bits = vec![false; entropy.len() * 8]; - for i in 0..entropy.len() { - for j in 0..8 { - bits[(i * 8) + j] = (entropy[i] & (1 << (7 - j))) != 0; - } - } - // Extract word index. - let extract_index = |i: usize| -> usize { - let mut index = 0; - for j in 0..11 { - index = index << 1; - if bits[(i * 11) + j] { - index += 1; - } - } - return index; - }; - // Setup words. - let mut words = "".to_string(); - for n in 0..total_words { - // Setup word index. - let index = extract_index(n); - // Setup word. - let empty_word = "".to_string(); - let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone(); - if word.is_empty() { - words = empty_word; - break; - } - words = if words.is_empty() { - format!("{}", word) - } else { - format!("{} {}", words, word) - }; - } - if !words.is_empty() { - return QrScanResult::SeedQR(ZeroingString::from(words)); - } - } + // Check Compact SeedQR format. + // https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#compactseedqr-specification + if data.len() <= 32 && 16 <= data.len() && data.len() % 4 == 0 { + // Setup words amount. + let total_bits = data.len() * 8; + let checksum_bits = total_bits / 32; + let total_words = (total_bits + checksum_bits) / 11; + // Setup entropy. + let mut entropy = data.clone(); + WalletUtils::setup_checksum(&mut entropy); + // Setup bits. + let mut bits = vec![false; entropy.len() * 8]; + for i in 0..entropy.len() { + for j in 0..8 { + bits[(i * 8) + j] = (entropy[i] & (1 << (7 - j))) != 0; + } + } + // Extract word index. + let extract_index = |i: usize| -> usize { + let mut index = 0; + for j in 0..11 { + index = index << 1; + if bits[(i * 11) + j] { + index += 1; + } + } + return index; + }; + // Setup words. + let mut words = "".to_string(); + for n in 0..total_words { + // Setup word index. + let index = extract_index(n); + // Setup word. + let empty_word = "".to_string(); + let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone(); + if word.is_empty() { + words = empty_word; + break; + } + words = if words.is_empty() { + format!("{}", word) + } else { + format!("{} {}", words, word) + }; + } + if !words.is_empty() { + return QrScanResult::SeedQR(ZeroingString::from(words)); + } + } - // Check Standard SeedQR format. - // https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#standard-seedqr-specification - let only_numbers = || { - for c in text.chars() { - if !c.is_numeric() { - return false; - } - } - true - }; - if !text.is_empty() && data.len() <= 96 && data.len() % 4 == 0 && only_numbers() { - if let Some(_) = PhraseSize::type_for_value(text.len() / 4) { - let chars: Vec = text.trim().chars().collect(); - let split = &chars.chunks(4) - .map(|chunk| chunk.iter().collect::() - .trim() - .trim_start_matches("0") - .to_string() - ) - .collect::>(); - let mut words = "".to_string(); - for i in split { - let index = if i.is_empty() { - 0usize - } else { - i.parse::().unwrap_or(WORDS.len()) - }; - let empty_word = "".to_string(); - let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone(); - // Return text result when BIP39 word was not found. - if word.is_empty() { - return QrScanResult::Text(ZeroingString::from(text)); - } - words = if words.is_empty() { - format!("{}", word) - } else { - format!("{} {}", words, word) - }; - } - return QrScanResult::SeedQR(ZeroingString::from(words)); - } - } + // Check Standard SeedQR format. + // https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#standard-seedqr-specification + let only_numbers = || { + for c in text.chars() { + if !c.is_numeric() { + return false; + } + } + true + }; + if !text.is_empty() && data.len() <= 96 && data.len() % 4 == 0 && only_numbers() { + if let Some(_) = PhraseSize::type_for_value(text.len() / 4) { + let chars: Vec = text.trim().chars().collect(); + let split = &chars + .chunks(4) + .map(|chunk| { + chunk + .iter() + .collect::() + .trim() + .trim_start_matches("0") + .to_string() + }) + .collect::>(); + let mut words = "".to_string(); + for i in split { + let index = if i.is_empty() { + 0usize + } else { + i.parse::().unwrap_or(WORDS.len()) + }; + let empty_word = "".to_string(); + let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone(); + // Return text result when BIP39 word was not found. + if word.is_empty() { + return QrScanResult::Text(ZeroingString::from(text)); + } + words = if words.is_empty() { + format!("{}", word) + } else { + format!("{} {}", words, word) + }; + } + return QrScanResult::SeedQR(ZeroingString::from(words)); + } + } - // Return default text result. - QrScanResult::Text(ZeroingString::from(text)) - } + // Return default text result. + QrScanResult::Text(ZeroingString::from(text)) + } - /// Get QR code scan result. - pub fn qr_scan_result(&self) -> Option { - let r_scan = self.qr_scan_state.read(); - if r_scan.qr_scan_result.is_some() { - return Some(r_scan.qr_scan_result.clone().unwrap()); - } - None - } -} \ No newline at end of file + /// Get QR code scan result. + pub fn qr_scan_result(&self) -> Option { + let r_scan = self.qr_scan_state.read(); + if r_scan.qr_scan_result.is_some() { + return Some(r_scan.qr_scan_result.clone().unwrap()); + } + None + } +} diff --git a/src/gui/views/content.rs b/src/gui/views/content.rs index fb9d478a..77b5c138 100644 --- a/src/gui/views/content.rs +++ b/src/gui/views/content.rs @@ -12,57 +12,57 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::os::OperatingSystem; use egui::RichText; +use egui::os::OperatingSystem; use lazy_static::lazy_static; use std::fs; use std::sync::atomic::{AtomicBool, Ordering}; +use crate::gui::Colors; use crate::gui::icons::FILE_X; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::network::NetworkContent; use crate::gui::views::types::{ContentContainer, ModalPosition}; use crate::gui::views::wallets::WalletsContent; use crate::gui::views::{Modal, View}; -use crate::gui::Colors; use crate::node::Node; use crate::{AppConfig, Settings}; lazy_static! { - /// Global state to check if [`NetworkContent`] panel is open. - static ref NETWORK_PANEL_OPEN: AtomicBool = AtomicBool::new(false); + /// Global state to check if [`NetworkContent`] panel is open. + static ref NETWORK_PANEL_OPEN: AtomicBool = AtomicBool::new(false); } /// Contains main ui content, handles side panel state. pub struct Content { - /// Side panel [`NetworkContent`] content. - network: NetworkContent, + /// Side panel [`NetworkContent`] content. + network: NetworkContent, - /// Central panel [`WalletsContent`] content. - wallets: WalletsContent, + /// Central panel [`WalletsContent`] content. + wallets: WalletsContent, - /// Check if app exit is allowed on Desktop close event. - pub exit_allowed: bool, - /// Flag to show exit progress at [`Modal`]. - show_exit_progress: bool, + /// Check if app exit is allowed on Desktop close event. + pub exit_allowed: bool, + /// Flag to show exit progress at [`Modal`]. + show_exit_progress: bool, - /// Flag to check it's first draw of content. - first_draw: bool, + /// Flag to check it's first draw of content. + first_draw: bool, } impl Default for Content { - fn default() -> Self { - // Exit from eframe only for non-mobile platforms. - let os = OperatingSystem::from_target_os(); - let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS; - Self { - network: NetworkContent::default(), - wallets: WalletsContent::default(), - exit_allowed, - show_exit_progress: false, - first_draw: true, - } - } + fn default() -> Self { + // Exit from eframe only for non-mobile platforms. + let os = OperatingSystem::from_target_os(); + let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS; + Self { + network: NetworkContent::default(), + wallets: WalletsContent::default(), + exit_allowed, + show_exit_progress: false, + first_draw: true, + } + } } /// Identifier for integrated node warning [`Modal`] on Android. @@ -71,250 +71,281 @@ const ANDROID_INTEGRATED_NODE_WARNING_MODAL: &'static str = "android_node_warnin const CRASH_REPORT_MODAL: &'static str = "crash_report_modal"; impl ContentContainer for Content { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - Self::EXIT_CONFIRMATION_MODAL, - ANDROID_INTEGRATED_NODE_WARNING_MODAL, - CRASH_REPORT_MODAL - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![ + Self::EXIT_CONFIRMATION_MODAL, + ANDROID_INTEGRATED_NODE_WARNING_MODAL, + CRASH_REPORT_MODAL, + ] + } - fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - match modal.id { - Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb), - ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui), - CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, cb), - _ => {} - } - } + fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + match modal.id { + Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb), + ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui), + CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, cb), + _ => {} + } + } - fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - let dual_panel = Self::is_dual_panel_mode(ui.ctx()); - let (is_panel_open, mut panel_width) = network_panel_state_width(ui.ctx(), dual_panel); - if self.network.showing_settings() { - panel_width = ui.available_width(); - } + fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + let dual_panel = Self::is_dual_panel_mode(ui.ctx()); + let (is_panel_open, mut panel_width) = network_panel_state_width(ui.ctx(), dual_panel); + if self.network.showing_settings() { + panel_width = ui.available_width(); + } - // Show network content. - egui::SidePanel::left("network_panel") - .resizable(false) - .exact_width(panel_width) - .frame(egui::Frame { - ..Default::default() - }) - .show_animated_inside(ui, is_panel_open, |ui| { - self.network.ui(ui, cb); - }); + // Show network content. + egui::SidePanel::left("network_panel") + .resizable(false) + .exact_width(panel_width) + .frame(egui::Frame { + ..Default::default() + }) + .show_animated_inside(ui, is_panel_open, |ui| { + self.network.ui(ui, cb); + }); - // Show wallets content. - egui::CentralPanel::default() - .frame(egui::Frame { - ..Default::default() - }) - .show_inside(ui, |ui| { - self.wallets.ui(ui, cb); - }); + // Show wallets content. + egui::CentralPanel::default() + .frame(egui::Frame { + ..Default::default() + }) + .show_inside(ui, |ui| { + self.wallets.ui(ui, cb); + }); - if self.first_draw { - // Show crash report or integrated node Android warning. - if Settings::crash_check_path().exists() { - Modal::new(CRASH_REPORT_MODAL) - .closeable(false) - .position(ModalPosition::Center) - .title(t!("crash_report")) - .show(); - } else if OperatingSystem::from_target_os() == OperatingSystem::Android && - AppConfig::android_integrated_node_warning_needed() { - Modal::new(ANDROID_INTEGRATED_NODE_WARNING_MODAL) - .title(t!("network.node")) - .show(); - } - self.first_draw = false; - } - } + if self.first_draw { + // Show crash report or integrated node Android warning. + if Settings::crash_check_path().exists() { + Modal::new(CRASH_REPORT_MODAL) + .closeable(false) + .position(ModalPosition::Center) + .title(t!("crash_report")) + .show(); + } else if OperatingSystem::from_target_os() == OperatingSystem::Android + && AppConfig::android_integrated_node_warning_needed() + { + Modal::new(ANDROID_INTEGRATED_NODE_WARNING_MODAL) + .title(t!("network.node")) + .show(); + } + self.first_draw = false; + } + } } impl Content { - /// Default width of side panel at application UI. - pub const SIDE_PANEL_WIDTH: f32 = 400.0; - /// Desktop window title height. - pub const WINDOW_TITLE_HEIGHT: f32 = 38.0; - /// Margin of window frame at desktop. - pub const WINDOW_FRAME_MARGIN: f32 = 6.0; + /// Default width of side panel at application UI. + pub const SIDE_PANEL_WIDTH: f32 = 400.0; + /// Desktop window title height. + pub const WINDOW_TITLE_HEIGHT: f32 = 38.0; + /// Margin of window frame at desktop. + pub const WINDOW_FRAME_MARGIN: f32 = 6.0; - /// Identifier for exit confirmation [`Modal`]. - pub const EXIT_CONFIRMATION_MODAL: &'static str = "exit_confirmation_modal"; + /// Identifier for exit confirmation [`Modal`]. + pub const EXIT_CONFIRMATION_MODAL: &'static str = "exit_confirmation_modal"; - /// Called to navigate back, return `true` if action was not consumed. - pub fn on_back(&mut self, ctx: &egui::Context, cb: &dyn PlatformCallbacks) -> bool { - if Modal::on_back() { - let dual_panel = Self::is_dual_panel_mode(ctx); - if !dual_panel && Self::is_network_panel_open() { - if self.network.on_back() { - Self::toggle_network_panel(); - return false; - } - } else if self.wallets.on_back(cb) { - Self::show_exit_modal(); - return false; - } - } - true - } + /// Called to navigate back, return `true` if action was not consumed. + pub fn on_back(&mut self, ctx: &egui::Context, cb: &dyn PlatformCallbacks) -> bool { + if Modal::on_back() { + let dual_panel = Self::is_dual_panel_mode(ctx); + if !dual_panel && Self::is_network_panel_open() { + if self.network.on_back() { + Self::toggle_network_panel(); + return false; + } + } else if self.wallets.on_back(cb) { + Self::show_exit_modal(); + return false; + } + } + true + } - /// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time. - pub fn is_dual_panel_mode(ctx: &egui::Context) -> bool { - let (w, h) = View::window_size(ctx); - // Screen is wide if width is greater than height or just 20% smaller. - let is_wide_screen = w > h || w + (w * 0.2) >= h; - // Dual panel mode is available when window is wide and its width is at least 2 times - // greater than minimal width of the side panel plus display insets from both sides. - let side_insets = View::get_left_inset() + View::get_right_inset(); - is_wide_screen && w >= (Self::SIDE_PANEL_WIDTH * 2.0) + side_insets - } + /// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time. + pub fn is_dual_panel_mode(ctx: &egui::Context) -> bool { + let (w, h) = View::window_size(ctx); + // Screen is wide if width is greater than height or just 20% smaller. + let is_wide_screen = w > h || w + (w * 0.2) >= h; + // Dual panel mode is available when window is wide and its width is at least 2 times + // greater than minimal width of the side panel plus display insets from both sides. + let side_insets = View::get_left_inset() + View::get_right_inset(); + is_wide_screen && w >= (Self::SIDE_PANEL_WIDTH * 2.0) + side_insets + } - /// Toggle [`NetworkContent`] panel state. - pub fn toggle_network_panel() { - let is_open = NETWORK_PANEL_OPEN.load(Ordering::Relaxed); - NETWORK_PANEL_OPEN.store(!is_open, Ordering::Relaxed); - } + /// Toggle [`NetworkContent`] panel state. + pub fn toggle_network_panel() { + let is_open = NETWORK_PANEL_OPEN.load(Ordering::Relaxed); + NETWORK_PANEL_OPEN.store(!is_open, Ordering::Relaxed); + } - /// Check if [`NetworkContent`] panel is open. - pub fn is_network_panel_open() -> bool { - NETWORK_PANEL_OPEN.load(Ordering::Relaxed) - } + /// Check if [`NetworkContent`] panel is open. + pub fn is_network_panel_open() -> bool { + NETWORK_PANEL_OPEN.load(Ordering::Relaxed) + } - /// Show exit confirmation [`Modal`]. - pub fn show_exit_modal() { - Modal::new(Self::EXIT_CONFIRMATION_MODAL) - .title(t!("confirmation")) - .show(); - } + /// Show exit confirmation [`Modal`]. + pub fn show_exit_modal() { + Modal::new(Self::EXIT_CONFIRMATION_MODAL) + .title(t!("confirmation")) + .show(); + } - /// Draw exit confirmation modal content. - fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - if self.show_exit_progress { - if !Node::is_running() && !Node::data_dir_changing() { - self.exit_allowed = true; - cb.exit(); - Modal::close(); - } - ui.add_space(16.0); - ui.vertical_centered(|ui| { - View::small_loading_spinner(ui); - ui.add_space(12.0); - let exit_status_text = if Node::data_dir_changing() { - t!("moving_files") - } else { - t!("sync_status.shutdown") - }; - ui.label(RichText::new(exit_status_text) - .size(17.0) - .color(Colors::text(false))); - }); - ui.add_space(10.0); - } else { - ui.add_space(8.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("modal_exit.description")) - .size(17.0) - .color(Colors::text(false))); - }); - ui.add_space(10.0); + /// Draw exit confirmation modal content. + fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + if self.show_exit_progress { + if !Node::is_running() && !Node::data_dir_changing() { + self.exit_allowed = true; + cb.exit(); + Modal::close(); + } + ui.add_space(16.0); + ui.vertical_centered(|ui| { + View::small_loading_spinner(ui); + ui.add_space(12.0); + let exit_status_text = if Node::data_dir_changing() { + t!("moving_files") + } else { + t!("sync_status.shutdown") + }; + ui.label( + RichText::new(exit_status_text) + .size(17.0) + .color(Colors::text(false)), + ); + }); + ui.add_space(10.0); + } else { + ui.add_space(8.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("modal_exit.description")) + .size(17.0) + .color(Colors::text(false)), + ); + }); + ui.add_space(10.0); - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |_| { - if !Node::is_running() && !Node::data_dir_changing() { - self.exit_allowed = true; - cb.exit(); - Modal::close(); - } else { - Node::stop(true); - modal.disable_closing(); - Modal::set_title(t!("modal_exit.exit")); - self.show_exit_progress = true; - } - }); - }); - }); - ui.add_space(6.0); - } - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button_ui( + ui, + t!("modal_exit.exit"), + Colors::white_or_black(false), + |_| { + if !Node::is_running() && !Node::data_dir_changing() { + self.exit_allowed = true; + cb.exit(); + Modal::close(); + } else { + Node::stop(true); + modal.disable_closing(); + Modal::set_title(t!("modal_exit.exit")); + self.show_exit_progress = true; + } + }, + ); + }); + }); + ui.add_space(6.0); + } + } - /// Draw content for integrated node warning [`Modal`] on Android. - fn android_warning_modal_ui(&mut self, ui: &mut egui::Ui) { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network.android_warning")) - .size(16.0) - .color(Colors::text(false))); - }); - ui.add_space(8.0); - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("continue"), Colors::white_or_black(false), || { - AppConfig::show_android_integrated_node_warning(); - Modal::close(); - }); - }); - ui.add_space(6.0); - } + /// Draw content for integrated node warning [`Modal`] on Android. + fn android_warning_modal_ui(&mut self, ui: &mut egui::Ui) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network.android_warning")) + .size(16.0) + .color(Colors::text(false)), + ); + }); + ui.add_space(8.0); + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("continue"), Colors::white_or_black(false), || { + AppConfig::show_android_integrated_node_warning(); + Modal::close(); + }); + }); + ui.add_space(6.0); + } - /// Draw content for integrated node warning [`Modal`] on Android. - fn crash_report_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("crash_report_warning")) - .size(16.0) - .color(Colors::text(false))); - ui.add_space(6.0); - // Draw button to share log file. - let text = format!("{} {}", FILE_X, t!("share")); - View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || { - if let Ok(data) = fs::read_to_string(Settings::log_path()) { - let name = Settings::LOG_FILE_NAME.to_string(); - let _ = cb.share_data(name, data.as_bytes().to_vec()); - } - Settings::delete_crash_check(); - Modal::close(); - }); - }); - ui.add_space(8.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - Settings::delete_crash_check(); - Modal::close(); - }); - }); - ui.add_space(6.0); - } + /// Draw content for integrated node warning [`Modal`] on Android. + fn crash_report_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("crash_report_warning")) + .size(16.0) + .color(Colors::text(false)), + ); + ui.add_space(6.0); + // Draw button to share log file. + let text = format!("{} {}", FILE_X, t!("share")); + View::colored_text_button( + ui, + text, + Colors::blue(), + Colors::white_or_black(false), + || { + if let Ok(data) = fs::read_to_string(Settings::log_path()) { + let name = Settings::LOG_FILE_NAME.to_string(); + let _ = cb.share_data(name, data.as_bytes().to_vec()); + } + Settings::delete_crash_check(); + Modal::close(); + }, + ); + }); + ui.add_space(8.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); + ui.vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + Settings::delete_crash_check(); + Modal::close(); + }, + ); + }); + ui.add_space(6.0); + } } /// Get [`NetworkContent`] panel state and width. fn network_panel_state_width(ctx: &egui::Context, dual_panel: bool) -> (bool, f32) { - let is_panel_open = dual_panel || Content::is_network_panel_open(); - let panel_width = if dual_panel { - Content::SIDE_PANEL_WIDTH + View::get_left_inset() - } else { - let is_fullscreen = ctx.input(|i| { - i.viewport().fullscreen.unwrap_or(false) - }); - View::window_size(ctx).0 - if View::is_desktop() && !is_fullscreen && - OperatingSystem::from_target_os() != OperatingSystem::Mac { - Content::WINDOW_FRAME_MARGIN * 2.0 - } else { - 0.0 - } - }; - (is_panel_open, panel_width) -} \ No newline at end of file + let is_panel_open = dual_panel || Content::is_network_panel_open(); + let panel_width = if dual_panel { + Content::SIDE_PANEL_WIDTH + View::get_left_inset() + } else { + let is_fullscreen = ctx.input(|i| i.viewport().fullscreen.unwrap_or(false)); + View::window_size(ctx).0 + - if View::is_desktop() + && !is_fullscreen + && OperatingSystem::from_target_os() != OperatingSystem::Mac + { + Content::WINDOW_FRAME_MARGIN * 2.0 + } else { + 0.0 + } + }; + (is_panel_open, panel_width) +} diff --git a/src/gui/views/file_pick.rs b/src/gui/views/file_pick.rs index 37f57c1e..7cff1289 100644 --- a/src/gui/views/file_pick.rs +++ b/src/gui/views/file_pick.rs @@ -14,179 +14,182 @@ use egui::CornerRadius; use parking_lot::RwLock; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::{fs, thread}; +use crate::gui::Colors; use crate::gui::icons::ARCHIVE_BOX; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::View; -use crate::gui::Colors; /// Type of button. pub enum FilePickContentType { - Button(String), ItemButton(CornerRadius), Tab + Button(String), + ItemButton(CornerRadius), + Tab, } /// Button to pick file and parse its data into text. pub struct FilePickContent { - /// Content type. - content_type: FilePickContentType, + /// Content type. + content_type: FilePickContentType, - /// Flag to check if button is active. - active: bool, + /// Flag to check if button is active. + active: bool, - /// Flag to check if file is picking. - file_picking: Arc, + /// Flag to check if file is picking. + file_picking: Arc, - /// Flag to check if folder should be picked. - pick_folder: bool, - /// Flag to parse file content after pick. - parse_file: bool, - /// Flag to check if file is parsing. - file_parsing: Arc, - /// File parsing result. - file_parsing_result: Arc>>, + /// Flag to check if folder should be picked. + pick_folder: bool, + /// Flag to parse file content after pick. + parse_file: bool, + /// Flag to check if file is parsing. + file_parsing: Arc, + /// File parsing result. + file_parsing_result: Arc>>, } impl FilePickContent { - /// Create new content from provided type. - pub fn new(content_type: FilePickContentType) -> Self { - Self { - content_type, - active: false, - file_picking: Arc::new(AtomicBool::new(false)), - pick_folder: false, - parse_file: true, - file_parsing: Arc::new(AtomicBool::new(false)), - file_parsing_result: Arc::new(RwLock::new(None)), - } - } + /// Create new content from provided type. + pub fn new(content_type: FilePickContentType) -> Self { + Self { + content_type, + active: false, + file_picking: Arc::new(AtomicBool::new(false)), + pick_folder: false, + parse_file: true, + file_parsing: Arc::new(AtomicBool::new(false)), + file_parsing_result: Arc::new(RwLock::new(None)), + } + } - /// Pick folder. - pub fn pick_folder(mut self) -> Self { - self.pick_folder = true; - self - } + /// Pick folder. + pub fn pick_folder(mut self) -> Self { + self.pick_folder = true; + self + } - /// Do not parse file content. - pub fn no_parse(mut self) -> Self { - self.parse_file = false; - self - } + /// Do not parse file content. + pub fn no_parse(mut self) -> Self { + self.parse_file = false; + self + } - /// Enable or disable the button. - pub fn set_active(&mut self, active: bool) { - self.active = active; - } + /// Enable or disable the button. + pub fn set_active(&mut self, active: bool) { + self.active = active; + } - /// Draw content with provided callback to return path of the file. - pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks, pick: impl FnOnce(String)) { - if self.file_picking.load(Ordering::Relaxed) { - View::small_loading_spinner(ui); - // Check file pick result. - if let Some(path) = cb.picked_file() { - self.file_picking.store(false, Ordering::Relaxed); - if !path.is_empty() { - if self.parse_file { - self.parse_file(path); - } else { - pick(path); - } - } - } - } else if self.file_parsing.load(Ordering::Relaxed) { - View::small_loading_spinner(ui); - // Check file parsing result. - let has_result = { - let r_res = self.file_parsing_result.read(); - r_res.is_some() - }; - if has_result { - let text = { - let r_res = self.file_parsing_result.read(); - r_res.clone().unwrap() - }; - // Callback on result. - pick(text); - // Clear result. - let mut w_res = self.file_parsing_result.write(); - *w_res = None; - self.file_parsing.store(false, Ordering::Relaxed); - } - } else { - // Draw button to pick file. - match &self.content_type { - FilePickContentType::Button(text) => { - let text = format!("{} {}", ARCHIVE_BOX, text); - let text_color = Colors::blue(); - let fill = Colors::white_or_black(false); - View::colored_text_button(ui, text, text_color, fill, || { - self.on_file_pick(pick, cb); - }); - } - FilePickContentType::ItemButton(r) => { - View::item_button(ui, r.clone(), ARCHIVE_BOX, Some(Colors::blue()), || { - self.on_file_pick(pick, cb); - }); - } - FilePickContentType::Tab => { - let active = match self.active { - true => Some(self.file_parsing.load(Ordering::Relaxed) || - self.file_picking.load(Ordering::Relaxed)), - false => None - }; - View::tab_button(ui, ARCHIVE_BOX, Some(Colors::blue()), active, |_| { - self.on_file_pick(pick, cb); - }); - } - } - } - } + /// Draw content with provided callback to return path of the file. + pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks, pick: impl FnOnce(String)) { + if self.file_picking.load(Ordering::Relaxed) { + View::small_loading_spinner(ui); + // Check file pick result. + if let Some(path) = cb.picked_file() { + self.file_picking.store(false, Ordering::Relaxed); + if !path.is_empty() { + if self.parse_file { + self.parse_file(path); + } else { + pick(path); + } + } + } + } else if self.file_parsing.load(Ordering::Relaxed) { + View::small_loading_spinner(ui); + // Check file parsing result. + let has_result = { + let r_res = self.file_parsing_result.read(); + r_res.is_some() + }; + if has_result { + let text = { + let r_res = self.file_parsing_result.read(); + r_res.clone().unwrap() + }; + // Callback on result. + pick(text); + // Clear result. + let mut w_res = self.file_parsing_result.write(); + *w_res = None; + self.file_parsing.store(false, Ordering::Relaxed); + } + } else { + // Draw button to pick file. + match &self.content_type { + FilePickContentType::Button(text) => { + let text = format!("{} {}", ARCHIVE_BOX, text); + let text_color = Colors::blue(); + let fill = Colors::white_or_black(false); + View::colored_text_button(ui, text, text_color, fill, || { + self.on_file_pick(pick, cb); + }); + } + FilePickContentType::ItemButton(r) => { + View::item_button(ui, r.clone(), ARCHIVE_BOX, Some(Colors::blue()), || { + self.on_file_pick(pick, cb); + }); + } + FilePickContentType::Tab => { + let active = match self.active { + true => Some( + self.file_parsing.load(Ordering::Relaxed) + || self.file_picking.load(Ordering::Relaxed), + ), + false => None, + }; + View::tab_button(ui, ARCHIVE_BOX, Some(Colors::blue()), active, |_| { + self.on_file_pick(pick, cb); + }); + } + } + } + } - /// Handle pick file request. - fn on_file_pick(&self, on_pick: impl FnOnce(String), cb: &dyn PlatformCallbacks) { - let path = if self.pick_folder { - cb.pick_folder() - } else { - cb.pick_file() - }; - if path.is_none() { - return; - } - let path = path.unwrap(); - // Wait for asynchronous file pick result if path is empty. - if path.is_empty() { - self.file_picking.store(true, Ordering::Relaxed); - return; - } - // Parse result if needed. - if self.parse_file { - self.parse_file(path); - } else { - on_pick(path); - } - } + /// Handle pick file request. + fn on_file_pick(&self, on_pick: impl FnOnce(String), cb: &dyn PlatformCallbacks) { + let path = if self.pick_folder { + cb.pick_folder() + } else { + cb.pick_file() + }; + if path.is_none() { + return; + } + let path = path.unwrap(); + // Wait for asynchronous file pick result if path is empty. + if path.is_empty() { + self.file_picking.store(true, Ordering::Relaxed); + return; + } + // Parse result if needed. + if self.parse_file { + self.parse_file(path); + } else { + on_pick(path); + } + } - /// Handle picked file path. - fn parse_file(&self, path: String) { - self.file_parsing.store(true, Ordering::Relaxed); - let result = self.file_parsing_result.clone(); - thread::spawn(move || { - if path.ends_with(".gif") { - //TODO: Detect QR codes on GIF file. - } else if path.ends_with(".jpeg") || path.ends_with(".jpg") || - path.ends_with(".png") { - //TODO: Detect QR codes on image files. - } else { - // Parse file as plain text. - let mut w_res = result.write(); - if let Ok(text) = fs::read_to_string(path) { - *w_res = Some(text); - } else { - *w_res = Some("".to_string()); - } - } - }); - } -} \ No newline at end of file + /// Handle picked file path. + fn parse_file(&self, path: String) { + self.file_parsing.store(true, Ordering::Relaxed); + let result = self.file_parsing_result.clone(); + thread::spawn(move || { + if path.ends_with(".gif") { + //TODO: Detect QR codes on GIF file. + } else if path.ends_with(".jpeg") || path.ends_with(".jpg") || path.ends_with(".png") { + //TODO: Detect QR codes on image files. + } else { + // Parse file as plain text. + let mut w_res = result.write(); + if let Ok(text) = fs::read_to_string(path) { + *w_res = Some(text); + } else { + *w_res = Some("".to_string()); + } + } + }); + } +} diff --git a/src/gui/views/input/edit.rs b/src/gui/views/input/edit.rs index 0650b5c9..4176d31a 100644 --- a/src/gui/views/input/edit.rs +++ b/src/gui/views/input/edit.rs @@ -18,387 +18,409 @@ use lazy_static::lazy_static; use parking_lot::RwLock; use std::sync::Arc; +use crate::gui::Colors; use crate::gui::icons::{CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SCAN}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::input::keyboard::KeyboardContent; use crate::gui::views::{KeyboardEvent, View}; -use crate::gui::Colors; /// Text input content. pub struct TextEdit { - /// View identifier. - id: egui::Id, - /// Check if input is enabled or disabled. - enabled: bool, - /// Horizontal text centering is needed. - h_center: bool, - /// Focus is needed. - focus: bool, - /// Focus request was passed. - focus_request: bool, - /// Hide letters and draw button to show/hide letters. - password: bool, - /// Show copy button. - copy: bool, - /// Show paste button. - paste: bool, - /// Show button to scan QR code into text. - scan_qr: bool, - /// Scan button was pressed. - pub scan_pressed: bool, - /// Tab or Enter keys were pressed to focus on next line. - pub enter_pressed: bool, - /// Flag to enter only numbers. - numeric: bool, - /// Flag to not show soft keyboard. - no_soft_keyboard: bool, + /// View identifier. + id: egui::Id, + /// Check if input is enabled or disabled. + enabled: bool, + /// Horizontal text centering is needed. + h_center: bool, + /// Focus is needed. + focus: bool, + /// Focus request was passed. + focus_request: bool, + /// Hide letters and draw button to show/hide letters. + password: bool, + /// Show copy button. + copy: bool, + /// Show paste button. + paste: bool, + /// Show button to scan QR code into text. + scan_qr: bool, + /// Scan button was pressed. + pub scan_pressed: bool, + /// Tab or Enter keys were pressed to focus on next line. + pub enter_pressed: bool, + /// Flag to enter only numbers. + numeric: bool, + /// Flag to not show soft keyboard. + no_soft_keyboard: bool, } impl TextEdit { - /// Default height of [`egui::TextEdit`] view. - const TEXT_EDIT_HEIGHT: f32 = 42.0; + /// Default height of [`egui::TextEdit`] view. + const TEXT_EDIT_HEIGHT: f32 = 42.0; - pub fn new(id: egui::Id) -> Self { - Self { - id, - enabled: true, - h_center: false, - focus: true, - focus_request: false, - password: false, - copy: false, - paste: false, - scan_qr: false, - scan_pressed: false, - enter_pressed: false, - numeric: false, - no_soft_keyboard: is_android(), - } - } + pub fn new(id: egui::Id) -> Self { + Self { + id, + enabled: true, + h_center: false, + focus: true, + focus_request: false, + password: false, + copy: false, + paste: false, + scan_qr: false, + scan_pressed: false, + enter_pressed: false, + numeric: false, + no_soft_keyboard: is_android(), + } + } - /// Draw text input. - pub fn ui(&mut self, ui: &mut egui::Ui, input: &mut String, cb: &dyn PlatformCallbacks) { - self.input_ui(ui, input, |_| {}, cb); - } + /// Draw text input. + pub fn ui(&mut self, ui: &mut egui::Ui, input: &mut String, cb: &dyn PlatformCallbacks) { + self.input_ui(ui, input, |_| {}, cb); + } - /// Draw text input with additional buttons (right to left order). - pub fn custom_buttons_ui(&mut self, - ui: &mut egui::Ui, - input: &mut String, - cb: &dyn PlatformCallbacks, - buttons_content: impl FnOnce(&mut egui::Ui)) { - self.input_ui(ui, input, buttons_content, cb); - } + /// Draw text input with additional buttons (right to left order). + pub fn custom_buttons_ui( + &mut self, + ui: &mut egui::Ui, + input: &mut String, + cb: &dyn PlatformCallbacks, + buttons_content: impl FnOnce(&mut egui::Ui), + ) { + self.input_ui(ui, input, buttons_content, cb); + } - /// Draw text input content. - fn input_ui(&mut self, - ui: &mut egui::Ui, - input: &mut String, - buttons_content: impl FnOnce(&mut egui::Ui), - cb: &dyn PlatformCallbacks) { - let mut layout_rect = ui.available_rect_before_wrap(); - layout_rect.set_height(Self::TEXT_EDIT_HEIGHT); - ui.allocate_ui_with_layout(layout_rect.size(), Layout::right_to_left(Align::Max), |ui| { - let mut hide_input = false; - if self.password { - let show_pass_id = egui::Id::new(self.id).with("_show_pass"); - hide_input = ui.data(|data| { - data.get_temp(show_pass_id) - }).unwrap_or(true); - // Draw button to show/hide current password. - let eye_icon = if hide_input { EYE } else { EYE_SLASH }; - View::button_ui(ui, eye_icon.to_string(), Colors::white_or_black(false), |ui| { - hide_input = !hide_input; - ui.data_mut(|data| { - data.insert_temp(show_pass_id, hide_input); - }); - }); - ui.add_space(8.0); - } + /// Draw text input content. + fn input_ui( + &mut self, + ui: &mut egui::Ui, + input: &mut String, + buttons_content: impl FnOnce(&mut egui::Ui), + cb: &dyn PlatformCallbacks, + ) { + let mut layout_rect = ui.available_rect_before_wrap(); + layout_rect.set_height(Self::TEXT_EDIT_HEIGHT); + ui.allocate_ui_with_layout( + layout_rect.size(), + Layout::right_to_left(Align::Max), + |ui| { + let mut hide_input = false; + if self.password { + let show_pass_id = egui::Id::new(self.id).with("_show_pass"); + hide_input = ui.data(|data| data.get_temp(show_pass_id)).unwrap_or(true); + // Draw button to show/hide current password. + let eye_icon = if hide_input { EYE } else { EYE_SLASH }; + View::button_ui( + ui, + eye_icon.to_string(), + Colors::white_or_black(false), + |ui| { + hide_input = !hide_input; + ui.data_mut(|data| { + data.insert_temp(show_pass_id, hide_input); + }); + }, + ); + ui.add_space(8.0); + } - // Extra buttons content. - (buttons_content)(ui); + // Extra buttons content. + (buttons_content)(ui); - // Setup copy button. - if self.copy { - let copy_icon = COPY.to_string(); - View::button(ui, copy_icon, Colors::white_or_black(false), || { - cb.copy_string_to_buffer(input.clone()); - }); - ui.add_space(8.0); - } + // Setup copy button. + if self.copy { + let copy_icon = COPY.to_string(); + View::button(ui, copy_icon, Colors::white_or_black(false), || { + cb.copy_string_to_buffer(input.clone()); + }); + ui.add_space(8.0); + } - // Setup paste button. - if self.paste { - let paste_icon = CLIPBOARD_TEXT.to_string(); - View::button(ui, paste_icon, Colors::white_or_black(false), || { - *input = cb.get_string_from_buffer(); - }); - ui.add_space(8.0); - } + // Setup paste button. + if self.paste { + let paste_icon = CLIPBOARD_TEXT.to_string(); + View::button(ui, paste_icon, Colors::white_or_black(false), || { + *input = cb.get_string_from_buffer(); + }); + ui.add_space(8.0); + } - // Setup scan QR code button. - if self.scan_qr { - let scan_icon = SCAN.to_string(); - View::button(ui, scan_icon, Colors::white_or_black(false), || { - cb.start_camera(); - self.scan_pressed = true; - }); - ui.add_space(8.0); - } + // Setup scan QR code button. + if self.scan_qr { + let scan_icon = SCAN.to_string(); + View::button(ui, scan_icon, Colors::white_or_black(false), || { + cb.start_camera(); + self.scan_pressed = true; + }); + ui.add_space(8.0); + } - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Min), |ui| { - // Setup text edit size. - let mut edit_rect = ui.available_rect_before_wrap(); - edit_rect.set_height(Self::TEXT_EDIT_HEIGHT); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Min), |ui| { + // Setup text edit size. + let mut edit_rect = ui.available_rect_before_wrap(); + edit_rect.set_height(Self::TEXT_EDIT_HEIGHT); - // Setup focused input value to avoid dismiss when click on keyboard. - let focused_input_id = egui::Id::new("focused_input_id"); - let focused = ui.data(|data| { - data.get_temp(focused_input_id) - }).unwrap_or(egui::Id::new("")) == self.id; + // Setup focused input value to avoid dismiss when click on keyboard. + let focused_input_id = egui::Id::new("focused_input_id"); + let focused = ui + .data(|data| data.get_temp(focused_input_id)) + .unwrap_or(egui::Id::new("")) + == self.id; - // Show text edit. - let text_edit_resp = egui::TextEdit::singleline(input) - .text_color(if self.enabled { - Colors::text(false) - } else { - Colors::inactive_text() - }) - .interactive(self.enabled) - .id(self.id) - .font(TextStyle::Heading) - .min_size(edit_rect.size()) - .margin(if View::is_desktop() { - egui::Margin::symmetric(4, 2) - } else { - egui::Margin::symmetric(8, 8) - }) - .horizontal_align(if self.h_center { Align::Center } else { Align::Min }) - .vertical_align(Align::Center) - .password(hide_input) - .cursor_at_end(true) - .ui(ui); + // Show text edit. + let text_edit_resp = egui::TextEdit::singleline(input) + .text_color(if self.enabled { + Colors::text(false) + } else { + Colors::inactive_text() + }) + .interactive(self.enabled) + .id(self.id) + .font(TextStyle::Heading) + .min_size(edit_rect.size()) + .margin(if View::is_desktop() { + egui::Margin::symmetric(4, 2) + } else { + egui::Margin::symmetric(8, 8) + }) + .horizontal_align(if self.h_center { + Align::Center + } else { + Align::Min + }) + .vertical_align(Align::Center) + .password(hide_input) + .cursor_at_end(true) + .ui(ui); - // Setup focus state. - let clicked = text_edit_resp.clicked(); - if !text_edit_resp.has_focus() && - (self.focus || self.focus_request || clicked || focused) { - text_edit_resp.request_focus(); - } + // Setup focus state. + let clicked = text_edit_resp.clicked(); + if !text_edit_resp.has_focus() + && (self.focus || self.focus_request || clicked || focused) + { + text_edit_resp.request_focus(); + } - // Reset keyboard state for newly focused. - if clicked || self.focus_request { - ui.ctx().send_viewport_cmd(ViewportCommand::IMEAllowed(true)); - KeyboardContent::reset_window_state(); - } + // Reset keyboard state for newly focused. + if clicked || self.focus_request { + ui.ctx() + .send_viewport_cmd(ViewportCommand::IMEAllowed(true)); + KeyboardContent::reset_window_state(); + } - // Apply text from software input. - if text_edit_resp.has_focus() { - ui.data_mut(|data| { - data.insert_temp(focused_input_id, self.id); - }); - self.enter_pressed = self.on_soft_input(ui, self.id, false, input); - // Check Enter or Tab keys press. - if !self.focus_request { - if ui.ctx().input(|i| i.key_pressed(egui::Key::Enter) || - i.key_pressed(egui::Key::Tab)) { - self.enter_pressed = true; - } - } - if self.enter_pressed { - KeyboardContent::unshift(); - } - if !self.no_soft_keyboard { - KeyboardContent::default().window_ui(self.numeric, ui.ctx()); - } - } - }); - }); - // Immediate repaint when input is open. - ui.ctx().request_repaint(); - } + // Apply text from software input. + if text_edit_resp.has_focus() { + ui.data_mut(|data| { + data.insert_temp(focused_input_id, self.id); + }); + self.enter_pressed = self.on_soft_input(ui, self.id, false, input); + // Check Enter or Tab keys press. + if !self.focus_request { + if ui.ctx().input(|i| { + i.key_pressed(egui::Key::Enter) || i.key_pressed(egui::Key::Tab) + }) { + self.enter_pressed = true; + } + } + if self.enter_pressed { + KeyboardContent::unshift(); + } + if !self.no_soft_keyboard { + KeyboardContent::default().window_ui(self.numeric, ui.ctx()); + } + } + }); + }, + ); + // Immediate repaint when input is open. + ui.ctx().request_repaint(); + } - /// Apply soft keyboard input data to provided String, returns `true` if Enter was pressed. - fn on_soft_input(&self, ui: &mut egui::Ui, id: egui::Id, multiline: bool, value: &mut String) - -> bool { - let event: Option = if is_android() { - let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write(); - w_input.take() - } else { - KeyboardContent::consume_event() - }; + /// Apply soft keyboard input data to provided String, returns `true` if Enter was pressed. + fn on_soft_input( + &self, + ui: &mut egui::Ui, + id: egui::Id, + multiline: bool, + value: &mut String, + ) -> bool { + let event: Option = if is_android() { + let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write(); + w_input.take() + } else { + KeyboardContent::consume_event() + }; - // Handle keyboard input event. - if let Some(e) = event { - let mut enter_pressed = false; - let mut state = TextEditState::load(ui.ctx(), id).unwrap(); - match state.cursor.char_range() { - None => {} - Some(range) => { - let mut r = range.clone(); - let mut index = r.primary.index; + // Handle keyboard input event. + if let Some(e) = event { + let mut enter_pressed = false; + let mut state = TextEditState::load(ui.ctx(), id).unwrap(); + match state.cursor.char_range() { + None => {} + Some(range) => { + let mut r = range.clone(); + let mut index = r.primary.index; - let selected = r.primary.index != r.secondary.index; - let start_select = f32::min(r.primary.index as f32, - r.secondary.index as f32) as usize; - let end_select = f32::max(r.primary.index as f32, - r.secondary.index as f32) as usize; - match e { - KeyboardEvent::TEXT(text) => { - if selected { - *value = { - let part1: String = value.chars() - .skip(0) - .take(start_select) - .collect(); - let part2: String = value.chars() - .skip(end_select) - .take(value.len() - end_select) - .collect(); - format!("{}{}{}", part1, text, part2) - }; - index = start_select + 1; - } else { - value.insert_text(text.as_str(), index); - index = index + 1; - } - } - KeyboardEvent::CLEAR => { - if selected { - *value = { - let part1: String = value.chars() - .skip(0) - .take(start_select) - .collect(); - let part2: String = value.chars() - .skip(end_select) - .take(value.len() - end_select) - .collect(); - format!("{}{}", part1, part2) - }; - index = start_select; - } else if index != 0 { - *value = { - let part1: String = value.chars() - .skip(0) - .take(index - 1) - .collect(); - let part2: String = value.chars() - .skip(index) - .take(value.len() - index) - .collect(); - format!("{}{}", part1, part2) - }; - index = index - 1; - } - } - KeyboardEvent::ENTER => { - if multiline { - value.insert_text("\n", index); - index = index + 1; - } else { - enter_pressed = true; - } - } - } - // Setup cursor index. - r.primary.index = index; - r.secondary.index = r.primary.index; + let selected = r.primary.index != r.secondary.index; + let start_select = + f32::min(r.primary.index as f32, r.secondary.index as f32) as usize; + let end_select = + f32::max(r.primary.index as f32, r.secondary.index as f32) as usize; + match e { + KeyboardEvent::TEXT(text) => { + if selected { + *value = { + let part1: String = + value.chars().skip(0).take(start_select).collect(); + let part2: String = value + .chars() + .skip(end_select) + .take(value.len() - end_select) + .collect(); + format!("{}{}{}", part1, text, part2) + }; + index = start_select + 1; + } else { + value.insert_text(text.as_str(), index); + index = index + 1; + } + } + KeyboardEvent::CLEAR => { + if selected { + *value = { + let part1: String = + value.chars().skip(0).take(start_select).collect(); + let part2: String = value + .chars() + .skip(end_select) + .take(value.len() - end_select) + .collect(); + format!("{}{}", part1, part2) + }; + index = start_select; + } else if index != 0 { + *value = { + let part1: String = + value.chars().skip(0).take(index - 1).collect(); + let part2: String = value + .chars() + .skip(index) + .take(value.len() - index) + .collect(); + format!("{}{}", part1, part2) + }; + index = index - 1; + } + } + KeyboardEvent::ENTER => { + if multiline { + value.insert_text("\n", index); + index = index + 1; + } else { + enter_pressed = true; + } + } + } + // Setup cursor index. + r.primary.index = index; + r.secondary.index = r.primary.index; - state.cursor.set_char_range(Some(r)); - TextEditState::store(state, ui.ctx(), id); - } - } - return enter_pressed; - } - false - } + state.cursor.set_char_range(Some(r)); + TextEditState::store(state, ui.ctx(), id); + } + } + return enter_pressed; + } + false + } - /// Set cursor to the end of text. - pub fn cursor_to_end(&self, text_len: usize, ui: &mut egui::Ui) { - let mut state = TextEditState::load(ui.ctx(), self.id).unwrap(); - match state.cursor.char_range() { - None => {} - Some(range) => { - let mut r = range.clone(); - r.primary.index = text_len; - r.secondary.index = text_len; - state.cursor.set_char_range(Some(r)); - TextEditState::store(state, ui.ctx(), self.id); - } - } - } + /// Set cursor to the end of text. + pub fn cursor_to_end(&self, text_len: usize, ui: &mut egui::Ui) { + let mut state = TextEditState::load(ui.ctx(), self.id).unwrap(); + match state.cursor.char_range() { + None => {} + Some(range) => { + let mut r = range.clone(); + r.primary.index = text_len; + r.secondary.index = text_len; + state.cursor.set_char_range(Some(r)); + TextEditState::store(state, ui.ctx(), self.id); + } + } + } - /// Disable input. - pub fn disable(mut self) -> Self { - self.enabled = false; - self - } + /// Disable input. + pub fn disable(mut self) -> Self { + self.enabled = false; + self + } - /// Center text horizontally. - pub fn h_center(mut self) -> Self { - self.h_center = true; - self - } + /// Center text horizontally. + pub fn h_center(mut self) -> Self { + self.h_center = true; + self + } - /// Enable or disable constant focus. - pub fn focus(mut self, focus: bool) -> Self { - self.focus = focus; - self - } + /// Enable or disable constant focus. + pub fn focus(mut self, focus: bool) -> Self { + self.focus = focus; + self + } - /// Focus on field. - pub fn focus_request(&mut self) { - self.focus_request = true; - } + /// Focus on field. + pub fn focus_request(&mut self) { + self.focus_request = true; + } - /// Allow input of numbers only. - pub fn numeric(mut self) -> Self { - self.numeric = true; - self - } + /// Allow input of numbers only. + pub fn numeric(mut self) -> Self { + self.numeric = true; + self + } - /// Hide letters and draw button to show/hide letters. - pub fn password(mut self) -> Self { - self.password = true; - self - } + /// Hide letters and draw button to show/hide letters. + pub fn password(mut self) -> Self { + self.password = true; + self + } - /// Show button to copy text. - pub fn copy(mut self) -> Self { - self.copy = true; - self - } + /// Show button to copy text. + pub fn copy(mut self) -> Self { + self.copy = true; + self + } - /// Show button to paste text. - pub fn paste(mut self) -> Self { - self.paste = true; - self - } + /// Show button to paste text. + pub fn paste(mut self) -> Self { + self.paste = true; + self + } - /// Show button to scan QR code to text. - pub fn scan_qr(mut self) -> Self { - self.scan_qr = true; - self.scan_pressed = false; - self - } + /// Show button to scan QR code to text. + pub fn scan_qr(mut self) -> Self { + self.scan_qr = true; + self.scan_pressed = false; + self + } - /// Do not show soft keyboard for input. - pub fn no_soft_keyboard(mut self) -> Self { - self.no_soft_keyboard = true; - self - } + /// Do not show soft keyboard for input. + pub fn no_soft_keyboard(mut self) -> Self { + self.no_soft_keyboard = true; + self + } } /// Check if current system is Android. fn is_android() -> bool { - egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Android + egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Android } lazy_static! { - static ref LAST_SOFT_KEYBOARD_EVENT: Arc>> = Arc::new(RwLock::new(None)); + static ref LAST_SOFT_KEYBOARD_EVENT: Arc>> = + Arc::new(RwLock::new(None)); } #[allow(dead_code)] @@ -407,23 +429,23 @@ lazy_static! { #[unsafe(no_mangle)] /// Callback from Java code with last entered character from soft keyboard. pub extern "C" fn Java_mw_gri_android_MainActivity_onTextInput( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - char: jni::sys::jstring + _env: jni::JNIEnv, + _class: jni::objects::JObject, + char: jni::sys::jstring, ) { - use jni::objects::JString; + use jni::objects::JString; - unsafe { - let j_obj = JString::from_raw(char); - let j_str = _env.get_string_unchecked(j_obj.as_ref()).unwrap(); - match j_str.to_str() { - Ok(str) => { - let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write(); - *w_input = Some(KeyboardEvent::TEXT(str.to_string())); - } - Err(_) => {} - } - } + unsafe { + let j_obj = JString::from_raw(char); + let j_str = _env.get_string_unchecked(j_obj.as_ref()).unwrap(); + match j_str.to_str() { + Ok(str) => { + let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write(); + *w_input = Some(KeyboardEvent::TEXT(str.to_string())); + } + Err(_) => {} + } + } } #[allow(dead_code)] @@ -432,11 +454,11 @@ pub extern "C" fn Java_mw_gri_android_MainActivity_onTextInput( #[unsafe(no_mangle)] /// Callback from Java code when Clear key was pressed at soft keyboard. pub extern "C" fn Java_mw_gri_android_MainActivity_onClearInput( - _env: jni::JNIEnv, - _class: jni::objects::JObject, + _env: jni::JNIEnv, + _class: jni::objects::JObject, ) { - let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write(); - *w_input = Some(KeyboardEvent::CLEAR); + let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write(); + *w_input = Some(KeyboardEvent::CLEAR); } #[allow(dead_code)] @@ -445,9 +467,9 @@ pub extern "C" fn Java_mw_gri_android_MainActivity_onClearInput( #[unsafe(no_mangle)] /// Callback from Java code when Enter key was pressed at soft keyboard. pub extern "C" fn Java_mw_gri_android_MainActivity_onEnterInput( - _env: jni::JNIEnv, - _class: jni::objects::JObject, + _env: jni::JNIEnv, + _class: jni::objects::JObject, ) { - let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write(); - *w_input = Some(KeyboardEvent::ENTER); -} \ No newline at end of file + let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write(); + *w_input = Some(KeyboardEvent::ENTER); +} diff --git a/src/gui/views/input/keyboard.rs b/src/gui/views/input/keyboard.rs index 83c3c135..721ea04d 100644 --- a/src/gui/views/input/keyboard.rs +++ b/src/gui/views/input/keyboard.rs @@ -12,498 +12,538 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::string::ToString; -use egui::{Align, Align2, Button, Color32, CursorIcon, Layout, Margin, Rect, Response, RichText, Sense, Shadow, Vec2, Widget}; +use egui::{ + Align, Align2, Button, Color32, CursorIcon, Layout, Margin, Rect, Response, RichText, Sense, + Shadow, Vec2, Widget, +}; use lazy_static::lazy_static; use parking_lot::RwLock; -use std::sync::atomic::Ordering; +use std::string::ToString; use std::sync::Arc; +use std::sync::atomic::Ordering; +use crate::AppConfig; +use crate::gui::Colors; use crate::gui::icons::{ARROW_FAT_UP, BACKSPACE, GLOBE_SIMPLE, KEY_RETURN}; use crate::gui::views::{KeyboardEvent, KeyboardLayout, KeyboardState, View}; -use crate::gui::Colors; -use crate::AppConfig; lazy_static! { - /// Keyboard window state. - static ref WINDOW_STATE: Arc> = Arc::new( - RwLock::new(KeyboardState::default()) - ); + /// Keyboard window state. + static ref WINDOW_STATE: Arc> = Arc::new( + RwLock::new(KeyboardState::default()) + ); } /// Software keyboard content. pub struct KeyboardContent { - /// Keyboard content state. - state: KeyboardState, + /// Keyboard content state. + state: KeyboardState, } impl Default for KeyboardContent { - fn default() -> Self { - Self { - state: KeyboardState::default(), - } - } + fn default() -> Self { + Self { + state: KeyboardState::default(), + } + } } impl KeyboardContent { - /// Maximum keyboard content width. - const MAX_WIDTH: f32 = 600.0; - /// Maximum numbers layout width. - const MAX_WIDTH_NUMBERS: f32 = 400.0; + /// Maximum keyboard content width. + const MAX_WIDTH: f32 = 600.0; + /// Maximum numbers layout width. + const MAX_WIDTH_NUMBERS: f32 = 400.0; - /// Keyboard window id. - pub const WINDOW_ID: &'static str = "soft_keyboard_window"; + /// Keyboard window id. + pub const WINDOW_ID: &'static str = "soft_keyboard_window"; - /// Draw keyboard content as separate [`Window`]. - pub fn window_ui(&mut self, numeric: bool, ctx: &egui::Context) { - let width = ctx.content_rect().width(); - let layer_id = egui::Window::new(Self::WINDOW_ID) - .title_bar(false) - .resizable(false) - .collapsible(false) - .min_width(width) - .default_width(width) - .anchor(Align2::CENTER_BOTTOM, Vec2::new(0.0, 0.0)) - .frame(egui::Frame { - shadow: Shadow { - offset: Default::default(), - blur: 30.0 as u8, - spread: 3.0 as u8, - color: Color32::from_black_alpha(32), - }, - inner_margin: Margin { - left: View::get_left_inset() as i8, - right: View::get_right_inset() as i8, - top: 1.0 as i8, - bottom: View::get_bottom_inset() as i8, - }, - fill: Colors::fill(), - ..Default::default() - }) - .show(ctx, |ui| { - ui.set_min_width(width); - // Setup state. - { - let r_state = WINDOW_STATE.read(); - self.state = (*r_state).clone(); - } - // Calculate content width. - let side_insets = View::get_left_inset() + View::get_right_inset(); - let available_width = width - side_insets; - let w = f32::min(available_width, if numeric { - Self::MAX_WIDTH_NUMBERS - } else { - Self::MAX_WIDTH - }); - // Draw content. - View::max_width_ui(ui, w, |ui| { - self.ui(numeric, ui); - }); - // Save state. - let mut w_state = WINDOW_STATE.write(); - *w_state = self.state.clone(); - }).unwrap().response.layer_id; + /// Draw keyboard content as separate [`Window`]. + pub fn window_ui(&mut self, numeric: bool, ctx: &egui::Context) { + let width = ctx.content_rect().width(); + let layer_id = egui::Window::new(Self::WINDOW_ID) + .title_bar(false) + .resizable(false) + .collapsible(false) + .min_width(width) + .default_width(width) + .anchor(Align2::CENTER_BOTTOM, Vec2::new(0.0, 0.0)) + .frame(egui::Frame { + shadow: Shadow { + offset: Default::default(), + blur: 30.0 as u8, + spread: 3.0 as u8, + color: Color32::from_black_alpha(32), + }, + inner_margin: Margin { + left: View::get_left_inset() as i8, + right: View::get_right_inset() as i8, + top: 1.0 as i8, + bottom: View::get_bottom_inset() as i8, + }, + fill: Colors::fill(), + ..Default::default() + }) + .show(ctx, |ui| { + ui.set_min_width(width); + // Setup state. + { + let r_state = WINDOW_STATE.read(); + self.state = (*r_state).clone(); + } + // Calculate content width. + let side_insets = View::get_left_inset() + View::get_right_inset(); + let available_width = width - side_insets; + let w = f32::min( + available_width, + if numeric { + Self::MAX_WIDTH_NUMBERS + } else { + Self::MAX_WIDTH + }, + ); + // Draw content. + View::max_width_ui(ui, w, |ui| { + self.ui(numeric, ui); + }); + // Save state. + let mut w_state = WINDOW_STATE.write(); + *w_state = self.state.clone(); + }) + .unwrap() + .response + .layer_id; - // Always show keyboard above others windows. - ctx.move_to_top(layer_id); - } + // Always show keyboard above others windows. + ctx.move_to_top(layer_id); + } - /// Draw keyboard content. - pub fn ui(&mut self, numeric: bool, ui: &mut egui::Ui) { - // Setup layout. - if numeric { - self.state.layout = Arc::new(KeyboardLayout::NUMBERS); - } else if *self.state.layout == KeyboardLayout::NUMBERS { - self.state.layout = Arc::new(KeyboardLayout::TEXT); - } + /// Draw keyboard content. + pub fn ui(&mut self, numeric: bool, ui: &mut egui::Ui) { + // Setup layout. + if numeric { + self.state.layout = Arc::new(KeyboardLayout::NUMBERS); + } else if *self.state.layout == KeyboardLayout::NUMBERS { + self.state.layout = Arc::new(KeyboardLayout::TEXT); + } - // Setup spacing between buttons. - ui.style_mut().spacing.item_spacing = egui::vec2(0.0, 0.0); - // Setup vertical padding inside buttons. - ui.style_mut().spacing.button_padding = egui::vec2(0.0, if numeric { - 12.0 - } else { - 10.0 - }); + // Setup spacing between buttons. + ui.style_mut().spacing.item_spacing = egui::vec2(0.0, 0.0); + // Setup vertical padding inside buttons. + ui.style_mut().spacing.button_padding = egui::vec2(0.0, if numeric { 12.0 } else { 10.0 }); - // Draw input buttons. - let button_rect = match *self.state.layout { - KeyboardLayout::TEXT => self.text_ui(ui), - KeyboardLayout::SYMBOLS => self.symbols_ui(ui), - KeyboardLayout::NUMBERS => self.numbers_ui(ui), - }; + // Draw input buttons. + let button_rect = match *self.state.layout { + KeyboardLayout::TEXT => self.text_ui(ui), + KeyboardLayout::SYMBOLS => self.symbols_ui(ui), + KeyboardLayout::NUMBERS => self.numbers_ui(ui), + }; - // Draw bottom keyboard buttons. - let bottom_size = { - let mut r = button_rect.clone(); - r.set_width(ui.available_width()); - r.size() - }; - let button_width = ui.available_width() / match *self.state.layout { - KeyboardLayout::TEXT => 11.0, - KeyboardLayout::SYMBOLS => 10.0, - KeyboardLayout::NUMBERS => 4.0, - }; - ui.allocate_ui_with_layout(bottom_size, Layout::right_to_left(Align::Center), |ui| { - match *self.state.layout { - KeyboardLayout::TEXT => { - // Enter key input. - ui.horizontal_centered(|ui| { - ui.set_max_width(button_width * 2.0); - self.custom_button_ui(KEY_RETURN.to_string(), - Colors::white_or_black(false), - Some(Colors::green()), - ui, - |_, c| { - c.state.last_event = - Arc::new(Some(KeyboardEvent::ENTER)); - }); - }); - // Custom input key. - ui.horizontal_centered(|ui| { - ui.set_max_width(button_width); - self.input_button_ui("m3", true, ui); - }); - // Space key input. - ui.horizontal_centered(|ui| { - ui.set_max_width(button_width * 5.0); - self.custom_button_ui(" ".to_string(), - Colors::inactive_text(), - None, - ui, - |l, c| { - c.state.last_event = - Arc::new(Some(KeyboardEvent::TEXT(l))); - }); - }); - // Switch to english and back. - ui.horizontal_centered(|ui| { - ui.set_max_width(button_width); - self.custom_button_ui(GLOBE_SIMPLE.to_string(), - Colors::text_button(), - Some(Colors::fill_lite()), - ui, - |_, _| { - AppConfig::toggle_english_keyboard() - }); - }); - // Switch to symbols layout. - self.custom_button_ui("!@ツ".to_string(), - Colors::text_button(), - Some(Colors::fill_lite()), - ui, - |_, c| { - c.state.layout = Arc::new(KeyboardLayout::SYMBOLS); - }); - } - KeyboardLayout::SYMBOLS => { - // Enter key input. - ui.horizontal_centered(|ui| { - ui.set_max_width(button_width * 2.0); - self.custom_button_ui(KEY_RETURN.to_string(), - Colors::white_or_black(false), - Some(Colors::green()), - ui, - |_, c| { - c.state.last_event = - Arc::new(Some(KeyboardEvent::ENTER)); - }); - }); - // Custom input key. - ui.horizontal_centered(|ui| { - ui.set_max_width(button_width); - self.input_button_ui("ツ", false, ui); - }); - // Space key input. - ui.horizontal_centered(|ui| { - ui.set_max_width(button_width * 4.0); - self.custom_button_ui(" ".to_string(), - Colors::inactive_text(), - None, - ui, - |l, c| { - c.state.last_event = - Arc::new(Some(KeyboardEvent::TEXT(l))); - }); - }); - // Switch to text layout. - let label = { - let q = t!("keyboard.q", locale = Self::input_locale().as_str()); - let w = t!("keyboard.w", locale = Self::input_locale().as_str()); - let e = t!("keyboard.e", locale = Self::input_locale().as_str()); - format!("{}{}{}", q, w, e).to_uppercase() - }; - self.custom_button_ui(label, - Colors::text_button(), - Some(Colors::fill_lite()), - ui, - |_, c| { - c.state.layout = Arc::new(KeyboardLayout::TEXT); - }); - } - KeyboardLayout::NUMBERS => { - ui.horizontal_centered(|ui| { - ui.set_max_width(button_width * 2.0); - self.custom_button_ui(KEY_RETURN.to_string(), - Colors::white_or_black(false), - Some(Colors::green()), - ui, - |_, c| { - c.state.last_event = - Arc::new(Some(KeyboardEvent::ENTER)); - }); - }); - ui.horizontal_centered(|ui| { - ui.set_max_width(button_width); - self.input_button_ui("0", true, ui); - }); - ui.horizontal_centered(|ui| { - ui.set_max_width(button_width); - self.input_button_ui(".", false, ui); - }); - } - } - }); - } + // Draw bottom keyboard buttons. + let bottom_size = { + let mut r = button_rect.clone(); + r.set_width(ui.available_width()); + r.size() + }; + let button_width = ui.available_width() + / match *self.state.layout { + KeyboardLayout::TEXT => 11.0, + KeyboardLayout::SYMBOLS => 10.0, + KeyboardLayout::NUMBERS => 4.0, + }; + ui.allocate_ui_with_layout(bottom_size, Layout::right_to_left(Align::Center), |ui| { + match *self.state.layout { + KeyboardLayout::TEXT => { + // Enter key input. + ui.horizontal_centered(|ui| { + ui.set_max_width(button_width * 2.0); + self.custom_button_ui( + KEY_RETURN.to_string(), + Colors::white_or_black(false), + Some(Colors::green()), + ui, + |_, c| { + c.state.last_event = Arc::new(Some(KeyboardEvent::ENTER)); + }, + ); + }); + // Custom input key. + ui.horizontal_centered(|ui| { + ui.set_max_width(button_width); + self.input_button_ui("m3", true, ui); + }); + // Space key input. + ui.horizontal_centered(|ui| { + ui.set_max_width(button_width * 5.0); + self.custom_button_ui( + " ".to_string(), + Colors::inactive_text(), + None, + ui, + |l, c| { + c.state.last_event = Arc::new(Some(KeyboardEvent::TEXT(l))); + }, + ); + }); + // Switch to english and back. + ui.horizontal_centered(|ui| { + ui.set_max_width(button_width); + self.custom_button_ui( + GLOBE_SIMPLE.to_string(), + Colors::text_button(), + Some(Colors::fill_lite()), + ui, + |_, _| AppConfig::toggle_english_keyboard(), + ); + }); + // Switch to symbols layout. + self.custom_button_ui( + "!@ツ".to_string(), + Colors::text_button(), + Some(Colors::fill_lite()), + ui, + |_, c| { + c.state.layout = Arc::new(KeyboardLayout::SYMBOLS); + }, + ); + } + KeyboardLayout::SYMBOLS => { + // Enter key input. + ui.horizontal_centered(|ui| { + ui.set_max_width(button_width * 2.0); + self.custom_button_ui( + KEY_RETURN.to_string(), + Colors::white_or_black(false), + Some(Colors::green()), + ui, + |_, c| { + c.state.last_event = Arc::new(Some(KeyboardEvent::ENTER)); + }, + ); + }); + // Custom input key. + ui.horizontal_centered(|ui| { + ui.set_max_width(button_width); + self.input_button_ui("ツ", false, ui); + }); + // Space key input. + ui.horizontal_centered(|ui| { + ui.set_max_width(button_width * 4.0); + self.custom_button_ui( + " ".to_string(), + Colors::inactive_text(), + None, + ui, + |l, c| { + c.state.last_event = Arc::new(Some(KeyboardEvent::TEXT(l))); + }, + ); + }); + // Switch to text layout. + let label = { + let q = t!("keyboard.q", locale = Self::input_locale().as_str()); + let w = t!("keyboard.w", locale = Self::input_locale().as_str()); + let e = t!("keyboard.e", locale = Self::input_locale().as_str()); + format!("{}{}{}", q, w, e).to_uppercase() + }; + self.custom_button_ui( + label, + Colors::text_button(), + Some(Colors::fill_lite()), + ui, + |_, c| { + c.state.layout = Arc::new(KeyboardLayout::TEXT); + }, + ); + } + KeyboardLayout::NUMBERS => { + ui.horizontal_centered(|ui| { + ui.set_max_width(button_width * 2.0); + self.custom_button_ui( + KEY_RETURN.to_string(), + Colors::white_or_black(false), + Some(Colors::green()), + ui, + |_, c| { + c.state.last_event = Arc::new(Some(KeyboardEvent::ENTER)); + }, + ); + }); + ui.horizontal_centered(|ui| { + ui.set_max_width(button_width); + self.input_button_ui("0", true, ui); + }); + ui.horizontal_centered(|ui| { + ui.set_max_width(button_width); + self.input_button_ui(".", false, ui); + }); + } + } + }); + } - /// Draw numbers content returning button [`Rect`]. - fn numbers_ui(&mut self, ui: &mut egui::Ui) -> Rect { - let mut button_rect = ui.available_rect_before_wrap(); - let tl_0: Vec<&str> = vec!["1", "2", "3", "+"]; - ui.columns(tl_0.len(), |columns| { - for (index, s) in tl_0.iter().enumerate() { - let last = index == tl_0.len() - 1; - button_rect = self.input_button_ui(s, !last, &mut columns[index]); - } - }); + /// Draw numbers content returning button [`Rect`]. + fn numbers_ui(&mut self, ui: &mut egui::Ui) -> Rect { + let mut button_rect = ui.available_rect_before_wrap(); + let tl_0: Vec<&str> = vec!["1", "2", "3", "+"]; + ui.columns(tl_0.len(), |columns| { + for (index, s) in tl_0.iter().enumerate() { + let last = index == tl_0.len() - 1; + button_rect = self.input_button_ui(s, !last, &mut columns[index]); + } + }); - let tl_1: Vec<&str> = vec!["4", "5", "6", ","]; - ui.columns(tl_1.len(), |columns| { - for (index, s) in tl_1.iter().enumerate() { - let last = index == tl_1.len() - 1; - self.input_button_ui(s, !last, &mut columns[index]); - } - }); + let tl_1: Vec<&str> = vec!["4", "5", "6", ","]; + ui.columns(tl_1.len(), |columns| { + for (index, s) in tl_1.iter().enumerate() { + let last = index == tl_1.len() - 1; + self.input_button_ui(s, !last, &mut columns[index]); + } + }); - let tl_2: Vec<&str> = vec!["7", "8", "9", BACKSPACE]; - ui.columns(tl_2.len(), |columns| { - for (index, s) in tl_2.iter().enumerate() { - if index == tl_2.len() - 1 { - self.custom_button_ui(BACKSPACE.to_string(), - Colors::red(), - Some(Colors::fill_lite()), - &mut columns[index], - |_, c| { - c.state.last_event = - Arc::new(Some(KeyboardEvent::CLEAR)); - }); - } else { - self.input_button_ui(s, true, &mut columns[index]); - } - } - }); + let tl_2: Vec<&str> = vec!["7", "8", "9", BACKSPACE]; + ui.columns(tl_2.len(), |columns| { + for (index, s) in tl_2.iter().enumerate() { + if index == tl_2.len() - 1 { + self.custom_button_ui( + BACKSPACE.to_string(), + Colors::red(), + Some(Colors::fill_lite()), + &mut columns[index], + |_, c| { + c.state.last_event = Arc::new(Some(KeyboardEvent::CLEAR)); + }, + ); + } else { + self.input_button_ui(s, true, &mut columns[index]); + } + } + }); - button_rect - } + button_rect + } - /// Draw text content returning button [`Rect`]. - fn text_ui(&mut self, ui: &mut egui::Ui) -> Rect { - let mut button_rect = ui.available_rect_before_wrap(); - let tl_0: Vec<&str> = vec!["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "01"]; - ui.columns(tl_0.len(), |columns| { - for (index, s) in tl_0.iter().enumerate() { - button_rect = self.input_button_ui(s, true, &mut columns[index]); - } - }); + /// Draw text content returning button [`Rect`]. + fn text_ui(&mut self, ui: &mut egui::Ui) -> Rect { + let mut button_rect = ui.available_rect_before_wrap(); + let tl_0: Vec<&str> = vec!["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "01"]; + ui.columns(tl_0.len(), |columns| { + for (index, s) in tl_0.iter().enumerate() { + button_rect = self.input_button_ui(s, true, &mut columns[index]); + } + }); - let tl_1: Vec<&str> = vec!["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "p1"]; - ui.columns(tl_1.len(), |columns| { - for (index, s) in tl_1.iter().enumerate() { - self.input_button_ui(s, true, &mut columns[index]); - } - }); + let tl_1: Vec<&str> = vec!["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "p1"]; + ui.columns(tl_1.len(), |columns| { + for (index, s) in tl_1.iter().enumerate() { + self.input_button_ui(s, true, &mut columns[index]); + } + }); - let tl_2: Vec<&str> = vec!["a", "s", "d", "f", "g", "h", "j", "k", "l", "l1", "l2"]; - ui.columns(tl_2.len(), |columns| { - for (index, s) in tl_2.iter().enumerate() { - self.input_button_ui(s, true, &mut columns[index]); - } - }); + let tl_2: Vec<&str> = vec!["a", "s", "d", "f", "g", "h", "j", "k", "l", "l1", "l2"]; + ui.columns(tl_2.len(), |columns| { + for (index, s) in tl_2.iter().enumerate() { + self.input_button_ui(s, true, &mut columns[index]); + } + }); - let tl_3: Vec<&str> = - vec![ARROW_FAT_UP, "z", "x", "c", "v", "b", "n", "m", "m1", "m2", BACKSPACE]; - ui.columns(tl_3.len(), |columns| { - for (index, s) in tl_3.iter().enumerate() { - if index == 0 { - let shift = self.state.shift.load(Ordering::Relaxed); - let color = if shift { - Colors::yellow_dark() - } else { - Colors::inactive_text() - }; - self.custom_button_ui(ARROW_FAT_UP.to_string(), - color, - Some(Colors::fill_lite()), - &mut columns[index], - |_, c| { - c.state.shift.store(!shift, Ordering::Relaxed); - }); - } else if index == tl_3.len() - 1 { - self.custom_button_ui(BACKSPACE.to_string(), - Colors::red(), - Some(Colors::fill_lite()), - &mut columns[index], - |_, c| { - c.state.last_event = - Arc::new(Some(KeyboardEvent::CLEAR)); - }); - } else { - self.input_button_ui(s, true, &mut columns[index]); - } - } - }); + let tl_3: Vec<&str> = vec![ + ARROW_FAT_UP, + "z", + "x", + "c", + "v", + "b", + "n", + "m", + "m1", + "m2", + BACKSPACE, + ]; + ui.columns(tl_3.len(), |columns| { + for (index, s) in tl_3.iter().enumerate() { + if index == 0 { + let shift = self.state.shift.load(Ordering::Relaxed); + let color = if shift { + Colors::yellow_dark() + } else { + Colors::inactive_text() + }; + self.custom_button_ui( + ARROW_FAT_UP.to_string(), + color, + Some(Colors::fill_lite()), + &mut columns[index], + |_, c| { + c.state.shift.store(!shift, Ordering::Relaxed); + }, + ); + } else if index == tl_3.len() - 1 { + self.custom_button_ui( + BACKSPACE.to_string(), + Colors::red(), + Some(Colors::fill_lite()), + &mut columns[index], + |_, c| { + c.state.last_event = Arc::new(Some(KeyboardEvent::CLEAR)); + }, + ); + } else { + self.input_button_ui(s, true, &mut columns[index]); + } + } + }); - button_rect - } + button_rect + } - /// Draw symbols content returning button [`Rect`]. - fn symbols_ui(&mut self, ui: &mut egui::Ui) -> Rect { - let mut button_rect = ui.available_rect_before_wrap(); - let tl_0: Vec<&str> = vec!["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]; - ui.columns(tl_0.len(), |columns| { - for (index, s) in tl_0.iter().enumerate() { - button_rect = self.input_button_ui(s, false, &mut columns[index]); - } - }); + /// Draw symbols content returning button [`Rect`]. + fn symbols_ui(&mut self, ui: &mut egui::Ui) -> Rect { + let mut button_rect = ui.available_rect_before_wrap(); + let tl_0: Vec<&str> = vec!["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]; + ui.columns(tl_0.len(), |columns| { + for (index, s) in tl_0.iter().enumerate() { + button_rect = self.input_button_ui(s, false, &mut columns[index]); + } + }); - let tl_1: Vec<&str> = vec!["_", "\\", "|", "~", "<", ">", "№", "√", "π", "•"]; - ui.columns(tl_1.len(), |columns| { - for (index, s) in tl_1.iter().enumerate() { - self.input_button_ui(s, false, &mut columns[index]); - } - }); + let tl_1: Vec<&str> = vec!["_", "\\", "|", "~", "<", ">", "№", "√", "π", "•"]; + ui.columns(tl_1.len(), |columns| { + for (index, s) in tl_1.iter().enumerate() { + self.input_button_ui(s, false, &mut columns[index]); + } + }); - let tl_2: Vec<&str> = vec!["-", "/", ":", ";", "(", ")", "`", "&", "@", "\""]; - ui.columns(tl_2.len(), |columns| { - for (index, s) in tl_2.iter().enumerate() { - self.input_button_ui(s, false, &mut columns[index]); - } - }); + let tl_2: Vec<&str> = vec!["-", "/", ":", ";", "(", ")", "`", "&", "@", "\""]; + ui.columns(tl_2.len(), |columns| { + for (index, s) in tl_2.iter().enumerate() { + self.input_button_ui(s, false, &mut columns[index]); + } + }); - let tl_3: Vec<&str> = vec![".", ",", "?", "!", "€", "£", "¥", "$", "¢", BACKSPACE]; - ui.columns(tl_3.len(), |columns| { - for (index, s) in tl_3.iter().enumerate() { - if index == tl_3.len() - 1 { - self.custom_button_ui(BACKSPACE.to_string(), - Colors::red(), - Some(Colors::fill_lite()), - &mut columns[index], - |_, c| { - c.state.last_event = - Arc::new(Some(KeyboardEvent::CLEAR)); - }); - } else { - self.input_button_ui(s, false, &mut columns[index]); - } - } - }); + let tl_3: Vec<&str> = vec![".", ",", "?", "!", "€", "£", "¥", "$", "¢", BACKSPACE]; + ui.columns(tl_3.len(), |columns| { + for (index, s) in tl_3.iter().enumerate() { + if index == tl_3.len() - 1 { + self.custom_button_ui( + BACKSPACE.to_string(), + Colors::red(), + Some(Colors::fill_lite()), + &mut columns[index], + |_, c| { + c.state.last_event = Arc::new(Some(KeyboardEvent::CLEAR)); + }, + ); + } else { + self.input_button_ui(s, false, &mut columns[index]); + } + } + }); - button_rect - } + button_rect + } - /// Draw custom keyboard button. - fn custom_button_ui(&mut self, - s: String, - color: Color32, - bg: Option, - ui: &mut egui::Ui, - cb: impl FnOnce(String, &mut KeyboardContent)) -> Response { - ui.vertical_centered_justified(|ui| { - // Disable expansion on click/hover. - ui.style_mut().visuals.widgets.hovered.expansion = 0.0; - ui.style_mut().visuals.widgets.active.expansion = 0.0; - // Setup fill colors. - ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false); - ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite(); - ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill(); - // Setup stroke colors. - ui.visuals_mut().widgets.inactive.bg_stroke = View::item_stroke(); - ui.visuals_mut().widgets.hovered.bg_stroke = View::item_stroke(); - ui.visuals_mut().widgets.active.bg_stroke = View::hover_stroke(); + /// Draw custom keyboard button. + fn custom_button_ui( + &mut self, + s: String, + color: Color32, + bg: Option, + ui: &mut egui::Ui, + cb: impl FnOnce(String, &mut KeyboardContent), + ) -> Response { + ui.vertical_centered_justified(|ui| { + // Disable expansion on click/hover. + ui.style_mut().visuals.widgets.hovered.expansion = 0.0; + ui.style_mut().visuals.widgets.active.expansion = 0.0; + // Setup fill colors. + ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false); + ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite(); + ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill(); + // Setup stroke colors. + ui.visuals_mut().widgets.inactive.bg_stroke = View::item_stroke(); + ui.visuals_mut().widgets.hovered.bg_stroke = View::item_stroke(); + ui.visuals_mut().widgets.active.bg_stroke = View::hover_stroke(); - let shift = self.state.shift.load(Ordering::Relaxed); - let label = if shift { - s.to_uppercase() - } else { - s.to_string() - }; - let mut button = Button::new(RichText::new(label.clone()).size(18.0).color(color)) - .corner_radius(egui::CornerRadius::ZERO); - if let Some(bg) = bg { - button = button.fill(bg); - } - // Setup long press/touch. - let long_press = s == BACKSPACE; - if long_press { - button = button.sense(Sense::click_and_drag()); - } - // Draw button. - let resp = button.ui(ui).on_hover_cursor(CursorIcon::PointingHand); - if resp.clicked() || resp.long_touched() || resp.dragged() { - cb(label, self); - } - }).response - } + let shift = self.state.shift.load(Ordering::Relaxed); + let label = if shift { + s.to_uppercase() + } else { + s.to_string() + }; + let mut button = Button::new(RichText::new(label.clone()).size(18.0).color(color)) + .corner_radius(egui::CornerRadius::ZERO); + if let Some(bg) = bg { + button = button.fill(bg); + } + // Setup long press/touch. + let long_press = s == BACKSPACE; + if long_press { + button = button.sense(Sense::click_and_drag()); + } + // Draw button. + let resp = button.ui(ui).on_hover_cursor(CursorIcon::PointingHand); + if resp.clicked() || resp.long_touched() || resp.dragged() { + cb(label, self); + } + }) + .response + } - /// Draw input button. - fn input_button_ui(&mut self, s: &str, translate: bool, ui: &mut egui::Ui) -> Rect { - let value = if translate { - t!(format!("keyboard.{}", s), locale = Self::input_locale().as_str()).into() - } else { - s.to_string() - }; - let rect = self.custom_button_ui(value, Colors::text_button(), None, ui, |l, c| { - c.state.last_event = Arc::new(Some(KeyboardEvent::TEXT(l))); - c.state.shift.store(false, Ordering::Relaxed); - }).rect; - rect - } + /// Draw input button. + fn input_button_ui(&mut self, s: &str, translate: bool, ui: &mut egui::Ui) -> Rect { + let value = if translate { + t!( + format!("keyboard.{}", s), + locale = Self::input_locale().as_str() + ) + .into() + } else { + s.to_string() + }; + let rect = self + .custom_button_ui(value, Colors::text_button(), None, ui, |l, c| { + c.state.last_event = Arc::new(Some(KeyboardEvent::TEXT(l))); + c.state.shift.store(false, Ordering::Relaxed); + }) + .rect; + rect + } - /// Get input locale. - fn input_locale() -> String { - let english = AppConfig::english_keyboard(); - if english { - "en".to_string() - } else { - AppConfig::locale().unwrap_or("en".to_string()) - } - } + /// Get input locale. + fn input_locale() -> String { + let english = AppConfig::english_keyboard(); + if english { + "en".to_string() + } else { + AppConfig::locale().unwrap_or("en".to_string()) + } + } - /// Check last keyboard input event. - pub fn consume_event() -> Option { - let empty = { - let r_state = WINDOW_STATE.read(); - r_state.last_event.is_none() - }; - if !empty { - let mut w_state = WINDOW_STATE.write(); - let event = w_state.last_event.as_ref().clone().unwrap(); - w_state.last_event = Arc::new(None); - return Some(event); - } - None - } + /// Check last keyboard input event. + pub fn consume_event() -> Option { + let empty = { + let r_state = WINDOW_STATE.read(); + r_state.last_event.is_none() + }; + if !empty { + let mut w_state = WINDOW_STATE.write(); + let event = w_state.last_event.as_ref().clone().unwrap(); + w_state.last_event = Arc::new(None); + return Some(event); + } + None + } - /// Emulate stop of Shift key press. - pub fn unshift() { - let r_state = WINDOW_STATE.read(); - r_state.shift.store(false, Ordering::Relaxed); - } + /// Emulate stop of Shift key press. + pub fn unshift() { + let r_state = WINDOW_STATE.read(); + r_state.shift.store(false, Ordering::Relaxed); + } - /// Reset keyboard window state. - pub fn reset_window_state() { - let mut w_state = WINDOW_STATE.write(); - w_state.layout = Arc::new(KeyboardLayout::TEXT); - // *w_state = KeyboardState::default(); - } -} \ No newline at end of file + /// Reset keyboard window state. + pub fn reset_window_state() { + let mut w_state = WINDOW_STATE.write(); + w_state.layout = Arc::new(KeyboardLayout::TEXT); + // *w_state = KeyboardState::default(); + } +} diff --git a/src/gui/views/input/mod.rs b/src/gui/views/input/mod.rs index da864160..8b258970 100644 --- a/src/gui/views/input/mod.rs +++ b/src/gui/views/input/mod.rs @@ -19,4 +19,4 @@ mod edit; pub use edit::*; mod keyboard; -pub use keyboard::*; \ No newline at end of file +pub use keyboard::*; diff --git a/src/gui/views/input/types.rs b/src/gui/views/input/types.rs index b49184cb..dc771175 100644 --- a/src/gui/views/input/types.rs +++ b/src/gui/views/input/types.rs @@ -18,32 +18,36 @@ use std::sync::atomic::AtomicBool; /// Software keyboard input type. #[derive(Clone, PartialOrd, PartialEq)] pub enum KeyboardLayout { - TEXT, SYMBOLS, NUMBERS + TEXT, + SYMBOLS, + NUMBERS, } /// Software keyboard input event. #[derive(Clone)] pub enum KeyboardEvent { - TEXT(String), CLEAR, ENTER + TEXT(String), + CLEAR, + ENTER, } /// Software keyboard Window State. #[derive(Clone)] pub struct KeyboardState { - /// Last input event. - pub last_event: Arc>, - /// Current layout. - pub layout: Arc, - /// Flag to enter uppercase symbol first. - pub shift: Arc, + /// Last input event. + pub last_event: Arc>, + /// Current layout. + pub layout: Arc, + /// Flag to enter uppercase symbol first. + pub shift: Arc, } impl Default for KeyboardState { - fn default() -> Self { - Self { - last_event: Arc::new(None), - layout: Arc::new(KeyboardLayout::TEXT), - shift: Arc::new(AtomicBool::new(false)), - } - } -} \ No newline at end of file + fn default() -> Self { + Self { + last_event: Arc::new(None), + layout: Arc::new(KeyboardLayout::TEXT), + shift: Arc::new(AtomicBool::new(false)), + } + } +} diff --git a/src/gui/views/mod.rs b/src/gui/views/mod.rs index 4e024fd7..b10f1d8d 100644 --- a/src/gui/views/mod.rs +++ b/src/gui/views/mod.rs @@ -27,8 +27,8 @@ mod content; pub use content::*; pub mod network; -pub mod wallets; pub mod settings; +pub mod wallets; mod camera; pub use camera::*; @@ -46,4 +46,4 @@ mod scan; pub use scan::*; mod input; -pub use input::*; \ No newline at end of file +pub use input::*; diff --git a/src/gui/views/modal.rs b/src/gui/views/modal.rs index 422f0bc9..6ca08cb5 100755 --- a/src/gui/views/modal.rs +++ b/src/gui/views/modal.rs @@ -17,357 +17,373 @@ use egui::os::OperatingSystem; use egui::{Align2, Color32, CornerRadius, RichText, Stroke, StrokeKind, UiBuilder, Vec2}; use lazy_static::lazy_static; use parking_lot::RwLock; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; -use crate::gui::views::types::{ModalPosition, ModalState}; -use crate::gui::views::{Content, View}; use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::types::{ModalPosition, ModalState}; +use crate::gui::views::{Content, View}; lazy_static! { - /// Showing [`Modal`] state to be accessible from different ui parts. - static ref MODAL_STATE: Arc> = Arc::new(RwLock::new(ModalState::default())); + /// Showing [`Modal`] state to be accessible from different ui parts. + static ref MODAL_STATE: Arc> = Arc::new(RwLock::new(ModalState::default())); } /// Modal [`egui::Window`] container. #[derive(Clone)] pub struct Modal { - /// Identifier for modal. - pub(crate) id: &'static str, - /// Position on the screen. - pub position: ModalPosition, - /// Flag to check if modal can be closed by keys. - closeable: Arc, - /// Title text. - title: Option, - /// Flag to check first content render. - first_draw: Arc, - /// Background color. - fill: Option, + /// Identifier for modal. + pub(crate) id: &'static str, + /// Position on the screen. + pub position: ModalPosition, + /// Flag to check if modal can be closed by keys. + closeable: Arc, + /// Title text. + title: Option, + /// Flag to check first content render. + first_draw: Arc, + /// Background color. + fill: Option, } impl Modal { - /// Margin from [`Modal`] window at top/left/right. - const DEFAULT_MARGIN: f32 = 8.0; - /// Maximum width of the content. - const DEFAULT_WIDTH: f32 = Content::SIDE_PANEL_WIDTH - (2.0 * Self::DEFAULT_MARGIN); - /// Modal content [`egui::Window`] id. - pub const WINDOW_ID: &'static str = "modal_window"; + /// Margin from [`Modal`] window at top/left/right. + const DEFAULT_MARGIN: f32 = 8.0; + /// Maximum width of the content. + const DEFAULT_WIDTH: f32 = Content::SIDE_PANEL_WIDTH - (2.0 * Self::DEFAULT_MARGIN); + /// Modal content [`egui::Window`] id. + pub const WINDOW_ID: &'static str = "modal_window"; - /// Create closeable [`Modal`] with center position. - pub fn new(id: &'static str) -> Self { - Self { - id, - position: ModalPosition::Center, - closeable: Arc::new(AtomicBool::new(true)), - title: None, - first_draw: Arc::new(AtomicBool::new(true)), - fill: None, - } - } + /// Create closeable [`Modal`] with center position. + pub fn new(id: &'static str) -> Self { + Self { + id, + position: ModalPosition::Center, + closeable: Arc::new(AtomicBool::new(true)), + title: None, + first_draw: Arc::new(AtomicBool::new(true)), + fill: None, + } + } - /// Setup position of [`Modal`] on the screen. - pub fn position(mut self, position: ModalPosition) -> Self { - self.position = position; - self - } + /// Setup position of [`Modal`] on the screen. + pub fn position(mut self, position: ModalPosition) -> Self { + self.position = position; + self + } - /// Change [`Modal`] position on the screen. - pub fn change_position(position: ModalPosition) { - let mut w_state = MODAL_STATE.write(); - w_state.modal.as_mut().unwrap().position = position; - } + /// Change [`Modal`] position on the screen. + pub fn change_position(position: ModalPosition) { + let mut w_state = MODAL_STATE.write(); + w_state.modal.as_mut().unwrap().position = position; + } - /// Close [`Modal`] by clearing its state. - pub fn close() { - let mut w_nav = MODAL_STATE.write(); - w_nav.modal = None; - } + /// Close [`Modal`] by clearing its state. + pub fn close() { + let mut w_nav = MODAL_STATE.write(); + w_nav.modal = None; + } - /// Setup possibility to close [`Modal`]. - pub fn closeable(self, closeable: bool) -> Self { - self.closeable.store(closeable, Ordering::Relaxed); - self - } + /// Setup possibility to close [`Modal`]. + pub fn closeable(self, closeable: bool) -> Self { + self.closeable.store(closeable, Ordering::Relaxed); + self + } - /// Disable possibility to close [`Modal`]. - pub fn disable_closing(&self) { - self.closeable.store(false, Ordering::Relaxed); - } + /// Disable possibility to close [`Modal`]. + pub fn disable_closing(&self) { + self.closeable.store(false, Ordering::Relaxed); + } - /// Enable possibility to close [`Modal`]. - pub fn enable_closing(&self) { - self.closeable.store(true, Ordering::Relaxed); - } + /// Enable possibility to close [`Modal`]. + pub fn enable_closing(&self) { + self.closeable.store(true, Ordering::Relaxed); + } - /// Check if [`Modal`] is closeable. - pub fn is_closeable(&self) -> bool { - self.closeable.load(Ordering::Relaxed) - } + /// Check if [`Modal`] is closeable. + pub fn is_closeable(&self) -> bool { + self.closeable.load(Ordering::Relaxed) + } - /// Set title text on [`Modal`] creation. - pub fn title(mut self, title: impl Into) -> Self { - self.title = Some(title.into().to_uppercase()); - self - } + /// Set title text on [`Modal`] creation. + pub fn title(mut self, title: impl Into) -> Self { + self.title = Some(title.into().to_uppercase()); + self + } - /// Set [`Modal`] instance into state to show at ui. - pub fn show(self) { - let mut w_nav = MODAL_STATE.write(); - self.first_draw.store(true, Ordering::Relaxed); - w_nav.modal = Some(self); - } + /// Set [`Modal`] instance into state to show at ui. + pub fn show(self) { + let mut w_nav = MODAL_STATE.write(); + self.first_draw.store(true, Ordering::Relaxed); + w_nav.modal = Some(self); + } - /// Remove [`Modal`] from [`ModalState`] if it's showing and can be closed. - /// Return `false` if modal existed in state before call. - pub fn on_back() -> bool { - if Self::opened().is_some() { - if Self::opened_closeable() { - Self::close(); - } - return false; - } - true - } + /// Remove [`Modal`] from [`ModalState`] if it's showing and can be closed. + /// Return `false` if modal existed in state before call. + pub fn on_back() -> bool { + if Self::opened().is_some() { + if Self::opened_closeable() { + Self::close(); + } + return false; + } + true + } - /// Return identifier of opened [`Modal`]. - pub fn opened() -> Option<&'static str> { - // Check if modal is showing. - { - if MODAL_STATE.read().modal.is_none() { - return None; - } - } + /// Return identifier of opened [`Modal`]. + pub fn opened() -> Option<&'static str> { + // Check if modal is showing. + { + if MODAL_STATE.read().modal.is_none() { + return None; + } + } - // Get identifier of opened modal. - let r_state = MODAL_STATE.read(); - let modal = r_state.modal.as_ref().unwrap(); - Some(modal.id) - } + // Get identifier of opened modal. + let r_state = MODAL_STATE.read(); + let modal = r_state.modal.as_ref().unwrap(); + Some(modal.id) + } - /// Check if [`Modal`] is opened and can be closed. - pub fn opened_closeable() -> bool { - // Check if modal is showing. - { - if MODAL_STATE.read().modal.is_none() { - return false; - } - } - let r_state = MODAL_STATE.read(); - let modal = r_state.modal.as_ref().unwrap(); - modal.closeable.load(Ordering::Relaxed) - } + /// Check if [`Modal`] is opened and can be closed. + pub fn opened_closeable() -> bool { + // Check if modal is showing. + { + if MODAL_STATE.read().modal.is_none() { + return false; + } + } + let r_state = MODAL_STATE.read(); + let modal = r_state.modal.as_ref().unwrap(); + modal.closeable.load(Ordering::Relaxed) + } - /// Set title text for current opened [`Modal`]. - pub fn set_title(title: impl Into) { - let mut w_state = MODAL_STATE.write(); - if w_state.modal.is_some() { - let mut modal = w_state.modal.clone().unwrap(); - modal.title = Some(title.into().to_uppercase()); - w_state.modal = Some(modal); - } - } + /// Set title text for current opened [`Modal`]. + pub fn set_title(title: impl Into) { + let mut w_state = MODAL_STATE.write(); + if w_state.modal.is_some() { + let mut modal = w_state.modal.clone().unwrap(); + modal.title = Some(title.into().to_uppercase()); + w_state.modal = Some(modal); + } + } - /// Check for first [`Modal`] content rendering. - pub fn first_draw() -> bool { - if Self::opened().is_none() { - return false; - } - let r_state = MODAL_STATE.read(); - let modal = r_state.modal.as_ref().unwrap(); - modal.first_draw.load(Ordering::Relaxed) - } + /// Check for first [`Modal`] content rendering. + pub fn first_draw() -> bool { + if Self::opened().is_none() { + return false; + } + let r_state = MODAL_STATE.read(); + let modal = r_state.modal.as_ref().unwrap(); + modal.first_draw.load(Ordering::Relaxed) + } - pub fn ui(ctx: &egui::Context, - cb: &dyn PlatformCallbacks, - add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks)) { - let has_modal = { - MODAL_STATE.read().modal.is_some() - }; - if has_modal { - let modal = { - let r_state = MODAL_STATE.read(); - r_state.modal.clone().unwrap() - }; - modal.window_ui(ctx, cb, add_content); - } - } + pub fn ui( + ctx: &egui::Context, + cb: &dyn PlatformCallbacks, + add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks), + ) { + let has_modal = { MODAL_STATE.read().modal.is_some() }; + if has_modal { + let modal = { + let r_state = MODAL_STATE.read(); + r_state.modal.clone().unwrap() + }; + modal.window_ui(ctx, cb, add_content); + } + } - /// Draw [`egui::Window`] with provided content. - fn window_ui(&self, - ctx: &egui::Context, - cb: &dyn PlatformCallbacks, - add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks)) { - let is_fullscreen = ctx.input(|i| { - i.viewport().fullscreen.unwrap_or(false) - }); + /// Draw [`egui::Window`] with provided content. + fn window_ui( + &self, + ctx: &egui::Context, + cb: &dyn PlatformCallbacks, + add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks), + ) { + let is_fullscreen = ctx.input(|i| i.viewport().fullscreen.unwrap_or(false)); - // Setup background rect. - let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os(); - let bg_rect = if View::is_desktop() && !is_win { - let mut r = ctx.content_rect(); - let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os(); - if !is_mac && !is_fullscreen { - r = r.shrink(Content::WINDOW_FRAME_MARGIN - 1.0); - } - r.min.y += Content::WINDOW_TITLE_HEIGHT; - r - } else { - ctx.content_rect() - }; + // Setup background rect. + let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os(); + let bg_rect = if View::is_desktop() && !is_win { + let mut r = ctx.content_rect(); + let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os(); + if !is_mac && !is_fullscreen { + r = r.shrink(Content::WINDOW_FRAME_MARGIN - 1.0); + } + r.min.y += Content::WINDOW_TITLE_HEIGHT; + r + } else { + ctx.content_rect() + }; - // Draw modal background. - egui::Window::new("modal_bg_window") - .title_bar(false) - .resizable(false) - .collapsible(false) - .fixed_rect(bg_rect) - .frame(egui::Frame { - fill: Colors::semi_transparent(), - ..Default::default() - }) - .show(ctx, |ui| { - ui.set_min_size(bg_rect.size()); - }); + // Draw modal background. + egui::Window::new("modal_bg_window") + .title_bar(false) + .resizable(false) + .collapsible(false) + .fixed_rect(bg_rect) + .frame(egui::Frame { + fill: Colors::semi_transparent(), + ..Default::default() + }) + .show(ctx, |ui| { + ui.set_min_size(bg_rect.size()); + }); - // Setup width of modal content. - let side_insets = View::get_left_inset() + View::get_right_inset(); - let available_width = ctx.content_rect().width() - (side_insets + Self::DEFAULT_MARGIN); - let width = f32::min(available_width, Self::DEFAULT_WIDTH); + // Setup width of modal content. + let side_insets = View::get_left_inset() + View::get_right_inset(); + let available_width = ctx.content_rect().width() - (side_insets + Self::DEFAULT_MARGIN); + let width = f32::min(available_width, Self::DEFAULT_WIDTH); - // Show main content window at given position. - let (content_align, content_offset) = self.modal_position(); - egui::Window::new(Self::WINDOW_ID) - .title_bar(false) - .resizable(false) - .collapsible(false) - .min_width(width) - .default_width(width) - .anchor(content_align, content_offset) - .frame(egui::Frame { - shadow: Shadow { - offset: Default::default(), - blur: 30.0 as u8, - spread: 3.0 as u8, - color: egui::Color32::from_black_alpha(32), - }, - corner_radius: CornerRadius::same(8.0 as u8), - ..Default::default() - }) - .show(ctx, |ui| { - if let Some(title) = &self.title { - title_ui(title, ui); - } - self.content_ui(ui, cb, add_content); - }); + // Show main content window at given position. + let (content_align, content_offset) = self.modal_position(); + egui::Window::new(Self::WINDOW_ID) + .title_bar(false) + .resizable(false) + .collapsible(false) + .min_width(width) + .default_width(width) + .anchor(content_align, content_offset) + .frame(egui::Frame { + shadow: Shadow { + offset: Default::default(), + blur: 30.0 as u8, + spread: 3.0 as u8, + color: egui::Color32::from_black_alpha(32), + }, + corner_radius: CornerRadius::same(8.0 as u8), + ..Default::default() + }) + .show(ctx, |ui| { + if let Some(title) = &self.title { + title_ui(title, ui); + } + self.content_ui(ui, cb, add_content); + }); - // Setup first draw flag. - if Self::first_draw() { - let r_state = MODAL_STATE.read(); - let modal = r_state.modal.as_ref().unwrap(); - modal.first_draw.store(false, Ordering::Relaxed); - } - } + // Setup first draw flag. + if Self::first_draw() { + let r_state = MODAL_STATE.read(); + let modal = r_state.modal.as_ref().unwrap(); + modal.first_draw.store(false, Ordering::Relaxed); + } + } - /// Get [`egui::Window`] position based on [`ModalPosition`]. - fn modal_position(&self) -> (Align2, Vec2) { - let align = match self.position { - ModalPosition::CenterTop => Align2::CENTER_TOP, - ModalPosition::Center => Align2::CENTER_CENTER - }; + /// Get [`egui::Window`] position based on [`ModalPosition`]. + fn modal_position(&self) -> (Align2, Vec2) { + let align = match self.position { + ModalPosition::CenterTop => Align2::CENTER_TOP, + ModalPosition::Center => Align2::CENTER_CENTER, + }; - let x_align = View::get_left_inset() - View::get_right_inset(); - let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os(); - let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os(); - let extra_y = if View::is_desktop() && !is_win { - Content::WINDOW_TITLE_HEIGHT + if !is_mac { - Content::WINDOW_FRAME_MARGIN - } else { - 0.0 - } - } else { - 0.0 - }; - let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN / 2.0 + extra_y; + let x_align = View::get_left_inset() - View::get_right_inset(); + let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os(); + let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os(); + let extra_y = if View::is_desktop() && !is_win { + Content::WINDOW_TITLE_HEIGHT + + if !is_mac { + Content::WINDOW_FRAME_MARGIN + } else { + 0.0 + } + } else { + 0.0 + }; + let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN / 2.0 + extra_y; - let offset = match self.position { - ModalPosition::CenterTop => Vec2::new(x_align, y_align), - ModalPosition::Center => Vec2::new(x_align, 0.0) - }; - (align, offset) - } + let offset = match self.position { + ModalPosition::CenterTop => Vec2::new(x_align, y_align), + ModalPosition::Center => Vec2::new(x_align, 0.0), + }; + (align, offset) + } - /// Set custom background color. - pub fn set_background_color(&self, color: Color32) { - let mut w_state = MODAL_STATE.write(); - w_state.modal.as_mut().unwrap().fill = Some(color); - } + /// Set custom background color. + pub fn set_background_color(&self, color: Color32) { + let mut w_state = MODAL_STATE.write(); + w_state.modal.as_mut().unwrap().fill = Some(color); + } - /// Draw provided content. - fn content_ui(&self, - ui: &mut egui::Ui, - cb: &dyn PlatformCallbacks, - add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks)) { - let mut rect = ui.available_rect_before_wrap(); + /// Draw provided content. + fn content_ui( + &self, + ui: &mut egui::Ui, + cb: &dyn PlatformCallbacks, + add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks), + ) { + let mut rect = ui.available_rect_before_wrap(); - // Create background shape. - let mut bg_shape = RectShape::new(rect, if self.title.is_none() { - CornerRadius::same(8.0 as u8) - } else { - CornerRadius { - nw: 0.0 as u8, - ne: 0.0 as u8, - sw: 8.0 as u8, - se: 8.0 as u8, - } - }, self.fill.unwrap_or(Colors::fill_lite()), Stroke::NONE, StrokeKind::Outside); - let bg_idx = ui.painter().add(bg_shape.clone()); + // Create background shape. + let mut bg_shape = RectShape::new( + rect, + if self.title.is_none() { + CornerRadius::same(8.0 as u8) + } else { + CornerRadius { + nw: 0.0 as u8, + ne: 0.0 as u8, + sw: 8.0 as u8, + se: 8.0 as u8, + } + }, + self.fill.unwrap_or(Colors::fill_lite()), + Stroke::NONE, + StrokeKind::Outside, + ); + let bg_idx = ui.painter().add(bg_shape.clone()); - rect.min += egui::emath::vec2(6.0, 0.0); - rect.max -= egui::emath::vec2(6.0, 0.0); - let resp = ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| { - (add_content)(ui, self, cb); - }).response; + rect.min += egui::emath::vec2(6.0, 0.0); + rect.max -= egui::emath::vec2(6.0, 0.0); + let resp = ui + .scope_builder(UiBuilder::new().max_rect(rect), |ui| { + (add_content)(ui, self, cb); + }) + .response; - // Setup background size. - let bg_rect = { - let mut r = resp.rect.clone(); - r.min -= egui::emath::vec2(6.0, 0.0); - r.max += egui::emath::vec2(6.0, 0.0); - r - }; - bg_shape.rect = bg_rect; - ui.painter().set(bg_idx, bg_shape); - } + // Setup background size. + let bg_rect = { + let mut r = resp.rect.clone(); + r.min -= egui::emath::vec2(6.0, 0.0); + r.max += egui::emath::vec2(6.0, 0.0); + r + }; + bg_shape.rect = bg_rect; + ui.painter().set(bg_idx, bg_shape); + } } /// Draw title content. fn title_ui(title: &String, ui: &mut egui::Ui) { - let rect = ui.available_rect_before_wrap(); + let rect = ui.available_rect_before_wrap(); - // Create background shape. - let mut bg_shape = RectShape::new(rect, CornerRadius { - nw: 8.0 as u8, - ne: 8.0 as u8, - sw: 0.0 as u8, - se: 0.0 as u8, - }, Colors::yellow(), Stroke::NONE, StrokeKind::Outside); - let bg_idx = ui.painter().add(bg_shape.clone()); + // Create background shape. + let mut bg_shape = RectShape::new( + rect, + CornerRadius { + nw: 8.0 as u8, + ne: 8.0 as u8, + sw: 0.0 as u8, + se: 0.0 as u8, + }, + Colors::yellow(), + Stroke::NONE, + StrokeKind::Outside, + ); + let bg_idx = ui.painter().add(bg_shape.clone()); - // Draw title content. - let resp = ui.vertical_centered(|ui| { - ui.add_space(Modal::DEFAULT_MARGIN + 2.0); - ui.label(RichText::new(title) - .size(19.0) - .color(Colors::title(true)) - ); - ui.add_space(Modal::DEFAULT_MARGIN + 1.0); - // Draw line below title. - View::horizontal_line(ui, Colors::item_stroke()); - }).response; + // Draw title content. + let resp = ui + .vertical_centered(|ui| { + ui.add_space(Modal::DEFAULT_MARGIN + 2.0); + ui.label(RichText::new(title).size(19.0).color(Colors::title(true))); + ui.add_space(Modal::DEFAULT_MARGIN + 1.0); + // Draw line below title. + View::horizontal_line(ui, Colors::item_stroke()); + }) + .response; - // Setup background size. - bg_shape.rect = resp.rect; - ui.painter().set(bg_idx, bg_shape); -} \ No newline at end of file + // Setup background size. + bg_shape.rect = resp.rect; + ui.painter().set(bg_idx, bg_shape); +} diff --git a/src/gui/views/network/connections.rs b/src/gui/views/network/connections.rs index 03c3df09..b21f4f0a 100644 --- a/src/gui/views/network/connections.rs +++ b/src/gui/views/network/connections.rs @@ -13,328 +13,381 @@ // limitations under the License. use eframe::epaint::RectShape; -use egui::{Align, Color32, CornerRadius, CursorIcon, Layout, RichText, Sense, StrokeKind, UiBuilder}; +use egui::{ + Align, Color32, CornerRadius, CursorIcon, Layout, RichText, Sense, StrokeKind, UiBuilder, +}; -use crate::gui::icons::{CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PLUS_CIRCLE, POWER, QR_CODE, TRASH, WARNING_CIRCLE, X_CIRCLE}; +use crate::AppConfig; +use crate::gui::Colors; +use crate::gui::icons::{ + CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PLUS_CIRCLE, POWER, QR_CODE, + TRASH, WARNING_CIRCLE, X_CIRCLE, +}; use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::network::NodeSetup; use crate::gui::views::network::modals::{ExternalConnectionModal, ShareConnectionContent}; use crate::gui::views::network::types::ShareConnection; -use crate::gui::views::network::NodeSetup; use crate::gui::views::types::{ContentContainer, ModalPosition}; use crate::gui::views::{Modal, View}; -use crate::gui::Colors; use crate::node::{Node, NodeConfig}; use crate::wallet::{ConnectionsConfig, ExternalConnection}; -use crate::AppConfig; /// Network connections content. pub struct ConnectionsContent { - /// Flag to check connections state on first draw. - first_draw: bool, + /// Flag to check connections state on first draw. + first_draw: bool, - /// External connection [`Modal`] content. - ext_conn_modal_content: ExternalConnectionModal, + /// External connection [`Modal`] content. + ext_conn_modal_content: ExternalConnectionModal, - /// [`Modal`] content to share connection with QR code. - share_conn_modal_content: Option + /// [`Modal`] content to share connection with QR code. + share_conn_modal_content: Option, } impl Default for ConnectionsContent { - fn default() -> Self { - Self { - first_draw: true, - ext_conn_modal_content: ExternalConnectionModal::new(None), - share_conn_modal_content: None - } - } + fn default() -> Self { + Self { + first_draw: true, + ext_conn_modal_content: ExternalConnectionModal::new(None), + share_conn_modal_content: None, + } + } } /// Identifier for [`Modal`] to share connection. const SHARE_CONN_QR_MODAL: &'static str = "share_conn_qr_modal"; impl ContentContainer for ConnectionsContent { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - ExternalConnectionModal::NETWORK_ID, - SHARE_CONN_QR_MODAL - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![ExternalConnectionModal::NETWORK_ID, SHARE_CONN_QR_MODAL] + } - fn modal_ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - match modal.id { - ExternalConnectionModal::NETWORK_ID => { - self.ext_conn_modal_content.ui(ui, cb, modal, |_| {}); - }, - SHARE_CONN_QR_MODAL => { - if let Some(c) = self.share_conn_modal_content.as_mut() { - c.ui(ui, modal, cb); - } - } - _ => {} - } - } + fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + match modal.id { + ExternalConnectionModal::NETWORK_ID => { + self.ext_conn_modal_content.ui(ui, cb, modal, |_| {}); + } + SHARE_CONN_QR_MODAL => { + if let Some(c) = self.share_conn_modal_content.as_mut() { + c.ui(ui, modal, cb); + } + } + _ => {} + } + } - fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { - // Check connections state on first draw. - if self.first_draw { - ExternalConnection::check(None, ui.ctx()); - self.first_draw = false; - } + fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { + // Check connections state on first draw. + if self.first_draw { + ExternalConnection::check(None, ui.ctx()); + self.first_draw = false; + } - ui.add_space(2.0); + ui.add_space(2.0); - // Show network type selection. - let saved_chain_type = AppConfig::chain_type(); - NodeSetup::chain_type_ui(ui); - ui.add_space(6.0); + // Show network type selection. + let saved_chain_type = AppConfig::chain_type(); + NodeSetup::chain_type_ui(ui); + ui.add_space(6.0); - // Check connections availability. - if saved_chain_type != AppConfig::chain_type() { - ExternalConnection::check(None, ui.ctx()); - } + // Check connections availability. + if saved_chain_type != AppConfig::chain_type() { + ExternalConnection::check(None, ui.ctx()); + } - // Show integrated node info content. - Self::integrated_node_item_ui(ui, Colors::fill_lite(), (true, || { - AppConfig::toggle_show_connections_network_panel(); - }), |ui| { - let r = View::item_rounding(0, 1, true); - View::item_button(ui, r, QR_CODE, None, || { - if let Ok(c) = ShareConnectionContent::new(ShareConnection { - url: format!("http://{}", NodeConfig::get_api_address()), - username: "grin".to_string(), - secret: NodeConfig::get_api_secret(true).unwrap_or("".to_string()), - }) { - self.share_conn_modal_content = Some(c); - // Show QR code to share integrated node connection. - Modal::new(SHARE_CONN_QR_MODAL) - .position(ModalPosition::Center) - .title(t!("network.node")) - .show(); - } - }); - true - }); + // Show integrated node info content. + Self::integrated_node_item_ui( + ui, + Colors::fill_lite(), + (true, || { + AppConfig::toggle_show_connections_network_panel(); + }), + |ui| { + let r = View::item_rounding(0, 1, true); + View::item_button(ui, r, QR_CODE, None, || { + if let Ok(c) = ShareConnectionContent::new(ShareConnection { + url: format!("http://{}", NodeConfig::get_api_address()), + username: "grin".to_string(), + secret: NodeConfig::get_api_secret(true).unwrap_or("".to_string()), + }) { + self.share_conn_modal_content = Some(c); + // Show QR code to share integrated node connection. + Modal::new(SHARE_CONN_QR_MODAL) + .position(ModalPosition::Center) + .title(t!("network.node")) + .show(); + } + }); + true + }, + ); - // Show external connections. - ui.add_space(8.0); - ui.label(RichText::new(t!("wallets.ext_conn")).size(16.0).color(Colors::gray())); - ui.add_space(6.0); + // Show external connections. + ui.add_space(8.0); + ui.label( + RichText::new(t!("wallets.ext_conn")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - // Show button to add new external node connection. - let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node")); - View::button(ui, add_node_text, Colors::white_or_black(false), || { - self.show_add_ext_conn_modal(None); - }); + // Show button to add new external node connection. + let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node")); + View::button(ui, add_node_text, Colors::white_or_black(false), || { + self.show_add_ext_conn_modal(None); + }); - ui.add_space(4.0); + ui.add_space(4.0); - let ext_conn_list = ConnectionsConfig::ext_conn_list(); - let len = ext_conn_list.len(); - if len != 0 { - ui.add_space(8.0); - for (i, c) in ext_conn_list.iter().enumerate() { - ui.horizontal_wrapped(|ui| { - let mut show_qr_content: Option = None; - // Draw external connection list item. - let bg = Colors::fill_lite(); - Self::ext_conn_item_ui(ui, bg, c, i, len, (true, || { - self.show_add_ext_conn_modal(Some(c.clone())); - }), |ui| { - // Draw button to delete connection. - let r = View::item_rounding(i, len, true); - View::item_button(ui, r, TRASH, Some(Colors::inactive_text()), || { - ConnectionsConfig::remove_ext_conn(c.id); - }); - // Draw button to share connection - let r = CornerRadius::default(); - View::item_button(ui, r, QR_CODE, None, || { - if let Ok(c) = ShareConnectionContent::new(ShareConnection { - url: c.url.clone(), - username: "grin".to_string(), - secret: c.secret.clone().unwrap_or("".to_string()), - }) { - show_qr_content = Some(c); - } - }); - }); - if let Some(c) = show_qr_content { - self.share_conn_modal_content = Some(c); - // Show QR code to share external connection. - Modal::new(SHARE_CONN_QR_MODAL) - .position(ModalPosition::Center) - .title(t!("wallets.ext_conn").replace(":", "")) - .show(); - } - }); - } - } - } + let ext_conn_list = ConnectionsConfig::ext_conn_list(); + let len = ext_conn_list.len(); + if len != 0 { + ui.add_space(8.0); + for (i, c) in ext_conn_list.iter().enumerate() { + ui.horizontal_wrapped(|ui| { + let mut show_qr_content: Option = None; + // Draw external connection list item. + let bg = Colors::fill_lite(); + Self::ext_conn_item_ui( + ui, + bg, + c, + i, + len, + (true, || { + self.show_add_ext_conn_modal(Some(c.clone())); + }), + |ui| { + // Draw button to delete connection. + let r = View::item_rounding(i, len, true); + View::item_button(ui, r, TRASH, Some(Colors::inactive_text()), || { + ConnectionsConfig::remove_ext_conn(c.id); + }); + // Draw button to share connection + let r = CornerRadius::default(); + View::item_button(ui, r, QR_CODE, None, || { + if let Ok(c) = ShareConnectionContent::new(ShareConnection { + url: c.url.clone(), + username: "grin".to_string(), + secret: c.secret.clone().unwrap_or("".to_string()), + }) { + show_qr_content = Some(c); + } + }); + }, + ); + if let Some(c) = show_qr_content { + self.share_conn_modal_content = Some(c); + // Show QR code to share external connection. + Modal::new(SHARE_CONN_QR_MODAL) + .position(ModalPosition::Center) + .title(t!("wallets.ext_conn").replace(":", "")) + .show(); + } + }); + } + } + } } impl ConnectionsContent { - /// Draw integrated node connection item content. - pub fn integrated_node_item_ui(ui: &mut egui::Ui, - bg: Color32, - on_click: (bool, impl FnOnce()), - custom_button: impl FnOnce(&mut egui::Ui) -> bool) { - // Draw round background. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(78.0); - let r = View::item_rounding(0, 1, false); - let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); - let bg_idx = ui.painter().add(bg_shape.clone()); + /// Draw integrated node connection item content. + pub fn integrated_node_item_ui( + ui: &mut egui::Ui, + bg: Color32, + on_click: (bool, impl FnOnce()), + custom_button: impl FnOnce(&mut egui::Ui) -> bool, + ) { + // Draw round background. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(78.0); + let r = View::item_rounding(0, 1, false); + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - let res = ui.scope_builder( - UiBuilder::new() - .sense(Sense::click()) - .layout(Layout::right_to_left(Align::Center)) - .max_rect(rect), |ui| { - // Draw custom button. - let extra_button = custom_button(ui); + let res = ui + .scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), + |ui| { + // Draw custom button. + let extra_button = custom_button(ui); - // Draw buttons to start/stop node. - if Node::get_error().is_none() { - let rounding = if extra_button { - CornerRadius::default() - } else { - View::item_rounding(0, 1, true) - }; - if !Node::is_running() { - View::item_button(ui, rounding, POWER, Some(Colors::green()), || { - Node::start(); - }); - } else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() { - View::item_button(ui, rounding, POWER, Some(Colors::red()), || { - Node::stop(false); - }); - } - } + // Draw buttons to start/stop node. + if Node::get_error().is_none() { + let rounding = if extra_button { + CornerRadius::default() + } else { + View::item_rounding(0, 1, true) + }; + if !Node::is_running() { + View::item_button(ui, rounding, POWER, Some(Colors::green()), || { + Node::start(); + }); + } else if !Node::is_starting() + && !Node::is_stopping() + && !Node::is_restarting() + { + View::item_button(ui, rounding, POWER, Some(Colors::red()), || { + Node::stop(false); + }); + } + } - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - ui.add_space(3.0); - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.add_space(1.0); - ui.label(RichText::new(t!("network.node")) - .size(18.0) - .color(Colors::title(false))); - }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout( + layout_size, + Layout::left_to_right(Align::Center), + |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(3.0); + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.add_space(1.0); + ui.label( + RichText::new(t!("network.node")) + .size(18.0) + .color(Colors::title(false)), + ); + }); - // Setup node status text. - let has_error = Node::get_error().is_some(); - let status_icon = if has_error { - WARNING_CIRCLE - } else if !Node::is_running() { - X_CIRCLE - } else if Node::not_syncing() { - CHECK_CIRCLE - } else { - DOTS_THREE_CIRCLE - }; - let status_text = format!("{} {}", status_icon, if has_error { - t!("error").into() - } else { - Node::get_sync_status_text() - }); - View::ellipsize_text(ui, status_text, 15.0, Colors::text(false)); - ui.add_space(1.0); + // Setup node status text. + let has_error = Node::get_error().is_some(); + let status_icon = if has_error { + WARNING_CIRCLE + } else if !Node::is_running() { + X_CIRCLE + } else if Node::not_syncing() { + CHECK_CIRCLE + } else { + DOTS_THREE_CIRCLE + }; + let status_text = format!( + "{} {}", + status_icon, + if has_error { + t!("error").into() + } else { + Node::get_sync_status_text() + } + ); + View::ellipsize_text(ui, status_text, 15.0, Colors::text(false)); + ui.add_space(1.0); - // Setup node API address text. - let api_address = NodeConfig::get_api_address(); - let address_text = format!("{} http://{}", COMPUTER_TOWER, api_address); - ui.label(RichText::new(address_text).size(15.0).color(Colors::gray())); - }) - }); - }).response; - let (clickable, on_click) = on_click; - let clicked = res.clicked() || res.long_touched(); - // Setup background and cursor. - if clickable && res.hovered() { - res.on_hover_cursor(CursorIcon::PointingHand); - bg_shape.fill = Colors::fill(); - } - ui.painter().set(bg_idx, bg_shape); - // Handle clicks on layout. - if clicked && clickable { - on_click(); - } - } + // Setup node API address text. + let api_address = NodeConfig::get_api_address(); + let address_text = + format!("{} http://{}", COMPUTER_TOWER, api_address); + ui.label( + RichText::new(address_text).size(15.0).color(Colors::gray()), + ); + }) + }, + ); + }, + ) + .response; + let (clickable, on_click) = on_click; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if clickable && res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked && clickable { + on_click(); + } + } - /// Draw external connection item content. - pub fn ext_conn_item_ui(ui: &mut egui::Ui, - bg: Color32, - conn: &ExternalConnection, - index: usize, - len: usize, - on_click: (bool, impl FnOnce()), - custom_button: impl FnOnce(&mut egui::Ui)) { - // Draw round background. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(52.0); - let r = View::item_rounding(index, len, false); - let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); - let bg_idx = ui.painter().add(bg_shape.clone()); + /// Draw external connection item content. + pub fn ext_conn_item_ui( + ui: &mut egui::Ui, + bg: Color32, + conn: &ExternalConnection, + index: usize, + len: usize, + on_click: (bool, impl FnOnce()), + custom_button: impl FnOnce(&mut egui::Ui), + ) { + // Draw round background. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(52.0); + let r = View::item_rounding(index, len, false); + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - let res = ui.scope_builder( - UiBuilder::new() - .sense(Sense::click()) - .layout(Layout::right_to_left(Align::Center)) - .max_rect(rect), |ui| { - // Draw custom button. - custom_button(ui); + let res = ui + .scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), + |ui| { + // Draw custom button. + custom_button(ui); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - // Draw connections URL. - ui.add_space(4.0); - let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url); - View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false)); - ui.add_space(1.0); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout( + layout_size, + Layout::left_to_right(Align::Center), + |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + // Draw connections URL. + ui.add_space(4.0); + let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url); + View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false)); + ui.add_space(1.0); - // Setup connection status text. - let status_text = if let Some(available) = conn.available { - if available { - format!("{} {}", CHECK_CIRCLE, t!("network.available")) - } else { - format!("{} {}", X_CIRCLE, t!("network.not_available")) - } - } else { - format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check")) - }; - ui.label(RichText::new(status_text).size(15.0).color(Colors::gray())); - ui.add_space(3.0); - }); - }); - } - ).response; - let (clickable, on_click) = on_click; - let clicked = res.clicked() || res.long_touched(); - // Setup background and cursor. - if clickable && res.hovered() { - res.on_hover_cursor(CursorIcon::PointingHand); - bg_shape.fill = Colors::fill(); - } - ui.painter().set(bg_idx, bg_shape); - // Handle clicks on layout. - if clicked && clickable { - on_click(); - } - } + // Setup connection status text. + let status_text = if let Some(available) = conn.available { + if available { + format!("{} {}", CHECK_CIRCLE, t!("network.available")) + } else { + format!("{} {}", X_CIRCLE, t!("network.not_available")) + } + } else { + format!( + "{} {}", + DOTS_THREE_CIRCLE, + t!("network.availability_check") + ) + }; + ui.label( + RichText::new(status_text).size(15.0).color(Colors::gray()), + ); + ui.add_space(3.0); + }); + }, + ); + }, + ) + .response; + let (clickable, on_click) = on_click; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if clickable && res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked && clickable { + on_click(); + } + } - /// Show [`Modal`] to add external connection. - pub fn show_add_ext_conn_modal(&mut self, conn: Option) { - self.ext_conn_modal_content = ExternalConnectionModal::new(conn); - // Show modal. - Modal::new(ExternalConnectionModal::NETWORK_ID) - .position(ModalPosition::CenterTop) - .title(t!("wallets.add_node")) - .show(); - } -} \ No newline at end of file + /// Show [`Modal`] to add external connection. + pub fn show_add_ext_conn_modal(&mut self, conn: Option) { + self.ext_conn_modal_content = ExternalConnectionModal::new(conn); + // Show modal. + Modal::new(ExternalConnectionModal::NETWORK_ID) + .position(ModalPosition::CenterTop) + .title(t!("wallets.add_node")) + .show(); + } +} diff --git a/src/gui/views/network/content.rs b/src/gui/views/network/content.rs index 41f119a8..7d2cc03a 100644 --- a/src/gui/views/network/content.rs +++ b/src/gui/views/network/content.rs @@ -15,423 +15,432 @@ use egui::scroll_area::ScrollBarVisibility; use egui::{Id, Margin, RichText, ScrollArea}; -use crate::gui::icons::{ARROWS_COUNTER_CLOCKWISE, ARROW_LEFT, BRIEFCASE, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE, GEAR, GLOBE, POWER}; +use crate::AppConfig; +use crate::gui::Colors; +use crate::gui::icons::{ + ARROW_LEFT, ARROWS_COUNTER_CLOCKWISE, BRIEFCASE, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, + FACTORY, FADERS, GAUGE, GEAR, GLOBE, POWER, +}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::network::types::{NodeTab, NodeTabType}; -use crate::gui::views::network::{ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings}; +use crate::gui::views::network::{ + ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings, +}; use crate::gui::views::settings::SettingsContent; use crate::gui::views::types::{ContentContainer, LinePosition, TitleContentType, TitleType}; use crate::gui::views::{Content, TitlePanel, View}; -use crate::gui::Colors; use crate::node::{Node, NodeConfig, NodeError}; -use crate::AppConfig; /// Network content. pub struct NetworkContent { - /// Current integrated node tab content. - node_tab_content: Box, - /// Connections content. - connections: ConnectionsContent, + /// Current integrated node tab content. + node_tab_content: Box, + /// Connections content. + connections: ConnectionsContent, - /// Application settings content. - settings_content: Option, + /// Application settings content. + settings_content: Option, } impl Default for NetworkContent { - fn default() -> Self { - Self { - node_tab_content: Box::new(NetworkNode::default()), - connections: ConnectionsContent::default(), - settings_content: None, - } - } + fn default() -> Self { + Self { + node_tab_content: Box::new(NetworkNode::default()), + connections: ConnectionsContent::default(), + settings_content: None, + } + } } impl NetworkContent { - pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - let show_settings = self.showing_settings(); - let show_connections = AppConfig::show_connections_network_panel(); - let dual_panel = Content::is_dual_panel_mode(ui.ctx()); + pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + let show_settings = self.showing_settings(); + let show_connections = AppConfig::show_connections_network_panel(); + let dual_panel = Content::is_dual_panel_mode(ui.ctx()); - // Show title panel. - self.title_ui(ui, dual_panel, show_connections); + // Show title panel. + self.title_ui(ui, dual_panel, show_connections); - // Show integrated node tabs content. - if !show_connections && !show_settings { - let side_padding = View::TAB_ITEMS_PADDING + if View::is_desktop() { - 0.0 - } else { - 4.0 - }; - let tabs_margin = Margin { - left: (View::get_left_inset() + side_padding) as i8, - right: (View::far_right_inset_margin(ui) + side_padding) as i8, - top: View::TAB_ITEMS_PADDING as i8, - bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8, - }; - egui::TopBottomPanel::bottom("network_tabs_content") - .min_height(0.5) - .resizable(false) - .frame(egui::Frame { - inner_margin: tabs_margin, - fill: Colors::fill(), - ..Default::default() - }) - .show_inside(ui, |ui| { - let rect = ui.available_rect_before_wrap(); - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - self.tabs_ui(ui); - }); - // Draw content divider line. - let r = { - let mut r = rect.clone(); - r.min.x -= tabs_margin.left as f32; - r.min.y -= tabs_margin.top as f32; - r.max.x += tabs_margin.right as f32; - r - }; - View::line(ui, LinePosition::TOP, &r, Colors::stroke()); - }); - } + // Show integrated node tabs content. + if !show_connections && !show_settings { + let side_padding = View::TAB_ITEMS_PADDING + if View::is_desktop() { 0.0 } else { 4.0 }; + let tabs_margin = Margin { + left: (View::get_left_inset() + side_padding) as i8, + right: (View::far_right_inset_margin(ui) + side_padding) as i8, + top: View::TAB_ITEMS_PADDING as i8, + bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8, + }; + egui::TopBottomPanel::bottom("network_tabs_content") + .min_height(0.5) + .resizable(false) + .frame(egui::Frame { + inner_margin: tabs_margin, + fill: Colors::fill(), + ..Default::default() + }) + .show_inside(ui, |ui| { + let rect = ui.available_rect_before_wrap(); + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + self.tabs_ui(ui); + }); + // Draw content divider line. + let r = { + let mut r = rect.clone(); + r.min.x -= tabs_margin.left as f32; + r.min.y -= tabs_margin.top as f32; + r.max.x += tabs_margin.right as f32; + r + }; + View::line(ui, LinePosition::TOP, &r, Colors::stroke()); + }); + } - // Show settings or integrated node content. - egui::SidePanel::right("network_side_content") - .resizable(false) - .exact_width(ui.available_width()) - .frame(egui::Frame { - ..Default::default() - }) - .show_animated_inside(ui, show_settings || !show_connections, |ui| { - egui::CentralPanel::default() - .frame(egui::Frame { - inner_margin: Margin { - left: (View::get_left_inset() + View::content_padding()) as i8, - right: (View::far_right_inset_margin(ui) + - View::content_padding()) as i8, - top: 3.0 as i8, - bottom: 4.0 as i8, - }, - ..Default::default() - }) - .show_inside(ui, |ui| { - let rect = ui.available_rect_before_wrap(); - if let Some(c) = &mut self.settings_content { - ScrollArea::vertical() - .id_salt("app_settings_network") - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(1.0); - ui.vertical_centered(|ui| { - // Show application settings content. - View::max_width_ui(ui, - Content::SIDE_PANEL_WIDTH * 1.3, - |ui| { - c.ui(ui, cb); - }); - }); - }); - } else if self.node_tab_content.get_type() != NodeTabType::Settings { - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - let node_err = Node::get_error(); - if let Some(err) = node_err { - node_error_ui(ui, err); - } else if !Node::is_running() { - disabled_node_ui(ui); - } else if Node::get_stats().is_none() || Node::is_restarting() || - Node::is_stopping() { - NetworkContent::loading_ui(ui, None::); - } else { - self.node_tab_content.tab_ui(ui, cb); - } - }); - } else { - self.node_tab_content.tab_ui(ui, cb); - } - // Draw content divider line. - let r = { - let mut r = rect.clone(); - r.min.y -= 3.0; - r.max.x += View::content_padding(); - r.max.y += View::content_padding(); - r - }; - if dual_panel { - View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke()); - } - }); - }); + // Show settings or integrated node content. + egui::SidePanel::right("network_side_content") + .resizable(false) + .exact_width(ui.available_width()) + .frame(egui::Frame { + ..Default::default() + }) + .show_animated_inside(ui, show_settings || !show_connections, |ui| { + egui::CentralPanel::default() + .frame(egui::Frame { + inner_margin: Margin { + left: (View::get_left_inset() + View::content_padding()) as i8, + right: (View::far_right_inset_margin(ui) + View::content_padding()) + as i8, + top: 3.0 as i8, + bottom: 4.0 as i8, + }, + ..Default::default() + }) + .show_inside(ui, |ui| { + let rect = ui.available_rect_before_wrap(); + if let Some(c) = &mut self.settings_content { + ScrollArea::vertical() + .id_salt("app_settings_network") + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(1.0); + ui.vertical_centered(|ui| { + // Show application settings content. + View::max_width_ui( + ui, + Content::SIDE_PANEL_WIDTH * 1.3, + |ui| { + c.ui(ui, cb); + }, + ); + }); + }); + } else if self.node_tab_content.get_type() != NodeTabType::Settings { + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + let node_err = Node::get_error(); + if let Some(err) = node_err { + node_error_ui(ui, err); + } else if !Node::is_running() { + disabled_node_ui(ui); + } else if Node::get_stats().is_none() + || Node::is_restarting() || Node::is_stopping() + { + NetworkContent::loading_ui(ui, None::); + } else { + self.node_tab_content.tab_ui(ui, cb); + } + }); + } else { + self.node_tab_content.tab_ui(ui, cb); + } + // Draw content divider line. + let r = { + let mut r = rect.clone(); + r.min.y -= 3.0; + r.max.x += View::content_padding(); + r.max.y += View::content_padding(); + r + }; + if dual_panel { + View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke()); + } + }); + }); - // Show connections content. - egui::CentralPanel::default() - .frame(egui::Frame { - inner_margin: Margin { - left: if show_connections { - View::get_left_inset() + View::content_padding() - } else { - 0.0 - } as i8, - right: if show_connections { - View::far_right_inset_margin(ui) + View::content_padding() - } else { - 0.0 - } as i8, - top: 3.0 as i8, - bottom: 0.0 as i8, - }, - ..Default::default() - }) - .show_inside(ui, |ui| { - let rect = ui.available_rect_before_wrap(); - ScrollArea::vertical() - .id_salt("connections_scroll") - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(1.0); - ui.vertical_centered(|ui| { - let max_width = if !dual_panel { - Content::SIDE_PANEL_WIDTH * 1.3 - } else { - ui.available_width() - }; - View::max_width_ui(ui, max_width, |ui| { - self.connections.ui(ui, cb); - }); - }); - ui.add_space(32.0); - }); - // Draw content divider line. - let r = { - let mut r = rect.clone(); - r.min.y -= 3.0; - r.max.x += View::content_padding(); - r.max.y += View::content_padding() + View::get_bottom_inset(); - r - }; - if show_connections && dual_panel { - View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke()); - } - }); + // Show connections content. + egui::CentralPanel::default() + .frame(egui::Frame { + inner_margin: Margin { + left: if show_connections { + View::get_left_inset() + View::content_padding() + } else { + 0.0 + } as i8, + right: if show_connections { + View::far_right_inset_margin(ui) + View::content_padding() + } else { + 0.0 + } as i8, + top: 3.0 as i8, + bottom: 0.0 as i8, + }, + ..Default::default() + }) + .show_inside(ui, |ui| { + let rect = ui.available_rect_before_wrap(); + ScrollArea::vertical() + .id_salt("connections_scroll") + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(1.0); + ui.vertical_centered(|ui| { + let max_width = if !dual_panel { + Content::SIDE_PANEL_WIDTH * 1.3 + } else { + ui.available_width() + }; + View::max_width_ui(ui, max_width, |ui| { + self.connections.ui(ui, cb); + }); + }); + ui.add_space(32.0); + }); + // Draw content divider line. + let r = { + let mut r = rect.clone(); + r.min.y -= 3.0; + r.max.x += View::content_padding(); + r.max.y += View::content_padding() + View::get_bottom_inset(); + r + }; + if show_connections && dual_panel { + View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke()); + } + }); - // Redraw after delay if node is running at non-dual-panel mode. - if ((!dual_panel && Content::is_network_panel_open()) || dual_panel) && Node::is_running() { - ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY); - } - } + // Redraw after delay if node is running at non-dual-panel mode. + if ((!dual_panel && Content::is_network_panel_open()) || dual_panel) && Node::is_running() { + ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY); + } + } - /// Navigate back, return `true` if action was not consumed. - pub fn on_back(&mut self) -> bool { - if self.showing_settings() { - // Close settings. - self.settings_content = None; - return false; - } - true - } + /// Navigate back, return `true` if action was not consumed. + pub fn on_back(&mut self) -> bool { + if self.showing_settings() { + // Close settings. + self.settings_content = None; + return false; + } + true + } - /// Check if application settings content is showing. - pub fn showing_settings(&self) -> bool { - self.settings_content.is_some() - } + /// Check if application settings content is showing. + pub fn showing_settings(&self) -> bool { + self.settings_content.is_some() + } - /// Draw tab buttons at bottom of the screen. - fn tabs_ui(&mut self, ui: &mut egui::Ui) { - ui.vertical_centered(|ui| { - // Setup spacing between tabs. - ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); + /// Draw tab buttons at bottom of the screen. + fn tabs_ui(&mut self, ui: &mut egui::Ui) { + ui.vertical_centered(|ui| { + // Setup spacing between tabs. + ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); - // Draw tab buttons. - let current_type = self.node_tab_content.get_type(); - ui.columns(4, |columns| { - columns[0].vertical_centered_justified(|ui| { - let active = Some(current_type == NodeTabType::Info); - View::tab_button(ui, DATABASE, None, active, |_| { - self.node_tab_content = Box::new(NetworkNode::default()); - }); - }); - columns[1].vertical_centered_justified(|ui| { - let active = Some(current_type == NodeTabType::Metrics); - View::tab_button(ui, GAUGE, None, active, |_| { - self.node_tab_content = Box::new(NetworkMetrics::default()); - }); - }); - columns[2].vertical_centered_justified(|ui| { - let active = Some(current_type == NodeTabType::Mining); - View::tab_button(ui, FACTORY, None, active, |_| { - self.node_tab_content = Box::new(NetworkMining::default()); - }); - }); - columns[3].vertical_centered_justified(|ui| { - let active = Some(current_type == NodeTabType::Settings); - View::tab_button(ui, FADERS, None, active, |_| { - self.node_tab_content = Box::new(NetworkSettings::default()); - }); - }); - }); - }); - } + // Draw tab buttons. + let current_type = self.node_tab_content.get_type(); + ui.columns(4, |columns| { + columns[0].vertical_centered_justified(|ui| { + let active = Some(current_type == NodeTabType::Info); + View::tab_button(ui, DATABASE, None, active, |_| { + self.node_tab_content = Box::new(NetworkNode::default()); + }); + }); + columns[1].vertical_centered_justified(|ui| { + let active = Some(current_type == NodeTabType::Metrics); + View::tab_button(ui, GAUGE, None, active, |_| { + self.node_tab_content = Box::new(NetworkMetrics::default()); + }); + }); + columns[2].vertical_centered_justified(|ui| { + let active = Some(current_type == NodeTabType::Mining); + View::tab_button(ui, FACTORY, None, active, |_| { + self.node_tab_content = Box::new(NetworkMining::default()); + }); + }); + columns[3].vertical_centered_justified(|ui| { + let active = Some(current_type == NodeTabType::Settings); + View::tab_button(ui, FADERS, None, active, |_| { + self.node_tab_content = Box::new(NetworkSettings::default()); + }); + }); + }); + }); + } - /// Draw title content. - fn title_ui(&mut self, ui: &mut egui::Ui, dual_panel: bool, show_connections: bool) { - let show_settings = self.showing_settings(); + /// Draw title content. + fn title_ui(&mut self, ui: &mut egui::Ui, dual_panel: bool, show_connections: bool) { + let show_settings = self.showing_settings(); - // Setup values for title panel. - let title_text = self.node_tab_content.get_type().title(); - let subtitle_text = Node::get_sync_status_text().into(); - let not_syncing = Node::not_syncing() && !Node::data_dir_changing(); - let title_content = if show_settings { - TitleContentType::Title(t!("settings").into()) - } else if !show_connections { - TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing) - } else { - TitleContentType::Title(t!("network.connections").into()) - }; + // Setup values for title panel. + let title_text = self.node_tab_content.get_type().title(); + let subtitle_text = Node::get_sync_status_text().into(); + let not_syncing = Node::not_syncing() && !Node::data_dir_changing(); + let title_content = if show_settings { + TitleContentType::Title(t!("settings").into()) + } else if !show_connections { + TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing) + } else { + TitleContentType::Title(t!("network.connections").into()) + }; - // Draw title panel. - TitlePanel::new(Id::from("network_title_panel")).ui(TitleType::Single(title_content), |ui| { - if show_settings { - View::title_button_big(ui, ARROW_LEFT, |_| { - self.settings_content = None; - }); - } else if !show_connections { - View::title_button_big(ui, GLOBE, |_| { - AppConfig::toggle_show_connections_network_panel(); - }); - } else if !dual_panel { - View::title_button_big(ui, GEAR, |_| { - self.settings_content = Some(SettingsContent::default()); - }); - } - }, |ui| { - if !dual_panel && !show_settings { - View::title_button_big(ui, BRIEFCASE, |_| { - Content::toggle_network_panel(); - }); - } - }, ui); - } + // Draw title panel. + TitlePanel::new(Id::from("network_title_panel")).ui( + TitleType::Single(title_content), + |ui| { + if show_settings { + View::title_button_big(ui, ARROW_LEFT, |_| { + self.settings_content = None; + }); + } else if !show_connections { + View::title_button_big(ui, GLOBE, |_| { + AppConfig::toggle_show_connections_network_panel(); + }); + } else if !dual_panel { + View::title_button_big(ui, GEAR, |_| { + self.settings_content = Some(SettingsContent::default()); + }); + } + }, + |ui| { + if !dual_panel && !show_settings { + View::title_button_big(ui, BRIEFCASE, |_| { + Content::toggle_network_panel(); + }); + } + }, + ui, + ); + } - /// Content to draw on loading. - pub fn loading_ui(ui: &mut egui::Ui, text: Option>) { - match text { - None => { - ui.centered_and_justified(|ui| { - View::big_loading_spinner(ui); - }); - } - Some(t) => { - View::center_content(ui, 162.0, |ui| { - View::big_loading_spinner(ui); - ui.add_space(18.0); - ui.label(RichText::new(t) - .size(16.0) - .color(Colors::inactive_text()) - ); - }); - } - } - } + /// Content to draw on loading. + pub fn loading_ui(ui: &mut egui::Ui, text: Option>) { + match text { + None => { + ui.centered_and_justified(|ui| { + View::big_loading_spinner(ui); + }); + } + Some(t) => { + View::center_content(ui, 162.0, |ui| { + View::big_loading_spinner(ui); + ui.add_space(18.0); + ui.label(RichText::new(t).size(16.0).color(Colors::inactive_text())); + }); + } + } + } - /// Draw checkbox to run integrated node on application launch. - pub fn autorun_node_ui(ui: &mut egui::Ui) { - let autostart = AppConfig::autostart_node(); - View::checkbox(ui, autostart, t!("network.autorun"), || { - AppConfig::toggle_node_autostart(); - }); - } + /// Draw checkbox to run integrated node on application launch. + pub fn autorun_node_ui(ui: &mut egui::Ui) { + let autostart = AppConfig::autostart_node(); + View::checkbox(ui, autostart, t!("network.autorun"), || { + AppConfig::toggle_node_autostart(); + }); + } } /// Content to draw when node is disabled. fn disabled_node_ui(ui: &mut egui::Ui) { - View::center_content(ui, 156.0, |ui| { - let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL); - ui.label(RichText::new(text) - .size(16.0) - .color(Colors::inactive_text()) - ); - ui.add_space(8.0); - View::action_button(ui, format!("{} {}", POWER, t!("network.enable_node")), || { - Node::start(); - }); - ui.add_space(2.0); - NetworkContent::autorun_node_ui(ui); - }); + View::center_content(ui, 156.0, |ui| { + let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL); + ui.label( + RichText::new(text) + .size(16.0) + .color(Colors::inactive_text()), + ); + ui.add_space(8.0); + View::action_button( + ui, + format!("{} {}", POWER, t!("network.enable_node")), + || { + Node::start(); + }, + ); + ui.add_space(2.0); + NetworkContent::autorun_node_ui(ui); + }); } /// Draw integrated node error content. pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) { - match e { - NodeError::Storage => { - View::center_content(ui, 156.0, |ui| { - ui.label(RichText::new(t!("network_node.error_clean")) - .size(16.0) - .color(Colors::red()) - ); - ui.add_space(8.0); - let btn_txt = format!("{} {}", - ARROWS_COUNTER_CLOCKWISE, - t!("network_node.resync")); - View::action_button(ui, btn_txt, || { - Node::clean_up_data(); - Node::start(); - }); - ui.add_space(2.0); - }); - return; - } - NodeError::P2P | NodeError::API => { - let msg_type = match e { - NodeError::API => "API", - _ => "P2P" - }; - View::center_content(ui, 106.0, |ui| { - let text = t!( - "network_node.error_p2p_api", - "p2p_api" => msg_type, - "settings" => FADERS - ); - ui.label(RichText::new(text) - .size(16.0) - .color(Colors::red()) - ); - ui.add_space(2.0); - }); - return; - } - NodeError::Configuration => { - View::center_content(ui, 106.0, |ui| { - ui.label(RichText::new(t!("network_node.error_config", "settings" => FADERS)) - .size(16.0) - .color(Colors::red()) - ); - ui.add_space(8.0); - let btn_txt = format!("{} {}", - ARROWS_COUNTER_CLOCKWISE, - t!("network_settings.reset")); - View::action_button(ui, btn_txt, || { - NodeConfig::reset_to_default(); - Node::start(); - }); - ui.add_space(2.0); - }); - } - NodeError::Unknown => { - View::center_content(ui, 156.0, |ui| { - ui.label(RichText::new(t!("network_node.error_unknown", "settings" => FADERS)) - .size(16.0) - .color(Colors::red()) - ); - ui.add_space(8.0); - let btn_txt = format!("{} {}", - ARROWS_COUNTER_CLOCKWISE, - t!("network_node.resync")); - View::action_button(ui, btn_txt, || { - Node::clean_up_data(); - Node::start(); - }); - ui.add_space(2.0); - }); - } - } -} \ No newline at end of file + match e { + NodeError::Storage => { + View::center_content(ui, 156.0, |ui| { + ui.label( + RichText::new(t!("network_node.error_clean")) + .size(16.0) + .color(Colors::red()), + ); + ui.add_space(8.0); + let btn_txt = format!("{} {}", ARROWS_COUNTER_CLOCKWISE, t!("network_node.resync")); + View::action_button(ui, btn_txt, || { + Node::clean_up_data(); + Node::start(); + }); + ui.add_space(2.0); + }); + return; + } + NodeError::P2P | NodeError::API => { + let msg_type = match e { + NodeError::API => "API", + _ => "P2P", + }; + View::center_content(ui, 106.0, |ui| { + let text = t!( + "network_node.error_p2p_api", + "p2p_api" => msg_type, + "settings" => FADERS + ); + ui.label(RichText::new(text).size(16.0).color(Colors::red())); + ui.add_space(2.0); + }); + return; + } + NodeError::Configuration => { + View::center_content(ui, 106.0, |ui| { + ui.label( + RichText::new(t!("network_node.error_config", "settings" => FADERS)) + .size(16.0) + .color(Colors::red()), + ); + ui.add_space(8.0); + let btn_txt = format!( + "{} {}", + ARROWS_COUNTER_CLOCKWISE, + t!("network_settings.reset") + ); + View::action_button(ui, btn_txt, || { + NodeConfig::reset_to_default(); + Node::start(); + }); + ui.add_space(2.0); + }); + } + NodeError::Unknown => { + View::center_content(ui, 156.0, |ui| { + ui.label( + RichText::new(t!("network_node.error_unknown", "settings" => FADERS)) + .size(16.0) + .color(Colors::red()), + ); + ui.add_space(8.0); + let btn_txt = format!("{} {}", ARROWS_COUNTER_CLOCKWISE, t!("network_node.resync")); + View::action_button(ui, btn_txt, || { + Node::clean_up_data(); + Node::start(); + }); + ui.add_space(2.0); + }); + } + } +} diff --git a/src/gui/views/network/metrics.rs b/src/gui/views/network/metrics.rs index 4fb921b3..5e4ff0d4 100644 --- a/src/gui/views/network/metrics.rs +++ b/src/gui/views/network/metrics.rs @@ -12,17 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{RichText, CornerRadius, ScrollArea, vec2, StrokeKind}; use egui::scroll_area::ScrollBarVisibility; +use egui::{CornerRadius, RichText, ScrollArea, StrokeKind, vec2}; use grin_core::consensus::{DAY_HEIGHT, GRIN_BASE, HOUR_SEC, REWARD}; use grin_servers::{DiffBlock, ServerStats}; use crate::gui::Colors; use crate::gui::icons::{AT, COINS, CUBE_TRANSPARENT, HOURGLASS_LOW, HOURGLASS_MEDIUM, TIMER}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Content, View}; use crate::gui::views::network::NetworkContent; use crate::gui::views::network::types::{NodeTab, NodeTabType}; +use crate::gui::views::{Content, View}; use crate::node::Node; /// Chain metrics tab content. @@ -34,162 +34,175 @@ const BLOCK_REWARD: u64 = REWARD / GRIN_BASE; const YEARLY_SUPPLY: u64 = (BLOCK_REWARD * DAY_HEIGHT * 365) + 6 * HOUR_SEC; impl NodeTab for NetworkMetrics { - fn get_type(&self) -> NodeTabType { - NodeTabType::Metrics - } + fn get_type(&self) -> NodeTabType { + NodeTabType::Metrics + } - fn tab_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { - let server_stats = Node::get_stats(); - let stats = server_stats.as_ref().unwrap(); - if stats.diff_stats.height == 0 { - NetworkContent::loading_ui(ui, Some(t!("network_metrics.loading"))); - return; - } - ui.add_space(1.0); - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - // Show emission and difficulty info. - info_ui(ui, stats); - // Show difficulty adjustment window blocks. - blocks_ui(ui, stats); - }); - } + fn tab_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { + let server_stats = Node::get_stats(); + let stats = server_stats.as_ref().unwrap(); + if stats.diff_stats.height == 0 { + NetworkContent::loading_ui(ui, Some(t!("network_metrics.loading"))); + return; + } + ui.add_space(1.0); + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + // Show emission and difficulty info. + info_ui(ui, stats); + // Show difficulty adjustment window blocks. + blocks_ui(ui, stats); + }); + } } /// Draw emission and difficulty info. fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) { - // Show emission info. - View::sub_title(ui, format!("{} {}", COINS, t!("network_metrics.emission"))); - ui.columns(3, |columns| { - let supply = stats.header_stats.height * BLOCK_REWARD; - let rate = (YEARLY_SUPPLY * 100) / supply; + // Show emission info. + View::sub_title(ui, format!("{} {}", COINS, t!("network_metrics.emission"))); + ui.columns(3, |columns| { + let supply = stats.header_stats.height * BLOCK_REWARD; + let rate = (YEARLY_SUPPLY * 100) / supply; - columns[0].vertical_centered(|ui| { - View::label_box(ui, - format!("{}ツ", BLOCK_REWARD), - t!("network_metrics.reward"), - [true, false, true, false]); - }); - columns[1].vertical_centered(|ui| { - View::label_box(ui, - format!("{:.2}%", rate), - t!("network_metrics.inflation"), - [false, false, false, false]); - }); - columns[2].vertical_centered(|ui| { - View::label_box(ui, - supply.to_string(), - t!("network_metrics.supply"), - [false, true, false, true]); - }); - }); - ui.add_space(5.0); + columns[0].vertical_centered(|ui| { + View::label_box( + ui, + format!("{}ツ", BLOCK_REWARD), + t!("network_metrics.reward"), + [true, false, true, false], + ); + }); + columns[1].vertical_centered(|ui| { + View::label_box( + ui, + format!("{:.2}%", rate), + t!("network_metrics.inflation"), + [false, false, false, false], + ); + }); + columns[2].vertical_centered(|ui| { + View::label_box( + ui, + supply.to_string(), + t!("network_metrics.supply"), + [false, true, false, true], + ); + }); + }); + ui.add_space(5.0); - // Show difficulty adjustment window info. - let difficulty_title = t!( - "network_metrics.difficulty_window", - "size" => stats.diff_stats.window_size - ); - View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, difficulty_title)); - ui.columns(3, |columns| { - columns[0].vertical_centered(|ui| { - View::label_box(ui, - stats.diff_stats.height.to_string(), - t!("network_node.height"), - [true, false, true, false]); - }); - columns[1].vertical_centered(|ui| { - View::label_box(ui, - format!("{}s", stats.diff_stats.average_block_time), - t!("network_metrics.block_time"), - [false, false, false, false]); - }); - columns[2].vertical_centered(|ui| { - View::label_box(ui, - stats.diff_stats.average_difficulty.to_string(), - t!("network_node.difficulty"), - [false, true, false, true]); - }); - }); + // Show difficulty adjustment window info. + let difficulty_title = t!( + "network_metrics.difficulty_window", + "size" => stats.diff_stats.window_size + ); + View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, difficulty_title)); + ui.columns(3, |columns| { + columns[0].vertical_centered(|ui| { + View::label_box( + ui, + stats.diff_stats.height.to_string(), + t!("network_node.height"), + [true, false, true, false], + ); + }); + columns[1].vertical_centered(|ui| { + View::label_box( + ui, + format!("{}s", stats.diff_stats.average_block_time), + t!("network_metrics.block_time"), + [false, false, false, false], + ); + }); + columns[2].vertical_centered(|ui| { + View::label_box( + ui, + stats.diff_stats.average_difficulty.to_string(), + t!("network_node.difficulty"), + [false, true, false, true], + ); + }); + }); } const BLOCK_ITEM_HEIGHT: f32 = 77.0; /// Draw difficulty adjustment window blocks content. fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) { - let blocks_size = stats.diff_stats.last_blocks.len(); - ui.add_space(4.0); - ScrollArea::vertical() - .id_salt("mining_difficulty_scroll") - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .auto_shrink([false; 2]) - .stick_to_bottom(true) - .show_rows( - ui, - BLOCK_ITEM_HEIGHT, - blocks_size, - |ui, row_range| { - ui.add_space(4.0); - for index in row_range { - let db = stats.diff_stats.last_blocks.get(index).unwrap(); - block_item_ui(ui, db, View::item_rounding(index, blocks_size, false)); - } - }, - ); + let blocks_size = stats.diff_stats.last_blocks.len(); + ui.add_space(4.0); + ScrollArea::vertical() + .id_salt("mining_difficulty_scroll") + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) + .stick_to_bottom(true) + .show_rows(ui, BLOCK_ITEM_HEIGHT, blocks_size, |ui, row_range| { + ui.add_space(4.0); + for index in row_range { + let db = stats.diff_stats.last_blocks.get(index).unwrap(); + block_item_ui(ui, db, View::item_rounding(index, blocks_size, false)); + } + }); } /// Draw block difficulty item. fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: CornerRadius) { - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(BLOCK_ITEM_HEIGHT); - ui.allocate_ui(rect.size(), |ui| { - ui.horizontal(|ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - ui.add_space(4.0); + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(BLOCK_ITEM_HEIGHT); + ui.allocate_ui(rect.size(), |ui| { + ui.horizontal(|ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(4.0); - // Draw round background. - rect.min += vec2(8.0, 0.0); - rect.max -= vec2(8.0, 0.0); - ui.painter().rect(rect, - rounding, - Colors::fill(), - View::item_stroke(), - StrokeKind::Outside); + // Draw round background. + rect.min += vec2(8.0, 0.0); + rect.max -= vec2(8.0, 0.0); + ui.painter().rect( + rect, + rounding, + Colors::fill(), + View::item_stroke(), + StrokeKind::Outside, + ); - // Draw block hash. - ui.horizontal(|ui| { - ui.add_space(8.0); - ui.label(RichText::new(db.block_hash.to_string()) - .color(Colors::white_or_black(true)) - .size(17.0)); - }); - // Draw block difficulty and height. - ui.horizontal(|ui| { - ui.add_space(7.0); - let diff_text = format!("{} {} {} {}", - CUBE_TRANSPARENT, - db.difficulty, - AT, - db.block_height); - ui.label(RichText::new(diff_text) - .color(Colors::title(false)) - .size(15.0)); - }); - // Draw block date. - ui.horizontal(|ui| { - ui.add_space(7.0); - let block_time = View::format_time(db.time as i64); - ui.label(RichText::new(format!("{} {}s {} {}", - TIMER, - db.duration, - HOURGLASS_LOW, - block_time)) - .color(Colors::gray()) - .size(15.0)); - }); - ui.add_space(3.0); - }); - ui.add_space(6.0); - }); - }); -} \ No newline at end of file + // Draw block hash. + ui.horizontal(|ui| { + ui.add_space(8.0); + ui.label( + RichText::new(db.block_hash.to_string()) + .color(Colors::white_or_black(true)) + .size(17.0), + ); + }); + // Draw block difficulty and height. + ui.horizontal(|ui| { + ui.add_space(7.0); + let diff_text = format!( + "{} {} {} {}", + CUBE_TRANSPARENT, db.difficulty, AT, db.block_height + ); + ui.label( + RichText::new(diff_text) + .color(Colors::title(false)) + .size(15.0), + ); + }); + // Draw block date. + ui.horizontal(|ui| { + ui.add_space(7.0); + let block_time = View::format_time(db.time as i64); + ui.label( + RichText::new(format!( + "{} {}s {} {}", + TIMER, db.duration, HOURGLASS_LOW, block_time + )) + .color(Colors::gray()) + .size(15.0), + ); + }); + ui.add_space(3.0); + }); + ui.add_space(6.0); + }); + }); +} diff --git a/src/gui/views/network/mining.rs b/src/gui/views/network/mining.rs index 9b506787..9ce097d6 100644 --- a/src/gui/views/network/mining.rs +++ b/src/gui/views/network/mining.rs @@ -12,179 +12,195 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{RichText, CornerRadius, ScrollArea, StrokeKind}; use egui::scroll_area::ScrollBarVisibility; +use egui::{CornerRadius, RichText, ScrollArea, StrokeKind}; use grin_chain::SyncStatus; use grin_servers::WorkerStats; use crate::gui::Colors; -use crate::gui::icons::{BARBELL, CLOCK_AFTERNOON, CPU, CUBE, FADERS, FOLDER_DASHED, FOLDER_SIMPLE_MINUS, FOLDER_SIMPLE_PLUS, HARD_DRIVES, PLUGS, PLUGS_CONNECTED, POLYGON}; +use crate::gui::icons::{ + BARBELL, CLOCK_AFTERNOON, CPU, CUBE, FADERS, FOLDER_DASHED, FOLDER_SIMPLE_MINUS, + FOLDER_SIMPLE_PLUS, HARD_DRIVES, PLUGS, PLUGS_CONNECTED, POLYGON, +}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Content, View}; use crate::gui::views::network::NetworkContent; use crate::gui::views::network::setup::StratumSetup; use crate::gui::views::network::types::{NodeTab, NodeTabType}; use crate::gui::views::types::ContentContainer; +use crate::gui::views::{Content, View}; use crate::node::{Node, NodeConfig}; /// Mining tab content. pub struct NetworkMining { - /// Stratum server setup content. - stratum_server_setup: StratumSetup, + /// Stratum server setup content. + stratum_server_setup: StratumSetup, } impl Default for NetworkMining { - fn default() -> Self { - Self { - stratum_server_setup: StratumSetup::default(), - } - } + fn default() -> Self { + Self { + stratum_server_setup: StratumSetup::default(), + } + } } impl NodeTab for NetworkMining { - fn get_type(&self) -> NodeTabType { - NodeTabType::Mining - } + fn get_type(&self) -> NodeTabType { + NodeTabType::Mining + } - fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - if Node::is_stratum_starting() || Node::get_sync_status().unwrap() != SyncStatus::NoSync { - NetworkContent::loading_ui(ui, Some(t!("network_mining.loading"))); - return; - } + fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + if Node::is_stratum_starting() || Node::get_sync_status().unwrap() != SyncStatus::NoSync { + NetworkContent::loading_ui(ui, Some(t!("network_mining.loading"))); + return; + } - // Show stratum server setup when mining server is not running. - let stratum_stats = Node::get_stratum_stats(); - if !stratum_stats.is_running { - ScrollArea::vertical() - .id_salt("stratum_setup_scroll") - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(1.0); - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - self.stratum_server_setup.ui(ui, cb); - }); - }); - return; - } + // Show stratum server setup when mining server is not running. + let stratum_stats = Node::get_stratum_stats(); + if !stratum_stats.is_running { + ScrollArea::vertical() + .id_salt("stratum_setup_scroll") + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(1.0); + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + self.stratum_server_setup.ui(ui, cb); + }); + }); + return; + } - ui.add_space(1.0); + ui.add_space(1.0); - // Show stratum mining server info. - View::sub_title(ui, format!("{} {}", HARD_DRIVES, t!("network_mining.server"))); - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - let (stratum_addr, stratum_port) = NodeConfig::get_stratum_address(); - View::label_box(ui, - format!("{}:{}", stratum_addr, stratum_port), - t!("network_mining.address"), - [true, false, true, false]); - }); - columns[1].vertical_centered(|ui| { - View::label_box(ui, - self.stratum_server_setup - .wallet_name - .clone() - .unwrap_or("-".to_string()), - t!("network_mining.rewards_wallet"), - [false, true, false, true]); - }); - }); - ui.add_space(4.0); + // Show stratum mining server info. + View::sub_title( + ui, + format!("{} {}", HARD_DRIVES, t!("network_mining.server")), + ); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + let (stratum_addr, stratum_port) = NodeConfig::get_stratum_address(); + View::label_box( + ui, + format!("{}:{}", stratum_addr, stratum_port), + t!("network_mining.address"), + [true, false, true, false], + ); + }); + columns[1].vertical_centered(|ui| { + View::label_box( + ui, + self.stratum_server_setup + .wallet_name + .clone() + .unwrap_or("-".to_string()), + t!("network_mining.rewards_wallet"), + [false, true, false, true], + ); + }); + }); + ui.add_space(4.0); - // Show network info. - View::sub_title(ui, format!("{} {}", POLYGON, t!("network.self"))); - ui.columns(3, |columns| { - columns[0].vertical_centered(|ui| { - let difficulty = if stratum_stats.network_difficulty > 0 { - stratum_stats.network_difficulty.to_string() - } else { - "-".into() - }; - View::label_box(ui, - difficulty, - t!("network_node.difficulty"), - [true, false, true, false]); - }); - columns[1].vertical_centered(|ui| { - let block_height = if stratum_stats.block_height > 0 { - stratum_stats.block_height.to_string() - } else { - "-".into() - }; - View::label_box(ui, - block_height, - t!("network_node.header"), - [false, false, false, false]); - }); - columns[2].vertical_centered(|ui| { - let hashrate = if stratum_stats.network_hashrate > 0.0 { - format!("{:.*}", 2, stratum_stats.network_hashrate) - } else { - "-".into() - }; - View::label_box(ui, - hashrate, - t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits), - [false, true, false, true]); - }); - }); - ui.add_space(4.0); + // Show network info. + View::sub_title(ui, format!("{} {}", POLYGON, t!("network.self"))); + ui.columns(3, |columns| { + columns[0].vertical_centered(|ui| { + let difficulty = if stratum_stats.network_difficulty > 0 { + stratum_stats.network_difficulty.to_string() + } else { + "-".into() + }; + View::label_box( + ui, + difficulty, + t!("network_node.difficulty"), + [true, false, true, false], + ); + }); + columns[1].vertical_centered(|ui| { + let block_height = if stratum_stats.block_height > 0 { + stratum_stats.block_height.to_string() + } else { + "-".into() + }; + View::label_box( + ui, + block_height, + t!("network_node.header"), + [false, false, false, false], + ); + }); + columns[2].vertical_centered(|ui| { + let hashrate = if stratum_stats.network_hashrate > 0.0 { + format!("{:.*}", 2, stratum_stats.network_hashrate) + } else { + "-".into() + }; + View::label_box( + ui, + hashrate, + t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits), + [false, true, false, true], + ); + }); + }); + ui.add_space(4.0); - // Show mining info. - View::sub_title(ui, format!("{} {}", CPU, t!("network_mining.miners"))); - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - View::label_box(ui, - stratum_stats.num_workers.to_string(), - t!("network_mining.devices"), - [true, false, true, false]); - }); + // Show mining info. + View::sub_title(ui, format!("{} {}", CPU, t!("network_mining.miners"))); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + View::label_box( + ui, + stratum_stats.num_workers.to_string(), + t!("network_mining.devices"), + [true, false, true, false], + ); + }); - columns[1].vertical_centered(|ui| { - View::label_box(ui, - stratum_stats.blocks_found.to_string(), - t!("network_mining.blocks_found"), - [false, true, false, true]); - }); - }); - ui.add_space(4.0); + columns[1].vertical_centered(|ui| { + View::label_box( + ui, + stratum_stats.blocks_found.to_string(), + t!("network_mining.blocks_found"), + [false, true, false, true], + ); + }); + }); + ui.add_space(4.0); - // Show workers stats or info text when possible. - let workers_size = stratum_stats.worker_stats.len(); - if workers_size != 0 && stratum_stats.num_workers > 0 { - ui.add_space(4.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(4.0); - ScrollArea::vertical() - .id_salt("stratum_workers_scroll") - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .auto_shrink([false; 2]) - .show_rows( - ui, - WORKER_ITEM_HEIGHT, - workers_size, - |ui, row_range| { - for index in row_range { - // Add space before the first item. - if index == 0 { - ui.add_space(4.0); - } - let worker = stratum_stats.worker_stats.get(index).unwrap(); - let item_rounding = View::item_rounding(index, workers_size, false); - worker_item_ui(ui, worker, item_rounding); - } - }, - ); - } else if ui.available_height() > 142.0 { - View::center_content(ui, 142.0, |ui| { - ui.label(RichText::new(t!("network_mining.info", "settings" => FADERS)) - .size(16.0) - .color(Colors::inactive_text()) - ); - }); - } - } + // Show workers stats or info text when possible. + let workers_size = stratum_stats.worker_stats.len(); + if workers_size != 0 && stratum_stats.num_workers > 0 { + ui.add_space(4.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(4.0); + ScrollArea::vertical() + .id_salt("stratum_workers_scroll") + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) + .show_rows(ui, WORKER_ITEM_HEIGHT, workers_size, |ui, row_range| { + for index in row_range { + // Add space before the first item. + if index == 0 { + ui.add_space(4.0); + } + let worker = stratum_stats.worker_stats.get(index).unwrap(); + let item_rounding = View::item_rounding(index, workers_size, false); + worker_item_ui(ui, worker, item_rounding); + } + }); + } else if ui.available_height() > 142.0 { + View::center_content(ui, 142.0, |ui| { + ui.label( + RichText::new(t!("network_mining.info", "settings" => FADERS)) + .size(16.0) + .color(Colors::inactive_text()), + ); + }); + } + } } /// Height of Stratum server worker list item. @@ -192,84 +208,96 @@ const WORKER_ITEM_HEIGHT: f32 = 76.0; /// Draw worker statistics item. fn worker_item_ui(ui: &mut egui::Ui, ws: &WorkerStats, rounding: CornerRadius) { - ui.horizontal_wrapped(|ui| { - ui.vertical_centered_justified(|ui| { - // Draw round background. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(WORKER_ITEM_HEIGHT); - ui.painter().rect(rect, - rounding, - Colors::white_or_black(false), - View::item_stroke(), - StrokeKind::Outside); + ui.horizontal_wrapped(|ui| { + ui.vertical_centered_justified(|ui| { + // Draw round background. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(WORKER_ITEM_HEIGHT); + ui.painter().rect( + rect, + rounding, + Colors::white_or_black(false), + View::item_stroke(), + StrokeKind::Outside, + ); - ui.add_space(2.0); - ui.horizontal(|ui| { - ui.add_space(5.0); + ui.add_space(2.0); + ui.horizontal(|ui| { + ui.add_space(5.0); - // Draw worker connection status. - let (status_text, status_icon, status_color) = match ws.is_connected { - true => ( - t!("network_mining.connected"), - PLUGS_CONNECTED, - Colors::white_or_black(true) - ), - false => (t!("network_mining.disconnected"), PLUGS, Colors::inactive_text()) - }; - let status_line_text = format!("{} {} {}", status_icon, ws.id, status_text); - ui.heading(RichText::new(status_line_text) - .color(status_color) - .size(17.0)); - ui.add_space(2.0); - }); - ui.horizontal(|ui| { - ui.add_space(6.0); + // Draw worker connection status. + let (status_text, status_icon, status_color) = match ws.is_connected { + true => ( + t!("network_mining.connected"), + PLUGS_CONNECTED, + Colors::white_or_black(true), + ), + false => ( + t!("network_mining.disconnected"), + PLUGS, + Colors::inactive_text(), + ), + }; + let status_line_text = format!("{} {} {}", status_icon, ws.id, status_text); + ui.heading( + RichText::new(status_line_text) + .color(status_color) + .size(17.0), + ); + ui.add_space(2.0); + }); + ui.horizontal(|ui| { + ui.add_space(6.0); - // Draw difficulty. - let diff_text = format!("{} {}", BARBELL, ws.pow_difficulty); - ui.heading(RichText::new(diff_text) - .color(Colors::title(false)) - .size(16.0)); - ui.add_space(6.0); + // Draw difficulty. + let diff_text = format!("{} {}", BARBELL, ws.pow_difficulty); + ui.heading( + RichText::new(diff_text) + .color(Colors::title(false)) + .size(16.0), + ); + ui.add_space(6.0); - // Draw accepted shares. - let accepted_text = format!("{} {}", FOLDER_SIMPLE_PLUS, ws.num_accepted); - ui.heading(RichText::new(accepted_text) - .color(Colors::green()) - .size(16.0)); - ui.add_space(6.0); + // Draw accepted shares. + let accepted_text = format!("{} {}", FOLDER_SIMPLE_PLUS, ws.num_accepted); + ui.heading( + RichText::new(accepted_text) + .color(Colors::green()) + .size(16.0), + ); + ui.add_space(6.0); - // Draw rejected shares. - let rejected_text = format!("{} {}", FOLDER_SIMPLE_MINUS, ws.num_rejected); - ui.heading(RichText::new(rejected_text) - .color(Colors::red()) - .size(16.0)); - ui.add_space(6.0); + // Draw rejected shares. + let rejected_text = format!("{} {}", FOLDER_SIMPLE_MINUS, ws.num_rejected); + ui.heading(RichText::new(rejected_text).color(Colors::red()).size(16.0)); + ui.add_space(6.0); - // Draw stale shares. - let stale_text = format!("{} {}", FOLDER_DASHED, ws.num_stale); - ui.heading(RichText::new(stale_text) - .color(Colors::gray()) - .size(16.0)); - ui.add_space(6.0); + // Draw stale shares. + let stale_text = format!("{} {}", FOLDER_DASHED, ws.num_stale); + ui.heading(RichText::new(stale_text).color(Colors::gray()).size(16.0)); + ui.add_space(6.0); - // Draw blocks found. - let blocks_found_text = format!("{} {}", CUBE, ws.num_blocks_found); - ui.heading(RichText::new(blocks_found_text) - .color(Colors::title(false)) - .size(16.0)); - }); - ui.horizontal(|ui| { - ui.add_space(6.0); + // Draw blocks found. + let blocks_found_text = format!("{} {}", CUBE, ws.num_blocks_found); + ui.heading( + RichText::new(blocks_found_text) + .color(Colors::title(false)) + .size(16.0), + ); + }); + ui.horizontal(|ui| { + ui.add_space(6.0); - // Draw block time - let seen_ts = ws.last_seen.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); - let seen_time = View::format_time(seen_ts as i64); - let seen_text = format!("{} {}", CLOCK_AFTERNOON, seen_time); - ui.heading(RichText::new(seen_text) - .color(Colors::gray()) - .size(16.0)); - }); - }); - }); -} \ No newline at end of file + // Draw block time + let seen_ts = ws + .last_seen + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + let seen_time = View::format_time(seen_ts as i64); + let seen_text = format!("{} {}", CLOCK_AFTERNOON, seen_time); + ui.heading(RichText::new(seen_text).color(Colors::gray()).size(16.0)); + }); + }); + }); +} diff --git a/src/gui/views/network/mod.rs b/src/gui/views/network/mod.rs index c68cf082..aef190eb 100644 --- a/src/gui/views/network/mod.rs +++ b/src/gui/views/network/mod.rs @@ -33,5 +33,5 @@ pub use content::*; mod connections; pub use connections::*; +pub mod modals; pub mod types; -pub mod modals; \ No newline at end of file diff --git a/src/gui/views/network/modals/ext_conn.rs b/src/gui/views/network/modals/ext_conn.rs index a91081f4..7a16f46d 100644 --- a/src/gui/views/network/modals/ext_conn.rs +++ b/src/gui/views/network/modals/ext_conn.rs @@ -15,241 +15,259 @@ use egui::{Id, RichText}; use url::Url; +use crate::gui::Colors; use crate::gui::icons::SCAN; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::network::types::ShareConnection; use crate::gui::views::{CameraContent, Modal, TextEdit, View}; -use crate::gui::Colors; use crate::wallet::{ConnectionsConfig, ExternalConnection}; /// Content to create or update external wallet connection. pub struct ExternalConnectionModal { - /// Flag to check if content was just rendered. - first_draw: bool, + /// Flag to check if content was just rendered. + first_draw: bool, - /// Editing external connection identifier. - id: Option, + /// Editing external connection identifier. + id: Option, - /// External connection URL. - url_edit: String, - /// Flag to show URL format error. - url_error: bool, + /// External connection URL. + url_edit: String, + /// Flag to show URL format error. + url_error: bool, - // /// External connection username. - // username_edit: String, - /// External connection API secret. - secret_edit: String, + // /// External connection username. + // username_edit: String, + /// External connection API secret. + secret_edit: String, - /// QR code scanner content. - scan_qr_content: Option, + /// QR code scanner content. + scan_qr_content: Option, } impl ExternalConnectionModal { - /// Network [`Modal`] identifier. - pub const NETWORK_ID: &'static str = "net_ext_conn_modal"; - /// Wallet [`Modal`] identifier. - pub const WALLET_ID: &'static str = "wallet_ext_conn_modal"; + /// Network [`Modal`] identifier. + pub const NETWORK_ID: &'static str = "net_ext_conn_modal"; + /// Wallet [`Modal`] identifier. + pub const WALLET_ID: &'static str = "wallet_ext_conn_modal"; - /// Create new instance from optional provided connection to update. - pub fn new(conn: Option) -> Self { - let (url_edit, secret_edit, id) = if let Some(c) = conn { - // let username = c.username.unwrap_or("grin".to_string()); - let secret = c.secret.unwrap_or("".to_string()); - (c.url, secret, Some(c.id)) - } else { - ("".to_string(), "".to_string(), None) - }; - Self { - first_draw: true, - url_edit, - url_error: false, - secret_edit, - id, - scan_qr_content: None - } - } + /// Create new instance from optional provided connection to update. + pub fn new(conn: Option) -> Self { + let (url_edit, secret_edit, id) = if let Some(c) = conn { + // let username = c.username.unwrap_or("grin".to_string()); + let secret = c.secret.unwrap_or("".to_string()); + (c.url, secret, Some(c.id)) + } else { + ("".to_string(), "".to_string(), None) + }; + Self { + first_draw: true, + url_edit, + url_error: false, + secret_edit, + id, + scan_qr_content: None, + } + } - /// Draw external connection [`Modal`] content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - cb: &dyn PlatformCallbacks, - modal: &Modal, - on_save: impl Fn(ExternalConnection)) { - // Show QR code scanner content. - if let Some(scan_content) = self.scan_qr_content.as_mut() { - if let Some(result) = scan_content.qr_scan_result() { - cb.stop_camera(); - modal.enable_closing(); - self.scan_qr_content = None; - // Parse scan result. - if let Ok(c) = serde_json::from_str::(&result.text()) { - let ext_conn = ExternalConnection::new(c.url, Some(c.username), Some(c.secret)); - ConnectionsConfig::add_ext_conn(ext_conn.clone()); - ExternalConnection::check(Some(ext_conn.id), ui.ctx()); - Modal::close(); - } - } else { - scan_content.ui(ui, cb); - } - ui.add_space(8.0); + /// Draw external connection [`Modal`] content. + pub fn ui( + &mut self, + ui: &mut egui::Ui, + cb: &dyn PlatformCallbacks, + modal: &Modal, + on_save: impl Fn(ExternalConnection), + ) { + // Show QR code scanner content. + if let Some(scan_content) = self.scan_qr_content.as_mut() { + if let Some(result) = scan_content.qr_scan_result() { + cb.stop_camera(); + modal.enable_closing(); + self.scan_qr_content = None; + // Parse scan result. + if let Ok(c) = serde_json::from_str::(&result.text()) { + let ext_conn = ExternalConnection::new(c.url, Some(c.username), Some(c.secret)); + ConnectionsConfig::add_ext_conn(ext_conn.clone()); + ExternalConnection::check(Some(ext_conn.id), ui.ctx()); + Modal::close(); + } + } else { + scan_content.ui(ui, cb); + } + ui.add_space(8.0); - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - // Show buttons to close modal or scanner. - ui.columns(2, |cols| { - cols[0].vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - cb.stop_camera(); - self.scan_qr_content = None; - Modal::close(); - }); - }); - cols[1].vertical_centered_justified(|ui| { - View::button(ui, t!("back"), Colors::white_or_black(false), || { - cb.stop_camera(); - self.scan_qr_content = None; - modal.enable_closing(); - }); - }); - }); - ui.add_space(6.0); - return; - } - // Add connection button callback. - let on_add = |ui: &mut egui::Ui, m: &mut ExternalConnectionModal| { - let url = if !m.url_edit.starts_with("http") { - format!("https://{}", m.url_edit) - } else { - m.url_edit.clone() - }; - let error = Url::parse(url.trim()).is_err(); - m.url_error = error; - if !error { - let username = if m.secret_edit.is_empty() { - Some("grin".to_string()) - } else { - Some(m.secret_edit.clone()) - }; - let secret = if m.secret_edit.is_empty() { - None - } else { - Some(m.secret_edit.clone()) - }; - // Update or create new connection. - let mut ext_conn = ExternalConnection::new(url, username, secret); - if let Some(id) = m.id { - ext_conn.id = id; - } - ConnectionsConfig::add_ext_conn(ext_conn.clone()); - ExternalConnection::check(Some(ext_conn.id), ui.ctx()); - on_save(ext_conn); + // Show buttons to close modal or scanner. + ui.columns(2, |cols| { + cols[0].vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + cb.stop_camera(); + self.scan_qr_content = None; + Modal::close(); + }); + }); + cols[1].vertical_centered_justified(|ui| { + View::button(ui, t!("back"), Colors::white_or_black(false), || { + cb.stop_camera(); + self.scan_qr_content = None; + modal.enable_closing(); + }); + }); + }); + ui.add_space(6.0); + return; + } + // Add connection button callback. + let on_add = |ui: &mut egui::Ui, m: &mut ExternalConnectionModal| { + let url = if !m.url_edit.starts_with("http") { + format!("https://{}", m.url_edit) + } else { + m.url_edit.clone() + }; + let error = Url::parse(url.trim()).is_err(); + m.url_error = error; + if !error { + let username = if m.secret_edit.is_empty() { + Some("grin".to_string()) + } else { + Some(m.secret_edit.clone()) + }; + let secret = if m.secret_edit.is_empty() { + None + } else { + Some(m.secret_edit.clone()) + }; + // Update or create new connection. + let mut ext_conn = ExternalConnection::new(url, username, secret); + if let Some(id) = m.id { + ext_conn.id = id; + } + ConnectionsConfig::add_ext_conn(ext_conn.clone()); + ExternalConnection::check(Some(ext_conn.id), ui.ctx()); + on_save(ext_conn); - // Close modal. - m.url_edit = "".to_string(); - m.secret_edit = "".to_string(); - m.url_error = false; - Modal::close(); - } - }; + // Close modal. + m.url_edit = "".to_string(); + m.secret_edit = "".to_string(); + m.url_error = false; + Modal::close(); + } + }; - ui.vertical_centered(|ui| { - ui.add_space(6.0); - ui.label(RichText::new(t!("wallets.node_url")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.vertical_centered(|ui| { + ui.add_space(6.0); + ui.label( + RichText::new(t!("wallets.node_url")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw node URL text edit. - let url_edit_id = Id::from(modal.id).with(self.id).with("node_url"); - let mut url_edit = TextEdit::new(url_edit_id).paste().focus(self.first_draw); - let url_edit_before = self.url_edit.clone(); - url_edit.ui(ui, &mut self.url_edit, cb); - if self.url_edit != url_edit_before { - self.url_error = false; - } + // Draw node URL text edit. + let url_edit_id = Id::from(modal.id).with(self.id).with("node_url"); + let mut url_edit = TextEdit::new(url_edit_id).paste().focus(self.first_draw); + let url_edit_before = self.url_edit.clone(); + url_edit.ui(ui, &mut self.url_edit, cb); + if self.url_edit != url_edit_before { + self.url_error = false; + } - // ui.add_space(8.0); - // ui.label(RichText::new(t!("wallets.name")) - // .size(17.0) - // .color(Colors::gray())); - // ui.add_space(8.0); - // - // // Draw node username text edit (disabled by default). - // let username_edit_id = Id::from(modal.id).with(self.id).with("node_username"); - // let mut username_edit = TextEdit::new(username_edit_id).focus(false).disable(); - // username_edit.ui(ui, &mut self.username_edit, cb); + // ui.add_space(8.0); + // ui.label(RichText::new(t!("wallets.name")) + // .size(17.0) + // .color(Colors::gray())); + // ui.add_space(8.0); + // + // // Draw node username text edit (disabled by default). + // let username_edit_id = Id::from(modal.id).with(self.id).with("node_username"); + // let mut username_edit = TextEdit::new(username_edit_id).focus(false).disable(); + // username_edit.ui(ui, &mut self.username_edit, cb); - ui.add_space(8.0); - ui.label(RichText::new(t!("wallets.node_secret")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(8.0); + ui.label( + RichText::new(t!("wallets.node_secret")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw node API secret text edit. - let secret_edit_id = Id::from(modal.id).with(self.id).with("node_secret"); - let mut secret_edit = TextEdit::new(secret_edit_id) - .h_center() - .password() - .paste() - .focus(false); - if url_edit.enter_pressed { - secret_edit.focus_request(); - } - secret_edit.ui(ui, &mut self.secret_edit, cb); - if secret_edit.enter_pressed { - on_add(ui, self); - } + // Draw node API secret text edit. + let secret_edit_id = Id::from(modal.id).with(self.id).with("node_secret"); + let mut secret_edit = TextEdit::new(secret_edit_id) + .h_center() + .password() + .paste() + .focus(false); + if url_edit.enter_pressed { + secret_edit.focus_request(); + } + secret_edit.ui(ui, &mut self.secret_edit, cb); + if secret_edit.enter_pressed { + on_add(ui, self); + } - // Show error when specified URL is not valid. - if self.url_error { - ui.add_space(12.0); - ui.label(RichText::new(t!("wallets.invalid_url")) - .size(17.0) - .color(Colors::red())); - } - ui.add_space(12.0); + // Show error when specified URL is not valid. + if self.url_error { + ui.add_space(12.0); + ui.label( + RichText::new(t!("wallets.invalid_url")) + .size(17.0) + .color(Colors::red()), + ); + } + ui.add_space(12.0); - let scan_text = format!("{} {}", SCAN, t!("scan")); - View::button(ui, scan_text, Colors::white_or_black(false), || { - modal.disable_closing(); - self.scan_qr_content = Some(CameraContent::default()); - cb.start_camera(); - }); - }); + let scan_text = format!("{} {}", SCAN, t!("scan")); + View::button(ui, scan_text, Colors::white_or_black(false), || { + modal.disable_closing(); + self.scan_qr_content = Some(CameraContent::default()); + cb.start_camera(); + }); + }); - ui.add_space(8.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); + ui.add_space(8.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - self.url_edit = "".to_string(); - self.secret_edit = "".to_string(); - self.url_error = false; - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button_ui(ui, if self.id.is_some() { - t!("modal.save") - } else { - t!("modal.add") - }, Colors::white_or_black(false), |ui| { - (on_add)(ui, self); - }); - }); - }); - ui.add_space(6.0); - }); + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + self.url_edit = "".to_string(); + self.secret_edit = "".to_string(); + self.url_error = false; + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button_ui( + ui, + if self.id.is_some() { + t!("modal.save") + } else { + t!("modal.add") + }, + Colors::white_or_black(false), + |ui| { + (on_add)(ui, self); + }, + ); + }); + }); + ui.add_space(6.0); + }); - self.first_draw = false; - } -} \ No newline at end of file + self.first_draw = false; + } +} diff --git a/src/gui/views/network/modals/mod.rs b/src/gui/views/network/modals/mod.rs index c3e841d4..02d0ef22 100644 --- a/src/gui/views/network/modals/mod.rs +++ b/src/gui/views/network/modals/mod.rs @@ -16,4 +16,4 @@ mod ext_conn; pub use ext_conn::*; mod share_conn; -pub use share_conn::*; \ No newline at end of file +pub use share_conn::*; diff --git a/src/gui/views/network/modals/share_conn.rs b/src/gui/views/network/modals/share_conn.rs index 6393873d..dea12a83 100644 --- a/src/gui/views/network/modals/share_conn.rs +++ b/src/gui/views/network/modals/share_conn.rs @@ -20,38 +20,38 @@ use crate::gui::views::{Modal, QrCodeContent, View}; /// [`Modal`] content to share connection with QR code. pub struct ShareConnectionContent { - /// QR code content. - pub qr_details_content: QrCodeContent, + /// QR code content. + pub qr_details_content: QrCodeContent, } impl ShareConnectionContent { - /// Create new content instance from connection details. - pub fn new(details: ShareConnection) -> Result { - let details = serde_json::to_string_pretty(&details)?; - let c = Self { - qr_details_content: QrCodeContent::new(details, false).hide_text().no_copy(), - }; - Ok(c) - } + /// Create new content instance from connection details. + pub fn new(details: ShareConnection) -> Result { + let details = serde_json::to_string_pretty(&details)?; + let c = Self { + qr_details_content: QrCodeContent::new(details, false).hide_text().no_copy(), + }; + Ok(c) + } - /// Draw QR code content. - pub fn ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - let dark_theme = AppConfig::dark_theme().unwrap_or(false); - // Set light theme for better scanning. - AppConfig::set_dark_theme(false); - modal.set_background_color(Colors::FILL_DEEP); - crate::setup_visuals(ui.ctx()); - // Draw QR code content. - ui.add_space(6.0); - self.qr_details_content.ui(ui, cb); - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - Modal::close(); - }); - }); - ui.add_space(6.0); - // Set color theme back. - AppConfig::set_dark_theme(dark_theme); - crate::setup_visuals(ui.ctx()); - } -} \ No newline at end of file + /// Draw QR code content. + pub fn ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + let dark_theme = AppConfig::dark_theme().unwrap_or(false); + // Set light theme for better scanning. + AppConfig::set_dark_theme(false); + modal.set_background_color(Colors::FILL_DEEP); + crate::setup_visuals(ui.ctx()); + // Draw QR code content. + ui.add_space(6.0); + self.qr_details_content.ui(ui, cb); + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + Modal::close(); + }); + }); + ui.add_space(6.0); + // Set color theme back. + AppConfig::set_dark_theme(dark_theme); + crate::setup_visuals(ui.ctx()); + } +} diff --git a/src/gui/views/network/node.rs b/src/gui/views/network/node.rs index 8c00ba83..816e51df 100644 --- a/src/gui/views/network/node.rs +++ b/src/gui/views/network/node.rs @@ -12,15 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{RichText, CornerRadius, ScrollArea, StrokeKind}; use egui::scroll_area::ScrollBarVisibility; +use egui::{CornerRadius, RichText, ScrollArea, StrokeKind}; use grin_servers::PeerStats; use crate::gui::Colors; use crate::gui::icons::{AT, CUBE, DEVICES, FLOW_ARROW, HANDSHAKE, PACKAGE, SHARE_NETWORK}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Content, View}; use crate::gui::views::network::types::{NodeTab, NodeTabType}; +use crate::gui::views::{Content, View}; use crate::node::{Node, NodeConfig}; /// Integrated node tab content. @@ -28,193 +28,227 @@ use crate::node::{Node, NodeConfig}; pub struct NetworkNode; impl NodeTab for NetworkNode { - fn get_type(&self) -> NodeTabType { - NodeTabType::Info - } + fn get_type(&self) -> NodeTabType { + NodeTabType::Info + } - fn tab_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { - ScrollArea::vertical() - .id_salt("integrated_node_info_scroll") - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(2.0); - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - // Show node stats content. - node_stats_ui(ui); - }); - }); - } + fn tab_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { + ScrollArea::vertical() + .id_salt("integrated_node_info_scroll") + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(2.0); + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + // Show node stats content. + node_stats_ui(ui); + }); + }); + } } /// Draw node statistics content. fn node_stats_ui(ui: &mut egui::Ui) { - let server_stats = Node::get_stats(); - let stats = server_stats.as_ref().unwrap(); + let server_stats = Node::get_stats(); + let stats = server_stats.as_ref().unwrap(); - // Show header info. - View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header"))); - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - View::label_box(ui, - stats.header_stats.last_block_h.to_string(), - t!("network_node.hash"), - [true, false, false, false]); - }); - columns[1].vertical_centered(|ui| { - View::label_box(ui, - stats.header_stats.height.to_string(), - t!("network_node.height"), - [false, true, false, false]); - }); - }); - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - View::label_box(ui, - stats.header_stats.total_difficulty.to_string(), - t!("network_node.difficulty"), - [false, false, true, false]); - }); - columns[1].vertical_centered(|ui| { - let h_ts = stats.header_stats.latest_timestamp.timestamp(); - let h_time = View::format_time(h_ts); - View::label_box(ui, - h_time, - t!("network_node.time"), - [false, false, false, true]); - }); - }); - ui.add_space(5.0); + // Show header info. + View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header"))); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + View::label_box( + ui, + stats.header_stats.last_block_h.to_string(), + t!("network_node.hash"), + [true, false, false, false], + ); + }); + columns[1].vertical_centered(|ui| { + View::label_box( + ui, + stats.header_stats.height.to_string(), + t!("network_node.height"), + [false, true, false, false], + ); + }); + }); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + View::label_box( + ui, + stats.header_stats.total_difficulty.to_string(), + t!("network_node.difficulty"), + [false, false, true, false], + ); + }); + columns[1].vertical_centered(|ui| { + let h_ts = stats.header_stats.latest_timestamp.timestamp(); + let h_time = View::format_time(h_ts); + View::label_box( + ui, + h_time, + t!("network_node.time"), + [false, false, false, true], + ); + }); + }); + ui.add_space(5.0); - // Show block info. - View::sub_title(ui, format!("{} {}", CUBE, t!("network_node.block"))); - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - View::label_box(ui, - stats.chain_stats.last_block_h.to_string(), - t!("network_node.hash"), - [true, false, false, false]); - }); - columns[1].vertical_centered(|ui| { - View::label_box(ui, - stats.chain_stats.height.to_string(), - t!("network_node.height"), - [false, true, false, false]); - }); - }); - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - View::label_box(ui, - stats.chain_stats.total_difficulty.to_string(), - t!("network_node.difficulty"), - [false, false, true, false]); - }); - columns[1].vertical_centered(|ui| { - let b_ts = stats.chain_stats.latest_timestamp.timestamp(); - let b_time = View::format_time(b_ts); - View::label_box(ui, - b_time, - t!("network_node.time"), - [false, false, false, true]); - }); - }); - ui.add_space(5.0); + // Show block info. + View::sub_title(ui, format!("{} {}", CUBE, t!("network_node.block"))); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + View::label_box( + ui, + stats.chain_stats.last_block_h.to_string(), + t!("network_node.hash"), + [true, false, false, false], + ); + }); + columns[1].vertical_centered(|ui| { + View::label_box( + ui, + stats.chain_stats.height.to_string(), + t!("network_node.height"), + [false, true, false, false], + ); + }); + }); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + View::label_box( + ui, + stats.chain_stats.total_difficulty.to_string(), + t!("network_node.difficulty"), + [false, false, true, false], + ); + }); + columns[1].vertical_centered(|ui| { + let b_ts = stats.chain_stats.latest_timestamp.timestamp(); + let b_time = View::format_time(b_ts); + View::label_box( + ui, + b_time, + t!("network_node.time"), + [false, false, false, true], + ); + }); + }); + ui.add_space(5.0); - // Show data info. - View::sub_title(ui, format!("{} {}", SHARE_NETWORK, t!("network_node.data"))); - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - let tx_stat = match &stats.tx_stats { - None => "0 (0)".to_string(), - Some(tx) => format!("{} ({})", tx.tx_pool_size, tx.tx_pool_kernels) - }; - View::label_box(ui, - tx_stat, - t!("network_node.main_pool"), - [true, false, false, false]); - }); - columns[1].vertical_centered(|ui| { - let stem_tx_stat = match &stats.tx_stats { - None => "0 (0)".to_string(), - Some(stx) => format!("{} ({})", - stx.stem_pool_size, - stx.stem_pool_kernels) - }; - View::label_box(ui, - stem_tx_stat, - t!("network_node.stem_pool"), - [false, true, false, false]); - }); - }); - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - View::label_box(ui, - stats.disk_usage_gb.to_string(), - t!("network_node.size"), - [false, false, true, false]); - }); - columns[1].vertical_centered(|ui| { - let peers_txt = format!("{} ({})", - stats.peer_count, - NodeConfig::get_max_outbound_peers()); - View::label_box(ui, peers_txt, t!("network_node.peers"), [false, false, false, true]); - }); - }); - ui.add_space(5.0); + // Show data info. + View::sub_title(ui, format!("{} {}", SHARE_NETWORK, t!("network_node.data"))); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + let tx_stat = match &stats.tx_stats { + None => "0 (0)".to_string(), + Some(tx) => format!("{} ({})", tx.tx_pool_size, tx.tx_pool_kernels), + }; + View::label_box( + ui, + tx_stat, + t!("network_node.main_pool"), + [true, false, false, false], + ); + }); + columns[1].vertical_centered(|ui| { + let stem_tx_stat = match &stats.tx_stats { + None => "0 (0)".to_string(), + Some(stx) => format!("{} ({})", stx.stem_pool_size, stx.stem_pool_kernels), + }; + View::label_box( + ui, + stem_tx_stat, + t!("network_node.stem_pool"), + [false, true, false, false], + ); + }); + }); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + View::label_box( + ui, + stats.disk_usage_gb.to_string(), + t!("network_node.size"), + [false, false, true, false], + ); + }); + columns[1].vertical_centered(|ui| { + let peers_txt = format!( + "{} ({})", + stats.peer_count, + NodeConfig::get_max_outbound_peers() + ); + View::label_box( + ui, + peers_txt, + t!("network_node.peers"), + [false, false, false, true], + ); + }); + }); + ui.add_space(5.0); - // Show peer stats when available. - if stats.peer_count > 0 { - View::sub_title(ui, format!("{} {}", HANDSHAKE, t!("network_node.peers"))); - let peers = &stats.peer_stats; - for (index, ps) in peers.iter().enumerate() { - peer_item_ui(ui, ps, View::item_rounding(index, peers.len(), false)); - } - ui.add_space(5.0); - } + // Show peer stats when available. + if stats.peer_count > 0 { + View::sub_title(ui, format!("{} {}", HANDSHAKE, t!("network_node.peers"))); + let peers = &stats.peer_stats; + for (index, ps) in peers.iter().enumerate() { + peer_item_ui(ui, ps, View::item_rounding(index, peers.len(), false)); + } + ui.add_space(5.0); + } } const PEER_ITEM_HEIGHT: f32 = 77.0; /// Draw connected peer info item. fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, r: CornerRadius) { - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(PEER_ITEM_HEIGHT); - ui.allocate_ui(rect.size(), |ui| { - ui.vertical(|ui| { - ui.add_space(4.0); + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(PEER_ITEM_HEIGHT); + ui.allocate_ui(rect.size(), |ui| { + ui.vertical(|ui| { + ui.add_space(4.0); - // Draw round background. - ui.painter().rect(rect, r, Colors::fill(), View::item_stroke(), StrokeKind::Outside); + // Draw round background. + ui.painter().rect( + rect, + r, + Colors::fill(), + View::item_stroke(), + StrokeKind::Outside, + ); - // Draw IP address. - ui.horizontal(|ui| { - ui.add_space(7.0); - ui.label(RichText::new(&peer.addr) - .color(Colors::white_or_black(true)) - .size(17.0)); - }); - // Draw difficulty and height. - ui.horizontal(|ui| { - ui.add_space(6.0); - let diff_text = format!("{} {} {} {}", - PACKAGE, - peer.total_difficulty, - AT, - peer.height); - ui.label(RichText::new(diff_text) - .color(Colors::title(false)) - .size(15.0)); - }); - // Draw user-agent. - ui.horizontal(|ui| { - ui.add_space(6.0); - let agent_text = format!("{} {}", DEVICES, &peer.user_agent); - ui.label(RichText::new(agent_text) - .color(Colors::gray()) - .size(15.0)); - }); + // Draw IP address. + ui.horizontal(|ui| { + ui.add_space(7.0); + ui.label( + RichText::new(&peer.addr) + .color(Colors::white_or_black(true)) + .size(17.0), + ); + }); + // Draw difficulty and height. + ui.horizontal(|ui| { + ui.add_space(6.0); + let diff_text = format!( + "{} {} {} {}", + PACKAGE, peer.total_difficulty, AT, peer.height + ); + ui.label( + RichText::new(diff_text) + .color(Colors::title(false)) + .size(15.0), + ); + }); + // Draw user-agent. + ui.horizontal(|ui| { + ui.add_space(6.0); + let agent_text = format!("{} {}", DEVICES, &peer.user_agent); + ui.label(RichText::new(agent_text).color(Colors::gray()).size(15.0)); + }); - ui.add_space(3.0); - }); - }); -} \ No newline at end of file + ui.add_space(3.0); + }); + }); +} diff --git a/src/gui/views/network/settings.rs b/src/gui/views/network/settings.rs index ed727834..17606c2d 100644 --- a/src/gui/views/network/settings.rs +++ b/src/gui/views/network/settings.rs @@ -12,278 +12,302 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{RichText, ScrollArea}; use egui::scroll_area::ScrollBarVisibility; +use egui::{RichText, ScrollArea}; use crate::gui::Colors; use crate::gui::icons::{ARROW_COUNTER_CLOCKWISE, TRASH}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, Content, View}; -use crate::gui::views::network::setup::{DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup}; +use crate::gui::views::network::setup::{ + DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup, +}; use crate::gui::views::network::types::{NodeTab, NodeTabType}; use crate::gui::views::types::{ContentContainer, ModalPosition}; +use crate::gui::views::{Content, Modal, View}; use crate::node::{Node, NodeConfig}; /// Integrated node settings tab content. pub struct NetworkSettings { - /// Integrated node general setup content. - node: NodeSetup, - /// P2P server setup content. - p2p: P2PSetup, - /// Stratum server setup content. - stratum: StratumSetup, - /// Pool setup content. - pool: PoolSetup, - /// Dandelion server setup content. - dandelion: DandelionSetup, + /// Integrated node general setup content. + node: NodeSetup, + /// P2P server setup content. + p2p: P2PSetup, + /// Stratum server setup content. + stratum: StratumSetup, + /// Pool setup content. + pool: PoolSetup, + /// Dandelion server setup content. + dandelion: DandelionSetup, - /// Flag to check if reset of data was called. - data_reset: bool, + /// Flag to check if reset of data was called. + data_reset: bool, } /// Identifier for settings reset confirmation [`Modal`]. pub const RESET_SETTINGS_CONFIRMATION_MODAL: &'static str = "reset_settings_confirmation"; impl Default for NetworkSettings { - fn default() -> Self { - Self { - node: NodeSetup::default(), - p2p: P2PSetup::default(), - stratum: StratumSetup::default(), - pool: PoolSetup::default(), - dandelion: DandelionSetup::default(), - data_reset: false, - } - } + fn default() -> Self { + Self { + node: NodeSetup::default(), + p2p: P2PSetup::default(), + stratum: StratumSetup::default(), + pool: PoolSetup::default(), + dandelion: DandelionSetup::default(), + data_reset: false, + } + } } impl ContentContainer for NetworkSettings { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - RESET_SETTINGS_CONFIRMATION_MODAL - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![RESET_SETTINGS_CONFIRMATION_MODAL] + } - fn modal_ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - _: &dyn PlatformCallbacks) { - match modal.id { - RESET_SETTINGS_CONFIRMATION_MODAL => reset_settings_confirmation_modal(ui), - _ => {} - } - } + fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, _: &dyn PlatformCallbacks) { + match modal.id { + RESET_SETTINGS_CONFIRMATION_MODAL => reset_settings_confirmation_modal(ui), + _ => {} + } + } - fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - ScrollArea::vertical() - .id_salt("node_settings_scroll") - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(1.0); - ui.vertical_centered(|ui| { - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - // Draw node setup section. - self.node.ui(ui, cb); + fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + ScrollArea::vertical() + .id_salt("node_settings_scroll") + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(1.0); + ui.vertical_centered(|ui| { + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + // Draw node setup section. + self.node.ui(ui, cb); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(4.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(4.0); - // Draw P2P server setup section. - self.p2p.ui(ui, cb); + // Draw P2P server setup section. + self.p2p.ui(ui, cb); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(4.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(4.0); - // Draw Stratum server setup section. - self.stratum.ui(ui, cb); + // Draw Stratum server setup section. + self.stratum.ui(ui, cb); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(4.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(4.0); - // Draw pool setup section. - self.pool.ui(ui, cb); + // Draw pool setup section. + self.pool.ui(ui, cb); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(4.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(4.0); - // Draw Dandelion server setup section. - self.dandelion.ui(ui, cb); + // Draw Dandelion server setup section. + self.dandelion.ui(ui, cb); - // Draw content to reset the data. - if !Node::is_restarting() && !self.data_reset { - ui.add_space(4.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + // Draw content to reset the data. + if !Node::is_restarting() && !self.data_reset { + ui.add_space(4.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - self.reset_data_ui(ui); - } + self.reset_data_ui(ui); + } - ui.add_space(6.0); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(6.0); - // Draw reset settings content. - reset_settings_ui(ui); - }); - }); - }); - } + // Draw reset settings content. + reset_settings_ui(ui); + }); + }); + }); + } } impl NodeTab for NetworkSettings { - fn get_type(&self) -> NodeTabType { - NodeTabType::Settings - } + fn get_type(&self) -> NodeTabType { + NodeTabType::Settings + } - fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - self.ui(ui, cb); - } + fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + self.ui(ui, cb); + } } impl NetworkSettings { - /// Reminder to restart enabled node to show on edit setting at [`Modal`]. - pub fn node_restart_required_ui(ui: &mut egui::Ui) { - if Node::is_running() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.restart_node_required")) - .size(16.0) - .color(Colors::green()) - ); - } - } + /// Reminder to restart enabled node to show on edit setting at [`Modal`]. + pub fn node_restart_required_ui(ui: &mut egui::Ui) { + if Node::is_running() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.restart_node_required")) + .size(16.0) + .color(Colors::green()), + ); + } + } - /// Draw IP addresses as radio buttons. - pub fn ip_addrs_ui(ui: &mut egui::Ui, - saved_ip: &String, - ips: &Vec, - on_change: impl FnOnce(&String)) { - let mut selected_ip = saved_ip; + /// Draw IP addresses as radio buttons. + pub fn ip_addrs_ui( + ui: &mut egui::Ui, + saved_ip: &String, + ips: &Vec, + on_change: impl FnOnce(&String), + ) { + let mut selected_ip = saved_ip; - // Set first IP address as current if saved is not present at system. - if !ips.contains(saved_ip) { - selected_ip = ips.get(0).unwrap(); - } + // Set first IP address as current if saved is not present at system. + if !ips.contains(saved_ip) { + selected_ip = ips.get(0).unwrap(); + } - ui.add_space(2.0); + ui.add_space(2.0); - // Show available IP addresses on the system. - let _ = ips.chunks(2).map(|x| { - if x.len() == 2 { - ui.columns(2, |columns| { - let ip_left = x.get(0).unwrap(); - columns[0].vertical_centered(|ui| { - View::radio_value(ui, &mut selected_ip, ip_left, ip_left.to_string()); - }); - let ip_right = x.get(1).unwrap(); - columns[1].vertical_centered(|ui| { - View::radio_value(ui, &mut selected_ip, ip_right, ip_right.to_string()); - }) - }); - } else { - let ip = x.get(0).unwrap(); - View::radio_value(ui, &mut selected_ip, ip, ip.to_string()); - } - ui.add_space(12.0); - }).collect::>(); + // Show available IP addresses on the system. + let _ = ips + .chunks(2) + .map(|x| { + if x.len() == 2 { + ui.columns(2, |columns| { + let ip_left = x.get(0).unwrap(); + columns[0].vertical_centered(|ui| { + View::radio_value(ui, &mut selected_ip, ip_left, ip_left.to_string()); + }); + let ip_right = x.get(1).unwrap(); + columns[1].vertical_centered(|ui| { + View::radio_value(ui, &mut selected_ip, ip_right, ip_right.to_string()); + }) + }); + } else { + let ip = x.get(0).unwrap(); + View::radio_value(ui, &mut selected_ip, ip, ip.to_string()); + } + ui.add_space(12.0); + }) + .collect::>(); - if saved_ip != selected_ip { - on_change(&selected_ip.to_string()); - } - } + if saved_ip != selected_ip { + on_change(&selected_ip.to_string()); + } + } - /// Show message when IP addresses are not available at system. - pub fn no_ip_address_ui(ui: &mut egui::Ui) { - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network.no_ips")) - .size(16.0) - .color(Colors::inactive_text()) - ); - ui.add_space(6.0); - }); - } - - /// Draw content to reset data. - fn reset_data_ui(&mut self, ui: &mut egui::Ui) { - ui.add_space(4.0); - View::colored_text_button(ui, - format!("{} {}", TRASH, t!("network_settings.reset_data")), - Colors::red(), - Colors::white_or_black(false), || { - Node::reset_data(false); - self.data_reset = true; - }); - ui.add_space(6.0); - ui.label(RichText::new(t!("network_settings.reset_data_desc")) - .size(16.0) - .color(Colors::inactive_text()) - ); - ui.add_space(4.0); - } + /// Show message when IP addresses are not available at system. + pub fn no_ip_address_ui(ui: &mut egui::Ui) { + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network.no_ips")) + .size(16.0) + .color(Colors::inactive_text()), + ); + ui.add_space(6.0); + }); + } + /// Draw content to reset data. + fn reset_data_ui(&mut self, ui: &mut egui::Ui) { + ui.add_space(4.0); + View::colored_text_button( + ui, + format!("{} {}", TRASH, t!("network_settings.reset_data")), + Colors::red(), + Colors::white_or_black(false), + || { + Node::reset_data(false); + self.data_reset = true; + }, + ); + ui.add_space(6.0); + ui.label( + RichText::new(t!("network_settings.reset_data_desc")) + .size(16.0) + .color(Colors::inactive_text()), + ); + ui.add_space(4.0); + } } /// Draw button to reset integrated node settings to default values. fn reset_settings_ui(ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.reset_settings_desc")) - .size(16.0) - .color(Colors::text(false))); - ui.add_space(8.0); - let button_text = format!("{} {}", - ARROW_COUNTER_CLOCKWISE, - t!("network_settings.reset_settings")); - View::action_button(ui, button_text, || { - // Show modal to confirm settings reset. - Modal::new(RESET_SETTINGS_CONFIRMATION_MODAL) - .position(ModalPosition::Center) - .title(t!("confirmation")) - .show(); - }); + ui.label( + RichText::new(t!("network_settings.reset_settings_desc")) + .size(16.0) + .color(Colors::text(false)), + ); + ui.add_space(8.0); + let button_text = format!( + "{} {}", + ARROW_COUNTER_CLOCKWISE, + t!("network_settings.reset_settings") + ); + View::action_button(ui, button_text, || { + // Show modal to confirm settings reset. + Modal::new(RESET_SETTINGS_CONFIRMATION_MODAL) + .position(ModalPosition::Center) + .title(t!("confirmation")) + .show(); + }); - // Show reminder to restart enabled node. - if Node::is_running() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.restart_node_required")) - .size(16.0) - .color(Colors::gray()) - ); - } - ui.add_space(10.0); + // Show reminder to restart enabled node. + if Node::is_running() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.restart_node_required")) + .size(16.0) + .color(Colors::gray()), + ); + } + ui.add_space(10.0); } /// Confirmation to reset settings to default values. fn reset_settings_confirmation_modal(ui: &mut egui::Ui) { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - let reset_text = format!("{}?", t!("network_settings.reset_settings_desc")); - ui.label(RichText::new(reset_text) - .size(17.0) - .color(Colors::text(false))); - ui.add_space(8.0); - }); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let reset_text = format!("{}?", t!("network_settings.reset_settings_desc")); + ui.label( + RichText::new(reset_text) + .size(17.0) + .color(Colors::text(false)), + ); + ui.add_space(8.0); + }); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("network_settings.reset"), Colors::white_or_black(false), || { - NodeConfig::reset_to_default(); - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - Modal::close(); - }); - }); - }); - ui.add_space(6.0); - }); -} \ No newline at end of file + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("network_settings.reset"), + Colors::white_or_black(false), + || { + NodeConfig::reset_to_default(); + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + Modal::close(); + }, + ); + }); + }); + ui.add_space(6.0); + }); +} diff --git a/src/gui/views/network/setup/dandelion.rs b/src/gui/views/network/setup/dandelion.rs index e66f2636..632c5920 100644 --- a/src/gui/views/network/setup/dandelion.rs +++ b/src/gui/views/network/setup/dandelion.rs @@ -14,27 +14,27 @@ use egui::{Id, RichText}; +use crate::gui::Colors; use crate::gui::icons::{CLOCK_COUNTDOWN, GRAPH, TIMER, WATCH}; use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::network::NetworkSettings; use crate::gui::views::types::{ContentContainer, ModalPosition}; use crate::gui::views::{Modal, TextEdit, View}; -use crate::gui::Colors; -use crate::gui::views::network::NetworkSettings; use crate::node::NodeConfig; /// Dandelion server setup section content. pub struct DandelionSetup { - /// Epoch duration value in seconds. - epoch_edit: String, + /// Epoch duration value in seconds. + epoch_edit: String, - /// Embargo expiration time value in seconds to fluff and broadcast if tx not seen on network. - embargo_edit: String, + /// Embargo expiration time value in seconds to fluff and broadcast if tx not seen on network. + embargo_edit: String, - /// Aggregation period value in seconds. - aggregation_edit: String, + /// Aggregation period value in seconds. + aggregation_edit: String, - /// Stem phase probability value (stem 90% of the time, fluff 10% of the time by default). - stem_prob_edit: String, + /// Stem phase probability value (stem 90% of the time, fluff 10% of the time by default). + stem_prob_edit: String, } /// Identifier epoch duration value [`Modal`]. @@ -47,396 +47,453 @@ pub const AGGREGATION_MODAL: &'static str = "aggregation_secs"; pub const STEM_PROBABILITY_MODAL: &'static str = "stem_probability"; impl Default for DandelionSetup { - fn default() -> Self { - Self { - epoch_edit: NodeConfig::get_dandelion_epoch(), - embargo_edit: NodeConfig::get_reorg_cache_period(), - aggregation_edit: NodeConfig::get_dandelion_aggregation(), - stem_prob_edit: NodeConfig::get_stem_probability(), - } - } + fn default() -> Self { + Self { + epoch_edit: NodeConfig::get_dandelion_epoch(), + embargo_edit: NodeConfig::get_reorg_cache_period(), + aggregation_edit: NodeConfig::get_dandelion_aggregation(), + stem_prob_edit: NodeConfig::get_stem_probability(), + } + } } impl ContentContainer for DandelionSetup { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - EPOCH_MODAL, - EMBARGO_MODAL, - AGGREGATION_MODAL, - STEM_PROBABILITY_MODAL - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![ + EPOCH_MODAL, + EMBARGO_MODAL, + AGGREGATION_MODAL, + STEM_PROBABILITY_MODAL, + ] + } - fn modal_ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - match modal.id { - EPOCH_MODAL => self.epoch_modal(ui, modal, cb), - EMBARGO_MODAL => self.embargo_modal(ui, modal, cb), - AGGREGATION_MODAL => self.aggregation_modal(ui, modal, cb), - STEM_PROBABILITY_MODAL => self.stem_prob_modal(ui, modal, cb), - _ => {} - } - } + fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + match modal.id { + EPOCH_MODAL => self.epoch_modal(ui, modal, cb), + EMBARGO_MODAL => self.embargo_modal(ui, modal, cb), + AGGREGATION_MODAL => self.aggregation_modal(ui, modal, cb), + STEM_PROBABILITY_MODAL => self.stem_prob_modal(ui, modal, cb), + _ => {} + } + } - fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { - View::sub_title(ui, format!("{} {}", GRAPH, "Dandelion")); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(6.0); + fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { + View::sub_title(ui, format!("{} {}", GRAPH, "Dandelion")); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(6.0); - ui.vertical_centered(|ui| { - // Show epoch duration setup. - self.epoch_ui(ui); + ui.vertical_centered(|ui| { + // Show epoch duration setup. + self.epoch_ui(ui); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Show embargo expiration time setup. - self.embargo_ui(ui); + // Show embargo expiration time setup. + self.embargo_ui(ui); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Show aggregation period setup. - self.aggregation_ui(ui); + // Show aggregation period setup. + self.aggregation_ui(ui); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Show Stem phase probability setup. - self.stem_prob_ui(ui); + // Show Stem phase probability setup. + self.stem_prob_ui(ui); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); - // Show setup to always stem our txs. - let always_stem = NodeConfig::always_stem_our_txs(); - View::checkbox(ui, always_stem, t!("network_settings.stem_txs"), || { - NodeConfig::toggle_always_stem_our_txs(); - }); - ui.add_space(6.0); - }); - } + // Show setup to always stem our txs. + let always_stem = NodeConfig::always_stem_our_txs(); + View::checkbox(ui, always_stem, t!("network_settings.stem_txs"), || { + NodeConfig::toggle_always_stem_our_txs(); + }); + ui.add_space(6.0); + }); + } } impl DandelionSetup { - /// Draw epoch duration setup content. - fn epoch_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.epoch_duration")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); + /// Draw epoch duration setup content. + fn epoch_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.epoch_duration")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - let epoch = NodeConfig::get_dandelion_epoch(); - View::button(ui, format!("{} {}", WATCH, &epoch), Colors::white_or_black(false), || { - // Setup values for modal. - self.epoch_edit = epoch; - // Show epoch setup modal. - Modal::new(EPOCH_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(6.0); - } + let epoch = NodeConfig::get_dandelion_epoch(); + View::button( + ui, + format!("{} {}", WATCH, &epoch), + Colors::white_or_black(false), + || { + // Setup values for modal. + self.epoch_edit = epoch; + // Show epoch setup modal. + Modal::new(EPOCH_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(6.0); + } - /// Draw epoch duration [`Modal`] content. - fn epoch_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - // Save button callback. - let on_save = |c: &mut DandelionSetup| { - if let Ok(epoch) = c.epoch_edit.parse::() { - NodeConfig::save_dandelion_epoch(epoch); - Modal::close(); - } - }; + /// Draw epoch duration [`Modal`] content. + fn epoch_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + // Save button callback. + let on_save = |c: &mut DandelionSetup| { + if let Ok(epoch) = c.epoch_edit.parse::() { + NodeConfig::save_dandelion_epoch(epoch); + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.epoch_duration")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.epoch_duration")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw epoch text edit. - let mut epoch_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - epoch_edit.ui(ui, &mut self.epoch_edit, cb); - if epoch_edit.enter_pressed { - on_save(self); - } + // Draw epoch text edit. + let mut epoch_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + epoch_edit.ui(ui, &mut self.epoch_edit, cb); + if epoch_edit.enter_pressed { + on_save(self); + } - // Show error when specified value is not valid or reminder to restart enabled node. - if self.epoch_edit.parse::().is_err() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.not_valid_value")) - .size(17.0) - .color(Colors::red())); - } else { - NetworkSettings::node_restart_required_ui(ui); - } - ui.add_space(12.0); - }); + // Show error when specified value is not valid or reminder to restart enabled node. + if self.epoch_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::red()), + ); + } else { + NetworkSettings::node_restart_required_ui(ui); + } + ui.add_space(12.0); + }); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + } - /// Draw embargo expiration time setup content. - fn embargo_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.embargo_timer")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); + /// Draw embargo expiration time setup content. + fn embargo_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.embargo_timer")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - let embargo = NodeConfig::get_dandelion_embargo(); - View::button(ui, format!("{} {}", TIMER, &embargo), Colors::white_or_black(false), || { - self.embargo_edit = embargo; - // Show embargo setup modal. - Modal::new(EMBARGO_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(6.0); - } + let embargo = NodeConfig::get_dandelion_embargo(); + View::button( + ui, + format!("{} {}", TIMER, &embargo), + Colors::white_or_black(false), + || { + self.embargo_edit = embargo; + // Show embargo setup modal. + Modal::new(EMBARGO_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(6.0); + } - /// Draw epoch duration [`Modal`] content. - fn embargo_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - // Save button callback. - let on_save = |c: &mut DandelionSetup| { - if let Ok(embargo) = c.embargo_edit.parse::() { - NodeConfig::save_dandelion_embargo(embargo); - Modal::close(); - } - }; + /// Draw epoch duration [`Modal`] content. + fn embargo_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + // Save button callback. + let on_save = |c: &mut DandelionSetup| { + if let Ok(embargo) = c.embargo_edit.parse::() { + NodeConfig::save_dandelion_embargo(embargo); + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.embargo_timer")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.embargo_timer")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw embargo text edit. - let mut embargo_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - embargo_edit.ui(ui, &mut self.embargo_edit, cb); - if embargo_edit.enter_pressed { - on_save(self); - } + // Draw embargo text edit. + let mut embargo_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + embargo_edit.ui(ui, &mut self.embargo_edit, cb); + if embargo_edit.enter_pressed { + on_save(self); + } - // Show error when specified value is not valid or reminder to restart enabled node. - if self.embargo_edit.parse::().is_err() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.not_valid_value")) - .size(17.0) - .color(Colors::red())); - } else { - NetworkSettings::node_restart_required_ui(ui); - } - ui.add_space(12.0); - }); + // Show error when specified value is not valid or reminder to restart enabled node. + if self.embargo_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::red()), + ); + } else { + NetworkSettings::node_restart_required_ui(ui); + } + ui.add_space(12.0); + }); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + } - /// Draw aggregation period setup content. - fn aggregation_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.aggregation_period")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); + /// Draw aggregation period setup content. + fn aggregation_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.aggregation_period")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - let ag = NodeConfig::get_dandelion_aggregation(); - View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, &ag), Colors::white_or_black(false), || { - // Setup values for modal. - self.aggregation_edit = ag; - // Show aggregation setup modal. - Modal::new(AGGREGATION_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(6.0); - } + let ag = NodeConfig::get_dandelion_aggregation(); + View::button( + ui, + format!("{} {}", CLOCK_COUNTDOWN, &ag), + Colors::white_or_black(false), + || { + // Setup values for modal. + self.aggregation_edit = ag; + // Show aggregation setup modal. + Modal::new(AGGREGATION_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(6.0); + } - /// Draw aggregation period [`Modal`] content. - fn aggregation_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - // Save button callback. - let on_save = |c: &mut DandelionSetup| { - if let Ok(embargo) = c.aggregation_edit.parse::() { - NodeConfig::save_dandelion_aggregation(embargo); - Modal::close(); - } - }; + /// Draw aggregation period [`Modal`] content. + fn aggregation_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + // Save button callback. + let on_save = |c: &mut DandelionSetup| { + if let Ok(embargo) = c.aggregation_edit.parse::() { + NodeConfig::save_dandelion_aggregation(embargo); + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.aggregation_period")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.aggregation_period")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw aggregation period text edit. - let mut aggregation_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - aggregation_edit.ui(ui, &mut self.aggregation_edit, cb); - if aggregation_edit.enter_pressed { - on_save(self); - } + // Draw aggregation period text edit. + let mut aggregation_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + aggregation_edit.ui(ui, &mut self.aggregation_edit, cb); + if aggregation_edit.enter_pressed { + on_save(self); + } - // Show error when specified value is not valid or reminder to restart enabled node. - if self.aggregation_edit.parse::().is_err() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.not_valid_value")) - .size(17.0) - .color(Colors::red())); - } else { - NetworkSettings::node_restart_required_ui(ui); - } - ui.add_space(12.0); - }); + // Show error when specified value is not valid or reminder to restart enabled node. + if self.aggregation_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::red()), + ); + } else { + NetworkSettings::node_restart_required_ui(ui); + } + ui.add_space(12.0); + }); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + } - /// Draw stem phase probability setup content. - fn stem_prob_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.stem_probability")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); + /// Draw stem phase probability setup content. + fn stem_prob_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.stem_probability")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - let stem_prob = NodeConfig::get_stem_probability(); - View::button(ui, format!("{}%", &stem_prob), Colors::white_or_black(false), || { - // Setup values for modal. - self.stem_prob_edit = stem_prob; - // Show stem probability setup modal. - Modal::new(STEM_PROBABILITY_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(6.0); - } + let stem_prob = NodeConfig::get_stem_probability(); + View::button( + ui, + format!("{}%", &stem_prob), + Colors::white_or_black(false), + || { + // Setup values for modal. + self.stem_prob_edit = stem_prob; + // Show stem probability setup modal. + Modal::new(STEM_PROBABILITY_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(6.0); + } - /// Draw stem phase probability [`Modal`] content. - fn stem_prob_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - // Save button callback. - let on_save = |c: &mut DandelionSetup| { - if let Ok(prob) = c.stem_prob_edit.parse::() { - NodeConfig::save_stem_probability(prob); - Modal::close(); - } - }; + /// Draw stem phase probability [`Modal`] content. + fn stem_prob_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + // Save button callback. + let on_save = |c: &mut DandelionSetup| { + if let Ok(prob) = c.stem_prob_edit.parse::() { + NodeConfig::save_stem_probability(prob); + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.stem_probability")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.stem_probability")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw stem phase probability text edit. - let mut stem_prob_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - stem_prob_edit.ui(ui, &mut self.stem_prob_edit, cb); - if stem_prob_edit.enter_pressed { - on_save(self); - } + // Draw stem phase probability text edit. + let mut stem_prob_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + stem_prob_edit.ui(ui, &mut self.stem_prob_edit, cb); + if stem_prob_edit.enter_pressed { + on_save(self); + } - // Show error when specified value is not valid or reminder to restart enabled node. - if self.stem_prob_edit.parse::().is_err() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.not_valid_value")) - .size(17.0) - .color(Colors::red())); - } else { - NetworkSettings::node_restart_required_ui(ui); - } - ui.add_space(12.0); - }); + // Show error when specified value is not valid or reminder to restart enabled node. + if self.stem_prob_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::red()), + ); + } else { + NetworkSettings::node_restart_required_ui(ui); + } + ui.add_space(12.0); + }); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - } -} \ No newline at end of file + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + } +} diff --git a/src/gui/views/network/setup/mod.rs b/src/gui/views/network/setup/mod.rs index 9770fcab..a269ef43 100644 --- a/src/gui/views/network/setup/mod.rs +++ b/src/gui/views/network/setup/mod.rs @@ -25,4 +25,4 @@ mod dandelion; pub use dandelion::DandelionSetup; mod stratum; -pub use stratum::StratumSetup; \ No newline at end of file +pub use stratum::StratumSetup; diff --git a/src/gui/views/network/setup/node.rs b/src/gui/views/network/setup/node.rs index 8affd2e8..1f030405 100644 --- a/src/gui/views/network/setup/node.rs +++ b/src/gui/views/network/setup/node.rs @@ -17,39 +17,41 @@ use eframe::epaint::{RectShape, StrokeKind}; use egui::{CursorIcon, Id, Layout, RichText, Sense, UiBuilder}; use grin_core::global::ChainTypes; -use crate::gui::icons::{CLOCK_CLOCKWISE, COMPUTER_TOWER, FOLDERS, PLUG, POWER, SHIELD, SHIELD_SLASH}; +use crate::AppConfig; +use crate::gui::Colors; +use crate::gui::icons::{ + CLOCK_CLOCKWISE, COMPUTER_TOWER, FOLDERS, PLUG, POWER, SHIELD, SHIELD_SLASH, +}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::network::settings::NetworkSettings; use crate::gui::views::network::NetworkContent; +use crate::gui::views::network::settings::NetworkSettings; use crate::gui::views::types::{ContentContainer, ModalPosition}; use crate::gui::views::{FilePickContent, FilePickContentType, Modal, TextEdit, View}; -use crate::gui::Colors; use crate::node::{Node, NodeConfig}; -use crate::AppConfig; /// Integrated node general setup section content. pub struct NodeSetup { - /// Data path value value for [`Modal`]. - data_path_edit: String, - /// Button to pick directory for chain data. - pick_data_dir: FilePickContent, + /// Data path value value for [`Modal`]. + data_path_edit: String, + /// Button to pick directory for chain data. + pick_data_dir: FilePickContent, - /// IP Addresses available at system. - available_ips: Vec, + /// IP Addresses available at system. + available_ips: Vec, - /// API port value. - api_port_edit: String, - /// Flag to check if API port is available. - api_port_available_edit: bool, + /// API port value. + api_port_edit: String, + /// Flag to check if API port is available. + api_port_available_edit: bool, - /// Flag to check if API port from saved config value is available. - is_api_port_available: bool, + /// Flag to check if API port from saved config value is available. + is_api_port_available: bool, - /// Secret edit value for modal. - secret_edit: String, + /// Secret edit value for modal. + secret_edit: String, - /// Future Time Limit value. - ftl_edit: String, + /// Future Time Limit value. + ftl_edit: String, } /// Identifier for chain data path value [`Modal`]. @@ -64,606 +66,692 @@ const FOREIGN_API_SECRET_MODAL: &'static str = "node_foreign_api_secret"; const FTL_MODAL: &'static str = "node_ftl"; impl Default for NodeSetup { - fn default() -> Self { - let (api_ip, api_port) = NodeConfig::get_api_ip_port(); - let is_api_port_available = NodeConfig::is_api_port_available(&api_ip, &api_port); - Self { - data_path_edit: NodeConfig::get_chain_data_path(), - pick_data_dir: FilePickContent::new( - FilePickContentType::ItemButton(View::item_rounding(0, 1, true)) - ).no_parse().pick_folder(), - available_ips: NodeConfig::get_ip_addrs(), - api_port_edit: api_port, - api_port_available_edit: is_api_port_available, - is_api_port_available, - secret_edit: "".to_string(), - ftl_edit: NodeConfig::get_ftl(), - } - } + fn default() -> Self { + let (api_ip, api_port) = NodeConfig::get_api_ip_port(); + let is_api_port_available = NodeConfig::is_api_port_available(&api_ip, &api_port); + Self { + data_path_edit: NodeConfig::get_chain_data_path(), + pick_data_dir: FilePickContent::new(FilePickContentType::ItemButton( + View::item_rounding(0, 1, true), + )) + .no_parse() + .pick_folder(), + available_ips: NodeConfig::get_ip_addrs(), + api_port_edit: api_port, + api_port_available_edit: is_api_port_available, + is_api_port_available, + secret_edit: "".to_string(), + ftl_edit: NodeConfig::get_ftl(), + } + } } impl ContentContainer for NodeSetup { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - DATA_PATH_MODAL, - API_PORT_MODAL, - API_SECRET_MODAL, - FOREIGN_API_SECRET_MODAL, - FTL_MODAL - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![ + DATA_PATH_MODAL, + API_PORT_MODAL, + API_SECRET_MODAL, + FOREIGN_API_SECRET_MODAL, + FTL_MODAL, + ] + } - fn modal_ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - match modal.id { - DATA_PATH_MODAL => self.data_path_edit_modal_ui(ui, cb), - API_PORT_MODAL => self.api_port_modal(ui, modal, cb), - API_SECRET_MODAL => self.secret_modal(ui, modal, cb), - FOREIGN_API_SECRET_MODAL => self.secret_modal(ui, modal, cb), - FTL_MODAL => self.ftl_modal(ui, modal, cb), - _ => {} - } - } + fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + match modal.id { + DATA_PATH_MODAL => self.data_path_edit_modal_ui(ui, cb), + API_PORT_MODAL => self.api_port_modal(ui, modal, cb), + API_SECRET_MODAL => self.secret_modal(ui, modal, cb), + FOREIGN_API_SECRET_MODAL => self.secret_modal(ui, modal, cb), + FTL_MODAL => self.ftl_modal(ui, modal, cb), + _ => {} + } + } - fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - View::sub_title(ui, format!("{} {}", COMPUTER_TOWER, t!("network_settings.server"))); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(6.0); + fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + View::sub_title( + ui, + format!("{} {}", COMPUTER_TOWER, t!("network_settings.server")), + ); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(6.0); - // Show chain type setup. - Self::chain_type_ui(ui); - ui.add_space(2.0); + // Show chain type setup. + Self::chain_type_ui(ui); + ui.add_space(2.0); - // Show loading indicator or controls to stop/start/restart node. - if Node::is_stopping() || Node::is_restarting() || Node::is_starting() - || Node::data_dir_changing() { - ui.vertical_centered(|ui| { - ui.add_space(8.0); - View::small_loading_spinner(ui); - ui.add_space(2.0); - }); - } else { - if Node::is_running() { - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0); - ui.add_space(6.0); + // Show loading indicator or controls to stop/start/restart node. + if Node::is_stopping() + || Node::is_restarting() + || Node::is_starting() + || Node::data_dir_changing() + { + ui.vertical_centered(|ui| { + ui.add_space(8.0); + View::small_loading_spinner(ui); + ui.add_space(2.0); + }); + } else { + if Node::is_running() { + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0); + ui.add_space(6.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::action_button(ui, t!("network_settings.disable"), || { - Node::stop(false); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::action_button(ui, t!("network_settings.restart"), || { - Node::restart(); - }); - }); - }); - }); - } else { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - let enable_text = format!("{} {}", POWER, t!("network_settings.enable")); - View::action_button(ui, enable_text, || { - Node::start(); - }); - }); - } - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::action_button(ui, t!("network_settings.disable"), || { + Node::stop(false); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::action_button(ui, t!("network_settings.restart"), || { + Node::restart(); + }); + }); + }); + }); + } else { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let enable_text = format!("{} {}", POWER, t!("network_settings.enable")); + View::action_button(ui, enable_text, || { + Node::start(); + }); + }); + } + } - // Autorun node setup. - ui.vertical_centered(|ui| { - ui.add_space(6.0); - NetworkContent::autorun_node_ui(ui); - if Node::is_running() { - ui.add_space(2.0); - ui.label(RichText::new(t!("network_settings.restart_node_required")) - .size(16.0) - .color(Colors::inactive_text()) - ); - ui.add_space(4.0); - } - }); - ui.add_space(6.0); + // Autorun node setup. + ui.vertical_centered(|ui| { + ui.add_space(6.0); + NetworkContent::autorun_node_ui(ui); + if Node::is_running() { + ui.add_space(2.0); + ui.label( + RichText::new(t!("network_settings.restart_node_required")) + .size(16.0) + .color(Colors::inactive_text()), + ); + ui.add_space(4.0); + } + }); + ui.add_space(6.0); - // Show data location selection for Desktop when it already started or turned off. - if !Node::is_restarting() && !Node::is_stopping() && !Node::is_starting() && - View::is_desktop() { - self.data_dir_ui(ui, cb); - ui.add_space(6.0); - } + // Show data location selection for Desktop when it already started or turned off. + if !Node::is_restarting() + && !Node::is_stopping() + && !Node::is_starting() + && View::is_desktop() + { + self.data_dir_ui(ui, cb); + ui.add_space(6.0); + } - if self.available_ips.is_empty() { - // Show message when IP addresses are not available on the system. - NetworkSettings::no_ip_address_ui(ui); - } else { - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + if self.available_ips.is_empty() { + // Show message when IP addresses are not available on the system. + NetworkSettings::no_ip_address_ui(ui); + } else { + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.api_ip")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.api_ip")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - // Show API IP addresses to select. - let (api_ip, api_port) = NodeConfig::get_api_ip_port(); - NetworkSettings::ip_addrs_ui(ui, &api_ip, &self.available_ips, |selected_ip| { - let api_available = NodeConfig::is_api_port_available(selected_ip, &api_port); - self.is_api_port_available = api_available; - NodeConfig::save_api_address(selected_ip, &api_port); - }); - // Show API port setup. - self.api_port_setup_ui(ui); - // Show API secret setup. - self.secret_ui(API_SECRET_MODAL, ui); - ui.add_space(12.0); - // Show Foreign API secret setup. - self.secret_ui(FOREIGN_API_SECRET_MODAL, ui); - ui.add_space(6.0); - }); - } + // Show API IP addresses to select. + let (api_ip, api_port) = NodeConfig::get_api_ip_port(); + NetworkSettings::ip_addrs_ui(ui, &api_ip, &self.available_ips, |selected_ip| { + let api_available = NodeConfig::is_api_port_available(selected_ip, &api_port); + self.is_api_port_available = api_available; + NodeConfig::save_api_address(selected_ip, &api_port); + }); + // Show API port setup. + self.api_port_setup_ui(ui); + // Show API secret setup. + self.secret_ui(API_SECRET_MODAL, ui); + ui.add_space(12.0); + // Show Foreign API secret setup. + self.secret_ui(FOREIGN_API_SECRET_MODAL, ui); + ui.add_space(6.0); + }); + } - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - ui.vertical_centered(|ui| { - // Show FTL setup. - self.ftl_ui(ui); + ui.vertical_centered(|ui| { + // Show FTL setup. + self.ftl_ui(ui); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Validation setup. - self.validation_mode_ui(ui); + // Validation setup. + self.validation_mode_ui(ui); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Archive mode setup. - self.archive_mode_ui(ui); - }); - } + // Archive mode setup. + self.archive_mode_ui(ui); + }); + } } impl NodeSetup { - /// Draw content to change chain data directory. - fn data_dir_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - // Draw round background. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(56.0); - let r = View::item_rounding(0, 1, false); - let bg = Colors::fill_lite(); - let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); - let bg_idx = ui.painter().add(bg_shape.clone()); + /// Draw content to change chain data directory. + fn data_dir_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + // Draw round background. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(56.0); + let r = View::item_rounding(0, 1, false); + let bg = Colors::fill_lite(); + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - let res = ui.scope_builder( - UiBuilder::new() - .sense(Sense::click()) - .layout(Layout::right_to_left(Align::Center)) - .max_rect(rect), |ui| { - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - self.pick_data_dir.ui(ui, cb, |path| { - Node::change_data_dir(path); - }); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(12.0); - ui.vertical(|ui| { - ui.add_space(4.0); - let path = NodeConfig::get_chain_data_path(); - View::ellipsize_text(ui, path, 18.0, Colors::title(false)); - ui.add_space(1.0); - let desc = format!("{} {}", FOLDERS, t!("files_location")); - ui.label(RichText::new(desc).size(15.0).color(Colors::gray())); - ui.add_space(8.0); - }); - }); - }); - } - ).response; - let clicked = res.clicked() || res.long_touched(); - // Setup background and cursor. - if res.hovered() { - res.on_hover_cursor(CursorIcon::PointingHand); - bg_shape.fill = Colors::fill(); - } - ui.painter().set(bg_idx, bg_shape); - // Handle clicks on layout. - if clicked { - self.data_path_edit = NodeConfig::get_chain_data_path(); - // Show chain data path edit modal. - Modal::new(DATA_PATH_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network.node")) - .show(); - } - } + let res = ui + .scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), + |ui| { + ui.allocate_ui_with_layout( + rect.size(), + Layout::right_to_left(Align::Center), + |ui| { + self.pick_data_dir.ui(ui, cb, |path| { + Node::change_data_dir(path); + }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout( + layout_size, + Layout::left_to_right(Align::Center), + |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + let path = NodeConfig::get_chain_data_path(); + View::ellipsize_text(ui, path, 18.0, Colors::title(false)); + ui.add_space(1.0); + let desc = format!("{} {}", FOLDERS, t!("files_location")); + ui.label( + RichText::new(desc).size(15.0).color(Colors::gray()), + ); + ui.add_space(8.0); + }); + }, + ); + }, + ); + }, + ) + .response; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked { + self.data_path_edit = NodeConfig::get_chain_data_path(); + // Show chain data path edit modal. + Modal::new(DATA_PATH_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network.node")) + .show(); + } + } - /// Draw data path input [`Modal`] content. - fn data_path_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - let on_save = |path: &String| { - Node::change_data_dir(path.clone()); - Modal::close(); - }; - ui.label(RichText::new(format!("{}:", t!("files_location"))) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + /// Draw data path input [`Modal`] content. + fn data_path_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let on_save = |path: &String| { + Node::change_data_dir(path.clone()); + Modal::close(); + }; + ui.label( + RichText::new(format!("{}:", t!("files_location"))) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw chain data path text edit. - let mut edit = TextEdit::new(Id::from(DATA_PATH_MODAL)).paste(); - edit.ui(ui, &mut self.data_path_edit, cb); - if edit.enter_pressed { - on_save(&self.data_path_edit); - } - ui.add_space(12.0); + // Draw chain data path text edit. + let mut edit = TextEdit::new(Id::from(DATA_PATH_MODAL)).paste(); + edit.ui(ui, &mut self.data_path_edit, cb); + if edit.enter_pressed { + on_save(&self.data_path_edit); + } + ui.add_space(12.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(&self.data_path_edit); - }); - }); - }); - ui.add_space(6.0); - }); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(&self.data_path_edit); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } - /// Draw [`ChainTypes`] setup content. - pub fn chain_type_ui(ui: &mut egui::Ui) { - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network.type")).size(16.0).color(Colors::gray())); - }); + /// Draw [`ChainTypes`] setup content. + pub fn chain_type_ui(ui: &mut egui::Ui) { + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network.type")) + .size(16.0) + .color(Colors::gray()), + ); + }); - let saved_chain_type = AppConfig::chain_type(); - let mut selected_chain_type = saved_chain_type; + let saved_chain_type = AppConfig::chain_type(); + let mut selected_chain_type = saved_chain_type; - ui.add_space(8.0); - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - let main_type = ChainTypes::Mainnet; - View::radio_value(ui, &mut selected_chain_type, main_type, t!("network.mainnet")); - }); - columns[1].vertical_centered(|ui| { - let test_type = ChainTypes::Testnet; - View::radio_value(ui, &mut selected_chain_type, test_type, t!("network.testnet")); - }) - }); - ui.add_space(8.0); + ui.add_space(8.0); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + let main_type = ChainTypes::Mainnet; + View::radio_value( + ui, + &mut selected_chain_type, + main_type, + t!("network.mainnet"), + ); + }); + columns[1].vertical_centered(|ui| { + let test_type = ChainTypes::Testnet; + View::radio_value( + ui, + &mut selected_chain_type, + test_type, + t!("network.testnet"), + ); + }) + }); + ui.add_space(8.0); - if saved_chain_type != selected_chain_type && !Node::is_restarting() { - AppConfig::change_chain_type(&selected_chain_type); - if Node::is_running() { - Node::restart(); - } - } - } + if saved_chain_type != selected_chain_type && !Node::is_restarting() { + AppConfig::change_chain_type(&selected_chain_type); + if Node::is_running() { + Node::restart(); + } + } + } - /// Draw API port setup content. - fn api_port_setup_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.api_port")).size(16.0).color(Colors::gray())); - ui.add_space(6.0); + /// Draw API port setup content. + fn api_port_setup_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.api_port")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - let (_, port) = NodeConfig::get_api_ip_port(); - View::button(ui, format!("{} {}", PLUG, &port), Colors::white_or_black(false), || { - // Setup values for modal. - self.api_port_edit = port; - self.api_port_available_edit = self.is_api_port_available; + let (_, port) = NodeConfig::get_api_ip_port(); + View::button( + ui, + format!("{} {}", PLUG, &port), + Colors::white_or_black(false), + || { + // Setup values for modal. + self.api_port_edit = port; + self.api_port_available_edit = self.is_api_port_available; - // Show API port modal. - Modal::new(API_PORT_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(6.0); + // Show API port modal. + Modal::new(API_PORT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(6.0); - if !self.is_api_port_available { - // Show error when API server port is unavailable. - ui.label(RichText::new(t!("network_settings.port_unavailable")) - .size(16.0) - .color(Colors::red())); - ui.add_space(6.0); - } - ui.add_space(6.0); - } + if !self.is_api_port_available { + // Show error when API server port is unavailable. + ui.label( + RichText::new(t!("network_settings.port_unavailable")) + .size(16.0) + .color(Colors::red()), + ); + ui.add_space(6.0); + } + ui.add_space(6.0); + } - /// Draw API port [`Modal`] content. - fn api_port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut NodeSetup| { - // Check if port is available. - let (api_ip, _) = NodeConfig::get_api_ip_port(); - let available = NodeConfig::is_api_port_available(&api_ip, &c.api_port_edit); - c.api_port_available_edit = available; - if available { - // Save port at config if it's available. - NodeConfig::save_api_address(&api_ip, &c.api_port_edit); - if Node::is_running() { - Node::restart(); - } - c.is_api_port_available = true; - Modal::close(); - } - }; + /// Draw API port [`Modal`] content. + fn api_port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut NodeSetup| { + // Check if port is available. + let (api_ip, _) = NodeConfig::get_api_ip_port(); + let available = NodeConfig::is_api_port_available(&api_ip, &c.api_port_edit); + c.api_port_available_edit = available; + if available { + // Save port at config if it's available. + NodeConfig::save_api_address(&api_ip, &c.api_port_edit); + if Node::is_running() { + Node::restart(); + } + c.is_api_port_available = true; + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.api_port")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(6.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.api_port")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - // Draw API port text edit. - let mut api_port_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - api_port_edit.ui(ui, &mut self.api_port_edit, cb); - if api_port_edit.enter_pressed { - on_save(self); - } + // Draw API port text edit. + let mut api_port_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + api_port_edit.ui(ui, &mut self.api_port_edit, cb); + if api_port_edit.enter_pressed { + on_save(self); + } - // Show error when specified port is unavailable or reminder to restart enabled node. - if !self.api_port_available_edit { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.port_unavailable")) - .size(16.0) - .color(Colors::red())); - } else { - NetworkSettings::node_restart_required_ui(ui); - } - ui.add_space(12.0); - }); + // Show error when specified port is unavailable or reminder to restart enabled node. + if !self.api_port_available_edit { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.port_unavailable")) + .size(16.0) + .color(Colors::red()), + ); + } else { + NetworkSettings::node_restart_required_ui(ui); + } + ui.add_space(12.0); + }); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + } - /// Draw API secret token setup content. - fn secret_ui(&mut self, modal_id: &'static str, ui: &mut egui::Ui) { - let secret_title = match modal_id { - API_SECRET_MODAL => t!("network_settings.api_secret"), - _ => t!("network_settings.foreign_api_secret") - }; - ui.label(RichText::new(secret_title) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); + /// Draw API secret token setup content. + fn secret_ui(&mut self, modal_id: &'static str, ui: &mut egui::Ui) { + let secret_title = match modal_id { + API_SECRET_MODAL => t!("network_settings.api_secret"), + _ => t!("network_settings.foreign_api_secret"), + }; + ui.label(RichText::new(secret_title).size(16.0).color(Colors::gray())); + ui.add_space(6.0); - let secret_value = match modal_id { - API_SECRET_MODAL => NodeConfig::get_api_secret(false), - _ => NodeConfig::get_api_secret(true) - }; + let secret_value = match modal_id { + API_SECRET_MODAL => NodeConfig::get_api_secret(false), + _ => NodeConfig::get_api_secret(true), + }; - let secret_text = if secret_value.is_some() { - format!("{} {}", SHIELD, t!("network_settings.enabled")) - } else { - format!("{} {}", SHIELD_SLASH, t!("network_settings.disabled")) - }; + let secret_text = if secret_value.is_some() { + format!("{} {}", SHIELD, t!("network_settings.enabled")) + } else { + format!("{} {}", SHIELD_SLASH, t!("network_settings.disabled")) + }; - View::button(ui, secret_text, Colors::white_or_black(false), || { - // Setup values for modal. - self.secret_edit = secret_value.unwrap_or("".to_string()); - // Show secret edit modal. - Modal::new(modal_id) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - } + View::button(ui, secret_text, Colors::white_or_black(false), || { + // Setup values for modal. + self.secret_edit = secret_value.unwrap_or("".to_string()); + // Show secret edit modal. + Modal::new(modal_id) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }); + } - /// Draw API secret token [`Modal`] content. - fn secret_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut NodeSetup| { - let secret = c.secret_edit.clone(); - match modal.id { - API_SECRET_MODAL => { - NodeConfig::save_api_secret(&secret); - } - _ => { - NodeConfig::save_foreign_api_secret(&secret); - } - }; - Modal::close(); - }; + /// Draw API secret token [`Modal`] content. + fn secret_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut NodeSetup| { + let secret = c.secret_edit.clone(); + match modal.id { + API_SECRET_MODAL => { + NodeConfig::save_api_secret(&secret); + } + _ => { + NodeConfig::save_foreign_api_secret(&secret); + } + }; + Modal::close(); + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - let description = match modal.id { - API_SECRET_MODAL => t!("network_settings.api_secret"), - _ => t!("network_settings.foreign_api_secret") - }; - ui.label(RichText::new(description).size(17.0).color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let description = match modal.id { + API_SECRET_MODAL => t!("network_settings.api_secret"), + _ => t!("network_settings.foreign_api_secret"), + }; + ui.label(RichText::new(description).size(17.0).color(Colors::gray())); + ui.add_space(8.0); - // Draw API secret token value text edit. - let mut secret_edit = TextEdit::new(Id::from(modal.id)) - .copy() - .paste(); - secret_edit.ui(ui, &mut self.secret_edit, cb); - if secret_edit.enter_pressed { - on_save(self); - } + // Draw API secret token value text edit. + let mut secret_edit = TextEdit::new(Id::from(modal.id)).copy().paste(); + secret_edit.ui(ui, &mut self.secret_edit, cb); + if secret_edit.enter_pressed { + on_save(self); + } - ui.add_space(6.0); + ui.add_space(6.0); - // Show reminder to restart enabled node. - if Node::is_running() { - ui.label(RichText::new(t!("network_settings.restart_node_required")) - .size(16.0) - .color(Colors::green()) - ); - ui.add_space(6.0); - } - ui.add_space(4.0); - }); + // Show reminder to restart enabled node. + if Node::is_running() { + ui.label( + RichText::new(t!("network_settings.restart_node_required")) + .size(16.0) + .color(Colors::green()), + ); + ui.add_space(6.0); + } + ui.add_space(4.0); + }); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + } - /// Draw FTL setup content. - fn ftl_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.ftl")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); + /// Draw FTL setup content. + fn ftl_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.ftl")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - let ftl = NodeConfig::get_ftl(); - View::button(ui, - format!("{} {}", CLOCK_CLOCKWISE, &ftl), - Colors::white_or_black(false), || { - // Setup values for modal. - self.ftl_edit = ftl; - // Show ftl value setup modal. - Modal::new(FTL_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(6.0); - ui.label(RichText::new(t!("network_settings.ftl_description")) - .size(16.0) - .color(Colors::inactive_text()) - ); - } + let ftl = NodeConfig::get_ftl(); + View::button( + ui, + format!("{} {}", CLOCK_CLOCKWISE, &ftl), + Colors::white_or_black(false), + || { + // Setup values for modal. + self.ftl_edit = ftl; + // Show ftl value setup modal. + Modal::new(FTL_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(6.0); + ui.label( + RichText::new(t!("network_settings.ftl_description")) + .size(16.0) + .color(Colors::inactive_text()), + ); + } - /// Draw FTL [`Modal`] content. - fn ftl_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - // Save button callback. - let on_save = |c: &mut NodeSetup| { - if let Ok(ftl) = c.ftl_edit.parse::() { - NodeConfig::save_ftl(ftl); - Modal::close(); - } - }; + /// Draw FTL [`Modal`] content. + fn ftl_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + // Save button callback. + let on_save = |c: &mut NodeSetup| { + if let Ok(ftl) = c.ftl_edit.parse::() { + NodeConfig::save_ftl(ftl); + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.ftl")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.ftl")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw ftl value text edit. - let mut ftl_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - ftl_edit.ui(ui, &mut self.ftl_edit, cb); - if ftl_edit.enter_pressed { - on_save(self); - } + // Draw ftl value text edit. + let mut ftl_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + ftl_edit.ui(ui, &mut self.ftl_edit, cb); + if ftl_edit.enter_pressed { + on_save(self); + } - // Show error when specified value is not valid or reminder to restart enabled node. - if self.ftl_edit.parse::().is_err() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.not_valid_value")) - .size(17.0) - .color(Colors::red())); - } else { - NetworkSettings::node_restart_required_ui(ui); - } - ui.add_space(12.0); - }); + // Show error when specified value is not valid or reminder to restart enabled node. + if self.ftl_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::red()), + ); + } else { + NetworkSettings::node_restart_required_ui(ui); + } + ui.add_space(12.0); + }); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + } - /// Draw chain validation mode setup content. - pub fn validation_mode_ui(&mut self, ui: &mut egui::Ui) { - let validate = NodeConfig::is_full_chain_validation(); - View::checkbox(ui, validate, t!("network_settings.full_validation"), || { - NodeConfig::toggle_full_chain_validation(); - }); - ui.add_space(4.0); - ui.label(RichText::new(t!("network_settings.full_validation_description")) - .size(16.0) - .color(Colors::inactive_text()) - ); - } + /// Draw chain validation mode setup content. + pub fn validation_mode_ui(&mut self, ui: &mut egui::Ui) { + let validate = NodeConfig::is_full_chain_validation(); + View::checkbox(ui, validate, t!("network_settings.full_validation"), || { + NodeConfig::toggle_full_chain_validation(); + }); + ui.add_space(4.0); + ui.label( + RichText::new(t!("network_settings.full_validation_description")) + .size(16.0) + .color(Colors::inactive_text()), + ); + } - /// Draw archive mode setup content. - fn archive_mode_ui(&mut self, ui: &mut egui::Ui) { - let archive_mode = NodeConfig::is_archive_mode(); - View::checkbox(ui, archive_mode, t!("network_settings.archive_mode"), || { - NodeConfig::toggle_archive_mode(); - }); - ui.add_space(4.0); - ui.label(RichText::new(t!("network_settings.archive_mode_desc")) - .size(16.0) - .color(Colors::inactive_text()) - ); - } -} \ No newline at end of file + /// Draw archive mode setup content. + fn archive_mode_ui(&mut self, ui: &mut egui::Ui) { + let archive_mode = NodeConfig::is_archive_mode(); + View::checkbox( + ui, + archive_mode, + t!("network_settings.archive_mode"), + || { + NodeConfig::toggle_archive_mode(); + }, + ); + ui.add_space(4.0); + ui.label( + RichText::new(t!("network_settings.archive_mode_desc")) + .size(16.0) + .color(Colors::inactive_text()), + ); + } +} diff --git a/src/gui/views/network/setup/p2p.rs b/src/gui/views/network/setup/p2p.rs index d7610ed3..4dfdbf92 100644 --- a/src/gui/views/network/setup/p2p.rs +++ b/src/gui/views/network/setup/p2p.rs @@ -18,53 +18,56 @@ use grin_core::global::ChainTypes; use crate::AppConfig; use crate::gui::Colors; -use crate::gui::icons::{ARROW_FAT_LINES_DOWN, ARROW_FAT_LINES_UP, GLOBE_SIMPLE, HANDSHAKE, PLUG, PLUS_CIRCLE, PROHIBIT_INSET, TRASH}; +use crate::gui::icons::{ + ARROW_FAT_LINES_DOWN, ARROW_FAT_LINES_UP, GLOBE_SIMPLE, HANDSHAKE, PLUG, PLUS_CIRCLE, + PROHIBIT_INSET, TRASH, +}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, TextEdit, View}; use crate::gui::views::network::settings::NetworkSettings; use crate::gui::views::types::{ContentContainer, ModalPosition}; +use crate::gui::views::{Modal, TextEdit, View}; use crate::node::{Node, NodeConfig, PeersConfig}; /// Type of peer. #[derive(Eq, PartialEq)] enum PeerType { - DefaultSeed, - CustomSeed, - Allowed, - Denied, - Preferred + DefaultSeed, + CustomSeed, + Allowed, + Denied, + Preferred, } /// P2P server setup section content. pub struct P2PSetup { - /// P2P port value. - port_edit: String, - /// Flag to check if p2p port is available. - port_available_edit: bool, + /// P2P port value. + port_edit: String, + /// Flag to check if p2p port is available. + port_available_edit: bool, - /// Flag to check if p2p port from saved config value is available. - is_port_available: bool, + /// Flag to check if p2p port from saved config value is available. + is_port_available: bool, - /// Async check entered peer address. - address_check: Bind, - /// Flag to check if peer is correct and/or available. - address_available: Option, - /// Peer edit value for modal. - peer_edit: String, + /// Async check entered peer address. + address_check: Bind, + /// Flag to check if peer is correct and/or available. + address_available: Option, + /// Peer edit value for modal. + peer_edit: String, - /// Default main network seeds. - default_main_seeds: Vec, - /// Default test network seeds. - default_test_seeds: Vec, + /// Default main network seeds. + default_main_seeds: Vec, + /// Default test network seeds. + default_test_seeds: Vec, - /// How long banned peer should stay banned. - ban_window_edit: String, + /// How long banned peer should stay banned. + ban_window_edit: String, - /// Maximum number of inbound peer connections. - max_inbound_count: String, + /// Maximum number of inbound peer connections. + max_inbound_count: String, - /// Maximum number of outbound peer connections. - max_outbound_count: String, + /// Maximum number of outbound peer connections. + max_outbound_count: String, } /// Identifier for port value [`Modal`]. @@ -85,714 +88,797 @@ pub const MAX_INBOUND_MODAL: &'static str = "p2p_max_inbound"; pub const MAX_OUTBOUND_MODAL: &'static str = "p2p_max_outbound"; impl Default for P2PSetup { - fn default() -> Self { - let port = NodeConfig::get_p2p_port(); - let is_port_available = NodeConfig::is_p2p_port_available(&port); - let default_main_seeds = Node::MAINNET_DNS_SEEDS - .iter() - .map(|s| s.to_string()) - .collect(); - let default_test_seeds = Node::TESTNET_DNS_SEEDS - .into_iter() - .map(|s| s.to_string()) - .collect(); - Self { - port_edit: port, - port_available_edit: is_port_available, - address_check: Bind::new(false), - address_available: Some(true), - is_port_available, - peer_edit: "".to_string(), - default_main_seeds, - default_test_seeds, - ban_window_edit: NodeConfig::get_p2p_ban_window(), - max_inbound_count: NodeConfig::get_max_inbound_peers(), - max_outbound_count: NodeConfig::get_max_outbound_peers(), - } - } + fn default() -> Self { + let port = NodeConfig::get_p2p_port(); + let is_port_available = NodeConfig::is_p2p_port_available(&port); + let default_main_seeds = Node::MAINNET_DNS_SEEDS + .iter() + .map(|s| s.to_string()) + .collect(); + let default_test_seeds = Node::TESTNET_DNS_SEEDS + .into_iter() + .map(|s| s.to_string()) + .collect(); + Self { + port_edit: port, + port_available_edit: is_port_available, + address_check: Bind::new(false), + address_available: Some(true), + is_port_available, + peer_edit: "".to_string(), + default_main_seeds, + default_test_seeds, + ban_window_edit: NodeConfig::get_p2p_ban_window(), + max_inbound_count: NodeConfig::get_max_inbound_peers(), + max_outbound_count: NodeConfig::get_max_outbound_peers(), + } + } } impl ContentContainer for P2PSetup { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - PORT_MODAL, - CUSTOM_SEED_MODAL, - ALLOW_PEER_MODAL, - DENY_PEER_MODAL, - PREFER_PEER_MODAL, - BAN_WINDOW_MODAL, - MAX_INBOUND_MODAL, - MAX_OUTBOUND_MODAL - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![ + PORT_MODAL, + CUSTOM_SEED_MODAL, + ALLOW_PEER_MODAL, + DENY_PEER_MODAL, + PREFER_PEER_MODAL, + BAN_WINDOW_MODAL, + MAX_INBOUND_MODAL, + MAX_OUTBOUND_MODAL, + ] + } - fn modal_ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - match modal.id { - PORT_MODAL => self.port_modal(ui, modal, cb), - CUSTOM_SEED_MODAL => self.peer_modal(ui, modal, cb), - ALLOW_PEER_MODAL => self.peer_modal(ui, modal, cb), - DENY_PEER_MODAL => self.peer_modal(ui, modal, cb), - PREFER_PEER_MODAL => self.peer_modal(ui, modal, cb), - BAN_WINDOW_MODAL => self.ban_window_modal(ui, modal, cb), - MAX_INBOUND_MODAL => self.max_inbound_modal(ui, modal, cb), - MAX_OUTBOUND_MODAL => self.max_outbound_modal(ui, modal, cb), - _ => {} - } - } + fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + match modal.id { + PORT_MODAL => self.port_modal(ui, modal, cb), + CUSTOM_SEED_MODAL => self.peer_modal(ui, modal, cb), + ALLOW_PEER_MODAL => self.peer_modal(ui, modal, cb), + DENY_PEER_MODAL => self.peer_modal(ui, modal, cb), + PREFER_PEER_MODAL => self.peer_modal(ui, modal, cb), + BAN_WINDOW_MODAL => self.ban_window_modal(ui, modal, cb), + MAX_INBOUND_MODAL => self.max_inbound_modal(ui, modal, cb), + MAX_OUTBOUND_MODAL => self.max_outbound_modal(ui, modal, cb), + _ => {} + } + } - fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { - View::sub_title(ui, format!("{} {}", HANDSHAKE, t!("network_settings.p2p_server"))); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(6.0); + fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { + View::sub_title( + ui, + format!("{} {}", HANDSHAKE, t!("network_settings.p2p_server")), + ); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(6.0); - ui.vertical_centered(|ui| { - // Show p2p port setup. - self.port_ui(ui); + ui.vertical_centered(|ui| { + // Show p2p port setup. + self.port_ui(ui); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Show seeding type setup. - self.seeding_type_ui(ui); + // Show seeding type setup. + self.seeding_type_ui(ui); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - ui.label(RichText::new(t!("network_settings.allow_list")) - .size(16.0) - .color(Colors::gray())); - ui.add_space(6.0); - // Show allowed peers setup. - self.peer_list_ui(ui, &PeerType::Allowed); + ui.label( + RichText::new(t!("network_settings.allow_list")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); + // Show allowed peers setup. + self.peer_list_ui(ui, &PeerType::Allowed); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - ui.label(RichText::new(t!("network_settings.deny_list")) - .size(16.0) - .color(Colors::gray())); - ui.add_space(6.0); - // Show denied peers setup. - self.peer_list_ui(ui, &PeerType::Denied); + ui.label( + RichText::new(t!("network_settings.deny_list")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); + // Show denied peers setup. + self.peer_list_ui(ui, &PeerType::Denied); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - ui.label(RichText::new(t!("network_settings.favourites")) - .size(16.0) - .color(Colors::gray())); - ui.add_space(6.0); - // Show preferred peers setup. - self.peer_list_ui(ui, &PeerType::Preferred); + ui.label( + RichText::new(t!("network_settings.favourites")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); + // Show preferred peers setup. + self.peer_list_ui(ui, &PeerType::Preferred); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Show ban window setup. - self.ban_window_ui(ui); + // Show ban window setup. + self.ban_window_ui(ui); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Show maximum inbound peers value setup. - self.max_inbound_ui(ui); + // Show maximum inbound peers value setup. + self.max_inbound_ui(ui); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Show maximum outbound peers value setup. - self.max_outbound_ui(ui); - }); - } + // Show maximum outbound peers value setup. + self.max_outbound_ui(ui); + }); + } } /// Title for custom DNS Seeds setup section. const DNS_SEEDS_TITLE: &'static str = "DNS Seeds"; impl P2PSetup { - /// Draw p2p port setup content. - fn port_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.p2p_port")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); + /// Draw p2p port setup content. + fn port_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.p2p_port")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - let port = NodeConfig::get_p2p_port(); - View::button(ui, - format!("{} {}", PLUG, &port), - Colors::white_or_black(false), || { - // Setup values for modal. - self.port_edit = port; - self.port_available_edit = self.is_port_available; - // Show p2p port modal. - Modal::new(PORT_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(6.0); + let port = NodeConfig::get_p2p_port(); + View::button( + ui, + format!("{} {}", PLUG, &port), + Colors::white_or_black(false), + || { + // Setup values for modal. + self.port_edit = port; + self.port_available_edit = self.is_port_available; + // Show p2p port modal. + Modal::new(PORT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(6.0); - // Show error when p2p port is unavailable. - if !self.is_port_available { - ui.add_space(6.0); - ui.label(RichText::new(t!("network_settings.port_unavailable")) - .size(16.0) - .color(Colors::red())); - ui.add_space(12.0); - } - } + // Show error when p2p port is unavailable. + if !self.is_port_available { + ui.add_space(6.0); + ui.label( + RichText::new(t!("network_settings.port_unavailable")) + .size(16.0) + .color(Colors::red()), + ); + ui.add_space(12.0); + } + } - /// Draw p2p port [`Modal`] content. - fn port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut P2PSetup| { - // Check if port is available. - let available = NodeConfig::is_p2p_port_available(&c.port_edit); - c.port_available_edit = available; + /// Draw p2p port [`Modal`] content. + fn port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut P2PSetup| { + // Check if port is available. + let available = NodeConfig::is_p2p_port_available(&c.port_edit); + c.port_available_edit = available; - // Save port at config if it's available. - if available { - NodeConfig::save_p2p_port(c.port_edit.parse::().unwrap()); - if Node::is_running() { - Node::restart(); - } - c.is_port_available = true; - Modal::close(); - } - }; + // Save port at config if it's available. + if available { + NodeConfig::save_p2p_port(c.port_edit.parse::().unwrap()); + if Node::is_running() { + Node::restart(); + } + c.is_port_available = true; + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.p2p_port")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.p2p_port")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw p2p port text edit. - let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - edit.ui(ui, &mut self.port_edit, cb); - if edit.enter_pressed { - on_save(self); - } + // Draw p2p port text edit. + let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + edit.ui(ui, &mut self.port_edit, cb); + if edit.enter_pressed { + on_save(self); + } - // Show error when specified port is unavailable. - if !self.port_available_edit { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.port_unavailable")) - .size(17.0) - .color(Colors::red())); - } + // Show error when specified port is unavailable. + if !self.port_available_edit { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.port_unavailable")) + .size(17.0) + .color(Colors::red()), + ); + } - ui.add_space(12.0); + ui.add_space(12.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } - /// Draw peer list content based on provided [`PeerType`]. - fn peer_list_ui(&mut self, ui: &mut egui::Ui, peer_type: &PeerType) { - let peers = match peer_type { - PeerType::DefaultSeed => { - if AppConfig::chain_type() == ChainTypes::Testnet { - self.default_test_seeds.clone() - } else { - self.default_main_seeds.clone() - } - } - PeerType::CustomSeed => NodeConfig::get_custom_seeds(), - PeerType::Allowed => NodeConfig::get_allowed_peers(), - PeerType::Denied => NodeConfig::get_denied_peers(), - PeerType::Preferred => NodeConfig::get_preferred_peers() - }; - for (index, peer) in peers.iter().enumerate() { - ui.horizontal_wrapped(|ui| { - // Draw peer list item. - peer_item_ui(ui, peer, peer_type, index, peers.len()); - }); - } + /// Draw peer list content based on provided [`PeerType`]. + fn peer_list_ui(&mut self, ui: &mut egui::Ui, peer_type: &PeerType) { + let peers = match peer_type { + PeerType::DefaultSeed => { + if AppConfig::chain_type() == ChainTypes::Testnet { + self.default_test_seeds.clone() + } else { + self.default_main_seeds.clone() + } + } + PeerType::CustomSeed => NodeConfig::get_custom_seeds(), + PeerType::Allowed => NodeConfig::get_allowed_peers(), + PeerType::Denied => NodeConfig::get_denied_peers(), + PeerType::Preferred => NodeConfig::get_preferred_peers(), + }; + for (index, peer) in peers.iter().enumerate() { + ui.horizontal_wrapped(|ui| { + // Draw peer list item. + peer_item_ui(ui, peer, peer_type, index, peers.len()); + }); + } - if peer_type != &PeerType::DefaultSeed { - // Draw description. - if peer_type != &PeerType::CustomSeed { - if !peers.is_empty() { - ui.add_space(12.0); - } - let desc = match peer_type { - PeerType::Allowed => t!("network_settings.allow_list_desc"), - PeerType::Denied => t!("network_settings.deny_list_desc"), - &_ => t!("network_settings.favourites_desc"), - }; - ui.label(RichText::new(desc) - .size(16.0) - .color(Colors::inactive_text())); - ui.add_space(12.0); - } else if !peers.is_empty() { - ui.add_space(12.0); - } + if peer_type != &PeerType::DefaultSeed { + // Draw description. + if peer_type != &PeerType::CustomSeed { + if !peers.is_empty() { + ui.add_space(12.0); + } + let desc = match peer_type { + PeerType::Allowed => t!("network_settings.allow_list_desc"), + PeerType::Denied => t!("network_settings.deny_list_desc"), + &_ => t!("network_settings.favourites_desc"), + }; + ui.label( + RichText::new(desc) + .size(16.0) + .color(Colors::inactive_text()), + ); + ui.add_space(12.0); + } else if !peers.is_empty() { + ui.add_space(12.0); + } - let add_text = if peer_type == &PeerType::CustomSeed { - format!("{} {}", PLUS_CIRCLE, t!("network_settings.add_seed")) - } else { - format!("{} {}", PLUS_CIRCLE, t!("network_settings.add_peer")) + let add_text = if peer_type == &PeerType::CustomSeed { + format!("{} {}", PLUS_CIRCLE, t!("network_settings.add_seed")) + } else { + format!("{} {}", PLUS_CIRCLE, t!("network_settings.add_peer")) + }; + View::button(ui, add_text, Colors::white_or_black(false), || { + // Setup values for modal. + self.address_check = Bind::new(false); + self.address_available = Some(true); + self.peer_edit = "".to_string(); + // Select modal id. + let modal_id = match peer_type { + PeerType::Allowed => ALLOW_PEER_MODAL, + PeerType::Denied => DENY_PEER_MODAL, + PeerType::Preferred => PREFER_PEER_MODAL, + _ => CUSTOM_SEED_MODAL, + }; + // Select modal title. + let modal_title = match peer_type { + PeerType::Allowed => t!("network_settings.allow_list").into(), + PeerType::Denied => t!("network_settings.deny_list").into(), + PeerType::Preferred => t!("network_settings.favourites").into(), + _ => DNS_SEEDS_TITLE.to_string(), + }; + // Show modal to add peer. + Modal::new(modal_id) + .position(ModalPosition::CenterTop) + .title(modal_title) + .show(); + }); + } + ui.add_space(6.0); + } - }; - View::button(ui, add_text, Colors::white_or_black(false), || { - // Setup values for modal. - self.address_check = Bind::new(false); - self.address_available = Some(true); - self.peer_edit = "".to_string(); - // Select modal id. - let modal_id = match peer_type { - PeerType::Allowed => ALLOW_PEER_MODAL, - PeerType::Denied => DENY_PEER_MODAL, - PeerType::Preferred => PREFER_PEER_MODAL, - _ => CUSTOM_SEED_MODAL - }; - // Select modal title. - let modal_title = match peer_type { - PeerType::Allowed => t!("network_settings.allow_list").into(), - PeerType::Denied => t!("network_settings.deny_list").into(), - PeerType::Preferred => t!("network_settings.favourites").into(), - _ => DNS_SEEDS_TITLE.to_string() - }; - // Show modal to add peer. - Modal::new(modal_id) - .position(ModalPosition::CenterTop) - .title(modal_title) - .show(); - }); - } - ui.add_space(6.0); - } + /// Draw peer creation [`Modal`] content. + fn peer_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + if self.address_available.is_none() { + let peer = self.peer_edit.clone(); + if let Some(res) = self.address_check.read_or_request(|| async { + let available = PeersConfig::peer_to_addr(peer).is_some(); + Ok(available) + }) { + match res { + Ok(available) => { + self.address_available = Some(*available); + // Save peer at config. + if *available { + let peer = self.peer_edit.clone(); + match modal.id { + CUSTOM_SEED_MODAL => NodeConfig::save_custom_seed(peer), + ALLOW_PEER_MODAL => NodeConfig::allow_peer(peer), + DENY_PEER_MODAL => NodeConfig::deny_peer(peer), + PREFER_PEER_MODAL => NodeConfig::prefer_peer(peer), + &_ => {} + } + Modal::close(); + } + } + Err(_) => { + self.address_available = Some(false); + } + } + } + } - /// Draw peer creation [`Modal`] content. - fn peer_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - if self.address_available.is_none() { - let peer = self.peer_edit.clone(); - if let Some(res) = self.address_check.read_or_request(|| async { - let available = PeersConfig::peer_to_addr(peer).is_some(); - Ok(available) - }) { - match res { - Ok(available) => { - self.address_available = Some(*available); - // Save peer at config. - if *available { - let peer = self.peer_edit.clone(); - match modal.id { - CUSTOM_SEED_MODAL => NodeConfig::save_custom_seed(peer), - ALLOW_PEER_MODAL => NodeConfig::allow_peer(peer), - DENY_PEER_MODAL => NodeConfig::deny_peer(peer), - PREFER_PEER_MODAL => NodeConfig::prefer_peer(peer), - &_ => {} - } - Modal::close(); - } - } - Err(_) => { - self.address_available = Some(false); - } - } - } - } + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let label_text = match modal.id { + CUSTOM_SEED_MODAL => t!("network_settings.seed_address"), + &_ => t!("network_settings.peer_address"), + }; + ui.label(RichText::new(label_text).size(17.0).color(Colors::gray())); + ui.add_space(8.0); - ui.add_space(6.0); - ui.vertical_centered(|ui| { - let label_text = match modal.id { - CUSTOM_SEED_MODAL => t!("network_settings.seed_address"), - &_ => t!("network_settings.peer_address") - }; - ui.label(RichText::new(label_text).size(17.0).color(Colors::gray())); - ui.add_space(8.0); + // Draw peer address text edit. + let mut edit = TextEdit::new(Id::from(modal.id)).paste(); + if self.address_available.is_none() { + edit = edit.disable(); + } + edit.ui(ui, &mut self.peer_edit, cb); + if edit.enter_pressed { + self.address_available = None; + } - // Draw peer address text edit. - let mut edit = TextEdit::new(Id::from(modal.id)).paste(); - if self.address_available.is_none() { - edit = edit.disable(); - } - edit.ui(ui, &mut self.peer_edit, cb); - if edit.enter_pressed { - self.address_available = None; - } + // Show error when specified address is incorrect. + if let Some(available) = self.address_available { + if !available { + ui.add_space(10.0); + ui.label( + RichText::new(t!("network_settings.peer_address_error")) + .size(16.0) + .color(Colors::red()), + ); + } + } + ui.add_space(12.0); - // Show error when specified address is incorrect. - if let Some(available) = self.address_available { - if !available { - ui.add_space(10.0); - ui.label(RichText::new(t!("network_settings.peer_address_error")) - .size(16.0) - .color(Colors::red())); - } - } - ui.add_space(12.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + if self.address_available.is_some() { + self.address_available = None; + } + }); + }); + }); + ui.add_space(6.0); + }); + }); + } - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - if self.address_available.is_some() { - self.address_available = None; - } - }); - }); - }); - ui.add_space(6.0); - }); - }); - } + /// Draw seeding type setup content. + fn seeding_type_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(DNS_SEEDS_TITLE) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(2.0); - /// Draw seeding type setup content. - fn seeding_type_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(DNS_SEEDS_TITLE).size(16.0).color(Colors::gray())); - ui.add_space(2.0); + let default_seeding = NodeConfig::is_default_seeding_type(); + View::checkbox(ui, default_seeding, t!("network_settings.default"), || { + NodeConfig::toggle_seeding_type(); + }); + ui.add_space(8.0); - let default_seeding = NodeConfig::is_default_seeding_type(); - View::checkbox(ui, default_seeding, t!("network_settings.default"), || { - NodeConfig::toggle_seeding_type(); - }); - ui.add_space(8.0); + let peers_type = if default_seeding { + PeerType::DefaultSeed + } else { + PeerType::CustomSeed + }; + self.peer_list_ui(ui, &peers_type); + } - let peers_type = if default_seeding { - PeerType::DefaultSeed - } else { - PeerType::CustomSeed - }; - self.peer_list_ui(ui, &peers_type); - } + /// Draw ban window setup content. + fn ban_window_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.ban_window")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - /// Draw ban window setup content. - fn ban_window_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.ban_window")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); + let ban_window = NodeConfig::get_p2p_ban_window(); + View::button( + ui, + format!("{} {}", PROHIBIT_INSET, &ban_window), + Colors::white_or_black(false), + || { + // Setup values for modal. + self.ban_window_edit = ban_window; + // Show ban window period setup modal. + Modal::new(BAN_WINDOW_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(6.0); + ui.label( + RichText::new(t!("network_settings.ban_window_desc")) + .size(16.0) + .color(Colors::inactive_text()), + ); + ui.add_space(2.0); + } - let ban_window = NodeConfig::get_p2p_ban_window(); - View::button(ui, - format!("{} {}", PROHIBIT_INSET, &ban_window), - Colors::white_or_black(false), || { - // Setup values for modal. - self.ban_window_edit = ban_window; - // Show ban window period setup modal. - Modal::new(BAN_WINDOW_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(6.0); - ui.label(RichText::new(t!("network_settings.ban_window_desc")) - .size(16.0) - .color(Colors::inactive_text()) - ); - ui.add_space(2.0); - } + /// Draw ban window [`Modal`] content. + fn ban_window_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut P2PSetup| { + if let Ok(ban_window) = c.ban_window_edit.parse::() { + NodeConfig::save_p2p_ban_window(ban_window); + Modal::close(); + } + }; - /// Draw ban window [`Modal`] content. - fn ban_window_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut P2PSetup| { - if let Ok(ban_window) = c.ban_window_edit.parse::() { - NodeConfig::save_p2p_ban_window(ban_window); - Modal::close(); - } - }; + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.ban_window")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.ban_window")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + // Draw ban window text edit. + let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + edit.ui(ui, &mut self.ban_window_edit, cb); + if edit.enter_pressed { + on_save(self); + } - // Draw ban window text edit. - let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - edit.ui(ui, &mut self.ban_window_edit, cb); - if edit.enter_pressed { - on_save(self); - } + // Show error when specified value is not valid or reminder to restart enabled node. + if self.ban_window_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::red()), + ); + } else { + NetworkSettings::node_restart_required_ui(ui); + } + ui.add_space(12.0); + }); - // Show error when specified value is not valid or reminder to restart enabled node. - if self.ban_window_edit.parse::().is_err() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.not_valid_value")) - .size(17.0) - .color(Colors::red())); - } else { - NetworkSettings::node_restart_required_ui(ui); - } - ui.add_space(12.0); - }); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + } - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - } + /// Draw maximum number of inbound peers setup content. + fn max_inbound_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.max_inbound_count")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - /// Draw maximum number of inbound peers setup content. - fn max_inbound_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.max_inbound_count")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); + let max_inbound = NodeConfig::get_max_inbound_peers(); + View::button( + ui, + format!("{} {}", ARROW_FAT_LINES_DOWN, &max_inbound), + Colors::white_or_black(false), + || { + // Setup values for modal. + self.max_inbound_count = max_inbound; + // Show maximum number of inbound peers setup modal. + Modal::new(MAX_INBOUND_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(6.0); + } - let max_inbound = NodeConfig::get_max_inbound_peers(); - View::button(ui, - format!("{} {}", ARROW_FAT_LINES_DOWN, &max_inbound), - Colors::white_or_black(false), || { - // Setup values for modal. - self.max_inbound_count = max_inbound; - // Show maximum number of inbound peers setup modal. - Modal::new(MAX_INBOUND_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(6.0); - } + /// Draw maximum number of inbound peers [`Modal`] content. + fn max_inbound_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut P2PSetup| { + if let Ok(max_inbound) = c.max_inbound_count.parse::() { + NodeConfig::save_max_inbound_peers(max_inbound); + Modal::close(); + } + }; - /// Draw maximum number of inbound peers [`Modal`] content. - fn max_inbound_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut P2PSetup| { - if let Ok(max_inbound) = c.max_inbound_count.parse::() { - NodeConfig::save_max_inbound_peers(max_inbound); - Modal::close(); - } - }; + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.max_inbound_count")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.max_inbound_count")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + // Draw maximum number of inbound peers text edit. + let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + edit.ui(ui, &mut self.max_inbound_count, cb); + if edit.enter_pressed { + on_save(self); + } - // Draw maximum number of inbound peers text edit. - let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - edit.ui(ui, &mut self.max_inbound_count, cb); - if edit.enter_pressed { - on_save(self); - } + // Show error when specified value is not valid or reminder to restart enabled node. + if self.max_inbound_count.parse::().is_err() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::red()), + ); + } else { + NetworkSettings::node_restart_required_ui(ui); + } + ui.add_space(12.0); + }); - // Show error when specified value is not valid or reminder to restart enabled node. - if self.max_inbound_count.parse::().is_err() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.not_valid_value")) - .size(17.0) - .color(Colors::red())); - } else { - NetworkSettings::node_restart_required_ui(ui); - } - ui.add_space(12.0); - }); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + } - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - } + /// Draw maximum number of outbound peers setup content. + fn max_outbound_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.max_outbound_count")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); + let max_outbound = NodeConfig::get_max_outbound_peers(); + View::button( + ui, + format!("{} {}", ARROW_FAT_LINES_UP, &max_outbound), + Colors::white_or_black(false), + || { + // Setup values for modal. + self.max_outbound_count = max_outbound; + // Show maximum number of outbound peers setup modal. + Modal::new(MAX_OUTBOUND_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(6.0); + } - /// Draw maximum number of outbound peers setup content. - fn max_outbound_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.max_outbound_count")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); - let max_outbound = NodeConfig::get_max_outbound_peers(); - View::button(ui, - format!("{} {}", ARROW_FAT_LINES_UP, &max_outbound), - Colors::white_or_black(false), || { - // Setup values for modal. - self.max_outbound_count = max_outbound; - // Show maximum number of outbound peers setup modal. - Modal::new(MAX_OUTBOUND_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(6.0); - } + /// Draw maximum number of outbound peers [`Modal`] content. + fn max_outbound_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut P2PSetup| { + if let Ok(max_outbound) = c.max_outbound_count.parse::() { + NodeConfig::save_max_outbound_peers(max_outbound); + Modal::close(); + } + }; - /// Draw maximum number of outbound peers [`Modal`] content. - fn max_outbound_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut P2PSetup| { - if let Ok(max_outbound) = c.max_outbound_count.parse::() { - NodeConfig::save_max_outbound_peers(max_outbound); - Modal::close(); - } - }; + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.max_outbound_count")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.max_outbound_count")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + // Draw maximum number of outbound peers text edit. + let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + edit.ui(ui, &mut self.max_outbound_count, cb); + if edit.enter_pressed { + on_save(self); + } - // Draw maximum number of outbound peers text edit. - let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - edit.ui(ui, &mut self.max_outbound_count, cb); - if edit.enter_pressed { - on_save(self); - } + // Show error when specified value is not valid or reminder to restart enabled node. + if self.max_outbound_count.parse::().is_err() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::red()), + ); + } else { + NetworkSettings::node_restart_required_ui(ui); + } + ui.add_space(12.0); + }); - // Show error when specified value is not valid or reminder to restart enabled node. - if self.max_outbound_count.parse::().is_err() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.not_valid_value")) - .size(17.0) - .color(Colors::red())); - } else { - NetworkSettings::node_restart_required_ui(ui); - } - ui.add_space(12.0); - }); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + } } /// Draw peer list item. -fn peer_item_ui(ui: &mut egui::Ui, - peer_addr: &String, - peer_type: &PeerType, - index: usize, - len: usize,) { - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(42.0); +fn peer_item_ui( + ui: &mut egui::Ui, + peer_addr: &String, + peer_type: &PeerType, + index: usize, + len: usize, +) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(42.0); - // Draw round background. - let item_rounding = View::item_rounding(index, len, false); - ui.painter().rect(rect, - item_rounding, - Colors::fill(), - View::item_stroke(), - StrokeKind::Outside); + // Draw round background. + let item_rounding = View::item_rounding(index, len, false); + ui.painter().rect( + rect, + item_rounding, + Colors::fill(), + View::item_stroke(), + StrokeKind::Outside, + ); - ui.vertical(|ui| { - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - // Draw delete button for non-default seed peers. - if peer_type != &PeerType::DefaultSeed { - let r = View::item_rounding(index, len, true); - View::item_button(ui, r, TRASH, Some(Colors::inactive_text()), || { - match peer_type { - PeerType::CustomSeed => { - NodeConfig::remove_custom_seed(peer_addr); - } - PeerType::Allowed => { - NodeConfig::remove_allowed_peer(peer_addr); - } - PeerType::Denied => { - NodeConfig::remove_denied_peer(peer_addr); - } - PeerType::Preferred => { - NodeConfig::remove_preferred_peer(peer_addr); - } - PeerType::DefaultSeed => {} - } - }); - } + ui.vertical(|ui| { + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + // Draw delete button for non-default seed peers. + if peer_type != &PeerType::DefaultSeed { + let r = View::item_rounding(index, len, true); + View::item_button( + ui, + r, + TRASH, + Some(Colors::inactive_text()), + || match peer_type { + PeerType::CustomSeed => { + NodeConfig::remove_custom_seed(peer_addr); + } + PeerType::Allowed => { + NodeConfig::remove_allowed_peer(peer_addr); + } + PeerType::Denied => { + NodeConfig::remove_denied_peer(peer_addr); + } + PeerType::Preferred => { + NodeConfig::remove_preferred_peer(peer_addr); + } + PeerType::DefaultSeed => {} + }, + ); + } - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(6.0); - // Draw peer address. - let peer_text = format!("{} {}", GLOBE_SIMPLE, &peer_addr); - ui.label(RichText::new(peer_text) - .color(Colors::text_button()) - .size(16.0)); - }); - }); - }); -} \ No newline at end of file + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(6.0); + // Draw peer address. + let peer_text = format!("{} {}", GLOBE_SIMPLE, &peer_addr); + ui.label( + RichText::new(peer_text) + .color(Colors::text_button()) + .size(16.0), + ); + }); + }); + }); +} diff --git a/src/gui/views/network/setup/pool.rs b/src/gui/views/network/setup/pool.rs index c791279b..f44b88f2 100644 --- a/src/gui/views/network/setup/pool.rs +++ b/src/gui/views/network/setup/pool.rs @@ -15,25 +15,27 @@ use egui::{Id, RichText}; use crate::gui::Colors; -use crate::gui::icons::{BEZIER_CURVE, BOUNDING_BOX, CHART_SCATTER, CIRCLES_THREE, CLOCK_COUNTDOWN, HAND_COINS}; +use crate::gui::icons::{ + BEZIER_CURVE, BOUNDING_BOX, CHART_SCATTER, CIRCLES_THREE, CLOCK_COUNTDOWN, HAND_COINS, +}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, TextEdit, View}; use crate::gui::views::network::settings::NetworkSettings; use crate::gui::views::types::{ContentContainer, ModalPosition}; +use crate::gui::views::{Modal, TextEdit, View}; use crate::node::NodeConfig; /// Memory pool setup section content. pub struct PoolSetup { - /// Base fee value that's accepted into the pool. - fee_base_edit: String, - /// Reorg cache retention period value in minutes. - reorg_period_edit: String, - /// Maximum number of transactions allowed in the pool. - pool_size_edit: String, - /// Maximum number of transactions allowed in the stempool. - stempool_size_edit: String, - /// Maximum total weight of transactions to build a block. - max_weight_edit: String, + /// Base fee value that's accepted into the pool. + fee_base_edit: String, + /// Reorg cache retention period value in minutes. + reorg_period_edit: String, + /// Maximum number of transactions allowed in the pool. + pool_size_edit: String, + /// Maximum number of transactions allowed in the stempool. + stempool_size_edit: String, + /// Maximum total weight of transactions to build a block. + max_weight_edit: String, } /// Identifier for base fee value [`Modal`]. @@ -48,475 +50,544 @@ const STEMPOOL_SIZE_MODAL: &'static str = "stempool_size"; const MAX_WEIGHT_MODAL: &'static str = "max_weight"; impl Default for PoolSetup { - fn default() -> Self { - Self { - fee_base_edit: NodeConfig::get_base_fee(), - reorg_period_edit: NodeConfig::get_reorg_cache_period(), - pool_size_edit: NodeConfig::get_max_pool_size(), - stempool_size_edit: NodeConfig::get_max_stempool_size(), - max_weight_edit: NodeConfig::get_mineable_max_weight(), - } - } + fn default() -> Self { + Self { + fee_base_edit: NodeConfig::get_base_fee(), + reorg_period_edit: NodeConfig::get_reorg_cache_period(), + pool_size_edit: NodeConfig::get_max_pool_size(), + stempool_size_edit: NodeConfig::get_max_stempool_size(), + max_weight_edit: NodeConfig::get_mineable_max_weight(), + } + } } impl ContentContainer for PoolSetup { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - FEE_BASE_MODAL, - REORG_PERIOD_MODAL, - POOL_SIZE_MODAL, - STEMPOOL_SIZE_MODAL, - MAX_WEIGHT_MODAL - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![ + FEE_BASE_MODAL, + REORG_PERIOD_MODAL, + POOL_SIZE_MODAL, + STEMPOOL_SIZE_MODAL, + MAX_WEIGHT_MODAL, + ] + } - fn modal_ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - match modal.id { - FEE_BASE_MODAL => self.fee_base_modal(ui, modal, cb), - REORG_PERIOD_MODAL => self.reorg_period_modal(ui, modal, cb), - POOL_SIZE_MODAL => self.pool_size_modal(ui, modal, cb), - STEMPOOL_SIZE_MODAL => self.stem_size_modal(ui, modal, cb), - MAX_WEIGHT_MODAL => self.max_weight_modal(ui, modal, cb), - _ => {} - } - } + fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + match modal.id { + FEE_BASE_MODAL => self.fee_base_modal(ui, modal, cb), + REORG_PERIOD_MODAL => self.reorg_period_modal(ui, modal, cb), + POOL_SIZE_MODAL => self.pool_size_modal(ui, modal, cb), + STEMPOOL_SIZE_MODAL => self.stem_size_modal(ui, modal, cb), + MAX_WEIGHT_MODAL => self.max_weight_modal(ui, modal, cb), + _ => {} + } + } - fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { - View::sub_title(ui, format!("{} {}", CHART_SCATTER, t!("network_settings.tx_pool"))); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(6.0); + fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { + View::sub_title( + ui, + format!("{} {}", CHART_SCATTER, t!("network_settings.tx_pool")), + ); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(6.0); - ui.vertical_centered(|ui| { - // Show base fee setup. - self.fee_base_ui(ui); + ui.vertical_centered(|ui| { + // Show base fee setup. + self.fee_base_ui(ui); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Show reorg cache retention period setup. - self.reorg_period_ui(ui); + // Show reorg cache retention period setup. + self.reorg_period_ui(ui); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Show pool size setup. - self.pool_size_ui(ui); + // Show pool size setup. + self.pool_size_ui(ui); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Show stem pool size setup. - self.stem_size_ui(ui); + // Show stem pool size setup. + self.stem_size_ui(ui); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Show max weight of transactions setup. - self.max_weight_ui(ui); - }); - } + // Show max weight of transactions setup. + self.max_weight_ui(ui); + }); + } } impl PoolSetup { - /// Draw fee base setup content. - fn fee_base_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.pool_fee")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); + /// Draw fee base setup content. + fn fee_base_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.pool_fee")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - let fee = NodeConfig::get_base_fee(); - View::button(ui, format!("{} {}", HAND_COINS, &fee), Colors::white_or_black(false), || { - // Setup values for modal. - self.fee_base_edit = fee; - // Show fee setup modal. - Modal::new(FEE_BASE_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(6.0); - } + let fee = NodeConfig::get_base_fee(); + View::button( + ui, + format!("{} {}", HAND_COINS, &fee), + Colors::white_or_black(false), + || { + // Setup values for modal. + self.fee_base_edit = fee; + // Show fee setup modal. + Modal::new(FEE_BASE_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(6.0); + } - /// Draw fee base [`Modal`] content. - fn fee_base_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut PoolSetup| { - if let Ok(fee) = c.fee_base_edit.parse::() { - NodeConfig::save_base_fee(fee); - Modal::close(); - } - }; + /// Draw fee base [`Modal`] content. + fn fee_base_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut PoolSetup| { + if let Ok(fee) = c.fee_base_edit.parse::() { + NodeConfig::save_base_fee(fee); + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.pool_fee")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.pool_fee")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw fee base text edit. - let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - edit.ui(ui, &mut self.fee_base_edit, cb); - if edit.enter_pressed { - on_save(self); - } + // Draw fee base text edit. + let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + edit.ui(ui, &mut self.fee_base_edit, cb); + if edit.enter_pressed { + on_save(self); + } - // Show error when specified value is not valid or reminder to restart enabled node. - if self.fee_base_edit.parse::().is_err() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.not_valid_value")) - .size(17.0) - .color(Colors::red())); - } else { - NetworkSettings::node_restart_required_ui(ui); - } - ui.add_space(12.0); + // Show error when specified value is not valid or reminder to restart enabled node. + if self.fee_base_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::red()), + ); + } else { + NetworkSettings::node_restart_required_ui(ui); + } + ui.add_space(12.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } - /// Draw reorg cache retention period setup content. - fn reorg_period_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.reorg_period")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); - let period = NodeConfig::get_reorg_cache_period(); - View::button(ui, - format!("{} {}", CLOCK_COUNTDOWN, &period), - Colors::white_or_black(false), || { - // Setup values for modal. - self.reorg_period_edit = period; - // Show reorg period setup modal. - Modal::new(REORG_PERIOD_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(6.0); - } + /// Draw reorg cache retention period setup content. + fn reorg_period_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.reorg_period")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); + let period = NodeConfig::get_reorg_cache_period(); + View::button( + ui, + format!("{} {}", CLOCK_COUNTDOWN, &period), + Colors::white_or_black(false), + || { + // Setup values for modal. + self.reorg_period_edit = period; + // Show reorg period setup modal. + Modal::new(REORG_PERIOD_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(6.0); + } - /// Draw reorg cache retention period [`Modal`] content. - fn reorg_period_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut PoolSetup| { - if let Ok(period) = c.reorg_period_edit.parse::() { - NodeConfig::save_reorg_cache_period(period); - Modal::close(); - } - }; + /// Draw reorg cache retention period [`Modal`] content. + fn reorg_period_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut PoolSetup| { + if let Ok(period) = c.reorg_period_edit.parse::() { + NodeConfig::save_reorg_cache_period(period); + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.reorg_period")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.reorg_period")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw reorg period text edit. - let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - edit.ui(ui, &mut self.reorg_period_edit, cb); - if edit.enter_pressed { - on_save(self); - } + // Draw reorg period text edit. + let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + edit.ui(ui, &mut self.reorg_period_edit, cb); + if edit.enter_pressed { + on_save(self); + } - // Show error when specified value is not valid or reminder to restart enabled node. - if self.reorg_period_edit.parse::().is_err() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.not_valid_value")) - .size(17.0) - .color(Colors::red())); - } else { - NetworkSettings::node_restart_required_ui(ui); - } - ui.add_space(12.0); + // Show error when specified value is not valid or reminder to restart enabled node. + if self.reorg_period_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::red()), + ); + } else { + NetworkSettings::node_restart_required_ui(ui); + } + ui.add_space(12.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } - /// Draw maximum number of transactions in the pool setup content. - fn pool_size_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.max_tx_pool")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); + /// Draw maximum number of transactions in the pool setup content. + fn pool_size_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.max_tx_pool")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - let size = NodeConfig::get_max_pool_size(); - View::button(ui, format!("{} {}", CIRCLES_THREE, size), Colors::white_or_black(false), || { - // Setup values for modal. - self.pool_size_edit = size; - // Show pool size setup modal. - Modal::new(POOL_SIZE_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(6.0); - } + let size = NodeConfig::get_max_pool_size(); + View::button( + ui, + format!("{} {}", CIRCLES_THREE, size), + Colors::white_or_black(false), + || { + // Setup values for modal. + self.pool_size_edit = size; + // Show pool size setup modal. + Modal::new(POOL_SIZE_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(6.0); + } - /// Draw maximum number of transactions in the pool [`Modal`] content. - fn pool_size_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut PoolSetup| { - if let Ok(size) = c.pool_size_edit.parse::() { - NodeConfig::save_max_pool_size(size); - Modal::close(); - } - }; + /// Draw maximum number of transactions in the pool [`Modal`] content. + fn pool_size_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut PoolSetup| { + if let Ok(size) = c.pool_size_edit.parse::() { + NodeConfig::save_max_pool_size(size); + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.max_tx_pool")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.max_tx_pool")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw pool size text edit. - let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - edit.ui(ui, &mut self.pool_size_edit, cb); - if edit.enter_pressed { - on_save(self); - } + // Draw pool size text edit. + let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + edit.ui(ui, &mut self.pool_size_edit, cb); + if edit.enter_pressed { + on_save(self); + } - // Show error when specified value is not valid or reminder to restart enabled node. - if self.pool_size_edit.parse::().is_err() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.not_valid_value")) - .size(17.0) - .color(Colors::red())); - } else { - NetworkSettings::node_restart_required_ui(ui); - } - ui.add_space(12.0); + // Show error when specified value is not valid or reminder to restart enabled node. + if self.pool_size_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::red()), + ); + } else { + NetworkSettings::node_restart_required_ui(ui); + } + ui.add_space(12.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } - /// Draw maximum number of transactions in the stempool setup content. - fn stem_size_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.max_tx_stempool")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); + /// Draw maximum number of transactions in the stempool setup content. + fn stem_size_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.max_tx_stempool")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - let size = NodeConfig::get_max_stempool_size(); - View::button(ui, - format!("{} {}", BEZIER_CURVE, &size), - Colors::white_or_black(false), || { - // Setup values for modal. - self.stempool_size_edit = size; - // Show stempool size setup modal. - Modal::new(STEMPOOL_SIZE_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(6.0); - } + let size = NodeConfig::get_max_stempool_size(); + View::button( + ui, + format!("{} {}", BEZIER_CURVE, &size), + Colors::white_or_black(false), + || { + // Setup values for modal. + self.stempool_size_edit = size; + // Show stempool size setup modal. + Modal::new(STEMPOOL_SIZE_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(6.0); + } - /// Draw maximum number of transactions in the stempool [`Modal`] content. - fn stem_size_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut PoolSetup| { - if let Ok(size) = c.stempool_size_edit.parse::() { - NodeConfig::save_max_stempool_size(size); - Modal::close(); - } - }; + /// Draw maximum number of transactions in the stempool [`Modal`] content. + fn stem_size_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut PoolSetup| { + if let Ok(size) = c.stempool_size_edit.parse::() { + NodeConfig::save_max_stempool_size(size); + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.max_tx_stempool")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.max_tx_stempool")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw stempool size text edit. - let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - edit.ui(ui, &mut self.stempool_size_edit, cb); - if edit.enter_pressed { - on_save(self); - } + // Draw stempool size text edit. + let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + edit.ui(ui, &mut self.stempool_size_edit, cb); + if edit.enter_pressed { + on_save(self); + } - // Show error when specified value is not valid or reminder to restart enabled node. - if self.stempool_size_edit.parse::().is_err() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.not_valid_value")) - .size(17.0) - .color(Colors::red())); - } else { - NetworkSettings::node_restart_required_ui(ui); - } - ui.add_space(12.0); + // Show error when specified value is not valid or reminder to restart enabled node. + if self.stempool_size_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::red()), + ); + } else { + NetworkSettings::node_restart_required_ui(ui); + } + ui.add_space(12.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } - /// Draw maximum total weight of transactions setup content. - fn max_weight_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.max_tx_weight")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); + /// Draw maximum total weight of transactions setup content. + fn max_weight_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.max_tx_weight")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - let weight = NodeConfig::get_mineable_max_weight(); - View::button(ui, - format!("{} {}", BOUNDING_BOX, &weight), - Colors::white_or_black(false), || { - // Setup values for modal. - self.max_weight_edit = weight; - // Show total tx weight setup modal. - Modal::new(MAX_WEIGHT_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(6.0); - } + let weight = NodeConfig::get_mineable_max_weight(); + View::button( + ui, + format!("{} {}", BOUNDING_BOX, &weight), + Colors::white_or_black(false), + || { + // Setup values for modal. + self.max_weight_edit = weight; + // Show total tx weight setup modal. + Modal::new(MAX_WEIGHT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(6.0); + } - /// Draw maximum total weight of transactions [`Modal`] content. - fn max_weight_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut PoolSetup| { - if let Ok(weight) = c.max_weight_edit.parse::() { - NodeConfig::save_mineable_max_weight(weight); - Modal::close(); - } - }; + /// Draw maximum total weight of transactions [`Modal`] content. + fn max_weight_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut PoolSetup| { + if let Ok(weight) = c.max_weight_edit.parse::() { + NodeConfig::save_mineable_max_weight(weight); + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.max_tx_weight")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.max_tx_weight")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw tx weight text edit. - let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - edit.ui(ui, &mut self.max_weight_edit, cb); - if edit.enter_pressed { - on_save(self); - } + // Draw tx weight text edit. + let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + edit.ui(ui, &mut self.max_weight_edit, cb); + if edit.enter_pressed { + on_save(self); + } - // Show error when specified value is not valid or reminder to restart enabled node. - if self.max_weight_edit.parse::().is_err() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.not_valid_value")) - .size(17.0) - .color(Colors::red())); - } else { - NetworkSettings::node_restart_required_ui(ui); - } - ui.add_space(12.0); + // Show error when specified value is not valid or reminder to restart enabled node. + if self.max_weight_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::red()), + ); + } else { + NetworkSettings::node_restart_required_ui(ui); + } + ui.add_space(12.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - }); - } -} \ No newline at end of file + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } +} diff --git a/src/gui/views/network/setup/stratum.rs b/src/gui/views/network/setup/stratum.rs index b63f2115..4933ef92 100644 --- a/src/gui/views/network/setup/stratum.rs +++ b/src/gui/views/network/setup/stratum.rs @@ -18,39 +18,39 @@ use grin_chain::SyncStatus; use crate::gui::Colors; use crate::gui::icons::{BARBELL, HARD_DRIVES, PLUG, POWER, TIMER}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, TextEdit, View}; use crate::gui::views::network::settings::NetworkSettings; use crate::gui::views::types::{ContentContainer, ModalPosition}; use crate::gui::views::wallets::modals::WalletListModal; +use crate::gui::views::{Modal, TextEdit, View}; use crate::node::{Node, NodeConfig}; use crate::wallet::{WalletConfig, WalletList}; /// Stratum server setup section content. pub struct StratumSetup { - /// Wallet list to select for mining rewards. - wallets: WalletList, - /// Wallets [`Modal`] content. - wallets_modal: WalletListModal, + /// Wallet list to select for mining rewards. + wallets: WalletList, + /// Wallets [`Modal`] content. + wallets_modal: WalletListModal, - /// IP Addresses available at system. - available_ips: Vec, + /// IP Addresses available at system. + available_ips: Vec, - /// Stratum port value. - stratum_port_edit: String, - /// Flag to check if stratum port is available. - stratum_port_available_edit: bool, + /// Stratum port value. + stratum_port_edit: String, + /// Flag to check if stratum port is available. + stratum_port_available_edit: bool, - /// Flag to check if stratum port from saved config value is available. - is_port_available: bool, + /// Flag to check if stratum port from saved config value is available. + is_port_available: bool, - /// Wallet name to receive rewards. - pub wallet_name: Option, + /// Wallet name to receive rewards. + pub wallet_name: Option, - /// Attempt time value in seconds to mine on a particular header. - attempt_time_edit: String, + /// Attempt time value in seconds to mine on a particular header. + attempt_time_edit: String, - /// Minimum share difficulty value to request from miners. - min_share_diff_edit: String, + /// Minimum share difficulty value to request from miners. + min_share_diff_edit: String, } /// Identifier for wallet selection [`Modal`]. @@ -63,460 +63,514 @@ const ATTEMPT_TIME_MODAL: &'static str = "stratum_attempt_time"; const MIN_SHARE_DIFF_MODAL: &'static str = "stratum_min_share_diff"; impl Default for StratumSetup { - fn default() -> Self { - let (ip, port) = NodeConfig::get_stratum_address(); - let is_port_available = NodeConfig::is_stratum_port_available(&ip, &port); + fn default() -> Self { + let (ip, port) = NodeConfig::get_stratum_address(); + let is_port_available = NodeConfig::is_stratum_port_available(&ip, &port); - // Setup mining rewards wallet name and identifier. - let mut wallet_id = NodeConfig::get_stratum_wallet_id(); - let wallet_name = if let Some(id) = wallet_id { - WalletConfig::read_name_by_id(id) - } else { - None - }; - if wallet_name.is_none() { - wallet_id = None; - } + // Setup mining rewards wallet name and identifier. + let mut wallet_id = NodeConfig::get_stratum_wallet_id(); + let wallet_name = if let Some(id) = wallet_id { + WalletConfig::read_name_by_id(id) + } else { + None + }; + if wallet_name.is_none() { + wallet_id = None; + } - Self { - wallets: WalletList::default(), - wallets_modal: WalletListModal::new(wallet_id, None, false), - available_ips: NodeConfig::get_ip_addrs(), - stratum_port_edit: port, - stratum_port_available_edit: is_port_available, - is_port_available, - wallet_name, - attempt_time_edit: NodeConfig::get_stratum_attempt_time(), - min_share_diff_edit: NodeConfig::get_stratum_min_share_diff(), - } - } + Self { + wallets: WalletList::default(), + wallets_modal: WalletListModal::new(wallet_id, None, false), + available_ips: NodeConfig::get_ip_addrs(), + stratum_port_edit: port, + stratum_port_available_edit: is_port_available, + is_port_available, + wallet_name, + attempt_time_edit: NodeConfig::get_stratum_attempt_time(), + min_share_diff_edit: NodeConfig::get_stratum_min_share_diff(), + } + } } impl ContentContainer for StratumSetup { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - WALLET_SELECTION_MODAL, - STRATUM_PORT_MODAL, - ATTEMPT_TIME_MODAL, - MIN_SHARE_DIFF_MODAL - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![ + WALLET_SELECTION_MODAL, + STRATUM_PORT_MODAL, + ATTEMPT_TIME_MODAL, + MIN_SHARE_DIFF_MODAL, + ] + } - fn modal_ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - match modal.id { - WALLET_SELECTION_MODAL => { - self.wallets_modal.ui(ui, &mut self.wallets, |wallet, _| { - let id = wallet.get_config().id; - NodeConfig::save_stratum_wallet_id(id); - self.wallet_name = WalletConfig::read_name_by_id(id); - }) - }, - STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb), - ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb), - MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb), - _ => {} - } - } + fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + match modal.id { + WALLET_SELECTION_MODAL => self.wallets_modal.ui(ui, &mut self.wallets, |wallet, _| { + let id = wallet.get_config().id; + NodeConfig::save_stratum_wallet_id(id); + self.wallet_name = WalletConfig::read_name_by_id(id); + }), + STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb), + ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb), + MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb), + _ => {} + } + } - fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { - View::sub_title(ui, format!("{} {}", HARD_DRIVES, t!("network_mining.server"))); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(6.0); + fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { + View::sub_title( + ui, + format!("{} {}", HARD_DRIVES, t!("network_mining.server")), + ); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(6.0); - ui.vertical_centered(|ui| { - // Show loading indicator or controls to start/stop stratum server. - if Node::get_sync_status().unwrap_or(SyncStatus::Initial) == SyncStatus::NoSync && - self.is_port_available && self.wallet_name.is_some() { - if Node::is_stratum_starting() || Node::is_stratum_stopping() { - ui.vertical_centered(|ui| { - ui.add_space(8.0); - View::small_loading_spinner(ui); - ui.add_space(8.0); - }); - } else if Node::get_stratum_stats().is_running { - ui.add_space(6.0); - let disable_text = format!("{} {}", POWER, t!("network_settings.disable")); - View::action_button(ui, disable_text, || { - Node::stop_stratum(); - let (ip, port) = NodeConfig::get_stratum_address(); - self.is_port_available = NodeConfig::is_stratum_port_available(&ip, &port); - }); - ui.add_space(6.0); - } else { - ui.add_space(6.0); - let enable_text = format!("{} {}", POWER, t!("network_settings.enable")); - View::action_button(ui, enable_text, || { - Node::start_stratum(); - }); - ui.add_space(6.0); - } - } + ui.vertical_centered(|ui| { + // Show loading indicator or controls to start/stop stratum server. + if Node::get_sync_status().unwrap_or(SyncStatus::Initial) == SyncStatus::NoSync + && self.is_port_available + && self.wallet_name.is_some() + { + if Node::is_stratum_starting() || Node::is_stratum_stopping() { + ui.vertical_centered(|ui| { + ui.add_space(8.0); + View::small_loading_spinner(ui); + ui.add_space(8.0); + }); + } else if Node::get_stratum_stats().is_running { + ui.add_space(6.0); + let disable_text = format!("{} {}", POWER, t!("network_settings.disable")); + View::action_button(ui, disable_text, || { + Node::stop_stratum(); + let (ip, port) = NodeConfig::get_stratum_address(); + self.is_port_available = NodeConfig::is_stratum_port_available(&ip, &port); + }); + ui.add_space(6.0); + } else { + ui.add_space(6.0); + let enable_text = format!("{} {}", POWER, t!("network_settings.enable")); + View::action_button(ui, enable_text, || { + Node::start_stratum(); + }); + ui.add_space(6.0); + } + } - // Show stratum server autorun checkbox. - let stratum_enabled = NodeConfig::is_stratum_autorun_enabled(); - View::checkbox(ui, stratum_enabled, t!("network.autorun"), || { - NodeConfig::toggle_stratum_autorun(); - }); + // Show stratum server autorun checkbox. + let stratum_enabled = NodeConfig::is_stratum_autorun_enabled(); + View::checkbox(ui, stratum_enabled, t!("network.autorun"), || { + NodeConfig::toggle_stratum_autorun(); + }); - // Show reminder to restart running server. - if Node::get_stratum_stats().is_running { - ui.add_space(2.0); - ui.label(RichText::new(t!("network_mining.restart_server_required")) - .size(16.0) - .color(Colors::inactive_text()) - ); - } - ui.add_space(8.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); + // Show reminder to restart running server. + if Node::get_stratum_stats().is_running { + ui.add_space(2.0); + ui.label( + RichText::new(t!("network_mining.restart_server_required")) + .size(16.0) + .color(Colors::inactive_text()), + ); + } + ui.add_space(8.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); - // Show wallet name. - ui.label(RichText::new(self.wallet_name.as_ref().unwrap_or(&"-".to_string())) - .size(16.0) - .color(Colors::white_or_black(true))); - ui.add_space(8.0); + // Show wallet name. + ui.label( + RichText::new(self.wallet_name.as_ref().unwrap_or(&"-".to_string())) + .size(16.0) + .color(Colors::white_or_black(true)), + ); + ui.add_space(8.0); - // Show button to select wallet. - View::button(ui, - t!("network_settings.choose_wallet"), - Colors::white_or_black(false), || { - self.show_wallets_modal(); - }); - ui.add_space(12.0); + // Show button to select wallet. + View::button( + ui, + t!("network_settings.choose_wallet"), + Colors::white_or_black(false), + || { + self.show_wallets_modal(); + }, + ); + ui.add_space(12.0); - if self.wallet_name.is_some() { - ui.label(RichText::new(t!("network_settings.stratum_wallet_warning")) - .size(16.0) - .color(Colors::inactive_text()) - ); - ui.add_space(12.0); - } - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); - }); + if self.wallet_name.is_some() { + ui.label( + RichText::new(t!("network_settings.stratum_wallet_warning")) + .size(16.0) + .color(Colors::inactive_text()), + ); + ui.add_space(12.0); + } + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); + }); - // Show message when IP addresses are not available on the system. - if self.available_ips.is_empty() { - NetworkSettings::no_ip_address_ui(ui); - return; - } + // Show message when IP addresses are not available on the system. + if self.available_ips.is_empty() { + NetworkSettings::no_ip_address_ui(ui); + return; + } - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.stratum_ip")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); - // Show stratum IP addresses to select. - let (ip, port) = NodeConfig::get_stratum_address(); - NetworkSettings::ip_addrs_ui(ui, &ip, &self.available_ips, |selected_ip| { - NodeConfig::save_stratum_address(selected_ip, &port); - self.is_port_available = NodeConfig::is_stratum_port_available(selected_ip, &port); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.stratum_ip")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); + // Show stratum IP addresses to select. + let (ip, port) = NodeConfig::get_stratum_address(); + NetworkSettings::ip_addrs_ui(ui, &ip, &self.available_ips, |selected_ip| { + NodeConfig::save_stratum_address(selected_ip, &port); + self.is_port_available = NodeConfig::is_stratum_port_available(selected_ip, &port); + }); + // Show stratum port setup. + self.port_setup_ui(ui); - }); - // Show stratum port setup. - self.port_setup_ui(ui); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + // Show attempt time setup. + self.attempt_time_ui(ui); - // Show attempt time setup. - self.attempt_time_ui(ui); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); - - // Show minimum acceptable share difficulty setup. - self.min_diff_ui(ui); - }); - } + // Show minimum acceptable share difficulty setup. + self.min_diff_ui(ui); + }); + } } impl StratumSetup { - /// Show wallet selection [`Modal`]. - fn show_wallets_modal(&mut self) { - self.wallets_modal = WalletListModal::new(NodeConfig::get_stratum_wallet_id(), None, false); - // Show modal. - Modal::new(WALLET_SELECTION_MODAL) - .position(ModalPosition::Center) - .title(t!("network_settings.choose_wallet")) - .show(); - } + /// Show wallet selection [`Modal`]. + fn show_wallets_modal(&mut self) { + self.wallets_modal = WalletListModal::new(NodeConfig::get_stratum_wallet_id(), None, false); + // Show modal. + Modal::new(WALLET_SELECTION_MODAL) + .position(ModalPosition::Center) + .title(t!("network_settings.choose_wallet")) + .show(); + } - /// Draw stratum port value setup content. - fn port_setup_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.stratum_port")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); + /// Draw stratum port value setup content. + fn port_setup_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.stratum_port")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - let (_, port) = NodeConfig::get_stratum_address(); - View::button(ui, format!("{} {}", PLUG, &port), Colors::white_or_black(false), || { - // Setup values for modal. - self.stratum_port_edit = port; - self.stratum_port_available_edit = self.is_port_available; - // Show stratum port modal. - Modal::new(STRATUM_PORT_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(12.0); + let (_, port) = NodeConfig::get_stratum_address(); + View::button( + ui, + format!("{} {}", PLUG, &port), + Colors::white_or_black(false), + || { + // Setup values for modal. + self.stratum_port_edit = port; + self.stratum_port_available_edit = self.is_port_available; + // Show stratum port modal. + Modal::new(STRATUM_PORT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(12.0); - // Show error when stratum server port is unavailable. - if !self.is_port_available { - ui.add_space(6.0); - ui.label(RichText::new(t!("network_settings.port_unavailable")) - .size(16.0) - .color(Colors::red())); - ui.add_space(12.0); - } - } + // Show error when stratum server port is unavailable. + if !self.is_port_available { + ui.add_space(6.0); + ui.label( + RichText::new(t!("network_settings.port_unavailable")) + .size(16.0) + .color(Colors::red()), + ); + ui.add_space(12.0); + } + } - /// Draw stratum port [`Modal`] content. - fn port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut StratumSetup| { - // Check if port is available. - let (stratum_ip, _) = NodeConfig::get_stratum_address(); - let available = NodeConfig::is_stratum_port_available( - &stratum_ip, - &c.stratum_port_edit - ); - c.stratum_port_available_edit = available; + /// Draw stratum port [`Modal`] content. + fn port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut StratumSetup| { + // Check if port is available. + let (stratum_ip, _) = NodeConfig::get_stratum_address(); + let available = + NodeConfig::is_stratum_port_available(&stratum_ip, &c.stratum_port_edit); + c.stratum_port_available_edit = available; - // Save port at config if it's available. - if available { - NodeConfig::save_stratum_address(&stratum_ip, &c.stratum_port_edit); + // Save port at config if it's available. + if available { + NodeConfig::save_stratum_address(&stratum_ip, &c.stratum_port_edit); - c.is_port_available = true; - Modal::close(); - } - }; + c.is_port_available = true; + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.stratum_port")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.stratum_port")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw stratum port text edit. - let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - edit.ui(ui, &mut self.stratum_port_edit, cb); - if edit.enter_pressed { - on_save(self); - } + // Draw stratum port text edit. + let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + edit.ui(ui, &mut self.stratum_port_edit, cb); + if edit.enter_pressed { + on_save(self); + } - // Show error when specified port is unavailable. - if !self.stratum_port_available_edit { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.port_unavailable")) - .size(17.0) - .color(Colors::red())); - } else { - server_restart_required_ui(ui); - } + // Show error when specified port is unavailable. + if !self.stratum_port_available_edit { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.port_unavailable")) + .size(17.0) + .color(Colors::red()), + ); + } else { + server_restart_required_ui(ui); + } - ui.add_space(12.0); + ui.add_space(12.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } - /// Draw attempt time value setup content. - fn attempt_time_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.attempt_time")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); + /// Draw attempt time value setup content. + fn attempt_time_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.attempt_time")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - let time = NodeConfig::get_stratum_attempt_time(); - View::button(ui, format!("{} {}", TIMER, &time), Colors::white_or_black(false), || { - // Setup values for modal. - self.attempt_time_edit = time; + let time = NodeConfig::get_stratum_attempt_time(); + View::button( + ui, + format!("{} {}", TIMER, &time), + Colors::white_or_black(false), + || { + // Setup values for modal. + self.attempt_time_edit = time; - // Show attempt time modal. - Modal::new(ATTEMPT_TIME_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(6.0); - ui.label(RichText::new(t!("network_settings.attempt_time_desc")) - .size(16.0) - .color(Colors::inactive_text()) - ); - ui.add_space(6.0); - } + // Show attempt time modal. + Modal::new(ATTEMPT_TIME_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(6.0); + ui.label( + RichText::new(t!("network_settings.attempt_time_desc")) + .size(16.0) + .color(Colors::inactive_text()), + ); + ui.add_space(6.0); + } - /// Draw attempt time [`Modal`] content. - fn attempt_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut StratumSetup| { - if let Ok(time) = c.attempt_time_edit.parse::() { - NodeConfig::save_stratum_attempt_time(time); - Modal::close(); - } - }; + /// Draw attempt time [`Modal`] content. + fn attempt_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut StratumSetup| { + if let Ok(time) = c.attempt_time_edit.parse::() { + NodeConfig::save_stratum_attempt_time(time); + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.attempt_time")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.attempt_time")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw attempt time text edit. - let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - edit.ui(ui, &mut self.attempt_time_edit, cb); - if edit.enter_pressed { - on_save(self); - } + // Draw attempt time text edit. + let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + edit.ui(ui, &mut self.attempt_time_edit, cb); + if edit.enter_pressed { + on_save(self); + } - // Show error when specified value is not valid or reminder to restart enabled node. - if self.attempt_time_edit.parse::().is_err() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.not_valid_value")) - .size(17.0) - .color(Colors::red())); - } else { - server_restart_required_ui(ui); - } - ui.add_space(12.0); - }); + // Show error when specified value is not valid or reminder to restart enabled node. + if self.attempt_time_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::red()), + ); + } else { + server_restart_required_ui(ui); + } + ui.add_space(12.0); + }); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + } - /// Draw minimum share difficulty value setup content. - fn min_diff_ui(&mut self, ui: &mut egui::Ui) { - ui.label(RichText::new(t!("network_settings.min_share_diff")) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(6.0); + /// Draw minimum share difficulty value setup content. + fn min_diff_ui(&mut self, ui: &mut egui::Ui) { + ui.label( + RichText::new(t!("network_settings.min_share_diff")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - let diff = NodeConfig::get_stratum_min_share_diff(); - View::button(ui, format!("{} {}", BARBELL, &diff), Colors::white_or_black(false), || { - // Setup values for modal. - self.min_share_diff_edit = diff; + let diff = NodeConfig::get_stratum_min_share_diff(); + View::button( + ui, + format!("{} {}", BARBELL, &diff), + Colors::white_or_black(false), + || { + // Setup values for modal. + self.min_share_diff_edit = diff; - // Show share difficulty setup modal. - Modal::new(MIN_SHARE_DIFF_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); - ui.add_space(6.0); - } + // Show share difficulty setup modal. + Modal::new(MIN_SHARE_DIFF_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }, + ); + ui.add_space(6.0); + } - /// Draw minimum acceptable share difficulty [`Modal`] content. - fn min_diff_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut StratumSetup| { - if let Ok(diff) = c.min_share_diff_edit.parse::() { - NodeConfig::save_stratum_min_share_diff(diff); - Modal::close(); - } - }; + /// Draw minimum acceptable share difficulty [`Modal`] content. + fn min_diff_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut StratumSetup| { + if let Ok(diff) = c.min_share_diff_edit.parse::() { + NodeConfig::save_stratum_min_share_diff(diff); + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.min_share_diff")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("network_settings.min_share_diff")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw share difficulty text edit. - let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - edit.ui(ui, &mut self.min_share_diff_edit, cb); - if edit.enter_pressed { - on_save(self); - } + // Draw share difficulty text edit. + let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + edit.ui(ui, &mut self.min_share_diff_edit, cb); + if edit.enter_pressed { + on_save(self); + } - // Show error when specified value is not valid or reminder to restart enabled node. - if self.min_share_diff_edit.parse::().is_err() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.not_valid_value")) - .size(17.0) - .color(Colors::red())); - } else { - server_restart_required_ui(ui); - } - ui.add_space(12.0); - }); + // Show error when specified value is not valid or reminder to restart enabled node. + if self.min_share_diff_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::red()), + ); + } else { + server_restart_required_ui(ui); + } + ui.add_space(12.0); + }); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + } } /// Reminder to restart enabled node to show on edit setting at [`Modal`]. pub fn server_restart_required_ui(ui: &mut egui::Ui) { - if Node::get_stratum_stats().is_running { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_mining.restart_server_required")) - .size(16.0) - .color(Colors::green()) - ); - } -} \ No newline at end of file + if Node::get_stratum_stats().is_running { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_mining.restart_server_required")) + .size(16.0) + .color(Colors::green()), + ); + } +} diff --git a/src/gui/views/network/types.rs b/src/gui/views/network/types.rs index c8d75448..d045a530 100644 --- a/src/gui/views/network/types.rs +++ b/src/gui/views/network/types.rs @@ -12,40 +12,40 @@ // See the License for the specific language governing permissions and // limitations under the License. -use serde_derive::{Deserialize, Serialize}; use crate::gui::platform::PlatformCallbacks; +use serde_derive::{Deserialize, Serialize}; /// Integrated node tab content interface. pub trait NodeTab { - fn get_type(&self) -> NodeTabType; - fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks); + fn get_type(&self) -> NodeTabType; + fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks); } /// Type of [`NodeTab`] content. #[derive(PartialEq)] pub enum NodeTabType { - Info, - Metrics, - Mining, - Settings + Info, + Metrics, + Mining, + Settings, } impl NodeTabType { - pub fn title(&self) -> String { - match *self { - NodeTabType::Info => t!("network.node").into(), - NodeTabType::Metrics => t!("network.metrics").into(), - NodeTabType::Mining => t!("network.mining").into(), - NodeTabType::Settings => t!("network.settings").into() - } - } + pub fn title(&self) -> String { + match *self { + NodeTabType::Info => t!("network.node").into(), + NodeTabType::Metrics => t!("network.metrics").into(), + NodeTabType::Mining => t!("network.mining").into(), + NodeTabType::Settings => t!("network.settings").into(), + } + } } /// Connection details to share. #[derive(Serialize, Deserialize, Clone)] pub struct ShareConnection { - #[serde(rename(serialize = "ipPort", deserialize = "ipPort"))] - pub url: String, - pub username: String, - pub secret: String -} \ No newline at end of file + #[serde(rename(serialize = "ipPort", deserialize = "ipPort"))] + pub url: String, + pub username: String, + pub secret: String, +} diff --git a/src/gui/views/pull_to_refresh.rs b/src/gui/views/pull_to_refresh.rs index ee8319a3..53e8544b 100644 --- a/src/gui/views/pull_to_refresh.rs +++ b/src/gui/views/pull_to_refresh.rs @@ -12,367 +12,370 @@ // See the License for the specific language governing permissions and // limitations under the License. +use egui::epaint::{Pos2, Shape, Stroke, emath::lerp, vec2}; use egui::scroll_area::ScrollAreaOutput; -use egui::{Sense, Align2, Area, Color32, Id, Rect, Response, Widget, Vec2, UiBuilder}; -use egui::epaint::{emath::lerp, vec2, Pos2, Shape, Stroke}; +use egui::{Align2, Area, Color32, Id, Rect, Response, Sense, UiBuilder, Vec2, Widget}; /// A spinner widget used to indicate loading. /// This was taken from egui and modified slightly to allow passing a progress value #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] #[derive(Default)] pub struct ProgressSpinner { - /// Uses the style's `interact_size` if `None`. - size: Option, - color: Option, - progress: Option, + /// Uses the style's `interact_size` if `None`. + size: Option, + color: Option, + progress: Option, } impl ProgressSpinner { - /// Create a new spinner that uses the style's `interact_size` unless changed. - pub fn new() -> Self { - Self::default() - } + /// Create a new spinner that uses the style's `interact_size` unless changed. + pub fn new() -> Self { + Self::default() + } - /// Sets the spinner's size. The size sets both the height and width, as the spinner is always - /// square. If the size isn't set explicitly, the active style's `interact_size` is used. - #[allow(unused)] - pub fn size(mut self, size: f32) -> Self { - self.size = Some(size); - self - } + /// Sets the spinner's size. The size sets both the height and width, as the spinner is always + /// square. If the size isn't set explicitly, the active style's `interact_size` is used. + #[allow(unused)] + pub fn size(mut self, size: f32) -> Self { + self.size = Some(size); + self + } - /// Sets the spinner's color. - pub fn color(mut self, color: impl Into) -> Self { - self.color = Some(color.into()); - self - } + /// Sets the spinner's color. + pub fn color(mut self, color: impl Into) -> Self { + self.color = Some(color.into()); + self + } - /// Sets the spinner's progress. - /// Should be in the range `[0.0, 1.0]`. - pub fn progress(mut self, progress: impl Into>) -> Self { - self.progress = progress.into(); - self - } + /// Sets the spinner's progress. + /// Should be in the range `[0.0, 1.0]`. + pub fn progress(mut self, progress: impl Into>) -> Self { + self.progress = progress.into(); + self + } - /// Paint the spinner in the given rectangle. - pub fn paint_at(&self, ui: &egui::Ui, rect: Rect) { - if ui.is_rect_visible(rect) { - ui.ctx().request_repaint(); // because it is animated + /// Paint the spinner in the given rectangle. + pub fn paint_at(&self, ui: &egui::Ui, rect: Rect) { + if ui.is_rect_visible(rect) { + ui.ctx().request_repaint(); // because it is animated - let color = self - .color - .unwrap_or_else(|| ui.visuals().strong_text_color()); - let radius = (rect.height() / 2.0) - 2.0; - let n_points = 20; + let color = self + .color + .unwrap_or_else(|| ui.visuals().strong_text_color()); + let radius = (rect.height() / 2.0) - 2.0; + let n_points = 20; - let (start_angle, end_angle) = if let Some(progress) = self.progress { - let start_angle = 0f64.to_radians(); - let end_angle = start_angle + 360f64.to_radians() * progress; - (start_angle, end_angle) - } else { - let time = ui.input(|i| i.time); - let start_angle = time * std::f64::consts::TAU; - let end_angle = start_angle + 240f64.to_radians() * time.sin(); - (start_angle, end_angle) - }; + let (start_angle, end_angle) = if let Some(progress) = self.progress { + let start_angle = 0f64.to_radians(); + let end_angle = start_angle + 360f64.to_radians() * progress; + (start_angle, end_angle) + } else { + let time = ui.input(|i| i.time); + let start_angle = time * std::f64::consts::TAU; + let end_angle = start_angle + 240f64.to_radians() * time.sin(); + (start_angle, end_angle) + }; - let points: Vec = (0..=n_points) - .map(|i| { - let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64); - let (sin, cos) = angle.sin_cos(); - rect.center() + radius * vec2(cos as f32, sin as f32) - }) - .collect(); - ui.painter() - .add(Shape::line(points, Stroke::new(3.0, color))); - } - } + let points: Vec = (0..=n_points) + .map(|i| { + let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64); + let (sin, cos) = angle.sin_cos(); + rect.center() + radius * vec2(cos as f32, sin as f32) + }) + .collect(); + ui.painter() + .add(Shape::line(points, Stroke::new(3.0, color))); + } + } } impl Widget for ProgressSpinner { - fn ui(self, ui: &mut egui::Ui) -> Response { - let size = self - .size - .unwrap_or_else(|| ui.style().spacing.interact_size.y); - let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover()); - self.paint_at(ui, rect); + fn ui(self, ui: &mut egui::Ui) -> Response { + let size = self + .size + .unwrap_or_else(|| ui.style().spacing.interact_size.y); + let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover()); + self.paint_at(ui, rect); - response - } + response + } } /// The current state of the pull to refresh widget. #[derive(Debug, Clone)] pub enum PullToRefreshState { - /// The widget is idle, no refresh is happening. - Idle, - /// The user is dragging. - Dragging { - /// `distance` is the distance the user dragged. - distance: f32, - /// `far_enough` is true if the user dragged far enough to trigger a refresh. - far_enough: bool, - }, - /// The user dragged far enough to trigger a refresh. - DoRefresh, - /// The refresh is currently happening. - Refreshing, + /// The widget is idle, no refresh is happening. + Idle, + /// The user is dragging. + Dragging { + /// `distance` is the distance the user dragged. + distance: f32, + /// `far_enough` is true if the user dragged far enough to trigger a refresh. + far_enough: bool, + }, + /// The user dragged far enough to trigger a refresh. + DoRefresh, + /// The refresh is currently happening. + Refreshing, } impl PullToRefreshState { - fn progress(&self, min_distance: f32) -> Option { - match self { - PullToRefreshState::Idle => Some(0.0), - PullToRefreshState::Dragging { distance, .. } => { - Some((distance / min_distance).min(1.0).max(0.0) as f64) - } - PullToRefreshState::DoRefresh => Some(1.0), - PullToRefreshState::Refreshing => None, - } - } + fn progress(&self, min_distance: f32) -> Option { + match self { + PullToRefreshState::Idle => Some(0.0), + PullToRefreshState::Dragging { distance, .. } => { + Some((distance / min_distance).min(1.0).max(0.0) as f64) + } + PullToRefreshState::DoRefresh => Some(1.0), + PullToRefreshState::Refreshing => None, + } + } } /// The response of the pull to refresh widget. #[derive(Debug, Clone)] pub struct PullToRefreshResponse { - /// Current state of the pull to refresh widget. - pub state: PullToRefreshState, - /// The inner response of the widget you wrapped in [`PullToRefresh::ui`] or [`PullToRefresh::scroll_area_ui`]. - pub inner: T, + /// Current state of the pull to refresh widget. + pub state: PullToRefreshState, + /// The inner response of the widget you wrapped in [`PullToRefresh::ui`] or [`PullToRefresh::scroll_area_ui`]. + pub inner: T, } impl PullToRefreshResponse { - /// Returns true if the user dragged far enough to trigger a refresh. - pub fn should_refresh(&self) -> bool { - matches!(self.state, PullToRefreshState::DoRefresh) - } + /// Returns true if the user dragged far enough to trigger a refresh. + pub fn should_refresh(&self) -> bool { + matches!(self.state, PullToRefreshState::DoRefresh) + } } /// A widget that allows the user to pull to refresh. pub struct PullToRefresh { - id: Id, - loading: bool, - min_refresh_distance: f32, - can_refresh: bool, + id: Id, + loading: bool, + min_refresh_distance: f32, + can_refresh: bool, } impl PullToRefresh { - /// Creates a new pull to refresh widget. - /// If `loading` is true, the widget will show the loading indicator. - pub fn new(loading: bool) -> Self { - Self { - id: Id::new("pull_to_refresh"), - loading, - min_refresh_distance: 100.0, - can_refresh: true, - } - } + /// Creates a new pull to refresh widget. + /// If `loading` is true, the widget will show the loading indicator. + pub fn new(loading: bool) -> Self { + Self { + id: Id::new("pull_to_refresh"), + loading, + min_refresh_distance: 100.0, + can_refresh: true, + } + } - /// Sets the minimum distance the user needs to drag to trigger a refresh. - pub fn min_refresh_distance(mut self, min_refresh_distance: f32) -> Self { - self.min_refresh_distance = min_refresh_distance; - self - } + /// Sets the minimum distance the user needs to drag to trigger a refresh. + pub fn min_refresh_distance(mut self, min_refresh_distance: f32) -> Self { + self.min_refresh_distance = min_refresh_distance; + self + } - /// You need to provide a id if you use multiple pull to refresh widgets at once. - pub fn id(mut self, id: Id) -> Self { - self.id = id; - self - } + /// You need to provide a id if you use multiple pull to refresh widgets at once. + pub fn id(mut self, id: Id) -> Self { + self.id = id; + self + } - /// If `can_refresh` is false, pulling will not trigger a refresh. - pub fn can_refresh(mut self, can_refresh: bool) -> Self { - self.can_refresh = can_refresh; - self - } + /// If `can_refresh` is false, pulling will not trigger a refresh. + pub fn can_refresh(mut self, can_refresh: bool) -> Self { + self.can_refresh = can_refresh; + self + } - /// Shows the pull to refresh widget. - /// Note: If you want to use the pull to refresh widget in a scroll area, use [`Self::scroll_area_ui`]. - /// You might want to disable text selection via [`egui::style::Interaction`] - /// to avoid conflicts with the drag gesture. - pub fn ui( - self, - ui: &mut egui::Ui, - content: impl FnOnce(&mut egui::Ui) -> T, - ) -> PullToRefreshResponse { - let mut child = ui.new_child(UiBuilder::new() - .max_rect(ui.available_rect_before_wrap()) - .layout(*ui.layout())); + /// Shows the pull to refresh widget. + /// Note: If you want to use the pull to refresh widget in a scroll area, use [`Self::scroll_area_ui`]. + /// You might want to disable text selection via [`egui::style::Interaction`] + /// to avoid conflicts with the drag gesture. + pub fn ui( + self, + ui: &mut egui::Ui, + content: impl FnOnce(&mut egui::Ui) -> T, + ) -> PullToRefreshResponse { + let mut child = ui.new_child( + UiBuilder::new() + .max_rect(ui.available_rect_before_wrap()) + .layout(*ui.layout()), + ); - let output = content(&mut child); + let output = content(&mut child); - let can_refresh = self.can_refresh; - let state = self.internal_ui(ui, can_refresh, None, child.min_rect()); + let can_refresh = self.can_refresh; + let state = self.internal_ui(ui, can_refresh, None, child.min_rect()); - PullToRefreshResponse { - state, - inner: output, - } - } + PullToRefreshResponse { + state, + inner: output, + } + } - /// Shows the pull to refresh widget, wrapping a [egui::ScrollArea]. - /// Pass the output of the scroll area to the content function. - pub fn scroll_area_ui( - self, - ui: &mut egui::Ui, - content: impl FnOnce(&mut egui::Ui) -> ScrollAreaOutput, - ) -> PullToRefreshResponse> { - let scroll_output = content(ui); - let content_rect = scroll_output.inner_rect; - let can_refresh = scroll_output.state.offset.y == 0.0 && self.can_refresh; - // This is the id used in the Sense of the scroll area - // I hope this id is stable across egui patches... - let allow_dragged_id = scroll_output.id.with("area"); - let state = self.internal_ui(ui, can_refresh, Some(allow_dragged_id), content_rect); - PullToRefreshResponse { - state, - inner: scroll_output, - } - } + /// Shows the pull to refresh widget, wrapping a [egui::ScrollArea]. + /// Pass the output of the scroll area to the content function. + pub fn scroll_area_ui( + self, + ui: &mut egui::Ui, + content: impl FnOnce(&mut egui::Ui) -> ScrollAreaOutput, + ) -> PullToRefreshResponse> { + let scroll_output = content(ui); + let content_rect = scroll_output.inner_rect; + let can_refresh = scroll_output.state.offset.y == 0.0 && self.can_refresh; + // This is the id used in the Sense of the scroll area + // I hope this id is stable across egui patches... + let allow_dragged_id = scroll_output.id.with("area"); + let state = self.internal_ui(ui, can_refresh, Some(allow_dragged_id), content_rect); + PullToRefreshResponse { + state, + inner: scroll_output, + } + } - fn internal_ui( - self, - ui: &mut egui::Ui, - can_refresh: bool, - allow_dragged_id: Option, - content_rect: Rect, - ) -> PullToRefreshState { - let last_state = ui.data_mut(|data| { - data.get_temp_mut_or(self.id, PullToRefreshState::Idle) - .clone() - }); + fn internal_ui( + self, + ui: &mut egui::Ui, + can_refresh: bool, + allow_dragged_id: Option, + content_rect: Rect, + ) -> PullToRefreshState { + let last_state = ui.data_mut(|data| { + data.get_temp_mut_or(self.id, PullToRefreshState::Idle) + .clone() + }); - let mut state = last_state; - if self.loading { - state = PullToRefreshState::Refreshing; - } + let mut state = last_state; + if self.loading { + state = PullToRefreshState::Refreshing; + } - if !self.loading && matches!(state, PullToRefreshState::Refreshing) { - state = PullToRefreshState::Idle; - } + if !self.loading && matches!(state, PullToRefreshState::Refreshing) { + state = PullToRefreshState::Idle; + } - if can_refresh && !self.loading { - let sense = ui.interact(content_rect, self.id, Sense::hover()); + if can_refresh && !self.loading { + let sense = ui.interact(content_rect, self.id, Sense::hover()); - let is_something_blocking_drag = ui.ctx().dragged_id().is_some() - && !allow_dragged_id.map_or(false, |id| ui.ctx().is_being_dragged(id)); + let is_something_blocking_drag = ui.ctx().dragged_id().is_some() + && !allow_dragged_id.map_or(false, |id| ui.ctx().is_being_dragged(id)); - if sense.contains_pointer() && !is_something_blocking_drag { - let (delta, any_released) = ui.input(|input| { - ( - if input.pointer.is_decidedly_dragging() { - Some(input.pointer.delta()) - } else { - None - }, - input.pointer.any_released(), - ) - }); - if let Some(delta) = delta { - if matches!(state, PullToRefreshState::Idle) { - state = PullToRefreshState::Dragging { - distance: 0.0, - far_enough: false, - }; - } - if let PullToRefreshState::Dragging { distance: drag, .. } = state.clone() { - let dist = drag + delta.y; - state = PullToRefreshState::Dragging { - distance: dist, - far_enough: dist > self.min_refresh_distance, - }; - } - } else { - state = PullToRefreshState::Idle; - } - if any_released { - if let PullToRefreshState::Dragging { - far_enough: enough, .. - } = state.clone() - { - if enough { - state = PullToRefreshState::DoRefresh; - } else { - state = PullToRefreshState::Idle; - } - } else { - state = PullToRefreshState::Idle; - } - } else if let PullToRefreshState::Dragging { - far_enough: enough, .. - } = state.clone() { - if enough { - state = PullToRefreshState::DoRefresh; - } - } - } else { - state = PullToRefreshState::Idle; - } - } else { - state = PullToRefreshState::Idle; - } + if sense.contains_pointer() && !is_something_blocking_drag { + let (delta, any_released) = ui.input(|input| { + ( + if input.pointer.is_decidedly_dragging() { + Some(input.pointer.delta()) + } else { + None + }, + input.pointer.any_released(), + ) + }); + if let Some(delta) = delta { + if matches!(state, PullToRefreshState::Idle) { + state = PullToRefreshState::Dragging { + distance: 0.0, + far_enough: false, + }; + } + if let PullToRefreshState::Dragging { distance: drag, .. } = state.clone() { + let dist = drag + delta.y; + state = PullToRefreshState::Dragging { + distance: dist, + far_enough: dist > self.min_refresh_distance, + }; + } + } else { + state = PullToRefreshState::Idle; + } + if any_released { + if let PullToRefreshState::Dragging { + far_enough: enough, .. + } = state.clone() + { + if enough { + state = PullToRefreshState::DoRefresh; + } else { + state = PullToRefreshState::Idle; + } + } else { + state = PullToRefreshState::Idle; + } + } else if let PullToRefreshState::Dragging { + far_enough: enough, .. + } = state.clone() + { + if enough { + state = PullToRefreshState::DoRefresh; + } + } + } else { + state = PullToRefreshState::Idle; + } + } else { + state = PullToRefreshState::Idle; + } - if self.loading { - state = PullToRefreshState::Refreshing; - } + if self.loading { + state = PullToRefreshState::Refreshing; + } - let spinner_size = Vec2::splat(24.0); + let spinner_size = Vec2::splat(24.0); - let progress_for_offset = match &state { - PullToRefreshState::Idle => 0.0, - PullToRefreshState::Dragging { .. } => { - state.progress(self.min_refresh_distance).unwrap_or(1.0) - } - PullToRefreshState::DoRefresh => 1.0, - PullToRefreshState::Refreshing => 1.0, - } as f32; + let progress_for_offset = match &state { + PullToRefreshState::Idle => 0.0, + PullToRefreshState::Dragging { .. } => { + state.progress(self.min_refresh_distance).unwrap_or(1.0) + } + PullToRefreshState::DoRefresh => 1.0, + PullToRefreshState::Refreshing => 1.0, + } as f32; - let anim_progress = ui.ctx().animate_value_with_time( - self.id.with("offset_top"), - progress_for_offset, - ui.style().animation_time, - ); + let anim_progress = ui.ctx().animate_value_with_time( + self.id.with("offset_top"), + progress_for_offset, + ui.style().animation_time, + ); - let offset_top = -spinner_size.y + spinner_size.y * anim_progress * 2.0; + let offset_top = -spinner_size.y + spinner_size.y * anim_progress * 2.0; - if anim_progress > 0.0 { - Area::new(Id::new("Pull to refresh indicator")) - .fixed_pos(content_rect.center_top()) - .pivot(Align2::CENTER_TOP) - .show(ui.ctx(), |ui| { - let (rect, _) = ui.allocate_exact_size(spinner_size, Sense::hover()); + if anim_progress > 0.0 { + Area::new(Id::new("Pull to refresh indicator")) + .fixed_pos(content_rect.center_top()) + .pivot(Align2::CENTER_TOP) + .show(ui.ctx(), |ui| { + let (rect, _) = ui.allocate_exact_size(spinner_size, Sense::hover()); - ui.set_clip_rect(Rect::everything_below(rect.min.y)); + ui.set_clip_rect(Rect::everything_below(rect.min.y)); - let rect = rect.translate(Vec2::new(0.0, offset_top)); + let rect = rect.translate(Vec2::new(0.0, offset_top)); - ui.painter().circle( - rect.center(), - spinner_size.x / 1.5, - ui.style().visuals.widgets.inactive.bg_fill, - ui.visuals().widgets.inactive.bg_stroke, - ); + ui.painter().circle( + rect.center(), + spinner_size.x / 1.5, + ui.style().visuals.widgets.inactive.bg_fill, + ui.visuals().widgets.inactive.bg_stroke, + ); - let mut spinner_color = ui.style().visuals.widgets.inactive.fg_stroke.color; - if anim_progress < 1.0 { - spinner_color = Color32::from_rgba_unmultiplied( - spinner_color.r(), - spinner_color.g(), - spinner_color.b(), - (spinner_color.a() as f32 * 0.7).round() as u8, - ); - } - ProgressSpinner::new() - .color(spinner_color) - .progress(state.progress(self.min_refresh_distance)) - .paint_at(ui, rect); - }); - } + let mut spinner_color = ui.style().visuals.widgets.inactive.fg_stroke.color; + if anim_progress < 1.0 { + spinner_color = Color32::from_rgba_unmultiplied( + spinner_color.r(), + spinner_color.g(), + spinner_color.b(), + (spinner_color.a() as f32 * 0.7).round() as u8, + ); + } + ProgressSpinner::new() + .color(spinner_color) + .progress(state.progress(self.min_refresh_distance)) + .paint_at(ui, rect); + }); + } - ui.data_mut(|data| { - data.insert_temp(self.id, state.clone()); - }); + ui.data_mut(|data| { + data.insert_temp(self.id, state.clone()); + }); - state - } -} \ No newline at end of file + state + } +} diff --git a/src/gui/views/qr.rs b/src/gui/views/qr.rs index 82746dc4..82f1ae67 100644 --- a/src/gui/views/qr.rs +++ b/src/gui/views/qr.rs @@ -22,481 +22,504 @@ use std::mem::size_of; use std::sync::Arc; use std::thread; +use crate::gui::Colors; use crate::gui::icons::{COPY, IMAGES_SQUARE}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::types::QrImageState; use crate::gui::views::View; -use crate::gui::Colors; +use crate::gui::views::types::QrImageState; /// QR code image from text. pub struct QrCodeContent { - /// QR code text. - pub text: String, - /// Flag to show text below QR code. - show_text: bool, - /// Flag to copy text below QR code. - can_copy_text: bool, + /// QR code text. + pub text: String, + /// Flag to show text below QR code. + show_text: bool, + /// Flag to copy text below QR code. + can_copy_text: bool, - /// Maximum QR code size. - max_size: f32, + /// Maximum QR code size. + max_size: f32, - /// Flag to draw animated QR with Uniform Resources - /// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md - animated: bool, - /// Index of current image at animation. - animated_index: Option, - /// Time of last image draw. - animation_time: Option, + /// Flag to draw animated QR with Uniform Resources + /// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md + animated: bool, + /// Index of current image at animation. + animated_index: Option, + /// Time of last image draw. + animation_time: Option, - /// Texture handle to show image when created. - texture_handle: Option, - /// QR code view data state. - qr_image_state: Arc>, + /// Texture handle to show image when created. + texture_handle: Option, + /// QR code view data state. + qr_image_state: Arc>, } const DEFAULT_QR_SIZE: u32 = 512; impl QrCodeContent { - pub fn new(text: String, animated: bool) -> Self { - Self { - text, - show_text: true, - can_copy_text: true, - max_size: DEFAULT_QR_SIZE as f32, - animated, - animated_index: None, - animation_time: None, - texture_handle: None, - qr_image_state: Arc::new(RwLock::new(QrImageState::default())), - } - } + pub fn new(text: String, animated: bool) -> Self { + Self { + text, + show_text: true, + can_copy_text: true, + max_size: DEFAULT_QR_SIZE as f32, + animated, + animated_index: None, + animation_time: None, + texture_handle: None, + qr_image_state: Arc::new(RwLock::new(QrImageState::default())), + } + } - /// Setup maximum QR code size. - pub fn with_max_size(mut self, max_size: f32) -> Self { - self.max_size = max_size; - self - } + /// Setup maximum QR code size. + pub fn with_max_size(mut self, max_size: f32) -> Self { + self.max_size = max_size; + self + } - /// Hide text below QR code. - pub fn hide_text(mut self) -> Self { - self.show_text = false; - self - } + /// Hide text below QR code. + pub fn hide_text(mut self) -> Self { + self.show_text = false; + self + } - /// Do not show button to copy QR code text. - pub fn no_copy(mut self) -> Self { - self.can_copy_text = false; - self - } + /// Do not show button to copy QR code text. + pub fn no_copy(mut self) -> Self { + self.can_copy_text = false; + self + } - /// Draw QR code. - pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - if self.animated { - // Show animated QR code. - self.animated_ui(ui, cb); - } else { - // Show static QR code. - self.static_ui(ui, cb); - } - ui.add_space(8.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); - } + /// Draw QR code. + pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + if self.animated { + // Show animated QR code. + self.animated_ui(ui, cb); + } else { + // Show static QR code. + self.static_ui(ui, cb); + } + ui.add_space(8.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); + } - /// Draw animated QR code content. - fn animated_ui(&mut self, ui: &mut egui::Ui, 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); - }); + /// Draw animated QR code content. + fn animated_ui(&mut self, ui: &mut egui::Ui, 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(); - } - } else { - let svg_list = { - let r_create = self.qr_image_state.read(); - r_create.svg_list.clone().unwrap() - }; + // Create multiple vector images from text if not creating. + if !self.loading() { + self.create_svg_list(); + } + } 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); - } + // 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(); + let svg = svg_list[self.animated_index.unwrap_or(0)].clone(); - // Create images from SVG data. - self.qr_image_ui(svg, ui); + // Create images from SVG data. + self.qr_image_ui(svg, ui); - // Show QR code text. - if self.show_text { - self.text_ui(ui); - } + // Show QR code text. + if self.show_text { + self.text_ui(ui); + } - ui.vertical_centered(|ui| { - let sharing = { - let r_state = self.qr_image_state.read(); - r_state.exporting || r_state.gif_creating - }; - if !sharing { - ui.vertical_centered(|ui| { - // Show button to share QR. - let share_text = format!("{} {}", IMAGES_SQUARE, t!("share")); - View::colored_text_button(ui, - share_text, - Colors::blue(), - Colors::white_or_black(false), || { - { - let mut w_state = self.qr_image_state.write(); - w_state.exporting = true; - } - // Create GIF to export. - self.create_qr_gif(); - }); - }); - } else { - ui.vertical_centered(|ui| { - ui.add_space(2.0); - View::small_loading_spinner(ui); - ui.add_space(1.0); - }); - } + ui.vertical_centered(|ui| { + let sharing = { + let r_state = self.qr_image_state.read(); + r_state.exporting || r_state.gif_creating + }; + if !sharing { + ui.vertical_centered(|ui| { + // Show button to share QR. + let share_text = format!("{} {}", IMAGES_SQUARE, t!("share")); + View::colored_text_button( + ui, + share_text, + Colors::blue(), + Colors::white_or_black(false), + || { + { + let mut w_state = self.qr_image_state.write(); + w_state.exporting = true; + } + // Create GIF to export. + self.create_qr_gif(); + }, + ); + }); + } else { + ui.vertical_centered(|ui| { + ui.add_space(2.0); + View::small_loading_spinner(ui); + ui.add_space(1.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; - } - } - }); + // 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(); - } - } + ui.ctx().request_repaint(); + } + } - /// Draw static QR code content. - fn static_ui(&mut self, ui: &mut egui::Ui, 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); - }); + /// Draw static QR code content. + fn static_ui(&mut self, ui: &mut egui::Ui, 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(); - } - } else { - // Create image from SVG data. - let svg = { - let r_state = self.qr_image_state.read(); - r_state.svg.clone().unwrap() - }; - self.qr_image_ui(svg, ui); + // Create vector image from text if not creating. + if !self.loading() { + self.create_svg(); + } + } else { + // Create image from SVG data. + let svg = { + let r_state = self.qr_image_state.read(); + r_state.svg.clone().unwrap() + }; + self.qr_image_ui(svg, ui); - // Show QR code text. - if self.show_text { - self.text_ui(ui); - } else { - ui.add_space(8.0); - } + // Show QR code text. + if self.show_text { + self.text_ui(ui); + } else { + ui.add_space(8.0); + } - if self.can_copy_text { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0); + if self.can_copy_text { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - // Draw copy button. - let copy_text = format!("{} {}", COPY, t!("copy")); - View::button(ui, copy_text, Colors::white_or_black(false), || { - cb.copy_string_to_buffer(self.text.clone()); - }); - }); - columns[1].vertical_centered_justified(|ui| { - self.share_static_button_ui(ui, cb); - }); - }); - } else { - ui.vertical_centered(|ui| { - self.share_static_button_ui(ui, cb); - }); - } - } - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + // Draw copy button. + let copy_text = format!("{} {}", COPY, t!("copy")); + View::button(ui, copy_text, Colors::white_or_black(false), || { + cb.copy_string_to_buffer(self.text.clone()); + }); + }); + columns[1].vertical_centered_justified(|ui| { + self.share_static_button_ui(ui, cb); + }); + }); + } else { + ui.vertical_centered(|ui| { + self.share_static_button_ui(ui, cb); + }); + } + } + } - /// Draw button to share static QR code. - fn share_static_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - let share_text = format!("{} {}", IMAGES_SQUARE, t!("share")); - View::colored_text_button(ui, - share_text, - Colors::blue(), - Colors::white_or_black(false), || { - self.share_static(cb); - }); - } + /// Draw button to share static QR code. + fn share_static_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + let share_text = format!("{} {}", IMAGES_SQUARE, t!("share")); + View::colored_text_button( + ui, + share_text, + Colors::blue(), + Colors::white_or_black(false), + || { + self.share_static(cb); + }, + ); + } - /// Share static QR code image. - fn share_static(&self, cb: &dyn PlatformCallbacks) { - let text = self.text.as_str(); - if let Ok(qr) = QrCode::encode_text(text, qrcodegen::QrCodeEcc::Low) { - let size = DEFAULT_QR_SIZE as usize; - if let Some(data) = Self::qr_to_image_data(qr, size) { - 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(); - } - } - } - } + /// Share static QR code image. + fn share_static(&self, cb: &dyn PlatformCallbacks) { + let text = self.text.as_str(); + if let Ok(qr) = QrCode::encode_text(text, qrcodegen::QrCodeEcc::Low) { + let size = DEFAULT_QR_SIZE as usize; + if let Some(data) = Self::qr_to_image_data(qr, size) { + 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(); + } + } + } + } - /// Draw QR code image content. - fn qr_image_ui(&mut self, svg: Vec, ui: &mut egui::Ui) { - View::max_width_ui(ui, self.max_size, |ui| { - let mut rect = ui.available_rect_before_wrap(); - rect.min += egui::emath::vec2(10.0, 0.0); - rect.max -= egui::emath::vec2(10.0, 0.0); + /// Draw QR code image content. + fn qr_image_ui(&mut self, svg: Vec, ui: &mut egui::Ui) { + View::max_width_ui(ui, self.max_size, |ui| { + let mut rect = ui.available_rect_before_wrap(); + rect.min += egui::emath::vec2(10.0, 0.0); + rect.max -= egui::emath::vec2(10.0, 0.0); - // Create background shape. - let mut bg_shape = RectShape::new( - rect, - egui::CornerRadius::default(), - egui::Color32::WHITE, - egui::Stroke::NONE, - egui::StrokeKind::Outside - ); - let bg_idx = ui.painter().add(bg_shape.clone()); + // Create background shape. + let mut bg_shape = RectShape::new( + rect, + egui::CornerRadius::default(), + egui::Color32::WHITE, + egui::Stroke::NONE, + egui::StrokeKind::Outside, + ); + let bg_idx = ui.painter().add(bg_shape.clone()); - // Draw QR code image. - let mut content_rect = ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| { - ui.add_space(10.0); - let size = SizeHint::Size { - width: ui.available_width() as u32, - height: ui.available_width() as u32, - maintain_aspect_ratio: true, - }; - self.texture_handle = Some(View::svg_image(ui, "qr", svg.as_slice(), size)); - ui.add_space(10.0); - }).response.rect; + // Draw QR code image. + let mut content_rect = ui + .scope_builder(UiBuilder::new().max_rect(rect), |ui| { + ui.add_space(10.0); + let size = SizeHint::Size { + width: ui.available_width() as u32, + height: ui.available_width() as u32, + maintain_aspect_ratio: true, + }; + self.texture_handle = Some(View::svg_image(ui, "qr", svg.as_slice(), size)); + ui.add_space(10.0); + }) + .response + .rect; - // Setup background size. - content_rect.min -= egui::emath::vec2(10.0, 0.0); - content_rect.max += egui::emath::vec2(10.0, 0.0); - bg_shape.rect = content_rect; - ui.painter().set(bg_idx, bg_shape); - }); - } + // Setup background size. + content_rect.min -= egui::emath::vec2(10.0, 0.0); + content_rect.max += egui::emath::vec2(10.0, 0.0); + bg_shape.rect = content_rect; + ui.painter().set(bg_idx, bg_shape); + }); + } - /// Draw QR code text. - fn text_ui(&self, ui: &mut egui::Ui) { - ui.add_space(6.0); - View::ellipsize_text(ui, self.text.clone(), 15.0, Colors::inactive_text()); - ui.add_space(6.0); - } + /// Draw QR code text. + fn text_ui(&self, ui: &mut egui::Ui) { + ui.add_space(6.0); + View::ellipsize_text(ui, self.text.clone(), 15.0, Colors::inactive_text()); + ui.add_space(6.0); + } - /// Check if QR code is loading. - fn loading(&self) -> bool { - let r_state = self.qr_image_state.read(); - r_state.loading - } + /// 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) { - let qr_state = self.qr_image_state.clone(); - let text = self.text.clone(); - thread::spawn(move || { - let mut encoder = ur::Encoder::bytes(text.as_bytes(), 64).unwrap(); - let mut data = Vec::with_capacity(encoder.fragment_count()); - for _ in 0..encoder.fragment_count() { - let ur = encoder.next_part().unwrap(); - if let Ok(qr) = QrCode::encode_text(ur.as_str(), qrcodegen::QrCodeEcc::Low) { - let svg = Self::qr_to_svg(qr, 0); - data.push(svg.into_bytes()); - } - } - let mut w_state = qr_state.write(); - if !data.is_empty() { - w_state.svg_list = Some(data); - } - w_state.loading = false; - }); - } + /// Create multiple vector QR code images at separate thread. + fn create_svg_list(&self) { + let qr_state = self.qr_image_state.clone(); + let text = self.text.clone(); + thread::spawn(move || { + let mut encoder = ur::Encoder::bytes(text.as_bytes(), 64).unwrap(); + let mut data = Vec::with_capacity(encoder.fragment_count()); + for _ in 0..encoder.fragment_count() { + let ur = encoder.next_part().unwrap(); + if let Ok(qr) = QrCode::encode_text(ur.as_str(), qrcodegen::QrCodeEcc::Low) { + let svg = Self::qr_to_svg(qr, 0); + data.push(svg.into_bytes()); + } + } + let mut w_state = qr_state.write(); + if !data.is_empty() { + w_state.svg_list = Some(data); + } + w_state.loading = false; + }); + } - /// Check if image was created. - fn has_image(&self) -> bool { - let r_state = self.qr_image_state.read(); - r_state.svg.is_some() || r_state.svg_list.is_some() - } + /// Check if image was created. + fn has_image(&self) -> bool { + 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) { - let qr_state = self.qr_image_state.clone(); - let text = self.text.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_state = qr_state.write(); - w_state.loading = false; - w_state.svg = Some(svg.into_bytes()); - } - }); - } + /// Create vector QR code image at separate thread. + fn create_svg(&self) { + let qr_state = self.qr_image_state.clone(); + let text = self.text.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_state = qr_state.write(); + w_state.loading = false; + w_state.svg = Some(svg.into_bytes()); + } + }); + } - /// Convert QR code to SVG string. - fn qr_to_svg(qr: 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 - } + /// Convert QR code to SVG string. + fn qr_to_svg(qr: 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 + } - /// Create GIF image at separate thread. - fn create_qr_gif(&self) { - { - let mut w_state = self.qr_image_state.write(); - w_state.gif_creating = true; + /// Create GIF image at separate thread. + fn create_qr_gif(&self) { + { + let mut w_state = self.qr_image_state.write(); + w_state.gif_creating = true; + } + let qr_state = self.qr_image_state.clone(); + let text = self.text.clone(); + thread::spawn(move || { + // Setup GIF image encoder. + let mut gif = vec![]; + { + // Generate QR codes from text. + let mut qrs = vec![]; + 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::QrCode::with_error_correction_level( + ur.as_bytes(), + qrcode::EcLevel::L, + ) { + // Create an image from QR data. + let image = qr + .render() + .max_dimensions(DEFAULT_QR_SIZE, DEFAULT_QR_SIZE) + .dark_color(image::Rgb([0, 0, 0])) + .light_color(image::Rgb([255, 255, 255])) + .build(); + qrs.push(image); + } + } - } - let qr_state = self.qr_image_state.clone(); - let text = self.text.clone(); - thread::spawn(move || { - // Setup GIF image encoder. - let mut gif = vec![]; - { - // Generate QR codes from text. - let mut qrs = vec![]; - 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::QrCode::with_error_correction_level( - ur.as_bytes(), - qrcode::EcLevel::L - ) { - // Create an image from QR data. - let image = qr.render() - .max_dimensions(DEFAULT_QR_SIZE, DEFAULT_QR_SIZE) - .dark_color(image::Rgb([0, 0, 0])) - .light_color(image::Rgb([255, 255, 255])) - .build(); - qrs.push(image); - } - } + if !qrs.is_empty() { + // Generate GIF data. + let color_map = &[0, 0, 0, 0xFF, 0xFF, 0xFF]; + let mut gif_enc = gif::Encoder::new( + &mut gif, + qrs[0].width() as u16, + qrs[0].height() as u16, + color_map, + ) + .unwrap(); + gif_enc.set_repeat(gif::Repeat::Infinite).unwrap(); + for qr in qrs { + let mut frame = gif::Frame::from_rgb( + qr.width() as u16, + qr.height() as u16, + qr.as_raw().as_slice(), + ); + frame.delay = 10; + // 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; + }); + } - if !qrs.is_empty() { - // Generate GIF data. - let color_map = &[0, 0, 0, 0xFF, 0xFF, 0xFF]; - let mut gif_enc = gif::Encoder::new(&mut gif, - qrs[0].width() as u16, - qrs[0].height() as u16, - color_map).unwrap(); - gif_enc.set_repeat(gif::Repeat::Infinite).unwrap(); - for qr in qrs { - let mut frame = gif::Frame::from_rgb(qr.width() as u16, - qr.height() as u16, - qr.as_raw().as_slice()); - frame.delay = 10; - // 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; - /// 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) - } -} \ No newline at end of file + 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) + } +} diff --git a/src/gui/views/scan.rs b/src/gui/views/scan.rs index e1a01df1..7aa81722 100644 --- a/src/gui/views/scan.rs +++ b/src/gui/views/scan.rs @@ -18,120 +18,135 @@ use egui::{Id, ScrollArea}; use crate::gui::Colors; use crate::gui::icons::COPY; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{CameraContent, Modal, View}; use crate::gui::views::types::QrScanResult; +use crate::gui::views::{CameraContent, Modal, View}; /// QR code scanning content. pub struct CameraScanContent { - /// Camera content. - camera_content: Option, - /// Scan result. - qr_scan_result: Option, + /// Camera content. + camera_content: Option, + /// Scan result. + qr_scan_result: Option, } impl Default for CameraScanContent { - fn default() -> Self { - Self { - camera_content: Some(CameraContent::default()), - qr_scan_result: None, - } - } + fn default() -> Self { + Self { + camera_content: Some(CameraContent::default()), + qr_scan_result: None, + } + } } impl CameraScanContent { - /// Draw [`Modal`] content. - pub fn modal_ui(&mut self, - ui: &mut egui::Ui, - cb: &dyn PlatformCallbacks, - mut on_result: impl FnMut(&QrScanResult)) { - // Show scan result if exists or show camera content while scanning. - if let Some(result) = &self.qr_scan_result.clone() { - Self::result_ui(ui, result, cb, || { - Modal::close(); - }, || { - self.qr_scan_result = None; - cb.start_camera(); - Modal::set_title(t!("scan_qr")); - }); - } else if let Some(camera_content) = self.camera_content.as_mut() { - if let Some(result) = camera_content.qr_scan_result() { - cb.stop_camera(); - self.camera_content = None; - on_result(&result); + /// Draw [`Modal`] content. + pub fn modal_ui( + &mut self, + ui: &mut egui::Ui, + cb: &dyn PlatformCallbacks, + mut on_result: impl FnMut(&QrScanResult), + ) { + // Show scan result if exists or show camera content while scanning. + if let Some(result) = &self.qr_scan_result.clone() { + Self::result_ui( + ui, + result, + cb, + || { + Modal::close(); + }, + || { + self.qr_scan_result = None; + cb.start_camera(); + Modal::set_title(t!("scan_qr")); + }, + ); + } else if let Some(camera_content) = self.camera_content.as_mut() { + if let Some(result) = camera_content.qr_scan_result() { + cb.stop_camera(); + self.camera_content = None; + on_result(&result); - // Set result and rename modal title. - self.qr_scan_result = Some(result); - Modal::set_title(t!("scan_result")); - } else { - // Draw camera content. - ui.add_space(6.0); - self.camera_content.as_mut().unwrap().ui(ui, cb); - ui.add_space(12.0); - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - cb.stop_camera(); - self.camera_content = None; - Modal::close(); - }); - }); - } - } - ui.add_space(6.0); - } + // Set result and rename modal title. + self.qr_scan_result = Some(result); + Modal::set_title(t!("scan_result")); + } else { + // Draw camera content. + ui.add_space(6.0); + self.camera_content.as_mut().unwrap().ui(ui, cb); + ui.add_space(12.0); + ui.vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + cb.stop_camera(); + self.camera_content = None; + Modal::close(); + }, + ); + }); + } + } + ui.add_space(6.0); + } - /// Draw scan result content. - pub fn result_ui(ui: &mut egui::Ui, - result: &QrScanResult, - cb: &dyn PlatformCallbacks, - on_close: impl FnOnce(), - on_repeat: impl FnOnce()) { - let mut result_text = result.text(); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(3.0); - ScrollArea::vertical() - .id_salt(Id::from("qr_scan_result_input")) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .max_height(128.0) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(7.0); - egui::TextEdit::multiline(&mut result_text) - .font(egui::TextStyle::Small) - .desired_rows(5) - .interactive(false) - .desired_width(f32::INFINITY) - .show(ui); - ui.add_space(6.0); - }); - ui.add_space(2.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(10.0); + /// Draw scan result content. + pub fn result_ui( + ui: &mut egui::Ui, + result: &QrScanResult, + cb: &dyn PlatformCallbacks, + on_close: impl FnOnce(), + on_repeat: impl FnOnce(), + ) { + let mut result_text = result.text(); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(3.0); + ScrollArea::vertical() + .id_salt(Id::from("qr_scan_result_input")) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(128.0) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + egui::TextEdit::multiline(&mut result_text) + .font(egui::TextStyle::Small) + .desired_rows(5) + .interactive(false) + .desired_width(f32::INFINITY) + .show(ui); + ui.add_space(6.0); + }); + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(10.0); - // Show copy button. - ui.vertical_centered(|ui| { - let copy_text = format!("{} {}", COPY, t!("copy")); - View::button(ui, copy_text, Colors::white_or_black(false), || { - cb.copy_string_to_buffer(result_text.to_string()); - }); - }); - ui.add_space(10.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + // Show copy button. + ui.vertical_centered(|ui| { + let copy_text = format!("{} {}", COPY, t!("copy")); + View::button(ui, copy_text, Colors::white_or_black(false), || { + cb.copy_string_to_buffer(result_text.to_string()); + }); + }); + ui.add_space(10.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - on_close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("repeat"), Colors::white_or_black(false), || { - on_repeat(); - }); - }); - }); - } -} \ No newline at end of file + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + on_close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("repeat"), Colors::white_or_black(false), || { + on_repeat(); + }); + }); + }); + } +} diff --git a/src/gui/views/settings/content.rs b/src/gui/views/settings/content.rs index c41036a4..f3822f0f 100644 --- a/src/gui/views/settings/content.rs +++ b/src/gui/views/settings/content.rs @@ -13,71 +13,71 @@ // limitations under the License. use crate::AppConfig; +use crate::gui::Colors; use crate::gui::icons::GLOBE_SIMPLE; use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::View; use crate::gui::views::settings::{InterfaceSettingsContent, NetworkSettingsContent}; use crate::gui::views::types::ContentContainer; -use crate::gui::views::View; -use crate::gui::Colors; /// Application settings content. pub struct SettingsContent { - /// User interface settings. - interface_settings: InterfaceSettingsContent, - /// Network communication settings. - network_settings: NetworkSettingsContent, - // tor_settings: TorSettingsContent, + /// User interface settings. + interface_settings: InterfaceSettingsContent, + /// Network communication settings. + network_settings: NetworkSettingsContent, + // tor_settings: TorSettingsContent, } impl Default for SettingsContent { - fn default() -> Self { - Self { - interface_settings: InterfaceSettingsContent::default(), - network_settings: NetworkSettingsContent::default(), - //tor_settings: TorSettingsContent::default(), - } - } + fn default() -> Self { + Self { + interface_settings: InterfaceSettingsContent::default(), + network_settings: NetworkSettingsContent::default(), + //tor_settings: TorSettingsContent::default(), + } + } } impl SettingsContent { - /// Draw application settings content. - pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - ui.add_space(5.0); - View::checkbox(ui, AppConfig::check_updates(), t!("check_updates"), || { - AppConfig::toggle_check_updates(); - }); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::stroke()); + /// Draw application settings content. + pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + ui.add_space(5.0); + View::checkbox(ui, AppConfig::check_updates(), t!("check_updates"), || { + AppConfig::toggle_check_updates(); + }); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::stroke()); - // Show interface settings. - self.interface_settings.ui(ui, cb); + // Show interface settings. + self.interface_settings.ui(ui, cb); - ui.add_space(8.0); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(6.0); + ui.add_space(8.0); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(6.0); - View::sub_title(ui, format!("{} {}", GLOBE_SIMPLE, t!("network.self"))); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(6.0); + View::sub_title(ui, format!("{} {}", GLOBE_SIMPLE, t!("network.self"))); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(6.0); - // Show network settings. - self.network_settings.ui(ui, cb); - ui.add_space(8.0); + // Show network settings. + self.network_settings.ui(ui, cb); + ui.add_space(8.0); - // Do not show Tor settings on Android. - // let os = OperatingSystem::from_target_os(); - // let show_tor = os != OperatingSystem::Android; - // if show_tor { - // View::horizontal_line(ui, Colors::stroke()); - // ui.add_space(6.0); - // - // View::sub_title(ui, format!("{} {}", CIRCLE_HALF, t!("transport.tor_network"))); - // View::horizontal_line(ui, Colors::stroke()); - // ui.add_space(6.0); - // - // // Show Tor settings. - // self.tor_settings.ui(ui, cb); - // ui.add_space(8.0); - // } - } -} \ No newline at end of file + // Do not show Tor settings on Android. + // let os = OperatingSystem::from_target_os(); + // let show_tor = os != OperatingSystem::Android; + // if show_tor { + // View::horizontal_line(ui, Colors::stroke()); + // ui.add_space(6.0); + // + // View::sub_title(ui, format!("{} {}", CIRCLE_HALF, t!("transport.tor_network"))); + // View::horizontal_line(ui, Colors::stroke()); + // ui.add_space(6.0); + // + // // Show Tor settings. + // self.tor_settings.ui(ui, cb); + // ui.add_space(8.0); + // } + } +} diff --git a/src/gui/views/settings/interface.rs b/src/gui/views/settings/interface.rs index a497228a..bdba0314 100644 --- a/src/gui/views/settings/interface.rs +++ b/src/gui/views/settings/interface.rs @@ -15,126 +15,135 @@ use eframe::epaint::RectShape; use egui::{Align, CursorIcon, Layout, RichText, Sense, StrokeKind, UiBuilder}; +use crate::AppConfig; +use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::ContentContainer; use crate::gui::views::{Modal, View}; -use crate::gui::Colors; -use crate::AppConfig; /// User interface settings content. pub struct InterfaceSettingsContent { - /// Current locale. - locale: String, + /// Current locale. + locale: String, } impl ContentContainer for InterfaceSettingsContent { - fn modal_ids(&self) -> Vec<&'static str> { vec![] } + fn modal_ids(&self) -> Vec<&'static str> { + vec![] + } - fn modal_ui(&mut self, _: &mut egui::Ui, _: &Modal, _: &dyn PlatformCallbacks) {} + fn modal_ui(&mut self, _: &mut egui::Ui, _: &Modal, _: &dyn PlatformCallbacks) {} - fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { - ui.add_space(5.0); + fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { + ui.add_space(5.0); - // Draw theme selection. - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("theme")).size(16.0).color(Colors::gray())); - }); + // Draw theme selection. + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("theme")).size(16.0).color(Colors::gray())); + }); - let saved_use_dark = AppConfig::dark_theme().unwrap_or(false); - let mut selected_use_dark = saved_use_dark; + let saved_use_dark = AppConfig::dark_theme().unwrap_or(false); + let mut selected_use_dark = saved_use_dark; - ui.add_space(8.0); - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - View::radio_value(ui, &mut selected_use_dark, false, t!("light")); - }); - columns[1].vertical_centered(|ui| { - View::radio_value(ui, &mut selected_use_dark, true, t!("dark")); - }) - }); - ui.add_space(14.0); - if saved_use_dark != selected_use_dark { - AppConfig::set_dark_theme(selected_use_dark); - crate::setup_visuals(ui.ctx()); - } + ui.add_space(8.0); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + View::radio_value(ui, &mut selected_use_dark, false, t!("light")); + }); + columns[1].vertical_centered(|ui| { + View::radio_value(ui, &mut selected_use_dark, true, t!("dark")); + }) + }); + ui.add_space(14.0); + if saved_use_dark != selected_use_dark { + AppConfig::set_dark_theme(selected_use_dark); + crate::setup_visuals(ui.ctx()); + } - // Draw language selection. - let locales = rust_i18n::available_locales!(); - for (index, locale) in locales.iter().enumerate() { - self.language_item_ui(locale, ui, index, locales.len()); - } - ui.add_space(4.0); - } + // Draw language selection. + let locales = rust_i18n::available_locales!(); + for (index, locale) in locales.iter().enumerate() { + self.language_item_ui(locale, ui, index, locales.len()); + } + ui.add_space(4.0); + } } impl Default for InterfaceSettingsContent { - fn default() -> Self { - let locale = if let Some(lang) = AppConfig::locale() { - lang - } else { - rust_i18n::locale().to_string() - }; - Self { - locale, - } - } + fn default() -> Self { + let locale = if let Some(lang) = AppConfig::locale() { + lang + } else { + rust_i18n::locale().to_string() + }; + Self { locale } + } } impl InterfaceSettingsContent { - /// Draw language selection item content. - fn language_item_ui(&mut self, locale: &str, ui: &mut egui::Ui, index: usize, len: usize) { - let is_current = self.locale == locale; - // Draw round background. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(50.0); - let r = View::item_rounding(index, len, false); - let bg = if is_current { - Colors::fill() - } else { - Colors::fill_lite() - }; - let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); - let bg_idx = ui.painter().add(bg_shape.clone()); + /// Draw language selection item content. + fn language_item_ui(&mut self, locale: &str, ui: &mut egui::Ui, index: usize, len: usize) { + let is_current = self.locale == locale; + // Draw round background. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(50.0); + let r = View::item_rounding(index, len, false); + let bg = if is_current { + Colors::fill() + } else { + Colors::fill_lite() + }; + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - let res = ui.scope_builder( - UiBuilder::new() - .sense(Sense::click()) - .layout(Layout::right_to_left(Align::Center)) - .max_rect(rect), |ui| { - if is_current { - View::selected_item_check(ui); - } - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(12.0); - ui.vertical(|ui| { - // Draw language name. - ui.add_space(12.0); - let color = if is_current { - Colors::title(false) - } else { - Colors::gray() - }; - ui.label(RichText::new(t!("lang_name", locale = locale)) - .size(17.0) - .color(color)); - ui.add_space(14.0); - }); - }); - } - ).response; - let clicked = res.clicked() || res.long_touched(); - // Setup background and cursor. - if res.hovered() && !is_current { - res.on_hover_cursor(CursorIcon::PointingHand); - bg_shape.fill = Colors::fill(); - } - ui.painter().set(bg_idx, bg_shape); - // Handle clicks on layout. - if clicked && !is_current { - rust_i18n::set_locale(locale); - AppConfig::save_locale(locale); - self.locale = locale.to_string(); - } - } -} \ No newline at end of file + let res = ui + .scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), + |ui| { + if is_current { + View::selected_item_check(ui); + } + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout( + layout_size, + Layout::left_to_right(Align::Center), + |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + // Draw language name. + ui.add_space(12.0); + let color = if is_current { + Colors::title(false) + } else { + Colors::gray() + }; + ui.label( + RichText::new(t!("lang_name", locale = locale)) + .size(17.0) + .color(color), + ); + ui.add_space(14.0); + }); + }, + ); + }, + ) + .response; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if res.hovered() && !is_current { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked && !is_current { + rust_i18n::set_locale(locale); + AppConfig::save_locale(locale); + self.locale = locale.to_string(); + } + } +} diff --git a/src/gui/views/settings/mod.rs b/src/gui/views/settings/mod.rs index 64e38531..91619cca 100644 --- a/src/gui/views/settings/mod.rs +++ b/src/gui/views/settings/mod.rs @@ -22,4 +22,4 @@ mod network; pub use network::*; mod tor; -pub use tor::*; \ No newline at end of file +pub use tor::*; diff --git a/src/gui/views/settings/network.rs b/src/gui/views/settings/network.rs index 465aa10b..68aebeec 100644 --- a/src/gui/views/settings/network.rs +++ b/src/gui/views/settings/network.rs @@ -16,276 +16,296 @@ use eframe::epaint::RectShape; use egui::{Align, CursorIcon, Id, Layout, RichText, Sense, StrokeKind, UiBuilder}; use url::Url; +use crate::AppConfig; +use crate::gui::Colors; use crate::gui::icons::{CLOUD_CHECK, CLOUD_SLASH}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::{ContentContainer, ModalPosition}; use crate::gui::views::{Modal, TextEdit, View}; -use crate::gui::Colors; -use crate::AppConfig; /// Network communication settings content. pub struct NetworkSettingsContent { - /// Proxy URL input value for [`Modal`]. - proxy_url_edit: String, - /// Flag to check if entered proxy address was correct. - proxy_url_error: bool, + /// Proxy URL input value for [`Modal`]. + proxy_url_edit: String, + /// Flag to check if entered proxy address was correct. + proxy_url_error: bool, } /// Identifier for proxy URL edit [`Modal`]. const PROXY_URL_EDIT_MODAL: &'static str = "settings_proxy_edit_modal"; impl ContentContainer for NetworkSettingsContent { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - PROXY_URL_EDIT_MODAL - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![PROXY_URL_EDIT_MODAL] + } - fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - match modal.id { - PROXY_URL_EDIT_MODAL => self.proxy_modal_ui(ui, cb), - _ => {} - } - } + fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + match modal.id { + PROXY_URL_EDIT_MODAL => self.proxy_modal_ui(ui, cb), + _ => {} + } + } - fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { - let use_proxy = AppConfig::use_proxy(); - View::checkbox(ui, use_proxy, t!("app_settings.proxy"), || { - // Show edit modal when both URLs are empty. - if AppConfig::http_proxy_url().is_none() && AppConfig::socks_proxy_url().is_none() && - !use_proxy { - Modal::new(PROXY_URL_EDIT_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("app_settings.proxy")) - .show(); - } else { - AppConfig::toggle_use_proxy(); - } - }); - if !use_proxy { - ui.add_space(4.0); - ui.label(RichText::new(t!("app_settings.proxy_desc")) - .size(16.0) - .color(Colors::inactive_text()) - ); - ui.add_space(8.0); - } else { - ui.add_space(8.0); + fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { + let use_proxy = AppConfig::use_proxy(); + View::checkbox(ui, use_proxy, t!("app_settings.proxy"), || { + // Show edit modal when both URLs are empty. + if AppConfig::http_proxy_url().is_none() + && AppConfig::socks_proxy_url().is_none() + && !use_proxy + { + Modal::new(PROXY_URL_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("app_settings.proxy")) + .show(); + } else { + AppConfig::toggle_use_proxy(); + } + }); + if !use_proxy { + ui.add_space(4.0); + ui.label( + RichText::new(t!("app_settings.proxy_desc")) + .size(16.0) + .color(Colors::inactive_text()), + ); + ui.add_space(8.0); + } else { + ui.add_space(8.0); - // Draw proxy type selection. - Self::proxy_type_ui(ui); + // Draw proxy type selection. + Self::proxy_type_ui(ui); - // Draw proxy URL info. - self.proxy_item_ui(ui); - ui.add_space(6.0); - } - } + // Draw proxy URL info. + self.proxy_item_ui(ui); + ui.add_space(6.0); + } + } } impl Default for NetworkSettingsContent { - fn default() -> Self { - Self { - proxy_url_edit: "".to_string(), - proxy_url_error: false, - } - } + fn default() -> Self { + Self { + proxy_url_edit: "".to_string(), + proxy_url_error: false, + } + } } impl NetworkSettingsContent { - /// Draw proxy edit modal content. - fn proxy_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut NetworkSettingsContent| { - let proxy = c.proxy_url_edit.trim().to_string(); - let use_socks = AppConfig::use_socks_proxy(); - // Clear value if empty. - if proxy.is_empty() { - if use_socks { - AppConfig::save_socks_proxy_url(None); - } else { - AppConfig::save_http_proxy_url(None); - } - Modal::close(); - return; - } - // Format URL. - let http = "http://"; - let socks = "socks5://"; - let url = if use_socks { - let p = proxy.replace(http, ""); - if !p.contains(socks) { - format!("{}{}", socks, p) - } else { - p - } - } else { - let p = proxy.replace(socks, ""); - if !p.contains(http) { - format!("{}{}", http, p) - } else { - p - } - }; - c.proxy_url_error = Url::parse(url.as_str()).is_err(); - if !c.proxy_url_error { - // Save result when no error. - if !AppConfig::use_proxy() { - AppConfig::toggle_use_proxy(); - } - if use_socks { - AppConfig::save_socks_proxy_url(Some(url)) - } else { - AppConfig::save_http_proxy_url(Some(url)); - } - Modal::close(); - } - }; + /// Draw proxy edit modal content. + fn proxy_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut NetworkSettingsContent| { + let proxy = c.proxy_url_edit.trim().to_string(); + let use_socks = AppConfig::use_socks_proxy(); + // Clear value if empty. + if proxy.is_empty() { + if use_socks { + AppConfig::save_socks_proxy_url(None); + } else { + AppConfig::save_http_proxy_url(None); + } + Modal::close(); + return; + } + // Format URL. + let http = "http://"; + let socks = "socks5://"; + let url = if use_socks { + let p = proxy.replace(http, ""); + if !p.contains(socks) { + format!("{}{}", socks, p) + } else { + p + } + } else { + let p = proxy.replace(socks, ""); + if !p.contains(http) { + format!("{}{}", http, p) + } else { + p + } + }; + c.proxy_url_error = Url::parse(url.as_str()).is_err(); + if !c.proxy_url_error { + // Save result when no error. + if !AppConfig::use_proxy() { + AppConfig::toggle_use_proxy(); + } + if use_socks { + AppConfig::save_socks_proxy_url(Some(url)) + } else { + AppConfig::save_http_proxy_url(Some(url)); + } + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - let label = format!("{}:", t!("enter_url")); - ui.label(RichText::new(label).size(17.0).color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let label = format!("{}:", t!("enter_url")); + ui.label(RichText::new(label).size(17.0).color(Colors::gray())); + ui.add_space(8.0); - // Draw proxy URL text edit. - let mut edit = TextEdit::new( - Id::from("proxy_url_edit") - .with(PROXY_URL_EDIT_MODAL) - .with(if AppConfig::use_proxy() { - "socks5" - } else { - "http" - }) - ).paste(); - edit.ui(ui, &mut self.proxy_url_edit, cb); - if edit.enter_pressed { - on_save(self); - } + // Draw proxy URL text edit. + let mut edit = + TextEdit::new(Id::from("proxy_url_edit").with(PROXY_URL_EDIT_MODAL).with( + if AppConfig::use_proxy() { + "socks5" + } else { + "http" + }, + )) + .paste(); + edit.ui(ui, &mut self.proxy_url_edit, cb); + if edit.enter_pressed { + on_save(self); + } - // Show error when specified address is incorrect. - if self.proxy_url_error { - ui.add_space(10.0); - ui.label(RichText::new(t!("wallets.invalid_url")) - .size(16.0) - .color(Colors::red())); - } - ui.add_space(12.0); + // Show error when specified address is incorrect. + if self.proxy_url_error { + ui.add_space(10.0); + ui.label( + RichText::new(t!("wallets.invalid_url")) + .size(16.0) + .color(Colors::red()), + ); + } + ui.add_space(12.0); - // Show type selection when both URLs are empty. - if AppConfig::socks_proxy_url().is_none() && AppConfig::http_proxy_url().is_none() { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - Self::proxy_type_ui(ui); - }); - ui.add_space(4.0); - } + // Show type selection when both URLs are empty. + if AppConfig::socks_proxy_url().is_none() && AppConfig::http_proxy_url().is_none() { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + Self::proxy_type_ui(ui); + }); + ui.add_space(4.0); + } - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } - /// Draw proxy item content. - fn proxy_item_ui(&mut self, ui: &mut egui::Ui) { - // Draw round background. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(56.0); - let r = View::item_rounding(0, 1, false); - let bg = Colors::fill_lite(); - let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); - let bg_idx = ui.painter().add(bg_shape.clone()); + /// Draw proxy item content. + fn proxy_item_ui(&mut self, ui: &mut egui::Ui) { + // Draw round background. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(56.0); + let r = View::item_rounding(0, 1, false); + let bg = Colors::fill_lite(); + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - let res = ui.scope_builder( - UiBuilder::new() - .sense(Sense::click()) - .layout(Layout::right_to_left(Align::Center)) - .max_rect(rect), |ui| { - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(12.0); - ui.vertical(|ui| { - ui.add_space(4.0); - let use_socks = AppConfig::use_socks_proxy(); - let proxy_url = if use_socks { - AppConfig::socks_proxy_url() - } else { - AppConfig::http_proxy_url() - }; - let (url, color, icon, text) = if let Some(url) = proxy_url { - (url, Colors::title(false), CLOUD_CHECK, t!("network_settings.enabled")) - } else { - ( - t!("enter_url").into(), - Colors::inactive_text(), - CLOUD_SLASH, - t!("network_settings.disabled") - ) - }; - View::ellipsize_text(ui, url, 18.0, color); - ui.add_space(1.0); + let res = ui + .scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), + |ui| { + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout( + layout_size, + Layout::left_to_right(Align::Center), + |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + let use_socks = AppConfig::use_socks_proxy(); + let proxy_url = if use_socks { + AppConfig::socks_proxy_url() + } else { + AppConfig::http_proxy_url() + }; + let (url, color, icon, text) = if let Some(url) = proxy_url { + ( + url, + Colors::title(false), + CLOUD_CHECK, + t!("network_settings.enabled"), + ) + } else { + ( + t!("enter_url").into(), + Colors::inactive_text(), + CLOUD_SLASH, + t!("network_settings.disabled"), + ) + }; + View::ellipsize_text(ui, url, 18.0, color); + ui.add_space(1.0); - let value = format!("{} {}", icon, text); - ui.label(RichText::new(value).size(15.0).color(Colors::gray())); - ui.add_space(3.0); - }); - }); - } - ).response; - let clicked = res.clicked() || res.long_touched(); - // Setup background and cursor. - if res.hovered() { - res.on_hover_cursor(CursorIcon::PointingHand); - bg_shape.fill = Colors::fill(); - } - ui.painter().set(bg_idx, bg_shape); - // Handle clicks on layout. - if clicked { - let url = if AppConfig::use_socks_proxy() { - AppConfig::socks_proxy_url().unwrap_or("".to_string()) - } else { - AppConfig::http_proxy_url().unwrap_or("".to_string()) - }; - self.proxy_url_edit = url; - // Show proxy URL edit modal. - Modal::new(PROXY_URL_EDIT_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("app_settings.proxy")) - .show(); - } - } + let value = format!("{} {}", icon, text); + ui.label(RichText::new(value).size(15.0).color(Colors::gray())); + ui.add_space(3.0); + }); + }, + ); + }, + ) + .response; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked { + let url = if AppConfig::use_socks_proxy() { + AppConfig::socks_proxy_url().unwrap_or("".to_string()) + } else { + AppConfig::http_proxy_url().unwrap_or("".to_string()) + }; + self.proxy_url_edit = url; + // Show proxy URL edit modal. + Modal::new(PROXY_URL_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("app_settings.proxy")) + .show(); + } + } - /// Draw proxy type selection. - fn proxy_type_ui(ui: &mut egui::Ui) { - // Draw proxy type selection. - let saved_use_socks = AppConfig::use_socks_proxy(); - let mut selected_use_socks = saved_use_socks; - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - View::radio_value(ui, &mut selected_use_socks, true, "SOCKS5".to_string()); - }); - columns[1].vertical_centered(|ui| { - View::radio_value(ui, &mut selected_use_socks, false, "HTTP".to_string()); - }) - }); - ui.add_space(14.0); - if saved_use_socks != selected_use_socks { - AppConfig::toggle_use_socks_proxy(); - } - } -} \ No newline at end of file + /// Draw proxy type selection. + fn proxy_type_ui(ui: &mut egui::Ui) { + // Draw proxy type selection. + let saved_use_socks = AppConfig::use_socks_proxy(); + let mut selected_use_socks = saved_use_socks; + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + View::radio_value(ui, &mut selected_use_socks, true, "SOCKS5".to_string()); + }); + columns[1].vertical_centered(|ui| { + View::radio_value(ui, &mut selected_use_socks, false, "HTTP".to_string()); + }) + }); + ui.add_space(14.0); + if saved_use_socks != selected_use_socks { + AppConfig::toggle_use_socks_proxy(); + } + } +} diff --git a/src/gui/views/settings/tor.rs b/src/gui/views/settings/tor.rs index c36d13d7..dcf55025 100644 --- a/src/gui/views/settings/tor.rs +++ b/src/gui/views/settings/tor.rs @@ -18,32 +18,34 @@ use egui::{Align, CursorIcon, Id, Layout, RichText, ScrollArea, Sense, StrokeKin use std::fs; use url::Url; +use crate::gui::Colors; use crate::gui::icons::{CLIPBOARD_TEXT, CLOUD_CHECK, NOTCHES, PENCIL, SCAN, TERMINAL}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::{ContentContainer, ModalPosition}; -use crate::gui::views::{CameraScanContent, FilePickContent, FilePickContentType, Modal, TextEdit, View}; -use crate::gui::Colors; +use crate::gui::views::{ + CameraScanContent, FilePickContent, FilePickContentType, Modal, TextEdit, View, +}; use crate::tor::{TorBridge, TorConfig, TorProxy}; /// Transport settings content. pub struct TorSettingsContent { - /// Flag to check if settings were changed. - pub settings_changed: bool, + /// Flag to check if settings were changed. + pub settings_changed: bool, - /// Proxy URL input value for [`Modal`]. - proxy_url_edit: String, - /// Flag to check if entered proxy address was correct. - proxy_url_error: bool, + /// Proxy URL input value for [`Modal`]. + proxy_url_edit: String, + /// Flag to check if entered proxy address was correct. + proxy_url_error: bool, - /// Tor bridge binary path value for [`Modal`]. - bridge_bin_path_edit: String, - /// Button to pick binary file for bridge. - bridge_bin_pick_file: FilePickContent, + /// Tor bridge binary path value for [`Modal`]. + bridge_bin_path_edit: String, + /// Button to pick binary file for bridge. + bridge_bin_pick_file: FilePickContent, - /// Tor bridge connection line value for [`Modal`]. - bridge_conn_line_edit: String, - /// Bridge line QR code scanner [`Modal`] content. - bridge_qr_scan_content: Option, + /// Tor bridge connection line value for [`Modal`]. + bridge_conn_line_edit: String, + /// Bridge line QR code scanner [`Modal`] content. + bridge_qr_scan_content: Option, } /// Identifier for proxy URL edit [`Modal`]. @@ -56,577 +58,621 @@ const BRIDGE_CONN_LINE_EDIT_MODAL: &'static str = "bridge_conn_line_edit_modal"; const SCAN_BRIDGE_CONN_LINE_MODAL: &'static str = "scan_bridge_conn_line_modal"; impl Default for TorSettingsContent { - fn default() -> Self { - // Setup Tor bridge binary path edit text. - let bridge = TorConfig::get_bridge(); - let (bin_path, conn_line) = if let Some(b) = bridge { - (b.binary_path(), b.connection_line()) - } else { - ("".to_string(), "".to_string()) - }; - Self { - settings_changed: false, - proxy_url_edit: "".to_string(), - proxy_url_error: false, - bridge_bin_path_edit: bin_path, - bridge_bin_pick_file: FilePickContent::new( - FilePickContentType::ItemButton(View::item_rounding(0, 1, true)) - ).no_parse(), - bridge_conn_line_edit: conn_line, - bridge_qr_scan_content: None, - } - } + fn default() -> Self { + // Setup Tor bridge binary path edit text. + let bridge = TorConfig::get_bridge(); + let (bin_path, conn_line) = if let Some(b) = bridge { + (b.binary_path(), b.connection_line()) + } else { + ("".to_string(), "".to_string()) + }; + Self { + settings_changed: false, + proxy_url_edit: "".to_string(), + proxy_url_error: false, + bridge_bin_path_edit: bin_path, + bridge_bin_pick_file: FilePickContent::new(FilePickContentType::ItemButton( + View::item_rounding(0, 1, true), + )) + .no_parse(), + bridge_conn_line_edit: conn_line, + bridge_qr_scan_content: None, + } + } } impl ContentContainer for TorSettingsContent { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - PROXY_URL_EDIT_MODAL, - BRIDGE_BIN_EDIT_MODAL, - BRIDGE_CONN_LINE_EDIT_MODAL, - SCAN_BRIDGE_CONN_LINE_MODAL - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![ + PROXY_URL_EDIT_MODAL, + BRIDGE_BIN_EDIT_MODAL, + BRIDGE_CONN_LINE_EDIT_MODAL, + SCAN_BRIDGE_CONN_LINE_MODAL, + ] + } - fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - match modal.id { - PROXY_URL_EDIT_MODAL => self.proxy_modal_ui(ui, cb), - BRIDGE_BIN_EDIT_MODAL => self.bridge_bin_edit_modal_ui(ui, cb), - BRIDGE_CONN_LINE_EDIT_MODAL => self.bridge_conn_line_edit_modal_ui(ui, cb), - SCAN_BRIDGE_CONN_LINE_MODAL => { - if let Some(content) = self.bridge_qr_scan_content.as_mut() { - let mut close = false; - content.modal_ui(ui, cb, |res| { - // Save connection line after scanning. - let line = res.text(); - let bridge = TorConfig::get_bridge().unwrap(); - if bridge.connection_line() != line { - TorBridge::save_bridge_conn_line(&bridge, line); - self.settings_changed = true; - } - close = true; - }); - if close { - self.bridge_qr_scan_content = None; - cb.stop_camera(); - Modal::close(); - } - } - } - _ => {} - } - } + fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + match modal.id { + PROXY_URL_EDIT_MODAL => self.proxy_modal_ui(ui, cb), + BRIDGE_BIN_EDIT_MODAL => self.bridge_bin_edit_modal_ui(ui, cb), + BRIDGE_CONN_LINE_EDIT_MODAL => self.bridge_conn_line_edit_modal_ui(ui, cb), + SCAN_BRIDGE_CONN_LINE_MODAL => { + if let Some(content) = self.bridge_qr_scan_content.as_mut() { + let mut close = false; + content.modal_ui(ui, cb, |res| { + // Save connection line after scanning. + let line = res.text(); + let bridge = TorConfig::get_bridge().unwrap(); + if bridge.connection_line() != line { + TorBridge::save_bridge_conn_line(&bridge, line); + self.settings_changed = true; + } + close = true; + }); + if close { + self.bridge_qr_scan_content = None; + cb.stop_camera(); + Modal::close(); + } + } + } + _ => {} + } + } - fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - ui.label(RichText::new(format!("{}:", t!("wallets.conn_method"))) - .size(17.0) - .color(Colors::inactive_text())); - ui.add_space(10.0); + fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + ui.label( + RichText::new(format!("{}:", t!("wallets.conn_method"))) + .size(17.0) + .color(Colors::inactive_text()), + ); + ui.add_space(10.0); - let mut proxy = TorConfig::get_proxy(); - let current_proxy = proxy.clone(); - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - let name = t!("network_settings.default"); - View::radio_value(ui, &mut proxy, None, name); - }); - columns[1].vertical_centered(|ui| { - let name = t!("app_settings.proxy"); - let val = current_proxy.clone() - .unwrap_or(TorProxy::SOCKS5(TorProxy::DEFAULT_SOCKS5_URL.to_string())); - View::radio_value(ui, &mut proxy, Some(val), name); - }); - }); - ui.add_space(14.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + let mut proxy = TorConfig::get_proxy(); + let current_proxy = proxy.clone(); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + let name = t!("network_settings.default"); + View::radio_value(ui, &mut proxy, None, name); + }); + columns[1].vertical_centered(|ui| { + let name = t!("app_settings.proxy"); + let val = current_proxy + .clone() + .unwrap_or(TorProxy::SOCKS5(TorProxy::DEFAULT_SOCKS5_URL.to_string())); + View::radio_value(ui, &mut proxy, Some(val), name); + }); + }); + ui.add_space(14.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - if let Some(p) = proxy.as_mut() { - ui.label(RichText::new(format!("{}:", t!("app_settings.proxy"))) - .size(17.0) - .color(Colors::inactive_text())); - ui.add_space(10.0); - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - let value = TorConfig::get_socks5_proxy(); - View::radio_value(ui, p, value, "SOCKS5".to_string()); - }); - columns[1].vertical_centered(|ui| { - let value = TorConfig::get_http_proxy(); - View::radio_value(ui, p, value, "HTTP".to_string()); - }); - }); - ui.add_space(14.0); - // Show proxy settings. - self.proxy_item_ui(p.url(), ui); - ui.add_space(8.0); - } + if let Some(p) = proxy.as_mut() { + ui.label( + RichText::new(format!("{}:", t!("app_settings.proxy"))) + .size(17.0) + .color(Colors::inactive_text()), + ); + ui.add_space(10.0); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + let value = TorConfig::get_socks5_proxy(); + View::radio_value(ui, p, value, "SOCKS5".to_string()); + }); + columns[1].vertical_centered(|ui| { + let value = TorConfig::get_http_proxy(); + View::radio_value(ui, p, value, "HTTP".to_string()); + }); + }); + ui.add_space(14.0); + // Show proxy settings. + self.proxy_item_ui(p.url(), ui); + ui.add_space(8.0); + } - // Check if proxy type was changed to save. - if current_proxy != proxy { - TorConfig::save_proxy(proxy.clone()); - self.settings_changed = true; - } - if proxy.is_some() { - return; - } + // Check if proxy type was changed to save. + if current_proxy != proxy { + TorConfig::save_proxy(proxy.clone()); + self.settings_changed = true; + } + if proxy.is_some() { + return; + } - let bridge = TorConfig::get_bridge(); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("transport.bridges_desc")) - .size(17.0) - .color(Colors::inactive_text())); + let bridge = TorConfig::get_bridge(); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("transport.bridges_desc")) + .size(17.0) + .color(Colors::inactive_text()), + ); - // Draw checkbox to enable/disable bridges. - View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || { - let value = if bridge.is_some() { - None - } else { - let default_bridge = TorConfig::get_webtunnel(); - self.bridge_bin_path_edit = default_bridge.binary_path(); - self.bridge_conn_line_edit = default_bridge.connection_line(); - Some(default_bridge) - }; - TorConfig::save_bridge(value); - self.settings_changed = true; - }); - }); + // Draw checkbox to enable/disable bridges. + View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || { + let value = if bridge.is_some() { + None + } else { + let default_bridge = TorConfig::get_webtunnel(); + self.bridge_bin_path_edit = default_bridge.binary_path(); + self.bridge_conn_line_edit = default_bridge.connection_line(); + Some(default_bridge) + }; + TorConfig::save_bridge(value); + self.settings_changed = true; + }); + }); - if bridge.is_some() { - ui.add_space(6.0); - // Show bridge selection for desktop. - if View::is_desktop() { - let current_bridge = bridge.unwrap(); - let mut bridge = current_bridge.clone(); + if bridge.is_some() { + ui.add_space(6.0); + // Show bridge selection for desktop. + if View::is_desktop() { + let current_bridge = bridge.unwrap(); + let mut bridge = current_bridge.clone(); - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - // Show Webtunnel bridge selector. - let webtunnel = TorConfig::get_webtunnel(); - let name = webtunnel.protocol_name().to_uppercase(); - View::radio_value(ui, &mut bridge, webtunnel, name); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + // Show Webtunnel bridge selector. + let webtunnel = TorConfig::get_webtunnel(); + let name = webtunnel.protocol_name().to_uppercase(); + View::radio_value(ui, &mut bridge, webtunnel, name); + }); + columns[1].vertical_centered(|ui| { + // Show Obfs4 bridge selector. + let obfs4 = TorConfig::get_obfs4(); + let name = obfs4.protocol_name().to_uppercase(); + View::radio_value(ui, &mut bridge, obfs4, name); + }); + }); + ui.add_space(10.0); + ui.vertical_centered(|ui| { + // Show Snowflake bridge selector. + let snowflake = TorConfig::get_snowflake(); + let name = snowflake.protocol_name().to_uppercase(); + View::radio_value(ui, &mut bridge, snowflake, name); + }); + ui.add_space(16.0); - }); - columns[1].vertical_centered(|ui| { - // Show Obfs4 bridge selector. - let obfs4 = TorConfig::get_obfs4(); - let name = obfs4.protocol_name().to_uppercase(); - View::radio_value(ui, &mut bridge, obfs4, name); - }); - }); - ui.add_space(10.0); - ui.vertical_centered(|ui| { - // Show Snowflake bridge selector. - let snowflake = TorConfig::get_snowflake(); - let name = snowflake.protocol_name().to_uppercase(); - View::radio_value(ui, &mut bridge, snowflake, name); - }); - ui.add_space(16.0); + // Check if bridge type was changed to save. + if current_bridge != bridge { + TorConfig::save_bridge(Some(bridge.clone())); + self.bridge_bin_path_edit = bridge.binary_path(); + self.bridge_conn_line_edit = bridge.connection_line(); + self.settings_changed = true; + } + } - // Check if bridge type was changed to save. - if current_bridge != bridge { - TorConfig::save_bridge(Some(bridge.clone())); - self.bridge_bin_path_edit = bridge.binary_path(); - self.bridge_conn_line_edit = bridge.connection_line(); - self.settings_changed = true; - } - } + if let Some(br) = TorConfig::get_bridge().as_ref() { + // Show bridge connection line setup. + self.conn_line_ui(ui, br, cb); + // Show bridge binary setup for desktop. + if View::is_desktop() { + self.bridge_bin_ui(ui, br, cb); + } + } - if let Some(br) = TorConfig::get_bridge().as_ref() { - // Show bridge connection line setup. - self.conn_line_ui(ui, br, cb); - // Show bridge binary setup for desktop. - if View::is_desktop() { - self.bridge_bin_ui(ui, br, cb); - } - } - - ui.add_space(8.0); - } - } + ui.add_space(8.0); + } + } } impl TorSettingsContent { - /// Draw proxy edit modal content. - fn proxy_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut TorSettingsContent| { - let http = "http://"; - let socks = "socks5://"; - let url = c.proxy_url_edit.trim().to_string(); - c.proxy_url_error = Url::parse(url.as_str()).is_err(); - if !c.proxy_url_error { - let proxy = TorConfig::get_proxy().unwrap(); - if url.contains(socks) { - TorConfig::save_proxy(Some(TorProxy::SOCKS5(url))); - } else if url.contains(http) { - TorConfig::save_proxy(Some(TorProxy::HTTP(url))); - } else { - match proxy { - TorProxy::SOCKS5(_) => { - TorConfig::save_proxy(Some(TorProxy::SOCKS5(url))); - } - TorProxy::HTTP(_) => { - TorConfig::save_proxy(Some(TorProxy::HTTP(url))); - } - } - } - Modal::close(); - } - }; + /// Draw proxy edit modal content. + fn proxy_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut TorSettingsContent| { + let http = "http://"; + let socks = "socks5://"; + let url = c.proxy_url_edit.trim().to_string(); + c.proxy_url_error = Url::parse(url.as_str()).is_err(); + if !c.proxy_url_error { + let proxy = TorConfig::get_proxy().unwrap(); + if url.contains(socks) { + TorConfig::save_proxy(Some(TorProxy::SOCKS5(url))); + } else if url.contains(http) { + TorConfig::save_proxy(Some(TorProxy::HTTP(url))); + } else { + match proxy { + TorProxy::SOCKS5(_) => { + TorConfig::save_proxy(Some(TorProxy::SOCKS5(url))); + } + TorProxy::HTTP(_) => { + TorConfig::save_proxy(Some(TorProxy::HTTP(url))); + } + } + } + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - let label = format!("{}:", t!("enter_url")); - ui.label(RichText::new(label).size(17.0).color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let label = format!("{}:", t!("enter_url")); + ui.label(RichText::new(label).size(17.0).color(Colors::gray())); + ui.add_space(8.0); - // Draw proxy URL text edit. - let mut edit = TextEdit::new(Id::from("proxy_url_edit").with(PROXY_URL_EDIT_MODAL)) - .paste(); - edit.ui(ui, &mut self.proxy_url_edit, cb); - if edit.enter_pressed { - on_save(self); - } + // Draw proxy URL text edit. + let mut edit = + TextEdit::new(Id::from("proxy_url_edit").with(PROXY_URL_EDIT_MODAL)).paste(); + edit.ui(ui, &mut self.proxy_url_edit, cb); + if edit.enter_pressed { + on_save(self); + } - // Show error when specified address is incorrect. - if self.proxy_url_error { - ui.add_space(10.0); - ui.label(RichText::new(t!("wallets.invalid_url")) - .size(16.0) - .color(Colors::red())); - } - ui.add_space(12.0); + // Show error when specified address is incorrect. + if self.proxy_url_error { + ui.add_space(10.0); + ui.label( + RichText::new(t!("wallets.invalid_url")) + .size(16.0) + .color(Colors::red()), + ); + } + ui.add_space(12.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } - /// Draw proxy item content. - fn proxy_item_ui(&mut self, url: String, ui: &mut egui::Ui) { - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(56.0); + /// Draw proxy item content. + fn proxy_item_ui(&mut self, url: String, ui: &mut egui::Ui) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(56.0); - // Draw round background. - let bg_rect = rect.clone(); - let item_rounding = View::item_rounding(0, 1, false); - ui.painter().rect(bg_rect, - item_rounding, - Colors::fill(), - View::item_stroke(), - StrokeKind::Outside); + // Draw round background. + let bg_rect = rect.clone(); + let item_rounding = View::item_rounding(0, 1, false); + ui.painter().rect( + bg_rect, + item_rounding, + Colors::fill(), + View::item_stroke(), + StrokeKind::Outside, + ); - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - View::item_button(ui, View::item_rounding(0, 1, true), PENCIL, None, || { - self.proxy_url_edit = url.clone(); - // Show proxy URL edit modal. - Modal::new(PROXY_URL_EDIT_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("app_settings.proxy")) - .show(); - }); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(12.0); - ui.vertical(|ui| { - ui.add_space(4.0); - View::ellipsize_text(ui, url, 18.0, Colors::title(false)); - ui.add_space(1.0); + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + View::item_button(ui, View::item_rounding(0, 1, true), PENCIL, None, || { + self.proxy_url_edit = url.clone(); + // Show proxy URL edit modal. + Modal::new(PROXY_URL_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("app_settings.proxy")) + .show(); + }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + View::ellipsize_text(ui, url, 18.0, Colors::title(false)); + ui.add_space(1.0); - let value = format!("{} {}", CLOUD_CHECK, t!("network_settings.enabled")); - ui.label(RichText::new(value).size(15.0).color(Colors::gray())); - ui.add_space(3.0); - }); - }); - }); - } + let value = format!("{} {}", CLOUD_CHECK, t!("network_settings.enabled")); + ui.label(RichText::new(value).size(15.0).color(Colors::gray())); + ui.add_space(3.0); + }); + }); + }); + } - /// Draw bridge connection line setup content. - fn conn_line_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) { - // Draw round background. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(56.0); - let r = if View::is_desktop() { - View::item_rounding(0, 2, false) - } else { - View::item_rounding(0, 1, false) - }; - let bg = Colors::fill_lite(); - let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); - let bg_idx = ui.painter().add(bg_shape.clone()); + /// Draw bridge connection line setup content. + fn conn_line_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) { + // Draw round background. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(56.0); + let r = if View::is_desktop() { + View::item_rounding(0, 2, false) + } else { + View::item_rounding(0, 1, false) + }; + let bg = Colors::fill_lite(); + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - let res = ui.scope_builder( - UiBuilder::new() - .sense(Sense::click()) - .layout(Layout::right_to_left(Align::Center)) - .max_rect(rect), |ui| { - View::item_button(ui, View::item_rounding(0, 1, true), SCAN, None, || { - self.show_qr_scan_bridge_modal(cb); - }); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(12.0); - ui.vertical(|ui| { - ui.add_space(4.0); - let line_text = bridge.connection_line(); - View::ellipsize_text(ui, line_text, 18.0, Colors::title(false)); - ui.add_space(1.0); - let line_desc = t!("transport.conn_line").replace(":", ""); - let value = format!("{} {}", NOTCHES, line_desc); - ui.label(RichText::new(value).size(15.0).color(Colors::gray())); - ui.add_space(3.0); - }); - }); - } - ).response; - let clicked = res.clicked() || res.long_touched(); - // Setup background and cursor. - if res.hovered() { - res.on_hover_cursor(CursorIcon::PointingHand); - bg_shape.fill = Colors::fill(); - } - ui.painter().set(bg_idx, bg_shape); - // Handle clicks on layout. - if clicked { - self.bridge_conn_line_edit = bridge.connection_line(); - // Show connection line edit modal. - let title = bridge.protocol_name(); - Modal::new(BRIDGE_CONN_LINE_EDIT_MODAL) - .position(ModalPosition::Center) - .title(title) - .show(); - } - } + let res = ui + .scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), + |ui| { + View::item_button(ui, View::item_rounding(0, 1, true), SCAN, None, || { + self.show_qr_scan_bridge_modal(cb); + }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout( + layout_size, + Layout::left_to_right(Align::Center), + |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + let line_text = bridge.connection_line(); + View::ellipsize_text(ui, line_text, 18.0, Colors::title(false)); + ui.add_space(1.0); + let line_desc = t!("transport.conn_line").replace(":", ""); + let value = format!("{} {}", NOTCHES, line_desc); + ui.label(RichText::new(value).size(15.0).color(Colors::gray())); + ui.add_space(3.0); + }); + }, + ); + }, + ) + .response; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked { + self.bridge_conn_line_edit = bridge.connection_line(); + // Show connection line edit modal. + let title = bridge.protocol_name(); + Modal::new(BRIDGE_CONN_LINE_EDIT_MODAL) + .position(ModalPosition::Center) + .title(title) + .show(); + } + } - /// Show bridge connection line QR code scanner. - fn show_qr_scan_bridge_modal(&mut self, cb: &dyn PlatformCallbacks) { - self.bridge_qr_scan_content = Some(CameraScanContent::default()); - // Show QR code scan modal. - Modal::new(SCAN_BRIDGE_CONN_LINE_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("scan_qr")) - .closeable(false) - .show(); - cb.start_camera(); - } + /// Show bridge connection line QR code scanner. + fn show_qr_scan_bridge_modal(&mut self, cb: &dyn PlatformCallbacks) { + self.bridge_qr_scan_content = Some(CameraScanContent::default()); + // Show QR code scan modal. + Modal::new(SCAN_BRIDGE_CONN_LINE_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("scan_qr")) + .closeable(false) + .show(); + cb.start_camera(); + } - /// Draw bridge connection line input [`Modal`] content. - fn bridge_conn_line_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut TorSettingsContent| { - let bridge = TorConfig::get_bridge().unwrap(); - if bridge.connection_line() != c.bridge_conn_line_edit { - TorBridge::save_bridge_conn_line(&bridge, c.bridge_conn_line_edit.clone()); - c.settings_changed = true; - } - Modal::close(); - }; + /// Draw bridge connection line input [`Modal`] content. + fn bridge_conn_line_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut TorSettingsContent| { + let bridge = TorConfig::get_bridge().unwrap(); + if bridge.connection_line() != c.bridge_conn_line_edit { + TorBridge::save_bridge_conn_line(&bridge, c.bridge_conn_line_edit.clone()); + c.settings_changed = true; + } + Modal::close(); + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("transport.conn_line")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("transport.conn_line")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw connection line text edit. - ui.vertical_centered(|ui| { - let scroll_id = Id::from(BRIDGE_CONN_LINE_EDIT_MODAL); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(3.0); - ScrollArea::both() - .id_salt(scroll_id) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .max_height(128.0) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(7.0); - let input_id = scroll_id.with("_input"); - egui::TextEdit::multiline(&mut self.bridge_conn_line_edit) - .id(input_id) - .font(egui::TextStyle::Body) - .desired_rows(5) - .interactive(false) - .desired_width(f32::INFINITY) - .show(ui); - ui.add_space(6.0); - }); - }); + // Draw connection line text edit. + ui.vertical_centered(|ui| { + let scroll_id = Id::from(BRIDGE_CONN_LINE_EDIT_MODAL); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(3.0); + ScrollArea::both() + .id_salt(scroll_id) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(128.0) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + let input_id = scroll_id.with("_input"); + egui::TextEdit::multiline(&mut self.bridge_conn_line_edit) + .id(input_id) + .font(egui::TextStyle::Body) + .desired_rows(5) + .interactive(false) + .desired_width(f32::INFINITY) + .show(ui); + ui.add_space(6.0); + }); + }); - ui.add_space(2.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - // Draw paste button. - let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); - View::button(ui, paste_text, Colors::white_or_black(false), || { - self.bridge_conn_line_edit = cb.get_string_from_buffer(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Draw button to scan bridge QR code. - let scan_text = format!("{} {}", SCAN, t!("scan")); - View::button(ui, scan_text, Colors::white_or_black(false), || { - self.show_qr_scan_bridge_modal(cb); - }); - }); - }); + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + // Draw paste button. + let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); + View::button(ui, paste_text, Colors::white_or_black(false), || { + self.bridge_conn_line_edit = cb.get_string_from_buffer(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + // Draw button to scan bridge QR code. + let scan_text = format!("{} {}", SCAN, t!("scan")); + View::button(ui, scan_text, Colors::white_or_black(false), || { + self.show_qr_scan_bridge_modal(cb); + }); + }); + }); - ui.add_space(8.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); + ui.add_space(8.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } - /// Draw bridge binary setup content. - fn bridge_bin_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) { - // Draw round background. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(56.0); - let r = View::item_rounding(1, 2, false); - let bg = Colors::fill_lite(); - let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); - let bg_idx = ui.painter().add(bg_shape.clone()); + /// Draw bridge binary setup content. + fn bridge_bin_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) { + // Draw round background. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(56.0); + let r = View::item_rounding(1, 2, false); + let bg = Colors::fill_lite(); + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - let res = ui.scope_builder( - UiBuilder::new() - .sense(Sense::click()) - .layout(Layout::right_to_left(Align::Center)) - .max_rect(rect), |ui| { - self.bridge_bin_pick_file.ui(ui, cb, |path| { - if bridge.binary_path() != path { - TorBridge::save_bridge_bin_path(bridge, path); - self.settings_changed = true; - } - }); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(12.0); - ui.vertical(|ui| { - ui.add_space(4.0); - let bin_text = bridge.binary_path(); - View::ellipsize_text(ui, bin_text, 18.0, Colors::title(false)); - ui.add_space(1.0); - let bin_desc = t!("transport.bin_file").replace(":", ""); - let value = format!("{} {}", TERMINAL, bin_desc); - ui.label(RichText::new(value).size(15.0).color(Colors::gray())); - ui.add_space(3.0); - }); - }); - } - ).response; - let clicked = res.clicked() || res.long_touched(); - // Setup background and cursor. - if res.hovered() { - res.on_hover_cursor(CursorIcon::PointingHand); - bg_shape.fill = Colors::fill(); - } - ui.painter().set(bg_idx, bg_shape); - // Handle clicks on layout. - if clicked { - self.bridge_bin_path_edit = bridge.binary_path(); - // Show binary path edit modal. - let title = bridge.protocol_name(); - Modal::new(BRIDGE_BIN_EDIT_MODAL) - .position(ModalPosition::CenterTop) - .title(title) - .show(); - } - } + let res = ui + .scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), + |ui| { + self.bridge_bin_pick_file.ui(ui, cb, |path| { + if bridge.binary_path() != path { + TorBridge::save_bridge_bin_path(bridge, path); + self.settings_changed = true; + } + }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout( + layout_size, + Layout::left_to_right(Align::Center), + |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + let bin_text = bridge.binary_path(); + View::ellipsize_text(ui, bin_text, 18.0, Colors::title(false)); + ui.add_space(1.0); + let bin_desc = t!("transport.bin_file").replace(":", ""); + let value = format!("{} {}", TERMINAL, bin_desc); + ui.label(RichText::new(value).size(15.0).color(Colors::gray())); + ui.add_space(3.0); + }); + }, + ); + }, + ) + .response; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked { + self.bridge_bin_path_edit = bridge.binary_path(); + // Show binary path edit modal. + let title = bridge.protocol_name(); + Modal::new(BRIDGE_BIN_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(title) + .show(); + } + } - /// Draw bridge binary input [`Modal`] content. - fn bridge_bin_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut TorSettingsContent| { - let bridge = TorConfig::get_bridge().unwrap(); - let exists = fs::exists(&c.bridge_bin_path_edit).unwrap_or_default(); - if !exists { - return; - } - if bridge.binary_path() != c.bridge_bin_path_edit { - TorBridge::save_bridge_bin_path(&bridge, c.bridge_bin_path_edit.clone()); - c.settings_changed = true; - } - Modal::close(); - }; + /// Draw bridge binary input [`Modal`] content. + fn bridge_bin_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut TorSettingsContent| { + let bridge = TorConfig::get_bridge().unwrap(); + let exists = fs::exists(&c.bridge_bin_path_edit).unwrap_or_default(); + if !exists { + return; + } + if bridge.binary_path() != c.bridge_bin_path_edit { + TorBridge::save_bridge_bin_path(&bridge, c.bridge_bin_path_edit.clone()); + c.settings_changed = true; + } + Modal::close(); + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("transport.bin_file")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("transport.bin_file")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw bridge text edit. - let mut edit = TextEdit::new(Id::from(BRIDGE_BIN_EDIT_MODAL)).paste(); - edit.ui(ui, &mut self.bridge_bin_path_edit, cb); - if edit.enter_pressed { - on_save(self); - } - ui.add_space(12.0); + // Draw bridge text edit. + let mut edit = TextEdit::new(Id::from(BRIDGE_BIN_EDIT_MODAL)).paste(); + edit.ui(ui, &mut self.bridge_bin_path_edit, cb); + if edit.enter_pressed { + on_save(self); + } + ui.add_space(12.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - }); - } -} \ No newline at end of file + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } +} diff --git a/src/gui/views/title_panel.rs b/src/gui/views/title_panel.rs index be077c31..92cc03b1 100644 --- a/src/gui/views/title_panel.rs +++ b/src/gui/views/title_panel.rs @@ -12,117 +12,111 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{Margin, Id, Layout, Align, UiBuilder}; +use egui::{Align, Id, Layout, Margin, UiBuilder}; use crate::gui::Colors; -use crate::gui::views::{Content, View}; use crate::gui::views::types::{TitleContentType, TitleType}; +use crate::gui::views::{Content, View}; /// Title panel with left/right action buttons and text in the middle. pub struct TitlePanel { - /// Widget identifier. - id: Id + /// Widget identifier. + id: Id, } impl TitlePanel { - /// Content height. - pub const HEIGHT: f32 = 54.0; + /// Content height. + pub const HEIGHT: f32 = 54.0; - /// Create new title panel with provided identifier. - pub fn new(id: Id) -> Self { - Self { - id, - } - } + /// Create new title panel with provided identifier. + pub fn new(id: Id) -> Self { + Self { id } + } - pub fn ui(&self, - title: TitleType, - mut left_content: impl FnMut(&mut egui::Ui), - mut right_content: impl FnMut(&mut egui::Ui), - ui: &mut egui::Ui) { - // Draw title panel. - egui::TopBottomPanel::top(self.id) - .resizable(false) - .exact_height(Self::HEIGHT + View::get_top_inset()) - .frame(egui::Frame { - inner_margin: Margin { - left: View::far_left_inset_margin(ui) as i8, - right: View::far_right_inset_margin(ui) as i8, - top: View::get_top_inset() as i8, - bottom: 0.0 as i8, - }, - ..Default::default() - }) - .show_inside(ui, |ui| { - let rect = ui.available_rect_before_wrap(); - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |ui| { - ui.horizontal_centered(|ui| { - (right_content)(ui); - }); - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.horizontal_centered(|ui| { - (left_content)(ui); - }); - }); - match title { - TitleType::Single(content) => { - let content_rect = { - let mut r = rect.clone(); - r.min.x += Self::HEIGHT; - r.max.x -= Self::HEIGHT; - r - }; - ui.scope_builder(UiBuilder::new().max_rect(content_rect), |ui| { - Self::title_text_content(ui, content); - }); - } - TitleType::Dual(first, second) => { - let first_rect = { - let mut r = rect.clone(); - r.max.x = r.min.x + Content::SIDE_PANEL_WIDTH - Self::HEIGHT; - r.min.x += Self::HEIGHT; - r - }; - // Draw first title content. - ui.scope_builder(UiBuilder::new().max_rect(first_rect), |ui| { - Self::title_text_content(ui, first); - }); + pub fn ui( + &self, + title: TitleType, + mut left_content: impl FnMut(&mut egui::Ui), + mut right_content: impl FnMut(&mut egui::Ui), + ui: &mut egui::Ui, + ) { + // Draw title panel. + egui::TopBottomPanel::top(self.id) + .resizable(false) + .exact_height(Self::HEIGHT + View::get_top_inset()) + .frame(egui::Frame { + inner_margin: Margin { + left: View::far_left_inset_margin(ui) as i8, + right: View::far_right_inset_margin(ui) as i8, + top: View::get_top_inset() as i8, + bottom: 0.0 as i8, + }, + ..Default::default() + }) + .show_inside(ui, |ui| { + let rect = ui.available_rect_before_wrap(); + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |ui| { + ui.horizontal_centered(|ui| { + (right_content)(ui); + }); + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.horizontal_centered(|ui| { + (left_content)(ui); + }); + }); + match title { + TitleType::Single(content) => { + let content_rect = { + let mut r = rect.clone(); + r.min.x += Self::HEIGHT; + r.max.x -= Self::HEIGHT; + r + }; + ui.scope_builder(UiBuilder::new().max_rect(content_rect), |ui| { + Self::title_text_content(ui, content); + }); + } + TitleType::Dual(first, second) => { + let first_rect = { + let mut r = rect.clone(); + r.max.x = r.min.x + Content::SIDE_PANEL_WIDTH - Self::HEIGHT; + r.min.x += Self::HEIGHT; + r + }; + // Draw first title content. + ui.scope_builder(UiBuilder::new().max_rect(first_rect), |ui| { + Self::title_text_content(ui, first); + }); - let second_rect = { - let mut r = rect.clone(); - r.min.x = first_rect.max.x + 2.0 * Self::HEIGHT; - r.max.x -= Self::HEIGHT; - r - }; - // Draw second title content. - ui.scope_builder(UiBuilder::new().max_rect(second_rect), |ui| { - Self::title_text_content(ui, second); - }); - } - } - }); - }); - } + let second_rect = { + let mut r = rect.clone(); + r.min.x = first_rect.max.x + 2.0 * Self::HEIGHT; + r.max.x -= Self::HEIGHT; + r + }; + // Draw second title content. + ui.scope_builder(UiBuilder::new().max_rect(second_rect), |ui| { + Self::title_text_content(ui, second); + }); + } + } + }); + }); + } - /// Setup title text content. - fn title_text_content(ui: &mut egui::Ui, content: TitleContentType) { - ui.vertical_centered(|ui| { - match content { - TitleContentType::Title(text) => { - ui.add_space(13.0 + if !View::is_desktop() { - 1.0 - } else { - 0.0 - }); - View::ellipsize_text(ui, text.to_uppercase(), 19.0, Colors::title(true)); - } - TitleContentType::WithSubTitle(text, subtitle, animate) => { - ui.add_space(4.0); - View::ellipsize_text(ui, text.to_uppercase(), 18.0, Colors::title(true)); - ui.add_space(-2.0); - View::animate_text(ui, subtitle, 15.0, Colors::text(true), animate) - } - } - }); - } + /// Setup title text content. + fn title_text_content(ui: &mut egui::Ui, content: TitleContentType) { + ui.vertical_centered(|ui| match content { + TitleContentType::Title(text) => { + ui.add_space(13.0 + if !View::is_desktop() { 1.0 } else { 0.0 }); + View::ellipsize_text(ui, text.to_uppercase(), 19.0, Colors::title(true)); + } + TitleContentType::WithSubTitle(text, subtitle, animate) => { + ui.add_space(4.0); + View::ellipsize_text(ui, text.to_uppercase(), 18.0, Colors::title(true)); + ui.add_space(-2.0); + View::animate_text(ui, subtitle, 15.0, Colors::text(true), animate) + } + }); + } } diff --git a/src/gui/views/types.rs b/src/gui/views/types.rs index 093aa12a..f82463a1 100644 --- a/src/gui/views/types.rs +++ b/src/gui/views/types.rs @@ -12,141 +12,144 @@ // See the License for the specific language governing permissions and // limitations under the License. -use grin_util::ZeroingString; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::Modal; +use grin_util::ZeroingString; /// Title type, can be single or dual title in the row. pub enum TitleType { - /// Single title content. - Single(TitleContentType), - /// Dual title content, will align first content for default panel size width. - Dual(TitleContentType, TitleContentType), + /// Single title content. + Single(TitleContentType), + /// Dual title content, will align first content for default panel size width. + Dual(TitleContentType, TitleContentType), } /// Title content type, can be single title or with animated subtitle. pub enum TitleContentType { - /// Single text. - Title(String), - /// With optionally animated subtitle text. - WithSubTitle(String, String, bool) + /// Single text. + Title(String), + /// With optionally animated subtitle text. + WithSubTitle(String, String, bool), } /// Stroke position against content. pub enum LinePosition { - TOP, LEFT, RIGHT, BOTTOM + TOP, + LEFT, + RIGHT, + BOTTOM, } /// Position of [`Modal`] on the screen. #[derive(Clone)] pub enum ModalPosition { - CenterTop, - Center + CenterTop, + Center, } /// Global [`Modal`] state. #[derive(Default)] pub struct ModalState { - /// Opened [`Modal`]. - pub modal: Option, + /// Opened [`Modal`]. + pub modal: Option, } /// Content container to simplify modals management and navigation. pub trait ContentContainer { - /// List of allowed [`Modal`] identifiers. - fn modal_ids(&self) -> Vec<&'static str>; - /// Draw modal content. - fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks); - /// Draw container content. - fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks); - /// Draw content, to call by parent container. - fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - // Draw modal content. - if let Some(id) = Modal::opened() { - if self.modal_ids().contains(&id) { - Modal::ui(ui.ctx(), cb, |ui, modal, cb| { - self.modal_ui(ui, modal, cb); - }); - } - } - self.container_ui(ui, cb); - } + /// List of allowed [`Modal`] identifiers. + fn modal_ids(&self) -> Vec<&'static str>; + /// Draw modal content. + fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks); + /// Draw container content. + fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks); + /// Draw content, to call by parent container. + fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + // Draw modal content. + if let Some(id) = Modal::opened() { + if self.modal_ids().contains(&id) { + Modal::ui(ui.ctx(), cb, |ui, modal, cb| { + self.modal_ui(ui, modal, cb); + }); + } + } + self.container_ui(ui, cb); + } } /// QR code scan result. #[derive(Clone)] pub enum QrScanResult { - /// Slatepack message. - Slatepack(String), - /// Slatepack address. - Address(ZeroingString), - /// Parsed text. - Text(ZeroingString), - /// Recovery phrase in standard or compact SeedQR format. - /// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md - SeedQR(ZeroingString), - /// Part of Uniform Resources as URI with current index and total messages amount. - /// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md - URPart(String, usize, usize), + /// Slatepack message. + Slatepack(String), + /// Slatepack address. + Address(ZeroingString), + /// Parsed text. + Text(ZeroingString), + /// Recovery phrase in standard or compact SeedQR format. + /// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md + SeedQR(ZeroingString), + /// Part of Uniform Resources as URI with current index and total messages amount. + /// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md + URPart(String, usize, usize), } impl QrScanResult { - /// Get text scanning result. - pub fn text(&self) -> String { - match self { - QrScanResult::Slatepack(text) => text.to_string(), - QrScanResult::Address(text) => text.to_string(), - QrScanResult::Text(text) => text.to_string(), - QrScanResult::SeedQR(text) => text.to_string(), - QrScanResult::URPart(uri, _, _) => uri.to_string(), - } - } + /// Get text scanning result. + pub fn text(&self) -> String { + match self { + QrScanResult::Slatepack(text) => text.to_string(), + QrScanResult::Address(text) => text.to_string(), + QrScanResult::Text(text) => text.to_string(), + QrScanResult::SeedQR(text) => text.to_string(), + QrScanResult::URPart(uri, _, _) => uri.to_string(), + } + } } /// QR code scanning state. pub struct QrScanState { - /// Flag to check if image is processing to find QR code. - pub image_processing: bool, - /// Processed QR code result. - pub qr_scan_result: Option + /// Flag to check if image is processing to find QR code. + pub image_processing: bool, + /// Processed QR code result. + pub qr_scan_result: Option, } impl Default for QrScanState { - fn default() -> Self { - Self { - image_processing: false, - qr_scan_result: None, - } - } + fn default() -> Self { + Self { + image_processing: false, + qr_scan_result: None, + } + } } /// 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, + /// 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, + /// 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 for animated QR code. - pub svg_list: Option>> + /// Vector image data. + pub svg: Option>, + /// Multiple vector image data for animated QR code. + pub svg_list: Option>>, } impl Default for QrImageState { - fn default() -> Self { - Self { - loading: false, - exporting: false, - gif_data: None, - gif_creating: false, - svg: None, - svg_list: None, - } - } -} \ No newline at end of file + fn default() -> Self { + Self { + loading: false, + exporting: false, + gif_data: None, + gif_creating: false, + svg: None, + svg_list: None, + } + } +} diff --git a/src/gui/views/views.rs b/src/gui/views/views.rs index 69f65d5f..a9b4e051 100644 --- a/src/gui/views/views.rs +++ b/src/gui/views/views.rs @@ -12,698 +12,759 @@ // See the License for the specific language governing permissions and // limitations under the License. -use lazy_static::lazy_static; -use std::sync::atomic::{AtomicI32, Ordering}; use egui::emath::GuiRounding; use egui::epaint::text::TextWrapping; use egui::epaint::{Color32, FontId, PathShape, PathStroke, RectShape, Stroke}; use egui::load::SizedTexture; use egui::os::OperatingSystem; use egui::text::{LayoutJob, TextFormat}; -use egui::{lerp, Button, CornerRadius, CursorIcon, Rect, Response, Rgba, RichText, Sense, SizeHint, Spinner, StrokeKind, TextureHandle, TextureOptions, UiBuilder, Widget}; +use egui::{ + Button, CornerRadius, CursorIcon, Rect, Response, Rgba, RichText, Sense, SizeHint, Spinner, + StrokeKind, TextureHandle, TextureOptions, UiBuilder, Widget, lerp, +}; use egui_extras::image::load_svg_bytes_with_size; +use lazy_static::lazy_static; +use std::sync::atomic::{AtomicI32, Ordering}; +use crate::AppConfig; +use crate::gui::Colors; use crate::gui::icons::{CHECK_FAT, CHECK_SQUARE, SQUARE}; use crate::gui::views::types::LinePosition; -use crate::gui::Colors; -use crate::AppConfig; pub struct View; impl View { - /// Check if current platform is desktop - pub fn is_desktop() -> bool { - let os = OperatingSystem::from_target_os(); - os != OperatingSystem::Android && os != OperatingSystem::IOS - } - - /// Format timestamp in seconds with local UTC offset. - pub fn format_time(ts: i64) -> String { - let utc_offset = chrono::Local::now().offset().local_minus_utc(); - let utc_time = ts + utc_offset as i64; - let tx_time = chrono::DateTime::from_timestamp(utc_time, 0).unwrap(); - tx_time.format("%d/%m/%Y %H:%M:%S").to_string() - } - - /// Get default stroke around views. - pub fn default_stroke() -> Stroke { - Stroke { width: 1.0, color: Colors::stroke() } - } - - /// Get default stroke around item buttons. - pub fn item_stroke() -> Stroke { - Stroke { width: 1.0, color: Colors::item_stroke() } - } - - /// Get stroke for hovered items and buttons. - pub fn hover_stroke() -> Stroke { - Stroke { width: 1.0, color: Colors::item_hover() } - } - - /// Draw content with maximum width value. - pub fn max_width_ui(ui: &mut egui::Ui, - max_width: f32, - add_content: impl FnOnce(&mut egui::Ui)) { - // Setup content width. - let mut width = ui.available_width(); - if width == 0.0 { - return; - } - let mut rect = ui.available_rect_before_wrap(); - width = f32::min(width, max_width); - rect.set_width(width); - - // Draw content. - ui.vertical_centered(|ui| { - ui.allocate_ui(rect.size(), |ui| { - (add_content)(ui); - }); - }); - } - - /// Get width and height of app window. - pub fn window_size(ctx: &egui::Context) -> (f32, f32) { - let rect = ctx.content_rect(); - (rect.width(), rect.height()) - } - - /// Calculate margin for far left view based on display insets (cutouts). - pub fn far_left_inset_margin(ui: &mut egui::Ui) -> f32 { - if ui.available_rect_before_wrap().min.x == 0.0 { - Self::get_left_inset() - } else { - 0.0 - } - } - - /// Calculate margin for far left view based on display insets (cutouts). - pub fn far_right_inset_margin(ui: &mut egui::Ui) -> f32 { - let container_width = ui.available_rect_before_wrap().max.x as i32; - let window_size = Self::window_size(ui.ctx()); - let display_width = window_size.0 as i32; - // Means end of the screen. - if container_width == display_width { - Self::get_right_inset() - } else { - 0.0 - } - } - - /// Content padding for current platform. - pub fn content_padding() -> f32 { - if View::is_desktop() { - 4.0 - } else { - 8.0 - } - } - - /// Cut long text with ﹍ character. - fn ellipsize(text: impl Into, size: f32, color: Color32) -> LayoutJob { - let mut job = LayoutJob::single_section(text.into(), TextFormat { - font_id: FontId::proportional(size), color, ..Default::default() - }); - job.wrap = TextWrapping { - max_rows: 1, - break_anywhere: true, - overflow_character: Option::from('﹍'), - ..Default::default() - }; - job - } - - /// Draw ellipsized text. - pub fn ellipsize_text(ui: &mut egui::Ui, text: impl Into, size: f32, color: Color32) { - ui.label(Self::ellipsize(text, size, color)); - } - - /// Draw animated ellipsized text. - pub fn animate_text(ui: &mut egui::Ui, - text: impl Into, - size: f32, - color: Color32, - animate: bool) { - // Setup text color animation if needed. - let (dark, bright) = (0.3, 1.0); - let color_factor = if animate { - lerp(dark..=bright, ui.input(|i| i.time).cos().abs()) as f32 - } else { - bright as f32 - }; - - // Draw subtitle text. - let sub_color_rgba = Rgba::from(color) * color_factor; - let sub_color = Color32::from(sub_color_rgba); - View::ellipsize_text(ui, text, size, sub_color); - - // Repaint delay based on animation status. - if animate { - ui.ctx().request_repaint(); - } - } - - /// Draw horizontally centered subtitle with space below. - pub fn sub_title(ui: &mut egui::Ui, text: String) { - ui.vertical_centered_justified(|ui| { - ui.label(RichText::new(text.to_uppercase()).size(16.0).color(Colors::text(false))); - }); - ui.add_space(4.0); - } - - /// Draw big size title button. - pub fn title_button_big(ui: &mut egui::Ui, icon: &str, action: impl FnOnce(&mut egui::Ui)) { - Self::title_button(ui, 22.0, icon, action); - } - - /// Draw small size title button. - pub fn title_button_small(ui: &mut egui::Ui, icon: &str, action: impl FnOnce(&mut egui::Ui)) { - Self::title_button(ui, 16.0, icon, action); - } - - /// Draw title button with transparent background color, contains only icon. - fn title_button(ui: &mut egui::Ui, size: f32, icon: &str, action: impl FnOnce(&mut egui::Ui)) { - ui.scope(|ui| { - // Setup padding for title buttons. - if !View::is_desktop() { - ui.style_mut().spacing.button_padding = egui::vec2(20.0, 8.0); - } else { - ui.style_mut().spacing.button_padding = egui::vec2(16.0, 8.0); - } - // Disable strokes. - ui.style_mut().visuals.widgets.inactive.bg_stroke = Stroke::NONE; - ui.style_mut().visuals.widgets.hovered.bg_stroke = Stroke::NONE; - ui.style_mut().visuals.widgets.active.bg_stroke = Stroke::NONE; - ui.style_mut().visuals.widgets.active.corner_radius = CornerRadius::default(); - ui.style_mut().visuals.widgets.active.expansion = 0.0; - - // Setup text. - let wt = RichText::new(icon.to_string()).size(size).color(Colors::title(true)); - // Draw button. - let br = Button::new(wt) - .sense(if View::is_desktop() { - Sense::click() - } else { - Sense::click_and_drag() - }) - .fill(Colors::TRANSPARENT) - .ui(ui) - .on_hover_cursor(CursorIcon::PointingHand); - br.surrender_focus(); - if br.clicked() || (!View::is_desktop() && br.drag_stopped()) { - action(ui); - } - }); - } - - /// Padding for tab items. - pub const TAB_ITEMS_PADDING: f32 = 5.0; - - /// Tab button with white background fill color, contains only icon. - pub fn tab_button(ui: &mut egui::Ui, - icon: &str, - color: Option, - selected: Option, - action: impl FnOnce(&mut egui::Ui)) { - ui.scope(|ui| { - let text_color = if let Some(c) = color { - if selected.is_none() { - Colors::inactive_text() - } else { - c - } - } else { - if let Some(active) = selected { - match active { - true => Colors::gray(), - false => Colors::item_button_text() - } - } else { - Colors::inactive_text() - } - }; - - let mut button = Button::new(RichText::new(icon).size(22.0).color(text_color)); - - let active_not_selected = selected.is_some() && !selected.unwrap(); - if active_not_selected { - // Disable expansion on click/hover. - ui.style_mut().visuals.widgets.hovered.expansion = 0.0; - ui.style_mut().visuals.widgets.active.expansion = 0.0; - // Setup fill colors. - ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false); - ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite(); - ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill(); - // Setup stroke colors. - ui.visuals_mut().widgets.inactive.bg_stroke = Self::default_stroke(); - ui.visuals_mut().widgets.hovered.bg_stroke = Self::hover_stroke(); - ui.visuals_mut().widgets.active.bg_stroke = Self::item_stroke(); - } else { - button = button.fill(Colors::fill()).stroke(Stroke::NONE); - } - - // Setup paddings for tab buttons. - if Self::is_desktop() { - ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0); - } else { - ui.style_mut().spacing.button_padding = egui::vec2(14.0, 8.0); - }; - - // Setup pointer style. - let br = if active_not_selected { - button.ui(ui).on_hover_cursor(CursorIcon::PointingHand) - } else { - button.ui(ui) - }; - - br.surrender_focus(); - if br.clicked() && active_not_selected { - action(ui); - } - }); - } - - /// Draw [`Button`] with specified background fill and text color. - fn button_resp(ui: &mut egui::Ui, text: String, text_color: Color32, bg: Color32) -> Response { - let button_text = Self::ellipsize(text.to_uppercase(), 17.0, text_color); - Button::new(button_text) - .stroke(Self::default_stroke()) - .fill(bg) - .ui(ui) - .on_hover_cursor(CursorIcon::PointingHand) - } - - /// Draw [`Button`] with specified background fill color and default text color. - pub fn button(ui: &mut egui::Ui, text: impl Into, fill: Color32, cb: impl FnOnce()) { - let br = Self::button_resp(ui, text.into(), Colors::text_button(), fill); - if br.clicked() { - cb(); - } - } - - /// Draw [`Button`] with specified background fill color and text color. - pub fn colored_text_button(ui: &mut egui::Ui, - text: String, - text_color: Color32, - fill: Color32, - action: impl FnOnce()) { - let br = Self::button_resp(ui, text, text_color, fill); - if br.clicked() { - action(); - } - } - - /// Draw [`Button`] with specified background fill color and text color. - pub fn colored_text_button_ui(ui: &mut egui::Ui, - text: String, - text_color: Color32, - fill: Color32, - action: impl FnOnce(&mut egui::Ui)) { - let br = Self::button_resp(ui, text, text_color, fill); - if br.clicked() { - action(ui); - } - } - - /// Draw gold action [`Button`]. - pub fn action_button(ui: &mut egui::Ui, - text: impl Into, action: impl FnOnce()) { - Self::colored_text_button(ui, text.into(), Colors::title(true), Colors::gold(), action); - } - - /// Draw [`Button`] with specified background fill color and ui at callback. - pub fn button_ui(ui: &mut egui::Ui, - text: impl Into, - fill: Color32, - action: impl FnOnce(&mut egui::Ui)) { - let button_text = Self::ellipsize(text.into().to_uppercase(), 17.0, Colors::text_button()); - let br = Button::new(button_text) - .stroke(Self::default_stroke()) - .fill(fill) - .ui(ui) - .on_hover_cursor(CursorIcon::PointingHand); - if br.clicked() { - action(ui); - } - } - - /// Draw list item [`Button`] with provided rounding. - pub fn item_button(ui: &mut egui::Ui, - rounding: CornerRadius, - text: &'static str, - color: Option, - action: impl FnOnce()) { - // Setup button size. - let mut rect = ui.available_rect_before_wrap(); - rect.set_width(42.0); - let button_size = rect.size(); - - ui.scope(|ui| { - // Setup padding for item buttons. - let padding = if Self::is_desktop() { - 15.0 - } else { - 18.0 - }; - ui.style_mut().spacing.button_padding = egui::vec2(padding, 0.0); - // Disable expansion on click/hover. - ui.style_mut().visuals.widgets.hovered.expansion = 0.0; - ui.style_mut().visuals.widgets.active.expansion = 0.0; - // Setup fill colors. - ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false); - ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite(); - ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill(); - // Disable strokes. - ui.visuals_mut().widgets.inactive.bg_stroke = Stroke::NONE; - ui.visuals_mut().widgets.hovered.bg_stroke = Stroke::NONE; - ui.visuals_mut().widgets.active.bg_stroke = Stroke::NONE; - - // Setup button text color. - let text_color = if let Some(c) = color { c } else { Colors::item_button_text() }; - - // Show button. - let br = Button::new(RichText::new(text).size(20.0).color(text_color)) - .corner_radius(rounding) - .min_size(button_size) - .ui(ui) - .on_hover_cursor(CursorIcon::PointingHand); - br.surrender_focus(); - if br.clicked() { - action(); - } - - // Draw stroke. - let r = { - let mut r = ui.available_rect_before_wrap(); - r.min = br.rect.min; - r.min.x += 0.5; - r - }; - Self::line(ui, LinePosition::LEFT, &r, Colors::item_stroke()); - }); - } - - /// Calculate item background/button rounding based on item index. - pub fn item_rounding(index: usize, len: usize, is_button: bool) -> CornerRadius { - let corners = if is_button { - if len == 1 { - [false, true, true, false] - } else if index == 0 { - [false, true, false, false] - } else if index == len - 1 { - [false, false, true, false] - } else { - [false, false, false, false] - } - } else { - if len == 1 { - [true, true, true, true] - } else if index == 0 { - [true, true, false, false] - } else if index == len - 1 { - [false, false, true, true] - } else { - [false, false, false, false] - } - }; - CornerRadius { - nw: if corners[0] { 8.0 as u8 } else { 0.0 as u8 }, - ne: if corners[1] { 8.0 as u8 } else { 0.0 as u8 }, - sw: if corners[3] { 8.0 as u8 } else { 0.0 as u8 }, - se: if corners[2] { 8.0 as u8 } else { 0.0 as u8 }, - } - } - - /// Draw selected item check. - pub fn selected_item_check(ui: &mut egui::Ui) { - let padding = if View::is_desktop() { - 14.0 - } else { - 18.0 - }; - ui.add_space(padding); - ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green())); - ui.add_space(padding); - } - - /// Draw rounded box with some value and label in the middle, - /// where is r = (top_left, top_right, bottom_left, bottom_right). - /// | VALUE | - /// | label | - pub fn label_box(ui: &mut egui::Ui, v: impl Into, l: impl Into, r: [bool; 4]) { - let rect = ui.available_rect_before_wrap(); - - // Create background shape. - let mut bg_shape = RectShape::new(rect, CornerRadius { - nw: if r[0] { 8.0 as u8 } else { 0.0 as u8 }, - ne: if r[1] { 8.0 as u8 } else { 0.0 as u8 }, - sw: if r[2] { 8.0 as u8 } else { 0.0 as u8 }, - se: if r[3] { 8.0 as u8 } else { 0.0 as u8 }, - }, Colors::fill(), Self::item_stroke(), StrokeKind::Outside); - let bg_idx = ui.painter().add(bg_shape.clone()); - - // Draw box content. - let content_resp = ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| { - ui.vertical_centered_justified(|ui| { - ui.add_space(4.0); - ui.scope(|ui| { - // Correct vertical spacing between items. - ui.style_mut().spacing.item_spacing.y = -3.0; - - // Draw box value. - let mut job = LayoutJob::single_section(v.into(), TextFormat { - font_id: FontId::proportional(17.0), - color: Colors::white_or_black(true), - ..Default::default() - }); - job.wrap = TextWrapping { - max_rows: 1, - break_anywhere: true, - overflow_character: Option::from('﹍'), - ..Default::default() - }; - ui.label(job); - - // Draw box label. - ui.label(RichText::new(l).color(Colors::gray()).size(15.0)); - }); - ui.add_space(2.0); - }); - }).response; - - // Setup background shape size. - bg_shape.rect = content_resp.rect; - ui.painter().set(bg_idx, bg_shape); - } - - /// Draw content in the center of current layout with specified width and height. - pub fn center_content(ui: &mut egui::Ui, height: f32, content: impl FnOnce(&mut egui::Ui)) { - ui.vertical_centered(|ui| { - let mut rect = ui.available_rect_before_wrap(); - let side_margin = 28.0; - rect.min += egui::emath::vec2(side_margin, ui.available_height() / 2.0 - height / 2.0); - rect.max -= egui::emath::vec2(side_margin, 0.0); - ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| { - content(ui); - }); - }); - } - - /// Draw loading spinner. - pub fn loading_spinner(ui: &mut egui::Ui, size: f32) { - Spinner::new().size(size).color(Colors::gold()).ui(ui); - } - - /// Size of big loading spinner. - pub const BIG_SPINNER_SIZE: f32 = 104.0; - - /// Draw big gold loading spinner. - pub fn big_loading_spinner(ui: &mut egui::Ui) { - View::loading_spinner(ui, View::BIG_SPINNER_SIZE); - } - - /// Size of big loading spinner. - pub const SMALL_SPINNER_SIZE: f32 = 32.0; - - /// Draw small gold loading spinner. - pub fn small_loading_spinner(ui: &mut egui::Ui) { - View::loading_spinner(ui, View::SMALL_SPINNER_SIZE); - } - - /// Draw the button that looks like checkbox with callback on check. - pub fn checkbox(ui: &mut egui::Ui, checked: bool, text: impl Into, cb: impl FnOnce()) { - let (text_value, color) = match checked { - true => (format!("{} {}", CHECK_SQUARE, text.into()), Colors::text_button()), - false => (format!("{} {}", SQUARE, text.into()), Colors::checkbox()) - }; - - let br = Button::new(RichText::new(text_value).size(17.0).color(color)) - .frame(false) - .stroke(Stroke::NONE) - .fill(Colors::TRANSPARENT) - .ui(ui) - .on_hover_cursor(CursorIcon::PointingHand); - if br.clicked() { - cb(); - } - } - - /// Show a [`RadioButton`]. It is selected if `*current_value == selected_value`. - /// If clicked, `selected_value` is assigned to `*current_value`. - pub fn radio_value(ui: &mut egui::Ui, - current: &mut T, - value: T, - text: impl Into) { - ui.scope(|ui| { - // Setup background color. - ui.visuals_mut().widgets.inactive.bg_fill = Colors::fill_deep(); - // Draw radio button. - let mut response = ui.radio(*current == value, text.into()) - .on_hover_cursor(CursorIcon::PointingHand); - if response.clicked() && *current != value { - *current = value; - response.mark_changed(); - } - }); - } - - /// Draw horizontal line. - pub fn horizontal_line(ui: &mut egui::Ui, color: Color32) { - let line_size = egui::Vec2::new(ui.available_width(), 1.0); - let (line_rect, _) = ui.allocate_exact_size(line_size, Sense::hover()); - let painter = ui.painter(); - painter.hline(line_rect.x_range(), - line_rect.center().y.round_to_pixels(painter.pixels_per_point()), - Stroke { width: 1.0, color }); - } - - /// Draw line for panel content. - pub fn line(ui: &mut egui::Ui, pos: LinePosition, rect: &Rect, color: Color32) { - let points = match pos { - LinePosition::RIGHT => { - vec![{ - let mut r = rect.clone(); - r.min.x = r.max.x; - r.min - }, rect.max] - } - LinePosition::BOTTOM => { - vec![{ - let mut r = rect.clone(); - r.min.y = r.max.y; - r.min - }, rect.max] - } - LinePosition::LEFT => { - vec![rect.min, { - let mut r = rect.clone(); - r.max.x = r.min.x; - r.max - }] - } - LinePosition::TOP => { - vec![rect.min, { - let mut r = rect.clone(); - r.max.y = r.min.y; - r.max - }] - } - }; - let stroke = PathShape { - points, - closed: false, - fill: Default::default(), - stroke: PathStroke::new(1.0, color), - }; - ui.painter().add(stroke); - } - - /// Draw SVG image from provided data with optional provided size. - pub fn svg_image(ui: &mut egui::Ui, - name: &str, - svg: &[u8], - size: SizeHint) -> TextureHandle { - let color_img = load_svg_bytes_with_size(svg, size, &usvg::Options::default()).unwrap(); - // Create image texture. - let texture_handle = ui.ctx().load_texture(name, - color_img.clone(), - TextureOptions::default()); - 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)); - texture_handle - } - - /// Draw application logo image with name and version. - pub fn app_logo_name_version(ui: &mut egui::Ui) { - ui.add_space(-1.0); - let logo = if AppConfig::dark_theme().unwrap_or(false) { - egui::include_image!("../../../img/logo_light.png") - } else { - egui::include_image!("../../../img/logo.png") - }; - // Show application logo and name. - ui.scope(|ui| { - ui.set_opacity(0.9); - egui::Image::new(logo).fit_to_exact_size(egui::vec2(182.0, 182.0)).ui(ui); - }); - ui.add_space(-11.0); - ui.label(RichText::new("GRIM") - .size(24.0) - .color(Colors::white_or_black(true)) - ); - ui.add_space(-2.0); - ui.label(RichText::new(crate::VERSION) - .size(16.0) - .color(Colors::title(false)) - ); - } - - /// Draw semi-transparent cover at specified area. - pub fn content_cover_ui(ui: &mut egui::Ui, - rect: Rect, - id: impl std::hash::Hash, - mut on_click: impl FnMut()) { - let resp = ui.interact(rect, egui::Id::new(id), Sense::click_and_drag()); - if resp.clicked() || resp.dragged() { - on_click(); - } - let shape = RectShape::filled(resp.rect, - CornerRadius::ZERO, - Colors::semi_transparent().gamma_multiply(0.7)); - ui.painter().add(shape); - } - - /// Get top display inset (cutout) size. - pub fn get_top_inset() -> f32 { - TOP_DISPLAY_INSET.load(Ordering::Relaxed) as f32 - } - - /// Get right display inset (cutout) size. - pub fn get_right_inset() -> f32 { - RIGHT_DISPLAY_INSET.load(Ordering::Relaxed) as f32 - } - - /// Get bottom display inset (cutout) size. - pub fn get_bottom_inset() -> f32 { - BOTTOM_DISPLAY_INSET.load(Ordering::Relaxed) as f32 - } - - /// Get left display inset (cutout) size. - pub fn get_left_inset() -> f32 { - LEFT_DISPLAY_INSET.load(Ordering::Relaxed) as f32 - } - + /// Check if current platform is desktop + pub fn is_desktop() -> bool { + let os = OperatingSystem::from_target_os(); + os != OperatingSystem::Android && os != OperatingSystem::IOS + } + + /// Format timestamp in seconds with local UTC offset. + pub fn format_time(ts: i64) -> String { + let utc_offset = chrono::Local::now().offset().local_minus_utc(); + let utc_time = ts + utc_offset as i64; + let tx_time = chrono::DateTime::from_timestamp(utc_time, 0).unwrap(); + tx_time.format("%d/%m/%Y %H:%M:%S").to_string() + } + + /// Get default stroke around views. + pub fn default_stroke() -> Stroke { + Stroke { + width: 1.0, + color: Colors::stroke(), + } + } + + /// Get default stroke around item buttons. + pub fn item_stroke() -> Stroke { + Stroke { + width: 1.0, + color: Colors::item_stroke(), + } + } + + /// Get stroke for hovered items and buttons. + pub fn hover_stroke() -> Stroke { + Stroke { + width: 1.0, + color: Colors::item_hover(), + } + } + + /// Draw content with maximum width value. + pub fn max_width_ui( + ui: &mut egui::Ui, + max_width: f32, + add_content: impl FnOnce(&mut egui::Ui), + ) { + // Setup content width. + let mut width = ui.available_width(); + if width == 0.0 { + return; + } + let mut rect = ui.available_rect_before_wrap(); + width = f32::min(width, max_width); + rect.set_width(width); + + // Draw content. + ui.vertical_centered(|ui| { + ui.allocate_ui(rect.size(), |ui| { + (add_content)(ui); + }); + }); + } + + /// Get width and height of app window. + pub fn window_size(ctx: &egui::Context) -> (f32, f32) { + let rect = ctx.content_rect(); + (rect.width(), rect.height()) + } + + /// Calculate margin for far left view based on display insets (cutouts). + pub fn far_left_inset_margin(ui: &mut egui::Ui) -> f32 { + if ui.available_rect_before_wrap().min.x == 0.0 { + Self::get_left_inset() + } else { + 0.0 + } + } + + /// Calculate margin for far left view based on display insets (cutouts). + pub fn far_right_inset_margin(ui: &mut egui::Ui) -> f32 { + let container_width = ui.available_rect_before_wrap().max.x as i32; + let window_size = Self::window_size(ui.ctx()); + let display_width = window_size.0 as i32; + // Means end of the screen. + if container_width == display_width { + Self::get_right_inset() + } else { + 0.0 + } + } + + /// Content padding for current platform. + pub fn content_padding() -> f32 { + if View::is_desktop() { 4.0 } else { 8.0 } + } + + /// Cut long text with ﹍ character. + fn ellipsize(text: impl Into, size: f32, color: Color32) -> LayoutJob { + let mut job = LayoutJob::single_section( + text.into(), + TextFormat { + font_id: FontId::proportional(size), + color, + ..Default::default() + }, + ); + job.wrap = TextWrapping { + max_rows: 1, + break_anywhere: true, + overflow_character: Option::from('﹍'), + ..Default::default() + }; + job + } + + /// Draw ellipsized text. + pub fn ellipsize_text(ui: &mut egui::Ui, text: impl Into, size: f32, color: Color32) { + ui.label(Self::ellipsize(text, size, color)); + } + + /// Draw animated ellipsized text. + pub fn animate_text( + ui: &mut egui::Ui, + text: impl Into, + size: f32, + color: Color32, + animate: bool, + ) { + // Setup text color animation if needed. + let (dark, bright) = (0.3, 1.0); + let color_factor = if animate { + lerp(dark..=bright, ui.input(|i| i.time).cos().abs()) as f32 + } else { + bright as f32 + }; + + // Draw subtitle text. + let sub_color_rgba = Rgba::from(color) * color_factor; + let sub_color = Color32::from(sub_color_rgba); + View::ellipsize_text(ui, text, size, sub_color); + + // Repaint delay based on animation status. + if animate { + ui.ctx().request_repaint(); + } + } + + /// Draw horizontally centered subtitle with space below. + pub fn sub_title(ui: &mut egui::Ui, text: String) { + ui.vertical_centered_justified(|ui| { + ui.label( + RichText::new(text.to_uppercase()) + .size(16.0) + .color(Colors::text(false)), + ); + }); + ui.add_space(4.0); + } + + /// Draw big size title button. + pub fn title_button_big(ui: &mut egui::Ui, icon: &str, action: impl FnOnce(&mut egui::Ui)) { + Self::title_button(ui, 22.0, icon, action); + } + + /// Draw small size title button. + pub fn title_button_small(ui: &mut egui::Ui, icon: &str, action: impl FnOnce(&mut egui::Ui)) { + Self::title_button(ui, 16.0, icon, action); + } + + /// Draw title button with transparent background color, contains only icon. + fn title_button(ui: &mut egui::Ui, size: f32, icon: &str, action: impl FnOnce(&mut egui::Ui)) { + ui.scope(|ui| { + // Setup padding for title buttons. + if !View::is_desktop() { + ui.style_mut().spacing.button_padding = egui::vec2(20.0, 8.0); + } else { + ui.style_mut().spacing.button_padding = egui::vec2(16.0, 8.0); + } + // Disable strokes. + ui.style_mut().visuals.widgets.inactive.bg_stroke = Stroke::NONE; + ui.style_mut().visuals.widgets.hovered.bg_stroke = Stroke::NONE; + ui.style_mut().visuals.widgets.active.bg_stroke = Stroke::NONE; + ui.style_mut().visuals.widgets.active.corner_radius = CornerRadius::default(); + ui.style_mut().visuals.widgets.active.expansion = 0.0; + + // Setup text. + let wt = RichText::new(icon.to_string()) + .size(size) + .color(Colors::title(true)); + // Draw button. + let br = Button::new(wt) + .sense(if View::is_desktop() { + Sense::click() + } else { + Sense::click_and_drag() + }) + .fill(Colors::TRANSPARENT) + .ui(ui) + .on_hover_cursor(CursorIcon::PointingHand); + br.surrender_focus(); + if br.clicked() || (!View::is_desktop() && br.drag_stopped()) { + action(ui); + } + }); + } + + /// Padding for tab items. + pub const TAB_ITEMS_PADDING: f32 = 5.0; + + /// Tab button with white background fill color, contains only icon. + pub fn tab_button( + ui: &mut egui::Ui, + icon: &str, + color: Option, + selected: Option, + action: impl FnOnce(&mut egui::Ui), + ) { + ui.scope(|ui| { + let text_color = if let Some(c) = color { + if selected.is_none() { + Colors::inactive_text() + } else { + c + } + } else { + if let Some(active) = selected { + match active { + true => Colors::gray(), + false => Colors::item_button_text(), + } + } else { + Colors::inactive_text() + } + }; + + let mut button = Button::new(RichText::new(icon).size(22.0).color(text_color)); + + let active_not_selected = selected.is_some() && !selected.unwrap(); + if active_not_selected { + // Disable expansion on click/hover. + ui.style_mut().visuals.widgets.hovered.expansion = 0.0; + ui.style_mut().visuals.widgets.active.expansion = 0.0; + // Setup fill colors. + ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false); + ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite(); + ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill(); + // Setup stroke colors. + ui.visuals_mut().widgets.inactive.bg_stroke = Self::default_stroke(); + ui.visuals_mut().widgets.hovered.bg_stroke = Self::hover_stroke(); + ui.visuals_mut().widgets.active.bg_stroke = Self::item_stroke(); + } else { + button = button.fill(Colors::fill()).stroke(Stroke::NONE); + } + + // Setup paddings for tab buttons. + if Self::is_desktop() { + ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0); + } else { + ui.style_mut().spacing.button_padding = egui::vec2(14.0, 8.0); + }; + + // Setup pointer style. + let br = if active_not_selected { + button.ui(ui).on_hover_cursor(CursorIcon::PointingHand) + } else { + button.ui(ui) + }; + + br.surrender_focus(); + if br.clicked() && active_not_selected { + action(ui); + } + }); + } + + /// Draw [`Button`] with specified background fill and text color. + fn button_resp(ui: &mut egui::Ui, text: String, text_color: Color32, bg: Color32) -> Response { + let button_text = Self::ellipsize(text.to_uppercase(), 17.0, text_color); + Button::new(button_text) + .stroke(Self::default_stroke()) + .fill(bg) + .ui(ui) + .on_hover_cursor(CursorIcon::PointingHand) + } + + /// Draw [`Button`] with specified background fill color and default text color. + pub fn button(ui: &mut egui::Ui, text: impl Into, fill: Color32, cb: impl FnOnce()) { + let br = Self::button_resp(ui, text.into(), Colors::text_button(), fill); + if br.clicked() { + cb(); + } + } + + /// Draw [`Button`] with specified background fill color and text color. + pub fn colored_text_button( + ui: &mut egui::Ui, + text: String, + text_color: Color32, + fill: Color32, + action: impl FnOnce(), + ) { + let br = Self::button_resp(ui, text, text_color, fill); + if br.clicked() { + action(); + } + } + + /// Draw [`Button`] with specified background fill color and text color. + pub fn colored_text_button_ui( + ui: &mut egui::Ui, + text: String, + text_color: Color32, + fill: Color32, + action: impl FnOnce(&mut egui::Ui), + ) { + let br = Self::button_resp(ui, text, text_color, fill); + if br.clicked() { + action(ui); + } + } + + /// Draw gold action [`Button`]. + pub fn action_button(ui: &mut egui::Ui, text: impl Into, action: impl FnOnce()) { + Self::colored_text_button(ui, text.into(), Colors::title(true), Colors::gold(), action); + } + + /// Draw [`Button`] with specified background fill color and ui at callback. + pub fn button_ui( + ui: &mut egui::Ui, + text: impl Into, + fill: Color32, + action: impl FnOnce(&mut egui::Ui), + ) { + let button_text = Self::ellipsize(text.into().to_uppercase(), 17.0, Colors::text_button()); + let br = Button::new(button_text) + .stroke(Self::default_stroke()) + .fill(fill) + .ui(ui) + .on_hover_cursor(CursorIcon::PointingHand); + if br.clicked() { + action(ui); + } + } + + /// Draw list item [`Button`] with provided rounding. + pub fn item_button( + ui: &mut egui::Ui, + rounding: CornerRadius, + text: &'static str, + color: Option, + action: impl FnOnce(), + ) { + // Setup button size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_width(42.0); + let button_size = rect.size(); + + ui.scope(|ui| { + // Setup padding for item buttons. + let padding = if Self::is_desktop() { 15.0 } else { 18.0 }; + ui.style_mut().spacing.button_padding = egui::vec2(padding, 0.0); + // Disable expansion on click/hover. + ui.style_mut().visuals.widgets.hovered.expansion = 0.0; + ui.style_mut().visuals.widgets.active.expansion = 0.0; + // Setup fill colors. + ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false); + ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite(); + ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill(); + // Disable strokes. + ui.visuals_mut().widgets.inactive.bg_stroke = Stroke::NONE; + ui.visuals_mut().widgets.hovered.bg_stroke = Stroke::NONE; + ui.visuals_mut().widgets.active.bg_stroke = Stroke::NONE; + + // Setup button text color. + let text_color = if let Some(c) = color { + c + } else { + Colors::item_button_text() + }; + + // Show button. + let br = Button::new(RichText::new(text).size(20.0).color(text_color)) + .corner_radius(rounding) + .min_size(button_size) + .ui(ui) + .on_hover_cursor(CursorIcon::PointingHand); + br.surrender_focus(); + if br.clicked() { + action(); + } + + // Draw stroke. + let r = { + let mut r = ui.available_rect_before_wrap(); + r.min = br.rect.min; + r.min.x += 0.5; + r + }; + Self::line(ui, LinePosition::LEFT, &r, Colors::item_stroke()); + }); + } + + /// Calculate item background/button rounding based on item index. + pub fn item_rounding(index: usize, len: usize, is_button: bool) -> CornerRadius { + let corners = if is_button { + if len == 1 { + [false, true, true, false] + } else if index == 0 { + [false, true, false, false] + } else if index == len - 1 { + [false, false, true, false] + } else { + [false, false, false, false] + } + } else { + if len == 1 { + [true, true, true, true] + } else if index == 0 { + [true, true, false, false] + } else if index == len - 1 { + [false, false, true, true] + } else { + [false, false, false, false] + } + }; + CornerRadius { + nw: if corners[0] { 8.0 as u8 } else { 0.0 as u8 }, + ne: if corners[1] { 8.0 as u8 } else { 0.0 as u8 }, + sw: if corners[3] { 8.0 as u8 } else { 0.0 as u8 }, + se: if corners[2] { 8.0 as u8 } else { 0.0 as u8 }, + } + } + + /// Draw selected item check. + pub fn selected_item_check(ui: &mut egui::Ui) { + let padding = if View::is_desktop() { 14.0 } else { 18.0 }; + ui.add_space(padding); + ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green())); + ui.add_space(padding); + } + + /// Draw rounded box with some value and label in the middle, + /// where is r = (top_left, top_right, bottom_left, bottom_right). + /// | VALUE | + /// | label | + pub fn label_box(ui: &mut egui::Ui, v: impl Into, l: impl Into, r: [bool; 4]) { + let rect = ui.available_rect_before_wrap(); + + // Create background shape. + let mut bg_shape = RectShape::new( + rect, + CornerRadius { + nw: if r[0] { 8.0 as u8 } else { 0.0 as u8 }, + ne: if r[1] { 8.0 as u8 } else { 0.0 as u8 }, + sw: if r[2] { 8.0 as u8 } else { 0.0 as u8 }, + se: if r[3] { 8.0 as u8 } else { 0.0 as u8 }, + }, + Colors::fill(), + Self::item_stroke(), + StrokeKind::Outside, + ); + let bg_idx = ui.painter().add(bg_shape.clone()); + + // Draw box content. + let content_resp = ui + .scope_builder(UiBuilder::new().max_rect(rect), |ui| { + ui.vertical_centered_justified(|ui| { + ui.add_space(4.0); + ui.scope(|ui| { + // Correct vertical spacing between items. + ui.style_mut().spacing.item_spacing.y = -3.0; + + // Draw box value. + let mut job = LayoutJob::single_section( + v.into(), + TextFormat { + font_id: FontId::proportional(17.0), + color: Colors::white_or_black(true), + ..Default::default() + }, + ); + job.wrap = TextWrapping { + max_rows: 1, + break_anywhere: true, + overflow_character: Option::from('﹍'), + ..Default::default() + }; + ui.label(job); + + // Draw box label. + ui.label(RichText::new(l).color(Colors::gray()).size(15.0)); + }); + ui.add_space(2.0); + }); + }) + .response; + + // Setup background shape size. + bg_shape.rect = content_resp.rect; + ui.painter().set(bg_idx, bg_shape); + } + + /// Draw content in the center of current layout with specified width and height. + pub fn center_content(ui: &mut egui::Ui, height: f32, content: impl FnOnce(&mut egui::Ui)) { + ui.vertical_centered(|ui| { + let mut rect = ui.available_rect_before_wrap(); + let side_margin = 28.0; + rect.min += egui::emath::vec2(side_margin, ui.available_height() / 2.0 - height / 2.0); + rect.max -= egui::emath::vec2(side_margin, 0.0); + ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| { + content(ui); + }); + }); + } + + /// Draw loading spinner. + pub fn loading_spinner(ui: &mut egui::Ui, size: f32) { + Spinner::new().size(size).color(Colors::gold()).ui(ui); + } + + /// Size of big loading spinner. + pub const BIG_SPINNER_SIZE: f32 = 104.0; + + /// Draw big gold loading spinner. + pub fn big_loading_spinner(ui: &mut egui::Ui) { + View::loading_spinner(ui, View::BIG_SPINNER_SIZE); + } + + /// Size of big loading spinner. + pub const SMALL_SPINNER_SIZE: f32 = 32.0; + + /// Draw small gold loading spinner. + pub fn small_loading_spinner(ui: &mut egui::Ui) { + View::loading_spinner(ui, View::SMALL_SPINNER_SIZE); + } + + /// Draw the button that looks like checkbox with callback on check. + pub fn checkbox(ui: &mut egui::Ui, checked: bool, text: impl Into, cb: impl FnOnce()) { + let (text_value, color) = match checked { + true => ( + format!("{} {}", CHECK_SQUARE, text.into()), + Colors::text_button(), + ), + false => (format!("{} {}", SQUARE, text.into()), Colors::checkbox()), + }; + + let br = Button::new(RichText::new(text_value).size(17.0).color(color)) + .frame(false) + .stroke(Stroke::NONE) + .fill(Colors::TRANSPARENT) + .ui(ui) + .on_hover_cursor(CursorIcon::PointingHand); + if br.clicked() { + cb(); + } + } + + /// Show a [`RadioButton`]. It is selected if `*current_value == selected_value`. + /// If clicked, `selected_value` is assigned to `*current_value`. + pub fn radio_value( + ui: &mut egui::Ui, + current: &mut T, + value: T, + text: impl Into, + ) { + ui.scope(|ui| { + // Setup background color. + ui.visuals_mut().widgets.inactive.bg_fill = Colors::fill_deep(); + // Draw radio button. + let mut response = ui + .radio(*current == value, text.into()) + .on_hover_cursor(CursorIcon::PointingHand); + if response.clicked() && *current != value { + *current = value; + response.mark_changed(); + } + }); + } + + /// Draw horizontal line. + pub fn horizontal_line(ui: &mut egui::Ui, color: Color32) { + let line_size = egui::Vec2::new(ui.available_width(), 1.0); + let (line_rect, _) = ui.allocate_exact_size(line_size, Sense::hover()); + let painter = ui.painter(); + painter.hline( + line_rect.x_range(), + line_rect + .center() + .y + .round_to_pixels(painter.pixels_per_point()), + Stroke { width: 1.0, color }, + ); + } + + /// Draw line for panel content. + pub fn line(ui: &mut egui::Ui, pos: LinePosition, rect: &Rect, color: Color32) { + let points = match pos { + LinePosition::RIGHT => { + vec![ + { + let mut r = rect.clone(); + r.min.x = r.max.x; + r.min + }, + rect.max, + ] + } + LinePosition::BOTTOM => { + vec![ + { + let mut r = rect.clone(); + r.min.y = r.max.y; + r.min + }, + rect.max, + ] + } + LinePosition::LEFT => { + vec![rect.min, { + let mut r = rect.clone(); + r.max.x = r.min.x; + r.max + }] + } + LinePosition::TOP => { + vec![rect.min, { + let mut r = rect.clone(); + r.max.y = r.min.y; + r.max + }] + } + }; + let stroke = PathShape { + points, + closed: false, + fill: Default::default(), + stroke: PathStroke::new(1.0, color), + }; + ui.painter().add(stroke); + } + + /// Draw SVG image from provided data with optional provided size. + pub fn svg_image(ui: &mut egui::Ui, name: &str, svg: &[u8], size: SizeHint) -> TextureHandle { + let color_img = load_svg_bytes_with_size(svg, size, &usvg::Options::default()).unwrap(); + // Create image texture. + let texture_handle = + ui.ctx() + .load_texture(name, color_img.clone(), TextureOptions::default()); + 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), + ); + texture_handle + } + + /// Draw application logo image with name and version. + pub fn app_logo_name_version(ui: &mut egui::Ui) { + ui.add_space(-1.0); + let logo = if AppConfig::dark_theme().unwrap_or(false) { + egui::include_image!("../../../img/logo_light.png") + } else { + egui::include_image!("../../../img/logo.png") + }; + // Show application logo and name. + ui.scope(|ui| { + ui.set_opacity(0.9); + egui::Image::new(logo) + .fit_to_exact_size(egui::vec2(182.0, 182.0)) + .ui(ui); + }); + ui.add_space(-11.0); + ui.label( + RichText::new("GRIM") + .size(24.0) + .color(Colors::white_or_black(true)), + ); + ui.add_space(-2.0); + ui.label( + RichText::new(crate::VERSION) + .size(16.0) + .color(Colors::title(false)), + ); + } + + /// Draw semi-transparent cover at specified area. + pub fn content_cover_ui( + ui: &mut egui::Ui, + rect: Rect, + id: impl std::hash::Hash, + mut on_click: impl FnMut(), + ) { + let resp = ui.interact(rect, egui::Id::new(id), Sense::click_and_drag()); + if resp.clicked() || resp.dragged() { + on_click(); + } + let shape = RectShape::filled( + resp.rect, + CornerRadius::ZERO, + Colors::semi_transparent().gamma_multiply(0.7), + ); + ui.painter().add(shape); + } + + /// Get top display inset (cutout) size. + pub fn get_top_inset() -> f32 { + TOP_DISPLAY_INSET.load(Ordering::Relaxed) as f32 + } + + /// Get right display inset (cutout) size. + pub fn get_right_inset() -> f32 { + RIGHT_DISPLAY_INSET.load(Ordering::Relaxed) as f32 + } + + /// Get bottom display inset (cutout) size. + pub fn get_bottom_inset() -> f32 { + BOTTOM_DISPLAY_INSET.load(Ordering::Relaxed) as f32 + } + + /// Get left display inset (cutout) size. + pub fn get_left_inset() -> f32 { + LEFT_DISPLAY_INSET.load(Ordering::Relaxed) as f32 + } } lazy_static! { - static ref TOP_DISPLAY_INSET: AtomicI32 = AtomicI32::new(0); - static ref RIGHT_DISPLAY_INSET: AtomicI32 = AtomicI32::new(0); - static ref BOTTOM_DISPLAY_INSET: AtomicI32 = AtomicI32::new(0); - static ref LEFT_DISPLAY_INSET: AtomicI32 = AtomicI32::new(0); + static ref TOP_DISPLAY_INSET: AtomicI32 = AtomicI32::new(0); + static ref RIGHT_DISPLAY_INSET: AtomicI32 = AtomicI32::new(0); + static ref BOTTOM_DISPLAY_INSET: AtomicI32 = AtomicI32::new(0); + static ref LEFT_DISPLAY_INSET: AtomicI32 = AtomicI32::new(0); } #[allow(dead_code)] @@ -712,20 +773,20 @@ lazy_static! { #[unsafe(no_mangle)] /// Callback from Java code to update display insets (cutouts). pub extern "C" fn Java_mw_gri_android_MainActivity_onDisplayInsets( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - cutouts: jni::sys::jarray + _env: jni::JNIEnv, + _class: jni::objects::JObject, + cutouts: jni::sys::jarray, ) { - use jni::objects::{JObject, JPrimitiveArray}; + use jni::objects::{JObject, JPrimitiveArray}; - let mut array: [i32; 4] = [0; 4]; - unsafe { - let j_obj = JObject::from_raw(cutouts); - let j_arr = JPrimitiveArray::from(j_obj); - _env.get_int_array_region(j_arr, 0, array.as_mut()).unwrap(); - } - TOP_DISPLAY_INSET.store(array[0], Ordering::Relaxed); - RIGHT_DISPLAY_INSET.store(array[1], Ordering::Relaxed); - BOTTOM_DISPLAY_INSET.store(array[2], Ordering::Relaxed); - LEFT_DISPLAY_INSET.store(array[3], Ordering::Relaxed); -} \ No newline at end of file + let mut array: [i32; 4] = [0; 4]; + unsafe { + let j_obj = JObject::from_raw(cutouts); + let j_arr = JPrimitiveArray::from(j_obj); + _env.get_int_array_region(j_arr, 0, array.as_mut()).unwrap(); + } + TOP_DISPLAY_INSET.store(array[0], Ordering::Relaxed); + RIGHT_DISPLAY_INSET.store(array[1], Ordering::Relaxed); + BOTTOM_DISPLAY_INSET.store(array[2], Ordering::Relaxed); + LEFT_DISPLAY_INSET.store(array[3], Ordering::Relaxed); +} diff --git a/src/gui/views/wallets/content.rs b/src/gui/views/wallets/content.rs index ac4922f2..90d40d5d 100644 --- a/src/gui/views/wallets/content.rs +++ b/src/gui/views/wallets/content.rs @@ -12,58 +12,68 @@ // See the License for the specific language governing permissions and // limitations under the License. +use eframe::epaint::RectShape; use egui::os::OperatingSystem; use egui::scroll_area::ScrollBarVisibility; -use egui::{Align, CornerRadius, CursorIcon, Id, Layout, Margin, OpenUrl, RichText, ScrollArea, Sense, StrokeKind, UiBuilder}; +use egui::{ + Align, CornerRadius, CursorIcon, Id, Layout, Margin, OpenUrl, RichText, ScrollArea, Sense, + StrokeKind, UiBuilder, +}; use egui_async::Bind; use std::time::Duration; -use eframe::epaint::RectShape; -use crate::gui::icons::{ARROW_LEFT, BOOKMARKS, CALENDAR_CHECK, CLOUD_ARROW_DOWN, COMPUTER_TOWER, FOLDER_PLUS, GEAR, GEAR_FINE, GLOBE, GLOBE_SIMPLE, LOCK_KEY, NOTEPAD, PLUS, SIDEBAR_SIMPLE, SUITCASE}; +use crate::AppConfig; +use crate::gui::Colors; +use crate::gui::icons::{ + ARROW_LEFT, BOOKMARKS, CALENDAR_CHECK, CLOUD_ARROW_DOWN, COMPUTER_TOWER, FOLDER_PLUS, GEAR, + GEAR_FINE, GLOBE, GLOBE_SIMPLE, LOCK_KEY, NOTEPAD, PLUS, SIDEBAR_SIMPLE, SUITCASE, +}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::settings::SettingsContent; -use crate::gui::views::types::{ContentContainer, LinePosition, ModalPosition, TitleContentType, TitleType}; -use crate::gui::views::wallets::creation::WalletCreationContent; -use crate::gui::views::wallets::modals::{AddWalletModal, ChangelogContent, OpenWalletModal, WalletListModal, WalletSettingsModal}; -use crate::gui::views::wallets::wallet::types::{wallet_status_text, WalletContentContainer}; -use crate::gui::views::wallets::wallet::RecoverySettings; +use crate::gui::views::types::{ + ContentContainer, LinePosition, ModalPosition, TitleContentType, TitleType, +}; use crate::gui::views::wallets::WalletContent; +use crate::gui::views::wallets::creation::WalletCreationContent; +use crate::gui::views::wallets::modals::{ + AddWalletModal, ChangelogContent, OpenWalletModal, WalletListModal, WalletSettingsModal, +}; +use crate::gui::views::wallets::wallet::RecoverySettings; +use crate::gui::views::wallets::wallet::types::{WalletContentContainer, wallet_status_text}; use crate::gui::views::{Content, Modal, TitlePanel, View}; -use crate::gui::Colors; -use crate::http::{retrieve_release, ReleaseInfo}; +use crate::http::{ReleaseInfo, retrieve_release}; use crate::settings::AppUpdate; use crate::wallet::types::{ConnectionMethod, WalletTask}; use crate::wallet::{Wallet, WalletList}; -use crate::AppConfig; /// Wallets content. pub struct WalletsContent { - /// List of wallets. - wallets: WalletList, + /// List of wallets. + wallets: WalletList, - /// Initial wallet creation [`Modal`] content. - add_wallet_modal_content: AddWalletModal, - /// Wallet opening [`Modal`] content. - open_wallet_content: OpenWalletModal, - /// Wallet settings [`Modal`] content. - wallet_settings_content: WalletSettingsModal, - /// Wallet selection [`Modal`] content. - wallet_selection_content: WalletListModal, + /// Initial wallet creation [`Modal`] content. + add_wallet_modal_content: AddWalletModal, + /// Wallet opening [`Modal`] content. + open_wallet_content: OpenWalletModal, + /// Wallet settings [`Modal`] content. + wallet_settings_content: WalletSettingsModal, + /// Wallet selection [`Modal`] content. + wallet_selection_content: WalletListModal, - /// Selected [`Wallet`] content. - wallet_content: WalletContent, - /// Wallet creation content. - creation_content: Option, + /// Selected [`Wallet`] content. + wallet_content: WalletContent, + /// Wallet creation content. + creation_content: Option, - /// Settings content. - settings_content: Option, + /// Settings content. + settings_content: Option, - /// Result of update check - check_update: Bind, - /// Application update information. - update_info: (bool, Option), - /// Update changelog [`Modal`] content. - changelog_content: Option + /// Result of update check + check_update: Bind, + /// Application update information. + update_info: (bool, Option), + /// Update changelog [`Modal`] content. + changelog_content: Option, } /// Identifier for [`Modal`] to add the wallet. @@ -76,721 +86,755 @@ const WALLET_SETTINGS_MODAL: &'static str = "wallets_settings_modal"; const SELECT_WALLET_MODAL: &'static str = "wallets_select_modal"; impl Default for WalletsContent { - fn default() -> Self { - Self { - wallets: WalletList::default(), - wallet_selection_content: WalletListModal::new(None, None, true), - open_wallet_content: OpenWalletModal::new(), - add_wallet_modal_content: AddWalletModal::default(), - wallet_settings_content: WalletSettingsModal::new(ConnectionMethod::Integrated), - wallet_content: WalletContent::default(), - creation_content: None, - settings_content: None, - check_update: Bind::new(false), - update_info: (false, None), - changelog_content: None, - } - } + fn default() -> Self { + Self { + wallets: WalletList::default(), + wallet_selection_content: WalletListModal::new(None, None, true), + open_wallet_content: OpenWalletModal::new(), + add_wallet_modal_content: AddWalletModal::default(), + wallet_settings_content: WalletSettingsModal::new(ConnectionMethod::Integrated), + wallet_content: WalletContent::default(), + creation_content: None, + settings_content: None, + check_update: Bind::new(false), + update_info: (false, None), + changelog_content: None, + } + } } impl ContentContainer for WalletsContent { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - ADD_WALLET_MODAL, - OPEN_WALLET_MODAL, - WALLET_SETTINGS_MODAL, - SELECT_WALLET_MODAL, - Self::DELETE_CONFIRMATION_MODAL, - ChangelogContent::MODAL_ID - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![ + ADD_WALLET_MODAL, + OPEN_WALLET_MODAL, + WALLET_SETTINGS_MODAL, + SELECT_WALLET_MODAL, + Self::DELETE_CONFIRMATION_MODAL, + ChangelogContent::MODAL_ID, + ] + } - fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - match modal.id { - ADD_WALLET_MODAL => { - self.add_wallet_modal_content.ui(ui, modal, cb, |name, pass| { - self.creation_content = Some( - WalletCreationContent::new(name.clone(), pass.clone()) - ); - }); - }, - OPEN_WALLET_MODAL => { - self.open_wallet_content.ui(ui, modal, cb, |pass| { - if let Some(w) = self.wallets.selected().as_ref() { - return match w.open(pass) { - Ok(_) => true, - Err(_) => false - }; - } - true - }); - }, - WALLET_SETTINGS_MODAL => { - self.wallet_settings_content.ui(ui, modal, cb, |conn| { - if let Some(w) = self.wallets.selected().as_ref() { - w.update_connection(&conn); - } - }); - } - SELECT_WALLET_MODAL => { - let mut w: Option = None; - let mut d: Option = None; - self.wallet_selection_content.ui(ui, &mut self.wallets, |wallet, data| { - w = Some(wallet); - d = data; - }); - if let Some(wallet) = &w { - if !wallet.is_open() { - self.show_opening_modal(wallet, d, cb); - } else { - self.select_wallet(wallet, d, cb); - } - } - } - Self::DELETE_CONFIRMATION_MODAL => { - if let Some(w) = self.wallets.selected().as_ref() { - RecoverySettings::deletion_modal_ui(ui, w); - } - } - ChangelogContent::MODAL_ID => { - if let Some(c) = self.changelog_content.as_mut() { - c.ui(ui); - } - } - _ => {} - } - } + fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + match modal.id { + ADD_WALLET_MODAL => { + self.add_wallet_modal_content + .ui(ui, modal, cb, |name, pass| { + self.creation_content = + Some(WalletCreationContent::new(name.clone(), pass.clone())); + }); + } + OPEN_WALLET_MODAL => { + self.open_wallet_content.ui(ui, modal, cb, |pass| { + if let Some(w) = self.wallets.selected().as_ref() { + return match w.open(pass) { + Ok(_) => true, + Err(_) => false, + }; + } + true + }); + } + WALLET_SETTINGS_MODAL => { + self.wallet_settings_content.ui(ui, modal, cb, |conn| { + if let Some(w) = self.wallets.selected().as_ref() { + w.update_connection(&conn); + } + }); + } + SELECT_WALLET_MODAL => { + let mut w: Option = None; + let mut d: Option = None; + self.wallet_selection_content + .ui(ui, &mut self.wallets, |wallet, data| { + w = Some(wallet); + d = data; + }); + if let Some(wallet) = &w { + if !wallet.is_open() { + self.show_opening_modal(wallet, d, cb); + } else { + self.select_wallet(wallet, d, cb); + } + } + } + Self::DELETE_CONFIRMATION_MODAL => { + if let Some(w) = self.wallets.selected().as_ref() { + RecoverySettings::deletion_modal_ui(ui, w); + } + } + ChangelogContent::MODAL_ID => { + if let Some(c) = self.changelog_content.as_mut() { + c.ui(ui); + } + } + _ => {} + } + } - fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - // Small repaint delay is needed for Android back navigation and account list opening. - let is_android = OperatingSystem::from_target_os() == OperatingSystem::Android; - let account_list_showing = self.wallet_content.account_content.show_list; - ui.ctx().request_repaint_after(Duration::from_millis(if account_list_showing { - 10 - } else if is_android { - 100 - } else { - 1000 - })); + fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + // Small repaint delay is needed for Android back navigation and account list opening. + let is_android = OperatingSystem::from_target_os() == OperatingSystem::Android; + let account_list_showing = self.wallet_content.account_content.show_list; + ui.ctx() + .request_repaint_after(Duration::from_millis(if account_list_showing { + 10 + } else if is_android { + 100 + } else { + 1000 + })); - if let Some(data) = crate::consume_incoming_data() { - if !data.is_empty() { - self.on_data(ui, Some(data), cb); - } - } + if let Some(data) = crate::consume_incoming_data() { + if !data.is_empty() { + self.on_data(ui, Some(data), cb); + } + } - let showing_settings = self.showing_settings(); - let creating_wallet = self.creating_wallet(); - let showing_wallet = self.showing_wallet() && !creating_wallet && !showing_settings; - let dual_panel = is_dual_panel_mode(ui); - let content_width = ui.available_width(); - let list_hidden = showing_settings || creating_wallet || self.wallets.list().is_empty() - || (showing_wallet && (!dual_panel || !AppConfig::show_wallets_at_dual_panel())); + let showing_settings = self.showing_settings(); + let creating_wallet = self.creating_wallet(); + let showing_wallet = self.showing_wallet() && !creating_wallet && !showing_settings; + let dual_panel = is_dual_panel_mode(ui); + let content_width = ui.available_width(); + let list_hidden = showing_settings + || creating_wallet + || self.wallets.list().is_empty() + || (showing_wallet && (!dual_panel || !AppConfig::show_wallets_at_dual_panel())); - // Show title panel. - self.title_ui(ui, dual_panel, cb); + // Show title panel. + self.title_ui(ui, dual_panel, cb); - egui::SidePanel::right("wallet_panel") - .resizable(false) - .exact_width(if list_hidden { - content_width - } else { - content_width - Content::SIDE_PANEL_WIDTH - }) - .frame(egui::Frame { - fill: Colors::fill_deep(), - ..Default::default() - }) - .show_animated_inside(ui, showing_wallet, |ui| { - // Show selected wallet content. - if let Some(w) = self.wallets.selected().as_ref() { - self.wallet_content.ui(ui, w, cb); - } - }); + egui::SidePanel::right("wallet_panel") + .resizable(false) + .exact_width(if list_hidden { + content_width + } else { + content_width - Content::SIDE_PANEL_WIDTH + }) + .frame(egui::Frame { + fill: Colors::fill_deep(), + ..Default::default() + }) + .show_animated_inside(ui, showing_wallet, |ui| { + // Show selected wallet content. + if let Some(w) = self.wallets.selected().as_ref() { + self.wallet_content.ui(ui, w, cb); + } + }); - // Show wallet list tabs. - let side_padding = View::TAB_ITEMS_PADDING + if View::is_desktop() { - 0.0 - } else { - 4.0 - }; - let tabs_margin = Margin { - left: (View::far_left_inset_margin(ui) + side_padding) as i8, - right: (View::far_right_inset_margin(ui) + side_padding) as i8, - top: View::TAB_ITEMS_PADDING as i8, - bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8, - }; - egui::TopBottomPanel::bottom("wallets_bottom_panel") - .frame(egui::Frame { - inner_margin: tabs_margin, - fill: Colors::fill(), - ..Default::default() - }) - .resizable(false) - .show_animated_inside(ui, !list_hidden, |ui| { - let rect = ui.available_rect_before_wrap(); + // Show wallet list tabs. + let side_padding = View::TAB_ITEMS_PADDING + if View::is_desktop() { 0.0 } else { 4.0 }; + let tabs_margin = Margin { + left: (View::far_left_inset_margin(ui) + side_padding) as i8, + right: (View::far_right_inset_margin(ui) + side_padding) as i8, + top: View::TAB_ITEMS_PADDING as i8, + bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8, + }; + egui::TopBottomPanel::bottom("wallets_bottom_panel") + .frame(egui::Frame { + inner_margin: tabs_margin, + fill: Colors::fill(), + ..Default::default() + }) + .resizable(false) + .show_animated_inside(ui, !list_hidden, |ui| { + let rect = ui.available_rect_before_wrap(); - // Setup spacing between tabs. - ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); + // Setup spacing between tabs. + ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); - ui.vertical_centered(|ui| { - let pressed = Modal::opened() == Some(ADD_WALLET_MODAL); - View::tab_button(ui, PLUS, None, Some(pressed), |_| { - self.show_add_wallet_modal(); - }); - }); + ui.vertical_centered(|ui| { + let pressed = Modal::opened() == Some(ADD_WALLET_MODAL); + View::tab_button(ui, PLUS, None, Some(pressed), |_| { + self.show_add_wallet_modal(); + }); + }); - // Draw content divider line. - let r = { - let mut r = rect.clone(); - r.min.y -= tabs_margin.top as f32; - r.min.x -= tabs_margin.left as f32; - r.max.x += tabs_margin.right as f32; - r - }; - View::line(ui, LinePosition::TOP, &r, Colors::stroke()); - }); + // Draw content divider line. + let r = { + let mut r = rect.clone(); + r.min.y -= tabs_margin.top as f32; + r.min.x -= tabs_margin.left as f32; + r.max.x += tabs_margin.right as f32; + r + }; + View::line(ui, LinePosition::TOP, &r, Colors::stroke()); + }); - egui::SidePanel::left("wallet_list_panel") - .exact_width(if dual_panel && showing_wallet { - Content::SIDE_PANEL_WIDTH - } else { - content_width - }) - .resizable(false) - .frame(egui::Frame { - inner_margin: Margin { - left: (View::far_left_inset_margin(ui) + View::content_padding()) as i8, - right: (View::far_right_inset_margin(ui) + View::content_padding()) as i8, - top: 3.0 as i8, - bottom: 4.0 as i8, - }, - fill: Colors::fill(), - ..Default::default() - }) - .show_animated_inside(ui, !list_hidden, |ui| { - // Show wallet list. - self.wallet_list_ui(ui, cb); - }); + egui::SidePanel::left("wallet_list_panel") + .exact_width(if dual_panel && showing_wallet { + Content::SIDE_PANEL_WIDTH + } else { + content_width + }) + .resizable(false) + .frame(egui::Frame { + inner_margin: Margin { + left: (View::far_left_inset_margin(ui) + View::content_padding()) as i8, + right: (View::far_right_inset_margin(ui) + View::content_padding()) as i8, + top: 3.0 as i8, + bottom: 4.0 as i8, + }, + fill: Colors::fill(), + ..Default::default() + }) + .show_animated_inside(ui, !list_hidden, |ui| { + // Show wallet list. + self.wallet_list_ui(ui, cb); + }); - egui::CentralPanel::default() - .frame(egui::Frame { - inner_margin: if self.showing_settings() { - Margin { - left: (View::far_left_inset_margin(ui) + View::content_padding()) as i8, - right: (View::far_right_inset_margin(ui) + View::content_padding()) as i8, - top: 0, - bottom: 0, - } - } else { - Margin::default() - }, - fill: if self.showing_settings() { - Colors::fill_lite() - } else { - Colors::fill_deep() - }, - ..Default::default() - }) - .show_inside(ui, |ui| { - if self.showing_settings() { - if let Some(c) = &mut self.settings_content { - ScrollArea::vertical() - .id_salt("app_settings_wallets") - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(1.0); - ui.vertical_centered(|ui| { - // Show application settings content. - View::max_width_ui(ui, - Content::SIDE_PANEL_WIDTH * 1.3, - |ui| { - c.ui(ui, cb); - }); - }); - }); - } - } else if self.creating_wallet() { - // Show wallet creation content. - let mut created_wallet: Option = None; - let creation = self.creation_content.as_mut().unwrap(); - let pass = creation.pass.clone(); - creation.content_ui(ui, cb, |wallet| { - created_wallet = Some(wallet); - }); - if let Some(w) = &created_wallet { - self.creation_content = None; - self.wallets.add(w.clone()); - if let Ok(_) = w.open(pass.clone()) { - self.select_wallet(w, None, cb); - } - } - } else if self.wallets.list().is_empty() { - View::center_content(ui, 350.0 + View::get_bottom_inset(), |ui| { - View::app_logo_name_version(ui); - ui.add_space(4.0); + egui::CentralPanel::default() + .frame(egui::Frame { + inner_margin: if self.showing_settings() { + Margin { + left: (View::far_left_inset_margin(ui) + View::content_padding()) as i8, + right: (View::far_right_inset_margin(ui) + View::content_padding()) as i8, + top: 0, + bottom: 0, + } + } else { + Margin::default() + }, + fill: if self.showing_settings() { + Colors::fill_lite() + } else { + Colors::fill_deep() + }, + ..Default::default() + }) + .show_inside(ui, |ui| { + if self.showing_settings() { + if let Some(c) = &mut self.settings_content { + ScrollArea::vertical() + .id_salt("app_settings_wallets") + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(1.0); + ui.vertical_centered(|ui| { + // Show application settings content. + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + c.ui(ui, cb); + }); + }); + }); + } + } else if self.creating_wallet() { + // Show wallet creation content. + let mut created_wallet: Option = None; + let creation = self.creation_content.as_mut().unwrap(); + let pass = creation.pass.clone(); + creation.content_ui(ui, cb, |wallet| { + created_wallet = Some(wallet); + }); + if let Some(w) = &created_wallet { + self.creation_content = None; + self.wallets.add(w.clone()); + if let Ok(_) = w.open(pass.clone()) { + self.select_wallet(w, None, cb); + } + } + } else if self.wallets.list().is_empty() { + View::center_content(ui, 350.0 + View::get_bottom_inset(), |ui| { + View::app_logo_name_version(ui); + ui.add_space(4.0); - let text = t!("wallets.create_desc"); - ui.label(RichText::new(text) - .size(16.0) - .color(Colors::gray()) - ); - ui.add_space(8.0); - // Show wallet creation button. - let add_text = format!("{} {}", FOLDER_PLUS, t!("wallets.add")); - View::button(ui, add_text, Colors::white_or_black(false), || { - self.show_add_wallet_modal(); - }); - }); - } else { - return; - } - }); - } + let text = t!("wallets.create_desc"); + ui.label(RichText::new(text).size(16.0).color(Colors::gray())); + ui.add_space(8.0); + // Show wallet creation button. + let add_text = format!("{} {}", FOLDER_PLUS, t!("wallets.add")); + View::button(ui, add_text, Colors::white_or_black(false), || { + self.show_add_wallet_modal(); + }); + }); + } else { + return; + } + }); + } } impl WalletsContent { - /// Identifier for wallet deletion confirmation [`Modal`]. - pub const DELETE_CONFIRMATION_MODAL: &'static str = "wallets_delete_confirmation_modal"; + /// Identifier for wallet deletion confirmation [`Modal`]. + pub const DELETE_CONFIRMATION_MODAL: &'static str = "wallets_delete_confirmation_modal"; - /// Called to navigate back, return `true` if action was not consumed. - pub fn on_back(&mut self, cb: &dyn PlatformCallbacks) -> bool { - if self.showing_settings() { - // Close settings. - self.settings_content = None; - return false; - } else if self.creating_wallet() { - // Close wallet creation. - let creation = self.creation_content.as_mut().unwrap(); - if creation.on_back() { - self.creation_content = None; - } - return false; - } else if self.showing_wallet() { - // Go back at stack or close wallet. - if self.wallet_content.can_back() { - self.wallet_content.back(cb); - } else { - self.wallets.select(None); - } - return false; - } - true - } + /// Called to navigate back, return `true` if action was not consumed. + pub fn on_back(&mut self, cb: &dyn PlatformCallbacks) -> bool { + if self.showing_settings() { + // Close settings. + self.settings_content = None; + return false; + } else if self.creating_wallet() { + // Close wallet creation. + let creation = self.creation_content.as_mut().unwrap(); + if creation.on_back() { + self.creation_content = None; + } + return false; + } else if self.showing_wallet() { + // Go back at stack or close wallet. + if self.wallet_content.can_back() { + self.wallet_content.back(cb); + } else { + self.wallets.select(None); + } + return false; + } + true + } - /// Check if opened wallet is showing. - pub fn showing_wallet(&self) -> bool { - if let Some(w) = self.wallets.selected().as_ref() { - return w.is_open() && !w.is_deleted() && - w.get_config().chain_type == AppConfig::chain_type(); - } - false - } + /// Check if opened wallet is showing. + pub fn showing_wallet(&self) -> bool { + if let Some(w) = self.wallets.selected().as_ref() { + return w.is_open() + && !w.is_deleted() + && w.get_config().chain_type == AppConfig::chain_type(); + } + false + } - /// Check if wallet is creating. - pub fn creating_wallet(&self) -> bool { - self.creation_content.is_some() - } + /// Check if wallet is creating. + pub fn creating_wallet(&self) -> bool { + self.creation_content.is_some() + } - /// Check if application settings are showing. - pub fn showing_settings(&self) -> bool { - self.settings_content.is_some() - } + /// Check if application settings are showing. + pub fn showing_settings(&self) -> bool { + self.settings_content.is_some() + } - /// Handle data from deeplink or opened file. - fn on_data(&mut self, ui: &mut egui::Ui, data: Option, cb: &dyn PlatformCallbacks) { - let wallets_size = self.wallets.list().len(); - if wallets_size == 0 { - return; - } - // Close network panel on single panel mode. - if !Content::is_dual_panel_mode(ui.ctx()) && Content::is_network_panel_open() { - Content::toggle_network_panel(); - } - // Pass data to single wallet or show wallets selection. - if wallets_size == 1 { - let w = self.wallets.list()[0].clone(); - if w.is_open() { - self.select_wallet(&w, data, cb); - } else { - self.show_opening_modal(&w, data, cb); - } - } else { - self.wallet_selection_content = WalletListModal::new(None, data, true); - Modal::new(SELECT_WALLET_MODAL) - .position(ModalPosition::Center) - .title(t!("network_settings.choose_wallet")) - .show(); - } - } + /// Handle data from deeplink or opened file. + fn on_data(&mut self, ui: &mut egui::Ui, data: Option, cb: &dyn PlatformCallbacks) { + let wallets_size = self.wallets.list().len(); + if wallets_size == 0 { + return; + } + // Close network panel on single panel mode. + if !Content::is_dual_panel_mode(ui.ctx()) && Content::is_network_panel_open() { + Content::toggle_network_panel(); + } + // Pass data to single wallet or show wallets selection. + if wallets_size == 1 { + let w = self.wallets.list()[0].clone(); + if w.is_open() { + self.select_wallet(&w, data, cb); + } else { + self.show_opening_modal(&w, data, cb); + } + } else { + self.wallet_selection_content = WalletListModal::new(None, data, true); + Modal::new(SELECT_WALLET_MODAL) + .position(ModalPosition::Center) + .title(t!("network_settings.choose_wallet")) + .show(); + } + } - /// Show initial wallet creation [`Modal`]. - pub fn show_add_wallet_modal(&mut self) { - self.add_wallet_modal_content = AddWalletModal::default(); - Modal::new(ADD_WALLET_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.add")) - .show(); - } + /// Show initial wallet creation [`Modal`]. + pub fn show_add_wallet_modal(&mut self) { + self.add_wallet_modal_content = AddWalletModal::default(); + Modal::new(ADD_WALLET_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.add")) + .show(); + } - /// Draw [`TitlePanel`] content. - fn title_ui(&mut self, ui: &mut egui::Ui, dual_panel: bool, cb: &dyn PlatformCallbacks) { - let showing_settings = self.showing_settings(); - let show_wallet = self.showing_wallet(); - let show_list = AppConfig::show_wallets_at_dual_panel(); - let creating_wallet = self.creating_wallet(); + /// Draw [`TitlePanel`] content. + fn title_ui(&mut self, ui: &mut egui::Ui, dual_panel: bool, cb: &dyn PlatformCallbacks) { + let showing_settings = self.showing_settings(); + let show_wallet = self.showing_wallet(); + let show_list = AppConfig::show_wallets_at_dual_panel(); + let creating_wallet = self.creating_wallet(); - // Setup title. - let title_content = if show_wallet && (!dual_panel - || (dual_panel && !show_list)) && !creating_wallet && !showing_settings { - let title = self.wallet_content.title().into(); - let subtitle = self.wallets.selected().unwrap().get_config().name; - let wallet_title_content = if self.wallet_content.settings_content.is_some() { - TitleContentType::Title(title) - } else { - TitleContentType::WithSubTitle(title, subtitle, false) - }; - TitleType::Single(wallet_title_content) - } else { - let title_text = if showing_settings { - t!("settings") - } else if creating_wallet { - t!("wallets.add") - } else { - t!("wallets.title") - }.into(); - let dual_title = !showing_settings && !creating_wallet && - show_wallet && dual_panel; - if dual_title { - let title = self.wallet_content.title().into(); - let subtitle = self.wallets.selected().unwrap().get_config().name; - let wallet_title_content = if self.wallet_content.settings_content.is_some() { - TitleContentType::Title(title) - } else { - TitleContentType::WithSubTitle(title, subtitle, false) - }; - TitleType::Dual(TitleContentType::Title(title_text), wallet_title_content) - } else { - TitleType::Single(TitleContentType::Title(title_text)) - } - }; + // Setup title. + let title_content = if show_wallet + && (!dual_panel || (dual_panel && !show_list)) + && !creating_wallet + && !showing_settings + { + let title = self.wallet_content.title().into(); + let subtitle = self.wallets.selected().unwrap().get_config().name; + let wallet_title_content = if self.wallet_content.settings_content.is_some() { + TitleContentType::Title(title) + } else { + TitleContentType::WithSubTitle(title, subtitle, false) + }; + TitleType::Single(wallet_title_content) + } else { + let title_text = if showing_settings { + t!("settings") + } else if creating_wallet { + t!("wallets.add") + } else { + t!("wallets.title") + } + .into(); + let dual_title = !showing_settings && !creating_wallet && show_wallet && dual_panel; + if dual_title { + let title = self.wallet_content.title().into(); + let subtitle = self.wallets.selected().unwrap().get_config().name; + let wallet_title_content = if self.wallet_content.settings_content.is_some() { + TitleContentType::Title(title) + } else { + TitleContentType::WithSubTitle(title, subtitle, false) + }; + TitleType::Dual(TitleContentType::Title(title_text), wallet_title_content) + } else { + TitleType::Single(TitleContentType::Title(title_text)) + } + }; - // Draw title panel. - let mut show_settings = false; - let showing_settings = self.showing_settings(); - TitlePanel::new(Id::new("wallets_title_panel")).ui(title_content, |ui| { - if self.showing_settings() { - View::title_button_big(ui, ARROW_LEFT, |_| { - self.settings_content = None; - }); - } else if show_wallet && !dual_panel { - View::title_button_big(ui, ARROW_LEFT, |_| { - if self.wallet_content.can_back() { - self.wallet_content.back(cb); - } else { - self.wallets.select(None); - } - }); - } else if self.creating_wallet() { - let mut close = false; - if let Some(creation) = self.creation_content.as_mut() { - View::title_button_big(ui, ARROW_LEFT, |_| { - if creation.on_back() { - close = true; - } - }); - } - if close { - self.creation_content = None; - } - } else if show_wallet && dual_panel { - let list_icon = if show_list { - SIDEBAR_SIMPLE - } else { - SUITCASE - }; - View::title_button_big(ui, list_icon, |_| { - AppConfig::toggle_show_wallets_at_dual_panel(); - }); - } else if !Content::is_dual_panel_mode(ui.ctx()) { - View::title_button_big(ui, GLOBE, |_| { - Content::toggle_network_panel(); - }); - } - }, |ui| { - if !showing_settings { - View::title_button_big(ui, GEAR, |_| { - // Show application settings. - show_settings = true; - }); - } - }, ui); - if show_settings { - self.wallet_content.back(cb); - self.settings_content = Some(SettingsContent::default()); - } - } + // Draw title panel. + let mut show_settings = false; + let showing_settings = self.showing_settings(); + TitlePanel::new(Id::new("wallets_title_panel")).ui( + title_content, + |ui| { + if self.showing_settings() { + View::title_button_big(ui, ARROW_LEFT, |_| { + self.settings_content = None; + }); + } else if show_wallet && !dual_panel { + View::title_button_big(ui, ARROW_LEFT, |_| { + if self.wallet_content.can_back() { + self.wallet_content.back(cb); + } else { + self.wallets.select(None); + } + }); + } else if self.creating_wallet() { + let mut close = false; + if let Some(creation) = self.creation_content.as_mut() { + View::title_button_big(ui, ARROW_LEFT, |_| { + if creation.on_back() { + close = true; + } + }); + } + if close { + self.creation_content = None; + } + } else if show_wallet && dual_panel { + let list_icon = if show_list { SIDEBAR_SIMPLE } else { SUITCASE }; + View::title_button_big(ui, list_icon, |_| { + AppConfig::toggle_show_wallets_at_dual_panel(); + }); + } else if !Content::is_dual_panel_mode(ui.ctx()) { + View::title_button_big(ui, GLOBE, |_| { + Content::toggle_network_panel(); + }); + } + }, + |ui| { + if !showing_settings { + View::title_button_big(ui, GEAR, |_| { + // Show application settings. + show_settings = true; + }); + } + }, + ui, + ); + if show_settings { + self.wallet_content.back(cb); + self.settings_content = Some(SettingsContent::default()); + } + } - /// Draw list of wallets. - fn wallet_list_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - ScrollArea::vertical() - .id_salt("wallet_list_scroll") - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .auto_shrink([false; 2]) - .show(ui, |ui| { - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - // Show application logo and name. - View::app_logo_name_version(ui); - ui.add_space(15.0); + /// Draw list of wallets. + fn wallet_list_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + ScrollArea::vertical() + .id_salt("wallet_list_scroll") + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) + .show(ui, |ui| { + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + // Show application logo and name. + View::app_logo_name_version(ui); + ui.add_space(15.0); - // Show result of update check. - if AppConfig::check_updates() { - if let Some(res) = self.check_update.read_or_request(|| async { - retrieve_release().await - }) { - let checked = self.update_info.0; - if !checked { - self.update_info.0 = true; - match res { - Ok(info) => { - if info.is_update() { - AppConfig::save_update(Some(info)); - } else { - AppConfig::save_update(None); - } - self.update_info.1 = AppConfig::app_update(); - } - Err(_) => AppConfig::save_update(None), - } - } - } - // Show update information. - self.update_info_ui(ui); - } + // Show result of update check. + if AppConfig::check_updates() { + if let Some(res) = self + .check_update + .read_or_request(|| async { retrieve_release().await }) + { + let checked = self.update_info.0; + if !checked { + self.update_info.0 = true; + match res { + Ok(info) => { + if info.is_update() { + AppConfig::save_update(Some(info)); + } else { + AppConfig::save_update(None); + } + self.update_info.1 = AppConfig::app_update(); + } + Err(_) => AppConfig::save_update(None), + } + } + } + // Show update information. + self.update_info_ui(ui); + } - let list = self.wallets.list().clone(); - for w in list.iter() { - let id = w.get_config().id; - // Remove deleted. - if w.is_deleted() { - self.wallets.select(None); - self.wallets.remove(id); - ui.ctx().request_repaint(); - continue; - } - // Check if wallet reopen is needed. - if w.reopen_needed() && !w.is_open() { - w.set_reopen(false); - self.show_opening_modal(w, None, cb); - } - // Check if wallet is selected. - let current = if let Some(selected) = self.wallets.selected().as_ref() { - selected.get_config().id == id - } else { - false - }; - // Unselect wallet when opening or settings modal was closed. - if current && !w.is_open() && Modal::opened().is_none() { - self.wallets.select(None); - } - self.wallet_item_ui(ui, w, current, cb); - ui.add_space(6.0); - } - }); - }); - } + let list = self.wallets.list().clone(); + for w in list.iter() { + let id = w.get_config().id; + // Remove deleted. + if w.is_deleted() { + self.wallets.select(None); + self.wallets.remove(id); + ui.ctx().request_repaint(); + continue; + } + // Check if wallet reopen is needed. + if w.reopen_needed() && !w.is_open() { + w.set_reopen(false); + self.show_opening_modal(w, None, cb); + } + // Check if wallet is selected. + let current = if let Some(selected) = self.wallets.selected().as_ref() { + selected.get_config().id == id + } else { + false + }; + // Unselect wallet when opening or settings modal was closed. + if current && !w.is_open() && Modal::opened().is_none() { + self.wallets.select(None); + } + self.wallet_item_ui(ui, w, current, cb); + ui.add_space(6.0); + } + }); + }); + } - /// Draw wallet list item. - fn wallet_item_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - current: bool, - cb: &dyn PlatformCallbacks) { - let config = wallet.get_config(); - let can_open = !wallet.is_open() && !wallet.files_moving(); + /// Draw wallet list item. + fn wallet_item_ui( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + current: bool, + cb: &dyn PlatformCallbacks, + ) { + let config = wallet.get_config(); + let can_open = !wallet.is_open() && !wallet.files_moving(); - // Draw round background. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(78.0); - let r = View::item_rounding(0, 1, false); - let bg = if current { - Colors::fill_deep() - } else { - Colors::fill() - }; - let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); - let bg_idx = ui.painter().add(bg_shape.clone()); + // Draw round background. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(78.0); + let r = View::item_rounding(0, 1, false); + let bg = if current { + Colors::fill_deep() + } else { + Colors::fill() + }; + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - let res = ui.scope_builder( - UiBuilder::new() - .sense(Sense::click()) - .layout(Layout::right_to_left(Align::Center)) - .max_rect(rect), |ui| { - if can_open { - if !wallet.is_repairing() { - View::item_button(ui, View::item_rounding(0, 1, true), GEAR_FINE, None, || { - self.select_wallet(wallet, None, cb); - let conn = wallet.get_current_connection(); - self.wallet_settings_content = WalletSettingsModal::new(conn); - // Show connection selection modal. - Modal::new(WALLET_SETTINGS_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.settings")) - .show(); - }); - } - } else if !wallet.is_closing() { - // Show button to close opened wallet. - View::item_button(ui, View::item_rounding(0, 1, true), LOCK_KEY, None, || { - wallet.close(); - }); - } + let res = ui + .scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), + |ui| { + if can_open { + if !wallet.is_repairing() { + View::item_button( + ui, + View::item_rounding(0, 1, true), + GEAR_FINE, + None, + || { + self.select_wallet(wallet, None, cb); + let conn = wallet.get_current_connection(); + self.wallet_settings_content = WalletSettingsModal::new(conn); + // Show connection selection modal. + Modal::new(WALLET_SETTINGS_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.settings")) + .show(); + }, + ); + } + } else if !wallet.is_closing() { + // Show button to close opened wallet. + View::item_button( + ui, + View::item_rounding(0, 1, true), + LOCK_KEY, + None, + || { + wallet.close(); + }, + ); + } - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - ui.add_space(3.0); - // Show wallet name text. - let name_color = if current { - Colors::white_or_black(true) - } else { - Colors::title(false) - }; - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.add_space(1.0); - View::ellipsize_text(ui, config.name, 18.0, name_color); - }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout( + layout_size, + Layout::left_to_right(Align::Center), + |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(3.0); + // Show wallet name text. + let name_color = if current { + Colors::white_or_black(true) + } else { + Colors::title(false) + }; + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.add_space(1.0); + View::ellipsize_text(ui, config.name, 18.0, name_color); + }); - // Show wallet status text. - let status_text = wallet_status_text(wallet); - View::ellipsize_text(ui, status_text, 15.0, Colors::text(false)); - ui.add_space(1.0); + // Show wallet status text. + let status_text = wallet_status_text(wallet); + View::ellipsize_text(ui, status_text, 15.0, Colors::text(false)); + ui.add_space(1.0); - // Show wallet connection text. - let connection = wallet.get_current_connection(); - let conn_text = match connection { - ConnectionMethod::Integrated => { - format!("{} {}", COMPUTER_TOWER, t!("network.node")) - } - ConnectionMethod::External(_, url) => { - format!("{} {}", GLOBE_SIMPLE, url) - } - }; - View::ellipsize_text(ui, conn_text, 15.0, Colors::gray()); - ui.add_space(3.0); - }); - }); - } - ).response; - let clicked = res.clicked() || res.long_touched(); - // Setup background and cursor. - if res.hovered() && (can_open || !current) { - res.on_hover_cursor(CursorIcon::PointingHand); - bg_shape.fill = Colors::fill_deep(); - } - ui.painter().set(bg_idx, bg_shape); - // Handle clicks on layout. - if clicked { - if can_open { - // Show modal to open the wallet. - self.show_opening_modal(wallet, None, cb); - } else if !current { - // Select opened wallet. - self.select_wallet(wallet, None, cb); - } - } - } + // Show wallet connection text. + let connection = wallet.get_current_connection(); + let conn_text = match connection { + ConnectionMethod::Integrated => { + format!("{} {}", COMPUTER_TOWER, t!("network.node")) + } + ConnectionMethod::External(_, url) => { + format!("{} {}", GLOBE_SIMPLE, url) + } + }; + View::ellipsize_text(ui, conn_text, 15.0, Colors::gray()); + ui.add_space(3.0); + }); + }, + ); + }, + ) + .response; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if res.hovered() && (can_open || !current) { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill_deep(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked { + if can_open { + // Show modal to open the wallet. + self.show_opening_modal(wallet, None, cb); + } else if !current { + // Select opened wallet. + self.select_wallet(wallet, None, cb); + } + } + } - /// Draw update information content. - fn update_info_ui(&mut self, ui: &mut egui::Ui) { - if self.update_info.1.is_none() { - return; - } - let update = self.update_info.1.as_ref().unwrap(); - ui.add_space(-4.0); - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(78.0); - let r = View::item_rounding(0, 1, false); - ui.painter().rect(rect, r, Colors::fill(), View::item_stroke(), StrokeKind::Outside); - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - // Show button to download the update. - let mut link_clicked = false; - View::item_button(ui, View::item_rounding(0, 1, true), CLOUD_ARROW_DOWN, None, || { - link_clicked = true; - }); - if link_clicked { - ui.ctx().open_url(OpenUrl { - url: update.url.clone(), - new_tab: true, - }); - } - // Show button to see update information. - View::item_button(ui, CornerRadius::default(), NOTEPAD, None, || { - self.changelog_content = Some(ChangelogContent::new(update.changelog.clone())); - let title = format!("Grim {}", update.version); - Modal::new(ChangelogContent::MODAL_ID) - .position(ModalPosition::Center) - .title(title) - .show(); - }); + /// Draw update information content. + fn update_info_ui(&mut self, ui: &mut egui::Ui) { + if self.update_info.1.is_none() { + return; + } + let update = self.update_info.1.as_ref().unwrap(); + ui.add_space(-4.0); + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(78.0); + let r = View::item_rounding(0, 1, false); + ui.painter().rect( + rect, + r, + Colors::fill(), + View::item_stroke(), + StrokeKind::Outside, + ); + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + // Show button to download the update. + let mut link_clicked = false; + View::item_button( + ui, + View::item_rounding(0, 1, true), + CLOUD_ARROW_DOWN, + None, + || { + link_clicked = true; + }, + ); + if link_clicked { + ui.ctx().open_url(OpenUrl { + url: update.url.clone(), + new_tab: true, + }); + } + // Show button to see update information. + View::item_button(ui, CornerRadius::default(), NOTEPAD, None, || { + self.changelog_content = Some(ChangelogContent::new(update.changelog.clone())); + let title = format!("Grim {}", update.version); + Modal::new(ChangelogContent::MODAL_ID) + .position(ModalPosition::Center) + .title(title) + .show(); + }); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - ui.add_space(3.0); - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.add_space(1.0); - let update_text = "Update is available!"; - View::ellipsize_text(ui, update_text, 18.0, Colors::green()); - }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(3.0); + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.add_space(1.0); + let update_text = "Update is available!"; + View::ellipsize_text(ui, update_text, 18.0, Colors::green()); + }); - // Show version info. - let ver_text = if let Some(size) = update.size.as_ref() { - format!("{} {} ({} MB)", BOOKMARKS, update.version, size) - } else { - format!("{} {} > {}", BOOKMARKS, crate::VERSION, update.version) - }; - View::ellipsize_text(ui, ver_text, 15.0, Colors::text(false)); - ui.add_space(1.0); + // Show version info. + let ver_text = if let Some(size) = update.size.as_ref() { + format!("{} {} ({} MB)", BOOKMARKS, update.version, size) + } else { + format!("{} {} > {}", BOOKMARKS, crate::VERSION, update.version) + }; + View::ellipsize_text(ui, ver_text, 15.0, Colors::text(false)); + ui.add_space(1.0); - // Show update date. - let date_text = format!("{} {}", CALENDAR_CHECK, update.date); - View::ellipsize_text(ui, date_text, 15.0, Colors::gray()); - ui.add_space(3.0); - }); - }); - }); - ui.add_space(12.0); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(12.0); - } + // Show update date. + let date_text = format!("{} {}", CALENDAR_CHECK, update.date); + View::ellipsize_text(ui, date_text, 15.0, Colors::gray()); + ui.add_space(3.0); + }); + }); + }); + ui.add_space(12.0); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(12.0); + } - /// Show [`Modal`] to select and open wallet. - fn show_opening_modal(&mut self, w: &Wallet, data: Option, cb: &dyn PlatformCallbacks) { - self.select_wallet(w, data, cb); - self.open_wallet_content = OpenWalletModal::new(); - Modal::new(OPEN_WALLET_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.open")) - .show(); - } + /// Show [`Modal`] to select and open wallet. + fn show_opening_modal(&mut self, w: &Wallet, data: Option, cb: &dyn PlatformCallbacks) { + self.select_wallet(w, data, cb); + self.open_wallet_content = OpenWalletModal::new(); + Modal::new(OPEN_WALLET_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.open")) + .show(); + } - /// Select wallet to make some actions on it. - fn select_wallet(&mut self, w: &Wallet, data: Option, cb: &dyn PlatformCallbacks) { - self.wallet_content.back(cb); - if let Some(data) = data { - w.task(WalletTask::OpenMessage(data)); - } - self.wallets.select(Some(w.get_config().id)); - } + /// Select wallet to make some actions on it. + fn select_wallet(&mut self, w: &Wallet, data: Option, cb: &dyn PlatformCallbacks) { + self.wallet_content.back(cb); + if let Some(data) = data { + w.task(WalletTask::OpenMessage(data)); + } + self.wallets.select(Some(w.get_config().id)); + } } /// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time. fn is_dual_panel_mode(ui: &mut egui::Ui) -> bool { - let dual_panel_root = Content::is_dual_panel_mode(ui.ctx()); - let max_width = ui.available_width(); - dual_panel_root && max_width >= (Content::SIDE_PANEL_WIDTH * 2.0) + View::get_right_inset() -} \ No newline at end of file + let dual_panel_root = Content::is_dual_panel_mode(ui.ctx()); + let max_width = ui.available_width(); + dual_panel_root && max_width >= (Content::SIDE_PANEL_WIDTH * 2.0) + View::get_right_inset() +} diff --git a/src/gui/views/wallets/creation/content.rs b/src/gui/views/wallets/creation/content.rs index bf9a76ea..57d5d469 100644 --- a/src/gui/views/wallets/creation/content.rs +++ b/src/gui/views/wallets/creation/content.rs @@ -12,362 +12,365 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{Id, Margin, RichText, ScrollArea}; use egui::scroll_area::ScrollBarVisibility; +use egui::{Id, Margin, RichText, ScrollArea}; use grin_util::ZeroingString; use crate::gui::Colors; use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, SCAN}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, Content, View, CameraScanContent}; -use crate::gui::views::types::{LinePosition, ContentContainer, ModalPosition, QrScanResult}; +use crate::gui::views::types::{ContentContainer, LinePosition, ModalPosition, QrScanResult}; +use crate::gui::views::wallets::ConnectionSettings; use crate::gui::views::wallets::creation::MnemonicSetup; use crate::gui::views::wallets::creation::types::Step; -use crate::gui::views::wallets::ConnectionSettings; +use crate::gui::views::{CameraScanContent, Content, Modal, View}; use crate::node::Node; -use crate::wallet::{ExternalConnection, Wallet}; use crate::wallet::types::PhraseMode; +use crate::wallet::{ExternalConnection, Wallet}; /// Wallet creation content. pub struct WalletCreationContent { - /// Wallet name. - pub name: String, - /// Wallet password. - pub pass: ZeroingString, + /// Wallet name. + pub name: String, + /// Wallet password. + pub pass: ZeroingString, - /// Wallet creation step. - step: Step, + /// Wallet creation step. + step: Step, - /// QR code scanning [`Modal`] content. - scan_modal_content: Option, + /// QR code scanning [`Modal`] content. + scan_modal_content: Option, - /// Mnemonic phrase setup content. - mnemonic_setup: MnemonicSetup, - /// Network setup content. - network_setup: ConnectionSettings, + /// Mnemonic phrase setup content. + mnemonic_setup: MnemonicSetup, + /// Network setup content. + network_setup: ConnectionSettings, - /// Flag to check if an error occurred during wallet creation. - creation_error: Option, + /// Flag to check if an error occurred during wallet creation. + creation_error: Option, } const QR_CODE_PHRASE_SCAN_MODAL: &'static str = "qr_code_rec_phrase_scan_modal"; impl ContentContainer for WalletCreationContent { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - QR_CODE_PHRASE_SCAN_MODAL - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![QR_CODE_PHRASE_SCAN_MODAL] + } - fn modal_ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - match modal.id { - QR_CODE_PHRASE_SCAN_MODAL => { - if let Some(content) = self.scan_modal_content.as_mut() { - content.modal_ui(ui, cb, |result| { - match result { - QrScanResult::Text(text) => { - self.mnemonic_setup.mnemonic.import(&text); - Modal::close(); - } - QrScanResult::SeedQR(text) => { - self.mnemonic_setup.mnemonic.import(&text); - Modal::close(); - } - _ => {} - } - }); - } - }, + fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + match modal.id { + QR_CODE_PHRASE_SCAN_MODAL => { + if let Some(content) = self.scan_modal_content.as_mut() { + content.modal_ui(ui, cb, |result| match result { + QrScanResult::Text(text) => { + self.mnemonic_setup.mnemonic.import(&text); + Modal::close(); + } + QrScanResult::SeedQR(text) => { + self.mnemonic_setup.mnemonic.import(&text); + Modal::close(); + } + _ => {} + }); + } + } - _ => {} - } - } + _ => {} + } + } - fn container_ui(&mut self, _: &mut egui::Ui, _: &dyn PlatformCallbacks) { - } + fn container_ui(&mut self, _: &mut egui::Ui, _: &dyn PlatformCallbacks) {} } impl WalletCreationContent { - /// Create new wallet creation content from name and password. - pub fn new(name: String, pass: ZeroingString) -> Self { - Self { - name, - pass, - step: Step::EnterMnemonic, - scan_modal_content: None, - mnemonic_setup: MnemonicSetup::default(), - network_setup: ConnectionSettings::default(), - creation_error: None, - } - } + /// Create new wallet creation content from name and password. + pub fn new(name: String, pass: ZeroingString) -> Self { + Self { + name, + pass, + step: Step::EnterMnemonic, + scan_modal_content: None, + mnemonic_setup: MnemonicSetup::default(), + network_setup: ConnectionSettings::default(), + creation_error: None, + } + } - /// Draw wallet creation content. - pub fn content_ui(&mut self, - ui: &mut egui::Ui, - cb: &dyn PlatformCallbacks, - on_create: impl FnMut(Wallet)) { - self.ui(ui, cb); - egui::TopBottomPanel::bottom("wallet_creation_step_panel") - .frame(egui::Frame { - inner_margin: Margin { - left: (View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING) as i8, - right: (View::get_right_inset() + View::TAB_ITEMS_PADDING) as i8, - top: View::TAB_ITEMS_PADDING as i8, - bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8, - }, - fill: Colors::fill(), - ..Default::default() - }) - .show_inside(ui, |ui| { - // Draw divider line. - let rect = { - let mut r = ui.available_rect_before_wrap(); - r.min.y -= View::TAB_ITEMS_PADDING; - r.min.x -= View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING; - r.max.x += View::get_right_inset() + View::TAB_ITEMS_PADDING; - r - }; - View::line(ui, LinePosition::TOP, &rect, Colors::item_stroke()); - // Show step control content. - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - self.step_control_ui(ui, on_create, cb); - }); - }); + /// Draw wallet creation content. + pub fn content_ui( + &mut self, + ui: &mut egui::Ui, + cb: &dyn PlatformCallbacks, + on_create: impl FnMut(Wallet), + ) { + self.ui(ui, cb); + egui::TopBottomPanel::bottom("wallet_creation_step_panel") + .frame(egui::Frame { + inner_margin: Margin { + left: (View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING) as i8, + right: (View::get_right_inset() + View::TAB_ITEMS_PADDING) as i8, + top: View::TAB_ITEMS_PADDING as i8, + bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8, + }, + fill: Colors::fill(), + ..Default::default() + }) + .show_inside(ui, |ui| { + // Draw divider line. + let rect = { + let mut r = ui.available_rect_before_wrap(); + r.min.y -= View::TAB_ITEMS_PADDING; + r.min.x -= View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING; + r.max.x += View::get_right_inset() + View::TAB_ITEMS_PADDING; + r + }; + View::line(ui, LinePosition::TOP, &rect, Colors::item_stroke()); + // Show step control content. + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + self.step_control_ui(ui, on_create, cb); + }); + }); - // Show wallet creation step content panel. - egui::CentralPanel::default() - .frame(egui::Frame { - inner_margin: Margin { - left: (View::far_left_inset_margin(ui) + 4.0) as i8, - right: (View::get_right_inset() + 4.0) as i8, - top: 3.0 as i8, - bottom: 4.0 as i8, - }, - fill: Colors::fill_lite(), - ..Default::default() - }) - .show_inside(ui, |ui| { - ScrollArea::vertical() - .id_salt(Id::from(format!("creation_step_scroll_{}", self.step.name()))) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .auto_shrink([false; 2]) - .show(ui, |ui| { - let max_width = if self.step == Step::SetupConnection { - Content::SIDE_PANEL_WIDTH * 1.3 - } else { - Content::SIDE_PANEL_WIDTH * 2.0 - }; - View::max_width_ui(ui, max_width, |ui| { - self.step_content_ui(ui, cb); - }); - }); - }); - } + // Show wallet creation step content panel. + egui::CentralPanel::default() + .frame(egui::Frame { + inner_margin: Margin { + left: (View::far_left_inset_margin(ui) + 4.0) as i8, + right: (View::get_right_inset() + 4.0) as i8, + top: 3.0 as i8, + bottom: 4.0 as i8, + }, + fill: Colors::fill_lite(), + ..Default::default() + }) + .show_inside(ui, |ui| { + ScrollArea::vertical() + .id_salt(Id::from(format!( + "creation_step_scroll_{}", + self.step.name() + ))) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) + .show(ui, |ui| { + let max_width = if self.step == Step::SetupConnection { + Content::SIDE_PANEL_WIDTH * 1.3 + } else { + Content::SIDE_PANEL_WIDTH * 2.0 + }; + View::max_width_ui(ui, max_width, |ui| { + self.step_content_ui(ui, cb); + }); + }); + }); + } - /// Draw [`Step`] description and confirmation control. - fn step_control_ui(&mut self, - ui: &mut egui::Ui, - on_create: impl FnOnce(Wallet), - cb: &dyn PlatformCallbacks) { - let step = &self.step; - // Setup description and next step availability. - let (step_text, mut next) = match step { - Step::EnterMnemonic => { - let mode = &self.mnemonic_setup.mnemonic.mode(); - let (text, available) = match mode { - PhraseMode::Generate => (t!("wallets.create_phrase_desc"), true), - PhraseMode::Import => { - let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid(); - (t!("wallets.restore_phrase_desc"), available) - } - }; - (text, available) - } - Step::ConfirmMnemonic => { - let text = t!("wallets.restore_phrase_desc"); - let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid(); - (text, available) - } - Step::SetupConnection => { - (t!("wallets.setup_conn_desc"), self.creation_error.is_none()) - } - }; + /// Draw [`Step`] description and confirmation control. + fn step_control_ui( + &mut self, + ui: &mut egui::Ui, + on_create: impl FnOnce(Wallet), + cb: &dyn PlatformCallbacks, + ) { + let step = &self.step; + // Setup description and next step availability. + let (step_text, mut next) = match step { + Step::EnterMnemonic => { + let mode = &self.mnemonic_setup.mnemonic.mode(); + let (text, available) = match mode { + PhraseMode::Generate => (t!("wallets.create_phrase_desc"), true), + PhraseMode::Import => { + let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid(); + (t!("wallets.restore_phrase_desc"), available) + } + }; + (text, available) + } + Step::ConfirmMnemonic => { + let text = t!("wallets.restore_phrase_desc"); + let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid(); + (text, available) + } + Step::SetupConnection => (t!("wallets.setup_conn_desc"), self.creation_error.is_none()), + }; - // Show step description or error. - let generate_step = step == &Step::EnterMnemonic && - self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate; - if (self.mnemonic_setup.mnemonic.valid() && self.creation_error.is_none()) || - generate_step { - ui.label(RichText::new(step_text).size(16.0).color(Colors::gray())); - ui.add_space(6.0); - } else { - next = false; - // Show error text. - if let Some(err) = &self.creation_error { - ui.add_space(10.0); - ui.label(RichText::new(err) - .size(16.0) - .color(Colors::red())); - ui.add_space(10.0); - } else { - ui.label(RichText::new(t!("wallets.not_valid_phrase")) - .size(16.0) - .color(Colors::red())); - ui.add_space(4.0); - }; - } + // Show step description or error. + let generate_step = step == &Step::EnterMnemonic + && self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate; + if (self.mnemonic_setup.mnemonic.valid() && self.creation_error.is_none()) || generate_step + { + ui.label(RichText::new(step_text).size(16.0).color(Colors::gray())); + ui.add_space(6.0); + } else { + next = false; + // Show error text. + if let Some(err) = &self.creation_error { + ui.add_space(10.0); + ui.label(RichText::new(err).size(16.0).color(Colors::red())); + ui.add_space(10.0); + } else { + ui.label( + RichText::new(t!("wallets.not_valid_phrase")) + .size(16.0) + .color(Colors::red()), + ); + ui.add_space(4.0); + }; + } - // Setup spacing between buttons. - ui.style_mut().spacing.item_spacing = egui::vec2(8.0, 0.0); - // Setup vertical padding inside button. - ui.style_mut().spacing.button_padding = egui::vec2(10.0, 7.0); + // Setup spacing between buttons. + ui.style_mut().spacing.item_spacing = egui::vec2(8.0, 0.0); + // Setup vertical padding inside button. + ui.style_mut().spacing.button_padding = egui::vec2(10.0, 7.0); - match step { - Step::EnterMnemonic => { - ui.columns(2, |columns| { - // Show copy or paste button for mnemonic phrase step. - columns[0].vertical_centered_justified(|ui| { - match self.mnemonic_setup.mnemonic.mode() { - PhraseMode::Generate => { - let c_t = format!("{} {}", COPY, t!("copy")); - View::button(ui, c_t, Colors::white_or_black(false), || { - cb.copy_string_to_buffer(self.mnemonic_setup - .mnemonic - .get_phrase()); - }); - } - PhraseMode::Import => { - let p_t = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); - View::button(ui, p_t, Colors::white_or_black(false), || { - let data = ZeroingString::from(cb.get_string_from_buffer()); - self.mnemonic_setup.mnemonic.import(&data); - }); - } - } - }); - // Show next step or QR code scan button. - columns[1].vertical_centered_justified(|ui| { - if next { - self.next_step_button_ui(ui, on_create); - } else { - let scan_text = format!("{} {}", SCAN, t!("scan")); - View::button(ui, scan_text, Colors::white_or_black(false), || { - self.scan_modal_content = Some(CameraScanContent::default()); - // Show QR code scan modal. - Modal::new(QR_CODE_PHRASE_SCAN_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("scan_qr")) - .closeable(false) - .show(); - cb.start_camera(); - }); - } - }); - }); - } - Step::ConfirmMnemonic => { - // Show next step or paste button. - if next { - self.next_step_button_ui(ui, on_create); - } else { - let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); - View::button(ui, paste_text, Colors::white_or_black(false), || { - let data = ZeroingString::from(cb.get_string_from_buffer()); - self.mnemonic_setup.mnemonic.import(&data); - }); - } - } - Step::SetupConnection => { - if next { - self.next_step_button_ui(ui, on_create); - ui.add_space(2.0); - } - } - } - } + match step { + Step::EnterMnemonic => { + ui.columns(2, |columns| { + // Show copy or paste button for mnemonic phrase step. + columns[0].vertical_centered_justified(|ui| { + match self.mnemonic_setup.mnemonic.mode() { + PhraseMode::Generate => { + let c_t = format!("{} {}", COPY, t!("copy")); + View::button(ui, c_t, Colors::white_or_black(false), || { + cb.copy_string_to_buffer( + self.mnemonic_setup.mnemonic.get_phrase(), + ); + }); + } + PhraseMode::Import => { + let p_t = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); + View::button(ui, p_t, Colors::white_or_black(false), || { + let data = ZeroingString::from(cb.get_string_from_buffer()); + self.mnemonic_setup.mnemonic.import(&data); + }); + } + } + }); + // Show next step or QR code scan button. + columns[1].vertical_centered_justified(|ui| { + if next { + self.next_step_button_ui(ui, on_create); + } else { + let scan_text = format!("{} {}", SCAN, t!("scan")); + View::button(ui, scan_text, Colors::white_or_black(false), || { + self.scan_modal_content = Some(CameraScanContent::default()); + // Show QR code scan modal. + Modal::new(QR_CODE_PHRASE_SCAN_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("scan_qr")) + .closeable(false) + .show(); + cb.start_camera(); + }); + } + }); + }); + } + Step::ConfirmMnemonic => { + // Show next step or paste button. + if next { + self.next_step_button_ui(ui, on_create); + } else { + let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); + View::button(ui, paste_text, Colors::white_or_black(false), || { + let data = ZeroingString::from(cb.get_string_from_buffer()); + self.mnemonic_setup.mnemonic.import(&data); + }); + } + } + Step::SetupConnection => { + if next { + self.next_step_button_ui(ui, on_create); + ui.add_space(2.0); + } + } + } + } - /// Draw button to go to next [`Step`]. - fn next_step_button_ui(&mut self, - ui: &mut egui::Ui, - on_create: impl FnOnce(Wallet)) { - // Setup button text. - let (next_text, text_color, bg_color) = if self.step == Step::SetupConnection { - (format!("{} {}", CHECK, t!("complete")), Colors::title(true), Colors::gold()) - } else { - (t!("continue").into(), Colors::green(), Colors::white_or_black(false)) - }; + /// Draw button to go to next [`Step`]. + fn next_step_button_ui(&mut self, ui: &mut egui::Ui, on_create: impl FnOnce(Wallet)) { + // Setup button text. + let (next_text, text_color, bg_color) = if self.step == Step::SetupConnection { + ( + format!("{} {}", CHECK, t!("complete")), + Colors::title(true), + Colors::gold(), + ) + } else { + ( + t!("continue").into(), + Colors::green(), + Colors::white_or_black(false), + ) + }; - // Show next step button. - View::colored_text_button_ui(ui, next_text.to_uppercase(), text_color, bg_color, |ui| { - self.step = match self.step { - Step::EnterMnemonic => { - if self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate { - Step::ConfirmMnemonic - } else { - Step::SetupConnection - } - } - Step::ConfirmMnemonic => { - Step::SetupConnection - }, - Step::SetupConnection => { - // Create wallet at last step. - match Wallet::create(&self.name, - &self.pass, - &self.mnemonic_setup.mnemonic, - &self.network_setup.method) { - Ok(w) => { - self.mnemonic_setup.reset(); - // Pass created wallet to callback. - (on_create)(w); - Step::EnterMnemonic - } - Err(e) => { - self.creation_error = Some(format!("{:?}", e)); - Step::SetupConnection - } - } - } - }; + // Show next step button. + View::colored_text_button_ui(ui, next_text.to_uppercase(), text_color, bg_color, |ui| { + self.step = match self.step { + Step::EnterMnemonic => { + if self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate { + Step::ConfirmMnemonic + } else { + Step::SetupConnection + } + } + Step::ConfirmMnemonic => Step::SetupConnection, + Step::SetupConnection => { + // Create wallet at last step. + match Wallet::create( + &self.name, + &self.pass, + &self.mnemonic_setup.mnemonic, + &self.network_setup.method, + ) { + Ok(w) => { + self.mnemonic_setup.reset(); + // Pass created wallet to callback. + (on_create)(w); + Step::EnterMnemonic + } + Err(e) => { + self.creation_error = Some(format!("{:?}", e)); + Step::SetupConnection + } + } + } + }; - // Check external connections availability on connection setup. - if self.step == Step::SetupConnection { - ExternalConnection::check(None, ui.ctx()); - } - }); - } + // Check external connections availability on connection setup. + if self.step == Step::SetupConnection { + ExternalConnection::check(None, ui.ctx()); + } + }); + } - /// Draw wallet creation [`Step`] content. - fn step_content_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - match &self.step { - Step::EnterMnemonic => self.mnemonic_setup.enter_ui(ui, cb), - Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui, cb), - Step::SetupConnection => { - // Redraw if node is running. - if Node::is_running() && !Content::is_dual_panel_mode(ui.ctx()) { - ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY); - } - self.network_setup.ui(ui, cb); - } - } - } + /// Draw wallet creation [`Step`] content. + fn step_content_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + match &self.step { + Step::EnterMnemonic => self.mnemonic_setup.enter_ui(ui, cb), + Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui, cb), + Step::SetupConnection => { + // Redraw if node is running. + if Node::is_running() && !Content::is_dual_panel_mode(ui.ctx()) { + ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY); + } + self.network_setup.ui(ui, cb); + } + } + } - /// Back to previous wallet creation [`Step`], return `true` to close creation. - pub fn on_back(&mut self) -> bool { - match &self.step { - Step::ConfirmMnemonic => { - self.step = Step::EnterMnemonic; - false - }, - Step::SetupConnection => { - self.creation_error = None; - self.step = Step::EnterMnemonic; - false - } - _ => true - } - } -} \ No newline at end of file + /// Back to previous wallet creation [`Step`], return `true` to close creation. + pub fn on_back(&mut self) -> bool { + match &self.step { + Step::ConfirmMnemonic => { + self.step = Step::EnterMnemonic; + false + } + Step::SetupConnection => { + self.creation_error = None; + self.step = Step::EnterMnemonic; + false + } + _ => true, + } + } +} diff --git a/src/gui/views/wallets/creation/mnemonic.rs b/src/gui/views/wallets/creation/mnemonic.rs index fc945d19..e7dfa8e4 100644 --- a/src/gui/views/wallets/creation/mnemonic.rs +++ b/src/gui/views/wallets/creation/mnemonic.rs @@ -17,298 +17,307 @@ use egui::{Id, RichText}; use crate::gui::Colors; use crate::gui::icons::PENCIL; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, Content, View, TextEdit}; use crate::gui::views::types::{ContentContainer, ModalPosition}; +use crate::gui::views::{Content, Modal, TextEdit, View}; use crate::wallet::Mnemonic; use crate::wallet::types::{PhraseMode, PhraseSize, PhraseWord}; /// Mnemonic phrase setup content. pub struct MnemonicSetup { - /// Current mnemonic phrase. - pub mnemonic: Mnemonic, + /// Current mnemonic phrase. + pub mnemonic: Mnemonic, - /// Current word number to edit at [`Modal`]. - word_index_edit: usize, - /// Entered word value for [`Modal`]. - word_edit: String, - /// Flag to check if entered word is valid at [`Modal`]. - valid_word_edit: bool, + /// Current word number to edit at [`Modal`]. + word_index_edit: usize, + /// Entered word value for [`Modal`]. + word_edit: String, + /// Flag to check if entered word is valid at [`Modal`]. + valid_word_edit: bool, } /// Identifier for word input [`Modal`]. pub const WORD_INPUT_MODAL: &'static str = "word_input_modal"; impl Default for MnemonicSetup { - fn default() -> Self { - Self { - mnemonic: Mnemonic::default(), - word_index_edit: 0, - word_edit: String::from(""), - valid_word_edit: true, - } - } + fn default() -> Self { + Self { + mnemonic: Mnemonic::default(), + word_index_edit: 0, + word_edit: String::from(""), + valid_word_edit: true, + } + } } impl ContentContainer for MnemonicSetup { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - WORD_INPUT_MODAL - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![WORD_INPUT_MODAL] + } - fn modal_ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - match modal.id { - WORD_INPUT_MODAL => self.word_modal_ui(ui, modal, cb), - _ => {} - } - } + fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + match modal.id { + WORD_INPUT_MODAL => self.word_modal_ui(ui, modal, cb), + _ => {} + } + } - fn container_ui(&mut self, _: &mut egui::Ui, _: &dyn PlatformCallbacks) { - } + fn container_ui(&mut self, _: &mut egui::Ui, _: &dyn PlatformCallbacks) {} } impl MnemonicSetup { - /// Draw content for phrase enter step. - pub fn enter_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - self.ui(ui, cb); - ui.add_space(10.0); + /// Draw content for phrase enter step. + pub fn enter_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + self.ui(ui, cb); + ui.add_space(10.0); - // Show mode setup. - let mut mode = self.mnemonic.mode(); - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - let create_mode = PhraseMode::Generate; - let create_text = t!("create"); - View::radio_value(ui, &mut mode, create_mode, create_text); - }); - columns[1].vertical_centered(|ui| { - let import_mode = PhraseMode::Import; - let import_text = t!("wallets.recover"); - View::radio_value(ui, &mut mode, import_mode, import_text); - }); - }); - if mode != self.mnemonic.mode() { - self.mnemonic.set_mode(mode); - } + // Show mode setup. + let mut mode = self.mnemonic.mode(); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + let create_mode = PhraseMode::Generate; + let create_text = t!("create"); + View::radio_value(ui, &mut mode, create_mode, create_text); + }); + columns[1].vertical_centered(|ui| { + let import_mode = PhraseMode::Import; + let import_text = t!("wallets.recover"); + View::radio_value(ui, &mut mode, import_mode, import_text); + }); + }); + if mode != self.mnemonic.mode() { + self.mnemonic.set_mode(mode); + } - ui.add_space(10.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("wallets.words_count")) - .size(16.0) - .color(Colors::gray()) - ); - }); - ui.add_space(6.0); + ui.add_space(10.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("wallets.words_count")) + .size(16.0) + .color(Colors::gray()), + ); + }); + ui.add_space(6.0); - // Show mnemonic phrase size setup. - let mut size = self.mnemonic.size(); - ui.columns(5, |columns| { - for (index, word) in PhraseSize::VALUES.iter().enumerate() { - columns[index].vertical_centered(|ui| { - let text = word.value().to_string(); - View::radio_value(ui, &mut size, word.clone(), text); - }); - } - }); - if size != self.mnemonic.size() { - self.mnemonic.set_size(size); - } + // Show mnemonic phrase size setup. + let mut size = self.mnemonic.size(); + ui.columns(5, |columns| { + for (index, word) in PhraseSize::VALUES.iter().enumerate() { + columns[index].vertical_centered(|ui| { + let text = word.value().to_string(); + View::radio_value(ui, &mut size, word.clone(), text); + }); + } + }); + if size != self.mnemonic.size() { + self.mnemonic.set_size(size); + } - ui.add_space(12.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(12.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Show words setup. - self.word_list_ui(ui, self.mnemonic.mode() == PhraseMode::Import); - } - /// Draw content for phrase confirmation step. - pub fn confirm_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - self.ui(ui, cb); + // Show words setup. + self.word_list_ui(ui, self.mnemonic.mode() == PhraseMode::Import); + } + /// Draw content for phrase confirmation step. + pub fn confirm_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + self.ui(ui, cb); - ui.add_space(4.0); - ui.vertical_centered(|ui| { - let text = format!("{}:", t!("wallets.recovery_phrase")); - ui.label(RichText::new(text).size(16.0).color(Colors::gray())); - }); - ui.add_space(4.0); - self.word_list_ui(ui, true); - } + ui.add_space(4.0); + ui.vertical_centered(|ui| { + let text = format!("{}:", t!("wallets.recovery_phrase")); + ui.label(RichText::new(text).size(16.0).color(Colors::gray())); + }); + ui.add_space(4.0); + self.word_list_ui(ui, true); + } - /// Draw grid of words for mnemonic phrase. - fn word_list_ui(&mut self, ui: &mut egui::Ui, edit: bool) { - ui.add_space(6.0); - ui.scope(|ui| { - // Setup spacing between columns. - ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 6.0); + /// Draw grid of words for mnemonic phrase. + fn word_list_ui(&mut self, ui: &mut egui::Ui, edit: bool) { + ui.add_space(6.0); + ui.scope(|ui| { + // Setup spacing between columns. + ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 6.0); - // Select list of words based on current mode and edit flag. - let words = self.mnemonic.words(edit); + // Select list of words based on current mode and edit flag. + let words = self.mnemonic.words(edit); - let mut word_number = 0; - let cols = list_columns_count(ui); - let _ = words.chunks(cols).map(|chunk| { - let size = chunk.len(); - word_number += 1; - if size > 1 { - ui.columns(cols, |columns| { - columns[0].horizontal(|ui| { - let word = chunk.get(0).unwrap(); - self.word_item_ui(ui, word_number, word, edit); - }); - columns[1].horizontal(|ui| { - word_number += 1; - let word = chunk.get(1).unwrap(); - self.word_item_ui(ui, word_number, word, edit); - }); - if size > 2 { - columns[2].horizontal(|ui| { - word_number += 1; - let word = chunk.get(2).unwrap(); - self.word_item_ui(ui, word_number, word, edit); - }); - } - if size > 3 { - columns[3].horizontal(|ui| { - word_number += 1; - let word = chunk.get(3).unwrap(); - self.word_item_ui(ui, word_number, word, edit); - }); - } - }); - } else { - ui.columns(cols, |columns| { - columns[0].horizontal(|ui| { - let word = chunk.get(0).unwrap(); - self.word_item_ui(ui, word_number, word, edit); - }); - }); - } - }).collect::>(); - }); - ui.add_space(6.0); - } + let mut word_number = 0; + let cols = list_columns_count(ui); + let _ = words + .chunks(cols) + .map(|chunk| { + let size = chunk.len(); + word_number += 1; + if size > 1 { + ui.columns(cols, |columns| { + columns[0].horizontal(|ui| { + let word = chunk.get(0).unwrap(); + self.word_item_ui(ui, word_number, word, edit); + }); + columns[1].horizontal(|ui| { + word_number += 1; + let word = chunk.get(1).unwrap(); + self.word_item_ui(ui, word_number, word, edit); + }); + if size > 2 { + columns[2].horizontal(|ui| { + word_number += 1; + let word = chunk.get(2).unwrap(); + self.word_item_ui(ui, word_number, word, edit); + }); + } + if size > 3 { + columns[3].horizontal(|ui| { + word_number += 1; + let word = chunk.get(3).unwrap(); + self.word_item_ui(ui, word_number, word, edit); + }); + } + }); + } else { + ui.columns(cols, |columns| { + columns[0].horizontal(|ui| { + let word = chunk.get(0).unwrap(); + self.word_item_ui(ui, word_number, word, edit); + }); + }); + } + }) + .collect::>(); + }); + ui.add_space(6.0); + } - /// Draw word grid item. - fn word_item_ui(&mut self, - ui: &mut egui::Ui, - num: usize, - word: &PhraseWord, - edit: bool) { - let color = if !word.valid || (word.text.is_empty() && !self.mnemonic.valid()) { - Colors::red() - } else { - Colors::white_or_black(true) - }; - if edit { - ui.add_space(6.0); - View::button(ui, PENCIL.to_string(), Colors::white_or_black(false), || { - self.word_index_edit = num - 1; - self.word_edit = word.text.clone(); - self.valid_word_edit = word.valid; - // Show word edit modal. - Modal::new(WORD_INPUT_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.recovery_phrase")) - .show(); - }); - ui.label(RichText::new(format!("#{} {}", num, word.text)) - .size(17.0) - .color(color)); - } else { - ui.add_space(12.0); - let text = format!("#{} {}", num, word.text); - ui.label(RichText::new(text).size(17.0).color(color)); - } - } + /// Draw word grid item. + fn word_item_ui(&mut self, ui: &mut egui::Ui, num: usize, word: &PhraseWord, edit: bool) { + let color = if !word.valid || (word.text.is_empty() && !self.mnemonic.valid()) { + Colors::red() + } else { + Colors::white_or_black(true) + }; + if edit { + ui.add_space(6.0); + View::button( + ui, + PENCIL.to_string(), + Colors::white_or_black(false), + || { + self.word_index_edit = num - 1; + self.word_edit = word.text.clone(); + self.valid_word_edit = word.valid; + // Show word edit modal. + Modal::new(WORD_INPUT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.recovery_phrase")) + .show(); + }, + ); + ui.label( + RichText::new(format!("#{} {}", num, word.text)) + .size(17.0) + .color(color), + ); + } else { + ui.add_space(12.0); + let text = format!("#{} {}", num, word.text); + ui.label(RichText::new(text).size(17.0).color(color)); + } + } - /// Reset mnemonic phrase state to default values. - pub fn reset(&mut self) { - self.mnemonic = Mnemonic::default(); - } + /// Reset mnemonic phrase state to default values. + pub fn reset(&mut self) { + self.mnemonic = Mnemonic::default(); + } - /// Draw word input [`Modal`] content. - fn word_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut MnemonicSetup| { - // Insert word checking validity. - let word = &c.word_edit.trim().to_string(); - c.valid_word_edit = c.mnemonic.insert(c.word_index_edit, word); - if !c.valid_word_edit { - return; - } - // Close modal or go to next word to edit. - let next_word = c.mnemonic.get(c.word_index_edit + 1); - let close_modal = next_word.is_none() || - (!next_word.as_ref().unwrap().text.is_empty() && - next_word.unwrap().valid); - if close_modal { - Modal::close(); - } else { - c.word_index_edit += 1; - c.word_edit = String::from(""); - } - }; + /// Draw word input [`Modal`] content. + fn word_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut MnemonicSetup| { + // Insert word checking validity. + let word = &c.word_edit.trim().to_string(); + c.valid_word_edit = c.mnemonic.insert(c.word_index_edit, word); + if !c.valid_word_edit { + return; + } + // Close modal or go to next word to edit. + let next_word = c.mnemonic.get(c.word_index_edit + 1); + let close_modal = next_word.is_none() + || (!next_word.as_ref().unwrap().text.is_empty() && next_word.unwrap().valid); + if close_modal { + Modal::close(); + } else { + c.word_index_edit += 1; + c.word_edit = String::from(""); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("wallets.enter_word", "number" => self.word_index_edit + 1)) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("wallets.enter_word", "number" => self.word_index_edit + 1)) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw word value text edit. - let mut word_edit = TextEdit::new(Id::from(modal.id).with(self.word_index_edit)); - word_edit.ui(ui, &mut self.word_edit, cb); - if word_edit.enter_pressed { - on_save(self); - } + // Draw word value text edit. + let mut word_edit = TextEdit::new(Id::from(modal.id).with(self.word_index_edit)); + word_edit.ui(ui, &mut self.word_edit, cb); + if word_edit.enter_pressed { + on_save(self); + } - // Show error when specified word is not valid. - if !self.valid_word_edit { - ui.add_space(12.0); - ui.label(RichText::new(t!("wallets.not_valid_word")) - .size(17.0) - .color(Colors::red())); - } - ui.add_space(12.0); - }); + // Show error when specified word is not valid. + if !self.valid_word_edit { + ui.add_space(12.0); + ui.label( + RichText::new(t!("wallets.not_valid_word")) + .size(17.0) + .color(Colors::red()), + ); + } + ui.add_space(12.0); + }); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Show save button. - View::button(ui, t!("continue"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + // Show save button. + View::button(ui, t!("continue"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + } } /// Calculate word list columns count based on available ui width. fn list_columns_count(ui: &mut egui::Ui) -> usize { - let w = ui.available_width(); - let min_panel_w = Content::SIDE_PANEL_WIDTH - 12.0; - let double_min_panel_w = min_panel_w * 2.0; - if w >= min_panel_w * 1.5 && w < double_min_panel_w { - 3 - } else if w >= double_min_panel_w { - 4 - } else { - 2 - } -} \ No newline at end of file + let w = ui.available_width(); + let min_panel_w = Content::SIDE_PANEL_WIDTH - 12.0; + let double_min_panel_w = min_panel_w * 2.0; + if w >= min_panel_w * 1.5 && w < double_min_panel_w { + 3 + } else if w >= double_min_panel_w { + 4 + } else { + 2 + } +} diff --git a/src/gui/views/wallets/creation/mod.rs b/src/gui/views/wallets/creation/mod.rs index 4f021b63..532df557 100644 --- a/src/gui/views/wallets/creation/mod.rs +++ b/src/gui/views/wallets/creation/mod.rs @@ -18,4 +18,4 @@ pub use mnemonic::MnemonicSetup; mod content; pub use content::WalletCreationContent; -pub mod types; \ No newline at end of file +pub mod types; diff --git a/src/gui/views/wallets/creation/types.rs b/src/gui/views/wallets/creation/types.rs index 4a48e231..6098f131 100644 --- a/src/gui/views/wallets/creation/types.rs +++ b/src/gui/views/wallets/creation/types.rs @@ -15,21 +15,21 @@ /// Wallet creation step. #[derive(PartialEq, Clone)] pub enum Step { - /// Mnemonic phrase input. - EnterMnemonic, - /// Mnemonic phrase confirmation. - ConfirmMnemonic, - /// Wallet connection setup. - SetupConnection + /// Mnemonic phrase input. + EnterMnemonic, + /// Mnemonic phrase confirmation. + ConfirmMnemonic, + /// Wallet connection setup. + SetupConnection, } impl Step { - /// Short name representing creation step. - pub fn name(&self) -> String { - match *self { - Step::EnterMnemonic => "enter_phrase".to_owned(), - Step::ConfirmMnemonic => "confirm_phrase".to_owned(), - Step::SetupConnection => "setup_conn".to_owned(), - } - } -} \ No newline at end of file + /// Short name representing creation step. + pub fn name(&self) -> String { + match *self { + Step::EnterMnemonic => "enter_phrase".to_owned(), + Step::ConfirmMnemonic => "confirm_phrase".to_owned(), + Step::SetupConnection => "setup_conn".to_owned(), + } + } +} diff --git a/src/gui/views/wallets/mod.rs b/src/gui/views/wallets/mod.rs index 3dcbef4d..f88e145b 100644 --- a/src/gui/views/wallets/mod.rs +++ b/src/gui/views/wallets/mod.rs @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod modals; mod creation; +pub mod modals; mod content; pub use content::*; mod wallet; -use wallet::*; \ No newline at end of file +use wallet::*; diff --git a/src/gui/views/wallets/modals/add.rs b/src/gui/views/wallets/modals/add.rs index f0293036..230d06b4 100644 --- a/src/gui/views/wallets/modals/add.rs +++ b/src/gui/views/wallets/modals/add.rs @@ -21,88 +21,98 @@ use crate::gui::views::{Modal, TextEdit, View}; /// Initial wallet creation [`Modal`] content. pub struct AddWalletModal { - /// Wallet name. - pub name_edit: String, - /// Password to encrypt created wallet. - pub pass_edit: String, + /// Wallet name. + pub name_edit: String, + /// Password to encrypt created wallet. + pub pass_edit: String, } impl Default for AddWalletModal { - fn default() -> Self { - Self { - name_edit: t!("wallets.default_wallet").into(), - pass_edit: "".to_string(), - } - } + fn default() -> Self { + Self { + name_edit: t!("wallets.default_wallet").into(), + pass_edit: "".to_string(), + } + } } impl AddWalletModal { - /// Draw creating wallet name/password input [`Modal`] content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - cb: &dyn PlatformCallbacks, - mut on_input: impl FnMut(String, ZeroingString)) { - let mut on_next = |m: &mut AddWalletModal| { - let name = m.name_edit.clone(); - let pass = m.pass_edit.clone(); - if name.is_empty() || pass.is_empty() { - return; - } - Modal::close(); - on_input(name, ZeroingString::from(pass)); - }; - ui.vertical_centered(|ui| { - ui.add_space(6.0); - ui.label(RichText::new(t!("wallets.name")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + /// Draw creating wallet name/password input [`Modal`] content. + pub fn ui( + &mut self, + ui: &mut egui::Ui, + modal: &Modal, + cb: &dyn PlatformCallbacks, + mut on_input: impl FnMut(String, ZeroingString), + ) { + let mut on_next = |m: &mut AddWalletModal| { + let name = m.name_edit.clone(); + let pass = m.pass_edit.clone(); + if name.is_empty() || pass.is_empty() { + return; + } + Modal::close(); + on_input(name, ZeroingString::from(pass)); + }; + ui.vertical_centered(|ui| { + ui.add_space(6.0); + ui.label( + RichText::new(t!("wallets.name")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Show wallet name text edit. - let mut name_input = TextEdit::new(Id::from(modal.id).with("name")) - .focus(false); - name_input.ui(ui, &mut self.name_edit, cb); + // Show wallet name text edit. + let mut name_input = TextEdit::new(Id::from(modal.id).with("name")).focus(false); + name_input.ui(ui, &mut self.name_edit, cb); - ui.add_space(8.0); - ui.label(RichText::new(t!("wallets.pass")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(8.0); + ui.label( + RichText::new(t!("wallets.pass")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Show wallet password text edit. - let mut pass_input = TextEdit::new(Id::from(modal.id).with("pass")) - .password() - .focus(Modal::first_draw()); - if name_input.enter_pressed { - pass_input.focus_request(); - } - pass_input.ui(ui, &mut self.pass_edit, cb); - if pass_input.enter_pressed { - on_next(self); - } - ui.add_space(12.0); - }); + // Show wallet password text edit. + let mut pass_input = TextEdit::new(Id::from(modal.id).with("pass")) + .password() + .focus(Modal::first_draw()); + if name_input.enter_pressed { + pass_input.focus_request(); + } + pass_input.ui(ui, &mut self.pass_edit, cb); + if pass_input.enter_pressed { + on_next(self); + } + ui.add_space(12.0); + }); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("continue"), Colors::white_or_black(false), || { - on_next(self); - }); - }); - }); - ui.add_space(6.0); - }); - } -} \ No newline at end of file + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("continue"), Colors::white_or_black(false), || { + on_next(self); + }); + }); + }); + ui.add_space(6.0); + }); + } +} diff --git a/src/gui/views/wallets/modals/changelog.rs b/src/gui/views/wallets/modals/changelog.rs index 7d324433..991c3aaf 100644 --- a/src/gui/views/wallets/modals/changelog.rs +++ b/src/gui/views/wallets/modals/changelog.rs @@ -15,14 +15,14 @@ use egui::scroll_area::ScrollBarVisibility; use egui::{Id, OpenUrl, RichText, ScrollArea}; -use crate::gui::views::{Modal, View}; use crate::gui::Colors; use crate::gui::icons::{BRACKETS_CURLY, GITHUB_LOGO, TELEGRAM_LOGO}; +use crate::gui::views::{Modal, View}; /// Application release changelog content. pub struct ChangelogContent { - /// Changelog text. - changelog: String, + /// Changelog text. + changelog: String, } /// Endpoint for GitHub repository. @@ -33,105 +33,109 @@ const TELEGRAM_URL: &'static str = "https://t.me/grim_releases"; const GIT_URL: &'static str = "https://code.gri.mw/GUI/grim"; impl ChangelogContent { - /// Create new content instance. - pub fn new(changelog: String) -> Self { - Self { changelog } - } + /// Create new content instance. + pub fn new(changelog: String) -> Self { + Self { changelog } + } - /// Identifier for [`Modal`]. - pub const MODAL_ID: &'static str = "release_changelog_modal"; + /// Identifier for [`Modal`]. + pub const MODAL_ID: &'static str = "release_changelog_modal"; - /// Draw changelog [`Modal`] content. - pub fn ui(&mut self, ui: &mut egui::Ui) { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("changelog")).size(16.0).color(Colors::gray())); - }); - ui.add_space(6.0); + /// Draw changelog [`Modal`] content. + pub fn ui(&mut self, ui: &mut egui::Ui) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("changelog")) + .size(16.0) + .color(Colors::gray()), + ); + }); + ui.add_space(6.0); - // Show changelog text. - ui.vertical_centered(|ui| { - let scroll_id = Id::from("release_changelog"); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(3.0); - ScrollArea::vertical() - .id_salt(scroll_id) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .max_height(128.0) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(7.0); - let input_id = scroll_id.with("_input"); - egui::TextEdit::multiline(&mut self.changelog) - .id(input_id) - .font(egui::TextStyle::Small) - .desired_rows(5) - .interactive(false) - .desired_width(f32::INFINITY) - .show(ui); - ui.add_space(6.0); - }); - }); + // Show changelog text. + ui.vertical_centered(|ui| { + let scroll_id = Id::from("release_changelog"); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(3.0); + ScrollArea::vertical() + .id_salt(scroll_id) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(128.0) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + let input_id = scroll_id.with("_input"); + egui::TextEdit::multiline(&mut self.changelog) + .id(input_id) + .font(egui::TextStyle::Small) + .desired_rows(5) + .interactive(false) + .desired_width(f32::INFINITY) + .show(ui); + ui.add_space(6.0); + }); + }); - ui.add_space(2.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(3, |columns| { - columns[0].vertical_centered_justified(|ui| { - // Draw button to open GitHub link. - let mut github_clicked = false; - View::button(ui, GITHUB_LOGO, Colors::white_or_black(false), || { - github_clicked = true; - }); - if github_clicked { - ui.ctx().open_url(OpenUrl { - url: GITHUB_URL.into(), - new_tab: true, - }); - } - }); - columns[1].vertical_centered_justified(|ui| { - // Draw button to open Telegram link. - let mut tg_clicked = false; - View::button(ui, TELEGRAM_LOGO, Colors::white_or_black(false), || { - tg_clicked = true; - }); - if tg_clicked { - ui.ctx().open_url(OpenUrl { - url: TELEGRAM_URL.into(), - new_tab: true, - }); - } - }); - columns[2].vertical_centered_justified(|ui| { - // Draw button to open repository link. - let mut git_clicked = false; - View::button(ui, BRACKETS_CURLY, Colors::white_or_black(false), || { - git_clicked = true; - }); - if git_clicked { - ui.ctx().open_url(OpenUrl { - url: GIT_URL.into(), - new_tab: true, - }); - } - }); - }); + ui.columns(3, |columns| { + columns[0].vertical_centered_justified(|ui| { + // Draw button to open GitHub link. + let mut github_clicked = false; + View::button(ui, GITHUB_LOGO, Colors::white_or_black(false), || { + github_clicked = true; + }); + if github_clicked { + ui.ctx().open_url(OpenUrl { + url: GITHUB_URL.into(), + new_tab: true, + }); + } + }); + columns[1].vertical_centered_justified(|ui| { + // Draw button to open Telegram link. + let mut tg_clicked = false; + View::button(ui, TELEGRAM_LOGO, Colors::white_or_black(false), || { + tg_clicked = true; + }); + if tg_clicked { + ui.ctx().open_url(OpenUrl { + url: TELEGRAM_URL.into(), + new_tab: true, + }); + } + }); + columns[2].vertical_centered_justified(|ui| { + // Draw button to open repository link. + let mut git_clicked = false; + View::button(ui, BRACKETS_CURLY, Colors::white_or_black(false), || { + git_clicked = true; + }); + if git_clicked { + ui.ctx().open_url(OpenUrl { + url: GIT_URL.into(), + new_tab: true, + }); + } + }); + }); - ui.add_space(8.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); + ui.add_space(8.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); - // Show button to close modal. - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - Modal::close(); - }); - }); - ui.add_space(6.0); - } -} \ No newline at end of file + // Show button to close modal. + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + Modal::close(); + }); + }); + ui.add_space(6.0); + } +} diff --git a/src/gui/views/wallets/modals/list.rs b/src/gui/views/wallets/modals/list.rs index b12ce5c8..725bacb4 100644 --- a/src/gui/views/wallets/modals/list.rs +++ b/src/gui/views/wallets/modals/list.rs @@ -15,150 +15,162 @@ use egui::scroll_area::ScrollBarVisibility; use egui::{Align, Layout, RichText, ScrollArea, StrokeKind}; +use crate::gui::Colors; use crate::gui::icons::{CHECK, COMPUTER_TOWER, FOLDER_OPEN, GLOBE_SIMPLE, PLUGS_CONNECTED}; use crate::gui::views::wallets::wallet::types::wallet_status_text; use crate::gui::views::{Modal, View}; -use crate::gui::Colors; use crate::wallet::types::ConnectionMethod; use crate::wallet::{Wallet, WalletList}; /// Wallet list [`Modal`] content pub struct WalletListModal { - /// Selected wallet id. - selected_id: Option, + /// Selected wallet id. + selected_id: Option, - /// Optional data to pass after wallet selection. - data: Option, + /// Optional data to pass after wallet selection. + data: Option, - /// Flag to check if wallet can be opened from the list. - can_open: bool, + /// Flag to check if wallet can be opened from the list. + can_open: bool, } impl WalletListModal { - /// Create new content instance. - pub fn new(selected_id: Option, data: Option, can_open: bool) -> Self { - Self { selected_id, data, can_open } - } + /// Create new content instance. + pub fn new(selected_id: Option, data: Option, can_open: bool) -> Self { + Self { + selected_id, + data, + can_open, + } + } - /// Draw content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - wallets: &WalletList, - mut on_select: impl FnMut(Wallet, Option)) { - ui.add_space(4.0); - ScrollArea::vertical() - .max_height(373.0) - .id_salt("select_wallet_list_scroll") - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .auto_shrink([true; 2]) - .show(ui, |ui| { - ui.add_space(2.0); - ui.vertical_centered(|ui| { - let data = self.data.clone(); - for wallet in wallets.list() { - // Draw wallet list item. - self.wallet_item_ui(ui, wallet, || { - Modal::close(); - on_select(wallet.clone(), data.clone()); - }); - ui.add_space(5.0); - } - }); - }); + /// Draw content. + pub fn ui( + &mut self, + ui: &mut egui::Ui, + wallets: &WalletList, + mut on_select: impl FnMut(Wallet, Option), + ) { + ui.add_space(4.0); + ScrollArea::vertical() + .max_height(373.0) + .id_salt("select_wallet_list_scroll") + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .auto_shrink([true; 2]) + .show(ui, |ui| { + ui.add_space(2.0); + ui.vertical_centered(|ui| { + let data = self.data.clone(); + for wallet in wallets.list() { + // Draw wallet list item. + self.wallet_item_ui(ui, wallet, || { + Modal::close(); + on_select(wallet.clone(), data.clone()); + }); + ui.add_space(5.0); + } + }); + }); - ui.add_space(2.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Show button to close modal. - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - self.data = None; - Modal::close(); - }); - }); - ui.add_space(6.0); - } + // Show button to close modal. + ui.vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + self.data = None; + Modal::close(); + }, + ); + }); + ui.add_space(6.0); + } - /// Draw wallet list item with provided callback on select. - fn wallet_item_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - on_select: impl FnOnce()) { - let config = wallet.get_config(); - let id = config.id; + /// Draw wallet list item with provided callback on select. + fn wallet_item_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, on_select: impl FnOnce()) { + let config = wallet.get_config(); + let id = config.id; - // Draw round background. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(78.0); - let rounding = View::item_rounding(0, 1, false); - ui.painter().rect(rect, - rounding, - Colors::fill(), - View::hover_stroke(), - StrokeKind::Outside); + // Draw round background. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(78.0); + let rounding = View::item_rounding(0, 1, false); + ui.painter().rect( + rect, + rounding, + Colors::fill(), + View::hover_stroke(), + StrokeKind::Outside, + ); - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - if self.can_open { - // Show button to select or open closed wallet. - let icon = if wallet.is_open() { - CHECK - } else { - FOLDER_OPEN - }; - View::item_button(ui, View::item_rounding(0, 1, true), icon, None, || { - on_select(); - }); - } else { - // Draw button to select wallet. - if self.selected_id.unwrap_or(0) == id { - View::selected_item_check(ui); - } else { - View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || { - on_select(); - }); - } - } + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + if self.can_open { + // Show button to select or open closed wallet. + let icon = if wallet.is_open() { CHECK } else { FOLDER_OPEN }; + View::item_button(ui, View::item_rounding(0, 1, true), icon, None, || { + on_select(); + }); + } else { + // Draw button to select wallet. + if self.selected_id.unwrap_or(0) == id { + View::selected_item_check(ui); + } else { + View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || { + on_select(); + }); + } + } - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - ui.add_space(3.0); - // Show wallet name text. - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.add_space(1.0); - View::ellipsize_text(ui, config.name, 18.0, Colors::title(false)); - }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(3.0); + // Show wallet name text. + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.add_space(1.0); + View::ellipsize_text(ui, config.name, 18.0, Colors::title(false)); + }); - // Show wallet connection text. - let connection = wallet.get_current_connection(); - let conn_text = match connection { - ConnectionMethod::Integrated => { - format!("{} {}", COMPUTER_TOWER, t!("network.node")) - } - ConnectionMethod::External(_, url) => format!("{} {}", GLOBE_SIMPLE, url) - }; - ui.label(RichText::new(conn_text).size(15.0).color(Colors::text(false))); - ui.add_space(1.0); + // Show wallet connection text. + let connection = wallet.get_current_connection(); + let conn_text = match connection { + ConnectionMethod::Integrated => { + format!("{} {}", COMPUTER_TOWER, t!("network.node")) + } + ConnectionMethod::External(_, url) => format!("{} {}", GLOBE_SIMPLE, url), + }; + ui.label( + RichText::new(conn_text) + .size(15.0) + .color(Colors::text(false)), + ); + ui.add_space(1.0); - // Show wallet API text or open status. - if self.can_open { - ui.label(RichText::new(wallet_status_text(wallet)) - .size(15.0) - .color(Colors::gray())); - } else { - let address = if let Some(port) = config.api_port { - format!("127.0.0.1:{}", port) - } else { - "-".to_string() - }; - let api_text = format!("{} {}", PLUGS_CONNECTED, address); - ui.label(RichText::new(api_text).size(15.0).color(Colors::gray())); - } - ui.add_space(3.0); - }); - }); - }); - } -} \ No newline at end of file + // Show wallet API text or open status. + if self.can_open { + ui.label( + RichText::new(wallet_status_text(wallet)) + .size(15.0) + .color(Colors::gray()), + ); + } else { + let address = if let Some(port) = config.api_port { + format!("127.0.0.1:{}", port) + } else { + "-".to_string() + }; + let api_text = format!("{} {}", PLUGS_CONNECTED, address); + ui.label(RichText::new(api_text).size(15.0).color(Colors::gray())); + } + ui.add_space(3.0); + }); + }); + }); + } +} diff --git a/src/gui/views/wallets/modals/mod.rs b/src/gui/views/wallets/modals/mod.rs index 837fc4c9..68eb04fe 100644 --- a/src/gui/views/wallets/modals/mod.rs +++ b/src/gui/views/wallets/modals/mod.rs @@ -25,4 +25,4 @@ mod add; pub use add::*; mod changelog; -pub use changelog::*; \ No newline at end of file +pub use changelog::*; diff --git a/src/gui/views/wallets/modals/open.rs b/src/gui/views/wallets/modals/open.rs index 08b51dfa..af3b0463 100644 --- a/src/gui/views/wallets/modals/open.rs +++ b/src/gui/views/wallets/modals/open.rs @@ -21,88 +21,101 @@ use crate::gui::views::{Modal, TextEdit, View}; /// Wallet opening [`Modal`] content. pub struct OpenWalletModal { - /// Password to open wallet. - pass_edit: String, - /// Flag to check if wrong password was entered. - wrong_pass: bool, + /// Password to open wallet. + pass_edit: String, + /// Flag to check if wrong password was entered. + wrong_pass: bool, } impl OpenWalletModal { - /// Create new content instance. - pub fn new() -> Self { - Self { - pass_edit: "".to_string(), - wrong_pass: false, - } - } - /// Draw [`Modal`] content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - cb: &dyn PlatformCallbacks, - mut on_continue: impl FnMut(ZeroingString) -> bool) { - // Callback for button to continue. - let mut on_continue = |m: &mut OpenWalletModal| { - let pass = m.pass_edit.clone(); - if pass.is_empty() { - return; - } - m.wrong_pass = !on_continue(ZeroingString::from(pass)); - if !m.wrong_pass { - m.pass_edit = "".to_string(); - Modal::close(); - } - }; + /// Create new content instance. + pub fn new() -> Self { + Self { + pass_edit: "".to_string(), + wrong_pass: false, + } + } + /// Draw [`Modal`] content. + pub fn ui( + &mut self, + ui: &mut egui::Ui, + modal: &Modal, + cb: &dyn PlatformCallbacks, + mut on_continue: impl FnMut(ZeroingString) -> bool, + ) { + // Callback for button to continue. + let mut on_continue = |m: &mut OpenWalletModal| { + let pass = m.pass_edit.clone(); + if pass.is_empty() { + return; + } + m.wrong_pass = !on_continue(ZeroingString::from(pass)); + if !m.wrong_pass { + m.pass_edit = "".to_string(); + Modal::close(); + } + }; - ui.vertical_centered(|ui| { - ui.add_space(6.0); - ui.label(RichText::new(t!("wallets.pass")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.vertical_centered(|ui| { + ui.add_space(6.0); + ui.label( + RichText::new(t!("wallets.pass")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Show password input. - let mut pass_edit = TextEdit::new(Id::from(modal.id).with("pass_edit")).password(); - pass_edit.ui(ui, &mut self.pass_edit, cb); - if pass_edit.enter_pressed { - (on_continue)(self); - } + // Show password input. + let mut pass_edit = TextEdit::new(Id::from(modal.id).with("pass_edit")).password(); + pass_edit.ui(ui, &mut self.pass_edit, cb); + if pass_edit.enter_pressed { + (on_continue)(self); + } - // Show information when password is empty. - if self.pass_edit.is_empty() { - self.wrong_pass = false; - ui.add_space(10.0); - ui.label(RichText::new(t!("wallets.pass_empty")) - .size(17.0) - .color(Colors::inactive_text())); - } else if self.wrong_pass { - ui.add_space(10.0); - ui.label(RichText::new(t!("wallets.wrong_pass")) - .size(17.0) - .color(Colors::red())); - } - ui.add_space(12.0); - }); + // Show information when password is empty. + if self.pass_edit.is_empty() { + self.wrong_pass = false; + ui.add_space(10.0); + ui.label( + RichText::new(t!("wallets.pass_empty")) + .size(17.0) + .color(Colors::inactive_text()), + ); + } else if self.wrong_pass { + ui.add_space(10.0); + ui.label( + RichText::new(t!("wallets.wrong_pass")) + .size(17.0) + .color(Colors::red()), + ); + } + ui.add_space(12.0); + }); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("continue"), Colors::white_or_black(false), || { - (on_continue)(self); - }); - }); - }); - ui.add_space(6.0); - }); - } -} \ No newline at end of file + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("continue"), Colors::white_or_black(false), || { + (on_continue)(self); + }); + }); + }); + ui.add_space(6.0); + }); + } +} diff --git a/src/gui/views/wallets/modals/settings.rs b/src/gui/views/wallets/modals/settings.rs index 892f1146..88b72136 100644 --- a/src/gui/views/wallets/modals/settings.rs +++ b/src/gui/views/wallets/modals/settings.rs @@ -15,155 +15,178 @@ use egui::scroll_area::ScrollBarVisibility; use egui::{RichText, ScrollArea}; +use crate::gui::Colors; use crate::gui::icons::{PLUS_CIRCLE, TRASH}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::network::modals::ExternalConnectionModal; use crate::gui::views::network::ConnectionsContent; +use crate::gui::views::network::modals::ExternalConnectionModal; use crate::gui::views::types::ModalPosition; use crate::gui::views::wallets::WalletsContent; use crate::gui::views::{Modal, View}; -use crate::gui::Colors; use crate::wallet::types::ConnectionMethod; use crate::wallet::{ConnectionsConfig, ExternalConnection}; /// Wallet connection selection [`Modal`] content. pub struct WalletSettingsModal { - /// Current connection method. - pub conn: ConnectionMethod, + /// Current connection method. + pub conn: ConnectionMethod, - /// External connection creation content. - new_ext_conn_content: Option + /// External connection creation content. + new_ext_conn_content: Option, } impl WalletSettingsModal { - /// Create from provided wallet connection. - pub fn new(conn: ConnectionMethod) -> Self { - Self { - conn, - new_ext_conn_content: None, - } - } + /// Create from provided wallet connection. + pub fn new(conn: ConnectionMethod) -> Self { + Self { + conn, + new_ext_conn_content: None, + } + } - /// Draw [`Modal`] content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - cb: &dyn PlatformCallbacks, - on_select: impl Fn(ConnectionMethod)) { - // Draw external connection creation content. - if let Some(ext_content) = self.new_ext_conn_content.as_mut() { - ext_content.ui(ui, cb, modal, |conn| { - on_select(ConnectionMethod::External(conn.id, conn.url)); - }); - return; - } + /// Draw [`Modal`] content. + pub fn ui( + &mut self, + ui: &mut egui::Ui, + modal: &Modal, + cb: &dyn PlatformCallbacks, + on_select: impl Fn(ConnectionMethod), + ) { + // Draw external connection creation content. + if let Some(ext_content) = self.new_ext_conn_content.as_mut() { + ext_content.ui(ui, cb, modal, |conn| { + on_select(ConnectionMethod::External(conn.id, conn.url)); + }); + return; + } - // Check connections state on first draw. - if Modal::first_draw() { - ExternalConnection::check(None, ui.ctx()); - } + // Check connections state on first draw. + if Modal::first_draw() { + ExternalConnection::check(None, ui.ctx()); + } - ui.add_space(4.0); + ui.add_space(4.0); - let ext_conn_list = ConnectionsConfig::ext_conn_list(); - ScrollArea::vertical() - .max_height(if ext_conn_list.len() < 4 { - 330.0 - } else { - 350.0 - }) - .id_salt("connections_scroll") - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .auto_shrink([true; 2]) - .show(ui, |ui| { - ui.add_space(2.0); + let ext_conn_list = ConnectionsConfig::ext_conn_list(); + ScrollArea::vertical() + .max_height(if ext_conn_list.len() < 4 { + 330.0 + } else { + 350.0 + }) + .id_salt("connections_scroll") + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .auto_shrink([true; 2]) + .show(ui, |ui| { + ui.add_space(2.0); - // Show integrated node selection. - let cur_integrated = self.conn == ConnectionMethod::Integrated; - let bg = if cur_integrated { - Colors::fill() - } else { - Colors::fill_lite() - }; - ConnectionsContent::integrated_node_item_ui(ui, bg, (!cur_integrated, || { - on_select(ConnectionMethod::Integrated); - Modal::close(); - }), |ui| { - if cur_integrated { - View::selected_item_check(ui); - } - cur_integrated - }); + // Show integrated node selection. + let cur_integrated = self.conn == ConnectionMethod::Integrated; + let bg = if cur_integrated { + Colors::fill() + } else { + Colors::fill_lite() + }; + ConnectionsContent::integrated_node_item_ui( + ui, + bg, + (!cur_integrated, || { + on_select(ConnectionMethod::Integrated); + Modal::close(); + }), + |ui| { + if cur_integrated { + View::selected_item_check(ui); + } + cur_integrated + }, + ); - ui.add_space(8.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("wallets.ext_conn")) - .size(16.0) - .color(Colors::gray())); - ui.add_space(6.0); - // Show button to add new external node connection. - let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node")); - View::button(ui, add_node_text, Colors::white_or_black(false), || { - self.new_ext_conn_content = Some(ExternalConnectionModal::new(None)); - }); - }); - ui.add_space(4.0); + ui.add_space(8.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("wallets.ext_conn")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); + // Show button to add new external node connection. + let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node")); + View::button(ui, add_node_text, Colors::white_or_black(false), || { + self.new_ext_conn_content = Some(ExternalConnectionModal::new(None)); + }); + }); + ui.add_space(4.0); - if !ext_conn_list.is_empty() { - ui.add_space(8.0); - } - for (i, c) in ext_conn_list.iter().enumerate() { - ui.horizontal_wrapped(|ui| { - let len = ext_conn_list.len(); - let is_current = match self.conn { - ConnectionMethod::External(id, _) => id == c.id, - _ => false - }; - let bg = if is_current { - Colors::fill() - } else { - Colors::fill_lite() - }; - ConnectionsContent::ext_conn_item_ui(ui, bg, c, i, len, (!is_current, || { - on_select( - ConnectionMethod::External(c.id, c.url.clone()) - ); - Modal::close(); - }), |ui| { - if is_current { - View::selected_item_check(ui); - } - }); - }); - } - ui.add_space(4.0); - }); + if !ext_conn_list.is_empty() { + ui.add_space(8.0); + } + for (i, c) in ext_conn_list.iter().enumerate() { + ui.horizontal_wrapped(|ui| { + let len = ext_conn_list.len(); + let is_current = match self.conn { + ConnectionMethod::External(id, _) => id == c.id, + _ => false, + }; + let bg = if is_current { + Colors::fill() + } else { + Colors::fill_lite() + }; + ConnectionsContent::ext_conn_item_ui( + ui, + bg, + c, + i, + len, + (!is_current, || { + on_select(ConnectionMethod::External(c.id, c.url.clone())); + Modal::close(); + }), + |ui| { + if is_current { + View::selected_item_check(ui); + } + }, + ); + }); + } + ui.add_space(4.0); + }); - ui.add_space(2.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); - ui.vertical_centered(|ui| { - // Draw button to delete the wallet. - View::colored_text_button(ui, - format!("{} {}", TRASH, t!("wallets.delete")), - Colors::red(), - Colors::white_or_black(false), || { - Modal::new(WalletsContent::DELETE_CONFIRMATION_MODAL) - .position(ModalPosition::Center) - .title(t!("confirmation")) - .show(); - }); - }); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + // Draw button to delete the wallet. + View::colored_text_button( + ui, + format!("{} {}", TRASH, t!("wallets.delete")), + Colors::red(), + Colors::white_or_black(false), + || { + Modal::new(WalletsContent::DELETE_CONFIRMATION_MODAL) + .position(ModalPosition::Center) + .title(t!("confirmation")) + .show(); + }, + ); + }); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Show button to close modal. - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - Modal::close(); - }); - }); - ui.add_space(6.0); - } -} \ No newline at end of file + // Show button to close modal. + ui.vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + Modal::close(); + }, + ); + }); + ui.add_space(6.0); + } +} diff --git a/src/gui/views/wallets/wallet/account/content.rs b/src/gui/views/wallets/wallet/account/content.rs index 21ba43f3..a8d58328 100644 --- a/src/gui/views/wallets/wallet/account/content.rs +++ b/src/gui/views/wallets/wallet/account/content.rs @@ -12,34 +12,34 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{Align, Layout, RichText, ScrollArea, StrokeKind}; use egui::scroll_area::ScrollBarVisibility; +use egui::{Align, Layout, RichText, ScrollArea, StrokeKind}; use grin_core::core::amount_to_hr_string; -use crate::gui::icons::{CHECK, FOLDER_USER, PACKAGE, PATH, SCAN, SPINNER, USERS_THREE, USER_PLUS}; +use crate::gui::Colors; +use crate::gui::icons::{CHECK, FOLDER_USER, PACKAGE, PATH, SCAN, SPINNER, USER_PLUS, USERS_THREE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::{ModalPosition, QrScanResult}; use crate::gui::views::wallets::wallet::account::create::CreateAccountContent; -use crate::gui::views::wallets::wallet::types::{WalletContentContainer, GRIN}; -use crate::gui::views::{CameraContent, CameraScanContent, Content, Modal, View}; -use crate::gui::Colors; use crate::gui::views::wallets::wallet::request::SendRequestContent; -use crate::wallet::{Wallet, WalletConfig}; +use crate::gui::views::wallets::wallet::types::{GRIN, WalletContentContainer}; +use crate::gui::views::{CameraContent, CameraScanContent, Content, Modal, View}; use crate::wallet::types::{WalletAccount, WalletTask}; +use crate::wallet::{Wallet, WalletConfig}; /// Wallet account panel content. pub struct WalletAccountContent { - /// Flag to show account list content. - pub show_list: bool, - /// Account creation [`Modal`] content. - create_account_content: CreateAccountContent, + /// Flag to show account list content. + pub show_list: bool, + /// Account creation [`Modal`] content. + create_account_content: CreateAccountContent, - /// QR code scan content. - qr_scan_content: Option, - /// QR code scan result - qr_scan_result: Option, - /// Send request creation [`Modal`] content. - send_content: Option, + /// QR code scan content. + qr_scan_content: Option, + /// QR code scan result + qr_scan_result: Option, + /// Send request creation [`Modal`] content. + send_content: Option, } /// Account creation [`Modal`] identifier. @@ -48,366 +48,387 @@ const CREATE_MODAL_ID: &'static str = "create_account_modal"; const SEND_MODAL_ID: &'static str = "account_send_request_modal"; impl WalletContentContainer for WalletAccountContent { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - CREATE_MODAL_ID, - SEND_MODAL_ID - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![CREATE_MODAL_ID, SEND_MODAL_ID] + } - fn modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - match modal.id { - CREATE_MODAL_ID => self.create_account_content.ui(ui, wallet, modal, cb), - SEND_MODAL_ID => { - if let Some(c) = self.send_content.as_mut() { - c.ui(ui, wallet, modal, cb); - } - } - _ => {} - } - } + fn modal_ui( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks, + ) { + match modal.id { + CREATE_MODAL_ID => self.create_account_content.ui(ui, wallet, modal, cb), + SEND_MODAL_ID => { + if let Some(c) = self.send_content.as_mut() { + c.ui(ui, wallet, modal, cb); + } + } + _ => {} + } + } - fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - if self.qr_scan_showing() { - self.qr_scan_ui(ui, wallet, cb); - } else { - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - if self.show_list { - self.list_ui(ui, wallet); - } else { - // Show account content. - self.account_ui(ui, wallet, cb); - } - }); - } - } + fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { + if self.qr_scan_showing() { + self.qr_scan_ui(ui, wallet, cb); + } else { + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + if self.show_list { + self.list_ui(ui, wallet); + } else { + // Show account content. + self.account_ui(ui, wallet, cb); + } + }); + } + } } impl Default for WalletAccountContent { - fn default() -> Self { - Self { - show_list: false, - create_account_content: CreateAccountContent::default(), - qr_scan_content: None, - qr_scan_result: None, - send_content: None, - } - } + fn default() -> Self { + Self { + show_list: false, + create_account_content: CreateAccountContent::default(), + qr_scan_content: None, + qr_scan_result: None, + send_content: None, + } + } } const ACCOUNT_ITEM_HEIGHT: f32 = 75.0; impl WalletAccountContent { - /// Check if QR code scanner was opened. - pub fn qr_scan_showing(&self) -> bool { - self.qr_scan_content.is_some() || self.qr_scan_result.is_some() - } + /// Check if QR code scanner was opened. + pub fn qr_scan_showing(&self) -> bool { + self.qr_scan_content.is_some() || self.qr_scan_result.is_some() + } - /// Close QR code scanner. - pub fn close_qr_scan(&mut self, cb: &dyn PlatformCallbacks) { - if !self.qr_scan_showing() { - return; - } - cb.stop_camera(); - self.qr_scan_content = None; - self.qr_scan_result = None; - } + /// Close QR code scanner. + pub fn close_qr_scan(&mut self, cb: &dyn PlatformCallbacks) { + if !self.qr_scan_showing() { + return; + } + cb.stop_camera(); + self.qr_scan_content = None; + self.qr_scan_result = None; + } - /// Check if it's possible to go back at navigation stack. - pub fn can_back(&self) -> bool { - self.qr_scan_showing() || self.show_list - } + /// Check if it's possible to go back at navigation stack. + pub fn can_back(&self) -> bool { + self.qr_scan_showing() || self.show_list + } - /// Navigate back on navigation stack. - pub fn back(&mut self, cb: &dyn PlatformCallbacks) { - if self.qr_scan_showing() { - self.close_qr_scan(cb); - } else if self.show_list { - self.show_list = false; - } - } + /// Navigate back on navigation stack. + pub fn back(&mut self, cb: &dyn PlatformCallbacks) { + if self.qr_scan_showing() { + self.close_qr_scan(cb); + } else if self.show_list { + self.show_list = false; + } + } - /// Draw wallet account content. - fn account_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - cb: &dyn PlatformCallbacks) { - // Check wallet data. - if wallet.get_data().is_none() { - return; - } + /// Draw wallet account content. + fn account_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { + // Check wallet data. + if wallet.get_data().is_none() { + return; + } - let data = wallet.get_data().unwrap(); + let data = wallet.get_data().unwrap(); - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(75.0); + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(75.0); - // Draw round background. - let rounding = View::item_rounding(0, 2, false); - ui.painter().rect(rect, - rounding, - Colors::fill(), - View::item_stroke(), - StrokeKind::Outside); + // Draw round background. + let rounding = View::item_rounding(0, 2, false); + ui.painter().rect( + rect, + rounding, + Colors::fill(), + View::item_stroke(), + StrokeKind::Outside, + ); - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - // Draw button to show QR code scanner. - View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || { - self.qr_scan_content = Some(CameraContent::default()); - cb.start_camera(); - }); + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + // Draw button to show QR code scanner. + View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || { + self.qr_scan_content = Some(CameraContent::default()); + cb.start_camera(); + }); - // Draw button to show list of accounts. - let accounts = wallet.accounts(); - let accounts_icon = if accounts.len() > 1 { - USERS_THREE - } else { - USER_PLUS - }; - let rounding = View::item_rounding(1, 3, true); - View::item_button(ui, rounding, accounts_icon, None, || { - if accounts.len() == 1 { - self.create_account_content = CreateAccountContent::default(); - Modal::new(CREATE_MODAL_ID) - .position(ModalPosition::CenterTop) - .title(t!("wallets.accounts")) - .show(); - } else { - self.show_list = true; - } - }); + // Draw button to show list of accounts. + let accounts = wallet.accounts(); + let accounts_icon = if accounts.len() > 1 { + USERS_THREE + } else { + USER_PLUS + }; + let rounding = View::item_rounding(1, 3, true); + View::item_button(ui, rounding, accounts_icon, None, || { + if accounts.len() == 1 { + self.create_account_content = CreateAccountContent::default(); + Modal::new(CREATE_MODAL_ID) + .position(ModalPosition::CenterTop) + .title(t!("wallets.accounts")) + .show(); + } else { + self.show_list = true; + } + }); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(8.0); - ui.vertical(|ui| { - ui.add_space(3.0); - // Show spendable amount. - let amount = amount_to_hr_string(data.info.amount_currently_spendable, true); - let amount_text = format!("{} {}", amount, GRIN); - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.add_space(1.0); - ui.label(RichText::new(amount_text) - .size(18.0) - .color(Colors::white_or_black(true))); - }); - ui.add_space(-2.0); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(8.0); + ui.vertical(|ui| { + ui.add_space(3.0); + // Show spendable amount. + let amount = amount_to_hr_string(data.info.amount_currently_spendable, true); + let amount_text = format!("{} {}", amount, GRIN); + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.add_space(1.0); + ui.label( + RichText::new(amount_text) + .size(18.0) + .color(Colors::white_or_black(true)), + ); + }); + ui.add_space(-2.0); - // Show account label. - let account = wallet.get_config().account; - let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string(); - let acc_label = if account == default_acc_label { - t!("wallets.default_account").into() - } else { - account.to_owned() - }; - let acc_text = format!("{} {}", FOLDER_USER, acc_label); - View::ellipsize_text(ui, acc_text, 15.0, Colors::text(false)); + // Show account label. + let account = wallet.get_config().account; + let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string(); + let acc_label = if account == default_acc_label { + t!("wallets.default_account").into() + } else { + account.to_owned() + }; + let acc_text = format!("{} {}", FOLDER_USER, acc_label); + View::ellipsize_text(ui, acc_text, 15.0, Colors::text(false)); - // Show confirmed height or sync progress. - let status_text = if wallet.message_opening() { - format!("{} {}", SPINNER, t!("wallets.loading")) - } else if !wallet.syncing() { - format!("{} {}", PACKAGE, data.info.last_confirmed_height) - } else { - let info_progress = wallet.info_sync_progress(); - if info_progress == 100 || info_progress == 0 { - format!("{} {}", SPINNER, t!("wallets.wallet_loading")) - } else { - if wallet.is_repairing() { - let rep_progress = wallet.repairing_progress(); - if rep_progress == 0 { - format!("{} {}", SPINNER, t!("wallets.wallet_checking")) - } else { - format!("{} {}: {}%", - SPINNER, - t!("wallets.wallet_checking"), - rep_progress) - } - } else { - format!("{} {}: {}%", - SPINNER, - t!("wallets.wallet_loading"), - info_progress) - } - } - }; - let animate = wallet.syncing() || wallet.message_opening(); - View::animate_text(ui, status_text, 15.0, Colors::gray(), animate); - }) - }); - }); - } + // Show confirmed height or sync progress. + let status_text = if wallet.message_opening() { + format!("{} {}", SPINNER, t!("wallets.loading")) + } else if !wallet.syncing() { + format!("{} {}", PACKAGE, data.info.last_confirmed_height) + } else { + let info_progress = wallet.info_sync_progress(); + if info_progress == 100 || info_progress == 0 { + format!("{} {}", SPINNER, t!("wallets.wallet_loading")) + } else { + if wallet.is_repairing() { + let rep_progress = wallet.repairing_progress(); + if rep_progress == 0 { + format!("{} {}", SPINNER, t!("wallets.wallet_checking")) + } else { + format!( + "{} {}: {}%", + SPINNER, + t!("wallets.wallet_checking"), + rep_progress + ) + } + } else { + format!( + "{} {}: {}%", + SPINNER, + t!("wallets.wallet_loading"), + info_progress + ) + } + } + }; + let animate = wallet.syncing() || wallet.message_opening(); + View::animate_text(ui, status_text, 15.0, Colors::gray(), animate); + }) + }); + }); + } - /// Draw account list content. - fn list_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { - let accounts = wallet.accounts(); - let size = accounts.len(); - ScrollArea::vertical() - .id_salt("account_list_scroll") - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .max_height(411.0) - .auto_shrink([true; 2]) - .show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| { - for index in row_range { - let acc = accounts.get(index).unwrap().clone(); - let current = wallet.get_config().account == acc.label; - account_item_ui(ui, &acc, current, index, size, || { - let _ = wallet.set_active_account(&acc.label); - self.show_list = false; - }); - if index == size - 1 { - ui.add_space(4.0); - } - } - }); + /// Draw account list content. + fn list_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { + let accounts = wallet.accounts(); + let size = accounts.len(); + ScrollArea::vertical() + .id_salt("account_list_scroll") + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(411.0) + .auto_shrink([true; 2]) + .show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| { + for index in row_range { + let acc = accounts.get(index).unwrap().clone(); + let current = wallet.get_config().account == acc.label; + account_item_ui(ui, &acc, current, index, size, || { + let _ = wallet.set_active_account(&acc.label); + self.show_list = false; + }); + if index == size - 1 { + ui.add_space(4.0); + } + } + }); - ui.add_space(2.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - // Show modal buttons. - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - self.show_list = false; - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.add"), Colors::white_or_black(false), || { - self.show_list = false; - self.create_account_content = CreateAccountContent::default(); - Modal::new(CREATE_MODAL_ID) - .position(ModalPosition::CenterTop) - .title(t!("wallets.accounts")) - .show(); - }); - }); - }); - ui.add_space(6.0); - } + // Show modal buttons. + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + self.show_list = false; + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.add"), Colors::white_or_black(false), || { + self.show_list = false; + self.create_account_content = CreateAccountContent::default(); + Modal::new(CREATE_MODAL_ID) + .position(ModalPosition::CenterTop) + .title(t!("wallets.accounts")) + .show(); + }); + }); + }); + ui.add_space(6.0); + } - /// Draw QR code scanner content. - fn qr_scan_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH, |ui| { - if self.qr_scan_content.is_some() { - if let Some(result) = self.qr_scan_content.as_ref().unwrap().qr_scan_result() { - cb.stop_camera(); - self.qr_scan_content = None; - match result { - QrScanResult::Address(a) => { - if let Some(data) = wallet.get_data() { - if data.info.amount_currently_spendable > 0 { - let address = Some(a.to_string()); - self.send_content = Some(SendRequestContent::new(address)); - Modal::new(SEND_MODAL_ID) - .position(ModalPosition::CenterTop) - .title(t!("wallets.send")) - .show(); - } - } - } - QrScanResult::Slatepack(m) => { - wallet.task(WalletTask::OpenMessage(m)); - } - _ => { - self.qr_scan_result = Some(result); - } - } - } else { - // Draw QR code scan content. - self.qr_scan_content.as_mut().unwrap().ui(ui, cb); - ui.add_space(6.0); - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.close_qr_scan(cb); - }); - }); - } - } else if let Some(res) = &self.qr_scan_result.clone() { - CameraScanContent::result_ui(ui, res, cb, || { - self.qr_scan_result = None; - }, || { - self.qr_scan_content = Some(CameraContent::default()); - cb.start_camera(); - }); - } - ui.add_space(6.0); - }); - } + /// Draw QR code scanner content. + fn qr_scan_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH, |ui| { + if self.qr_scan_content.is_some() { + if let Some(result) = self.qr_scan_content.as_ref().unwrap().qr_scan_result() { + cb.stop_camera(); + self.qr_scan_content = None; + match result { + QrScanResult::Address(a) => { + if let Some(data) = wallet.get_data() { + if data.info.amount_currently_spendable > 0 { + let address = Some(a.to_string()); + self.send_content = Some(SendRequestContent::new(address)); + Modal::new(SEND_MODAL_ID) + .position(ModalPosition::CenterTop) + .title(t!("wallets.send")) + .show(); + } + } + } + QrScanResult::Slatepack(m) => { + wallet.task(WalletTask::OpenMessage(m)); + } + _ => { + self.qr_scan_result = Some(result); + } + } + } else { + // Draw QR code scan content. + self.qr_scan_content.as_mut().unwrap().ui(ui, cb); + ui.add_space(6.0); + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + self.close_qr_scan(cb); + }); + }); + } + } else if let Some(res) = &self.qr_scan_result.clone() { + CameraScanContent::result_ui( + ui, + res, + cb, + || { + self.qr_scan_result = None; + }, + || { + self.qr_scan_content = Some(CameraContent::default()); + cb.start_camera(); + }, + ); + } + ui.add_space(6.0); + }); + } } /// Draw account item. -fn account_item_ui(ui: &mut egui::Ui, - acc: &WalletAccount, - current: bool, - index: usize, - size: usize, - mut on_select: impl FnMut()) { - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(ACCOUNT_ITEM_HEIGHT); +fn account_item_ui( + ui: &mut egui::Ui, + acc: &WalletAccount, + current: bool, + index: usize, + size: usize, + mut on_select: impl FnMut(), +) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(ACCOUNT_ITEM_HEIGHT); - // Draw round background. - let bg_rect = rect.clone(); - let item_rounding = View::item_rounding(index, size, false); - ui.painter().rect(bg_rect, - item_rounding, - Colors::fill(), - View::item_stroke(), - StrokeKind::Outside); + // Draw round background. + let bg_rect = rect.clone(); + let item_rounding = View::item_rounding(index, size, false); + ui.painter().rect( + bg_rect, + item_rounding, + Colors::fill(), + View::item_stroke(), + StrokeKind::Outside, + ); - ui.vertical(|ui| { - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - // Draw button to select account. - if current { - View::selected_item_check(ui); - } else { - let button_rounding = View::item_rounding(index, size, true); - View::item_button(ui, button_rounding, CHECK, None, || { - on_select(); - }); - } + ui.vertical(|ui| { + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + // Draw button to select account. + if current { + View::selected_item_check(ui); + } else { + let button_rounding = View::item_rounding(index, size, true); + View::item_button(ui, button_rounding, CHECK, None, || { + on_select(); + }); + } - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(8.0); - ui.vertical(|ui| { - ui.add_space(3.0); - // Show spendable amount. - let amount = amount_to_hr_string(acc.spendable_amount, true); - let amount_text = format!("{} {}", amount, GRIN); - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.add_space(1.0); - ui.label(RichText::new(amount_text) - .size(18.0) - .color(Colors::white_or_black(true))); - }); - ui.add_space(-2.0); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(8.0); + ui.vertical(|ui| { + ui.add_space(3.0); + // Show spendable amount. + let amount = amount_to_hr_string(acc.spendable_amount, true); + let amount_text = format!("{} {}", amount, GRIN); + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.add_space(1.0); + ui.label( + RichText::new(amount_text) + .size(18.0) + .color(Colors::white_or_black(true)), + ); + }); + ui.add_space(-2.0); - // Show account name. - let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string(); - let acc_label = if acc.label == default_acc_label { - t!("wallets.default_account").into() - } else { - acc.label.to_owned() - }; - let acc_name = format!("{} {}", FOLDER_USER, acc_label); - View::ellipsize_text(ui, acc_name, 15.0, Colors::text(false)); + // Show account name. + let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string(); + let acc_label = if acc.label == default_acc_label { + t!("wallets.default_account").into() + } else { + acc.label.to_owned() + }; + let acc_name = format!("{} {}", FOLDER_USER, acc_label); + View::ellipsize_text(ui, acc_name, 15.0, Colors::text(false)); - // Show account BIP32 derivation path. - let acc_path = format!("{} {}", PATH, acc.path); - ui.label(RichText::new(acc_path).size(15.0).color(Colors::gray())); - ui.add_space(3.0); - }); - }); - }); - }); -} \ No newline at end of file + // Show account BIP32 derivation path. + let acc_path = format!("{} {}", PATH, acc.path); + ui.label(RichText::new(acc_path).size(15.0).color(Colors::gray())); + ui.add_space(3.0); + }); + }); + }); + }); +} diff --git a/src/gui/views/wallets/wallet/account/create.rs b/src/gui/views/wallets/wallet/account/create.rs index 5b2ec836..b209e1cf 100644 --- a/src/gui/views/wallets/wallet/account/create.rs +++ b/src/gui/views/wallets/wallet/account/create.rs @@ -14,90 +14,97 @@ use egui::{Id, RichText}; +use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, TextEdit, View}; -use crate::gui::Colors; use crate::wallet::Wallet; /// Account creation [`Modal`] content. pub struct CreateAccountContent { - /// Account label value. - account_label_edit: String, - /// Flag to check if error occurred during account creation. - account_creation_error: bool, + /// Account label value. + account_label_edit: String, + /// Flag to check if error occurred during account creation. + account_creation_error: bool, } impl Default for CreateAccountContent { - fn default() -> Self { - Self { - account_label_edit: "".to_string(), - account_creation_error: false, - } - } + fn default() -> Self { + Self { + account_label_edit: "".to_string(), + account_creation_error: false, + } + } } impl CreateAccountContent { - /// Draw account creation [`Modal`] content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - let on_create = |m: &mut CreateAccountContent| { - if m.account_label_edit.is_empty() { - return; - } - let label = &m.account_label_edit; - match wallet.create_account(label) { - Ok(_) => { - let _ = wallet.set_active_account(label); - Modal::close(); - }, - Err(_) => m.account_creation_error = true - }; - }; + /// Draw account creation [`Modal`] content. + pub fn ui( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks, + ) { + let on_create = |m: &mut CreateAccountContent| { + if m.account_label_edit.is_empty() { + return; + } + let label = &m.account_label_edit; + match wallet.create_account(label) { + Ok(_) => { + let _ = wallet.set_active_account(label); + Modal::close(); + } + Err(_) => m.account_creation_error = true, + }; + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("wallets.new_account_desc")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("wallets.new_account_desc")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw account name edit. - let mut name_edit = TextEdit::new(Id::from(modal.id).with(wallet.get_config().id)); - name_edit.ui(ui, &mut self.account_label_edit, cb); - if name_edit.enter_pressed { - on_create(self); - } + // Draw account name edit. + let mut name_edit = TextEdit::new(Id::from(modal.id).with(wallet.get_config().id)); + name_edit.ui(ui, &mut self.account_label_edit, cb); + if name_edit.enter_pressed { + on_create(self); + } - // Show error occurred during account creation. - if self.account_creation_error { - ui.add_space(12.0); - ui.label(RichText::new(t!("error")) - .size(17.0) - .color(Colors::red())); - } - ui.add_space(12.0); - }); + // Show error occurred during account creation. + if self.account_creation_error { + ui.add_space(12.0); + ui.label(RichText::new(t!("error")).size(17.0).color(Colors::red())); + } + ui.add_space(12.0); + }); - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - // Show modal buttons. - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("create"), Colors::white_or_black(false), || { - on_create(self); - }); - }); - }); - ui.add_space(6.0); - } -} \ No newline at end of file + // Show modal buttons. + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("create"), Colors::white_or_black(false), || { + on_create(self); + }); + }); + }); + ui.add_space(6.0); + } +} diff --git a/src/gui/views/wallets/wallet/account/mod.rs b/src/gui/views/wallets/wallet/account/mod.rs index 8e64e092..739622cd 100644 --- a/src/gui/views/wallets/wallet/account/mod.rs +++ b/src/gui/views/wallets/wallet/account/mod.rs @@ -15,4 +15,4 @@ mod content; pub use content::*; -mod create; \ No newline at end of file +mod create; diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 3a85af4e..92bb1a81 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -16,42 +16,44 @@ use egui::scroll_area::ScrollBarVisibility; use egui::{Id, Margin, RichText, ScrollArea}; use grin_chain::SyncStatus; -use crate::gui::icons::{ARROWS_CLOCKWISE, FILE_ARROW_DOWN, FILE_ARROW_UP, FILE_TEXT, GEAR_FINE, POWER, STACK}; +use crate::AppConfig; +use crate::gui::Colors; +use crate::gui::icons::{ + ARROWS_CLOCKWISE, FILE_ARROW_DOWN, FILE_ARROW_UP, FILE_TEXT, GEAR_FINE, POWER, STACK, +}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::{LinePosition, ModalPosition}; use crate::gui::views::wallets::wallet::account::WalletAccountContent; use crate::gui::views::wallets::wallet::message::MessageInputContent; +use crate::gui::views::wallets::wallet::proof::PaymentProofContent; use crate::gui::views::wallets::wallet::request::{InvoiceRequestContent, SendRequestContent}; use crate::gui::views::wallets::wallet::transport::WalletTransportContent; use crate::gui::views::wallets::wallet::types::WalletContentContainer; use crate::gui::views::wallets::wallet::{WalletSettingsContent, WalletTransactionsContent}; use crate::gui::views::{Content, Modal, View}; -use crate::gui::Colors; use crate::node::Node; use crate::wallet::types::{ConnectionMethod, WalletTask}; use crate::wallet::{ExternalConnection, Wallet}; -use crate::AppConfig; -use crate::gui::views::wallets::wallet::proof::PaymentProofContent; /// Wallet content. pub struct WalletContent { - /// Transactions content. - pub txs_content: Option, + /// Transactions content. + pub txs_content: Option, - /// Settings content. - pub settings_content: Option, + /// Settings content. + pub settings_content: Option, - /// Account panel content. - pub account_content: WalletAccountContent, - /// Transport panel content. - pub transport_content: WalletTransportContent, + /// Account panel content. + pub account_content: WalletAccountContent, + /// Transport panel content. + pub transport_content: WalletTransportContent, - /// Invoice request creation [`Modal`] content. - invoice_content: Option, - /// Send request creation [`Modal`] content. - send_content: Option, - /// Slatepack message input [`Modal`] content. - message_content: Option + /// Invoice request creation [`Modal`] content. + invoice_content: Option, + /// Send request creation [`Modal`] content. + send_content: Option, + /// Slatepack message input [`Modal`] content. + message_content: Option, } /// Identifier for invoice creation [`Modal`]. @@ -60,590 +62,587 @@ const INVOICE_MODAL_ID: &'static str = "invoice_request_modal"; const SEND_MODAL_ID: &'static str = "send_request_modal"; impl WalletContentContainer for WalletContent { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - INVOICE_MODAL_ID, - SEND_MODAL_ID, - MessageInputContent::MODAL_ID - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![ + INVOICE_MODAL_ID, + SEND_MODAL_ID, + MessageInputContent::MODAL_ID, + ] + } - fn modal_ui(&mut self, ui: &mut egui::Ui, w: &Wallet, m: &Modal, cb: &dyn PlatformCallbacks) { - match m.id { - INVOICE_MODAL_ID => { - if self.invoice_content.is_none() { - self.invoice_content = Some(InvoiceRequestContent::default()); - } - self.invoice_content.as_mut().unwrap().ui(ui, w, m, cb); - } - SEND_MODAL_ID => { - if self.send_content.is_none() { - self.send_content = Some(SendRequestContent::new(None)); - } - self.send_content.as_mut().unwrap().ui(ui, w, m, cb); - } - MessageInputContent::MODAL_ID => { - if self.message_content.is_none() { - self.message_content = Some(MessageInputContent::default()); - } - self.message_content.as_mut().unwrap().ui(ui, w, m, cb); - } - _ => { - self.invoice_content = None; - self.send_content = None; - self.message_content = None; - } - } - } + fn modal_ui(&mut self, ui: &mut egui::Ui, w: &Wallet, m: &Modal, cb: &dyn PlatformCallbacks) { + match m.id { + INVOICE_MODAL_ID => { + if self.invoice_content.is_none() { + self.invoice_content = Some(InvoiceRequestContent::default()); + } + self.invoice_content.as_mut().unwrap().ui(ui, w, m, cb); + } + SEND_MODAL_ID => { + if self.send_content.is_none() { + self.send_content = Some(SendRequestContent::new(None)); + } + self.send_content.as_mut().unwrap().ui(ui, w, m, cb); + } + MessageInputContent::MODAL_ID => { + if self.message_content.is_none() { + self.message_content = Some(MessageInputContent::default()); + } + self.message_content.as_mut().unwrap().ui(ui, w, m, cb); + } + _ => { + self.invoice_content = None; + self.send_content = None; + self.message_content = None; + } + } + } - fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - let dual_panel = Content::is_dual_panel_mode(ui.ctx()); - let show_wallets_dual = AppConfig::show_wallets_at_dual_panel(); + fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { + let dual_panel = Content::is_dual_panel_mode(ui.ctx()); + let show_wallets_dual = AppConfig::show_wallets_at_dual_panel(); - let wallet_id = wallet.identifier(); - let data = wallet.get_data(); - let block_nav = self.block_navigation_on_sync(wallet); + let wallet_id = wallet.identifier(); + let data = wallet.get_data(); + let block_nav = self.block_navigation_on_sync(wallet); - // Show wallet account panel not on settings tab when navigation is not blocked and QR code - // scanner is not showing and wallet data is not empty. - let mut show_account = self.settings_content.is_none() && !block_nav - && !wallet.sync_error() && data.is_some(); - if wallet.get_current_connection() == ConnectionMethod::Integrated && - !Node::is_running() { - show_account = false; - } + // Show wallet account panel not on settings tab when navigation is not blocked and QR code + // scanner is not showing and wallet data is not empty. + let mut show_account = + self.settings_content.is_none() && !block_nav && !wallet.sync_error() && data.is_some(); + if wallet.get_current_connection() == ConnectionMethod::Integrated && !Node::is_running() { + show_account = false; + } - // Show wallet tabs. - let side_padding = View::TAB_ITEMS_PADDING + if View::is_desktop() { - 0.0 - } else { - 4.0 - }; - let tabs_margin = Margin { - left: (View::far_left_inset_margin(ui) + side_padding) as i8, - right: (View::get_right_inset() + side_padding) as i8, - top: View::TAB_ITEMS_PADDING as i8, - bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8, - }; - egui::TopBottomPanel::bottom("wallet_tabs") - .frame(egui::Frame { - inner_margin: tabs_margin, - fill: Colors::fill(), - ..Default::default() - }) - .show_animated_inside(ui, !block_nav, |ui| { - let r = ui.available_rect_before_wrap(); - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - self.tabs_ui(wallet, ui); - }); - let rect = { - let mut r = r.clone(); - r.min.x -= tabs_margin.left as f32; - r.min.y -= tabs_margin.top as f32; - r.max.x += tabs_margin.right as f32; - r.max.y += tabs_margin.bottom as f32; - r - }; - // Draw cover for content below opened panel. - if self.can_back() && show_account { - View::content_cover_ui(ui, rect, "wallet_tabs_content_cover", || { - self.back(cb); - }); - } else { - // Draw content divider line. - View::line(ui, LinePosition::TOP, &rect, Colors::stroke()); - } - }); + // Show wallet tabs. + let side_padding = View::TAB_ITEMS_PADDING + if View::is_desktop() { 0.0 } else { 4.0 }; + let tabs_margin = Margin { + left: (View::far_left_inset_margin(ui) + side_padding) as i8, + right: (View::get_right_inset() + side_padding) as i8, + top: View::TAB_ITEMS_PADDING as i8, + bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8, + }; + egui::TopBottomPanel::bottom("wallet_tabs") + .frame(egui::Frame { + inner_margin: tabs_margin, + fill: Colors::fill(), + ..Default::default() + }) + .show_animated_inside(ui, !block_nav, |ui| { + let r = ui.available_rect_before_wrap(); + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + self.tabs_ui(wallet, ui); + }); + let rect = { + let mut r = r.clone(); + r.min.x -= tabs_margin.left as f32; + r.min.y -= tabs_margin.top as f32; + r.max.x += tabs_margin.right as f32; + r.max.y += tabs_margin.bottom as f32; + r + }; + // Draw cover for content below opened panel. + if self.can_back() && show_account { + View::content_cover_ui(ui, rect, "wallet_tabs_content_cover", || { + self.back(cb); + }); + } else { + // Draw content divider line. + View::line(ui, LinePosition::TOP, &rect, Colors::stroke()); + } + }); - // Close scanner or account list when account panel got hidden. - if !show_account { - if self.account_content.qr_scan_showing() { - self.account_content.close_qr_scan(cb); - } else { - self.account_content.show_list = false; - } - } + // Close scanner or account list when account panel got hidden. + if !show_account { + if self.account_content.qr_scan_showing() { + self.account_content.close_qr_scan(cb); + } else { + self.account_content.show_list = false; + } + } - // Flag to check if account panel is opened. - let top_panel_expanded = self.account_content.can_back() || - self.transport_content.can_back(); + // Flag to check if account panel is opened. + let top_panel_expanded = + self.account_content.can_back() || self.transport_content.can_back(); - // Show wallet account content. - if !self.transport_content.can_back() && show_account { - egui::TopBottomPanel::top(Id::from("wallet_account").with(wallet.identifier())) - .frame(egui::Frame { - inner_margin: Margin { - left: (View::far_left_inset_margin(ui) + View::content_padding()) as i8, - right: (View::get_right_inset() + View::content_padding()) as i8, - top: View::content_padding() as i8, - bottom: 0.0 as i8, - }, - fill: if top_panel_expanded { - Colors::fill_lite() - } else { - Colors::TRANSPARENT - }, - ..Default::default() - }) - .show_inside(ui, |ui| { - let rect = ui.available_rect_before_wrap(); - self.account_content.ui(ui, &wallet, cb); - // Draw content divider lines. - let r = { - let mut r = rect.clone(); - r.min.x -= View::content_padding() + View::far_left_inset_margin(ui); - r.min.y -= View::content_padding(); - r.max.x += View::content_padding() + View::get_right_inset(); - r - }; - if dual_panel && show_wallets_dual { - View::line(ui, LinePosition::LEFT, &r, Colors::item_stroke()); - } - }); - } + // Show wallet account content. + if !self.transport_content.can_back() && show_account { + egui::TopBottomPanel::top(Id::from("wallet_account").with(wallet.identifier())) + .frame(egui::Frame { + inner_margin: Margin { + left: (View::far_left_inset_margin(ui) + View::content_padding()) as i8, + right: (View::get_right_inset() + View::content_padding()) as i8, + top: View::content_padding() as i8, + bottom: 0.0 as i8, + }, + fill: if top_panel_expanded { + Colors::fill_lite() + } else { + Colors::TRANSPARENT + }, + ..Default::default() + }) + .show_inside(ui, |ui| { + let rect = ui.available_rect_before_wrap(); + self.account_content.ui(ui, &wallet, cb); + // Draw content divider lines. + let r = { + let mut r = rect.clone(); + r.min.x -= View::content_padding() + View::far_left_inset_margin(ui); + r.min.y -= View::content_padding(); + r.max.x += View::content_padding() + View::get_right_inset(); + r + }; + if dual_panel && show_wallets_dual { + View::line(ui, LinePosition::LEFT, &r, Colors::item_stroke()); + } + }); + } - // Show wallet transport content. - if !self.account_content.can_back() && show_account { - egui::TopBottomPanel::top(Id::from("wallet_transport").with(wallet.identifier())) - .frame(egui::Frame { - inner_margin: Margin { - left: (View::far_left_inset_margin(ui) + View::content_padding()) as i8, - right: (View::get_right_inset() + View::content_padding()) as i8, - top: 1.0 as i8, - bottom: 1.0 as i8, - }, - fill: if top_panel_expanded { - if self.transport_content.qr_address_content.is_some() { - Colors::FILL_DEEP - } else { - Colors::fill_lite() - } - } else { - Colors::TRANSPARENT - }, - ..Default::default() - }) - .show_inside(ui, |ui| { - let rect = ui.available_rect_before_wrap(); - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - self.transport_content.ui(ui, &wallet, cb); - }); - // Draw content divider lines. - let r = { - let mut r = rect.clone(); - r.min.x -= View::content_padding() + View::far_left_inset_margin(ui); - r.min.y -= 1.0; - r.max.x += View::content_padding() + View::get_right_inset(); - r - }; - if dual_panel && show_wallets_dual { - View::line(ui, LinePosition::LEFT, &r, Colors::item_stroke()); - } - }); - } + // Show wallet transport content. + if !self.account_content.can_back() && show_account { + egui::TopBottomPanel::top(Id::from("wallet_transport").with(wallet.identifier())) + .frame(egui::Frame { + inner_margin: Margin { + left: (View::far_left_inset_margin(ui) + View::content_padding()) as i8, + right: (View::get_right_inset() + View::content_padding()) as i8, + top: 1.0 as i8, + bottom: 1.0 as i8, + }, + fill: if top_panel_expanded { + if self.transport_content.qr_address_content.is_some() { + Colors::FILL_DEEP + } else { + Colors::fill_lite() + } + } else { + Colors::TRANSPARENT + }, + ..Default::default() + }) + .show_inside(ui, |ui| { + let rect = ui.available_rect_before_wrap(); + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + self.transport_content.ui(ui, &wallet, cb); + }); + // Draw content divider lines. + let r = { + let mut r = rect.clone(); + r.min.x -= View::content_padding() + View::far_left_inset_margin(ui); + r.min.y -= 1.0; + r.max.x += View::content_padding() + View::get_right_inset(); + r + }; + if dual_panel && show_wallets_dual { + View::line(ui, LinePosition::LEFT, &r, Colors::item_stroke()); + } + }); + } - // Show tab content. - egui::CentralPanel::default() - .frame(egui::Frame { - inner_margin: Margin { - left: (View::far_left_inset_margin(ui) + View::content_padding()) as i8, - right: (View::get_right_inset() + View::content_padding()) as i8, - top: 0.0 as i8, - bottom: 4.0 as i8, - }, - fill: if self.settings_content.is_some() { - Colors::fill_lite() - } else { - Colors::TRANSPARENT - }, - ..Default::default() - }) - .show_inside(ui, |ui| { - let rect = ui.available_rect_before_wrap(); - let show_settings = self.settings_content.is_some(); - let show_txs = self.txs_content.is_some() && !top_panel_expanded; - let show_sync = (!show_settings || block_nav) && - sync_ui(ui, &wallet); - if !show_sync { - if show_settings { - ui.add_space(3.0); - ScrollArea::vertical() - .id_salt(Id::from("wallet_tab_content_scroll").with(wallet_id)) - .auto_shrink([false; 2]) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .show(ui, |ui| { - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - self.settings_content - .as_mut() - .unwrap() - .ui(ui, &wallet, cb); - }); - }); - } else if show_txs { - self.txs_content - .as_mut() - .unwrap() - .ui(ui, &wallet, cb); - } + // Show tab content. + egui::CentralPanel::default() + .frame(egui::Frame { + inner_margin: Margin { + left: (View::far_left_inset_margin(ui) + View::content_padding()) as i8, + right: (View::get_right_inset() + View::content_padding()) as i8, + top: 0.0 as i8, + bottom: 4.0 as i8, + }, + fill: if self.settings_content.is_some() { + Colors::fill_lite() + } else { + Colors::TRANSPARENT + }, + ..Default::default() + }) + .show_inside(ui, |ui| { + let rect = ui.available_rect_before_wrap(); + let show_settings = self.settings_content.is_some(); + let show_txs = self.txs_content.is_some() && !top_panel_expanded; + let show_sync = (!show_settings || block_nav) && sync_ui(ui, &wallet); + if !show_sync { + if show_settings { + ui.add_space(3.0); + ScrollArea::vertical() + .id_salt(Id::from("wallet_tab_content_scroll").with(wallet_id)) + .auto_shrink([false; 2]) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .show(ui, |ui| { + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + self.settings_content.as_mut().unwrap().ui(ui, &wallet, cb); + }); + }); + } else if show_txs { + self.txs_content.as_mut().unwrap().ui(ui, &wallet, cb); + } - // Handle wallet task result. - self.handle_task_result(wallet); - } - let rect = { - let mut r = rect.clone(); - r.min.x -= View::far_left_inset_margin(ui) + View::content_padding(); - r.max.x += View::get_right_inset() + View::content_padding(); - r.max.y += 4.0; - r - }; - // Draw cover for content below opened panel. - if !show_sync && self.can_back() { - View::content_cover_ui(ui, rect, "wallet_panel_content_cover", || { - self.back(cb); - }); - } - // Draw content divider line. - if dual_panel && show_wallets_dual { - View::line(ui, LinePosition::LEFT, &rect, Colors::item_stroke()); - } - }); - } + // Handle wallet task result. + self.handle_task_result(wallet); + } + let rect = { + let mut r = rect.clone(); + r.min.x -= View::far_left_inset_margin(ui) + View::content_padding(); + r.max.x += View::get_right_inset() + View::content_padding(); + r.max.y += 4.0; + r + }; + // Draw cover for content below opened panel. + if !show_sync && self.can_back() { + View::content_cover_ui(ui, rect, "wallet_panel_content_cover", || { + self.back(cb); + }); + } + // Draw content divider line. + if dual_panel && show_wallets_dual { + View::line(ui, LinePosition::LEFT, &rect, Colors::item_stroke()); + } + }); + } } impl Default for WalletContent { - fn default() -> Self { - Self { - txs_content: Some(WalletTransactionsContent::new(None)), - settings_content: None, - account_content: WalletAccountContent::default(), - transport_content: WalletTransportContent::default(), - invoice_content: None, - send_content: None, - message_content: None, - } - } + fn default() -> Self { + Self { + txs_content: Some(WalletTransactionsContent::new(None)), + settings_content: None, + account_content: WalletAccountContent::default(), + transport_content: WalletTransportContent::default(), + invoice_content: None, + send_content: None, + message_content: None, + } + } } impl WalletContent { - /// Get title based on current navigation state. - pub fn title(&self) -> impl Into { - if self.account_content.qr_scan_showing() { - t!("scan_qr") - } else if self.account_content.show_list { - t!("wallets.accounts") - } else if self.transport_content.settings_content.is_some() { - t!("wallets.transport") - } else if self.transport_content.qr_address_content.is_some() { - t!("network_mining.address") - } else if self.settings_content.is_some() { - t!("wallets.settings") - } else { - t!("wallets.txs") - } - } + /// Get title based on current navigation state. + pub fn title(&self) -> impl Into { + if self.account_content.qr_scan_showing() { + t!("scan_qr") + } else if self.account_content.show_list { + t!("wallets.accounts") + } else if self.transport_content.settings_content.is_some() { + t!("wallets.transport") + } else if self.transport_content.qr_address_content.is_some() { + t!("network_mining.address") + } else if self.settings_content.is_some() { + t!("wallets.settings") + } else { + t!("wallets.txs") + } + } - /// Check if it's possible to go back at navigation stack. - pub fn can_back(&self) -> bool { - self.account_content.can_back() || self.transport_content.can_back() - } + /// Check if it's possible to go back at navigation stack. + pub fn can_back(&self) -> bool { + self.account_content.can_back() || self.transport_content.can_back() + } - /// Navigate back on navigation stack. - pub fn back(&mut self, cb: &dyn PlatformCallbacks) { - if self.account_content.can_back() { - self.account_content.back(cb); - } else if self.transport_content.can_back() { - self.transport_content.back(); - } - } + /// Navigate back on navigation stack. + pub fn back(&mut self, cb: &dyn PlatformCallbacks) { + if self.account_content.can_back() { + self.account_content.back(cb); + } else if self.transport_content.can_back() { + self.transport_content.back(); + } + } - /// Check when to block tabs navigation on sync progress. - fn block_navigation_on_sync(&self, wallet: &Wallet) -> bool { - let sync_error = wallet.sync_error(); - let integrated_node = wallet.get_current_connection() == ConnectionMethod::Integrated; - let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync); - let sync_after_opening = wallet.get_data().is_none() && !wallet.sync_error(); - // Block navigation if wallet is repairing and integrated node is not launching - // and if wallet is closing or syncing after opening when there is no data to show. - (wallet.is_repairing() && (integrated_node_ready || !integrated_node) && !sync_error) - || wallet.is_closing() || (sync_after_opening && - (!integrated_node || integrated_node_ready)) - } + /// Check when to block tabs navigation on sync progress. + fn block_navigation_on_sync(&self, wallet: &Wallet) -> bool { + let sync_error = wallet.sync_error(); + let integrated_node = wallet.get_current_connection() == ConnectionMethod::Integrated; + let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync); + let sync_after_opening = wallet.get_data().is_none() && !wallet.sync_error(); + // Block navigation if wallet is repairing and integrated node is not launching + // and if wallet is closing or syncing after opening when there is no data to show. + (wallet.is_repairing() && (integrated_node_ready || !integrated_node) && !sync_error) + || wallet.is_closing() + || (sync_after_opening && (!integrated_node || integrated_node_ready)) + } - /// Draw tab buttons at the bottom of the screen. - fn tabs_ui(&mut self, wallet: &Wallet, ui: &mut egui::Ui) { - ui.scope(|ui| { - // Setup spacing between tabs. - ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); + /// Draw tab buttons at the bottom of the screen. + fn tabs_ui(&mut self, wallet: &Wallet, ui: &mut egui::Ui) { + ui.scope(|ui| { + // Setup spacing between tabs. + ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); - let has_wallet_data = wallet.get_data().is_some(); - let can_send = if has_wallet_data { - wallet.get_data().unwrap().info.amount_currently_spendable > 0 - } else { - false - }; + let has_wallet_data = wallet.get_data().is_some(); + let can_send = if has_wallet_data { + wallet.get_data().unwrap().info.amount_currently_spendable > 0 + } else { + false + }; - let tabs_amount = if can_send { 5 } else { 4 }; - ui.columns(tabs_amount, |columns| { - columns[0].vertical_centered_justified(|ui| { - let active = self.settings_content.is_none() && self.txs_content.is_some(); - View::tab_button(ui, STACK, None, Some(active), |_| { - self.txs_content = Some(WalletTransactionsContent::new(None)); - self.settings_content = None; - }); - }); - let active = if has_wallet_data { Some(false) } else { None }; - columns[1].vertical_centered_justified(|ui| { - if wallet.invoice_creating() { - ui.add_space(4.0); - View::small_loading_spinner(ui); - } else { - let (icon, color) = (FILE_ARROW_DOWN, Some(Colors::green())); - View::tab_button(ui, icon, color, active, |_| { - if self.txs_content.is_none() { - self.txs_content = Some(WalletTransactionsContent::new(None)); - self.settings_content = None; - } - self.message_content = None; - self.invoice_content = Some(InvoiceRequestContent::default()); - Modal::new(INVOICE_MODAL_ID) - .position(ModalPosition::CenterTop) - .title(t!("wallets.receive")) - .show(); - }); - } - }); - columns[2].vertical_centered_justified(|ui| { - if wallet.message_opening() { - ui.add_space(4.0); - View::small_loading_spinner(ui); - } else { - let (icon, color) = (FILE_TEXT, Some(Colors::gold_dark())); - View::tab_button(ui, icon, color, active, |_| { - if self.txs_content.is_none() { - self.txs_content = Some(WalletTransactionsContent::new(None)); - self.settings_content = None; - } - self.message_content = Some(MessageInputContent::default()); - Modal::new(MessageInputContent::MODAL_ID) - .position(ModalPosition::Center) - .title(t!("wallets.messages")) - .show(); - }); - } - }); - if can_send { - columns[3].vertical_centered_justified(|ui| { - if wallet.send_creating() { - ui.add_space(4.0); - View::small_loading_spinner(ui); - } else { - let (icon, color) = (FILE_ARROW_UP, Some(Colors::red())); - View::tab_button(ui, icon, color, active, |_| { - if self.txs_content.is_none() { - self.txs_content = Some(WalletTransactionsContent::new(None)); - self.settings_content = None; - } - self.message_content = None; - self.send_content = Some(SendRequestContent::new(None)); - Modal::new(SEND_MODAL_ID) - .position(ModalPosition::CenterTop) - .title(t!("wallets.send")) - .show(); - }); - } - }); - } - columns[tabs_amount - 1].vertical_centered_justified(|ui| { - let active = self.settings_content.is_some(); - View::tab_button(ui, GEAR_FINE, None, Some(active), |ui| { - ExternalConnection::check(None, ui.ctx()); - self.txs_content = None; - self.settings_content = Some(WalletSettingsContent::default()); - }); - }); - }); - }); - } + let tabs_amount = if can_send { 5 } else { 4 }; + ui.columns(tabs_amount, |columns| { + columns[0].vertical_centered_justified(|ui| { + let active = self.settings_content.is_none() && self.txs_content.is_some(); + View::tab_button(ui, STACK, None, Some(active), |_| { + self.txs_content = Some(WalletTransactionsContent::new(None)); + self.settings_content = None; + }); + }); + let active = if has_wallet_data { Some(false) } else { None }; + columns[1].vertical_centered_justified(|ui| { + if wallet.invoice_creating() { + ui.add_space(4.0); + View::small_loading_spinner(ui); + } else { + let (icon, color) = (FILE_ARROW_DOWN, Some(Colors::green())); + View::tab_button(ui, icon, color, active, |_| { + if self.txs_content.is_none() { + self.txs_content = Some(WalletTransactionsContent::new(None)); + self.settings_content = None; + } + self.message_content = None; + self.invoice_content = Some(InvoiceRequestContent::default()); + Modal::new(INVOICE_MODAL_ID) + .position(ModalPosition::CenterTop) + .title(t!("wallets.receive")) + .show(); + }); + } + }); + columns[2].vertical_centered_justified(|ui| { + if wallet.message_opening() { + ui.add_space(4.0); + View::small_loading_spinner(ui); + } else { + let (icon, color) = (FILE_TEXT, Some(Colors::gold_dark())); + View::tab_button(ui, icon, color, active, |_| { + if self.txs_content.is_none() { + self.txs_content = Some(WalletTransactionsContent::new(None)); + self.settings_content = None; + } + self.message_content = Some(MessageInputContent::default()); + Modal::new(MessageInputContent::MODAL_ID) + .position(ModalPosition::Center) + .title(t!("wallets.messages")) + .show(); + }); + } + }); + if can_send { + columns[3].vertical_centered_justified(|ui| { + if wallet.send_creating() { + ui.add_space(4.0); + View::small_loading_spinner(ui); + } else { + let (icon, color) = (FILE_ARROW_UP, Some(Colors::red())); + View::tab_button(ui, icon, color, active, |_| { + if self.txs_content.is_none() { + self.txs_content = Some(WalletTransactionsContent::new(None)); + self.settings_content = None; + } + self.message_content = None; + self.send_content = Some(SendRequestContent::new(None)); + Modal::new(SEND_MODAL_ID) + .position(ModalPosition::CenterTop) + .title(t!("wallets.send")) + .show(); + }); + } + }); + } + columns[tabs_amount - 1].vertical_centered_justified(|ui| { + let active = self.settings_content.is_some(); + View::tab_button(ui, GEAR_FINE, None, Some(active), |ui| { + ExternalConnection::check(None, ui.ctx()); + self.txs_content = None; + self.settings_content = Some(WalletSettingsContent::default()); + }); + }); + }); + }); + } - /// Handle wallet task result. - fn handle_task_result(&mut self, wallet: &Wallet) { - let res = wallet.consume_task_result(); - if res.is_none() || wallet.get_data().is_none() { - return; - } - let (id, t) = res.unwrap(); - match Modal::opened() { - None => { - // Show transaction modal on wallet task result. - if let Some(id) = id { - let tx = wallet.get_data().unwrap().tx_by_id(id); - if tx.is_some() { - self.txs_content = Some(WalletTransactionsContent::new(tx)); - self.settings_content = None; - } - } - } - Some(modal_id) => { - match modal_id { - SEND_MODAL_ID => { - match t { - WalletTask::CalculateFee(_, f) => { - // Setup calculated tx fee at modal. - if let Some(m) = self.send_content.as_mut() { - if m.max_calculating { - let data = wallet.get_data().unwrap(); - let a = data.info.amount_currently_spendable; - let max = if f > a { - 0 - } else { - a - f - }; - m.on_max_amount_calculated(max, f); - } else { - m.on_fee_calculated(f); - } - } - } - _ => {} - } - } - MessageInputContent::MODAL_ID => { - match t { - WalletTask::VerifyProof(proof, res) => { - if let Some(res) = res { - // Update message content on validation error - // or when tx not belongs to current wallet. - if res.is_err() || (!res.as_ref().unwrap().1 && - !res.as_ref().unwrap().2) { - if let Some(m) = self.message_content.as_mut() { - if let Ok(p) = serde_json::to_string_pretty(&proof) { - let mut c = PaymentProofContent::new(Some(p)); - c.validation_result = Some(res); - m.proof_content = Some(c); - } - } - } else if let Ok((id, _, _)) = res { - let tx = wallet.get_data().unwrap().tx_by_id(id); - if let Some(tx) = tx { - let mut tx_c = WalletTransactionsContent::new(Some(tx)); - if let Ok(p) = serde_json::to_string_pretty(&proof) { - let proof_c = PaymentProofContent::new(Some(p)); - tx_c.tx_info_content - .as_mut() - .unwrap() - .proof_content = Some(proof_c); - self.txs_content = Some(tx_c); - self.settings_content = None; - } - } - } - } - } - _ => {} - } - } - _ => {} - } - } - } - } + /// Handle wallet task result. + fn handle_task_result(&mut self, wallet: &Wallet) { + let res = wallet.consume_task_result(); + if res.is_none() || wallet.get_data().is_none() { + return; + } + let (id, t) = res.unwrap(); + match Modal::opened() { + None => { + // Show transaction modal on wallet task result. + if let Some(id) = id { + let tx = wallet.get_data().unwrap().tx_by_id(id); + if tx.is_some() { + self.txs_content = Some(WalletTransactionsContent::new(tx)); + self.settings_content = None; + } + } + } + Some(modal_id) => { + match modal_id { + SEND_MODAL_ID => { + match t { + WalletTask::CalculateFee(_, f) => { + // Setup calculated tx fee at modal. + if let Some(m) = self.send_content.as_mut() { + if m.max_calculating { + let data = wallet.get_data().unwrap(); + let a = data.info.amount_currently_spendable; + let max = if f > a { 0 } else { a - f }; + m.on_max_amount_calculated(max, f); + } else { + m.on_fee_calculated(f); + } + } + } + _ => {} + } + } + MessageInputContent::MODAL_ID => { + match t { + WalletTask::VerifyProof(proof, res) => { + if let Some(res) = res { + // Update message content on validation error + // or when tx not belongs to current wallet. + if res.is_err() + || (!res.as_ref().unwrap().1 && !res.as_ref().unwrap().2) + { + if let Some(m) = self.message_content.as_mut() { + if let Ok(p) = serde_json::to_string_pretty(&proof) { + let mut c = PaymentProofContent::new(Some(p)); + c.validation_result = Some(res); + m.proof_content = Some(c); + } + } + } else if let Ok((id, _, _)) = res { + let tx = wallet.get_data().unwrap().tx_by_id(id); + if let Some(tx) = tx { + let mut tx_c = WalletTransactionsContent::new(Some(tx)); + if let Ok(p) = serde_json::to_string_pretty(&proof) { + let proof_c = PaymentProofContent::new(Some(p)); + tx_c.tx_info_content + .as_mut() + .unwrap() + .proof_content = Some(proof_c); + self.txs_content = Some(tx_c); + self.settings_content = None; + } + } + } + } + } + _ => {} + } + } + _ => {} + } + } + } + } } /// Draw content when wallet is syncing and not ready to use, returns `true` at this case. fn sync_ui(ui: &mut egui::Ui, wallet: &Wallet) -> bool { - if wallet.is_repairing() && !wallet.sync_error() { - sync_progress_ui(ui, wallet); - return true; - } else if wallet.is_closing() || wallet.files_moving() { - sync_progress_ui(ui, wallet); - return true; - } else if wallet.get_current_connection() == ConnectionMethod::Integrated { - if !Node::is_running() || Node::is_stopping() { - View::center_content(ui, 108.0, |ui| { - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - let text = t!("wallets.enable_node", "settings" => GEAR_FINE); - ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text())); - ui.add_space(8.0); - // Show button to enable integrated node at non-dual root panel mode - // or when network connections are not showing and node is not stopping - let dual_panel = Content::is_dual_panel_mode(ui.ctx()); - if (!dual_panel || AppConfig::show_connections_network_panel()) - && !Node::is_stopping() { - let enable_text = format!("{} {}", POWER, t!("network.enable_node")); - View::action_button(ui, enable_text, || { - Node::start(); - }); - } - }); - }); - return true - } else if wallet.sync_error() - && Node::get_sync_status() == Some(SyncStatus::NoSync) { - sync_error_ui(ui, wallet); - return true; - } else if wallet.get_data().is_none() { - sync_progress_ui(ui, wallet); - return true; - } - } else if wallet.sync_error() { - sync_error_ui(ui, wallet); - return true; - } else if wallet.get_data().is_none() { - sync_progress_ui(ui, wallet); - return true; - } - false + if wallet.is_repairing() && !wallet.sync_error() { + sync_progress_ui(ui, wallet); + return true; + } else if wallet.is_closing() || wallet.files_moving() { + sync_progress_ui(ui, wallet); + return true; + } else if wallet.get_current_connection() == ConnectionMethod::Integrated { + if !Node::is_running() || Node::is_stopping() { + View::center_content(ui, 108.0, |ui| { + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + let text = t!("wallets.enable_node", "settings" => GEAR_FINE); + ui.label( + RichText::new(text) + .size(16.0) + .color(Colors::inactive_text()), + ); + ui.add_space(8.0); + // Show button to enable integrated node at non-dual root panel mode + // or when network connections are not showing and node is not stopping + let dual_panel = Content::is_dual_panel_mode(ui.ctx()); + if (!dual_panel || AppConfig::show_connections_network_panel()) + && !Node::is_stopping() + { + let enable_text = format!("{} {}", POWER, t!("network.enable_node")); + View::action_button(ui, enable_text, || { + Node::start(); + }); + } + }); + }); + return true; + } else if wallet.sync_error() && Node::get_sync_status() == Some(SyncStatus::NoSync) { + sync_error_ui(ui, wallet); + return true; + } else if wallet.get_data().is_none() { + sync_progress_ui(ui, wallet); + return true; + } + } else if wallet.sync_error() { + sync_error_ui(ui, wallet); + return true; + } else if wallet.get_data().is_none() { + sync_progress_ui(ui, wallet); + return true; + } + false } /// Draw wallet sync error content. fn sync_error_ui(ui: &mut egui::Ui, wallet: &Wallet) { - View::center_content(ui, 108.0, |ui| { - let text = t!("wallets.wallet_loading_err", "settings" => GEAR_FINE); - ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text())); - ui.add_space(8.0); - let retry_text = format!("{} {}", ARROWS_CLOCKWISE, t!("retry")); - View::action_button(ui, retry_text, || { - wallet.set_sync_error(false); - }); - }); + View::center_content(ui, 108.0, |ui| { + let text = t!("wallets.wallet_loading_err", "settings" => GEAR_FINE); + ui.label( + RichText::new(text) + .size(16.0) + .color(Colors::inactive_text()), + ); + ui.add_space(8.0); + let retry_text = format!("{} {}", ARROWS_CLOCKWISE, t!("retry")); + View::action_button(ui, retry_text, || { + wallet.set_sync_error(false); + }); + }); } /// Draw wallet sync progress content. fn sync_progress_ui(ui: &mut egui::Ui, wallet: &Wallet) { - View::center_content(ui, 162.0, |ui| { - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - View::big_loading_spinner(ui); - ui.add_space(18.0); - // Setup sync progress text. - let text = { - let int_node = wallet.get_current_connection() == ConnectionMethod::Integrated; - let int_ready = Node::get_sync_status() == Some(SyncStatus::NoSync); - let info_progress = wallet.info_sync_progress(); + View::center_content(ui, 162.0, |ui| { + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + View::big_loading_spinner(ui); + ui.add_space(18.0); + // Setup sync progress text. + let text = { + let int_node = wallet.get_current_connection() == ConnectionMethod::Integrated; + let int_ready = Node::get_sync_status() == Some(SyncStatus::NoSync); + let info_progress = wallet.info_sync_progress(); - if wallet.files_moving() { - t!("moving_files").into() - } else if wallet.is_closing() { - t!("wallets.wallet_closing").into() - } else if int_node && !int_ready { - t!("wallets.node_loading", "settings" => GEAR_FINE).into() - } else if wallet.is_repairing() { - let repair_progress = wallet.repairing_progress(); - if repair_progress == 0 { - t!("wallets.wallet_checking").into() - } else { - format!("{}: {}%", t!("wallets.wallet_checking"), repair_progress) - } - } else if info_progress != 100 { - if info_progress == 0 { - t!("wallets.wallet_loading").into() - } else { - format!("{}: {}%", t!("wallets.wallet_loading"), info_progress) - } - } else { - t!("wallets.tx_loading").into() - } - }; - ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text())); - }); - }); -} \ No newline at end of file + if wallet.files_moving() { + t!("moving_files").into() + } else if wallet.is_closing() { + t!("wallets.wallet_closing").into() + } else if int_node && !int_ready { + t!("wallets.node_loading", "settings" => GEAR_FINE).into() + } else if wallet.is_repairing() { + let repair_progress = wallet.repairing_progress(); + if repair_progress == 0 { + t!("wallets.wallet_checking").into() + } else { + format!("{}: {}%", t!("wallets.wallet_checking"), repair_progress) + } + } else if info_progress != 100 { + if info_progress == 0 { + t!("wallets.wallet_loading").into() + } else { + format!("{}: {}%", t!("wallets.wallet_loading"), info_progress) + } + } else { + t!("wallets.tx_loading").into() + } + }; + ui.label( + RichText::new(text) + .size(16.0) + .color(Colors::inactive_text()), + ); + }); + }); +} diff --git a/src/gui/views/wallets/wallet/message.rs b/src/gui/views/wallets/wallet/message.rs index 1c7c7f6a..86695840 100644 --- a/src/gui/views/wallets/wallet/message.rs +++ b/src/gui/views/wallets/wallet/message.rs @@ -15,237 +15,243 @@ use egui::scroll_area::ScrollBarVisibility; use egui::{Id, RichText, ScrollArea}; +use crate::gui::Colors; use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, SCAN, SEAL_CHECK}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{CameraContent, FilePickContent, FilePickContentType, Modal, View}; -use crate::gui::Colors; use crate::gui::views::wallets::wallet::proof::PaymentProofContent; -use crate::wallet::types::WalletTask; +use crate::gui::views::{CameraContent, FilePickContent, FilePickContentType, Modal, View}; use crate::wallet::Wallet; +use crate::wallet::types::WalletTask; pub struct MessageInputContent { - /// Slatepack input text. - message_edit: String, - /// Flag to check if error happened at Slatepack message parsing. - parse_error: bool, - /// Button to parse picked file content. - file_pick_button: FilePickContent, + /// Slatepack input text. + message_edit: String, + /// Flag to check if error happened at Slatepack message parsing. + parse_error: bool, + /// Button to parse picked file content. + file_pick_button: FilePickContent, - /// QR code scanner content. - scan_qr_content: Option, + /// QR code scanner content. + scan_qr_content: Option, - /// Payment proof input content. - pub proof_content: Option, + /// Payment proof input content. + pub proof_content: Option, } /// Hint for Slatepack message input. const SLATEPACK_MESSAGE_HINT: &'static str = "BEGINSLATEPACK.\n...\n...\n...\nENDSLATEPACK."; impl Default for MessageInputContent { - fn default() -> Self { - Self { - message_edit: "".to_string(), - parse_error: false, - file_pick_button: FilePickContent::new( - FilePickContentType::Button(t!("choose_file").into()) - ), - scan_qr_content: None, - proof_content: None, - } - } + fn default() -> Self { + Self { + message_edit: "".to_string(), + parse_error: false, + file_pick_button: FilePickContent::new(FilePickContentType::Button( + t!("choose_file").into(), + )), + scan_qr_content: None, + proof_content: None, + } + } } impl MessageInputContent { - /// Identifier for [`Modal`]. - pub const MODAL_ID: &'static str = "input_message_modal"; + /// Identifier for [`Modal`]. + pub const MODAL_ID: &'static str = "input_message_modal"; - /// Draw [`Modal`] content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - if let Some(scan_content) = self.scan_qr_content.as_mut() { - if let Some(result) = scan_content.qr_scan_result() { - cb.stop_camera(); - modal.enable_closing(); - self.scan_qr_content = None; - // Parse scan result. - self.on_message_input(result.text(), wallet); - } else { - scan_content.ui(ui, cb); - } - ui.add_space(8.0); + /// Draw [`Modal`] content. + pub fn ui( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks, + ) { + if let Some(scan_content) = self.scan_qr_content.as_mut() { + if let Some(result) = scan_content.qr_scan_result() { + cb.stop_camera(); + modal.enable_closing(); + self.scan_qr_content = None; + // Parse scan result. + self.on_message_input(result.text(), wallet); + } else { + scan_content.ui(ui, cb); + } + ui.add_space(8.0); - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - // Show buttons to close modal or scanner. - ui.columns(2, |cols| { - cols[0].vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - cb.stop_camera(); - self.scan_qr_content = None; - Modal::close(); - }); - }); - cols[1].vertical_centered_justified(|ui| { - View::button(ui, t!("back"), Colors::white_or_black(false), || { - cb.stop_camera(); - self.scan_qr_content = None; - modal.enable_closing(); - }); - }); - }); - } else if let Some(proof_content) = self.proof_content.as_mut() { - proof_content.input_ui(ui, wallet, cb); - ui.add_space(8.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); + // Show buttons to close modal or scanner. + ui.columns(2, |cols| { + cols[0].vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + cb.stop_camera(); + self.scan_qr_content = None; + Modal::close(); + }); + }); + cols[1].vertical_centered_justified(|ui| { + View::button(ui, t!("back"), Colors::white_or_black(false), || { + cb.stop_camera(); + self.scan_qr_content = None; + modal.enable_closing(); + }); + }); + }); + } else if let Some(proof_content) = self.proof_content.as_mut() { + proof_content.input_ui(ui, wallet, cb); + ui.add_space(8.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); - // Show button to close modal. - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.message_edit = "".to_string(); - Modal::close(); - }); - }); - } else { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - let (text, color) = if self.parse_error { - (t!("wallets.parse_slatepack_err"), Colors::red()) - } else { - (t!("wallets.input_slatepack_desc"), Colors::gray()) - }; - ui.label(RichText::new(text).size(16.0).color(color)); - }); - ui.add_space(6.0); + // Show button to close modal. + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + self.message_edit = "".to_string(); + Modal::close(); + }); + }); + } else { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let (text, color) = if self.parse_error { + (t!("wallets.parse_slatepack_err"), Colors::red()) + } else { + (t!("wallets.input_slatepack_desc"), Colors::gray()) + }; + ui.label(RichText::new(text).size(16.0).color(color)); + }); + ui.add_space(6.0); - // Draw slatepack message content. - let message_before = self.message_edit.clone(); - ui.vertical_centered(|ui| { - let scroll_id = Id::from("message_input").with(wallet.identifier()); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(3.0); - ScrollArea::vertical() - .id_salt(scroll_id) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .max_height(128.0) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(7.0); - let input_id = scroll_id.with("_input"); - let resp = egui::TextEdit::multiline(&mut self.message_edit) - .id(input_id) - .font(egui::TextStyle::Small) - .desired_rows(5) - .interactive(true) - .hint_text(SLATEPACK_MESSAGE_HINT) - .desired_width(f32::INFINITY) - .show(ui) - .response; - if View::is_desktop() { - resp.request_focus(); - } - ui.add_space(6.0); - }); - }); - // Parse message on input change. - if message_before != self.message_edit { - self.on_message_input(self.message_edit.clone(), wallet); - } + // Draw slatepack message content. + let message_before = self.message_edit.clone(); + ui.vertical_centered(|ui| { + let scroll_id = Id::from("message_input").with(wallet.identifier()); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(3.0); + ScrollArea::vertical() + .id_salt(scroll_id) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(128.0) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + let input_id = scroll_id.with("_input"); + let resp = egui::TextEdit::multiline(&mut self.message_edit) + .id(input_id) + .font(egui::TextStyle::Small) + .desired_rows(5) + .interactive(true) + .hint_text(SLATEPACK_MESSAGE_HINT) + .desired_width(f32::INFINITY) + .show(ui) + .response; + if View::is_desktop() { + resp.request_focus(); + } + ui.add_space(6.0); + }); + }); + // Parse message on input change. + if message_before != self.message_edit { + self.on_message_input(self.message_edit.clone(), wallet); + } - ui.add_space(2.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - if self.parse_error { - // Draw button to clear message input. - let clear_text = format!("{} {}", BROOM, t!("clear")); - View::button(ui, clear_text, Colors::white_or_black(false), || { - self.message_edit = "".to_string(); - self.parse_error = false; - }); - } else { - // Draw button to scan Slatepack message QR code. - let scan_text = format!("{} {}", SCAN, t!("scan")); - View::button(ui, scan_text, Colors::white_or_black(false), || { - modal.disable_closing(); - self.scan_qr_content = Some(CameraContent::default()); - cb.start_camera(); - }); - } - }); - columns[1].vertical_centered_justified(|ui| { - // Draw paste button. - let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); - View::button(ui, paste_text, Colors::white_or_black(false), || { - self.on_message_input(cb.get_string_from_buffer(), wallet); - }); - }); - }); + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + if self.parse_error { + // Draw button to clear message input. + let clear_text = format!("{} {}", BROOM, t!("clear")); + View::button(ui, clear_text, Colors::white_or_black(false), || { + self.message_edit = "".to_string(); + self.parse_error = false; + }); + } else { + // Draw button to scan Slatepack message QR code. + let scan_text = format!("{} {}", SCAN, t!("scan")); + View::button(ui, scan_text, Colors::white_or_black(false), || { + modal.disable_closing(); + self.scan_qr_content = Some(CameraContent::default()); + cb.start_camera(); + }); + } + }); + columns[1].vertical_centered_justified(|ui| { + // Draw paste button. + let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); + View::button(ui, paste_text, Colors::white_or_black(false), || { + self.on_message_input(cb.get_string_from_buffer(), wallet); + }); + }); + }); - // Draw button to pick Slatepack message file. - ui.add_space(8.0); - ui.vertical_centered(|ui| { - let mut picked_data = None; - self.file_pick_button.ui(ui, cb, |data| { - picked_data = Some(data); - }); - if let Some(data) = picked_data { - self.on_message_input(data, wallet); - } - }); + // Draw button to pick Slatepack message file. + ui.add_space(8.0); + ui.vertical_centered(|ui| { + let mut picked_data = None; + self.file_pick_button.ui(ui, cb, |data| { + picked_data = Some(data); + }); + if let Some(data) = picked_data { + self.on_message_input(data, wallet); + } + }); - ui.add_space(8.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); + ui.add_space(8.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); - ui.vertical_centered(|ui| { - let proof_label = format!("{} {}", SEAL_CHECK, t!("wallets.payment_proof")); - View::colored_text_button(ui, - proof_label, - Colors::gold_dark(), - Colors::white_or_black(false), || { - Modal::set_title(t!("wallets.payment_proof")); - self.proof_content = Some(PaymentProofContent::new(None)); - }); - }); - ui.add_space(8.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); + ui.vertical_centered(|ui| { + let proof_label = format!("{} {}", SEAL_CHECK, t!("wallets.payment_proof")); + View::colored_text_button( + ui, + proof_label, + Colors::gold_dark(), + Colors::white_or_black(false), + || { + Modal::set_title(t!("wallets.payment_proof")); + self.proof_content = Some(PaymentProofContent::new(None)); + }, + ); + }); + ui.add_space(8.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); - // Show button to close modal. - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.message_edit = "".to_string(); - Modal::close(); - }); - }); - } - ui.add_space(6.0); - } + // Show button to close modal. + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + self.message_edit = "".to_string(); + Modal::close(); + }); + }); + } + ui.add_space(6.0); + } - /// Parse Slatepack message on input change. - fn on_message_input(&mut self, text: String, wallet: &Wallet) { - self.parse_error = false; - self.message_edit = text; - if self.message_edit.is_empty() { - return; - } - if self.message_edit.starts_with("BEGINSLATEPACK.") && - self.message_edit.ends_with("ENDSLATEPACK.") { - wallet.task(WalletTask::OpenMessage(self.message_edit.to_string())); - self.message_edit = "".to_string(); - Modal::close(); - } else { - self.parse_error = true; - } - } -} \ No newline at end of file + /// Parse Slatepack message on input change. + fn on_message_input(&mut self, text: String, wallet: &Wallet) { + self.parse_error = false; + self.message_edit = text; + if self.message_edit.is_empty() { + return; + } + if self.message_edit.starts_with("BEGINSLATEPACK.") + && self.message_edit.ends_with("ENDSLATEPACK.") + { + wallet.task(WalletTask::OpenMessage(self.message_edit.to_string())); + self.message_edit = "".to_string(); + Modal::close(); + } else { + self.parse_error = true; + } + } +} diff --git a/src/gui/views/wallets/wallet/mod.rs b/src/gui/views/wallets/wallet/mod.rs index 6832c31e..d15fe093 100644 --- a/src/gui/views/wallets/wallet/mod.rs +++ b/src/gui/views/wallets/wallet/mod.rs @@ -24,7 +24,7 @@ mod content; pub use content::WalletContent; mod account; -mod transport; -mod request; mod message; -mod proof; \ No newline at end of file +mod proof; +mod request; +mod transport; diff --git a/src/gui/views/wallets/wallet/proof.rs b/src/gui/views/wallets/wallet/proof.rs index 9ad5a53b..7ded49fa 100644 --- a/src/gui/views/wallets/wallet/proof.rs +++ b/src/gui/views/wallets/wallet/proof.rs @@ -3,221 +3,230 @@ use egui::{Id, RichText, ScrollArea}; use grin_util::ToHex; use grin_wallet_libwallet::{Error, PaymentProof, TxLogEntryType}; +use crate::gui::Colors; use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, COPY, FILE_TEXT, SEAL_CHECK}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{FilePickContent, FilePickContentType, Modal, View}; -use crate::gui::Colors; -use crate::wallet::types::{WalletTask, WalletTx}; use crate::wallet::Wallet; +use crate::wallet::types::{WalletTask, WalletTx}; pub struct PaymentProofContent { - /// Payment proof text. - input_edit: String, - /// Button to pick payment proof file. - pick_button: FilePickContent, - /// Flag to check if an error occurred during proof parsing. - parse_error: bool, - /// Proof validation result. - pub validation_result: Option>, + /// Payment proof text. + input_edit: String, + /// Button to pick payment proof file. + pick_button: FilePickContent, + /// Flag to check if an error occurred during proof parsing. + parse_error: bool, + /// Proof validation result. + pub validation_result: Option>, } impl PaymentProofContent { - /// Create new content to share or validate payment proof. - pub fn new(proof_text: Option) -> Self { - Self { - input_edit: proof_text.unwrap_or("".to_string()), - pick_button: FilePickContent::new(FilePickContentType::Button(t!("file").into())), - parse_error: false, - validation_result: None, - } - } + /// Create new content to share or validate payment proof. + pub fn new(proof_text: Option) -> Self { + Self { + input_edit: proof_text.unwrap_or("".to_string()), + pick_button: FilePickContent::new(FilePickContentType::Button(t!("file").into())), + parse_error: false, + validation_result: None, + } + } - /// Draw transaction payment proof input. - pub fn input_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - if self.parse_error { - let label_text = t!("wallets.payment_proof_error"); - ui.label(RichText::new(label_text).size(16.0).color(Colors::red())); - } else if let Some(proof) = self.validation_result.as_ref() { - match proof { - Ok(_) => { - let label_text = t!("wallets.payment_proof_valid"); - ui.label(RichText::new(label_text).size(16.0).color(Colors::green())); - } - Err(e) => { - let error_text = t!("wallets.payment_proof_error"); - let label_text = format!("{} ({:?})", error_text, e); - ui.label(RichText::new(label_text).size(16.0).color(Colors::red())); - } - } - } else { - let desc_label = t!("wallets.payment_proof_desc"); - ui.label(RichText::new(desc_label).size(16.0).color(Colors::inactive_text())); - } - }); - ui.add_space(6.0); - ui.vertical_centered(|ui| { - let scroll_id = Id::from("tx_info_payment_proof_input"); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(3.0); - ScrollArea::vertical() - .id_salt(scroll_id) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .max_height(128.0) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(7.0); - let input_id = scroll_id.with("edit"); - let proof_input_before = self.input_edit.clone(); - let resp = egui::TextEdit::multiline(&mut self.input_edit) - .id(input_id) - .font(egui::TextStyle::Small) - .desired_rows(5) - .interactive(!wallet.payment_proof_verifying()) - .desired_width(f32::INFINITY) - .show(ui) - .response; - if View::is_desktop() { - resp.request_focus(); - } - // Parse payment proof on input change. - if self.input_edit != proof_input_before { - self.on_proof_edit_change(wallet); - } - ui.add_space(6.0); - }); - }); + /// Draw transaction payment proof input. + pub fn input_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + if self.parse_error { + let label_text = t!("wallets.payment_proof_error"); + ui.label(RichText::new(label_text).size(16.0).color(Colors::red())); + } else if let Some(proof) = self.validation_result.as_ref() { + match proof { + Ok(_) => { + let label_text = t!("wallets.payment_proof_valid"); + ui.label(RichText::new(label_text).size(16.0).color(Colors::green())); + } + Err(e) => { + let error_text = t!("wallets.payment_proof_error"); + let label_text = format!("{} ({:?})", error_text, e); + ui.label(RichText::new(label_text).size(16.0).color(Colors::red())); + } + } + } else { + let desc_label = t!("wallets.payment_proof_desc"); + ui.label( + RichText::new(desc_label) + .size(16.0) + .color(Colors::inactive_text()), + ); + } + }); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let scroll_id = Id::from("tx_info_payment_proof_input"); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(3.0); + ScrollArea::vertical() + .id_salt(scroll_id) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(128.0) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + let input_id = scroll_id.with("edit"); + let proof_input_before = self.input_edit.clone(); + let resp = egui::TextEdit::multiline(&mut self.input_edit) + .id(input_id) + .font(egui::TextStyle::Small) + .desired_rows(5) + .interactive(!wallet.payment_proof_verifying()) + .desired_width(f32::INFINITY) + .show(ui) + .response; + if View::is_desktop() { + resp.request_focus(); + } + // Parse payment proof on input change. + if self.input_edit != proof_input_before { + self.on_proof_edit_change(wallet); + } + ui.add_space(6.0); + }); + }); - ui.add_space(2.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); - if wallet.payment_proof_verifying() { - ui.vertical_centered(|ui| { - View::small_loading_spinner(ui); - }); - } else { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + if wallet.payment_proof_verifying() { + ui.vertical_centered(|ui| { + View::small_loading_spinner(ui); + }); + } else { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - if self.parse_error || (self.validation_result.is_some() && - self.validation_result.as_ref().unwrap().is_err()) { - // Draw button to clear message input. - let clear_text = format!("{} {}", BROOM, t!("clear")); - View::button(ui, clear_text, Colors::white_or_black(false), || { - self.input_edit = "".to_string(); - self.parse_error = false; - self.validation_result = None; - }); - } else { - // Draw button to paste proof text. - let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); - View::button(ui, paste_text, Colors::white_or_black(false), || { - self.input_edit = cb.get_string_from_buffer(); - self.on_proof_edit_change(wallet); - }); - } - }); - columns[1].vertical_centered_justified(|ui| { - let mut changed = false; - self.pick_button.ui(ui, cb, |data| { - self.input_edit = data.clone(); - changed = true; - }); - if changed { - self.on_proof_edit_change(wallet); - } - }); - }); - } - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + if self.parse_error + || (self.validation_result.is_some() + && self.validation_result.as_ref().unwrap().is_err()) + { + // Draw button to clear message input. + let clear_text = format!("{} {}", BROOM, t!("clear")); + View::button(ui, clear_text, Colors::white_or_black(false), || { + self.input_edit = "".to_string(); + self.parse_error = false; + self.validation_result = None; + }); + } else { + // Draw button to paste proof text. + let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); + View::button(ui, paste_text, Colors::white_or_black(false), || { + self.input_edit = cb.get_string_from_buffer(); + self.on_proof_edit_change(wallet); + }); + } + }); + columns[1].vertical_centered_justified(|ui| { + let mut changed = false; + self.pick_button.ui(ui, cb, |data| { + self.input_edit = data.clone(); + changed = true; + }); + if changed { + self.on_proof_edit_change(wallet); + } + }); + }); + } + } - /// Callback on payment proof input change. - fn on_proof_edit_change(&mut self, wallet: &Wallet) { - if wallet.payment_proof_verifying() { - return; - } - if self.input_edit.is_empty() { - self.parse_error = false; - return; - } - if let Ok(p) = serde_json::from_str::(self.input_edit.as_str()) { - wallet.task(WalletTask::VerifyProof(p, None)); - } else { - self.parse_error = true; - } - } + /// Callback on payment proof input change. + fn on_proof_edit_change(&mut self, wallet: &Wallet) { + if wallet.payment_proof_verifying() { + return; + } + if self.input_edit.is_empty() { + self.parse_error = false; + return; + } + if let Ok(p) = serde_json::from_str::(self.input_edit.as_str()) { + wallet.task(WalletTask::VerifyProof(p, None)); + } else { + self.parse_error = true; + } + } - /// Draw transaction payment proof content to share. - pub fn share_ui(&mut self, - ui: &mut egui::Ui, - tx: &WalletTx, - cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - let (desc_text, color) = if tx.data.tx_type == TxLogEntryType::TxReceived { - (t!("wallets.payment_proof_valid").into(), Colors::green()) - } else { - (format!("{}:", t!("wallets.payment_proof")), Colors::inactive_text()) - }; - let desc = format!("{} {}", SEAL_CHECK, desc_text); - ui.label(RichText::new(desc).size(16.0).color(color)); - }); - ui.add_space(6.0); - ui.vertical_centered(|ui| { - let scroll_id = Id::from("tx_info_payment_proof_share"); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(3.0); - ScrollArea::vertical() - .id_salt(scroll_id) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .max_height(128.0) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(7.0); - let input_id = scroll_id.with("edit"); - egui::TextEdit::multiline(&mut self.input_edit) - .id(input_id) - .font(egui::TextStyle::Small) - .desired_rows(5) - .interactive(false) - .desired_width(f32::INFINITY) - .show(ui); - ui.add_space(6.0); - }); - }); + /// Draw transaction payment proof content to share. + pub fn share_ui(&mut self, ui: &mut egui::Ui, tx: &WalletTx, cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let (desc_text, color) = if tx.data.tx_type == TxLogEntryType::TxReceived { + (t!("wallets.payment_proof_valid").into(), Colors::green()) + } else { + ( + format!("{}:", t!("wallets.payment_proof")), + Colors::inactive_text(), + ) + }; + let desc = format!("{} {}", SEAL_CHECK, desc_text); + ui.label(RichText::new(desc).size(16.0).color(color)); + }); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let scroll_id = Id::from("tx_info_payment_proof_share"); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(3.0); + ScrollArea::vertical() + .id_salt(scroll_id) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(128.0) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + let input_id = scroll_id.with("edit"); + egui::TextEdit::multiline(&mut self.input_edit) + .id(input_id) + .font(egui::TextStyle::Small) + .desired_rows(5) + .interactive(false) + .desired_width(f32::INFINITY) + .show(ui); + ui.add_space(6.0); + }); + }); - ui.add_space(2.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - // Draw copy button. - let copy_text = format!("{} {}", COPY, t!("copy")); - View::button(ui, copy_text, Colors::white_or_black(false), || { - cb.copy_string_to_buffer(self.input_edit.clone()); - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - let share_text = format!("{} {}", FILE_TEXT, t!("share")); - View::colored_text_button(ui, - share_text, - Colors::blue(), - Colors::white_or_black(false), || { - let file_name = format!("{}.txt", tx.data.kernel_excess.unwrap().to_hex()); - let data = self.input_edit.as_bytes().to_vec(); - cb.share_data(file_name, data).unwrap_or_default(); - Modal::close(); - }); - }); - }); - } -} \ No newline at end of file + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + // Draw copy button. + let copy_text = format!("{} {}", COPY, t!("copy")); + View::button(ui, copy_text, Colors::white_or_black(false), || { + cb.copy_string_to_buffer(self.input_edit.clone()); + Modal::close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + let share_text = format!("{} {}", FILE_TEXT, t!("share")); + View::colored_text_button( + ui, + share_text, + Colors::blue(), + Colors::white_or_black(false), + || { + let file_name = format!("{}.txt", tx.data.kernel_excess.unwrap().to_hex()); + let data = self.input_edit.as_bytes().to_vec(); + cb.share_data(file_name, data).unwrap_or_default(); + Modal::close(); + }, + ); + }); + }); + } +} diff --git a/src/gui/views/wallets/wallet/request/invoice.rs b/src/gui/views/wallets/wallet/request/invoice.rs index e4c66d51..5a1199fe 100644 --- a/src/gui/views/wallets/wallet/request/invoice.rs +++ b/src/gui/views/wallets/wallet/request/invoice.rs @@ -15,114 +15,124 @@ use egui::{Id, RichText}; use grin_core::core::{amount_from_hr_string, amount_to_hr_string}; +use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, TextEdit, View}; -use crate::gui::Colors; -use crate::wallet::types::WalletTask; use crate::wallet::Wallet; +use crate::wallet::types::WalletTask; /// Invoice request creation content. pub struct InvoiceRequestContent { - /// Amount to receive. - amount_edit: String, + /// Amount to receive. + amount_edit: String, } impl Default for InvoiceRequestContent { - fn default() -> Self { - Self { - amount_edit: "".to_string(), - } - } + fn default() -> Self { + Self { + amount_edit: "".to_string(), + } + } } impl InvoiceRequestContent { - /// Draw [`Modal`] content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - // Setup callback on continue. - let on_continue = |m: &mut InvoiceRequestContent| { - if m.amount_edit.is_empty() { - return; - } - if let Ok(a) = amount_from_hr_string(m.amount_edit.as_str()) { - m.amount_edit = "".to_string(); - wallet.task(WalletTask::Receive(a)); - Modal::close(); - } - }; + /// Draw [`Modal`] content. + pub fn ui( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks, + ) { + // Setup callback on continue. + let on_continue = |m: &mut InvoiceRequestContent| { + if m.amount_edit.is_empty() { + return; + } + if let Ok(a) = amount_from_hr_string(m.amount_edit.as_str()) { + m.amount_edit = "".to_string(); + wallet.task(WalletTask::Receive(a)); + Modal::close(); + } + }; - ui.add_space(6.0); + ui.add_space(6.0); - // Draw amount input content. - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("wallets.enter_amount_receive")) - .size(17.0) - .color(Colors::gray())); - }); - ui.add_space(8.0); + // Draw amount input content. + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("wallets.enter_amount_receive")) + .size(17.0) + .color(Colors::gray()), + ); + }); + ui.add_space(8.0); - // Draw request amount text input. - let amount_edit_before = self.amount_edit.clone(); - let mut amount_edit = TextEdit::new(Id::from(modal.id).with(wallet.get_config().id)) - .h_center() - .numeric(); - amount_edit.ui(ui, &mut self.amount_edit, cb); - if amount_edit.enter_pressed { - on_continue(self); - } + // Draw request amount text input. + let amount_edit_before = self.amount_edit.clone(); + let mut amount_edit = TextEdit::new(Id::from(modal.id).with(wallet.get_config().id)) + .h_center() + .numeric(); + amount_edit.ui(ui, &mut self.amount_edit, cb); + if amount_edit.enter_pressed { + on_continue(self); + } - // Check value if input was changed. - if amount_edit_before != self.amount_edit { - if !self.amount_edit.is_empty() { - self.amount_edit = self.amount_edit.trim().replace(",", "."); - match amount_from_hr_string(self.amount_edit.as_str()) { - Ok(amount) => { - if !self.amount_edit.contains(".") { - // To avoid input of several `0` before `.` and put `.` after first `0`. - if self.amount_edit.len() != 1 && self.amount_edit.starts_with("0") { - let amount_text = amount_to_hr_string(amount, true); - let amount_parts = amount_text.split(".").collect::>(); - self.amount_edit = format!("0.{}", amount_parts[0]); - amount_edit.cursor_to_end(self.amount_edit.len(), ui); - } - } else { - // Check input after `.`. - let parts = self.amount_edit.split(".").collect::>(); - if parts.len() == 2 && (parts[1].len() > 9 || - (amount == 0 && parts[1].len() > 8)) { - self.amount_edit = amount_edit_before; - } - } - } - Err(_) => { - self.amount_edit = amount_edit_before; - } - } - } - } + // Check value if input was changed. + if amount_edit_before != self.amount_edit { + if !self.amount_edit.is_empty() { + self.amount_edit = self.amount_edit.trim().replace(",", "."); + match amount_from_hr_string(self.amount_edit.as_str()) { + Ok(amount) => { + if !self.amount_edit.contains(".") { + // To avoid input of several `0` before `.` and put `.` after first `0`. + if self.amount_edit.len() != 1 && self.amount_edit.starts_with("0") { + let amount_text = amount_to_hr_string(amount, true); + let amount_parts = amount_text.split(".").collect::>(); + self.amount_edit = format!("0.{}", amount_parts[0]); + amount_edit.cursor_to_end(self.amount_edit.len(), ui); + } + } else { + // Check input after `.`. + let parts = self.amount_edit.split(".").collect::>(); + if parts.len() == 2 + && (parts[1].len() > 9 || (amount == 0 && parts[1].len() > 8)) + { + self.amount_edit = amount_edit_before; + } + } + } + Err(_) => { + self.amount_edit = amount_edit_before; + } + } + } + } - ui.add_space(12.0); + ui.add_space(12.0); - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - self.amount_edit = "".to_string(); - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Button to create Slatepack message request. - View::button(ui, t!("continue"), Colors::white_or_black(false), || { - on_continue(self); - }); - }); - }); - ui.add_space(6.0); - } -} \ No newline at end of file + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + self.amount_edit = "".to_string(); + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + // Button to create Slatepack message request. + View::button(ui, t!("continue"), Colors::white_or_black(false), || { + on_continue(self); + }); + }); + }); + ui.add_space(6.0); + } +} diff --git a/src/gui/views/wallets/wallet/request/mod.rs b/src/gui/views/wallets/wallet/request/mod.rs index c97a1d5b..24914892 100644 --- a/src/gui/views/wallets/wallet/request/mod.rs +++ b/src/gui/views/wallets/wallet/request/mod.rs @@ -16,4 +16,4 @@ mod invoice; pub use invoice::*; mod send; -pub use send::*; \ No newline at end of file +pub use send::*; diff --git a/src/gui/views/wallets/wallet/request/send.rs b/src/gui/views/wallets/wallet/request/send.rs index 4dd9a28c..e5ca1f4e 100644 --- a/src/gui/views/wallets/wallet/request/send.rs +++ b/src/gui/views/wallets/wallet/request/send.rs @@ -17,314 +17,327 @@ use grin_core::core::{amount_from_hr_string, amount_to_hr_string}; use grin_core::global::get_accept_fee_base; use grin_wallet_libwallet::SlatepackAddress; +use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{CameraContent, Modal, TextEdit, View}; -use crate::gui::Colors; -use crate::wallet::types::WalletTask; use crate::wallet::Wallet; +use crate::wallet::types::WalletTask; /// Content to create a request to send funds. pub struct SendRequestContent { - /// Amount to send. - amount_edit: String, - /// Flag to check if maximum amount is calculating. - pub max_calculating: bool, + /// Amount to send. + amount_edit: String, + /// Flag to check if maximum amount is calculating. + pub max_calculating: bool, - /// Fee amount. - fee_edit: String, + /// Fee amount. + fee_edit: String, - /// Receiver address. - address_edit: String, - /// Flag to check if entered address is incorrect. - address_error: bool, + /// Receiver address. + address_edit: String, + /// Flag to check if entered address is incorrect. + address_error: bool, - /// Address QR code scanner content. - address_scan_content: Option, + /// Address QR code scanner content. + address_scan_content: Option, } impl SendRequestContent { - /// Create new content instance with optional receiver address. - pub fn new(addr: Option) -> Self { - Self { - amount_edit: "".to_string(), - max_calculating: false, - fee_edit: "".to_string(), - address_edit: addr.unwrap_or("".to_string()), - address_error: false, - address_scan_content: None, - } - } + /// Create new content instance with optional receiver address. + pub fn new(addr: Option) -> Self { + Self { + amount_edit: "".to_string(), + max_calculating: false, + fee_edit: "".to_string(), + address_edit: addr.unwrap_or("".to_string()), + address_error: false, + address_scan_content: None, + } + } - /// Setup fee amount. - pub fn on_fee_calculated(&mut self, fee: u64) { - self.fee_edit = amount_to_hr_string(fee, true); - } + /// Setup fee amount. + pub fn on_fee_calculated(&mut self, fee: u64) { + self.fee_edit = amount_to_hr_string(fee, true); + } - /// Setup maximum amount to send and fee. - pub fn on_max_amount_calculated(&mut self, amount: u64, fee: u64) { - self.max_calculating = false; - if amount == 0 { - self.amount_edit = "".to_string(); - self.fee_edit = "".to_string(); - } else { - self.amount_edit = amount_to_hr_string(amount, true); - self.fee_edit = amount_to_hr_string(fee, true); - } - } + /// Setup maximum amount to send and fee. + pub fn on_max_amount_calculated(&mut self, amount: u64, fee: u64) { + self.max_calculating = false; + if amount == 0 { + self.amount_edit = "".to_string(); + self.fee_edit = "".to_string(); + } else { + self.amount_edit = amount_to_hr_string(amount, true); + self.fee_edit = amount_to_hr_string(fee, true); + } + } - /// Draw [`Modal`] content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); + /// Draw [`Modal`] content. + pub fn ui( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks, + ) { + ui.add_space(6.0); - // Draw QR code scanner content if requested. - if let Some(scanner) = self.address_scan_content.as_mut() { - let on_stop = || { - cb.stop_camera(); - modal.enable_closing(); - }; + // Draw QR code scanner content if requested. + if let Some(scanner) = self.address_scan_content.as_mut() { + let on_stop = || { + cb.stop_camera(); + modal.enable_closing(); + }; - if let Some(result) = scanner.qr_scan_result() { - self.address_edit = result.text(); - on_stop(); - self.address_scan_content = None; - } else { - ui.add_space(6.0); - scanner.ui(ui, cb); - ui.add_space(6.0); + if let Some(result) = scanner.qr_scan_result() { + self.address_edit = result.text(); + on_stop(); + self.address_scan_content = None; + } else { + ui.add_space(6.0); + scanner.ui(ui, cb); + ui.add_space(6.0); - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - // Show buttons to close modal or come back to sending input. - ui.columns(2, |cols| { - cols[0].vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - on_stop(); - self.close(); - }); - }); - cols[1].vertical_centered_justified(|ui| { - View::button(ui, t!("back"), Colors::white_or_black(false), || { - on_stop(); - self.address_scan_content = None; - }); - }); - }); - ui.add_space(6.0); - } - return; - } + // Show buttons to close modal or come back to sending input. + ui.columns(2, |cols| { + cols[0].vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + on_stop(); + self.close(); + }); + }); + cols[1].vertical_centered_justified(|ui| { + View::button(ui, t!("back"), Colors::white_or_black(false), || { + on_stop(); + self.address_scan_content = None; + }); + }); + }); + ui.add_space(6.0); + } + return; + } - ui.vertical_centered(|ui| { - let data = wallet.get_data().unwrap(); - let amount = amount_to_hr_string(data.info.amount_currently_spendable, true); - let enter_text = t!("wallets.enter_amount_send","amount" => amount); - ui.label(RichText::new(enter_text) - .size(17.0) - .color(Colors::gray())); - }); - ui.add_space(8.0); + ui.vertical_centered(|ui| { + let data = wallet.get_data().unwrap(); + let amount = amount_to_hr_string(data.info.amount_currently_spendable, true); + let enter_text = t!("wallets.enter_amount_send","amount" => amount); + ui.label(RichText::new(enter_text).size(17.0).color(Colors::gray())); + }); + ui.add_space(8.0); - // Draw amount text edit. - let amount_edit_id = Id::from(modal.id).with("amount").with(wallet.get_config().id); - let mut amount_edit = TextEdit::new(amount_edit_id) - .h_center() - .numeric() - .focus(Modal::first_draw()); - if self.max_calculating { - amount_edit = amount_edit.disable(); - } - let amount_edit_before = self.amount_edit.clone(); + // Draw amount text edit. + let amount_edit_id = Id::from(modal.id) + .with("amount") + .with(wallet.get_config().id); + let mut amount_edit = TextEdit::new(amount_edit_id) + .h_center() + .numeric() + .focus(Modal::first_draw()); + if self.max_calculating { + amount_edit = amount_edit.disable(); + } + let amount_edit_before = self.amount_edit.clone(); - // Draw button to calculate maximum amount to send. - let mut calculate_max = false; - amount_edit.custom_buttons_ui(ui, &mut self.amount_edit, cb, |ui| { - if self.max_calculating { - ui.add_space(12.0); - View::loading_spinner(ui, 40.0); - ui.add_space(12.0); - } else { - View::button(ui, t!("max_short"), Colors::white_or_black(false), || { - calculate_max = true; - }); - ui.add_space(8.0); - } - }); - if calculate_max { - self.max_calculating = true; - let max = wallet.get_data().unwrap().info.amount_currently_spendable; - self.amount_edit = amount_to_hr_string(max, true); - } - ui.add_space(8.0); + // Draw button to calculate maximum amount to send. + let mut calculate_max = false; + amount_edit.custom_buttons_ui(ui, &mut self.amount_edit, cb, |ui| { + if self.max_calculating { + ui.add_space(12.0); + View::loading_spinner(ui, 40.0); + ui.add_space(12.0); + } else { + View::button(ui, t!("max_short"), Colors::white_or_black(false), || { + calculate_max = true; + }); + ui.add_space(8.0); + } + }); + if calculate_max { + self.max_calculating = true; + let max = wallet.get_data().unwrap().info.amount_currently_spendable; + self.amount_edit = amount_to_hr_string(max, true); + } + ui.add_space(8.0); - // Check value if input was changed. - if amount_edit_before != self.amount_edit { - if !self.amount_edit.is_empty() { - // Trim text, replace `,` by `.` and parse amount. - self.amount_edit = self.amount_edit.trim().replace(",", "."); - match amount_from_hr_string(self.amount_edit.as_str()) { - Ok(mut amount) => { - if !self.amount_edit.contains(".") { - // To avoid input of several `0` before `.` and put `.` after first `0`. - if self.amount_edit.len() != 1 && self.amount_edit.starts_with("0") { - let amount_text = amount_to_hr_string(amount, true); - let amount_parts = amount_text.split(".").collect::>(); - self.amount_edit = format!("0.{}", amount_parts[0]); - amount = amount_from_hr_string(self.amount_edit.as_str()) - .unwrap_or_else(|_| amount); - amount_edit.cursor_to_end(self.amount_edit.len(), ui); - } - // Reset fee amount on `0`. - if amount == 0 { - self.fee_edit = "".to_string(); - } - } else { - // Check input after `.`. - let parts = self.amount_edit.split(".").collect::>(); - if parts.len() == 2 && (parts[1].len() > 9 || - (amount == 0 && parts[1].len() > 8)) { - self.amount_edit = amount_edit_before.clone(); - } - } - // Do not input amount more than balance. - if amount != 0 && self.amount_edit != amount_edit_before { - let fee = amount_from_hr_string(self.fee_edit.as_str()).unwrap_or(0); - let max = wallet.get_data().unwrap().info.amount_currently_spendable; - if amount > max || amount + fee > max { - self.max_calculating = true; - wallet.task(WalletTask::CalculateFee(max, 0)); - } else { - wallet.task(WalletTask::CalculateFee(amount, 0)); - } - } - } - Err(_) => { - self.amount_edit = amount_edit_before; - } - } - } else { - self.fee_edit = "".to_string(); - } - } + // Check value if input was changed. + if amount_edit_before != self.amount_edit { + if !self.amount_edit.is_empty() { + // Trim text, replace `,` by `.` and parse amount. + self.amount_edit = self.amount_edit.trim().replace(",", "."); + match amount_from_hr_string(self.amount_edit.as_str()) { + Ok(mut amount) => { + if !self.amount_edit.contains(".") { + // To avoid input of several `0` before `.` and put `.` after first `0`. + if self.amount_edit.len() != 1 && self.amount_edit.starts_with("0") { + let amount_text = amount_to_hr_string(amount, true); + let amount_parts = amount_text.split(".").collect::>(); + self.amount_edit = format!("0.{}", amount_parts[0]); + amount = amount_from_hr_string(self.amount_edit.as_str()) + .unwrap_or_else(|_| amount); + amount_edit.cursor_to_end(self.amount_edit.len(), ui); + } + // Reset fee amount on `0`. + if amount == 0 { + self.fee_edit = "".to_string(); + } + } else { + // Check input after `.`. + let parts = self.amount_edit.split(".").collect::>(); + if parts.len() == 2 + && (parts[1].len() > 9 || (amount == 0 && parts[1].len() > 8)) + { + self.amount_edit = amount_edit_before.clone(); + } + } + // Do not input amount more than balance. + if amount != 0 && self.amount_edit != amount_edit_before { + let fee = amount_from_hr_string(self.fee_edit.as_str()).unwrap_or(0); + let max = wallet.get_data().unwrap().info.amount_currently_spendable; + if amount > max || amount + fee > max { + self.max_calculating = true; + wallet.task(WalletTask::CalculateFee(max, 0)); + } else { + wallet.task(WalletTask::CalculateFee(amount, 0)); + } + } + } + Err(_) => { + self.amount_edit = amount_edit_before; + } + } + } else { + self.fee_edit = "".to_string(); + } + } - // Show fee value. - ui.vertical_centered(|ui| { - let fee_label = t!( - "wallets.fee_base_desc", - "value" => format!(": {}", get_accept_fee_base()) - ); - ui.label(RichText::new(fee_label) - .size(17.0) - .color(Colors::gray())); - }); - ui.add_space(6.0); - let fee_edit_id = Id::from(modal.id).with("_fee").with(wallet.get_config().id); - let mut fee_edit = TextEdit::new(fee_edit_id) - .focus(false) - .h_center() - .disable(); - let mut loading_label = format!("{}...", t!("wallets.loading")); - fee_edit.ui(ui, if wallet.fee_calculating() { - &mut loading_label - } else { - &mut self.fee_edit - }, cb); + // Show fee value. + ui.vertical_centered(|ui| { + let fee_label = t!( + "wallets.fee_base_desc", + "value" => format!(": {}", get_accept_fee_base()) + ); + ui.label(RichText::new(fee_label).size(17.0).color(Colors::gray())); + }); + ui.add_space(6.0); + let fee_edit_id = Id::from(modal.id).with("_fee").with(wallet.get_config().id); + let mut fee_edit = TextEdit::new(fee_edit_id).focus(false).h_center().disable(); + let mut loading_label = format!("{}...", t!("wallets.loading")); + fee_edit.ui( + ui, + if wallet.fee_calculating() { + &mut loading_label + } else { + &mut self.fee_edit + }, + cb, + ); - ui.add_space(8.0); + ui.add_space(8.0); - // Show address error or input description. - ui.vertical_centered(|ui| { - if self.address_error { - ui.label(RichText::new(t!("transport.incorrect_addr_err")) - .size(17.0) - .color(Colors::red())); - } else { - ui.label(RichText::new(t!("transport.receiver_address")) - .size(17.0) - .color(Colors::gray())); - } - }); - ui.add_space(6.0); + // Show address error or input description. + ui.vertical_centered(|ui| { + if self.address_error { + ui.label( + RichText::new(t!("transport.incorrect_addr_err")) + .size(17.0) + .color(Colors::red()), + ); + } else { + ui.label( + RichText::new(t!("transport.receiver_address")) + .size(17.0) + .color(Colors::gray()), + ); + } + }); + ui.add_space(6.0); - // Show address text edit. - let addr_edit_before = self.address_edit.clone(); - let address_edit_id = Id::from(modal.id).with("_address").with(wallet.get_config().id); - let mut address_edit = TextEdit::new(address_edit_id) - .paste() - .focus(false) - .scan_qr(); - if amount_edit.enter_pressed { - address_edit.focus_request(); - } - address_edit.ui(ui, &mut self.address_edit, cb); - // Check if scan button was pressed. - if address_edit.scan_pressed { - modal.disable_closing(); - self.address_scan_content = Some(CameraContent::default()); - } - ui.add_space(12.0); - // Check value if input was changed. - if addr_edit_before != self.address_edit { - self.address_error = false; - } - // Continue on Enter press. - if address_edit.enter_pressed { - self.on_continue(wallet); - } + // Show address text edit. + let addr_edit_before = self.address_edit.clone(); + let address_edit_id = Id::from(modal.id) + .with("_address") + .with(wallet.get_config().id); + let mut address_edit = TextEdit::new(address_edit_id) + .paste() + .focus(false) + .scan_qr(); + if amount_edit.enter_pressed { + address_edit.focus_request(); + } + address_edit.ui(ui, &mut self.address_edit, cb); + // Check if scan button was pressed. + if address_edit.scan_pressed { + modal.disable_closing(); + self.address_scan_content = Some(CameraContent::default()); + } + ui.add_space(12.0); + // Check value if input was changed. + if addr_edit_before != self.address_edit { + self.address_error = false; + } + // Continue on Enter press. + if address_edit.enter_pressed { + self.on_continue(wallet); + } - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - self.close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Button to create Slatepack message request. - if self.max_calculating || wallet.fee_calculating() { - ui.add_space(4.0); - View::small_loading_spinner(ui); - } else { - View::button(ui, t!("continue"), Colors::white_or_black(false), || { - self.on_continue(wallet); - }); - } - }); - }); - ui.add_space(6.0); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + self.close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + // Button to create Slatepack message request. + if self.max_calculating || wallet.fee_calculating() { + ui.add_space(4.0); + View::small_loading_spinner(ui); + } else { + View::button(ui, t!("continue"), Colors::white_or_black(false), || { + self.on_continue(wallet); + }); + } + }); + }); + ui.add_space(6.0); + } - /// Callback when Continue button was pressed. - fn on_continue(&mut self, wallet: &Wallet) { - if self.amount_edit.is_empty() { - return; - } - // Check address to send over Tor if enabled. - let addr_str = self.address_edit.as_str(); - if let Ok(r) = SlatepackAddress::try_from(addr_str.trim()) { - if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) { - wallet.task(WalletTask::Send(a, Some(r))); - Modal::close(); - } - } else if !addr_str.is_empty() { - self.address_error = true; - } else if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) { - wallet.task(WalletTask::Send(a, None)); - Modal::close(); - } - } + /// Callback when Continue button was pressed. + fn on_continue(&mut self, wallet: &Wallet) { + if self.amount_edit.is_empty() { + return; + } + // Check address to send over Tor if enabled. + let addr_str = self.address_edit.as_str(); + if let Ok(r) = SlatepackAddress::try_from(addr_str.trim()) { + if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) { + wallet.task(WalletTask::Send(a, Some(r))); + Modal::close(); + } + } else if !addr_str.is_empty() { + self.address_error = true; + } else if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) { + wallet.task(WalletTask::Send(a, None)); + Modal::close(); + } + } - /// Close modal and clear data. - fn close(&mut self) { - self.amount_edit = "".to_string(); - self.address_edit = "".to_string(); - self.address_scan_content = None; - Modal::close(); - } -} \ No newline at end of file + /// Close modal and clear data. + fn close(&mut self) { + self.amount_edit = "".to_string(); + self.address_edit = "".to_string(); + self.address_scan_content = None; + Modal::close(); + } +} diff --git a/src/gui/views/wallets/wallet/settings/common.rs b/src/gui/views/wallets/wallet/settings/common.rs index fcf44c1e..a2ae1aff 100644 --- a/src/gui/views/wallets/wallet/settings/common.rs +++ b/src/gui/views/wallets/wallet/settings/common.rs @@ -16,33 +16,33 @@ use eframe::emath::Align; use eframe::epaint::{RectShape, StrokeKind}; use egui::{CursorIcon, Id, Layout, RichText, Sense, UiBuilder}; -use crate::gui::icons::{CLOCK_COUNTDOWN, FOLDERS, FOLDER_USER, PASSWORD}; +use crate::gui::Colors; +use crate::gui::icons::{CLOCK_COUNTDOWN, FOLDER_USER, FOLDERS, PASSWORD}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::ModalPosition; use crate::gui::views::wallets::wallet::types::WalletContentContainer; use crate::gui::views::{FilePickContent, FilePickContentType, Modal, TextEdit, View}; -use crate::gui::Colors; use crate::wallet::Wallet; /// Common wallet settings content. pub struct CommonSettings { - /// Wallet name [`Modal`] value. - name_edit: String, + /// Wallet name [`Modal`] value. + name_edit: String, - /// Flag to check if wrong password was entered. - wrong_pass: bool, - /// Current wallet password [`Modal`] value. - old_pass_edit: String, - /// New wallet password [`Modal`] value. - new_pass_edit: String, + /// Flag to check if wrong password was entered. + wrong_pass: bool, + /// Current wallet password [`Modal`] value. + old_pass_edit: String, + /// New wallet password [`Modal`] value. + new_pass_edit: String, - /// Data path value value for [`Modal`]. - data_path_edit: String, - /// Button to pick directory for wallet data. - pick_data_dir: FilePickContent, + /// Data path value value for [`Modal`]. + data_path_edit: String, + /// Button to pick directory for wallet data. + pick_data_dir: FilePickContent, - /// Minimum confirmations number value. - min_confirmations_edit: String, + /// Minimum confirmations number value. + min_confirmations_edit: String, } /// Identifier for wallet name [`Modal`]. @@ -55,454 +55,527 @@ const DATA_PATH_MODAL: &'static str = "wallet_data_path"; const MIN_CONFIRMATIONS_EDIT_MODAL: &'static str = "wallet_min_conf_edit_modal"; impl WalletContentContainer for CommonSettings { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - NAME_EDIT_MODAL, - PASS_EDIT_MODAL, - DATA_PATH_MODAL, - MIN_CONFIRMATIONS_EDIT_MODAL - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![ + NAME_EDIT_MODAL, + PASS_EDIT_MODAL, + DATA_PATH_MODAL, + MIN_CONFIRMATIONS_EDIT_MODAL, + ] + } - fn modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - match modal.id { - NAME_EDIT_MODAL => self.name_modal_ui(ui, wallet, modal, cb), - PASS_EDIT_MODAL => self.pass_modal_ui(ui, wallet, modal, cb), - DATA_PATH_MODAL => self.data_path_modal_ui(ui, wallet, cb), - MIN_CONFIRMATIONS_EDIT_MODAL => self.min_conf_modal_ui(ui, wallet, modal, cb), - _ => {} - } - } + fn modal_ui( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks, + ) { + match modal.id { + NAME_EDIT_MODAL => self.name_modal_ui(ui, wallet, modal, cb), + PASS_EDIT_MODAL => self.pass_modal_ui(ui, wallet, modal, cb), + DATA_PATH_MODAL => self.data_path_modal_ui(ui, wallet, cb), + MIN_CONFIRMATIONS_EDIT_MODAL => self.min_conf_modal_ui(ui, wallet, modal, cb), + _ => {} + } + } - fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - if View::is_desktop() { - ui.add_space(1.0); - } else { - ui.add_space(8.0); - } - ui.vertical_centered(|ui| { - let config = wallet.get_config(); - // Show wallet name. - self.name_ui(ui, config.name); - // Show data dir for desktop. - if View::is_desktop() { - ui.add_space(-4.0); - self.data_dir_ui(ui, wallet, cb); - } - ui.add_space(6.0); - ui.label(RichText::new(t!("wallets.min_tx_conf_count")).size(16.0).color(Colors::gray())); - ui.add_space(6.0); + fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { + if View::is_desktop() { + ui.add_space(1.0); + } else { + ui.add_space(8.0); + } + ui.vertical_centered(|ui| { + let config = wallet.get_config(); + // Show wallet name. + self.name_ui(ui, config.name); + // Show data dir for desktop. + if View::is_desktop() { + ui.add_space(-4.0); + self.data_dir_ui(ui, wallet, cb); + } + ui.add_space(6.0); + ui.label( + RichText::new(t!("wallets.min_tx_conf_count")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - // Show minimum amount of confirmations value setup. - let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, config.min_confirmations); - View::button(ui, min_conf_text, Colors::white_or_black(false), || { - self.min_confirmations_edit = config.min_confirmations.to_string(); - // Show minimum amount of confirmations value modal. - Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")) - .show(); - }); + // Show minimum amount of confirmations value setup. + let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, config.min_confirmations); + View::button(ui, min_conf_text, Colors::white_or_black(false), || { + self.min_confirmations_edit = config.min_confirmations.to_string(); + // Show minimum amount of confirmations value modal. + Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + }); - ui.add_space(8.0); + ui.add_space(8.0); - // Ability to post wallet transactions with Dandelion. - View::checkbox(ui, wallet.can_use_dandelion(), t!("wallets.use_dandelion"), || { - wallet.update_use_dandelion(!wallet.can_use_dandelion()); - }); + // Ability to post wallet transactions with Dandelion. + View::checkbox( + ui, + wallet.can_use_dandelion(), + t!("wallets.use_dandelion"), + || { + wallet.update_use_dandelion(!wallet.can_use_dandelion()); + }, + ); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(4.0); - }); - } + ui.add_space(6.0); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(4.0); + }); + } } impl Default for CommonSettings { - fn default() -> Self { - Self { - name_edit: "".to_string(), - wrong_pass: false, - old_pass_edit: "".to_string(), - new_pass_edit: "".to_string(), - data_path_edit: "".to_string(), - pick_data_dir: FilePickContent::new( - FilePickContentType::ItemButton(View::item_rounding(1, 2, true)) - ).no_parse().pick_folder(), - min_confirmations_edit: "".to_string(), - } - } + fn default() -> Self { + Self { + name_edit: "".to_string(), + wrong_pass: false, + old_pass_edit: "".to_string(), + new_pass_edit: "".to_string(), + data_path_edit: "".to_string(), + pick_data_dir: FilePickContent::new(FilePickContentType::ItemButton( + View::item_rounding(1, 2, true), + )) + .no_parse() + .pick_folder(), + min_confirmations_edit: "".to_string(), + } + } } impl CommonSettings { - /// Draw content to change wallet name and password. - fn name_ui(&mut self, ui: &mut egui::Ui, name: String) { - // Draw round background. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(56.0); - let r = if View::is_desktop() { - View::item_rounding(0, 2, false) - } else { - View::item_rounding(0, 1, false) - }; - let bg = Colors::fill_lite(); - let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); - let bg_idx = ui.painter().add(bg_shape.clone()); + /// Draw content to change wallet name and password. + fn name_ui(&mut self, ui: &mut egui::Ui, name: String) { + // Draw round background. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(56.0); + let r = if View::is_desktop() { + View::item_rounding(0, 2, false) + } else { + View::item_rounding(0, 1, false) + }; + let bg = Colors::fill_lite(); + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - let res = ui.scope_builder( - UiBuilder::new() - .sense(Sense::click()) - .layout(Layout::right_to_left(Align::Center)) - .max_rect(rect), |ui| { - let r = if View::is_desktop() { - View::item_rounding(0, 2, true) - } else { - View::item_rounding(0, 1, true) - }; - View::item_button(ui, r, PASSWORD, None, || { - self.old_pass_edit = "".to_string(); - self.new_pass_edit = "".to_string(); - self.wrong_pass = false; - // Show wallet password modal. - Modal::new(PASS_EDIT_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.wallet")) - .show(); - }); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(12.0); - ui.vertical(|ui| { - ui.add_space(4.0); - View::ellipsize_text(ui, name.clone(), 18.0, Colors::title(false)); - ui.add_space(1.0); - let desc = format!("{} {}", FOLDER_USER, t!("wallets.name").replace(":", "")); - ui.label(RichText::new(desc).size(15.0).color(Colors::gray())); - ui.add_space(8.0); - }); - }); - } - ).response; - let clicked = res.clicked() || res.long_touched(); - // Setup background and cursor. - if res.hovered() { - res.on_hover_cursor(CursorIcon::PointingHand); - bg_shape.fill = Colors::fill(); - } - ui.painter().set(bg_idx, bg_shape); - // Handle clicks on layout. - if clicked { - self.name_edit = name; - // Show wallet name modal. - Modal::new(NAME_EDIT_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.wallet")) - .show(); - } - } + let res = ui + .scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), + |ui| { + let r = if View::is_desktop() { + View::item_rounding(0, 2, true) + } else { + View::item_rounding(0, 1, true) + }; + View::item_button(ui, r, PASSWORD, None, || { + self.old_pass_edit = "".to_string(); + self.new_pass_edit = "".to_string(); + self.wrong_pass = false; + // Show wallet password modal. + Modal::new(PASS_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.wallet")) + .show(); + }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout( + layout_size, + Layout::left_to_right(Align::Center), + |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + View::ellipsize_text(ui, name.clone(), 18.0, Colors::title(false)); + ui.add_space(1.0); + let desc = format!( + "{} {}", + FOLDER_USER, + t!("wallets.name").replace(":", "") + ); + ui.label(RichText::new(desc).size(15.0).color(Colors::gray())); + ui.add_space(8.0); + }); + }, + ); + }, + ) + .response; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked { + self.name_edit = name; + // Show wallet name modal. + Modal::new(NAME_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.wallet")) + .show(); + } + } - /// Draw wallet name [`Modal`] content. - fn name_modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut CommonSettings| { - if !c.name_edit.is_empty() { - wallet.change_name(c.name_edit.clone()); - Modal::close(); - } - }; + /// Draw wallet name [`Modal`] content. + fn name_modal_ui( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks, + ) { + let on_save = |c: &mut CommonSettings| { + if !c.name_edit.is_empty() { + wallet.change_name(c.name_edit.clone()); + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("wallets.name")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); - // Show wallet name text edit. - let mut name_edit = TextEdit::new(Id::from(modal.id).with(wallet.get_config().id)); - name_edit.ui(ui, &mut self.name_edit, cb); - if name_edit.enter_pressed { - on_save(self); - } - ui.add_space(12.0); - }); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("wallets.name")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); + // Show wallet name text edit. + let mut name_edit = TextEdit::new(Id::from(modal.id).with(wallet.get_config().id)); + name_edit.ui(ui, &mut self.name_edit, cb); + if name_edit.enter_pressed { + on_save(self); + } + ui.add_space(12.0); + }); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + } - /// Draw wallet pass [`Modal`] content. - fn pass_modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - let wallet_id = wallet.get_config().id; - let on_continue = |c: &mut CommonSettings| { - if c.new_pass_edit.is_empty() { - return; - } - let old_pass = c.old_pass_edit.clone(); - let new_pass = c.new_pass_edit.clone(); - match wallet.change_password(old_pass, new_pass) { - Ok(_) => { - // Clear password values. - c.old_pass_edit = "".to_string(); - c.new_pass_edit = "".to_string(); - // Close modal. - Modal::close(); - } - Err(_) => c.wrong_pass = true - } - }; + /// Draw wallet pass [`Modal`] content. + fn pass_modal_ui( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks, + ) { + let wallet_id = wallet.get_config().id; + let on_continue = |c: &mut CommonSettings| { + if c.new_pass_edit.is_empty() { + return; + } + let old_pass = c.old_pass_edit.clone(); + let new_pass = c.new_pass_edit.clone(); + match wallet.change_password(old_pass, new_pass) { + Ok(_) => { + // Clear password values. + c.old_pass_edit = "".to_string(); + c.new_pass_edit = "".to_string(); + // Close modal. + Modal::close(); + } + Err(_) => c.wrong_pass = true, + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("wallets.current_pass")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("wallets.current_pass")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw old password text edit. - let pass_edit_id = Id::from(modal.id).with(wallet_id).with("old_pass"); - let mut pass_edit = TextEdit::new(pass_edit_id) - .password() - .focus(Modal::first_draw()); - pass_edit.ui(ui, &mut self.old_pass_edit, cb); - ui.add_space(8.0); + // Draw old password text edit. + let pass_edit_id = Id::from(modal.id).with(wallet_id).with("old_pass"); + let mut pass_edit = TextEdit::new(pass_edit_id) + .password() + .focus(Modal::first_draw()); + pass_edit.ui(ui, &mut self.old_pass_edit, cb); + ui.add_space(8.0); - ui.label(RichText::new(t!("wallets.new_pass")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.label( + RichText::new(t!("wallets.new_pass")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw new password text edit. - let new_pass_edit_id = Id::from(modal.id).with(wallet_id).with("new_pass"); - let mut new_pass_edit = TextEdit::new(new_pass_edit_id) - .password() - .focus(false); - if pass_edit.enter_pressed { - new_pass_edit.focus_request(); - } - new_pass_edit.ui(ui, &mut self.new_pass_edit, cb); - if new_pass_edit.enter_pressed { - on_continue(self); - } + // Draw new password text edit. + let new_pass_edit_id = Id::from(modal.id).with(wallet_id).with("new_pass"); + let mut new_pass_edit = TextEdit::new(new_pass_edit_id).password().focus(false); + if pass_edit.enter_pressed { + new_pass_edit.focus_request(); + } + new_pass_edit.ui(ui, &mut self.new_pass_edit, cb); + if new_pass_edit.enter_pressed { + on_continue(self); + } - // Show information when password is empty. - if self.old_pass_edit.is_empty() || self.new_pass_edit.is_empty() { - ui.add_space(10.0); - ui.label(RichText::new(t!("wallets.pass_empty")) - .size(17.0) - .color(Colors::inactive_text())); - } else if self.wrong_pass { - ui.add_space(10.0); - ui.label(RichText::new(t!("wallets.wrong_pass")) - .size(17.0) - .color(Colors::red())); - } - ui.add_space(12.0); - }); + // Show information when password is empty. + if self.old_pass_edit.is_empty() || self.new_pass_edit.is_empty() { + ui.add_space(10.0); + ui.label( + RichText::new(t!("wallets.pass_empty")) + .size(17.0) + .color(Colors::inactive_text()), + ); + } else if self.wrong_pass { + ui.add_space(10.0); + ui.label( + RichText::new(t!("wallets.wrong_pass")) + .size(17.0) + .color(Colors::red()), + ); + } + ui.add_space(12.0); + }); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("change"), Colors::white_or_black(false), || { - on_continue(self); - }); - }); - }); - ui.add_space(6.0); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("change"), Colors::white_or_black(false), || { + on_continue(self); + }); + }); + }); + ui.add_space(6.0); + }); + } - /// Draw content to change wallet data directory. - fn data_dir_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - // Draw round background. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(56.0); - let r = View::item_rounding(1, 2, false); - let bg = Colors::fill_lite(); - let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); - let bg_idx = ui.painter().add(bg_shape.clone()); + /// Draw content to change wallet data directory. + fn data_dir_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { + // Draw round background. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(56.0); + let r = View::item_rounding(1, 2, false); + let bg = Colors::fill_lite(); + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - let res = ui.scope_builder( - UiBuilder::new() - .sense(Sense::click()) - .layout(Layout::right_to_left(Align::Center)) - .max_rect(rect), |ui| { - self.pick_data_dir.ui(ui, cb, |path| { - wallet.change_data_path(path); - }); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(12.0); - ui.vertical(|ui| { - ui.add_space(4.0); - let path = wallet.get_config().data_path.unwrap_or_default(); - View::ellipsize_text(ui, path, 18.0, Colors::title(false)); - ui.add_space(1.0); - let desc = format!("{} {}", FOLDERS, t!("files_location")); - ui.label(RichText::new(desc).size(15.0).color(Colors::gray())); - ui.add_space(8.0); - }); - }); - } - ).response; - let clicked = res.clicked() || res.long_touched(); - // Setup background and cursor. - if res.hovered() { - res.on_hover_cursor(CursorIcon::PointingHand); - bg_shape.fill = Colors::fill(); - } - ui.painter().set(bg_idx, bg_shape); - // Handle clicks on layout. - if clicked { - self.data_path_edit = wallet.get_config().data_path.unwrap_or_default(); - // Show chain data path edit modal. - Modal::new(DATA_PATH_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.wallet")) - .show(); - } - } + let res = ui + .scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), + |ui| { + self.pick_data_dir.ui(ui, cb, |path| { + wallet.change_data_path(path); + }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout( + layout_size, + Layout::left_to_right(Align::Center), + |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + let path = wallet.get_config().data_path.unwrap_or_default(); + View::ellipsize_text(ui, path, 18.0, Colors::title(false)); + ui.add_space(1.0); + let desc = format!("{} {}", FOLDERS, t!("files_location")); + ui.label(RichText::new(desc).size(15.0).color(Colors::gray())); + ui.add_space(8.0); + }); + }, + ); + }, + ) + .response; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked { + self.data_path_edit = wallet.get_config().data_path.unwrap_or_default(); + // Show chain data path edit modal. + Modal::new(DATA_PATH_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.wallet")) + .show(); + } + } - /// Draw data path input [`Modal`] content. - fn data_path_modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - let on_save = |path: &String| { - wallet.change_data_path(path.clone()); - Modal::close(); - }; - ui.label(RichText::new(format!("{}:", t!("files_location"))) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + /// Draw data path input [`Modal`] content. + fn data_path_modal_ui( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + cb: &dyn PlatformCallbacks, + ) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let on_save = |path: &String| { + wallet.change_data_path(path.clone()); + Modal::close(); + }; + ui.label( + RichText::new(format!("{}:", t!("files_location"))) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw chain data path text edit. - let mut edit = TextEdit::new(Id::from(DATA_PATH_MODAL)).paste(); - edit.ui(ui, &mut self.data_path_edit, cb); - if edit.enter_pressed { - on_save(&self.data_path_edit); - } - ui.add_space(12.0); + // Draw chain data path text edit. + let mut edit = TextEdit::new(Id::from(DATA_PATH_MODAL)).paste(); + edit.ui(ui, &mut self.data_path_edit, cb); + if edit.enter_pressed { + on_save(&self.data_path_edit); + } + ui.add_space(12.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(&self.data_path_edit); - }); - }); - }); - ui.add_space(6.0); - }); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(&self.data_path_edit); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } - /// Draw wallet name [`Modal`] content. - fn min_conf_modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut CommonSettings| { - if let Ok(min_conf) = c.min_confirmations_edit.parse::() { - wallet.update_min_confirmations(min_conf); - Modal::close(); - } - }; + /// Draw wallet name [`Modal`] content. + fn min_conf_modal_ui( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks, + ) { + let on_save = |c: &mut CommonSettings| { + if let Ok(min_conf) = c.min_confirmations_edit.parse::() { + wallet.update_min_confirmations(min_conf); + Modal::close(); + } + }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("wallets.min_tx_conf_count")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("wallets.min_tx_conf_count")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Minimum amount of confirmations text edit. - let mut min_confirmations_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); - min_confirmations_edit.ui(ui, &mut self.min_confirmations_edit, cb); - if min_confirmations_edit.enter_pressed { - on_save(self); - } + // Minimum amount of confirmations text edit. + let mut min_confirmations_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric(); + min_confirmations_edit.ui(ui, &mut self.min_confirmations_edit, cb); + if min_confirmations_edit.enter_pressed { + on_save(self); + } - // Show error when specified value is not valid or reminder to restart enabled node. - if self.min_confirmations_edit.parse::().is_err() { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.not_valid_value")) - .size(17.0) - .color(Colors::red())); - } - ui.add_space(12.0); - }); + // Show error when specified value is not valid or reminder to restart enabled node. + if self.min_confirmations_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::red()), + ); + } + ui.add_space(12.0); + }); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - } -} \ No newline at end of file + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + // Close modal. + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + } +} diff --git a/src/gui/views/wallets/wallet/settings/connection.rs b/src/gui/views/wallets/wallet/settings/connection.rs index 6beaba21..a80c20f6 100644 --- a/src/gui/views/wallets/wallet/settings/connection.rs +++ b/src/gui/views/wallets/wallet/settings/connection.rs @@ -14,151 +14,163 @@ use egui::RichText; +use crate::gui::Colors; use crate::gui::icons::{GLOBE, PLUS_CIRCLE}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::network::modals::ExternalConnectionModal; use crate::gui::views::network::ConnectionsContent; +use crate::gui::views::network::modals::ExternalConnectionModal; use crate::gui::views::types::{ContentContainer, ModalPosition}; use crate::gui::views::{Modal, View}; -use crate::gui::Colors; use crate::node::Node; use crate::wallet::types::ConnectionMethod; use crate::wallet::{ConnectionsConfig, ExternalConnection}; /// Wallet connection settings content. pub struct ConnectionSettings { - /// Selected connection method. - pub method: ConnectionMethod, + /// Selected connection method. + pub method: ConnectionMethod, - /// External connection [`Modal`] content. - ext_conn_modal: ExternalConnectionModal, + /// External connection [`Modal`] content. + ext_conn_modal: ExternalConnectionModal, } impl Default for ConnectionSettings { - fn default() -> Self { - let method = { - let ext_conn_list = ConnectionsConfig::ext_conn_list(); - if Node::is_running() || Node::is_starting() || ext_conn_list.is_empty() { - ConnectionMethod::Integrated - } else { - let ext_conn = ext_conn_list.get(0).unwrap(); - ConnectionMethod::External(ext_conn.id, ext_conn.url.clone()) - } - }; - Self { - method, - ext_conn_modal: ExternalConnectionModal::new(None), - } - } + fn default() -> Self { + let method = { + let ext_conn_list = ConnectionsConfig::ext_conn_list(); + if Node::is_running() || Node::is_starting() || ext_conn_list.is_empty() { + ConnectionMethod::Integrated + } else { + let ext_conn = ext_conn_list.get(0).unwrap(); + ConnectionMethod::External(ext_conn.id, ext_conn.url.clone()) + } + }; + Self { + method, + ext_conn_modal: ExternalConnectionModal::new(None), + } + } } impl ContentContainer for ConnectionSettings { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - ExternalConnectionModal::WALLET_ID - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![ExternalConnectionModal::WALLET_ID] + } - fn modal_ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - match modal.id { - ExternalConnectionModal::WALLET_ID => { - self.ext_conn_modal.ui(ui, cb, modal, |_| {}); - }, - _ => {} - } - } + fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + match modal.id { + ExternalConnectionModal::WALLET_ID => { + self.ext_conn_modal.ui(ui, cb, modal, |_| {}); + } + _ => {} + } + } - fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { - ui.add_space(2.0); - View::sub_title(ui, format!("{} {}", GLOBE, t!("wallets.conn_method"))); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(6.0); + fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { + ui.add_space(2.0); + View::sub_title(ui, format!("{} {}", GLOBE, t!("wallets.conn_method"))); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.add_space(6.0); - // Show integrated node selection. - let cur_integrated = self.method == ConnectionMethod::Integrated; - let bg = if cur_integrated { - Colors::fill_deep() - } else { - Colors::fill_lite() - }; - ConnectionsContent::integrated_node_item_ui(ui, bg, (!cur_integrated, || { - self.method = ConnectionMethod::Integrated; - }), |ui| { - if cur_integrated { - View::selected_item_check(ui); - } - cur_integrated - }); + ui.vertical_centered(|ui| { + ui.add_space(6.0); + // Show integrated node selection. + let cur_integrated = self.method == ConnectionMethod::Integrated; + let bg = if cur_integrated { + Colors::fill_deep() + } else { + Colors::fill_lite() + }; + ConnectionsContent::integrated_node_item_ui( + ui, + bg, + (!cur_integrated, || { + self.method = ConnectionMethod::Integrated; + }), + |ui| { + if cur_integrated { + View::selected_item_check(ui); + } + cur_integrated + }, + ); - ui.add_space(8.0); - ui.label(RichText::new(t!("wallets.ext_conn")).size(16.0).color(Colors::gray())); - ui.add_space(6.0); + ui.add_space(8.0); + ui.label( + RichText::new(t!("wallets.ext_conn")) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - // Show button to add new external node connection. - let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node")); - View::button(ui, add_node_text, Colors::white_or_black(false), || { - self.ext_conn_modal = ExternalConnectionModal::new(None); - Modal::new(ExternalConnectionModal::WALLET_ID) - .position(ModalPosition::CenterTop) - .title(t!("wallets.add_node")) - .show(); - }); - ui.add_space(4.0); + // Show button to add new external node connection. + let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node")); + View::button(ui, add_node_text, Colors::white_or_black(false), || { + self.ext_conn_modal = ExternalConnectionModal::new(None); + Modal::new(ExternalConnectionModal::WALLET_ID) + .position(ModalPosition::CenterTop) + .title(t!("wallets.add_node")) + .show(); + }); + ui.add_space(4.0); - // Check for removed active connection. - let cur_method = &self.method.clone(); - let mut ext_conn_list = ConnectionsConfig::ext_conn_list(); - let has_method = !ext_conn_list.iter().filter(|c| { - match cur_method { - ConnectionMethod::Integrated => true, - ConnectionMethod::External(id, url) => id == &c.id || url == &c.url - } - }).collect::>().is_empty(); - if !has_method { - match cur_method { - ConnectionMethod::External(id, url) => { - ext_conn_list.push(ExternalConnection { - id: *id, - url: url.clone(), - username: Some("grin".to_string()), - secret: None, - available: Some(true), - }) - } - _ => {} - } - } + // Check for removed active connection. + let cur_method = &self.method.clone(); + let mut ext_conn_list = ConnectionsConfig::ext_conn_list(); + let has_method = !ext_conn_list + .iter() + .filter(|c| match cur_method { + ConnectionMethod::Integrated => true, + ConnectionMethod::External(id, url) => id == &c.id || url == &c.url, + }) + .collect::>() + .is_empty(); + if !has_method { + match cur_method { + ConnectionMethod::External(id, url) => ext_conn_list.push(ExternalConnection { + id: *id, + url: url.clone(), + username: Some("grin".to_string()), + secret: None, + available: Some(true), + }), + _ => {} + } + } - let len = ext_conn_list.len(); - if len != 0 { - ui.add_space(8.0); - for (i, c) in ext_conn_list.iter().enumerate() { - ui.horizontal_wrapped(|ui| { - // Draw external connection item. - let is_current = match cur_method { - ConnectionMethod::External(id, url) => id == &c.id || url == &c.url, - _ => false - }; - let bg = if is_current { - Colors::fill() - } else { - Colors::fill_lite() - }; - ConnectionsContent::ext_conn_item_ui(ui, bg, c, i, len, (!is_current, || { - self.method = ConnectionMethod::External(c.id, c.url.clone()); - }), |ui| { - if is_current { - View::selected_item_check(ui); - } - }); - }); - } - } - }); - } -} \ No newline at end of file + let len = ext_conn_list.len(); + if len != 0 { + ui.add_space(8.0); + for (i, c) in ext_conn_list.iter().enumerate() { + ui.horizontal_wrapped(|ui| { + // Draw external connection item. + let is_current = match cur_method { + ConnectionMethod::External(id, url) => id == &c.id || url == &c.url, + _ => false, + }; + let bg = if is_current { + Colors::fill() + } else { + Colors::fill_lite() + }; + ConnectionsContent::ext_conn_item_ui( + ui, + bg, + c, + i, + len, + (!is_current, || { + self.method = ConnectionMethod::External(c.id, c.url.clone()); + }), + |ui| { + if is_current { + View::selected_item_check(ui); + } + }, + ); + }); + } + } + }); + } +} diff --git a/src/gui/views/wallets/wallet/settings/content.rs b/src/gui/views/wallets/wallet/settings/content.rs index 113aa598..29cc0ce4 100644 --- a/src/gui/views/wallets/wallet/settings/content.rs +++ b/src/gui/views/wallets/wallet/settings/content.rs @@ -14,52 +14,49 @@ use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::ContentContainer; -use crate::gui::views::wallets::{CommonSettings, ConnectionSettings, RecoverySettings}; use crate::gui::views::wallets::wallet::types::WalletContentContainer; +use crate::gui::views::wallets::{CommonSettings, ConnectionSettings, RecoverySettings}; use crate::wallet::Wallet; /// Wallet settings tab content. pub struct WalletSettingsContent { - /// Common setup content. - common_setup: CommonSettings, - /// Connection setup content. - conn_setup: ConnectionSettings, - /// Recovery setup content. - recovery_setup: RecoverySettings + /// Common setup content. + common_setup: CommonSettings, + /// Connection setup content. + conn_setup: ConnectionSettings, + /// Recovery setup content. + recovery_setup: RecoverySettings, } impl Default for WalletSettingsContent { - fn default() -> Self { - Self { - common_setup: CommonSettings::default(), - conn_setup: ConnectionSettings::default(), - recovery_setup: RecoverySettings::default() - } - } + fn default() -> Self { + Self { + common_setup: CommonSettings::default(), + conn_setup: ConnectionSettings::default(), + recovery_setup: RecoverySettings::default(), + } + } } impl WalletSettingsContent { - pub fn ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - cb: &dyn PlatformCallbacks) { - // Show common wallet setup. - self.common_setup.ui(ui, wallet, cb); + pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { + // Show common wallet setup. + self.common_setup.ui(ui, wallet, cb); - // Show wallet connections setup. - self.conn_setup.method = wallet.get_current_connection(); - let method = self.conn_setup.method.clone(); - self.conn_setup.ui(ui, cb); - if method != self.conn_setup.method { - wallet.update_connection(&self.conn_setup.method); - // Reopen wallet if connection changed. - if !wallet.reopen_needed() { - wallet.set_reopen(true); - wallet.close(); - } - } + // Show wallet connections setup. + self.conn_setup.method = wallet.get_current_connection(); + let method = self.conn_setup.method.clone(); + self.conn_setup.ui(ui, cb); + if method != self.conn_setup.method { + wallet.update_connection(&self.conn_setup.method); + // Reopen wallet if connection changed. + if !wallet.reopen_needed() { + wallet.set_reopen(true); + wallet.close(); + } + } - // Show wallet recovery setup. - self.recovery_setup.ui(ui, wallet, cb); - } -} \ No newline at end of file + // Show wallet recovery setup. + self.recovery_setup.ui(ui, wallet, cb); + } +} diff --git a/src/gui/views/wallets/wallet/settings/mod.rs b/src/gui/views/wallets/wallet/settings/mod.rs index a976b6d7..433323a2 100644 --- a/src/gui/views/wallets/wallet/settings/mod.rs +++ b/src/gui/views/wallets/wallet/settings/mod.rs @@ -22,4 +22,4 @@ mod common; pub use common::CommonSettings; mod recovery; -pub use recovery::RecoverySettings; \ No newline at end of file +pub use recovery::RecoverySettings; diff --git a/src/gui/views/wallets/wallet/settings/recovery.rs b/src/gui/views/wallets/wallet/settings/recovery.rs index 5ec1d7af..9a229212 100644 --- a/src/gui/views/wallets/wallet/settings/recovery.rs +++ b/src/gui/views/wallets/wallet/settings/recovery.rs @@ -16,25 +16,25 @@ use egui::{Id, RichText}; use grin_chain::SyncStatus; use grin_util::ZeroingString; +use crate::gui::Colors; use crate::gui::icons::{EYE, KEY, LIFEBUOY, STETHOSCOPE, TRASH}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::ModalPosition; use crate::gui::views::wallets::wallet::types::WalletContentContainer; use crate::gui::views::{Modal, TextEdit, View}; -use crate::gui::Colors; use crate::node::Node; -use crate::wallet::types::ConnectionMethod; use crate::wallet::Wallet; +use crate::wallet::types::ConnectionMethod; /// Wallet recovery settings content. pub struct RecoverySettings { - /// Wallet password [`Modal`] value. - pass_edit: String, - /// Flag to check if wrong password was entered. - wrong_pass: bool, + /// Wallet password [`Modal`] value. + pass_edit: String, + /// Flag to check if wrong password was entered. + wrong_pass: bool, - /// Recovery phrase value. - recovery_phrase: Option, + /// Recovery phrase value. + recovery_phrase: Option, } /// Identifier for recovery phrase [`Modal`]. @@ -43,251 +43,290 @@ const RECOVERY_PHRASE_MODAL: &'static str = "recovery_phrase_modal"; const DELETE_CONFIRMATION_MODAL: &'static str = "delete_wallet_confirmation_modal"; impl WalletContentContainer for RecoverySettings { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - RECOVERY_PHRASE_MODAL, - DELETE_CONFIRMATION_MODAL - ] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![RECOVERY_PHRASE_MODAL, DELETE_CONFIRMATION_MODAL] + } - fn modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - modal: &Modal, cb: &dyn PlatformCallbacks) { - match modal.id { - RECOVERY_PHRASE_MODAL => { - self.recovery_phrase_modal_ui(ui, wallet, modal, cb); - } - DELETE_CONFIRMATION_MODAL => { - Self::deletion_modal_ui(ui, wallet); - } - _ => {} - } - } + fn modal_ui( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks, + ) { + match modal.id { + RECOVERY_PHRASE_MODAL => { + self.recovery_phrase_modal_ui(ui, wallet, modal, cb); + } + DELETE_CONFIRMATION_MODAL => { + Self::deletion_modal_ui(ui, wallet); + } + _ => {} + } + } - fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, _: &dyn PlatformCallbacks) { - ui.add_space(10.0); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(6.0); - View::sub_title(ui, format!("{} {}", KEY, t!("wallets.recovery"))); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(4.0); + fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, _: &dyn PlatformCallbacks) { + ui.add_space(10.0); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(6.0); + View::sub_title(ui, format!("{} {}", KEY, t!("wallets.recovery"))); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(4.0); - ui.vertical_centered(|ui| { - let integrated_node = wallet.get_current_connection() == ConnectionMethod::Integrated; - let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync); - if wallet.sync_error() || (integrated_node && !integrated_node_ready) { - ui.add_space(2.0); - ui.label(RichText::new(t!("wallets.repair_unavailable")) - .size(16.0) - .color(Colors::red())); - ui.add_space(2.0); - } else if !wallet.is_repairing() { - ui.add_space(6.0); + ui.vertical_centered(|ui| { + let integrated_node = wallet.get_current_connection() == ConnectionMethod::Integrated; + let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync); + if wallet.sync_error() || (integrated_node && !integrated_node_ready) { + ui.add_space(2.0); + ui.label( + RichText::new(t!("wallets.repair_unavailable")) + .size(16.0) + .color(Colors::red()), + ); + ui.add_space(2.0); + } else if !wallet.is_repairing() { + ui.add_space(6.0); - // Draw button to repair the wallet. - let repair_text = format!("{} {}", STETHOSCOPE, t!("wallets.repair_wallet")); - View::action_button(ui, repair_text, || { - wallet.repair(); - }); + // Draw button to repair the wallet. + let repair_text = format!("{} {}", STETHOSCOPE, t!("wallets.repair_wallet")); + View::action_button(ui, repair_text, || { + wallet.repair(); + }); - ui.add_space(6.0); - ui.label(RichText::new(t!("wallets.repair_desc")) - .size(16.0) - .color(Colors::inactive_text())); - } + ui.add_space(6.0); + ui.label( + RichText::new(t!("wallets.repair_desc")) + .size(16.0) + .color(Colors::inactive_text()), + ); + } - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - // Draw button to restore the wallet. - ui.add_space(4.0); - View::colored_text_button(ui, - format!("{} {}", LIFEBUOY, t!("wallets.recover")), - Colors::green(), - Colors::white_or_black(false), || { - wallet.delete_db(); - }); - ui.add_space(6.0); - ui.label(RichText::new(t!("wallets.restore_wallet_desc")) - .size(16.0) - .color(Colors::inactive_text())); + // Draw button to restore the wallet. + ui.add_space(4.0); + View::colored_text_button( + ui, + format!("{} {}", LIFEBUOY, t!("wallets.recover")), + Colors::green(), + Colors::white_or_black(false), + || { + wallet.delete_db(); + }, + ); + ui.add_space(6.0); + ui.label( + RichText::new(t!("wallets.restore_wallet_desc")) + .size(16.0) + .color(Colors::inactive_text()), + ); - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); - let recovery_text = format!("{}:", t!("wallets.recovery_phrase")); - ui.label(RichText::new(recovery_text).size(16.0).color(Colors::gray())); - ui.add_space(6.0); + let recovery_text = format!("{}:", t!("wallets.recovery_phrase")); + ui.label( + RichText::new(recovery_text) + .size(16.0) + .color(Colors::gray()), + ); + ui.add_space(6.0); - // Draw button to show recovery phrase. - let show_text = format!("{} {}", EYE, t!("show")); - View::button(ui, show_text, Colors::white_or_black(false), || { - self.show_recovery_phrase_modal(); - }); + // Draw button to show recovery phrase. + let show_text = format!("{} {}", EYE, t!("show")); + View::button(ui, show_text, Colors::white_or_black(false), || { + self.show_recovery_phrase_modal(); + }); - ui.add_space(12.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); - ui.label(RichText::new(t!("wallets.delete_desc")).size(16.0).color(Colors::red())); - ui.add_space(6.0); + ui.add_space(12.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); + ui.label( + RichText::new(t!("wallets.delete_desc")) + .size(16.0) + .color(Colors::red()), + ); + ui.add_space(6.0); - // Draw button to delete the wallet. - View::colored_text_button(ui, - format!("{} {}", TRASH, t!("wallets.delete")), - Colors::red(), - Colors::white_or_black(false), || { - Modal::new(DELETE_CONFIRMATION_MODAL) - .position(ModalPosition::Center) - .title(t!("confirmation")) - .show(); - }); - ui.add_space(8.0); - }); - } + // Draw button to delete the wallet. + View::colored_text_button( + ui, + format!("{} {}", TRASH, t!("wallets.delete")), + Colors::red(), + Colors::white_or_black(false), + || { + Modal::new(DELETE_CONFIRMATION_MODAL) + .position(ModalPosition::Center) + .title(t!("confirmation")) + .show(); + }, + ); + ui.add_space(8.0); + }); + } } impl Default for RecoverySettings { - fn default() -> Self { - Self { - wrong_pass: false, - pass_edit: "".to_string(), - recovery_phrase: None, - } - } + fn default() -> Self { + Self { + wrong_pass: false, + pass_edit: "".to_string(), + recovery_phrase: None, + } + } } impl RecoverySettings { - /// Show recovery phrase [`Modal`]. - fn show_recovery_phrase_modal(&mut self) { - // Setup modal values. - self.pass_edit = "".to_string(); - self.wrong_pass = false; - self.recovery_phrase = None; - // Show recovery phrase modal. - Modal::new(RECOVERY_PHRASE_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.recovery_phrase")) - .show(); - } + /// Show recovery phrase [`Modal`]. + fn show_recovery_phrase_modal(&mut self) { + // Setup modal values. + self.pass_edit = "".to_string(); + self.wrong_pass = false; + self.recovery_phrase = None; + // Show recovery phrase modal. + Modal::new(RECOVERY_PHRASE_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.recovery_phrase")) + .show(); + } - /// Draw recovery phrase [`Modal`] content. - fn recovery_phrase_modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - let on_next = |c: &mut RecoverySettings| { - match wallet.get_recovery(c.pass_edit.clone()) { - Ok(phrase) => { - c.wrong_pass = false; - c.recovery_phrase = Some(phrase); - } - Err(_) => { - c.wrong_pass = true; - } - } - }; + /// Draw recovery phrase [`Modal`] content. + fn recovery_phrase_modal_ui( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks, + ) { + let on_next = |c: &mut RecoverySettings| match wallet.get_recovery(c.pass_edit.clone()) { + Ok(phrase) => { + c.wrong_pass = false; + c.recovery_phrase = Some(phrase); + } + Err(_) => { + c.wrong_pass = true; + } + }; - ui.add_space(6.0); - if self.recovery_phrase.is_some() { - ui.vertical_centered(|ui| { - ui.label(RichText::new(self.recovery_phrase.clone().unwrap().to_string()) - .size(17.0) - .color(Colors::white_or_black(true))); - }); - ui.add_space(10.0); - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.recovery_phrase = None; - Modal::close(); - }); - }); - } else { - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("wallets.pass")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); + ui.add_space(6.0); + if self.recovery_phrase.is_some() { + ui.vertical_centered(|ui| { + ui.label( + RichText::new(self.recovery_phrase.clone().unwrap().to_string()) + .size(17.0) + .color(Colors::white_or_black(true)), + ); + }); + ui.add_space(10.0); + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + self.recovery_phrase = None; + Modal::close(); + }); + }); + } else { + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("wallets.pass")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); - // Draw current wallet password text edit. - let pass_edit_id = Id::from(modal.id).with(wallet.get_config().id); - let mut pass_edit = TextEdit::new(pass_edit_id) - .password(); - pass_edit.ui(ui, &mut self.pass_edit, cb); - if pass_edit.enter_pressed { - on_next(self); - } + // Draw current wallet password text edit. + let pass_edit_id = Id::from(modal.id).with(wallet.get_config().id); + let mut pass_edit = TextEdit::new(pass_edit_id).password(); + pass_edit.ui(ui, &mut self.pass_edit, cb); + if pass_edit.enter_pressed { + on_next(self); + } - // Show information when password is empty or wrong. - if self.pass_edit.is_empty() { - ui.add_space(12.0); - ui.label(RichText::new(t!("wallets.pass_empty")) - .size(17.0) - .color(Colors::inactive_text())); - } else if self.wrong_pass { - ui.add_space(12.0); - ui.label(RichText::new(t!("wallets.wrong_pass")) - .size(17.0) - .color(Colors::red())); - } - }); - ui.add_space(12.0); + // Show information when password is empty or wrong. + if self.pass_edit.is_empty() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("wallets.pass_empty")) + .size(17.0) + .color(Colors::inactive_text()), + ); + } else if self.wrong_pass { + ui.add_space(12.0); + ui.label( + RichText::new(t!("wallets.wrong_pass")) + .size(17.0) + .color(Colors::red()), + ); + } + }); + ui.add_space(12.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - self.recovery_phrase = None; - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || { - on_next(self); - }); - }); - }); - }); - } - ui.add_space(6.0); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + self.recovery_phrase = None; + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || { + on_next(self); + }); + }); + }); + }); + } + ui.add_space(6.0); + } - /// Draw wallet deletion [`Modal`] content. - pub fn deletion_modal_ui(ui: &mut egui::Ui, wallet: &Wallet) { - ui.add_space(8.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("wallets.delete_conf")) - .size(17.0) - .color(Colors::text(false))); - }); - ui.add_space(12.0); + /// Draw wallet deletion [`Modal`] content. + pub fn deletion_modal_ui(ui: &mut egui::Ui, wallet: &Wallet) { + ui.add_space(8.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("wallets.delete_conf")) + .size(17.0) + .color(Colors::text(false)), + ); + }); + ui.add_space(12.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("delete"), Colors::white_or_black(false), || { - wallet.delete_wallet(); - Modal::close(); - }); - }); - }); - ui.add_space(6.0); - }); - } -} \ No newline at end of file + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("delete"), Colors::white_or_black(false), || { + wallet.delete_wallet(); + Modal::close(); + }); + }); + }); + ui.add_space(6.0); + }); + } +} diff --git a/src/gui/views/wallets/wallet/transport/content.rs b/src/gui/views/wallets/wallet/transport/content.rs index fc2e8893..bfe188c1 100644 --- a/src/gui/views/wallets/wallet/transport/content.rs +++ b/src/gui/views/wallets/wallet/transport/content.rs @@ -15,210 +15,227 @@ use egui::{Align, CornerRadius, Layout, RichText, StrokeKind}; use crate::AppConfig; -use crate::gui::icons::{CIRCLE_HALF, DOTS_THREE_CIRCLE, PLUGS, PLUGS_CONNECTED, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, WRENCH}; +use crate::gui::Colors; +use crate::gui::icons::{ + CIRCLE_HALF, DOTS_THREE_CIRCLE, PLUGS, PLUGS_CONNECTED, POWER, QR_CODE, SHIELD_CHECKERED, + SHIELD_SLASH, WARNING_CIRCLE, WRENCH, +}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::wallets::wallet::transport::settings::WalletTransportSettingsContent; use crate::gui::views::wallets::wallet::types::WalletContentContainer; use crate::gui::views::{Modal, QrCodeContent, View}; -use crate::gui::Colors; use crate::tor::{Tor, TorConfig}; use crate::wallet::Wallet; /// Wallet transport panel content. pub struct WalletTransportContent { - /// QR code address content. - pub qr_address_content: Option, + /// QR code address content. + pub qr_address_content: Option, - /// Settings content. - pub settings_content: Option, + /// Settings content. + pub settings_content: Option, } impl WalletContentContainer for WalletTransportContent { - fn modal_ids(&self) -> Vec<&'static str> { vec![] } + fn modal_ids(&self) -> Vec<&'static str> { + vec![] + } - fn modal_ui(&mut self, _: &mut egui::Ui, _: &Wallet, _: &Modal, _: &dyn PlatformCallbacks) { - } + fn modal_ui(&mut self, _: &mut egui::Ui, _: &Wallet, _: &Modal, _: &dyn PlatformCallbacks) {} - fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - if let Some(content) = self.qr_address_content.as_mut() { - let dark_theme = AppConfig::dark_theme().unwrap_or(false); - // Set light theme for better scanning. - AppConfig::set_dark_theme(false); - crate::setup_visuals(ui.ctx()); - // Draw QR code content. - ui.add_space(6.0); - content.ui(ui, cb); - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.qr_address_content = None; - }); - }); - ui.add_space(6.0); - // Set color theme back. - AppConfig::set_dark_theme(dark_theme); - crate::setup_visuals(ui.ctx()); - } else if let Some(content) = self.settings_content.as_mut() { - let mut closed = false; - content.ui(ui, wallet, cb, || { - closed = true; - }); - if closed { - self.settings_content = None; - } - } else { - self.tor_header_ui(ui, wallet); - } - } + fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { + if let Some(content) = self.qr_address_content.as_mut() { + let dark_theme = AppConfig::dark_theme().unwrap_or(false); + // Set light theme for better scanning. + AppConfig::set_dark_theme(false); + crate::setup_visuals(ui.ctx()); + // Draw QR code content. + ui.add_space(6.0); + content.ui(ui, cb); + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + self.qr_address_content = None; + }); + }); + ui.add_space(6.0); + // Set color theme back. + AppConfig::set_dark_theme(dark_theme); + crate::setup_visuals(ui.ctx()); + } else if let Some(content) = self.settings_content.as_mut() { + let mut closed = false; + content.ui(ui, wallet, cb, || { + closed = true; + }); + if closed { + self.settings_content = None; + } + } else { + self.tor_header_ui(ui, wallet); + } + } } impl Default for WalletTransportContent { - fn default() -> Self { - Self { - qr_address_content: None, - settings_content: None, - } - } + fn default() -> Self { + Self { + qr_address_content: None, + settings_content: None, + } + } } impl WalletTransportContent { - /// Check if it's possible to go back at navigation stack. - pub fn can_back(&self) -> bool { - self.settings_content.is_some() || self.qr_address_content.is_some() - } + /// Check if it's possible to go back at navigation stack. + pub fn can_back(&self) -> bool { + self.settings_content.is_some() || self.qr_address_content.is_some() + } - /// Navigate back on navigation stack. - pub fn back(&mut self) { - if let Some(content) = self.settings_content.as_ref() { - if content.tor_settings_content.settings_changed { - Tor::restart(); - } - self.settings_content = None; - } else if self.qr_address_content.is_some() { - self.qr_address_content = None; - } - } + /// Navigate back on navigation stack. + pub fn back(&mut self) { + if let Some(content) = self.settings_content.as_ref() { + if content.tor_settings_content.settings_changed { + Tor::restart(); + } + self.settings_content = None; + } else if self.qr_address_content.is_some() { + self.qr_address_content = None; + } + } - /// Draw Tor transport header content. - fn tor_header_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { - let wallet_data = wallet.get_data(); - if wallet_data.is_none() { - return; - } - let addr = wallet.slatepack_address().unwrap(); + /// Draw Tor transport header content. + fn tor_header_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { + let wallet_data = wallet.get_data(); + if wallet_data.is_none() { + return; + } + let addr = wallet.slatepack_address().unwrap(); - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(78.0); + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(78.0); - // Draw round background. - let info = wallet.get_data().unwrap().info; - let awaiting_balance = info.amount_awaiting_confirmation > 0 || - info.amount_awaiting_finalization > 0 || info.amount_locked > 0; - let rounding = if awaiting_balance { - View::item_rounding(1, 3, false) - } else { - View::item_rounding(1, 2, false) - }; - ui.painter().rect(rect, - rounding, - Colors::fill(), - View::item_stroke(), - StrokeKind::Outside); + // Draw round background. + let info = wallet.get_data().unwrap().info; + let awaiting_balance = info.amount_awaiting_confirmation > 0 + || info.amount_awaiting_finalization > 0 + || info.amount_locked > 0; + let rounding = if awaiting_balance { + View::item_rounding(1, 3, false) + } else { + View::item_rounding(1, 2, false) + }; + ui.painter().rect( + rect, + rounding, + Colors::fill(), + View::item_stroke(), + StrokeKind::Outside, + ); - ui.vertical(|ui| { - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - // Show button to show QR code address. - let r = if awaiting_balance { - View::item_rounding(1, 3, true) - } else { - View::item_rounding(1, 2, true) - }; - View::item_button(ui, r, QR_CODE, None, || { - self.qr_address_content = Some(QrCodeContent::new(addr.clone(), false) - .with_max_size(320.0)); - }); + ui.vertical(|ui| { + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + // Show button to show QR code address. + let r = if awaiting_balance { + View::item_rounding(1, 3, true) + } else { + View::item_rounding(1, 2, true) + }; + View::item_button(ui, r, QR_CODE, None, || { + self.qr_address_content = + Some(QrCodeContent::new(addr.clone(), false).with_max_size(320.0)); + }); - let service_id = &wallet.identifier(); - // Draw button to enable/disable Tor listener for current wallet. - if wallet.foreign_api_port().is_some() && wallet.secret_key().is_some() { - let port = wallet.foreign_api_port().unwrap(); - let key = wallet.secret_key().unwrap(); - if !Tor::is_service_starting(service_id) { - if !Tor::is_service_running(service_id) { - let r = CornerRadius::default(); - View::item_button(ui, r, POWER, Some(Colors::green()), || { - Tor::start_service(port, key.clone(), service_id); - }); - } else { - let r = CornerRadius::default(); - View::item_button(ui, r, POWER, Some(Colors::red()), || { - Tor::stop_service(service_id); - }); - } - } - } - // Draw button to show Tor transport settings. - let button_rounding = View::item_rounding(1, 3, true); - View::item_button(ui, button_rounding, WRENCH, None, || { - self.settings_content = Some(WalletTransportSettingsContent::default()); - }); + let service_id = &wallet.identifier(); + // Draw button to enable/disable Tor listener for current wallet. + if wallet.foreign_api_port().is_some() && wallet.secret_key().is_some() { + let port = wallet.foreign_api_port().unwrap(); + let key = wallet.secret_key().unwrap(); + if !Tor::is_service_starting(service_id) { + if !Tor::is_service_running(service_id) { + let r = CornerRadius::default(); + View::item_button(ui, r, POWER, Some(Colors::green()), || { + Tor::start_service(port, key.clone(), service_id); + }); + } else { + let r = CornerRadius::default(); + View::item_button(ui, r, POWER, Some(Colors::red()), || { + Tor::stop_service(service_id); + }); + } + } + } + // Draw button to show Tor transport settings. + let button_rounding = View::item_rounding(1, 3, true); + View::item_button(ui, button_rounding, WRENCH, None, || { + self.settings_content = Some(WalletTransportSettingsContent::default()); + }); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - ui.add_space(3.0); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout( + layout_size, + Layout::left_to_right(Align::Center), + |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(3.0); - let is_running = Tor::is_service_running(service_id); - let has_error = Tor::is_service_failed(service_id); - let is_starting = Tor::is_service_starting(service_id); - let address_color = if is_running && !is_starting { - Colors::green() - } else if has_error { - Colors::red() - } else { - Colors::inactive_text() - }; - // Show slatepack address text. - View::animate_text(ui, addr.clone(), 17.0, address_color, is_starting); - ui.add_space(1.0); + let is_running = Tor::is_service_running(service_id); + let has_error = Tor::is_service_failed(service_id); + let is_starting = Tor::is_service_starting(service_id); + let address_color = if is_running && !is_starting { + Colors::green() + } else if has_error { + Colors::red() + } else { + Colors::inactive_text() + }; + // Show slatepack address text. + View::animate_text(ui, addr.clone(), 17.0, address_color, is_starting); + ui.add_space(1.0); - let (icon, text) = if is_starting { - (DOTS_THREE_CIRCLE, t!("transport.connecting")) - } else if has_error { - (WARNING_CIRCLE, t!("transport.conn_error")) - } else if is_running { - (PLUGS_CONNECTED, t!("transport.connected")) - } else if let Some(_) = TorConfig::get_proxy() { - (PLUGS_CONNECTED, t!("app_settings.proxy")) - } else { - (PLUGS, t!("transport.disconnected")) - }; - let status_text = format!("{} {}", icon, text); - // Show connection status text. - View::ellipsize_text(ui, status_text, 15.0, Colors::text(false)); - ui.add_space(1.0); + let (icon, text) = if is_starting { + (DOTS_THREE_CIRCLE, t!("transport.connecting")) + } else if has_error { + (WARNING_CIRCLE, t!("transport.conn_error")) + } else if is_running { + (PLUGS_CONNECTED, t!("transport.connected")) + } else if let Some(_) = TorConfig::get_proxy() { + (PLUGS_CONNECTED, t!("app_settings.proxy")) + } else { + (PLUGS, t!("transport.disconnected")) + }; + let status_text = format!("{} {}", icon, text); + // Show connection status text. + View::ellipsize_text(ui, status_text, 15.0, Colors::text(false)); + ui.add_space(1.0); - let bridges_text = if is_starting || has_error { - match TorConfig::get_bridge() { - None => { - format!("{} {}", SHIELD_SLASH, t!("transport.bridges_disabled")) - } - Some(b) => { - let name = b.protocol_name().to_uppercase(); - format!("{} {}", - SHIELD_CHECKERED, - t!("transport.bridge_name", "b" = name)) - } - } - } else { - format!("{} {}", CIRCLE_HALF, t!("transport.tor_network")) - }; - // Show bridge info text. - ui.label(RichText::new(bridges_text).size(15.0).color(Colors::gray())); - }); - }); - }); - }); - } -} \ No newline at end of file + let bridges_text = if is_starting || has_error { + match TorConfig::get_bridge() { + None => { + format!( + "{} {}", + SHIELD_SLASH, + t!("transport.bridges_disabled") + ) + } + Some(b) => { + let name = b.protocol_name().to_uppercase(); + format!( + "{} {}", + SHIELD_CHECKERED, + t!("transport.bridge_name", "b" = name) + ) + } + } + } else { + format!("{} {}", CIRCLE_HALF, t!("transport.tor_network")) + }; + // Show bridge info text. + ui.label(RichText::new(bridges_text).size(15.0).color(Colors::gray())); + }); + }, + ); + }); + }); + } +} diff --git a/src/gui/views/wallets/wallet/transport/mod.rs b/src/gui/views/wallets/wallet/transport/mod.rs index e0f555cc..4669389c 100644 --- a/src/gui/views/wallets/wallet/transport/mod.rs +++ b/src/gui/views/wallets/wallet/transport/mod.rs @@ -15,4 +15,4 @@ mod content; pub use content::*; -mod settings; \ No newline at end of file +mod settings; diff --git a/src/gui/views/wallets/wallet/transport/settings.rs b/src/gui/views/wallets/wallet/transport/settings.rs index 30ed6eb1..abe9b003 100644 --- a/src/gui/views/wallets/wallet/transport/settings.rs +++ b/src/gui/views/wallets/wallet/transport/settings.rs @@ -14,60 +14,64 @@ use egui::RichText; +use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::View; use crate::gui::views::settings::TorSettingsContent; use crate::gui::views::types::ContentContainer; -use crate::gui::views::View; -use crate::gui::Colors; use crate::tor::Tor; use crate::wallet::Wallet; /// Wallet transport settings content. pub struct WalletTransportSettingsContent { - /// Tor transport content settings. - pub tor_settings_content: TorSettingsContent, + /// Tor transport content settings. + pub tor_settings_content: TorSettingsContent, } impl Default for WalletTransportSettingsContent { - fn default() -> Self { - Self { - tor_settings_content: TorSettingsContent::default() - } - } + fn default() -> Self { + Self { + tor_settings_content: TorSettingsContent::default(), + } + } } impl WalletTransportSettingsContent { - /// Draw transport settings content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - cb: &dyn PlatformCallbacks, - on_close: impl FnOnce()) { - ui.add_space(8.0); - ui.vertical_centered(|ui| { - // Show Tor settings. - self.tor_settings_content.ui(ui, cb); - ui.add_space(4.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); - ui.label(RichText::new(t!("transport.tor_autorun_desc")) - .size(17.0) - .color(Colors::inactive_text())); - // Show Tor service autorun checkbox. - let autorun = wallet.auto_start_tor_listener(); - View::checkbox(ui, autorun, t!("network.autorun"), || { - wallet.update_auto_start_tor_listener(!autorun); - }); - }); - ui.add_space(8.0); - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - if self.tor_settings_content.settings_changed { - Tor::restart(); - } - on_close(); - }); - }); - ui.add_space(6.0); - } -} \ No newline at end of file + /// Draw transport settings content. + pub fn ui( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + cb: &dyn PlatformCallbacks, + on_close: impl FnOnce(), + ) { + ui.add_space(8.0); + ui.vertical_centered(|ui| { + // Show Tor settings. + self.tor_settings_content.ui(ui, cb); + ui.add_space(4.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); + ui.label( + RichText::new(t!("transport.tor_autorun_desc")) + .size(17.0) + .color(Colors::inactive_text()), + ); + // Show Tor service autorun checkbox. + let autorun = wallet.auto_start_tor_listener(); + View::checkbox(ui, autorun, t!("network.autorun"), || { + wallet.update_auto_start_tor_listener(!autorun); + }); + }); + ui.add_space(8.0); + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + if self.tor_settings_content.settings_changed { + Tor::restart(); + } + on_close(); + }); + }); + ui.add_space(6.0); + } +} diff --git a/src/gui/views/wallets/wallet/txs/content.rs b/src/gui/views/wallets/wallet/txs/content.rs index b892b395..de3df8f5 100644 --- a/src/gui/views/wallets/wallet/txs/content.rs +++ b/src/gui/views/wallets/wallet/txs/content.rs @@ -14,35 +14,41 @@ use egui::epaint::RectShape; use egui::scroll_area::ScrollBarVisibility; -use egui::{Align, Color32, CornerRadius, CursorIcon, Id, Layout, Rect, RichText, ScrollArea, Sense, StrokeKind, UiBuilder}; +use egui::{ + Align, Color32, CornerRadius, CursorIcon, Id, Layout, Rect, RichText, ScrollArea, Sense, + StrokeKind, UiBuilder, +}; use grin_core::consensus::COINBASE_MATURITY; use grin_core::core::amount_to_hr_string; use grin_wallet_libwallet::TxLogEntryType; use std::ops::Range; use std::time::{SystemTime, UNIX_EPOCH}; -use crate::gui::icons::{ARROWS_CLOCKWISE, ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, CALENDAR_CHECK, DOTS_THREE_CIRCLE, FILE_ARROW_DOWN, FILE_TEXT, FILE_X, GEAR_FINE, PROHIBIT, WARNING, X_CIRCLE}; +use crate::gui::Colors; +use crate::gui::icons::{ + ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, ARROWS_CLOCKWISE, CALENDAR_CHECK, DOTS_THREE_CIRCLE, + FILE_ARROW_DOWN, FILE_TEXT, FILE_X, GEAR_FINE, PROHIBIT, WARNING, X_CIRCLE, +}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::{LinePosition, ModalPosition}; -use crate::gui::views::wallets::wallet::types::{WalletContentContainer, GRIN}; use crate::gui::views::wallets::wallet::WalletTransactionContent; +use crate::gui::views::wallets::wallet::types::{GRIN, WalletContentContainer}; use crate::gui::views::{Content, Modal, PullToRefresh, View}; -use crate::gui::Colors; -use crate::wallet::types::{WalletData, WalletTask, WalletTx, WalletTxAction}; use crate::wallet::Wallet; +use crate::wallet::types::{WalletData, WalletTask, WalletTx, WalletTxAction}; /// Wallet transactions tab content. pub struct WalletTransactionsContent { - /// Transaction information [`Modal`] content. - pub tx_info_content: Option, + /// Transaction information [`Modal`] content. + pub tx_info_content: Option, - /// Transaction identifier to use at confirmation [`Modal`] to cancel. - confirm_cancel_tx_id: Option, - /// Transaction identifier to use at confirmation [`Modal`] to delete. - confirm_delete_tx_id: Option, + /// Transaction identifier to use at confirmation [`Modal`] to cancel. + confirm_cancel_tx_id: Option, + /// Transaction identifier to use at confirmation [`Modal`] to delete. + confirm_delete_tx_id: Option, - /// Flag to check if sync of wallet was initiated manually at time. - manual_sync: Option + /// Flag to check if sync of wallet was initiated manually at time. + manual_sync: Option, } /// Identifier for transaction information [`Modal`]. @@ -53,641 +59,732 @@ const CANCEL_TX_CONFIRMATION_MODAL: &'static str = "cancel_tx_conf_modal"; const DELETE_TX_CONFIRMATION_MODAL: &'static str = "delete_tx_conf_modal"; impl WalletContentContainer for WalletTransactionsContent { - fn modal_ids(&self) -> Vec<&'static str> { - vec![TX_INFO_MODAL, CANCEL_TX_CONFIRMATION_MODAL, DELETE_TX_CONFIRMATION_MODAL] - } + fn modal_ids(&self) -> Vec<&'static str> { + vec![ + TX_INFO_MODAL, + CANCEL_TX_CONFIRMATION_MODAL, + DELETE_TX_CONFIRMATION_MODAL, + ] + } - fn modal_ui(&mut self, ui: &mut egui::Ui, w: &Wallet, m: &Modal, cb: &dyn PlatformCallbacks) { - match m.id { - TX_INFO_MODAL => { - if let Some(content) = self.tx_info_content.as_mut() { - let mut on_delete_id = None; - content.ui(ui, m, w, cb, |id| { - on_delete_id = Some(id); - }); - if let Some(id) = on_delete_id { - self.show_delete_confirmation_modal(id); - } - } - } - CANCEL_TX_CONFIRMATION_MODAL => { - self.cancel_confirmation_modal(ui, w); - } - DELETE_TX_CONFIRMATION_MODAL => { - self.delete_confirmation_modal(ui, w); - } - _ => {} - } - } + fn modal_ui(&mut self, ui: &mut egui::Ui, w: &Wallet, m: &Modal, cb: &dyn PlatformCallbacks) { + match m.id { + TX_INFO_MODAL => { + if let Some(content) = self.tx_info_content.as_mut() { + let mut on_delete_id = None; + content.ui(ui, m, w, cb, |id| { + on_delete_id = Some(id); + }); + if let Some(id) = on_delete_id { + self.show_delete_confirmation_modal(id); + } + } + } + CANCEL_TX_CONFIRMATION_MODAL => { + self.cancel_confirmation_modal(ui, w); + } + DELETE_TX_CONFIRMATION_MODAL => { + self.delete_confirmation_modal(ui, w); + } + _ => {} + } + } - fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, _: &dyn PlatformCallbacks) { - self.txs_ui(ui, wallet); - } + fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, _: &dyn PlatformCallbacks) { + self.txs_ui(ui, wallet); + } } impl WalletTransactionsContent { - /// Height of transaction list item. - pub const TX_ITEM_HEIGHT: f32 = 75.0; + /// Height of transaction list item. + pub const TX_ITEM_HEIGHT: f32 = 75.0; - /// Create new content instance with opening tx info. - pub fn new(tx: Option) -> Self { - let mut content = Self { - tx_info_content: None, - confirm_cancel_tx_id: None, - confirm_delete_tx_id: None, - manual_sync: None, - }; - if let Some(tx) = &tx { - content.show_tx_info_modal(tx.data.id); - } - content - } + /// Create new content instance with opening tx info. + pub fn new(tx: Option) -> Self { + let mut content = Self { + tx_info_content: None, + confirm_cancel_tx_id: None, + confirm_delete_tx_id: None, + manual_sync: None, + }; + if let Some(tx) = &tx { + content.show_tx_info_modal(tx.data.id); + } + content + } - /// Draw transactions content. - fn txs_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { - let data = wallet.get_data().unwrap(); - let config = wallet.get_config(); - if data.txs.is_none() { - ui.centered_and_justified(|ui| { - View::big_loading_spinner(ui); - }); - return; - } - let txs = data.txs.as_ref().unwrap() - .iter() - .filter(|tx| !tx.deleting()) - .collect::>(); - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - // Show message when txs are empty. - if txs.is_empty() { - View::center_content(ui, 96.0, |ui| { - let empty_text = t!( - "wallets.txs_empty", - "message" => FILE_ARROW_DOWN, - "transport" => FILE_TEXT, - "settings" => GEAR_FINE - ); - ui.label(RichText::new(empty_text) - .size(16.0) - .color(Colors::inactive_text())); - }); - return; - } - // Draw awaiting amount info if exists. - self.awaiting_info_ui(ui, &data); - }); - ui.add_space(4.0); + /// Draw transactions content. + fn txs_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { + let data = wallet.get_data().unwrap(); + let config = wallet.get_config(); + if data.txs.is_none() { + ui.centered_and_justified(|ui| { + View::big_loading_spinner(ui); + }); + return; + } + let txs = data + .txs + .as_ref() + .unwrap() + .iter() + .filter(|tx| !tx.deleting()) + .collect::>(); + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + // Show message when txs are empty. + if txs.is_empty() { + View::center_content(ui, 96.0, |ui| { + let empty_text = t!( + "wallets.txs_empty", + "message" => FILE_ARROW_DOWN, + "transport" => FILE_TEXT, + "settings" => GEAR_FINE + ); + ui.label( + RichText::new(empty_text) + .size(16.0) + .color(Colors::inactive_text()), + ); + }); + return; + } + // Draw awaiting amount info if exists. + self.awaiting_info_ui(ui, &data); + }); + ui.add_space(4.0); - // Show list of transactions. - let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(); - let refresh = self.manual_sync.unwrap_or(0) + 1600 > now; - let refresh_resp = PullToRefresh::new(refresh) - .id(Id::from("refresh_tx_list").with(config.id)) - .can_refresh(!refresh && !wallet.syncing() && !txs.is_empty()) - .min_refresh_distance(70.0) - .scroll_area_ui(ui, |ui| { - let rows_size = if txs.is_empty() { - 0 - } else { - // Last index is for list pagination. - txs.len() + 1 - }; - ScrollArea::vertical() - .id_salt(Id::from("wallet_tx_list_scroll").with(config.id)) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .auto_shrink([false; 2]) - .show_rows(ui, Self::TX_ITEM_HEIGHT, rows_size, |ui, row_range| { - ui.add_space(1.0); - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - self.tx_list_ui(ui, row_range, &wallet, txs); - }); - }) - }); + // Show list of transactions. + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(); + let refresh = self.manual_sync.unwrap_or(0) + 1600 > now; + let refresh_resp = PullToRefresh::new(refresh) + .id(Id::from("refresh_tx_list").with(config.id)) + .can_refresh(!refresh && !wallet.syncing() && !txs.is_empty()) + .min_refresh_distance(70.0) + .scroll_area_ui(ui, |ui| { + let rows_size = if txs.is_empty() { + 0 + } else { + // Last index is for list pagination. + txs.len() + 1 + }; + ScrollArea::vertical() + .id_salt(Id::from("wallet_tx_list_scroll").with(config.id)) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) + .show_rows(ui, Self::TX_ITEM_HEIGHT, rows_size, |ui, row_range| { + ui.add_space(1.0); + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + self.tx_list_ui(ui, row_range, &wallet, txs); + }); + }) + }); - // Sync wallet on refresh. - if refresh_resp.should_refresh() { - self.manual_sync = Some(now); - if !wallet.syncing() { - wallet.sync(); - } - } - } + // Sync wallet on refresh. + if refresh_resp.should_refresh() { + self.manual_sync = Some(now); + if !wallet.syncing() { + wallet.sync(); + } + } + } - /// Draw transaction list content. - fn tx_list_ui(&mut self, - ui: &mut egui::Ui, - row_range: Range, - wallet: &Wallet, - txs: Vec<&WalletTx>) { - let data = wallet.get_data().unwrap(); - for index in row_range { - if index == txs.len() && ui.is_visible() { - // Load more txs when needed. - if !wallet.more_txs_loading() { - if let Some(data) = wallet.get_data() { - if txs.len() as u32 >= data.txs_limit { - wallet.load_more_txs(); - } - } - } - // Show loader when more txs are loading. - if wallet.more_txs_loading() { - ui.vertical_centered(|ui| { - ui.add_space(24.0); - View::small_loading_spinner(ui); - }); - } - return; - } - // Transaction item background setup. - let rect = { - let mut r = ui.available_rect_before_wrap(); - r.min += egui::emath::vec2(6.0, 0.0); - r.max -= egui::emath::vec2(6.0, 0.0); - r.set_height(Self::TX_ITEM_HEIGHT); - r - }; - let rounding = View::item_rounding(index, txs.len(), false); - let bg = Colors::fill(); - let tx = txs.get(index).unwrap(); - let mut show_tx_info = false; - // Draw transaction list item. - Self::tx_item_ui(ui, tx, rect, bg, rounding, &data, (true, || { - show_tx_info = true; - }), |ui| { - let btn_rounding = { - let mut r = rounding.clone(); - r.nw = 0.0 as u8; - r.sw = 0.0 as u8; - r - }; - // Draw button to delete transaction. - if tx.data.confirmed || tx.cancelled() { - View::item_button(ui, btn_rounding, FILE_X, Some(Colors::inactive_text()), || { - self.show_delete_confirmation_modal(tx.data.id); - }); - } else if !tx.cancelled() && !tx.cancelling() && !tx.posting() && - wallet.synced_from_node() { - let repeat = tx.broadcasting_timed_out(wallet); - // Draw button to cancel transaction. - if tx.can_cancel() || repeat { - let (icon, color) = (PROHIBIT, Some(Colors::red())); - View::item_button(ui, btn_rounding, icon, color, || { - self.confirm_cancel_tx_id = Some(tx.data.id); - // Show transaction cancellation confirmation modal. - Modal::new(CANCEL_TX_CONFIRMATION_MODAL) - .position(ModalPosition::Center) - .title(t!("confirmation")) - .show(); - }); - } - // Draw button to repeat transaction action. - if tx.can_repeat_action(wallet) || repeat { - Self::tx_repeat_button_ui(ui, CornerRadius::default(), tx, wallet, repeat); - } - } - }); - // Show transaction info on click. - if show_tx_info { - self.show_tx_info_modal(tx.data.id); - } - } - } + /// Draw transaction list content. + fn tx_list_ui( + &mut self, + ui: &mut egui::Ui, + row_range: Range, + wallet: &Wallet, + txs: Vec<&WalletTx>, + ) { + let data = wallet.get_data().unwrap(); + for index in row_range { + if index == txs.len() && ui.is_visible() { + // Load more txs when needed. + if !wallet.more_txs_loading() { + if let Some(data) = wallet.get_data() { + if txs.len() as u32 >= data.txs_limit { + wallet.load_more_txs(); + } + } + } + // Show loader when more txs are loading. + if wallet.more_txs_loading() { + ui.vertical_centered(|ui| { + ui.add_space(24.0); + View::small_loading_spinner(ui); + }); + } + return; + } + // Transaction item background setup. + let rect = { + let mut r = ui.available_rect_before_wrap(); + r.min += egui::emath::vec2(6.0, 0.0); + r.max -= egui::emath::vec2(6.0, 0.0); + r.set_height(Self::TX_ITEM_HEIGHT); + r + }; + let rounding = View::item_rounding(index, txs.len(), false); + let bg = Colors::fill(); + let tx = txs.get(index).unwrap(); + let mut show_tx_info = false; + // Draw transaction list item. + Self::tx_item_ui( + ui, + tx, + rect, + bg, + rounding, + &data, + (true, || { + show_tx_info = true; + }), + |ui| { + let btn_rounding = { + let mut r = rounding.clone(); + r.nw = 0.0 as u8; + r.sw = 0.0 as u8; + r + }; + // Draw button to delete transaction. + if tx.data.confirmed || tx.cancelled() { + View::item_button( + ui, + btn_rounding, + FILE_X, + Some(Colors::inactive_text()), + || { + self.show_delete_confirmation_modal(tx.data.id); + }, + ); + } else if !tx.cancelled() + && !tx.cancelling() + && !tx.posting() && wallet.synced_from_node() + { + let repeat = tx.broadcasting_timed_out(wallet); + // Draw button to cancel transaction. + if tx.can_cancel() || repeat { + let (icon, color) = (PROHIBIT, Some(Colors::red())); + View::item_button(ui, btn_rounding, icon, color, || { + self.confirm_cancel_tx_id = Some(tx.data.id); + // Show transaction cancellation confirmation modal. + Modal::new(CANCEL_TX_CONFIRMATION_MODAL) + .position(ModalPosition::Center) + .title(t!("confirmation")) + .show(); + }); + } + // Draw button to repeat transaction action. + if tx.can_repeat_action(wallet) || repeat { + Self::tx_repeat_button_ui( + ui, + CornerRadius::default(), + tx, + wallet, + repeat, + ); + } + } + }, + ); + // Show transaction info on click. + if show_tx_info { + self.show_tx_info_modal(tx.data.id); + } + } + } - /// Draw information about locked, finalizing or confirming balance. - fn awaiting_info_ui(&mut self, ui: &mut egui::Ui, 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; - if amount_conf == 0 && amount_fin == 0 && amount_locked == 0 { - return; - } - ui.add_space(-1.0); - let rect = ui.available_rect_before_wrap(); - // Draw background. - let mut bg = RectShape::new(rect, CornerRadius { - nw: 0.0 as u8, - ne: 0.0 as u8, - sw: 8.0 as u8, - se: 8.0 as u8, - }, Colors::fill(), View::item_stroke(), StrokeKind::Outside); - let bg_idx = ui.painter().add(bg.clone()); - let resp = ui.allocate_ui(rect.size(), |ui| { - ui.vertical_centered_justified(|ui| { - // Correct vertical spacing between items. - ui.style_mut().spacing.item_spacing.y = -3.0; - if amount_conf != 0 { - // Draw awaiting confirmation amount. - awaiting_item_ui(ui, amount_conf, t!("wallets.await_conf_amount")); - } - if amount_fin != 0 { - // Draw awaiting confirmation amount. - awaiting_item_ui(ui, amount_fin, t!("wallets.await_fin_amount")); - } - if amount_locked != 0 { - // Draw locked amount. - awaiting_item_ui(ui, amount_locked, t!("wallets.locked_amount")); - } - }); - }).response; - // Setup background size. - bg.rect = resp.rect; - ui.painter().set(bg_idx, bg); - } + /// Draw information about locked, finalizing or confirming balance. + fn awaiting_info_ui(&mut self, ui: &mut egui::Ui, 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; + if amount_conf == 0 && amount_fin == 0 && amount_locked == 0 { + return; + } + ui.add_space(-1.0); + let rect = ui.available_rect_before_wrap(); + // Draw background. + let mut bg = RectShape::new( + rect, + CornerRadius { + nw: 0.0 as u8, + ne: 0.0 as u8, + sw: 8.0 as u8, + se: 8.0 as u8, + }, + Colors::fill(), + View::item_stroke(), + StrokeKind::Outside, + ); + let bg_idx = ui.painter().add(bg.clone()); + let resp = ui + .allocate_ui(rect.size(), |ui| { + ui.vertical_centered_justified(|ui| { + // Correct vertical spacing between items. + ui.style_mut().spacing.item_spacing.y = -3.0; + if amount_conf != 0 { + // Draw awaiting confirmation amount. + awaiting_item_ui(ui, amount_conf, t!("wallets.await_conf_amount")); + } + if amount_fin != 0 { + // Draw awaiting confirmation amount. + awaiting_item_ui(ui, amount_fin, t!("wallets.await_fin_amount")); + } + if amount_locked != 0 { + // Draw locked amount. + awaiting_item_ui(ui, amount_locked, t!("wallets.locked_amount")); + } + }); + }) + .response; + // Setup background size. + bg.rect = resp.rect; + ui.painter().set(bg_idx, bg); + } - /// Draw transaction item. - pub fn tx_item_ui(ui: &mut egui::Ui, - tx: &WalletTx, - rect: Rect, - bg: Color32, - r: CornerRadius, - data: &WalletData, - on_click: (bool, impl FnOnce()), - buttons_ui: impl FnOnce(&mut egui::Ui)) { - // Draw background. - let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); - let bg_idx = ui.painter().add(bg_shape.clone()); - let res = ui.scope_builder( - UiBuilder::new() - .sense(Sense::click()) - .layout(Layout::right_to_left(Align::Center)) - .max_rect(rect), |ui| { - ui.horizontal_centered(|ui| { - // Draw buttons. - buttons_ui(ui); - }); + /// Draw transaction item. + pub fn tx_item_ui( + ui: &mut egui::Ui, + tx: &WalletTx, + rect: Rect, + bg: Color32, + r: CornerRadius, + data: &WalletData, + on_click: (bool, impl FnOnce()), + buttons_ui: impl FnOnce(&mut egui::Ui), + ) { + // Draw background. + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); + let res = ui + .scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), + |ui| { + ui.horizontal_centered(|ui| { + // Draw buttons. + buttons_ui(ui); + }); - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - ui.add_space(3.0); + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(3.0); - // Setup transaction amount. - let mut amount_text = if tx.data.tx_type == TxLogEntryType::TxSent || - tx.data.tx_type == TxLogEntryType::TxSentCancelled { - "-" - } else if tx.data.tx_type == TxLogEntryType::TxReceived || - tx.data.tx_type == TxLogEntryType::TxReceivedCancelled { - "+" - } else { - "" - }.to_string(); - amount_text = format!("{}{} {}", - amount_text, - amount_to_hr_string(tx.amount, true), - GRIN); + // Setup transaction amount. + let mut amount_text = if tx.data.tx_type == TxLogEntryType::TxSent + || tx.data.tx_type == TxLogEntryType::TxSentCancelled + { + "-" + } else if tx.data.tx_type == TxLogEntryType::TxReceived + || tx.data.tx_type == TxLogEntryType::TxReceivedCancelled + { + "+" + } else { + "" + } + .to_string(); + amount_text = format!( + "{}{} {}", + amount_text, + amount_to_hr_string(tx.amount, true), + GRIN + ); - // Setup amount color. - let amount_color = match tx.data.tx_type { - TxLogEntryType::ConfirmedCoinbase => Colors::white_or_black(true), - TxLogEntryType::TxReceived => Colors::white_or_black(true), - TxLogEntryType::TxSent => Colors::white_or_black(true), - TxLogEntryType::TxReceivedCancelled => Colors::text(false), - TxLogEntryType::TxSentCancelled => Colors::text(false), - TxLogEntryType::TxReverted => Colors::text(false) - }; - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.add_space(1.0); - View::ellipsize_text(ui, amount_text, 18.0, amount_color); - }); - ui.add_space(-2.0); + // Setup amount color. + let amount_color = match tx.data.tx_type { + TxLogEntryType::ConfirmedCoinbase => Colors::white_or_black(true), + TxLogEntryType::TxReceived => Colors::white_or_black(true), + TxLogEntryType::TxSent => Colors::white_or_black(true), + TxLogEntryType::TxReceivedCancelled => Colors::text(false), + TxLogEntryType::TxSentCancelled => Colors::text(false), + TxLogEntryType::TxReverted => Colors::text(false), + }; + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.add_space(1.0); + View::ellipsize_text(ui, amount_text, 18.0, amount_color); + }); + ui.add_space(-2.0); - // Setup transaction status text. - let height = data.info.last_confirmed_height; - let status_text = if !tx.data.confirmed { - let is_canceled = tx.data.tx_type == TxLogEntryType::TxSentCancelled - || tx.data.tx_type == TxLogEntryType::TxReceivedCancelled; - if is_canceled { - format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled")) - } else if let Some(action) = &tx.action { - let error = if tx.action_error.is_none() { - "".to_string() - } else { - format!("{}: ", t!("error")) - }; - let status = match action { - WalletTxAction::Finalizing => t!("wallets.tx_finalizing"), - WalletTxAction::Posting => t!("wallets.tx_posting"), - WalletTxAction::SendingTor => t!("transport.tor_sending"), - _ => t!("wallets.tx_cancelling") - }; - let icon = if error.is_empty() { - DOTS_THREE_CIRCLE - } else { - WARNING - }; - format!("{} {}{}", icon, error, status) - } else { - match tx.data.tx_type { - TxLogEntryType::TxReceived => { - let text = match tx.finalized() { - true => t!("wallets.await_fin_amount"), - false => t!("wallets.tx_receiving") - }; - format!("{} {}", DOTS_THREE_CIRCLE, text) - }, - TxLogEntryType::TxSent => { - let text = match tx.finalized() { - true => t!("wallets.await_fin_amount"), - false => t!("wallets.tx_sending") - }; - format!("{} {}", DOTS_THREE_CIRCLE, text) - }, - TxLogEntryType::ConfirmedCoinbase => { - let tx_h = tx.height.unwrap_or(1) - 1; - if tx_h != 0 { - let left_conf = height - tx_h; - if height >= tx_h && left_conf < COINBASE_MATURITY { - let conf_info = format!("{}/{}", - left_conf, - COINBASE_MATURITY); - format!("{} {} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_confirming"), - conf_info - ) - } else { - format!("{} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_confirming")) - } - } else { - format!("{} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_confirming")) - } - }, - _ => { - format!("{} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_confirming")) - } - } - } - } else { - match tx.data.tx_type { - TxLogEntryType::ConfirmedCoinbase => { - let tx_h = tx.height.unwrap_or(1) - 1; - if tx_h != 0 { - let left_conf = height - tx_h; - if height >= tx_h && left_conf < COINBASE_MATURITY { - let conf_info = format!("{}/{}", - left_conf, - COINBASE_MATURITY); - format!("{} {} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_confirming"), - conf_info - ) - } else { - format!("{} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_confirmed")) - } - } else { - format!("{} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_confirmed")) - } + // Setup transaction status text. + let height = data.info.last_confirmed_height; + let status_text = if !tx.data.confirmed { + let is_canceled = tx.data.tx_type + == TxLogEntryType::TxSentCancelled + || tx.data.tx_type == TxLogEntryType::TxReceivedCancelled; + if is_canceled { + format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled")) + } else if let Some(action) = &tx.action { + let error = if tx.action_error.is_none() { + "".to_string() + } else { + format!("{}: ", t!("error")) + }; + let status = match action { + WalletTxAction::Finalizing => t!("wallets.tx_finalizing"), + WalletTxAction::Posting => t!("wallets.tx_posting"), + WalletTxAction::SendingTor => t!("transport.tor_sending"), + _ => t!("wallets.tx_cancelling"), + }; + let icon = if error.is_empty() { + DOTS_THREE_CIRCLE + } else { + WARNING + }; + format!("{} {}{}", icon, error, status) + } else { + match tx.data.tx_type { + TxLogEntryType::TxReceived => { + let text = match tx.finalized() { + true => t!("wallets.await_fin_amount"), + false => t!("wallets.tx_receiving"), + }; + format!("{} {}", DOTS_THREE_CIRCLE, text) + } + TxLogEntryType::TxSent => { + let text = match tx.finalized() { + true => t!("wallets.await_fin_amount"), + false => t!("wallets.tx_sending"), + }; + format!("{} {}", DOTS_THREE_CIRCLE, text) + } + TxLogEntryType::ConfirmedCoinbase => { + let tx_h = tx.height.unwrap_or(1) - 1; + if tx_h != 0 { + let left_conf = height - tx_h; + if height >= tx_h && left_conf < COINBASE_MATURITY { + let conf_info = format!( + "{}/{}", + left_conf, COINBASE_MATURITY + ); + format!( + "{} {} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_confirming"), + conf_info + ) + } else { + format!( + "{} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_confirming") + ) + } + } else { + format!( + "{} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_confirming") + ) + } + } + _ => { + format!( + "{} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_confirming") + ) + } + } + } + } else { + match tx.data.tx_type { + TxLogEntryType::ConfirmedCoinbase => { + let tx_h = tx.height.unwrap_or(1) - 1; + if tx_h != 0 { + let left_conf = height - tx_h; + if height >= tx_h && left_conf < COINBASE_MATURITY { + let conf_info = + format!("{}/{}", left_conf, COINBASE_MATURITY); + format!( + "{} {} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_confirming"), + conf_info + ) + } else { + format!( + "{} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_confirmed") + ) + } + } else { + format!( + "{} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_confirmed") + ) + } + } + TxLogEntryType::TxSent | TxLogEntryType::TxReceived => { + let min_conf = data.info.minimum_confirmations; + if tx.height.is_none() + || (tx.height.unwrap() != 0 + && height - tx.height.unwrap() >= min_conf - 1) + { + let (i, t) = + if tx.data.tx_type == TxLogEntryType::TxSent { + (ARROW_CIRCLE_UP, t!("wallets.tx_sent")) + } else { + (ARROW_CIRCLE_DOWN, t!("wallets.tx_received")) + }; + format!("{} {}", i, t) + } else { + let tx_height = tx.height.unwrap() - 1; + let left_conf = height - tx_height; + let conf_info = if tx_height != 0 + && height >= tx_height && left_conf + < min_conf + { + format!("{}/{}", left_conf, min_conf) + } else { + "".to_string() + }; + format!( + "{} {} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_confirming"), + conf_info + ) + } + } + _ => format!("{} {}", X_CIRCLE, t!("wallets.canceled")), + } + }; - }, - TxLogEntryType::TxSent | TxLogEntryType::TxReceived => { - let min_conf = data.info.minimum_confirmations; - if tx.height.is_none() || (tx.height.unwrap() != 0 && - height - tx.height.unwrap() >= min_conf - 1) { - let (i, t) = if tx.data.tx_type == TxLogEntryType::TxSent { - (ARROW_CIRCLE_UP, t!("wallets.tx_sent")) - } else { - (ARROW_CIRCLE_DOWN, t!("wallets.tx_received")) - }; - format!("{} {}", i, t) - } else { - let tx_height = tx.height.unwrap() - 1; - let left_conf = height - tx_height; - let conf_info = if tx_height != 0 && height >= tx_height && - left_conf < min_conf { - format!("{}/{}", left_conf, min_conf) - } else { - "".to_string() - }; - format!("{} {} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_confirming"), - conf_info - ) - } - }, - _ => format!("{} {}", X_CIRCLE, t!("wallets.canceled")) - } - }; + // Setup status text color. + let status_color = match tx.data.tx_type { + TxLogEntryType::ConfirmedCoinbase => Colors::text(false), + TxLogEntryType::TxReceived => { + if tx.data.confirmed { + Colors::green() + } else { + Colors::text(false) + } + } + TxLogEntryType::TxSent => { + if tx.data.confirmed { + Colors::red() + } else { + Colors::text(false) + } + } + TxLogEntryType::TxReceivedCancelled => Colors::inactive_text(), + TxLogEntryType::TxSentCancelled => Colors::inactive_text(), + TxLogEntryType::TxReverted => Colors::inactive_text(), + }; + View::ellipsize_text(ui, status_text, 15.0, status_color); - // Setup status text color. - let status_color = match tx.data.tx_type { - TxLogEntryType::ConfirmedCoinbase => Colors::text(false), - TxLogEntryType::TxReceived => if tx.data.confirmed { - Colors::green() - } else { - Colors::text(false) - }, - TxLogEntryType::TxSent => if tx.data.confirmed { - Colors::red() - } else { - Colors::text(false) - }, - TxLogEntryType::TxReceivedCancelled => Colors::inactive_text(), - TxLogEntryType::TxSentCancelled => Colors::inactive_text(), - TxLogEntryType::TxReverted => Colors::inactive_text(), - }; - View::ellipsize_text(ui, status_text, 15.0, status_color); + // Setup transaction time. + let tx_time = View::format_time(tx.data.creation_ts.timestamp()); + let tx_time_text = format!("{} {}", CALENDAR_CHECK, tx_time); + ui.label(RichText::new(tx_time_text).size(15.0).color(Colors::gray())); + ui.add_space(4.0); + }); + }); + }, + ) + .response; + let (clickable, on_click) = on_click; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if clickable && res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::TRANSPARENT; + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked && clickable { + on_click(); + } + } - // Setup transaction time. - let tx_time = View::format_time(tx.data.creation_ts.timestamp()); - let tx_time_text = format!("{} {}", CALENDAR_CHECK, tx_time); - ui.label(RichText::new(tx_time_text).size(15.0).color(Colors::gray())); - ui.add_space(4.0); - }); - }); - } - ).response; - let (clickable, on_click) = on_click; - let clicked = res.clicked() || res.long_touched(); - // Setup background and cursor. - if clickable && res.hovered() { - res.on_hover_cursor(CursorIcon::PointingHand); - bg_shape.fill = Colors::TRANSPARENT; - } - ui.painter().set(bg_idx, bg_shape); - // Handle clicks on layout. - if clicked && clickable { - on_click(); - } - } + /// Draw button to repeat transaction action on error or repost. + pub fn tx_repeat_button_ui( + ui: &mut egui::Ui, + rounding: CornerRadius, + tx: &WalletTx, + wallet: &Wallet, + repost: bool, + ) { + let (icon, color) = (ARROWS_CLOCKWISE, Some(Colors::green())); + View::item_button(ui, rounding, icon, color, || { + if repost { + wallet.task(WalletTask::Post(tx.data.id)); + } else if let Some(action) = tx.action.as_ref() { + match action { + WalletTxAction::Finalizing => { + wallet.task(WalletTask::Finalize(tx.data.id)); + } + WalletTxAction::Posting => { + wallet.task(WalletTask::Post(tx.data.id)); + } + _ => { + if let Some(a) = &tx.receiver { + wallet.task(WalletTask::SendTor(tx.data.clone(), a.clone())); + } + } + } + } else { + if let Some(a) = &tx.receiver { + wallet.task(WalletTask::SendTor(tx.data.clone(), a.clone())); + } + } + }); + } - /// Draw button to repeat transaction action on error or repost. - pub fn tx_repeat_button_ui(ui: &mut egui::Ui, - rounding: CornerRadius, - tx: &WalletTx, - wallet: &Wallet, - repost: bool) { - let (icon, color) = (ARROWS_CLOCKWISE, Some(Colors::green())); - View::item_button(ui, rounding, icon, color, || { - if repost { - wallet.task(WalletTask::Post(tx.data.id)); - } else if let Some(action) = tx.action.as_ref() { - match action { - WalletTxAction::Finalizing => { - wallet.task(WalletTask::Finalize(tx.data.id)); - } - WalletTxAction::Posting => { - wallet.task(WalletTask::Post(tx.data.id)); - } - _ => { - if let Some(a) = &tx.receiver { - wallet.task(WalletTask::SendTor(tx.data.clone(), a.clone())); - } - } - } - } else { - if let Some(a) = &tx.receiver { - wallet.task(WalletTask::SendTor(tx.data.clone(), a.clone())); - } - } - }); - } + /// Show transaction information [`Modal`]. + fn show_tx_info_modal(&mut self, id: u32) { + let modal = WalletTransactionContent::new(id); + self.tx_info_content = Some(modal); + Modal::new(TX_INFO_MODAL) + .position(ModalPosition::Center) + .title(t!("wallets.tx")) + .show(); + } - /// Show transaction information [`Modal`]. - fn show_tx_info_modal(&mut self, id: u32) { - let modal = WalletTransactionContent::new(id); - self.tx_info_content = Some(modal); - Modal::new(TX_INFO_MODAL) - .position(ModalPosition::Center) - .title(t!("wallets.tx")) - .show(); - } + /// Confirmation [`Modal`] to cancel transaction. + fn cancel_confirmation_modal(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { + let data = wallet.get_data().unwrap(); + let data_txs = data.txs.unwrap(); + let txs = data_txs + .into_iter() + .filter(|tx| tx.data.id == self.confirm_cancel_tx_id.unwrap_or_default()) + .collect::>(); + if txs.is_empty() { + Modal::close(); + return; + } + let tx = txs.get(0).unwrap(); + let amount = amount_to_hr_string(tx.amount, true); + let text = match tx.data.tx_type { + TxLogEntryType::TxReceived => { + t!("wallets.tx_receive_cancel_conf", "amount" => amount) + } + _ => { + t!("wallets.tx_send_cancel_conf", "amount" => amount) + } + }; - /// Confirmation [`Modal`] to cancel transaction. - fn cancel_confirmation_modal(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { - let data = wallet.get_data().unwrap(); - let data_txs = data.txs.unwrap(); - let txs = data_txs.into_iter() - .filter(|tx| tx.data.id == self.confirm_cancel_tx_id.unwrap_or_default()) - .collect::>(); - if txs.is_empty() { - Modal::close(); - return; - } - let tx = txs.get(0).unwrap(); - let amount = amount_to_hr_string(tx.amount, true); - let text = match tx.data.tx_type { - TxLogEntryType::TxReceived => { - t!("wallets.tx_receive_cancel_conf", "amount" => amount) - }, - _ => { - t!("wallets.tx_send_cancel_conf", "amount" => amount) - } - }; + // Show confirmation text. + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(text).size(17.0).color(Colors::text(false))); + ui.add_space(8.0); + }); - // Show confirmation text. - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(text) - .size(17.0) - .color(Colors::text(false))); - ui.add_space(8.0); - }); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + self.confirm_cancel_tx_id = None; + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, "OK".to_string(), Colors::white_or_black(false), || { + wallet.task(WalletTask::Cancel(tx.data.id)); + self.confirm_cancel_tx_id = None; + Modal::close(); + }); + }); + }); + ui.add_space(6.0); + }); + } - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - self.confirm_cancel_tx_id = None; - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, "OK".to_string(), Colors::white_or_black(false), || { - wallet.task(WalletTask::Cancel(tx.data.id)); - self.confirm_cancel_tx_id = None; - Modal::close(); - }); - }); - }); - ui.add_space(6.0); - }); - } + /// Show transaction deletion confirmation [`Modal`]. + fn show_delete_confirmation_modal(&mut self, id: u32) { + self.confirm_delete_tx_id = Some(id); + // Show transaction deletion confirmation modal. + Modal::new(DELETE_TX_CONFIRMATION_MODAL) + .position(ModalPosition::Center) + .title(t!("confirmation")) + .show(); + } - /// Show transaction deletion confirmation [`Modal`]. - fn show_delete_confirmation_modal(&mut self, id: u32) { - self.confirm_delete_tx_id = Some(id); - // Show transaction deletion confirmation modal. - Modal::new(DELETE_TX_CONFIRMATION_MODAL) - .position(ModalPosition::Center) - .title(t!("confirmation")) - .show(); - } + /// Confirmation [`Modal`] to delete transaction. + fn delete_confirmation_modal(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { + let data = wallet.get_data().unwrap(); + let data_txs = data.txs.unwrap(); + let txs = data_txs + .into_iter() + .filter(|tx| tx.data.id == self.confirm_delete_tx_id.unwrap_or_default()) + .collect::>(); + if txs.is_empty() { + Modal::close(); + return; + } + let tx = txs.get(0).unwrap(); - /// Confirmation [`Modal`] to delete transaction. - fn delete_confirmation_modal(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { - let data = wallet.get_data().unwrap(); - let data_txs = data.txs.unwrap(); - let txs = data_txs.into_iter() - .filter(|tx| tx.data.id == self.confirm_delete_tx_id.unwrap_or_default()) - .collect::>(); - if txs.is_empty() { - Modal::close(); - return; - } - let tx = txs.get(0).unwrap(); + // Show confirmation text. + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("wallets.tx_delete_confirmation")) + .size(17.0) + .color(Colors::text(false)), + ); + ui.add_space(8.0); + }); - // Show confirmation text. - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("wallets.tx_delete_confirmation")) - .size(17.0) - .color(Colors::text(false))); - ui.add_space(8.0); - }); + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - self.confirm_delete_tx_id = None; - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, "OK".to_string(), Colors::white_or_black(false), || { - wallet.task(WalletTask::Delete(tx.data.id)); - self.confirm_delete_tx_id = None; - Modal::close(); - }); - }); - }); - ui.add_space(6.0); - }); - } + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + self.confirm_delete_tx_id = None; + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, "OK".to_string(), Colors::white_or_black(false), || { + wallet.task(WalletTask::Delete(tx.data.id)); + self.confirm_delete_tx_id = None; + Modal::close(); + }); + }); + }); + ui.add_space(6.0); + }); + } } /// Draw awaiting balance item content. fn awaiting_item_ui(ui: &mut egui::Ui, amount: u64, label: impl Into) { - let rect = ui.available_rect_before_wrap(); - View::line(ui, LinePosition::TOP, &rect, Colors::item_stroke()); - ui.add_space(4.0); - let amount_format = amount_to_hr_string(amount, true); - ui.label(RichText::new(format!("{} ツ", amount_format)) - .color(Colors::white_or_black(true)) - .size(17.0)); - ui.label(RichText::new(label) - .color(Colors::gray()) - .size(15.0)); - ui.add_space(8.0); -} \ No newline at end of file + let rect = ui.available_rect_before_wrap(); + View::line(ui, LinePosition::TOP, &rect, Colors::item_stroke()); + ui.add_space(4.0); + let amount_format = amount_to_hr_string(amount, true); + ui.label( + RichText::new(format!("{} ツ", amount_format)) + .color(Colors::white_or_black(true)) + .size(17.0), + ); + ui.label(RichText::new(label).color(Colors::gray()).size(15.0)); + ui.add_space(8.0); +} diff --git a/src/gui/views/wallets/wallet/txs/mod.rs b/src/gui/views/wallets/wallet/txs/mod.rs index e283c72f..613ed6e0 100644 --- a/src/gui/views/wallets/wallet/txs/mod.rs +++ b/src/gui/views/wallets/wallet/txs/mod.rs @@ -16,4 +16,4 @@ mod content; pub use content::*; mod tx; -pub use tx::*; \ No newline at end of file +pub use tx::*; diff --git a/src/gui/views/wallets/wallet/txs/tx.rs b/src/gui/views/wallets/wallet/txs/tx.rs index 71a2b1e3..9a903b53 100644 --- a/src/gui/views/wallets/wallet/txs/tx.rs +++ b/src/gui/views/wallets/wallet/txs/tx.rs @@ -19,383 +19,405 @@ use grin_util::ToHex; use grin_wallet_libwallet::TxLogEntryType; use std::fs; -use crate::gui::icons::{CIRCLE_HALF, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, FILE_X, HASH_STRAIGHT, PROHIBIT, QR_CODE, SEAL_CHECK}; +use crate::AppConfig; +use crate::gui::Colors; +use crate::gui::icons::{ + CIRCLE_HALF, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, FILE_X, HASH_STRAIGHT, PROHIBIT, QR_CODE, + SEAL_CHECK, +}; use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::types::ModalPosition; +use crate::gui::views::wallets::wallet::message::MessageInputContent; use crate::gui::views::wallets::wallet::proof::PaymentProofContent; use crate::gui::views::wallets::wallet::txs::WalletTransactionsContent; use crate::gui::views::{Modal, QrCodeContent, View}; -use crate::gui::Colors; -use crate::wallet::types::{WalletTask, WalletTx}; use crate::wallet::Wallet; -use crate::AppConfig; -use crate::gui::views::types::ModalPosition; -use crate::gui::views::wallets::wallet::message::MessageInputContent; +use crate::wallet::types::{WalletTask, WalletTx}; /// Transaction information [`Modal`] content. pub struct WalletTransactionContent { - /// Transaction identifier. - tx_id: u32, - /// Slatepack message text. - message: Option, + /// Transaction identifier. + tx_id: u32, + /// Slatepack message text. + message: Option, - /// QR code Slatepack message image content. - qr_code_content: Option, + /// QR code Slatepack message image content. + qr_code_content: Option, - /// Payment proof sharing content. - pub proof_content: Option, + /// Payment proof sharing content. + pub proof_content: Option, } impl WalletTransactionContent { - /// Create new content instance with [`Wallet`] from provided [`WalletTx`]. - pub fn new(tx_id: u32) -> Self { - Self { - tx_id, - message: None, - qr_code_content: None, - proof_content: None, - } - } + /// Create new content instance with [`Wallet`] from provided [`WalletTx`]. + pub fn new(tx_id: u32) -> Self { + Self { + tx_id, + message: None, + qr_code_content: None, + proof_content: None, + } + } - /// Draw [`Modal`] content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - wallet: &Wallet, - cb: &dyn PlatformCallbacks, - on_delete: impl FnOnce(u32)) { - // Check values and setup transaction data. - let wallet_data = wallet.get_data(); - if wallet_data.is_none() { - Modal::close(); - return; - } - let data = wallet_data.unwrap(); - let data_txs = data.txs.clone().unwrap(); - let txs = data_txs.into_iter() - .filter(|tx| tx.data.id == self.tx_id) - .collect::>(); - if txs.is_empty() { - Modal::close(); - return; - } - let tx = txs.get(0).unwrap(); + /// Draw [`Modal`] content. + pub fn ui( + &mut self, + ui: &mut egui::Ui, + modal: &Modal, + wallet: &Wallet, + cb: &dyn PlatformCallbacks, + on_delete: impl FnOnce(u32), + ) { + // Check values and setup transaction data. + let wallet_data = wallet.get_data(); + if wallet_data.is_none() { + Modal::close(); + return; + } + let data = wallet_data.unwrap(); + let data_txs = data.txs.clone().unwrap(); + let txs = data_txs + .into_iter() + .filter(|tx| tx.data.id == self.tx_id) + .collect::>(); + if txs.is_empty() { + Modal::close(); + return; + } + let tx = txs.get(0).unwrap(); - if let Some(content) = self.qr_code_content.as_mut() { - let dark_theme = AppConfig::dark_theme().unwrap_or(false); - // Set light theme for better scanning. - AppConfig::set_dark_theme(false); - crate::setup_visuals(ui.ctx()); - modal.set_background_color(Colors::FILL_DEEP); + if let Some(content) = self.qr_code_content.as_mut() { + let dark_theme = AppConfig::dark_theme().unwrap_or(false); + // Set light theme for better scanning. + AppConfig::set_dark_theme(false); + crate::setup_visuals(ui.ctx()); + modal.set_background_color(Colors::FILL_DEEP); - ui.add_space(6.0); - content.ui(ui, cb); + ui.add_space(6.0); + content.ui(ui, cb); - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - // Show buttons to close modal or come back to text request content. - ui.columns(2, |cols| { - cols[0].vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.qr_code_content = None; - Modal::close(); - }); - }); - cols[1].vertical_centered_justified(|ui| { - View::button(ui, t!("back"), Colors::white_or_black(false), || { - self.qr_code_content = None; - }); - }); - }); - // Set color theme back. - AppConfig::set_dark_theme(dark_theme); - crate::setup_visuals(ui.ctx()); - } else { - modal.set_background_color(Colors::fill()); - // Show transaction information. - self.info_ui(ui, tx, wallet, cb, on_delete); + // Show buttons to close modal or come back to text request content. + ui.columns(2, |cols| { + cols[0].vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + self.qr_code_content = None; + Modal::close(); + }); + }); + cols[1].vertical_centered_justified(|ui| { + View::button(ui, t!("back"), Colors::white_or_black(false), || { + self.qr_code_content = None; + }); + }); + }); + // Set color theme back. + AppConfig::set_dark_theme(dark_theme); + crate::setup_visuals(ui.ctx()); + } else { + modal.set_background_color(Colors::fill()); + // Show transaction information. + self.info_ui(ui, tx, wallet, cb, on_delete); - // Show transaction sharing content or payment proof. - if self.proof_content.is_none() && tx.can_cancel() && !tx.finalized() { - self.share_ui(ui, wallet, tx, cb); - } else { - if let Some(proof_content) = self.proof_content.as_mut() { - // Draw payment proof sharing content. - proof_content.share_ui(ui, tx, cb); - } else if tx.proof.is_some() && !tx.sending_tor() && - tx.action_error.is_none() { - ui.vertical_centered(|ui| { - ui.add_space(8.0); - let label = format!("{} {}", SEAL_CHECK, t!("wallets.payment_proof")); - let text_color = Colors::gold_dark(); - let btn_color = Colors::white_or_black(false); - // Draw button to show payment proof sharing content. - View::colored_text_button(ui, label, text_color, btn_color, || { - if let Ok(p) = serde_json::to_string_pretty(&tx.proof) { - let c = PaymentProofContent::new(Some(p)); - self.proof_content = Some(c); - } - }); - }); - } - } + // Show transaction sharing content or payment proof. + if self.proof_content.is_none() && tx.can_cancel() && !tx.finalized() { + self.share_ui(ui, wallet, tx, cb); + } else { + if let Some(proof_content) = self.proof_content.as_mut() { + // Draw payment proof sharing content. + proof_content.share_ui(ui, tx, cb); + } else if tx.proof.is_some() && !tx.sending_tor() && tx.action_error.is_none() { + ui.vertical_centered(|ui| { + ui.add_space(8.0); + let label = format!("{} {}", SEAL_CHECK, t!("wallets.payment_proof")); + let text_color = Colors::gold_dark(); + let btn_color = Colors::white_or_black(false); + // Draw button to show payment proof sharing content. + View::colored_text_button(ui, label, text_color, btn_color, || { + if let Ok(p) = serde_json::to_string_pretty(&tx.proof) { + let c = PaymentProofContent::new(Some(p)); + self.proof_content = Some(c); + } + }); + }); + } + } - ui.add_space(8.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); + ui.add_space(8.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); - // Show button to close modal. - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - Modal::close(); - }); - }); - } - ui.add_space(6.0); - } + // Show button to close modal. + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + Modal::close(); + }); + }); + } + ui.add_space(6.0); + } - /// Draw transaction sharing content. - fn share_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - tx: &WalletTx, - cb: &dyn PlatformCallbacks) { - if self.message.is_none() { - let slatepack_path = wallet.get_config().get_tx_slate_path(tx); - self.message = Some(fs::read_to_string(slatepack_path).unwrap_or("".to_string())); - } - if let Some(m) = &self.message { - if m.is_empty() { - return; - } + /// Draw transaction sharing content. + fn share_ui( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + tx: &WalletTx, + cb: &dyn PlatformCallbacks, + ) { + if self.message.is_none() { + let slatepack_path = wallet.get_config().get_tx_slate_path(tx); + self.message = Some(fs::read_to_string(slatepack_path).unwrap_or("".to_string())); + } + if let Some(m) = &self.message { + if m.is_empty() { + return; + } - let amount = amount_to_hr_string(tx.amount, true); - let desc_text = if tx.can_finalize() { - if tx.data.tx_type == TxLogEntryType::TxSent { - t!("wallets.send_request_desc", "amount" => amount) - } else { - t!("wallets.invoice_desc", "amount" => amount) - } - } else { - if tx.data.tx_type == TxLogEntryType::TxSent { - t!("wallets.parse_i1_slatepack_desc", "amount" => amount) - } else { - t!("wallets.parse_s1_slatepack_desc", "amount" => amount) - } - }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(desc_text).size(16.0).color(Colors::inactive_text())); - }); - ui.add_space(6.0); + let amount = amount_to_hr_string(tx.amount, true); + let desc_text = if tx.can_finalize() { + if tx.data.tx_type == TxLogEntryType::TxSent { + t!("wallets.send_request_desc", "amount" => amount) + } else { + t!("wallets.invoice_desc", "amount" => amount) + } + } else { + if tx.data.tx_type == TxLogEntryType::TxSent { + t!("wallets.parse_i1_slatepack_desc", "amount" => amount) + } else { + t!("wallets.parse_s1_slatepack_desc", "amount" => amount) + } + }; + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(desc_text) + .size(16.0) + .color(Colors::inactive_text()), + ); + }); + ui.add_space(6.0); - let mut message = m.clone(); - // Draw Slatepack message content. - ui.vertical_centered(|ui| { - let scroll_id = Id::from("tx_info_message_request").with(tx.data.id); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(3.0); - ScrollArea::vertical() - .id_salt(scroll_id) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .max_height(128.0) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(7.0); - let input_id = scroll_id.with("_input"); - egui::TextEdit::multiline(&mut message) - .id(input_id) - .font(egui::TextStyle::Small) - .desired_rows(5) - .interactive(false) - .desired_width(f32::INFINITY) - .show(ui); - ui.add_space(6.0); - }); - }); + let mut message = m.clone(); + // Draw Slatepack message content. + ui.vertical_centered(|ui| { + let scroll_id = Id::from("tx_info_message_request").with(tx.data.id); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(3.0); + ScrollArea::vertical() + .id_salt(scroll_id) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(128.0) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + let input_id = scroll_id.with("_input"); + egui::TextEdit::multiline(&mut message) + .id(input_id) + .font(egui::TextStyle::Small) + .desired_rows(5) + .interactive(false) + .desired_width(f32::INFINITY) + .show(ui); + ui.add_space(6.0); + }); + }); - ui.add_space(2.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - let mut finalization_needed = false; + let mut finalization_needed = false; - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - // Draw button to show Slatepack message as QR code. - let qr_text = format!("{} {}", QR_CODE, t!("qr_code")); - View::button(ui, qr_text, Colors::white_or_black(false), || { - self.qr_code_content = Some(QrCodeContent::new(message, true)); - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Draw copy button. - let copy_text = format!("{} {}", COPY, t!("copy")); - View::button(ui, copy_text, Colors::white_or_black(false), || { - cb.copy_string_to_buffer(m.clone()); - // Show message input or close modal. - if tx.can_finalize() { - finalization_needed = true; - } else { - Modal::close(); - } - }); - }); - }); + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + // Draw button to show Slatepack message as QR code. + let qr_text = format!("{} {}", QR_CODE, t!("qr_code")); + View::button(ui, qr_text, Colors::white_or_black(false), || { + self.qr_code_content = Some(QrCodeContent::new(message, true)); + }); + }); + columns[1].vertical_centered_justified(|ui| { + // Draw copy button. + let copy_text = format!("{} {}", COPY, t!("copy")); + View::button(ui, copy_text, Colors::white_or_black(false), || { + cb.copy_string_to_buffer(m.clone()); + // Show message input or close modal. + if tx.can_finalize() { + finalization_needed = true; + } else { + Modal::close(); + } + }); + }); + }); - // Draw button to share response as file. - ui.add_space(8.0); - ui.vertical_centered(|ui| { - let share_text = format!("{} {}", FILE_TEXT, t!("share")); - View::colored_text_button(ui, - share_text, - Colors::blue(), - Colors::white_or_black(false), || { - if let Some(slate_id) = tx.data.tx_slate_id { - let name = format!("{}.{}.slatepack", slate_id, tx.state); - let data = m.as_bytes().to_vec(); - cb.share_data(name, data).unwrap_or_default(); - // Show message input or close modal. - if tx.can_finalize() { - finalization_needed = true; - } else { - Modal::close(); - } - } - }); - }); + // Draw button to share response as file. + ui.add_space(8.0); + ui.vertical_centered(|ui| { + let share_text = format!("{} {}", FILE_TEXT, t!("share")); + View::colored_text_button( + ui, + share_text, + Colors::blue(), + Colors::white_or_black(false), + || { + if let Some(slate_id) = tx.data.tx_slate_id { + let name = format!("{}.{}.slatepack", slate_id, tx.state); + let data = m.as_bytes().to_vec(); + cb.share_data(name, data).unwrap_or_default(); + // Show message input or close modal. + if tx.can_finalize() { + finalization_needed = true; + } else { + Modal::close(); + } + } + }, + ); + }); - if finalization_needed { - Modal::new(MessageInputContent::MODAL_ID) - .position(ModalPosition::Center) - .title(t!("wallets.messages")) - .show(); - } - } - } + if finalization_needed { + Modal::new(MessageInputContent::MODAL_ID) + .position(ModalPosition::Center) + .title(t!("wallets.messages")) + .show(); + } + } + } - /// Draw transaction information content. - fn info_ui(&mut self, - ui: &mut egui::Ui, - tx: &WalletTx, - wallet: &Wallet, - cb: &dyn PlatformCallbacks, - on_delete: impl FnOnce(u32)) { - ui.add_space(6.0); - // Transaction item background setup. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(WalletTransactionsContent::TX_ITEM_HEIGHT); - let rounding = View::item_rounding(0, 2, false); - let bg = Colors::TRANSPARENT; - // Show transaction amount status and time. - let data = wallet.get_data().unwrap(); - let on_click = (false, || {}); - WalletTransactionsContent::tx_item_ui(ui, tx, rect, bg, rounding, &data, on_click, |ui| { - // Show button to delete transaction from database. - if tx.data.confirmed || tx.cancelled() { - let r = View::item_rounding(0, 2, true); - View::item_button(ui, r, FILE_X, Some(Colors::inactive_text()), || { - on_delete(tx.data.id); - }); - } - // Show block height or buttons. - if let Some(h) = tx.height { - if h != 0 { - ui.add_space(6.0); - let height = format!("{} {}", CUBE, h.to_string()); - ui.with_layout(Layout::bottom_up(Align::Max), |ui| { - ui.add_space(3.0); - ui.label(RichText::new(height) - .size(15.0) - .color(Colors::text(false))); - }); - } - return; - } - if wallet.synced_from_node() && !tx.cancelled() && !tx.cancelling() && !tx.posting() { - let repeat = tx.broadcasting_timed_out(&wallet); - // Draw button to cancel transaction. - if tx.can_cancel() || repeat { - let r = View::item_rounding(0, 2, true); - View::item_button(ui, r, PROHIBIT, Some(Colors::red()), || { - wallet.task(WalletTask::Cancel(tx.data.id)); - Modal::close(); - }); - } - // Draw button to repeat transaction action. - if tx.can_repeat_action(wallet) || repeat { - let r = if tx.can_finalize() || tx.can_cancel() { - CornerRadius::default() - } else { - View::item_rounding(0, 2, true) - }; - WalletTransactionsContent::tx_repeat_button_ui(ui, r, tx, wallet, repeat); - } - } - }); + /// Draw transaction information content. + fn info_ui( + &mut self, + ui: &mut egui::Ui, + tx: &WalletTx, + wallet: &Wallet, + cb: &dyn PlatformCallbacks, + on_delete: impl FnOnce(u32), + ) { + ui.add_space(6.0); + // Transaction item background setup. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(WalletTransactionsContent::TX_ITEM_HEIGHT); + let rounding = View::item_rounding(0, 2, false); + let bg = Colors::TRANSPARENT; + // Show transaction amount status and time. + let data = wallet.get_data().unwrap(); + let on_click = (false, || {}); + WalletTransactionsContent::tx_item_ui(ui, tx, rect, bg, rounding, &data, on_click, |ui| { + // Show button to delete transaction from database. + if tx.data.confirmed || tx.cancelled() { + let r = View::item_rounding(0, 2, true); + View::item_button(ui, r, FILE_X, Some(Colors::inactive_text()), || { + on_delete(tx.data.id); + }); + } + // Show block height or buttons. + if let Some(h) = tx.height { + if h != 0 { + ui.add_space(6.0); + let height = format!("{} {}", CUBE, h.to_string()); + ui.with_layout(Layout::bottom_up(Align::Max), |ui| { + ui.add_space(3.0); + ui.label(RichText::new(height).size(15.0).color(Colors::text(false))); + }); + } + return; + } + if wallet.synced_from_node() && !tx.cancelled() && !tx.cancelling() && !tx.posting() { + let repeat = tx.broadcasting_timed_out(&wallet); + // Draw button to cancel transaction. + if tx.can_cancel() || repeat { + let r = View::item_rounding(0, 2, true); + View::item_button(ui, r, PROHIBIT, Some(Colors::red()), || { + wallet.task(WalletTask::Cancel(tx.data.id)); + Modal::close(); + }); + } + // Draw button to repeat transaction action. + if tx.can_repeat_action(wallet) || repeat { + let r = if tx.can_finalize() || tx.can_cancel() { + CornerRadius::default() + } else { + View::item_rounding(0, 2, true) + }; + WalletTransactionsContent::tx_repeat_button_ui(ui, r, tx, wallet, repeat); + } + } + }); - // Show identifier. - if let Some(id) = tx.data.tx_slate_id { - let label = format!("{} {}", HASH_STRAIGHT, t!("id")); - info_item_ui(ui, id.to_string(), label, true, cb); - } - // Show kernel. - if let Some(kernel) = tx.data.kernel_excess { - let label = format!("{} {}", FILE_ARCHIVE, t!("kernel")); - info_item_ui(ui, kernel.0.to_hex(), label, true, cb); - } - // Show receiver or sender address. - let addr = if tx.data.tx_type == TxLogEntryType::TxSent { - &tx.receiver - } else { - &tx.sender - }; - if let Some(addr) = addr { - let label = format!("{} {}", CIRCLE_HALF, t!("network_mining.address")); - info_item_ui(ui, addr.to_string(), label, true, cb); - } - } + // Show identifier. + if let Some(id) = tx.data.tx_slate_id { + let label = format!("{} {}", HASH_STRAIGHT, t!("id")); + info_item_ui(ui, id.to_string(), label, true, cb); + } + // Show kernel. + if let Some(kernel) = tx.data.kernel_excess { + let label = format!("{} {}", FILE_ARCHIVE, t!("kernel")); + info_item_ui(ui, kernel.0.to_hex(), label, true, cb); + } + // Show receiver or sender address. + let addr = if tx.data.tx_type == TxLogEntryType::TxSent { + &tx.receiver + } else { + &tx.sender + }; + if let Some(addr) = addr { + let label = format!("{} {}", CIRCLE_HALF, t!("network_mining.address")); + info_item_ui(ui, addr.to_string(), label, true, cb); + } + } } /// Draw transaction information item content. -fn info_item_ui(ui: &mut egui::Ui, - value: String, - label: String, - copy: bool, - cb: &dyn PlatformCallbacks) { - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(50.0); +fn info_item_ui( + ui: &mut egui::Ui, + value: String, + label: String, + copy: bool, + cb: &dyn PlatformCallbacks, +) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(50.0); - // Draw round background. - let bg_rect = rect.clone(); - let mut rounding = View::item_rounding(1, 3, false); + // Draw round background. + let bg_rect = rect.clone(); + let mut rounding = View::item_rounding(1, 3, false); - ui.painter().rect(bg_rect, rounding, Colors::fill(), View::item_stroke(), StrokeKind::Outside); + ui.painter().rect( + bg_rect, + rounding, + Colors::fill(), + View::item_stroke(), + StrokeKind::Outside, + ); - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - // Draw button to copy transaction info value. - if copy { - rounding.nw = 0.0 as u8; - rounding.sw = 0.0 as u8; - View::item_button(ui, rounding, COPY, None, || { - cb.copy_string_to_buffer(value.clone()); - }); - } + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + // Draw button to copy transaction info value. + if copy { + rounding.nw = 0.0 as u8; + rounding.sw = 0.0 as u8; + View::item_button(ui, rounding, COPY, None, || { + cb.copy_string_to_buffer(value.clone()); + }); + } - // Draw value information. - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - ui.add_space(3.0); - View::ellipsize_text(ui, value, 15.0, Colors::title(false)); - ui.label(RichText::new(label).size(15.0).color(Colors::gray())); - ui.add_space(3.0); - }); - }); - }); -} \ No newline at end of file + // Draw value information. + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(3.0); + View::ellipsize_text(ui, value, 15.0, Colors::title(false)); + ui.label(RichText::new(label).size(15.0).color(Colors::gray())); + ui.add_space(3.0); + }); + }); + }); +} diff --git a/src/gui/views/wallets/wallet/types.rs b/src/gui/views/wallets/wallet/types.rs index db50fda6..b3d98bc2 100644 --- a/src/gui/views/wallets/wallet/types.rs +++ b/src/gui/views/wallets/wallet/types.rs @@ -22,55 +22,60 @@ pub const GRIN: &str = "ツ"; /// Content container to simplify modals management and navigation. pub trait WalletContentContainer { - /// List of allowed [`Modal`] identifiers. - fn modal_ids(&self) -> Vec<&'static str>; - /// Draw modal content. - fn modal_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal, cb: &dyn PlatformCallbacks); - /// Draw container content. - fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks); - /// Draw content, to call by parent container. - fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - // Draw modal content. - if let Some(id) = Modal::opened() { - if self.modal_ids().contains(&id) { - Modal::ui(ui.ctx(), cb, |ui, modal, cb| { - self.modal_ui(ui, wallet, modal, cb); - }); - } - } - self.container_ui(ui, wallet, cb); - } + /// List of allowed [`Modal`] identifiers. + fn modal_ids(&self) -> Vec<&'static str>; + /// Draw modal content. + fn modal_ui( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks, + ); + /// Draw container content. + fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks); + /// Draw content, to call by parent container. + fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { + // Draw modal content. + if let Some(id) = Modal::opened() { + if self.modal_ids().contains(&id) { + Modal::ui(ui.ctx(), cb, |ui, modal, cb| { + self.modal_ui(ui, wallet, modal, cb); + }); + } + } + self.container_ui(ui, wallet, cb); + } } /// Get wallet status text. pub fn wallet_status_text(wallet: &Wallet) -> String { - if wallet.sync_error() && wallet.is_open() { - format!("{} {}", WARNING_CIRCLE, t!("error")) - } else if wallet.is_closing() { - format!("{} {}", SPINNER, t!("wallets.closing")) - } else if wallet.is_repairing() { - let repair_progress = wallet.repairing_progress(); - if repair_progress == 0 { - format!("{} {}", SPINNER, t!("wallets.checking")) - } else { - format!("{} {}: {}%", - SPINNER, - t!("wallets.checking"), - repair_progress) - } - } else if wallet.syncing() { - let info_progress = wallet.info_sync_progress(); - if info_progress == 100 || info_progress == 0 { - format!("{} {}", SPINNER, t!("wallets.loading")) - } else { - format!("{} {}: {}%", - SPINNER, - t!("wallets.loading"), - info_progress) - } - } else if wallet.is_open() { - format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked")) - } else { - format!("{} {}", FOLDER_LOCK, t!("wallets.locked")) - } -} \ No newline at end of file + if wallet.sync_error() && wallet.is_open() { + format!("{} {}", WARNING_CIRCLE, t!("error")) + } else if wallet.is_closing() { + format!("{} {}", SPINNER, t!("wallets.closing")) + } else if wallet.is_repairing() { + let repair_progress = wallet.repairing_progress(); + if repair_progress == 0 { + format!("{} {}", SPINNER, t!("wallets.checking")) + } else { + format!( + "{} {}: {}%", + SPINNER, + t!("wallets.checking"), + repair_progress + ) + } + } else if wallet.syncing() { + let info_progress = wallet.info_sync_progress(); + if info_progress == 100 || info_progress == 0 { + format!("{} {}", SPINNER, t!("wallets.loading")) + } else { + format!("{} {}: {}%", SPINNER, t!("wallets.loading"), info_progress) + } + } else if wallet.is_open() { + format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked")) + } else { + format!("{} {}", FOLDER_LOCK, t!("wallets.locked")) + } +} diff --git a/src/http/client.rs b/src/http/client.rs index 71817e7e..7ea01096 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -23,61 +23,69 @@ use serde::de::StdError; use crate::AppConfig; /// Handles http requests. -pub struct HttpClient { -} +pub struct HttpClient {} impl HttpClient { - /// Send request. - pub async fn send(req: Request) -> Result, Error> - where B: Body + Send + 'static + Unpin, ::Data: Send, - B::Data: Send, - B::Error: Into>, - { - if AppConfig::use_proxy() { - if let Some(url) = AppConfig::socks_proxy_url() { - Self::send_socks_proxy(url, req).await - } else { - Self::send_http_proxy(AppConfig::http_proxy_url().unwrap(), req).await - } - } else { - let client = Client::builder(TokioExecutor::new()) - .build::<_, B>(HttpsConnector::new()); - client.request(req).await - } - } + /// Send request. + pub async fn send(req: Request) -> Result, Error> + where + B: Body + Send + 'static + Unpin, + ::Data: Send, + B::Data: Send, + B::Error: Into>, + { + if AppConfig::use_proxy() { + if let Some(url) = AppConfig::socks_proxy_url() { + Self::send_socks_proxy(url, req).await + } else { + Self::send_http_proxy(AppConfig::http_proxy_url().unwrap(), req).await + } + } else { + let client = Client::builder(TokioExecutor::new()).build::<_, B>(HttpsConnector::new()); + client.request(req).await + } + } - /// Create socks proxy client. - pub async fn send_socks_proxy(proxy_url: String, req: Request) - -> Result, Error> - where B: Body + Send + 'static + Unpin, ::Data: Send, - B::Data: Send, - B::Error: Into>, - { - let connector = HttpsConnector::new(); - let uri = proxy_url.parse().unwrap(); - let proxy = hyper_socks2::SocksConnector { - proxy_addr: uri, - auth: None, - connector, - }.with_tls().unwrap(); - let client = Client::builder(TokioExecutor::new()) - .build::<_, B>(proxy); - client.request(req).await - } + /// Create socks proxy client. + pub async fn send_socks_proxy( + proxy_url: String, + req: Request, + ) -> Result, Error> + where + B: Body + Send + 'static + Unpin, + ::Data: Send, + B::Data: Send, + B::Error: Into>, + { + let connector = HttpsConnector::new(); + let uri = proxy_url.parse().unwrap(); + let proxy = hyper_socks2::SocksConnector { + proxy_addr: uri, + auth: None, + connector, + } + .with_tls() + .unwrap(); + let client = Client::builder(TokioExecutor::new()).build::<_, B>(proxy); + client.request(req).await + } - /// Create http proxy client. - pub async fn send_http_proxy(proxy_url: String, req: Request) - -> Result, Error> - where B: Body + Send + 'static + Unpin, ::Data: Send, - B::Data: Send, - B::Error: Into>, - { - let uri = proxy_url.parse().unwrap(); - let proxy = Proxy::new(Intercept::All, uri); - let connector = HttpsConnector::new(); - let proxy_connector = ProxyConnector::from_proxy(connector, proxy).unwrap(); - let client = Client::builder(TokioExecutor::new()) - .build::<_, B>(proxy_connector); - client.request(req).await - } -} \ No newline at end of file + /// Create http proxy client. + pub async fn send_http_proxy( + proxy_url: String, + req: Request, + ) -> Result, Error> + where + B: Body + Send + 'static + Unpin, + ::Data: Send, + B::Data: Send, + B::Error: Into>, + { + let uri = proxy_url.parse().unwrap(); + let proxy = Proxy::new(Intercept::All, uri); + let connector = HttpsConnector::new(); + let proxy_connector = ProxyConnector::from_proxy(connector, proxy).unwrap(); + let client = Client::builder(TokioExecutor::new()).build::<_, B>(proxy_connector); + client.request(req).await + } +} diff --git a/src/http/mod.rs b/src/http/mod.rs index e952fc1a..78270406 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -16,4 +16,4 @@ mod client; pub use client::*; mod release; -pub use release::*; \ No newline at end of file +pub use release::*; diff --git a/src/http/release.rs b/src/http/release.rs index ab95f5df..99e2363c 100644 --- a/src/http/release.rs +++ b/src/http/release.rs @@ -12,27 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::gui::views::View; +use crate::http::HttpClient; use bytes::Bytes; use chrono::NaiveDateTime; use egui::os::OperatingSystem; use http_body_util::{BodyExt, Empty}; use serde_derive::Deserialize; -use crate::gui::views::View; -use crate::http::HttpClient; #[derive(Deserialize)] pub struct ReleaseAsset { - pub name: String, - pub browser_download_url: String, - pub size: u64, + pub name: String, + pub browser_download_url: String, + pub size: u64, } #[derive(Deserialize)] pub struct ReleaseInfo { - pub tag_name: String, - pub body: String, - pub published_at: String, - pub assets: Vec, + pub tag_name: String, + pub body: String, + pub published_at: String, + pub assets: Vec, } #[cfg(target_arch = "x86_64")] @@ -50,129 +50,127 @@ const ARCH: &'static str = ARM_ARCH; const BASE_DOWNLOAD_URL: &'static str = "https://code.gri.mw/GUI/grim/releases/download/"; impl ReleaseInfo { - /// Get version number. - pub fn version(&self) -> String { - self.tag_name.replace("v", "") - } + /// Get version number. + pub fn version(&self) -> String { + self.tag_name.replace("v", "") + } - /// Get artifact release name based on current platform. - fn name(&self) -> Option { - let os = OperatingSystem::from_target_os(); - match os { - OperatingSystem::Unknown => None, - OperatingSystem::Android => { - let name = if ARCH == ARM_ARCH { - format!("grim-{}-android.apk", self.tag_name) - } else { - format!("grim-{}-android-x86_64.apk", self.tag_name) - }; - Some(name) - }, - OperatingSystem::IOS => None, - OperatingSystem::Nix => { - let name = if ARCH == ARM_ARCH { - format!("grim-{}-linux-arm.AppImage", self.tag_name) - } else { - format!("grim-{}-linux-x86_64.AppImage", self.tag_name) - }; - Some(name) - } - OperatingSystem::Mac => { - Some(format!("grim-{}-macos-universal.zip", self.tag_name)) - }, - OperatingSystem::Windows => { - if ARCH == ARM_ARCH { - None - } else { - Some(format!("grim-{}-win-x86_64.msi", self.tag_name)) - } - } - } - } + /// Get artifact release name based on current platform. + fn name(&self) -> Option { + let os = OperatingSystem::from_target_os(); + match os { + OperatingSystem::Unknown => None, + OperatingSystem::Android => { + let name = if ARCH == ARM_ARCH { + format!("grim-{}-android.apk", self.tag_name) + } else { + format!("grim-{}-android-x86_64.apk", self.tag_name) + }; + Some(name) + } + OperatingSystem::IOS => None, + OperatingSystem::Nix => { + let name = if ARCH == ARM_ARCH { + format!("grim-{}-linux-arm.AppImage", self.tag_name) + } else { + format!("grim-{}-linux-x86_64.AppImage", self.tag_name) + }; + Some(name) + } + OperatingSystem::Mac => Some(format!("grim-{}-macos-universal.zip", self.tag_name)), + OperatingSystem::Windows => { + if ARCH == ARM_ARCH { + None + } else { + Some(format!("grim-{}-win-x86_64.msi", self.tag_name)) + } + } + } + } - /// Get link to download the release. - pub fn url(&self) -> Option { - let base_url = format!("{}{}/", BASE_DOWNLOAD_URL, self.tag_name); - if let Some(name) = self.name() { - return Some(format!("{}{}", base_url, name)); - } - None - } + /// Get link to download the release. + pub fn url(&self) -> Option { + let base_url = format!("{}{}/", BASE_DOWNLOAD_URL, self.tag_name); + if let Some(name) = self.name() { + return Some(format!("{}{}", base_url, name)); + } + None + } - /// Get formatted release date. - pub fn date(&self) -> String { - let date = self.published_at.clone().replace("T", " ").replace("Z", ""); - let date_format = NaiveDateTime::parse_from_str(date.as_str(), "%Y-%m-%d %H:%M:%S"); - if let Ok(date) = date_format { - return View::format_time(date.and_utc().timestamp()); - } - date - } + /// Get formatted release date. + pub fn date(&self) -> String { + let date = self.published_at.clone().replace("T", " ").replace("Z", ""); + let date_format = NaiveDateTime::parse_from_str(date.as_str(), "%Y-%m-%d %H:%M:%S"); + if let Ok(date) = date_format { + return View::format_time(date.and_utc().timestamp()); + } + date + } - /// Get release size in megabytes. - pub fn size(&self) -> Option { - let name = self.name()?; - for a in &self.assets { - if a.name == name { - let size_mb = a.size as f64 / 1000000.0; - return Some(format!("{:.2}", size_mb)); - } - } - None - } + /// Get release size in megabytes. + pub fn size(&self) -> Option { + let name = self.name()?; + for a in &self.assets { + if a.name == name { + let size_mb = a.size as f64 / 1000000.0; + return Some(format!("{:.2}", size_mb)); + } + } + None + } - /// Check if release is update. - pub fn is_update(&self) -> bool { - let cur = crate::VERSION; - let ver = self.version(); - if cur == ver { - return false; - } - let cur_numbers: Vec = cur - .split(".") - .filter_map(|s| s.parse::().ok()) - .collect(); - let ver_numbers: Vec = ver - .split(".") - .filter_map(|s| s.parse::().ok()) - .collect(); - if cur_numbers.len() != ver_numbers.len() { - return true; - } - for (i, num) in ver_numbers.iter().enumerate() { - if num > &cur_numbers.get(i).unwrap() { - if i == 0 { - return true; - } else if i == 1 && cur_numbers.get(0).unwrap() == ver_numbers.get(0).unwrap() { - return true; - } else if i == 2 && cur_numbers.get(1).unwrap() == ver_numbers.get(1).unwrap() { - return true; - } - } - } - false - } + /// Check if release is update. + pub fn is_update(&self) -> bool { + let cur = crate::VERSION; + let ver = self.version(); + if cur == ver { + return false; + } + let cur_numbers: Vec = cur + .split(".") + .filter_map(|s| s.parse::().ok()) + .collect(); + let ver_numbers: Vec = ver + .split(".") + .filter_map(|s| s.parse::().ok()) + .collect(); + if cur_numbers.len() != ver_numbers.len() { + return true; + } + for (i, num) in ver_numbers.iter().enumerate() { + if num > &cur_numbers.get(i).unwrap() { + if i == 0 { + return true; + } else if i == 1 && cur_numbers.get(0).unwrap() == ver_numbers.get(0).unwrap() { + return true; + } else if i == 2 && cur_numbers.get(1).unwrap() == ver_numbers.get(1).unwrap() { + return true; + } + } + } + false + } } /// API endpoint to check last release. const REQUEST_URL: &'static str = "https://code.gri.mw/api/v1/repos/gui/grim/releases/latest"; pub async fn retrieve_release() -> Result { - let req = hyper::Request::builder() - .method(hyper::Method::GET) - .uri(REQUEST_URL) - .body(Empty::::new()) - .unwrap(); - if let Ok(resp) = HttpClient::send(req).await { - let status = resp.status().as_u16(); - if status == 200 { - if let Ok(body) = resp.into_body().collect().await { - let body_bytes = body.to_bytes(); - if let Ok(update_info) = serde_json::from_slice::(&body_bytes) { - return Ok(update_info); - } - } - } - } - Err("Error checking update".to_string()) -} \ No newline at end of file + let req = hyper::Request::builder() + .method(hyper::Method::GET) + .uri(REQUEST_URL) + .body(Empty::::new()) + .unwrap(); + if let Ok(resp) = HttpClient::send(req).await { + let status = resp.status().as_u16(); + if status == 200 { + if let Ok(body) = resp.into_body().collect().await { + let body_bytes = body.to_bytes(); + if let Ok(update_info) = serde_json::from_slice::(&body_bytes) { + return Ok(update_info); + } + } + } + } + Err("Error checking update".to_string()) +} diff --git a/src/lib.rs b/src/lib.rs index a90479dd..66a94a75 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,13 +33,13 @@ use crate::gui::views::View; use crate::gui::{App, Colors}; use crate::node::Node; -mod node; -mod wallet; -mod tor; -mod settings; -mod http; pub mod gui; +mod http; pub mod logger; +mod node; +mod settings; +mod tor; +mod wallet; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -48,225 +48,241 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); #[cfg(target_os = "android")] #[unsafe(no_mangle)] fn android_main(app: AndroidApp) { - // Setup logger. - logger::init_logger(); + // Setup logger. + logger::init_logger(); - use gui::platform::Android; - let platform = Android::new(app.clone()); - use winit::platform::android::EventLoopBuilderExtAndroid; + use gui::platform::Android; + let platform = Android::new(app.clone()); + use winit::platform::android::EventLoopBuilderExtAndroid; - // Setup system theme if not set. - if let None = AppConfig::dark_theme() { - let use_dark = use_dark_theme(&platform); - AppConfig::set_dark_theme(use_dark); - } + // Setup system theme if not set. + if let None = AppConfig::dark_theme() { + let use_dark = use_dark_theme(&platform); + AppConfig::set_dark_theme(use_dark); + } - let width = app.config().screen_width_dp().unwrap() as f32; - let height = app.config().screen_height_dp().unwrap() as f32; - let size = egui::emath::vec2(width, height); - let mut options = NativeOptions { - android_app: Some(app.clone()), - viewport: egui::ViewportBuilder::default().with_inner_size(size), - ..Default::default() - }; - options.event_loop_builder = Some(Box::new(move |builder| { - builder.with_android_app(app); - })); + let width = app.config().screen_width_dp().unwrap() as f32; + let height = app.config().screen_height_dp().unwrap() as f32; + let size = egui::emath::vec2(width, height); + let mut options = NativeOptions { + android_app: Some(app.clone()), + viewport: egui::ViewportBuilder::default().with_inner_size(size), + ..Default::default() + }; + options.event_loop_builder = Some(Box::new(move |builder| { + builder.with_android_app(app); + })); - let app = App::new(platform); - start(options, app_creator(app)).unwrap(); + let app = App::new(platform); + start(options, app_creator(app)).unwrap(); } /// Check if system is using dark theme. #[allow(dead_code)] #[cfg(target_os = "android")] fn use_dark_theme(platform: &gui::platform::Android) -> bool { - let res = platform.call_java_method("useDarkTheme", "()Z", &[]).unwrap(); - unsafe { res.z != 0 } + let res = platform + .call_java_method("useDarkTheme", "()Z", &[]) + .unwrap(); + unsafe { res.z != 0 } } /// [`App`] setup for [`eframe`]. pub fn app_creator(app: App) -> eframe::AppCreator<'static> -where App: eframe::App, T: PlatformCallbacks { - Box::new(|cc| { - // Setup images support. - egui_extras::install_image_loaders(&cc.egui_ctx); - Ok(Box::new(app)) - }) +where + App: eframe::App, + T: PlatformCallbacks, +{ + Box::new(|cc| { + // Setup images support. + egui_extras::install_image_loaders(&cc.egui_ctx); + Ok(Box::new(app)) + }) } /// Entry point to start ui with [`eframe`]. pub fn start(options: NativeOptions, app_creator: eframe::AppCreator) -> eframe::Result<()> { - // Setup translations. - setup_i18n(); - // Start integrated node if needed. - if AppConfig::autostart_node() { - Node::start(); - } - // Launch graphical interface. - eframe::run_native("Grim", options, app_creator) + // Setup translations. + setup_i18n(); + // Start integrated node if needed. + if AppConfig::autostart_node() { + Node::start(); + } + // Launch graphical interface. + eframe::run_native("Grim", options, app_creator) } /// Setup application [`egui::Style`] and [`egui::Visuals`]. pub fn setup_visuals(ctx: &Context) { - let use_dark = AppConfig::dark_theme().unwrap_or_else(|| { - let use_dark = ctx.system_theme().unwrap_or(Theme::Dark) == Theme::Dark; - AppConfig::set_dark_theme(use_dark); - use_dark - }); + let use_dark = AppConfig::dark_theme().unwrap_or_else(|| { + let use_dark = ctx.system_theme().unwrap_or(Theme::Dark) == Theme::Dark; + AppConfig::set_dark_theme(use_dark); + use_dark + }); - let mut style = (*ctx.style()).clone(); - // Setup selection. - style.interaction.selectable_labels = false; - style.interaction.multi_widget_text_select = false; - // Setup spacing for buttons. - if View::is_desktop() { - style.spacing.button_padding = egui::vec2(12.0, 8.0); - } else { - style.spacing.button_padding = egui::vec2(14.0, 10.0); - } - // Make scroll-bar thinner and lighter. - style.spacing.scroll.bar_width = 4.0; - style.spacing.scroll.bar_outer_margin = -2.0; - style.spacing.scroll.foreground_color = false; - // Disable spacing between items. - style.spacing.item_spacing = egui::vec2(0.0, 0.0); - style.spacing.text_edit_width = 500.0; - // Setup radio button/checkbox size and spacing. - style.spacing.icon_width = 24.0; - style.spacing.icon_width_inner = 14.0; - style.spacing.icon_spacing = 10.0; - // Setup style - ctx.set_style(style); + let mut style = (*ctx.style()).clone(); + // Setup selection. + style.interaction.selectable_labels = false; + style.interaction.multi_widget_text_select = false; + // Setup spacing for buttons. + if View::is_desktop() { + style.spacing.button_padding = egui::vec2(12.0, 8.0); + } else { + style.spacing.button_padding = egui::vec2(14.0, 10.0); + } + // Make scroll-bar thinner and lighter. + style.spacing.scroll.bar_width = 4.0; + style.spacing.scroll.bar_outer_margin = -2.0; + style.spacing.scroll.foreground_color = false; + // Disable spacing between items. + style.spacing.item_spacing = egui::vec2(0.0, 0.0); + style.spacing.text_edit_width = 500.0; + // Setup radio button/checkbox size and spacing. + style.spacing.icon_width = 24.0; + style.spacing.icon_width_inner = 14.0; + style.spacing.icon_spacing = 10.0; + // Setup style + ctx.set_style(style); - // Setup visuals based on app color theme. - let mut visuals = if use_dark { - egui::Visuals::dark() - } else { - egui::Visuals::light() - }; - // Setup selection color. - visuals.selection.stroke = Stroke { width: 1.0, color: Colors::text(false) }; - visuals.selection.bg_fill = Colors::gold(); - // Disable stroke around panels by default. - visuals.widgets.noninteractive.bg_stroke = Stroke::NONE; - // Setup stroke around inactive widgets. - visuals.widgets.inactive.bg_stroke = View::default_stroke(); - // Setup background and foreground stroke color for widgets like pull-to-refresher. - visuals.widgets.inactive.bg_fill = if use_dark { - Colors::white_or_black(false) - } else { - Colors::yellow() - }; - visuals.widgets.inactive.fg_stroke.color = Colors::item_button_text(); - // Setup visuals. - ctx.set_visuals(visuals); + // Setup visuals based on app color theme. + let mut visuals = if use_dark { + egui::Visuals::dark() + } else { + egui::Visuals::light() + }; + // Setup selection color. + visuals.selection.stroke = Stroke { + width: 1.0, + color: Colors::text(false), + }; + visuals.selection.bg_fill = Colors::gold(); + // Disable stroke around panels by default. + visuals.widgets.noninteractive.bg_stroke = Stroke::NONE; + // Setup stroke around inactive widgets. + visuals.widgets.inactive.bg_stroke = View::default_stroke(); + // Setup background and foreground stroke color for widgets like pull-to-refresher. + visuals.widgets.inactive.bg_fill = if use_dark { + Colors::white_or_black(false) + } else { + Colors::yellow() + }; + visuals.widgets.inactive.fg_stroke.color = Colors::item_button_text(); + // Setup visuals. + ctx.set_visuals(visuals); } /// Setup application fonts. pub fn setup_fonts(ctx: &Context) { - use egui::FontFamily::Proportional; + use egui::FontFamily::Proportional; - let mut fonts = egui::FontDefinitions::default(); + let mut fonts = egui::FontDefinitions::default(); - fonts.font_data.insert( - "phosphor".to_owned(), - Arc::new(egui::FontData::from_static(include_bytes!( - "../fonts/phosphor.ttf" - )).tweak(egui::FontTweak { - scale: 1.0, - y_offset_factor: -0.04, - y_offset: 0.0, - })) - ); - fonts - .families - .entry(Proportional) - .or_default() - .insert(0, "phosphor".to_owned()); + fonts.font_data.insert( + "phosphor".to_owned(), + Arc::new( + egui::FontData::from_static(include_bytes!("../fonts/phosphor.ttf")).tweak( + egui::FontTweak { + scale: 1.0, + y_offset_factor: -0.04, + y_offset: 0.0, + }, + ), + ), + ); + fonts + .families + .entry(Proportional) + .or_default() + .insert(0, "phosphor".to_owned()); - fonts.font_data.insert( - "noto".to_owned(), - Arc::new(egui::FontData::from_static(include_bytes!( - "../fonts/noto_sc_reg.otf" - )).tweak(egui::FontTweak { - scale: 1.0, - y_offset_factor: -0.08, - y_offset: 0.0, - })) - ); - fonts - .families - .entry(Proportional) - .or_default() - .insert(0, "noto".to_owned()); + fonts.font_data.insert( + "noto".to_owned(), + Arc::new( + egui::FontData::from_static(include_bytes!("../fonts/noto_sc_reg.otf")).tweak( + egui::FontTweak { + scale: 1.0, + y_offset_factor: -0.08, + y_offset: 0.0, + }, + ), + ), + ); + fonts + .families + .entry(Proportional) + .or_default() + .insert(0, "noto".to_owned()); - ctx.set_fonts(fonts); + ctx.set_fonts(fonts); - use egui::FontId; - use egui::TextStyle::*; + use egui::FontId; + use egui::TextStyle::*; - let mut style = (*ctx.style()).clone(); - style.text_styles = [ - (Heading, FontId::new(19.0, Proportional)), - (Body, FontId::new(16.0, Proportional)), - (Button, FontId::new(17.0, Proportional)), - (Small, FontId::new(15.0, Proportional)), - (Monospace, FontId::new(16.0, Proportional)), - ].into(); + let mut style = (*ctx.style()).clone(); + style.text_styles = [ + (Heading, FontId::new(19.0, Proportional)), + (Body, FontId::new(16.0, Proportional)), + (Button, FontId::new(17.0, Proportional)), + (Small, FontId::new(15.0, Proportional)), + (Monospace, FontId::new(16.0, Proportional)), + ] + .into(); - ctx.set_style(style); + ctx.set_style(style); } /// Setup translations. fn setup_i18n() { - // Set saved locale or get from system. - if let Some(lang) = AppConfig::locale() { - if rust_i18n::available_locales!().contains(&lang.as_str()) { - rust_i18n::set_locale(lang.as_str()); - } - } else { - let locale = sys_locale::get_locale().unwrap_or(String::from(AppConfig::DEFAULT_LOCALE)); - let locale_str = if locale.contains("-") { - locale.split("-").next().unwrap_or(AppConfig::DEFAULT_LOCALE) - } else { - locale.as_str() - }; + // Set saved locale or get from system. + if let Some(lang) = AppConfig::locale() { + if rust_i18n::available_locales!().contains(&lang.as_str()) { + rust_i18n::set_locale(lang.as_str()); + } + } else { + let locale = sys_locale::get_locale().unwrap_or(String::from(AppConfig::DEFAULT_LOCALE)); + let locale_str = if locale.contains("-") { + locale + .split("-") + .next() + .unwrap_or(AppConfig::DEFAULT_LOCALE) + } else { + locale.as_str() + }; - // Set best possible locale. - if rust_i18n::available_locales!().contains(&locale_str) { - rust_i18n::set_locale(locale_str); - } else { - rust_i18n::set_locale(AppConfig::DEFAULT_LOCALE); - } - } + // Set best possible locale. + if rust_i18n::available_locales!().contains(&locale_str) { + rust_i18n::set_locale(locale_str); + } else { + rust_i18n::set_locale(AppConfig::DEFAULT_LOCALE); + } + } } /// Get data from deeplink or opened file. pub fn consume_incoming_data() -> Option { - let has_data = { - let r_data = INCOMING_DATA.read(); - r_data.is_some() - }; - if has_data { - // Clear data. - let mut w_data = INCOMING_DATA.write(); - let data = w_data.clone(); - *w_data = None; - return data; - } - None + let has_data = { + let r_data = INCOMING_DATA.read(); + r_data.is_some() + }; + if has_data { + // Clear data. + let mut w_data = INCOMING_DATA.write(); + let data = w_data.clone(); + *w_data = None; + return data; + } + None } /// Provide data from deeplink or opened file. pub fn on_data(data: String) { - let mut w_data = INCOMING_DATA.write(); - *w_data = Some(data); + let mut w_data = INCOMING_DATA.write(); + *w_data = Some(data); } lazy_static! { - /// Data provided from deeplink or opened file. - pub static ref INCOMING_DATA: Arc>> = Arc::new(RwLock::new(None)); + /// Data provided from deeplink or opened file. + pub static ref INCOMING_DATA: Arc>> = Arc::new(RwLock::new(None)); } /// Callback from Java code with passed data. @@ -275,20 +291,20 @@ lazy_static! { #[cfg(target_os = "android")] #[unsafe(no_mangle)] pub extern "C" fn Java_mw_gri_android_MainActivity_onData( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - char: jni::sys::jstring + _env: jni::JNIEnv, + _class: jni::objects::JObject, + char: jni::sys::jstring, ) { - unsafe { - let j_obj = jni::objects::JString::from_raw(char); - if let Ok(j_str) = _env.get_string_unchecked(j_obj.as_ref()) { - match j_str.to_str() { - Ok(str) => { - let mut w_path = INCOMING_DATA.write(); - *w_path = Some(str.to_string()); - } - Err(_) => {} - } - }; - } -} \ No newline at end of file + unsafe { + let j_obj = jni::objects::JString::from_raw(char); + if let Ok(j_str) = _env.get_string_unchecked(j_obj.as_ref()) { + match j_str.to_str() { + Ok(str) => { + let mut w_path = INCOMING_DATA.write(); + *w_path = Some(str.to_string()); + } + Err(_) => {} + } + }; + } +} diff --git a/src/logger.rs b/src/logger.rs index 9a6dd1f9..d55878fe 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -12,21 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{panic, thread}; -use std::fs::File; use backtrace::Backtrace; +use log::{LevelFilter, Record, error}; +use log4rs::Config; use log4rs::append::Append; use log4rs::append::console::ConsoleAppender; +use log4rs::append::rolling_file::RollingFileAppender; use log4rs::append::rolling_file::policy::compound::CompoundPolicy; use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller; use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger; -use log4rs::append::rolling_file::RollingFileAppender; -use log4rs::Config; use log4rs::config::{Appender, Root}; use log4rs::encode::pattern::PatternEncoder; use log4rs::filter::threshold::ThresholdFilter; use log4rs::filter::{Filter, Response}; -use log::{error, LevelFilter, Record}; +use std::fs::File; +use std::{panic, thread}; use crate::Settings; @@ -39,7 +39,7 @@ const MAX_FILE_SIZE: u64 = 1024 * 1024 * 6; /// Include build information. pub mod built_info { - include!(concat!(env!("OUT_DIR"), "/built.rs")); + include!(concat!(env!("OUT_DIR"), "/built.rs")); } /// Filter is rejecting messages that doesn't start with "grin" or "grim" @@ -47,118 +47,122 @@ pub mod built_info { struct AppFilter; impl Filter for AppFilter { - fn filter(&self, record: &Record<'_>) -> Response { - if let Some(module_path) = record.module_path() { - if module_path.starts_with("grin") || module_path.starts_with("grim") - || module_path.starts_with("arti") { - return Response::Neutral; - } - } - Response::Reject - } + fn filter(&self, record: &Record<'_>) -> Response { + if let Some(module_path) = record.module_path() { + if module_path.starts_with("grin") + || module_path.starts_with("grim") + || module_path.starts_with("arti") + { + return Response::Neutral; + } + } + Response::Reject + } } /// Initialize the logger. pub fn init_logger() { - let stdout = ConsoleAppender::builder() - .encoder(Box::new(PatternEncoder::new(&LOGGING_PATTERN))) - .build(); + let stdout = ConsoleAppender::builder() + .encoder(Box::new(PatternEncoder::new(&LOGGING_PATTERN))) + .build(); - let mut root = Root::builder(); + let mut root = Root::builder(); - let level_filter = Box::new(ThresholdFilter::new(LevelFilter::Debug)); + let level_filter = Box::new(ThresholdFilter::new(LevelFilter::Debug)); - let mut app = vec![]; - app.push( - Appender::builder() - .filter(level_filter.clone()) - .filter(Box::new(AppFilter)) - .build("stdout", Box::new(stdout)), - ); - root = root.appender("stdout"); + let mut app = vec![]; + app.push( + Appender::builder() + .filter(level_filter.clone()) + .filter(Box::new(AppFilter)) + .build("stdout", Box::new(stdout)), + ); + root = root.appender("stdout"); - // Setup file logging. - let file: Box = { - let path = Settings::log_path(); - let roller = FixedWindowRoller::builder() - .build(&format!("{}.{{}}.gz", path), ROTATE_LOG_FILES) - .unwrap(); - let trigger = SizeTrigger::new(MAX_FILE_SIZE); - let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller)); - Box::new( - RollingFileAppender::builder() - .append(true) - .encoder(Box::new(PatternEncoder::new(&LOGGING_PATTERN))) - .build(path, Box::new(policy)) - .expect("Failed to create logfile"), - ) - }; - app.push( - Appender::builder() - .filter(level_filter) - .filter(Box::new(AppFilter)) - .build("file", file), - ); - root = root.appender("file"); + // Setup file logging. + let file: Box = { + let path = Settings::log_path(); + let roller = FixedWindowRoller::builder() + .build(&format!("{}.{{}}.gz", path), ROTATE_LOG_FILES) + .unwrap(); + let trigger = SizeTrigger::new(MAX_FILE_SIZE); + let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller)); + Box::new( + RollingFileAppender::builder() + .append(true) + .encoder(Box::new(PatternEncoder::new(&LOGGING_PATTERN))) + .build(path, Box::new(policy)) + .expect("Failed to create logfile"), + ) + }; + app.push( + Appender::builder() + .filter(level_filter) + .filter(Box::new(AppFilter)) + .build("file", file), + ); + root = root.appender("file"); - let config = Config::builder() - .appenders(app) - .build(root.build(LevelFilter::Debug)) - .unwrap(); - let _ = log4rs::init_config(config).unwrap(); + let config = Config::builder() + .appenders(app) + .build(root.build(LevelFilter::Debug)) + .unwrap(); + let _ = log4rs::init_config(config).unwrap(); - log::info!("{}", build_info()); + log::info!("{}", build_info()); - send_panic_to_log(); + send_panic_to_log(); } /// Get information about application build. fn build_info() -> String { - format!( - "This is Grim version {}, built for {} by {}.", - built_info::PKG_VERSION, - built_info::TARGET, - built_info::RUSTC_VERSION, - ) + format!( + "This is Grim version {}, built for {} by {}.", + built_info::PKG_VERSION, + built_info::TARGET, + built_info::RUSTC_VERSION, + ) } /// Hook to send panics to logs as well as stderr. fn send_panic_to_log() { - panic::set_hook(Box::new(|info| { - let backtrace = Backtrace::new(); + panic::set_hook(Box::new(|info| { + let backtrace = Backtrace::new(); - let thread = thread::current(); - let thread = thread.name().unwrap_or("unnamed"); + let thread = thread::current(); + let thread = thread.name().unwrap_or("unnamed"); - let msg = match info.payload().downcast_ref::<&'static str>() { - Some(s) => *s, - None => match info.payload().downcast_ref::() { - Some(s) => &**s, - None => "Box", - }, - }; + let msg = match info.payload().downcast_ref::<&'static str>() { + Some(s) => *s, + None => match info.payload().downcast_ref::() { + Some(s) => &**s, + None => "Box", + }, + }; - match info.location() { - Some(location) => { - error!( + match info.location() { + Some(location) => { + error!( "{}\nThread '{}' panicked at '{}': {}:{}{:?}\n\n", - build_info(), + build_info(), thread, msg, location.file(), location.line(), backtrace ); - } - None => error!("Thread '{}' panicked at '{}'{:?}", thread, msg, backtrace), - } - // Also print to stderr. - eprintln!( - "Thread '{}' panicked with message:\n\"{}\"\nSee {} for further details.", - thread, msg, Settings::log_path() - ); - // Create file to show report send on launch. - let log = Settings::crash_check_path(); - let _ = File::create(log); - })); -} \ No newline at end of file + } + None => error!("Thread '{}' panicked at '{}'{:?}", thread, msg, backtrace), + } + // Also print to stderr. + eprintln!( + "Thread '{}' panicked with message:\n\"{}\"\nSee {} for further details.", + thread, + msg, + Settings::log_path() + ); + // Create file to show report send on launch. + let log = Settings::crash_check_path(); + let _ = File::create(log); + })); +} diff --git a/src/main.rs b/src/main.rs index fd54a8c8..f0a2e9fc 100755 --- a/src/main.rs +++ b/src/main.rs @@ -15,229 +15,224 @@ #![windows_subsystem = "windows"] pub fn main() { - #[allow(dead_code)] - #[cfg(not(target_os = "android"))] - real_main(); + #[allow(dead_code)] + #[cfg(not(target_os = "android"))] + real_main(); } #[allow(dead_code)] #[cfg(not(target_os = "android"))] fn real_main() { - // Initialize logger. - grim::logger::init_logger(); - // Handle file path argument passing. - let args: Vec<_> = std::env::args().collect(); - let mut data = None; - if args.len() > 1 { - let path = std::path::PathBuf::from(&args[1]); - let content = match std::fs::read_to_string(path) { - Ok(s) => Some(s), - Err(_) => Some(args[1].clone()) - }; - data = content - } + // Initialize logger. + grim::logger::init_logger(); + // Handle file path argument passing. + let args: Vec<_> = std::env::args().collect(); + let mut data = None; + if args.len() > 1 { + let path = std::path::PathBuf::from(&args[1]); + let content = match std::fs::read_to_string(path) { + Ok(s) => Some(s), + Err(_) => Some(args[1].clone()), + }; + data = content + } - // Start GUI. - if is_app_running(&data) { - return; - } else if let Some(data) = data { - grim::on_data(data); - } - let platform = grim::gui::platform::Desktop::new(); - start_app_socket(platform.clone()); - start_desktop_gui(platform); + // Start GUI. + if is_app_running(&data) { + return; + } else if let Some(data) = data { + grim::on_data(data); + } + let platform = grim::gui::platform::Desktop::new(); + start_app_socket(platform.clone()); + start_desktop_gui(platform); } /// Get panic message from crash payload. #[allow(dead_code)] #[cfg(not(target_os = "android"))] fn panic_info_message<'pi>(panic_info: &'pi std::panic::PanicHookInfo<'_>) -> &'pi str { - let payload = panic_info.payload(); - // taken from: https://github.com/rust-lang/rust/blob/4b9f4b221b92193c7e95b1beb502c6eb32c3b613/library/std/src/panicking.rs#L194-L200 - match payload.downcast_ref::<&'static str>() { - Some(msg) => *msg, - None => match payload.downcast_ref::() { - Some(msg) => msg.as_str(), - // Copy what rustc does in the default panic handler - None => "Box", - }, - } + let payload = panic_info.payload(); + // taken from: https://github.com/rust-lang/rust/blob/4b9f4b221b92193c7e95b1beb502c6eb32c3b613/library/std/src/panicking.rs#L194-L200 + match payload.downcast_ref::<&'static str>() { + Some(msg) => *msg, + None => match payload.downcast_ref::() { + Some(msg) => msg.as_str(), + // Copy what rustc does in the default panic handler + None => "Box", + }, + } } /// Start GUI with Desktop related setup passing data from opening. #[allow(dead_code)] #[cfg(not(target_os = "android"))] fn start_desktop_gui(platform: grim::gui::platform::Desktop) { - use grim::AppConfig; - let os = egui::os::OperatingSystem::from_target_os(); - let (width, height) = AppConfig::window_size(); - let mut viewport = egui::ViewportBuilder::default() - .with_min_inner_size([AppConfig::MIN_WIDTH, AppConfig::MIN_HEIGHT]) - .with_inner_size([width, height]); + use grim::AppConfig; + let os = egui::os::OperatingSystem::from_target_os(); + let (width, height) = AppConfig::window_size(); + let mut viewport = egui::ViewportBuilder::default() + .with_min_inner_size([AppConfig::MIN_WIDTH, AppConfig::MIN_HEIGHT]) + .with_inner_size([width, height]); - // Setup icon. - if let Ok(icon) = eframe::icon_data::from_png_bytes(include_bytes!("../img/icon.png")) { - viewport = viewport.with_icon(std::sync::Arc::new(icon)); - } - // Setup window position. - if let Some((x, y)) = AppConfig::window_pos() { - viewport = viewport.with_position(egui::pos2(x, y)); - } - // Setup window decorations. - let is_mac = os == egui::os::OperatingSystem::Mac; - let is_win = os == egui::os::OperatingSystem::Windows; - viewport = viewport - .with_fullsize_content_view(true) - .with_window_level(egui::WindowLevel::Normal) - .with_title_shown(is_win) - .with_titlebar_buttons_shown(is_win) - .with_titlebar_shown(is_win) - .with_transparent(true) - .with_decorations(is_mac || is_win); + // Setup icon. + if let Ok(icon) = eframe::icon_data::from_png_bytes(include_bytes!("../img/icon.png")) { + viewport = viewport.with_icon(std::sync::Arc::new(icon)); + } + // Setup window position. + if let Some((x, y)) = AppConfig::window_pos() { + viewport = viewport.with_position(egui::pos2(x, y)); + } + // Setup window decorations. + let is_mac = os == egui::os::OperatingSystem::Mac; + let is_win = os == egui::os::OperatingSystem::Windows; + viewport = viewport + .with_fullsize_content_view(true) + .with_window_level(egui::WindowLevel::Normal) + .with_title_shown(is_win) + .with_titlebar_buttons_shown(is_win) + .with_titlebar_shown(is_win) + .with_transparent(true) + .with_decorations(is_mac || is_win); - let mut options = eframe::NativeOptions { - renderer: eframe::Renderer::Glow, - viewport, - ..Default::default() - }; + let mut options = eframe::NativeOptions { + renderer: eframe::Renderer::Glow, + viewport, + ..Default::default() + }; - // Start GUI. - let app = grim::gui::App::new(platform.clone()); - match grim::start(options.clone(), grim::app_creator(app)) { - Ok(_) => {} - Err(_) => { - // Start with another renderer on error. - options.renderer = eframe::Renderer::Wgpu; + // Start GUI. + let app = grim::gui::App::new(platform.clone()); + match grim::start(options.clone(), grim::app_creator(app)) { + Ok(_) => {} + Err(_) => { + // Start with another renderer on error. + options.renderer = eframe::Renderer::Wgpu; - let app = grim::gui::App::new(platform); - match grim::start(options, grim::app_creator(app)) { - Ok(_) => {} - Err(e) => { - panic!("{}", e); - } - } - } - } + let app = grim::gui::App::new(platform); + match grim::start(options, grim::app_creator(app)) { + Ok(_) => {} + Err(e) => { + panic!("{}", e); + } + } + } + } } /// Check if application is already running to pass data. #[allow(dead_code)] #[cfg(not(target_os = "android"))] fn is_app_running(data: &Option) -> bool { - let res: Result<(), Box> = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(async { - use interprocess::local_socket::{ - tokio::{prelude::*, Stream} - }; - use tokio::{ - io::AsyncWriteExt, - }; + let res: Result<(), Box> = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + use interprocess::local_socket::tokio::{Stream, prelude::*}; + use tokio::io::AsyncWriteExt; - let socket_path = grim::Settings::socket_path(); - let name = socket_name(&socket_path)?; + let socket_path = grim::Settings::socket_path(); + let name = socket_name(&socket_path)?; - // Connect to running application socket. - let conn = Stream::connect(name).await?; - let data = data.clone().unwrap_or("".to_string()); - if data.is_empty() { - return Ok(()); - } - let (rec, mut sen) = conn.split(); + // Connect to running application socket. + let conn = Stream::connect(name).await?; + let data = data.clone().unwrap_or("".to_string()); + if data.is_empty() { + return Ok(()); + } + let (rec, mut sen) = conn.split(); - // Send data to socket. - let _ = sen.write_all(data.as_bytes()).await; + // Send data to socket. + let _ = sen.write_all(data.as_bytes()).await; - drop((rec, sen)); - Ok(()) - }); - match res { - Ok(_) => true, - Err(_) => false - } + drop((rec, sen)); + Ok(()) + }); + match res { + Ok(_) => true, + Err(_) => false, + } } /// Start desktop socket that handles data for single application instance. #[allow(dead_code)] #[cfg(not(target_os = "android"))] fn start_app_socket(platform: grim::gui::platform::Desktop) { - std::thread::spawn(move || { - let _ = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(async { - use interprocess::local_socket::{ - tokio::{prelude::*, Stream}, - Listener, ListenerOptions, - }; - use std::io; - use tokio::{ - io::{AsyncBufReadExt, BufReader}, - }; - use grim::gui::platform::PlatformCallbacks; + std::thread::spawn(move || { + let _ = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + use grim::gui::platform::PlatformCallbacks; + use interprocess::local_socket::{ + Listener, ListenerOptions, + tokio::{Stream, prelude::*}, + }; + use std::io; + use tokio::io::{AsyncBufReadExt, BufReader}; - // Handle incoming connection. - async fn handle_conn(conn: Stream) - -> io::Result { - let mut read = BufReader::new(&conn); - let mut buffer = String::new(); - // Read data. - let _ = read.read_line(&mut buffer).await; - Ok(buffer) - } + // Handle incoming connection. + async fn handle_conn(conn: Stream) -> io::Result { + let mut read = BufReader::new(&conn); + let mut buffer = String::new(); + // Read data. + let _ = read.read_line(&mut buffer).await; + Ok(buffer) + } - // Setup socket name. - let socket_path = grim::Settings::socket_path(); - if socket_path.exists() { - let _ = std::fs::remove_file(&socket_path); - } - let name = socket_name(&socket_path)?; + // Setup socket name. + let socket_path = grim::Settings::socket_path(); + if socket_path.exists() { + let _ = std::fs::remove_file(&socket_path); + } + let name = socket_name(&socket_path)?; - // Create listener. - let opts = ListenerOptions::new().name(name); - let listener = match opts.create_tokio() { - Err(e) if e.kind() == io::ErrorKind::AddrInUse => { - log::error!("Socket file is occupied."); - return Err::(e); - } - x => x?, - }; + // Create listener. + let opts = ListenerOptions::new().name(name); + let listener = match opts.create_tokio() { + Err(e) if e.kind() == io::ErrorKind::AddrInUse => { + log::error!("Socket file is occupied."); + return Err::(e); + } + x => x?, + }; - loop { - let conn = match listener.accept().await { - Ok(c) => c, - Err(e) => { - log::error!("{:?}", e); - continue - } - }; - // Handle connection. - let res = handle_conn(conn).await; - match res { - Ok(data) => { - grim::on_data(data); - platform.request_user_attention(); - }, - Err(_) => {} - } - } - }); - }); + loop { + let conn = match listener.accept().await { + Ok(c) => c, + Err(e) => { + log::error!("{:?}", e); + continue; + } + }; + // Handle connection. + let res = handle_conn(conn).await; + match res { + Ok(data) => { + grim::on_data(data); + platform.request_user_attention(); + } + Err(_) => {} + } + } + }); + }); } /// Get application socket name from provided path. #[allow(dead_code)] #[cfg(not(target_os = "android"))] fn socket_name(path: &std::path::PathBuf) -> std::io::Result> { - use interprocess::local_socket::{NameType, ToFsName, ToNsName}; - let name = if egui::os::OperatingSystem::Mac != egui::os::OperatingSystem::from_target_os() && - interprocess::local_socket::GenericNamespaced::is_supported() { - grim::Settings::SOCKET_NAME.to_ns_name::()? - } else { - path.clone().to_fs_name::()? - }; - Ok(name) -} \ No newline at end of file + use interprocess::local_socket::{NameType, ToFsName, ToNsName}; + let name = if egui::os::OperatingSystem::Mac != egui::os::OperatingSystem::from_target_os() + && interprocess::local_socket::GenericNamespaced::is_supported() + { + grim::Settings::SOCKET_NAME.to_ns_name::()? + } else { + path.clone() + .to_fs_name::()? + }; + Ok(name) +} diff --git a/src/node/config.rs b/src/node/config.rs index 57404da8..04286c5a 100644 --- a/src/node/config.rs +++ b/src/node/config.rs @@ -12,956 +12,1048 @@ // See the License for the specific language governing permissions and // limitations under the License. +use local_ip_address::list_afinet_netifas; +use serde::{Deserialize, Serialize}; use std::fs::File; use std::io::{BufRead, BufReader, Write}; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, ToSocketAddrs}; use std::path::PathBuf; use std::str::FromStr; -use local_ip_address::list_afinet_netifas; -use serde::{Deserialize, Serialize}; -use grin_config::{config, ConfigError, ConfigMembers, GlobalConfig}; -use grin_config::config::{API_SECRET_FILE_NAME, FOREIGN_API_SECRET_FILE_NAME, SERVER_CONFIG_FILE_NAME}; +use grin_config::config::{ + API_SECRET_FILE_NAME, FOREIGN_API_SECRET_FILE_NAME, SERVER_CONFIG_FILE_NAME, +}; +use grin_config::{ConfigError, ConfigMembers, GlobalConfig, config}; use grin_core::global::ChainTypes; -use grin_p2p::{PeerAddr, Seeding}; use grin_p2p::msg::PeerAddrs; +use grin_p2p::{PeerAddr, Seeding}; use grin_servers::common::types::ChainValidationMode; use rand::Rng; -use crate::{AppConfig, Settings}; use crate::node::Node; +use crate::{AppConfig, Settings}; /// Peers config to save peers DNS names into the file. #[derive(Serialize, Deserialize, Default)] pub struct PeersConfig { - seeds: Vec, - allowed: Vec, - denied: Vec, - preferred: Vec + seeds: Vec, + allowed: Vec, + denied: Vec, + preferred: Vec, } impl PeersConfig { - /// File name for peers config. - pub const FILE_NAME: &'static str = "peers.toml"; + /// File name for peers config. + pub const FILE_NAME: &'static str = "peers.toml"; - /// Save peers config to the file. - pub fn save(&self) { - let chain_type = AppConfig::chain_type(); - let config_path = Settings::config_path(Self::FILE_NAME, Some(chain_type.shortname())); - Settings::write_to_file(self, config_path); - } + /// Save peers config to the file. + pub fn save(&self) { + let chain_type = AppConfig::chain_type(); + let config_path = Settings::config_path(Self::FILE_NAME, Some(chain_type.shortname())); + Settings::write_to_file(self, config_path); + } - /// Convert string to [`PeerAddr`] if address is in correct format (`host:port`) and available. - pub fn peer_to_addr(peer: String) -> Option { - match SocketAddr::from_str(peer.as_str()) { - // Try to parse IP address first. - Ok(ip) => Some(PeerAddr(ip)), - // If that fails it's probably a DNS record. - Err(_) => { - if let Ok(mut socket_addr_list) = peer.to_socket_addrs() { - if let Some(addr) = socket_addr_list.next() { - return Some(PeerAddr(addr)); - } - } - None - } - } - } + /// Convert string to [`PeerAddr`] if address is in correct format (`host:port`) and available. + pub fn peer_to_addr(peer: String) -> Option { + match SocketAddr::from_str(peer.as_str()) { + // Try to parse IP address first. + Ok(ip) => Some(PeerAddr(ip)), + // If that fails it's probably a DNS record. + Err(_) => { + if let Ok(mut socket_addr_list) = peer.to_socket_addrs() { + if let Some(addr) = socket_addr_list.next() { + return Some(PeerAddr(addr)); + } + } + None + } + } + } - /// Load saved peers to node server [`ConfigMembers`] config. - pub fn load_to_server_config(config: &mut ConfigMembers) { - // Load seeds. - let r_config = Settings::node_config_to_read(); - for seed in r_config.peers.seeds.clone() { - if let Some(p) = Self::peer_to_addr(seed.to_string()) { - let mut seeds = config - .server - .p2p_config - .seeds - .clone() - .unwrap_or(PeerAddrs::default()); - seeds.peers.insert(seeds.peers.len(), p); - config.server.p2p_config.seeds = Some(seeds); - } - } - // Load allowed peers. - for peer in r_config.peers.allowed.clone() { - if let Some(p) = Self::peer_to_addr(peer.clone()) { - let mut allowed = config - .server - .p2p_config - .peers_allow - .clone() - .unwrap_or(PeerAddrs::default()); - allowed.peers.insert(allowed.peers.len(), p); - config.server.p2p_config.peers_allow = Some(allowed); - } - } - // Load denied peers. - for peer in r_config.peers.denied.clone() { - if let Some(p) = Self::peer_to_addr(peer.clone()) { - let mut denied = config - .server - .p2p_config - .peers_deny - .clone() - .unwrap_or(PeerAddrs::default()); - denied.peers.insert(denied.peers.len(), p); - config.server.p2p_config.peers_deny = Some(denied); - } - } - // Load preferred peers. - for peer in &r_config.peers.preferred.clone() { - if let Some(p) = Self::peer_to_addr(peer.clone()) { - let mut preferred = config - .server - .p2p_config - .peers_preferred - .clone() - .unwrap_or(PeerAddrs::default()); - preferred.peers.insert(preferred.peers.len(), p); - config.server.p2p_config.peers_preferred = Some(preferred); - } - } - } + /// Load saved peers to node server [`ConfigMembers`] config. + pub fn load_to_server_config(config: &mut ConfigMembers) { + // Load seeds. + let r_config = Settings::node_config_to_read(); + for seed in r_config.peers.seeds.clone() { + if let Some(p) = Self::peer_to_addr(seed.to_string()) { + let mut seeds = config + .server + .p2p_config + .seeds + .clone() + .unwrap_or(PeerAddrs::default()); + seeds.peers.insert(seeds.peers.len(), p); + config.server.p2p_config.seeds = Some(seeds); + } + } + // Load allowed peers. + for peer in r_config.peers.allowed.clone() { + if let Some(p) = Self::peer_to_addr(peer.clone()) { + let mut allowed = config + .server + .p2p_config + .peers_allow + .clone() + .unwrap_or(PeerAddrs::default()); + allowed.peers.insert(allowed.peers.len(), p); + config.server.p2p_config.peers_allow = Some(allowed); + } + } + // Load denied peers. + for peer in r_config.peers.denied.clone() { + if let Some(p) = Self::peer_to_addr(peer.clone()) { + let mut denied = config + .server + .p2p_config + .peers_deny + .clone() + .unwrap_or(PeerAddrs::default()); + denied.peers.insert(denied.peers.len(), p); + config.server.p2p_config.peers_deny = Some(denied); + } + } + // Load preferred peers. + for peer in &r_config.peers.preferred.clone() { + if let Some(p) = Self::peer_to_addr(peer.clone()) { + let mut preferred = config + .server + .p2p_config + .peers_preferred + .clone() + .unwrap_or(PeerAddrs::default()); + preferred.peers.insert(preferred.peers.len(), p); + config.server.p2p_config.peers_preferred = Some(preferred); + } + } + } } /// Wrapped node config to be used by [`grin_servers::Server`]. #[derive(Serialize, Deserialize)] pub struct NodeConfig { - pub(crate) node: ConfigMembers, - pub(crate) peers: PeersConfig + pub(crate) node: ConfigMembers, + pub(crate) peers: PeersConfig, } impl NodeConfig { - /// Initialize config fields from provided [`ChainTypes`]. - pub fn for_chain_type(chain_type: &ChainTypes) -> Self { - // Check secret files for current chain type. - let _ = Self::check_api_secret_files(chain_type, API_SECRET_FILE_NAME); - let _ = Self::check_api_secret_files(chain_type, FOREIGN_API_SECRET_FILE_NAME); - - // Initialize peers config. - let peers_config = { - let sub_dir = Some(chain_type.shortname()); - let path = Settings::config_path(PeersConfig::FILE_NAME, sub_dir); - let config = Settings::read_from_file::(path.clone()); - if !path.exists() || config.is_err() { - Self::save_default_peers_config(chain_type) - } else { - config.unwrap() - } - }; - - // Initialize node config. - let node_config = { - let sub_dir = Some(chain_type.shortname()); - let path = Settings::config_path(SERVER_CONFIG_FILE_NAME, sub_dir); - let config = Settings::read_from_file::(path.clone()); - if !path.exists() || config.is_err() { - Self::save_default_node_server_config(chain_type) - } else { - config.unwrap() - } - }; - - Self { node: node_config, peers: peers_config } - } - - /// Save default node config for specified [`ChainTypes`]. - fn save_default_node_server_config(chain_type: &ChainTypes) -> ConfigMembers { - let sub_dir = Some(chain_type.shortname()); - let path = Settings::config_path(SERVER_CONFIG_FILE_NAME, sub_dir.clone()); - - let mut default_config = GlobalConfig::for_chain(chain_type); - default_config.update_paths(&Settings::base_path(sub_dir)); - let mut config = default_config.members.unwrap(); - - // Generate random p2p and api ports. - Self::setup_default_ports(&mut config); - - // Clear wallet listener url (actually it will be wallet id). - config.server.stratum_mining_config.clone().unwrap().wallet_listener_url = "".to_string(); - - Settings::write_to_file(&config, path); - config - } - - /// Generate random p2p and api ports in ranges based on [`ChainTypes`]. - fn setup_default_ports(config: &mut ConfigMembers) { - let (api, p2p) = match config.server.chain_type { - ChainTypes::Mainnet => { - let api = rand::rng().random_range(30000..33000); - let p2p = rand::rng().random_range(33000..37000); - (api, p2p) - }, - _ => { - let api = rand::rng().random_range(40000..43000); - let p2p = rand::rng().random_range(43000..47000); - (api, p2p) - } - }; - let api_addr = config.server.api_http_addr.split_once(":").unwrap().0; - config.server.api_http_addr = format!("{}:{}", api_addr, api); - config.server.p2p_config.port = p2p; - } - - /// Save default peers config for specified [`ChainTypes`]. - fn save_default_peers_config(chain_type: &ChainTypes) -> PeersConfig { - let sub_dir = Some(chain_type.shortname()); - let path = Settings::config_path(PeersConfig::FILE_NAME, sub_dir); - let config = PeersConfig::default(); - Settings::write_to_file(&config, path); - config - } - - /// Save node config to the file. - pub fn save(&self) { - let sub_dir = Some(self.node.server.chain_type.shortname()); - let config_path = Settings::config_path(SERVER_CONFIG_FILE_NAME, sub_dir); - Settings::write_to_file(&self.node, config_path); - } - - /// Get server config to use for node server before start. - pub fn node_server_config() -> ConfigMembers { - let r_config = Settings::node_config_to_read(); - r_config.node.clone() - } - - /// Reset node config to default values. - pub fn reset_to_default() { - let chain_type = { - let r_config = Settings::node_config_to_read(); - r_config.node.server.chain_type - }; - let node_server_config = Self::save_default_node_server_config(&chain_type); - let peers_config = Self::save_default_peers_config(&chain_type); - { - let mut w_config = Settings::node_config_to_update(); - w_config.node = node_server_config; - w_config.peers = peers_config; - } - } - - /// Check that the api secret files exist and are valid. - fn check_api_secret_files( - chain_type: &ChainTypes, - secret_file_name: &str, - ) -> Result<(), ConfigError> { - let api_secret_path = Self::get_secret_path(chain_type, secret_file_name); - if !api_secret_path.exists() { - config::init_api_secret(&api_secret_path) - } else { - config::check_api_secret(&api_secret_path) - } - } - - /// Get path for secret file. - fn get_secret_path(chain_type: &ChainTypes, secret_file_name: &str) -> PathBuf { - let sub_dir = Some(chain_type.shortname()); - let grin_path = Settings::base_path(sub_dir); - let mut api_secret_path = grin_path; - api_secret_path.push(secret_file_name); - api_secret_path - } - - /// List of available IP addresses. - pub fn get_ip_addrs() -> Vec { - let mut ip_addrs = Vec::new(); - let network_interfaces = list_afinet_netifas(); - if let Ok(network_interfaces) = network_interfaces { - for (_, ip) in network_interfaces.iter() { - if ip.is_ipv4() { - ip_addrs.push(ip.to_string()); - } - } - } - ip_addrs - } - - /// Check whether a port is available on the provided host. - fn is_host_port_available(host: &String, port: &String) -> bool { - if let Ok(p) = port.parse::() { - let ip_addr = Ipv4Addr::from_str(host.as_str()).unwrap(); - let ipv4 = SocketAddrV4::new(ip_addr, p); - return TcpListener::bind(ipv4).is_ok(); - } - false - } - - /// Check whether a port is available across the system at all hosts. - fn is_port_available(port: &String) -> bool { - if let Ok(p) = port.parse::() { - for ip in Self::get_ip_addrs() { - let ip_addr = Ipv4Addr::from_str(ip.as_str()).unwrap(); - let ipv4 = SocketAddrV4::new(ip_addr, p); - if TcpListener::bind(ipv4).is_err() { - return false; - } - } - } else { - return false; - } - true - } - - /// Get chain data path. - pub fn get_chain_data_path() -> String { - let r_config = Settings::node_config_to_read(); - r_config.node.server.db_root.clone() - } - - /// Save chain data path. - pub fn save_chain_data_path(path: String) { - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.db_root = path; - w_config.save(); - } - - /// Get stratum server IP address and port. - pub fn get_stratum_address() -> (String, String) { - let r_config = Settings::node_config_to_read(); - let saved_stratum_addr = r_config - .node - .server - .stratum_mining_config - .as_ref() - .unwrap() - .stratum_server_addr - .as_ref() - .unwrap(); - let (addr, port) = saved_stratum_addr.split_once(":").unwrap(); - (addr.into(), port.into()) - } - - /// Save stratum server IP address and port. - pub fn save_stratum_address(addr: &String, port: &String) { - let addr_to_save = format!("{}:{}", addr, port); - let mut w_config = Settings::node_config_to_update(); - w_config - .node - .server - .stratum_mining_config - .as_mut() - .unwrap() - .stratum_server_addr = Some(addr_to_save); - w_config.save(); - } - - /// Check if stratum server port is available across the system and config. - pub fn is_stratum_port_available(ip: &String, port: &String) -> bool { - if Node::get_stratum_stats().is_running { - // Check if Stratum server with same address is running. - let (cur_ip, cur_port) = Self::get_stratum_address(); - let same_running = ip == &cur_ip && port == &cur_port; - return same_running || Self::is_not_running_stratum_port_available(ip, port); - } - Self::is_not_running_stratum_port_available(&ip, &port) - } - - /// Check if stratum port is available when server is not running. - fn is_not_running_stratum_port_available(ip: &String, port: &String) -> bool { - if Self::is_host_port_available(&ip, &port) { - if &Self::get_p2p_port() != port { - let (api_ip, api_port) = Self::get_api_ip_port(); - return if &api_ip == ip { - &api_port != port - } else { - true - }; - } - } - false - } - - /// Get stratum mining server wallet address to get rewards. - pub fn get_stratum_wallet_id() -> Option { - let r_config = Settings::node_config_to_read(); - let id = r_config.node.clone().server.stratum_mining_config.unwrap().wallet_listener_url; - return if id.is_empty() { - None - } else { - if let Ok(id) = id.parse::() { - Some(id) - } else { - None - } - } - } - - /// Save stratum mining server wallet address to get rewards. - pub fn save_stratum_wallet_id(id: i64) { - let mut w_config = Settings::node_config_to_update(); - w_config.node - .server - .stratum_mining_config - .as_mut() - .unwrap() - .wallet_listener_url = id.to_string(); - w_config.save(); - } - - /// Get the amount of time in seconds to attempt to mine on a particular header. - pub fn get_stratum_attempt_time() -> String { - let r_config = Settings::node_config_to_read(); - r_config.node - .server - .stratum_mining_config - .as_ref() - .unwrap() - .attempt_time_per_block - .to_string() - } - - /// Save stratum attempt time value in seconds. - pub fn save_stratum_attempt_time(time: u32) { - let mut w_config = Settings::node_config_to_update(); - w_config.node - .server - .stratum_mining_config - .as_mut() - .unwrap() - .attempt_time_per_block = time; - w_config.save(); - } - - /// Get minimum acceptable share difficulty to request from miners. - pub fn get_stratum_min_share_diff() -> String { - let r_config = Settings::node_config_to_read(); - r_config.node - .server - .stratum_mining_config - .as_ref() - .unwrap() - .minimum_share_difficulty - .to_string() - } - - /// Save minimum acceptable share difficulty. - pub fn save_stratum_min_share_diff(diff: u64) { - let mut w_config = Settings::node_config_to_update(); - w_config.node - .server - .stratum_mining_config - .as_mut() - .unwrap() - .minimum_share_difficulty = diff; - w_config.save(); - } - - /// Check if stratum mining server autorun is enabled. - pub fn is_stratum_autorun_enabled() -> bool { - let r_config = Settings::node_config_to_read(); - let stratum_config = r_config - .node - .server - .stratum_mining_config - .as_ref() - .unwrap(); - if let Some(enable) = stratum_config.enable_stratum_server { - return enable; - } - false - } - - /// Toggle stratum mining server autorun. - pub fn toggle_stratum_autorun() { - let autorun = Self::is_stratum_autorun_enabled(); - let mut w_config = Settings::node_config_to_update(); - w_config.node - .server - .stratum_mining_config - .as_mut() - .unwrap() - .enable_stratum_server = Some(!autorun); - w_config.save(); - } - - /// Get API server address. - pub fn get_api_address() -> String { - let r_config = Settings::node_config_to_read(); - r_config.node.server.api_http_addr.clone() - } - - /// Get API server IP and port. - pub fn get_api_ip_port() -> (String, String) { - let saved_addr = Self::get_api_address(); - let (addr, port) = saved_addr.split_once(":").unwrap(); - (addr.into(), port.into()) - } - - /// Save API server IP address and port. - pub fn save_api_address(addr: &String, port: &String) { - let addr_to_save = format!("{}:{}", addr, port); - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.api_http_addr = addr_to_save; - w_config.save(); - } - - /// Check if api server port is available across the system and config. - pub fn is_api_port_available(ip: &String, port: &String) -> bool { - if Node::is_running() { - // Check if API server with same address is running. - let same_running = NodeConfig::get_api_address() == format!("{}:{}", ip, port); - if same_running || Self::is_host_port_available(ip, port) { - return &Self::get_p2p_port() != port; - } - return false; - } else if Self::is_host_port_available(ip, port) { - return &Self::get_p2p_port() != port; - } - false - } - - /// Get API secret text. - pub fn get_api_secret(foreign: bool) -> Option { - let r_config = Settings::node_config_to_read(); - let api_secret_path = if foreign { - &r_config - .node - .server - .foreign_api_secret_path - } else { - &r_config - .node - .server - .api_secret_path - }.clone(); - if let Some(secret_path) = api_secret_path { - if let Ok(file) = File::open(secret_path) { - let buf_reader = BufReader::new(file); - let mut lines_iter = buf_reader.lines(); - if let Some(Ok(line)) = lines_iter.next() { - return Some(line); - } - } - } - None - } - - /// Save API secret text. - pub fn save_api_secret(api_secret: &String) { - Self::save_secret(api_secret, API_SECRET_FILE_NAME); - } - - /// Update Foreign API secret. - pub fn save_foreign_api_secret(api_secret: &String) { - Self::save_secret(api_secret, FOREIGN_API_SECRET_FILE_NAME); - } - - /// Save secret value into specified file. - fn save_secret(value: &String, file_name: &str) { - // Remove config value to remove authorization. - if value.is_empty() { - let mut w_config = Settings::node_config_to_update(); - match file_name { - API_SECRET_FILE_NAME => w_config.node.server.api_secret_path = None, - _ => w_config.node.server.foreign_api_secret_path = None - } - w_config.save(); - return; - } - - let mut secret_enabled = true; - // Get path for specified secret file. - let secret_path = { - let r_config = Settings::node_config_to_read(); - let path = match file_name { - API_SECRET_FILE_NAME => r_config.node.server.api_secret_path.clone(), - _ => r_config.node.server.foreign_api_secret_path.clone() - }; - path.unwrap_or_else(|| { - secret_enabled = false; - let chain_type = AppConfig::chain_type(); - let path = Self::get_secret_path(&chain_type, file_name); - path.to_str().unwrap().to_string() - }) - }; - // Update secret path at config if authorization was disabled before. - if !secret_enabled { - let mut w_config = Settings::node_config_to_update(); - match file_name { - API_SECRET_FILE_NAME => w_config - .node - .server - .api_secret_path = Some(secret_path.clone()), - _ => w_config.node.server.foreign_api_secret_path = Some(secret_path.clone()) - }; - - w_config.save(); - } - // Write secret text into file. - let mut secret_file = File::create(secret_path).unwrap(); - secret_file.write_all(value.as_bytes()).unwrap(); - } - - /// Get Future Time Limit. - pub fn get_ftl() -> String { - Settings::node_config_to_read().node.server.future_time_limit.to_string() - } - - /// Save Future Time Limit. - pub fn save_ftl(ftl: u64) { - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.future_time_limit = ftl; - w_config.save(); - } - - /// Check if full chain validation mode is enabled. - pub fn is_full_chain_validation() -> bool { - let mode = Settings::node_config_to_read().node.clone().server.chain_validation_mode; - mode == ChainValidationMode::EveryBlock - } - - /// Toggle full chain validation. - pub fn toggle_full_chain_validation() { - let validation_enabled = Self::is_full_chain_validation(); - let new_mode = if validation_enabled { - ChainValidationMode::Disabled - } else { - ChainValidationMode::EveryBlock - }; - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.chain_validation_mode = new_mode; - w_config.save(); - } - - /// Check if node is running in archive mode. - pub fn is_archive_mode() -> bool { - let archive_mode = Settings::node_config_to_read().node.clone().server.archive_mode; - archive_mode.is_some() && archive_mode.unwrap() - } - - /// Toggle archive node mode. - pub fn toggle_archive_mode() { - let archive_mode = Self::is_archive_mode(); - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.archive_mode = Some(!archive_mode); - w_config.save(); - } - - /// Get P2P server port. - pub fn get_p2p_port() -> String { - Settings::node_config_to_read().node.server.p2p_config.port.to_string() - } - - /// Check if P2P server port is available across the system and config. - pub fn is_p2p_port_available(port: &String) -> bool { - if port.parse::().is_err() { - return false; - } - let (_, api_port) = Self::get_api_ip_port(); - if Node::is_running() { - // Check if P2P server with same port is running. - let same_running = &NodeConfig::get_p2p_port() == port; - if same_running || Self::is_port_available(port) { - return &api_port != port; - } - return false; - } else if Self::is_port_available(port) { - return &api_port != port; - } - false - } - - /// Save P2P server port. - pub fn save_p2p_port(port: u16) { - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.p2p_config.port = port; - w_config.save(); - } - - /// Check if default seed list is used. - pub fn is_default_seeding_type() -> bool { - Settings::node_config_to_read().node.server.p2p_config.seeding_type == Seeding::DNSSeed - } - - /// Toggle seeding type to use default or custom seed list. - pub fn toggle_seeding_type() { - let seeding_type = match Self::is_default_seeding_type() { - true => Seeding::List, - false => Seeding::DNSSeed - }; - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.p2p_config.seeding_type = seeding_type; - w_config.save(); - } - - /// Get custom seed peers. - pub fn get_custom_seeds() -> Vec { - Settings::node_config_to_read().peers.seeds.clone() - } - - /// Save custom seed peer. - pub fn save_custom_seed(peer: String) { - let mut w_config = Settings::node_config_to_update(); - let size = w_config.peers.seeds.len(); - w_config.peers.seeds.insert(size, peer); - w_config.peers.save(); - } - - /// Remove custom seed peer. - pub fn remove_custom_seed(peer: &String) { - let mut w_config = Settings::node_config_to_update(); - let mut seeds = w_config.peers.seeds.clone(); - if let Some(index) = seeds.iter().position(|x| x == peer) { - seeds.remove(index); - } - w_config.peers.seeds = seeds; - w_config.peers.save(); - } - - /// Get denied peer list. - pub fn get_denied_peers() -> Vec { - Settings::node_config_to_read().peers.denied.clone() - } - - /// Save peer to denied list. - pub fn deny_peer(peer: String) { - let mut w_config = Settings::node_config_to_update(); - let size = w_config.peers.denied.len(); - w_config.peers.denied.insert(size, peer); - w_config.peers.save(); - } - - /// Remove denied peer. - pub fn remove_denied_peer(peer: &String) { - let mut w_config = Settings::node_config_to_update(); - let mut denied = w_config.peers.denied.clone(); - if let Some(index) = denied.iter().position(|x| x == peer) { - denied.remove(index); - } - w_config.peers.denied = denied; - w_config.peers.save(); - } - - /// Get allowed peer list. - pub fn get_allowed_peers() -> Vec { - Settings::node_config_to_read().peers.allowed.clone() - } - - /// Save peer to allowed list. - pub fn allow_peer(peer: String) { - let mut w_config = Settings::node_config_to_update(); - let size = w_config.peers.allowed.len(); - w_config.peers.allowed.insert(size, peer); - w_config.peers.save(); - } - - /// Remove allowed peer. - pub fn remove_allowed_peer(peer: &String) { - let mut w_config = Settings::node_config_to_update(); - let mut allowed = w_config.peers.allowed.clone(); - if let Some(index) = allowed.iter().position(|x| x == peer) { - allowed.remove(index); - } - w_config.peers.allowed = allowed; - w_config.peers.save(); - } - - /// Get preferred peer list. - pub fn get_preferred_peers() -> Vec { - Settings::node_config_to_read().peers.preferred.clone() - } - - /// Add peer at preferred list. - pub fn prefer_peer(peer: String) { - let mut w_config = Settings::node_config_to_update(); - let size = w_config.peers.preferred.len(); - w_config.peers.preferred.insert(size, peer); - w_config.peers.save(); - } - - /// Remove preferred peer. - pub fn remove_preferred_peer(peer: &String) { - let mut w_config = Settings::node_config_to_update(); - let mut preferred = w_config.peers.preferred.clone(); - if let Some(index) = preferred.iter().position(|x| x == peer) { - preferred.remove(index); - } - w_config.peers.preferred = preferred; - w_config.peers.save(); - } - - /// How long a banned peer should stay banned in ms. - pub fn get_p2p_ban_window() -> String { - Settings::node_config_to_read().node.server.p2p_config.ban_window().to_string() - } - - /// Save for how long a banned peer should stay banned in ms. - pub fn save_p2p_ban_window(time: i64) { - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.p2p_config.ban_window = Some(time); - w_config.save(); - } - - /// Maximum number of inbound peer connections. - pub fn get_max_inbound_peers() -> String { - Settings::node_config_to_read() - .node.server - .p2p_config - .peer_max_inbound_count() - .to_string() - } - - /// Save maximum number of inbound peer connections. - pub fn save_max_inbound_peers(count: u32) { - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.p2p_config.peer_max_inbound_count = Some(count); - w_config.save(); - } - - /// Maximum number of outbound peer connections. - pub fn get_max_outbound_peers() -> String { - Settings::node_config_to_read() - .node - .server - .p2p_config - .peer_max_outbound_count() - .to_string() - } - - /// Save maximum number of outbound peer connections. - pub fn save_max_outbound_peers(count: u32) { - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.p2p_config.peer_max_outbound_count = Some(count); - // Same value for preferred. - w_config.node.server.p2p_config.peer_min_preferred_outbound_count = Some(count); - w_config.save(); - } - - /// Base fee that's accepted into the pool. - pub fn get_base_fee() -> String { - Settings::node_config_to_read().node.server.pool_config.accept_fee_base.to_string() - } - - /// Save base fee that's accepted into the pool. - pub fn save_base_fee(fee: u64) { - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.pool_config.accept_fee_base = fee; - w_config.save(); - } - - /// Reorg cache retention period in minutes. - pub fn get_reorg_cache_period() -> String { - Settings::node_config_to_read().node.server.pool_config.reorg_cache_period.to_string() - } - - /// Save reorg cache retention period in minutes. - pub fn save_reorg_cache_period(period: u32) { - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.pool_config.reorg_cache_period = period; - w_config.save(); - } - - /// Max amount of transactions at pool. - pub fn get_max_pool_size() -> String { - Settings::node_config_to_read().node.server.pool_config.max_pool_size.to_string() - } - - /// Save max amount of transactions at pool. - pub fn save_max_pool_size(amount: usize) { - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.pool_config.max_pool_size = amount; - w_config.save(); - } - - /// Max amount of transactions at stem pool. - pub fn get_max_stempool_size() -> String { - Settings::node_config_to_read().node.server.pool_config.max_stempool_size.to_string() - } - - /// Save max amount of transactions at stem pool. - pub fn save_max_stempool_size(amount: usize) { - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.pool_config.max_stempool_size = amount; - w_config.save(); - } - - /// Max total weight of transactions that can get selected to build a block. - pub fn get_mineable_max_weight() -> String { - Settings::node_config_to_read().node.server.pool_config.mineable_max_weight.to_string() - } - - /// Set max total weight of transactions that can get selected to build a block. - pub fn save_mineable_max_weight(weight: u64) { - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.pool_config.mineable_max_weight = weight; - w_config.save(); - } - - // Dandelion settings - - /// Dandelion epoch duration in seconds. - pub fn get_dandelion_epoch() -> String { - Settings::node_config_to_read().node.server.dandelion_config.epoch_secs.to_string() - } - - /// Save Dandelion epoch duration in seconds. - pub fn save_dandelion_epoch(secs: u16) { - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.dandelion_config.epoch_secs = secs; - w_config.save(); - } - - /// Dandelion embargo timer in seconds. - /// Fluff and broadcast after embargo expires if tx not seen on network. - pub fn get_dandelion_embargo() -> String { - Settings::node_config_to_read().node.server.dandelion_config.embargo_secs.to_string() - } - - /// Save Dandelion embargo timer in seconds. - pub fn save_dandelion_embargo(secs: u16) { - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.dandelion_config.embargo_secs = secs; - w_config.save(); - } - - /// Dandelion aggregation period in seconds. - pub fn get_dandelion_aggregation() -> String { - Settings::node_config_to_read().node.server.dandelion_config.aggregation_secs.to_string() - } - - /// Save Dandelion aggregation period in seconds. - pub fn save_dandelion_aggregation(secs: u16) { - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.dandelion_config.aggregation_secs = secs; - w_config.save(); - } - - /// Dandelion stem probability (default: stem 90% of the time, fluff 10% of the time). - pub fn get_stem_probability() -> String { - Settings::node_config_to_read().node.server.dandelion_config.stem_probability.to_string() - } - - /// Save Dandelion stem probability. - pub fn save_stem_probability(percent: u8) { - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.dandelion_config.stem_probability = percent; - w_config.save(); - } - - /// Default to always stem our txs as described in Dandelion++ paper. - pub fn always_stem_our_txs() -> bool { - Settings::node_config_to_read().node.server.dandelion_config.always_stem_our_txs - } - - /// Toggle stem of our txs. - pub fn toggle_always_stem_our_txs() { - let stem_txs = Self::always_stem_our_txs(); - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.dandelion_config.always_stem_our_txs = !stem_txs; - w_config.save(); - } - - /// Save database node storage directory path. - pub fn get_storage_path() -> String { - Settings::node_config_to_read().node.server.db_root.clone() - } - - /// Save database node storage directory path. - pub fn save_storage_path(path: String) { - let mut w_config = Settings::node_config_to_update(); - w_config.node.server.db_root = path; - w_config.save(); - } -} \ No newline at end of file + /// Initialize config fields from provided [`ChainTypes`]. + pub fn for_chain_type(chain_type: &ChainTypes) -> Self { + // Check secret files for current chain type. + let _ = Self::check_api_secret_files(chain_type, API_SECRET_FILE_NAME); + let _ = Self::check_api_secret_files(chain_type, FOREIGN_API_SECRET_FILE_NAME); + + // Initialize peers config. + let peers_config = { + let sub_dir = Some(chain_type.shortname()); + let path = Settings::config_path(PeersConfig::FILE_NAME, sub_dir); + let config = Settings::read_from_file::(path.clone()); + if !path.exists() || config.is_err() { + Self::save_default_peers_config(chain_type) + } else { + config.unwrap() + } + }; + + // Initialize node config. + let node_config = { + let sub_dir = Some(chain_type.shortname()); + let path = Settings::config_path(SERVER_CONFIG_FILE_NAME, sub_dir); + let config = Settings::read_from_file::(path.clone()); + if !path.exists() || config.is_err() { + Self::save_default_node_server_config(chain_type) + } else { + config.unwrap() + } + }; + + Self { + node: node_config, + peers: peers_config, + } + } + + /// Save default node config for specified [`ChainTypes`]. + fn save_default_node_server_config(chain_type: &ChainTypes) -> ConfigMembers { + let sub_dir = Some(chain_type.shortname()); + let path = Settings::config_path(SERVER_CONFIG_FILE_NAME, sub_dir.clone()); + + let mut default_config = GlobalConfig::for_chain(chain_type); + default_config.update_paths(&Settings::base_path(sub_dir)); + let mut config = default_config.members.unwrap(); + + // Generate random p2p and api ports. + Self::setup_default_ports(&mut config); + + // Clear wallet listener url (actually it will be wallet id). + config + .server + .stratum_mining_config + .clone() + .unwrap() + .wallet_listener_url = "".to_string(); + + Settings::write_to_file(&config, path); + config + } + + /// Generate random p2p and api ports in ranges based on [`ChainTypes`]. + fn setup_default_ports(config: &mut ConfigMembers) { + let (api, p2p) = match config.server.chain_type { + ChainTypes::Mainnet => { + let api = rand::rng().random_range(30000..33000); + let p2p = rand::rng().random_range(33000..37000); + (api, p2p) + } + _ => { + let api = rand::rng().random_range(40000..43000); + let p2p = rand::rng().random_range(43000..47000); + (api, p2p) + } + }; + let api_addr = config.server.api_http_addr.split_once(":").unwrap().0; + config.server.api_http_addr = format!("{}:{}", api_addr, api); + config.server.p2p_config.port = p2p; + } + + /// Save default peers config for specified [`ChainTypes`]. + fn save_default_peers_config(chain_type: &ChainTypes) -> PeersConfig { + let sub_dir = Some(chain_type.shortname()); + let path = Settings::config_path(PeersConfig::FILE_NAME, sub_dir); + let config = PeersConfig::default(); + Settings::write_to_file(&config, path); + config + } + + /// Save node config to the file. + pub fn save(&self) { + let sub_dir = Some(self.node.server.chain_type.shortname()); + let config_path = Settings::config_path(SERVER_CONFIG_FILE_NAME, sub_dir); + Settings::write_to_file(&self.node, config_path); + } + + /// Get server config to use for node server before start. + pub fn node_server_config() -> ConfigMembers { + let r_config = Settings::node_config_to_read(); + r_config.node.clone() + } + + /// Reset node config to default values. + pub fn reset_to_default() { + let chain_type = { + let r_config = Settings::node_config_to_read(); + r_config.node.server.chain_type + }; + let node_server_config = Self::save_default_node_server_config(&chain_type); + let peers_config = Self::save_default_peers_config(&chain_type); + { + let mut w_config = Settings::node_config_to_update(); + w_config.node = node_server_config; + w_config.peers = peers_config; + } + } + + /// Check that the api secret files exist and are valid. + fn check_api_secret_files( + chain_type: &ChainTypes, + secret_file_name: &str, + ) -> Result<(), ConfigError> { + let api_secret_path = Self::get_secret_path(chain_type, secret_file_name); + if !api_secret_path.exists() { + config::init_api_secret(&api_secret_path) + } else { + config::check_api_secret(&api_secret_path) + } + } + + /// Get path for secret file. + fn get_secret_path(chain_type: &ChainTypes, secret_file_name: &str) -> PathBuf { + let sub_dir = Some(chain_type.shortname()); + let grin_path = Settings::base_path(sub_dir); + let mut api_secret_path = grin_path; + api_secret_path.push(secret_file_name); + api_secret_path + } + + /// List of available IP addresses. + pub fn get_ip_addrs() -> Vec { + let mut ip_addrs = Vec::new(); + let network_interfaces = list_afinet_netifas(); + if let Ok(network_interfaces) = network_interfaces { + for (_, ip) in network_interfaces.iter() { + if ip.is_ipv4() { + ip_addrs.push(ip.to_string()); + } + } + } + ip_addrs + } + + /// Check whether a port is available on the provided host. + fn is_host_port_available(host: &String, port: &String) -> bool { + if let Ok(p) = port.parse::() { + let ip_addr = Ipv4Addr::from_str(host.as_str()).unwrap(); + let ipv4 = SocketAddrV4::new(ip_addr, p); + return TcpListener::bind(ipv4).is_ok(); + } + false + } + + /// Check whether a port is available across the system at all hosts. + fn is_port_available(port: &String) -> bool { + if let Ok(p) = port.parse::() { + for ip in Self::get_ip_addrs() { + let ip_addr = Ipv4Addr::from_str(ip.as_str()).unwrap(); + let ipv4 = SocketAddrV4::new(ip_addr, p); + if TcpListener::bind(ipv4).is_err() { + return false; + } + } + } else { + return false; + } + true + } + + /// Get chain data path. + pub fn get_chain_data_path() -> String { + let r_config = Settings::node_config_to_read(); + r_config.node.server.db_root.clone() + } + + /// Save chain data path. + pub fn save_chain_data_path(path: String) { + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.db_root = path; + w_config.save(); + } + + /// Get stratum server IP address and port. + pub fn get_stratum_address() -> (String, String) { + let r_config = Settings::node_config_to_read(); + let saved_stratum_addr = r_config + .node + .server + .stratum_mining_config + .as_ref() + .unwrap() + .stratum_server_addr + .as_ref() + .unwrap(); + let (addr, port) = saved_stratum_addr.split_once(":").unwrap(); + (addr.into(), port.into()) + } + + /// Save stratum server IP address and port. + pub fn save_stratum_address(addr: &String, port: &String) { + let addr_to_save = format!("{}:{}", addr, port); + let mut w_config = Settings::node_config_to_update(); + w_config + .node + .server + .stratum_mining_config + .as_mut() + .unwrap() + .stratum_server_addr = Some(addr_to_save); + w_config.save(); + } + + /// Check if stratum server port is available across the system and config. + pub fn is_stratum_port_available(ip: &String, port: &String) -> bool { + if Node::get_stratum_stats().is_running { + // Check if Stratum server with same address is running. + let (cur_ip, cur_port) = Self::get_stratum_address(); + let same_running = ip == &cur_ip && port == &cur_port; + return same_running || Self::is_not_running_stratum_port_available(ip, port); + } + Self::is_not_running_stratum_port_available(&ip, &port) + } + + /// Check if stratum port is available when server is not running. + fn is_not_running_stratum_port_available(ip: &String, port: &String) -> bool { + if Self::is_host_port_available(&ip, &port) { + if &Self::get_p2p_port() != port { + let (api_ip, api_port) = Self::get_api_ip_port(); + return if &api_ip == ip { + &api_port != port + } else { + true + }; + } + } + false + } + + /// Get stratum mining server wallet address to get rewards. + pub fn get_stratum_wallet_id() -> Option { + let r_config = Settings::node_config_to_read(); + let id = r_config + .node + .clone() + .server + .stratum_mining_config + .unwrap() + .wallet_listener_url; + return if id.is_empty() { + None + } else { + if let Ok(id) = id.parse::() { + Some(id) + } else { + None + } + }; + } + + /// Save stratum mining server wallet address to get rewards. + pub fn save_stratum_wallet_id(id: i64) { + let mut w_config = Settings::node_config_to_update(); + w_config + .node + .server + .stratum_mining_config + .as_mut() + .unwrap() + .wallet_listener_url = id.to_string(); + w_config.save(); + } + + /// Get the amount of time in seconds to attempt to mine on a particular header. + pub fn get_stratum_attempt_time() -> String { + let r_config = Settings::node_config_to_read(); + r_config + .node + .server + .stratum_mining_config + .as_ref() + .unwrap() + .attempt_time_per_block + .to_string() + } + + /// Save stratum attempt time value in seconds. + pub fn save_stratum_attempt_time(time: u32) { + let mut w_config = Settings::node_config_to_update(); + w_config + .node + .server + .stratum_mining_config + .as_mut() + .unwrap() + .attempt_time_per_block = time; + w_config.save(); + } + + /// Get minimum acceptable share difficulty to request from miners. + pub fn get_stratum_min_share_diff() -> String { + let r_config = Settings::node_config_to_read(); + r_config + .node + .server + .stratum_mining_config + .as_ref() + .unwrap() + .minimum_share_difficulty + .to_string() + } + + /// Save minimum acceptable share difficulty. + pub fn save_stratum_min_share_diff(diff: u64) { + let mut w_config = Settings::node_config_to_update(); + w_config + .node + .server + .stratum_mining_config + .as_mut() + .unwrap() + .minimum_share_difficulty = diff; + w_config.save(); + } + + /// Check if stratum mining server autorun is enabled. + pub fn is_stratum_autorun_enabled() -> bool { + let r_config = Settings::node_config_to_read(); + let stratum_config = r_config.node.server.stratum_mining_config.as_ref().unwrap(); + if let Some(enable) = stratum_config.enable_stratum_server { + return enable; + } + false + } + + /// Toggle stratum mining server autorun. + pub fn toggle_stratum_autorun() { + let autorun = Self::is_stratum_autorun_enabled(); + let mut w_config = Settings::node_config_to_update(); + w_config + .node + .server + .stratum_mining_config + .as_mut() + .unwrap() + .enable_stratum_server = Some(!autorun); + w_config.save(); + } + + /// Get API server address. + pub fn get_api_address() -> String { + let r_config = Settings::node_config_to_read(); + r_config.node.server.api_http_addr.clone() + } + + /// Get API server IP and port. + pub fn get_api_ip_port() -> (String, String) { + let saved_addr = Self::get_api_address(); + let (addr, port) = saved_addr.split_once(":").unwrap(); + (addr.into(), port.into()) + } + + /// Save API server IP address and port. + pub fn save_api_address(addr: &String, port: &String) { + let addr_to_save = format!("{}:{}", addr, port); + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.api_http_addr = addr_to_save; + w_config.save(); + } + + /// Check if api server port is available across the system and config. + pub fn is_api_port_available(ip: &String, port: &String) -> bool { + if Node::is_running() { + // Check if API server with same address is running. + let same_running = NodeConfig::get_api_address() == format!("{}:{}", ip, port); + if same_running || Self::is_host_port_available(ip, port) { + return &Self::get_p2p_port() != port; + } + return false; + } else if Self::is_host_port_available(ip, port) { + return &Self::get_p2p_port() != port; + } + false + } + + /// Get API secret text. + pub fn get_api_secret(foreign: bool) -> Option { + let r_config = Settings::node_config_to_read(); + let api_secret_path = if foreign { + &r_config.node.server.foreign_api_secret_path + } else { + &r_config.node.server.api_secret_path + } + .clone(); + if let Some(secret_path) = api_secret_path { + if let Ok(file) = File::open(secret_path) { + let buf_reader = BufReader::new(file); + let mut lines_iter = buf_reader.lines(); + if let Some(Ok(line)) = lines_iter.next() { + return Some(line); + } + } + } + None + } + + /// Save API secret text. + pub fn save_api_secret(api_secret: &String) { + Self::save_secret(api_secret, API_SECRET_FILE_NAME); + } + + /// Update Foreign API secret. + pub fn save_foreign_api_secret(api_secret: &String) { + Self::save_secret(api_secret, FOREIGN_API_SECRET_FILE_NAME); + } + + /// Save secret value into specified file. + fn save_secret(value: &String, file_name: &str) { + // Remove config value to remove authorization. + if value.is_empty() { + let mut w_config = Settings::node_config_to_update(); + match file_name { + API_SECRET_FILE_NAME => w_config.node.server.api_secret_path = None, + _ => w_config.node.server.foreign_api_secret_path = None, + } + w_config.save(); + return; + } + + let mut secret_enabled = true; + // Get path for specified secret file. + let secret_path = { + let r_config = Settings::node_config_to_read(); + let path = match file_name { + API_SECRET_FILE_NAME => r_config.node.server.api_secret_path.clone(), + _ => r_config.node.server.foreign_api_secret_path.clone(), + }; + path.unwrap_or_else(|| { + secret_enabled = false; + let chain_type = AppConfig::chain_type(); + let path = Self::get_secret_path(&chain_type, file_name); + path.to_str().unwrap().to_string() + }) + }; + // Update secret path at config if authorization was disabled before. + if !secret_enabled { + let mut w_config = Settings::node_config_to_update(); + match file_name { + API_SECRET_FILE_NAME => { + w_config.node.server.api_secret_path = Some(secret_path.clone()) + } + _ => w_config.node.server.foreign_api_secret_path = Some(secret_path.clone()), + }; + + w_config.save(); + } + // Write secret text into file. + let mut secret_file = File::create(secret_path).unwrap(); + secret_file.write_all(value.as_bytes()).unwrap(); + } + + /// Get Future Time Limit. + pub fn get_ftl() -> String { + Settings::node_config_to_read() + .node + .server + .future_time_limit + .to_string() + } + + /// Save Future Time Limit. + pub fn save_ftl(ftl: u64) { + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.future_time_limit = ftl; + w_config.save(); + } + + /// Check if full chain validation mode is enabled. + pub fn is_full_chain_validation() -> bool { + let mode = Settings::node_config_to_read() + .node + .clone() + .server + .chain_validation_mode; + mode == ChainValidationMode::EveryBlock + } + + /// Toggle full chain validation. + pub fn toggle_full_chain_validation() { + let validation_enabled = Self::is_full_chain_validation(); + let new_mode = if validation_enabled { + ChainValidationMode::Disabled + } else { + ChainValidationMode::EveryBlock + }; + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.chain_validation_mode = new_mode; + w_config.save(); + } + + /// Check if node is running in archive mode. + pub fn is_archive_mode() -> bool { + let archive_mode = Settings::node_config_to_read() + .node + .clone() + .server + .archive_mode; + archive_mode.is_some() && archive_mode.unwrap() + } + + /// Toggle archive node mode. + pub fn toggle_archive_mode() { + let archive_mode = Self::is_archive_mode(); + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.archive_mode = Some(!archive_mode); + w_config.save(); + } + + /// Get P2P server port. + pub fn get_p2p_port() -> String { + Settings::node_config_to_read() + .node + .server + .p2p_config + .port + .to_string() + } + + /// Check if P2P server port is available across the system and config. + pub fn is_p2p_port_available(port: &String) -> bool { + if port.parse::().is_err() { + return false; + } + let (_, api_port) = Self::get_api_ip_port(); + if Node::is_running() { + // Check if P2P server with same port is running. + let same_running = &NodeConfig::get_p2p_port() == port; + if same_running || Self::is_port_available(port) { + return &api_port != port; + } + return false; + } else if Self::is_port_available(port) { + return &api_port != port; + } + false + } + + /// Save P2P server port. + pub fn save_p2p_port(port: u16) { + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.p2p_config.port = port; + w_config.save(); + } + + /// Check if default seed list is used. + pub fn is_default_seeding_type() -> bool { + Settings::node_config_to_read() + .node + .server + .p2p_config + .seeding_type + == Seeding::DNSSeed + } + + /// Toggle seeding type to use default or custom seed list. + pub fn toggle_seeding_type() { + let seeding_type = match Self::is_default_seeding_type() { + true => Seeding::List, + false => Seeding::DNSSeed, + }; + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.p2p_config.seeding_type = seeding_type; + w_config.save(); + } + + /// Get custom seed peers. + pub fn get_custom_seeds() -> Vec { + Settings::node_config_to_read().peers.seeds.clone() + } + + /// Save custom seed peer. + pub fn save_custom_seed(peer: String) { + let mut w_config = Settings::node_config_to_update(); + let size = w_config.peers.seeds.len(); + w_config.peers.seeds.insert(size, peer); + w_config.peers.save(); + } + + /// Remove custom seed peer. + pub fn remove_custom_seed(peer: &String) { + let mut w_config = Settings::node_config_to_update(); + let mut seeds = w_config.peers.seeds.clone(); + if let Some(index) = seeds.iter().position(|x| x == peer) { + seeds.remove(index); + } + w_config.peers.seeds = seeds; + w_config.peers.save(); + } + + /// Get denied peer list. + pub fn get_denied_peers() -> Vec { + Settings::node_config_to_read().peers.denied.clone() + } + + /// Save peer to denied list. + pub fn deny_peer(peer: String) { + let mut w_config = Settings::node_config_to_update(); + let size = w_config.peers.denied.len(); + w_config.peers.denied.insert(size, peer); + w_config.peers.save(); + } + + /// Remove denied peer. + pub fn remove_denied_peer(peer: &String) { + let mut w_config = Settings::node_config_to_update(); + let mut denied = w_config.peers.denied.clone(); + if let Some(index) = denied.iter().position(|x| x == peer) { + denied.remove(index); + } + w_config.peers.denied = denied; + w_config.peers.save(); + } + + /// Get allowed peer list. + pub fn get_allowed_peers() -> Vec { + Settings::node_config_to_read().peers.allowed.clone() + } + + /// Save peer to allowed list. + pub fn allow_peer(peer: String) { + let mut w_config = Settings::node_config_to_update(); + let size = w_config.peers.allowed.len(); + w_config.peers.allowed.insert(size, peer); + w_config.peers.save(); + } + + /// Remove allowed peer. + pub fn remove_allowed_peer(peer: &String) { + let mut w_config = Settings::node_config_to_update(); + let mut allowed = w_config.peers.allowed.clone(); + if let Some(index) = allowed.iter().position(|x| x == peer) { + allowed.remove(index); + } + w_config.peers.allowed = allowed; + w_config.peers.save(); + } + + /// Get preferred peer list. + pub fn get_preferred_peers() -> Vec { + Settings::node_config_to_read().peers.preferred.clone() + } + + /// Add peer at preferred list. + pub fn prefer_peer(peer: String) { + let mut w_config = Settings::node_config_to_update(); + let size = w_config.peers.preferred.len(); + w_config.peers.preferred.insert(size, peer); + w_config.peers.save(); + } + + /// Remove preferred peer. + pub fn remove_preferred_peer(peer: &String) { + let mut w_config = Settings::node_config_to_update(); + let mut preferred = w_config.peers.preferred.clone(); + if let Some(index) = preferred.iter().position(|x| x == peer) { + preferred.remove(index); + } + w_config.peers.preferred = preferred; + w_config.peers.save(); + } + + /// How long a banned peer should stay banned in ms. + pub fn get_p2p_ban_window() -> String { + Settings::node_config_to_read() + .node + .server + .p2p_config + .ban_window() + .to_string() + } + + /// Save for how long a banned peer should stay banned in ms. + pub fn save_p2p_ban_window(time: i64) { + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.p2p_config.ban_window = Some(time); + w_config.save(); + } + + /// Maximum number of inbound peer connections. + pub fn get_max_inbound_peers() -> String { + Settings::node_config_to_read() + .node + .server + .p2p_config + .peer_max_inbound_count() + .to_string() + } + + /// Save maximum number of inbound peer connections. + pub fn save_max_inbound_peers(count: u32) { + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.p2p_config.peer_max_inbound_count = Some(count); + w_config.save(); + } + + /// Maximum number of outbound peer connections. + pub fn get_max_outbound_peers() -> String { + Settings::node_config_to_read() + .node + .server + .p2p_config + .peer_max_outbound_count() + .to_string() + } + + /// Save maximum number of outbound peer connections. + pub fn save_max_outbound_peers(count: u32) { + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.p2p_config.peer_max_outbound_count = Some(count); + // Same value for preferred. + w_config + .node + .server + .p2p_config + .peer_min_preferred_outbound_count = Some(count); + w_config.save(); + } + + /// Base fee that's accepted into the pool. + pub fn get_base_fee() -> String { + Settings::node_config_to_read() + .node + .server + .pool_config + .accept_fee_base + .to_string() + } + + /// Save base fee that's accepted into the pool. + pub fn save_base_fee(fee: u64) { + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.pool_config.accept_fee_base = fee; + w_config.save(); + } + + /// Reorg cache retention period in minutes. + pub fn get_reorg_cache_period() -> String { + Settings::node_config_to_read() + .node + .server + .pool_config + .reorg_cache_period + .to_string() + } + + /// Save reorg cache retention period in minutes. + pub fn save_reorg_cache_period(period: u32) { + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.pool_config.reorg_cache_period = period; + w_config.save(); + } + + /// Max amount of transactions at pool. + pub fn get_max_pool_size() -> String { + Settings::node_config_to_read() + .node + .server + .pool_config + .max_pool_size + .to_string() + } + + /// Save max amount of transactions at pool. + pub fn save_max_pool_size(amount: usize) { + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.pool_config.max_pool_size = amount; + w_config.save(); + } + + /// Max amount of transactions at stem pool. + pub fn get_max_stempool_size() -> String { + Settings::node_config_to_read() + .node + .server + .pool_config + .max_stempool_size + .to_string() + } + + /// Save max amount of transactions at stem pool. + pub fn save_max_stempool_size(amount: usize) { + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.pool_config.max_stempool_size = amount; + w_config.save(); + } + + /// Max total weight of transactions that can get selected to build a block. + pub fn get_mineable_max_weight() -> String { + Settings::node_config_to_read() + .node + .server + .pool_config + .mineable_max_weight + .to_string() + } + + /// Set max total weight of transactions that can get selected to build a block. + pub fn save_mineable_max_weight(weight: u64) { + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.pool_config.mineable_max_weight = weight; + w_config.save(); + } + + // Dandelion settings + + /// Dandelion epoch duration in seconds. + pub fn get_dandelion_epoch() -> String { + Settings::node_config_to_read() + .node + .server + .dandelion_config + .epoch_secs + .to_string() + } + + /// Save Dandelion epoch duration in seconds. + pub fn save_dandelion_epoch(secs: u16) { + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.dandelion_config.epoch_secs = secs; + w_config.save(); + } + + /// Dandelion embargo timer in seconds. + /// Fluff and broadcast after embargo expires if tx not seen on network. + pub fn get_dandelion_embargo() -> String { + Settings::node_config_to_read() + .node + .server + .dandelion_config + .embargo_secs + .to_string() + } + + /// Save Dandelion embargo timer in seconds. + pub fn save_dandelion_embargo(secs: u16) { + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.dandelion_config.embargo_secs = secs; + w_config.save(); + } + + /// Dandelion aggregation period in seconds. + pub fn get_dandelion_aggregation() -> String { + Settings::node_config_to_read() + .node + .server + .dandelion_config + .aggregation_secs + .to_string() + } + + /// Save Dandelion aggregation period in seconds. + pub fn save_dandelion_aggregation(secs: u16) { + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.dandelion_config.aggregation_secs = secs; + w_config.save(); + } + + /// Dandelion stem probability (default: stem 90% of the time, fluff 10% of the time). + pub fn get_stem_probability() -> String { + Settings::node_config_to_read() + .node + .server + .dandelion_config + .stem_probability + .to_string() + } + + /// Save Dandelion stem probability. + pub fn save_stem_probability(percent: u8) { + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.dandelion_config.stem_probability = percent; + w_config.save(); + } + + /// Default to always stem our txs as described in Dandelion++ paper. + pub fn always_stem_our_txs() -> bool { + Settings::node_config_to_read() + .node + .server + .dandelion_config + .always_stem_our_txs + } + + /// Toggle stem of our txs. + pub fn toggle_always_stem_our_txs() { + let stem_txs = Self::always_stem_our_txs(); + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.dandelion_config.always_stem_our_txs = !stem_txs; + w_config.save(); + } + + /// Save database node storage directory path. + pub fn get_storage_path() -> String { + Settings::node_config_to_read().node.server.db_root.clone() + } + + /// Save database node storage directory path. + pub fn save_storage_path(path: String) { + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.db_root = path; + w_config.save(); + } +} diff --git a/src/node/mine_block.rs b/src/node/mine_block.rs index aeb1b27b..3b0bc6a5 100644 --- a/src/node/mine_block.rs +++ b/src/node/mine_block.rs @@ -16,169 +16,169 @@ //! them into a block and returns it. use chrono::prelude::{DateTime, Utc}; -use rand::{rng, Rng}; -use serde_json::{json, Value}; +use rand::{Rng, rng}; +use serde_json::{Value, json}; use std::sync::Arc; use std::thread; use std::time::Duration; +use crate::node::stratum::StratumStopState; use grin_api; use grin_chain; -use grin_servers::common::types::Error; use grin_core::core::{Output, TxKernel}; -use grin_core::libtx::secp_ser; use grin_core::libtx::ProofBuilder; +use grin_core::libtx::secp_ser; use grin_core::{consensus, core, global}; use grin_keychain::{ExtKeychain, Identifier, Keychain}; use grin_servers::ServerTxPool; +use grin_servers::common::types::Error; use log::{debug, error, trace, warn}; use serde_derive::{Deserialize, Serialize}; -use crate::node::stratum::StratumStopState; /// Fees in block to use for coinbase amount calculation /// (Duplicated from Grin wallet project) #[derive(Serialize, Deserialize, Debug, Clone)] pub struct BlockFees { - /// fees - #[serde(with = "secp_ser::string_or_u64")] - pub fees: u64, - /// height - #[serde(with = "secp_ser::string_or_u64")] - pub height: u64, - /// key id - pub key_id: Option, + /// fees + #[serde(with = "secp_ser::string_or_u64")] + pub fees: u64, + /// height + #[serde(with = "secp_ser::string_or_u64")] + pub height: u64, + /// key id + pub key_id: Option, } impl BlockFees { - /// return key id - pub fn key_id(&self) -> Option { - self.key_id.clone() - } + /// return key id + pub fn key_id(&self) -> Option { + self.key_id.clone() + } } /// Response to build a coinbase output. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct CbData { - /// Output - pub output: Output, - /// Kernel - pub kernel: TxKernel, - /// Key Id - pub key_id: Option, + /// Output + pub output: Output, + /// Kernel + pub kernel: TxKernel, + /// Key Id + pub key_id: Option, } // Ensure a block suitable for mining is built and returned // If a wallet listener URL is not provided the reward will be "burnt" // Warning: This call does not return until/unless a new block can be built pub fn get_block( - chain: &Arc, - tx_pool: &ServerTxPool, - key_id: Option, - wallet_listener_url: Option, - stop_state: &Arc + chain: &Arc, + tx_pool: &ServerTxPool, + key_id: Option, + wallet_listener_url: Option, + stop_state: &Arc, ) -> Option<(core::Block, BlockFees)> { - let wallet_retry_interval = 5; - // get the latest chain state and build a block on top of it - let mut result = build_block(chain, tx_pool, key_id.clone(), wallet_listener_url.clone()); - while let Err(e) = result { - let mut new_key_id = key_id.to_owned(); - match e { - Error::Chain(c) => match c { - grin_chain::Error::DuplicateCommitment(_) => { - debug!( + let wallet_retry_interval = 5; + // get the latest chain state and build a block on top of it + let mut result = build_block(chain, tx_pool, key_id.clone(), wallet_listener_url.clone()); + while let Err(e) = result { + let mut new_key_id = key_id.to_owned(); + match e { + Error::Chain(c) => match c { + grin_chain::Error::DuplicateCommitment(_) => { + debug!( "Duplicate commit for potential coinbase detected. Trying next derivation." ); - // use the next available key to generate a different coinbase commitment - new_key_id = None; - } - _ => { - error!("Chain Error: {:?}", c); - } - }, - Error::WalletComm(_) => { - error!( + // use the next available key to generate a different coinbase commitment + new_key_id = None; + } + _ => { + error!("Chain Error: {:?}", c); + } + }, + Error::WalletComm(_) => { + error!( "Error building new block: Can't connect to wallet listener at {:?}; will retry", wallet_listener_url.as_ref().unwrap() ); - thread::sleep(Duration::from_secs(wallet_retry_interval)); - } - ae => { - warn!("Error building new block: {:?}. Retrying.", ae); - } - } + thread::sleep(Duration::from_secs(wallet_retry_interval)); + } + ae => { + warn!("Error building new block: {:?}. Retrying.", ae); + } + } - // only wait if we are still using the same key: a different coinbase commitment is unlikely - // to have duplication - if new_key_id.is_some() { - thread::sleep(Duration::from_millis(100)); - } + // only wait if we are still using the same key: a different coinbase commitment is unlikely + // to have duplication + if new_key_id.is_some() { + thread::sleep(Duration::from_millis(100)); + } - // Stop attempts to build a block on stop. - if stop_state.is_stopped() { - return None; - } - result = build_block(chain, tx_pool, new_key_id, wallet_listener_url.clone()); - } - Some(result.unwrap()) + // Stop attempts to build a block on stop. + if stop_state.is_stopped() { + return None; + } + result = build_block(chain, tx_pool, new_key_id, wallet_listener_url.clone()); + } + Some(result.unwrap()) } /// Builds a new block with the chain head as previous and eligible /// transactions from the pool. fn build_block( - chain: &Arc, - tx_pool: &ServerTxPool, - key_id: Option, - wallet_listener_url: Option, + chain: &Arc, + tx_pool: &ServerTxPool, + key_id: Option, + wallet_listener_url: Option, ) -> Result<(core::Block, BlockFees), Error> { - let head = chain.head_header()?; + let head = chain.head_header()?; - // prepare the block header timestamp - let mut now_sec = Utc::now().timestamp(); - let head_sec = head.timestamp.timestamp(); - if now_sec <= head_sec { - now_sec = head_sec + 1; - } + // prepare the block header timestamp + let mut now_sec = Utc::now().timestamp(); + let head_sec = head.timestamp.timestamp(); + if now_sec <= head_sec { + now_sec = head_sec + 1; + } - // Determine the difficulty our block should be at. - // Note: do not keep the difficulty_iter in scope (it has an active batch). - let difficulty = consensus::next_difficulty(head.height + 1, chain.difficulty_iter()?); + // Determine the difficulty our block should be at. + // Note: do not keep the difficulty_iter in scope (it has an active batch). + let difficulty = consensus::next_difficulty(head.height + 1, chain.difficulty_iter()?); - // Extract current "mineable" transactions from the pool. - // If this fails for *any* reason then fallback to an empty vec of txs. - // This will allow us to mine an "empty" block if the txpool is in an - // invalid (and unexpected) state. - let txs = match tx_pool.read().prepare_mineable_transactions() { - Ok(txs) => txs, - Err(e) => { - error!( + // Extract current "mineable" transactions from the pool. + // If this fails for *any* reason then fallback to an empty vec of txs. + // This will allow us to mine an "empty" block if the txpool is in an + // invalid (and unexpected) state. + let txs = match tx_pool.read().prepare_mineable_transactions() { + Ok(txs) => txs, + Err(e) => { + error!( "build_block: Failed to prepare mineable txs from txpool: {:?}", e ); - warn!("build_block: Falling back to mining empty block."); - vec![] - } - }; + warn!("build_block: Falling back to mining empty block."); + vec![] + } + }; - // build the coinbase and the block itself - let fees = txs.iter().map(|tx| tx.fee()).sum(); - let height = head.height + 1; - let block_fees = BlockFees { - fees, - key_id, - height, - }; + // build the coinbase and the block itself + let fees = txs.iter().map(|tx| tx.fee()).sum(); + let height = head.height + 1; + let block_fees = BlockFees { + fees, + key_id, + height, + }; - let (output, kernel, block_fees) = get_coinbase(wallet_listener_url, block_fees)?; - let mut b = core::Block::from_reward(&head, &txs, output, kernel, difficulty.difficulty)?; + let (output, kernel, block_fees) = get_coinbase(wallet_listener_url, block_fees)?; + let mut b = core::Block::from_reward(&head, &txs, output, kernel, difficulty.difficulty)?; - // making sure we're not spending time mining a useless block - b.validate(&head.total_kernel_offset)?; + // making sure we're not spending time mining a useless block + b.validate(&head.total_kernel_offset)?; - b.header.pow.nonce = rng().random(); - b.header.pow.secondary_scaling = difficulty.secondary_scaling; - b.header.timestamp = DateTime::from_timestamp(now_sec, 0).unwrap(); + b.header.pow.nonce = rng().random(); + b.header.pow.secondary_scaling = difficulty.secondary_scaling; + b.header.timestamp = DateTime::from_timestamp(now_sec, 0).unwrap(); - debug!( + debug!( "Built new block with {} inputs and {} outputs, block difficulty: {}, cumulative difficulty {}", b.inputs().len(), b.outputs().len(), @@ -186,78 +186,78 @@ fn build_block( b.header.total_difficulty().to_num(), ); - // Now set txhashset roots and sizes on the header of the block being built. - match chain.set_txhashset_roots(&mut b) { - Ok(_) => Ok((b, block_fees)), - Err(e) => { - match e { - // If this is a duplicate commitment then likely trying to use - // a key that hass already been derived but not in the wallet - // for some reason, allow caller to retry. - grin_chain::Error::DuplicateCommitment(e) => { - Err(Error::Chain(grin_chain::Error::DuplicateCommitment(e))) - } + // Now set txhashset roots and sizes on the header of the block being built. + match chain.set_txhashset_roots(&mut b) { + Ok(_) => Ok((b, block_fees)), + Err(e) => { + match e { + // If this is a duplicate commitment then likely trying to use + // a key that hass already been derived but not in the wallet + // for some reason, allow caller to retry. + grin_chain::Error::DuplicateCommitment(e) => { + Err(Error::Chain(grin_chain::Error::DuplicateCommitment(e))) + } - // Some other issue, possibly duplicate kernel - _ => { - error!("Error setting txhashset root to build a block: {:?}", e); - Err(Error::Chain(grin_chain::Error::Other(format!("{:?}", e)))) - } - } - } - } + // Some other issue, possibly duplicate kernel + _ => { + error!("Error setting txhashset root to build a block: {:?}", e); + Err(Error::Chain(grin_chain::Error::Other(format!("{:?}", e)))) + } + } + } + } } /// /// Probably only want to do this when testing. /// fn burn_reward(block_fees: BlockFees) -> Result<(Output, TxKernel, BlockFees), Error> { - warn!("Burning block fees: {:?}", block_fees); - let keychain = ExtKeychain::from_random_seed(global::is_testnet())?; - let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); - let (out, kernel) = grin_core::libtx::reward::output( - &keychain, - &ProofBuilder::new(&keychain), - &key_id, - block_fees.fees, - false, - ) - .unwrap(); - Ok((out, kernel, block_fees)) + warn!("Burning block fees: {:?}", block_fees); + let keychain = ExtKeychain::from_random_seed(global::is_testnet())?; + let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let (out, kernel) = grin_core::libtx::reward::output( + &keychain, + &ProofBuilder::new(&keychain), + &key_id, + block_fees.fees, + false, + ) + .unwrap(); + Ok((out, kernel, block_fees)) } // Connect to the wallet listener and get coinbase. // Warning: If a wallet listener URL is not provided the reward will be "burnt" fn get_coinbase( - wallet_listener_url: Option, - block_fees: BlockFees, + wallet_listener_url: Option, + block_fees: BlockFees, ) -> Result<(Output, TxKernel, BlockFees), Error> { - return match wallet_listener_url { - None => { - // Burn it - burn_reward(block_fees) - } - Some(wallet_listener_url) => { - let res = create_coinbase(&wallet_listener_url, &block_fees)?; - let output = res.output; - let kernel = res.kernel; - let key_id = res.key_id; - let block_fees = BlockFees { - key_id, - ..block_fees - }; + return match wallet_listener_url { + None => { + // Burn it + burn_reward(block_fees) + } + Some(wallet_listener_url) => { + let res = create_coinbase(&wallet_listener_url, &block_fees)?; + let output = res.output; + let kernel = res.kernel; + let key_id = res.key_id; + let block_fees = BlockFees { + key_id, + ..block_fees + }; - debug!("get_coinbase: {:?}", block_fees); - Ok((output, kernel, block_fees)) - } - } + debug!("get_coinbase: {:?}", block_fees); + Ok((output, kernel, block_fees)) + } + }; } /// Call the wallet API to create a coinbase output for the given block_fees. /// Will retry based on default "retry forever with backoff" behavior. fn create_coinbase(dest: &str, block_fees: &BlockFees) -> Result { - let url = format!("{}/v2/foreign", dest); - let req_body = json!({ + let url = format!("{}/v2/foreign", dest); + let req_body = json!({ "jsonrpc": "2.0", "method": "build_coinbase", "id": 1, @@ -266,39 +266,39 @@ fn create_coinbase(dest: &str, block_fees: &BlockFees) -> Result } }); - trace!("Sending build_coinbase request: {}", req_body); - let req = grin_api::client::create_post_request(url.as_str(), None, &req_body)?; - let timeout = grin_api::client::TimeOut::default(); - let res: String = grin_api::client::send_request(req, timeout).map_err(|e| { - let report = format!( - "Failed to get coinbase from {}. Is the wallet listening? {:?}", - dest, e - ); - error!("{}", report); - Error::WalletComm(report) - })?; + trace!("Sending build_coinbase request: {}", req_body); + let req = grin_api::client::create_post_request(url.as_str(), None, &req_body)?; + let timeout = grin_api::client::TimeOut::default(); + let res: String = grin_api::client::send_request(req, timeout).map_err(|e| { + let report = format!( + "Failed to get coinbase from {}. Is the wallet listening? {:?}", + dest, e + ); + error!("{}", report); + Error::WalletComm(report) + })?; - let res: Value = serde_json::from_str(&res).unwrap(); - trace!("Response: {}", res); - if res["error"] != json!(null) { - let report = format!( - "Failed to get coinbase from {}: Error: {}, Message: {}", - dest, res["error"]["code"], res["error"]["message"] - ); - error!("{}", report); - return Err(Error::WalletComm(report)); - } + let res: Value = serde_json::from_str(&res).unwrap(); + trace!("Response: {}", res); + if res["error"] != json!(null) { + let report = format!( + "Failed to get coinbase from {}: Error: {}, Message: {}", + dest, res["error"]["code"], res["error"]["message"] + ); + error!("{}", report); + return Err(Error::WalletComm(report)); + } - let cb_data = res["result"]["Ok"].clone(); - trace!("cb_data: {}", cb_data); - let ret_val = match serde_json::from_value::(cb_data) { - Ok(r) => r, - Err(e) => { - let report = format!("Couldn't deserialize CbData: {}", e); - error!("{}", report); - return Err(Error::WalletComm(report)); - } - }; + let cb_data = res["result"]["Ok"].clone(); + trace!("cb_data: {}", cb_data); + let ret_val = match serde_json::from_value::(cb_data) { + Ok(r) => r, + Err(e) => { + let report = format!("Couldn't deserialize CbData: {}", e); + error!("{}", report); + return Err(Error::WalletComm(report)); + } + }; - Ok(ret_val) + Ok(ret_val) } diff --git a/src/node/mod.rs b/src/node/mod.rs index 15d4ccfd..73090511 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod stratum; mod mine_block; +mod stratum; mod node; pub use node::Node; @@ -22,4 +22,4 @@ mod config; pub use config::*; mod types; -pub use types::*; \ No newline at end of file +pub use types::*; diff --git a/src/node/node.rs b/src/node/node.rs index f95675f1..b2f6c059 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -12,694 +12,703 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{fs, thread}; +use futures::channel::oneshot; +use lazy_static::lazy_static; +use parking_lot::RwLock; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; -use lazy_static::lazy_static; -use parking_lot::RwLock; -use futures::channel::oneshot; +use std::{fs, thread}; +use crate::node::stratum::{StratumServer, StratumStopState}; +use crate::node::{NodeConfig, NodeError, PeersConfig}; use grin_chain::SyncStatus; use grin_core::global; use grin_core::global::ChainTypes; -use grin_p2p::msg::PeerAddrs; use grin_p2p::Seeding; -use grin_servers::{Server, ServerStats, StratumServerConfig, StratumStats}; +use grin_p2p::msg::PeerAddrs; use grin_servers::common::types::Error; +use grin_servers::{Server, ServerStats, StratumServerConfig, StratumStats}; use log::error; -use crate::node::{NodeConfig, NodeError, PeersConfig}; -use crate::node::stratum::{StratumStopState, StratumServer}; lazy_static! { - /// Static thread-aware state of [`Node`] to be updated from separate thread. - static ref NODE_STATE: Arc = Arc::new(Node::default()); + /// Static thread-aware state of [`Node`] to be updated from separate thread. + static ref NODE_STATE: Arc = Arc::new(Node::default()); } /// Provides [`Server`] control, holds current status and statistics. pub struct Node { - /// Node [`Server`] statistics information. - stats: Arc>>, + /// Node [`Server`] statistics information. + stats: Arc>>, - /// [`StratumServer`] statistics information. - stratum_stats: Arc>, - /// Flag to start [`StratumServer`]. - start_stratum_needed: AtomicBool, - /// State to stop [`StratumServer`] from outside. - stratum_stop_state: Arc, + /// [`StratumServer`] statistics information. + stratum_stats: Arc>, + /// Flag to start [`StratumServer`]. + start_stratum_needed: AtomicBool, + /// State to stop [`StratumServer`] from outside. + stratum_stop_state: Arc, - /// Indicator if node [`Server`] is starting. - starting: AtomicBool, - /// Flag to stop the [`Server`] and start it again. - restart_needed: AtomicBool, - /// Flag to stop the [`Server`]. - stop_needed: AtomicBool, - /// Flag to check if app exit is needed after [`Server`] stop. - exit_after_stop: AtomicBool, - /// Flag to reset data and restart the [`Server`]. - reset_data: AtomicBool, - /// Flag to change data directory and restart the [`Server`]. - change_data_dir: AtomicBool, + /// Indicator if node [`Server`] is starting. + starting: AtomicBool, + /// Flag to stop the [`Server`] and start it again. + restart_needed: AtomicBool, + /// Flag to stop the [`Server`]. + stop_needed: AtomicBool, + /// Flag to check if app exit is needed after [`Server`] stop. + exit_after_stop: AtomicBool, + /// Flag to reset data and restart the [`Server`]. + reset_data: AtomicBool, + /// Flag to change data directory and restart the [`Server`]. + change_data_dir: AtomicBool, - /// An error occurred on [`Server`] start. - error: Arc>> + /// An error occurred on [`Server`] start. + error: Arc>>, } impl Default for Node { - fn default() -> Self { - Self { - stats: Arc::new(RwLock::new(None)), - stratum_stats: Arc::new(grin_util::RwLock::new(StratumStats::default())), - stratum_stop_state: Arc::new(StratumStopState::default()), - starting: AtomicBool::new(false), - restart_needed: AtomicBool::new(false), - stop_needed: AtomicBool::new(false), - exit_after_stop: AtomicBool::new(false), - start_stratum_needed: AtomicBool::new(false), - error: Arc::new(RwLock::new(None)), - reset_data: AtomicBool::new(false), - change_data_dir: AtomicBool::new(false), - } - } + fn default() -> Self { + Self { + stats: Arc::new(RwLock::new(None)), + stratum_stats: Arc::new(grin_util::RwLock::new(StratumStats::default())), + stratum_stop_state: Arc::new(StratumStopState::default()), + starting: AtomicBool::new(false), + restart_needed: AtomicBool::new(false), + stop_needed: AtomicBool::new(false), + exit_after_stop: AtomicBool::new(false), + start_stratum_needed: AtomicBool::new(false), + error: Arc::new(RwLock::new(None)), + reset_data: AtomicBool::new(false), + change_data_dir: AtomicBool::new(false), + } + } } impl Node { - /// Delay for thread to update the stats. - pub const STATS_UPDATE_DELAY: Duration = Duration::from_millis(1000); + /// Delay for thread to update the stats. + pub const STATS_UPDATE_DELAY: Duration = Duration::from_millis(1000); - /// Default Mainnet DNS Seeds - pub const MAINNET_DNS_SEEDS: &[&str] = &[ - "mainnet-seed.grinnode.live", // info@grinnode.live - "grincoin.org", // xmpp:aglkm@conversations.im - "main.gri.mw", // admin@gri.mw - "mainnet.grinffindor.org", // support@grinffindor.org - "main-seed.grin.money", // support@grinily.com - ]; + /// Default Mainnet DNS Seeds + pub const MAINNET_DNS_SEEDS: &[&str] = &[ + "mainnet-seed.grinnode.live", // info@grinnode.live + "grincoin.org", // xmpp:aglkm@conversations.im + "main.gri.mw", // admin@gri.mw + "mainnet.grinffindor.org", // support@grinffindor.org + "main-seed.grin.money", // support@grinily.com + ]; - /// Default Testnet DNS Seeds - pub const TESTNET_DNS_SEEDS: &[&str] = &[ - "testnet.grincoin.org", // xmpp:aglkm@conversations.im - "test.gri.mw", // admin@gri.mw - "testnet.grinffindor.org", // support@grinffindor.org - "test-seed.grin.money", // support@grinily.com - ]; + /// Default Testnet DNS Seeds + pub const TESTNET_DNS_SEEDS: &[&str] = &[ + "testnet.grincoin.org", // xmpp:aglkm@conversations.im + "test.gri.mw", // admin@gri.mw + "testnet.grinffindor.org", // support@grinffindor.org + "test-seed.grin.money", // support@grinily.com + ]; - /// Stop the [`Server`] and setup exit flag after if needed. - pub fn stop(exit_after_stop: bool) { - NODE_STATE.stop_needed.store(true, Ordering::Relaxed); - NODE_STATE.exit_after_stop.store(exit_after_stop, Ordering::Relaxed); - } + /// Stop the [`Server`] and setup exit flag after if needed. + pub fn stop(exit_after_stop: bool) { + NODE_STATE.stop_needed.store(true, Ordering::Relaxed); + NODE_STATE + .exit_after_stop + .store(exit_after_stop, Ordering::Relaxed); + } - /// Request to start the [`Node`]. - pub fn start() { - if !Self::is_running() { - Self::start_server_thread(); - } - } + /// Request to start the [`Node`]. + pub fn start() { + if !Self::is_running() { + Self::start_server_thread(); + } + } - /// Request to restart the [`Node`]. - pub fn restart() { - if Self::is_running() { - NODE_STATE.restart_needed.store(true, Ordering::Relaxed); - } else { - Node::start(); - } - } + /// Request to restart the [`Node`]. + pub fn restart() { + if Self::is_running() { + NODE_STATE.restart_needed.store(true, Ordering::Relaxed); + } else { + Node::start(); + } + } - /// Request to start [`StratumServer`]. - pub fn start_stratum() { - NODE_STATE.start_stratum_needed.store(true, Ordering::Relaxed); - } + /// Request to start [`StratumServer`]. + pub fn start_stratum() { + NODE_STATE + .start_stratum_needed + .store(true, Ordering::Relaxed); + } - /// Check if [`StratumServer`] is starting. - pub fn is_stratum_starting() -> bool { - NODE_STATE.start_stratum_needed.load(Ordering::Relaxed) - } + /// Check if [`StratumServer`] is starting. + pub fn is_stratum_starting() -> bool { + NODE_STATE.start_stratum_needed.load(Ordering::Relaxed) + } - /// Get [`StratumServer`] statistics. - pub fn get_stratum_stats() -> StratumStats { - NODE_STATE.stratum_stats.read().clone() - } + /// Get [`StratumServer`] statistics. + pub fn get_stratum_stats() -> StratumStats { + NODE_STATE.stratum_stats.read().clone() + } - /// Stop [`StratumServer`]. - pub fn stop_stratum() { - NODE_STATE.stratum_stop_state.stop() - } + /// Stop [`StratumServer`]. + pub fn stop_stratum() { + NODE_STATE.stratum_stop_state.stop() + } - /// Check if [`StratumServer`] is stopping. - pub fn is_stratum_stopping() -> bool { - NODE_STATE.stratum_stop_state.is_stopped() - } + /// Check if [`StratumServer`] is stopping. + pub fn is_stratum_stopping() -> bool { + NODE_STATE.stratum_stop_state.is_stopped() + } - /// Check if [`Node`] is starting. - pub fn is_starting() -> bool { - NODE_STATE.starting.load(Ordering::Relaxed) - } + /// Check if [`Node`] is starting. + pub fn is_starting() -> bool { + NODE_STATE.starting.load(Ordering::Relaxed) + } - /// Check if [`Node`] is running. - pub fn is_running() -> bool { - Self::get_sync_status().is_some() - } + /// Check if [`Node`] is running. + pub fn is_running() -> bool { + Self::get_sync_status().is_some() + } - /// Check if [`Node`] is stopping. - pub fn is_stopping() -> bool { - NODE_STATE.stop_needed.load(Ordering::Relaxed) - } + /// Check if [`Node`] is stopping. + pub fn is_stopping() -> bool { + NODE_STATE.stop_needed.load(Ordering::Relaxed) + } - /// Check if [`Node`] is restarting. - pub fn is_restarting() -> bool { - NODE_STATE.restart_needed.load(Ordering::Relaxed) || Self::reset_data_needed() - } + /// Check if [`Node`] is restarting. + pub fn is_restarting() -> bool { + NODE_STATE.restart_needed.load(Ordering::Relaxed) || Self::reset_data_needed() + } - /// Check if reset of [`Server`] peers is needed. - fn reset_data_needed() -> bool { - NODE_STATE.reset_data.load(Ordering::Relaxed) - } + /// Check if reset of [`Server`] peers is needed. + fn reset_data_needed() -> bool { + NODE_STATE.reset_data.load(Ordering::Relaxed) + } - /// Get node [`Server`] statistics. - pub fn get_stats() -> Option { - NODE_STATE.stats.read().clone() - } + /// Get node [`Server`] statistics. + pub fn get_stats() -> Option { + NODE_STATE.stats.read().clone() + } - /// Check if [`Server`] is not syncing (disabled or just running after synchronization). - pub fn not_syncing() -> bool { - match Node::get_sync_status() { - None => true, - Some(ss) => ss == SyncStatus::NoSync - } - } + /// Check if [`Server`] is not syncing (disabled or just running after synchronization). + pub fn not_syncing() -> bool { + match Node::get_sync_status() { + None => true, + Some(ss) => ss == SyncStatus::NoSync, + } + } - /// Get synchronization status, empty when [`Server`] is not running. - pub fn get_sync_status() -> Option { - // Return Shutdown status when node is stopping. - if Self::is_stopping() { - return Some(SyncStatus::Shutdown); - } + /// Get synchronization status, empty when [`Server`] is not running. + pub fn get_sync_status() -> Option { + // Return Shutdown status when node is stopping. + if Self::is_stopping() { + return Some(SyncStatus::Shutdown); + } - // Return Initial status when node is starting or restarting or peers are deleting. - if Self::is_starting() || Self::is_restarting() { - return Some(SyncStatus::Initial); - } + // Return Initial status when node is starting or restarting or peers are deleting. + if Self::is_starting() || Self::is_restarting() { + return Some(SyncStatus::Initial); + } - let stats = Self::get_stats(); - // Return sync status when server is running (stats are not empty). - if stats.is_some() { - return Some(stats.as_ref().unwrap().sync_status); - } - None - } + let stats = Self::get_stats(); + // Return sync status when server is running (stats are not empty). + if stats.is_some() { + return Some(stats.as_ref().unwrap().sync_status); + } + None + } - /// Get [`Server`] error. - pub fn get_error() -> Option { - let r_err = NODE_STATE.error.read(); - if r_err.is_some() { - let e = r_err.as_ref().unwrap(); - // Flag setup to show an error to clean up data. - let store_err = match e { - Error::Store(_) => true, - Error::Chain(_) => true, - _ => false - }; - if store_err { - return Some(NodeError::Storage); - } + /// Get [`Server`] error. + pub fn get_error() -> Option { + let r_err = NODE_STATE.error.read(); + if r_err.is_some() { + let e = r_err.as_ref().unwrap(); + // Flag setup to show an error to clean up data. + let store_err = match e { + Error::Store(_) => true, + Error::Chain(_) => true, + _ => false, + }; + if store_err { + return Some(NodeError::Storage); + } - // Flag setup to show P2P or API server error. - let p2p_api_err = match e { - Error::P2P(_) => Some(NodeError::P2P), - Error::API(_) => Some(NodeError::API), - _ => None - }; - if p2p_api_err.is_some() { - return p2p_api_err; - } + // Flag setup to show P2P or API server error. + let p2p_api_err = match e { + Error::P2P(_) => Some(NodeError::P2P), + Error::API(_) => Some(NodeError::API), + _ => None, + }; + if p2p_api_err.is_some() { + return p2p_api_err; + } - // Flag setup to show configuration error. - let config_err = match e { - Error::Configuration(_) => true, - _ => false - }; - return if config_err { - Some(NodeError::Configuration) - } else { - Some(NodeError::Unknown) - } - } - None - } + // Flag setup to show configuration error. + let config_err = match e { + Error::Configuration(_) => true, + _ => false, + }; + return if config_err { + Some(NodeError::Configuration) + } else { + Some(NodeError::Unknown) + }; + } + None + } - /// Start the [`Server`] at separate thread to update state with stats and handle statuses. - fn start_server_thread() { - thread::spawn(move || { - NODE_STATE.starting.store(true, Ordering::Relaxed); - // Start the server. - match start_node_server() { - Ok(mut server) => { - let mut first_start = true; - loop { - // Restart server if request or peers clean up is needed - if Self::is_restarting() { - server.stop(); - // Wait server after stop. - thread::sleep(Duration::from_millis(5000)); - // Reset data if requested. - if Self::reset_data_needed() { - Node::reset_data(true); - } - // Reset stratum stats. - { - let mut w_stratum_stats = NODE_STATE.stratum_stats.write(); - *w_stratum_stats = StratumStats::default(); - } - // Create new server. - match start_node_server() { - Ok(s) => { - server = s; - NODE_STATE.restart_needed.store(false, Ordering::Relaxed); - } - Err(e) => { - error!("Error starting node: {:?}", e); - { - let mut w_err = NODE_STATE.error.write(); - *w_err = Some(e); - } - // Reset server state. - Self::reset_server_state(true); - break; - } - } - } else if Self::is_stopping() { - // Stop the server. - server.stop(); - // Clean stats and statuses. - Self::reset_server_state(false); - break; - } + /// Start the [`Server`] at separate thread to update state with stats and handle statuses. + fn start_server_thread() { + thread::spawn(move || { + NODE_STATE.starting.store(true, Ordering::Relaxed); + // Start the server. + match start_node_server() { + Ok(mut server) => { + let mut first_start = true; + loop { + // Restart server if request or peers clean up is needed + if Self::is_restarting() { + server.stop(); + // Wait server after stop. + thread::sleep(Duration::from_millis(5000)); + // Reset data if requested. + if Self::reset_data_needed() { + Node::reset_data(true); + } + // Reset stratum stats. + { + let mut w_stratum_stats = NODE_STATE.stratum_stats.write(); + *w_stratum_stats = StratumStats::default(); + } + // Create new server. + match start_node_server() { + Ok(s) => { + server = s; + NODE_STATE.restart_needed.store(false, Ordering::Relaxed); + } + Err(e) => { + error!("Error starting node: {:?}", e); + { + let mut w_err = NODE_STATE.error.write(); + *w_err = Some(e); + } + // Reset server state. + Self::reset_server_state(true); + break; + } + } + } else if Self::is_stopping() { + // Stop the server. + server.stop(); + // Clean stats and statuses. + Self::reset_server_state(false); + break; + } - // Start stratum mining server if requested. - let stratum_start_requested = Self::is_stratum_starting(); - if stratum_start_requested { - let (s_ip, s_port) = NodeConfig::get_stratum_address(); - if NodeConfig::is_stratum_port_available(&s_ip, &s_port) { - let stratum_config = server - .config - .stratum_mining_config - .clone() - .unwrap(); - start_stratum_mining_server(&server, stratum_config); - } - } + // Start stratum mining server if requested. + let stratum_start_requested = Self::is_stratum_starting(); + if stratum_start_requested { + let (s_ip, s_port) = NodeConfig::get_stratum_address(); + if NodeConfig::is_stratum_port_available(&s_ip, &s_port) { + let stratum_config = + server.config.stratum_mining_config.clone().unwrap(); + start_stratum_mining_server(&server, stratum_config); + } + } - // Update server stats. - if let Ok(stats) = server.get_server_stats() { - { - let mut w_stats = NODE_STATE.stats.write(); - *w_stats = Some(stats.clone()); - } + // Update server stats. + if let Ok(stats) = server.get_server_stats() { + { + let mut w_stats = NODE_STATE.stats.write(); + *w_stats = Some(stats.clone()); + } - if first_start { - NODE_STATE.starting.store(false, Ordering::Relaxed); - first_start = false; - } - } + if first_start { + NODE_STATE.starting.store(false, Ordering::Relaxed); + first_start = false; + } + } - // Reset stratum server start flag. - if stratum_start_requested && NODE_STATE.stratum_stats.read().is_running { - NODE_STATE.start_stratum_needed.store(false, Ordering::Relaxed); - } + // Reset stratum server start flag. + if stratum_start_requested && NODE_STATE.stratum_stats.read().is_running { + NODE_STATE + .start_stratum_needed + .store(false, Ordering::Relaxed); + } - thread::sleep(Self::STATS_UPDATE_DELAY); - } - } - Err(e) => { - error!("Error starting node: {:?}", e); - { - let mut w_err = NODE_STATE.error.write(); - *w_err = Some(e); - } - // Reset server state. - Self::reset_server_state(true); - } - } - }); - } + thread::sleep(Self::STATS_UPDATE_DELAY); + } + } + Err(e) => { + error!("Error starting node: {:?}", e); + { + let mut w_err = NODE_STATE.error.write(); + *w_err = Some(e); + } + // Reset server state. + Self::reset_server_state(true); + } + } + }); + } - /// Clean up [`Server`] stats and statuses. - fn reset_server_state(has_error: bool) { - NODE_STATE.starting.store(false, Ordering::Relaxed); - NODE_STATE.restart_needed.store(false, Ordering::Relaxed); - NODE_STATE.start_stratum_needed.store(false, Ordering::Relaxed); - NODE_STATE.stop_needed.store(false, Ordering::Relaxed); + /// Clean up [`Server`] stats and statuses. + fn reset_server_state(has_error: bool) { + NODE_STATE.starting.store(false, Ordering::Relaxed); + NODE_STATE.restart_needed.store(false, Ordering::Relaxed); + NODE_STATE + .start_stratum_needed + .store(false, Ordering::Relaxed); + NODE_STATE.stop_needed.store(false, Ordering::Relaxed); - // Reset stratum stats. - { - let mut w_stratum_stats = NODE_STATE.stratum_stats.write(); - *w_stratum_stats = StratumStats::default(); - } - // Reset server stats. - { - let mut w_stats = NODE_STATE.stats.write(); - *w_stats = None; - } - // Reset an error if needed. - if !has_error { - let mut w_err = NODE_STATE.error.write(); - *w_err = None; - } - } + // Reset stratum stats. + { + let mut w_stratum_stats = NODE_STATE.stratum_stats.write(); + *w_stratum_stats = StratumStats::default(); + } + // Reset server stats. + { + let mut w_stats = NODE_STATE.stats.write(); + *w_stats = None; + } + // Reset an error if needed. + if !has_error { + let mut w_err = NODE_STATE.error.write(); + *w_err = None; + } + } - /// Change chain data directory. - pub fn change_data_dir(path: String) { - if Self::data_dir_changing() || NodeConfig::get_chain_data_path() == path { - return; - } - NODE_STATE.change_data_dir.store(true, Ordering::Relaxed); - thread::spawn(move || { - let running = Node::is_running(); - if running { - Node::stop(false); - // Wait node to stop before moving files. - while Node::is_running() { - thread::sleep(Self::STATS_UPDATE_DELAY); - } - } - let cfg_path = NodeConfig::get_chain_data_path(); - let old = Path::new(cfg_path.as_str()); - let new = Path::new(path.as_str()); - if !old.exists() { - NodeConfig::save_chain_data_path(path); - } else { - fs::create_dir_all(&new).unwrap_or_default(); - if let Ok(_) = fs::rename(old, new) { - NodeConfig::save_chain_data_path(path); - } else { - fs::remove_dir_all(old).unwrap(); - NodeConfig::save_chain_data_path(path); - } - } - NODE_STATE.change_data_dir.store(false, Ordering::Relaxed); - // Restart node after migration. - if running && !Node::is_stopping() { - Node::start(); - } - }); - } + /// Change chain data directory. + pub fn change_data_dir(path: String) { + if Self::data_dir_changing() || NodeConfig::get_chain_data_path() == path { + return; + } + NODE_STATE.change_data_dir.store(true, Ordering::Relaxed); + thread::spawn(move || { + let running = Node::is_running(); + if running { + Node::stop(false); + // Wait node to stop before moving files. + while Node::is_running() { + thread::sleep(Self::STATS_UPDATE_DELAY); + } + } + let cfg_path = NodeConfig::get_chain_data_path(); + let old = Path::new(cfg_path.as_str()); + let new = Path::new(path.as_str()); + if !old.exists() { + NodeConfig::save_chain_data_path(path); + } else { + fs::create_dir_all(&new).unwrap_or_default(); + if let Ok(_) = fs::rename(old, new) { + NodeConfig::save_chain_data_path(path); + } else { + fs::remove_dir_all(old).unwrap(); + NodeConfig::save_chain_data_path(path); + } + } + NODE_STATE.change_data_dir.store(false, Ordering::Relaxed); + // Restart node after migration. + if running && !Node::is_stopping() { + Node::start(); + } + }); + } - /// Check if chain data directory is changing. - pub fn data_dir_changing() -> bool { - NODE_STATE.change_data_dir.load(Ordering::Relaxed) - } + /// Check if chain data directory is changing. + pub fn data_dir_changing() -> bool { + NODE_STATE.change_data_dir.load(Ordering::Relaxed) + } - /// Clean-up [`Server`] data if server is not running. - pub fn clean_up_data() { - if Self::is_running() { - return; - } - let config = NodeConfig::node_server_config(); - let server_config = config.server.clone(); - let dirs_to_remove: Vec<&str> = vec!["header", "lmdb", "txhashset"]; - for dir in dirs_to_remove { - let mut path = PathBuf::from(&server_config.db_root); - path.push(dir); - if path.exists() { - fs::remove_dir_all(path).unwrap(); - } - } - } + /// Clean-up [`Server`] data if server is not running. + pub fn clean_up_data() { + if Self::is_running() { + return; + } + let config = NodeConfig::node_server_config(); + let server_config = config.server.clone(); + let dirs_to_remove: Vec<&str> = vec!["header", "lmdb", "txhashset"]; + for dir in dirs_to_remove { + let mut path = PathBuf::from(&server_config.db_root); + path.push(dir); + if path.exists() { + fs::remove_dir_all(path).unwrap(); + } + } + } - /// Reset [`Server`] data. - pub fn reset_data(force: bool) { - if force || !Node::is_running() { - let config = NodeConfig::node_server_config(); - let server_config = config.server.clone(); - // Remove data folder. - let data_dir = PathBuf::from(&server_config.db_root); - match fs::remove_dir_all(data_dir) { - Ok(_) => {} - Err(_) => {} - } - NODE_STATE.reset_data.store(false, Ordering::Relaxed); - } else { - NODE_STATE.reset_data.store(true, Ordering::Relaxed); - } - } + /// Reset [`Server`] data. + pub fn reset_data(force: bool) { + if force || !Node::is_running() { + let config = NodeConfig::node_server_config(); + let server_config = config.server.clone(); + // Remove data folder. + let data_dir = PathBuf::from(&server_config.db_root); + match fs::remove_dir_all(data_dir) { + Ok(_) => {} + Err(_) => {} + } + NODE_STATE.reset_data.store(false, Ordering::Relaxed); + } else { + NODE_STATE.reset_data.store(true, Ordering::Relaxed); + } + } - /// Get synchronization status i18n text. - pub fn get_sync_status_text() -> String { - if Node::data_dir_changing() { - return t!("moving_files").into(); - } + /// Get synchronization status i18n text. + pub fn get_sync_status_text() -> String { + if Node::data_dir_changing() { + return t!("moving_files").into(); + } - if Node::is_stopping() { - return t!("sync_status.shutdown").into(); - }; - if Node::is_starting() { - return t!("sync_status.initial").into(); - }; - if Node::is_restarting() { - return t!("sync_status.node_restarting").into(); - } + if Node::is_stopping() { + return t!("sync_status.shutdown").into(); + }; + if Node::is_starting() { + return t!("sync_status.initial").into(); + }; + if Node::is_restarting() { + return t!("sync_status.node_restarting").into(); + } - let sync_status = Self::get_sync_status(); + let sync_status = Self::get_sync_status(); - if sync_status.is_none() { - return t!("sync_status.node_down").into(); - } + if sync_status.is_none() { + return t!("sync_status.node_down").into(); + } - let sync_status = match sync_status.unwrap() { - SyncStatus::Initial => t!("sync_status.initial"), - SyncStatus::NoSync => t!("sync_status.no_sync"), - SyncStatus::AwaitingPeers(_) => t!("sync_status.awaiting_peers"), - SyncStatus::HeaderSync { - sync_head, - highest_height, - .. - } => { - if highest_height == 0 { - t!("sync_status.header_sync") - } else { - let percent = sync_head.height * 100 / highest_height; - t!("sync_status.header_sync_percent", "percent" => percent) - } - } - SyncStatus::TxHashsetPibd { - aborted: _, - errored: _, - completed_leaves, - leaves_required, - completed_to_height: _, - required_height: _, - } => { - if completed_leaves == 0 { - t!("sync_status.tx_hashset_pibd") - } else { - let percent = completed_leaves * 100 / leaves_required; - t!("sync_status.tx_hashset_pibd_percent", "percent" => percent) - } - } - SyncStatus::TxHashsetDownload(stat) => { - if stat.total_size > 0 { - let percent = stat.downloaded_size * 100 / stat.total_size; - t!("sync_status.tx_hashset_download_percent", "percent" => percent) - } else { - t!("sync_status.tx_hashset_download") - } - } - SyncStatus::TxHashsetSetup { - headers, - headers_total, - kernel_pos, - kernel_pos_total, - } => { - if headers.is_some() && headers_total.is_some() { - let h = headers.unwrap(); - let ht = headers_total.unwrap(); - let percent = h * 100 / ht; - t!("sync_status.tx_hashset_setup_history", "percent" => percent) - } else if kernel_pos.is_some() && kernel_pos_total.is_some() { - let k = kernel_pos.unwrap(); - let kt = kernel_pos_total.unwrap(); - let percent = k * 100 / kt; - t!("sync_status.tx_hashset_setup_position", "percent" => percent) - } else { - t!("sync_status.tx_hashset_setup") - } - } - SyncStatus::TxHashsetRangeProofsValidation { - rproofs, - rproofs_total, - } => { - let r_percent = if rproofs_total > 0 { - (rproofs * 100) / rproofs_total - } else { - 0 - }; - t!("sync_status.tx_hashset_range_proofs_validation", "percent" => r_percent) - } - SyncStatus::TxHashsetKernelsValidation { - kernels, - kernels_total, - } => { - let k_percent = if kernels_total > 0 { - (kernels * 100) / kernels_total - } else { - 0 - }; - t!("sync_status.tx_hashset_kernels_validation", "percent" => k_percent) - } - SyncStatus::TxHashsetSave | SyncStatus::TxHashsetDone => { - t!("sync_status.tx_hashset_save") - } - SyncStatus::BodySync { - current_height, - highest_height, - } => { - if highest_height == 0 { - t!("sync_status.body_sync") - } else { - let percent = current_height * 100 / highest_height; - t!("sync_status.body_sync_percent", "percent" => percent) - } - } - SyncStatus::Shutdown => t!("sync_status.shutdown"), - }; - sync_status.into() - } + let sync_status = match sync_status.unwrap() { + SyncStatus::Initial => t!("sync_status.initial"), + SyncStatus::NoSync => t!("sync_status.no_sync"), + SyncStatus::AwaitingPeers(_) => t!("sync_status.awaiting_peers"), + SyncStatus::HeaderSync { + sync_head, + highest_height, + .. + } => { + if highest_height == 0 { + t!("sync_status.header_sync") + } else { + let percent = sync_head.height * 100 / highest_height; + t!("sync_status.header_sync_percent", "percent" => percent) + } + } + SyncStatus::TxHashsetPibd { + aborted: _, + errored: _, + completed_leaves, + leaves_required, + completed_to_height: _, + required_height: _, + } => { + if completed_leaves == 0 { + t!("sync_status.tx_hashset_pibd") + } else { + let percent = completed_leaves * 100 / leaves_required; + t!("sync_status.tx_hashset_pibd_percent", "percent" => percent) + } + } + SyncStatus::TxHashsetDownload(stat) => { + if stat.total_size > 0 { + let percent = stat.downloaded_size * 100 / stat.total_size; + t!("sync_status.tx_hashset_download_percent", "percent" => percent) + } else { + t!("sync_status.tx_hashset_download") + } + } + SyncStatus::TxHashsetSetup { + headers, + headers_total, + kernel_pos, + kernel_pos_total, + } => { + if headers.is_some() && headers_total.is_some() { + let h = headers.unwrap(); + let ht = headers_total.unwrap(); + let percent = h * 100 / ht; + t!("sync_status.tx_hashset_setup_history", "percent" => percent) + } else if kernel_pos.is_some() && kernel_pos_total.is_some() { + let k = kernel_pos.unwrap(); + let kt = kernel_pos_total.unwrap(); + let percent = k * 100 / kt; + t!("sync_status.tx_hashset_setup_position", "percent" => percent) + } else { + t!("sync_status.tx_hashset_setup") + } + } + SyncStatus::TxHashsetRangeProofsValidation { + rproofs, + rproofs_total, + } => { + let r_percent = if rproofs_total > 0 { + (rproofs * 100) / rproofs_total + } else { + 0 + }; + t!("sync_status.tx_hashset_range_proofs_validation", "percent" => r_percent) + } + SyncStatus::TxHashsetKernelsValidation { + kernels, + kernels_total, + } => { + let k_percent = if kernels_total > 0 { + (kernels * 100) / kernels_total + } else { + 0 + }; + t!("sync_status.tx_hashset_kernels_validation", "percent" => k_percent) + } + SyncStatus::TxHashsetSave | SyncStatus::TxHashsetDone => { + t!("sync_status.tx_hashset_save") + } + SyncStatus::BodySync { + current_height, + highest_height, + } => { + if highest_height == 0 { + t!("sync_status.body_sync") + } else { + let percent = current_height * 100 / highest_height; + t!("sync_status.body_sync_percent", "percent" => percent) + } + } + SyncStatus::Shutdown => t!("sync_status.shutdown"), + }; + sync_status.into() + } } /// Start the node [`Server`]. -fn start_node_server() -> Result { - // Setup server config. - let mut config = NodeConfig::node_server_config(); - PeersConfig::load_to_server_config(&mut config); - let mut server_config = config.server.clone(); +fn start_node_server() -> Result { + // Setup server config. + let mut config = NodeConfig::node_server_config(); + PeersConfig::load_to_server_config(&mut config); + let mut server_config = config.server.clone(); - // DNS seed setup. - if NodeConfig::is_default_seeding_type() { - server_config.p2p_config.seeding_type = Seeding::List; - server_config.p2p_config.seeds = Some(PeerAddrs::default()); - let is_mainnet = server_config.chain_type == ChainTypes::Mainnet; - let seed_list = if is_mainnet { - Node::MAINNET_DNS_SEEDS - } else { - Node::TESTNET_DNS_SEEDS - }; - let seed_port = if is_mainnet { - 3414 - } else { - 13414 - }; - for seed_addr in seed_list { - let addr = format!("{}:{}", seed_addr, seed_port); - if let Some(p) = PeersConfig::peer_to_addr(addr) { - server_config.p2p_config.seeds.as_mut().unwrap().peers.push(p) - } - } - } + // DNS seed setup. + if NodeConfig::is_default_seeding_type() { + server_config.p2p_config.seeding_type = Seeding::List; + server_config.p2p_config.seeds = Some(PeerAddrs::default()); + let is_mainnet = server_config.chain_type == ChainTypes::Mainnet; + let seed_list = if is_mainnet { + Node::MAINNET_DNS_SEEDS + } else { + Node::TESTNET_DNS_SEEDS + }; + let seed_port = if is_mainnet { 3414 } else { 13414 }; + for seed_addr in seed_list { + let addr = format!("{}:{}", seed_addr, seed_port); + if let Some(p) = PeersConfig::peer_to_addr(addr) { + server_config + .p2p_config + .seeds + .as_mut() + .unwrap() + .peers + .push(p) + } + } + } - // Fix to avoid too many opened files. - server_config.p2p_config.peer_min_preferred_outbound_count = - server_config.p2p_config.peer_max_outbound_count; + // Fix to avoid too many opened files. + server_config.p2p_config.peer_min_preferred_outbound_count = + server_config.p2p_config.peer_max_outbound_count; - // Remove temporary file dir. - { - let mut tmp_dir = PathBuf::from(&server_config.db_root); - tmp_dir = tmp_dir.parent().unwrap().to_path_buf(); - tmp_dir.push("tmp"); - if tmp_dir.exists() { - match fs::remove_dir_all(tmp_dir) { - Ok(_) => {} - Err(_) => {} - } - } - } + // Remove temporary file dir. + { + let mut tmp_dir = PathBuf::from(&server_config.db_root); + tmp_dir = tmp_dir.parent().unwrap().to_path_buf(); + tmp_dir.push("tmp"); + if tmp_dir.exists() { + match fs::remove_dir_all(tmp_dir) { + Ok(_) => {} + Err(_) => {} + } + } + } - // Initialize our global chain_type, feature flags (NRD kernel support currently), - // accept_fee_base, and future_time_limit. - // These are read via global and not read from config beyond this point. - if !global::GLOBAL_CHAIN_TYPE.is_init() { - global::init_global_chain_type(config.server.chain_type); - } else { - global::set_global_chain_type(config.server.chain_type); - global::set_local_chain_type(config.server.chain_type); - } + // Initialize our global chain_type, feature flags (NRD kernel support currently), + // accept_fee_base, and future_time_limit. + // These are read via global and not read from config beyond this point. + if !global::GLOBAL_CHAIN_TYPE.is_init() { + global::init_global_chain_type(config.server.chain_type); + } else { + global::set_global_chain_type(config.server.chain_type); + global::set_local_chain_type(config.server.chain_type); + } - if !global::GLOBAL_NRD_FEATURE_ENABLED.is_init() { - match global::get_chain_type() { - ChainTypes::Mainnet => { - global::init_global_nrd_enabled(false); - } - _ => { - global::init_global_nrd_enabled(true); - } - } - } else { - match global::get_chain_type() { - ChainTypes::Mainnet => { - global::set_global_nrd_enabled(false); - } - _ => { - global::set_global_nrd_enabled(true); - } - } - } + if !global::GLOBAL_NRD_FEATURE_ENABLED.is_init() { + match global::get_chain_type() { + ChainTypes::Mainnet => { + global::init_global_nrd_enabled(false); + } + _ => { + global::init_global_nrd_enabled(true); + } + } + } else { + match global::get_chain_type() { + ChainTypes::Mainnet => { + global::set_global_nrd_enabled(false); + } + _ => { + global::set_global_nrd_enabled(true); + } + } + } - let afb = config.server.pool_config.accept_fee_base; - if !global::GLOBAL_ACCEPT_FEE_BASE.is_init() { - global::init_global_accept_fee_base(afb); - } else { - global::set_global_accept_fee_base(afb); - } + let afb = config.server.pool_config.accept_fee_base; + if !global::GLOBAL_ACCEPT_FEE_BASE.is_init() { + global::init_global_accept_fee_base(afb); + } else { + global::set_global_accept_fee_base(afb); + } - let future_time_limit = config.server.future_time_limit; - if !global::GLOBAL_FUTURE_TIME_LIMIT.is_init() { - global::init_global_future_time_limit(future_time_limit); - } else { - global::set_global_future_time_limit(future_time_limit); - } + let future_time_limit = config.server.future_time_limit; + if !global::GLOBAL_FUTURE_TIME_LIMIT.is_init() { + global::init_global_future_time_limit(future_time_limit); + } else { + global::set_global_future_time_limit(future_time_limit); + } - // Put flag to start stratum server if autorun is available. - if NodeConfig::is_stratum_autorun_enabled() { - NODE_STATE.start_stratum_needed.store(true, Ordering::Relaxed); - } + // Put flag to start stratum server if autorun is available. + if NodeConfig::is_stratum_autorun_enabled() { + NODE_STATE + .start_stratum_needed + .store(true, Ordering::Relaxed); + } - // Reset an error. - { - let mut w_err = NODE_STATE.error.write(); - *w_err = None; - } + // Reset an error. + { + let mut w_err = NODE_STATE.error.write(); + *w_err = None; + } - // Start integrated node server. - let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) = - Box::leak(Box::new(oneshot::channel::<()>())); - let server_result = Server::new(server_config, None, api_chan); - server_result + // Start integrated node server. + let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) = + Box::leak(Box::new(oneshot::channel::<()>())); + let server_result = Server::new(server_config, None, api_chan); + server_result } /// Start stratum mining server on a separate thread. pub fn start_stratum_mining_server(server: &Server, config: StratumServerConfig) { - let proof_size = global::proofsize(); - let sync_state = server.sync_state.clone(); + let proof_size = global::proofsize(); + let sync_state = server.sync_state.clone(); - let mut stratum_server = StratumServer::new( - config, - server.chain.clone(), - server.tx_pool.clone(), - NODE_STATE.stratum_stats.clone(), - ); - let stop_state = NODE_STATE.stratum_stop_state.clone(); - stop_state.reset(); - let server_state = stop_state.clone(); - thread::spawn(move || { - stratum_server.run_loop(proof_size, sync_state, stop_state); - server_state.reset(); - // Reset stratum stats. - { - let mut w_stratum_stats = NODE_STATE.stratum_stats.write(); - *w_stratum_stats = StratumStats::default(); - } - }); + let mut stratum_server = StratumServer::new( + config, + server.chain.clone(), + server.tx_pool.clone(), + NODE_STATE.stratum_stats.clone(), + ); + let stop_state = NODE_STATE.stratum_stop_state.clone(); + stop_state.reset(); + let server_state = stop_state.clone(); + thread::spawn(move || { + stratum_server.run_loop(proof_size, sync_state, stop_state); + server_state.reset(); + // Reset stratum stats. + { + let mut w_stratum_stats = NODE_STATE.stratum_stats.write(); + *w_stratum_stats = StratumStats::default(); + } + }); } #[allow(dead_code)] @@ -708,13 +717,13 @@ pub fn start_stratum_mining_server(server: &Server, config: StratumServerConfig) #[unsafe(no_mangle)] /// Get sync status text for Android notification from [`NODE_STATE`] in Java string format. pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncStatusText( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - _activity: jni::objects::JObject, + _env: jni::JNIEnv, + _class: jni::objects::JObject, + _activity: jni::objects::JObject, ) -> jni::sys::jstring { - let status_text = Node::get_sync_status_text(); - let j_text = _env.new_string(status_text); - return j_text.unwrap().into_raw(); + let status_text = Node::get_sync_status_text(); + let j_text = _env.new_string(status_text); + return j_text.unwrap().into_raw(); } #[allow(dead_code)] @@ -723,12 +732,12 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncStatusText( #[unsafe(no_mangle)] /// Get sync title for Android notification in Java string format. pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncTitle( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - _activity: jni::objects::JObject, + _env: jni::JNIEnv, + _class: jni::objects::JObject, + _activity: jni::objects::JObject, ) -> jni::sys::jstring { - let j_text = _env.new_string(t!("network.node")); - return j_text.unwrap().into_raw(); + let j_text = _env.new_string(t!("network.node")); + return j_text.unwrap().into_raw(); } #[allow(dead_code)] @@ -737,12 +746,12 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncTitle( #[unsafe(no_mangle)] /// Get start text for Android notification in Java string format. pub extern "C" fn Java_mw_gri_android_BackgroundService_getStartText( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - _activity: jni::objects::JObject, + _env: jni::JNIEnv, + _class: jni::objects::JObject, + _activity: jni::objects::JObject, ) -> jni::sys::jstring { - let j_text = _env.new_string(t!("network_settings.enable")); - return j_text.unwrap().into_raw(); + let j_text = _env.new_string(t!("network_settings.enable")); + return j_text.unwrap().into_raw(); } #[allow(dead_code)] @@ -751,12 +760,12 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_getStartText( #[unsafe(no_mangle)] /// Get stop text for Android notification in Java string format. pub extern "C" fn Java_mw_gri_android_BackgroundService_getStopText( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - _activity: jni::objects::JObject, + _env: jni::JNIEnv, + _class: jni::objects::JObject, + _activity: jni::objects::JObject, ) -> jni::sys::jstring { - let j_text = _env.new_string(t!("network_settings.disable")); - return j_text.unwrap().into_raw(); + let j_text = _env.new_string(t!("network_settings.disable")); + return j_text.unwrap().into_raw(); } #[allow(dead_code)] @@ -765,12 +774,12 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_getStopText( #[unsafe(no_mangle)] /// Get exit text for Android notification in Java string format. pub extern "C" fn Java_mw_gri_android_BackgroundService_getExitText( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - _activity: jni::objects::JObject, + _env: jni::JNIEnv, + _class: jni::objects::JObject, + _activity: jni::objects::JObject, ) -> jni::sys::jstring { - let j_text = _env.new_string(t!("modal_exit.exit")); - return j_text.unwrap().into_raw(); + let j_text = _env.new_string(t!("modal_exit.exit")); + return j_text.unwrap().into_raw(); } #[allow(dead_code)] @@ -779,12 +788,12 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_getExitText( #[unsafe(no_mangle)] /// Check if node launch is possible. pub extern "C" fn Java_mw_gri_android_BackgroundService_canStartNode( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - _activity: jni::objects::JObject, + _env: jni::JNIEnv, + _class: jni::objects::JObject, + _activity: jni::objects::JObject, ) -> jni::sys::jboolean { - let loading = Node::is_stopping() || Node::is_restarting() || Node::is_starting(); - return (!loading && !Node::is_running()) as jni::sys::jboolean; + let loading = Node::is_stopping() || Node::is_restarting() || Node::is_starting(); + return (!loading && !Node::is_running()) as jni::sys::jboolean; } #[allow(dead_code)] @@ -793,12 +802,12 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_canStartNode( #[unsafe(no_mangle)] /// Check if node stop is possible. pub extern "C" fn Java_mw_gri_android_BackgroundService_canStopNode( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - _activity: jni::objects::JObject, + _env: jni::JNIEnv, + _class: jni::objects::JObject, + _activity: jni::objects::JObject, ) -> jni::sys::jboolean { - let loading = Node::is_stopping() || Node::is_restarting() || Node::is_starting(); - return (!loading && Node::is_running()) as jni::sys::jboolean; + let loading = Node::is_stopping() || Node::is_restarting() || Node::is_starting(); + return (!loading && Node::is_running()) as jni::sys::jboolean; } #[allow(dead_code)] @@ -807,11 +816,11 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_canStopNode( #[unsafe(no_mangle)] /// Start node from Android Java code. pub extern "C" fn Java_mw_gri_android_NotificationActionsReceiver_startNode( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - _activity: jni::objects::JObject, + _env: jni::JNIEnv, + _class: jni::objects::JObject, + _activity: jni::objects::JObject, ) { - Node::start(); + Node::start(); } #[allow(dead_code)] @@ -820,11 +829,11 @@ pub extern "C" fn Java_mw_gri_android_NotificationActionsReceiver_startNode( #[unsafe(no_mangle)] /// Stop node from Android Java code. pub extern "C" fn Java_mw_gri_android_NotificationActionsReceiver_stopNode( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - _activity: jni::objects::JObject, + _env: jni::JNIEnv, + _class: jni::objects::JObject, + _activity: jni::objects::JObject, ) { - Node::stop(false); + Node::stop(false); } #[allow(dead_code)] @@ -833,15 +842,15 @@ pub extern "C" fn Java_mw_gri_android_NotificationActionsReceiver_stopNode( #[unsafe(no_mangle)] /// Stop node from Android Java code. pub extern "C" fn Java_mw_gri_android_NotificationActionsReceiver_stopNodeToExit( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - _activity: jni::objects::JObject, + _env: jni::JNIEnv, + _class: jni::objects::JObject, + _activity: jni::objects::JObject, ) { - if Node::is_running() { - Node::stop(true); - } else { - NODE_STATE.exit_after_stop.store(true, Ordering::Relaxed); - } + if Node::is_running() { + Node::stop(true); + } else { + NODE_STATE.exit_after_stop.store(true, Ordering::Relaxed); + } } #[allow(dead_code)] @@ -850,12 +859,12 @@ pub extern "C" fn Java_mw_gri_android_NotificationActionsReceiver_stopNodeToExit #[unsafe(no_mangle)] /// Check if app exit is needed after node stop to finish Android app at background. pub extern "C" fn Java_mw_gri_android_BackgroundService_exitAppAfterNodeStop( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - _activity: jni::objects::JObject, + _env: jni::JNIEnv, + _class: jni::objects::JObject, + _activity: jni::objects::JObject, ) -> jni::sys::jboolean { - let exit_needed = !Node::is_running() && NODE_STATE.exit_after_stop.load(Ordering::Relaxed); - return exit_needed as jni::sys::jboolean; + let exit_needed = !Node::is_running() && NODE_STATE.exit_after_stop.load(Ordering::Relaxed); + return exit_needed as jni::sys::jboolean; } #[allow(dead_code)] @@ -864,9 +873,9 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_exitAppAfterNodeStop( #[unsafe(no_mangle)] /// Handle unexpected application termination on Android (removal from recent apps). pub extern "C" fn Java_mw_gri_android_MainActivity_onTermination( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - _activity: jni::objects::JObject, + _env: jni::JNIEnv, + _class: jni::objects::JObject, + _activity: jni::objects::JObject, ) { - Node::stop(false); -} \ No newline at end of file + Node::stop(false); +} diff --git a/src/node/stratum.rs b/src/node/stratum.rs index 4406665f..d0c9acd0 100644 --- a/src/node/stratum.rs +++ b/src/node/stratum.rs @@ -21,8 +21,9 @@ use tokio_old::net::TcpListener; use tokio_old::runtime::Runtime; use tokio_util_old::codec::{Framed, LinesCodec}; -use grin_util::RwLock; use chrono::prelude::Utc; +use futures::future::{AbortHandle, abortable}; +use grin_util::RwLock; use serde_json::Value; use std::collections::HashMap; use std::net::SocketAddr; @@ -30,23 +31,22 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; use std::time::{Duration, SystemTime}; -use futures::future::{abortable, AbortHandle}; use grin_chain::{self, SyncState}; -use grin_servers::common::stats::{StratumStats, WorkerStats}; -use grin_servers::common::types::StratumServerConfig; use grin_core::consensus::graph_weight; -use grin_core::core::hash::Hashed; use grin_core::core::Block; +use grin_core::core::hash::Hashed; use grin_core::global::min_edge_bits; use grin_core::{pow, ser}; -use grin_util::ToHex; use grin_servers::ServerTxPool; +use grin_servers::common::stats::{StratumStats, WorkerStats}; +use grin_servers::common::types::StratumServerConfig; +use grin_util::ToHex; -use log::{debug, error}; -use serde_derive::{Deserialize, Serialize}; use crate::node::mine_block::get_block; use crate::wallet::WalletConfig; +use log::{debug, error}; +use serde_derive::{Deserialize, Serialize}; type Tx = mpsc::UnboundedSender; @@ -59,921 +59,942 @@ type Tx = mpsc::UnboundedSender; #[derive(Serialize, Deserialize, Debug, PartialEq)] #[serde(untagged)] enum JsonId { - IntId(u32), - StrId(String), + IntId(u32), + StrId(String), } #[derive(Serialize, Deserialize, Debug, PartialEq)] struct RpcRequest { - id: JsonId, - jsonrpc: String, - method: String, - params: Option, + id: JsonId, + jsonrpc: String, + method: String, + params: Option, } #[derive(Serialize, Deserialize, Debug, PartialEq)] struct RpcResponse { - id: JsonId, - jsonrpc: String, - method: String, - result: Option, - error: Option, + id: JsonId, + jsonrpc: String, + method: String, + result: Option, + error: Option, } #[derive(Serialize, Deserialize, Debug)] struct RpcError { - code: i32, - message: String, + code: i32, + message: String, } impl RpcError { - pub fn internal_error() -> Self { - RpcError { - code: 32603, - message: "Internal error".to_owned(), - } - } - pub fn node_is_syncing() -> Self { - RpcError { - code: -32000, - message: "Node is syncing - Please wait".to_owned(), - } - } - pub fn method_not_found() -> Self { - RpcError { - code: -32601, - message: "Method not found".to_owned(), - } - } - pub fn too_late() -> Self { - RpcError { - code: -32503, - message: "Solution submitted too late".to_string(), - } - } - pub fn cannot_validate() -> Self { - RpcError { - code: -32502, - message: "Failed to validate solution".to_string(), - } - } - pub fn too_low_difficulty() -> Self { - RpcError { - code: -32501, - message: "Share rejected due to low difficulty".to_string(), - } - } - pub fn invalid_request() -> Self { - RpcError { - code: -32600, - message: "Invalid Request".to_string(), - } - } + pub fn internal_error() -> Self { + RpcError { + code: 32603, + message: "Internal error".to_owned(), + } + } + pub fn node_is_syncing() -> Self { + RpcError { + code: -32000, + message: "Node is syncing - Please wait".to_owned(), + } + } + pub fn method_not_found() -> Self { + RpcError { + code: -32601, + message: "Method not found".to_owned(), + } + } + pub fn too_late() -> Self { + RpcError { + code: -32503, + message: "Solution submitted too late".to_string(), + } + } + pub fn cannot_validate() -> Self { + RpcError { + code: -32502, + message: "Failed to validate solution".to_string(), + } + } + pub fn too_low_difficulty() -> Self { + RpcError { + code: -32501, + message: "Share rejected due to low difficulty".to_string(), + } + } + pub fn invalid_request() -> Self { + RpcError { + code: -32600, + message: "Invalid Request".to_string(), + } + } } impl From for Value { - fn from(e: RpcError) -> Self { - serde_json::to_value(e).unwrap() - } + fn from(e: RpcError) -> Self { + serde_json::to_value(e).unwrap() + } } impl From for RpcError - where - T: std::error::Error, +where + T: std::error::Error, { - fn from(e: T) -> Self { - println!("Received unhandled error: {}", e); - RpcError::internal_error() - } + fn from(e: T) -> Self { + println!("Received unhandled error: {}", e); + RpcError::internal_error() + } } #[derive(Serialize, Deserialize, Debug)] struct LoginParams { - login: String, - pass: String, - agent: String, + login: String, + pass: String, + agent: String, } #[derive(Serialize, Deserialize, Debug)] struct SubmitParams { - height: u64, - job_id: u64, - nonce: u64, - edge_bits: u32, - pow: Vec, + height: u64, + job_id: u64, + nonce: u64, + edge_bits: u32, + pow: Vec, } #[derive(Serialize, Deserialize, Debug)] pub struct JobTemplate { - height: u64, - job_id: u64, - difficulty: u64, - pre_pow: String, + height: u64, + job_id: u64, + difficulty: u64, + pre_pow: String, } #[derive(Serialize, Deserialize, Debug)] pub struct WorkerStatus { - id: String, - height: u64, - difficulty: u64, - accepted: u64, - rejected: u64, - stale: u64, + id: String, + height: u64, + difficulty: u64, + accepted: u64, + rejected: u64, + stale: u64, } struct State { - current_block_versions: Vec, - // to prevent the wallet from generating a new HD key derivation for each - // iteration, we keep the returned derivation to provide it back when - // nothing has changed. We only want to create a key_id for each new block, - // and reuse it when we rebuild the current block to add new tx. - current_key_id: Option, - current_difficulty: u64, // scaled - minimum_share_difficulty: u64, // unscaled + current_block_versions: Vec, + // to prevent the wallet from generating a new HD key derivation for each + // iteration, we keep the returned derivation to provide it back when + // nothing has changed. We only want to create a key_id for each new block, + // and reuse it when we rebuild the current block to add new tx. + current_key_id: Option, + current_difficulty: u64, // scaled + minimum_share_difficulty: u64, // unscaled } impl State { - pub fn new(minimum_share_difficulty: u64) -> Self { - let blocks = vec![Block::default()]; - State { - current_block_versions: blocks, - current_key_id: None, - current_difficulty: ::max_value(), - minimum_share_difficulty: minimum_share_difficulty, - } - } + pub fn new(minimum_share_difficulty: u64) -> Self { + let blocks = vec![Block::default()]; + State { + current_block_versions: blocks, + current_key_id: None, + current_difficulty: ::max_value(), + minimum_share_difficulty: minimum_share_difficulty, + } + } } /// Stratum server stop state to stop it from node thread. pub struct StratumStopState { - stopping: AtomicBool, + stopping: AtomicBool, } impl Default for StratumStopState { - fn default() -> Self { - Self { - stopping: AtomicBool::new(false) - } - } + fn default() -> Self { + Self { + stopping: AtomicBool::new(false), + } + } } impl StratumStopState { - /// Check if stratum server should be stopped. - pub fn is_stopped(&self) -> bool { - self.stopping.load(Ordering::Relaxed) - } + /// Check if stratum server should be stopped. + pub fn is_stopped(&self) -> bool { + self.stopping.load(Ordering::Relaxed) + } - /// Called to stop stratum server from node thread. - pub fn stop(&self) { - self.stopping.store(true, Ordering::Relaxed); - } + /// Called to stop stratum server from node thread. + pub fn stop(&self) { + self.stopping.store(true, Ordering::Relaxed); + } - /// Called before stratum server start to reset state. - pub fn reset(&self) { - self.stopping.store(false, Ordering::Relaxed); - } + /// Called before stratum server start to reset state. + pub fn reset(&self) { + self.stopping.store(false, Ordering::Relaxed); + } } struct Handler { - id: String, - workers: Arc, - sync_state: Arc, - chain: Arc, - current_state: Arc>, + id: String, + workers: Arc, + sync_state: Arc, + chain: Arc, + current_state: Arc>, } impl Handler { - pub fn new( - id: String, - stratum_stats: Arc>, - sync_state: Arc, - minimum_share_difficulty: u64, - chain: Arc, - ) -> Self { - Handler { - id: id, - workers: Arc::new(WorkersList::new(stratum_stats)), - sync_state: sync_state, - chain: chain, - current_state: Arc::new(RwLock::new(State::new(minimum_share_difficulty))), - } - } - pub fn from_stratum(stratum: &StratumServer) -> Self { - Handler::new( - stratum.id.clone(), - stratum.stratum_stats.clone(), - stratum.sync_state.clone(), - stratum.config.minimum_share_difficulty, - stratum.chain.clone(), - ) - } - fn handle_rpc_requests(&self, request: RpcRequest, worker_id: usize) -> String { - self.workers.last_seen(worker_id); + pub fn new( + id: String, + stratum_stats: Arc>, + sync_state: Arc, + minimum_share_difficulty: u64, + chain: Arc, + ) -> Self { + Handler { + id: id, + workers: Arc::new(WorkersList::new(stratum_stats)), + sync_state: sync_state, + chain: chain, + current_state: Arc::new(RwLock::new(State::new(minimum_share_difficulty))), + } + } + pub fn from_stratum(stratum: &StratumServer) -> Self { + Handler::new( + stratum.id.clone(), + stratum.stratum_stats.clone(), + stratum.sync_state.clone(), + stratum.config.minimum_share_difficulty, + stratum.chain.clone(), + ) + } + fn handle_rpc_requests(&self, request: RpcRequest, worker_id: usize) -> String { + self.workers.last_seen(worker_id); - // Call the handler function for requested method - let response = match request.method.as_str() { - "login" => self.handle_login(request.params, worker_id), - "submit" => { - let res = self.handle_submit(request.params, worker_id); - // this key_id has been used now, reset - if let Ok((_, true)) = res { - self.current_state.write().current_key_id = None; - } - res.map(|(v, _)| v) - } - "keepalive" => self.handle_keepalive(), - "getjobtemplate" => { - if self.sync_state.is_syncing() { - Err(RpcError::node_is_syncing()) - } else { - self.handle_getjobtemplate() - } - } - "status" => self.handle_status(worker_id), - _ => { - // Called undefined method - Err(RpcError::method_not_found()) - } - }; + // Call the handler function for requested method + let response = match request.method.as_str() { + "login" => self.handle_login(request.params, worker_id), + "submit" => { + let res = self.handle_submit(request.params, worker_id); + // this key_id has been used now, reset + if let Ok((_, true)) = res { + self.current_state.write().current_key_id = None; + } + res.map(|(v, _)| v) + } + "keepalive" => self.handle_keepalive(), + "getjobtemplate" => { + if self.sync_state.is_syncing() { + Err(RpcError::node_is_syncing()) + } else { + self.handle_getjobtemplate() + } + } + "status" => self.handle_status(worker_id), + _ => { + // Called undefined method + Err(RpcError::method_not_found()) + } + }; - // Package the reply as RpcResponse json - let resp = match response { - Err(rpc_error) => RpcResponse { - id: request.id, - jsonrpc: String::from("2.0"), - method: request.method, - result: None, - error: Some(rpc_error.into()), - }, - Ok(response) => RpcResponse { - id: request.id, - jsonrpc: String::from("2.0"), - method: request.method, - result: Some(response), - error: None, - }, - }; - serde_json::to_string(&resp).unwrap() - } - fn handle_login(&self, params: Option, worker_id: usize) -> Result { - let params: LoginParams = parse_params(params)?; - self.workers.login(worker_id, params.login, params.agent)?; - return Ok("ok".into()); - } + // Package the reply as RpcResponse json + let resp = match response { + Err(rpc_error) => RpcResponse { + id: request.id, + jsonrpc: String::from("2.0"), + method: request.method, + result: None, + error: Some(rpc_error.into()), + }, + Ok(response) => RpcResponse { + id: request.id, + jsonrpc: String::from("2.0"), + method: request.method, + result: Some(response), + error: None, + }, + }; + serde_json::to_string(&resp).unwrap() + } + fn handle_login(&self, params: Option, worker_id: usize) -> Result { + let params: LoginParams = parse_params(params)?; + self.workers.login(worker_id, params.login, params.agent)?; + return Ok("ok".into()); + } - // Handle KEEPALIVE message - fn handle_keepalive(&self) -> Result { - return Ok("ok".into()); - } + // Handle KEEPALIVE message + fn handle_keepalive(&self) -> Result { + return Ok("ok".into()); + } - fn handle_status(&self, worker_id: usize) -> Result { - // Return worker status in json for use by a dashboard or healthcheck. - let stats = self.workers.get_stats(worker_id)?; - let status = WorkerStatus { - id: stats.id.clone(), - height: self - .current_state - .read() - .current_block_versions - .last() - .unwrap() - .header - .height, - difficulty: stats.pow_difficulty, - accepted: stats.num_accepted, - rejected: stats.num_rejected, - stale: stats.num_stale, - }; - let response = serde_json::to_value(&status).unwrap(); - return Ok(response); - } - // Handle GETJOBTEMPLATE message - fn handle_getjobtemplate(&self) -> Result { - // Build a JobTemplate from a BlockHeader and return JSON - let job_template = self.build_block_template(); - let response = serde_json::to_value(&job_template).unwrap(); - println!( + fn handle_status(&self, worker_id: usize) -> Result { + // Return worker status in json for use by a dashboard or healthcheck. + let stats = self.workers.get_stats(worker_id)?; + let status = WorkerStatus { + id: stats.id.clone(), + height: self + .current_state + .read() + .current_block_versions + .last() + .unwrap() + .header + .height, + difficulty: stats.pow_difficulty, + accepted: stats.num_accepted, + rejected: stats.num_rejected, + stale: stats.num_stale, + }; + let response = serde_json::to_value(&status).unwrap(); + return Ok(response); + } + // Handle GETJOBTEMPLATE message + fn handle_getjobtemplate(&self) -> Result { + // Build a JobTemplate from a BlockHeader and return JSON + let job_template = self.build_block_template(); + let response = serde_json::to_value(&job_template).unwrap(); + println!( "(Server ID: {}) sending block {} with id {} to single worker", self.id, job_template.height, job_template.job_id, ); - return Ok(response); - } + return Ok(response); + } - // Build and return a JobTemplate for mining the current block - fn build_block_template(&self) -> JobTemplate { - let bh = self - .current_state - .read() - .current_block_versions - .last() - .unwrap() - .header - .clone(); - // Serialize the block header into pre and post nonce strings - let mut header_buf = vec![]; - { - let mut writer = ser::BinWriter::default(&mut header_buf); - bh.write_pre_pow(&mut writer).unwrap(); - bh.pow.write_pre_pow(&mut writer).unwrap(); - } - let pre_pow = header_buf.to_hex(); - let current_state = self.current_state.read(); - let job_template = JobTemplate { - height: bh.height, - job_id: (current_state.current_block_versions.len() - 1) as u64, - difficulty: current_state.minimum_share_difficulty, - pre_pow, - }; - return job_template; - } - // Handle SUBMIT message - // params contains a solved block header - // We accept and log valid shares of all difficulty above configured minimum - // Accepted shares that are full solutions will also be submitted to the - // network - fn handle_submit( - &self, - params: Option, - worker_id: usize, - ) -> Result<(Value, bool), RpcError> { - // Validate parameters - let params: SubmitParams = parse_params(params)?; + // Build and return a JobTemplate for mining the current block + fn build_block_template(&self) -> JobTemplate { + let bh = self + .current_state + .read() + .current_block_versions + .last() + .unwrap() + .header + .clone(); + // Serialize the block header into pre and post nonce strings + let mut header_buf = vec![]; + { + let mut writer = ser::BinWriter::default(&mut header_buf); + bh.write_pre_pow(&mut writer).unwrap(); + bh.pow.write_pre_pow(&mut writer).unwrap(); + } + let pre_pow = header_buf.to_hex(); + let current_state = self.current_state.read(); + let job_template = JobTemplate { + height: bh.height, + job_id: (current_state.current_block_versions.len() - 1) as u64, + difficulty: current_state.minimum_share_difficulty, + pre_pow, + }; + return job_template; + } + // Handle SUBMIT message + // params contains a solved block header + // We accept and log valid shares of all difficulty above configured minimum + // Accepted shares that are full solutions will also be submitted to the + // network + fn handle_submit( + &self, + params: Option, + worker_id: usize, + ) -> Result<(Value, bool), RpcError> { + // Validate parameters + let params: SubmitParams = parse_params(params)?; - let state = self.current_state.read(); - // Find the correct version of the block to match this header - let b: Option<&Block> = state.current_block_versions.get(params.job_id as usize); - if params.height != state.current_block_versions.last().unwrap().header.height - || b.is_none() - { - // Return error status - println!( - "(Server ID: {}) Share at height {}, edge_bits {}, nonce {}, job_id {} submitted too late", - self.id, params.height, params.edge_bits, params.nonce, params.job_id, - ); - self.workers.update_stats(worker_id, |ws| ws.num_stale += 1); - return Err(RpcError::too_late()); - } - - let scaled_share_difficulty: u64; - let unscaled_share_difficulty: u64; - let mut share_is_block = false; - - let mut b: Block = b.unwrap().clone(); - // Reconstruct the blocks header with this nonce and pow added - b.header.pow.proof.edge_bits = params.edge_bits as u8; - b.header.pow.nonce = params.nonce; - b.header.pow.proof.nonces = params.pow; - - if !b.header.pow.is_primary() && !b.header.pow.is_secondary() { - // Return error status - println!( - "(Server ID: {}) Failed to validate solution at height {}, hash {}, edge_bits {}, nonce {}, job_id {}: cuckoo size too small", - self.id, params.height, b.hash(), params.edge_bits, params.nonce, params.job_id, - ); - self.workers - .update_stats(worker_id, |worker_stats| worker_stats.num_rejected += 1); - return Err(RpcError::cannot_validate()); - } - - // Get share difficulty values - scaled_share_difficulty = b.header.pow.to_difficulty(b.header.height).to_num(); - unscaled_share_difficulty = b.header.pow.to_unscaled_difficulty().to_num(); - // Note: state.minimum_share_difficulty is unscaled - // state.current_difficulty is scaled - // If the difficulty is too low its an error - if unscaled_share_difficulty < state.minimum_share_difficulty { - // Return error status - println!( - "(Server ID: {}) Share at height {}, hash {}, edge_bits {}, nonce {}, job_id {} rejected due to low difficulty: {}/{}", - self.id, params.height, b.hash(), params.edge_bits, params.nonce, params.job_id, unscaled_share_difficulty, state.minimum_share_difficulty, - ); - self.workers - .update_stats(worker_id, |worker_stats| worker_stats.num_rejected += 1); - return Err(RpcError::too_low_difficulty()); - } - - // If the difficulty is high enough, submit it (which also validates it) - if scaled_share_difficulty >= state.current_difficulty { - // This is a full solution, submit it to the network - let res = self.chain.process_block(b.clone(), grin_chain::Options::MINE); - if let Err(e) = res { - // Return error status - println!( - "(Server ID: {}) Failed to validate solution at height {}, hash {}, edge_bits {}, nonce {}, job_id {}, {:?}", - self.id, - params.height, - b.hash(), - params.edge_bits, - params.nonce, - params.job_id, - e, - ); - self.workers - .update_stats(worker_id, |worker_stats| worker_stats.num_rejected += 1); - return Err(RpcError::cannot_validate()); - } - share_is_block = true; - self.workers - .update_stats(worker_id, |worker_stats| worker_stats.num_blocks_found += 1); - self.workers.stratum_stats.write().blocks_found += 1; - // Log message to make it obvious we found a block - let stats = self.workers.get_stats(worker_id)?; - println!( - "(Server ID: {}) Solution Found for block {}, hash {} - Yay!!! Worker ID: {}, blocks found: {}, shares: {}", - self.id, params.height, - b.hash(), - stats.id, - stats.num_blocks_found, - stats.num_accepted, - ); - } else { - // Do some validation but dont submit - let res = pow::verify_size(&b.header); - if res.is_err() { - // Return error status - println!( - "(Server ID: {}) Failed to validate share at height {}, hash {}, edge_bits {}, nonce {}, job_id {}. {:?}", - self.id, - params.height, - b.hash(), - params.edge_bits, - b.header.pow.nonce, - params.job_id, - res, - ); - self.workers - .update_stats(worker_id, |worker_stats| worker_stats.num_rejected += 1); - return Err(RpcError::cannot_validate()); - } - } - // Log this as a valid share - self.workers.update_edge_bits(params.edge_bits as u16); - let worker = self.workers.get_worker(worker_id)?; - let submitted_by = match worker.login { - None => worker.id.to_string(), - Some(login) => login, - }; - - println!( - "(Server ID: {}) Got share at height {}, hash {}, edge_bits {}, nonce {}, job_id {}, difficulty {}/{}, submitted by {}", - self.id, - b.header.height, - b.hash(), - b.header.pow.proof.edge_bits, - b.header.pow.nonce, - params.job_id, - scaled_share_difficulty, - state.current_difficulty, - submitted_by, + let state = self.current_state.read(); + // Find the correct version of the block to match this header + let b: Option<&Block> = state.current_block_versions.get(params.job_id as usize); + if params.height != state.current_block_versions.last().unwrap().header.height + || b.is_none() + { + // Return error status + println!( + "(Server ID: {}) Share at height {}, edge_bits {}, nonce {}, job_id {} submitted too late", + self.id, params.height, params.edge_bits, params.nonce, params.job_id, ); - self.workers - .update_stats(worker_id, |worker_stats| worker_stats.num_accepted += 1); - let submit_response = if share_is_block { - format!("blockfound - {}", b.hash().to_hex()) - } else { - "ok".to_string() - }; - return Ok(( - serde_json::to_value(submit_response).unwrap(), - share_is_block, - )); - } // handle submit a solution + self.workers.update_stats(worker_id, |ws| ws.num_stale += 1); + return Err(RpcError::too_late()); + } - fn broadcast_job(&self) { - debug!("broadcast job"); - // Package new block into RpcRequest - let job_template = self.build_block_template(); - let job_template_json = serde_json::to_string(&job_template).unwrap(); - // Issue #1159 - use a serde_json Value type to avoid extra quoting - let job_template_value: Value = serde_json::from_str(&job_template_json).unwrap(); - let job_request = RpcRequest { - id: JsonId::StrId(String::from("Stratum")), - jsonrpc: String::from("2.0"), - method: String::from("job"), - params: Some(job_template_value), - }; - let job_request_json = serde_json::to_string(&job_request).unwrap(); - debug!( + let scaled_share_difficulty: u64; + let unscaled_share_difficulty: u64; + let mut share_is_block = false; + + let mut b: Block = b.unwrap().clone(); + // Reconstruct the blocks header with this nonce and pow added + b.header.pow.proof.edge_bits = params.edge_bits as u8; + b.header.pow.nonce = params.nonce; + b.header.pow.proof.nonces = params.pow; + + if !b.header.pow.is_primary() && !b.header.pow.is_secondary() { + // Return error status + println!( + "(Server ID: {}) Failed to validate solution at height {}, hash {}, edge_bits {}, nonce {}, job_id {}: cuckoo size too small", + self.id, + params.height, + b.hash(), + params.edge_bits, + params.nonce, + params.job_id, + ); + self.workers + .update_stats(worker_id, |worker_stats| worker_stats.num_rejected += 1); + return Err(RpcError::cannot_validate()); + } + + // Get share difficulty values + scaled_share_difficulty = b.header.pow.to_difficulty(b.header.height).to_num(); + unscaled_share_difficulty = b.header.pow.to_unscaled_difficulty().to_num(); + // Note: state.minimum_share_difficulty is unscaled + // state.current_difficulty is scaled + // If the difficulty is too low its an error + if unscaled_share_difficulty < state.minimum_share_difficulty { + // Return error status + println!( + "(Server ID: {}) Share at height {}, hash {}, edge_bits {}, nonce {}, job_id {} rejected due to low difficulty: {}/{}", + self.id, + params.height, + b.hash(), + params.edge_bits, + params.nonce, + params.job_id, + unscaled_share_difficulty, + state.minimum_share_difficulty, + ); + self.workers + .update_stats(worker_id, |worker_stats| worker_stats.num_rejected += 1); + return Err(RpcError::too_low_difficulty()); + } + + // If the difficulty is high enough, submit it (which also validates it) + if scaled_share_difficulty >= state.current_difficulty { + // This is a full solution, submit it to the network + let res = self + .chain + .process_block(b.clone(), grin_chain::Options::MINE); + if let Err(e) = res { + // Return error status + println!( + "(Server ID: {}) Failed to validate solution at height {}, hash {}, edge_bits {}, nonce {}, job_id {}, {:?}", + self.id, + params.height, + b.hash(), + params.edge_bits, + params.nonce, + params.job_id, + e, + ); + self.workers + .update_stats(worker_id, |worker_stats| worker_stats.num_rejected += 1); + return Err(RpcError::cannot_validate()); + } + share_is_block = true; + self.workers + .update_stats(worker_id, |worker_stats| worker_stats.num_blocks_found += 1); + self.workers.stratum_stats.write().blocks_found += 1; + // Log message to make it obvious we found a block + let stats = self.workers.get_stats(worker_id)?; + println!( + "(Server ID: {}) Solution Found for block {}, hash {} - Yay!!! Worker ID: {}, blocks found: {}, shares: {}", + self.id, + params.height, + b.hash(), + stats.id, + stats.num_blocks_found, + stats.num_accepted, + ); + } else { + // Do some validation but dont submit + let res = pow::verify_size(&b.header); + if res.is_err() { + // Return error status + println!( + "(Server ID: {}) Failed to validate share at height {}, hash {}, edge_bits {}, nonce {}, job_id {}. {:?}", + self.id, + params.height, + b.hash(), + params.edge_bits, + b.header.pow.nonce, + params.job_id, + res, + ); + self.workers + .update_stats(worker_id, |worker_stats| worker_stats.num_rejected += 1); + return Err(RpcError::cannot_validate()); + } + } + // Log this as a valid share + self.workers.update_edge_bits(params.edge_bits as u16); + let worker = self.workers.get_worker(worker_id)?; + let submitted_by = match worker.login { + None => worker.id.to_string(), + Some(login) => login, + }; + + println!( + "(Server ID: {}) Got share at height {}, hash {}, edge_bits {}, nonce {}, job_id {}, difficulty {}/{}, submitted by {}", + self.id, + b.header.height, + b.hash(), + b.header.pow.proof.edge_bits, + b.header.pow.nonce, + params.job_id, + scaled_share_difficulty, + state.current_difficulty, + submitted_by, + ); + self.workers + .update_stats(worker_id, |worker_stats| worker_stats.num_accepted += 1); + let submit_response = if share_is_block { + format!("blockfound - {}", b.hash().to_hex()) + } else { + "ok".to_string() + }; + return Ok(( + serde_json::to_value(submit_response).unwrap(), + share_is_block, + )); + } // handle submit a solution + + fn broadcast_job(&self) { + debug!("broadcast job"); + // Package new block into RpcRequest + let job_template = self.build_block_template(); + let job_template_json = serde_json::to_string(&job_template).unwrap(); + // Issue #1159 - use a serde_json Value type to avoid extra quoting + let job_template_value: Value = serde_json::from_str(&job_template_json).unwrap(); + let job_request = RpcRequest { + id: JsonId::StrId(String::from("Stratum")), + jsonrpc: String::from("2.0"), + method: String::from("job"), + params: Some(job_template_value), + }; + let job_request_json = serde_json::to_string(&job_request).unwrap(); + debug!( "(Server ID: {}) sending block {} with id {} to stratum clients", self.id, job_template.height, job_template.job_id, ); - self.workers.broadcast(job_request_json); - } + self.workers.broadcast(job_request_json); + } - pub fn run(&self, - config: &StratumServerConfig, - tx_pool: &ServerTxPool, - stop_state: Arc) { - debug!("Run main loop"); - let mut deadline: i64 = 0; - let mut head = self.chain.head().unwrap(); - let mut current_hash = head.prev_block_h; - loop { - if stop_state.is_stopped() { - thread::sleep(Duration::from_millis(1500)); - break; - } - // get the latest chain state - head = self.chain.head().unwrap(); - let latest_hash = head.last_block_h; + pub fn run( + &self, + config: &StratumServerConfig, + tx_pool: &ServerTxPool, + stop_state: Arc, + ) { + debug!("Run main loop"); + let mut deadline: i64 = 0; + let mut head = self.chain.head().unwrap(); + let mut current_hash = head.prev_block_h; + loop { + if stop_state.is_stopped() { + thread::sleep(Duration::from_millis(1500)); + break; + } + // get the latest chain state + head = self.chain.head().unwrap(); + let latest_hash = head.last_block_h; - // Build a new block if there is at least one worker and - // There is a new block on the chain or its time to rebuild - // the current one to include new transactions - if (current_hash != latest_hash || Utc::now().timestamp() >= deadline) - && self.workers.count() > 0 - { - { - debug!("resend updated block"); - let mut state = self.current_state.write(); - let wallet_listener_url = if !config.burn_reward { - if let Ok(id) = config.wallet_listener_url.parse::() { - if let Some(port) = WalletConfig::read_api_port_by_id(id) { - let url = format!("http://127.0.0.1:{}", port); - Some(url) - } else { - None - } - } else { - None - } - } else { - None - }; - // If this is a new block we will clear the current_block version history - let clear_blocks = current_hash != latest_hash; + // Build a new block if there is at least one worker and + // There is a new block on the chain or its time to rebuild + // the current one to include new transactions + if (current_hash != latest_hash || Utc::now().timestamp() >= deadline) + && self.workers.count() > 0 + { + { + debug!("resend updated block"); + let mut state = self.current_state.write(); + let wallet_listener_url = if !config.burn_reward { + if let Ok(id) = config.wallet_listener_url.parse::() { + if let Some(port) = WalletConfig::read_api_port_by_id(id) { + let url = format!("http://127.0.0.1:{}", port); + Some(url) + } else { + None + } + } else { + None + } + } else { + None + }; + // If this is a new block we will clear the current_block version history + let clear_blocks = current_hash != latest_hash; - // Build the new block (version) - if let Some((new_block, block_fees)) = get_block( - &self.chain, - tx_pool, - state.current_key_id.clone(), - wallet_listener_url, - &stop_state - ) { - // scaled difficulty - state.current_difficulty = - (new_block.header.total_difficulty() - head.total_difficulty).to_num(); + // Build the new block (version) + if let Some((new_block, block_fees)) = get_block( + &self.chain, + tx_pool, + state.current_key_id.clone(), + wallet_listener_url, + &stop_state, + ) { + // scaled difficulty + state.current_difficulty = + (new_block.header.total_difficulty() - head.total_difficulty).to_num(); - state.current_key_id = block_fees.key_id(); + state.current_key_id = block_fees.key_id(); - current_hash = latest_hash; - // set the minimum acceptable share unscaled difficulty for this block - state.minimum_share_difficulty = config.minimum_share_difficulty; + current_hash = latest_hash; + // set the minimum acceptable share unscaled difficulty for this block + state.minimum_share_difficulty = config.minimum_share_difficulty; - // set a new deadline for rebuilding with fresh transactions - deadline = Utc::now().timestamp() + config.attempt_time_per_block as i64; + // set a new deadline for rebuilding with fresh transactions + deadline = Utc::now().timestamp() + config.attempt_time_per_block as i64; - // If this is a new block we will clear the current_block version history - if clear_blocks { - state.current_block_versions.clear(); - } + // If this is a new block we will clear the current_block version history + if clear_blocks { + state.current_block_versions.clear(); + } - // Update the mining stats - self.workers.update_block_height(new_block.header.height); - let difficulty = new_block.header.total_difficulty() - head.total_difficulty; - self.workers.update_network_difficulty(difficulty.to_num()); - self.workers.update_network_hashrate(); + // Update the mining stats + self.workers.update_block_height(new_block.header.height); + let difficulty = + new_block.header.total_difficulty() - head.total_difficulty; + self.workers.update_network_difficulty(difficulty.to_num()); + self.workers.update_network_hashrate(); - // Add this new block candidate onto our list of block versions for height - state.current_block_versions.push(new_block); - } else { - thread::sleep(Duration::from_millis(1500)); - break; - } - } - // Send this job to all connected workers - self.broadcast_job(); - } + // Add this new block candidate onto our list of block versions for height + state.current_block_versions.push(new_block); + } else { + thread::sleep(Duration::from_millis(1500)); + break; + } + } + // Send this job to all connected workers + self.broadcast_job(); + } - // sleep before restarting loop - thread::sleep(Duration::from_millis(5)); - } // Main Loop - } + // sleep before restarting loop + thread::sleep(Duration::from_millis(5)); + } // Main Loop + } } // ---------------------------------------- // Worker Factory Thread Function -fn accept_connections(listen_addr: SocketAddr, - handler: Arc, - stop_state: Arc) { - debug!("Start tokio stratum server"); - let task = async move { - let mut listener = TcpListener::bind(&listen_addr).await.unwrap_or_else(|_| { - panic!("Stratum: Failed to bind to listen address {}", listen_addr) - }); - let server = listener - .incoming() - .filter_map(|s| async { s.map_err(|e| error!("accept error = {:?}", e)).ok() }) - .for_each(move |socket| { - let handler = handler.clone(); - async move { - // Spawn a task to process the connection - let (tx, mut rx) = mpsc::unbounded(); +fn accept_connections( + listen_addr: SocketAddr, + handler: Arc, + stop_state: Arc, +) { + debug!("Start tokio stratum server"); + let task = async move { + let mut listener = TcpListener::bind(&listen_addr).await.unwrap_or_else(|_| { + panic!("Stratum: Failed to bind to listen address {}", listen_addr) + }); + let server = listener + .incoming() + .filter_map(|s| async { s.map_err(|e| error!("accept error = {:?}", e)).ok() }) + .for_each(move |socket| { + let handler = handler.clone(); + async move { + // Spawn a task to process the connection + let (tx, mut rx) = mpsc::unbounded(); - let worker_id = handler.workers.add_worker(tx); - debug!("Worker {} connected", worker_id); + let worker_id = handler.workers.add_worker(tx); + debug!("Worker {} connected", worker_id); - let framed = Framed::new(socket, LinesCodec::new()); - let (mut writer, mut reader) = framed.split(); + let framed = Framed::new(socket, LinesCodec::new()); + let (mut writer, mut reader) = framed.split(); - let h = handler.clone(); - let read = async move { - while let Some(line) = reader - .try_next() - .await - .map_err(|e| debug!("error reading line: {}", e))? - { - let request = serde_json::from_str(&line) - .map_err(|e| debug!("error serializing line: {}", e))?; - let resp = h.handle_rpc_requests(request, worker_id); - h.workers.send_to(worker_id, resp); - } + let h = handler.clone(); + let read = async move { + while let Some(line) = reader + .try_next() + .await + .map_err(|e| debug!("error reading line: {}", e))? + { + let request = serde_json::from_str(&line) + .map_err(|e| debug!("error serializing line: {}", e))?; + let resp = h.handle_rpc_requests(request, worker_id); + h.workers.send_to(worker_id, resp); + } - Result::<_, ()>::Ok(()) - }; + Result::<_, ()>::Ok(()) + }; - let write = async move { - while let Some(line) = rx.next().await { - writer - .send(line) - .await - .map_err(|e| debug!("error writing line: {}", e))?; - } + let write = async move { + while let Some(line) = rx.next().await { + writer + .send(line) + .await + .map_err(|e| debug!("error writing line: {}", e))?; + } - Result::<_, ()>::Ok(()) - }; + Result::<_, ()>::Ok(()) + }; - let task = async move { - pin_mut!(read, write); - futures::future::select(read, write).await; - handler.workers.remove_worker(worker_id); - debug!("Worker {} disconnected", worker_id); - }; - tokio_old::spawn(task); - } - }); - server.await - }; + let task = async move { + pin_mut!(read, write); + futures::future::select(read, write).await; + handler.workers.remove_worker(worker_id); + debug!("Worker {} disconnected", worker_id); + }; + tokio_old::spawn(task); + } + }); + server.await + }; - let mut rt = Runtime::new().unwrap(); - let (task, handle) = abortable(task); - rt.spawn(check_stop_state(stop_state, handle)); - rt.block_on(task).unwrap_or_default(); + let mut rt = Runtime::new().unwrap(); + let (task, handle) = abortable(task); + rt.spawn(check_stop_state(stop_state, handle)); + rt.block_on(task).unwrap_or_default(); } async fn check_stop_state(stop_state: Arc, handle: AbortHandle) { - loop { - // Ping stratum socket on stop to handle TcpListener unbind. - if stop_state.is_stopped() { - handle.abort(); - break; - } - thread::sleep(Duration::from_millis(1000)); - } + loop { + // Ping stratum socket on stop to handle TcpListener unbind. + if stop_state.is_stopped() { + handle.abort(); + break; + } + thread::sleep(Duration::from_millis(1000)); + } } - // ---------------------------------------- // Worker Object - a connected stratum client - a miner, pool, proxy, etc... #[derive(Clone)] pub struct Worker { - id: usize, - agent: String, - login: Option, - authenticated: bool, - tx: Tx, + id: usize, + agent: String, + login: Option, + authenticated: bool, + tx: Tx, } impl Worker { - /// Creates a new Stratum Worker. - pub fn new(id: usize, tx: Tx) -> Worker { - Worker { - id: id, - agent: String::from(""), - login: None, - authenticated: false, - tx: tx, - } - } + /// Creates a new Stratum Worker. + pub fn new(id: usize, tx: Tx) -> Worker { + Worker { + id: id, + agent: String::from(""), + login: None, + authenticated: false, + tx: tx, + } + } } // impl Worker struct WorkersList { - workers_list: Arc>>, - stratum_stats: Arc>, + workers_list: Arc>>, + stratum_stats: Arc>, } impl WorkersList { - pub fn new(stratum_stats: Arc>) -> Self { - WorkersList { - workers_list: Arc::new(RwLock::new(HashMap::new())), - stratum_stats: stratum_stats, - } - } + pub fn new(stratum_stats: Arc>) -> Self { + WorkersList { + workers_list: Arc::new(RwLock::new(HashMap::new())), + stratum_stats: stratum_stats, + } + } - pub fn add_worker(&self, tx: Tx) -> usize { - let mut stratum_stats = self.stratum_stats.write(); - let worker_id = stratum_stats.worker_stats.len(); - let worker = Worker::new(worker_id, tx); - let mut workers_list = self.workers_list.write(); - workers_list.insert(worker_id, worker); + pub fn add_worker(&self, tx: Tx) -> usize { + let mut stratum_stats = self.stratum_stats.write(); + let worker_id = stratum_stats.worker_stats.len(); + let worker = Worker::new(worker_id, tx); + let mut workers_list = self.workers_list.write(); + workers_list.insert(worker_id, worker); - let mut worker_stats = WorkerStats::default(); - worker_stats.is_connected = true; - worker_stats.id = worker_id.to_string(); - worker_stats.pow_difficulty = stratum_stats.minimum_share_difficulty; - stratum_stats.worker_stats.push(worker_stats); - stratum_stats.num_workers = workers_list.len(); - worker_id - } - pub fn remove_worker(&self, worker_id: usize) { - self.update_stats(worker_id, |ws| ws.is_connected = false); - let mut stratum_stats = self.stratum_stats.write(); - let mut workers_list = self.workers_list.write(); - workers_list - .remove(&worker_id) - .expect("Stratum: no such addr in map"); + let mut worker_stats = WorkerStats::default(); + worker_stats.is_connected = true; + worker_stats.id = worker_id.to_string(); + worker_stats.pow_difficulty = stratum_stats.minimum_share_difficulty; + stratum_stats.worker_stats.push(worker_stats); + stratum_stats.num_workers = workers_list.len(); + worker_id + } + pub fn remove_worker(&self, worker_id: usize) { + self.update_stats(worker_id, |ws| ws.is_connected = false); + let mut stratum_stats = self.stratum_stats.write(); + let mut workers_list = self.workers_list.write(); + workers_list + .remove(&worker_id) + .expect("Stratum: no such addr in map"); - stratum_stats.num_workers = workers_list.len(); - } + stratum_stats.num_workers = workers_list.len(); + } - pub fn login(&self, worker_id: usize, login: String, agent: String) -> Result<(), RpcError> { - let mut wl = self.workers_list.write(); - let worker = wl - .get_mut(&worker_id) - .ok_or_else(RpcError::internal_error)?; - worker.login = Some(login); - // XXX TODO Future - Validate password? - worker.agent = agent; - worker.authenticated = true; - Ok(()) - } + pub fn login(&self, worker_id: usize, login: String, agent: String) -> Result<(), RpcError> { + let mut wl = self.workers_list.write(); + let worker = wl + .get_mut(&worker_id) + .ok_or_else(RpcError::internal_error)?; + worker.login = Some(login); + // XXX TODO Future - Validate password? + worker.agent = agent; + worker.authenticated = true; + Ok(()) + } - pub fn get_worker(&self, worker_id: usize) -> Result { - self.workers_list - .read() - .get(&worker_id) - .ok_or_else(|| { - error!("Worker {} not found", worker_id); - RpcError::internal_error() - }) - .map(|w| w.clone()) - } + pub fn get_worker(&self, worker_id: usize) -> Result { + self.workers_list + .read() + .get(&worker_id) + .ok_or_else(|| { + error!("Worker {} not found", worker_id); + RpcError::internal_error() + }) + .map(|w| w.clone()) + } - pub fn get_stats(&self, worker_id: usize) -> Result { - self.stratum_stats - .read() - .worker_stats - .get(worker_id) - .ok_or_else(RpcError::internal_error) - .map(|ws| ws.clone()) - } + pub fn get_stats(&self, worker_id: usize) -> Result { + self.stratum_stats + .read() + .worker_stats + .get(worker_id) + .ok_or_else(RpcError::internal_error) + .map(|ws| ws.clone()) + } - pub fn last_seen(&self, worker_id: usize) { - //self.stratum_stats.write().worker_stats[worker_id].last_seen = SystemTime::now(); - self.update_stats(worker_id, |ws| ws.last_seen = SystemTime::now()); - } + pub fn last_seen(&self, worker_id: usize) { + //self.stratum_stats.write().worker_stats[worker_id].last_seen = SystemTime::now(); + self.update_stats(worker_id, |ws| ws.last_seen = SystemTime::now()); + } - pub fn update_stats(&self, worker_id: usize, f: impl FnOnce(&mut WorkerStats) -> ()) { - let mut stratum_stats = self.stratum_stats.write(); - f(&mut stratum_stats.worker_stats[worker_id]); - } + pub fn update_stats(&self, worker_id: usize, f: impl FnOnce(&mut WorkerStats) -> ()) { + let mut stratum_stats = self.stratum_stats.write(); + f(&mut stratum_stats.worker_stats[worker_id]); + } - pub fn send_to(&self, worker_id: usize, msg: String) { - let _ = self - .workers_list - .read() - .get(&worker_id) - .unwrap() - .tx - .unbounded_send(msg); - } + pub fn send_to(&self, worker_id: usize, msg: String) { + let _ = self + .workers_list + .read() + .get(&worker_id) + .unwrap() + .tx + .unbounded_send(msg); + } - pub fn broadcast(&self, msg: String) { - for worker in self.workers_list.read().values() { - let _ = worker.tx.unbounded_send(msg.clone()); - } - } + pub fn broadcast(&self, msg: String) { + for worker in self.workers_list.read().values() { + let _ = worker.tx.unbounded_send(msg.clone()); + } + } - pub fn count(&self) -> usize { - self.workers_list.read().len() - } + pub fn count(&self) -> usize { + self.workers_list.read().len() + } - pub fn update_edge_bits(&self, edge_bits: u16) { - { - let mut stratum_stats = self.stratum_stats.write(); - stratum_stats.edge_bits = edge_bits; - } - self.update_network_hashrate(); - } + pub fn update_edge_bits(&self, edge_bits: u16) { + { + let mut stratum_stats = self.stratum_stats.write(); + stratum_stats.edge_bits = edge_bits; + } + self.update_network_hashrate(); + } - pub fn update_block_height(&self, height: u64) { - let mut stratum_stats = self.stratum_stats.write(); - stratum_stats.block_height = height; - } + pub fn update_block_height(&self, height: u64) { + let mut stratum_stats = self.stratum_stats.write(); + stratum_stats.block_height = height; + } - pub fn update_network_difficulty(&self, difficulty: u64) { - let mut stratum_stats = self.stratum_stats.write(); - stratum_stats.network_difficulty = difficulty; - } + pub fn update_network_difficulty(&self, difficulty: u64) { + let mut stratum_stats = self.stratum_stats.write(); + stratum_stats.network_difficulty = difficulty; + } - pub fn update_network_hashrate(&self) { - let mut stratum_stats = self.stratum_stats.write(); - stratum_stats.network_hashrate = 42.0 - * (stratum_stats.network_difficulty as f64 - / graph_weight(stratum_stats.block_height, stratum_stats.edge_bits as u8) as f64) - / 60.0; - } + pub fn update_network_hashrate(&self) { + let mut stratum_stats = self.stratum_stats.write(); + stratum_stats.network_hashrate = 42.0 + * (stratum_stats.network_difficulty as f64 + / graph_weight(stratum_stats.block_height, stratum_stats.edge_bits as u8) as f64) + / 60.0; + } } // ---------------------------------------- // Grin Stratum Server pub struct StratumServer { - id: String, - config: StratumServerConfig, - chain: Arc, - pub tx_pool: ServerTxPool, - sync_state: Arc, - stratum_stats: Arc>, + id: String, + config: StratumServerConfig, + chain: Arc, + pub tx_pool: ServerTxPool, + sync_state: Arc, + stratum_stats: Arc>, } impl StratumServer { - /// Creates a new Stratum Server. - pub fn new( - config: StratumServerConfig, - chain: Arc, - tx_pool: ServerTxPool, - stratum_stats: Arc>, - ) -> StratumServer { - StratumServer { - id: String::from("0"), - config, - chain, - tx_pool, - sync_state: Arc::new(SyncState::new()), - stratum_stats: stratum_stats, - } - } + /// Creates a new Stratum Server. + pub fn new( + config: StratumServerConfig, + chain: Arc, + tx_pool: ServerTxPool, + stratum_stats: Arc>, + ) -> StratumServer { + StratumServer { + id: String::from("0"), + config, + chain, + tx_pool, + sync_state: Arc::new(SyncState::new()), + stratum_stats: stratum_stats, + } + } - /// "main()" - Starts the stratum-server. Creates a thread to Listens for - /// a connection, then enters a loop, building a new block on top of the - /// existing chain anytime required and sending that to the connected - /// stratum miner, proxy, or pool, and accepts full solutions to - /// be submitted. - pub fn run_loop(&mut self, - proof_size: usize, - sync_state: Arc, - stop_state: Arc) { - debug!( + /// "main()" - Starts the stratum-server. Creates a thread to Listens for + /// a connection, then enters a loop, building a new block on top of the + /// existing chain anytime required and sending that to the connected + /// stratum miner, proxy, or pool, and accepts full solutions to + /// be submitted. + pub fn run_loop( + &mut self, + proof_size: usize, + sync_state: Arc, + stop_state: Arc, + ) { + debug!( "(Server ID: {}) Starting stratum server with proof_size = {}", self.id, proof_size ); - self.sync_state = sync_state; + self.sync_state = sync_state; - let listen_addr = self - .config - .stratum_server_addr - .clone() - .unwrap() - .parse() - .expect("Stratum: Incorrect address "); + let listen_addr = self + .config + .stratum_server_addr + .clone() + .unwrap() + .parse() + .expect("Stratum: Incorrect address "); - let handler = Arc::new(Handler::from_stratum(&self)); - let h = handler.clone(); + let handler = Arc::new(Handler::from_stratum(&self)); + let h = handler.clone(); - let stop_socket = stop_state.clone(); - let check_state = stop_socket.clone(); - let _listener_th = thread::spawn(move || { - accept_connections(listen_addr, h, stop_socket); - }); + let stop_socket = stop_state.clone(); + let check_state = stop_socket.clone(); + let _listener_th = thread::spawn(move || { + accept_connections(listen_addr, h, stop_socket); + }); - // We have started - { - let mut stratum_stats = self.stratum_stats.write(); - stratum_stats.is_running = true; - stratum_stats.edge_bits = (min_edge_bits() + 1) as u16; - stratum_stats.minimum_share_difficulty = self.config.minimum_share_difficulty; - } + // We have started + { + let mut stratum_stats = self.stratum_stats.write(); + stratum_stats.is_running = true; + stratum_stats.edge_bits = (min_edge_bits() + 1) as u16; + stratum_stats.minimum_share_difficulty = self.config.minimum_share_difficulty; + } - // Initial Loop. Waiting node complete syncing - while self.sync_state.is_syncing() { - thread::sleep(Duration::from_millis(50)); - } + // Initial Loop. Waiting node complete syncing + while self.sync_state.is_syncing() { + thread::sleep(Duration::from_millis(50)); + } - handler.run(&self.config, &self.tx_pool, check_state); - } // fn run_loop() + handler.run(&self.config, &self.tx_pool, check_state); + } // fn run_loop() } // StratumServer // Utility function to parse a JSON RPC parameter object, returning a proper // error if things go wrong. fn parse_params(params: Option) -> Result - where - for<'de> T: serde::Deserialize<'de>, +where + for<'de> T: serde::Deserialize<'de>, { - params - .and_then(|v| serde_json::from_value(v).ok()) - .ok_or_else(RpcError::invalid_request) -} \ No newline at end of file + params + .and_then(|v| serde_json::from_value(v).ok()) + .ok_or_else(RpcError::invalid_request) +} diff --git a/src/node/types.rs b/src/node/types.rs index 31a731cd..3db9c134 100644 --- a/src/node/types.rs +++ b/src/node/types.rs @@ -15,14 +15,14 @@ /// Integrated node error type. #[derive(Clone)] pub enum NodeError { - /// Storage issue. - Storage, - /// P2P server issue. - P2P, - /// API server issue. - API, - /// Configuration issue. - Configuration, - /// Unknown error. - Unknown + /// Storage issue. + Storage, + /// P2P server issue. + P2P, + /// API server issue. + API, + /// Configuration issue. + Configuration, + /// Unknown error. + Unknown, } diff --git a/src/settings/config.rs b/src/settings/config.rs index 29c51c59..128ab76b 100644 --- a/src/settings/config.rs +++ b/src/settings/config.rs @@ -16,387 +16,388 @@ use grin_core::global; use grin_core::global::ChainTypes; use serde_derive::{Deserialize, Serialize}; +use crate::Settings; use crate::gui::views::Content; use crate::http::ReleaseInfo; use crate::node::NodeConfig; use crate::wallet::ConnectionsConfig; -use crate::Settings; /// Application update information. #[derive(Serialize, Deserialize, Clone)] pub struct AppUpdate { - /// Version of release. - pub version: String, - /// Size of release in megabytes. - pub size: Option, - /// Date of release. - pub date: String, - /// Changes in the release. - pub changelog: String, - /// Link to download the release. - pub url: String, + /// Version of release. + pub version: String, + /// Size of release in megabytes. + pub size: Option, + /// Date of release. + pub date: String, + /// Changes in the release. + pub changelog: String, + /// Link to download the release. + pub url: String, } /// Application configuration, stored at toml file. #[derive(Serialize, Deserialize)] pub struct AppConfig { - /// Run node server on startup. - pub(crate) auto_start_node: bool, - /// Chain type for node and wallets. - pub(crate) chain_type: ChainTypes, + /// Run node server on startup. + pub(crate) auto_start_node: bool, + /// Chain type for node and wallets. + pub(crate) chain_type: ChainTypes, - /// Flag to check if Android integrated node warning was shown. - android_integrated_node_warning: Option, + /// Flag to check if Android integrated node warning was shown. + android_integrated_node_warning: Option, - /// Flag to show wallet list at dual panel wallets mode. - show_wallets_at_dual_panel: bool, - /// Flag to show all connections at network panel or integrated node info. - show_connections_network_panel: bool, + /// Flag to show wallet list at dual panel wallets mode. + show_wallets_at_dual_panel: bool, + /// Flag to show all connections at network panel or integrated node info. + show_connections_network_panel: bool, - /// Width of the desktop window. - width: f32, - /// Height of the desktop window. - height: f32, + /// Width of the desktop window. + width: f32, + /// Height of the desktop window. + height: f32, - /// Position of the desktop window. - x: Option, y: Option, + /// Position of the desktop window. + x: Option, + y: Option, - /// Locale code for i18n. - lang: Option, - /// Flag to use English locale layout on keyboard. - english_keyboard: Option, + /// Locale code for i18n. + lang: Option, + /// Flag to use English locale layout on keyboard. + english_keyboard: Option, - /// Flag to check if dark theme should be used, use system settings if not set. - use_dark_theme: Option, + /// Flag to check if dark theme should be used, use system settings if not set. + use_dark_theme: Option, - /// Flag to use proxy for network requests. - use_proxy: Option, - /// Flag to use SOCKS5 or HTTP proxy for network requests. - use_socks_proxy: Option, - /// HTTP proxy URL. - http_proxy_url: Option, - /// SOCKS5 proxy URL. - socks_proxy_url: Option, + /// Flag to use proxy for network requests. + use_proxy: Option, + /// Flag to use SOCKS5 or HTTP proxy for network requests. + use_socks_proxy: Option, + /// HTTP proxy URL. + http_proxy_url: Option, + /// SOCKS5 proxy URL. + socks_proxy_url: Option, - /// Flag to check updates on startup. - check_updates: Option, - /// Application update information. - app_update: Option, + /// Flag to check updates on startup. + check_updates: Option, + /// Application update information. + app_update: Option, } impl Default for AppConfig { - fn default() -> Self { - Self { - auto_start_node: false, - chain_type: ChainTypes::default(), - android_integrated_node_warning: None, - show_wallets_at_dual_panel: false, - show_connections_network_panel: false, - width: Self::DEFAULT_WIDTH, - height: Self::DEFAULT_HEIGHT, - x: None, - y: None, - lang: None, - english_keyboard: None, - use_dark_theme: None, - use_proxy: None, - use_socks_proxy: None, - http_proxy_url: None, - socks_proxy_url: None, - check_updates: Some(true), - app_update: None, - } - } + fn default() -> Self { + Self { + auto_start_node: false, + chain_type: ChainTypes::default(), + android_integrated_node_warning: None, + show_wallets_at_dual_panel: false, + show_connections_network_panel: false, + width: Self::DEFAULT_WIDTH, + height: Self::DEFAULT_HEIGHT, + x: None, + y: None, + lang: None, + english_keyboard: None, + use_dark_theme: None, + use_proxy: None, + use_socks_proxy: None, + http_proxy_url: None, + socks_proxy_url: None, + check_updates: Some(true), + app_update: None, + } + } } impl AppConfig { - /// Desktop window frame margin sum, horizontal or vertical. - const FRAME_MARGIN: f32 = Content::WINDOW_FRAME_MARGIN * 2.0; - /// Default desktop window width. - pub const DEFAULT_WIDTH: f32 = Content::SIDE_PANEL_WIDTH * 3.0 + Self::FRAME_MARGIN; - /// Default desktop window height. - pub const DEFAULT_HEIGHT: f32 = 706.0; - /// Minimal desktop window width. - pub const MIN_WIDTH: f32 = Content::SIDE_PANEL_WIDTH + Self::FRAME_MARGIN; - /// Minimal desktop window height. - pub const MIN_HEIGHT: f32 = 630.0 + Content::WINDOW_TITLE_HEIGHT + Self::FRAME_MARGIN; + /// Desktop window frame margin sum, horizontal or vertical. + const FRAME_MARGIN: f32 = Content::WINDOW_FRAME_MARGIN * 2.0; + /// Default desktop window width. + pub const DEFAULT_WIDTH: f32 = Content::SIDE_PANEL_WIDTH * 3.0 + Self::FRAME_MARGIN; + /// Default desktop window height. + pub const DEFAULT_HEIGHT: f32 = 706.0; + /// Minimal desktop window width. + pub const MIN_WIDTH: f32 = Content::SIDE_PANEL_WIDTH + Self::FRAME_MARGIN; + /// Minimal desktop window height. + pub const MIN_HEIGHT: f32 = 630.0 + Content::WINDOW_TITLE_HEIGHT + Self::FRAME_MARGIN; - /// Application configuration file name. - pub const FILE_NAME: &'static str = "app.toml"; + /// Application configuration file name. + pub const FILE_NAME: &'static str = "app.toml"; - /// Default i18n locale. - pub const DEFAULT_LOCALE: &'static str = "en"; + /// Default i18n locale. + pub const DEFAULT_LOCALE: &'static str = "en"; - /// Save application configuration to the file. - pub fn save(&self) { - Settings::write_to_file(self, Settings::config_path(Self::FILE_NAME, None)); - } + /// Save application configuration to the file. + pub fn save(&self) { + Settings::write_to_file(self, Settings::config_path(Self::FILE_NAME, None)); + } - /// Change global [`ChainTypes`] and load new [`NodeConfig`]. - pub fn change_chain_type(chain_type: &ChainTypes) { - let current_chain_type = Self::chain_type(); - if current_chain_type != *chain_type { - // Save chain type at app config. - { - let mut w_app_config = Settings::app_config_to_update(); - w_app_config.chain_type = *chain_type; - w_app_config.save(); - } - // Load node configuration for selected chain type. - { - let mut w_node_config = Settings::node_config_to_update(); - let node_config = NodeConfig::for_chain_type(chain_type); - w_node_config.node = node_config.node; - w_node_config.peers = node_config.peers; - } - // Load connections configuration - { - let mut w_conn_config = Settings::conn_config_to_update(); - *w_conn_config = ConnectionsConfig::for_chain_type(chain_type); - } - } - if !global::GLOBAL_CHAIN_TYPE.is_init() { - global::init_global_chain_type(*chain_type); - } else { - global::set_global_chain_type(*chain_type); - global::set_local_chain_type(*chain_type); - } - } + /// Change global [`ChainTypes`] and load new [`NodeConfig`]. + pub fn change_chain_type(chain_type: &ChainTypes) { + let current_chain_type = Self::chain_type(); + if current_chain_type != *chain_type { + // Save chain type at app config. + { + let mut w_app_config = Settings::app_config_to_update(); + w_app_config.chain_type = *chain_type; + w_app_config.save(); + } + // Load node configuration for selected chain type. + { + let mut w_node_config = Settings::node_config_to_update(); + let node_config = NodeConfig::for_chain_type(chain_type); + w_node_config.node = node_config.node; + w_node_config.peers = node_config.peers; + } + // Load connections configuration + { + let mut w_conn_config = Settings::conn_config_to_update(); + *w_conn_config = ConnectionsConfig::for_chain_type(chain_type); + } + } + if !global::GLOBAL_CHAIN_TYPE.is_init() { + global::init_global_chain_type(*chain_type); + } else { + global::set_global_chain_type(*chain_type); + global::set_local_chain_type(*chain_type); + } + } - /// Get current [`ChainTypes`] for node and wallets. - pub fn chain_type() -> ChainTypes { - let r_config = Settings::app_config_to_read(); - r_config.chain_type - } + /// Get current [`ChainTypes`] for node and wallets. + pub fn chain_type() -> ChainTypes { + let r_config = Settings::app_config_to_read(); + r_config.chain_type + } - /// Check if integrated node is starting with application. - pub fn autostart_node() -> bool { - let r_config = Settings::app_config_to_read(); - r_config.auto_start_node - } + /// Check if integrated node is starting with application. + pub fn autostart_node() -> bool { + let r_config = Settings::app_config_to_read(); + r_config.auto_start_node + } - /// Toggle integrated node autostart. - pub fn toggle_node_autostart() { - let autostart = Self::autostart_node(); - let mut w_app_config = Settings::app_config_to_update(); - w_app_config.auto_start_node = !autostart; - w_app_config.save(); - } + /// Toggle integrated node autostart. + pub fn toggle_node_autostart() { + let autostart = Self::autostart_node(); + let mut w_app_config = Settings::app_config_to_update(); + w_app_config.auto_start_node = !autostart; + w_app_config.save(); + } - /// Check if it's needed to show wallet list at dual panel wallets mode. - pub fn show_wallets_at_dual_panel() -> bool { - let r_config = Settings::app_config_to_read(); - r_config.show_wallets_at_dual_panel - } + /// Check if it's needed to show wallet list at dual panel wallets mode. + pub fn show_wallets_at_dual_panel() -> bool { + let r_config = Settings::app_config_to_read(); + r_config.show_wallets_at_dual_panel + } - /// Toggle flag to show wallet list at dual panel wallets mode. - pub fn toggle_show_wallets_at_dual_panel() { - let show = Self::show_wallets_at_dual_panel(); - let mut w_app_config = Settings::app_config_to_update(); - w_app_config.show_wallets_at_dual_panel = !show; - w_app_config.save(); - } + /// Toggle flag to show wallet list at dual panel wallets mode. + pub fn toggle_show_wallets_at_dual_panel() { + let show = Self::show_wallets_at_dual_panel(); + let mut w_app_config = Settings::app_config_to_update(); + w_app_config.show_wallets_at_dual_panel = !show; + w_app_config.save(); + } - /// Check if it's needed to show all connections or integrated node info at network panel. - pub fn show_connections_network_panel() -> bool { - let r_config = Settings::app_config_to_read(); - r_config.show_connections_network_panel - } + /// Check if it's needed to show all connections or integrated node info at network panel. + pub fn show_connections_network_panel() -> bool { + let r_config = Settings::app_config_to_read(); + r_config.show_connections_network_panel + } - /// Toggle flag to show all connections or integrated node info at network panel. - pub fn toggle_show_connections_network_panel() { - let show = Self::show_connections_network_panel(); - let mut w_app_config = Settings::app_config_to_update(); - w_app_config.show_connections_network_panel = !show; - w_app_config.save(); - } + /// Toggle flag to show all connections or integrated node info at network panel. + pub fn toggle_show_connections_network_panel() { + let show = Self::show_connections_network_panel(); + let mut w_app_config = Settings::app_config_to_update(); + w_app_config.show_connections_network_panel = !show; + w_app_config.save(); + } - /// Save desktop window width and height. - pub fn save_window_size(w: f32, h: f32) { - let mut w_app_config = Settings::app_config_to_update(); - w_app_config.width = w; - w_app_config.height = h; - w_app_config.save(); - } + /// Save desktop window width and height. + pub fn save_window_size(w: f32, h: f32) { + let mut w_app_config = Settings::app_config_to_update(); + w_app_config.width = w; + w_app_config.height = h; + w_app_config.save(); + } - /// Get desktop window width and height. - pub fn window_size() -> (f32, f32) { - let r_config = Settings::app_config_to_read(); - (r_config.width, r_config.height) - } + /// Get desktop window width and height. + pub fn window_size() -> (f32, f32) { + let r_config = Settings::app_config_to_read(); + (r_config.width, r_config.height) + } - /// Save desktop window position. - pub fn save_window_pos(x: f32, y: f32) { - let mut w_app_config = Settings::app_config_to_update(); - w_app_config.x = Some(x); - w_app_config.y = Some(y); - w_app_config.save(); - } + /// Save desktop window position. + pub fn save_window_pos(x: f32, y: f32) { + let mut w_app_config = Settings::app_config_to_update(); + w_app_config.x = Some(x); + w_app_config.y = Some(y); + w_app_config.save(); + } - /// Get desktop window position. - pub fn window_pos() -> Option<(f32, f32)> { - let r_config = Settings::app_config_to_read(); - if r_config.x.is_some() && r_config.y.is_some() { - return Some((r_config.x.unwrap(), r_config.y.unwrap())) - } - None - } + /// Get desktop window position. + pub fn window_pos() -> Option<(f32, f32)> { + let r_config = Settings::app_config_to_read(); + if r_config.x.is_some() && r_config.y.is_some() { + return Some((r_config.x.unwrap(), r_config.y.unwrap())); + } + None + } - /// Save locale code. - pub fn save_locale(lang: &str) { - let mut w_app_config = Settings::app_config_to_update(); - w_app_config.lang = Some(lang.to_string()); - w_app_config.save(); - } + /// Save locale code. + pub fn save_locale(lang: &str) { + let mut w_app_config = Settings::app_config_to_update(); + w_app_config.lang = Some(lang.to_string()); + w_app_config.save(); + } - /// Get current saved locale code. - pub fn locale() -> Option { - let r_config = Settings::app_config_to_read(); - if r_config.lang.is_some() { - return Some(r_config.lang.clone().unwrap()) - } - None - } + /// Get current saved locale code. + pub fn locale() -> Option { + let r_config = Settings::app_config_to_read(); + if r_config.lang.is_some() { + return Some(r_config.lang.clone().unwrap()); + } + None + } - /// Toggle English locale layout. for software keyboard. - pub fn toggle_english_keyboard() { - let english = Self::english_keyboard(); - let mut w_app_config = Settings::app_config_to_update(); - w_app_config.english_keyboard = Some(!english); - w_app_config.save(); - } + /// Toggle English locale layout. for software keyboard. + pub fn toggle_english_keyboard() { + let english = Self::english_keyboard(); + let mut w_app_config = Settings::app_config_to_update(); + w_app_config.english_keyboard = Some(!english); + w_app_config.save(); + } - /// Check if English locale layout should be used for software keyboard. - pub fn english_keyboard() -> bool { - let r_config = Settings::app_config_to_read(); - r_config.english_keyboard.unwrap_or(false) - } + /// Check if English locale layout should be used for software keyboard. + pub fn english_keyboard() -> bool { + let r_config = Settings::app_config_to_read(); + r_config.english_keyboard.unwrap_or(false) + } - /// Check if integrated node warning is needed for Android. - pub fn android_integrated_node_warning_needed() -> bool { - let r_config = Settings::app_config_to_read(); - r_config.android_integrated_node_warning.unwrap_or(true) - } + /// Check if integrated node warning is needed for Android. + pub fn android_integrated_node_warning_needed() -> bool { + let r_config = Settings::app_config_to_read(); + r_config.android_integrated_node_warning.unwrap_or(true) + } - /// Mark integrated node warning for Android as shown. - pub fn show_android_integrated_node_warning() { - let mut w_config = Settings::app_config_to_update(); - w_config.android_integrated_node_warning = Some(false); - w_config.save(); - } + /// Mark integrated node warning for Android as shown. + pub fn show_android_integrated_node_warning() { + let mut w_config = Settings::app_config_to_update(); + w_config.android_integrated_node_warning = Some(false); + w_config.save(); + } - /// Check if dark theme should be used. - pub fn dark_theme() -> Option { - let r_config = Settings::app_config_to_read(); - r_config.use_dark_theme.clone() - } + /// Check if dark theme should be used. + pub fn dark_theme() -> Option { + let r_config = Settings::app_config_to_read(); + r_config.use_dark_theme.clone() + } - /// Setup flag to use dark theme. - pub fn set_dark_theme(use_dark: bool) { - let mut w_config = Settings::app_config_to_update(); - w_config.use_dark_theme = Some(use_dark); - w_config.save(); - } + /// Setup flag to use dark theme. + pub fn set_dark_theme(use_dark: bool) { + let mut w_config = Settings::app_config_to_update(); + w_config.use_dark_theme = Some(use_dark); + w_config.save(); + } - /// Check if proxy for network requests is needed. - pub fn use_proxy() -> bool { - let r_config = Settings::app_config_to_read(); - r_config.use_proxy.clone().unwrap_or(false) - } + /// Check if proxy for network requests is needed. + pub fn use_proxy() -> bool { + let r_config = Settings::app_config_to_read(); + r_config.use_proxy.clone().unwrap_or(false) + } - /// Enable or disable proxy for network requests. - pub fn toggle_use_proxy() { - let use_proxy = Self::use_proxy(); - let mut w_config = Settings::app_config_to_update(); - w_config.use_proxy = Some(!use_proxy); - w_config.save(); - } + /// Enable or disable proxy for network requests. + pub fn toggle_use_proxy() { + let use_proxy = Self::use_proxy(); + let mut w_config = Settings::app_config_to_update(); + w_config.use_proxy = Some(!use_proxy); + w_config.save(); + } - /// Check if SOCKS5 or HTTP proxy should be used. - pub fn use_socks_proxy() -> bool { - let r_config = Settings::app_config_to_read(); - r_config.use_socks_proxy.clone().unwrap_or(true) - } + /// Check if SOCKS5 or HTTP proxy should be used. + pub fn use_socks_proxy() -> bool { + let r_config = Settings::app_config_to_read(); + r_config.use_socks_proxy.clone().unwrap_or(true) + } - /// Enable SOCKS5 or HTTP proxy. - pub fn toggle_use_socks_proxy() { - let use_proxy = Self::use_socks_proxy(); - let mut w_config = Settings::app_config_to_update(); - w_config.use_socks_proxy = Some(!use_proxy); - w_config.save(); - } + /// Enable SOCKS5 or HTTP proxy. + pub fn toggle_use_socks_proxy() { + let use_proxy = Self::use_socks_proxy(); + let mut w_config = Settings::app_config_to_update(); + w_config.use_socks_proxy = Some(!use_proxy); + w_config.save(); + } - /// Get SOCKS proxy URL. - pub fn socks_proxy_url() -> Option { - let r_config = Settings::app_config_to_read(); - r_config.socks_proxy_url.clone() - } + /// Get SOCKS proxy URL. + pub fn socks_proxy_url() -> Option { + let r_config = Settings::app_config_to_read(); + r_config.socks_proxy_url.clone() + } - /// Save SOCKS proxy URL. - pub fn save_socks_proxy_url(url: Option) { - let mut w_config = Settings::app_config_to_update(); - w_config.socks_proxy_url = url; - w_config.save(); - } + /// Save SOCKS proxy URL. + pub fn save_socks_proxy_url(url: Option) { + let mut w_config = Settings::app_config_to_update(); + w_config.socks_proxy_url = url; + w_config.save(); + } - /// Get HTTP proxy URL. - pub fn http_proxy_url() -> Option { - let r_config = Settings::app_config_to_read(); - r_config.http_proxy_url.clone() - } + /// Get HTTP proxy URL. + pub fn http_proxy_url() -> Option { + let r_config = Settings::app_config_to_read(); + r_config.http_proxy_url.clone() + } - /// Save HTTP proxy URL. - pub fn save_http_proxy_url(url: Option) { - let mut w_config = Settings::app_config_to_update(); - w_config.http_proxy_url = url; - w_config.save(); - } + /// Save HTTP proxy URL. + pub fn save_http_proxy_url(url: Option) { + let mut w_config = Settings::app_config_to_update(); + w_config.http_proxy_url = url; + w_config.save(); + } - /// Check updates on startup. - pub fn check_updates() -> bool { - let r_config = Settings::app_config_to_read(); - r_config.check_updates.unwrap_or(true) - } + /// Check updates on startup. + pub fn check_updates() -> bool { + let r_config = Settings::app_config_to_read(); + r_config.check_updates.unwrap_or(true) + } - /// Disable or enable updates checking. - pub fn toggle_check_updates() { - let check = Self::check_updates(); - // Clear update info on disable. - if !check { - Self::save_update(None); - } - let mut w_config = Settings::app_config_to_update(); - w_config.check_updates = Some(!check); - w_config.save(); - } + /// Disable or enable updates checking. + pub fn toggle_check_updates() { + let check = Self::check_updates(); + // Clear update info on disable. + if !check { + Self::save_update(None); + } + let mut w_config = Settings::app_config_to_update(); + w_config.check_updates = Some(!check); + w_config.save(); + } - /// Get last update information, that includes: version, date and description. - pub fn app_update() -> Option { - let r_config = Settings::app_config_to_read(); - r_config.app_update.clone() - } + /// Get last update information, that includes: version, date and description. + pub fn app_update() -> Option { + let r_config = Settings::app_config_to_read(); + r_config.app_update.clone() + } - /// Save update information. - pub fn save_update(release: Option<&ReleaseInfo>) { - let mut w_config = Settings::app_config_to_update(); - match release { - None => { - w_config.app_update = None; - } - Some(release) => { - let url = release.url(); - if let Some(url) = url { - let app_update = AppUpdate { - version: release.version(), - size: release.size(), - date: release.date(), - changelog: release.body.clone(), - url, - }; - w_config.app_update = Some(app_update); - } - } - } - w_config.save(); - } -} \ No newline at end of file + /// Save update information. + pub fn save_update(release: Option<&ReleaseInfo>) { + let mut w_config = Settings::app_config_to_update(); + match release { + None => { + w_config.app_update = None; + } + Some(release) => { + let url = release.url(); + if let Some(url) = url { + let app_update = AppUpdate { + version: release.version(), + size: release.size(), + date: release.date(), + changelog: release.body.clone(), + url, + }; + w_config.app_update = Some(app_update); + } + } + } + w_config.save(); + } +} diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 1fb7d1a2..8af595ea 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -16,4 +16,4 @@ mod settings; pub use settings::Settings; mod config; -pub use config::*; \ No newline at end of file +pub use config::*; diff --git a/src/settings/settings.rs b/src/settings/settings.rs index a335a0e1..a314a9e8 100644 --- a/src/settings/settings.rs +++ b/src/settings/settings.rs @@ -16,8 +16,8 @@ use grin_config::ConfigError; use grin_core::global; use lazy_static::lazy_static; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; -use serde::de::DeserializeOwned; use serde::Serialize; +use serde::de::DeserializeOwned; use std::fs::{self, File}; use std::io::Write; use std::path::PathBuf; @@ -29,180 +29,178 @@ use crate::tor::TorConfig; use crate::wallet::ConnectionsConfig; lazy_static! { - /// Static settings state to be accessible globally. - static ref SETTINGS_STATE: Arc = Arc::new(Settings::init()); + /// Static settings state to be accessible globally. + static ref SETTINGS_STATE: Arc = Arc::new(Settings::init()); } /// Contains initialized configurations. pub struct Settings { - /// Application configuration. - app_config: Arc>, - /// Integrated node configuration. - node_config: Arc>, - /// Wallet connections configuration. - conn_config: Arc>, - /// Tor server configuration. - tor_config: Arc> + /// Application configuration. + app_config: Arc>, + /// Integrated node configuration. + node_config: Arc>, + /// Wallet connections configuration. + conn_config: Arc>, + /// Tor server configuration. + tor_config: Arc>, } impl Settings { - /// Main application directory name. - pub const MAIN_DIR_NAME: &'static str = ".grim"; - /// Application socket name. - pub const SOCKET_NAME: &'static str = "grim.sock"; - /// Log file name. - pub const LOG_FILE_NAME: &'static str = "grim.log"; + /// Main application directory name. + pub const MAIN_DIR_NAME: &'static str = ".grim"; + /// Application socket name. + pub const SOCKET_NAME: &'static str = "grim.sock"; + /// Log file name. + pub const LOG_FILE_NAME: &'static str = "grim.log"; - /// Initialize settings with app and node configs. - fn init() -> Self { - // Initialize app config. - let app_config_path = Settings::config_path(AppConfig::FILE_NAME, None); - let app_config = Self::init_config::(app_config_path); + /// Initialize settings with app and node configs. + fn init() -> Self { + // Initialize app config. + let app_config_path = Settings::config_path(AppConfig::FILE_NAME, None); + let app_config = Self::init_config::(app_config_path); - // Initialize tor config. - let tor_config_path = Settings::config_path(TorConfig::FILE_NAME, None); - let mut tor_config = Self::init_config::(tor_config_path); - tor_config.migrate(); + // Initialize tor config. + let tor_config_path = Settings::config_path(TorConfig::FILE_NAME, None); + let mut tor_config = Self::init_config::(tor_config_path); + tor_config.migrate(); - // Setup chain type. - let chain_type = &app_config.chain_type; - if !global::GLOBAL_CHAIN_TYPE.is_init() { - global::init_global_chain_type(*chain_type); - } else { - global::set_global_chain_type(*chain_type); - global::set_local_chain_type(*chain_type); - } + // Setup chain type. + let chain_type = &app_config.chain_type; + if !global::GLOBAL_CHAIN_TYPE.is_init() { + global::init_global_chain_type(*chain_type); + } else { + global::set_global_chain_type(*chain_type); + global::set_local_chain_type(*chain_type); + } - Self { - node_config: Arc::new(RwLock::new(NodeConfig::for_chain_type(chain_type))), - conn_config: Arc::new(RwLock::new(ConnectionsConfig::for_chain_type(chain_type))), - app_config: Arc::new(RwLock::new(app_config)), - tor_config: Arc::new(RwLock::new(tor_config)), - } - } + Self { + node_config: Arc::new(RwLock::new(NodeConfig::for_chain_type(chain_type))), + conn_config: Arc::new(RwLock::new(ConnectionsConfig::for_chain_type(chain_type))), + app_config: Arc::new(RwLock::new(app_config)), + tor_config: Arc::new(RwLock::new(tor_config)), + } + } - /// Initialize configuration from provided file path or set [`Default`] if file not exists. - pub fn init_config(path: PathBuf) -> T { - let parsed = Self::read_from_file::(path.clone()); - if !path.exists() || parsed.is_err() { - let default_config = T::default(); - Settings::write_to_file(&default_config, path); - default_config - } else { - parsed.unwrap() - } - } + /// Initialize configuration from provided file path or set [`Default`] if file not exists. + pub fn init_config(path: PathBuf) -> T { + let parsed = Self::read_from_file::(path.clone()); + if !path.exists() || parsed.is_err() { + let default_config = T::default(); + Settings::write_to_file(&default_config, path); + default_config + } else { + parsed.unwrap() + } + } - /// Get node configuration to read values. - pub fn node_config_to_read() -> RwLockReadGuard<'static, NodeConfig> { - SETTINGS_STATE.node_config.read() - } + /// Get node configuration to read values. + pub fn node_config_to_read() -> RwLockReadGuard<'static, NodeConfig> { + SETTINGS_STATE.node_config.read() + } - /// Get node configuration to update values. - pub fn node_config_to_update() -> RwLockWriteGuard<'static, NodeConfig> { - SETTINGS_STATE.node_config.write() - } + /// Get node configuration to update values. + pub fn node_config_to_update() -> RwLockWriteGuard<'static, NodeConfig> { + SETTINGS_STATE.node_config.write() + } - /// Get app configuration to read values. - pub fn app_config_to_read() -> RwLockReadGuard<'static, AppConfig> { - SETTINGS_STATE.app_config.read() - } + /// Get app configuration to read values. + pub fn app_config_to_read() -> RwLockReadGuard<'static, AppConfig> { + SETTINGS_STATE.app_config.read() + } - /// Get app configuration to update values. - pub fn app_config_to_update() -> RwLockWriteGuard<'static, AppConfig> { - SETTINGS_STATE.app_config.write() - } + /// Get app configuration to update values. + pub fn app_config_to_update() -> RwLockWriteGuard<'static, AppConfig> { + SETTINGS_STATE.app_config.write() + } - /// Get connections configuration to read values. - pub fn conn_config_to_read() -> RwLockReadGuard<'static, ConnectionsConfig> { - SETTINGS_STATE.conn_config.read() - } + /// Get connections configuration to read values. + pub fn conn_config_to_read() -> RwLockReadGuard<'static, ConnectionsConfig> { + SETTINGS_STATE.conn_config.read() + } - /// Get connections configuration to update values. - pub fn conn_config_to_update() -> RwLockWriteGuard<'static, ConnectionsConfig> { - SETTINGS_STATE.conn_config.write() - } + /// Get connections configuration to update values. + pub fn conn_config_to_update() -> RwLockWriteGuard<'static, ConnectionsConfig> { + SETTINGS_STATE.conn_config.write() + } - /// Get tor server configuration to read values. - pub fn tor_config_to_read() -> RwLockReadGuard<'static, TorConfig> { - SETTINGS_STATE.tor_config.read() - } + /// Get tor server configuration to read values. + pub fn tor_config_to_read() -> RwLockReadGuard<'static, TorConfig> { + SETTINGS_STATE.tor_config.read() + } - /// Get tor server configuration to update values. - pub fn tor_config_to_update() -> RwLockWriteGuard<'static, TorConfig> { - SETTINGS_STATE.tor_config.write() - } + /// Get tor server configuration to update values. + pub fn tor_config_to_update() -> RwLockWriteGuard<'static, TorConfig> { + SETTINGS_STATE.tor_config.write() + } - /// Get base directory path for configuration. - pub fn base_path(sub_dir: Option) -> PathBuf { - // Check if dir exists. - let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::new()); - path.push(Self::MAIN_DIR_NAME); - if sub_dir.is_some() { - path.push(sub_dir.unwrap()); - } - // Create if the default path doesn't exist. - if !path.exists() { - let _ = fs::create_dir_all(path.clone()); - } - path - } + /// Get base directory path for configuration. + pub fn base_path(sub_dir: Option) -> PathBuf { + // Check if dir exists. + let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::new()); + path.push(Self::MAIN_DIR_NAME); + if sub_dir.is_some() { + path.push(sub_dir.unwrap()); + } + // Create if the default path doesn't exist. + if !path.exists() { + let _ = fs::create_dir_all(path.clone()); + } + path + } - /// Get log file path. - pub fn log_path() -> String { - let mut log_path = Self::base_path(None); - log_path.push(Self::LOG_FILE_NAME); - log_path.to_str().unwrap().into() - } + /// Get log file path. + pub fn log_path() -> String { + let mut log_path = Self::base_path(None); + log_path.push(Self::LOG_FILE_NAME); + log_path.to_str().unwrap().into() + } - /// Get desktop application socket path. - pub fn socket_path() -> PathBuf { - let mut socket_path = Self::base_path(None); - socket_path.push(Self::SOCKET_NAME); - socket_path - } + /// Get desktop application socket path. + pub fn socket_path() -> PathBuf { + let mut socket_path = Self::base_path(None); + socket_path.push(Self::SOCKET_NAME); + socket_path + } - /// Get configuration file path from provided name and subdirectory if needed. - pub fn config_path(config_name: &str, sub_dir: Option) -> PathBuf { - let mut path = Self::base_path(sub_dir); - path.push(config_name); - path - } + /// Get configuration file path from provided name and subdirectory if needed. + pub fn config_path(config_name: &str, sub_dir: Option) -> PathBuf { + let mut path = Self::base_path(sub_dir); + path.push(config_name); + path + } - /// Get path of file created when application crashed. - pub fn crash_check_path() -> PathBuf { - let mut path = Self::base_path(None); - path.push("crashed"); - path - } + /// Get path of file created when application crashed. + pub fn crash_check_path() -> PathBuf { + let mut path = Self::base_path(None); + path.push("crashed"); + path + } - /// Delete crash file. - pub fn delete_crash_check() { - let log = Self::crash_check_path(); - if log.exists() { - let _ = fs::remove_file(log.clone()); - } - } + /// Delete crash file. + pub fn delete_crash_check() { + let log = Self::crash_check_path(); + if log.exists() { + let _ = fs::remove_file(log.clone()); + } + } - /// Read configuration from the file. - pub fn read_from_file(config_path: PathBuf) -> Result { - let file_content = fs::read_to_string(config_path.clone())?; - let parsed = toml::from_str::(file_content.as_str()); - match parsed { - Ok(cfg) => Ok(cfg), - Err(e) => { - Err(ConfigError::ParseError( - config_path.to_str().unwrap().to_string(), - format!("{}", e), - )) - } - } - } + /// Read configuration from the file. + pub fn read_from_file(config_path: PathBuf) -> Result { + let file_content = fs::read_to_string(config_path.clone())?; + let parsed = toml::from_str::(file_content.as_str()); + match parsed { + Ok(cfg) => Ok(cfg), + Err(e) => Err(ConfigError::ParseError( + config_path.to_str().unwrap().to_string(), + format!("{}", e), + )), + } + } - /// Write configuration to the file. - pub fn write_to_file(config: &T, path: PathBuf) { - let conf_out = toml::to_string(config).unwrap(); - let mut file = File::create(path.to_str().unwrap()).unwrap(); - file.write_all(conf_out.as_bytes()).unwrap(); - } -} \ No newline at end of file + /// Write configuration to the file. + pub fn write_to_file(config: &T, path: PathBuf) { + let conf_out = toml::to_string(config).unwrap(); + let mut file = File::create(path.to_str().unwrap()).unwrap(); + file.write_all(conf_out.as_bytes()).unwrap(); + } +} diff --git a/src/tor/config.rs b/src/tor/config.rs index 4a8312f2..869e3bec 100644 --- a/src/tor/config.rs +++ b/src/tor/config.rs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::path::PathBuf; use serde_derive::{Deserialize, Serialize}; +use std::path::PathBuf; use crate::Settings; use crate::tor::{TorBridge, TorProxy}; @@ -23,217 +23,207 @@ const TOR_CONFIG_VERSION: i32 = 1; /// Tor configuration. #[derive(Serialize, Deserialize, Clone)] pub struct TorConfig { - /// Proxy for tor connections. - proxy: Option, - /// SOCKS5 proxy type. - proxy_socks5: TorProxy, - /// HTTP proxy type. - proxy_http: TorProxy, + /// Proxy for tor connections. + proxy: Option, + /// SOCKS5 proxy type. + proxy_socks5: TorProxy, + /// HTTP proxy type. + proxy_http: TorProxy, - /// Selected bridge type. - bridge: Option, - /// Webtunnel bridge type. - webtunnel: TorBridge, - /// Obfs4 bridge type. - obfs4: TorBridge, - /// Snowflake bridge type. - snowflake: TorBridge, + /// Selected bridge type. + bridge: Option, + /// Webtunnel bridge type. + webtunnel: TorBridge, + /// Obfs4 bridge type. + obfs4: TorBridge, + /// Snowflake bridge type. + snowflake: TorBridge, - /// Config version. - ver: Option + /// Config version. + ver: Option, } impl Default for TorConfig { - fn default() -> Self { - let webtunnel = Self::default_webtunnel_bridge(); - Self { - proxy: None, - proxy_socks5: TorProxy::HTTP(TorProxy::DEFAULT_SOCKS5_URL.to_string()), - proxy_http: TorProxy::HTTP(TorProxy::DEFAULT_HTTP_URL.to_string()), - bridge: Some(webtunnel.clone()), - webtunnel, - obfs4: TorBridge::Obfs4( - TorBridge::DEFAULT_OBFS4_BIN_PATH.to_string(), - TorBridge::DEFAULT_OBFS4_CONN_LINE.to_string() - ), - snowflake: TorBridge::Snowflake( - TorBridge::DEFAULT_SNOWFLAKE_BIN_PATH.to_string(), - TorBridge::DEFAULT_SNOWFLAKE_CONN_LINE.to_string() - ), - ver: Some(TOR_CONFIG_VERSION), - } - } + fn default() -> Self { + let webtunnel = Self::default_webtunnel_bridge(); + Self { + proxy: None, + proxy_socks5: TorProxy::HTTP(TorProxy::DEFAULT_SOCKS5_URL.to_string()), + proxy_http: TorProxy::HTTP(TorProxy::DEFAULT_HTTP_URL.to_string()), + bridge: Some(webtunnel.clone()), + webtunnel, + obfs4: TorBridge::Obfs4( + TorBridge::DEFAULT_OBFS4_BIN_PATH.to_string(), + TorBridge::DEFAULT_OBFS4_CONN_LINE.to_string(), + ), + snowflake: TorBridge::Snowflake( + TorBridge::DEFAULT_SNOWFLAKE_BIN_PATH.to_string(), + TorBridge::DEFAULT_SNOWFLAKE_CONN_LINE.to_string(), + ), + ver: Some(TOR_CONFIG_VERSION), + } + } } impl TorConfig { - /// Tor configuration file name. - pub const FILE_NAME: &'static str = "tor.toml"; + /// Tor configuration file name. + pub const FILE_NAME: &'static str = "tor.toml"; - /// Directory for Tor data files. - const DIR_NAME: &'static str = "tor"; + /// Directory for Tor data files. + const DIR_NAME: &'static str = "tor"; - /// Subdirectory name for Tor state. - const STATE_SUB_DIR: &'static str = "state"; - /// Subdirectory name for Tor cache. - const CACHE_SUB_DIR: &'static str = "cache"; - /// Subdirectory name for Tor keystore. - const KEYSTORE_DIR: &'static str = "keystore"; + /// Subdirectory name for Tor state. + const STATE_SUB_DIR: &'static str = "state"; + /// Subdirectory name for Tor cache. + const CACHE_SUB_DIR: &'static str = "cache"; + /// Subdirectory name for Tor keystore. + const KEYSTORE_DIR: &'static str = "keystore"; - /// Webtunnel binary name. - pub const WEBTUNNEL_BIN: &'static str = "webtunnel"; - /// Webtunnel Android binary name. - pub const WEBTUNNEL_ANDROID_BIN: &'static str = "libwebtunnel.so"; + /// Webtunnel binary name. + pub const WEBTUNNEL_BIN: &'static str = "webtunnel"; + /// Webtunnel Android binary name. + pub const WEBTUNNEL_ANDROID_BIN: &'static str = "libwebtunnel.so"; - /// Save application configuration to the file. - pub fn save(&self) { - Settings::write_to_file(self, Settings::config_path(Self::FILE_NAME, None)); - } + /// Save application configuration to the file. + pub fn save(&self) { + Settings::write_to_file(self, Settings::config_path(Self::FILE_NAME, None)); + } - /// Get base Tor directory path. - fn base_path() -> PathBuf { - Settings::base_path(Some(Self::DIR_NAME.to_string())) - } + /// Get base Tor directory path. + fn base_path() -> PathBuf { + Settings::base_path(Some(Self::DIR_NAME.to_string())) + } - /// Get path from subdirectory name. - fn sub_dir_path(name: &str) -> String { - let mut base = Self::base_path(); - base.push(name); - base.to_str().unwrap().to_string() - } + /// Get path from subdirectory name. + fn sub_dir_path(name: &str) -> String { + let mut base = Self::base_path(); + base.push(name); + base.to_str().unwrap().to_string() + } - /// Get Tor state directory path. - pub fn state_path() -> String { - Self::sub_dir_path(Self::STATE_SUB_DIR) - } + /// Get Tor state directory path. + pub fn state_path() -> String { + Self::sub_dir_path(Self::STATE_SUB_DIR) + } - /// Get Tor cache directory path. - pub fn cache_path() -> String { - Self::sub_dir_path(Self::CACHE_SUB_DIR) - } + /// Get Tor cache directory path. + pub fn cache_path() -> String { + Self::sub_dir_path(Self::CACHE_SUB_DIR) + } - /// Get Tor keystore directory path. - pub fn keystore_path() -> String { - let mut base = PathBuf::from(Self::state_path()); - base.push(Self::KEYSTORE_DIR); - base.to_str().unwrap().to_string() - } + /// Get Tor keystore directory path. + pub fn keystore_path() -> String { + let mut base = PathBuf::from(Self::state_path()); + base.push(Self::KEYSTORE_DIR); + base.to_str().unwrap().to_string() + } - /// Get default webtunnel bridge. - pub fn default_webtunnel_bridge() -> TorBridge { - TorBridge::Webtunnel( - if egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Android { - "".to_string() - } else { - TorConfig::webtunnel_path() - }, - serde_json::to_string(&TorBridge::DEFAULT_WEBTUNNEL_CONN_LINES) - .unwrap_or(TorBridge::DEFAULT_WEBTUNNEL_CONN_LINE.to_string()) - ) - } + /// Get default webtunnel bridge. + pub fn default_webtunnel_bridge() -> TorBridge { + TorBridge::Webtunnel( + if egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Android { + "".to_string() + } else { + TorConfig::webtunnel_path() + }, + serde_json::to_string(&TorBridge::DEFAULT_WEBTUNNEL_CONN_LINES) + .unwrap_or(TorBridge::DEFAULT_WEBTUNNEL_CONN_LINE.to_string()), + ) + } - /// Webtunnel binary path. - pub fn webtunnel_path() -> String { - let os = egui::os::OperatingSystem::from_target_os(); - if os == egui::os::OperatingSystem::Android { - let base = std::env::var("NATIVE_LIBS_DIR").unwrap_or_default(); - format!("{}/{}", base, Self::WEBTUNNEL_ANDROID_BIN) - } else { - let mut base = Self::base_path(); - base.push(Self::WEBTUNNEL_BIN); - base.to_str().unwrap().to_string() - } - } + /// Webtunnel binary path. + pub fn webtunnel_path() -> String { + let os = egui::os::OperatingSystem::from_target_os(); + if os == egui::os::OperatingSystem::Android { + let base = std::env::var("NATIVE_LIBS_DIR").unwrap_or_default(); + format!("{}/{}", base, Self::WEBTUNNEL_ANDROID_BIN) + } else { + let mut base = Self::base_path(); + base.push(Self::WEBTUNNEL_BIN); + base.to_str().unwrap().to_string() + } + } - /// Save Tor bridge. - pub fn save_bridge(bridge: Option) { - let mut w_tor_config = Settings::tor_config_to_update(); - w_tor_config.bridge = bridge.clone(); - if bridge.is_some() { - let bridge = bridge.unwrap(); - match &bridge { - TorBridge::Webtunnel(_, _) => { - w_tor_config.webtunnel = bridge - } - TorBridge::Obfs4(_, _) => { - w_tor_config.obfs4 = bridge - } - TorBridge::Snowflake(_, _) => { - w_tor_config.snowflake = bridge - } - } - } - w_tor_config.save(); - } + /// Save Tor bridge. + pub fn save_bridge(bridge: Option) { + let mut w_tor_config = Settings::tor_config_to_update(); + w_tor_config.bridge = bridge.clone(); + if bridge.is_some() { + let bridge = bridge.unwrap(); + match &bridge { + TorBridge::Webtunnel(_, _) => w_tor_config.webtunnel = bridge, + TorBridge::Obfs4(_, _) => w_tor_config.obfs4 = bridge, + TorBridge::Snowflake(_, _) => w_tor_config.snowflake = bridge, + } + } + w_tor_config.save(); + } - /// Get current Tor bridge if enabled. - pub fn get_bridge() -> Option { - let r_config = Settings::tor_config_to_read(); - r_config.bridge.clone() - } + /// Get current Tor bridge if enabled. + pub fn get_bridge() -> Option { + let r_config = Settings::tor_config_to_read(); + r_config.bridge.clone() + } - /// Get saved Webtunnel bridge. - pub fn get_webtunnel() -> TorBridge { - let r_config = Settings::tor_config_to_read(); - r_config.webtunnel.clone() - } + /// Get saved Webtunnel bridge. + pub fn get_webtunnel() -> TorBridge { + let r_config = Settings::tor_config_to_read(); + r_config.webtunnel.clone() + } - /// Get saved Obfs4 bridge. - pub fn get_obfs4() -> TorBridge { - let r_config = Settings::tor_config_to_read(); - r_config.obfs4.clone() - } + /// Get saved Obfs4 bridge. + pub fn get_obfs4() -> TorBridge { + let r_config = Settings::tor_config_to_read(); + r_config.obfs4.clone() + } - /// Get saved Snowflake bridge. - pub fn get_snowflake() -> TorBridge { - let r_config = Settings::tor_config_to_read(); - r_config.snowflake.clone() - } + /// Get saved Snowflake bridge. + pub fn get_snowflake() -> TorBridge { + let r_config = Settings::tor_config_to_read(); + r_config.snowflake.clone() + } - /// Save proxy for Tor connections. - pub fn save_proxy(proxy: Option) { - let mut w_config = Settings::tor_config_to_update(); - w_config.proxy = proxy.clone(); - if let Some(p) = proxy { - match p { - TorProxy::SOCKS5(_) => { - w_config.proxy_socks5 = p - } - TorProxy::HTTP(_) => { - w_config.proxy_http = p - } - } - } - w_config.save(); - } + /// Save proxy for Tor connections. + pub fn save_proxy(proxy: Option) { + let mut w_config = Settings::tor_config_to_update(); + w_config.proxy = proxy.clone(); + if let Some(p) = proxy { + match p { + TorProxy::SOCKS5(_) => w_config.proxy_socks5 = p, + TorProxy::HTTP(_) => w_config.proxy_http = p, + } + } + w_config.save(); + } - /// Get used proxy for Tor connections. - pub fn get_proxy() -> Option { - let r_config = Settings::tor_config_to_read(); - r_config.proxy.clone() - } + /// Get used proxy for Tor connections. + pub fn get_proxy() -> Option { + let r_config = Settings::tor_config_to_read(); + r_config.proxy.clone() + } - /// Get saved SOCKS5 proxy. - pub fn get_socks5_proxy() -> TorProxy { - let r_config = Settings::tor_config_to_read(); - r_config.proxy_socks5.clone() - } + /// Get saved SOCKS5 proxy. + pub fn get_socks5_proxy() -> TorProxy { + let r_config = Settings::tor_config_to_read(); + r_config.proxy_socks5.clone() + } - /// Get saved HTTP proxy. - pub fn get_http_proxy() -> TorProxy { - let r_config = Settings::tor_config_to_read(); - r_config.proxy_http.clone() - } + /// Get saved HTTP proxy. + pub fn get_http_proxy() -> TorProxy { + let r_config = Settings::tor_config_to_read(); + r_config.proxy_http.clone() + } - /// Check config version to migrate if needed. - pub fn migrate(&mut self) { - match self.ver { - None => { - // Migrate to 1st version. - self.bridge = Some(TorConfig::default_webtunnel_bridge()); - self.ver = Some(1); - } - Some(_) => {} - } - self.save(); - } -} \ No newline at end of file + /// Check config version to migrate if needed. + pub fn migrate(&mut self) { + match self.ver { + None => { + // Migrate to 1st version. + self.bridge = Some(TorConfig::default_webtunnel_bridge()); + self.ver = Some(1); + } + Some(_) => {} + } + self.save(); + } +} diff --git a/src/tor/http.rs b/src/tor/http.rs index 692942e8..cfc235da 100644 --- a/src/tor/http.rs +++ b/src/tor/http.rs @@ -20,8 +20,8 @@ use std::task::{Context, Poll}; use arti_client::{DataStream, IntoTorAddr, TorClient}; use hyper_tor::client::connect::{Connected, Connection}; -use hyper_tor::http::uri::Scheme; use hyper_tor::http::Uri; +use hyper_tor::http::uri::Scheme; use hyper_tor::service::Service; use pin_project::pin_project; use thiserror::Error; @@ -36,33 +36,33 @@ use tor_rtcompat::Runtime; #[derive(Error, Clone, Debug)] #[non_exhaustive] pub enum ConnectionError { - /// Unsupported URI scheme - #[error("unsupported URI scheme in {uri:?}")] - UnsupportedUriScheme { - /// URI - uri: Uri, - }, + /// Unsupported URI scheme + #[error("unsupported URI scheme in {uri:?}")] + UnsupportedUriScheme { + /// URI + uri: Uri, + }, - /// Missing hostname - #[error("Missing hostname in {uri:?}")] - MissingHostname { - /// URI - uri: Uri, - }, + /// Missing hostname + #[error("Missing hostname in {uri:?}")] + MissingHostname { + /// URI + uri: Uri, + }, - /// Tor connection failed - #[error("Tor connection failed")] - Arti(#[from] arti_client::Error), + /// Tor connection failed + #[error("Tor connection failed")] + Arti(#[from] arti_client::Error), - /// TLS connection failed - #[error("TLS connection failed")] - TLS(#[source] Arc), + /// TLS connection failed + #[error("TLS connection failed")] + TLS(#[source] Arc), } /// We implement this for form's sake impl tor_error::HasKind for ConnectionError { - #[rustfmt::skip] - fn kind(&self) -> tor_error::ErrorKind { + #[rustfmt::skip] + fn kind(&self) -> tor_error::ErrorKind { use ConnectionError as CE; use tor_error::ErrorKind as EK; match self { @@ -89,21 +89,21 @@ impl tor_error::HasKind for ConnectionError { #[derive(Educe)] #[educe(Clone)] // #[derive(Debug)] infers an unwanted bound TC: Clone pub struct ArtiHttpConnector { - /// The client - client: TorClient, + /// The client + client: TorClient, - /// TLS for using across Tor. - tls_conn: Arc, + /// TLS for using across Tor. + tls_conn: Arc, } // #[derive(Clone)] infers a TC: Clone bound impl ArtiHttpConnector { - /// Make a new `ArtiHttpConnector` using an Arti `TorClient` object. - pub fn new(client: TorClient, tls_conn: TC) -> Self { - let tls_conn = tls_conn.into(); - Self { client, tls_conn } - } + /// Make a new `ArtiHttpConnector` using an Arti `TorClient` object. + pub fn new(client: TorClient, tls_conn: TC) -> Self { + let tls_conn = tls_conn.into(); + Self { client, tls_conn } + } } /// Wrapper type that makes an Arti `DataStream` implement necessary traits to be used as @@ -118,141 +118,141 @@ impl ArtiHttpConnector { /// and then used by hyper as the transport for hyper's HTTP implementation. #[pin_project] pub struct ArtiHttpConnection { - /// The stream - #[pin] - inner: MaybeHttpsStream, + /// The stream + #[pin] + inner: MaybeHttpsStream, } /// The actual stream; might be TLS, might not #[pin_project(project = MaybeHttpsStreamProj)] enum MaybeHttpsStream { - /// http - Http(Pin>), // Tc:TlsStream is generally boxed; box this one too + /// http + Http(Pin>), // Tc:TlsStream is generally boxed; box this one too - /// https - Https(#[pin] TC::TlsStream), + /// https + Https(#[pin] TC::TlsStream), } impl Connection for ArtiHttpConnection { - fn connected(&self) -> Connected { - Connected::new() - } + fn connected(&self) -> Connected { + Connected::new() + } } // These trait implementations just defer to the inner `DataStream`; the wrapper type is just // there to implement the `Connection` trait. impl AsyncRead for ArtiHttpConnection { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - match self.project().inner.project() { - MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_read(cx, buf), - MaybeHttpsStreamProj::Https(t) => t.poll_read(cx, buf), - } - } + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + match self.project().inner.project() { + MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_read(cx, buf), + MaybeHttpsStreamProj::Https(t) => t.poll_read(cx, buf), + } + } } impl AsyncWrite for ArtiHttpConnection { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - match self.project().inner.project() { - MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_write(cx, buf), - MaybeHttpsStreamProj::Https(t) => t.poll_write(cx, buf), - } - } + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + match self.project().inner.project() { + MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_write(cx, buf), + MaybeHttpsStreamProj::Https(t) => t.poll_write(cx, buf), + } + } - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.project().inner.project() { - MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_flush(cx), - MaybeHttpsStreamProj::Https(t) => t.poll_flush(cx), - } - } + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.project().inner.project() { + MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_flush(cx), + MaybeHttpsStreamProj::Https(t) => t.poll_flush(cx), + } + } - fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.project().inner.project() { - MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_shutdown(cx), - MaybeHttpsStreamProj::Https(t) => t.poll_shutdown(cx), - } - } + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.project().inner.project() { + MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_shutdown(cx), + MaybeHttpsStreamProj::Https(t) => t.poll_shutdown(cx), + } + } } #[derive(Debug, Clone, Copy, Eq, PartialEq)] /// Are we doing TLS? enum UseTls { - /// No - Bare, + /// No + Bare, - /// Yes - Tls, + /// Yes + Tls, } /// Convert uri to http\[s\] host and port, and whether to do tls fn uri_to_host_port_tls(uri: Uri) -> Result<(String, u16, UseTls), ConnectionError> { - let use_tls = { - // Scheme doesn't derive PartialEq so can't be matched on - let scheme = uri.scheme(); - if scheme == Some(&Scheme::HTTP) { - UseTls::Bare - } else if scheme == Some(&Scheme::HTTPS) { - UseTls::Tls - } else { - return Err(ConnectionError::UnsupportedUriScheme { uri }); - } - }; - let host = match uri.host() { - Some(h) => h, - _ => return Err(ConnectionError::MissingHostname { uri }), - }; - let port = uri.port().map(|x| x.as_u16()).unwrap_or(match use_tls { - UseTls::Tls => 443, - UseTls::Bare => 80, - }); + let use_tls = { + // Scheme doesn't derive PartialEq so can't be matched on + let scheme = uri.scheme(); + if scheme == Some(&Scheme::HTTP) { + UseTls::Bare + } else if scheme == Some(&Scheme::HTTPS) { + UseTls::Tls + } else { + return Err(ConnectionError::UnsupportedUriScheme { uri }); + } + }; + let host = match uri.host() { + Some(h) => h, + _ => return Err(ConnectionError::MissingHostname { uri }), + }; + let port = uri.port().map(|x| x.as_u16()).unwrap_or(match use_tls { + UseTls::Tls => 443, + UseTls::Bare => 80, + }); - Ok((host.to_owned(), port, use_tls)) + Ok((host.to_owned(), port, use_tls)) } impl Service for ArtiHttpConnector { - type Response = ArtiHttpConnection; - type Error = ConnectionError; - #[allow(clippy::type_complexity)] - type Future = Pin> + Send>>; + type Response = ArtiHttpConnection; + type Error = ConnectionError; + #[allow(clippy::type_complexity)] + type Future = Pin> + Send>>; - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } - fn call(&mut self, req: Uri) -> Self::Future { - // `TorClient` objects can be cloned cheaply (the cloned objects refer to the same - // underlying handles required to make Tor connections internally). - // We use this to avoid the returned future having to borrow `self`. - let client = self.client.clone(); - let tls_conn = self.tls_conn.clone(); - Box::pin(async move { - // Extract the host and port to connect to from the URI. - let (host, port, use_tls) = uri_to_host_port_tls(req)?; - // Initiate a new Tor connection, producing a `DataStream` if successful. - let addr = (&host as &str, port) - .into_tor_addr() - .map_err(arti_client::Error::from)?; - let ds = client.connect(addr).await?; + fn call(&mut self, req: Uri) -> Self::Future { + // `TorClient` objects can be cloned cheaply (the cloned objects refer to the same + // underlying handles required to make Tor connections internally). + // We use this to avoid the returned future having to borrow `self`. + let client = self.client.clone(); + let tls_conn = self.tls_conn.clone(); + Box::pin(async move { + // Extract the host and port to connect to from the URI. + let (host, port, use_tls) = uri_to_host_port_tls(req)?; + // Initiate a new Tor connection, producing a `DataStream` if successful. + let addr = (&host as &str, port) + .into_tor_addr() + .map_err(arti_client::Error::from)?; + let ds = client.connect(addr).await?; - let inner = match use_tls { - UseTls::Tls => { - let conn = tls_conn - .connect_impl_tls_stream(&host, ds) - .await - .map_err(|e| ConnectionError::TLS(e.into()))?; - MaybeHttpsStream::Https(conn) - } - UseTls::Bare => MaybeHttpsStream::Http(Box::new(ds).into()), - }; + let inner = match use_tls { + UseTls::Tls => { + let conn = tls_conn + .connect_impl_tls_stream(&host, ds) + .await + .map_err(|e| ConnectionError::TLS(e.into()))?; + MaybeHttpsStream::Https(conn) + } + UseTls::Bare => MaybeHttpsStream::Http(Box::new(ds).into()), + }; - Ok(ArtiHttpConnection { inner }) - }) - } + Ok(ArtiHttpConnection { inner }) + }) + } } diff --git a/src/tor/mod.rs b/src/tor/mod.rs index 5f4067cc..67a3bd81 100644 --- a/src/tor/mod.rs +++ b/src/tor/mod.rs @@ -21,4 +21,4 @@ pub use tor::Tor; mod types; pub use types::*; -mod http; \ No newline at end of file +mod http; diff --git a/src/tor/tor.rs b/src/tor/tor.rs index 1657e088..47906143 100644 --- a/src/tor/tor.rs +++ b/src/tor/tor.rs @@ -15,34 +15,34 @@ use arti_client::config::pt::TransportConfigBuilder; use arti_client::config::{CfgPath, TorClientConfigBuilder}; use arti_client::{TorClient, TorClientConfig}; +use bytes::Bytes; use curve25519_dalek::digest::Digest; use ed25519_dalek::hazmat::ExpandedSecretKey; use fs_mistrust::Mistrust; use grin_util::secp::SecretKey; use http_body_util::{BodyExt, Full}; use lazy_static::lazy_static; +use log::error; use parking_lot::RwLock; +use safelog::DisplayRedacted; use sha2::Sha512; use std::collections::{BTreeMap, BTreeSet}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::str::FromStr; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use std::{fs, thread}; -use std::sync::atomic::{AtomicBool, Ordering}; -use bytes::Bytes; -use log::error; -use safelog::DisplayRedacted; use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder}; use tls_api_native_tls::TlsConnector; use tor_hscrypto::pk::{HsIdKey, HsIdKeypair}; -use tor_hsrproxy::config::{ - Encapsulation, ProxyAction, ProxyConfigBuilder, ProxyPattern, ProxyRule, TargetAddr, -}; use tor_hsrproxy::OnionServiceReverseProxy; +use tor_hsrproxy::config::{ + Encapsulation, ProxyAction, ProxyConfigBuilder, ProxyPattern, ProxyRule, TargetAddr, +}; use tor_hsservice::config::OnionServiceConfigBuilder; use tor_hsservice::{ - HsIdKeypairSpecifier, HsIdPublicKeySpecifier, HsNickname, RunningOnionService, + HsIdKeypairSpecifier, HsIdPublicKeySpecifier, HsNickname, RunningOnionService, }; use tor_keymgr::{ArtiNativeKeystore, KeyMgrBuilder, KeystoreSelector}; use tor_llcrypto::pk::ed25519::ExpandedKeypair; @@ -54,715 +54,731 @@ use crate::tor::http::ArtiHttpConnector; use crate::tor::{TorBridge, TorConfig, TorProxy}; lazy_static! { - /// Static thread-aware state of Tor to be updated from separate thread. - static ref TOR_STATE: Arc = Arc::new(Tor::default()); + /// Static thread-aware state of Tor to be updated from separate thread. + static ref TOR_STATE: Arc = Arc::new(Tor::default()); } /// Tor client to use as SOCKS proxy for requests and to launch Onion services. pub struct Tor { - /// Tor client and config. - client_config: Arc, TorClientConfig)>>>, - /// Flag to check if client is launching. - client_launching: Arc, + /// Tor client and config. + client_config: Arc, TorClientConfig)>>>, + /// Flag to check if client is launching. + client_launching: Arc, - /// Mapping of running Onion services identifiers to proxy. - run: Arc, Arc)> - >>, - /// Mapping of starting Onion services identifiers. - start: Arc - >>, - /// Failed Onion services identifiers. - fail: Arc>>, - /// Checking Onion services identifiers. - check: Arc>>, + /// Mapping of running Onion services identifiers to proxy. + run: Arc< + RwLock< + BTreeMap< + String, + ( + u16, + SecretKey, + Arc, + Arc, + ), + >, + >, + >, + /// Mapping of starting Onion services identifiers. + start: Arc>>, + /// Failed Onion services identifiers. + fail: Arc>>, + /// Checking Onion services identifiers. + check: Arc>>, } impl Default for Tor { - fn default() -> Self { - // Extract webtunnel bridge binary. - if !fs::exists(TorConfig::webtunnel_path()).unwrap_or(true) { - let webtunnel = include_bytes!(concat!(env!("OUT_DIR"), "/tor/webtunnel")); - if !webtunnel.is_empty() { - fs::write(TorConfig::webtunnel_path(), webtunnel).unwrap_or_default(); - } - } - Self { - client_config: Arc::new(RwLock::new(None)), - client_launching: Arc::new(AtomicBool::new(false)), - run: Arc::new(RwLock::new(BTreeMap::new())), - start: Arc::new(RwLock::new(BTreeMap::new())), - fail: Arc::new(RwLock::new(BTreeSet::new())), - check: Arc::new(RwLock::new(BTreeSet::new())), - } - } + fn default() -> Self { + // Extract webtunnel bridge binary. + if !fs::exists(TorConfig::webtunnel_path()).unwrap_or(true) { + let webtunnel = include_bytes!(concat!(env!("OUT_DIR"), "/tor/webtunnel")); + if !webtunnel.is_empty() { + fs::write(TorConfig::webtunnel_path(), webtunnel).unwrap_or_default(); + } + } + Self { + client_config: Arc::new(RwLock::new(None)), + client_launching: Arc::new(AtomicBool::new(false)), + run: Arc::new(RwLock::new(BTreeMap::new())), + start: Arc::new(RwLock::new(BTreeMap::new())), + fail: Arc::new(RwLock::new(BTreeSet::new())), + check: Arc::new(RwLock::new(BTreeSet::new())), + } + } } impl Tor { - /// Create Tor configuration returning unused bridges (exclude more than 2 to avoid stuck). - fn build_config(bridges: Option>) - -> (TorClientConfig, Vec, Vec) { - let mut builder = TorClientConfigBuilder::from_directories( - TorConfig::state_path(), - TorConfig::cache_path(), - ); - // Build bridges. - let mut bridges = bridges.unwrap_or(vec![]); - let max_two_bridges = if bridges.len() > 2 { - let two_bridges = bridges.iter().take(2) - .cloned() - .collect::>(); - bridges = bridges.iter().filter(|b| !two_bridges.contains(b)) - .cloned() - .collect::>(); - two_bridges - } else { - bridges.clone() - }; - for b in max_two_bridges.clone() { - Self::build_bridge(&mut builder, b); - } - builder.address_filter().allow_onion_addrs(true); - // Create config. - let config = builder.build().unwrap(); - (config, bridges, max_two_bridges) - } + /// Create Tor configuration returning unused bridges (exclude more than 2 to avoid stuck). + fn build_config( + bridges: Option>, + ) -> (TorClientConfig, Vec, Vec) { + let mut builder = TorClientConfigBuilder::from_directories( + TorConfig::state_path(), + TorConfig::cache_path(), + ); + // Build bridges. + let mut bridges = bridges.unwrap_or(vec![]); + let max_two_bridges = if bridges.len() > 2 { + let two_bridges = bridges.iter().take(2).cloned().collect::>(); + bridges = bridges + .iter() + .filter(|b| !two_bridges.contains(b)) + .cloned() + .collect::>(); + two_bridges + } else { + bridges.clone() + }; + for b in max_two_bridges.clone() { + Self::build_bridge(&mut builder, b); + } + builder.address_filter().allow_onion_addrs(true); + // Create config. + let config = builder.build().unwrap(); + (config, bridges, max_two_bridges) + } - /// Build bootstrapped client from provided config. - fn build_client_bootstrap(config: TorClientConfig) -> Option> { - let runtime = TokioNativeTlsRuntime::create().unwrap(); - let client_res = TorClient::with_runtime(runtime) - .config(config.clone()) - .create_unbootstrapped(); - if client_res.is_err() { - return None; - } - let client = client_res.unwrap(); - let bootstrapping = Arc::new(AtomicBool::new(true)); - let bootstrap_success = Arc::new(AtomicBool::new(false)); - let bootstrapping_t = bootstrapping.clone(); - let bootstrap_success_t = bootstrap_success.clone(); - let c = client.clone(); - client.runtime().spawn(async move { - let task = c.bootstrap(); - // Bootstrap client with 60s timeout. - if tokio::time::timeout(Duration::from_millis(60000), task).await.is_ok() { - bootstrap_success_t.store(true, Ordering::Relaxed); - } - bootstrapping_t.store(false, Ordering::Relaxed); - }).unwrap(); - // Wait client to finish bootstrap. - while bootstrapping.load(Ordering::Relaxed) { - thread::sleep(Duration::from_millis(1000)); - } - if bootstrap_success.load(Ordering::Relaxed) { - Some(client) - } else { - None - } - } + /// Build bootstrapped client from provided config. + fn build_client_bootstrap(config: TorClientConfig) -> Option> { + let runtime = TokioNativeTlsRuntime::create().unwrap(); + let client_res = TorClient::with_runtime(runtime) + .config(config.clone()) + .create_unbootstrapped(); + if client_res.is_err() { + return None; + } + let client = client_res.unwrap(); + let bootstrapping = Arc::new(AtomicBool::new(true)); + let bootstrap_success = Arc::new(AtomicBool::new(false)); + let bootstrapping_t = bootstrapping.clone(); + let bootstrap_success_t = bootstrap_success.clone(); + let c = client.clone(); + client + .runtime() + .spawn(async move { + let task = c.bootstrap(); + // Bootstrap client with 60s timeout. + if tokio::time::timeout(Duration::from_millis(60000), task) + .await + .is_ok() + { + bootstrap_success_t.store(true, Ordering::Relaxed); + } + bootstrapping_t.store(false, Ordering::Relaxed); + }) + .unwrap(); + // Wait client to finish bootstrap. + while bootstrapping.load(Ordering::Relaxed) { + thread::sleep(Duration::from_millis(1000)); + } + if bootstrap_success.load(Ordering::Relaxed) { + Some(client) + } else { + None + } + } - /// Launch Tor client. - fn launch() { - // Wait client to finish launch and exit on success. - while TOR_STATE.client_launching.load(Ordering::Relaxed) { - thread::sleep(Duration::from_millis(1000)); - } - { - if TOR_STATE.client_config.read().is_some() { - return; - } - } - // Cleanup keys, state and cache. - fs::remove_dir_all(TorConfig::keystore_path()).unwrap_or_default(); - fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default(); - fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default(); - TOR_STATE.client_launching.store(true, Ordering::Relaxed); - // Get initial bridges. - let initial_bridges = if let Some(b) = TorConfig::get_bridge() { - let lines_parse = serde_json::from_str::>(&b.connection_line()); - let bridges = lines_parse.unwrap_or_else(|_| b.connection_line() - .lines() - .map(|l| l.to_string()).collect() - ).iter().map(|l| { - let mut bridge = b.clone(); - bridge.update_conn_line(l.clone()); - bridge - }).collect::>(); - Some(bridges) - } else { - None - }; - // Bootstrap client in the loop, trying different bridges. - let mut default_attempt = false; - let mut config_bridges = Self::build_config(initial_bridges); - loop { - let (config, unused_bridges, used_bridges) = config_bridges.clone(); - let client = Self::build_client_bootstrap(config.clone()); - if let Some(c) = client { - // Update bridges order. - if let Some(b) = TorConfig::get_bridge() { - let lines_parse = serde_json::from_str::>(&b.connection_line()); - match lines_parse { - Ok(mut lines) => { - lines.sort_by_key(|l| { - let mut bridge = b.clone(); - bridge.update_conn_line(l.clone()); - !used_bridges.contains(&bridge) - }); - let lines_str = serde_json::to_string(&lines) - .unwrap_or_else(|_| { - TorConfig::default_webtunnel_bridge().connection_line() - }); - TorBridge::save_bridge_conn_line(&b, lines_str); - } - Err(_) => {}, - } - } - TOR_STATE.client_config.write().replace((c, config.clone())); - break; - } else { - // Cleanup state and cache. - fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default(); - fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default(); - // Check unused bridges to check another part. - if !unused_bridges.is_empty() { - config_bridges = Self::build_config(Some(unused_bridges)); - continue; - } - if !default_attempt { - default_attempt = true; - // Launch client with default Webtunnel bridges if failed. - let add_bridges = TorBridge::DEFAULT_WEBTUNNEL_CONN_LINES.iter() - .map(|b| TorBridge::Webtunnel(TorConfig::webtunnel_path(), b.to_string())) - .collect::>(); - config_bridges = Self::build_config(Some(add_bridges)); - continue; - } else if TorConfig::get_bridge().is_some() { - // Launch without bridges if all attempts failed. - let (config, _, _) = Self::build_config(None); - let client = Self::build_client_bootstrap(config.clone()); - if let Some(c) = client { - TOR_STATE.client_config.write().replace((c, config)); - } - break; - } - } - }; - TOR_STATE.client_launching.store(false, Ordering::Relaxed); - } + /// Launch Tor client. + fn launch() { + // Wait client to finish launch and exit on success. + while TOR_STATE.client_launching.load(Ordering::Relaxed) { + thread::sleep(Duration::from_millis(1000)); + } + { + if TOR_STATE.client_config.read().is_some() { + return; + } + } + // Cleanup keys, state and cache. + fs::remove_dir_all(TorConfig::keystore_path()).unwrap_or_default(); + fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default(); + fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default(); + TOR_STATE.client_launching.store(true, Ordering::Relaxed); + // Get initial bridges. + let initial_bridges = if let Some(b) = TorConfig::get_bridge() { + let lines_parse = serde_json::from_str::>(&b.connection_line()); + let bridges = lines_parse + .unwrap_or_else(|_| b.connection_line().lines().map(|l| l.to_string()).collect()) + .iter() + .map(|l| { + let mut bridge = b.clone(); + bridge.update_conn_line(l.clone()); + bridge + }) + .collect::>(); + Some(bridges) + } else { + None + }; + // Bootstrap client in the loop, trying different bridges. + let mut default_attempt = false; + let mut config_bridges = Self::build_config(initial_bridges); + loop { + let (config, unused_bridges, used_bridges) = config_bridges.clone(); + let client = Self::build_client_bootstrap(config.clone()); + if let Some(c) = client { + // Update bridges order. + if let Some(b) = TorConfig::get_bridge() { + let lines_parse = serde_json::from_str::>(&b.connection_line()); + match lines_parse { + Ok(mut lines) => { + lines.sort_by_key(|l| { + let mut bridge = b.clone(); + bridge.update_conn_line(l.clone()); + !used_bridges.contains(&bridge) + }); + let lines_str = serde_json::to_string(&lines).unwrap_or_else(|_| { + TorConfig::default_webtunnel_bridge().connection_line() + }); + TorBridge::save_bridge_conn_line(&b, lines_str); + } + Err(_) => {} + } + } + TOR_STATE.client_config.write().replace((c, config.clone())); + break; + } else { + // Cleanup state and cache. + fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default(); + fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default(); + // Check unused bridges to check another part. + if !unused_bridges.is_empty() { + config_bridges = Self::build_config(Some(unused_bridges)); + continue; + } + if !default_attempt { + default_attempt = true; + // Launch client with default Webtunnel bridges if failed. + let add_bridges = TorBridge::DEFAULT_WEBTUNNEL_CONN_LINES + .iter() + .map(|b| TorBridge::Webtunnel(TorConfig::webtunnel_path(), b.to_string())) + .collect::>(); + config_bridges = Self::build_config(Some(add_bridges)); + continue; + } else if TorConfig::get_bridge().is_some() { + // Launch without bridges if all attempts failed. + let (config, _, _) = Self::build_config(None); + let client = Self::build_client_bootstrap(config.clone()); + if let Some(c) = client { + TOR_STATE.client_config.write().replace((c, config)); + } + break; + } + } + } + TOR_STATE.client_launching.store(false, Ordering::Relaxed); + } - /// Send post request using Tor. - pub async fn post(body: String, url: String) -> Option { - if let Some(proxy) = TorConfig::get_proxy() { - let req: hyper::Request> = hyper::Request::builder() - .method(hyper::Method::POST) - .uri(url) - .body(Full::from(body)) - .unwrap(); - let res = match proxy { - TorProxy::SOCKS5(url) => { - HttpClient::send_socks_proxy(url, req).await - } - TorProxy::HTTP(url) => { - HttpClient::send_http_proxy(url, req).await - } - }; - match res { - Ok(res) => { - match res.into_body().collect().await { - Ok(r) => { - let body = r.to_bytes().into(); - match String::from_utf8(body) { - Ok(r) => Some(r), - Err(e) => { - error!("Tor: POST with proxy, response to string error: {}", e); - None - } - } - } - Err(e) => { - error!("Tor: POST with proxy, response parse error: {}", e); - None - } - } - } - Err(e) => { - error!("Tor: POST failed with proxy: {}", e); - None - } - } - } else { - if let Some(b) = TorConfig::get_bridge() { - if !fs::exists(b.binary_path()).unwrap() { - error!("Tor: bridge binary not exists"); - return None; - } - } - if Self::client_config().is_none() { - error!("Tor: client not launched"); - return None; - } - // Create http tor-powered client to post data. - let client = Self::client_config().unwrap().0.isolated_client(); - let tls_conn = TlsConnector::builder().unwrap().build().unwrap(); - let conn = ArtiHttpConnector::new(client, tls_conn); - let http = hyper_tor::Client::builder().build::<_, hyper_tor::Body>(conn); - // Create request. - let req = hyper_tor::Request::builder() - .method(hyper_tor::Method::POST) - .uri(url) - .body(hyper_tor::Body::from(body)) - .unwrap(); - // Send request. - let mut resp = None; - match http.request(req).await { - Ok(r) => match hyper_tor::body::to_bytes(r).await { - Ok(raw) => resp = Some(String::from_utf8_lossy(&raw).to_string()), - Err(e) => { - error!("Tor: POST response parse error: {}", e); - } - }, - Err(e) => { - error!("Tor: POST failed: {}", e); - } - } - resp - } - } + /// Send post request using Tor. + pub async fn post(body: String, url: String) -> Option { + if let Some(proxy) = TorConfig::get_proxy() { + let req: hyper::Request> = hyper::Request::builder() + .method(hyper::Method::POST) + .uri(url) + .body(Full::from(body)) + .unwrap(); + let res = match proxy { + TorProxy::SOCKS5(url) => HttpClient::send_socks_proxy(url, req).await, + TorProxy::HTTP(url) => HttpClient::send_http_proxy(url, req).await, + }; + match res { + Ok(res) => match res.into_body().collect().await { + Ok(r) => { + let body = r.to_bytes().into(); + match String::from_utf8(body) { + Ok(r) => Some(r), + Err(e) => { + error!("Tor: POST with proxy, response to string error: {}", e); + None + } + } + } + Err(e) => { + error!("Tor: POST with proxy, response parse error: {}", e); + None + } + }, + Err(e) => { + error!("Tor: POST failed with proxy: {}", e); + None + } + } + } else { + if let Some(b) = TorConfig::get_bridge() { + if !fs::exists(b.binary_path()).unwrap() { + error!("Tor: bridge binary not exists"); + return None; + } + } + if Self::client_config().is_none() { + error!("Tor: client not launched"); + return None; + } + // Create http tor-powered client to post data. + let client = Self::client_config().unwrap().0.isolated_client(); + let tls_conn = TlsConnector::builder().unwrap().build().unwrap(); + let conn = ArtiHttpConnector::new(client, tls_conn); + let http = hyper_tor::Client::builder().build::<_, hyper_tor::Body>(conn); + // Create request. + let req = hyper_tor::Request::builder() + .method(hyper_tor::Method::POST) + .uri(url) + .body(hyper_tor::Body::from(body)) + .unwrap(); + // Send request. + let mut resp = None; + match http.request(req).await { + Ok(r) => match hyper_tor::body::to_bytes(r).await { + Ok(raw) => resp = Some(String::from_utf8_lossy(&raw).to_string()), + Err(e) => { + error!("Tor: POST response parse error: {}", e); + } + }, + Err(e) => { + error!("Tor: POST failed: {}", e); + } + } + resp + } + } - fn client_config() -> Option<(TorClient, TorClientConfig)> { - let r_client_config = TOR_STATE.client_config.read(); - r_client_config.clone() - } + fn client_config() -> Option<(TorClient, TorClientConfig)> { + let r_client_config = TOR_STATE.client_config.read(); + r_client_config.clone() + } - /// Check if Onion service is starting. - pub fn is_service_starting(id: &String) -> bool { - let r_services = TOR_STATE.start.read(); - r_services.contains_key(id) - } + /// Check if Onion service is starting. + pub fn is_service_starting(id: &String) -> bool { + let r_services = TOR_STATE.start.read(); + r_services.contains_key(id) + } - /// Check if Onion service is running. - pub fn is_service_running(id: &String) -> bool { - let r_services = TOR_STATE.run.read(); - r_services.contains_key(id) - } + /// Check if Onion service is running. + pub fn is_service_running(id: &String) -> bool { + let r_services = TOR_STATE.run.read(); + r_services.contains_key(id) + } - /// Check if Onion service failed on start. - pub fn is_service_failed(id: &String) -> bool { - let r_services = TOR_STATE.fail.read(); - r_services.contains(id) - } + /// Check if Onion service failed on start. + pub fn is_service_failed(id: &String) -> bool { + let r_services = TOR_STATE.fail.read(); + r_services.contains(id) + } - /// Check if Onion service is checking. - pub fn is_service_checking(id: &String) -> bool { - let r_services = TOR_STATE.check.read(); - r_services.contains(id) - } + /// Check if Onion service is checking. + pub fn is_service_checking(id: &String) -> bool { + let r_services = TOR_STATE.check.read(); + r_services.contains(id) + } - /// Restart Tor client at separate thread. - pub fn restart() { - thread::spawn(|| { - // Exit if client was not launched. - if Self::client_config().is_none() { - return; - } - Self::restart_services(); - }); - } + /// Restart Tor client at separate thread. + pub fn restart() { + thread::spawn(|| { + // Exit if client was not launched. + if Self::client_config().is_none() { + return; + } + Self::restart_services(); + }); + } - /// Restart running Onion services. - fn restart_services() { - // Stop all services saving keys to relaunch. - let service_ids = { - let r_services = TOR_STATE.run.read().clone(); - r_services.keys().map(|s| s.to_string()).collect::>() - }; - let mut services: BTreeMap = TOR_STATE.start.read().clone(); - for id in service_ids.clone() { - if let Some(res) = Self::stop_service(&id) { - services.insert(id, res); - } - } - // Put stopped services to start. - { - let mut w_services = TOR_STATE.start.write(); - *w_services = services.clone(); - } - // Cleanup keys, state and cache. - fs::remove_dir_all(TorConfig::keystore_path()).unwrap_or_default(); - fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default(); - fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default(); - { - let mut w_client = TOR_STATE.client_config.write(); - *w_client = None; - } - // Relaunch client. - Self::launch(); - // Save failed services if client was not created. - if Self::client_config().is_none() { - for id in service_ids { - TOR_STATE.start.write().remove(&id); - TOR_STATE.fail.write().insert(id); - } - return; - } - // Start services. - for id in services.keys() { - let (port, key) = services.get(id).unwrap(); - Self::start_service(port.clone(), key.clone(), &id); - } - } + /// Restart running Onion services. + fn restart_services() { + // Stop all services saving keys to relaunch. + let service_ids = { + let r_services = TOR_STATE.run.read().clone(); + r_services + .keys() + .map(|s| s.to_string()) + .collect::>() + }; + let mut services: BTreeMap = TOR_STATE.start.read().clone(); + for id in service_ids.clone() { + if let Some(res) = Self::stop_service(&id) { + services.insert(id, res); + } + } + // Put stopped services to start. + { + let mut w_services = TOR_STATE.start.write(); + *w_services = services.clone(); + } + // Cleanup keys, state and cache. + fs::remove_dir_all(TorConfig::keystore_path()).unwrap_or_default(); + fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default(); + fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default(); + { + let mut w_client = TOR_STATE.client_config.write(); + *w_client = None; + } + // Relaunch client. + Self::launch(); + // Save failed services if client was not created. + if Self::client_config().is_none() { + for id in service_ids { + TOR_STATE.start.write().remove(&id); + TOR_STATE.fail.write().insert(id); + } + return; + } + // Start services. + for id in services.keys() { + let (port, key) = services.get(id).unwrap(); + Self::start_service(port.clone(), key.clone(), &id); + } + } - /// Stop running Onion service returning port and key. - pub fn stop_service(id: &String) -> Option<(u16, SecretKey)> { - let mut port_key = None; - { - // Remove service from checking. - let mut w_services = TOR_STATE.check.write(); - w_services.remove(id); - } - // Remove service from starting. - { - let mut w_services = TOR_STATE.start.write(); - if let Some((port, key)) = w_services.remove(id) { - port_key = Some((port, key)); - } - } - // Remove service from running. - { - let mut w_services = TOR_STATE.run.write(); - if let Some((port, key, svc, proxy)) = w_services.remove(id) { - proxy.shutdown(); - drop(proxy); - drop(svc); - port_key = Some((port, key)); - } - } - // Remove client when no running services left. - if TOR_STATE.start.read().is_empty() && TOR_STATE.run.read().is_empty() { - let mut w_client = TOR_STATE.client_config.write(); - *w_client = None; - // Clear state. - fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default(); - } - port_key - } + /// Stop running Onion service returning port and key. + pub fn stop_service(id: &String) -> Option<(u16, SecretKey)> { + let mut port_key = None; + { + // Remove service from checking. + let mut w_services = TOR_STATE.check.write(); + w_services.remove(id); + } + // Remove service from starting. + { + let mut w_services = TOR_STATE.start.write(); + if let Some((port, key)) = w_services.remove(id) { + port_key = Some((port, key)); + } + } + // Remove service from running. + { + let mut w_services = TOR_STATE.run.write(); + if let Some((port, key, svc, proxy)) = w_services.remove(id) { + proxy.shutdown(); + drop(proxy); + drop(svc); + port_key = Some((port, key)); + } + } + // Remove client when no running services left. + if TOR_STATE.start.read().is_empty() && TOR_STATE.run.read().is_empty() { + let mut w_client = TOR_STATE.client_config.write(); + *w_client = None; + // Clear state. + fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default(); + } + port_key + } - /// Start Onion service from listening local port and [`SecretKey`]. - pub fn start_service(port: u16, key: SecretKey, id: &String) { - // Check if service is already running. - if Self::is_service_running(id) { - return; - } - let service_id = id.clone(); - thread::spawn(move || { - { - // Save starting service. - let mut w_services = TOR_STATE.start.write(); - w_services.insert(service_id.clone(), (port, key.clone())); - // Remove service from failed. - let mut w_services = TOR_STATE.fail.write(); - w_services.remove(&service_id); - } + /// Start Onion service from listening local port and [`SecretKey`]. + pub fn start_service(port: u16, key: SecretKey, id: &String) { + // Check if service is already running. + if Self::is_service_running(id) { + return; + } + let service_id = id.clone(); + thread::spawn(move || { + { + // Save starting service. + let mut w_services = TOR_STATE.start.write(); + w_services.insert(service_id.clone(), (port, key.clone())); + // Remove service from failed. + let mut w_services = TOR_STATE.fail.write(); + w_services.remove(&service_id); + } - let on_error = |service_id: String| { - // Remove service from starting. - let mut w_services = TOR_STATE.start.write(); - w_services.remove(&service_id); - // Save failed service. - let mut w_services = TOR_STATE.fail.write(); - w_services.insert(service_id); - }; + let on_error = |service_id: String| { + // Remove service from starting. + let mut w_services = TOR_STATE.start.write(); + w_services.remove(&service_id); + // Save failed service. + let mut w_services = TOR_STATE.fail.write(); + w_services.insert(service_id); + }; - // Check bridge binary existence and permissions. - if let Some(bridge) = TorConfig::get_bridge() { - if !fs::exists(bridge.binary_path()).unwrap() { - on_error(service_id); - return; - } - // Add execute permission for Unix. - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut perms = fs::metadata(bridge.binary_path()) - .unwrap() - .permissions(); - let mode = perms.mode() | 0o100; - perms.set_mode(mode); - fs::set_permissions(bridge.binary_path(), perms).unwrap_or_default(); - } - } + // Check bridge binary existence and permissions. + if let Some(bridge) = TorConfig::get_bridge() { + if !fs::exists(bridge.binary_path()).unwrap() { + on_error(service_id); + return; + } + // Add execute permission for Unix. + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(bridge.binary_path()).unwrap().permissions(); + let mode = perms.mode() | 0o100; + perms.set_mode(mode); + fs::set_permissions(bridge.binary_path(), perms).unwrap_or_default(); + } + } - // Launch client if not exists. - Self::launch(); - let client_config = Self::client_config(); - if client_config.is_none() { - on_error(service_id); - return; - } - let (client, config) = client_config.unwrap(); - client - .runtime() - .spawn(async move { - // Add service key to keystore. - let hs = HsNickname::new(service_id.clone()).unwrap(); - if let Err(_) = Self::add_service_key(config.fs_mistrust(), &key, &hs) { - on_error(service_id); - return; - } - // Launch Onion service. - let service_config = OnionServiceConfigBuilder::default() - .nickname(hs.clone()) - .build() - .unwrap(); - let client_config = Self::client_config(); - if client_config.is_none() { - on_error(service_id.clone()); - } - let c = client_config.unwrap().0.isolated_client(); - if let Ok(res) = c.launch_onion_service(service_config) { - if let Some((service, request)) = res { - // Launch service proxy. - let addr = SocketAddr::new(IpAddr::from(Ipv4Addr::LOCALHOST), port); - let run = Self::run_service_proxy(c, addr, request, hs.clone()); - let proxy = tokio::spawn(run).await.unwrap(); - let onion_addr = service.onion_address(); - // Save running service. - { - let mut w_services = TOR_STATE.run.write(); - let id = service_id.clone(); - w_services.insert(id, (port, key.clone(), service, proxy)); - } - // Remove service from starting. - { - let mut w_services = TOR_STATE.start.write(); - w_services.remove(&service_id); - } - // Check service availability. - let addr = onion_addr.unwrap().display_unredacted().to_string(); - let url = format!("http://{}/", addr); - if !Self::is_service_checking(&service_id) { - Self::check_service(service_id, url) - } - return; - } - } - on_error(service_id); - }) - .unwrap(); - }); - } + // Launch client if not exists. + Self::launch(); + let client_config = Self::client_config(); + if client_config.is_none() { + on_error(service_id); + return; + } + let (client, config) = client_config.unwrap(); + client + .runtime() + .spawn(async move { + // Add service key to keystore. + let hs = HsNickname::new(service_id.clone()).unwrap(); + if let Err(_) = Self::add_service_key(config.fs_mistrust(), &key, &hs) { + on_error(service_id); + return; + } + // Launch Onion service. + let service_config = OnionServiceConfigBuilder::default() + .nickname(hs.clone()) + .build() + .unwrap(); + let client_config = Self::client_config(); + if client_config.is_none() { + on_error(service_id.clone()); + } + let c = client_config.unwrap().0.isolated_client(); + if let Ok(res) = c.launch_onion_service(service_config) { + if let Some((service, request)) = res { + // Launch service proxy. + let addr = SocketAddr::new(IpAddr::from(Ipv4Addr::LOCALHOST), port); + let run = Self::run_service_proxy(c, addr, request, hs.clone()); + let proxy = tokio::spawn(run).await.unwrap(); + let onion_addr = service.onion_address(); + // Save running service. + { + let mut w_services = TOR_STATE.run.write(); + let id = service_id.clone(); + w_services.insert(id, (port, key.clone(), service, proxy)); + } + // Remove service from starting. + { + let mut w_services = TOR_STATE.start.write(); + w_services.remove(&service_id); + } + // Check service availability. + let addr = onion_addr.unwrap().display_unredacted().to_string(); + let url = format!("http://{}/", addr); + if !Self::is_service_checking(&service_id) { + Self::check_service(service_id, url) + } + return; + } + } + on_error(service_id); + }) + .unwrap(); + }); + } - /// Check is service is running on check. - fn check_running(service_id: &String) -> bool { - let running = Tor::is_service_running(service_id) && Self::client_config().is_some(); - if !running { - // Remove service from checking. - let mut w_services = TOR_STATE.check.write(); - w_services.remove(service_id); - } - running - } + /// Check is service is running on check. + fn check_running(service_id: &String) -> bool { + let running = Tor::is_service_running(service_id) && Self::client_config().is_some(); + if !running { + // Remove service from checking. + let mut w_services = TOR_STATE.check.write(); + w_services.remove(service_id); + } + running + } - /// Check service availability. - fn check_service(service_id: String, url: String) { - { - let mut w_services = TOR_STATE.check.write(); - w_services.insert(service_id.clone()); - } - thread::spawn(move || { - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap() - .block_on(async { - const MAX_ERRORS: i32 = 16; - let mut errors_count = 0; - // Wait 10 seconds. - tokio::time::sleep(Duration::from_millis(10000)).await; - loop { - if !Self::check_running(&service_id) { - break; - } - let duration = { - // Send request. - let tls_conn = TlsConnector::builder().unwrap().build().unwrap(); - let client_config = Self::client_config(); - if client_config.is_none() { - return; - } - let client = client_config.unwrap().0.isolated_client(); - let conn = ArtiHttpConnector::new(client, tls_conn); - let http = hyper_tor::Client::builder() - .build::<_, hyper_tor::Body>(conn); - let uri = hyper_tor::Uri::from_str(url.clone().as_str()).unwrap(); - let check = http.get(uri.clone()); - // Setup error callback. - let mut on_error = |service_id: &String| -> bool { - if !Self::check_running(service_id) { - return true; - } - // Restart service after maximum amount of errors. - errors_count += 1; - let max_errors = errors_count >= MAX_ERRORS; - if max_errors { - { - // Remove service from checking. - let mut w_services = TOR_STATE.check.write(); - w_services.remove(service_id); - } - // Restart services. - Self::restart(); - } - max_errors - }; - // Check with timeout of 30s. - match tokio::time::timeout(Duration::from_millis(30000), check).await { - Ok(resp) => { - match resp { - Ok(_) => { - if !Self::check_running(&service_id) { - break; - } - errors_count = 0; - // Check again after 60s. - Duration::from_millis(60000) - } - Err(e) => { - if on_error(&service_id) { - break; - } - error!("Tor check failed: {} for {}, errors: {}/{}", - e, service_id, errors_count, MAX_ERRORS); - // Check again after 5s. - Duration::from_millis(5000) - } - } - } - Err(_) => { - if on_error(&service_id) { - break; - } - error!("Tor check for {} timeout, errors: {}/{}", - &service_id, errors_count, MAX_ERRORS); - // Check again after 5s. - Duration::from_millis(5000) - } - } - }; - // Wait to check service again. - tokio::time::sleep(duration).await; - } - }); - }); - } + /// Check service availability. + fn check_service(service_id: String, url: String) { + { + let mut w_services = TOR_STATE.check.write(); + w_services.insert(service_id.clone()); + } + thread::spawn(move || { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + const MAX_ERRORS: i32 = 16; + let mut errors_count = 0; + // Wait 10 seconds. + tokio::time::sleep(Duration::from_millis(10000)).await; + loop { + if !Self::check_running(&service_id) { + break; + } + let duration = { + // Send request. + let tls_conn = TlsConnector::builder().unwrap().build().unwrap(); + let client_config = Self::client_config(); + if client_config.is_none() { + return; + } + let client = client_config.unwrap().0.isolated_client(); + let conn = ArtiHttpConnector::new(client, tls_conn); + let http = + hyper_tor::Client::builder().build::<_, hyper_tor::Body>(conn); + let uri = hyper_tor::Uri::from_str(url.clone().as_str()).unwrap(); + let check = http.get(uri.clone()); + // Setup error callback. + let mut on_error = |service_id: &String| -> bool { + if !Self::check_running(service_id) { + return true; + } + // Restart service after maximum amount of errors. + errors_count += 1; + let max_errors = errors_count >= MAX_ERRORS; + if max_errors { + { + // Remove service from checking. + let mut w_services = TOR_STATE.check.write(); + w_services.remove(service_id); + } + // Restart services. + Self::restart(); + } + max_errors + }; + // Check with timeout of 30s. + match tokio::time::timeout(Duration::from_millis(30000), check).await { + Ok(resp) => { + match resp { + Ok(_) => { + if !Self::check_running(&service_id) { + break; + } + errors_count = 0; + // Check again after 60s. + Duration::from_millis(60000) + } + Err(e) => { + if on_error(&service_id) { + break; + } + error!( + "Tor check failed: {} for {}, errors: {}/{}", + e, service_id, errors_count, MAX_ERRORS + ); + // Check again after 5s. + Duration::from_millis(5000) + } + } + } + Err(_) => { + if on_error(&service_id) { + break; + } + error!( + "Tor check for {} timeout, errors: {}/{}", + &service_id, errors_count, MAX_ERRORS + ); + // Check again after 5s. + Duration::from_millis(5000) + } + } + }; + // Wait to check service again. + tokio::time::sleep(duration).await; + } + }); + }); + } - /// Launch Onion service proxy. - async fn run_service_proxy( - client: TorClient, - addr: SocketAddr, - request: S, - nickname: HsNickname, - ) -> Arc where - S: futures::Stream + Unpin + Send + 'static, - { - let id = nickname.to_string(); - let runtime = client.runtime().clone(); + /// Launch Onion service proxy. + async fn run_service_proxy( + client: TorClient, + addr: SocketAddr, + request: S, + nickname: HsNickname, + ) -> Arc + where + S: futures::Stream + Unpin + Send + 'static, + { + let id = nickname.to_string(); + let runtime = client.runtime().clone(); - // Setup proxy to forward request from Tor address to local address. - let proxy_rule = ProxyRule::new( - ProxyPattern::one_port(80).unwrap(), - ProxyAction::Forward(Encapsulation::Simple, TargetAddr::Inet(addr)), - ); - let mut proxy_cfg_builder = ProxyConfigBuilder::default(); - proxy_cfg_builder.set_proxy_ports(vec![proxy_rule]); - let proxy = OnionServiceReverseProxy::new(proxy_cfg_builder.build().unwrap()); + // Setup proxy to forward request from Tor address to local address. + let proxy_rule = ProxyRule::new( + ProxyPattern::one_port(80).unwrap(), + ProxyAction::Forward(Encapsulation::Simple, TargetAddr::Inet(addr)), + ); + let mut proxy_cfg_builder = ProxyConfigBuilder::default(); + proxy_cfg_builder.set_proxy_ports(vec![proxy_rule]); + let proxy = OnionServiceReverseProxy::new(proxy_cfg_builder.build().unwrap()); - // Remove service from failed. - let mut w_services = TOR_STATE.fail.write(); - w_services.remove(&id); + // Remove service from failed. + let mut w_services = TOR_STATE.fail.write(); + w_services.remove(&id); - // Start proxy for launched service. - let p = proxy.clone(); - client - .runtime() - .spawn(async move { - match p.handle_requests(runtime, nickname.clone(), request).await { - Ok(()) => { - // Remove service from running. - let mut w_services = TOR_STATE.run.write(); - w_services.remove(&id); - } - Err(_) => { - if Self::is_service_running(&id) { - // Remove service from running. - let mut w_services = TOR_STATE.run.write(); - w_services.remove(&id); - // Save failed service. - let mut w_services = TOR_STATE.fail.write(); - w_services.insert(id); - } - } - } - }) - .unwrap(); - proxy - } + // Start proxy for launched service. + let p = proxy.clone(); + client + .runtime() + .spawn(async move { + match p.handle_requests(runtime, nickname.clone(), request).await { + Ok(()) => { + // Remove service from running. + let mut w_services = TOR_STATE.run.write(); + w_services.remove(&id); + } + Err(_) => { + if Self::is_service_running(&id) { + // Remove service from running. + let mut w_services = TOR_STATE.run.write(); + w_services.remove(&id); + // Save failed service. + let mut w_services = TOR_STATE.fail.write(); + w_services.insert(id); + } + } + } + }) + .unwrap(); + proxy + } - /// Save Onion service key to keystore. - fn add_service_key( - mistrust: &Mistrust, - key: &SecretKey, - hs_nickname: &HsNickname, - ) -> tor_keymgr::Result<()> { - let arti_store = - ArtiNativeKeystore::from_path_and_mistrust(TorConfig::keystore_path(), mistrust)?; + /// Save Onion service key to keystore. + fn add_service_key( + mistrust: &Mistrust, + key: &SecretKey, + hs_nickname: &HsNickname, + ) -> tor_keymgr::Result<()> { + let arti_store = + ArtiNativeKeystore::from_path_and_mistrust(TorConfig::keystore_path(), mistrust)?; - let key_manager = KeyMgrBuilder::default() - .primary_store(Box::new(arti_store)) - .build() - .unwrap(); + let key_manager = KeyMgrBuilder::default() + .primary_store(Box::new(arti_store)) + .build() + .unwrap(); - let expanded_sk = - ExpandedSecretKey::from_bytes(Sha512::default().chain_update(key).finalize().as_ref()); + let expanded_sk = + ExpandedSecretKey::from_bytes(Sha512::default().chain_update(key).finalize().as_ref()); - let mut sk_bytes = [0_u8; 64]; - sk_bytes[0..32].copy_from_slice(&expanded_sk.scalar.to_bytes()); - sk_bytes[32..64].copy_from_slice(&expanded_sk.hash_prefix); - let expanded_kp = ExpandedKeypair::from_secret_key_bytes(sk_bytes).unwrap(); + let mut sk_bytes = [0_u8; 64]; + sk_bytes[0..32].copy_from_slice(&expanded_sk.scalar.to_bytes()); + sk_bytes[32..64].copy_from_slice(&expanded_sk.hash_prefix); + let expanded_kp = ExpandedKeypair::from_secret_key_bytes(sk_bytes).unwrap(); - key_manager.insert( - HsIdKey::from(expanded_kp.public().clone()), - &HsIdPublicKeySpecifier::new(hs_nickname.clone()), - KeystoreSelector::Primary, - true - )?; + key_manager.insert( + HsIdKey::from(expanded_kp.public().clone()), + &HsIdPublicKeySpecifier::new(hs_nickname.clone()), + KeystoreSelector::Primary, + true, + )?; - key_manager.insert( - HsIdKeypair::from(expanded_kp), - &HsIdKeypairSpecifier::new(hs_nickname.clone()), - KeystoreSelector::Primary, - true - )?; - Ok(()) - } + key_manager.insert( + HsIdKeypair::from(expanded_kp), + &HsIdKeypairSpecifier::new(hs_nickname.clone()), + KeystoreSelector::Primary, + true, + )?; + Ok(()) + } - fn build_bridge(builder: &mut TorClientConfigBuilder, bridge: TorBridge) { - let bridge_line = format!("Bridge {}", bridge.connection_line()); - if let Ok(bridge) = bridge_line.parse() { - builder.bridges().bridges().push(bridge); - } + fn build_bridge(builder: &mut TorClientConfigBuilder, bridge: TorBridge) { + let bridge_line = format!("Bridge {}", bridge.connection_line()); + if let Ok(bridge) = bridge_line.parse() { + builder.bridges().bridges().push(bridge); + } - // Now configure bridge transport. (Requires the "pt-client" feature) - let mut transport = TransportConfigBuilder::default(); - transport - .protocols(vec![bridge.protocol_name().parse().unwrap()]) - // Specify either the name or the absolute path of pluggable transport client binary, - // this may differ from system to system. - .path(CfgPath::new(bridge.binary_path().into())) - .run_on_startup(true); - builder.bridges().transports().push(transport); - } + // Now configure bridge transport. (Requires the "pt-client" feature) + let mut transport = TransportConfigBuilder::default(); + transport + .protocols(vec![bridge.protocol_name().parse().unwrap()]) + // Specify either the name or the absolute path of pluggable transport client binary, + // this may differ from system to system. + .path(CfgPath::new(bridge.binary_path().into())) + .run_on_startup(true); + builder.bridges().transports().push(transport); + } } diff --git a/src/tor/types.rs b/src/tor/types.rs index ded58a1d..c2d644fa 100644 --- a/src/tor/types.rs +++ b/src/tor/types.rs @@ -12,155 +12,156 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::tor::TorConfig; use egui::os; use serde_derive::{Deserialize, Serialize}; -use crate::tor::TorConfig; /// Tor connection proxy type. #[derive(Serialize, Deserialize, Clone, PartialEq)] pub enum TorProxy { - /// SOCKS5 proxy URL. - SOCKS5(String), - /// HTTP proxy URL. - HTTP(String) + /// SOCKS5 proxy URL. + SOCKS5(String), + /// HTTP proxy URL. + HTTP(String), } impl TorProxy { - /// Default SOCKS5 proxy URL. - pub const DEFAULT_SOCKS5_URL: &'static str = "socks5://127.0.0.1:9050"; - /// Default HTTP proxy URL. - pub const DEFAULT_HTTP_URL: &'static str = "http://127.0.0.1:9050"; + /// Default SOCKS5 proxy URL. + pub const DEFAULT_SOCKS5_URL: &'static str = "socks5://127.0.0.1:9050"; + /// Default HTTP proxy URL. + pub const DEFAULT_HTTP_URL: &'static str = "http://127.0.0.1:9050"; - /// Get proxy URL. - pub fn url(&self) -> String { - match self { - TorProxy::SOCKS5(url) => url.into(), - TorProxy::HTTP(url) => url.into() - } - } + /// Get proxy URL. + pub fn url(&self) -> String { + match self { + TorProxy::SOCKS5(url) => url.into(), + TorProxy::HTTP(url) => url.into(), + } + } } /// Tor network bridge type. #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] pub enum TorBridge { - /// Obfs4 bridge with binary path and connection line. - Webtunnel(String, String), - /// Obfs4 bridge with binary path and connection line. - Obfs4(String, String), - /// Snowflake bridge with binary path and connection line. - Snowflake(String, String) + /// Obfs4 bridge with binary path and connection line. + Webtunnel(String, String), + /// Obfs4 bridge with binary path and connection line. + Obfs4(String, String), + /// Snowflake bridge with binary path and connection line. + Snowflake(String, String), } impl TorBridge { - /// Default Obfs4 protocol proxy client binary path. - pub const DEFAULT_OBFS4_BIN_PATH: &'static str = "/usr/bin/obfs4proxy"; - /// Default Snowflake protocol client binary path. - pub const DEFAULT_SNOWFLAKE_BIN_PATH: &'static str = "/usr/bin/snowflake-client"; + /// Default Obfs4 protocol proxy client binary path. + pub const DEFAULT_OBFS4_BIN_PATH: &'static str = "/usr/bin/obfs4proxy"; + /// Default Snowflake protocol client binary path. + pub const DEFAULT_SNOWFLAKE_BIN_PATH: &'static str = "/usr/bin/snowflake-client"; - /// Default webtunnel protocol connection line. - pub const DEFAULT_WEBTUNNEL_CONN_LINE: &'static str = "webtunnel [2001:db8:beb:5884:ffcc:bfe3:2858:b06b]:443 1E242C749707B4A68A269F0D31311CE36CDFEC28 url=https://wt.gri.mw/74Fm0lKUWWMMjZpKf6iSC0UH"; - pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_1: &'static str = "webtunnel [2001:db8:289b:84cd:4be3:77f1:1cdd:9cb1]:443 D71C8E9C2180D2F35DEBF4A39BFCA6972F076D1C sni-imitation=yandex.ru,google.com,dzen.ru,vk.com,mail.ru,ozon.ru,ya.ru,www.wildberries.ru,rutube.ru,www.avito.ru,ok.ru,vkvideo.ru url=https://streaming.the-forgotten-tales.com/gz9X1VBgl0r1Xfx3dHdNl5Tl"; - pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_2: &'static str = "webtunnel [2001:db8:dee9:5852:b4dc:7e14:21bd:c99b]:443 8ADF1761FA735FDD763781BB94A16EAB64A1CF6C url=https://app01.oneclickhost.eu/WJSgXJRlNnMStkuLZygVJ7lo"; - pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_3: &'static str = "webtunnel [2001:db8:eedb:cae7:a345:4f72:f9cc:5de0]:443 B3C81E7A0CA474270DAA4A2C8633E1CA8935C37D url=https://wordpress.far-east-investment.ru/sORes7268CEUSRD7hAWvJU5A"; - pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_4: &'static str = "webtunnel [2001:db8:945c:e0b9:7e4c:c974:ff00:d4c5]:443 91937F3EFB3BE5169788AC7C8BF07460B7E306DB url=https://kabel.entreri.de/YXbp1dNrJeOF8giAFFYWxvmf"; - pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_5: &'static str = "webtunnel [2001:db8:7d4:9e13:8c7a:7e3:1f62:d790]:443 B7E362F9079D0C908F204581EB019034023BB224 url=https://balades-et-gouts.fr/xt70R9oyJt3B1xj89UCWPdLt"; - pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_6: &'static str = "webtunnel [2001:db8:8c88:1b17:d7ae:cb68:f28e:e31c]:443 C115DAC2FE991CA25DDD43D7D4D398FEA9AA4C01 url=https://foglab.net/t9crLwo4LzFDWHdwcGf9gFrk"; - pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_7: &'static str = "webtunnel [2001:db8:8ed6:e6c9:5fc9:9f20:a373:2374]:443 1636A2EFFBAA4B162F5FF461A1663EB55C41AE11 url=https://hanoi.delivery/roQFPLtlspWT6yIKeXD6lEci"; + /// Default webtunnel protocol connection line. + pub const DEFAULT_WEBTUNNEL_CONN_LINE: &'static str = "webtunnel [2001:db8:beb:5884:ffcc:bfe3:2858:b06b]:443 1E242C749707B4A68A269F0D31311CE36CDFEC28 url=https://wt.gri.mw/74Fm0lKUWWMMjZpKf6iSC0UH"; + pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_1: &'static str = "webtunnel [2001:db8:289b:84cd:4be3:77f1:1cdd:9cb1]:443 D71C8E9C2180D2F35DEBF4A39BFCA6972F076D1C sni-imitation=yandex.ru,google.com,dzen.ru,vk.com,mail.ru,ozon.ru,ya.ru,www.wildberries.ru,rutube.ru,www.avito.ru,ok.ru,vkvideo.ru url=https://streaming.the-forgotten-tales.com/gz9X1VBgl0r1Xfx3dHdNl5Tl"; + pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_2: &'static str = "webtunnel [2001:db8:dee9:5852:b4dc:7e14:21bd:c99b]:443 8ADF1761FA735FDD763781BB94A16EAB64A1CF6C url=https://app01.oneclickhost.eu/WJSgXJRlNnMStkuLZygVJ7lo"; + pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_3: &'static str = "webtunnel [2001:db8:eedb:cae7:a345:4f72:f9cc:5de0]:443 B3C81E7A0CA474270DAA4A2C8633E1CA8935C37D url=https://wordpress.far-east-investment.ru/sORes7268CEUSRD7hAWvJU5A"; + pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_4: &'static str = "webtunnel [2001:db8:945c:e0b9:7e4c:c974:ff00:d4c5]:443 91937F3EFB3BE5169788AC7C8BF07460B7E306DB url=https://kabel.entreri.de/YXbp1dNrJeOF8giAFFYWxvmf"; + pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_5: &'static str = "webtunnel [2001:db8:7d4:9e13:8c7a:7e3:1f62:d790]:443 B7E362F9079D0C908F204581EB019034023BB224 url=https://balades-et-gouts.fr/xt70R9oyJt3B1xj89UCWPdLt"; + pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_6: &'static str = "webtunnel [2001:db8:8c88:1b17:d7ae:cb68:f28e:e31c]:443 C115DAC2FE991CA25DDD43D7D4D398FEA9AA4C01 url=https://foglab.net/t9crLwo4LzFDWHdwcGf9gFrk"; + pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_7: &'static str = "webtunnel [2001:db8:8ed6:e6c9:5fc9:9f20:a373:2374]:443 1636A2EFFBAA4B162F5FF461A1663EB55C41AE11 url=https://hanoi.delivery/roQFPLtlspWT6yIKeXD6lEci"; - pub const DEFAULT_WEBTUNNEL_CONN_LINES: [&'static str; 8] = [ - TorBridge::DEFAULT_WEBTUNNEL_CONN_LINE, - TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_1, - TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_2, - TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_3, - TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_4, - TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_5, - TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_6, - TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_7, - ]; + pub const DEFAULT_WEBTUNNEL_CONN_LINES: [&'static str; 8] = [ + TorBridge::DEFAULT_WEBTUNNEL_CONN_LINE, + TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_1, + TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_2, + TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_3, + TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_4, + TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_5, + TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_6, + TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_7, + ]; - /// Default Obfs4 protocol connection line. - pub const DEFAULT_OBFS4_CONN_LINE: &'static str = "obfs4 51.83.248.35:25981 D08B4760D128C1A65506577E063D9D26C2A71815 cert=UJWUh+sIDdOKja/byBM2+qP9AFNl86hkGRFJ/lM1GWKP79eCu3PT4WTXI2gdXYULbQ0EMg iat-mode=0"; - /// Default Snowflake protocol connection line. - pub const DEFAULT_SNOWFLAKE_CONN_LINE: &'static str = "snowflake 192.0.2.4:80 8838024498816A039FCBBAB14E6F40A0843051FA fingerprint=8838024498816A039FCBBAB14E6F40A0843051FA url=https://1098762253.rsc.cdn77.org/ fronts=www.cdn77.com,www.phpmyadmin.net ice=stun:stun.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.net:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 utls-imitate=hellorandomizedalpn"; + /// Default Obfs4 protocol connection line. + pub const DEFAULT_OBFS4_CONN_LINE: &'static str = "obfs4 51.83.248.35:25981 D08B4760D128C1A65506577E063D9D26C2A71815 cert=UJWUh+sIDdOKja/byBM2+qP9AFNl86hkGRFJ/lM1GWKP79eCu3PT4WTXI2gdXYULbQ0EMg iat-mode=0"; + /// Default Snowflake protocol connection line. + pub const DEFAULT_SNOWFLAKE_CONN_LINE: &'static str = "snowflake 192.0.2.4:80 8838024498816A039FCBBAB14E6F40A0843051FA fingerprint=8838024498816A039FCBBAB14E6F40A0843051FA url=https://1098762253.rsc.cdn77.org/ fronts=www.cdn77.com,www.phpmyadmin.net ice=stun:stun.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.net:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 utls-imitate=hellorandomizedalpn"; - /// Get bridge protocol name. - pub fn protocol_name(&self) -> String { - match *self { - TorBridge::Webtunnel(_, _) => "webtunnel".to_string(), - TorBridge::Obfs4(_, _) => "obfs4".to_string(), - TorBridge::Snowflake(_, _) => "snowflake".to_string(), - } - } + /// Get bridge protocol name. + pub fn protocol_name(&self) -> String { + match *self { + TorBridge::Webtunnel(_, _) => "webtunnel".to_string(), + TorBridge::Obfs4(_, _) => "obfs4".to_string(), + TorBridge::Snowflake(_, _) => "snowflake".to_string(), + } + } - /// Get bridge client binary path. - pub fn binary_path(&self) -> String { - let is_android = os::OperatingSystem::from_target_os() == os::OperatingSystem::Android; - match self { - TorBridge::Webtunnel(path, _) => if is_android { - TorConfig::webtunnel_path() - } else { - path.clone() - }, - TorBridge::Obfs4(path, _) => path.clone(), - TorBridge::Snowflake(path, _) => path.clone() - } - } + /// Get bridge client binary path. + pub fn binary_path(&self) -> String { + let is_android = os::OperatingSystem::from_target_os() == os::OperatingSystem::Android; + match self { + TorBridge::Webtunnel(path, _) => { + if is_android { + TorConfig::webtunnel_path() + } else { + path.clone() + } + } + TorBridge::Obfs4(path, _) => path.clone(), + TorBridge::Snowflake(path, _) => path.clone(), + } + } - /// Get bridge client binary name. - pub fn binary_name(&self) -> String { - let path = self.binary_path(); - path.split(std::path::MAIN_SEPARATOR_STR).last().unwrap().to_string() - } + /// Get bridge client binary name. + pub fn binary_name(&self) -> String { + let path = self.binary_path(); + path.split(std::path::MAIN_SEPARATOR_STR) + .last() + .unwrap() + .to_string() + } - /// Get bridge client connection line. - pub fn connection_line(&self) -> String { - match self { - TorBridge::Webtunnel(_, line) => line.clone(), - TorBridge::Obfs4(_, line) => line.clone(), - TorBridge::Snowflake(_, line) => line.clone() - } - } + /// Get bridge client connection line. + pub fn connection_line(&self) -> String { + match self { + TorBridge::Webtunnel(_, line) => line.clone(), + TorBridge::Obfs4(_, line) => line.clone(), + TorBridge::Snowflake(_, line) => line.clone(), + } + } - /// Update bridge connection line. - pub fn update_conn_line(&mut self, l: String) { - *self = match TorConfig::get_bridge().unwrap() { - TorBridge::Webtunnel(bin, _) => TorBridge::Webtunnel(bin, l.clone()), - TorBridge::Obfs4(bin, _) => TorBridge::Obfs4(bin, l.clone()), - TorBridge::Snowflake(bin, _) => TorBridge::Snowflake(bin, l.clone()), - }; - } + /// Update bridge connection line. + pub fn update_conn_line(&mut self, l: String) { + *self = match TorConfig::get_bridge().unwrap() { + TorBridge::Webtunnel(bin, _) => TorBridge::Webtunnel(bin, l.clone()), + TorBridge::Obfs4(bin, _) => TorBridge::Obfs4(bin, l.clone()), + TorBridge::Snowflake(bin, _) => TorBridge::Snowflake(bin, l.clone()), + }; + } - /// Save binary path to provided bridge. - pub fn save_bridge_bin_path(bridge: &TorBridge, path: String) { - match bridge { - TorBridge::Webtunnel(_, line) => { - TorConfig::save_bridge(Some(TorBridge::Webtunnel(path, line.into()))); - } - TorBridge::Obfs4(_, line) => { - TorConfig::save_bridge(Some(TorBridge::Obfs4(path, line.into()))); - } - TorBridge::Snowflake(_, line) => { - TorConfig::save_bridge(Some(TorBridge::Snowflake(path, line.into()))); - } - } - } + /// Save binary path to provided bridge. + pub fn save_bridge_bin_path(bridge: &TorBridge, path: String) { + match bridge { + TorBridge::Webtunnel(_, line) => { + TorConfig::save_bridge(Some(TorBridge::Webtunnel(path, line.into()))); + } + TorBridge::Obfs4(_, line) => { + TorConfig::save_bridge(Some(TorBridge::Obfs4(path, line.into()))); + } + TorBridge::Snowflake(_, line) => { + TorConfig::save_bridge(Some(TorBridge::Snowflake(path, line.into()))); + } + } + } - /// Save connection line to provided bridge. - pub fn save_bridge_conn_line(bridge: &TorBridge, line: String) { - match bridge { - TorBridge::Webtunnel(path, _) => { - TorConfig::save_bridge(Some(TorBridge::Webtunnel(path.into(), line))); - } - TorBridge::Obfs4(path, _) => { - TorConfig::save_bridge( - Some(TorBridge::Obfs4(path.into(), line)) - ); - } - TorBridge::Snowflake(path, _) => { - TorConfig::save_bridge( - Some(TorBridge::Snowflake(path.into(), line)) - ); - } - } - } -} \ No newline at end of file + /// Save connection line to provided bridge. + pub fn save_bridge_conn_line(bridge: &TorBridge, line: String) { + match bridge { + TorBridge::Webtunnel(path, _) => { + TorConfig::save_bridge(Some(TorBridge::Webtunnel(path.into(), line))); + } + TorBridge::Obfs4(path, _) => { + TorConfig::save_bridge(Some(TorBridge::Obfs4(path.into(), line))); + } + TorBridge::Snowflake(path, _) => { + TorConfig::save_bridge(Some(TorBridge::Snowflake(path.into(), line))); + } + } + } +} diff --git a/src/wallet/config.rs b/src/wallet/config.rs index 5f94baec..534afdf8 100644 --- a/src/wallet/config.rs +++ b/src/wallet/config.rs @@ -17,43 +17,43 @@ use std::path::PathBuf; use std::string::ToString; use grin_core::global::ChainTypes; -use grin_wallet_libwallet::{Slate}; +use grin_wallet_libwallet::Slate; use rand::Rng; use serde_derive::{Deserialize, Serialize}; -use crate::{AppConfig, Settings}; use crate::wallet::ConnectionsConfig; use crate::wallet::types::{ConnectionMethod, WalletTx}; +use crate::{AppConfig, Settings}; /// Wallet configuration. #[derive(Serialize, Deserialize, Clone)] pub struct WalletConfig { - /// Current wallet account label. - pub account: String, - /// Chain type for current wallet. - pub chain_type: ChainTypes, - /// Identifier for a wallet. - pub id: i64, - /// Wallet name. - pub name: String, - /// External connection identifier. - pub ext_conn_id: Option, - /// Minimal amount of confirmations. - pub min_confirmations: u64, - /// Flag to use Dandelion to broadcast transactions. - pub use_dandelion: Option, - /// Flag to enable Tor listener on start. - pub enable_tor_listener: Option, - /// Wallet API port. - pub api_port: Option, - /// Delay in blocks before another transaction broadcasting attempt. - pub tx_broadcast_timeout: Option, + /// Current wallet account label. + pub account: String, + /// Chain type for current wallet. + pub chain_type: ChainTypes, + /// Identifier for a wallet. + pub id: i64, + /// Wallet name. + pub name: String, + /// External connection identifier. + pub ext_conn_id: Option, + /// Minimal amount of confirmations. + pub min_confirmations: u64, + /// Flag to use Dandelion to broadcast transactions. + pub use_dandelion: Option, + /// Flag to enable Tor listener on start. + pub enable_tor_listener: Option, + /// Wallet API port. + pub api_port: Option, + /// Delay in blocks before another transaction broadcasting attempt. + pub tx_broadcast_timeout: Option, - /// Wallet data directory path. - pub data_path: Option, + /// Wallet data directory path. + pub data_path: Option, - /// Config version. - ver: Option + /// Config version. + ver: Option, } /// Base wallets directory name. @@ -73,215 +73,215 @@ const MIN_CONFIRMATIONS_DEFAULT: u64 = 10; const WALLET_CONFIG_VERSION: i32 = 1; impl WalletConfig { - /// Wallet data directory name. - pub const DATA_DIR_NAME: &'static str = "wallet_data"; + /// Wallet data directory name. + pub const DATA_DIR_NAME: &'static str = "wallet_data"; - /// Default account name value. - pub const DEFAULT_ACCOUNT_LABEL: &'static str = "default"; + /// Default account name value. + pub const DEFAULT_ACCOUNT_LABEL: &'static str = "default"; - /// Default value of timeout for broadcasting transaction in blocks. - pub const BROADCASTING_TIMEOUT_DEFAULT: u64 = 10; + /// Default value of timeout for broadcasting transaction in blocks. + pub const BROADCASTING_TIMEOUT_DEFAULT: u64 = 10; - /// Create new wallet config. - pub fn create(name: String, conn_method: &ConnectionMethod) -> WalletConfig { - // Setup configuration path. - let id = chrono::Utc::now().timestamp(); - let chain_type = AppConfig::chain_type(); - let config_path = Self::get_config_file_path(chain_type, id); - // Write configuration to the file. - let config = WalletConfig { - account: Self::DEFAULT_ACCOUNT_LABEL.to_string(), - chain_type, - id, - name, - ext_conn_id: match conn_method { - ConnectionMethod::Integrated => None, - ConnectionMethod::External(id, _) => Some(*id) - }, - min_confirmations: MIN_CONFIRMATIONS_DEFAULT, - use_dandelion: Some(true), - enable_tor_listener: Some(false), - api_port: Some(rand::rng().random_range(10000..30000)), - tx_broadcast_timeout: Some(Self::BROADCASTING_TIMEOUT_DEFAULT), - data_path: Some(Self::wallet_path(id.to_string())), - ver: Some(WALLET_CONFIG_VERSION), - }; - Settings::write_to_file(&config, config_path); - config - } + /// Create new wallet config. + pub fn create(name: String, conn_method: &ConnectionMethod) -> WalletConfig { + // Setup configuration path. + let id = chrono::Utc::now().timestamp(); + let chain_type = AppConfig::chain_type(); + let config_path = Self::get_config_file_path(chain_type, id); + // Write configuration to the file. + let config = WalletConfig { + account: Self::DEFAULT_ACCOUNT_LABEL.to_string(), + chain_type, + id, + name, + ext_conn_id: match conn_method { + ConnectionMethod::Integrated => None, + ConnectionMethod::External(id, _) => Some(*id), + }, + min_confirmations: MIN_CONFIRMATIONS_DEFAULT, + use_dandelion: Some(true), + enable_tor_listener: Some(false), + api_port: Some(rand::rng().random_range(10000..30000)), + tx_broadcast_timeout: Some(Self::BROADCASTING_TIMEOUT_DEFAULT), + data_path: Some(Self::wallet_path(id.to_string())), + ver: Some(WALLET_CONFIG_VERSION), + }; + Settings::write_to_file(&config, config_path); + config + } - /// Load config from provided wallet dir. - pub fn load(wallet_dir: PathBuf) -> Option { - let mut config_path: PathBuf = wallet_dir.clone(); - config_path.push(CONFIG_FILE_NAME); - if let Ok(mut config) = Settings::read_from_file::(config_path) { - config.migrate(); - return Some(config) - } - None - } + /// Load config from provided wallet dir. + pub fn load(wallet_dir: PathBuf) -> Option { + let mut config_path: PathBuf = wallet_dir.clone(); + config_path.push(CONFIG_FILE_NAME); + if let Ok(mut config) = Settings::read_from_file::(config_path) { + config.migrate(); + return Some(config); + } + None + } - /// Get wallet name by provided identifier. - pub fn read_name_by_id(id: i64) -> Option { - let mut wallet_dir = WalletConfig::get_base_path(AppConfig::chain_type()); - wallet_dir.push(id.to_string()); - if let Some(cfg) = Self::load(wallet_dir) { - return Some(cfg.name); - } - None - } + /// Get wallet name by provided identifier. + pub fn read_name_by_id(id: i64) -> Option { + let mut wallet_dir = WalletConfig::get_base_path(AppConfig::chain_type()); + wallet_dir.push(id.to_string()); + if let Some(cfg) = Self::load(wallet_dir) { + return Some(cfg.name); + } + None + } - /// Get wallet API port by provided identifier. - pub fn read_api_port_by_id(id: i64) -> Option { - let mut wallet_dir = WalletConfig::get_base_path(AppConfig::chain_type()); - wallet_dir.push(id.to_string()); - if let Some(cfg) = Self::load(wallet_dir) { - return cfg.api_port; - } - None - } + /// Get wallet API port by provided identifier. + pub fn read_api_port_by_id(id: i64) -> Option { + let mut wallet_dir = WalletConfig::get_base_path(AppConfig::chain_type()); + wallet_dir.push(id.to_string()); + if let Some(cfg) = Self::load(wallet_dir) { + return cfg.api_port; + } + None + } - /// Get wallet connection method. - pub fn connection(&self) -> ConnectionMethod { - if let Some(ext_conn_id) = self.ext_conn_id { - if let Some(conn) = ConnectionsConfig::ext_conn(ext_conn_id) { - return ConnectionMethod::External(conn.id, conn.url); - } - } - ConnectionMethod::Integrated - } + /// Get wallet connection method. + pub fn connection(&self) -> ConnectionMethod { + if let Some(ext_conn_id) = self.ext_conn_id { + if let Some(conn) = ConnectionsConfig::ext_conn(ext_conn_id) { + return ConnectionMethod::External(conn.id, conn.url); + } + } + ConnectionMethod::Integrated + } - /// Save wallet config. - pub fn save(&self) { - let config_path = Self::get_config_file_path(self.chain_type, self.id); - Settings::write_to_file(self, config_path); - } + /// Save wallet config. + pub fn save(&self) { + let config_path = Self::get_config_file_path(self.chain_type, self.id); + Settings::write_to_file(self, config_path); + } - /// Get wallets base directory path for provided [`ChainTypes`]. - pub fn get_base_path(chain_type: ChainTypes) -> PathBuf { - let sub_dir = Some(chain_type.shortname()); - let mut wallets_path = Settings::base_path(sub_dir); - wallets_path.push(BASE_DIR_NAME); - // Create wallets base directory if it doesn't exist. - if !wallets_path.exists() { - let _ = fs::create_dir_all(wallets_path.clone()); - } - wallets_path - } + /// Get wallets base directory path for provided [`ChainTypes`]. + pub fn get_base_path(chain_type: ChainTypes) -> PathBuf { + let sub_dir = Some(chain_type.shortname()); + let mut wallets_path = Settings::base_path(sub_dir); + wallets_path.push(BASE_DIR_NAME); + // Create wallets base directory if it doesn't exist. + if !wallets_path.exists() { + let _ = fs::create_dir_all(wallets_path.clone()); + } + wallets_path + } - /// Get config file path for provided [`ChainTypes`] and wallet identifier. - fn get_config_file_path(chain_type: ChainTypes, id: i64) -> PathBuf { - let mut config_path = Self::get_base_path(chain_type); - config_path.push(id.to_string()); - // Create if the config path doesn't exist. - if !config_path.exists() { - let _ = fs::create_dir_all(config_path.clone()); - } - config_path.push(CONFIG_FILE_NAME); - config_path - } + /// Get config file path for provided [`ChainTypes`] and wallet identifier. + fn get_config_file_path(chain_type: ChainTypes, id: i64) -> PathBuf { + let mut config_path = Self::get_base_path(chain_type); + config_path.push(id.to_string()); + // Create if the config path doesn't exist. + if !config_path.exists() { + let _ = fs::create_dir_all(config_path.clone()); + } + config_path.push(CONFIG_FILE_NAME); + config_path + } - /// Get wallet path from provided identifier. - fn wallet_path(id: String) -> String { - let chain_type = AppConfig::chain_type(); - let mut data_path = Self::get_base_path(chain_type); - data_path.push(id); - data_path.to_str().unwrap().to_string() - } + /// Get wallet path from provided identifier. + fn wallet_path(id: String) -> String { + let chain_type = AppConfig::chain_type(); + let mut data_path = Self::get_base_path(chain_type); + data_path.push(id); + data_path.to_str().unwrap().to_string() + } - /// Get current wallet path. - pub fn get_wallet_path(&self) -> String { - Self::wallet_path(self.id.to_string()) - } + /// Get current wallet path. + pub fn get_wallet_path(&self) -> String { + Self::wallet_path(self.id.to_string()) + } - /// Get wallet data path. - pub fn get_data_path(&self) -> String { - if let Some(path) = &self.data_path { - path.clone() - } else { - self.get_wallet_path() - } - } + /// Get wallet data path. + pub fn get_data_path(&self) -> String { + if let Some(path) = &self.data_path { + path.clone() + } else { + self.get_wallet_path() + } + } - /// Get base wallet data path. - fn get_base_data_path(&self) -> String { - let mut path = PathBuf::from(self.get_data_path()); - path.push(Self::DATA_DIR_NAME); - if !path.exists() { - let _ = fs::create_dir_all(path.clone()); - } - path.to_str().unwrap().to_string() - } + /// Get base wallet data path. + fn get_base_data_path(&self) -> String { + let mut path = PathBuf::from(self.get_data_path()); + path.push(Self::DATA_DIR_NAME); + if !path.exists() { + let _ = fs::create_dir_all(path.clone()); + } + path.to_str().unwrap().to_string() + } - /// Get wallet seed path. - pub fn seed_path(&self) -> String { - let mut path = PathBuf::from(self.get_base_data_path()); - path.push(SEED_FILE); - path.to_str().unwrap().to_string() - } + /// Get wallet seed path. + pub fn seed_path(&self) -> String { + let mut path = PathBuf::from(self.get_base_data_path()); + path.push(SEED_FILE); + path.to_str().unwrap().to_string() + } - /// Get wallet database data path. - pub fn get_db_path(&self) -> String { - let mut path = PathBuf::from(self.get_base_data_path()); - path.push(DB_DIR_NAME); - path.to_str().unwrap().to_string() - } + /// Get wallet database data path. + pub fn get_db_path(&self) -> String { + let mut path = PathBuf::from(self.get_base_data_path()); + path.push(DB_DIR_NAME); + path.to_str().unwrap().to_string() + } - /// Get Slatepack file path for transaction. - pub fn get_tx_slate_path(&self, tx: &WalletTx) -> PathBuf { - let mut path = PathBuf::from(self.get_base_data_path()); - path.push(SLATEPACKS_DIR_NAME); - if !path.exists() { - let _ = fs::create_dir_all(path.clone()); - } - let file = format!("{}.{}.slatepack", tx.data.tx_slate_id.unwrap(), tx.state); - path.push(file); - path - } + /// Get Slatepack file path for transaction. + pub fn get_tx_slate_path(&self, tx: &WalletTx) -> PathBuf { + let mut path = PathBuf::from(self.get_base_data_path()); + path.push(SLATEPACKS_DIR_NAME); + if !path.exists() { + let _ = fs::create_dir_all(path.clone()); + } + let file = format!("{}.{}.slatepack", tx.data.tx_slate_id.unwrap(), tx.state); + path.push(file); + path + } - /// Get Slatepack file path for Slate. - pub fn get_slate_path(&self, slate: &Slate) -> PathBuf { - let mut path = PathBuf::from(self.get_base_data_path()); - path.push(SLATEPACKS_DIR_NAME); - if !path.exists() { - let _ = fs::create_dir_all(path.clone()); - } - let file = format!("{}.{}.slatepack", slate.id, slate.state); - path.push(file); - path - } + /// Get Slatepack file path for Slate. + pub fn get_slate_path(&self, slate: &Slate) -> PathBuf { + let mut path = PathBuf::from(self.get_base_data_path()); + path.push(SLATEPACKS_DIR_NAME); + if !path.exists() { + let _ = fs::create_dir_all(path.clone()); + } + let file = format!("{}.{}.slatepack", slate.id, slate.state); + path.push(file); + path + } - /// Get path to extra db storage. - pub fn get_extra_db_path(&self) -> PathBuf { - let mut path = PathBuf::from(self.get_db_path()); - path.push("extra"); - if !path.exists() { - let _ = fs::create_dir_all(path.clone()); - } - path - } + /// Get path to extra db storage. + pub fn get_extra_db_path(&self) -> PathBuf { + let mut path = PathBuf::from(self.get_db_path()); + path.push("extra"); + if !path.exists() { + let _ = fs::create_dir_all(path.clone()); + } + path + } - /// Check config version to migrate if needed. - fn migrate(&mut self) { - match self.ver { - None => { - // Migrate Slatepack data. - let mut old_slate_path = PathBuf::from(self.get_wallet_path()); - old_slate_path.push(SLATEPACKS_DIR_NAME); - if old_slate_path.exists() { - let mut new_slate_path = PathBuf::from(self.get_data_path()); - new_slate_path.push(SLATEPACKS_DIR_NAME); - let _ = fs::rename(&old_slate_path, &new_slate_path); - } - // Write data path to config. - if self.data_path.is_none() { - self.data_path = Some(self.get_wallet_path()); - } - // Migrate to 1st version. - self.ver = Some(1); - } - Some(_) => {} - } - self.save(); - } -} \ No newline at end of file + /// Check config version to migrate if needed. + fn migrate(&mut self) { + match self.ver { + None => { + // Migrate Slatepack data. + let mut old_slate_path = PathBuf::from(self.get_wallet_path()); + old_slate_path.push(SLATEPACKS_DIR_NAME); + if old_slate_path.exists() { + let mut new_slate_path = PathBuf::from(self.get_data_path()); + new_slate_path.push(SLATEPACKS_DIR_NAME); + let _ = fs::rename(&old_slate_path, &new_slate_path); + } + // Write data path to config. + if self.data_path.is_none() { + self.data_path = Some(self.get_wallet_path()); + } + // Migrate to 1st version. + self.ver = Some(1); + } + Some(_) => {} + } + self.save(); + } +} diff --git a/src/wallet/connections/config.rs b/src/wallet/connections/config.rs index dcab4b42..9e3abf14 100644 --- a/src/wallet/connections/config.rs +++ b/src/wallet/connections/config.rs @@ -15,92 +15,99 @@ use grin_core::global::ChainTypes; use serde_derive::{Deserialize, Serialize}; -use crate::{AppConfig, Settings}; use crate::wallet::ExternalConnection; +use crate::{AppConfig, Settings}; /// Wallet connections configuration. #[derive(Serialize, Deserialize, Clone)] pub struct ConnectionsConfig { - /// Network type for connections. - chain_type: ChainTypes, - /// URLs of external connections for wallets. - external: Vec + /// Network type for connections. + chain_type: ChainTypes, + /// URLs of external connections for wallets. + external: Vec, } impl ConnectionsConfig { - /// Wallet connections configuration file name. - pub const FILE_NAME: &'static str = "connections.toml"; + /// Wallet connections configuration file name. + pub const FILE_NAME: &'static str = "connections.toml"; - /// Initialize configuration for provided [`ChainTypes`]. - pub fn for_chain_type(chain_type: &ChainTypes) -> Self { - let path = Settings::config_path(Self::FILE_NAME, Some(chain_type.shortname())); - let parsed = Settings::read_from_file::(path.clone()); - if !path.exists() || parsed.is_err() { - let default_config = ConnectionsConfig { - chain_type: *chain_type, - external: ExternalConnection::default(chain_type), - }; - Settings::write_to_file(&default_config, path); - default_config - } else { - parsed.unwrap() - } - } + /// Initialize configuration for provided [`ChainTypes`]. + pub fn for_chain_type(chain_type: &ChainTypes) -> Self { + let path = Settings::config_path(Self::FILE_NAME, Some(chain_type.shortname())); + let parsed = Settings::read_from_file::(path.clone()); + if !path.exists() || parsed.is_err() { + let default_config = ConnectionsConfig { + chain_type: *chain_type, + external: ExternalConnection::default(chain_type), + }; + Settings::write_to_file(&default_config, path); + default_config + } else { + parsed.unwrap() + } + } - /// Save connections configuration. - pub fn save(&mut self) { - let config = self.clone(); - let sub_dir = Some(AppConfig::chain_type().shortname()); - Settings::write_to_file(&config, Settings::config_path(Self::FILE_NAME, sub_dir)); - } + /// Save connections configuration. + pub fn save(&mut self) { + let config = self.clone(); + let sub_dir = Some(AppConfig::chain_type().shortname()); + Settings::write_to_file(&config, Settings::config_path(Self::FILE_NAME, sub_dir)); + } - /// Get [`ExternalConnection`] list. - pub fn ext_conn_list() -> Vec { - let r_config = Settings::conn_config_to_read(); - r_config.external.clone() - } + /// Get [`ExternalConnection`] list. + pub fn ext_conn_list() -> Vec { + let r_config = Settings::conn_config_to_read(); + r_config.external.clone() + } - /// Save [`ExternalConnection`] in configuration. - pub fn add_ext_conn(conn: ExternalConnection) { - let mut w_config = Settings::conn_config_to_update(); - if let Some(pos) = w_config.external.iter().position(|c| { - c.id == conn.id || c.url == conn.url - }) { - w_config.external.remove(pos); - w_config.external.insert(pos, conn); - } else { - w_config.external.push(conn); - } - w_config.save(); - } + /// Save [`ExternalConnection`] in configuration. + pub fn add_ext_conn(conn: ExternalConnection) { + let mut w_config = Settings::conn_config_to_update(); + if let Some(pos) = w_config + .external + .iter() + .position(|c| c.id == conn.id || c.url == conn.url) + { + w_config.external.remove(pos); + w_config.external.insert(pos, conn); + } else { + w_config.external.push(conn); + } + w_config.save(); + } - /// Get external node connection with provided identifier. - pub fn ext_conn(id: i64) -> Option { - let r_config = Settings::conn_config_to_read(); - for c in &r_config.external { - if c.id == id { - return Some(c.clone()); - } - } - None - } + /// Get external node connection with provided identifier. + pub fn ext_conn(id: i64) -> Option { + let r_config = Settings::conn_config_to_read(); + for c in &r_config.external { + if c.id == id { + return Some(c.clone()); + } + } + None + } - /// Set [`ExternalConnection`] availability flag. - pub fn update_ext_conn_status(id: i64, available: Option) { - let mut w_config = Settings::conn_config_to_update(); - for c in w_config.external.iter_mut() { - if c.id == id { - c.available = available; - w_config.save(); - break; - } - } - } + /// Set [`ExternalConnection`] availability flag. + pub fn update_ext_conn_status(id: i64, available: Option) { + let mut w_config = Settings::conn_config_to_update(); + for c in w_config.external.iter_mut() { + if c.id == id { + c.available = available; + w_config.save(); + break; + } + } + } - /// Remove [`ExternalConnection`] with provided identifier. - pub fn remove_ext_conn(id: i64) { - let mut w_config = Settings::conn_config_to_update(); - w_config.external = w_config.external.iter().filter(|c| c.id != id).cloned().collect(); - w_config.save(); - } -} \ No newline at end of file + /// Remove [`ExternalConnection`] with provided identifier. + pub fn remove_ext_conn(id: i64) { + let mut w_config = Settings::conn_config_to_update(); + w_config.external = w_config + .external + .iter() + .filter(|c| c.id != id) + .cloned() + .collect(); + w_config.save(); + } +} diff --git a/src/wallet/connections/external.rs b/src/wallet/connections/external.rs index 7775704a..ce37907b 100644 --- a/src/wallet/connections/external.rs +++ b/src/wallet/connections/external.rs @@ -24,146 +24,147 @@ use crate::wallet::ConnectionsConfig; /// External connection for the wallet. #[derive(Serialize, Deserialize, Clone, PartialEq)] pub struct ExternalConnection { - /// Connection identifier. - pub id: i64, - /// Node URL. - pub url: String, - /// Optional username. - pub username: Option, - /// Optional API secret key. - pub secret: Option, + /// Connection identifier. + pub id: i64, + /// Node URL. + pub url: String, + /// Optional username. + pub username: Option, + /// Optional API secret key. + pub secret: Option, - /// Flag to check if server is available. - #[serde(skip_serializing, skip_deserializing)] - pub available: Option, + /// Flag to check if server is available. + #[serde(skip_serializing, skip_deserializing)] + pub available: Option, } /// Default external node URL for main network. const DEFAULT_MAIN_URLS: [&'static str; 3] = [ - "https://main.gri.mw", - "https://grincoin.org", - "https://mainnet.grinffindor.org" + "https://main.gri.mw", + "https://grincoin.org", + "https://mainnet.grinffindor.org", ]; /// Default external node URL for main network. const DEFAULT_TEST_URLS: [&'static str; 3] = [ - "https://test.gri.mw", - "https://testnet.grincoin.org", - "https://testnet.grinffindor.org" + "https://test.gri.mw", + "https://testnet.grincoin.org", + "https://testnet.grinffindor.org", ]; impl ExternalConnection { - /// Get default connections for provided chain type. - pub fn default(chain_type: &ChainTypes) -> Vec { - let urls = match chain_type { - ChainTypes::Mainnet => DEFAULT_MAIN_URLS.to_vec(), - _ => DEFAULT_TEST_URLS.to_vec() - }; - urls.iter().enumerate().map(|(index, url)| { - ExternalConnection { - id: index as i64, - url: url.to_string(), - username: Some("grin".to_string()), - secret: None, - available: None, - } - }).collect::>() - } + /// Get default connections for provided chain type. + pub fn default(chain_type: &ChainTypes) -> Vec { + let urls = match chain_type { + ChainTypes::Mainnet => DEFAULT_MAIN_URLS.to_vec(), + _ => DEFAULT_TEST_URLS.to_vec(), + }; + urls.iter() + .enumerate() + .map(|(index, url)| ExternalConnection { + id: index as i64, + url: url.to_string(), + username: Some("grin".to_string()), + secret: None, + available: None, + }) + .collect::>() + } - /// Create new external connection. - pub fn new(url: String, username: Option, secret: Option) -> Self { - let id = chrono::Utc::now().timestamp(); - Self { - id, - url, - username, - secret, - available: None, - } - } + /// Create new external connection. + pub fn new(url: String, username: Option, secret: Option) -> Self { + let id = chrono::Utc::now().timestamp(); + Self { + id, + url, + username, + secret, + available: None, + } + } - /// Check external connection availability. - pub fn check(id: Option, ui_ctx: &egui::Context) { - let conn_list = ConnectionsConfig::ext_conn_list(); - for conn in conn_list { - if let Some(id) = id { - if id == conn.id { - check_ext_conn(&conn, ui_ctx); - } - } else { - check_ext_conn(&conn, ui_ctx); - } - } - } + /// Check external connection availability. + pub fn check(id: Option, ui_ctx: &egui::Context) { + let conn_list = ConnectionsConfig::ext_conn_list(); + for conn in conn_list { + if let Some(id) = id { + if id == conn.id { + check_ext_conn(&conn, ui_ctx); + } + } else { + check_ext_conn(&conn, ui_ctx); + } + } + } } /// Check connection availability. fn check_ext_conn(conn: &ExternalConnection, ui_ctx: &egui::Context) { - let conn = conn.clone(); - let ui_ctx = ui_ctx.clone(); - ConnectionsConfig::update_ext_conn_status(conn.id, None); - std::thread::spawn(move || { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(async { - // Check URL format. - let conn_url = url::Url::parse(conn.url.as_str()); - let url_res = match conn_url { - Ok(url) => Some(url), - Err(_) => { - let mut url_res = None; - if !conn.url.starts_with("http") { - let mut conn = conn.clone(); - let url_text = format!("https://{}", conn.url); - let url = url::Url::parse(url_text.as_str()); - if let Ok(url) = url { - url_res = Some(url); - conn.url = url_text; - ConnectionsConfig::add_ext_conn(conn.clone()); - } - } - url_res - } - }; - if url_res.is_none() { - ConnectionsConfig::remove_ext_conn(conn.id); - return; - } - let url = url_res.unwrap(); - if let Ok(_) = url.socket_addrs(|| None) { - let addr = format!("{}v2/foreign", url.to_string()); - let mut req_setup = hyper::Request::builder() - .method(hyper::Method::POST) - .uri(addr.clone()); - // Setup secret key auth. - if let Some(key) = conn.secret { - let username = conn.username.unwrap_or("grin".to_string()); - let basic_auth = format!( - "Basic {}", - to_base64(&format!("{}:{}", username, key)) - ); - req_setup = req_setup - .header(hyper::header::AUTHORIZATION, basic_auth.clone()); - } - let req: hyper::Request> = req_setup.body(Full::from( - r#"{"id":1,"jsonrpc":"2.0","method":"get_version","params":{} }"#) - ).unwrap(); - // Send request. - match HttpClient::send(req).await { - Ok(res) => { - let status = res.status().as_u16(); - // Available on 200 HTTP status code. - ConnectionsConfig::update_ext_conn_status(conn.id, Some(status == 200)); - } - Err(_) => ConnectionsConfig::update_ext_conn_status(conn.id, Some(false)) - } - } else { - ConnectionsConfig::update_ext_conn_status(conn.id, Some(false)); - } - // Repaint ui on change. - ui_ctx.request_repaint(); - }); - }); -} \ No newline at end of file + let conn = conn.clone(); + let ui_ctx = ui_ctx.clone(); + ConnectionsConfig::update_ext_conn_status(conn.id, None); + std::thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + // Check URL format. + let conn_url = url::Url::parse(conn.url.as_str()); + let url_res = match conn_url { + Ok(url) => Some(url), + Err(_) => { + let mut url_res = None; + if !conn.url.starts_with("http") { + let mut conn = conn.clone(); + let url_text = format!("https://{}", conn.url); + let url = url::Url::parse(url_text.as_str()); + if let Ok(url) = url { + url_res = Some(url); + conn.url = url_text; + ConnectionsConfig::add_ext_conn(conn.clone()); + } + } + url_res + } + }; + if url_res.is_none() { + ConnectionsConfig::remove_ext_conn(conn.id); + return; + } + let url = url_res.unwrap(); + if let Ok(_) = url.socket_addrs(|| None) { + let addr = format!("{}v2/foreign", url.to_string()); + let mut req_setup = hyper::Request::builder() + .method(hyper::Method::POST) + .uri(addr.clone()); + // Setup secret key auth. + if let Some(key) = conn.secret { + let username = conn.username.unwrap_or("grin".to_string()); + let basic_auth = + format!("Basic {}", to_base64(&format!("{}:{}", username, key))); + req_setup = + req_setup.header(hyper::header::AUTHORIZATION, basic_auth.clone()); + } + let req: hyper::Request> = req_setup + .body(Full::from( + r#"{"id":1,"jsonrpc":"2.0","method":"get_version","params":{} }"#, + )) + .unwrap(); + // Send request. + match HttpClient::send(req).await { + Ok(res) => { + let status = res.status().as_u16(); + // Available on 200 HTTP status code. + ConnectionsConfig::update_ext_conn_status(conn.id, Some(status == 200)); + } + Err(_) => ConnectionsConfig::update_ext_conn_status(conn.id, Some(false)), + } + } else { + ConnectionsConfig::update_ext_conn_status(conn.id, Some(false)); + } + // Repaint ui on change. + ui_ctx.request_repaint(); + }); + }); +} diff --git a/src/wallet/connections/mod.rs b/src/wallet/connections/mod.rs index b4e5ca1a..2f68f822 100644 --- a/src/wallet/connections/mod.rs +++ b/src/wallet/connections/mod.rs @@ -16,4 +16,4 @@ mod external; pub use external::ExternalConnection; mod config; -pub use config::ConnectionsConfig; \ No newline at end of file +pub use config::ConnectionsConfig; diff --git a/src/wallet/list.rs b/src/wallet/list.rs index 44872e54..85c8e727 100644 --- a/src/wallet/list.rs +++ b/src/wallet/list.rs @@ -20,106 +20,106 @@ use crate::wallet::{Wallet, WalletConfig}; /// [`Wallet`] list container. #[derive(Clone)] pub struct WalletList { - /// List of wallets for [`ChainTypes::Mainnet`]. - pub main_list: Vec, - /// List of wallets for [`ChainTypes::Testnet`]. - pub test_list: Vec, + /// List of wallets for [`ChainTypes::Mainnet`]. + pub main_list: Vec, + /// List of wallets for [`ChainTypes::Testnet`]. + pub test_list: Vec, - /// Selected wallet id. - selected: Option, + /// Selected wallet id. + selected: Option, } impl Default for WalletList { - fn default() -> Self { - let (main_list, test_list) = Self::init(); - Self { - main_list, - test_list, - selected: None - } - } + fn default() -> Self { + let (main_list, test_list) = Self::init(); + Self { + main_list, + test_list, + selected: None, + } + } } impl WalletList { - /// Initialize [`Wallet`] lists for [`ChainTypes::Mainnet`] and [`ChainTypes::Testnet`]. - fn init() -> (Vec, Vec) { - let mut main_wallets = Vec::new(); - let mut test_wallets = Vec::new(); - let chain_types = vec![ChainTypes::Mainnet, ChainTypes::Testnet]; - for chain in chain_types { - // initialize wallets from base directory. - let wallets_dir = WalletConfig::get_base_path(chain); - for dir in wallets_dir.read_dir().unwrap() { - let wallet_dir = dir.unwrap().path(); - if wallet_dir.is_dir() { - let wallet = Wallet::init(wallet_dir); - if let Some(w) = wallet { - if chain == ChainTypes::Testnet { - test_wallets.push(w); - } else if chain == ChainTypes::Mainnet { - main_wallets.push(w); - } - } - } - } - } - // Sort wallets by id. - main_wallets.sort_by_key(|w| -w.get_config().id); - test_wallets.sort_by_key(|w| -w.get_config().id); - (main_wallets, test_wallets) - } + /// Initialize [`Wallet`] lists for [`ChainTypes::Mainnet`] and [`ChainTypes::Testnet`]. + fn init() -> (Vec, Vec) { + let mut main_wallets = Vec::new(); + let mut test_wallets = Vec::new(); + let chain_types = vec![ChainTypes::Mainnet, ChainTypes::Testnet]; + for chain in chain_types { + // initialize wallets from base directory. + let wallets_dir = WalletConfig::get_base_path(chain); + for dir in wallets_dir.read_dir().unwrap() { + let wallet_dir = dir.unwrap().path(); + if wallet_dir.is_dir() { + let wallet = Wallet::init(wallet_dir); + if let Some(w) = wallet { + if chain == ChainTypes::Testnet { + test_wallets.push(w); + } else if chain == ChainTypes::Mainnet { + main_wallets.push(w); + } + } + } + } + } + // Sort wallets by id. + main_wallets.sort_by_key(|w| -w.get_config().id); + test_wallets.sort_by_key(|w| -w.get_config().id); + (main_wallets, test_wallets) + } - /// Get [`Wallet`] list for current [`ChainTypes`]. - pub fn list(&self) -> &Vec { - if AppConfig::chain_type() == ChainTypes::Mainnet { - &self.main_list - } else { - &self.test_list - } - } + /// Get [`Wallet`] list for current [`ChainTypes`]. + pub fn list(&self) -> &Vec { + if AppConfig::chain_type() == ChainTypes::Mainnet { + &self.main_list + } else { + &self.test_list + } + } - /// Get mutable [`Wallet`] list for current [`ChainTypes`]. - pub fn mut_list(&mut self) -> &mut Vec { - if AppConfig::chain_type() == ChainTypes::Mainnet { - &mut self.main_list - } else { - &mut self.test_list - } - } + /// Get mutable [`Wallet`] list for current [`ChainTypes`]. + pub fn mut_list(&mut self) -> &mut Vec { + if AppConfig::chain_type() == ChainTypes::Mainnet { + &mut self.main_list + } else { + &mut self.test_list + } + } - /// Add created [`Wallet`] to the list. - pub fn add(&mut self, wallet: Wallet) { - let list = self.mut_list(); - list.insert(0, wallet); - } + /// Add created [`Wallet`] to the list. + pub fn add(&mut self, wallet: Wallet) { + let list = self.mut_list(); + list.insert(0, wallet); + } - /// Remove [`Wallet`] with provided identifier. - pub fn remove(&mut self, id: i64) { - let list = self.mut_list(); - for (index, wallet) in list.iter().enumerate() { - if wallet.get_config().id == id { - list.remove(index); - return; - } - } - } + /// Remove [`Wallet`] with provided identifier. + pub fn remove(&mut self, id: i64) { + let list = self.mut_list(); + for (index, wallet) in list.iter().enumerate() { + if wallet.get_config().id == id { + list.remove(index); + return; + } + } + } - /// Select wallet. - pub fn select(&mut self, id: Option) { - self.selected = id; - } + /// Select wallet. + pub fn select(&mut self, id: Option) { + self.selected = id; + } - /// Get selected wallet. - pub fn selected(&self) -> Box> { - if self.selected.is_none() { - return Box::new(None); - } - let list = self.list(); - for wallet in list { - if wallet.get_config().id == self.selected.unwrap() { - return Box::new(Some(wallet)); - } - } - Box::new(None) - } -} \ No newline at end of file + /// Get selected wallet. + pub fn selected(&self) -> Box> { + if self.selected.is_none() { + return Box::new(None); + } + let list = self.list(); + for wallet in list { + if wallet.get_config().id == self.selected.unwrap() { + return Box::new(Some(wallet)); + } + } + Box::new(None) + } +} diff --git a/src/wallet/mnemonic.rs b/src/wallet/mnemonic.rs index 2986dc42..b7db5072 100644 --- a/src/wallet/mnemonic.rs +++ b/src/wallet/mnemonic.rs @@ -20,221 +20,237 @@ use crate::wallet::types::{PhraseMode, PhraseSize, PhraseWord}; /// Mnemonic phrase container. pub struct Mnemonic { - /// Phrase setup mode. - mode: PhraseMode, - /// Size of phrase based on words count. - size: PhraseSize, - /// Generated words. - words: Vec, - /// Words to confirm the phrase. - confirmation: Vec, - /// Flag to check if entered phrase if valid. - valid: bool, + /// Phrase setup mode. + mode: PhraseMode, + /// Size of phrase based on words count. + size: PhraseSize, + /// Generated words. + words: Vec, + /// Words to confirm the phrase. + confirmation: Vec, + /// Flag to check if entered phrase if valid. + valid: bool, } impl Default for Mnemonic { - fn default() -> Self { - let size = PhraseSize::Words24; - let mode = PhraseMode::Generate; - let words = Self::generate_words(&mode, &size); - let confirmation = Self::empty_words(&size); - Self { mode, size, words, confirmation, valid: true } - } + fn default() -> Self { + let size = PhraseSize::Words24; + let mode = PhraseMode::Generate; + let words = Self::generate_words(&mode, &size); + let confirmation = Self::empty_words(&size); + Self { + mode, + size, + words, + confirmation, + valid: true, + } + } } impl Mnemonic { - /// Generate words based on provided [`PhraseMode`]. - pub fn set_mode(&mut self, mode: PhraseMode) { - self.mode = mode; - self.words = Self::generate_words(&self.mode, &self.size); - self.confirmation = Self::empty_words(&self.size); - self.valid = true; - } + /// Generate words based on provided [`PhraseMode`]. + pub fn set_mode(&mut self, mode: PhraseMode) { + self.mode = mode; + self.words = Self::generate_words(&self.mode, &self.size); + self.confirmation = Self::empty_words(&self.size); + self.valid = true; + } - /// Get current phrase mode. - pub fn mode(&self) -> PhraseMode { - self.mode.clone() - } + /// Get current phrase mode. + pub fn mode(&self) -> PhraseMode { + self.mode.clone() + } - /// Generate words based on provided [`PhraseSize`]. - pub fn set_size(&mut self, size: PhraseSize) { - self.size = size; - self.words = Self::generate_words(&self.mode, &self.size); - self.confirmation = Self::empty_words(&self.size); - self.valid = true; - } + /// Generate words based on provided [`PhraseSize`]. + pub fn set_size(&mut self, size: PhraseSize) { + self.size = size; + self.words = Self::generate_words(&self.mode, &self.size); + self.confirmation = Self::empty_words(&self.size); + self.valid = true; + } - /// Get current phrase size. - pub fn size(&self) -> PhraseSize { - self.size.clone() - } + /// Get current phrase size. + pub fn size(&self) -> PhraseSize { + self.size.clone() + } - /// Get words based on current [`PhraseMode`]. - pub fn words(&self, edit: bool) -> Vec { - match self.mode { - PhraseMode::Generate => { - if edit { - &self.confirmation - } else { - &self.words - } - } - PhraseMode::Import => &self.words - }.clone() - } + /// Get words based on current [`PhraseMode`]. + pub fn words(&self, edit: bool) -> Vec { + match self.mode { + PhraseMode::Generate => { + if edit { + &self.confirmation + } else { + &self.words + } + } + PhraseMode::Import => &self.words, + } + .clone() + } - /// Check if current phrase is valid. - pub fn valid(&self) -> bool { - self.valid - } + /// Check if current phrase is valid. + pub fn valid(&self) -> bool { + self.valid + } - /// Get phrase from words. - pub fn get_phrase(&self) -> String { - self.words.iter() - .enumerate() - .map(|(i, x)| if i == 0 { "" } else { " " }.to_owned() + &x.text) - .collect::() - } + /// Get phrase from words. + pub fn get_phrase(&self) -> String { + self.words + .iter() + .enumerate() + .map(|(i, x)| if i == 0 { "" } else { " " }.to_owned() + &x.text) + .collect::() + } - /// Generate [`PhraseWord`] list based on provided [`PhraseMode`] and [`PhraseSize`]. - fn generate_words(mode: &PhraseMode, size: &PhraseSize) -> Vec { - match mode { - PhraseMode::Generate => { - let mut rng = rand::rng(); - let mut entropy: Vec = Vec::with_capacity(size.entropy_size()); - for _ in 0..size.entropy_size() { - entropy.push(rng.random()); - } - from_entropy(&entropy).unwrap() - .split(" ") - .map(|s| { - let text = s.to_string(); - PhraseWord { - text, - valid: true, - } - }) - .collect::>() - }, - PhraseMode::Import => { - Self::empty_words(size) - } - } - } + /// Generate [`PhraseWord`] list based on provided [`PhraseMode`] and [`PhraseSize`]. + fn generate_words(mode: &PhraseMode, size: &PhraseSize) -> Vec { + match mode { + PhraseMode::Generate => { + let mut rng = rand::rng(); + let mut entropy: Vec = Vec::with_capacity(size.entropy_size()); + for _ in 0..size.entropy_size() { + entropy.push(rng.random()); + } + from_entropy(&entropy) + .unwrap() + .split(" ") + .map(|s| { + let text = s.to_string(); + PhraseWord { text, valid: true } + }) + .collect::>() + } + PhraseMode::Import => Self::empty_words(size), + } + } - /// Generate empty list of words based on provided [`PhraseSize`]. - fn empty_words(size: &PhraseSize) -> Vec { - let mut words = Vec::with_capacity(size.value()); - for _ in 0..size.value() { - words.push(PhraseWord { - text: "".to_string(), - valid: true, - }); - } - words - } + /// Generate empty list of words based on provided [`PhraseSize`]. + fn empty_words(size: &PhraseSize) -> Vec { + let mut words = Vec::with_capacity(size.value()); + for _ in 0..size.value() { + words.push(PhraseWord { + text: "".to_string(), + valid: true, + }); + } + words + } - /// Insert word into provided index and return validation result. - pub fn insert(&mut self, index: usize, word: &String) -> bool { - // Check if word is valid. - let found = search(word).is_ok(); - if !found { - return false; - } - let is_confirmation = self.mode == PhraseMode::Generate; - if is_confirmation { - let w = self.words.get(index).unwrap(); - if word != &w.text { - return false; - } - } + /// Insert word into provided index and return validation result. + pub fn insert(&mut self, index: usize, word: &String) -> bool { + // Check if word is valid. + let found = search(word).is_ok(); + if !found { + return false; + } + let is_confirmation = self.mode == PhraseMode::Generate; + if is_confirmation { + let w = self.words.get(index).unwrap(); + if word != &w.text { + return false; + } + } - // Save valid word at list. - let words = if is_confirmation { - &mut self.confirmation - } else { - &mut self.words - }; - words.remove(index); - words.insert(index, PhraseWord { text: word.to_owned(), valid: true }); + // Save valid word at list. + let words = if is_confirmation { + &mut self.confirmation + } else { + &mut self.words + }; + words.remove(index); + words.insert( + index, + PhraseWord { + text: word.to_owned(), + valid: true, + }, + ); - // Validate phrase when all words are entered. - let mut has_empty = false; - let _: Vec<_> = words.iter().map(|w| { - if w.text.is_empty() { - has_empty = true; - } - }).collect(); - if !has_empty { - self.valid = to_entropy(self.get_phrase().as_str()).is_ok(); - } - true - } + // Validate phrase when all words are entered. + let mut has_empty = false; + let _: Vec<_> = words + .iter() + .map(|w| { + if w.text.is_empty() { + has_empty = true; + } + }) + .collect(); + if !has_empty { + self.valid = to_entropy(self.get_phrase().as_str()).is_ok(); + } + true + } - /// Get word from provided index. - pub fn get(&self, index: usize) -> Option { - let words = match self.mode { - PhraseMode::Generate => &self.confirmation, - PhraseMode::Import => &self.words - }; - let word = words.get(index); - if let Some(w) = word { - return Some(PhraseWord { - text: w.text.clone(), - valid: w.valid - }); - } - None - } + /// Get word from provided index. + pub fn get(&self, index: usize) -> Option { + let words = match self.mode { + PhraseMode::Generate => &self.confirmation, + PhraseMode::Import => &self.words, + }; + let word = words.get(index); + if let Some(w) = word { + return Some(PhraseWord { + text: w.text.clone(), + valid: w.valid, + }); + } + None + } - /// Setup phrase from provided text if possible. - pub fn import(&mut self, text: &ZeroingString) { - let words_split = text.trim().split(" "); - let count = words_split.clone().count(); - if let Some(size) = PhraseSize::type_for_value(count) { - // Setup phrase size. - let confirm = self.mode == PhraseMode::Generate; - if !confirm { - self.words = Self::empty_words(&size); - self.size = size; - } else if self.size != size { - return; - } + /// Setup phrase from provided text if possible. + pub fn import(&mut self, text: &ZeroingString) { + let words_split = text.trim().split(" "); + let count = words_split.clone().count(); + if let Some(size) = PhraseSize::type_for_value(count) { + // Setup phrase size. + let confirm = self.mode == PhraseMode::Generate; + if !confirm { + self.words = Self::empty_words(&size); + self.size = size; + } else if self.size != size { + return; + } - // Setup word list. - let mut words = vec![]; - words_split.for_each(|w| { - let mut text = w.to_string(); - text.retain(|c| c.is_alphabetic()); - let valid = search(&text).is_ok(); - words.push(PhraseWord { text, valid }); - }); - let mut has_invalid = false; - for (i, w) in words.iter().enumerate() { - if !self.insert(i, &w.text) { - has_invalid = true; - } - } - self.valid = !has_invalid; - } - } + // Setup word list. + let mut words = vec![]; + words_split.for_each(|w| { + let mut text = w.to_string(); + text.retain(|c| c.is_alphabetic()); + let valid = search(&text).is_ok(); + words.push(PhraseWord { text, valid }); + }); + let mut has_invalid = false; + for (i, w) in words.iter().enumerate() { + if !self.insert(i, &w.text) { + has_invalid = true; + } + } + self.valid = !has_invalid; + } + } - /// Check if phrase has invalid or empty words. - pub fn has_empty_or_invalid(&self) -> bool { - let words = match self.mode { - PhraseMode::Generate => &self.confirmation, - PhraseMode::Import => &self.words - }; - let mut has_empty = false; - let mut has_invalid = false; - let _: Vec<_> = words.iter().map(|w| { - if w.text.is_empty() { - has_empty = true; - } - if !w.valid { - has_invalid = true; - } - }).collect(); - has_empty || has_invalid - } -} \ No newline at end of file + /// Check if phrase has invalid or empty words. + pub fn has_empty_or_invalid(&self) -> bool { + let words = match self.mode { + PhraseMode::Generate => &self.confirmation, + PhraseMode::Import => &self.words, + }; + let mut has_empty = false; + let mut has_invalid = false; + let _: Vec<_> = words + .iter() + .map(|w| { + if w.text.is_empty() { + has_empty = true; + } + if !w.valid { + has_invalid = true; + } + }) + .collect(); + has_empty || has_invalid + } +} diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index d75d2e24..8884aaaf 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -32,5 +32,5 @@ pub use list::*; mod utils; pub use utils::WalletUtils; +mod seed; pub mod store; -mod seed; \ No newline at end of file diff --git a/src/wallet/seed.rs b/src/wallet/seed.rs index 0710780b..509134a7 100644 --- a/src/wallet/seed.rs +++ b/src/wallet/seed.rs @@ -15,7 +15,7 @@ use core::num::NonZeroU32; use grin_util::{ToHex, ZeroingString}; use grin_wallet_impls::Error; -use rand::{rng, Rng}; +use rand::{Rng, rng}; use serde_derive::{Deserialize, Serialize}; use serde_json; use std::fs::File; @@ -28,75 +28,75 @@ use ring::pbkdf2; pub struct WalletSeed(Vec); impl WalletSeed { - pub fn from_bytes(bytes: &[u8]) -> WalletSeed { - WalletSeed(bytes.to_vec()) - } + pub fn from_bytes(bytes: &[u8]) -> WalletSeed { + WalletSeed(bytes.to_vec()) + } - pub fn from_mnemonic(word_list: ZeroingString) -> Result { - let res = grin_keychain::mnemonic::to_entropy(&word_list); - match res { - Ok(s) => Ok(WalletSeed::from_bytes(&s)), - Err(_) => Err(Error::Mnemonic.into()), - } - } + pub fn from_mnemonic(word_list: ZeroingString) -> Result { + let res = grin_keychain::mnemonic::to_entropy(&word_list); + match res { + Ok(s) => Ok(WalletSeed::from_bytes(&s)), + Err(_) => Err(Error::Mnemonic.into()), + } + } - pub fn init_file( - seed_file_path: &str, - recovery_phrase: ZeroingString, - password: ZeroingString, - ) -> Result { - let seed = WalletSeed::from_mnemonic(recovery_phrase)?; - let enc_seed = EncryptedWalletSeed::from_seed(&seed, password)?; - let enc_seed_json = serde_json::to_string_pretty(&enc_seed).map_err(|_| Error::Format)?; - let mut file = File::create(seed_file_path).map_err(|_| Error::IO)?; - file.write_all(&enc_seed_json.as_bytes()) - .map_err(|_| Error::IO)?; - Ok(seed) - } + pub fn init_file( + seed_file_path: &str, + recovery_phrase: ZeroingString, + password: ZeroingString, + ) -> Result { + let seed = WalletSeed::from_mnemonic(recovery_phrase)?; + let enc_seed = EncryptedWalletSeed::from_seed(&seed, password)?; + let enc_seed_json = serde_json::to_string_pretty(&enc_seed).map_err(|_| Error::Format)?; + let mut file = File::create(seed_file_path).map_err(|_| Error::IO)?; + file.write_all(&enc_seed_json.as_bytes()) + .map_err(|_| Error::IO)?; + Ok(seed) + } } /// Encrypted wallet seed, for storing on disk and decrypting with provided password. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct EncryptedWalletSeed { - encrypted_seed: String, - pub salt: String, - pub nonce: String, + encrypted_seed: String, + pub salt: String, + pub nonce: String, } impl EncryptedWalletSeed { - /// Create a new encrypted seed from the given seed + password. - pub fn from_seed( - seed: &WalletSeed, - password: ZeroingString, - ) -> Result { - let salt: [u8; 8] = rng().random(); - let nonce: [u8; 12] = rng().random(); - let password = password.as_bytes(); - let mut key = [0; 32]; - pbkdf2::derive( - pbkdf2::PBKDF2_HMAC_SHA512, - NonZeroU32::new(100).unwrap(), - &salt, - password, - &mut key, - ); - let content = seed.0.to_vec(); - let mut enc_bytes = content; - let unbound_key = aead::UnboundKey::new(&aead::CHACHA20_POLY1305, &key).unwrap(); - let sealing_key: aead::LessSafeKey = aead::LessSafeKey::new(unbound_key); - let aad = aead::Aad::from(&[]); - let res = sealing_key.seal_in_place_append_tag( - aead::Nonce::assume_unique_for_key(nonce), - aad, - &mut enc_bytes, - ); - if let Err(_) = res { - return Err(Error::Encryption); - } - Ok(EncryptedWalletSeed { - encrypted_seed: enc_bytes.to_hex(), - salt: salt.to_hex(), - nonce: nonce.to_hex(), - }) - } -} \ No newline at end of file + /// Create a new encrypted seed from the given seed + password. + pub fn from_seed( + seed: &WalletSeed, + password: ZeroingString, + ) -> Result { + let salt: [u8; 8] = rng().random(); + let nonce: [u8; 12] = rng().random(); + let password = password.as_bytes(); + let mut key = [0; 32]; + pbkdf2::derive( + pbkdf2::PBKDF2_HMAC_SHA512, + NonZeroU32::new(100).unwrap(), + &salt, + password, + &mut key, + ); + let content = seed.0.to_vec(); + let mut enc_bytes = content; + let unbound_key = aead::UnboundKey::new(&aead::CHACHA20_POLY1305, &key).unwrap(); + let sealing_key: aead::LessSafeKey = aead::LessSafeKey::new(unbound_key); + let aad = aead::Aad::from(&[]); + let res = sealing_key.seal_in_place_append_tag( + aead::Nonce::assume_unique_for_key(nonce), + aad, + &mut enc_bytes, + ); + if let Err(_) = res { + return Err(Error::Encryption); + } + Ok(EncryptedWalletSeed { + encrypted_seed: enc_bytes.to_hex(), + salt: salt.to_hex(), + nonce: nonce.to_hex(), + }) + } +} diff --git a/src/wallet/store.rs b/src/wallet/store.rs index d0c57201..6ab90887 100644 --- a/src/wallet/store.rs +++ b/src/wallet/store.rs @@ -12,90 +12,100 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::path::PathBuf; -use std::sync::{Arc, RwLock}; use rkv::backend::{SafeMode, SafeModeDatabase, SafeModeEnvironment}; use rkv::{Manager, Rkv, SingleStore, StoreOptions, Value}; +use std::path::PathBuf; +use std::sync::{Arc, RwLock}; /// Transaction height storage. pub struct TxHeightStore { - env: Arc>>, - /// Confirmed heights. - confirmed: SingleStore, - /// Broadcasting heights. - broadcasting: SingleStore + env: Arc>>, + /// Confirmed heights. + confirmed: SingleStore, + /// Broadcasting heights. + broadcasting: SingleStore, } impl TxHeightStore { - /// Create new transaction height storage from provided directory. - pub fn new(dir: PathBuf) -> Self { - let mut manager = Manager::::singleton().write().unwrap(); - let created_arc = manager.get_or_create(dir.as_path(), Rkv::new::).unwrap(); - let env = created_arc.clone(); - let k = created_arc.read().unwrap(); + /// Create new transaction height storage from provided directory. + pub fn new(dir: PathBuf) -> Self { + let mut manager = Manager::::singleton().write().unwrap(); + let created_arc = manager + .get_or_create(dir.as_path(), Rkv::new::) + .unwrap(); + let env = created_arc.clone(); + let k = created_arc.read().unwrap(); - let confirmed = k.open_single("tx_height", StoreOptions::create()).unwrap(); - let broadcasting = k.open_single("broadcast_tx_height", StoreOptions::create()).unwrap(); - Self { - env, - confirmed, - broadcasting - } - } + let confirmed = k.open_single("tx_height", StoreOptions::create()).unwrap(); + let broadcasting = k + .open_single("broadcast_tx_height", StoreOptions::create()) + .unwrap(); + Self { + env, + confirmed, + broadcasting, + } + } - /// Read transaction height from database. - pub fn read_tx_height(&self, slate_id: &String) -> Option { - let env = self.env.read().unwrap(); - let reader = env.read().unwrap(); - if let Ok(value) = self.confirmed.get(&reader, slate_id) { - if let Some(height) = value { - return match height { - Value::U64(v) => Some(v), - _ => None - }; - } - return None; - } - None - } + /// Read transaction height from database. + pub fn read_tx_height(&self, slate_id: &String) -> Option { + let env = self.env.read().unwrap(); + let reader = env.read().unwrap(); + if let Ok(value) = self.confirmed.get(&reader, slate_id) { + if let Some(height) = value { + return match height { + Value::U64(v) => Some(v), + _ => None, + }; + } + return None; + } + None + } - /// Write transaction height to database. - pub fn write_tx_height(&self, slate_id: &String, height: u64) { - let env = self.env.read().unwrap(); - let mut writer = env.write().unwrap(); - self.confirmed.put(&mut writer, slate_id, &Value::U64(height)).unwrap(); - writer.commit().unwrap(); - } + /// Write transaction height to database. + pub fn write_tx_height(&self, slate_id: &String, height: u64) { + let env = self.env.read().unwrap(); + let mut writer = env.write().unwrap(); + self.confirmed + .put(&mut writer, slate_id, &Value::U64(height)) + .unwrap(); + writer.commit().unwrap(); + } - /// Read broadcasting height from database. - pub fn read_broadcasting_height(&self, slate_id: &String) -> Option { - let env = self.env.read().unwrap(); - let reader = env.read().unwrap(); - if let Ok(value) = self.broadcasting.get(&reader, slate_id) { - if let Some(height) = value { - return match height { - Value::U64(v) => Some(v), - _ => None - }; - } - return None; - } - None - } + /// Read broadcasting height from database. + pub fn read_broadcasting_height(&self, slate_id: &String) -> Option { + let env = self.env.read().unwrap(); + let reader = env.read().unwrap(); + if let Ok(value) = self.broadcasting.get(&reader, slate_id) { + if let Some(height) = value { + return match height { + Value::U64(v) => Some(v), + _ => None, + }; + } + return None; + } + None + } - /// Write broadcasting height to database. - pub fn write_broadcasting_height(&self, slate_id: &String, height: u64) { - let env = self.env.read().unwrap(); - let mut writer = env.write().unwrap(); - self.broadcasting.put(&mut writer, slate_id, &Value::U64(height)).unwrap(); - writer.commit().unwrap(); - } + /// Write broadcasting height to database. + pub fn write_broadcasting_height(&self, slate_id: &String, height: u64) { + let env = self.env.read().unwrap(); + let mut writer = env.write().unwrap(); + self.broadcasting + .put(&mut writer, slate_id, &Value::U64(height)) + .unwrap(); + writer.commit().unwrap(); + } - /// Delete broadcasting height from database. - pub fn delete_broadcasting_height(&self, slate_id: &String) { - let env = self.env.read().unwrap(); - let mut writer = env.write().unwrap(); - self.broadcasting.delete(&mut writer, slate_id).unwrap_or_default(); - writer.commit().unwrap(); - } + /// Delete broadcasting height from database. + pub fn delete_broadcasting_height(&self, slate_id: &String) { + let env = self.env.read().unwrap(); + let mut writer = env.write().unwrap(); + self.broadcasting + .delete(&mut writer, slate_id) + .unwrap_or_default(); + writer.commit().unwrap(); + } } diff --git a/src/wallet/types.rs b/src/wallet/types.rs index 99c7cce7..05d46b48 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -15,7 +15,10 @@ use grin_keychain::ExtKeychain; use grin_util::Mutex; use grin_wallet_impls::{DefaultLCProvider, HTTPNodeClient}; -use grin_wallet_libwallet::{Error, PaymentProof, Slate, SlateState, SlatepackAddress, TxLogEntry, TxLogEntryType, WalletInfo, WalletInst}; +use grin_wallet_libwallet::{ + Error, PaymentProof, Slate, SlateState, SlatepackAddress, TxLogEntry, TxLogEntryType, + WalletInfo, WalletInst, +}; use grin_wallet_util::OnionV3Address; use serde_derive::{Deserialize, Serialize}; use std::sync::Arc; @@ -26,433 +29,439 @@ use crate::wallet::Wallet; /// Mnemonic phrase word. #[derive(Clone)] pub struct PhraseWord { - /// Word text. - pub text: String, - /// Flag to check if word is valid. - pub valid: bool, + /// Word text. + pub text: String, + /// Flag to check if word is valid. + pub valid: bool, } /// Mnemonic phrase setup mode. #[derive(PartialEq, Clone)] pub enum PhraseMode { - /// Generate new mnemonic phrase. - Generate, - /// Import existing mnemonic phrase. - Import + /// Generate new mnemonic phrase. + Generate, + /// Import existing mnemonic phrase. + Import, } /// Mnemonic phrase size based on entropy. #[derive(PartialEq, Clone)] -pub enum PhraseSize { Words12, Words15, Words18, Words21, Words24 } +pub enum PhraseSize { + Words12, + Words15, + Words18, + Words21, + Words24, +} impl PhraseSize { - pub const VALUES: [PhraseSize; 5] = [ - PhraseSize::Words12, - PhraseSize::Words15, - PhraseSize::Words18, - PhraseSize::Words21, - PhraseSize::Words24 - ]; + pub const VALUES: [PhraseSize; 5] = [ + PhraseSize::Words12, + PhraseSize::Words15, + PhraseSize::Words18, + PhraseSize::Words21, + PhraseSize::Words24, + ]; - /// Get entropy value. - pub fn value(&self) -> usize { - match *self { - PhraseSize::Words12 => 12, - PhraseSize::Words15 => 15, - PhraseSize::Words18 => 18, - PhraseSize::Words21 => 21, - PhraseSize::Words24 => 24 - } - } + /// Get entropy value. + pub fn value(&self) -> usize { + match *self { + PhraseSize::Words12 => 12, + PhraseSize::Words15 => 15, + PhraseSize::Words18 => 18, + PhraseSize::Words21 => 21, + PhraseSize::Words24 => 24, + } + } - /// Get entropy size for current phrase size. - pub fn entropy_size(&self) -> usize { - match *self { - PhraseSize::Words12 => 16, - PhraseSize::Words15 => 20, - PhraseSize::Words18 => 24, - PhraseSize::Words21 => 28, - PhraseSize::Words24 => 32 - } - } + /// Get entropy size for current phrase size. + pub fn entropy_size(&self) -> usize { + match *self { + PhraseSize::Words12 => 16, + PhraseSize::Words15 => 20, + PhraseSize::Words18 => 24, + PhraseSize::Words21 => 28, + PhraseSize::Words24 => 32, + } + } - /// Get phrase type for entropy size. - pub fn type_for_value(count: usize) -> Option { - if Self::is_correct_count(count) { - match count { - 12 => { - Some(PhraseSize::Words12) - } - 15 => { - Some(PhraseSize::Words15) - } - 18 => { - Some(PhraseSize::Words18) - } - 21 => { - Some(PhraseSize::Words21) - } - 24 => { - Some(PhraseSize::Words24) - } - _ => { - None - } - } - } else { - None - } - } + /// Get phrase type for entropy size. + pub fn type_for_value(count: usize) -> Option { + if Self::is_correct_count(count) { + match count { + 12 => Some(PhraseSize::Words12), + 15 => Some(PhraseSize::Words15), + 18 => Some(PhraseSize::Words18), + 21 => Some(PhraseSize::Words21), + 24 => Some(PhraseSize::Words24), + _ => None, + } + } else { + None + } + } - /// Check if correct entropy size was provided. - pub fn is_correct_count(count: usize) -> bool { - count == 12 || count == 15 || count == 18 || count == 21 || count == 24 - } + /// Check if correct entropy size was provided. + pub fn is_correct_count(count: usize) -> bool { + count == 12 || count == 15 || count == 18 || count == 21 || count == 24 + } } /// Wallet connection method. #[derive(Serialize, Deserialize, Clone, PartialEq)] pub enum ConnectionMethod { - /// Integrated node. - Integrated, - /// External node, contains connection identifier and URL. - External(i64, String) + /// Integrated node. + Integrated, + /// External node, contains connection identifier and URL. + External(i64, String), } /// Wallet instance type. pub type WalletInstance = Arc< - Mutex< - Box< - dyn WalletInst< - 'static, - DefaultLCProvider<'static, HTTPNodeClient, ExtKeychain>, - HTTPNodeClient, - ExtKeychain, - >, - >, - >, + Mutex< + Box< + dyn WalletInst< + 'static, + DefaultLCProvider<'static, HTTPNodeClient, ExtKeychain>, + HTTPNodeClient, + ExtKeychain, + >, + >, + >, >; /// Wallet account data. #[derive(Clone)] pub struct WalletAccount { - /// Spendable balance amount. - pub spendable_amount: u64, - /// Account label. - pub label: String, - /// Account BIP32 derivation path. - pub path: String + /// Spendable balance amount. + pub spendable_amount: u64, + /// Account label. + pub label: String, + /// Account BIP32 derivation path. + pub path: String, } /// Wallet balance and transactions data. #[derive(Clone)] pub struct WalletData { - /// Balance data for current account. - pub info: WalletInfo, + /// Balance data for current account. + pub info: WalletInfo, - /// Transactions data. - pub txs: Option>, - /// Number of txs to show on select from database. - pub txs_limit: u32, + /// Transactions data. + pub txs: Option>, + /// Number of txs to show on select from database. + pub txs_limit: u32, } impl WalletData { - /// Number of transactions per select to show at list. - pub const TXS_LIMIT: u32 = 30; + /// Number of transactions per select to show at list. + pub const TXS_LIMIT: u32 = 30; - /// Update transaction action status. - pub fn on_tx_action(&mut self, id: u32, action: Option) { - if self.txs.is_none() { - return; - } - for tx in self.txs.as_mut().unwrap() { - if id == tx.data.id { - tx.action = action; - tx.action_error = None; - break; - } - } - } + /// Update transaction action status. + pub fn on_tx_action(&mut self, id: u32, action: Option) { + if self.txs.is_none() { + return; + } + for tx in self.txs.as_mut().unwrap() { + if id == tx.data.id { + tx.action = action; + tx.action_error = None; + break; + } + } + } - /// Update transaction action error status. - pub fn on_tx_error(&mut self, id: u32, err: Option) { - if self.txs.is_none() { - return; - } - for tx in self.txs.as_mut().unwrap() { - if id == tx.data.id { - tx.action_error = err; - break; - } - } - } + /// Update transaction action error status. + pub fn on_tx_error(&mut self, id: u32, err: Option) { + if self.txs.is_none() { + return; + } + for tx in self.txs.as_mut().unwrap() { + if id == tx.data.id { + tx.action_error = err; + break; + } + } + } - /// Get transaction by identifier. - pub fn tx_by_id(&self, id: u32) -> Option { - if self.txs.is_none() { - return None; - } - for tx in self.txs.as_ref().unwrap() { - if tx.data.id == id { - return Some(tx.clone()); - } - } - None - } + /// Get transaction by identifier. + pub fn tx_by_id(&self, id: u32) -> Option { + if self.txs.is_none() { + return None; + } + for tx in self.txs.as_ref().unwrap() { + if tx.data.id == id { + return Some(tx.clone()); + } + } + None + } } /// Wallet transaction action. #[derive(Clone, PartialEq)] pub enum WalletTxAction { - Cancelling, Finalizing, Posting, SendingTor, Deleting + Cancelling, + Finalizing, + Posting, + SendingTor, + Deleting, } /// Wallet transaction data. #[derive(Clone)] pub struct WalletTx { - /// Information from database. - pub data: TxLogEntry, - /// State of transaction Slate. - pub state: SlateState, - /// Payment proof. - pub(crate) proof: Option, + /// Information from database. + pub data: TxLogEntry, + /// State of transaction Slate. + pub state: SlateState, + /// Payment proof. + pub(crate) proof: Option, - /// Transaction amount without fees. - pub amount: u64, - /// Possible receiver of transaction. - pub receiver: Option, - /// Possible sender of transaction. - pub sender: Option, - /// Block height where tx was included. - pub height: Option, - /// Block height where tx started broadcasting. - pub broadcasting_height: Option, + /// Transaction amount without fees. + pub amount: u64, + /// Possible receiver of transaction. + pub receiver: Option, + /// Possible sender of transaction. + pub sender: Option, + /// Block height where tx was included. + pub height: Option, + /// Block height where tx started broadcasting. + pub broadcasting_height: Option, - /// Action on transaction. - pub action: Option, - /// Action result error. - pub action_error: Option + /// Action on transaction. + pub action: Option, + /// Action result error. + pub action_error: Option, } impl WalletTx { - /// Create new wallet transaction. - pub fn new(tx: TxLogEntry, - proof: Option, - wallet: &Wallet, - height: Option, - broadcasting_height: Option, - action: Option, - action_error: Option) -> Self { - let amount = if tx.amount_debited > tx.amount_credited { - tx.amount_debited - tx.amount_credited - } else { - tx.amount_credited - tx.amount_debited - }; - let mut receiver: Option = None; - let mut sender: Option = None; - if let Some(proof) = &tx.payment_proof { - let rec_onion_addr = OnionV3Address::from_bytes(proof.receiver_address.to_bytes()); - if let Ok(addr) = SlatepackAddress::try_from(rec_onion_addr) { - receiver = Some(addr); - } - let send_onion_addr = OnionV3Address::from_bytes(proof.sender_address.to_bytes()); - if let Ok(addr) = SlatepackAddress::try_from(send_onion_addr) { - sender = Some(addr); - } - } - let mut t = Self { - data: tx, - state: SlateState::Unknown, - proof, - amount, - receiver, - sender, - height, - broadcasting_height, - action, - action_error, - }; - // Update Slate state for unconfirmed. - if !t.data.confirmed { - t.update_slate_state(wallet); - } - t - } + /// Create new wallet transaction. + pub fn new( + tx: TxLogEntry, + proof: Option, + wallet: &Wallet, + height: Option, + broadcasting_height: Option, + action: Option, + action_error: Option, + ) -> Self { + let amount = if tx.amount_debited > tx.amount_credited { + tx.amount_debited - tx.amount_credited + } else { + tx.amount_credited - tx.amount_debited + }; + let mut receiver: Option = None; + let mut sender: Option = None; + if let Some(proof) = &tx.payment_proof { + let rec_onion_addr = OnionV3Address::from_bytes(proof.receiver_address.to_bytes()); + if let Ok(addr) = SlatepackAddress::try_from(rec_onion_addr) { + receiver = Some(addr); + } + let send_onion_addr = OnionV3Address::from_bytes(proof.sender_address.to_bytes()); + if let Ok(addr) = SlatepackAddress::try_from(send_onion_addr) { + sender = Some(addr); + } + } + let mut t = Self { + data: tx, + state: SlateState::Unknown, + proof, + amount, + receiver, + sender, + height, + broadcasting_height, + action, + action_error, + }; + // Update Slate state for unconfirmed. + if !t.data.confirmed { + t.update_slate_state(wallet); + } + t + } - /// Update transaction [`Slate`] state for provided wallet. - pub fn update_slate_state(&mut self, wallet: &Wallet) { - let tx = &self.data; - let mut slate = Slate::blank(1, false); - slate.id = tx.tx_slate_id.unwrap(); - slate.state = match tx.tx_type { - TxLogEntryType::TxReceived => SlateState::Invoice3, - _ => SlateState::Standard3 - }; - // Transaction was finalized. - if wallet.slatepack_exists(&slate) { - self.state = slate.state; - } else { - slate.id = tx.tx_slate_id.unwrap(); - slate.state = match tx.tx_type { - TxLogEntryType::TxReceived => SlateState::Standard2, - _ => SlateState::Invoice2 - }; - // Transaction signed to be finalized. - if wallet.slatepack_exists(&slate) { - self.state = slate.state; - } else { - // Transaction just was created. - slate.state = match tx.tx_type { - TxLogEntryType::TxReceived => SlateState::Invoice1, - _ => SlateState::Standard1 - }; - if wallet.slatepack_exists(&slate) { - self.state = slate.state; - } else { - self.state = SlateState::Unknown; - } - } - } - } + /// Update transaction [`Slate`] state for provided wallet. + pub fn update_slate_state(&mut self, wallet: &Wallet) { + let tx = &self.data; + let mut slate = Slate::blank(1, false); + slate.id = tx.tx_slate_id.unwrap(); + slate.state = match tx.tx_type { + TxLogEntryType::TxReceived => SlateState::Invoice3, + _ => SlateState::Standard3, + }; + // Transaction was finalized. + if wallet.slatepack_exists(&slate) { + self.state = slate.state; + } else { + slate.id = tx.tx_slate_id.unwrap(); + slate.state = match tx.tx_type { + TxLogEntryType::TxReceived => SlateState::Standard2, + _ => SlateState::Invoice2, + }; + // Transaction signed to be finalized. + if wallet.slatepack_exists(&slate) { + self.state = slate.state; + } else { + // Transaction just was created. + slate.state = match tx.tx_type { + TxLogEntryType::TxReceived => SlateState::Invoice1, + _ => SlateState::Standard1, + }; + if wallet.slatepack_exists(&slate) { + self.state = slate.state; + } else { + self.state = SlateState::Unknown; + } + } + } + } - /// Check if transactions can be finalized after receiving response. - pub fn can_finalize(&self) -> bool { - !self.cancelling() && !self.data.confirmed && - (!self.sending_tor() || self.action_error.is_some()) && - (self.data.tx_type == TxLogEntryType::TxSent || - self.data.tx_type == TxLogEntryType::TxReceived) && - (self.state == SlateState::Invoice1 || self.state == SlateState::Standard1) - } + /// Check if transactions can be finalized after receiving response. + pub fn can_finalize(&self) -> bool { + !self.cancelling() + && !self.data.confirmed + && (!self.sending_tor() || self.action_error.is_some()) + && (self.data.tx_type == TxLogEntryType::TxSent + || self.data.tx_type == TxLogEntryType::TxReceived) + && (self.state == SlateState::Invoice1 || self.state == SlateState::Standard1) + } - /// Check if transaction was finalized. - pub fn finalized(&self) -> bool { - (self.data.tx_type == TxLogEntryType::TxSent || - self.data.tx_type == TxLogEntryType::TxReceived) && - self.state == SlateState::Invoice3 || self.state == SlateState::Standard3 - } + /// Check if transaction was finalized. + pub fn finalized(&self) -> bool { + (self.data.tx_type == TxLogEntryType::TxSent + || self.data.tx_type == TxLogEntryType::TxReceived) + && self.state == SlateState::Invoice3 + || self.state == SlateState::Standard3 + } - /// Check if transaction is sending over Tor. - pub fn sending_tor(&self) -> bool { - if let Some(a) = self.action.as_ref() { - return a == &WalletTxAction::SendingTor; - } - false - } + /// Check if transaction is sending over Tor. + pub fn sending_tor(&self) -> bool { + if let Some(a) = self.action.as_ref() { + return a == &WalletTxAction::SendingTor; + } + false + } - /// Check if transaction is cancelling. - pub fn cancelling(&self) -> bool { - if let Some(a) = self.action.as_ref() { - return a == &WalletTxAction::Cancelling; - } - false - } + /// Check if transaction is cancelling. + pub fn cancelling(&self) -> bool { + if let Some(a) = self.action.as_ref() { + return a == &WalletTxAction::Cancelling; + } + false + } - /// Check if transaction is posting. - pub fn posting(&self) -> bool { - if let Some(a) = self.action.as_ref() { - return a == &WalletTxAction::Posting; - } - false - } + /// Check if transaction is posting. + pub fn posting(&self) -> bool { + if let Some(a) = self.action.as_ref() { + return a == &WalletTxAction::Posting; + } + false + } - /// Check if transaction can be cancelled. - pub fn can_cancel(&self) -> bool { - !self.cancelling() && !self.data.confirmed && !self.broadcasting() && - (!self.sending_tor() || self.action_error.is_some()) && - self.data.tx_type != TxLogEntryType::TxReceivedCancelled && - self.data.tx_type != TxLogEntryType::TxSentCancelled - } + /// Check if transaction can be cancelled. + pub fn can_cancel(&self) -> bool { + !self.cancelling() + && !self.data.confirmed + && !self.broadcasting() + && (!self.sending_tor() || self.action_error.is_some()) + && self.data.tx_type != TxLogEntryType::TxReceivedCancelled + && self.data.tx_type != TxLogEntryType::TxSentCancelled + } - /// Check if transaction was canceled. - pub fn cancelled(&self) -> bool { - self.data.tx_type == TxLogEntryType::TxReceivedCancelled || - self.data.tx_type == TxLogEntryType::TxSentCancelled - } + /// Check if transaction was canceled. + pub fn cancelled(&self) -> bool { + self.data.tx_type == TxLogEntryType::TxReceivedCancelled + || self.data.tx_type == TxLogEntryType::TxSentCancelled + } - /// Check if transaction is finalizing. - pub fn finalizing(&self) -> bool { - if let Some(a) = self.action.as_ref() { - return a == &WalletTxAction::Finalizing; - } - false - } + /// Check if transaction is finalizing. + pub fn finalizing(&self) -> bool { + if let Some(a) = self.action.as_ref() { + return a == &WalletTxAction::Finalizing; + } + false + } - /// Check if possible to repeat transaction action. - pub fn can_repeat_action(&self, wallet: &Wallet) -> bool { - if let Some(a) = &self.action { - self.action_error.is_some() && a != &WalletTxAction::Cancelling - } else { - // Can resend over Tor. - !self.data.confirmed && !self.sending_tor() && - Tor::is_service_running(&wallet.identifier()) && !self.broadcasting() && - self.receiver.is_some() - } - } + /// Check if possible to repeat transaction action. + pub fn can_repeat_action(&self, wallet: &Wallet) -> bool { + if let Some(a) = &self.action { + self.action_error.is_some() && a != &WalletTxAction::Cancelling + } else { + // Can resend over Tor. + !self.data.confirmed + && !self.sending_tor() + && Tor::is_service_running(&wallet.identifier()) + && !self.broadcasting() + && self.receiver.is_some() + } + } - /// Check if transaction is broadcasting after finalization. - pub fn broadcasting(&self) -> bool { - !self.data.confirmed && self.finalized() - } + /// Check if transaction is broadcasting after finalization. + pub fn broadcasting(&self) -> bool { + !self.data.confirmed && self.finalized() + } - /// Check if broadcasting of transaction was timed out. - pub fn broadcasting_timed_out(&self, wallet: &Wallet) -> bool { - if let Some(data) = wallet.get_data() { - if self.broadcasting() { - let last_height = data.info.last_confirmed_height; - let broadcasting_height = self.broadcasting_height.unwrap_or(0); - if broadcasting_height == 0 { - return false; - } - let delay = wallet.broadcasting_delay(); - return last_height - broadcasting_height > delay; - } - } - false - } + /// Check if broadcasting of transaction was timed out. + pub fn broadcasting_timed_out(&self, wallet: &Wallet) -> bool { + if let Some(data) = wallet.get_data() { + if self.broadcasting() { + let last_height = data.info.last_confirmed_height; + let broadcasting_height = self.broadcasting_height.unwrap_or(0); + if broadcasting_height == 0 { + return false; + } + let delay = wallet.broadcasting_delay(); + return last_height - broadcasting_height > delay; + } + } + false + } - /// Check if transaction is deleting. - pub fn deleting(&self) -> bool { - if let Some(a) = self.action.as_ref() { - return a == &WalletTxAction::Deleting; - } - false - } + /// Check if transaction is deleting. + pub fn deleting(&self) -> bool { + if let Some(a) = self.action.as_ref() { + return a == &WalletTxAction::Deleting; + } + false + } } /// Task for the wallet. #[derive(Clone)] pub enum WalletTask { - /// Open Slatepack message parsing result and making an action. - OpenMessage(String), - /// Calculate fee to send amount. - /// * amount - /// * fee (to read at result) - CalculateFee(u64, u64), - /// Verify payment proof. - /// * payment proof - /// * result (tx id, sender mine, receiver mine) - VerifyProof(PaymentProof, Option>), - /// Create request to send. - /// * amount - /// * receiver - Send(u64, Option), - /// Send request over Tor. - /// * tx - /// * receiver - SendTor(TxLogEntry, SlatepackAddress), - /// Invoice creation. - /// * amount - Receive(u64), - /// Transaction finalization. - /// * tx id - Finalize(u32), - /// Post transaction to blockchain. - /// * tx id - Post(u32), - /// Cancel transaction. - /// * tx id - Cancel(u32), - /// Delete transaction. - /// * tx id - Delete(u32) -} \ No newline at end of file + /// Open Slatepack message parsing result and making an action. + OpenMessage(String), + /// Calculate fee to send amount. + /// * amount + /// * fee (to read at result) + CalculateFee(u64, u64), + /// Verify payment proof. + /// * payment proof + /// * result (tx id, sender mine, receiver mine) + VerifyProof(PaymentProof, Option>), + /// Create request to send. + /// * amount + /// * receiver + Send(u64, Option), + /// Send request over Tor. + /// * tx + /// * receiver + SendTor(TxLogEntry, SlatepackAddress), + /// Invoice creation. + /// * amount + Receive(u64), + /// Transaction finalization. + /// * tx id + Finalize(u32), + /// Post transaction to blockchain. + /// * tx id + Post(u32), + /// Cancel transaction. + /// * tx id + Cancel(u32), + /// Delete transaction. + /// * tx id + Delete(u32), +} diff --git a/src/wallet/utils.rs b/src/wallet/utils.rs index 26ec9138..b875542d 100644 --- a/src/wallet/utils.rs +++ b/src/wallet/utils.rs @@ -12,17 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -use sha2::{Sha256, Digest}; +use sha2::{Digest, Sha256}; /// Wallet utilities functions. pub struct WalletUtils {} impl WalletUtils { - /// Setup entropy data checksum. - pub fn setup_checksum(data: &mut Vec) { - let mut hasher = Sha256::new(); - hasher.update(data.clone()); - let checksum = hasher.finalize(); - data.extend(checksum); - } -} \ No newline at end of file + /// Setup entropy data checksum. + pub fn setup_checksum(data: &mut Vec) { + let mut hasher = Sha256::new(); + hasher.update(data.clone()); + let checksum = hasher.finalize(); + data.extend(checksum); + } +} diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 0010cffb..cb46e96e 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -12,14 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::AppConfig; use crate::node::{Node, NodeConfig}; use crate::tor::Tor; use crate::wallet::seed::WalletSeed; use crate::wallet::store::TxHeightStore; -use crate::wallet::types::{ConnectionMethod, PhraseMode, WalletAccount, WalletData, WalletInstance, WalletTask, WalletTx, WalletTxAction}; +use crate::wallet::types::{ + ConnectionMethod, PhraseMode, WalletAccount, WalletData, WalletInstance, WalletTask, WalletTx, + WalletTxAction, +}; use crate::wallet::{ConnectionsConfig, Mnemonic, WalletConfig}; -use crate::AppConfig; +use chrono::Utc; use futures::channel::oneshot; use grin_api::{ApiServer, Router}; use grin_chain::SyncStatus; @@ -32,12 +36,20 @@ use grin_wallet_controller::command::parse_slatepack; use grin_wallet_controller::controller; use grin_wallet_controller::controller::ForeignAPIHandlerV2; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient, LMDBBackend}; -use grin_wallet_libwallet::api_impl::owner::{cancel_tx, init_send_tx, retrieve_summary_info, retrieve_txs, verify_payment_proof}; -use grin_wallet_libwallet::{address, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, PaymentProof, Slate, SlateState, SlateVersion, SlatepackAddress, StatusMessage, StoredProofInfo, TxLogEntry, TxLogEntryType, VersionedSlate, WalletBackend, WalletInitStatus, WalletInst, WalletLCProvider}; +use grin_wallet_libwallet::api_impl::owner::{ + cancel_tx, init_send_tx, retrieve_summary_info, retrieve_txs, verify_payment_proof, +}; +use grin_wallet_libwallet::{ + Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, PaymentProof, Slate, SlateState, + SlateVersion, SlatepackAddress, StatusMessage, StoredProofInfo, TxLogEntry, TxLogEntryType, + VersionedSlate, WalletBackend, WalletInitStatus, WalletInst, WalletLCProvider, address, +}; use grin_wallet_util::OnionV3Address; +use log::error; +use num_bigint::BigInt; use parking_lot::RwLock; use rand::Rng; -use serde_json::{json, Value}; +use serde_json::{Value, json}; use std::fs::File; use std::io::Write; use std::net::{SocketAddr, TcpListener, ToSocketAddrs}; @@ -45,1424 +57,1463 @@ use std::path::PathBuf; use std::str::FromStr; use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU8, Ordering}; use std::sync::mpsc::Sender; -use std::sync::{mpsc, Arc}; +use std::sync::{Arc, mpsc}; use std::thread::Thread; use std::time::Duration; use std::{fs, path, thread}; -use chrono::Utc; -use log::error; -use num_bigint::BigInt; use tor_config::deps::Itertools; use uuid::Uuid; /// Contains wallet instance, configuration and state, handles wallet commands. #[derive(Clone)] pub struct Wallet { - /// Wallet configuration. - config: Arc>, - /// Wallet instance, initializing on wallet opening and clearing on wallet closing. - instance: Arc>>, - /// Connection of current wallet instance. - connection: Arc>, - /// Keychain mask for API calls. - keychain_mask: Arc>>, + /// Wallet configuration. + config: Arc>, + /// Wallet instance, initializing on wallet opening and clearing on wallet closing. + instance: Arc>>, + /// Connection of current wallet instance. + connection: Arc>, + /// Keychain mask for API calls. + keychain_mask: Arc>>, - /// Wallet Slatepack address to receive txs at transport. - slatepack_address: Arc>>, + /// Wallet Slatepack address to receive txs at transport. + slatepack_address: Arc>>, - /// Wallet accounts. - accounts: Arc>>, - /// Timestamp when wallet account was selected to form unique identifier for transport. - account_time: Arc, + /// Wallet accounts. + accounts: Arc>>, + /// Timestamp when wallet account was selected to form unique identifier for transport. + account_time: Arc, - /// Wallet sync thread. - sync_thread: Arc>>, - /// Flag to check if wallet is syncing. - syncing: Arc, - /// Info loading progress in percents. - info_sync_progress: Arc, - /// Error on wallet loading. - sync_error: Arc, - /// Attempts amount to update wallet data. - sync_attempts: Arc, + /// Wallet sync thread. + sync_thread: Arc>>, + /// Flag to check if wallet is syncing. + syncing: Arc, + /// Info loading progress in percents. + info_sync_progress: Arc, + /// Error on wallet loading. + sync_error: Arc, + /// Attempts amount to update wallet data. + sync_attempts: Arc, - /// Wallet data. - data: Arc>>, - /// Flag to check if wallet data was synced from node. - from_node: Arc, - /// Flag to check if more transactions need to be loaded. - more_txs_loading: Arc, + /// Wallet data. + data: Arc>>, + /// Flag to check if wallet data was synced from node. + from_node: Arc, + /// Flag to check if more transactions need to be loaded. + more_txs_loading: Arc, - /// Flag to check if wallet reopening is needed. - reopen: Arc, - /// Flag to check if wallet is open. - is_open: Arc, - /// Flag to check if wallet is closing. - closing: Arc, - /// Flag to check if wallet was deleted to remove it from the list. - deleted: Arc, + /// Flag to check if wallet reopening is needed. + reopen: Arc, + /// Flag to check if wallet is open. + is_open: Arc, + /// Flag to check if wallet is closing. + closing: Arc, + /// Flag to check if wallet was deleted to remove it from the list. + deleted: Arc, - /// Running wallet foreign API server and port. - foreign_api_server: Arc>>, - /// Wallet secret key for transport service. - secret_key: Arc>>, + /// Running wallet foreign API server and port. + foreign_api_server: Arc>>, + /// Wallet secret key for transport service. + secret_key: Arc>>, - /// Flag to check if wallet repairing and restoring missing outputs is needed. - repair_needed: Arc, - /// Wallet repair progress in percents. - repair_progress: Arc, + /// Flag to check if wallet repairing and restoring missing outputs is needed. + repair_needed: Arc, + /// Wallet repair progress in percents. + repair_progress: Arc, - /// Flag to check if wallet files are moving. - files_moving: Arc, + /// Flag to check if wallet files are moving. + files_moving: Arc, - /// Flag to check if Slatepack message file is opening. - message_opening: Arc, + /// Flag to check if Slatepack message file is opening. + message_opening: Arc, - /// Amount requests to calculate fee. - fee_calculating: Arc, + /// Amount requests to calculate fee. + fee_calculating: Arc, - /// Flag to check if sending request is creating. - send_creating: Arc, - /// Flag to check if invoice is creating. - invoice_creating: Arc, + /// Flag to check if sending request is creating. + send_creating: Arc, + /// Flag to check if invoice is creating. + invoice_creating: Arc, - /// Amount requests to calculate fee. - proof_verifying: Arc, + /// Amount requests to calculate fee. + proof_verifying: Arc, - /// Tasks sender. - tasks_sender: Arc>>>, - /// Task result with optional transaction identifier. - task_result: Arc, WalletTask)>>>, + /// Tasks sender. + tasks_sender: Arc>>>, + /// Task result with optional transaction identifier. + task_result: Arc, WalletTask)>>>, } impl Wallet { - /// Create new [`Wallet`] instance with provided [`WalletConfig`]. - fn new(config: WalletConfig) -> Self { - let connection = config.connection(); - Self { - config: Arc::new(RwLock::new(config)), - instance: Arc::new(RwLock::new(None)), - connection: Arc::new(RwLock::new(connection)), - keychain_mask: Arc::new(RwLock::new(None)), - slatepack_address: Arc::new(RwLock::new(None)), - accounts: Arc::new(RwLock::new(vec![])), - account_time: Arc::new(Default::default()), - sync_thread: Arc::from(RwLock::new(None)), - syncing: Arc::new(AtomicBool::new(false)), - info_sync_progress: Arc::from(AtomicU8::new(0)), - sync_error: Arc::from(AtomicBool::new(false)), - sync_attempts: Arc::new(AtomicU8::new(0)), - data: Arc::new(RwLock::new(None)), - from_node: Arc::new(AtomicBool::new(false)), - more_txs_loading: Arc::new(AtomicBool::new(false)), - reopen: Arc::new(AtomicBool::new(false)), - is_open: Arc::from(AtomicBool::new(false)), - closing: Arc::new(AtomicBool::new(false)), - deleted: Arc::new(AtomicBool::new(false)), - foreign_api_server: Arc::new(RwLock::new(None)), - secret_key: Arc::new(RwLock::new(None)), - repair_needed: Arc::new(AtomicBool::new(false)), - repair_progress: Arc::new(AtomicU8::new(0)), - files_moving: Arc::new(AtomicBool::new(false)), - message_opening: Arc::new(AtomicBool::from(false)), - send_creating: Arc::new(AtomicBool::new(false)), - fee_calculating: Arc::new(AtomicU8::new(0)), - invoice_creating: Arc::new(AtomicBool::new(false)), - proof_verifying: Arc::new(AtomicBool::new(false)), - tasks_sender: Arc::new(RwLock::new(None)), - task_result: Arc::new(RwLock::new(None)), - } - } - - /// Create new wallet. - pub fn create( - name: &String, - password: &ZeroingString, - mnemonic: &Mnemonic, - conn_method: &ConnectionMethod - ) -> Result { - let config = WalletConfig::create(name.clone(), conn_method); - let w = Wallet::new(config.clone()); - { - // Wallet directory setup. - let mut path = PathBuf::from(config.get_data_path()); - path.push(WalletConfig::DATA_DIR_NAME); - fs::create_dir_all(&path) - .map_err(|_| Error::IO("Directory creation error".to_string()))?; - // Create seed file. - let _ = WalletSeed::init_file(config.seed_path().as_str(), - ZeroingString::from(mnemonic.get_phrase()), - password.clone()) - .map_err(|_| Error::IO("Seed file creation error".to_string()))?; - let node_client = Self::create_node_client(&config)?; - let mut wallet: LMDBBackend<'static, HTTPNodeClient, ExtKeychain> = - match LMDBBackend::new(path.to_str().unwrap(), node_client) { - Err(_) => { - return Err(Error::Lifecycle("DB creation error".to_string()).into()); - } - Ok(d) => d, - }; - // Save init status of this wallet, to determine whether it needs a full UTXO scan. - let mut batch = wallet.batch_no_mask()?; - match mnemonic.mode() { - PhraseMode::Generate => batch.save_init_status(WalletInitStatus::InitNoScanning)?, - PhraseMode::Import => batch.save_init_status(WalletInitStatus::InitNeedsScanning)?, - } - batch.commit()?; - } - Ok(w) - } - - /// Initialize [`Wallet`] from provided data path. - pub fn init(data_path: PathBuf) -> Option { - let wallet_config = WalletConfig::load(data_path); - if let Some(config) = wallet_config { - return Some(Wallet::new(config)); - } - None - } - - /// Create [`HTTPNodeClient`] from provided config. - fn create_node_client(config: &WalletConfig) -> Result { - let integrated = || { - let api_url = format!("http://{}", NodeConfig::get_api_address()); - let api_secret = NodeConfig::get_api_secret(true); - (api_url, api_secret) - }; - let (node_api_url, node_secret) = if let Some(id) = config.ext_conn_id { - if let Some(conn) = ConnectionsConfig::ext_conn(id) { - (conn.url, conn.secret) - } else { - integrated() - } - } else { - integrated() - }; - let client = if AppConfig::use_proxy() { - let socks = AppConfig::use_socks_proxy(); - let url = if socks { - AppConfig::socks_proxy_url() - } else { - AppConfig::http_proxy_url() - }.unwrap_or("".to_string()).replace("http://", "").replace("socks5://", ""); - - // Convert URL to SocketAddr. - let addr_res = match SocketAddr::from_str(url.as_str()) { - Ok(ip_addr) => Some(ip_addr), - Err(_) => { - if let Ok(mut socket_addr_list) = url.to_socket_addrs() { - if let Some(addr) = socket_addr_list.next() { - Some(addr) - } else { - None - } - } else { - None - } - } - }; - - match addr_res { - None => { - HTTPNodeClient::new(&node_api_url, node_secret)? - } - Some(addr) => { - let scheme = if socks { - "socks5://" - } else { - "http://" - }; - HTTPNodeClient::new_proxy(&node_api_url, node_secret, Some((addr, scheme)))? - } - } - } else { - HTTPNodeClient::new(&node_api_url, node_secret)? - }; - Ok(client) - } - - /// Create [`WalletInstance`] from provided [`WalletConfig`]. - fn create_wallet_instance(config: &mut WalletConfig) -> Result { - // Setup node client. - let node_client = Self::create_node_client(config)?; - - // Create wallet instance. - let wallet = Self::inst_wallet::< - DefaultLCProvider, - HTTPNodeClient, - ExtKeychain, - >(config, node_client)?; - Ok(wallet) - } - - /// Instantiate [`WalletInstance`] from provided node client and [`WalletConfig`]. - fn inst_wallet( - config: &mut WalletConfig, - node_client: C, - ) -> Result>>>, Error> - where - DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, - L: WalletLCProvider<'static, C, K>, - C: NodeClient + 'static, - K: Keychain + 'static, - { - let mut wallet = Box::new(DefaultWalletImpl::<'static, C>::new(node_client).unwrap()) - as Box>; - let lc = wallet.lc_provider()?; - lc.set_top_level_directory(config.get_data_path().as_str())?; - Ok(Arc::new(Mutex::new(wallet))) - } - - /// Open the wallet and start [`WalletData`] sync at separate thread. - pub fn open(&self, password: ZeroingString) -> Result<(), Error> { - if self.is_open() { - return Err(Error::GenericError("Already opened".to_string())); - } - - // Create new wallet instance if sync thread was stopped or instance was not created. - let has_instance = { - let r_inst = self.instance.as_ref().read(); - r_inst.is_some() - }; - if self.sync_thread.read().is_none() || !has_instance { - let mut config = self.get_config(); - // Setup current connection. - { - let mut w_conn = self.connection.write(); - *w_conn = config.connection(); - } - let new_instance = Self::create_wallet_instance(&mut config)?; - let mut w_inst = self.instance.write(); - *w_inst = Some(new_instance); - } - - // Open the wallet. - { - let instance = { - let r_inst = self.instance.as_ref().read(); - r_inst.clone().unwrap() - }; - let mut wallet_lock = instance.lock(); - let lc = wallet_lock.lc_provider()?; - match lc.open_wallet(None, password, true, false) { - Ok(m) => { - { - let mut w_mask = self.keychain_mask.write(); - *w_mask = m; - } - // Reset an error on opening. - self.set_sync_error(false); - self.reset_sync_attempts(); - - // Set current account. - let wallet_inst = lc.wallet_inst()?; - let label = self.get_config().account.to_owned(); - wallet_inst.set_parent_key_id_by_name(label.as_str())?; - self.account_time.store(Utc::now().timestamp(), Ordering::Relaxed); - - // Start new synchronization thread or wake up existing one. - let mut thread_w = self.sync_thread.write(); - if thread_w.is_none() { - let thread = start_sync(self.clone()); - *thread_w = Some(thread); - } else { - thread_w.clone().unwrap().unpark(); - } - self.is_open.store(true, Ordering::Relaxed); - } - Err(e) => { - if !self.syncing() { - let mut w_inst = self.instance.write(); - *w_inst = None; - } - return Err(e) - } - } - } - - // Update Slatepack address and secret key. - self.update_secret_key_addr()?; - - Ok(()) - } - - /// Get keychain mask [`SecretKey`]. - pub fn keychain_mask(&self) -> Option { - let r_key = self.keychain_mask.read(); - r_key.clone() - } - - /// Get wallet [`SecretKey`] for transport. - pub fn secret_key(&self) -> Option { - let r_key = self.secret_key.read(); - r_key.clone() - } - - /// Retrieve wallet [`SecretKey`] and Slatepack address for transport. - fn update_secret_key_addr(&self) -> Result<(), Error> { - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let mut w_lock = instance.lock(); - let lc = w_lock.lc_provider()?; - let w_inst = lc.wallet_inst()?; - let k = w_inst.keychain(self.keychain_mask().as_ref())?; - let parent_key_id = w_inst.parent_key_id(); - let sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0) - .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; - let addr = SlatepackAddress::try_from(&sec_key)?; - let mut w_key = self.secret_key.write(); - *w_key = Some(sec_key); - let mut w_address = self.slatepack_address.write(); - *w_address = Some(addr.to_string()); - Ok(()) - } - - /// Get unique opened wallet identifier, including current account. - pub fn identifier(&self) -> String { - let config = self.get_config(); - let account_ts = self.account_time.load(Ordering::Relaxed); - format!("{}_{}_{}", config.id, config.account.to_hex(), account_ts) - } - - /// Get Slatepack address to receive txs at transport. - pub fn slatepack_address(&self) -> Option { - let r_address = self.slatepack_address.read(); - if r_address.is_some() { - let addr = r_address.clone(); - return addr - } - None - } - - /// Get wallet config. - pub fn get_config(&self) -> WalletConfig { - self.config.read().clone() - } - - /// Change wallet name. - pub fn change_name(&self, name: String) { - let mut w_config = self.config.write(); - w_config.name = name; - w_config.save(); - } - - /// Check if start of Tor listener on wallet opening is needed. - pub fn auto_start_tor_listener(&self) -> bool { - let r_config = self.config.read(); - r_config.enable_tor_listener.unwrap_or(true) - } - - /// Update start of Tor listener on wallet opening. - pub fn update_auto_start_tor_listener(&self, start: bool) { - let mut w_config = self.config.write(); - w_config.enable_tor_listener = Some(start); - w_config.save(); - } - - /// Check if Dandelion usage is needed to post transactions. - pub fn can_use_dandelion(&self) -> bool { - let r_config = self.config.read(); - r_config.use_dandelion.unwrap_or(true) - } - - /// Update usage of Dandelion to post transactions. - pub fn update_use_dandelion(&self, use_dandelion: bool) { - let mut w_config = self.config.write(); - w_config.use_dandelion = Some(use_dandelion); - w_config.save(); - } - - /// Update minimal amount of confirmations. - pub fn update_min_confirmations(&self, min_confirmations: u64) { - let mut w_config = self.config.write(); - w_config.min_confirmations = min_confirmations; - w_config.save(); - } - - /// Get transaction broadcasting delay in blocks. - pub fn broadcasting_delay(&self) -> u64 { - let r_config = self.config.read(); - r_config.tx_broadcast_timeout.unwrap_or(WalletConfig::BROADCASTING_TIMEOUT_DEFAULT) - } - - /// Update transaction broadcasting delay in blocks. - pub fn update_broadcasting_delay(&self, delay: u64) { - let mut w_config = self.config.write(); - w_config.tx_broadcast_timeout = Some(delay); - w_config.save(); - } - - /// Update external connection identifier. - pub fn update_connection(&self, conn: &ConnectionMethod) { - let mut w_config = self.config.write(); - w_config.ext_conn_id = match conn { - ConnectionMethod::Integrated => None, - ConnectionMethod::External(id, _) => Some(id.clone()) - }; - w_config.save(); - } - - /// Get external connection URL applied to [`WalletInstance`] - /// after wallet opening if sync is running or get it from config. - pub fn get_current_connection(&self) -> ConnectionMethod { - if self.sync_thread.read().is_some() { - let r_conn = self.connection.read(); - r_conn.clone() - } else { - let config = self.get_config(); - config.connection() - } - } - - /// Check if wallet is open. - pub fn is_open(&self) -> bool { - self.is_open.load(Ordering::Relaxed) - } - - /// Check if wallet is closing. - pub fn is_closing(&self) -> bool { - self.closing.load(Ordering::Relaxed) - } - - /// Close the wallet. - pub fn close(&self) { - let has_instance = { - let r_inst = self.instance.read(); - r_inst.is_some() - }; - if !self.is_open() || !has_instance { - return; - } - // Stop repairing. - if self.is_repairing() { - self.repair_needed.store(false, Ordering::Relaxed); - } - // Close wallet at separate thread. - let wallet_close = self.clone(); - let service_id = wallet_close.identifier(); - let conn = wallet_close.connection.clone(); - thread::spawn(move || { - wallet_close.closing.store(true, Ordering::Relaxed); - // Wait common operations to finish. - while wallet_close.message_opening() || wallet_close.send_creating() || - wallet_close.invoice_creating() { - thread::sleep(Duration::from_millis(300)); - } - // Stop running API server. - let api_server_exists = { - wallet_close.foreign_api_server.read().is_some() - }; - if api_server_exists { - let mut w_api_server = wallet_close.foreign_api_server.write(); - w_api_server.as_mut().unwrap().0.stop(); - *w_api_server = None; - } - // Stop running Tor service. - Tor::stop_service(&service_id); - // Close the wallet. - let r_inst = wallet_close.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - Self::close_wallet(&instance); - wallet_close.closing.store(false, Ordering::Relaxed); - wallet_close.is_open.store(false, Ordering::Relaxed); - // Setup current connection. - { - let mut w_conn = conn.write(); - *w_conn = wallet_close.get_config().connection(); - } - wallet_close.from_node.store(false, Ordering::Relaxed); - // Start sync to exit from thread. - wallet_close.sync(); - }); - } - - /// Close wallet for provided [`WalletInstance`]. - fn close_wallet(instance: &WalletInstance) { - let mut wallet_lock = instance.lock(); - let lc = wallet_lock.lc_provider().unwrap(); - let _ = lc.close_wallet(None); - } - - /// Set wallet reopen status. - pub fn set_reopen(&self, reopen: bool) { - self.reopen.store(reopen, Ordering::Relaxed); - } - - /// Check if wallet reopen is needed. - pub fn reopen_needed(&self) -> bool { - self.reopen.load(Ordering::Relaxed) - } - - /// Get wallet info synchronization progress. - pub fn info_sync_progress(&self) -> u8 { - self.info_sync_progress.load(Ordering::Relaxed) - } - - /// Check if wallet had an error on synchronization. - pub fn sync_error(&self) -> bool { - self.sync_error.load(Ordering::Relaxed) - } - - /// Set an error for wallet on synchronization. - pub fn set_sync_error(&self, error: bool) { - self.sync_error.store(error, Ordering::Relaxed); - } - - /// Check if wallet was synced from node after opening. - pub fn synced_from_node(&self) -> bool { - self.from_node.load(Ordering::Relaxed) - } - - /// Get current wallet synchronization attempts before setting an error. - fn get_sync_attempts(&self) -> u8 { - self.sync_attempts.load(Ordering::Relaxed) - } - - /// Increment wallet synchronization attempts before setting an error. - fn increment_sync_attempts(&self) { - let mut attempts = self.get_sync_attempts(); - attempts += 1; - self.sync_attempts.store(attempts, Ordering::Relaxed); - } - - /// Reset wallet synchronization attempts. - fn reset_sync_attempts(&self) { - self.sync_attempts.store(0, Ordering::Relaxed); - } - - /// Select transaction by slate id. - fn retrieve_tx_by_id(&self, id: Option, slate_id: Option) -> Option { - let r_inst = self.instance.as_ref().read(); - let inst = r_inst.clone().unwrap(); - let mask = self.keychain_mask(); - if let Ok((_, txs)) = retrieve_txs(inst, mask.as_ref(), &None, false, id, slate_id, None) { - if !txs.is_empty() { - return Some(txs.get(0).unwrap().clone()) - } - } - None - } - - /// Select transactions with provided limit. - fn retrieve_txs(&self, limit: u32) -> Result, Error> { - let r_inst = self.instance.as_ref().read(); - let inst = r_inst.clone().unwrap(); - let mut wallet_lock = inst.lock(); - let lc = wallet_lock.lc_provider()?; - let w = lc.wallet_inst()?; - let parent_key_id = w.parent_key_id(); - // Retrieve txs from database. - let txs: Vec = w.tx_log_iter() - .filter(|tx_entry| tx_entry.parent_key_id == parent_key_id) - // Filter transactions to not show txs without slate (usually unspent outputs). - .filter(|tx| { - tx.tx_slate_id.is_some() || (tx.tx_slate_id.is_none() && tx.payment_proof.is_some()) - }) - .filter(|tx_entry| { - if tx_entry.tx_type == TxLogEntryType::TxSent - || tx_entry.tx_type == TxLogEntryType::TxSentCancelled { - BigInt::from(tx_entry.amount_debited) - - BigInt::from(tx_entry.amount_credited) - >= BigInt::from(1) - } else { - BigInt::from(tx_entry.amount_credited) - - BigInt::from(tx_entry.amount_debited) - >= BigInt::from(1) - } - }) - // Sort txs by creation date and confirmation status. - .sorted_by_key(|tx| -tx.creation_ts.timestamp()) - // Sort to show unconfirmed at top. - .sorted_by_key(|tx| { - tx.confirmed || tx.tx_type == TxLogEntryType::TxReceivedCancelled || - tx.tx_type == TxLogEntryType::TxSentCancelled || - tx.tx_type == TxLogEntryType::TxReverted - }) - // Apply limit. - .take(limit as usize) - .collect(); - // Reverse an order. - // txs.reverse(); - Ok(txs) - } - - /// Send a task to the wallet. - pub fn task(&self, task: WalletTask) { - let r_tasks = self.tasks_sender.read(); - if r_tasks.is_some() { - match task { - WalletTask::CalculateFee(_, _) => { - let calculating = self.fee_calculating.load(Ordering::Relaxed); - self.fee_calculating.store(calculating + 1, Ordering::Relaxed); - } - _ => {} - } - let _ = r_tasks.as_ref().unwrap().send(task); - } - } - - /// Create account into wallet. - pub fn create_account(&self, label: &String) -> Result<(), Error> { - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let mut api = Owner::new(instance, None); - controller::owner_single_use(None, self.keychain_mask().as_ref(), Some(&mut api), |api, m| { - let id = api.create_account_path(m, label)?; - if self.get_data().is_none() { - return Err(Error::GenericError("No wallet data".to_string())); - } - let current_height = self.get_data().unwrap().info.last_confirmed_height; - if let Some(spendable_amount) = self.account_balance(current_height, api, m) { - let mut w_data = self.accounts.write(); - w_data.push(WalletAccount { - spendable_amount, - label: label.clone(), - path: id.to_bip_32_string(), - }); - w_data.sort_by_key(|w| w.label != label.clone()); - } - Ok(()) - }) - } - - /// Set active account from provided label. - pub fn set_active_account(&self, label: &String) -> Result<(), Error> { - // Stop service from previous account. - let cur_service_id = self.identifier(); - Tor::stop_service(&cur_service_id); - - // Clear secret key for previous account. - { - let mut w_key = self.secret_key.write(); - *w_key = None; - } - - // Set new active account. - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let mut api = Owner::new(instance.clone(), None); - controller::owner_single_use(None, self.keychain_mask().as_ref(), Some(&mut api), |api, m| { - api.set_active_account(m, label)?; - self.account_time.store(Utc::now().timestamp(), Ordering::Relaxed); - Ok(()) - })?; - - // Update Slatepack address and secret key. - self.update_secret_key_addr()?; - - // Save account label into config. - let mut w_config = self.config.write(); - w_config.account = label.to_owned(); - w_config.save(); - - // Clear wallet info. - let mut w_data = self.data.write(); - *w_data = None; - - // Reset progress values. - self.info_sync_progress.store(0, Ordering::Relaxed); - - // Sync wallet data. - self.sync(); - Ok(()) - } - - /// Calculate current account balance. - fn account_balance( - &self, - current_height: u64, - o: &mut Owner, HTTPNodeClient, ExtKeychain>, - m: Option<&SecretKey>) - -> Option { - if let Ok(outputs) = o.retrieve_outputs(m, false, false, None) { - let mut spendable = 0; - let min_confirmations = self.get_config().min_confirmations; - for out_mapping in outputs.1 { - let out = out_mapping.output; - if out.status == grin_wallet_libwallet::OutputStatus::Unspent { - if !out.is_coinbase || out.lock_height <= current_height - || out.num_confirmations(current_height) >= min_confirmations { - spendable += out.value; - } - } - } - return Some(spendable); - } - None - } - - /// Get list of accounts for the wallet. - pub fn accounts(&self) -> Vec { - self.accounts.read().clone() - } - - /// Get wallet data. - pub fn get_data(&self) -> Option { - let r_data = self.data.read(); - r_data.clone() - } - - /// Load more transactions at list by increasing limit. - pub fn load_more_txs(&self) { - self.more_txs_loading.store(true, Ordering::Relaxed); - let wallet = self.clone(); - thread::spawn(move || { - // Wait when current sync will be finished. - while wallet.syncing() { - thread::sleep(Duration::from_secs(1)); - } - // Sync wallet data with new limit. - { - let mut w_data = wallet.data.write(); - if w_data.is_some() { - w_data.as_mut().unwrap().txs_limit += WalletData::TXS_LIMIT; - } - } - sync_wallet_data(&wallet, false); - wallet.more_txs_loading.store(false, Ordering::Relaxed); - }); - } - - /// Check if more transaction are loading. - pub fn more_txs_loading(&self) -> bool { - self.more_txs_loading.load(Ordering::Relaxed) - } - - /// Sync wallet data from node at sync thread or locally synchronously. - pub fn sync(&self) { - let thread_r = self.sync_thread.read(); - if let Some(thread) = thread_r.as_ref() { - thread.unpark(); - } - } - - /// Check if wallet is syncing. - pub fn syncing(&self) -> bool { - self.syncing.load(Ordering::Relaxed) - } - - /// Get running Foreign API server port. - pub fn foreign_api_port(&self) -> Option { - let r_api = self.foreign_api_server.read(); - if r_api.is_some() { - let api = r_api.as_ref().unwrap(); - return Some(api.1); - } - None - } - - /// Check if Slatepack message is opening. - pub fn message_opening(&self) -> bool { - self.message_opening.load(Ordering::Relaxed) - } - - /// Parse Slatepack message into [`Slate`]. - pub fn parse_slatepack(&self, text: &String) - -> Result<(Slate, Option), grin_wallet_controller::Error> { - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let mut api = Owner::new(instance, None); - match parse_slatepack(&mut api, self.keychain_mask().as_ref(), None, Some(text.clone())) { - Ok(s) => Ok(s), - Err(e) => Err(e) - } - } - - /// Create Slatepack message from provided slate. - fn create_slatepack_message(&self, slate: &Slate, dest: Option) - -> Result { - let mut message = "".to_string(); - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let mut api = Owner::new(instance, None); - controller::owner_single_use(None, self.keychain_mask().as_ref(), Some(&mut api), |api, m| { - let recipients = match dest { - Some(a) => vec![a], - None => vec![], - }; - message = api.create_slatepack_message(m, &slate, Some(0), recipients)?; - Ok(()) - })?; - - // Write Slatepack message to file. - let slatepack_dir = self.get_config().get_slate_path(&slate); - let mut output = File::create(slatepack_dir)?; - output.write_all(message.as_bytes())?; - output.sync_all()?; - Ok(message) - } - - /// Check if Slatepack file exists. - pub fn slatepack_exists(&self, slate: &Slate) -> bool { - let slatepack_path = self.get_config().get_slate_path(slate); - fs::exists(slatepack_path).unwrap_or(false) - } - - /// Calculate transaction fee for provided amount. - fn calculate_fee(&self, a: u64) -> Result { - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let mut w_lock = instance.lock(); - let w = w_lock.lc_provider()?.wallet_inst()?; - let config = self.get_config(); - let args = InitTxArgs { - src_acct_name: Some(config.account.clone()), - amount: a, - minimum_confirmations: config.min_confirmations, - num_change_outputs: 1, - selection_strategy_is_use_all: false, - estimate_only: Some(true), - ..Default::default() - }; - let res = init_send_tx(&mut **w, self.keychain_mask().as_ref(), args, false); - match res { - Ok(slate) => { - Ok(slate.fee_fields.fee()) - } - Err(e) => { - match e { - Error::NotEnoughFunds { available, needed, .. } => { - Ok(needed - available) - }, - e => { - Err(e) - } - } - } - } - } - - /// Check if transaction fee is calculating. - pub fn fee_calculating(&self) -> bool { - self.fee_calculating.load(Ordering::Relaxed) > 0 - } - - /// Initialize a transaction to send amount. - fn send(&self, a: u64, dest: Option) -> Result { - let config = self.get_config(); - let args = InitTxArgs { - payment_proof_recipient_address: dest.clone(), - src_acct_name: Some(config.account), - amount: a, - minimum_confirmations: config.min_confirmations, - num_change_outputs: 1, - selection_strategy_is_use_all: false, - ..Default::default() - }; - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let mut api = Owner::new(instance, None); - let mut slate = None; - let keychain_mask = self.keychain_mask(); - controller::owner_single_use(None, keychain_mask.as_ref(), Some(&mut api), |api, m| { - let s = api.init_send_tx(m, args)?; - // Create Slatepack message response. - let _ = self.create_slatepack_message(&s, dest)?; - // Lock outputs to for this transaction. - api.tx_lock_outputs(m, &s)?; - slate = Some(s); - Ok(()) - })?; - if let Some(slate) = slate { - Ok(slate) - } else { - Err(Error::GenericError("slate was not created".to_string())) - } - } - - /// Send slate to Tor address. - async fn send_tor(&self, id: u32, s: &Slate, addr: &SlatepackAddress) -> Result { - self.on_tx_action(id, Some(WalletTxAction::SendingTor)); - - let tor_addr = OnionV3Address::try_from(addr).unwrap().to_http_str(); - let url = format!("{}/v2/foreign", tor_addr); - let slate_send = VersionedSlate::into_version(s.clone(), SlateVersion::V4)?; - let body = json!({ - "jsonrpc": "2.0", - "method": "receive_tx", - "id": 1, - "params": [ - slate_send, - null, - null - ] - }).to_string(); - // Wait Tor service to launch. - while Tor::is_service_starting(&self.identifier()) { - tokio::time::sleep(Duration::from_secs(1)).await; - } - // Send request to receiver. - let req_res = Tor::post(body, url).await; - if req_res.is_none() { - return Err(Error::GenericError("Tor request error".to_string())); - } - - // Parse response. - if let Ok(res) = serde_json::from_str::(&req_res.unwrap()) { - if res["error"] != json!(null) { - return Err(Error::GenericError("Response error".to_string())); - } - let slate_value = res["result"]["Ok"].clone(); - if let Ok(res) = &serde_json::to_string::(&slate_value) { - let res = Slate::deserialize_upgrade(res); - return res; - } - } - Err(Error::GenericError("Parse error".to_string())) - } - - /// Check if request to send funds is creating. - pub fn send_creating(&self) -> bool { - self.send_creating.load(Ordering::Relaxed) - } - - /// Initialize an invoice transaction to receive amount, return request for funds sender. - fn issue_invoice(&self, amount: u64) -> Result { - let args = IssueInvoiceTxArgs { - dest_acct_name: None, - amount, - target_slate_version: None, - }; - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let api = Owner::new(instance, None); - let slate = api.issue_invoice_tx(self.keychain_mask().as_ref(), args)?; - - // Create Slatepack message response. - let _ = self.create_slatepack_message(&slate, None)?; - - Ok(slate) - } - - /// Handle message from the invoice issuer to send founds, return response for funds receiver. - fn pay(&self, slate: &Slate) -> Result { - let config = self.get_config(); - let args = InitTxArgs { - src_acct_name: None, - amount: slate.amount, - minimum_confirmations: config.min_confirmations, - selection_strategy_is_use_all: false, - ..Default::default() - }; - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let api = Owner::new(instance, None); - let slate = api.process_invoice_tx(self.keychain_mask().as_ref(), &slate, args)?; - api.tx_lock_outputs(self.keychain_mask().as_ref(), &slate)?; - - // Create Slatepack message response. - let _ = self.create_slatepack_message(&slate, None)?; - - Ok(slate) - } - - /// Check if request to receive funds is creating. - pub fn invoice_creating(&self) -> bool { - self.invoice_creating.load(Ordering::Relaxed) - } - - /// Create response to sender to receive funds. - fn receive(&self, slate: &Slate, dest: Option) -> Result { - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let api = Owner::new(instance, None); - let mut slate = slate.clone(); - controller::foreign_single_use(api.wallet_inst.clone(), self.keychain_mask(), |api| { - slate = api.receive_tx(&slate, Some(self.get_config().account.as_str()), None)?; - Ok(()) - })?; - - // Create Slatepack message response. - let _ = self.create_slatepack_message(&slate, dest)?; - - Ok(slate) - } - - /// Finalize transaction from provided message as sender or invoice issuer. - fn finalize(&self, slate: &Slate, id: u32) -> Result { - self.on_tx_action(id, Some(WalletTxAction::Finalizing)); - - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let api = Owner::new(instance, None); - let mut slate = slate.clone(); - controller::foreign_single_use(api.wallet_inst.clone(), self.keychain_mask(), |api| { - slate = api.finalize_tx(&slate, false)?; - Ok(()) - })?; - - // Save Slatepack message to file. - let _ = self.create_slatepack_message(&slate, None)?; - - // Clear tx action. - self.on_tx_action(id, None); - - Ok(slate) - } - - /// Post transaction to blockchain. - fn post(&self, slate: &Slate, id: Option) -> Result<(), Error> { - if let Some(id) = id { - self.on_tx_action(id, Some(WalletTxAction::Posting)); - } - - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let mut api = Owner::new(instance, None); - controller::owner_single_use(None, self.keychain_mask().as_ref(), Some(&mut api), |api, m| { - api.post_tx(m, &slate, self.can_use_dandelion())?; - Ok(()) - })?; - - // Clear tx action. - if let Some(id) = id { - self.on_tx_action(id, None); - } - Ok(()) - } - - /// Cancel transaction. - fn cancel(&self, id: u32) -> Result<(), Error> { - self.on_tx_action(id, Some(WalletTxAction::Cancelling)); - - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - cancel_tx(instance, self.keychain_mask().as_ref(), &None, Some(id), None)?; - - // Clear tx action. - self.on_tx_action(id, None); - - Ok(()) - } - - /// Update transaction action status. - fn on_tx_action(&self, id: u32, action: Option) { - let mut w_data = self.data.write(); - if let Some(data) = w_data.as_mut() { - data.on_tx_action(id, action); - } - } - - /// Update transaction action error status. - fn on_tx_error(&self, id: u32, err: Option) { - let mut w_data = self.data.write(); - if let Some(data) = w_data.as_mut() { - data.on_tx_error(id, err); - } - } - - /// Save task result to consume later. - fn on_task_result(&self, tx: Option, task: &WalletTask) { - let mut w_res = self.task_result.write(); - let id = if let Some(t) = tx { - Some(t.id) - } else { - None - }; - *w_res = Some((id, task.clone())); - } - - /// Consume result of successful task. - pub fn consume_task_result(&self) -> Option<(Option, WalletTask)> { - let res = { - let r_res = self.task_result.read(); - r_res.clone() - }; - // Clear result for task. - let mut w_res = self.task_result.write(); - *w_res = None; - res - } - - /// Get possible transaction confirmation height. - fn tx_height(&self, tx: &WalletTx) -> Result, Error> { - let mut tx_height = None; - if tx.data.confirmed && tx.data.kernel_excess.is_some() { - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let mut w_lock = instance.lock(); - let w = w_lock.lc_provider()?.wallet_inst()?; - if let Ok(res) = w.w2n_client().get_kernel( - tx.data.kernel_excess.as_ref().unwrap(), - tx.data.kernel_lookup_min_height, - None - ) { - tx_height = Some(match res { - None => 0, - Some((_, h, _)) => h - }); - } - } else if tx.broadcasting() { - tx_height = match self.get_data() { - None => None, - Some(data) => Some(data.info.last_confirmed_height) - }; - } - Ok(tx_height) - } - - /// Get stored transaction Slate. - fn get_tx_slate(&self, tx_id: Option, slate_id: Option<&Uuid>) -> Option { - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let api = Owner::new(instance, None); - if let Ok(s) = api.get_stored_tx(self.keychain_mask().as_ref(), tx_id, slate_id) { - return s; - } - None - } - - /// Delete transaction from database. - fn delete_tx(&self, id: u32) -> Result<(), Error> { - self.on_tx_action(id, Some(WalletTxAction::Deleting)); - - let slate = self.get_tx_slate(Some(id), None); - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let keychain_mask = self.keychain_mask(); - let mut wallet_lock = instance.lock(); - let lc = wallet_lock.lc_provider()?; - let w = lc.wallet_inst()?; - let parent_key = w.parent_key_id(); - let mut batch = w.batch(keychain_mask.as_ref())?; - batch.delete_tx_log_entry(id, &parent_key)?; - batch.commit()?; - - // Delete transaction files. - if let Some(s) = slate { - let slatepack_path = self.get_config().get_slate_path(&s); - fs::remove_file(&slatepack_path).unwrap_or_default(); - let path = path::Path::new(&self.get_config().get_data_path()) - .join("saved_txs") - .join(format!("{}.grintx", s.id)); - fs::remove_file(&path).unwrap_or_default(); - } - Ok(()) - } - - /// Change wallet password. - pub fn change_password(&self, old: String, new: String) -> Result<(), Error> { - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let mut wallet_lock = instance.lock(); - let lc = wallet_lock.lc_provider()?; - lc.change_password(None, ZeroingString::from(old), ZeroingString::from(new)) - } - - /// Initiate wallet repair by scanning its outputs. - pub fn repair(&self) { - self.repair_needed.store(true, Ordering::Relaxed); - self.sync(); - } - - /// Check if wallet is repairing. - pub fn is_repairing(&self) -> bool { - self.repair_needed.load(Ordering::Relaxed) - } - - /// Get wallet repairing progress. - pub fn repairing_progress(&self) -> u8 { - self.repair_progress.load(Ordering::Relaxed) - } - - /// Change wallet data path, migrating all files to new directory. - pub fn change_data_path(&self, path: String) { - let wallet = self.clone(); - wallet.files_moving.store(true, Ordering::Relaxed); - // Close wallet if open. - if self.is_open() { - self.close(); - } - thread::spawn(move || { - // Wait wallet to be closed. - while wallet.is_open() || wallet.syncing() { - thread::sleep(Duration::from_millis(100)); - } - // Move wallet db files. - if let Some(old_path) = wallet.get_config().data_path { - let mut old = PathBuf::from(old_path.as_str()); - old.push(WalletConfig::DATA_DIR_NAME); - let mut new = PathBuf::from(path.as_str()); - new.push(WalletConfig::DATA_DIR_NAME); - if old.exists() { - fs::create_dir_all(&new).unwrap_or_default(); - if let Ok(_) = fs::rename(old.as_path(), new.as_path()) { - // Save new path to config. - let mut w_config = wallet.config.write(); - w_config.data_path = Some(path); - w_config.save(); - } - } - } - wallet.files_moving.store(false, Ordering::Relaxed); - // Mark wallet to reopen. - if !wallet.is_open() { - wallet.set_reopen(true); - } - }); - } - - /// Deleting wallet database files. - pub fn delete_db(&self) { - let wallet = self.clone(); - wallet.files_moving.store(true, Ordering::Relaxed); - // Close wallet if open. - if self.is_open() { - self.close(); - } - thread::spawn(move || { - // Wait wallet to be closed. - while wallet.is_open() || wallet.syncing() { - thread::sleep(Duration::from_millis(100)); - } - // Remove wallet db files. - let _ = fs::remove_dir_all(wallet.get_config().get_db_path()); - wallet.files_moving.store(false, Ordering::Relaxed); - // Mark wallet to repair. - wallet.repair(); - // Mark wallet to reopen. - if !wallet.is_open() { - wallet.set_reopen(true); - } - }); - } - - /// Check if data files are moving. - pub fn files_moving(&self) -> bool { - self.files_moving.load(Ordering::Relaxed) - } - - /// Retrieve payment proof. - pub fn get_payment_proof(&self, tx_id: Option, slate_id: Option) - -> Result, Error> { - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let key_mask = self.keychain_mask(); - let mut api = Owner::new(instance, None); - let mut proof = None; - controller::owner_single_use(None, key_mask.as_ref(), Some(&mut api), |api, m| { - let result = api.retrieve_payment_proof(m, false, tx_id, slate_id); - proof = match result { - Ok(p) => Some(p), - Err(e) => { - error!("retrieve_payment_proof error: {}", e); - None - } - }; - Ok(()) - })?; - Ok(proof) - } - - /// Verify payment proof. - fn verify_payment_proof(&self, proof: &PaymentProof) -> Result<(u32, bool, bool), Error> { - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let keychain_mask = self.keychain_mask(); - let verify_res = verify_payment_proof(instance.clone(), keychain_mask.as_ref(), proof); - let res = match verify_res { - Ok((send, rec)) => { - // Update proof at local database for valid proof. - if send || rec { - let mut wallet_lock = instance.lock(); - let lc = wallet_lock.lc_provider()?; - let w = lc.wallet_inst()?; - // Find wallet transaction to update or create. - let txs = w.tx_log_iter().filter(|entry| { - if let Some(excess) = entry.kernel_excess { - return excess == proof.excess; - } - false - }).collect::>(); - if let Some(tx) = txs.get(0) { - let mut tx = tx.clone(); - let mut batch = w.batch(keychain_mask.as_ref())?; - let parent_key = &tx.parent_key_id; - tx.payment_proof = Some(StoredProofInfo { - receiver_address: proof.recipient_address.pub_key, - receiver_signature: Some(proof.recipient_sig), - sender_address_path: 0, - sender_address: proof.sender_address.pub_key, - sender_signature: Some(proof.sender_sig), - }); - batch.save_tx_log_entry(tx.clone(), &parent_key)?; - batch.commit()?; - Ok((tx.id, send, rec)) - } else { - let parent_key = w.parent_key_id(); - let mut batch = w.batch(keychain_mask.as_ref())?; - let log_id = batch.next_tx_log_id(&parent_key)?; - let log_type = TxLogEntryType::TxSent; - let mut tx = TxLogEntry::new(parent_key.clone(), log_type, log_id); - tx.amount_debited = proof.amount; - tx.kernel_excess = Some(proof.excess); - tx.tx_type = TxLogEntryType::TxSent; - tx.confirmed = true; - tx.payment_proof = Some(StoredProofInfo { - receiver_address: proof.recipient_address.pub_key, - receiver_signature: Some(proof.recipient_sig), - sender_address_path: 0, - sender_address: proof.sender_address.pub_key, - sender_signature: Some(proof.sender_sig), - }); - batch.save_tx_log_entry(tx.clone(), &parent_key)?; - batch.commit()?; - Ok((tx.id, send, rec)) - } - } else { - Ok((0, send, rec)) - } - } - Err(e) => Err(e) - }; - // Sync wallet data on success. - if res.is_ok() { - sync_wallet_data(self, false); - } - res - } - - /// Check if payment proof is verifying. - pub fn payment_proof_verifying(&self) -> bool { - self.proof_verifying.load(Ordering::Relaxed) - } - - /// Get recovery phrase. - pub fn get_recovery(&self, password: String) -> Result { - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let mut wallet_lock = instance.lock(); - let lc = wallet_lock.lc_provider()?; - lc.get_mnemonic(None, ZeroingString::from(password)) - } - - /// Close the wallet, delete its files and mark it as deleted. - pub fn delete_wallet(&self) { - if self.is_open() { - self.close(); - } - // Mark wallet as deleted. - let wallet_delete = self.clone(); - wallet_delete.deleted.store(true, Ordering::Relaxed); - - thread::spawn(move || { - // Wait wallet to be closed. - if wallet_delete.is_open() { - thread::sleep(Duration::from_millis(100)); - } - // Remove wallet files. - let _ = fs::remove_dir_all(wallet_delete.get_config().get_wallet_path()); - // Mark wallet as deleted. - wallet_delete.deleted.store(true, Ordering::Relaxed); - // Start sync to close thread. - wallet_delete.sync(); - }); - } - - /// Check if wallet was deleted to remove it from list. - pub fn is_deleted(&self) -> bool { - self.deleted.load(Ordering::Relaxed) - } + /// Create new [`Wallet`] instance with provided [`WalletConfig`]. + fn new(config: WalletConfig) -> Self { + let connection = config.connection(); + Self { + config: Arc::new(RwLock::new(config)), + instance: Arc::new(RwLock::new(None)), + connection: Arc::new(RwLock::new(connection)), + keychain_mask: Arc::new(RwLock::new(None)), + slatepack_address: Arc::new(RwLock::new(None)), + accounts: Arc::new(RwLock::new(vec![])), + account_time: Arc::new(Default::default()), + sync_thread: Arc::from(RwLock::new(None)), + syncing: Arc::new(AtomicBool::new(false)), + info_sync_progress: Arc::from(AtomicU8::new(0)), + sync_error: Arc::from(AtomicBool::new(false)), + sync_attempts: Arc::new(AtomicU8::new(0)), + data: Arc::new(RwLock::new(None)), + from_node: Arc::new(AtomicBool::new(false)), + more_txs_loading: Arc::new(AtomicBool::new(false)), + reopen: Arc::new(AtomicBool::new(false)), + is_open: Arc::from(AtomicBool::new(false)), + closing: Arc::new(AtomicBool::new(false)), + deleted: Arc::new(AtomicBool::new(false)), + foreign_api_server: Arc::new(RwLock::new(None)), + secret_key: Arc::new(RwLock::new(None)), + repair_needed: Arc::new(AtomicBool::new(false)), + repair_progress: Arc::new(AtomicU8::new(0)), + files_moving: Arc::new(AtomicBool::new(false)), + message_opening: Arc::new(AtomicBool::from(false)), + send_creating: Arc::new(AtomicBool::new(false)), + fee_calculating: Arc::new(AtomicU8::new(0)), + invoice_creating: Arc::new(AtomicBool::new(false)), + proof_verifying: Arc::new(AtomicBool::new(false)), + tasks_sender: Arc::new(RwLock::new(None)), + task_result: Arc::new(RwLock::new(None)), + } + } + + /// Create new wallet. + pub fn create( + name: &String, + password: &ZeroingString, + mnemonic: &Mnemonic, + conn_method: &ConnectionMethod, + ) -> Result { + let config = WalletConfig::create(name.clone(), conn_method); + let w = Wallet::new(config.clone()); + { + // Wallet directory setup. + let mut path = PathBuf::from(config.get_data_path()); + path.push(WalletConfig::DATA_DIR_NAME); + fs::create_dir_all(&path) + .map_err(|_| Error::IO("Directory creation error".to_string()))?; + // Create seed file. + let _ = WalletSeed::init_file( + config.seed_path().as_str(), + ZeroingString::from(mnemonic.get_phrase()), + password.clone(), + ) + .map_err(|_| Error::IO("Seed file creation error".to_string()))?; + let node_client = Self::create_node_client(&config)?; + let mut wallet: LMDBBackend<'static, HTTPNodeClient, ExtKeychain> = + match LMDBBackend::new(path.to_str().unwrap(), node_client) { + Err(_) => { + return Err(Error::Lifecycle("DB creation error".to_string()).into()); + } + Ok(d) => d, + }; + // Save init status of this wallet, to determine whether it needs a full UTXO scan. + let mut batch = wallet.batch_no_mask()?; + match mnemonic.mode() { + PhraseMode::Generate => batch.save_init_status(WalletInitStatus::InitNoScanning)?, + PhraseMode::Import => { + batch.save_init_status(WalletInitStatus::InitNeedsScanning)? + } + } + batch.commit()?; + } + Ok(w) + } + + /// Initialize [`Wallet`] from provided data path. + pub fn init(data_path: PathBuf) -> Option { + let wallet_config = WalletConfig::load(data_path); + if let Some(config) = wallet_config { + return Some(Wallet::new(config)); + } + None + } + + /// Create [`HTTPNodeClient`] from provided config. + fn create_node_client(config: &WalletConfig) -> Result { + let integrated = || { + let api_url = format!("http://{}", NodeConfig::get_api_address()); + let api_secret = NodeConfig::get_api_secret(true); + (api_url, api_secret) + }; + let (node_api_url, node_secret) = if let Some(id) = config.ext_conn_id { + if let Some(conn) = ConnectionsConfig::ext_conn(id) { + (conn.url, conn.secret) + } else { + integrated() + } + } else { + integrated() + }; + let client = if AppConfig::use_proxy() { + let socks = AppConfig::use_socks_proxy(); + let url = if socks { + AppConfig::socks_proxy_url() + } else { + AppConfig::http_proxy_url() + } + .unwrap_or("".to_string()) + .replace("http://", "") + .replace("socks5://", ""); + + // Convert URL to SocketAddr. + let addr_res = match SocketAddr::from_str(url.as_str()) { + Ok(ip_addr) => Some(ip_addr), + Err(_) => { + if let Ok(mut socket_addr_list) = url.to_socket_addrs() { + if let Some(addr) = socket_addr_list.next() { + Some(addr) + } else { + None + } + } else { + None + } + } + }; + + match addr_res { + None => HTTPNodeClient::new(&node_api_url, node_secret)?, + Some(addr) => { + let scheme = if socks { "socks5://" } else { "http://" }; + HTTPNodeClient::new_proxy(&node_api_url, node_secret, Some((addr, scheme)))? + } + } + } else { + HTTPNodeClient::new(&node_api_url, node_secret)? + }; + Ok(client) + } + + /// Create [`WalletInstance`] from provided [`WalletConfig`]. + fn create_wallet_instance(config: &mut WalletConfig) -> Result { + // Setup node client. + let node_client = Self::create_node_client(config)?; + + // Create wallet instance. + let wallet = Self::inst_wallet::< + DefaultLCProvider, + HTTPNodeClient, + ExtKeychain, + >(config, node_client)?; + Ok(wallet) + } + + /// Instantiate [`WalletInstance`] from provided node client and [`WalletConfig`]. + fn inst_wallet( + config: &mut WalletConfig, + node_client: C, + ) -> Result>>>, Error> + where + DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: Keychain + 'static, + { + let mut wallet = Box::new(DefaultWalletImpl::<'static, C>::new(node_client).unwrap()) + as Box>; + let lc = wallet.lc_provider()?; + lc.set_top_level_directory(config.get_data_path().as_str())?; + Ok(Arc::new(Mutex::new(wallet))) + } + + /// Open the wallet and start [`WalletData`] sync at separate thread. + pub fn open(&self, password: ZeroingString) -> Result<(), Error> { + if self.is_open() { + return Err(Error::GenericError("Already opened".to_string())); + } + + // Create new wallet instance if sync thread was stopped or instance was not created. + let has_instance = { + let r_inst = self.instance.as_ref().read(); + r_inst.is_some() + }; + if self.sync_thread.read().is_none() || !has_instance { + let mut config = self.get_config(); + // Setup current connection. + { + let mut w_conn = self.connection.write(); + *w_conn = config.connection(); + } + let new_instance = Self::create_wallet_instance(&mut config)?; + let mut w_inst = self.instance.write(); + *w_inst = Some(new_instance); + } + + // Open the wallet. + { + let instance = { + let r_inst = self.instance.as_ref().read(); + r_inst.clone().unwrap() + }; + let mut wallet_lock = instance.lock(); + let lc = wallet_lock.lc_provider()?; + match lc.open_wallet(None, password, true, false) { + Ok(m) => { + { + let mut w_mask = self.keychain_mask.write(); + *w_mask = m; + } + // Reset an error on opening. + self.set_sync_error(false); + self.reset_sync_attempts(); + + // Set current account. + let wallet_inst = lc.wallet_inst()?; + let label = self.get_config().account.to_owned(); + wallet_inst.set_parent_key_id_by_name(label.as_str())?; + self.account_time + .store(Utc::now().timestamp(), Ordering::Relaxed); + + // Start new synchronization thread or wake up existing one. + let mut thread_w = self.sync_thread.write(); + if thread_w.is_none() { + let thread = start_sync(self.clone()); + *thread_w = Some(thread); + } else { + thread_w.clone().unwrap().unpark(); + } + self.is_open.store(true, Ordering::Relaxed); + } + Err(e) => { + if !self.syncing() { + let mut w_inst = self.instance.write(); + *w_inst = None; + } + return Err(e); + } + } + } + + // Update Slatepack address and secret key. + self.update_secret_key_addr()?; + + Ok(()) + } + + /// Get keychain mask [`SecretKey`]. + pub fn keychain_mask(&self) -> Option { + let r_key = self.keychain_mask.read(); + r_key.clone() + } + + /// Get wallet [`SecretKey`] for transport. + pub fn secret_key(&self) -> Option { + let r_key = self.secret_key.read(); + r_key.clone() + } + + /// Retrieve wallet [`SecretKey`] and Slatepack address for transport. + fn update_secret_key_addr(&self) -> Result<(), Error> { + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let mut w_lock = instance.lock(); + let lc = w_lock.lc_provider()?; + let w_inst = lc.wallet_inst()?; + let k = w_inst.keychain(self.keychain_mask().as_ref())?; + let parent_key_id = w_inst.parent_key_id(); + let sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0) + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + let addr = SlatepackAddress::try_from(&sec_key)?; + let mut w_key = self.secret_key.write(); + *w_key = Some(sec_key); + let mut w_address = self.slatepack_address.write(); + *w_address = Some(addr.to_string()); + Ok(()) + } + + /// Get unique opened wallet identifier, including current account. + pub fn identifier(&self) -> String { + let config = self.get_config(); + let account_ts = self.account_time.load(Ordering::Relaxed); + format!("{}_{}_{}", config.id, config.account.to_hex(), account_ts) + } + + /// Get Slatepack address to receive txs at transport. + pub fn slatepack_address(&self) -> Option { + let r_address = self.slatepack_address.read(); + if r_address.is_some() { + let addr = r_address.clone(); + return addr; + } + None + } + + /// Get wallet config. + pub fn get_config(&self) -> WalletConfig { + self.config.read().clone() + } + + /// Change wallet name. + pub fn change_name(&self, name: String) { + let mut w_config = self.config.write(); + w_config.name = name; + w_config.save(); + } + + /// Check if start of Tor listener on wallet opening is needed. + pub fn auto_start_tor_listener(&self) -> bool { + let r_config = self.config.read(); + r_config.enable_tor_listener.unwrap_or(true) + } + + /// Update start of Tor listener on wallet opening. + pub fn update_auto_start_tor_listener(&self, start: bool) { + let mut w_config = self.config.write(); + w_config.enable_tor_listener = Some(start); + w_config.save(); + } + + /// Check if Dandelion usage is needed to post transactions. + pub fn can_use_dandelion(&self) -> bool { + let r_config = self.config.read(); + r_config.use_dandelion.unwrap_or(true) + } + + /// Update usage of Dandelion to post transactions. + pub fn update_use_dandelion(&self, use_dandelion: bool) { + let mut w_config = self.config.write(); + w_config.use_dandelion = Some(use_dandelion); + w_config.save(); + } + + /// Update minimal amount of confirmations. + pub fn update_min_confirmations(&self, min_confirmations: u64) { + let mut w_config = self.config.write(); + w_config.min_confirmations = min_confirmations; + w_config.save(); + } + + /// Get transaction broadcasting delay in blocks. + pub fn broadcasting_delay(&self) -> u64 { + let r_config = self.config.read(); + r_config + .tx_broadcast_timeout + .unwrap_or(WalletConfig::BROADCASTING_TIMEOUT_DEFAULT) + } + + /// Update transaction broadcasting delay in blocks. + pub fn update_broadcasting_delay(&self, delay: u64) { + let mut w_config = self.config.write(); + w_config.tx_broadcast_timeout = Some(delay); + w_config.save(); + } + + /// Update external connection identifier. + pub fn update_connection(&self, conn: &ConnectionMethod) { + let mut w_config = self.config.write(); + w_config.ext_conn_id = match conn { + ConnectionMethod::Integrated => None, + ConnectionMethod::External(id, _) => Some(id.clone()), + }; + w_config.save(); + } + + /// Get external connection URL applied to [`WalletInstance`] + /// after wallet opening if sync is running or get it from config. + pub fn get_current_connection(&self) -> ConnectionMethod { + if self.sync_thread.read().is_some() { + let r_conn = self.connection.read(); + r_conn.clone() + } else { + let config = self.get_config(); + config.connection() + } + } + + /// Check if wallet is open. + pub fn is_open(&self) -> bool { + self.is_open.load(Ordering::Relaxed) + } + + /// Check if wallet is closing. + pub fn is_closing(&self) -> bool { + self.closing.load(Ordering::Relaxed) + } + + /// Close the wallet. + pub fn close(&self) { + let has_instance = { + let r_inst = self.instance.read(); + r_inst.is_some() + }; + if !self.is_open() || !has_instance { + return; + } + // Stop repairing. + if self.is_repairing() { + self.repair_needed.store(false, Ordering::Relaxed); + } + // Close wallet at separate thread. + let wallet_close = self.clone(); + let service_id = wallet_close.identifier(); + let conn = wallet_close.connection.clone(); + thread::spawn(move || { + wallet_close.closing.store(true, Ordering::Relaxed); + // Wait common operations to finish. + while wallet_close.message_opening() + || wallet_close.send_creating() + || wallet_close.invoice_creating() + { + thread::sleep(Duration::from_millis(300)); + } + // Stop running API server. + let api_server_exists = { wallet_close.foreign_api_server.read().is_some() }; + if api_server_exists { + let mut w_api_server = wallet_close.foreign_api_server.write(); + w_api_server.as_mut().unwrap().0.stop(); + *w_api_server = None; + } + // Stop running Tor service. + Tor::stop_service(&service_id); + // Close the wallet. + let r_inst = wallet_close.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + Self::close_wallet(&instance); + wallet_close.closing.store(false, Ordering::Relaxed); + wallet_close.is_open.store(false, Ordering::Relaxed); + // Setup current connection. + { + let mut w_conn = conn.write(); + *w_conn = wallet_close.get_config().connection(); + } + wallet_close.from_node.store(false, Ordering::Relaxed); + // Start sync to exit from thread. + wallet_close.sync(); + }); + } + + /// Close wallet for provided [`WalletInstance`]. + fn close_wallet(instance: &WalletInstance) { + let mut wallet_lock = instance.lock(); + let lc = wallet_lock.lc_provider().unwrap(); + let _ = lc.close_wallet(None); + } + + /// Set wallet reopen status. + pub fn set_reopen(&self, reopen: bool) { + self.reopen.store(reopen, Ordering::Relaxed); + } + + /// Check if wallet reopen is needed. + pub fn reopen_needed(&self) -> bool { + self.reopen.load(Ordering::Relaxed) + } + + /// Get wallet info synchronization progress. + pub fn info_sync_progress(&self) -> u8 { + self.info_sync_progress.load(Ordering::Relaxed) + } + + /// Check if wallet had an error on synchronization. + pub fn sync_error(&self) -> bool { + self.sync_error.load(Ordering::Relaxed) + } + + /// Set an error for wallet on synchronization. + pub fn set_sync_error(&self, error: bool) { + self.sync_error.store(error, Ordering::Relaxed); + } + + /// Check if wallet was synced from node after opening. + pub fn synced_from_node(&self) -> bool { + self.from_node.load(Ordering::Relaxed) + } + + /// Get current wallet synchronization attempts before setting an error. + fn get_sync_attempts(&self) -> u8 { + self.sync_attempts.load(Ordering::Relaxed) + } + + /// Increment wallet synchronization attempts before setting an error. + fn increment_sync_attempts(&self) { + let mut attempts = self.get_sync_attempts(); + attempts += 1; + self.sync_attempts.store(attempts, Ordering::Relaxed); + } + + /// Reset wallet synchronization attempts. + fn reset_sync_attempts(&self) { + self.sync_attempts.store(0, Ordering::Relaxed); + } + + /// Select transaction by slate id. + fn retrieve_tx_by_id(&self, id: Option, slate_id: Option) -> Option { + let r_inst = self.instance.as_ref().read(); + let inst = r_inst.clone().unwrap(); + let mask = self.keychain_mask(); + if let Ok((_, txs)) = retrieve_txs(inst, mask.as_ref(), &None, false, id, slate_id, None) { + if !txs.is_empty() { + return Some(txs.get(0).unwrap().clone()); + } + } + None + } + + /// Select transactions with provided limit. + fn retrieve_txs(&self, limit: u32) -> Result, Error> { + let r_inst = self.instance.as_ref().read(); + let inst = r_inst.clone().unwrap(); + let mut wallet_lock = inst.lock(); + let lc = wallet_lock.lc_provider()?; + let w = lc.wallet_inst()?; + let parent_key_id = w.parent_key_id(); + // Retrieve txs from database. + let txs: Vec = w + .tx_log_iter() + .filter(|tx_entry| tx_entry.parent_key_id == parent_key_id) + // Filter transactions to not show txs without slate (usually unspent outputs). + .filter(|tx| { + tx.tx_slate_id.is_some() || (tx.tx_slate_id.is_none() && tx.payment_proof.is_some()) + }) + .filter(|tx_entry| { + if tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxSentCancelled + { + BigInt::from(tx_entry.amount_debited) - BigInt::from(tx_entry.amount_credited) + >= BigInt::from(1) + } else { + BigInt::from(tx_entry.amount_credited) - BigInt::from(tx_entry.amount_debited) + >= BigInt::from(1) + } + }) + // Sort txs by creation date and confirmation status. + .sorted_by_key(|tx| -tx.creation_ts.timestamp()) + // Sort to show unconfirmed at top. + .sorted_by_key(|tx| { + tx.confirmed + || tx.tx_type == TxLogEntryType::TxReceivedCancelled + || tx.tx_type == TxLogEntryType::TxSentCancelled + || tx.tx_type == TxLogEntryType::TxReverted + }) + // Apply limit. + .take(limit as usize) + .collect(); + // Reverse an order. + // txs.reverse(); + Ok(txs) + } + + /// Send a task to the wallet. + pub fn task(&self, task: WalletTask) { + let r_tasks = self.tasks_sender.read(); + if r_tasks.is_some() { + match task { + WalletTask::CalculateFee(_, _) => { + let calculating = self.fee_calculating.load(Ordering::Relaxed); + self.fee_calculating + .store(calculating + 1, Ordering::Relaxed); + } + _ => {} + } + let _ = r_tasks.as_ref().unwrap().send(task); + } + } + + /// Create account into wallet. + pub fn create_account(&self, label: &String) -> Result<(), Error> { + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let mut api = Owner::new(instance, None); + controller::owner_single_use( + None, + self.keychain_mask().as_ref(), + Some(&mut api), + |api, m| { + let id = api.create_account_path(m, label)?; + if self.get_data().is_none() { + return Err(Error::GenericError("No wallet data".to_string())); + } + let current_height = self.get_data().unwrap().info.last_confirmed_height; + if let Some(spendable_amount) = self.account_balance(current_height, api, m) { + let mut w_data = self.accounts.write(); + w_data.push(WalletAccount { + spendable_amount, + label: label.clone(), + path: id.to_bip_32_string(), + }); + w_data.sort_by_key(|w| w.label != label.clone()); + } + Ok(()) + }, + ) + } + + /// Set active account from provided label. + pub fn set_active_account(&self, label: &String) -> Result<(), Error> { + // Stop service from previous account. + let cur_service_id = self.identifier(); + Tor::stop_service(&cur_service_id); + + // Clear secret key for previous account. + { + let mut w_key = self.secret_key.write(); + *w_key = None; + } + + // Set new active account. + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let mut api = Owner::new(instance.clone(), None); + controller::owner_single_use( + None, + self.keychain_mask().as_ref(), + Some(&mut api), + |api, m| { + api.set_active_account(m, label)?; + self.account_time + .store(Utc::now().timestamp(), Ordering::Relaxed); + Ok(()) + }, + )?; + + // Update Slatepack address and secret key. + self.update_secret_key_addr()?; + + // Save account label into config. + let mut w_config = self.config.write(); + w_config.account = label.to_owned(); + w_config.save(); + + // Clear wallet info. + let mut w_data = self.data.write(); + *w_data = None; + + // Reset progress values. + self.info_sync_progress.store(0, Ordering::Relaxed); + + // Sync wallet data. + self.sync(); + Ok(()) + } + + /// Calculate current account balance. + fn account_balance( + &self, + current_height: u64, + o: &mut Owner, HTTPNodeClient, ExtKeychain>, + m: Option<&SecretKey>, + ) -> Option { + if let Ok(outputs) = o.retrieve_outputs(m, false, false, None) { + let mut spendable = 0; + let min_confirmations = self.get_config().min_confirmations; + for out_mapping in outputs.1 { + let out = out_mapping.output; + if out.status == grin_wallet_libwallet::OutputStatus::Unspent { + if !out.is_coinbase + || out.lock_height <= current_height + || out.num_confirmations(current_height) >= min_confirmations + { + spendable += out.value; + } + } + } + return Some(spendable); + } + None + } + + /// Get list of accounts for the wallet. + pub fn accounts(&self) -> Vec { + self.accounts.read().clone() + } + + /// Get wallet data. + pub fn get_data(&self) -> Option { + let r_data = self.data.read(); + r_data.clone() + } + + /// Load more transactions at list by increasing limit. + pub fn load_more_txs(&self) { + self.more_txs_loading.store(true, Ordering::Relaxed); + let wallet = self.clone(); + thread::spawn(move || { + // Wait when current sync will be finished. + while wallet.syncing() { + thread::sleep(Duration::from_secs(1)); + } + // Sync wallet data with new limit. + { + let mut w_data = wallet.data.write(); + if w_data.is_some() { + w_data.as_mut().unwrap().txs_limit += WalletData::TXS_LIMIT; + } + } + sync_wallet_data(&wallet, false); + wallet.more_txs_loading.store(false, Ordering::Relaxed); + }); + } + + /// Check if more transaction are loading. + pub fn more_txs_loading(&self) -> bool { + self.more_txs_loading.load(Ordering::Relaxed) + } + + /// Sync wallet data from node at sync thread or locally synchronously. + pub fn sync(&self) { + let thread_r = self.sync_thread.read(); + if let Some(thread) = thread_r.as_ref() { + thread.unpark(); + } + } + + /// Check if wallet is syncing. + pub fn syncing(&self) -> bool { + self.syncing.load(Ordering::Relaxed) + } + + /// Get running Foreign API server port. + pub fn foreign_api_port(&self) -> Option { + let r_api = self.foreign_api_server.read(); + if r_api.is_some() { + let api = r_api.as_ref().unwrap(); + return Some(api.1); + } + None + } + + /// Check if Slatepack message is opening. + pub fn message_opening(&self) -> bool { + self.message_opening.load(Ordering::Relaxed) + } + + /// Parse Slatepack message into [`Slate`]. + pub fn parse_slatepack( + &self, + text: &String, + ) -> Result<(Slate, Option), grin_wallet_controller::Error> { + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let mut api = Owner::new(instance, None); + match parse_slatepack( + &mut api, + self.keychain_mask().as_ref(), + None, + Some(text.clone()), + ) { + Ok(s) => Ok(s), + Err(e) => Err(e), + } + } + + /// Create Slatepack message from provided slate. + fn create_slatepack_message( + &self, + slate: &Slate, + dest: Option, + ) -> Result { + let mut message = "".to_string(); + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let mut api = Owner::new(instance, None); + controller::owner_single_use( + None, + self.keychain_mask().as_ref(), + Some(&mut api), + |api, m| { + let recipients = match dest { + Some(a) => vec![a], + None => vec![], + }; + message = api.create_slatepack_message(m, &slate, Some(0), recipients)?; + Ok(()) + }, + )?; + + // Write Slatepack message to file. + let slatepack_dir = self.get_config().get_slate_path(&slate); + let mut output = File::create(slatepack_dir)?; + output.write_all(message.as_bytes())?; + output.sync_all()?; + Ok(message) + } + + /// Check if Slatepack file exists. + pub fn slatepack_exists(&self, slate: &Slate) -> bool { + let slatepack_path = self.get_config().get_slate_path(slate); + fs::exists(slatepack_path).unwrap_or(false) + } + + /// Calculate transaction fee for provided amount. + fn calculate_fee(&self, a: u64) -> Result { + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let mut w_lock = instance.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let config = self.get_config(); + let args = InitTxArgs { + src_acct_name: Some(config.account.clone()), + amount: a, + minimum_confirmations: config.min_confirmations, + num_change_outputs: 1, + selection_strategy_is_use_all: false, + estimate_only: Some(true), + ..Default::default() + }; + let res = init_send_tx(&mut **w, self.keychain_mask().as_ref(), args, false); + match res { + Ok(slate) => Ok(slate.fee_fields.fee()), + Err(e) => match e { + Error::NotEnoughFunds { + available, needed, .. + } => Ok(needed - available), + e => Err(e), + }, + } + } + + /// Check if transaction fee is calculating. + pub fn fee_calculating(&self) -> bool { + self.fee_calculating.load(Ordering::Relaxed) > 0 + } + + /// Initialize a transaction to send amount. + fn send(&self, a: u64, dest: Option) -> Result { + let config = self.get_config(); + let args = InitTxArgs { + payment_proof_recipient_address: dest.clone(), + src_acct_name: Some(config.account), + amount: a, + minimum_confirmations: config.min_confirmations, + num_change_outputs: 1, + selection_strategy_is_use_all: false, + ..Default::default() + }; + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let mut api = Owner::new(instance, None); + let mut slate = None; + let keychain_mask = self.keychain_mask(); + controller::owner_single_use(None, keychain_mask.as_ref(), Some(&mut api), |api, m| { + let s = api.init_send_tx(m, args)?; + // Create Slatepack message response. + let _ = self.create_slatepack_message(&s, dest)?; + // Lock outputs to for this transaction. + api.tx_lock_outputs(m, &s)?; + slate = Some(s); + Ok(()) + })?; + if let Some(slate) = slate { + Ok(slate) + } else { + Err(Error::GenericError("slate was not created".to_string())) + } + } + + /// Send slate to Tor address. + async fn send_tor(&self, id: u32, s: &Slate, addr: &SlatepackAddress) -> Result { + self.on_tx_action(id, Some(WalletTxAction::SendingTor)); + + let tor_addr = OnionV3Address::try_from(addr).unwrap().to_http_str(); + let url = format!("{}/v2/foreign", tor_addr); + let slate_send = VersionedSlate::into_version(s.clone(), SlateVersion::V4)?; + let body = json!({ + "jsonrpc": "2.0", + "method": "receive_tx", + "id": 1, + "params": [ + slate_send, + null, + null + ] + }) + .to_string(); + // Wait Tor service to launch. + while Tor::is_service_starting(&self.identifier()) { + tokio::time::sleep(Duration::from_secs(1)).await; + } + // Send request to receiver. + let req_res = Tor::post(body, url).await; + if req_res.is_none() { + return Err(Error::GenericError("Tor request error".to_string())); + } + + // Parse response. + if let Ok(res) = serde_json::from_str::(&req_res.unwrap()) { + if res["error"] != json!(null) { + return Err(Error::GenericError("Response error".to_string())); + } + let slate_value = res["result"]["Ok"].clone(); + if let Ok(res) = &serde_json::to_string::(&slate_value) { + let res = Slate::deserialize_upgrade(res); + return res; + } + } + Err(Error::GenericError("Parse error".to_string())) + } + + /// Check if request to send funds is creating. + pub fn send_creating(&self) -> bool { + self.send_creating.load(Ordering::Relaxed) + } + + /// Initialize an invoice transaction to receive amount, return request for funds sender. + fn issue_invoice(&self, amount: u64) -> Result { + let args = IssueInvoiceTxArgs { + dest_acct_name: None, + amount, + target_slate_version: None, + }; + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let api = Owner::new(instance, None); + let slate = api.issue_invoice_tx(self.keychain_mask().as_ref(), args)?; + + // Create Slatepack message response. + let _ = self.create_slatepack_message(&slate, None)?; + + Ok(slate) + } + + /// Handle message from the invoice issuer to send founds, return response for funds receiver. + fn pay(&self, slate: &Slate) -> Result { + let config = self.get_config(); + let args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: config.min_confirmations, + selection_strategy_is_use_all: false, + ..Default::default() + }; + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let api = Owner::new(instance, None); + let slate = api.process_invoice_tx(self.keychain_mask().as_ref(), &slate, args)?; + api.tx_lock_outputs(self.keychain_mask().as_ref(), &slate)?; + + // Create Slatepack message response. + let _ = self.create_slatepack_message(&slate, None)?; + + Ok(slate) + } + + /// Check if request to receive funds is creating. + pub fn invoice_creating(&self) -> bool { + self.invoice_creating.load(Ordering::Relaxed) + } + + /// Create response to sender to receive funds. + fn receive(&self, slate: &Slate, dest: Option) -> Result { + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let api = Owner::new(instance, None); + let mut slate = slate.clone(); + controller::foreign_single_use(api.wallet_inst.clone(), self.keychain_mask(), |api| { + slate = api.receive_tx(&slate, Some(self.get_config().account.as_str()), None)?; + Ok(()) + })?; + + // Create Slatepack message response. + let _ = self.create_slatepack_message(&slate, dest)?; + + Ok(slate) + } + + /// Finalize transaction from provided message as sender or invoice issuer. + fn finalize(&self, slate: &Slate, id: u32) -> Result { + self.on_tx_action(id, Some(WalletTxAction::Finalizing)); + + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let api = Owner::new(instance, None); + let mut slate = slate.clone(); + controller::foreign_single_use(api.wallet_inst.clone(), self.keychain_mask(), |api| { + slate = api.finalize_tx(&slate, false)?; + Ok(()) + })?; + + // Save Slatepack message to file. + let _ = self.create_slatepack_message(&slate, None)?; + + // Clear tx action. + self.on_tx_action(id, None); + + Ok(slate) + } + + /// Post transaction to blockchain. + fn post(&self, slate: &Slate, id: Option) -> Result<(), Error> { + if let Some(id) = id { + self.on_tx_action(id, Some(WalletTxAction::Posting)); + } + + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let mut api = Owner::new(instance, None); + controller::owner_single_use( + None, + self.keychain_mask().as_ref(), + Some(&mut api), + |api, m| { + api.post_tx(m, &slate, self.can_use_dandelion())?; + Ok(()) + }, + )?; + + // Clear tx action. + if let Some(id) = id { + self.on_tx_action(id, None); + } + Ok(()) + } + + /// Cancel transaction. + fn cancel(&self, id: u32) -> Result<(), Error> { + self.on_tx_action(id, Some(WalletTxAction::Cancelling)); + + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + cancel_tx( + instance, + self.keychain_mask().as_ref(), + &None, + Some(id), + None, + )?; + + // Clear tx action. + self.on_tx_action(id, None); + + Ok(()) + } + + /// Update transaction action status. + fn on_tx_action(&self, id: u32, action: Option) { + let mut w_data = self.data.write(); + if let Some(data) = w_data.as_mut() { + data.on_tx_action(id, action); + } + } + + /// Update transaction action error status. + fn on_tx_error(&self, id: u32, err: Option) { + let mut w_data = self.data.write(); + if let Some(data) = w_data.as_mut() { + data.on_tx_error(id, err); + } + } + + /// Save task result to consume later. + fn on_task_result(&self, tx: Option, task: &WalletTask) { + let mut w_res = self.task_result.write(); + let id = if let Some(t) = tx { Some(t.id) } else { None }; + *w_res = Some((id, task.clone())); + } + + /// Consume result of successful task. + pub fn consume_task_result(&self) -> Option<(Option, WalletTask)> { + let res = { + let r_res = self.task_result.read(); + r_res.clone() + }; + // Clear result for task. + let mut w_res = self.task_result.write(); + *w_res = None; + res + } + + /// Get possible transaction confirmation height. + fn tx_height(&self, tx: &WalletTx) -> Result, Error> { + let mut tx_height = None; + if tx.data.confirmed && tx.data.kernel_excess.is_some() { + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let mut w_lock = instance.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + if let Ok(res) = w.w2n_client().get_kernel( + tx.data.kernel_excess.as_ref().unwrap(), + tx.data.kernel_lookup_min_height, + None, + ) { + tx_height = Some(match res { + None => 0, + Some((_, h, _)) => h, + }); + } + } else if tx.broadcasting() { + tx_height = match self.get_data() { + None => None, + Some(data) => Some(data.info.last_confirmed_height), + }; + } + Ok(tx_height) + } + + /// Get stored transaction Slate. + fn get_tx_slate(&self, tx_id: Option, slate_id: Option<&Uuid>) -> Option { + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let api = Owner::new(instance, None); + if let Ok(s) = api.get_stored_tx(self.keychain_mask().as_ref(), tx_id, slate_id) { + return s; + } + None + } + + /// Delete transaction from database. + fn delete_tx(&self, id: u32) -> Result<(), Error> { + self.on_tx_action(id, Some(WalletTxAction::Deleting)); + + let slate = self.get_tx_slate(Some(id), None); + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let keychain_mask = self.keychain_mask(); + let mut wallet_lock = instance.lock(); + let lc = wallet_lock.lc_provider()?; + let w = lc.wallet_inst()?; + let parent_key = w.parent_key_id(); + let mut batch = w.batch(keychain_mask.as_ref())?; + batch.delete_tx_log_entry(id, &parent_key)?; + batch.commit()?; + + // Delete transaction files. + if let Some(s) = slate { + let slatepack_path = self.get_config().get_slate_path(&s); + fs::remove_file(&slatepack_path).unwrap_or_default(); + let path = path::Path::new(&self.get_config().get_data_path()) + .join("saved_txs") + .join(format!("{}.grintx", s.id)); + fs::remove_file(&path).unwrap_or_default(); + } + Ok(()) + } + + /// Change wallet password. + pub fn change_password(&self, old: String, new: String) -> Result<(), Error> { + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let mut wallet_lock = instance.lock(); + let lc = wallet_lock.lc_provider()?; + lc.change_password(None, ZeroingString::from(old), ZeroingString::from(new)) + } + + /// Initiate wallet repair by scanning its outputs. + pub fn repair(&self) { + self.repair_needed.store(true, Ordering::Relaxed); + self.sync(); + } + + /// Check if wallet is repairing. + pub fn is_repairing(&self) -> bool { + self.repair_needed.load(Ordering::Relaxed) + } + + /// Get wallet repairing progress. + pub fn repairing_progress(&self) -> u8 { + self.repair_progress.load(Ordering::Relaxed) + } + + /// Change wallet data path, migrating all files to new directory. + pub fn change_data_path(&self, path: String) { + let wallet = self.clone(); + wallet.files_moving.store(true, Ordering::Relaxed); + // Close wallet if open. + if self.is_open() { + self.close(); + } + thread::spawn(move || { + // Wait wallet to be closed. + while wallet.is_open() || wallet.syncing() { + thread::sleep(Duration::from_millis(100)); + } + // Move wallet db files. + if let Some(old_path) = wallet.get_config().data_path { + let mut old = PathBuf::from(old_path.as_str()); + old.push(WalletConfig::DATA_DIR_NAME); + let mut new = PathBuf::from(path.as_str()); + new.push(WalletConfig::DATA_DIR_NAME); + if old.exists() { + fs::create_dir_all(&new).unwrap_or_default(); + if let Ok(_) = fs::rename(old.as_path(), new.as_path()) { + // Save new path to config. + let mut w_config = wallet.config.write(); + w_config.data_path = Some(path); + w_config.save(); + } + } + } + wallet.files_moving.store(false, Ordering::Relaxed); + // Mark wallet to reopen. + if !wallet.is_open() { + wallet.set_reopen(true); + } + }); + } + + /// Deleting wallet database files. + pub fn delete_db(&self) { + let wallet = self.clone(); + wallet.files_moving.store(true, Ordering::Relaxed); + // Close wallet if open. + if self.is_open() { + self.close(); + } + thread::spawn(move || { + // Wait wallet to be closed. + while wallet.is_open() || wallet.syncing() { + thread::sleep(Duration::from_millis(100)); + } + // Remove wallet db files. + let _ = fs::remove_dir_all(wallet.get_config().get_db_path()); + wallet.files_moving.store(false, Ordering::Relaxed); + // Mark wallet to repair. + wallet.repair(); + // Mark wallet to reopen. + if !wallet.is_open() { + wallet.set_reopen(true); + } + }); + } + + /// Check if data files are moving. + pub fn files_moving(&self) -> bool { + self.files_moving.load(Ordering::Relaxed) + } + + /// Retrieve payment proof. + pub fn get_payment_proof( + &self, + tx_id: Option, + slate_id: Option, + ) -> Result, Error> { + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let key_mask = self.keychain_mask(); + let mut api = Owner::new(instance, None); + let mut proof = None; + controller::owner_single_use(None, key_mask.as_ref(), Some(&mut api), |api, m| { + let result = api.retrieve_payment_proof(m, false, tx_id, slate_id); + proof = match result { + Ok(p) => Some(p), + Err(e) => { + error!("retrieve_payment_proof error: {}", e); + None + } + }; + Ok(()) + })?; + Ok(proof) + } + + /// Verify payment proof. + fn verify_payment_proof(&self, proof: &PaymentProof) -> Result<(u32, bool, bool), Error> { + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let keychain_mask = self.keychain_mask(); + let verify_res = verify_payment_proof(instance.clone(), keychain_mask.as_ref(), proof); + let res = match verify_res { + Ok((send, rec)) => { + // Update proof at local database for valid proof. + if send || rec { + let mut wallet_lock = instance.lock(); + let lc = wallet_lock.lc_provider()?; + let w = lc.wallet_inst()?; + // Find wallet transaction to update or create. + let txs = w + .tx_log_iter() + .filter(|entry| { + if let Some(excess) = entry.kernel_excess { + return excess == proof.excess; + } + false + }) + .collect::>(); + if let Some(tx) = txs.get(0) { + let mut tx = tx.clone(); + let mut batch = w.batch(keychain_mask.as_ref())?; + let parent_key = &tx.parent_key_id; + tx.payment_proof = Some(StoredProofInfo { + receiver_address: proof.recipient_address.pub_key, + receiver_signature: Some(proof.recipient_sig), + sender_address_path: 0, + sender_address: proof.sender_address.pub_key, + sender_signature: Some(proof.sender_sig), + }); + batch.save_tx_log_entry(tx.clone(), &parent_key)?; + batch.commit()?; + Ok((tx.id, send, rec)) + } else { + let parent_key = w.parent_key_id(); + let mut batch = w.batch(keychain_mask.as_ref())?; + let log_id = batch.next_tx_log_id(&parent_key)?; + let log_type = TxLogEntryType::TxSent; + let mut tx = TxLogEntry::new(parent_key.clone(), log_type, log_id); + tx.amount_debited = proof.amount; + tx.kernel_excess = Some(proof.excess); + tx.tx_type = TxLogEntryType::TxSent; + tx.confirmed = true; + tx.payment_proof = Some(StoredProofInfo { + receiver_address: proof.recipient_address.pub_key, + receiver_signature: Some(proof.recipient_sig), + sender_address_path: 0, + sender_address: proof.sender_address.pub_key, + sender_signature: Some(proof.sender_sig), + }); + batch.save_tx_log_entry(tx.clone(), &parent_key)?; + batch.commit()?; + Ok((tx.id, send, rec)) + } + } else { + Ok((0, send, rec)) + } + } + Err(e) => Err(e), + }; + // Sync wallet data on success. + if res.is_ok() { + sync_wallet_data(self, false); + } + res + } + + /// Check if payment proof is verifying. + pub fn payment_proof_verifying(&self) -> bool { + self.proof_verifying.load(Ordering::Relaxed) + } + + /// Get recovery phrase. + pub fn get_recovery(&self, password: String) -> Result { + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let mut wallet_lock = instance.lock(); + let lc = wallet_lock.lc_provider()?; + lc.get_mnemonic(None, ZeroingString::from(password)) + } + + /// Close the wallet, delete its files and mark it as deleted. + pub fn delete_wallet(&self) { + if self.is_open() { + self.close(); + } + // Mark wallet as deleted. + let wallet_delete = self.clone(); + wallet_delete.deleted.store(true, Ordering::Relaxed); + + thread::spawn(move || { + // Wait wallet to be closed. + if wallet_delete.is_open() { + thread::sleep(Duration::from_millis(100)); + } + // Remove wallet files. + let _ = fs::remove_dir_all(wallet_delete.get_config().get_wallet_path()); + // Mark wallet as deleted. + wallet_delete.deleted.store(true, Ordering::Relaxed); + // Start sync to close thread. + wallet_delete.sync(); + }); + } + + /// Check if wallet was deleted to remove it from list. + pub fn is_deleted(&self) -> bool { + self.deleted.load(Ordering::Relaxed) + } } /// Delay in seconds to sync [`WalletData`] (60 seconds as average block time). @@ -1474,744 +1525,773 @@ const SYNC_ATTEMPTS: u8 = 10; /// Launch thread to sync wallet data from node. fn start_sync(wallet: Wallet) -> Thread { - // Start tasks thread. - let (tx, rx) = mpsc::channel(); - { - let mut w_tasks = wallet.tasks_sender.write(); - *w_tasks = Some(tx); - } - let wallet_thread = wallet.clone(); - thread::spawn(move || loop { - let wallet_task = wallet_thread.clone(); - if let Ok(task) = rx.recv() { - thread::spawn(move || { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(async { - handle_task(&wallet_task, task).await; - }); - }); - } - if wallet_thread.is_closing() || !wallet_thread.is_open() { - break; - } - }); + // Start tasks thread. + let (tx, rx) = mpsc::channel(); + { + let mut w_tasks = wallet.tasks_sender.write(); + *w_tasks = Some(tx); + } + let wallet_thread = wallet.clone(); + thread::spawn(move || { + loop { + let wallet_task = wallet_thread.clone(); + if let Ok(task) = rx.recv() { + thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + handle_task(&wallet_task, task).await; + }); + }); + } + if wallet_thread.is_closing() || !wallet_thread.is_open() { + break; + } + } + }); - // Reset progress values. - wallet.info_sync_progress.store(0, Ordering::Relaxed); - wallet.repair_progress.store(0, Ordering::Relaxed); + // Reset progress values. + wallet.info_sync_progress.store(0, Ordering::Relaxed); + wallet.repair_progress.store(0, Ordering::Relaxed); - // To call on sync thread stop. - let on_thread_stop = |wallet: Wallet| { - // Clear thread instance. - let mut thread_w = wallet.sync_thread.write(); - *thread_w = None; + // To call on sync thread stop. + let on_thread_stop = |wallet: Wallet| { + // Clear thread instance. + let mut thread_w = wallet.sync_thread.write(); + *thread_w = None; - // Clear wallet info. - let mut w_data = wallet.data.write(); - *w_data = None; + // Clear wallet info. + let mut w_data = wallet.data.write(); + *w_data = None; - // Clear syncing status. - wallet.syncing.store(false, Ordering::Relaxed); - }; + // Clear syncing status. + wallet.syncing.store(false, Ordering::Relaxed); + }; - thread::spawn(move || loop { - // Set syncing status. - wallet.syncing.store(true, Ordering::Relaxed); + thread::spawn(move || { + loop { + // Set syncing status. + wallet.syncing.store(true, Ordering::Relaxed); - // Close wallet on chain type change. - if wallet.get_config().chain_type != AppConfig::chain_type() { - wallet.close(); - } + // Close wallet on chain type change. + if wallet.get_config().chain_type != AppConfig::chain_type() { + wallet.close(); + } - // Stop syncing if wallet was closed. - if !wallet.is_open() || wallet.is_closing() { - on_thread_stop(wallet); - return; - } + // Stop syncing if wallet was closed. + if !wallet.is_open() || wallet.is_closing() { + on_thread_stop(wallet); + return; + } - // Check integrated node state. - if wallet.get_current_connection() == ConnectionMethod::Integrated { - let not_enabled = !Node::is_running() || Node::is_stopping(); - if not_enabled { - // Reset loading progress. - wallet.info_sync_progress.store(0, Ordering::Relaxed); - } - // Set an error when integrated node is not enabled. - wallet.set_sync_error(not_enabled); - // Skip cycle when node sync is not finished. - if !Node::is_running() || Node::get_sync_status() != Some(SyncStatus::NoSync) { - thread::park_timeout(ATTEMPT_DELAY); - continue; - } - } + // Check integrated node state. + if wallet.get_current_connection() == ConnectionMethod::Integrated { + let not_enabled = !Node::is_running() || Node::is_stopping(); + if not_enabled { + // Reset loading progress. + wallet.info_sync_progress.store(0, Ordering::Relaxed); + } + // Set an error when integrated node is not enabled. + wallet.set_sync_error(not_enabled); + // Skip cycle when node sync is not finished. + if !Node::is_running() || Node::get_sync_status() != Some(SyncStatus::NoSync) { + thread::park_timeout(ATTEMPT_DELAY); + continue; + } + } - // Scan outputs if repair is needed or sync data if there is no error. - if !wallet.sync_error() { - if wallet.is_repairing() { - repair_wallet(&wallet); - // Stop sync if wallet was closed. - if !wallet.is_open() || wallet.is_closing() { - on_thread_stop(wallet); - return; - } - } - // Retrieve data from local database if current data is empty. - if wallet.get_data().is_none() { - sync_wallet_data(&wallet, false); - } + // Scan outputs if repair is needed or sync data if there is no error. + if !wallet.sync_error() { + if wallet.is_repairing() { + repair_wallet(&wallet); + // Stop sync if wallet was closed. + if !wallet.is_open() || wallet.is_closing() { + on_thread_stop(wallet); + return; + } + } + // Retrieve data from local database if current data is empty. + if wallet.get_data().is_none() { + sync_wallet_data(&wallet, false); + } - if wallet.is_open() && !wallet.is_closing() { - // Start Foreign API listener if not running. - let mut api_server_running = { - wallet.foreign_api_server.read().is_some() - }; - if !api_server_running { - match start_api_server(&wallet) { - Ok(api_server) => { - let mut api_server_w = wallet.foreign_api_server.write(); - *api_server_w = Some(api_server); - api_server_running = true; - } - Err(_) => {} - } - } + if wallet.is_open() && !wallet.is_closing() { + // Start Foreign API listener if not running. + let mut api_server_running = { wallet.foreign_api_server.read().is_some() }; + if !api_server_running { + match start_api_server(&wallet) { + Ok(api_server) => { + let mut api_server_w = wallet.foreign_api_server.write(); + *api_server_w = Some(api_server); + api_server_running = true; + } + Err(_) => {} + } + } - // Start unfailed Tor service if API server is running. - let service_id = wallet.identifier(); - if wallet.auto_start_tor_listener() && api_server_running && - !Tor::is_service_failed(&service_id) { - let r_foreign_api = wallet.foreign_api_server.read(); - let api = r_foreign_api.as_ref().unwrap(); - if let Some(key) = wallet.secret_key() { - Tor::start_service(api.1, key, &wallet.identifier()); - } - } - } + // Start unfailed Tor service if API server is running. + let service_id = wallet.identifier(); + if wallet.auto_start_tor_listener() + && api_server_running + && !Tor::is_service_failed(&service_id) + { + let r_foreign_api = wallet.foreign_api_server.read(); + let api = r_foreign_api.as_ref().unwrap(); + if let Some(key) = wallet.secret_key() { + Tor::start_service(api.1, key, &wallet.identifier()); + } + } + } - // Sync wallet from node. - sync_wallet_data(&wallet, true); - } + // Sync wallet from node. + sync_wallet_data(&wallet, true); + } - // Stop sync if wallet was closed. - if !wallet.is_open() || wallet.is_closing() { - on_thread_stop(wallet); - return; - } + // Stop sync if wallet was closed. + if !wallet.is_open() || wallet.is_closing() { + on_thread_stop(wallet); + return; + } - // Setup flag to check if sync was failed. - let failed_sync = wallet.sync_error() || wallet.get_sync_attempts() != 0; + // Setup flag to check if sync was failed. + let failed_sync = wallet.sync_error() || wallet.get_sync_attempts() != 0; - // Clear syncing status. - if !failed_sync { - wallet.syncing.store(false, Ordering::Relaxed); - } + // Clear syncing status. + if !failed_sync { + wallet.syncing.store(false, Ordering::Relaxed); + } - // Repeat after default or attempt delay if synchronization was not successful. - let delay = if failed_sync { - ATTEMPT_DELAY - } else { - SYNC_DELAY - }; - thread::park_timeout(delay); - }).thread().clone() + // Repeat after default or attempt delay if synchronization was not successful. + let delay = if failed_sync { + ATTEMPT_DELAY + } else { + SYNC_DELAY + }; + thread::park_timeout(delay); + } + }) + .thread() + .clone() } /// Handle wallet task. async fn handle_task(w: &Wallet, t: WalletTask) { - let send_tor = async |tx: TxLogEntry, s: &Slate, r: &SlatepackAddress| { - match w.send_tor(tx.id, &s, r).await { - Ok(s) => { - match w.finalize(&s, tx.id) { - Ok(s) => { - match w.post(&s, Some(tx.id)) { - Ok(_) => { - sync_wallet_data(&w, false); - w.on_task_result(Some(tx), &t); - } - Err(e) => { - error!("send tor post error: {:?}", e); - w.on_tx_error(tx.id, Some(e)); - } - } - } - Err(e) => { - error!("send tor finalize error: {:?}", e); - w.task(WalletTask::Cancel(tx.id)); - } - } - } - Err(e) => { - error!("send tor error: {:?}", e); - w.on_tx_error(tx.id, Some(e)); - w.on_task_result(Some(tx), &t); - } - } - }; - match &t { - WalletTask::OpenMessage(m) => { - if !w.is_open() || m.is_empty() { - return; - } - let w = w.clone(); - let msg = m.clone(); - w.message_opening.store(true, Ordering::Relaxed); - if let Ok((s, dest)) = w.parse_slatepack(&msg) { - let tx = w.retrieve_tx_by_id(None, Some(s.id)); - // Check if message already exists. - let exists = { - let mut exists = w.slatepack_exists(&s); - if !exists && (s.state == SlateState::Invoice2 || - s.state == SlateState::Standard2) { - let mut slate = s.clone(); - slate.state = if s.state == SlateState::Standard2 { - SlateState::Standard3 - } else { - SlateState::Invoice3 - }; - exists = w.slatepack_exists(&slate); - } - exists - }; - if exists { - w.on_task_result(tx, &t); - w.message_opening.store(false, Ordering::Relaxed); - return; - } - // Create response or finalize. - match s.state { - SlateState::Standard1 | SlateState::Invoice1 => { - if s.state != SlateState::Standard1 { - if let Ok(_) = w.pay(&s) { - sync_wallet_data(&w, false); - let tx = w.retrieve_tx_by_id(None, Some(s.id)); - w.on_task_result(tx, &t); - } - } else { - if let Ok(_) = w.receive(&s, dest) { - sync_wallet_data(&w, false); - let tx = w.retrieve_tx_by_id(None, Some(s.id)); - w.on_task_result(tx, &t); - } - } - } - SlateState::Standard2 | SlateState::Invoice2 => { - if let Some(tx) = tx { - match w.finalize(&s, tx.id) { - Ok(s) => { - match w.post(&s, Some(tx.id)) { - Ok(_) => { - sync_wallet_data(&w, false); - } - Err(e) => { - error!("message tx post error: {:?}", e); - w.on_tx_error(tx.id, Some(e)); - } - } - } - Err(e) => { - error!("message tx finalize error: {:?}", e); - w.task(WalletTask::Cancel(tx.id)); - } - } - } - } - _ => {} - }; - } - w.message_opening.store(false, Ordering::Relaxed); - } - WalletTask::CalculateFee(a, _) => { - // Wait if there are no more fee tasks or handle next input value. - let calculating = w.fee_calculating.load(Ordering::Relaxed); - if calculating == 1 { - async_std::task::sleep(Duration::from_millis(100)).await; - let calculating = w.fee_calculating.load(Ordering::Relaxed); - if calculating > 1 { - w.fee_calculating.store(calculating - 1, Ordering::Relaxed); - return; - } - } else { - w.fee_calculating.store(calculating - 1, Ordering::Relaxed); - return; - } - // Calculate fee for provided amount. - if let Ok(fee) = w.calculate_fee(*a) { - w.on_task_result(None, &WalletTask::CalculateFee(*a, fee)) - } - let calculating = w.fee_calculating.load(Ordering::Relaxed); - w.fee_calculating.store(calculating - 1, Ordering::Relaxed); - } - WalletTask::Send(a, r) => { - w.send_creating.store(true, Ordering::Relaxed); - if let Ok(s) = w.send(*a, r.clone()) { - sync_wallet_data(&w, false); - let tx = w.retrieve_tx_by_id(None, Some(s.id)); - if let Some(tx) = tx { - if let Some(addr) = r { - let id = w.identifier(); - if Tor::is_service_running(&id) || Tor::is_service_starting(&id) { - w.send_creating.store(false, Ordering::Relaxed); - send_tor(tx, &s, addr).await; - return; - } else { - w.on_task_result(Some(tx), &t); - } - } else { - w.on_task_result(Some(tx), &t); - } - } - } - w.send_creating.store(false, Ordering::Relaxed); - } - WalletTask::SendTor(tx, r) => { - if let Some(s) = w.get_tx_slate(Some(tx.id), None) { - send_tor(tx.clone(), &s, r).await; - } - } - WalletTask::Receive(a) => { - w.invoice_creating.store(true, Ordering::Relaxed); - if let Ok(s) = w.issue_invoice(*a) { - sync_wallet_data(&w, false); - let tx = w.retrieve_tx_by_id(None, Some(s.id)); - if let Some(tx) = tx { - w.on_task_result(Some(tx), &t); - } - } - w.invoice_creating.store(false, Ordering::Relaxed); - }, - WalletTask::Finalize(id) => { - if let Some(s) = w.get_tx_slate(Some(*id), None) { - w.on_tx_error(*id, None); - match w.finalize(&s, *id) { - Ok(s) => { - match w.post(&s, Some(*id)) { - Ok(_) => { - sync_wallet_data(&w, false); - } - Err(e) => { - error!("tx finalize post error: {:?}", e); - w.on_tx_error(*id, Some(e)); - } - } - } - Err(e) => { - error!("tx finalize error: {:?}", e); - w.task(WalletTask::Cancel(*id)); - } - } - } else { - error!("tx finalize: slate not found"); - w.task(WalletTask::Cancel(*id)); - } - } - WalletTask::Post(id) => { - if let Some(s) = w.get_tx_slate(Some(*id), None) { - w.on_tx_error(*id, None); - // Cleanup broadcasting tx height. - let tx_height_store = TxHeightStore::new(w.get_config().get_extra_db_path()); - tx_height_store.delete_broadcasting_height(&id.to_string()); - let has_data = { - let r_data = w.data.read(); - r_data.is_some() - }; - if has_data { - let mut w_data = w.data.write(); - for tx in w_data.as_mut().unwrap().txs.as_mut().unwrap() { - if tx.data.id == *id { - tx.broadcasting_height = None; - break; - } - } - } - // Post transaction. - match w.post(&s, Some(*id)) { - Ok(_) => { - sync_wallet_data(&w, false); - } - Err(e) => { - error!("tx post error: {:?}", e); - w.on_tx_error(*id, Some(e)); - } - } - } else { - error!("tx post: slate not found"); - w.task(WalletTask::Cancel(*id)); - } - } - WalletTask::Cancel(id) => { - match w.cancel(*id) { - Ok(_) => { - sync_wallet_data(&w, false); - } - Err(e) => { - error!("tx cancel error: {:?}", e); - w.on_tx_error(*id, Some(e)); - } - } - } - WalletTask::VerifyProof(p, _) => { - w.proof_verifying.store(true, Ordering::Relaxed); - let res = w.verify_payment_proof(p); - w.proof_verifying.store(false, Ordering::Relaxed); - w.on_task_result(None, &WalletTask::VerifyProof(p.clone(), Some(res))); - } - WalletTask::Delete(id) => { - match w.delete_tx(*id) { - Ok(_) => sync_wallet_data(&w, false), - Err(e) => { - error!("tx delete error: {:?}", e); - w.on_tx_error(*id, Some(e)); - } - } - } - }; + let send_tor = async |tx: TxLogEntry, s: &Slate, r: &SlatepackAddress| match w + .send_tor(tx.id, &s, r) + .await + { + Ok(s) => match w.finalize(&s, tx.id) { + Ok(s) => match w.post(&s, Some(tx.id)) { + Ok(_) => { + sync_wallet_data(&w, false); + w.on_task_result(Some(tx), &t); + } + Err(e) => { + error!("send tor post error: {:?}", e); + w.on_tx_error(tx.id, Some(e)); + } + }, + Err(e) => { + error!("send tor finalize error: {:?}", e); + w.task(WalletTask::Cancel(tx.id)); + } + }, + Err(e) => { + error!("send tor error: {:?}", e); + w.on_tx_error(tx.id, Some(e)); + w.on_task_result(Some(tx), &t); + } + }; + match &t { + WalletTask::OpenMessage(m) => { + if !w.is_open() || m.is_empty() { + return; + } + let w = w.clone(); + let msg = m.clone(); + w.message_opening.store(true, Ordering::Relaxed); + if let Ok((s, dest)) = w.parse_slatepack(&msg) { + let tx = w.retrieve_tx_by_id(None, Some(s.id)); + // Check if message already exists. + let exists = { + let mut exists = w.slatepack_exists(&s); + if !exists + && (s.state == SlateState::Invoice2 || s.state == SlateState::Standard2) + { + let mut slate = s.clone(); + slate.state = if s.state == SlateState::Standard2 { + SlateState::Standard3 + } else { + SlateState::Invoice3 + }; + exists = w.slatepack_exists(&slate); + } + exists + }; + if exists { + w.on_task_result(tx, &t); + w.message_opening.store(false, Ordering::Relaxed); + return; + } + // Create response or finalize. + match s.state { + SlateState::Standard1 | SlateState::Invoice1 => { + if s.state != SlateState::Standard1 { + if let Ok(_) = w.pay(&s) { + sync_wallet_data(&w, false); + let tx = w.retrieve_tx_by_id(None, Some(s.id)); + w.on_task_result(tx, &t); + } + } else { + if let Ok(_) = w.receive(&s, dest) { + sync_wallet_data(&w, false); + let tx = w.retrieve_tx_by_id(None, Some(s.id)); + w.on_task_result(tx, &t); + } + } + } + SlateState::Standard2 | SlateState::Invoice2 => { + if let Some(tx) = tx { + match w.finalize(&s, tx.id) { + Ok(s) => match w.post(&s, Some(tx.id)) { + Ok(_) => { + sync_wallet_data(&w, false); + } + Err(e) => { + error!("message tx post error: {:?}", e); + w.on_tx_error(tx.id, Some(e)); + } + }, + Err(e) => { + error!("message tx finalize error: {:?}", e); + w.task(WalletTask::Cancel(tx.id)); + } + } + } + } + _ => {} + }; + } + w.message_opening.store(false, Ordering::Relaxed); + } + WalletTask::CalculateFee(a, _) => { + // Wait if there are no more fee tasks or handle next input value. + let calculating = w.fee_calculating.load(Ordering::Relaxed); + if calculating == 1 { + async_std::task::sleep(Duration::from_millis(100)).await; + let calculating = w.fee_calculating.load(Ordering::Relaxed); + if calculating > 1 { + w.fee_calculating.store(calculating - 1, Ordering::Relaxed); + return; + } + } else { + w.fee_calculating.store(calculating - 1, Ordering::Relaxed); + return; + } + // Calculate fee for provided amount. + if let Ok(fee) = w.calculate_fee(*a) { + w.on_task_result(None, &WalletTask::CalculateFee(*a, fee)) + } + let calculating = w.fee_calculating.load(Ordering::Relaxed); + w.fee_calculating.store(calculating - 1, Ordering::Relaxed); + } + WalletTask::Send(a, r) => { + w.send_creating.store(true, Ordering::Relaxed); + if let Ok(s) = w.send(*a, r.clone()) { + sync_wallet_data(&w, false); + let tx = w.retrieve_tx_by_id(None, Some(s.id)); + if let Some(tx) = tx { + if let Some(addr) = r { + let id = w.identifier(); + if Tor::is_service_running(&id) || Tor::is_service_starting(&id) { + w.send_creating.store(false, Ordering::Relaxed); + send_tor(tx, &s, addr).await; + return; + } else { + w.on_task_result(Some(tx), &t); + } + } else { + w.on_task_result(Some(tx), &t); + } + } + } + w.send_creating.store(false, Ordering::Relaxed); + } + WalletTask::SendTor(tx, r) => { + if let Some(s) = w.get_tx_slate(Some(tx.id), None) { + send_tor(tx.clone(), &s, r).await; + } + } + WalletTask::Receive(a) => { + w.invoice_creating.store(true, Ordering::Relaxed); + if let Ok(s) = w.issue_invoice(*a) { + sync_wallet_data(&w, false); + let tx = w.retrieve_tx_by_id(None, Some(s.id)); + if let Some(tx) = tx { + w.on_task_result(Some(tx), &t); + } + } + w.invoice_creating.store(false, Ordering::Relaxed); + } + WalletTask::Finalize(id) => { + if let Some(s) = w.get_tx_slate(Some(*id), None) { + w.on_tx_error(*id, None); + match w.finalize(&s, *id) { + Ok(s) => match w.post(&s, Some(*id)) { + Ok(_) => { + sync_wallet_data(&w, false); + } + Err(e) => { + error!("tx finalize post error: {:?}", e); + w.on_tx_error(*id, Some(e)); + } + }, + Err(e) => { + error!("tx finalize error: {:?}", e); + w.task(WalletTask::Cancel(*id)); + } + } + } else { + error!("tx finalize: slate not found"); + w.task(WalletTask::Cancel(*id)); + } + } + WalletTask::Post(id) => { + if let Some(s) = w.get_tx_slate(Some(*id), None) { + w.on_tx_error(*id, None); + // Cleanup broadcasting tx height. + let tx_height_store = TxHeightStore::new(w.get_config().get_extra_db_path()); + tx_height_store.delete_broadcasting_height(&id.to_string()); + let has_data = { + let r_data = w.data.read(); + r_data.is_some() + }; + if has_data { + let mut w_data = w.data.write(); + for tx in w_data.as_mut().unwrap().txs.as_mut().unwrap() { + if tx.data.id == *id { + tx.broadcasting_height = None; + break; + } + } + } + // Post transaction. + match w.post(&s, Some(*id)) { + Ok(_) => { + sync_wallet_data(&w, false); + } + Err(e) => { + error!("tx post error: {:?}", e); + w.on_tx_error(*id, Some(e)); + } + } + } else { + error!("tx post: slate not found"); + w.task(WalletTask::Cancel(*id)); + } + } + WalletTask::Cancel(id) => match w.cancel(*id) { + Ok(_) => { + sync_wallet_data(&w, false); + } + Err(e) => { + error!("tx cancel error: {:?}", e); + w.on_tx_error(*id, Some(e)); + } + }, + WalletTask::VerifyProof(p, _) => { + w.proof_verifying.store(true, Ordering::Relaxed); + let res = w.verify_payment_proof(p); + w.proof_verifying.store(false, Ordering::Relaxed); + w.on_task_result(None, &WalletTask::VerifyProof(p.clone(), Some(res))); + } + WalletTask::Delete(id) => match w.delete_tx(*id) { + Ok(_) => sync_wallet_data(&w, false), + Err(e) => { + error!("tx delete error: {:?}", e); + w.on_tx_error(*id, Some(e)); + } + }, + }; } /// Refresh [`WalletData`] from local base or node. fn sync_wallet_data(wallet: &Wallet, from_node: bool) { - // Update info sync progress at separate thread. - let wallet_info = wallet.clone(); - let (info_tx, info_rx) = mpsc::channel::(); - thread::spawn(move || { - while let Ok(m) = info_rx.recv() { - match m { - StatusMessage::UpdatingOutputs(_) => {} - StatusMessage::UpdatingTransactions(_) => {} - StatusMessage::FullScanWarn(_) => {} - StatusMessage::Scanning(_, progress) => { - wallet_info.info_sync_progress.store(progress, Ordering::Relaxed); - } - StatusMessage::ScanningComplete(_) => { - wallet_info.info_sync_progress.store(100, Ordering::Relaxed); - } - StatusMessage::UpdateWarning(_) => {} - } - } - }); + // Update info sync progress at separate thread. + let wallet_info = wallet.clone(); + let (info_tx, info_rx) = mpsc::channel::(); + thread::spawn(move || { + while let Ok(m) = info_rx.recv() { + match m { + StatusMessage::UpdatingOutputs(_) => {} + StatusMessage::UpdatingTransactions(_) => {} + StatusMessage::FullScanWarn(_) => {} + StatusMessage::Scanning(_, progress) => { + wallet_info + .info_sync_progress + .store(progress, Ordering::Relaxed); + } + StatusMessage::ScanningComplete(_) => { + wallet_info.info_sync_progress.store(100, Ordering::Relaxed); + } + StatusMessage::UpdateWarning(_) => {} + } + } + }); - let config = wallet.get_config(); + let config = wallet.get_config(); - // Retrieve wallet info. - let r_inst = wallet.instance.as_ref().read(); - if r_inst.is_some() { - let instance = r_inst.clone().unwrap(); - if let Ok((_, info)) = retrieve_summary_info( - instance.clone(), - wallet.keychain_mask().as_ref(), - &Some(info_tx), - from_node, - config.min_confirmations - ) { - // Do not retrieve txs if wallet was closed or its first sync. - if !wallet.is_open() || wallet.is_closing() || - (!from_node && info.last_confirmed_height == 0) { - return; - } + // Retrieve wallet info. + let r_inst = wallet.instance.as_ref().read(); + if r_inst.is_some() { + let instance = r_inst.clone().unwrap(); + if let Ok((_, info)) = retrieve_summary_info( + instance.clone(), + wallet.keychain_mask().as_ref(), + &Some(info_tx), + from_node, + config.min_confirmations, + ) { + // Do not retrieve txs if wallet was closed or its first sync. + if !wallet.is_open() + || wallet.is_closing() + || (!from_node && info.last_confirmed_height == 0) + { + return; + } - // Setup accounts data. - let last_height = info.last_confirmed_height; - let spendable = if wallet.get_data().is_none() { - None - } else { - Some(info.amount_currently_spendable) - }; - update_accounts(wallet, last_height, spendable); + // Setup accounts data. + let last_height = info.last_confirmed_height; + let spendable = if wallet.get_data().is_none() { + None + } else { + Some(info.amount_currently_spendable) + }; + update_accounts(wallet, last_height, spendable); - if wallet.info_sync_progress() == 100 || !from_node { - // Transactions limit setup. - let txs_limit = { - let r_data = wallet.data.read(); - if r_data.is_some() { - let data = r_data.as_ref().unwrap(); - data.txs_limit - } else { - WalletData::TXS_LIMIT - } - }; - // Update wallet info. - { - let mut w_data = wallet.data.write(); - if w_data.is_some() { - w_data.as_mut().unwrap().info = info; - } else { - *w_data = Some(WalletData { info, txs: None, txs_limit }); - } - } - // Update wallet transactions. - if update_txs(wallet, txs_limit).is_ok() { - if !wallet.from_node.load(Ordering::Relaxed) { - wallet.from_node.store(from_node, Ordering::Relaxed); - } - wallet.reset_sync_attempts(); - return; - } - } - } - } + if wallet.info_sync_progress() == 100 || !from_node { + // Transactions limit setup. + let txs_limit = { + let r_data = wallet.data.read(); + if r_data.is_some() { + let data = r_data.as_ref().unwrap(); + data.txs_limit + } else { + WalletData::TXS_LIMIT + } + }; + // Update wallet info. + { + let mut w_data = wallet.data.write(); + if w_data.is_some() { + w_data.as_mut().unwrap().info = info; + } else { + *w_data = Some(WalletData { + info, + txs: None, + txs_limit, + }); + } + } + // Update wallet transactions. + if update_txs(wallet, txs_limit).is_ok() { + if !wallet.from_node.load(Ordering::Relaxed) { + wallet.from_node.store(from_node, Ordering::Relaxed); + } + wallet.reset_sync_attempts(); + return; + } + } + } + } - // Reset progress. - wallet.info_sync_progress.store(0, Ordering::Relaxed); + // Reset progress. + wallet.info_sync_progress.store(0, Ordering::Relaxed); - // Exit if wallet was closed or closing. - if !wallet.is_open() || wallet.is_closing() { - return; - } + // Exit if wallet was closed or closing. + if !wallet.is_open() || wallet.is_closing() { + return; + } - // Set an error if data was not loaded after opening or increment attempts count. - if wallet.get_data().is_none() { - wallet.set_sync_error(true); - } else { - wallet.increment_sync_attempts(); - } + // Set an error if data was not loaded after opening or increment attempts count. + if wallet.get_data().is_none() { + wallet.set_sync_error(true); + } else { + wallet.increment_sync_attempts(); + } - // Set an error if maximum number of attempts was reached. - if wallet.get_sync_attempts() >= SYNC_ATTEMPTS { - wallet.reset_sync_attempts(); - wallet.set_sync_error(true); - } + // Set an error if maximum number of attempts was reached. + if wallet.get_sync_attempts() >= SYNC_ATTEMPTS { + wallet.reset_sync_attempts(); + wallet.set_sync_error(true); + } } /// Update wallet transactions. fn update_txs(wallet: &Wallet, mut txs_limit: u32) -> Result<(), Error> { - let txs = wallet.retrieve_txs(txs_limit)?; + let txs = wallet.retrieve_txs(txs_limit)?; - // Exit if wallet was closed. - if !wallet.is_open() || wallet.is_closing() { - return Err(Error::GenericError("Wallet is not open".to_string())); - } + // Exit if wallet was closed. + if !wallet.is_open() || wallet.is_closing() { + return Err(Error::GenericError("Wallet is not open".to_string())); + } - // Update limit with actual length. - let txs_size = txs.len() as u32; - let filter_size = txs.len() as u32; + // Update limit with actual length. + let txs_size = txs.len() as u32; + let filter_size = txs.len() as u32; - if txs_size > filter_size && txs_limit >= filter_size { - txs_limit = txs_limit - (txs_size - filter_size); - } + if txs_size > filter_size && txs_limit >= filter_size { + txs_limit = txs_limit - (txs_size - filter_size); + } - // Update existing tx list. - let tx_height_store = TxHeightStore::new(wallet.get_config().get_extra_db_path()); - let data = wallet.get_data().unwrap(); - let data_txs = data.txs.unwrap_or(vec![]); - let mut new_txs: Vec = vec![]; - for tx in &txs { - let mut height: Option = None; - let mut broadcasting_height: Option = None; - let mut action: Option = None; - let mut action_error: Option = None; - let mut proof: Option = None; - for t in &data_txs { - if t.data.id == tx.id { - action = t.action.clone(); - action_error = t.action_error.clone(); - height = t.height; - broadcasting_height = t.broadcasting_height; - proof = t.proof.clone(); - break; - } - } - let mut new = WalletTx::new(tx.clone(), - proof.clone(), - wallet, - height, - broadcasting_height, - action, - action_error); - // Update Slate state for unconfirmed. - let unconfirmed = !tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent || - tx.tx_type == TxLogEntryType::TxReceived); - if unconfirmed { - new.update_slate_state(wallet); - } - // Payment proof setup. - if proof.is_none() && tx.payment_proof.is_some() && - tx.payment_proof.as_ref().unwrap().receiver_signature.is_some() && - tx.payment_proof.as_ref().unwrap().sender_signature.is_some() && - tx.kernel_excess.is_some() { - if let Ok(p) = wallet.get_payment_proof(Some(tx.id), tx.tx_slate_id) { - proof = p.clone(); - new.proof = proof; - } - } - // Initial tx heights setup. - if let Some(slate_id) = tx.tx_slate_id { - let id = slate_id.to_string(); - if height.is_none() && tx.confirmed { - height = if let Some(height) = tx_height_store.read_tx_height(&id) { - Some(height) - } else { - tx_height_store.delete_broadcasting_height(&id); - let h = wallet.tx_height(&new)?; - if let Some(h) = h { - tx_height_store.write_tx_height(&id, h); - } - h - }; - new.height = height; - } else if broadcasting_height.is_none() && new.broadcasting() { - let br_height = tx_height_store.read_broadcasting_height(&id); - broadcasting_height = if br_height.is_none() || br_height.unwrap() == 0 { - let h = data.info.last_confirmed_height; - tx_height_store.write_broadcasting_height(&id, h); - Some(h) - } else { - Some(br_height.unwrap()) - }; - new.broadcasting_height = broadcasting_height; - } - } - if !new.deleting() { - new_txs.push(new); - } - } - // Update wallet txs. - let mut w_data = wallet.data.write(); - if w_data.is_some() { - w_data.as_mut().unwrap().txs_limit = txs_limit; - w_data.as_mut().unwrap().txs = Some(new_txs); - } - Ok(()) + // Update existing tx list. + let tx_height_store = TxHeightStore::new(wallet.get_config().get_extra_db_path()); + let data = wallet.get_data().unwrap(); + let data_txs = data.txs.unwrap_or(vec![]); + let mut new_txs: Vec = vec![]; + for tx in &txs { + let mut height: Option = None; + let mut broadcasting_height: Option = None; + let mut action: Option = None; + let mut action_error: Option = None; + let mut proof: Option = None; + for t in &data_txs { + if t.data.id == tx.id { + action = t.action.clone(); + action_error = t.action_error.clone(); + height = t.height; + broadcasting_height = t.broadcasting_height; + proof = t.proof.clone(); + break; + } + } + let mut new = WalletTx::new( + tx.clone(), + proof.clone(), + wallet, + height, + broadcasting_height, + action, + action_error, + ); + // Update Slate state for unconfirmed. + let unconfirmed = !tx.confirmed + && (tx.tx_type == TxLogEntryType::TxSent || tx.tx_type == TxLogEntryType::TxReceived); + if unconfirmed { + new.update_slate_state(wallet); + } + // Payment proof setup. + if proof.is_none() + && tx.payment_proof.is_some() + && tx + .payment_proof + .as_ref() + .unwrap() + .receiver_signature + .is_some() + && tx + .payment_proof + .as_ref() + .unwrap() + .sender_signature + .is_some() + && tx.kernel_excess.is_some() + { + if let Ok(p) = wallet.get_payment_proof(Some(tx.id), tx.tx_slate_id) { + proof = p.clone(); + new.proof = proof; + } + } + // Initial tx heights setup. + if let Some(slate_id) = tx.tx_slate_id { + let id = slate_id.to_string(); + if height.is_none() && tx.confirmed { + height = if let Some(height) = tx_height_store.read_tx_height(&id) { + Some(height) + } else { + tx_height_store.delete_broadcasting_height(&id); + let h = wallet.tx_height(&new)?; + if let Some(h) = h { + tx_height_store.write_tx_height(&id, h); + } + h + }; + new.height = height; + } else if broadcasting_height.is_none() && new.broadcasting() { + let br_height = tx_height_store.read_broadcasting_height(&id); + broadcasting_height = if br_height.is_none() || br_height.unwrap() == 0 { + let h = data.info.last_confirmed_height; + tx_height_store.write_broadcasting_height(&id, h); + Some(h) + } else { + Some(br_height.unwrap()) + }; + new.broadcasting_height = broadcasting_height; + } + } + if !new.deleting() { + new_txs.push(new); + } + } + // Update wallet txs. + let mut w_data = wallet.data.write(); + if w_data.is_some() { + w_data.as_mut().unwrap().txs_limit = txs_limit; + w_data.as_mut().unwrap().txs = Some(new_txs); + } + Ok(()) } /// Start Foreign API server to receive txs over transport and mining rewards. fn start_api_server(wallet: &Wallet) -> Result<(ApiServer, u16), Error> { - let host = "127.0.0.1"; - let port = wallet.get_config().api_port.unwrap_or(rand::rng().random_range(10000..30000)); - let free_port = (port..).find(|port| { - return match TcpListener::bind((host, port.to_owned())) { - Ok(_) => { - let node_p2p_port = NodeConfig::get_p2p_port(); - let node_api_port = NodeConfig::get_api_ip_port().1; - let free = port.to_string() != node_p2p_port && port.to_string() != node_api_port; - if free { - let mut config = wallet.config.write(); - config.api_port = Some(*port); - config.save(); - } - free - }, - Err(_) => false - } - }).unwrap(); + let host = "127.0.0.1"; + let port = wallet + .get_config() + .api_port + .unwrap_or(rand::rng().random_range(10000..30000)); + let free_port = (port..) + .find(|port| { + return match TcpListener::bind((host, port.to_owned())) { + Ok(_) => { + let node_p2p_port = NodeConfig::get_p2p_port(); + let node_api_port = NodeConfig::get_api_ip_port().1; + let free = + port.to_string() != node_p2p_port && port.to_string() != node_api_port; + if free { + let mut config = wallet.config.write(); + config.api_port = Some(*port); + config.save(); + } + free + } + Err(_) => false, + }; + }) + .unwrap(); - // Setup API server address. - let api_addr = format!("{}:{}", host, free_port); + // Setup API server address. + let api_addr = format!("{}:{}", host, free_port); - // Start Foreign API server thread. - let r_inst = wallet.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let keychain_mask = wallet.keychain_mask(); - let api_handler_v2 = ForeignAPIHandlerV2::new(instance, - Arc::new(Mutex::new(keychain_mask)), - false, - Mutex::new(None)); - let mut router = Router::new(); - router - .add_route("/v2/foreign", Arc::new(api_handler_v2)) - .map_err(|_| Error::GenericError("Router failed to add route".to_string()))?; + // Start Foreign API server thread. + let r_inst = wallet.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let keychain_mask = wallet.keychain_mask(); + let api_handler_v2 = ForeignAPIHandlerV2::new( + instance, + Arc::new(Mutex::new(keychain_mask)), + false, + Mutex::new(None), + ); + let mut router = Router::new(); + router + .add_route("/v2/foreign", Arc::new(api_handler_v2)) + .map_err(|_| Error::GenericError("Router failed to add route".to_string()))?; - let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) = - Box::leak(Box::new(oneshot::channel::<()>())); + let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) = + Box::leak(Box::new(oneshot::channel::<()>())); - let mut apis = ApiServer::new(); - let socket_addr: SocketAddr = api_addr.parse().unwrap(); - let _ = apis.start(socket_addr, router, None, api_chan) - .map_err(|_| Error::GenericError("API thread failed to start".to_string()))?; - Ok((apis, free_port)) + let mut apis = ApiServer::new(); + let socket_addr: SocketAddr = api_addr.parse().unwrap(); + let _ = apis + .start(socket_addr, router, None, api_chan) + .map_err(|_| Error::GenericError("API thread failed to start".to_string()))?; + Ok((apis, free_port)) } /// Update wallet accounts data. fn update_accounts(wallet: &Wallet, height: u64, spendable: Option) { - let current_account = wallet.get_config().account; - if let Some(amount) = spendable { - let mut accounts = wallet.accounts.read().clone(); - for a in accounts.iter_mut() { - if a.label == current_account { - a.spendable_amount = amount; - } - } - // Save accounts data. - let mut w_data = wallet.accounts.write(); - *w_data = accounts; - } else { - let r_inst = wallet.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let mut api = Owner::new(instance, None); - let key_mask = wallet.keychain_mask(); - let _ = controller::owner_single_use(None, key_mask.as_ref(), Some(&mut api), |api, m| { - let mut accounts = vec![]; - for a in api.accounts(m)? { - api.set_active_account(m, a.label.as_str())?; - // Calculate account balance. - if let Some(spendable_amount) = wallet.account_balance(height, api, m) { - accounts.push(WalletAccount { - spendable_amount, - label: a.label, - path: a.path.to_bip_32_string(), - }); - } - } - accounts.sort_by_key(|w| w.label != current_account); + let current_account = wallet.get_config().account; + if let Some(amount) = spendable { + let mut accounts = wallet.accounts.read().clone(); + for a in accounts.iter_mut() { + if a.label == current_account { + a.spendable_amount = amount; + } + } + // Save accounts data. + let mut w_data = wallet.accounts.write(); + *w_data = accounts; + } else { + let r_inst = wallet.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let mut api = Owner::new(instance, None); + let key_mask = wallet.keychain_mask(); + let _ = controller::owner_single_use(None, key_mask.as_ref(), Some(&mut api), |api, m| { + let mut accounts = vec![]; + for a in api.accounts(m)? { + api.set_active_account(m, a.label.as_str())?; + // Calculate account balance. + if let Some(spendable_amount) = wallet.account_balance(height, api, m) { + accounts.push(WalletAccount { + spendable_amount, + label: a.label, + path: a.path.to_bip_32_string(), + }); + } + } + accounts.sort_by_key(|w| w.label != current_account); - // Save accounts data. - let mut w_data = wallet.accounts.write(); - *w_data = accounts; + // Save accounts data. + let mut w_data = wallet.accounts.write(); + *w_data = accounts; - // Set current active account from config. - api.set_active_account(m, current_account.as_str())?; + // Set current active account from config. + api.set_active_account(m, current_account.as_str())?; - Ok(()) - }); - } + Ok(()) + }); + } } /// Scan wallet's outputs, repairing and restoring missing outputs if required. fn repair_wallet(wallet: &Wallet) { - let (info_tx, info_rx) = mpsc::channel::(); - // Update scan progress at separate thread. - let wallet_scan = wallet.clone(); - thread::spawn(move || { - while let Ok(m) = info_rx.recv() { - match m { - StatusMessage::UpdatingOutputs(_) => {} - StatusMessage::UpdatingTransactions(_) => {} - StatusMessage::FullScanWarn(_) => {} - StatusMessage::Scanning(_, progress) => { - wallet_scan.repair_progress.store(progress, Ordering::Relaxed); - } - StatusMessage::ScanningComplete(_) => { - wallet_scan.repair_progress.store(100, Ordering::Relaxed); - } - StatusMessage::UpdateWarning(_) => {} - } - } - }); + let (info_tx, info_rx) = mpsc::channel::(); + // Update scan progress at separate thread. + let wallet_scan = wallet.clone(); + thread::spawn(move || { + while let Ok(m) = info_rx.recv() { + match m { + StatusMessage::UpdatingOutputs(_) => {} + StatusMessage::UpdatingTransactions(_) => {} + StatusMessage::FullScanWarn(_) => {} + StatusMessage::Scanning(_, progress) => { + wallet_scan + .repair_progress + .store(progress, Ordering::Relaxed); + } + StatusMessage::ScanningComplete(_) => { + wallet_scan.repair_progress.store(100, Ordering::Relaxed); + } + StatusMessage::UpdateWarning(_) => {} + } + } + }); - let r_inst = wallet.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let api = Owner::new(instance, Some(info_tx)); - // Start wallet scanning. - match api.scan(wallet.keychain_mask().as_ref(), Some(1), false) { - Ok(()) => { - // Set sync error if scanning was not complete and wallet is open. - if wallet.is_open() && wallet.repair_progress.load(Ordering::Relaxed) != 100 { - wallet.set_sync_error(true); - } else { - wallet.repair_needed.store(false, Ordering::Relaxed); - } - } - Err(_) => { - // Set sync error if wallet is open. - if wallet.is_open() { - wallet.set_sync_error(true); - } else { - wallet.repair_needed.store(false, Ordering::Relaxed); - } - } - } + let r_inst = wallet.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let api = Owner::new(instance, Some(info_tx)); + // Start wallet scanning. + match api.scan(wallet.keychain_mask().as_ref(), Some(1), false) { + Ok(()) => { + // Set sync error if scanning was not complete and wallet is open. + if wallet.is_open() && wallet.repair_progress.load(Ordering::Relaxed) != 100 { + wallet.set_sync_error(true); + } else { + wallet.repair_needed.store(false, Ordering::Relaxed); + } + } + Err(_) => { + // Set sync error if wallet is open. + if wallet.is_open() { + wallet.set_sync_error(true); + } else { + wallet.repair_needed.store(false, Ordering::Relaxed); + } + } + } - // Reset repair progress. - wallet.repair_progress.store(0, Ordering::Relaxed); -} \ No newline at end of file + // Reset repair progress. + wallet.repair_progress.store(0, Ordering::Relaxed); +} diff --git a/wix/main.wxs b/wix/main.wxs index a0102713..37cc6183 100644 --- a/wix/main.wxs +++ b/wix/main.wxs @@ -7,8 +7,8 @@ - - + +