Merge #223: Gui add launcher
63be34b3d887d0d39f861b6ae46996d64a245b18 Add confirm network step to gui (edouard)
Pull request description:
ACKs for top commit:
edouardparis:
Self-ACK 63be34b3d887d0d39f861b6ae46996d64a245b18
Tree-SHA512: 5952af35c6559ab91aeed330973dd2496a35d246c18d87266f81abb65effcfc4da11005c1e15ab7923d4f4db3709079987b0c3e527ca7611c244737ea8f10505
This commit is contained in:
commit
88c807d599
116
gui/src/launcher.rs
Normal file
116
gui/src/launcher.rs
Normal 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),
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
123
gui/src/main.rs
123
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<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
|
||||
|
||||
@ -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()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user