Merge #19: Gui settings

29e134748df363963092e16dcd288ed6431d7434 Add settings panel to gui (edouard)

Pull request description:

  based on #8

ACKs for top commit:
  edouardparis:
    ACK 29e134748df363963092e16dcd288ed6431d7434

Tree-SHA512: 7a09c00d53ddef091219ffe5bf702795171bc0575b55f4930680a251d712d8cfdf30afd1ad38218c01c8430346fe95625202eb30b8cb9e1190d40806a7e6d226
This commit is contained in:
edouard 2022-08-24 17:29:47 +02:00
commit 4e26f18bc2
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
21 changed files with 772 additions and 175 deletions

3
gui/src/app/cache.rs Normal file
View File

@ -0,0 +1,3 @@
pub struct Cache {
pub blockheight: i32,
}

View File

@ -1,75 +0,0 @@
use std::fs::OpenOptions;
use std::io::Write;
use std::sync::Arc;
use minisafe::config::Config as DaemonConfig;
use crate::{
app::{config, error::Error, menu::Menu},
conversion::Converter,
daemon::Daemon,
};
/// Context is an object passing general information
/// and service clients through the application components.
pub struct Context {
pub config: ConfigContext,
pub blockheight: i32,
pub daemon: Arc<dyn Daemon + Sync + Send>,
pub converter: Converter,
pub menu: Menu,
pub managers_threshold: usize,
}
impl Context {
pub fn new(
config: ConfigContext,
daemon: Arc<dyn Daemon + Sync + Send>,
converter: Converter,
menu: Menu,
) -> Self {
Self {
config,
blockheight: 0,
daemon,
converter,
menu,
managers_threshold: 0,
}
}
pub fn network(&self) -> bitcoin::Network {
self.config.daemon.bitcoin_config.network
}
pub fn load_daemon_config(&mut self, cfg: DaemonConfig) -> Result<(), Error> {
loop {
if let Some(daemon) = Arc::get_mut(&mut self.daemon) {
daemon.load_config(cfg)?;
break;
}
}
let mut daemon_config_file = OpenOptions::new()
.write(true)
.open(&self.config.gui.minisafed_config_path)
.map_err(|e| Error::Config(e.to_string()))?;
let content =
toml::to_string(&self.config.daemon).map_err(|e| Error::Config(e.to_string()))?;
daemon_config_file
.write_all(content.as_bytes())
.map_err(|e| {
log::warn!("failed to write to file: {:?}", e);
Error::Config(e.to_string())
})?;
Ok(())
}
}
pub struct ConfigContext {
pub daemon: DaemonConfig,
pub gui: config::Config,
}

View File

@ -9,4 +9,5 @@ pub enum Message {
View(view::Message),
LoadDaemonConfig(Box<DaemonConfig>),
DaemonConfigLoaded(Result<(), Error>),
BlockHeight(Result<i32, Error>),
}

View File

@ -1,5 +1,5 @@
pub mod cache;
pub mod config;
pub mod context;
pub mod menu;
pub mod message;
pub mod state;
@ -7,6 +7,8 @@ pub mod view;
mod error;
use std::fs::OpenOptions;
use std::io::Write;
use std::sync::Arc;
use std::time::Duration;
@ -14,37 +16,57 @@ use iced::pure::Element;
use iced::{clipboard, time, Command, Subscription};
use iced_native::{window, Event};
pub use minisafe::config::Config as DaemonConfig;
pub use config::Config;
pub use message::Message;
use state::{Home, State};
use crate::app::context::Context;
use crate::{
app::{cache::Cache, error::Error, menu::Menu},
daemon::Daemon,
};
pub struct App {
should_exit: bool,
state: Box<dyn State>,
context: Context,
}
pub fn new_state(_context: &Context) -> Box<dyn State> {
Home {}.into()
cache: Cache,
config: Config,
daemon: Arc<dyn Daemon + Sync + Send>,
}
impl App {
pub fn new(context: Context) -> (App, Command<Message>) {
let state = new_state(&context);
let cmd = state.load(&context);
pub fn new(
cache: Cache,
config: Config,
daemon: Arc<dyn Daemon + Sync + Send>,
) -> (App, Command<Message>) {
let state: Box<dyn State> = Home {}.into();
let cmd = state.load(daemon.clone());
(
Self {
should_exit: false,
state,
context,
cache,
config,
daemon,
},
cmd,
)
}
fn load_state(&mut self, menu: &Menu) -> Command<Message> {
self.state = match menu {
menu::Menu::Settings => {
state::SettingsState::new(self.daemon.config().clone(), self.daemon.is_external())
.into()
}
menu::Menu::Home => Home {}.into(),
};
self.state.load(self.daemon.clone())
}
pub fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![
iced_native::subscription::events().map(Message::Event),
@ -59,9 +81,9 @@ impl App {
pub fn stop(&mut self) {
log::info!("Close requested");
if !self.context.daemon.is_external() {
if !self.daemon.is_external() {
log::info!("Stopping internal daemon...");
if let Some(d) = Arc::get_mut(&mut self.context.daemon) {
if let Some(d) = Arc::get_mut(&mut self.daemon) {
d.stop().expect("Daemon is internal");
log::info!("Internal daemon stopped");
self.should_exit = true;
@ -73,25 +95,65 @@ impl App {
pub fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::Tick => {
let daemon = self.daemon.clone();
Command::perform(
async move {
daemon
.get_info()
.map(|res| res.blockheight)
.map_err(|e| e.into())
},
Message::BlockHeight,
)
}
Message::BlockHeight(res) => {
if let Ok(blockheight) = res {
self.cache.blockheight = blockheight;
}
Command::none()
}
Message::LoadDaemonConfig(cfg) => {
let res = self.context.load_daemon_config(*cfg);
let res = self.load_daemon_config(*cfg);
self.update(Message::DaemonConfigLoaded(res))
}
Message::View(view::Message::Menu(menu)) => {
self.context.menu = menu;
self.state = new_state(&self.context);
self.state.load(&self.context)
}
Message::View(view::Message::Menu(menu)) => self.load_state(&menu),
Message::View(view::Message::Clipboard(text)) => clipboard::write(text),
Message::Event(Event::Window(window::Event::CloseRequested)) => {
self.stop();
Command::none()
}
_ => self.state.update(&self.context, message),
_ => self.state.update(self.daemon.clone(), &self.cache, message),
}
}
pub fn load_daemon_config(&mut self, cfg: DaemonConfig) -> Result<(), Error> {
loop {
if let Some(daemon) = Arc::get_mut(&mut self.daemon) {
daemon.load_config(cfg)?;
break;
}
}
let mut daemon_config_file = OpenOptions::new()
.write(true)
.open(&self.config.minisafed_config_path)
.map_err(|e| Error::Config(e.to_string()))?;
let content =
toml::to_string(&self.daemon.config()).map_err(|e| Error::Config(e.to_string()))?;
daemon_config_file
.write_all(content.as_bytes())
.map_err(|e| {
log::warn!("failed to write to file: {:?}", e);
Error::Config(e.to_string())
})?;
Ok(())
}
pub fn view(&self) -> Element<Message> {
self.state.view(&self.context).map(Message::View)
self.state.view(&self.cache).map(Message::View)
}
}

View File

@ -1,15 +1,28 @@
mod settings;
use std::sync::Arc;
use iced::pure::{column, Element};
use iced::{Command, Subscription};
use super::{context::Context, message::Message, view};
use super::{cache::Cache, menu::Menu, message::Message, view};
pub use settings::SettingsState;
use crate::daemon::Daemon;
pub trait State {
fn view<'a>(&self, ctx: &'a Context) -> Element<'a, view::Message>;
fn update(&mut self, ctx: &Context, message: Message) -> Command<Message>;
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message>;
fn update(
&mut self,
daemon: Arc<dyn Daemon + Send + Sync>,
cache: &Cache,
message: Message,
) -> Command<Message>;
fn subscription(&self) -> Subscription<Message> {
Subscription::none()
}
fn load(&self, _ctx: &Context) -> Command<Message> {
fn load(&self, _daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
Command::none()
}
}
@ -17,10 +30,15 @@ pub trait State {
pub struct Home {}
impl State for Home {
fn view<'a>(&self, ctx: &'a Context) -> Element<'a, view::Message> {
view::dashboard(&ctx.menu, None, column())
fn view<'a>(&self, _cache: &'a Cache) -> Element<'a, view::Message> {
view::dashboard(&Menu::Home, None, column())
}
fn update(&mut self, _ctx: &Context, _message: Message) -> Command<Message> {
fn update(
&mut self,
_daemon: Arc<dyn Daemon + Send + Sync>,
_cache: &Cache,
_message: Message,
) -> Command<Message> {
Command::none()
}
}

View File

@ -0,0 +1,238 @@
use std::convert::From;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use iced::{pure::Element, Command};
use minisafe::config::Config;
use crate::{
app::{cache::Cache, error::Error, message::Message, state::State, view},
daemon::Daemon,
ui::component::form,
};
trait Setting: std::fmt::Debug {
fn edited(&mut self, success: bool);
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
cache: &Cache,
message: view::SettingsMessage,
) -> Command<Message>;
fn view<'a>(
&self,
cfg: &'a Config,
cache: &'a Cache,
can_edit: bool,
) -> Element<'a, view::SettingsMessage>;
}
#[derive(Debug)]
pub struct SettingsState {
warning: Option<Error>,
config_updated: bool,
config: Config,
daemon_is_external: bool,
settings: Vec<Box<dyn Setting>>,
current: Option<usize>,
}
impl SettingsState {
pub fn new(config: Config, daemon_is_external: bool) -> Self {
let settings = vec![BitcoindSettings::new(&config).into()];
SettingsState {
daemon_is_external,
warning: None,
config_updated: false,
config,
settings,
current: None,
}
}
}
impl State for SettingsState {
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
cache: &Cache,
message: Message,
) -> Command<Message> {
match message {
Message::DaemonConfigLoaded(res) => match res {
Ok(()) => {
self.config_updated = true;
self.warning = None;
if let Some(current) = self.current {
if let Some(setting) = self.settings.get_mut(current) {
setting.edited(true)
}
}
self.current = None;
}
Err(e) => {
self.config_updated = false;
self.warning = Some(e);
if let Some(current) = self.current {
if let Some(setting) = self.settings.get_mut(current) {
setting.edited(false);
}
}
}
},
Message::View(view::Message::Settings(i, msg)) => {
if let Some(setting) = self.settings.get_mut(i) {
match msg {
view::SettingsMessage::Edit => self.current = Some(i),
view::SettingsMessage::CancelEdit => self.current = None,
_ => {}
};
return setting.update(daemon, cache, msg);
}
}
_ => {}
};
Command::none()
}
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
let can_edit = self.current.is_none() && !self.daemon_is_external;
view::settings::list(
self.warning.as_ref(),
self.settings
.iter()
.enumerate()
.map(|(i, setting)| {
setting
.view(&self.config, cache, can_edit)
.map(move |msg| view::Message::Settings(i, msg))
})
.collect(),
)
}
}
impl From<SettingsState> for Box<dyn State> {
fn from(s: SettingsState) -> Box<dyn State> {
Box::new(s)
}
}
#[derive(Debug)]
pub struct BitcoindSettings {
edit: bool,
processing: bool,
cookie_path: form::Value<String>,
addr: form::Value<String>,
}
impl From<BitcoindSettings> for Box<dyn Setting> {
fn from(s: BitcoindSettings) -> Box<dyn Setting> {
Box::new(s)
}
}
impl BitcoindSettings {
fn new(cfg: &Config) -> BitcoindSettings {
let cfg = cfg.bitcoind_config.as_ref().unwrap();
BitcoindSettings {
edit: false,
processing: false,
cookie_path: form::Value {
valid: true,
value: cfg.cookie_path.to_str().unwrap().to_string(),
},
addr: form::Value {
valid: true,
value: cfg.addr.to_string(),
},
}
}
}
impl Setting for BitcoindSettings {
fn edited(&mut self, success: bool) {
self.processing = false;
if success {
self.edit = false;
}
}
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
_cache: &Cache,
message: view::SettingsMessage,
) -> Command<Message> {
match message {
view::SettingsMessage::Edit => {
if !self.processing {
self.edit = true;
}
}
view::SettingsMessage::CancelEdit => {
if !self.processing {
self.edit = false;
}
}
view::SettingsMessage::FieldEdited(field, value) => {
if !self.processing {
match field {
"socket_address" => self.addr.value = value,
"cookie_file_path" => self.cookie_path.value = value,
_ => {}
}
}
}
view::SettingsMessage::ConfirmEdit => {
let new_addr = SocketAddr::from_str(&self.addr.value);
self.addr.valid = new_addr.is_ok();
let new_path = PathBuf::from_str(&self.cookie_path.value);
self.cookie_path.valid = new_path.is_ok();
if self.addr.valid & self.cookie_path.valid {
let mut daemon_config = daemon.config().clone();
daemon_config.bitcoind_config = Some(minisafe::config::BitcoindConfig {
cookie_path: new_path.unwrap(),
addr: new_addr.unwrap(),
});
self.processing = true;
return Command::perform(async move { daemon_config }, |cfg| {
Message::LoadDaemonConfig(Box::new(cfg))
});
}
}
};
Command::none()
}
fn view<'a>(
&self,
config: &'a Config,
cache: &'a Cache,
can_edit: bool,
) -> Element<'a, view::SettingsMessage> {
if self.edit {
view::settings::bitcoind_edit(
config.bitcoin_config.network,
cache.blockheight,
&self.addr,
&self.cookie_path,
self.processing,
)
} else {
view::settings::bitcoind(
config.bitcoin_config.network,
config.bitcoind_config.as_ref().unwrap(),
cache.blockheight,
Some(cache.blockheight != 0),
can_edit,
)
}
}
}

View File

@ -5,4 +5,13 @@ pub enum Message {
Reload,
Clipboard(String),
Menu(Menu),
Settings(usize, SettingsMessage),
}
#[derive(Debug, Clone)]
pub enum SettingsMessage {
Edit,
FieldEdited(&'static str, String),
CancelEdit,
ConfirmEdit,
}

View File

@ -1,7 +1,9 @@
mod message;
mod warning;
pub use message::Message;
pub mod settings;
pub use message::*;
use warning::warn;
use iced::{

View File

@ -0,0 +1,241 @@
use iced::{
alignment,
pure::{column, container, row, widget, Element},
Alignment, Length,
};
use super::{
dashboard,
message::{Message, SettingsMessage},
};
use crate::{
app::{error::Error, menu::Menu},
ui::{
color,
component::{badge, button, card, form, separation, text::*},
icon,
},
};
pub fn list<'a>(
warning: Option<&Error>,
settings: Vec<Element<'a, Message>>,
) -> Element<'a, Message> {
dashboard(
&Menu::Settings,
warning,
widget::Column::with_children(settings).spacing(20),
)
}
pub fn bitcoind_edit<'a>(
network: bitcoin::Network,
blockheight: i32,
addr: &form::Value<String>,
cookie_path: &form::Value<String>,
processing: bool,
) -> Element<'a, SettingsMessage> {
let mut col = column().spacing(20);
if blockheight != 0 {
col = col
.push(
row()
.push(
row()
.push(badge::Badge::new(icon::network_icon()))
.push(
column()
.push(text("Network:"))
.push(text(&network.to_string()).bold()),
)
.spacing(10)
.width(Length::FillPortion(1)),
)
.push(
row()
.push(badge::Badge::new(icon::block_icon()))
.push(
column()
.push(text("Block Height:"))
.push(text(&blockheight.to_string()).bold()),
)
.spacing(10)
.width(Length::FillPortion(1)),
),
)
.push(separation().width(Length::Fill));
}
col = col
.push(
column()
.push(text("Cookie file path:").bold().small())
.push(
form::Form::new("Cookie file path", cookie_path, |value| {
SettingsMessage::FieldEdited("cookie_file_path", value)
})
.warning("Please enter a valid filesystem path")
.size(20)
.padding(5),
)
.spacing(5),
)
.push(
column()
.push(text("Socket address:").bold().small())
.push(
form::Form::new("Socket address:", addr, |value| {
SettingsMessage::FieldEdited("socket_address", value)
})
.warning("Please enter a valid address")
.size(20)
.padding(5),
)
.spacing(5),
);
let mut cancel_button = button::transparent(None, " Cancel ").padding(5);
let mut confirm_button = button::primary(None, " Save ").padding(5);
if !processing {
cancel_button = cancel_button.on_press(SettingsMessage::CancelEdit);
confirm_button = confirm_button.on_press(SettingsMessage::ConfirmEdit);
}
card::simple(container(
column()
.push(
row()
.push(badge::Badge::new(icon::bitcoin_icon()))
.push(text("Bitcoind"))
.padding(10)
.spacing(20)
.align_items(Alignment::Center)
.width(Length::Fill),
)
.push(separation().width(Length::Fill))
.push(col)
.push(
container(
row()
.push(cancel_button)
.push(confirm_button)
.spacing(10)
.align_items(Alignment::Center),
)
.width(Length::Fill)
.align_x(alignment::Horizontal::Right),
)
.spacing(20),
))
.width(Length::Fill)
.into()
}
pub fn bitcoind<'a>(
network: bitcoin::Network,
config: &minisafe::config::BitcoindConfig,
blockheight: i32,
is_running: Option<bool>,
can_edit: bool,
) -> Element<'a, SettingsMessage> {
let mut col = column().spacing(20);
if blockheight != 0 {
col = col
.push(
row()
.push(
row()
.push(badge::Badge::new(icon::network_icon()))
.push(
column()
.push(text("Network:"))
.push(text(&network.to_string()).bold()),
)
.spacing(10)
.width(Length::FillPortion(1)),
)
.push(
row()
.push(badge::Badge::new(icon::block_icon()))
.push(
column()
.push(text("Block Height:"))
.push(text(&blockheight.to_string()).bold()),
)
.spacing(10)
.width(Length::FillPortion(1)),
),
)
.push(separation().width(Length::Fill));
}
let rows = vec![
(
"Cookie file path:",
config.cookie_path.to_str().unwrap().to_string(),
),
("Socket address:", config.addr.to_string()),
];
let mut col_fields = column();
for (k, v) in rows {
col_fields = col_fields.push(
row()
.push(container(text(k).bold().small()).width(Length::Fill))
.push(text(&v).small()),
);
}
card::simple(container(
column()
.push(
row()
.push(
row()
.push(badge::Badge::new(icon::bitcoin_icon()))
.push(text("Bitcoind"))
.push(is_running_label(is_running))
.spacing(20)
.align_items(Alignment::Center)
.width(Length::Fill),
)
.push(if can_edit {
widget::Button::new(icon::pencil_icon())
.style(button::Style::TransparentBorder)
.on_press(SettingsMessage::Edit)
} else {
widget::Button::new(icon::pencil_icon())
.style(button::Style::TransparentBorder)
})
.align_items(Alignment::Center),
)
.push(separation().width(Length::Fill))
.push(col.push(col_fields))
.spacing(20),
))
.width(Length::Fill)
.into()
}
pub fn is_running_label<'a, T: 'a>(is_running: Option<bool>) -> widget::Container<'a, T> {
if let Some(running) = is_running {
if running {
container(
row()
.push(icon::dot_icon().size(5).color(color::SUCCESS))
.push(text("Running").small().color(color::SUCCESS))
.align_items(Alignment::Center),
)
} else {
container(
row()
.push(icon::dot_icon().size(5).color(color::ALERT))
.push(text("Not running").small().color(color::ALERT))
.align_items(Alignment::Center),
)
}
} else {
container(column())
}
}

View File

@ -1,43 +0,0 @@
use bitcoin::Network;
/// Converter purpose is to give a Conversion from a given amount in satoshis according to its
/// parameters.
pub struct Converter {
pub unit: Unit,
}
impl Converter {
pub fn new(bitcoin_network: Network) -> Self {
let unit = match bitcoin_network {
Network::Testnet => Unit::TestnetBitcoin,
Network::Bitcoin => Unit::Bitcoin,
Network::Regtest => Unit::RegtestBitcoin,
Network::Signet => Unit::SignetBitcoin,
};
Self { unit }
}
/// converts amount in satoshis to BTC float.
pub fn converts(&self, amount: bitcoin::Amount) -> String {
format!("{:.8}", amount.as_btc())
}
}
/// Unit is the bitcoin ticker according to the network used.
pub enum Unit {
TestnetBitcoin,
RegtestBitcoin,
SignetBitcoin,
Bitcoin,
}
impl std::fmt::Display for Unit {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::TestnetBitcoin => write!(f, "tBTC"),
Self::RegtestBitcoin => write!(f, "rBTC"),
Self::SignetBitcoin => write!(f, "sBTC"),
Self::Bitcoin => write!(f, "BTC"),
}
}
}

View File

@ -7,6 +7,8 @@ use serde::{Deserialize, Serialize};
pub mod error;
pub mod jsonrpc;
use minisafe::config::Config;
use super::{model::*, Daemon, DaemonError};
pub trait Client {
@ -20,12 +22,13 @@ pub trait Client {
#[derive(Debug, Clone)]
pub struct Minisafed<C: Client> {
config: Config,
client: C,
}
impl<C: Client> Minisafed<C> {
pub fn new(client: C) -> Minisafed<C> {
Minisafed { client }
pub fn new(client: C, config: Config) -> Minisafed<C> {
Minisafed { client, config }
}
/// Generic call function for RPC calls.
@ -47,6 +50,10 @@ impl<C: Client + Debug> Daemon for Minisafed<C> {
true
}
fn config(&self) -> &Config {
&self.config
}
fn stop(&mut self) -> Result<(), DaemonError> {
let _res: serde_json::value::Value = self.call("stop", Option::<Request>::None)?;
Ok(())

View File

@ -3,15 +3,22 @@ use std::sync::Mutex;
use super::{model::*, Daemon, DaemonError};
use minisafe::{config::Config, DaemonHandle};
#[derive(Default)]
pub struct EmbeddedDaemon {
config: Config,
handle: Option<Mutex<DaemonHandle>>,
}
impl EmbeddedDaemon {
pub fn start(&mut self, config: Config) -> Result<(), DaemonError> {
let handle =
DaemonHandle::start_default(config).map_err(|e| DaemonError::Start(e.to_string()))?;
pub fn new(config: Config) -> Self {
Self {
config,
handle: None,
}
}
pub fn start(&mut self) -> Result<(), DaemonError> {
let handle = DaemonHandle::start_default(self.config.clone())
.map_err(|e| DaemonError::Start(e.to_string()))?;
self.handle = Some(Mutex::new(handle));
Ok(())
}
@ -40,6 +47,10 @@ impl Daemon for EmbeddedDaemon {
Ok(())
}
fn config(&self) -> &Config {
&self.config
}
fn stop(&mut self) -> Result<(), DaemonError> {
if let Some(h) = self.handle.take() {
let handle = h.into_inner().unwrap();

View File

@ -40,6 +40,8 @@ pub trait Daemon: Debug {
Ok(())
}
fn config(&self) -> &Config;
fn stop(&mut self) -> Result<(), DaemonError>;
fn get_info(&self) -> Result<model::GetInfoResult, DaemonError>;

View File

@ -1,5 +1,4 @@
pub mod app;
pub mod conversion;
pub mod daemon;
pub mod installer;
pub mod loader;

View File

@ -19,7 +19,6 @@ type Minisafed = client::Minisafed<client::jsonrpc::JsonRPCClient>;
pub struct Loader {
pub gui_config: GUIConfig,
pub daemon_config: Config,
pub daemon_started: bool,
should_exit: bool,
@ -56,13 +55,12 @@ impl Loader {
.unwrap();
(
Loader {
daemon_config,
gui_config,
step: Step::Connecting,
should_exit: false,
daemon_started: false,
},
Command::perform(connect(path), Message::Loaded),
Command::perform(connect(path, daemon_config), Message::Loaded),
)
}
@ -203,9 +201,12 @@ pub fn cover<'a, T: 'a, C: Into<Element<'a, T>>>(content: C) -> Element<'a, T> {
.into()
}
async fn connect(socket_path: PathBuf) -> Result<Arc<dyn Daemon + Sync + Send>, Error> {
async fn connect(
socket_path: PathBuf,
config: Config,
) -> Result<Arc<dyn Daemon + Sync + Send>, Error> {
let client = client::jsonrpc::JsonRPCClient::new(socket_path);
let minisafed = Minisafed::new(client);
let minisafed = Minisafed::new(client, config);
debug!("Searching for external daemon");
minisafed.get_info()?;
@ -221,8 +222,8 @@ pub async fn start_daemon(config_path: PathBuf) -> Result<Arc<dyn Daemon + Sync
let config = Config::from_file(Some(config_path))
.map_err(|e| DaemonError::Start(format!("Error parsing config: {}", e)))?;
let mut daemon = EmbeddedDaemon::default();
daemon.start(config)?;
let mut daemon = EmbeddedDaemon::new(config);
daemon.start()?;
Ok(Arc::new(daemon))
}

View File

@ -10,12 +10,10 @@ use minisafe::config::Config as DaemonConfig;
use minisafe_gui::{
app::{
self,
cache::Cache,
config::{default_datadir, ConfigError},
context::{ConfigContext, Context},
menu::Menu,
App,
},
conversion::Converter,
installer::{self, Installer},
loader::{self, Loader},
};
@ -163,17 +161,12 @@ impl Application for GUI {
}
}
(State::Loader(loader), Message::Load(msg)) => {
if let loader::Message::Synced(_info, minisafed) = *msg {
let config = ConfigContext {
gui: loader.gui_config.clone(),
daemon: loader.daemon_config.clone(),
if let loader::Message::Synced(info, minisafed) = *msg {
let cache = Cache {
blockheight: info.blockheight,
};
let converter = Converter::new(config.daemon.bitcoin_config.network);
let context = Context::new(config, minisafed, converter, Menu::Home);
let (app, command) = App::new(context);
let (app, command) = App::new(cache, loader.gui_config.clone(), minisafed);
self.state = State::App(app);
command.map(|msg| Message::Run(Box::new(msg)))
} else {

View File

@ -0,0 +1,70 @@
use iced::{
pure::{container, widget, Element},
Length,
};
use crate::ui::color;
pub enum Style {
Standard,
Success,
Warning,
}
impl widget::container::StyleSheet for Style {
fn style(&self) -> widget::container::Style {
match self {
Self::Standard => widget::container::Style {
border_radius: 40.0,
background: color::BACKGROUND.into(),
..widget::container::Style::default()
},
Self::Success => widget::container::Style {
border_radius: 40.0,
background: color::SUCCESS_LIGHT.into(),
text_color: color::SUCCESS.into(),
..widget::container::Style::default()
},
Self::Warning => widget::container::Style {
border_radius: 40.0,
background: color::WARNING_LIGHT.into(),
text_color: color::WARNING.into(),
..widget::container::Style::default()
},
}
}
}
pub struct Badge<S: widget::container::StyleSheet> {
icon: widget::Text,
style: S,
}
impl Badge<Style> {
pub fn new(icon: widget::Text) -> Self {
Self {
icon,
style: Style::Standard,
}
}
pub fn style(self, style: Style) -> Self {
Self {
icon: self.icon,
style,
}
}
}
impl<'a, Message: 'a, S: 'a + widget::container::StyleSheet> From<Badge<S>>
for Element<'a, Message>
{
fn from(badge: Badge<S>) -> Element<'a, Message> {
container(badge.icon.width(Length::Units(20)))
.width(Length::Units(40))
.height(Length::Units(40))
.style(badge.style)
.center_x()
.center_y()
.into()
}
}

View File

@ -15,6 +15,10 @@ pub fn transparent<'a, T: 'a>(icon: Option<iced::Text>, t: &str) -> button::Butt
button::Button::new(content(icon, t)).style(Style::Transparent)
}
pub fn transparent_border<'a, T: 'a>(icon: Option<iced::Text>, t: &str) -> button::Button<'a, T> {
button::Button::new(content(icon, t)).style(Style::TransparentBorder)
}
fn content<'a, T: 'a>(icon: Option<iced::Text>, t: &str) -> Container<'a, T> {
match icon {
None => container(text(t)).width(Length::Fill).center_x().padding(5),
@ -33,9 +37,10 @@ fn content<'a, T: 'a>(icon: Option<iced::Text>, t: &str) -> Container<'a, T> {
}
#[derive(Debug, Clone, Copy)]
enum Style {
pub enum Style {
Primary,
Transparent,
TransparentBorder,
}
impl button::StyleSheet for Style {
@ -49,7 +54,7 @@ impl button::StyleSheet for Style {
border_color: Color::TRANSPARENT,
text_color: color::FOREGROUND,
},
Style::Transparent => button::Style {
Style::Transparent | Style::TransparentBorder => button::Style {
shadow_offset: Vector::default(),
background: Color::TRANSPARENT.into(),
border_radius: 10.0,
@ -59,4 +64,33 @@ impl button::StyleSheet for Style {
},
}
}
fn hovered(&self) -> button::Style {
match self {
Style::Primary => button::Style {
shadow_offset: Vector::default(),
background: color::PRIMARY.into(),
border_radius: 10.0,
border_width: 0.0,
border_color: Color::TRANSPARENT,
text_color: color::FOREGROUND,
},
Style::Transparent => button::Style {
shadow_offset: Vector::default(),
background: color::FOREGROUND.into(),
border_radius: 10.0,
border_width: 0.0,
border_color: Color::TRANSPARENT,
text_color: color::DARK_GREY,
},
Style::TransparentBorder => button::Style {
shadow_offset: Vector::default(),
background: Color::TRANSPARENT.into(),
border_radius: 10.0,
border_width: 2.0,
border_color: Color::TRANSPARENT,
text_color: Color::BLACK,
},
}
}
}

View File

@ -0,0 +1,18 @@
use iced::pure::{container, widget, Element};
use crate::ui::color;
pub fn simple<'a, T: 'a, C: Into<Element<'a, T>>>(content: C) -> widget::Container<'a, T> {
container(content).padding(15).style(SimpleCardStyle)
}
pub struct SimpleCardStyle;
impl widget::container::StyleSheet for SimpleCardStyle {
fn style(&self) -> widget::container::Style {
widget::container::Style {
border_radius: 10.0,
background: color::FOREGROUND.into(),
..widget::container::Style::default()
}
}
}

View File

@ -1,4 +1,6 @@
pub mod badge;
pub mod button;
pub mod card;
pub mod form;
pub mod text;

View File

@ -8,10 +8,14 @@ pub fn text(content: &str) -> iced::pure::widget::Text {
pub trait Text {
fn bold(self) -> Self;
fn small(self) -> Self;
}
impl Text for iced::pure::widget::Text {
fn bold(self) -> Self {
self.font(font::BOLD)
}
fn small(self) -> Self {
self.size(20)
}
}