diff --git a/locales/de.yml b/locales/de.yml index 3328479f..dd0beb95 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -33,6 +33,9 @@ crash_report_warning: Anwendung wurde beim letzten Mal unerwartet geschlossen, S confirmation: Bestätigung enter_url: URL eingeben max_short: MAX +files_location: Dateistandort +moving_files: Dateien verschieben +wrong_path_error: Falscher Weg angegeben wallets: await_conf_amount: Erwarte Bestätigung await_fin_amount: Warten auf die Fertigstellung diff --git a/locales/en.yml b/locales/en.yml index 6aad6cb2..3adf73fd 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -33,6 +33,9 @@ crash_report_warning: Application closed unexpectedly last time, you can share c confirmation: Confirmation enter_url: Enter URL max_short: MAX +files_location: Files location +moving_files: Moving files +wrong_path_error: Wrong path specified wallets: await_conf_amount: Awaiting confirmation await_fin_amount: Awaiting finalization diff --git a/locales/fr.yml b/locales/fr.yml index b7e07e53..d72ba765 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -33,6 +33,9 @@ crash_report_warning: L'application s'est fermée de manière inattendue la dern confirmation: Confirmation enter_url: Entrez l'URL max_short: MAX +files_location: Emplacement du fichier +moving_files: Déplacer des fichiers +wrong_path_error: Chemin incorrect spécifié wallets: await_conf_amount: En attente de confirmation await_fin_amount: En attente de finalisation diff --git a/locales/ru.yml b/locales/ru.yml index 71f14e22..65fc323b 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -33,6 +33,9 @@ crash_report_warning: В прошлый раз приложение неожид confirmation: Подтверждение enter_url: Введите URL-адрес max_short: МАКС +files_location: Расположение файлов +moving_files: Перемещение файлов +wrong_path_error: Указан неправильный путь wallets: await_conf_amount: Ожидает подтверждения await_fin_amount: Ожидает завершения diff --git a/locales/tr.yml b/locales/tr.yml index 83c8cd18..55122a46 100644 --- a/locales/tr.yml +++ b/locales/tr.yml @@ -33,6 +33,9 @@ crash_report_warning: Uygulama beklenmedik bir sekilde kapandi son kez, kilitlen confirmation: Onay enter_url: URL'yi girin max_short: MAKS +files_location: Dosya konumu +moving_files: Dosyalari Tasima +wrong_path_error: Yanlis yol belirtildi wallets: await_conf_amount: Onay bekleniyor await_fin_amount: Tamamlanma bekleniyor diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index ef4a50c5..be01c913 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -33,6 +33,9 @@ crash_report_warning: 上次应用程序意外关闭,您可以报告开发人 confirmation: 确认 enter_url: 输入 URL max_short: 最大數量 +files_location: 檔案位置 +moving_files: 檔案移動 +wrong_path_error: 指定錯誤路徑 wallets: await_conf_amount: 等待确认中 await_fin_amount: 等待确定中 diff --git a/src/gui/views/content.rs b/src/gui/views/content.rs index 827ab3f7..f67e1946 100644 --- a/src/gui/views/content.rs +++ b/src/gui/views/content.rs @@ -194,7 +194,7 @@ impl Content { /// Draw exit confirmation modal content. fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { if self.show_exit_progress { - if !Node::is_running() { + if !Node::is_running() && !Node::data_dir_changing() { self.exit_allowed = true; cb.exit(); Modal::close(); @@ -203,7 +203,12 @@ impl Content { ui.vertical_centered(|ui| { View::small_loading_spinner(ui); ui.add_space(12.0); - ui.label(RichText::new(t!("sync_status.shutdown")) + let exit_status_text = if Node::data_dir_changing() { + t!("moving_files") + } else { + t!("sync_status.shutdown") + }; + ui.label(RichText::new(exit_status_text) .size(17.0) .color(Colors::text(false))); }); @@ -228,7 +233,7 @@ impl Content { }); columns[1].vertical_centered_justified(|ui| { View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |_| { - if !Node::is_running() { + if !Node::is_running() && !Node::data_dir_changing() { self.exit_allowed = true; cb.exit(); Modal::close(); diff --git a/src/gui/views/file_pick.rs b/src/gui/views/file_pick.rs index 2c767d11..b4e94f4f 100644 --- a/src/gui/views/file_pick.rs +++ b/src/gui/views/file_pick.rs @@ -39,6 +39,8 @@ pub struct FilePickContent { /// Flag to check if file is picking. file_picking: Arc, + /// Flag to check if folder should be picked. + pick_folder: bool, /// Flag to parse file content after pick. parse_file: bool, /// Flag to check if file is parsing. @@ -54,12 +56,19 @@ impl FilePickContent { content_type, active: false, file_picking: Arc::new(AtomicBool::new(false)), + pick_folder: false, parse_file: true, file_parsing: Arc::new(AtomicBool::new(false)), file_parsing_result: Arc::new(RwLock::new(None)), } } + /// Pick folder. + pub fn pick_folder(mut self) -> Self { + self.pick_folder = true; + self + } + /// Do not parse file content. pub fn no_parse(mut self) -> Self { self.parse_file = false; @@ -79,7 +88,11 @@ impl FilePickContent { if let Some(path) = cb.picked_file() { self.file_picking.store(false, Ordering::Relaxed); if !path.is_empty() { - self.on_file_pick(path); + if self.parse_file { + self.parse_file(path); + } else { + pick(path); + } } } } else if self.file_parsing.load(Ordering::Relaxed) { @@ -106,29 +119,15 @@ impl FilePickContent { match self.content_type { FilePickContentType::Button => { let text = format!("{} {}", ARCHIVE_BOX, t!("choose_file")); - View::colored_text_button(ui, - text, - Colors::blue(), - Colors::white_or_black(false), - || { - if let Some(path) = cb.pick_file() { - if !self.parse_file { - pick(path); - return; - } - self.on_file_pick(path); - } - }); + let text_color = Colors::blue(); + let fill = Colors::white_or_black(false); + View::colored_text_button(ui, text, text_color, fill, || { + self.on_file_pick(pick, cb); + }); } FilePickContentType::ItemButton(r) => { View::item_button(ui, r, ARCHIVE_BOX, Some(Colors::blue()), || { - if let Some(path) = cb.pick_file() { - if !self.parse_file { - pick(path); - return; - } - self.on_file_pick(path); - } + self.on_file_pick(pick, cb); }); } FilePickContentType::Tab => { @@ -138,30 +137,39 @@ impl FilePickContent { false => None }; View::tab_button(ui, ARCHIVE_BOX, Some(Colors::blue()), active, |_| { - if let Some(path) = cb.pick_file() { - if !self.parse_file { - pick(path); - return; - } - self.on_file_pick(path); - } + self.on_file_pick(pick, cb); }); } } } } - /// Handle picked file path. - fn on_file_pick(&self, path: String) { + /// Handle pick file request. + fn on_file_pick(&self, on_pick: impl FnOnce(String), cb: &dyn PlatformCallbacks) { + let path = if self.pick_folder { + cb.pick_folder() + } else { + cb.pick_file() + }; + if path.is_none() { + return; + } + let path = path.unwrap(); // Wait for asynchronous file pick result if path is empty. if path.is_empty() { self.file_picking.store(true, Ordering::Relaxed); return; } - // Do not parse result. - if !self.parse_file { - return; + // Parse result if needed. + if self.parse_file { + self.parse_file(path); + } else { + on_pick(path); } + } + + /// Handle picked file path. + fn parse_file(&self, path: String) { self.file_parsing.store(true, Ordering::Relaxed); let result = self.file_parsing_result.clone(); thread::spawn(move || { diff --git a/src/gui/views/network/content.rs b/src/gui/views/network/content.rs index 7c114649..3eb871e6 100644 --- a/src/gui/views/network/content.rs +++ b/src/gui/views/network/content.rs @@ -274,7 +274,7 @@ impl NetworkContent { // Setup values for title panel. let title_text = self.node_tab_content.get_type().title(); let subtitle_text = Node::get_sync_status_text(); - let not_syncing = Node::not_syncing(); + let not_syncing = Node::not_syncing() && !Node::data_dir_changing(); let title_content = if show_settings { TitleContentType::Title(t!("settings")) } else if !show_connections { diff --git a/src/gui/views/network/setup/node.rs b/src/gui/views/network/setup/node.rs index a2e61713..dcb8fc72 100644 --- a/src/gui/views/network/setup/node.rs +++ b/src/gui/views/network/setup/node.rs @@ -12,21 +12,28 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{Id, RichText}; +use eframe::emath::Align; +use eframe::epaint::StrokeKind; +use egui::{Id, Layout, RichText}; use grin_core::global::ChainTypes; -use crate::gui::icons::{CLOCK_CLOCKWISE, COMPUTER_TOWER, PLUG, POWER, SHIELD, SHIELD_SLASH}; +use crate::gui::icons::{CLOCK_CLOCKWISE, COMPUTER_TOWER, FOLDERS, PENCIL, PLUG, POWER, SHIELD, SHIELD_SLASH}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::network::settings::NetworkSettings; use crate::gui::views::network::NetworkContent; use crate::gui::views::types::{ContentContainer, ModalPosition}; -use crate::gui::views::{Modal, TextEdit, View}; +use crate::gui::views::{FilePickContent, FilePickContentType, Modal, TextEdit, View}; use crate::gui::Colors; use crate::node::{Node, NodeConfig}; use crate::AppConfig; /// Integrated node general setup section content. pub struct NodeSetup { + /// Data path value value for [`Modal`]. + data_path_edit: String, + /// Button to pick directory for chain data. + pick_data_dir: FilePickContent, + /// IP Addresses available at system. available_ips: Vec, @@ -45,20 +52,26 @@ pub struct NodeSetup { ftl_edit: String, } +/// Identifier for chain data path value [`Modal`]. +const DATA_PATH_MODAL: &'static str = "node_data_path"; /// Identifier for API port value [`Modal`]. -pub const API_PORT_MODAL: &'static str = "api_port"; +const API_PORT_MODAL: &'static str = "node_api_port"; /// Identifier for API secret value [`Modal`]. -pub const API_SECRET_MODAL: &'static str = "api_secret"; +const API_SECRET_MODAL: &'static str = "node_api_secret"; /// Identifier for Foreign API secret value [`Modal`]. -pub const FOREIGN_API_SECRET_MODAL: &'static str = "foreign_api_secret"; +const FOREIGN_API_SECRET_MODAL: &'static str = "node_foreign_api_secret"; /// Identifier for FTL value [`Modal`]. -pub const FTL_MODAL: &'static str = "ftl"; +const FTL_MODAL: &'static str = "node_ftl"; impl Default for NodeSetup { fn default() -> Self { let (api_ip, api_port) = NodeConfig::get_api_ip_port(); let is_api_port_available = NodeConfig::is_api_port_available(&api_ip, &api_port); Self { + data_path_edit: NodeConfig::get_chain_data_path(), + pick_data_dir: FilePickContent::new( + FilePickContentType::ItemButton(View::item_rounding(0, 1, true)) + ).no_parse().pick_folder(), available_ips: NodeConfig::get_ip_addrs(), api_port_edit: api_port, api_port_available_edit: is_api_port_available, @@ -72,6 +85,7 @@ impl Default for NodeSetup { impl ContentContainer for NodeSetup { fn modal_ids(&self) -> Vec<&'static str> { vec![ + DATA_PATH_MODAL, API_PORT_MODAL, API_SECRET_MODAL, FOREIGN_API_SECRET_MODAL, @@ -84,6 +98,7 @@ impl ContentContainer for NodeSetup { modal: &Modal, cb: &dyn PlatformCallbacks) { match modal.id { + DATA_PATH_MODAL => self.data_path_edit_modal_ui(ui, cb), API_PORT_MODAL => self.api_port_modal(ui, modal, cb), API_SECRET_MODAL => self.secret_modal(ui, modal, cb), FOREIGN_API_SECRET_MODAL => self.secret_modal(ui, modal, cb), @@ -92,7 +107,7 @@ impl ContentContainer for NodeSetup { } } - fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { + fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { View::sub_title(ui, format!("{} {}", COMPUTER_TOWER, t!("network_settings.server"))); View::horizontal_line(ui, Colors::stroke()); ui.add_space(6.0); @@ -102,7 +117,8 @@ impl ContentContainer for NodeSetup { ui.add_space(2.0); // Show loading indicator or controls to stop/start/restart node. - if Node::is_stopping() || Node::is_restarting() || Node::is_starting() { + if Node::is_stopping() || Node::is_restarting() || Node::is_starting() + || Node::data_dir_changing() { ui.vertical_centered(|ui| { ui.add_space(8.0); View::small_loading_spinner(ui); @@ -154,6 +170,13 @@ impl ContentContainer for NodeSetup { }); ui.add_space(6.0); + // Show data location selection for Desktop when it already started or turned off. + if !Node::is_restarting() && !Node::is_stopping() && !Node::is_starting() && + View::is_desktop() { + self.pick_data_dir_ui(ui, cb); + ui.add_space(6.0); + } + if self.available_ips.is_empty() { // Show message when IP addresses are not available on the system. NetworkSettings::no_ip_address_ui(ui); @@ -212,6 +235,92 @@ impl ContentContainer for NodeSetup { } impl NodeSetup { + /// Draw content to change chain data directory. + fn pick_data_dir_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(56.0); + + // Draw round background. + let bg_rect = rect.clone(); + let item_rounding = View::item_rounding(0, 1, false); + ui.painter().rect(bg_rect, + item_rounding, + Colors::fill(), + View::item_stroke(), + StrokeKind::Outside); + + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + self.pick_data_dir.ui(ui, cb, |path| { + Node::change_data_dir(path); + }); + View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || { + self.data_path_edit = NodeConfig::get_chain_data_path(); + // Show chain data path edit modal. + Modal::new(DATA_PATH_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network.node")) + .show(); + }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + let path = NodeConfig::get_chain_data_path(); + View::ellipsize_text(ui, path, 18.0, Colors::title(false)); + ui.add_space(1.0); + let desc = format!("{} {}", FOLDERS, t!("files_location")); + ui.label(RichText::new(desc).size(15.0).color(Colors::gray())); + ui.add_space(8.0); + }); + }); + }); + } + + /// Draw data path input [`Modal`] content. + fn data_path_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let on_save = |path: &String| { + Node::change_data_dir(path.clone()); + Modal::close(); + }; + ui.label(RichText::new(format!("{}:", t!("files_location"))) + .size(17.0) + .color(Colors::gray())); + ui.add_space(8.0); + + // Draw chain data path text edit. + let mut edit = TextEdit::new(Id::from(DATA_PATH_MODAL)).paste(); + edit.ui(ui, &mut self.data_path_edit, cb); + if edit.enter_pressed { + on_save(&self.data_path_edit); + } + 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), || { + Modal::close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(&self.data_path_edit); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } + /// Draw [`ChainTypes`] setup content. pub fn chain_type_ui(ui: &mut egui::Ui) { ui.vertical_centered(|ui| { diff --git a/src/gui/views/settings/tor.rs b/src/gui/views/settings/tor.rs index 64a355a4..8660f993 100644 --- a/src/gui/views/settings/tor.rs +++ b/src/gui/views/settings/tor.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fs; use egui::{Align, Id, Layout, RichText, StrokeKind}; use egui::os::OperatingSystem; use url::Url; @@ -412,6 +413,10 @@ impl TorSettingsContent { fn bridge_bin_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { let on_save = |c: &mut TorSettingsContent| { let bridge = TorConfig::get_bridge().unwrap(); + let exists = fs::exists(&c.bridge_bin_path_edit).unwrap_or_default(); + if !exists { + return; + } if bridge.binary_path() != c.bridge_bin_path_edit { TorBridge::save_bridge_bin_path(&bridge, c.bridge_bin_path_edit.clone()); c.settings_changed = true; diff --git a/src/gui/views/wallets/content.rs b/src/gui/views/wallets/content.rs index 694b9d39..7c9bbdf2 100644 --- a/src/gui/views/wallets/content.rs +++ b/src/gui/views/wallets/content.rs @@ -542,7 +542,7 @@ impl WalletsContent { ui.painter().rect(rect, rounding, bg, View::item_stroke(), StrokeKind::Outside); ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - if !wallet.is_open() { + if !wallet.is_open() && !wallet.files_moving() { // Show button to open closed wallet. View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || { self.show_opening_modal(wallet, None, cb); diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 2c520d22..62ba6a81 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -473,7 +473,7 @@ fn sync_ui(ui: &mut egui::Ui, wallet: &Wallet) -> bool { if wallet.is_repairing() && !wallet.sync_error() { sync_progress_ui(ui, wallet); return true; - } else if wallet.is_closing() { + } else if wallet.is_closing() || wallet.files_moving() { sync_progress_ui(ui, wallet); return true; } else if wallet.get_current_connection() == ConnectionMethod::Integrated { @@ -539,7 +539,9 @@ fn sync_progress_ui(ui: &mut egui::Ui, wallet: &Wallet) { let int_ready = Node::get_sync_status() == Some(SyncStatus::NoSync); let info_progress = wallet.info_sync_progress(); - if wallet.is_closing() { + if wallet.files_moving() { + t!("moving_files") + } else if wallet.is_closing() { t!("wallets.wallet_closing") } else if int_node && !int_ready { t!("wallets.node_loading", "settings" => GEAR_FINE) diff --git a/src/gui/views/wallets/wallet/settings/common.rs b/src/gui/views/wallets/wallet/settings/common.rs index 757a2de9..dbd3f645 100644 --- a/src/gui/views/wallets/wallet/settings/common.rs +++ b/src/gui/views/wallets/wallet/settings/common.rs @@ -12,14 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{Id, RichText}; +use eframe::emath::Align; +use eframe::epaint::StrokeKind; +use egui::{Id, Layout, RichText}; -use crate::gui::Colors; -use crate::gui::icons::{CLOCK_COUNTDOWN, PASSWORD, PENCIL}; +use crate::gui::icons::{CLOCK_COUNTDOWN, FOLDERS, FOLDER_USER, PASSWORD, PENCIL}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, TextEdit, View}; use crate::gui::views::types::ModalPosition; use crate::gui::views::wallets::wallet::types::WalletContentContainer; +use crate::gui::views::{FilePickContent, FilePickContentType, Modal, TextEdit, View}; +use crate::gui::Colors; use crate::wallet::Wallet; /// Common wallet settings content. @@ -34,6 +36,11 @@ pub struct CommonSettings { /// New wallet password [`Modal`] value. new_pass_edit: String, + /// Data path value value for [`Modal`]. + data_path_edit: String, + /// Button to pick directory for wallet data. + pick_data_dir: FilePickContent, + /// Minimum confirmations number value. min_confirmations_edit: String, } @@ -42,6 +49,8 @@ pub struct CommonSettings { const NAME_EDIT_MODAL: &'static str = "wallet_name_edit_modal"; /// Identifier for wallet password [`Modal`]. const PASS_EDIT_MODAL: &'static str = "wallet_pass_edit_modal"; +/// Identifier for wallet data path value [`Modal`]. +const DATA_PATH_MODAL: &'static str = "wallet_data_path"; /// Identifier for minimum confirmations [`Modal`]. const MIN_CONFIRMATIONS_EDIT_MODAL: &'static str = "wallet_min_conf_edit_modal"; @@ -50,6 +59,7 @@ impl WalletContentContainer for CommonSettings { vec![ NAME_EDIT_MODAL, PASS_EDIT_MODAL, + DATA_PATH_MODAL, MIN_CONFIRMATIONS_EDIT_MODAL ] } @@ -60,66 +70,25 @@ impl WalletContentContainer for CommonSettings { modal: &Modal, cb: &dyn PlatformCallbacks) { match modal.id { - NAME_EDIT_MODAL => { - self.name_modal_ui(ui, wallet, modal, cb); - } - PASS_EDIT_MODAL => { - self.pass_modal_ui(ui, wallet, modal, cb); - } - MIN_CONFIRMATIONS_EDIT_MODAL => { - self.min_conf_modal_ui(ui, wallet, modal, cb); - } + NAME_EDIT_MODAL => self.name_modal_ui(ui, wallet, modal, cb), + PASS_EDIT_MODAL => self.pass_modal_ui(ui, wallet, modal, cb), + DATA_PATH_MODAL => self.data_path_modal_ui(ui, wallet, cb), + MIN_CONFIRMATIONS_EDIT_MODAL => self.min_conf_modal_ui(ui, wallet, modal, cb), _ => {} } } - fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, _: &dyn PlatformCallbacks) { + fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { + ui.add_space(8.0); ui.vertical_centered(|ui| { let config = wallet.get_config(); // Show wallet name. - ui.add_space(2.0); - ui.label(RichText::new(t!("wallets.name")) - .size(16.0) - .color(Colors::gray())); - ui.add_space(2.0); - ui.label(RichText::new(&config.name) - .size(16.0) - .color(Colors::white_or_black(true))); - ui.add_space(8.0); - - // Show wallet name setup. - let name_text = format!("{} {}", PENCIL, t!("change")); - View::button(ui, name_text, Colors::white_or_black(false), || { - self.name_edit = config.name; - // Show wallet name modal. - Modal::new(NAME_EDIT_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.wallet")) - .show(); - }); - - ui.add_space(12.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); - ui.label(RichText::new(t!("wallets.pass")).size(16.0).color(Colors::gray())); - ui.add_space(6.0); - - // Show wallet password setup. - let pass_text = format!("{} {}", PASSWORD, t!("change")); - View::button(ui, pass_text, Colors::white_or_black(false), || { - // Setup modal values. - self.old_pass_edit = "".to_string(); - self.new_pass_edit = "".to_string(); - self.wrong_pass = false; - // Show wallet password modal. - Modal::new(PASS_EDIT_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.wallet")) - .show(); - }); - - ui.add_space(12.0); - View::horizontal_line(ui, Colors::item_stroke()); + self.name_ui(ui, config.name); + // Show data dir for desktop. + if View::is_desktop() { + ui.add_space(-4.0); + self.data_dir_ui(ui, wallet, cb); + } ui.add_space(6.0); ui.label(RichText::new(t!("wallets.min_tx_conf_count")).size(16.0).color(Colors::gray())); ui.add_space(6.0); @@ -135,7 +104,7 @@ impl WalletContentContainer for CommonSettings { .show(); }); - ui.add_space(12.0); + ui.add_space(8.0); // Setup ability to post wallet transactions with Dandelion. View::checkbox(ui, wallet.can_use_dandelion(), t!("wallets.use_dandelion"), || { @@ -144,7 +113,7 @@ impl WalletContentContainer for CommonSettings { ui.add_space(6.0); View::horizontal_line(ui, Colors::stroke()); - ui.add_space(6.0); + ui.add_space(4.0); }); } } @@ -156,12 +125,69 @@ impl Default for CommonSettings { wrong_pass: false, old_pass_edit: "".to_string(), new_pass_edit: "".to_string(), + data_path_edit: "".to_string(), + pick_data_dir: FilePickContent::new( + FilePickContentType::ItemButton(View::item_rounding(1, 2, true)) + ).no_parse().pick_folder(), min_confirmations_edit: "".to_string(), } } } impl CommonSettings { + /// Draw content to change wallet name and password. + fn name_ui(&mut self, ui: &mut egui::Ui, name: String) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(56.0); + + // Draw round background. + let bg_rect = rect.clone(); + let item_rounding = if View::is_desktop() { + View::item_rounding(0, 2, false) + } else { + View::item_rounding(0, 1, false) + }; + ui.painter().rect(bg_rect, + item_rounding, + Colors::fill(), + View::item_stroke(), + StrokeKind::Outside); + + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + View::item_button(ui, View::item_rounding(0, 2, true), PASSWORD, None, || { + self.old_pass_edit = "".to_string(); + self.new_pass_edit = "".to_string(); + self.wrong_pass = false; + // Show wallet password modal. + Modal::new(PASS_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.wallet")) + .show(); + }); + View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || { + self.name_edit = name.clone(); + // Show wallet name modal. + Modal::new(NAME_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.wallet")) + .show(); + }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + View::ellipsize_text(ui, name, 18.0, Colors::title(false)); + ui.add_space(1.0); + let desc = format!("{} {}", FOLDER_USER, t!("wallets.name").replace(":", "")); + ui.label(RichText::new(desc).size(15.0).color(Colors::gray())); + ui.add_space(8.0); + }); + }); + }); + } + /// Draw wallet name [`Modal`] content. fn name_modal_ui(&mut self, ui: &mut egui::Ui, @@ -307,6 +333,95 @@ impl CommonSettings { }); } + /// Draw content to change wallet data directory. + fn data_dir_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(56.0); + + // Draw round background. + let bg_rect = rect.clone(); + let item_rounding = View::item_rounding(1, 2, false); + ui.painter().rect(bg_rect, + item_rounding, + Colors::fill(), + View::item_stroke(), + StrokeKind::Outside); + + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + self.pick_data_dir.ui(ui, cb, |path| { + wallet.change_data_path(path); + }); + View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || { + self.data_path_edit = wallet.get_config().data_path.unwrap_or_default(); + // Show chain data path edit modal. + Modal::new(DATA_PATH_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.wallet")) + .show(); + }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + let path = wallet.get_config().data_path.unwrap_or_default(); + View::ellipsize_text(ui, path, 18.0, Colors::title(false)); + ui.add_space(1.0); + let desc = format!("{} {}", FOLDERS, t!("files_location")); + ui.label(RichText::new(desc).size(15.0).color(Colors::gray())); + ui.add_space(8.0); + }); + }); + }); + } + + /// Draw data path input [`Modal`] content. + fn data_path_modal_ui(&mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let on_save = |path: &String| { + wallet.change_data_path(path.clone()); + Modal::close(); + }; + ui.label(RichText::new(format!("{}:", t!("files_location"))) + .size(17.0) + .color(Colors::gray())); + ui.add_space(8.0); + + // Draw chain data path text edit. + let mut edit = TextEdit::new(Id::from(DATA_PATH_MODAL)).paste(); + edit.ui(ui, &mut self.data_path_edit, cb); + if edit.enter_pressed { + on_save(&self.data_path_edit); + } + 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), || { + Modal::close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(&self.data_path_edit); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } + /// Draw wallet name [`Modal`] content. fn min_conf_modal_ui(&mut self, ui: &mut egui::Ui, diff --git a/src/gui/views/wallets/wallet/settings/recovery.rs b/src/gui/views/wallets/wallet/settings/recovery.rs index f5b59faa..a35a8508 100644 --- a/src/gui/views/wallets/wallet/settings/recovery.rs +++ b/src/gui/views/wallets/wallet/settings/recovery.rs @@ -107,7 +107,7 @@ impl WalletContentContainer for RecoverySettings { format!("{} {}", LIFEBUOY, t!("wallets.recover")), Colors::green(), Colors::white_or_black(false), || { - wallet.delete_db(true); + wallet.delete_db(); }); ui.add_space(6.0); ui.label(RichText::new(t!("wallets.restore_wallet_desc")) diff --git a/src/gui/views/wallets/wallet/transport/content.rs b/src/gui/views/wallets/wallet/transport/content.rs index da5bd1b3..a686339d 100644 --- a/src/gui/views/wallets/wallet/transport/content.rs +++ b/src/gui/views/wallets/wallet/transport/content.rs @@ -119,7 +119,12 @@ impl WalletTransportContent { ui.vertical(|ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { // Show button to show QR code address. - View::item_button(ui, View::item_rounding(1, 2, true), QR_CODE, None, || { + let r = if awaiting_balance { + View::item_rounding(1, 3, true) + } else { + View::item_rounding(1, 2, true) + }; + View::item_button(ui, r, QR_CODE, None, || { self.qr_address_content = Some(QrCodeContent::new(addr.clone(), false) .with_max_size(320.0)); }); diff --git a/src/node/config.rs b/src/node/config.rs index 018002a4..1b6b85e2 100644 --- a/src/node/config.rs +++ b/src/node/config.rs @@ -304,6 +304,19 @@ impl NodeConfig { true } + /// Get chain data path. + pub fn get_chain_data_path() -> String { + let r_config = Settings::node_config_to_read(); + r_config.node.server.db_root.clone() + } + + /// Save chain data path. + pub fn save_chain_data_path(path: String) { + let mut w_config = Settings::node_config_to_update(); + w_config.node.server.db_root = path; + w_config.save(); + } + /// Get stratum server IP address and port. pub fn get_stratum_address() -> (String, String) { let r_config = Settings::node_config_to_read(); diff --git a/src/node/node.rs b/src/node/node.rs index c11e31a8..fd27a261 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -13,7 +13,7 @@ // limitations under the License. use std::{fs, thread}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; @@ -59,6 +59,8 @@ pub struct Node { exit_after_stop: AtomicBool, /// Flag to reset peers data and restart the [`Server`]. reset_peers: AtomicBool, + /// Flag to change data directory and restart the [`Server`]. + change_data_dir: AtomicBool, /// An error occurred on [`Server`] start. error: Arc>> @@ -77,6 +79,7 @@ impl Default for Node { start_stratum_needed: AtomicBool::new(false), error: Arc::new(RwLock::new(None)), reset_peers: AtomicBool::new(false), + change_data_dir: AtomicBool::new(false), } } } @@ -187,10 +190,10 @@ impl Node { /// Check if [`Server`] is not syncing (disabled or just running after synchronization). pub fn not_syncing() -> bool { - return match Node::get_sync_status() { + match Node::get_sync_status() { None => true, Some(ss) => ss == SyncStatus::NoSync - }; + } } /// Get synchronization status, empty when [`Server`] is not running. @@ -218,7 +221,7 @@ impl Node { let r_err = NODE_STATE.error.read(); if r_err.is_some() { let e = r_err.as_ref().unwrap(); - // Setup a flag to show an error to clean up data. + // Flag setup to show an error to clean up data. let store_err = match e { Error::Store(_) => true, Error::Chain(_) => true, @@ -228,7 +231,7 @@ impl Node { return Some(NodeError::Storage); } - // Setup a flag to show P2P or API server error. + // Flag setup to show P2P or API server error. let p2p_api_err = match e { Error::P2P(_) => Some(NodeError::P2P), Error::API(_) => Some(NodeError::API), @@ -238,7 +241,7 @@ impl Node { return p2p_api_err; } - // Setup a flag to show configuration error. + // Flag setup to show configuration error. let config_err = match e { Error::Configuration(_) => true, _ => false @@ -282,7 +285,7 @@ impl Node { NODE_STATE.restart_needed.store(false, Ordering::Relaxed); } Err(e) => { - // Setup an error. + // Set an error. { let mut w_err = NODE_STATE.error.write(); *w_err = Some(e); @@ -336,7 +339,7 @@ impl Node { } } Err(e) => { - // Setup an error. + // Set an error. { let mut w_err = NODE_STATE.error.write(); *w_err = Some(e); @@ -372,6 +375,48 @@ impl Node { } } + /// Change chain data directory. + pub fn change_data_dir(path: String) { + if Self::data_dir_changing() || NodeConfig::get_chain_data_path() == path { + return; + } + NODE_STATE.change_data_dir.store(true, Ordering::Relaxed); + thread::spawn(move || { + let running = Node::is_running(); + if running { + Node::stop(false); + // Wait node to stop before moving files. + while Node::is_running() { + thread::sleep(Self::STATS_UPDATE_DELAY); + } + } + let cfg_path = NodeConfig::get_chain_data_path(); + let old = Path::new(cfg_path.as_str()); + let new = Path::new(path.as_str()); + if !old.exists() { + NodeConfig::save_chain_data_path(path); + } else { + fs::create_dir_all(&new).unwrap_or_default(); + if let Ok(_) = fs::rename(old, new) { + NodeConfig::save_chain_data_path(path); + } else { + fs::remove_dir_all(old).unwrap(); + NodeConfig::save_chain_data_path(path); + } + } + NODE_STATE.change_data_dir.store(false, Ordering::Relaxed); + // Restart node after migration. + if running && !Node::is_stopping() { + Node::start(); + } + }); + } + + /// Check if chain data directory is changing. + pub fn data_dir_changing() -> bool { + NODE_STATE.change_data_dir.load(Ordering::Relaxed) + } + /// Clean-up [`Server`] data if server is not running. pub fn clean_up_data() { if Self::is_running() { @@ -412,14 +457,16 @@ impl Node { /// Get synchronization status i18n text. pub fn get_sync_status_text() -> String { + if Node::data_dir_changing() { + return t!("moving_files"); + } + if Node::is_stopping() { return t!("sync_status.shutdown"); }; - if Node::is_starting() { return t!("sync_status.initial"); }; - if Node::is_restarting() { return t!("sync_status.node_restarting"); } diff --git a/src/wallet/config.rs b/src/wallet/config.rs index d165ad4e..1d7f4371 100644 --- a/src/wallet/config.rs +++ b/src/wallet/config.rs @@ -48,14 +48,18 @@ pub struct WalletConfig { pub api_port: Option, /// Delay in blocks before another transaction broadcasting attempt. pub tx_broadcast_timeout: Option, + + /// Wallet data directory path. + pub data_path: Option, + + /// Config version. + ver: Option } /// Base wallets directory name. const BASE_DIR_NAME: &'static str = "wallets"; /// Base wallets directory name. const DB_DIR_NAME: &'static str = "db"; -/// Wallet data directory name. -const DATA_DIR_NAME: &'static str = "wallet_data"; /// Wallet configuration file name. const CONFIG_FILE_NAME: &'static str = "grim-wallet.toml"; /// Slatepacks directory name. @@ -66,7 +70,12 @@ const SEED_FILE: &str = "wallet.seed"; /// Default value of minimal amount of confirmations. const MIN_CONFIRMATIONS_DEFAULT: u64 = 10; +const WALLET_CONFIG_VERSION: i32 = 1; + impl WalletConfig { + /// Wallet data directory name. + pub const DATA_DIR_NAME: &'static str = "wallet_data"; + /// Default account name value. pub const DEFAULT_ACCOUNT_LABEL: &'static str = "default"; @@ -94,6 +103,8 @@ impl WalletConfig { enable_tor_listener: Some(false), api_port: Some(rand::rng().random_range(10000..30000)), tx_broadcast_timeout: Some(Self::BROADCASTING_TIMEOUT_DEFAULT), + data_path: Some(Self::wallet_path(id.to_string())), + ver: Some(WALLET_CONFIG_VERSION), }; Settings::write_to_file(&config, config_path); config @@ -103,7 +114,8 @@ impl WalletConfig { pub fn load(wallet_dir: PathBuf) -> Option { let mut config_path: PathBuf = wallet_dir.clone(); config_path.push(CONFIG_FILE_NAME); - if let Ok(config) = Settings::read_from_file::(config_path) { + if let Ok(mut config) = Settings::read_from_file::(config_path) { + config.migrate(); return Some(config) } None @@ -169,24 +181,32 @@ impl WalletConfig { config_path } - /// Get current wallet path. - pub fn get_wallet_path(&self) -> String { + /// Get wallet path from provided identifier. + fn wallet_path(id: String) -> String { let chain_type = AppConfig::chain_type(); let mut data_path = Self::get_base_path(chain_type); - data_path.push(self.id.to_string()); + data_path.push(id); data_path.to_str().unwrap().to_string() } + /// Get current wallet path. + pub fn get_wallet_path(&self) -> String { + Self::wallet_path(self.id.to_string()) + } + /// Get wallet data path. pub fn get_data_path(&self) -> String { - let mut data_path = PathBuf::from(self.get_wallet_path()); - data_path.push(DATA_DIR_NAME); - data_path.to_str().unwrap().to_string() + if let Some(path) = &self.data_path { + path.clone() + } else { + self.get_wallet_path() + } } /// Get wallet seed path. pub fn seed_path(&self) -> String { let mut path = PathBuf::from(self.get_data_path()); + path.push(Self::DATA_DIR_NAME); path.push(SEED_FILE); path.to_str().unwrap().to_string() } @@ -194,13 +214,15 @@ impl WalletConfig { /// Get wallet database data path. pub fn get_db_path(&self) -> String { let mut path = PathBuf::from(self.get_data_path()); + path.push(Self::DATA_DIR_NAME); path.push(DB_DIR_NAME); path.to_str().unwrap().to_string() } /// Get Slatepack file path for transaction. pub fn get_tx_slate_path(&self, tx: &WalletTransaction) -> PathBuf { - let mut path = PathBuf::from(self.get_wallet_path()); + let mut path = PathBuf::from(self.get_data_path()); + path.push(Self::DATA_DIR_NAME); path.push(SLATEPACKS_DIR_NAME); if !path.exists() { let _ = fs::create_dir_all(path.clone()); @@ -212,7 +234,8 @@ impl WalletConfig { /// Get Slatepack file path for Slate. pub fn get_slate_path(&self, slate: &Slate) -> PathBuf { - let mut path = PathBuf::from(self.get_wallet_path()); + let mut path = PathBuf::from(self.get_data_path()); + path.push(Self::DATA_DIR_NAME); path.push(SLATEPACKS_DIR_NAME); if !path.exists() { let _ = fs::create_dir_all(path.clone()); @@ -231,4 +254,28 @@ impl WalletConfig { } path.to_str().unwrap().to_string() } + + /// Check config version to migrate if needed. + fn migrate(&mut self) { + match self.ver { + None => { + // Migrate Slatepack data. + let mut old_slate_path = PathBuf::from(self.get_wallet_path()); + old_slate_path.push(SLATEPACKS_DIR_NAME); + if old_slate_path.exists() { + let mut new_slate_path = PathBuf::from(self.get_data_path()); + new_slate_path.push(SLATEPACKS_DIR_NAME); + let _ = fs::rename(&old_slate_path, &new_slate_path); + } + // Write data path to config. + if self.data_path.is_none() { + self.data_path = Some(self.get_wallet_path()); + } + // Migrate to 1st version. + self.ver = Some(1); + } + Some(_) => {} + } + self.save(); + } } \ No newline at end of file diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 93a515b7..22c57fe2 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -93,7 +93,7 @@ pub struct Wallet { /// Running wallet foreign API server and port. foreign_api_server: Arc>>, - /// Running wallet foreign API server and port. + /// Wallet secret key for transport service. secret_key: Arc>>, /// Flag to check if wallet repairing and restoring missing outputs is needed. @@ -101,6 +101,9 @@ pub struct Wallet { /// Wallet repair progress in percents. repair_progress: Arc, + /// Flag to check if wallet files are moving. + files_moving: Arc, + /// Flag to check if Slatepack message file is opening. message_opening: Arc, @@ -142,6 +145,7 @@ impl Wallet { syncing: Arc::new(AtomicBool::new(false)), repair_needed: Arc::new(AtomicBool::new(false)), repair_progress: Arc::new(AtomicU8::new(0)), + files_moving: Arc::new(AtomicBool::new(false)), message_opening: Arc::new(AtomicBool::from(false)), send_creating: Arc::new(AtomicBool::new(false)), fee_calculating: Arc::new(AtomicU8::new(0)), @@ -190,7 +194,7 @@ impl Wallet { /// Initialize [`Wallet`] from provided data path. pub fn init(data_path: PathBuf) -> Option { - let wallet_config = WalletConfig::load(data_path.clone()); + let wallet_config = WalletConfig::load(data_path); if let Some(config) = wallet_config { return Some(Wallet::new(config)); } @@ -284,7 +288,7 @@ impl Wallet { let mut wallet = Box::new(DefaultWalletImpl::<'static, C>::new(node_client).unwrap()) as Box>; let lc = wallet.lc_provider()?; - lc.set_top_level_directory(config.get_wallet_path().as_str())?; + lc.set_top_level_directory(config.get_data_path().as_str())?; Ok(Arc::new(Mutex::new(wallet))) } @@ -1089,28 +1093,73 @@ impl Wallet { self.repair_progress.load(Ordering::Relaxed) } - /// Deleting wallet database files. - pub fn delete_db(&self, reopen: bool) { - let wallet_delete = self.clone(); + /// Change wallet data path, migrating all files to new directory. + pub fn change_data_path(&self, path: String) { + let wallet = self.clone(); + wallet.files_moving.store(true, Ordering::Relaxed); // Close wallet if open. if self.is_open() { self.close(); } thread::spawn(move || { // Wait wallet to be closed. - if wallet_delete.is_open() { - thread::sleep(Duration::from_millis(300)); + while wallet.is_open() || wallet.syncing() { + thread::sleep(Duration::from_millis(100)); + } + // Move wallet db files. + if let Some(old_path) = wallet.get_config().data_path { + let mut old = PathBuf::from(old_path.as_str()); + old.push(WalletConfig::DATA_DIR_NAME); + let mut new = PathBuf::from(path.as_str()); + new.push(WalletConfig::DATA_DIR_NAME); + if old.exists() { + fs::create_dir_all(&new).unwrap_or_default(); + if let Ok(_) = fs::rename(old.as_path(), new.as_path()) { + // Save new path to config. + let mut w_config = wallet.config.write(); + w_config.data_path = Some(path); + w_config.save(); + } + } + } + wallet.files_moving.store(false, Ordering::Relaxed); + // Mark wallet to reopen. + if !wallet.is_open() { + wallet.set_reopen(true); + } + }); + } + + /// Deleting wallet database files. + pub fn delete_db(&self) { + let wallet = self.clone(); + wallet.files_moving.store(true, Ordering::Relaxed); + // Close wallet if open. + if self.is_open() { + self.close(); + } + thread::spawn(move || { + // Wait wallet to be closed. + while wallet.is_open() || wallet.syncing() { + thread::sleep(Duration::from_millis(100)); } // Remove wallet db files. - let _ = fs::remove_dir_all(wallet_delete.get_config().get_db_path()); - // Start sync to close thread. - wallet_delete.sync(); - wallet_delete.repair(); + let _ = fs::remove_dir_all(wallet.get_config().get_db_path()); + wallet.files_moving.store(false, Ordering::Relaxed); + // Mark wallet to repair. + wallet.repair(); // Mark wallet to reopen. - wallet_delete.set_reopen(reopen); + if !wallet.is_open() { + wallet.set_reopen(true); + } }); } + /// Check if data files are moving. + pub fn files_moving(&self) -> bool { + self.files_moving.load(Ordering::Relaxed) + } + /// Get recovery phrase. pub fn get_recovery(&self, password: String) -> Result { let r_inst = self.instance.as_ref().read();