From 63be34b3d887d0d39f861b6ae46996d64a245b18 Mon Sep 17 00:00:00 2001 From: edouard Date: Tue, 13 Dec 2022 15:56:33 +0100 Subject: [PATCH] Add confirm network step to gui User is asked which network to start with. This is a required step in case the user start the software without a console and can not set himself the network or launch the install for another non existing network. --- gui/src/launcher.rs | 116 ++++++++++++++++++++++++++++++++ gui/src/lib.rs | 1 + gui/src/loader.rs | 35 +++++++--- gui/src/main.rs | 123 +++++++++++++++++++++++----------- gui/src/ui/component/badge.rs | 7 ++ 5 files changed, 235 insertions(+), 47 deletions(-) create mode 100644 gui/src/launcher.rs diff --git a/gui/src/launcher.rs b/gui/src/launcher.rs new file mode 100644 index 00000000..625fcdd0 --- /dev/null +++ b/gui/src/launcher.rs @@ -0,0 +1,116 @@ +use std::path::PathBuf; + +use iced::{ + widget::{Button, Column, Container, Row}, + Alignment, Element, Length, +}; + +use liana::miniscript::bitcoin::Network; + +use crate::ui::{ + component::{badge, button, text::*}, + icon, +}; + +pub struct Launcher { + should_exit: bool, + choices: Vec, + pub datadir_path: PathBuf, +} + +impl Launcher { + pub fn new(datadir_path: PathBuf) -> Self { + let mut choices = Vec::new(); + for network in [ + Network::Bitcoin, + Network::Testnet, + Network::Signet, + Network::Regtest, + ] { + if datadir_path.join(network.to_string()).exists() { + choices.push(network) + } + } + Self { + datadir_path, + choices, + should_exit: false, + } + } + + pub fn stop(&mut self) { + self.should_exit = true; + } + + pub fn should_exit(&self) -> bool { + self.should_exit + } + + pub fn view(&self) -> Element { + Container::new( + Column::new() + .spacing(30) + .push(text("Welcome back").size(50).bold()) + .push( + self.choices + .iter() + .fold( + Column::new() + .push(text("Select network:").small().bold()) + .spacing(10), + |col, choice| { + col.push( + Button::new( + Row::new() + .spacing(20) + .align_items(Alignment::Center) + .push(badge::Badge::new(icon::bitcoin_icon()).style( + match choice { + Network::Bitcoin => badge::Style::Bitcoin, + _ => badge::Style::Standard, + }, + )) + .push(text(match choice { + Network::Bitcoin => "Bitcoin Mainnet", + Network::Testnet => "Bitcoin Testnet", + Network::Signet => "Bitcoin Signet", + Network::Regtest => "Bitcoin Regtest", + })), + ) + .on_press(Message::Run(*choice)) + .padding(10) + .width(Length::Fill) + .style(button::Style::Border.into()), + ) + }, + ) + .push( + Button::new( + Row::new() + .spacing(20) + .align_items(Alignment::Center) + .push(badge::Badge::new(icon::plus_icon())) + .push(text("Install Liana on another network")), + ) + .on_press(Message::Install) + .padding(10) + .width(Length::Fill) + .style(button::Style::TransparentBorder.into()), + ), + ) + .max_width(500) + .align_items(Alignment::Center), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} + +#[derive(Debug, Clone)] +pub enum Message { + Install, + Run(Network), +} diff --git a/gui/src/lib.rs b/gui/src/lib.rs index 66125b3f..e3d7c750 100644 --- a/gui/src/lib.rs +++ b/gui/src/lib.rs @@ -2,6 +2,7 @@ pub mod app; pub mod daemon; pub mod hw; pub mod installer; +pub mod launcher; pub mod loader; pub mod ui; pub mod utils; diff --git a/gui/src/loader.rs b/gui/src/loader.rs index 854dfd34..12def382 100644 --- a/gui/src/loader.rs +++ b/gui/src/loader.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use std::sync::Arc; use iced::{ - widget::{Column, Container, ProgressBar}, + widget::{Column, Container, ProgressBar, Row}, Element, }; use iced::{Alignment, Command, Length, Subscription}; @@ -30,6 +30,7 @@ use crate::{ type Lianad = client::Lianad; pub struct Loader { + pub datadir_path: Option, pub gui_config: GUIConfig, pub daemon_started: bool, @@ -65,7 +66,11 @@ pub enum Message { } impl Loader { - pub fn new(gui_config: GUIConfig, daemon_config: Config) -> (Self, Command) { + pub fn new( + datadir_path: Option, + gui_config: GUIConfig, + daemon_config: Config, + ) -> (Self, Command) { let path = socket_path( &daemon_config.data_dir, daemon_config.bitcoin_config.network, @@ -73,6 +78,7 @@ impl Loader { .unwrap(); ( Loader { + datadir_path, daemon_config: daemon_config.clone(), gui_config, step: Step::Connecting, @@ -188,7 +194,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()); + let (loader, cmd) = Self::new( + self.datadir_path.clone(), + self.gui_config.clone(), + self.daemon_config.clone(), + ); *self = loader; cmd } @@ -216,16 +226,17 @@ impl Loader { } pub fn view(&self) -> Element { - view(&self.step).map(Message::View) + view(self.datadir_path.as_ref(), &self.step).map(Message::View) } } #[derive(Clone, Debug)] pub enum ViewMessage { Retry, + SwitchNetwork, } -pub fn view(step: &Step) -> Element { +pub fn view<'a>(datadir_path: Option<&'a PathBuf>, step: &'a Step) -> Element<'a, ViewMessage> { match &step { Step::StartingDaemon => cover( None, @@ -266,9 +277,17 @@ pub fn view(step: &Step) -> Element { }, ) .push( - button::primary(None, "Retry") - .width(Length::Units(200)) - .on_press(ViewMessage::Retry), + Row::new() + .spacing(10) + .push_maybe(datadir_path.map(|_| { + button::border(None, "Use another Bitcoin network") + .on_press(ViewMessage::SwitchNetwork) + })) + .push( + button::primary(None, "Retry") + .width(Length::Units(200)) + .on_press(ViewMessage::Retry), + ), ), ), } diff --git a/gui/src/main.rs b/gui/src/main.rs index b2c08533..2246afa2 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -14,6 +14,7 @@ use liana_gui::{ App, }, installer::{self, Installer}, + launcher::{self, Launcher}, loader::{self, Loader}, }; @@ -68,6 +69,7 @@ pub struct GUI { } enum State { + Launcher(Box), Installer(Box), Loader(Box), App(App), @@ -76,6 +78,7 @@ enum State { #[derive(Debug)] pub enum Message { CtrlC, + Launch(Box), Install(Box), Load(Box), Run(Box), @@ -98,15 +101,23 @@ impl Application for GUI { fn title(&self) -> String { match self.state { State::Installer(_) => String::from("Liana Installer"), - State::App(_) => String::from("Liana"), - State::Loader(..) => String::from("Liana"), + _ => String::from("Liana"), } } fn new(config: Config) -> (GUI, Command) { match config { - Config::Install(config_path, network) => { - let (install, command) = Installer::new(config_path, network); + Config::Launcher(datadir_path) => { + let launcher = Launcher::new(datadir_path); + ( + Self { + state: State::Launcher(Box::new(launcher)), + }, + Command::perform(ctrl_c(), |_| Message::CtrlC), + ) + } + Config::Install(datadir_path, network) => { + let (install, command) = Installer::new(datadir_path, network); ( Self { state: State::Installer(Box::new(install)), @@ -117,10 +128,10 @@ impl Application for GUI { ]), ) } - Config::Run(cfg) => { + Config::Run(datadir_path, cfg) => { let daemon_cfg = DaemonConfig::from_file(Some(cfg.daemon_config_path.clone())).unwrap(); - let (loader, command) = Loader::new(cfg, daemon_cfg); + let (loader, command) = Loader::new(datadir_path, cfg, daemon_cfg); ( Self { state: State::Loader(Box::new(loader)), @@ -136,32 +147,55 @@ impl Application for GUI { fn update(&mut self, message: Self::Message) -> Command { match (&mut self.state, message) { - (State::Installer(i), Message::CtrlC) => { - i.stop(); - Command::none() - } - (State::Loader(i), Message::CtrlC) => { - i.stop(); - Command::none() - } - (State::App(i), Message::CtrlC) => { - i.stop(); + (_, Message::CtrlC) => { + match &mut self.state { + State::Loader(s) => s.stop(), + State::Launcher(s) => s.stop(), + State::Installer(s) => s.stop(), + State::App(s) => s.stop(), + } Command::none() } + (State::Launcher(l), Message::Launch(msg)) => match *msg { + launcher::Message::Install => { + let (install, command) = + Installer::new(l.datadir_path.clone(), bitcoin::Network::Bitcoin); + self.state = State::Installer(Box::new(install)); + command.map(|msg| Message::Install(Box::new(msg))) + } + launcher::Message::Run(network) => { + let mut path = l.datadir_path.clone(); + path.push(network.to_string()); + path.push(app::config::DEFAULT_FILE_NAME); + let cfg = app::Config::from_file(&path).unwrap(); + let daemon_cfg = + DaemonConfig::from_file(Some(cfg.daemon_config_path.clone())).unwrap(); + let (loader, command) = + Loader::new(Some(l.datadir_path.clone()), cfg, daemon_cfg); + self.state = State::Loader(Box::new(loader)); + command.map(|msg| Message::Load(Box::new(msg))) + } + }, (State::Installer(i), Message::Install(msg)) => { if let installer::Message::Exit(path) = *msg { let cfg = app::Config::from_file(&path).unwrap(); let daemon_cfg = DaemonConfig::from_file(Some(cfg.daemon_config_path.clone())).unwrap(); - let (loader, command) = Loader::new(cfg, daemon_cfg); + let (loader, command) = Loader::new(None, cfg, daemon_cfg); self.state = State::Loader(Box::new(loader)); command.map(|msg| Message::Load(Box::new(msg))) } else { i.update(*msg).map(|msg| Message::Install(Box::new(msg))) } } - (State::Loader(loader), Message::Load(msg)) => { - if let loader::Message::Synced(info, coins, spend_txs, daemon) = *msg { + (State::Loader(loader), Message::Load(msg)) => match *msg { + loader::Message::View(loader::ViewMessage::SwitchNetwork) => { + self.state = State::Launcher(Box::new(Launcher::new( + loader.datadir_path.clone().unwrap(), + ))); + Command::none() + } + loader::Message::Synced(info, coins, spend_txs, daemon) => { let cache = Cache { network: daemon.config().bitcoin_config.network, blockheight: info.block_height, @@ -173,10 +207,9 @@ impl Application for GUI { let (app, command) = App::new(cache, loader.gui_config.clone(), daemon); self.state = State::App(app); command.map(|msg| Message::Run(Box::new(msg))) - } else { - loader.update(*msg).map(|msg| Message::Load(Box::new(msg))) } - } + _ => loader.update(*msg).map(|msg| Message::Load(Box::new(msg))), + }, (State::App(i), Message::Run(msg)) => { i.update(*msg).map(|msg| Message::Run(Box::new(msg))) } @@ -186,6 +219,7 @@ impl Application for GUI { fn should_exit(&self) -> bool { match &self.state { + State::Launcher(v) => v.should_exit(), State::Installer(v) => v.should_exit(), State::Loader(v) => v.should_exit(), State::App(v) => v.should_exit(), @@ -197,6 +231,7 @@ impl Application for GUI { State::Installer(v) => v.subscription().map(|msg| Message::Install(Box::new(msg))), State::Loader(v) => v.subscription().map(|msg| Message::Load(Box::new(msg))), State::App(v) => v.subscription().map(|msg| Message::Run(Box::new(msg))), + _ => Subscription::none(), } } @@ -204,6 +239,7 @@ impl Application for GUI { match &self.state { State::Installer(v) => v.view().map(|msg| Message::Install(Box::new(msg))), State::App(v) => v.view().map(|msg| Message::Run(Box::new(msg))), + State::Launcher(v) => v.view().map(|msg| Message::Launch(Box::new(msg))), State::Loader(v) => v.view().map(|msg| Message::Load(Box::new(msg))), } } @@ -214,19 +250,30 @@ impl Application for GUI { } pub enum Config { - Run(app::Config), + /// Datadir is optional because app can run with the config path only. + Run(Option, app::Config), + Launcher(PathBuf), Install(PathBuf, bitcoin::Network), } impl Config { - pub fn new(datadir_path: PathBuf, network: bitcoin::Network) -> Result> { - let mut path = datadir_path.clone(); - path.push(network.to_string()); - path.push(app::config::DEFAULT_FILE_NAME); - match app::Config::from_file(&path) { - Ok(cfg) => Ok(Config::Run(cfg)), - Err(ConfigError::NotFound) => Ok(Config::Install(datadir_path, network)), - Err(e) => Err(format!("Failed to read configuration file: {}", e).into()), + pub fn new( + datadir_path: PathBuf, + network: Option, + ) -> Result> { + if let Some(network) = network { + let mut path = datadir_path.clone(); + path.push(network.to_string()); + path.push(app::config::DEFAULT_FILE_NAME); + match app::Config::from_file(&path) { + Ok(cfg) => Ok(Config::Run(Some(datadir_path), cfg)), + Err(ConfigError::NotFound) => Ok(Config::Install(datadir_path, network)), + Err(e) => Err(format!("Failed to read configuration file: {}", e).into()), + } + } else if !datadir_path.exists() { + Ok(Config::Install(datadir_path, bitcoin::Network::Bitcoin)) + } else { + Ok(Config::Launcher(datadir_path)) } } } @@ -236,26 +283,24 @@ fn main() -> Result<(), Box> { let config = match args.as_slice() { [] => { let datadir_path = default_datadir().unwrap(); - Config::new(datadir_path, bitcoin::Network::Bitcoin) + Config::new(datadir_path, None) } [Arg::Network(network)] => { let datadir_path = default_datadir().unwrap(); - Config::new(datadir_path, *network) - } - [Arg::ConfigPath(path)] => Ok(Config::Run(app::Config::from_file(path)?)), - [Arg::DatadirPath(datadir_path)] => { - Config::new(datadir_path.clone(), bitcoin::Network::Bitcoin) + Config::new(datadir_path, Some(*network)) } + [Arg::ConfigPath(path)] => Ok(Config::Run(None, app::Config::from_file(path)?)), + [Arg::DatadirPath(datadir_path)] => Config::new(datadir_path.clone(), None), [Arg::DatadirPath(datadir_path), Arg::Network(network)] | [Arg::Network(network), Arg::DatadirPath(datadir_path)] => { - Config::new(datadir_path.clone(), *network) + Config::new(datadir_path.clone(), Some(*network)) } _ => { return Err("Unknown args combination".into()); } }?; - let level = if let Config::Run(cfg) = &config { + let level = if let Config::Run(_, cfg) = &config { log_level_from_config(cfg)? } else { log::LevelFilter::Info diff --git a/gui/src/ui/component/badge.rs b/gui/src/ui/component/badge.rs index 673783d8..3bba0afd 100644 --- a/gui/src/ui/component/badge.rs +++ b/gui/src/ui/component/badge.rs @@ -9,6 +9,7 @@ pub enum Style { Standard, Success, Warning, + Bitcoin, } impl widget::container::StyleSheet for Style { @@ -32,6 +33,12 @@ impl widget::container::StyleSheet for Style { text_color: color::WARNING.into(), ..widget::container::Appearance::default() }, + Self::Bitcoin => widget::container::Appearance { + border_radius: 40.0, + background: color::WARNING.into(), + text_color: iced::Color::WHITE.into(), + ..widget::container::Appearance::default() + }, } } }