From d3e81826e832ab04cf49a12c6fcd15c980f04828 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 2 Jun 2023 02:05:34 +0300 Subject: [PATCH] ui: modals, exit modal, app exit logic on Android, refactor display cutouts, optimize translations --- .../mw/gri/android/BackgroundService.java | 5 +- .../java/mw/gri/android/MainActivity.java | 37 ++- locales/en.yml | 60 ++-- locales/ru.yml | 60 ++-- src/grim.rs | 5 + src/gui/app.rs | 106 +++++-- src/gui/mod.rs | 12 +- src/gui/navigator.rs | 209 ++++++++++++++ src/gui/platform/android/mod.rs | 21 +- src/gui/platform/mod.rs | 9 +- src/gui/screens/account.rs | 2 +- src/gui/screens/accounts.rs | 74 ++--- src/gui/screens/mod.rs | 2 - src/gui/screens/navigator.rs | 78 ------ src/gui/screens/root.rs | 48 ++-- src/gui/views/mod.rs | 17 +- src/gui/views/modal.rs | 260 ++++++++++++++++++ src/gui/views/network.rs | 112 ++++---- src/gui/views/network_metrics.rs | 25 +- src/gui/views/network_node.rs | 34 +-- ...{network_tuning.rs => network_settings.rs} | 0 src/gui/views/progress_loading.rs | 39 +++ src/gui/views/title_panel.rs | 87 ++---- src/gui/views/views.rs | 104 +++++-- src/node/mod.rs | 2 +- src/node/node.rs | 127 +++++---- 26 files changed, 1051 insertions(+), 484 deletions(-) create mode 100644 src/gui/navigator.rs delete mode 100644 src/gui/screens/navigator.rs create mode 100644 src/gui/views/modal.rs rename src/gui/views/{network_tuning.rs => network_settings.rs} (100%) create mode 100644 src/gui/views/progress_loading.rs diff --git a/app/src/main/java/mw/gri/android/BackgroundService.java b/app/src/main/java/mw/gri/android/BackgroundService.java index 1be3aa6..3b6b013 100644 --- a/app/src/main/java/mw/gri/android/BackgroundService.java +++ b/app/src/main/java/mw/gri/android/BackgroundService.java @@ -88,15 +88,14 @@ public class BackgroundService extends Service { public void onStop() { mStopped = true; + stopForeground(Service.STOP_FOREGROUND_REMOVE); + if (mWakeLock.isHeld()) { mWakeLock.release(); mWakeLock = null; } mHandler.removeCallbacks(mUpdateSyncStatus); - - stopForeground(Service.STOP_FOREGROUND_REMOVE); - stopSelf(); } public static void start(Context context) { diff --git a/app/src/main/java/mw/gri/android/MainActivity.java b/app/src/main/java/mw/gri/android/MainActivity.java index d6ebb08..1015845 100644 --- a/app/src/main/java/mw/gri/android/MainActivity.java +++ b/app/src/main/java/mw/gri/android/MainActivity.java @@ -9,6 +9,8 @@ import android.view.KeyEvent; import android.view.OrientationEventListener; import com.google.androidgamesdk.GameActivity; +import java.util.concurrent.atomic.AtomicBoolean; + public class MainActivity extends GameActivity { static { @@ -17,13 +19,15 @@ public class MainActivity extends GameActivity { @Override protected void onCreate(Bundle savedInstanceState) { + // Setup HOME environment variable for native code configurations. try { Os.setenv("HOME", getExternalFilesDir("").getPath(), true); } catch (ErrnoException e) { throw new RuntimeException(e); } - super.onCreate(savedInstanceState); + super.onCreate(null); + // Callback to update display cutouts at native code. OrientationEventListener orientationEventListener = new OrientationEventListener(this, SensorManager.SENSOR_DELAY_GAME) { @Override @@ -34,9 +38,10 @@ public class MainActivity extends GameActivity { if (orientationEventListener.canDetectOrientation()) { orientationEventListener.enable(); } - onDisplayCutoutsChanged(Utils.getDisplayCutouts(MainActivity.this)); + onDisplayCutoutsChanged(Utils.getDisplayCutouts(this)); - BackgroundService.start(getApplicationContext()); + // Start notification service. + BackgroundService.start(this); } native void onDisplayCutoutsChanged(int[] cutouts); @@ -52,23 +57,35 @@ public class MainActivity extends GameActivity { public native void onBackButtonPress(); + private boolean mManualExit; + private final AtomicBoolean mActivityDestroyed = new AtomicBoolean(false); + @Override protected void onDestroy() { if (!mManualExit) { - BackgroundService.stop(getApplicationContext()); - // Temporary fix to prevent app hanging when closed from recent apps - Process.killProcess(Process.myPid()); + onTermination(); } + // Temp fix: kill process after 3 seconds to prevent app hanging at next launch + new Thread(() -> { + try { + Thread.sleep(3000); + if (!mActivityDestroyed.get()) { + Process.killProcess(Process.myPid()); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }).start(); super.onDestroy(); + mActivityDestroyed.set(true); } - - private boolean mManualExit = false; - // Called from native code public void onExit() { mManualExit = true; - BackgroundService.stop(getApplicationContext()); + BackgroundService.stop(this); finish(); } + + public native void onTermination(); } \ No newline at end of file diff --git a/locales/en.yml b/locales/en.yml index 371b3ee..c92a05a 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -1,22 +1,13 @@ -accounts: Accounts -integrated_node: Integrated Node -metrics: Metrics -settings: Settings -server_restarting: Server is restarting -server_down: Server is down -header: Header -block: Block -hash: Hash -height: Height -difficulty: Difficulty -time_utc: Time (UTC) -transactions: Transactions -main_pool: Main pool -stem_pool: Stem pool -data: Data -size: Size (GB) -peers: Peers +screen_accounts: + title: Accounts +network: + node: Integrated node + metrics: Metrics + mining: Mining + settings: Server settings sync_status: + server_restarting: Server is restarting + server_down: Server is down initial: Server is starting no_sync: Server is running awaiting_peers: Waiting for peers @@ -30,10 +21,29 @@ sync_status: tx_hashset_save: Finalizing chain state body_sync: Downloading blocks body_sync_percent: 'Downloading blocks: %{percent}%' - shutdown: Shutting down -emission: Emission -inflation: Inflation -supply: Supply -block_time: Block time -reward: Reward -difficulty_at_window: 'Difficulty at window %{size}' \ No newline at end of file + shutdown: Server is shutting down +network_node: + header: Header + block: Block + hash: Hash + height: Height + difficulty: Difficulty + time_utc: Time (UTC) + transactions: Transactions + main_pool: Main pool + stem_pool: Stem pool + data: Data + size: Size (GB) + peers: Peers +network_metrics: + emission: Emission + inflation: Inflation + supply: Supply + block_time: Block time + reward: Reward + difficulty_window: 'Difficulty at window %{size}' +modal: + cancel: Cancel +modal_exit: + description: Are you sure you want to quit the app? + exit: Exit \ No newline at end of file diff --git a/locales/ru.yml b/locales/ru.yml index b241c3c..6403468 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -1,22 +1,13 @@ -accounts: Аккаунты -integrated_node: Встроенный узел -metrics: Метрики -settings: Настройки -server_restarting: Сервер перезапускается -server_down: Сервер выключен -header: Заголовок -block: Блок -hash: Хэш -height: Высота -difficulty: Сложность -time_utc: Время (UTC) -transactions: Транзакции -main_pool: Основной пул -stem_pool: Stem пул -data: Данные -size: Размер (ГБ) -peers: Пиры +screen_accounts: + title: Аккаунты +network: + node: Встроенный узел + metrics: Метрики + mining: Майнинг + settings: Настройки сервера sync_status: + server_restarting: Сервер перезапускается + server_down: Сервер выключен initial: Запуск сервера no_sync: Сервер запущен awaiting_peers: Ожидание пиров @@ -30,10 +21,29 @@ sync_status: tx_hashset_save: Сохранение состояния цепи body_sync: Загрузка блоков body_sync_percent: 'Загрузка блоков: %{percent}%' - shutdown: Выключение -emission: Эмиссия -inflation: Инфляция -supply: Предложение -block_time: Время блока -reward: Награда -difficulty_at_window: 'Сложность в окне %{size}' \ No newline at end of file + shutdown: Выключение сервера +network_node: + header: Заголовок + block: Блок + hash: Хэш + height: Высота + difficulty: Сложность + time_utc: Время (UTC) + transactions: Транзакции + main_pool: Основной пул + stem_pool: Stem пул + data: Данные + size: Размер (ГБ) + peers: Пиры +network_metrics: + emission: Эмиссия + inflation: Инфляция + supply: Предложение + block_time: Время блока + reward: Награда + difficulty_window: 'Сложность в окне %{size}' +modal: + cancel: Отмена +modal_exit: + description: Вы уверены, что хотите выйти из приложения? + exit: Выход \ No newline at end of file diff --git a/src/grim.rs b/src/grim.rs index 19fc5c2..b490b3a 100644 --- a/src/grim.rs +++ b/src/grim.rs @@ -13,11 +13,13 @@ // limitations under the License. use eframe::{AppCreator, NativeOptions, Renderer, Theme}; +use grin_core::global::ChainTypes; use log::LevelFilter::Info; #[cfg(target_os = "android")] use winit::platform::android::activity::AndroidApp; use crate::gui::PlatformApp; +use crate::node::Node; #[allow(dead_code)] #[cfg(target_os = "android")] @@ -67,6 +69,9 @@ fn start(mut options: NativeOptions, app_creator: AppCreator) { setup_i18n(); + //TODO: Take network type and server check from config + Node::start(ChainTypes::Mainnet); + eframe::run_native("Grim", options, app_creator); } diff --git a/src/gui/app.rs b/src/gui/app.rs index e01bc44..b8ed59f 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -12,53 +12,109 @@ // See the License for the specific language governing permissions and // limitations under the License. -use eframe::epaint::Stroke; -use egui::{Context, Frame}; +use egui::{Context, Stroke, Widget}; +use egui::os::OperatingSystem; use egui::style::Margin; use crate::gui::colors::COLOR_LIGHT; +use crate::gui::Navigator; use crate::gui::platform::PlatformCallbacks; use crate::gui::screens::Root; +use crate::gui::views::{Modal, ModalId, ModalLocation, ProgressLoading, View}; +use crate::node::Node; pub struct PlatformApp { pub(crate) app: App, pub(crate) platform: Platform, } +#[derive(Default)] pub struct App { root: Root, -} - -impl Default for App { - fn default() -> Self { - Self { - root: Root::default(), - } - } + show_exit_progress: bool } impl App { pub fn ui(&mut self, ctx: &Context, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { - let Self { root } = self; + let modal_open = Navigator::is_modal_open(ModalLocation::Global); egui::CentralPanel::default() - .frame(Frame { - inner_margin: Margin::same(0.0), - outer_margin: Margin::same(0.0), - stroke: Stroke::NONE, + .frame(egui::Frame { fill: COLOR_LIGHT, .. Default::default() }) .show(ctx, |ui| { - root.ui(ui, frame, cb) - }); + if modal_open { + self.show_global_modal(ui, frame, cb); + } + self.root.ui(ui, frame, cb); + }).response.enabled = !modal_open; + } + + fn show_global_modal(&mut self, + ui: &mut egui::Ui, + frame: &mut eframe::Frame, + cb: &dyn PlatformCallbacks) { + let location = ModalLocation::Global; + Navigator::modal_ui(ui, frame, location, |ui, frame, modal| { + match modal.id { + ModalId::Exit => { + if self.show_exit_progress { + if !Node::is_running() { + Self::exit(frame, cb); + } else { + ui.add_space(10.0); + let text = Node::get_sync_status_text(Node::get_sync_status()); + ProgressLoading::new(text).ui(ui); + ui.add_space(10.0); + } + } else { + ui.add_space(8.0); + ui.vertical_centered(|ui| { + ui.label(t!("modal_exit.description")); + }); + ui.add_space(10.0); + // Setup spacing between buttons + ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0); + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::modal_button(ui, t!("modal_exit.exit"), || { + if !Node::is_running() { + Self::exit(frame, cb); + modal.close(); + } else { + modal.disable_closing(); + Node::stop(); + self.show_exit_progress = true; + } + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::modal_button(ui, t!("modal.cancel"), || { + modal.close(); + }); + }); + }); + ui.add_space(6.0); + } + } + } + }); + } + + fn exit(frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { + match OperatingSystem::from_target_os() { + OperatingSystem::Android => { + cb.exit(); + } + OperatingSystem::IOS => { + //TODO: exit on iOS + } + OperatingSystem::Nix | OperatingSystem::Mac | OperatingSystem::Windows => { + frame.close(); + } + // Web + OperatingSystem::Unknown => {} + } } } -pub fn is_dual_panel_mode(frame: &mut eframe::Frame) -> bool { - is_landscape(frame) && frame.info().window_info.size.x > 400.0 -} - -pub fn is_landscape(frame: &mut eframe::Frame) -> bool { - return frame.info().window_info.size.x > frame.info().window_info.size.y -} - diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 1b3fd8b..b9b4e9f 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -13,14 +13,14 @@ // limitations under the License. -pub use app::App; -pub use app::PlatformApp; +mod app; +pub use app::{App, PlatformApp}; + +mod navigator; +pub use navigator::Navigator; pub mod platform; pub mod screens; pub mod views; pub mod icons; -pub mod colors; - -mod app; - +pub mod colors; \ No newline at end of file diff --git a/src/gui/navigator.rs b/src/gui/navigator.rs new file mode 100644 index 0000000..ba07813 --- /dev/null +++ b/src/gui/navigator.rs @@ -0,0 +1,209 @@ +// Copyright 2023 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 std::collections::BTreeSet; +use std::sync::{RwLock, RwLockWriteGuard}; +use std::sync::atomic::{AtomicBool, Ordering}; + +use lazy_static::lazy_static; + +use crate::gui::screens::ScreenId; +use crate::gui::views::{Modal, ModalId, ModalLocation}; + +lazy_static! { + /// Static [Navigator] state to be accessible from anywhere. + static ref NAVIGATOR_STATE: RwLock = RwLock::new(Navigator::default()); +} + +/// Logic of navigation for UI, stores screen identifiers stack, open modals and side panel state. +pub struct Navigator { + /// Screen identifiers in navigation stack. + screen_stack: BTreeSet, + /// Indicator if side panel is open. + side_panel_open: AtomicBool, + /// Modal state to show globally above panel and screen. + global_modal: Option, + /// Modal state to show on the side panel. + side_panel_modal: Option, + /// Modal state to show on the screen. + screen_modal: Option, +} + +impl Default for Navigator { + fn default() -> Self { + Self { + screen_stack: BTreeSet::new(), + side_panel_open: AtomicBool::new(false), + global_modal: None, + side_panel_modal: None, + screen_modal: None, + } + } +} + +impl Navigator { + /// Initialize navigation from provided [ScreenId]. + pub fn init(from: ScreenId) { + let mut w_nav = NAVIGATOR_STATE.write().unwrap(); + w_nav.screen_stack.clear(); + w_nav.screen_stack.insert(from); + } + + /// Check if provided [ScreenId] is current. + pub fn is_current(id: &ScreenId) -> bool { + let r_nav = NAVIGATOR_STATE.read().unwrap(); + r_nav.screen_stack.last().unwrap() == id + } + + /// Navigate to screen with provided [ScreenId]. + pub fn to(id: ScreenId) { + NAVIGATOR_STATE.write().unwrap().screen_stack.insert(id); + } + + /// Go back at navigation stack, close showing modals first. + pub fn back() { + let mut w_nav = NAVIGATOR_STATE.write().unwrap(); + + // If global Modal is showing and closeable, remove it from Navigator. + if w_nav.global_modal.is_some() { + let global_modal = w_nav.global_modal.as_ref().unwrap(); + if global_modal.is_closeable() { + w_nav.global_modal = None; + } + return; + } + + // If side panel Modal is showing and closeable, remove it from Navigator. + if w_nav.side_panel_modal.is_some() { + let side_panel_modal = w_nav.side_panel_modal.as_ref().unwrap(); + if side_panel_modal.is_closeable() { + w_nav.side_panel_modal = None; + } + return; + } + + // If screen Modal is showing and closeable, remove it from Navigator. + if w_nav.screen_modal.is_some() { + let screen_modal = w_nav.screen_modal.as_ref().unwrap(); + if screen_modal.is_closeable() { + w_nav.screen_modal = None; + } + return; + } + + // Go back at screen stack or show exit confirmation Modal. + if w_nav.screen_stack.len() > 1 { + w_nav.screen_stack.pop_last(); + } else { + Self::open_exit_modal_nav(w_nav); + } + } + + /// Open exit confirmation [Modal]. + pub fn open_exit_modal() { + let w_nav = NAVIGATOR_STATE.write().unwrap(); + Self::open_exit_modal_nav(w_nav); + } + + /// Open exit confirmation [Modal] with provided [NAVIGATOR_STATE] lock. + fn open_exit_modal_nav(mut w_nav: RwLockWriteGuard) { + let m = Modal::new(ModalId::Exit, ModalLocation::Global).title(t!("modal_exit.exit")); + w_nav.global_modal = Some(m); + } + + /// Open [Modal] at specified location. + pub fn open_modal(modal: Modal) { + let mut w_nav = NAVIGATOR_STATE.write().unwrap(); + match modal.location { + ModalLocation::Global => { + w_nav.global_modal = Some(modal); + } + ModalLocation::SidePanel => { + w_nav.side_panel_modal = Some(modal); + } + ModalLocation::Screen => { + w_nav.screen_modal = Some(modal); + } + } + } + + /// Check if [Modal] is open at specified location and remove it from [Navigator] if closed. + pub fn is_modal_open(location: ModalLocation) -> bool { + // Check if Modal is showing. + { + let r_nav = NAVIGATOR_STATE.read().unwrap(); + let showing = match location { + ModalLocation::Global => { r_nav.global_modal.is_some() } + ModalLocation::SidePanel => { r_nav.side_panel_modal.is_some() } + ModalLocation::Screen => { r_nav.screen_modal.is_some() } + }; + if !showing { + return false; + } + } + + // Check if Modal is open. + let mut is_open = false; + { + let r_nav = NAVIGATOR_STATE.read().unwrap(); + is_open = match location { + ModalLocation::Global => { r_nav.global_modal.as_ref().unwrap().is_open() } + ModalLocation::SidePanel => { r_nav.side_panel_modal.as_ref().unwrap().is_open() } + ModalLocation::Screen => {r_nav.screen_modal.as_ref().unwrap().is_open() } + }; + } + + // If Modal is not open, remove it from navigator state. + if !is_open { + let mut w_nav = NAVIGATOR_STATE.write().unwrap(); + match location { + ModalLocation::Global => { w_nav.global_modal = None } + ModalLocation::SidePanel => { w_nav.side_panel_modal = None } + ModalLocation::Screen => { w_nav.screen_modal = None } + } + return false; + } + true + } + + /// Show [Modal] with provided location at app UI. + pub fn modal_ui(ui: &mut egui::Ui, + frame: &mut eframe::Frame, + location: ModalLocation, + add_content: impl FnOnce(&mut egui::Ui, &mut eframe::Frame, &Modal)) { + + let r_nav = NAVIGATOR_STATE.read().unwrap(); + let modal = match location { + ModalLocation::Global => { &r_nav.global_modal } + ModalLocation::SidePanel => { &r_nav.side_panel_modal } + ModalLocation::Screen => { &r_nav.screen_modal } + }; + if modal.is_some() { + modal.as_ref().unwrap().ui(ui, frame, add_content); + } + } + + /// Change state of side panel to opposite. + pub fn toggle_side_panel() { + let r_nav = NAVIGATOR_STATE.read().unwrap(); + let is_open = r_nav.side_panel_open.load(Ordering::Relaxed); + r_nav.side_panel_open.store(!is_open, Ordering::Relaxed); + } + + /// Check if side panel is open. + pub fn is_side_panel_open() -> bool { + let r_nav = NAVIGATOR_STATE.read().unwrap(); + r_nav.side_panel_open.load(Ordering::Relaxed) + } +} diff --git a/src/gui/platform/android/mod.rs b/src/gui/platform/android/mod.rs index 8b3017c..a96ee98 100644 --- a/src/gui/platform/android/mod.rs +++ b/src/gui/platform/android/mod.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; +use std::sync::atomic::{AtomicI32, Ordering}; use eframe::epaint::Stroke; use lazy_static::lazy_static; use winit::platform::android::activity::AndroidApp; @@ -34,22 +34,33 @@ impl Android { } impl PlatformCallbacks for Android { - fn show_keyboard(&mut self) { + fn show_keyboard(&self) { self.android_app.show_soft_input(true); } - fn hide_keyboard(&mut self) { + fn hide_keyboard(&self) { self.android_app.hide_soft_input(true); } - fn copy_string_to_buffer(&mut self, data: String) { + fn copy_string_to_buffer(&self, data: String) { //TODO } - fn get_string_from_buffer(&mut self) -> String { + fn get_string_from_buffer(&self) -> String { //TODO "".to_string() } + + fn exit(&self) { + use jni::objects::{JObject}; + + let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); + let mut env = vm.attach_current_thread().unwrap(); + let activity = unsafe { + JObject::from_raw(self.android_app.activity_as_ptr() as jni::sys::jobject) + }; + env.call_method(activity, "onExit", "()V", &[]).unwrap(); + } } //TODO diff --git a/src/gui/platform/mod.rs b/src/gui/platform/mod.rs index f4e13d4..b76133f 100644 --- a/src/gui/platform/mod.rs +++ b/src/gui/platform/mod.rs @@ -21,8 +21,9 @@ pub mod platform; pub mod app; pub trait PlatformCallbacks { - fn show_keyboard(&mut self); - fn hide_keyboard(&mut self); - fn copy_string_to_buffer(&mut self, data: String); - fn get_string_from_buffer(&mut self) -> String; + fn show_keyboard(&self); + fn hide_keyboard(&self); + fn copy_string_to_buffer(&self, data: String); + fn get_string_from_buffer(&self) -> String; + fn exit(&self); } \ No newline at end of file diff --git a/src/gui/screens/account.rs b/src/gui/screens/account.rs index d12fa71..aa0e5bd 100644 --- a/src/gui/screens/account.rs +++ b/src/gui/screens/account.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::gui::platform::PlatformCallbacks; -use crate::gui::screens::{Navigator, ScreenId}; +use crate::gui::screens::ScreenId; pub struct Account { diff --git a/src/gui/screens/accounts.rs b/src/gui/screens/accounts.rs index d9c11d8..9c16cc2 100644 --- a/src/gui/screens/accounts.rs +++ b/src/gui/screens/accounts.rs @@ -14,48 +14,56 @@ use egui::Frame; -use crate::gui::app::is_dual_panel_mode; -use crate::gui::icons::{ARROW_CIRCLE_LEFT, GEAR_SIX, GLOBE}; +use crate::gui::icons::{ARROW_CIRCLE_LEFT, GLOBE, PLUS}; +use crate::gui::Navigator; use crate::gui::platform::PlatformCallbacks; -use crate::gui::screens::{Navigator, Screen, ScreenId}; +use crate::gui::screens::{Screen, ScreenId}; use crate::gui::views::{TitlePanel, TitlePanelAction, View}; -#[derive(Default)] -pub struct Accounts {} +pub struct Accounts { + title: String +} + +impl Default for Accounts { + fn default() -> Self { + Self { + title: t!("screen_accounts.title").to_uppercase(), + } + } +} impl Screen for Accounts { fn id(&self) -> ScreenId { ScreenId::Accounts } - fn ui(&mut self, - ui: &mut egui::Ui, - frame: &mut eframe::Frame, - cb: &dyn PlatformCallbacks) { - TitlePanel::new(t!("accounts")) - .left_action( - if !is_dual_panel_mode(frame) { - TitlePanelAction::new(GLOBE.into(), || { - Navigator::toggle_side_panel(); - }) - } else { - None - } - ).right_action(TitlePanelAction::new(GEAR_SIX.into(), || { - //TODO: settings - })).ui(ui); + fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { + let Self { title } = self; - egui::CentralPanel::default().frame(Frame { - stroke: View::DEFAULT_STROKE, - .. Default::default() - }).show_inside(ui, |ui| { - ui.label(format!("{}Here we go 10000 ツ", ARROW_CIRCLE_LEFT)); - if ui.button("TEST").clicked() { - Navigator::to(ScreenId::Account) - }; - if ui.button(format!("{}BACK ", ARROW_CIRCLE_LEFT)).clicked() { - Navigator::to(ScreenId::Account) - }; - }); + TitlePanel::new(title) + .ui(if !View::is_dual_panel_mode(frame) { + TitlePanelAction::new(GLOBE, || { + Navigator::toggle_side_panel(); + }) + } else { + None + }, TitlePanelAction::new(PLUS, || { + //TODO: add account + }), ui); + + egui::CentralPanel::default() + .frame(Frame { + stroke: View::DEFAULT_STROKE, + ..Default::default() + }) + .show_inside(ui, |ui| { + ui.label(format!("{}Here we go 10000 ツ", ARROW_CIRCLE_LEFT)); + if ui.button("TEST").clicked() { + Navigator::to(ScreenId::Account) + }; + if ui.button(format!("{}BACK ", ARROW_CIRCLE_LEFT)).clicked() { + Navigator::to(ScreenId::Account) + }; + }); } } \ No newline at end of file diff --git a/src/gui/screens/mod.rs b/src/gui/screens/mod.rs index b10c139..3051436 100644 --- a/src/gui/screens/mod.rs +++ b/src/gui/screens/mod.rs @@ -14,12 +14,10 @@ pub use account::Account; pub use accounts::Accounts; -pub use navigator::Navigator; pub use root::Root; use crate::gui::platform::PlatformCallbacks; -mod navigator; mod root; mod accounts; mod account; diff --git a/src/gui/screens/navigator.rs b/src/gui/screens/navigator.rs deleted file mode 100644 index 7977d9c..0000000 --- a/src/gui/screens/navigator.rs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2023 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 std::collections::BTreeSet; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::RwLock; - -use lazy_static::lazy_static; - -use crate::gui::screens::ScreenId; - -lazy_static! { - static ref NAVIGATOR_STATE: RwLock = RwLock::new(Navigator::default()); -} - -pub struct Navigator { - screens_stack: BTreeSet, - side_panel_open: AtomicBool, -} - -impl Default for Navigator { - fn default() -> Self { - Self { - screens_stack: BTreeSet::new(), - side_panel_open: AtomicBool::new(false) - } - } -} - -impl Navigator { - pub fn init(from: ScreenId) { - let mut w_nav = NAVIGATOR_STATE.write().unwrap(); - w_nav.screens_stack.clear(); - w_nav.screens_stack.insert(from); - } - - pub fn is_current(id: &ScreenId) -> bool { - let r_nav = NAVIGATOR_STATE.read().unwrap(); - r_nav.screens_stack.last().unwrap() == id - } - - pub fn to(id: ScreenId) { - NAVIGATOR_STATE.write().unwrap().screens_stack.insert(id); - } - - pub fn back() { - let mut w_nav = NAVIGATOR_STATE.write().unwrap(); - if w_nav.screens_stack.len() > 1 { - w_nav.screens_stack.pop_last(); - } else { - - } - } - - pub fn toggle_side_panel() { - let w_nav = NAVIGATOR_STATE.write().unwrap(); - w_nav.side_panel_open.store( - !w_nav.side_panel_open.load(Ordering::Relaxed), - Ordering::Relaxed - ); - } - - pub fn is_side_panel_open() -> bool { - let r_nav = NAVIGATOR_STATE.read().unwrap(); - r_nav.side_panel_open.load(Ordering::Relaxed) - } -} diff --git a/src/gui/screens/root.rs b/src/gui/screens/root.rs index 7b94650..7cae7d5 100644 --- a/src/gui/screens/root.rs +++ b/src/gui/screens/root.rs @@ -14,10 +14,10 @@ use std::cmp::min; -use crate::gui::app::is_dual_panel_mode; +use crate::gui::Navigator; use crate::gui::platform::PlatformCallbacks; -use crate::gui::screens::{Account, Accounts, Navigator, Screen, ScreenId}; -use crate::gui::views::Network; +use crate::gui::screens::{Account, Accounts, Screen, ScreenId}; +use crate::gui::views::{Network, View}; pub struct Root { screens: Vec>, @@ -26,7 +26,7 @@ pub struct Root { impl Default for Root { fn default() -> Self { - Navigator::init_from(ScreenId::Accounts); + Navigator::init(ScreenId::Accounts); Self { screens: (vec![ @@ -40,31 +40,23 @@ impl Default for Root { impl Root { pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { - let is_network_panel_open = Navigator::is_side_panel_open() || is_dual_panel_mode(frame); - + let (is_panel_open, panel_width) = dual_panel_state_width(frame); egui::SidePanel::left("network_panel") .resizable(false) - .exact_width(if is_dual_panel_mode(frame) { - min(frame.info().window_info.size.x as i64, 400) as f32 - } else { - frame.info().window_info.size.x - }) - .frame(egui::Frame { - .. Default::default() - }) - .show_animated_inside(ui, is_network_panel_open, |ui| { + .exact_width(panel_width) + .frame(egui::Frame::default()) + .show_animated_inside(ui, is_panel_open, |ui| { self.network.ui(ui, frame, cb); }); - egui::CentralPanel::default().frame(egui::Frame { - ..Default::default() - }).show_inside(ui, |ui| { - self.show_current_screen(ui, frame, cb); - }); - + egui::CentralPanel::default() + .frame(egui::Frame::default()) + .show_inside(ui, |ui| { + self.show_current_screen(ui, frame, cb); + }); } - pub fn show_current_screen(&mut self, + fn show_current_screen(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { @@ -78,6 +70,18 @@ impl Root { } } +/// Get dual panel state and width +fn dual_panel_state_width(frame: &mut eframe::Frame) -> (bool, f32) { + let dual_panel_mode = View::is_dual_panel_mode(frame); + let is_panel_open = dual_panel_mode || Navigator::is_side_panel_open(); + let panel_width = if dual_panel_mode { + min(frame.info().window_info.size.x as i64, View::SIDE_PANEL_MIN_WIDTH) as f32 + } else { + frame.info().window_info.size.x + }; + (is_panel_open, panel_width) +} + #[allow(dead_code)] #[cfg(target_os = "android")] #[allow(non_snake_case)] diff --git a/src/gui/views/mod.rs b/src/gui/views/mod.rs index 2d9577f..93c6733 100644 --- a/src/gui/views/mod.rs +++ b/src/gui/views/mod.rs @@ -13,22 +13,25 @@ // limitations under the License. mod views; -pub use self::views::View; +pub use views::View; mod title_panel; -pub use self::title_panel::{TitlePanel, TitlePanelAction}; +pub use title_panel::*; + +mod modal; +pub use modal::*; mod network; mod network_node; -mod network_tuning; +mod network_settings; mod network_metrics; mod network_mining; +pub use network::Network; -pub use self::network::Network; +mod progress_loading; +pub use progress_loading::ProgressLoading; pub trait NetworkTab { fn name(&self) -> &String; fn ui(&mut self, ui: &mut egui::Ui); -} - - +} \ No newline at end of file diff --git a/src/gui/views/modal.rs b/src/gui/views/modal.rs new file mode 100644 index 0000000..7e1faa9 --- /dev/null +++ b/src/gui/views/modal.rs @@ -0,0 +1,260 @@ +// Copyright 2023 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 std::cmp::min; +use std::sync::atomic::{AtomicBool, Ordering}; + +use egui::{Align2, Color32, RichText, Rounding, Sense, Stroke, Vec2}; +use egui::epaint::RectShape; +use egui::style::Margin; +use egui_extras::{Size, StripBuilder}; + +use crate::gui::colors::{COLOR_DARK, COLOR_LIGHT, COLOR_YELLOW}; +use crate::gui::views::View; + +/// Identifier for [`Modal`] content to draw at [`Modal::ui`]. +pub enum ModalId { + Exit +} + +/// Location for [`Modal`] at application UI. +#[derive(Clone, Copy)] +pub enum ModalLocation { + /// To draw globally above side panel and screen. + Global, + /// To draw on the side panel. + SidePanel, + /// To draw on the screen. + Screen +} + +/// Position of [`Modal`] on the screen at provided [`ModalLocation`]. +pub enum ModalPosition { + /// Center-top position. + CenterTop, + /// Center of the location. + Center +} + +/// Stores data to draw dialog box/popup at UI, powered by [`egui::Window`]. +pub struct Modal { + /// Identifier for content. + pub(crate) id: ModalId, + /// Location at UI. + pub(crate) location: ModalLocation, + /// Position on the screen. + position: ModalPosition, + /// Flag to show the content. + open: AtomicBool, + /// To check if it can be closed. + closeable: AtomicBool, + /// Title text + title: Option +} + +impl Modal { + /// Default width of the content. + const DEFAULT_WIDTH: i64 = 380; + + /// Create open and closeable [`Modal`] with center position. + pub fn new(id: ModalId, location: ModalLocation) -> Self { + Self { + id, + location, + position: ModalPosition::Center, + open: AtomicBool::new(true), + closeable: AtomicBool::new(true), + title: None + } + } + + /// Setup position of [`Modal`] on the screen. + pub fn position(mut self, position: ModalPosition) -> Self { + self.position = position; + self + } + + /// Check if [`Modal`] is open. + pub fn is_open(&self) -> bool { + self.open.load(Ordering::Relaxed) + } + + /// Mark [`Modal`] closed. + pub fn close(&self) { + self.open.store(false, Ordering::Relaxed); + } + + /// Setup possibility to close [`Modal`]. + pub fn closeable(self, closeable: bool) -> Self { + self.closeable.store(closeable, Ordering::Relaxed); + self + } + + /// Disable possibility to close [`Modal`]. + pub fn disable_closing(&self) { + self.closeable.store(false, Ordering::Relaxed); + } + + /// Check if [`Modal`] is closeable. + pub fn is_closeable(&self) -> bool { + self.closeable.load(Ordering::Relaxed) + } + + /// Set title text. + pub fn title(mut self, title: String) -> Self { + self.title = Some(title.to_uppercase()); + self + } + + /// Show [`Modal`] with provided content. + pub fn ui(&self, + ui: &mut egui::Ui, + frame: &mut eframe::Frame, + add_content: impl FnOnce(&mut egui::Ui, &mut eframe::Frame, &Modal)) { + let cw = min(frame.info().window_info.size.x as i64 - 20, Self::DEFAULT_WIDTH) as f32; + + // Show background Window at full available size + egui::Window::new(self.window_id(true)) + .title_bar(false) + .resizable(false) + .collapsible(false) + .fixed_pos(ui.next_widget_position()) + .fixed_size(ui.available_size()) + .frame(egui::Frame { + fill: Color32::from_black_alpha(100), + ..Default::default() + }) + .show(ui.ctx(), |ui| { + ui.set_min_size(ui.available_size()); + }); + + // Show main content Window at give position + let layer_id = egui::Window::new(self.window_id(false)) + .title_bar(false) + .resizable(false) + .collapsible(false) + .default_width(cw) + .anchor(self.modal_position(), Vec2::default()) + .frame(egui::Frame { + rounding: Rounding::same(8.0), + fill: COLOR_YELLOW, + ..Default::default() + }) + .show(ui.ctx(), |ui| { + if self.title.is_some() { + self.draw_title(ui); + } + self.draw_content(ui, frame, add_content); + }).unwrap().response.layer_id; + + // Always show main content Window above background Window + ui.ctx().move_to_top(layer_id); + + } + + /// Generate identifier for inner [`egui::Window`] parts based on [`ModalLocation`]. + fn window_id(&self, background: bool) -> &'static str { + match self.location { + ModalLocation::Global => { + if background { "global.bg" } else { "global" } + } + ModalLocation::SidePanel => { + if background { "side_panel.bg" } else { "side_panel" } + } + ModalLocation::Screen => { + if background { "global.bg" } else { "global" } + } + } + } + + /// Get [`egui::Window`] position based on [`ModalPosition`]. + fn modal_position(&self) -> Align2 { + match self.position { + ModalPosition::CenterTop => { Align2::CENTER_TOP } + ModalPosition::Center => { Align2::CENTER_CENTER } + } + } + + /// Draw provided content. + fn draw_content(&self, + ui: &mut egui::Ui, + frame: &mut eframe::Frame, + add_content: impl FnOnce(&mut egui::Ui, &mut eframe::Frame, &Modal)) { + let mut rect = ui.available_rect_before_wrap(); + rect.min += egui::emath::vec2(6.0, 0.0); + rect.max -= egui::emath::vec2(6.0, 0.0); + + // Create background shape. + let rounding = if self.title.is_some() { + Rounding { + nw: 0.0, + ne: 0.0, + sw: 8.0, + se: 8.0, + } + } else { + Rounding::same(8.0) + }; + let mut bg_shape = RectShape { + rect, + rounding, + fill: COLOR_LIGHT, + stroke: View::DEFAULT_STROKE, + }; + let bg_idx = ui.painter().add(bg_shape); + + // Draw main content. + let mut content_resp_rect = ui.allocate_ui_at_rect(rect, |ui| { + (add_content)(ui, frame, self); + }).response.rect; + + // Setup background shape to be painted behind main content. + content_resp_rect.min -= egui::emath::vec2(6.0, 0.0); + content_resp_rect.max += egui::emath::vec2(6.0, 0.0); + bg_shape.rect = content_resp_rect; + ui.painter().set(bg_idx, bg_shape); + } + + /// Draw the title. + fn draw_title(&self, ui: &mut egui::Ui) { + let rect = ui.available_rect_before_wrap(); + + // Create background shape. + let mut bg_shape = RectShape { + rect, + rounding: Rounding { + nw: 8.0, + ne: 8.0, + sw: 0.0, + se: 0.0, + }, + fill: COLOR_YELLOW, + stroke: Stroke::NONE, + }; + let bg_idx = ui.painter().add(bg_shape); + + // Draw title content. + let title_resp = ui.allocate_ui_at_rect(rect, |ui| { + ui.vertical_centered_justified(|ui| { + ui.add_space(8.0); + ui.label(RichText::new(self.title.as_ref().unwrap()).size(20.0).color(COLOR_DARK)); + ui.add_space(8.0); + }); + }).response; + + // Setup background shape to be painted behind title content. + bg_shape.rect = title_resp.rect; + ui.painter().set(bg_idx, bg_shape); + } +} \ No newline at end of file diff --git a/src/gui/views/network.rs b/src/gui/views/network.rs index 0ea03c8..cf2b238 100644 --- a/src/gui/views/network.rs +++ b/src/gui/views/network.rs @@ -14,20 +14,16 @@ use std::time::Duration; -use eframe::emath::lerp; -use eframe::epaint::{Color32, FontId, Rgba, Stroke}; -use eframe::epaint::text::{LayoutJob, TextFormat, TextWrapping}; -use egui::RichText; +use egui::{Color32, lerp, Rgba, RichText, Stroke}; use egui::style::Margin; use egui_extras::{Size, StripBuilder}; use grin_chain::SyncStatus; use grin_core::global::ChainTypes; -use crate::gui::app::is_dual_panel_mode; -use crate::gui::colors::{COLOR_DARK, COLOR_YELLOW}; +use crate::gui::colors::{COLOR_DARK, COLOR_GRAY_DARK, COLOR_YELLOW}; use crate::gui::icons::{CARDHOLDER, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE}; +use crate::gui::Navigator; use crate::gui::platform::PlatformCallbacks; -use crate::gui::screens::Navigator; use crate::gui::views::{NetworkTab, View}; use crate::gui::views::network_metrics::NetworkMetrics; use crate::gui::views::network_node::NetworkNode; @@ -50,7 +46,6 @@ pub struct Network { impl Default for Network { fn default() -> Self { - Node::start(ChainTypes::Mainnet); Self { current_mode: Mode::Node, node_view: NetworkNode::default(), @@ -60,11 +55,7 @@ impl Default for Network { } impl Network { - pub fn ui(&mut self, - ui: &mut egui::Ui, - frame: &mut eframe::Frame, - _: &dyn PlatformCallbacks) { - + pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, _: &dyn PlatformCallbacks) { egui::TopBottomPanel::top("network_title") .resizable(false) .frame(egui::Frame { @@ -88,44 +79,45 @@ impl Network { self.draw_tabs(ui); }); - egui::CentralPanel::default().frame(egui::Frame { - stroke: View::DEFAULT_STROKE, - inner_margin: Margin::same(4.0), - fill: Color32::WHITE, - .. Default::default() - }).show_inside(ui, |ui| { - self.draw_tab_content(ui); - }); - - + egui::CentralPanel::default() + .frame(egui::Frame { + stroke: View::DEFAULT_STROKE, + inner_margin: Margin::same(4.0), + fill: Color32::WHITE, + .. Default::default() + }) + .show_inside(ui, |ui| { + self.draw_tab_content(ui); + }); } fn draw_tabs(&mut self, ui: &mut egui::Ui) { - //Setup spacing between tabs - ui.style_mut().spacing.item_spacing = egui::vec2(6.0, 0.0); + ui.scope(|ui| { + //Setup spacing between tabs + ui.style_mut().spacing.item_spacing = egui::vec2(6.0, 0.0); - ui.columns(4, |columns| { - columns[0].vertical_centered(|ui| { - View::tab_button(ui, DATABASE, self.current_mode == Mode::Node, || { - self.current_mode = Mode::Node; + ui.columns(4, |columns| { + columns[0].vertical_centered(|ui| { + View::tab_button(ui, DATABASE, self.current_mode == Mode::Node, || { + self.current_mode = Mode::Node; + }); + }); + columns[1].vertical_centered(|ui| { + View::tab_button(ui, GAUGE, self.current_mode == Mode::Metrics, || { + self.current_mode = Mode::Metrics; + }); + }); + columns[2].vertical_centered(|ui| { + View::tab_button(ui, FACTORY, self.current_mode == Mode::Miner, || { + self.current_mode = Mode::Miner; + }); + }); + columns[3].vertical_centered(|ui| { + View::tab_button(ui, FADERS, self.current_mode == Mode::Tuning, || { + self.current_mode = Mode::Tuning; + }); }); }); - columns[1].vertical_centered(|ui| { - View::tab_button(ui, GAUGE, self.current_mode == Mode::Metrics, || { - self.current_mode = Mode::Metrics; - }); - }); - columns[2].vertical_centered(|ui| { - View::tab_button(ui, FACTORY, self.current_mode == Mode::Miner, || { - self.current_mode = Mode::Miner; - }); - }); - columns[3].vertical_centered(|ui| { - View::tab_button(ui, FADERS, self.current_mode == Mode::Tuning, || { - self.current_mode = Mode::Tuning; - }); - }); - }); } @@ -165,7 +157,7 @@ impl Network { self.draw_title_text(builder); }); strip.cell(|ui| { - if !is_dual_panel_mode(frame) { + if !View::is_dual_panel_mode(frame) { ui.centered_and_justified(|ui| { View::title_button(ui, CARDHOLDER, || { Navigator::toggle_side_panel(); @@ -195,13 +187,14 @@ impl Network { }; builder - .size(Size::exact(19.0)) .size(Size::remainder()) + .size(Size::exact(32.0)) .vertical(|mut strip| { strip.cell(|ui| { - ui.centered_and_justified(|ui| { - ui.label(RichText::new(title_text.to_uppercase()) - .size(19.0) + ui.add_space(2.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(title_text) + .size(18.0) .color(COLOR_DARK)); }); }); @@ -223,26 +216,17 @@ impl Network { bright }; + // Draw sync text + let status_color_rgba = Rgba::from(COLOR_GRAY_DARK) * color_factor as f32; + let status_color = Color32::from(status_color_rgba); + View::ellipsize_text(ui, status_text, 15.0, status_color); + // Repaint based on sync status if idle { ui.ctx().request_repaint_after(Duration::from_millis(600)); } else { ui.ctx().request_repaint(); } - - // Draw sync text - let mut job = LayoutJob::single_section(status_text, TextFormat { - font_id: FontId::proportional(15.0), - color: Color32::from(Rgba::from(COLOR_DARK) * color_factor as f32), - .. Default::default() - }); - job.wrap = TextWrapping { - max_rows: 1, - break_anywhere: false, - overflow_character: Option::from('﹍'), - ..Default::default() - }; - ui.label(job); }); }); }); diff --git a/src/gui/views/network_metrics.rs b/src/gui/views/network_metrics.rs index d370ffd..fcde1ae 100644 --- a/src/gui/views/network_metrics.rs +++ b/src/gui/views/network_metrics.rs @@ -29,7 +29,7 @@ pub struct NetworkMetrics { impl Default for NetworkMetrics { fn default() -> Self { Self { - title: t!("metrics"), + title: t!("network.metrics").to_uppercase(), } } } @@ -57,7 +57,7 @@ impl NetworkTab for NetworkMetrics { // Show emission info ui.vertical_centered_justified(|ui| { - View::sub_title(ui, format!("{} {}", COINS, t!("emission"))); + View::sub_header(ui, format!("{} {}", COINS, t!("network_metrics.emission"))); }); ui.add_space(4.0); @@ -68,19 +68,19 @@ impl NetworkTab for NetworkMetrics { columns[0].vertical_centered(|ui| { View::rounded_box(ui, format!("{}ツ", BLOCK_REWARD), - t!("reward"), + t!("network_metrics.reward"), [true, false, true, false]); }); columns[1].vertical_centered(|ui| { View::rounded_box(ui, format!("{:.2}%", rate), - t!("inflation"), + t!("network_metrics.inflation"), [false, false, false, false]); }); columns[2].vertical_centered(|ui| { View::rounded_box(ui, supply.to_string(), - t!("supply"), + t!("network_metrics.supply"), [false, true, false, true]); }); }); @@ -88,27 +88,30 @@ impl NetworkTab for NetworkMetrics { // Show difficulty adjustment window info ui.vertical_centered_justified(|ui| { - let title = t!("difficulty_at_window", "size" => stats.diff_stats.window_size); - View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, title)); + let title = t!( + "network_metrics.difficulty_window", + "size" => stats.diff_stats.window_size + ); + View::sub_header(ui, format!("{} {}", HOURGLASS_MEDIUM, title)); }); ui.add_space(4.0); ui.columns(3, |columns| { columns[0].vertical_centered(|ui| { View::rounded_box(ui, stats.diff_stats.height.to_string(), - t!("height"), + t!("network_node.height"), [true, false, true, false]); }); columns[1].vertical_centered(|ui| { View::rounded_box(ui, format!("{}s", stats.diff_stats.average_block_time), - t!("block_time"), + t!("network_metrics.block_time"), [false, false, false, false]); }); columns[2].vertical_centered(|ui| { View::rounded_box(ui, stats.diff_stats.average_difficulty.to_string(), - t!("difficulty"), + t!("network_node.difficulty"), [false, true, false, true]); }); }); @@ -119,7 +122,7 @@ impl NetworkTab for NetworkMetrics { ScrollArea::vertical() .auto_shrink([false; 2]) .stick_to_bottom(true) - .id_source("diff_scroll") + .id_source("difficulty_scroll") .show_rows( ui, DIFF_BLOCK_UI_HEIGHT, diff --git a/src/gui/views/network_node.rs b/src/gui/views/network_node.rs index 6a65c16..d37881d 100644 --- a/src/gui/views/network_node.rs +++ b/src/gui/views/network_node.rs @@ -28,7 +28,7 @@ pub struct NetworkNode { impl Default for NetworkNode { fn default() -> Self { Self { - title: t!("integrated_node"), + title: t!("network.node").to_uppercase(), } } } @@ -54,20 +54,20 @@ impl NetworkTab for NetworkNode { .show(ui, |ui| { // Show header stats ui.vertical_centered_justified(|ui| { - View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("header"))); + View::sub_header(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header"))); }); ui.add_space(4.0); ui.columns(2, |columns| { columns[0].vertical_centered(|ui| { View::rounded_box(ui, stats.header_stats.last_block_h.to_string(), - t!("hash"), + t!("network_node.hash"), [true, false, false, false]); }); columns[1].vertical_centered(|ui| { View::rounded_box(ui, stats.header_stats.height.to_string(), - t!("height"), + t!("network_node.height"), [false, true, false, false]); }); }); @@ -75,14 +75,14 @@ impl NetworkTab for NetworkNode { columns[0].vertical_centered(|ui| { View::rounded_box(ui, stats.header_stats.total_difficulty.to_string(), - t!("difficulty"), + t!("network_node.difficulty"), [false, false, true, false]); }); columns[1].vertical_centered(|ui| { let h_ts = stats.header_stats.latest_timestamp; View::rounded_box(ui, format!("{}", h_ts.format("%d/%m/%Y %H:%M")), - t!("time_utc"), + t!("network_node.time_utc"), [false, false, false, true]); }); }); @@ -90,20 +90,20 @@ impl NetworkTab for NetworkNode { // Show block stats ui.add_space(6.0); ui.vertical_centered_justified(|ui| { - View::sub_title(ui, format!("{} {}", CUBE, t!("block"))); + View::sub_header(ui, format!("{} {}", CUBE, t!("network_node.block"))); }); ui.add_space(4.0); ui.columns(2, |columns| { columns[0].vertical_centered(|ui| { View::rounded_box(ui, stats.chain_stats.last_block_h.to_string(), - t!("hash"), + t!("network_node.hash"), [true, false, false, false]); }); columns[1].vertical_centered(|ui| { View::rounded_box(ui, stats.chain_stats.height.to_string(), - t!("height"), + t!("network_node.height"), [false, true, false, false]); }); }); @@ -111,14 +111,14 @@ impl NetworkTab for NetworkNode { columns[0].vertical_centered(|ui| { View::rounded_box(ui, stats.chain_stats.total_difficulty.to_string(), - t!("difficulty"), + t!("network_node.difficulty"), [false, false, true, false]); }); columns[1].vertical_centered(|ui| { let b_ts = stats.chain_stats.latest_timestamp; View::rounded_box(ui, format!("{}", b_ts.format("%d/%m/%Y %H:%M")), - t!("time_utc"), + t!("network_node.time_utc"), [false, false, false, true]); }); }); @@ -126,7 +126,7 @@ impl NetworkTab for NetworkNode { // Show data stats ui.add_space(6.0); ui.vertical_centered_justified(|ui| { - View::sub_title(ui, format!("{} {}", SHARE_NETWORK, t!("data"))); + View::sub_header(ui, format!("{} {}", SHARE_NETWORK, t!("network_node.data"))); }); ui.add_space(4.0); ui.columns(2, |columns| { @@ -139,7 +139,7 @@ impl NetworkTab for NetworkNode { }; View::rounded_box(ui, tx_stat, - t!("main_pool"), + t!("network_node.main_pool"), [true, false, false, false]); }); columns[1].vertical_centered(|ui| { @@ -151,7 +151,7 @@ impl NetworkTab for NetworkNode { }; View::rounded_box(ui, stem_tx_stat, - t!("stem_pool"), + t!("network_node.stem_pool"), [false, true, false, false]); }); }); @@ -159,13 +159,13 @@ impl NetworkTab for NetworkNode { columns[0].vertical_centered(|ui| { View::rounded_box(ui, stats.disk_usage_gb.to_string(), - t!("size"), + t!("network_node.size"), [false, false, true, false]); }); columns[1].vertical_centered(|ui| { View::rounded_box(ui, stats.peer_count.to_string(), - t!("peers"), + t!("network_node.peers"), [false, false, false, true]); }); }); @@ -174,7 +174,7 @@ impl NetworkTab for NetworkNode { if stats.peer_count > 0 { ui.add_space(6.0); ui.vertical_centered_justified(|ui| { - View::sub_title(ui,format!("{} {}", HANDSHAKE, t!("peers"))); + View::sub_header(ui, format!("{} {}", HANDSHAKE, t!("network_node.peers"))); }); ui.add_space(4.0); diff --git a/src/gui/views/network_tuning.rs b/src/gui/views/network_settings.rs similarity index 100% rename from src/gui/views/network_tuning.rs rename to src/gui/views/network_settings.rs diff --git a/src/gui/views/progress_loading.rs b/src/gui/views/progress_loading.rs new file mode 100644 index 0000000..caf1bbf --- /dev/null +++ b/src/gui/views/progress_loading.rs @@ -0,0 +1,39 @@ +// Copyright 2023 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::{Response, RichText, Spinner, Ui, Widget}; + +use crate::gui::colors::COLOR_DARK; + +pub struct ProgressLoading { + text: String +} + +impl ProgressLoading { + pub fn new(text: String) -> Self { + Self { + text + } + } +} + +impl Widget for ProgressLoading { + fn ui(self, ui: &mut Ui) -> Response { + ui.vertical_centered_justified(|ui| { + Spinner::new().size(36.0).color(COLOR_DARK).ui(ui); + ui.add_space(10.0); + ui.label(RichText::new(self.text).size(18.0).color(COLOR_DARK)); + }).response + } +} \ No newline at end of file diff --git a/src/gui/views/title_panel.rs b/src/gui/views/title_panel.rs index 7e4a96f..92433b2 100644 --- a/src/gui/views/title_panel.rs +++ b/src/gui/views/title_panel.rs @@ -12,57 +12,36 @@ // See the License for the specific language governing permissions and // limitations under the License. -use eframe::epaint::{FontId, Stroke}; -use eframe::epaint::text::{LayoutJob, TextFormat, TextWrapping}; use egui::style::Margin; use egui_extras::{Size, StripBuilder}; use crate::gui::colors::{COLOR_DARK, COLOR_YELLOW}; -use crate::gui::screens::Navigator; use crate::gui::views::View; -pub struct TitlePanelAction { - pub(crate) icon: Box, +pub struct TitlePanelAction<'action> { + pub(crate) icon: Box<&'action str>, pub(crate) on_click: Box, } -impl TitlePanelAction { - pub fn new(icon: Box, on_click: fn()) -> Option { - Option::from(Self { icon, on_click: Box::new(on_click) }) +impl<'action> TitlePanelAction<'action> { + pub fn new(icon: &'action str, on_click: fn()) -> Option { + Option::from(Self { icon: Box::new(icon), on_click: Box::new(on_click) }) } } -#[derive(Default)] -pub struct TitlePanelActions { - left: Option, - right: Option -} - pub struct TitlePanel { title: String, - actions: TitlePanelActions, } impl TitlePanel { - pub fn new(title: String) -> Self { - Self { - title, - actions: TitlePanelActions::default() - } + const PANEL_SIZE: f32 = 52.0; + + pub fn new(title: &String) -> Self { + Self { title: title.to_uppercase() } } - pub fn left_action(mut self, action: Option) -> Self { - self.actions.left = action; - self - } - - pub fn right_action(mut self, action: Option) -> Self { - self.actions.right = action; - self - } - - pub fn ui(&mut self, ui: &mut egui::Ui) { - let Self { actions, title } = self; + pub fn ui(&self, l: Option, r: Option, ui: &mut egui::Ui) { + let Self { title } = self; egui::TopBottomPanel::top("title_panel") .resizable(false) @@ -70,33 +49,29 @@ impl TitlePanel { fill: COLOR_YELLOW, inner_margin: Margin::same(0.0), outer_margin: Margin::same(0.0), - stroke: Stroke::NONE, + stroke: egui::Stroke::NONE, ..Default::default() }) .show_inside(ui, |ui| { StripBuilder::new(ui) - .size(Size::exact(52.0)) + .size(Size::exact(Self::PANEL_SIZE)) .vertical(|mut strip| { strip.strip(|builder| { builder - .size(Size::exact(52.0)) + .size(Size::exact(Self::PANEL_SIZE)) .size(Size::remainder()) - .size(Size::exact(52.0)) + .size(Size::exact(Self::PANEL_SIZE)) .horizontal(|mut strip| { strip.cell(|ui| { - show_action(ui, actions.left.as_ref()); - }); - strip.strip(|builder| { - builder - .size(Size::remainder()) - .vertical(|mut strip| { - strip.cell(|ui| { - show_title(title, ui); - }); - }); + show_action(ui, l.as_ref()); }); strip.cell(|ui| { - show_action(ui, actions.right.as_ref()); + ui.centered_and_justified(|ui| { + View::ellipsize_text(ui, title.into(), 20.0, COLOR_DARK); + }); + }); + strip.cell(|ui| { + show_action(ui, r.as_ref()); }); }); }); @@ -116,21 +91,3 @@ fn show_action(ui: &mut egui::Ui, action: Option<&TitlePanelAction>) { } } -fn show_title(title: &String, ui: &mut egui::Ui) { - ui.centered_and_justified(|ui| { - let mut job = LayoutJob::single_section(title.to_uppercase(), TextFormat { - font_id: FontId::proportional(20.0), - color: COLOR_DARK, - .. Default::default() - }); - job.wrap = TextWrapping { - max_rows: 1, - break_anywhere: false, - overflow_character: Option::from('﹍'), - ..Default::default() - }; - ui.label(job); - - }); -} - diff --git a/src/gui/views/views.rs b/src/gui/views/views.rs index dfafc57..a8b871c 100644 --- a/src/gui/views/views.rs +++ b/src/gui/views/views.rs @@ -12,35 +12,87 @@ // See the License for the specific language governing permissions and // limitations under the License. -use eframe::epaint::{Color32, FontId, Rounding, Stroke}; -use eframe::epaint::text::{LayoutJob, TextFormat, TextWrapping}; -use egui::{RichText, Sense, Widget}; +use egui::epaint::{Color32, FontId, Rounding, Stroke}; +use egui::text::{LayoutJob, TextFormat}; +use egui::{Button, PointerState, Response, RichText, Sense, Widget}; +use egui::epaint::text::TextWrapping; use crate::gui::colors::{COLOR_DARK, COLOR_GRAY, COLOR_LIGHT, COLOR_GRAY_LIGHT, COLOR_GRAY_DARK}; pub struct View; impl View { + /// Default stroke around views. pub const DEFAULT_STROKE: Stroke = Stroke { width: 1.0, color: Color32::from_gray(190) }; + /// Default width of side panel at application UI. + pub const SIDE_PANEL_MIN_WIDTH: i64 = 400; + + /// Check if UI can show side panel and screen at same time. + pub fn is_dual_panel_mode(frame: &mut eframe::Frame) -> bool { + let w = frame.info().window_info.size.x; + let h = frame.info().window_info.size.y; + // Screen is wide if width is greater than height or just 20% smaller. + let is_wide_screen = w > h || w + (w * 0.2) >= h; + // Dual panel mode is available when window is wide and its width is at least 2 times + // greater than minimal width of the side panel. + is_wide_screen && w >= Self::SIDE_PANEL_MIN_WIDTH as f32 * 2.0 + } + + /// Show and cut long text with ﹍ character. + pub fn ellipsize_text(ui: &mut egui::Ui, text: String, size: f32, color: Color32) { + let mut job = LayoutJob::single_section(text, TextFormat { + font_id: FontId::proportional(size), color, .. Default::default() + }); + job.wrap = TextWrapping { + max_rows: 1, + break_anywhere: false, + overflow_character: Option::from('﹍'), + ..Default::default() + }; + ui.label(job); + } + + /// Sub-header with uppercase characters and more lighter color. + pub fn sub_header(ui: &mut egui::Ui, text: String) { + ui.label(RichText::new(text.to_uppercase()).size(16.0).color(COLOR_GRAY_DARK)); + } + + /// Temporary button click optimization for touch screens. + fn on_button_click(ui: &mut egui::Ui, resp: Response, action: impl FnOnce()) { + // Clear pointer event if dragging is out of button area + if resp.dragged() && !ui.rect_contains_pointer(resp.rect) { + ui.input_mut().pointer = PointerState::default(); + } + // Call click action if button is clicked or drag released + if resp.drag_released() || resp.clicked() { + (action)(); + }; + } + + /// Title button with transparent background fill color, contains only icon. pub fn title_button(ui: &mut egui::Ui, icon: &str, action: impl FnOnce()) { ui.scope(|ui| { // Disable stroke around title buttons on hover ui.style_mut().visuals.widgets.active.bg_stroke = Stroke::NONE; - let b = egui::widgets::Button::new( - RichText::new(icon.to_string()).size(24.0).color(COLOR_DARK) - ).fill(Color32::TRANSPARENT) + let wt = RichText::new(icon.to_string()).size(24.0).color(COLOR_DARK); + let br = Button::new(wt) + .fill(Color32::TRANSPARENT) .ui(ui).interact(Sense::click_and_drag()); - // Click optimization for touch screens - if b.drag_released() || b.clicked() { - (action)(); - }; + Self::on_button_click(ui, br, action); }); } - pub fn tab_button(ui: &mut egui::Ui, icon: &str, active: bool, mut action: impl FnMut()) { + /// Tab button with white background fill color, contains only icon. + pub fn tab_button(ui: &mut egui::Ui, icon: &str, active: bool, action: impl FnOnce()) { + let text_color = match active { + true => { COLOR_GRAY_DARK } + false => { COLOR_DARK } + }; + let wt = RichText::new(icon.to_string()).size(24.0).color(text_color); + let stroke = match active { true => { Stroke::NONE } false => { Self::DEFAULT_STROKE } @@ -50,26 +102,32 @@ impl View { true => { COLOR_LIGHT } false => { Color32::WHITE } }; - - let b = egui::widgets::Button::new( - RichText::new(icon.to_string()).size(24.0).color(COLOR_DARK) - ).min_size(ui.available_size_before_wrap()) + let br = Button::new(wt) + .min_size(ui.available_size_before_wrap()) .stroke(stroke) .fill(color) .ui(ui).interact(Sense::click_and_drag()); - // Click optimization for touch screens - if b.drag_released() || b.clicked() { - (action)(); - }; + Self::on_button_click(ui, br, action); } - pub fn sub_title(ui: &mut egui::Ui, text: String) { - ui.label(RichText::new(text.to_uppercase()).size(16.0).color(COLOR_GRAY_DARK)); + /// Modal button with white background fill color, contains text. + pub fn modal_button(ui: &mut egui::Ui, text: String, action: impl FnOnce()) { + let mut size = ui.available_size_before_wrap(); + size.y = 36.0; + + let wt = RichText::new(text.to_uppercase()).size(18.0).color(COLOR_GRAY_DARK); + let br = Button::new(wt) + .stroke(Self::DEFAULT_STROKE) + .min_size(size) + .fill(Color32::WHITE) + .ui(ui).interact(Sense::click_and_drag()); + + Self::on_button_click(ui, br, action); } - /// Draw rounded box with some value and label in the middle - /// where is r = [top_left, top_right, bottom_left, bottom_right] + /// Draw rounded box with some value and label in the middle, + /// where is r = (top_left, top_right, bottom_left, bottom_right). /// | VALUE | /// | label | pub fn rounded_box(ui: &mut egui::Ui, value: String, label: String, r: [bool; 4]) { diff --git a/src/node/mod.rs b/src/node/mod.rs index a521996..ecac403 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -14,4 +14,4 @@ mod node; -pub use self::node::Node; \ No newline at end of file +pub use node::Node; \ No newline at end of file diff --git a/src/node/node.rs b/src/node/node.rs index 9e5881f..a3dfb53 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -25,25 +25,26 @@ use grin_config::config; use grin_core::global; use grin_core::global::ChainTypes; use grin_servers::{Server, ServerStats}; -use jni::objects::JString; use jni::sys::jstring; use lazy_static::lazy_static; use log::info; lazy_static! { + /// Static thread-aware state of [Node] to be updated from another thread. static ref NODE_STATE: Arc = Arc::new(Node::default()); } +/// Provides [Server] control, holds current status and statistics. pub struct Node { - /// Data for UI + /// Statistics data for UI. stats: Arc>>, - /// Chain type of launched server + /// Chain type of launched server. chain_type: Arc>, - /// Indicator if server is starting + /// Indicator if server is starting. starting: AtomicBool, - /// Thread flag to stop the server and start it again + /// Thread flag to stop the server and start it again. restart_needed: AtomicBool, - /// Thread flag to stop the server + /// Thread flag to stop the server. stop_needed: AtomicBool, } @@ -60,12 +61,12 @@ impl Default for Node { } impl Node { - /// Stop server + /// Stop [Server]. pub fn stop() { NODE_STATE.stop_needed.store(true, Ordering::Relaxed); } - /// Start server with provided chain type + /// Start [Server] with provided chain type. pub fn start(chain_type: ChainTypes) { if !Self::is_running() { let mut w_chain_type = NODE_STATE.chain_type.write().unwrap(); @@ -74,7 +75,7 @@ impl Node { } } - /// Restart server with provided chain type + /// Restart [Server] with provided chain type. pub fn restart(chain_type: ChainTypes) { if Self::is_running() { let mut w_chain_type = NODE_STATE.chain_type.write().unwrap(); @@ -85,52 +86,52 @@ impl Node { } } - /// Check if server is starting + /// Check if [Server] is starting. pub fn is_starting() -> bool { NODE_STATE.starting.load(Ordering::Relaxed) } - /// Check if server is running + /// Check if [Server] is running. pub fn is_running() -> bool { Self::get_stats().is_some() || Self::is_starting() } - /// Check if server is stopping + /// Check if [Server] is stopping. pub fn is_stopping() -> bool { NODE_STATE.stop_needed.load(Ordering::Relaxed) } - /// Check if server is restarting + /// Check if [Server] is restarting. pub fn is_restarting() -> bool { NODE_STATE.restart_needed.load(Ordering::Relaxed) } - /// Get server stats + /// Get [Server] statistics. pub fn get_stats() -> RwLockReadGuard<'static, Option> { NODE_STATE.stats.read().unwrap() } - /// Get server sync status, empty when server is not running + /// Get [Server] synchronization status, empty when it is not running. pub fn get_sync_status() -> Option { - // return Shutdown status when node is stopping + // Return Shutdown status when node is stopping. if Self::is_stopping() { return Some(SyncStatus::Shutdown) } - // return Initial status when node is starting + // Return Initial status when node is starting. if Self::is_starting() { return Some(SyncStatus::Initial) } let stats = Self::get_stats(); - // return sync status when server is running (stats are not empty) + // Return sync status when server is running (stats are not empty). if stats.is_some() { return Some(stats.as_ref().unwrap().sync_status) } None } - /// Start a thread to launch server and update state with server stats + /// Start a thread to launch [Server] and update [NODE_STATE] with server statistics. fn start_server_thread() -> JoinHandle<()> { thread::spawn(move || { NODE_STATE.starting.store(true, Ordering::Relaxed); @@ -142,7 +143,7 @@ impl Node { if Self::is_restarting() { server.stop(); - // Create new server with current chain type + // Create new server with current chain type. server = start_server(&NODE_STATE.chain_type.read().unwrap()); NODE_STATE.restart_needed.store(false, Ordering::Relaxed); @@ -152,6 +153,7 @@ impl Node { let mut w_stats = NODE_STATE.stats.write().unwrap(); *w_stats = None; + NODE_STATE.starting.store(false, Ordering::Relaxed); NODE_STATE.stop_needed.store(false, Ordering::Relaxed); break; } else { @@ -166,18 +168,23 @@ impl Node { } } } - thread::sleep(Duration::from_millis(300)); + thread::sleep(Duration::from_millis(250)); } }) } + /// Get synchronization status i18n text. pub fn get_sync_status_text(sync_status: Option) -> String { if Node::is_restarting() { - return t!("server_restarting") + return t!("sync_status.server_restarting") + } + + if Node::is_stopping() { + return t!("sync_status.shutdown") } if sync_status.is_none() { - return t!("server_down") + return t!("sync_status.server_down") } match sync_status.unwrap() { @@ -249,8 +256,9 @@ impl Node { } -/// Start server with provided chain type +/// Start [Server] with provided chain type. fn start_server(chain_type: &ChainTypes) -> Server { + // Initialize config let mut node_config_result = config::initial_setup_server(chain_type); if node_config_result.is_err() { // Remove config file on init error @@ -268,18 +276,6 @@ fn start_server(chain_type: &ChainTypes) -> Server { let config = node_config.clone().unwrap(); let server_config = config.members.as_ref().unwrap().server.clone(); - // Remove lock file (in case if we have running node from another app) - { - let mut lock_file = PathBuf::from(&server_config.db_root); - lock_file.push("grin.lock"); - if lock_file.exists() { - match fs::remove_file(lock_file) { - Ok(_) => {} - Err(_) => { println!("Cannot remove grin.lock file") } - }; - } - } - // Remove temporary file dir { let mut tmp_dir = PathBuf::from(&server_config.db_root); @@ -332,26 +328,28 @@ fn start_server(chain_type: &ChainTypes) -> Server { let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) = Box::leak(Box::new(oneshot::channel::<()>())); let mut server_result = Server::new(server_config.clone(), None, api_chan); - if server_result.is_err() { - let mut db_path = PathBuf::from(&server_config.db_root); - db_path.push("grin.lock"); - fs::remove_file(db_path).unwrap(); - - // Remove chain data on server start error - let dirs_to_remove: Vec<&str> = vec!["header", "lmdb", "txhashset"]; - for dir in dirs_to_remove { - let mut path = PathBuf::from(&server_config.db_root); - path.push(dir); - fs::remove_dir_all(path).unwrap(); - } - - // Recreate server - let config = node_config.clone().unwrap(); - let server_config = config.members.as_ref().unwrap().server.clone(); - let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) = - Box::leak(Box::new(oneshot::channel::<()>())); - server_result = Server::new(server_config.clone(), None, api_chan); - } + //TODO: handle server errors + // + // if server_result.is_err() { + // let mut db_path = PathBuf::from(&server_config.db_root); + // db_path.push("grin.lock"); + // fs::remove_file(db_path).unwrap(); + // + // // Remove chain data on server start error + // let dirs_to_remove: Vec<&str> = vec!["header", "lmdb", "txhashset"]; + // for dir in dirs_to_remove { + // let mut path = PathBuf::from(&server_config.db_root); + // path.push(dir); + // fs::remove_dir_all(path).unwrap(); + // } + // + // // Recreate server + // let config = node_config.clone().unwrap(); + // let server_config = config.members.as_ref().unwrap().server.clone(); + // let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) = + // Box::leak(Box::new(oneshot::channel::<()>())); + // server_result = Server::new(server_config.clone(), None, api_chan); + // } server_result.unwrap() } @@ -360,6 +358,7 @@ fn start_server(chain_type: &ChainTypes) -> Server { #[cfg(target_os = "android")] #[allow(non_snake_case)] #[no_mangle] +/// Get sync status text for Android notification from [NODE_STATE] in Java string format. pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncStatusText( _env: jni::JNIEnv, _class: jni::objects::JObject, @@ -375,11 +374,25 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncStatusText( #[cfg(target_os = "android")] #[allow(non_snake_case)] #[no_mangle] +/// Get sync title for Android notification in Java string format. pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncTitle( _env: jni::JNIEnv, _class: jni::objects::JObject, _activity: jni::objects::JObject, ) -> jstring { - let j_text = _env.new_string(t!("integrated_node")); + let j_text = _env.new_string(t!("network.node")); return j_text.unwrap().into_raw(); +} + +#[allow(dead_code)] +#[cfg(target_os = "android")] +#[allow(non_snake_case)] +#[no_mangle] +/// Calling on unexpected application termination (removal from recent apps) +pub extern "C" fn Java_mw_gri_android_MainActivity_onTermination( + _env: jni::JNIEnv, + _class: jni::objects::JObject, + _activity: jni::objects::JObject, +) { + Node::stop(); } \ No newline at end of file