Merge #452: Gui new psbts panel
41c5a37eab1a37a299146080c3eec361a1d6c8a6 gui: separate psbts and send panels (edouard) Pull request description: based on #447 ACKs for top commit: edouardparis: Self-ACK 41c5a37eab1a37a299146080c3eec361a1d6c8a6 Tree-SHA512: 0cb5aaba2f658696d00b06619796362fe694686d152e7fd76d79ebb83d7de327c4f349d294f97d82cb229c837e76185ee47e00b06e5e206ef7ad37769d697e49
This commit is contained in:
commit
5d028b03f7
@ -2,7 +2,7 @@
|
||||
pub enum Menu {
|
||||
Home,
|
||||
Receive,
|
||||
Spend,
|
||||
PSBTs,
|
||||
Settings,
|
||||
Coins,
|
||||
CreateSpendTx,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>;
|
||||
|
||||
197
gui/src/app/state/psbts.rs
Normal file
197
gui/src/app/state/psbts.rs
Normal file
@ -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<Wallet>,
|
||||
selected_tx: Option<detail::SpendTxState>,
|
||||
spend_txs: Vec<SpendTx>,
|
||||
warning: Option<Error>,
|
||||
import_tx: Option<ImportPsbtModal>,
|
||||
}
|
||||
|
||||
impl PsbtsPanel {
|
||||
pub fn new(wallet: Arc<Wallet>, 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<dyn Daemon + Sync + Send>,
|
||||
cache: &Cache,
|
||||
message: Message,
|
||||
) -> Command<Message> {
|
||||
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<dyn Daemon + Sync + Send>) -> Command<Message> {
|
||||
let daemon = daemon.clone();
|
||||
Command::perform(
|
||||
async move { daemon.list_spend_transactions().map_err(|e| e.into()) },
|
||||
Message::SpendTxs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PsbtsPanel> for Box<dyn State> {
|
||||
fn from(s: PsbtsPanel) -> Box<dyn State> {
|
||||
Box::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImportPsbtModal {
|
||||
imported: form::Value<String>,
|
||||
processing: bool,
|
||||
error: Option<Error>,
|
||||
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<dyn Daemon + Sync + Send>,
|
||||
_cache: &Cache,
|
||||
message: Message,
|
||||
) -> Command<Message> {
|
||||
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::<Psbt>(&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()
|
||||
}
|
||||
}
|
||||
@ -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<Wallet>,
|
||||
selected_tx: Option<detail::SpendTxState>,
|
||||
spend_txs: Vec<SpendTx>,
|
||||
warning: Option<Error>,
|
||||
import_tx: Option<ImportSpendState>,
|
||||
}
|
||||
|
||||
impl SpendPanel {
|
||||
pub fn new(wallet: Arc<Wallet>, 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<dyn Daemon + Sync + Send>,
|
||||
cache: &Cache,
|
||||
message: Message,
|
||||
) -> Command<Message> {
|
||||
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<dyn Daemon + Sync + Send>) -> Command<Message> {
|
||||
let daemon = daemon.clone();
|
||||
Command::perform(
|
||||
async move { daemon.list_spend_transactions().map_err(|e| e.into()) },
|
||||
Message::SpendTxs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SpendPanel> for Box<dyn State> {
|
||||
fn from(s: SpendPanel) -> Box<dyn State> {
|
||||
Box::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateSpendPanel {
|
||||
draft: step::TransactionDraft,
|
||||
current: usize,
|
||||
@ -168,7 +51,7 @@ impl State for CreateSpendPanel {
|
||||
message: Message,
|
||||
) -> Command<Message> {
|
||||
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<CreateSpendPanel> for Box<dyn State> {
|
||||
Box::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImportSpendState {
|
||||
imported: form::Value<String>,
|
||||
processing: bool,
|
||||
error: Option<Error>,
|
||||
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<dyn Daemon + Sync + Send>,
|
||||
_cache: &Cache,
|
||||
message: Message,
|
||||
) -> Command<Message> {
|
||||
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::<Psbt>(&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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,6 @@ pub fn home_view<'a>(
|
||||
events: &Vec<HistoryTransaction>,
|
||||
) -> 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()
|
||||
|
||||
@ -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<Element<'a, Message>>>(
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
146
gui/src/app/view/psbts.rs
Normal file
146
gui/src/app/view/psbts.rs
Normal file
@ -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<String>,
|
||||
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()
|
||||
}
|
||||
@ -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<String>,
|
||||
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()
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ pub fn coin<T>() -> 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",
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
5
gui/ui/static/icons/history-icon.svg
Normal file
5
gui/ui/static/icons/history-icon.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 6L4 6" stroke="#CDCDCD" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M20 12L4 12" stroke="#CDCDCD" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M20 18H4" stroke="#CDCDCD" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 347 B |
Loading…
x
Reference in New Issue
Block a user