From 6e50b2b38abe8176b9efebf686b8bb87d0c94655 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Mon, 23 Mar 2026 01:21:09 +0300 Subject: [PATCH] ui: make list items clickable, ability to delete tx --- locales/de.yml | 1 + locales/en.yml | 1 + locales/fr.yml | 1 + locales/ru.yml | 1 + locales/tr.yml | 1 + locales/zh-CN.yml | 1 + src/gui/views/modal.rs | 8 +- src/gui/views/network/connections.rs | 258 +++++---- src/gui/views/network/setup/node.rs | 88 +-- src/gui/views/network/setup/p2p.rs | 2 +- src/gui/views/settings/interface.rs | 148 ++--- src/gui/views/settings/network.rs | 117 ++-- src/gui/views/settings/tor.rs | 323 +++++------ src/gui/views/views.rs | 4 +- src/gui/views/wallets/content.rs | 176 +++--- src/gui/views/wallets/modals/settings.rs | 72 +-- src/gui/views/wallets/wallet/content.rs | 4 +- src/gui/views/wallets/wallet/proof.rs | 4 +- .../views/wallets/wallet/settings/common.rs | 192 ++++--- .../wallets/wallet/settings/connection.rs | 98 +--- src/gui/views/wallets/wallet/txs/content.rs | 520 +++++++++++------- src/gui/views/wallets/wallet/txs/tx.rs | 55 +- src/gui/views/wallets/wallet/types.rs | 2 +- src/tor/tor.rs | 4 +- src/wallet/config.rs | 4 +- src/wallet/types.rs | 86 ++- src/wallet/wallet.rs | 392 +++++++------ wallet | 2 +- 28 files changed, 1359 insertions(+), 1206 deletions(-) diff --git a/locales/de.yml b/locales/de.yml index 20bfc18c..00376fa8 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -142,6 +142,7 @@ wallets: payment_proof_desc: 'Geben Sie den erhaltenen Zahlungsnachweis ein, um die Transaktion zu verifizieren:' payment_proof_valid: 'Der eingegebene Zahlungsnachweis ist gültig:' payment_proof_error: 'Der eingetragene Zahlungsnachweis ist nicht gültig:' + tx_delete_confirmation: Bist du sicher, dass du die Transaktion aus dem Verlauf löschen möchtest? transport: desc: 'Transport verwenden, um Nachrichten synchron zu empfangen oder zu senden:' tor_network: Tor Netzwek diff --git a/locales/en.yml b/locales/en.yml index 6e2e5806..075e70b5 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -142,6 +142,7 @@ wallets: payment_proof_desc: 'Enter received payment proof to verify transaction:' payment_proof_valid: 'Entered payment proof is valid:' payment_proof_error: 'Entered payment proof is not valid:' + tx_delete_confirmation: Are you sure you want to delete the transaction from history? transport: desc: 'Use transport to receive or send messages synchronously:' tor_network: Tor network diff --git a/locales/fr.yml b/locales/fr.yml index fe1e8bd8..cd4789fc 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -142,6 +142,7 @@ wallets: payment_proof_desc: 'Saisissez la preuve de paiement reçue pour vérifier la transaction:' payment_proof_valid: 'La preuve de paiement saisie est valide:' payment_proof_error: "La preuve de paiement saisie n'est pas valide:" + tx_delete_confirmation: Êtes-vous sûr de vouloir supprimer la transaction de l'historique? transport: desc: 'Utilisez le transport pour recevoir ou envoyer des messages de manière synchronisée:' tor_network: Réseau Tor diff --git a/locales/ru.yml b/locales/ru.yml index 4e3d0898..32aff369 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -142,6 +142,7 @@ wallets: payment_proof_desc: 'Введите полученное подтверждение оплаты для проверки транзакции:' payment_proof_valid: 'Введённое подтверждение оплаты действительно:' payment_proof_error: 'Введённое подтверждение оплаты недействительно:' + tx_delete_confirmation: Вы уверены, что хотите удалить транзакцию из истории? transport: desc: 'Используйте транспорт для синхронных получения или отправки сообщений:' tor_network: Сеть Tor diff --git a/locales/tr.yml b/locales/tr.yml index 1294f14b..1ae3d017 100644 --- a/locales/tr.yml +++ b/locales/tr.yml @@ -142,6 +142,7 @@ wallets: payment_proof_desc: 'Islemi doğrulamak için alinan ödeme kanitini girin:' payment_proof_valid: 'Girilen ödeme kaniti geçerlidir:' payment_proof_error: 'Girilen ödeme kaniti geçerli değildir:' + tx_delete_confirmation: Islemi geçmişten silmek istediğinizden emin misiniz? transport: desc: 'Adresten senkronize GONDER veya AL:' tor_network: Tor network diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 5a7e0184..afb2c7e8 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -142,6 +142,7 @@ wallets: payment_proof_desc: '輸入已收款證明以驗證交易:' payment_proof_valid: '輸入的付款證明有效:' payment_proof_error: '輸入的付款證明無效:' + tx_delete_confirmation: 你確定要從歷史紀錄中刪除這筆交易嗎? transport: desc: '使用传输同步接收或发送消息:' tor_network: Tor 网络 diff --git a/src/gui/views/modal.rs b/src/gui/views/modal.rs index 478471a4..422f0bc9 100755 --- a/src/gui/views/modal.rs +++ b/src/gui/views/modal.rs @@ -44,7 +44,7 @@ pub struct Modal { /// Flag to check first content render. first_draw: Arc, /// Background color. - fill: Color32, + fill: Option, } impl Modal { @@ -63,7 +63,7 @@ impl Modal { closeable: Arc::new(AtomicBool::new(true)), title: None, first_draw: Arc::new(AtomicBool::new(true)), - fill: Colors::fill(), + fill: None, } } @@ -301,7 +301,7 @@ impl Modal { /// Set custom background color. pub fn set_background_color(&self, color: Color32) { let mut w_state = MODAL_STATE.write(); - w_state.modal.as_mut().unwrap().fill = color; + w_state.modal.as_mut().unwrap().fill = Some(color); } /// Draw provided content. @@ -321,7 +321,7 @@ impl Modal { sw: 8.0 as u8, se: 8.0 as u8, } - }, self.fill, Stroke::NONE, StrokeKind::Outside); + }, self.fill.unwrap_or(Colors::fill_lite()), Stroke::NONE, StrokeKind::Outside); let bg_idx = ui.painter().add(bg_shape.clone()); rect.min += egui::emath::vec2(6.0, 0.0); diff --git a/src/gui/views/network/connections.rs b/src/gui/views/network/connections.rs index 57aa8bbf..7d59c7be 100644 --- a/src/gui/views/network/connections.rs +++ b/src/gui/views/network/connections.rs @@ -12,18 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{Align, Layout, RichText, CornerRadius, StrokeKind}; +use eframe::epaint::RectShape; +use egui::{Align, Color32, CornerRadius, CursorIcon, Layout, RichText, Sense, StrokeKind, UiBuilder}; -use crate::AppConfig; -use crate::gui::Colors; -use crate::gui::icons::{CARET_RIGHT, CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PENCIL, PLUS_CIRCLE, POWER, TRASH, WARNING_CIRCLE, X_CIRCLE}; +use crate::gui::icons::{CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PLUS_CIRCLE, POWER, TRASH, WARNING_CIRCLE, X_CIRCLE}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, View}; use crate::gui::views::network::modals::ExternalConnectionModal; use crate::gui::views::network::NodeSetup; use crate::gui::views::types::{ContentContainer, ModalPosition}; +use crate::gui::views::{Modal, View}; +use crate::gui::Colors; use crate::node::{Node, NodeConfig}; use crate::wallet::{ConnectionsConfig, ExternalConnection}; +use crate::AppConfig; /// Network connections content. pub struct ConnectionsContent { @@ -81,12 +82,9 @@ impl ContentContainer for ConnectionsContent { } // Show integrated node info content. - Self::integrated_node_item_ui(ui, |ui| { - // Draw button to show integrated node info. - View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || { - AppConfig::toggle_show_connections_network_panel(); - }); - }); + Self::integrated_node_item_ui(ui, Colors::fill_lite(), (true, || { + AppConfig::toggle_show_connections_network_panel(); + }), |_| false); // Show external connections. ui.add_space(8.0); @@ -102,19 +100,19 @@ impl ContentContainer for ConnectionsContent { ui.add_space(4.0); let ext_conn_list = ConnectionsConfig::ext_conn_list(); - let ext_conn_size = ext_conn_list.len(); - if ext_conn_size != 0 { + let len = ext_conn_list.len(); + if len != 0 { ui.add_space(8.0); - for (index, conn) in ext_conn_list.iter().enumerate() { + for (i, c) in ext_conn_list.iter().enumerate() { ui.horizontal_wrapped(|ui| { - // Draw connection list item. - Self::ext_conn_item_ui(ui, conn, index, ext_conn_size, |ui| { - let button_rounding = View::item_rounding(index, ext_conn_size, true); + // Draw external connection list item. + let bg = Colors::fill_lite(); + Self::ext_conn_item_ui(ui, bg, c, i, len, (true, || { + self.show_add_ext_conn_modal(Some(c.clone())); + }), |ui| { + let button_rounding = View::item_rounding(i, len, true); View::item_button(ui, button_rounding, TRASH, None, || { - ConnectionsConfig::remove_ext_conn(conn.id); - }); - View::item_button(ui, CornerRadius::default(), PENCIL, None, || { - self.show_add_ext_conn_modal(Some(conn.clone())); + ConnectionsConfig::remove_ext_conn(c.id); }); }); }); @@ -125,118 +123,156 @@ impl ContentContainer for ConnectionsContent { impl ConnectionsContent { /// Draw integrated node connection item content. - pub fn integrated_node_item_ui(ui: &mut egui::Ui, custom_button: impl FnOnce(&mut egui::Ui)) { + pub fn integrated_node_item_ui(ui: &mut egui::Ui, + bg: Color32, + on_click: (bool, impl FnOnce()), + custom_button: impl FnOnce(&mut egui::Ui) -> bool) { // Draw round background. let mut rect = ui.available_rect_before_wrap(); rect.set_height(78.0); - let rounding = View::item_rounding(0, 1, false); - ui.painter().rect(rect, rounding, Colors::fill(), View::item_stroke(), StrokeKind::Outside); + let r = View::item_rounding(0, 1, false); + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - // Draw custom button. - custom_button(ui); + let res = ui.scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), |ui| { + // Draw custom button. + let extra_button = custom_button(ui); - // Draw buttons to start/stop node. - if Node::get_error().is_none() { - if !Node::is_running() { - View::item_button(ui, CornerRadius::default(), POWER, Some(Colors::green()), || { - Node::start(); - }); - } else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() { - View::item_button(ui, CornerRadius::default(), POWER, Some(Colors::red()), || { - Node::stop(false); - }); - } - } - - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - ui.add_space(3.0); - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.add_space(1.0); - ui.label(RichText::new(t!("network.node")) - .size(18.0) - .color(Colors::title(false))); - }); - - // Setup node status text. - let has_error = Node::get_error().is_some(); - let status_icon = if has_error { - WARNING_CIRCLE - } else if !Node::is_running() { - X_CIRCLE - } else if Node::not_syncing() { - CHECK_CIRCLE + // Draw buttons to start/stop node. + if Node::get_error().is_none() { + let rounding = if extra_button { + CornerRadius::default() } else { - DOTS_THREE_CIRCLE + View::item_rounding(0, 1, true) }; - let status_text = format!("{} {}", status_icon, if has_error { - t!("error").into() - } else { - Node::get_sync_status_text() - }); - View::ellipsize_text(ui, status_text, 15.0, Colors::text(false)); - ui.add_space(1.0); + if !Node::is_running() { + View::item_button(ui, rounding, POWER, Some(Colors::green()), || { + Node::start(); + }); + } else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() { + View::item_button(ui, rounding, POWER, Some(Colors::red()), || { + Node::stop(false); + }); + } + } - // Setup node API address text. - let api_address = NodeConfig::get_api_address(); - let address_text = format!("{} http://{}", COMPUTER_TOWER, api_address); - ui.label(RichText::new(address_text).size(15.0).color(Colors::gray())); - }) - }); - }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(3.0); + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.add_space(1.0); + ui.label(RichText::new(t!("network.node")) + .size(18.0) + .color(Colors::title(false))); + }); + + // Setup node status text. + let has_error = Node::get_error().is_some(); + let status_icon = if has_error { + WARNING_CIRCLE + } else if !Node::is_running() { + X_CIRCLE + } else if Node::not_syncing() { + CHECK_CIRCLE + } else { + DOTS_THREE_CIRCLE + }; + let status_text = format!("{} {}", status_icon, if has_error { + t!("error").into() + } else { + Node::get_sync_status_text() + }); + View::ellipsize_text(ui, status_text, 15.0, Colors::text(false)); + ui.add_space(1.0); + + // Setup node API address text. + let api_address = NodeConfig::get_api_address(); + let address_text = format!("{} http://{}", COMPUTER_TOWER, api_address); + ui.label(RichText::new(address_text).size(15.0).color(Colors::gray())); + }) + }); + }).response; + let (clickable, on_click) = on_click; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if clickable && res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked && clickable { + on_click(); + } } /// Draw external connection item content. pub fn ext_conn_item_ui(ui: &mut egui::Ui, + bg: Color32, conn: &ExternalConnection, index: usize, len: usize, - buttons_ui: impl FnOnce(&mut egui::Ui)) { - // Setup layout size. + on_click: (bool, impl FnOnce()), + custom_button: impl FnOnce(&mut egui::Ui)) { + // Draw round background. let mut rect = ui.available_rect_before_wrap(); rect.set_height(52.0); + let r = View::item_rounding(index, len, false); + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - // Draw round background. - let bg_rect = rect.clone(); - let item_rounding = View::item_rounding(index, len, false); - ui.painter().rect(bg_rect, - item_rounding, - Colors::fill(), - View::item_stroke(), - StrokeKind::Outside); + let res = ui.scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), |ui| { + // Draw custom button. + custom_button(ui); - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - // Draw provided buttons. - buttons_ui(ui); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + // Draw connections URL. + ui.add_space(4.0); + let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url); + View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false)); + ui.add_space(1.0); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - // Draw connections URL. - ui.add_space(4.0); - let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url); - View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false)); - ui.add_space(1.0); - - // Setup connection status text. - let status_text = if let Some(available) = conn.available { - if available { - format!("{} {}", CHECK_CIRCLE, t!("network.available")) + // Setup connection status text. + let status_text = if let Some(available) = conn.available { + if available { + format!("{} {}", CHECK_CIRCLE, t!("network.available")) + } else { + format!("{} {}", X_CIRCLE, t!("network.not_available")) + } } else { - format!("{} {}", X_CIRCLE, t!("network.not_available")) - } - } else { - format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check")) - }; - ui.label(RichText::new(status_text).size(15.0).color(Colors::gray())); - ui.add_space(3.0); + format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check")) + }; + ui.label(RichText::new(status_text).size(15.0).color(Colors::gray())); + ui.add_space(3.0); + }); }); - }); - }); + } + ).response; + let (clickable, on_click) = on_click; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if clickable && res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked && clickable { + on_click(); + } } /// Show [`Modal`] to add external connection. diff --git a/src/gui/views/network/setup/node.rs b/src/gui/views/network/setup/node.rs index dcb8fc72..d0942a69 100644 --- a/src/gui/views/network/setup/node.rs +++ b/src/gui/views/network/setup/node.rs @@ -13,8 +13,8 @@ // limitations under the License. use eframe::emath::Align; -use eframe::epaint::StrokeKind; -use egui::{Id, Layout, RichText}; +use eframe::epaint::{RectShape, StrokeKind}; +use egui::{CursorIcon, Id, Layout, RichText, Sense, UiBuilder}; use grin_core::global::ChainTypes; use crate::gui::icons::{CLOCK_CLOCKWISE, COMPUTER_TOWER, FOLDERS, PENCIL, PLUG, POWER, SHIELD, SHIELD_SLASH}; @@ -173,7 +173,7 @@ impl ContentContainer for NodeSetup { // Show data location selection for Desktop when it already started or turned off. if !Node::is_restarting() && !Node::is_stopping() && !Node::is_starting() && View::is_desktop() { - self.pick_data_dir_ui(ui, cb); + self.data_dir_ui(ui, cb); ui.add_space(6.0); } @@ -236,46 +236,56 @@ impl ContentContainer for NodeSetup { impl NodeSetup { /// Draw content to change chain data directory. - fn pick_data_dir_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - // Setup layout size. + fn data_dir_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + // Draw round background. let mut rect = ui.available_rect_before_wrap(); rect.set_height(56.0); + let r = View::item_rounding(0, 1, false); + let bg = Colors::fill_lite(); + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - // Draw round background. - let bg_rect = rect.clone(); - let item_rounding = View::item_rounding(0, 1, false); - ui.painter().rect(bg_rect, - item_rounding, - Colors::fill(), - View::item_stroke(), - StrokeKind::Outside); - - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - self.pick_data_dir.ui(ui, cb, |path| { - Node::change_data_dir(path); - }); - View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || { - self.data_path_edit = NodeConfig::get_chain_data_path(); - // Show chain data path edit modal. - Modal::new(DATA_PATH_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network.node")) - .show(); - }); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(12.0); - ui.vertical(|ui| { - ui.add_space(4.0); - let path = NodeConfig::get_chain_data_path(); - View::ellipsize_text(ui, path, 18.0, Colors::title(false)); - ui.add_space(1.0); - let desc = format!("{} {}", FOLDERS, t!("files_location")); - ui.label(RichText::new(desc).size(15.0).color(Colors::gray())); - ui.add_space(8.0); + let res = ui.scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), |ui| { + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + self.pick_data_dir.ui(ui, cb, |path| { + Node::change_data_dir(path); + }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + let path = NodeConfig::get_chain_data_path(); + View::ellipsize_text(ui, path, 18.0, Colors::title(false)); + ui.add_space(1.0); + let desc = format!("{} {}", FOLDERS, t!("files_location")); + ui.label(RichText::new(desc).size(15.0).color(Colors::gray())); + ui.add_space(8.0); + }); + }); }); - }); - }); + } + ).response; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked { + self.data_path_edit = NodeConfig::get_chain_data_path(); + // Show chain data path edit modal. + Modal::new(DATA_PATH_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network.node")) + .show(); + } } /// Draw data path input [`Modal`] content. diff --git a/src/gui/views/network/setup/p2p.rs b/src/gui/views/network/setup/p2p.rs index d3ca2850..21dd8ba4 100644 --- a/src/gui/views/network/setup/p2p.rs +++ b/src/gui/views/network/setup/p2p.rs @@ -786,7 +786,7 @@ fn peer_item_ui(ui: &mut egui::Ui, let item_rounding = View::item_rounding(index, len, false); ui.painter().rect(rect, item_rounding, - Colors::white_or_black(false), + Colors::fill(), View::item_stroke(), StrokeKind::Outside); diff --git a/src/gui/views/settings/interface.rs b/src/gui/views/settings/interface.rs index 06e77a5e..a497228a 100644 --- a/src/gui/views/settings/interface.rs +++ b/src/gui/views/settings/interface.rs @@ -12,12 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::scroll_area::ScrollBarVisibility; -use egui::{Align, Layout, RichText, ScrollArea, StrokeKind}; +use eframe::epaint::RectShape; +use egui::{Align, CursorIcon, Layout, RichText, Sense, StrokeKind, UiBuilder}; -use crate::gui::icons::{CHECK, PENCIL, TRANSLATE}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::types::{ContentContainer, ModalPosition}; +use crate::gui::views::types::ContentContainer; use crate::gui::views::{Modal, View}; use crate::gui::Colors; use crate::AppConfig; @@ -28,22 +27,10 @@ pub struct InterfaceSettingsContent { locale: String, } -/// Identifier for language selection [`Modal`]. -const LANGUAGE_SELECTION_MODAL: &'static str = "language_selection_modal"; - impl ContentContainer for InterfaceSettingsContent { - fn modal_ids(&self) -> Vec<&'static str> { - vec![ - LANGUAGE_SELECTION_MODAL - ] - } + fn modal_ids(&self) -> Vec<&'static str> { vec![] } - fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, _: &dyn PlatformCallbacks) { - match modal.id { - LANGUAGE_SELECTION_MODAL => self.language_selection_ui(ui), - _ => {} - } - } + fn modal_ui(&mut self, _: &mut egui::Ui, _: &Modal, _: &dyn PlatformCallbacks) {} fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { ui.add_space(5.0); @@ -72,7 +59,10 @@ impl ContentContainer for InterfaceSettingsContent { } // Draw language selection. - self.language_item_ui(self.locale.clone().as_str(), ui, true, 0, 1); + let locales = rust_i18n::available_locales!(); + for (index, locale) in locales.iter().enumerate() { + self.language_item_ui(locale, ui, index, locales.len()); + } ui.add_space(4.0); } } @@ -91,96 +81,29 @@ impl Default for InterfaceSettingsContent { } impl InterfaceSettingsContent { - /// Draw language selection content. - fn language_selection_ui(&mut self, ui: &mut egui::Ui) { - ui.add_space(4.0); - ScrollArea::vertical() - .max_height(373.0) - .id_salt("select_language_scroll") - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .auto_shrink([true; 2]) - .show(ui, |ui| { - ui.add_space(2.0); - ui.vertical_centered(|ui| { - let locales = rust_i18n::available_locales!(); - for (index, locale) in locales.iter().enumerate() { - self.language_item_ui(locale, ui, false, index, locales.len()); - } - }); - }); - - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); - - // Show button to close modal. - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - Modal::close(); - }); - }); - ui.add_space(6.0); - } - /// Draw language selection item content. - fn language_item_ui(&mut self, locale: &str, ui: &mut egui::Ui, edit: bool, index: usize, len: usize) { - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - if edit { - rect.set_height(56.0); - } else { - rect.set_height(50.0); - } - + fn language_item_ui(&mut self, locale: &str, ui: &mut egui::Ui, index: usize, len: usize) { + let is_current = self.locale == locale; // Draw round background. - let bg_rect = rect.clone(); - let item_rounding = View::item_rounding(index, len, false); - ui.painter().rect(bg_rect, - item_rounding, - Colors::fill(), - View::item_stroke(), - StrokeKind::Outside); + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(50.0); + let r = View::item_rounding(index, len, false); + let bg = if is_current { + Colors::fill() + } else { + Colors::fill_lite() + }; + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - if edit { - View::item_button(ui, View::item_rounding(index, len, true), PENCIL, None, || { - // Show language selection modal. - Modal::new(LANGUAGE_SELECTION_MODAL) - .position(ModalPosition::Center) - .title(t!("language")) - .show(); - }); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(12.0); - ui.vertical(|ui| { - ui.add_space(4.0); - View::ellipsize_text(ui, - t!("lang_name", locale = locale), - 18.0, - Colors::title(false)); - ui.add_space(1.0); - let value = format!("{} {}", - TRANSLATE, - t!("language")); - ui.label(RichText::new(value).size(15.0).color(Colors::gray())); - ui.add_space(3.0); - }); - }); - } else { - // Draw button to select language. - let is_current = self.locale == locale; - if !is_current { - View::item_button(ui, View::item_rounding(index, len, true), CHECK, None, || { - rust_i18n::set_locale(locale); - AppConfig::save_locale(locale); - self.locale = locale.to_string(); - Modal::close(); - }); - } else { + let res = ui.scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), |ui| { + if is_current { View::selected_item_check(ui); } - let layout_size = ui.available_size(); ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { ui.add_space(12.0); @@ -195,10 +118,23 @@ impl InterfaceSettingsContent { ui.label(RichText::new(t!("lang_name", locale = locale)) .size(17.0) .color(color)); - ui.add_space(3.0); + ui.add_space(14.0); }); }); } - }); + ).response; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if res.hovered() && !is_current { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked && !is_current { + rust_i18n::set_locale(locale); + AppConfig::save_locale(locale); + self.locale = locale.to_string(); + } } } \ No newline at end of file diff --git a/src/gui/views/settings/network.rs b/src/gui/views/settings/network.rs index 7874c130..465aa10b 100644 --- a/src/gui/views/settings/network.rs +++ b/src/gui/views/settings/network.rs @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{Align, Id, Layout, RichText, StrokeKind}; +use eframe::epaint::RectShape; +use egui::{Align, CursorIcon, Id, Layout, RichText, Sense, StrokeKind, UiBuilder}; use url::Url; -use crate::gui::icons::{CLOUD_CHECK, CLOUD_SLASH, PENCIL}; +use crate::gui::icons::{CLOUD_CHECK, CLOUD_SLASH}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::{ContentContainer, ModalPosition}; use crate::gui::views::{Modal, TextEdit, View}; @@ -202,63 +203,71 @@ impl NetworkSettingsContent { /// Draw proxy item content. fn proxy_item_ui(&mut self, ui: &mut egui::Ui) { - // Setup layout size. + // Draw round background. let mut rect = ui.available_rect_before_wrap(); rect.set_height(56.0); + let r = View::item_rounding(0, 1, false); + let bg = Colors::fill_lite(); + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - // Draw round background. - let bg_rect = rect.clone(); - let item_rounding = View::item_rounding(0, 1, false); - ui.painter().rect(bg_rect, - item_rounding, - Colors::fill(), - View::item_stroke(), - StrokeKind::Outside); + let res = ui.scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), |ui| { + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + let use_socks = AppConfig::use_socks_proxy(); + let proxy_url = if use_socks { + AppConfig::socks_proxy_url() + } else { + AppConfig::http_proxy_url() + }; + let (url, color, icon, text) = if let Some(url) = proxy_url { + (url, Colors::title(false), CLOUD_CHECK, t!("network_settings.enabled")) + } else { + ( + t!("enter_url").into(), + Colors::inactive_text(), + CLOUD_SLASH, + t!("network_settings.disabled") + ) + }; + View::ellipsize_text(ui, url, 18.0, color); + ui.add_space(1.0); - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - View::item_button(ui, View::item_rounding(0, 1, true), PENCIL, None, || { - let url = if AppConfig::use_socks_proxy() { - AppConfig::socks_proxy_url().unwrap_or("".to_string()) - } else { - AppConfig::http_proxy_url().unwrap_or("".to_string()) - }; - self.proxy_url_edit = url; - // Show proxy URL edit modal. - Modal::new(PROXY_URL_EDIT_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("app_settings.proxy")) - .show(); - }); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(12.0); - ui.vertical(|ui| { - ui.add_space(4.0); - let use_socks = AppConfig::use_socks_proxy(); - let proxy_url = if use_socks { - AppConfig::socks_proxy_url() - } else { - AppConfig::http_proxy_url() - }; - let (url, color, icon, text) = if let Some(url) = proxy_url { - (url, Colors::title(false), CLOUD_CHECK, t!("network_settings.enabled")) - } else { - ( - t!("enter_url").into(), - Colors::inactive_text(), - CLOUD_SLASH, - t!("network_settings.disabled") - ) - }; - View::ellipsize_text(ui, url, 18.0, color); - ui.add_space(1.0); - - let value = format!("{} {}", icon, text); - ui.label(RichText::new(value).size(15.0).color(Colors::gray())); - ui.add_space(3.0); + let value = format!("{} {}", icon, text); + ui.label(RichText::new(value).size(15.0).color(Colors::gray())); + ui.add_space(3.0); + }); }); - }); - }); + } + ).response; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked { + let url = if AppConfig::use_socks_proxy() { + AppConfig::socks_proxy_url().unwrap_or("".to_string()) + } else { + AppConfig::http_proxy_url().unwrap_or("".to_string()) + }; + self.proxy_url_edit = url; + // Show proxy URL edit modal. + Modal::new(PROXY_URL_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("app_settings.proxy")) + .show(); + } } /// Draw proxy type selection. diff --git a/src/gui/views/settings/tor.rs b/src/gui/views/settings/tor.rs index 066d4549..c36d13d7 100644 --- a/src/gui/views/settings/tor.rs +++ b/src/gui/views/settings/tor.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::os::OperatingSystem; +use eframe::epaint::RectShape; use egui::scroll_area::ScrollBarVisibility; -use egui::{Align, Id, Layout, RichText, ScrollArea, StrokeKind}; +use egui::{Align, CursorIcon, Id, Layout, RichText, ScrollArea, Sense, StrokeKind, UiBuilder}; use std::fs; use url::Url; @@ -194,9 +194,8 @@ impl ContentContainer for TorSettingsContent { if bridge.is_some() { ui.add_space(6.0); - // Show bridge selection for non-Android. - let is_android = OperatingSystem::from_target_os() == OperatingSystem::Android; - if !is_android { + // Show bridge selection for desktop. + if View::is_desktop() { let current_bridge = bridge.unwrap(); let mut bridge = current_bridge.clone(); @@ -234,13 +233,12 @@ impl ContentContainer for TorSettingsContent { } if let Some(br) = TorConfig::get_bridge().as_ref() { - // Show bridge binary setup for non-Android. - if !is_android { - self.bridge_bin_ui(ui, br, cb); - ui.add_space(10.0); - } // Show bridge connection line setup. - self.bridge_conn_line_ui(ui, br, cb); + self.conn_line_ui(ui, br, cb); + // Show bridge binary setup for desktop. + if View::is_desktop() { + self.bridge_bin_ui(ui, br, cb); + } } ui.add_space(8.0); @@ -361,153 +359,61 @@ impl TorSettingsContent { }); } - /// Draw bridge binary setup content. - fn bridge_bin_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) { - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(56.0); - - // Draw round background. - let bg_rect = rect.clone(); - let item_rounding = View::item_rounding(0, 1, false); - ui.painter().rect(bg_rect, - item_rounding, - Colors::fill(), - View::item_stroke(), - StrokeKind::Outside); - - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - self.bridge_bin_pick_file.ui(ui, cb, |path| { - if bridge.binary_path() != path { - TorBridge::save_bridge_bin_path(bridge, path); - self.settings_changed = true; - } - }); - View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || { - self.bridge_bin_path_edit = bridge.binary_path(); - // Show binary path edit modal. - let title = bridge.protocol_name(); - Modal::new(BRIDGE_BIN_EDIT_MODAL) - .position(ModalPosition::CenterTop) - .title(title) - .show(); - }); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(12.0); - ui.vertical(|ui| { - ui.add_space(4.0); - View::ellipsize_text(ui, bridge.binary_path(), 18.0, Colors::title(false)); - ui.add_space(1.0); - let value = format!("{} {}", - TERMINAL, - t!("transport.bin_file").replace(":", "")); - ui.label(RichText::new(value).size(15.0).color(Colors::gray())); - ui.add_space(3.0); - }); - }); - }); - } - - /// Draw bridge binary input [`Modal`] content. - fn bridge_bin_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - let on_save = |c: &mut TorSettingsContent| { - let bridge = TorConfig::get_bridge().unwrap(); - let exists = fs::exists(&c.bridge_bin_path_edit).unwrap_or_default(); - if !exists { - return; - } - if bridge.binary_path() != c.bridge_bin_path_edit { - TorBridge::save_bridge_bin_path(&bridge, c.bridge_bin_path_edit.clone()); - c.settings_changed = true; - } - Modal::close(); - }; - - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("transport.bin_file")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); - - // Draw bridge text edit. - let mut edit = TextEdit::new(Id::from(BRIDGE_BIN_EDIT_MODAL)).paste(); - edit.ui(ui, &mut self.bridge_bin_path_edit, cb); - if edit.enter_pressed { - on_save(self); - } - ui.add_space(12.0); - - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - Modal::close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { - on_save(self); - }); - }); - }); - ui.add_space(6.0); - }); - }); - } - /// Draw bridge connection line setup content. - fn bridge_conn_line_ui(&mut self, - ui: &mut egui::Ui, - bridge: &TorBridge, - cb: &dyn PlatformCallbacks) { - // Setup layout size. + fn conn_line_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) { + // Draw round background. let mut rect = ui.available_rect_before_wrap(); rect.set_height(56.0); + let r = if View::is_desktop() { + View::item_rounding(0, 2, false) + } else { + View::item_rounding(0, 1, false) + }; + let bg = Colors::fill_lite(); + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - // Draw round background. - let bg_rect = rect.clone(); - let item_rounding = View::item_rounding(0, 1, false); - ui.painter().rect(bg_rect, - item_rounding, - Colors::fill(), - View::item_stroke(), - StrokeKind::Outside); - - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - View::item_button(ui, View::item_rounding(0, 1, true), SCAN, None, || { - self.show_qr_scan_bridge_modal(cb); - }); - View::item_button(ui, View::item_rounding(1, 3 , true), PENCIL, None, || { - self.bridge_conn_line_edit = bridge.connection_line(); - // Show connection line edit modal. - let title = bridge.protocol_name(); - Modal::new(BRIDGE_CONN_LINE_EDIT_MODAL) - .position(ModalPosition::Center) - .title(title) - .show(); - }); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(12.0); - ui.vertical(|ui| { - ui.add_space(4.0); - View::ellipsize_text(ui, bridge.connection_line(), 18.0, Colors::title(false)); - ui.add_space(1.0); - let value = format!("{} {}", - NOTCHES, - t!("transport.conn_line").replace(":", "")); - ui.label(RichText::new(value).size(15.0).color(Colors::gray())); - ui.add_space(3.0); + let res = ui.scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), |ui| { + View::item_button(ui, View::item_rounding(0, 1, true), SCAN, None, || { + self.show_qr_scan_bridge_modal(cb); }); - }); - }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + let line_text = bridge.connection_line(); + View::ellipsize_text(ui, line_text, 18.0, Colors::title(false)); + ui.add_space(1.0); + let line_desc = t!("transport.conn_line").replace(":", ""); + let value = format!("{} {}", NOTCHES, line_desc); + ui.label(RichText::new(value).size(15.0).color(Colors::gray())); + ui.add_space(3.0); + }); + }); + } + ).response; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked { + self.bridge_conn_line_edit = bridge.connection_line(); + // Show connection line edit modal. + let title = bridge.protocol_name(); + Modal::new(BRIDGE_CONN_LINE_EDIT_MODAL) + .position(ModalPosition::Center) + .title(title) + .show(); + } } /// Show bridge connection line QR code scanner. @@ -557,7 +463,7 @@ impl TorSettingsContent { .id(input_id) .font(egui::TextStyle::Body) .desired_rows(5) - .interactive(true) + .interactive(false) .desired_width(f32::INFINITY) .show(ui); ui.add_space(6.0); @@ -614,4 +520,113 @@ impl TorSettingsContent { }); }); } + + /// Draw bridge binary setup content. + fn bridge_bin_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) { + // Draw round background. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(56.0); + let r = View::item_rounding(1, 2, false); + let bg = Colors::fill_lite(); + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); + + let res = ui.scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), |ui| { + self.bridge_bin_pick_file.ui(ui, cb, |path| { + if bridge.binary_path() != path { + TorBridge::save_bridge_bin_path(bridge, path); + self.settings_changed = true; + } + }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + let bin_text = bridge.binary_path(); + View::ellipsize_text(ui, bin_text, 18.0, Colors::title(false)); + ui.add_space(1.0); + let bin_desc = t!("transport.bin_file").replace(":", ""); + let value = format!("{} {}", TERMINAL, bin_desc); + ui.label(RichText::new(value).size(15.0).color(Colors::gray())); + ui.add_space(3.0); + }); + }); + } + ).response; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked { + self.bridge_bin_path_edit = bridge.binary_path(); + // Show binary path edit modal. + let title = bridge.protocol_name(); + Modal::new(BRIDGE_BIN_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(title) + .show(); + } + } + + /// Draw bridge binary input [`Modal`] content. + fn bridge_bin_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut TorSettingsContent| { + let bridge = TorConfig::get_bridge().unwrap(); + let exists = fs::exists(&c.bridge_bin_path_edit).unwrap_or_default(); + if !exists { + return; + } + if bridge.binary_path() != c.bridge_bin_path_edit { + TorBridge::save_bridge_bin_path(&bridge, c.bridge_bin_path_edit.clone()); + c.settings_changed = true; + } + Modal::close(); + }; + + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("transport.bin_file")) + .size(17.0) + .color(Colors::gray())); + ui.add_space(8.0); + + // Draw bridge text edit. + let mut edit = TextEdit::new(Id::from(BRIDGE_BIN_EDIT_MODAL)).paste(); + edit.ui(ui, &mut self.bridge_bin_path_edit, cb); + if edit.enter_pressed { + on_save(self); + } + ui.add_space(12.0); + + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + // Close modal. + Modal::close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } } \ No newline at end of file diff --git a/src/gui/views/views.rs b/src/gui/views/views.rs index bb36c21f..c8a31615 100644 --- a/src/gui/views/views.rs +++ b/src/gui/views/views.rs @@ -186,6 +186,8 @@ impl View { // Setup padding for title buttons. if !View::is_desktop() { ui.style_mut().spacing.button_padding = egui::vec2(20.0, 8.0); + } else { + ui.style_mut().spacing.button_padding = egui::vec2(16.0, 8.0); } // Disable strokes. ui.style_mut().visuals.widgets.inactive.bg_stroke = Stroke::NONE; @@ -358,7 +360,7 @@ impl View { ui.scope(|ui| { // Setup padding for item buttons. let padding = if Self::is_desktop() { - 14.0 + 15.0 } else { 18.0 }; diff --git a/src/gui/views/wallets/content.rs b/src/gui/views/wallets/content.rs index 3a9a77b2..ac4922f2 100644 --- a/src/gui/views/wallets/content.rs +++ b/src/gui/views/wallets/content.rs @@ -12,27 +12,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::time::Duration; -use egui::scroll_area::ScrollBarVisibility; -use egui::{Align, CornerRadius, Id, Layout, Margin, OpenUrl, RichText, ScrollArea, StrokeKind}; use egui::os::OperatingSystem; +use egui::scroll_area::ScrollBarVisibility; +use egui::{Align, CornerRadius, CursorIcon, Id, Layout, Margin, OpenUrl, RichText, ScrollArea, Sense, StrokeKind, UiBuilder}; use egui_async::Bind; -use crate::gui::icons::{ARROW_LEFT, BOOKMARKS, CALENDAR_CHECK, CARET_RIGHT, CLOUD_ARROW_DOWN, COMPUTER_TOWER, FOLDER_OPEN, FOLDER_PLUS, GEAR, GEAR_FINE, GLOBE, GLOBE_SIMPLE, LOCK_KEY, NOTEPAD, PLUS, SIDEBAR_SIMPLE, SUITCASE}; +use std::time::Duration; +use eframe::epaint::RectShape; + +use crate::gui::icons::{ARROW_LEFT, BOOKMARKS, CALENDAR_CHECK, CLOUD_ARROW_DOWN, COMPUTER_TOWER, FOLDER_PLUS, GEAR, GEAR_FINE, GLOBE, GLOBE_SIMPLE, LOCK_KEY, NOTEPAD, PLUS, SIDEBAR_SIMPLE, SUITCASE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::settings::SettingsContent; use crate::gui::views::types::{ContentContainer, LinePosition, ModalPosition, TitleContentType, TitleType}; use crate::gui::views::wallets::creation::WalletCreationContent; -use crate::gui::views::wallets::modals::{AddWalletModal, OpenWalletModal, WalletSettingsModal, WalletListModal, ChangelogContent}; +use crate::gui::views::wallets::modals::{AddWalletModal, ChangelogContent, OpenWalletModal, WalletListModal, WalletSettingsModal}; use crate::gui::views::wallets::wallet::types::{wallet_status_text, WalletContentContainer}; +use crate::gui::views::wallets::wallet::RecoverySettings; use crate::gui::views::wallets::WalletContent; use crate::gui::views::{Content, Modal, TitlePanel, View}; use crate::gui::Colors; +use crate::http::{retrieve_release, ReleaseInfo}; +use crate::settings::AppUpdate; use crate::wallet::types::{ConnectionMethod, WalletTask}; use crate::wallet::{Wallet, WalletList}; use crate::AppConfig; -use crate::gui::views::wallets::wallet::RecoverySettings; -use crate::http::{retrieve_release, ReleaseInfo}; -use crate::settings::AppUpdate; /// Wallets content. pub struct WalletsContent { @@ -444,7 +446,12 @@ impl WalletsContent { || (dual_panel && !show_list)) && !creating_wallet && !showing_settings { let title = self.wallet_content.title().into(); let subtitle = self.wallets.selected().unwrap().get_config().name; - TitleType::Single(TitleContentType::WithSubTitle(title, subtitle, false)) + let wallet_title_content = if self.wallet_content.settings_content.is_some() { + TitleContentType::Title(title) + } else { + TitleContentType::WithSubTitle(title, subtitle, false) + }; + TitleType::Single(wallet_title_content) } else { let title_text = if showing_settings { t!("settings") @@ -458,7 +465,11 @@ impl WalletsContent { if dual_title { let title = self.wallet_content.title().into(); let subtitle = self.wallets.selected().unwrap().get_config().name; - let wallet_title_content = TitleContentType::WithSubTitle(title, subtitle, false); + let wallet_title_content = if self.wallet_content.settings_content.is_some() { + TitleContentType::Title(title) + } else { + TitleContentType::WithSubTitle(title, subtitle, false) + }; TitleType::Dual(TitleContentType::Title(title_text), wallet_title_content) } else { TitleType::Single(TitleContentType::Title(title_text)) @@ -579,8 +590,12 @@ impl WalletsContent { } else { false }; + // Unselect wallet when opening or settings modal was closed. + if current && !w.is_open() && Modal::opened().is_none() { + self.wallets.select(None); + } self.wallet_item_ui(ui, w, current, cb); - ui.add_space(5.0); + ui.add_space(6.0); } }); }); @@ -593,88 +608,99 @@ impl WalletsContent { current: bool, cb: &dyn PlatformCallbacks) { let config = wallet.get_config(); + let can_open = !wallet.is_open() && !wallet.files_moving(); // Draw round background. let mut rect = ui.available_rect_before_wrap(); rect.set_height(78.0); - let rounding = View::item_rounding(0, 1, false); + let r = View::item_rounding(0, 1, false); let bg = if current { Colors::fill_deep() } else { Colors::fill() }; - ui.painter().rect(rect, rounding, bg, View::item_stroke(), StrokeKind::Outside); + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - if !wallet.is_open() && !wallet.files_moving() { - // Show button to open closed wallet. - View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || { - self.show_opening_modal(wallet, None, cb); - }); - if !wallet.is_repairing() { - View::item_button(ui, CornerRadius::default(), GEAR_FINE, None, || { - self.select_wallet(wallet, None, cb); - let conn = wallet.get_current_connection(); - self.wallet_settings_content = WalletSettingsModal::new(conn); - // Show connection selection modal. - Modal::new(WALLET_SETTINGS_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.settings")) - .show(); - }); - } - } else { - if !current { - // Show button to select opened wallet. - View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || { - self.select_wallet(wallet, None, cb); - }); - } - // Show button to close opened wallet. - if !wallet.is_closing() { - View::item_button(ui, if !current { - CornerRadius::default() - } else { - View::item_rounding(0, 1, true) - }, LOCK_KEY, None, || { + let res = ui.scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), |ui| { + if can_open { + if !wallet.is_repairing() { + View::item_button(ui, View::item_rounding(0, 1, true), GEAR_FINE, None, || { + self.select_wallet(wallet, None, cb); + let conn = wallet.get_current_connection(); + self.wallet_settings_content = WalletSettingsModal::new(conn); + // Show connection selection modal. + Modal::new(WALLET_SETTINGS_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.settings")) + .show(); + }); + } + } else if !wallet.is_closing() { + // Show button to close opened wallet. + View::item_button(ui, View::item_rounding(0, 1, true), LOCK_KEY, None, || { wallet.close(); }); } - } - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - ui.add_space(3.0); - // Show wallet name text. - let name_color = if current { - Colors::white_or_black(true) - } else { - Colors::title(false) - }; - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(3.0); + // Show wallet name text. + let name_color = if current { + Colors::white_or_black(true) + } else { + Colors::title(false) + }; + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { ui.add_space(1.0); View::ellipsize_text(ui, config.name, 18.0, name_color); + }); + + // Show wallet status text. + let status_text = wallet_status_text(wallet); + View::ellipsize_text(ui, status_text, 15.0, Colors::text(false)); + ui.add_space(1.0); + + // Show wallet connection text. + let connection = wallet.get_current_connection(); + let conn_text = match connection { + ConnectionMethod::Integrated => { + format!("{} {}", COMPUTER_TOWER, t!("network.node")) + } + ConnectionMethod::External(_, url) => { + format!("{} {}", GLOBE_SIMPLE, url) + } + }; + View::ellipsize_text(ui, conn_text, 15.0, Colors::gray()); + ui.add_space(3.0); }); - - // Show wallet status text. - View::ellipsize_text(ui, wallet_status_text(wallet), 15.0, Colors::text(false)); - ui.add_space(1.0); - - // Show wallet connection text. - let connection = wallet.get_current_connection(); - let conn_text = match connection { - ConnectionMethod::Integrated => { - format!("{} {}", COMPUTER_TOWER, t!("network.node")) - } - ConnectionMethod::External(_, url) => format!("{} {}", GLOBE_SIMPLE, url) - }; - View::ellipsize_text(ui, conn_text, 15.0, Colors::gray()); - ui.add_space(3.0); }); - }); - }); + } + ).response; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if res.hovered() && (can_open || !current) { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill_deep(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked { + if can_open { + // Show modal to open the wallet. + self.show_opening_modal(wallet, None, cb); + } else if !current { + // Select opened wallet. + self.select_wallet(wallet, None, cb); + } + } } /// Draw update information content. diff --git a/src/gui/views/wallets/modals/settings.rs b/src/gui/views/wallets/modals/settings.rs index f02a03ec..892f1146 100644 --- a/src/gui/views/wallets/modals/settings.rs +++ b/src/gui/views/wallets/modals/settings.rs @@ -15,7 +15,7 @@ use egui::scroll_area::ScrollBarVisibility; use egui::{RichText, ScrollArea}; -use crate::gui::icons::{CHECK, PLUS_CIRCLE, TRASH}; +use crate::gui::icons::{PLUS_CIRCLE, TRASH}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::network::modals::ExternalConnectionModal; use crate::gui::views::network::ConnectionsContent; @@ -79,18 +79,20 @@ impl WalletSettingsModal { ui.add_space(2.0); // Show integrated node selection. - ConnectionsContent::integrated_node_item_ui(ui, |ui| { - match self.conn { - ConnectionMethod::Integrated => { - View::selected_item_check(ui); - } - _ => { - View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || { - on_select(ConnectionMethod::Integrated); - Modal::close(); - }); - } + let cur_integrated = self.conn == ConnectionMethod::Integrated; + let bg = if cur_integrated { + Colors::fill() + } else { + Colors::fill_lite() + }; + ConnectionsContent::integrated_node_item_ui(ui, bg, (!cur_integrated, || { + on_select(ConnectionMethod::Integrated); + Modal::close(); + }), |ui| { + if cur_integrated { + View::selected_item_check(ui); } + cur_integrated }); ui.add_space(8.0); @@ -109,29 +111,31 @@ impl WalletSettingsModal { if !ext_conn_list.is_empty() { ui.add_space(8.0); - for (index, conn) in ext_conn_list.iter().enumerate() { - ui.horizontal_wrapped(|ui| { - let len = ext_conn_list.len(); - ConnectionsContent::ext_conn_item_ui(ui, conn, index, len, |ui| { - let current_ext_conn = match self.conn { - ConnectionMethod::Integrated => false, - ConnectionMethod::External(id, _) => id == conn.id - }; - if current_ext_conn { - View::selected_item_check(ui); - } else { - let button_rounding = View::item_rounding(index, len, true); - View::item_button(ui, button_rounding, CHECK, None, || { - on_select( - ConnectionMethod::External(conn.id, conn.url.clone()) - ); - Modal::close(); - }); - } - }); - }); - } } + for (i, c) in ext_conn_list.iter().enumerate() { + ui.horizontal_wrapped(|ui| { + let len = ext_conn_list.len(); + let is_current = match self.conn { + ConnectionMethod::External(id, _) => id == c.id, + _ => false + }; + let bg = if is_current { + Colors::fill() + } else { + Colors::fill_lite() + }; + ConnectionsContent::ext_conn_item_ui(ui, bg, c, i, len, (!is_current, || { + on_select( + ConnectionMethod::External(c.id, c.url.clone()) + ); + Modal::close(); + }), |ui| { + if is_current { + View::selected_item_check(ui); + } + }); + }); + } ui.add_space(4.0); }); diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 1b90cc07..dd06832d 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -256,7 +256,7 @@ impl WalletContentContainer for WalletContent { .show_inside(ui, |ui| { let rect = ui.available_rect_before_wrap(); let show_settings = self.settings_content.is_some(); - let show_txs = self.txs_content.is_some(); + let show_txs = self.txs_content.is_some() && !top_panel_expanded; let show_sync = (!show_settings || block_nav) && sync_ui(ui, &wallet); if !show_sync { @@ -457,7 +457,7 @@ impl WalletContent { None => { // Show transaction modal on wallet task result. if let Some(id) = id { - let tx = wallet.get_data().unwrap().tx_by_slate_id(id); + let tx = wallet.get_data().unwrap().tx_by_id(id); if tx.is_some() { self.txs_content = Some(WalletTransactionsContent::new(tx)); self.settings_content = None; diff --git a/src/gui/views/wallets/wallet/proof.rs b/src/gui/views/wallets/wallet/proof.rs index ab6785f7..9ad5a53b 100644 --- a/src/gui/views/wallets/wallet/proof.rs +++ b/src/gui/views/wallets/wallet/proof.rs @@ -7,7 +7,7 @@ use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, COPY, FILE_TEXT, SEAL_CHECK}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{FilePickContent, FilePickContentType, Modal, View}; use crate::gui::Colors; -use crate::wallet::types::{WalletTask, WalletTransaction}; +use crate::wallet::types::{WalletTask, WalletTx}; use crate::wallet::Wallet; pub struct PaymentProofContent { @@ -154,7 +154,7 @@ impl PaymentProofContent { /// Draw transaction payment proof content to share. pub fn share_ui(&mut self, ui: &mut egui::Ui, - tx: &WalletTransaction, + tx: &WalletTx, cb: &dyn PlatformCallbacks) { ui.add_space(6.0); ui.vertical_centered(|ui| { diff --git a/src/gui/views/wallets/wallet/settings/common.rs b/src/gui/views/wallets/wallet/settings/common.rs index dfc87c59..fcf44c1e 100644 --- a/src/gui/views/wallets/wallet/settings/common.rs +++ b/src/gui/views/wallets/wallet/settings/common.rs @@ -13,10 +13,10 @@ // limitations under the License. use eframe::emath::Align; -use eframe::epaint::StrokeKind; -use egui::{Id, Layout, RichText}; +use eframe::epaint::{RectShape, StrokeKind}; +use egui::{CursorIcon, Id, Layout, RichText, Sense, UiBuilder}; -use crate::gui::icons::{CLOCK_COUNTDOWN, FOLDERS, FOLDER_USER, PASSWORD, PENCIL}; +use crate::gui::icons::{CLOCK_COUNTDOWN, FOLDERS, FOLDER_USER, PASSWORD}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::ModalPosition; use crate::gui::views::wallets::wallet::types::WalletContentContainer; @@ -79,7 +79,11 @@ impl WalletContentContainer for CommonSettings { } fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - ui.add_space(8.0); + if View::is_desktop() { + ui.add_space(1.0); + } else { + ui.add_space(8.0); + } ui.vertical_centered(|ui| { let config = wallet.get_config(); // Show wallet name. @@ -106,7 +110,7 @@ impl WalletContentContainer for CommonSettings { ui.add_space(8.0); - // Setup ability to post wallet transactions with Dandelion. + // Ability to post wallet transactions with Dandelion. View::checkbox(ui, wallet.can_use_dandelion(), t!("wallets.use_dandelion"), || { wallet.update_use_dandelion(!wallet.can_use_dandelion()); }); @@ -137,60 +141,68 @@ impl Default for CommonSettings { impl CommonSettings { /// Draw content to change wallet name and password. fn name_ui(&mut self, ui: &mut egui::Ui, name: String) { - // Setup layout size. + // Draw round background. let mut rect = ui.available_rect_before_wrap(); rect.set_height(56.0); - - // Draw round background. - let bg_rect = rect.clone(); - let item_rounding = if View::is_desktop() { + let r = if View::is_desktop() { View::item_rounding(0, 2, false) } else { View::item_rounding(0, 1, false) }; - ui.painter().rect(bg_rect, - item_rounding, - Colors::fill(), - View::item_stroke(), - StrokeKind::Outside); + let bg = Colors::fill_lite(); + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - let r = if View::is_desktop() { - View::item_rounding(0, 2, true) - } else { - View::item_rounding(0, 1, true) - }; - View::item_button(ui, r, PASSWORD, None, || { - self.old_pass_edit = "".to_string(); - self.new_pass_edit = "".to_string(); - self.wrong_pass = false; - // Show wallet password modal. - Modal::new(PASS_EDIT_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.wallet")) - .show(); - }); - View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || { - self.name_edit = name.clone(); - // Show wallet name modal. - Modal::new(NAME_EDIT_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.wallet")) - .show(); - }); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(12.0); - ui.vertical(|ui| { - ui.add_space(4.0); - View::ellipsize_text(ui, name, 18.0, Colors::title(false)); - ui.add_space(1.0); - let desc = format!("{} {}", FOLDER_USER, t!("wallets.name").replace(":", "")); - ui.label(RichText::new(desc).size(15.0).color(Colors::gray())); - ui.add_space(8.0); + let res = ui.scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), |ui| { + let r = if View::is_desktop() { + View::item_rounding(0, 2, true) + } else { + View::item_rounding(0, 1, true) + }; + View::item_button(ui, r, PASSWORD, None, || { + self.old_pass_edit = "".to_string(); + self.new_pass_edit = "".to_string(); + self.wrong_pass = false; + // Show wallet password modal. + Modal::new(PASS_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.wallet")) + .show(); }); - }); - }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + View::ellipsize_text(ui, name.clone(), 18.0, Colors::title(false)); + ui.add_space(1.0); + let desc = format!("{} {}", FOLDER_USER, t!("wallets.name").replace(":", "")); + ui.label(RichText::new(desc).size(15.0).color(Colors::gray())); + ui.add_space(8.0); + }); + }); + } + ).response; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked { + self.name_edit = name; + // Show wallet name modal. + Modal::new(NAME_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.wallet")) + .show(); + } } /// Draw wallet name [`Modal`] content. @@ -340,45 +352,53 @@ impl CommonSettings { /// Draw content to change wallet data directory. fn data_dir_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - // Setup layout size. + // Draw round background. let mut rect = ui.available_rect_before_wrap(); rect.set_height(56.0); + let r = View::item_rounding(1, 2, false); + let bg = Colors::fill_lite(); + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); - // Draw 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::Outside); - - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - self.pick_data_dir.ui(ui, cb, |path| { - wallet.change_data_path(path); - }); - View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || { - self.data_path_edit = wallet.get_config().data_path.unwrap_or_default(); - // Show chain data path edit modal. - Modal::new(DATA_PATH_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.wallet")) - .show(); - }); - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(12.0); - ui.vertical(|ui| { - ui.add_space(4.0); - let path = wallet.get_config().data_path.unwrap_or_default(); - View::ellipsize_text(ui, path, 18.0, Colors::title(false)); - ui.add_space(1.0); - let desc = format!("{} {}", FOLDERS, t!("files_location")); - ui.label(RichText::new(desc).size(15.0).color(Colors::gray())); - ui.add_space(8.0); + let res = ui.scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), |ui| { + self.pick_data_dir.ui(ui, cb, |path| { + wallet.change_data_path(path); }); - }); - }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + let path = wallet.get_config().data_path.unwrap_or_default(); + View::ellipsize_text(ui, path, 18.0, Colors::title(false)); + ui.add_space(1.0); + let desc = format!("{} {}", FOLDERS, t!("files_location")); + ui.label(RichText::new(desc).size(15.0).color(Colors::gray())); + ui.add_space(8.0); + }); + }); + } + ).response; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::fill(); + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked { + self.data_path_edit = wallet.get_config().data_path.unwrap_or_default(); + // Show chain data path edit modal. + Modal::new(DATA_PATH_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.wallet")) + .show(); + } } /// Draw data path input [`Modal`] content. diff --git a/src/gui/views/wallets/wallet/settings/connection.rs b/src/gui/views/wallets/wallet/settings/connection.rs index d7d85cb2..5af2a74c 100644 --- a/src/gui/views/wallets/wallet/settings/connection.rs +++ b/src/gui/views/wallets/wallet/settings/connection.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{Align, Layout, RichText, StrokeKind}; +use egui::RichText; -use crate::gui::icons::{CHECK, CHECK_CIRCLE, DOTS_THREE_CIRCLE, GLOBE, GLOBE_SIMPLE, PLUS_CIRCLE, X_CIRCLE}; +use crate::gui::icons::{GLOBE, PLUS_CIRCLE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::network::modals::ExternalConnectionModal; use crate::gui::views::network::ConnectionsContent; @@ -70,15 +70,19 @@ impl ContentContainer for ConnectionSettings { ui.vertical_centered(|ui| { ui.add_space(6.0); // Show integrated node selection. - ConnectionsContent::integrated_node_item_ui(ui, |ui| { - let is_current_method = self.method == ConnectionMethod::Integrated; - if !is_current_method { - View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || { - self.method = ConnectionMethod::Integrated; - }); - } else { + let cur_integrated = self.method == ConnectionMethod::Integrated; + let bg = if cur_integrated { + Colors::fill_deep() + } else { + Colors::fill_lite() + }; + ConnectionsContent::integrated_node_item_ui(ui, bg, (!cur_integrated, || { + self.method = ConnectionMethod::Integrated; + }), |ui| { + if cur_integrated { View::selected_item_check(ui); } + cur_integrated }); ui.add_space(8.0); @@ -119,8 +123,8 @@ impl ContentContainer for ConnectionSettings { } } - let ext_size = ext_conn_list.len(); - if ext_size != 0 { + let len = ext_conn_list.len(); + if len != 0 { ui.add_space(8.0); for (i, c) in ext_conn_list.iter().enumerate() { ui.horizontal_wrapped(|ui| { @@ -129,73 +133,21 @@ impl ContentContainer for ConnectionSettings { ConnectionMethod::External(id, url) => id == &c.id || url == &c.url, _ => false }; - Self::ext_conn_item_ui(ui, c, is_current, i, ext_size, || { + let bg = if is_current { + Colors::fill() + } else { + Colors::fill_lite() + }; + ConnectionsContent::ext_conn_item_ui(ui, bg, c, i, len, (!is_current, || { self.method = ConnectionMethod::External(c.id, c.url.clone()); + }), |ui| { + if is_current { + View::selected_item_check(ui); + } }); }); } } }); } -} - -impl ConnectionSettings { - /// Draw external connection item content. - fn ext_conn_item_ui(ui: &mut egui::Ui, - conn: &ExternalConnection, - is_current: bool, - index: usize, - len: usize, - mut on_select: impl FnMut()) { - // 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 = View::item_rounding(index, len, false); - ui.painter().rect(bg_rect, - item_rounding, - Colors::fill(), - View::item_stroke(), - StrokeKind::Outside); - - ui.vertical(|ui| { - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - if is_current { - View::selected_item_check(ui); - } else { - // Draw button to select connection. - let button_rounding = View::item_rounding(index, len, true); - View::item_button(ui, button_rounding, CHECK, None, || { - on_select(); - }); - } - - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - // Draw connections URL. - ui.add_space(4.0); - let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url); - View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false)); - ui.add_space(1.0); - // Setup connection status text. - let status_text = if let Some(available) = conn.available { - if available { - format!("{} {}", CHECK_CIRCLE, t!("network.available")) - } else { - format!("{} {}", X_CIRCLE, t!("network.not_available")) - } - } else { - format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check")) - }; - ui.label(RichText::new(status_text).size(15.0).color(Colors::gray())); - ui.add_space(3.0); - }); - }); - }); - }); - } } \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/txs/content.rs b/src/gui/views/wallets/wallet/txs/content.rs index ee726a18..8e1bcf25 100644 --- a/src/gui/views/wallets/wallet/txs/content.rs +++ b/src/gui/views/wallets/wallet/txs/content.rs @@ -14,21 +14,21 @@ use egui::epaint::RectShape; use egui::scroll_area::ScrollBarVisibility; -use egui::{Align, CornerRadius, Id, Layout, Rect, RichText, ScrollArea, StrokeKind}; +use egui::{Align, Color32, CornerRadius, CursorIcon, Id, Layout, Rect, RichText, ScrollArea, Sense, StrokeKind, UiBuilder}; use grin_core::consensus::COINBASE_MATURITY; use grin_core::core::amount_to_hr_string; use grin_wallet_libwallet::TxLogEntryType; use std::ops::Range; use std::time::{SystemTime, UNIX_EPOCH}; -use crate::gui::icons::{ARROWS_CLOCKWISE, ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, CALENDAR_CHECK, DOTS_THREE_CIRCLE, FILE_ARROW_DOWN, FILE_TEXT, GEAR_FINE, PROHIBIT, WARNING, X_CIRCLE}; +use crate::gui::icons::{ARROWS_CLOCKWISE, ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, CALENDAR_CHECK, DOTS_THREE_CIRCLE, FILE_ARROW_DOWN, FILE_TEXT, FILE_X, GEAR_FINE, PROHIBIT, WARNING, X_CIRCLE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::{LinePosition, ModalPosition}; use crate::gui::views::wallets::wallet::types::{WalletContentContainer, GRIN}; use crate::gui::views::wallets::wallet::WalletTransactionContent; use crate::gui::views::{Content, Modal, PullToRefresh, View}; use crate::gui::Colors; -use crate::wallet::types::{WalletData, WalletTask, WalletTransaction, WalletTransactionAction}; +use crate::wallet::types::{WalletData, WalletTask, WalletTx, WalletTxAction}; use crate::wallet::Wallet; /// Wallet transactions tab content. @@ -36,8 +36,10 @@ pub struct WalletTransactionsContent { /// Transaction information [`Modal`] content. pub tx_info_content: Option, - /// Transaction identifier to use at confirmation [`Modal`]. + /// Transaction identifier to use at confirmation [`Modal`] to cancel. confirm_cancel_tx_id: Option, + /// Transaction identifier to use at confirmation [`Modal`] to delete. + confirm_delete_tx_id: Option, /// Flag to check if sync of wallet was initiated manually at time. manual_sync: Option @@ -45,19 +47,28 @@ pub struct WalletTransactionsContent { impl WalletContentContainer for WalletTransactionsContent { fn modal_ids(&self) -> Vec<&'static str> { - vec![TX_INFO_MODAL, CANCEL_TX_CONFIRMATION_MODAL] + vec![TX_INFO_MODAL, CANCEL_TX_CONFIRMATION_MODAL, DELETE_TX_CONFIRMATION_MODAL] } fn modal_ui(&mut self, ui: &mut egui::Ui, w: &Wallet, m: &Modal, cb: &dyn PlatformCallbacks) { match m.id { TX_INFO_MODAL => { if let Some(content) = self.tx_info_content.as_mut() { - content.ui(ui, m, w, cb); + let mut on_delete_id = None; + content.ui(ui, m, w, cb, |id| { + on_delete_id = Some(id); + }); + if let Some(id) = on_delete_id { + self.show_delete_confirmation_modal(id); + } } } CANCEL_TX_CONFIRMATION_MODAL => { self.cancel_confirmation_modal(ui, w); } + DELETE_TX_CONFIRMATION_MODAL => { + self.delete_confirmation_modal(ui, w); + } _ => {} } } @@ -69,19 +80,21 @@ impl WalletContentContainer for WalletTransactionsContent { /// Identifier for transaction information [`Modal`]. const TX_INFO_MODAL: &'static str = "tx_info_modal"; - /// Identifier for transaction cancellation confirmation [`Modal`]. const CANCEL_TX_CONFIRMATION_MODAL: &'static str = "cancel_tx_conf_modal"; +/// Identifier for transaction deletion confirmation [`Modal`]. +const DELETE_TX_CONFIRMATION_MODAL: &'static str = "delete_tx_conf_modal"; impl WalletTransactionsContent { /// Height of transaction list item. - pub const TX_ITEM_HEIGHT: f32 = 75.0; + pub const TX_ITEM_HEIGHT: f32 = 73.0; /// Create new content instance with opening tx info. - pub fn new(tx: Option) -> Self { + pub fn new(tx: Option) -> Self { let mut content = Self { tx_info_content: None, confirm_cancel_tx_id: None, + confirm_delete_tx_id: None, manual_sync: None, }; if let Some(tx) = &tx { @@ -100,7 +113,10 @@ impl WalletTransactionsContent { }); return; } - let txs = data.txs.as_ref().unwrap(); + let txs = data.txs.as_ref().unwrap() + .iter() + .filter(|tx| !tx.deleting()) + .collect::>(); View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { // Show message when txs are empty. if txs.is_empty() { @@ -162,7 +178,7 @@ impl WalletTransactionsContent { ui: &mut egui::Ui, row_range: Range, wallet: &Wallet, - txs: &Vec) { + txs: Vec<&WalletTx>) { let data = wallet.get_data().unwrap(); for index in row_range { if index == txs.len() && ui.is_visible() { @@ -183,34 +199,40 @@ impl WalletTransactionsContent { } return; } - 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(), View::item_stroke(), StrokeKind::Outside); - + // Transaction item background setup. + let rect = { + let mut r = ui.available_rect_before_wrap(); + r.min += egui::emath::vec2(6.0, 0.0); + r.max -= egui::emath::vec2(6.0, 0.0); + r.set_height(Self::TX_ITEM_HEIGHT); + r + }; + let rounding = View::item_rounding(index, txs.len(), false); + let bg = Colors::fill(); let tx = txs.get(index).unwrap(); - Self::tx_item_ui(ui, tx, rect, &data, |ui| { - // Draw button to show transaction info. - if tx.data.tx_slate_id.is_some() || tx.data.payment_proof.is_some() { + let mut show_tx_info = false; + // Draw transaction list item. + Self::tx_item_ui(ui, tx, rect, bg, rounding, &data, (true, || { + show_tx_info = true; + }), |ui| { + let btn_rounding = { + let mut r = rounding.clone(); r.nw = 0.0 as u8; r.sw = 0.0 as u8; - View::item_button(ui, r, FILE_TEXT, None, || { - self.show_tx_info_modal(tx.data.id); + r + }; + // Draw button to delete transaction. + if tx.data.confirmed || tx.cancelled() { + View::item_button(ui, btn_rounding, FILE_X, Some(Colors::inactive_text()), || { + self.show_delete_confirmation_modal(tx.data.id); }); - } - - if wallet.synced_from_node() && !tx.cancelled() && !tx.cancelling() && !tx.posting() { - let resend = tx.broadcasting_timed_out(wallet); - + } else if !tx.cancelled() && !tx.cancelling() && !tx.posting() && + wallet.synced_from_node() { + let repeat = tx.broadcasting_timed_out(wallet); // Draw button to cancel transaction. - if tx.can_cancel() || resend { + if tx.can_cancel() || repeat { let (icon, color) = (PROHIBIT, Some(Colors::red())); - View::item_button(ui, CornerRadius::default(), icon, color, || { + View::item_button(ui, btn_rounding, icon, color, || { self.confirm_cancel_tx_id = Some(tx.data.id); // Show transaction cancellation confirmation modal. Modal::new(CANCEL_TX_CONFIRMATION_MODAL) @@ -219,13 +241,16 @@ impl WalletTransactionsContent { .show(); }); } - // Draw button to repeat transaction action. - if tx.can_repeat_action() || resend { - Self::tx_repeat_button_ui(ui, CornerRadius::default(), tx, wallet, resend); + if tx.can_repeat_action() || repeat { + Self::tx_repeat_button_ui(ui, CornerRadius::default(), tx, wallet, repeat); } } }); + // Show transaction info on click. + if show_tx_info { + self.show_tx_info_modal(tx.data.id); + } } } @@ -272,92 +297,135 @@ impl WalletTransactionsContent { /// Draw transaction item. pub fn tx_item_ui(ui: &mut egui::Ui, - tx: &WalletTransaction, + tx: &WalletTx, rect: Rect, + bg: Color32, + r: CornerRadius, data: &WalletData, + on_click: (bool, impl FnOnce()), buttons_ui: impl FnOnce(&mut egui::Ui)) { - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |ui| { - ui.horizontal_centered(|ui| { - // Draw buttons. - buttons_ui(ui); - }); + // Draw background. + let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside); + let bg_idx = ui.painter().add(bg_shape.clone()); + let res = ui.scope_builder( + UiBuilder::new() + .sense(Sense::click()) + .layout(Layout::right_to_left(Align::Center)) + .max_rect(rect), |ui| { + ui.horizontal_centered(|ui| { + // Draw buttons. + buttons_ui(ui); + }); - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - ui.add_space(3.0); + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(3.0); - // Setup transaction amount. - let mut amount_text = if tx.data.tx_type == TxLogEntryType::TxSent || - tx.data.tx_type == TxLogEntryType::TxSentCancelled { - "-" - } else if tx.data.tx_type == TxLogEntryType::TxReceived || - tx.data.tx_type == TxLogEntryType::TxReceivedCancelled { - "+" - } else { - "" - }.to_string(); - amount_text = format!("{}{} {}", - amount_text, - amount_to_hr_string(tx.amount, true), - GRIN); + // Setup transaction amount. + let mut amount_text = if tx.data.tx_type == TxLogEntryType::TxSent || + tx.data.tx_type == TxLogEntryType::TxSentCancelled { + "-" + } else if tx.data.tx_type == TxLogEntryType::TxReceived || + tx.data.tx_type == TxLogEntryType::TxReceivedCancelled { + "+" + } else { + "" + }.to_string(); + amount_text = format!("{}{} {}", + amount_text, + amount_to_hr_string(tx.amount, true), + GRIN); - // Setup amount color. - let amount_color = match tx.data.tx_type { - TxLogEntryType::ConfirmedCoinbase => Colors::white_or_black(true), - TxLogEntryType::TxReceived => Colors::white_or_black(true), - TxLogEntryType::TxSent => Colors::white_or_black(true), - TxLogEntryType::TxReceivedCancelled => Colors::text(false), - TxLogEntryType::TxSentCancelled => Colors::text(false), - TxLogEntryType::TxReverted => Colors::text(false) - }; - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.add_space(1.0); - View::ellipsize_text(ui, amount_text, 18.0, amount_color); - }); - ui.add_space(-2.0); + // Setup amount color. + let amount_color = match tx.data.tx_type { + TxLogEntryType::ConfirmedCoinbase => Colors::white_or_black(true), + TxLogEntryType::TxReceived => Colors::white_or_black(true), + TxLogEntryType::TxSent => Colors::white_or_black(true), + TxLogEntryType::TxReceivedCancelled => Colors::text(false), + TxLogEntryType::TxSentCancelled => Colors::text(false), + TxLogEntryType::TxReverted => Colors::text(false) + }; + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.add_space(1.0); + View::ellipsize_text(ui, amount_text, 18.0, amount_color); + }); + ui.add_space(-2.0); - // Setup transaction status text. - let height = data.info.last_confirmed_height; - let status_text = if !tx.data.confirmed { - let is_canceled = tx.data.tx_type == TxLogEntryType::TxSentCancelled - || tx.data.tx_type == TxLogEntryType::TxReceivedCancelled; - if is_canceled { - format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled")) - } else if let Some(a) = &tx.action { - let error = if tx.action_error.is_none() { - "".to_string() + // Setup transaction status text. + let height = data.info.last_confirmed_height; + let status_text = if !tx.data.confirmed { + let is_canceled = tx.data.tx_type == TxLogEntryType::TxSentCancelled + || tx.data.tx_type == TxLogEntryType::TxReceivedCancelled; + if is_canceled { + format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled")) + } else if let Some(action) = &tx.action { + let error = if tx.action_error.is_none() { + "".to_string() + } else { + format!("{}: ", t!("error")) + }; + let status = match action { + WalletTxAction::Finalizing => t!("wallets.tx_finalizing"), + WalletTxAction::Posting => t!("wallets.tx_posting"), + WalletTxAction::SendingTor => t!("transport.tor_sending"), + _ => t!("wallets.tx_cancelling") + }; + let icon = if error.is_empty() { + DOTS_THREE_CIRCLE + } else { + WARNING + }; + format!("{} {}{}", icon, error, status) } else { - format!("{}: ", t!("error")) - }; - let status = match a { - WalletTransactionAction::Cancelling => t!("wallets.tx_cancelling"), - WalletTransactionAction::Finalizing => t!("wallets.tx_finalizing"), - WalletTransactionAction::Posting => t!("wallets.tx_posting"), - WalletTransactionAction::SendingTor => t!("transport.tor_sending") - }; - let icon = if error.is_empty() { - DOTS_THREE_CIRCLE - } else { - WARNING - }; - format!("{} {}{}", icon, error, status) + match tx.data.tx_type { + TxLogEntryType::TxReceived => { + let text = match tx.finalized() { + true => t!("wallets.await_fin_amount"), + false => t!("wallets.tx_receiving") + }; + format!("{} {}", DOTS_THREE_CIRCLE, text) + }, + TxLogEntryType::TxSent => { + let text = match tx.finalized() { + true => t!("wallets.await_fin_amount"), + false => t!("wallets.tx_sending") + }; + format!("{} {}", DOTS_THREE_CIRCLE, text) + }, + TxLogEntryType::ConfirmedCoinbase => { + let tx_h = tx.height.unwrap_or(1) - 1; + if tx_h != 0 { + let left_conf = height - tx_h; + if height >= tx_h && left_conf < COINBASE_MATURITY { + let conf_info = format!("{}/{}", + left_conf, + COINBASE_MATURITY); + format!("{} {} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_confirming"), + conf_info + ) + } else { + format!("{} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_confirming")) + } + } else { + format!("{} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_confirming")) + } + }, + _ => { + format!("{} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_confirming")) + } + } + } } else { match tx.data.tx_type { - TxLogEntryType::TxReceived => { - let text = match tx.finalized() { - true => t!("wallets.await_fin_amount"), - false => t!("wallets.tx_receiving") - }; - format!("{} {}", DOTS_THREE_CIRCLE, text) - }, - TxLogEntryType::TxSent => { - let text = match tx.finalized() { - true => t!("wallets.await_fin_amount"), - false => t!("wallets.tx_sending") - }; - format!("{} {}", DOTS_THREE_CIRCLE, text) - }, TxLogEntryType::ConfirmedCoinbase => { let tx_h = tx.height.unwrap_or(1) - 1; if tx_h != 0 { @@ -374,111 +442,91 @@ impl WalletTransactionsContent { } else { format!("{} {}", DOTS_THREE_CIRCLE, - t!("wallets.tx_confirming")) + t!("wallets.tx_confirmed")) } - } else { - format!("{} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_confirming")) - } - }, - _ => { - format!("{} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_confirming")) - } - } - } - } else { - match tx.data.tx_type { - TxLogEntryType::ConfirmedCoinbase => { - let tx_h = tx.height.unwrap_or(1) - 1; - if tx_h != 0 { - let left_conf = height - tx_h; - if height >= tx_h && left_conf < COINBASE_MATURITY { - let conf_info = format!("{}/{}", - left_conf, - COINBASE_MATURITY); - format!("{} {} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_confirming"), - conf_info - ) } else { format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_confirmed")) } - } else { - format!("{} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_confirmed")) - } - }, - TxLogEntryType::TxSent | TxLogEntryType::TxReceived => { - let min_conf = data.info.minimum_confirmations; - if tx.height.is_none() || (tx.height.unwrap() != 0 && - height - tx.height.unwrap() >= min_conf - 1) { - let (i, t) = if tx.data.tx_type == TxLogEntryType::TxSent { - (ARROW_CIRCLE_UP, t!("wallets.tx_sent")) + }, + TxLogEntryType::TxSent | TxLogEntryType::TxReceived => { + let min_conf = data.info.minimum_confirmations; + if tx.height.is_none() || (tx.height.unwrap() != 0 && + height - tx.height.unwrap() >= min_conf - 1) { + let (i, t) = if tx.data.tx_type == TxLogEntryType::TxSent { + (ARROW_CIRCLE_UP, t!("wallets.tx_sent")) + } else { + (ARROW_CIRCLE_DOWN, t!("wallets.tx_received")) + }; + format!("{} {}", i, t) } else { - (ARROW_CIRCLE_DOWN, t!("wallets.tx_received")) - }; - format!("{} {}", i, t) - } else { - let tx_height = tx.height.unwrap() - 1; - let left_conf = height - tx_height; - let conf_info = if tx_height != 0 && height >= tx_height && - left_conf < min_conf { - format!("{}/{}", left_conf, min_conf) - } else { - "".to_string() - }; - format!("{} {} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_confirming"), - conf_info - ) - } + let tx_height = tx.height.unwrap() - 1; + let left_conf = height - tx_height; + let conf_info = if tx_height != 0 && height >= tx_height && + left_conf < min_conf { + format!("{}/{}", left_conf, min_conf) + } else { + "".to_string() + }; + format!("{} {} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_confirming"), + conf_info + ) + } + }, + _ => format!("{} {}", X_CIRCLE, t!("wallets.canceled")) + } + }; + + // Setup status text color. + let status_color = match tx.data.tx_type { + TxLogEntryType::ConfirmedCoinbase => Colors::text(false), + TxLogEntryType::TxReceived => if tx.data.confirmed { + Colors::green() + } else { + Colors::text(false) }, - _ => format!("{} {}", X_CIRCLE, t!("wallets.canceled")) - } - }; + TxLogEntryType::TxSent => if tx.data.confirmed { + Colors::red() + } else { + Colors::text(false) + }, + TxLogEntryType::TxReceivedCancelled => Colors::inactive_text(), + TxLogEntryType::TxSentCancelled => Colors::inactive_text(), + TxLogEntryType::TxReverted => Colors::inactive_text(), + }; + View::ellipsize_text(ui, status_text, 15.0, status_color); - // Setup status text color. - let status_color = match tx.data.tx_type { - TxLogEntryType::ConfirmedCoinbase => Colors::text(false), - TxLogEntryType::TxReceived => if tx.data.confirmed { - Colors::green() - } else { - Colors::text(false) - }, - TxLogEntryType::TxSent => if tx.data.confirmed { - Colors::red() - } else { - Colors::text(false) - }, - TxLogEntryType::TxReceivedCancelled => Colors::inactive_text(), - TxLogEntryType::TxSentCancelled => Colors::inactive_text(), - TxLogEntryType::TxReverted => Colors::inactive_text(), - }; - View::ellipsize_text(ui, status_text, 15.0, status_color); - - // Setup transaction time. - let tx_time = View::format_time(tx.data.creation_ts.timestamp()); - let tx_time_text = format!("{} {}", CALENDAR_CHECK, tx_time); - ui.label(RichText::new(tx_time_text).size(15.0).color(Colors::gray())); - ui.add_space(3.0); + // Setup transaction time. + let tx_time = View::format_time(tx.data.creation_ts.timestamp()); + let tx_time_text = format!("{} {}", CALENDAR_CHECK, tx_time); + ui.label(RichText::new(tx_time_text).size(15.0).color(Colors::gray())); + ui.add_space(4.0); + }); }); - }); - }); + } + ).response; + let (clickable, on_click) = on_click; + let clicked = res.clicked() || res.long_touched(); + // Setup background and cursor. + if clickable && res.hovered() { + res.on_hover_cursor(CursorIcon::PointingHand); + bg_shape.fill = Colors::TRANSPARENT; + } + ui.painter().set(bg_idx, bg_shape); + // Handle clicks on layout. + if clicked && clickable { + on_click(); + } } /// Draw button to repeat transaction action on error or repost. pub fn tx_repeat_button_ui(ui: &mut egui::Ui, rounding: CornerRadius, - tx: &WalletTransaction, + tx: &WalletTx, wallet: &Wallet, repost: bool) { let (icon, color) = (ARROWS_CLOCKWISE, Some(Colors::green())); @@ -487,21 +535,21 @@ impl WalletTransactionsContent { wallet.task(WalletTask::Post(tx.data.id)); } else if let Some(action) = tx.action.as_ref() { match action { - WalletTransactionAction::Finalizing => { + WalletTxAction::Finalizing => { wallet.task(WalletTask::Finalize(tx.data.id)); } - WalletTransactionAction::Posting => { + WalletTxAction::Posting => { wallet.task(WalletTask::Post(tx.data.id)); } _ => { if let Some(a) = &tx.receiver { - wallet.task(WalletTask::SendTor(tx.data.id, a.clone())); + wallet.task(WalletTask::SendTor(tx.data.clone(), a.clone())); } } } } else { if let Some(a) = &tx.receiver { - wallet.task(WalletTask::SendTor(tx.data.id, a.clone())); + wallet.task(WalletTask::SendTor(tx.data.clone(), a.clone())); } } }); @@ -522,8 +570,8 @@ impl WalletTransactionsContent { let data = wallet.get_data().unwrap(); let data_txs = data.txs.unwrap(); let txs = data_txs.into_iter() - .filter(|tx| tx.data.id == self.confirm_cancel_tx_id.unwrap()) - .collect::>(); + .filter(|tx| tx.data.id == self.confirm_cancel_tx_id.unwrap_or_default()) + .collect::>(); if txs.is_empty() { Modal::close(); return; @@ -562,7 +610,7 @@ impl WalletTransactionsContent { }); columns[1].vertical_centered_justified(|ui| { View::button(ui, "OK".to_string(), Colors::white_or_black(false), || { - wallet.task(WalletTask::Cancel(tx.data.clone())); + wallet.task(WalletTask::Cancel(tx.data.id)); self.confirm_cancel_tx_id = None; Modal::close(); }); @@ -571,6 +619,62 @@ impl WalletTransactionsContent { ui.add_space(6.0); }); } + + /// Show transaction deletion confirmation [`Modal`]. + fn show_delete_confirmation_modal(&mut self, id: u32) { + self.confirm_delete_tx_id = Some(id); + // Show transaction deletion confirmation modal. + Modal::new(DELETE_TX_CONFIRMATION_MODAL) + .position(ModalPosition::Center) + .title(t!("confirmation")) + .show(); + } + + /// Confirmation [`Modal`] to delete transaction. + fn delete_confirmation_modal(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { + let data = wallet.get_data().unwrap(); + let data_txs = data.txs.unwrap(); + let txs = data_txs.into_iter() + .filter(|tx| tx.data.id == self.confirm_delete_tx_id.unwrap_or_default()) + .collect::>(); + if txs.is_empty() { + Modal::close(); + return; + } + let tx = txs.get(0).unwrap(); + + // Show confirmation text. + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("wallets.tx_delete_confirmation")) + .size(17.0) + .color(Colors::text(false))); + ui.add_space(8.0); + }); + + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + self.confirm_delete_tx_id = None; + Modal::close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, "OK".to_string(), Colors::white_or_black(false), || { + wallet.task(WalletTask::Delete(tx.data.id)); + self.confirm_delete_tx_id = None; + Modal::close(); + }); + }); + }); + ui.add_space(6.0); + }); + } } /// Draw awaiting balance item content. diff --git a/src/gui/views/wallets/wallet/txs/tx.rs b/src/gui/views/wallets/wallet/txs/tx.rs index 476b5499..544bd562 100644 --- a/src/gui/views/wallets/wallet/txs/tx.rs +++ b/src/gui/views/wallets/wallet/txs/tx.rs @@ -19,15 +19,15 @@ use grin_util::ToHex; use grin_wallet_libwallet::TxLogEntryType; use std::fs; -use crate::AppConfig; -use crate::gui::icons::{CIRCLE_HALF, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SEAL_CHECK}; +use crate::gui::icons::{CIRCLE_HALF, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, FILE_X, HASH_STRAIGHT, PROHIBIT, QR_CODE, SEAL_CHECK}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::wallets::wallet::proof::PaymentProofContent; use crate::gui::views::wallets::wallet::txs::WalletTransactionsContent; use crate::gui::views::{Modal, QrCodeContent, View}; use crate::gui::Colors; -use crate::wallet::types::{WalletTask, WalletTransaction}; +use crate::wallet::types::{WalletTask, WalletTx}; use crate::wallet::Wallet; +use crate::AppConfig; /// Transaction information [`Modal`] content. pub struct WalletTransactionContent { @@ -44,7 +44,7 @@ pub struct WalletTransactionContent { } impl WalletTransactionContent { - /// Create new content instance with [`Wallet`] from provided [`WalletTransaction`]. + /// Create new content instance with [`Wallet`] from provided [`WalletTx`]. pub fn new(tx_id: u32) -> Self { Self { tx_id, @@ -59,7 +59,8 @@ impl WalletTransactionContent { ui: &mut egui::Ui, modal: &Modal, wallet: &Wallet, - cb: &dyn PlatformCallbacks) { + cb: &dyn PlatformCallbacks, + on_delete: impl FnOnce(u32)) { // Check values and setup transaction data. let wallet_data = wallet.get_data(); if wallet_data.is_none() { @@ -70,7 +71,7 @@ impl WalletTransactionContent { let data_txs = data.txs.clone().unwrap(); let txs = data_txs.into_iter() .filter(|tx| tx.data.id == self.tx_id) - .collect::>(); + .collect::>(); if txs.is_empty() { Modal::close(); return; @@ -110,7 +111,7 @@ impl WalletTransactionContent { } else { modal.set_background_color(Colors::fill()); // Show transaction information. - self.info_ui(ui, tx, wallet, cb); + self.info_ui(ui, tx, wallet, cb, on_delete); // Show transaction sharing content or payment proof. if self.proof_content.is_none() && tx.can_cancel() && !tx.finalized() { @@ -155,7 +156,7 @@ impl WalletTransactionContent { fn share_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, - tx: &WalletTransaction, + tx: &WalletTx, cb: &dyn PlatformCallbacks) { if self.message.is_none() { let slatepack_path = wallet.get_config().get_tx_slate_path(tx); @@ -258,22 +259,27 @@ impl WalletTransactionContent { /// Draw transaction information content. fn info_ui(&mut self, ui: &mut egui::Ui, - tx: &WalletTransaction, + tx: &WalletTx, wallet: &Wallet, - cb: &dyn PlatformCallbacks) { + cb: &dyn PlatformCallbacks, + on_delete: impl FnOnce(u32)) { ui.add_space(6.0); - + // Transaction item background setup. let mut rect = ui.available_rect_before_wrap(); rect.set_height(WalletTransactionsContent::TX_ITEM_HEIGHT); - - // Draw tx item background. - let p = ui.painter(); - let r = View::item_rounding(0, 2, false); - p.rect(rect, r, Colors::TRANSPARENT, View::item_stroke(), StrokeKind::Outside); - + let rounding = View::item_rounding(0, 2, false); + let bg = Colors::TRANSPARENT; // Show transaction amount status and time. let data = wallet.get_data().unwrap(); - WalletTransactionsContent::tx_item_ui(ui, tx, rect, &data, |ui| { + let on_click = (false, || {}); + WalletTransactionsContent::tx_item_ui(ui, tx, rect, bg, rounding, &data, on_click, |ui| { + // Show button to delete transaction from database. + if tx.data.confirmed || tx.cancelled() { + let r = View::item_rounding(0, 2, true); + View::item_button(ui, r, FILE_X, Some(Colors::inactive_text()), || { + on_delete(tx.data.id); + }); + } // Show block height or buttons. if let Some(h) = tx.height { if h != 0 { @@ -288,27 +294,24 @@ impl WalletTransactionContent { } return; } - if wallet.synced_from_node() && !tx.cancelled() && !tx.cancelling() && !tx.posting() { - let rebroadcast = tx.broadcasting_timed_out(&wallet); - + let repeat = tx.broadcasting_timed_out(&wallet); // Draw button to cancel transaction. - if tx.can_cancel() || rebroadcast { + if tx.can_cancel() || repeat { let r = View::item_rounding(0, 2, true); View::item_button(ui, r, PROHIBIT, Some(Colors::red()), || { - wallet.task(WalletTask::Cancel(tx.data.clone())); + wallet.task(WalletTask::Cancel(tx.data.id)); Modal::close(); }); } - // Draw button to repeat transaction action. - if tx.can_repeat_action() || rebroadcast { + if tx.can_repeat_action() || repeat { let r = if tx.can_finalize() || tx.can_cancel() { CornerRadius::default() } else { View::item_rounding(0, 2, true) }; - WalletTransactionsContent::tx_repeat_button_ui(ui, r, tx, wallet, rebroadcast); + WalletTransactionsContent::tx_repeat_button_ui(ui, r, tx, wallet, repeat); } } }); diff --git a/src/gui/views/wallets/wallet/types.rs b/src/gui/views/wallets/wallet/types.rs index 1816ff81..db50fda6 100644 --- a/src/gui/views/wallets/wallet/types.rs +++ b/src/gui/views/wallets/wallet/types.rs @@ -44,7 +44,7 @@ pub trait WalletContentContainer { /// Get wallet status text. pub fn wallet_status_text(wallet: &Wallet) -> String { - if wallet.sync_error() { + if wallet.sync_error() && wallet.is_open() { format!("{} {}", WARNING_CIRCLE, t!("error")) } else if wallet.is_closing() { format!("{} {}", SPINNER, t!("wallets.closing")) diff --git a/src/tor/tor.rs b/src/tor/tor.rs index 495eae4f..8d77c578 100644 --- a/src/tor/tor.rs +++ b/src/tor/tor.rs @@ -404,8 +404,8 @@ impl Tor { let mut w_client = TOR_STATE.client_config.write(); *w_client = None; // Clear state. - fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default();} - + fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default(); + } port_key } diff --git a/src/wallet/config.rs b/src/wallet/config.rs index 6ee8375d..5f94baec 100644 --- a/src/wallet/config.rs +++ b/src/wallet/config.rs @@ -23,7 +23,7 @@ use serde_derive::{Deserialize, Serialize}; use crate::{AppConfig, Settings}; use crate::wallet::ConnectionsConfig; -use crate::wallet::types::{ConnectionMethod, WalletTransaction}; +use crate::wallet::types::{ConnectionMethod, WalletTx}; /// Wallet configuration. #[derive(Serialize, Deserialize, Clone)] @@ -228,7 +228,7 @@ impl WalletConfig { } /// Get Slatepack file path for transaction. - pub fn get_tx_slate_path(&self, tx: &WalletTransaction) -> PathBuf { + pub fn get_tx_slate_path(&self, tx: &WalletTx) -> PathBuf { let mut path = PathBuf::from(self.get_base_data_path()); path.push(SLATEPACKS_DIR_NAME); if !path.exists() { diff --git a/src/wallet/types.rs b/src/wallet/types.rs index 264bf951..e0e966ca 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -150,7 +150,7 @@ pub struct WalletData { pub info: WalletInfo, /// Transactions data. - pub txs: Option>, + pub txs: Option>, /// Number of txs to show on select from database. pub txs_limit: u32, } @@ -160,53 +160,34 @@ impl WalletData { pub const TXS_LIMIT: u32 = 30; /// Update transaction action status. - pub fn on_tx_action(&mut self, id: String, action: Option) { + pub fn on_tx_action(&mut self, id: u32, action: Option) { if self.txs.is_none() { return; } for tx in self.txs.as_mut().unwrap() { - if let Some(slate_id) = tx.data.tx_slate_id { - if slate_id.to_string() == id { - tx.action = action; - tx.action_error = None; - break; - } + if id == tx.data.id { + tx.action = action; + tx.action_error = None; + break; } } } /// Update transaction action error status. - pub fn on_tx_error(&mut self, id: String, err: Option) { + pub fn on_tx_error(&mut self, id: u32, err: Option) { if self.txs.is_none() { return; } for tx in self.txs.as_mut().unwrap() { - if let Some(slate_id) = tx.data.tx_slate_id { - if slate_id.to_string() == id { - tx.action_error = err; - break; - } + if id == tx.data.id { + tx.action_error = err; + break; } } } - /// Get transaction by slate identifier. - pub fn tx_by_slate_id(&self, id: String) -> Option { - if self.txs.is_none() { - return None; - } - for tx in self.txs.as_ref().unwrap() { - if let Some(slate_id) = tx.data.tx_slate_id { - if slate_id.to_string() == id { - return Some(tx.clone()); - } - } - } - None - } - /// Get transaction by identifier. - pub fn tx_by_id(&self, id: u32) -> Option { + pub fn tx_by_id(&self, id: u32) -> Option { if self.txs.is_none() { return None; } @@ -221,13 +202,13 @@ impl WalletData { /// Wallet transaction action. #[derive(Clone, PartialEq)] -pub enum WalletTransactionAction { - Cancelling, Finalizing, Posting, SendingTor +pub enum WalletTxAction { + Cancelling, Finalizing, Posting, SendingTor, Deleting } /// Wallet transaction data. #[derive(Clone)] -pub struct WalletTransaction { +pub struct WalletTx { /// Information from database. pub data: TxLogEntry, /// State of transaction Slate. @@ -247,19 +228,19 @@ pub struct WalletTransaction { pub broadcasting_height: Option, /// Action on transaction. - pub action: Option, + pub action: Option, /// Action result error. pub action_error: Option } -impl WalletTransaction { +impl WalletTx { /// Create new wallet transaction. pub fn new(tx: TxLogEntry, proof: Option, wallet: &Wallet, height: Option, broadcasting_height: Option, - action: Option, + action: Option, action_error: Option) -> Self { let amount = if tx.amount_debited > tx.amount_credited { tx.amount_debited - tx.amount_credited @@ -352,7 +333,7 @@ impl WalletTransaction { /// Check if transaction is sending over Tor. pub fn sending_tor(&self) -> bool { if let Some(a) = self.action.as_ref() { - return a == &WalletTransactionAction::SendingTor; + return a == &WalletTxAction::SendingTor; } false } @@ -360,7 +341,7 @@ impl WalletTransaction { /// Check if transaction is cancelling. pub fn cancelling(&self) -> bool { if let Some(a) = self.action.as_ref() { - return a == &WalletTransactionAction::Cancelling; + return a == &WalletTxAction::Cancelling; } false } @@ -368,7 +349,7 @@ impl WalletTransaction { /// Check if transaction is posting. pub fn posting(&self) -> bool { if let Some(a) = self.action.as_ref() { - return a == &WalletTransactionAction::Posting; + return a == &WalletTxAction::Posting; } false } @@ -390,7 +371,7 @@ impl WalletTransaction { /// Check if transaction is finalizing. pub fn finalizing(&self) -> bool { if let Some(a) = self.action.as_ref() { - return a == &WalletTransactionAction::Finalizing; + return a == &WalletTxAction::Finalizing; } false } @@ -398,7 +379,7 @@ impl WalletTransaction { /// Check if possible to repeat transaction action. pub fn can_repeat_action(&self) -> bool { if let Some(a) = &self.action { - self.action_error.is_some() && a != &WalletTransactionAction::Cancelling + self.action_error.is_some() && a != &WalletTxAction::Cancelling } else { // Can resend over Tor. !self.data.confirmed && !self.sending_tor() && !self.broadcasting() && @@ -426,6 +407,14 @@ impl WalletTransaction { } false } + + /// Check if transaction is deleting. + pub fn deleting(&self) -> bool { + if let Some(a) = self.action.as_ref() { + return a == &WalletTxAction::Deleting; + } + false + } } /// Task for the wallet. @@ -446,19 +435,22 @@ pub enum WalletTask { /// * receiver Send(u64, Option), /// Send request over Tor. - /// * local tx id + /// * tx /// * receiver - SendTor(u32, SlatepackAddress), + SendTor(TxLogEntry, SlatepackAddress), /// Invoice creation. /// * amount Receive(u64), /// Transaction finalization. - /// * local tx id + /// * tx id Finalize(u32), /// Post transaction to blockchain. - /// * local tx id + /// * tx id Post(u32), /// Cancel transaction. - /// * tx - Cancel(TxLogEntry), + /// * tx id + Cancel(u32), + /// Delete transaction. + /// * tx id + Delete(u32) } \ No newline at end of file diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index e97525d4..357d70c9 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -16,7 +16,7 @@ use crate::node::{Node, NodeConfig}; use crate::tor::Tor; use crate::wallet::seed::WalletSeed; use crate::wallet::store::TxHeightStore; -use crate::wallet::types::{ConnectionMethod, PhraseMode, WalletAccount, WalletData, WalletInstance, WalletTask, WalletTransaction, WalletTransactionAction}; +use crate::wallet::types::{ConnectionMethod, PhraseMode, WalletAccount, WalletData, WalletInstance, WalletTask, WalletTx, WalletTxAction}; use crate::wallet::{ConnectionsConfig, Mnemonic, WalletConfig}; use crate::AppConfig; @@ -48,7 +48,7 @@ use std::sync::mpsc::Sender; use std::sync::{mpsc, Arc}; use std::thread::Thread; use std::time::Duration; -use std::{fs, thread}; +use std::{fs, path, thread}; use chrono::Utc; use log::error; use num_bigint::BigInt; @@ -131,7 +131,7 @@ pub struct Wallet { /// Tasks sender. tasks_sender: Arc>>>, /// Task result with optional transaction identifier. - task_result: Arc, WalletTask)>>>, + task_result: Arc, WalletTask)>>>, } impl Wallet { @@ -379,13 +379,8 @@ impl Wallet { } } - // Set Slatepack address and secret key. - if let Ok((key, addr)) = self.get_secret_key_addr() { - let mut w_key = self.secret_key.write(); - *w_key = Some(key); - let mut w_address = self.slatepack_address.write(); - *w_address = Some(addr.to_string()); - } + // Update Slatepack address and secret key. + self.update_secret_key_addr()?; Ok(()) } @@ -403,7 +398,7 @@ impl Wallet { } /// Retrieve wallet [`SecretKey`] and Slatepack address for transport. - fn get_secret_key_addr(&self) -> Result<(SecretKey, SlatepackAddress), Error> { + fn update_secret_key_addr(&self) -> Result<(), Error> { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let mut w_lock = instance.lock(); @@ -414,7 +409,11 @@ impl Wallet { let sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0) .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; let addr = SlatepackAddress::try_from(&sec_key)?; - Ok((sec_key, addr)) + let mut w_key = self.secret_key.write(); + *w_key = Some(sec_key); + let mut w_address = self.slatepack_address.write(); + *w_address = Some(addr.to_string()); + Ok(()) } /// Get unique opened wallet identifier, including current account. @@ -628,12 +627,11 @@ impl Wallet { } /// Select transaction by slate id. - fn retrieve_tx_by_id(&self, id: Uuid) -> Option { + fn retrieve_tx_by_id(&self, id: Option, slate_id: Option) -> Option { let r_inst = self.instance.as_ref().read(); let inst = r_inst.clone().unwrap(); let mask = self.keychain_mask(); - let tx_id = Some(id); - if let Ok((_, txs)) = retrieve_txs(inst, mask.as_ref(), &None, false, None, tx_id, None) { + if let Ok((_, txs)) = retrieve_txs(inst, mask.as_ref(), &None, false, id, slate_id, None) { if !txs.is_empty() { return Some(txs.get(0).unwrap().clone()) } @@ -740,13 +738,8 @@ impl Wallet { Ok(()) })?; - // Setup secret key and Slatepack address. - if let Ok((key, addr)) = self.get_secret_key_addr() { - let mut w_key = self.secret_key.write(); - *w_key = Some(key); - let mut w_address = self.slatepack_address.write(); - *w_address = Some(addr.to_string()); - } + // Update Slatepack address and secret key. + self.update_secret_key_addr()?; // Save account label into config. let mut w_config = self.config.write(); @@ -893,7 +886,7 @@ impl Wallet { /// Check if Slatepack file exists. pub fn slatepack_exists(&self, slate: &Slate) -> bool { let slatepack_path = self.get_config().get_slate_path(slate); - fs::exists(slatepack_path).unwrap() + fs::exists(slatepack_path).unwrap_or(false) } /// Calculate transaction fee for provided amount. @@ -969,12 +962,12 @@ impl Wallet { } /// Send slate to Tor address. - async fn send_tor(&self, slate: &Slate, addr: &SlatepackAddress) -> Result { - self.on_tx_action(slate.id.to_string(), Some(WalletTransactionAction::SendingTor)); + async fn send_tor(&self, id: u32, s: &Slate, addr: &SlatepackAddress) -> Result { + self.on_tx_action(id, Some(WalletTxAction::SendingTor)); let tor_addr = OnionV3Address::try_from(addr).unwrap().to_http_str(); let url = format!("{}/v2/foreign", tor_addr); - let slate_send = VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?; + let slate_send = VersionedSlate::into_version(s.clone(), SlateVersion::V4)?; let body = json!({ "jsonrpc": "2.0", "method": "receive_tx", @@ -1074,8 +1067,8 @@ impl Wallet { } /// Finalize transaction from provided message as sender or invoice issuer. - fn finalize(&self, slate: &Slate) -> Result { - self.on_tx_action(slate.id.to_string(), Some(WalletTransactionAction::Finalizing)); + fn finalize(&self, slate: &Slate, id: u32) -> Result { + self.on_tx_action(id, Some(WalletTxAction::Finalizing)); let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); @@ -1090,14 +1083,16 @@ impl Wallet { let _ = self.create_slatepack_message(&slate, None)?; // Clear tx action. - self.on_tx_action(slate.id.to_string(), None); + self.on_tx_action(id, None); Ok(slate) } /// Post transaction to blockchain. - fn post(&self, slate: &Slate) -> Result<(), Error> { - self.on_tx_action(slate.id.to_string(), Some(WalletTransactionAction::Posting)); + fn post(&self, slate: &Slate, id: Option) -> Result<(), Error> { + if let Some(id) = id { + self.on_tx_action(id, Some(WalletTxAction::Posting)); + } let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); @@ -1108,19 +1103,19 @@ impl Wallet { })?; // Clear tx action. - self.on_tx_action(slate.id.to_string(), None); - + if let Some(id) = id { + self.on_tx_action(id, None); + } Ok(()) } /// Cancel transaction. - fn cancel(&self, tx: &TxLogEntry) -> Result<(), Error> { - let id = tx.tx_slate_id.unwrap().to_string(); - self.on_tx_action(id.clone(), Some(WalletTransactionAction::Cancelling)); + fn cancel(&self, id: u32) -> Result<(), Error> { + self.on_tx_action(id, Some(WalletTxAction::Cancelling)); let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); - cancel_tx(instance, self.keychain_mask().as_ref(), &None, Some(tx.id), None)?; + cancel_tx(instance, self.keychain_mask().as_ref(), &None, Some(id), None)?; // Clear tx action. self.on_tx_action(id, None); @@ -1129,25 +1124,30 @@ impl Wallet { } /// Update transaction action status. - fn on_tx_action(&self, id: String, action: Option) { + fn on_tx_action(&self, id: u32, action: Option) { let mut w_data = self.data.write(); w_data.as_mut().unwrap().on_tx_action(id, action); } /// Update transaction action error status. - fn on_tx_error(&self, id: String, err: Option) { + fn on_tx_error(&self, id: u32, err: Option) { let mut w_data = self.data.write(); w_data.as_mut().unwrap().on_tx_error(id, err); } /// Save task result to consume later. - fn on_task_result(&self, id: Option, task: &WalletTask) { + fn on_task_result(&self, tx: Option, task: &WalletTask) { let mut w_res = self.task_result.write(); + let id = if let Some(t) = tx { + Some(t.id) + } else { + None + }; *w_res = Some((id, task.clone())); } /// Consume result of successful task. - pub fn consume_task_result(&self) -> Option<(Option, WalletTask)> { + pub fn consume_task_result(&self) -> Option<(Option, WalletTask)> { let res = { let r_res = self.task_result.read(); r_res.clone() @@ -1159,7 +1159,7 @@ impl Wallet { } /// Get possible transaction confirmation height. - fn tx_height(&self, tx: &WalletTransaction) -> Result, Error> { + fn tx_height(&self, tx: &WalletTx) -> Result, Error> { let mut tx_height = None; if tx.data.confirmed && tx.data.kernel_excess.is_some() { let r_inst = self.instance.as_ref().read(); @@ -1185,17 +1185,45 @@ impl Wallet { Ok(tx_height) } - /// Get transaction Slate from database. - fn get_tx(&self, tx_id: u32) -> Option { + /// Get stored transaction Slate. + fn get_tx_slate(&self, tx_id: Option, slate_id: Option<&Uuid>) -> Option { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let api = Owner::new(instance, None); - if let Ok(s) = api.get_stored_tx(self.keychain_mask().as_ref(), Some(tx_id), None) { + if let Ok(s) = api.get_stored_tx(self.keychain_mask().as_ref(), tx_id, slate_id) { return s; } None } + /// Delete transaction from database. + fn delete_tx(&self, id: u32) -> Result<(), Error> { + self.on_tx_action(id, Some(WalletTxAction::Deleting)); + + let slate = self.get_tx_slate(Some(id), None); + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let keychain_mask = self.keychain_mask(); + let mut wallet_lock = instance.lock(); + let lc = wallet_lock.lc_provider()?; + let w = lc.wallet_inst()?; + let parent_key = w.parent_key_id(); + let mut batch = w.batch(keychain_mask.as_ref())?; + batch.delete_tx_log_entry(id, &parent_key)?; + batch.commit()?; + + // Delete transaction files. + if let Some(s) = slate { + let slatepack_path = self.get_config().get_slate_path(&s); + fs::remove_file(&slatepack_path).unwrap_or_default(); + let path = path::Path::new(&self.get_config().get_data_path()) + .join("saved_txs") + .join(format!("{}.grintx", s.id)); + fs::remove_file(&path).unwrap_or_default(); + } + Ok(()) + } + /// Change wallet password. pub fn change_password(&self, old: String, new: String) -> Result<(), Error> { let r_inst = self.instance.as_ref().read(); @@ -1577,36 +1605,33 @@ fn start_sync(wallet: Wallet) -> Thread { /// Handle wallet task. async fn handle_task(w: &Wallet, t: WalletTask) { - let send_tor = async |s: &Slate, r: &SlatepackAddress| { - let id = s.id.to_string(); - match w.send_tor(&s, r).await { + let send_tor = async |tx: TxLogEntry, s: &Slate, r: &SlatepackAddress| { + match w.send_tor(tx.id, &s, r).await { Ok(s) => { - match w.finalize(&s) { + match w.finalize(&s, tx.id) { Ok(s) => { - match w.post(&s) { + match w.post(&s, Some(tx.id)) { Ok(_) => { sync_wallet_data(&w, false); - w.on_task_result(Some(id), &t); + w.on_task_result(Some(tx), &t); } Err(e) => { error!("send tor post error: {:?}", e); - w.on_tx_error(id, Some(e)); + w.on_tx_error(tx.id, Some(e)); } } } Err(e) => { error!("send tor finalize error: {:?}", e); - if let Some(tx) = w.retrieve_tx_by_id(s.id) { - let _ = w.cancel(&tx); - sync_wallet_data(&w, false); - } + let _ = w.cancel(tx.id); + sync_wallet_data(&w, false); } } } Err(e) => { error!("send tor error: {:?}", e); - w.on_tx_error(id.clone(), Some(e)); - w.on_task_result(Some(id), &t); + w.on_tx_error(tx.id, Some(e)); + w.on_task_result(Some(tx), &t); } } }; @@ -1616,74 +1641,71 @@ async fn handle_task(w: &Wallet, t: WalletTask) { return; } let w = w.clone(); - let load = w.message_opening.clone(); let msg = m.clone(); - thread::spawn(move || { - load.store(true, Ordering::Relaxed); - if let Ok((s, dest)) = w.parse_slatepack(&msg) { - let id = s.id.to_string(); - // Check if message already exists. - let exists = { - let mut exists = w.slatepack_exists(&s); - if !exists && (s.state == SlateState::Invoice2 || - s.state == SlateState::Standard2) { - let mut slate = s.clone(); - slate.state = if s.state == SlateState::Standard2 { - SlateState::Standard3 - } else { - SlateState::Invoice3 - }; - exists = w.slatepack_exists(&slate); - } - exists - }; - if exists { - w.on_task_result(Some(id), &t); - load.store(false, Ordering::Relaxed); - return; + w.message_opening.store(true, Ordering::Relaxed); + if let Ok((s, dest)) = w.parse_slatepack(&msg) { + let tx = w.retrieve_tx_by_id(None, Some(s.id)); + // Check if message already exists. + let exists = { + let mut exists = w.slatepack_exists(&s); + if !exists && (s.state == SlateState::Invoice2 || + s.state == SlateState::Standard2) { + let mut slate = s.clone(); + slate.state = if s.state == SlateState::Standard2 { + SlateState::Standard3 + } else { + SlateState::Invoice3 + }; + exists = w.slatepack_exists(&slate); } - // Create response or finalize. - match s.state { - SlateState::Standard1 | SlateState::Invoice1 => { - if s.state != SlateState::Standard1 { - if let Ok(_) = w.pay(&s) { - sync_wallet_data(&w, false); - w.on_task_result(Some(id), &t); - } - } else { - if let Ok(_) = w.receive(&s, dest) { - sync_wallet_data(&w, false); - w.on_task_result(Some(id), &t); - } + exists + }; + if exists { + w.on_task_result(tx, &t); + w.message_opening.store(false, Ordering::Relaxed); + return; + } + // Create response or finalize. + match s.state { + SlateState::Standard1 | SlateState::Invoice1 => { + if s.state != SlateState::Standard1 { + if let Ok(_) = w.pay(&s) { + sync_wallet_data(&w, false); + w.on_task_result(tx, &t); + } + } else { + if let Ok(_) = w.receive(&s, dest) { + sync_wallet_data(&w, false); + w.on_task_result(tx, &t); } } - SlateState::Standard2 | SlateState::Invoice2 => { - match w.finalize(&s) { + } + SlateState::Standard2 | SlateState::Invoice2 => { + if let Some(tx) = tx { + match w.finalize(&s, tx.id) { Ok(s) => { - match w.post(&s) { + match w.post(&s, Some(tx.id)) { Ok(_) => { sync_wallet_data(&w, false); } Err(e) => { error!("message tx post error: {:?}", e); - w.on_tx_error(id, Some(e)); + w.on_tx_error(tx.id, Some(e)); } } } Err(e) => { - if let Some(tx) = w.retrieve_tx_by_id(s.id) { - let _ = w.cancel(&tx); - } + let _ = w.cancel(tx.id); error!("message tx finalize error: {:?}", e); - w.on_tx_error(id, Some(e)); + w.on_tx_error(tx.id, Some(e)); } } } - _ => {} - }; - } - load.store(false, Ordering::Relaxed); - }); + } + _ => {} + }; + } + w.message_opening.store(false, Ordering::Relaxed); } WalletTask::CalculateFee(a, _) => { // Wait if there are no more fee tasks or handle next input value. @@ -1710,95 +1732,101 @@ async fn handle_task(w: &Wallet, t: WalletTask) { w.send_creating.store(true, Ordering::Relaxed); if let Ok(s) = w.send(*a, r.clone()) { sync_wallet_data(&w, false); - if let Some(addr) = r { - w.send_creating.store(false, Ordering::Relaxed); - send_tor(&s, addr).await; - return; - } else { - w.on_task_result(Some(s.id.to_string()), &t); + let tx = w.retrieve_tx_by_id(None, Some(s.id)); + if let Some(tx) = tx { + if let Some(addr) = r { + w.send_creating.store(false, Ordering::Relaxed); + send_tor(tx, &s, addr).await; + return; + } else { + w.on_task_result(Some(tx), &t); + } } } w.send_creating.store(false, Ordering::Relaxed); } - WalletTask::SendTor(id, r) => { - if let Some(s) = w.get_tx(*id) { - send_tor(&s, r).await; + WalletTask::SendTor(tx, r) => { + if let Some(s) = w.get_tx_slate(Some(tx.id), None) { + send_tor(tx.clone(), &s, r).await; } } WalletTask::Receive(a) => { w.invoice_creating.store(true, Ordering::Relaxed); if let Ok(s) = w.issue_invoice(*a) { sync_wallet_data(&w, false); - w.on_task_result(Some(s.id.to_string()), &t); + let tx = w.retrieve_tx_by_id(None, Some(s.id)); + if let Some(tx) = tx { + w.on_task_result(Some(tx), &t); + } } w.invoice_creating.store(false, Ordering::Relaxed); }, WalletTask::Finalize(id) => { - let slate = &w.get_tx(*id).unwrap(); - w.on_tx_error(slate.id.to_string(), None); - match w.finalize(slate) { - Ok(s) => { - match w.post(&s) { - Ok(_) => { - sync_wallet_data(&w, false); - } - Err(e) => { - error!("tx finalize post error: {:?}", e); - w.on_tx_error(slate.id.to_string(), Some(e)); + if let Some(s) = w.get_tx_slate(Some(*id), None) { + w.on_tx_error(*id, None); + match w.finalize(&s, *id) { + Ok(s) => { + match w.post(&s, Some(*id)) { + Ok(_) => { + sync_wallet_data(&w, false); + } + Err(e) => { + error!("tx finalize post error: {:?}", e); + w.on_tx_error(*id, Some(e)); + } } } - } - Err(e) => { - if let Some(tx) = w.retrieve_tx_by_id(slate.id) { - let _ = w.cancel(&tx); + Err(e) => { + let _ = w.cancel(*id); + error!("tx finalize error: {:?}", e); + w.on_tx_error(*id, Some(e)); } - error!("tx finalize error: {:?}", e); - w.on_tx_error(slate.id.to_string(), Some(e)); } + } else { + w.on_tx_error(*id, Some(Error::GenericError("tx slate not found".to_string()))); } } WalletTask::Post(id) => { - let slate = &w.get_tx(*id).unwrap(); - w.on_tx_error(slate.id.to_string(), None); - - // Cleanup broadcasting tx height. - let tx_height_store = TxHeightStore::new(w.get_config().get_extra_db_path()); - tx_height_store.delete_broadcasting_height(&slate.id.to_string()); - - let has_data = { - let r_data = w.data.read(); - r_data.is_some() - }; - if has_data { - let mut w_data = w.data.write(); - for tx in w_data.as_mut().unwrap().txs.as_mut().unwrap() { - if tx.data.id == *id { - tx.broadcasting_height = None; - break; + if let Some(s) = w.get_tx_slate(Some(*id), None) { + w.on_tx_error(*id, None); + // Cleanup broadcasting tx height. + let tx_height_store = TxHeightStore::new(w.get_config().get_extra_db_path()); + tx_height_store.delete_broadcasting_height(&id.to_string()); + let has_data = { + let r_data = w.data.read(); + r_data.is_some() + }; + if has_data { + let mut w_data = w.data.write(); + for tx in w_data.as_mut().unwrap().txs.as_mut().unwrap() { + if tx.data.id == *id { + tx.broadcasting_height = None; + break; + } } } - } - - match w.post(slate) { - Ok(_) => { - sync_wallet_data(&w, false); - } - Err(e) => { - error!("tx post error: {:?}", e); - w.on_tx_error(slate.id.to_string(), Some(e)); + // Post transaction. + match w.post(&s, Some(*id)) { + Ok(_) => { + sync_wallet_data(&w, false); + } + Err(e) => { + error!("tx post error: {:?}", e); + w.on_tx_error(*id, Some(e)); + } } + } else { + w.on_tx_error(*id, Some(Error::GenericError("tx slate not found".to_string()))); } - } - WalletTask::Cancel(tx) => { - match w.cancel(tx) { + WalletTask::Cancel(id) => { + match w.cancel(*id) { Ok(_) => { sync_wallet_data(&w, false); } Err(e) => { error!("tx cancel error: {:?}", e); - let id = tx.tx_slate_id.unwrap().to_string(); - w.on_tx_error(id, Some(e)); + w.on_tx_error(*id, Some(e)); } } } @@ -1808,6 +1836,15 @@ async fn handle_task(w: &Wallet, t: WalletTask) { w.proof_verifying.store(false, Ordering::Relaxed); w.on_task_result(None, &WalletTask::VerifyProof(p.clone(), Some(res))); } + WalletTask::Delete(id) => { + match w.delete_tx(*id) { + Ok(_) => sync_wallet_data(&w, false), + Err(e) => { + error!("tx delete error: {:?}", e); + w.on_tx_error(*id, Some(e)); + } + } + } }; } @@ -1947,11 +1984,11 @@ fn update_txs(wallet: &Wallet, mut txs_limit: u32) -> Result<(), Error> { let tx_height_store = TxHeightStore::new(wallet.get_config().get_extra_db_path()); let data = wallet.get_data().unwrap(); let data_txs = data.txs.unwrap_or(vec![]); - let mut new_txs: Vec = vec![]; + let mut new_txs: Vec = vec![]; for tx in &filter_txs { let mut height: Option = None; let mut broadcasting_height: Option = None; - let mut action: Option = None; + let mut action: Option = None; let mut action_error: Option = None; let mut proof: Option = None; for t in &data_txs { @@ -1964,13 +2001,13 @@ fn update_txs(wallet: &Wallet, mut txs_limit: u32) -> Result<(), Error> { break; } } - let mut new = WalletTransaction::new(tx.clone(), - proof.clone(), - wallet, - height, - broadcasting_height, - action, - action_error); + let mut new = WalletTx::new(tx.clone(), + proof.clone(), + wallet, + height, + broadcasting_height, + action, + action_error); // Update Slate state for unconfirmed. let unconfirmed = !tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent || tx.tx_type == TxLogEntryType::TxReceived); @@ -2014,8 +2051,9 @@ fn update_txs(wallet: &Wallet, mut txs_limit: u32) -> Result<(), Error> { new.broadcasting_height = broadcasting_height; } } - - new_txs.push(new); + if !new.deleting() { + new_txs.push(new); + } } // Update wallet txs. let mut w_data = wallet.data.write(); diff --git a/wallet b/wallet index a6e0cdae..da17e165 160000 --- a/wallet +++ b/wallet @@ -1 +1 @@ -Subproject commit a6e0cdae0c071feec23544036a5eadfadbcdb93f +Subproject commit da17e16541207e1cc3d24415b50863e9b36fa13c