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.
This commit is contained in:
edouard 2022-12-13 15:56:33 +01:00
parent a4299877eb
commit 63be34b3d8
5 changed files with 235 additions and 47 deletions

116
gui/src/launcher.rs Normal file
View File

@ -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<Network>,
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<Message> {
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),
}

View File

@ -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;

View File

@ -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<client::jsonrpc::JsonRPCClient>;
pub struct Loader {
pub datadir_path: Option<PathBuf>,
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<Message>) {
pub fn new(
datadir_path: Option<PathBuf>,
gui_config: GUIConfig,
daemon_config: Config,
) -> (Self, Command<Message>) {
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<Message> {
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<Message> {
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<ViewMessage> {
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<ViewMessage> {
},
)
.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),
),
),
),
}

View File

@ -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<Launcher>),
Installer(Box<Installer>),
Loader(Box<Loader>),
App(App),
@ -76,6 +78,7 @@ enum State {
#[derive(Debug)]
pub enum Message {
CtrlC,
Launch(Box<launcher::Message>),
Install(Box<installer::Message>),
Load(Box<loader::Message>),
Run(Box<app::Message>),
@ -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<Self::Message>) {
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<Self::Message> {
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<PathBuf>, app::Config),
Launcher(PathBuf),
Install(PathBuf, bitcoin::Network),
}
impl Config {
pub fn new(datadir_path: PathBuf, network: bitcoin::Network) -> Result<Self, Box<dyn Error>> {
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<bitcoin::Network>,
) -> Result<Self, Box<dyn Error>> {
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<dyn Error>> {
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

View File

@ -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()
},
}
}
}