Merge #454: Gui new transactions panel
49be063ccd7629da19910db57a19061e8b19fb7e gui: separate accounting events in the home panel (edouard) e7e8028d8a2d49dde176f2ed3a32ec078ea4fabe ui: add history events to design-system (edouard) 196b8cc3e91fcbbe9f17a7e0945010dc38dae4fb gui: new transactions panel (edouard) Pull request description: close #437 close #442 ACKs for top commit: edouardparis: Self-ACK 49be063ccd7629da19910db57a19061e8b19fb7e Tree-SHA512: 9538fe9f08e6d43b172c74009f40d5858817313f3f4d1dae691aaccc1e1c0cbbe149fe649663a21b5e2797c5cda5d4df6a3fe2bd6f832076db40c000b94cb187
This commit is contained in:
commit
6582706f83
2
gui/Cargo.lock
generated
2
gui/Cargo.lock
generated
@ -1887,6 +1887,8 @@ dependencies = [
|
||||
name = "liana_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"chrono",
|
||||
"iced",
|
||||
"iced_lazy",
|
||||
"iced_native",
|
||||
|
||||
@ -3,6 +3,7 @@ pub enum Menu {
|
||||
Home,
|
||||
Receive,
|
||||
PSBTs,
|
||||
Transactions,
|
||||
Settings,
|
||||
Coins,
|
||||
CreateSpendTx,
|
||||
|
||||
@ -24,7 +24,10 @@ use liana_ui::widget::Element;
|
||||
pub use config::Config;
|
||||
pub use message::Message;
|
||||
|
||||
use state::{CoinsPanel, CreateSpendPanel, Home, PsbtsPanel, ReceivePanel, RecoveryPanel, State};
|
||||
use state::{
|
||||
CoinsPanel, CreateSpendPanel, Home, PsbtsPanel, ReceivePanel, RecoveryPanel, State,
|
||||
TransactionsPanel,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::{cache::Cache, error::Error, menu::Menu, wallet::Wallet},
|
||||
@ -81,6 +84,7 @@ impl App {
|
||||
)
|
||||
.into(),
|
||||
menu::Menu::Receive => ReceivePanel::default().into(),
|
||||
menu::Menu::Transactions => TransactionsPanel::new().into(),
|
||||
menu::Menu::PSBTs => PsbtsPanel::new(self.wallet.clone(), &self.cache.spend_txs).into(),
|
||||
menu::Menu::CreateSpendTx => CreateSpendPanel::new(
|
||||
self.wallet.clone(),
|
||||
|
||||
@ -3,6 +3,7 @@ mod psbts;
|
||||
mod recovery;
|
||||
mod settings;
|
||||
mod spend;
|
||||
mod transactions;
|
||||
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
@ -23,6 +24,7 @@ pub use psbts::PsbtsPanel;
|
||||
pub use recovery::RecoveryPanel;
|
||||
pub use settings::SettingsState;
|
||||
pub use spend::CreateSpendPanel;
|
||||
pub use transactions::TransactionsPanel;
|
||||
|
||||
pub trait State {
|
||||
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message>;
|
||||
@ -91,7 +93,7 @@ impl State for Home {
|
||||
return view::modal(
|
||||
false,
|
||||
self.warning.as_ref(),
|
||||
view::home::event_view(cache, event),
|
||||
view::transactions::tx_view(cache, event),
|
||||
None::<Element<view::Message>>,
|
||||
);
|
||||
}
|
||||
|
||||
171
gui/src/app/state/transactions.rs
Normal file
171
gui/src/app/state/transactions.rs
Normal file
@ -0,0 +1,171 @@
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use iced::Command;
|
||||
use liana_ui::widget::*;
|
||||
|
||||
use crate::app::{cache::Cache, error::Error, menu::Menu, message::Message, view, State};
|
||||
|
||||
use crate::daemon::{model::HistoryTransaction, Daemon};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TransactionsPanel {
|
||||
pending_txs: Vec<HistoryTransaction>,
|
||||
txs: Vec<HistoryTransaction>,
|
||||
selected_tx: Option<usize>,
|
||||
warning: Option<Error>,
|
||||
}
|
||||
|
||||
impl TransactionsPanel {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
selected_tx: None,
|
||||
txs: Vec::new(),
|
||||
pending_txs: Vec::new(),
|
||||
warning: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State for TransactionsPanel {
|
||||
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
|
||||
if let Some(i) = self.selected_tx {
|
||||
let tx = if i < self.pending_txs.len() {
|
||||
&self.pending_txs[i]
|
||||
} else {
|
||||
&self.txs[i - self.pending_txs.len()]
|
||||
};
|
||||
return view::modal(
|
||||
false,
|
||||
self.warning.as_ref(),
|
||||
view::transactions::tx_view(cache, tx),
|
||||
None::<Element<view::Message>>,
|
||||
);
|
||||
}
|
||||
view::dashboard(
|
||||
&Menu::Transactions,
|
||||
cache,
|
||||
None,
|
||||
view::transactions::transactions_view(&self.pending_txs, &self.txs),
|
||||
)
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
_cache: &Cache,
|
||||
message: Message,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::HistoryTransactions(res) => match res {
|
||||
Err(e) => self.warning = Some(e),
|
||||
Ok(txs) => {
|
||||
self.warning = None;
|
||||
for tx in txs {
|
||||
if !self.txs.iter().any(|other| other.tx == tx.tx) {
|
||||
self.txs.push(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Message::PendingTransactions(res) => match res {
|
||||
Err(e) => self.warning = Some(e),
|
||||
Ok(txs) => {
|
||||
self.warning = None;
|
||||
for tx in txs {
|
||||
if !self.pending_txs.iter().any(|other| other.tx == tx.tx) {
|
||||
self.pending_txs.push(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Message::View(view::Message::Close) => {
|
||||
self.selected_tx = None;
|
||||
}
|
||||
Message::View(view::Message::Select(i)) => {
|
||||
self.selected_tx = Some(i);
|
||||
}
|
||||
Message::View(view::Message::Next) => {
|
||||
if let Some(last) = self.txs.last() {
|
||||
let daemon = daemon.clone();
|
||||
let last_tx_date = last.time.unwrap();
|
||||
return Command::perform(
|
||||
async move {
|
||||
let mut limit = view::home::HISTORY_EVENT_PAGE_SIZE;
|
||||
let mut txs = daemon.list_history_txs(0_u32, last_tx_date, limit)?;
|
||||
|
||||
// because gethistory cursor is inclusive and use blocktime
|
||||
// multiple txs can occur in the same block.
|
||||
// If there is more tx in the same block that the
|
||||
// HISTORY_EVENT_PAGE_SIZE they can not be retrieved by changing
|
||||
// the cursor value (blocktime) but by increasing the limit.
|
||||
//
|
||||
// 1. Check if the txs retrieved have all the same blocktime
|
||||
let blocktime = if let Some(tx) = txs.first() {
|
||||
tx.time
|
||||
} else {
|
||||
return Ok(txs);
|
||||
};
|
||||
|
||||
// 2. Retrieve a larger batch of tx with the same cursor but
|
||||
// a larger limit.
|
||||
while !txs.iter().any(|evt| evt.time != blocktime)
|
||||
&& txs.len() as u64 == limit
|
||||
{
|
||||
// increments of the equivalent of one page more.
|
||||
limit += view::home::HISTORY_EVENT_PAGE_SIZE;
|
||||
txs = daemon.list_history_txs(0, last_tx_date, limit)?;
|
||||
}
|
||||
Ok(txs)
|
||||
},
|
||||
Message::HistoryTransactions,
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
|
||||
let daemon1 = daemon.clone();
|
||||
let daemon2 = daemon.clone();
|
||||
let daemon3 = daemon.clone();
|
||||
let now: u32 = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
Command::batch(vec![
|
||||
Command::perform(
|
||||
async move { daemon3.list_pending_txs().map_err(|e| e.into()) },
|
||||
Message::PendingTransactions,
|
||||
),
|
||||
Command::perform(
|
||||
async move {
|
||||
daemon1
|
||||
.list_history_txs(0, now, view::home::HISTORY_EVENT_PAGE_SIZE)
|
||||
.map_err(|e| e.into())
|
||||
},
|
||||
Message::HistoryTransactions,
|
||||
),
|
||||
Command::perform(
|
||||
async move {
|
||||
daemon2
|
||||
.list_coins()
|
||||
.map(|res| res.coins)
|
||||
.map_err(|e| e.into())
|
||||
},
|
||||
Message::Coins,
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TransactionsPanel> for Box<dyn State> {
|
||||
fn from(s: TransactionsPanel) -> Box<dyn State> {
|
||||
Box::new(s)
|
||||
}
|
||||
}
|
||||
@ -2,17 +2,14 @@ use iced::{Alignment, Length};
|
||||
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{badge, separation, text::*},
|
||||
component::{amount::*, badge, separation, text::*},
|
||||
icon, theme,
|
||||
util::Collection,
|
||||
widget::*,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
cache::Cache,
|
||||
view::{message::Message, util::*},
|
||||
},
|
||||
app::{cache::Cache, view::message::Message},
|
||||
daemon::model::{remaining_sequence, Coin},
|
||||
};
|
||||
|
||||
|
||||
@ -5,19 +5,13 @@ use iced::{alignment, Alignment, Length};
|
||||
use liana::miniscript::bitcoin;
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{badge, card, text::*},
|
||||
component::{amount::*, event, text::*},
|
||||
icon, theme,
|
||||
util::Collection,
|
||||
widget::*,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
cache::Cache,
|
||||
view::{message::Message, util::*},
|
||||
},
|
||||
daemon::model::HistoryTransaction,
|
||||
};
|
||||
use crate::{app::view::message::Message, daemon::model::HistoryTransaction};
|
||||
|
||||
pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20;
|
||||
|
||||
@ -29,7 +23,8 @@ pub fn home_view<'a>(
|
||||
events: &Vec<HistoryTransaction>,
|
||||
) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.push(amount_with_size(balance, 50))
|
||||
.push(h3("Balance"))
|
||||
.push(amount_with_size(balance, H1_SIZE))
|
||||
.push_maybe(recovery_warning.map(|(a, c)| {
|
||||
Row::new()
|
||||
.spacing(15)
|
||||
@ -66,6 +61,7 @@ pub fn home_view<'a>(
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.push(h4_bold("Last payments"))
|
||||
.push(
|
||||
pending_events
|
||||
.iter()
|
||||
@ -77,6 +73,7 @@ pub fn home_view<'a>(
|
||||
.push(
|
||||
events
|
||||
.iter()
|
||||
.filter(|event| !event.is_self_send())
|
||||
.enumerate()
|
||||
.fold(Column::new().spacing(10), |col, (i, event)| {
|
||||
col.push(event_list_view(i + pending_events.len(), event))
|
||||
@ -104,128 +101,43 @@ pub fn home_view<'a>(
|
||||
},
|
||||
),
|
||||
)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn event_list_view<'a>(i: usize, event: &HistoryTransaction) -> Element<'a, Message> {
|
||||
Container::new(
|
||||
Button::new(
|
||||
Row::new()
|
||||
.push(
|
||||
Row::new()
|
||||
.push(if event.is_external() {
|
||||
badge::receive()
|
||||
} else {
|
||||
badge::spend()
|
||||
})
|
||||
.push(if let Some(t) = event.time {
|
||||
Container::new(
|
||||
text(format!(
|
||||
"{}",
|
||||
NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(),
|
||||
))
|
||||
.small(),
|
||||
)
|
||||
} else {
|
||||
badge::unconfirmed()
|
||||
})
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(if event.is_external() {
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(text("+"))
|
||||
.push(amount(&event.incoming_amount))
|
||||
.align_items(Alignment::Center)
|
||||
fn event_list_view<'a>(i: usize, event: &HistoryTransaction) -> Column<'a, Message> {
|
||||
event.tx.output.iter().enumerate().fold(
|
||||
Column::new().spacing(10),
|
||||
|col, (output_index, output)| {
|
||||
if event.is_external() {
|
||||
if !event.change_indexes.contains(&output_index) {
|
||||
col
|
||||
} else if let Some(t) = event.time {
|
||||
col.push(event::confirmed_incoming_event(
|
||||
NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(),
|
||||
&Amount::from_sat(output.value),
|
||||
Message::Select(i),
|
||||
))
|
||||
} else {
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(text("-"))
|
||||
.push(amount(&event.outgoing_amount))
|
||||
.align_items(Alignment::Center)
|
||||
})
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20),
|
||||
)
|
||||
.padding(10)
|
||||
.on_press(Message::Select(i))
|
||||
.style(theme::Button::TransparentBorder),
|
||||
col.push(event::unconfirmed_incoming_event(
|
||||
&Amount::from_sat(output.value),
|
||||
Message::Select(i),
|
||||
))
|
||||
}
|
||||
} else if event.change_indexes.contains(&output_index) {
|
||||
col
|
||||
} else if let Some(t) = event.time {
|
||||
col.push(event::confirmed_outgoing_event(
|
||||
NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(),
|
||||
&Amount::from_sat(output.value),
|
||||
Message::Select(i),
|
||||
))
|
||||
} else {
|
||||
col.push(event::unconfirmed_outgoing_event(
|
||||
&Amount::from_sat(output.value),
|
||||
Message::Select(i),
|
||||
))
|
||||
}
|
||||
},
|
||||
)
|
||||
.style(theme::Container::Card(theme::Card::Simple))
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn event_view<'a>(cache: &Cache, event: &'a HistoryTransaction) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.push(
|
||||
Row::new()
|
||||
.push(if event.is_external() {
|
||||
badge::receive()
|
||||
} else {
|
||||
badge::spend()
|
||||
})
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.push(if event.is_external() {
|
||||
amount_with_size(&event.incoming_amount, 50)
|
||||
} else {
|
||||
amount_with_size(&event.outgoing_amount, 50)
|
||||
})
|
||||
.push_maybe(
|
||||
event
|
||||
.fee_amount
|
||||
.map(|fee| Row::new().push(text("Miner Fee: ")).push(amount(&fee))),
|
||||
)
|
||||
.push(card::simple(
|
||||
Column::new()
|
||||
.push_maybe(event.time.map(|t| {
|
||||
let date = NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap();
|
||||
Row::new()
|
||||
.width(Length::Fill)
|
||||
.push(Container::new(text("Date:").bold()).width(Length::Fill))
|
||||
.push(Container::new(text(format!("{}", date))).width(Length::Shrink))
|
||||
}))
|
||||
.push(
|
||||
Row::new()
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center)
|
||||
.push(Container::new(text("Txid:").bold()).width(Length::Fill))
|
||||
.push(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(Container::new(text(format!("{}", event.tx.txid())).small()))
|
||||
.push(
|
||||
Button::new(icon::clipboard_icon())
|
||||
.on_press(Message::Clipboard(event.tx.txid().to_string()))
|
||||
.style(theme::Button::TransparentBorder),
|
||||
)
|
||||
.width(Length::Shrink),
|
||||
),
|
||||
)
|
||||
.spacing(5),
|
||||
))
|
||||
.push(super::spend::detail::inputs_and_outputs_view(
|
||||
&event.coins,
|
||||
&event.tx,
|
||||
cache.network,
|
||||
if event.is_external() {
|
||||
None
|
||||
} else {
|
||||
Some(event.change_indexes.clone())
|
||||
},
|
||||
if event.is_external() {
|
||||
Some(event.change_indexes.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
))
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20)
|
||||
.max_width(800)
|
||||
.into()
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
mod message;
|
||||
mod util;
|
||||
mod warning;
|
||||
|
||||
pub mod coins;
|
||||
@ -10,6 +9,7 @@ pub mod receive;
|
||||
pub mod recovery;
|
||||
pub mod settings;
|
||||
pub mod spend;
|
||||
pub mod transactions;
|
||||
|
||||
pub use message::*;
|
||||
use warning::warn;
|
||||
@ -41,6 +41,34 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> {
|
||||
.width(iced::Length::Fill)
|
||||
};
|
||||
|
||||
let transactions_button = if *menu == Menu::Transactions {
|
||||
Button::new(
|
||||
row!(
|
||||
history_icon().width(Length::Units(20)),
|
||||
text("Transactions")
|
||||
)
|
||||
.spacing(10)
|
||||
.padding(10)
|
||||
.align_items(iced::Alignment::Center),
|
||||
)
|
||||
.style(theme::Button::Menu(true))
|
||||
.on_press(Message::Reload)
|
||||
.width(iced::Length::Fill)
|
||||
} else {
|
||||
Button::new(
|
||||
row!(
|
||||
history_icon().width(Length::Units(20)),
|
||||
text("Transactions")
|
||||
)
|
||||
.spacing(10)
|
||||
.padding(10)
|
||||
.align_items(iced::Alignment::Center),
|
||||
)
|
||||
.style(theme::Button::Menu(false))
|
||||
.on_press(Message::Menu(Menu::Transactions))
|
||||
.width(iced::Length::Fill)
|
||||
};
|
||||
|
||||
let coins_button = if *menu == Menu::Coins {
|
||||
Button::new(
|
||||
Container::new(
|
||||
@ -266,6 +294,7 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> {
|
||||
.push(receive_button)
|
||||
.push(coins_button)
|
||||
.push(psbt_button)
|
||||
.push(transactions_button)
|
||||
.spacing(15)
|
||||
.height(Length::Fill),
|
||||
)
|
||||
|
||||
@ -2,14 +2,14 @@ use iced::{widget::Space, Alignment, Length};
|
||||
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{badge, button, card, form, text::*},
|
||||
component::{amount::*, badge, button, card, form, text::*},
|
||||
icon, theme,
|
||||
util::Collection,
|
||||
widget::*,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::{error::Error, menu::Menu, view::util::*},
|
||||
app::{error::Error, menu::Menu},
|
||||
daemon::model::{SpendStatus, SpendTx},
|
||||
};
|
||||
|
||||
@ -63,6 +63,7 @@ pub fn psbts_view<'a>(spend_txs: &[SpendTx]) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.push(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(10)
|
||||
.push(Container::new(h3("PSBTs")).width(Length::Fill))
|
||||
.push(
|
||||
|
||||
@ -11,15 +11,12 @@ use liana::miniscript::bitcoin::{
|
||||
};
|
||||
|
||||
use liana_ui::{
|
||||
component::{button, form, text::*},
|
||||
component::{amount::*, button, form, text::*},
|
||||
icon, theme,
|
||||
widget::*,
|
||||
};
|
||||
|
||||
use crate::app::view::{
|
||||
message::{CreateSpendMessage, Message},
|
||||
util::amount,
|
||||
};
|
||||
use crate::app::view::message::{CreateSpendMessage, Message};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn recovery<'a>(
|
||||
|
||||
@ -16,6 +16,7 @@ use liana::{
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{
|
||||
amount::*,
|
||||
badge, button, card,
|
||||
collapse::Collapse,
|
||||
form, hw, separation,
|
||||
@ -29,7 +30,7 @@ use liana_ui::{
|
||||
use crate::{
|
||||
app::{
|
||||
error::Error,
|
||||
view::{hw::hw_list_view, message::*, util::*, warning::warn},
|
||||
view::{hw::hw_list_view, message::*, warning::warn},
|
||||
},
|
||||
daemon::model::{Coin, SpendStatus, SpendTx},
|
||||
hw::HardwareWallet,
|
||||
@ -205,6 +206,8 @@ fn spend_header<'a>(tx: &SpendTx) -> Element<'a, Message> {
|
||||
.push(badge::Badge::new(icon::send_icon()).style(theme::Badge::Standard))
|
||||
.push(if !tx.sigs.recovery_paths().is_empty() {
|
||||
text("Recovery").bold()
|
||||
} else if tx.spend_amount == Amount::from_sat(0) {
|
||||
text("Self send").bold()
|
||||
} else {
|
||||
text("Spend").bold()
|
||||
})
|
||||
|
||||
@ -5,6 +5,7 @@ use liana::miniscript::bitcoin::Amount;
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{
|
||||
amount::*,
|
||||
badge, button, form,
|
||||
text::{text, Text},
|
||||
},
|
||||
@ -17,7 +18,7 @@ use crate::{
|
||||
app::{
|
||||
cache::Cache,
|
||||
error::Error,
|
||||
view::{message::*, modal, util::amount},
|
||||
view::{message::*, modal},
|
||||
},
|
||||
daemon::model::{remaining_sequence, Coin},
|
||||
};
|
||||
|
||||
190
gui/src/app/view/transactions.rs
Normal file
190
gui/src/app/view/transactions.rs
Normal file
@ -0,0 +1,190 @@
|
||||
use chrono::NaiveDateTime;
|
||||
|
||||
use iced::{alignment, Alignment, Length};
|
||||
|
||||
use liana_ui::{
|
||||
component::{amount::*, badge, card, text::*},
|
||||
icon, theme,
|
||||
util::Collection,
|
||||
widget::*,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::{cache::Cache, view::message::Message},
|
||||
daemon::model::HistoryTransaction,
|
||||
};
|
||||
|
||||
pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20;
|
||||
|
||||
pub fn transactions_view<'a>(
|
||||
pending_txs: &[HistoryTransaction],
|
||||
txs: &Vec<HistoryTransaction>,
|
||||
) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.push(Container::new(h3("Transactions")).width(Length::Fill))
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
pending_txs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(Column::new().spacing(10), |col, (i, tx)| {
|
||||
col.push(tx_list_view(i, tx))
|
||||
}),
|
||||
)
|
||||
.push(
|
||||
txs.iter()
|
||||
.enumerate()
|
||||
.fold(Column::new().spacing(10), |col, (i, tx)| {
|
||||
col.push(tx_list_view(i + pending_txs.len(), tx))
|
||||
}),
|
||||
)
|
||||
.push_maybe(
|
||||
if txs.len() % HISTORY_EVENT_PAGE_SIZE as usize == 0 && !txs.is_empty() {
|
||||
Some(
|
||||
Container::new(
|
||||
Button::new(
|
||||
text("See more")
|
||||
.width(Length::Fill)
|
||||
.horizontal_alignment(alignment::Horizontal::Center),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.padding(15)
|
||||
.style(theme::Button::TransparentBorder)
|
||||
.on_press(Message::Next),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.style(theme::Container::Card(theme::Card::Simple)),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
),
|
||||
)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn tx_list_view<'a>(i: usize, tx: &HistoryTransaction) -> Element<'a, Message> {
|
||||
Container::new(
|
||||
Button::new(
|
||||
Row::new()
|
||||
.push(
|
||||
Row::new()
|
||||
.push(if tx.is_external() {
|
||||
badge::receive()
|
||||
} else {
|
||||
badge::spend()
|
||||
})
|
||||
.push(if let Some(t) = tx.time {
|
||||
Container::new(
|
||||
text(format!(
|
||||
"{}",
|
||||
NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(),
|
||||
))
|
||||
.small(),
|
||||
)
|
||||
} else {
|
||||
badge::unconfirmed()
|
||||
})
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(if tx.is_external() {
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(text("+"))
|
||||
.push(amount(&tx.incoming_amount))
|
||||
.align_items(Alignment::Center)
|
||||
} else if tx.outgoing_amount != Amount::from_sat(0) {
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(text("-"))
|
||||
.push(amount(&tx.outgoing_amount))
|
||||
.align_items(Alignment::Center)
|
||||
} else {
|
||||
Row::new().push(text("Self send"))
|
||||
})
|
||||
.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()
|
||||
}
|
||||
|
||||
pub fn tx_view<'a>(cache: &Cache, tx: &'a HistoryTransaction) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.push(
|
||||
Row::new()
|
||||
.push(if tx.is_external() {
|
||||
badge::receive()
|
||||
} else {
|
||||
badge::spend()
|
||||
})
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.push(if tx.is_external() {
|
||||
amount_with_size(&tx.incoming_amount, 50)
|
||||
} else {
|
||||
amount_with_size(&tx.outgoing_amount, 50)
|
||||
})
|
||||
.push_maybe(
|
||||
tx.fee_amount
|
||||
.map(|fee| Row::new().push(text("Miner Fee: ")).push(amount(&fee))),
|
||||
)
|
||||
.push(card::simple(
|
||||
Column::new()
|
||||
.push_maybe(tx.time.map(|t| {
|
||||
let date = NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap();
|
||||
Row::new()
|
||||
.width(Length::Fill)
|
||||
.push(Container::new(text("Date:").bold()).width(Length::Fill))
|
||||
.push(Container::new(text(format!("{}", date))).width(Length::Shrink))
|
||||
}))
|
||||
.push(
|
||||
Row::new()
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center)
|
||||
.push(Container::new(text("Txid:").bold()).width(Length::Fill))
|
||||
.push(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(Container::new(text(format!("{}", tx.tx.txid())).small()))
|
||||
.push(
|
||||
Button::new(icon::clipboard_icon())
|
||||
.on_press(Message::Clipboard(tx.tx.txid().to_string()))
|
||||
.style(theme::Button::TransparentBorder),
|
||||
)
|
||||
.width(Length::Shrink),
|
||||
),
|
||||
)
|
||||
.spacing(5),
|
||||
))
|
||||
.push(super::spend::detail::inputs_and_outputs_view(
|
||||
&tx.coins,
|
||||
&tx.tx,
|
||||
cache.network,
|
||||
if tx.is_external() {
|
||||
None
|
||||
} else {
|
||||
Some(tx.change_indexes.clone())
|
||||
},
|
||||
if tx.is_external() {
|
||||
Some(tx.change_indexes.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
))
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20)
|
||||
.max_width(800)
|
||||
.into()
|
||||
}
|
||||
@ -180,4 +180,8 @@ impl HistoryTransaction {
|
||||
pub fn is_external(&self) -> bool {
|
||||
self.coins.is_empty()
|
||||
}
|
||||
|
||||
pub fn is_self_send(&self) -> bool {
|
||||
!self.coins.is_empty() && self.outgoing_amount == Amount::from_sat(0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,3 +9,5 @@ edition = "2021"
|
||||
iced = { version = "0.7", features = ["svg", "image"] }
|
||||
iced_native = "0.8"
|
||||
iced_lazy = { version = "0.4"}
|
||||
bitcoin = "0.29"
|
||||
chrono = "0.4"
|
||||
|
||||
@ -9,6 +9,7 @@ edition = "2021"
|
||||
iced = "0.7"
|
||||
iced_native = "0.8"
|
||||
web-sys = "0.3.61"
|
||||
chrono = "0.4"
|
||||
liana_ui = { path = "../.." }
|
||||
|
||||
[workspace]
|
||||
|
||||
@ -45,6 +45,7 @@ impl Application for DesignSystem {
|
||||
Box::new(section::Typography {}),
|
||||
Box::new(section::Buttons {}),
|
||||
Box::new(section::HardwareWallets {}),
|
||||
Box::new(section::Events {}),
|
||||
],
|
||||
current: 0,
|
||||
};
|
||||
@ -123,12 +124,14 @@ impl Application for DesignSystem {
|
||||
.height(Length::Fill);
|
||||
|
||||
container(row![
|
||||
sidebar.width(Length::Units(200)),
|
||||
Space::with_width(Length::Units(150)),
|
||||
scrollable(column![
|
||||
sidebar.width(Length::FillPortion(2)),
|
||||
Space::with_width(Length::FillPortion(1)),
|
||||
container(scrollable(column![
|
||||
Space::with_height(Length::Units(150)),
|
||||
container(self.sections[self.current].view()).width(Length::Fill)
|
||||
]),
|
||||
]))
|
||||
.width(Length::FillPortion(8)),
|
||||
Space::with_width(Length::FillPortion(1)),
|
||||
])
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
|
||||
@ -5,7 +5,7 @@ use iced::{
|
||||
};
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{hw, separation, text::*},
|
||||
component::{amount::Amount, event, hw, separation, text::*},
|
||||
theme,
|
||||
widget::Element,
|
||||
};
|
||||
@ -253,3 +253,35 @@ impl Section for HardwareWallets {
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Events {}
|
||||
|
||||
impl Section for Events {
|
||||
fn title(&self) -> &'static str {
|
||||
"Events "
|
||||
}
|
||||
fn view(&self) -> Element<Message> {
|
||||
let d = chrono::NaiveDate::from_ymd_opt(2015, 6, 3).unwrap();
|
||||
let t = chrono::NaiveTime::from_hms_milli_opt(12, 34, 56, 789).unwrap();
|
||||
column![
|
||||
h1(self.title()),
|
||||
column![
|
||||
event::unconfirmed_outgoing_event(&Amount::from_sat(32934234), Message::Ignore),
|
||||
event::confirmed_outgoing_event(
|
||||
chrono::NaiveDateTime::new(d, t),
|
||||
&Amount::from_sat(32934234),
|
||||
Message::Ignore
|
||||
),
|
||||
event::unconfirmed_incoming_event(&Amount::from_sat(32934234), Message::Ignore),
|
||||
event::confirmed_incoming_event(
|
||||
chrono::NaiveDateTime::new(d, t),
|
||||
&Amount::from_sat(32934234),
|
||||
Message::Ignore
|
||||
)
|
||||
]
|
||||
.spacing(20)
|
||||
]
|
||||
.spacing(100)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
use liana::miniscript::bitcoin::Amount;
|
||||
pub use bitcoin::Amount;
|
||||
|
||||
use liana_ui::{color, component::text::*, util::Collection, widget::*};
|
||||
use crate::{color, component::text::*, util::Collection, widget::*};
|
||||
|
||||
pub fn amount<'a, T: 'a>(a: &Amount) -> impl Into<Element<'a, T>> {
|
||||
pub fn amount<'a, T: 'a>(a: &Amount) -> Row<'a, T> {
|
||||
amount_with_size(a, P1_SIZE)
|
||||
}
|
||||
|
||||
pub fn amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> impl Into<Element<'a, T>> {
|
||||
pub fn amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> {
|
||||
let spacing = if size > P1_SIZE { 10 } else { 5 };
|
||||
let sats = format!("{:.8}", a.to_btc());
|
||||
assert!(sats.len() >= 9);
|
||||
@ -36,9 +36,12 @@ pub fn amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> impl Into<Element<'
|
||||
.into()
|
||||
});
|
||||
|
||||
Row::with_children(vec![row.into(), text("BTC").size(size).into()])
|
||||
.spacing(spacing)
|
||||
.align_items(iced::Alignment::Center)
|
||||
Row::with_children(vec![
|
||||
row.into(),
|
||||
text("BTC").size(size).style(color::GREY_3).into(),
|
||||
])
|
||||
.spacing(spacing)
|
||||
.align_items(iced::Alignment::Center)
|
||||
}
|
||||
|
||||
fn split_digits<'a, T: 'a>(mut s: String, size: u16) -> impl Into<Element<'a, T>> {
|
||||
@ -47,7 +50,7 @@ fn split_digits<'a, T: 'a>(mut s: String, size: u16) -> impl Into<Element<'a, T>
|
||||
if s.starts_with(prefix) {
|
||||
let right = s.split_off(prefix.len());
|
||||
return Row::new()
|
||||
.push(text(s).size(size).style(color::GREY_2))
|
||||
.push(text(s).size(size).style(color::GREY_3))
|
||||
.push_maybe(if right.is_empty() {
|
||||
None
|
||||
} else {
|
||||
106
gui/ui/src/component/event.rs
Normal file
106
gui/ui/src/component/event.rs
Normal file
@ -0,0 +1,106 @@
|
||||
use crate::{
|
||||
component::{amount, badge, text},
|
||||
theme,
|
||||
widget::*,
|
||||
};
|
||||
use bitcoin::Amount;
|
||||
use iced::{
|
||||
widget::{button, row},
|
||||
Alignment, Length,
|
||||
};
|
||||
|
||||
pub fn unconfirmed_outgoing_event<'a, T: Clone + 'a>(amount: &Amount, msg: T) -> Container<'a, T> {
|
||||
Container::new(
|
||||
button(
|
||||
row!(
|
||||
row!(badge::spend(), badge::unconfirmed())
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill),
|
||||
row!(text::p1_regular("-"), amount::amount(amount))
|
||||
.spacing(5)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.align_items(Alignment::Center)
|
||||
.padding(5)
|
||||
.spacing(20),
|
||||
)
|
||||
.on_press(msg)
|
||||
.style(theme::Button::TransparentBorder),
|
||||
)
|
||||
.style(theme::Container::Card(theme::Card::Simple))
|
||||
}
|
||||
|
||||
pub fn confirmed_outgoing_event<'a, T: Clone + 'a>(
|
||||
date: chrono::NaiveDateTime,
|
||||
amount: &Amount,
|
||||
msg: T,
|
||||
) -> Container<'a, T> {
|
||||
Container::new(
|
||||
button(
|
||||
row!(
|
||||
row!(badge::spend(), text::p2_regular(date.to_string()))
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill),
|
||||
row!(text::p1_regular("-"), amount::amount(amount))
|
||||
.spacing(5)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.align_items(Alignment::Center)
|
||||
.padding(5)
|
||||
.spacing(20),
|
||||
)
|
||||
.on_press(msg)
|
||||
.style(theme::Button::TransparentBorder),
|
||||
)
|
||||
.style(theme::Container::Card(theme::Card::Simple))
|
||||
}
|
||||
|
||||
pub fn unconfirmed_incoming_event<'a, T: Clone + 'a>(amount: &Amount, msg: T) -> Container<'a, T> {
|
||||
Container::new(
|
||||
button(
|
||||
row!(
|
||||
row!(badge::receive(), badge::unconfirmed())
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill),
|
||||
row!(text::p1_regular("+"), amount::amount(amount))
|
||||
.spacing(5)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.align_items(Alignment::Center)
|
||||
.padding(5)
|
||||
.spacing(20),
|
||||
)
|
||||
.on_press(msg)
|
||||
.style(theme::Button::TransparentBorder),
|
||||
)
|
||||
.style(theme::Container::Card(theme::Card::Simple))
|
||||
}
|
||||
|
||||
pub fn confirmed_incoming_event<'a, T: Clone + 'a>(
|
||||
date: chrono::NaiveDateTime,
|
||||
amount: &Amount,
|
||||
msg: T,
|
||||
) -> Container<'a, T> {
|
||||
Container::new(
|
||||
button(
|
||||
row!(
|
||||
row!(badge::receive(), text::p2_regular(date.to_string()))
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill),
|
||||
row!(text::p1_regular("+"), amount::amount(amount))
|
||||
.spacing(5)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.align_items(Alignment::Center)
|
||||
.padding(5)
|
||||
.spacing(20),
|
||||
)
|
||||
.on_press(msg)
|
||||
.style(theme::Button::TransparentBorder),
|
||||
)
|
||||
.style(theme::Container::Card(theme::Card::Simple))
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
pub mod amount;
|
||||
pub mod badge;
|
||||
pub mod button;
|
||||
pub mod card;
|
||||
pub mod collapse;
|
||||
pub mod event;
|
||||
pub mod form;
|
||||
pub mod hw;
|
||||
pub mod modal;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user