desktop: parse file content from argument on launch, single app instance, wallets selection and opening modals refactoring

This commit is contained in:
ardocrat 2024-09-11 17:01:05 +03:00
parent a3ed3bd234
commit dbc28205e8
19 changed files with 672 additions and 243 deletions

28
Cargo.lock generated
View file

@ -2483,6 +2483,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
[[package]]
name = "doctest-file"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562"
[[package]] [[package]]
name = "document-features" name = "document-features"
version = "0.2.8" version = "0.2.8"
@ -3833,6 +3839,7 @@ dependencies = [
"hyper 0.14.29", "hyper 0.14.29",
"hyper-tls 0.5.0", "hyper-tls 0.5.0",
"image 0.25.1", "image 0.25.1",
"interprocess",
"jni", "jni",
"lazy_static", "lazy_static",
"local-ip-address", "local-ip-address",
@ -4976,6 +4983,21 @@ dependencies = [
"syn 2.0.66", "syn 2.0.66",
] ]
[[package]]
name = "interprocess"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f4e4a06d42fab3e85ab1b419ad32b09eab58b901d40c57935ff92db3287a13"
dependencies = [
"doctest-file",
"futures-core",
"libc",
"recvmsg",
"tokio 1.38.0",
"widestring",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "intl-memoizer" name = "intl-memoizer"
version = "0.5.2" version = "0.5.2"
@ -7432,6 +7454,12 @@ dependencies = [
"rand_core 0.3.1", "rand_core 0.3.1",
] ]
[[package]]
name = "recvmsg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.1.57" version = "0.1.57"

View file

@ -119,6 +119,7 @@ eframe = { version = "0.28.1", features = ["wgpu", "glow"] }
arboard = "3.2.0" arboard = "3.2.0"
rfd = "0.14.1" rfd = "0.14.1"
dark-light = "1.1.1" dark-light = "1.1.1"
interprocess = { version = "2.2.1", features = ["tokio"] }
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13.1" android_logger = "0.13.1"

View file

@ -4,3 +4,4 @@ Exec=grim
Icon=grim Icon=grim
Type=Application Type=Application
Categories=Finance Categories=Finance
MimeType=application/x-slatepack;text/plain;

View file

@ -32,25 +32,38 @@ lazy_static! {
/// Implements ui entry point and contains platform-specific callbacks. /// Implements ui entry point and contains platform-specific callbacks.
pub struct App<Platform> { pub struct App<Platform> {
/// Platform specific callbacks handler. /// Platform specific callbacks handler.
pub(crate) platform: Platform, pub platform: Platform,
/// Main content.
/// Main ui content.
content: Content, content: Content,
/// Last window resize direction. /// Last window resize direction.
resize_direction: Option<ResizeDirection> resize_direction: Option<ResizeDirection>,
/// Flag to check if it's first draw.
first_draw: bool,
} }
impl<Platform: PlatformCallbacks> App<Platform> { impl<Platform: PlatformCallbacks> App<Platform> {
pub fn new(platform: Platform) -> Self { pub fn new(platform: Platform) -> Self {
Self { platform, content: Content::default(), resize_direction: None } Self {
platform,
content: Content::default(),
resize_direction: None,
first_draw: true,
}
} }
/// Draw application content. /// Draw application content.
pub fn ui(&mut self, ctx: &Context) { pub fn ui(&mut self, ctx: &Context) {
// Set Desktop platform context on first draw.
if self.first_draw {
if View::is_desktop() {
self.platform.set_context(ctx);
}
self.first_draw = false;
}
// Handle Esc keyboard key event and platform Back button key event. // Handle Esc keyboard key event and platform Back button key event.
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed); let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) || back_pressed { if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) {
self.content.on_back(); self.content.on_back();
if back_pressed { if back_pressed {
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed); BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
@ -59,8 +72,8 @@ impl<Platform: PlatformCallbacks> App<Platform> {
ctx.request_repaint(); ctx.request_repaint();
} }
// Handle Close event (on desktop). // Handle Close event on desktop.
if ctx.input(|i| i.viewport().close_requested()) { if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) {
if !self.content.exit_allowed { if !self.content.exit_allowed {
ctx.send_viewport_cmd(ViewportCommand::CancelClose); ctx.send_viewport_cmd(ViewportCommand::CancelClose);
Content::show_exit_modal(); Content::show_exit_modal();
@ -92,6 +105,11 @@ impl<Platform: PlatformCallbacks> App<Platform> {
} }
self.content.ui(ui, &self.platform); self.content.ui(ui, &self.platform);
} }
// Provide incoming data to wallets.
if let Some(data) = self.platform.consume_data() {
self.content.wallets.on_data(ui, Some(data), &self.platform);
}
}); });
} }

View file

@ -167,6 +167,10 @@ impl PlatformCallbacks for Android {
} }
None None
} }
fn consume_data(&mut self) -> Option<String> {
None
}
} }
lazy_static! { lazy_static! {

View file

@ -13,12 +13,13 @@
// limitations under the License. // limitations under the License.
use std::fs::File; use std::fs::File;
use std::io:: Write; use std::io::Write;
use lazy_static::lazy_static; use std::thread;
use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::thread; use parking_lot::RwLock;
use lazy_static::lazy_static;
use egui::{UserAttentionType, ViewportCommand};
use rfd::FileDialog; use rfd::FileDialog;
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
@ -28,17 +29,16 @@ use crate::gui::platform::PlatformCallbacks;
pub struct Desktop { pub struct Desktop {
/// Flag to check if camera stop is needed. /// Flag to check if camera stop is needed.
stop_camera: Arc<AtomicBool>, stop_camera: Arc<AtomicBool>,
} /// Context to repaint content and handle viewport commands.
ctx: Arc<RwLock<Option<egui::Context>>>,
impl Default for Desktop {
fn default() -> Self {
Self {
stop_camera: Arc::new(AtomicBool::new(false)),
}
}
} }
impl PlatformCallbacks for Desktop { impl PlatformCallbacks for Desktop {
fn set_context(&mut self, ctx: &egui::Context) {
let mut w_ctx = self.ctx.write();
*w_ctx = Some(ctx.clone());
}
fn show_keyboard(&self) {} fn show_keyboard(&self) {}
fn hide_keyboard(&self) {} fn hide_keyboard(&self) {}
@ -119,9 +119,61 @@ impl PlatformCallbacks for Desktop {
fn picked_file(&self) -> Option<String> { fn picked_file(&self) -> Option<String> {
None None
} }
fn consume_data(&mut self) -> Option<String> {
let has_data = {
let r_data = PASSED_DATA.read();
r_data.is_some()
};
if has_data {
// Reset window state.
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
ctx.send_viewport_cmd(
ViewportCommand::RequestUserAttention(UserAttentionType::Reset)
);
}
// Clear data.
let mut w_data = PASSED_DATA.write();
let data = w_data.clone();
*w_data = None;
return data;
}
None
}
} }
impl Desktop { impl Desktop {
/// Create new instance with provided extra data from app opening.
pub fn new(data: Option<String>) -> Self {
let mut w_data = PASSED_DATA.write();
*w_data = data;
Self {
stop_camera: Arc::new(AtomicBool::new(false)),
ctx: Arc::new(RwLock::new(None)),
}
}
/// Handle data passed to application.
pub fn on_data(&self, data: String) {
let mut w_data = PASSED_DATA.write();
*w_data = Some(data);
// Bring focus on window.
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
ctx.send_viewport_cmd(ViewportCommand::Visible(true));
ctx.send_viewport_cmd(ViewportCommand::Minimized(false));
ctx.send_viewport_cmd(
ViewportCommand::RequestUserAttention(UserAttentionType::Informational)
);
ctx.send_viewport_cmd(ViewportCommand::Focus);
ctx.request_repaint();
}
}
#[allow(dead_code)] #[allow(dead_code)]
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn start_camera_capture(stop_camera: Arc<AtomicBool>) { fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
@ -205,4 +257,7 @@ impl Desktop {
lazy_static! { lazy_static! {
/// Last captured image from started camera. /// Last captured image from started camera.
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None)); static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
/// Data passed from deeplink or opened file.
static ref PASSED_DATA: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
} }

View file

@ -22,6 +22,7 @@ pub mod platform;
pub mod platform; pub mod platform;
pub trait PlatformCallbacks { pub trait PlatformCallbacks {
fn set_context(&mut self, ctx: &egui::Context);
fn show_keyboard(&self); fn show_keyboard(&self);
fn hide_keyboard(&self); fn hide_keyboard(&self);
fn copy_string_to_buffer(&self, data: String); fn copy_string_to_buffer(&self, data: String);
@ -34,4 +35,5 @@ pub trait PlatformCallbacks {
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>; fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
fn pick_file(&self) -> Option<String>; fn pick_file(&self) -> Option<String>;
fn picked_file(&self) -> Option<String>; fn picked_file(&self) -> Option<String>;
fn consume_data(&mut self) -> Option<String>;
} }

View file

@ -35,7 +35,7 @@ pub struct Modal {
/// Identifier for modal. /// Identifier for modal.
pub(crate) id: &'static str, pub(crate) id: &'static str,
/// Position on the screen. /// Position on the screen.
position: ModalPosition, pub position: ModalPosition,
/// To check if it can be closed. /// To check if it can be closed.
closeable: Arc<AtomicBool>, closeable: Arc<AtomicBool>,
/// Title text /// Title text
@ -64,6 +64,12 @@ impl Modal {
self self
} }
/// Change [`Modal`] position on the screen.
pub fn change_position(position: ModalPosition) {
let mut w_state = MODAL_STATE.write();
w_state.modal.as_mut().unwrap().position = position;
}
/// Mark [`Modal`] closed. /// Mark [`Modal`] closed.
pub fn close(&self) { pub fn close(&self) {
let mut w_nav = MODAL_STATE.write(); let mut w_nav = MODAL_STATE.write();

View file

@ -83,7 +83,7 @@ impl Default for StratumSetup {
Self { Self {
wallets: WalletList::default(), wallets: WalletList::default(),
wallets_modal: WalletsModal::new(wallet_id), wallets_modal: WalletsModal::new(wallet_id, None, false),
available_ips: NodeConfig::get_ip_addrs(), available_ips: NodeConfig::get_ip_addrs(),
stratum_port_edit: port, stratum_port_edit: port,
stratum_port_available_edit: is_port_available, stratum_port_available_edit: is_port_available,
@ -111,10 +111,12 @@ impl ModalContainer for StratumSetup {
modal: &Modal, modal: &Modal,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
match modal.id { match modal.id {
WALLET_SELECTION_MODAL => self.wallets_modal.ui(ui, modal, &self.wallets, |id| { WALLET_SELECTION_MODAL => {
self.wallets_modal.ui(ui, modal, &mut self.wallets, cb, |id, _| {
NodeConfig::save_stratum_wallet_id(id); NodeConfig::save_stratum_wallet_id(id);
self.wallet_name = WalletConfig::name_by_id(id); self.wallet_name = WalletConfig::name_by_id(id);
}), })
},
STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb), STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb),
ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb), ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb),
MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb), MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb),
@ -240,7 +242,7 @@ impl StratumSetup {
/// Show wallet selection [`Modal`]. /// Show wallet selection [`Modal`].
fn show_wallets_modal(&mut self) { fn show_wallets_modal(&mut self) {
self.wallets_modal = WalletsModal::new(NodeConfig::get_stratum_wallet_id()); self.wallets_modal = WalletsModal::new(NodeConfig::get_stratum_wallet_id(), None, false);
// Show modal. // Show modal.
Modal::new(WALLET_SELECTION_MODAL) Modal::new(WALLET_SELECTION_MODAL)
.position(ModalPosition::Center) .position(ModalPosition::Center)

View file

@ -18,13 +18,14 @@ use egui::scroll_area::ScrollBarVisibility;
use crate::AppConfig; use crate::AppConfig;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_LOCK, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SPINNER, SUITCASE, WARNING_CIRCLE}; use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, TitlePanel, View}; use crate::gui::views::{Modal, Content, TitlePanel, View};
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions, TitleContentType, TitleType}; use crate::gui::views::types::{ModalContainer, ModalPosition, TitleContentType, TitleType};
use crate::gui::views::wallets::creation::WalletCreation; use crate::gui::views::wallets::creation::WalletCreation;
use crate::gui::views::wallets::modals::WalletConnectionModal; use crate::gui::views::wallets::modals::{OpenWalletModal, WalletConnectionModal, WalletsModal};
use crate::gui::views::wallets::types::WalletTabType; use crate::gui::views::wallets::types::WalletTabType;
use crate::gui::views::wallets::wallet::types::status_text;
use crate::gui::views::wallets::WalletContent; use crate::gui::views::wallets::WalletContent;
use crate::wallet::{Wallet, WalletList}; use crate::wallet::{Wallet, WalletList};
@ -33,10 +34,11 @@ pub struct WalletsContent {
/// List of wallets. /// List of wallets.
wallets: WalletList, wallets: WalletList,
/// Password to open wallet for [`Modal`]. /// Wallet selection [`Modal`] content.
pass_edit: String, wallet_selection_content: Option<WalletsModal>,
/// Flag to check if wrong password was entered at [`Modal`].
wrong_pass: bool, /// Wallet opening [`Modal`] content.
open_wallet_content: Option<OpenWalletModal>,
/// Wallet connection selection content. /// Wallet connection selection content.
conn_modal_content: Option<WalletConnectionModal>, conn_modal_content: Option<WalletConnectionModal>,
@ -54,24 +56,29 @@ pub struct WalletsContent {
} }
/// Identifier for connection selection [`Modal`]. /// Identifier for connection selection [`Modal`].
const CONNECTION_SELECTION_MODAL: &'static str = "wallets_connection_selection_modal"; const CONNECTION_SELECTION_MODAL: &'static str = "wallets_connection_selection";
/// Identifier for wallet opening [`Modal`]. /// Identifier for wallet opening [`Modal`].
const OPEN_WALLET_MODAL: &'static str = "open_wallet_modal"; const OPEN_WALLET_MODAL: &'static str = "wallets_open_wallet";
/// Identifier for wallet opening [`Modal`].
const SELECT_WALLET_MODAL: &'static str = "wallets_select_wallet";
impl Default for WalletsContent { impl Default for WalletsContent {
fn default() -> Self { fn default() -> Self {
Self { Self {
wallets: WalletList::default(), wallets: WalletList::default(),
pass_edit: "".to_string(), wallet_selection_content: None,
wrong_pass: false, open_wallet_content: None,
conn_modal_content: None, conn_modal_content: None,
wallet_content: WalletContent::default(), wallet_content: WalletContent::new(None),
creation_content: WalletCreation::default(), creation_content: WalletCreation::default(),
show_wallets_at_dual_panel: AppConfig::show_wallets_at_dual_panel(), show_wallets_at_dual_panel: AppConfig::show_wallets_at_dual_panel(),
modal_ids: vec![ modal_ids: vec![
OPEN_WALLET_MODAL, OPEN_WALLET_MODAL,
WalletCreation::NAME_PASS_MODAL, WalletCreation::NAME_PASS_MODAL,
CONNECTION_SELECTION_MODAL, CONNECTION_SELECTION_MODAL,
SELECT_WALLET_MODAL
] ]
} }
} }
@ -87,13 +94,21 @@ impl ModalContainer for WalletsContent {
modal: &Modal, modal: &Modal,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
match modal.id { match modal.id {
OPEN_WALLET_MODAL => self.open_wallet_modal_ui(ui, modal, cb), OPEN_WALLET_MODAL => {
if let Some(content) = self.open_wallet_content.as_mut() {
content.ui(ui, modal, &mut self.wallets, cb, |data| {
// Setup wallet content.
self.wallet_content = WalletContent::new(data);
});
}
},
WalletCreation::NAME_PASS_MODAL => { WalletCreation::NAME_PASS_MODAL => {
self.creation_content.name_pass_modal_ui(ui, modal, cb) self.creation_content.name_pass_modal_ui(ui, modal, cb)
}, },
CONNECTION_SELECTION_MODAL => { CONNECTION_SELECTION_MODAL => {
if let Some(content) = self.conn_modal_content.as_mut() { if let Some(content) = self.conn_modal_content.as_mut() {
content.ui(ui, modal, cb, |id| { content.ui(ui, modal, cb, |id| {
// Update wallet connection on select.
let list = self.wallets.list(); let list = self.wallets.list();
for w in list { for w in list {
if self.wallets.selected_id == Some(w.get_config().id) { if self.wallets.selected_id == Some(w.get_config().id) {
@ -103,12 +118,20 @@ impl ModalContainer for WalletsContent {
}); });
} }
} }
SELECT_WALLET_MODAL => {
if let Some(content) = self.wallet_selection_content.as_mut() {
content.ui(ui, modal, &mut self.wallets, cb, |_, data| {
self.wallet_content = WalletContent::new(data);
});
}
}
_ => {} _ => {}
} }
} }
} }
impl WalletsContent { impl WalletsContent {
/// Draw wallets content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw modal content for current ui container. // Draw modal content for current ui container.
self.current_modal_ui(ui, cb); self.current_modal_ui(ui, cb);
@ -159,7 +182,7 @@ impl WalletsContent {
// Add created wallet to list. // Add created wallet to list.
self.wallets.add(wallet); self.wallets.add(wallet);
// Reset wallet content. // Reset wallet content.
self.wallet_content = WalletContent::default(); self.wallet_content = WalletContent::new(None);
}); });
} else { } else {
let selected_id = self.wallets.selected_id.clone(); let selected_id = self.wallets.selected_id.clone();
@ -254,6 +277,56 @@ impl WalletsContent {
self.creation_content.can_go_back() self.creation_content.can_go_back()
} }
/// Handle data from deeplink or opened file.
pub fn on_data(&mut self, ui: &mut egui::Ui, data: Option<String>, cb: &dyn PlatformCallbacks) {
let wallets_size = self.wallets.list().len();
if wallets_size == 0 {
return;
}
// Close network panel on single panel mode.
if !Content::is_dual_panel_mode(ui) && Content::is_network_panel_open() {
Content::toggle_network_panel();
}
// Pass data to opened selected wallet or show wallets selection.
if self.wallets.is_selected_open() {
if wallets_size == 1 {
self.wallet_content = WalletContent::new(data);
} else {
self.show_wallet_selection_modal(data);
}
} else {
if wallets_size == 1 {
self.show_opening_modal(self.wallets.list()[0].get_config().id, data, cb);
} else {
self.show_wallet_selection_modal(data);
}
}
}
fn show_wallet_selection_modal(&mut self, data: Option<String>) {
self.wallet_selection_content = Some(WalletsModal::new(None, data, true));
// Show wallet selection modal.
Modal::new(SELECT_WALLET_MODAL)
.position(ModalPosition::Center)
.title(t!("network_settings.choose_wallet"))
.show();
}
/// Handle Back key event returning `false` when event was handled.
pub fn on_back(&mut self) -> bool {
let can_go_back = self.creation_content.can_go_back();
if can_go_back {
self.creation_content.back();
return false
} else {
if self.wallets.is_selected_open() {
self.wallets.select(None);
return false
}
}
true
}
/// Draw [`TitlePanel`] content. /// Draw [`TitlePanel`] content.
fn title_ui(&mut self, fn title_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
@ -383,8 +456,7 @@ impl WalletsContent {
// Check if wallet reopen is needed. // Check if wallet reopen is needed.
if !wallet.is_open() && wallet.reopen_needed() { if !wallet.is_open() && wallet.reopen_needed() {
wallet.set_reopen(false); wallet.set_reopen(false);
self.wallets.select(Some(wallet.get_config().id)); self.show_opening_modal(wallet.get_config().id, None, cb);
self.show_open_wallet_modal(cb);
} }
// Draw wallet list item. // Draw wallet list item.
self.wallet_item_ui(ui, wallet, cb); self.wallet_item_ui(ui, wallet, cb);
@ -420,8 +492,7 @@ impl WalletsContent {
if !wallet.is_open() { if !wallet.is_open() {
// Show button to open closed wallet. // Show button to open closed wallet.
View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || { View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || {
self.wallets.select(Some(id)); self.show_opening_modal(id, None, cb);
self.show_open_wallet_modal(cb);
}); });
// Show button to select connection if not syncing. // Show button to select connection if not syncing.
if !wallet.syncing() { if !wallet.syncing() {
@ -435,7 +506,7 @@ impl WalletsContent {
// Show button to select opened wallet. // Show button to select opened wallet.
View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || { View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || {
self.wallets.select(Some(id)); self.wallets.select(Some(id));
self.wallet_content = WalletContent::default(); self.wallet_content = WalletContent::new(None);
}); });
} }
// Show button to close opened wallet. // Show button to close opened wallet.
@ -455,7 +526,7 @@ impl WalletsContent {
ui.add_space(6.0); ui.add_space(6.0);
ui.vertical(|ui| { ui.vertical(|ui| {
ui.add_space(3.0); ui.add_space(3.0);
// Setup wallet name text. // Show wallet name text.
let name_color = if is_selected { let name_color = if is_selected {
Colors::white_or_black(true) Colors::white_or_black(true)
} else { } else {
@ -466,42 +537,11 @@ impl WalletsContent {
View::ellipsize_text(ui, config.name, 18.0, name_color); View::ellipsize_text(ui, config.name, 18.0, name_color);
}); });
// Setup wallet status text. // Show wallet status text.
let status_text = if wallet.is_open() { View::ellipsize_text(ui, status_text(wallet), 15.0, Colors::text(false));
if wallet.sync_error() {
format!("{} {}", WARNING_CIRCLE, t!("error"))
} else if wallet.is_closing() {
format!("{} {}", SPINNER, t!("wallets.closing"))
} else if wallet.is_repairing() {
let repair_progress = wallet.repairing_progress();
if repair_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.checking"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.checking"),
repair_progress)
}
} else if wallet.syncing() {
let info_progress = wallet.info_sync_progress();
if info_progress == 100 || info_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.loading"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.loading"),
info_progress)
}
} else {
format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked"))
}
} else {
format!("{} {}", FOLDER_LOCK, t!("wallets.locked"))
};
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
ui.add_space(1.0); ui.add_space(1.0);
// Setup wallet connection text. // Show wallet connection text.
let conn_text = if let Some(conn) = wallet.get_current_ext_conn() { let conn_text = if let Some(conn) = wallet.get_current_ext_conn() {
format!("{} {}", GLOBE_SIMPLE, conn.url) format!("{} {}", GLOBE_SIMPLE, conn.url)
} else { } else {
@ -525,11 +565,10 @@ impl WalletsContent {
.show(); .show();
} }
/// Show [`Modal`] to open selected wallet. /// Show [`Modal`] to select and open wallet.
fn show_open_wallet_modal(&mut self, cb: &dyn PlatformCallbacks) { fn show_opening_modal(&mut self, id: i64, data: Option<String>, cb: &dyn PlatformCallbacks) {
// Reset modal values. self.wallets.select(Some(id));
self.pass_edit = String::from(""); self.open_wallet_content = Some(OpenWalletModal::new(data));
self.wrong_pass = false;
// Show modal. // Show modal.
Modal::new(OPEN_WALLET_MODAL) Modal::new(OPEN_WALLET_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)
@ -537,100 +576,6 @@ impl WalletsContent {
.show(); .show();
cb.show_keyboard(); cb.show_keyboard();
} }
/// Draw wallet opening [`Modal`] content.
fn open_wallet_modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show password input.
let mut pass_edit_opts = TextEditOptions::new(Id::from(modal.id)).password();
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_edit_opts);
// Show information when password is empty.
if self.pass_edit.is_empty() {
self.wrong_pass = false;
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.pass_empty"))
.size(17.0)
.color(Colors::inactive_text()));
} else if self.wrong_pass {
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.wrong_pass"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Callback for button to continue.
let mut on_continue = || {
if self.pass_edit.is_empty() {
return;
}
match self.wallets.open_selected(&self.pass_edit) {
Ok(_) => {
// Clear values.
self.pass_edit = "".to_string();
self.wrong_pass = false;
// Close modal.
cb.hide_keyboard();
modal.close();
// Reset wallet content.
self.wallet_content = WalletContent::default();
}
Err(_) => self.wrong_pass = true
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
(on_continue)();
});
View::button(ui, t!("continue"), Colors::white_or_black(false), on_continue);
});
});
ui.add_space(6.0);
});
}
/// Handle Back key event.
/// Return `false` when event was handled.
pub fn on_back(&mut self) -> bool {
let can_go_back = self.creation_content.can_go_back();
if can_go_back {
self.creation_content.back();
return false
} else {
if self.wallets.is_selected_open() {
self.wallets.select(None);
return false
}
}
true
}
} }
/// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time. /// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time.

View file

@ -17,3 +17,6 @@ pub use conn::*;
mod wallets; mod wallets;
pub use wallets::*; pub use wallets::*;
mod open;
pub use open::*;

View file

@ -0,0 +1,121 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Id, RichText};
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::TextEditOptions;
use crate::wallet::WalletList;
/// Wallet opening [`Modal`] content.
pub struct OpenWalletModal {
/// Password to open wallet.
pass_edit: String,
/// Flag to check if wrong password was entered.
wrong_pass: bool,
/// Optional data to pass after wallet opening.
data: Option<String>,
}
impl OpenWalletModal {
/// Create new content instance.
pub fn new(data: Option<String>) -> Self {
Self {
pass_edit: "".to_string(),
wrong_pass: false,
data,
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
wallets: &mut WalletList,
cb: &dyn PlatformCallbacks,
mut on_continue: impl FnMut(Option<String>)) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show password input.
let mut pass_edit_opts = TextEditOptions::new(Id::from(modal.id)).password();
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_edit_opts);
// Show information when password is empty.
if self.pass_edit.is_empty() {
self.wrong_pass = false;
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.pass_empty"))
.size(17.0)
.color(Colors::inactive_text()));
} else if self.wrong_pass {
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.wrong_pass"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Callback for button to continue.
let mut on_continue = || {
if self.pass_edit.is_empty() {
return;
}
match wallets.open_selected(&self.pass_edit) {
Ok(_) => {
// Clear values.
self.pass_edit = "".to_string();
self.wrong_pass = false;
// Close modal.
cb.hide_keyboard();
modal.close();
on_continue(self.data.clone());
}
Err(_) => self.wrong_pass = true
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
(on_continue)();
});
View::button(ui, t!("continue"), Colors::white_or_black(false), on_continue);
});
});
ui.add_space(6.0);
});
}
}

View file

@ -16,29 +16,53 @@ use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, Layout, RichText, ScrollArea}; use egui::{Align, Layout, RichText, ScrollArea};
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{CHECK, CHECK_FAT, COMPUTER_TOWER, GLOBE_SIMPLE, PLUGS_CONNECTED}; use crate::gui::icons::{CHECK, CHECK_FAT, COMPUTER_TOWER, FOLDER_OPEN, GLOBE_SIMPLE, PLUGS_CONNECTED};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View}; use crate::gui::views::{Modal, View};
use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::modals::OpenWalletModal;
use crate::gui::views::wallets::wallet::types::status_text;
use crate::wallet::{Wallet, WalletList}; use crate::wallet::{Wallet, WalletList};
/// Wallet list [`Modal`] content /// Wallet list [`Modal`] content
pub struct WalletsModal { pub struct WalletsModal {
/// Selected wallet id. /// Selected wallet id.
selected: Option<i64> selected: Option<i64>,
/// Optional data to pass after wallet selection.
data: Option<String>,
/// Flag to check if wallet can be opened from the list.
can_open: bool,
/// Wallet opening content.
open_wallet_content: Option<OpenWalletModal>,
} }
impl WalletsModal { impl WalletsModal {
pub fn new(selected: Option<i64>) -> Self { /// Create new content instance.
Self { pub fn new(selected: Option<i64>, data: Option<String>, can_open: bool) -> Self {
selected, Self { selected, data, can_open, open_wallet_content: None }
}
} }
/// Draw [`Modal`] content. /// Draw content.
pub fn ui(&mut self, pub fn ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
modal: &Modal, modal: &Modal,
wallets: &WalletList, wallets: &mut WalletList,
mut on_select: impl FnMut(i64)) { cb: &dyn PlatformCallbacks,
mut on_select: impl FnMut(i64, Option<String>)) {
// Draw wallet opening content if requested.
if let Some(open_content) = self.open_wallet_content.as_mut() {
open_content.ui(ui, modal, wallets, cb, |data| {
modal.close();
if let Some(id) = self.selected {
on_select(id, data);
}
self.data = None;
});
return;
}
ui.add_space(4.0); ui.add_space(4.0);
ScrollArea::vertical() ScrollArea::vertical()
.max_height(373.0) .max_height(373.0)
@ -48,10 +72,12 @@ impl WalletsModal {
.show(ui, |ui| { .show(ui, |ui| {
ui.add_space(2.0); ui.add_space(2.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
for wallet in wallets.list() { let data = self.data.clone();
for wallet in wallets.clone().list() {
// Draw wallet list item. // Draw wallet list item.
self.wallet_item_ui(ui, wallet, modal, |id| { self.wallet_item_ui(ui, wallet, wallets, |id| {
on_select(id); modal.close();
on_select(id, data.clone());
}); });
ui.add_space(5.0); ui.add_space(5.0);
} }
@ -65,18 +91,19 @@ impl WalletsModal {
// Show button to close modal. // Show button to close modal.
ui.vertical_centered_justified(|ui| { ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.data = None;
modal.close(); modal.close();
}); });
}); });
ui.add_space(6.0); ui.add_space(6.0);
} }
/// Draw wallet list item. /// Draw wallet list item with provided callback on select.
fn wallet_item_ui(&mut self, fn wallet_item_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &Wallet, wallet: &Wallet,
modal: &Modal, wallets: &mut WalletList,
mut on_select: impl FnMut(i64)) { mut select: impl FnMut(i64)) {
let config = wallet.get_config(); let config = wallet.get_config();
let id = config.id; let id = config.id;
@ -87,6 +114,24 @@ impl WalletsModal {
ui.painter().rect(rect, rounding, Colors::fill(), View::hover_stroke()); ui.painter().rect(rect, rounding, Colors::fill(), View::hover_stroke());
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
if self.can_open {
// Show button to select or open closed wallet.
let icon = if wallet.is_open() {
CHECK
} else {
FOLDER_OPEN
};
View::item_button(ui, View::item_rounding(0, 1, true), icon, None, || {
wallets.select(Some(id));
if wallet.is_open() {
select(id);
} else {
self.selected = wallets.selected_id;
Modal::change_position(ModalPosition::CenterTop);
self.open_wallet_content = Some(OpenWalletModal::new(self.data.clone()));
}
});
} else {
// Draw button to select wallet. // Draw button to select wallet.
let current = self.selected.unwrap_or(0) == id; let current = self.selected.unwrap_or(0) == id;
if current { if current {
@ -94,23 +139,23 @@ impl WalletsModal {
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green())); ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
} else { } else {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || { View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
on_select(id); select(id);
modal.close();
}); });
} }
}
let layout_size = ui.available_size(); let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0); ui.add_space(6.0);
ui.vertical(|ui| { ui.vertical(|ui| {
ui.add_space(3.0); ui.add_space(3.0);
// Setup wallet name text. // Show wallet name text.
ui.with_layout(Layout::left_to_right(Align::Min), |ui| { ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0); ui.add_space(1.0);
View::ellipsize_text(ui, config.name, 18.0, Colors::title(false)); View::ellipsize_text(ui, config.name, 18.0, Colors::title(false));
}); });
// Setup wallet connection text. // Show wallet connection text.
let conn = if let Some(conn) = wallet.get_current_ext_conn() { let conn = if let Some(conn) = wallet.get_current_ext_conn() {
format!("{} {}", GLOBE_SIMPLE, conn.url) format!("{} {}", GLOBE_SIMPLE, conn.url)
} else { } else {
@ -119,7 +164,12 @@ impl WalletsModal {
View::ellipsize_text(ui, conn, 15.0, Colors::text(false)); View::ellipsize_text(ui, conn, 15.0, Colors::text(false));
ui.add_space(1.0); ui.add_space(1.0);
// Setup wallet API text. // Show wallet API text or open status.
if self.can_open {
ui.label(RichText::new(status_text(wallet))
.size(15.0)
.color(Colors::gray()));
} else {
let address = if let Some(port) = config.api_port { let address = if let Some(port) = config.api_port {
format!("127.0.0.1:{}", port) format!("127.0.0.1:{}", port)
} else { } else {
@ -127,6 +177,7 @@ impl WalletsModal {
}; };
let api_text = format!("{} {}", PLUGS_CONNECTED, address); let api_text = format!("{} {}", PLUGS_CONNECTED, address);
ui.label(RichText::new(api_text).size(15.0).color(Colors::gray())); ui.label(RichText::new(api_text).size(15.0).color(Colors::gray()));
}
ui.add_space(3.0); ui.add_space(3.0);
}); });
}); });

View file

@ -35,7 +35,6 @@ use crate::wallet::types::{WalletAccount, WalletData};
pub struct WalletContent { pub struct WalletContent {
/// List of wallet accounts for [`Modal`]. /// List of wallet accounts for [`Modal`].
accounts: Vec<WalletAccount>, accounts: Vec<WalletAccount>,
/// Flag to check if account is creating. /// Flag to check if account is creating.
account_creating: bool, account_creating: bool,
/// Account label [`Modal`] value. /// Account label [`Modal`] value.
@ -49,21 +48,7 @@ pub struct WalletContent {
qr_scan_result: Option<QrScanResult>, qr_scan_result: Option<QrScanResult>,
/// Current tab content to show. /// Current tab content to show.
pub current_tab: Box<dyn WalletTab> pub current_tab: Box<dyn WalletTab>,
}
impl Default for WalletContent {
fn default() -> Self {
Self {
accounts: vec![],
account_creating: false,
account_label_edit: "".to_string(),
account_creation_error: false,
camera_content: CameraContent::default(),
qr_scan_result: None,
current_tab: Box::new(WalletTransactions::default())
}
}
} }
/// Identifier for account list [`Modal`]. /// Identifier for account list [`Modal`].
@ -73,6 +58,24 @@ const ACCOUNT_LIST_MODAL: &'static str = "account_list_modal";
const QR_CODE_SCAN_MODAL: &'static str = "qr_code_scan_modal"; const QR_CODE_SCAN_MODAL: &'static str = "qr_code_scan_modal";
impl WalletContent { impl WalletContent {
/// Create new instance with optional data.
pub fn new(data: Option<String>) -> Self {
let mut content = Self {
accounts: vec![],
account_creating: false,
account_label_edit: "".to_string(),
account_creation_error: false,
camera_content: CameraContent::default(),
qr_scan_result: None,
current_tab: Box::new(WalletTransactions::default()),
};
// Provide data to messages.
if data.is_some() {
content.current_tab = Box::new(WalletMessages::new(data));
}
content
}
/// Draw wallet content. /// Draw wallet content.
pub fn ui(&mut self, pub fn ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,

View file

@ -33,13 +33,16 @@ use crate::wallet::Wallet;
/// Slatepack messages interaction tab content. /// Slatepack messages interaction tab content.
pub struct WalletMessages { pub struct WalletMessages {
/// Flag to check if it's first content draw.
first_draw: bool,
/// Slatepacks message input text. /// Slatepacks message input text.
message_edit: String, message_edit: String,
/// Flag to check if message request is loading. /// Flag to check if message request is loading.
message_loading: bool, message_loading: bool,
/// Error on finalization, parse or response creation. /// Error on finalization, parse or response creation.
message_error: String, message_error: String,
/// Parsed message result with finalization flag and transaction. /// Parsed message result.
message_result: Arc<RwLock<Option<(Slate, Result<WalletTransaction, Error>)>>>, message_result: Arc<RwLock<Option<(Slate, Result<WalletTransaction, Error>)>>>,
/// Wallet transaction [`Modal`] content. /// Wallet transaction [`Modal`] content.
@ -111,6 +114,7 @@ impl WalletMessages {
/// Create new content instance, put message into input if provided. /// Create new content instance, put message into input if provided.
pub fn new(message: Option<String>) -> Self { pub fn new(message: Option<String>) -> Self {
Self { Self {
first_draw: true,
message_edit: message.unwrap_or("".to_string()), message_edit: message.unwrap_or("".to_string()),
message_loading: false, message_loading: false,
message_error: "".to_string(), message_error: "".to_string(),
@ -128,6 +132,14 @@ impl WalletMessages {
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &mut Wallet, wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
if self.first_draw {
// Parse provided message on first draw.
if !self.message_edit.is_empty() {
self.parse_message(wallet);
}
self.first_draw = false;
}
ui.add_space(3.0); ui.add_space(3.0);
// Show creation of request to send or receive funds. // Show creation of request to send or receive funds.

View file

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use crate::gui::icons::{FOLDER_LOCK, FOLDER_OPEN, SPINNER, WARNING_CIRCLE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::wallet::Wallet; use crate::wallet::Wallet;
@ -49,3 +50,38 @@ impl WalletTabType {
} }
} }
} }
/// Get wallet status text.
pub fn status_text(wallet: &Wallet) -> String {
if wallet.is_open() {
if wallet.sync_error() {
format!("{} {}", WARNING_CIRCLE, t!("error"))
} else if wallet.is_closing() {
format!("{} {}", SPINNER, t!("wallets.closing"))
} else if wallet.is_repairing() {
let repair_progress = wallet.repairing_progress();
if repair_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.checking"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.checking"),
repair_progress)
}
} else if wallet.syncing() {
let info_progress = wallet.info_sync_progress();
if info_progress == 100 || info_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.loading"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.loading"),
info_progress)
}
} else {
format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked"))
}
} else {
format!("{} {}", FOLDER_LOCK, t!("wallets.locked"))
}
}

View file

@ -29,6 +29,23 @@ fn real_main() {
.parse_default_env() .parse_default_env()
.init(); .init();
// Handle file path argument passing.
let args: Vec<_> = std::env::args().collect();
let mut data = None;
if args.len() > 1 {
let path = std::path::PathBuf::from(&args[1]);
let content = match std::fs::read_to_string(path) {
Ok(s) => Some(s),
Err(_) => None
};
data = content
}
// Check if another app instance already running.
if is_app_running(data.clone()) {
return;
}
// Setup callback on panic crash. // Setup callback on panic crash.
std::panic::set_hook(Box::new(|info| { std::panic::set_hook(Box::new(|info| {
let backtrace = backtrace::Backtrace::new(); let backtrace = backtrace::Backtrace::new();
@ -50,22 +67,61 @@ fn real_main() {
// Start GUI. // Start GUI.
match std::panic::catch_unwind(|| { match std::panic::catch_unwind(|| {
start_desktop_gui(); start_desktop_gui(data);
}) { }) {
Ok(_) => {} Ok(_) => {}
Err(e) => println!("{:?}", e) Err(e) => println!("{:?}", e)
} }
} }
/// Start GUI with Desktop related setup. /// Check if application is already running to pass extra data.
#[allow(dead_code)] #[allow(dead_code)]
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
fn start_desktop_gui() { fn is_app_running(data: Option<String>) -> bool {
use tor_rtcompat::BlockOn;
let runtime = tor_rtcompat::tokio::TokioNativeTlsRuntime::create().unwrap();
let res: Result<(), Box<dyn std::error::Error>> = runtime
.block_on(async {
use interprocess::local_socket::{
tokio::{prelude::*, Stream},
GenericFilePath
};
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
try_join,
};
let socket_path = grim::Settings::socket_path();
let name = socket_path.to_fs_name::<GenericFilePath>()?;
// Connect to running application socket.
let conn = Stream::connect(name).await?;
let (rec, mut sen) = conn.split();
let mut rec = BufReader::new(rec);
let data = data.unwrap_or("".to_string());
let mut buffer = String::with_capacity(data.len());
// Send extra data to socket.
let send = sen.write_all(data.as_bytes());
let recv = rec.read_line(&mut buffer);
try_join!(send, recv)?;
drop((rec, sen));
Ok(())
});
return match res {
Ok(_) => true,
Err(_) => false
}
}
/// Start GUI with Desktop related setup passing extra data from opening.
#[allow(dead_code)]
#[cfg(not(target_os = "android"))]
fn start_desktop_gui(data: Option<String>) {
use grim::AppConfig; use grim::AppConfig;
use dark_light::Mode; use dark_light::Mode;
let platform = grim::gui::platform::Desktop::default();
// Setup system theme if not set. // Setup system theme if not set.
if let None = AppConfig::dark_theme() { if let None = AppConfig::dark_theme() {
let dark = match dark_light::detect() { let dark = match dark_light::detect() {
@ -76,12 +132,11 @@ fn start_desktop_gui() {
AppConfig::set_dark_theme(dark); AppConfig::set_dark_theme(dark);
} }
// Setup window size.
let (width, height) = AppConfig::window_size(); let (width, height) = AppConfig::window_size();
let mut viewport = egui::ViewportBuilder::default() let mut viewport = egui::ViewportBuilder::default()
.with_min_inner_size([AppConfig::MIN_WIDTH, AppConfig::MIN_HEIGHT]) .with_min_inner_size([AppConfig::MIN_WIDTH, AppConfig::MIN_HEIGHT])
.with_inner_size([width, height]); .with_inner_size([width, height]);
// Setup an icon. // Setup an icon.
if let Ok(icon) = eframe::icon_data::from_png_bytes(include_bytes!("../img/icon.png")) { if let Ok(icon) = eframe::icon_data::from_png_bytes(include_bytes!("../img/icon.png")) {
viewport = viewport.with_icon(std::sync::Arc::new(icon)); viewport = viewport.with_icon(std::sync::Arc::new(icon));
@ -93,6 +148,7 @@ fn start_desktop_gui() {
// Setup window decorations. // Setup window decorations.
let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac; let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac;
viewport = viewport viewport = viewport
.with_window_level(egui::WindowLevel::Normal)
.with_fullsize_content_view(true) .with_fullsize_content_view(true)
.with_title_shown(false) .with_title_shown(false)
.with_titlebar_buttons_shown(false) .with_titlebar_buttons_shown(false)
@ -112,8 +168,18 @@ fn start_desktop_gui() {
eframe::Renderer::Wgpu eframe::Renderer::Wgpu
}; };
let mut platform = grim::gui::platform::Desktop::new(data);
// Start app socket at separate thread.
let socket_pl = platform.clone();
platform = socket_pl.clone();
std::thread::spawn(move || {
start_app_socket(socket_pl);
});
// Start GUI. // Start GUI.
match grim::start(options.clone(), grim::app_creator(grim::gui::App::new(platform.clone()))) { let app = grim::gui::App::new(platform.clone());
match grim::start(options.clone(), grim::app_creator(app)) {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
if win { if win {
@ -121,7 +187,9 @@ fn start_desktop_gui() {
} }
// Start with another renderer on error. // Start with another renderer on error.
options.renderer = eframe::Renderer::Glow; options.renderer = eframe::Renderer::Glow;
match grim::start(options, grim::app_creator(grim::gui::App::new(platform))) {
let app = grim::gui::App::new(platform);
match grim::start(options, grim::app_creator(app)) {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
panic!("{}", e); panic!("{}", e);
@ -130,3 +198,68 @@ fn start_desktop_gui() {
} }
} }
} }
/// Start socket that handles data for single application instance.
#[allow(dead_code)]
#[cfg(not(target_os = "android"))]
fn start_app_socket(platform: grim::gui::platform::Desktop) {
use tor_rtcompat::BlockOn;
let runtime = tor_rtcompat::tokio::TokioNativeTlsRuntime::create().unwrap();
let _: Result<_, _> = runtime
.block_on(async {
use interprocess::local_socket::{
tokio::{prelude::*, Stream},
GenericFilePath, Listener, ListenerOptions,
};
use std::io;
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
try_join,
};
// Handle incoming connection.
async fn handle_conn(conn: Stream)
-> io::Result<String> {
let mut rec = BufReader::new(&conn);
let mut sen = &conn;
let mut buffer = String::new();
let send = sen.write_all(b"");
let recv = rec.read_line(&mut buffer);
// Read data and send answer.
try_join!(recv, send)?;
Ok(buffer)
}
let socket_path = grim::Settings::socket_path();
std::fs::remove_file(socket_path.clone()).unwrap();
let name = socket_path.to_fs_name::<GenericFilePath>()?;
let opts = ListenerOptions::new().name(name);
// Create socket listener.
let listener = match opts.create_tokio() {
Err(e) if e.kind() == io::ErrorKind::AddrInUse => {
eprintln!("Socket file is occupied.");
return Err::<Listener, io::Error>(e);
}
x => x?,
};
// Handle connections.
loop {
let conn = match listener.accept().await {
Ok(c) => c,
Err(_) => continue
};
let res = handle_conn(conn).await;
match res {
Ok(data) => {
platform.on_data(data)
},
Err(_) => {}
}
}
});
}

View file

@ -141,6 +141,13 @@ impl Settings {
path path
} }
/// Get desktop application socket path.
pub fn socket_path() -> String {
let mut socket_path = Self::base_path(None);
socket_path.push("grim.socket");
socket_path.to_str().unwrap().to_string()
}
/// Get configuration file path from provided name and sub-directory if needed. /// Get configuration file path from provided name and sub-directory if needed.
pub fn config_path(config_name: &str, sub_dir: Option<String>) -> PathBuf { pub fn config_path(config_name: &str, sub_dir: Option<String>) -> PathBuf {
let mut path = Self::base_path(sub_dir); let mut path = Self::base_path(sub_dir);

View file

@ -18,7 +18,8 @@ use grin_wallet_libwallet::Error;
use crate::AppConfig; use crate::AppConfig;
use crate::wallet::{Wallet, WalletConfig}; use crate::wallet::{Wallet, WalletConfig};
/// Wrapper for [`Wallet`] list. /// [`Wallet`] list container.
#[derive(Clone)]
pub struct WalletList { pub struct WalletList {
/// List of wallets for [`ChainTypes::Mainnet`]. /// List of wallets for [`ChainTypes::Mainnet`].
pub main_list: Vec<Wallet>, pub main_list: Vec<Wallet>,