ui: title panel refactoring, initial wallet creation, update translations

This commit is contained in:
ardocrat 2023-07-21 04:17:57 +03:00
parent 83b8de5ad6
commit 8a60e31555
18 changed files with 890 additions and 145 deletions

View file

@ -1,7 +1,23 @@
copy: Copy copy: Copy
paste: Paste paste: Paste
accounts: continue: Continue
title: Accounts complete: Complete
wallets:
title: Wallets
new: New wallet
create_desc: Create or import existing wallet from saved recovery phrase.
add: Add wallet
name: 'Name:'
pass: 'Password:'
name_empty: Enter name of wallet
pass_empty: Enter password for wallet
word_number: 'Word #%{number}'
word_empty: 'Enter word #%{number} from recovery phrase'
not_valid_word: Entered word is not valid
create: Create
import: Import
mnemonic_desc: 'Safely write down and save your %{}-word recovery phrase below. If you lose your device, you will need them to restore access to your funds.'
mnemonic_conf: Select words in the same order as they are displayed in your recovery phrase.
network: network:
self: Network self: Network
node: Integrated node node: Integrated node

View file

@ -1,7 +1,23 @@
copy: Копировать copy: Копировать
paste: Вставить paste: Вставить
accounts: continue: Продолжить
title: Аккаунты complete: Завершить
wallets:
title: Кошельки
new: Новый кошелёк
create_desc: Создайте или импортируйте существующий кошелёк из сохранённой фразы восстановления.
add: Добавить кошелёк
name: 'Название:'
pass: 'Пароль:'
name_empty: Введите название кошелька
pass_empty: Введите пароль для кошелька
word_number: 'Слово #%{number}'
word_empty: 'Введите слово #%{number} из фразы восстановления'
not_valid_word: Введено недопустимое слово
create: Создать
import: Импортировать
mnemonic_desc: 'Безопасно запишите и сохраните свою фразу восстановления из %{} слов ниже. Они понадобятся вам, чтобы восстановить доступ к вашим средствам, если вы потеряете устройство.'
mnemonic_conf: Выберите слова в таком же порядке, как они отображены в вашей фразе восстановления.
network: network:
self: Сеть self: Сеть
node: Встроенный узел node: Встроенный узел

View file

@ -12,12 +12,20 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::sync::atomic::{AtomicBool, Ordering};
use egui::Context; use egui::Context;
use lazy_static::lazy_static;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::Root; use crate::gui::views::Root;
lazy_static! {
/// State to check if platform Back button was pressed.
static ref BACK_BUTTON_PRESSED: AtomicBool = AtomicBool::new(false);
}
/// Implements ui entry point and contains platform-specific callbacks. /// Implements ui entry point and contains platform-specific callbacks.
pub struct PlatformApp<Platform> { pub struct PlatformApp<Platform> {
/// Platform specific callbacks handler. /// Platform specific callbacks handler.
@ -34,9 +42,13 @@ impl<Platform> PlatformApp<Platform> {
impl<Platform: PlatformCallbacks> eframe::App for PlatformApp<Platform> { impl<Platform: PlatformCallbacks> eframe::App for PlatformApp<Platform> {
fn update(&mut self, ctx: &Context, frame: &mut eframe::Frame) { fn update(&mut self, ctx: &Context, frame: &mut eframe::Frame) {
// Handle Esc keyboard key event. // Handle Esc keyboard key event and platform Back button key event.
if ctx.input(|i| i.key_pressed(egui::Key::Escape)) { let back_button_pressed = back_button_pressed();
Root::on_back(); if ctx.input(|i| i.key_pressed(egui::Key::Escape) || back_button_pressed) {
if back_button_pressed {
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
}
self.root.on_back();
} }
// Show main content. // Show main content.
@ -56,6 +68,13 @@ impl<Platform: PlatformCallbacks> eframe::App for PlatformApp<Platform> {
} }
} }
/// Check if platform Back button was pressed.
fn back_button_pressed() -> bool {
BACK_BUTTON_PRESSED.load(Ordering::Relaxed)
}
#[allow(dead_code)] #[allow(dead_code)]
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
#[allow(non_snake_case)] #[allow(non_snake_case)]
@ -66,8 +85,5 @@ pub extern "C" fn Java_mw_gri_android_MainActivity_onBack(
_class: jni::objects::JObject, _class: jni::objects::JObject,
_activity: jni::objects::JObject, _activity: jni::objects::JObject,
) { ) {
Root::on_back(); BACK_BUTTON_PRESSED.store(true, Ordering::Relaxed);
} }

View file

@ -27,5 +27,5 @@ pub use root::*;
mod network; mod network;
pub use network::*; pub use network::*;
mod accounts; mod wallets;
pub use accounts::*; pub use wallets::*;

View file

@ -0,0 +1,32 @@
// 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 crate::gui::platform::PlatformCallbacks;
/// Network connections content.
pub struct ConnectionsContent {
}
impl Default for ConnectionsContent {
fn default() -> Self {
Self {}
}
}
impl ConnectionsContent {
pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) {
}
}

View file

@ -28,4 +28,8 @@ mod setup;
pub use setup::*; pub use setup::*;
mod network; mod network;
pub use network::*; pub use network::*;
mod connections;
pub use connections::*;

View file

@ -19,17 +19,18 @@ use crate::AppConfig;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{CARDHOLDER, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE, POWER}; use crate::gui::icons::{CARDHOLDER, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE, POWER};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, ModalContainer, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings, Root, TitleAction, TitlePanel, TitleType, View}; use crate::gui::views::{Modal, ModalContainer, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings, Root, TitlePanel, TitleType, View};
use crate::gui::views::network::setup::{DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup}; use crate::gui::views::network::setup::{DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup};
use crate::node::Node; use crate::node::Node;
/// Network tab content interface.
pub trait NetworkTab { pub trait NetworkTab {
fn get_type(&self) -> NetworkTabType; fn get_type(&self) -> NetworkTabType;
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks); fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
fn on_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks); fn on_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks);
} }
/// Type of [`NetworkTab`] content.
#[derive(PartialEq)] #[derive(PartialEq)]
pub enum NetworkTabType { pub enum NetworkTabType {
Node, Node,
@ -108,15 +109,17 @@ impl ModalContainer for Network {
impl Network { impl Network {
pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) {
// Show modal content if it's opened. // Show modal content for current ui container.
if self.can_draw_modal() { if self.can_draw_modal() {
Modal::ui(ui, |ui, modal| { Modal::ui(ui, |ui, modal| {
self.current_tab.as_mut().on_modal_ui(ui, modal, cb); self.current_tab.as_mut().on_modal_ui(ui, modal, cb);
}); });
} }
// Show title panel.
self.title_ui(ui, frame); self.title_ui(ui, frame);
// Show bottom tabs.
egui::TopBottomPanel::bottom("network_tabs") egui::TopBottomPanel::bottom("network_tabs")
.frame(egui::Frame { .frame(egui::Frame {
fill: Colors::FILL, fill: Colors::FILL,
@ -127,6 +130,7 @@ impl Network {
self.tabs_ui(ui); self.tabs_ui(ui);
}); });
// Show tab content.
egui::CentralPanel::default() egui::CentralPanel::default()
.frame(egui::Frame { .frame(egui::Frame {
stroke: View::DEFAULT_STROKE, stroke: View::DEFAULT_STROKE,
@ -147,7 +151,7 @@ impl Network {
/// Calculate tabs inner margin based on display insets (cutouts). /// Calculate tabs inner margin based on display insets (cutouts).
fn tabs_inner_margin(ui: &mut egui::Ui, frame: &mut eframe::Frame) -> Margin { fn tabs_inner_margin(ui: &mut egui::Ui, frame: &mut eframe::Frame) -> Margin {
Margin { Margin {
left: View::far_left_inset_margin(ui) + 4.0, left: View::get_left_inset() + 4.0,
right: View::far_right_inset_margin(ui, frame) + 4.0, right: View::far_right_inset_margin(ui, frame) + 4.0,
top: 4.0, top: 4.0,
bottom: View::get_bottom_inset() + 4.0, bottom: View::get_bottom_inset() + 4.0,
@ -157,7 +161,7 @@ impl Network {
/// Calculate content inner margin based on display insets (cutouts). /// Calculate content inner margin based on display insets (cutouts).
fn content_inner_margin(ui: &mut egui::Ui, frame: &mut eframe::Frame) -> Margin { fn content_inner_margin(ui: &mut egui::Ui, frame: &mut eframe::Frame) -> Margin {
Margin { Margin {
left: View::far_left_inset_margin(ui) + 4.0, left: View::get_left_inset() + 4.0,
right: View::far_right_inset_margin(ui, frame) + 4.0, right: View::far_right_inset_margin(ui, frame) + 4.0,
top: 3.0, top: 3.0,
bottom: 4.0, bottom: 4.0,
@ -206,27 +210,30 @@ impl Network {
let subtitle_text = Node::get_sync_status_text(); let subtitle_text = Node::get_sync_status_text();
let not_syncing = Node::not_syncing(); let not_syncing = Node::not_syncing();
let title_content = TitleType::WithSubTitle(title_text, subtitle_text, !not_syncing); let title_content = TitleType::WithSubTitle(title_text, subtitle_text, !not_syncing);
// Draw title panel. // Draw title panel.
TitlePanel::ui(title_content, TitleAction::new(DOTS_THREE_OUTLINE_VERTICAL, || { TitlePanel::ui(title_content, |ui, frame| {
//TODO: Show connections View::title_button(ui, DOTS_THREE_OUTLINE_VERTICAL, || {
}), if !Root::is_dual_panel_mode(frame) { //TODO: Show connections
TitleAction::new(CARDHOLDER, || { });
Root::toggle_side_panel(); }, |ui, frame| {
}) if !Root::is_dual_panel_mode(frame) {
} else { View::title_button(ui, CARDHOLDER, || {
None Root::toggle_network_panel();
});
}
}, ui, frame); }, ui, frame);
} }
/// Content to draw when node is disabled. /// Content to draw when node is disabled.
pub fn disabled_node_ui(ui: &mut egui::Ui) { pub fn disabled_node_ui(ui: &mut egui::Ui) {
View::center_content(ui, 162.0, |ui| { View::center_content(ui, 156.0, |ui| {
let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL); let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL);
ui.label(RichText::new(text) ui.label(RichText::new(text)
.size(16.0) .size(16.0)
.color(Colors::INACTIVE_TEXT) .color(Colors::INACTIVE_TEXT)
); );
ui.add_space(10.0); ui.add_space(8.0);
View::button(ui, format!("{} {}", POWER, t!("network.enable_node")), Colors::GOLD, || { View::button(ui, format!("{} {}", POWER, t!("network.enable_node")), Colors::GOLD, || {
Node::start(); Node::start();
}); });

View file

@ -11,30 +11,31 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use egui::os::OperatingSystem; use egui::os::OperatingSystem;
use egui::RichText; use egui::RichText;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use crate::gui::Colors;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Accounts, Modal, ModalContainer, Network, View}; use crate::gui::views::{Wallets, Modal, ModalContainer, Network, View};
use crate::node::Node; use crate::node::Node;
lazy_static! { lazy_static! {
/// To check if side panel is open from any part of ui. /// Global state to check if [`Network`] panel is open.
static ref SIDE_PANEL_OPEN: AtomicBool = AtomicBool::new(false); static ref NETWORK_PANEL_OPEN: AtomicBool = AtomicBool::new(false);
} }
/// Contains main ui content, handles side panel state. /// Contains main ui content, handles side panel state.
pub struct Root { pub struct Root {
/// Side panel content. /// Side panel [`Network`] content.
side_panel: Network, network: Network,
/// Central panel content. /// Central panel [`Wallets`] content.
central_content: Accounts, wallets: Wallets,
/// Check if app exit is allowed on close event of [`eframe::App`] platform implementation. /// Check if app exit is allowed on close event of [`eframe::App`] implementation.
pub(crate) exit_allowed: bool, pub(crate) exit_allowed: bool,
/// Flag to show exit progress at [`Modal`]. /// Flag to show exit progress at [`Modal`].
@ -50,8 +51,8 @@ impl Default for Root {
let os = OperatingSystem::from_target_os(); let os = OperatingSystem::from_target_os();
let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS; let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS;
Self { Self {
side_panel: Network::default(), network: Network::default(),
central_content: Accounts::default(), wallets: Wallets::default(),
exit_allowed, exit_allowed,
show_exit_progress: false, show_exit_progress: false,
allowed_modal_ids: vec![ allowed_modal_ids: vec![
@ -75,33 +76,39 @@ impl Root {
pub const SIDE_PANEL_MIN_WIDTH: f32 = 400.0; pub const SIDE_PANEL_MIN_WIDTH: f32 = 400.0;
pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) {
// Show opened exit confirmation Modal content. // Show opened exit confirmation modal content.
if self.can_draw_modal() { if self.can_draw_modal() {
self.exit_modal_content(ui, frame, cb); self.exit_modal_content(ui, frame, cb);
} }
let (is_panel_open, panel_width) = Self::side_panel_state_width(frame); let (is_panel_open, panel_width) = Self::network_panel_state_width(frame);
// Show network content.
egui::SidePanel::left("network_panel") egui::SidePanel::left("network_panel")
.resizable(false) .resizable(false)
.exact_width(panel_width) .exact_width(panel_width)
.frame(egui::Frame::none()) .frame(egui::Frame {
fill: Colors::WHITE,
..Default::default()
})
.show_animated_inside(ui, is_panel_open, |ui| { .show_animated_inside(ui, is_panel_open, |ui| {
// Show network content on side panel. self.network.ui(ui, frame, cb);
self.side_panel.ui(ui, frame, cb);
}); });
// Show wallets content.
egui::CentralPanel::default() egui::CentralPanel::default()
.frame(egui::Frame::none()) .frame(egui::Frame {
fill: Colors::FILL_DARK,
..Default::default()
})
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
// Show accounts content on central panel. self.wallets.ui(ui, frame, cb);
self.central_content.ui(ui, frame, cb);
}); });
} }
/// Get side panel state and width. /// Get [`Network`] panel state and width.
fn side_panel_state_width(frame: &mut eframe::Frame) -> (bool, f32) { fn network_panel_state_width(frame: &mut eframe::Frame) -> (bool, f32) {
let dual_panel_mode = Self::is_dual_panel_mode(frame); let dual_panel_mode = Self::is_dual_panel_mode(frame);
let is_panel_open = dual_panel_mode || Self::is_side_panel_open(); let is_panel_open = dual_panel_mode || Self::is_network_panel_open();
let panel_width = if dual_panel_mode { let panel_width = if dual_panel_mode {
Self::SIDE_PANEL_MIN_WIDTH + View::get_left_inset() Self::SIDE_PANEL_MIN_WIDTH + View::get_left_inset()
} else { } else {
@ -110,7 +117,7 @@ impl Root {
(is_panel_open, panel_width) (is_panel_open, panel_width)
} }
/// Check if ui can show [`Network`] and [`Accounts`] at same time. /// Check if ui can show [`Network`] and [`Wallets`] at same time.
pub fn is_dual_panel_mode(frame: &mut eframe::Frame) -> bool { pub fn is_dual_panel_mode(frame: &mut eframe::Frame) -> bool {
let w = frame.info().window_info.size.x; let w = frame.info().window_info.size.x;
let h = frame.info().window_info.size.y; let h = frame.info().window_info.size.y;
@ -123,14 +130,14 @@ impl Root {
} }
/// Toggle [`Network`] panel state. /// Toggle [`Network`] panel state.
pub fn toggle_side_panel() { pub fn toggle_network_panel() {
let is_open = SIDE_PANEL_OPEN.load(Ordering::Relaxed); let is_open = NETWORK_PANEL_OPEN.load(Ordering::Relaxed);
SIDE_PANEL_OPEN.store(!is_open, Ordering::Relaxed); NETWORK_PANEL_OPEN.store(!is_open, Ordering::Relaxed);
} }
/// Check if side panel is open. /// Check if [`Network`] panel is open.
pub fn is_side_panel_open() -> bool { pub fn is_network_panel_open() -> bool {
SIDE_PANEL_OPEN.load(Ordering::Relaxed) NETWORK_PANEL_OPEN.load(Ordering::Relaxed)
} }
/// Show exit confirmation modal. /// Show exit confirmation modal.
@ -215,26 +222,12 @@ impl Root {
} }
} }
/// Handle platform-specific Back key code event. /// Handle Back key event.
pub fn on_back() { pub fn on_back(&mut self) {
if Modal::on_back() { if Modal::on_back() {
Self::show_exit_modal() if self.wallets.on_back() {
Self::show_exit_modal()
}
} }
} }
} }
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
/// Handle Back key code event from Android.
pub extern "C" fn Java_mw_gri_android_MainActivity_onBack(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_activity: jni::objects::JObject,
) {
Root::on_back();
}

View file

@ -19,18 +19,6 @@ use egui_extras::{Size, StripBuilder};
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::views::View; use crate::gui::views::View;
/// Title action button.
pub struct TitleAction {
pub(crate) icon: Box<&'static str>,
pub(crate) on_click: Box<dyn Fn()>,
}
impl TitleAction {
pub fn new(icon: &'static str, on_click: fn()) -> Option<Self> {
Option::from(Self { icon: Box::new(icon), on_click: Box::new(on_click) })
}
}
/// Represents title content, can be single title or with animated sub-title. /// Represents title content, can be single title or with animated sub-title.
pub enum TitleType { pub enum TitleType {
Single(String), Single(String),
@ -44,8 +32,8 @@ impl TitlePanel {
pub const DEFAULT_HEIGHT: f32 = 54.0; pub const DEFAULT_HEIGHT: f32 = 54.0;
pub fn ui(title: TitleType, pub fn ui(title: TitleType,
left_action: Option<TitleAction>, mut left_content: impl FnMut(&mut egui::Ui, &mut eframe::Frame),
right_action: Option<TitleAction>, mut right_content: impl FnMut(&mut egui::Ui, &mut eframe::Frame),
ui: &mut egui::Ui, ui: &mut egui::Ui,
frame: &mut eframe::Frame) { frame: &mut eframe::Frame) {
// Setup identifier. // Setup identifier.
@ -68,7 +56,9 @@ impl TitlePanel {
.size(Size::exact(Self::DEFAULT_HEIGHT)) .size(Size::exact(Self::DEFAULT_HEIGHT))
.horizontal(|mut strip| { .horizontal(|mut strip| {
strip.cell(|ui| { strip.cell(|ui| {
Self::action_ui(ui, left_action); ui.centered_and_justified(|ui| {
(left_content)(ui, frame);
});
}); });
match title { match title {
TitleType::Single(text) => { TitleType::Single(text) => {
@ -86,7 +76,9 @@ impl TitlePanel {
} }
} }
strip.cell(|ui| { strip.cell(|ui| {
Self::action_ui(ui, right_action); ui.centered_and_justified(|ui| {
(right_content)(ui, frame);
});
}); });
}); });
}); });
@ -102,18 +94,6 @@ impl TitlePanel {
} }
} }
/// Draw panel [`TitleAction`].
fn action_ui(ui: &mut egui::Ui, action: Option<TitleAction>) {
if action.is_some() {
let action = action.unwrap();
ui.centered_and_justified(|ui| {
View::title_button(ui, &action.icon, || {
(action.on_click)();
});
});
}
}
/// Draw title text for [`TitleType::WithSubTitle`] type. /// Draw title text for [`TitleType::WithSubTitle`] type.
fn with_sub_title(builder: StripBuilder, title: String, subtitle: String, animate_sub: bool) { fn with_sub_title(builder: StripBuilder, title: String, subtitle: String, animate_sub: bool) {
builder builder

View file

@ -0,0 +1,27 @@
// 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 crate::gui::platform::PlatformCallbacks;
use crate::gui::views::wallets::creation::StepControl;
#[derive(Default)]
pub struct ConnectionSetup {
}
impl ConnectionSetup {
pub fn ui(&mut self, ui: &mut egui::Ui, step: &dyn StepControl, cb: &dyn PlatformCallbacks) {
}
}

View file

@ -0,0 +1,310 @@
// 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::{Margin, RichText, TextStyle, Widget};
use egui_extras::{Size, StripBuilder};
use crate::gui::Colors;
use crate::gui::icons::{EYE, EYE_SLASH, PLUS_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, ModalPosition, View};
use crate::gui::views::wallets::creation::{ConnectionSetup, MnemonicSetup, StepControl};
use crate::gui::views::wallets::creation::mnemonic::PhraseMode;
/// Wallet creation step.
enum Step {
/// Mnemonic phrase input.
EnterMnemonic,
/// Mnemonic phrase confirmation for [`Mnemonic`].
ConfirmMnemonic,
/// Wallet connection setup.
SetupConnection
}
/// Wallet creation content.
pub struct WalletCreation {
/// Wallet creation ui step.
step: Option<Step>,
/// Flag to check if [`Modal`] just was opened to focus on first field.
modal_just_opened: bool,
/// Wallet name value.
name_edit: String,
/// Password to encrypt created wallet.
pass_edit: String,
/// Flag to show/hide password at [`egui::TextEdit`] field.
hide_pass: bool,
/// Mnemonic phrase setup content.
pub(crate) mnemonic_setup: MnemonicSetup,
/// Network setup content.
pub(crate) network_setup: ConnectionSetup,
}
impl Default for WalletCreation {
fn default() -> Self {
Self {
step: None,
modal_just_opened: true,
name_edit: "".to_string(),
pass_edit: "".to_string(),
hide_pass: true,
mnemonic_setup: MnemonicSetup::default(),
network_setup: ConnectionSetup::default(),
}
}
}
impl StepControl for WalletCreation {
/// Go to next wallet creation [`Step`].
fn next_step(&mut self) {
self.step = match &self.step {
None => Some(Step::EnterMnemonic),
Some(step) => {
match step {
Step::EnterMnemonic => {
if self.mnemonic_setup.get_mnemonic_mode() == &PhraseMode::Generate {
Some(Step::SetupConnection)
} else {
Some(Step::ConfirmMnemonic)
}
}
Step::ConfirmMnemonic => Some(Step::SetupConnection),
Step::SetupConnection => {
//TODO: Confirm mnemonic
None
}
}
}
}
}
/// Go to previous wallet creation [`Step`].
fn prev_step(&mut self) {
match &self.step {
None => {}
Some(step) => {
match step {
Step::EnterMnemonic => {
// Clear values if it needs to go back on first step.
self.step = None;
self.name_edit = "".to_string();
self.pass_edit = "".to_string();
self.mnemonic_setup.reset();
}
Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic),
Step::SetupConnection => self.step = Some(Step::ConfirmMnemonic)
}
}
}
}
}
impl WalletCreation {
/// Wallet name/password input modal identifier.
pub const MODAL_ID: &'static str = "create_wallet_modal";
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Show wallet creation step content.
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::DEFAULT_STROKE,
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0,
top: 3.0,
bottom: 4.0,
},
..Default::default()
})
.show_inside(ui, |ui| {
self.step_ui(ui, cb);
});
}
/// Draw wallet creation [`Step`] content.
fn step_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
match &self.step {
None => {
// Show wallet creation message if step is empty.
View::center_content(ui, 124.0 + View::get_bottom_inset(), |ui| {
let text = t!("wallets.create_desc");
ui.label(RichText::new(text)
.size(16.0)
.color(Colors::INACTIVE_TEXT)
);
ui.add_space(8.0);
let add_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add"));
View::button(ui, add_text, Colors::BUTTON, || {
Self::show_modal();
});
});
}
Some(step) => {
match step {
Step::EnterMnemonic => {
self.mnemonic_setup.ui(ui, self, cb);
}
Step::ConfirmMnemonic => {}
Step::SetupConnection => {}
}
}
}
}
/// Check if it's possible to go back for current step.
pub fn can_go_back(&self) -> bool {
self.step.is_some()
}
/// Back button key event handling.
pub fn go_back(&mut self) {
self.prev_step();
}
/// Start wallet creation from showing [`Modal`] to enter name and password.
pub fn show_modal() {
Modal::show(Modal::new(Self::MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.add")));
}
/// Callback to go to next step for wallet creation from [`Modal`].
fn on_modal_confirmation(&mut self, modal: &Modal, cb: &dyn PlatformCallbacks) {
// Check if input values are not empty.
if self.name_edit.is_empty() || self.pass_edit.is_empty() {
return;
}
self.step = Some(Step::EnterMnemonic);
cb.hide_keyboard();
modal.close();
}
/// Draw wallet creation [`Modal`] content.
pub fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.name"))
.size(17.0)
.color(Colors::GRAY));
ui.add_space(8.0);
// Show wallet name text edit.
let name_resp = egui::TextEdit::singleline(&mut self.name_edit)
.id(ui.id().with("wallet_name_edit"))
.font(TextStyle::Heading)
.desired_width(ui.available_width())
.cursor_at_end(true)
.ui(ui);
ui.add_space(8.0);
if name_resp.clicked() {
cb.show_keyboard();
}
// Check if modal was just opened to show focus on name text input.
if self.modal_just_opened {
self.modal_just_opened = false;
cb.show_keyboard();
name_resp.request_focus();
}
ui.label(RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::GRAY));
ui.add_space(8.0);
StripBuilder::new(ui)
.size(Size::exact(34.0))
.vertical(|mut strip| {
strip.strip(|builder| {
builder
.size(Size::remainder())
.size(Size::exact(48.0))
.horizontal(|mut strip| {
strip.cell(|ui| {
ui.add_space(2.0);
// Draw wallet password text edit.
let pass_resp = egui::TextEdit::singleline(&mut self.pass_edit)
.id(ui.id().with("wallet_pass_edit"))
.font(TextStyle::Heading)
.desired_width(ui.available_width())
.cursor_at_end(true)
.password(self.hide_pass)
.ui(ui);
if pass_resp.clicked() {
cb.show_keyboard();
}
// Hide keyboard if input fields has no focus.
if !pass_resp.has_focus() && !name_resp.has_focus() {
cb.hide_keyboard();
}
});
strip.cell(|ui| {
ui.vertical_centered(|ui| {
// Draw button to show/hide password.
let eye_icon = if self.hide_pass { EYE } else { EYE_SLASH };
View::button(ui, eye_icon.to_string(), Colors::WHITE, || {
self.hide_pass = !self.hide_pass;
});
});
});
});
})
});
// Show information when specified values are empty.
if self.name_edit.is_empty() {
ui.add_space(12.0);
ui.label(RichText::new(t!("wallets.name_empty"))
.size(17.0)
.color(Colors::INACTIVE_TEXT));
} else if self.pass_edit.is_empty() {
ui.add_space(12.0);
ui.label(RichText::new(t!("wallets.pass_empty"))
.size(17.0)
.color(Colors::INACTIVE_TEXT));
}
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, || {
// Clear values.
self.hide_pass = false;
self.modal_just_opened = true;
self.name_edit = "".to_string();
self.pass_edit = "".to_string();
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("continue"), Colors::WHITE, || {
self.on_modal_confirmation(modal, cb);
});
});
});
ui.add_space(6.0);
});
}
}

View file

@ -0,0 +1,124 @@
// 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 grin_keychain::mnemonic::from_entropy;
use rand::{Rng, thread_rng};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::wallets::creation::StepControl;
/// Mnemonic phrase setup mode.
#[derive(PartialEq)]
pub enum PhraseMode {
/// Generate new mnemonic phrase.
Generate,
/// Import existing mnemonic phrase.
Import
}
/// Mnemonic phrase type based on words count.
pub enum PhraseType { Words12, Words15, Words18, Words21, Words24 }
impl PhraseType {
pub fn value(&self) -> usize {
match *self {
PhraseType::Words12 => 12,
PhraseType::Words15 => 15,
PhraseType::Words18 => 18,
PhraseType::Words21 => 21,
PhraseType::Words24 => 24
}
}
}
/// Mnemonic phrase container.
pub struct Mnemonic {
/// Phrase setup mode.
pub(crate) mode: PhraseMode,
/// Type of phrase based on words count.
size: PhraseType,
/// Words for phrase.
words: Vec<String>
}
impl Default for Mnemonic {
fn default() -> Self {
let size = PhraseType::Words12;
let size_value = size.value();
Self { mode: PhraseMode::Generate, size, words: Vec::with_capacity(size_value) }
}
}
impl Mnemonic {
/// Change mnemonic phrase setup [`PhraseMode`].
fn set_mode(&mut self, mode: PhraseMode) {
self.mode = mode;
self.setup_words();
}
/// Change mnemonic phrase words [`PhraseType`].
fn set_size(&mut self, size: PhraseType) {
self.size = size;
self.setup_words();
}
/// Setup words based on current [`PhraseMode`] and [`PhraseType`].
fn setup_words(&mut self) {
self.words = match self.mode {
PhraseMode::Generate => {
let mut rng = thread_rng();
let mut entropy: Vec<u8> = Vec::with_capacity(self.size.value());
for _ in 0..self.size.value() {
entropy.push(rng.gen());
}
from_entropy(&entropy).unwrap()
.split(" ")
.map(|s| s.to_string())
.collect::<Vec<String>>()
},
PhraseMode::Import => Vec::with_capacity(self.size.value())
};
}
}
/// Mnemonic phrase setup content.
pub struct MnemonicSetup {
/// Current mnemonic phrase.
mnemonic: Mnemonic,
/// Word value for [`Modal`].
word_edit: String,
}
impl Default for MnemonicSetup {
fn default() -> Self {
Self {
mnemonic: Mnemonic::default(),
word_edit: "".to_string(),
}
}
}
impl MnemonicSetup {
pub fn ui(&self, ui: &mut egui::Ui, step: &dyn StepControl, cb: &dyn PlatformCallbacks) {
}
pub fn get_mnemonic_mode(&self) -> &PhraseMode {
&self.mnemonic.mode
}
/// Reset mnemonic phrase to default values.
pub fn reset(&mut self) {
self.mnemonic = Mnemonic::default();
}
}

View file

@ -0,0 +1,30 @@
// 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.
mod mnemonic;
pub use mnemonic::MnemonicSetup;
mod connection;
pub use connection::ConnectionSetup;
mod content;
pub use content::WalletCreation;
/// Interface to provide moving between wallet creation steps.
pub trait StepControl {
/// Go to next wallet creation step.
fn next_step(&mut self);
/// Go to previous wallet creation Step.
fn prev_step(&mut self);
}

View file

@ -12,5 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
mod accounts; mod wallets;
pub use accounts::*; mod creation;
mod wallet;
pub use wallets::*;

View file

@ -12,46 +12,41 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use crate::gui::Colors; use egui::Margin;
use crate::gui::icons::{GLOBE, PLUS};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Root, TitleAction, TitleType, TitlePanel, View};
/// Accounts content. use crate::gui::Colors;
pub struct Accounts { use crate::gui::platform::PlatformCallbacks;
/// List of accounts. use crate::gui::views::View;
list: Vec<String>
/// Selected wallet list item content.
pub struct WalletContent {
/// Current wallet instance.
item: String
} }
impl Default for Accounts { impl WalletContent {
fn default() -> Self { fn new(item: String) -> Self {
Self { Self { item }
list: vec![],
}
} }
} }
impl Accounts { impl WalletContent {
pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) {
let title_content = TitleType::Single(t!("accounts.title").to_uppercase()); // Show wallet content.
TitlePanel::ui(title_content, if !Root::is_dual_panel_mode(frame) {
TitleAction::new(GLOBE, || {
Root::toggle_side_panel();
})
} else {
None
}, TitleAction::new(PLUS, || {
//TODO: add account
}), ui, frame);
egui::CentralPanel::default() egui::CentralPanel::default()
.frame(egui::Frame { .frame(egui::Frame {
stroke: View::DEFAULT_STROKE, stroke: View::DEFAULT_STROKE,
fill: Colors::FILL_DARK, fill: Colors::WHITE,
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::far_right_inset_margin(ui, frame) + 4.0,
top: 3.0,
bottom: 4.0,
},
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
//TODO: accounts list //TODO: wallet content
}); });
} }
} }

View file

@ -0,0 +1,199 @@
// 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::max;
use egui::{Align2, Margin, Vec2};
use crate::gui::Colors;
use crate::gui::icons::{ARROW_LEFT, GEAR, GLOBE, PLUS};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, ModalContainer, Root, TitlePanel, TitleType, View};
use crate::gui::views::wallets::creation::WalletCreation;
use crate::gui::views::wallets::wallet::WalletContent;
/// Wallets content.
pub struct Wallets {
/// List of wallets.
list: Vec<String>,
/// Selected list item content.
item_content: Option<WalletContent>,
/// Wallet creation content.
creation_content: WalletCreation,
/// [`Modal`] ids allowed at this ui container.
modal_ids: Vec<&'static str>
}
impl Default for Wallets {
fn default() -> Self {
//TODO load list.
Self {
list: vec![],
item_content: None,
creation_content: WalletCreation::default(),
modal_ids: vec![
WalletCreation::MODAL_ID
]
}
}
}
impl ModalContainer for Wallets {
fn modal_ids(&self) -> &Vec<&'static str> {
&self.modal_ids
}
}
impl Wallets {
pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) {
// Show modal content for current ui container.
if self.can_draw_modal() {
Modal::ui(ui, |ui, modal| {
match modal.id {
WalletCreation::MODAL_ID => self.creation_content.modal_ui(ui, modal, cb),
_ => {}
}
});
}
// Show title panel.
self.title_ui(ui, frame);
// Show wallet content.
let is_wallet_panel_open = Self::is_dual_panel_mode(ui, frame) || self.list.is_empty();
let wallet_panel_width = self.wallet_panel_width(ui, frame);
egui::SidePanel::right("wallet_panel")
.resizable(false)
.min_width(wallet_panel_width)
.frame(egui::Frame {
fill: if self.list.is_empty() { Colors::FILL_DARK } else { Colors::WHITE },
..Default::default()
})
.show_animated_inside(ui, is_wallet_panel_open, |ui| {
self.wallet_content_ui(ui, frame, cb);
});
// Show list of wallets.
if !self.list.is_empty() {
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::DEFAULT_STROKE,
fill: Colors::FILL_DARK,
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::far_right_inset_margin(ui, frame) + 4.0,
top: 3.0,
bottom: 4.0,
},
..Default::default()
})
.show_inside(ui, |ui| {
//TODO: wallets list
});
// Show wallet creation button if wallet panel is not open.
if !is_wallet_panel_open {
self.add_wallet_btn_ui(ui);
}
}
}
/// Draw title content.
fn title_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
// Setup title text.
let title_content = TitleType::Single(t!("wallets.title").to_uppercase());
// Draw title panel.
TitlePanel::ui(title_content, |ui, frame| {
if !Root::is_dual_panel_mode(frame) {
View::title_button(ui, GLOBE, || {
Root::toggle_network_panel();
});
} else if self.creation_content.can_go_back() {
View::title_button(ui, ARROW_LEFT, || {
self.creation_content.go_back();
});
};
}, |ui, frame| {
View::title_button(ui, GEAR, || {
//TODO: show settings.
});
}, ui, frame);
}
/// Draw [`WalletContent`] ui.
fn wallet_content_ui(&mut self,
ui: &mut egui::Ui,
frame: &mut eframe::Frame,
cb: &dyn PlatformCallbacks) {
if self.list.is_empty() || self.item_content.is_none() {
self.creation_content.ui(ui, cb)
} else {
self.item_content.as_mut().unwrap().ui(ui, frame, cb);
}
}
/// Get [`WalletContent`] panel width.
fn wallet_panel_width(&self, ui: &mut egui::Ui, frame: &mut eframe::Frame) -> f32 {
if Self::is_dual_panel_mode(ui, frame) {
let min_width = (Root::SIDE_PANEL_MIN_WIDTH + View::get_right_inset()) as i64;
let available_width = if self.list.is_empty() {
ui.available_width()
} else {
ui.available_width() - Root::SIDE_PANEL_MIN_WIDTH
} as i64;
max(min_width, available_width) as f32
} else {
let dual_panel_root = Root::is_dual_panel_mode(frame);
if dual_panel_root {
ui.available_width()
} else {
frame.info().window_info.size.x
}
}
}
/// Check if ui can show [`Wallets`] list and [`WalletContent`] content at same time.
fn is_dual_panel_mode(ui: &mut egui::Ui, frame: &mut eframe::Frame) -> bool {
let dual_panel_root = Root::is_dual_panel_mode(frame);
let max_width = ui.available_width();
dual_panel_root && max_width >= (Root::SIDE_PANEL_MIN_WIDTH * 2.0) + View::get_right_inset()
}
/// Draw floating button to create the wallet.
fn add_wallet_btn_ui(&self, ui: &mut egui::Ui) {
egui::Window::new("create_wallet_button")
.title_bar(false)
.resizable(false)
.collapsible(false)
.anchor(Align2::RIGHT_BOTTOM, Vec2::new(-8.0, -8.0))
.frame(egui::Frame::default())
.show(ui.ctx(), |ui| {
View::round_button(ui, PLUS, || {
WalletCreation::show_modal();
});
});
}
/// Handle Back key event.
/// Return `false` when event was handled.
pub fn on_back(&mut self) -> bool {
let can_go_back = self.creation_content.can_go_back();
if can_go_back {
self.creation_content.go_back();
}
!can_go_back
}
}

View file

@ -12,6 +12,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
pub mod wallet; mod wallet;
// pub use self::wallet::{init, init_from_seed}; // pub use self::wallet::{init, init_from_seed};

View file

@ -12,10 +12,3 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
pub fn create_from_seed() {
}
pub fn create() {
}