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" }
## ui
egui = { version = "0.23.0", default-features = false }
egui_extras = { version = "0.23.0", features = ["image"] }
egui = { version = "0.27.2", default-features = false }
egui_extras = { version = "0.27.2", features = ["image"] }
rust-i18n = "2.1.0"
## other
futures = "0.3"
dirs = "5.0.1"
sys-locale = "0.3.0"
chrono = "0.4.37"
chrono = "0.4.31"
lazy_static = "1.4.0"
toml = "0.8.2"
serde = "1.0.170"
pnet = "0.34.0"
local-ip-address = "0.6.1"
url = "2.4.0"
# stratum server
serde_derive = "1"
serde_json = "1"
serde_derive = "1.0.197"
serde_json = "1.0.115"
tokio = {version = "1.29.1", features = ["full"] }
tokio-util = { version = "0.7.8", features = ["codec"] }
rand = "0.8.5"
@ -60,13 +60,13 @@ built = { version = "0.7.0", features = ["git2"]}
[target.'cfg(not(target_os = "android"))'.dependencies]
env_logger = "0.10.0"
winit = { version = "0.28" }
eframe = { version = "0.23.0", features = [ "wgpu" ] }
winit = { version = "0.29.15" }
eframe = { version = "0.27.2", features = [ "wgpu" ] }
arboard = "3.2.0"
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13.1"
jni = "0.21.1"
android-activity = "0.4.3"
winit = { version = "0.28", features = [ "android-game-activity" ] }
eframe = { version = "0.23.0", features = [ "wgpu", "android-game-activity" ] }
android-activity = "0.5.2"
winit = { version = "0.29", features = [ "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"
// 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
//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.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

View file

@ -7,6 +7,7 @@ import android.system.ErrnoException;
import android.system.Os;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import androidx.core.graphics.Insets;
import androidx.core.view.DisplayCutoutCompat;
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.
native void onDisplayInsets(int[] cutouts);
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
onBack();
return true;
}
@ -146,4 +168,14 @@ public class MainActivity extends GameActivity {
}
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.
plugins {
id 'com.android.application' version '7.4.1' apply false
id 'com.android.library' version '7.4.1' apply false
id 'com.android.application' version '8.1.0' apply false
id 'com.android.library' version '8.1.0' apply false
}
task clean(type: Delete) {

View file

@ -29,6 +29,10 @@ type=$1
[[ $2 == "v7" ]] && platform+=(armv7-linux-androideabi)
[[ $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" \
&& cargo ndk -t ${arch} build ${release_param[@]}

View file

@ -18,4 +18,6 @@ android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# 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
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
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View file

@ -64,11 +64,18 @@ wallets:
tx_canceled: Canceled
tx_confirmed: Confirmed
manually: Manually
receive_paste_slatepack: 'Enter Slatepack message received from the sender to create a response:'
receive_send_slatepack: 'Send response to the sender to finalize the transaction:'
receive_slatepack_err: An error occurred during creation of the response, check input data.
response_copied: Response copied to the clipboard.
receive_slatepack_desc: 'Enter Slatepack message received from the sender to create response or 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:'
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
repair_wallet: Repair wallet
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: Заблокирован
unlocked: Разблокирован
enable_node: 'Чтобы использовать кошелёк, включите встроенный узел или измените настройки подключения, выбрав %{settings} внизу экрана.'
node_loading: Кошелёк будет загружен после синхронизации встроенного узла, вы можете изменить настройки подключения, выбрав %{settings} внизу экрана.
node_loading: 'Кошелёк будет загружен после синхронизации встроенного узла, вы можете изменить настройки подключения, выбрав %{settings} внизу экрана.'
loading: Загружается
closing: Закрывается
checking: Проверяется
@ -64,11 +64,18 @@ wallets:
tx_canceled: Отменено
tx_confirmed: Подтверждено
manually: Вручную
receive_paste_slatepack: 'Введите Slatepack сообщение, полученное от отправителя для создания ответа:'
receive_send_slatepack: 'Отправьте ответ отправителю для завершения транзакции:'
receive_slatepack_err: Во время создания ответа произошла ошибка, проверьте входные данные.
response_copied: Ответ скопирован в буфер обмена.
receive_slatepack_desc: 'Введите Slatepack сообщение, полученное от отправителя, для создания ответа или завершения транзакции:'
receive_send_slatepack: 'Отправьте Slatepack сообщение отправителю для завершения транзакции:'
receive_slatepack_err: 'Во время создания ответа произошла ошибка, проверьте входные данные:'
create_response: Создать ответ
invoice: Инвойс
issue_invoice: Выставить счёт
issue_invoice_desc: 'Создайте запрос на получение средств, введя требуемое количество:'
invoice_desc: 'Вы создали запрос на получение %{amount} ツ. Отправьте Slatepack сообщение отправителю:'
invoice_slatepack_err: Во время выставления счёта произошла ошибка, проверьте входные данные.
finalize_slatepack_err: 'Во время завершения транзакции произошла ошибка, проверьте входные данные:'
finalize: Завершить
enter_amount: 'Введите количество:'
recovery: Восстановление
repair_wallet: Починить кошелёк
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.
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 back_button_pressed {
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
}
self.root.on_back();
// Request repaint to update previous content.
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.
@ -63,14 +72,6 @@ impl<Platform: PlatformCallbacks> eframe::App for PlatformApp<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)]

View file

@ -30,11 +30,41 @@ impl Android {
impl PlatformCallbacks for Android {
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) {
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) {

View file

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

View file

@ -13,7 +13,6 @@
// limitations under the License.
use std::sync::atomic::{AtomicBool, Ordering};
use egui::os::OperatingSystem;
use egui::RichText;
use lazy_static::lazy_static;
@ -70,11 +69,11 @@ impl ModalContainer for Root {
fn modal_ui(&mut self,
ui: &mut egui::Ui,
frame: &mut eframe::Frame,
_: &mut eframe::Frame,
modal: &Modal,
_: &dyn PlatformCallbacks) {
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.
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.
egui::SidePanel::left("network_panel")
.resizable(false)
@ -103,7 +102,8 @@ impl Root {
.show_animated_inside(ui, is_panel_open, |ui| {
// Set content height as window height.
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| {
self.network.ui(ui, frame, cb);
});
@ -118,7 +118,8 @@ impl Root {
.show_inside(ui, |ui| {
// Set content height as window height.
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| {
self.wallets.ui(ui, frame, cb);
});
@ -126,21 +127,20 @@ impl Root {
}
/// Get [`NetworkContent`] panel state and width.
fn network_panel_state_width(frame: &mut eframe::Frame) -> (bool, f32) {
let dual_panel_mode = Self::is_dual_panel_mode(frame);
fn network_panel_state_width(ui: &mut egui::Ui) -> (bool, f32) {
let dual_panel_mode = Self::is_dual_panel_mode(ui);
let is_panel_open = dual_panel_mode || Self::is_network_panel_open();
let panel_width = if dual_panel_mode {
Self::SIDE_PANEL_WIDTH + View::get_left_inset()
} else {
frame.info().window_info.size.x
View::window_size(ui).0
};
(is_panel_open, panel_width)
}
/// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time.
pub fn is_dual_panel_mode(frame: &mut eframe::Frame) -> bool {
let w = frame.info().window_info.size.x;
let h = frame.info().window_info.size.y;
pub fn is_dual_panel_mode(ui: &mut egui::Ui) -> bool {
let (w, h) = View::window_size(ui);
// Screen is wide if width is greater than height or just 20% smaller.
let is_wide_screen = w > h || w + (w * 0.2) >= h;
// Dual panel mode is available when window is wide and its width is at least 2 times
@ -168,10 +168,11 @@ impl Root {
}
/// 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 !Node::is_running() {
self.exit(frame);
self.exit_allowed = true;
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
modal.close();
}
ui.add_space(16.0);
@ -199,9 +200,10 @@ impl Root {
ui.columns(2, |columns| {
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() {
self.exit(frame);
self.exit_allowed = true;
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
modal.close();
} else {
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.
pub fn on_back(&mut self) {
if Modal::on_back() {

View file

@ -12,8 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Color32, Id, lerp, Rgba};
use egui::style::Margin;
use egui::{Margin, Color32, Id, lerp, Rgba};
use egui_extras::{Size, Strip, StripBuilder};
use crate::gui::Colors;
@ -59,7 +58,7 @@ impl TitlePanel {
.resizable(false)
.exact_height(Self::DEFAULT_HEIGHT)
.frame(egui::Frame {
inner_margin: Self::inner_margin(ui, frame),
inner_margin: Self::inner_margin(ui),
fill: Colors::YELLOW,
..Default::default()
})
@ -132,10 +131,10 @@ impl TitlePanel {
}
/// 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 {
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(),
bottom: 0.0,
}

View file

@ -12,13 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::ops::Add;
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::text::TextWrapping;
use egui::os::OperatingSystem;
use egui::text::{LayoutJob, TextFormat};
use lazy_static::lazy_static;
use egui::text_edit::TextEditState;
use crate::gui::Colors;
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.
pub fn on_enter_key(ui: &mut egui::Ui, cb: impl FnOnce()) {
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).
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 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.
if container_width == display_width {
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.
pub fn item_button(ui: &mut egui::Ui,
rounding: Rounding,
@ -336,15 +369,17 @@ impl View {
// Setup text edit size.
let mut edit_rect = ui.available_rect_before_wrap();
edit_rect.set_height(Self::TEXT_EDIT_HEIGHT);
// Show text edit.
let text_edit_resp = egui::TextEdit::singleline(value)
.id(options.id)
.margin(Vec2::new(2.0, 0.0))
.margin(egui::Vec2::new(2.0, 0.0))
.font(TextStyle::Heading)
.min_size(edit_rect.size())
.horizontal_align(if options.h_center { Align::Center } else { Align::Min })
.vertical_align(Align::Center)
.password(show_pass)
.cursor_at_end(true)
.ui(ui);
// Show keyboard on click.
if text_edit_resp.clicked() {
@ -355,6 +390,42 @@ impl View {
text_edit_resp.request_focus();
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();
}
});
});
}
@ -571,4 +642,33 @@ pub extern "C" fn Java_mw_gri_android_MainActivity_onDisplayInsets(
RIGHT_DISPLAY_INSET.store(array[1], Ordering::Relaxed);
BOTTOM_DISPLAY_INSET.store(array[2], 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();
// 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 wallet_panel_width = self.wallet_panel_width(ui, empty_list, dual_panel, show_wallet);
let content_width = ui.available_width();
@ -131,10 +131,10 @@ impl WalletsContent {
if create_wallet || !show_wallet {
// Show wallet creation content.
self.creation_content.ui(ui, frame, cb, |wallet| {
// Reset wallet content.
self.wallet_content = WalletContent::default();
// Add created wallet to list.
self.wallets.add(wallet);
// Reset wallet content.
self.wallet_content = WalletContent::default();
});
} else {
let selected_id = self.wallets.selected_id.clone();
@ -180,7 +180,7 @@ impl WalletsContent {
right: if list_hidden {
0.0
} else {
View::far_right_inset_margin(ui, frame) + 4.0
View::far_right_inset_margin(ui) + 4.0
},
top: 4.0,
bottom: View::get_bottom_inset() + 4.0,
@ -274,7 +274,7 @@ impl WalletsContent {
self.show_wallets_at_dual_panel = !show_list;
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, || {
Root::toggle_network_panel();
});
@ -605,8 +605,8 @@ impl WalletsContent {
}
/// 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 {
let dual_panel_root = Root::is_dual_panel_mode(frame);
fn is_dual_panel_mode(ui: &mut egui::Ui) -> bool {
let dual_panel_root = Root::is_dual_panel_mode(ui);
let max_width = ui.available_width();
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::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::views::{Modal, Root, View};
use crate::gui::views::types::{ModalPosition, TextEditOptions};
@ -86,7 +86,7 @@ impl WalletCreation {
ui.vertical_centered(|ui| {
ui.vertical_centered(|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.
fn step_control_ui(&mut self, ui: &mut egui::Ui, on_create: impl FnOnce(Wallet)) {
if let Some(step) = &self.step {
fn step_control_ui(&mut self,
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.
let (step_text, mut step_available) = match step {
Step::EnterMnemonic => {
@ -179,27 +174,121 @@ impl WalletCreation {
.color(Colors::RED));
ui.add_space(2.0);
}
// Show next step button if there are no empty words.
if step_available {
// Setup button text.
let (next_text, color) = if step == &Step::SetupConnection {
(format!("{} {}", CHECK, t!("complete")), Colors::GOLD)
if step == Step::EnterMnemonic {
ui.add_space(4.0);
if !step_available {
self.copy_or_paste_button_ui(ui, cb);
} else {
let text = format!("{} {}", SHARE_FAT, t!("continue"));
(text, Colors::WHITE)
};
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0);
ui.add_space(4.0);
// Show button.
View::button(ui, next_text.to_uppercase(), color, || {
self.forward(on_create);
ui.columns(2, |columns| {
// Show copy or paste button for mnemonic phrase step.
columns[0].vertical_centered_justified(|ui| {
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.
fn step_content_ui(&mut self,
ui: &mut egui::Ui,
@ -256,7 +345,12 @@ impl WalletCreation {
None => {}
Some(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::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.
pub fn show_name_pass_modal(&mut self, cb: &dyn PlatformCallbacks) {
// Reset modal values.

View file

@ -167,13 +167,13 @@ impl MnemonicSetup {
let words = match self.mnemonic.mode {
PhraseMode::Generate => {
if edit_words {
self.mnemonic.confirm_words.clone()
&self.mnemonic.confirm_words
} 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 cols = list_columns_count(ui);

View file

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

View file

@ -185,22 +185,20 @@ impl RecoverySetup {
.size(17.0)
.color(Colors::GRAY));
ui.add_space(8.0);
});
// Draw current wallet password text edit.
let pass_edit_id = Id::from(modal.id).with(wallet.get_config().id);
let pass_edit_opts = TextEditOptions::new(pass_edit_id).password();
View::text_edit(ui, cb, &mut self.pass_edit, pass_edit_opts);
// Draw current wallet password text edit.
let pass_edit_id = Id::from(modal.id).with(wallet.get_config().id);
let pass_edit_opts = TextEditOptions::new(pass_edit_id).password();
View::text_edit(ui, cb, &mut self.pass_edit, pass_edit_opts);
// Show information when password is empty.
ui.vertical_centered(|ui| {
// Show information when password is empty or wrong.
if self.pass_edit.is_empty() {
ui.add_space(10.0);
ui.add_space(12.0);
ui.label(RichText::new(t!("wallets.pass_empty"))
.size(17.0)
.color(Colors::INACTIVE_TEXT));
} else if self.wrong_pass {
ui.add_space(10.0);
ui.add_space(12.0);
ui.label(RichText::new(t!("wallets.wrong_pass"))
.size(17.0)
.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.
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() {
Self::sync_progress_ui(ui, wallet);
return true;
@ -384,7 +384,7 @@ impl WalletContent {
return true;
} else if wallet.get_current_ext_conn_id().is_none() {
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| {
let text = t!("wallets.enable_node", "settings" => GEAR_FINE);
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,
ui: &mut egui::Ui,
frame: &mut eframe::Frame,
_: &mut eframe::Frame,
wallet: &mut Wallet,
_: &dyn PlatformCallbacks) {
if WalletContent::sync_ui(ui, frame, wallet) {
if WalletContent::sync_ui(ui, wallet) {
return;
}

View file

@ -12,35 +12,58 @@
// See the License for the specific language governing permissions and
// 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::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::views::{Root, View};
use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType};
use crate::gui::views::{Modal, Root, View};
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::wallet::Wallet;
/// Receiving tab content.
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,
/// Generated Slatepack response.
/// Generated Slatepack response message for sender.
response_edit: String,
/// Flag to check if there is an error happened on receive.
receive_error: bool,
/// Flag to check if response was copied to the clipboard.
response_copied: bool,
/// Flag to check if there is an error happened on response creation.
response_error: 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 {
fn default() -> Self {
Self {
is_invoice: false,
message_edit: "".to_string(),
response_edit: "".to_string(),
receive_error: false,
response_copied: false,
response_error: 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,
ui: &mut egui::Ui,
frame: &mut eframe::Frame,
_: &mut eframe::Frame,
wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) {
if WalletContent::sync_ui(ui, frame, wallet) {
if WalletContent::sync_ui(ui, wallet) {
return;
}
// Show modal content for this ui container.
self.modal_content_ui(ui, wallet, cb);
// Show receiving content panel.
egui::CentralPanel::default()
.frame(egui::Frame {
@ -73,18 +99,21 @@ impl WalletTab for WalletReceive {
..Default::default()
})
.show_inside(ui, |ui| {
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.receive_ui(ui, wallet, cb);
ScrollArea::vertical()
.scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible)
.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 {
/// Draw receiving content.
pub fn receive_ui(&mut self,
@ -95,18 +124,91 @@ impl WalletReceive {
View::sub_title(ui, format!("{} {}", HAND_COINS, t!("wallets.manually")));
View::horizontal_line(ui, Colors::ITEM_STROKE);
ui.add_space(3.0);
// Show manual receiving content.
self.manual_ui(ui, wallet, cb);
}
// Setup manual sending description.
let response_empty = self.response_edit.is_empty();
let desc_text = if response_empty {
t!("wallets.receive_paste_slatepack")
/// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self,
ui: &mut egui::Ui,
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 {
t!("wallets.receive_send_slatepack")
};
ui.label(RichText::new(desc_text).size(16.0).color(Colors::INACTIVE_TEXT));
ui.add_space(3.0);
// Show manual transaction receiving content.
self.manual_receive_ui(ui, wallet, cb);
}
}
// 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 {
&mut self.message_edit
} else {
@ -126,12 +228,12 @@ impl WalletReceive {
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(response_empty)
.hint_text(RECEIVE_SLATEPACK_HINT)
.hint_text(SLATEPACK_MESSAGE_HINT)
.desired_width(f32::INFINITY)
.show(ui);
// Clear an error when message changed.
if &message_before != message {
self.receive_error = false;
self.response_error = false;
}
ui.add_space(6.0);
});
@ -139,38 +241,27 @@ impl WalletReceive {
View::horizontal_line(ui, Colors::ITEM_STROKE);
ui.add_space(10.0);
// Show receiving input control buttons.
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) {
// Draw buttons to clear/copy/paste.
let field_is_empty = self.message_edit.is_empty() && self.response_edit.is_empty();
let columns_num = if !field_is_empty { 2 } else { 1 };
// Draw buttons to clear/copy/paste.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(columns_num, |columns| {
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"));
View::button(ui, clear_text, Colors::BUTTON, || {
self.receive_error = false;
self.response_copied = false;
self.response_error = false;
self.message_edit.clear();
self.response_edit.clear();
});
} else if self.message_edit.is_empty() {
} else {
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste_text, Colors::BUTTON, || {
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);
} else {
columns[0].vertical_centered_justified(first_column_content);
}
if !field_is_empty {
columns[1].vertical_centered_justified(|ui| {
if !self.message_edit.is_empty() {
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste_text, Colors::BUTTON, || {
self.message_edit = cb.get_string_from_buffer();
self.receive_error = false;
if self.response_error {
let clear_text = format!("{} {}", BROOM, t!("clear"));
View::button(ui, clear_text, Colors::BUTTON, || {
self.response_error = false;
self.message_edit.clear();
self.response_edit.clear();
});
} else if !self.response_edit.is_empty() {
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::BUTTON, || {
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.
if !self.message_edit.is_empty() && !self.receive_error {
ui.add_space(8.0);
let create_text = format!("{} {}", ARCHIVE_BOX, t!("wallets.create_response"));
View::button(ui, create_text, Colors::GOLD, || {
match wallet.receive(self.message_edit.clone()) {
Ok(response) => {
self.response_edit = response.trim().to_string();
self.message_edit.clear();
// Copy response to clipboard.
cb.copy_string_to_buffer(response);
self.response_copied = true;
},
Err(_) => self.receive_error = true
/// Draw invoice creation content.
fn manual_invoice_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) {
ui.label(RichText::new(t!("wallets.issue_invoice_desc"))
.size(16.0)
.color(Colors::INACTIVE_TEXT));
ui.add_space(6.0);
// Draw invoice creation button.
let invoice_text = format!("{} {}", RECEIPT, t!("wallets.issue_invoice"));
View::button(ui, invoice_text, Colors::BUTTON, || {
// 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);
} else if self.receive_error {
ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.receive_slatepack_err"))
ui.label(RichText::new(t!("wallets.finalize_slatepack_err"))
.size(16.0)
.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);
} else if self.response_copied {
ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.response_copied"))
.size(16.0)
.color(Colors::GREEN));
ui.add_space(8.0);
// Draw invoice amount text edit.
let amount_edit_id = Id::from(modal.id).with(wallet.get_config().id);
let amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center();
let mut amount_edit = self.amount_edit.clone();
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,
ui: &mut egui::Ui,
frame: &mut eframe::Frame,
_: &mut eframe::Frame,
wallet: &mut Wallet,
_: &dyn PlatformCallbacks) {
if WalletContent::sync_ui(ui, frame, wallet) {
if WalletContent::sync_ui(ui, wallet) {
return;
}

View file

@ -16,7 +16,7 @@
extern crate rust_i18n;
use eframe::wgpu;
use egui::{Context, Stroke};
use egui::{Context, Stroke, vec2};
#[cfg(target_os = "android")]
use winit::platform::android::activity::AndroidApp;
@ -61,16 +61,21 @@ fn android_main(app: AndroidApp) {
use gui::PlatformApp;
let platform = Android::new(app.clone());
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.
options.wgpu_options.device_descriptor = std::sync::Arc::new(|adapter| {
let base_limits = wgpu::Limits::downlevel_webgl2_defaults();
wgpu::DeviceDescriptor {
label: Some("egui wgpu device"),
features: wgpu::Features::default(),
limits: wgpu::Limits {
required_features: wgpu::Features::default(),
required_limits: wgpu::Limits {
max_texture_dimension_2d: 8192,
..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) {
options.default_theme = eframe::Theme::Light;
options.renderer = eframe::Renderer::Wgpu;
options.initial_window_size = Some(egui::Vec2::new(1200.0, 720.0));
// Setup translations.
setup_i18n();
// Start integrated node if needed.
@ -119,7 +123,7 @@ pub fn setup_visuals(ctx: &Context) {
// Setup spacing for buttons.
style.spacing.button_padding = egui::vec2(12.0, 8.0);
// Make scroll-bar thinner.
style.spacing.scroll_bar_width = 4.0;
style.spacing.scroll.bar_width = 4.0;
// Disable spacing between items.
style.spacing.item_spacing = egui::vec2(0.0, 0.0);
// Setup radio button/checkbox size and spacing.

View file

@ -31,6 +31,9 @@ fn real_main() {
use grim::gui::PlatformApp;
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)));
}

View file

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

View file

@ -27,7 +27,7 @@ use futures::channel::oneshot;
use grin_api::{ApiServer, Router};
use grin_chain::SyncStatus;
use grin_core::global;
use grin_keychain::{ExtKeychain, Keychain};
use grin_keychain::{ExtKeychain, Identifier, Keychain};
use grin_util::Mutex;
use grin_util::types::ZeroingString;
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::ForeignAPIHandlerV2;
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 crate::node::{Node, NodeConfig};
@ -205,6 +205,15 @@ impl 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.
pub fn get_config(&self) -> WalletConfig {
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.
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 new_instance = Self::create_wallet_instance(config.clone())?;
self.instance = Some(new_instance);
@ -352,6 +361,9 @@ impl Wallet {
let mut api = Owner::new(self.instance.clone().unwrap(), None);
controller::owner_single_use(None, None, Some(&mut api), |api, m| {
api.create_account_path(m, label)?;
// Sync wallet data.
self.sync();
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> {
let mut api = Owner::new(self.instance.clone().unwrap(), None);
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)?;
Ok(())
})?;
let mut response = "".to_string();
controller::owner_single_use(None, None, Some(&mut api), |api, m| {
response = api.create_slatepack_message(m, &slate, Some(0), vec![])?;
Ok(())
})?;
// Create Slatepack message response.
let response = self.create_slatepack_message(slate)?;
// Create a directory to which slatepack files will be output.
let mut slatepack_dir = config.get_slatepacks_path();
let slatepack_file_name = format!("{}.{}.slatepack", slate.id, slate.state);
slatepack_dir.push(slatepack_file_name);
// Sync wallet info.
self.sync();
Ok(response)
}
Err(_) => {
Err(Error::GenericError("Parsing error".to_string()))
}
}
}
// Write Slatepack response into the file.
let mut output = File::create(slatepack_dir)?;
output.write_all(response.as_bytes())?;
output.sync_all()?;
/// S transaction via Slatepack message and return response to sender.
pub fn pay(&self, message: String) -> Result<String, Error> {
let mut api = Owner::new(self.instance.clone().unwrap(), None);
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.
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) {
}
@ -513,6 +581,7 @@ impl Wallet {
cancelling_r.contains(id)
}
/// Finalize transaction from provided Slatepack message.
pub fn finalize(&self) {
}
@ -571,7 +640,7 @@ impl Wallet {
wallet_delete.is_open.store(false, Ordering::Relaxed);
wallet_delete.deleted.store(true, Ordering::Relaxed);
// Wake up thread to exit.
// Start sync to exit.
wallet_delete.sync();
});
}
@ -786,16 +855,13 @@ fn sync_wallet_data(wallet: &Wallet) {
}
});
// Retrieve txs.
match retrieve_txs(
instance.clone(),
None,
&Some(txs_tx),
true,
None,
None,
None
) {
match retrieve_txs(instance.clone(),
None,
&Some(txs_tx),
true,
None,
None,
None) {
Ok(txs) => {
// Do not sync data if wallet was closed.
if !wallet.is_open() {
@ -807,9 +873,20 @@ fn sync_wallet_data(wallet: &Wallet) {
wallet.reset_sync_attempts();
// Setup transactions.
let mut txs = txs.1;
let mut sort_txs = txs.1;
// 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.
for tx in &txs {
if tx.tx_type == TxLogEntryType::TxSentCancelled