From 8bc1cfc20948a49413dd2febb663eed009569c4e Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Sun, 3 Dec 2023 23:41:09 +0200 Subject: [PATCH] wip (themes, misc) --- .rustfmt.toml | 6 + Cargo.toml | 1 + core/Cargo.toml | 1 + core/src/app.rs | 42 ++-- core/src/core.rs | 119 ++++++---- core/src/egui/container.rs | 141 ------------ core/src/egui/extensions.rs | 8 +- core/src/egui/mnemonic.rs | 3 +- core/src/egui/network.rs | 5 +- core/src/egui/panel.rs | 8 +- core/src/egui/popup.rs | 13 +- core/src/egui/theme.rs | 142 ------------ core/src/egui/theme/color.rs | 207 ++++++++++++++++++ core/src/egui/theme/mod.rs | 170 ++++++++++++++ core/src/egui/theme/style.rs | 99 +++++++++ core/src/events.rs | 3 + core/src/imports.rs | 3 +- core/src/menu.rs | 88 +++++++- core/src/modules/account_create.rs | 14 +- core/src/modules/account_manager/menus.rs | 6 +- core/src/modules/account_manager/mod.rs | 2 +- core/src/modules/account_manager/overview.rs | 28 +-- core/src/modules/block_dag.rs | 70 ++++-- core/src/modules/export.rs | 2 +- core/src/modules/logs.rs | 2 +- core/src/modules/metrics.rs | 50 ++--- core/src/modules/node.rs | 4 +- core/src/modules/overview.rs | 180 ++++++++------- core/src/modules/private_key_create.rs | 6 +- core/src/modules/settings/mod.rs | 8 +- core/src/modules/welcome.rs | 37 +++- core/src/primitives/account.rs | 5 + core/src/primitives/block.rs | 18 +- core/src/primitives/transaction.rs | 19 +- core/src/runtime/services/blockdag_monitor.rs | 40 +++- core/src/runtime/services/kaspa/logs.rs | 2 +- core/src/runtime/services/kaspa/mod.rs | 2 +- core/src/runtime/system.rs | 2 +- core/src/settings.rs | 28 ++- core/src/status.rs | 16 +- core/src/sync.rs | 2 +- core/src/utils/color.rs | 14 ++ core/src/utils/image.rs | 14 ++ core/src/utils/mod.rs | 7 +- core/src/utils/qr.rs | 4 +- core/src/utils/version.rs | 127 +++++++++++ resources/i18n/i18n.json | 166 ++++---------- 47 files changed, 1231 insertions(+), 703 deletions(-) create mode 100644 .rustfmt.toml delete mode 100644 core/src/egui/container.rs delete mode 100644 core/src/egui/theme.rs create mode 100644 core/src/egui/theme/color.rs create mode 100644 core/src/egui/theme/mod.rs create mode 100644 core/src/egui/theme/style.rs create mode 100644 core/src/utils/image.rs create mode 100644 core/src/utils/version.rs diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..82dce4d --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,6 @@ +# max_width = 100 +# use_field_init_shorthand = true +# use_try_shorthand = true +# use_small_heuristics = "Max" +# newline_style = "auto" +# edition = "2021" \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 8aae64c..d9a4724 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ kaspa-ng-macros = { version = "0.1.0", path = "macros/" } # ___________________ egui = "0.24.0" +epaint = "0.24.0" egui_plot = "0.24.0" egui_extras = { version = "0.24.0", features = ["svg","image"] } eframe = { version = "0.24.0", default-features = false, features = [ diff --git a/core/Cargo.toml b/core/Cargo.toml index d41c18b..2147c64 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -61,6 +61,7 @@ wasm-bindgen.workspace = true zeroize.workspace = true egui.workspace = true +epaint.workspace = true egui_plot.workspace = true egui_extras.workspace = true chrono.workspace = true diff --git a/core/src/app.rs b/core/src/app.rs index f0a1f21..1362647 100644 --- a/core/src/app.rs +++ b/core/src/app.rs @@ -7,8 +7,8 @@ use std::sync::Arc; use workflow_i18n::*; use workflow_log::*; -#[allow(unused)] -pub const KASPA_NG_ICON_256X256: &[u8] = include_bytes!("../../resources/icons/icon-256.png"); +// #[allow(unused)] +pub const KASPA_NG_ICON_SVG: &[u8] = include_bytes!("../../resources/images/kaspa.svg"); pub const KASPA_NG_LOGO_SVG: &[u8] = include_bytes!("../../resources/images/kaspa-ng.svg"); pub const I18N_EMBEDDED: &str = include_str!("../../resources/i18n/i18n.json"); pub const BUILD_TIMESTAMP: &str = env!("VERGEN_BUILD_TIMESTAMP"); @@ -21,12 +21,16 @@ pub const RUSTC_HOST_TRIPLE: &str = env!("VERGEN_RUSTC_HOST_TRIPLE"); pub const RUSTC_LLVM_VERSION: &str = env!("VERGEN_RUSTC_LLVM_VERSION"); pub const RUSTC_SEMVER: &str = env!("VERGEN_RUSTC_SEMVER"); pub const CARGO_TARGET_TRIPLE: &str = env!("VERGEN_CARGO_TARGET_TRIPLE"); -// pub const CARGO_PROFILE: &str = env!("VERGEN_CARGO_PROFILE"); +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub const CODENAME: &str = "DNA"; cfg_if! { if #[cfg(not(target_arch = "wasm32"))] { - use kaspad_lib::daemon::create_core; + use kaspad_lib::daemon::{ + create_core, + DESIRED_DAEMON_SOFT_FD_LIMIT, + MINIMUM_DAEMON_SOFT_FD_LIMIT + }; use kaspad_lib::args::Args as NodeArgs; use kaspad_lib::args::parse_args as parse_kaspad_args; use kaspa_utils::fd_budget; @@ -37,9 +41,9 @@ cfg_if! { #[derive(Debug)] enum I18n { - Import, Export, + Reset, } enum Args { @@ -62,29 +66,31 @@ cfg_if! { let cmd = Command::new("kaspa-ng") - .about(format!("kaspa-ng v{}-{GIT_DESCRIBE} (rusty-kaspa v{})", env!("CARGO_PKG_VERSION"), kaspa_wallet_core::version())) + .about(format!("kaspa-ng v{VERSION}-{GIT_DESCRIBE} (rusty-kaspa v{})", kaspa_wallet_core::version())) .arg(arg!(--disable "Disable node services when starting")) + .arg(arg!(--daemon "Run as Rusty Kaspa p2p daemon")) .arg( Arg::new("reset-settings") .long("reset-settings") .action(ArgAction::SetTrue) .help("Reset kaspa-ng settings") ) - .subcommand( Command::new("i18n").hide(true) .about("kaspa-ng i18n user interface translation") .subcommand( Command::new("import") - // .help("import i18n data") - .about("import JSON files suffixed with language codes (*_en.json, *_de.json, etc.)") + .about("import JSON files suffixed with language codes (*_en.json, *_de.json, etc.)") ) .subcommand( Command::new("export") - .about("export default 'en' translations as JSON") + .about("export default 'en' translations as JSON") + ) + .subcommand( + Command::new("reset") + .about("reset i18n data file") ) ) - .subcommand( Command::new("cli") .about("Run kaspa-ng as rusty-kaspa cli wallet") @@ -99,6 +105,8 @@ cfg_if! { Args::I18n { op : I18n::Import } } else if let Some(_matches) = matches.subcommand_matches("export") { Args::I18n { op : I18n::Export } + } else if let Some(_matches) = matches.subcommand_matches("reset") { + Args::I18n { op : I18n::Reset } } else { println!(); println!("please specify a valid i18n subcommand"); @@ -119,9 +127,6 @@ cfg_if! { runtime::panic::init_panic_handler(); - // TODO - import from kaspad_lib - const DESIRED_DAEMON_SOFT_FD_LIMIT: u64 = 16 * 1024; - const MINIMUM_DAEMON_SOFT_FD_LIMIT: u64 = 4 * 1024; match try_set_fd_limit(DESIRED_DAEMON_SOFT_FD_LIMIT) { Ok(limit) => { if limit < MINIMUM_DAEMON_SOFT_FD_LIMIT { @@ -209,7 +214,7 @@ cfg_if! { .with_resizable(true) .with_title(i18n("Kaspa NG")) .with_inner_size([1000.0,600.0]) - .with_icon(eframe::icon_data::from_png_bytes(KASPA_NG_ICON_256X256).unwrap()), + .with_icon(svg_to_icon_data(KASPA_NG_ICON_SVG, FitTo::Size(256,256))), ..Default::default() }; eframe::run_native( @@ -308,6 +313,12 @@ cfg_if! { cfg_if! { if #[cfg(not(target_arch = "wasm32"))] { fn manage_i18n(op : I18n) -> Result<()> { + if matches!(op, I18n::Reset) { + println!("resetting i18n data file"); + i18n::create(i18n_storage_file()?)?; + return Ok(()); + } + let i18n_json_file = i18n_storage_file()?; let i18n_json_file_store = i18n_storage_file()?; i18n::Builder::new("en", "en") @@ -338,6 +349,7 @@ cfg_if! { Ok(fs::write(&target_folder, json_data)?) })?; } + _ => unreachable!() } Ok(()) diff --git a/core/src/core.rs b/core/src/core.rs index 7a231ec..34a4364 100644 --- a/core/src/core.rs +++ b/core/src/core.rs @@ -7,6 +7,7 @@ use kaspa_wallet_core::api::TransactionDataGetResponse; use kaspa_wallet_core::events::Events as CoreWallet; use kaspa_wallet_core::storage::{Binding, Hint, PrvKeyDataInfo}; use std::borrow::Cow; +#[allow(unused_imports)] use workflow_i18n::*; pub enum Exception { @@ -42,6 +43,7 @@ pub struct Core { pub prv_key_data_map: HashMap>, pub account_collection: Option, // pub selected_account: Option, + pub release: Option, } impl Core { @@ -92,6 +94,12 @@ impl Core { egui::FontId::new(18.0, egui::FontFamily::Proportional), ); + apply_theme_by_name( + &cc.egui_ctx, + settings.user_interface.theme_color.as_str(), + settings.user_interface.theme_style.as_str(), + ); + // cc.egui_ctx.set_style(style); // This is also where you can customize the look and feel of egui using @@ -162,6 +170,8 @@ impl Core { hint: None, discard_hint: false, exception: None, + + release: None, }; modules.values().for_each(|module| { @@ -170,6 +180,17 @@ impl Core { this.wallet_update_list(); + #[cfg(not(target_arch = "wasm32"))] + spawn(async move { + use workflow_core::task::sleep; + + loop { + println!("Checking version..."); + let _ = check_version().await; + sleep(Duration::from_secs(60 * 60 * 6)).await; + } + }); + this } @@ -322,24 +343,21 @@ impl eframe::App for Core { } }); - // ctx.set_visuals(self.default_style.clone()); - let mut current_visuals = ctx.style().visuals.clone(); //.widgets.noninteractive; + // - TODO - TOAST BACKGROUND + // --- + let current_visuals = ctx.style().visuals.clone(); //.widgets.noninteractive; let mut visuals = current_visuals.clone(); - // visuals.widgets.noninteractive.fg_stroke = Stroke::new(1.0, Color32::from_rgb(0, 0, 0)); visuals.widgets.noninteractive.bg_fill = egui::Color32::from_rgb(0, 0, 0); - - cfg_if! { - if #[cfg(target_arch = "wasm32")] { - visuals.interact_cursor = Some(CursorIcon::PointingHand); - } - } - - // visuals.bg_fill = egui::Color32::from_rgb(0, 0, 0); ctx.set_visuals(visuals); self.toasts.show(ctx); - - theme().apply(&mut current_visuals); ctx.set_visuals(current_visuals); + // --- + + // cfg_if! { + // if #[cfg(target_arch = "wasm32")] { + // visuals.interact_cursor = Some(CursorIcon::PointingHand); + // } + // } if !self.settings.initialized { cfg_if! { @@ -443,36 +461,41 @@ impl eframe::App for Core { #[cfg(not(target_arch = "wasm32"))] if let Some(screenshot) = self.screenshot.as_ref() { - if let Some(mut path) = rfd::FileDialog::new().save_file() { - path.set_extension("png"); - let screen_rect = ctx.screen_rect(); - let pixels_per_point = ctx.pixels_per_point(); - let screenshot = screenshot.clone(); - let sender = self.sender(); - std::thread::Builder::new() - .name("screenshot".to_string()) - .spawn(move || { - let image = screenshot.region(&screen_rect, Some(pixels_per_point)); - image::save_buffer( - &path, - image.as_raw(), - image.width() as u32, - image.height() as u32, - image::ColorType::Rgba8, - ) - .unwrap(); - - sender - .try_send(Events::Notify { - user_notification: UserNotification::success(format!( - "Capture saved to\n{}", - path.to_string_lossy() - )), - }) - .unwrap() - }) - .expect("Unable to spawn screenshot thread"); - self.screenshot.take(); + match rfd::FileDialog::new().save_file() { + Some(mut path) => { + path.set_extension("png"); + let screen_rect = ctx.screen_rect(); + let pixels_per_point = ctx.pixels_per_point(); + let screenshot = screenshot.clone(); + let sender = self.sender(); + std::thread::Builder::new() + .name("screenshot".to_string()) + .spawn(move || { + let image = screenshot.region(&screen_rect, Some(pixels_per_point)); + image::save_buffer( + &path, + image.as_raw(), + image.width() as u32, + image.height() as u32, + image::ColorType::Rgba8, + ) + .unwrap(); + + sender + .try_send(Events::Notify { + user_notification: UserNotification::success(format!( + "Capture saved to\n{}", + path.to_string_lossy() + )), + }) + .unwrap() + }) + .expect("Unable to spawn screenshot thread"); + self.screenshot.take(); + } + None => { + self.screenshot.take(); + } } } } @@ -505,6 +528,16 @@ impl Core { _frame: &mut eframe::Frame, ) -> Result<()> { match event { + Events::ThemeChange => { + if let Some(account_collection) = self.account_collection.as_ref() { + account_collection + .iter() + .for_each(|account| account.update_theme()); + } + } + Events::VersionUpdate(release) => { + self.release = Some(release); + } Events::StoreSettings => { self.settings_storage_requested = true; self.last_settings_storage_request = Instant::now(); diff --git a/core/src/egui/container.rs b/core/src/egui/container.rs deleted file mode 100644 index 593daac..0000000 --- a/core/src/egui/container.rs +++ /dev/null @@ -1,141 +0,0 @@ -//use {Widget, Response, Ui, Button}; -use egui::*; - -pub struct Container{ - rect: Option, - hovered: bool, - render_body: Box -} - -impl Container{ - pub fn new(body: impl FnMut(&mut egui::Ui)+'static)->Self{ - Self { rect:None, hovered: false, render_body: Box::new(body)} - } -} - -impl Container{ - fn render(&mut self, ui: &mut Ui) -> Response{ - //ui.child_ui_with_id_source(max_rect, layout, id_source); - let mut ui_rect = ui.available_rect_before_wrap(); - let padding = 4.0; - ui_rect.min.x += padding; - ui_rect.min.y += padding; - ui_rect.max.x -= padding; - ui_rect.max.y -= padding; - - let mut child_ui = ui.child_ui(ui_rect, Layout::top_down(Align::Min)); - - (self.render_body)(&mut child_ui); - - let mut rect = child_ui.min_rect(); - rect.min.x -= padding; - rect.min.y -= padding; - rect.max.x += padding; - rect.max.y += padding; - - self.rect = Some(rect); - - //let mut child_ui2 = ui.child_ui(ui_rect, Layout::top_down(Align::Min)); - - let response = ui.interact(rect, child_ui.id(), Sense::click()); - //let response = ui.allocate_rect(rect, Sense::click()); - self.hovered = !response.clicked() && response.hovered(); - - - let painter = child_ui.painter(); - - if self.hovered && self.rect.is_some(){ - let rect = self.rect.as_ref().unwrap(); - painter.rect(*rect, 2.3, Color32::BLUE, (1.0, Color32::BLUE)); - //painter.rect_stroke(*rect, 1.0, (1.0, Color32::GREEN)); - ui.ctx().set_cursor_icon(CursorIcon::PointingHand); - }else if let Some(rect) = self.rect.as_ref() { - painter.rect(*rect, 2.3, Color32::LIGHT_BLUE, (1.0, Color32::LIGHT_BLUE)); - //painter.rect_stroke(*rect, 1.0, (1.0, Color32::GRAY)); - } - - let mut child_ui = ui.child_ui(ui_rect, Layout::top_down(Align::Min)); - (self.render_body)(&mut child_ui); - - - //if let Some(cursor) = ui.visuals().interact_cursor { - //if !clicked && response.hovered { - // rect.max.y += 1.0; - ui.advance_cursor_after_rect(rect); - //let response = ui.interact(rect, child_ui.id(), Sense::click()); - // let clicked = response.clicked(); - - //let response = ui.allocate_rect(rect, Sense::click()); - - response - } -} - -impl Widget for Container{ - fn ui(mut self, ui: &mut Ui) -> Response { - self.render(ui) - } -} - -// impl Widget for &mut Container{ -// fn ui(self, ui: &mut Ui) -> Response { -// //ui.child_ui_with_id_source(max_rect, layout, id_source); -// let mut ui_rect = ui.available_rect_before_wrap(); -// let padding = 4.0; -// ui_rect.min.x += padding; -// ui_rect.min.y += padding; -// ui_rect.max.x -= padding; -// ui_rect.max.y -= padding; - -// let mut child_ui = ui.child_ui(ui_rect, Layout::top_down(Align::Min)); -// let painter = child_ui.painter(); - -// if self.hovered && self.rect.is_some(){ -// let rect = self.rect.as_ref().unwrap(); -// painter.rect(*rect, 2.3, Color32::BLUE, (1.0, Color32::BLUE)); -// //painter.rect_stroke(*rect, 1.0, (1.0, Color32::GREEN)); -// ui.ctx().set_cursor_icon(CursorIcon::PointingHand); -// }else if let Some(rect) = self.rect.as_ref() { -// painter.rect(*rect, 2.3, Color32::LIGHT_BLUE, (1.0, Color32::LIGHT_BLUE)); -// //painter.rect_stroke(*rect, 1.0, (1.0, Color32::GRAY)); -// } -// // if child_ui.add(Button::new("Hello")).clicked(){ - -// // } -// // if child_ui.add(Button::new("Button 2")).clicked(){ -// // child_ui.painter().rect_filled(child_ui.min_rect(), 2.3, Color32::BLUE); -// // } - -// (self.render_body)(&mut child_ui); - -// let mut rect = child_ui.min_rect(); -// rect.min.x -= padding; -// rect.min.y -= padding; -// rect.max.x += padding; -// rect.max.y += padding; - -// self.rect = Some(rect); -// let response = ui.allocate_rect(rect, Sense::click());//vec2(200.0, 100.0), Sense::click()); -// let clicked = response.clicked(); -// // if clicked { -// // child_ui.painter().rect_filled(response.rect, 2.3, Color32::GREEN); -// // child_ui.painter().rect_stroke(response.rect, 1.0, (1.0, Color32::BLUE)); -// // }else{ -// // child_ui.painter().rect_stroke(response.rect, 1.0, (2.0, Color32::RED)); -// // } - - - -// //if let Some(cursor) = ui.visuals().interact_cursor { -// if !clicked && response.hovered { -// self.hovered = true; -// //ui.ctx().set_cursor_icon(cursor); -// // child_ui.painter().rect(response.rect, 2.3, Color32::BLUE, (1.0, Color32::YELLOW)); -// //child_ui.painter().rect_stroke(response.rect, 1.0, (1.0, Color32::GREEN)); -// }else{ -// self.hovered = false; -// } -// //} -// response -// } -// } \ No newline at end of file diff --git a/core/src/egui/extensions.rs b/core/src/egui/extensions.rs index 1a389cd..5e15641 100644 --- a/core/src/egui/extensions.rs +++ b/core/src/egui/extensions.rs @@ -39,14 +39,14 @@ impl UiExtension for Ui { fn medium_button_enabled(&mut self, enabled: bool, text: impl Into) -> Response { self.add_enabled( enabled, - Button::new(text).min_size(theme().medium_button_size()), + Button::new(text).min_size(theme_style().medium_button_size()), ) } fn large_button_enabled(&mut self, enabled: bool, text: impl Into) -> Response { self.add_enabled( enabled, - Button::new(text).min_size(theme().large_button_size()), + Button::new(text).min_size(theme_style().large_button_size()), ) } @@ -62,7 +62,7 @@ impl UiExtension for Ui { ui.add_space( ui.available_width() - 16. - - (theme().medium_button_size.x + ui.spacing().item_spacing.x) * 2., + - (theme_style().medium_button_size.x + ui.spacing().item_spacing.x) * 2., ); } @@ -274,6 +274,6 @@ pub trait WidgetSpacerExtension { impl WidgetSpacerExtension for Ui { fn space(&mut self) { - self.add_space(theme().widget_spacing); + self.add_space(theme_style().widget_spacing); } } diff --git a/core/src/egui/mnemonic.rs b/core/src/egui/mnemonic.rs index 08ec295..2c68fff 100644 --- a/core/src/egui/mnemonic.rs +++ b/core/src/egui/mnemonic.rs @@ -32,7 +32,8 @@ impl<'render> MnemonicPresenter<'render> { pub fn render(&mut self, ui: &mut Ui) { ui.vertical_centered(|ui| { ui.label( - RichText::new("Never share your mnemonic with anyone!").color(Color32::LIGHT_RED), + RichText::new("Never share your mnemonic with anyone!") + .color(theme_color().alert_color), ); ui.separator(); ui.label(" "); diff --git a/core/src/egui/network.rs b/core/src/egui/network.rs index 7b075c0..1683dff 100644 --- a/core/src/egui/network.rs +++ b/core/src/egui/network.rs @@ -70,11 +70,12 @@ impl NetworkInterfaceEditor { if self.custom.is_empty() { ui.label( RichText::new("Please enter custom interface address: IP[:PORT]") - .color(Color32::LIGHT_YELLOW), + .color(theme_color().warning_color), ); } else if let Err(err) = ContextualNetAddress::from_str(self.custom.as_str()) { ui.label( - RichText::new(format!("Error: {}", err)).color(Color32::LIGHT_RED), + RichText::new(format!("Error: {}", err)) + .color(theme_color().error_color), ); } }); diff --git a/core/src/egui/panel.rs b/core/src/egui/panel.rs index 9c21b90..db1fa79 100644 --- a/core/src/egui/panel.rs +++ b/core/src/egui/panel.rs @@ -117,11 +117,9 @@ impl<'panel, Context> Panel<'panel, Context> { } pub fn render(self, ui: &mut Ui) { - let theme = theme(); - - let icon_size = theme.panel_icon_size(); + let icon_size = theme_style().panel_icon_size(); let icon_padding = (icon_size.outer - icon_size.inner) / 2.0; - let panel_margin_size = theme.panel_margin_size(); + let panel_margin_size = theme_style().panel_margin_size(); let panel_width = ui.available_width(); let inner_panel_width = panel_width - panel_margin_size * 2.; @@ -189,7 +187,7 @@ impl<'panel, Context> Panel<'panel, Context> { }); } - let padding = ui.available_height() - theme.panel_footer_height; + let padding = ui.available_height() - theme_style().panel_footer_height; if padding > 0. { ui.add_space(padding); } diff --git a/core/src/egui/popup.rs b/core/src/egui/popup.rs index 41cf407..9206480 100644 --- a/core/src/egui/popup.rs +++ b/core/src/egui/popup.rs @@ -1,6 +1,6 @@ use crate::imports::*; -type PopupHandler<'panel> = Box; +type PopupHandler<'panel> = Box; pub struct PopupPanel<'panel> { id: Id, @@ -19,7 +19,7 @@ impl<'panel> PopupPanel<'panel> { ui: &mut Ui, id: impl Into, title: impl Into, - content: impl FnOnce(&mut Ui) + 'panel, + content: impl FnOnce(&mut Ui, &mut bool) + 'panel, ) -> Self { let id = ui.make_persistent_id(id.into()); @@ -120,7 +120,8 @@ impl<'panel> PopupPanel<'panel> { ui.space(); } - content(ui); + let mut close_popup = false; + content(ui, &mut close_popup); if self.with_close_button { ui.space(); @@ -129,11 +130,15 @@ impl<'panel> PopupPanel<'panel> { ui.add_space(8.); ui.vertical_centered(|ui| { if ui.medium_button("Close").clicked() { - ui.memory_mut(|mem| mem.close_popup()); + close_popup = true; } }); ui.add_space(8.); } + + if close_popup { + ui.memory_mut(|mem| mem.close_popup()); + } }, ); } diff --git a/core/src/egui/theme.rs b/core/src/egui/theme.rs deleted file mode 100644 index 22fa80f..0000000 --- a/core/src/egui/theme.rs +++ /dev/null @@ -1,142 +0,0 @@ -use kaspa_metrics::MetricGroup; - -use crate::imports::*; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct Theme { - pub visuals: Visuals, - - pub kaspa_color: Color32, - pub hyperlink_color: Color32, - pub node_data_color: Color32, - pub balance_color: Color32, - pub panel_icon_size: IconSize, - pub panel_margin_size: f32, - pub error_icon_size: IconSize, - pub medium_button_size: Vec2, - pub large_button_size: Vec2, - pub panel_footer_height: f32, - pub panel_editor_size: Vec2, - - pub widget_spacing: f32, - pub error_color: Color32, - pub warning_color: Color32, - pub ack_color: Color32, - pub nack_color: Color32, - - pub icon_size_large: f32, - pub icon_color_default: Color32, - pub status_icon_size: f32, - pub progress_color: Color32, - pub graph_frame_color: Color32, - pub performance_graph_color: Color32, - pub storage_graph_color: Color32, - pub connections_graph_color: Color32, - pub bandwidth_graph_color: Color32, - pub network_graph_color: Color32, - pub node_log_font_size: f32, - pub block_dag_new_block_fill_color: Color32, - pub block_dag_block_fill_color: Color32, - pub block_dag_block_stroke_color: Color32, - pub block_dag_vspc_connect_color: Color32, - pub block_dag_parent_connect_color: Color32, -} - -impl Default for Theme { - fn default() -> Self { - Self { - visuals: Visuals::light(), - // visuals: Visuals::dark(), - kaspa_color: Color32::from_rgb(58, 221, 190), - // hyperlink_color: Color32::from_rgb(58, 221, 190), - hyperlink_color: Color32::from_rgb(141, 184, 178), - // node_data_color : Color32::from_rgb(217, 233,230), - node_data_color: Color32::WHITE, - balance_color: Color32::WHITE, - panel_icon_size: IconSize::new(Vec2::splat(26.)).with_padding(Vec2::new(6., 0.)), - error_icon_size: IconSize::new(Vec2::splat(64.)).with_padding(Vec2::new(6., 6.)), - medium_button_size: Vec2::new(100_f32, 30_f32), - large_button_size: Vec2::new(200_f32, 40_f32), - panel_footer_height: 72_f32, - panel_margin_size: 24_f32, - panel_editor_size: Vec2::new(200_f32, 40_f32), - - widget_spacing: 4_f32, - error_color: Color32::from_rgb(255, 136, 136), - warning_color: egui::Color32::from_rgb(255, 255, 136), - ack_color: Color32::from_rgb(100, 200, 100), - nack_color: Color32::from_rgb(200, 100, 100), - - icon_size_large: 96_f32, - icon_color_default: Color32::from_rgb(255, 255, 255), - status_icon_size: 18_f32, - progress_color: Color32::from_rgb(21, 82, 71), - - graph_frame_color: Color32::GRAY, - performance_graph_color: Color32::from_rgb(186, 238, 255), - storage_graph_color: Color32::from_rgb(255, 231, 186), - connections_graph_color: Color32::from_rgb(241, 255, 186), - bandwidth_graph_color: Color32::from_rgb(196, 255, 199), - network_graph_color: Color32::from_rgb(186, 255, 241), - node_log_font_size: 15_f32, - - block_dag_new_block_fill_color: Color32::from_rgb(220, 220, 220), - block_dag_block_fill_color: Color32::from_rgb(0xAD, 0xD8, 0xE6), - block_dag_block_stroke_color: Color32::from_rgb(15, 84, 77), - block_dag_vspc_connect_color: Color32::from_rgb(23, 150, 137), - block_dag_parent_connect_color: Color32::from_rgba_premultiplied(0xAD, 0xD8, 0xE6, 220), - } - } -} - -impl Theme { - pub fn panel_icon_size(&self) -> &IconSize { - &self.panel_icon_size - } - - pub fn panel_margin_size(&self) -> f32 { - self.panel_margin_size - } - - pub fn medium_button_size(&self) -> Vec2 { - self.medium_button_size - } - - pub fn large_button_size(&self) -> Vec2 { - self.large_button_size - } - - pub fn apply(&self, visuals: &mut Visuals) { - // let visuals = ui.visuals_mut(); - visuals.hyperlink_color = self.hyperlink_color; - } -} - -static mut THEME: Option = None; -#[inline(always)] -pub fn theme() -> &'static Theme { - unsafe { THEME.get_or_insert_with(Theme::default) } -} - -pub fn apply_theme(theme: Theme) { - unsafe { - THEME = Some(theme); - } -} - -pub trait MetricGroupExtension { - fn to_color(&self) -> Color32; -} - -impl MetricGroupExtension for MetricGroup { - fn to_color(&self) -> Color32 { - match self { - MetricGroup::System => theme().performance_graph_color, - MetricGroup::Storage => theme().storage_graph_color, - MetricGroup::Connections => theme().connections_graph_color, - MetricGroup::Bandwidth => theme().bandwidth_graph_color, - MetricGroup::Network => theme().network_graph_color, - } - } -} diff --git a/core/src/egui/theme/color.rs b/core/src/egui/theme/color.rs new file mode 100644 index 0000000..4a4b86b --- /dev/null +++ b/core/src/egui/theme/color.rs @@ -0,0 +1,207 @@ +use crate::imports::*; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct ThemeColor { + pub name: String, + pub dark_mode: bool, + + pub kaspa_color: Color32, + pub hyperlink_color: Color32, + pub node_data_color: Color32, + pub balance_color: Color32, + pub error_color: Color32, + pub alert_color: Color32, + pub warning_color: Color32, + pub syncing_color: Color32, + pub connected_color: Color32, + pub icon_color_default: Color32, + pub ack_color: Color32, + pub nack_color: Color32, + + pub raised_text_color: Color32, + pub raised_text_shadow: Color32, + + pub qr_background: Color32, + pub qr_foreground: Color32, + // pub toast_background : Color32, + pub selection_color: Color32, + pub progress_color: Color32, + + pub default_color: Color32, + pub strong_color: Color32, + pub transaction_incoming: Color32, + pub transaction_outgoing: Color32, + pub transaction_external: Color32, + pub transaction_reorg: Color32, + pub transaction_batch: Color32, + pub transaction_stasis: Color32, + + pub logs_info_color: Color32, + pub logs_error_color: Color32, + pub logs_warning_color: Color32, + pub logs_debug_color: Color32, + pub logs_trace_color: Color32, + pub logs_processed_color: Color32, + + pub graph_frame_color: Color32, + pub performance_graph_color: Color32, + pub storage_graph_color: Color32, + pub connections_graph_color: Color32, + pub bandwidth_graph_color: Color32, + pub network_graph_color: Color32, + + pub block_dag_separator_color: Color32, + pub block_dag_new_block_fill_color: Color32, + pub block_dag_block_fill_color: Color32, + pub block_dag_block_stroke_color: Color32, + pub block_dag_vspc_connect_color: Color32, + pub block_dag_parent_connect_color: Color32, +} + +impl ThemeColor { + pub fn dark() -> Self { + Self { + name: "Dark".to_string(), + dark_mode: true, + kaspa_color: Color32::from_rgb(58, 221, 190), + hyperlink_color: Color32::from_rgb(141, 184, 178), + + default_color: Color32::LIGHT_GRAY, + strong_color: Color32::WHITE, + + node_data_color: Color32::WHITE, + balance_color: Color32::WHITE, + error_color: Color32::from_rgb(255, 136, 136), + alert_color: Color32::from_rgb(255, 136, 136), + warning_color: egui::Color32::from_rgb(255, 255, 136), + syncing_color: egui::Color32::from_rgb(255, 255, 136), + connected_color: egui::Color32::from_rgb(144, 238, 144), + icon_color_default: Color32::from_rgb(255, 255, 255), + ack_color: Color32::from_rgb(100, 200, 100), + nack_color: Color32::from_rgb(200, 100, 100), + + raised_text_color: Color32::from_rgb(255, 255, 255), + raised_text_shadow: Color32::from_rgb(0, 0, 0), + + qr_background: Color32::from_rgba(0, 0, 0, 0), + qr_foreground: Color32::WHITE, + selection_color: Color32::from_rgb(71, 105, 97), + progress_color: Color32::from_rgb(71, 105, 97), + + transaction_incoming: Color32::from_rgb(162, 245, 187), + transaction_outgoing: Color32::from_rgb(245, 162, 162), + transaction_external: Color32::from_rgb(162, 245, 187), + transaction_reorg: Color32::from_rgb(79, 64, 64), + transaction_batch: Color32::GRAY, + transaction_stasis: Color32::GRAY, + + logs_info_color: Color32::WHITE, + logs_error_color: Color32::LIGHT_RED, + logs_warning_color: Color32::LIGHT_YELLOW, + logs_debug_color: Color32::LIGHT_BLUE, + logs_trace_color: Color32::LIGHT_GRAY, + logs_processed_color: Color32::LIGHT_GREEN, + + graph_frame_color: Color32::GRAY, + performance_graph_color: Color32::from_rgb(186, 238, 255), + storage_graph_color: Color32::from_rgb(255, 231, 186), + connections_graph_color: Color32::from_rgb(241, 255, 186), + bandwidth_graph_color: Color32::from_rgb(196, 255, 199), + network_graph_color: Color32::from_rgb(186, 255, 241), + + block_dag_separator_color: Color32::from_rgb(220, 220, 220), + block_dag_new_block_fill_color: Color32::from_rgb(220, 220, 220), + block_dag_block_fill_color: Color32::from_rgb(173, 216, 230), + block_dag_block_stroke_color: Color32::from_rgb(15, 84, 77), + block_dag_vspc_connect_color: Color32::from_rgb(23, 150, 137), + block_dag_parent_connect_color: Color32::from_rgba_premultiplied(173, 216, 230, 220), + } + } + + pub fn light() -> Self { + Self { + name: "Light".to_string(), + dark_mode: false, + kaspa_color: Color32::from_rgb(58, 221, 190), + hyperlink_color: Color32::from_rgb(15, 84, 73), + + default_color: Color32::DARK_GRAY, + strong_color: Color32::BLACK, + + node_data_color: Color32::BLACK, + balance_color: Color32::BLACK, + error_color: Color32::from_rgb(77, 41, 41), + alert_color: Color32::from_rgb(77, 41, 41), + warning_color: egui::Color32::from_rgb(77, 77, 41), + syncing_color: egui::Color32::from_rgb(76, 77, 41), + connected_color: egui::Color32::from_rgb(8, 110, 65), + icon_color_default: Color32::from_rgb(255, 255, 255), + ack_color: Color32::from_rgb(100, 200, 100), + nack_color: Color32::from_rgb(200, 100, 100), + + raised_text_color: Color32::from_rgb(0, 0, 0), + raised_text_shadow: Color32::from_rgb(255, 255, 255), + + qr_background: Color32::from_rgba(255, 255, 255, 0), + qr_foreground: Color32::BLACK, + selection_color: Color32::from_rgb(165, 201, 197), + progress_color: Color32::from_rgb(165, 201, 197), + + transaction_incoming: Color32::from_rgb(15, 77, 35), + transaction_outgoing: Color32::from_rgb(77, 15, 15), + transaction_external: Color32::from_rgb(15, 77, 35), + transaction_reorg: Color32::from_rgb(38, 31, 31), + transaction_batch: Color32::GRAY, + transaction_stasis: Color32::GRAY, + + logs_info_color: Color32::BLACK, + logs_error_color: Color32::DARK_RED, + logs_warning_color: Color32::BROWN, + logs_debug_color: Color32::DARK_BLUE, + logs_trace_color: Color32::DARK_GRAY, + logs_processed_color: Color32::DARK_GREEN, + + graph_frame_color: Color32::GRAY, + performance_graph_color: Color32::from_rgb(56, 71, 77), + storage_graph_color: Color32::from_rgb(77, 69, 56), + connections_graph_color: Color32::from_rgb(72, 77, 56), + bandwidth_graph_color: Color32::from_rgb(59, 77, 60), + network_graph_color: Color32::from_rgb(56, 77, 72), + + block_dag_separator_color: Color32::from_rgb(120, 120, 120), + block_dag_new_block_fill_color: Color32::from_rgb(220, 220, 220), + block_dag_block_fill_color: Color32::from_rgb(201, 230, 240), + block_dag_block_stroke_color: Color32::from_rgb(42, 51, 50), + block_dag_vspc_connect_color: Color32::from_rgb(11, 77, 70), + block_dag_parent_connect_color: Color32::from_rgba_premultiplied(0, 0, 0, 220), + } + } +} + +impl Default for ThemeColor { + fn default() -> Self { + Self::dark() + } +} + +impl ThemeColor { + pub fn name(&self) -> &str { + &self.name + } +} + +static mut THEME_COLOR_LIST: Option> = None; +pub fn theme_colors() -> &'static HashMap { + unsafe { + THEME_COLOR_LIST.get_or_insert_with(|| { + let mut themes = HashMap::new(); + [ThemeColor::dark(), ThemeColor::light()] + .into_iter() + .for_each(|theme| { + themes.insert(theme.name.clone(), theme.clone()); + }); + themes + }) + } +} diff --git a/core/src/egui/theme/mod.rs b/core/src/egui/theme/mod.rs new file mode 100644 index 0000000..8844abe --- /dev/null +++ b/core/src/egui/theme/mod.rs @@ -0,0 +1,170 @@ +// use egui::style::WidgetVisuals; +use kaspa_metrics::MetricGroup; + +mod color; +pub use color::*; +mod style; +pub use style::*; + +use crate::imports::*; + +#[derive(Clone)] +pub struct Theme { + pub color: ThemeColor, + pub style: ThemeStyle, +} + +impl Theme { + pub fn new(color: ThemeColor, style: ThemeStyle) -> Self { + Self { color, style } + } + + #[inline(always)] + pub fn color(&self) -> &ThemeColor { + &self.color + } + + #[inline(always)] + pub fn style(&self) -> &ThemeStyle { + &self.style + } +} + +impl Default for Theme { + fn default() -> Self { + Self { + color: ThemeColor::dark(), + style: ThemeStyle::rounded(), + } + } +} + +impl From<&Theme> for Visuals { + fn from(theme: &Theme) -> Self { + let mut visuals = if theme.color.dark_mode { + Visuals::dark() + } else { + Visuals::light() + }; + + visuals.widgets.active.rounding = theme.style.widget_rounding; + visuals.widgets.inactive.rounding = theme.style.widget_rounding; + visuals.widgets.hovered.rounding = theme.style.widget_rounding; + visuals.widgets.noninteractive.rounding = theme.style.widget_rounding; + visuals.widgets.open.rounding = theme.style.widget_rounding; + + visuals.hyperlink_color = theme.color.hyperlink_color; + visuals.selection.bg_fill = theme.color.selection_color; + visuals.warn_fg_color = theme.color.warning_color; + visuals.error_fg_color = theme.color.error_color; + + visuals + } +} + +impl AsRef for Theme { + fn as_ref(&self) -> &Self { + self + } +} + +// impl AsMut for Theme { +// fn as_mut(&mut self) -> &mut Self { +// self +// } +// } + +static mut THEME: Option = None; +#[inline(always)] +pub fn theme() -> &'static Theme { + unsafe { THEME.get_or_insert_with(Theme::default) } +} + +#[inline(always)] +pub fn theme_color() -> &'static ThemeColor { + &theme().color +} + +#[inline(always)] +pub fn theme_style() -> &'static ThemeStyle { + &theme().style +} + +pub fn apply_theme_by_name( + ctx: &Context, + theme_color_name: impl Into, + theme_style_name: impl Into, +) { + let theme_color_name = theme_color_name.into(); + let theme_color = theme_colors() + .get(&theme_color_name) + .cloned() + .unwrap_or_else(|| { + log_error!("Theme color not found: {}", theme_color_name); + ThemeColor::default() + }); + + let theme_style_name = theme_style_name.into(); + let theme_style = theme_styles() + .get(&theme_style_name) + .cloned() + .unwrap_or_else(|| { + log_error!("Theme style not found: {}", theme_style_name); + ThemeStyle::default() + }); + + apply_theme(ctx, Theme::new(theme_color, theme_style)); +} + +pub fn apply_theme_color_by_name(ctx: &Context, theme_color_name: impl Into) { + let theme_color_name = theme_color_name.into(); + let theme_color = theme_colors() + .get(&theme_color_name) + .cloned() + .unwrap_or_else(|| { + log_error!("Theme not found: {}", theme_color_name); + ThemeColor::default() + }); + + apply_theme(ctx, Theme::new(theme_color, theme_style().clone())); +} + +pub fn apply_theme_style_by_name(ctx: &Context, theme_style_name: impl Into) { + let theme_style_name = theme_style_name.into(); + let theme_style = theme_styles() + .get(&theme_style_name) + .cloned() + .unwrap_or_else(|| { + log_error!("Theme not found: {}", theme_style_name); + ThemeStyle::default() + }); + + apply_theme(ctx, Theme::new(theme_color().clone(), theme_style)); +} + +pub fn apply_theme(ctx: &Context, theme: Theme) { + unsafe { + THEME = Some(theme.clone()); + } + ctx.set_visuals(theme.as_ref().into()); + runtime() + .application_events() + .try_send(Events::ThemeChange) + .unwrap(); +} + +pub trait MetricGroupExtension { + fn to_color(&self) -> Color32; +} + +impl MetricGroupExtension for MetricGroup { + fn to_color(&self) -> Color32 { + match self { + MetricGroup::System => theme_color().performance_graph_color, + MetricGroup::Storage => theme_color().storage_graph_color, + MetricGroup::Connections => theme_color().connections_graph_color, + MetricGroup::Bandwidth => theme_color().bandwidth_graph_color, + MetricGroup::Network => theme_color().network_graph_color, + } + } +} diff --git a/core/src/egui/theme/style.rs b/core/src/egui/theme/style.rs new file mode 100644 index 0000000..ef1b3fb --- /dev/null +++ b/core/src/egui/theme/style.rs @@ -0,0 +1,99 @@ +use crate::imports::*; + +#[derive(Clone)] +pub struct ThemeStyle { + pub name: String, + pub widget_rounding: Rounding, + pub widget_spacing: f32, + pub panel_icon_size: IconSize, + pub panel_margin_size: f32, + pub error_icon_size: IconSize, + pub medium_button_size: Vec2, + pub large_button_size: Vec2, + pub panel_footer_height: f32, + pub panel_editor_size: Vec2, + pub icon_size_large: f32, + pub status_icon_size: f32, + pub node_log_font_size: f32, +} + +impl ThemeStyle { + pub fn rounded() -> ThemeStyle { + Self { + name: "Rounded".to_string(), + widget_rounding: Rounding::from(6.), + widget_spacing: 6_f32, + panel_icon_size: IconSize::new(Vec2::splat(26.)).with_padding(Vec2::new(6., 0.)), + error_icon_size: IconSize::new(Vec2::splat(64.)).with_padding(Vec2::new(6., 6.)), + medium_button_size: Vec2::new(100_f32, 30_f32), + large_button_size: Vec2::new(200_f32, 40_f32), + panel_footer_height: 72_f32, + panel_margin_size: 24_f32, + panel_editor_size: Vec2::new(200_f32, 40_f32), + icon_size_large: 96_f32, + status_icon_size: 18_f32, + node_log_font_size: 15_f32, + } + } + + pub fn sharp() -> ThemeStyle { + Self { + name: "Sharp".to_string(), + widget_rounding: Rounding::from(0.), + widget_spacing: 6_f32, + panel_icon_size: IconSize::new(Vec2::splat(26.)).with_padding(Vec2::new(6., 0.)), + error_icon_size: IconSize::new(Vec2::splat(64.)).with_padding(Vec2::new(6., 6.)), + medium_button_size: Vec2::new(100_f32, 30_f32), + large_button_size: Vec2::new(200_f32, 40_f32), + panel_footer_height: 72_f32, + panel_margin_size: 24_f32, + panel_editor_size: Vec2::new(200_f32, 40_f32), + icon_size_large: 96_f32, + status_icon_size: 18_f32, + node_log_font_size: 15_f32, + } + } +} + +impl Default for ThemeStyle { + fn default() -> Self { + ThemeStyle::rounded() + } +} + +impl ThemeStyle { + pub fn name(&self) -> &str { + &self.name + } + + pub fn panel_icon_size(&self) -> &IconSize { + &self.panel_icon_size + } + + pub fn panel_margin_size(&self) -> f32 { + self.panel_margin_size + } + + pub fn medium_button_size(&self) -> Vec2 { + self.medium_button_size + } + + pub fn large_button_size(&self) -> Vec2 { + self.large_button_size + } +} + +static mut THEME_STYLE_LIST: Option> = None; +pub fn theme_styles() -> &'static HashMap { + unsafe { + THEME_STYLE_LIST.get_or_insert_with(|| { + let mut themes = HashMap::new(); + [ThemeStyle::rounded(), ThemeStyle::sharp()] + .into_iter() + .for_each(|theme| { + themes.insert(theme.name.clone(), theme.clone()); + }); + themes + }) + } +} diff --git a/core/src/events.rs b/core/src/events.rs index 5cd720a..f9ab90a 100644 --- a/core/src/events.rs +++ b/core/src/events.rs @@ -1,4 +1,5 @@ use crate::imports::*; +use crate::utils::Release; use kaspa_metrics::MetricsSnapshot; use kaspa_wallet_core::{events as kaspa, storage::PrvKeyDataInfo}; @@ -6,6 +7,8 @@ pub type ApplicationEventsChannel = crate::runtime::channel::Channel; #[derive(Clone)] pub enum Events { + VersionUpdate(Release), + ThemeChange, StoreSettings, UpdateLogs, Metrics { diff --git a/core/src/imports.rs b/core/src/imports.rs index 72fb5fe..5c627fe 100644 --- a/core/src/imports.rs +++ b/core/src/imports.rs @@ -49,6 +49,7 @@ pub use workflow_log::*; pub use ahash::{AHashMap, AHashSet}; pub use pad::{Alignment, PadStr}; +pub use slug::slugify; pub use zeroize::Zeroize; pub use egui::epaint::{ @@ -76,7 +77,7 @@ pub use crate::result::Result; pub use crate::runtime::{runtime, spawn, spawn_with_result, Device, Payload, Runtime, Service}; pub use crate::settings::{ KaspadNodeKind, NetworkInterfaceConfig, NetworkInterfaceKind, NodeSettings, PluginSettings, - PluginSettingsMap, RpcConfig, Settings, UxSettings, + PluginSettingsMap, RpcConfig, Settings, UserInterfaceSettings, }; pub use crate::state::State; pub use crate::status::Status; diff --git a/core/src/menu.rs b/core/src/menu.rs index 298ead5..a1e1996 100644 --- a/core/src/menu.rs +++ b/core/src/menu.rs @@ -127,20 +127,88 @@ impl<'core> Menu<'core> { ui, "display_settings", egui_phosphor::light::MONITOR, - |ui| { - ui.label("hello world"); + |ui, close_popup| { + // ui.horizontal(|ui| { + // ui.label("Font Size"); + // ui.add( + // egui::DragValue::new(&mut self.core.settings.font_size) + // .speed(0.1) + // .clamp_range(0.5..=2.0), + // ); + // }); + + // ui.horizontal(|ui| { + // ui.label("Text Scale"); + // ui.add( + // egui::DragValue::new(&mut self.core.settings.text_scale) + // .speed(0.1) + // .clamp_range(0.5..=2.0), + // ); + // }); + + ui.horizontal(|ui| { + ui.label("Theme Color"); + + let current_theme_color_name = theme_color().name(); + ui.menu_button(format!("{} ⏷", current_theme_color_name), |ui| { + theme_colors().keys().for_each(|name| { + if name.as_str() != current_theme_color_name + && ui.button(name).clicked() + { + apply_theme_color_by_name(ui.ctx(), name); + self.core.settings.user_interface.theme_color = + name.to_string(); + self.core.store_settings(); + ui.close_menu(); + } + }); + }); + }); - if ui.button("Change").clicked() { - ui.ctx().set_visuals(Visuals::light()); - } + ui.horizontal(|ui| { + ui.label("Theme Style"); + + let current_theme_style_name = theme_style().name(); + ui.menu_button(format!("{} ⏷", current_theme_style_name), |ui| { + theme_styles().keys().for_each(|name| { + if name.as_str() != current_theme_style_name + && ui.button(name).clicked() + { + apply_theme_style_by_name(ui.ctx(), name); + self.core.settings.user_interface.theme_style = + name.to_string(); + self.core.store_settings(); + ui.close_menu(); + } + }); + }); + }); + + // if ui.button("Change").clicked() { + // if theme().name == "Light" { + // apply_theme(ui, Theme::dark()); + // } else { + // apply_theme(ui, Theme::light()); + // } + // // ui.ctx().set_visuals(Visuals::light()); + // } if self.core.settings.developer.enable_screen_capture() { ui.add_space(8.); - use egui_phosphor::thin::CAMERA; - if ui.button(CAMERA).clicked() { - ui.ctx() - .send_viewport_cmd(egui::ViewportCommand::Screenshot); - } + ui.vertical_centered(|ui| { + use egui_phosphor::light::CAMERA; + if ui + .add_sized( + vec2(32., 32.), + Button::new(RichText::new(CAMERA).size(20.)), + ) + .clicked() + { + *close_popup = true; + ui.ctx() + .send_viewport_cmd(egui::ViewportCommand::Screenshot); + } + }); } }, ) diff --git a/core/src/modules/account_create.rs b/core/src/modules/account_create.rs index c4bf97e..406236d 100644 --- a/core/src/modules/account_create.rs +++ b/core/src/modules/account_create.rs @@ -189,7 +189,7 @@ impl ModuleT for AccountCreate { |ui, text| { // ui.add_space(8.); ui.label(RichText::new("Enter account name (optional)").size(12.).raised()); - ui.add_sized(theme().panel_editor_size, TextEdit::singleline(text) + ui.add_sized(theme_style().panel_editor_size, TextEdit::singleline(text) .vertical_align(Align::Center)) }, ).submit(|_,focus| { @@ -200,7 +200,7 @@ impl ModuleT for AccountCreate { }) .with_footer(|this,ui| { - let size = theme().large_button_size; + let size = theme_style().large_button_size; if ui.add_sized(size, egui::Button::new("Continue")).clicked() { this.state = State::WalletSecret; this.context.focus = Focus::WalletSecret; @@ -232,7 +232,7 @@ impl ModuleT for AccountCreate { Focus::WalletSecret, |ui, text| { ui.label(egui::RichText::new("Enter your wallet secret").size(12.).raised()); - ui.add_sized(theme().panel_editor_size, TextEdit::singleline(text) + ui.add_sized(theme_style().panel_editor_size, TextEdit::singleline(text) .vertical_align(Align::Center) .password(true)) }, @@ -244,7 +244,7 @@ impl ModuleT for AccountCreate { .build(ui); }) .with_footer(|this,ui| { - let size = theme().large_button_size; + let size = theme_style().large_button_size; let enabled = !this.context.wallet_secret.is_empty(); if ui.add_enabled(enabled, egui::Button::new("Continue").min_size(size)).clicked() { submit_via_footer = true; @@ -283,7 +283,7 @@ impl ModuleT for AccountCreate { Focus::PaymentSecret, |ui, text| { ui.label(egui::RichText::new("Enter your BIP39 passphrase").size(12.).raised()); - ui.add_sized(theme().panel_editor_size, TextEdit::singleline(text) + ui.add_sized(theme_style().panel_editor_size, TextEdit::singleline(text) .vertical_align(Align::Center) .password(true)) }, @@ -295,7 +295,7 @@ impl ModuleT for AccountCreate { }); }) .with_footer(|this,ui| { - let size = theme().large_button_size; + let size = theme_style().large_button_size; let enabled = !this.context.payment_secret.is_empty(); if ui.add_enabled(enabled, Button::new("Continue").min_size(size)).clicked() { this.state = State::CreateAccount; @@ -384,7 +384,7 @@ impl ModuleT for AccountCreate { ui.label(egui::RichText::new("Error creating account").color(egui::Color32::from_rgb(255, 120, 120))); ui.label(egui::RichText::new(err.to_string()).color(egui::Color32::from_rgb(255, 120, 120))); - if ui.add_sized(theme().panel_editor_size, egui::Button::new("Restart")).clicked() { + if ui.add_sized(theme_style().panel_editor_size, egui::Button::new("Restart")).clicked() { this.state = State::Start; } }) diff --git a/core/src/modules/account_manager/menus.rs b/core/src/modules/account_manager/menus.rs index 576820a..4be1b5b 100644 --- a/core/src/modules/account_manager/menus.rs +++ b/core/src/modules/account_manager/menus.rs @@ -17,7 +17,7 @@ impl WalletMenu { return; }; - PopupPanel::new(ui, "wallet_selector_popup",format!("Wallet: {}", wallet_name), |ui| { + PopupPanel::new(ui, "wallet_selector_popup",format!("Wallet: {}", wallet_name), |ui, _| { ScrollArea::vertical() .id_source("wallet_selector_popup_scroll") @@ -77,7 +77,7 @@ impl AccountMenu { } pub fn render(&mut self, core: &mut Core, ui : &mut Ui, account_manager : &mut AccountManager, rc : &RenderContext<'_>, max_height: f32) { let RenderContext { account, network_type, .. } = rc; - PopupPanel::new(ui, "account_selector_popup",format!("Account: {}", account.name_or_id()), |ui| { + PopupPanel::new(ui, "account_selector_popup",format!("Account: {}", account.name_or_id()), |ui, _| { egui::ScrollArea::vertical() .id_source("account_selector_popup_scroll") @@ -126,7 +126,7 @@ impl ToolsMenu { } pub fn render(&mut self, _core: &mut Core, ui : &mut Ui, _account_manager : &mut AccountManager, _rc : &RenderContext<'_>, max_height: f32) { - PopupPanel::new(ui, "tools_popup",i18n("Tools"), |ui| { + PopupPanel::new(ui, "tools_popup",i18n("Tools"), |ui, _| { egui::ScrollArea::vertical() .id_source("tools_popup_scroll") diff --git a/core/src/modules/account_manager/mod.rs b/core/src/modules/account_manager/mod.rs index 3b3c22c..c24a7a5 100644 --- a/core/src/modules/account_manager/mod.rs +++ b/core/src/modules/account_manager/mod.rs @@ -194,7 +194,7 @@ impl ModuleT for AccountManager { ui: &mut egui::Ui, ) { if let Err(err) = self.render_state(core, ui) { - ui.colored_label(theme().error_color, err.to_string()); + ui.colored_label(theme_color().error_color, err.to_string()); } } diff --git a/core/src/modules/account_manager/overview.rs b/core/src/modules/account_manager/overview.rs index 433965d..4d46d8c 100644 --- a/core/src/modules/account_manager/overview.rs +++ b/core/src/modules/account_manager/overview.rs @@ -44,22 +44,24 @@ impl<'manager> Overview<'manager> { self.render_qr(core, ui, rc); ui.vertical_centered(|ui|{ + + ui.add_space(8.); ui.horizontal(|ui| { let mut layout = CenterLayoutBuilder::new(); - layout = layout.add(Button::new(format!("{} Send", ARROW_CIRCLE_UP)).min_size(theme().medium_button_size()), |(this, _):&mut (&mut Overview<'_>, &mut Core)| { + layout = layout.add(Button::new(format!("{} Send", ARROW_CIRCLE_UP)).min_size(theme_style().medium_button_size()), |(this, _):&mut (&mut Overview<'_>, &mut Core)| { this.context.action = Action::Estimating; this.context.transaction_kind = TransactionKind::Send; }); if core.account_collection().as_ref().map(|collection|collection.len()).unwrap_or(0) > 1 { - layout = layout.add(Button::new(format!("{} Transfer", ARROWS_DOWN_UP)).min_size(theme().medium_button_size()), |(this,_)| { + layout = layout.add(Button::new(format!("{} Transfer", ARROWS_DOWN_UP)).min_size(theme_style().medium_button_size()), |(this,_)| { this.context.action = Action::Estimating; this.context.transaction_kind = TransactionKind::Transfer; }); } - layout = layout.add(Button::new(format!("{} Request", QR_CODE)).min_size(theme().medium_button_size()), |(_,core)| { + layout = layout.add(Button::new(format!("{} Request", QR_CODE)).min_size(theme_style().medium_button_size()), |(_,core)| { core.select::(); }); @@ -82,8 +84,8 @@ impl<'manager> Overview<'manager> { ui.add_space(32.); ui.label( RichText::new(CLOUD_SLASH) - .size(theme().icon_size_large) - .color(theme().icon_color_default) + .size(theme_style().icon_size_large) + .color(theme_color().icon_color_default) ); ui.add_space(32.); @@ -93,8 +95,8 @@ impl<'manager> Overview<'manager> { ui.add_space(32.); ui.label( RichText::new(CLOUD_ARROW_DOWN) - .size(theme().icon_size_large) - .color(theme().icon_color_default) + .size(theme_style().icon_size_large) + .color(theme_color().icon_color_default) ); ui.add_space(32.); @@ -139,7 +141,7 @@ impl<'manager> Overview<'manager> { if let Some(balance) = account.balance() { ui.heading( - RichText::new(sompi_to_kaspa_string_with_suffix(balance.mature, network_type)).font(FontId::proportional(28.)).color(theme().balance_color) + RichText::new(sompi_to_kaspa_string_with_suffix(balance.mature, network_type)).font(FontId::proportional(28.)).color(theme_color().balance_color) ); if balance.pending != 0 { @@ -311,7 +313,7 @@ impl<'manager> Overview<'manager> { self.context.address_status == AddressStatus::Valid } EstimatorStatus::Error(error) => { - ui.label(RichText::new(error.to_string()).color(theme().error_color)); + ui.label(RichText::new(error.to_string()).color(theme_color().error_color)); false } EstimatorStatus::None => { @@ -325,11 +327,11 @@ impl<'manager> Overview<'manager> { ui.vertical_centered(|ui|{ ui.horizontal(|ui| { CenterLayoutBuilder::new() - .add_enabled(ready_to_send, Button::new(format!("{CHECK} Send")).min_size(theme().medium_button_size()), |this: &mut Overview<'_>| { + .add_enabled(ready_to_send, Button::new(format!("{CHECK} Send")).min_size(theme_style().medium_button_size()), |this: &mut Overview<'_>| { this.context.action = Action::Sending; this.context.focus = Focus::WalletSecret; }) - .add(Button::new(format!("{X} Cancel")).min_size(theme().medium_button_size()), |this| { + .add(Button::new(format!("{X} Cancel")).min_size(theme_style().medium_button_size()), |this| { this.context.reset_send_state(); }) .build(ui, self) @@ -396,10 +398,10 @@ impl<'manager> Overview<'manager> { ui.add_space(8.); CenterLayoutBuilder::new() - .add_enabled(is_ready_to_send, Button::new(format!("{CHECK} Submit")).min_size(theme().medium_button_size()), |_this: &mut Overview<'_>| { + .add_enabled(is_ready_to_send, Button::new(format!("{CHECK} Submit")).min_size(theme_style().medium_button_size()), |_this: &mut Overview<'_>| { proceed_with_send = true; }) - .add(Button::new(format!("{X} Cancel")).min_size(theme().medium_button_size()), |this| { + .add(Button::new(format!("{X} Cancel")).min_size(theme_style().medium_button_size()), |this| { this.context.action = Action::Estimating; }) .build(ui,self); diff --git a/core/src/modules/block_dag.rs b/core/src/modules/block_dag.rs index 8b57875..abc9fdc 100644 --- a/core/src/modules/block_dag.rs +++ b/core/src/modules/block_dag.rs @@ -51,9 +51,9 @@ impl ModuleT for BlockDag { fn status_bar(&self, core: &mut Core, ui : &mut Ui) { ui.separator(); if !core.state().is_connected() { - ui.label(RichText::new(i18n("You must be connected to a node...")).color(theme().error_color)); + ui.label(RichText::new(i18n("You must be connected to a node...")).color(theme_color().error_color)); } else if !core.state().is_synced() { - ui.label(RichText::new(i18n("Please wait for the node to sync...")).color(theme().warning_color)); + ui.label(RichText::new(i18n("Please wait for the node to sync...")).color(theme_color().warning_color)); } else { ui.label(i18n("Double click on the graph to re-center...")); } @@ -66,19 +66,20 @@ impl ModuleT for BlockDag { _frame: &mut eframe::Frame, ui: &mut egui::Ui, ) { - let theme = theme(); + let theme_color = theme_color(); let y_dist = self.settings.y_dist; - let vspc_center = self.settings.vspc_center; + let vspc_center = self.settings.center_vspc; ui.horizontal(|ui| { ui.heading("Block DAG"); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - PopupPanel::new(ui, "block_dag_settings","Settings", |ui| { + PopupPanel::new(ui, "block_dag_settings","Settings", |ui, _| { ui.add( - Slider::new(&mut self.daa_range, 1.0..=100.0) + Slider::new(&mut self.daa_range, 1.0..=self.settings.graph_length_daa as f64) .clamp_to_range(true) + .logarithmic(true) .text(i18n("DAA Range")) ); ui.space(); @@ -105,7 +106,13 @@ impl ModuleT for BlockDag { ui.separator(); ui.space(); ui.horizontal_wrapped(|ui| { - ui.checkbox(&mut self.settings.vspc_center, i18n("Center VSPC")); + ui.checkbox(&mut self.settings.center_vspc, i18n("Center VSPC")); + ui.space(); + ui.checkbox(&mut self.settings.show_vspc, i18n("Show VSPC")); + ui.space(); + ui.checkbox(&mut self.settings.show_grid, i18n("Show Grid")); + ui.space(); + ui.checkbox(&mut self.settings.show_daa, i18n("Show DAA")); ui.space(); ui.checkbox(&mut self.bezier, i18n("Bezier Curves")); ui.space(); @@ -121,7 +128,7 @@ impl ModuleT for BlockDag { }); ui.separator(); - if y_dist != self.settings.y_dist || vspc_center != self.settings.vspc_center { + if y_dist != self.settings.y_dist || vspc_center != self.settings.center_vspc { runtime().block_dag_monitor_service().update_settings(self.settings.clone()); } @@ -161,8 +168,8 @@ impl ModuleT for BlockDag { .include_y(-15.) .data_aspect(0.2) .y_axis_width(0) - .show_axes([true, false]) - .show_grid(true) + .show_axes([self.settings.show_daa, false]) + .show_grid(self.settings.show_grid) .allow_drag([true, true]) .allow_scroll(true) .allow_double_click_reset(true) @@ -199,14 +206,23 @@ impl ModuleT for BlockDag { let daa_margin = 10; let daa_min = self.plot_bounds.min()[0] as u64 - daa_margin; let daa_max = self.plot_bounds.max()[0] as u64 + daa_margin; + let blocks = if let Ok(mut daa_buckets) = self.runtime.block_dag_monitor_service().chain.lock() { daa_buckets.iter_mut().filter_map(|(daa_score,bucket)| { - (*daa_score > daa_min || *daa_score < daa_max).then_some(bucket) + (*daa_score > daa_min && *daa_score < daa_max).then_some(bucket) }).flat_map(DaaBucket::render).collect::>() } else { return; }; + // let separators = if let Ok(separators) = self.runtime.block_dag_monitor_service().separators.lock() { + // separators.iter().filter_map(|daa_score| { + // (*daa_score > daa_min || *daa_score < daa_max).then_some(*daa_score) + // }).collect::>() + // } else { + // return; + // }; + let parent_levels = self.parent_levels.max(1); let block_map : AHashMap = blocks.clone().into_iter().map(|(block, plot_point,vspc, _)|(block.header.hash,(plot_point,vspc))).collect(); let new_blocks = self.runtime.block_dag_monitor_service().new_blocks().clone(); @@ -237,10 +253,10 @@ impl ModuleT for BlockDag { [parent_x, parent_y], ].into_iter().map(|pt|pt.into()).collect::>() }; - if level == 0 && *current_vspc && parent_vspc { - lines_vspc.push(Line::new(PlotPoints::Owned(points)).color(theme.block_dag_vspc_connect_color).style(LineStyle::Solid).width(3.0)); + if self.settings.show_vspc && level == 0 && *current_vspc && parent_vspc { + lines_vspc.push(Line::new(PlotPoints::Owned(points)).color(theme_color.block_dag_vspc_connect_color).style(LineStyle::Solid).width(3.0)); } else { - lines_parent.push(Line::new(PlotPoints::Owned(points)).color(theme.block_dag_parent_connect_color).style(LineStyle::Solid)); + lines_parent.push(Line::new(PlotPoints::Owned(points)).color(theme_color.block_dag_parent_connect_color).style(LineStyle::Solid)); } } } @@ -255,21 +271,33 @@ impl ModuleT for BlockDag { ].to_vec().into(); let fill_color = if new_blocks.contains(&block.header.hash) { - theme.block_dag_new_block_fill_color + theme_color.block_dag_new_block_fill_color } else { - theme.block_dag_block_fill_color + theme_color.block_dag_block_fill_color }; Polygon::new(points) .name(block.header.hash.to_string()) .fill_color(fill_color) - .stroke(Stroke::new(1.0, theme.block_dag_block_stroke_color)) + .stroke(Stroke::new(1.0, theme_color.block_dag_block_stroke_color)) .style(LineStyle::Solid) }).collect::>(); + // let lines_separators = separators.iter().map(|daa_score| { + // let x = *daa_score as f64; + // let points: PlotPoints = [ + // [x, 0.0 - y_dist], + // [x, 0.0 + y_dist], + // ].to_vec().into(); + // Line::new(points).color(theme_color.block_dag_separator_color).style(LineStyle::Dotted { spacing: 0.75 }) + // }).collect::>(); + let plot_response = plot.show(ui, |plot_ui| { + // lines_separators.into_iter().for_each(|line| { + // plot_ui.line(line); + // }); lines_parent.into_iter().for_each(|line| { plot_ui.line(line); }); @@ -290,14 +318,14 @@ impl ModuleT for BlockDag { } - fn activate(&mut self, _core: &mut Core) { - crate::runtime::runtime().block_dag_monitor_service().enable(); + fn activate(&mut self, core: &mut Core) { + crate::runtime::runtime().block_dag_monitor_service().enable(core.state().current_daa_score().map(|score|score - 2)); } - fn deactivate(&mut self, _core: &mut Core) { + fn deactivate(&mut self, core: &mut Core) { if !self.background { self.running = false; - crate::runtime::runtime().block_dag_monitor_service().disable(); + crate::runtime::runtime().block_dag_monitor_service().disable(core.state().current_daa_score()); } } } diff --git a/core/src/modules/export.rs b/core/src/modules/export.rs index 1bb7cd4..2bf9519 100644 --- a/core/src/modules/export.rs +++ b/core/src/modules/export.rs @@ -278,7 +278,7 @@ impl ModuleT for Export { Focus::WalletSecret, |ui, text| { ui.label(egui::RichText::new("Enter your wallet secret").size(12.).raised()); - ui.add_sized(theme().panel_editor_size, TextEdit::singleline(text) + ui.add_sized(theme_style().panel_editor_size, TextEdit::singleline(text) .vertical_align(Align::Center) .password(true)) }, diff --git a/core/src/modules/logs.rs b/core/src/modules/logs.rs index 330bc87..8fa87dc 100644 --- a/core/src/modules/logs.rs +++ b/core/src/modules/logs.rs @@ -36,7 +36,7 @@ impl ModuleT for Logs { } }); - let copy_to_clipboard = Button::new(format!(" {CLIPBOARD_TEXT} ")); + let copy_to_clipboard = Button::new(RichText::new(format!(" {CLIPBOARD_TEXT} ")).size(20.)); let screen_rect = ui.ctx().screen_rect(); let button_rect = Rect::from_min_size( diff --git a/core/src/modules/metrics.rs b/core/src/modules/metrics.rs index 41de25d..e3c43c1 100644 --- a/core/src/modules/metrics.rs +++ b/core/src/modules/metrics.rs @@ -41,17 +41,17 @@ impl ModuleT for Metrics { let mut store_settings = false; - let mut graph_columns = core.settings.ux.metrics.graph_columns; - let mut graph_height = core.settings.ux.metrics.graph_height; + let mut graph_columns = core.settings.user_interface.metrics.graph_columns; + let mut graph_height = core.settings.user_interface.metrics.graph_height; #[allow(unused_mut)] - let mut graph_range_from = core.settings.ux.metrics.graph_range_from; - let mut graph_range_to = core.settings.ux.metrics.graph_range_to; + let mut graph_range_from = core.settings.user_interface.metrics.graph_range_from; + let mut graph_range_to = core.settings.user_interface.metrics.graph_range_to; ui.horizontal(|ui|{ ui.heading("Node Metrics"); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - PopupPanel::new(ui, "metrics_settings","Settings", |ui| { + PopupPanel::new(ui, "metrics_settings","Settings", |ui, _| { ui.add( Slider::new(&mut graph_columns, 1..=8) .text("Columns") @@ -97,15 +97,15 @@ impl ModuleT for Metrics { ui.horizontal(|ui| { if ui.button(i18n("All")).clicked() { - core.settings.ux.metrics.disabled.clear(); + core.settings.user_interface.metrics.disabled.clear(); } if ui.button(i18n("None")).clicked() { - core.settings.ux.metrics.disabled = Metric::list().into_iter().collect::>(); + core.settings.user_interface.metrics.disabled = Metric::list().into_iter().collect::>(); } if ui.button(i18n("Key Perf.")).clicked() { - core.settings.ux.metrics.disabled = Metric::list().into_iter().filter(|metric|!metric.is_key_performance_metric()).collect::>(); + core.settings.user_interface.metrics.disabled = Metric::list().into_iter().filter(|metric|!metric.is_key_performance_metric()).collect::>(); } }); @@ -119,12 +119,12 @@ impl ModuleT for Metrics { for metric in group.metrics() { ui.space(); - let mut state = !core.settings.ux.metrics.disabled.contains(&metric); + let mut state = !core.settings.user_interface.metrics.disabled.contains(&metric); if ui.checkbox(&mut state, i18n(metric.title().0)).changed() { if state { - core.settings.ux.metrics.disabled.remove(&metric); + core.settings.user_interface.metrics.disabled.remove(&metric); } else { - core.settings.ux.metrics.disabled.insert(metric); + core.settings.user_interface.metrics.disabled.insert(metric); } // core.store_settings(); store_settings = true; @@ -162,15 +162,15 @@ impl ModuleT for Metrics { } if store_settings - || graph_columns != core.settings.ux.metrics.graph_columns - || graph_height != core.settings.ux.metrics.graph_height - || graph_range_from != core.settings.ux.metrics.graph_range_from - || graph_range_to != core.settings.ux.metrics.graph_range_to + || graph_columns != core.settings.user_interface.metrics.graph_columns + || graph_height != core.settings.user_interface.metrics.graph_height + || graph_range_from != core.settings.user_interface.metrics.graph_range_from + || graph_range_to != core.settings.user_interface.metrics.graph_range_to { - core.settings.ux.metrics.graph_columns = graph_columns; - core.settings.ux.metrics.graph_height = graph_height; - core.settings.ux.metrics.graph_range_from = graph_range_from; - core.settings.ux.metrics.graph_range_to = graph_range_to; + core.settings.user_interface.metrics.graph_columns = graph_columns; + core.settings.user_interface.metrics.graph_height = graph_height; + core.settings.user_interface.metrics.graph_range_from = graph_range_from; + core.settings.user_interface.metrics.graph_range_to = graph_range_to; core.store_settings(); } @@ -185,17 +185,17 @@ impl ModuleT for Metrics { .show(ui, |ui| { let view_width = ui.available_width() - 32.; - let graph_height = core.settings.ux.metrics.graph_height as f32; - let graph_width = view_width / core.settings.ux.metrics.graph_columns as f32; + let graph_height = core.settings.user_interface.metrics.graph_height as f32; + let graph_width = view_width / core.settings.user_interface.metrics.graph_columns as f32; - let mut metric_iter = Metric::list().into_iter().filter(|metric| !core.settings.ux.metrics.disabled.contains(metric)); + let mut metric_iter = Metric::list().into_iter().filter(|metric| !core.settings.user_interface.metrics.disabled.contains(metric)); let mut draw = true; while draw { ui.horizontal(|ui| { - for _ in 0..core.settings.ux.metrics.graph_columns { + for _ in 0..core.settings.user_interface.metrics.graph_columns { if let Some(metric) = metric_iter.next() { - let range_from = core.settings.ux.metrics.graph_range_from; - let range_to = core.settings.ux.metrics.graph_range_to; + let range_from = core.settings.user_interface.metrics.graph_range_from; + let range_to = core.settings.user_interface.metrics.graph_range_to; self.render_metric(ui,metric,metrics,range_from..range_to,graph_width,graph_height); } else { draw = false; diff --git a/core/src/modules/node.rs b/core/src/modules/node.rs index ec36a6a..3311f5f 100644 --- a/core/src/modules/node.rs +++ b/core/src/modules/node.rs @@ -74,7 +74,7 @@ impl ModuleT for Node { ui.label("Updating..."); }); } else { - ui.colored_label(theme().warning_color, i18n("No peers")); + ui.colored_label(theme_color().warning_color, i18n("No peers")); } }); @@ -96,7 +96,7 @@ impl ModuleT for Node { fn render_peer(ui : &mut Ui, peer: &RpcPeerInfo) { - let color = theme().node_data_color; + let color = theme_color().node_data_color; CollapsingHeader::new(peer.id.to_string()) .default_open(true) diff --git a/core/src/modules/overview.rs b/core/src/modules/overview.rs index d088cd1..bf18444 100644 --- a/core/src/modules/overview.rs +++ b/core/src/modules/overview.rs @@ -79,7 +79,7 @@ impl Overview { ui.add_space(48.); } - fn render_details(&mut self, _core: &mut Core, ui : &mut Ui) { + fn render_details(&mut self, core: &mut Core, ui : &mut Ui) { let screen_rect = ui.ctx().screen_rect(); let logo_size = vec2(648., 994.,) * 0.25; @@ -115,7 +115,7 @@ impl Overview { // egui::special_emojis // use egui_phosphor::light::{DISCORD_LOGO,GITHUB_LOGO}; ui.hyperlink_to_tab( - format!("• {}",i18n("Kaspa NextGen on GitHub")), + format!("• {}",i18n("Kaspa NG on GitHub")), "https://github.com/aspectron/kaspa-ng" ); ui.hyperlink_to_tab( @@ -140,29 +140,44 @@ impl Overview { ); }); - let version = env!("CARGO_PKG_VERSION"); - let download = |platform: &str| { format!("https://github.com/aspectron/kaspa-ng/releases/download/{}/kaspa-ng-{}-{}.zip", version, version, platform) }; - CollapsingHeader::new(i18n("Redistributables")) - .default_open(false) - .show(ui, |ui| { - ["windows-x64", "linux-gnu-amd64", "macos-arm64"].into_iter().for_each(|platform| { - Hyperlink::from_label_and_url( - format!("• kaspa-ng-{}-{}.zip", version, platform), - download(platform), - ).open_in_new_tab(true).ui(ui); - }); - }); + if let Some(release) = core.release.as_ref() { + if release.version == crate::app::VERSION { + CollapsingHeader::new(i18n("Redistributables")) + .id_source("redistributables") + .default_open(false) + .show(ui, |ui| { + release.assets.iter().for_each(|asset| { + Hyperlink::from_label_and_url( + format!("• {}", asset.name), + asset.browser_download_url.clone(), + ).open_in_new_tab(true).ui(ui); + }); + }); + } else { + CollapsingHeader::new(RichText::new(format!("{} {}",i18n("Update Available to version"), release.version)).color(theme_color().alert_color).strong()) + .id_source("redistributables-update") + .default_open(true) + .show(ui, |ui| { + + if let Some(html_url) = &release.html_url { + Hyperlink::from_label_and_url( + format!("• {} {}", i18n("GitHub Release"), release.version), + html_url, + ).open_in_new_tab(true).ui(ui); + } + + release.assets.iter().for_each(|asset| { + Hyperlink::from_label_and_url( + format!("• {}", asset.name), + asset.browser_download_url.clone(), + ).open_in_new_tab(true).ui(ui); + }); - CollapsingHeader::new(i18n("Music")) - .default_open(true) - .show(ui, |ui| { - ui.label("TODO"); - }); + }); - if let Some(system) = runtime().system() { - system.render(ui); + } } - + CollapsingHeader::new(i18n("Build")) .default_open(true) .show(ui, |ui| { @@ -177,10 +192,12 @@ impl Overview { ui.label(format!("architecture {}", crate::app::CARGO_TARGET_TRIPLE )); - ui.label(format!("Codename: \"{}\"", crate::app::CODENAME)); }); - + if let Some(system) = runtime().system() { + system.render(ui); + } + CollapsingHeader::new(i18n("License Information")) .default_open(false) .show(ui, |ui| { @@ -193,7 +210,7 @@ impl Overview { ui.label("Kaspa NG"); ui.label("Copyright (c) 2023 ASPECTRON"); ui.label("License: MIT or Apache 2.0"); - ui.hyperlink_url_to_tab("https://aspectron.com"); + ui.hyperlink_url_to_tab("https://github.com/aspectron/kaspa-ng"); ui.label(""); ui.label("WORKFLOW-RS"); ui.label("Copyright (c) 2023 ASPECTRON"); @@ -210,7 +227,7 @@ impl Overview { ui.label("License: MIT"); ui.hyperlink_url_to_tab("https://phosphoricons.com/"); ui.label(""); - ui.label("Graphics Design (Illustration Art)"); + ui.label("Illustration Art"); ui.label("Copyright (c) 2023 Rhubarb Media"); ui.label("License: CC BY 4.0"); ui.hyperlink_url_to_tab("https://rhubarbmedia.ca/"); @@ -237,7 +254,6 @@ impl Overview { "Gennady Gorin", "hashdag", "Helix", - "Helix", "jablonx", "jwj", "KaffinPX", @@ -266,7 +282,11 @@ impl Overview { .default_open(true) .show(ui, |ui| { ui.label("Please support Kaspa NG development"); - ui.label("kaspatest:qqdr2mv4vkes6kvhgy8elsxhvzwde42629vnpcxe4f802346rnfkklrhz0x7x"); + // if ui.link("kaspatest:qqdr2mv4vkes6kvhgy8elsxhvzwde42629vnpcxe4f802346rnfkklrhz0x7x").clicked() { + let donation_address = "kaspatest:qqdr2mv4vkes6kvhgy8elsxhvzwde42629vnpcxe4f802346rnfkklrhz0x7x"; + if ui.link(format_address(&Address::try_from(donation_address).unwrap(), Some(12))).clicked() { + println!("link clicked..."); + } }); }); @@ -285,7 +305,6 @@ impl Overview { let mut metric_iter = METRICS.iter(); if let Some(snapshot) = core.metrics.as_ref() { - let theme = theme(); let view_width = ui.available_width(); if view_width < 200. { return; @@ -298,7 +317,7 @@ impl Overview { for _ in 0..graph_columns { if let Some(metric) = metric_iter.next() { let value = snapshot.get(metric); - self.render_graph(ui, *metric, value, theme); + self.render_graph(ui, *metric, value); } else { draw = false; } @@ -309,7 +328,7 @@ impl Overview { } - fn render_graph(&mut self, ui : &mut Ui, metric : Metric, value : f64, theme : &Theme) { + fn render_graph(&mut self, ui : &mut Ui, metric : Metric, value : f64) { let group = MetricGroup::from(metric); let graph_color = group.to_color(); @@ -328,56 +347,61 @@ impl Overview { ui.vertical(|ui|{ + let frame = Frame::none() - // .fill(theme.performance_graph_color) - .stroke(Stroke::new(1.0, theme.graph_frame_color)) - .inner_margin(4.) + // .fill(Color32::from_rgb(240,240,240)) + .stroke(Stroke::new(1.0, theme_color().graph_frame_color)) + // .inner_margin(4.) + .inner_margin(Margin { left: 3., right: 3., top: 4., bottom: 4. }) .outer_margin(8.) - .rounding(8.) - .show(ui, |ui| { - - let mut plot = Plot::new(metric.as_str()) - .legend(Legend::default()) - .width(128.) - .height(32.) - .auto_bounds_x() - .auto_bounds_y() - .set_margin_fraction(vec2(0.0,0.0) ) - .show_axes(false) - .show_grid(false) - .allow_drag([false, false]) - .allow_scroll(false) - .show_background(false) - .show_x(false) - .show_y(false) - ; - - if [Metric::NodeCpuUsage].contains(&metric) { - plot = plot.include_y(100.); - } - - let color = graph_color.gamma_multiply(0.5); - let line = Line::new(PlotPoints::Owned(graph_data)) - .color(color) - .style(LineStyle::Solid) - .fill(0.0); - - let plot_result = plot.show(ui, |plot_ui| { - plot_ui.line(line); - }); - - let text = format!("{} {}", i18n(metric.title().1).to_uppercase(), metric.format(value, true, true)); - let rich_text_top = egui::RichText::new(&text).size(10.).color(Color32::WHITE);//.background_color(Color32::from_black_alpha(32)); - let rich_text_back = egui::RichText::new(text).size(10.).color(Color32::BLACK); - let label_top = Label::new(rich_text_top).wrap(false); - let label_back = Label::new(rich_text_back).wrap(false); - let mut rect_top = plot_result.response.rect; - rect_top.set_bottom(rect_top.top() + 12.); - let mut rect_back = rect_top; - rect_back.set_center(rect_back.center()+vec2(0.8,0.8)); - ui.put(rect_back, label_back); - ui.put(rect_top, label_top); + // .rounding(8.) + .rounding(6.); + + frame.show(ui, |ui| { + + let mut plot = Plot::new(metric.as_str()) + .legend(Legend::default()) + .width(128.) + .height(32.) + .auto_bounds_x() + .auto_bounds_y() + .set_margin_fraction(vec2(0.0,0.0) ) + .show_axes(false) + .show_grid(false) + .allow_drag([false, false]) + .allow_scroll(false) + .show_background(false) + .show_x(false) + .show_y(false) + ; + + if [Metric::NodeCpuUsage].contains(&metric) { + plot = plot.include_y(100.); + } + + // let color = graph_color.gamma_multiply(0.5); + let line = Line::new(PlotPoints::Owned(graph_data)) + // .color(color) + .color(graph_color) + .style(LineStyle::Solid) + .fill(0.0); + + let plot_result = plot.show(ui, |plot_ui| { + plot_ui.line(line); }); + + let text = format!("{} {}", i18n(metric.title().1).to_uppercase(), metric.format(value, true, true)); + let rich_text_top = egui::RichText::new(&text).size(10.).color(theme_color().raised_text_color); + let rich_text_back = egui::RichText::new(text).size(10.).color(theme_color().raised_text_shadow); + let label_top = Label::new(rich_text_top).wrap(false); + let label_back = Label::new(rich_text_back).wrap(false); + let mut rect_top = plot_result.response.rect; + rect_top.set_bottom(rect_top.top() + 12.); + let mut rect_back = rect_top; + rect_back.set_center(rect_back.center()+vec2(0.8,0.8)); + ui.put(rect_back, label_back); + ui.put(rect_top, label_top); + }); }); } } diff --git a/core/src/modules/private_key_create.rs b/core/src/modules/private_key_create.rs index 551560f..f731c3d 100644 --- a/core/src/modules/private_key_create.rs +++ b/core/src/modules/private_key_create.rs @@ -159,7 +159,7 @@ impl ModuleT for PrivateKeyCreate { }) .with_footer(|_this,ui| { // if ui.add_sized(theme().large_button_size, egui::Button::new("Continue")).clicked() { - let size = theme().large_button_size; + let size = theme_style().large_button_size; if ui.add_sized(size, egui::Button::new("Continue")).clicked() { // this.state = State::WalletName; } @@ -191,7 +191,7 @@ impl ModuleT for PrivateKeyCreate { ); }) .with_footer(|this,ui| { - let size = theme().large_button_size; + let size = theme_style().large_button_size; if ui.add_sized(size, egui::Button::new("Continue")).clicked() { this.state = State::AccountName; } @@ -245,7 +245,7 @@ impl ModuleT for PrivateKeyCreate { } }) .with_footer(|this,ui| { - let size = theme().large_button_size; + let size = theme_style().large_button_size; let ok = this.args.payment_secret == this.args.payment_secret_confirm;// && this.args.wallet_secret.len() > 0; if ui.add_enabled(ok, egui::Button::new("Continue").min_size(size)).clicked() { this.state = State::Start; diff --git a/core/src/modules/settings/mod.rs b/core/src/modules/settings/mod.rs index 2f18977..1d91f28 100644 --- a/core/src/modules/settings/mod.rs +++ b/core/src/modules/settings/mod.rs @@ -119,7 +119,7 @@ impl ModuleT for Settings { if path.exists() && !path.is_file() { ui.label( RichText::new(format!("Rusty Kaspa Daemon not found at '{path}'", path = self.settings.node.kaspad_daemon_binary)) - .color(theme().error_color), + .color(theme_color().error_color), ); node_settings_error = Some("Rusty Kaspa Daemon not found"); } @@ -160,7 +160,7 @@ impl ModuleT for Settings { if let Err(err) = KaspaRpcClient::parse_url(self.settings.node.wrpc_url.clone(), self.settings.node.wrpc_encoding, self.settings.node.network.into()) { ui.label( RichText::new(format!("{err}")) - .color(theme().warning_color), + .color(theme_color().warning_color), ); node_settings_error = Some("Invalid wRPC URL"); } @@ -222,7 +222,7 @@ impl ModuleT for Settings { if let Some(error) = node_settings_error { ui.label( RichText::new(error.to_string()) - .color(theme().warning_color), + .color(theme_color().warning_color), ); ui.add_space(16.); ui.label(i18n("Unable to change node settings until the problem is resolved.")); @@ -237,6 +237,7 @@ impl ModuleT for Settings { if let Some(response) = ui.confirm_medium_apply_cancel(Align::Max) { match response { Confirm::Ack => { + core.settings = self.settings.clone(); core.settings.store_sync().unwrap(); if restart { @@ -245,6 +246,7 @@ impl ModuleT for Settings { }, Confirm::Nack => { self.settings = core.settings.clone(); + self.grpc_network_interface = NetworkInterfaceEditor::try_from(&self.settings.node.grpc_network_interface).unwrap(); } } } diff --git a/core/src/modules/welcome.rs b/core/src/modules/welcome.rs index 969fc73..a46cc1c 100644 --- a/core/src/modules/welcome.rs +++ b/core/src/modules/welcome.rs @@ -89,17 +89,42 @@ impl ModuleT for Welcome { }); ui.add_space(16.); - ui.label("Theme:"); + ui.label("Theme Color:"); - egui::ComboBox::from_id_source("theme_selector") - .selected_text("Dark") + let mut theme_color = self.settings.user_interface.theme_color.clone(); + egui::ComboBox::from_id_source("theme_color_selector") + .selected_text(theme_color.as_str()) .show_ui(ui, |ui| { ui.style_mut().wrap = Some(false); ui.set_min_width(60.0); - ["Dark","Light"].into_iter().for_each(|theme| { - ui.selectable_value(&mut self.settings.theme, theme.to_string(), theme); + theme_colors().keys().for_each(|name| { + ui.selectable_value(&mut theme_color, name.to_string(), name); }); }); + + if theme_color != self.settings.user_interface.theme_color { + self.settings.user_interface.theme_color = theme_color; + apply_theme_color_by_name(ui.ctx(), self.settings.user_interface.theme_color.clone()); + } + + ui.add_space(16.); + ui.label("Theme Style:"); + + let mut theme_style = self.settings.user_interface.theme_style.clone(); + egui::ComboBox::from_id_source("theme_style_selector") + .selected_text(theme_style.as_str()) + .show_ui(ui, |ui| { + ui.style_mut().wrap = Some(false); + ui.set_min_width(60.0); + theme_styles().keys().for_each(|name| { + ui.selectable_value(&mut theme_style, name.to_string(), name); + }); + }); + + if theme_style != self.settings.user_interface.theme_style { + self.settings.user_interface.theme_style = theme_style; + apply_theme_style_by_name(ui.ctx(), self.settings.user_interface.theme_style.clone()); + } }); }); @@ -108,7 +133,7 @@ impl ModuleT for Welcome { ui.add_space( ui.available_width() - 16. - - (theme().medium_button_size.x + ui.spacing().item_spacing.x), + - (theme_style().medium_button_size.x + ui.spacing().item_spacing.x), ); if ui.medium_button(format!("{} {}", egui_phosphor::light::CHECK, "Apply")).clicked() { let mut settings = self.settings.clone(); diff --git a/core/src/primitives/account.rs b/core/src/primitives/account.rs index 138190b..dcb874d 100644 --- a/core/src/primitives/account.rs +++ b/core/src/primitives/account.rs @@ -101,6 +101,11 @@ impl Account { self.inner.balance.lock().unwrap().clone() } + pub fn update_theme(&self) { + let descriptor = self.descriptor().clone(); + *self.inner.context.lock().unwrap() = AccountContext::new(&descriptor); + } + // pub fn address(&self) -> Result { // self.inner.context.lock().unwrap().receive_address // Ok(self.inner.runtime.receive_address()?.into()) diff --git a/core/src/primitives/block.rs b/core/src/primitives/block.rs index 7d74895..19ebdb1 100644 --- a/core/src/primitives/block.rs +++ b/core/src/primitives/block.rs @@ -6,7 +6,10 @@ pub struct BlockDagGraphSettings { pub y_scale: f64, pub y_dist: f64, pub graph_length_daa: usize, - pub vspc_center: bool, + pub center_vspc: bool, + pub show_vspc: bool, + pub show_daa: bool, + pub show_grid: bool, } impl Default for BlockDagGraphSettings { @@ -16,7 +19,10 @@ impl Default for BlockDagGraphSettings { // y_dist: 70.0, y_dist: 7.0, graph_length_daa: 1024, - vspc_center: false, + center_vspc: false, + show_vspc: true, + show_daa: true, + show_grid: true, } } } @@ -75,7 +81,7 @@ impl DaaBucket { if let Some(block) = self.blocks.iter_mut().find(|b| b.data.header.hash == hash) { block.vspc = flag; block.settled = false; - if flag && settings.vspc_center { + if flag && settings.center_vspc { block.dst_y = 0.0; } else { block.dst_y = hash_to_y_coord(&block.data.header.hash, settings.y_scale); @@ -92,7 +98,7 @@ impl DaaBucket { let y_distance = settings.y_dist; let len = self.blocks.len(); if let Some(mut vspc_idx) = self.blocks.iter().position(|block| block.vspc) { - if settings.vspc_center && len > 2 { + if settings.center_vspc && len > 2 { let mid = len / 2; if vspc_idx != mid { self.blocks.swap(vspc_idx, mid); @@ -103,7 +109,7 @@ impl DaaBucket { } } - let vspc_y = if settings.vspc_center { + let vspc_y = if settings.center_vspc { 0.0 } else { self.blocks @@ -138,7 +144,7 @@ impl DaaBucket { self.blocks.iter_mut().for_each(|block| { // block.dst_y = hash_to_y_coord(&block.data.header.hash, settings.y_scale); block.settled = false; - if block.vspc && settings.vspc_center { + if block.vspc && settings.center_vspc { block.dst_y = 0.0; } else { block.dst_y = hash_to_y_coord(&block.data.header.hash, settings.y_scale); diff --git a/core/src/primitives/transaction.rs b/core/src/primitives/transaction.rs index 2d3637d..2ea2e1a 100644 --- a/core/src/primitives/transaction.rs +++ b/core/src/primitives/transaction.rs @@ -13,12 +13,12 @@ pub trait AsColor { impl AsColor for TransactionType { fn as_color(&self) -> Color32 { match self { - TransactionType::Incoming => Color32::from_rgb(162, 245, 187), - TransactionType::Outgoing => Color32::from_rgb(245, 162, 162), - TransactionType::External => Color32::from_rgb(162, 245, 187), - TransactionType::Reorg => Color32::from_rgb(79, 64, 64), - TransactionType::Batch => Color32::GRAY, - TransactionType::Stasis => Color32::GRAY, + TransactionType::Incoming => theme_color().transaction_incoming, + TransactionType::Outgoing => theme_color().transaction_outgoing, + TransactionType::External => theme_color().transaction_external, + TransactionType::Reorg => theme_color().transaction_reorg, + TransactionType::Batch => theme_color().transaction_batch, + TransactionType::Stasis => theme_color().transaction_stasis, } } } @@ -154,11 +154,8 @@ impl Transaction { // let short_id = transaction_id.chars().take(10).collect::() + "..."; // let suffix = kaspa_suffix(&network_type); - let (default_color, strong_color) = if ui.visuals().dark_mode { - (Color32::LIGHT_GRAY, Color32::WHITE) //Color32::from_rgb(125, 125, 125)) - } else { - (Color32::DARK_GRAY, Color32::BLACK) - }; + let default_color = theme_color().default_color; + let strong_color = theme_color().strong_color; let font_id_header = FontId::monospace(15.0); let font_id_content = FontId::monospace(15.0); diff --git a/core/src/runtime/services/blockdag_monitor.rs b/core/src/runtime/services/blockdag_monitor.rs index ae1e1c2..ea60a93 100644 --- a/core/src/runtime/services/blockdag_monitor.rs +++ b/core/src/runtime/services/blockdag_monitor.rs @@ -7,7 +7,7 @@ use kaspa_rpc_core::{RpcBlock, VirtualChainChangedNotification}; pub enum BlockDagMonitorEvents { Enable, Disable, - Settings(BlockDagGraphSettings), + Settings(Arc), Exit, } @@ -21,8 +21,9 @@ pub struct BlockDagMonitorService { is_enabled: Arc, is_connected: Arc, pub chain: Mutex>, + pub separators: Mutex>, pub new_blocks: Arc>>, - pub settings: Arc, + pub settings: Mutex>, } impl BlockDagMonitorService { @@ -35,10 +36,11 @@ impl BlockDagMonitorService { listener_id: Mutex::new(None), notification_channel: Channel::::unbounded(), chain: Mutex::new(AHashMap::new()), + separators: Mutex::new(Vec::new()), new_blocks: Arc::new(Mutex::new(AHashSet::new())), is_enabled: Arc::new(AtomicBool::new(false)), is_connected: Arc::new(AtomicBool::new(false)), - settings: Arc::new(BlockDagGraphSettings::default()), + settings: Mutex::new(Arc::new(BlockDagGraphSettings::default())), } } @@ -80,24 +82,47 @@ impl BlockDagMonitorService { self.rpc_api.lock().unwrap().clone() } - pub fn enable(&self) { + pub fn enable(&self, _current_daa_score: Option) { self.service_events .sender .try_send(BlockDagMonitorEvents::Enable) .unwrap(); + // if let Some(current_daa_score) = current_daa_score { + // self._update_separators(current_daa_score); + // } } - pub fn disable(&self) { + pub fn disable(&self, _current_daa_score: Option) { self.service_events .sender .try_send(BlockDagMonitorEvents::Disable) .unwrap(); + // if let Some(current_daa_score) = current_daa_score { + // self._update_separators(current_daa_score); + // } + } + + fn _update_separators(&self, current_daa_score: u64) { + let graph_length_daa = self.settings.lock().unwrap().graph_length_daa as u64; + let mut separators = self.separators.lock().unwrap(); + while let Some(first) = separators.first() { + if *first < (current_daa_score - graph_length_daa) { + separators.remove(0); + } else { + break; + } + } + separators.push(current_daa_score); + } + + pub fn separators(&self) -> MutexGuard<'_, Vec> { + self.separators.lock().unwrap() } pub fn update_settings(&self, settings: BlockDagGraphSettings) { self.service_events .sender - .try_send(BlockDagMonitorEvents::Settings(settings)) + .try_send(BlockDagMonitorEvents::Settings(Arc::new(settings))) .unwrap(); } @@ -160,7 +185,7 @@ impl Service for BlockDagMonitorService { let mut blocks_by_hash: AHashMap> = AHashMap::default(); - let mut settings = (*self.settings).clone(); + let mut settings = (*self.settings.lock().unwrap()).clone(); loop { select! { @@ -260,6 +285,7 @@ impl Service for BlockDagMonitorService { break; } BlockDagMonitorEvents::Settings(new_settings) => { + *self.settings.lock().unwrap() = new_settings.clone(); settings = new_settings; let mut chain = self.chain.lock().unwrap(); for bucket in chain.values_mut() { diff --git a/core/src/runtime/services/kaspa/logs.rs b/core/src/runtime/services/kaspa/logs.rs index c78aae7..6293067 100644 --- a/core/src/runtime/services/kaspa/logs.rs +++ b/core/src/runtime/services/kaspa/logs.rs @@ -49,7 +49,7 @@ impl From<&Log> for RichText { Log::Processed(text) => RichText::from(text).color(egui::Color32::LIGHT_GREEN), }; - text.font(FontId::monospace(theme().node_log_font_size)) + text.font(FontId::monospace(theme_style().node_log_font_size)) } } diff --git a/core/src/runtime/services/kaspa/mod.rs b/core/src/runtime/services/kaspa/mod.rs index 3e61703..169e590 100644 --- a/core/src/runtime/services/kaspa/mod.rs +++ b/core/src/runtime/services/kaspa/mod.rs @@ -112,7 +112,7 @@ impl KaspaService { } } } else { - log_warning!("Node settings are not initialized"); + // log_warning!("Node settings are not initialized"); } Self { diff --git a/core/src/runtime/system.rs b/core/src/runtime/system.rs index 7c8dcd4..acee622 100644 --- a/core/src/runtime/system.rs +++ b/core/src/runtime/system.rs @@ -63,7 +63,7 @@ cfg_if! { ui.label(format!("{} CPU cores {freq}", cpu_physical_core_count)); } ui.label(format!("{} RAM", as_data_size(self.total_memory as f64, false))); - ui.label(format!("File descriptors: {}", self.fd_limit.separated_string())); + ui.label(format!("File Descriptors: {}", self.fd_limit.separated_string())); }); } } diff --git a/core/src/settings.rs b/core/src/settings.rs index 3e5aca9..8c37215 100644 --- a/core/src/settings.rs +++ b/core/src/settings.rs @@ -327,17 +327,25 @@ impl Default for MetricsSettings { } } -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] -pub struct UxSettings { +pub struct UserInterfaceSettings { + pub theme_color: String, + pub theme_style: String, + pub scale: f32, pub metrics: MetricsSettings, } -// impl Default for UxSettings { -// fn default() -> Self { -// Self {} -// } -// } +impl Default for UserInterfaceSettings { + fn default() -> Self { + Self { + theme_color: "Dark".to_string(), + theme_style: "Rounded".to_string(), + scale: 1.0, + metrics: MetricsSettings::default(), + } + } +} // pub type PluginSettings = HashMap; @@ -382,9 +390,8 @@ pub struct Settings { // pub developer_mode: bool, pub developer: DeveloperSettings, pub node: NodeSettings, - pub ux: UxSettings, + pub user_interface: UserInterfaceSettings, pub language_code: String, - pub theme: String, pub enable_plugins: bool, #[serde(skip_serializing_if = "Option::is_none")] pub plugins: Option, @@ -403,9 +410,8 @@ impl Default for Settings { // developer_mode: false, developer: DeveloperSettings::default(), node: NodeSettings::default(), - ux: UxSettings::default(), + user_interface: UserInterfaceSettings::default(), language_code: "en".to_string(), - theme: "Dark".to_string(), enable_plugins: true, plugins: Some(PluginSettingsMap::default()), } diff --git a/core/src/status.rs b/core/src/status.rs index 4d4ebdc..66bec94 100644 --- a/core/src/status.rs +++ b/core/src/status.rs @@ -100,7 +100,7 @@ impl<'core> Status<'core> { } fn render_peers(&self, ui: &mut egui::Ui, peers: Option) { - let status_icon_size = theme().status_icon_size; + let status_icon_size = theme_style().status_icon_size; let peers = peers.unwrap_or(0); if peers != 0 { @@ -109,9 +109,9 @@ impl<'core> Status<'core> { ui.label( RichText::new(egui_phosphor::light::CLOUD_SLASH) .size(status_icon_size) - .color(Color32::LIGHT_RED), + .color(theme_color().error_color), ); - ui.label(RichText::new("No peers").color(Color32::LIGHT_RED)); + ui.label(RichText::new("No peers").color(theme_color().error_color)); } } @@ -129,7 +129,7 @@ impl<'core> Status<'core> { fn render_connected_state(&mut self, ui: &mut egui::Ui, state: ConnectionStatus) { //connected : bool, icon: &str, color : Color32) { let status_area_width = ui.available_width() - 24.; - let status_icon_size = theme().status_icon_size; + let status_icon_size = theme_style().status_icon_size; let module = self.module().clone(); match state { @@ -141,7 +141,7 @@ impl<'core> Status<'core> { ui.label( RichText::new(egui_phosphor::light::PLUGS) .size(status_icon_size) - .color(Color32::LIGHT_RED), + .color(theme_color().error_color), ); ui.separator(); ui.label("Not Connected"); @@ -150,7 +150,7 @@ impl<'core> Status<'core> { ui.label( RichText::new(egui_phosphor::light::TREE_STRUCTURE) .size(status_icon_size) - .color(Color32::LIGHT_RED), + .color(theme_color().error_color), ); ui.separator(); // ui.label("Connecting..."); @@ -172,7 +172,7 @@ impl<'core> Status<'core> { "Error connecting to {}: {err}", settings.node.wrpc_url )) - .color(theme().warning_color), + .color(theme_color().warning_color), ); } } @@ -212,7 +212,7 @@ impl<'core> Status<'core> { ui.label( RichText::new(egui_phosphor::light::CPU) .size(status_icon_size) - .color(Color32::LIGHT_GREEN), + .color(theme_color().connected_color), ); ui.separator(); ui.label("CONNECTED").on_hover_ui(|ui| { diff --git a/core/src/sync.rs b/core/src/sync.rs index b4abcae..c8615cb 100644 --- a/core/src/sync.rs +++ b/core/src/sync.rs @@ -83,7 +83,7 @@ impl SyncStatus { } pub fn progress_bar(&self) -> Option { - let progress_color = theme().progress_color; + let progress_color = theme_color().progress_color; if let Some(progress_bar_percentage) = self.progress_bar_percentage { if let Some(progress_bar_text) = &self.progress_bar_text { Some( diff --git a/core/src/utils/color.rs b/core/src/utils/color.rs index d834c14..e65ae8b 100644 --- a/core/src/utils/color.rs +++ b/core/src/utils/color.rs @@ -2,9 +2,14 @@ use egui::Color32; pub trait Color32Extension { fn from_f32(value: f32) -> Self; + fn from_rgba(r: u8, g: u8, b: u8, a: u8) -> Self; + fn to_hex(&self) -> String; } impl Color32Extension for Color32 { + fn from_rgba(r: u8, g: u8, b: u8, a: u8) -> Self { + Color32::from_rgba_premultiplied(r, g, b, a) + } fn from_f32(value: f32) -> Self { Color32::from_rgba_premultiplied( (value * 255.0) as u8, @@ -13,4 +18,13 @@ impl Color32Extension for Color32 { (value * 255.0) as u8, ) } + fn to_hex(&self) -> String { + format!( + "#{:02X}{:02X}{:02X}{:02X}", + self.r(), + self.g(), + self.b(), + self.a() + ) + } } diff --git a/core/src/utils/image.rs b/core/src/utils/image.rs new file mode 100644 index 0000000..2420025 --- /dev/null +++ b/core/src/utils/image.rs @@ -0,0 +1,14 @@ +pub use egui_extras::image::{load_svg_bytes, load_svg_bytes_with_size, FitTo}; + +pub fn color_image_to_icon_data(image: epaint::ColorImage) -> egui::IconData { + egui::IconData { + width: image.size[0] as u32, + height: image.size[1] as u32, + rgba: image.as_raw().to_vec(), + } +} + +pub fn svg_to_icon_data(svg_bytes: &[u8], fit_to: FitTo) -> egui::IconData { + let image = load_svg_bytes_with_size(svg_bytes, fit_to).unwrap(); + color_image_to_icon_data(image) +} diff --git a/core/src/utils/mod.rs b/core/src/utils/mod.rs index c833228..7ff5650 100644 --- a/core/src/utils/mod.rs +++ b/core/src/utils/mod.rs @@ -14,6 +14,10 @@ mod arglist; pub use arglist::*; mod color; pub use color::*; +mod image; +pub use image::*; +mod version; +pub use version::*; #[macro_export] macro_rules! spawn { @@ -114,8 +118,7 @@ where } pub fn build(self, ui: &mut Ui, context: &mut Context) { - let theme = theme(); - let button_size = theme.medium_button_size(); + let button_size = theme_style().medium_button_size(); let available_width = ui.available_width(); let buttons_len = self.list.len() as f32; let spacing = ui.spacing().item_spacing.x; diff --git a/core/src/utils/qr.rs b/core/src/utils/qr.rs index d928a21..7cca936 100644 --- a/core/src/utils/qr.rs +++ b/core/src/utils/qr.rs @@ -9,8 +9,8 @@ pub fn render_qrcode(text: &str, width: usize, height: usize) -> String { code.render::>() .min_dimensions(width as u32, height as u32) - .dark_color(svg::Color("#ffffff")) - .light_color(svg::Color("#00000000")) + .light_color(svg::Color(theme_color().qr_background.to_hex().as_str())) + .dark_color(svg::Color(theme_color().qr_foreground.to_hex().as_str())) .build() .to_string() } diff --git a/core/src/utils/version.rs b/core/src/utils/version.rs new file mode 100644 index 0000000..6dec42e --- /dev/null +++ b/core/src/utils/version.rs @@ -0,0 +1,127 @@ +use crate::imports::*; +use workflow_http::Request; + +#[derive(Debug, Clone)] +pub struct Asset { + pub name: String, + pub browser_download_url: String, +} + +#[derive(Debug, Clone)] +pub struct Release { + pub version: String, + pub timestamp: Option, + pub html_url: Option, + pub assets: Vec, +} + +pub async fn check_version() -> Result<()> { + let current_version = crate::app::VERSION; + + let url = "https://api.github.com/repos/kaspanet/kaspad/releases/latest"; + let response = Request::new(url) + .with_user_agent(format!("kaspa-ng {current_version} software update check")) + .get_json::() + .await; + match response { + Ok(data) => { + let latest_version = data["tag_name"] + .as_str() + .ok_or(Error::custom("No tag_name found"))?; + if latest_version != current_version { + let timestamp = data["published_at"].as_str(); + let html_url = data["html_url"].as_str(); + let assets = data["assets"] + .as_array() + .ok_or(Error::custom("No assets found"))?; + let mut assets = assets + .iter() + .filter_map(|asset| { + if let (Some(name), Some(browser_download_url)) = ( + asset["name"].as_str(), + asset["browser_download_url"].as_str(), + ) { + Some(Asset { + name: name.to_string(), + browser_download_url: browser_download_url.to_string(), + }) + } else { + None + } + }) + .collect::>(); + + assets.sort_by_key(|asset| !platform_match(&asset.name)); + + let release = Release { + version: latest_version.to_string(), + timestamp: timestamp.map(String::from), + html_url: html_url.map(String::from), + assets, + }; + + runtime() + .application_events() + .send(Events::VersionUpdate(release)) + .await + .unwrap(); + } + } + Err(err) => { + println!("Unable to check for software update with GitHub: {:?}", err); + } + } + Ok(()) +} + +pub fn platform_match(name: impl Into) -> bool { + let name: String = name.into(); + + Path::new(name.as_str()) + .file_stem() + .and_then(|file_stem| file_stem.to_str()) + .and_then(|file_stem| file_stem.split('-').collect::>().last().cloned()) + .map(|suffix| { + cfg_if! { + if #[cfg(target_os = "macos")] { + suffix.contains("osx") || suffix.contains("macos") + } else if #[cfg(target_os = "windows")] { + suffix.contains("win") + } else if #[cfg(target_os = "linux")] { + suffix.contains("linux") + } else { + suffix.contains("wasm") + } + } + }) + .unwrap_or_default() +} + +pub fn release_link_name(name: impl Into) -> impl Into { + let name: String = name.into(); + + let matches = Path::new(name.as_str()) + .file_stem() + .and_then(|file_stem| file_stem.to_str()) + .and_then(|file_stem| file_stem.split('-').collect::>().last().cloned()) + .map(|_suffix| { + cfg_if! { + if #[cfg(target_os = "macos")] { + _suffix.contains("osx") || _suffix.contains("macos") + } else if #[cfg(target_os = "windows")] { + _suffix.contains("win") + } else if #[cfg(target_os = "linux")] { + _suffix.contains("linux") + } else { + false + } + } + }) + .unwrap_or_default(); + + if matches { + RichText::new(format!("• {name}")).strong() + } else { + RichText::new(format!("• {name}")) + } +} diff --git a/resources/i18n/i18n.json b/resources/i18n/i18n.json index 57e0c8c..571e13c 100644 --- a/resources/i18n/i18n.json +++ b/resources/i18n/i18n.json @@ -1,8 +1,6 @@ { "enabled": [ - "en", - "ja", - "zh_HANT" + "en" ], "aliases": { "en-US": "en", @@ -13,16 +11,16 @@ "languages": { "pt": "Português", "hi": "Hindi", - "fi": "Finnish", - "fa": "Farsi", + "nl": "Dutch", "vi": "Vietnamese", + "fa": "Farsi", "pa": "Panjabi", + "fi": "Finnish", "fil": "Filipino", "lt": "Lithuanian", - "es": "Español", - "nl": "Dutch", "sv": "Swedish", "uk": "Ukrainian", + "es": "Español", "af": "Afrikaans", "et": "Esti", "en": "English", @@ -62,229 +60,157 @@ "pt": {}, "hi": {}, "fi": {}, - "fa": {}, "vi": {}, + "fa": {}, "pa": {}, + "nl": {}, "fil": {}, - "lt": {}, "es": {}, - "nl": {}, - "uk": {}, + "lt": {}, "sv": {}, + "uk": {}, "af": {}, "et": {}, "en": { - "Very weak": "Very weak", - "Json Tx": "Json Tx", - "Allows you to take screenshots from within the application": "Allows you to take screenshots from within the application", - "Network Rx/s": "Network Rx/s", "Mainnet (Main Kaspa network)": "Mainnet (Main Kaspa network)", - "Virt. Parents": "Virt. Parents", "Network Tx/s": "Network Tx/s", + "Network Rx/s": "Network Rx/s", "Market": "Market", "Submitted Blocks": "Submitted Blocks", "Credits": "Credits", "Connection": "Connection", - "Include Priority Fees": "Include Priority Fees", - "Disable Password Restrictions": "Disable Password Restrictions", "The node runs as a part of the Kaspa-NG application process. This reduces communication overhead (experimental).": "The node runs as a part of the Kaspa-NG application process. This reduces communication overhead (experimental).", - "Please note that the integrated mode is experimental and does not currently show the sync progress information.": "Please note that the integrated mode is experimental and does not currently show the sync progress information.", - "Active Peers": "Active Peers", - "Removes security restrictions, allows for single-letter passwords": "Removes security restrictions, allows for single-letter passwords", + "Include Priority Fees": "Include Priority Fees", + "Kaspa NG on GitHub": "Kaspa NG on GitHub", "wRPC Borsh Rx/s": "wRPC Borsh Rx/s", "Testnet-11 (10 BPS)": "Testnet-11 (10 BPS)", "Kaspa Discord": "Kaspa Discord", "Developer Mode": "Developer Mode", "Borsh Handshake Failures": "Borsh Handshake Failures", "Kaspa NextGen on GitHub": "Kaspa NextGen on GitHub", - "None": "None", - "Type": "Type", + "Show Grid": "Show Grid", "Memory": "Memory", "Blocks": "Blocks", "DAA Range": "DAA Range", - "IBD is": "IBD is", + "Update Available to version": "Update Available to version", "DB Headers": "DB Headers", "Virtual Parent Hashes": "Virtual Parent Hashes", - "Node Status": "Node Status", - "Kaspa NG": "Kaspa NG", "Spread": "Spread", "Track in the background": "Track in the background", - "Borsh Rx": "Borsh Rx", - "Rusty Kaspa Daemon Path:": "Rusty Kaspa Daemon Path:", + "Node Status": "Node Status", + "Kaspa NG": "Kaspa NG", "Storage Read/s": "Storage Read/s", - "Connections": "Connections", - "Grpc User Tx/s": "Grpc User Tx/s", - "Copied to clipboard": "Copied to clipboard", - "Enable Plugins": "Enable Plugins", "CPU": "CPU", "Testnet-10 (1 BPS)": "Testnet-10 (1 BPS)", - "Connect to Remote RPC": "Connect to Remote RPC", "Storage Write/s": "Storage Write/s", - "Kaspa": "Kaspa", - "The node runs as a part of the Kaspa-NG process (experimental)": "The node runs as a part of the Kaspa-NG process (experimental)", + "GitHub Release": "GitHub Release", "Mass Processed": "Mass Processed", - "Derivation Index": "Derivation Index", - "A binary at another location is spawned a child process (experimental, for development purposes only)": "A binary at another location is spawned a child process (experimental, for development purposes only)", "Median T": "Median T", "DAA Offset": "DAA Offset", - "Grpc User Rx/s": "Grpc User Rx/s", - "Connection duration": "Connection duration", + "Disables node connectivity (Offline Mode).": "Disables node connectivity (Offline Mode).", "A binary at another location is spawned a child process (experimental, for development purposes only).": "A binary at another location is spawned a child process (experimental, for development purposes only).", "System": "System", "Virtual Memory": "Virtual Memory", "Resident Memory": "Resident Memory", - "Inbound": "Inbound", - "Json Rx/s": "Json Rx/s", "Donations": "Donations", - "Blocks Submitted": "Blocks Submitted", - "Total Tx/s": "Total Tx/s", - "Account Name": "Account Name", + "gRPC User Rx": "gRPC User Rx", + "gRPC p2p Tx": "gRPC p2p Tx", + "Inbound": "Inbound", "Music": "Music", - "Bezier Curves": "Bezier Curves", - "IBD": "IBD", "Enable gRPC": "Enable gRPC", + "Bezier Curves": "Bezier Curves", "gRPC p2p Tx/s": "gRPC p2p Tx/s", "Processed Headers": "Processed Headers", - "wRPC URL:": "wRPC URL:", - "gRPC User Rx": "gRPC User Rx", - "gRPC p2p Tx": "gRPC p2p Tx", - "All": "All", + "Enable Experimental Features": "Enable Experimental Features", "Mempool Size": "Mempool Size", "Json Connection Attempts": "Json Connection Attempts", - "Enable Experimental Features": "Enable Experimental Features", - "Disables node connectivity (Offline Mode).": "Disables node connectivity (Offline Mode).", - "p2p Rx/s": "p2p Rx/s", - "Borsh Tx": "Borsh Tx", "Redistributables": "Redistributables", - "Copy logs to clipboard": "Copy logs to clipboard", + "Connects to a Remote Rusty Kaspa Node via wRPC.": "Connects to a Remote Rusty Kaspa Node via wRPC.", "Difficulty": "Difficulty", "Uptime:": "Uptime:", "gRPC p2p Rx/s": "gRPC p2p Rx/s", "Not connected": "Not connected", - "Password is too weak": "Password is too weak", - "BlockDAG": "BlockDAG", + "Copy logs to clipboard": "Copy logs to clipboard", "Handles": "Handles", - "Please create a stronger password": "Please create a stronger password", - "Connects to a Remote Rusty Kaspa Node via wRPC.": "Connects to a Remote Rusty Kaspa Node via wRPC.", - "Very dangerous (may be cracked within few seconds)": "Very dangerous (may be cracked within few seconds)", - "Good": "Good", "DAA": "DAA", - "Signature Type": "Signature Type", "Time Offset:": "Time Offset:", - "Address & Protocol": "Address & Protocol", "Net Rx/s": "Net Rx/s", - "Network": "Network", - "Grpc User Tx": "Grpc User Tx", - "Group": "Group", - "Ping": "Ping", "Dependencies": "Dependencies", - "Disable Password Score Restrictions": "Disable Password Score Restrictions", "Active p2p Peers": "Active p2p Peers", - "Disable": "Disable", + "Disable Password Score Restrictions": "Disable Password Score Restrictions", "Chain Blocks": "Chain Blocks", - "p2p Rx": "p2p Rx", - "p2p Tx": "p2p Tx", + "GitHub Release Page for": "GitHub Release Page for", "Parent levels": "Parent levels", - "Continue": "Continue", "WASM SDK for JavaScript and TypeScript": "WASM SDK for JavaScript and TypeScript", "Kaspa p2p Node": "Kaspa p2p Node", - "p2p Tx/s": "p2p Tx/s", "gRPC User Tx": "gRPC User Tx", "NPM Modules for NodeJS": "NPM Modules for NodeJS", "License Information": "License Information", - "wRPC Encoding:": "wRPC Encoding:", - "Borsh Tx/s": "Borsh Tx/s", "Storage Write": "Storage Write", - "Address": "Address", "Processed Bodies": "Processed Bodies", - "Borsh Rx/s": "Borsh Rx/s", - "Disables node connectivity (Offline Mode)": "Disables node connectivity (Offline Mode)", "Stor Write": "Stor Write", - "Total Rx/s": "Total Rx/s", "Processed Mass Counts": "Processed Mass Counts", "User Agent": "User Agent", - "The node is spawned as a child daemon process (recommended)": "The node is spawned as a child daemon process (recommended)", - "Time Offset": "Time Offset", - "The node is spawned as a child daemon process (recommended).": "The node is spawned as a child daemon process (recommended).", "bye!": "bye!", "Json Handshake Failures": "Json Handshake Failures", + "Net Tx/s": "Net Tx/s", "Storage Read": "Storage Read", "gRPC User Rx/s": "gRPC User Rx/s", - "Change Address": "Change Address", - "No peers": "No peers", + "wRPC JSON Rx": "wRPC JSON Rx", + "Network Peers": "Network Peers", "wRPC JSON Rx/s": "wRPC JSON Rx/s", + "The node is spawned as a child daemon process (recommended).": "The node is spawned as a child daemon process (recommended).", "Screen Capture": "Screen Capture", - "Network Peers": "Network Peers", - "Peers Connected": "Peers Connected", - "Json Rx": "Json Rx", - "Receive Address": "Receive Address", - "DAA Margin": "DAA Margin", + "Allows you to take screenshots from within the application": "Allows you to take screenshots from within the application", "Outbound": "Outbound", "Build": "Build", - "wRPC JSON Rx": "wRPC JSON Rx", + "Removes security restrictions, allows for single-letter passwords": "Removes security restrictions, allows for single-letter passwords", "gRPC p2p Rx": "gRPC p2p Rx", - "Enable optional BIP39 passphrase": "Enable optional BIP39 passphrase", - "Skip": "Skip", "Total Tx": "Total Tx", "Peers": "Peers", "Processed Transactions": "Processed Transactions", "Borsh Connection Attempts": "Borsh Connection Attempts", "TPS": "TPS", - "Net Tx/s": "Net Tx/s", "wRPC Borsh Tx": "wRPC Borsh Tx", - "Protocol:": "Protocol:", + "Mempool": "Mempool", "wRPC Borsh Rx": "wRPC Borsh Rx", - "Grpc User Rx": "Grpc User Rx", + "Protocol:": "Protocol:", "Transactions": "Transactions", - "Mempool": "Mempool", - "Mass Counts": "Mass Counts", "gRPC User Tx/s": "gRPC User Tx/s", "wRPC JSON Tx/s": "wRPC JSON Tx/s", "Virt Parents": "Virt Parents", "DB Blocks": "DB Blocks", - "Connect to Remote": "Connect to Remote", "File Handles": "File Handles", + "Show DAA": "Show DAA", "Json Active Connections": "Json Active Connections", - "Weak": "Weak", - "Dangerous": "Dangerous", - "Passwords do not match": "Passwords do not match", - "Store Read": "Store Read", + "Tools": "Tools", "wRPC Borsh Tx/s": "wRPC Borsh Tx/s", - "Your mnemonic phrase allows your to re-create your private key. The person who has access to this mnemonic will have full control of the Kaspa stored in it. Keep your mnemonic safe. Write it down and store it in a safe, preferably in a fire-resistant location. Do not store your mnemonic on this computer or a mobile device. This wallet will never ask you for this mnemonic phrase unless you manually initiate a private key recovery.": "Your mnemonic phrase allows your to re-create your private key. The person who has access to this mnemonic will have full control of the Kaspa stored in it. Keep your mnemonic safe. Write it down and store it in a safe, preferably in a fire-resistant location. Do not store your mnemonic on this computer or a mobile device. This wallet will never ask you for this mnemonic phrase unless you manually initiate a private key recovery.", "Database Headers": "Database Headers", "Tip Hashes": "Tip Hashes", - "Strong": "Strong", "Resources": "Resources", - "Json Tx/s": "Json Tx/s", "Center VSPC": "Center VSPC", - "Enable UPnP": "Enable UPnP", - "Integrated Daemon": "Integrated Daemon", - "Ping:": "Ping:", + "An update is available to version": "An update is available to version", "Rusty Kaspa on GitHub": "Rusty Kaspa on GitHub", - "IBD:": "IBD:", + "Ping:": "Ping:", + "Enable UPnP": "Enable UPnP", "Bodies": "Bodies", "Network Difficulty": "Network Difficulty", - "Key Perf.": "Key Perf.", + "IBD:": "IBD:", "Processed Dependencies": "Processed Dependencies", - "Body Counts": "Body Counts", - "Bandwidth": "Bandwidth", - "Uptime": "Uptime", - "Rust Wallet SDK": "Rust Wallet SDK", "Total Rx": "Total Rx", - "Tools": "Tools", - "Protocol": "Protocol", + "Rust Wallet SDK": "Rust Wallet SDK", + "Enables features currently in development": "Enables features currently in development", + "Show VSPC": "Show VSPC", + "Update Available": "Update Available", "Enter the password to unlock your wallet": "Enter the password to unlock your wallet", "Borsh Active Connections": "Borsh Active Connections", - "Connects to a Remote Rusty Kaspa Node via wRPC": "Connects to a Remote Rusty Kaspa Node via wRPC", "wRPC JSON Tx": "wRPC JSON Tx", "Virtual DAA Score": "Virtual DAA Score", "Please wait for the node to sync...": "Please wait for the node to sync...", - "Storage": "Storage", - "Enables features currently in development": "Enables features currently in development", "Double click on the graph to re-center...": "Double click on the graph to re-center...", - "Past Median Time": "Past Median Time", "Stor Read": "Stor Read", "Database Blocks": "Database Blocks", + "Past Median Time": "Past Median Time", "Headers": "Headers", "Address:": "Address:" },