diff --git a/src/gui/app.rs b/src/gui/app.rs index d3062799..044c9d22 100755 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -17,11 +17,10 @@ use egui::{Align, Context, CornerRadius, CursorIcon, LayerId, Layout, Modifiers, 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::wallet::ExternalConnection; use crate::AppConfig; -use crate::gui::views::types::ContentContainer; /// Implements ui entry point and contains platform-specific callbacks. pub struct App { diff --git a/src/gui/colors.rs b/src/gui/colors.rs index 58c45180..f85a7e00 100644 --- a/src/gui/colors.rs +++ b/src/gui/colors.rs @@ -34,20 +34,20 @@ const GREEN: Color32 = Color32::from_rgb(0, 0x64, 0); const GREEN_DARK: Color32 = Color32::from_rgb(0, (0x64 as f32 * 1.3 + 0.5) as u8, 0); 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, 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 FILL: Color32 = Color32::from_gray(244); -const FILL_DARK: Color32 = Color32::from_gray(24); +const FILL_DARK: Color32 = Color32::from_gray(26); const FILL_DEEP: Color32 = Color32::from_gray(238); -const FILL_DEEP_DARK: Color32 = Color32::from_gray(18); +const FILL_DEEP_DARK: Color32 = Color32::from_gray(32); const FILL_LITE: Color32 = Color32::from_gray(249); -const FILL_LITE_DARK: Color32 = Color32::from_gray(16); +const FILL_LITE_DARK: Color32 = Color32::from_gray(21); const TEXT: Color32 = Color32::from_gray(80); const TEXT_DARK: Color32 = Color32::from_gray(185); @@ -231,7 +231,7 @@ impl Colors { } } - pub fn item_button() -> Color32 { + pub fn item_button_text() -> Color32 { if use_dark() { ITEM_BUTTON_DARK } else { diff --git a/src/gui/views/camera.rs b/src/gui/views/camera.rs index 7cff7086..0a4c9419 100644 --- a/src/gui/views/camera.rs +++ b/src/gui/views/camera.rs @@ -50,7 +50,6 @@ impl Default for CameraContent { impl CameraContent { /// Draw camera content. pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - ui.ctx().request_repaint(); let rect = if let Some(img_data) = cb.camera_image() { if let Ok(img) = image::load_from_memory(&*img_data.0) { @@ -85,6 +84,7 @@ impl CameraContent { }); }); } + ui.ctx().request_repaint(); } /// Draw camera image. @@ -311,7 +311,7 @@ impl CameraContent { // Check if string contains Slatepack message prefix and postfix. if text.starts_with("BEGINSLATEPACK.") && text.ends_with("ENDSLATEPACK.") { - return QrScanResult::Slatepack(ZeroingString::from(text)); + return QrScanResult::Slatepack(text.to_string()); } // Check Uniform Resource data. diff --git a/src/gui/views/file_pick.rs b/src/gui/views/file_pick.rs index 7917c93c..b974d368 100644 --- a/src/gui/views/file_pick.rs +++ b/src/gui/views/file_pick.rs @@ -12,42 +12,58 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::{fs, thread}; +use egui::CornerRadius; use parking_lot::RwLock; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +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, ItemButton(CornerRadius), Tab +} /// Button to pick file and parse its data into text. -pub struct FilePickButton { +pub struct FilePickContent { + /// Content type. + content_type: FilePickContentType, + /// Flag to check if file is picking. - pub file_picking: Arc, + file_picking: Arc, + + /// Flag to parse file content after pick. + parse_file: bool, /// Flag to check if file is parsing. - pub file_parsing: Arc, + file_parsing: Arc, /// File parsing result. - pub file_parsing_result: Arc>> + file_parsing_result: Arc>>, } -impl Default for FilePickButton { - fn default() -> Self { +impl FilePickContent { + /// Create new content from provided type. + pub fn new(content_type: FilePickContentType) -> Self { Self { + content_type, file_picking: Arc::new(AtomicBool::new(false)), + parse_file: true, file_parsing: Arc::new(AtomicBool::new(false)), - file_parsing_result: Arc::new(RwLock::new(None)) + file_parsing_result: Arc::new(RwLock::new(None)), } } -} -impl FilePickButton { - /// Draw button content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - cb: &dyn PlatformCallbacks, - on_result: impl FnOnce(String)) { + /// Do not parse file content. + pub fn no_parse(mut self) -> Self { + self.parse_file = false; + self + } + + /// Draw content with provided callback to return path of the file. + pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks, on_pick: impl FnOnce(String)) { if self.file_picking.load(Ordering::Relaxed) { View::small_loading_spinner(ui); // Check file pick result. @@ -70,7 +86,7 @@ impl FilePickButton { r_res.clone().unwrap() }; // Callback on result. - on_result(text); + on_pick(text); // Clear result. let mut w_res = self.file_parsing_result.write(); *w_res = None; @@ -78,12 +94,48 @@ impl FilePickButton { } } else { // Draw button to pick file. - let text = format!("{} {}", ARCHIVE_BOX, t!("choose_file")); - View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || { - if let Some(path) = cb.pick_file() { - self.on_file_pick(path); + match self.content_type { + FilePickContentType::Button => { + let text = format!("{} {}", ARCHIVE_BOX, t!("choose_file")); + View::colored_text_button(ui, + text, + Colors::blue(), + Colors::white_or_black(false), + || { + if let Some(path) = cb.pick_file() { + if !self.parse_file { + on_pick(path); + return; + } + self.on_file_pick(path); + } + }); } - }); + FilePickContentType::ItemButton(r) => { + View::item_button(ui, r, ARCHIVE_BOX, Some(Colors::blue()), || { + if let Some(path) = cb.pick_file() { + if !self.parse_file { + on_pick(path); + return; + } + self.on_file_pick(path); + } + }); + } + FilePickContentType::Tab => { + let active = self.file_parsing.load(Ordering::Relaxed) || + self.file_picking.load(Ordering::Relaxed); + View::tab_button(ui, ARCHIVE_BOX, Some(Colors::blue()), Some(active), |_| { + if let Some(path) = cb.pick_file() { + if !self.parse_file { + on_pick(path); + return; + } + self.on_file_pick(path); + } + }); + } + } } } @@ -94,6 +146,10 @@ impl FilePickButton { self.file_picking.store(true, Ordering::Relaxed); return; } + // Do not parse result. + if !self.parse_file { + return; + } self.file_parsing.store(true, Ordering::Relaxed); let result = self.file_parsing_result.clone(); thread::spawn(move || { diff --git a/src/gui/views/network/content.rs b/src/gui/views/network/content.rs index 5fda40db..7701823f 100644 --- a/src/gui/views/network/content.rs +++ b/src/gui/views/network/content.rs @@ -15,7 +15,7 @@ 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, POWER}; +use crate::gui::icons::{ARROWS_COUNTER_CLOCKWISE, ARROW_LEFT, 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}; @@ -230,22 +230,26 @@ impl NetworkContent { let current_type = self.node_tab_content.get_type(); ui.columns(4, |columns| { columns[0].vertical_centered_justified(|ui| { - View::tab_button(ui, DATABASE, current_type == NodeTabType::Info, |_| { + 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| { - View::tab_button(ui, GAUGE, current_type == NodeTabType::Metrics, |_| { + 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| { - View::tab_button(ui, FACTORY, current_type == NodeTabType::Mining, |_| { + 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| { - View::tab_button(ui, FADERS, current_type == NodeTabType::Settings, |_| { + let active = Some(current_type == NodeTabType::Settings); + View::tab_button(ui, FADERS, None, active, |_| { self.node_tab_content = Box::new(NetworkSettings::default()); }); }); @@ -276,7 +280,7 @@ impl NetworkContent { self.settings_content = None; }); } else if !show_connections { - View::title_button_big(ui, DOTS_THREE_OUTLINE_VERTICAL, |ui| { + View::title_button_big(ui, GLOBE, |_| { AppConfig::toggle_show_connections_network_panel(); }); } else if !dual_panel { diff --git a/src/gui/views/network/setup/p2p.rs b/src/gui/views/network/setup/p2p.rs index 638838cc..3457404d 100644 --- a/src/gui/views/network/setup/p2p.rs +++ b/src/gui/views/network/setup/p2p.rs @@ -764,10 +764,8 @@ fn peer_item_ui(ui: &mut egui::Ui, rect.set_height(42.0); // Draw round background. - let mut bg_rect = rect.clone(); - bg_rect.min += egui::emath::vec2(6.0, 0.0); let item_rounding = View::item_rounding(index, len, false); - ui.painter().rect(bg_rect, + ui.painter().rect(rect, item_rounding, Colors::white_or_black(false), View::item_stroke(), @@ -798,7 +796,7 @@ fn peer_item_ui(ui: &mut egui::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.add_space(6.0); // Draw peer address. let peer_text = format!("{} {}", GLOBE_SIMPLE, &peer_addr); ui.label(RichText::new(peer_text) diff --git a/src/gui/views/scan.rs b/src/gui/views/scan.rs index f291648a..e1a01df1 100644 --- a/src/gui/views/scan.rs +++ b/src/gui/views/scan.rs @@ -21,15 +21,15 @@ use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{CameraContent, Modal, View}; use crate::gui::views::types::QrScanResult; -/// QR code scan [`Modal`] content. -pub struct CameraScanModal { - /// Camera content for QR scan [`Modal`]. +/// QR code scanning content. +pub struct CameraScanContent { + /// Camera content. camera_content: Option, - /// QR code scan result + /// Scan result. qr_scan_result: Option, } -impl Default for CameraScanModal { +impl Default for CameraScanContent { fn default() -> Self { Self { camera_content: Some(CameraContent::default()), @@ -38,68 +38,20 @@ impl Default for CameraScanModal { } } -impl CameraScanModal { +impl CameraScanContent { /// Draw [`Modal`] content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - cb: &dyn PlatformCallbacks, - mut on_result: impl FnMut(&QrScanResult)) { + 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 { - 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()); - self.qr_scan_result = None; - Modal::close(); - }); - }); - 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); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.qr_scan_result = None; - self.camera_content = None; - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("repeat"), Colors::white_or_black(false), || { - Modal::set_title(t!("scan_qr")); - self.qr_scan_result = None; - self.camera_content = Some(CameraContent::default()); - cb.start_camera(); - }); - }); + 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() { @@ -126,4 +78,60 @@ impl CameraScanModal { } 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); + + // 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); + + 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 diff --git a/src/gui/views/settings/content.rs b/src/gui/views/settings/content.rs index 5ae17b82..4f470780 100644 --- a/src/gui/views/settings/content.rs +++ b/src/gui/views/settings/content.rs @@ -14,8 +14,7 @@ use crate::gui::icons::GLOBE_SIMPLE; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::settings::interface::InterfaceSettingsContent; -use crate::gui::views::settings::network::NetworkSettingsContent; +use crate::gui::views::settings::{InterfaceSettingsContent, NetworkSettingsContent}; use crate::gui::views::types::ContentContainer; use crate::gui::views::View; use crate::gui::Colors; @@ -26,6 +25,7 @@ pub struct SettingsContent { interface_settings: InterfaceSettingsContent, /// Network communication settings. network_settings: NetworkSettingsContent, + // tor_settings: TorSettingsContent, } impl Default for SettingsContent { @@ -33,6 +33,7 @@ impl Default for SettingsContent { Self { interface_settings: InterfaceSettingsContent::default(), network_settings: NetworkSettingsContent::default(), + //tor_settings: TorSettingsContent::default(), } } } @@ -53,6 +54,22 @@ impl SettingsContent { // Show network settings. self.network_settings.ui(ui, cb); - ui.add_space(6.0); + 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 diff --git a/src/gui/views/settings/mod.rs b/src/gui/views/settings/mod.rs index ab475feb..64e38531 100644 --- a/src/gui/views/settings/mod.rs +++ b/src/gui/views/settings/mod.rs @@ -13,7 +13,13 @@ // limitations under the License. mod content; -mod interface; -mod network; +pub use content::*; -pub use content::*; \ No newline at end of file +mod interface; +pub use interface::*; + +mod network; +pub use network::*; + +mod tor; +pub use tor::*; \ No newline at end of file diff --git a/src/gui/views/settings/network.rs b/src/gui/views/settings/network.rs index cbdefb2e..78c68c3e 100644 --- a/src/gui/views/settings/network.rs +++ b/src/gui/views/settings/network.rs @@ -94,9 +94,6 @@ 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| { - // Check if URL proxy is correct. - let http = "http://"; - let socks = "socks5://"; let proxy = c.proxy_url_edit.trim().to_string(); let use_socks = AppConfig::use_socks_proxy(); // Clear value if empty. @@ -109,6 +106,9 @@ impl NetworkSettingsContent { 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) { @@ -200,7 +200,7 @@ impl NetworkSettingsContent { }); } - /// Draw language selection item content. + /// Draw proxy item content. fn proxy_item_ui(&mut self, ui: &mut egui::Ui) { // Setup layout size. let mut rect = ui.available_rect_before_wrap(); diff --git a/src/gui/views/types.rs b/src/gui/views/types.rs index b55c533c..093aa12a 100644 --- a/src/gui/views/types.rs +++ b/src/gui/views/types.rs @@ -77,7 +77,7 @@ pub trait ContentContainer { #[derive(Clone)] pub enum QrScanResult { /// Slatepack message. - Slatepack(ZeroingString), + Slatepack(String), /// Slatepack address. Address(ZeroingString), /// Parsed text. diff --git a/src/gui/views/views.rs b/src/gui/views/views.rs index 70746d5a..6cb45abb 100644 --- a/src/gui/views/views.rs +++ b/src/gui/views/views.rs @@ -197,17 +197,31 @@ impl View { /// Tab button with white background fill color, contains only icon. pub fn tab_button(ui: &mut egui::Ui, icon: &str, - active: bool, + color: Option, + selected: Option, action: impl FnOnce(&mut egui::Ui)) { ui.scope(|ui| { - let text_color = match active { - true => Colors::title(false), - false => Colors::text(false) + let text_color = if let Some(c) = color { + if selected.is_none() { + Colors::inactive_text().gamma_multiply(1.2) + } else { + c + } + } else { + if let Some(active) = selected { + match active { + true => Colors::gray(), + false => Colors::item_button_text() + } + } else { + Colors::inactive_text().gamma_multiply(1.2) + } }; let mut button = Button::new(RichText::new(icon).size(22.0).color(text_color)); - if !active { + 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; @@ -223,7 +237,13 @@ impl View { button = button.fill(Colors::fill()).stroke(Stroke::NONE); } - let br = button.ui(ui).on_hover_cursor(CursorIcon::PointingHand); + // 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() { action(ui); @@ -322,7 +342,7 @@ impl View { 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() }; + 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)) @@ -592,7 +612,9 @@ impl View { if resp.clicked() || resp.dragged() { on_click(); } - let shape = RectShape::filled(resp.rect, CornerRadius::ZERO, Colors::semi_transparent()); + let shape = RectShape::filled(resp.rect, + CornerRadius::ZERO, + Colors::semi_transparent().gamma_multiply(0.7)); ui.painter().add(shape); } diff --git a/src/gui/views/wallets/content.rs b/src/gui/views/wallets/content.rs index 3a7d17b8..f5463118 100644 --- a/src/gui/views/wallets/content.rs +++ b/src/gui/views/wallets/content.rs @@ -22,13 +22,12 @@ 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, OpenWalletModal, WalletConnectionModal, WalletsModal}; -use crate::gui::views::wallets::types::WalletTabType; -use crate::gui::views::wallets::wallet::types::wallet_status_text; +use crate::gui::views::wallets::wallet::types::{wallet_status_text, WalletContentContainer}; use crate::gui::views::wallets::WalletContent; use crate::gui::views::{Content, Modal, TitlePanel, View}; use crate::gui::Colors; use crate::wallet::types::ConnectionMethod; -use crate::wallet::{ExternalConnection, Wallet, WalletList}; +use crate::wallet::{Wallet, WalletList}; use crate::AppConfig; /// Wallets content. @@ -46,7 +45,7 @@ pub struct WalletsContent { wallet_selection_content: WalletsModal, /// Selected [`Wallet`] content. - wallet_content: Option, + wallet_content: WalletContent, /// Wallet creation content. creation_content: Option, @@ -64,10 +63,10 @@ impl Default for WalletsContent { Self { wallets: WalletList::default(), wallet_selection_content: WalletsModal::new(None, None, true), - open_wallet_content: OpenWalletModal::new(None), + open_wallet_content: OpenWalletModal::new(), add_wallet_modal_content: AddWalletModal::default(), conn_selection_content: WalletConnectionModal::new(ConnectionMethod::Integrated), - wallet_content: None, + wallet_content: WalletContent::default(), creation_content: None, settings_content: None, } @@ -94,9 +93,9 @@ impl ContentContainer for WalletsContent { }); }, OPEN_WALLET_MODAL => { - self.open_wallet_content.ui(ui, modal, cb, |pass, _| { - if let Some(content) = &self.wallet_content { - return match content.wallet.open(pass) { + 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 }; @@ -106,22 +105,25 @@ impl ContentContainer for WalletsContent { }, SELECT_CONNECTION_MODAL => { self.conn_selection_content.ui(ui, modal, cb, |conn| { - if let Some(wallet_content) = &self.wallet_content { - wallet_content.wallet.update_connection(&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| { - if !wallet.is_open() { - self.open_wallet_content = OpenWalletModal::new(data.clone()); - Modal::new(OPEN_WALLET_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.open")) - .show(); - } - self.wallet_content = Some(WalletContent::new(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); + } + } } _ => {} } @@ -130,7 +132,7 @@ impl ContentContainer for WalletsContent { fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { if let Some(data) = crate::consume_incoming_data() { if !data.is_empty() { - self.on_data(ui, Some(data)); + self.on_data(ui, Some(data), cb); } } @@ -140,12 +142,10 @@ impl ContentContainer for WalletsContent { 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 && self.wallet_content.as_ref().unwrap().qr_scan_showing()) - || (dual_panel && showing_wallet && !AppConfig::show_wallets_at_dual_panel()) - || (!dual_panel && showing_wallet); + || (showing_wallet && (!dual_panel || !AppConfig::show_wallets_at_dual_panel())); // Show title panel. - self.title_ui(ui, dual_panel, showing_wallet, cb); + self.title_ui(ui, dual_panel, cb); egui::SidePanel::right("wallet_panel") .resizable(false) @@ -155,54 +155,53 @@ impl ContentContainer for WalletsContent { content_width - Content::SIDE_PANEL_WIDTH }) .frame(egui::Frame { + fill: Colors::fill_deep(), ..Default::default() }) .show_animated_inside(ui, showing_wallet, |ui| { - // Show opened wallet content. - if let Some(content) = self.wallet_content.as_mut() { - content.ui(ui, cb); + // Show selected wallet content. + if let Some(w) = self.wallets.selected().as_ref() { + self.wallet_content.ui(ui, w, cb); } }); - if !list_hidden { - egui::TopBottomPanel::bottom("wallets_bottom_panel") - .frame(egui::Frame { - inner_margin: Margin { - left: (View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING) as i8, - right: (View::far_right_inset_margin(ui) + 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() - }) - .resizable(false) - .show_inside(ui, |ui| { - let rect = ui.available_rect_before_wrap(); + egui::TopBottomPanel::bottom("wallets_bottom_panel") + .frame(egui::Frame { + inner_margin: Margin { + left: (View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING) as i8, + right: (View::far_right_inset_margin(ui) + 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() + }) + .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 vertical padding inside buttons. - ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0); + // Setup spacing between tabs. + ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); + // Setup vertical padding inside buttons. + ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0); - ui.vertical_centered(|ui| { - let pressed = Modal::opened() == Some(ADD_WALLET_MODAL); - View::tab_button(ui, PLUS, 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 -= View::TAB_ITEMS_PADDING; - r.min.x -= View::TAB_ITEMS_PADDING; - r.max.x += View::TAB_ITEMS_PADDING; - r - }; - View::line(ui, LinePosition::TOP, &r, Colors::stroke()); }); - } + + // Draw content divider line. + let r = { + let mut r = rect.clone(); + r.min.y -= View::TAB_ITEMS_PADDING; + r.min.x -= View::TAB_ITEMS_PADDING; + r.max.x += View::TAB_ITEMS_PADDING; + r + }; + View::line(ui, LinePosition::TOP, &r, Colors::stroke()); + }); egui::SidePanel::left("wallet_list_panel") .exact_width(if dual_panel && showing_wallet { @@ -218,7 +217,7 @@ impl ContentContainer for WalletsContent { top: 3.0 as i8, bottom: 4.0 as i8, }, - fill: Colors::fill_deep(), + fill: Colors::fill(), ..Default::default() }) .show_animated_inside(ui, !list_hidden, |ui| { @@ -226,13 +225,23 @@ impl ContentContainer for WalletsContent { ui.ctx().request_repaint_after(Duration::from_millis(1000)); } // Show wallet list. - self.wallet_list_ui(ui); + self.wallet_list_ui(ui, cb); }); egui::CentralPanel::default() .frame(egui::Frame { - fill: if creating_wallet { - Colors::TRANSPARENT + inner_margin: if self.showing_settings() { + Margin { + left: (View::far_left_inset_margin(ui) + 4.0) as i8, + right: (View::far_right_inset_margin(ui) + 4.0) as i8, + top: 0, + bottom: 0, + } + } else { + Margin::default() + }, + fill: if self.showing_settings() { + Colors::fill_lite() } else { Colors::fill_deep() }, @@ -258,19 +267,19 @@ impl ContentContainer for WalletsContent { }); } } 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(); - let mut created = false; - // Show wallet creation content. creation.content_ui(ui, cb, |wallet| { - self.wallets.add(wallet.clone()); - if let Ok(_) = wallet.open(pass.clone()) { - self.wallet_content = Some(WalletContent::new(wallet, None)); - } - created = true; + created_wallet = Some(wallet); }); - if created { + 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| { @@ -311,20 +320,12 @@ impl WalletsContent { } return false; } else if self.showing_wallet() { - let content = self.wallet_content.as_mut().unwrap(); - // Close opened QR code scanner. - if content.qr_scan_showing() { - cb.stop_camera(); - content.close_qr_scan(); - return false; + // Go back at stack or close wallet. + if self.wallet_content.can_back() { + self.wallet_content.back(cb); + } else { + self.wallets.select(None); } - // Close account list. - if content.account_list_showing() { - content.close_qr_scan(); - return false; - } - // Close opened wallet. - self.wallet_content = None; return false; } true @@ -332,8 +333,7 @@ impl WalletsContent { /// Check if opened wallet is showing. pub fn showing_wallet(&self) -> bool { - if let Some(wallet_content) = &self.wallet_content { - let w = &wallet_content.wallet; + if let Some(w) = self.wallets.selected().as_ref() { return w.is_open() && !w.is_deleted() && w.get_config().chain_type == AppConfig::chain_type(); } @@ -351,7 +351,7 @@ impl WalletsContent { } /// Handle data from deeplink or opened file. - fn on_data(&mut self, ui: &mut egui::Ui, data: Option) { + 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; @@ -364,9 +364,9 @@ impl WalletsContent { if wallets_size == 1 { let w = self.wallets.list()[0].clone(); if w.is_open() { - self.wallet_content = Some(WalletContent::new(w, data)); + self.select_wallet(&w, data, cb); } else { - self.show_opening_modal(w, data); + self.show_opening_modal(&w, data, cb); } } else { self.wallet_selection_content = WalletsModal::new(None, data, true); @@ -387,69 +387,32 @@ impl WalletsContent { } /// Draw [`TitlePanel`] content. - fn title_ui(&mut self, - ui: &mut egui::Ui, - dual_panel: bool, - show_wallet: bool, - cb: &dyn PlatformCallbacks) { - let show_list = AppConfig::show_wallets_at_dual_panel(); + 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(); - let account_list_showing = show_wallet && self.wallet_content - .as_ref() - .unwrap() - .account_list_showing(); - let qr_scan = { - let mut scan = false; - if show_wallet { - scan = self.wallet_content.as_mut().unwrap().qr_scan_showing(); - } - scan - }; + // Setup title. let title_content = if show_wallet && (!dual_panel || (dual_panel && !show_list)) && !creating_wallet && !showing_settings { - let wallet_content = self.wallet_content.as_ref().unwrap(); - let wallet_tab_type = wallet_content.current_tab.get_type(); - let title_text = if account_list_showing { - t!("wallets.accounts") - } else if qr_scan { - t!("scan_qr") - } else { - wallet_tab_type.name() - }; - if wallet_tab_type == WalletTabType::Settings { - TitleType::Single(TitleContentType::Title(title_text)) - } else { - let subtitle_text = wallet_content.wallet.get_config().name; - TitleType::Single(TitleContentType::WithSubTitle(title_text, subtitle_text, false)) - } + let title = self.wallet_content.title(); + let subtitle = self.wallets.selected().unwrap().get_config().name; + TitleType::Single(TitleContentType::WithSubTitle(title, subtitle, false)) } else { let title_text = if showing_settings { t!("settings") - } else if qr_scan { - t!("scan_qr") } else if creating_wallet { t!("wallets.add") } else { t!("wallets.title") }; - let dual_title = !showing_settings && !qr_scan && !creating_wallet && + let dual_title = !showing_settings && !creating_wallet && show_wallet && dual_panel; if dual_title { - let wallet_content = self.wallet_content.as_ref().unwrap(); - let wallet_tab_type = wallet_content.current_tab.get_type(); - let wallet_title_text = if account_list_showing { - t!("wallets.accounts") - } else { - wallet_tab_type.name() - }; - let wallet_title_content = if wallet_tab_type == WalletTabType::Settings { - TitleContentType::Title(wallet_title_text) - } else { - let subtitle_text = wallet_content.wallet.get_config().name; - TitleContentType::WithSubTitle(wallet_title_text, subtitle_text, false) - }; + let title = self.wallet_content.title(); + let subtitle = self.wallets.selected().unwrap().get_config().name; + let wallet_title_content = TitleContentType::WithSubTitle(title, subtitle, false); TitleType::Dual(TitleContentType::Title(title_text), wallet_title_content) } else { TitleType::Single(TitleContentType::Title(title_text)) @@ -466,22 +429,11 @@ impl WalletsContent { }); } else if show_wallet && !dual_panel { View::title_button_big(ui, ARROW_LEFT, |_| { - // Close QR code scanner. - let wallet_qr_scan = self.wallet_content - .as_ref() - .unwrap() - .qr_scan_showing(); - if wallet_qr_scan { - cb.stop_camera(); - self.wallet_content.as_mut().unwrap().close_qr_scan(); - return; + if self.wallet_content.can_back() { + self.wallet_content.back(cb); + } else { + self.wallets.select(None); } - // Close account list. - if account_list_showing { - self.wallet_content.as_mut().unwrap().close_account_list(); - return; - } - self.wallet_content = None; }); } else if self.creating_wallet() { let mut close = false; @@ -496,21 +448,14 @@ impl WalletsContent { self.creation_content = None; } } else if show_wallet && dual_panel { - if qr_scan { - View::title_button_big(ui, ARROW_LEFT, |_| { - cb.stop_camera(); - self.wallet_content.as_mut().unwrap().close_qr_scan(); - }); + let list_icon = if show_list { + SIDEBAR_SIMPLE } else { - let list_icon = if show_list { - SIDEBAR_SIMPLE - } else { - SUITCASE - }; - View::title_button_big(ui, list_icon, |_| { - AppConfig::toggle_show_wallets_at_dual_panel(); - }); - } + 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(); @@ -530,7 +475,7 @@ impl WalletsContent { } /// Draw list of wallets. - fn wallet_list_ui(&mut self, ui: &mut egui::Ui) { + 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) @@ -542,20 +487,27 @@ impl WalletsContent { ui.add_space(15.0); let list = self.wallets.list().clone(); - for w in &list { + for w in list.iter() { + let id = w.get_config().id; // Remove deleted. if w.is_deleted() { - self.wallet_content = None; - self.wallets.remove(w.get_config().id); + 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.clone(), None); + self.show_opening_modal(w, None, cb); } - self.wallet_item_ui(ui, w); + // Check if wallet is selected. + let current = if let Some(selected) = self.wallets.selected().as_ref() { + selected.get_config().id == id + } else { + false + }; + self.wallet_item_ui(ui, w, current, cb); ui.add_space(5.0); } }); @@ -563,13 +515,12 @@ impl WalletsContent { } /// Draw wallet list item. - fn wallet_item_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { + fn wallet_item_ui(&mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + current: bool, + cb: &dyn PlatformCallbacks) { let config = wallet.get_config(); - let current = if let Some(content) = &self.wallet_content { - content.wallet.get_config().id == config.id && wallet.is_open() - } else { - false - }; // Draw round background. let mut rect = ui.available_rect_before_wrap(); @@ -586,12 +537,11 @@ impl WalletsContent { if !wallet.is_open() { // Show button to open closed wallet. View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || { - self.show_opening_modal(wallet.clone(), None); + self.show_opening_modal(wallet, None, cb); }); if !wallet.syncing() { - let mut show_selection = false; View::item_button(ui, CornerRadius::default(), GLOBE, None, || { - self.wallet_content = Some(WalletContent::new(wallet.clone(), None)); + self.select_wallet(wallet, None, cb); self.conn_selection_content = WalletConnectionModal::new(wallet.get_current_connection()); // Show connection selection modal. @@ -599,17 +549,13 @@ impl WalletsContent { .position(ModalPosition::CenterTop) .title(t!("wallets.conn_method")) .show(); - show_selection = true; }); - if show_selection { - ExternalConnection::check(None, ui.ctx()); - } } } else { if !current { // Show button to select opened wallet. View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || { - self.wallet_content = Some(WalletContent::new(wallet.clone(), None)); + self.select_wallet(wallet, None, cb); }); } // Show button to close opened wallet. @@ -660,14 +606,23 @@ impl WalletsContent { } /// Show [`Modal`] to select and open wallet. - fn show_opening_modal(&mut self, wallet: Wallet, data: Option) { - self.wallet_content = Some(WalletContent::new(wallet.clone(), None)); - self.open_wallet_content = OpenWalletModal::new(data); + fn show_opening_modal(&mut self, wallet: &Wallet, data: Option, cb: &dyn PlatformCallbacks) { + self.select_wallet(wallet, 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, wallet: &Wallet, data: Option, cb: &dyn PlatformCallbacks) { + self.wallet_content.account_content.close_qr_scan(cb); + if let Some(data) = data { + wallet.open_slatepack(data); + } + self.wallets.select(Some(wallet.get_config().id)); + } } /// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time. diff --git a/src/gui/views/wallets/creation/content.rs b/src/gui/views/wallets/creation/content.rs index 422dd48a..4dc9d731 100644 --- a/src/gui/views/wallets/creation/content.rs +++ b/src/gui/views/wallets/creation/content.rs @@ -19,7 +19,7 @@ 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, CameraScanModal}; +use crate::gui::views::{Modal, Content, View, CameraScanContent}; use crate::gui::views::types::{LinePosition, ContentContainer, ModalPosition, QrScanResult}; use crate::gui::views::wallets::creation::MnemonicSetup; use crate::gui::views::wallets::creation::types::Step; @@ -39,7 +39,7 @@ pub struct WalletCreationContent { step: Step, /// QR code scanning [`Modal`] content. - scan_modal_content: Option, + scan_modal_content: Option, /// Mnemonic phrase setup content. mnemonic_setup: MnemonicSetup, @@ -66,7 +66,7 @@ impl ContentContainer for WalletCreationContent { match modal.id { QR_CODE_PHRASE_SCAN_MODAL => { if let Some(content) = self.scan_modal_content.as_mut() { - content.ui(ui, cb, |result| { + content.modal_ui(ui, cb, |result| { match result { QrScanResult::Text(text) => { self.mnemonic_setup.mnemonic.import(&text); @@ -118,7 +118,7 @@ impl WalletCreationContent { top: View::TAB_ITEMS_PADDING as i8, bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8, }, - fill: Colors::fill_deep(), + fill: Colors::fill(), ..Default::default() }) .show_inside(ui, |ui| { @@ -146,6 +146,7 @@ impl WalletCreationContent { top: 3.0 as i8, bottom: 4.0 as i8, }, + fill: Colors::fill_lite(), ..Default::default() }) .show_inside(ui, |ui| { @@ -260,7 +261,7 @@ impl WalletCreationContent { SCAN, t!("scan").to_uppercase()); View::button(ui, scan_text, Colors::white_or_black(false), || { - self.scan_modal_content = Some(CameraScanModal::default()); + self.scan_modal_content = Some(CameraScanContent::default()); // Show QR code scan modal. Modal::new(QR_CODE_PHRASE_SCAN_MODAL) .position(ModalPosition::CenterTop) diff --git a/src/gui/views/wallets/modals/open.rs b/src/gui/views/wallets/modals/open.rs index 4a117691..08b51dfa 100644 --- a/src/gui/views/wallets/modals/open.rs +++ b/src/gui/views/wallets/modals/open.rs @@ -25,18 +25,14 @@ pub struct OpenWalletModal { pass_edit: String, /// Flag to check if wrong password was entered. wrong_pass: bool, - - /// Optional data to pass after wallet opening. - data: Option, } impl OpenWalletModal { /// Create new content instance. - pub fn new(data: Option) -> Self { + pub fn new() -> Self { Self { pass_edit: "".to_string(), wrong_pass: false, - data, } } /// Draw [`Modal`] content. @@ -44,14 +40,14 @@ impl OpenWalletModal { ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks, - mut on_continue: impl FnMut(ZeroingString, &Option) -> bool) { + 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), &m.data); + m.wrong_pass = !on_continue(ZeroingString::from(pass)); if !m.wrong_pass { m.pass_edit = "".to_string(); Modal::close(); diff --git a/src/gui/views/wallets/wallet/account/content.rs b/src/gui/views/wallets/wallet/account/content.rs index e97f769c..923de02f 100644 --- a/src/gui/views/wallets/wallet/account/content.rs +++ b/src/gui/views/wallets/wallet/account/content.rs @@ -7,36 +7,35 @@ // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, +// distributed under the License is distr1ibuted on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use eframe::emath::Align; -use eframe::epaint::StrokeKind; -use egui::{Layout, RichText}; +use egui::{Align, Layout, RichText, StrokeKind}; use grin_core::core::amount_to_hr_string; -use crate::gui::icons::{FOLDER_USER, PACKAGE, SCAN, SPINNER, USERS_THREE}; +use crate::gui::icons::{FOLDER_USER, PACKAGE, SCAN, SPINNER, USERS_THREE, USER_PLUS}; 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::account::list::WalletAccountsContent; use crate::gui::views::wallets::wallet::types::{WalletContentContainer, GRIN}; -use crate::gui::views::{CameraContent, Content, Modal, View}; +use crate::gui::views::{CameraContent, CameraScanContent, Content, Modal, View}; use crate::gui::Colors; -use crate::gui::views::types::ModalPosition; -use crate::gui::views::wallets::wallet::account::create::CreateAccountContent; use crate::wallet::{Wallet, WalletConfig}; /// Wallet account panel content. pub struct AccountContent { /// Account list content. - accounts_content: Option, - + pub list_content: Option, /// Account creation [`Modal`] content. create_account_content: CreateAccountContent, /// QR code scan content. qr_scan_content: Option, + /// QR code scan result + qr_scan_result: Option, } /// Account creation [`Modal`] identifier. @@ -49,7 +48,11 @@ impl WalletContentContainer for AccountContent { ] } - fn modal_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal, cb: &dyn PlatformCallbacks) { + 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), _ => {} @@ -57,13 +60,14 @@ impl WalletContentContainer for AccountContent { } fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - if self.qr_scan_content.is_some() { + 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.accounts_content.is_some() { + if self.list_content.is_some() { self.list_ui(ui, wallet); } else { + // Show account content. self.account_ui(ui, wallet, cb); } }); @@ -74,14 +78,44 @@ impl WalletContentContainer for AccountContent { impl Default for AccountContent { fn default() -> Self { Self { - accounts_content: None, + list_content: None, create_account_content: CreateAccountContent::default(), qr_scan_content: None, + qr_scan_result: None, } } } impl AccountContent { + /// 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; + } + + /// Check if it's possible to go back at navigation stack. + pub fn can_back(&self) -> bool { + self.qr_scan_showing() || self.list_content.is_some() + } + + /// 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.list_content.is_some() { + self.list_content = None; + } + } + /// Draw wallet account content. fn account_ui(&mut self, ui: &mut egui::Ui, @@ -91,17 +125,19 @@ impl AccountContent { if wallet.get_data().is_none() { return; } + let data = wallet.get_data().unwrap(); 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_lite(), + Colors::fill(), View::item_stroke(), - StrokeKind::Middle); + StrokeKind::Outside); ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { // Draw button to show QR code scanner. @@ -111,8 +147,13 @@ impl AccountContent { }); // Draw button to show list of accounts. - View::item_button(ui, View::item_rounding(1, 3, true), USERS_THREE, None, || { - let accounts = wallet.accounts(); + let accounts = wallet.accounts(); + let accounts_icon = if accounts.len() > 1 { + USERS_THREE + } else { + USER_PLUS + }; + View::item_button(ui, View::item_rounding(1, 3, true), accounts_icon, None, || { if accounts.len() == 1 { self.create_account_content = CreateAccountContent::default(); Modal::new(CREATE_MODAL_ID) @@ -120,7 +161,7 @@ impl AccountContent { .title(t!("wallets.accounts")) .show(); } else { - self.accounts_content = Some( + self.list_content = Some( WalletAccountsContent::new(accounts, wallet.get_config().account) ); } @@ -154,7 +195,9 @@ impl AccountContent { View::ellipsize_text(ui, acc_text, 15.0, Colors::text(false)); // Show confirmed height or sync progress. - let status_text = if !wallet.syncing() { + 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(); @@ -183,7 +226,7 @@ impl AccountContent { status_text, 15.0, Colors::gray(), - wallet.syncing()); + wallet.syncing() || wallet.message_opening()); }) }); }); @@ -191,14 +234,14 @@ impl AccountContent { /// Draw account list content. fn list_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { - if let Some(accounts) = self.accounts_content.as_mut() { + if let Some(accounts) = self.list_content.as_mut() { let mut selected = false; accounts.ui(ui, |acc| { let _ = wallet.set_active_account(&acc.label); selected = true; }); if selected { - self.accounts_content = None; + self.list_content = None; return; } } else { @@ -216,12 +259,12 @@ impl AccountContent { ui.columns(2, |columns| { columns[0].vertical_centered_justified(|ui| { View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - self.accounts_content = None; + self.list_content = None; }); }); columns[1].vertical_centered_justified(|ui| { View::button(ui, t!("modal.add"), Colors::white_or_black(false), || { - self.accounts_content = None; + self.list_content = None; self.create_account_content = CreateAccountContent::default(); Modal::new(CREATE_MODAL_ID) .position(ModalPosition::CenterTop) @@ -234,53 +277,42 @@ impl AccountContent { } /// Draw QR code scanner content. - fn qr_scan_ui(&mut self, ui: &mut egui::Ui, _: &Wallet, cb: &dyn PlatformCallbacks) { - if let Some(_) = self.qr_scan_content.as_ref().unwrap().qr_scan_result() { - // match result { - // QrScanResult::Address(address) => { - // self.current_tab = - // Box::new(WalletTransport::new(Some(address.to_string()))); - // } - // _ => { - // self.current_tab = - // Box::new(WalletMessages::new(Some(result.text()))) - // } - // } - // Stop camera and close scanning. - cb.stop_camera(); - self.qr_scan_content = None; - } else { - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH, |ui| { - 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), || { - cb.stop_camera(); - self.qr_scan_content = None; + 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(_) => { + //TODO: send with address + } + QrScanResult::Slatepack(m) => { + wallet.open_slatepack(m.to_string()); + } + _ => { + 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); - }); - } - } - - /// Check if account list is showing. - pub fn account_list_showing(&self) -> bool { - self.accounts_content.is_some() - } - - /// Close account list. - pub fn close_account_list(&mut self) { - self.accounts_content = None; - } - - /// Check if QR code scanner is showing. - pub fn qr_scan_showing(&self) -> bool { - self.qr_scan_content.is_some() - } - - /// Close QR code scanner. - pub fn close_qr_scan(&mut self) { - self.qr_scan_content = None; + } + ui.add_space(6.0); + }); } } \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/account/list.rs b/src/gui/views/wallets/wallet/account/list.rs index ccdf139e..83a63459 100644 --- a/src/gui/views/wallets/wallet/account/list.rs +++ b/src/gui/views/wallets/wallet/account/list.rs @@ -45,14 +45,10 @@ impl WalletAccountsContent { ScrollArea::vertical() .id_salt("account_list_scroll") .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .max_height(266.0) + .max_height(411.0) .auto_shrink([true; 2]) .show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| { for index in row_range { - // Add space before the first item. - if index == 0 { - ui.add_space(4.0); - } let acc = self.accounts.get(index).unwrap().clone(); self.account_item_ui(ui, &acc, index, size, || { on_select(acc.clone()); @@ -100,15 +96,18 @@ impl WalletAccountsContent { 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.add_space(8.0); ui.vertical(|ui| { - ui.add_space(4.0); + 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.label(RichText::new(amount_text) - .size(18.0) - .color(Colors::white_or_black(true))); + 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. diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 283c8dc6..79cd4026 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -16,95 +16,101 @@ use egui::scroll_area::ScrollBarVisibility; use egui::{Id, Margin, RichText, ScrollArea}; use grin_chain::SyncStatus; use std::time::Duration; - -use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CHAT_CIRCLE_TEXT, GEAR_FINE, GRAPH, POWER}; +use grin_wallet_libwallet::Error; +use crate::gui::icons::{ARROWS_CLOCKWISE, FILE_ARROW_DOWN, FILE_ARROW_UP, GEAR_FINE, POWER, STACK}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::types::{ContentContainer, LinePosition}; +use crate::gui::views::types::{LinePosition, ModalPosition}; use crate::gui::views::wallets::types::{WalletTab, WalletTabType}; use crate::gui::views::wallets::wallet::account::AccountContent; +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::WalletSettings; -use crate::gui::views::wallets::{WalletMessages, WalletTransactions, WalletTransport}; -use crate::gui::views::{Content, Modal, View}; +use crate::gui::views::wallets::WalletTransactions; +use crate::gui::views::{Content, FilePickContent, FilePickContentType, Modal, View}; use crate::gui::Colors; use crate::node::Node; -use crate::wallet::types::ConnectionMethod; +use crate::wallet::types::{ConnectionMethod, WalletTransaction}; use crate::wallet::{ExternalConnection, Wallet}; use crate::AppConfig; -use crate::gui::views::wallets::wallet::types::WalletContentContainer; /// Wallet content. pub struct WalletContent { - /// Selected and opened wallet. - pub wallet: Wallet, /// Current tab content to show. - pub current_tab: Box, + current_tab: Box, /// Account panel content. - account_content: AccountContent, + pub account_content: AccountContent, + /// Transport panel content. + pub transport_content: WalletTransportContent, + + /// Invoice request creation [`Modal`] content. + invoice_request_content: Option, + /// Send request creation [`Modal`] content. + send_request_content: Option, + + /// Tab button to pick file for parsing. + file_pick_tab_button: FilePickContent, } -impl ContentContainer for WalletContent { +/// Identifier for invoice creation [`Modal`]. +const INVOICE_MODAL_ID: &'static str = "invoice_request_modal"; +/// Identifier for sending request creation [`Modal`]. +const SEND_MODAL_ID: &'static str = "send_request_modal"; + +impl WalletContentContainer for WalletContent { fn modal_ids(&self) -> Vec<&'static str> { - vec![] + vec![ + INVOICE_MODAL_ID, + SEND_MODAL_ID + ] } - fn modal_ui(&mut self, _: &mut egui::Ui, _: &Modal, _: &dyn PlatformCallbacks) { + fn modal_ui(&mut self, ui: &mut egui::Ui, w: &Wallet, m: &Modal, cb: &dyn PlatformCallbacks) { + match m.id { + INVOICE_MODAL_ID => { + if let Some(c) = self.invoice_request_content.as_mut() { + c.modal_ui(ui, w, m, cb); + } + } + SEND_MODAL_ID => { + if let Some(c) = self.send_request_content.as_mut() { + c.modal_ui(ui, w, m, cb); + } + } + _ => {} + } } - fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - ui.ctx().request_repaint_after(Duration::from_millis(1000)); + fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { + if self.account_content.can_back() { + ui.ctx().request_repaint(); + } else { + ui.ctx().request_repaint_after(Duration::from_millis(1000)); + } let dual_panel = Content::is_dual_panel_mode(ui.ctx()); let show_wallets_dual = AppConfig::show_wallets_at_dual_panel(); - let wallet = &self.wallet; let wallet_id = wallet.identifier(); let data = wallet.get_data(); - let show_qr_scan = self.account_content.qr_scan_showing(); - let block_nav = Self::block_navigation_on_sync(wallet); + 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.current_tab.get_type() != WalletTabType::Settings && !block_nav && !wallet.sync_error() && data.is_some(); - if wallet.get_current_connection() == ConnectionMethod::Integrated && !Node::is_running() { + if wallet.get_current_connection() == ConnectionMethod::Integrated && + !Node::is_running() { show_account = false; } - // Close scanner when balance got hidden. - if !show_account && show_qr_scan { - cb.stop_camera(); - self.account_content.close_qr_scan(); + + // Consume inserted message. + if let Some(res) = wallet.consume_message_result() { + self.on_transaction(res); } - egui::TopBottomPanel::top(Id::from("wallet_account").with(wallet.identifier())) - .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: 4.0 as i8, - bottom: 0.0 as i8, - }, - fill: Colors::fill(), - ..Default::default() - }) - .show_animated_inside(ui, show_account, |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 -= 4.0 + View::far_left_inset_margin(ui); - r.min.y -= 4.0; - r.max.x += 4.0 + View::get_right_inset(); - r - }; - View::line(ui, LinePosition::BOTTOM, &r, Colors::item_stroke()); - if dual_panel && show_wallets_dual && !show_qr_scan { - View::line(ui, LinePosition::LEFT, &r, Colors::item_stroke()); - } - }); // Show wallet tabs. - let show_tabs = !block_nav && !self.qr_scan_showing() && !self.account_list_showing(); egui::TopBottomPanel::bottom("wallet_tabs") .frame(egui::Frame { inner_margin: Margin { @@ -116,55 +122,146 @@ impl ContentContainer for WalletContent { fill: Colors::fill(), ..Default::default() }) - .show_animated_inside(ui, show_tabs, |ui| { - let rect = ui.available_rect_before_wrap(); + .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(ui); + self.tabs_ui(wallet, ui, cb); }); let rect = { - let mut r = rect.clone(); + let mut r = r.clone(); r.min.x -= View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING; r.min.y -= View::TAB_ITEMS_PADDING; r.max.x += View::get_right_inset() + View::TAB_ITEMS_PADDING; r.max.y += View::get_bottom_inset() + View::TAB_ITEMS_PADDING; r }; - // Draw content divider line. - View::line(ui, LinePosition::TOP, &rect, Colors::stroke()); + // 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 when account panel got hidden. + if !show_account && self.account_content.qr_scan_showing() { + self.account_content.close_qr_scan(cb); + } + + // 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) + 4.0) as i8, + right: (View::get_right_inset() + 4.0) as i8, + top: 4.0 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 -= 4.0 + View::far_left_inset_margin(ui); + r.min.y -= 4.0; + r.max.x += 4.0 + 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) + 4.0) as i8, + right: (View::get_right_inset() + 4.0) as i8, + top: 1.0 as i8, + bottom: 1.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(); + 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 -= 4.0 + View::far_left_inset_margin(ui); + r.min.y -= 1.0; + r.max.x += 4.0 + 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 { + inner_margin: Margin { left: (View::far_left_inset_margin(ui) + 4.0) as i8, right: (View::get_right_inset() + 4.0) as i8, top: 0.0 as i8, bottom: 4.0 as i8, }, + fill: if self.current_tab.get_type() == WalletTabType::Settings { + Colors::fill_lite() + } else { + Colors::TRANSPARENT + }, ..Default::default() }) .show_inside(ui, |ui| { let rect = ui.available_rect_before_wrap(); let tab_type = self.current_tab.get_type(); let show_sync = (tab_type != WalletTabType::Settings || block_nav) && - sync_ui(ui, &self.wallet); + sync_ui(ui, &wallet); if !show_sync { if tab_type != WalletTabType::Txs { ui.add_space(3.0); ScrollArea::vertical() - .id_salt(Id::from("wallet_scroll") + .id_salt(Id::from("wallet_tab_content_scroll") .with(tab_type.name()) .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.current_tab.ui(ui, &self.wallet, cb); + self.current_tab.ui(ui, &wallet, cb); }); }); } else { - self.current_tab.ui(ui, &self.wallet, cb); + self.current_tab.ui(ui, &wallet, cb); } } let rect = { @@ -174,12 +271,10 @@ impl ContentContainer for WalletContent { r.max.y += 4.0; r }; - // Draw cover when account list or QR code scanner is active. - if !show_sync && (self.account_list_showing() || self.qr_scan_showing()) { - View::content_cover_ui(ui, rect, "wallet_content_cover", || { - cb.stop_camera(); - self.account_content.close_qr_scan(); - self.account_content.close_account_list(); + // 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. @@ -190,48 +285,58 @@ impl ContentContainer for WalletContent { } } -impl WalletContent { - /// Create new instance with optional data. - pub fn new(wallet: Wallet, data: Option) -> Self { - let account_content = AccountContent::default(); - let mut content = Self { - wallet, - current_tab: Box::new(WalletTransactions::default()), - account_content, - }; - if data.is_some() { - content.on_data(data); +impl Default for WalletContent { + fn default() -> Self { + Self { + current_tab: Box::new(WalletTransactions::new(None)), + account_content: AccountContent::default(), + transport_content: WalletTransportContent::default(), + invoice_request_content: None, + send_request_content: None, + file_pick_tab_button: FilePickContent::new(FilePickContentType::Tab), + } + } +} + +impl WalletContent { + /// Get title based on current navigation state. + pub fn title(&self) -> String { + if self.account_content.qr_scan_showing() { + t!("scan_qr") + } else if self.account_content.list_content.is_some() { + 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 { + self.current_tab.get_type().name() } - content } - /// Check account list is showing. - pub fn account_list_showing(&self) -> bool { - self.account_content.account_list_showing() + /// Callback on incoming transaction for user to take action. + fn on_transaction(&mut self, tx_result: Result) { + if let Ok(tx) = tx_result { + self.current_tab = Box::new(WalletTransactions::new(Some(tx))); + } } - /// Close QR code scanner. - pub fn close_account_list(&mut self) { - self.account_content.close_account_list(); + /// 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 QR code scanner is opened. - pub fn qr_scan_showing(&self) -> bool { - self.account_content.qr_scan_showing() - } - - /// Close QR code scanner. - pub fn close_qr_scan(&mut self) { - self.account_content.close_qr_scan(); - } - - /// Handle data from deeplink or opened file. - pub fn on_data(&mut self, data: Option) { - self.current_tab = Box::new(WalletMessages::new(data)); + /// 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. - pub fn block_navigation_on_sync(wallet: &Wallet) -> bool { + 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); @@ -244,7 +349,7 @@ impl WalletContent { } /// Draw tab buttons at the bottom of the screen. - fn tabs_ui(&mut self, ui: &mut egui::Ui) { + fn tabs_ui(&mut self, wallet: &Wallet, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { ui.scope(|ui| { // Setup spacing between tabs. ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); @@ -252,28 +357,60 @@ impl WalletContent { // Setup vertical padding inside buttons. ui.style_mut().spacing.button_padding = egui::vec2(0.0, 4.0); + let has_wallet_data = wallet.get_data().is_some(); + let can_send = has_wallet_data && + wallet.get_data().unwrap().info.amount_currently_spendable > 0; + let current_type = self.current_tab.get_type(); - ui.columns(4, |columns| { + let tabs_amount = if can_send { 5 } else { 4 }; + ui.columns(tabs_amount, |columns| { columns[0].vertical_centered_justified(|ui| { - View::tab_button(ui, GRAPH, current_type == WalletTabType::Txs, |_| { - self.current_tab = Box::new(WalletTransactions::default()); + View::tab_button(ui, STACK, None, Some(current_type == WalletTabType::Txs), |_| { + self.current_tab = Box::new(WalletTransactions::new(None)); }); }); columns[1].vertical_centered_justified(|ui| { - let is_messages = current_type == WalletTabType::Messages; - View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, |_| { - self.current_tab = Box::new( - WalletMessages::new(None) - ); + let active = if has_wallet_data { + Some(false) + } else { + None + }; + View::tab_button(ui, FILE_ARROW_DOWN, Some(Colors::green()), active, |_| { + self.invoice_request_content = Some(InvoiceRequestContent::default()); + Modal::new(INVOICE_MODAL_ID) + .position(ModalPosition::CenterTop) + .title(t!("wallets.receive")) + .show(); }); }); columns[2].vertical_centered_justified(|ui| { - View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, |_| { - self.current_tab = Box::new(WalletTransport::new(None)); - }); + if wallet.message_opening() { + View::small_loading_spinner(ui); + } else { + let mut message = "".to_string(); + self.file_pick_tab_button.ui(ui, cb, |m| { + message = m; + }); + if !message.is_empty() { + wallet.open_slatepack(message); + } + } }); - columns[3].vertical_centered_justified(|ui| { - View::tab_button(ui, GEAR_FINE, current_type == WalletTabType::Settings, |ui| { + if can_send { + columns[3].vertical_centered_justified(|ui| { + View::tab_button(ui, FILE_ARROW_UP, Some(Colors::red()), Some(false), |_| { + self.send_request_content = Some(SendRequestContent::new(None)); + Modal::new(SEND_MODAL_ID) + .position(ModalPosition::CenterTop) + .title(t!("wallets.send")) + .show(); + }); + }); + } + let settings_index = if tabs_amount == 5 { 4 } else { 3 }; + columns[settings_index].vertical_centered_justified(|ui| { + let active = Some(current_type == WalletTabType::Settings); + View::tab_button(ui, GEAR_FINE, None, active, |ui| { ExternalConnection::check(None, ui.ctx()); self.current_tab = Box::new(WalletSettings::default()); }); diff --git a/src/gui/views/wallets/wallet/messages/content.rs b/src/gui/views/wallets/wallet/messages/content.rs index 1df6c3f6..51bb940a 100644 --- a/src/gui/views/wallets/wallet/messages/content.rs +++ b/src/gui/views/wallets/wallet/messages/content.rs @@ -23,11 +23,11 @@ use parking_lot::RwLock; use crate::gui::Colors; use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, DOWNLOAD_SIMPLE, SCAN, UPLOAD_SIMPLE}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{FilePickButton, Modal, View, CameraScanModal}; +use crate::gui::views::{FilePickContent, Modal, View, CameraScanContent, FilePickContentType}; use crate::gui::views::types::{ModalPosition, QrScanResult}; -use crate::gui::views::wallets::wallet::messages::request::MessageRequestModal; +use crate::gui::views::wallets::wallet::messages::request::RequestModalContent; use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType}; -use crate::gui::views::wallets::wallet::WalletTransactionModal; +use crate::gui::views::wallets::wallet::WalletTransactionContent; use crate::wallet::types::WalletTransaction; use crate::wallet::Wallet; @@ -37,10 +37,7 @@ pub struct WalletMessages { first_draw: bool, /// Invoice or sending request creation [`Modal`] content. - request_modal_content: MessageRequestModal, - - /// Wallet transaction [`Modal`] content. - tx_info_content: WalletTransactionModal, + request_modal_content: RequestModalContent, /// Slatepacks message input text. message_edit: String, @@ -52,10 +49,10 @@ pub struct WalletMessages { message_result: Arc)>>>, /// QR code scanner [`Modal`] content. - scan_modal_content: Option, + scan_modal_content: Option, /// Button to parse picked file content. - file_pick_button: FilePickButton, + file_pick_button: FilePickContent, } /// Identifier for amount input [`Modal`] to create invoice or sending request. @@ -65,17 +62,6 @@ const TX_INFO_MODAL: &'static str = "messages_tx_info_modal"; /// Identifier for [`Modal`] to scan Slatepack message from QR code. const SCAN_QR_MODAL: &'static str = "messages_scan_qr_modal"; -impl WalletTab for WalletMessages { - fn get_type(&self) -> WalletTabType { - WalletTabType::Messages - } - - fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - self.modal_content_ui(ui, wallet, cb); - self.messages_ui(ui, wallet, cb); - } -} - impl WalletMessages { /// Create new content instance, put message into input if provided. pub fn new(message: Option) -> Self { @@ -85,9 +71,8 @@ impl WalletMessages { message_loading: false, message_error: "".to_string(), message_result: Arc::new(Default::default()), - tx_info_content: WalletTransactionModal::new(None, false), - request_modal_content: MessageRequestModal::new(false), - file_pick_button: FilePickButton::default(), + request_modal_content: RequestModalContent::new(false), + file_pick_button: FilePickContent::new(FilePickContentType::Button), scan_modal_content: None, } } @@ -133,16 +118,11 @@ impl WalletMessages { self.request_modal_content.ui(ui, wallet, modal, cb); }); } - TX_INFO_MODAL => { - Modal::ui(ui.ctx(), cb, |ui, modal, cb| { - self.tx_info_content.ui(ui, wallet, modal, cb); - }); - } SCAN_QR_MODAL => { let mut result = None; if let Some(content) = self.scan_modal_content.as_mut() { Modal::ui(ui.ctx(), cb, |ui, _, cb| { - content.ui(ui, cb, |res| { + content.modal_ui(ui, cb, |res| { result = Some(res.clone()); Modal::close(); }); @@ -213,7 +193,7 @@ impl WalletMessages { /// Show [`Modal`] to create invoice or sending request. fn show_request_modal(&mut self, invoice: bool) { - self.request_modal_content = MessageRequestModal::new(invoice); + self.request_modal_content = RequestModalContent::new(invoice); let title = if invoice { t!("wallets.receive") } else { @@ -295,7 +275,7 @@ impl WalletMessages { Ok(tx) => { self.message_edit.clear(); // Show transaction modal on success. - self.tx_info_content = WalletTransactionModal::new(Some(tx.data.id), false); + // self.tx_info_content = WalletTransactionContent::new(Some(tx.data.id), false); Modal::new(TX_INFO_MODAL) .position(ModalPosition::CenterTop) .title(t!("wallets.tx")) @@ -318,8 +298,8 @@ impl WalletMessages { // Show tx modal or show default error message. if let Some(tx) = wallet.tx_by_slate(&slate).as_ref() { self.message_edit.clear(); - self.tx_info_content = - WalletTransactionModal::new(Some(tx.data.id), false); + // self.tx_info_content = + // WalletTransactionContent::new(Some(tx.data.id), false); Modal::new(TX_INFO_MODAL) .position(ModalPosition::CenterTop) .title(t!("wallets.tx")) @@ -352,7 +332,7 @@ impl WalletMessages { View::button(ui, scan_text, Colors::white_or_black(false), || { self.message_edit.clear(); self.message_error.clear(); - self.scan_modal_content = Some(CameraScanModal::default()); + self.scan_modal_content = Some(CameraScanContent::default()); // Show QR code scan modal. Modal::new(SCAN_QR_MODAL) .position(ModalPosition::CenterTop) @@ -422,7 +402,7 @@ impl WalletMessages { if exists { if let Some(tx) = wallet.tx_by_slate(&slate).as_ref() { self.message_edit.clear(); - self.tx_info_content = WalletTransactionModal::new(Some(tx.data.id), false); + // self.tx_info_content = WalletTransactionContent::new(Some(tx.data.id), false); Modal::new(TX_INFO_MODAL) .position(ModalPosition::CenterTop) .title(t!("wallets.tx")) diff --git a/src/gui/views/wallets/wallet/messages/request.rs b/src/gui/views/wallets/wallet/messages/request.rs index 36aa08f2..4beb0f68 100644 --- a/src/gui/views/wallets/wallet/messages/request.rs +++ b/src/gui/views/wallets/wallet/messages/request.rs @@ -22,12 +22,12 @@ use grin_wallet_libwallet::Error; use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, TextEdit, View}; -use crate::gui::views::wallets::wallet::WalletTransactionModal; +use crate::gui::views::wallets::wallet::WalletTransactionContent; use crate::wallet::types::WalletTransaction; use crate::wallet::Wallet; /// Invoice or sending request creation [`Modal`] content. -pub struct MessageRequestModal { +pub struct RequestModalContent { /// Flag to check if invoice or sending request was opened. invoice: bool, @@ -42,10 +42,10 @@ pub struct MessageRequestModal { request_error: Option, /// Request result transaction content. - result_tx_content: Option, + result_tx_content: Option, } -impl MessageRequestModal { +impl RequestModalContent { /// Create new content instance. pub fn new(invoice: bool) -> Self { Self { @@ -71,7 +71,7 @@ impl MessageRequestModal { } // Setup callback on continue. - let on_continue = |m: &mut MessageRequestModal| { + let on_continue = |m: &mut RequestModalContent| { if m.amount_edit.is_empty() { return; } @@ -230,7 +230,7 @@ impl MessageRequestModal { match result { Ok(tx) => { self.result_tx_content = - Some(WalletTransactionModal::new(Some(tx.data.id), false)); + Some(WalletTransactionContent::new(tx, false)); } Err(err) => { match err { diff --git a/src/gui/views/wallets/wallet/mod.rs b/src/gui/views/wallets/wallet/mod.rs index 90dc16d0..3ec02686 100644 --- a/src/gui/views/wallets/wallet/mod.rs +++ b/src/gui/views/wallets/wallet/mod.rs @@ -20,13 +20,9 @@ pub use settings::*; mod txs; pub use txs::*; -mod messages; -pub use messages::WalletMessages; - -mod transport; -pub use transport::WalletTransport; - mod content; pub use content::WalletContent; -mod account; \ No newline at end of file +mod account; +mod transport; +mod request; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/request/invoice.rs b/src/gui/views/wallets/wallet/request/invoice.rs new file mode 100644 index 00000000..226728a3 --- /dev/null +++ b/src/gui/views/wallets/wallet/request/invoice.rs @@ -0,0 +1,214 @@ +// Copyright 2025 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use egui::{Id, RichText}; +use grin_core::core::amount_from_hr_string; +use grin_wallet_libwallet::Error; +use parking_lot::RwLock; +use std::sync::Arc; +use std::thread; + +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::wallets::wallet::WalletTransactionContent; +use crate::gui::views::{Modal, TextEdit, View}; +use crate::gui::Colors; +use crate::wallet::types::WalletTransaction; +use crate::wallet::Wallet; + +/// Invoice request creation content. +pub struct InvoiceRequestContent { + /// Amount to receive. + amount_edit: String, + + /// Flag to check if request is loading. + request_loading: bool, + /// Request result if there is no error. + request_result: Arc>>>, + /// Flag to check if there is an error happened on request creation. + request_error: Option, + + /// Request result transaction content. + result_tx_content: Option, +} + +impl Default for InvoiceRequestContent { + fn default() -> Self { + Self { + amount_edit: "".to_string(), + request_loading: false, + request_result: Arc::new(RwLock::new(None)), + request_error: None, + result_tx_content: None, + } + } +} + +impl InvoiceRequestContent { + /// Draw [`Modal`] content. + pub fn modal_ui(&mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + // Draw transaction information on request result. + if let Some(tx) = self.result_tx_content.as_mut() { + tx.ui(ui, wallet, modal, cb); + return; + } + + // 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()) { + modal.disable_closing(); + // Setup data for request. + let wallet = wallet.clone(); + let result = m.request_result.clone(); + // Send request at another thread. + m.request_loading = true; + thread::spawn(move || { + let res = wallet.issue_invoice(a); + let mut w_result = result.write(); + *w_result = Some(res); + }); + } else { + m.request_error = Some(t!("wallets.invoice_slatepack_err")); + } + }; + + ui.add_space(6.0); + + // Draw content on request loading. + if self.request_loading { + self.loading_request_ui(ui, modal); + return; + } + + // 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); + } + + // Check value if input was changed. + if amount_edit_before != self.amount_edit { + self.request_error = None; + 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(a) => { + if !self.amount_edit.contains(".") { + // To avoid input of several "0". + if a == 0 { + self.amount_edit = "0".to_string(); + return; + } + } else { + // Check input after ".". + let parts = self.amount_edit + .split(".") + .collect::>(); + if parts.len() == 2 && parts[1].len() > 9 { + self.amount_edit = amount_edit_before; + return; + } + } + } + Err(_) => { + self.amount_edit = amount_edit_before; + } + } + } + } + + // Show request creation error. + if let Some(err) = &self.request_error { + ui.add_space(12.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(err) + .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); + + 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(); + self.request_error = None; + 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); + } + + /// Draw loading request content. + fn loading_request_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) { + ui.add_space(34.0); + ui.vertical_centered(|ui| { + View::big_loading_spinner(ui); + }); + ui.add_space(50.0); + + // Check if there is request result error. + if self.request_error.is_some() { + modal.enable_closing(); + self.request_loading = false; + return; + } + + // Update data on request result. + let r_request = self.request_result.read(); + if r_request.is_some() { + modal.enable_closing(); + let result = r_request.as_ref().unwrap(); + match result { + Ok(tx) => { + self.result_tx_content = + Some(WalletTransactionContent::new(tx, false)); + } + Err(_) => { + self.request_error = Some(t!("wallets.invoice_slatepack_err")); + self.request_loading = false; + } + } + } + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/request/mod.rs b/src/gui/views/wallets/wallet/request/mod.rs new file mode 100644 index 00000000..c97a1d5b --- /dev/null +++ b/src/gui/views/wallets/wallet/request/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2025 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod invoice; +pub use invoice::*; + +mod send; +pub use send::*; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/transport/send.rs b/src/gui/views/wallets/wallet/request/send.rs similarity index 57% rename from src/gui/views/wallets/wallet/transport/send.rs rename to src/gui/views/wallets/wallet/request/send.rs index 3b9ee40b..a2341c10 100644 --- a/src/gui/views/wallets/wallet/transport/send.rs +++ b/src/gui/views/wallets/wallet/request/send.rs @@ -1,4 +1,4 @@ -// Copyright 2024 The Grim Developers +// Copyright 2025 The Grim Developers // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,87 +12,129 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::Arc; -use std::thread; -use egui::{Id, RichText}; use grin_core::core::{amount_from_hr_string, amount_to_hr_string}; use grin_wallet_libwallet::{Error, SlatepackAddress}; use parking_lot::RwLock; +use std::sync::Arc; +use std::thread; +use egui::{Id, RichText}; use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::wallets::wallet::WalletTransactionContent; use crate::gui::views::{CameraContent, Modal, TextEdit, View}; -use crate::gui::views::wallets::wallet::WalletTransactionModal; +use crate::gui::views::types::ModalPosition; use crate::wallet::types::WalletTransaction; use crate::wallet::Wallet; -/// Transport sending [`Modal`] content. -pub struct TransportSendModal { - /// Flag to check if transaction is sending to show progress. - sending: bool, - /// Flag to check if there is an error to repeat. - error: bool, - /// Transaction result. - send_result: Arc>>>, - - /// Entered amount value. +/// Content to create a request to send funds. +pub struct SendRequestContent { + /// Amount to send or receive. amount_edit: String, - /// Entered address value. + /// Receiver address. address_edit: String, /// Flag to check if entered address is incorrect. address_error: bool, + /// Flag to check if request is loading. + request_loading: bool, + /// Request result if there is no error. + request_result: Arc>>>, + /// Flag to check if there is an error happened on request creation. + request_error: Option, + /// Address QR code scanner content. address_scan_content: Option, - /// Transaction information content. - tx_info_content: Option, + /// Request result transaction content. + result_tx_content: Option, } -impl TransportSendModal { - /// Create new instance from provided address. +impl SendRequestContent { + /// Create new content instance with optional receiver address. pub fn new(addr: Option) -> Self { Self { - sending: false, - error: false, - send_result: Arc::new(RwLock::new(None)), amount_edit: "".to_string(), address_edit: addr.unwrap_or("".to_string()), address_error: false, + request_loading: false, + request_result: Arc::new(RwLock::new(None)), + request_error: None, address_scan_content: None, - tx_info_content: None, + result_tx_content: None, } } /// Draw [`Modal`] content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { + pub fn modal_ui(&mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { // Draw transaction information on request result. - if let Some(tx) = self.tx_info_content.as_mut() { + if let Some(tx) = self.result_tx_content.as_mut() { tx.ui(ui, wallet, modal, cb); return; } - // Draw sending content, progress or an error. - if self.sending { - self.progress_ui(ui); - } else if self.error { - self.error_ui(ui, wallet, modal); - } else { - self.content_ui(ui, wallet, modal, cb); - } - } + // Setup callback on continue. + let on_continue = |m: &mut SendRequestContent| { + if m.amount_edit.is_empty() { + return; + } + // Check address to send over Tor if enabled. + let addr_str = m.address_edit.as_str(); + if let Ok(addr) = SlatepackAddress::try_from(addr_str.trim()) { + if let Ok(a) = amount_from_hr_string(m.amount_edit.as_str()) { + Modal::change_position(ModalPosition::Center); + modal.disable_closing(); + + let mut wallet = wallet.clone(); + let res = m.request_result.clone(); + + // Send request at another thread. + m.request_loading = true; + thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + let result = wallet.send_tor(a, &addr).await; + let mut w_res = res.write(); + *w_res = Some(result); + }); + }); + } + } else if !addr_str.is_empty() { + m.address_error = true; + } else if let Ok(amount) = amount_from_hr_string(m.amount_edit.as_str()) { + Modal::change_position(ModalPosition::Center); + modal.disable_closing(); + + let wallet = wallet.clone(); + let result = m.request_result.clone(); + + // Send request at another thread. + m.request_loading = true; + thread::spawn(move || { + let res = wallet.send(amount, None); + let mut w_result = result.write(); + *w_result = Some(res); + }); + } else { + m.request_error = Some(t!("wallets.send_slatepack_err")); + } + }; + + // Draw content on request loading. + if self.request_loading { + self.loading_request_ui(ui, modal); + return; + } - /// Draw content to send. - fn content_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 = || { @@ -105,6 +147,7 @@ impl TransportSendModal { on_stop(); self.address_scan_content = None; } else { + ui.add_space(6.0); scanner.ui(ui, cb); ui.add_space(6.0); @@ -115,9 +158,7 @@ impl TransportSendModal { ui.columns(2, |cols| { cols[0].vertical_centered_justified(|ui| { View::button(ui, t!("close"), Colors::white_or_black(false), || { - on_stop(); - self.address_scan_content = None; - Modal::close(); + self.close(); }); }); cols[1].vertical_centered_justified(|ui| { @@ -201,7 +242,7 @@ impl TransportSendModal { }); ui.add_space(6.0); - // Draw address text edit. + // 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) @@ -224,9 +265,19 @@ impl TransportSendModal { self.address_error = false; } - // Send on Enter press. + // Continue on Enter press. if address_edit.enter_pressed { - self.send(wallet, modal); + on_continue(self); + } + + // Show request creation error. + if let Some(err) = &self.request_error { + ui.vertical_centered(|ui| { + ui.label(RichText::new(err) + .size(17.0) + .color(Colors::red())); + }); + ui.add_space(12.0); } // Setup spacing between buttons. @@ -239,36 +290,9 @@ impl TransportSendModal { }); }); columns[1].vertical_centered_justified(|ui| { + // Button to create Slatepack message request. View::button(ui, t!("continue"), Colors::white_or_black(false), || { - self.send(wallet, modal); - }); - }); - }); - ui.add_space(6.0); - } - - /// Draw error content. - fn error_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal) { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("transport.tor_send_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); - - 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| { - View::button(ui, t!("repeat"), Colors::white_or_black(false), || { - self.send(wallet, modal); + on_continue(self); }); }); }); @@ -280,78 +304,72 @@ impl TransportSendModal { self.amount_edit = "".to_string(); self.address_edit = "".to_string(); - let mut w_res = self.send_result.write(); + let mut w_res = self.request_result.write(); *w_res = None; - self.tx_info_content = None; + self.result_tx_content = None; self.address_scan_content = None; Modal::close(); } - /// Send entered amount to address. - fn send(&mut self, wallet: &Wallet, modal: &Modal) { - if self.amount_edit.is_empty() { - return; - } - let addr_str = self.address_edit.as_str(); - if let Ok(addr) = SlatepackAddress::try_from(addr_str.trim()) { - if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) { - modal.disable_closing(); - // Send amount over Tor. - let mut wallet = wallet.clone(); - let res = self.send_result.clone(); - self.sending = true; - thread::spawn(move || { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(async { - let result = wallet.send_tor(a, &addr).await; - let mut w_res = res.write(); - *w_res = Some(result); - }); - }); - } - } else { - self.address_error = true; - } - } - - /// Draw sending progress content. - fn progress_ui(&mut self, ui: &mut egui::Ui) { - ui.add_space(16.0); + /// Draw loading request content. + fn loading_request_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) { + ui.add_space(40.0); ui.vertical_centered(|ui| { - View::small_loading_spinner(ui); - ui.add_space(12.0); - ui.label(RichText::new(t!("transport.tor_sending", "amount" => self.amount_edit)) - .size(17.0) - .color(Colors::gray())); + View::big_loading_spinner(ui); }); - ui.add_space(10.0); + ui.add_space(40.0); - // Check sending result. - let has_result = { - let r_result = self.send_result.read(); - r_result.is_some() + if !self.address_edit.is_empty() { + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("transport.tor_sending", "amount" => self.amount_edit)) + .size(17.0) + .color(Colors::inactive_text())); + }); + ui.add_space(12.0); + } + + // Update data on request result. + let has_res = { + let r_request = self.request_result.read(); + r_request.is_some() }; - if has_result { - { - let res = self.send_result.read().clone().unwrap(); - match res { - Ok(tx) => { - self.tx_info_content = - Some(WalletTransactionModal::new(Some(tx.data.id), false)); - } - Err(_) => { - self.error = true; - } + if has_res { + self.request_loading = false; + modal.enable_closing(); + let r_request = self.request_result.read(); + let result = r_request.as_ref().unwrap(); + match result { + Ok(tx) => { + self.result_tx_content = + Some(WalletTransactionContent::new(tx, false)); + } + Err(err) => { + let m = match err { + Error::NotEnoughFunds { .. } => { + t!("wallets.pay_balance_error", "amount" => self.amount_edit) + } + _ => { + if !self.address_edit.is_empty() { + t!("transport.tor_send_error") + } else { + t!("wallets.send_slatepack_err") + } + } + }; + self.request_error = Some(m); } } - let mut w_res = self.send_result.write(); - *w_res = None; - self.sending = false; + } + + // Check if there is request result error. + if self.request_error.is_some() { + Modal::change_position(ModalPosition::CenterTop); + modal.enable_closing(); + let mut w_request = self.request_result.write(); + *w_request = None; + self.request_loading = false; } } } \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/settings/recovery.rs b/src/gui/views/wallets/wallet/settings/recovery.rs index 00c7ed90..f5b59faa 100644 --- a/src/gui/views/wallets/wallet/settings/recovery.rs +++ b/src/gui/views/wallets/wallet/settings/recovery.rs @@ -16,12 +16,12 @@ use egui::{Id, RichText}; use grin_chain::SyncStatus; use grin_util::ZeroingString; -use crate::gui::Colors; -use crate::gui::icons::{EYE, LIFEBUOY, STETHOSCOPE, TRASH, WRENCH}; +use crate::gui::icons::{EYE, KEY, LIFEBUOY, STETHOSCOPE, TRASH}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, TextEdit, View}; 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; @@ -69,7 +69,7 @@ impl WalletContentContainer for RecoverySettings { ui.add_space(10.0); View::horizontal_line(ui, Colors::stroke()); ui.add_space(6.0); - View::sub_title(ui, format!("{} {}", WRENCH, t!("wallets.recovery"))); + View::sub_title(ui, format!("{} {}", KEY, t!("wallets.recovery"))); View::horizontal_line(ui, Colors::stroke()); ui.add_space(4.0); diff --git a/src/gui/views/wallets/wallet/transport/content.rs b/src/gui/views/wallets/wallet/transport/content.rs index 5809c2e4..6f0a4f3e 100644 --- a/src/gui/views/wallets/wallet/transport/content.rs +++ b/src/gui/views/wallets/wallet/transport/content.rs @@ -1,4 +1,4 @@ -// Copyright 2023 The Grim Developers +// Copyright 2025 The Grim Developers // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,362 +14,197 @@ use egui::{Align, CornerRadius, Layout, RichText, StrokeKind}; -use crate::gui::Colors; -use crate::gui::icons::{CHECK_CIRCLE, COPY, DOTS_THREE_CIRCLE, EXPORT, GEAR_SIX, GLOBE_SIMPLE, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, X_CIRCLE}; +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::views::types::ModalPosition; -use crate::gui::views::wallets::wallet::transport::send::TransportSendModal; -use crate::gui::views::wallets::wallet::transport::settings::TransportSettingsModal; -use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType}; +use crate::gui::Colors; use crate::tor::{Tor, TorConfig}; -use crate::wallet::types::WalletData; use crate::wallet::Wallet; -/// Wallet transport tab content. -pub struct WalletTransport { - /// Sending [`Modal`] content. - send_modal_content: Option, +/// Wallet transport panel content. +pub struct WalletTransportContent { + /// QR code address content. + pub qr_address_content: Option, - /// QR code address image [`Modal`] content. - qr_address_content: Option, - - /// Tor settings [`Modal`] content. - settings_modal_content: Option, + /// Settings content. + pub settings_content: Option, } -impl WalletTab for WalletTransport { - fn get_type(&self) -> WalletTabType { - WalletTabType::Transport +impl WalletContentContainer for WalletTransportContent { + fn modal_ids(&self) -> Vec<&'static str> { vec![] } + + fn modal_ui(&mut self, _: &mut egui::Ui, _: &Wallet, _: &Modal, _: &dyn PlatformCallbacks) { } - fn ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - cb: &dyn PlatformCallbacks) { - self.modal_content_ui(ui, wallet, cb); - self.transport_ui(ui, wallet, cb); - } -} + fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { + if let Some(content) = self.qr_address_content.as_mut() { + ui.add_space(6.0); -/// Identifier for [`Modal`] to send amount over Tor. -const SEND_TOR_MODAL: &'static str = "send_tor_modal"; -/// Identifier for [`Modal`] to setup Tor service. -const TOR_SETTINGS_MODAL: &'static str = "tor_settings_modal"; -/// Identifier for [`Modal`] to show QR code address image. -const QR_ADDRESS_MODAL: &'static str = "qr_address_modal"; + // Draw QR code content. + content.ui(ui, cb); -impl WalletTransport { - /// Create new transport content instance, opening sending `Modal` if address was provided. - pub fn new(address: Option) -> Self { - let mut content = Self { - send_modal_content: None, - qr_address_content: None, - settings_modal_content: None, - }; - if address.is_some() { - content.show_send_tor_modal(address) - } - content - } - - /// Draw wallet transport content. - fn transport_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - ui.add_space(3.0); - ui.label(RichText::new(t!("transport.desc")) - .size(16.0) - .color(Colors::inactive_text())); - ui.add_space(7.0); - - // Draw Tor transport content. - self.tor_ui(ui, wallet, cb); - } - - /// Draw [`Modal`] content for this ui container. - fn modal_content_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - cb: &dyn PlatformCallbacks) { - match Modal::opened() { - None => {} - Some(id) => { - match id { - SEND_TOR_MODAL => { - if let Some(content) = self.send_modal_content.as_mut() { - Modal::ui(ui.ctx(), cb, |ui, modal, cb| { - content.ui(ui, wallet, modal, cb); - }); - } - } - TOR_SETTINGS_MODAL => { - if let Some(content) = self.settings_modal_content.as_mut() { - Modal::ui(ui.ctx(), cb, |ui, modal, cb| { - content.ui(ui, wallet, modal, cb); - }); - } - } - QR_ADDRESS_MODAL => { - Modal::ui(ui.ctx(), cb, |ui, _, cb| { - self.qr_address_modal_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); + } 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); } } +} - /// Draw Tor transport content. - fn tor_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - let data = wallet.get_data().unwrap(); - - // Draw header content. - self.tor_header_ui(ui, wallet); - - // Draw receive info content. - if wallet.slatepack_address().is_some() { - self.tor_receive_ui(ui, wallet, &data, cb); +impl Default for WalletTransportContent { + fn default() -> Self { + Self { + qr_address_content: None, + settings_content: None, } + } +} - // Draw send content. - let service_id = &wallet.identifier(); - if data.info.amount_currently_spendable > 0 && wallet.foreign_api_port().is_some() && - !Tor::is_service_starting(service_id) { - self.tor_send_ui(ui); +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() + } + + /// Navigate back on navigation stack. + pub fn back(&mut self) { + if self.settings_content.is_some() { + 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(); + // Setup layout size. let mut rect = ui.available_rect_before_wrap(); rect.set_height(78.0); // Draw round background. - let bg_rect = rect.clone(); - let item_rounding = View::item_rounding(0, 2, false); - ui.painter().rect(bg_rect, - item_rounding, - Colors::fill_lite(), + 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::Middle); + StrokeKind::Outside); ui.vertical(|ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - // Draw button to setup Tor transport. - let button_rounding = View::item_rounding(0, 2, true); - View::item_button(ui, button_rounding, GEAR_SIX, None, || { - self.settings_modal_content = Some(TransportSettingsModal::default()); - // Show Tor settings modal. - Modal::new(TOR_SETTINGS_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("transport.tor_settings")) - .closeable(false) - .show(); + // Show button to show QR code address. + View::item_button(ui, View::item_rounding(1, 2, true), QR_CODE, None, || { + self.qr_address_content = Some(QrCodeContent::new(addr.clone(), false) + .with_max_size(320.0)); }); // Draw button to enable/disable Tor listener for current wallet. let service_id = &wallet.identifier(); if !Tor::is_service_starting(service_id) && wallet.foreign_api_port().is_some() { if !Tor::is_service_running(service_id) { - View::item_button(ui, CornerRadius::default(), POWER, Some(Colors::green()), || { + let r = CornerRadius::default(); + View::item_button(ui, r, POWER, Some(Colors::green()), || { if let Ok(key) = wallet.secret_key() { let api_port = wallet.foreign_api_port().unwrap(); Tor::start_service(api_port, key, service_id); } }); } else { - View::item_button(ui, CornerRadius::default(), POWER, Some(Colors::red()), || { + 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); - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.add_space(1.0); - ui.label(RichText::new(t!("transport.tor_network")) - .size(18.0) - .color(Colors::title(false))); - }); - // Setup Tor status text. let is_running = Tor::is_service_running(service_id); - let is_starting = Tor::is_service_starting(service_id); let has_error = Tor::is_service_failed(service_id); - let (icon, text) = if wallet.foreign_api_port().is_none() { - (DOTS_THREE_CIRCLE, t!("wallets.loading")) - } else if is_starting { + let address_color = if is_running { + Colors::green() + } else if has_error { + Colors::red() + } else { + Colors::inactive_text() + }; + let is_starting = Tor::is_service_starting(service_id); + // 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 { - (CHECK_CIRCLE, t!("transport.connected")) + (PLUGS_CONNECTED, t!("transport.connected")) + } else if let Some(_) = TorConfig::get_proxy() { + (PLUGS_CONNECTED, t!("app_settings.proxy")) } else { - (X_CIRCLE, t!("transport.disconnected")) + (PLUGS, t!("transport.disconnected")) }; let status_text = format!("{} {}", icon, text); - ui.label(RichText::new(status_text).size(15.0).color(Colors::text(false))); + // Show connection status text. + View::ellipsize_text(ui, status_text, 15.0, Colors::text(false)); ui.add_space(1.0); - // Setup bridges status text. - let bridge = TorConfig::get_bridge(); - let bridges_text = match &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)) + 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())); }); }); }); }); } - - /// Draw Tor receive content. - fn tor_receive_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - data: &WalletData, - cb: &dyn PlatformCallbacks) { - let addr = wallet.slatepack_address().unwrap(); - let service_id = &wallet.identifier(); - let can_send = data.info.amount_currently_spendable > 0; - - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(52.0); - - // Draw round background. - let bg_rect = rect.clone(); - let item_rounding = if can_send { - View::item_rounding(1, 3, false) - } else { - View::item_rounding(1, 2, false) - }; - ui.painter().rect(bg_rect, - item_rounding, - Colors::fill_lite(), - View::item_stroke(), - StrokeKind::Middle); - - ui.vertical(|ui| { - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - // Draw button to setup Tor transport. - let button_rounding = if can_send { - View::item_rounding(1, 3, true) - } else { - View::item_rounding(1, 2, true) - }; - View::item_button(ui, button_rounding, QR_CODE, None, || { - // Show QR code image address modal. - self.qr_address_content = Some(QrCodeContent::new(addr.clone(), false)); - Modal::new(QR_ADDRESS_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_mining.address")) - .show(); - }); - - // Show button to enable/disable Tor listener for current wallet. - View::item_button(ui, CornerRadius::default(), COPY, None, || { - cb.copy_string_to_buffer(addr.clone()); - }); - - 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 Slatepack address. - let address_color = if Tor::is_service_starting(service_id) || - wallet.foreign_api_port().is_none() { - Colors::inactive_text() - } else if Tor::is_service_running(service_id) { - Colors::green() - } else { - Colors::red() - }; - View::ellipsize_text(ui, addr, 15.0, address_color); - - let address_label = format!("{} {}", - GLOBE_SIMPLE, - t!("network_mining.address")); - ui.label(RichText::new(address_label).size(15.0).color(Colors::gray())); - }); - }); - }); - }); - } - - /// Draw QR code image address [`Modal`] content. - fn qr_address_modal_ui(&mut self, - ui: &mut egui::Ui, - cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - - // Draw QR code content. - if let Some(content) = self.qr_address_content.as_mut() { - content.ui(ui, cb); - } else { - Modal::close(); - return; - } - - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.qr_address_content = None; - Modal::close(); - }); - }); - ui.add_space(6.0); - } - - /// Draw Tor send content. - fn tor_send_ui(&mut self, ui: &mut egui::Ui) { - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(55.0); - - // Draw round background. - let bg_rect = rect.clone(); - let item_rounding = View::item_rounding(1, 2, false); - ui.painter().rect(bg_rect, - item_rounding, - Colors::fill(), - View::item_stroke(), - StrokeKind::Middle); - - ui.vertical(|ui| { - ui.allocate_ui_with_layout(rect.size(), Layout::top_down(Align::Center), |ui| { - ui.add_space(7.0); - // Draw button to open sending modal. - let send_text = format!("{} {}", EXPORT, t!("wallets.send")); - View::button(ui, send_text, Colors::white_or_black(false), || { - self.show_send_tor_modal(None); - }); - }); - }); - } - - /// Show [`Modal`] to send over Tor. - fn show_send_tor_modal(&mut self, address: Option) { - self.send_modal_content = Some(TransportSendModal::new(address)); - // Show modal. - Modal::new(SEND_TOR_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.send")) - .show(); - } } \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/transport/mod.rs b/src/gui/views/wallets/wallet/transport/mod.rs index 845225a9..e0f555cc 100644 --- a/src/gui/views/wallets/wallet/transport/mod.rs +++ b/src/gui/views/wallets/wallet/transport/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2024 The Grim Developers +// Copyright 2025 The Grim Developers // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,5 +15,4 @@ mod content; pub use content::*; -mod send; mod settings; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/transport/settings.rs b/src/gui/views/wallets/wallet/transport/settings.rs index 6534b93a..51553f46 100644 --- a/src/gui/views/wallets/wallet/transport/settings.rs +++ b/src/gui/views/wallets/wallet/transport/settings.rs @@ -1,4 +1,4 @@ -// Copyright 2024 The Grim Developers +// Copyright 2025 The Grim Developers // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,226 +13,66 @@ // limitations under the License. use egui::os::OperatingSystem; -use egui::{Id, RichText}; +use egui::RichText; use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{CameraContent, Modal, TextEdit, View}; -use crate::tor::{Tor, TorBridge, TorConfig}; +use crate::gui::views::settings::TorSettingsContent; +use crate::gui::views::types::ContentContainer; +use crate::gui::views::View; +use crate::tor::Tor; use crate::wallet::Wallet; -/// Transport settings [`Modal`] content. -pub struct TransportSettingsModal { - /// Flag to check if Tor settings were changed. +/// Wallet transport settings content. +pub struct WalletTransportSettingsContent { + /// Flag to check if settings were changed to restart Tor service. settings_changed: bool, - /// Tor bridge binary path edit text. - bridge_bin_path_edit: String, - /// Tor bridge connection line edit text. - bridge_conn_line_edit: String, - /// Address QR code scanner [`Modal`] content. - bridge_qr_scan_content: Option, + /// Tor transport content settings. + tor_settings_content: TorSettingsContent, } -impl Default for TransportSettingsModal { +impl Default for WalletTransportSettingsContent { 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, - bridge_bin_path_edit: bin_path, - bridge_conn_line_edit: conn_line, - bridge_qr_scan_content: None, + tor_settings_content: TorSettingsContent::default(), } } } -impl TransportSettingsModal { +impl WalletTransportSettingsContent { + /// Draw transport settings 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.bridge_qr_scan_content.as_mut() { - let on_stop = || { - cb.stop_camera(); - modal.enable_closing(); - }; - - if let Some(result) = scanner.qr_scan_result() { - self.bridge_conn_line_edit = result.text(); - on_stop(); - self.bridge_qr_scan_content = None; - } else { - scanner.ui(ui, cb); - ui.add_space(12.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.bridge_qr_scan_content = None; - Modal::close(); - }); - }); - cols[1].vertical_centered_justified(|ui| { - View::button(ui, t!("back"), Colors::white_or_black(false), || { - on_stop(); - self.bridge_qr_scan_content = None; - }); - }); - }); - ui.add_space(6.0); - } - return; - } - - // Do not show bridges setup on Android. - let os = OperatingSystem::from_target_os(); - let show_bridges = os != OperatingSystem::Android; - if show_bridges { - 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"), || { - // Save value. - let value = if bridge.is_some() { - None - } else { - let default_bridge = TorConfig::get_obfs4(); - 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 bridges selection and path. - if bridge.is_some() { - let current_bridge = bridge.unwrap(); - let mut bridge = current_bridge.clone(); - - ui.add_space(6.0); - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - // Draw Obfs4 bridge selector. - let obfs4 = TorConfig::get_obfs4(); - let name = obfs4.protocol_name().to_uppercase(); - View::radio_value(ui, &mut bridge, obfs4, name); - }); - columns[1].vertical_centered(|ui| { - // Draw 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(12.0); - - // Check if bridge type was changed to save. - if current_bridge != bridge { - self.settings_changed = true; - TorConfig::save_bridge(Some(bridge.clone())); - self.bridge_bin_path_edit = bridge.binary_path(); - self.bridge_conn_line_edit = bridge.connection_line(); - } - - // Draw binary path text edit. - let bin_edit_id = Id::from(modal.id) - .with(wallet.get_config().id) - .with("_bin_edit"); - let mut bin_edit = TextEdit::new(bin_edit_id) - .no_soft_keyboard() - .paste() - .focus(false); - let bin_edit_before = self.bridge_bin_path_edit.clone(); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("transport.bin_file")) - .size(17.0) - .color(Colors::inactive_text())); - ui.add_space(6.0); - bin_edit.ui(ui, &mut self.bridge_bin_path_edit, cb); - ui.add_space(6.0); - }); - - // Draw connection line text edit. - let conn_edit_before = self.bridge_conn_line_edit.clone(); - let conn_edit_id = Id::from(modal.id) - .with(wallet.get_config().id) - .with("_conn_edit"); - let mut conn_edit = TextEdit::new(conn_edit_id) - .no_soft_keyboard() - .paste() - .focus(false) - .scan_qr(); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("transport.conn_line")) - .size(17.0) - .color(Colors::inactive_text())); - ui.add_space(6.0); - conn_edit.ui(ui, &mut self.bridge_conn_line_edit, cb); - // Check if scan button was pressed. - if conn_edit.scan_pressed { - modal.disable_closing(); - self.bridge_qr_scan_content = Some(CameraContent::default()); - } - }); - - // Check if bin path or connection line text was changed to save bridge. - if conn_edit_before != self.bridge_conn_line_edit || - bin_edit_before != self.bridge_bin_path_edit { - let bin_path = self.bridge_bin_path_edit.trim().to_string(); - let conn_line = self.bridge_conn_line_edit.trim().to_string(); - let b = match bridge { - TorBridge::Snowflake(_, _) => { - TorBridge::Snowflake(bin_path, conn_line) - }, - TorBridge::Obfs4(_, _) => { - TorBridge::Obfs4(bin_path, conn_line) - } - }; - TorConfig::save_bridge(Some(b)); - self.settings_changed = true; - } - - ui.add_space(2.0); - } - - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); - } - + cb: &dyn PlatformCallbacks, + on_close: impl FnOnce()) { + ui.add_space(8.0); ui.vertical_centered(|ui| { + // Do not show bridges settings on Android. + let os = OperatingSystem::from_target_os(); + if os != OperatingSystem::Android { + // Show Tor settings. + self.tor_settings_content.ui(ui, cb); + if !self.tor_settings_content.settings_changed { + self.settings_changed = true; + } + 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); + self.settings_changed = true; }); }); - ui.add_space(6.0); + ui.add_space(8.0); ui.vertical_centered_justified(|ui| { View::button(ui, t!("close"), Colors::white_or_black(false), || { if self.settings_changed { @@ -248,7 +88,7 @@ impl TransportSettingsModal { Tor::rebuild_client(); } } - Modal::close(); + 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 a9c83845..09b61e28 100644 --- a/src/gui/views/wallets/wallet/txs/content.rs +++ b/src/gui/views/wallets/wallet/txs/content.rs @@ -12,30 +12,30 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::ops::Range; -use std::time::{SystemTime, UNIX_EPOCH}; -use egui::{Align, Id, Layout, Rect, RichText, CornerRadius, ScrollArea, StrokeKind}; use egui::epaint::RectShape; use egui::scroll_area::ScrollBarVisibility; +use egui::{Align, CornerRadius, Id, Layout, Rect, RichText, ScrollArea, StrokeKind}; 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::Colors; -use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, DOTS_THREE_CIRCLE, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE}; +use crate::gui::icons::{ARCHIVE_BOX, ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, CALENDAR_CHECK, CHECK, DOTS_THREE_CIRCLE, FILE_ARROW_DOWN, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, PullToRefresh, Content, View}; use crate::gui::views::types::{LinePosition, ModalPosition}; use crate::gui::views::wallets::types::WalletTab; -use crate::gui::views::wallets::wallet::types::{GRIN, WalletTabType}; -use crate::gui::views::wallets::wallet::WalletTransactionModal; +use crate::gui::views::wallets::wallet::types::{WalletTabType, GRIN}; +use crate::gui::views::wallets::wallet::WalletTransactionContent; +use crate::gui::views::{Content, Modal, PullToRefresh, View}; +use crate::gui::Colors; use crate::wallet::types::{WalletData, WalletTransaction}; use crate::wallet::Wallet; /// Wallet transactions tab content. pub struct WalletTransactions { /// Transaction information [`Modal`] content. - tx_info_content: Option, + tx_info_content: Option, /// Transaction identifier to use at confirmation [`Modal`]. confirm_cancel_tx_id: Option, @@ -44,16 +44,6 @@ pub struct WalletTransactions { manual_sync: Option } -impl Default for WalletTransactions { - fn default() -> Self { - Self { - tx_info_content: None, - confirm_cancel_tx_id: None, - manual_sync: None, - } - } -} - impl WalletTab for WalletTransactions { fn get_type(&self) -> WalletTabType { WalletTabType::Txs @@ -74,6 +64,19 @@ impl WalletTransactions { /// 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, + manual_sync: None, + }; + if let Some(tx) = &tx { + content.show_tx_info_modal(tx, false); + } + content + } + /// Draw transactions content. fn txs_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { let data = wallet.get_data().unwrap(); @@ -84,15 +87,14 @@ impl WalletTransactions { return; } let txs = data.txs.as_ref().unwrap(); - let mut awaiting_amount = false; 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" => CHAT_CIRCLE_TEXT, - "transport" => BRIDGE, + "message" => FILE_ARROW_DOWN, + "transport" => ARCHIVE_BOX, "settings" => GEAR_FINE ); ui.label(RichText::new(empty_text) @@ -102,7 +104,7 @@ impl WalletTransactions { return; } // Draw awaiting amount info if exists. - awaiting_amount = self.awaiting_info_ui(ui, &data); + self.awaiting_info_ui(ui, &data); }); ui.add_space(4.0); @@ -120,7 +122,7 @@ impl WalletTransactions { .show_rows(ui, Self::TX_ITEM_HEIGHT, txs.len(), |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, awaiting_amount, row_range, wallet, txs); + self.tx_list_ui(ui, row_range, wallet, txs); }); }) }); @@ -137,25 +139,19 @@ impl WalletTransactions { /// Draw transaction list content. fn tx_list_ui(&mut self, ui: &mut egui::Ui, - awaiting: bool, row_range: Range, wallet: &Wallet, txs: &Vec) { for index in row_range { - let mut rect = if awaiting { - let mut rect = ui.available_rect_before_wrap(); - rect.min += egui::emath::vec2(6.0, 0.0); - rect.max -= egui::emath::vec2(6.0, 0.0); - rect - } else { - ui.available_rect_before_wrap() - }; + let mut rect = ui.available_rect_before_wrap(); + rect.min += egui::emath::vec2(6.0, 0.0); + rect.max -= egui::emath::vec2(6.0, 0.0); rect.set_height(Self::TX_ITEM_HEIGHT); // Draw tx item background. let mut r = View::item_rounding(index, txs.len(), false); let p = ui.painter(); - p.rect(rect, r, Colors::fill_lite(), View::item_stroke(), StrokeKind::Middle); + p.rect(rect, r, Colors::fill(), View::item_stroke(), StrokeKind::Middle); let tx = txs.get(index).unwrap(); let data = wallet.get_data().unwrap(); @@ -195,14 +191,15 @@ impl WalletTransactions { } } - /// Draw information about locked, finalizing or confirming balance, return `true` if exists. - fn awaiting_info_ui(&mut self, ui: &mut egui::Ui, data: &WalletData) -> bool { + /// 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 false; + return; } + ui.add_space(-1.0); let rect = ui.available_rect_before_wrap(); // Draw background. let mut bg = RectShape::new(rect, CornerRadius { @@ -210,7 +207,7 @@ impl WalletTransactions { ne: 0.0 as u8, sw: 8.0 as u8, se: 8.0 as u8, - }, Colors::TRANSPARENT, View::item_stroke(), StrokeKind::Middle); + }, Colors::fill(), View::item_stroke(), StrokeKind::Middle); let bg_idx = ui.painter().add(bg.clone()); let resp = ui.allocate_ui(rect.size(), |ui| { ui.vertical_centered_justified(|ui| { @@ -233,7 +230,6 @@ impl WalletTransactions { // Setup background size. bg.rect = resp.rect; ui.painter().set(bg_idx, bg); - true } /// Draw [`Modal`] content for this ui container. @@ -454,7 +450,7 @@ impl WalletTransactions { /// Show transaction information [`Modal`]. fn show_tx_info_modal(&mut self, tx: &WalletTransaction, finalize: bool) { - let modal = WalletTransactionModal::new(Some(tx.data.id), finalize); + let modal = WalletTransactionContent::new(tx, finalize); self.tx_info_content = Some(modal); Modal::new(TX_INFO_MODAL) .position(ModalPosition::CenterTop) @@ -529,5 +525,5 @@ fn awaiting_item_ui(ui: &mut egui::Ui, amount: u64, label: String) { ui.label(RichText::new(label) .color(Colors::gray()) .size(15.0)); - ui.add_space(4.0); + ui.add_space(8.0); } \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/txs/tx.rs b/src/gui/views/wallets/wallet/txs/tx.rs index 9bece142..cffec607 100644 --- a/src/gui/views/wallets/wallet/txs/tx.rs +++ b/src/gui/views/wallets/wallet/txs/tx.rs @@ -24,14 +24,14 @@ use grin_wallet_libwallet::{Error, Slate, SlateState, TxLogEntryType}; use crate::gui::Colors; use crate::gui::icons::{BROOM, CHECK, CLIPBOARD_TEXT, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{CameraContent, FilePickButton, Modal, QrCodeContent, View}; +use crate::gui::views::{CameraContent, FilePickContent, FilePickContentType, Modal, QrCodeContent, View}; use crate::gui::views::wallets::wallet::txs::WalletTransactions; use crate::gui::views::wallets::wallet::types::SLATEPACK_MESSAGE_HINT; use crate::wallet::types::WalletTransaction; use crate::wallet::Wallet; /// Transaction information [`Modal`] content. -pub struct WalletTransactionModal { +pub struct WalletTransactionContent { /// Transaction identifier. tx_id: u32, @@ -56,14 +56,14 @@ pub struct WalletTransactionModal { scan_qr_content: Option, /// Button to parse picked file content. - file_pick_button: FilePickButton, + file_pick_button: FilePickContent, } -impl WalletTransactionModal { +impl WalletTransactionContent { /// Create new content instance with [`Wallet`] from provided [`WalletTransaction`]. - pub fn new(tx_id: Option, show_finalization: bool) -> Self { + pub fn new(tx: &WalletTransaction, show_finalization: bool) -> Self { Self { - tx_id: tx_id.unwrap_or(0), + tx_id: tx.data.id, response_edit: None, finalize_edit: "".to_string(), finalize_error: false, @@ -72,7 +72,7 @@ impl WalletTransactionModal { final_result: Arc::new(RwLock::new(None)), qr_code_content: None, scan_qr_content: None, - file_pick_button: FilePickButton::default(), + file_pick_button: FilePickContent::new(FilePickContentType::Button), } } diff --git a/src/gui/views/wallets/wallet/types.rs b/src/gui/views/wallets/wallet/types.rs index 81948d4f..fd0d1eb2 100644 --- a/src/gui/views/wallets/wallet/types.rs +++ b/src/gui/views/wallets/wallet/types.rs @@ -57,8 +57,6 @@ pub trait WalletTab { #[derive(PartialEq)] pub enum WalletTabType { Txs, - Messages, - Transport, Settings } @@ -67,8 +65,6 @@ impl WalletTabType { pub fn name(&self) -> String { match *self { WalletTabType::Txs => t!("wallets.txs"), - WalletTabType::Messages => t!("wallets.messages"), - WalletTabType::Transport => t!("wallets.transport"), WalletTabType::Settings => t!("wallets.settings") } } diff --git a/src/lib.rs b/src/lib.rs index 7f7e1088..b9c0a78d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -161,7 +161,7 @@ pub fn setup_visuals(ctx: &Context) { } else { Colors::yellow() }; - visuals.widgets.inactive.fg_stroke.color = Colors::item_button(); + visuals.widgets.inactive.fg_stroke.color = Colors::item_button_text(); // Setup visuals. ctx.set_visuals(visuals); } diff --git a/src/wallet/list.rs b/src/wallet/list.rs index 295b39ef..44872e54 100644 --- a/src/wallet/list.rs +++ b/src/wallet/list.rs @@ -24,12 +24,19 @@ pub struct WalletList { pub main_list: Vec, /// List of wallets for [`ChainTypes::Testnet`]. pub test_list: Vec, + + /// Selected wallet id. + selected: Option, } impl Default for WalletList { fn default() -> Self { let (main_list, test_list) = Self::init(); - Self { main_list, test_list } + Self { + main_list, + test_list, + selected: None + } } } @@ -96,4 +103,23 @@ impl WalletList { } } } + + /// 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