Merge #720: gui: Trim text inputs
9895b493f1d79ae8fc5a1591ef455ccda2d81ccd gui: trim text input strings (jp1ac4) d3f2931375ff95c201a60164bf1015f18c6e87ce gui: remove unused file (jp1ac4) Pull request description: This is to resolve #323. Following suggestions in #323, I've added a new `TrimmedString` struct that takes a string and applies `trim()`. This type is then used as `form::Value<TrimmedString>` to ensure text inputs are trimmed. I'm creating this PR as draft to check if this is the right approach before applying the change to other inputs. I've also removed a view file that seems to have been created accidentally. ACKs for top commit: edouardparis: ACK 9895b493f1d79ae8fc5a1591ef455ccda2d81ccd Tree-SHA512: 76c7f28ed2d0b6b6b76658a9368b918b0b211e2fabd72ac5d19c13adcbb2d3645b78680a8849f235ee58620fbe8df440ef8354f2d50d39601879bb7727465335
This commit is contained in:
commit
19491d1d00
@ -1,836 +0,0 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use iced::{
|
||||
widget::{scrollable, tooltip, Space},
|
||||
Alignment, Length,
|
||||
};
|
||||
|
||||
use liana::{
|
||||
descriptors::{LianaPolicy, PathInfo, PathSpendInfo},
|
||||
miniscript::bitcoin::{
|
||||
util::bip32::{DerivationPath, Fingerprint},
|
||||
Address, Amount, Network, Transaction,
|
||||
},
|
||||
};
|
||||
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{
|
||||
amount::*,
|
||||
badge, button, card,
|
||||
collapse::Collapse,
|
||||
form, hw, separation,
|
||||
text::{text, Text},
|
||||
},
|
||||
icon, theme,
|
||||
util::Collection,
|
||||
widget::*,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
cache::Cache,
|
||||
error::Error,
|
||||
menu::Menu,
|
||||
view::{dashboard, hw::hw_list_view, message::*, warning::warn},
|
||||
},
|
||||
daemon::model::{Coin, SpendStatus, SpendTx},
|
||||
hw::HardwareWallet,
|
||||
};
|
||||
|
||||
pub fn psbt_view<'a>(
|
||||
cache: &'a Cache,
|
||||
tx: &'a SpendTx,
|
||||
desc_info: &'a LianaPolicy,
|
||||
key_aliases: &'a HashMap<Fingerprint, String>,
|
||||
network: Network,
|
||||
) -> Element<'a, Message> {
|
||||
dashboard(
|
||||
&Menu::PSBTs,
|
||||
&cache,
|
||||
None,
|
||||
Column::new()
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20)
|
||||
.push(spend_header(tx))
|
||||
.push(spend_overview_view(tx, desc_info, key_aliases))
|
||||
.push(inputs_and_outputs_view(
|
||||
&tx.coins,
|
||||
&tx.psbt.unsigned_tx,
|
||||
network,
|
||||
Some(tx.change_indexes.clone()),
|
||||
None,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn save_action<'a>(warning: Option<&Error>, saved: bool) -> Element<'a, Message> {
|
||||
if saved {
|
||||
card::simple(text("Transaction is saved"))
|
||||
.width(Length::Units(400))
|
||||
.align_x(iced::alignment::Horizontal::Center)
|
||||
.into()
|
||||
} else {
|
||||
card::simple(
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.push_maybe(warning.map(|w| warn(Some(w))))
|
||||
.push(text("Save the transaction as draft"))
|
||||
.push(
|
||||
Row::new()
|
||||
.push(Column::new().width(Length::Fill))
|
||||
.push(button::alert(None, "Ignore").on_press(Message::Close))
|
||||
.push(
|
||||
button::primary(None, "Save")
|
||||
.on_press(Message::Spend(SpendTxMessage::Confirm)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.width(Length::Units(400))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn broadcast_action<'a>(warning: Option<&Error>, saved: bool) -> Element<'a, Message> {
|
||||
if saved {
|
||||
card::simple(text("Transaction is broadcast"))
|
||||
.width(Length::Units(400))
|
||||
.align_x(iced::alignment::Horizontal::Center)
|
||||
.into()
|
||||
} else {
|
||||
card::simple(
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.push_maybe(warning.map(|w| warn(Some(w))))
|
||||
.push(text("Broadcast the transaction"))
|
||||
.push(
|
||||
Row::new().push(Column::new().width(Length::Fill)).push(
|
||||
button::primary(None, "Broadcast")
|
||||
.on_press(Message::Spend(SpendTxMessage::Confirm)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.width(Length::Units(400))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_action<'a>(warning: Option<&Error>, deleted: bool) -> Element<'a, Message> {
|
||||
if deleted {
|
||||
card::simple(
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.align_items(Alignment::Center)
|
||||
.push(text("Transaction is deleted"))
|
||||
.push(button::primary(None, "Go back to drafts").on_press(Message::Close)),
|
||||
)
|
||||
.align_x(iced::alignment::Horizontal::Center)
|
||||
.width(Length::Units(400))
|
||||
.into()
|
||||
} else {
|
||||
card::simple(
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.push_maybe(warning.map(|w| warn(Some(w))))
|
||||
.push(text("Delete the transaction draft"))
|
||||
.push(
|
||||
Row::new()
|
||||
.push(Column::new().width(Length::Fill))
|
||||
.push(
|
||||
button::transparent(None, "Cancel")
|
||||
.on_press(Message::Spend(SpendTxMessage::Cancel)),
|
||||
)
|
||||
.push(
|
||||
button::alert(None, "Delete")
|
||||
.on_press(Message::Spend(SpendTxMessage::Confirm)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.width(Length::Units(400))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spend_modal<'a, T: Into<Element<'a, Message>>>(
|
||||
saved: bool,
|
||||
warning: Option<&Error>,
|
||||
content: T,
|
||||
) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.push(warn(warning))
|
||||
.push(
|
||||
Container::new(
|
||||
Row::new()
|
||||
.push(if saved {
|
||||
Column::new()
|
||||
.push(
|
||||
button::alert(Some(icon::trash_icon()), "Delete")
|
||||
.on_press(Message::Spend(SpendTxMessage::Delete)),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
} else {
|
||||
Column::new()
|
||||
.push(
|
||||
button::transparent(None, "< Previous").on_press(Message::Previous),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
})
|
||||
.align_items(iced::Alignment::Center)
|
||||
.push(if saved {
|
||||
button::primary(Some(icon::cross_icon()), "Close").on_press(Message::Close)
|
||||
} else {
|
||||
button::primary(Some(icon::cross_icon()), "Close")
|
||||
.on_press(Message::Spend(SpendTxMessage::Save))
|
||||
}),
|
||||
)
|
||||
.padding(10)
|
||||
.style(theme::Container::Background),
|
||||
)
|
||||
.push(
|
||||
Container::new(scrollable(
|
||||
Container::new(Container::new(content).max_width(800))
|
||||
.width(Length::Fill)
|
||||
.center_x(),
|
||||
))
|
||||
.height(Length::Fill)
|
||||
.style(theme::Container::Background),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn spend_header<'a>(tx: &SpendTx) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
Row::new()
|
||||
.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()
|
||||
})
|
||||
.spacing(5)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.push_maybe(match tx.status {
|
||||
SpendStatus::Deprecated => Some(badge::deprecated()),
|
||||
SpendStatus::Broadcast => Some(badge::unconfirmed()),
|
||||
SpendStatus::Spent => Some(badge::spent()),
|
||||
_ => None,
|
||||
})
|
||||
.push(
|
||||
Column::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(amount_with_size(&tx.spend_amount, 50))
|
||||
.push(
|
||||
Row::new()
|
||||
.push(text("Miner fee: "))
|
||||
.push(amount(&tx.fee_amount)),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn spend_overview_view<'a>(
|
||||
tx: &'a SpendTx,
|
||||
desc_info: &'a LianaPolicy,
|
||||
key_aliases: &'a HashMap<Fingerprint, String>,
|
||||
) -> Element<'a, Message> {
|
||||
Container::new(
|
||||
Column::new()
|
||||
.push(
|
||||
Column::new()
|
||||
.padding(15)
|
||||
.spacing(10)
|
||||
.push(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(text("PSBT:").bold().width(Length::Fill))
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(
|
||||
button::secondary(Some(icon::clipboard_icon()), "Copy")
|
||||
.on_press(Message::Clipboard(tx.psbt.to_string())),
|
||||
)
|
||||
.push(
|
||||
button::secondary(Some(icon::import_icon()), "Update")
|
||||
.on_press(Message::Spend(SpendTxMessage::EditPsbt)),
|
||||
),
|
||||
)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.push(
|
||||
Row::new()
|
||||
.push(text("Tx ID:").bold().width(Length::Fill))
|
||||
.push(text(tx.psbt.unsigned_tx.txid().to_string()).small())
|
||||
.push(
|
||||
Button::new(icon::clipboard_icon())
|
||||
.on_press(Message::Clipboard(
|
||||
tx.psbt.unsigned_tx.txid().to_string(),
|
||||
))
|
||||
.style(theme::Button::TransparentBorder),
|
||||
)
|
||||
.align_items(Alignment::Center),
|
||||
),
|
||||
)
|
||||
.push(signatures(tx, desc_info, key_aliases)),
|
||||
)
|
||||
.style(theme::Container::Card(theme::Card::Simple))
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn signatures<'a>(
|
||||
tx: &'a SpendTx,
|
||||
desc_info: &'a LianaPolicy,
|
||||
keys_aliases: &'a HashMap<Fingerprint, String>,
|
||||
) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.push(
|
||||
if let Some(sigs) = tx.path_ready() {
|
||||
Container::new(
|
||||
scrollable(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.align_items(Alignment::Center)
|
||||
.push(icon::circle_check_icon().style(color::GREEN))
|
||||
.push(text("Ready").bold().style(color::GREEN))
|
||||
.push(text(", signed by"))
|
||||
.push(
|
||||
sigs.signed_pubkeys
|
||||
.keys()
|
||||
.fold(Row::new().spacing(5), |row, value| {
|
||||
row.push(if let Some(alias) = keys_aliases.get(&value.0) {
|
||||
Container::new(
|
||||
tooltip::Tooltip::new(
|
||||
Container::new(text(alias))
|
||||
.padding(3)
|
||||
.style(theme::Container::Pill(theme::Pill::Simple)),
|
||||
value.0.to_string(),
|
||||
tooltip::Position::Bottom,
|
||||
)
|
||||
.style(theme::Container::Card(theme::Card::Simple)),
|
||||
)
|
||||
} else {
|
||||
Container::new(text(value.0.to_string()))
|
||||
.padding(3)
|
||||
.style(theme::Container::Pill(theme::Pill::Simple))
|
||||
})
|
||||
}),
|
||||
)
|
||||
).horizontal_scroll(scrollable::Properties::new().width(2).scroller_width(2))
|
||||
).padding(15)
|
||||
} else{
|
||||
Container::new(
|
||||
Collapse::new(
|
||||
move || {
|
||||
Button::new(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(Row::new()
|
||||
.spacing(5)
|
||||
.align_items(Alignment::Center)
|
||||
.push(icon::circle_cross_icon())
|
||||
.push(text("Not ready").bold())
|
||||
.width(Length::Fill)
|
||||
)
|
||||
.push(icon::collapse_icon()),
|
||||
)
|
||||
.padding(15)
|
||||
.width(Length::Fill)
|
||||
.style(theme::Button::TransparentBorder)
|
||||
},
|
||||
move || {
|
||||
Button::new(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.align_items(Alignment::Center)
|
||||
.push(icon::circle_cross_icon())
|
||||
.push(text("Not ready").bold())
|
||||
.width(Length::Fill)
|
||||
)
|
||||
.push(icon::collapsed_icon()),
|
||||
)
|
||||
.padding(15)
|
||||
.width(Length::Fill)
|
||||
.style(theme::Button::TransparentBorder)
|
||||
},
|
||||
move || {
|
||||
Into::<Element<'a, Message>>::into(
|
||||
Column::new().push(separation().width(Length::Fill)).push(
|
||||
Column::new()
|
||||
.padding(15)
|
||||
.spacing(10)
|
||||
.push(text(if !tx.sigs.recovery_paths().is_empty() {
|
||||
"Multiple spending paths available. Finalizing this transaction requires either:"
|
||||
} else {
|
||||
"1 spending path available. Finalizing this transaction requires:"
|
||||
}))
|
||||
.push(path_view(
|
||||
desc_info.primary_path(),
|
||||
tx.sigs.primary_path(),
|
||||
keys_aliases,
|
||||
))
|
||||
.push(tx.sigs.recovery_paths().iter().fold(Column::new().spacing(10), |col, (seq, path)| {
|
||||
let keys = &desc_info.recovery_paths()[seq];
|
||||
col.push(path_view(keys, path, keys_aliases))
|
||||
})),
|
||||
),
|
||||
)
|
||||
},
|
||||
))})
|
||||
.push_maybe(if tx.status == SpendStatus::Pending {
|
||||
Some(
|
||||
Column::new().push(separation().width(Length::Fill)).push(
|
||||
Container::new(
|
||||
Row::new()
|
||||
.push(Space::with_width(Length::Fill))
|
||||
.push_maybe(if tx.path_ready().is_none() {
|
||||
Some(
|
||||
button::primary(None, "Sign")
|
||||
.on_press(Message::Spend(SpendTxMessage::Sign))
|
||||
.width(Length::Units(150)),
|
||||
)
|
||||
} else {
|
||||
Some(
|
||||
button::primary(None, "Broadcast")
|
||||
.on_press(Message::Spend(SpendTxMessage::Broadcast))
|
||||
.width(Length::Units(150)),
|
||||
)
|
||||
})
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20),
|
||||
)
|
||||
.padding(15),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn path_view<'a>(
|
||||
path: &'a PathInfo,
|
||||
sigs: &'a PathSpendInfo,
|
||||
key_aliases: &'a HashMap<Fingerprint, String>,
|
||||
) -> Element<'a, Message> {
|
||||
let mut keys: Vec<(Fingerprint, DerivationPath)> =
|
||||
path.thresh_origins().1.into_iter().collect();
|
||||
let missing_signatures = if sigs.sigs_count >= sigs.threshold {
|
||||
0
|
||||
} else {
|
||||
sigs.threshold - sigs.sigs_count
|
||||
};
|
||||
keys.sort();
|
||||
scrollable(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(if sigs.sigs_count >= sigs.threshold {
|
||||
icon::circle_check_icon().style(color::GREEN)
|
||||
} else {
|
||||
icon::circle_cross_icon()
|
||||
})
|
||||
.push(text(format!(" {}", missing_signatures)).bold())
|
||||
.push(text(format!(
|
||||
" more signature{}",
|
||||
if missing_signatures > 1 {
|
||||
"s from "
|
||||
} else if missing_signatures == 0 {
|
||||
""
|
||||
} else {
|
||||
" from "
|
||||
}
|
||||
)))
|
||||
.push_maybe(if keys.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(keys.iter().fold(Row::new().spacing(5), |row, value| {
|
||||
row.push_maybe(if !sigs.signed_pubkeys.contains_key(value) {
|
||||
Some(if let Some(alias) = key_aliases.get(&value.0) {
|
||||
Container::new(
|
||||
tooltip::Tooltip::new(
|
||||
Container::new(text(alias))
|
||||
.padding(3)
|
||||
.style(theme::Container::Pill(theme::Pill::Simple)),
|
||||
value.0.to_string(),
|
||||
tooltip::Position::Bottom,
|
||||
)
|
||||
.style(theme::Container::Card(theme::Card::Simple)),
|
||||
)
|
||||
} else {
|
||||
Container::new(text(value.0.to_string()))
|
||||
.padding(3)
|
||||
.style(theme::Container::Pill(theme::Pill::Simple))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}))
|
||||
})
|
||||
.push_maybe(if sigs.signed_pubkeys.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(text(", already signed by "))
|
||||
})
|
||||
.push(
|
||||
sigs.signed_pubkeys
|
||||
.keys()
|
||||
.fold(Row::new().spacing(5), |row, value| {
|
||||
row.push(if let Some(alias) = key_aliases.get(&value.0) {
|
||||
Container::new(
|
||||
tooltip::Tooltip::new(
|
||||
Container::new(text(alias))
|
||||
.padding(3)
|
||||
.style(theme::Container::Pill(theme::Pill::Simple)),
|
||||
value.0.to_string(),
|
||||
tooltip::Position::Bottom,
|
||||
)
|
||||
.style(theme::Container::Card(theme::Card::Simple)),
|
||||
)
|
||||
} else {
|
||||
Container::new(text(value.0.to_string()))
|
||||
.padding(3)
|
||||
.style(theme::Container::Pill(theme::Pill::Simple))
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
.horizontal_scroll(scrollable::Properties::new().width(2).scroller_width(2))
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn inputs_and_outputs_view<'a>(
|
||||
coins: &'a [Coin],
|
||||
tx: &'a Transaction,
|
||||
network: Network,
|
||||
change_indexes: Option<Vec<usize>>,
|
||||
receive_indexes: Option<Vec<usize>>,
|
||||
) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.push_maybe(if !coins.is_empty() {
|
||||
Some(
|
||||
Container::new(Collapse::new(
|
||||
move || {
|
||||
Button::new(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
text(format!(
|
||||
"{} spent coin{}",
|
||||
coins.len(),
|
||||
if coins.len() == 1 { "" } else { "s" }
|
||||
))
|
||||
.bold()
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(icon::collapse_icon()),
|
||||
)
|
||||
.padding(15)
|
||||
.width(Length::Fill)
|
||||
.style(theme::Button::TransparentBorder)
|
||||
},
|
||||
move || {
|
||||
Button::new(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
text(format!(
|
||||
"{} spent coin{}",
|
||||
coins.len(),
|
||||
if coins.len() == 1 { "" } else { "s" }
|
||||
))
|
||||
.bold()
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(icon::collapsed_icon()),
|
||||
)
|
||||
.padding(15)
|
||||
.width(Length::Fill)
|
||||
.style(theme::Button::TransparentBorder)
|
||||
},
|
||||
move || {
|
||||
coins
|
||||
.iter()
|
||||
.fold(Column::new(), |col: Column<'a, Message>, coin| {
|
||||
col.push(
|
||||
Row::new()
|
||||
.padding(15)
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill)
|
||||
.push(
|
||||
Row::new()
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
text(coin.outpoint.to_string())
|
||||
.small()
|
||||
)
|
||||
.push(
|
||||
Button::new(icon::clipboard_icon())
|
||||
.on_press(Message::Clipboard(
|
||||
coin.outpoint.to_string(),
|
||||
))
|
||||
.style(
|
||||
theme::Button::TransparentBorder,
|
||||
),
|
||||
),
|
||||
)
|
||||
.push(amount(&coin.amount)),
|
||||
)
|
||||
})
|
||||
.into()
|
||||
},
|
||||
))
|
||||
.style(theme::Container::Card(theme::Card::Simple)),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.push(
|
||||
Container::new(Collapse::new(
|
||||
move || {
|
||||
Button::new(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
text(format!(
|
||||
"{} recipient{}",
|
||||
tx.output.len(),
|
||||
if tx.output.len() == 1 { "" } else { "s" }
|
||||
))
|
||||
.bold()
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(icon::collapse_icon()),
|
||||
)
|
||||
.padding(15)
|
||||
.width(Length::Fill)
|
||||
.style(theme::Button::TransparentBorder)
|
||||
},
|
||||
move || {
|
||||
Button::new(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
text(format!(
|
||||
"{} recipient{}",
|
||||
tx.output.len(),
|
||||
if tx.output.len() == 1 { "" } else { "s" }
|
||||
))
|
||||
.bold()
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(icon::collapsed_icon()),
|
||||
)
|
||||
.padding(15)
|
||||
.width(Length::Fill)
|
||||
.style(theme::Button::TransparentBorder)
|
||||
},
|
||||
move || {
|
||||
tx.output
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(Column::new(), |col: Column<'a, Message>, (i, output)| {
|
||||
let addr = Address::from_script(&output.script_pubkey, network).unwrap();
|
||||
col.push(
|
||||
Column::new()
|
||||
.padding(15)
|
||||
.width(Length::Fill)
|
||||
.spacing(10)
|
||||
.push(
|
||||
Row::new()
|
||||
.width(Length::Fill)
|
||||
.push(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill)
|
||||
.push(text(addr.to_string()).small())
|
||||
.push(
|
||||
Button::new(icon::clipboard_icon())
|
||||
.on_press(Message::Clipboard(
|
||||
addr.to_string(),
|
||||
))
|
||||
.style(
|
||||
theme::Button::TransparentBorder,
|
||||
),
|
||||
),
|
||||
)
|
||||
.push(
|
||||
amount(&Amount::from_sat(output.value))
|
||||
),
|
||||
)
|
||||
.push_maybe(
|
||||
if let Some(indexes) = change_indexes.as_ref() {
|
||||
if indexes.contains(&i) {
|
||||
Some(
|
||||
Container::new(text("Change"))
|
||||
.padding(5)
|
||||
.style(theme::Container::Pill(theme::Pill::Success)),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
.push_maybe(
|
||||
if let Some(indexes) = receive_indexes.as_ref() {
|
||||
if indexes.contains(&i) {
|
||||
Some(
|
||||
Container::new(text("Deposit"))
|
||||
.padding(5)
|
||||
.style(theme::Container::Pill(theme::Pill::Success)),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
.into()
|
||||
},
|
||||
))
|
||||
.style(theme::Container::Card(theme::Card::Simple)),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn sign_action<'a>(
|
||||
warning: Option<&Error>,
|
||||
hws: &'a [HardwareWallet],
|
||||
signer: Option<Fingerprint>,
|
||||
signer_alias: Option<&'a String>,
|
||||
processing: bool,
|
||||
chosen_hw: Option<usize>,
|
||||
signed: &HashSet<Fingerprint>,
|
||||
) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.push_maybe(warning.map(|w| warn(Some(w))))
|
||||
.push(card::simple(
|
||||
Column::new()
|
||||
.push(
|
||||
Column::new()
|
||||
.push(
|
||||
Row::new()
|
||||
.push(
|
||||
text("Select signing device to sign with:")
|
||||
.bold()
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(button::secondary(None, "Refresh").on_press(Message::Reload))
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.spacing(10)
|
||||
.push(hws.iter().enumerate().fold(
|
||||
Column::new().spacing(10),
|
||||
|col, (i, hw)| {
|
||||
col.push(hw_list_view(
|
||||
i,
|
||||
hw,
|
||||
Some(i) == chosen_hw,
|
||||
processing,
|
||||
hw.fingerprint()
|
||||
.map(|f| signed.contains(&f))
|
||||
.unwrap_or(false),
|
||||
))
|
||||
},
|
||||
))
|
||||
.push_maybe(signer.map(|fingerprint| {
|
||||
Button::new(if signed.contains(&fingerprint) {
|
||||
hw::sign_success_hot_signer(fingerprint, signer_alias)
|
||||
} else {
|
||||
hw::hot_signer(fingerprint, signer_alias)
|
||||
})
|
||||
.on_press(Message::Spend(SpendTxMessage::SelectHotSigner))
|
||||
.padding(10)
|
||||
.style(theme::Button::Border)
|
||||
.width(Length::Fill)
|
||||
}))
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.spacing(20)
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center),
|
||||
))
|
||||
.width(Length::Units(500))
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn update_spend_view<'a>(
|
||||
psbt: String,
|
||||
updated: &form::Value<String>,
|
||||
error: Option<&Error>,
|
||||
processing: bool,
|
||||
) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.push(warn(error))
|
||||
.push(card::simple(
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.push(
|
||||
Row::new()
|
||||
.push(text("PSBT:").bold().width(Length::Fill))
|
||||
.push(
|
||||
button::border(Some(icon::clipboard_icon()), "Copy")
|
||||
.on_press(Message::Clipboard(psbt)),
|
||||
)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.push(separation().width(Length::Fill))
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.push(text("Insert updated PSBT:").bold())
|
||||
.push(
|
||||
form::Form::new("PSBT", updated, move |msg| {
|
||||
Message::ImportSpend(ImportSpendMessage::PsbtEdited(msg))
|
||||
})
|
||||
.warning("Please enter the correct base64 encoded PSBT")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.push(Row::new().push(Space::with_width(Length::Fill)).push(
|
||||
if updated.valid && !updated.value.is_empty() && !processing {
|
||||
button::primary(None, "Update")
|
||||
.on_press(Message::ImportSpend(ImportSpendMessage::Confirm))
|
||||
} else if processing {
|
||||
button::primary(None, "Processing...")
|
||||
} else {
|
||||
button::primary(None, "Update")
|
||||
},
|
||||
)),
|
||||
),
|
||||
))
|
||||
.max_width(400)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn update_spend_success_view<'a>() -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.push(
|
||||
card::simple(Container::new(
|
||||
text("Spend transaction is updated").style(color::GREEN),
|
||||
))
|
||||
.padding(50),
|
||||
)
|
||||
.width(Length::Units(400))
|
||||
.align_items(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
@ -770,7 +770,7 @@ pub fn update_spend_view<'a>(
|
||||
.spacing(10)
|
||||
.push(text("Insert updated PSBT:").bold())
|
||||
.push(
|
||||
form::Form::new("PSBT", updated, move |msg| {
|
||||
form::Form::new_trimmed("PSBT", updated, move |msg| {
|
||||
Message::ImportSpend(ImportSpendMessage::PsbtEdited(msg))
|
||||
})
|
||||
.warning("Please enter the correct base64 encoded PSBT")
|
||||
|
||||
@ -27,7 +27,7 @@ pub fn import_psbt_view<'a>(
|
||||
.spacing(10)
|
||||
.push(text("Insert PSBT:").bold())
|
||||
.push(
|
||||
form::Form::new("PSBT", imported, move |msg| {
|
||||
form::Form::new_trimmed("PSBT", imported, move |msg| {
|
||||
Message::ImportSpend(ImportSpendMessage::PsbtEdited(msg))
|
||||
})
|
||||
.warning("Please enter a base64 encoded PSBT")
|
||||
|
||||
@ -66,7 +66,7 @@ pub fn recovery<'a>(
|
||||
.push(text("Destination").bold())
|
||||
.push(
|
||||
Container::new(
|
||||
form::Form::new("Address", address, move |msg| {
|
||||
form::Form::new_trimmed("Address", address, move |msg| {
|
||||
Message::CreateSpend(CreateSpendMessage::RecipientEdited(
|
||||
0, "address", msg,
|
||||
))
|
||||
@ -81,7 +81,7 @@ pub fn recovery<'a>(
|
||||
.push(text("Feerate").bold())
|
||||
.push(
|
||||
Container::new(
|
||||
form::Form::new("42 (sats/vbyte)", feerate, move |msg| {
|
||||
form::Form::new_trimmed("42 (sats/vbyte)", feerate, move |msg| {
|
||||
Message::CreateSpend(CreateSpendMessage::FeerateEdited(msg))
|
||||
})
|
||||
.warning("Invalid feerate")
|
||||
|
||||
@ -247,7 +247,7 @@ pub fn bitcoind_edit<'a>(
|
||||
Column::new()
|
||||
.push(text("Cookie file path:").bold().small())
|
||||
.push(
|
||||
form::Form::new("Cookie file path", cookie_path, |value| {
|
||||
form::Form::new_trimmed("Cookie file path", cookie_path, |value| {
|
||||
SettingsEditMessage::FieldEdited("cookie_file_path", value)
|
||||
})
|
||||
.warning("Please enter a valid filesystem path")
|
||||
@ -260,7 +260,7 @@ pub fn bitcoind_edit<'a>(
|
||||
Column::new()
|
||||
.push(text("Socket address:").bold().small())
|
||||
.push(
|
||||
form::Form::new("Socket address:", addr, |value| {
|
||||
form::Form::new_trimmed("Socket address:", addr, |value| {
|
||||
SettingsEditMessage::FieldEdited("socket_address", value)
|
||||
})
|
||||
.warning("Please enter a valid address")
|
||||
@ -456,7 +456,7 @@ pub fn rescan<'a>(
|
||||
Row::new()
|
||||
.push(text("Year:").bold().small())
|
||||
.push(
|
||||
form::Form::new("2022", year, |value| {
|
||||
form::Form::new_trimmed("2022", year, |value| {
|
||||
SettingsEditMessage::FieldEdited("rescan_year", value)
|
||||
})
|
||||
.size(20)
|
||||
@ -464,7 +464,7 @@ pub fn rescan<'a>(
|
||||
)
|
||||
.push(text("Month:").bold().small())
|
||||
.push(
|
||||
form::Form::new("12", month, |value| {
|
||||
form::Form::new_trimmed("12", month, |value| {
|
||||
SettingsEditMessage::FieldEdited("rescan_month", value)
|
||||
})
|
||||
.size(20)
|
||||
@ -472,7 +472,7 @@ pub fn rescan<'a>(
|
||||
)
|
||||
.push(text("Day:").bold().small())
|
||||
.push(
|
||||
form::Form::new("31", day, |value| {
|
||||
form::Form::new_trimmed("31", day, |value| {
|
||||
SettingsEditMessage::FieldEdited("rescan_day", value)
|
||||
})
|
||||
.size(20)
|
||||
|
||||
@ -141,9 +141,13 @@ pub fn create_spend_tx<'a>(
|
||||
.push(Container::new(p1_bold("Feerate")).padding(10))
|
||||
.spacing(10)
|
||||
.push(
|
||||
form::Form::new("42 (in sats/vbyte)", feerate, move |msg| {
|
||||
Message::CreateSpend(CreateSpendMessage::FeerateEdited(msg))
|
||||
})
|
||||
form::Form::new_trimmed(
|
||||
"42 (in sats/vbyte)",
|
||||
feerate,
|
||||
move |msg| {
|
||||
Message::CreateSpend(CreateSpendMessage::FeerateEdited(msg))
|
||||
},
|
||||
)
|
||||
.warning("Invalid feerate")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
@ -265,7 +269,7 @@ pub fn recipient_view<'a>(
|
||||
.width(Length::Fixed(80.0)),
|
||||
)
|
||||
.push(
|
||||
form::Form::new("Address", address, move |msg| {
|
||||
form::Form::new_trimmed("Address", address, move |msg| {
|
||||
CreateSpendMessage::RecipientEdited(index, "address", msg)
|
||||
})
|
||||
.warning("Invalid address (maybe it is for another network?)")
|
||||
@ -284,7 +288,7 @@ pub fn recipient_view<'a>(
|
||||
.width(Length::Fixed(80.0)),
|
||||
)
|
||||
.push(
|
||||
form::Form::new("0.001 (in BTC)", amount, move |msg| {
|
||||
form::Form::new_trimmed("0.001 (in BTC)", amount, move |msg| {
|
||||
CreateSpendMessage::RecipientEdited(index, "amount", msg)
|
||||
})
|
||||
.warning(
|
||||
|
||||
@ -388,7 +388,7 @@ pub fn import_descriptor<'a>(
|
||||
let col_descriptor = Column::new()
|
||||
.push(text("Descriptor:").bold())
|
||||
.push(
|
||||
form::Form::new("Descriptor", imported_descriptor, |msg| {
|
||||
form::Form::new_trimmed("Descriptor", imported_descriptor, |msg| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(msg))
|
||||
})
|
||||
.warning("Incompatible descriptor.")
|
||||
@ -807,7 +807,7 @@ pub fn define_bitcoin<'a>(
|
||||
let col_address = Column::new()
|
||||
.push(text("Address:").bold())
|
||||
.push(
|
||||
form::Form::new("Address", address, |msg| {
|
||||
form::Form::new_trimmed("Address", address, |msg| {
|
||||
Message::DefineBitcoind(message::DefineBitcoind::AddressEdited(msg))
|
||||
})
|
||||
.warning("Please enter correct address")
|
||||
@ -819,7 +819,7 @@ pub fn define_bitcoin<'a>(
|
||||
let col_cookie = Column::new()
|
||||
.push(text("Cookie path:").bold())
|
||||
.push(
|
||||
form::Form::new("Cookie path", cookie_path, |msg| {
|
||||
form::Form::new_trimmed("Cookie path", cookie_path, |msg| {
|
||||
Message::DefineBitcoind(message::DefineBitcoind::CookiePathEdited(msg))
|
||||
})
|
||||
.warning("Please enter correct path")
|
||||
@ -1342,7 +1342,7 @@ pub fn edit_key_modal<'a>(
|
||||
.push(
|
||||
Row::new()
|
||||
.push(
|
||||
form::Form::new("Extended public key", form_xpub, |msg| {
|
||||
form::Form::new_trimmed("Extended public key", form_xpub, |msg| {
|
||||
Message::DefineDescriptor(
|
||||
message::DefineDescriptor::KeyModal(
|
||||
message::ImportKeyModal::XPubEdited(msg),
|
||||
@ -1456,7 +1456,7 @@ pub fn edit_sequence_modal<'a>(sequence: &form::Value<String>) -> Element<'a, Me
|
||||
Row::new()
|
||||
.push(
|
||||
Container::new(
|
||||
form::Form::new("ex: 1000", sequence, |v| {
|
||||
form::Form::new_trimmed("ex: 1000", sequence, |v| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::SequenceModal(
|
||||
message::SequenceModal::SequenceEdited(v),
|
||||
))
|
||||
|
||||
@ -44,6 +44,24 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`Form`] that trims input values before applying the `on_change` function.
|
||||
///
|
||||
/// It expects:
|
||||
/// - a placeholder
|
||||
/// - the current value
|
||||
/// - a function that produces a message when the [`Form`] changes
|
||||
pub fn new_trimmed<F>(placeholder: &str, value: &Value<String>, on_change: F) -> Self
|
||||
where
|
||||
F: 'static + Fn(String) -> Message,
|
||||
{
|
||||
Self {
|
||||
input: text_input::TextInput::new(placeholder, &value.value)
|
||||
.on_input(move |s| on_change(s.trim().to_string())),
|
||||
warning: None,
|
||||
valid: value.valid,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the [`Form`] with a warning message
|
||||
pub fn warning(mut self, warning: &'a str) -> Self {
|
||||
self.warning = Some(warning);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user