From 2d53f70416e7ab490f1e129bf5ccbcbc03a22553 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Fri, 17 May 2024 15:34:44 -0500 Subject: [PATCH] add a prettier txid output to receive success --- assets/icons/restart.svg | 4 + src/components/button.rs | 34 ++--- src/components/icon.rs | 11 +- src/components/layout.rs | 12 +- src/components/mini_copy.rs | 39 +++++ src/components/mod.rs | 3 + src/components/screen_header.rs | 2 +- src/routes/receive.rs | 243 +++++++++++++++++++------------- 8 files changed, 217 insertions(+), 131 deletions(-) create mode 100644 assets/icons/restart.svg create mode 100644 src/components/mini_copy.rs diff --git a/assets/icons/restart.svg b/assets/icons/restart.svg new file mode 100644 index 0000000..a01bcae --- /dev/null +++ b/assets/icons/restart.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/button.rs b/src/components/button.rs index 53b1dac..d14c7bc 100644 --- a/src/components/button.rs +++ b/src/components/button.rs @@ -1,7 +1,7 @@ use iced::{ widget::{ button::{self, Status}, - center, horizontal_space, row, text, Button, Svg, + center, horizontal_space, row, text, Button, }, Border, Color, Element, Length, Shadow, Theme, }; @@ -12,16 +12,13 @@ use super::{darken, lighten, map_icon, the_spinner, SvgIcon}; pub fn h_button(text_str: &str, icon: SvgIcon, loading: bool) -> Button<'_, Message, Theme> { let spinner: Element<'static, Message, Theme> = the_spinner(); - let svg: Svg<'_, Theme> = map_icon(icon); + let svg = map_icon(icon, 24., 24.); let content = if loading { row![spinner].align_items(iced::Alignment::Center) } else { - row![ - svg.width(Length::Fixed(24.)).height(Length::Fixed(24.)), - text(text_str).size(24.) - ] - .align_items(iced::Alignment::Center) - .spacing(16) + row![svg, text(text_str).size(24.)] + .align_items(iced::Alignment::Center) + .spacing(16) }; Button::new(center(content)) @@ -62,22 +59,11 @@ pub fn sidebar_button( active_route: Route, ) -> Button<'_, Message, Theme> { let is_active = self_route == active_route; - let svg: Svg<'_, Theme> = map_icon(icon); - let content = row!( - svg.width(Length::Fixed(24.)).height(Length::Fixed(24.)), - text(text_str).size(24.), - horizontal_space(), - // .font(Font { - - // family: iced::font::Family::default(), - // weight: iced::font::Weight::Bold, - // stretch: iced::font::Stretch::Normal, - // style: iced::font::Style::Normal, - // }) - ) - .align_items(iced::Alignment::Center) - .spacing(16) - .padding(16); + let svg = map_icon(icon, 24., 24.); + let content = row!(svg, text(text_str).size(24.), horizontal_space(),) + .align_items(iced::Alignment::Center) + .spacing(16) + .padding(16); Button::new(content) .style(move |theme, status| { diff --git a/src/components/icon.rs b/src/components/icon.rs index 3960839..f6a6467 100644 --- a/src/components/icon.rs +++ b/src/components/icon.rs @@ -1,4 +1,6 @@ -use iced::{widget::Svg, Theme}; +use iced::{widget::Svg, Element, Theme}; + +use crate::Message; pub enum SvgIcon { ChevronDown, @@ -13,9 +15,10 @@ pub enum SvgIcon { Copy, Plus, Qr, + Restart, } -pub fn map_icon(icon: SvgIcon) -> Svg<'static, Theme> { +pub fn map_icon(icon: SvgIcon, width: f32, height: f32) -> Element<'static, Message, Theme> { match icon { SvgIcon::ChevronDown => Svg::from_path("assets/icons/chevron_down.svg"), SvgIcon::DownLeft => Svg::from_path("assets/icons/down_left.svg"), @@ -29,5 +32,9 @@ pub fn map_icon(icon: SvgIcon) -> Svg<'static, Theme> { SvgIcon::Copy => Svg::from_path("assets/icons/copy.svg"), SvgIcon::Plus => Svg::from_path("assets/icons/plus.svg"), SvgIcon::Qr => Svg::from_path("assets/icons/qr.svg"), + SvgIcon::Restart => Svg::from_path("assets/icons/restart.svg"), } + .width(width) + .height(height) + .into() } diff --git a/src/components/layout.rs b/src/components/layout.rs index 54c25de..45a5ec4 100644 --- a/src/components/layout.rs +++ b/src/components/layout.rs @@ -1,4 +1,4 @@ -use iced::widget::{container, scrollable, Column}; +use iced::widget::{container, horizontal_space, row, scrollable, Column}; use iced::Length; use iced::{Element, Padding}; @@ -6,8 +6,14 @@ use crate::Message; pub fn basic_layout(column: Column) -> Element { container( - scrollable(column.width(Length::Fixed(512.)).padding(Padding::new(48.))) - .height(Length::Fill), + scrollable(row![ + column + .width(Length::Fixed(512.)) + .padding(Padding::new(48.)) + .max_width(512), + horizontal_space(), + ]) + .height(Length::Fill), ) .into() } diff --git a/src/components/mini_copy.rs b/src/components/mini_copy.rs new file mode 100644 index 0000000..6d9590f --- /dev/null +++ b/src/components/mini_copy.rs @@ -0,0 +1,39 @@ +use iced::{ + widget::{ + button::{self, Status}, Button, + }, + Border, Color, Length, Shadow, Theme, +}; + +use crate::Message; + +use super::{darken, lighten, map_icon, SvgIcon}; + +pub fn mini_copy(text: String) -> Button<'static, Message, Theme> { + let icon = map_icon(SvgIcon::Copy, 24., 24.); + + Button::new(icon) + .on_press(Message::CopyToClipboard(text.to_string())) + .style(|theme: &Theme, status| { + let border = Border { + color: Color::WHITE, + width: 0., + radius: (8.).into(), + }; + + let background = match status { + Status::Hovered => lighten(theme.palette().background, 0.1), + Status::Pressed => darken(Color::BLACK, 0.1), + _ => theme.palette().background, + }; + button::Style { + background: Some(background.into()), + text_color: Color::WHITE, + border, + shadow: Shadow::default(), + } + }) + .padding(6) + .width(Length::Fixed(32.)) + .height(Length::Fixed(32.)) +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 167aa16..bdf9004 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -39,3 +39,6 @@ pub use rules::*; mod layout; pub use layout::*; + +mod mini_copy; +pub use mini_copy::*; diff --git a/src/components/screen_header.rs b/src/components/screen_header.rs index e8eb19f..7290b7a 100644 --- a/src/components/screen_header.rs +++ b/src/components/screen_header.rs @@ -10,7 +10,7 @@ use super::{hr, map_icon, vr, FederationItem, SvgIcon}; pub fn h_screen_header(harbor: &HarborWallet, show_balance: bool) -> Element { if let Some(item) = harbor.federation_list.first() { let FederationItem { name, id: _id } = item; - let people_icon = map_icon(SvgIcon::People).width(24).height(24); + let people_icon = map_icon(SvgIcon::People, 24., 24.); let current_federation = row![people_icon, text(name).size(24)] .align_items(Alignment::Center) .spacing(16) diff --git a/src/routes/receive.rs b/src/routes/receive.rs index 90d6981..26e9be8 100644 --- a/src/routes/receive.rs +++ b/src/routes/receive.rs @@ -1,120 +1,161 @@ use iced::widget::container::Style; -use iced::widget::{column, container, qr_code, radio, text}; +use iced::widget::{column, container, qr_code, radio, row, text}; use iced::Color; use iced::{Border, Element, Font}; +use crate::bridge::ReceiveSuccessMsg; use crate::components::{ - basic_layout, h_button, h_caption_text, h_header, h_input, h_screen_header, SvgIcon, + basic_layout, h_button, h_caption_text, h_header, h_input, h_screen_header, mini_copy, SvgIcon, }; use crate::{HarborWallet, Message, ReceiveMethod, ReceiveStatus}; pub fn receive(harbor: &HarborWallet) -> Element { - let lightning_choice = radio( - "Lightning", - ReceiveMethod::Lightning, - Some(harbor.receive_method), - Message::ReceiveMethodChanged, - ) - .text_size(18); - - let lightning_caption = h_caption_text("Good for small amounts. Instant settlement, low fees."); - - let lightning = column![lightning_choice, lightning_caption,].spacing(8); - - let onchain_choice = radio( - "On-chain", - ReceiveMethod::OnChain, - Some(harbor.receive_method), - Message::ReceiveMethodChanged, - ) - .text_size(18); - - let onchain_caption = h_caption_text( - "Good for large amounts. Requires on-chain fees and 10 block confirmations.", - ); - - let onchain = column![onchain_choice, onchain_caption,].spacing(8); - - let method_choice_label = text("Receive method").size(24); - - let method_choice = column![method_choice_label, lightning, onchain].spacing(16); - let receive_string = harbor .receive_invoice .as_ref() .map(|i| i.to_string()) .or_else(|| harbor.receive_address.as_ref().map(|a| a.to_string())); - let success_message = harbor.receive_success_msg.as_ref().map(|r| { - text(format!("Success: {r:?}")) - .size(32) - .color(Color::from_rgb(0., 255., 0.)) - }); - - let column = if let Some(success_message) = success_message { - let header = h_header("Success!", "You did a good job!"); - let reset_button = - h_button("Start over", SvgIcon::Squirrel, false).on_press(Message::ReceiveStateReset); - column![header, success_message, reset_button] - } else if let Some(string) = receive_string { - let header = h_header("Receive", "Scan this QR or copy the string."); - - let data = harbor.receive_qr_data.as_ref().unwrap(); - let qr_code = qr_code(data).style(|_theme| iced::widget::qr_code::Style { - background: Color::WHITE, - cell: Color::BLACK, - }); - let qr_container = container(qr_code).padding(16).style(|_theme| Style { - background: Some(iced::Background::Color(Color::WHITE)), - border: Border { - radius: (8.).into(), - ..Border::default() - }, - ..Style::default() - }); - - let first_20_chars = string.chars().take(20).collect::(); - - column![ - header, - qr_container, - text(format!("{first_20_chars}...")).size(16).font(Font { - family: iced::font::Family::Monospace, - weight: iced::font::Weight::Normal, - stretch: iced::font::Stretch::Normal, - style: iced::font::Style::Normal, - }), - h_button("Copy to clipboard", SvgIcon::Copy, false) - .on_press(Message::CopyToClipboard(string)), - h_button("Start over", SvgIcon::Squirrel, false).on_press(Message::ReceiveStateReset), - ] - } else { - let header = h_header("Receive", "Receive on-chain or via lightning."); - - let amount_input = h_input( - "Amount", - "420", - &harbor.receive_amount_str, - Message::ReceiveAmountChanged, - None, - false, - None, - Some("sats"), - ); - - let generating = harbor.receive_status == ReceiveStatus::Generating; - - let generate_button = h_button("Generate Invoice", SvgIcon::Qr, generating) - .on_press(Message::GenerateInvoice); - - let generate_address_button = h_button("Generate Address", SvgIcon::Qr, generating) - .on_press(Message::GenerateAddress); - - match harbor.receive_method { - ReceiveMethod::Lightning => { - column![header, method_choice, amount_input, generate_button] + let reset_button = + h_button("Start over", SvgIcon::Restart, false).on_press(Message::ReceiveStateReset); + + let bold_font = Font { + family: iced::font::Family::Monospace, + weight: iced::font::Weight::Bold, + stretch: iced::font::Stretch::Normal, + style: iced::font::Style::Normal, + }; + + let mono_font = Font { + family: iced::font::Family::Monospace, + weight: iced::font::Weight::Normal, + stretch: iced::font::Stretch::Normal, + style: iced::font::Style::Normal, + }; + + let column = match (&harbor.receive_success_msg, receive_string) { + // Starting state + (None, None) => { + let header = h_header("Receive", "Receive on-chain or via lightning."); + + let lightning_choice = radio( + "Lightning", + ReceiveMethod::Lightning, + Some(harbor.receive_method), + Message::ReceiveMethodChanged, + ) + .text_size(18); + let lightning_caption = + h_caption_text("Good for small amounts. Instant settlement, low fees."); + let lightning = column![lightning_choice, lightning_caption,].spacing(8); + + let onchain_choice = radio( + "On-chain", + ReceiveMethod::OnChain, + Some(harbor.receive_method), + Message::ReceiveMethodChanged, + ) + .text_size(18); + let onchain_caption = h_caption_text( + "Good for large amounts. Requires on-chain fees and 10 block confirmations.", + ); + let onchain = column![onchain_choice, onchain_caption,].spacing(8); + + let method_choice_label = text("Receive method").size(24); + let method_choice = column![method_choice_label, lightning, onchain].spacing(16); + + let amount_input = h_input( + "Amount", + "420", + &harbor.receive_amount_str, + Message::ReceiveAmountChanged, + None, + false, + None, + Some("sats"), + ); + + let generating = harbor.receive_status == ReceiveStatus::Generating; + + let generate_button = h_button("Generate Invoice", SvgIcon::Qr, generating) + .on_press(Message::GenerateInvoice); + + let generate_address_button = h_button("Generate Address", SvgIcon::Qr, generating) + .on_press(Message::GenerateAddress); + + match harbor.receive_method { + ReceiveMethod::Lightning => { + column![header, method_choice, amount_input, generate_button] + } + ReceiveMethod::OnChain => column![header, method_choice, generate_address_button], } - ReceiveMethod::OnChain => column![header, method_choice, generate_address_button], + } + // We've generated an invoice or address + (None, Some(receive_string)) => { + let header = h_header("Receive", "Scan this QR or copy the string."); + + let data = harbor.receive_qr_data.as_ref().unwrap(); + let qr_code = qr_code(data).style(|_theme| iced::widget::qr_code::Style { + background: Color::WHITE, + cell: Color::BLACK, + }); + let qr_container = container(qr_code).padding(16).style(|_theme| Style { + background: Some(iced::Background::Color(Color::WHITE)), + border: Border { + radius: (8.).into(), + ..Border::default() + }, + ..Style::default() + }); + + let first_20_chars = receive_string.chars().take(20).collect::(); + + column![ + header, + qr_container, + text(format!("{first_20_chars}...")).size(16).font(Font { + family: iced::font::Family::Monospace, + weight: iced::font::Weight::Normal, + stretch: iced::font::Stretch::Normal, + style: iced::font::Style::Normal, + }), + h_button("Copy to clipboard", SvgIcon::Copy, false) + .on_press(Message::CopyToClipboard(receive_string)), + reset_button + ] + } + // Success states + (Some(ReceiveSuccessMsg::Lightning), _) => { + let header = h_header("Got it", "Payment received"); + + // TODO: should have some info here we can show like amount, fee, etc. + + column![header, reset_button] + } + (Some(ReceiveSuccessMsg::Onchain { txid }), _) => { + let txid_str = txid.to_string(); + let header = h_header("Got it", "Payment received"); + + let txid_str_shortened = if txid_str.len() > 20 { + // get the first 10 and last 10 chars + let txid_str_start = &txid_str[0..10]; + let txid_str_end = &txid_str[txid_str.len() - 10..]; + + // add ellipsis + format!("{txid_str_start}...{txid_str_end}") + } else { + txid_str.clone() + }; + + let txid = row![ + text("txid").font(bold_font), + text(txid_str_shortened).font(mono_font), + mini_copy(txid_str) + ] + .align_items(iced::Alignment::Center) + .spacing(8); + + column![header, txid, reset_button] } };