From 41c5a37eab1a37a299146080c3eec361a1d6c8a6 Mon Sep 17 00:00:00 2001 From: edouard Date: Mon, 17 Apr 2023 17:15:33 +0200 Subject: [PATCH] gui: separate psbts and send panels close #440 --- gui/src/app/menu.rs | 2 +- gui/src/app/mod.rs | 4 +- gui/src/app/state/mod.rs | 4 +- gui/src/app/state/psbts.rs | 197 +++++++++++++++++++++++++++ gui/src/app/state/spend/mod.rs | 197 +-------------------------- gui/src/app/view/home.rs | 1 - gui/src/app/view/mod.rs | 100 ++++++++++---- gui/src/app/view/psbts.rs | 146 ++++++++++++++++++++ gui/src/app/view/spend/mod.rs | 155 --------------------- gui/ui/src/component/badge.rs | 6 +- gui/ui/src/image.rs | 6 + gui/ui/src/theme.rs | 7 +- gui/ui/static/icons/history-icon.svg | 5 + 13 files changed, 445 insertions(+), 385 deletions(-) create mode 100644 gui/src/app/state/psbts.rs create mode 100644 gui/src/app/view/psbts.rs create mode 100644 gui/ui/static/icons/history-icon.svg diff --git a/gui/src/app/menu.rs b/gui/src/app/menu.rs index f373a132..08995e63 100644 --- a/gui/src/app/menu.rs +++ b/gui/src/app/menu.rs @@ -2,7 +2,7 @@ pub enum Menu { Home, Receive, - Spend, + PSBTs, Settings, Coins, CreateSpendTx, diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index 4e0be44f..0c7085b0 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -24,7 +24,7 @@ use liana_ui::widget::Element; pub use config::Config; pub use message::Message; -use state::{CoinsPanel, CreateSpendPanel, Home, ReceivePanel, RecoveryPanel, SpendPanel, State}; +use state::{CoinsPanel, CreateSpendPanel, Home, PsbtsPanel, ReceivePanel, RecoveryPanel, State}; use crate::{ app::{cache::Cache, error::Error, menu::Menu, wallet::Wallet}, @@ -81,7 +81,7 @@ impl App { ) .into(), menu::Menu::Receive => ReceivePanel::default().into(), - menu::Menu::Spend => SpendPanel::new(self.wallet.clone(), &self.cache.spend_txs).into(), + menu::Menu::PSBTs => PsbtsPanel::new(self.wallet.clone(), &self.cache.spend_txs).into(), menu::Menu::CreateSpendTx => CreateSpendPanel::new( self.wallet.clone(), &self.cache.coins, diff --git a/gui/src/app/state/mod.rs b/gui/src/app/state/mod.rs index 2de90431..ebbabfae 100644 --- a/gui/src/app/state/mod.rs +++ b/gui/src/app/state/mod.rs @@ -1,4 +1,5 @@ mod coins; +mod psbts; mod recovery; mod settings; mod spend; @@ -18,9 +19,10 @@ use crate::daemon::{ Daemon, }; pub use coins::CoinsPanel; +pub use psbts::PsbtsPanel; pub use recovery::RecoveryPanel; pub use settings::SettingsState; -pub use spend::{CreateSpendPanel, SpendPanel}; +pub use spend::CreateSpendPanel; pub trait State { fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message>; diff --git a/gui/src/app/state/psbts.rs b/gui/src/app/state/psbts.rs new file mode 100644 index 00000000..73f6a413 --- /dev/null +++ b/gui/src/app/state/psbts.rs @@ -0,0 +1,197 @@ +use std::sync::Arc; + +use iced::Command; + +use liana::miniscript::bitcoin::{consensus, util::psbt::Psbt}; +use liana_ui::{ + component::{form, modal}, + widget::Element, +}; + +use super::{spend::detail, State}; +use crate::{ + app::{cache::Cache, error::Error, menu::Menu, message::Message, view, wallet::Wallet}, + daemon::{model::SpendTx, Daemon}, +}; + +pub struct PsbtsPanel { + wallet: Arc, + selected_tx: Option, + spend_txs: Vec, + warning: Option, + import_tx: Option, +} + +impl PsbtsPanel { + pub fn new(wallet: Arc, spend_txs: &[SpendTx]) -> Self { + Self { + wallet, + spend_txs: spend_txs.to_vec(), + warning: None, + selected_tx: None, + import_tx: None, + } + } +} + +impl State for PsbtsPanel { + fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> { + if let Some(tx) = &self.selected_tx { + tx.view(cache) + } else { + let list_view = view::dashboard( + &Menu::PSBTs, + cache, + self.warning.as_ref(), + view::psbts::psbts_view(&self.spend_txs), + ); + if let Some(import_tx) = &self.import_tx { + modal::Modal::new(list_view, import_tx.view()) + .on_blur(if import_tx.processing { + None + } else { + Some(view::Message::Close) + }) + .into() + } else { + list_view + } + } + } + + fn update( + &mut self, + daemon: Arc, + cache: &Cache, + message: Message, + ) -> Command { + match message { + Message::SpendTxs(res) => match res { + Err(e) => self.warning = Some(e), + Ok(txs) => { + self.warning = None; + self.spend_txs = txs; + } + }, + Message::View(view::Message::ImportSpend(view::ImportSpendMessage::Import)) => { + if self.import_tx.is_none() { + self.import_tx = Some(ImportPsbtModal::new()); + } + } + Message::View(view::Message::Close) => { + if self.selected_tx.is_some() { + self.selected_tx = None; + return self.load(daemon); + } + if self.import_tx.is_some() { + self.import_tx = None; + return self.load(daemon); + } + } + Message::View(view::Message::Select(i)) => { + if let Some(tx) = self.spend_txs.get(i) { + let tx = detail::SpendTxState::new(self.wallet.clone(), tx.clone(), true); + let cmd = tx.load(daemon); + self.selected_tx = Some(tx); + return cmd; + } + } + _ => { + if let Some(tx) = &mut self.selected_tx { + return tx.update(daemon, cache, message); + } + + if let Some(import_tx) = &mut self.import_tx { + return import_tx.update(daemon, cache, message); + } + } + } + Command::none() + } + + fn load(&self, daemon: Arc) -> Command { + let daemon = daemon.clone(); + Command::perform( + async move { daemon.list_spend_transactions().map_err(|e| e.into()) }, + Message::SpendTxs, + ) + } +} + +impl From for Box { + fn from(s: PsbtsPanel) -> Box { + Box::new(s) + } +} + +pub struct ImportPsbtModal { + imported: form::Value, + processing: bool, + error: Option, + success: bool, +} + +impl ImportPsbtModal { + pub fn new() -> Self { + Self { + imported: form::Value::default(), + processing: false, + error: None, + success: false, + } + } +} + +impl ImportPsbtModal { + fn view<'a>(&self) -> Element<'a, view::Message> { + if self.success { + view::psbts::import_psbt_success_view() + } else { + view::psbts::import_psbt_view(&self.imported, self.error.as_ref(), self.processing) + } + } + + fn update( + &mut self, + daemon: Arc, + _cache: &Cache, + message: Message, + ) -> Command { + match message { + Message::Updated(res) => { + self.processing = false; + match res { + Ok(()) => { + self.success = true; + self.error = None; + } + Err(e) => self.error = e.into(), + } + } + Message::View(view::Message::ImportSpend(view::ImportSpendMessage::PsbtEdited(s))) => { + self.imported.value = s; + self.imported.valid = base64::decode(&self.imported.value) + .ok() + .and_then(|bytes| consensus::encode::deserialize::(&bytes).ok()) + .is_some(); + } + Message::View(view::Message::ImportSpend(view::ImportSpendMessage::Confirm)) => { + if self.imported.valid { + self.processing = true; + self.error = None; + let imported: Psbt = consensus::encode::deserialize( + &base64::decode(&self.imported.value).expect("Already checked"), + ) + .unwrap(); + return Command::perform( + async move { daemon.update_spend_tx(&imported).map_err(|e| e.into()) }, + Message::Updated, + ); + } + } + _ => {} + } + + Command::none() + } +} diff --git a/gui/src/app/state/spend/mod.rs b/gui/src/app/state/spend/mod.rs index 3204ecb5..cffb9865 100644 --- a/gui/src/app/state/spend/mod.rs +++ b/gui/src/app/state/spend/mod.rs @@ -4,131 +4,14 @@ use std::sync::Arc; use iced::Command; -use liana::miniscript::bitcoin::{consensus, util::psbt::Psbt}; -use liana_ui::{ - component::{form, modal}, - widget::Element, -}; +use liana_ui::widget::Element; use super::{redirect, State}; use crate::{ - app::{cache::Cache, error::Error, menu::Menu, message::Message, view, wallet::Wallet}, - daemon::{ - model::{Coin, SpendTx}, - Daemon, - }, + app::{cache::Cache, menu::Menu, message::Message, view, wallet::Wallet}, + daemon::{model::Coin, Daemon}, }; -pub struct SpendPanel { - wallet: Arc, - selected_tx: Option, - spend_txs: Vec, - warning: Option, - import_tx: Option, -} - -impl SpendPanel { - pub fn new(wallet: Arc, spend_txs: &[SpendTx]) -> Self { - Self { - wallet, - spend_txs: spend_txs.to_vec(), - warning: None, - selected_tx: None, - import_tx: None, - } - } -} - -impl State for SpendPanel { - fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> { - if let Some(tx) = &self.selected_tx { - tx.view(cache) - } else { - let list_view = view::dashboard( - &Menu::Spend, - cache, - self.warning.as_ref(), - view::spend::spend_view(&self.spend_txs), - ); - if let Some(import_tx) = &self.import_tx { - modal::Modal::new(list_view, import_tx.view()) - .on_blur(if import_tx.processing { - None - } else { - Some(view::Message::Close) - }) - .into() - } else { - list_view - } - } - } - - fn update( - &mut self, - daemon: Arc, - cache: &Cache, - message: Message, - ) -> Command { - match message { - Message::SpendTxs(res) => match res { - Err(e) => self.warning = Some(e), - Ok(txs) => { - self.warning = None; - self.spend_txs = txs; - } - }, - Message::View(view::Message::ImportSpend(view::ImportSpendMessage::Import)) => { - if self.import_tx.is_none() { - self.import_tx = Some(ImportSpendState::new()); - } - } - Message::View(view::Message::Close) => { - if self.selected_tx.is_some() { - self.selected_tx = None; - return self.load(daemon); - } - if self.import_tx.is_some() { - self.import_tx = None; - return self.load(daemon); - } - } - Message::View(view::Message::Select(i)) => { - if let Some(tx) = self.spend_txs.get(i) { - let tx = detail::SpendTxState::new(self.wallet.clone(), tx.clone(), true); - let cmd = tx.load(daemon); - self.selected_tx = Some(tx); - return cmd; - } - } - _ => { - if let Some(tx) = &mut self.selected_tx { - return tx.update(daemon, cache, message); - } - - if let Some(import_tx) = &mut self.import_tx { - return import_tx.update(daemon, cache, message); - } - } - } - Command::none() - } - - fn load(&self, daemon: Arc) -> Command { - let daemon = daemon.clone(); - Command::perform( - async move { daemon.list_spend_transactions().map_err(|e| e.into()) }, - Message::SpendTxs, - ) - } -} - -impl From for Box { - fn from(s: SpendPanel) -> Box { - Box::new(s) - } -} - pub struct CreateSpendPanel { draft: step::TransactionDraft, current: usize, @@ -168,7 +51,7 @@ impl State for CreateSpendPanel { message: Message, ) -> Command { if matches!(message, Message::View(view::Message::Close)) { - return redirect(Menu::Spend); + return redirect(Menu::PSBTs); } if matches!(message, Message::View(view::Message::Next)) { @@ -214,75 +97,3 @@ impl From for Box { Box::new(s) } } - -pub struct ImportSpendState { - imported: form::Value, - processing: bool, - error: Option, - success: bool, -} - -impl ImportSpendState { - pub fn new() -> Self { - Self { - imported: form::Value::default(), - processing: false, - error: None, - success: false, - } - } -} - -impl ImportSpendState { - fn view<'a>(&self) -> Element<'a, view::Message> { - if self.success { - view::spend::import_spend_success_view() - } else { - view::spend::import_spend_view(&self.imported, self.error.as_ref(), self.processing) - } - } - - fn update( - &mut self, - daemon: Arc, - _cache: &Cache, - message: Message, - ) -> Command { - match message { - Message::Updated(res) => { - self.processing = false; - match res { - Ok(()) => { - self.success = true; - self.error = None; - } - Err(e) => self.error = e.into(), - } - } - Message::View(view::Message::ImportSpend(view::ImportSpendMessage::PsbtEdited(s))) => { - self.imported.value = s; - self.imported.valid = base64::decode(&self.imported.value) - .ok() - .and_then(|bytes| consensus::encode::deserialize::(&bytes).ok()) - .is_some(); - } - Message::View(view::Message::ImportSpend(view::ImportSpendMessage::Confirm)) => { - if self.imported.valid { - self.processing = true; - self.error = None; - let imported: Psbt = consensus::encode::deserialize( - &base64::decode(&self.imported.value).expect("Already checked"), - ) - .unwrap(); - return Command::perform( - async move { daemon.update_spend_tx(&imported).map_err(|e| e.into()) }, - Message::Updated, - ); - } - } - _ => {} - } - - Command::none() - } -} diff --git a/gui/src/app/view/home.rs b/gui/src/app/view/home.rs index 35ad9ade..c546bb99 100644 --- a/gui/src/app/view/home.rs +++ b/gui/src/app/view/home.rs @@ -29,7 +29,6 @@ pub fn home_view<'a>( events: &Vec, ) -> Element<'a, Message> { Column::new() - .push(Column::new().padding(40)) .push(amount_with_size(balance, 50)) .push_maybe(recovery_warning.map(|(a, c)| { Row::new() diff --git a/gui/src/app/view/mod.rs b/gui/src/app/view/mod.rs index 0e13cca0..19d92876 100644 --- a/gui/src/app/view/mod.rs +++ b/gui/src/app/view/mod.rs @@ -5,6 +5,7 @@ mod warning; pub mod coins; pub mod home; pub mod hw; +pub mod psbts; pub mod receive; pub mod recovery; pub mod settings; @@ -13,11 +14,15 @@ pub mod spend; pub use message::*; use warning::warn; -use iced::{widget::scrollable, Length}; +use iced::{ + widget::{column, row, scrollable, Space}, + Length, +}; use liana_ui::{ component::{button, text::*}, icon::{coin_icon, cross_icon, home_icon, receive_icon, send_icon, settings_icon}, + image::*, theme, util::Collection, widget::*, @@ -29,11 +34,11 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { let home_button = if *menu == Menu::Home { button::menu_active(Some(home_icon()), "Home") .on_press(Message::Reload) - .width(iced::Length::Units(200)) + .width(iced::Length::Fill) } else { button::menu(Some(home_icon()), "Home") .on_press(Message::Menu(Menu::Home)) - .width(iced::Length::Units(200)) + .width(iced::Length::Fill) }; let coins_button = if *menu == Menu::Coins { @@ -74,7 +79,7 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { ) .style(theme::Button::Menu(true)) .on_press(Message::Reload) - .width(iced::Length::Units(200)) + .width(iced::Length::Fill) } else { Button::new( Container::new( @@ -113,17 +118,17 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { ) .style(theme::Button::Menu(false)) .on_press(Message::Menu(Menu::Coins)) - .width(iced::Length::Units(200)) + .width(iced::Length::Fill) }; - let spend_button = if *menu == Menu::Spend { + let psbt_button = if *menu == Menu::PSBTs { Button::new( Container::new( Row::new() .push( Row::new() - .push(send_icon()) - .push(text("Send")) + .push(history_icon().width(Length::Units(20))) + .push(text("PSBTs")) .spacing(10) .width(iced::Length::Fill) .align_items(iced::Alignment::Center), @@ -150,15 +155,15 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { ) .style(theme::Button::Menu(true)) .on_press(Message::Reload) - .width(iced::Length::Units(200)) + .width(iced::Length::Fill) } else { Button::new( Container::new( Row::new() .push( Row::new() - .push(send_icon()) - .push(text("Send")) + .push(history_icon().width(Length::Units(20))) + .push(text("PSBTs")) .spacing(10) .width(iced::Length::Fill) .align_items(iced::Alignment::Center), @@ -184,47 +189,83 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { .center_x(), ) .style(theme::Button::Menu(false)) - .on_press(Message::Menu(Menu::Spend)) - .width(iced::Length::Units(200)) + .on_press(Message::Menu(Menu::PSBTs)) + .width(iced::Length::Fill) + }; + + let spend_button = if *menu == Menu::CreateSpendTx { + Button::new( + Container::new( + Row::new() + .push(send_icon()) + .push(text("Send")) + .spacing(10) + .width(iced::Length::Fill) + .align_items(iced::Alignment::Center), + ) + .width(iced::Length::Fill) + .padding(10) + .center_x(), + ) + .style(theme::Button::Menu(true)) + .on_press(Message::Reload) + .width(iced::Length::Fill) + } else { + Button::new( + Container::new( + Row::new() + .push(send_icon()) + .push(text("Send")) + .spacing(10) + .width(iced::Length::Fill) + .align_items(iced::Alignment::Center), + ) + .width(iced::Length::Fill) + .padding(10) + .center_x(), + ) + .style(theme::Button::Menu(false)) + .on_press(Message::Menu(Menu::CreateSpendTx)) + .width(iced::Length::Fill) }; let receive_button = if *menu == Menu::Receive { button::menu_active(Some(receive_icon()), "Receive") .on_press(Message::Reload) - .width(iced::Length::Units(200)) + .width(iced::Length::Fill) } else { button::menu(Some(receive_icon()), "Receive") .on_press(Message::Menu(Menu::Receive)) - .width(iced::Length::Units(200)) + .width(iced::Length::Fill) }; let settings_button = if *menu == Menu::Settings { button::menu_active(Some(settings_icon()), "Settings") .on_press(Message::Menu(Menu::Settings)) - .width(iced::Length::Units(200)) + .width(iced::Length::Fill) } else { button::menu(Some(settings_icon()), "Settings") .on_press(Message::Menu(Menu::Settings)) - .width(iced::Length::Units(200)) + .width(iced::Length::Fill) }; Container::new( Column::new() - .padding(10) .push( Column::new() .push( Container::new( - liana_ui::image::liana_grey_logo() + liana_grey_logo() .height(Length::Units(150)) .width(Length::Units(60)), ) .padding(15), ) .push(home_button) - .push(coins_button) .push(spend_button) .push(receive_button) + .push(coins_button) + .push(psbt_button) .spacing(15) .height(Length::Fill), ) @@ -254,18 +295,25 @@ pub fn dashboard<'a, T: Into>>( Row::new() .push( sidebar(menu, cache) - .width(Length::Shrink) + .width(Length::FillPortion(2)) .height(Length::Fill), ) .push( Column::new() .push(warn(warning)) - .push(main_section(Container::new(scrollable( - Container::new(content).padding(20), - )))), + .push( + main_section(Container::new(scrollable(row!( + Space::with_width(Length::FillPortion(1)), + column!(Space::with_height(Length::Units(150)), content.into()) + .width(Length::FillPortion(8)), + Space::with_width(Length::FillPortion(1)), + )))) + .width(Length::Fill), + ) + .width(Length::FillPortion(10)), ) - .width(iced::Length::Fill) - .height(iced::Length::Fill) + .width(Length::Fill) + .height(Length::Fill) .into() } diff --git a/gui/src/app/view/psbts.rs b/gui/src/app/view/psbts.rs new file mode 100644 index 00000000..53cc6cac --- /dev/null +++ b/gui/src/app/view/psbts.rs @@ -0,0 +1,146 @@ +use iced::{widget::Space, Alignment, Length}; + +use liana_ui::{ + color, + component::{badge, button, card, form, text::*}, + icon, theme, + util::Collection, + widget::*, +}; + +use crate::{ + app::{error::Error, menu::Menu, view::util::*}, + daemon::model::{SpendStatus, SpendTx}, +}; + +use super::{message::*, warning::warn}; + +pub fn import_psbt_view<'a>( + imported: &form::Value, + error: Option<&Error>, + processing: bool, +) -> Element<'a, Message> { + Column::new() + .push(warn(error)) + .push(card::simple( + Column::new() + .spacing(10) + .push(text("Insert PSBT:").bold()) + .push( + form::Form::new("PSBT", imported, move |msg| { + Message::ImportSpend(ImportSpendMessage::PsbtEdited(msg)) + }) + .warning("Please enter a base64 encoded PSBT") + .size(20) + .padding(10), + ) + .push(Row::new().push(Space::with_width(Length::Fill)).push( + if imported.valid && !imported.value.is_empty() && !processing { + button::primary(None, "Import") + .on_press(Message::ImportSpend(ImportSpendMessage::Confirm)) + } else if processing { + button::primary(None, "Processing...") + } else { + button::primary(None, "Import") + }, + )), + )) + .max_width(400) + .into() +} + +pub fn import_psbt_success_view<'a>() -> Element<'a, Message> { + Column::new() + .push( + card::simple(Container::new(text("PSBT is imported").style(color::GREEN))).padding(50), + ) + .width(Length::Units(400)) + .align_items(Alignment::Center) + .into() +} + +pub fn psbts_view<'a>(spend_txs: &[SpendTx]) -> Element<'a, Message> { + Column::new() + .push( + Row::new() + .spacing(10) + .push(Container::new(h3("PSBTs")).width(Length::Fill)) + .push( + button::secondary(Some(icon::import_icon()), "Import") + .on_press(Message::ImportSpend(ImportSpendMessage::Import)), + ) + .push( + button::primary(Some(icon::plus_icon()), "New") + .on_press(Message::Menu(Menu::CreateSpendTx)), + ), + ) + .push( + Column::new().spacing(10).push( + spend_txs + .iter() + .enumerate() + .fold(Column::new().spacing(10), |col, (i, tx)| { + col.push(spend_tx_list_view(i, tx)) + }), + ), + ) + .align_items(Alignment::Center) + .spacing(20) + .into() +} + +fn spend_tx_list_view<'a>(i: usize, tx: &SpendTx) -> Element<'a, Message> { + Container::new( + Button::new( + Row::new() + .push( + Row::new() + .push(badge::spend()) + .push(if !tx.sigs.recovery_paths().is_empty() { + Row::new().push( + Container::new(text(" Recovery ").small()) + .padding(3) + .style(theme::Container::Pill(theme::Pill::Simple)), + ) + } else { + let sigs = tx.sigs.primary_path(); + Row::new() + .spacing(5) + .align_items(Alignment::Center) + .push(text(format!( + "{}/{}", + if sigs.sigs_count <= sigs.threshold { + sigs.sigs_count + } else { + sigs.threshold + }, + sigs.threshold + ))) + .push(icon::key_icon()) + }) + .spacing(10) + .align_items(Alignment::Center) + .width(Length::Fill), + ) + .push_maybe(match tx.status { + SpendStatus::Deprecated => Some(badge::deprecated()), + SpendStatus::Broadcast => Some(badge::unconfirmed()), + SpendStatus::Spent => Some(badge::spent()), + _ => None, + }) + .push( + Column::new() + .push(amount(&tx.spend_amount)) + .push(text(format!("fee: {:8}", tx.fee_amount.to_btc())).small()) + .width(Length::Shrink), + ) + .align_items(Alignment::Center) + .spacing(20), + ) + .padding(10) + .on_press(Message::Select(i)) + .style(theme::Button::TransparentBorder), + ) + .style(theme::Container::Card(theme::Card::Simple)) + .into() +} diff --git a/gui/src/app/view/spend/mod.rs b/gui/src/app/view/spend/mod.rs index 9e08a39c..0536ceab 100644 --- a/gui/src/app/view/spend/mod.rs +++ b/gui/src/app/view/spend/mod.rs @@ -1,157 +1,2 @@ pub mod detail; pub mod step; - -use iced::{widget::Space, Alignment, Length}; - -use liana_ui::{ - color, - component::{badge, button, card, form, text::*}, - icon, theme, - util::Collection, - widget::*, -}; - -use crate::{ - app::{error::Error, menu::Menu, view::util::*}, - daemon::model::{SpendStatus, SpendTx}, -}; - -use super::{message::*, warning::warn}; - -pub fn import_spend_view<'a>( - imported: &form::Value, - error: Option<&Error>, - processing: bool, -) -> Element<'a, Message> { - Column::new() - .push(warn(error)) - .push(card::simple( - Column::new() - .spacing(10) - .push(text("Insert PSBT:").bold()) - .push( - form::Form::new("PSBT", imported, move |msg| { - Message::ImportSpend(ImportSpendMessage::PsbtEdited(msg)) - }) - .warning("Please enter a base64 encoded PSBT") - .size(20) - .padding(10), - ) - .push(Row::new().push(Space::with_width(Length::Fill)).push( - if imported.valid && !imported.value.is_empty() && !processing { - button::primary(None, "Import") - .on_press(Message::ImportSpend(ImportSpendMessage::Confirm)) - } else if processing { - button::primary(None, "Processing...") - } else { - button::primary(None, "Import") - }, - )), - )) - .max_width(400) - .into() -} - -pub fn import_spend_success_view<'a>() -> Element<'a, Message> { - Column::new() - .push( - card::simple(Container::new(text("PSBT is imported").style(color::GREEN))).padding(50), - ) - .width(Length::Units(400)) - .align_items(Alignment::Center) - .into() -} - -pub fn spend_view<'a>(spend_txs: &[SpendTx]) -> Element<'a, Message> { - Column::new() - .push( - Row::new() - .spacing(10) - .push(Column::new().width(Length::Fill)) - .push( - button::secondary(Some(icon::import_icon()), "Import") - .on_press(Message::ImportSpend(ImportSpendMessage::Import)), - ) - .push( - button::primary(Some(icon::plus_icon()), "New") - .on_press(Message::Menu(Menu::CreateSpendTx)), - ), - ) - .push( - Container::new( - Row::new() - .push(text(format!(" {}", spend_txs.len())).bold()) - .push(text(" draft transactions")), - ) - .width(Length::Fill), - ) - .push( - Column::new().spacing(10).push( - spend_txs - .iter() - .enumerate() - .fold(Column::new().spacing(10), |col, (i, tx)| { - col.push(spend_tx_list_view(i, tx)) - }), - ), - ) - .align_items(Alignment::Center) - .spacing(20) - .into() -} - -fn spend_tx_list_view<'a>(i: usize, tx: &SpendTx) -> Element<'a, Message> { - Container::new( - Button::new( - Row::new() - .push( - Row::new() - .push(badge::spend()) - .push(if !tx.sigs.recovery_paths().is_empty() { - Row::new().push( - Container::new(text(" Recovery ").small()) - .padding(3) - .style(theme::Container::Pill(theme::Pill::Simple)), - ) - } else { - let sigs = tx.sigs.primary_path(); - Row::new() - .spacing(5) - .align_items(Alignment::Center) - .push(text(format!( - "{}/{}", - if sigs.sigs_count <= sigs.threshold { - sigs.sigs_count - } else { - sigs.threshold - }, - sigs.threshold - ))) - .push(icon::key_icon()) - }) - .spacing(10) - .align_items(Alignment::Center) - .width(Length::Fill), - ) - .push_maybe(match tx.status { - SpendStatus::Deprecated => Some(badge::deprecated()), - SpendStatus::Broadcast => Some(badge::unconfirmed()), - SpendStatus::Spent => Some(badge::spent()), - _ => None, - }) - .push( - Column::new() - .push(amount(&tx.spend_amount)) - .push(text(format!("fee: {:8}", tx.fee_amount.to_btc())).small()) - .width(Length::Shrink), - ) - .align_items(Alignment::Center) - .spacing(20), - ) - .padding(10) - .on_press(Message::Select(i)) - .style(theme::Button::TransparentBorder), - ) - .style(theme::Container::Card(theme::Card::Simple)) - .into() -} diff --git a/gui/ui/src/component/badge.rs b/gui/ui/src/component/badge.rs index 47360bc7..afa653f9 100644 --- a/gui/ui/src/component/badge.rs +++ b/gui/ui/src/component/badge.rs @@ -64,7 +64,7 @@ pub fn coin() -> Container<'static, T> { pub fn unconfirmed<'a, T: 'a>() -> Container<'a, T> { Container::new( tooltip::Tooltip::new( - Container::new(text::caption(" Unconfirmed ")) + Container::new(text::p2_regular(" Unconfirmed ")) .padding(3) .style(theme::Container::Pill(theme::Pill::Simple)), "Do not treat this as a payment until it is confirmed", @@ -77,7 +77,7 @@ pub fn unconfirmed<'a, T: 'a>() -> Container<'a, T> { pub fn deprecated<'a, T: 'a>() -> Container<'a, T> { Container::new( tooltip::Tooltip::new( - Container::new(text::caption(" Deprecated ")) + Container::new(text::p2_regular(" Deprecated ")) .padding(3) .style(theme::Container::Pill(theme::Pill::Simple)), "This spend cannot be included anymore in the blockchain", @@ -90,7 +90,7 @@ pub fn deprecated<'a, T: 'a>() -> Container<'a, T> { pub fn spent<'a, T: 'a>() -> Container<'a, T> { Container::new( tooltip::Tooltip::new( - Container::new(text::caption(" Spent ")) + Container::new(text::p2_regular(" Spent ")) .padding(3) .style(theme::Container::Pill(theme::Pill::Simple)), "The spend transaction was included in the blockchain", diff --git a/gui/ui/src/image.rs b/gui/ui/src/image.rs index 55783f6a..dde177a3 100644 --- a/gui/ui/src/image.rs +++ b/gui/ui/src/image.rs @@ -12,3 +12,9 @@ pub fn liana_grey_logo() -> Svg { let h = Handle::from_memory(LIANA_LOGO_GREY.to_vec()); Svg::new(h) } + +const HISTORY_ICON: &[u8] = include_bytes!("../static/icons/history-icon.svg"); +pub fn history_icon() -> Svg { + let h = Handle::from_memory(HISTORY_ICON.to_vec()); + Svg::new(h) +} diff --git a/gui/ui/src/theme.rs b/gui/ui/src/theme.rs index 6ca688a0..6dce8354 100644 --- a/gui/ui/src/theme.rs +++ b/gui/ui/src/theme.rs @@ -306,10 +306,11 @@ impl Pill { ..container::Appearance::default() }, Self::Simple => container::Appearance { - background: color::GREEN.into(), + background: iced::Color::TRANSPARENT.into(), border_radius: 25.0, - text_color: color::LIGHT_BLACK.into(), - ..container::Appearance::default() + border_width: 1.0, + border_color: color::GREY_3, + text_color: color::GREY_3.into(), }, } } diff --git a/gui/ui/static/icons/history-icon.svg b/gui/ui/static/icons/history-icon.svg new file mode 100644 index 00000000..9987658e --- /dev/null +++ b/gui/ui/static/icons/history-icon.svg @@ -0,0 +1,5 @@ + + + + +