From 17669547ed8716c5bba08f73067c74ba7cb9c0ab Mon Sep 17 00:00:00 2001 From: edouard Date: Tue, 29 Nov 2022 22:47:08 +0100 Subject: [PATCH] Add loader error message and retry button Refac notification::warning component to use a Collapse instead of Tooltip. Add a retry button to start again Liana. close #134 --- gui/src/app/error.rs | 2 +- gui/src/app/view/warning.rs | 2 +- gui/src/daemon/embedded.rs | 7 +- gui/src/daemon/mod.rs | 5 +- gui/src/loader.rs | 104 ++++++++++++++++++++++----- gui/src/ui/component/button.rs | 2 +- gui/src/ui/component/notification.rs | 87 +++++++++++----------- gui/src/ui/icon.rs | 4 ++ 8 files changed, 144 insertions(+), 69 deletions(-) diff --git a/gui/src/app/error.rs b/gui/src/app/error.rs index 438f9ca2..11013c56 100644 --- a/gui/src/app/error.rs +++ b/gui/src/app/error.rs @@ -3,7 +3,7 @@ use liana::config::ConfigError; use std::convert::From; use std::io::ErrorKind; -#[derive(Debug, Clone)] +#[derive(Debug)] pub enum Error { Config(String), Daemon(DaemonError), diff --git a/gui/src/app/view/warning.rs b/gui/src/app/view/warning.rs index 74120573..9702b2a4 100644 --- a/gui/src/app/view/warning.rs +++ b/gui/src/app/view/warning.rs @@ -44,7 +44,7 @@ impl std::fmt::Display for WarningMessage { } } -pub fn warn<'a, T: 'a>(error: Option<&Error>) -> widget::Container<'a, T> { +pub fn warn<'a, T: 'a + Clone>(error: Option<&Error>) -> widget::Container<'a, T> { if let Some(w) = error { let message: WarningMessage = w.into(); notification::warning(message.to_string(), w.to_string()).width(Length::Fill) diff --git a/gui/src/daemon/embedded.rs b/gui/src/daemon/embedded.rs index e882ee4d..ab75e9d5 100644 --- a/gui/src/daemon/embedded.rs +++ b/gui/src/daemon/embedded.rs @@ -22,8 +22,8 @@ impl EmbeddedDaemon { } pub fn start(&mut self) -> Result<(), DaemonError> { - let handle = DaemonHandle::start_default(self.config.clone()) - .map_err(|e| DaemonError::Start(e.to_string()))?; + let handle = + DaemonHandle::start_default(self.config.clone()).map_err(DaemonError::Start)?; self.handle = Some(RwLock::new(handle)); Ok(()) } @@ -45,8 +45,7 @@ impl Daemon for EmbeddedDaemon { return Ok(()); } - let next = - DaemonHandle::start_default(cfg).map_err(|e| DaemonError::Start(e.to_string()))?; + let next = DaemonHandle::start_default(cfg).map_err(DaemonError::Start)?; self.handle.take().unwrap().into_inner().unwrap().shutdown(); self.handle = Some(RwLock::new(next)); Ok(()) diff --git a/gui/src/daemon/mod.rs b/gui/src/daemon/mod.rs index cd8de64c..e7db4b71 100644 --- a/gui/src/daemon/mod.rs +++ b/gui/src/daemon/mod.rs @@ -9,9 +9,10 @@ use std::io::ErrorKind; use liana::{ config::Config, miniscript::bitcoin::{util::psbt::Psbt, Address, OutPoint, Txid}, + StartupError, }; -#[derive(Debug, Clone)] +#[derive(Debug)] pub enum DaemonError { /// Something was wrong with the request. Rpc(i32, String), @@ -22,7 +23,7 @@ pub enum DaemonError { /// No response. NoAnswer, // Error at start up. - Start(String), + Start(StartupError), } impl std::fmt::Display for DaemonError { diff --git a/gui/src/loader.rs b/gui/src/loader.rs index 55662345..83a75889 100644 --- a/gui/src/loader.rs +++ b/gui/src/loader.rs @@ -4,21 +4,27 @@ use std::path::PathBuf; use std::sync::Arc; use iced::{ - widget::{Column, Text}, + widget::{Column, Container, ProgressBar}, Element, }; -use iced::{Alignment, Command, Subscription}; +use iced::{Alignment, Command, Length, Subscription}; use iced_native::{window, Event}; use log::{debug, info}; use liana::{ config::{Config, ConfigError}, miniscript::bitcoin, + StartupError, }; use crate::{ app::config::{default_datadir, Config as GUIConfig}, daemon::{client, embedded::EmbeddedDaemon, model::*, Daemon, DaemonError}, + ui::{ + component::{button, notification, text::*}, + icon, + util::Collection, + }, }; type Lianad = client::Lianad; @@ -27,11 +33,12 @@ pub struct Loader { pub gui_config: GUIConfig, pub daemon_started: bool, + daemon_config: Config, should_exit: bool, step: Step, } -enum Step { +pub enum Step { Connecting, StartingDaemon, Syncing { @@ -43,6 +50,7 @@ enum Step { #[derive(Debug)] pub enum Message { + View(ViewMessage), Event(iced_native::Event), Syncing(Result), Synced( @@ -65,6 +73,7 @@ impl Loader { .unwrap(); ( Loader { + daemon_config: daemon_config.clone(), gui_config, step: Step::Connecting, should_exit: false, @@ -177,6 +186,11 @@ impl Loader { pub fn update(&mut self, message: Message) -> Command { match message { + Message::View(ViewMessage::Retry) => { + let (loader, cmd) = Self::new(self.gui_config.clone(), self.daemon_config.clone()); + *self = loader; + cmd + } Message::Started(res) => self.on_start(res), Message::Loaded(res) => self.on_load(res), Message::Syncing(res) => self.on_sync(res), @@ -201,23 +215,78 @@ impl Loader { } pub fn view(&self) -> Element { - match &self.step { - Step::StartingDaemon => cover(Text::new("Starting daemon...")), - Step::Connecting => cover(Text::new("Connecting to daemon...")), - Step::Syncing { progress, .. } => cover(Text::new(format!("Syncing... {}%", progress))), - Step::Error(error) => cover(Text::new(format!("Error: {}", error))), - } + view(&self.step).map(Message::View) } } -pub fn cover<'a, T: 'a, C: Into>>(content: C) -> Element<'a, T> { +#[derive(Clone, Debug)] +pub enum ViewMessage { + Retry, +} + +pub fn view(step: &Step) -> Element { + match &step { + Step::StartingDaemon => cover( + None, + Column::new() + .width(Length::Fill) + .push(ProgressBar::new(0.0..=1.0, 0.0).width(Length::Fill)) + .push(text("Starting daemon...")), + ), + Step::Connecting => cover( + None, + Column::new() + .width(Length::Fill) + .push(ProgressBar::new(0.0..=1.0, 0.0).width(Length::Fill)) + .push(text("Connecting to daemon...")), + ), + Step::Syncing { progress, .. } => cover( + None, + Column::new() + .width(Length::Fill) + .push(ProgressBar::new(0.0..=1.0, *progress as f32).width(Length::Fill)) + .push(text("Syncing the wallet with the blockchain...")), + ), + Step::Error(error) => cover( + Some(("Error while starting the internal daemon", error)), + Column::new() + .spacing(20) + .width(Length::Fill) + .align_items(Alignment::Center) + .push(icon::plug_icon().size(100).width(Length::Units(300))) + .push( + if matches!( + error.as_ref(), + Error::Daemon(DaemonError::Start(StartupError::Bitcoind(_))) + ) { + text("Liana failed to start, please check if bitcoind is running") + } else { + text("Liana failed to start") + }, + ) + .push( + button::primary(None, "Retry") + .width(Length::Units(200)) + .on_press(ViewMessage::Retry), + ), + ), + } +} + +pub fn cover<'a, T: 'a + Clone, C: Into>>( + warn: Option<(&'static str, &Error)>, + content: C, +) -> Element<'a, T> { Column::new() - .push(content) - .width(iced::Length::Fill) - .height(iced::Length::Fill) - .padding(50) - .spacing(50) - .align_items(Alignment::Center) + .push_maybe(warn.map(|w| notification::warning(w.0.to_string(), w.1.to_string()))) + .push( + Container::new(content) + .width(iced::Length::Fill) + .height(iced::Length::Fill) + .center_x() + .center_y() + .padding(50), + ) .into() } @@ -239,8 +308,7 @@ async fn connect( pub async fn start_daemon(config_path: PathBuf) -> Result, Error> { debug!("starting liana daemon"); - let config = Config::from_file(Some(config_path)) - .map_err(|e| DaemonError::Start(format!("Error parsing config: {}", e)))?; + let config = Config::from_file(Some(config_path)).map_err(Error::Config)?; let mut daemon = EmbeddedDaemon::new(config); daemon.start()?; diff --git a/gui/src/ui/component/button.rs b/gui/src/ui/component/button.rs index 8f537114..8b7e63ca 100644 --- a/gui/src/ui/component/button.rs +++ b/gui/src/ui/component/button.rs @@ -112,7 +112,7 @@ impl button::StyleSheet for Style { }, Style::Transparent => button::Appearance { shadow_offset: Vector::default(), - background: color::BACKGROUND.into(), + background: Color::TRANSPARENT.into(), border_radius: 10.0, border_width: 0.0, border_color: Color::TRANSPARENT, diff --git a/gui/src/ui/component/notification.rs b/gui/src/ui/component/notification.rs index 92af34e5..feedc1f8 100644 --- a/gui/src/ui/component/notification.rs +++ b/gui/src/ui/component/notification.rs @@ -1,23 +1,52 @@ -use crate::ui::{color, icon}; +use crate::ui::{ + color, + component::{button, collapse, text::*}, + icon, +}; use iced::{ - widget::{container, tooltip, Container, Row, Text, Tooltip}, - Length, + widget::{container, Button, Container, Row}, + Alignment, Element, Length, }; -pub fn warning<'a, T: 'a>(message: String, error: String) -> Container<'a, T> { - Container::new(Container::new( - Tooltip::new( - Row::new() - .push(icon::warning_icon()) - .push(Text::new(message)) - .spacing(20), - error, - tooltip::Position::Bottom, - ) - .style(TooltipWarningStyle), - )) +pub fn warning<'a, T: 'a + Clone>(message: String, error: String) -> Container<'a, T> { + let message_clone = message.clone(); + Container::new(Container::new(collapse::Collapse::new( + move || { + Button::new( + Row::new() + .push( + Container::new(text(message_clone.to_string()).small().bold()) + .width(Length::Fill), + ) + .push( + Row::new() + .align_items(Alignment::Center) + .spacing(10) + .push(text("Learn more").small().bold()) + .push(icon::collapse_icon()), + ), + ) + .style(button::Style::Transparent.into()) + }, + move || { + Button::new( + Row::new() + .push( + Container::new(text(message.to_owned()).small().bold()).width(Length::Fill), + ) + .push( + Row::new() + .align_items(Alignment::Center) + .spacing(10) + .push(text("Learn more").small().bold()) + .push(icon::collapsed_icon()), + ), + ) + .style(button::Style::Transparent.into()) + }, + move || Element::<'a, T>::from(text(error.to_owned()).small()), + ))) .padding(15) - .center_x() .style(WarningStyle) .width(Length::Fill) } @@ -47,29 +76,3 @@ impl From for iced::theme::Container { iced::theme::Container::Custom(i.into()) } } - -pub struct TooltipWarningStyle; -impl container::StyleSheet for TooltipWarningStyle { - type Style = iced::Theme; - fn appearance(&self, _style: &Self::Style) -> container::Appearance { - container::Appearance { - border_radius: 0.0, - border_width: 1.0, - text_color: color::WARNING.into(), - background: color::FOREGROUND.into(), - border_color: color::WARNING, - } - } -} - -impl From for Box> { - fn from(s: TooltipWarningStyle) -> Box> { - Box::new(s) - } -} - -impl From for iced::theme::Container { - fn from(i: TooltipWarningStyle) -> iced::theme::Container { - iced::theme::Container::Custom(i.into()) - } -} diff --git a/gui/src/ui/icon.rs b/gui/src/ui/icon.rs index 50b7aee6..f740f911 100644 --- a/gui/src/ui/icon.rs +++ b/gui/src/ui/icon.rs @@ -13,6 +13,10 @@ fn icon(unicode: char) -> Text<'static> { .size(20) } +pub fn plug_icon() -> Text<'static> { + icon('\u{F4F6}') +} + pub fn reload_icon() -> Text<'static> { icon('\u{F130}') }