Open .slatepack file with the app #13

Merged
ardocrat merged 28 commits from slatepack_ext_file into master 2024-09-16 19:08:27 +03:00
19 changed files with 672 additions and 243 deletions
Showing only changes of commit dbc28205e8 - Show all commits

28
Cargo.lock generated
View file

@ -2483,6 +2483,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
[[package]]
name = "doctest-file"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562"
[[package]]
name = "document-features"
version = "0.2.8"
@ -3833,6 +3839,7 @@ dependencies = [
"hyper 0.14.29",
"hyper-tls 0.5.0",
"image 0.25.1",
"interprocess",
"jni",
"lazy_static",
"local-ip-address",
@ -4976,6 +4983,21 @@ dependencies = [
"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]]
name = "intl-memoizer"
version = "0.5.2"
@ -7432,6 +7454,12 @@ dependencies = [
"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]]
name = "redox_syscall"
version = "0.1.57"

View file

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

View file

@ -4,3 +4,4 @@ Exec=grim
Icon=grim
Type=Application
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.
pub struct App<Platform> {
/// Platform specific callbacks handler.
pub(crate) platform: Platform,
/// Main ui content.
pub platform: Platform,
/// Main content.
content: Content,
/// 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> {
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.
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.
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();
if back_pressed {
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
@ -59,8 +72,8 @@ impl<Platform: PlatformCallbacks> App<Platform> {
ctx.request_repaint();
}
// Handle Close event (on desktop).
if ctx.input(|i| i.viewport().close_requested()) {
// Handle Close event on desktop.
if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) {
if !self.content.exit_allowed {
ctx.send_viewport_cmd(ViewportCommand::CancelClose);
Content::show_exit_modal();
@ -92,6 +105,11 @@ impl<Platform: PlatformCallbacks> App<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
}
fn consume_data(&mut self) -> Option<String> {
None
}
}
lazy_static! {

View file

@ -13,12 +13,13 @@
// limitations under the License.
use std::fs::File;
use std::io:: Write;
use lazy_static::lazy_static;
use parking_lot::RwLock;
use std::io::Write;
use std::thread;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use parking_lot::RwLock;
use lazy_static::lazy_static;
use egui::{UserAttentionType, ViewportCommand};
use rfd::FileDialog;
use crate::gui::platform::PlatformCallbacks;
@ -28,17 +29,16 @@ use crate::gui::platform::PlatformCallbacks;
pub struct Desktop {
/// Flag to check if camera stop is needed.
stop_camera: Arc<AtomicBool>,
}
impl Default for Desktop {
fn default() -> Self {
Self {
stop_camera: Arc::new(AtomicBool::new(false)),
}
}
/// Context to repaint content and handle viewport commands.
ctx: Arc<RwLock<Option<egui::Context>>>,
}
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 hide_keyboard(&self) {}
@ -119,9 +119,61 @@ impl PlatformCallbacks for Desktop {
fn picked_file(&self) -> Option<String> {
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 {
/// 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)]
#[cfg(target_os = "windows")]
fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
@ -205,4 +257,7 @@ impl Desktop {
lazy_static! {
/// Last captured image from started camera.
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 trait PlatformCallbacks {
fn set_context(&mut self, ctx: &egui::Context);
fn show_keyboard(&self);
fn hide_keyboard(&self);
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 pick_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.
pub(crate) id: &'static str,
/// Position on the screen.
position: ModalPosition,
pub position: ModalPosition,
/// To check if it can be closed.
closeable: Arc<AtomicBool>,
/// Title text
@ -64,6 +64,12 @@ impl Modal {
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.
pub fn close(&self) {
let mut w_nav = MODAL_STATE.write();

View file

@ -83,7 +83,7 @@ impl Default for StratumSetup {
Self {
wallets: WalletList::default(),
wallets_modal: WalletsModal::new(wallet_id),
wallets_modal: WalletsModal::new(wallet_id, None, false),
available_ips: NodeConfig::get_ip_addrs(),
stratum_port_edit: port,
stratum_port_available_edit: is_port_available,
@ -111,10 +111,12 @@ impl ModalContainer for StratumSetup {
modal: &Modal,
cb: &dyn PlatformCallbacks) {
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);
self.wallet_name = WalletConfig::name_by_id(id);
}),
})
},
STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb),
ATTEMPT_TIME_MODAL => self.attempt_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`].
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.
Modal::new(WALLET_SELECTION_MODAL)
.position(ModalPosition::Center)

View file

@ -18,13 +18,14 @@ use egui::scroll_area::ScrollBarVisibility;
use crate::AppConfig;
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::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::modals::WalletConnectionModal;
use crate::gui::views::wallets::modals::{OpenWalletModal, WalletConnectionModal, WalletsModal};
use crate::gui::views::wallets::types::WalletTabType;
use crate::gui::views::wallets::wallet::types::status_text;
use crate::gui::views::wallets::WalletContent;
use crate::wallet::{Wallet, WalletList};
@ -33,10 +34,11 @@ pub struct WalletsContent {
/// List of wallets.
wallets: WalletList,
/// Password to open wallet for [`Modal`].
pass_edit: String,
/// Flag to check if wrong password was entered at [`Modal`].
wrong_pass: bool,
/// Wallet selection [`Modal`] content.
wallet_selection_content: Option<WalletsModal>,
/// Wallet opening [`Modal`] content.
open_wallet_content: Option<OpenWalletModal>,
/// Wallet connection selection content.
conn_modal_content: Option<WalletConnectionModal>,
@ -54,24 +56,29 @@ pub struct WalletsContent {
}
/// 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`].
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 {
fn default() -> Self {
Self {
wallets: WalletList::default(),
pass_edit: "".to_string(),
wrong_pass: false,
wallet_selection_content: None,
open_wallet_content: None,
conn_modal_content: None,
wallet_content: WalletContent::default(),
wallet_content: WalletContent::new(None),
creation_content: WalletCreation::default(),
show_wallets_at_dual_panel: AppConfig::show_wallets_at_dual_panel(),
modal_ids: vec![
OPEN_WALLET_MODAL,
WalletCreation::NAME_PASS_MODAL,
CONNECTION_SELECTION_MODAL,
SELECT_WALLET_MODAL
]
}
}
@ -87,13 +94,21 @@ impl ModalContainer for WalletsContent {
modal: &Modal,
cb: &dyn PlatformCallbacks) {
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 => {
self.creation_content.name_pass_modal_ui(ui, modal, cb)
},
CONNECTION_SELECTION_MODAL => {
if let Some(content) = self.conn_modal_content.as_mut() {
content.ui(ui, modal, cb, |id| {
// Update wallet connection on select.
let list = self.wallets.list();
for w in list {
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 {
/// Draw wallets content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw modal content for current ui container.
self.current_modal_ui(ui, cb);
@ -159,7 +182,7 @@ impl WalletsContent {
// Add created wallet to list.
self.wallets.add(wallet);
// Reset wallet content.
self.wallet_content = WalletContent::default();
self.wallet_content = WalletContent::new(None);
});
} else {
let selected_id = self.wallets.selected_id.clone();
@ -254,6 +277,56 @@ impl WalletsContent {
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.
fn title_ui(&mut self,
ui: &mut egui::Ui,
@ -383,8 +456,7 @@ impl WalletsContent {
// Check if wallet reopen is needed.
if !wallet.is_open() && wallet.reopen_needed() {
wallet.set_reopen(false);
self.wallets.select(Some(wallet.get_config().id));
self.show_open_wallet_modal(cb);
self.show_opening_modal(wallet.get_config().id, None, cb);
}
// Draw wallet list item.
self.wallet_item_ui(ui, wallet, cb);
@ -420,8 +492,7 @@ impl WalletsContent {
if !wallet.is_open() {
// Show button to open closed wallet.
View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || {
self.wallets.select(Some(id));
self.show_open_wallet_modal(cb);
self.show_opening_modal(id, None, cb);
});
// Show button to select connection if not syncing.
if !wallet.syncing() {
@ -435,7 +506,7 @@ impl WalletsContent {
// Show button to select opened wallet.
View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || {
self.wallets.select(Some(id));
self.wallet_content = WalletContent::default();
self.wallet_content = WalletContent::new(None);
});
}
// Show button to close opened wallet.
@ -455,7 +526,7 @@ impl WalletsContent {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Setup wallet name text.
// Show wallet name text.
let name_color = if is_selected {
Colors::white_or_black(true)
} else {
@ -466,42 +537,11 @@ impl WalletsContent {
View::ellipsize_text(ui, config.name, 18.0, name_color);
});
// Setup wallet status text.
let status_text = 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::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
// Show wallet status text.
View::ellipsize_text(ui, status_text(wallet), 15.0, Colors::text(false));
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() {
format!("{} {}", GLOBE_SIMPLE, conn.url)
} else {
@ -525,11 +565,10 @@ impl WalletsContent {
.show();
}
/// Show [`Modal`] to open selected wallet.
fn show_open_wallet_modal(&mut self, cb: &dyn PlatformCallbacks) {
// Reset modal values.
self.pass_edit = String::from("");
self.wrong_pass = false;
/// Show [`Modal`] to select and open wallet.
fn show_opening_modal(&mut self, id: i64, data: Option<String>, cb: &dyn PlatformCallbacks) {
self.wallets.select(Some(id));
self.open_wallet_content = Some(OpenWalletModal::new(data));
// Show modal.
Modal::new(OPEN_WALLET_MODAL)
.position(ModalPosition::CenterTop)
@ -537,100 +576,6 @@ impl WalletsContent {
.show();
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.

View file

@ -17,3 +17,6 @@ pub use conn::*;
mod 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 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::types::ModalPosition;
use crate::gui::views::wallets::modals::OpenWalletModal;
use crate::gui::views::wallets::wallet::types::status_text;
use crate::wallet::{Wallet, WalletList};
/// Wallet list [`Modal`] content
pub struct WalletsModal {
/// 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 {
pub fn new(selected: Option<i64>) -> Self {
Self {
selected,
}
/// Create new content instance.
pub fn new(selected: Option<i64>, data: Option<String>, can_open: bool) -> Self {
Self { selected, data, can_open, open_wallet_content: None }
}
/// Draw [`Modal`] content.
/// Draw content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
wallets: &WalletList,
mut on_select: impl FnMut(i64)) {
wallets: &mut WalletList,
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);
ScrollArea::vertical()
.max_height(373.0)
@ -48,10 +72,12 @@ impl WalletsModal {
.show(ui, |ui| {
ui.add_space(2.0);
ui.vertical_centered(|ui| {
for wallet in wallets.list() {
let data = self.data.clone();
for wallet in wallets.clone().list() {
// Draw wallet list item.
self.wallet_item_ui(ui, wallet, modal, |id| {
on_select(id);
self.wallet_item_ui(ui, wallet, wallets, |id| {
modal.close();
on_select(id, data.clone());
});
ui.add_space(5.0);
}
@ -65,18 +91,19 @@ impl WalletsModal {
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.data = None;
modal.close();
});
});
ui.add_space(6.0);
}
/// Draw wallet list item.
/// Draw wallet list item with provided callback on select.
fn wallet_item_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
mut on_select: impl FnMut(i64)) {
wallets: &mut WalletList,
mut select: impl FnMut(i64)) {
let config = wallet.get_config();
let id = config.id;
@ -87,6 +114,24 @@ impl WalletsModal {
ui.painter().rect(rect, rounding, Colors::fill(), View::hover_stroke());
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.
let current = self.selected.unwrap_or(0) == id;
if current {
@ -94,23 +139,23 @@ impl WalletsModal {
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
} else {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
on_select(id);
modal.close();
select(id);
});
}
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Setup wallet name text.
// Show wallet name text.
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
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() {
format!("{} {}", GLOBE_SIMPLE, conn.url)
} else {
@ -119,7 +164,12 @@ impl WalletsModal {
View::ellipsize_text(ui, conn, 15.0, Colors::text(false));
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 {
format!("127.0.0.1:{}", port)
} else {
@ -127,6 +177,7 @@ impl WalletsModal {
};
let api_text = format!("{} {}", PLUGS_CONNECTED, address);
ui.label(RichText::new(api_text).size(15.0).color(Colors::gray()));
}
ui.add_space(3.0);
});
});

View file

@ -35,7 +35,6 @@ use crate::wallet::types::{WalletAccount, WalletData};
pub struct WalletContent {
/// List of wallet accounts for [`Modal`].
accounts: Vec<WalletAccount>,
/// Flag to check if account is creating.
account_creating: bool,
/// Account label [`Modal`] value.
@ -49,21 +48,7 @@ pub struct WalletContent {
qr_scan_result: Option<QrScanResult>,
/// Current tab content to show.
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())
}
}
pub current_tab: Box<dyn WalletTab>,
}
/// 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";
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.
pub fn ui(&mut self,
ui: &mut egui::Ui,

View file

@ -33,13 +33,16 @@ use crate::wallet::Wallet;
/// Slatepack messages interaction tab content.
pub struct WalletMessages {
/// Flag to check if it's first content draw.
first_draw: bool,
/// Slatepacks message input text.
message_edit: String,
/// Flag to check if message request is loading.
message_loading: bool,
/// Error on finalization, parse or response creation.
message_error: String,
/// Parsed message result with finalization flag and transaction.
/// Parsed message result.
message_result: Arc<RwLock<Option<(Slate, Result<WalletTransaction, Error>)>>>,
/// Wallet transaction [`Modal`] content.
@ -111,6 +114,7 @@ impl WalletMessages {
/// Create new content instance, put message into input if provided.
pub fn new(message: Option<String>) -> Self {
Self {
first_draw: true,
message_edit: message.unwrap_or("".to_string()),
message_loading: false,
message_error: "".to_string(),
@ -128,6 +132,14 @@ impl WalletMessages {
ui: &mut egui::Ui,
wallet: &mut Wallet,
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);
// 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
// limitations under the License.
use crate::gui::icons::{FOLDER_LOCK, FOLDER_OPEN, SPINNER, WARNING_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
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()
.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.
std::panic::set_hook(Box::new(|info| {
let backtrace = backtrace::Backtrace::new();
@ -50,22 +67,61 @@ fn real_main() {
// Start GUI.
match std::panic::catch_unwind(|| {
start_desktop_gui();
start_desktop_gui(data);
}) {
Ok(_) => {}
Err(e) => println!("{:?}", e)
}
}
/// Start GUI with Desktop related setup.
/// Check if application is already running to pass extra data.
#[allow(dead_code)]
#[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 dark_light::Mode;
let platform = grim::gui::platform::Desktop::default();
// Setup system theme if not set.
if let None = AppConfig::dark_theme() {
let dark = match dark_light::detect() {
@ -76,12 +132,11 @@ fn start_desktop_gui() {
AppConfig::set_dark_theme(dark);
}
// Setup window size.
let (width, height) = AppConfig::window_size();
let mut viewport = egui::ViewportBuilder::default()
.with_min_inner_size([AppConfig::MIN_WIDTH, AppConfig::MIN_HEIGHT])
.with_inner_size([width, height]);
// Setup an icon.
if let Ok(icon) = eframe::icon_data::from_png_bytes(include_bytes!("../img/icon.png")) {
viewport = viewport.with_icon(std::sync::Arc::new(icon));
@ -93,6 +148,7 @@ fn start_desktop_gui() {
// Setup window decorations.
let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac;
viewport = viewport
.with_window_level(egui::WindowLevel::Normal)
.with_fullsize_content_view(true)
.with_title_shown(false)
.with_titlebar_buttons_shown(false)
@ -112,8 +168,18 @@ fn start_desktop_gui() {
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.
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(_) => {}
Err(e) => {
if win {
@ -121,7 +187,9 @@ fn start_desktop_gui() {
}
// Start with another renderer on error.
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(_) => {}
Err(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
}
/// 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.
pub fn config_path(config_name: &str, sub_dir: Option<String>) -> PathBuf {
let mut path = Self::base_path(sub_dir);

View file

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