From c8bca08bdce3bbe72c4f226fb996a54bf10b18f0 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Thu, 15 Aug 2024 23:09:42 +0300 Subject: [PATCH 01/28] txs: share message as file from modal, module refactoring --- src/gui/views/{file.rs => file_pick.rs} | 0 src/gui/views/mod.rs | 4 +- src/gui/views/wallets/creation/creation.rs | 2 +- src/gui/views/wallets/wallet/content.rs | 2 +- src/gui/views/wallets/wallet/mod.rs | 6 +- .../views/wallets/wallet/settings/content.rs | 2 +- src/gui/views/wallets/wallet/txs.rs | 1041 ----------------- src/gui/views/wallets/wallet/txs/content.rs | 496 ++++++++ src/gui/views/wallets/wallet/txs/mod.rs | 19 + src/gui/views/wallets/wallet/txs/tx.rs | 614 ++++++++++ src/main.rs | 7 +- 11 files changed, 1143 insertions(+), 1050 deletions(-) rename src/gui/views/{file.rs => file_pick.rs} (100%) delete mode 100644 src/gui/views/wallets/wallet/txs.rs create mode 100644 src/gui/views/wallets/wallet/txs/content.rs create mode 100644 src/gui/views/wallets/wallet/txs/mod.rs create mode 100644 src/gui/views/wallets/wallet/txs/tx.rs diff --git a/src/gui/views/file.rs b/src/gui/views/file_pick.rs similarity index 100% rename from src/gui/views/file.rs rename to src/gui/views/file_pick.rs diff --git a/src/gui/views/mod.rs b/src/gui/views/mod.rs index 03a2fbd..3be7242 100644 --- a/src/gui/views/mod.rs +++ b/src/gui/views/mod.rs @@ -36,8 +36,8 @@ pub use camera::*; mod qr; pub use qr::*; -mod file; -pub use file::*; +mod file_pick; +pub use file_pick::*; mod pull_to_refresh; pub use pull_to_refresh::*; \ No newline at end of file diff --git a/src/gui/views/wallets/creation/creation.rs b/src/gui/views/wallets/creation/creation.rs index d8b3cc8..eaf3621 100644 --- a/src/gui/views/wallets/creation/creation.rs +++ b/src/gui/views/wallets/creation/creation.rs @@ -23,7 +23,7 @@ use crate::gui::views::{Modal, Content, View}; use crate::gui::views::types::{ModalPosition, TextEditOptions}; use crate::gui::views::wallets::creation::MnemonicSetup; use crate::gui::views::wallets::creation::types::Step; -use crate::gui::views::wallets::settings::ConnectionSettings; +use crate::gui::views::wallets::ConnectionSettings; use crate::node::Node; use crate::wallet::{ExternalConnection, Wallet}; use crate::wallet::types::PhraseMode; diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 1de3632..edc8134 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -26,7 +26,7 @@ use crate::gui::views::{CameraContent, Modal, Content, View}; use crate::gui::views::types::{ModalPosition, QrScanResult, TextEditOptions}; use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport}; use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType}; -use crate::gui::views::wallets::settings::WalletSettings; +use crate::gui::views::wallets::wallet::WalletSettings; use crate::node::Node; use crate::wallet::{Wallet, WalletConfig}; use crate::wallet::types::{WalletAccount, WalletData}; diff --git a/src/gui/views/wallets/wallet/mod.rs b/src/gui/views/wallets/wallet/mod.rs index 0c82d31..dc40505 100644 --- a/src/gui/views/wallets/wallet/mod.rs +++ b/src/gui/views/wallets/wallet/mod.rs @@ -13,10 +13,12 @@ // limitations under the License. pub mod types; -pub mod settings; + +mod settings; +pub use settings::*; mod txs; -pub use txs::WalletTransactions; +pub use txs::*; mod messages; pub use messages::WalletMessages; diff --git a/src/gui/views/wallets/wallet/settings/content.rs b/src/gui/views/wallets/wallet/settings/content.rs index 085ff9b..4a77572 100644 --- a/src/gui/views/wallets/wallet/settings/content.rs +++ b/src/gui/views/wallets/wallet/settings/content.rs @@ -18,7 +18,7 @@ use egui::scroll_area::ScrollBarVisibility; use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Content, View}; -use crate::gui::views::wallets::settings::{CommonSettings, ConnectionSettings, RecoverySettings}; +use crate::gui::views::wallets::{CommonSettings, ConnectionSettings, RecoverySettings}; use crate::gui::views::wallets::types::{WalletTab, WalletTabType}; use crate::gui::views::wallets::WalletContent; use crate::wallet::Wallet; diff --git a/src/gui/views/wallets/wallet/txs.rs b/src/gui/views/wallets/wallet/txs.rs deleted file mode 100644 index 1fd7053..0000000 --- a/src/gui/views/wallets/wallet/txs.rs +++ /dev/null @@ -1,1041 +0,0 @@ -// Copyright 2023 The Grim Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::sync::Arc; -use std::thread; -use std::time::{SystemTime, UNIX_EPOCH}; -use egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea}; -use egui::scroll_area::ScrollBarVisibility; -use grin_core::core::amount_to_hr_string; -use grin_util::ToHex; -use grin_wallet_libwallet::{Error, Slate, SlateState, TxLogEntryType}; -use parking_lot::RwLock; - -use crate::gui::Colors; -use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, ARROW_CLOCKWISE, BRIDGE, BROOM, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, CHECK_CIRCLE, CLIPBOARD_TEXT, COPY, DOTS_THREE_CIRCLE, FILE_ARCHIVE, FILE_TEXT, GEAR_FINE, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN, X_CIRCLE}; -use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{CameraContent, FilePickButton, Modal, PullToRefresh, QrCodeContent, Content, View}; -use crate::gui::views::types::ModalPosition; -use crate::gui::views::wallets::types::WalletTab; -use crate::gui::views::wallets::wallet::types::{GRIN, SLATEPACK_MESSAGE_HINT, WalletTabType}; -use crate::gui::views::wallets::wallet::WalletContent; -use crate::wallet::types::{WalletData, WalletTransaction}; -use crate::wallet::Wallet; - -/// Wallet transactions tab content. -pub struct WalletTransactions { - /// Transaction identifier to use at [`Modal`]. - tx_info_id: Option, - /// Identifier for [`Slate`] to use at [`Modal`]. - tx_info_slate_id: Option, - /// Response Slatepack message input value at [`Modal`]. - tx_info_response_edit: String, - /// Finalization Slatepack message input value at [`Modal`]. - tx_info_finalize_edit: String, - /// Flag to check if error happened during transaction finalization at [`Modal`]. - tx_info_finalize_error: bool, - /// Flag to check if tx finalization requested at [`Modal`]. - tx_info_finalize: bool, - /// Flag to check if tx is finalizing at [`Modal`]. - tx_info_finalizing: bool, - /// Transaction finalization result for [`Modal`]. - tx_info_final_result: Arc>>>, - /// Flag to check if QR code is showing at [`Modal`]. - tx_info_show_qr: bool, - /// QR code Slatepack message image [`Modal`] content. - tx_info_qr_code_content: QrCodeContent, - /// Flag to check if QR code scanner is showing at [`Modal`]. - tx_info_show_scanner: bool, - /// QR code scanner [`Modal`] content. - tx_info_scanner_content: CameraContent, - /// Button to parse picked file content at [`Modal`]. - tx_info_file_pick_button: FilePickButton, - - /// Transaction identifier to use at confirmation [`Modal`]. - confirm_cancel_tx_id: Option, - - /// Flag to check if sync of wallet was initiated manually at time. - manual_sync: Option -} - -impl Default for WalletTransactions { - fn default() -> Self { - Self { - tx_info_id: None, - tx_info_slate_id: None, - tx_info_response_edit: "".to_string(), - tx_info_finalize_edit: "".to_string(), - tx_info_finalize_error: false, - tx_info_finalize: false, - tx_info_finalizing: false, - tx_info_final_result: Arc::new(RwLock::new(None)), - tx_info_show_qr: false, - tx_info_qr_code_content: QrCodeContent::new("".to_string(), true), - tx_info_show_scanner: false, - tx_info_scanner_content: CameraContent::default(), - tx_info_file_pick_button: FilePickButton::default(), - confirm_cancel_tx_id: None, - manual_sync: None, - } - } -} - -impl WalletTab for WalletTransactions { - fn get_type(&self) -> WalletTabType { - WalletTabType::Txs - } - - fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { - if WalletContent::sync_ui(ui, wallet) { - return; - } - - // Show modal content for this ui container. - self.modal_content_ui(ui, wallet, cb); - - // Show wallet transactions content. - egui::CentralPanel::default() - .frame(egui::Frame { - stroke: View::item_stroke(), - fill: Colors::button(), - inner_margin: Margin { - left: View::far_left_inset_margin(ui) + 4.0, - right: View::get_right_inset() + 4.0, - top: 0.0, - bottom: 4.0, - }, - ..Default::default() - }) - .show_inside(ui, |ui| { - ui.vertical_centered(|ui| { - let data = wallet.get_data().unwrap(); - self.txs_ui(ui, wallet, &data, cb); - }); - }); - } -} - -/// 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"; - -/// Height of transaction list item. -const TX_ITEM_HEIGHT: f32 = 76.0; - -impl WalletTransactions { - /// Draw transactions content. - fn txs_ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - data: &WalletData, - cb: &dyn PlatformCallbacks) { - let amount_conf = data.info.amount_awaiting_confirmation; - let amount_fin = data.info.amount_awaiting_finalization; - let amount_locked = data.info.amount_locked; - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - // Show non-zero awaiting confirmation amount. - if amount_conf != 0 { - let awaiting_conf = amount_to_hr_string(amount_conf, true); - let rounding = if amount_fin != 0 || amount_locked != 0 { - [false, false, false, false] - } else { - [false, false, true, true] - }; - View::rounded_box(ui, - format!("{} ツ", awaiting_conf), - t!("wallets.await_conf_amount"), - rounding); - } - // Show non-zero awaiting finalization amount. - if amount_fin != 0 { - let awaiting_conf = amount_to_hr_string(amount_fin, true); - let rounding = if amount_locked != 0 { - [false, false, false, false] - } else { - [false, false, true, true] - }; - View::rounded_box(ui, - format!("{} ツ", awaiting_conf), - t!("wallets.await_fin_amount"), - rounding); - } - // Show non-zero locked amount. - if amount_locked != 0 { - let awaiting_conf = amount_to_hr_string(amount_locked, true); - View::rounded_box(ui, - format!("{} ツ", awaiting_conf), - t!("wallets.locked_amount"), - [false, false, true, true]); - } - - // Show message when txs are empty. - if let Some(txs) = data.txs.as_ref() { - if txs.is_empty() { - View::center_content(ui, 96.0, |ui| { - let empty_text = t!( - "wallets.txs_empty", - "message" => CHAT_CIRCLE_TEXT, - "transport" => BRIDGE, - "settings" => GEAR_FINE - ); - ui.label(RichText::new(empty_text).size(16.0).color(Colors::inactive_text())); - }); - return; - } - } - }); - - // Show loader when txs are not loaded. - if data.txs.is_none() { - ui.centered_and_justified(|ui| { - View::big_loading_spinner(ui); - }); - return; - } - - ui.add_space(4.0); - - // Show list of transactions. - let txs = data.txs.as_ref().unwrap(); - let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(); - let refresh = self.manual_sync.unwrap_or(0) + 1600 > now; - let refresh_resp = PullToRefresh::new(refresh) - .can_refresh(!refresh && !wallet.syncing()) - .min_refresh_distance(70.0) - .scroll_area_ui(ui, |ui| { - ScrollArea::vertical() - .id_source(Id::from("txs_content").with(wallet.get_config().id)) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .auto_shrink([false; 2]) - .show_rows(ui, TX_ITEM_HEIGHT, txs.len(), |ui, row_range| { - ui.add_space(1.0); - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - let padding = amount_conf != 0 || amount_fin != 0 || amount_locked != 0; - for index in row_range { - let tx = txs.get(index).unwrap(); - let r = View::item_rounding(index, txs.len(), false); - self.tx_item_ui(ui, tx, r, padding, true, &data, wallet, cb); - } - }); - }) - }); - - // Sync wallet on refresh. - if refresh_resp.should_refresh() { - self.manual_sync = Some(now); - if !wallet.syncing() { - wallet.sync(true); - } - } - } - - /// Draw [`Modal`] content for this ui container. - fn modal_content_ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - cb: &dyn PlatformCallbacks) { - match Modal::opened() { - None => {} - Some(id) => { - match id { - TX_INFO_MODAL => { - Modal::ui(ui.ctx(), |ui, modal| { - self.tx_info_modal_ui(ui, wallet, modal, cb); - }); - } - CANCEL_TX_CONFIRMATION_MODAL => { - Modal::ui(ui.ctx(), |ui, modal| { - self.cancel_confirmation_modal(ui, wallet, modal); - }); - } - _ => {} - } - } - } - } - - /// Draw transaction item. - fn tx_item_ui(&mut self, - ui: &mut egui::Ui, - tx: &WalletTransaction, - mut rounding: Rounding, - extra_padding: bool, - can_show_info: bool, - data: &WalletData, - wallet: &mut Wallet, - cb: &dyn PlatformCallbacks) { - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - if extra_padding { - rect.min += egui::emath::vec2(6.0, 0.0); - rect.max -= egui::emath::vec2(6.0, 0.0); - } - rect.set_height(TX_ITEM_HEIGHT); - - // Draw round background. - let bg_rect = rect.clone(); - let color = if can_show_info { - Colors::button() - } else { - Colors::fill() - }; - ui.painter().rect(bg_rect, rounding, color, View::item_stroke()); - - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |ui| { - ui.horizontal_centered(|ui| { - // Draw button to show transaction info. - if can_show_info && tx.data.tx_slate_id.is_some() { - rounding.nw = 0.0; - rounding.sw = 0.0; - View::item_button(ui, rounding, FILE_TEXT, None, || { - self.tx_info_finalize = false; - self.show_tx_info_modal(wallet, tx); - }); - } - - // Draw finalization button for tx that can be finalized. - let finalize = ((!can_show_info && !self.tx_info_finalizing) || can_show_info) - && tx.can_finalize; - if finalize { - let (icon, color) = if !can_show_info && self.tx_info_finalize { - (FILE_TEXT, None) - } else { - (CHECK, Some(Colors::green())) - }; - let final_rounding = if can_show_info { - Rounding::default() - } else { - rounding.nw = 0.0; - rounding.sw = 0.0; - rounding - }; - View::item_button(ui, final_rounding, icon, color, || { - cb.hide_keyboard(); - if !can_show_info && self.tx_info_finalize { - self.tx_info_finalize = false; - return; - } - self.tx_info_finalize = true; - // Show transaction information modal. - if can_show_info { - self.show_tx_info_modal(wallet, tx); - } - }); - } - - // Draw cancel button for tx that can be reposted and canceled. - let wallet_loaded = wallet.foreign_api_port().is_some(); - if wallet_loaded && ((!can_show_info && !self.tx_info_finalizing) || can_show_info) && - (tx.can_repost(data) || tx.can_cancel()) { - View::item_button(ui, Rounding::default(), PROHIBIT, Some(Colors::red()), || { - if can_show_info { - self.confirm_cancel_tx_id = Some(tx.data.id); - // Show transaction cancellation confirmation modal. - Modal::new(CANCEL_TX_CONFIRMATION_MODAL) - .position(ModalPosition::Center) - .title(t!("modal.confirmation")) - .show(); - } else { - cb.hide_keyboard(); - wallet.cancel(tx.data.id); - } - }); - } - - // Draw button to repost transaction. - if ((!can_show_info && !self.tx_info_finalizing) || can_show_info) && - tx.can_repost(data) { - let r = if finalize || can_show_info { - Rounding::default() - } else { - rounding.nw = 0.0; - rounding.sw = 0.0; - rounding - }; - View::item_button(ui, r, ARROW_CLOCKWISE, Some(Colors::green()), || { - cb.hide_keyboard(); - // Post tx after getting slate from slatepack file. - if let Some((s, _)) = wallet.read_slate_by_tx(tx) { - let _ = wallet.post(&s, wallet.can_use_dandelion()); - } - }); - } - }); - - 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 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 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 tx.posting { - format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_finalizing")) - } else { - if tx.cancelling { - format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_cancelling")) - } else { - match tx.data.tx_type { - TxLogEntryType::TxReceived => { - format!("{} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_receiving")) - }, - TxLogEntryType::TxSent => { - format!("{} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_sending")) - }, - _ => { - format!("{} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_confirmed")) - } - } - } - } - } else { - match tx.data.tx_type { - TxLogEntryType::ConfirmedCoinbase => { - format!("{} {}", CHECK_CIRCLE, t!("wallets.tx_confirmed")) - }, - TxLogEntryType::TxSent | TxLogEntryType::TxReceived => { - let height = data.info.last_confirmed_height; - let min_conf = data.info.minimum_confirmations; - if tx.conf_height.is_none() || (tx.conf_height.unwrap() != 0 && - height - tx.conf_height.unwrap() > min_conf - 1) { - let (i, t) = if tx.data.tx_type == TxLogEntryType::TxSent { - (ARROW_CIRCLE_UP, t!("wallets.tx_sent")) - } else { - (ARROW_CIRCLE_DOWN, t!("wallets.tx_received")) - }; - format!("{} {}", i, t) - } else { - let tx_height = tx.conf_height.unwrap() - 1; - let left_conf = height - tx_height; - let conf_info = if tx_height != 0 && height >= tx_height && - left_conf < min_conf { - format!("{}/{}", left_conf, min_conf) - } else { - "".to_string() - }; - format!("{} {} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_confirming"), - conf_info - ) - } - }, - _ => format!("{} {}", X_CIRCLE, t!("wallets.canceled")) - } - }; - - // Setup status text color. - let status_color = match tx.data.tx_type { - TxLogEntryType::ConfirmedCoinbase => Colors::text(false), - TxLogEntryType::TxReceived => if tx.data.confirmed { - Colors::green() - } else { - Colors::text(false) - }, - TxLogEntryType::TxSent => if tx.data.confirmed { - Colors::red() - } else { - Colors::text(false) - }, - TxLogEntryType::TxReceivedCancelled => Colors::inactive_text(), - TxLogEntryType::TxSentCancelled => Colors::inactive_text(), - TxLogEntryType::TxReverted => Colors::inactive_text(), - }; - ui.label(RichText::new(status_text).size(15.0).color(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); - }); - }); - }); - } - - /// Show transaction information [`Modal`]. - fn show_tx_info_modal(&mut self, wallet: &Wallet, tx: &WalletTransaction) { - self.tx_info_response_edit = "".to_string(); - self.tx_info_finalize_edit = "".to_string(); - self.tx_info_finalize_error = false; - self.tx_info_id = Some(tx.data.id); - self.tx_info_show_qr = false; - self.tx_info_slate_id = if let Some(id) = tx.data.tx_slate_id { - Some(id.to_string()) - } else { - None - }; - - // Setup slate and message from transaction. - self.tx_info_response_edit = if !tx.data.confirmed && tx.data.tx_slate_id.is_some() && - (tx.data.tx_type == TxLogEntryType::TxSent || - tx.data.tx_type == TxLogEntryType::TxReceived) { - let mut slate = Slate::blank(1, false); - slate.state = if tx.can_finalize { - if tx.data.tx_type == TxLogEntryType::TxSent { - SlateState::Standard1 - } else { - SlateState::Invoice1 - } - } else { - if tx.data.tx_type == TxLogEntryType::TxReceived { - SlateState::Standard2 - } else { - SlateState::Invoice2 - } - }; - slate.id = tx.data.tx_slate_id.unwrap(); - wallet.read_slatepack(&slate).unwrap_or("".to_string()) - } else { - "".to_string() - }; - - // Show transaction information modal. - Modal::new(TX_INFO_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.tx")) - .show(); - } - - /// Draw transaction info [`Modal`] content. - fn tx_info_modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - // Check values and setup transaction data. - let wallet_data = wallet.get_data(); - if wallet_data.is_none() { - modal.close(); - return; - } - let data = wallet_data.unwrap(); - let tx_id = self.tx_info_id.unwrap(); - let data_txs = data.txs.clone().unwrap(); - let txs = data_txs.into_iter() - .filter(|tx| tx.data.id == tx_id) - .collect::>(); - if txs.is_empty() { - cb.hide_keyboard(); - modal.close(); - return; - } - let tx = txs.get(0).unwrap(); - - if !self.tx_info_show_qr && !self.tx_info_show_scanner { - ui.add_space(6.0); - - // Show transaction amount status and time. - let rounding = View::item_rounding(0, 2, false); - self.tx_item_ui(ui, tx, rounding, false, false, &data, wallet, cb); - - // Show transaction ID info. - if let Some(id) = tx.data.tx_slate_id { - let label = format!("{} {}", HASH_STRAIGHT, t!("id")); - Self::tx_info_modal_item_ui(ui, id.to_string(), label, true, cb); - } - // Show transaction kernel info. - if let Some(kernel) = tx.data.kernel_excess { - let label = format!("{} {}", FILE_ARCHIVE, t!("kernel")); - Self::tx_info_modal_item_ui(ui, kernel.0.to_hex(), label, true, cb); - } - } - - // Show Slatepack message or reset flag to show QR if not available. - if !tx.posting && !tx.data.confirmed && !tx.cancelling && - (tx.data.tx_type == TxLogEntryType::TxSent || - tx.data.tx_type == TxLogEntryType::TxReceived) { - self.tx_info_modal_slate_ui(ui, tx, wallet, modal, cb); - } else if self.tx_info_show_qr { - self.tx_info_qr_code_content.clear_state(); - self.tx_info_show_qr = false; - } - - if !self.tx_info_finalizing { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - if self.tx_info_show_qr { - // Show buttons to close modal or come back to text request content. - ui.columns(2, |cols| { - cols[0].vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.tx_info_qr_code_content.clear_state(); - self.tx_info_show_qr = false; - modal.close(); - }); - }); - cols[1].vertical_centered_justified(|ui| { - View::button(ui, t!("back"), Colors::white_or_black(false), || { - self.tx_info_qr_code_content.clear_state(); - self.tx_info_show_qr = false; - }); - }); - }); - } else if self.tx_info_show_scanner { - ui.add_space(8.0); - // Show buttons to close modal or scanner. - ui.columns(2, |cols| { - cols[0].vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - cb.stop_camera(); - self.tx_info_scanner_content.clear_state(); - self.tx_info_show_scanner = false; - modal.close(); - }); - }); - cols[1].vertical_centered_justified(|ui| { - View::button(ui, t!("back"), Colors::white_or_black(false), || { - cb.stop_camera(); - self.tx_info_scanner_content.clear_state(); - self.tx_info_show_scanner = false; - modal.enable_closing(); - }); - }); - }); - } else { - ui.add_space(8.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); - - // Show button to close modal. - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.tx_info_id = None; - self.tx_info_finalize = false; - cb.hide_keyboard(); - modal.close(); - }); - }); - } - ui.add_space(6.0); - } else { - // Show loader on finalizing. - ui.vertical_centered(|ui| { - View::small_loading_spinner(ui); - ui.add_space(16.0); - }); - // Check finalization result. - let has_res = { - let r_res = self.tx_info_final_result.read(); - r_res.is_some() - }; - if has_res { - let res = { - let r_res = self.tx_info_final_result.read(); - r_res.as_ref().unwrap().clone() - }; - if let Ok(_) = res { - self.tx_info_finalize = false; - self.tx_info_finalize_edit = "".to_string(); - } else { - self.tx_info_finalize_error = true; - } - // Clear status and result. - { - let mut w_res = self.tx_info_final_result.write(); - *w_res = None; - } - self.tx_info_finalizing = false; - modal.enable_closing(); - } - } - } - - /// Draw transaction information [`Modal`] item content. - fn tx_info_modal_item_ui(ui: &mut egui::Ui, - value: String, - label: String, - copy: bool, - cb: &dyn PlatformCallbacks) { - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(50.0); - - // Draw round background. - let bg_rect = rect.clone(); - let mut rounding = View::item_rounding(1, 3, false); - - ui.painter().rect(bg_rect, rounding, Colors::fill(), View::item_stroke()); - - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - // Draw button to copy transaction info value. - if copy { - rounding.nw = 0.0; - rounding.sw = 0.0; - View::item_button(ui, rounding, COPY, None, || { - cb.copy_string_to_buffer(value.clone()); - }); - } - - // Draw value information. - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - ui.add_space(3.0); - View::ellipsize_text(ui, value, 15.0, Colors::title(false)); - ui.label(RichText::new(label).size(15.0).color(Colors::gray())); - ui.add_space(3.0); - }); - }); - }); - } - - /// Draw Slate content to show response or generate payment proof. - fn tx_info_modal_slate_ui(&mut self, - ui: &mut egui::Ui, - tx: &WalletTransaction, - wallet: &Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - if self.tx_info_slate_id.is_none() { - cb.hide_keyboard(); - modal.close(); - return; - } - ui.add_space(6.0); - - // Draw QR code scanner content if requested. - if self.tx_info_show_scanner { - if let Some(result) = self.tx_info_scanner_content.qr_scan_result() { - cb.stop_camera(); - self.tx_info_scanner_content.clear_state(); - - // Setup value to finalization input field. - self.tx_info_finalize_edit = result.text(); - self.on_finalization_input_change(tx, wallet, modal, cb); - - modal.enable_closing(); - self.tx_info_scanner_content.clear_state(); - self.tx_info_show_scanner = false; - } else { - self.tx_info_scanner_content.ui(ui, cb); - } - return; - } - - let amount = amount_to_hr_string(tx.amount, true); - - // Draw Slatepack message description text. - ui.vertical_centered(|ui| { - if self.tx_info_finalize { - let desc_text = if self.tx_info_finalize_error { - t!("wallets.finalize_slatepack_err") - } else { - if tx.data.tx_type == TxLogEntryType::TxSent { - t!("wallets.parse_s2_slatepack_desc", "amount" => amount) - } else { - t!("wallets.parse_i2_slatepack_desc", "amount" => amount) - } - }; - let desc_color = if self.tx_info_finalize_error { - Colors::red() - } else { - Colors::gray() - }; - ui.label(RichText::new(desc_text).size(16.0).color(desc_color)); - } else { - let desc_text = if tx.can_finalize { - if tx.data.tx_type == TxLogEntryType::TxSent { - t!("wallets.send_request_desc", "amount" => amount) - } else { - t!("wallets.invoice_desc", "amount" => amount) - } - } else { - if tx.data.tx_type == TxLogEntryType::TxSent { - t!("wallets.parse_i1_slatepack_desc", "amount" => amount) - } else { - t!("wallets.parse_s1_slatepack_desc", "amount" => amount) - } - }; - ui.label(RichText::new(desc_text).size(16.0).color(Colors::gray())); - } - }); - ui.add_space(6.0); - - // Setup message input value. - let message_edit = if self.tx_info_finalize { - &mut self.tx_info_finalize_edit - } else { - &mut self.tx_info_response_edit - }; - let message_before = message_edit.clone(); - - // Draw QR code content if requested. - if self.tx_info_show_qr { - let text = message_edit.clone(); - if text.is_empty() { - self.tx_info_qr_code_content.clear_state(); - self.tx_info_show_qr = false; - } else { - // Draw QR code content. - self.tx_info_qr_code_content.ui(ui, text.clone(), cb); - return; - } - } - - // Draw Slatepack message finalization input or request text. - ui.vertical_centered(|ui| { - let scroll_id = if self.tx_info_finalize { - Id::from("tx_info_message_finalize") - } else { - Id::from("tx_info_message_request") - }.with(self.tx_info_slate_id.clone().unwrap()).with(tx.data.id); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(3.0); - ScrollArea::vertical() - .id_source(scroll_id) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .max_height(128.0) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(7.0); - let input_id = scroll_id.with("_input"); - let resp = egui::TextEdit::multiline(message_edit) - .id(input_id) - .font(egui::TextStyle::Small) - .desired_rows(5) - .interactive(self.tx_info_finalize && !self.tx_info_finalizing) - .hint_text(SLATEPACK_MESSAGE_HINT) - .desired_width(f32::INFINITY) - .show(ui).response; - // Show soft keyboard on click. - if self.tx_info_finalize && resp.clicked() { - resp.request_focus(); - cb.show_keyboard(); - } - if self.tx_info_finalize && resp.has_focus() { - // Apply text from input on Android as temporary fix for egui. - View::on_soft_input(ui, input_id, message_edit); - } - ui.add_space(6.0); - }); - }); - - ui.add_space(2.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); - - // Do not show buttons on finalization. - if self.tx_info_finalizing { - return; - } - - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - if self.tx_info_finalize { - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - // Draw button to scan Slatepack message QR code. - let qr_text = format!("{} {}", SCAN, t!("scan")); - View::button(ui, qr_text, Colors::button(), || { - cb.hide_keyboard(); - modal.disable_closing(); - cb.start_camera(); - self.tx_info_show_scanner = true; - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Draw button to paste data from clipboard. - let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); - View::button(ui, paste_text, Colors::button(), || { - self.tx_info_finalize_edit = cb.get_string_from_buffer(); - }); - }); - }); - ui.add_space(8.0); - ui.vertical_centered(|ui| { - if self.tx_info_finalize_error { - // Draw button to clear message input. - let clear_text = format!("{} {}", BROOM, t!("clear")); - View::button(ui, clear_text, Colors::button(), || { - self.tx_info_finalize_edit.clear(); - self.tx_info_finalize_error = false; - }); - } else { - // Draw button to choose file. - self.tx_info_file_pick_button.ui(ui, cb, |text| { - self.tx_info_finalize_edit = text; - }); - } - }); - - // Callback on finalization message input change. - if message_before != self.tx_info_finalize_edit { - self.on_finalization_input_change(tx, wallet, modal, cb); - } - } else { - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - // Draw button to show Slatepack message as QR code. - let qr_text = format!("{} {}", QR_CODE, t!("qr_code")); - View::button(ui, qr_text, Colors::button(), || { - cb.hide_keyboard(); - self.tx_info_show_qr = true; - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Draw copy button. - let copy_text = format!("{} {}", COPY, t!("copy")); - View::button(ui, copy_text, Colors::button(), || { - cb.copy_string_to_buffer(self.tx_info_response_edit.clone()); - self.tx_info_finalize_edit = "".to_string(); - if tx.can_finalize { - self.tx_info_finalize = true; - } else { - cb.hide_keyboard(); - modal.close(); - } - }); - }); - }); - } - } - - /// Parse Slatepack message on transaction finalization input change. - fn on_finalization_input_change(&mut self, - tx: &WalletTransaction, - wallet: &Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - let message = &self.tx_info_finalize_edit; - if message.is_empty() { - self.tx_info_finalize_error = false; - } else { - // Parse input message to finalize. - if let Ok(slate) = wallet.parse_slatepack(message) { - let send = slate.state == SlateState::Standard2 && - tx.data.tx_type == TxLogEntryType::TxSent; - let receive = slate.state == SlateState::Invoice2 && - tx.data.tx_type == TxLogEntryType::TxReceived; - if Some(slate.id) == tx.data.tx_slate_id && (send || receive) { - let message = message.clone(); - let wallet = wallet.clone(); - let final_res = self.tx_info_final_result.clone(); - // Finalize transaction at separate thread. - cb.hide_keyboard(); - self.tx_info_finalizing = true; - modal.disable_closing(); - thread::spawn(move || { - let res = wallet.finalize(&message, wallet.can_use_dandelion()); - let mut w_res = final_res.write(); - *w_res = Some(res); - }); - } else { - self.tx_info_finalize_error = true; - } - } else { - self.tx_info_finalize_error = true; - } - } - } - - /// Confirmation [`Modal`] to cancel transaction. - fn cancel_confirmation_modal(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, modal: &Modal) { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - // Setup confirmation text. - 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::>(); - if txs.is_empty() { - modal.close(); - return; - } - let tx = txs.get(0).unwrap(); - let amount = amount_to_hr_string(tx.amount, true); - let text = match tx.data.tx_type { - TxLogEntryType::TxReceived => { - t!("wallets.tx_receive_cancel_conf", "amount" => amount) - }, - _ => { - t!("wallets.tx_send_cancel_conf", "amount" => amount) - } - }; - ui.label(RichText::new(text) - .size(17.0) - .color(Colors::text(false))); - ui.add_space(8.0); - }); - - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - self.confirm_cancel_tx_id = None; - modal.close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, "OK".to_string(), Colors::white_or_black(false), || { - wallet.cancel(self.confirm_cancel_tx_id.unwrap()); - self.confirm_cancel_tx_id = None; - modal.close(); - }); - }); - }); - ui.add_space(6.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 new file mode 100644 index 0000000..1977f9d --- /dev/null +++ b/src/gui/views/wallets/wallet/txs/content.rs @@ -0,0 +1,496 @@ +// Copyright 2023 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::time::{SystemTime, UNIX_EPOCH}; +use egui::{Align, Id, Layout, Margin, Rect, RichText, Rounding, ScrollArea}; +use egui::scroll_area::ScrollBarVisibility; +use grin_core::core::amount_to_hr_string; +use grin_wallet_libwallet::TxLogEntryType; + +use crate::gui::Colors; +use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, ARROW_CLOCKWISE, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, CHECK_CIRCLE, DOTS_THREE_CIRCLE, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE}; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{Modal, PullToRefresh, Content, View}; +use crate::gui::views::types::ModalPosition; +use crate::gui::views::wallets::types::WalletTab; +use crate::gui::views::wallets::wallet::types::{GRIN, WalletTabType}; +use crate::gui::views::wallets::wallet::{WalletContent, WalletTransactionModal}; +use crate::wallet::types::{WalletData, WalletTransaction}; +use crate::wallet::Wallet; + +/// Wallet transactions tab content. +pub struct WalletTransactions { + /// Transaction information [`Modal`] content. + tx_info_content: Option, + + /// Transaction identifier to use at confirmation [`Modal`]. + confirm_cancel_tx_id: Option, + + /// Flag to check if sync of wallet was initiated manually at time. + manual_sync: Option +} + +impl Default for WalletTransactions { + fn default() -> Self { + Self { + tx_info_content: None, + confirm_cancel_tx_id: None, + manual_sync: None, + } + } +} + +impl WalletTab for WalletTransactions { + fn get_type(&self) -> WalletTabType { + WalletTabType::Txs + } + + fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { + if WalletContent::sync_ui(ui, wallet) { + return; + } + + // Show modal content for this ui container. + self.modal_content_ui(ui, wallet, cb); + + // Show wallet transactions content. + egui::CentralPanel::default() + .frame(egui::Frame { + stroke: View::item_stroke(), + fill: Colors::button(), + inner_margin: Margin { + left: View::far_left_inset_margin(ui) + 4.0, + right: View::get_right_inset() + 4.0, + top: 0.0, + bottom: 4.0, + }, + ..Default::default() + }) + .show_inside(ui, |ui| { + ui.vertical_centered(|ui| { + let data = wallet.get_data().unwrap(); + self.txs_ui(ui, wallet, &data, cb); + }); + }); + } +} + +/// 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"; + + + +impl WalletTransactions { + /// Height of transaction list item. + pub const TX_ITEM_HEIGHT: f32 = 76.0; + + /// Draw transactions content. + fn txs_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + data: &WalletData, + cb: &dyn PlatformCallbacks) { + let amount_conf = data.info.amount_awaiting_confirmation; + let amount_fin = data.info.amount_awaiting_finalization; + let amount_locked = data.info.amount_locked; + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + // Show non-zero awaiting confirmation amount. + if amount_conf != 0 { + let awaiting_conf = amount_to_hr_string(amount_conf, true); + let rounding = if amount_fin != 0 || amount_locked != 0 { + [false, false, false, false] + } else { + [false, false, true, true] + }; + View::rounded_box(ui, + format!("{} ツ", awaiting_conf), + t!("wallets.await_conf_amount"), + rounding); + } + // Show non-zero awaiting finalization amount. + if amount_fin != 0 { + let awaiting_conf = amount_to_hr_string(amount_fin, true); + let rounding = if amount_locked != 0 { + [false, false, false, false] + } else { + [false, false, true, true] + }; + View::rounded_box(ui, + format!("{} ツ", awaiting_conf), + t!("wallets.await_fin_amount"), + rounding); + } + // Show non-zero locked amount. + if amount_locked != 0 { + let awaiting_conf = amount_to_hr_string(amount_locked, true); + View::rounded_box(ui, + format!("{} ツ", awaiting_conf), + t!("wallets.locked_amount"), + [false, false, true, true]); + } + + // Show message when txs are empty. + if let Some(txs) = data.txs.as_ref() { + if txs.is_empty() { + View::center_content(ui, 96.0, |ui| { + let empty_text = t!( + "wallets.txs_empty", + "message" => CHAT_CIRCLE_TEXT, + "transport" => BRIDGE, + "settings" => GEAR_FINE + ); + ui.label(RichText::new(empty_text).size(16.0).color(Colors::inactive_text())); + }); + return; + } + } + }); + + // Show loader when txs are not loaded. + if data.txs.is_none() { + ui.centered_and_justified(|ui| { + View::big_loading_spinner(ui); + }); + return; + } + + ui.add_space(4.0); + + // Show list of transactions. + let txs = data.txs.as_ref().unwrap(); + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(); + let refresh = self.manual_sync.unwrap_or(0) + 1600 > now; + let refresh_resp = PullToRefresh::new(refresh) + .can_refresh(!refresh && !wallet.syncing()) + .min_refresh_distance(70.0) + .scroll_area_ui(ui, |ui| { + ScrollArea::vertical() + .id_source(Id::from("txs_content").with(wallet.get_config().id)) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) + .show_rows(ui, Self::TX_ITEM_HEIGHT, txs.len(), |ui, row_range| { + ui.add_space(1.0); + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + let padding = amount_conf != 0 || amount_fin != 0 || amount_locked != 0; + for index in row_range { + let tx = txs.get(index).unwrap(); + let mut r = View::item_rounding(index, txs.len(), false); + let mut rect = ui.available_rect_before_wrap(); + if padding { + 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); + Self::tx_item_ui(ui, tx, rect, r, &data, |ui| { + // Draw button to show transaction info. + if tx.data.tx_slate_id.is_some() { + r.nw = 0.0; + r.sw = 0.0; + View::item_button(ui, r, FILE_TEXT, None, || { + self.show_tx_info_modal(wallet, tx, false); + }); + } + + // Draw button to show transaction finalization. + if tx.can_finalize { + let (icon, color) = (CHECK, Some(Colors::green())); + View::item_button(ui, Rounding::default(), icon, color, || { + cb.hide_keyboard(); + self.show_tx_info_modal(wallet, tx, true); + }); + } + + // Draw button to cancel transaction. + let wallet_loaded = wallet.foreign_api_port().is_some(); + if wallet_loaded && tx.can_cancel() { + let (icon, color) = (PROHIBIT, Some(Colors::red())); + View::item_button(ui, Rounding::default(), icon, color, || { + self.confirm_cancel_tx_id = Some(tx.data.id); + // Show transaction cancellation confirmation modal. + Modal::new(CANCEL_TX_CONFIRMATION_MODAL) + .position(ModalPosition::Center) + .title(t!("modal.confirmation")) + .show(); + }); + } + + // Draw button to repost transaction. + if tx.can_repost(data) { + let r = Rounding::default(); + let (icon, color) = (ARROW_CLOCKWISE, Colors::green()); + View::item_button(ui, r, icon, Some(color), || { + cb.hide_keyboard(); + // Post tx after getting slate from slatepack file. + if let Some((s, _)) = wallet.read_slate_by_tx(tx) { + let _ = wallet.post(&s, wallet.can_use_dandelion()); + } + }); + } + }); + } + }); + }) + }); + + // Sync wallet on refresh. + if refresh_resp.should_refresh() { + self.manual_sync = Some(now); + if !wallet.syncing() { + wallet.sync(true); + } + } + } + + /// Draw [`Modal`] content for this ui container. + fn modal_content_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + cb: &dyn PlatformCallbacks) { + match Modal::opened() { + None => {} + Some(id) => { + match id { + TX_INFO_MODAL => { + Modal::ui(ui.ctx(), |ui, modal| { + if let Some(content) = self.tx_info_content.as_mut() { + content.ui(ui, wallet, modal, cb); + } + }); + } + CANCEL_TX_CONFIRMATION_MODAL => { + Modal::ui(ui.ctx(), |ui, modal| { + self.cancel_confirmation_modal(ui, wallet, modal); + }); + } + _ => {} + } + } + } + } + + /// Draw transaction item. + pub fn tx_item_ui(ui: &mut egui::Ui, + tx: &WalletTransaction, + rect: Rect, + rounding: Rounding, + data: &WalletData, + buttons_ui: impl FnOnce(&mut egui::Ui)) { + // Draw round background. + let bg_rect = rect.clone(); + ui.painter().rect(bg_rect, rounding, Colors::TRANSPARENT, View::item_stroke()); + + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |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); + + // 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 transaction status text. + 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 tx.posting { + format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_finalizing")) + } else { + if tx.cancelling { + format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_cancelling")) + } else { + match tx.data.tx_type { + TxLogEntryType::TxReceived => { + format!("{} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_receiving")) + }, + TxLogEntryType::TxSent => { + format!("{} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_sending")) + }, + _ => { + format!("{} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_confirmed")) + } + } + } + } + } else { + match tx.data.tx_type { + TxLogEntryType::ConfirmedCoinbase => { + format!("{} {}", CHECK_CIRCLE, t!("wallets.tx_confirmed")) + }, + TxLogEntryType::TxSent | TxLogEntryType::TxReceived => { + let height = data.info.last_confirmed_height; + let min_conf = data.info.minimum_confirmations; + if tx.conf_height.is_none() || (tx.conf_height.unwrap() != 0 && + height - tx.conf_height.unwrap() > min_conf - 1) { + let (i, t) = if tx.data.tx_type == TxLogEntryType::TxSent { + (ARROW_CIRCLE_UP, t!("wallets.tx_sent")) + } else { + (ARROW_CIRCLE_DOWN, t!("wallets.tx_received")) + }; + format!("{} {}", i, t) + } else { + let tx_height = tx.conf_height.unwrap() - 1; + let left_conf = height - tx_height; + let conf_info = if tx_height != 0 && height >= tx_height && + left_conf < min_conf { + format!("{}/{}", left_conf, min_conf) + } else { + "".to_string() + }; + format!("{} {} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_confirming"), + conf_info + ) + } + }, + _ => format!("{} {}", X_CIRCLE, t!("wallets.canceled")) + } + }; + + // Setup status text color. + let status_color = match tx.data.tx_type { + TxLogEntryType::ConfirmedCoinbase => Colors::text(false), + TxLogEntryType::TxReceived => if tx.data.confirmed { + Colors::green() + } else { + Colors::text(false) + }, + TxLogEntryType::TxSent => if tx.data.confirmed { + Colors::red() + } else { + Colors::text(false) + }, + TxLogEntryType::TxReceivedCancelled => Colors::inactive_text(), + TxLogEntryType::TxSentCancelled => Colors::inactive_text(), + TxLogEntryType::TxReverted => Colors::inactive_text(), + }; + ui.label(RichText::new(status_text).size(15.0).color(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); + }); + }); + }); + } + + /// Show transaction information [`Modal`]. + fn show_tx_info_modal(&mut self, wallet: &Wallet, tx: &WalletTransaction, finalize: bool) { + let mut modal = WalletTransactionModal::new(wallet, tx); + modal.show_finalization = finalize; + self.tx_info_content = Some(modal); + Modal::new(TX_INFO_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.tx")) + .show(); + } + + /// Confirmation [`Modal`] to cancel transaction. + fn cancel_confirmation_modal(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, modal: &Modal) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + // Setup confirmation text. + 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::>(); + if txs.is_empty() { + modal.close(); + return; + } + let tx = txs.get(0).unwrap(); + let amount = amount_to_hr_string(tx.amount, true); + let text = match tx.data.tx_type { + TxLogEntryType::TxReceived => { + t!("wallets.tx_receive_cancel_conf", "amount" => amount) + }, + _ => { + t!("wallets.tx_send_cancel_conf", "amount" => amount) + } + }; + ui.label(RichText::new(text) + .size(17.0) + .color(Colors::text(false))); + ui.add_space(8.0); + }); + + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + self.confirm_cancel_tx_id = None; + modal.close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, "OK".to_string(), Colors::white_or_black(false), || { + wallet.cancel(self.confirm_cancel_tx_id.unwrap()); + self.confirm_cancel_tx_id = None; + modal.close(); + }); + }); + }); + ui.add_space(6.0); + }); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/txs/mod.rs b/src/gui/views/wallets/wallet/txs/mod.rs new file mode 100644 index 0000000..e283c72 --- /dev/null +++ b/src/gui/views/wallets/wallet/txs/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2024 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod content; +pub use content::*; + +mod tx; +pub use tx::*; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/txs/tx.rs b/src/gui/views/wallets/wallet/txs/tx.rs new file mode 100644 index 0000000..f6ec4e6 --- /dev/null +++ b/src/gui/views/wallets/wallet/txs/tx.rs @@ -0,0 +1,614 @@ +// Copyright 2024 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; +use std::thread; +use egui::{Align, Id, Layout, RichText, Rounding, ScrollArea}; +use egui::scroll_area::ScrollBarVisibility; +use grin_core::core::amount_to_hr_string; +use grin_util::ToHex; +use grin_wallet_libwallet::{Error, Slate, SlateState, TxLogEntryType}; +use parking_lot::RwLock; +use crate::gui::Colors; +use crate::gui::icons::{ARROW_CLOCKWISE, BROOM, CHECK, CLIPBOARD_TEXT, COPY, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN}; +use crate::gui::platform::PlatformCallbacks; + +use crate::gui::views::{CameraContent, FilePickButton, Modal, QrCodeContent, View}; +use crate::gui::views::wallets::wallet::txs::WalletTransactions; +use crate::gui::views::wallets::wallet::types::SLATEPACK_MESSAGE_HINT; +use crate::wallet::types::WalletTransaction; +use crate::wallet::Wallet; + +/// Transaction information [`Modal`] content. +pub struct WalletTransactionModal { + /// Transaction identifier. + tx_id: u32, + /// Identifier for [`Slate`]. + slate_id: Option, + + /// Response Slatepack message input value. + response_edit: String, + + /// Flag to show transaction finalization input. + pub show_finalization: bool, + /// Finalization Slatepack message input value. + finalize_edit: String, + /// Flag to check if error happened during transaction finalization. + finalize_error: bool, + /// Flag to check if transaction is finalizing. + finalizing: bool, + /// Transaction finalization result. + final_result: Arc>>>, + + /// Flag to check if QR code is showing. + show_qr: bool, + /// QR code Slatepack message image content. + qr_code_content: QrCodeContent, + + /// Flag to check if QR code scanner is showing. + show_scanner: bool, + /// QR code scanner content. + scanner_content: CameraContent, + + /// Button to parse picked file content. + file_pick_button: FilePickButton, +} + +impl WalletTransactionModal { + /// Create new content instance with [`Wallet`] from provided [`WalletTransaction`]. + pub fn new(wallet: &Wallet, tx: &WalletTransaction) -> Self { + Self { + tx_id: tx.data.id, + slate_id: match tx.data.tx_slate_id { + None => None, + Some(id) => Some(id.to_string()) + }, + response_edit: if !tx.data.confirmed && tx.data.tx_slate_id.is_some() && + (tx.data.tx_type == TxLogEntryType::TxSent || + tx.data.tx_type == TxLogEntryType::TxReceived) { + let mut slate = Slate::blank(1, false); + slate.state = if tx.can_finalize { + if tx.data.tx_type == TxLogEntryType::TxSent { + SlateState::Standard1 + } else { + SlateState::Invoice1 + } + } else { + if tx.data.tx_type == TxLogEntryType::TxReceived { + SlateState::Standard2 + } else { + SlateState::Invoice2 + } + }; + slate.id = tx.data.tx_slate_id.unwrap(); + wallet.read_slatepack(&slate).unwrap_or("".to_string()) + } else { + "".to_string() + }, + finalize_edit: "".to_string(), + finalize_error: false, + show_finalization: false, + finalizing: false, + final_result: Arc::new(RwLock::new(None)), + show_qr: false, + qr_code_content: QrCodeContent::new("".to_string(), true), + show_scanner: false, + scanner_content: CameraContent::default(), + file_pick_button: FilePickButton::default(), + } + } + + /// Draw [`Modal`] content. + pub fn ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + // Check values and setup transaction data. + let wallet_data = wallet.get_data(); + if wallet_data.is_none() { + modal.close(); + return; + } + let data = wallet_data.unwrap(); + let data_txs = data.txs.clone().unwrap(); + let txs = data_txs.into_iter() + .filter(|tx| tx.data.id == self.tx_id) + .collect::>(); + if txs.is_empty() { + cb.hide_keyboard(); + modal.close(); + return; + } + let tx = txs.get(0).unwrap(); + + if !self.show_qr && !self.show_scanner { + ui.add_space(6.0); + + // Show transaction amount status and time. + let r = View::item_rounding(0, 2, false); + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(WalletTransactions::TX_ITEM_HEIGHT); + WalletTransactions::tx_item_ui(ui, tx, rect, r, &data, |ui| { + // Do not show buttons on finalizing. + if self.finalizing { + return; + } + + // Draw button to show transaction finalization or transaction info. + if tx.can_finalize { + let (icon, color) = if self.show_finalization { + (FILE_TEXT, None) + } else { + (CHECK, Some(Colors::green())) + }; + let mut r = r.clone(); + r.nw = 0.0; + r.sw = 0.0; + View::item_button(ui, r, icon, color, || { + cb.hide_keyboard(); + if self.show_finalization { + self.show_finalization = false; + return; + } + self.show_finalization = true; + }); + } + + // Draw button to cancel transaction. + let wallet_loaded = wallet.foreign_api_port().is_some(); + if wallet_loaded && tx.can_cancel() { + View::item_button(ui, Rounding::default(), PROHIBIT, Some(Colors::red()), || { + cb.hide_keyboard(); + wallet.cancel(tx.data.id); + }); + } + + // Draw button to repost transaction. + if wallet_loaded && tx.can_repost(&data) { + let r = if self.show_finalization { + Rounding::default() + } else { + let mut r = r.clone(); + r.nw = 0.0; + r.sw = 0.0; + r + }; + View::item_button(ui, r, ARROW_CLOCKWISE, Some(Colors::green()), || { + cb.hide_keyboard(); + // Post tx after getting slate from slatepack file. + if let Some((s, _)) = wallet.read_slate_by_tx(tx) { + let _ = wallet.post(&s, wallet.can_use_dandelion()); + } + }); + } + }); + + // Show transaction ID info. + if let Some(id) = tx.data.tx_slate_id { + let label = format!("{} {}", HASH_STRAIGHT, t!("id")); + Self::info_item_ui(ui, id.to_string(), label, true, cb); + } + // Show transaction kernel info. + if let Some(kernel) = tx.data.kernel_excess { + let label = format!("{} {}", FILE_ARCHIVE, t!("kernel")); + Self::info_item_ui(ui, kernel.0.to_hex(), label, true, cb); + } + } + + // Show Slatepack message or reset flag to show QR if not available. + if !tx.posting && !tx.data.confirmed && !tx.cancelling && + (tx.data.tx_type == TxLogEntryType::TxSent || + tx.data.tx_type == TxLogEntryType::TxReceived) { + self.message_ui(ui, tx, wallet, modal, cb); + } else if self.show_qr { + self.qr_code_content.clear_state(); + self.show_qr = false; + } + + if !self.finalizing { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + if self.show_qr { + // Show buttons to close modal or come back to text request content. + ui.columns(2, |cols| { + cols[0].vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + self.qr_code_content.clear_state(); + self.show_qr = false; + modal.close(); + }); + }); + cols[1].vertical_centered_justified(|ui| { + View::button(ui, t!("back"), Colors::white_or_black(false), || { + self.qr_code_content.clear_state(); + self.show_qr = false; + }); + }); + }); + } else if self.show_scanner { + ui.add_space(8.0); + // Show buttons to close modal or scanner. + ui.columns(2, |cols| { + cols[0].vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + cb.stop_camera(); + self.scanner_content.clear_state(); + self.show_scanner = false; + modal.close(); + }); + }); + cols[1].vertical_centered_justified(|ui| { + View::button(ui, t!("back"), Colors::white_or_black(false), || { + cb.stop_camera(); + self.scanner_content.clear_state(); + self.show_scanner = false; + modal.enable_closing(); + }); + }); + }); + } else { + ui.add_space(8.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); + + // Show button to close modal. + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + cb.hide_keyboard(); + modal.close(); + }); + }); + } + ui.add_space(6.0); + } else { + // Show loader on finalizing. + ui.vertical_centered(|ui| { + View::small_loading_spinner(ui); + ui.add_space(16.0); + }); + // Check finalization result. + let has_res = { + let r_res = self.final_result.read(); + r_res.is_some() + }; + if has_res { + let res = { + let r_res = self.final_result.read(); + r_res.as_ref().unwrap().clone() + }; + if let Ok(_) = res { + self.show_finalization = false; + self.finalize_edit = "".to_string(); + } else { + self.finalize_error = true; + } + // Clear status and result. + { + let mut w_res = self.final_result.write(); + *w_res = None; + } + self.finalizing = false; + modal.enable_closing(); + } + } + } + + /// Draw transaction information item content. + fn info_item_ui(ui: &mut egui::Ui, + value: String, + label: String, + copy: bool, + cb: &dyn PlatformCallbacks) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(50.0); + + // Draw round background. + let bg_rect = rect.clone(); + let mut rounding = View::item_rounding(1, 3, false); + + ui.painter().rect(bg_rect, rounding, Colors::fill(), View::item_stroke()); + + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + // Draw button to copy transaction info value. + if copy { + rounding.nw = 0.0; + rounding.sw = 0.0; + View::item_button(ui, rounding, COPY, None, || { + cb.copy_string_to_buffer(value.clone()); + }); + } + + // Draw value information. + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(3.0); + View::ellipsize_text(ui, value, 15.0, Colors::title(false)); + ui.label(RichText::new(label).size(15.0).color(Colors::gray())); + ui.add_space(3.0); + }); + }); + }); + } + + /// Draw Slatepack message content. + fn message_ui(&mut self, + ui: &mut egui::Ui, + tx: &WalletTransaction, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + if self.slate_id.is_none() { + cb.hide_keyboard(); + modal.close(); + return; + } + ui.add_space(6.0); + + // Draw QR code scanner content if requested. + if self.show_scanner { + if let Some(result) = self.scanner_content.qr_scan_result() { + cb.stop_camera(); + self.scanner_content.clear_state(); + + // Setup value to finalization input field. + self.finalize_edit = result.text(); + self.on_finalization_input_change(tx, wallet, modal, cb); + + modal.enable_closing(); + self.scanner_content.clear_state(); + self.show_scanner = false; + } else { + self.scanner_content.ui(ui, cb); + } + return; + } + + let amount = amount_to_hr_string(tx.amount, true); + + // Draw Slatepack message description text. + ui.vertical_centered(|ui| { + if self.show_finalization { + let desc_text = if self.finalize_error { + t!("wallets.finalize_slatepack_err") + } else { + if tx.data.tx_type == TxLogEntryType::TxSent { + t!("wallets.parse_s2_slatepack_desc", "amount" => amount) + } else { + t!("wallets.parse_i2_slatepack_desc", "amount" => amount) + } + }; + let desc_color = if self.finalize_error { + Colors::red() + } else { + Colors::gray() + }; + ui.label(RichText::new(desc_text).size(16.0).color(desc_color)); + } else { + let desc_text = if tx.can_finalize { + if tx.data.tx_type == TxLogEntryType::TxSent { + t!("wallets.send_request_desc", "amount" => amount) + } else { + t!("wallets.invoice_desc", "amount" => amount) + } + } else { + if tx.data.tx_type == TxLogEntryType::TxSent { + t!("wallets.parse_i1_slatepack_desc", "amount" => amount) + } else { + t!("wallets.parse_s1_slatepack_desc", "amount" => amount) + } + }; + ui.label(RichText::new(desc_text).size(16.0).color(Colors::gray())); + } + }); + ui.add_space(6.0); + + // Setup message input value. + let message_edit = if self.show_finalization { + &mut self.finalize_edit + } else { + &mut self.response_edit + }; + let message_before = message_edit.clone(); + + // Draw QR code content if requested. + if self.show_qr { + let text = message_edit.clone(); + if text.is_empty() { + self.qr_code_content.clear_state(); + self.show_qr = false; + } else { + // Draw QR code content. + self.qr_code_content.ui(ui, text.clone(), cb); + return; + } + } + + // Draw Slatepack message finalization input or request text. + ui.vertical_centered(|ui| { + let scroll_id = if self.show_finalization { + Id::from("tx_info_message_finalize") + } else { + Id::from("tx_info_message_request") + }.with(self.slate_id.clone().unwrap()).with(tx.data.id); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(3.0); + ScrollArea::vertical() + .id_source(scroll_id) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(128.0) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + let input_id = scroll_id.with("_input"); + let resp = egui::TextEdit::multiline(message_edit) + .id(input_id) + .font(egui::TextStyle::Small) + .desired_rows(5) + .interactive(self.show_finalization && !self.finalizing) + .hint_text(SLATEPACK_MESSAGE_HINT) + .desired_width(f32::INFINITY) + .show(ui).response; + // Show soft keyboard on click. + if self.show_finalization && resp.clicked() { + resp.request_focus(); + cb.show_keyboard(); + } + if self.show_finalization && resp.has_focus() { + // Apply text from input on Android as temporary fix for egui. + View::on_soft_input(ui, input_id, message_edit); + } + ui.add_space(6.0); + }); + }); + + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); + + // Do not show buttons on finalization. + if self.finalizing { + return; + } + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + if self.show_finalization { + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + // Draw button to scan Slatepack message QR code. + let qr_text = format!("{} {}", SCAN, t!("scan")); + View::button(ui, qr_text, Colors::button(), || { + cb.hide_keyboard(); + modal.disable_closing(); + cb.start_camera(); + self.show_scanner = true; + }); + }); + columns[1].vertical_centered_justified(|ui| { + // Draw button to paste data from clipboard. + let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); + View::button(ui, paste_text, Colors::button(), || { + self.finalize_edit = cb.get_string_from_buffer(); + }); + }); + }); + ui.add_space(8.0); + ui.vertical_centered(|ui| { + if self.finalize_error { + // Draw button to clear message input. + let clear_text = format!("{} {}", BROOM, t!("clear")); + View::button(ui, clear_text, Colors::button(), || { + self.finalize_edit.clear(); + self.finalize_error = false; + }); + } else { + // Draw button to choose file. + self.file_pick_button.ui(ui, cb, |text| { + self.finalize_edit = text; + }); + } + }); + + // Callback on finalization message input change. + if message_before != self.finalize_edit { + self.on_finalization_input_change(tx, wallet, modal, cb); + } + } else { + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + // Draw button to show Slatepack message as QR code. + let qr_text = format!("{} {}", QR_CODE, t!("qr_code")); + View::button(ui, qr_text, Colors::button(), || { + cb.hide_keyboard(); + self.show_qr = true; + }); + }); + columns[1].vertical_centered_justified(|ui| { + // Draw copy button. + let copy_text = format!("{} {}", COPY, t!("copy")); + View::button(ui, copy_text, Colors::button(), || { + cb.copy_string_to_buffer(self.response_edit.clone()); + self.finalize_edit = "".to_string(); + if tx.can_finalize { + self.show_finalization = true; + } else { + cb.hide_keyboard(); + modal.close(); + } + }); + }); + }); + + // Show button to share response as file. + ui.add_space(8.0); + ui.vertical_centered(|ui| { + let share_text = format!("{} {}", FILE_TEXT, t!("share")); + View::colored_text_button(ui, + share_text, + Colors::blue(), + Colors::white_or_black(false), || { + if let Some((s, _)) = wallet.read_slate_by_tx(tx) { + let name = format!("{}.{}.slatepack", s.id, s.state); + let data = self.response_edit.as_bytes().to_vec(); + cb.share_data(name, data).unwrap_or_default(); + } + }); + }); + } + } + + /// Parse Slatepack message on transaction finalization input change. + fn on_finalization_input_change(&mut self, + tx: &WalletTransaction, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + let message = &self.finalize_edit; + if message.is_empty() { + self.finalize_error = false; + } else { + // Parse input message to finalize. + if let Ok(slate) = wallet.parse_slatepack(message) { + let send = slate.state == SlateState::Standard2 && + tx.data.tx_type == TxLogEntryType::TxSent; + let receive = slate.state == SlateState::Invoice2 && + tx.data.tx_type == TxLogEntryType::TxReceived; + if Some(slate.id) == tx.data.tx_slate_id && (send || receive) { + let message = message.clone(); + let wallet = wallet.clone(); + let final_res = self.final_result.clone(); + // Finalize transaction at separate thread. + cb.hide_keyboard(); + self.finalizing = true; + modal.disable_closing(); + thread::spawn(move || { + let res = wallet.finalize(&message, wallet.can_use_dandelion()); + let mut w_res = final_res.write(); + *w_res = Some(res); + }); + } else { + self.finalize_error = true; + } + } else { + self.finalize_error = true; + } + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index f50480e..0ff285d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,9 +49,12 @@ fn real_main() { })); // Start GUI. - let _ = std::panic::catch_unwind(|| { + match std::panic::catch_unwind(|| { start_desktop_gui(); - }); + }) { + Ok(_) => {} + Err(e) => println!("{:?}", e) + } } /// Start GUI with Desktop related setup. From 21ecf200b8b3e0668509960779b2bd1ddf664c60 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 7 Sep 2024 00:11:17 +0300 Subject: [PATCH 02/28] wallet + ui: optimize sync after tx actions, remove tx repost, share message as file from tx modal, show tx info after tor sending and message creation or finalization, messages and transport modules refactoring, qr code text optimization, wallet dandelion setting, recovery phrase modal next step on enter --- src/gui/views/qr.rs | 62 +- src/gui/views/wallets/wallet/content.rs | 14 +- src/gui/views/wallets/wallet/messages.rs | 1166 ----------------- .../views/wallets/wallet/messages/content.rs | 541 ++++++++ src/gui/views/wallets/wallet/messages/mod.rs | 18 + .../views/wallets/wallet/messages/request.rs | 260 ++++ .../views/wallets/wallet/settings/common.rs | 25 +- .../views/wallets/wallet/settings/recovery.rs | 8 +- src/gui/views/wallets/wallet/transport.rs | 944 ------------- .../views/wallets/wallet/transport/content.rs | 397 ++++++ src/gui/views/wallets/wallet/transport/mod.rs | 19 + .../views/wallets/wallet/transport/send.rs | 357 +++++ .../wallets/wallet/transport/settings.rs | 258 ++++ src/gui/views/wallets/wallet/txs/content.rs | 22 +- src/gui/views/wallets/wallet/txs/tx.rs | 105 +- src/wallet/types.rs | 16 +- src/wallet/wallet.rs | 277 ++-- 17 files changed, 2051 insertions(+), 2438 deletions(-) delete mode 100644 src/gui/views/wallets/wallet/messages.rs create mode 100644 src/gui/views/wallets/wallet/messages/content.rs create mode 100644 src/gui/views/wallets/wallet/messages/mod.rs create mode 100644 src/gui/views/wallets/wallet/messages/request.rs delete mode 100644 src/gui/views/wallets/wallet/transport.rs create mode 100644 src/gui/views/wallets/wallet/transport/content.rs create mode 100644 src/gui/views/wallets/wallet/transport/mod.rs create mode 100644 src/gui/views/wallets/wallet/transport/send.rs create mode 100644 src/gui/views/wallets/wallet/transport/settings.rs diff --git a/src/gui/views/qr.rs b/src/gui/views/qr.rs index e0690ff..86599cf 100644 --- a/src/gui/views/qr.rs +++ b/src/gui/views/qr.rs @@ -30,8 +30,8 @@ use crate::gui::views::View; /// QR code image from text. pub struct QrCodeContent { - /// Text to create QR code. - pub(crate) text: String, + /// QR code text. + text: String, /// Flag to draw animated QR with Uniform Resources /// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md @@ -62,18 +62,18 @@ impl QrCodeContent { } /// Draw QR code. - pub fn ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { + pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { if self.animated { // Show animated QR code. - self.animated_ui(ui, text, cb); + self.animated_ui(ui, cb); } else { // Show static QR code. - self.static_ui(ui, text, cb); + self.static_ui(ui, cb); } } /// Draw animated QR code content. - fn animated_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { + fn animated_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { if !self.has_image() { let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; ui.vertical_centered(|ui| { @@ -84,7 +84,7 @@ impl QrCodeContent { // Create multiple vector images from text if not creating. if !self.loading() { - self.create_svg_list(text); + self.create_svg_list(); } } else { let svg_list = { @@ -111,7 +111,7 @@ impl QrCodeContent { // Show QR code text. ui.add_space(6.0); - View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text()); + View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text()); ui.add_space(6.0); ui.vertical_centered(|ui| { @@ -131,7 +131,7 @@ impl QrCodeContent { w_state.exporting = true; } // Create GIF to export. - self.create_qr_gif(text, DEFAULT_QR_SIZE as usize); + self.create_qr_gif(); }); } else { ui.vertical_centered(|ui| { @@ -171,7 +171,7 @@ impl QrCodeContent { } /// Draw static QR code content. - fn static_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { + fn static_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { if !self.has_image() { let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; ui.vertical_centered(|ui| { @@ -182,7 +182,7 @@ impl QrCodeContent { // Create vector image from text if not creating. if !self.loading() { - self.create_svg(text); + self.create_svg(); } } else { // Create image from SVG data. @@ -194,7 +194,7 @@ impl QrCodeContent { // Show QR code text. ui.add_space(6.0); - View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text()); + View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text()); ui.add_space(6.0); // Show button to share QR. @@ -204,21 +204,22 @@ impl QrCodeContent { share_text, Colors::blue(), Colors::white_or_black(false), || { - if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) { - if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) { - let mut png = vec![]; - let png_enc = PngEncoder::new_with_quality(&mut png, - CompressionType::Best, - FilterType::NoFilter); - if let Ok(()) = png_enc.write_image(data.as_slice(), - DEFAULT_QR_SIZE, - DEFAULT_QR_SIZE, - ExtendedColorType::L8) { - let name = format!("{}.png", chrono::Utc::now().timestamp()); - cb.share_data(name, png).unwrap_or_default(); + let text = self.text.as_str(); + if let Ok(qr) = QrCode::encode_text(text, qrcodegen::QrCodeEcc::Low) { + if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) { + let mut png = vec![]; + let png_enc = PngEncoder::new_with_quality(&mut png, + CompressionType::Best, + FilterType::NoFilter); + if let Ok(()) = png_enc.write_image(data.as_slice(), + DEFAULT_QR_SIZE, + DEFAULT_QR_SIZE, + ExtendedColorType::L8) { + let name = format!("{}.png", chrono::Utc::now().timestamp()); + cb.share_data(name, png).unwrap_or_default(); + } } } - } }); }); ui.add_space(8.0); @@ -267,8 +268,9 @@ impl QrCodeContent { } /// Create multiple vector QR code images at separate thread. - fn create_svg_list(&self, text: String) { + fn create_svg_list(&self) { let qr_state = self.qr_image_state.clone(); + let text = self.text.clone(); thread::spawn(move || { let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap(); let mut data = Vec::with_capacity(encoder.fragment_count()); @@ -294,8 +296,9 @@ impl QrCodeContent { } /// Create vector QR code image at separate thread. - fn create_svg(&self, text: String) { + fn create_svg(&self) { let qr_state = self.qr_image_state.clone(); + let text = self.text.clone(); thread::spawn(move || { if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) { let svg = Self::qr_to_svg(qr, 0); @@ -332,13 +335,14 @@ impl QrCodeContent { } /// Create GIF image at separate thread. - fn create_qr_gif(&self, text: String, size: usize) { + fn create_qr_gif(&self) { { let mut w_state = self.qr_image_state.write(); w_state.gif_creating = true; } let qr_state = self.qr_image_state.clone(); + let text = self.text.clone(); thread::spawn(move || { // Setup GIF image encoder. let mut gif = vec![]; @@ -354,7 +358,7 @@ impl QrCodeContent { ) { // Create an image from QR data. let image = qr.render() - .max_dimensions(size as u32, size as u32) + .max_dimensions(DEFAULT_QR_SIZE, DEFAULT_QR_SIZE) .dark_color(image::Rgb([0, 0, 0])) .light_color(image::Rgb([255, 255, 255])) .build(); diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index edc8134..3cd5061 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -148,7 +148,7 @@ impl WalletContent { ui.vertical_centered(|ui| { // Draw wallet tabs. View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - self.tabs_ui(ui, wallet); + self.tabs_ui(ui); }); }); }); @@ -468,7 +468,7 @@ impl WalletContent { QrScanResult::Slatepack(message) => { // Redirect to messages to handle parsed message. let mut messages = - WalletMessages::new(wallet.can_use_dandelion(), Some(message.to_string())); + WalletMessages::new(Some(message.to_string())); messages.parse_message(wallet); modal.close(); self.current_tab = Box::new(messages); @@ -477,8 +477,7 @@ impl WalletContent { QrScanResult::Address(receiver) => { if wallet.get_data().unwrap().info.amount_currently_spendable > 0 { // Redirect to send amount with Tor. - let addr = wallet.slatepack_address().unwrap(); - let mut transport = WalletTransport::new(addr.clone()); + let mut transport = WalletTransport::default(); modal.close(); transport.show_send_tor_modal(cb, Some(receiver.to_string())); self.current_tab = Box::new(transport); @@ -528,7 +527,7 @@ impl WalletContent { } /// Draw tab buttons in the bottom of the screen. - fn tabs_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { + fn tabs_ui(&mut self, ui: &mut egui::Ui) { ui.scope(|ui| { // Setup spacing between tabs. ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); @@ -547,14 +546,13 @@ impl WalletContent { let is_messages = current_type == WalletTabType::Messages; View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, || { self.current_tab = Box::new( - WalletMessages::new(wallet.can_use_dandelion(), None) + WalletMessages::new(None) ); }); }); columns[2].vertical_centered_justified(|ui| { View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, || { - let addr = wallet.slatepack_address().unwrap(); - self.current_tab = Box::new(WalletTransport::new(addr)); + self.current_tab = Box::new(WalletTransport::default()); }); }); columns[3].vertical_centered_justified(|ui| { diff --git a/src/gui/views/wallets/wallet/messages.rs b/src/gui/views/wallets/wallet/messages.rs deleted file mode 100644 index 1306b1e..0000000 --- a/src/gui/views/wallets/wallet/messages.rs +++ /dev/null @@ -1,1166 +0,0 @@ -// Copyright 2023 The Grim Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::sync::Arc; -use std::thread; -use egui::{Id, Margin, RichText, ScrollArea}; -use egui::scroll_area::ScrollBarVisibility; -use grin_core::core::{amount_from_hr_string, amount_to_hr_string}; -use grin_wallet_libwallet::{Error, Slate, SlateState}; -use log::error; -use parking_lot::RwLock; - -use crate::gui::Colors; -use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, COPY, DOWNLOAD_SIMPLE, PROHIBIT, QR_CODE, SCAN, UPLOAD_SIMPLE}; -use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{CameraContent, FilePickButton, Modal, QrCodeContent, Content, View}; -use crate::gui::views::types::{ModalPosition, QrScanResult, TextEditOptions}; -use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType}; -use crate::gui::views::wallets::wallet::WalletContent; -use crate::wallet::types::WalletTransaction; -use crate::wallet::Wallet; - -#[derive(Clone, Eq, PartialEq, Debug, thiserror::Error)] -enum MessageError { - #[error("{0}")] - Response(String), - #[error("{0}")] - Parse(String), - #[error("{0}")] - Finalize(String), - #[error("{0}")] - Other(String), -} - -impl MessageError { - pub fn text(&self) -> &String { - match self { - MessageError::Response(text) => text, - MessageError::Parse(text) => text, - MessageError::Finalize(text) => text, - MessageError::Other(text) => text - } - } -} - -/// Slatepacks messages interaction tab content. -pub struct WalletMessages { - /// Slatepack message to create response message. - message_edit: String, - /// Parsed Slatepack message. - message_slate: Option, - /// Flag to check if message request is loading. - message_loading: bool, - /// Message request result. - receive_pay_result: Arc)>>>, - /// Message finalize or post result. - final_post_result: Arc>>>, - /// Slatepack error on finalization, parse and response creation. - message_error: Option, - /// Generated Slatepack response message. - response_edit: String, - /// Flag to check if Dandelion is needed to finalize transaction. - dandelion: bool, - /// Button to parse picked file content. - file_pick_button: FilePickButton, - - /// Flag to check if invoice or sending request was opened for [`Modal`]. - request_invoice: bool, - /// Amount to send or receive at [`Modal`]. - request_amount_edit: String, - /// Generated Slatepack message as request to send or receive funds at [`Modal`]. - request_edit: String, - /// Flag to check if there is an error happened on request creation at [`Modal`]. - request_error: Option, - /// Flag to check if response Slatepack message is showing as QR code image at [`Modal`]. - request_qr: bool, - /// Request Slatepack message QR code image [`Modal`] content. - request_qr_content: QrCodeContent, - /// Flag to check if request is loading at [`Modal`]. - request_loading: bool, - /// Request result if there is no error at [`Modal`]. - request_result: Arc>>>, - - /// Camera content for Slatepack message QR code scanning [`Modal`]. - message_camera_content: CameraContent, - /// Flag to check if there is an error on scanning Slatepack message QR code at [`Modal`]. - message_scan_error: bool, - - /// QR code Slatepacks message text to show at [`Modal`]. - qr_message_text: Option, - /// QR code Slatepack message image [`Modal`] content. - qr_message_content: QrCodeContent, -} - -/// Identifier for amount input [`Modal`] to create invoice or sending request. -const REQUEST_MODAL: &'static str = "messages_request_modal"; - -/// Identifier for QR code Slatepack message scan [`Modal`]. -const QR_SLATEPACK_MESSAGE_SCAN_MODAL: &'static str = "qr_slatepack_message_scan_modal"; - -/// Identifier for [`Modal`] to show QR code Slatepack message image. -const QR_SLATEPACK_MESSAGE_MODAL: &'static str = "qr_slatepack_message_modal"; - -impl WalletTab for WalletMessages { - fn get_type(&self) -> WalletTabType { - WalletTabType::Messages - } - - fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { - if WalletContent::sync_ui(ui, wallet) { - return; - } - - // Show modal content for this ui container. - self.modal_content_ui(ui, wallet, cb); - - egui::CentralPanel::default() - .frame(egui::Frame { - stroke: View::item_stroke(), - fill: Colors::white_or_black(false), - inner_margin: Margin { - left: View::far_left_inset_margin(ui) + 4.0, - right: View::get_right_inset() + 4.0, - top: 3.0, - bottom: 4.0, - }, - ..Default::default() - }) - .show_inside(ui, |ui| { - ScrollArea::vertical() - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .id_source(Id::from("wallet_messages").with(wallet.get_config().id)) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.vertical_centered(|ui| { - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - self.ui(ui, wallet, cb); - }); - }); - }); - }); - } -} - -impl WalletMessages { - /// Create new content instance, put message into input if provided. - pub fn new(dandelion: bool, message: Option) -> Self { - Self { - request_invoice: false, - message_edit: message.unwrap_or("".to_string()), - message_slate: None, - message_loading: false, - receive_pay_result: Arc::new(RwLock::new(None)), - final_post_result: Arc::new(RwLock::new(None)), - message_error: None, - response_edit: "".to_string(), - dandelion, - file_pick_button: FilePickButton::default(), - request_amount_edit: "".to_string(), - request_edit: "".to_string(), - request_error: None, - request_qr: false, - request_qr_content: QrCodeContent::new("".to_string(), true), - request_loading: false, - request_result: Arc::new(RwLock::new(None)), - message_camera_content: CameraContent::default(), - message_scan_error: false, - qr_message_text: None, - qr_message_content: QrCodeContent::new("".to_string(), true), - } - } - - /// Draw manual wallet transaction interaction content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - cb: &dyn PlatformCallbacks) { - ui.add_space(3.0); - - // Show creation of request to send or receive funds. - self.request_ui(ui, wallet, cb); - - ui.add_space(12.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); - - // Show Slatepack message input field. - self.input_slatepack_ui(ui, wallet, cb); - - ui.add_space(6.0); - } - - /// Draw [`Modal`] content for this ui container. - fn modal_content_ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - cb: &dyn PlatformCallbacks) { - match Modal::opened() { - None => {} - Some(id) => { - match id { - REQUEST_MODAL => { - Modal::ui(ui.ctx(), |ui, modal| { - self.request_modal_ui(ui, wallet, modal, cb); - }); - } - QR_SLATEPACK_MESSAGE_SCAN_MODAL => { - Modal::ui(ui.ctx(), |ui, modal| { - self.qr_message_scan_modal_ui(ui, modal, wallet, cb); - }); - } - QR_SLATEPACK_MESSAGE_MODAL => { - Modal::ui(ui.ctx(), |ui, modal| { - self.qr_message_modal_ui(ui, modal, cb); - }); - } - _ => {} - } - } - } - } - - /// Draw creation of request to send or receive funds. - fn request_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - cb: &dyn PlatformCallbacks) { - ui.label(RichText::new(t!("wallets.create_request_desc")) - .size(16.0) - .color(Colors::inactive_text())); - ui.add_space(7.0); - - // Show send button only if balance is not empty. - let data = wallet.get_data().unwrap(); - if data.info.amount_currently_spendable > 0 { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - // Draw sending request creation button. - let send_text = format!("{} {}", UPLOAD_SIMPLE, t!("wallets.send")); - View::colored_text_button(ui, send_text, Colors::red(), Colors::button(), || { - self.show_request_modal(false, cb); - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Draw invoice request creation button. - self.receive_button_ui(ui, cb); - }); - }); - } else { - // Draw invoice creation button. - self.receive_button_ui(ui, cb); - } - } - - /// Draw invoice request creation button. - fn receive_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - let receive_text = format!("{} {}", DOWNLOAD_SIMPLE, t!("wallets.receive")); - View::colored_text_button(ui, receive_text, Colors::green(), Colors::button(), || { - self.show_request_modal(true, cb); - }); - } - - /// Show [`Modal`] to create invoice or sending request. - fn show_request_modal(&mut self, invoice: bool, cb: &dyn PlatformCallbacks) { - // Setup modal values. - self.request_invoice = invoice; - self.request_qr = false; - self.request_edit = "".to_string(); - self.request_amount_edit = "".to_string(); - self.request_error = None; - { - let mut w_result = self.request_result.write(); - *w_result = None; - } - // Show receive amount modal. - let title = if self.request_invoice { - t!("wallets.receive") - } else { - t!("wallets.send") - }; - Modal::new(REQUEST_MODAL).position(ModalPosition::CenterTop).title(title).show(); - cb.show_keyboard(); - } - - /// Draw invoice or sending request creation [`Modal`] content. - fn request_modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - - if self.request_loading { - ui.add_space(34.0); - ui.vertical_centered(|ui| { - View::big_loading_spinner(ui); - }); - ui.add_space(50.0); - - // Check if there is request result error. - if self.request_error.is_some() { - modal.enable_closing(); - self.request_loading = false; - return; - } - - // Update data on request result. - let r_request = self.request_result.read(); - if r_request.is_some() { - let message = r_request.as_ref().unwrap(); - match message { - Ok((_, message)) => { - self.request_edit = message.clone(); - } - Err(err) => { - match err { - Error::NotEnoughFunds { .. } => { - let m = t!( - "wallets.pay_balance_error", - "amount" => self.request_amount_edit - ); - self.request_error = Some(MessageError::Other(m)); - } - _ => { - let m = t!("wallets.invoice_slatepack_err"); - self.request_error = Some(MessageError::Other(m)); - } - } - } - } - modal.enable_closing(); - self.request_loading = false; - } - } else if self.request_edit.is_empty() { - ui.vertical_centered(|ui| { - let enter_text = if self.request_invoice { - t!("wallets.enter_amount_receive") - } else { - let data = wallet.get_data().unwrap(); - let amount = amount_to_hr_string(data.info.amount_currently_spendable, true); - t!("wallets.enter_amount_send","amount" => amount) - }; - ui.label(RichText::new(enter_text) - .size(17.0) - .color(Colors::gray())); - }); - ui.add_space(8.0); - - // Draw request amount text input. - let amount_edit_id = Id::from(modal.id).with(wallet.get_config().id); - let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center(); - let amount_edit_before = self.request_amount_edit.clone(); - View::text_edit(ui, cb, &mut self.request_amount_edit, &mut amount_edit_opts); - - // Check value if input was changed. - if amount_edit_before != self.request_amount_edit { - self.request_error = None; - if !self.request_amount_edit.is_empty() { - self.request_amount_edit = self.request_amount_edit.trim().replace(",", "."); - match amount_from_hr_string(self.request_amount_edit.as_str()) { - Ok(a) => { - if !self.request_amount_edit.contains(".") { - // To avoid input of several "0". - if a == 0 { - self.request_amount_edit = "0".to_string(); - return; - } - } else { - // Check input after ".". - let parts = self.request_amount_edit - .split(".") - .collect::>(); - if parts.len() == 2 && parts[1].len() > 9 { - self.request_amount_edit = amount_edit_before; - return; - } - } - - // Do not input amount more than balance in sending. - if !self.request_invoice { - let b = wallet.get_data().unwrap().info.amount_currently_spendable; - if b < a { - self.request_amount_edit = amount_edit_before; - } - } - } - Err(_) => { - self.request_amount_edit = amount_edit_before; - } - } - } - } - - // Show request creation error. - if self.request_error.is_some() { - ui.add_space(12.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(self.request_error.clone().unwrap().text()) - .size(17.0) - .color(Colors::red())); - }); - } - - ui.add_space(12.0); - - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - self.request_amount_edit = "".to_string(); - self.request_error = None; - cb.hide_keyboard(); - modal.close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Button to create Slatepack message request. - View::button(ui, t!("continue"), Colors::white_or_black(false), || { - if self.request_amount_edit.is_empty() { - return; - } - if let Ok(a) = amount_from_hr_string(self.request_amount_edit.as_str()) { - cb.hide_keyboard(); - // Setup data for request. - let wallet = wallet.clone(); - let invoice = self.request_invoice.clone(); - let result = self.request_result.clone(); - // Send request at another thread. - self.request_loading = true; - modal.disable_closing(); - thread::spawn(move || { - let message = if invoice { - wallet.issue_invoice(a) - } else { - wallet.send(a) - }; - let mut w_result = result.write(); - *w_result = Some(message); - }); - } else { - self.request_error = Some( - MessageError::Other(t!("wallets.invoice_slatepack_err")) - ); - } - }); - }); - }); - ui.add_space(6.0); - } else { - ui.vertical_centered(|ui| { - let amount = amount_from_hr_string(self.request_amount_edit.as_str()).unwrap(); - let amount_format = amount_to_hr_string(amount, true); - let desc_text = if self.request_invoice { - t!("wallets.invoice_desc","amount" => amount_format) - } else { - t!("wallets.send_request_desc","amount" => amount_format) - }; - ui.label(RichText::new(desc_text).size(16.0).color(Colors::gray())); - }); - ui.add_space(6.0); - - // Draw QR code content if requested. - if self.request_qr { - // Draw QR code content. - let text = self.request_edit.clone(); - if text.is_empty() { - self.request_qr = false; - } - self.request_qr_content.ui(ui, text.clone(), cb); - - // Show button to close modal. - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.request_qr_content.clear_state(); - self.request_qr = false; - modal.close(); - }); - }); - ui.add_space(6.0); - return; - } - - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(3.0); - - // Draw request Slatepack message text. - let scroll_id = if self.request_invoice { - Id::from("receive_request").with(wallet.get_config().id) - } else { - Id::from("send_request").with(wallet.get_config().id) - }; - ScrollArea::vertical() - .id_source(scroll_id) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .max_height(128.0) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(7.0); - let input_id = Id::from(scroll_id).with("_input"); - egui::TextEdit::multiline(&mut self.request_edit) - .id(input_id) - .font(egui::TextStyle::Small) - .desired_rows(5) - .interactive(false) - .hint_text(SLATEPACK_MESSAGE_HINT) - .desired_width(f32::INFINITY) - .show(ui); - ui.add_space(6.0); - }); - ui.add_space(2.0); - View::horizontal_line(ui, Colors::item_stroke()); - - ui.add_space(10.0); - - 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| { - // Draw button to show request as QR code. - let qr_text = format!("{} {}", QR_CODE, t!("qr_code")); - View::button(ui, qr_text, Colors::button(), || { - self.request_qr = true; - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Draw button to copy request to clipboard. - let copy_text = format!("{} {}", COPY, t!("copy")); - View::button(ui, copy_text, Colors::button(), || { - cb.copy_string_to_buffer(self.request_edit.clone()); - self.request_amount_edit = "".to_string(); - self.request_edit = "".to_string(); - modal.close(); - }); - }); - }); - - ui.add_space(10.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - // Draw button to cancel transaction. - let cancel = t!("modal.cancel"); - View::colored_text_button(ui, cancel, Colors::red(), Colors::button(), || { - if let Ok(slate) = wallet.parse_slatepack(&self.request_edit) { - if let Some(tx) = wallet.tx_by_slate(&slate) { - wallet.cancel(tx.data.id); - } - } - self.request_amount_edit = "".to_string(); - self.request_edit = "".to_string(); - cb.hide_keyboard(); - modal.close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Draw button to close modal. - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.request_amount_edit = "".to_string(); - self.request_edit = "".to_string(); - modal.close(); - }); - }); - }); - }); - ui.add_space(6.0); - } - } - - /// Draw Slatepack message input content. - fn input_slatepack_ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - cb: &dyn PlatformCallbacks) { - // Setup description text. - let empty_fields = self.message_edit.is_empty() && self.request_edit.is_empty(); - let response_empty = self.response_edit.is_empty(); - if let Some(err) = &self.message_error { - ui.label(RichText::new(err.text()).size(16.0).color(Colors::red())); - } else { - let desc_text = if self.message_slate.is_none() || empty_fields { - t!("wallets.input_slatepack_desc") - } else { - let slate = self.message_slate.clone().unwrap(); - let amount = amount_to_hr_string(slate.amount, true); - match slate.state { - SlateState::Standard1 => { - t!("wallets.parse_s1_slatepack_desc","amount" => amount) - } - SlateState::Standard2 => { - t!("wallets.parse_s2_slatepack_desc","amount" => amount) - } - SlateState::Standard3 => { - t!("wallets.parse_s3_slatepack_desc","amount" => amount) - } - SlateState::Invoice1 => { - t!("wallets.parse_i1_slatepack_desc","amount" => amount) - } - SlateState::Invoice2 => { - t!("wallets.parse_i2_slatepack_desc","amount" => amount) - } - SlateState::Invoice3 => { - t!("wallets.parse_i3_slatepack_desc","amount" => amount) - } - _ => { - t!("wallets.input_slatepack_desc") - } - } - }; - ui.label(RichText::new(desc_text).size(16.0).color(Colors::inactive_text())); - } - ui.add_space(6.0); - - // Setup Slatepack message text input. - let message = if response_empty { - &mut self.message_edit - } else { - &mut self.response_edit - }; - - // Save message to check for changes. - let message_before = message.clone(); - - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(3.0); - let scroll_id = Id::from( - if response_empty { - "message_input" - } else { - "response_input" - }).with(wallet.get_config().id); - ScrollArea::vertical() - .id_source(scroll_id) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .max_height(128.0) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(7.0); - let input_id = scroll_id.with("_input"); - let resp = egui::TextEdit::multiline(message) - .id(input_id) - .font(egui::TextStyle::Small) - .desired_rows(5) - .interactive(response_empty && !self.message_loading) - .hint_text(SLATEPACK_MESSAGE_HINT) - .desired_width(f32::INFINITY) - .show(ui) - .response; - // Show soft keyboard on click. - if response_empty && resp.clicked() { - resp.request_focus(); - cb.show_keyboard(); - } - if response_empty && resp.has_focus() { - // Apply text from input on Android as temporary fix for egui. - View::on_soft_input(ui, input_id, message); - } - ui.add_space(6.0); - }); - ui.add_space(2.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(10.0); - - // Parse Slatepack message if input field was changed, resetting message error. - if &message_before != message { - self.parse_message(wallet); - } - - // Draw buttons to clear/copy/paste. - let columns_num = if self.message_loading { 1 } else { 2 }; - let mut show_dandelion = false; - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(columns_num, |columns| { - let first_column_content = |ui: &mut egui::Ui| { - if self.message_slate.is_some() && !empty_fields { - if self.response_edit.is_empty() { - // Draw button to clear message input. - let clear_text = format!("{} {}", BROOM, t!("clear")); - View::button(ui, clear_text, Colors::button(), || { - self.message_edit.clear(); - self.response_edit.clear(); - self.message_error = None; - self.message_slate = None; - }); - } else { - // Draw button to show Slatepack message as QR code. - let qr_text = format!("{} {}", QR_CODE, t!("qr_code")); - View::button(ui, qr_text, Colors::button(), || { - let text = self.response_edit.clone(); - self.message_edit.clear(); - self.response_edit.clear(); - self.show_qr_message_modal(text); - }); - } - } else { - if self.message_loading { - View::small_loading_spinner(ui); - // Check loading result. - self.check_message_loading_result(wallet); - } else { - // Draw button to scan Slatepack message QR code. - let scan_text = format!("{} {}", SCAN, t!("scan")); - View::button(ui, scan_text, Colors::button(), || { - self.message_edit.clear(); - self.message_error = None; - self.show_qr_message_scan_modal(cb); - }); - } - } - }; - if columns_num == 1 { - columns[0].vertical_centered(first_column_content); - } else { - columns[0].vertical_centered_justified(first_column_content); - columns[1].vertical_centered_justified(|ui| { - if self.message_slate.is_some() && !empty_fields { - if !self.response_edit.is_empty() { - // Draw button to copy response to clipboard. - let copy_text = format!("{} {}", COPY, t!("copy")); - View::button(ui, copy_text, Colors::button(), || { - cb.copy_string_to_buffer(self.response_edit.clone()); - self.message_edit.clear(); - self.response_edit.clear(); - self.message_slate = None; - }); - } else { - show_dandelion = true; - // Draw button to finalize or repost transaction. - View::action_button(ui, t!("wallets.finalize"), || { - let slate = self.message_slate.clone().unwrap(); - self.message_slate = None; - let dandelion = self.dandelion; - let message_edit = self.message_edit.clone(); - let wallet = wallet.clone(); - let result = self.final_post_result.clone(); - - // Finalize or post transaction at separate thread. - self.message_loading = true; - thread::spawn(move || { - let res = if slate.state == SlateState::Invoice3 || - slate.state == SlateState::Standard3 { - wallet.post(&slate, dandelion) - } else { - match wallet.finalize(&message_edit, dandelion) { - Ok(_) => { - Ok(()) - } - Err(e) => { - Err(e) - } - } - }; - let mut w_res = result.write(); - *w_res = Some(res); - }); - }); - } - } else { - // Draw button to paste text from clipboard. - let paste = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); - View::button(ui, paste, Colors::button(), || { - let buf = cb.get_string_from_buffer(); - let previous = self.message_edit.clone(); - self.message_edit = buf.clone().trim().to_string(); - // Parse Slatepack message resetting message error. - if buf != previous { - self.parse_message(wallet); - } - }); - } - }); - } - }); - - ui.add_space(10.0); - - // Draw clear button on message input, - // cancel and clear buttons on response - // or button to choose text or image file. - if !self.message_loading { - if self.message_slate.is_none() && !self.message_edit.is_empty() { - // Draw button to clear message input. - let clear_text = format!("{} {}", BROOM, t!("clear")); - View::button(ui, clear_text, Colors::button(), || { - self.message_edit.clear(); - self.response_edit.clear(); - self.message_error = None; - self.message_slate = None; - }); - } else if !self.response_edit.is_empty() && self.message_slate.is_some() { - // Draw cancel button. - let cancel_text = format!("{} {}", PROHIBIT, t!("modal.cancel")); - View::colored_text_button(ui, cancel_text, Colors::red(), Colors::button(), || { - let slate = self.message_slate.clone().unwrap(); - if let Some(tx) = wallet.tx_by_slate(&slate) { - wallet.cancel(tx.data.id); - self.message_edit.clear(); - self.response_edit.clear(); - self.message_slate = None; - } - }); - } else if self.message_slate.is_none() { - // Draw button to choose file. - let mut parsed_text = "".to_string(); - self.file_pick_button.ui(ui, cb, |text| { - parsed_text = text; - }); - if !parsed_text.is_empty() { - // Parse Slatepack message from file content. - self.message_edit = parsed_text; - self.parse_message(wallet); - } - } - } - }); - - // Draw setup of ability to post transaction with Dandelion. - if show_dandelion { - let dandelion_before = self.dandelion; - View::checkbox(ui, dandelion_before, t!("wallets.use_dandelion"), || { - self.dandelion = !dandelion_before; - wallet.update_use_dandelion(self.dandelion); - }); - } - } - - /// Show QR code Slatepack message [`Modal`]. - pub fn show_qr_message_modal(&mut self, text: String) { - self.qr_message_text = Some(text); - self.qr_message_content.clear_state(); - let slate = self.message_slate.clone().unwrap(); - let title = if slate.state == SlateState::Standard1 { - t!("wallets.receive") - } else { - t!("wallets.send") - }; - Modal::new(QR_SLATEPACK_MESSAGE_MODAL) - .position(ModalPosition::CenterTop) - .title(title) - .show(); - } - - /// Draw QR code Slatepack message image [`Modal`] content. - fn qr_message_modal_ui(&mut self, ui: &mut egui::Ui, m: &Modal, cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - - // Setup title for Slatepack message. - ui.vertical_centered(|ui| { - let slate = self.message_slate.clone().unwrap(); - let amount = amount_to_hr_string(slate.amount, true); - let title = if slate.state == SlateState::Standard1 { - t!("wallets.parse_s1_slatepack_desc","amount" => amount) - } else { - t!("wallets.parse_i1_slatepack_desc","amount" => amount) - }; - ui.label(RichText::new(title).size(16.0).color(Colors::inactive_text())); - }); - ui.add_space(6.0); - - // Draw QR code content. - let text = self.qr_message_text.clone().unwrap(); - self.qr_message_content.ui(ui, text.clone(), cb); - - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.qr_message_text = None; - self.qr_message_content.clear_state(); - self.response_edit.clear(); - self.message_slate = None; - m.close(); - }); - }); - ui.add_space(6.0); - } - - /// Show QR code Slatepack message scanner [`Modal`]. - pub fn show_qr_message_scan_modal(&mut self, cb: &dyn PlatformCallbacks) { - self.message_scan_error = false; - // Show QR code scan modal. - Modal::new(QR_SLATEPACK_MESSAGE_SCAN_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("scan_qr")) - .closeable(false) - .show(); - cb.start_camera(); - } - - /// Draw QR code scanner [`Modal`] content. - fn qr_message_scan_modal_ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - wallet: &Wallet, - cb: &dyn PlatformCallbacks) { - if self.message_scan_error { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - let err_text = format!("{}", t!("wallets.parse_slatepack_err")).replace(":", "."); - ui.label(RichText::new(err_text) - .size(17.0) - .color(Colors::red())); - }); - ui.add_space(12.0); - - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.message_scan_error = false; - modal.close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("repeat"), Colors::white_or_black(false), || { - Modal::set_title(t!("scan_qr")); - self.message_scan_error = false; - cb.start_camera(); - }); - }); - }); - ui.add_space(6.0); - return; - } else if let Some(result) = self.message_camera_content.qr_scan_result() { - cb.stop_camera(); - self.message_camera_content.clear_state(); - match &result { - QrScanResult::Slatepack(text) => { - self.message_edit = text.to_string(); - self.parse_message(wallet); - modal.close(); - } - _ => { - self.message_scan_error = true; - } - } - } else { - ui.add_space(6.0); - self.message_camera_content.ui(ui, cb); - ui.add_space(8.0); - } - - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - cb.stop_camera(); - modal.close(); - }); - }); - ui.add_space(6.0); - } - - /// Check Slatepack message request loading result. - fn check_message_loading_result(&mut self, wallet: &Wallet) { - // Check finalize post pay result. - let has_finalize_post_result = { - let r_res = self.final_post_result.read(); - r_res.is_some() - }; - if has_finalize_post_result { - let resp = { - let r_res = self.final_post_result.read(); - r_res.as_ref().unwrap().clone() - }; - if resp.is_ok() { - self.message_edit.clear(); - self.message_slate = None; - } else { - self.message_error = Some( - MessageError::Finalize( - t!("wallets.finalize_slatepack_err") - ) - ); - } - self.message_loading = false; - } - - // Check receive pay result. - let has_receive_pay_result = { - let r_res = self.receive_pay_result.read(); - r_res.is_some() - }; - if has_receive_pay_result { - let (slate, resp) = { - let r_res = self.receive_pay_result.read(); - r_res.as_ref().unwrap().clone() - }; - if resp.is_ok() { - self.response_edit = resp.as_ref().unwrap().clone(); - } else { - let err = resp.as_ref().err().unwrap(); - match err { - // Set already canceled transaction error message. - Error::TransactionWasCancelled {..} - => { - self.message_error = Some( - MessageError::Response( - t!("wallets.resp_canceled_err") - ) - ); - } - // Set an error when there is not enough funds to pay. - Error::NotEnoughFunds {..} => { - let m = t!( - "wallets.pay_balance_error", - "amount" => amount_to_hr_string(slate.amount, true) - ); - self.message_error = Some(MessageError::Response(m)); - } - // Set default error message. - _ => { - self.message_error = Some( - MessageError::Response( - t!("wallets.resp_slatepack_err") - ) - ); - } - } - // Check if tx with same slate id already exists. - if self.message_error.is_none() { - let exists_tx = wallet.tx_by_slate(&slate).is_some(); - if exists_tx { - let mut sl = slate.clone(); - sl.state = if sl.state == SlateState::Standard1 { - SlateState::Standard2 - } else { - SlateState::Invoice2 - }; - match wallet.read_slatepack(&sl) { - None => { - self.message_error = Some( - MessageError::Response( - t!("wallets.resp_slatepack_err") - ) - ); - } - Some(sp) => { - self.response_edit = sp; - } - } - } - } - } - // Setup message slate. - if self.message_error.is_none() { - self.message_slate = Some(slate); - } - // Clear message loading result and status. - { - let mut w_res = self.receive_pay_result.write(); - *w_res = None; - } - self.message_loading = false; - } - } - - /// Parse message input into [`Slate`] updating slate and response input. - pub fn parse_message(&mut self, wallet: &Wallet) { - self.message_slate = None; - self.message_error = None; - if self.message_edit.is_empty() { - return; - } - // Trim message. - self.message_edit = self.message_edit.trim().to_string(); - - // Parse message. - if let Ok(mut slate) = wallet.parse_slatepack(&self.message_edit) { - // Try to setup empty amount from transaction by id. - if slate.amount == 0 { - let _ = wallet.get_data().unwrap().txs.as_ref().unwrap().iter().map(|tx| { - if tx.data.tx_slate_id == Some(slate.id) { - if slate.amount == 0 { - slate.amount = tx.amount; - } - } - tx - }).collect::>(); - } - - if slate.amount == 0 { - self.message_error = Some( - MessageError::Response(t!("wallets.resp_slatepack_err")) - ); - return; - } - - // Make operation based on incoming state status. - match slate.state { - SlateState::Standard1 | SlateState::Invoice1 => { - let slate = slate.clone(); - let message = self.message_edit.clone(); - let message_result = self.receive_pay_result.clone(); - let wallet = wallet.clone(); - // Create response to sender or receiver at separate thread. - self.message_loading = true; - thread::spawn(move || { - let resp = if slate.state == SlateState::Standard1 { - wallet.receive(&message) - } else { - wallet.pay(&message) - }; - let mut w_res = message_result.write(); - *w_res = Some((slate, resp)); - }); - return; - } - SlateState::Standard2 | SlateState::Invoice2 => { - // Check if slatepack with same id and state already exists. - let mut sl = slate.clone(); - sl.state = if sl.state == SlateState::Standard2 { - SlateState::Standard1 - } else { - SlateState::Invoice1 - }; - match wallet.read_slatepack(&sl) { - None => { - match wallet.read_slatepack(&slate) { - None => { - self.message_error = Some( - MessageError::Response(t!("wallets.resp_slatepack_err")) - ); - } - Some(sp) => { - self.message_slate = Some(sl); - self.response_edit = sp; - return; - } - } - } - Some(_) => { - self.message_slate = Some(slate.clone()); - return; - } - } - } - _ => { - self.response_edit = "".to_string(); - } - } - self.message_slate = Some(slate); - } else { - self.message_slate = None; - self.message_error = Some(MessageError::Parse(t!("wallets.resp_slatepack_err"))); - } - } -} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/messages/content.rs b/src/gui/views/wallets/wallet/messages/content.rs new file mode 100644 index 0000000..b0214e5 --- /dev/null +++ b/src/gui/views/wallets/wallet/messages/content.rs @@ -0,0 +1,541 @@ +// Copyright 2024 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; +use std::thread; +use egui::{Id, Margin, RichText, ScrollArea}; +use egui::scroll_area::ScrollBarVisibility; +use grin_core::core::amount_to_hr_string; +use grin_wallet_libwallet::{Error, Slate, SlateState}; +use parking_lot::RwLock; + +use crate::gui::Colors; +use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, DOWNLOAD_SIMPLE, SCAN, UPLOAD_SIMPLE}; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{FilePickButton, Modal, Content, View, CameraContent}; +use crate::gui::views::types::{ModalPosition, QrScanResult}; +use crate::gui::views::wallets::wallet::messages::request::MessageRequestModal; +use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType}; +use crate::gui::views::wallets::wallet::{WalletContent, WalletTransactionModal}; +use crate::wallet::types::WalletTransaction; +use crate::wallet::Wallet; + +/// Slatepack messages interaction tab content. +pub struct WalletMessages { + /// Slatepacks message input text. + message_edit: String, + /// Flag to check if message request is loading. + message_loading: bool, + /// Error on finalization, parse or response creation. + message_error: String, + /// Parsed message result with finalization flag and transaction. + message_result: Arc)>>>, + + /// Wallet transaction [`Modal`] content. + tx_info_content: Option, + + /// Invoice or sending request creation [`Modal`] content. + request_modal_content: Option, + + /// Camera content for Slatepack message QR code scanning [`Modal`]. + message_camera_content: CameraContent, + /// Flag to check if there is an error on scanning Slatepack message QR code at [`Modal`]. + message_scan_error: bool, + + /// Button to parse picked file content. + file_pick_button: FilePickButton, +} + +/// Identifier for amount input [`Modal`] to create invoice or sending request. +const REQUEST_MODAL: &'static str = "messages_request"; + +/// Identifier for [`Modal`] modal to show transaction information. +const TX_INFO_MODAL: &'static str = "messages_tx_info"; + +/// Identifier for [`Modal`] to scan Slatepack message from QR code. +const SCAN_QR_MESSAGE_MODAL: &'static str = "qr_slatepack_message_scan_modal"; + +impl WalletTab for WalletMessages { + fn get_type(&self) -> WalletTabType { + WalletTabType::Messages + } + + fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { + if WalletContent::sync_ui(ui, wallet) { + return; + } + + // Show modal content for this ui container. + self.modal_content_ui(ui, wallet, cb); + + egui::CentralPanel::default() + .frame(egui::Frame { + stroke: View::item_stroke(), + fill: Colors::white_or_black(false), + inner_margin: Margin { + left: View::far_left_inset_margin(ui) + 4.0, + right: View::get_right_inset() + 4.0, + top: 3.0, + bottom: 4.0, + }, + ..Default::default() + }) + .show_inside(ui, |ui| { + ScrollArea::vertical() + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .id_source(Id::from("wallet_messages").with(wallet.get_config().id)) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.vertical_centered(|ui| { + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + self.ui(ui, wallet, cb); + }); + }); + }); + }); + } +} + +impl WalletMessages { + /// Create new content instance, put message into input if provided. + pub fn new(message: Option) -> Self { + Self { + message_edit: message.unwrap_or("".to_string()), + message_loading: false, + message_error: "".to_string(), + message_result: Arc::new(Default::default()), + tx_info_content: None, + request_modal_content: None, + message_camera_content: Default::default(), + message_scan_error: false, + file_pick_button: FilePickButton::default(), + } + } + + /// Draw manual wallet transaction interaction content. + pub fn ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + cb: &dyn PlatformCallbacks) { + ui.add_space(3.0); + + // Show creation of request to send or receive funds. + self.request_ui(ui, wallet, cb); + + ui.add_space(12.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); + + // Show Slatepack message input field. + self.input_slatepack_ui(ui, wallet, cb); + + ui.add_space(6.0); + } + + /// Draw [`Modal`] content for this ui container. + fn modal_content_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + cb: &dyn PlatformCallbacks) { + match Modal::opened() { + None => {} + Some(id) => { + match id { + REQUEST_MODAL => { + if let Some(content) = self.request_modal_content.as_mut() { + Modal::ui(ui.ctx(), |ui, modal| { + content.ui(ui, wallet, modal, cb); + }); + } + } + TX_INFO_MODAL => { + if let Some(content) = self.tx_info_content.as_mut() { + Modal::ui(ui.ctx(), |ui, modal| { + content.ui(ui, wallet, modal, cb); + }); + } + } + SCAN_QR_MESSAGE_MODAL => { + Modal::ui(ui.ctx(), |ui, modal| { + self.qr_message_scan_modal_ui(ui, modal, wallet, cb); + }); + } + _ => {} + } + } + } + } + + /// Draw creation of request to send or receive funds. + fn request_ui(&mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + cb: &dyn PlatformCallbacks) { + ui.label(RichText::new(t!("wallets.create_request_desc")) + .size(16.0) + .color(Colors::inactive_text())); + ui.add_space(7.0); + + // Show send button only if balance is not empty. + let data = wallet.get_data().unwrap(); + if data.info.amount_currently_spendable > 0 { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + let send_text = format!("{} {}", UPLOAD_SIMPLE, t!("wallets.send")); + View::colored_text_button(ui, send_text, Colors::red(), Colors::button(), || { + self.show_request_modal(false, cb); + }); + }); + columns[1].vertical_centered_justified(|ui| { + self.receive_button_ui(ui, cb); + }); + }); + } else { + self.receive_button_ui(ui, cb); + } + } + + /// Draw invoice request creation button. + fn receive_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + let receive_text = format!("{} {}", DOWNLOAD_SIMPLE, t!("wallets.receive")); + View::colored_text_button(ui, receive_text, Colors::green(), Colors::button(), || { + self.show_request_modal(true, cb); + }); + } + + /// Show [`Modal`] to create invoice or sending request. + fn show_request_modal(&mut self, invoice: bool, cb: &dyn PlatformCallbacks) { + self.request_modal_content = Some(MessageRequestModal::new(invoice)); + let title = if invoice { + t!("wallets.receive") + } else { + t!("wallets.send") + }; + Modal::new(REQUEST_MODAL).position(ModalPosition::CenterTop).title(title).show(); + cb.show_keyboard(); + } + + /// Draw Slatepack message input content. + fn input_slatepack_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + cb: &dyn PlatformCallbacks) { + // Setup description text. + if !self.message_error.is_empty() { + ui.label(RichText::new(&self.message_error).size(16.0).color(Colors::red())); + } else { + ui.label(RichText::new(t!("wallets.input_slatepack_desc")) + .size(16.0) + .color(Colors::inactive_text())); + } + ui.add_space(6.0); + + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(3.0); + + // Save message to check for changes. + let message_before = self.message_edit.clone(); + + let scroll_id = Id::from("message_input_scroll").with(wallet.get_config().id); + ScrollArea::vertical() + .id_source(scroll_id) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(128.0) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + let input_id = scroll_id.with("_input"); + let resp = egui::TextEdit::multiline(&mut self.message_edit) + .id(input_id) + .font(egui::TextStyle::Small) + .desired_rows(5) + .interactive(!self.message_loading) + .hint_text(SLATEPACK_MESSAGE_HINT) + .desired_width(f32::INFINITY) + .show(ui) + .response; + // Show soft keyboard on click. + if resp.clicked() { + resp.request_focus(); + cb.show_keyboard(); + } + if resp.has_focus() { + // Apply text from input on Android as temporary fix for egui. + View::on_soft_input(ui, input_id, &mut self.message_edit); + } + ui.add_space(6.0); + }); + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(10.0); + + // Parse message if input field was changed. + if message_before != self.message_edit { + self.parse_message(wallet); + } + + if self.message_loading { + View::small_loading_spinner(ui); + // Check loading result. + let has_tx = { + let r_res = self.message_result.read(); + r_res.is_some() + }; + if has_tx { + let mut w_res = self.message_result.write(); + let tx_res = w_res.as_ref().unwrap(); + let slate = &tx_res.0; + match &tx_res.1 { + Ok(tx) => { + self.message_edit.clear(); + // Show transaction modal on success. + self.tx_info_content = Some(WalletTransactionModal::new(wallet, tx, false)); + Modal::new(TX_INFO_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.tx")) + .show(); + *w_res = None; + } + Err(err) => { + match err { + // Set already canceled transaction error message. + Error::TransactionWasCancelled {..} => { + self.message_error = t!("wallets.resp_canceled_err"); + } + // Set an error when there is not enough funds to pay. + Error::NotEnoughFunds {..} => { + let m = t!( + "wallets.pay_balance_error", + "amount" => amount_to_hr_string(slate.amount, true) + ); + self.message_error = m; + } + // Set default error message. + _ => { + let finalize = slate.state == SlateState::Standard2 || + slate.state == SlateState::Invoice2; + self.message_error = if finalize { + t!("wallets.finalize_slatepack_err") + } else { + t!("wallets.resp_slatepack_err") + }; + } + } + } + } + self.message_loading = false; + } + return; + } + + 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| { + // Draw button to scan Slatepack message QR code. + let scan_text = format!("{} {}", SCAN, t!("scan")); + View::button(ui, scan_text, Colors::button(), || { + self.message_edit.clear(); + self.message_error.clear(); + self.show_qr_message_scan_modal(cb); + }); + }); + columns[1].vertical_centered_justified(|ui| { + // Draw button to paste text from clipboard. + let paste = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); + View::button(ui, paste, Colors::button(), || { + let buf = cb.get_string_from_buffer(); + let previous = self.message_edit.clone(); + self.message_edit = buf.clone().trim().to_string(); + // Parse Slatepack message resetting message error. + if buf != previous { + self.parse_message(wallet); + self.parse_message(wallet); + } + }); + }); + }); + ui.add_space(10.0); + }); + + if self.message_edit.is_empty() { + // Draw button to choose file. + let mut parsed_text = "".to_string(); + self.file_pick_button.ui(ui, cb, |text| { + parsed_text = text; + }); + self.message_edit = parsed_text; + self.parse_message(wallet); + } else { + // Draw button to clear message input. + let clear_text = format!("{} {}", BROOM, t!("clear")); + View::button(ui, clear_text, Colors::button(), || { + self.message_edit.clear(); + self.message_error.clear(); + }); + } + } + + /// Parse message input making operation based on incoming status. + pub fn parse_message(&mut self, wallet: &Wallet) { + self.message_error.clear(); + self.message_edit = self.message_edit.trim().to_string(); + if self.message_edit.is_empty() { + return; + } + if let Ok(mut slate) = wallet.parse_slatepack(&self.message_edit) { + // Try to setup empty amount from transaction by id. + if slate.amount == 0 { + let _ = wallet.get_data().unwrap().txs.as_ref().unwrap().iter().map(|tx| { + if tx.data.tx_slate_id == Some(slate.id) { + if slate.amount == 0 { + slate.amount = tx.amount; + } + } + tx + }).collect::>(); + } + + // Check if message with same id and state already exists to show tx modal. + let exists = wallet.read_slatepack(&slate).is_some(); + if exists { + if let Some(tx) = wallet.tx_by_slate(&slate).as_ref() { + self.message_edit.clear(); + self.tx_info_content = Some(WalletTransactionModal::new(wallet, tx, false)); + Modal::new(TX_INFO_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.tx")) + .show(); + } else { + self.message_error = t!("wallets.parse_slatepack_err"); + } + return; + } + + // Create response or finalize at separate thread. + let sl = slate.clone(); + let message = self.message_edit.clone(); + let message_result = self.message_result.clone(); + let wallet = wallet.clone(); + + self.message_loading = true; + thread::spawn(move || { + let result = match slate.state { + SlateState::Standard1 | SlateState::Invoice1 => { + if sl.state != SlateState::Standard1 { + wallet.pay(&message) + } else { + wallet.receive(&message) + } + } + SlateState::Standard2 | SlateState::Invoice2 => { + wallet.finalize(&message) + } + _ => { + if let Some(tx) = wallet.tx_by_slate(&slate) { + Ok(tx) + } else { + Err(Error::GenericError(t!("wallets.parse_slatepack_err"))) + } + } + }; + let mut w_res = message_result.write(); + *w_res = Some((slate, result)); + }); + } else { + self.message_error = t!("wallets.parse_slatepack_err"); + } + } + + /// Show QR code Slatepack message scanner [`Modal`]. + pub fn show_qr_message_scan_modal(&mut self, cb: &dyn PlatformCallbacks) { + self.message_scan_error = false; + // Show QR code scan modal. + Modal::new(SCAN_QR_MESSAGE_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("scan_qr")) + .closeable(false) + .show(); + cb.start_camera(); + } + + /// Draw QR code scanner [`Modal`] content. + fn qr_message_scan_modal_ui(&mut self, + ui: &mut egui::Ui, + modal: &Modal, + wallet: &Wallet, + cb: &dyn PlatformCallbacks) { + if self.message_scan_error { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let err_text = format!("{}", t!("wallets.parse_slatepack_err")).replace(":", "."); + ui.label(RichText::new(err_text) + .size(17.0) + .color(Colors::red())); + }); + ui.add_space(12.0); + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + self.message_scan_error = false; + modal.close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("repeat"), Colors::white_or_black(false), || { + Modal::set_title(t!("scan_qr")); + self.message_scan_error = false; + cb.start_camera(); + }); + }); + }); + ui.add_space(6.0); + return; + } else if let Some(result) = self.message_camera_content.qr_scan_result() { + cb.stop_camera(); + self.message_camera_content.clear_state(); + match &result { + QrScanResult::Slatepack(text) => { + self.message_edit = text.to_string(); + self.parse_message(wallet); + modal.close(); + } + _ => { + self.message_scan_error = true; + } + } + } else { + ui.add_space(6.0); + self.message_camera_content.ui(ui, cb); + ui.add_space(8.0); + } + + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + cb.stop_camera(); + modal.close(); + }); + }); + ui.add_space(6.0); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/messages/mod.rs b/src/gui/views/wallets/wallet/messages/mod.rs new file mode 100644 index 0000000..5199266 --- /dev/null +++ b/src/gui/views/wallets/wallet/messages/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2024 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod content; +pub use content::*; + +mod request; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/messages/request.rs b/src/gui/views/wallets/wallet/messages/request.rs new file mode 100644 index 0000000..6517f7b --- /dev/null +++ b/src/gui/views/wallets/wallet/messages/request.rs @@ -0,0 +1,260 @@ +// Copyright 2024 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; +use std::thread; +use parking_lot::RwLock; +use egui::{Id, RichText}; +use grin_core::core::{amount_from_hr_string, amount_to_hr_string}; +use grin_wallet_libwallet::Error; + +use crate::gui::Colors; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{Modal, View}; +use crate::gui::views::types::TextEditOptions; +use crate::gui::views::wallets::wallet::WalletTransactionModal; +use crate::wallet::types::WalletTransaction; +use crate::wallet::Wallet; + +/// Invoice or sending request creation [`Modal`] content. +pub struct MessageRequestModal { + /// Flag to check if invoice or sending request was opened. + invoice: bool, + + /// Amount to send or receive. + amount_edit: String, + + /// Flag to check if request is loading. + request_loading: bool, + /// Request result if there is no error. + request_result: Arc>>>, + /// Flag to check if there is an error happened on request creation. + request_error: Option, + + /// Request result transaction content. + result_tx_content: Option, +} + +impl MessageRequestModal { + /// Create new content instance. + pub fn new(invoice: bool) -> Self { + Self { + invoice, + amount_edit: "".to_string(), + request_loading: false, + request_result: Arc::new(RwLock::new(None)), + request_error: None, + result_tx_content: None, + } + } + + /// Draw [`Modal`] content. + pub fn ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + // Draw transaction information on request result. + if let Some(tx) = self.result_tx_content.as_mut() { + tx.ui(ui, wallet, modal, cb); + return; + } + + ui.add_space(6.0); + + // Draw content on request loading. + if self.request_loading { + self.loading_request_ui(ui, wallet, modal); + return; + } + + // Draw amount input content. + self.amount_input_ui(ui, wallet, modal, cb); + + // Show request creation error. + if let Some(err) = &self.request_error { + ui.add_space(12.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(err) + .size(17.0) + .color(Colors::red())); + }); + } + + ui.add_space(12.0); + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + self.amount_edit = "".to_string(); + self.request_error = None; + cb.hide_keyboard(); + modal.close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + // Button to create Slatepack message request. + View::button(ui, t!("continue"), Colors::white_or_black(false), || { + if self.amount_edit.is_empty() { + return; + } + if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) { + cb.hide_keyboard(); + modal.disable_closing(); + // Setup data for request. + let wallet = wallet.clone(); + let invoice = self.invoice.clone(); + let result = self.request_result.clone(); + // Send request at another thread. + self.request_loading = true; + thread::spawn(move || { + let res = if invoice { + wallet.issue_invoice(a) + } else { + wallet.send(a) + }; + let mut w_result = result.write(); + *w_result = Some(res); + }); + } else { + let err = if self.invoice { + t!("wallets.invoice_slatepack_err") + } else { + t!("wallets.send_slatepack_err") + }; + self.request_error = Some(err); + } + }); + }); + }); + ui.add_space(6.0); + } + + /// Draw amount input content. + fn amount_input_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + ui.vertical_centered(|ui| { + let enter_text = if self.invoice { + t!("wallets.enter_amount_receive") + } else { + let data = wallet.get_data().unwrap(); + let amount = amount_to_hr_string(data.info.amount_currently_spendable, true); + t!("wallets.enter_amount_send","amount" => amount) + }; + ui.label(RichText::new(enter_text) + .size(17.0) + .color(Colors::gray())); + }); + ui.add_space(8.0); + + // Draw request amount text input. + let amount_edit_id = Id::from(modal.id).with(wallet.get_config().id); + let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center(); + let amount_edit_before = self.amount_edit.clone(); + View::text_edit(ui, cb, &mut self.amount_edit, &mut amount_edit_opts); + + // Check value if input was changed. + if amount_edit_before != self.amount_edit { + self.request_error = None; + if !self.amount_edit.is_empty() { + self.amount_edit = self.amount_edit.trim().replace(",", "."); + match amount_from_hr_string(self.amount_edit.as_str()) { + Ok(a) => { + if !self.amount_edit.contains(".") { + // To avoid input of several "0". + if a == 0 { + self.amount_edit = "0".to_string(); + return; + } + } else { + // Check input after ".". + let parts = self.amount_edit + .split(".") + .collect::>(); + if parts.len() == 2 && parts[1].len() > 9 { + self.amount_edit = amount_edit_before; + return; + } + } + + // Do not input amount more than balance in sending. + if !self.invoice { + let b = wallet.get_data().unwrap().info.amount_currently_spendable; + if b < a { + self.amount_edit = amount_edit_before; + } + } + } + Err(_) => { + self.amount_edit = amount_edit_before; + } + } + } + } + } + + /// Draw loading request content. + fn loading_request_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, modal: &Modal) { + ui.add_space(34.0); + ui.vertical_centered(|ui| { + View::big_loading_spinner(ui); + }); + ui.add_space(50.0); + + // Check if there is request result error. + if self.request_error.is_some() { + modal.enable_closing(); + self.request_loading = false; + return; + } + + // Update data on request result. + let r_request = self.request_result.read(); + if r_request.is_some() { + modal.enable_closing(); + let result = r_request.as_ref().unwrap(); + match result { + Ok(tx) => { + self.result_tx_content = Some(WalletTransactionModal::new(wallet, tx, false)); + } + Err(err) => { + match err { + Error::NotEnoughFunds { .. } => { + let m = t!( + "wallets.pay_balance_error", + "amount" => self.amount_edit + ); + self.request_error = Some(m); + } + _ => { + let m = if self.invoice { + t!("wallets.invoice_slatepack_err") + } else { + t!("wallets.send_slatepack_err") + }; + self.request_error = Some(m); + } + } + self.request_loading = false; + } + } + } + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/settings/common.rs b/src/gui/views/wallets/wallet/settings/common.rs index e312e70..f60d26e 100644 --- a/src/gui/views/wallets/wallet/settings/common.rs +++ b/src/gui/views/wallets/wallet/settings/common.rs @@ -36,7 +36,7 @@ pub struct CommonSettings { new_pass_edit: String, /// Minimum confirmations number value. - min_confirmations_edit: String + min_confirmations_edit: String, } /// Identifier for wallet name [`Modal`]. @@ -54,25 +54,26 @@ impl Default for CommonSettings { wrong_pass: false, old_pass_edit: "".to_string(), new_pass_edit: "".to_string(), - min_confirmations_edit: "".to_string() + min_confirmations_edit: "".to_string(), } } } impl CommonSettings { + /// Draw common wallet settings content. pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { // Show modal content for this ui container. self.modal_content_ui(ui, wallet, cb); ui.vertical_centered(|ui| { - let wallet_name = wallet.get_config().name; + let config = wallet.get_config(); // Show wallet name. ui.add_space(2.0); ui.label(RichText::new(t!("wallets.name")) .size(16.0) .color(Colors::gray())); ui.add_space(2.0); - ui.label(RichText::new(wallet_name.clone()) + ui.label(RichText::new(&config.name) .size(16.0) .color(Colors::white_or_black(true))); ui.add_space(8.0); @@ -80,7 +81,7 @@ impl CommonSettings { // Show wallet name setup. let name_text = format!("{} {}", PENCIL, t!("change")); View::button(ui, name_text, Colors::button(), || { - self.name_edit = wallet_name; + self.name_edit = config.name; // Show wallet name modal. Modal::new(NAME_EDIT_MODAL) .position(ModalPosition::CenterTop) @@ -118,10 +119,9 @@ impl CommonSettings { ui.add_space(6.0); // Show minimum amount of confirmations value setup. - let min_confirmations = wallet.get_config().min_confirmations; - let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, min_confirmations); + let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, config.min_confirmations); View::button(ui, min_conf_text, Colors::button(), || { - self.min_confirmations_edit = min_confirmations.to_string(); + self.min_confirmations_edit = config.min_confirmations.to_string(); // Show minimum amount of confirmations value modal. Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL) .position(ModalPosition::CenterTop) @@ -131,8 +131,15 @@ impl CommonSettings { }); ui.add_space(12.0); + + // Setup ability to post wallet transactions with Dandelion. + View::checkbox(ui, wallet.can_use_dandelion(), t!("wallets.use_dandelion"), || { + wallet.update_use_dandelion(!wallet.can_use_dandelion()); + }); + + ui.add_space(6.0); View::horizontal_line(ui, Colors::stroke()); - ui.add_space(4.0); + ui.add_space(6.0); }); } diff --git a/src/gui/views/wallets/wallet/settings/recovery.rs b/src/gui/views/wallets/wallet/settings/recovery.rs index 969e2cd..686d059 100644 --- a/src/gui/views/wallets/wallet/settings/recovery.rs +++ b/src/gui/views/wallets/wallet/settings/recovery.rs @@ -232,7 +232,7 @@ impl RecoverySettings { }); }); columns[1].vertical_centered_justified(|ui| { - View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || { + let mut on_next = || { match wallet.get_recovery(self.pass_edit.clone()) { Ok(phrase) => { self.wrong_pass = false; @@ -243,6 +243,12 @@ impl RecoverySettings { self.wrong_pass = true; } } + }; + View::on_enter_key(ui, || { + (on_next)(); + }); + View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || { + on_next(); }); }); }); diff --git a/src/gui/views/wallets/wallet/transport.rs b/src/gui/views/wallets/wallet/transport.rs deleted file mode 100644 index e3192cd..0000000 --- a/src/gui/views/wallets/wallet/transport.rs +++ /dev/null @@ -1,944 +0,0 @@ -// Copyright 2023 The Grim Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::sync::Arc; -use std::thread; -use egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea}; -use egui::os::OperatingSystem; -use egui::scroll_area::ScrollBarVisibility; -use parking_lot::RwLock; -use tor_rtcompat::BlockOn; -use tor_rtcompat::tokio::TokioNativeTlsRuntime; -use grin_core::core::{amount_from_hr_string, amount_to_hr_string}; -use grin_wallet_libwallet::SlatepackAddress; - -use crate::gui::Colors; -use crate::gui::icons::{CHECK_CIRCLE, COPY, DOTS_THREE_CIRCLE, EXPORT, GEAR_SIX, GLOBE_SIMPLE, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, X_CIRCLE}; -use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{CameraContent, Modal, QrCodeContent, Content, View}; -use crate::gui::views::types::{ModalPosition, TextEditOptions}; -use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType}; -use crate::gui::views::wallets::wallet::WalletContent; -use crate::tor::{Tor, TorBridge, TorConfig}; -use crate::wallet::types::WalletData; -use crate::wallet::Wallet; - -/// Wallet transport tab content. -pub struct WalletTransport { - /// Flag to check if transaction is sending over Tor to show progress at [`Modal`]. - tor_sending: Arc>, - /// Flag to check if error occurred during sending of transaction over Tor at [`Modal`]. - tor_send_error: Arc>, - /// Flag to check if transaction sent successfully over Tor [`Modal`]. - tor_success: Arc>, - /// Entered amount value for [`Modal`]. - amount_edit: String, - /// Entered address value for [`Modal`]. - address_edit: String, - /// Flag to check if entered address is incorrect at [`Modal`]. - address_error: bool, - /// Flag to check if QR code scanner is opened at address [`Modal`]. - show_address_scan: bool, - /// Address QR code scanner [`Modal`] content. - address_scan_content: CameraContent, - /// Flag to check if [`Modal`] was just opened to focus on first field. - modal_just_opened: bool, - - /// QR code address image [`Modal`] content. - qr_address_content: QrCodeContent, - - /// Flag to check if Tor settings were changed. - tor_settings_changed: bool, - /// Tor bridge binary path edit text. - bridge_bin_path_edit: String, - /// Tor bridge connection line edit text. - bridge_conn_line_edit: String, - /// Flag to check if QR code scanner is opened at bridge [`Modal`]. - show_bridge_scan: bool, - /// Address QR code scanner [`Modal`] content. - bridge_qr_scan_content: CameraContent, -} - -impl WalletTab for WalletTransport { - fn get_type(&self) -> WalletTabType { - WalletTabType::Transport - } - - fn ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - cb: &dyn PlatformCallbacks) { - if WalletContent::sync_ui(ui, wallet) { - return; - } - - // Show modal content for this ui container. - self.modal_content_ui(ui, wallet, cb); - - // Show transport content panel. - egui::CentralPanel::default() - .frame(egui::Frame { - stroke: View::item_stroke(), - fill: Colors::white_or_black(false), - inner_margin: Margin { - left: View::far_left_inset_margin(ui) + 4.0, - right: View::get_right_inset() + 4.0, - top: 3.0, - bottom: 4.0, - }, - ..Default::default() - }) - .show_inside(ui, |ui| { - ScrollArea::vertical() - .id_source(Id::from("wallet_transport").with(wallet.get_config().id)) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.vertical_centered(|ui| { - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - self.ui(ui, wallet, cb); - }); - }); - }); - }); - } -} - -/// Identifier for [`Modal`] to send amount over Tor. -const SEND_TOR_MODAL: &'static str = "send_tor_modal"; - -/// Identifier for [`Modal`] to setup Tor service. -const TOR_SETTINGS_MODAL: &'static str = "tor_settings_modal"; - -/// Identifier for [`Modal`] to show QR code address image. -const QR_ADDRESS_MODAL: &'static str = "qr_address_modal"; - -impl WalletTransport { - /// Create new content instance from provided Slatepack address text. - pub fn new(addr: String) -> Self { - // Setup Tor bridge binary path edit text. - let bridge = TorConfig::get_bridge(); - let (bin_path, conn_line) = if let Some(b) = bridge { - (b.binary_path(), b.connection_line()) - } else { - ("".to_string(), "".to_string()) - }; - Self { - tor_sending: Arc::new(RwLock::new(false)), - tor_send_error: Arc::new(RwLock::new(false)), - tor_success: Arc::new(RwLock::new(false)), - amount_edit: "".to_string(), - address_edit: "".to_string(), - address_error: false, - show_address_scan: false, - address_scan_content: CameraContent::default(), - modal_just_opened: false, - qr_address_content: QrCodeContent::new(addr, false), - tor_settings_changed: false, - bridge_bin_path_edit: bin_path, - bridge_conn_line_edit: conn_line, - show_bridge_scan: false, - bridge_qr_scan_content: CameraContent::default(), - } - } - - /// Draw wallet transport content. - pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { - ui.add_space(3.0); - ui.label(RichText::new(t!("transport.desc")) - .size(16.0) - .color(Colors::inactive_text())); - ui.add_space(7.0); - - // Draw Tor content. - self.tor_ui(ui, wallet, cb); - } - - /// Draw [`Modal`] content for this ui container. - fn modal_content_ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - cb: &dyn PlatformCallbacks) { - match Modal::opened() { - None => {} - Some(id) => { - match id { - SEND_TOR_MODAL => { - Modal::ui(ui.ctx(), |ui, modal| { - self.send_tor_modal_ui(ui, wallet, modal, cb); - }); - } - TOR_SETTINGS_MODAL => { - Modal::ui(ui.ctx(), |ui, modal| { - self.tor_settings_modal_ui(ui, wallet, modal, cb); - }); - } - QR_ADDRESS_MODAL => { - Modal::ui(ui.ctx(), |ui, modal| { - self.qr_address_modal_ui(ui, modal, cb); - }); - } - _ => {} - } - } - } - } - - /// Draw Tor transport content. - fn tor_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { - let data = wallet.get_data().unwrap(); - - // Draw header content. - self.tor_header_ui(ui, wallet); - - // Draw receive info content. - if wallet.slatepack_address().is_some() { - self.tor_receive_ui(ui, wallet, &data, cb); - } - - // Draw send content. - if data.info.amount_currently_spendable > 0 && wallet.foreign_api_port().is_some() { - self.tor_send_ui(ui, cb); - } - } - - /// Draw Tor transport header content. - fn tor_header_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(78.0); - - // Draw round background. - let bg_rect = rect.clone(); - let item_rounding = View::item_rounding(0, 2, false); - ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke()); - - ui.vertical(|ui| { - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - // Draw button to setup Tor transport. - let button_rounding = View::item_rounding(0, 2, true); - View::item_button(ui, button_rounding, GEAR_SIX, None, || { - self.show_tor_settings_modal(); - }); - - // Draw button to enable/disable Tor listener for current wallet. - let service_id = &wallet.identifier(); - if !Tor::is_service_starting(service_id) && wallet.foreign_api_port().is_some() { - if !Tor::is_service_running(service_id) { - View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || { - if let Ok(key) = wallet.secret_key() { - let api_port = wallet.foreign_api_port().unwrap(); - Tor::start_service(api_port, key, service_id); - } - }); - } else { - View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || { - Tor::stop_service(service_id); - }); - } - } - - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - ui.add_space(3.0); - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.add_space(1.0); - ui.label(RichText::new(t!("transport.tor_network")) - .size(18.0) - .color(Colors::title(false))); - }); - - // Setup Tor status text. - let is_running = Tor::is_service_running(service_id); - let is_starting = Tor::is_service_starting(service_id); - let has_error = Tor::is_service_failed(service_id); - let (icon, text) = if wallet.foreign_api_port().is_none() { - (DOTS_THREE_CIRCLE, t!("wallets.loading")) - } else if is_starting { - (DOTS_THREE_CIRCLE, t!("transport.connecting")) - } else if has_error { - (WARNING_CIRCLE, t!("transport.conn_error")) - } else if is_running { - (CHECK_CIRCLE, t!("transport.connected")) - } else { - (X_CIRCLE, t!("transport.disconnected")) - }; - let status_text = format!("{} {}", icon, text); - ui.label(RichText::new(status_text).size(15.0).color(Colors::text(false))); - ui.add_space(1.0); - - // Setup bridges status text. - let bridge = TorConfig::get_bridge(); - let bridges_text = match &bridge { - None => { - format!("{} {}", SHIELD_SLASH, t!("transport.bridges_disabled")) - } - Some(b) => { - let name = b.protocol_name().to_uppercase(); - format!("{} {}", - SHIELD_CHECKERED, - t!("transport.bridge_name", "b" = name)) - } - }; - - ui.label(RichText::new(bridges_text).size(15.0).color(Colors::gray())); - }); - }); - }); - }); - } - - /// Show Tor transport settings [`Modal`]. - fn show_tor_settings_modal(&mut self) { - self.tor_settings_changed = false; - // Show Tor settings modal. - Modal::new(TOR_SETTINGS_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("transport.tor_settings")) - .closeable(false) - .show(); - } - - /// Draw Tor transport settings [`Modal`] content. - fn tor_settings_modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - - // Draw QR code scanner content if requested. - if self.show_bridge_scan { - let mut on_stop = |content: &mut CameraContent| { - cb.stop_camera(); - content.clear_state(); - modal.enable_closing(); - self.show_bridge_scan = false; - }; - - if let Some(result) = self.bridge_qr_scan_content.qr_scan_result() { - self.bridge_conn_line_edit = result.text(); - on_stop(&mut self.bridge_qr_scan_content); - cb.show_keyboard(); - } else { - self.bridge_qr_scan_content.ui(ui, cb); - ui.add_space(12.0); - - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - // Show buttons to close modal or come back to sending input. - ui.columns(2, |cols| { - cols[0].vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - on_stop(&mut self.bridge_qr_scan_content); - modal.close(); - }); - }); - cols[1].vertical_centered_justified(|ui| { - View::button(ui, t!("back"), Colors::white_or_black(false), || { - on_stop(&mut self.bridge_qr_scan_content); - }); - }); - }); - ui.add_space(6.0); - } - return; - } - - // Do not show bridges setup on Android. - let os = OperatingSystem::from_target_os(); - let show_bridges = os != OperatingSystem::Android; - if show_bridges { - let bridge = TorConfig::get_bridge(); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("transport.bridges_desc")) - .size(17.0) - .color(Colors::inactive_text())); - - // Draw checkbox to enable/disable bridges. - View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || { - // Save value. - let value = if bridge.is_some() { - None - } else { - let default_bridge = TorConfig::get_obfs4(); - self.bridge_bin_path_edit = default_bridge.binary_path(); - self.bridge_conn_line_edit = default_bridge.connection_line(); - Some(default_bridge) - }; - TorConfig::save_bridge(value); - self.tor_settings_changed = true; - }); - }); - - // Draw bridges selection and path. - if bridge.is_some() { - let current_bridge = bridge.unwrap(); - let mut bridge = current_bridge.clone(); - - ui.add_space(6.0); - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - // Draw Obfs4 bridge selector. - let obfs4 = TorConfig::get_obfs4(); - let name = obfs4.protocol_name().to_uppercase(); - View::radio_value(ui, &mut bridge, obfs4, name); - }); - columns[1].vertical_centered(|ui| { - // Draw Snowflake bridge selector. - let snowflake = TorConfig::get_snowflake(); - let name = snowflake.protocol_name().to_uppercase(); - View::radio_value(ui, &mut bridge, snowflake, name); - }); - }); - ui.add_space(12.0); - - // Check if bridge type was changed to save. - if current_bridge != bridge { - self.tor_settings_changed = true; - TorConfig::save_bridge(Some(bridge.clone())); - self.bridge_bin_path_edit = bridge.binary_path(); - self.bridge_conn_line_edit = bridge.connection_line(); - } - - // Draw binary path text edit. - let bin_edit_id = Id::from(modal.id) - .with(wallet.get_config().id) - .with("_bin_edit"); - let mut bin_edit_opts = TextEditOptions::new(bin_edit_id) - .paste() - .no_focus(); - let bin_edit_before = self.bridge_bin_path_edit.clone(); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("transport.bin_file")) - .size(17.0) - .color(Colors::inactive_text())); - ui.add_space(6.0); - View::text_edit(ui, cb, &mut self.bridge_bin_path_edit, &mut bin_edit_opts); - ui.add_space(6.0); - }); - - // Draw connection line text edit. - let conn_edit_before = self.bridge_conn_line_edit.clone(); - let conn_edit_id = Id::from(modal.id) - .with(wallet.get_config().id) - .with("_conn_edit"); - let mut conn_edit_opts = TextEditOptions::new(conn_edit_id) - .paste() - .no_focus() - .scan_qr(); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("transport.conn_line")) - .size(17.0) - .color(Colors::inactive_text())); - ui.add_space(6.0); - View::text_edit(ui, cb, &mut self.bridge_conn_line_edit, &mut conn_edit_opts); - // Check if scan button was pressed. - if conn_edit_opts.scan_pressed { - cb.hide_keyboard(); - modal.disable_closing(); - conn_edit_opts.scan_pressed = false; - self.show_bridge_scan = true; - } - }); - - // Check if bin path or connection line text was changed to save bridge. - if conn_edit_before != self.bridge_conn_line_edit || - bin_edit_before != self.bridge_bin_path_edit { - let bin_path = self.bridge_bin_path_edit.trim().to_string(); - let conn_line = self.bridge_conn_line_edit.trim().to_string(); - let b = match bridge { - TorBridge::Snowflake(_, _) => { - TorBridge::Snowflake(bin_path, conn_line) - }, - TorBridge::Obfs4(_, _) => { - TorBridge::Obfs4(bin_path, conn_line) - } - }; - TorConfig::save_bridge(Some(b)); - self.tor_settings_changed = true; - } - - ui.add_space(2.0); - } - - ui.add_space(6.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); - } - - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("transport.tor_autorun_desc")) - .size(17.0) - .color(Colors::inactive_text())); - - // Show Tor service autorun checkbox. - let autorun = wallet.auto_start_tor_listener(); - View::checkbox(ui, autorun, t!("network.autorun"), || { - wallet.update_auto_start_tor_listener(!autorun); - }); - }); - ui.add_space(6.0); - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - if self.tor_settings_changed { - self.tor_settings_changed = false; - // Restart running service or rebuild client. - let service_id = &wallet.identifier(); - if Tor::is_service_running(service_id) { - if let Ok(key) = wallet.secret_key() { - let api_port = wallet.foreign_api_port().unwrap(); - Tor::restart_service(api_port, key, service_id); - } - } else { - Tor::rebuild_client(); - } - } - modal.close(); - }); - }); - ui.add_space(6.0); - } - - /// Draw Tor receive content. - fn tor_receive_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - data: &WalletData, - cb: &dyn PlatformCallbacks) { - let slatepack_addr = wallet.slatepack_address().unwrap(); - let service_id = &wallet.identifier(); - let can_send = data.info.amount_currently_spendable > 0; - - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(52.0); - - // Draw round background. - let bg_rect = rect.clone(); - let item_rounding = if can_send { - View::item_rounding(1, 3, false) - } else { - View::item_rounding(1, 2, false) - }; - ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke()); - - ui.vertical(|ui| { - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - // Draw button to setup Tor transport. - let button_rounding = if can_send { - View::item_rounding(1, 3, true) - } else { - View::item_rounding(1, 2, true) - }; - View::item_button(ui, button_rounding, QR_CODE, None, || { - // Show QR code image address modal. - self.qr_address_content.clear_state(); - Modal::new(QR_ADDRESS_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_mining.address")) - .show(); - }); - - // Show button to enable/disable Tor listener for current wallet. - View::item_button(ui, Rounding::default(), COPY, None, || { - cb.copy_string_to_buffer(slatepack_addr.clone()); - }); - - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - ui.add_space(3.0); - - // Show wallet Slatepack address. - let address_color = if Tor::is_service_starting(service_id) || - wallet.foreign_api_port().is_none() { - Colors::inactive_text() - } else if Tor::is_service_running(service_id) { - Colors::green() - } else { - Colors::red() - }; - View::ellipsize_text(ui, slatepack_addr, 15.0, address_color); - - let address_label = format!("{} {}", - GLOBE_SIMPLE, - t!("network_mining.address")); - ui.label(RichText::new(address_label).size(15.0).color(Colors::gray())); - }); - }); - }); - }); - } - - /// Draw QR code image address [`Modal`] content. - fn qr_address_modal_ui(&mut self, ui: &mut egui::Ui, m: &Modal, cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - - // Draw QR code content. - let text = self.qr_address_content.text.clone(); - self.qr_address_content.ui(ui, text.clone(), cb); - - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.qr_address_content.clear_state(); - m.close(); - }); - }); - ui.add_space(6.0); - } - - /// Draw Tor send content. - fn tor_send_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(55.0); - - // Draw round background. - let bg_rect = rect.clone(); - let item_rounding = View::item_rounding(1, 2, false); - ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke()); - - ui.vertical(|ui| { - ui.allocate_ui_with_layout(rect.size(), Layout::top_down(Align::Center), |ui| { - ui.add_space(7.0); - // Draw button to open sending modal. - let send_text = format!("{} {}", EXPORT, t!("wallets.send")); - View::button(ui, send_text, Colors::white_or_black(false), || { - self.show_send_tor_modal(cb, None); - }); - }); - }); - } - - /// Show [`Modal`] to send over Tor. - pub fn show_send_tor_modal(&mut self, cb: &dyn PlatformCallbacks, address: Option) { - { - let mut w_send_err = self.tor_send_error.write(); - *w_send_err = false; - let mut w_sending = self.tor_sending.write(); - *w_sending = false; - let mut w_success = self.tor_success.write(); - *w_success = false; - } - self.modal_just_opened = true; - self.amount_edit = "".to_string(); - self.address_edit = address.unwrap_or("".to_string()); - self.address_error = false; - // Show modal. - Modal::new(SEND_TOR_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.send")) - .show(); - cb.show_keyboard(); - } - - /// Check if error occurred during sending over Tor at [`Modal`]. - fn has_tor_send_error(&self) -> bool { - let r_send_err = self.tor_send_error.read(); - r_send_err.clone() - } - - /// Check if transaction is sending over Tor to show progress at [`Modal`]. - fn tor_sending(&self) -> bool { - let r_sending = self.tor_sending.read(); - r_sending.clone() - } - - /// Check if transaction sent over Tor with success at [`Modal`]. - fn tor_success(&self) -> bool { - let r_success = self.tor_success.read(); - r_success.clone() - } - - /// Draw amount input [`Modal`] content to send over Tor. - fn send_tor_modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - - let has_send_err = self.has_tor_send_error(); - let sending = self.tor_sending(); - if !has_send_err && !sending { - // Draw QR code scanner content if requested. - if self.show_address_scan { - let mut on_stop = |content: &mut CameraContent| { - cb.stop_camera(); - content.clear_state(); - modal.enable_closing(); - self.show_address_scan = false; - }; - - if let Some(result) = self.address_scan_content.qr_scan_result() { - self.address_edit = result.text(); - self.modal_just_opened = true; - on_stop(&mut self.address_scan_content); - cb.show_keyboard(); - } else { - self.address_scan_content.ui(ui, cb); - ui.add_space(6.0); - - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - // Show buttons to close modal or come back to sending input. - ui.columns(2, |cols| { - cols[0].vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - on_stop(&mut self.address_scan_content); - modal.close(); - }); - }); - cols[1].vertical_centered_justified(|ui| { - View::button(ui, t!("back"), Colors::white_or_black(false), || { - self.modal_just_opened = true; - on_stop(&mut self.address_scan_content); - cb.show_keyboard(); - }); - }); - }); - ui.add_space(6.0); - } - return; - } - - ui.vertical_centered(|ui| { - let data = wallet.get_data().unwrap(); - let amount = amount_to_hr_string(data.info.amount_currently_spendable, true); - let enter_text = t!("wallets.enter_amount_send","amount" => amount); - ui.label(RichText::new(enter_text) - .size(17.0) - .color(Colors::gray())); - }); - ui.add_space(8.0); - - // Draw amount text edit. - let amount_edit_id = Id::from(modal.id).with("amount").with(wallet.get_config().id); - let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center().no_focus(); - let amount_edit_before = self.amount_edit.clone(); - if self.modal_just_opened { - self.modal_just_opened = false; - amount_edit_opts.focus = true; - } - View::text_edit(ui, cb, &mut self.amount_edit, &mut amount_edit_opts); - ui.add_space(8.0); - - // Check value if input was changed. - if amount_edit_before != self.amount_edit { - if !self.amount_edit.is_empty() { - // Trim text, replace "," by "." and parse amount. - self.amount_edit = self.amount_edit.trim().replace(",", "."); - match amount_from_hr_string(self.amount_edit.as_str()) { - Ok(a) => { - if !self.amount_edit.contains(".") { - // To avoid input of several "0". - if a == 0 { - self.amount_edit = "0".to_string(); - return; - } - } else { - // Check input after ".". - let parts = self.amount_edit.split(".").collect::>(); - if parts.len() == 2 && parts[1].len() > 9 { - self.amount_edit = amount_edit_before; - return; - } - } - - // Do not input amount more than balance in sending. - let b = wallet.get_data().unwrap().info.amount_currently_spendable; - if b < a { - self.amount_edit = amount_edit_before; - } - } - Err(_) => { - self.amount_edit = amount_edit_before; - } - } - } - } - - // Show address error or input description. - ui.vertical_centered(|ui| { - if self.address_error { - ui.label(RichText::new(t!("transport.incorrect_addr_err")) - .size(17.0) - .color(Colors::red())); - } else { - ui.label(RichText::new(t!("transport.receiver_address")) - .size(17.0) - .color(Colors::gray())); - } - }); - ui.add_space(6.0); - - // Draw address text edit. - let addr_edit_before = self.address_edit.clone(); - let address_edit_id = Id::from(modal.id).with("address").with(wallet.get_config().id); - let mut address_edit_opts = TextEditOptions::new(address_edit_id) - .paste() - .no_focus() - .scan_qr(); - View::text_edit(ui, cb, &mut self.address_edit, &mut address_edit_opts); - // Check if scan button was pressed. - if address_edit_opts.scan_pressed { - cb.hide_keyboard(); - modal.disable_closing(); - address_edit_opts.scan_pressed = false; - self.show_address_scan = true; - } - ui.add_space(12.0); - - // Check value if input was changed. - if addr_edit_before != self.address_edit { - self.address_error = false; - } - - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - self.amount_edit = "".to_string(); - self.address_edit = "".to_string(); - cb.hide_keyboard(); - modal.close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("continue"), Colors::white_or_black(false), || { - if self.amount_edit.is_empty() { - return; - } - - // Check entered address. - let addr_str = self.address_edit.as_str(); - if let Ok(addr) = SlatepackAddress::try_from(addr_str) { - // Parse amount and send over Tor. - if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) { - cb.hide_keyboard(); - modal.disable_closing(); - let mut w_sending = self.tor_sending.write(); - *w_sending = true; - { - let send_error = self.tor_send_error.clone(); - let send_success = self.tor_success.clone(); - let mut wallet = wallet.clone(); - thread::spawn(move || { - let runtime = TokioNativeTlsRuntime::create().unwrap(); - runtime - .block_on(async { - if wallet.send_tor(a, &addr) - .await - .is_some() { - let mut w_send_success = send_success.write(); - *w_send_success = true; - } else { - let mut w_send_error = send_error.write(); - *w_send_error = true; - } - }); - }); - } - } - } else { - self.address_error = true; - } - }); - }); - }); - ui.add_space(6.0); - } else if has_send_err { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("transport.tor_send_error")) - .size(17.0) - .color(Colors::red())); - }); - ui.add_space(12.0); - - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - self.amount_edit = "".to_string(); - self.address_edit = "".to_string(); - cb.hide_keyboard(); - modal.close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("repeat"), Colors::white_or_black(false), || { - // Parse amount and send over Tor. - if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) { - let mut w_send_error = self.tor_send_error.write(); - *w_send_error = false; - let mut w_sending = self.tor_sending.write(); - *w_sending = true; - { - let addr_text = self.address_edit.clone(); - let send_error = self.tor_send_error.clone(); - let send_success = self.tor_success.clone(); - let mut wallet = wallet.clone(); - thread::spawn(move || { - let runtime = TokioNativeTlsRuntime::create().unwrap(); - runtime - .block_on(async { - let addr_str = addr_text.as_str(); - let addr = &SlatepackAddress::try_from(addr_str) - .unwrap(); - if wallet.send_tor(a, &addr) - .await - .is_some() { - let mut w_send_success = send_success.write(); - *w_send_success = true; - } else { - let mut w_send_error = send_error.write(); - *w_send_error = true; - } - }); - }); - } - } - }); - }); - }); - ui.add_space(6.0); - } else { - ui.add_space(16.0); - ui.vertical_centered(|ui| { - View::small_loading_spinner(ui); - ui.add_space(12.0); - ui.label(RichText::new(t!("transport.tor_sending", "amount" => self.amount_edit)) - .size(17.0) - .color(Colors::gray())); - }); - ui.add_space(10.0); - - // Close modal on success sending. - if self.tor_success() { - modal.close(); - } - } - } -} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/transport/content.rs b/src/gui/views/wallets/wallet/transport/content.rs new file mode 100644 index 0000000..4876d1d --- /dev/null +++ b/src/gui/views/wallets/wallet/transport/content.rs @@ -0,0 +1,397 @@ +// Copyright 2023 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea}; +use egui::scroll_area::ScrollBarVisibility; + +use crate::gui::Colors; +use crate::gui::icons::{CHECK_CIRCLE, COPY, DOTS_THREE_CIRCLE, EXPORT, GEAR_SIX, GLOBE_SIMPLE, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, X_CIRCLE}; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{Modal, QrCodeContent, Content, View}; +use crate::gui::views::types::ModalPosition; +use crate::gui::views::wallets::wallet::transport::send::TransportSendModal; +use crate::gui::views::wallets::wallet::transport::settings::TransportSettingsModal; +use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType}; +use crate::gui::views::wallets::wallet::WalletContent; +use crate::tor::{Tor, TorConfig}; +use crate::wallet::types::WalletData; +use crate::wallet::Wallet; + +/// Wallet transport tab content. +pub struct WalletTransport { + /// Sending [`Modal`] content. + send_modal_content: Option, + + /// QR code address image [`Modal`] content. + qr_address_content: Option, + + /// Tor settings [`Modal`] content. + settings_modal_content: Option, +} + +impl WalletTab for WalletTransport { + fn get_type(&self) -> WalletTabType { + WalletTabType::Transport + } + + fn ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + cb: &dyn PlatformCallbacks) { + if WalletContent::sync_ui(ui, wallet) { + return; + } + + // Show modal content for this ui container. + self.modal_content_ui(ui, wallet, cb); + + // Show transport content panel. + egui::CentralPanel::default() + .frame(egui::Frame { + stroke: View::item_stroke(), + fill: Colors::white_or_black(false), + inner_margin: Margin { + left: View::far_left_inset_margin(ui) + 4.0, + right: View::get_right_inset() + 4.0, + top: 3.0, + bottom: 4.0, + }, + ..Default::default() + }) + .show_inside(ui, |ui| { + ScrollArea::vertical() + .id_source(Id::from("wallet_transport").with(wallet.get_config().id)) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.vertical_centered(|ui| { + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + self.ui(ui, wallet, cb); + }); + }); + }); + }); + } +} + +/// Identifier for [`Modal`] to send amount over Tor. +const SEND_TOR_MODAL: &'static str = "send_tor_modal"; + +/// Identifier for [`Modal`] to setup Tor service. +const TOR_SETTINGS_MODAL: &'static str = "tor_settings_modal"; + +/// Identifier for [`Modal`] to show QR code address image. +const QR_ADDRESS_MODAL: &'static str = "qr_address_modal"; + +impl Default for WalletTransport { + fn default() -> Self { + Self { + send_modal_content: None, + qr_address_content: None, + settings_modal_content: None, + } + } +} + +impl WalletTransport { + /// Draw wallet transport content. + pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { + ui.add_space(3.0); + ui.label(RichText::new(t!("transport.desc")) + .size(16.0) + .color(Colors::inactive_text())); + ui.add_space(7.0); + + // Draw Tor transport content. + self.tor_ui(ui, wallet, cb); + } + + /// Draw [`Modal`] content for this ui container. + fn modal_content_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + cb: &dyn PlatformCallbacks) { + match Modal::opened() { + None => {} + Some(id) => { + match id { + SEND_TOR_MODAL => { + if let Some(content) = self.send_modal_content.as_mut() { + Modal::ui(ui.ctx(), |ui, modal| { + content.ui(ui, wallet, modal, cb); + }); + } + } + TOR_SETTINGS_MODAL => { + if let Some(content) = self.settings_modal_content.as_mut() { + Modal::ui(ui.ctx(), |ui, modal| { + content.ui(ui, wallet, modal, cb); + }); + } + } + QR_ADDRESS_MODAL => { + Modal::ui(ui.ctx(), |ui, modal| { + self.qr_address_modal_ui(ui, modal, cb); + }); + } + _ => {} + } + } + } + } + + /// Draw Tor transport content. + fn tor_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { + let data = wallet.get_data().unwrap(); + + // Draw header content. + self.tor_header_ui(ui, wallet); + + // Draw receive info content. + if wallet.slatepack_address().is_some() { + self.tor_receive_ui(ui, wallet, &data, cb); + } + + // Draw send content. + let service_id = &wallet.identifier(); + if data.info.amount_currently_spendable > 0 && wallet.foreign_api_port().is_some() && + !Tor::is_service_starting(service_id) { + self.tor_send_ui(ui, cb); + } + } + + /// Draw Tor transport header content. + fn tor_header_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(78.0); + + // Draw round background. + let bg_rect = rect.clone(); + let item_rounding = View::item_rounding(0, 2, false); + ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke()); + + ui.vertical(|ui| { + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + // Draw button to setup Tor transport. + let button_rounding = View::item_rounding(0, 2, true); + View::item_button(ui, button_rounding, GEAR_SIX, None, || { + self.settings_modal_content = Some(TransportSettingsModal::default()); + // Show Tor settings modal. + Modal::new(TOR_SETTINGS_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("transport.tor_settings")) + .closeable(false) + .show(); + }); + + // Draw button to enable/disable Tor listener for current wallet. + let service_id = &wallet.identifier(); + if !Tor::is_service_starting(service_id) && wallet.foreign_api_port().is_some() { + if !Tor::is_service_running(service_id) { + View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || { + if let Ok(key) = wallet.secret_key() { + let api_port = wallet.foreign_api_port().unwrap(); + Tor::start_service(api_port, key, service_id); + } + }); + } else { + View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || { + Tor::stop_service(service_id); + }); + } + } + + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(3.0); + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.add_space(1.0); + ui.label(RichText::new(t!("transport.tor_network")) + .size(18.0) + .color(Colors::title(false))); + }); + + // Setup Tor status text. + let is_running = Tor::is_service_running(service_id); + let is_starting = Tor::is_service_starting(service_id); + let has_error = Tor::is_service_failed(service_id); + let (icon, text) = if wallet.foreign_api_port().is_none() { + (DOTS_THREE_CIRCLE, t!("wallets.loading")) + } else if is_starting { + (DOTS_THREE_CIRCLE, t!("transport.connecting")) + } else if has_error { + (WARNING_CIRCLE, t!("transport.conn_error")) + } else if is_running { + (CHECK_CIRCLE, t!("transport.connected")) + } else { + (X_CIRCLE, t!("transport.disconnected")) + }; + let status_text = format!("{} {}", icon, text); + ui.label(RichText::new(status_text).size(15.0).color(Colors::text(false))); + ui.add_space(1.0); + + // Setup bridges status text. + let bridge = TorConfig::get_bridge(); + let bridges_text = match &bridge { + None => { + format!("{} {}", SHIELD_SLASH, t!("transport.bridges_disabled")) + } + Some(b) => { + let name = b.protocol_name().to_uppercase(); + format!("{} {}", + SHIELD_CHECKERED, + t!("transport.bridge_name", "b" = name)) + } + }; + + ui.label(RichText::new(bridges_text).size(15.0).color(Colors::gray())); + }); + }); + }); + }); + } + + /// Draw Tor receive content. + fn tor_receive_ui(&mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + data: &WalletData, + cb: &dyn PlatformCallbacks) { + let addr = wallet.slatepack_address().unwrap(); + let service_id = &wallet.identifier(); + let can_send = data.info.amount_currently_spendable > 0; + + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(52.0); + + // Draw round background. + let bg_rect = rect.clone(); + let item_rounding = if can_send { + View::item_rounding(1, 3, false) + } else { + View::item_rounding(1, 2, false) + }; + ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke()); + + ui.vertical(|ui| { + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + // Draw button to setup Tor transport. + let button_rounding = if can_send { + View::item_rounding(1, 3, true) + } else { + View::item_rounding(1, 2, true) + }; + View::item_button(ui, button_rounding, QR_CODE, None, || { + // Show QR code image address modal. + self.qr_address_content = Some(QrCodeContent::new(addr.clone(), false)); + Modal::new(QR_ADDRESS_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_mining.address")) + .show(); + }); + + // Show button to enable/disable Tor listener for current wallet. + View::item_button(ui, Rounding::default(), COPY, None, || { + cb.copy_string_to_buffer(addr.clone()); + }); + + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(3.0); + + // Show wallet Slatepack address. + let address_color = if Tor::is_service_starting(service_id) || + wallet.foreign_api_port().is_none() { + Colors::inactive_text() + } else if Tor::is_service_running(service_id) { + Colors::green() + } else { + Colors::red() + }; + View::ellipsize_text(ui, addr, 15.0, address_color); + + let address_label = format!("{} {}", + GLOBE_SIMPLE, + t!("network_mining.address")); + ui.label(RichText::new(address_label).size(15.0).color(Colors::gray())); + }); + }); + }); + }); + } + + /// Draw QR code image address [`Modal`] content. + fn qr_address_modal_ui(&mut self, + ui: &mut egui::Ui, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + + // Draw QR code content. + if let Some(content) = self.qr_address_content.as_mut() { + content.ui(ui, cb); + } else { + modal.close(); + return; + } + + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + self.qr_address_content = None; + modal.close(); + }); + }); + ui.add_space(6.0); + } + + /// Draw Tor send content. + fn tor_send_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(55.0); + + // Draw round background. + let bg_rect = rect.clone(); + let item_rounding = View::item_rounding(1, 2, false); + ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke()); + + ui.vertical(|ui| { + ui.allocate_ui_with_layout(rect.size(), Layout::top_down(Align::Center), |ui| { + ui.add_space(7.0); + // Draw button to open sending modal. + let send_text = format!("{} {}", EXPORT, t!("wallets.send")); + View::button(ui, send_text, Colors::white_or_black(false), || { + self.show_send_tor_modal(cb, None); + }); + }); + }); + } + + /// Show [`Modal`] to send over Tor. + pub fn show_send_tor_modal(&mut self, cb: &dyn PlatformCallbacks, address: Option) { + self.send_modal_content = Some(TransportSendModal::new(address)); + // Show modal. + Modal::new(SEND_TOR_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.send")) + .show(); + cb.show_keyboard(); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/transport/mod.rs b/src/gui/views/wallets/wallet/transport/mod.rs new file mode 100644 index 0000000..845225a --- /dev/null +++ b/src/gui/views/wallets/wallet/transport/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2024 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod content; +pub use content::*; + +mod send; +mod settings; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/transport/send.rs b/src/gui/views/wallets/wallet/transport/send.rs new file mode 100644 index 0000000..76e08b3 --- /dev/null +++ b/src/gui/views/wallets/wallet/transport/send.rs @@ -0,0 +1,357 @@ +// Copyright 2024 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; +use std::thread; +use egui::{Id, RichText}; +use grin_core::core::{amount_from_hr_string, amount_to_hr_string}; +use grin_wallet_libwallet::{Error, SlatepackAddress}; +use parking_lot::RwLock; +use tor_rtcompat::BlockOn; +use tor_rtcompat::tokio::TokioNativeTlsRuntime; +use crate::gui::Colors; +use crate::gui::platform::PlatformCallbacks; + +use crate::gui::views::{CameraContent, Modal, View}; +use crate::gui::views::types::TextEditOptions; +use crate::gui::views::wallets::wallet::WalletTransactionModal; +use crate::wallet::types::WalletTransaction; +use crate::wallet::Wallet; + +/// Transport sending [`Modal`] content. +pub struct TransportSendModal { + /// Flag to focus on first input field after opening. + first_draw: bool, + + /// Flag to check if transaction is sending to show progress. + sending: bool, + /// Flag to check if there is an error to repeat. + error: bool, + /// Transaction result. + send_result: Arc>>>, + + /// Entered amount value. + amount_edit: String, + /// Entered address value. + address_edit: String, + /// Flag to check if entered address is incorrect. + address_error: bool, + + /// Address QR code scanner content. + address_scan_content: Option, + + /// Transaction information content. + tx_info_content: Option, +} + +impl TransportSendModal { + /// Create new instance from provided address. + pub fn new(addr: Option) -> Self { + Self { + first_draw: true, + sending: false, + error: false, + send_result: Arc::new(RwLock::new(None)), + amount_edit: "".to_string(), + address_edit: addr.unwrap_or("".to_string()), + address_error: false, + address_scan_content: None, + tx_info_content: None, + } + } + + /// Draw [`Modal`] content. + pub fn ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + // Draw transaction information on request result. + if let Some(tx) = self.tx_info_content.as_mut() { + tx.ui(ui, wallet, modal, cb); + return; + } + + // Draw sending content, progress or an error. + if self.sending { + self.progress_ui(ui, wallet); + } else if self.error { + self.error_ui(ui, wallet, modal, cb); + } else { + self.content_ui(ui, wallet, modal, cb); + } + } + + /// Draw content to send. + fn content_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal, + cb: &dyn PlatformCallbacks) { + // Draw QR code scanner content if requested. + if let Some(scanner) = self.address_scan_content.as_mut() { + let mut on_stop = || { + self.first_draw = true; + cb.stop_camera(); + modal.enable_closing(); + }; + + if let Some(result) = scanner.qr_scan_result() { + self.address_edit = result.text(); + on_stop(); + self.address_scan_content = None; + cb.show_keyboard(); + } else { + scanner.ui(ui, cb); + ui.add_space(6.0); + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + // Show buttons to close modal or come back to sending input. + ui.columns(2, |cols| { + cols[0].vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + on_stop(); + self.address_scan_content = None; + modal.close(); + }); + }); + cols[1].vertical_centered_justified(|ui| { + View::button(ui, t!("back"), Colors::white_or_black(false), || { + on_stop(); + self.address_scan_content = None; + cb.show_keyboard(); + }); + }); + }); + ui.add_space(6.0); + } + return; + } + + ui.vertical_centered(|ui| { + let data = wallet.get_data().unwrap(); + let amount = amount_to_hr_string(data.info.amount_currently_spendable, true); + let enter_text = t!("wallets.enter_amount_send","amount" => amount); + ui.label(RichText::new(enter_text) + .size(17.0) + .color(Colors::gray())); + }); + ui.add_space(8.0); + + // Draw amount text edit. + let amount_edit_id = Id::from(modal.id).with("amount").with(wallet.get_config().id); + let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center().no_focus(); + let amount_edit_before = self.amount_edit.clone(); + if self.first_draw { + self.first_draw = false; + amount_edit_opts.focus = true; + } + View::text_edit(ui, cb, &mut self.amount_edit, &mut amount_edit_opts); + ui.add_space(8.0); + + // Check value if input was changed. + if amount_edit_before != self.amount_edit { + if !self.amount_edit.is_empty() { + // Trim text, replace "," by "." and parse amount. + self.amount_edit = self.amount_edit.trim().replace(",", "."); + match amount_from_hr_string(self.amount_edit.as_str()) { + Ok(a) => { + if !self.amount_edit.contains(".") { + // To avoid input of several "0". + if a == 0 { + self.amount_edit = "0".to_string(); + return; + } + } else { + // Check input after ".". + let parts = self.amount_edit.split(".").collect::>(); + if parts.len() == 2 && parts[1].len() > 9 { + self.amount_edit = amount_edit_before; + return; + } + } + + // Do not input amount more than balance in sending. + let b = wallet.get_data().unwrap().info.amount_currently_spendable; + if b < a { + self.amount_edit = amount_edit_before; + } + } + Err(_) => { + self.amount_edit = amount_edit_before; + } + } + } + } + + // Show address error or input description. + ui.vertical_centered(|ui| { + if self.address_error { + ui.label(RichText::new(t!("transport.incorrect_addr_err")) + .size(17.0) + .color(Colors::red())); + } else { + ui.label(RichText::new(t!("transport.receiver_address")) + .size(17.0) + .color(Colors::gray())); + } + }); + ui.add_space(6.0); + + // Draw address text edit. + let addr_edit_before = self.address_edit.clone(); + let address_edit_id = Id::from(modal.id).with("_address").with(wallet.get_config().id); + let mut address_edit_opts = TextEditOptions::new(address_edit_id) + .paste() + .no_focus() + .scan_qr(); + View::text_edit(ui, cb, &mut self.address_edit, &mut address_edit_opts); + // Check if scan button was pressed. + if address_edit_opts.scan_pressed { + cb.hide_keyboard(); + modal.disable_closing(); + address_edit_opts.scan_pressed = false; + self.address_scan_content = Some(CameraContent::default()); + } + ui.add_space(12.0); + + // Check value if input was changed. + if addr_edit_before != self.address_edit { + self.address_error = false; + } + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + self.close(modal, cb); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("continue"), Colors::white_or_black(false), || { + self.send(wallet, modal, cb); + }); + }); + }); + ui.add_space(6.0); + } + + /// Draw error content. + fn error_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal, cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("transport.tor_send_error")) + .size(17.0) + .color(Colors::red())); + }); + ui.add_space(12.0); + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + self.close(modal, cb); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("repeat"), Colors::white_or_black(false), || { + self.send(wallet, modal, cb); + }); + }); + }); + ui.add_space(6.0); + } + + /// Close modal and clear data. + fn close(&mut self, modal: &Modal, cb: &dyn PlatformCallbacks) { + self.amount_edit = "".to_string(); + self.address_edit = "".to_string(); + + let mut w_res = self.send_result.write(); + *w_res = None; + + self.tx_info_content = None; + self.address_scan_content = None; + + cb.hide_keyboard(); + modal.close(); + } + + /// Send entered amount to address. + fn send(&mut self, wallet: &Wallet, modal: &Modal, cb: &dyn PlatformCallbacks) { + if self.amount_edit.is_empty() { + return; + } + let addr_str = self.address_edit.as_str(); + if let Ok(addr) = SlatepackAddress::try_from(addr_str) { + if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) { + cb.hide_keyboard(); + modal.disable_closing(); + // Send amount over Tor. + let mut wallet = wallet.clone(); + let res = self.send_result.clone(); + self.sending = true; + thread::spawn(move || { + let runtime = TokioNativeTlsRuntime::create().unwrap(); + runtime + .block_on(async { + let result = wallet.send_tor(a, &addr).await; + let mut w_res = res.write(); + *w_res = Some(result); + }); + }); + } + } else { + self.address_error = true; + } + } + + /// Draw sending progress content. + fn progress_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { + ui.add_space(16.0); + ui.vertical_centered(|ui| { + View::small_loading_spinner(ui); + ui.add_space(12.0); + ui.label(RichText::new(t!("transport.tor_sending", "amount" => self.amount_edit)) + .size(17.0) + .color(Colors::gray())); + }); + ui.add_space(10.0); + + // Check sending result. + let has_result = { + let r_result = self.send_result.read(); + r_result.is_some() + }; + if has_result { + { + let res = self.send_result.read().clone().unwrap(); + match res { + Ok(tx) => { + self.tx_info_content = Some(WalletTransactionModal::new(wallet, &tx, false)); + } + Err(_) => { + self.error = true; + } + } + } + let mut w_res = self.send_result.write(); + *w_res = None; + self.sending = false; + } + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/transport/settings.rs b/src/gui/views/wallets/wallet/transport/settings.rs new file mode 100644 index 0000000..377d2f0 --- /dev/null +++ b/src/gui/views/wallets/wallet/transport/settings.rs @@ -0,0 +1,258 @@ +// Copyright 2024 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use egui::os::OperatingSystem; +use egui::{Id, RichText}; + +use crate::gui::Colors; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{CameraContent, Modal, View}; +use crate::gui::views::types::TextEditOptions; +use crate::tor::{Tor, TorBridge, TorConfig}; +use crate::wallet::Wallet; + +/// Transport settings [`Modal`] content. +pub struct TransportSettingsModal { + /// Flag to check if Tor settings were changed. + settings_changed: bool, + + /// Tor bridge binary path edit text. + bridge_bin_path_edit: String, + /// Tor bridge connection line edit text. + bridge_conn_line_edit: String, + /// Address QR code scanner [`Modal`] content. + bridge_qr_scan_content: Option, +} + +impl Default for TransportSettingsModal { + fn default() -> Self { + // Setup Tor bridge binary path edit text. + let bridge = TorConfig::get_bridge(); + let (bin_path, conn_line) = if let Some(b) = bridge { + (b.binary_path(), b.connection_line()) + } else { + ("".to_string(), "".to_string()) + }; + Self { + settings_changed: false, + bridge_bin_path_edit: bin_path, + bridge_conn_line_edit: conn_line, + bridge_qr_scan_content: None, + } + } +} + +impl TransportSettingsModal { + pub fn ui(&mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + + // Draw QR code scanner content if requested. + if let Some(scanner) = self.bridge_qr_scan_content.as_mut() { + let on_stop = || { + cb.stop_camera(); + modal.enable_closing(); + }; + + if let Some(result) = scanner.qr_scan_result() { + self.bridge_conn_line_edit = result.text(); + on_stop(); + self.bridge_qr_scan_content = None; + cb.show_keyboard(); + } else { + scanner.ui(ui, cb); + ui.add_space(12.0); + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + // Show buttons to close modal or come back to sending input. + ui.columns(2, |cols| { + cols[0].vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + on_stop(); + self.bridge_qr_scan_content = None; + modal.close(); + }); + }); + cols[1].vertical_centered_justified(|ui| { + View::button(ui, t!("back"), Colors::white_or_black(false), || { + on_stop(); + self.bridge_qr_scan_content = None; + }); + }); + }); + ui.add_space(6.0); + } + return; + } + + // Do not show bridges setup on Android. + let os = OperatingSystem::from_target_os(); + let show_bridges = os != OperatingSystem::Android; + if show_bridges { + let bridge = TorConfig::get_bridge(); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("transport.bridges_desc")) + .size(17.0) + .color(Colors::inactive_text())); + + // Draw checkbox to enable/disable bridges. + View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || { + // Save value. + let value = if bridge.is_some() { + None + } else { + let default_bridge = TorConfig::get_obfs4(); + self.bridge_bin_path_edit = default_bridge.binary_path(); + self.bridge_conn_line_edit = default_bridge.connection_line(); + Some(default_bridge) + }; + TorConfig::save_bridge(value); + self.settings_changed = true; + }); + }); + + // Draw bridges selection and path. + if bridge.is_some() { + let current_bridge = bridge.unwrap(); + let mut bridge = current_bridge.clone(); + + ui.add_space(6.0); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + // Draw Obfs4 bridge selector. + let obfs4 = TorConfig::get_obfs4(); + let name = obfs4.protocol_name().to_uppercase(); + View::radio_value(ui, &mut bridge, obfs4, name); + }); + columns[1].vertical_centered(|ui| { + // Draw Snowflake bridge selector. + let snowflake = TorConfig::get_snowflake(); + let name = snowflake.protocol_name().to_uppercase(); + View::radio_value(ui, &mut bridge, snowflake, name); + }); + }); + ui.add_space(12.0); + + // Check if bridge type was changed to save. + if current_bridge != bridge { + self.settings_changed = true; + TorConfig::save_bridge(Some(bridge.clone())); + self.bridge_bin_path_edit = bridge.binary_path(); + self.bridge_conn_line_edit = bridge.connection_line(); + } + + // Draw binary path text edit. + let bin_edit_id = Id::from(modal.id) + .with(wallet.get_config().id) + .with("_bin_edit"); + let mut bin_edit_opts = TextEditOptions::new(bin_edit_id) + .paste() + .no_focus(); + let bin_edit_before = self.bridge_bin_path_edit.clone(); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("transport.bin_file")) + .size(17.0) + .color(Colors::inactive_text())); + ui.add_space(6.0); + View::text_edit(ui, cb, &mut self.bridge_bin_path_edit, &mut bin_edit_opts); + ui.add_space(6.0); + }); + + // Draw connection line text edit. + let conn_edit_before = self.bridge_conn_line_edit.clone(); + let conn_edit_id = Id::from(modal.id) + .with(wallet.get_config().id) + .with("_conn_edit"); + let mut conn_edit_opts = TextEditOptions::new(conn_edit_id) + .paste() + .no_focus() + .scan_qr(); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("transport.conn_line")) + .size(17.0) + .color(Colors::inactive_text())); + ui.add_space(6.0); + View::text_edit(ui, cb, &mut self.bridge_conn_line_edit, &mut conn_edit_opts); + // Check if scan button was pressed. + if conn_edit_opts.scan_pressed { + cb.hide_keyboard(); + modal.disable_closing(); + conn_edit_opts.scan_pressed = false; + self.bridge_qr_scan_content = Some(CameraContent::default()); + } + }); + + // Check if bin path or connection line text was changed to save bridge. + if conn_edit_before != self.bridge_conn_line_edit || + bin_edit_before != self.bridge_bin_path_edit { + let bin_path = self.bridge_bin_path_edit.trim().to_string(); + let conn_line = self.bridge_conn_line_edit.trim().to_string(); + let b = match bridge { + TorBridge::Snowflake(_, _) => { + TorBridge::Snowflake(bin_path, conn_line) + }, + TorBridge::Obfs4(_, _) => { + TorBridge::Obfs4(bin_path, conn_line) + } + }; + TorConfig::save_bridge(Some(b)); + self.settings_changed = true; + } + + ui.add_space(2.0); + } + + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); + } + + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("transport.tor_autorun_desc")) + .size(17.0) + .color(Colors::inactive_text())); + + // Show Tor service autorun checkbox. + let autorun = wallet.auto_start_tor_listener(); + View::checkbox(ui, autorun, t!("network.autorun"), || { + wallet.update_auto_start_tor_listener(!autorun); + }); + }); + ui.add_space(6.0); + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + if self.settings_changed { + self.settings_changed = false; + // Restart running service or rebuild client. + let service_id = &wallet.identifier(); + if Tor::is_service_running(service_id) { + if let Ok(key) = wallet.secret_key() { + let api_port = wallet.foreign_api_port().unwrap(); + Tor::restart_service(api_port, key, service_id); + } + } else { + Tor::rebuild_client(); + } + } + modal.close(); + }); + }); + ui.add_space(6.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 1977f9d..b1d98d0 100644 --- a/src/gui/views/wallets/wallet/txs/content.rs +++ b/src/gui/views/wallets/wallet/txs/content.rs @@ -19,7 +19,7 @@ use grin_core::core::amount_to_hr_string; use grin_wallet_libwallet::TxLogEntryType; use crate::gui::Colors; -use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, ARROW_CLOCKWISE, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, CHECK_CIRCLE, DOTS_THREE_CIRCLE, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE}; +use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, CHECK_CIRCLE, DOTS_THREE_CIRCLE, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, PullToRefresh, Content, View}; use crate::gui::views::types::ModalPosition; @@ -226,19 +226,6 @@ impl WalletTransactions { .show(); }); } - - // Draw button to repost transaction. - if tx.can_repost(data) { - let r = Rounding::default(); - let (icon, color) = (ARROW_CLOCKWISE, Colors::green()); - View::item_button(ui, r, icon, Some(color), || { - cb.hide_keyboard(); - // Post tx after getting slate from slatepack file. - if let Some((s, _)) = wallet.read_slate_by_tx(tx) { - let _ = wallet.post(&s, wallet.can_use_dandelion()); - } - }); - } }); } }); @@ -249,7 +236,7 @@ impl WalletTransactions { if refresh_resp.should_refresh() { self.manual_sync = Some(now); if !wallet.syncing() { - wallet.sync(true); + wallet.sync(); } } } @@ -339,7 +326,7 @@ impl WalletTransactions { || tx.data.tx_type == TxLogEntryType::TxReceivedCancelled; if is_canceled { format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled")) - } else if tx.posting { + } else if tx.finalizing { format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_finalizing")) } else { if tx.cancelling { @@ -431,8 +418,7 @@ impl WalletTransactions { /// Show transaction information [`Modal`]. fn show_tx_info_modal(&mut self, wallet: &Wallet, tx: &WalletTransaction, finalize: bool) { - let mut modal = WalletTransactionModal::new(wallet, tx); - modal.show_finalization = finalize; + let modal = WalletTransactionModal::new(wallet, tx, finalize); self.tx_info_content = Some(modal); Modal::new(TX_INFO_MODAL) .position(ModalPosition::CenterTop) diff --git a/src/gui/views/wallets/wallet/txs/tx.rs b/src/gui/views/wallets/wallet/txs/tx.rs index f6ec4e6..8bcc89a 100644 --- a/src/gui/views/wallets/wallet/txs/tx.rs +++ b/src/gui/views/wallets/wallet/txs/tx.rs @@ -21,7 +21,7 @@ use grin_util::ToHex; use grin_wallet_libwallet::{Error, Slate, SlateState, TxLogEntryType}; use parking_lot::RwLock; use crate::gui::Colors; -use crate::gui::icons::{ARROW_CLOCKWISE, BROOM, CHECK, CLIPBOARD_TEXT, COPY, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN}; +use crate::gui::icons::{BROOM, CHECK, CLIPBOARD_TEXT, COPY, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{CameraContent, FilePickButton, Modal, QrCodeContent, View}; @@ -41,7 +41,7 @@ pub struct WalletTransactionModal { response_edit: String, /// Flag to show transaction finalization input. - pub show_finalization: bool, + show_finalization: bool, /// Finalization Slatepack message input value. finalize_edit: String, /// Flag to check if error happened during transaction finalization. @@ -49,17 +49,13 @@ pub struct WalletTransactionModal { /// Flag to check if transaction is finalizing. finalizing: bool, /// Transaction finalization result. - final_result: Arc>>>, + final_result: Arc>>>, - /// Flag to check if QR code is showing. - show_qr: bool, /// QR code Slatepack message image content. - qr_code_content: QrCodeContent, + qr_code_content: Option, - /// Flag to check if QR code scanner is showing. - show_scanner: bool, /// QR code scanner content. - scanner_content: CameraContent, + qr_scan_content: Option, /// Button to parse picked file content. file_pick_button: FilePickButton, @@ -67,7 +63,7 @@ pub struct WalletTransactionModal { impl WalletTransactionModal { /// Create new content instance with [`Wallet`] from provided [`WalletTransaction`]. - pub fn new(wallet: &Wallet, tx: &WalletTransaction) -> Self { + pub fn new(wallet: &Wallet, tx: &WalletTransaction, show_finalization: bool) -> Self { Self { tx_id: tx.data.id, slate_id: match tx.data.tx_slate_id { @@ -98,13 +94,11 @@ impl WalletTransactionModal { }, finalize_edit: "".to_string(), finalize_error: false, - show_finalization: false, + show_finalization, finalizing: false, final_result: Arc::new(RwLock::new(None)), - show_qr: false, - qr_code_content: QrCodeContent::new("".to_string(), true), - show_scanner: false, - scanner_content: CameraContent::default(), + qr_code_content: None, + qr_scan_content: None, file_pick_button: FilePickButton::default(), } } @@ -133,7 +127,7 @@ impl WalletTransactionModal { } let tx = txs.get(0).unwrap(); - if !self.show_qr && !self.show_scanner { + if self.qr_code_content.is_none() && self.qr_scan_content.is_none() { ui.add_space(6.0); // Show transaction amount status and time. @@ -174,25 +168,6 @@ impl WalletTransactionModal { wallet.cancel(tx.data.id); }); } - - // Draw button to repost transaction. - if wallet_loaded && tx.can_repost(&data) { - let r = if self.show_finalization { - Rounding::default() - } else { - let mut r = r.clone(); - r.nw = 0.0; - r.sw = 0.0; - r - }; - View::item_button(ui, r, ARROW_CLOCKWISE, Some(Colors::green()), || { - cb.hide_keyboard(); - // Post tx after getting slate from slatepack file. - if let Some((s, _)) = wallet.read_slate_by_tx(tx) { - let _ = wallet.post(&s, wallet.can_use_dandelion()); - } - }); - } }); // Show transaction ID info. @@ -207,54 +182,49 @@ impl WalletTransactionModal { } } - // Show Slatepack message or reset flag to show QR if not available. - if !tx.posting && !tx.data.confirmed && !tx.cancelling && + // Show Slatepack message or reset QR code state if not available. + if !tx.finalizing && !tx.data.confirmed && !tx.cancelling && (tx.data.tx_type == TxLogEntryType::TxSent || - tx.data.tx_type == TxLogEntryType::TxReceived) { + tx.data.tx_type == TxLogEntryType::TxReceived) && !self.response_edit.is_empty() { self.message_ui(ui, tx, wallet, modal, cb); - } else if self.show_qr { - self.qr_code_content.clear_state(); - self.show_qr = false; + } else if let Some(qr_content) = self.qr_code_content.as_mut() { + qr_content.clear_state(); } if !self.finalizing { // Setup spacing between buttons. ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - if self.show_qr { + if self.qr_code_content.is_some() { // Show buttons to close modal or come back to text request content. ui.columns(2, |cols| { cols[0].vertical_centered_justified(|ui| { View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.qr_code_content.clear_state(); - self.show_qr = false; + self.qr_code_content = None; modal.close(); }); }); cols[1].vertical_centered_justified(|ui| { View::button(ui, t!("back"), Colors::white_or_black(false), || { - self.qr_code_content.clear_state(); - self.show_qr = false; + self.qr_code_content = None; }); }); }); - } else if self.show_scanner { + } else if self.qr_scan_content.is_some() { ui.add_space(8.0); // Show buttons to close modal or scanner. ui.columns(2, |cols| { cols[0].vertical_centered_justified(|ui| { View::button(ui, t!("close"), Colors::white_or_black(false), || { cb.stop_camera(); - self.scanner_content.clear_state(); - self.show_scanner = false; + self.qr_scan_content = None; modal.close(); }); }); cols[1].vertical_centered_justified(|ui| { View::button(ui, t!("back"), Colors::white_or_black(false), || { cb.stop_camera(); - self.scanner_content.clear_state(); - self.show_scanner = false; + self.qr_scan_content = None; modal.enable_closing(); }); }); @@ -361,20 +331,19 @@ impl WalletTransactionModal { ui.add_space(6.0); // Draw QR code scanner content if requested. - if self.show_scanner { - if let Some(result) = self.scanner_content.qr_scan_result() { + if let Some(qr_scan_content) = self.qr_scan_content.as_mut() { + if let Some(result) = qr_scan_content.qr_scan_result() { cb.stop_camera(); - self.scanner_content.clear_state(); + qr_scan_content.clear_state(); // Setup value to finalization input field. self.finalize_edit = result.text(); self.on_finalization_input_change(tx, wallet, modal, cb); modal.enable_closing(); - self.scanner_content.clear_state(); - self.show_scanner = false; + self.qr_scan_content = None; } else { - self.scanner_content.ui(ui, cb); + qr_scan_content.ui(ui, cb); } return; } @@ -427,16 +396,9 @@ impl WalletTransactionModal { let message_before = message_edit.clone(); // Draw QR code content if requested. - if self.show_qr { - let text = message_edit.clone(); - if text.is_empty() { - self.qr_code_content.clear_state(); - self.show_qr = false; - } else { - // Draw QR code content. - self.qr_code_content.ui(ui, text.clone(), cb); - return; - } + if let Some(qr_content) = self.qr_code_content.as_mut() { + qr_content.ui(ui, cb); + return; } // Draw Slatepack message finalization input or request text. @@ -498,7 +460,7 @@ impl WalletTransactionModal { cb.hide_keyboard(); modal.disable_closing(); cb.start_camera(); - self.show_scanner = true; + self.qr_scan_content = Some(CameraContent::default()); }); }); columns[1].vertical_centered_justified(|ui| { @@ -535,9 +497,10 @@ impl WalletTransactionModal { columns[0].vertical_centered_justified(|ui| { // Draw button to show Slatepack message as QR code. let qr_text = format!("{} {}", QR_CODE, t!("qr_code")); - View::button(ui, qr_text, Colors::button(), || { + View::button(ui, qr_text.clone(), Colors::button(), || { cb.hide_keyboard(); - self.show_qr = true; + let text = self.response_edit.clone(); + self.qr_code_content = Some(QrCodeContent::new(text, true)); }); }); columns[1].vertical_centered_justified(|ui| { @@ -599,7 +562,7 @@ impl WalletTransactionModal { self.finalizing = true; modal.disable_closing(); thread::spawn(move || { - let res = wallet.finalize(&message, wallet.can_use_dandelion()); + let res = wallet.finalize(&message); let mut w_res = final_res.write(); *w_res = Some(res); }); diff --git a/src/wallet/types.rs b/src/wallet/types.rs index 49885c0..067f1a6 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -158,14 +158,12 @@ pub struct WalletTransaction { pub amount: u64, /// Flag to check if transaction is cancelling. pub cancelling: bool, - /// Flag to check if transaction is posting after finalization. - pub posting: bool, /// Flag to check if transaction can be finalized based on Slatepack message state. pub can_finalize: bool, + /// Flag to check if transaction is finalizing. + pub finalizing: bool, /// Block height when tx was confirmed. pub conf_height: Option, - /// Block height when tx was reposted. - pub repost_height: Option, /// Flag to check if tx was received after sync from node. pub from_node: bool, } @@ -173,16 +171,8 @@ pub struct WalletTransaction { impl WalletTransaction { /// Check if transaction can be cancelled. pub fn can_cancel(&self) -> bool { - self.from_node && !self.cancelling && !self.posting && !self.data.confirmed && + self.from_node && !self.cancelling && !self.data.confirmed && self.data.tx_type != TxLogEntryType::TxReceivedCancelled && self.data.tx_type != TxLogEntryType::TxSentCancelled } - - /// Check if transaction can be reposted. - pub fn can_repost(&self, data: &WalletData) -> bool { - let last_height = data.info.last_confirmed_height; - let min_conf = data.info.minimum_confirmations; - self.from_node && self.posting && self.repost_height.is_some() && - last_height - self.repost_height.unwrap() > min_conf - } } \ No newline at end of file diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 99d313e..771e013 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -437,8 +437,8 @@ impl Wallet { // Mark wallet as not opened. wallet_close.closing.store(false, Ordering::Relaxed); wallet_close.is_open.store(false, Ordering::Relaxed); - // Wake up thread to exit. - wallet_close.sync(true); + // Start sync to exit from thread. + wallet_close.sync(); }); } @@ -464,8 +464,8 @@ impl Wallet { }); } - // Sync wallet data. - self.sync(false); + // Refresh wallet info. + sync_wallet_data(&self, false); Ok(()) }) } @@ -498,7 +498,7 @@ impl Wallet { self.info_sync_progress.store(0, Ordering::Relaxed); // Sync wallet data. - self.sync(false); + self.sync(); Ok(()) } @@ -555,18 +555,11 @@ impl Wallet { r_data.clone() } - /// Sync wallet data from node or locally. - pub fn sync(&self, from_node: bool) { - if from_node { - let thread_r = self.sync_thread.read(); - if let Some(thread) = thread_r.as_ref() { - thread.unpark(); - } - } else { - let wallet = self.clone(); - thread::spawn(move || { - sync_wallet_data(&wallet, false); - }); + /// Sync wallet data from node at sync thread or locally synchronously. + pub fn sync(&self) { + let thread_r = self.sync_thread.read(); + if let Some(thread) = thread_r.as_ref() { + thread.unpark(); } } @@ -625,13 +618,7 @@ impl Wallet { let mut slate = None; if let Some(slate_id) = tx.data.tx_slate_id { // Get slate state based on tx state and status. - let state = if tx.posting { - if tx.data.tx_type == TxLogEntryType::TxSent { - Some(SlateState::Standard3) - } else { - Some(SlateState::Invoice3) - } - } else if !tx.data.confirmed && (tx.data.tx_type == TxLogEntryType::TxSent || + let state = if !tx.data.confirmed && (tx.data.tx_type == TxLogEntryType::TxSent || tx.data.tx_type == TxLogEntryType::TxReceived) { if tx.can_finalize { if tx.data.tx_type == TxLogEntryType::TxSent { @@ -681,7 +668,7 @@ impl Wallet { } /// Initialize a transaction to send amount, return request for funds receiver. - pub fn send(&self, amount: u64) -> Result<(Slate, String), Error> { + pub fn send(&self, amount: u64) -> Result { let config = self.get_config(); let args = InitTxArgs { src_acct_name: Some(config.account), @@ -698,51 +685,35 @@ impl Wallet { api.tx_lock_outputs(None, &slate)?; // Create Slatepack message response. - let message_resp = self.create_slatepack_message(&slate)?; + let _ = self.create_slatepack_message(&slate)?; - // Sync wallet info. - self.sync(false); + // Refresh wallet info. + sync_wallet_data(&self, false); - Ok((slate, message_resp)) + let tx = self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?; + Ok(tx) } /// Send amount to provided address with Tor transport. - pub async fn send_tor(&mut self, amount: u64, addr: &SlatepackAddress) -> Option { + pub async fn send_tor(&mut self, + amount: u64, + addr: &SlatepackAddress) -> Result { // Initialize transaction. - let send_res = self.send(amount); - - if send_res.is_err() { - return None; + let tx = self.send(amount)?; + let slate_res = self.read_slate_by_tx(&tx); + if slate_res.is_none() { + return Err(Error::GenericError("Slate not found".to_string())); } - let slate = send_res.unwrap().0; + let (slate, _) = slate_res.unwrap(); // Function to cancel initialized tx in case of error. let cancel_tx = || { let instance = self.instance.clone().unwrap(); let id = slate.clone().id; cancel_tx(instance, None, &None, None, Some(id.clone())).unwrap(); - // Setup posting flag, and ability to finalize. - { - let mut w_data = self.data.write(); - let mut data = w_data.clone().unwrap(); - let txs = data.txs.clone().unwrap().iter_mut().map(|tx| { - if tx.data.tx_slate_id == Some(id) { - tx.cancelling = false; - tx.posting = false; - tx.can_finalize = false; - tx.data.tx_type = if tx.data.tx_type == TxLogEntryType::TxReceived { - TxLogEntryType::TxReceivedCancelled - } else { - TxLogEntryType::TxSentCancelled - }; - } - tx.clone() - }).collect::>(); - data.txs = Some(txs); - *w_data = Some(data); - } + // Refresh wallet info to update statuses. - self.sync(false); + sync_wallet_data(&self, false); }; // Initialize parameters. @@ -764,17 +735,15 @@ impl Wallet { let req_res = Tor::post(body, url).await; if req_res.is_none() { cancel_tx(); - return None; + return Err(Error::GenericError("Tor post error".to_string())); } - // Parse response and finalize transaction. + // Parse response. let res: Value = serde_json::from_str(&req_res.unwrap()).unwrap(); if res["error"] != json!(null) { cancel_tx(); - return None; + return Err(Error::GenericError("Tx error".to_string())); } - - // Slatepack message json value. let slate_value = res["result"]["Ok"].clone(); let mut ret_slate = None; @@ -788,7 +757,7 @@ impl Wallet { // Save Slatepack message to file. let _ = self.create_slatepack_message(&slate).unwrap_or("".to_string()); // Post transaction to blockchain. - let result = self.post(&slate, self.can_use_dandelion()); + let result = self.post(&slate); match result { Ok(_) => { Ok(()) @@ -798,21 +767,25 @@ impl Wallet { } } } else { - Err(Error::GenericError("TX finalization error".to_string())) + Err(Error::GenericError("Tx finalization error".to_string())) }; - }).unwrap(); + })?; } Err(_) => {} }; + // Cancel transaction on error. if ret_slate.is_none() { cancel_tx(); + return Err(Error::GenericError("Tx error".to_string())); } - ret_slate + let tx = self.tx_by_slate(ret_slate.as_ref().unwrap()) + .ok_or(Error::GenericError("No tx found".to_string()))?; + Ok(tx) } /// Initialize an invoice transaction to receive amount, return request for funds sender. - pub fn issue_invoice(&self, amount: u64) -> Result<(Slate, String), Error> { + pub fn issue_invoice(&self, amount: u64) -> Result { let args = IssueInvoiceTxArgs { dest_acct_name: None, amount, @@ -822,16 +795,17 @@ impl Wallet { let slate = api.issue_invoice_tx(None, args)?; // Create Slatepack message response. - let response = self.create_slatepack_message(&slate.clone())?; + let _ = self.create_slatepack_message(&slate)?; - // Sync wallet info. - self.sync(false); + // Refresh wallet info. + sync_wallet_data(&self, false); - Ok((slate, response)) + let tx = self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?; + Ok(tx) } /// Handle message from the invoice issuer to send founds, return response for funds receiver. - pub fn pay(&self, message: &String) -> Result { + pub fn pay(&self, message: &String) -> Result { if let Ok(slate) = self.parse_slatepack(message) { let config = self.get_config(); let args = InitTxArgs { @@ -846,19 +820,19 @@ impl Wallet { api.tx_lock_outputs(None, &slate)?; // Create Slatepack message response. - let response = self.create_slatepack_message(&slate)?; + let _ = self.create_slatepack_message(&slate)?; - // Sync wallet info. - self.sync(false); + // Refresh wallet info. + sync_wallet_data(&self, false); - Ok(response) + Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?) } else { Err(Error::SlatepackDeser("Slatepack parsing error".to_string())) } } /// Handle message to receive funds, return response to sender. - pub fn receive(&self, message: &String) -> Result { + pub fn receive(&self, message: &String) -> Result { if let Ok(mut slate) = self.parse_slatepack(message) { let api = Owner::new(self.instance.clone().unwrap(), None); controller::foreign_single_use(api.wallet_inst.clone(), None, |api| { @@ -866,61 +840,47 @@ impl Wallet { Ok(()) })?; // Create Slatepack message response. - let response = self.create_slatepack_message(&slate)?; + let _ = self.create_slatepack_message(&slate)?; - // Sync wallet info. - self.sync(false); + // Refresh wallet info. + sync_wallet_data(&self, false); - Ok(response) + Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?) } else { Err(Error::SlatepackDeser("Slatepack parsing error".to_string())) } } /// Finalize transaction from provided message as sender or invoice issuer with Dandelion. - pub fn finalize(&self, message: &String, dandelion: bool) -> Result { + pub fn finalize(&self, message: &String) -> Result { if let Ok(mut slate) = self.parse_slatepack(message) { let api = Owner::new(self.instance.clone().unwrap(), None); slate = api.finalize_tx(None, &slate)?; // Save Slatepack message to file. let _ = self.create_slatepack_message(&slate)?; + // Post transaction to blockchain. - let _ = self.post(&slate, dandelion); - Ok(slate) + let tx = self.post(&slate)?; + + // Refresh wallet info. + sync_wallet_data(&self, false); + + Ok(tx) } else { Err(Error::SlatepackDeser("Slatepack parsing error".to_string())) } } /// Post transaction to blockchain. - pub fn post(&self, slate: &Slate, dandelion: bool) -> Result<(), Error> { + fn post(&self, slate: &Slate) -> Result { // Post transaction to blockchain. let api = Owner::new(self.instance.clone().unwrap(), None); - api.post_tx(None, slate, dandelion)?; - // Setup transaction repost height, posting flag and ability to finalize. - let mut slate = slate.clone(); - if slate.state == SlateState::Invoice2 { - slate.state = SlateState::Invoice3 - } else if slate.state == SlateState::Standard2 { - slate.state = SlateState::Standard3 - }; - if let Some(tx) = self.tx_by_slate(&slate) { - let mut w_data = self.data.write(); - let mut data = w_data.clone().unwrap(); - let mut data_txs = data.txs.unwrap(); - for t in &mut data_txs { - if t.data.id == tx.data.id { - t.repost_height = Some(data.info.last_confirmed_height); - t.posting = true; - t.can_finalize = false; - } - } - data.txs = Some(data_txs); - *w_data = Some(data); - } - // Sync local wallet info. - self.sync(false); - Ok(()) + api.post_tx(None, slate, self.can_use_dandelion())?; + + // Refresh wallet info. + sync_wallet_data(&self, false); + + Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?) } /// Cancel transaction. @@ -948,27 +908,7 @@ impl Wallet { } let instance = wallet.instance.clone().unwrap(); let _ = cancel_tx(instance, None, &None, Some(id), None); - // Setup tx status, cancelling, posting flag, and ability to finalize. - { - let mut w_data = wallet.data.write(); - let mut data = w_data.clone().unwrap(); - let mut data_txs = data.txs.unwrap(); - let txs = data_txs.iter_mut().map(|tx| { - if tx.data.id == id { - tx.cancelling = false; - tx.posting = false; - tx.can_finalize = false; - tx.data.tx_type = if tx.data.tx_type == TxLogEntryType::TxReceived { - TxLogEntryType::TxReceivedCancelled - } else { - TxLogEntryType::TxSentCancelled - }; - } - tx.clone() - }).collect::>(); - data.txs = Some(txs); - *w_data = Some(data); - } + // Refresh wallet info to update statuses. sync_wallet_data(&wallet, false); }); @@ -985,7 +925,7 @@ impl Wallet { /// Initiate wallet repair by scanning its outputs. pub fn repair(&self) { self.repair_needed.store(true, Ordering::Relaxed); - self.sync(true); + self.sync(); } /// Check if wallet is repairing. @@ -1013,7 +953,7 @@ impl Wallet { // Remove wallet db files. let _ = fs::remove_dir_all(wallet_delete.get_config().get_db_path()); // Start sync to close thread. - wallet_delete.sync(true); + wallet_delete.sync(); // Mark wallet to reopen. wallet_delete.set_reopen(reopen); }); @@ -1046,7 +986,7 @@ impl Wallet { // Mark wallet as deleted. wallet_delete.deleted.store(true, Ordering::Relaxed); // Start sync to close thread. - wallet_delete.sync(true); + wallet_delete.sync(); }); } @@ -1268,7 +1208,7 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { wallet.reset_sync_attempts(); // Filter transactions for current account. - let filter_txs = txs.1.iter().map(|v| v.clone()).filter(|tx| { + let account_txs = txs.1.iter().map(|v| v.clone()).filter(|tx| { match wallet.get_parent_key_id() { Ok(key) => { tx.parent_key_id == key @@ -1286,7 +1226,7 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { // Create wallet txs. let mut new_txs: Vec = vec![]; - for tx in &filter_txs { + for tx in &account_txs { // Setup transaction amount. let amount = if tx.amount_debited > tx.amount_credited { tx.amount_debited - tx.amount_credited @@ -1294,54 +1234,36 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { tx.amount_credited - tx.amount_debited }; + // Setup flag for ability to finalize transaction. let unconfirmed_sent_or_received = tx.tx_slate_id.is_some() && !tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent || tx.tx_type == TxLogEntryType::TxReceived); - - // Setup transaction posting status based on slate state. - let posting = if unconfirmed_sent_or_received { - // Create slate to check existing file. - let is_invoice = tx.tx_type == TxLogEntryType::TxReceived; - let mut slate = Slate::blank(0, is_invoice); - slate.id = tx.tx_slate_id.unwrap(); - slate.state = match is_invoice { - true => SlateState::Invoice3, - _ => SlateState::Standard3 + let mut finalizing = false; + let can_finalize = if unconfirmed_sent_or_received { + let initial_state = { + let mut slate = Slate::blank(1, false); + slate.id = tx.tx_slate_id.unwrap(); + slate.state = match tx.tx_type { + TxLogEntryType::TxReceived => SlateState::Invoice1, + _ => SlateState::Standard1 + }; + wallet.read_slatepack(&slate).is_some() }; - - // Setup posting status if we have other tx with same slate id. - let mut same_tx_posting = false; - for t in &mut new_txs { - if t.data.tx_slate_id == tx.tx_slate_id && - tx.tx_type != t.data.tx_type { - same_tx_posting = t.posting || - wallet.read_slatepack(&slate).is_some(); - if same_tx_posting && !t.posting { - t.posting = true; - } - break; - } - } - same_tx_posting || wallet.read_slatepack(&slate).is_some() + finalizing = { + let mut slate = Slate::blank(1, false); + slate.id = tx.tx_slate_id.unwrap(); + slate.state = match tx.tx_type { + TxLogEntryType::TxReceived => SlateState::Invoice3, + _ => SlateState::Standard3 + }; + wallet.read_slatepack(&slate).is_some() + }; + initial_state && !finalizing } else { false }; - // Setup flag for ability to finalize transaction. - let can_finalize = if !posting && unconfirmed_sent_or_received { - // Check existing file. - let mut slate = Slate::blank(1, false); - slate.id = tx.tx_slate_id.unwrap(); - slate.state = match tx.tx_type { - TxLogEntryType::TxReceived => SlateState::Invoice1, - _ => SlateState::Standard1 - }; - wallet.read_slatepack(&slate).is_some() - } else { - false - }; - - // Setup confirmation, reposting height and cancelling status. + // Setup confirmation and cancelling status. let mut conf_height = None; let mut setup_conf_height = |t: &TxLogEntry, current_empty: bool| -> bool { if current_empty && t.kernel_lookup_min_height.is_some() && @@ -1376,7 +1298,6 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { false }; - let mut repost_height = None; let mut cancelling = false; if data_txs.is_empty() { setup_conf_height(tx, true); @@ -1387,7 +1308,6 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { t.conf_height.unwrap() == 0) { conf_height = t.conf_height; } - repost_height = t.repost_height; if t.cancelling && tx.tx_type != TxLogEntryType::TxReceivedCancelled && tx.tx_type != TxLogEntryType::TxSentCancelled { @@ -1403,10 +1323,9 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { data: tx.clone(), amount, cancelling, - posting, can_finalize, + finalizing, conf_height, - repost_height, from_node: !fresh_sync || from_node }); } From a3ed3bd2347789d895a623ca56828f7b4325a646 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 7 Sep 2024 12:45:05 +0300 Subject: [PATCH 03/28] build: linux release --- linux/build_release.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/linux/build_release.sh b/linux/build_release.sh index b669567..f1042b4 100755 --- a/linux/build_release.sh +++ b/linux/build_release.sh @@ -17,9 +17,7 @@ cd .. [[ $2 == "x86_64" ]] && arch+=(x86_64-unknown-linux-gnu) [[ $2 == "arm" ]] && arch+=(aarch64-unknown-linux-gnu) -# Start release build with zig linker for cross-compilation -cargo install cargo-zigbuild -cargo zigbuild --release --target ${arch} +cargo build --release --target ${arch} # Create AppImage with https://github.com/AppImage/appimagetool cp target/${arch}/release/grim linux/Grim.AppDir/AppRun From dbc28205e8960bdf8a5a7b6a9613b3571d9bee2d Mon Sep 17 00:00:00 2001 From: ardocrat Date: Wed, 11 Sep 2024 17:01:05 +0300 Subject: [PATCH 04/28] desktop: parse file content from argument on launch, single app instance, wallets selection and opening modals refactoring --- Cargo.lock | 28 ++ Cargo.toml | 1 + linux/Grim.AppDir/grim.desktop | 3 +- src/gui/app.rs | 36 ++- src/gui/platform/android/mod.rs | 4 + src/gui/platform/desktop/mod.rs | 79 +++++- src/gui/platform/mod.rs | 2 + src/gui/views/modal.rs | 8 +- src/gui/views/network/setup/stratum.rs | 14 +- src/gui/views/wallets/content.rs | 251 +++++++----------- src/gui/views/wallets/modals/mod.rs | 5 +- src/gui/views/wallets/modals/open.rs | 121 +++++++++ src/gui/views/wallets/modals/wallets.rs | 117 +++++--- src/gui/views/wallets/wallet/content.rs | 35 +-- .../views/wallets/wallet/messages/content.rs | 14 +- src/gui/views/wallets/wallet/types.rs | 36 +++ src/main.rs | 151 ++++++++++- src/settings/settings.rs | 7 + src/wallet/list.rs | 3 +- 19 files changed, 672 insertions(+), 243 deletions(-) create mode 100644 src/gui/views/wallets/modals/open.rs diff --git a/Cargo.lock b/Cargo.lock index 0134201..7e53eb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2483,6 +2483,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "document-features" version = "0.2.8" @@ -3833,6 +3839,7 @@ dependencies = [ "hyper 0.14.29", "hyper-tls 0.5.0", "image 0.25.1", + "interprocess", "jni", "lazy_static", "local-ip-address", @@ -4976,6 +4983,21 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "interprocess" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f4e4a06d42fab3e85ab1b419ad32b09eab58b901d40c57935ff92db3287a13" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio 1.38.0", + "widestring", + "windows-sys 0.52.0", +] + [[package]] name = "intl-memoizer" version = "0.5.2" @@ -7432,6 +7454,12 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redox_syscall" version = "0.1.57" diff --git a/Cargo.toml b/Cargo.toml index e2ff09d..79edc3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,6 +119,7 @@ eframe = { version = "0.28.1", features = ["wgpu", "glow"] } arboard = "3.2.0" rfd = "0.14.1" dark-light = "1.1.1" +interprocess = { version = "2.2.1", features = ["tokio"] } [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.13.1" diff --git a/linux/Grim.AppDir/grim.desktop b/linux/Grim.AppDir/grim.desktop index 907cf68..781f500 100644 --- a/linux/Grim.AppDir/grim.desktop +++ b/linux/Grim.AppDir/grim.desktop @@ -3,4 +3,5 @@ Name=Grim Exec=grim Icon=grim Type=Application -Categories=Finance \ No newline at end of file +Categories=Finance +MimeType=application/x-slatepack;text/plain; \ No newline at end of file diff --git a/src/gui/app.rs b/src/gui/app.rs index 17fe021..dbbbcd3 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -32,25 +32,38 @@ lazy_static! { /// Implements ui entry point and contains platform-specific callbacks. pub struct App { /// Platform specific callbacks handler. - pub(crate) platform: Platform, - - /// Main ui content. + pub platform: Platform, + /// Main content. content: Content, - /// Last window resize direction. - resize_direction: Option + resize_direction: Option, + /// Flag to check if it's first draw. + first_draw: bool, } impl App { pub fn new(platform: Platform) -> Self { - Self { platform, content: Content::default(), resize_direction: None } + Self { + platform, + content: Content::default(), + resize_direction: None, + first_draw: true, + } } /// Draw application content. pub fn ui(&mut self, ctx: &Context) { + // Set Desktop platform context on first draw. + if self.first_draw { + if View::is_desktop() { + self.platform.set_context(ctx); + } + self.first_draw = false; + } + // Handle Esc keyboard key event and platform Back button key event. let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed); - if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) || back_pressed { + if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) { self.content.on_back(); if back_pressed { BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed); @@ -59,8 +72,8 @@ impl App { ctx.request_repaint(); } - // Handle Close event (on desktop). - if ctx.input(|i| i.viewport().close_requested()) { + // Handle Close event on desktop. + if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) { if !self.content.exit_allowed { ctx.send_viewport_cmd(ViewportCommand::CancelClose); Content::show_exit_modal(); @@ -92,6 +105,11 @@ impl App { } self.content.ui(ui, &self.platform); } + + // Provide incoming data to wallets. + if let Some(data) = self.platform.consume_data() { + self.content.wallets.on_data(ui, Some(data), &self.platform); + } }); } diff --git a/src/gui/platform/android/mod.rs b/src/gui/platform/android/mod.rs index bb077f7..7fbe6ac 100644 --- a/src/gui/platform/android/mod.rs +++ b/src/gui/platform/android/mod.rs @@ -167,6 +167,10 @@ impl PlatformCallbacks for Android { } None } + + fn consume_data(&mut self) -> Option { + None + } } lazy_static! { diff --git a/src/gui/platform/desktop/mod.rs b/src/gui/platform/desktop/mod.rs index 5318bf4..9b4b210 100644 --- a/src/gui/platform/desktop/mod.rs +++ b/src/gui/platform/desktop/mod.rs @@ -13,12 +13,13 @@ // limitations under the License. use std::fs::File; -use std::io:: Write; -use lazy_static::lazy_static; -use parking_lot::RwLock; +use std::io::Write; +use std::thread; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; -use std::thread; +use parking_lot::RwLock; +use lazy_static::lazy_static; +use egui::{UserAttentionType, ViewportCommand}; use rfd::FileDialog; use crate::gui::platform::PlatformCallbacks; @@ -28,17 +29,16 @@ use crate::gui::platform::PlatformCallbacks; pub struct Desktop { /// Flag to check if camera stop is needed. stop_camera: Arc, -} - -impl Default for Desktop { - fn default() -> Self { - Self { - stop_camera: Arc::new(AtomicBool::new(false)), - } - } + /// Context to repaint content and handle viewport commands. + ctx: Arc>>, } impl PlatformCallbacks for Desktop { + fn set_context(&mut self, ctx: &egui::Context) { + let mut w_ctx = self.ctx.write(); + *w_ctx = Some(ctx.clone()); + } + fn show_keyboard(&self) {} fn hide_keyboard(&self) {} @@ -119,9 +119,61 @@ impl PlatformCallbacks for Desktop { fn picked_file(&self) -> Option { None } + + fn consume_data(&mut self) -> Option { + let has_data = { + let r_data = PASSED_DATA.read(); + r_data.is_some() + }; + if has_data { + // Reset window state. + let r_ctx = self.ctx.read(); + if r_ctx.is_some() { + let ctx = r_ctx.as_ref().unwrap(); + ctx.send_viewport_cmd( + ViewportCommand::RequestUserAttention(UserAttentionType::Reset) + ); + } + // Clear data. + let mut w_data = PASSED_DATA.write(); + let data = w_data.clone(); + *w_data = None; + return data; + } + None + } } impl Desktop { + /// Create new instance with provided extra data from app opening. + pub fn new(data: Option) -> Self { + let mut w_data = PASSED_DATA.write(); + *w_data = data; + Self { + stop_camera: Arc::new(AtomicBool::new(false)), + ctx: Arc::new(RwLock::new(None)), + } + } + + /// Handle data passed to application. + pub fn on_data(&self, data: String) { + let mut w_data = PASSED_DATA.write(); + *w_data = Some(data); + + // Bring focus on window. + let r_ctx = self.ctx.read(); + if r_ctx.is_some() { + let ctx = r_ctx.as_ref().unwrap(); + ctx.send_viewport_cmd(ViewportCommand::Visible(true)); + ctx.send_viewport_cmd(ViewportCommand::Minimized(false)); + ctx.send_viewport_cmd( + ViewportCommand::RequestUserAttention(UserAttentionType::Informational) + ); + ctx.send_viewport_cmd(ViewportCommand::Focus); + ctx.request_repaint(); + } + } + #[allow(dead_code)] #[cfg(target_os = "windows")] fn start_camera_capture(stop_camera: Arc) { @@ -205,4 +257,7 @@ impl Desktop { lazy_static! { /// Last captured image from started camera. static ref LAST_CAMERA_IMAGE: Arc, u32)>>> = Arc::new(RwLock::new(None)); + + /// Data passed from deeplink or opened file. + static ref PASSED_DATA: Arc>> = Arc::new(RwLock::new(None)); } diff --git a/src/gui/platform/mod.rs b/src/gui/platform/mod.rs index e5bc3be..06f6a3e 100644 --- a/src/gui/platform/mod.rs +++ b/src/gui/platform/mod.rs @@ -22,6 +22,7 @@ pub mod platform; pub mod platform; pub trait PlatformCallbacks { + fn set_context(&mut self, ctx: &egui::Context); fn show_keyboard(&self); fn hide_keyboard(&self); fn copy_string_to_buffer(&self, data: String); @@ -34,4 +35,5 @@ pub trait PlatformCallbacks { fn share_data(&self, name: String, data: Vec) -> Result<(), std::io::Error>; fn pick_file(&self) -> Option; fn picked_file(&self) -> Option; + fn consume_data(&mut self) -> Option; } \ No newline at end of file diff --git a/src/gui/views/modal.rs b/src/gui/views/modal.rs index 6cff50e..7b3359a 100644 --- a/src/gui/views/modal.rs +++ b/src/gui/views/modal.rs @@ -35,7 +35,7 @@ pub struct Modal { /// Identifier for modal. pub(crate) id: &'static str, /// Position on the screen. - position: ModalPosition, + pub position: ModalPosition, /// To check if it can be closed. closeable: Arc, /// Title text @@ -64,6 +64,12 @@ impl Modal { self } + /// Change [`Modal`] position on the screen. + pub fn change_position(position: ModalPosition) { + let mut w_state = MODAL_STATE.write(); + w_state.modal.as_mut().unwrap().position = position; + } + /// Mark [`Modal`] closed. pub fn close(&self) { let mut w_nav = MODAL_STATE.write(); diff --git a/src/gui/views/network/setup/stratum.rs b/src/gui/views/network/setup/stratum.rs index 046c412..3d85aca 100644 --- a/src/gui/views/network/setup/stratum.rs +++ b/src/gui/views/network/setup/stratum.rs @@ -83,7 +83,7 @@ impl Default for StratumSetup { Self { wallets: WalletList::default(), - wallets_modal: WalletsModal::new(wallet_id), + wallets_modal: WalletsModal::new(wallet_id, None, false), available_ips: NodeConfig::get_ip_addrs(), stratum_port_edit: port, stratum_port_available_edit: is_port_available, @@ -111,10 +111,12 @@ impl ModalContainer for StratumSetup { modal: &Modal, cb: &dyn PlatformCallbacks) { match modal.id { - WALLET_SELECTION_MODAL => self.wallets_modal.ui(ui, modal, &self.wallets, |id| { - NodeConfig::save_stratum_wallet_id(id); - self.wallet_name = WalletConfig::name_by_id(id); - }), + WALLET_SELECTION_MODAL => { + self.wallets_modal.ui(ui, modal, &mut self.wallets, cb, |id, _| { + NodeConfig::save_stratum_wallet_id(id); + self.wallet_name = WalletConfig::name_by_id(id); + }) + }, STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb), ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb), MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb), @@ -240,7 +242,7 @@ impl StratumSetup { /// Show wallet selection [`Modal`]. fn show_wallets_modal(&mut self) { - self.wallets_modal = WalletsModal::new(NodeConfig::get_stratum_wallet_id()); + self.wallets_modal = WalletsModal::new(NodeConfig::get_stratum_wallet_id(), None, false); // Show modal. Modal::new(WALLET_SELECTION_MODAL) .position(ModalPosition::Center) diff --git a/src/gui/views/wallets/content.rs b/src/gui/views/wallets/content.rs index 61b34c0..45ca51d 100644 --- a/src/gui/views/wallets/content.rs +++ b/src/gui/views/wallets/content.rs @@ -18,13 +18,14 @@ use egui::scroll_area::ScrollBarVisibility; use crate::AppConfig; use crate::gui::Colors; -use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_LOCK, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SPINNER, SUITCASE, WARNING_CIRCLE}; +use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, Content, TitlePanel, View}; -use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions, TitleContentType, TitleType}; +use crate::gui::views::types::{ModalContainer, ModalPosition, TitleContentType, TitleType}; use crate::gui::views::wallets::creation::WalletCreation; -use crate::gui::views::wallets::modals::WalletConnectionModal; +use crate::gui::views::wallets::modals::{OpenWalletModal, WalletConnectionModal, WalletsModal}; use crate::gui::views::wallets::types::WalletTabType; +use crate::gui::views::wallets::wallet::types::status_text; use crate::gui::views::wallets::WalletContent; use crate::wallet::{Wallet, WalletList}; @@ -33,10 +34,11 @@ pub struct WalletsContent { /// List of wallets. wallets: WalletList, - /// Password to open wallet for [`Modal`]. - pass_edit: String, - /// Flag to check if wrong password was entered at [`Modal`]. - wrong_pass: bool, + /// Wallet selection [`Modal`] content. + wallet_selection_content: Option, + + /// Wallet opening [`Modal`] content. + open_wallet_content: Option, /// Wallet connection selection content. conn_modal_content: Option, @@ -54,24 +56,29 @@ pub struct WalletsContent { } /// Identifier for connection selection [`Modal`]. -const CONNECTION_SELECTION_MODAL: &'static str = "wallets_connection_selection_modal"; +const CONNECTION_SELECTION_MODAL: &'static str = "wallets_connection_selection"; + /// Identifier for wallet opening [`Modal`]. -const OPEN_WALLET_MODAL: &'static str = "open_wallet_modal"; +const OPEN_WALLET_MODAL: &'static str = "wallets_open_wallet"; + +/// Identifier for wallet opening [`Modal`]. +const SELECT_WALLET_MODAL: &'static str = "wallets_select_wallet"; impl Default for WalletsContent { fn default() -> Self { Self { wallets: WalletList::default(), - pass_edit: "".to_string(), - wrong_pass: false, + wallet_selection_content: None, + open_wallet_content: None, conn_modal_content: None, - wallet_content: WalletContent::default(), + wallet_content: WalletContent::new(None), creation_content: WalletCreation::default(), show_wallets_at_dual_panel: AppConfig::show_wallets_at_dual_panel(), modal_ids: vec![ OPEN_WALLET_MODAL, WalletCreation::NAME_PASS_MODAL, CONNECTION_SELECTION_MODAL, + SELECT_WALLET_MODAL ] } } @@ -87,13 +94,21 @@ impl ModalContainer for WalletsContent { modal: &Modal, cb: &dyn PlatformCallbacks) { match modal.id { - OPEN_WALLET_MODAL => self.open_wallet_modal_ui(ui, modal, cb), + OPEN_WALLET_MODAL => { + if let Some(content) = self.open_wallet_content.as_mut() { + content.ui(ui, modal, &mut self.wallets, cb, |data| { + // Setup wallet content. + self.wallet_content = WalletContent::new(data); + }); + } + }, WalletCreation::NAME_PASS_MODAL => { self.creation_content.name_pass_modal_ui(ui, modal, cb) }, CONNECTION_SELECTION_MODAL => { if let Some(content) = self.conn_modal_content.as_mut() { content.ui(ui, modal, cb, |id| { + // Update wallet connection on select. let list = self.wallets.list(); for w in list { if self.wallets.selected_id == Some(w.get_config().id) { @@ -103,12 +118,20 @@ impl ModalContainer for WalletsContent { }); } } + SELECT_WALLET_MODAL => { + if let Some(content) = self.wallet_selection_content.as_mut() { + content.ui(ui, modal, &mut self.wallets, cb, |_, data| { + self.wallet_content = WalletContent::new(data); + }); + } + } _ => {} } } } impl WalletsContent { + /// Draw wallets content. pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { // Draw modal content for current ui container. self.current_modal_ui(ui, cb); @@ -159,7 +182,7 @@ impl WalletsContent { // Add created wallet to list. self.wallets.add(wallet); // Reset wallet content. - self.wallet_content = WalletContent::default(); + self.wallet_content = WalletContent::new(None); }); } else { let selected_id = self.wallets.selected_id.clone(); @@ -254,6 +277,56 @@ impl WalletsContent { self.creation_content.can_go_back() } + /// Handle data from deeplink or opened file. + pub fn on_data(&mut self, ui: &mut egui::Ui, data: Option, cb: &dyn PlatformCallbacks) { + let wallets_size = self.wallets.list().len(); + if wallets_size == 0 { + return; + } + // Close network panel on single panel mode. + if !Content::is_dual_panel_mode(ui) && Content::is_network_panel_open() { + Content::toggle_network_panel(); + } + // Pass data to opened selected wallet or show wallets selection. + if self.wallets.is_selected_open() { + if wallets_size == 1 { + self.wallet_content = WalletContent::new(data); + } else { + self.show_wallet_selection_modal(data); + } + } else { + if wallets_size == 1 { + self.show_opening_modal(self.wallets.list()[0].get_config().id, data, cb); + } else { + self.show_wallet_selection_modal(data); + } + } + } + + fn show_wallet_selection_modal(&mut self, data: Option) { + self.wallet_selection_content = Some(WalletsModal::new(None, data, true)); + // Show wallet selection modal. + Modal::new(SELECT_WALLET_MODAL) + .position(ModalPosition::Center) + .title(t!("network_settings.choose_wallet")) + .show(); + } + + /// Handle Back key event returning `false` when event was handled. + pub fn on_back(&mut self) -> bool { + let can_go_back = self.creation_content.can_go_back(); + if can_go_back { + self.creation_content.back(); + return false + } else { + if self.wallets.is_selected_open() { + self.wallets.select(None); + return false + } + } + true + } + /// Draw [`TitlePanel`] content. fn title_ui(&mut self, ui: &mut egui::Ui, @@ -383,8 +456,7 @@ impl WalletsContent { // Check if wallet reopen is needed. if !wallet.is_open() && wallet.reopen_needed() { wallet.set_reopen(false); - self.wallets.select(Some(wallet.get_config().id)); - self.show_open_wallet_modal(cb); + self.show_opening_modal(wallet.get_config().id, None, cb); } // Draw wallet list item. self.wallet_item_ui(ui, wallet, cb); @@ -420,8 +492,7 @@ impl WalletsContent { if !wallet.is_open() { // Show button to open closed wallet. View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || { - self.wallets.select(Some(id)); - self.show_open_wallet_modal(cb); + self.show_opening_modal(id, None, cb); }); // Show button to select connection if not syncing. if !wallet.syncing() { @@ -435,7 +506,7 @@ impl WalletsContent { // Show button to select opened wallet. View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || { self.wallets.select(Some(id)); - self.wallet_content = WalletContent::default(); + self.wallet_content = WalletContent::new(None); }); } // Show button to close opened wallet. @@ -455,7 +526,7 @@ impl WalletsContent { ui.add_space(6.0); ui.vertical(|ui| { ui.add_space(3.0); - // Setup wallet name text. + // Show wallet name text. let name_color = if is_selected { Colors::white_or_black(true) } else { @@ -466,42 +537,11 @@ impl WalletsContent { View::ellipsize_text(ui, config.name, 18.0, name_color); }); - // Setup wallet status text. - let status_text = if wallet.is_open() { - if wallet.sync_error() { - format!("{} {}", WARNING_CIRCLE, t!("error")) - } else if wallet.is_closing() { - format!("{} {}", SPINNER, t!("wallets.closing")) - } else if wallet.is_repairing() { - let repair_progress = wallet.repairing_progress(); - if repair_progress == 0 { - format!("{} {}", SPINNER, t!("wallets.checking")) - } else { - format!("{} {}: {}%", - SPINNER, - t!("wallets.checking"), - repair_progress) - } - } else if wallet.syncing() { - let info_progress = wallet.info_sync_progress(); - if info_progress == 100 || info_progress == 0 { - format!("{} {}", SPINNER, t!("wallets.loading")) - } else { - format!("{} {}: {}%", - SPINNER, - t!("wallets.loading"), - info_progress) - } - } else { - format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked")) - } - } else { - format!("{} {}", FOLDER_LOCK, t!("wallets.locked")) - }; - View::ellipsize_text(ui, status_text, 15.0, Colors::text(false)); + // Show wallet status text. + View::ellipsize_text(ui, status_text(wallet), 15.0, Colors::text(false)); ui.add_space(1.0); - // Setup wallet connection text. + // Show wallet connection text. let conn_text = if let Some(conn) = wallet.get_current_ext_conn() { format!("{} {}", GLOBE_SIMPLE, conn.url) } else { @@ -525,11 +565,10 @@ impl WalletsContent { .show(); } - /// Show [`Modal`] to open selected wallet. - fn show_open_wallet_modal(&mut self, cb: &dyn PlatformCallbacks) { - // Reset modal values. - self.pass_edit = String::from(""); - self.wrong_pass = false; + /// Show [`Modal`] to select and open wallet. + fn show_opening_modal(&mut self, id: i64, data: Option, cb: &dyn PlatformCallbacks) { + self.wallets.select(Some(id)); + self.open_wallet_content = Some(OpenWalletModal::new(data)); // Show modal. Modal::new(OPEN_WALLET_MODAL) .position(ModalPosition::CenterTop) @@ -537,100 +576,6 @@ impl WalletsContent { .show(); cb.show_keyboard(); } - - /// Draw wallet opening [`Modal`] content. - fn open_wallet_modal_ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("wallets.pass")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); - - // Show password input. - let mut pass_edit_opts = TextEditOptions::new(Id::from(modal.id)).password(); - View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_edit_opts); - - // Show information when password is empty. - if self.pass_edit.is_empty() { - self.wrong_pass = false; - ui.add_space(10.0); - ui.label(RichText::new(t!("wallets.pass_empty")) - .size(17.0) - .color(Colors::inactive_text())); - } else if self.wrong_pass { - ui.add_space(10.0); - ui.label(RichText::new(t!("wallets.wrong_pass")) - .size(17.0) - .color(Colors::red())); - } - ui.add_space(12.0); - }); - - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - cb.hide_keyboard(); - modal.close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Callback for button to continue. - let mut on_continue = || { - if self.pass_edit.is_empty() { - return; - } - match self.wallets.open_selected(&self.pass_edit) { - Ok(_) => { - // Clear values. - self.pass_edit = "".to_string(); - self.wrong_pass = false; - // Close modal. - cb.hide_keyboard(); - modal.close(); - // Reset wallet content. - self.wallet_content = WalletContent::default(); - } - Err(_) => self.wrong_pass = true - } - }; - - // Continue on Enter key press. - View::on_enter_key(ui, || { - (on_continue)(); - }); - - View::button(ui, t!("continue"), Colors::white_or_black(false), on_continue); - }); - }); - ui.add_space(6.0); - }); - } - - /// Handle Back key event. - /// Return `false` when event was handled. - pub fn on_back(&mut self) -> bool { - let can_go_back = self.creation_content.can_go_back(); - if can_go_back { - self.creation_content.back(); - return false - } else { - if self.wallets.is_selected_open() { - self.wallets.select(None); - return false - } - } - true - } } /// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time. diff --git a/src/gui/views/wallets/modals/mod.rs b/src/gui/views/wallets/modals/mod.rs index c4f566c..bdb7bb6 100644 --- a/src/gui/views/wallets/modals/mod.rs +++ b/src/gui/views/wallets/modals/mod.rs @@ -16,4 +16,7 @@ mod conn; pub use conn::*; mod wallets; -pub use wallets::*; \ No newline at end of file +pub use wallets::*; + +mod open; +pub use open::*; \ No newline at end of file diff --git a/src/gui/views/wallets/modals/open.rs b/src/gui/views/wallets/modals/open.rs new file mode 100644 index 0000000..c585d99 --- /dev/null +++ b/src/gui/views/wallets/modals/open.rs @@ -0,0 +1,121 @@ +// Copyright 2024 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use egui::{Id, RichText}; + +use crate::gui::Colors; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{Modal, View}; +use crate::gui::views::types::TextEditOptions; +use crate::wallet::WalletList; + +/// Wallet opening [`Modal`] content. +pub struct OpenWalletModal { + /// Password to open wallet. + pass_edit: String, + /// Flag to check if wrong password was entered. + wrong_pass: bool, + + /// Optional data to pass after wallet opening. + data: Option, +} + +impl OpenWalletModal { + /// Create new content instance. + pub fn new(data: Option) -> Self { + Self { + pass_edit: "".to_string(), + wrong_pass: false, + data, + } + } + /// Draw [`Modal`] content. + pub fn ui(&mut self, + ui: &mut egui::Ui, + modal: &Modal, + wallets: &mut WalletList, + cb: &dyn PlatformCallbacks, + mut on_continue: impl FnMut(Option)) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("wallets.pass")) + .size(17.0) + .color(Colors::gray())); + ui.add_space(8.0); + + // Show password input. + let mut pass_edit_opts = TextEditOptions::new(Id::from(modal.id)).password(); + View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_edit_opts); + + // Show information when password is empty. + if self.pass_edit.is_empty() { + self.wrong_pass = false; + ui.add_space(10.0); + ui.label(RichText::new(t!("wallets.pass_empty")) + .size(17.0) + .color(Colors::inactive_text())); + } else if self.wrong_pass { + ui.add_space(10.0); + ui.label(RichText::new(t!("wallets.wrong_pass")) + .size(17.0) + .color(Colors::red())); + } + ui.add_space(12.0); + }); + + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + // Close modal. + cb.hide_keyboard(); + modal.close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + // Callback for button to continue. + let mut on_continue = || { + if self.pass_edit.is_empty() { + return; + } + match wallets.open_selected(&self.pass_edit) { + Ok(_) => { + // Clear values. + self.pass_edit = "".to_string(); + self.wrong_pass = false; + // Close modal. + cb.hide_keyboard(); + modal.close(); + on_continue(self.data.clone()); + } + Err(_) => self.wrong_pass = true + } + }; + + // Continue on Enter key press. + View::on_enter_key(ui, || { + (on_continue)(); + }); + + View::button(ui, t!("continue"), Colors::white_or_black(false), on_continue); + }); + }); + ui.add_space(6.0); + }); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/modals/wallets.rs b/src/gui/views/wallets/modals/wallets.rs index b2bb8d5..b8b846b 100644 --- a/src/gui/views/wallets/modals/wallets.rs +++ b/src/gui/views/wallets/modals/wallets.rs @@ -16,29 +16,53 @@ use egui::scroll_area::ScrollBarVisibility; use egui::{Align, Layout, RichText, ScrollArea}; use crate::gui::Colors; -use crate::gui::icons::{CHECK, CHECK_FAT, COMPUTER_TOWER, GLOBE_SIMPLE, PLUGS_CONNECTED}; +use crate::gui::icons::{CHECK, CHECK_FAT, COMPUTER_TOWER, FOLDER_OPEN, GLOBE_SIMPLE, PLUGS_CONNECTED}; +use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, View}; +use crate::gui::views::types::ModalPosition; +use crate::gui::views::wallets::modals::OpenWalletModal; +use crate::gui::views::wallets::wallet::types::status_text; use crate::wallet::{Wallet, WalletList}; /// Wallet list [`Modal`] content pub struct WalletsModal { /// Selected wallet id. - selected: Option + selected: Option, + + /// Optional data to pass after wallet selection. + data: Option, + + /// Flag to check if wallet can be opened from the list. + can_open: bool, + /// Wallet opening content. + open_wallet_content: Option, } impl WalletsModal { - pub fn new(selected: Option) -> Self { - Self { - selected, - } + /// Create new content instance. + pub fn new(selected: Option, data: Option, can_open: bool) -> Self { + Self { selected, data, can_open, open_wallet_content: None } } - /// Draw [`Modal`] content. + /// Draw content. pub fn ui(&mut self, ui: &mut egui::Ui, modal: &Modal, - wallets: &WalletList, - mut on_select: impl FnMut(i64)) { + wallets: &mut WalletList, + cb: &dyn PlatformCallbacks, + mut on_select: impl FnMut(i64, Option)) { + // Draw wallet opening content if requested. + if let Some(open_content) = self.open_wallet_content.as_mut() { + open_content.ui(ui, modal, wallets, cb, |data| { + modal.close(); + if let Some(id) = self.selected { + on_select(id, data); + } + self.data = None; + }); + return; + } + ui.add_space(4.0); ScrollArea::vertical() .max_height(373.0) @@ -48,10 +72,12 @@ impl WalletsModal { .show(ui, |ui| { ui.add_space(2.0); ui.vertical_centered(|ui| { - for wallet in wallets.list() { + let data = self.data.clone(); + for wallet in wallets.clone().list() { // Draw wallet list item. - self.wallet_item_ui(ui, wallet, modal, |id| { - on_select(id); + self.wallet_item_ui(ui, wallet, wallets, |id| { + modal.close(); + on_select(id, data.clone()); }); ui.add_space(5.0); } @@ -65,18 +91,19 @@ impl WalletsModal { // Show button to close modal. ui.vertical_centered_justified(|ui| { View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + self.data = None; modal.close(); }); }); ui.add_space(6.0); } - /// Draw wallet list item. + /// Draw wallet list item with provided callback on select. fn wallet_item_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, - modal: &Modal, - mut on_select: impl FnMut(i64)) { + wallets: &mut WalletList, + mut select: impl FnMut(i64)) { let config = wallet.get_config(); let id = config.id; @@ -87,16 +114,34 @@ impl WalletsModal { ui.painter().rect(rect, rounding, Colors::fill(), View::hover_stroke()); ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - // Draw button to select wallet. - let current = self.selected.unwrap_or(0) == id; - if current { - ui.add_space(12.0); - ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green())); - } else { - View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || { - on_select(id); - modal.close(); + if self.can_open { + // Show button to select or open closed wallet. + let icon = if wallet.is_open() { + CHECK + } else { + FOLDER_OPEN + }; + View::item_button(ui, View::item_rounding(0, 1, true), icon, None, || { + wallets.select(Some(id)); + if wallet.is_open() { + select(id); + } else { + self.selected = wallets.selected_id; + Modal::change_position(ModalPosition::CenterTop); + self.open_wallet_content = Some(OpenWalletModal::new(self.data.clone())); + } }); + } else { + // Draw button to select wallet. + let current = self.selected.unwrap_or(0) == id; + if current { + ui.add_space(12.0); + ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green())); + } else { + View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || { + select(id); + }); + } } let layout_size = ui.available_size(); @@ -104,13 +149,13 @@ impl WalletsModal { ui.add_space(6.0); ui.vertical(|ui| { ui.add_space(3.0); - // Setup wallet name text. + // Show wallet name text. ui.with_layout(Layout::left_to_right(Align::Min), |ui| { ui.add_space(1.0); View::ellipsize_text(ui, config.name, 18.0, Colors::title(false)); }); - // Setup wallet connection text. + // Show wallet connection text. let conn = if let Some(conn) = wallet.get_current_ext_conn() { format!("{} {}", GLOBE_SIMPLE, conn.url) } else { @@ -119,14 +164,20 @@ impl WalletsModal { View::ellipsize_text(ui, conn, 15.0, Colors::text(false)); ui.add_space(1.0); - // Setup wallet API text. - let address = if let Some(port) = config.api_port { - format!("127.0.0.1:{}", port) + // Show wallet API text or open status. + if self.can_open { + ui.label(RichText::new(status_text(wallet)) + .size(15.0) + .color(Colors::gray())); } else { - "-".to_string() - }; - let api_text = format!("{} {}", PLUGS_CONNECTED, address); - ui.label(RichText::new(api_text).size(15.0).color(Colors::gray())); + let address = if let Some(port) = config.api_port { + format!("127.0.0.1:{}", port) + } else { + "-".to_string() + }; + let api_text = format!("{} {}", PLUGS_CONNECTED, address); + ui.label(RichText::new(api_text).size(15.0).color(Colors::gray())); + } ui.add_space(3.0); }); }); diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 3cd5061..8888931 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -35,7 +35,6 @@ use crate::wallet::types::{WalletAccount, WalletData}; pub struct WalletContent { /// List of wallet accounts for [`Modal`]. accounts: Vec, - /// Flag to check if account is creating. account_creating: bool, /// Account label [`Modal`] value. @@ -49,21 +48,7 @@ pub struct WalletContent { qr_scan_result: Option, /// Current tab content to show. - pub current_tab: Box -} - -impl Default for WalletContent { - fn default() -> Self { - Self { - accounts: vec![], - account_creating: false, - account_label_edit: "".to_string(), - account_creation_error: false, - camera_content: CameraContent::default(), - qr_scan_result: None, - current_tab: Box::new(WalletTransactions::default()) - } - } + pub current_tab: Box, } /// Identifier for account list [`Modal`]. @@ -73,6 +58,24 @@ const ACCOUNT_LIST_MODAL: &'static str = "account_list_modal"; const QR_CODE_SCAN_MODAL: &'static str = "qr_code_scan_modal"; impl WalletContent { + /// Create new instance with optional data. + pub fn new(data: Option) -> Self { + let mut content = Self { + accounts: vec![], + account_creating: false, + account_label_edit: "".to_string(), + account_creation_error: false, + camera_content: CameraContent::default(), + qr_scan_result: None, + current_tab: Box::new(WalletTransactions::default()), + }; + // Provide data to messages. + if data.is_some() { + content.current_tab = Box::new(WalletMessages::new(data)); + } + content + } + /// Draw wallet content. pub fn ui(&mut self, ui: &mut egui::Ui, diff --git a/src/gui/views/wallets/wallet/messages/content.rs b/src/gui/views/wallets/wallet/messages/content.rs index b0214e5..5fded1b 100644 --- a/src/gui/views/wallets/wallet/messages/content.rs +++ b/src/gui/views/wallets/wallet/messages/content.rs @@ -33,13 +33,16 @@ use crate::wallet::Wallet; /// Slatepack messages interaction tab content. pub struct WalletMessages { + /// Flag to check if it's first content draw. + first_draw: bool, + /// Slatepacks message input text. message_edit: String, /// Flag to check if message request is loading. message_loading: bool, /// Error on finalization, parse or response creation. message_error: String, - /// Parsed message result with finalization flag and transaction. + /// Parsed message result. message_result: Arc)>>>, /// Wallet transaction [`Modal`] content. @@ -111,6 +114,7 @@ impl WalletMessages { /// Create new content instance, put message into input if provided. pub fn new(message: Option) -> Self { Self { + first_draw: true, message_edit: message.unwrap_or("".to_string()), message_loading: false, message_error: "".to_string(), @@ -128,6 +132,14 @@ impl WalletMessages { ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { + if self.first_draw { + // Parse provided message on first draw. + if !self.message_edit.is_empty() { + self.parse_message(wallet); + } + self.first_draw = false; + } + ui.add_space(3.0); // Show creation of request to send or receive funds. diff --git a/src/gui/views/wallets/wallet/types.rs b/src/gui/views/wallets/wallet/types.rs index e9f5a4d..750fc02 100644 --- a/src/gui/views/wallets/wallet/types.rs +++ b/src/gui/views/wallets/wallet/types.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::gui::icons::{FOLDER_LOCK, FOLDER_OPEN, SPINNER, WARNING_CIRCLE}; use crate::gui::platform::PlatformCallbacks; use crate::wallet::Wallet; @@ -48,4 +49,39 @@ impl WalletTabType { WalletTabType::Settings => t!("wallets.settings") } } +} + +/// Get wallet status text. +pub fn status_text(wallet: &Wallet) -> String { + if wallet.is_open() { + if wallet.sync_error() { + format!("{} {}", WARNING_CIRCLE, t!("error")) + } else if wallet.is_closing() { + format!("{} {}", SPINNER, t!("wallets.closing")) + } else if wallet.is_repairing() { + let repair_progress = wallet.repairing_progress(); + if repair_progress == 0 { + format!("{} {}", SPINNER, t!("wallets.checking")) + } else { + format!("{} {}: {}%", + SPINNER, + t!("wallets.checking"), + repair_progress) + } + } else if wallet.syncing() { + let info_progress = wallet.info_sync_progress(); + if info_progress == 100 || info_progress == 0 { + format!("{} {}", SPINNER, t!("wallets.loading")) + } else { + format!("{} {}: {}%", + SPINNER, + t!("wallets.loading"), + info_progress) + } + } else { + format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked")) + } + } else { + format!("{} {}", FOLDER_LOCK, t!("wallets.locked")) + } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 0ff285d..9b3a2e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,23 @@ fn real_main() { .parse_default_env() .init(); + // Handle file path argument passing. + let args: Vec<_> = std::env::args().collect(); + let mut data = None; + if args.len() > 1 { + let path = std::path::PathBuf::from(&args[1]); + let content = match std::fs::read_to_string(path) { + Ok(s) => Some(s), + Err(_) => None + }; + data = content + } + + // Check if another app instance already running. + if is_app_running(data.clone()) { + return; + } + // Setup callback on panic crash. std::panic::set_hook(Box::new(|info| { let backtrace = backtrace::Backtrace::new(); @@ -50,22 +67,61 @@ fn real_main() { // Start GUI. match std::panic::catch_unwind(|| { - start_desktop_gui(); + start_desktop_gui(data); }) { Ok(_) => {} Err(e) => println!("{:?}", e) } } -/// Start GUI with Desktop related setup. +/// Check if application is already running to pass extra data. #[allow(dead_code)] #[cfg(not(target_os = "android"))] -fn start_desktop_gui() { +fn is_app_running(data: Option) -> bool { + use tor_rtcompat::BlockOn; + let runtime = tor_rtcompat::tokio::TokioNativeTlsRuntime::create().unwrap(); + let res: Result<(), Box> = runtime + .block_on(async { + use interprocess::local_socket::{ + tokio::{prelude::*, Stream}, + GenericFilePath + }; + use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, + try_join, + }; + + let socket_path = grim::Settings::socket_path(); + let name = socket_path.to_fs_name::()?; + // Connect to running application socket. + let conn = Stream::connect(name).await?; + + let (rec, mut sen) = conn.split(); + let mut rec = BufReader::new(rec); + let data = data.unwrap_or("".to_string()); + let mut buffer = String::with_capacity(data.len()); + + // Send extra data to socket. + let send = sen.write_all(data.as_bytes()); + let recv = rec.read_line(&mut buffer); + try_join!(send, recv)?; + + drop((rec, sen)); + Ok(()) + }); + return match res { + Ok(_) => true, + Err(_) => false + } +} + +/// Start GUI with Desktop related setup passing extra data from opening. +#[allow(dead_code)] +#[cfg(not(target_os = "android"))] +fn start_desktop_gui(data: Option) { use grim::AppConfig; use dark_light::Mode; - let platform = grim::gui::platform::Desktop::default(); - // Setup system theme if not set. if let None = AppConfig::dark_theme() { let dark = match dark_light::detect() { @@ -76,12 +132,11 @@ fn start_desktop_gui() { AppConfig::set_dark_theme(dark); } - // Setup window size. let (width, height) = AppConfig::window_size(); - let mut viewport = egui::ViewportBuilder::default() .with_min_inner_size([AppConfig::MIN_WIDTH, AppConfig::MIN_HEIGHT]) .with_inner_size([width, height]); + // Setup an icon. if let Ok(icon) = eframe::icon_data::from_png_bytes(include_bytes!("../img/icon.png")) { viewport = viewport.with_icon(std::sync::Arc::new(icon)); @@ -93,6 +148,7 @@ fn start_desktop_gui() { // Setup window decorations. let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac; viewport = viewport + .with_window_level(egui::WindowLevel::Normal) .with_fullsize_content_view(true) .with_title_shown(false) .with_titlebar_buttons_shown(false) @@ -112,8 +168,18 @@ fn start_desktop_gui() { eframe::Renderer::Wgpu }; + let mut platform = grim::gui::platform::Desktop::new(data); + + // Start app socket at separate thread. + let socket_pl = platform.clone(); + platform = socket_pl.clone(); + std::thread::spawn(move || { + start_app_socket(socket_pl); + }); + // Start GUI. - match grim::start(options.clone(), grim::app_creator(grim::gui::App::new(platform.clone()))) { + let app = grim::gui::App::new(platform.clone()); + match grim::start(options.clone(), grim::app_creator(app)) { Ok(_) => {} Err(e) => { if win { @@ -121,7 +187,9 @@ fn start_desktop_gui() { } // Start with another renderer on error. options.renderer = eframe::Renderer::Glow; - match grim::start(options, grim::app_creator(grim::gui::App::new(platform))) { + + let app = grim::gui::App::new(platform); + match grim::start(options, grim::app_creator(app)) { Ok(_) => {} Err(e) => { panic!("{}", e); @@ -129,4 +197,69 @@ fn start_desktop_gui() { } } } +} + +/// Start socket that handles data for single application instance. +#[allow(dead_code)] +#[cfg(not(target_os = "android"))] +fn start_app_socket(platform: grim::gui::platform::Desktop) { + use tor_rtcompat::BlockOn; + let runtime = tor_rtcompat::tokio::TokioNativeTlsRuntime::create().unwrap(); + let _: Result<_, _> = runtime + .block_on(async { + use interprocess::local_socket::{ + tokio::{prelude::*, Stream}, + GenericFilePath, Listener, ListenerOptions, + }; + use std::io; + use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, + try_join, + }; + + // Handle incoming connection. + async fn handle_conn(conn: Stream) + -> io::Result { + let mut rec = BufReader::new(&conn); + let mut sen = &conn; + + let mut buffer = String::new(); + let send = sen.write_all(b""); + let recv = rec.read_line(&mut buffer); + + // Read data and send answer. + try_join!(recv, send)?; + + Ok(buffer) + } + + let socket_path = grim::Settings::socket_path(); + std::fs::remove_file(socket_path.clone()).unwrap(); + let name = socket_path.to_fs_name::()?; + let opts = ListenerOptions::new().name(name); + + // Create socket listener. + let listener = match opts.create_tokio() { + Err(e) if e.kind() == io::ErrorKind::AddrInUse => { + eprintln!("Socket file is occupied."); + return Err::(e); + } + x => x?, + }; + + // Handle connections. + loop { + let conn = match listener.accept().await { + Ok(c) => c, + Err(_) => continue + }; + let res = handle_conn(conn).await; + match res { + Ok(data) => { + platform.on_data(data) + }, + Err(_) => {} + } + } + }); } \ No newline at end of file diff --git a/src/settings/settings.rs b/src/settings/settings.rs index 0f7e947..5c5164c 100644 --- a/src/settings/settings.rs +++ b/src/settings/settings.rs @@ -141,6 +141,13 @@ impl Settings { path } + /// Get desktop application socket path. + pub fn socket_path() -> String { + let mut socket_path = Self::base_path(None); + socket_path.push("grim.socket"); + socket_path.to_str().unwrap().to_string() + } + /// Get configuration file path from provided name and sub-directory if needed. pub fn config_path(config_name: &str, sub_dir: Option) -> PathBuf { let mut path = Self::base_path(sub_dir); diff --git a/src/wallet/list.rs b/src/wallet/list.rs index 2298cc3..598fa3b 100644 --- a/src/wallet/list.rs +++ b/src/wallet/list.rs @@ -18,7 +18,8 @@ use grin_wallet_libwallet::Error; use crate::AppConfig; use crate::wallet::{Wallet, WalletConfig}; -/// Wrapper for [`Wallet`] list. +/// [`Wallet`] list container. +#[derive(Clone)] pub struct WalletList { /// List of wallets for [`ChainTypes::Mainnet`]. pub main_list: Vec, From fb7312cb804d2494a792301a836dd01354539c83 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Wed, 11 Sep 2024 21:13:52 +0300 Subject: [PATCH 05/28] desktop: request window focus on data --- src/gui/app.rs | 17 +++++++++++++++++ src/gui/platform/desktop/mod.rs | 24 +++++++++++------------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/gui/app.rs b/src/gui/app.rs index dbbbcd3..3fdfdb0 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -33,12 +33,17 @@ lazy_static! { pub struct App { /// Platform specific callbacks handler. pub platform: Platform, + /// Main content. content: Content, + /// Last window resize direction. resize_direction: Option, + /// Flag to check if it's first draw. first_draw: bool, + /// Flag to check if attention required after window focus. + attention_required: bool, } impl App { @@ -48,6 +53,7 @@ impl App { content: Content::default(), resize_direction: None, first_draw: true, + attention_required: false, } } @@ -109,8 +115,19 @@ impl App { // Provide incoming data to wallets. if let Some(data) = self.platform.consume_data() { self.content.wallets.on_data(ui, Some(data), &self.platform); + self.attention_required = true; } }); + + // Check if desktop window was focused after requested attention. + if View::is_desktop() && self.attention_required + && ctx.input(|i| i.viewport().focused.unwrap_or(true)) { + self.attention_required = false; + ctx.send_viewport_cmd( + ViewportCommand::RequestUserAttention(egui::UserAttentionType::Reset) + ); + ctx.send_viewport_cmd(ViewportCommand::WindowLevel(egui::WindowLevel::Normal)); + } } /// Draw custom resizeable window content. diff --git a/src/gui/platform/desktop/mod.rs b/src/gui/platform/desktop/mod.rs index 9b4b210..dbd266a 100644 --- a/src/gui/platform/desktop/mod.rs +++ b/src/gui/platform/desktop/mod.rs @@ -19,7 +19,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use parking_lot::RwLock; use lazy_static::lazy_static; -use egui::{UserAttentionType, ViewportCommand}; +use egui::{UserAttentionType, ViewportCommand, WindowLevel}; use rfd::FileDialog; use crate::gui::platform::PlatformCallbacks; @@ -126,14 +126,6 @@ impl PlatformCallbacks for Desktop { r_data.is_some() }; if has_data { - // Reset window state. - let r_ctx = self.ctx.read(); - if r_ctx.is_some() { - let ctx = r_ctx.as_ref().unwrap(); - ctx.send_viewport_cmd( - ViewportCommand::RequestUserAttention(UserAttentionType::Reset) - ); - } // Clear data. let mut w_data = PASSED_DATA.write(); let data = w_data.clone(); @@ -160,16 +152,22 @@ impl Desktop { let mut w_data = PASSED_DATA.write(); *w_data = Some(data); - // Bring focus on window. let r_ctx = self.ctx.read(); if r_ctx.is_some() { let ctx = r_ctx.as_ref().unwrap(); - ctx.send_viewport_cmd(ViewportCommand::Visible(true)); - ctx.send_viewport_cmd(ViewportCommand::Minimized(false)); + // Request attention on taskbar. ctx.send_viewport_cmd( ViewportCommand::RequestUserAttention(UserAttentionType::Informational) ); - ctx.send_viewport_cmd(ViewportCommand::Focus); + // Un-minimize window. + if ctx.input(|i| i.viewport().minimized.unwrap_or(false)) { + ctx.send_viewport_cmd(ViewportCommand::Minimized(false)); + } + // Focus to window. + if !ctx.input(|i| i.viewport().focused.unwrap_or(false)) { + ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::AlwaysOnTop)); + ctx.send_viewport_cmd(ViewportCommand::Focus); + } ctx.request_repaint(); } } From dd45f7ce382f1395aed9641870f438c3605dea28 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Thu, 12 Sep 2024 18:02:02 +0300 Subject: [PATCH 06/28] desktop: platform socket fix, file extension association for windows --- src/gui/app.rs | 6 ++- src/main.rs | 126 ++++++++++++++++++++++++------------------------- wix/main.wxs | 14 ++++-- 3 files changed, 77 insertions(+), 69 deletions(-) diff --git a/src/gui/app.rs b/src/gui/app.rs index 3fdfdb0..92ea0aa 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -114,13 +114,15 @@ impl App { // Provide incoming data to wallets. if let Some(data) = self.platform.consume_data() { - self.content.wallets.on_data(ui, Some(data), &self.platform); + if !data.is_empty() { + self.content.wallets.on_data(ui, Some(data), &self.platform); + } self.attention_required = true; } }); // Check if desktop window was focused after requested attention. - if View::is_desktop() && self.attention_required + if self.attention_required && View::is_desktop() && ctx.input(|i| i.viewport().focused.unwrap_or(true)) { self.attention_required = false; ctx.send_viewport_cmd( diff --git a/src/main.rs b/src/main.rs index 9b3a2e2..2944c0a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,7 +42,7 @@ fn real_main() { } // Check if another app instance already running. - if is_app_running(data.clone()) { + if is_app_running(&data) { return; } @@ -58,7 +58,7 @@ fn real_main() { // Save backtrace to file. let log = grim::Settings::crash_report_path(); if log.exists() { - std::fs::remove_file(log.clone()).unwrap(); + let _ = std::fs::remove_file(log.clone()); } std::fs::write(log, err.as_bytes()).unwrap(); // Setup flag to show crash after app restart. @@ -74,48 +74,7 @@ fn real_main() { } } -/// Check if application is already running to pass extra data. -#[allow(dead_code)] -#[cfg(not(target_os = "android"))] -fn is_app_running(data: Option) -> bool { - use tor_rtcompat::BlockOn; - let runtime = tor_rtcompat::tokio::TokioNativeTlsRuntime::create().unwrap(); - let res: Result<(), Box> = runtime - .block_on(async { - use interprocess::local_socket::{ - tokio::{prelude::*, Stream}, - GenericFilePath - }; - use tokio::{ - io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, - try_join, - }; - - let socket_path = grim::Settings::socket_path(); - let name = socket_path.to_fs_name::()?; - // Connect to running application socket. - let conn = Stream::connect(name).await?; - - let (rec, mut sen) = conn.split(); - let mut rec = BufReader::new(rec); - let data = data.unwrap_or("".to_string()); - let mut buffer = String::with_capacity(data.len()); - - // Send extra data to socket. - let send = sen.write_all(data.as_bytes()); - let recv = rec.read_line(&mut buffer); - try_join!(send, recv)?; - - drop((rec, sen)); - Ok(()) - }); - return match res { - Ok(_) => true, - Err(_) => false - } -} - -/// Start GUI with Desktop related setup passing extra data from opening. +/// Start GUI with Desktop related setup passing data from opening. #[allow(dead_code)] #[cfg(not(target_os = "android"))] fn start_desktop_gui(data: Option) { @@ -199,7 +158,46 @@ fn start_desktop_gui(data: Option) { } } -/// Start socket that handles data for single application instance. +/// Check if application is already running to pass data. +#[allow(dead_code)] +#[cfg(not(target_os = "android"))] +fn is_app_running(data: &Option) -> bool { + use tor_rtcompat::BlockOn; + let runtime = tor_rtcompat::tokio::TokioNativeTlsRuntime::create().unwrap(); + let res: Result<(), Box> = runtime + .block_on(async { + use interprocess::local_socket::{ + tokio::{prelude::*, Stream}, + GenericFilePath, GenericNamespaced + }; + use tokio::{ + io::AsyncWriteExt, + }; + + let socket_path = grim::Settings::socket_path(); + let name = if GenericNamespaced::is_supported() { + "grim.sock".to_ns_name::()? + } else { + socket_path.clone().to_fs_name::()? + }; + // Connect to running application socket. + let conn = Stream::connect(name).await?; + let (rec, mut sen) = conn.split(); + + // Send data to socket. + let data = data.clone().unwrap_or("".to_string()); + let _ = sen.write_all(data.as_bytes()).await; + + drop((rec, sen)); + Ok(()) + }); + return match res { + Ok(_) => true, + Err(_) => false + } +} + +/// Start desktop socket that handles data for single application instance. #[allow(dead_code)] #[cfg(not(target_os = "android"))] fn start_app_socket(platform: grim::gui::platform::Desktop) { @@ -209,36 +207,35 @@ fn start_app_socket(platform: grim::gui::platform::Desktop) { .block_on(async { use interprocess::local_socket::{ tokio::{prelude::*, Stream}, - GenericFilePath, Listener, ListenerOptions, + GenericFilePath, GenericNamespaced, Listener, ListenerOptions, }; use std::io; use tokio::{ - io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, - try_join, + io::{AsyncBufReadExt, BufReader}, }; // Handle incoming connection. async fn handle_conn(conn: Stream) - -> io::Result { - let mut rec = BufReader::new(&conn); - let mut sen = &conn; - + -> io::Result { + let mut read = BufReader::new(&conn); let mut buffer = String::new(); - let send = sen.write_all(b""); - let recv = rec.read_line(&mut buffer); - - // Read data and send answer. - try_join!(recv, send)?; - + // Read data. + let _ = read.read_line(&mut buffer).await; Ok(buffer) } let socket_path = grim::Settings::socket_path(); - std::fs::remove_file(socket_path.clone()).unwrap(); - let name = socket_path.to_fs_name::()?; - let opts = ListenerOptions::new().name(name); + let name = if GenericNamespaced::is_supported() { + "grim.sock".to_ns_name::()? + } else { + socket_path.clone().to_fs_name::()? + }; + if socket_path.exists() { + let _ = std::fs::remove_file(socket_path); + } - // Create socket listener. + // Create listener. + let opts = ListenerOptions::new().name(name); let listener = match opts.create_tokio() { Err(e) if e.kind() == io::ErrorKind::AddrInUse => { eprintln!("Socket file is occupied."); @@ -251,7 +248,10 @@ fn start_app_socket(platform: grim::gui::platform::Desktop) { loop { let conn = match listener.accept().await { Ok(c) => c, - Err(_) => continue + Err(e) => { + println!("{:?}", e); + continue + } }; let res = handle_conn(conn).await; match res { diff --git a/wix/main.wxs b/wix/main.wxs index 2735064..ef83099 100644 --- a/wix/main.wxs +++ b/wix/main.wxs @@ -16,8 +16,8 @@ AllowSameVersionUpgrades = "yes" /> - - + + @@ -28,7 +28,7 @@ @@ -55,6 +55,12 @@ + + + + + + @@ -64,7 +70,7 @@ From d78ec570b08430c78d6606de6007eda5a19735d2 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Thu, 12 Sep 2024 21:27:37 +0300 Subject: [PATCH 07/28] platform: passed data at lib, desktop user attention, check existing file on share at android --- src/gui/app.rs | 20 ++--- src/gui/platform/android/mod.rs | 27 ++++-- src/gui/platform/desktop/mod.rs | 71 +++++++-------- src/gui/platform/mod.rs | 4 +- src/lib.rs | 30 +++++++ src/main.rs | 154 ++++++++++++++++---------------- 6 files changed, 172 insertions(+), 134 deletions(-) diff --git a/src/gui/app.rs b/src/gui/app.rs index 92ea0aa..4e1c07a 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -42,8 +42,6 @@ pub struct App { /// Flag to check if it's first draw. first_draw: bool, - /// Flag to check if attention required after window focus. - attention_required: bool, } impl App { @@ -52,14 +50,13 @@ impl App { platform, content: Content::default(), resize_direction: None, - first_draw: true, - attention_required: false, + first_draw: true } } /// Draw application content. pub fn ui(&mut self, ctx: &Context) { - // Set Desktop platform context on first draw. + // Set platform context on first draw. if self.first_draw { if View::is_desktop() { self.platform.set_context(ctx); @@ -113,22 +110,17 @@ impl App { } // Provide incoming data to wallets. - if let Some(data) = self.platform.consume_data() { + if let Some(data) = crate::consume_passed_data() { if !data.is_empty() { self.content.wallets.on_data(ui, Some(data), &self.platform); } - self.attention_required = true; } }); // Check if desktop window was focused after requested attention. - if self.attention_required && View::is_desktop() - && ctx.input(|i| i.viewport().focused.unwrap_or(true)) { - self.attention_required = false; - ctx.send_viewport_cmd( - ViewportCommand::RequestUserAttention(egui::UserAttentionType::Reset) - ); - ctx.send_viewport_cmd(ViewportCommand::WindowLevel(egui::WindowLevel::Normal)); + if self.platform.user_attention_required() && + ctx.input(|i| i.viewport().focused.unwrap_or(true)) { + self.platform.clear_user_attention(); } } diff --git a/src/gui/platform/android/mod.rs b/src/gui/platform/android/mod.rs index 7fbe6ac..fce8afc 100644 --- a/src/gui/platform/android/mod.rs +++ b/src/gui/platform/android/mod.rs @@ -30,7 +30,11 @@ use crate::gui::platform::PlatformCallbacks; /// Android platform implementation. #[derive(Clone)] pub struct Android { + /// Android related state. android_app: AndroidApp, + + /// Context to repaint content and handle viewport commands. + ctx: Arc>>, } impl Android { @@ -38,6 +42,7 @@ impl Android { pub fn new(app: AndroidApp) -> Self { Self { android_app: app, + ctx: Arc::new(RwLock::new(None)), } } @@ -56,6 +61,11 @@ impl Android { } impl PlatformCallbacks for Android { + fn set_context(&mut self, ctx: &egui::Context) { + let mut w_ctx = self.ctx.write(); + *w_ctx = Some(ctx.clone()); + } + fn show_keyboard(&self) { // Disable NDK soft input show call before fix for egui. // self.android_app.show_soft_input(false); @@ -131,9 +141,12 @@ impl PlatformCallbacks for Android { cache.push("images"); std::fs::create_dir_all(cache.to_str().unwrap())?; cache.push(name); - let mut image = File::create_new(cache.clone()).unwrap(); - image.write_all(data.as_slice()).unwrap(); - image.sync_all().unwrap(); + if cache.exists() { + std::fs::remove_file(cache.clone())?; + } + let mut image = File::create_new(cache.clone())?; + image.write_all(data.as_slice())?; + image.sync_all()?; // Call share modal at system. let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); let env = vm.attach_current_thread().unwrap(); @@ -168,9 +181,13 @@ impl PlatformCallbacks for Android { None } - fn consume_data(&mut self) -> Option { - None + fn request_user_attention(&self) {} + + fn user_attention_required(&self) -> bool { + false } + + fn clear_user_attention(&self) {} } lazy_static! { diff --git a/src/gui/platform/desktop/mod.rs b/src/gui/platform/desktop/mod.rs index dbd266a..bc18707 100644 --- a/src/gui/platform/desktop/mod.rs +++ b/src/gui/platform/desktop/mod.rs @@ -27,10 +27,14 @@ use crate::gui::platform::PlatformCallbacks; /// Desktop platform related actions. #[derive(Clone)] pub struct Desktop { - /// Flag to check if camera stop is needed. - stop_camera: Arc, /// Context to repaint content and handle viewport commands. ctx: Arc>>, + + /// Flag to check if camera stop is needed. + stop_camera: Arc, + + /// Flag to check if attention required after window focusing. + attention_required: Arc, } impl PlatformCallbacks for Desktop { @@ -120,38 +124,7 @@ impl PlatformCallbacks for Desktop { None } - fn consume_data(&mut self) -> Option { - let has_data = { - let r_data = PASSED_DATA.read(); - r_data.is_some() - }; - if has_data { - // Clear data. - let mut w_data = PASSED_DATA.write(); - let data = w_data.clone(); - *w_data = None; - return data; - } - None - } -} - -impl Desktop { - /// Create new instance with provided extra data from app opening. - pub fn new(data: Option) -> Self { - let mut w_data = PASSED_DATA.write(); - *w_data = data; - Self { - stop_camera: Arc::new(AtomicBool::new(false)), - ctx: Arc::new(RwLock::new(None)), - } - } - - /// Handle data passed to application. - pub fn on_data(&self, data: String) { - let mut w_data = PASSED_DATA.write(); - *w_data = Some(data); - + fn request_user_attention(&self) { let r_ctx = self.ctx.read(); if r_ctx.is_some() { let ctx = r_ctx.as_ref().unwrap(); @@ -170,6 +143,33 @@ impl Desktop { } ctx.request_repaint(); } + self.attention_required.store(true, Ordering::Relaxed); + } + + fn user_attention_required(&self) -> bool { + self.attention_required.load(Ordering::Relaxed) + } + + fn clear_user_attention(&self) { + let r_ctx = self.ctx.read(); + if r_ctx.is_some() { + let ctx = r_ctx.as_ref().unwrap(); + ctx.send_viewport_cmd( + ViewportCommand::RequestUserAttention(UserAttentionType::Reset) + ); + ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::Normal)); + } + self.attention_required.store(false, Ordering::Relaxed); + } +} + +impl Desktop { + pub fn new() -> Self { + Self { + stop_camera: Arc::new(AtomicBool::new(false)), + ctx: Arc::new(RwLock::new(None)), + attention_required: Arc::new(AtomicBool::new(false)), + } } #[allow(dead_code)] @@ -255,7 +255,4 @@ impl Desktop { lazy_static! { /// Last captured image from started camera. static ref LAST_CAMERA_IMAGE: Arc, u32)>>> = Arc::new(RwLock::new(None)); - - /// Data passed from deeplink or opened file. - static ref PASSED_DATA: Arc>> = Arc::new(RwLock::new(None)); } diff --git a/src/gui/platform/mod.rs b/src/gui/platform/mod.rs index 06f6a3e..605924e 100644 --- a/src/gui/platform/mod.rs +++ b/src/gui/platform/mod.rs @@ -35,5 +35,7 @@ pub trait PlatformCallbacks { fn share_data(&self, name: String, data: Vec) -> Result<(), std::io::Error>; fn pick_file(&self) -> Option; fn picked_file(&self) -> Option; - fn consume_data(&mut self) -> Option; + fn request_user_attention(&self); + fn user_attention_required(&self) -> bool; + fn clear_user_attention(&self); } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 54ac58b..aba2528 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,9 @@ extern crate rust_i18n; use eframe::NativeOptions; use egui::{Context, Stroke}; +use lazy_static::lazy_static; +use std::sync::Arc; +use parking_lot::RwLock; #[cfg(target_os = "android")] use winit::platform::android::activity::AndroidApp; @@ -255,4 +258,31 @@ fn setup_i18n() { rust_i18n::set_locale(AppConfig::DEFAULT_LOCALE); } } +} + +/// Get data provided from deeplink or opened file. +pub fn consume_passed_data() -> Option { + let has_data = { + let r_data = PASSED_DATA.read(); + r_data.is_some() + }; + if has_data { + // Clear data. + let mut w_data = PASSED_DATA.write(); + let data = w_data.clone(); + *w_data = None; + return data; + } + None +} + +/// Provide data from deeplink or opened file. +pub fn on_data(data: String) { + let mut w_data = PASSED_DATA.write(); + *w_data = Some(data); +} + +lazy_static! { + /// Data provided from deeplink or opened file. + pub static ref PASSED_DATA: Arc>> = Arc::new(RwLock::new(None)); } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 2944c0a..9bb0185 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,11 +41,6 @@ fn real_main() { data = content } - // Check if another app instance already running. - if is_app_running(&data) { - return; - } - // Setup callback on panic crash. std::panic::set_hook(Box::new(|info| { let backtrace = backtrace::Backtrace::new(); @@ -67,7 +62,14 @@ fn real_main() { // Start GUI. match std::panic::catch_unwind(|| { - start_desktop_gui(data); + if is_app_running(&data) { + return; + } else if let Some(data) = data { + grim::on_data(data); + } + let platform = grim::gui::platform::Desktop::new(); + start_app_socket(platform.clone()); + start_desktop_gui(platform); }) { Ok(_) => {} Err(e) => println!("{:?}", e) @@ -77,7 +79,7 @@ fn real_main() { /// Start GUI with Desktop related setup passing data from opening. #[allow(dead_code)] #[cfg(not(target_os = "android"))] -fn start_desktop_gui(data: Option) { +fn start_desktop_gui(platform: grim::gui::platform::Desktop) { use grim::AppConfig; use dark_light::Mode; @@ -127,15 +129,6 @@ fn start_desktop_gui(data: Option) { eframe::Renderer::Wgpu }; - let mut platform = grim::gui::platform::Desktop::new(data); - - // Start app socket at separate thread. - let socket_pl = platform.clone(); - platform = socket_pl.clone(); - std::thread::spawn(move || { - start_app_socket(socket_pl); - }); - // Start GUI. let app = grim::gui::App::new(platform.clone()); match grim::start(options.clone(), grim::app_creator(app)) { @@ -176,16 +169,19 @@ fn is_app_running(data: &Option) -> bool { let socket_path = grim::Settings::socket_path(); let name = if GenericNamespaced::is_supported() { - "grim.sock".to_ns_name::()? + grim::Settings::SOCKET_NAME.to_ns_name::()? } else { socket_path.clone().to_fs_name::()? }; // Connect to running application socket. let conn = Stream::connect(name).await?; + let data = data.clone().unwrap_or("".to_string()); + if data.is_empty() { + return Ok(()); + } let (rec, mut sen) = conn.split(); // Send data to socket. - let data = data.clone().unwrap_or("".to_string()); let _ = sen.write_all(data.as_bytes()).await; drop((rec, sen)); @@ -201,65 +197,69 @@ fn is_app_running(data: &Option) -> bool { #[allow(dead_code)] #[cfg(not(target_os = "android"))] fn start_app_socket(platform: grim::gui::platform::Desktop) { - use tor_rtcompat::BlockOn; - let runtime = tor_rtcompat::tokio::TokioNativeTlsRuntime::create().unwrap(); - let _: Result<_, _> = runtime - .block_on(async { - use interprocess::local_socket::{ - tokio::{prelude::*, Stream}, - GenericFilePath, GenericNamespaced, Listener, ListenerOptions, - }; - use std::io; - use tokio::{ - io::{AsyncBufReadExt, BufReader}, - }; - - // Handle incoming connection. - async fn handle_conn(conn: Stream) - -> io::Result { - let mut read = BufReader::new(&conn); - let mut buffer = String::new(); - // Read data. - let _ = read.read_line(&mut buffer).await; - Ok(buffer) - } - - let socket_path = grim::Settings::socket_path(); - let name = if GenericNamespaced::is_supported() { - "grim.sock".to_ns_name::()? - } else { - socket_path.clone().to_fs_name::()? - }; - if socket_path.exists() { - let _ = std::fs::remove_file(socket_path); - } - - // Create listener. - let opts = ListenerOptions::new().name(name); - let listener = match opts.create_tokio() { - Err(e) if e.kind() == io::ErrorKind::AddrInUse => { - eprintln!("Socket file is occupied."); - return Err::(e); - } - x => x?, - }; - - // Handle connections. - loop { - let conn = match listener.accept().await { - Ok(c) => c, - Err(e) => { - println!("{:?}", e); - continue - } + std::thread::spawn(move || { + use tor_rtcompat::BlockOn; + let runtime = tor_rtcompat::tokio::TokioNativeTlsRuntime::create().unwrap(); + let _: Result<_, _> = runtime + .block_on(async { + use interprocess::local_socket::{ + tokio::{prelude::*, Stream}, + GenericFilePath, GenericNamespaced, Listener, ListenerOptions, }; - let res = handle_conn(conn).await; - match res { - Ok(data) => { - platform.on_data(data) - }, - Err(_) => {} + use std::io; + use tokio::{ + io::{AsyncBufReadExt, BufReader}, + }; + use grim::gui::platform::PlatformCallbacks; + + // Handle incoming connection. + async fn handle_conn(conn: Stream) + -> io::Result { + let mut read = BufReader::new(&conn); + let mut buffer = String::new(); + // Read data. + let _ = read.read_line(&mut buffer).await; + Ok(buffer) } - } - }); + + let socket_path = grim::Settings::socket_path(); + let name = if GenericNamespaced::is_supported() { + grim::Settings::SOCKET_NAME.to_ns_name::()? + } else { + socket_path.clone().to_fs_name::()? + }; + if socket_path.exists() { + let _ = std::fs::remove_file(socket_path); + } + + // Create listener. + let opts = ListenerOptions::new().name(name); + let listener = match opts.create_tokio() { + Err(e) if e.kind() == io::ErrorKind::AddrInUse => { + eprintln!("Socket file is occupied."); + return Err::(e); + } + x => x?, + }; + + loop { + let conn = match listener.accept().await { + Ok(c) => c, + Err(e) => { + println!("{:?}", e); + continue + } + }; + // Handle connection. + let res = handle_conn(conn).await; + match res { + Ok(data) => { + grim::on_data(data); + platform.request_user_attention(); + }, + Err(_) => {} + } + } + }); + }); } \ No newline at end of file From c73cd58eed613c7a1bcbad884d702626de5ec431 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 13 Sep 2024 14:22:15 +0300 Subject: [PATCH 08/28] platform: android file opening, better exit --- android/app/src/main/AndroidManifest.xml | 25 +++- .../mw/gri/android/BackgroundService.java | 18 ++- .../java/mw/gri/android/MainActivity.java | 121 ++++++++++++++---- src/gui/app.rs | 2 +- src/gui/platform/android/mod.rs | 18 +-- src/gui/platform/desktop/mod.rs | 8 ++ src/gui/platform/mod.rs | 1 + src/gui/views/content.rs | 12 +- src/gui/views/wallets/content.rs | 11 +- src/lib.rs | 36 +++++- src/settings/settings.rs | 9 +- 11 files changed, 193 insertions(+), 68 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 04e6d7b..39aadbd 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,15 +1,17 @@ - - + - + + + @@ -44,6 +45,22 @@ + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/mw/gri/android/BackgroundService.java b/android/app/src/main/java/mw/gri/android/BackgroundService.java index f34b700..bb668a5 100644 --- a/android/app/src/main/java/mw/gri/android/BackgroundService.java +++ b/android/app/src/main/java/mw/gri/android/BackgroundService.java @@ -152,13 +152,17 @@ public class BackgroundService extends Service { // Show notification with sync status. Intent i = getPackageManager().getLaunchIntentForPackage(this.getPackageName()); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_IMMUTABLE); - mNotificationBuilder = new NotificationCompat.Builder(this, TAG) - .setContentTitle(this.getSyncTitle()) - .setContentText(this.getSyncStatusText()) - .setStyle(new NotificationCompat.BigTextStyle().bigText(this.getSyncStatusText())) - .setSmallIcon(R.drawable.ic_stat_name) - .setPriority(NotificationCompat.PRIORITY_MAX) - .setContentIntent(pendingIntent); + try { + mNotificationBuilder = new NotificationCompat.Builder(this, TAG) + .setContentTitle(this.getSyncTitle()) + .setContentText(this.getSyncStatusText()) + .setStyle(new NotificationCompat.BigTextStyle().bigText(this.getSyncStatusText())) + .setSmallIcon(R.drawable.ic_stat_name) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setContentIntent(pendingIntent); + } catch (UnsatisfiedLinkError e) { + return; + } Notification notification = mNotificationBuilder.build(); // Start service at foreground state to prevent killing by system. diff --git a/android/app/src/main/java/mw/gri/android/MainActivity.java b/android/app/src/main/java/mw/gri/android/MainActivity.java index 409d23c..a589934 100644 --- a/android/app/src/main/java/mw/gri/android/MainActivity.java +++ b/android/app/src/main/java/mw/gri/android/MainActivity.java @@ -7,9 +7,9 @@ import android.content.*; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.Uri; -import android.os.Build; -import android.os.Bundle; +import android.os.*; import android.os.Process; +import android.provider.Settings; import android.system.ErrnoException; import android.system.Os; import android.util.Size; @@ -51,8 +51,7 @@ public class MainActivity extends GameActivity { @Override public void onReceive(Context ctx, Intent i) { if (i.getAction().equals(STOP_APP_ACTION)) { - onExit(); - Process.killProcess(Process.myPid()); + exit(); } } }; @@ -67,11 +66,19 @@ public class MainActivity extends GameActivity { private ExecutorService mCameraExecutor = null; private boolean mUseBackCamera = true; - private ActivityResultLauncher mFilePickResultLauncher = null; + private ActivityResultLauncher mFilePickResult = null; + private ActivityResultLauncher mOpenFilePermissionsResult = null; @SuppressLint("UnspecifiedRegisterReceiverFlag") @Override protected void onCreate(Bundle savedInstanceState) { + // Check if activity was launched to exclude from recent apps on exit. + if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0) { + super.onCreate(null); + finish(); + return; + } + // Clear cache on start. if (savedInstanceState == null) { Utils.deleteDirectoryContent(new File(getExternalCacheDir().getPath()), false); @@ -91,8 +98,21 @@ public class MainActivity extends GameActivity { // Register receiver to finish activity from the BackgroundService. registerReceiver(mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION)); - // Register file pick result launcher. - mFilePickResultLauncher = registerForActivityResult( + // Register associated file opening result. + mOpenFilePermissionsResult = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (Build.VERSION.SDK_INT >= 30) { + if (Environment.isExternalStorageManager()) { + onFile(); + } + } else if (result.getResultCode() == RESULT_OK) { + onFile(); + } + } + ); + // Register file pick result. + mFilePickResult = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { int resultCode = result.getResultCode(); @@ -105,11 +125,11 @@ public class MainActivity extends GameActivity { File file = new File(getExternalCacheDir(), name); try (InputStream is = getContentResolver().openInputStream(uri); OutputStream os = new FileOutputStream(file)) { - byte[] buffer = new byte[1024]; - int length; - while ((length = is.read(buffer)) > 0) { - os.write(buffer, 0, length); - } + byte[] buffer = new byte[1024]; + int length; + while ((length = is.read(buffer)) > 0) { + os.write(buffer, 0, length); + } } catch (Exception e) { e.printStackTrace(); } @@ -124,7 +144,7 @@ public class MainActivity extends GameActivity { // Listener for display insets (cutouts) to pass values into native code. View content = getWindow().getDecorView().findViewById(android.R.id.content); ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> { - // Setup cutouts values. + // Get display cutouts. DisplayCutoutCompat dc = insets.getDisplayCutout(); int cutoutTop = 0; int cutoutRight = 0; @@ -140,7 +160,7 @@ public class MainActivity extends GameActivity { // Get display insets. Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); - // Setup values to pass into native code. + // Pass values into native code. int[] values = new int[]{0, 0, 0, 0}; values[0] = Utils.pxToDp(Integer.max(cutoutTop, systemBars.top), this); values[1] = Utils.pxToDp(Integer.max(cutoutRight, systemBars.right), this); @@ -166,8 +186,61 @@ public class MainActivity extends GameActivity { BackgroundService.start(this); } }); + + // Check if intent has data on launch. + if (savedInstanceState == null) { + onNewIntent(getIntent()); + } } + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + String action = intent.getAction(); + // Check if file was open with the application. + if (action != null && action.equals(Intent.ACTION_VIEW)) { + Intent i = getIntent(); + i.setData(intent.getData()); + setIntent(i); + onFile(); + } + } + + // Callback when associated file was open. + private void onFile() { + Uri data = getIntent().getData(); + if (data == null) { + return; + } + if (Build.VERSION.SDK_INT >= 30) { + if (!Environment.isExternalStorageManager()) { + Intent i = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); + mOpenFilePermissionsResult.launch(i); + return; + } + } + try { + ParcelFileDescriptor parcelFile = getContentResolver().openFileDescriptor(data, "r"); + FileReader fileReader = new FileReader(parcelFile.getFileDescriptor()); + BufferedReader reader = new BufferedReader(fileReader); + String line; + StringBuilder buff = new StringBuilder(); + while ((line = reader.readLine()) != null) { + buff.append(line); + } + reader.close(); + fileReader.close(); + + // Provide file content into native code. + onData(buff.toString()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + // Pass data into native code. + public native void onData(String data); + @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); @@ -232,17 +305,17 @@ public class MainActivity extends GameActivity { // Implemented into native code to handle key code BACK event. public native void onBack(); - // Actions on app exit. - private void onExit() { - unregisterReceiver(mBroadcastReceiver); - BackgroundService.stop(this); + // Called from native code to exit app. + public void exit() { + finishAndRemoveTask(); } @Override protected void onDestroy() { - onExit(); + unregisterReceiver(mBroadcastReceiver); + BackgroundService.stop(this); - // Kill process after 3 seconds if app was terminated from recent apps to prevent app hanging. + // Kill process after 3 secs if app was terminated from recent apps to prevent app hang. new Thread(() -> { try { onTermination(); @@ -253,9 +326,7 @@ public class MainActivity extends GameActivity { } }).start(); - // Destroy an app and kill process. super.onDestroy(); - Process.killProcess(Process.myPid()); } // Notify native code to stop activity (e.g. node) if app was terminated from recent apps. @@ -298,18 +369,16 @@ public class MainActivity extends GameActivity { // Called from native code to start camera. public void startCamera() { - // Check permissions. String notificationsPermission = Manifest.permission.CAMERA; if (checkSelfPermission(notificationsPermission) != PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[] { notificationsPermission }, CAMERA_PERMISSION_CODE); } else { - // Start . if (mCameraProviderFuture == null) { mCameraProviderFuture = ProcessCameraProvider.getInstance(this); mCameraProviderFuture.addListener(() -> { try { mCameraProvider = mCameraProviderFuture.get(); - // Launch camera. + // Start camera. openCamera(); } catch (Exception e) { View content = findViewById(android.R.id.content); @@ -402,7 +471,7 @@ public class MainActivity extends GameActivity { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*"); try { - mFilePickResultLauncher.launch(Intent.createChooser(intent, "Pick file")); + mFilePickResult.launch(Intent.createChooser(intent, "Pick file")); } catch (android.content.ActivityNotFoundException ex) { onFilePick(""); } diff --git a/src/gui/app.rs b/src/gui/app.rs index 4e1c07a..f95176a 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -110,7 +110,7 @@ impl App { } // Provide incoming data to wallets. - if let Some(data) = crate::consume_passed_data() { + if let Some(data) = crate::consume_incoming_data() { if !data.is_empty() { self.content.wallets.on_data(ui, Some(data), &self.platform); } diff --git a/src/gui/platform/android/mod.rs b/src/gui/platform/android/mod.rs index fce8afc..b93739c 100644 --- a/src/gui/platform/android/mod.rs +++ b/src/gui/platform/android/mod.rs @@ -66,6 +66,10 @@ impl PlatformCallbacks for Android { *w_ctx = Some(ctx.clone()); } + fn exit(&self) { + self.call_java_method("exit", "()V", &[]).unwrap(); + } + fn show_keyboard(&self) { // Disable NDK soft input show call before fix for egui. // self.android_app.show_soft_input(false); @@ -137,20 +141,18 @@ impl PlatformCallbacks for Android { fn share_data(&self, name: String, data: Vec) -> Result<(), std::io::Error> { // Create file at cache dir. let default_cache = OsString::from(dirs::cache_dir().unwrap()); - let mut cache = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache)); - cache.push("images"); - std::fs::create_dir_all(cache.to_str().unwrap())?; - cache.push(name); - if cache.exists() { - std::fs::remove_file(cache.clone())?; + let mut file = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache)); + file.push(name); + if file.exists() { + std::fs::remove_file(file.clone())?; } - let mut image = File::create_new(cache.clone())?; + let mut image = File::create_new(file.clone())?; image.write_all(data.as_slice())?; image.sync_all()?; // Call share modal at system. let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); let env = vm.attach_current_thread().unwrap(); - let arg_value = env.new_string(cache.to_str().unwrap()).unwrap(); + let arg_value = env.new_string(file.to_str().unwrap()).unwrap(); self.call_java_method("shareImage", "(Ljava/lang/String;)V", &[JValue::Object(&JObject::from(arg_value))]).unwrap(); diff --git a/src/gui/platform/desktop/mod.rs b/src/gui/platform/desktop/mod.rs index bc18707..85c0f0b 100644 --- a/src/gui/platform/desktop/mod.rs +++ b/src/gui/platform/desktop/mod.rs @@ -43,6 +43,14 @@ impl PlatformCallbacks for Desktop { *w_ctx = Some(ctx.clone()); } + fn exit(&self) { + let r_ctx = self.ctx.read(); + if r_ctx.is_some() { + let ctx = r_ctx.as_ref().unwrap(); + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } + } + fn show_keyboard(&self) {} fn hide_keyboard(&self) {} diff --git a/src/gui/platform/mod.rs b/src/gui/platform/mod.rs index 605924e..cd4ed15 100644 --- a/src/gui/platform/mod.rs +++ b/src/gui/platform/mod.rs @@ -23,6 +23,7 @@ pub mod platform; pub trait PlatformCallbacks { fn set_context(&mut self, ctx: &egui::Context); + fn exit(&self); fn show_keyboard(&self); fn hide_keyboard(&self); fn copy_string_to_buffer(&self, data: String); diff --git a/src/gui/views/content.rs b/src/gui/views/content.rs index 8780813..bc36690 100644 --- a/src/gui/views/content.rs +++ b/src/gui/views/content.rs @@ -40,8 +40,8 @@ pub struct Content { /// Central panel [`WalletsContent`] content. pub wallets: WalletsContent, - /// Check if app exit is allowed on close event of [`eframe::App`] implementation. - pub(crate) exit_allowed: bool, + /// Check if app exit is allowed on Desktop close event. + pub exit_allowed: bool, /// Flag to show exit progress at [`Modal`]. show_exit_progress: bool, @@ -83,7 +83,7 @@ impl ModalContainer for Content { modal: &Modal, cb: &dyn PlatformCallbacks) { match modal.id { - Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal), + Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb), Self::SETTINGS_MODAL => self.settings_modal_ui(ui, modal), Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal), Self::CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb), @@ -206,11 +206,11 @@ impl Content { } /// Draw exit confirmation modal content. - fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal) { + fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { if self.show_exit_progress { if !Node::is_running() { self.exit_allowed = true; - ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close); + cb.exit(); modal.close(); } ui.add_space(16.0); @@ -244,7 +244,7 @@ impl Content { View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |ui| { if !Node::is_running() { self.exit_allowed = true; - ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close); + cb.exit(); modal.close(); } else { Node::stop(true); diff --git a/src/gui/views/wallets/content.rs b/src/gui/views/wallets/content.rs index 45ca51d..df5495b 100644 --- a/src/gui/views/wallets/content.rs +++ b/src/gui/views/wallets/content.rs @@ -36,12 +36,10 @@ pub struct WalletsContent { /// Wallet selection [`Modal`] content. wallet_selection_content: Option, - /// Wallet opening [`Modal`] content. open_wallet_content: Option, - /// Wallet connection selection content. - conn_modal_content: Option, + conn_selection_content: Option, /// Selected [`Wallet`] content. wallet_content: WalletContent, @@ -70,7 +68,7 @@ impl Default for WalletsContent { wallets: WalletList::default(), wallet_selection_content: None, open_wallet_content: None, - conn_modal_content: None, + conn_selection_content: None, wallet_content: WalletContent::new(None), creation_content: WalletCreation::default(), show_wallets_at_dual_panel: AppConfig::show_wallets_at_dual_panel(), @@ -106,7 +104,7 @@ impl ModalContainer for WalletsContent { self.creation_content.name_pass_modal_ui(ui, modal, cb) }, CONNECTION_SELECTION_MODAL => { - if let Some(content) = self.conn_modal_content.as_mut() { + if let Some(content) = self.conn_selection_content.as_mut() { content.ui(ui, modal, cb, |id| { // Update wallet connection on select. let list = self.wallets.list(); @@ -303,6 +301,7 @@ impl WalletsContent { } } + /// Show wallet selection with provided optional data. fn show_wallet_selection_modal(&mut self, data: Option) { self.wallet_selection_content = Some(WalletsModal::new(None, data, true)); // Show wallet selection modal. @@ -557,7 +556,7 @@ impl WalletsContent { /// Show [`Modal`] to select connection for the wallet. fn show_connection_selector_modal(&mut self, wallet: &Wallet) { let ext_conn = wallet.get_current_ext_conn(); - self.conn_modal_content = Some(WalletConnectionModal::new(ext_conn)); + self.conn_selection_content = Some(WalletConnectionModal::new(ext_conn)); // Show modal. Modal::new(CONNECTION_SELECTION_MODAL) .position(ModalPosition::CenterTop) diff --git a/src/lib.rs b/src/lib.rs index aba2528..a6d8840 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -260,15 +260,15 @@ fn setup_i18n() { } } -/// Get data provided from deeplink or opened file. -pub fn consume_passed_data() -> Option { +/// Get data from deeplink or opened file. +pub fn consume_incoming_data() -> Option { let has_data = { - let r_data = PASSED_DATA.read(); + let r_data = INCOMING_DATA.read(); r_data.is_some() }; if has_data { // Clear data. - let mut w_data = PASSED_DATA.write(); + let mut w_data = INCOMING_DATA.write(); let data = w_data.clone(); *w_data = None; return data; @@ -278,11 +278,35 @@ pub fn consume_passed_data() -> Option { /// Provide data from deeplink or opened file. pub fn on_data(data: String) { - let mut w_data = PASSED_DATA.write(); + let mut w_data = INCOMING_DATA.write(); *w_data = Some(data); } lazy_static! { /// Data provided from deeplink or opened file. - pub static ref PASSED_DATA: Arc>> = Arc::new(RwLock::new(None)); + pub static ref INCOMING_DATA: Arc>> = Arc::new(RwLock::new(None)); +} + +/// Callback from Java code with with passed data. +#[allow(dead_code)] +#[allow(non_snake_case)] +#[cfg(target_os = "android")] +#[no_mangle] +pub extern "C" fn Java_mw_gri_android_MainActivity_onData( + _env: jni::JNIEnv, + _class: jni::objects::JObject, + char: jni::sys::jstring +) { + unsafe { + let j_obj = jni::objects::JString::from_raw(char); + if let Ok(j_str) = _env.get_string_unchecked(j_obj.as_ref()) { + match j_str.to_str() { + Ok(str) => { + let mut w_path = INCOMING_DATA.write(); + *w_path = Some(str.to_string()); + } + Err(_) => {} + } + }; + } } \ No newline at end of file diff --git a/src/settings/settings.rs b/src/settings/settings.rs index 5c5164c..c065f59 100644 --- a/src/settings/settings.rs +++ b/src/settings/settings.rs @@ -48,9 +48,10 @@ pub struct Settings { impl Settings { /// Main application directory name. pub const MAIN_DIR_NAME: &'static str = ".grim"; - /// Crash report file name. pub const CRASH_REPORT_FILE_NAME: &'static str = "crash.log"; + /// Application socket name. + pub const SOCKET_NAME: &'static str = "grim.sock"; /// Initialize settings with app and node configs. fn init() -> Self { @@ -142,10 +143,10 @@ impl Settings { } /// Get desktop application socket path. - pub fn socket_path() -> String { + pub fn socket_path() -> PathBuf { let mut socket_path = Self::base_path(None); - socket_path.push("grim.socket"); - socket_path.to_str().unwrap().to_string() + socket_path.push(Self::SOCKET_NAME); + socket_path } /// Get configuration file path from provided name and sub-directory if needed. From 8ed2308340b55c333cd9e1b472417e7c2e7f2dfe Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 13 Sep 2024 14:53:22 +0300 Subject: [PATCH 09/28] macos: build, warn fix --- macos/build_release.sh | 9 ++++----- src/gui/views/content.rs | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/macos/build_release.sh b/macos/build_release.sh index f533246..d9330d1 100755 --- a/macos/build_release.sh +++ b/macos/build_release.sh @@ -9,14 +9,13 @@ case $2 in exit 1 esac -if [[ ! -v SDKROOT ]]; then +if [[ "$OSTYPE" != "darwin"* ]]; then + if [ -z ${SDKROOT+x} ]; then echo "MacOS SDKROOT is not set" exit 1 -elif [[ -z "SDKROOT" ]]; then - echo "MacOS SDKROOT is set to the empty string" - exit 1 -else + else echo "Use MacOS SDK: ${SDKROOT}" + fi fi # Setup build directory diff --git a/src/gui/views/content.rs b/src/gui/views/content.rs index bc36690..2590a42 100644 --- a/src/gui/views/content.rs +++ b/src/gui/views/content.rs @@ -241,7 +241,7 @@ impl Content { }); }); columns[1].vertical_centered_justified(|ui| { - View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |ui| { + View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |_| { if !Node::is_running() { self.exit_allowed = true; cb.exit(); From 34376d34900f2d98a0f51187424365c74132455f Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 13 Sep 2024 14:56:04 +0300 Subject: [PATCH 10/28] build: fix macos --- macos/build_release.sh | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/macos/build_release.sh b/macos/build_release.sh index d9330d1..b1e94fd 100755 --- a/macos/build_release.sh +++ b/macos/build_release.sh @@ -34,11 +34,16 @@ rm -rf target/aarch64-apple-darwin [[ $2 == "arm" ]] && arch+=(aarch64-apple-darwin) [[ $2 == "universal" ]] && arch+=(universal2-apple-darwin) -# Start release build with zig linker for cross-compilation -# zig 0.12+ required -cargo install cargo-zigbuild -cargo zigbuild --release --target ${arch} -rm -rf .intentionally-empty-file.o +if [[ "$OSTYPE" != "darwin"* ]]; then + # Start release build with zig linker for cross-compilation + # zig 0.12+ required + cargo install cargo-zigbuild + cargo zigbuild --release --target ${arch} + rm -rf .intentionally-empty-file.o +else + cargo build --release --target ${arch} +fi + mkdir macos/Grim.app/Contents/MacOS yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS From bcf821c06a388695099ecc752946c7a46c816039 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 13 Sep 2024 15:21:43 +0300 Subject: [PATCH 11/28] macos: initial file type association --- macos/Grim.app/Contents/Info.plist | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/macos/Grim.app/Contents/Info.plist b/macos/Grim.app/Contents/Info.plist index c56394a..669d7aa 100644 --- a/macos/Grim.app/Contents/Info.plist +++ b/macos/Grim.app/Contents/Info.plist @@ -40,6 +40,34 @@ CFBundleVersion 1 + CFBundleDocumentTypes + + + CFBundleTypeName + Apple SimpleText document + CFBundleTypeRole + Viewer + LSItemContentTypes + + com.apple.traditional-mac-plain-text + + NSDocumentClass + Document + + + CFBundleTypeName + Unknown document + CFBundleTypeRole + Viewer + LSItemContentTypes + + public.data + + NSDocumentClass + Document + + + LSApplicationCategoryType public.app-category.finance From 17545c1b7c4cdb125fc49b12a5d71433babce137 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 13 Sep 2024 18:57:09 +0300 Subject: [PATCH 12/28] macos: platform build --- .gitignore | 1 - macos/Grim.app/Contents/MacOS/.gitignore | 1 + macos/build_release.sh | 19 +++++++++++-------- 3 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 macos/Grim.app/Contents/MacOS/.gitignore diff --git a/.gitignore b/.gitignore index af26ef6..ea10035 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ android/keystore.properties target .cargo/ app/src/main/jniLibs -macos/Grim.app/Contents/MacOS/grim macos/cert.pem linux/Grim.AppDir/AppRun .intentionally-empty-file.o \ No newline at end of file diff --git a/macos/Grim.app/Contents/MacOS/.gitignore b/macos/Grim.app/Contents/MacOS/.gitignore new file mode 100644 index 0000000..b722e9e --- /dev/null +++ b/macos/Grim.app/Contents/MacOS/.gitignore @@ -0,0 +1 @@ +!.gitignore \ No newline at end of file diff --git a/macos/build_release.sh b/macos/build_release.sh index b1e94fd..da4722e 100755 --- a/macos/build_release.sh +++ b/macos/build_release.sh @@ -27,16 +27,20 @@ cd .. rustup target add x86_64-apple-darwin rustup target add aarch64-apple-darwin -rm -rf target/x86_64-apple-darwin -rm -rf target/aarch64-apple-darwin - [[ $2 == "x86_64" ]] && arch+=(x86_64-apple-darwin) [[ $2 == "arm" ]] && arch+=(aarch64-apple-darwin) -[[ $2 == "universal" ]] && arch+=(universal2-apple-darwin) if [[ "$OSTYPE" != "darwin"* ]]; then - # Start release build with zig linker for cross-compilation - # zig 0.12+ required + rustup target add x86_64h-apple-darwin + [[ $2 == "universal" ]] && arch+=(x86_64h-apple-darwin) +else + [[ $2 == "universal" ]] && arch+=(universal2-apple-darwin) + rm -rf target/x86_64-apple-darwin + rm -rf target/aarch64-apple-darwin +fi + +# Start release build with zig linker on non-MacOS systems +if [[ "$OSTYPE" != "darwin"* ]]; then cargo install cargo-zigbuild cargo zigbuild --release --target ${arch} rm -rf .intentionally-empty-file.o @@ -44,10 +48,9 @@ else cargo build --release --target ${arch} fi -mkdir macos/Grim.app/Contents/MacOS yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS -### Sign .app resources on change: +# Sign .app resources on change: #rcodesign generate-self-signed-certificate #rcodesign sign --pem-file cert.pem macos/Grim.app From 0205e01b3c448ab66915b375a01d99d23113c679 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 13 Sep 2024 19:51:33 +0300 Subject: [PATCH 13/28] build: macos fix --- macos/build_release.sh | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/macos/build_release.sh b/macos/build_release.sh index da4722e..5f50d35 100755 --- a/macos/build_release.sh +++ b/macos/build_release.sh @@ -29,24 +29,12 @@ rustup target add aarch64-apple-darwin [[ $2 == "x86_64" ]] && arch+=(x86_64-apple-darwin) [[ $2 == "arm" ]] && arch+=(aarch64-apple-darwin) +[[ $2 == "universal" ]]; arch+=(universal2-apple-darwin) -if [[ "$OSTYPE" != "darwin"* ]]; then - rustup target add x86_64h-apple-darwin - [[ $2 == "universal" ]] && arch+=(x86_64h-apple-darwin) -else - [[ $2 == "universal" ]] && arch+=(universal2-apple-darwin) - rm -rf target/x86_64-apple-darwin - rm -rf target/aarch64-apple-darwin -fi - -# Start release build with zig linker on non-MacOS systems -if [[ "$OSTYPE" != "darwin"* ]]; then - cargo install cargo-zigbuild - cargo zigbuild --release --target ${arch} - rm -rf .intentionally-empty-file.o -else - cargo build --release --target ${arch} -fi +# Start release build with zig linker, requires zig 0.12.1 +cargo install cargo-zigbuild +cargo zigbuild --release --target ${arch} +rm -rf .intentionally-empty-file.o yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS From 8594279b9803fb1d99b3c18996c759f7f4f649c6 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 13 Sep 2024 21:08:14 +0300 Subject: [PATCH 14/28] android: java call result fixes --- src/gui/platform/android/mod.rs | 40 +++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/gui/platform/android/mod.rs b/src/gui/platform/android/mod.rs index b93739c..01565b9 100644 --- a/src/gui/platform/android/mod.rs +++ b/src/gui/platform/android/mod.rs @@ -67,30 +67,30 @@ impl PlatformCallbacks for Android { } fn exit(&self) { - self.call_java_method("exit", "()V", &[]).unwrap(); + let _ = self.call_java_method("exit", "()V", &[]); } fn show_keyboard(&self) { // Disable NDK soft input show call before fix for egui. // self.android_app.show_soft_input(false); - self.call_java_method("showKeyboard", "()V", &[]).unwrap(); + let _ = self.call_java_method("showKeyboard", "()V", &[]); } fn hide_keyboard(&self) { // Disable NDK soft input hide call before fix for egui. // self.android_app.hide_soft_input(false); - self.call_java_method("hideKeyboard", "()V", &[]).unwrap(); + let _ = self.call_java_method("hideKeyboard", "()V", &[]); } fn copy_string_to_buffer(&self, data: String) { let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); let env = vm.attach_current_thread().unwrap(); let arg_value = env.new_string(data).unwrap(); - self.call_java_method("copyText", - "(Ljava/lang/String;)V", - &[JValue::Object(&JObject::from(arg_value))]).unwrap(); + let _ = self.call_java_method("copyText", + "(Ljava/lang/String;)V", + &[JValue::Object(&JObject::from(arg_value))]); } fn get_string_from_buffer(&self) -> String { @@ -109,12 +109,12 @@ impl PlatformCallbacks for Android { let mut w_image = LAST_CAMERA_IMAGE.write(); *w_image = None; // Start camera. - self.call_java_method("startCamera", "()V", &[]).unwrap(); + let _ = self.call_java_method("startCamera", "()V", &[]); } fn stop_camera(&self) { // Stop camera. - self.call_java_method("stopCamera", "()V", &[]).unwrap(); + let _ = self.call_java_method("stopCamera", "()V", &[]); // Clear image. let mut w_image = LAST_CAMERA_IMAGE.write(); *w_image = None; @@ -129,19 +129,25 @@ impl PlatformCallbacks for Android { } fn can_switch_camera(&self) -> bool { - let result = self.call_java_method("camerasAmount", "()I", &[]).unwrap(); - let amount = unsafe { result.i }; - amount > 1 + if let Some(res) = self.call_java_method("camerasAmount", "()I", &[]) { + let amount = unsafe { res.i }; + return amount > 1; + } + false } fn switch_camera(&self) { - self.call_java_method("switchCamera", "()V", &[]).unwrap(); + let _ = self.call_java_method("switchCamera", "()V", &[]); } fn share_data(&self, name: String, data: Vec) -> Result<(), std::io::Error> { - // Create file at cache dir. let default_cache = OsString::from(dirs::cache_dir().unwrap()); let mut file = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache)); + // File path for Android provider. + file.push("images"); + if !file.exists() { + std::fs::create_dir(file.clone())?; + } file.push(name); if file.exists() { std::fs::remove_file(file.clone())?; @@ -153,9 +159,9 @@ impl PlatformCallbacks for Android { let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); let env = vm.attach_current_thread().unwrap(); let arg_value = env.new_string(file.to_str().unwrap()).unwrap(); - self.call_java_method("shareImage", - "(Ljava/lang/String;)V", - &[JValue::Object(&JObject::from(arg_value))]).unwrap(); + let _ = self.call_java_method("shareImage", + "(Ljava/lang/String;)V", + &[JValue::Object(&JObject::from(arg_value))]); Ok(()) } @@ -164,7 +170,7 @@ impl PlatformCallbacks for Android { let mut w_path = PICKED_FILE_PATH.write(); *w_path = None; // Launch file picker. - let _ = self.call_java_method("pickFile", "()V", &[]).unwrap(); + let _ = self.call_java_method("pickFile", "()V", &[]); // Return empty string to identify async pick. Some("".to_string()) } From 50638ff54e438d1a375fcab10f1c03b55d2c7232 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 13 Sep 2024 22:00:59 +0300 Subject: [PATCH 15/28] github: android keystore --- .github/workflows/build.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f199f0b..7262a2e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,8 +23,8 @@ jobs: run: | chmod +x android/gradlew echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc - gpg -d --passphrase "${{ secrets.ANDROID_RELEASE_SECRET }}" --batch release.keystore.asc > android/keystore - echo -e "storePassword=${{ secrets.ANDROID_PASS }}\nkeyPassword=${{ secrets.ANDROID_PASS }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties + base64 -d release.keystore.asc > android/keystore + echo -e "storePassword=${{ secrets.ANDROID_RELEASE_SECRET }}\nkeyPassword=${{ secrets.ANDROID_RELEASE_SECRET }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties - name: Build lib 1/2 continue-on-error: true run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55daabd..910a6c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,8 +30,8 @@ jobs: run: | chmod +x android/gradlew echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc - gpg -d --passphrase "${{ secrets.ANDROID_RELEASE_SECRET }}" --batch release.keystore.asc > android/keystore - echo -e "storePassword=${{ secrets.ANDROID_PASS }}\nkeyPassword=${{ secrets.ANDROID_PASS }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties + base64 -d release.keystore.asc > android/keystore + echo -e "storePassword=${{ secrets.ANDROID_RELEASE_SECRET }}\nkeyPassword=${{ secrets.ANDROID_RELEASE_SECRET }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties - name: Build lib ARMv8 1/2 continue-on-error: true run: | From 6f7156ef173ab7806603ac2ed8a7f46f17862766 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 13 Sep 2024 22:31:28 +0300 Subject: [PATCH 16/28] github: android secrets --- .github/workflows/build.yml | 6 +++--- .github/workflows/release.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7262a2e..64ae3cd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,9 +22,9 @@ jobs: - name: Setup Java build run: | chmod +x android/gradlew - echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc - base64 -d release.keystore.asc > android/keystore - echo -e "storePassword=${{ secrets.ANDROID_RELEASE_SECRET }}\nkeyPassword=${{ secrets.ANDROID_RELEASE_SECRET }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties + echo "${{ secrets.ANDROID_RELEASE }}" > release.keystore.txt + base64 -d release.keystore.txt > android/keystore + echo -e "storePassword=${{ secrets.ANDROID_RELEASE_STORE }}\nkeyPassword=${{ secrets.ANDROID_RELEASE_KEY }}\nkeyAlias=${{ secrets.ANDROID_RELEASE_ALIAS }}\nstoreFile=../keystore" > android/keystore.properties - name: Build lib 1/2 continue-on-error: true run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 910a6c9..e0bc60f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,9 +29,9 @@ jobs: - name: Setup Java build run: | chmod +x android/gradlew - echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc - base64 -d release.keystore.asc > android/keystore - echo -e "storePassword=${{ secrets.ANDROID_RELEASE_SECRET }}\nkeyPassword=${{ secrets.ANDROID_RELEASE_SECRET }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties + echo "${{ secrets.ANDROID_RELEASE }}" > release.keystore.txt + base64 -d release.keystore.txt > android/keystore + echo -e "storePassword=${{ secrets.ANDROID_RELEASE_STORE }}\nkeyPassword=${{ secrets.ANDROID_RELEASE_KEY }}\nkeyAlias=${{ secrets.ANDROID_RELEASE_ALIAS }}\nstoreFile=../keystore" > android/keystore.properties - name: Build lib ARMv8 1/2 continue-on-error: true run: | From 68c9c9df04b8e70e99de07d60e673b5a7f85db6c Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 14 Sep 2024 01:47:06 +0300 Subject: [PATCH 17/28] build: local android release --- .github/workflows/build.yml | 37 ----------- .github/workflows/release.yml | 83 ------------------------ scripts/android.sh | 117 +++++++++++++++++++--------------- 3 files changed, 67 insertions(+), 170 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64ae3cd..c47b10f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,43 +2,6 @@ name: Build on: [push, pull_request] jobs: - android: - name: Android Build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Setup JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: gradle - - name: Setup build - run: | - cargo install cargo-ndk - rustup target add aarch64-linux-android - rustup target add armv7-linux-androideabi - rustup target add x86_64-linux-android - - name: Setup Java build - run: | - chmod +x android/gradlew - echo "${{ secrets.ANDROID_RELEASE }}" > release.keystore.txt - base64 -d release.keystore.txt > android/keystore - echo -e "storePassword=${{ secrets.ANDROID_RELEASE_STORE }}\nkeyPassword=${{ secrets.ANDROID_RELEASE_KEY }}\nkeyAlias=${{ secrets.ANDROID_RELEASE_ALIAS }}\nstoreFile=../keystore" > android/keystore.properties - - name: Build lib 1/2 - continue-on-error: true - run: | - sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml - export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t arm64-v8a build --profile release-apk - - name: Build lib 2/2 - run: | - unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk - sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml - - name: Build APK - working-directory: android - run: | - ./gradlew assembleRelease - linux: name: Linux Build runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e0bc60f..655b3f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,89 +6,6 @@ on: - "v*.*.*" jobs: - android_release: - name: Android Release - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Setup JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: gradle - - name: Setup Rust build - run: | - cargo install cargo-ndk - rustup target add aarch64-linux-android - rustup target add armv7-linux-androideabi - rustup target add x86_64-linux-android - - name: Setup Java build - run: | - chmod +x android/gradlew - echo "${{ secrets.ANDROID_RELEASE }}" > release.keystore.txt - base64 -d release.keystore.txt > android/keystore - echo -e "storePassword=${{ secrets.ANDROID_RELEASE_STORE }}\nkeyPassword=${{ secrets.ANDROID_RELEASE_KEY }}\nkeyAlias=${{ secrets.ANDROID_RELEASE_ALIAS }}\nstoreFile=../keystore" > android/keystore.properties - - name: Build lib ARMv8 1/2 - continue-on-error: true - run: | - sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml - export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t arm64-v8a build --profile release-apk - - name: Build lib ARMv8 2/2 - run: | - unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk - sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml - - name: Build lib ARMv7 1/2 - continue-on-error: true - run: | - sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml - export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t armeabi-v7a build --profile release-apk - - name: Build lib ARMv7 2/2 - run: | - unset CPPFLAGS && unset CFLAGS && cargo ndk -t armeabi-v7a -o android/app/src/main/jniLibs build --profile release-apk - sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml - - name: Build APK ARM - working-directory: android - run: | - rm -rf app/build - ./gradlew assembleRelease - mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android.apk - rm -rf app/src/main/jniLibs/* - - name: Checksum APK ARM - working-directory: android - shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-sha256sum.txt - - name: Build lib x86 1/2 - continue-on-error: true - run: | - sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml - export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t x86_64 build --profile release-apk - - name: Build lib x86 2/2 - run: | - unset CPPFLAGS && unset CFLAGS && cargo ndk -t x86_64 -o android/app/src/main/jniLibs build --profile release-apk - sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml - - name: Build APK x86 - working-directory: android - run: | - rm -rf app/build - ./gradlew assembleRelease - mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android-x86_64.apk - - name: Checksum APK x86 - working-directory: android - shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android-x86_64.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-x86_64-sha256sum.txt - - name: Release - uses: softprops/action-gh-release@v1 - with: - files: | - android/grim-${{ github.ref_name }}-android.apk - android/grim-${{ github.ref_name }}-android-sha256sum.txt - android/grim-${{ github.ref_name }}-android-x86_64.apk - android/grim-${{ github.ref_name }}-android-x86_64-sha256sum.txt - linux_release: name: Linux Release runs-on: ubuntu-latest diff --git a/scripts/android.sh b/scripts/android.sh index 6366791..519ce6c 100755 --- a/scripts/android.sh +++ b/scripts/android.sh @@ -1,81 +1,98 @@ #!/bin/bash -usage="Usage: build_run_android.sh [type] [platform]\n - type: 'debug', 'release'\n - platform: 'v7', 'v8'" +usage="Usage: android.sh [type] [platform]\n - type: 'build', 'release', ''\n - platform, for build type: 'v7', 'v8', 'x86'" case $1 in - debug|release) + build|release) ;; *) printf "$usage" exit 1 esac -case $2 in - v7|v8) - ;; - *) - printf "$usage" - exit 1 -esac +if [[ $1 == "build" ]]; then + case $2 in + v7|v8|x86) + ;; + *) + printf "$usage" + exit 1 + esac +fi # Setup build directory BASEDIR=$(cd $(dirname $0) && pwd) cd ${BASEDIR} cd .. -# Setup release argument -type=$1 -[[ ${type} == "release" ]] && release_param="--profile release-apk" - -# Setup platform argument -[[ $2 == "v7" ]] && arch+=(armeabi-v7a) -[[ $2 == "v8" ]] && arch+=(arm64-v8a) - -# Setup platform path -[[ $2 == "v7" ]] && platform+=(armv7-linux-androideabi) -[[ $2 == "v8" ]] && platform+=(aarch64-linux-android) - -# Install platform -[[ $2 == "v7" ]] && rustup target install armv7-linux-androideabi -[[ $2 == "v8" ]] && rustup target install aarch64-linux-android - -# Build native code +# Install platforms and tools +rustup target add armv7-linux-androideabi +rustup target add aarch64-linux-android +rustup target add x86_64-linux-android cargo install cargo-ndk -sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml - -# temp fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s success=0 -export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" -cargo ndk -t ${arch} build ${release_param} -unset CPPFLAGS && unset CFLAGS -cargo ndk -t ${arch} -o android/app/src/main/jniLibs build ${release_param} -if [ $? -eq 0 ] -then - success=1 -fi -sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml +### Build native code +function build_lib() { + [[ $1 == "v7" ]] && arch=(armeabi-v7a) + [[ $1 == "v8" ]] && arch=(arm64-v8a) + [[ $1 == "x86" ]] && arch=(x86_64) + + sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml + + # Fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s + export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" + cargo ndk -t ${arch} build --profile release-apk + unset CPPFLAGS && unset CFLAGS + cargo ndk -t ${arch} -o android/app/src/main/jniLibs build --profile release-apk + if [ $? -eq 0 ] + then + success=1 + fi + + sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml +} + +### Build application +function build_apk() { + version=$(grep -m 1 -Po 'version = "\K[^"]*' Cargo.toml) -# Build Android application and launch at all connected devices -if [ $success -eq 1 ] -then cd android - - # Setup gradle argument - [[ $1 == "release" ]] && gradle_param+=(assembleRelease) - [[ $1 == "debug" ]] && gradle_param+=(build) - ./gradlew clean - ./gradlew ${gradle_param} + ./gradlew assembleRelease - # Setup apk path - [[ $1 == "release" ]] && apk_path+=(app/build/outputs/apk/release/app-release.apk) - [[ $1 == "debug" ]] && apk_path+=(app/build/outputs/apk/debug/app-debug.apk) + # Setup release file name + if [ -n $1 ]; then + rm -rf grim-${version}-$1.apk + mv app/build/outputs/apk/release/app-release.apk grim-${version}-$1.apk + fi + cd .. +} + +# Remove build targets +rm -rf target/release-apk +rm -rf target/aarch64-linux-android +rm -rf target/x86_64-linux-android +rm -rf target/armv7-linux-androideabi +rm -rf android/app/src/main/jniLibs/* + +if [[ $1 == "build" ]]; then + build_lib $2 + [ $success -eq 1 ] && build_apk + + # Launch application at all connected devices. for SERIAL in $(adb devices | grep -v List | cut -f 1); do adb -s $SERIAL install ${apk_path} sleep 1s adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity; done +else + build_lib "v7" + [ $success -eq 1 ] && build_lib "v8" + [ $success -eq 1 ] && build_apk "arm" + rm -rf android/app/src/main/jniLibs/* + [ $success -eq 1 ] && build_lib "x86" + [ $success -eq 1 ] && build_apk "x86_64" fi \ No newline at end of file From d39e2ec21e93d8289ee8ba9c5b3bd75b3bd32ab4 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 14 Sep 2024 02:06:35 +0300 Subject: [PATCH 18/28] build: android signed release --- android/app/build.gradle | 30 +++++++++++++++++++----------- scripts/android.sh | 4 ++-- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 98a54b8..71d2129 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -2,10 +2,6 @@ plugins { id 'com.android.application' } -def keystorePropertiesFile = rootProject.file("keystore.properties") -def keystoreProperties = new Properties() -keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) - android { compileSdk 33 ndkVersion '26.0.10792818' @@ -18,20 +14,32 @@ android { versionName "0.1.3" } - signingConfigs { - release { - keyAlias keystoreProperties['keyAlias'] - keyPassword keystoreProperties['keyPassword'] - storeFile file(keystoreProperties['storeFile']) - storePassword keystoreProperties['storePassword'] + def keystorePropertiesFile = rootProject.file("keystore.properties") + def keystoreProperties = new Properties() + if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile file(keystoreProperties['storeFile']) + storePassword keystoreProperties['storePassword'] + } } + } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - signingConfig signingConfigs.release + } + if (keystorePropertiesFile.exists()) { + signedRelease { + initWith release + signingConfig signingConfigs.release + } } debug { minifyEnabled false diff --git a/scripts/android.sh b/scripts/android.sh index 519ce6c..d377e70 100755 --- a/scripts/android.sh +++ b/scripts/android.sh @@ -59,12 +59,12 @@ function build_apk() { cd android ./gradlew clean - ./gradlew assembleRelease + ./gradlew assembleSignedRelease # Setup release file name if [ -n $1 ]; then rm -rf grim-${version}-$1.apk - mv app/build/outputs/apk/release/app-release.apk grim-${version}-$1.apk + mv app/build/outputs/apk/signedRelease/app-signedRelease.apk grim-${version}-$1.apk fi cd .. From 9ea0da95b71daf18edba10676694f28c090af24c Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 14 Sep 2024 12:12:50 +0300 Subject: [PATCH 19/28] build: release sha256sum --- .github/workflows/release.yml | 14 +++++++------- scripts/android.sh | 30 ++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 655b3f5..cb40fb2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,7 @@ jobs: - name: Checksum AppImage x86 working-directory: target/x86_64-unknown-linux-gnu/release shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-x86_64.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt + run: sha256sum grim-${{ github.ref_name }}-linux-x86_64.AppImage > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt - name: AppImage ARM run: | cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun @@ -46,7 +46,7 @@ jobs: - name: Checksum AppImage ARM working-directory: target/aarch64-unknown-linux-gnu/release shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-arm.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt + run: sha256sum grim-${{ github.ref_name }}-linux-arm.AppImage > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt - name: Release uses: softprops/action-gh-release@v1 with: @@ -74,7 +74,7 @@ jobs: - name: Checksum release working-directory: target/release shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt + run: sha256sum grim-${{ github.ref_name }}-win-x86_64.zip > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt - name: Install cargo-wix run: cargo install cargo-wix - name: Run cargo-wix @@ -82,7 +82,7 @@ jobs: - name: Checksum msi working-directory: target/wix shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.msi | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt + run: sha256sum grim-${{ github.ref_name }}-win-x86_64.msi > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt - name: Release uses: softprops/action-gh-release@v1 with: @@ -121,7 +121,7 @@ jobs: - name: Checksum Release x86 working-directory: target/x86_64-apple-darwin/release shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt + run: sha256sum grim-${{ github.ref_name }}-macos-x86_64.zip > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt - name: Release ARM run: | rustup target add aarch64-apple-darwin @@ -136,7 +136,7 @@ jobs: - name: Checksum Release ARM working-directory: target/aarch64-apple-darwin/release shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-arm.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt + run: sha256sum grim-${{ github.ref_name }}-macos-arm.zip > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt - name: Release Universal run: | rustup target add aarch64-apple-darwin @@ -152,7 +152,7 @@ jobs: - name: Checksum Release Universal working-directory: target/universal2-apple-darwin/release shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-universal.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt + run: sha256sum grim-${{ github.ref_name }}-macos-universal.zip > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt - name: Release uses: softprops/action-gh-release@v1 with: diff --git a/scripts/android.sh b/scripts/android.sh index d377e70..4e940eb 100755 --- a/scripts/android.sh +++ b/scripts/android.sh @@ -33,6 +33,7 @@ cargo install cargo-ndk success=0 ### Build native code +[[ $1 == "release" ]] && release_param="--profile release-apk" function build_lib() { [[ $1 == "v7" ]] && arch=(armeabi-v7a) [[ $1 == "v8" ]] && arch=(arm64-v8a) @@ -42,9 +43,9 @@ function build_lib() { # Fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" - cargo ndk -t ${arch} build --profile release-apk + cargo ndk -t ${arch} build ${release_param} unset CPPFLAGS && unset CFLAGS - cargo ndk -t ${arch} -o android/app/src/main/jniLibs build --profile release-apk + cargo ndk -t ${arch} -o android/app/src/main/jniLibs build ${release_param} if [ $? -eq 0 ] then success=1 @@ -61,20 +62,24 @@ function build_apk() { ./gradlew clean ./gradlew assembleSignedRelease - # Setup release file name if [ -n $1 ]; then - rm -rf grim-${version}-$1.apk - mv app/build/outputs/apk/signedRelease/app-signedRelease.apk grim-${version}-$1.apk + # Setup release file name + name=grim-${version}-android-$1.apk + if [[ $1 == "arm" ]]; then + name=grim-${version}-android.apk + fi + rm -rf ${name} + mv app/build/outputs/apk/signedRelease/app-signedRelease.apk ${name} + + # Calculate checksum + checksum=grim-${version}-$1-sha256sum.txt + rm -rf ${checksum} + sha256sum ${name} > ${checksum} fi cd .. } -# Remove build targets -rm -rf target/release-apk -rm -rf target/aarch64-linux-android -rm -rf target/x86_64-linux-android -rm -rf target/armv7-linux-androideabi rm -rf android/app/src/main/jniLibs/* if [[ $1 == "build" ]]; then @@ -89,6 +94,11 @@ if [[ $1 == "build" ]]; then adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity; done else + rm -rf target/release-apk + rm -rf target/aarch64-linux-android + rm -rf target/x86_64-linux-android + rm -rf target/armv7-linux-androideabi + build_lib "v7" [ $success -eq 1 ] && build_lib "v8" [ $success -eq 1 ] && build_apk "arm" From 8af06d886098eb0323cf2594986048464e069ca0 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 14 Sep 2024 13:07:48 +0300 Subject: [PATCH 20/28] build: android fix --- scripts/android.sh | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/scripts/android.sh b/scripts/android.sh index 4e940eb..caed848 100755 --- a/scripts/android.sh +++ b/scripts/android.sh @@ -30,7 +30,7 @@ rustup target add aarch64-linux-android rustup target add x86_64-linux-android cargo install cargo-ndk -success=0 +success=1 ### Build native code [[ $1 == "release" ]] && release_param="--profile release-apk" @@ -60,19 +60,33 @@ function build_apk() { cd android ./gradlew clean - ./gradlew assembleSignedRelease + # Build signed apk if keystore exists + if [ ! -f keystore.properties ]; then + ./gradlew assembleRelease + apk_path=app/build/outputs/apk/release/app-release.apk + else + ./gradlew assembleSignedRelease + apk_path=app/build/outputs/apk/signedRelease/app-signedRelease.apk + fi - if [ -n $1 ]; then + if [[ $1 == "" ]]; then + # Launch application at all connected devices. + for SERIAL in $(adb devices | grep -v List | cut -f 1); + do + adb -s $SERIAL install ${apk_path} + sleep 1s + adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity; + done + else # Setup release file name name=grim-${version}-android-$1.apk - if [[ $1 == "arm" ]]; then - name=grim-${version}-android.apk - fi + [[ $1 == "arm" ]] && name=grim-${version}-android.apk rm -rf ${name} - mv app/build/outputs/apk/signedRelease/app-signedRelease.apk ${name} + mv ${apk_path} ${name} # Calculate checksum - checksum=grim-${version}-$1-sha256sum.txt + checksum=grim-${version}-android-$1-sha256sum.txt + [[ $1 == "arm" ]] && checksum=grim-${version}-android-sha256sum.txt rm -rf ${checksum} sha256sum ${name} > ${checksum} fi @@ -85,14 +99,6 @@ rm -rf android/app/src/main/jniLibs/* if [[ $1 == "build" ]]; then build_lib $2 [ $success -eq 1 ] && build_apk - - # Launch application at all connected devices. - for SERIAL in $(adb devices | grep -v List | cut -f 1); - do - adb -s $SERIAL install ${apk_path} - sleep 1s - adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity; - done else rm -rf target/release-apk rm -rf target/aarch64-linux-android From 1934dc33772fdaa4a1bd552e1f92ed390f29f10d Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 14 Sep 2024 15:04:11 +0300 Subject: [PATCH 21/28] desktop: args text --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 9bb0185..a51b407 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,7 @@ fn real_main() { let path = std::path::PathBuf::from(&args[1]); let content = match std::fs::read_to_string(path) { Ok(s) => Some(s), - Err(_) => None + Err(_) => Some(args[1].clone()) }; data = content } From 1431e307ee5d0489d34e4d83ac7277d11bfafcb6 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 14 Sep 2024 15:21:08 +0300 Subject: [PATCH 22/28] ui: separate wallet accounts modal --- src/gui/views/wallets/wallet/accounts.rs | 240 +++++++++++++++++++++++ src/gui/views/wallets/wallet/content.rs | 213 ++------------------ src/gui/views/wallets/wallet/mod.rs | 4 +- 3 files changed, 256 insertions(+), 201 deletions(-) create mode 100644 src/gui/views/wallets/wallet/accounts.rs diff --git a/src/gui/views/wallets/wallet/accounts.rs b/src/gui/views/wallets/wallet/accounts.rs new file mode 100644 index 0000000..6b50168 --- /dev/null +++ b/src/gui/views/wallets/wallet/accounts.rs @@ -0,0 +1,240 @@ +// Copyright 2024 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use egui::{Align, Id, Layout, RichText, ScrollArea}; +use egui::scroll_area::ScrollBarVisibility; +use grin_core::core::amount_to_hr_string; + +use crate::gui::Colors; +use crate::gui::icons::{CHECK, CHECK_FAT, FOLDER_USER, PATH}; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{Modal, View}; +use crate::gui::views::types::TextEditOptions; +use crate::gui::views::wallets::wallet::types::GRIN; +use crate::wallet::types::WalletAccount; +use crate::wallet::{Wallet, WalletConfig}; + +/// Wallet accounts content. +pub struct WalletAccounts { + /// List of wallet accounts. + accounts: Vec, + /// Flag to check if account is creating. + account_creating: bool, + /// Account label value. + account_label_edit: String, + /// Flag to check if error occurred during account creation. + account_creation_error: bool, +} + +impl Default for WalletAccounts { + fn default() -> Self { + Self { + accounts: vec![], + account_creating: false, + account_label_edit: "".to_string(), + account_creation_error: false, + } + } +} + +impl WalletAccounts { + /// Create new instance from wallet accounts. + pub fn new(accounts: Vec) -> Self { + Self { + accounts, + account_creating: false, + account_label_edit: "".to_string(), + account_creation_error: false, + } + } + + /// Draw [`Modal`] content. + pub fn ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + if self.account_creating { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("wallets.new_account_desc")) + .size(17.0) + .color(Colors::gray())); + ui.add_space(8.0); + + // Draw account name edit. + let text_edit_id = Id::from(modal.id).with(wallet.get_config().id); + let mut text_edit_opts = TextEditOptions::new(text_edit_id); + View::text_edit(ui, cb, &mut self.account_label_edit, &mut text_edit_opts); + + // Show error occurred during account creation.. + if self.account_creation_error { + ui.add_space(12.0); + ui.label(RichText::new(t!("error")) + .size(17.0) + .color(Colors::red())); + } + ui.add_space(12.0); + }); + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + // Show modal buttons. + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + // Close modal. + cb.hide_keyboard(); + modal.close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + // Create button callback. + let mut on_create = || { + if !self.account_label_edit.is_empty() { + let label = &self.account_label_edit; + match wallet.create_account(label) { + Ok(_) => { + let _ = wallet.set_active_account(label); + cb.hide_keyboard(); + modal.close(); + }, + Err(_) => self.account_creation_error = true + }; + } + }; + + View::on_enter_key(ui, || { + (on_create)(); + }); + + View::button(ui, t!("create"), Colors::white_or_black(false), on_create); + }); + }); + ui.add_space(6.0); + } else { + ui.add_space(3.0); + + // Show list of accounts. + let size = self.accounts.len(); + ScrollArea::vertical() + .id_source("account_list_modal_scroll") + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(266.0) + .auto_shrink([true; 2]) + .show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| { + for index in row_range { + // Add space before the first item. + if index == 0 { + ui.add_space(4.0); + } + let acc = self.accounts.get(index).unwrap(); + account_item_ui(ui, modal, wallet, acc, index, size); + if index == size - 1 { + ui.add_space(4.0); + } + } + }); + + ui.add_space(2.0); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(6.0); + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + // Show modal buttons. + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + modal.close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("create"), Colors::white_or_black(false), || { + self.account_creating = true; + cb.show_keyboard(); + }); + }); + }); + ui.add_space(6.0); + } + } + +} + +const ACCOUNT_ITEM_HEIGHT: f32 = 75.0; + +/// Draw account item. +fn account_item_ui(ui: &mut egui::Ui, + modal: &Modal, + wallet: &mut Wallet, + acc: &WalletAccount, + index: usize, + size: usize) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(ACCOUNT_ITEM_HEIGHT); + + // Draw round background. + let bg_rect = rect.clone(); + let item_rounding = View::item_rounding(index, size, false); + ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke()); + + ui.vertical(|ui| { + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + // Draw button to select account. + let is_current_account = wallet.get_config().account == acc.label; + if !is_current_account { + let button_rounding = View::item_rounding(index, size, true); + View::item_button(ui, button_rounding, CHECK, None, || { + let _ = wallet.set_active_account(&acc.label); + modal.close(); + }); + } else { + ui.add_space(12.0); + ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green())); + } + + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(4.0); + // Show spendable amount. + let amount = amount_to_hr_string(acc.spendable_amount, true); + let amount_text = format!("{} {}", amount, GRIN); + ui.label(RichText::new(amount_text).size(18.0).color(Colors::white_or_black(true))); + ui.add_space(-2.0); + + // Show account name. + let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string(); + let acc_label = if acc.label == default_acc_label { + t!("wallets.default_account") + } else { + acc.label.to_owned() + }; + let acc_name = format!("{} {}", FOLDER_USER, acc_label); + View::ellipsize_text(ui, acc_name, 15.0, Colors::text(false)); + + // Show account BIP32 derivation path. + let acc_path = format!("{} {}", PATH, acc.path); + ui.label(RichText::new(acc_path).size(15.0).color(Colors::gray())); + ui.add_space(3.0); + }); + }); + }); + }); +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 8888931..b7aad65 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -20,27 +20,22 @@ use grin_core::core::amount_to_hr_string; use crate::AppConfig; use crate::gui::Colors; -use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CHAT_CIRCLE_TEXT, CHECK, CHECK_FAT, COPY, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, PATH, POWER, SCAN, SPINNER, USERS_THREE}; +use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CHAT_CIRCLE_TEXT, COPY, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, POWER, SCAN, SPINNER, USERS_THREE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{CameraContent, Modal, Content, View}; -use crate::gui::views::types::{ModalPosition, QrScanResult, TextEditOptions}; +use crate::gui::views::types::{ModalPosition, QrScanResult}; use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport}; use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType}; +use crate::gui::views::wallets::wallet::accounts::WalletAccounts; use crate::gui::views::wallets::wallet::WalletSettings; use crate::node::Node; use crate::wallet::{Wallet, WalletConfig}; -use crate::wallet::types::{WalletAccount, WalletData}; +use crate::wallet::types::WalletData; /// Selected and opened wallet content. pub struct WalletContent { - /// List of wallet accounts for [`Modal`]. - accounts: Vec, - /// Flag to check if account is creating. - account_creating: bool, - /// Account label [`Modal`] value. - account_label_edit: String, - /// Flag to check if error occurred during account creation at [`Modal`]. - account_creation_error: bool, + /// Wallet accounts [`Modal`] content. + accounts_modal_content: Option, /// Camera content for QR scan [`Modal`]. camera_content: CameraContent, @@ -61,10 +56,7 @@ impl WalletContent { /// Create new instance with optional data. pub fn new(data: Option) -> Self { let mut content = Self { - accounts: vec![], - account_creating: false, - account_label_edit: "".to_string(), - account_creation_error: false, + accounts_modal_content: None, camera_content: CameraContent::default(), qr_scan_result: None, current_tab: Box::new(WalletTransactions::default()), @@ -195,9 +187,11 @@ impl WalletContent { Some(id) => { match id { ACCOUNT_LIST_MODAL => { - Modal::ui(ui.ctx(), |ui, modal| { - self.account_list_modal_ui(ui, wallet, modal, cb); - }); + if let Some(content) = self.accounts_modal_content.as_mut() { + Modal::ui(ui.ctx(), |ui, modal| { + content.ui(ui, wallet, modal, cb); + }); + } } QR_CODE_SCAN_MODAL => { Modal::ui(ui.ctx(), |ui, modal| { @@ -238,10 +232,7 @@ impl WalletContent { // Draw button to show list of accounts. View::item_button(ui, View::item_rounding(1, 3, true), USERS_THREE, None, || { - // Load accounts. - self.account_label_edit = "".to_string(); - self.accounts = wallet.accounts(); - self.account_creating = false; + self.accounts_modal_content = Some(WalletAccounts::new(wallet.accounts())); // Show account list modal. Modal::new(ACCOUNT_LIST_MODAL) .position(ModalPosition::CenterTop) @@ -308,120 +299,6 @@ impl WalletContent { }); } - /// Draw account list [`Modal`] content. - fn account_list_modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - if self.account_creating { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("wallets.new_account_desc")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); - - // Draw account name edit. - let text_edit_id = Id::from(modal.id).with(wallet.get_config().id); - let mut text_edit_opts = TextEditOptions::new(text_edit_id); - View::text_edit(ui, cb, &mut self.account_label_edit, &mut text_edit_opts); - - // Show error occurred during account creation.. - if self.account_creation_error { - ui.add_space(12.0); - ui.label(RichText::new(t!("error")) - .size(17.0) - .color(Colors::red())); - } - ui.add_space(12.0); - }); - - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - // Show modal buttons. - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - cb.hide_keyboard(); - modal.close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Create button callback. - let mut on_create = || { - if !self.account_label_edit.is_empty() { - let label = &self.account_label_edit; - match wallet.create_account(label) { - Ok(_) => { - let _ = wallet.set_active_account(label); - cb.hide_keyboard(); - modal.close(); - }, - Err(_) => self.account_creation_error = true - }; - } - }; - - View::on_enter_key(ui, || { - (on_create)(); - }); - - View::button(ui, t!("create"), Colors::white_or_black(false), on_create); - }); - }); - ui.add_space(6.0); - } else { - ui.add_space(3.0); - - // Show list of accounts. - let size = self.accounts.len(); - ScrollArea::vertical() - .id_source("account_list_modal_scroll") - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .max_height(266.0) - .auto_shrink([true; 2]) - .show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| { - for index in row_range { - // Add space before the first item. - if index == 0 { - ui.add_space(4.0); - } - let acc = self.accounts.get(index).unwrap(); - account_item_ui(ui, modal, wallet, acc, index, size); - if index == size - 1 { - ui.add_space(4.0); - } - } - }); - - ui.add_space(2.0); - View::horizontal_line(ui, Colors::stroke()); - ui.add_space(6.0); - - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - // Show modal buttons. - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - modal.close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("create"), Colors::white_or_black(false), || { - self.account_creating = true; - cb.show_keyboard(); - }); - }); - }); - ui.add_space(6.0); - } - } - /// Draw QR code scan [`Modal`] content. fn scan_qr_modal_ui(&mut self, ui: &mut egui::Ui, @@ -676,68 +553,4 @@ impl WalletContent { }); }); } -} - -const ACCOUNT_ITEM_HEIGHT: f32 = 75.0; - -/// Draw account item. -fn account_item_ui(ui: &mut egui::Ui, - modal: &Modal, - wallet: &mut Wallet, - acc: &WalletAccount, - index: usize, - size: usize) { - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(ACCOUNT_ITEM_HEIGHT); - - // Draw round background. - let bg_rect = rect.clone(); - let item_rounding = View::item_rounding(index, size, false); - ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke()); - - ui.vertical(|ui| { - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - // Draw button to select account. - let is_current_account = wallet.get_config().account == acc.label; - if !is_current_account { - let button_rounding = View::item_rounding(index, size, true); - View::item_button(ui, button_rounding, CHECK, None, || { - let _ = wallet.set_active_account(&acc.label); - modal.close(); - }); - } else { - ui.add_space(12.0); - ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green())); - } - - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - ui.add_space(4.0); - // Show spendable amount. - let amount = amount_to_hr_string(acc.spendable_amount, true); - let amount_text = format!("{} {}", amount, GRIN); - ui.label(RichText::new(amount_text).size(18.0).color(Colors::white_or_black(true))); - ui.add_space(-2.0); - - // Show account name. - let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string(); - let acc_label = if acc.label == default_acc_label { - t!("wallets.default_account") - } else { - acc.label.to_owned() - }; - let acc_name = format!("{} {}", FOLDER_USER, acc_label); - View::ellipsize_text(ui, acc_name, 15.0, Colors::text(false)); - - // Show account BIP32 derivation path. - let acc_path = format!("{} {}", PATH, acc.path); - ui.label(RichText::new(acc_path).size(15.0).color(Colors::gray())); - ui.add_space(3.0); - }); - }); - }); - }); } \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/mod.rs b/src/gui/views/wallets/wallet/mod.rs index dc40505..a8f4233 100644 --- a/src/gui/views/wallets/wallet/mod.rs +++ b/src/gui/views/wallets/wallet/mod.rs @@ -27,4 +27,6 @@ mod transport; pub use transport::WalletTransport; mod content; -pub use content::WalletContent; \ No newline at end of file +pub use content::WalletContent; + +mod accounts; \ No newline at end of file From 5d83710fed77717e6e55adc6870f749a6aa154f8 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 14 Sep 2024 16:02:20 +0300 Subject: [PATCH 23/28] ui: dark colors fix --- src/gui/colors.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/gui/colors.rs b/src/gui/colors.rs index 109725e..0799e02 100644 --- a/src/gui/colors.rs +++ b/src/gui/colors.rs @@ -31,10 +31,14 @@ const YELLOW: Color32 = Color32::from_rgb(254, 241, 2); const YELLOW_DARK: Color32 = Color32::from_rgb(239, 229, 3); const GREEN: Color32 = Color32::from_rgb(0, 0x64, 0); +const GREEN_DARK: Color32 = Color32::from_rgb(0, (0x64 as f32 * 1.3 + 0.5) as u8, 0); const RED: Color32 = Color32::from_rgb(0x8B, 0, 0); +const RED_DARK: Color32 = Color32::from_rgb((0x8B as f32 * 1.3 + 0.5) as u8, 0, 0); const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4); +const BLUE_DARK: Color32 = + Color32::from_rgb(0, (0x66 as f32 * 1.3 + 0.5) as u8, (0xE4 as f32 * 1.3 + 0.5) as u8); const FILL: Color32 = Color32::from_gray(244); const FILL_DARK: Color32 = Color32::from_gray(24); @@ -125,7 +129,7 @@ impl Colors { pub fn green() -> Color32 { if use_dark() { - GREEN.gamma_multiply(1.3) + GREEN_DARK } else { GREEN } @@ -133,7 +137,7 @@ impl Colors { pub fn red() -> Color32 { if use_dark() { - RED.gamma_multiply(1.3) + RED_DARK } else { RED } @@ -141,7 +145,7 @@ impl Colors { pub fn blue() -> Color32 { if use_dark() { - BLUE.gamma_multiply(1.3) + BLUE_DARK } else { BLUE } From fe5aca6f0eb79ce46dc8741ed91c238358a14123 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 14 Sep 2024 16:08:40 +0300 Subject: [PATCH 24/28] build: remove debug from release profile --- Cargo.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 79edc3e..87fe238 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,6 @@ path = "src/main.rs" name="grim" crate-type = ["rlib"] -[profile.release] -debug = 1 - [profile.release-apk] inherits = "release" strip = true From 7cedebc70e88f4026776baf45d5ad4677ead5a59 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 14 Sep 2024 21:11:52 +0300 Subject: [PATCH 25/28] ui: qr scan and accounts modals module, parsing messages fix --- src/gui/views/wallets/wallet/content.rs | 167 +++++------------- .../views/wallets/wallet/messages/content.rs | 3 +- src/gui/views/wallets/wallet/mod.rs | 2 +- .../wallets/wallet/{ => modals}/accounts.rs | 12 +- src/gui/views/wallets/wallet/modals/mod.rs | 19 ++ src/gui/views/wallets/wallet/modals/scan.rs | 133 ++++++++++++++ src/wallet/wallet.rs | 2 +- 7 files changed, 202 insertions(+), 136 deletions(-) rename src/gui/views/wallets/wallet/{ => modals}/accounts.rs (97%) create mode 100644 src/gui/views/wallets/wallet/modals/mod.rs create mode 100644 src/gui/views/wallets/wallet/modals/scan.rs diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index b7aad65..f235666 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -13,20 +13,19 @@ // limitations under the License. use std::time::Duration; -use egui::{Align, Id, Layout, Margin, RichText, ScrollArea}; -use egui::scroll_area::ScrollBarVisibility; +use egui::{Align, Id, Layout, Margin, RichText}; use grin_chain::SyncStatus; use grin_core::core::amount_to_hr_string; use crate::AppConfig; use crate::gui::Colors; -use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CHAT_CIRCLE_TEXT, COPY, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, POWER, SCAN, SPINNER, USERS_THREE}; +use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CHAT_CIRCLE_TEXT, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, POWER, SCAN, SPINNER, USERS_THREE}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{CameraContent, Modal, Content, View}; +use crate::gui::views::{Modal, Content, View}; use crate::gui::views::types::{ModalPosition, QrScanResult}; use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport}; use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType}; -use crate::gui::views::wallets::wallet::accounts::WalletAccounts; +use crate::gui::views::wallets::wallet::modals::{WalletAccountsModal, WalletScanModal}; use crate::gui::views::wallets::wallet::WalletSettings; use crate::node::Node; use crate::wallet::{Wallet, WalletConfig}; @@ -35,12 +34,10 @@ use crate::wallet::types::WalletData; /// Selected and opened wallet content. pub struct WalletContent { /// Wallet accounts [`Modal`] content. - accounts_modal_content: Option, + accounts_modal_content: Option, - /// Camera content for QR scan [`Modal`]. - camera_content: CameraContent, - /// QR code scan result - qr_scan_result: Option, + /// QR code scan [`Modal`] content. + scan_modal_content: Option, /// Current tab content to show. pub current_tab: Box, @@ -57,8 +54,7 @@ impl WalletContent { pub fn new(data: Option) -> Self { let mut content = Self { accounts_modal_content: None, - camera_content: CameraContent::default(), - qr_scan_result: None, + scan_modal_content: None, current_tab: Box::new(WalletTransactions::default()), }; // Provide data to messages. @@ -180,7 +176,7 @@ impl WalletContent { /// Draw [`Modal`] content for this ui container. fn modal_content_ui(&mut self, ui: &mut egui::Ui, - wallet: &mut Wallet, + wallet: &Wallet, cb: &dyn PlatformCallbacks) { match Modal::opened() { None => {} @@ -194,9 +190,36 @@ impl WalletContent { } } QR_CODE_SCAN_MODAL => { - Modal::ui(ui.ctx(), |ui, modal| { - self.scan_qr_modal_ui(ui, wallet, modal, cb); - }); + if let Some(content) = self.scan_modal_content.as_mut() { + Modal::ui(ui.ctx(), |ui, modal| { + content.ui(ui, wallet, modal, cb, |result| { + match result { + QrScanResult::Slatepack(message) => { + modal.close(); + let msg = Some(message.to_string()); + let messages = WalletMessages::new(msg); + self.current_tab = Box::new(messages); + return; + } + QrScanResult::Address(receiver) => { + let balance = wallet.get_data() + .unwrap() + .info + .amount_currently_spendable; + if balance > 0 { + modal.close(); + let mut transport = WalletTransport::default(); + let rec = Some(receiver.to_string()); + transport.show_send_tor_modal(cb, rec); + self.current_tab = Box::new(transport); + return; + } + } + _ => {} + } + }); + }); + } } _ => {} } @@ -219,8 +242,7 @@ impl WalletContent { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { // Draw button to scan QR code. View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || { - self.qr_scan_result = None; - self.camera_content.clear_state(); + self.scan_modal_content = Some(WalletScanModal::default()); // Show QR code scan modal. Modal::new(QR_CODE_SCAN_MODAL) .position(ModalPosition::CenterTop) @@ -232,7 +254,7 @@ impl WalletContent { // Draw button to show list of accounts. View::item_button(ui, View::item_rounding(1, 3, true), USERS_THREE, None, || { - self.accounts_modal_content = Some(WalletAccounts::new(wallet.accounts())); + self.accounts_modal_content = Some(WalletAccountsModal::new(wallet.accounts())); // Show account list modal. Modal::new(ACCOUNT_LIST_MODAL) .position(ModalPosition::CenterTop) @@ -299,113 +321,6 @@ impl WalletContent { }); } - /// Draw QR code scan [`Modal`] content. - fn scan_qr_modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - // Show scan result if exists or show camera content while scanning. - if let Some(result) = &self.qr_scan_result { - let mut result_text = result.text(); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(3.0); - ScrollArea::vertical() - .id_source(Id::from("qr_scan_result_input").with(wallet.get_config().id)) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .max_height(128.0) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(7.0); - egui::TextEdit::multiline(&mut result_text) - .font(egui::TextStyle::Small) - .desired_rows(5) - .interactive(false) - .desired_width(f32::INFINITY) - .show(ui); - ui.add_space(6.0); - }); - ui.add_space(2.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(10.0); - - // Show copy button. - ui.vertical_centered(|ui| { - let copy_text = format!("{} {}", COPY, t!("copy")); - View::button(ui, copy_text, Colors::button(), || { - cb.copy_string_to_buffer(result_text.to_string()); - self.qr_scan_result = None; - modal.close(); - }); - }); - ui.add_space(10.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); - } else if let Some(result) = self.camera_content.qr_scan_result() { - cb.stop_camera(); - self.camera_content.clear_state(); - match &result { - QrScanResult::Slatepack(message) => { - // Redirect to messages to handle parsed message. - let mut messages = - WalletMessages::new(Some(message.to_string())); - messages.parse_message(wallet); - modal.close(); - self.current_tab = Box::new(messages); - return; - } - QrScanResult::Address(receiver) => { - if wallet.get_data().unwrap().info.amount_currently_spendable > 0 { - // Redirect to send amount with Tor. - let mut transport = WalletTransport::default(); - modal.close(); - transport.show_send_tor_modal(cb, Some(receiver.to_string())); - self.current_tab = Box::new(transport); - return; - } - } - _ => {} - } - - // Set result and rename modal title. - self.qr_scan_result = Some(result); - Modal::set_title(t!("scan_result")); - } else { - ui.add_space(6.0); - self.camera_content.ui(ui, cb); - ui.add_space(6.0); - } - - if self.qr_scan_result.is_some() { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.qr_scan_result = None; - modal.close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("repeat"), Colors::white_or_black(false), || { - Modal::set_title(t!("scan_qr")); - self.qr_scan_result = None; - cb.start_camera(); - }); - }); - }); - } else { - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - cb.stop_camera(); - modal.close(); - }); - }); - } - ui.add_space(6.0); - } - /// Draw tab buttons in the bottom of the screen. fn tabs_ui(&mut self, ui: &mut egui::Ui) { ui.scope(|ui| { diff --git a/src/gui/views/wallets/wallet/messages/content.rs b/src/gui/views/wallets/wallet/messages/content.rs index 5fded1b..5c02a60 100644 --- a/src/gui/views/wallets/wallet/messages/content.rs +++ b/src/gui/views/wallets/wallet/messages/content.rs @@ -378,7 +378,6 @@ impl WalletMessages { // Parse Slatepack message resetting message error. if buf != previous { self.parse_message(wallet); - self.parse_message(wallet); } }); }); @@ -405,7 +404,7 @@ impl WalletMessages { } /// Parse message input making operation based on incoming status. - pub fn parse_message(&mut self, wallet: &Wallet) { + fn parse_message(&mut self, wallet: &Wallet) { self.message_error.clear(); self.message_edit = self.message_edit.trim().to_string(); if self.message_edit.is_empty() { diff --git a/src/gui/views/wallets/wallet/mod.rs b/src/gui/views/wallets/wallet/mod.rs index a8f4233..34d3af7 100644 --- a/src/gui/views/wallets/wallet/mod.rs +++ b/src/gui/views/wallets/wallet/mod.rs @@ -29,4 +29,4 @@ pub use transport::WalletTransport; mod content; pub use content::WalletContent; -mod accounts; \ No newline at end of file +mod modals; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/accounts.rs b/src/gui/views/wallets/wallet/modals/accounts.rs similarity index 97% rename from src/gui/views/wallets/wallet/accounts.rs rename to src/gui/views/wallets/wallet/modals/accounts.rs index 6b50168..2974214 100644 --- a/src/gui/views/wallets/wallet/accounts.rs +++ b/src/gui/views/wallets/wallet/modals/accounts.rs @@ -25,8 +25,8 @@ use crate::gui::views::wallets::wallet::types::GRIN; use crate::wallet::types::WalletAccount; use crate::wallet::{Wallet, WalletConfig}; -/// Wallet accounts content. -pub struct WalletAccounts { +/// Wallet accounts [`Modal`] content. +pub struct WalletAccountsModal { /// List of wallet accounts. accounts: Vec, /// Flag to check if account is creating. @@ -37,7 +37,7 @@ pub struct WalletAccounts { account_creation_error: bool, } -impl Default for WalletAccounts { +impl Default for WalletAccountsModal { fn default() -> Self { Self { accounts: vec![], @@ -48,7 +48,7 @@ impl Default for WalletAccounts { } } -impl WalletAccounts { +impl WalletAccountsModal { /// Create new instance from wallet accounts. pub fn new(accounts: Vec) -> Self { Self { @@ -62,7 +62,7 @@ impl WalletAccounts { /// Draw [`Modal`] content. pub fn ui(&mut self, ui: &mut egui::Ui, - wallet: &mut Wallet, + wallet: &Wallet, modal: &Modal, cb: &dyn PlatformCallbacks) { if self.account_creating { @@ -180,7 +180,7 @@ const ACCOUNT_ITEM_HEIGHT: f32 = 75.0; /// Draw account item. fn account_item_ui(ui: &mut egui::Ui, modal: &Modal, - wallet: &mut Wallet, + wallet: &Wallet, acc: &WalletAccount, index: usize, size: usize) { diff --git a/src/gui/views/wallets/wallet/modals/mod.rs b/src/gui/views/wallets/wallet/modals/mod.rs new file mode 100644 index 0000000..7a350f7 --- /dev/null +++ b/src/gui/views/wallets/wallet/modals/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2024 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod accounts; +pub use accounts::*; + +mod scan; +pub use scan::*; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/modals/scan.rs b/src/gui/views/wallets/wallet/modals/scan.rs new file mode 100644 index 0000000..3ac383c --- /dev/null +++ b/src/gui/views/wallets/wallet/modals/scan.rs @@ -0,0 +1,133 @@ +// Copyright 2024 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use egui::scroll_area::ScrollBarVisibility; +use egui::{Id, ScrollArea}; + +use crate::gui::Colors; +use crate::gui::icons::COPY; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{CameraContent, Modal, View}; +use crate::gui::views::types::QrScanResult; +use crate::wallet::Wallet; + +/// QR code scan [`Modal`] content. +pub struct WalletScanModal { + /// Camera content for QR scan [`Modal`]. + camera_content: Option, + /// QR code scan result + qr_scan_result: Option, +} + +impl Default for WalletScanModal { + fn default() -> Self { + Self { + camera_content: None, + qr_scan_result: None, + } + } +} + +impl WalletScanModal { + /// Draw [`Modal`] content. + pub fn ui(&mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks, + mut on_result: impl FnMut(&QrScanResult)) { + // Show scan result if exists or show camera content while scanning. + if let Some(result) = &self.qr_scan_result { + let mut result_text = result.text(); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(3.0); + ScrollArea::vertical() + .id_source(Id::from("qr_scan_result_input").with(wallet.get_config().id)) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(128.0) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + egui::TextEdit::multiline(&mut result_text) + .font(egui::TextStyle::Small) + .desired_rows(5) + .interactive(false) + .desired_width(f32::INFINITY) + .show(ui); + ui.add_space(6.0); + }); + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(10.0); + + // Show copy button. + ui.vertical_centered(|ui| { + let copy_text = format!("{} {}", COPY, t!("copy")); + View::button(ui, copy_text, Colors::button(), || { + cb.copy_string_to_buffer(result_text.to_string()); + self.qr_scan_result = None; + modal.close(); + }); + }); + ui.add_space(10.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); + } else if let Some(result) = self.camera_content.get_or_insert(CameraContent::default()) + .qr_scan_result() { + cb.stop_camera(); + self.camera_content = None; + on_result(&result); + + // Set result and rename modal title. + self.qr_scan_result = Some(result); + Modal::set_title(t!("scan_result")); + } else { + ui.add_space(6.0); + self.camera_content.as_mut().unwrap().ui(ui, cb); + ui.add_space(6.0); + } + + if self.qr_scan_result.is_some() { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + self.qr_scan_result = None; + self.camera_content = None; + modal.close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("repeat"), Colors::white_or_black(false), || { + Modal::set_title(t!("scan_qr")); + self.qr_scan_result = None; + self.camera_content = Some(CameraContent::default()); + cb.start_camera(); + }); + }); + }); + } else { + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + cb.stop_camera(); + self.camera_content = None; + modal.close(); + }); + }); + } + ui.add_space(6.0); + } +} \ No newline at end of file diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 771e013..56c1171 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -471,7 +471,7 @@ impl Wallet { } /// Set active account from provided label. - pub fn set_active_account(&mut self, label: &String) -> Result<(), Error> { + pub fn set_active_account(&self, label: &String) -> Result<(), Error> { let mut api = Owner::new(self.instance.clone().unwrap(), None); controller::owner_single_use(None, None, Some(&mut api), |api, m| { api.set_active_account(m, label)?; From 150a0de1c4b5a4b35fbbce248e005f0dd0223cba Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 14 Sep 2024 21:17:43 +0300 Subject: [PATCH 26/28] android: always build with release-apk profile --- scripts/android.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/android.sh b/scripts/android.sh index caed848..4446447 100755 --- a/scripts/android.sh +++ b/scripts/android.sh @@ -33,7 +33,6 @@ cargo install cargo-ndk success=1 ### Build native code -[[ $1 == "release" ]] && release_param="--profile release-apk" function build_lib() { [[ $1 == "v7" ]] && arch=(armeabi-v7a) [[ $1 == "v8" ]] && arch=(arm64-v8a) @@ -43,9 +42,9 @@ function build_lib() { # Fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" - cargo ndk -t ${arch} build ${release_param} + cargo ndk -t ${arch} build --profile release-apk unset CPPFLAGS && unset CFLAGS - cargo ndk -t ${arch} -o android/app/src/main/jniLibs build ${release_param} + cargo ndk -t ${arch} -o android/app/src/main/jniLibs build --profile release-apk if [ $? -eq 0 ] then success=1 From d6ec4213abc797db3dc26682eae59102b515ae82 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 14 Sep 2024 21:21:03 +0300 Subject: [PATCH 27/28] ui: ability to finalize tx only when wallet is loaded --- src/gui/views/wallets/wallet/txs/content.rs | 5 +++-- src/gui/views/wallets/wallet/txs/tx.rs | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/gui/views/wallets/wallet/txs/content.rs b/src/gui/views/wallets/wallet/txs/content.rs index b1d98d0..e7a63dd 100644 --- a/src/gui/views/wallets/wallet/txs/content.rs +++ b/src/gui/views/wallets/wallet/txs/content.rs @@ -204,8 +204,10 @@ impl WalletTransactions { }); } + let wallet_loaded = wallet.foreign_api_port().is_some(); + // Draw button to show transaction finalization. - if tx.can_finalize { + if wallet_loaded && tx.can_finalize { let (icon, color) = (CHECK, Some(Colors::green())); View::item_button(ui, Rounding::default(), icon, color, || { cb.hide_keyboard(); @@ -214,7 +216,6 @@ impl WalletTransactions { } // Draw button to cancel transaction. - let wallet_loaded = wallet.foreign_api_port().is_some(); if wallet_loaded && tx.can_cancel() { let (icon, color) = (PROHIBIT, Some(Colors::red())); View::item_button(ui, Rounding::default(), icon, color, || { diff --git a/src/gui/views/wallets/wallet/txs/tx.rs b/src/gui/views/wallets/wallet/txs/tx.rs index 8bcc89a..cfc4ae7 100644 --- a/src/gui/views/wallets/wallet/txs/tx.rs +++ b/src/gui/views/wallets/wallet/txs/tx.rs @@ -140,8 +140,10 @@ impl WalletTransactionModal { return; } + let wallet_loaded = wallet.foreign_api_port().is_some(); + // Draw button to show transaction finalization or transaction info. - if tx.can_finalize { + if wallet_loaded && tx.can_finalize { let (icon, color) = if self.show_finalization { (FILE_TEXT, None) } else { @@ -161,7 +163,6 @@ impl WalletTransactionModal { } // Draw button to cancel transaction. - let wallet_loaded = wallet.foreign_api_port().is_some(); if wallet_loaded && tx.can_cancel() { View::item_button(ui, Rounding::default(), PROHIBIT, Some(Colors::red()), || { cb.hide_keyboard(); From c3fae38d5cb302e4ff40f99b179facded6b392df Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sun, 15 Sep 2024 15:54:07 +0300 Subject: [PATCH 28/28] desktop: open camera check --- src/gui/platform/desktop/mod.rs | 51 ++++++++++++++++----------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/gui/platform/desktop/mod.rs b/src/gui/platform/desktop/mod.rs index 85c0f0b..5c02cd2 100644 --- a/src/gui/platform/desktop/mod.rs +++ b/src/gui/platform/desktop/mod.rs @@ -226,36 +226,35 @@ impl Desktop { let ctx = PlatformContext::default(); let devices = ctx.devices().unwrap(); - let dev = ctx.open_device(&devices[0].uri).unwrap(); + if let Ok(dev) = ctx.open_device(&devices[0].uri) { + let streams = dev.streams().unwrap(); + let stream_desc = streams[0].clone(); + let w = stream_desc.width; + let h = stream_desc.height; - let streams = dev.streams().unwrap(); - let stream_desc = streams[0].clone(); - let w = stream_desc.width; - let h = stream_desc.height; + let mut stream = dev.start_stream(&stream_desc).unwrap(); - let mut stream = dev.start_stream(&stream_desc).unwrap(); - - loop { - // Stop if camera was stopped. - if stop_camera.load(Ordering::Relaxed) { - stop_camera.store(false, Ordering::Relaxed); - // Clear image. + loop { + // Stop if camera was stopped. + if stop_camera.load(Ordering::Relaxed) { + stop_camera.store(false, Ordering::Relaxed); + let mut w_image = LAST_CAMERA_IMAGE.write(); + *w_image = None; + break; + } + // Get a frame. + let frame = stream.next().expect("Stream is dead").expect("Failed to capture a frame"); + let mut out = vec![]; + if let Some(buf) = image::ImageBuffer::, &[u8]>::from_raw(w, h, &frame) { + image::codecs::jpeg::JpegEncoder::new(&mut out) + .write_image(buf.as_raw(), w, h, image::ExtendedColorType::Rgb8).unwrap(); + } else { + out = frame.to_vec(); + } + // Save image. let mut w_image = LAST_CAMERA_IMAGE.write(); - *w_image = None; - break; + *w_image = Some((out, 0)); } - // Get a frame. - let frame = stream.next().expect("Stream is dead").expect("Failed to capture a frame"); - let mut out = vec![]; - if let Some(buf) = image::ImageBuffer::, &[u8]>::from_raw(w, h, &frame) { - image::codecs::jpeg::JpegEncoder::new(&mut out) - .write_image(buf.as_raw(), w, h, image::ExtendedColorType::Rgb8).unwrap(); - } else { - out = frame.to_vec(); - } - // Save image. - let mut w_image = LAST_CAMERA_IMAGE.write(); - *w_image = Some((out, 0)); } } }