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:
commit
4e26f18bc2
3
gui/src/app/cache.rs
Normal file
3
gui/src/app/cache.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub struct Cache {
|
||||||
|
pub blockheight: i32,
|
||||||
|
}
|
||||||
@ -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,
|
|
||||||
}
|
|
||||||
@ -9,4 +9,5 @@ pub enum Message {
|
|||||||
View(view::Message),
|
View(view::Message),
|
||||||
LoadDaemonConfig(Box<DaemonConfig>),
|
LoadDaemonConfig(Box<DaemonConfig>),
|
||||||
DaemonConfigLoaded(Result<(), Error>),
|
DaemonConfigLoaded(Result<(), Error>),
|
||||||
|
BlockHeight(Result<i32, Error>),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
pub mod cache;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod context;
|
|
||||||
pub mod menu;
|
pub mod menu;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
@ -7,6 +7,8 @@ pub mod view;
|
|||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
|
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@ -14,37 +16,57 @@ use iced::pure::Element;
|
|||||||
use iced::{clipboard, time, Command, Subscription};
|
use iced::{clipboard, time, Command, Subscription};
|
||||||
use iced_native::{window, Event};
|
use iced_native::{window, Event};
|
||||||
|
|
||||||
|
pub use minisafe::config::Config as DaemonConfig;
|
||||||
|
|
||||||
pub use config::Config;
|
pub use config::Config;
|
||||||
pub use message::Message;
|
pub use message::Message;
|
||||||
|
|
||||||
use state::{Home, State};
|
use state::{Home, State};
|
||||||
|
|
||||||
use crate::app::context::Context;
|
use crate::{
|
||||||
|
app::{cache::Cache, error::Error, menu::Menu},
|
||||||
|
daemon::Daemon,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
should_exit: bool,
|
should_exit: bool,
|
||||||
state: Box<dyn State>,
|
state: Box<dyn State>,
|
||||||
context: Context,
|
cache: Cache,
|
||||||
}
|
config: Config,
|
||||||
|
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||||
pub fn new_state(_context: &Context) -> Box<dyn State> {
|
|
||||||
Home {}.into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new(context: Context) -> (App, Command<Message>) {
|
pub fn new(
|
||||||
let state = new_state(&context);
|
cache: Cache,
|
||||||
let cmd = state.load(&context);
|
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 {
|
Self {
|
||||||
should_exit: false,
|
should_exit: false,
|
||||||
state,
|
state,
|
||||||
context,
|
cache,
|
||||||
|
config,
|
||||||
|
daemon,
|
||||||
},
|
},
|
||||||
cmd,
|
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> {
|
pub fn subscription(&self) -> Subscription<Message> {
|
||||||
Subscription::batch(vec![
|
Subscription::batch(vec![
|
||||||
iced_native::subscription::events().map(Message::Event),
|
iced_native::subscription::events().map(Message::Event),
|
||||||
@ -59,9 +81,9 @@ impl App {
|
|||||||
|
|
||||||
pub fn stop(&mut self) {
|
pub fn stop(&mut self) {
|
||||||
log::info!("Close requested");
|
log::info!("Close requested");
|
||||||
if !self.context.daemon.is_external() {
|
if !self.daemon.is_external() {
|
||||||
log::info!("Stopping internal daemon...");
|
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");
|
d.stop().expect("Daemon is internal");
|
||||||
log::info!("Internal daemon stopped");
|
log::info!("Internal daemon stopped");
|
||||||
self.should_exit = true;
|
self.should_exit = true;
|
||||||
@ -73,25 +95,65 @@ impl App {
|
|||||||
|
|
||||||
pub fn update(&mut self, message: Message) -> Command<Message> {
|
pub fn update(&mut self, message: Message) -> Command<Message> {
|
||||||
match 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) => {
|
Message::LoadDaemonConfig(cfg) => {
|
||||||
let res = self.context.load_daemon_config(*cfg);
|
let res = self.load_daemon_config(*cfg);
|
||||||
self.update(Message::DaemonConfigLoaded(res))
|
self.update(Message::DaemonConfigLoaded(res))
|
||||||
}
|
}
|
||||||
Message::View(view::Message::Menu(menu)) => {
|
Message::View(view::Message::Menu(menu)) => self.load_state(&menu),
|
||||||
self.context.menu = menu;
|
|
||||||
self.state = new_state(&self.context);
|
|
||||||
self.state.load(&self.context)
|
|
||||||
}
|
|
||||||
Message::View(view::Message::Clipboard(text)) => clipboard::write(text),
|
Message::View(view::Message::Clipboard(text)) => clipboard::write(text),
|
||||||
Message::Event(Event::Window(window::Event::CloseRequested)) => {
|
Message::Event(Event::Window(window::Event::CloseRequested)) => {
|
||||||
self.stop();
|
self.stop();
|
||||||
Command::none()
|
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> {
|
pub fn view(&self) -> Element<Message> {
|
||||||
self.state.view(&self.context).map(Message::View)
|
self.state.view(&self.cache).map(Message::View)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,28 @@
|
|||||||
|
mod settings;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use iced::pure::{column, Element};
|
use iced::pure::{column, Element};
|
||||||
use iced::{Command, Subscription};
|
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 {
|
pub trait State {
|
||||||
fn view<'a>(&self, ctx: &'a Context) -> Element<'a, view::Message>;
|
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message>;
|
||||||
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>;
|
||||||
fn subscription(&self) -> Subscription<Message> {
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
Subscription::none()
|
Subscription::none()
|
||||||
}
|
}
|
||||||
fn load(&self, _ctx: &Context) -> Command<Message> {
|
fn load(&self, _daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
|
||||||
Command::none()
|
Command::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -17,10 +30,15 @@ pub trait State {
|
|||||||
pub struct Home {}
|
pub struct Home {}
|
||||||
|
|
||||||
impl State for Home {
|
impl State for Home {
|
||||||
fn view<'a>(&self, ctx: &'a Context) -> Element<'a, view::Message> {
|
fn view<'a>(&self, _cache: &'a Cache) -> Element<'a, view::Message> {
|
||||||
view::dashboard(&ctx.menu, None, column())
|
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()
|
Command::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
238
gui/src/app/state/settings.rs
Normal file
238
gui/src/app/state/settings.rs
Normal 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,4 +5,13 @@ pub enum Message {
|
|||||||
Reload,
|
Reload,
|
||||||
Clipboard(String),
|
Clipboard(String),
|
||||||
Menu(Menu),
|
Menu(Menu),
|
||||||
|
Settings(usize, SettingsMessage),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum SettingsMessage {
|
||||||
|
Edit,
|
||||||
|
FieldEdited(&'static str, String),
|
||||||
|
CancelEdit,
|
||||||
|
ConfirmEdit,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
mod message;
|
mod message;
|
||||||
mod warning;
|
mod warning;
|
||||||
|
|
||||||
pub use message::Message;
|
pub mod settings;
|
||||||
|
|
||||||
|
pub use message::*;
|
||||||
use warning::warn;
|
use warning::warn;
|
||||||
|
|
||||||
use iced::{
|
use iced::{
|
||||||
|
|||||||
241
gui/src/app/view/settings.rs
Normal file
241
gui/src/app/view/settings.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,6 +7,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod jsonrpc;
|
pub mod jsonrpc;
|
||||||
|
|
||||||
|
use minisafe::config::Config;
|
||||||
|
|
||||||
use super::{model::*, Daemon, DaemonError};
|
use super::{model::*, Daemon, DaemonError};
|
||||||
|
|
||||||
pub trait Client {
|
pub trait Client {
|
||||||
@ -20,12 +22,13 @@ pub trait Client {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Minisafed<C: Client> {
|
pub struct Minisafed<C: Client> {
|
||||||
|
config: Config,
|
||||||
client: C,
|
client: C,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Client> Minisafed<C> {
|
impl<C: Client> Minisafed<C> {
|
||||||
pub fn new(client: C) -> Minisafed<C> {
|
pub fn new(client: C, config: Config) -> Minisafed<C> {
|
||||||
Minisafed { client }
|
Minisafed { client, config }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generic call function for RPC calls.
|
/// Generic call function for RPC calls.
|
||||||
@ -47,6 +50,10 @@ impl<C: Client + Debug> Daemon for Minisafed<C> {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn config(&self) -> &Config {
|
||||||
|
&self.config
|
||||||
|
}
|
||||||
|
|
||||||
fn stop(&mut self) -> Result<(), DaemonError> {
|
fn stop(&mut self) -> Result<(), DaemonError> {
|
||||||
let _res: serde_json::value::Value = self.call("stop", Option::<Request>::None)?;
|
let _res: serde_json::value::Value = self.call("stop", Option::<Request>::None)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -3,15 +3,22 @@ use std::sync::Mutex;
|
|||||||
use super::{model::*, Daemon, DaemonError};
|
use super::{model::*, Daemon, DaemonError};
|
||||||
use minisafe::{config::Config, DaemonHandle};
|
use minisafe::{config::Config, DaemonHandle};
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct EmbeddedDaemon {
|
pub struct EmbeddedDaemon {
|
||||||
|
config: Config,
|
||||||
handle: Option<Mutex<DaemonHandle>>,
|
handle: Option<Mutex<DaemonHandle>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EmbeddedDaemon {
|
impl EmbeddedDaemon {
|
||||||
pub fn start(&mut self, config: Config) -> Result<(), DaemonError> {
|
pub fn new(config: Config) -> Self {
|
||||||
let handle =
|
Self {
|
||||||
DaemonHandle::start_default(config).map_err(|e| DaemonError::Start(e.to_string()))?;
|
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));
|
self.handle = Some(Mutex::new(handle));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -40,6 +47,10 @@ impl Daemon for EmbeddedDaemon {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn config(&self) -> &Config {
|
||||||
|
&self.config
|
||||||
|
}
|
||||||
|
|
||||||
fn stop(&mut self) -> Result<(), DaemonError> {
|
fn stop(&mut self) -> Result<(), DaemonError> {
|
||||||
if let Some(h) = self.handle.take() {
|
if let Some(h) = self.handle.take() {
|
||||||
let handle = h.into_inner().unwrap();
|
let handle = h.into_inner().unwrap();
|
||||||
|
|||||||
@ -40,6 +40,8 @@ pub trait Daemon: Debug {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn config(&self) -> &Config;
|
||||||
|
|
||||||
fn stop(&mut self) -> Result<(), DaemonError>;
|
fn stop(&mut self) -> Result<(), DaemonError>;
|
||||||
|
|
||||||
fn get_info(&self) -> Result<model::GetInfoResult, DaemonError>;
|
fn get_info(&self) -> Result<model::GetInfoResult, DaemonError>;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod conversion;
|
|
||||||
pub mod daemon;
|
pub mod daemon;
|
||||||
pub mod installer;
|
pub mod installer;
|
||||||
pub mod loader;
|
pub mod loader;
|
||||||
|
|||||||
@ -19,7 +19,6 @@ type Minisafed = client::Minisafed<client::jsonrpc::JsonRPCClient>;
|
|||||||
|
|
||||||
pub struct Loader {
|
pub struct Loader {
|
||||||
pub gui_config: GUIConfig,
|
pub gui_config: GUIConfig,
|
||||||
pub daemon_config: Config,
|
|
||||||
pub daemon_started: bool,
|
pub daemon_started: bool,
|
||||||
|
|
||||||
should_exit: bool,
|
should_exit: bool,
|
||||||
@ -56,13 +55,12 @@ impl Loader {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
(
|
(
|
||||||
Loader {
|
Loader {
|
||||||
daemon_config,
|
|
||||||
gui_config,
|
gui_config,
|
||||||
step: Step::Connecting,
|
step: Step::Connecting,
|
||||||
should_exit: false,
|
should_exit: false,
|
||||||
daemon_started: 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()
|
.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 client = client::jsonrpc::JsonRPCClient::new(socket_path);
|
||||||
let minisafed = Minisafed::new(client);
|
let minisafed = Minisafed::new(client, config);
|
||||||
|
|
||||||
debug!("Searching for external daemon");
|
debug!("Searching for external daemon");
|
||||||
minisafed.get_info()?;
|
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))
|
let config = Config::from_file(Some(config_path))
|
||||||
.map_err(|e| DaemonError::Start(format!("Error parsing config: {}", e)))?;
|
.map_err(|e| DaemonError::Start(format!("Error parsing config: {}", e)))?;
|
||||||
|
|
||||||
let mut daemon = EmbeddedDaemon::default();
|
let mut daemon = EmbeddedDaemon::new(config);
|
||||||
daemon.start(config)?;
|
daemon.start()?;
|
||||||
|
|
||||||
Ok(Arc::new(daemon))
|
Ok(Arc::new(daemon))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,12 +10,10 @@ use minisafe::config::Config as DaemonConfig;
|
|||||||
use minisafe_gui::{
|
use minisafe_gui::{
|
||||||
app::{
|
app::{
|
||||||
self,
|
self,
|
||||||
|
cache::Cache,
|
||||||
config::{default_datadir, ConfigError},
|
config::{default_datadir, ConfigError},
|
||||||
context::{ConfigContext, Context},
|
|
||||||
menu::Menu,
|
|
||||||
App,
|
App,
|
||||||
},
|
},
|
||||||
conversion::Converter,
|
|
||||||
installer::{self, Installer},
|
installer::{self, Installer},
|
||||||
loader::{self, Loader},
|
loader::{self, Loader},
|
||||||
};
|
};
|
||||||
@ -163,17 +161,12 @@ impl Application for GUI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
(State::Loader(loader), Message::Load(msg)) => {
|
(State::Loader(loader), Message::Load(msg)) => {
|
||||||
if let loader::Message::Synced(_info, minisafed) = *msg {
|
if let loader::Message::Synced(info, minisafed) = *msg {
|
||||||
let config = ConfigContext {
|
let cache = Cache {
|
||||||
gui: loader.gui_config.clone(),
|
blockheight: info.blockheight,
|
||||||
daemon: loader.daemon_config.clone(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let converter = Converter::new(config.daemon.bitcoin_config.network);
|
let (app, command) = App::new(cache, loader.gui_config.clone(), minisafed);
|
||||||
|
|
||||||
let context = Context::new(config, minisafed, converter, Menu::Home);
|
|
||||||
|
|
||||||
let (app, command) = App::new(context);
|
|
||||||
self.state = State::App(app);
|
self.state = State::App(app);
|
||||||
command.map(|msg| Message::Run(Box::new(msg)))
|
command.map(|msg| Message::Run(Box::new(msg)))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
70
gui/src/ui/component/badge.rs
Normal file
70
gui/src/ui/component/badge.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
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> {
|
fn content<'a, T: 'a>(icon: Option<iced::Text>, t: &str) -> Container<'a, T> {
|
||||||
match icon {
|
match icon {
|
||||||
None => container(text(t)).width(Length::Fill).center_x().padding(5),
|
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)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum Style {
|
pub enum Style {
|
||||||
Primary,
|
Primary,
|
||||||
Transparent,
|
Transparent,
|
||||||
|
TransparentBorder,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl button::StyleSheet for Style {
|
impl button::StyleSheet for Style {
|
||||||
@ -49,7 +54,7 @@ impl button::StyleSheet for Style {
|
|||||||
border_color: Color::TRANSPARENT,
|
border_color: Color::TRANSPARENT,
|
||||||
text_color: color::FOREGROUND,
|
text_color: color::FOREGROUND,
|
||||||
},
|
},
|
||||||
Style::Transparent => button::Style {
|
Style::Transparent | Style::TransparentBorder => button::Style {
|
||||||
shadow_offset: Vector::default(),
|
shadow_offset: Vector::default(),
|
||||||
background: Color::TRANSPARENT.into(),
|
background: Color::TRANSPARENT.into(),
|
||||||
border_radius: 10.0,
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
gui/src/ui/component/card.rs
Normal file
18
gui/src/ui/component/card.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,6 @@
|
|||||||
|
pub mod badge;
|
||||||
pub mod button;
|
pub mod button;
|
||||||
|
pub mod card;
|
||||||
pub mod form;
|
pub mod form;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
|
||||||
|
|||||||
@ -8,10 +8,14 @@ pub fn text(content: &str) -> iced::pure::widget::Text {
|
|||||||
|
|
||||||
pub trait Text {
|
pub trait Text {
|
||||||
fn bold(self) -> Self;
|
fn bold(self) -> Self;
|
||||||
|
fn small(self) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Text for iced::pure::widget::Text {
|
impl Text for iced::pure::widget::Text {
|
||||||
fn bold(self) -> Self {
|
fn bold(self) -> Self {
|
||||||
self.font(font::BOLD)
|
self.font(font::BOLD)
|
||||||
}
|
}
|
||||||
|
fn small(self) -> Self {
|
||||||
|
self.size(20)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user