From 853bdcd76e870e402603a6eb70f9ed0c33d9cfb4 Mon Sep 17 00:00:00 2001 From: Uncle Stretch Date: Sun, 2 Feb 2025 16:41:08 +0300 Subject: [PATCH] additional pop-up for balance detalization in address book Signed-off-by: Uncle Stretch --- src/action.rs | 1 + src/components/wallet/address_book.rs | 20 ++- src/components/wallet/balance.rs | 18 +-- src/components/wallet/details.rs | 202 ++++++++++++++++++++++++++ src/components/wallet/mod.rs | 47 ++++-- 5 files changed, 264 insertions(+), 24 deletions(-) create mode 100644 src/components/wallet/details.rs diff --git a/src/action.rs b/src/action.rs index a3b8103..ffd93ef 100644 --- a/src/action.rs +++ b/src/action.rs @@ -45,6 +45,7 @@ pub enum Action { UpdateAddressBookRecord(String), UpdateKnownValidator(String), TransferTo(String), + AccountDetailsOf(String, Option), TransferBalance(String, [u8; 32], u128), EventLog(String, ActionLevel, ActionTarget), diff --git a/src/components/wallet/address_book.rs b/src/components/wallet/address_book.rs index eae96d8..8f57430 100644 --- a/src/components/wallet/address_book.rs +++ b/src/components/wallet/address_book.rs @@ -231,6 +231,19 @@ impl AddressBook { } } + fn show_account_details(&mut self) { + if let Some(index) = self.table_state.selected() { + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::AccountDetailsOf( + self.address_book[index].name.clone(), + self.balances + .get(&self.address_book[index].account_id) + .map(|data| data.clone()), + )); + } + } + } + fn swap_up(&mut self) { if let Some(src_index) = self.table_state.selected() { let dst_index = src_index.saturating_sub(1); @@ -390,6 +403,7 @@ impl Component for AddressBook { KeyCode::Char('J') => self.swap_down(), KeyCode::Char('D') => self.delete_row(), KeyCode::Char('R') => self.update_address_book_record(), + KeyCode::Char('I') => self.show_account_details(), KeyCode::Enter => self.send_transfer_to(), _ => {}, }; @@ -407,11 +421,7 @@ impl Component for AddressBook { .map(|info| { let balance = self.balances .get(&info.account_id) - .map(|inner_balance| { - inner_balance.free - .saturating_add(inner_balance.reserved) - .saturating_add(inner_balance.frozen) - }); + .map(|inner_balance| inner_balance.free); Row::new(vec![ Cell::from(Text::from(info.name.clone()).alignment(Alignment::Left)), Cell::from(Text::from(info.address.clone()).alignment(Alignment::Center)), diff --git a/src/components/wallet/balance.rs b/src/components/wallet/balance.rs index c8d3f51..2cb56ff 100644 --- a/src/components/wallet/balance.rs +++ b/src/components/wallet/balance.rs @@ -20,7 +20,7 @@ pub struct Balance { total_balance: Option, transferable_balance: Option, locked_balance: Option, - bonded_balance: Option, + reserved_balance: Option, nonce: Option, palette: StylePalette } @@ -41,7 +41,7 @@ impl Balance { total_balance: None, transferable_balance: None, locked_balance: None, - bonded_balance: None, + reserved_balance: None, nonce: None, palette: StylePalette::default(), } @@ -87,19 +87,19 @@ impl Component for Balance { match maybe_balance { Some(balance) => { self.total_balance = Some(balance.free); - self.locked_balance = Some(balance.reserved); - self.bonded_balance = Some(balance.frozen); + self.locked_balance = Some(balance.frozen); + self.reserved_balance = Some(balance.reserved); self.nonce = Some(balance.nonce); let transferable = balance.free - .saturating_add(balance.reserved) - .saturating_add(balance.frozen); + .saturating_sub(balance.reserved) + .saturating_sub(balance.frozen); self.transferable_balance = Some(transferable); }, None => { self.transferable_balance = None; self.locked_balance = None; - self.bonded_balance = None; + self.reserved_balance = None; self.total_balance = None; self.nonce = None; } @@ -137,8 +137,8 @@ impl Component for Balance { Cell::from(Text::from(self.prepare_u128(self.locked_balance)).alignment(Alignment::Right)), ]), Row::new(vec![ - Cell::from(Text::from("bonded: ".to_string()).alignment(Alignment::Left)), - Cell::from(Text::from(self.prepare_u128(self.bonded_balance)).alignment(Alignment::Right)), + Cell::from(Text::from("reserved: ".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.prepare_u128(self.reserved_balance)).alignment(Alignment::Right)), ]), ], [ diff --git a/src/components/wallet/details.rs b/src/components/wallet/details.rs new file mode 100644 index 0000000..c5e6645 --- /dev/null +++ b/src/components/wallet/details.rs @@ -0,0 +1,202 @@ +use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; +use color_eyre::Result; +use ratatui::{ + layout::{Alignment, Constraint, Flex, Layout, Rect}, + widgets::{Block, Cell, Clear, Row, Table}, + text::Text, + Frame, +}; +use tokio::sync::mpsc::UnboundedSender; + +use super::{Component, PartialComponent, CurrentTab}; +use crate::{ + action::Action, + config::Config, + palette::StylePalette, + widgets::DotSpinner, +}; + +#[derive(Debug)] +pub struct AccountDetails { + is_active: bool, + action_tx: Option>, + palette: StylePalette, + name: String, + transferable_balance: Option, + locked_balance: Option, + reserved_balance: Option, + total_balance: Option, + nonce: Option, +} + +impl Default for AccountDetails { + fn default() -> Self { + Self::new() + } +} + +impl AccountDetails { + const TICKER: &str = " CSPR"; + const DECIMALS: usize = 6; + + pub fn new() -> Self { + Self { + is_active: false, + action_tx: None, + palette: StylePalette::default(), + name: String::new(), + transferable_balance: None, + locked_balance: None, + reserved_balance: None, + total_balance: None, + nonce: None, + } + } + + fn close_popup(&mut self) { + self.is_active = false; + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::ClosePopup); + } + } + + fn prepare_u128(&self, maybe_value: Option) -> String { + match maybe_value { + Some(value) => { + let value = value as f64 / 10f64.powi(18); + let after = Self::DECIMALS; + format!("{:.after$}{}", value, Self::TICKER) + }, + None => format!("{}{}", DotSpinner::default().to_string(), Self::TICKER) + } + } +} + +impl PartialComponent for AccountDetails { + fn set_active(&mut self, current_tab: CurrentTab) { + match current_tab { + CurrentTab::AccountDetails => self.is_active = true, + _ => { + if self.is_active { + self.is_active = false; + self.name = String::new(); + self.transferable_balance = None; + self.locked_balance = None; + self.reserved_balance = None; + self.total_balance = None; + self.nonce = None; + } + } + }; + } +} + +impl Component for AccountDetails { + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.action_tx = Some(tx); + Ok(()) + } + + fn register_config_handler(&mut self, config: Config) -> Result<()> { + if let Some(style) = config.styles.get(&crate::app::Mode::Wallet) { + self.palette.with_normal_style(style.get("normal_style").copied()); + self.palette.with_normal_border_style(style.get("normal_border_style").copied()); + self.palette.with_normal_title_style(style.get("normal_title_style").copied()); + self.palette.with_popup_style(style.get("popup_style").copied()); + self.palette.with_popup_title_style(style.get("popup_title_style").copied()); + } + Ok(()) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::AccountDetailsOf(name, maybe_account_info) => { + self.name = name; + match maybe_account_info { + Some(account_info) => { + self.total_balance = Some(account_info.free); + self.locked_balance = Some(account_info.frozen); + self.reserved_balance = Some(account_info.reserved); + self.nonce = Some(account_info.nonce); + + let transferable = account_info.free + .saturating_sub(account_info.reserved) + .saturating_sub(account_info.frozen); + self.transferable_balance = Some(transferable); + }, + None => { + self.transferable_balance = None; + self.locked_balance = None; + self.reserved_balance = None; + self.total_balance = None; + self.nonce = None; + } + } + } + _ => {} + }; + Ok(None) + } + + fn handle_key_event(&mut self, key: KeyEvent) -> Result> { + if self.is_active && key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Esc | KeyCode::Enter => self.close_popup(), + _ => {}, + }; + } + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + if self.is_active { + let (border_style, border_type) = self.palette.create_popup_style(); + let table = Table::new( + [ + Row::new(vec![ + Cell::from(Text::from("nonce: ".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.nonce + .map(|n| n.to_string()) + .unwrap_or(DotSpinner::default().to_string()) + ).alignment(Alignment::Right)), + ]), + Row::new(vec![ + Cell::from(Text::from("total: ".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.prepare_u128(self.total_balance)).alignment(Alignment::Right)) + ]), + Row::new(vec![ + Cell::from(Text::from("free: ".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.prepare_u128(self.transferable_balance)).alignment(Alignment::Right)) + ]), + Row::new(vec![ + Cell::from(Text::from("locked: ".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.prepare_u128(self.locked_balance)).alignment(Alignment::Right)), + ]), + Row::new(vec![ + Cell::from(Text::from("reserved: ".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.prepare_u128(self.reserved_balance)).alignment(Alignment::Right)), + ]), + ], + [ + Constraint::Max(10), + Constraint::Min(14), + ] + ) + .block(Block::bordered() + .border_style(border_style) + .border_type(border_type) + .title_alignment(Alignment::Right) + .title_style(self.palette.create_title_style(false)) + .title(format!("Details for {}", &self.name))); + + let v = Layout::vertical([Constraint::Max(7)]).flex(Flex::Center); + let h = Layout::horizontal([Constraint::Max(50)]).flex(Flex::Center); + let [area] = v.areas(area); + let [area] = h.areas(area); + + frame.render_widget(Clear, area); + frame.render_widget(table, area); + } + Ok(()) + } +} diff --git a/src/components/wallet/mod.rs b/src/components/wallet/mod.rs index 17f38d5..2c5134f 100644 --- a/src/components/wallet/mod.rs +++ b/src/components/wallet/mod.rs @@ -18,6 +18,7 @@ mod accounts; mod overview; mod add_address_book_record; mod rename_address_book_record; +mod details; use balance::Balance; use transfer::Transfer; @@ -29,11 +30,12 @@ use accounts::Accounts; use overview::Overview; use add_address_book_record::AddAddressBookRecord; use rename_address_book_record::RenameAddressBookRecord; +use details::AccountDetails; use super::Component; use crate::{action::Action, app::Mode, config::Config}; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq)] pub enum CurrentTab { Nothing, Accounts, @@ -44,6 +46,7 @@ pub enum CurrentTab { RenameAccount, RenameAddressBookRecord, Transfer, + AccountDetails, } pub trait PartialComponent: Component { @@ -53,6 +56,7 @@ pub trait PartialComponent: Component { pub struct Wallet { is_active: bool, current_tab: CurrentTab, + previous_tab: CurrentTab, components: Vec>, } @@ -61,6 +65,7 @@ impl Default for Wallet { Self { is_active: false, current_tab: CurrentTab::Accounts, + previous_tab: CurrentTab::Accounts, components: vec![ Box::new(Overview::default()), Box::new(Accounts::default()), @@ -72,6 +77,7 @@ impl Default for Wallet { Box::new(AddAddressBookRecord::default()), Box::new(RenameAddressBookRecord::default()), Box::new(Transfer::default()), + Box::new(AccountDetails::default()), ], } } @@ -127,9 +133,10 @@ impl Component for Wallet { CurrentTab::RenameAccount | CurrentTab::RenameAddressBookRecord | CurrentTab::Transfer | + CurrentTab::AccountDetails | CurrentTab::AddAddressBookRecord => match key.code { KeyCode::Esc => { - self.current_tab = CurrentTab::Accounts; + self.current_tab = self.previous_tab; for component in self.components.iter_mut() { component.set_active(self.current_tab.clone()); } @@ -150,18 +157,21 @@ impl Component for Wallet { return Ok(Some(Action::SetActiveScreen(Mode::Menu))); }, KeyCode::Char('W') => { + self.previous_tab = self.current_tab; self.current_tab = CurrentTab::AddAccount; for component in self.components.iter_mut() { component.set_active(self.current_tab.clone()); } }, KeyCode::Char('A') => { + self.previous_tab = self.current_tab; self.current_tab = CurrentTab::AddAddressBookRecord; for component in self.components.iter_mut() { component.set_active(self.current_tab.clone()); } }, KeyCode::Char('T') => { + self.previous_tab = self.current_tab; self.current_tab = CurrentTab::Transfer; for component in self.components.iter_mut() { component.set_active(self.current_tab.clone()); @@ -194,15 +204,32 @@ impl Component for Wallet { Action::SetActiveScreen(Mode::Wallet) => { self.is_active = true; self.current_tab = CurrentTab::Accounts; + self.previous_tab = CurrentTab::Accounts; + }, + Action::UpdateAccountName(_) | Action::NewAccount(_) => { + self.previous_tab = self.current_tab; + self.current_tab = CurrentTab::Accounts; + }, + Action::UpdateAddressBookRecord(_) | Action::NewAddressBookRecord(_, _) | Action::ClosePopup => { + self.previous_tab = self.current_tab; + self.current_tab = CurrentTab::AddressBook; + }, + Action::RenameAccount(_) => { + self.previous_tab = self.current_tab; + self.current_tab = CurrentTab::RenameAccount; + }, + Action::RenameAddressBookRecord(_) => { + self.previous_tab = self.current_tab; + self.current_tab = CurrentTab::RenameAddressBookRecord; + } + Action::TransferTo(_) => { + self.previous_tab = self.current_tab; + self.current_tab = CurrentTab::Transfer; + }, + Action::AccountDetailsOf(_, _) => { + self.previous_tab = self.current_tab; + self.current_tab = CurrentTab::AccountDetails; }, - Action::UpdateAccountName(_) | Action::NewAccount(_) => - self.current_tab = CurrentTab::Accounts, - Action::UpdateAddressBookRecord(_) | Action::NewAddressBookRecord(_, _) | Action::ClosePopup => - self.current_tab = CurrentTab::AddressBook, - Action::RenameAccount(_) => self.current_tab = CurrentTab::RenameAccount, - Action::RenameAddressBookRecord(_) => - self.current_tab = CurrentTab::RenameAddressBookRecord, - Action::TransferTo(_) => self.current_tab = CurrentTab::Transfer, _ => {} } for component in self.components.iter_mut() {