From 7fac7bd9dd2919f2efd209e7e1eadf4f2fa1b564 Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Sat, 2 Dec 2023 12:37:34 +0200 Subject: [PATCH] wip --- Cargo.toml | 16 +- core/Cargo.toml | 4 +- core/src/app.rs | 4 +- core/src/core.rs | 305 ++--- core/src/device.rs | 15 - core/src/egui/collapsable.rs | 40 + core/src/egui/extensions.rs | 32 +- core/src/egui/mod.rs | 4 + core/src/{ => egui}/panel.rs | 4 - core/src/egui/popup.rs | 96 +- core/src/egui/theme.rs | 24 +- core/src/events.rs | 5 +- core/src/imports.rs | 6 +- core/src/lib.rs | 6 +- core/src/menu.rs | 166 +++ core/src/mobile.rs | 140 +++ core/src/modules/account_manager.rs | 1024 ----------------- core/src/modules/account_manager/details.rs | 56 + core/src/modules/account_manager/menus.rs | 172 +++ core/src/modules/account_manager/mod.rs | 402 +++++++ core/src/modules/account_manager/overview.rs | 582 ++++++++++ .../modules/account_manager/transactions.rs | 26 + core/src/modules/account_manager/utxo.rs | 20 + core/src/modules/settings/mobile.rs | 0 .../modules/{settings.rs => settings/mod.rs} | 119 +- core/src/modules/settings/native.rs | 0 core/src/modules/wallet_create.rs | 8 +- core/src/modules/welcome.rs | 4 +- core/src/primitives/account.rs | 14 +- core/src/primitives/transaction.rs | 143 ++- core/src/prompt.rs | 165 --- core/src/{ => runtime}/channel.rs | 0 core/src/runtime/device.rs | 50 + core/src/runtime/mod.rs | 21 +- .../src/runtime/plugins/market_monitor/mod.rs | 12 +- core/src/runtime/services/plugin_manager.rs | 69 +- core/src/settings.rs | 45 +- core/src/status.rs | 14 +- 38 files changed, 2156 insertions(+), 1657 deletions(-) delete mode 100644 core/src/device.rs create mode 100644 core/src/egui/collapsable.rs rename core/src/{ => egui}/panel.rs (99%) create mode 100644 core/src/menu.rs create mode 100644 core/src/mobile.rs delete mode 100644 core/src/modules/account_manager.rs create mode 100644 core/src/modules/account_manager/details.rs create mode 100644 core/src/modules/account_manager/menus.rs create mode 100644 core/src/modules/account_manager/mod.rs create mode 100644 core/src/modules/account_manager/overview.rs create mode 100644 core/src/modules/account_manager/transactions.rs create mode 100644 core/src/modules/account_manager/utxo.rs create mode 100644 core/src/modules/settings/mobile.rs rename core/src/modules/{settings.rs => settings/mod.rs} (68%) create mode 100644 core/src/modules/settings/native.rs delete mode 100644 core/src/prompt.rs rename core/src/{ => runtime}/channel.rs (100%) create mode 100644 core/src/runtime/device.rs diff --git a/Cargo.toml b/Cargo.toml index 0ce53be..8aae64c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,24 +131,26 @@ derivative = "2.2.0" downcast = "0.11.0" downcast-rs = "1.2.0" env_logger = "0.10" -futures = { version = "0.3" } -futures-util = { version = "0.3", default-features = false, features = ["alloc"] } +futures = { version = "0.3.29" } +futures-util = { version = "0.3.29", default-features = false, features = ["alloc"] } +image = { version = "0.24.7", default-features = false, features = ["png"] } js-sys = "0.3.64" -log = "0.4" +log = "0.4.20" nix = "0.27.1" num_cpus = "1.15.0" pad = "0.1.6" passwords = "3.1.16" qrcode = "0.12.0" -rand = "0.8" +rand = "0.8.5" +rfd = "0.12.1" ritehash = "0.2.0" rlimit = "0.10.1" separator = "0.4.1" serde = { version = "1", features = ["derive"] } -serde_json = "1.0.107" -slug = "0.1.4" +serde_json = "1.0.108" +slug = "0.1.5" sysinfo = "0.29.10" -thiserror = "1.0.47" +thiserror = "1.0.50" tokio = { version = "1", features = ["sync", "rt-multi-thread", "process"] } wasm-bindgen = "0.2.87" wasm-bindgen-futures = "0.4" diff --git a/core/Cargo.toml b/core/Cargo.toml index 14e04a4..d41c18b 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -44,15 +44,17 @@ downcast-rs.workspace = true downcast.workspace = true futures-util.workspace = true futures.workspace = true +image.workspace = true js-sys.workspace = true log.workspace = true pad.workspace = true passwords.workspace = true qrcode.workspace = true +rfd.workspace = true ritehash.workspace = true separator.workspace = true -serde.workspace = true serde_json.workspace = true +serde.workspace = true slug.workspace = true thiserror.workspace = true wasm-bindgen.workspace = true diff --git a/core/src/app.rs b/core/src/app.rs index dcca68e..c803915 100644 --- a/core/src/app.rs +++ b/core/src/app.rs @@ -92,8 +92,6 @@ cfg_if! { ; let matches = cmd.get_matches(); - // println!("matches: {:#?}", matches); - if matches.subcommand_matches("cli").is_some() { Args::Cli } else if let Some(matches) = matches.subcommand_matches("i18n") { @@ -107,8 +105,8 @@ cfg_if! { std::process::exit(1); } } else { - let reset_settings = matches.get_one::("reset-settings").cloned().unwrap_or(false); let disable = matches.get_one::("disable").cloned().unwrap_or(false); + let reset_settings = matches.get_one::("reset-settings").cloned().unwrap_or(false); Args::Kng { reset_settings, disable } } diff --git a/core/src/core.rs b/core/src/core.rs index c46a5f9..08110d2 100644 --- a/core/src/core.rs +++ b/core/src/core.rs @@ -1,4 +1,5 @@ use crate::imports::*; +use crate::mobile::MobileMenu; use egui::load::Bytes; use egui_notify::Toasts; use kaspa_metrics::MetricsSnapshot; @@ -16,7 +17,6 @@ pub struct Core { is_shutdown_pending: bool, settings_storage_requested: bool, last_settings_storage_request: Instant, - device: Device, runtime: Runtime, wallet: Arc, @@ -27,20 +27,21 @@ pub struct Core { modules: HashMap, pub settings: Settings, pub toasts: Toasts, + screenshot: Option>, pub mobile_style: egui::Style, pub default_style: egui::Style, - pub metrics: Option>, state: State, hint: Option, discard_hint: bool, exception: Option, + pub metrics: Option>, pub wallet_descriptor: Option, pub wallet_list: Vec, pub prv_key_data_map: HashMap>, pub account_collection: Option, - pub selected_account: Option, + // pub selected_account: Option, } impl Core { @@ -114,17 +115,10 @@ impl Core { } }; - let mut module = if settings.developer_mode { - modules - .get(&TypeId::of::()) - .unwrap() - .clone() - } else { - modules - .get(&TypeId::of::()) - .unwrap() - .clone() - }; + let mut module = modules + .get(&TypeId::of::()) + .unwrap() + .clone(); if settings.version != env!("CARGO_PKG_VERSION") { settings.version = env!("CARGO_PKG_VERSION").to_string(); @@ -144,7 +138,6 @@ impl Core { is_shutdown_pending: false, settings_storage_requested: false, last_settings_storage_request: Instant::now(), - device: Device::default(), wallet, application_events_channel, @@ -154,6 +147,7 @@ impl Core { stack: VecDeque::new(), settings: settings.clone(), toasts: Toasts::default(), + screenshot: None, // status_bar_message: None, default_style, mobile_style, @@ -162,8 +156,7 @@ impl Core { wallet_list: Vec::new(), prv_key_data_map: HashMap::new(), account_collection: None, - selected_account: None, - + // selected_account: None, metrics: None, state: Default::default(), hint: None, @@ -184,10 +177,11 @@ impl Core { where T: 'static, { - let module = self - .modules - .get(&TypeId::of::()) - .expect("Unknown module"); + self.select_with_type_id(TypeId::of::()); + } + + pub fn select_with_type_id(&mut self, type_id: TypeId) { + let module = self.modules.get(&type_id).expect("Unknown module"); if self.module.type_id() != module.type_id() { let next = module.clone(); @@ -215,7 +209,7 @@ impl Core { } } - pub fn sender(&self) -> crate::channel::Sender { + pub fn sender(&self) -> crate::runtime::channel::Sender { self.application_events_channel.sender.clone() } @@ -254,10 +248,6 @@ impl Core { &self.module } - pub fn device(&self) -> &Device { - &self.device - } - pub fn get(&self) -> Ref<'_, T> where T: ModuleT + 'static, @@ -324,6 +314,12 @@ impl eframe::App for Core { self.handle_keyboard_events(*key, *pressed, modifiers, *repeat); } }); + + for event in &input.raw.events { + if let Event::Screenshot { image, .. } = event { + self.screenshot = Some(image.clone()); + } + } }); // ctx.set_visuals(self.default_style.clone()); @@ -361,42 +357,69 @@ impl eframe::App for Core { } } - if !self.module.modal() { + let device = runtime().device(); + + if !self.module.modal() && !device.is_singular_layout() { egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { - self.render_menu(ui, frame); + Menu::new(self).render(ui); + // self.render_menu(ui, frame); }); } - if self.device().is_portrait() || self.device().is_mobile() { - if !self.device().is_mobile() { + if device.is_singular_layout() { + if !device.is_mobile() { egui::TopBottomPanel::bottom("bottom_panel").show(ctx, |ui| { Status::new(self).render(ui); egui::warn_if_debug_build(ui); }); } - let width = (ctx.screen_rect().width() - 390.) * 0.5; + let device_width = 390.0; + let margin_width = (ctx.screen_rect().width() - device_width) * 0.5; SidePanel::right("portrait_right") - .exact_width(width) + .exact_width(margin_width) .resizable(false) .show_separator_line(true) .frame(Frame::default().fill(Color32::BLACK)) .show(ctx, |_ui| {}); SidePanel::left("portrait_left") - .exact_width(width) + .exact_width(margin_width) .resizable(false) .show_separator_line(true) .frame(Frame::default().fill(Color32::BLACK)) .show(ctx, |_ui| {}); - CentralPanel::default().show(ctx, |ui| { - egui::TopBottomPanel::bottom("mobile_bottom_panel").show_inside(ui, |ui| { - Status::new(self).render(ui); - }); + CentralPanel::default() + .frame( + Frame::default() + .fill(ctx.style().visuals.panel_fill), + ) + .show(ctx, |ui| { + ui.set_max_width(device_width); - self.module.clone().render(self, ctx, frame, ui); - }); + egui::TopBottomPanel::bottom("mobile_bottom_panel").show_inside(ui, |ui| { + Status::new(self).render(ui); + }); + + if device.is_mobile() { + egui::TopBottomPanel::bottom("mobile_menu_panel") + .show_inside(ui, |ui| { + MobileMenu::new(self).render(ui); + }); + } + + egui::CentralPanel::default() + .frame( + Frame::default() + .inner_margin(0.) + .outer_margin(4.) + .fill(ctx.style().visuals.panel_fill), + ) + .show_inside(ui, |ui| { + self.module.clone().render(self, ctx, frame, ui); + }); + }); } else { egui::TopBottomPanel::bottom("bottom_panel").show(ctx, |ui| { Status::new(self).render(ui); @@ -421,6 +444,41 @@ impl eframe::App for Core { if let Some(module) = self.deactivation.take() { module.deactivate(self); } + + #[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(); + } + } } } @@ -444,152 +502,6 @@ impl Core { .paint_at(ui, logo_rect); } - fn render_menu(&mut self, ui: &mut Ui, _frame: &mut eframe::Frame) { - egui::menu::bar(ui, |ui| { - ui.columns(2, |cols| { - cols[0].horizontal(|ui| { - ui.menu_button("File", |ui| { - #[cfg(not(target_arch = "wasm32"))] - if ui.button("Quit").clicked() { - ui.ctx().send_viewport_cmd(ViewportCommand::Close) - } - ui.separator(); - ui.label(" ~ Debug Modules ~"); - ui.label(" "); - - // let mut modules = self.modules.values().cloned().collect::>(); - - let (tests, mut modules): (Vec<_>, Vec<_>) = self - .modules - .values() - .cloned() - .partition(|module| module.name().starts_with('~')); - - tests.into_iter().for_each(|module| { - if ui.button(module.name()).clicked() { - self.module = module; //.type_id(); - ui.close_menu(); - } - }); - - ui.label(" "); - - modules.sort_by(|a, b| a.name().partial_cmp(b.name()).unwrap()); - modules.into_iter().for_each(|module| { - // let SectionInner { name,type_id, .. } = section.inner; - if ui.button(module.name()).clicked() { - self.module = module; //.type_id(); - ui.close_menu(); - } - }); - }); - - ui.separator(); - if ui.button("Overview").clicked() { - self.select::(); - } - ui.separator(); - if ui.button("Wallet").clicked() { - if self.state().is_open() { - self.select::(); - } else { - self.select::(); - } - } - ui.separator(); - // if ui.button(icon_with_text(ui, egui_phosphor::light::GEAR, Color32::WHITE, "Settings")).clicked() { - // self.select::(); - // } - // ui.separator(); - // if ui.button(RichText::new(format!("{} Settings",egui_phosphor::light::GEAR))).clicked() { - if ui.button("Settings").clicked() { - self.select::(); - } - - #[cfg(not(target_arch = "wasm32"))] - { - ui.separator(); - if ui.button("Node").clicked() { - self.select::(); - } - } - - ui.separator(); - if ui.button("Metrics").clicked() { - self.select::(); - } - - ui.separator(); - if ui.button("Block DAG").clicked() { - self.select::(); - } - - #[cfg(not(target_arch = "wasm32"))] - { - ui.separator(); - if ui.button("Logs").clicked() { - self.select::(); - } - } - - // ui.separator(); - // if ui.button("About").clicked() { - // self.select::(); - // } - ui.separator(); - }); - - cols[1].with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let dictionary = i18n::dictionary(); - // use egui_phosphor::light::TRANSLATE; - #[allow(clippy::useless_format)] - ui.menu_button(format!("{} ⏷", dictionary.current_title()), |ui| { - // ui.menu_button(RichText::new(format!("{TRANSLATE} ⏷")).size(18.), |ui| { - dictionary - .enabled_languages() - .into_iter() - .for_each(|(code, lang)| { - if ui.button(lang).clicked() { - self.settings.language_code = code.to_string(); - dictionary - .activate_language_code(code) - .expect("Unable to activate language"); - ui.close_menu(); - } - }); - }); - - ui.separator(); - - // let theme = theme(); - - PopupPanel::new( - ui, - "display_settings", - egui_phosphor::light::MONITOR, - |ui| { - ui.label("hello world"); - }, - ) - .build(ui); - - // // let icon_size = theme.panel_icon_size(); - // let icon = CompositeIcon::new(egui_phosphor::light::MONITOR).icon_size(18.); - // // .padding(Some(icon_padding)); - // // if ui.add_enabled(true, icon).clicked() { - // if ui.add(icon).clicked() { - // // close(self.this); - // } - - // if ui.button("Theme").clicked() { - // self.select::(); - // } - ui.separator(); - }); - }); - }); - // ui.spacing() - } pub fn handle_events( &mut self, @@ -729,6 +641,8 @@ impl Core { CoreWallet::DAAScoreChange { current_daa_score } => { self.state.current_daa_score.replace(current_daa_score); } + // Ignore scan notifications + CoreWallet::Scan { record: _ } => {} // Ignore stasis notifications CoreWallet::Stasis { record: _ } => {} // This notification is for a UTXO change, which is @@ -786,20 +700,11 @@ impl Core { } }, - CoreWallet::Balance { - balance, - id, - mature_utxo_size, - pending_utxo_size, - } => { + CoreWallet::Balance { balance, id } => { if let Some(account_collection) = &self.account_collection { if let Some(account) = account_collection.get(&id.into()) { // println!("*** updating account balance: {}", id); - account.update_balance( - balance, - mature_utxo_size, - pending_utxo_size, - )?; + account.update_balance(balance)?; } else { log_error!("unable to find account {}", id); } @@ -929,10 +834,10 @@ impl Core { self.select::(); } Key::M => { - self.device.is_mobile = !self.device.is_mobile; + runtime().device().toggle_mobile(); } Key::P => { - self.device.is_portrait = !self.device.is_portrait; + runtime().device().toggle_portrait(); } _ => {} } @@ -944,10 +849,10 @@ impl Core { self.select::(); } Key::M => { - self.device.is_mobile = !self.device.is_mobile; + runtime().device().toggle_mobile(); } Key::P => { - self.device.is_portrait = !self.device.is_portrait; + runtime().device().toggle_portrait(); } _ => {} } diff --git a/core/src/device.rs b/core/src/device.rs deleted file mode 100644 index 7e2aaa8..0000000 --- a/core/src/device.rs +++ /dev/null @@ -1,15 +0,0 @@ -#[derive(Default)] -pub struct Device { - pub is_portrait: bool, - pub is_mobile: bool, -} - -impl Device { - pub fn is_portrait(&self) -> bool { - self.is_portrait - } - - pub fn is_mobile(&self) -> bool { - self.is_mobile - } -} diff --git a/core/src/egui/collapsable.rs b/core/src/egui/collapsable.rs new file mode 100644 index 0000000..143b146 --- /dev/null +++ b/core/src/egui/collapsable.rs @@ -0,0 +1,40 @@ +use crate::imports::*; +use egui::collapsing_header::CollapsingState; + +pub trait CollapsingExtension { + fn collapsable( + &mut self, + id: impl Into, + default_open: bool, + heading: impl FnOnce(&mut Ui, &mut bool) -> HeaderRet, + body: impl FnOnce(&mut Ui) -> BodyRet, + ); +} + +impl CollapsingExtension for Ui { + fn collapsable( + &mut self, + id: impl Into, + default_open: bool, + heading: impl FnOnce(&mut Ui, &mut bool) -> HeaderRet, + body: impl FnOnce(&mut Ui) -> BodyRet, + ) { + let id: String = id.into(); + let id = self.make_persistent_id(id); + let previous_state = CollapsingState::load(self.ctx(), id) + .map(|state| state.is_open()) + .unwrap_or_default(); + let mut state = previous_state; + let header = CollapsingState::load_with_default_open(self.ctx(), id, default_open); + header + .show_header(self, |ui| heading(ui, &mut state)) + .body(body); + + if state != previous_state { + if let Some(mut state) = CollapsingState::load(self.ctx(), id) { + state.toggle(self); + state.store(self.ctx()); + } + } + } +} diff --git a/core/src/egui/extensions.rs b/core/src/egui/extensions.rs index 406ffd4..1a389cd 100644 --- a/core/src/egui/extensions.rs +++ b/core/src/egui/extensions.rs @@ -1,3 +1,5 @@ +use egui::text::TextWrapping; + use crate::imports::*; pub enum Confirm { @@ -31,28 +33,16 @@ pub trait UiExtension { nack: impl Into, ) -> Option; fn confirm_medium_apply_cancel(&mut self, align: Align) -> Option; - // { - // self.confirm_medium( - // align, - // icon_with_text(self, egui_phosphor::light::CHECK, "Apply"), - // icon_with_text(self, egui_phosphor::light::X,"Cancel") - // ) - // } } impl UiExtension for Ui { - // fn medium_button(&mut self, text: impl Into) -> Response { - // self.add_sized(theme().medium_button_size(), Button::new(text)) - // } 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()), ) } - // fn large_button(&mut self, text: impl Into) -> Response { - // self.add_sized(theme().large_button_size(), Button::new(text)) - // } + fn large_button_enabled(&mut self, enabled: bool, text: impl Into) -> Response { self.add_enabled( enabled, @@ -95,8 +85,6 @@ impl UiExtension for Ui { align, format!("{} {}", egui_phosphor::light::CHECK, "Apply"), format!("{} {}", egui_phosphor::light::X, "Cancel"), - // icon_with_text(self, egui_phosphor::light::CHECK, theme.ack_color, "Apply"), - // icon_with_text(self, egui_phosphor::light::X,theme.nack_color, "Cancel") ) } } @@ -110,9 +98,19 @@ pub struct LayoutJobBuilder { } impl LayoutJobBuilder { - pub fn new(leading: f32, font_id: Option) -> Self { + pub fn new(width: f32, leading: f32, font_id: Option) -> Self { + let job = LayoutJob { + wrap: TextWrapping { + max_width: width, + max_rows: 1, + break_anywhere: true, + overflow_character: Some('…'), + }, + ..Default::default() + }; + Self { - job: LayoutJob::default(), + job, leading, font_id, ..Default::default() diff --git a/core/src/egui/mod.rs b/core/src/egui/mod.rs index ccf9b9a..04c42f3 100644 --- a/core/src/egui/mod.rs +++ b/core/src/egui/mod.rs @@ -1,3 +1,4 @@ +mod collapsable; mod composite_button; mod composite_icon; mod easy_mark; @@ -5,9 +6,11 @@ mod extensions; mod icon; mod mnemonic; mod network; +mod panel; mod popup; mod theme; +pub use collapsable::*; pub use composite_button::CompositeButton; pub use composite_icon::CompositeIcon; pub use easy_mark::easy_mark; @@ -15,5 +18,6 @@ pub use extensions::*; pub use icon::{icons, Icon, IconSize, Icons}; pub use mnemonic::*; pub use network::NetworkInterfaceEditor; +pub use panel::Panel; pub use popup::PopupPanel; pub use theme::*; diff --git a/core/src/panel.rs b/core/src/egui/panel.rs similarity index 99% rename from core/src/panel.rs rename to core/src/egui/panel.rs index 19d6053..9c21b90 100644 --- a/core/src/panel.rs +++ b/core/src/egui/panel.rs @@ -1,9 +1,5 @@ use crate::imports::*; use std::fmt::Display; -pub enum PanelEvents { - Back, - Close, -} type ActionFn<'panel, Context> = Box; type RenderFn<'panel, Context> = Box; diff --git a/core/src/egui/popup.rs b/core/src/egui/popup.rs index ab474bd..6044bb8 100644 --- a/core/src/egui/popup.rs +++ b/core/src/egui/popup.rs @@ -11,6 +11,7 @@ pub struct PopupPanel<'panel> { with_caption: bool, with_close_button: bool, with_pulldown_marker: bool, + close_on_interaction: bool, } impl<'panel> PopupPanel<'panel> { @@ -31,6 +32,7 @@ impl<'panel> PopupPanel<'panel> { with_caption: false, with_close_button: false, with_pulldown_marker: false, + close_on_interaction: false, } } @@ -59,6 +61,11 @@ impl<'panel> PopupPanel<'panel> { self } + pub fn with_close_on_interaction(mut self, close_on_interaction: bool) -> Self { + self.close_on_interaction = close_on_interaction; + self + } + pub fn build(&mut self, ui: &mut Ui) { let title = self.title.clone(); let content = self.content.take().unwrap(); @@ -79,49 +86,56 @@ impl<'panel> PopupPanel<'panel> { // ignores clicks inside of its area allowing the panel to // persist while the user interacts with it and closing // once triggered via `mem.close_popup()` or clicking outside of it. - popup_above_or_below_widget_local(ui, self.id, &response, AboveOrBelow::Below, |ui| { - if let Some(width) = self.min_width { - ui.set_min_width(width); - } - - if let Some(height) = self.max_height { - ui.set_max_height(height); - } + popup_above_or_below_widget_local( + ui, + self.id, + &response, + AboveOrBelow::Below, + self.close_on_interaction, + |ui| { + if let Some(width) = self.min_width { + ui.set_min_width(width); + } - if self.with_caption { - ui.horizontal(|ui| { - ui.label(title); + if let Some(height) = self.max_height { + ui.set_max_height(height); + } - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - use egui_phosphor::light::X; - if ui - .add(Label::new(RichText::new(X).size(16.)).sense(Sense::click())) - .clicked() - { - ui.memory_mut(|mem| mem.close_popup()); - } + if self.with_caption { + ui.horizontal(|ui| { + ui.label(title); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + use egui_phosphor::light::X; + if ui + .add(Label::new(RichText::new(X).size(16.)).sense(Sense::click())) + .clicked() + { + ui.memory_mut(|mem| mem.close_popup()); + } + }); }); - }); - ui.separator(); - ui.space(); - } + ui.separator(); + ui.space(); + } - content(ui); + content(ui); - if self.with_close_button { - ui.space(); - ui.separator(); + if self.with_close_button { + ui.space(); + ui.separator(); - ui.add_space(8.); - ui.vertical_centered(|ui| { - if ui.medium_button("Close").clicked() { - ui.memory_mut(|mem| mem.close_popup()); - } - }); - ui.add_space(8.); - } - }); + ui.add_space(8.); + ui.vertical_centered(|ui| { + if ui.medium_button("Close").clicked() { + ui.memory_mut(|mem| mem.close_popup()); + } + }); + ui.add_space(8.); + } + }, + ); } } @@ -130,6 +144,7 @@ pub fn popup_above_or_below_widget_local( popup_id: Id, widget_response: &Response, above_or_below: AboveOrBelow, + close_on_interaction: bool, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { if ui.memory(|mem| mem.is_popup_open(popup_id)) { @@ -158,9 +173,14 @@ pub fn popup_above_or_below_widget_local( }) .inner }); - // .inner; + + let mut close_popup = false; - if ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() { + if close_on_interaction { + if ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() { + close_popup = true; + } + } else if ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() { let response = inner.response; ui.ctx().input(|i| { let pointer = &i.pointer; diff --git a/core/src/egui/theme.rs b/core/src/egui/theme.rs index 3c476a2..22fa80f 100644 --- a/core/src/egui/theme.rs +++ b/core/src/egui/theme.rs @@ -5,6 +5,8 @@ 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, @@ -18,8 +20,6 @@ pub struct Theme { pub panel_editor_size: Vec2, pub widget_spacing: f32, - // pub panel_alert_icon_size : IconSize, - // pub panel_icon_size : IconSize, pub error_color: Color32, pub warning_color: Color32, pub ack_color: Color32, @@ -36,8 +36,6 @@ pub struct Theme { pub bandwidth_graph_color: Color32, pub network_graph_color: Color32, pub node_log_font_size: f32, - // pub panel_icon_size : f32, - // pub panel_icon_padding : f32, pub block_dag_new_block_fill_color: Color32, pub block_dag_block_fill_color: Color32, pub block_dag_block_stroke_color: Color32, @@ -48,14 +46,14 @@ pub struct Theme { 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, - // node_data_color : Color32::from_rgb(151, 209, 198), - // panel_icon_size : IconSize::new(26.,36.), 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), @@ -65,8 +63,7 @@ impl Default for Theme { panel_editor_size: Vec2::new(200_f32, 40_f32), widget_spacing: 4_f32, - // panel_error_icon_size : IconSize::new(Vec2::splat(26.)).with_padding(Vec2::new(6.,0.)), - error_color: Color32::from_rgb(255, 136, 136), //Color32::from_rgb(255, 0, 0), + 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), @@ -89,9 +86,6 @@ impl Default for Theme { 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), - // network_graph_color: Color32::from_rgb(58, 221, 190), - // graph_color: Color32::from_rgb(21, 82, 71), - // panel_icon_size : IconSize::new(Vec2::splat(26.),Vec2::new(36.,26.)), } } } @@ -113,10 +107,6 @@ impl Theme { self.large_button_size } - // pub fn panel_icon_padding(&self) -> f32 { - // self.panel_icon_padding - // } - pub fn apply(&self, visuals: &mut Visuals) { // let visuals = ui.visuals_mut(); visuals.hyperlink_color = self.hyperlink_color; @@ -126,9 +116,7 @@ impl Theme { static mut THEME: Option = None; #[inline(always)] pub fn theme() -> &'static Theme { - unsafe { - THEME.get_or_insert_with(Theme::default) - } + unsafe { THEME.get_or_insert_with(Theme::default) } } pub fn apply_theme(theme: Theme) { diff --git a/core/src/events.rs b/core/src/events.rs index 472b33a..5cd720a 100644 --- a/core/src/events.rs +++ b/core/src/events.rs @@ -2,10 +2,7 @@ use crate::imports::*; use kaspa_metrics::MetricsSnapshot; use kaspa_wallet_core::{events as kaspa, storage::PrvKeyDataInfo}; -pub type ApplicationEventsChannel = crate::channel::Channel; - -// impl Notify { -// } +pub type ApplicationEventsChannel = crate::runtime::channel::Channel; #[derive(Clone)] pub enum Events { diff --git a/core/src/imports.rs b/core/src/imports.rs index 58b9380..72fb5fe 100644 --- a/core/src/imports.rs +++ b/core/src/imports.rs @@ -60,22 +60,20 @@ pub use egui_plot::{PlotPoint, PlotPoints}; pub use crate::collection::Collection; pub use crate::core::Core; -pub use crate::device::Device; pub use crate::egui::*; pub use crate::error::Error; pub use crate::events::{ApplicationEventsChannel, Events}; +pub use crate::menu::Menu; pub use crate::modules; pub use crate::modules::{Module, ModuleCaps, ModuleStyle, ModuleT}; pub use crate::network::Network; pub use crate::notifications::{UserNotification, UserNotifyKind}; -pub use crate::panel::Panel; pub use crate::primitives::{ Account, AccountCollection, BlockDagGraphSettings, DaaBucket, DagBlock, Transaction, TransactionCollection, }; -pub use crate::prompt::{cascade, with_secret}; pub use crate::result::Result; -pub use crate::runtime::{runtime, spawn, spawn_with_result, Payload, Runtime, Service}; +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, diff --git a/core/src/lib.rs b/core/src/lib.rs index bbf7bb7..539d903 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -7,20 +7,18 @@ pub use core::Core; pub mod adaptor; pub mod app; -pub mod channel; pub mod collection; -pub mod device; pub mod egui; pub mod error; pub mod events; pub mod fonts; pub mod imports; +pub mod menu; +pub mod mobile; pub mod modules; pub mod network; pub mod notifications; -pub mod panel; pub mod primitives; -pub mod prompt; pub mod result; pub mod runtime; pub mod settings; diff --git a/core/src/menu.rs b/core/src/menu.rs new file mode 100644 index 0000000..298ead5 --- /dev/null +++ b/core/src/menu.rs @@ -0,0 +1,166 @@ +use crate::imports::*; + +pub struct Menu<'core> { + core: &'core mut Core, +} + +impl<'core> Menu<'core> { + pub fn new(core: &'core mut Core) -> Self { + Self { core } + } + + fn select(&mut self) + where + T: ModuleT + 'static, + { + self.core.select::(); + } + + pub fn render(&mut self, ui: &mut Ui) { + egui::menu::bar(ui, |ui| { + ui.columns(2, |cols| { + cols[0].horizontal(|ui| { + ui.menu_button("File", |ui| { + #[cfg(not(target_arch = "wasm32"))] + if ui.button("Quit").clicked() { + ui.ctx().send_viewport_cmd(ViewportCommand::Close) + } + ui.separator(); + ui.label(" ~ Debug Modules ~"); + ui.label(" "); + + let (tests, mut modules): (Vec<_>, Vec<_>) = self + .core + .modules() + .values() + .cloned() + .partition(|module| module.name().starts_with('~')); + + tests.into_iter().for_each(|module| { + if ui.button(module.name()).clicked() { + self.core.select_with_type_id(module.type_id()); + ui.close_menu(); + } + }); + + ui.label(" "); + + modules.sort_by(|a, b| a.name().partial_cmp(b.name()).unwrap()); + modules.into_iter().for_each(|module| { + if ui.button(module.name()).clicked() { + self.core.select_with_type_id(module.type_id()); + ui.close_menu(); + } + }); + }); + + ui.separator(); + if ui.button("Overview").clicked() { + self.select::(); + } + ui.separator(); + if ui.button("Wallet").clicked() { + if self.core.state().is_open() { + self.select::(); + } else { + self.select::(); + } + } + ui.separator(); + + if ui.button("Settings").clicked() { + self.select::(); + } + + #[cfg(not(target_arch = "wasm32"))] + { + ui.separator(); + if ui.button("Node").clicked() { + self.select::(); + } + } + + ui.separator(); + if ui.button("Metrics").clicked() { + self.select::(); + } + + ui.separator(); + if ui.button("Block DAG").clicked() { + self.select::(); + } + + #[cfg(not(target_arch = "wasm32"))] + { + ui.separator(); + if ui.button("Logs").clicked() { + self.select::(); + } + } + + ui.separator(); + }); + + cols[1].with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let dictionary = i18n::dictionary(); + // use egui_phosphor::light::TRANSLATE; + #[allow(clippy::useless_format)] + ui.menu_button(format!("{} ⏷", dictionary.current_title()), |ui| { + // ui.menu_button(RichText::new(format!("{TRANSLATE} ⏷")).size(18.), |ui| { + dictionary + .enabled_languages() + .into_iter() + .for_each(|(code, lang)| { + if ui.button(lang).clicked() { + self.core.settings.language_code = code.to_string(); + dictionary + .activate_language_code(code) + .expect("Unable to activate language"); + ui.close_menu(); + } + }); + }); + + ui.separator(); + + PopupPanel::new( + ui, + "display_settings", + egui_phosphor::light::MONITOR, + |ui| { + ui.label("hello world"); + + if ui.button("Change").clicked() { + 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); + } + } + }, + ) + .with_min_width(64.) + .build(ui); + + // // let icon_size = theme.panel_icon_size(); + // let icon = CompositeIcon::new(egui_phosphor::light::MONITOR).icon_size(18.); + // // .padding(Some(icon_padding)); + // // if ui.add_enabled(true, icon).clicked() { + // if ui.add(icon).clicked() { + // // close(self.this); + // } + + // if ui.button("Theme").clicked() { + // self.select::(); + // } + ui.separator(); + }); + }); + }); + } +} diff --git a/core/src/mobile.rs b/core/src/mobile.rs new file mode 100644 index 0000000..3d68ce3 --- /dev/null +++ b/core/src/mobile.rs @@ -0,0 +1,140 @@ +use crate::{imports::*, modules::account_manager::AccountManagerSection}; +use egui_extras::{Size, StripBuilder}; +use egui_phosphor::thin::*; + +type MobileMenuHandler<'handler> = dyn FnOnce(&mut Core, &mut Ui) + 'handler; + +pub struct Handler<'handler> { + icon: &'handler str, + text: &'handler str, + handler: Box>, +} + +impl<'handler> Handler<'handler> { + pub fn new( + icon: &'handler str, + text: &'handler str, + handler: Box>, + ) -> Self { + Self { + icon, + text, + handler, + } + } +} + +pub struct MobileMenu<'core> { + core: &'core mut Core, +} + +impl<'core> MobileMenu<'core> { + pub fn new(core: &'core mut Core) -> Self { + Self { core } + } + + pub fn render(&mut self, ui: &mut Ui) { + if self.core.state().is_open() { + self.render_open(ui); + } else { + self.render_closed(ui); + } + } + + pub fn render_closed(&mut self, ui: &mut Ui) { + let handlers = vec![Handler::new( + LOCK_KEY_OPEN, + "OPEN", + Box::new(|core, _ui| { + core.select::(); + }), + )]; + + self.render_strip(ui, handlers); + } + + pub fn render_open(&mut self, ui: &mut Ui) { + let mut handlers = vec![ + // Handler::new(HOUSE, "HOME", Box::new(|core, _ui| { + // core.select::(); + // })), + Handler::new( + WALLET, + "ACCOUNT", + Box::new(|core, _ui| { + core.get_mut::() + .section(AccountManagerSection::Overview); + core.select::(); + }), + ), + Handler::new( + LIST_BULLETS, + "TRANSACTIONS", + Box::new(|core, _ui| { + core.get_mut::() + .section(AccountManagerSection::Transactions); + core.select::(); + }), + ), + Handler::new( + SLIDERS, + "SETTINGS", + Box::new(|core, _ui| { + core.select::(); + }), + ), + ]; + + let account_collection = self + .core + .account_collection + .as_ref() + .expect("account collection"); + + if account_collection.len() > 1 { + handlers.insert( + 1, + Handler::new( + HOUSE, + "HOME", + Box::new(|core, _ui| { + core.get_mut::().select(None); + core.select::(); + }), + ), + ); + } + + self.render_strip(ui, handlers); + } + + fn render_strip(&mut self, ui: &mut Ui, handlers: Vec>) { + let mut strip_builder = StripBuilder::new(ui).cell_layout(Layout::top_down(Align::Center)); + + for _ in handlers.iter() { + strip_builder = strip_builder.size(Size::remainder()); + } + + strip_builder.horizontal(|mut strip| { + for handler in handlers.into_iter() { + let Handler { + icon, + text, + handler, + } = handler; + strip.cell(|ui| { + ui.vertical_centered(|ui| { + if ui + .add(Label::new(RichText::new(icon).size(48.)).sense(Sense::click())) + .clicked() + { + handler(self.core, ui); + } + + ui.label(RichText::new(text).size(8.)); + }); + }); + } + }); + } +} diff --git a/core/src/modules/account_manager.rs b/core/src/modules/account_manager.rs deleted file mode 100644 index fd65bf6..0000000 --- a/core/src/modules/account_manager.rs +++ /dev/null @@ -1,1024 +0,0 @@ -use crate::imports::*; -use crate::primitives::account; -use std::borrow::Cow; -use kaspa_wallet_core::tx::{GeneratorSummary, PaymentOutput, Fees}; -use kaspa_wallet_core::api::*; -use crate::primitives::descriptors::*; - -#[allow(dead_code)] -#[derive(Clone)] -enum State { - Select, - Overview { account: Account }, - Send { account: Account }, - Receive { account: Account }, -} - -#[derive(Default)] -enum Details { - #[default] - Transactions, - Account, - UtxoSelector -} - -#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)] -enum Action { - #[default] - None, - Estimating, - Sending, - // Reset, - Processing, -} - - -impl Action { - fn is_sending(&self) -> bool { - matches!(self, Action::Sending | Action::Estimating | Action::Processing) - } -} - -#[derive(Default, Clone, Copy, Eq, PartialEq)] -enum TransactionKind { - #[default] - None, - Send, - Transfer, - // Request, -} - - - -#[derive(Default, Clone, Copy, Eq, PartialEq)] -enum Focus { - #[default] - None, - Address, - Amount, - Fees, - WalletSecret, - PaymentSecret, -} - -#[derive(Default)] -enum Status { - #[default] - None, - GeneratorSummary(GeneratorSummary), - Error(String), - // Success(GeneratorSummary), -} - -// impl Estimate { -// fn is_ok(&self) -> bool { -// matches!(self, Estimate::GeneratorSummary(_)) -// } - -// fn error(&mut self, error : impl Into) { -// *self = Estimate::Error(error.into()); -// } -// } - -#[derive(Default, Clone, Eq, PartialEq)] -enum AddressStatus { - #[default] - None, - Valid, - NetworkMismatch(NetworkType), - Invalid(String), -} - -#[derive(Default)] -pub struct ManagerContext { - selected: Option, - details : Details, - - // send state - destination_address_string : String, - send_amount_text: String, - send_amount_sompi : u64, - enable_priority_fees : bool, - priority_fees_text : String, - priority_fees_sompi : u64, - estimate : Arc>, - address_status : AddressStatus, - action : Action, - transaction_kind : TransactionKind, - focus : Focus, - wallet_secret : String, - payment_secret : String, -} - -impl ManagerContext { - fn reset_send_state(&mut self) { - - println!("*** resetting send state..."); - - self.destination_address_string = String::default(); - self.send_amount_text = String::default(); - self.send_amount_sompi = 0; - self.enable_priority_fees = false; - self.priority_fees_text = String::default(); - self.priority_fees_sompi = 0; - *self.estimate.lock().unwrap() = Status::None; - self.address_status = AddressStatus::None; - self.action = Action::None; - self.transaction_kind = TransactionKind::None; - self.focus = Focus::None; - self.wallet_secret.zeroize(); - self.payment_secret.zeroize(); - } -} - -pub struct AccountManager { - #[allow(dead_code)] - runtime: Runtime, - - state: State, - context : ManagerContext, - editor_size : Vec2, -} - -impl AccountManager { - pub fn new(runtime: Runtime) -> Self { - Self { - runtime, - state: State::Select, - context : ManagerContext::default(), - editor_size : Vec2::INFINITY, - } - } - - pub fn select(&mut self, account: Option) { - self.context.selected = account; - } -} - -impl ModuleT for AccountManager { - - fn reset(&mut self, _core: &mut Core) { - self.context = ManagerContext::default(); - self.state = State::Select; - } - - fn render( - &mut self, - core: &mut Core, - _ctx: &egui::Context, - _frame: &mut eframe::Frame, - ui: &mut egui::Ui, - ) { - use egui_phosphor::light::{ARROW_CIRCLE_UP,ARROWS_DOWN_UP,QR_CODE}; - // let screen_rect = ui.ctx().screen_rect(); - - let network_type = if let Some(network_id) = core.state().network_id() { - network_id.network_type() - } else { - core.settings.node.network.into() - }; - - let current_daa_score = core.state().current_daa_score(); - - match self.state.clone() { - State::Select => { - // core.apply_large_style(ui); - - if let Some(account_collection) = core.account_collection() { - if account_collection.is_empty() { - ui.label("Please create an account"); - } else if account_collection.len() == 1 { - self.state = State::Overview { - account: account_collection.first().unwrap().clone(), - }; - } else { - ui.heading("Select Account"); - ui.separator(); - - account_collection.iter().for_each(|account| { - if ui - .button(format!("Select {}", account.name_or_id())) - .clicked() - { - self.state = State::Overview { - account: account.clone(), - }; - } - }); - } - - } else { - ui.label("Unable to access account list"); - } - - } - - // State::Create => { - - // //- ACCOUNT TYPE - // //- TODO ACCOUNT NAME - // //- PROMPT FOR PASSWORD - // //- PAYMENT PASSWORD? 25th WORD? - - - // } - - State::Overview { account } => { - let screen_rect_height = ui.ctx().screen_rect().height(); - let width = ui.available_width(); - - ui.horizontal(|ui| { - - let wallet_name = if let Some(wallet_descriptor) = core.wallet_descriptor.as_ref() { - wallet_descriptor.title.as_deref().unwrap_or(wallet_descriptor.filename.as_str()) - } else { - ui.label("Missing wallet descriptor"); - return; - }; - - PopupPanel::new(ui, "wallet_selector_popup",format!("Wallet: {}", wallet_name), |ui| { - - ScrollArea::vertical() - .id_source("wallet_selector_popup_scroll") - .auto_shrink([true; 2]) - .show(ui, |ui| { - - let wallet_list = core.wallet_list().clone(); - - wallet_list.into_iter().for_each(|wallet_descriptor| { - - let title = if let Some(title) = wallet_descriptor.title.clone() { - title - } else if wallet_descriptor.filename.as_str() == "kaspa" { - "Kaspa Wallet".to_string() - } else { - "NO NAME".to_string() - }; - - if ui.add(CompositeButton::new( - title, - wallet_descriptor.filename.clone(), - )).clicked() - { - core.get_mut::().open(wallet_descriptor.clone()); - core.select::(); - } - }); - - ui.label(""); - ui.separator(); - ui.label(""); - - if ui.medium_button( - "Create New Wallet", - ).clicked() - { - core.select::(); - } - - }); - - - }) - .with_min_width(240.) - .with_max_height(screen_rect_height * 0.8) - .with_caption(true) - .with_close_button(true) - .with_pulldown_marker(true) - .build(ui); - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - PopupPanel::new(ui, "account_selector_popup",format!("Account: {}", account.name_or_id()), |ui| { - - - egui::ScrollArea::vertical() - .id_source("account_selector_popup_scroll") - .auto_shrink([true; 2]) - .show(ui, |ui| { - - if let Some(account_collection) = core.account_collection() { - account_collection.iter().for_each(|account| { - if ui - .button(format!("Select {}\n{}", account.name_or_id(),account.balance().map(|balance|sompi_to_kaspa_string_with_suffix(balance.mature, &network_type)).unwrap_or("N/A".to_string()))) - .clicked() - { - self.state = State::Overview { - account: account.clone(), - }; - } - }); - - ui.label(""); - ui.separator(); - ui.label(""); - use egui_phosphor::light::FOLDER_NOTCH_PLUS; - if ui.medium_button(format!("{FOLDER_NOTCH_PLUS} Create New Account")).clicked() { - core.select::(); - } - } - - }); - - }) - .with_min_width(240.) - .with_max_height(screen_rect_height * 0.8) - .with_caption(true) - .with_close_button(true) - .with_pulldown_marker(true) - .build(ui); - - }); - }); - - SidePanel::left("account_manager_left").exact_width(width/2.).resizable(false).show_separator_line(true).show_inside(ui, |ui| { - - core.apply_mobile_style(ui); - - ui.separator(); - ui.add_space(8.); - - egui::ScrollArea::vertical() - .id_source("overview_metrics") - .auto_shrink([false; 2]) - .show(ui, |ui| { - - self.editor_size = Vec2::new(ui.available_width() * 0.75, 32.); - - ui.vertical_centered(|ui| { - - let account_context = if let Some(account_context) = account.context() { - account_context - } else { - ui.label("Account is missing context"); - return; - }; - - self.render_address(core, ui, &account, &account_context, network_type); - - if !core.state().is_synced() || !core.state().is_connected() { - self.render_network_state(core,ui); - return; - } - - self.render_balance(core, ui, &account, &account_context, network_type); - - if self.context.action.is_sending() { - self.render_send_ui(core, ui, &account, &account_context, network_type); - } else { - - self.render_qr(core, ui, &account_context); - - ui.vertical_centered(|ui|{ - ui.horizontal(|ui| { - - CenterLayoutBuilder::new() - .add(Button::new(format!("{} Send", ARROW_CIRCLE_UP)).min_size(theme().medium_button_size()), |(this, _):&mut (&mut AccountManager, &mut Core)| { - this.context.action = Action::Estimating; - this.context.transaction_kind = TransactionKind::Send; - }) - .add(Button::new(format!("{} Transfer", ARROWS_DOWN_UP)).min_size(theme().medium_button_size()), |(this,_)| { - this.context.action = Action::Estimating; - this.context.transaction_kind = TransactionKind::Transfer; - }) - .add(Button::new(format!("{} Request", QR_CODE)).min_size(theme().medium_button_size()), |(_,core)| { - core.select::(); - - }) - .build(ui,&mut (self,core)); - }); - }); - - } - }); - }); - }); - - SidePanel::right("account_manager_right") - .exact_width(width/2.) - .resizable(false) - .show_separator_line(false) - .show_inside(ui, |ui| { - ui.separator(); - - // --- - ui.style_mut().text_styles = core.default_style.text_styles.clone(); - // --- - - egui::menu::bar(ui, |ui| { - ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| { - - ui.add_space(32.); - - if ui.button("UTXOs").clicked() { - self.context.details = Details::UtxoSelector; - } - ui.separator(); - if ui.button("Details").clicked() { - self.context.details = Details::Account; - } - ui.separator(); - if ui.button("Transactions").clicked() { - self.context.details = Details::Transactions; - } - }); - }); - ui.separator(); - - match self.context.details { - Details::Transactions => { - self.render_transactions(ui, core, &account, network_type, current_daa_score); - } - Details::Account => { - self.render_account_details(ui, core, &account); - } - Details::UtxoSelector => { - self.render_utxo_selector(ui, core, &account); - } - } - }); - } - - State::Send { account: _ } => {} - - State::Receive { account: _ } => {} - } - } -} - -impl AccountManager { - - // fn render_account_overview(&mut self, ui: &mut Ui, _core : &mut Core, account : &Account, network_type : NetworkType, current_daa_score : Option) { - - // } - - fn render_network_state(&mut self, core : &mut Core, ui: &mut Ui) { - use egui_phosphor::light::{CLOUD_SLASH,CLOUD_ARROW_DOWN}; - - ui.vertical_centered(|ui|{ - ui.add_space(32.); - if !core.state().is_connected() { - ui.add_space(32.); - ui.label( - RichText::new(CLOUD_SLASH) - .size(theme().icon_size_large) - .color(theme().icon_color_default) - ); - ui.add_space(32.); - - ui.label("You are currently not connected to the Kaspa node."); - } else if !core.state().is_synced() { - - ui.add_space(32.); - ui.label( - RichText::new(CLOUD_ARROW_DOWN) - .size(theme().icon_size_large) - .color(theme().icon_color_default) - ); - ui.add_space(32.); - - ui.label("The node is currently syncing with the Kaspa p2p network."); - ui.add_space(16.); - ui.label("Please wait for the node to sync or connect to a remote node."); - } - ui.add_space(32.); - ui.label("You can configure a remote connection in Settings"); - ui.add_space(16.); - if ui.large_button("Go to Settings").clicked() { - core.select::(); - } - }); - - - } - - fn render_transactions(&mut self, ui: &mut Ui, _core : &mut Core, account : &Account, network_type : NetworkType, current_daa_score : Option) { - egui::ScrollArea::vertical().auto_shrink([false,false]).show(ui, |ui| { - - let transactions = account.transactions(); - - if transactions.is_empty() { - ui.label("No transactions"); - } else { - let total: u64 = transactions.iter().map(|transaction|transaction.aggregate_input_value()).sum(); - transactions.iter().for_each(|transaction| { - transaction.render(ui, network_type, current_daa_score, true, Some(total)); - }); - } - }); - } - - fn render_account_details(&mut self, ui: &mut Ui, _core : &mut Core, account : &Account) { - egui::ScrollArea::vertical().auto_shrink([false,false]).show(ui, |ui| { - - let descriptor = account.descriptor(); - - match &*descriptor { - AccountDescriptor::Bip32(descriptor) => { - descriptor.render(ui); - ui.add_space(8.); - - let mut address_kind : Option = None; - - ui.horizontal(|ui|{ - if ui.medium_button("Generate New Receive Address").clicked() { - address_kind = Some(NewAddressKind::Receive); - } - if ui.medium_button("Generate New Change Address").clicked() { - address_kind = Some(NewAddressKind::Change); - } - }); - - if let Some(address_kind) = address_kind { - let account_id = account.id(); - spawn(async move { - runtime() - .wallet() - .accounts_create_new_address(account_id, address_kind) - .await - .map_err(|err|Error::custom(format!("Failed to create new address\n{err}")))?; - // if let Err(err) = runtime().wallet().accounts_create_new_address(account_id, address_kind).await { - // log_error!("Failed to create new address: {err}"); - // } - - runtime().request_repaint(); - - Ok(()) - }); - } - }, - _ => { - ui.label("Unknown descriptor type"); - } - } - }); - } - - fn render_utxo_selector(&mut self, ui: &mut Ui, _core : &mut Core, _account : &Account) { - egui::ScrollArea::vertical().auto_shrink([false,false]).show(ui, |ui| { - ui.label("UTXO Selection"); - }); - - } - - fn render_address(&mut self, _core: &mut Core, ui : &mut Ui, _account : &Account, context : &account::AccountContext, _network_type : NetworkType) { - use egui_phosphor::light::CLIPBOARD_TEXT; - let address = format_address(context.address(), Some(8)); - if ui.add(Label::new(format!("Address: {address} {CLIPBOARD_TEXT}")).sense(Sense::click())) - // .on_hover_ui_at_pointer(|ui|{ - // ui.vertical(|ui|{ - // ui.add(Label::new(format!("{}", context.address().to_string()))); - // ui.add_space(16.); - // ui.label("Click to copy address to clipboard".to_string()); - // }); - // }) - .clicked() { - ui.output_mut(|o| o.copied_text = context.address().to_string()); - runtime().notify(UserNotification::info(format!("{CLIPBOARD_TEXT} {}", i18n("Copied to clipboard"))).short()) - } - } - - fn render_balance(&mut self, _core: &mut Core, ui : &mut Ui, account : &Account, _context : &account::AccountContext, network_type : NetworkType) { - - // let theme = theme(); - - ui.add_space(10.); - - 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) - ); - - if balance.pending != 0 { - ui.label(format!( - "Pending: {}", - sompi_to_kaspa_string_with_suffix( - balance.pending, - &network_type - ) - )); - } - if balance.outgoing != 0 { - ui.label(format!( - "Sending: {}", - sompi_to_kaspa_string_with_suffix( - balance.outgoing, - &network_type - ) - )); - } - } else { - ui.label("Balance: N/A"); - } - - ui.add_space(10.); - - - if let Some((mature_utxo_size, pending_utxo_size)) = - account.utxo_sizes() - { - if pending_utxo_size == 0 { - ui.label(format!( - "UTXOs: {}", - mature_utxo_size, - )); - } else { - ui.label(format!( - "UTXOs: {} ({} pending)", - mature_utxo_size, pending_utxo_size - )); - } - } else { - ui.label("No UTXOs"); - } - - } - - fn render_qr(&mut self, _core: &mut Core, ui : &mut Ui, context : &account::AccountContext) { - - let scale = if self.context.action == Action::None { 1. } else { 0.35 }; - ui.add( - egui::Image::new(ImageSource::Bytes { uri : Cow::Borrowed("bytes://qr.svg"), bytes: context.qr() }) - .fit_to_original_size(scale) - .texture_options(TextureOptions::NEAREST) - ); - - } - - fn render_estimation_ui(&mut self, _core: &mut Core, ui: &mut egui::Ui, _account : &Account, _context : &Arc, network_type: NetworkType) -> bool { - use egui_phosphor::light::{CHECK, X}; - - let mut request_estimate = false; - - TextEditor::new( - &mut self.context.destination_address_string, - // None, - &mut self.context.focus, - Focus::Address, - |ui, text| { - ui.add_space(8.); - ui.label(egui::RichText::new("Enter destination address").size(12.).raised()); - ui.add_sized(self.editor_size, TextEdit::singleline(text) - .vertical_align(Align::Center)) - }, - ) - .change(|address| { - match Address::try_from(address) { - Ok(address) => { - let address_network_type = NetworkType::try_from(address.prefix).expect("prefix to network type"); - if address_network_type != network_type { - self.context.address_status = AddressStatus::NetworkMismatch(address_network_type); - } else { - self.context.address_status = AddressStatus::Valid; - } - } - Err(err) => { - self.context.address_status = AddressStatus::Invalid(err.to_string()); - } - } - }) - .submit(|_, focus|{ - *focus = Focus::Amount; - }) - .build(ui); - - match &self.context.address_status { - AddressStatus::Valid => {}, - AddressStatus::None => {}, - AddressStatus::NetworkMismatch(address_network_type) => { - ui.label(format!("This address if for the different\nnetwork ({address_network_type})")); - }, - AddressStatus::Invalid(err) => { - ui.label(format!("Please enter a valid address\n{err}")); - } - } - - let response = TextEditor::new( - &mut self.context.send_amount_text, - &mut self.context.focus, - Focus::Amount, - |ui, text| { - ui.add_space(8.); - ui.label(egui::RichText::new("Enter KAS amount to send").size(12.).raised()); - ui.add_sized(self.editor_size, TextEdit::singleline(text) - .vertical_align(Align::Center)) - }, - ) - .change(|_| { - request_estimate = true; - }) - .build(ui); - - if response.text_edit_submit(ui) { - if self.context.enable_priority_fees { - self.context.focus = Focus::Fees; - } else if self.update_user_args() { - self.context.action = Action::Sending; - } - } - - ui.add_space(8.); - ui.checkbox(&mut self.context.enable_priority_fees,i18n("Include Priority Fees")); - - if self.context.enable_priority_fees { - TextEditor::new( - &mut self.context.priority_fees_text, - &mut self.context.focus, - Focus::Fees, - |ui, text| { - ui.add_space(8.); - ui.label(egui::RichText::new("Enter priority fees").size(12.).raised()); - ui.add_sized(self.editor_size, TextEdit::singleline(text) - .vertical_align(Align::Center)) - }, - ) - .change(|_| { - request_estimate = true; - }) - .submit(|_,_|{ - self.context.action = Action::Sending; - }) - .build(ui); - } - - ui.add_space(8.); - let ready_to_send = match &*self.context.estimate.lock().unwrap() { - Status::GeneratorSummary(estimate) => { - if let Some(final_transaction_amount) = estimate.final_transaction_amount { - ui.label(format!("Final Amount: {}", sompi_to_kaspa_string_with_suffix(final_transaction_amount + estimate.aggregated_fees, &network_type))); - } - let fee_title = if self.context.priority_fees_sompi != 0 { - "Network and Priority Fees:" - } else { - "Network Fees:" - }; - ui.label(format!("{} {}", fee_title, sompi_to_kaspa_string_with_suffix(estimate.aggregated_fees, &network_type))); - ui.label(format!("Transactions: {} UTXOs: {}", estimate.number_of_generated_transactions, estimate.aggregated_utxos)); - - self.context.address_status == AddressStatus::Valid - } - Status::Error(error) => { - ui.label(RichText::new(error.to_string()).color(theme().error_color)); - false - } - Status::None => { - ui.label("Please enter KAS amount to send"); - false - } - }; - ui.add_space(8.); - - ui.horizontal(|ui| { - 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 AccountManager| { - this.context.action = Action::Sending; - this.context.focus = Focus::WalletSecret; - }) - .add(Button::new(format!("{X} Cancel")).min_size(theme().medium_button_size()), |this| { - this.context.reset_send_state(); - }) - .build(ui, self) - }); - }); - - }); - - request_estimate - } - - fn render_passphrase_ui(&mut self, _core: &mut Core, ui: &mut egui::Ui, account : &Account, _context : &Arc, _network_type: NetworkType) -> bool { - use egui_phosphor::light::{CHECK, X}; - - let requires_payment_passphrase = account.requires_bip39_passphrase(); - let mut proceed_with_send = false; - - let response = TextEditor::new( - &mut self.context.wallet_secret, - &mut self.context.focus, - Focus::WalletSecret, - |ui, text| { - ui.add_space(8.); - ui.label(egui::RichText::new("Enter wallet password").size(12.).raised()); - ui.add_sized(self.editor_size, TextEdit::singleline(text) - .password(true) - .vertical_align(Align::Center)) - }, - ) - .build(ui); - - if response.text_edit_submit(ui) { - if account.requires_bip39_passphrase() { - self.context.focus = Focus::PaymentSecret; - } else if !self.context.wallet_secret.is_empty() { - proceed_with_send = true; - } - } - - if requires_payment_passphrase { - let response = TextEditor::new( - &mut self.context.payment_secret, - &mut self.context.focus, - Focus::PaymentSecret, - |ui, text| { - ui.add_space(8.); - ui.label(egui::RichText::new("Enter bip39 passphrase").size(12.).raised()); - ui.add_sized(self.editor_size, TextEdit::singleline(text) - .password(true) - .vertical_align(Align::Center)) - }, - ) - .build(ui); - - if response.text_edit_submit(ui) && !self.context.wallet_secret.is_empty() && !self.context.payment_secret.is_empty() { - proceed_with_send = true; - } - - } - - let is_ready_to_send = !(self.context.wallet_secret.is_empty() || requires_payment_passphrase && self.context.payment_secret.is_empty()); - - 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 AccountManager| { - proceed_with_send = true; - }) - .add(Button::new(format!("{X} Cancel")).min_size(theme().medium_button_size()), |this| { - this.context.action = Action::Estimating; - }) - .build(ui,self); - - - - proceed_with_send - } - - fn render_send_ui(&mut self, core: &mut Core, ui: &mut egui::Ui, account : &Account, context : &Arc, network_type: NetworkType) { - - ui.add_space(8.); - ui.label("Sending funds"); - ui.add_space(8.); - - - let send_result = Payload::>::new("send_result"); - - - match self.context.action { - Action::Estimating => { - - let request_estimate = self.render_estimation_ui(core, ui, account, context, network_type); - - if request_estimate && self.update_user_args() { - - let priority_fees_sompi = if self.context.enable_priority_fees { - self.context.priority_fees_sompi - } else { 0 }; - - let address = match network_type { - NetworkType::Testnet => Address::try_from("kaspatest:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhqrxplya").unwrap(), - NetworkType::Mainnet => Address::try_from("kaspa:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkx9awp4e").unwrap(), - _ => panic!("Unsupported network"), - }; - - let account_id = account.id(); - let payment_output = PaymentOutput { - address, - amount: self.context.send_amount_sompi, - }; - - let status = self.context.estimate.clone(); - spawn(async move { - let request = AccountsEstimateRequest { - task_id: None, - account_id, - destination: payment_output.into(), - priority_fee_sompi: Fees::SenderPaysAll(priority_fees_sompi), - payload: None, - }; - - match runtime().wallet().accounts_estimate_call(request).await { - Ok(response) => { - *status.lock().unwrap() = Status::GeneratorSummary(response.generator_summary); - } - Err(error) => { - *status.lock().unwrap() = Status::Error(error.to_string()); - } - } - - runtime().egui_ctx().request_repaint(); - Ok(()) - }); - } - - } - - Action::Sending => { - - let proceed_with_send = self.render_passphrase_ui(core, ui, account, context, network_type); - - if proceed_with_send { - - let priority_fees_sompi = if self.context.enable_priority_fees { - self.context.priority_fees_sompi - } else { 0 }; - - let address = Address::try_from(self.context.destination_address_string.as_str()).expect("Invalid address"); - let account_id = account.id(); - let payment_output = PaymentOutput { - address, - amount: self.context.send_amount_sompi, - }; - let wallet_secret = Secret::try_from(self.context.wallet_secret.clone()).expect("Invalid secret"); - let payment_secret = None; - - spawn_with_result(&send_result, async move { - let request = AccountsSendRequest { - // task_id: None, - account_id, - destination: payment_output.into(), - wallet_secret, - payment_secret, - priority_fee_sompi: Fees::SenderPaysAll(priority_fees_sompi), - payload: None, - }; - - let generator_summary = runtime().wallet().accounts_send_call(request).await?.generator_summary; - // let result = match runtime().wallet().accounts_send_call(request).await; - - // { - // Ok(_response) => { - // // println!("RESPONSE: {:?}", response); - // // *estimate.lock().unwrap() = Estimate::GeneratorSummary(response.generator_summary); - // } - // Err(error) => { - // *status.lock().unwrap() = Status::Error(error.to_string()); - // // self.context.action = Action::Estimating; - // // println!("ERROR: {}", error); - // // *estimate.lock().unwrap() = Estimate::Error(error.to_string()); - // } - // } - - runtime().request_repaint(); - Ok(generator_summary) - }); - - self.context.action = Action::Processing; - } - - } - Action::Processing => { - ui.add_space(16.); - ui.add(egui::Spinner::new().size(92.)); - - if let Some(_result) = send_result.take() { - - // - TODO - SET AND DISPLAY AN ERROR - // - PRESENT CLOSE BUTTON BEFORE CONTINUING - // - RESET STATE TO ESTIMATING? - - self.context.action = Action::None; - } - } - Action::None => {} - } - - } - - fn update_user_args(&mut self) -> bool { - let mut valid = true; - - match try_kaspa_str_to_sompi(self.context.send_amount_text.as_str()) { - Ok(Some(sompi)) => { - self.context.send_amount_sompi = sompi; - } - Ok(None) => { - self.user_error("Please enter an amount".to_string()); - valid = false; - } - Err(err) => { - self.user_error(format!("Invalid amount: {err}")); - valid = false; - } - } - - match try_kaspa_str_to_sompi(self.context.priority_fees_text.as_str()) { - Ok(Some(sompi)) => { - self.context.priority_fees_sompi = sompi; - } - Ok(None) => { - self.context.priority_fees_sompi = 0; - } - Err(err) => { - self.user_error(format!("Invalid fee amount: {err}")); - valid = false; - } - } - - valid - } - - fn user_error(&self, error : impl Into) { - *self.context.estimate.lock().unwrap() = Status::Error(error.into()); - } - -} \ No newline at end of file diff --git a/core/src/modules/account_manager/details.rs b/core/src/modules/account_manager/details.rs new file mode 100644 index 0000000..0a5ee9a --- /dev/null +++ b/core/src/modules/account_manager/details.rs @@ -0,0 +1,56 @@ +use crate::imports::*; +use super::*; + +pub struct Details { +} + +impl Details { + pub fn new() -> Self { + Self { } + } + + pub fn render(&mut self, _core: &mut Core, ui : &mut Ui, rc : &RenderContext<'_>) { + let RenderContext { account, .. } = rc; + + egui::ScrollArea::vertical().auto_shrink([false,false]).show(ui, |ui| { + + let descriptor = account.descriptor(); + + match &*descriptor { + AccountDescriptor::Bip32(descriptor) => { + descriptor.render(ui); + ui.add_space(8.); + + let mut address_kind : Option = None; + + ui.horizontal(|ui|{ + if ui.medium_button("Generate New Receive Address").clicked() { + address_kind = Some(NewAddressKind::Receive); + } + if ui.medium_button("Generate New Change Address").clicked() { + address_kind = Some(NewAddressKind::Change); + } + }); + + if let Some(address_kind) = address_kind { + let account_id = account.id(); + spawn(async move { + runtime() + .wallet() + .accounts_create_new_address(account_id, address_kind) + .await + .map_err(|err|Error::custom(format!("Failed to create new address\n{err}")))?; + + runtime().request_repaint(); + + Ok(()) + }); + } + }, + _ => { + ui.label("Unknown descriptor type"); + } + } + }); + } +} \ No newline at end of file diff --git a/core/src/modules/account_manager/menus.rs b/core/src/modules/account_manager/menus.rs new file mode 100644 index 0000000..576820a --- /dev/null +++ b/core/src/modules/account_manager/menus.rs @@ -0,0 +1,172 @@ +use crate::imports::*; +use super::*; + +pub struct WalletMenu { } + +impl WalletMenu { + pub fn new() -> Self { + Self { } + } + + pub fn render(&mut self, core: &mut Core, ui : &mut Ui, max_height: f32) { + + let wallet_name = if let Some(wallet_descriptor) = core.wallet_descriptor.as_ref() { + wallet_descriptor.title.as_deref().unwrap_or(wallet_descriptor.filename.as_str()) + } else { + ui.label("Missing wallet descriptor"); + return; + }; + + PopupPanel::new(ui, "wallet_selector_popup",format!("Wallet: {}", wallet_name), |ui| { + + ScrollArea::vertical() + .id_source("wallet_selector_popup_scroll") + .auto_shrink([true; 2]) + .show(ui, |ui| { + + let wallet_list = core.wallet_list().clone(); + + wallet_list.into_iter().for_each(|wallet_descriptor| { + + let title = if let Some(title) = wallet_descriptor.title.clone() { + title + } else if wallet_descriptor.filename.as_str() == "kaspa" { + "Kaspa Wallet".to_string() + } else { + "NO NAME".to_string() + }; + + if ui.add(CompositeButton::new( + title, + wallet_descriptor.filename.clone(), + )).clicked() + { + core.get_mut::().open(wallet_descriptor.clone()); + core.select::(); + } + }); + + ui.label(""); + ui.separator(); + ui.label(""); + + if ui.medium_button( + "Create New Wallet", + ).clicked() + { + core.select::(); + } + }); + + }) + .with_min_width(240.) + .with_max_height(max_height) + .with_caption(true) + .with_close_button(true) + .with_pulldown_marker(true) + .build(ui); + + } +} + +pub struct AccountMenu { } + +impl AccountMenu { + pub fn new() -> Self { + Self { } + } + 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| { + + egui::ScrollArea::vertical() + .id_source("account_selector_popup_scroll") + .auto_shrink([true; 2]) + .show(ui, |ui| { + + if let Some(account_collection) = core.account_collection() { + account_collection.iter().for_each(|account| { + if ui + .button(format!("Select {}\n{}", account.name_or_id(),account.balance().map(|balance|sompi_to_kaspa_string_with_suffix(balance.mature, network_type)).unwrap_or("N/A".to_string()))) + .clicked() + { + account_manager.state = AccountManagerState::Overview { + account: account.clone(), + }; + } + }); + + ui.label(""); + ui.separator(); + ui.label(""); + use egui_phosphor::light::FOLDER_NOTCH_PLUS; + if ui.medium_button(format!("{FOLDER_NOTCH_PLUS} Create New Account")).clicked() { + core.select::(); + } + } + + }); + + }) + .with_min_width(240.) + .with_max_height(max_height) + .with_caption(true) + .with_close_button(true) + .with_pulldown_marker(true) + .build(ui); + } +} + + +pub struct ToolsMenu { } + +impl ToolsMenu { + pub fn new() -> Self { + Self { } + } + 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| { + + egui::ScrollArea::vertical() + .id_source("tools_popup_scroll") + .auto_shrink([true; 2]) + .show(ui, |ui| { + + let _ = ui.button("Create Account"); + let _ = ui.button("Import"); + let _ = ui.button("Export"); + // ui.button("Export"); + + // if let Some(account_collection) = core.account_collection() { + // account_collection.iter().for_each(|account| { + // if ui + // .button(format!("Select {}\n{}", account.name_or_id(),account.balance().map(|balance|sompi_to_kaspa_string_with_suffix(balance.mature, network_type)).unwrap_or("N/A".to_string()))) + // .clicked() + // { + // account_manager.state = AccountManagerState::Overview { + // account: account.clone(), + // }; + // } + // }); + + // ui.label(""); + // ui.separator(); + // ui.label(""); + // use egui_phosphor::light::FOLDER_NOTCH_PLUS; + // if ui.medium_button(format!("{FOLDER_NOTCH_PLUS} Create New Account")).clicked() { + // core.select::(); + // } + // } + + }); + + }) + .with_min_width(240.) + .with_max_height(max_height) + .with_pulldown_marker(true) + .with_close_on_interaction(true) + .build(ui); + } +} + diff --git a/core/src/modules/account_manager/mod.rs b/core/src/modules/account_manager/mod.rs new file mode 100644 index 0000000..3b3c22c --- /dev/null +++ b/core/src/modules/account_manager/mod.rs @@ -0,0 +1,402 @@ +use crate::imports::*; +use crate::primitives::account; +use std::borrow::Cow; +use kaspa_wallet_core::tx::{GeneratorSummary, PaymentOutput, Fees}; +use kaspa_wallet_core::api::*; +use crate::primitives::descriptors::*; + +mod overview; +mod transactions; +mod details; +mod utxo; +mod menus; + +use overview::*; +use transactions::*; +use details::*; +use utxo::*; +use menus::*; + + +#[allow(dead_code)] +#[derive(Clone)] +enum AccountManagerState { + Select, + Overview { account: Account }, +} + +#[derive(Default, Copy, Clone, Eq, PartialEq)] +pub enum AccountManagerSection { + #[default] + // None, + Overview, + Transactions, + Details, + UtxoManager +} + +#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)] +enum Action { + #[default] + None, + Estimating, + Sending, + // Reset, + Processing, +} + + +impl Action { + fn is_sending(&self) -> bool { + matches!(self, Action::Sending | Action::Estimating | Action::Processing) + } +} + +#[derive(Default, Clone, Copy, Eq, PartialEq)] +enum TransactionKind { + #[default] + None, + Send, + Transfer, +} + + + +#[derive(Default, Clone, Copy, Eq, PartialEq)] +enum Focus { + #[default] + None, + Address, + Amount, + Fees, + WalletSecret, + PaymentSecret, +} + +#[derive(Default)] +pub enum EstimatorStatus { + #[default] + None, + GeneratorSummary(GeneratorSummary), + Error(String), +} + +#[derive(Default, Clone, Eq, PartialEq)] +enum AddressStatus { + #[default] + None, + Valid, + NetworkMismatch(NetworkType), + Invalid(String), +} + +#[derive(Default)] +pub struct ManagerContext { + destination_address_string : String, + send_amount_text: String, + send_amount_sompi : u64, + enable_priority_fees : bool, + priority_fees_text : String, + priority_fees_sompi : u64, + estimate : Arc>, + address_status : AddressStatus, + action : Action, + transaction_kind : TransactionKind, + focus : Focus, + wallet_secret : String, + payment_secret : String, +} + +impl ManagerContext { + pub fn new() -> Self { + Self { + ..Default::default() + } + } + + fn reset_send_state(&mut self) { + + println!("*** resetting send state..."); + + self.destination_address_string = String::default(); + self.send_amount_text = String::default(); + self.send_amount_sompi = 0; + self.enable_priority_fees = false; + self.priority_fees_text = String::default(); + self.priority_fees_sompi = 0; + *self.estimate.lock().unwrap() = EstimatorStatus::None; + self.address_status = AddressStatus::None; + self.action = Action::None; + self.transaction_kind = TransactionKind::None; + self.focus = Focus::None; + self.wallet_secret.zeroize(); + self.payment_secret.zeroize(); + } +} + +pub struct RenderContext<'render> { + pub account : &'render Account, + pub context : Arc, + pub network_type : NetworkType, + pub current_daa_score : Option, +} + +impl<'render> RenderContext<'render> { + pub fn new(account : &'render Account, network_type : NetworkType, current_daa_score : Option) -> Result { + + let context = if let Some(context) = account.context() { + context + } else { + return Err(Error::custom("Account is missing context")); + }; + + Ok(Self { + account, + context, + network_type, + current_daa_score, + }) + } +} + +pub struct AccountManager { + #[allow(dead_code)] + runtime: Runtime, + + state: AccountManagerState, + section: AccountManagerSection, + context : ManagerContext, +} + +impl AccountManager { + pub fn new(runtime: Runtime) -> Self { + Self { + runtime, + state: AccountManagerState::Select, + section: AccountManagerSection::Overview, + context : ManagerContext::default(), + } + } +} + +impl ModuleT for AccountManager { + + fn reset(&mut self, _core: &mut Core) { + self.context = ManagerContext::default(); + self.state = AccountManagerState::Select; + } + + fn render( + &mut self, + core: &mut Core, + _ctx: &egui::Context, + _frame: &mut eframe::Frame, + ui: &mut egui::Ui, + ) { + if let Err(err) = self.render_state(core, ui) { + ui.colored_label(theme().error_color, err.to_string()); + } + } + +} + +impl AccountManager { + + pub fn select(&mut self, account: Option) { + if let Some(account) = account { + self.state = AccountManagerState::Overview { + account: account.clone(), + }; + + if runtime().device().is_portrait() { + self.section = AccountManagerSection::Overview; + } else { + self.section = AccountManagerSection::Transactions; + } + } else { + self.state = AccountManagerState::Select; + } + + } + + pub fn section(&mut self, section : AccountManagerSection) { + self.section = section; + } + + fn render_state( + &mut self, + core: &mut Core, + ui: &mut egui::Ui, + ) -> Result<()> { + + let network_type = if let Some(network_id) = core.state().network_id() { + network_id.network_type() + } else { + core.settings.node.network.into() + }; + + let current_daa_score = core.state().current_daa_score(); + + match self.state.clone() { + AccountManagerState::Select => { + + if !core.state().is_open() { + core.select::(); + } else if let Some(account_collection) = core.account_collection() { + if account_collection.is_empty() { + ui.label("Please create an account"); + } else if account_collection.len() == 1 { + self.select(Some(account_collection.first().unwrap().clone())); + // self.state = AccountManagerState::Overview { + // account: account_collection.first().unwrap().clone(), + // }; + } else { + ui.heading("Select Account"); + ui.separator(); + + account_collection.iter().for_each(|account| { + if ui + .button(format!("Select {}", account.name_or_id())) + .clicked() + { + self.select(Some(account.clone())); + if runtime().device().is_singular_layout() { + self.section = AccountManagerSection::Overview; + } else { + self.section = AccountManagerSection::Transactions; + } + } + }); + } + + } else { + ui.label("Unable to access account list"); + } + } + + AccountManagerState::Overview { account } => { + let rc = RenderContext::new(&account, network_type, current_daa_score)?; + // let section = self.section; + if runtime().device().is_singular_layout() { + self.render_singular_layout(core,ui,&rc, self.section); + } else { + if self.section == AccountManagerSection::Overview { + self.section = AccountManagerSection::Transactions; + } + self.render_landscape(core,ui,&rc, self.section); + } + } + } + + Ok(()) + } + + + fn render_menu(&mut self, core: &mut Core, ui: &mut Ui, rc : &RenderContext<'_>) { + ui.horizontal(|ui| { + let screen_rect_height = ui.ctx().screen_rect().height(); + WalletMenu::new().render(core,ui,screen_rect_height * 0.8); + ui.separator(); + AccountMenu::new().render(core,ui,self,rc, screen_rect_height * 0.8); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ToolsMenu::new().render(core,ui,self, rc, screen_rect_height * 0.8); + }); + }); + } + + fn render_landscape(&mut self, core: &mut Core, ui: &mut Ui, rc : &RenderContext<'_>, section : AccountManagerSection) { + + let panel_width = ui.available_width() * 0.5; + + self.render_menu(core,ui,rc); + + SidePanel::left("account_manager_left") + .exact_width(panel_width) + .resizable(false) + .show_separator_line(true) + // .frame( + // Frame::default() + // .inner_margin(0.) + // .outer_margin(4.) + // .fill(ui.ctx().style().visuals.panel_fill), + // ) + .show_inside(ui, |ui| { + Overview::new(&mut self.context).render(core,ui,rc); + }); + + SidePanel::right("account_manager_right") + .exact_width(panel_width) + .resizable(false) + .show_separator_line(false) + // .frame( + // Frame::default() + // .inner_margin(0.) + // .outer_margin(4.) + // .fill(ui.ctx().style().visuals.panel_fill), + // ) + .show_inside(ui, |ui| { + ui.separator(); + + // --- + ui.style_mut().text_styles = core.default_style.text_styles.clone(); + // --- + + egui::menu::bar(ui, |ui| { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| { + + ui.add_space(32.); + + if ui.button("UTXOs").clicked() { + self.section = AccountManagerSection::UtxoManager; + } + ui.separator(); + if ui.button("Details").clicked() { + self.section = AccountManagerSection::Details; + } + ui.separator(); + if ui.button("Transactions").clicked() { + self.section = AccountManagerSection::Transactions; + } + }); + }); + ui.separator(); + + match section { + AccountManagerSection::Overview => { + Overview::new(&mut self.context).render(core,ui,rc); + } + AccountManagerSection::Transactions => { + Transactions::new().render(ui,core,rc); + } + AccountManagerSection::Details => { + Details::new().render(core,ui,rc); + } + AccountManagerSection::UtxoManager => { + UtxoManager::new().render(core,ui,rc); + } + } + }); + + + } + + fn render_singular_layout(&mut self, core: &mut Core, ui: &mut Ui, rc : &RenderContext<'_>, section : AccountManagerSection) { + + match section { + AccountManagerSection::Overview => { + Overview::new(&mut self.context).render(core,ui,rc); + } + AccountManagerSection::Transactions => { + Transactions::new().render(ui,core,rc); + } + AccountManagerSection::Details => { + Details::new().render(core,ui,rc); + } + AccountManagerSection::UtxoManager => { + UtxoManager::new().render(core,ui,rc); + } + } + + } + +} \ No newline at end of file diff --git a/core/src/modules/account_manager/overview.rs b/core/src/modules/account_manager/overview.rs new file mode 100644 index 0000000..433965d --- /dev/null +++ b/core/src/modules/account_manager/overview.rs @@ -0,0 +1,582 @@ +use crate::imports::*; +use super::*; + +pub struct Overview<'manager> { + context : &'manager mut ManagerContext, + editor_size : Vec2, +} + +impl<'manager> Overview<'manager> { + pub fn new(context : &'manager mut ManagerContext) -> Self { + Self { context, editor_size : Vec2::INFINITY } + } + + pub fn render(&mut self, core: &mut Core, ui : &mut Ui, rc : &RenderContext<'_>) { + use egui_phosphor::light::{ARROW_CIRCLE_UP,ARROWS_DOWN_UP,QR_CODE}; + + core.apply_mobile_style(ui); + + ui.separator(); + ui.add_space(8.); + + egui::ScrollArea::vertical() + .id_source("overview_metrics") + .auto_shrink([false; 2]) + .show(ui, |ui| { + + self.editor_size = Vec2::new(ui.available_width() * 0.75, 32.); + + ui.vertical_centered(|ui| { + + self.render_address(core, ui, rc); + + if !core.state().is_synced() || !core.state().is_connected() { + self.render_network_state(core,ui); + return; + } + + self.render_balance(core, ui, rc); + + if self.context.action.is_sending() { + self.render_send_ui(core, ui, rc); + } else { + + self.render_qr(core, ui, rc); + + ui.vertical_centered(|ui|{ + 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)| { + 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,_)| { + 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)| { + core.select::(); + + }); + + layout.build(ui,&mut (self,core)); + }); + }); + + } + }); + }); + } + + fn render_network_state(&mut self, core : &mut Core, ui: &mut Ui) { + use egui_phosphor::light::{CLOUD_SLASH,CLOUD_ARROW_DOWN}; + + ui.vertical_centered(|ui|{ + ui.add_space(32.); + if !core.state().is_connected() { + ui.add_space(32.); + ui.label( + RichText::new(CLOUD_SLASH) + .size(theme().icon_size_large) + .color(theme().icon_color_default) + ); + ui.add_space(32.); + + ui.label("You are currently not connected to the Kaspa node."); + } else if !core.state().is_synced() { + + ui.add_space(32.); + ui.label( + RichText::new(CLOUD_ARROW_DOWN) + .size(theme().icon_size_large) + .color(theme().icon_color_default) + ); + ui.add_space(32.); + + ui.label("The node is currently syncing with the Kaspa p2p network."); + ui.add_space(16.); + ui.label("Please wait for the node to sync or connect to a remote node."); + } + ui.add_space(32.); + ui.label("You can configure a remote connection in Settings"); + ui.add_space(16.); + if ui.large_button("Go to Settings").clicked() { + core.select::(); + } + }); + + + } + + fn render_address(&mut self, _core: &mut Core, ui : &mut Ui, rc : &RenderContext<'_>) { + use egui_phosphor::light::CLIPBOARD_TEXT; + let address = format_address(rc.context.address(), Some(8)); + if ui.add(Label::new(format!("Address: {address} {CLIPBOARD_TEXT}")).sense(Sense::click())) + // .on_hover_ui_at_pointer(|ui|{ + // ui.vertical(|ui|{ + // ui.add(Label::new(format!("{}", context.address().to_string()))); + // ui.add_space(16.); + // ui.label("Click to copy address to clipboard".to_string()); + // }); + // }) + .clicked() { + ui.output_mut(|o| o.copied_text = rc.context.address().to_string()); + runtime().notify(UserNotification::info(format!("{CLIPBOARD_TEXT} {}", i18n("Copied to clipboard"))).short()) + } + } + + fn render_balance(&mut self, _core: &mut Core, ui : &mut Ui, rc: &RenderContext<'_>) { + + // let theme = theme(); + let RenderContext { account, network_type, .. } = rc; + + ui.add_space(10.); + + 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) + ); + + if balance.pending != 0 { + ui.label(format!( + "Pending: {}", + sompi_to_kaspa_string_with_suffix( + balance.pending, + network_type + ) + )); + } + if balance.outgoing != 0 { + ui.label(format!( + "Sending: {}", + sompi_to_kaspa_string_with_suffix( + balance.outgoing, + network_type + ) + )); + } + + ui.add_space(10.); + + let suffix = if balance.pending_utxo_count != 0 && balance.stasis_utxo_count != 0 { + format!(" ({} pending, {} processing)", balance.pending_utxo_count, balance.stasis_utxo_count) + } else if balance.pending_utxo_count != 0 { + format!(" ({} pending)", balance.pending_utxo_count) + } else if balance.stasis_utxo_count != 0 { + format!(" ({} processing)", balance.stasis_utxo_count) + } else { + "".to_string() + }; + + ui.label(format!( + "UTXOs: {}{suffix}", + balance.mature_utxo_count.separated_string(), + )); + } else { + ui.label("Balance: N/A"); + } + + + + } + + fn render_qr(&mut self, _core: &mut Core, ui : &mut Ui, rc: &RenderContext<'_>) { + + let scale = if self.context.action == Action::None { 1. } else { 0.35 }; + ui.add( + egui::Image::new(ImageSource::Bytes { uri : Cow::Borrowed("bytes://qr.svg"), bytes: rc.context.qr() }) + .fit_to_original_size(scale) + .texture_options(TextureOptions::NEAREST) + ); + + } + + fn render_estimation_ui(&mut self, _core: &mut Core, ui: &mut egui::Ui, rc: &RenderContext<'_>) -> bool { + use egui_phosphor::light::{CHECK, X}; + + let RenderContext { network_type, .. } = rc; + + let mut request_estimate = false; + + TextEditor::new( + &mut self.context.destination_address_string, + // None, + &mut self.context.focus, + Focus::Address, + |ui, text| { + ui.add_space(8.); + ui.label(egui::RichText::new("Enter destination address").size(12.).raised()); + ui.add_sized(self.editor_size, TextEdit::singleline(text) + .vertical_align(Align::Center)) + }, + ) + .change(|address| { + match Address::try_from(address) { + Ok(address) => { + let address_network_type = NetworkType::try_from(address.prefix).expect("prefix to network type"); + if address_network_type != *network_type { + self.context.address_status = AddressStatus::NetworkMismatch(address_network_type); + } else { + self.context.address_status = AddressStatus::Valid; + } + } + Err(err) => { + self.context.address_status = AddressStatus::Invalid(err.to_string()); + } + } + }) + .submit(|_, focus|{ + *focus = Focus::Amount; + }) + .build(ui); + + match &self.context.address_status { + AddressStatus::Valid => {}, + AddressStatus::None => {}, + AddressStatus::NetworkMismatch(address_network_type) => { + ui.label(format!("This address if for the different\nnetwork ({address_network_type})")); + }, + AddressStatus::Invalid(err) => { + ui.label(format!("Please enter a valid address\n{err}")); + } + } + + let response = TextEditor::new( + &mut self.context.send_amount_text, + &mut self.context.focus, + Focus::Amount, + |ui, text| { + ui.add_space(8.); + ui.label(egui::RichText::new("Enter KAS amount to send").size(12.).raised()); + ui.add_sized(self.editor_size, TextEdit::singleline(text) + .vertical_align(Align::Center)) + }, + ) + .change(|_| { + request_estimate = true; + }) + .build(ui); + + if response.text_edit_submit(ui) { + if self.context.enable_priority_fees { + self.context.focus = Focus::Fees; + } else if self.update_user_args() { + self.context.action = Action::Sending; + } + } + + ui.add_space(8.); + ui.checkbox(&mut self.context.enable_priority_fees,i18n("Include Priority Fees")); + + if self.context.enable_priority_fees { + TextEditor::new( + &mut self.context.priority_fees_text, + &mut self.context.focus, + Focus::Fees, + |ui, text| { + ui.add_space(8.); + ui.label(egui::RichText::new("Enter priority fees").size(12.).raised()); + ui.add_sized(self.editor_size, TextEdit::singleline(text) + .vertical_align(Align::Center)) + }, + ) + .change(|_| { + request_estimate = true; + }) + .submit(|_,_|{ + self.context.action = Action::Sending; + }) + .build(ui); + } + + ui.add_space(8.); + let ready_to_send = match &*self.context.estimate.lock().unwrap() { + EstimatorStatus::GeneratorSummary(estimate) => { + if let Some(final_transaction_amount) = estimate.final_transaction_amount { + ui.label(format!("Final Amount: {}", sompi_to_kaspa_string_with_suffix(final_transaction_amount + estimate.aggregated_fees, network_type))); + } + let fee_title = if self.context.priority_fees_sompi != 0 { + "Network and Priority Fees:" + } else { + "Network Fees:" + }; + ui.label(format!("{} {}", fee_title, sompi_to_kaspa_string_with_suffix(estimate.aggregated_fees, network_type))); + ui.label(format!("Transactions: {} UTXOs: {}", estimate.number_of_generated_transactions, estimate.aggregated_utxos)); + + self.context.address_status == AddressStatus::Valid + } + EstimatorStatus::Error(error) => { + ui.label(RichText::new(error.to_string()).color(theme().error_color)); + false + } + EstimatorStatus::None => { + ui.label("Please enter KAS amount to send"); + false + } + }; + ui.add_space(8.); + + ui.horizontal(|ui| { + 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<'_>| { + this.context.action = Action::Sending; + this.context.focus = Focus::WalletSecret; + }) + .add(Button::new(format!("{X} Cancel")).min_size(theme().medium_button_size()), |this| { + this.context.reset_send_state(); + }) + .build(ui, self) + }); + }); + + }); + + request_estimate + } + + fn render_passphrase_ui(&mut self, _core: &mut Core, ui: &mut egui::Ui, rc: &RenderContext<'_>) -> bool { + use egui_phosphor::light::{CHECK, X}; + + let RenderContext { account, .. } = rc; + + let requires_payment_passphrase = account.requires_bip39_passphrase(); + let mut proceed_with_send = false; + + let response = TextEditor::new( + &mut self.context.wallet_secret, + &mut self.context.focus, + Focus::WalletSecret, + |ui, text| { + ui.add_space(8.); + ui.label(egui::RichText::new("Enter wallet password").size(12.).raised()); + ui.add_sized(self.editor_size, TextEdit::singleline(text) + .password(true) + .vertical_align(Align::Center)) + }, + ) + .build(ui); + + if response.text_edit_submit(ui) { + if account.requires_bip39_passphrase() { + self.context.focus = Focus::PaymentSecret; + } else if !self.context.wallet_secret.is_empty() { + proceed_with_send = true; + } + } + + if requires_payment_passphrase { + let response = TextEditor::new( + &mut self.context.payment_secret, + &mut self.context.focus, + Focus::PaymentSecret, + |ui, text| { + ui.add_space(8.); + ui.label(egui::RichText::new("Enter bip39 passphrase").size(12.).raised()); + ui.add_sized(self.editor_size, TextEdit::singleline(text) + .password(true) + .vertical_align(Align::Center)) + }, + ) + .build(ui); + + if response.text_edit_submit(ui) && !self.context.wallet_secret.is_empty() && !self.context.payment_secret.is_empty() { + proceed_with_send = true; + } + + } + + let is_ready_to_send = !(self.context.wallet_secret.is_empty() || requires_payment_passphrase && self.context.payment_secret.is_empty()); + + 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<'_>| { + proceed_with_send = true; + }) + .add(Button::new(format!("{X} Cancel")).min_size(theme().medium_button_size()), |this| { + this.context.action = Action::Estimating; + }) + .build(ui,self); + + + + proceed_with_send + } + + fn render_send_ui(&mut self, core: &mut Core, ui: &mut egui::Ui, rc: &RenderContext<'_>) { + + let RenderContext { account, network_type, .. } = rc; + + ui.add_space(8.); + ui.label("Sending funds"); + ui.add_space(8.); + + + let send_result = Payload::>::new("send_result"); + + + match self.context.action { + Action::Estimating => { + + let request_estimate = self.render_estimation_ui(core, ui, rc); + + if request_estimate && self.update_user_args() { + + let priority_fees_sompi = if self.context.enable_priority_fees { + self.context.priority_fees_sompi + } else { 0 }; + + let address = match network_type { + NetworkType::Testnet => Address::try_from("kaspatest:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhqrxplya").unwrap(), + NetworkType::Mainnet => Address::try_from("kaspa:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkx9awp4e").unwrap(), + _ => panic!("Unsupported network"), + }; + + let account_id = account.id(); + let payment_output = PaymentOutput { + address, + amount: self.context.send_amount_sompi, + }; + + let status = self.context.estimate.clone(); + spawn(async move { + let request = AccountsEstimateRequest { + task_id: None, + account_id, + destination: payment_output.into(), + priority_fee_sompi: Fees::SenderPaysAll(priority_fees_sompi), + payload: None, + }; + + match runtime().wallet().accounts_estimate_call(request).await { + Ok(response) => { + *status.lock().unwrap() = EstimatorStatus::GeneratorSummary(response.generator_summary); + } + Err(error) => { + *status.lock().unwrap() = EstimatorStatus::Error(error.to_string()); + } + } + + runtime().egui_ctx().request_repaint(); + Ok(()) + }); + } + + } + + Action::Sending => { + + let proceed_with_send = self.render_passphrase_ui(core, ui, rc); + + if proceed_with_send { + + let priority_fees_sompi = if self.context.enable_priority_fees { + self.context.priority_fees_sompi + } else { 0 }; + + let address = Address::try_from(self.context.destination_address_string.as_str()).expect("Invalid address"); + let account_id = account.id(); + let payment_output = PaymentOutput { + address, + amount: self.context.send_amount_sompi, + }; + let wallet_secret = Secret::try_from(self.context.wallet_secret.clone()).expect("Invalid secret"); + let payment_secret = None; + + spawn_with_result(&send_result, async move { + let request = AccountsSendRequest { + // task_id: None, + account_id, + destination: payment_output.into(), + wallet_secret, + payment_secret, + priority_fee_sompi: Fees::SenderPaysAll(priority_fees_sompi), + payload: None, + }; + + let generator_summary = runtime().wallet().accounts_send_call(request).await?.generator_summary; + // let result = match runtime().wallet().accounts_send_call(request).await; + + // { + // Ok(_response) => { + // // println!("RESPONSE: {:?}", response); + // // *estimate.lock().unwrap() = Estimate::GeneratorSummary(response.generator_summary); + // } + // Err(error) => { + // *status.lock().unwrap() = Status::Error(error.to_string()); + // // self.context.action = Action::Estimating; + // // println!("ERROR: {}", error); + // // *estimate.lock().unwrap() = Estimate::Error(error.to_string()); + // } + // } + + runtime().request_repaint(); + Ok(generator_summary) + }); + + self.context.action = Action::Processing; + } + + } + Action::Processing => { + ui.add_space(16.); + ui.add(egui::Spinner::new().size(92.)); + + if let Some(_result) = send_result.take() { + + // - TODO - SET AND DISPLAY AN ERROR + // - PRESENT CLOSE BUTTON BEFORE CONTINUING + // - RESET STATE TO ESTIMATING? + + self.context.action = Action::None; + } + } + Action::None => {} + } + + } + + fn update_user_args(&mut self) -> bool { + let mut valid = true; + + match try_kaspa_str_to_sompi(self.context.send_amount_text.as_str()) { + Ok(Some(sompi)) => { + self.context.send_amount_sompi = sompi; + } + Ok(None) => { + self.user_error("Please enter an amount".to_string()); + valid = false; + } + Err(err) => { + self.user_error(format!("Invalid amount: {err}")); + valid = false; + } + } + + match try_kaspa_str_to_sompi(self.context.priority_fees_text.as_str()) { + Ok(Some(sompi)) => { + self.context.priority_fees_sompi = sompi; + } + Ok(None) => { + self.context.priority_fees_sompi = 0; + } + Err(err) => { + self.user_error(format!("Invalid fee amount: {err}")); + valid = false; + } + } + + valid + } + + fn user_error(&self, error : impl Into) { + *self.context.estimate.lock().unwrap() = EstimatorStatus::Error(error.into()); + } + +} \ No newline at end of file diff --git a/core/src/modules/account_manager/transactions.rs b/core/src/modules/account_manager/transactions.rs new file mode 100644 index 0000000..50fde49 --- /dev/null +++ b/core/src/modules/account_manager/transactions.rs @@ -0,0 +1,26 @@ +use crate::imports::*; +use super::*; + +pub struct Transactions { } + +impl Transactions { + pub fn new() -> Self { + Self { } + } + + pub fn render(&mut self, ui: &mut Ui, _core : &mut Core, rc : &RenderContext<'_>) { + let RenderContext { account, network_type, current_daa_score, .. } = rc; + + egui::ScrollArea::vertical().auto_shrink([false,false]).show(ui, |ui| { + let transactions = account.transactions(); + if transactions.is_empty() { + ui.label("No transactions"); + } else { + let total: u64 = transactions.iter().map(|transaction|transaction.aggregate_input_value()).sum(); + transactions.iter().for_each(|transaction| { + transaction.render(ui, *network_type, *current_daa_score, true, Some(total)); + }); + } + }); + } +} diff --git a/core/src/modules/account_manager/utxo.rs b/core/src/modules/account_manager/utxo.rs new file mode 100644 index 0000000..d852752 --- /dev/null +++ b/core/src/modules/account_manager/utxo.rs @@ -0,0 +1,20 @@ +use crate::imports::*; +use super::*; + +pub struct UtxoManager { +} + +impl UtxoManager { + pub fn new() -> Self { + Self { } + } + + pub fn render(&mut self, _core: &mut Core, ui : &mut Ui, rc : &RenderContext<'_>) { + let RenderContext { account: _, .. } = rc; + + egui::ScrollArea::vertical().auto_shrink([false,false]).show(ui, |ui| { + ui.label("UTXO Manager"); + }); + + } +} \ No newline at end of file diff --git a/core/src/modules/settings/mobile.rs b/core/src/modules/settings/mobile.rs new file mode 100644 index 0000000..e69de29 diff --git a/core/src/modules/settings.rs b/core/src/modules/settings/mod.rs similarity index 68% rename from core/src/modules/settings.rs rename to core/src/modules/settings/mod.rs index 5babee1..2f18977 100644 --- a/core/src/modules/settings.rs +++ b/core/src/modules/settings/mod.rs @@ -1,10 +1,5 @@ use crate::imports::*; - -// pub struct Config { -// network: Network, -// } - pub struct Settings { #[allow(dead_code)] runtime: Runtime, @@ -93,11 +88,11 @@ impl ModuleT for Settings { ui.horizontal_wrapped(|ui|{ KaspadNodeKind::iter().for_each(|node_kind| { #[cfg(not(target_arch = "wasm32"))] { - if !self.settings.developer_mode && matches!(*node_kind,KaspadNodeKind::IntegratedInProc|KaspadNodeKind::ExternalAsDaemon) { + if !core.settings.developer.enable_experimental_features() && matches!(*node_kind,KaspadNodeKind::IntegratedInProc|KaspadNodeKind::ExternalAsDaemon) { return; } } - ui.radio_value(&mut self.settings.node.node_kind, *node_kind, node_kind.to_string()); + ui.radio_value(&mut self.settings.node.node_kind, *node_kind, node_kind.to_string()).on_hover_text_at_pointer(node_kind.describe()); }); }); @@ -239,18 +234,13 @@ impl ModuleT for Settings { if let Some(restart) = self.settings.node.compare(&core.settings.node) { ui.add_space(16.); - 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 { - // println!("NODE INTERFACE UPDATE: {:?}", self.settings.node); self.runtime.kaspa_service().update_services(&self.settings.node); - // println!("TODO - restart"); } }, Confirm::Nack => { @@ -258,21 +248,118 @@ impl ModuleT for Settings { } } } - ui.separator(); } } + CollapsingHeader::new("Plugins") + .default_open(true) + .show(ui, |ui| { + // let enable_plugins = self.settings.enable_plugins; + // if ui.checkbox(&mut self.settings.enable_plugins, i18n("Enable Plugins")).changed() { + // if self.settings.enable_plugins { + // self.runtime.plugin_manager_service().start_plugins(&self.settings).await.unwrap(); + // } else { + // self.runtime.plugin_manager_service().terminate_plugins(); + // } + // } + + if self.settings.enable_plugins { + + let plugins = runtime().plugin_manager_service().plugins(); + for plugin in plugins.iter() { + let plugin_name = plugin.name(); + let mut plugin_enabled = runtime().plugin_manager_service().is_enabled(plugin); + ui.collapsable(plugin_name, true, |ui,state|{ + if ui.add(Label::new(plugin.name()).sense(Sense::click())).clicked() { + *state = !*state; + } + ui.add_space(8.); + if ui.checkbox(&mut plugin_enabled,"Enable").changed() { + runtime().plugin_manager_service().enable(plugin, plugin_enabled); + } + }, |ui|{ + ui.vertical(|ui| { + // ui.set_max_width(340.); + + // ui.separator(); + plugin.render(ui); + // ui.label(plugin_name); + // ui.add(Separator::default().horizontal().); + + }); + + }); + // CollapsingHeader::new(plugin_name) + // .default_open(true) + // .show(ui, |ui| { + // }); + + // ui.horizontal(|ui|{ + // ui.checkbox(&mut runtime().plugin_manager_service().plugin_settings_mut().get_mut(plugin_name).unwrap().enabled, plugin_name); + // ui.label(plugin.description()); + // }); + } + } + + }); // ---------------------------- CollapsingHeader::new("Advanced") .default_open(false) .show(ui, |ui| { ui.vertical(|ui|{ - ui.checkbox(&mut self.settings.developer_mode, i18n("Developer Mode")); - ui.label("Developer mode enables experimental features"); + // ui.set_max_width(340.); + ui.checkbox(&mut self.settings.developer.enable, i18n("Developer Mode")); + ui.label("Developer mode enables advanced and experimental features"); + }); + ui.horizontal(|ui|{ + if self.settings.developer.enable { + ui.checkbox( + &mut self.settings.developer.enable_experimental_features, + i18n("Enable Experimental Features") + ).on_hover_text_at_pointer( + i18n("Enables features currently in development") + ); + + ui.checkbox( + &mut self.settings.developer.disable_password_restrictions, + i18n("Disable Password Score Restrictions") + ).on_hover_text_at_pointer( + i18n("Removes security restrictions, allows for single-letter passwords") + ); + ui.checkbox( + &mut self.settings.developer.enable_screen_capture, + i18n("Screen Capture") + ).on_hover_text_at_pointer( + i18n("Allows you to take screenshots from within the application") + ); + } + }); + + // ui.separator(); + + if self.settings.developer != core.settings.developer { + ui.add_space(16.); + if let Some(response) = ui.confirm_medium_apply_cancel(Align::Max) { + match response { + Confirm::Ack => { + core.settings.developer = self.settings.developer.clone(); + core.settings.store_sync().unwrap(); + }, + Confirm::Nack => { + self.settings.developer = core.settings.developer.clone(); + } + } + } + ui.separator(); + } + + // ui.add_space(16.); + ui.vertical(|ui|{ + ui.set_max_width(340.); ui.separator(); if ui.medium_button("Reset Settings").clicked() { let settings = crate::settings::Settings::default(); @@ -282,6 +369,8 @@ impl ModuleT for Settings { } }); + + }); // if ui.button("Test Toast").clicked() { diff --git a/core/src/modules/settings/native.rs b/core/src/modules/settings/native.rs new file mode 100644 index 0000000..e69de29 diff --git a/core/src/modules/wallet_create.rs b/core/src/modules/wallet_create.rs index 85ad4a8..263bcb1 100644 --- a/core/src/modules/wallet_create.rs +++ b/core/src/modules/wallet_create.rs @@ -512,7 +512,7 @@ impl ModuleT for WalletCreate { if score < 80.0 { ui.label(""); ui.label(RichText::new(i18n("Password is too weak")).color(egui::Color32::from_rgb(255, 120, 120))); - if !core.settings.developer_mode { + if !core.settings.developer.disable_password_restrictions() { submit = false; ui.label(RichText::new(i18n("Please create a stronger password")).color(egui::Color32::from_rgb(255, 120, 120))); } @@ -535,7 +535,7 @@ impl ModuleT for WalletCreate { } }) .with_footer(|this,ui| { - let is_weak = !core.settings.developer_mode && this.context.wallet_secret_score.unwrap_or_default() < 80.0; + let is_weak = !core.settings.developer.disable_password_restrictions() && this.context.wallet_secret_score.unwrap_or_default() < 80.0; let enabled = this.context.wallet_secret == this.context.wallet_secret_confirm && this.context.wallet_secret.is_not_empty(); if ui.large_button_enabled(enabled && !is_weak, "Continue").clicked() { this.state = State::PaymentSecret; @@ -639,7 +639,7 @@ impl ModuleT for WalletCreate { if score < 80.0 { ui.label(""); ui.label(RichText::new(i18n("Passphrase is too weak")).color(egui::Color32::from_rgb(255, 120, 120))); - if !core.settings.developer_mode { + if !core.settings.developer.disable_password_restrictions() { submit = false; ui.label(RichText::new(i18n("Please create a stronger passphrase")).color(egui::Color32::from_rgb(255, 120, 120))); } @@ -664,7 +664,7 @@ impl ModuleT for WalletCreate { }) .with_footer(|this,ui| { if this.context.enable_payment_secret { - let is_weak = !core.settings.developer_mode && this.context.payment_secret_score.unwrap_or_default() < 80.0; + let is_weak = !core.settings.developer.disable_password_restrictions() && this.context.payment_secret_score.unwrap_or_default() < 80.0; let enabled = this.context.wallet_secret == this.context.wallet_secret_confirm && this.context.wallet_secret.is_not_empty(); if ui.large_button_enabled(enabled && !is_weak, "Continue").clicked() { this.state = State::CreateWalletConfirm; diff --git a/core/src/modules/welcome.rs b/core/src/modules/welcome.rs index c8c10b4..969fc73 100644 --- a/core/src/modules/welcome.rs +++ b/core/src/modules/welcome.rs @@ -60,8 +60,8 @@ impl ModuleT for Welcome { KaspadNodeKind::IntegratedAsDaemon, // KaspadNodeKind::ExternalAsDaemon, // KaspadNodeKind::IntegratedInProc, - ].iter().for_each(|node| { - ui.radio_value(&mut self.settings.node.node_kind, *node, node.describe()); + ].iter().for_each(|node_kind| { + ui.radio_value(&mut self.settings.node.node_kind, *node_kind, node_kind.to_string()).on_hover_text_at_pointer(node_kind.describe()); }); }); }); diff --git a/core/src/primitives/account.rs b/core/src/primitives/account.rs index 105caf4..138190b 100644 --- a/core/src/primitives/account.rs +++ b/core/src/primitives/account.rs @@ -33,7 +33,6 @@ struct Inner { // runtime: Arc, id: AccountId, balance: Mutex>, - utxo_sizes: Mutex>, descriptor: Mutex, context: Mutex>>, transactions: Mutex, @@ -54,7 +53,6 @@ impl Inner { Self { id: *descriptor.account_id(), balance: Mutex::new(None), - utxo_sizes: Mutex::new(None), descriptor: Mutex::new(descriptor), context: Mutex::new(context), transactions: Mutex::new(TransactionCollection::default()), @@ -103,10 +101,6 @@ impl Account { self.inner.balance.lock().unwrap().clone() } - pub fn utxo_sizes(&self) -> Option<(usize, usize)> { - *self.inner.utxo_sizes.lock().unwrap() - } - // pub fn address(&self) -> Result { // self.inner.context.lock().unwrap().receive_address // Ok(self.inner.runtime.receive_address()?.into()) @@ -121,14 +115,8 @@ impl Account { *self.inner.descriptor.lock().unwrap() = descriptor; } - pub fn update_balance( - &self, - balance: Option, - mature_utxo_size: usize, - pending_utxo_size: usize, - ) -> Result<()> { + pub fn update_balance(&self, balance: Option) -> Result<()> { *self.inner.balance.lock().unwrap() = balance; - *self.inner.utxo_sizes.lock().unwrap() = Some((mature_utxo_size, pending_utxo_size)); Ok(()) } diff --git a/core/src/primitives/transaction.rs b/core/src/primitives/transaction.rs index a0d90a2..2d3637d 100644 --- a/core/src/primitives/transaction.rs +++ b/core/src/primitives/transaction.rs @@ -1,5 +1,4 @@ use crate::imports::*; -use egui::collapsing_header::CollapsingState; use kaspa_consensus_core::tx::{TransactionInput, TransactionOutpoint, TransactionOutput}; use kaspa_wallet_core::storage::{ transaction::{TransactionData, UtxoRecord}, @@ -129,6 +128,11 @@ impl Transaction { _include_utxos: bool, largest: Option, ) { + let ppp = ui.ctx().pixels_per_point(); + // println!("ppp: {}", ppp); + let width = ui.available_width() / ppp; + // println!("width: {}", width); + let Context { record, maturity } = &*self.context(); // let record = context.record; // self.record(); // let maturity = context.maturity; @@ -142,7 +146,10 @@ impl Transaction { let ps2k = |sompi| padded_sompi_to_kaspa_string_with_suffix(sompi, &network_type, padding); let s2k = |sompi| sompi_to_kaspa_string_with_suffix(sompi, &network_type); - let block_daa_score = format!("@{} DAA", record.block_daa_score().separated_string()); + let timestamp = record + .unixtime_as_locale_string() + .unwrap_or_else(|| format!("@{} DAA", record.block_daa_score().separated_string())); + let block_daa_score = record.block_daa_score().separated_string(); let transaction_id = record.id().to_string(); // let short_id = transaction_id.chars().take(10).collect::() + "..."; // let suffix = kaspa_suffix(&network_type); @@ -167,6 +174,10 @@ impl Transaction { utxo_entries, aggregate_input_value, } + | TransactionData::Stasis { + utxo_entries, + aggregate_input_value, + } | TransactionData::Incoming { utxo_entries, aggregate_input_value, @@ -176,7 +187,7 @@ impl Transaction { aggregate_input_value, } => { let aggregate_input_value = ps2k(*aggregate_input_value); - let mut job = LayoutJobBuilder::new(8.0, Some(font_id_header.clone())) + let mut job = LayoutJobBuilder::new(width, 8.0, Some(font_id_header.clone())) .with_icon_font(icon_font_id); job = job.icon( egui_phosphor::light::ARROW_SQUARE_RIGHT, @@ -196,10 +207,10 @@ impl Transaction { } job = job - .text(block_daa_score.as_str(), default_color) + .text(timestamp.as_str(), default_color) .text(&aggregate_input_value, TransactionType::Incoming.as_color()); - // ui.collapsable(&transaction_id, false, |ui,state| { + // ui.LayoutJobBuilder::new(width,8.0(&transaction_id, false, |ui,state| { // ui.horizontal( |ui| { // let icon = RichText::new(egui_phosphor::light::ARROW_SQUARE_RIGHT).color(TransactionType::Incoming.as_color()); @@ -239,10 +250,17 @@ impl Transaction { .icon(paint_header_icon) .default_open(false) .show(ui, |ui| { - let text = LayoutJobBuilder::new(8.0, Some(font_id_content.clone())).text( - &format!("Transaction id: {}", shorten(&transaction_id)), - default_color, - ); + let width = 32.; //ui.available_width() / ppp; + + let text = LayoutJobBuilder::new(width, 8.0, Some(font_id_content.clone())) + .text( + &format!("Transaction id: {}", shorten(&transaction_id)), + default_color, + ); + ui.label(text); + + let text = LayoutJobBuilder::new(width, 8.0, Some(font_id_content.clone())) + .text(&format!("DAA: {}", block_daa_score), default_color); ui.label(text); utxo_entries.iter().for_each(|utxo_entry| { @@ -259,13 +277,15 @@ impl Transaction { .map(|addr| addr.to_string()) .unwrap_or_else(|| "n/a".to_string()); - let text = LayoutJobBuilder::new(8.0, Some(font_id_content.clone())) - .text(&address, default_color); + let text = + LayoutJobBuilder::new(width, 8.0, Some(font_id_content.clone())) + .text(&address, default_color); ui.label(text); // ui.label(address); if *is_coinbase { let text = LayoutJobBuilder::new( + width, 8.0, Some(font_id_content.clone()), ) @@ -274,6 +294,7 @@ impl Transaction { // ui.label(format!("{} {amount} {suffix} COINBASE UTXO", "")); } else { let text = LayoutJobBuilder::new( + width, 8.0, Some(font_id_content.clone()), ) @@ -281,11 +302,12 @@ impl Transaction { ui.label(text); } - let text = LayoutJobBuilder::new(8.0, Some(font_id_content.clone())) - .text( - &format!("Script: {}", script_public_key.script_as_hex()), - default_color, - ); + let text = + LayoutJobBuilder::new(width, 8.0, Some(font_id_content.clone())) + .text( + &format!("Script: {}", script_public_key.script_as_hex()), + default_color, + ); ui.label(text); // let job = job @@ -309,10 +331,10 @@ impl Transaction { // }); let job = if let Some(payment_value) = payment_value { - let mut job = LayoutJobBuilder::new(8.0, Some(font_id_header.clone())) + let mut job = LayoutJobBuilder::new(width, 8.0, Some(font_id_header.clone())) .with_icon_font(icon_font_id); - // LayoutJobBuilder::new(8.0, Some(font_id.clone())) + // LayoutJobBuilder::new(width,8.0, Some(font_id.clone())) job = job // .text("SEND", TransactionType::Outgoing.as_color()) @@ -320,7 +342,7 @@ impl Transaction { egui_phosphor::light::ARROW_SQUARE_LEFT, TransactionType::Outgoing.as_color(), ) - .text(block_daa_score.as_str(), default_color) + .text(timestamp.as_str(), default_color) // .text(short_id.as_str(), default_color) .text( &ps2k(*payment_value + *fees), @@ -336,7 +358,7 @@ impl Transaction { // transaction.inputs.len(), // transaction.outputs.len(), } else { - LayoutJobBuilder::new(16.0, Some(font_id_header.clone())) + LayoutJobBuilder::new(width, 16.0, Some(font_id_header.clone())) .text("Sweep:", default_color) .text(&sompi_to_kaspa_string(*aggregate_input_value), strong_color) .text("Fees:", default_color) @@ -413,13 +435,19 @@ impl Transaction { }); } collapsing_header.show(ui, |ui| { - let text = LayoutJobBuilder::new(8.0, Some(font_id_content.clone())).text( - &format!("Transaction id: {}", shorten(&transaction_id)), - default_color, - ); + let width = ui.available_width() - 64.0; + + let text = LayoutJobBuilder::new(width, 8.0, Some(font_id_content.clone())) + .text( + &format!("Transaction id: {}", shorten(&transaction_id)), + default_color, + ); + ui.label(text); + let text = LayoutJobBuilder::new(width, 8.0, Some(font_id_content.clone())) + .text(&format!("DAA: {}", block_daa_score), default_color); ui.label(text); - let text = LayoutJobBuilder::new(8.0, Some(font_id_content.clone())) + let text = LayoutJobBuilder::new(width, 8.0, Some(font_id_content.clone())) .text("Fees:", default_color) .text( &sompi_to_kaspa_string(*fees), @@ -437,10 +465,11 @@ impl Transaction { // transaction.inputs.len(), // transaction.outputs.len(), - let text = LayoutJobBuilder::new(16.0, Some(font_id_content.clone())).text( - &format!("{} UTXO inputs", transaction.inputs.len()), - default_color, - ); + let text = LayoutJobBuilder::new(width, 16.0, Some(font_id_content.clone())) + .text( + &format!("{} UTXO inputs", transaction.inputs.len()), + default_color, + ); ui.label(text); for input in transaction.inputs.iter() { @@ -464,10 +493,11 @@ impl Transaction { ui.label(text); } - let text = LayoutJobBuilder::new(16.0, Some(font_id_content.clone())).text( - &format!("{} UTXO outputs:", transaction.outputs.len()), - default_color, - ); + let text = LayoutJobBuilder::new(width, 16.0, Some(font_id_content.clone())) + .text( + &format!("{} UTXO outputs:", transaction.outputs.len()), + default_color, + ); ui.label(text); for output in transaction.outputs.iter() { @@ -544,8 +574,9 @@ pub fn padded_sompi_to_kaspa_string_with_suffix( } pub fn shorten(s: impl Into) -> String { - let s: String = s.into(); - s.chars().take(10).collect::() + "..." + s.into() + // let s: String = s.into(); + // s.chars().take(10).collect::() + "..." } pub fn paint_header_icon(ui: &mut Ui, openness: f32, response: &Response) { @@ -573,45 +604,3 @@ pub fn paint_header_icon(ui: &mut Ui, openness: f32, response: &Response) { Stroke::NONE, )); } - -pub trait CollapsingExtension { - // fn collapsing(&mut self, id: impl Into) -> CollapsingState; - fn collapsable( - &mut self, - id: impl Into, - default_open: bool, - heading: impl FnOnce(&mut Ui, &mut bool) -> HeaderRet, - body: impl FnOnce(&mut Ui) -> BodyRet, - ); -} - -impl CollapsingExtension for Ui { - fn collapsable( - &mut self, - id: impl Into, - default_open: bool, - heading: impl FnOnce(&mut Ui, &mut bool) -> HeaderRet, - body: impl FnOnce(&mut Ui) -> BodyRet, - ) { - let id: String = id.into(); - let id = self.make_persistent_id(id); - let previous_state = CollapsingState::load(self.ctx(), id) - .map(|state| state.is_open()) - .unwrap_or_default(); - let mut state = previous_state; - let header = CollapsingState::load_with_default_open(self.ctx(), id, default_open); - header - .show_header(self, |ui| heading(ui, &mut state)) - .body(body); - - // if selected != self.selected { - if state != previous_state { - if let Some(mut state) = CollapsingState::load(self.ctx(), id) { - // println!("CLICK"); - state.toggle(self); - state.store(self.ctx()); - // state.mark_changed(); - } - } - } -} diff --git a/core/src/prompt.rs b/core/src/prompt.rs deleted file mode 100644 index 4887aed..0000000 --- a/core/src/prompt.rs +++ /dev/null @@ -1,165 +0,0 @@ -use crate::imports::*; -pub use futures::{future::FutureExt, select, Future}; - -trait BindingT: Send + Sync + 'static { - fn render(&self, ui: &mut Ui) -> bool; -} - -pub struct Binding -where - FnRender: Fn(&mut Ui) -> Option + Send + Sync + 'static, - FnHandler: Fn(V) + Send + Sync + 'static, -{ - render: FnRender, - handler: FnHandler, -} - -impl BindingT for Binding -where - FnRender: Fn(&mut Ui) -> Option + Send + Sync + 'static, - FnHandler: Fn(V) + Send + Sync + 'static, - V: 'static, -{ - fn render(&self, ui: &mut Ui) -> bool { - if let Some(resp) = (self.render)(ui) { - (self.handler)(resp); - true - } else { - false - } - } -} - -#[derive(Default)] -pub struct Prompt { - secret: String, - callback: Option>, - binding: Option>, -} - -impl Prompt { - pub fn new() -> Self { - Self { - secret: String::new(), - callback: None, - binding: None, - } - } - - pub fn with_secret(&mut self, callback: impl Fn(Secret) + Send + Sync + 'static) { - self.callback = Some(Arc::new(callback)); - } - - pub fn cascade( - &mut self, - render: impl Fn(&mut Ui) -> Option + Send + Sync + 'static, - handler: impl Fn(V) + Send + Sync + 'static, - ) where - FnRender: Fn(&mut Ui) -> Option + Send + Sync + 'static, - FnHandler: Fn(V) + Send + Sync + 'static, - V: 'static, - { - let binding = Binding { render, handler }; - - let binding: Arc = Arc::new(binding); - - self.binding = Some(binding); - } - - pub fn render(&mut self, ctx: &egui::Context) -> bool { - if let Some(binding) = &self.binding.clone() { - egui::Window::new("Please enter your password") - .collapsible(false) - .show(ctx, |ui| { - if binding.render(ui) { - self.binding = None - } - }); - false - } else if self.callback.is_some() { - egui::Window::new("Please enter your password") - .collapsible(false) - .show(ctx, |ui| { - if let Some(secret) = self.render_secret_request(ui) { - (self.callback.take().unwrap())(secret.clone()); - } - }); - - true - } else { - false - } - } - - fn render_secret_request(&mut self, ui: &mut Ui) -> Option { - let size = egui::Vec2::new(200_f32, 40_f32); - - let message = Some("Please enter you secret TEST:".to_string()); - if let Some(message) = &message { - ui.label(" "); - ui.label(egui::RichText::new(message).color(egui::Color32::from_rgb(255, 128, 128))); - ui.label(" "); - } - - ui.label(" "); - ui.label(" "); - - ui.add_sized( - size, - egui::TextEdit::singleline(&mut self.secret) - .hint_text("Enter Password...") - .password(true) - .vertical_align(egui::Align::Center), - ); - - // ui.add_sized(egui::Vec2::new(120_f32,40_f32), egui::Button::new("Testing 123")); - - if ui.add_sized(size, egui::Button::new("Unlock")).clicked() { - println!("secret: {}", self.secret); - let secret = kaspa_wallet_core::secret::Secret::new(self.secret.as_bytes().to_vec()); - self.secret.zeroize(); - Some(secret) - } else { - None - } - } -} - -pub fn with_secret(callback: impl Fn(Secret) + Send + Sync + 'static) { - prompt().with_secret(callback); - // self.callback = Some(Arc::new(callback)); -} - -// pub fn cascade( -pub fn cascade( - render: impl Fn(&mut Ui) -> Option + Send + Sync + 'static, - handler: impl Fn(V) + Send + Sync + 'static, -) where - FnRender: Fn(&mut Ui) -> Option + Send + Sync + 'static, - FnHandler: Fn(V) + Send + Sync + 'static, - V: 'static, -{ - prompt().cascade::(render, handler); -} - -// pub fn cascade( -// render: impl Fn(&mut Ui) -> Option + Send + Sync + 'static, -// handler: impl Fn(V) + Send + Sync + 'static, -// ) where -// FnRender: Fn(&mut Ui) -> Option + Send + Sync + 'static, -// FnHandler: Fn(V) + Send + Sync + 'static, -// V: 'static, -// { -// prompt().cascade::(render, handler); -// } - -static mut PROMPT: Option = None; - -pub fn prompt() -> &'static mut Prompt { - unsafe { - if PROMPT.is_none() { - PROMPT = Some(Prompt::new()); - } - PROMPT.as_mut().unwrap() - } -} diff --git a/core/src/channel.rs b/core/src/runtime/channel.rs similarity index 100% rename from core/src/channel.rs rename to core/src/runtime/channel.rs diff --git a/core/src/runtime/device.rs b/core/src/runtime/device.rs new file mode 100644 index 0000000..483a767 --- /dev/null +++ b/core/src/runtime/device.rs @@ -0,0 +1,50 @@ +use crate::imports::*; + +#[derive(Default)] +struct Inner { + pub is_portrait: bool, + pub is_mobile: bool, +} + +#[derive(Default)] +pub struct Device { + inner: Arc>, +} + +impl Device { + pub fn new() -> Self { + Self { + inner: Arc::new(Mutex::new(Inner { + is_portrait: false, + is_mobile: false, + })), + } + } + + fn inner(&self) -> MutexGuard<'_, Inner> { + self.inner.lock().unwrap() + } + + pub fn is_portrait(&self) -> bool { + self.inner().is_portrait + } + + pub fn is_mobile(&self) -> bool { + self.inner().is_mobile + } + + pub fn toggle_portrait(&self) { + let mut inner = self.inner(); + inner.is_portrait = !inner.is_portrait; + } + + pub fn toggle_mobile(&self) { + let mut inner = self.inner(); + inner.is_mobile = !inner.is_mobile; + } + + pub fn is_singular_layout(&self) -> bool { + let inner = self.inner(); + inner.is_mobile || inner.is_portrait + } +} diff --git a/core/src/runtime/mod.rs b/core/src/runtime/mod.rs index 13cc592..590d1dc 100644 --- a/core/src/runtime/mod.rs +++ b/core/src/runtime/mod.rs @@ -9,14 +9,17 @@ cfg_if! { } } +pub mod channel; +pub mod device; pub mod payload; pub mod plugins; pub mod services; pub mod system; -pub use payload::Payload; -pub use services::Service; use services::*; use system::*; +pub use device::Device; +pub use payload::Payload; +pub use services::Service; pub struct Inner { // services: Mutex>>, @@ -32,6 +35,7 @@ pub struct Inner { is_running: Arc, start_time: Instant, system: Option, + device: Device, } /// Runtime is a core component of the Kaspa NG application responsible for @@ -87,6 +91,7 @@ impl Runtime { start_time: Instant::now(), system: Some(system), + device: Device::default(), }), }; @@ -103,6 +108,10 @@ impl Runtime { &self.inner.system } + pub fn device(&self) -> &Device { + &self.inner.device + } + pub fn start_services(&self) { let services = self.services(); for service in services { @@ -122,10 +131,10 @@ impl Runtime { pub async fn join_services(&self) { // for service in self.services() { - // let name = service.name(); - // println!("⚡ {name}"); - // service.join().await.expect("service join failure"); - // println!("💀 {name}"); + // let name = service.name(); + // println!("⚡ {name}"); + // service.join().await.expect("service join failure"); + // println!("💀 {name}"); // } let futures = self diff --git a/core/src/runtime/plugins/market_monitor/mod.rs b/core/src/runtime/plugins/market_monitor/mod.rs index 605ca10..4978b11 100644 --- a/core/src/runtime/plugins/market_monitor/mod.rs +++ b/core/src/runtime/plugins/market_monitor/mod.rs @@ -103,11 +103,12 @@ impl MarketMonitorPlugin { task_ctl: Channel::oneshot(), is_enabled: AtomicBool::new(false), provider: Mutex::new(MarketDataProvider::default()), - currencies: Mutex::new(None), + // ------ + currencies: Mutex::new(Some(vec!["usd".to_string()])), + // ------ + // currencies: Mutex::new(None), available_currencies: Mutex::new(None), market_price_list: Mutex::new(None), - // rpc_api: Mutex::new(None), - // peer_info: Mutex::new(None), } } @@ -154,7 +155,7 @@ impl Plugin for MarketMonitorPlugin { } fn name(&self) -> &'static str { - "market-monitor" + "Market Monitor" } fn load(&self, settings: serde_json::Value) -> Result<()> { @@ -189,6 +190,7 @@ impl Plugin for MarketMonitorPlugin { loop { select! { _ = interval.next().fuse() => { + println!("Updating market price list..."); this.update_market_price_list().await?; }, @@ -225,5 +227,7 @@ impl Plugin for MarketMonitorPlugin { fn render(&self, ui: &mut Ui) { ui.label("Market Monitor"); + + ui.label("TODO - Add Market Monitor Settings"); } } diff --git a/core/src/runtime/services/plugin_manager.rs b/core/src/runtime/services/plugin_manager.rs index 2821c04..d7176e5 100644 --- a/core/src/runtime/services/plugin_manager.rs +++ b/core/src/runtime/services/plugin_manager.rs @@ -1,13 +1,9 @@ use crate::imports::*; -// pub use futures::{future::FutureExt, select, Future}; -// use kaspa_rpc_core::RpcPeerInfo; - -// pub const PEER_POLLING_INTERVAL: usize = 1; // 1 sec - -// use crate::runtime::plugins::Plugin; use crate::runtime::plugins::*; pub enum PluginManagerEvents { + Start(Arc), + Stop(Arc), Exit, } @@ -15,13 +11,9 @@ pub struct PluginManagerService { pub application_events: ApplicationEventsChannel, pub service_events: Channel, pub task_ctl: Channel<()>, - // pub rpc_api: Mutex>>, - // pub peer_info: Mutex>>>, pub plugins: Arc>>, - // pub plugins_running : HashMap, pub plugin_settings: Mutex, - pub is_running_map: HashMap, - // pub is_enabled_map: HashMap, + pub running_plugins: HashMap, } impl PluginManagerService { @@ -30,6 +22,11 @@ impl PluginManagerService { application_events.clone(), ))]; + let running_plugins = plugins + .iter() + .map(|plugin| (plugin.ident().to_string(), AtomicBool::new(false))) + .collect::>(); + // let plugins: Vec = plugins // .into_iter() // .map(|plugin| PluginContainer::new(plugin)) @@ -41,16 +38,16 @@ impl PluginManagerService { task_ctl: Channel::oneshot(), plugins: Arc::new(plugins), plugin_settings: Mutex::new(settings.plugins.clone().unwrap_or_default()), - is_running_map: HashMap::new(), + running_plugins, // is_enabled_map: HashMap::new(), } } pub fn is_running(&self, plugin: &Arc) -> bool { - self.is_running_map + self.running_plugins .get(plugin.ident()) .map(|is_running| is_running.load(Ordering::Relaxed)) - .unwrap_or(false) + .expect("is_running(): no such plugin") } pub fn is_enabled(&self, plugin: &Arc) -> bool { @@ -62,6 +59,19 @@ impl PluginManagerService { .unwrap_or(false) } + pub fn enable(&self, plugin: &Arc, enabled: bool) { + if let Some(plugin_settings) = self.plugin_settings.lock().unwrap().get_mut(plugin.ident()) + { + plugin_settings.enabled = enabled; + } + + // - TODO - START PLUGIN IF NOT RUNNING + // - TODO - START PLUGIN IF NOT RUNNING + // - TODO - START PLUGIN IF NOT RUNNING + // - TODO - START PLUGIN IF NOT RUNNING + // - TODO - START PLUGIN IF NOT RUNNING + } + pub fn plugins(&self) -> &Arc>> { &self.plugins } @@ -158,11 +168,42 @@ impl Service for PluginManagerService { } async fn spawn(self: Arc) -> Result<()> { + let this = self.clone(); + let _application_events_sender = self.application_events.sender.clone(); + + loop { + select! { + + msg = this.service_events.recv().fuse() => { + if let Ok(event) = msg { + match event { + PluginManagerEvents::Start(_plugin) => { + } + PluginManagerEvents::Stop(_plugin) => { + } + PluginManagerEvents::Exit => { + break; + } + } + } else { + break; + } + } + } + } + + this.task_ctl.send(()).await.unwrap(); + Ok(()) } fn terminate(self: Arc) { self.terminate_plugins(); + + self.service_events + .sender + .try_send(PluginManagerEvents::Exit) + .unwrap(); } async fn join(self: Arc) -> Result<()> { diff --git a/core/src/settings.rs b/core/src/settings.rs index 4f04a87..bb75920 100644 --- a/core/src/settings.rs +++ b/core/src/settings.rs @@ -30,7 +30,7 @@ cfg_if! { match self { KaspadNodeKind::Disable => write!(f, "Disabled"), KaspadNodeKind::Remote => write!(f, "Remote"), - KaspadNodeKind::IntegratedInProc => write!(f, "Integrated"), + KaspadNodeKind::IntegratedInProc => write!(f, "Integrated Node"), KaspadNodeKind::IntegratedAsDaemon => write!(f, "Integrated Daemon"), KaspadNodeKind::ExternalAsDaemon => write!(f, "External Daemon"), } @@ -68,14 +68,14 @@ impl KaspadNodeKind { pub fn describe(&self) -> &str { match self { - KaspadNodeKind::Disable => i18n("Disable"), - KaspadNodeKind::Remote => i18n("Connect to Remote"), + KaspadNodeKind::Disable => i18n("Disables node connectivity (Offline Mode)."), + KaspadNodeKind::Remote => i18n("Connects to a Remote Rusty Kaspa Node via wRPC."), #[cfg(not(target_arch = "wasm32"))] - KaspadNodeKind::IntegratedInProc => i18n("Integrated Node"), + KaspadNodeKind::IntegratedInProc => i18n("The node runs as a part of the Kaspa-NG application process.\n This reduces communication overhead (experimental)."), #[cfg(not(target_arch = "wasm32"))] - KaspadNodeKind::IntegratedAsDaemon => i18n("Integrated Daemon"), + KaspadNodeKind::IntegratedAsDaemon => i18n("The node is spawned as a child daemon process (recommended)."), #[cfg(not(target_arch = "wasm32"))] - KaspadNodeKind::ExternalAsDaemon => i18n("External Daemon"), + KaspadNodeKind::ExternalAsDaemon => i18n("A binary at another location is spawned a child process (experimental, for development purposes only)."), } } @@ -342,6 +342,7 @@ pub struct UxSettings { // pub type PluginSettings = HashMap; #[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] pub struct PluginSettings { pub enabled: bool, //HashMap, pub settings: serde_json::Value, //HashMap, @@ -349,13 +350,37 @@ pub struct PluginSettings { pub type PluginSettingsMap = HashMap; +#[derive(Default, Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct DeveloperSettings { + pub enable: bool, + pub enable_screen_capture: bool, + pub disable_password_restrictions: bool, + pub enable_experimental_features: bool, +} + +impl DeveloperSettings { + pub fn enable_screen_capture(&self) -> bool { + self.enable && self.enable_screen_capture + } + + pub fn disable_password_restrictions(&self) -> bool { + self.enable && self.disable_password_restrictions + } + + pub fn enable_experimental_features(&self) -> bool { + self.enable && self.enable_experimental_features + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct Settings { pub initialized: bool, pub splash_screen: bool, pub version: String, - pub developer_mode: bool, + // pub developer_mode: bool, + pub developer: DeveloperSettings, pub node: NodeSettings, pub ux: UxSettings, pub language_code: String, @@ -375,12 +400,13 @@ impl Default for Settings { splash_screen: true, version: "0.0.0".to_string(), - developer_mode: false, + // developer_mode: false, + developer: DeveloperSettings::default(), node: NodeSettings::default(), ux: UxSettings::default(), language_code: "en".to_string(), theme: "Dark".to_string(), - enable_plugins: false, + enable_plugins: true, plugins: Some(PluginSettingsMap::default()), } } @@ -394,7 +420,6 @@ fn storage() -> Result { impl Settings { pub async fn store(&self) -> Result<()> { - workflow_log::log_info!("AAAA SSSSS"); let storage = storage()?; storage.ensure_dir().await?; workflow_store::fs::write_json(storage.filename(), self).await?; diff --git a/core/src/status.rs b/core/src/status.rs index 33a84c8..4d4ebdc 100644 --- a/core/src/status.rs +++ b/core/src/status.rs @@ -1,6 +1,6 @@ use crate::imports::*; -use kaspa_metrics::MetricsSnapshot; use crate::sync::SyncStatus; +use kaspa_metrics::MetricsSnapshot; enum ConnectionStatus { Connected { @@ -38,7 +38,7 @@ impl<'core> Status<'core> { } fn device(&self) -> &Device { - self.core.device() + runtime().device() } fn metrics(&self) -> &Option> { @@ -47,7 +47,6 @@ impl<'core> Status<'core> { pub fn render(&mut self, ui: &mut egui::Ui) { menu::bar(ui, |ui| { - if !self.state().is_connected() { self.render_connected_state(ui, ConnectionStatus::Disconnected); } else { @@ -86,7 +85,7 @@ impl<'core> Status<'core> { }); } - if !self.module().modal() && !self.device().is_portrait() { + if !self.module().modal() && !self.device().is_singular_layout() { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if icons() .sliders @@ -199,10 +198,9 @@ impl<'core> Status<'core> { } } - if !self.device().is_portrait() { + if !self.device().is_singular_layout() { module.status_bar(self.core, ui); } - } ConnectionStatus::Connected { @@ -233,7 +231,7 @@ impl<'core> Status<'core> { // }); // }); - if !self.device().is_portrait() { + if !self.device().is_singular_layout() { ui.separator(); self.render_peers(ui, peers); if let Some(current_daa_score) = current_daa_score { @@ -258,7 +256,7 @@ impl<'core> Status<'core> { ui.separator(); self.render_network_selector(ui); - if !self.device().is_portrait() { + if !self.device().is_singular_layout() { ui.separator(); self.render_peers(ui, peers); if let Some(status) = sync_status.as_ref() {