Files
grim/src/gui/views/wallets/wallet/content.rs
T

505 lines
22 KiB
Rust

// 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::Duration;
use egui::{Align, Id, Layout, Margin, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
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, CAMERA_ROTATE, CHAT_CIRCLE_TEXT, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, POWER, SCAN, SPINNER, USERS_THREE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, View, CameraContent};
use crate::gui::views::types::{LinePosition, ModalContainer, ModalPosition};
use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport};
use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::modals::WalletAccountsModal;
use crate::gui::views::wallets::wallet::WalletSettings;
use crate::node::Node;
use crate::wallet::{ExternalConnection, Wallet, WalletConfig};
use crate::wallet::types::{ConnectionMethod, WalletData};
/// Wallet content.
pub struct WalletContent {
/// Selected and opened wallet.
pub wallet: Wallet,
/// Current tab content to show.
pub current_tab: Box<dyn WalletTab>,
/// Wallet accounts [`Modal`] content.
accounts_modal_content: Option<WalletAccountsModal>,
/// QR code scan content.
pub qr_scan_content: Option<CameraContent>,
/// List of allowed [`Modal`] ids for this [`ModalContainer`].
allowed_modal_ids: Vec<&'static str>
}
/// Identifier for account list [`Modal`].
const ACCOUNT_LIST_MODAL: &'static str = "account_list_modal";
impl ModalContainer for WalletContent {
fn modal_ids(&self) -> &Vec<&'static str> {
&self.allowed_modal_ids
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
ACCOUNT_LIST_MODAL => {
if let Some(content) = self.accounts_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, &self.wallet, modal, cb);
});
}
}
_ => {}
}
}
}
impl WalletContent {
/// Create new instance with optional data.
pub fn new(wallet: Wallet, data: Option<String>) -> Self {
let mut content = Self {
wallet,
accounts_modal_content: None,
qr_scan_content: None,
current_tab: Box::new(WalletTransactions::default()),
allowed_modal_ids: vec![
ACCOUNT_LIST_MODAL,
],
};
if data.is_some() {
content.on_data(data);
}
content
}
/// Handle data from deeplink or opened file.
pub fn on_data(&mut self, data: Option<String>) {
self.current_tab = Box::new(WalletMessages::new(data));
}
/// Draw wallet content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.ctx().request_repaint_after(Duration::from_millis(1000));
self.current_modal_ui(ui, cb);
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
let show_wallets_dual = AppConfig::show_wallets_at_dual_panel();
let wallet = &self.wallet;
let wallet_id = wallet.identifier();
let data = wallet.get_data();
let show_qr_scan = self.qr_scan_content.is_some();
let hide_tabs = Self::block_navigation_on_sync(wallet);
// Show wallet account panel not on settings tab when navigation is not blocked and QR code
// scanner is not showing and wallet data is not empty.
let mut show_account = self.current_tab.get_type() != WalletTabType::Settings && !hide_tabs
&& !wallet.sync_error() && data.is_some();
if wallet.get_current_connection() == ConnectionMethod::Integrated && !Node::is_running() {
show_account = false;
}
// Close scanner when balance got hidden.
if !show_account && show_qr_scan {
cb.stop_camera();
self.qr_scan_content = None;
}
egui::TopBottomPanel::top(Id::from("wallet_account").with(wallet.identifier()))
.frame(egui::Frame {
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0,
top: 4.0,
bottom: 0.0,
},
fill: Colors::fill(),
..Default::default()
})
.show_animated_inside(ui, show_account, |ui| {
let rect = ui.available_rect_before_wrap();
if show_qr_scan {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH, |ui| {
self.qr_scan_content.as_mut().unwrap().ui(ui, cb);
ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
cb.stop_camera();
self.qr_scan_content = None;
});
});
ui.add_space(6.0);
});
} else {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.account_ui(ui, data.unwrap(), cb);
});
}
// Draw content divider lines.
let r = {
let mut r = rect.clone();
r.min.x -= 4.0 + View::far_left_inset_margin(ui);
r.min.y -= 4.0;
r.max.x += 4.0 + View::get_right_inset();
r
};
View::line(ui, LinePosition::BOTTOM, &r, Colors::item_stroke());
if dual_panel && show_wallets_dual && !show_qr_scan {
View::line(ui, LinePosition::LEFT, &r, Colors::item_stroke());
}
});
// Show wallet tabs.
let show_tabs = !hide_tabs && self.qr_scan_content.is_none();
egui::TopBottomPanel::bottom("wallet_tabs")
.frame(egui::Frame {
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING,
right: View::get_right_inset() + View::TAB_ITEMS_PADDING,
top: View::TAB_ITEMS_PADDING,
bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
},
fill: Colors::fill(),
..Default::default()
})
.show_animated_inside(ui, show_tabs, |ui| {
let rect = ui.available_rect_before_wrap();
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tabs_ui(ui, cb);
});
let rect = {
let mut r = rect.clone();
r.min.x -= View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING;
r.min.y -= View::TAB_ITEMS_PADDING;
r.max.x += View::get_right_inset() + View::TAB_ITEMS_PADDING;
r.max.y += View::get_bottom_inset() + View::TAB_ITEMS_PADDING;
r
};
// Draw content divider line.
View::line(ui, LinePosition::TOP, &rect, Colors::stroke());
});
// Show tab content.
egui::CentralPanel::default()
.frame(egui::Frame {
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| {
let rect = ui.available_rect_before_wrap();
let tab_type = self.current_tab.get_type();
let show_sync = (tab_type != WalletTabType::Settings || hide_tabs) &&
sync_ui(ui, &self.wallet);
if !show_sync {
if tab_type != WalletTabType::Txs {
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(Id::from("wallet_scroll")
.with(tab_type.name())
.with(wallet_id))
.auto_shrink([false; 2])
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.show(ui, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.current_tab.ui(ui, &self.wallet, cb);
});
});
} else {
self.current_tab.ui(ui, &self.wallet, cb);
}
}
let rect = {
let mut r = rect.clone();
r.min.x -= View::far_left_inset_margin(ui) + 4.0;
r.max.x += View::get_right_inset() + 4.0;
r.max.y += 4.0;
r
};
// Draw cover when QR code scanner is active.
if show_qr_scan {
View::content_cover_ui(ui, rect, "wallet_tab", || {
cb.stop_camera();
self.qr_scan_content = None;
});
}
// Draw content divider line.
if dual_panel && show_wallets_dual {
View::line(ui, LinePosition::LEFT, &rect, Colors::item_stroke());
}
});
}
/// Check when to block tabs navigation on sync progress.
pub fn block_navigation_on_sync(wallet: &Wallet) -> bool {
let sync_error = wallet.sync_error();
let integrated_node = wallet.get_current_connection() == ConnectionMethod::Integrated;
let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
let sync_after_opening = wallet.get_data().is_none() && !wallet.sync_error();
// Block navigation if wallet is repairing and integrated node is not launching
// and if wallet is closing or syncing after opening when there is no data to show.
(wallet.is_repairing() && (integrated_node_ready || !integrated_node) && !sync_error)
|| wallet.is_closing() || (sync_after_opening &&
(!integrated_node || integrated_node_ready))
}
/// Draw wallet account content.
fn account_ui(&mut self,
ui: &mut egui::Ui,
data: WalletData,
cb: &dyn PlatformCallbacks) {
let mut rect = ui.available_rect_before_wrap();
rect.set_height(75.0);
// Draw round background.
let rounding = View::item_rounding(0, 2, false);
ui.painter().rect(rect, rounding, Colors::fill_lite(), View::item_stroke());
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to show QR code scanner.
View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || {
self.qr_scan_content = Some(CameraContent::default());
cb.start_camera();
});
// Draw button to show list of accounts.
View::item_button(ui, View::item_rounding(1, 3, true), USERS_THREE, None, || {
self.accounts_modal_content = Some(
WalletAccountsModal::new(self.wallet.accounts())
);
Modal::new(ACCOUNT_LIST_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.accounts"))
.show();
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(8.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Show spendable amount.
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
let amount_text = format!("{} {}", amount, GRIN);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(RichText::new(amount_text)
.size(18.0)
.color(Colors::white_or_black(true)));
});
ui.add_space(-2.0);
// Show account label.
let account = self.wallet.get_config().account;
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
let acc_label = if account == default_acc_label {
t!("wallets.default_account")
} else {
account.to_owned()
};
let acc_text = format!("{} {}", FOLDER_USER, acc_label);
View::ellipsize_text(ui, acc_text, 15.0, Colors::text(false));
// Show confirmed height or sync progress.
let status_text = if !self.wallet.syncing() {
format!("{} {}", PACKAGE, data.info.last_confirmed_height)
} else {
let info_progress = self.wallet.info_sync_progress();
if info_progress == 100 || info_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.wallet_loading"))
} else {
if self.wallet.is_repairing() {
let rep_progress = self.wallet.repairing_progress();
if rep_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.wallet_checking"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.wallet_checking"),
rep_progress)
}
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.wallet_loading"),
info_progress)
}
}
};
View::animate_text(ui,
status_text,
15.0,
Colors::gray(),
self.wallet.syncing());
})
});
});
}
/// Draw tab buttons at the bottom of the screen.
fn tabs_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.scope(|ui| {
// Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
// Show camera switch button at QR code scan.
if self.qr_scan_content.is_some() && cb.can_switch_camera() {
// Setup vertical padding inside tab button.
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0);
ui.vertical_centered(|ui| {
View::tab_button(ui, CAMERA_ROTATE, false, |_| {
cb.switch_camera();
});
});
return;
}
// Setup vertical padding inside buttons.
ui.style_mut().spacing.button_padding = egui::vec2(0.0, 4.0);
let current_type = self.current_tab.get_type();
ui.columns(4, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::tab_button(ui, GRAPH, current_type == WalletTabType::Txs, |_| {
self.current_tab = Box::new(WalletTransactions::default());
});
});
columns[1].vertical_centered_justified(|ui| {
let is_messages = current_type == WalletTabType::Messages;
View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, |_| {
self.current_tab = Box::new(
WalletMessages::new(None)
);
});
});
columns[2].vertical_centered_justified(|ui| {
View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, |_| {
self.current_tab = Box::new(WalletTransport::default());
});
});
columns[3].vertical_centered_justified(|ui| {
View::tab_button(ui, GEAR_FINE, current_type == WalletTabType::Settings, |ui| {
ExternalConnection::check(None, ui.ctx());
self.current_tab = Box::new(WalletSettings::default());
});
});
});
});
}
}
/// Draw content when wallet is syncing and not ready to use, returns `true` at this case.
fn sync_ui(ui: &mut egui::Ui, wallet: &Wallet) -> bool {
if wallet.is_repairing() && !wallet.sync_error() {
sync_progress_ui(ui, wallet);
return true;
} else if wallet.is_closing() {
sync_progress_ui(ui, wallet);
return true;
} else if wallet.get_current_connection() == ConnectionMethod::Integrated {
if !Node::is_running() || Node::is_stopping() {
View::center_content(ui, 108.0, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
let text = t!("wallets.enable_node", "settings" => GEAR_FINE);
ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text()));
ui.add_space(8.0);
// Show button to enable integrated node at non-dual root panel mode
// or when network connections are not showing and node is not stopping
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
if (!dual_panel || AppConfig::show_connections_network_panel())
&& !Node::is_stopping() {
let enable_text = format!("{} {}", POWER, t!("network.enable_node"));
View::action_button(ui, enable_text, || {
Node::start();
});
}
});
});
return true
} else if wallet.sync_error()
&& Node::get_sync_status() == Some(SyncStatus::NoSync) {
sync_error_ui(ui, wallet);
return true;
} else if wallet.get_data().is_none() {
sync_progress_ui(ui, wallet);
return true;
}
} else if wallet.sync_error() {
sync_error_ui(ui, wallet);
return true;
} else if wallet.get_data().is_none() {
sync_progress_ui(ui, wallet);
return true;
}
false
}
/// Draw wallet sync error content.
fn sync_error_ui(ui: &mut egui::Ui, wallet: &Wallet) {
View::center_content(ui, 108.0, |ui| {
let text = t!("wallets.wallet_loading_err", "settings" => GEAR_FINE);
ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text()));
ui.add_space(8.0);
let retry_text = format!("{} {}", ARROWS_CLOCKWISE, t!("retry"));
View::action_button(ui, retry_text, || {
wallet.set_sync_error(false);
});
});
}
/// Draw wallet sync progress content.
fn sync_progress_ui(ui: &mut egui::Ui, wallet: &Wallet) {
View::center_content(ui, 162.0, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
View::big_loading_spinner(ui);
ui.add_space(18.0);
// Setup sync progress text.
let text = {
let int_node = wallet.get_current_connection() == ConnectionMethod::Integrated;
let int_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
let info_progress = wallet.info_sync_progress();
if wallet.is_closing() {
t!("wallets.wallet_closing")
} else if int_node && !int_ready {
t!("wallets.node_loading", "settings" => GEAR_FINE)
} else if wallet.is_repairing() {
let repair_progress = wallet.repairing_progress();
if repair_progress == 0 {
t!("wallets.wallet_checking")
} else {
format!("{}: {}%", t!("wallets.wallet_checking"), repair_progress)
}
} else if info_progress != 100 {
if info_progress == 0 {
t!("wallets.wallet_loading")
} else {
format!("{}: {}%", t!("wallets.wallet_loading"), info_progress)
}
} else {
t!("wallets.tx_loading")
}
};
ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text()));
});
});
}