build + android + wallet + ui: new lib to get IPs, egui, android and grin deps, add input from soft keyboard to text edit, receive txs, filter txs by account, finalize input (in rework), wallet creation copy/paste buttons

This commit is contained in:
ardocrat 2024-04-16 15:24:22 +03:00
parent 02a79188bf
commit b93a2efd5e
32 changed files with 1518 additions and 743 deletions

1020
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -33,24 +33,24 @@ grin_wallet_util = { git = "https://github.com/mimblewimble/grin-wallet", branch
grin_wallet_controller = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" } grin_wallet_controller = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" }
## ui ## ui
egui = { version = "0.23.0", default-features = false } egui = { version = "0.27.2", default-features = false }
egui_extras = { version = "0.23.0", features = ["image"] } egui_extras = { version = "0.27.2", features = ["image"] }
rust-i18n = "2.1.0" rust-i18n = "2.1.0"
## other ## other
futures = "0.3" futures = "0.3"
dirs = "5.0.1" dirs = "5.0.1"
sys-locale = "0.3.0" sys-locale = "0.3.0"
chrono = "0.4.37" chrono = "0.4.31"
lazy_static = "1.4.0" lazy_static = "1.4.0"
toml = "0.8.2" toml = "0.8.2"
serde = "1.0.170" serde = "1.0.170"
pnet = "0.34.0" local-ip-address = "0.6.1"
url = "2.4.0" url = "2.4.0"
# stratum server # stratum server
serde_derive = "1" serde_derive = "1.0.197"
serde_json = "1" serde_json = "1.0.115"
tokio = {version = "1.29.1", features = ["full"] } tokio = {version = "1.29.1", features = ["full"] }
tokio-util = { version = "0.7.8", features = ["codec"] } tokio-util = { version = "0.7.8", features = ["codec"] }
rand = "0.8.5" rand = "0.8.5"
@ -60,13 +60,13 @@ built = { version = "0.7.0", features = ["git2"]}
[target.'cfg(not(target_os = "android"))'.dependencies] [target.'cfg(not(target_os = "android"))'.dependencies]
env_logger = "0.10.0" env_logger = "0.10.0"
winit = { version = "0.28" } winit = { version = "0.29.15" }
eframe = { version = "0.23.0", features = [ "wgpu" ] } eframe = { version = "0.27.2", features = [ "wgpu" ] }
arboard = "3.2.0" arboard = "3.2.0"
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13.1" android_logger = "0.13.1"
jni = "0.21.1" jni = "0.21.1"
android-activity = "0.4.3" android-activity = "0.5.2"
winit = { version = "0.28", features = [ "android-game-activity" ] } winit = { version = "0.29", features = [ "android-game-activity" ] }
eframe = { version = "0.23.0", features = [ "wgpu", "android-game-activity" ] } eframe = { version = "0.27.2", features = [ "wgpu", "android-game-activity" ] }

View file

@ -41,7 +41,7 @@ dependencies {
//implementation "androidx.games:games-performance-tuner:1.5.0" //implementation "androidx.games:games-performance-tuner:1.5.0"
// To use the Games Activity library // To use the Games Activity library
implementation "androidx.games:games-activity:1.1.0" implementation "androidx.games:games-activity:2.0.2"
// To use the Games Controller Library // To use the Games Controller Library
//implementation "androidx.games:games-controller:1.1.0" //implementation "androidx.games:games-controller:1.1.0"

View file

@ -3,7 +3,6 @@
> >
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />

View file

@ -7,6 +7,7 @@ import android.system.ErrnoException;
import android.system.Os; import android.system.Os;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.View; import android.view.View;
import android.view.inputmethod.InputMethodManager;
import androidx.core.graphics.Insets; import androidx.core.graphics.Insets;
import androidx.core.view.DisplayCutoutCompat; import androidx.core.view.DisplayCutoutCompat;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
@ -80,12 +81,33 @@ public class MainActivity extends GameActivity {
}); });
} }
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// To support non-english input.
if (event.getAction() == KeyEvent.ACTION_MULTIPLE && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
if (!event.getCharacters().isEmpty()) {
onInput(event.getCharacters());
return false;
}
// Pass any other input values into native code.
} else if (event.getAction() == KeyEvent.ACTION_UP &&
event.getKeyCode() != KeyEvent.KEYCODE_ENTER &&
event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
onInput(String.valueOf((char)event.getUnicodeChar()));
return false;
}
return super.dispatchKeyEvent(event);
}
// Provide last entered character from soft keyboard into native code.
public native void onInput(String character);
// Implemented into native code to handle display insets change. // Implemented into native code to handle display insets change.
native void onDisplayInsets(int[] cutouts); native void onDisplayInsets(int[] cutouts);
@Override @Override
public boolean onKeyDown(int keyCode, KeyEvent event) { public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) { if (keyCode == KeyEvent.KEYCODE_BACK) {
onBack(); onBack();
return true; return true;
} }
@ -146,4 +168,14 @@ public class MainActivity extends GameActivity {
} }
return text; return text;
} }
public void showKeyboard() {
InputMethodManager imm = (InputMethodManager )getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(getWindow().getDecorView(), InputMethodManager.SHOW_IMPLICIT);
}
public void hideKeyboard() {
InputMethodManager imm = (InputMethodManager )getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0);
}
} }

View file

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id 'com.android.application' version '7.4.1' apply false id 'com.android.application' version '8.1.0' apply false
id 'com.android.library' version '7.4.1' apply false id 'com.android.library' version '8.1.0' apply false
} }
task clean(type: Delete) { task clean(type: Delete) {

View file

@ -29,6 +29,10 @@ type=$1
[[ $2 == "v7" ]] && platform+=(armv7-linux-androideabi) [[ $2 == "v7" ]] && platform+=(armv7-linux-androideabi)
[[ $2 == "v8" ]] && platform+=(aarch64-linux-android) [[ $2 == "v8" ]] && platform+=(aarch64-linux-android)
# Install platform
[[ $2 == "v7" ]] && rustup target install armv7-linux-androideabi
[[ $2 == "v8" ]] && rustup target install aarch64-linux-android
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" \ export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" \
&& cargo ndk -t ${arch} build ${release_param[@]} && cargo ndk -t ${arch} build ${release_param[@]}

View file

@ -19,3 +19,5 @@ android.useAndroidX=true
# resources declared in the library itself and none from the library's dependencies, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=false

View file

@ -1,6 +1,6 @@
#Mon May 02 15:39:12 BST 2022 #Mon May 02 15:39:12 BST 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View file

@ -64,11 +64,18 @@ wallets:
tx_canceled: Canceled tx_canceled: Canceled
tx_confirmed: Confirmed tx_confirmed: Confirmed
manually: Manually manually: Manually
receive_paste_slatepack: 'Enter Slatepack message received from the sender to create a response:' receive_slatepack_desc: 'Enter Slatepack message received from the sender to create response or finalize the transaction:'
receive_send_slatepack: 'Send response to the sender to finalize the transaction:' receive_send_slatepack: 'Send Slatepack message to the sender to finalize the transaction:'
receive_slatepack_err: An error occurred during creation of the response, check input data. receive_slatepack_err: 'An error occurred during creation of the response, check input data:'
response_copied: Response copied to the clipboard.
create_response: Create response create_response: Create response
invoice: Invoice
issue_invoice: Issue invoice
issue_invoice_desc: 'Create a request to receive funds by entering the required amount:'
invoice_desc: 'You have created a request to receive %{amount} ツ. Send Slatepack message to the sender:'
invoice_slatepack_err: An error occurred during issuing of the invoice, check input data.
finalize_slatepack_err: 'An error occurred during finalization of the transaction, check input data:'
finalize: Finalize
enter_amount: 'Enter amount:'
recovery: Recovery recovery: Recovery
repair_wallet: Repair wallet repair_wallet: Repair wallet
repair_desc: Check a wallet, repairing and restoring missing outputs if required. This operation will take time. repair_desc: Check a wallet, repairing and restoring missing outputs if required. This operation will take time.

View file

@ -44,7 +44,7 @@ wallets:
locked: Заблокирован locked: Заблокирован
unlocked: Разблокирован unlocked: Разблокирован
enable_node: 'Чтобы использовать кошелёк, включите встроенный узел или измените настройки подключения, выбрав %{settings} внизу экрана.' enable_node: 'Чтобы использовать кошелёк, включите встроенный узел или измените настройки подключения, выбрав %{settings} внизу экрана.'
node_loading: Кошелёк будет загружен после синхронизации встроенного узла, вы можете изменить настройки подключения, выбрав %{settings} внизу экрана. node_loading: 'Кошелёк будет загружен после синхронизации встроенного узла, вы можете изменить настройки подключения, выбрав %{settings} внизу экрана.'
loading: Загружается loading: Загружается
closing: Закрывается closing: Закрывается
checking: Проверяется checking: Проверяется
@ -64,11 +64,18 @@ wallets:
tx_canceled: Отменено tx_canceled: Отменено
tx_confirmed: Подтверждено tx_confirmed: Подтверждено
manually: Вручную manually: Вручную
receive_paste_slatepack: 'Введите Slatepack сообщение, полученное от отправителя для создания ответа:' receive_slatepack_desc: 'Введите Slatepack сообщение, полученное от отправителя, для создания ответа или завершения транзакции:'
receive_send_slatepack: 'Отправьте ответ отправителю для завершения транзакции:' receive_send_slatepack: 'Отправьте Slatepack сообщение отправителю для завершения транзакции:'
receive_slatepack_err: Во время создания ответа произошла ошибка, проверьте входные данные. receive_slatepack_err: 'Во время создания ответа произошла ошибка, проверьте входные данные:'
response_copied: Ответ скопирован в буфер обмена.
create_response: Создать ответ create_response: Создать ответ
invoice: Инвойс
issue_invoice: Выставить счёт
issue_invoice_desc: 'Создайте запрос на получение средств, введя требуемое количество:'
invoice_desc: 'Вы создали запрос на получение %{amount} ツ. Отправьте Slatepack сообщение отправителю:'
invoice_slatepack_err: Во время выставления счёта произошла ошибка, проверьте входные данные.
finalize_slatepack_err: 'Во время завершения транзакции произошла ошибка, проверьте входные данные:'
finalize: Завершить
enter_amount: 'Введите количество:'
recovery: Восстановление recovery: Восстановление
repair_wallet: Починить кошелёк repair_wallet: Починить кошелёк
repair_desc: Проверить кошелёк, исправляя и восстанавливая недостающие выходы, если это необходимо. Эта операция займёт время. repair_desc: Проверить кошелёк, исправляя и восстанавливая недостающие выходы, если это необходимо. Эта операция займёт время.

View file

@ -45,12 +45,21 @@ impl<Platform: PlatformCallbacks> eframe::App for PlatformApp<Platform> {
// Handle Esc keyboard key event and platform Back button key event. // Handle Esc keyboard key event and platform Back button key event.
let back_button_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed); let back_button_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) || back_button_pressed { if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) || back_button_pressed {
if back_button_pressed {
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
}
self.root.on_back(); self.root.on_back();
// Request repaint to update previous content. // Request repaint to update previous content.
ctx.request_repaint(); ctx.request_repaint();
if back_button_pressed {
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
}
}
// Handle Close event (on desktop).
if ctx.input(|i| i.viewport().close_requested()) {
if !self.root.exit_allowed {
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
Root::show_exit_modal();
}
} }
// Show main content. // Show main content.
@ -63,14 +72,6 @@ impl<Platform: PlatformCallbacks> eframe::App for PlatformApp<Platform> {
self.root.ui(ui, frame, &self.platform); self.root.ui(ui, frame, &self.platform);
}); });
} }
fn on_close_event(&mut self) -> bool {
let exit = self.root.exit_allowed;
if !exit {
Root::show_exit_modal();
}
exit
}
} }
#[allow(dead_code)] #[allow(dead_code)]

View file

@ -30,11 +30,41 @@ impl Android {
impl PlatformCallbacks for Android { impl PlatformCallbacks for Android {
fn show_keyboard(&self) { fn show_keyboard(&self) {
self.android_app.show_soft_input(true); // Disable NDK soft input show call before fix for egui.
// self.android_app.show_soft_input(false);
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)
};
let _ = env.call_method(
activity,
"showKeyboard",
"()V",
&[]
).unwrap();
} }
fn hide_keyboard(&self) { fn hide_keyboard(&self) {
self.android_app.hide_soft_input(true); // Disable NDK soft input hide call before fix for egui.
// self.android_app.hide_soft_input(false);
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)
};
let _ = env.call_method(
activity,
"hideKeyboard",
"()V",
&[]
).unwrap();
} }
fn copy_string_to_buffer(&self, data: String) { fn copy_string_to_buffer(&self, data: String) {

View file

@ -12,8 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use egui::{RichText, ScrollArea, Stroke}; use egui::{Margin, RichText, ScrollArea, Stroke};
use egui::style::Margin;
use crate::AppConfig; use crate::AppConfig;
use crate::gui::Colors; use crate::gui::Colors;
@ -57,7 +56,7 @@ impl NetworkContent {
fill: Colors::FILL, fill: Colors::FILL,
inner_margin: Margin { inner_margin: Margin {
left: View::get_left_inset() + 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) + 4.0,
top: 4.0, top: 4.0,
bottom: View::get_bottom_inset() + 4.0, bottom: View::get_bottom_inset() + 4.0,
}, },
@ -82,7 +81,7 @@ impl NetworkContent {
stroke: View::DEFAULT_STROKE, stroke: View::DEFAULT_STROKE,
inner_margin: Margin { inner_margin: Margin {
left: View::get_left_inset() + 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) + 4.0,
top: 3.0, top: 3.0,
bottom: 4.0, bottom: 4.0,
}, },
@ -108,7 +107,7 @@ impl NetworkContent {
0.0 0.0
}, },
right: if show_connections { right: if show_connections {
View::far_right_inset_margin(ui, frame) + 4.0 View::far_right_inset_margin(ui) + 4.0
} else { } else {
0.0 0.0
}, },
@ -128,7 +127,7 @@ impl NetworkContent {
.show(ui, |ui| { .show(ui, |ui| {
ui.add_space(1.0); ui.add_space(1.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
let max_width = if !Root::is_dual_panel_mode(frame) { let max_width = if !Root::is_dual_panel_mode(ui) {
Root::SIDE_PANEL_WIDTH * 1.3 Root::SIDE_PANEL_WIDTH * 1.3
} else { } else {
ui.available_width() ui.available_width()
@ -212,7 +211,7 @@ impl NetworkContent {
}); });
} }
}, |ui, frame| { }, |ui, frame| {
if !Root::is_dual_panel_mode(frame) { if !Root::is_dual_panel_mode(ui) {
View::title_button(ui, CARDHOLDER, || { View::title_button(ui, CARDHOLDER, || {
Root::toggle_network_panel(); Root::toggle_network_panel();
}); });

View file

@ -13,7 +13,6 @@
// 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;
@ -70,11 +69,11 @@ impl ModalContainer for Root {
fn modal_ui(&mut self, fn modal_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
frame: &mut eframe::Frame, _: &mut eframe::Frame,
modal: &Modal, modal: &Modal,
_: &dyn PlatformCallbacks) { _: &dyn PlatformCallbacks) {
match modal.id { match modal.id {
Self::EXIT_MODAL_ID => self.exit_modal_content(ui, frame, modal), Self::EXIT_MODAL_ID => self.exit_modal_content(ui, modal),
_ => {} _ => {}
} }
} }
@ -91,7 +90,7 @@ impl Root {
// Draw modal content for current ui container. // Draw modal content for current ui container.
self.current_modal_ui(ui, frame, cb); self.current_modal_ui(ui, frame, cb);
let (is_panel_open, panel_width) = Self::network_panel_state_width(frame); let (is_panel_open, panel_width) = Self::network_panel_state_width(ui);
// Show network content. // Show network content.
egui::SidePanel::left("network_panel") egui::SidePanel::left("network_panel")
.resizable(false) .resizable(false)
@ -103,7 +102,8 @@ impl Root {
.show_animated_inside(ui, is_panel_open, |ui| { .show_animated_inside(ui, is_panel_open, |ui| {
// Set content height as window height. // Set content height as window height.
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
rect.set_height(frame.info().window_info.size.y); let window_size = View::window_size(ui);
rect.set_height(window_size.1);
ui.allocate_ui_at_rect(rect, |ui| { ui.allocate_ui_at_rect(rect, |ui| {
self.network.ui(ui, frame, cb); self.network.ui(ui, frame, cb);
}); });
@ -118,7 +118,8 @@ impl Root {
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
// Set content height as window height. // Set content height as window height.
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
rect.set_height(frame.info().window_info.size.y); let window_size = View::window_size(ui);
rect.set_height(window_size.1);
ui.allocate_ui_at_rect(rect, |ui| { ui.allocate_ui_at_rect(rect, |ui| {
self.wallets.ui(ui, frame, cb); self.wallets.ui(ui, frame, cb);
}); });
@ -126,21 +127,20 @@ impl Root {
} }
/// Get [`NetworkContent`] panel state and width. /// Get [`NetworkContent`] panel state and width.
fn network_panel_state_width(frame: &mut eframe::Frame) -> (bool, f32) { fn network_panel_state_width(ui: &mut egui::Ui) -> (bool, f32) {
let dual_panel_mode = Self::is_dual_panel_mode(frame); let dual_panel_mode = Self::is_dual_panel_mode(ui);
let is_panel_open = dual_panel_mode || Self::is_network_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_WIDTH + View::get_left_inset() Self::SIDE_PANEL_WIDTH + View::get_left_inset()
} else { } else {
frame.info().window_info.size.x View::window_size(ui).0
}; };
(is_panel_open, panel_width) (is_panel_open, panel_width)
} }
/// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time. /// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time.
pub fn is_dual_panel_mode(frame: &mut eframe::Frame) -> bool { pub fn is_dual_panel_mode(ui: &mut egui::Ui) -> bool {
let w = frame.info().window_info.size.x; let (w, h) = View::window_size(ui);
let h = frame.info().window_info.size.y;
// Screen is wide if width is greater than height or just 20% smaller. // Screen is wide if width is greater than height or just 20% smaller.
let is_wide_screen = w > h || w + (w * 0.2) >= h; 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 // Dual panel mode is available when window is wide and its width is at least 2 times
@ -168,10 +168,11 @@ impl Root {
} }
/// Draw exit confirmation modal content. /// Draw exit confirmation modal content.
fn exit_modal_content(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, modal: &Modal) { fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal) {
if self.show_exit_progress { if self.show_exit_progress {
if !Node::is_running() { if !Node::is_running() {
self.exit(frame); self.exit_allowed = true;
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
modal.close(); modal.close();
} }
ui.add_space(16.0); ui.add_space(16.0);
@ -199,9 +200,10 @@ impl Root {
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| { columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal_exit.exit"), Colors::WHITE, || { View::button_ui(ui, t!("modal_exit.exit"), Colors::WHITE, |ui| {
if !Node::is_running() { if !Node::is_running() {
self.exit(frame); self.exit_allowed = true;
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
modal.close(); modal.close();
} else { } else {
Node::stop(true); Node::stop(true);
@ -221,12 +223,6 @@ impl Root {
} }
} }
/// Exit from the application.
fn exit(&mut self, frame: &mut eframe::Frame) {
self.exit_allowed = true;
frame.close();
}
/// Handle Back key event. /// Handle Back key event.
pub fn on_back(&mut self) { pub fn on_back(&mut self) {
if Modal::on_back() { if Modal::on_back() {

View file

@ -12,8 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use egui::{Color32, Id, lerp, Rgba}; use egui::{Margin, Color32, Id, lerp, Rgba};
use egui::style::Margin;
use egui_extras::{Size, Strip, StripBuilder}; use egui_extras::{Size, Strip, StripBuilder};
use crate::gui::Colors; use crate::gui::Colors;
@ -59,7 +58,7 @@ impl TitlePanel {
.resizable(false) .resizable(false)
.exact_height(Self::DEFAULT_HEIGHT) .exact_height(Self::DEFAULT_HEIGHT)
.frame(egui::Frame { .frame(egui::Frame {
inner_margin: Self::inner_margin(ui, frame), inner_margin: Self::inner_margin(ui),
fill: Colors::YELLOW, fill: Colors::YELLOW,
..Default::default() ..Default::default()
}) })
@ -132,10 +131,10 @@ impl TitlePanel {
} }
/// Calculate inner margin based on display insets (cutouts). /// Calculate inner margin based on display insets (cutouts).
fn inner_margin(ui: &mut egui::Ui, frame: &mut eframe::Frame) -> Margin { fn inner_margin(ui: &mut egui::Ui) -> Margin {
Margin { Margin {
left: View::far_left_inset_margin(ui), left: View::far_left_inset_margin(ui),
right: View::far_right_inset_margin(ui, frame), right: View::far_right_inset_margin(ui),
top: View::get_top_inset(), top: View::get_top_inset(),
bottom: 0.0, bottom: 0.0,
} }

View file

@ -12,13 +12,17 @@
// 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::ops::Add;
use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::{Arc, RwLock};
use lazy_static::lazy_static;
use egui::{Align, Button, CursorIcon, Layout, PointerState, Rect, Response, RichText, Sense, Spinner, TextStyle, Vec2, Widget}; use egui::{Align, Button, CursorIcon, Layout, PointerState, Rect, Response, RichText, Sense, Spinner, TextBuffer, TextStyle, Widget};
use egui::epaint::{CircleShape, Color32, FontId, RectShape, Rounding, Stroke}; use egui::epaint::{CircleShape, Color32, FontId, RectShape, Rounding, Stroke};
use egui::epaint::text::TextWrapping; use egui::epaint::text::TextWrapping;
use egui::os::OperatingSystem;
use egui::text::{LayoutJob, TextFormat}; use egui::text::{LayoutJob, TextFormat};
use lazy_static::lazy_static; use egui::text_edit::TextEditState;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{CHECK_SQUARE, CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SQUARE}; use crate::gui::icons::{CHECK_SQUARE, CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SQUARE};
@ -54,6 +58,21 @@ impl View {
}); });
} }
/// Get width and height of app window.
pub fn window_size(ui: &mut egui::Ui) -> (f32, f32) {
ui.ctx().input(|i| {
return match i.viewport().inner_rect {
None => {
let size = i.viewport().monitor_size.unwrap();
(size.x, size.y)
}
Some(rect) => {
(rect.width(), rect.height())
}
};
})
}
/// Callback on Enter key press event. /// Callback on Enter key press event.
pub fn on_enter_key(ui: &mut egui::Ui, cb: impl FnOnce()) { pub fn on_enter_key(ui: &mut egui::Ui, cb: impl FnOnce()) {
if ui.ctx().input(|i| i.key_pressed(egui::Key::Enter)) { if ui.ctx().input(|i| i.key_pressed(egui::Key::Enter)) {
@ -71,9 +90,10 @@ impl View {
} }
/// Calculate margin for far left view based on display insets (cutouts). /// Calculate margin for far left view based on display insets (cutouts).
pub fn far_right_inset_margin(ui: &mut egui::Ui, frame: &mut eframe::Frame) -> f32 { pub fn far_right_inset_margin(ui: &mut egui::Ui) -> f32 {
let container_width = ui.available_rect_before_wrap().max.x as i32; let container_width = ui.available_rect_before_wrap().max.x as i32;
let display_width = frame.info().window_info.size.x as i32; let window_size = Self::window_size(ui);
let display_width = window_size.0 as i32;
// Means end of the screen. // Means end of the screen.
if container_width == display_width { if container_width == display_width {
Self::get_right_inset() Self::get_right_inset()
@ -198,6 +218,19 @@ impl View {
} }
} }
/// Draw [`Button`] with specified background fill color.
pub fn button_ui(ui: &mut egui::Ui, text: String, fill: Color32, action: impl FnOnce(&mut egui::Ui)) {
let button_text = Self::ellipsize(text.to_uppercase(), 17.0, Colors::TEXT_BUTTON);
let br = Button::new(button_text)
.stroke(Self::DEFAULT_STROKE)
.fill(fill)
.ui(ui)
.on_hover_cursor(CursorIcon::PointingHand);
if Self::touched(ui, br) {
(action)(ui);
}
}
/// Draw list item [`Button`] with provided rounding. /// Draw list item [`Button`] with provided rounding.
pub fn item_button(ui: &mut egui::Ui, pub fn item_button(ui: &mut egui::Ui,
rounding: Rounding, rounding: Rounding,
@ -336,15 +369,17 @@ impl View {
// Setup text edit size. // Setup text edit size.
let mut edit_rect = ui.available_rect_before_wrap(); let mut edit_rect = ui.available_rect_before_wrap();
edit_rect.set_height(Self::TEXT_EDIT_HEIGHT); edit_rect.set_height(Self::TEXT_EDIT_HEIGHT);
// Show text edit. // Show text edit.
let text_edit_resp = egui::TextEdit::singleline(value) let text_edit_resp = egui::TextEdit::singleline(value)
.id(options.id) .id(options.id)
.margin(Vec2::new(2.0, 0.0)) .margin(egui::Vec2::new(2.0, 0.0))
.font(TextStyle::Heading) .font(TextStyle::Heading)
.min_size(edit_rect.size()) .min_size(edit_rect.size())
.horizontal_align(if options.h_center { Align::Center } else { Align::Min }) .horizontal_align(if options.h_center { Align::Center } else { Align::Min })
.vertical_align(Align::Center) .vertical_align(Align::Center)
.password(show_pass) .password(show_pass)
.cursor_at_end(true)
.ui(ui); .ui(ui);
// Show keyboard on click. // Show keyboard on click.
if text_edit_resp.clicked() { if text_edit_resp.clicked() {
@ -355,6 +390,42 @@ impl View {
text_edit_resp.request_focus(); text_edit_resp.request_focus();
cb.show_keyboard(); cb.show_keyboard();
} }
// Apply text from input on Android as temporary fix for egui.
let os = OperatingSystem::from_target_os();
if os == OperatingSystem::Android && text_edit_resp.has_focus() {
let mut w_input = LAST_SOFT_KEYBOARD_INPUT.write().unwrap();
if !w_input.is_empty() {
let mut state = TextEditState::load(ui.ctx(), options.id).unwrap();
match state.cursor.char_range() {
None => {}
Some(range) => {
let mut r = range.clone();
let mut index = r.primary.index;
println!("insert_str {} {}", index, w_input.as_str());
value.insert_text(w_input.as_str(), index);
index = index + 1;
println!("12345 {} {}", value, r.primary.index);
if index == 0 {
r.primary.index = value.len();
r.secondary.index = r.primary.index;
} else {
r.primary.index = index;
r.secondary.index = r.primary.index;
}
state.cursor.set_char_range(Some(r));
TextEditState::store(state, ui.ctx(), options.id);
}
}
}
*w_input = "".to_string();
ui.ctx().request_repaint();
}
}); });
}); });
} }
@ -572,3 +643,32 @@ pub extern "C" fn Java_mw_gri_android_MainActivity_onDisplayInsets(
BOTTOM_DISPLAY_INSET.store(array[2], Ordering::Relaxed); BOTTOM_DISPLAY_INSET.store(array[2], Ordering::Relaxed);
LEFT_DISPLAY_INSET.store(array[3], Ordering::Relaxed); LEFT_DISPLAY_INSET.store(array[3], Ordering::Relaxed);
} }
lazy_static! {
static ref LAST_SOFT_KEYBOARD_INPUT: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
/// Callback from Java code with last entered character from soft keyboard.
pub extern "C" fn Java_mw_gri_android_MainActivity_onInput(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
char: jni::sys::jstring
) {
use jni::objects::{JString};
unsafe {
let j_obj = JString::from_raw(char);
let j_str = _env.get_string_unchecked(j_obj.as_ref()).unwrap();
match j_str.to_str() {
Ok(str) => {
let mut w_input = LAST_SOFT_KEYBOARD_INPUT.write().unwrap();
*w_input = w_input.clone().add(str);
}
Err(_) => {}
}
}
}

View file

@ -98,7 +98,7 @@ impl WalletsContent {
let show_wallet = self.wallets.is_selected_open(); let show_wallet = self.wallets.is_selected_open();
// Setup panels parameters. // Setup panels parameters.
let dual_panel = is_dual_panel_mode(ui, frame); let dual_panel = is_dual_panel_mode(ui);
let open_wallet_panel = dual_panel || show_wallet || create_wallet || empty_list; let open_wallet_panel = dual_panel || show_wallet || create_wallet || empty_list;
let wallet_panel_width = self.wallet_panel_width(ui, empty_list, dual_panel, show_wallet); let wallet_panel_width = self.wallet_panel_width(ui, empty_list, dual_panel, show_wallet);
let content_width = ui.available_width(); let content_width = ui.available_width();
@ -131,10 +131,10 @@ impl WalletsContent {
if create_wallet || !show_wallet { if create_wallet || !show_wallet {
// Show wallet creation content. // Show wallet creation content.
self.creation_content.ui(ui, frame, cb, |wallet| { self.creation_content.ui(ui, frame, cb, |wallet| {
// Reset wallet content.
self.wallet_content = WalletContent::default();
// Add created wallet to list. // Add created wallet to list.
self.wallets.add(wallet); self.wallets.add(wallet);
// Reset wallet content.
self.wallet_content = WalletContent::default();
}); });
} else { } else {
let selected_id = self.wallets.selected_id.clone(); let selected_id = self.wallets.selected_id.clone();
@ -180,7 +180,7 @@ impl WalletsContent {
right: if list_hidden { right: if list_hidden {
0.0 0.0
} else { } else {
View::far_right_inset_margin(ui, frame) + 4.0 View::far_right_inset_margin(ui) + 4.0
}, },
top: 4.0, top: 4.0,
bottom: View::get_bottom_inset() + 4.0, bottom: View::get_bottom_inset() + 4.0,
@ -274,7 +274,7 @@ impl WalletsContent {
self.show_wallets_at_dual_panel = !show_list; self.show_wallets_at_dual_panel = !show_list;
AppConfig::toggle_show_wallets_at_dual_panel(); AppConfig::toggle_show_wallets_at_dual_panel();
}); });
} else if !Root::is_dual_panel_mode(frame) { } else if !Root::is_dual_panel_mode(ui) {
View::title_button(ui, GLOBE, || { View::title_button(ui, GLOBE, || {
Root::toggle_network_panel(); Root::toggle_network_panel();
}); });
@ -605,8 +605,8 @@ impl WalletsContent {
} }
/// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time. /// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time.
fn is_dual_panel_mode(ui: &mut egui::Ui, frame: &mut eframe::Frame) -> bool { fn is_dual_panel_mode(ui: &mut egui::Ui) -> bool {
let dual_panel_root = Root::is_dual_panel_mode(frame); let dual_panel_root = Root::is_dual_panel_mode(ui);
let max_width = ui.available_width(); let max_width = ui.available_width();
dual_panel_root && max_width >= (Root::SIDE_PANEL_WIDTH * 2.0) + View::get_right_inset() dual_panel_root && max_width >= (Root::SIDE_PANEL_WIDTH * 2.0) + View::get_right_inset()
} }

View file

@ -17,7 +17,7 @@ use egui_extras::{RetainedImage, Size, StripBuilder};
use crate::built_info; use crate::built_info;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{CHECK, EYE, EYE_SLASH, FOLDER_PLUS, SHARE_FAT}; use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, FOLDER_PLUS, SHARE_FAT};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Root, View}; use crate::gui::views::{Modal, Root, View};
use crate::gui::views::types::{ModalPosition, TextEditOptions}; use crate::gui::views::types::{ModalPosition, TextEditOptions};
@ -86,7 +86,7 @@ impl WalletCreation {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 2.0, |ui| { View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 2.0, |ui| {
self.step_control_ui(ui, on_create); self.step_control_ui(ui, on_create, cb);
}); });
}); });
@ -130,17 +130,12 @@ impl WalletCreation {
}); });
} }
/// Reset wallet creation to default values.
fn reset(&mut self) {
self.step = None;
self.name_edit = String::from("");
self.pass_edit = String::from("");
self.mnemonic_setup.reset();
}
/// Draw [`Step`] description and confirmation control. /// Draw [`Step`] description and confirmation control.
fn step_control_ui(&mut self, ui: &mut egui::Ui, on_create: impl FnOnce(Wallet)) { fn step_control_ui(&mut self,
if let Some(step) = &self.step { ui: &mut egui::Ui,
on_create: impl FnOnce(Wallet),
cb: &dyn PlatformCallbacks) {
if let Some(step) = self.step.clone() {
// Setup step description text and availability. // Setup step description text and availability.
let (step_text, mut step_available) = match step { let (step_text, mut step_available) = match step {
Step::EnterMnemonic => { Step::EnterMnemonic => {
@ -179,27 +174,121 @@ impl WalletCreation {
.color(Colors::RED)); .color(Colors::RED));
ui.add_space(2.0); ui.add_space(2.0);
} }
if step == Step::EnterMnemonic {
// Show next step button if there are no empty words. ui.add_space(4.0);
if step_available { if !step_available {
// Setup button text. self.copy_or_paste_button_ui(ui, cb);
let (next_text, color) = if step == &Step::SetupConnection {
(format!("{} {}", CHECK, t!("complete")), Colors::GOLD)
} else { } else {
let text = format!("{} {}", SHARE_FAT, t!("continue")); // Setup spacing between buttons.
(text, Colors::WHITE) ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0);
};
ui.add_space(4.0); ui.columns(2, |columns| {
// Show button. // Show copy or paste button for mnemonic phrase step.
View::button(ui, next_text.to_uppercase(), color, || { columns[0].vertical_centered_justified(|ui| {
self.forward(on_create); self.copy_or_paste_button_ui(ui, cb);
});
// Show next step button if there are no empty words.
if step_available {
columns[1].vertical_centered_justified(|ui| {
self.next_step_button_ui(ui, step, on_create);
});
}
});
}
} else {
if step_available {
ui.add_space(4.0);
self.next_step_button_ui(ui, step, on_create);
}
}
ui.add_space(4.0);
}
}
/// Draw copy or paste button at [`Step::EnterMnemonic`].
fn copy_or_paste_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
match self.mnemonic_setup.mnemonic.mode {
PhraseMode::Generate => {
// Show copy button.
let c_t = format!("{} {}", COPY, t!("copy").to_uppercase());
View::button(ui, c_t.to_uppercase(), Colors::WHITE, || {
cb.copy_string_to_buffer(self.mnemonic_setup.mnemonic.get_phrase());
});
}
PhraseMode::Import => {
// Show paste button.
let p_t = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
View::button(ui, p_t, Colors::WHITE, || {
self.mnemonic_setup.mnemonic.import_text(cb.get_string_from_buffer());
}); });
ui.add_space(4.0);
} }
} }
} }
/// Draw button to go to next [`Step`].
fn next_step_button_ui(&mut self,
ui: &mut egui::Ui,
step: Step,
on_create: impl FnOnce(Wallet)) {
// Setup button text.
let (next_text, color) = if step == Step::SetupConnection {
(format!("{} {}", CHECK, t!("complete")), Colors::GOLD)
} else {
let text = format!("{} {}", SHARE_FAT, t!("continue"));
(text, Colors::WHITE)
};
// Show next step button.
View::button(ui, next_text.to_uppercase(), color, || {
self.step = if let Some(step) = &self.step {
match step {
Step::EnterMnemonic => {
if self.mnemonic_setup.mnemonic.mode == PhraseMode::Generate {
Some(Step::ConfirmMnemonic)
} else {
// Check if entered phrase was valid.
if self.mnemonic_setup.valid_phrase {
Some(Step::SetupConnection)
} else {
Some(Step::EnterMnemonic)
}
}
}
Step::ConfirmMnemonic => {
// Check external connections availability on connection setup.
ExternalConnection::start_ext_conn_availability_check();
Some(Step::SetupConnection)
},
Step::SetupConnection => {
// Create wallet at last step.
let name = self.name_edit.clone();
let pass = self.pass_edit.clone();
let phrase = self.mnemonic_setup.mnemonic.get_phrase();
let conn_method = &self.network_setup.method;
let mut wallet = Wallet::create(name,
pass.clone(),
phrase,
conn_method).unwrap();
// Open created wallet.
wallet.open(pass).unwrap();
// Pass created wallet to callback.
(on_create)(wallet);
// Reset input data.
self.step = None;
self.name_edit = String::from("");
self.pass_edit = String::from("");
self.mnemonic_setup.reset();
None
}
}
} else {
Some(Step::EnterMnemonic)
};
});
}
/// Draw wallet creation [`Step`] content. /// Draw wallet creation [`Step`] content.
fn step_content_ui(&mut self, fn step_content_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
@ -256,7 +345,12 @@ impl WalletCreation {
None => {} None => {}
Some(step) => { Some(step) => {
match step { match step {
Step::EnterMnemonic => self.reset(), Step::EnterMnemonic => {
self.step = None;
self.name_edit = String::from("");
self.pass_edit = String::from("");
self.mnemonic_setup.reset();
},
Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic), Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic),
Step::SetupConnection => self.step = Some(Step::EnterMnemonic) Step::SetupConnection => self.step = Some(Step::EnterMnemonic)
} }
@ -264,49 +358,6 @@ impl WalletCreation {
} }
} }
/// Go to the next wallet creation [`Step`].
fn forward(&mut self, on_create: impl FnOnce(Wallet)) {
self.step = if let Some(step) = &self.step {
match step {
Step::EnterMnemonic => {
if self.mnemonic_setup.mnemonic.mode == PhraseMode::Generate {
Some(Step::ConfirmMnemonic)
} else {
// Check if entered phrase was valid.
if self.mnemonic_setup.valid_phrase {
Some(Step::SetupConnection)
} else {
Some(Step::EnterMnemonic)
}
}
}
Step::ConfirmMnemonic => {
// Check external connections availability on connection setup.
ExternalConnection::start_ext_conn_availability_check();
Some(Step::SetupConnection)
},
Step::SetupConnection => {
// Create wallet at last step.
let name = self.name_edit.clone();
let pass = self.pass_edit.clone();
let phrase = self.mnemonic_setup.mnemonic.get_phrase();
let conn_method = &self.network_setup.method;
let mut wallet
= Wallet::create(name, pass.clone(), phrase, conn_method).unwrap();
// Open created wallet.
wallet.open(pass).unwrap();
// Pass created wallet to callback.
(on_create)(wallet);
// Reset input data.
self.reset();
None
}
}
} else {
Some(Step::EnterMnemonic)
};
}
/// Start wallet creation from showing [`Modal`] to enter name and password. /// Start wallet creation from showing [`Modal`] to enter name and password.
pub fn show_name_pass_modal(&mut self, cb: &dyn PlatformCallbacks) { pub fn show_name_pass_modal(&mut self, cb: &dyn PlatformCallbacks) {
// Reset modal values. // Reset modal values.

View file

@ -167,13 +167,13 @@ impl MnemonicSetup {
let words = match self.mnemonic.mode { let words = match self.mnemonic.mode {
PhraseMode::Generate => { PhraseMode::Generate => {
if edit_words { if edit_words {
self.mnemonic.confirm_words.clone() &self.mnemonic.confirm_words
} else { } else {
self.mnemonic.words.clone() &self.mnemonic.words
} }
} }
PhraseMode::Import => self.mnemonic.words.clone() PhraseMode::Import => &self.mnemonic.words
}; }.clone();
let mut word_number = 0; let mut word_number = 0;
let cols = list_columns_count(ui); let cols = list_columns_count(ui);

View file

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
/// Wallet creation step. /// Wallet creation step.
#[derive(PartialEq)] #[derive(PartialEq, Clone)]
pub enum Step { pub enum Step {
/// Mnemonic phrase input. /// Mnemonic phrase input.
EnterMnemonic, EnterMnemonic,

View file

@ -185,22 +185,20 @@ impl RecoverySetup {
.size(17.0) .size(17.0)
.color(Colors::GRAY)); .color(Colors::GRAY));
ui.add_space(8.0); ui.add_space(8.0);
});
// Draw current wallet password text edit. // Draw current wallet password text edit.
let pass_edit_id = Id::from(modal.id).with(wallet.get_config().id); let pass_edit_id = Id::from(modal.id).with(wallet.get_config().id);
let pass_edit_opts = TextEditOptions::new(pass_edit_id).password(); let pass_edit_opts = TextEditOptions::new(pass_edit_id).password();
View::text_edit(ui, cb, &mut self.pass_edit, pass_edit_opts); View::text_edit(ui, cb, &mut self.pass_edit, pass_edit_opts);
// Show information when password is empty. // Show information when password is empty or wrong.
ui.vertical_centered(|ui| {
if self.pass_edit.is_empty() { if self.pass_edit.is_empty() {
ui.add_space(10.0); ui.add_space(12.0);
ui.label(RichText::new(t!("wallets.pass_empty")) ui.label(RichText::new(t!("wallets.pass_empty"))
.size(17.0) .size(17.0)
.color(Colors::INACTIVE_TEXT)); .color(Colors::INACTIVE_TEXT));
} else if self.wrong_pass { } else if self.wrong_pass {
ui.add_space(10.0); ui.add_space(12.0);
ui.label(RichText::new(t!("wallets.wrong_pass")) ui.label(RichText::new(t!("wallets.wrong_pass"))
.size(17.0) .size(17.0)
.color(Colors::RED)); .color(Colors::RED));

View file

@ -375,7 +375,7 @@ impl WalletContent {
} }
/// Draw content when wallet is syncing and not ready to use, returns `true` at this case. /// Draw content when wallet is syncing and not ready to use, returns `true` at this case.
pub fn sync_ui(ui: &mut egui::Ui, frame: &mut eframe::Frame, wallet: &Wallet) -> bool { pub fn sync_ui(ui: &mut egui::Ui, wallet: &Wallet) -> bool {
if wallet.is_repairing() && !wallet.sync_error() { if wallet.is_repairing() && !wallet.sync_error() {
Self::sync_progress_ui(ui, wallet); Self::sync_progress_ui(ui, wallet);
return true; return true;
@ -384,7 +384,7 @@ impl WalletContent {
return true; return true;
} else if wallet.get_current_ext_conn_id().is_none() { } else if wallet.get_current_ext_conn_id().is_none() {
if !Node::is_running() || Node::is_stopping() { if !Node::is_running() || Node::is_stopping() {
let dual_panel_root = Root::is_dual_panel_mode(frame); let dual_panel_root = Root::is_dual_panel_mode(ui);
View::center_content(ui, 108.0, |ui| { View::center_content(ui, 108.0, |ui| {
let text = t!("wallets.enable_node", "settings" => GEAR_FINE); let text = t!("wallets.enable_node", "settings" => GEAR_FINE);
ui.label(RichText::new(text).size(16.0).color(Colors::INACTIVE_TEXT)); ui.label(RichText::new(text).size(16.0).color(Colors::INACTIVE_TEXT));

View file

@ -37,10 +37,10 @@ impl WalletTab for WalletInfo {
fn ui(&mut self, fn ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
frame: &mut eframe::Frame, _: &mut eframe::Frame,
wallet: &mut Wallet, wallet: &mut Wallet,
_: &dyn PlatformCallbacks) { _: &dyn PlatformCallbacks) {
if WalletContent::sync_ui(ui, frame, wallet) { if WalletContent::sync_ui(ui, wallet) {
return; return;
} }

View file

@ -12,35 +12,58 @@
// 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 egui::{Id, Margin, RichText, ScrollArea, Widget}; use egui::{Id, Margin, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{ARCHIVE_BOX, BROOM, CLIPBOARD_TEXT, COPY, HAND_COINS}; use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, COPY, HAND_COINS, RECEIPT};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Root, View}; use crate::gui::views::{Modal, Root, View};
use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType}; use crate::gui::views::types::{ModalPosition, TextEditOptions};
use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::WalletContent; use crate::gui::views::wallets::wallet::WalletContent;
use crate::wallet::Wallet; use crate::wallet::Wallet;
/// Receiving tab content. /// Receiving tab content.
pub struct WalletReceive { pub struct WalletReceive {
/// Slatepack text from sender to create response. /// Flag to check if there is invoice transaction type.
is_invoice: bool,
/// Slatepack message from sender to create response message.
message_edit: String, message_edit: String,
/// Generated Slatepack response. /// Generated Slatepack response message for sender.
response_edit: String, response_edit: String,
/// Flag to check if there is an error happened on receive. /// Flag to check if there is an error happened on response creation.
receive_error: bool, response_error: bool,
/// Flag to check if response was copied to the clipboard.
response_copied: bool, /// Amount to receive for invoice transaction type.
amount_edit: String,
/// Generated Slatepack message for invoice transaction.
request_edit: String,
/// Flag to check if there is an error happened on invoice creation.
request_error: bool,
/// Slatepack message from sender to finalize transaction.
finalization_edit: String,
/// Flag to check if there is an error happened on transaction finalization.
finalization_error: bool,
} }
/// Identifier for invoice amount [`Modal`].
const INVOICE_AMOUNT_MODAL: &'static str = "invoice_amount_modal";
impl Default for WalletReceive { impl Default for WalletReceive {
fn default() -> Self { fn default() -> Self {
Self { Self {
is_invoice: false,
message_edit: "".to_string(), message_edit: "".to_string(),
response_edit: "".to_string(), response_edit: "".to_string(),
receive_error: false, response_error: false,
response_copied: false, amount_edit: "".to_string(),
request_edit: "".to_string(),
request_error: false,
finalization_edit: "".to_string(),
finalization_error: false,
} }
} }
} }
@ -52,13 +75,16 @@ impl WalletTab for WalletReceive {
fn ui(&mut self, fn ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
frame: &mut eframe::Frame, _: &mut eframe::Frame,
wallet: &mut Wallet, wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
if WalletContent::sync_ui(ui, frame, wallet) { if WalletContent::sync_ui(ui, wallet) {
return; return;
} }
// Show modal content for this ui container.
self.modal_content_ui(ui, wallet, cb);
// Show receiving content panel. // Show receiving content panel.
egui::CentralPanel::default() egui::CentralPanel::default()
.frame(egui::Frame { .frame(egui::Frame {
@ -73,18 +99,21 @@ impl WalletTab for WalletReceive {
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
ui.vertical_centered(|ui| { ScrollArea::vertical()
View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.3, |ui| { .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible)
self.receive_ui(ui, wallet, cb); .id_source(Id::from("wallet_receive").with(wallet.get_config().id))
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.receive_ui(ui, wallet, cb);
});
});
}); });
});
}); });
} }
} }
/// Hint for Slatepack Message input.
const RECEIVE_SLATEPACK_HINT: &'static str = "BEGINSLATEPACK.\n...\n...\n...\nENDSLATEPACK.";
impl WalletReceive { impl WalletReceive {
/// Draw receiving content. /// Draw receiving content.
pub fn receive_ui(&mut self, pub fn receive_ui(&mut self,
@ -95,18 +124,91 @@ impl WalletReceive {
View::sub_title(ui, format!("{} {}", HAND_COINS, t!("wallets.manually"))); View::sub_title(ui, format!("{} {}", HAND_COINS, t!("wallets.manually")));
View::horizontal_line(ui, Colors::ITEM_STROKE); View::horizontal_line(ui, Colors::ITEM_STROKE);
ui.add_space(3.0); ui.add_space(3.0);
// Show manual receiving content.
self.manual_ui(ui, wallet, cb);
}
// Setup manual sending description. /// Draw [`Modal`] content for this ui container.
let response_empty = self.response_edit.is_empty(); fn modal_content_ui(&mut self,
let desc_text = if response_empty { ui: &mut egui::Ui,
t!("wallets.receive_paste_slatepack") wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) {
match Modal::opened() {
None => {}
Some(id) => {
match id {
INVOICE_AMOUNT_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.invoice_amount_modal(ui, wallet, modal, cb);
});
}
_ => {}
}
}
}
}
/// Draw manual receiving content.
fn manual_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
ui.add_space(10.0);
ui.columns(2, |columns| {
let mut is_invoice = self.is_invoice;
columns[0].vertical_centered(|ui| {
View::radio_value(ui, &mut is_invoice, false, t!("wallets.receive"));
});
columns[1].vertical_centered(|ui| {
View::radio_value(ui, &mut is_invoice, true, t!("wallets.invoice"));
});
if is_invoice != self.is_invoice {
self.is_invoice = is_invoice;
// Reset fields to default values on mode change.
if is_invoice {
self.amount_edit = "".to_string();
self.request_edit = "".to_string();
self.request_error = false;
self.finalization_edit = "".to_string();
self.finalization_error = false;
} else {
self.message_edit = "".to_string();
self.response_edit = "".to_string();
self.response_error = false;
}
}
});
ui.add_space(10.0);
if self.is_invoice {
// Show invoice creation content.
self.manual_invoice_ui(ui, wallet, cb);
} else { } else {
t!("wallets.receive_send_slatepack") // Show manual transaction receiving content.
}; self.manual_receive_ui(ui, wallet, cb);
ui.label(RichText::new(desc_text).size(16.0).color(Colors::INACTIVE_TEXT)); }
ui.add_space(3.0); }
// Show Slatepack text input. /// Draw manual receiving content.
fn manual_receive_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) {
// Setup description.
let response_empty = self.response_edit.is_empty();
if self.response_error {
ui.label(RichText::new(t!("wallets.receive_slatepack_err"))
.size(16.0)
.color(Colors::RED));
} else {
let desc_text = if response_empty {
t!("wallets.receive_slatepack_desc")
} else {
t!("wallets.receive_send_slatepack")
};
ui.label(RichText::new(desc_text).size(16.0).color(Colors::INACTIVE_TEXT));
}
ui.add_space(6.0);
// Setup Slatepack message text input.
let message = if response_empty { let message = if response_empty {
&mut self.message_edit &mut self.message_edit
} else { } else {
@ -126,12 +228,12 @@ impl WalletReceive {
.font(egui::TextStyle::Small) .font(egui::TextStyle::Small)
.desired_rows(5) .desired_rows(5)
.interactive(response_empty) .interactive(response_empty)
.hint_text(RECEIVE_SLATEPACK_HINT) .hint_text(SLATEPACK_MESSAGE_HINT)
.desired_width(f32::INFINITY) .desired_width(f32::INFINITY)
.show(ui); .show(ui);
// Clear an error when message changed. // Clear an error when message changed.
if &message_before != message { if &message_before != message {
self.receive_error = false; self.response_error = false;
} }
ui.add_space(6.0); ui.add_space(6.0);
}); });
@ -139,38 +241,27 @@ impl WalletReceive {
View::horizontal_line(ui, Colors::ITEM_STROKE); View::horizontal_line(ui, Colors::ITEM_STROKE);
ui.add_space(10.0); ui.add_space(10.0);
// Show receiving input control buttons. // Draw buttons to clear/copy/paste.
self.receive_buttons_ui(ui, wallet, cb);
}
/// Draw manual receiving input control buttons.
fn receive_buttons_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) {
let field_is_empty = self.message_edit.is_empty() && self.response_edit.is_empty(); let field_is_empty = self.message_edit.is_empty() && self.response_edit.is_empty();
let columns_num = if !field_is_empty { 2 } else { 1 }; let columns_num = if !field_is_empty { 2 } else { 1 };
// Draw buttons to clear/copy/paste.
ui.scope(|ui| { ui.scope(|ui| {
// Setup spacing between buttons. // Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(columns_num, |columns| { ui.columns(columns_num, |columns| {
let first_column_content = |ui: &mut egui::Ui| { let first_column_content = |ui: &mut egui::Ui| {
if !field_is_empty { if !self.response_edit.is_empty() && !self.response_error {
let clear_text = format!("{} {}", BROOM, t!("clear")); let clear_text = format!("{} {}", BROOM, t!("clear"));
View::button(ui, clear_text, Colors::BUTTON, || { View::button(ui, clear_text, Colors::BUTTON, || {
self.receive_error = false; self.response_error = false;
self.response_copied = false;
self.message_edit.clear(); self.message_edit.clear();
self.response_edit.clear(); self.response_edit.clear();
}); });
} else if self.message_edit.is_empty() { } else {
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste_text, Colors::BUTTON, || { View::button(ui, paste_text, Colors::BUTTON, || {
self.message_edit = cb.get_string_from_buffer(); self.message_edit = cb.get_string_from_buffer();
self.receive_error = false; self.response_error = false;
}); });
} }
}; };
@ -178,56 +269,253 @@ impl WalletReceive {
columns[0].vertical_centered(first_column_content); columns[0].vertical_centered(first_column_content);
} else { } else {
columns[0].vertical_centered_justified(first_column_content); columns[0].vertical_centered_justified(first_column_content);
}
if !field_is_empty {
columns[1].vertical_centered_justified(|ui| { columns[1].vertical_centered_justified(|ui| {
if !self.message_edit.is_empty() { if self.response_error {
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); let clear_text = format!("{} {}", BROOM, t!("clear"));
View::button(ui, paste_text, Colors::BUTTON, || { View::button(ui, clear_text, Colors::BUTTON, || {
self.message_edit = cb.get_string_from_buffer(); self.response_error = false;
self.receive_error = false; self.message_edit.clear();
self.response_edit.clear();
}); });
} else if !self.response_edit.is_empty() { } else if !self.response_edit.is_empty() {
let copy_text = format!("{} {}", COPY, t!("copy")); let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::BUTTON, || { View::button(ui, copy_text, Colors::BUTTON, || {
cb.copy_string_to_buffer(self.response_edit.clone()); cb.copy_string_to_buffer(self.response_edit.clone());
self.response_copied = true; });
} else {
View::button(ui, t!("wallets.create_response"), Colors::GOLD, || {
match wallet.receive(self.message_edit.clone()) {
Ok(response) => {
self.response_edit = response.trim().to_string();
self.message_edit.clear();
cb.copy_string_to_buffer(response);
},
Err(e) => {
wallet.sync();
println!("error {}", e);
self.response_error = true
}
}
}); });
} }
}); });
} }
}); });
}); });
}
// Draw button to create response. /// Draw invoice creation content.
if !self.message_edit.is_empty() && !self.receive_error { fn manual_invoice_ui(&mut self,
ui.add_space(8.0); ui: &mut egui::Ui,
let create_text = format!("{} {}", ARCHIVE_BOX, t!("wallets.create_response")); wallet: &mut Wallet,
View::button(ui, create_text, Colors::GOLD, || { cb: &dyn PlatformCallbacks) {
match wallet.receive(self.message_edit.clone()) { ui.label(RichText::new(t!("wallets.issue_invoice_desc"))
Ok(response) => { .size(16.0)
self.response_edit = response.trim().to_string(); .color(Colors::INACTIVE_TEXT));
self.message_edit.clear(); ui.add_space(6.0);
// Copy response to clipboard.
cb.copy_string_to_buffer(response); // Draw invoice creation button.
self.response_copied = true; let invoice_text = format!("{} {}", RECEIPT, t!("wallets.issue_invoice"));
}, View::button(ui, invoice_text, Colors::BUTTON, || {
Err(_) => self.receive_error = true // Reset modal values.
self.amount_edit = "".to_string();
self.request_error = false;
// Show invoice amount modal.
Modal::new(INVOICE_AMOUNT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.issue_invoice"))
.show();
cb.show_keyboard();
});
ui.add_space(12.0);
View::horizontal_line(ui, Colors::ITEM_STROKE);
ui.add_space(6.0);
ui.label(RichText::new(t!("wallets.receive_slatepack_desc"))
.size(16.0)
.color(Colors::INACTIVE_TEXT));
ui.add_space(6.0);
View::horizontal_line(ui, Colors::ITEM_STROKE);
ui.add_space(3.0);
// Draw invoice finalization text input.
ScrollArea::vertical()
.max_height(128.0)
.id_source(Id::from("receive_input").with(wallet.get_config().id))
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
let finalization_before = self.finalization_edit.clone();
egui::TextEdit::multiline(&mut self.finalization_edit)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(true)
.hint_text(SLATEPACK_MESSAGE_HINT)
.desired_width(f32::INFINITY)
.show(ui);
// Clear an error when message changed.
if finalization_before != self.finalization_edit {
self.finalization_error = false;
} }
ui.add_space(6.0);
}); });
ui.add_space(2.0);
View::horizontal_line(ui, Colors::ITEM_STROKE);
ui.add_space(10.0);
// Draw buttons to clear/paste.
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| {
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste_text, Colors::BUTTON, || {
self.finalization_edit = cb.get_string_from_buffer();
self.response_error = false;
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("wallets.finalize"), Colors::GOLD, || {
wallet.finalize();
//TODO: finalize
});
});
});
});
if self.finalization_error {
ui.add_space(8.0); ui.add_space(8.0);
} else if self.receive_error { ui.label(RichText::new(t!("wallets.finalize_slatepack_err"))
ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.receive_slatepack_err"))
.size(16.0) .size(16.0)
.color(Colors::RED)); .color(Colors::RED));
}
ui.add_space(8.0);
}
/// Draw invoice amount [`Modal`] content.
fn invoice_amount_modal(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
if self.request_edit.is_empty() {
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.enter_amount"))
.size(17.0)
.color(Colors::GRAY));
});
ui.add_space(8.0); ui.add_space(8.0);
} else if self.response_copied {
ui.add_space(8.0); // Draw invoice amount text edit.
ui.label(RichText::new(t!("wallets.response_copied")) let amount_edit_id = Id::from(modal.id).with(wallet.get_config().id);
.size(16.0) let amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center();
.color(Colors::GREEN)); let mut amount_edit = self.amount_edit.clone();
ui.add_space(8.0); View::text_edit(ui, cb, &mut amount_edit, amount_edit_opts);
if amount_edit != self.amount_edit {
self.request_error = false;
match amount_from_hr_string(amount_edit.as_str()) {
Ok(_) => {
self.amount_edit = amount_edit;
}
Err(_) => {}
}
}
// Show invoice creation error.
if self.request_error {
ui.add_space(12.0);
ui.label(RichText::new(t!("wallets.invoice_slatepack_err"))
.size(17.0)
.color(Colors::RED));
}
// Show modal buttons.
ui.add_space(12.0);
ui.scope(|ui| {
// 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::button(ui, t!("modal.cancel"), Colors::WHITE, || {
self.amount_edit = "".to_string();
self.request_error = false;
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("continue"), Colors::WHITE, || {
match amount_from_hr_string(self.amount_edit.as_str()) {
Ok(amount) => {
match wallet.issue_invoice(amount) {
Ok(message) => {
self.request_edit = message;
cb.hide_keyboard();
}
Err(_) => {
self.request_error = true;
}
}
}
Err(_) => {
self.request_error = true;
}
}
});
});
});
});
ui.add_space(6.0);
} else {
ui.vertical_centered(|ui| {
let amount = amount_from_hr_string(self.amount_edit.as_str()).unwrap();
let amount_format = amount_to_hr_string(amount, true);
let desc_text = t!("wallets.invoice_desc","amount" => amount_format);
ui.label(RichText::new(desc_text).size(16.0).color(Colors::INACTIVE_TEXT));
ui.add_space(6.0);
View::horizontal_line(ui, Colors::ITEM_STROKE);
ui.add_space(3.0);
// Draw invoice request text.
ScrollArea::vertical()
.max_height(128.0)
.id_source(Id::from("receive_input").with(wallet.get_config().id))
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
egui::TextEdit::multiline(&mut self.request_edit)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(false)
.hint_text(SLATEPACK_MESSAGE_HINT)
.desired_width(f32::INFINITY)
.show(ui);
ui.add_space(6.0);
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::ITEM_STROKE);
ui.add_space(10.0);
// Draw copy button.
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::BUTTON, || {
cb.copy_string_to_buffer(self.request_edit.clone());
});
});
// Draw button to close modal.
ui.add_space(12.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::WHITE, || {
self.amount_edit = "".to_string();
self.request_edit = "".to_string();
modal.close();
});
});
ui.add_space(6.0);
} }
} }
} }

View file

@ -31,10 +31,10 @@ impl WalletTab for WalletSend {
fn ui(&mut self, fn ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
frame: &mut eframe::Frame, _: &mut eframe::Frame,
wallet: &mut Wallet, wallet: &mut Wallet,
_: &dyn PlatformCallbacks) { _: &dyn PlatformCallbacks) {
if WalletContent::sync_ui(ui, frame, wallet) { if WalletContent::sync_ui(ui, wallet) {
return; return;
} }

View file

@ -16,7 +16,7 @@
extern crate rust_i18n; extern crate rust_i18n;
use eframe::wgpu; use eframe::wgpu;
use egui::{Context, Stroke}; use egui::{Context, Stroke, vec2};
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
use winit::platform::android::activity::AndroidApp; use winit::platform::android::activity::AndroidApp;
@ -61,16 +61,21 @@ fn android_main(app: AndroidApp) {
use gui::PlatformApp; use gui::PlatformApp;
let platform = Android::new(app.clone()); let platform = Android::new(app.clone());
use winit::platform::android::EventLoopBuilderExtAndroid; use winit::platform::android::EventLoopBuilderExtAndroid;
let mut options = eframe::NativeOptions::default();
let width = app.config().screen_width_dp().unwrap() as f32;
let height = app.config().screen_height_dp().unwrap() as f32;
let mut options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size(vec2(width, height)),
..Default::default()
};
// Setup limits that are guaranteed to be compatible with Android devices. // Setup limits that are guaranteed to be compatible with Android devices.
options.wgpu_options.device_descriptor = std::sync::Arc::new(|adapter| { options.wgpu_options.device_descriptor = std::sync::Arc::new(|adapter| {
let base_limits = wgpu::Limits::downlevel_webgl2_defaults(); let base_limits = wgpu::Limits::downlevel_webgl2_defaults();
wgpu::DeviceDescriptor { wgpu::DeviceDescriptor {
label: Some("egui wgpu device"), label: Some("egui wgpu device"),
features: wgpu::Features::default(), required_features: wgpu::Features::default(),
limits: wgpu::Limits { required_limits: wgpu::Limits {
max_texture_dimension_2d: 8192, max_texture_dimension_2d: 8192,
..base_limits ..base_limits
}, },
@ -102,7 +107,6 @@ pub fn app_creator<T: 'static>(app: PlatformApp<T>) -> eframe::AppCreator
pub fn start(mut options: eframe::NativeOptions, app_creator: eframe::AppCreator) { pub fn start(mut options: eframe::NativeOptions, app_creator: eframe::AppCreator) {
options.default_theme = eframe::Theme::Light; options.default_theme = eframe::Theme::Light;
options.renderer = eframe::Renderer::Wgpu; options.renderer = eframe::Renderer::Wgpu;
options.initial_window_size = Some(egui::Vec2::new(1200.0, 720.0));
// Setup translations. // Setup translations.
setup_i18n(); setup_i18n();
// Start integrated node if needed. // Start integrated node if needed.
@ -119,7 +123,7 @@ pub fn setup_visuals(ctx: &Context) {
// Setup spacing for buttons. // Setup spacing for buttons.
style.spacing.button_padding = egui::vec2(12.0, 8.0); style.spacing.button_padding = egui::vec2(12.0, 8.0);
// Make scroll-bar thinner. // Make scroll-bar thinner.
style.spacing.scroll_bar_width = 4.0; style.spacing.scroll.bar_width = 4.0;
// Disable spacing between items. // Disable spacing between items.
style.spacing.item_spacing = egui::vec2(0.0, 0.0); style.spacing.item_spacing = egui::vec2(0.0, 0.0);
// Setup radio button/checkbox size and spacing. // Setup radio button/checkbox size and spacing.

View file

@ -31,6 +31,9 @@ fn real_main() {
use grim::gui::PlatformApp; use grim::gui::PlatformApp;
let platform = Desktop::default(); let platform = Desktop::default();
let options = eframe::NativeOptions::default(); let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([1200.0, 720.0]),
..Default::default()
};
grim::start(options, grim::app_creator(PlatformApp::new(platform))); grim::start(options, grim::app_creator(PlatformApp::new(platform)));
} }

View file

@ -24,6 +24,7 @@ use grin_core::global::ChainTypes;
use grin_p2p::{PeerAddr, Seeding}; use grin_p2p::{PeerAddr, Seeding};
use grin_p2p::msg::PeerAddrs; use grin_p2p::msg::PeerAddrs;
use grin_servers::common::types::ChainValidationMode; use grin_servers::common::types::ChainValidationMode;
use local_ip_address::list_afinet_netifas;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{AppConfig, Settings}; use crate::{AppConfig, Settings};
@ -244,10 +245,11 @@ impl NodeConfig {
/// List of available IP addresses. /// List of available IP addresses.
pub fn get_ip_addrs() -> Vec<String> { pub fn get_ip_addrs() -> Vec<String> {
let mut ip_addrs = Vec::new(); let mut ip_addrs = Vec::new();
for net_if in pnet::datalink::interfaces() { let network_interfaces = list_afinet_netifas();
for ip in net_if.ips { if let Ok(network_interfaces) = network_interfaces {
for (_, ip) in network_interfaces.iter() {
if ip.is_ipv4() { if ip.is_ipv4() {
ip_addrs.push(ip.ip().to_string()); ip_addrs.push(ip.to_string());
} }
} }
} }

View file

@ -103,4 +103,22 @@ impl Mnemonic {
} }
words words
} }
/// Set words from provided text if possible.
pub fn import_text(&mut self, text: String) {
if self.mode != PhraseMode::Import {
return;
}
let words_split = text.trim().split(" ");
let count = words_split.clone().count();
if PhraseSize::is_correct_count(count) {
if self.size == PhraseSize::type_for_value(count).unwrap() {
let mut words = vec![];
words_split.enumerate().for_each(|(i, word)| {
words.insert(i, word.to_string())
});
self.words = words;
}
}
}
} }

View file

@ -62,6 +62,38 @@ impl PhraseSize {
PhraseSize::Words24 => 32 PhraseSize::Words24 => 32
} }
} }
pub fn type_for_value(count: usize) -> Option<PhraseSize> {
if Self::is_correct_count(count) {
match count {
12 => {
Some(PhraseSize::Words12)
}
15 => {
Some(PhraseSize::Words15)
}
18 => {
Some(PhraseSize::Words18)
}
21 => {
Some(PhraseSize::Words21)
}
24 => {
Some(PhraseSize::Words24)
}
_ => {
None
}
}
} else {
None
}
}
/// Check if correct word count provided.
pub fn is_correct_count(count: usize) -> bool {
count == 12 || count == 15 || count == 18 || count == 21 || count == 24
}
} }
/// Wallet connection method. /// Wallet connection method.

View file

@ -27,7 +27,7 @@ use futures::channel::oneshot;
use grin_api::{ApiServer, Router}; use grin_api::{ApiServer, Router};
use grin_chain::SyncStatus; use grin_chain::SyncStatus;
use grin_core::global; use grin_core::global;
use grin_keychain::{ExtKeychain, Keychain}; use grin_keychain::{ExtKeychain, Identifier, Keychain};
use grin_util::Mutex; use grin_util::Mutex;
use grin_util::types::ZeroingString; use grin_util::types::ZeroingString;
use grin_wallet_api::Owner; use grin_wallet_api::Owner;
@ -35,7 +35,7 @@ use grin_wallet_controller::command::parse_slatepack;
use grin_wallet_controller::controller; use grin_wallet_controller::controller;
use grin_wallet_controller::controller::ForeignAPIHandlerV2; use grin_wallet_controller::controller::ForeignAPIHandlerV2;
use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient}; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient};
use grin_wallet_libwallet::{AcctPathMapping, Error, NodeClient, StatusMessage, TxLogEntryType, WalletInst, WalletLCProvider}; use grin_wallet_libwallet::{Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, RetrieveTxQueryArgs, Slate, StatusMessage, TxLogEntry, TxLogEntryType, WalletInst, WalletLCProvider};
use grin_wallet_libwallet::api_impl::owner::{cancel_tx, retrieve_summary_info, retrieve_txs}; use grin_wallet_libwallet::api_impl::owner::{cancel_tx, retrieve_summary_info, retrieve_txs};
use crate::node::{Node, NodeConfig}; use crate::node::{Node, NodeConfig};
@ -205,6 +205,15 @@ impl Wallet {
Ok(Arc::new(Mutex::new(wallet))) Ok(Arc::new(Mutex::new(wallet)))
} }
/// Get parent key identifier for current account.
pub fn get_parent_key_id(&self) -> Result<Identifier, Error> {
let instance = self.instance.clone().unwrap();
let mut w_lock = instance.lock();
let lc = w_lock.lc_provider()?;
let w_inst = lc.wallet_inst()?;
Ok(w_inst.parent_key_id())
}
/// Get wallet config. /// Get wallet config.
pub fn get_config(&self) -> WalletConfig { pub fn get_config(&self) -> WalletConfig {
self.config.read().unwrap().clone() self.config.read().unwrap().clone()
@ -238,7 +247,7 @@ impl Wallet {
} }
// Create new wallet instance if sync thread was stopped or instance was not created. // Create new wallet instance if sync thread was stopped or instance was not created.
if self.sync_thread.write().unwrap().is_none() || self.instance.is_none() { if self.sync_thread.read().unwrap().is_none() || self.instance.is_none() {
let config = self.get_config(); let config = self.get_config();
let new_instance = Self::create_wallet_instance(config.clone())?; let new_instance = Self::create_wallet_instance(config.clone())?;
self.instance = Some(new_instance); self.instance = Some(new_instance);
@ -352,6 +361,9 @@ impl Wallet {
let mut api = Owner::new(self.instance.clone().unwrap(), None); let mut api = Owner::new(self.instance.clone().unwrap(), None);
controller::owner_single_use(None, None, Some(&mut api), |api, m| { controller::owner_single_use(None, None, Some(&mut api), |api, m| {
api.create_account_path(m, label)?; api.create_account_path(m, label)?;
// Sync wallet data.
self.sync();
Ok(()) Ok(())
}) })
} }
@ -448,7 +460,28 @@ impl Wallet {
} }
} }
/// Receive transaction via Slatepack Message. /// Create Slatepack message from provided slate.
fn create_slatepack_message(&self, slate: Slate) -> Result<String, Error> {
let mut message = "".to_string();
let mut api = Owner::new(self.instance.clone().unwrap(), None);
controller::owner_single_use(None, None, Some(&mut api), |api, m| {
message = api.create_slatepack_message(m, &slate, Some(0), vec![])?;
Ok(())
})?;
// Create a directory to which slatepack files will be output.
let mut slatepack_dir = self.get_config().get_slatepacks_path();
let slatepack_file_name = format!("{}.{}.slatepack", slate.id, slate.state);
slatepack_dir.push(slatepack_file_name);
// Write Slatepack response into the file.
let mut output = File::create(slatepack_dir)?;
output.write_all(message.as_bytes())?;
output.sync_all()?;
Ok(message)
}
/// Receive transaction via Slatepack message, return response to sender.
pub fn receive(&self, message: String) -> Result<String, Error> { pub fn receive(&self, message: String) -> Result<String, Error> {
let mut api = Owner::new(self.instance.clone().unwrap(), None); let mut api = Owner::new(self.instance.clone().unwrap(), None);
match parse_slatepack(&mut api, None, None, Some(message.clone())) { match parse_slatepack(&mut api, None, None, Some(message.clone())) {
@ -458,21 +491,37 @@ impl Wallet {
slate = api.receive_tx(&slate, Some(config.account.as_str()), None)?; slate = api.receive_tx(&slate, Some(config.account.as_str()), None)?;
Ok(()) Ok(())
})?; })?;
let mut response = "".to_string(); // Create Slatepack message response.
controller::owner_single_use(None, None, Some(&mut api), |api, m| { let response = self.create_slatepack_message(slate)?;
response = api.create_slatepack_message(m, &slate, Some(0), vec![])?;
Ok(())
})?;
// Create a directory to which slatepack files will be output. // Sync wallet info.
let mut slatepack_dir = config.get_slatepacks_path(); self.sync();
let slatepack_file_name = format!("{}.{}.slatepack", slate.id, slate.state); Ok(response)
slatepack_dir.push(slatepack_file_name); }
Err(_) => {
Err(Error::GenericError("Parsing error".to_string()))
}
}
}
// Write Slatepack response into the file. /// S transaction via Slatepack message and return response to sender.
let mut output = File::create(slatepack_dir)?; pub fn pay(&self, message: String) -> Result<String, Error> {
output.write_all(response.as_bytes())?; let mut api = Owner::new(self.instance.clone().unwrap(), None);
output.sync_all()?; match parse_slatepack(&mut api, None, None, Some(message.clone())) {
Ok((mut slate, _)) => {
let config = self.get_config();
let args = InitTxArgs {
src_acct_name: None,
amount: slate.amount,
minimum_confirmations: config.min_confirmations,
selection_strategy_is_use_all: false,
..Default::default()
};
let mut api = Owner::new(self.instance.clone().unwrap(), None);
let slate = api.process_invoice_tx(None, &slate, args)?;
// Create Slatepack message response.
let response = self.create_slatepack_message(slate)?;
// Sync wallet info. // Sync wallet info.
self.sync(); self.sync();
@ -485,6 +534,25 @@ impl Wallet {
} }
} }
/// Initialize an invoice transaction.
pub fn issue_invoice(&self, amount: u64) -> Result<String, Error> {
let args = IssueInvoiceTxArgs {
dest_acct_name: None,
amount,
target_slate_version: None,
};
let mut api = Owner::new(self.instance.clone().unwrap(), None);
let slate = api.issue_invoice_tx(None, args)?;
// Create Slatepack message response.
let response = self.create_slatepack_message(slate)?;
// Sync wallet info.
self.sync();
Ok(response)
}
pub fn send(&self) { pub fn send(&self) {
} }
@ -513,6 +581,7 @@ impl Wallet {
cancelling_r.contains(id) cancelling_r.contains(id)
} }
/// Finalize transaction from provided Slatepack message.
pub fn finalize(&self) { pub fn finalize(&self) {
} }
@ -571,7 +640,7 @@ impl Wallet {
wallet_delete.is_open.store(false, Ordering::Relaxed); wallet_delete.is_open.store(false, Ordering::Relaxed);
wallet_delete.deleted.store(true, Ordering::Relaxed); wallet_delete.deleted.store(true, Ordering::Relaxed);
// Wake up thread to exit. // Start sync to exit.
wallet_delete.sync(); wallet_delete.sync();
}); });
} }
@ -786,16 +855,13 @@ fn sync_wallet_data(wallet: &Wallet) {
} }
}); });
// Retrieve txs. match retrieve_txs(instance.clone(),
match retrieve_txs( None,
instance.clone(), &Some(txs_tx),
None, true,
&Some(txs_tx), None,
true, None,
None, None) {
None,
None
) {
Ok(txs) => { Ok(txs) => {
// Do not sync data if wallet was closed. // Do not sync data if wallet was closed.
if !wallet.is_open() { if !wallet.is_open() {
@ -807,9 +873,20 @@ fn sync_wallet_data(wallet: &Wallet) {
wallet.reset_sync_attempts(); wallet.reset_sync_attempts();
// Setup transactions. // Setup transactions.
let mut txs = txs.1; let mut sort_txs = txs.1;
// Sort txs by creation date. // Sort txs by creation date.
txs.sort_by_key(|tx| -tx.creation_ts.timestamp()); sort_txs.sort_by_key(|tx| -tx.creation_ts.timestamp());
// Filter txs by current wallet account.
let mut txs = sort_txs.iter().map(|v| v.clone()).filter(|tx| {
match wallet.get_parent_key_id() {
Ok(key) => {
tx.parent_key_id == key
}
Err(_) => {
true
}
}
}).collect::<Vec<TxLogEntry>>();
// Update txs statuses. // Update txs statuses.
for tx in &txs { for tx in &txs {
if tx.tx_type == TxLogEntryType::TxSentCancelled if tx.tx_type == TxLogEntryType::TxSentCancelled