ui: refactoring of wallet screen, fix colors

This commit is contained in:
ardocrat
2025-06-09 12:34:07 +03:00
parent b54a573f61
commit 8b369b6049
33 changed files with 1401 additions and 1252 deletions
+1 -2
View File
@@ -17,11 +17,10 @@ use egui::{Align, Context, CornerRadius, CursorIcon, LayerId, Layout, Modifiers,
use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::ContentContainer;
use crate::gui::views::{Content, KeyboardContent, Modal, TitlePanel, View};
use crate::gui::Colors;
use crate::wallet::ExternalConnection;
use crate::AppConfig;
use crate::gui::views::types::ContentContainer;
/// Implements ui entry point and contains platform-specific callbacks.
pub struct App<Platform> {
+5 -5
View File
@@ -34,20 +34,20 @@ const GREEN: Color32 = Color32::from_rgb(0, 0x64, 0);
const GREEN_DARK: Color32 = Color32::from_rgb(0, (0x64 as f32 * 1.3 + 0.5) as u8, 0);
const RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
const RED_DARK: Color32 = Color32::from_rgb((0x8B as f32 * 1.3 + 0.5) as u8, 0, 0);
const RED_DARK: Color32 = Color32::from_rgb((0x8B as f32 * 1.3 + 0.5) as u8, 50, 30);
const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4);
const BLUE_DARK: Color32 =
Color32::from_rgb(0, (0x66 as f32 * 1.3 + 0.5) as u8, (0xE4 as f32 * 1.3 + 0.5) as u8);
const FILL: Color32 = Color32::from_gray(244);
const FILL_DARK: Color32 = Color32::from_gray(24);
const FILL_DARK: Color32 = Color32::from_gray(26);
const FILL_DEEP: Color32 = Color32::from_gray(238);
const FILL_DEEP_DARK: Color32 = Color32::from_gray(18);
const FILL_DEEP_DARK: Color32 = Color32::from_gray(32);
const FILL_LITE: Color32 = Color32::from_gray(249);
const FILL_LITE_DARK: Color32 = Color32::from_gray(16);
const FILL_LITE_DARK: Color32 = Color32::from_gray(21);
const TEXT: Color32 = Color32::from_gray(80);
const TEXT_DARK: Color32 = Color32::from_gray(185);
@@ -231,7 +231,7 @@ impl Colors {
}
}
pub fn item_button() -> Color32 {
pub fn item_button_text() -> Color32 {
if use_dark() {
ITEM_BUTTON_DARK
} else {
+2 -2
View File
@@ -50,7 +50,6 @@ impl Default for CameraContent {
impl CameraContent {
/// Draw camera content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.ctx().request_repaint();
let rect = if let Some(img_data) = cb.camera_image() {
if let Ok(img) =
image::load_from_memory(&*img_data.0) {
@@ -85,6 +84,7 @@ impl CameraContent {
});
});
}
ui.ctx().request_repaint();
}
/// Draw camera image.
@@ -311,7 +311,7 @@ impl CameraContent {
// Check if string contains Slatepack message prefix and postfix.
if text.starts_with("BEGINSLATEPACK.") && text.ends_with("ENDSLATEPACK.") {
return QrScanResult::Slatepack(ZeroingString::from(text));
return QrScanResult::Slatepack(text.to_string());
}
// Check Uniform Resource data.
+80 -24
View File
@@ -12,42 +12,58 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{fs, thread};
use egui::CornerRadius;
use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::{fs, thread};
use crate::gui::Colors;
use crate::gui::icons::ARCHIVE_BOX;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::View;
use crate::gui::Colors;
/// Type of button.
pub enum FilePickContentType {
Button, ItemButton(CornerRadius), Tab
}
/// Button to pick file and parse its data into text.
pub struct FilePickButton {
pub struct FilePickContent {
/// Content type.
content_type: FilePickContentType,
/// Flag to check if file is picking.
pub file_picking: Arc<AtomicBool>,
file_picking: Arc<AtomicBool>,
/// Flag to parse file content after pick.
parse_file: bool,
/// Flag to check if file is parsing.
pub file_parsing: Arc<AtomicBool>,
file_parsing: Arc<AtomicBool>,
/// File parsing result.
pub file_parsing_result: Arc<RwLock<Option<String>>>
file_parsing_result: Arc<RwLock<Option<String>>>,
}
impl Default for FilePickButton {
fn default() -> Self {
impl FilePickContent {
/// Create new content from provided type.
pub fn new(content_type: FilePickContentType) -> Self {
Self {
content_type,
file_picking: Arc::new(AtomicBool::new(false)),
parse_file: true,
file_parsing: Arc::new(AtomicBool::new(false)),
file_parsing_result: Arc::new(RwLock::new(None))
file_parsing_result: Arc::new(RwLock::new(None)),
}
}
}
impl FilePickButton {
/// Draw button content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
on_result: impl FnOnce(String)) {
/// Do not parse file content.
pub fn no_parse(mut self) -> Self {
self.parse_file = false;
self
}
/// Draw content with provided callback to return path of the file.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks, on_pick: impl FnOnce(String)) {
if self.file_picking.load(Ordering::Relaxed) {
View::small_loading_spinner(ui);
// Check file pick result.
@@ -70,7 +86,7 @@ impl FilePickButton {
r_res.clone().unwrap()
};
// Callback on result.
on_result(text);
on_pick(text);
// Clear result.
let mut w_res = self.file_parsing_result.write();
*w_res = None;
@@ -78,12 +94,48 @@ impl FilePickButton {
}
} else {
// Draw button to pick file.
let text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || {
if let Some(path) = cb.pick_file() {
self.on_file_pick(path);
match self.content_type {
FilePickContentType::Button => {
let text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
View::colored_text_button(ui,
text,
Colors::blue(),
Colors::white_or_black(false),
|| {
if let Some(path) = cb.pick_file() {
if !self.parse_file {
on_pick(path);
return;
}
self.on_file_pick(path);
}
});
}
});
FilePickContentType::ItemButton(r) => {
View::item_button(ui, r, ARCHIVE_BOX, Some(Colors::blue()), || {
if let Some(path) = cb.pick_file() {
if !self.parse_file {
on_pick(path);
return;
}
self.on_file_pick(path);
}
});
}
FilePickContentType::Tab => {
let active = self.file_parsing.load(Ordering::Relaxed) ||
self.file_picking.load(Ordering::Relaxed);
View::tab_button(ui, ARCHIVE_BOX, Some(Colors::blue()), Some(active), |_| {
if let Some(path) = cb.pick_file() {
if !self.parse_file {
on_pick(path);
return;
}
self.on_file_pick(path);
}
});
}
}
}
}
@@ -94,6 +146,10 @@ impl FilePickButton {
self.file_picking.store(true, Ordering::Relaxed);
return;
}
// Do not parse result.
if !self.parse_file {
return;
}
self.file_parsing.store(true, Ordering::Relaxed);
let result = self.file_parsing_result.clone();
thread::spawn(move || {
+10 -6
View File
@@ -15,7 +15,7 @@
use egui::scroll_area::ScrollBarVisibility;
use egui::{Id, Margin, RichText, ScrollArea};
use crate::gui::icons::{ARROWS_COUNTER_CLOCKWISE, ARROW_LEFT, BRIEFCASE, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE, GEAR, POWER};
use crate::gui::icons::{ARROWS_COUNTER_CLOCKWISE, ARROW_LEFT, BRIEFCASE, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE, GEAR, GLOBE, POWER};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::network::{ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings};
@@ -230,22 +230,26 @@ impl NetworkContent {
let current_type = self.node_tab_content.get_type();
ui.columns(4, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::tab_button(ui, DATABASE, current_type == NodeTabType::Info, |_| {
let active = Some(current_type == NodeTabType::Info);
View::tab_button(ui, DATABASE, None, active, |_| {
self.node_tab_content = Box::new(NetworkNode::default());
});
});
columns[1].vertical_centered_justified(|ui| {
View::tab_button(ui, GAUGE, current_type == NodeTabType::Metrics, |_| {
let active = Some(current_type == NodeTabType::Metrics);
View::tab_button(ui, GAUGE, None, active, |_| {
self.node_tab_content = Box::new(NetworkMetrics::default());
});
});
columns[2].vertical_centered_justified(|ui| {
View::tab_button(ui, FACTORY, current_type == NodeTabType::Mining, |_| {
let active = Some(current_type == NodeTabType::Mining);
View::tab_button(ui, FACTORY, None, active, |_| {
self.node_tab_content = Box::new(NetworkMining::default());
});
});
columns[3].vertical_centered_justified(|ui| {
View::tab_button(ui, FADERS, current_type == NodeTabType::Settings, |_| {
let active = Some(current_type == NodeTabType::Settings);
View::tab_button(ui, FADERS, None, active, |_| {
self.node_tab_content = Box::new(NetworkSettings::default());
});
});
@@ -276,7 +280,7 @@ impl NetworkContent {
self.settings_content = None;
});
} else if !show_connections {
View::title_button_big(ui, DOTS_THREE_OUTLINE_VERTICAL, |ui| {
View::title_button_big(ui, GLOBE, |_| {
AppConfig::toggle_show_connections_network_panel();
});
} else if !dual_panel {
+2 -4
View File
@@ -764,10 +764,8 @@ fn peer_item_ui(ui: &mut egui::Ui,
rect.set_height(42.0);
// Draw round background.
let mut bg_rect = rect.clone();
bg_rect.min += egui::emath::vec2(6.0, 0.0);
let item_rounding = View::item_rounding(index, len, false);
ui.painter().rect(bg_rect,
ui.painter().rect(rect,
item_rounding,
Colors::white_or_black(false),
View::item_stroke(),
@@ -798,7 +796,7 @@ fn peer_item_ui(ui: &mut egui::Ui,
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(12.0);
ui.add_space(6.0);
// Draw peer address.
let peer_text = format!("{} {}", GLOBE_SIMPLE, &peer_addr);
ui.label(RichText::new(peer_text)
+73 -65
View File
@@ -21,15 +21,15 @@ use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, View};
use crate::gui::views::types::QrScanResult;
/// QR code scan [`Modal`] content.
pub struct CameraScanModal {
/// Camera content for QR scan [`Modal`].
/// QR code scanning content.
pub struct CameraScanContent {
/// Camera content.
camera_content: Option<CameraContent>,
/// QR code scan result
/// Scan result.
qr_scan_result: Option<QrScanResult>,
}
impl Default for CameraScanModal {
impl Default for CameraScanContent {
fn default() -> Self {
Self {
camera_content: Some(CameraContent::default()),
@@ -38,68 +38,20 @@ impl Default for CameraScanModal {
}
}
impl CameraScanModal {
impl CameraScanContent {
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
mut on_result: impl FnMut(&QrScanResult)) {
pub fn modal_ui(&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
mut on_result: impl FnMut(&QrScanResult)) {
// Show scan result if exists or show camera content while scanning.
if let Some(result) = &self.qr_scan_result {
let mut result_text = result.text();
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(Id::from("qr_scan_result_input"))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
egui::TextEdit::multiline(&mut result_text)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(false)
.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);
// Show copy button.
ui.vertical_centered(|ui| {
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(result_text.to_string());
self.qr_scan_result = None;
Modal::close();
});
});
ui.add_space(10.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_scan_result = None;
self.camera_content = None;
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
Modal::set_title(t!("scan_qr"));
self.qr_scan_result = None;
self.camera_content = Some(CameraContent::default());
cb.start_camera();
});
});
if let Some(result) = &self.qr_scan_result.clone() {
Self::result_ui(ui, result, cb, || {
Modal::close();
}, || {
self.qr_scan_result = None;
cb.start_camera();
Modal::set_title(t!("scan_qr"));
});
} else if let Some(camera_content) = self.camera_content.as_mut() {
if let Some(result) = camera_content.qr_scan_result() {
@@ -126,4 +78,60 @@ impl CameraScanModal {
}
ui.add_space(6.0);
}
/// Draw scan result content.
pub fn result_ui(ui: &mut egui::Ui,
result: &QrScanResult,
cb: &dyn PlatformCallbacks,
on_close: impl FnOnce(),
on_repeat: impl FnOnce()) {
let mut result_text = result.text();
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(Id::from("qr_scan_result_input"))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
egui::TextEdit::multiline(&mut result_text)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(false)
.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);
// Show copy button.
ui.vertical_centered(|ui| {
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(result_text.to_string());
});
});
ui.add_space(10.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
on_close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
on_repeat();
});
});
});
}
}
+20 -3
View File
@@ -14,8 +14,7 @@
use crate::gui::icons::GLOBE_SIMPLE;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::settings::interface::InterfaceSettingsContent;
use crate::gui::views::settings::network::NetworkSettingsContent;
use crate::gui::views::settings::{InterfaceSettingsContent, NetworkSettingsContent};
use crate::gui::views::types::ContentContainer;
use crate::gui::views::View;
use crate::gui::Colors;
@@ -26,6 +25,7 @@ pub struct SettingsContent {
interface_settings: InterfaceSettingsContent,
/// Network communication settings.
network_settings: NetworkSettingsContent,
// tor_settings: TorSettingsContent,
}
impl Default for SettingsContent {
@@ -33,6 +33,7 @@ impl Default for SettingsContent {
Self {
interface_settings: InterfaceSettingsContent::default(),
network_settings: NetworkSettingsContent::default(),
//tor_settings: TorSettingsContent::default(),
}
}
}
@@ -53,6 +54,22 @@ impl SettingsContent {
// Show network settings.
self.network_settings.ui(ui, cb);
ui.add_space(6.0);
ui.add_space(8.0);
// Do not show Tor settings on Android.
// let os = OperatingSystem::from_target_os();
// let show_tor = os != OperatingSystem::Android;
// if show_tor {
// View::horizontal_line(ui, Colors::stroke());
// ui.add_space(6.0);
//
// View::sub_title(ui, format!("{} {}", CIRCLE_HALF, t!("transport.tor_network")));
// View::horizontal_line(ui, Colors::stroke());
// ui.add_space(6.0);
//
// // Show Tor settings.
// self.tor_settings.ui(ui, cb);
// ui.add_space(8.0);
// }
}
}
+9 -3
View File
@@ -13,7 +13,13 @@
// limitations under the License.
mod content;
mod interface;
mod network;
pub use content::*;
pub use content::*;
mod interface;
pub use interface::*;
mod network;
pub use network::*;
mod tor;
pub use tor::*;
+4 -4
View File
@@ -94,9 +94,6 @@ impl NetworkSettingsContent {
/// Draw proxy edit modal content.
fn proxy_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut NetworkSettingsContent| {
// Check if URL proxy is correct.
let http = "http://";
let socks = "socks5://";
let proxy = c.proxy_url_edit.trim().to_string();
let use_socks = AppConfig::use_socks_proxy();
// Clear value if empty.
@@ -109,6 +106,9 @@ impl NetworkSettingsContent {
Modal::close();
return;
}
// Format URL.
let http = "http://";
let socks = "socks5://";
let url = if use_socks {
let p = proxy.replace(http, "");
if !p.contains(socks) {
@@ -200,7 +200,7 @@ impl NetworkSettingsContent {
});
}
/// Draw language selection item content.
/// Draw proxy item content.
fn proxy_item_ui(&mut self, ui: &mut egui::Ui) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
+1 -1
View File
@@ -77,7 +77,7 @@ pub trait ContentContainer {
#[derive(Clone)]
pub enum QrScanResult {
/// Slatepack message.
Slatepack(ZeroingString),
Slatepack(String),
/// Slatepack address.
Address(ZeroingString),
/// Parsed text.
+30 -8
View File
@@ -197,17 +197,31 @@ impl View {
/// Tab button with white background fill color, contains only icon.
pub fn tab_button(ui: &mut egui::Ui,
icon: &str,
active: bool,
color: Option<Color32>,
selected: Option<bool>,
action: impl FnOnce(&mut egui::Ui)) {
ui.scope(|ui| {
let text_color = match active {
true => Colors::title(false),
false => Colors::text(false)
let text_color = if let Some(c) = color {
if selected.is_none() {
Colors::inactive_text().gamma_multiply(1.2)
} else {
c
}
} else {
if let Some(active) = selected {
match active {
true => Colors::gray(),
false => Colors::item_button_text()
}
} else {
Colors::inactive_text().gamma_multiply(1.2)
}
};
let mut button = Button::new(RichText::new(icon).size(22.0).color(text_color));
if !active {
let active_not_selected = selected.is_some() && !selected.unwrap();
if active_not_selected {
// Disable expansion on click/hover.
ui.style_mut().visuals.widgets.hovered.expansion = 0.0;
ui.style_mut().visuals.widgets.active.expansion = 0.0;
@@ -223,7 +237,13 @@ impl View {
button = button.fill(Colors::fill()).stroke(Stroke::NONE);
}
let br = button.ui(ui).on_hover_cursor(CursorIcon::PointingHand);
// Setup pointer style.
let br = if active_not_selected {
button.ui(ui).on_hover_cursor(CursorIcon::PointingHand)
} else {
button.ui(ui)
};
br.surrender_focus();
if br.clicked() {
action(ui);
@@ -322,7 +342,7 @@ impl View {
ui.visuals_mut().widgets.active.bg_stroke = Stroke::NONE;
// Setup button text color.
let text_color = if let Some(c) = color { c } else { Colors::item_button() };
let text_color = if let Some(c) = color { c } else { Colors::item_button_text() };
// Show button.
let br = Button::new(RichText::new(text).size(20.0).color(text_color))
@@ -592,7 +612,9 @@ impl View {
if resp.clicked() || resp.dragged() {
on_click();
}
let shape = RectShape::filled(resp.rect, CornerRadius::ZERO, Colors::semi_transparent());
let shape = RectShape::filled(resp.rect,
CornerRadius::ZERO,
Colors::semi_transparent().gamma_multiply(0.7));
ui.painter().add(shape);
}
+147 -192
View File
@@ -22,13 +22,12 @@ use crate::gui::views::settings::SettingsContent;
use crate::gui::views::types::{ContentContainer, LinePosition, ModalPosition, TitleContentType, TitleType};
use crate::gui::views::wallets::creation::WalletCreationContent;
use crate::gui::views::wallets::modals::{AddWalletModal, OpenWalletModal, WalletConnectionModal, WalletsModal};
use crate::gui::views::wallets::types::WalletTabType;
use crate::gui::views::wallets::wallet::types::wallet_status_text;
use crate::gui::views::wallets::wallet::types::{wallet_status_text, WalletContentContainer};
use crate::gui::views::wallets::WalletContent;
use crate::gui::views::{Content, Modal, TitlePanel, View};
use crate::gui::Colors;
use crate::wallet::types::ConnectionMethod;
use crate::wallet::{ExternalConnection, Wallet, WalletList};
use crate::wallet::{Wallet, WalletList};
use crate::AppConfig;
/// Wallets content.
@@ -46,7 +45,7 @@ pub struct WalletsContent {
wallet_selection_content: WalletsModal,
/// Selected [`Wallet`] content.
wallet_content: Option<WalletContent>,
wallet_content: WalletContent,
/// Wallet creation content.
creation_content: Option<WalletCreationContent>,
@@ -64,10 +63,10 @@ impl Default for WalletsContent {
Self {
wallets: WalletList::default(),
wallet_selection_content: WalletsModal::new(None, None, true),
open_wallet_content: OpenWalletModal::new(None),
open_wallet_content: OpenWalletModal::new(),
add_wallet_modal_content: AddWalletModal::default(),
conn_selection_content: WalletConnectionModal::new(ConnectionMethod::Integrated),
wallet_content: None,
wallet_content: WalletContent::default(),
creation_content: None,
settings_content: None,
}
@@ -94,9 +93,9 @@ impl ContentContainer for WalletsContent {
});
},
OPEN_WALLET_MODAL => {
self.open_wallet_content.ui(ui, modal, cb, |pass, _| {
if let Some(content) = &self.wallet_content {
return match content.wallet.open(pass) {
self.open_wallet_content.ui(ui, modal, cb, |pass| {
if let Some(w) = self.wallets.selected().as_ref() {
return match w.open(pass) {
Ok(_) => true,
Err(_) => false
};
@@ -106,22 +105,25 @@ impl ContentContainer for WalletsContent {
},
SELECT_CONNECTION_MODAL => {
self.conn_selection_content.ui(ui, modal, cb, |conn| {
if let Some(wallet_content) = &self.wallet_content {
wallet_content.wallet.update_connection(&conn);
if let Some(w) = self.wallets.selected().as_ref() {
w.update_connection(&conn);
}
});
}
SELECT_WALLET_MODAL => {
let mut w: Option<Wallet> = None;
let mut d: Option<String> = None;
self.wallet_selection_content.ui(ui, &mut self.wallets, |wallet, data| {
if !wallet.is_open() {
self.open_wallet_content = OpenWalletModal::new(data.clone());
Modal::new(OPEN_WALLET_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.open"))
.show();
}
self.wallet_content = Some(WalletContent::new(wallet, data));
w = Some(wallet);
d = data;
});
if let Some(wallet) = &w {
if !wallet.is_open() {
self.show_opening_modal(wallet, d, cb);
} else {
self.select_wallet(wallet, d, cb);
}
}
}
_ => {}
}
@@ -130,7 +132,7 @@ impl ContentContainer for WalletsContent {
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if let Some(data) = crate::consume_incoming_data() {
if !data.is_empty() {
self.on_data(ui, Some(data));
self.on_data(ui, Some(data), cb);
}
}
@@ -140,12 +142,10 @@ impl ContentContainer for WalletsContent {
let dual_panel = is_dual_panel_mode(ui);
let content_width = ui.available_width();
let list_hidden = showing_settings || creating_wallet || self.wallets.list().is_empty()
|| (showing_wallet && self.wallet_content.as_ref().unwrap().qr_scan_showing())
|| (dual_panel && showing_wallet && !AppConfig::show_wallets_at_dual_panel())
|| (!dual_panel && showing_wallet);
|| (showing_wallet && (!dual_panel || !AppConfig::show_wallets_at_dual_panel()));
// Show title panel.
self.title_ui(ui, dual_panel, showing_wallet, cb);
self.title_ui(ui, dual_panel, cb);
egui::SidePanel::right("wallet_panel")
.resizable(false)
@@ -155,54 +155,53 @@ impl ContentContainer for WalletsContent {
content_width - Content::SIDE_PANEL_WIDTH
})
.frame(egui::Frame {
fill: Colors::fill_deep(),
..Default::default()
})
.show_animated_inside(ui, showing_wallet, |ui| {
// Show opened wallet content.
if let Some(content) = self.wallet_content.as_mut() {
content.ui(ui, cb);
// Show selected wallet content.
if let Some(w) = self.wallets.selected().as_ref() {
self.wallet_content.ui(ui, w, cb);
}
});
if !list_hidden {
egui::TopBottomPanel::bottom("wallets_bottom_panel")
.frame(egui::Frame {
inner_margin: Margin {
left: (View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING) as i8,
right: (View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING) as i8,
top: View::TAB_ITEMS_PADDING as i8,
bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8,
},
fill: Colors::fill(),
..Default::default()
})
.resizable(false)
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
egui::TopBottomPanel::bottom("wallets_bottom_panel")
.frame(egui::Frame {
inner_margin: Margin {
left: (View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING) as i8,
right: (View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING) as i8,
top: View::TAB_ITEMS_PADDING as i8,
bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8,
},
fill: Colors::fill(),
..Default::default()
})
.resizable(false)
.show_animated_inside(ui, !list_hidden, |ui| {
let rect = ui.available_rect_before_wrap();
// Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
// Setup vertical padding inside buttons.
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0);
// Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
// Setup vertical padding inside buttons.
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0);
ui.vertical_centered(|ui| {
let pressed = Modal::opened() == Some(ADD_WALLET_MODAL);
View::tab_button(ui, PLUS, pressed, |_| {
self.show_add_wallet_modal();
});
ui.vertical_centered(|ui| {
let pressed = Modal::opened() == Some(ADD_WALLET_MODAL);
View::tab_button(ui, PLUS, None, Some(pressed), |_| {
self.show_add_wallet_modal();
});
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.y -= View::TAB_ITEMS_PADDING;
r.min.x -= View::TAB_ITEMS_PADDING;
r.max.x += View::TAB_ITEMS_PADDING;
r
};
View::line(ui, LinePosition::TOP, &r, Colors::stroke());
});
}
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.y -= View::TAB_ITEMS_PADDING;
r.min.x -= View::TAB_ITEMS_PADDING;
r.max.x += View::TAB_ITEMS_PADDING;
r
};
View::line(ui, LinePosition::TOP, &r, Colors::stroke());
});
egui::SidePanel::left("wallet_list_panel")
.exact_width(if dual_panel && showing_wallet {
@@ -218,7 +217,7 @@ impl ContentContainer for WalletsContent {
top: 3.0 as i8,
bottom: 4.0 as i8,
},
fill: Colors::fill_deep(),
fill: Colors::fill(),
..Default::default()
})
.show_animated_inside(ui, !list_hidden, |ui| {
@@ -226,13 +225,23 @@ impl ContentContainer for WalletsContent {
ui.ctx().request_repaint_after(Duration::from_millis(1000));
}
// Show wallet list.
self.wallet_list_ui(ui);
self.wallet_list_ui(ui, cb);
});
egui::CentralPanel::default()
.frame(egui::Frame {
fill: if creating_wallet {
Colors::TRANSPARENT
inner_margin: if self.showing_settings() {
Margin {
left: (View::far_left_inset_margin(ui) + 4.0) as i8,
right: (View::far_right_inset_margin(ui) + 4.0) as i8,
top: 0,
bottom: 0,
}
} else {
Margin::default()
},
fill: if self.showing_settings() {
Colors::fill_lite()
} else {
Colors::fill_deep()
},
@@ -258,19 +267,19 @@ impl ContentContainer for WalletsContent {
});
}
} else if self.creating_wallet() {
// Show wallet creation content.
let mut created_wallet: Option<Wallet> = None;
let creation = self.creation_content.as_mut().unwrap();
let pass = creation.pass.clone();
let mut created = false;
// Show wallet creation content.
creation.content_ui(ui, cb, |wallet| {
self.wallets.add(wallet.clone());
if let Ok(_) = wallet.open(pass.clone()) {
self.wallet_content = Some(WalletContent::new(wallet, None));
}
created = true;
created_wallet = Some(wallet);
});
if created {
if let Some(w) = &created_wallet {
self.creation_content = None;
self.wallets.add(w.clone());
if let Ok(_) = w.open(pass.clone()) {
self.select_wallet(w, None, cb);
}
}
} else if self.wallets.list().is_empty() {
View::center_content(ui, 350.0 + View::get_bottom_inset(), |ui| {
@@ -311,20 +320,12 @@ impl WalletsContent {
}
return false;
} else if self.showing_wallet() {
let content = self.wallet_content.as_mut().unwrap();
// Close opened QR code scanner.
if content.qr_scan_showing() {
cb.stop_camera();
content.close_qr_scan();
return false;
// Go back at stack or close wallet.
if self.wallet_content.can_back() {
self.wallet_content.back(cb);
} else {
self.wallets.select(None);
}
// Close account list.
if content.account_list_showing() {
content.close_qr_scan();
return false;
}
// Close opened wallet.
self.wallet_content = None;
return false;
}
true
@@ -332,8 +333,7 @@ impl WalletsContent {
/// Check if opened wallet is showing.
pub fn showing_wallet(&self) -> bool {
if let Some(wallet_content) = &self.wallet_content {
let w = &wallet_content.wallet;
if let Some(w) = self.wallets.selected().as_ref() {
return w.is_open() && !w.is_deleted() &&
w.get_config().chain_type == AppConfig::chain_type();
}
@@ -351,7 +351,7 @@ impl WalletsContent {
}
/// Handle data from deeplink or opened file.
fn on_data(&mut self, ui: &mut egui::Ui, data: Option<String>) {
fn on_data(&mut self, ui: &mut egui::Ui, data: Option<String>, cb: &dyn PlatformCallbacks) {
let wallets_size = self.wallets.list().len();
if wallets_size == 0 {
return;
@@ -364,9 +364,9 @@ impl WalletsContent {
if wallets_size == 1 {
let w = self.wallets.list()[0].clone();
if w.is_open() {
self.wallet_content = Some(WalletContent::new(w, data));
self.select_wallet(&w, data, cb);
} else {
self.show_opening_modal(w, data);
self.show_opening_modal(&w, data, cb);
}
} else {
self.wallet_selection_content = WalletsModal::new(None, data, true);
@@ -387,69 +387,32 @@ impl WalletsContent {
}
/// Draw [`TitlePanel`] content.
fn title_ui(&mut self,
ui: &mut egui::Ui,
dual_panel: bool,
show_wallet: bool,
cb: &dyn PlatformCallbacks) {
let show_list = AppConfig::show_wallets_at_dual_panel();
fn title_ui(&mut self, ui: &mut egui::Ui, dual_panel: bool, cb: &dyn PlatformCallbacks) {
let showing_settings = self.showing_settings();
let show_wallet = self.showing_wallet();
let show_list = AppConfig::show_wallets_at_dual_panel();
let creating_wallet = self.creating_wallet();
let account_list_showing = show_wallet && self.wallet_content
.as_ref()
.unwrap()
.account_list_showing();
let qr_scan = {
let mut scan = false;
if show_wallet {
scan = self.wallet_content.as_mut().unwrap().qr_scan_showing();
}
scan
};
// Setup title.
let title_content = if show_wallet && (!dual_panel
|| (dual_panel && !show_list)) && !creating_wallet && !showing_settings {
let wallet_content = self.wallet_content.as_ref().unwrap();
let wallet_tab_type = wallet_content.current_tab.get_type();
let title_text = if account_list_showing {
t!("wallets.accounts")
} else if qr_scan {
t!("scan_qr")
} else {
wallet_tab_type.name()
};
if wallet_tab_type == WalletTabType::Settings {
TitleType::Single(TitleContentType::Title(title_text))
} else {
let subtitle_text = wallet_content.wallet.get_config().name;
TitleType::Single(TitleContentType::WithSubTitle(title_text, subtitle_text, false))
}
let title = self.wallet_content.title();
let subtitle = self.wallets.selected().unwrap().get_config().name;
TitleType::Single(TitleContentType::WithSubTitle(title, subtitle, false))
} else {
let title_text = if showing_settings {
t!("settings")
} else if qr_scan {
t!("scan_qr")
} else if creating_wallet {
t!("wallets.add")
} else {
t!("wallets.title")
};
let dual_title = !showing_settings && !qr_scan && !creating_wallet &&
let dual_title = !showing_settings && !creating_wallet &&
show_wallet && dual_panel;
if dual_title {
let wallet_content = self.wallet_content.as_ref().unwrap();
let wallet_tab_type = wallet_content.current_tab.get_type();
let wallet_title_text = if account_list_showing {
t!("wallets.accounts")
} else {
wallet_tab_type.name()
};
let wallet_title_content = if wallet_tab_type == WalletTabType::Settings {
TitleContentType::Title(wallet_title_text)
} else {
let subtitle_text = wallet_content.wallet.get_config().name;
TitleContentType::WithSubTitle(wallet_title_text, subtitle_text, false)
};
let title = self.wallet_content.title();
let subtitle = self.wallets.selected().unwrap().get_config().name;
let wallet_title_content = TitleContentType::WithSubTitle(title, subtitle, false);
TitleType::Dual(TitleContentType::Title(title_text), wallet_title_content)
} else {
TitleType::Single(TitleContentType::Title(title_text))
@@ -466,22 +429,11 @@ impl WalletsContent {
});
} else if show_wallet && !dual_panel {
View::title_button_big(ui, ARROW_LEFT, |_| {
// Close QR code scanner.
let wallet_qr_scan = self.wallet_content
.as_ref()
.unwrap()
.qr_scan_showing();
if wallet_qr_scan {
cb.stop_camera();
self.wallet_content.as_mut().unwrap().close_qr_scan();
return;
if self.wallet_content.can_back() {
self.wallet_content.back(cb);
} else {
self.wallets.select(None);
}
// Close account list.
if account_list_showing {
self.wallet_content.as_mut().unwrap().close_account_list();
return;
}
self.wallet_content = None;
});
} else if self.creating_wallet() {
let mut close = false;
@@ -496,21 +448,14 @@ impl WalletsContent {
self.creation_content = None;
}
} else if show_wallet && dual_panel {
if qr_scan {
View::title_button_big(ui, ARROW_LEFT, |_| {
cb.stop_camera();
self.wallet_content.as_mut().unwrap().close_qr_scan();
});
let list_icon = if show_list {
SIDEBAR_SIMPLE
} else {
let list_icon = if show_list {
SIDEBAR_SIMPLE
} else {
SUITCASE
};
View::title_button_big(ui, list_icon, |_| {
AppConfig::toggle_show_wallets_at_dual_panel();
});
}
SUITCASE
};
View::title_button_big(ui, list_icon, |_| {
AppConfig::toggle_show_wallets_at_dual_panel();
});
} else if !Content::is_dual_panel_mode(ui.ctx()) {
View::title_button_big(ui, GLOBE, |_| {
Content::toggle_network_panel();
@@ -530,7 +475,7 @@ impl WalletsContent {
}
/// Draw list of wallets.
fn wallet_list_ui(&mut self, ui: &mut egui::Ui) {
fn wallet_list_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ScrollArea::vertical()
.id_salt("wallet_list_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
@@ -542,20 +487,27 @@ impl WalletsContent {
ui.add_space(15.0);
let list = self.wallets.list().clone();
for w in &list {
for w in list.iter() {
let id = w.get_config().id;
// Remove deleted.
if w.is_deleted() {
self.wallet_content = None;
self.wallets.remove(w.get_config().id);
self.wallets.select(None);
self.wallets.remove(id);
ui.ctx().request_repaint();
continue;
}
// Check if wallet reopen is needed.
if w.reopen_needed() && !w.is_open() {
w.set_reopen(false);
self.show_opening_modal(w.clone(), None);
self.show_opening_modal(w, None, cb);
}
self.wallet_item_ui(ui, w);
// Check if wallet is selected.
let current = if let Some(selected) = self.wallets.selected().as_ref() {
selected.get_config().id == id
} else {
false
};
self.wallet_item_ui(ui, w, current, cb);
ui.add_space(5.0);
}
});
@@ -563,13 +515,12 @@ impl WalletsContent {
}
/// Draw wallet list item.
fn wallet_item_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
fn wallet_item_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
current: bool,
cb: &dyn PlatformCallbacks) {
let config = wallet.get_config();
let current = if let Some(content) = &self.wallet_content {
content.wallet.get_config().id == config.id && wallet.is_open()
} else {
false
};
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
@@ -586,12 +537,11 @@ impl WalletsContent {
if !wallet.is_open() {
// Show button to open closed wallet.
View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || {
self.show_opening_modal(wallet.clone(), None);
self.show_opening_modal(wallet, None, cb);
});
if !wallet.syncing() {
let mut show_selection = false;
View::item_button(ui, CornerRadius::default(), GLOBE, None, || {
self.wallet_content = Some(WalletContent::new(wallet.clone(), None));
self.select_wallet(wallet, None, cb);
self.conn_selection_content =
WalletConnectionModal::new(wallet.get_current_connection());
// Show connection selection modal.
@@ -599,17 +549,13 @@ impl WalletsContent {
.position(ModalPosition::CenterTop)
.title(t!("wallets.conn_method"))
.show();
show_selection = true;
});
if show_selection {
ExternalConnection::check(None, ui.ctx());
}
}
} else {
if !current {
// Show button to select opened wallet.
View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || {
self.wallet_content = Some(WalletContent::new(wallet.clone(), None));
self.select_wallet(wallet, None, cb);
});
}
// Show button to close opened wallet.
@@ -660,14 +606,23 @@ impl WalletsContent {
}
/// Show [`Modal`] to select and open wallet.
fn show_opening_modal(&mut self, wallet: Wallet, data: Option<String>) {
self.wallet_content = Some(WalletContent::new(wallet.clone(), None));
self.open_wallet_content = OpenWalletModal::new(data);
fn show_opening_modal(&mut self, wallet: &Wallet, data: Option<String>, cb: &dyn PlatformCallbacks) {
self.select_wallet(wallet, data, cb);
self.open_wallet_content = OpenWalletModal::new();
Modal::new(OPEN_WALLET_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.open"))
.show();
}
/// Select wallet to make some actions on it.
fn select_wallet(&mut self, wallet: &Wallet, data: Option<String>, cb: &dyn PlatformCallbacks) {
self.wallet_content.account_content.close_qr_scan(cb);
if let Some(data) = data {
wallet.open_slatepack(data);
}
self.wallets.select(Some(wallet.get_config().id));
}
}
/// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time.
+6 -5
View File
@@ -19,7 +19,7 @@ use grin_util::ZeroingString;
use crate::gui::Colors;
use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, SCAN};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, View, CameraScanModal};
use crate::gui::views::{Modal, Content, View, CameraScanContent};
use crate::gui::views::types::{LinePosition, ContentContainer, ModalPosition, QrScanResult};
use crate::gui::views::wallets::creation::MnemonicSetup;
use crate::gui::views::wallets::creation::types::Step;
@@ -39,7 +39,7 @@ pub struct WalletCreationContent {
step: Step,
/// QR code scanning [`Modal`] content.
scan_modal_content: Option<CameraScanModal>,
scan_modal_content: Option<CameraScanContent>,
/// Mnemonic phrase setup content.
mnemonic_setup: MnemonicSetup,
@@ -66,7 +66,7 @@ impl ContentContainer for WalletCreationContent {
match modal.id {
QR_CODE_PHRASE_SCAN_MODAL => {
if let Some(content) = self.scan_modal_content.as_mut() {
content.ui(ui, cb, |result| {
content.modal_ui(ui, cb, |result| {
match result {
QrScanResult::Text(text) => {
self.mnemonic_setup.mnemonic.import(&text);
@@ -118,7 +118,7 @@ impl WalletCreationContent {
top: View::TAB_ITEMS_PADDING as i8,
bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8,
},
fill: Colors::fill_deep(),
fill: Colors::fill(),
..Default::default()
})
.show_inside(ui, |ui| {
@@ -146,6 +146,7 @@ impl WalletCreationContent {
top: 3.0 as i8,
bottom: 4.0 as i8,
},
fill: Colors::fill_lite(),
..Default::default()
})
.show_inside(ui, |ui| {
@@ -260,7 +261,7 @@ impl WalletCreationContent {
SCAN,
t!("scan").to_uppercase());
View::button(ui, scan_text, Colors::white_or_black(false), || {
self.scan_modal_content = Some(CameraScanModal::default());
self.scan_modal_content = Some(CameraScanContent::default());
// Show QR code scan modal.
Modal::new(QR_CODE_PHRASE_SCAN_MODAL)
.position(ModalPosition::CenterTop)
+3 -7
View File
@@ -25,18 +25,14 @@ pub struct OpenWalletModal {
pass_edit: String,
/// Flag to check if wrong password was entered.
wrong_pass: bool,
/// Optional data to pass after wallet opening.
data: Option<String>,
}
impl OpenWalletModal {
/// Create new content instance.
pub fn new(data: Option<String>) -> Self {
pub fn new() -> Self {
Self {
pass_edit: "".to_string(),
wrong_pass: false,
data,
}
}
/// Draw [`Modal`] content.
@@ -44,14 +40,14 @@ impl OpenWalletModal {
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
mut on_continue: impl FnMut(ZeroingString, &Option<String>) -> bool) {
mut on_continue: impl FnMut(ZeroingString) -> bool) {
// Callback for button to continue.
let mut on_continue = |m: &mut OpenWalletModal| {
let pass = m.pass_edit.clone();
if pass.is_empty() {
return;
}
m.wrong_pass = !on_continue(ZeroingString::from(pass), &m.data);
m.wrong_pass = !on_continue(ZeroingString::from(pass));
if !m.wrong_pass {
m.pass_edit = "".to_string();
Modal::close();
+103 -71
View File
@@ -7,36 +7,35 @@
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// distributed under the License is distr1ibuted on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use eframe::emath::Align;
use eframe::epaint::StrokeKind;
use egui::{Layout, RichText};
use egui::{Align, Layout, RichText, StrokeKind};
use grin_core::core::amount_to_hr_string;
use crate::gui::icons::{FOLDER_USER, PACKAGE, SCAN, SPINNER, USERS_THREE};
use crate::gui::icons::{FOLDER_USER, PACKAGE, SCAN, SPINNER, USERS_THREE, USER_PLUS};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{ModalPosition, QrScanResult};
use crate::gui::views::wallets::wallet::account::create::CreateAccountContent;
use crate::gui::views::wallets::wallet::account::list::WalletAccountsContent;
use crate::gui::views::wallets::wallet::types::{WalletContentContainer, GRIN};
use crate::gui::views::{CameraContent, Content, Modal, View};
use crate::gui::views::{CameraContent, CameraScanContent, Content, Modal, View};
use crate::gui::Colors;
use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::wallet::account::create::CreateAccountContent;
use crate::wallet::{Wallet, WalletConfig};
/// Wallet account panel content.
pub struct AccountContent {
/// Account list content.
accounts_content: Option<WalletAccountsContent>,
pub list_content: Option<WalletAccountsContent>,
/// Account creation [`Modal`] content.
create_account_content: CreateAccountContent,
/// QR code scan content.
qr_scan_content: Option<CameraContent>,
/// QR code scan result
qr_scan_result: Option<QrScanResult>,
}
/// Account creation [`Modal`] identifier.
@@ -49,7 +48,11 @@ impl WalletContentContainer for AccountContent {
]
}
fn modal_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal, cb: &dyn PlatformCallbacks) {
fn modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
CREATE_MODAL_ID => self.create_account_content.ui(ui, wallet, modal, cb),
_ => {}
@@ -57,13 +60,14 @@ impl WalletContentContainer for AccountContent {
}
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
if self.qr_scan_content.is_some() {
if self.qr_scan_showing() {
self.qr_scan_ui(ui, wallet, cb);
} else {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
if self.accounts_content.is_some() {
if self.list_content.is_some() {
self.list_ui(ui, wallet);
} else {
// Show account content.
self.account_ui(ui, wallet, cb);
}
});
@@ -74,14 +78,44 @@ impl WalletContentContainer for AccountContent {
impl Default for AccountContent {
fn default() -> Self {
Self {
accounts_content: None,
list_content: None,
create_account_content: CreateAccountContent::default(),
qr_scan_content: None,
qr_scan_result: None,
}
}
}
impl AccountContent {
/// Check if QR code scanner was opened.
pub fn qr_scan_showing(&self) -> bool {
self.qr_scan_content.is_some() || self.qr_scan_result.is_some()
}
/// Close QR code scanner.
pub fn close_qr_scan(&mut self, cb: &dyn PlatformCallbacks) {
if !self.qr_scan_showing() {
return;
}
cb.stop_camera();
self.qr_scan_content = None;
self.qr_scan_result = None;
}
/// Check if it's possible to go back at navigation stack.
pub fn can_back(&self) -> bool {
self.qr_scan_showing() || self.list_content.is_some()
}
/// Navigate back on navigation stack.
pub fn back(&mut self, cb: &dyn PlatformCallbacks) {
if self.qr_scan_showing() {
self.close_qr_scan(cb);
} else if self.list_content.is_some() {
self.list_content = None;
}
}
/// Draw wallet account content.
fn account_ui(&mut self,
ui: &mut egui::Ui,
@@ -91,17 +125,19 @@ impl AccountContent {
if wallet.get_data().is_none() {
return;
}
let data = wallet.get_data().unwrap();
let mut rect = ui.available_rect_before_wrap();
rect.set_height(75.0);
// Draw round background.
let rounding = View::item_rounding(0, 2, false);
ui.painter().rect(rect,
rounding,
Colors::fill_lite(),
Colors::fill(),
View::item_stroke(),
StrokeKind::Middle);
StrokeKind::Outside);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to show QR code scanner.
@@ -111,8 +147,13 @@ impl AccountContent {
});
// Draw button to show list of accounts.
View::item_button(ui, View::item_rounding(1, 3, true), USERS_THREE, None, || {
let accounts = wallet.accounts();
let accounts = wallet.accounts();
let accounts_icon = if accounts.len() > 1 {
USERS_THREE
} else {
USER_PLUS
};
View::item_button(ui, View::item_rounding(1, 3, true), accounts_icon, None, || {
if accounts.len() == 1 {
self.create_account_content = CreateAccountContent::default();
Modal::new(CREATE_MODAL_ID)
@@ -120,7 +161,7 @@ impl AccountContent {
.title(t!("wallets.accounts"))
.show();
} else {
self.accounts_content = Some(
self.list_content = Some(
WalletAccountsContent::new(accounts, wallet.get_config().account)
);
}
@@ -154,7 +195,9 @@ impl AccountContent {
View::ellipsize_text(ui, acc_text, 15.0, Colors::text(false));
// Show confirmed height or sync progress.
let status_text = if !wallet.syncing() {
let status_text = if wallet.message_opening() {
format!("{} {}", SPINNER, t!("wallets.loading"))
} else if !wallet.syncing() {
format!("{} {}", PACKAGE, data.info.last_confirmed_height)
} else {
let info_progress = wallet.info_sync_progress();
@@ -183,7 +226,7 @@ impl AccountContent {
status_text,
15.0,
Colors::gray(),
wallet.syncing());
wallet.syncing() || wallet.message_opening());
})
});
});
@@ -191,14 +234,14 @@ impl AccountContent {
/// Draw account list content.
fn list_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
if let Some(accounts) = self.accounts_content.as_mut() {
if let Some(accounts) = self.list_content.as_mut() {
let mut selected = false;
accounts.ui(ui, |acc| {
let _ = wallet.set_active_account(&acc.label);
selected = true;
});
if selected {
self.accounts_content = None;
self.list_content = None;
return;
}
} else {
@@ -216,12 +259,12 @@ impl AccountContent {
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.accounts_content = None;
self.list_content = None;
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.add"), Colors::white_or_black(false), || {
self.accounts_content = None;
self.list_content = None;
self.create_account_content = CreateAccountContent::default();
Modal::new(CREATE_MODAL_ID)
.position(ModalPosition::CenterTop)
@@ -234,53 +277,42 @@ impl AccountContent {
}
/// Draw QR code scanner content.
fn qr_scan_ui(&mut self, ui: &mut egui::Ui, _: &Wallet, cb: &dyn PlatformCallbacks) {
if let Some(_) = self.qr_scan_content.as_ref().unwrap().qr_scan_result() {
// match result {
// QrScanResult::Address(address) => {
// self.current_tab =
// Box::new(WalletTransport::new(Some(address.to_string())));
// }
// _ => {
// self.current_tab =
// Box::new(WalletMessages::new(Some(result.text())))
// }
// }
// Stop camera and close scanning.
cb.stop_camera();
self.qr_scan_content = None;
} else {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH, |ui| {
self.qr_scan_content.as_mut().unwrap().ui(ui, cb);
ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
cb.stop_camera();
self.qr_scan_content = None;
fn qr_scan_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH, |ui| {
if self.qr_scan_content.is_some() {
if let Some(result) = self.qr_scan_content.as_ref().unwrap().qr_scan_result() {
cb.stop_camera();
self.qr_scan_content = None;
match result {
QrScanResult::Address(_) => {
//TODO: send with address
}
QrScanResult::Slatepack(m) => {
wallet.open_slatepack(m.to_string());
}
_ => {
self.qr_scan_result = Some(result);
}
}
} else {
// Draw QR code scan content.
self.qr_scan_content.as_mut().unwrap().ui(ui, cb);
ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.close_qr_scan(cb);
});
});
}
} else if let Some(res) = &self.qr_scan_result.clone() {
CameraScanContent::result_ui(ui, res, cb, || {
self.qr_scan_result = None;
}, || {
self.qr_scan_content = Some(CameraContent::default());
cb.start_camera();
});
ui.add_space(6.0);
});
}
}
/// Check if account list is showing.
pub fn account_list_showing(&self) -> bool {
self.accounts_content.is_some()
}
/// Close account list.
pub fn close_account_list(&mut self) {
self.accounts_content = None;
}
/// Check if QR code scanner is showing.
pub fn qr_scan_showing(&self) -> bool {
self.qr_scan_content.is_some()
}
/// Close QR code scanner.
pub fn close_qr_scan(&mut self) {
self.qr_scan_content = None;
}
ui.add_space(6.0);
});
}
}
+9 -10
View File
@@ -45,14 +45,10 @@ impl WalletAccountsContent {
ScrollArea::vertical()
.id_salt("account_list_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(266.0)
.max_height(411.0)
.auto_shrink([true; 2])
.show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| {
for index in row_range {
// Add space before the first item.
if index == 0 {
ui.add_space(4.0);
}
let acc = self.accounts.get(index).unwrap().clone();
self.account_item_ui(ui, &acc, index, size, || {
on_select(acc.clone());
@@ -100,15 +96,18 @@ impl WalletAccountsContent {
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0);
ui.add_space(8.0);
ui.vertical(|ui| {
ui.add_space(4.0);
ui.add_space(3.0);
// Show spendable amount.
let amount = amount_to_hr_string(acc.spendable_amount, true);
let amount_text = format!("{} {}", amount, GRIN);
ui.label(RichText::new(amount_text)
.size(18.0)
.color(Colors::white_or_black(true)));
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(RichText::new(amount_text)
.size(18.0)
.color(Colors::white_or_black(true)));
});
ui.add_space(-2.0);
// Show account name.
+252 -115
View File
@@ -16,95 +16,101 @@ use egui::scroll_area::ScrollBarVisibility;
use egui::{Id, Margin, RichText, ScrollArea};
use grin_chain::SyncStatus;
use std::time::Duration;
use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CHAT_CIRCLE_TEXT, GEAR_FINE, GRAPH, POWER};
use grin_wallet_libwallet::Error;
use crate::gui::icons::{ARROWS_CLOCKWISE, FILE_ARROW_DOWN, FILE_ARROW_UP, GEAR_FINE, POWER, STACK};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{ContentContainer, LinePosition};
use crate::gui::views::types::{LinePosition, ModalPosition};
use crate::gui::views::wallets::types::{WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::account::AccountContent;
use crate::gui::views::wallets::wallet::request::{InvoiceRequestContent, SendRequestContent};
use crate::gui::views::wallets::wallet::transport::WalletTransportContent;
use crate::gui::views::wallets::wallet::types::WalletContentContainer;
use crate::gui::views::wallets::wallet::WalletSettings;
use crate::gui::views::wallets::{WalletMessages, WalletTransactions, WalletTransport};
use crate::gui::views::{Content, Modal, View};
use crate::gui::views::wallets::WalletTransactions;
use crate::gui::views::{Content, FilePickContent, FilePickContentType, Modal, View};
use crate::gui::Colors;
use crate::node::Node;
use crate::wallet::types::ConnectionMethod;
use crate::wallet::types::{ConnectionMethod, WalletTransaction};
use crate::wallet::{ExternalConnection, Wallet};
use crate::AppConfig;
use crate::gui::views::wallets::wallet::types::WalletContentContainer;
/// Wallet content.
pub struct WalletContent {
/// Selected and opened wallet.
pub wallet: Wallet,
/// Current tab content to show.
pub current_tab: Box<dyn WalletTab>,
current_tab: Box<dyn WalletTab>,
/// Account panel content.
account_content: AccountContent,
pub account_content: AccountContent,
/// Transport panel content.
pub transport_content: WalletTransportContent,
/// Invoice request creation [`Modal`] content.
invoice_request_content: Option<InvoiceRequestContent>,
/// Send request creation [`Modal`] content.
send_request_content: Option<SendRequestContent>,
/// Tab button to pick file for parsing.
file_pick_tab_button: FilePickContent,
}
impl ContentContainer for WalletContent {
/// Identifier for invoice creation [`Modal`].
const INVOICE_MODAL_ID: &'static str = "invoice_request_modal";
/// Identifier for sending request creation [`Modal`].
const SEND_MODAL_ID: &'static str = "send_request_modal";
impl WalletContentContainer for WalletContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![]
vec![
INVOICE_MODAL_ID,
SEND_MODAL_ID
]
}
fn modal_ui(&mut self, _: &mut egui::Ui, _: &Modal, _: &dyn PlatformCallbacks) {
fn modal_ui(&mut self, ui: &mut egui::Ui, w: &Wallet, m: &Modal, cb: &dyn PlatformCallbacks) {
match m.id {
INVOICE_MODAL_ID => {
if let Some(c) = self.invoice_request_content.as_mut() {
c.modal_ui(ui, w, m, cb);
}
}
SEND_MODAL_ID => {
if let Some(c) = self.send_request_content.as_mut() {
c.modal_ui(ui, w, m, cb);
}
}
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.ctx().request_repaint_after(Duration::from_millis(1000));
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
if self.account_content.can_back() {
ui.ctx().request_repaint();
} else {
ui.ctx().request_repaint_after(Duration::from_millis(1000));
}
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
let show_wallets_dual = AppConfig::show_wallets_at_dual_panel();
let wallet = &self.wallet;
let wallet_id = wallet.identifier();
let data = wallet.get_data();
let show_qr_scan = self.account_content.qr_scan_showing();
let block_nav = Self::block_navigation_on_sync(wallet);
let block_nav = self.block_navigation_on_sync(wallet);
// Show wallet account panel not on settings tab when navigation is not blocked and QR code
// scanner is not showing and wallet data is not empty.
let mut show_account = self.current_tab.get_type() != WalletTabType::Settings && !block_nav
&& !wallet.sync_error() && data.is_some();
if wallet.get_current_connection() == ConnectionMethod::Integrated && !Node::is_running() {
if wallet.get_current_connection() == ConnectionMethod::Integrated &&
!Node::is_running() {
show_account = false;
}
// Close scanner when balance got hidden.
if !show_account && show_qr_scan {
cb.stop_camera();
self.account_content.close_qr_scan();
// Consume inserted message.
if let Some(res) = wallet.consume_message_result() {
self.on_transaction(res);
}
egui::TopBottomPanel::top(Id::from("wallet_account").with(wallet.identifier()))
.frame(egui::Frame {
inner_margin: Margin {
left: (View::far_left_inset_margin(ui) + 4.0) as i8,
right: (View::get_right_inset() + 4.0) as i8,
top: 4.0 as i8,
bottom: 0.0 as i8,
},
fill: Colors::fill(),
..Default::default()
})
.show_animated_inside(ui, show_account, |ui| {
let rect = ui.available_rect_before_wrap();
self.account_content.ui(ui, wallet, cb);
// Draw content divider lines.
let r = {
let mut r = rect.clone();
r.min.x -= 4.0 + View::far_left_inset_margin(ui);
r.min.y -= 4.0;
r.max.x += 4.0 + View::get_right_inset();
r
};
View::line(ui, LinePosition::BOTTOM, &r, Colors::item_stroke());
if dual_panel && show_wallets_dual && !show_qr_scan {
View::line(ui, LinePosition::LEFT, &r, Colors::item_stroke());
}
});
// Show wallet tabs.
let show_tabs = !block_nav && !self.qr_scan_showing() && !self.account_list_showing();
egui::TopBottomPanel::bottom("wallet_tabs")
.frame(egui::Frame {
inner_margin: Margin {
@@ -116,55 +122,146 @@ impl ContentContainer for WalletContent {
fill: Colors::fill(),
..Default::default()
})
.show_animated_inside(ui, show_tabs, |ui| {
let rect = ui.available_rect_before_wrap();
.show_animated_inside(ui, !block_nav, |ui| {
let r = ui.available_rect_before_wrap();
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tabs_ui(ui);
self.tabs_ui(wallet, ui, cb);
});
let rect = {
let mut r = rect.clone();
let mut r = r.clone();
r.min.x -= View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING;
r.min.y -= View::TAB_ITEMS_PADDING;
r.max.x += View::get_right_inset() + View::TAB_ITEMS_PADDING;
r.max.y += View::get_bottom_inset() + View::TAB_ITEMS_PADDING;
r
};
// Draw content divider line.
View::line(ui, LinePosition::TOP, &rect, Colors::stroke());
// Draw cover for content below opened panel.
if self.can_back() && show_account {
View::content_cover_ui(ui, rect, "wallet_tabs_content_cover", || {
self.back(cb);
});
} else {
// Draw content divider line.
View::line(ui, LinePosition::TOP, &rect, Colors::stroke());
}
});
// Close scanner when account panel got hidden.
if !show_account && self.account_content.qr_scan_showing() {
self.account_content.close_qr_scan(cb);
}
// Flag to check if account panel is opened.
let top_panel_expanded = self.account_content.can_back() ||
self.transport_content.can_back();
// Show wallet account content.
if !self.transport_content.can_back() && show_account {
egui::TopBottomPanel::top(Id::from("wallet_account").with(wallet.identifier()))
.frame(egui::Frame {
inner_margin: Margin {
left: (View::far_left_inset_margin(ui) + 4.0) as i8,
right: (View::get_right_inset() + 4.0) as i8,
top: 4.0 as i8,
bottom: 0.0 as i8,
},
fill: if top_panel_expanded {
Colors::fill_lite()
} else {
Colors::TRANSPARENT
},
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
self.account_content.ui(ui, &wallet, cb);
// Draw content divider lines.
let r = {
let mut r = rect.clone();
r.min.x -= 4.0 + View::far_left_inset_margin(ui);
r.min.y -= 4.0;
r.max.x += 4.0 + View::get_right_inset();
r
};
if dual_panel && show_wallets_dual {
View::line(ui, LinePosition::LEFT, &r, Colors::item_stroke());
}
});
}
// Show wallet transport content.
if !self.account_content.can_back() && show_account {
egui::TopBottomPanel::top(Id::from("wallet_transport").with(wallet.identifier()))
.frame(egui::Frame {
inner_margin: Margin {
left: (View::far_left_inset_margin(ui) + 4.0) as i8,
right: (View::get_right_inset() + 4.0) as i8,
top: 1.0 as i8,
bottom: 1.0 as i8,
},
fill: if top_panel_expanded {
Colors::fill_lite()
} else {
Colors::TRANSPARENT
},
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.transport_content.ui(ui, &wallet, cb);
});
// Draw content divider lines.
let r = {
let mut r = rect.clone();
r.min.x -= 4.0 + View::far_left_inset_margin(ui);
r.min.y -= 1.0;
r.max.x += 4.0 + View::get_right_inset();
r
};
if dual_panel && show_wallets_dual {
View::line(ui, LinePosition::LEFT, &r, Colors::item_stroke());
}
});
}
// Show tab content.
egui::CentralPanel::default()
.frame(egui::Frame {
inner_margin: Margin {
inner_margin: Margin {
left: (View::far_left_inset_margin(ui) + 4.0) as i8,
right: (View::get_right_inset() + 4.0) as i8,
top: 0.0 as i8,
bottom: 4.0 as i8,
},
fill: if self.current_tab.get_type() == WalletTabType::Settings {
Colors::fill_lite()
} else {
Colors::TRANSPARENT
},
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
let tab_type = self.current_tab.get_type();
let show_sync = (tab_type != WalletTabType::Settings || block_nav) &&
sync_ui(ui, &self.wallet);
sync_ui(ui, &wallet);
if !show_sync {
if tab_type != WalletTabType::Txs {
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(Id::from("wallet_scroll")
.id_salt(Id::from("wallet_tab_content_scroll")
.with(tab_type.name())
.with(wallet_id))
.auto_shrink([false; 2])
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.show(ui, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.current_tab.ui(ui, &self.wallet, cb);
self.current_tab.ui(ui, &wallet, cb);
});
});
} else {
self.current_tab.ui(ui, &self.wallet, cb);
self.current_tab.ui(ui, &wallet, cb);
}
}
let rect = {
@@ -174,12 +271,10 @@ impl ContentContainer for WalletContent {
r.max.y += 4.0;
r
};
// Draw cover when account list or QR code scanner is active.
if !show_sync && (self.account_list_showing() || self.qr_scan_showing()) {
View::content_cover_ui(ui, rect, "wallet_content_cover", || {
cb.stop_camera();
self.account_content.close_qr_scan();
self.account_content.close_account_list();
// Draw cover for content below opened panel.
if !show_sync && self.can_back() {
View::content_cover_ui(ui, rect, "wallet_panel_content_cover", || {
self.back(cb);
});
}
// Draw content divider line.
@@ -190,48 +285,58 @@ impl ContentContainer for WalletContent {
}
}
impl WalletContent {
/// Create new instance with optional data.
pub fn new(wallet: Wallet, data: Option<String>) -> Self {
let account_content = AccountContent::default();
let mut content = Self {
wallet,
current_tab: Box::new(WalletTransactions::default()),
account_content,
};
if data.is_some() {
content.on_data(data);
impl Default for WalletContent {
fn default() -> Self {
Self {
current_tab: Box::new(WalletTransactions::new(None)),
account_content: AccountContent::default(),
transport_content: WalletTransportContent::default(),
invoice_request_content: None,
send_request_content: None,
file_pick_tab_button: FilePickContent::new(FilePickContentType::Tab),
}
}
}
impl WalletContent {
/// Get title based on current navigation state.
pub fn title(&self) -> String {
if self.account_content.qr_scan_showing() {
t!("scan_qr")
} else if self.account_content.list_content.is_some() {
t!("wallets.accounts")
} else if self.transport_content.settings_content.is_some() {
t!("wallets.transport")
} else if self.transport_content.qr_address_content.is_some() {
t!("network_mining.address")
} else {
self.current_tab.get_type().name()
}
content
}
/// Check account list is showing.
pub fn account_list_showing(&self) -> bool {
self.account_content.account_list_showing()
/// Callback on incoming transaction for user to take action.
fn on_transaction(&mut self, tx_result: Result<WalletTransaction, Error>) {
if let Ok(tx) = tx_result {
self.current_tab = Box::new(WalletTransactions::new(Some(tx)));
}
}
/// Close QR code scanner.
pub fn close_account_list(&mut self) {
self.account_content.close_account_list();
/// Check if it's possible to go back at navigation stack.
pub fn can_back(&self) -> bool {
self.account_content.can_back() || self.transport_content.can_back()
}
/// Check if QR code scanner is opened.
pub fn qr_scan_showing(&self) -> bool {
self.account_content.qr_scan_showing()
}
/// Close QR code scanner.
pub fn close_qr_scan(&mut self) {
self.account_content.close_qr_scan();
}
/// Handle data from deeplink or opened file.
pub fn on_data(&mut self, data: Option<String>) {
self.current_tab = Box::new(WalletMessages::new(data));
/// Navigate back on navigation stack.
pub fn back(&mut self, cb: &dyn PlatformCallbacks) {
if self.account_content.can_back() {
self.account_content.back(cb);
} else if self.transport_content.can_back() {
self.transport_content.back();
}
}
/// Check when to block tabs navigation on sync progress.
pub fn block_navigation_on_sync(wallet: &Wallet) -> bool {
fn block_navigation_on_sync(&self, wallet: &Wallet) -> bool {
let sync_error = wallet.sync_error();
let integrated_node = wallet.get_current_connection() == ConnectionMethod::Integrated;
let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
@@ -244,7 +349,7 @@ impl WalletContent {
}
/// Draw tab buttons at the bottom of the screen.
fn tabs_ui(&mut self, ui: &mut egui::Ui) {
fn tabs_ui(&mut self, wallet: &Wallet, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.scope(|ui| {
// Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
@@ -252,28 +357,60 @@ impl WalletContent {
// Setup vertical padding inside buttons.
ui.style_mut().spacing.button_padding = egui::vec2(0.0, 4.0);
let has_wallet_data = wallet.get_data().is_some();
let can_send = has_wallet_data &&
wallet.get_data().unwrap().info.amount_currently_spendable > 0;
let current_type = self.current_tab.get_type();
ui.columns(4, |columns| {
let tabs_amount = if can_send { 5 } else { 4 };
ui.columns(tabs_amount, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::tab_button(ui, GRAPH, current_type == WalletTabType::Txs, |_| {
self.current_tab = Box::new(WalletTransactions::default());
View::tab_button(ui, STACK, None, Some(current_type == WalletTabType::Txs), |_| {
self.current_tab = Box::new(WalletTransactions::new(None));
});
});
columns[1].vertical_centered_justified(|ui| {
let is_messages = current_type == WalletTabType::Messages;
View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, |_| {
self.current_tab = Box::new(
WalletMessages::new(None)
);
let active = if has_wallet_data {
Some(false)
} else {
None
};
View::tab_button(ui, FILE_ARROW_DOWN, Some(Colors::green()), active, |_| {
self.invoice_request_content = Some(InvoiceRequestContent::default());
Modal::new(INVOICE_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.receive"))
.show();
});
});
columns[2].vertical_centered_justified(|ui| {
View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, |_| {
self.current_tab = Box::new(WalletTransport::new(None));
});
if wallet.message_opening() {
View::small_loading_spinner(ui);
} else {
let mut message = "".to_string();
self.file_pick_tab_button.ui(ui, cb, |m| {
message = m;
});
if !message.is_empty() {
wallet.open_slatepack(message);
}
}
});
columns[3].vertical_centered_justified(|ui| {
View::tab_button(ui, GEAR_FINE, current_type == WalletTabType::Settings, |ui| {
if can_send {
columns[3].vertical_centered_justified(|ui| {
View::tab_button(ui, FILE_ARROW_UP, Some(Colors::red()), Some(false), |_| {
self.send_request_content = Some(SendRequestContent::new(None));
Modal::new(SEND_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.send"))
.show();
});
});
}
let settings_index = if tabs_amount == 5 { 4 } else { 3 };
columns[settings_index].vertical_centered_justified(|ui| {
let active = Some(current_type == WalletTabType::Settings);
View::tab_button(ui, GEAR_FINE, None, active, |ui| {
ExternalConnection::check(None, ui.ctx());
self.current_tab = Box::new(WalletSettings::default());
});
@@ -23,11 +23,11 @@ use parking_lot::RwLock;
use crate::gui::Colors;
use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, DOWNLOAD_SIMPLE, SCAN, UPLOAD_SIMPLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{FilePickButton, Modal, View, CameraScanModal};
use crate::gui::views::{FilePickContent, Modal, View, CameraScanContent, FilePickContentType};
use crate::gui::views::types::{ModalPosition, QrScanResult};
use crate::gui::views::wallets::wallet::messages::request::MessageRequestModal;
use crate::gui::views::wallets::wallet::messages::request::RequestModalContent;
use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::WalletTransactionModal;
use crate::gui::views::wallets::wallet::WalletTransactionContent;
use crate::wallet::types::WalletTransaction;
use crate::wallet::Wallet;
@@ -37,10 +37,7 @@ pub struct WalletMessages {
first_draw: bool,
/// Invoice or sending request creation [`Modal`] content.
request_modal_content: MessageRequestModal,
/// Wallet transaction [`Modal`] content.
tx_info_content: WalletTransactionModal,
request_modal_content: RequestModalContent,
/// Slatepacks message input text.
message_edit: String,
@@ -52,10 +49,10 @@ pub struct WalletMessages {
message_result: Arc<RwLock<Option<(Slate, Result<WalletTransaction, Error>)>>>,
/// QR code scanner [`Modal`] content.
scan_modal_content: Option<CameraScanModal>,
scan_modal_content: Option<CameraScanContent>,
/// Button to parse picked file content.
file_pick_button: FilePickButton,
file_pick_button: FilePickContent,
}
/// Identifier for amount input [`Modal`] to create invoice or sending request.
@@ -65,17 +62,6 @@ const TX_INFO_MODAL: &'static str = "messages_tx_info_modal";
/// Identifier for [`Modal`] to scan Slatepack message from QR code.
const SCAN_QR_MODAL: &'static str = "messages_scan_qr_modal";
impl WalletTab for WalletMessages {
fn get_type(&self) -> WalletTabType {
WalletTabType::Messages
}
fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
self.modal_content_ui(ui, wallet, cb);
self.messages_ui(ui, wallet, cb);
}
}
impl WalletMessages {
/// Create new content instance, put message into input if provided.
pub fn new(message: Option<String>) -> Self {
@@ -85,9 +71,8 @@ impl WalletMessages {
message_loading: false,
message_error: "".to_string(),
message_result: Arc::new(Default::default()),
tx_info_content: WalletTransactionModal::new(None, false),
request_modal_content: MessageRequestModal::new(false),
file_pick_button: FilePickButton::default(),
request_modal_content: RequestModalContent::new(false),
file_pick_button: FilePickContent::new(FilePickContentType::Button),
scan_modal_content: None,
}
}
@@ -133,16 +118,11 @@ impl WalletMessages {
self.request_modal_content.ui(ui, wallet, modal, cb);
});
}
TX_INFO_MODAL => {
Modal::ui(ui.ctx(), cb, |ui, modal, cb| {
self.tx_info_content.ui(ui, wallet, modal, cb);
});
}
SCAN_QR_MODAL => {
let mut result = None;
if let Some(content) = self.scan_modal_content.as_mut() {
Modal::ui(ui.ctx(), cb, |ui, _, cb| {
content.ui(ui, cb, |res| {
content.modal_ui(ui, cb, |res| {
result = Some(res.clone());
Modal::close();
});
@@ -213,7 +193,7 @@ impl WalletMessages {
/// Show [`Modal`] to create invoice or sending request.
fn show_request_modal(&mut self, invoice: bool) {
self.request_modal_content = MessageRequestModal::new(invoice);
self.request_modal_content = RequestModalContent::new(invoice);
let title = if invoice {
t!("wallets.receive")
} else {
@@ -295,7 +275,7 @@ impl WalletMessages {
Ok(tx) => {
self.message_edit.clear();
// Show transaction modal on success.
self.tx_info_content = WalletTransactionModal::new(Some(tx.data.id), false);
// self.tx_info_content = WalletTransactionContent::new(Some(tx.data.id), false);
Modal::new(TX_INFO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.tx"))
@@ -318,8 +298,8 @@ impl WalletMessages {
// Show tx modal or show default error message.
if let Some(tx) = wallet.tx_by_slate(&slate).as_ref() {
self.message_edit.clear();
self.tx_info_content =
WalletTransactionModal::new(Some(tx.data.id), false);
// self.tx_info_content =
// WalletTransactionContent::new(Some(tx.data.id), false);
Modal::new(TX_INFO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.tx"))
@@ -352,7 +332,7 @@ impl WalletMessages {
View::button(ui, scan_text, Colors::white_or_black(false), || {
self.message_edit.clear();
self.message_error.clear();
self.scan_modal_content = Some(CameraScanModal::default());
self.scan_modal_content = Some(CameraScanContent::default());
// Show QR code scan modal.
Modal::new(SCAN_QR_MODAL)
.position(ModalPosition::CenterTop)
@@ -422,7 +402,7 @@ impl WalletMessages {
if exists {
if let Some(tx) = wallet.tx_by_slate(&slate).as_ref() {
self.message_edit.clear();
self.tx_info_content = WalletTransactionModal::new(Some(tx.data.id), false);
// self.tx_info_content = WalletTransactionContent::new(Some(tx.data.id), false);
Modal::new(TX_INFO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.tx"))
@@ -22,12 +22,12 @@ use grin_wallet_libwallet::Error;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::views::wallets::wallet::WalletTransactionModal;
use crate::gui::views::wallets::wallet::WalletTransactionContent;
use crate::wallet::types::WalletTransaction;
use crate::wallet::Wallet;
/// Invoice or sending request creation [`Modal`] content.
pub struct MessageRequestModal {
pub struct RequestModalContent {
/// Flag to check if invoice or sending request was opened.
invoice: bool,
@@ -42,10 +42,10 @@ pub struct MessageRequestModal {
request_error: Option<String>,
/// Request result transaction content.
result_tx_content: Option<WalletTransactionModal>,
result_tx_content: Option<WalletTransactionContent>,
}
impl MessageRequestModal {
impl RequestModalContent {
/// Create new content instance.
pub fn new(invoice: bool) -> Self {
Self {
@@ -71,7 +71,7 @@ impl MessageRequestModal {
}
// Setup callback on continue.
let on_continue = |m: &mut MessageRequestModal| {
let on_continue = |m: &mut RequestModalContent| {
if m.amount_edit.is_empty() {
return;
}
@@ -230,7 +230,7 @@ impl MessageRequestModal {
match result {
Ok(tx) => {
self.result_tx_content =
Some(WalletTransactionModal::new(Some(tx.data.id), false));
Some(WalletTransactionContent::new(tx, false));
}
Err(err) => {
match err {
+3 -7
View File
@@ -20,13 +20,9 @@ pub use settings::*;
mod txs;
pub use txs::*;
mod messages;
pub use messages::WalletMessages;
mod transport;
pub use transport::WalletTransport;
mod content;
pub use content::WalletContent;
mod account;
mod account;
mod transport;
mod request;
@@ -0,0 +1,214 @@
// Copyright 2025 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Id, RichText};
use grin_core::core::amount_from_hr_string;
use grin_wallet_libwallet::Error;
use parking_lot::RwLock;
use std::sync::Arc;
use std::thread;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::wallets::wallet::WalletTransactionContent;
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::Colors;
use crate::wallet::types::WalletTransaction;
use crate::wallet::Wallet;
/// Invoice request creation content.
pub struct InvoiceRequestContent {
/// Amount to receive.
amount_edit: String,
/// Flag to check if request is loading.
request_loading: bool,
/// Request result if there is no error.
request_result: Arc<RwLock<Option<Result<WalletTransaction, Error>>>>,
/// Flag to check if there is an error happened on request creation.
request_error: Option<String>,
/// Request result transaction content.
result_tx_content: Option<WalletTransactionContent>,
}
impl Default for InvoiceRequestContent {
fn default() -> Self {
Self {
amount_edit: "".to_string(),
request_loading: false,
request_result: Arc::new(RwLock::new(None)),
request_error: None,
result_tx_content: None,
}
}
}
impl InvoiceRequestContent {
/// Draw [`Modal`] content.
pub fn modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Draw transaction information on request result.
if let Some(tx) = self.result_tx_content.as_mut() {
tx.ui(ui, wallet, modal, cb);
return;
}
// Setup callback on continue.
let on_continue = |m: &mut InvoiceRequestContent| {
if m.amount_edit.is_empty() {
return;
}
if let Ok(a) = amount_from_hr_string(m.amount_edit.as_str()) {
modal.disable_closing();
// Setup data for request.
let wallet = wallet.clone();
let result = m.request_result.clone();
// Send request at another thread.
m.request_loading = true;
thread::spawn(move || {
let res = wallet.issue_invoice(a);
let mut w_result = result.write();
*w_result = Some(res);
});
} else {
m.request_error = Some(t!("wallets.invoice_slatepack_err"));
}
};
ui.add_space(6.0);
// Draw content on request loading.
if self.request_loading {
self.loading_request_ui(ui, modal);
return;
}
// Draw amount input content.
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.enter_amount_receive"))
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(8.0);
// Draw request amount text input.
let amount_edit_before = self.amount_edit.clone();
let mut amount_edit = TextEdit::new(Id::from(modal.id).with(wallet.get_config().id))
.h_center()
.numeric();
amount_edit.ui(ui, &mut self.amount_edit, cb);
if amount_edit.enter_pressed {
on_continue(self);
}
// Check value if input was changed.
if amount_edit_before != self.amount_edit {
self.request_error = None;
if !self.amount_edit.is_empty() {
self.amount_edit = self.amount_edit.trim().replace(",", ".");
match amount_from_hr_string(self.amount_edit.as_str()) {
Ok(a) => {
if !self.amount_edit.contains(".") {
// To avoid input of several "0".
if a == 0 {
self.amount_edit = "0".to_string();
return;
}
} else {
// Check input after ".".
let parts = self.amount_edit
.split(".")
.collect::<Vec<&str>>();
if parts.len() == 2 && parts[1].len() > 9 {
self.amount_edit = amount_edit_before;
return;
}
}
}
Err(_) => {
self.amount_edit = amount_edit_before;
}
}
}
}
// Show request creation error.
if let Some(err) = &self.request_error {
ui.add_space(12.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(err)
.size(17.0)
.color(Colors::red()));
});
}
ui.add_space(12.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.amount_edit = "".to_string();
self.request_error = None;
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Button to create Slatepack message request.
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
on_continue(self);
});
});
});
ui.add_space(6.0);
}
/// Draw loading request content.
fn loading_request_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) {
ui.add_space(34.0);
ui.vertical_centered(|ui| {
View::big_loading_spinner(ui);
});
ui.add_space(50.0);
// Check if there is request result error.
if self.request_error.is_some() {
modal.enable_closing();
self.request_loading = false;
return;
}
// Update data on request result.
let r_request = self.request_result.read();
if r_request.is_some() {
modal.enable_closing();
let result = r_request.as_ref().unwrap();
match result {
Ok(tx) => {
self.result_tx_content =
Some(WalletTransactionContent::new(tx, false));
}
Err(_) => {
self.request_error = Some(t!("wallets.invoice_slatepack_err"));
self.request_loading = false;
}
}
}
}
}
@@ -0,0 +1,19 @@
// Copyright 2025 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod invoice;
pub use invoice::*;
mod send;
pub use send::*;
@@ -1,4 +1,4 @@
// Copyright 2024 The Grim Developers
// Copyright 2025 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -12,87 +12,129 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use std::thread;
use egui::{Id, RichText};
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
use grin_wallet_libwallet::{Error, SlatepackAddress};
use parking_lot::RwLock;
use std::sync::Arc;
use std::thread;
use egui::{Id, RichText};
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::wallets::wallet::WalletTransactionContent;
use crate::gui::views::{CameraContent, Modal, TextEdit, View};
use crate::gui::views::wallets::wallet::WalletTransactionModal;
use crate::gui::views::types::ModalPosition;
use crate::wallet::types::WalletTransaction;
use crate::wallet::Wallet;
/// Transport sending [`Modal`] content.
pub struct TransportSendModal {
/// Flag to check if transaction is sending to show progress.
sending: bool,
/// Flag to check if there is an error to repeat.
error: bool,
/// Transaction result.
send_result: Arc<RwLock<Option<Result<WalletTransaction, Error>>>>,
/// Entered amount value.
/// Content to create a request to send funds.
pub struct SendRequestContent {
/// Amount to send or receive.
amount_edit: String,
/// Entered address value.
/// Receiver address.
address_edit: String,
/// Flag to check if entered address is incorrect.
address_error: bool,
/// Flag to check if request is loading.
request_loading: bool,
/// Request result if there is no error.
request_result: Arc<RwLock<Option<Result<WalletTransaction, Error>>>>,
/// Flag to check if there is an error happened on request creation.
request_error: Option<String>,
/// Address QR code scanner content.
address_scan_content: Option<CameraContent>,
/// Transaction information content.
tx_info_content: Option<WalletTransactionModal>,
/// Request result transaction content.
result_tx_content: Option<WalletTransactionContent>,
}
impl TransportSendModal {
/// Create new instance from provided address.
impl SendRequestContent {
/// Create new content instance with optional receiver address.
pub fn new(addr: Option<String>) -> Self {
Self {
sending: false,
error: false,
send_result: Arc::new(RwLock::new(None)),
amount_edit: "".to_string(),
address_edit: addr.unwrap_or("".to_string()),
address_error: false,
request_loading: false,
request_result: Arc::new(RwLock::new(None)),
request_error: None,
address_scan_content: None,
tx_info_content: None,
result_tx_content: None,
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
pub fn modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Draw transaction information on request result.
if let Some(tx) = self.tx_info_content.as_mut() {
if let Some(tx) = self.result_tx_content.as_mut() {
tx.ui(ui, wallet, modal, cb);
return;
}
// Draw sending content, progress or an error.
if self.sending {
self.progress_ui(ui);
} else if self.error {
self.error_ui(ui, wallet, modal);
} else {
self.content_ui(ui, wallet, modal, cb);
}
}
// Setup callback on continue.
let on_continue = |m: &mut SendRequestContent| {
if m.amount_edit.is_empty() {
return;
}
// Check address to send over Tor if enabled.
let addr_str = m.address_edit.as_str();
if let Ok(addr) = SlatepackAddress::try_from(addr_str.trim()) {
if let Ok(a) = amount_from_hr_string(m.amount_edit.as_str()) {
Modal::change_position(ModalPosition::Center);
modal.disable_closing();
let mut wallet = wallet.clone();
let res = m.request_result.clone();
// Send request at another thread.
m.request_loading = true;
thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let result = wallet.send_tor(a, &addr).await;
let mut w_res = res.write();
*w_res = Some(result);
});
});
}
} else if !addr_str.is_empty() {
m.address_error = true;
} else if let Ok(amount) = amount_from_hr_string(m.amount_edit.as_str()) {
Modal::change_position(ModalPosition::Center);
modal.disable_closing();
let wallet = wallet.clone();
let result = m.request_result.clone();
// Send request at another thread.
m.request_loading = true;
thread::spawn(move || {
let res = wallet.send(amount, None);
let mut w_result = result.write();
*w_result = Some(res);
});
} else {
m.request_error = Some(t!("wallets.send_slatepack_err"));
}
};
// Draw content on request loading.
if self.request_loading {
self.loading_request_ui(ui, modal);
return;
}
/// Draw content to send.
fn content_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
// Draw QR code scanner content if requested.
if let Some(scanner) = self.address_scan_content.as_mut() {
let on_stop = || {
@@ -105,6 +147,7 @@ impl TransportSendModal {
on_stop();
self.address_scan_content = None;
} else {
ui.add_space(6.0);
scanner.ui(ui, cb);
ui.add_space(6.0);
@@ -115,9 +158,7 @@ impl TransportSendModal {
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
on_stop();
self.address_scan_content = None;
Modal::close();
self.close();
});
});
cols[1].vertical_centered_justified(|ui| {
@@ -201,7 +242,7 @@ impl TransportSendModal {
});
ui.add_space(6.0);
// Draw address text edit.
// Show address text edit.
let addr_edit_before = self.address_edit.clone();
let address_edit_id = Id::from(modal.id).with("_address").with(wallet.get_config().id);
let mut address_edit = TextEdit::new(address_edit_id)
@@ -224,9 +265,19 @@ impl TransportSendModal {
self.address_error = false;
}
// Send on Enter press.
// Continue on Enter press.
if address_edit.enter_pressed {
self.send(wallet, modal);
on_continue(self);
}
// Show request creation error.
if let Some(err) = &self.request_error {
ui.vertical_centered(|ui| {
ui.label(RichText::new(err)
.size(17.0)
.color(Colors::red()));
});
ui.add_space(12.0);
}
// Setup spacing between buttons.
@@ -239,36 +290,9 @@ impl TransportSendModal {
});
});
columns[1].vertical_centered_justified(|ui| {
// Button to create Slatepack message request.
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
self.send(wallet, modal);
});
});
});
ui.add_space(6.0);
}
/// Draw error content.
fn error_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.tor_send_error"))
.size(17.0)
.color(Colors::red()));
});
ui.add_space(12.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
self.send(wallet, modal);
on_continue(self);
});
});
});
@@ -280,78 +304,72 @@ impl TransportSendModal {
self.amount_edit = "".to_string();
self.address_edit = "".to_string();
let mut w_res = self.send_result.write();
let mut w_res = self.request_result.write();
*w_res = None;
self.tx_info_content = None;
self.result_tx_content = None;
self.address_scan_content = None;
Modal::close();
}
/// Send entered amount to address.
fn send(&mut self, wallet: &Wallet, modal: &Modal) {
if self.amount_edit.is_empty() {
return;
}
let addr_str = self.address_edit.as_str();
if let Ok(addr) = SlatepackAddress::try_from(addr_str.trim()) {
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
modal.disable_closing();
// Send amount over Tor.
let mut wallet = wallet.clone();
let res = self.send_result.clone();
self.sending = true;
thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let result = wallet.send_tor(a, &addr).await;
let mut w_res = res.write();
*w_res = Some(result);
});
});
}
} else {
self.address_error = true;
}
}
/// Draw sending progress content.
fn progress_ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(16.0);
/// Draw loading request content.
fn loading_request_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) {
ui.add_space(40.0);
ui.vertical_centered(|ui| {
View::small_loading_spinner(ui);
ui.add_space(12.0);
ui.label(RichText::new(t!("transport.tor_sending", "amount" => self.amount_edit))
.size(17.0)
.color(Colors::gray()));
View::big_loading_spinner(ui);
});
ui.add_space(10.0);
ui.add_space(40.0);
// Check sending result.
let has_result = {
let r_result = self.send_result.read();
r_result.is_some()
if !self.address_edit.is_empty() {
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.tor_sending", "amount" => self.amount_edit))
.size(17.0)
.color(Colors::inactive_text()));
});
ui.add_space(12.0);
}
// Update data on request result.
let has_res = {
let r_request = self.request_result.read();
r_request.is_some()
};
if has_result {
{
let res = self.send_result.read().clone().unwrap();
match res {
Ok(tx) => {
self.tx_info_content =
Some(WalletTransactionModal::new(Some(tx.data.id), false));
}
Err(_) => {
self.error = true;
}
if has_res {
self.request_loading = false;
modal.enable_closing();
let r_request = self.request_result.read();
let result = r_request.as_ref().unwrap();
match result {
Ok(tx) => {
self.result_tx_content =
Some(WalletTransactionContent::new(tx, false));
}
Err(err) => {
let m = match err {
Error::NotEnoughFunds { .. } => {
t!("wallets.pay_balance_error", "amount" => self.amount_edit)
}
_ => {
if !self.address_edit.is_empty() {
t!("transport.tor_send_error")
} else {
t!("wallets.send_slatepack_err")
}
}
};
self.request_error = Some(m);
}
}
let mut w_res = self.send_result.write();
*w_res = None;
self.sending = false;
}
// Check if there is request result error.
if self.request_error.is_some() {
Modal::change_position(ModalPosition::CenterTop);
modal.enable_closing();
let mut w_request = self.request_result.write();
*w_request = None;
self.request_loading = false;
}
}
}
@@ -16,12 +16,12 @@ use egui::{Id, RichText};
use grin_chain::SyncStatus;
use grin_util::ZeroingString;
use crate::gui::Colors;
use crate::gui::icons::{EYE, LIFEBUOY, STETHOSCOPE, TRASH, WRENCH};
use crate::gui::icons::{EYE, KEY, LIFEBUOY, STETHOSCOPE, TRASH};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::wallet::types::WalletContentContainer;
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::Colors;
use crate::node::Node;
use crate::wallet::types::ConnectionMethod;
use crate::wallet::Wallet;
@@ -69,7 +69,7 @@ impl WalletContentContainer for RecoverySettings {
ui.add_space(10.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
View::sub_title(ui, format!("{} {}", WRENCH, t!("wallets.recovery")));
View::sub_title(ui, format!("{} {}", KEY, t!("wallets.recovery")));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
+120 -285
View File
@@ -1,4 +1,4 @@
// Copyright 2023 The Grim Developers
// Copyright 2025 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,362 +14,197 @@
use egui::{Align, CornerRadius, Layout, RichText, StrokeKind};
use crate::gui::Colors;
use crate::gui::icons::{CHECK_CIRCLE, COPY, DOTS_THREE_CIRCLE, EXPORT, GEAR_SIX, GLOBE_SIMPLE, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, X_CIRCLE};
use crate::gui::icons::{CIRCLE_HALF, DOTS_THREE_CIRCLE, PLUGS, PLUGS_CONNECTED, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, WRENCH};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::wallets::wallet::transport::settings::WalletTransportSettingsContent;
use crate::gui::views::wallets::wallet::types::WalletContentContainer;
use crate::gui::views::{Modal, QrCodeContent, View};
use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::wallet::transport::send::TransportSendModal;
use crate::gui::views::wallets::wallet::transport::settings::TransportSettingsModal;
use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType};
use crate::gui::Colors;
use crate::tor::{Tor, TorConfig};
use crate::wallet::types::WalletData;
use crate::wallet::Wallet;
/// Wallet transport tab content.
pub struct WalletTransport {
/// Sending [`Modal`] content.
send_modal_content: Option<TransportSendModal>,
/// Wallet transport panel content.
pub struct WalletTransportContent {
/// QR code address content.
pub qr_address_content: Option<QrCodeContent>,
/// QR code address image [`Modal`] content.
qr_address_content: Option<QrCodeContent>,
/// Tor settings [`Modal`] content.
settings_modal_content: Option<TransportSettingsModal>,
/// Settings content.
pub settings_content: Option<WalletTransportSettingsContent>,
}
impl WalletTab for WalletTransport {
fn get_type(&self) -> WalletTabType {
WalletTabType::Transport
impl WalletContentContainer for WalletTransportContent {
fn modal_ids(&self) -> Vec<&'static str> { vec![] }
fn modal_ui(&mut self, _: &mut egui::Ui, _: &Wallet, _: &Modal, _: &dyn PlatformCallbacks) {
}
fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
self.modal_content_ui(ui, wallet, cb);
self.transport_ui(ui, wallet, cb);
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
if let Some(content) = self.qr_address_content.as_mut() {
ui.add_space(6.0);
/// Identifier for [`Modal`] to send amount over Tor.
const SEND_TOR_MODAL: &'static str = "send_tor_modal";
/// Identifier for [`Modal`] to setup Tor service.
const TOR_SETTINGS_MODAL: &'static str = "tor_settings_modal";
/// Identifier for [`Modal`] to show QR code address image.
const QR_ADDRESS_MODAL: &'static str = "qr_address_modal";
// Draw QR code content.
content.ui(ui, cb);
impl WalletTransport {
/// Create new transport content instance, opening sending `Modal` if address was provided.
pub fn new(address: Option<String>) -> Self {
let mut content = Self {
send_modal_content: None,
qr_address_content: None,
settings_modal_content: None,
};
if address.is_some() {
content.show_send_tor_modal(address)
}
content
}
/// Draw wallet transport content.
fn transport_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
ui.add_space(3.0);
ui.label(RichText::new(t!("transport.desc"))
.size(16.0)
.color(Colors::inactive_text()));
ui.add_space(7.0);
// Draw Tor transport content.
self.tor_ui(ui, wallet, cb);
}
/// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
match Modal::opened() {
None => {}
Some(id) => {
match id {
SEND_TOR_MODAL => {
if let Some(content) = self.send_modal_content.as_mut() {
Modal::ui(ui.ctx(), cb, |ui, modal, cb| {
content.ui(ui, wallet, modal, cb);
});
}
}
TOR_SETTINGS_MODAL => {
if let Some(content) = self.settings_modal_content.as_mut() {
Modal::ui(ui.ctx(), cb, |ui, modal, cb| {
content.ui(ui, wallet, modal, cb);
});
}
}
QR_ADDRESS_MODAL => {
Modal::ui(ui.ctx(), cb, |ui, _, cb| {
self.qr_address_modal_ui(ui, cb);
});
}
_ => {}
}
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_address_content = None;
});
});
ui.add_space(6.0);
} else if let Some(content) = self.settings_content.as_mut() {
let mut closed = false;
content.ui(ui, wallet, cb, || {
closed = true;
});
if closed {
self.settings_content = None;
}
} else {
self.tor_header_ui(ui, wallet);
}
}
}
/// Draw Tor transport content.
fn tor_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
let data = wallet.get_data().unwrap();
// Draw header content.
self.tor_header_ui(ui, wallet);
// Draw receive info content.
if wallet.slatepack_address().is_some() {
self.tor_receive_ui(ui, wallet, &data, cb);
impl Default for WalletTransportContent {
fn default() -> Self {
Self {
qr_address_content: None,
settings_content: None,
}
}
}
// Draw send content.
let service_id = &wallet.identifier();
if data.info.amount_currently_spendable > 0 && wallet.foreign_api_port().is_some() &&
!Tor::is_service_starting(service_id) {
self.tor_send_ui(ui);
impl WalletTransportContent {
/// Check if it's possible to go back at navigation stack.
pub fn can_back(&self) -> bool {
self.settings_content.is_some() || self.qr_address_content.is_some()
}
/// Navigate back on navigation stack.
pub fn back(&mut self) {
if self.settings_content.is_some() {
self.settings_content = None;
} else if self.qr_address_content.is_some() {
self.qr_address_content = None;
}
}
/// Draw Tor transport header content.
fn tor_header_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
let wallet_data = wallet.get_data();
if wallet_data.is_none() {
return;
}
let addr = wallet.slatepack_address().unwrap();
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(0, 2, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill_lite(),
let info = wallet.get_data().unwrap().info;
let awaiting_balance = info.amount_awaiting_confirmation > 0 ||
info.amount_awaiting_finalization > 0 || info.amount_locked > 0;
let rounding = if awaiting_balance {
View::item_rounding(1, 3, false)
} else {
View::item_rounding(1, 2, false)
};
ui.painter().rect(rect,
rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Middle);
StrokeKind::Outside);
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to setup Tor transport.
let button_rounding = View::item_rounding(0, 2, true);
View::item_button(ui, button_rounding, GEAR_SIX, None, || {
self.settings_modal_content = Some(TransportSettingsModal::default());
// Show Tor settings modal.
Modal::new(TOR_SETTINGS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("transport.tor_settings"))
.closeable(false)
.show();
// Show button to show QR code address.
View::item_button(ui, View::item_rounding(1, 2, true), QR_CODE, None, || {
self.qr_address_content = Some(QrCodeContent::new(addr.clone(), false)
.with_max_size(320.0));
});
// Draw button to enable/disable Tor listener for current wallet.
let service_id = &wallet.identifier();
if !Tor::is_service_starting(service_id) && wallet.foreign_api_port().is_some() {
if !Tor::is_service_running(service_id) {
View::item_button(ui, CornerRadius::default(), POWER, Some(Colors::green()), || {
let r = CornerRadius::default();
View::item_button(ui, r, POWER, Some(Colors::green()), || {
if let Ok(key) = wallet.secret_key() {
let api_port = wallet.foreign_api_port().unwrap();
Tor::start_service(api_port, key, service_id);
}
});
} else {
View::item_button(ui, CornerRadius::default(), POWER, Some(Colors::red()), || {
let r = CornerRadius::default();
View::item_button(ui, r, POWER, Some(Colors::red()), || {
Tor::stop_service(service_id);
});
}
}
// Draw button to show Tor transport settings.
let button_rounding = View::item_rounding(1, 3, true);
View::item_button(ui, button_rounding, WRENCH, None, || {
self.settings_content = Some(WalletTransportSettingsContent::default());
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(RichText::new(t!("transport.tor_network"))
.size(18.0)
.color(Colors::title(false)));
});
// Setup Tor status text.
let is_running = Tor::is_service_running(service_id);
let is_starting = Tor::is_service_starting(service_id);
let has_error = Tor::is_service_failed(service_id);
let (icon, text) = if wallet.foreign_api_port().is_none() {
(DOTS_THREE_CIRCLE, t!("wallets.loading"))
} else if is_starting {
let address_color = if is_running {
Colors::green()
} else if has_error {
Colors::red()
} else {
Colors::inactive_text()
};
let is_starting = Tor::is_service_starting(service_id);
// Show slatepack address text.
View::animate_text(ui, addr.clone(), 17.0, address_color, is_starting);
ui.add_space(1.0);
let (icon, text) = if is_starting {
(DOTS_THREE_CIRCLE, t!("transport.connecting"))
} else if has_error {
(WARNING_CIRCLE, t!("transport.conn_error"))
} else if is_running {
(CHECK_CIRCLE, t!("transport.connected"))
(PLUGS_CONNECTED, t!("transport.connected"))
} else if let Some(_) = TorConfig::get_proxy() {
(PLUGS_CONNECTED, t!("app_settings.proxy"))
} else {
(X_CIRCLE, t!("transport.disconnected"))
(PLUGS, t!("transport.disconnected"))
};
let status_text = format!("{} {}", icon, text);
ui.label(RichText::new(status_text).size(15.0).color(Colors::text(false)));
// Show connection status text.
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
ui.add_space(1.0);
// Setup bridges status text.
let bridge = TorConfig::get_bridge();
let bridges_text = match &bridge {
None => {
format!("{} {}", SHIELD_SLASH, t!("transport.bridges_disabled"))
}
Some(b) => {
let name = b.protocol_name().to_uppercase();
format!("{} {}",
SHIELD_CHECKERED,
t!("transport.bridge_name", "b" = name))
let bridges_text = if is_starting || has_error {
match TorConfig::get_bridge() {
None => {
format!("{} {}", SHIELD_SLASH, t!("transport.bridges_disabled"))
}
Some(b) => {
let name = b.protocol_name().to_uppercase();
format!("{} {}",
SHIELD_CHECKERED,
t!("transport.bridge_name", "b" = name))
}
}
} else {
format!("{} {}", CIRCLE_HALF, t!("transport.tor_network"))
};
// Show bridge info text.
ui.label(RichText::new(bridges_text).size(15.0).color(Colors::gray()));
});
});
});
});
}
/// Draw Tor receive content.
fn tor_receive_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
data: &WalletData,
cb: &dyn PlatformCallbacks) {
let addr = wallet.slatepack_address().unwrap();
let service_id = &wallet.identifier();
let can_send = data.info.amount_currently_spendable > 0;
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(52.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = if can_send {
View::item_rounding(1, 3, false)
} else {
View::item_rounding(1, 2, false)
};
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill_lite(),
View::item_stroke(),
StrokeKind::Middle);
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to setup Tor transport.
let button_rounding = if can_send {
View::item_rounding(1, 3, true)
} else {
View::item_rounding(1, 2, true)
};
View::item_button(ui, button_rounding, QR_CODE, None, || {
// Show QR code image address modal.
self.qr_address_content = Some(QrCodeContent::new(addr.clone(), false));
Modal::new(QR_ADDRESS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_mining.address"))
.show();
});
// Show button to enable/disable Tor listener for current wallet.
View::item_button(ui, CornerRadius::default(), COPY, None, || {
cb.copy_string_to_buffer(addr.clone());
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Show wallet Slatepack address.
let address_color = if Tor::is_service_starting(service_id) ||
wallet.foreign_api_port().is_none() {
Colors::inactive_text()
} else if Tor::is_service_running(service_id) {
Colors::green()
} else {
Colors::red()
};
View::ellipsize_text(ui, addr, 15.0, address_color);
let address_label = format!("{} {}",
GLOBE_SIMPLE,
t!("network_mining.address"));
ui.label(RichText::new(address_label).size(15.0).color(Colors::gray()));
});
});
});
});
}
/// Draw QR code image address [`Modal`] content.
fn qr_address_modal_ui(&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
// Draw QR code content.
if let Some(content) = self.qr_address_content.as_mut() {
content.ui(ui, cb);
} else {
Modal::close();
return;
}
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_address_content = None;
Modal::close();
});
});
ui.add_space(6.0);
}
/// Draw Tor send content.
fn tor_send_ui(&mut self, ui: &mut egui::Ui) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(55.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(1, 2, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Middle);
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::top_down(Align::Center), |ui| {
ui.add_space(7.0);
// Draw button to open sending modal.
let send_text = format!("{} {}", EXPORT, t!("wallets.send"));
View::button(ui, send_text, Colors::white_or_black(false), || {
self.show_send_tor_modal(None);
});
});
});
}
/// Show [`Modal`] to send over Tor.
fn show_send_tor_modal(&mut self, address: Option<String>) {
self.send_modal_content = Some(TransportSendModal::new(address));
// Show modal.
Modal::new(SEND_TOR_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.send"))
.show();
}
}
@@ -1,4 +1,4 @@
// Copyright 2024 The Grim Developers
// Copyright 2025 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -15,5 +15,4 @@
mod content;
pub use content::*;
mod send;
mod settings;
@@ -1,4 +1,4 @@
// Copyright 2024 The Grim Developers
// Copyright 2025 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -13,226 +13,66 @@
// limitations under the License.
use egui::os::OperatingSystem;
use egui::{Id, RichText};
use egui::RichText;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, TextEdit, View};
use crate::tor::{Tor, TorBridge, TorConfig};
use crate::gui::views::settings::TorSettingsContent;
use crate::gui::views::types::ContentContainer;
use crate::gui::views::View;
use crate::tor::Tor;
use crate::wallet::Wallet;
/// Transport settings [`Modal`] content.
pub struct TransportSettingsModal {
/// Flag to check if Tor settings were changed.
/// Wallet transport settings content.
pub struct WalletTransportSettingsContent {
/// Flag to check if settings were changed to restart Tor service.
settings_changed: bool,
/// Tor bridge binary path edit text.
bridge_bin_path_edit: String,
/// Tor bridge connection line edit text.
bridge_conn_line_edit: String,
/// Address QR code scanner [`Modal`] content.
bridge_qr_scan_content: Option<CameraContent>,
/// Tor transport content settings.
tor_settings_content: TorSettingsContent,
}
impl Default for TransportSettingsModal {
impl Default for WalletTransportSettingsContent {
fn default() -> Self {
// Setup Tor bridge binary path edit text.
let bridge = TorConfig::get_bridge();
let (bin_path, conn_line) = if let Some(b) = bridge {
(b.binary_path(), b.connection_line())
} else {
("".to_string(), "".to_string())
};
Self {
settings_changed: false,
bridge_bin_path_edit: bin_path,
bridge_conn_line_edit: conn_line,
bridge_qr_scan_content: None,
tor_settings_content: TorSettingsContent::default(),
}
}
}
impl TransportSettingsModal {
impl WalletTransportSettingsContent {
/// Draw transport settings content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
// Draw QR code scanner content if requested.
if let Some(scanner) = self.bridge_qr_scan_content.as_mut() {
let on_stop = || {
cb.stop_camera();
modal.enable_closing();
};
if let Some(result) = scanner.qr_scan_result() {
self.bridge_conn_line_edit = result.text();
on_stop();
self.bridge_qr_scan_content = None;
} else {
scanner.ui(ui, cb);
ui.add_space(12.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show buttons to close modal or come back to sending input.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
on_stop();
self.bridge_qr_scan_content = None;
Modal::close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
on_stop();
self.bridge_qr_scan_content = None;
});
});
});
ui.add_space(6.0);
}
return;
}
// Do not show bridges setup on Android.
let os = OperatingSystem::from_target_os();
let show_bridges = os != OperatingSystem::Android;
if show_bridges {
let bridge = TorConfig::get_bridge();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.bridges_desc"))
.size(17.0)
.color(Colors::inactive_text()));
// Draw checkbox to enable/disable bridges.
View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || {
// Save value.
let value = if bridge.is_some() {
None
} else {
let default_bridge = TorConfig::get_obfs4();
self.bridge_bin_path_edit = default_bridge.binary_path();
self.bridge_conn_line_edit = default_bridge.connection_line();
Some(default_bridge)
};
TorConfig::save_bridge(value);
self.settings_changed = true;
});
});
// Draw bridges selection and path.
if bridge.is_some() {
let current_bridge = bridge.unwrap();
let mut bridge = current_bridge.clone();
ui.add_space(6.0);
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
// Draw Obfs4 bridge selector.
let obfs4 = TorConfig::get_obfs4();
let name = obfs4.protocol_name().to_uppercase();
View::radio_value(ui, &mut bridge, obfs4, name);
});
columns[1].vertical_centered(|ui| {
// Draw Snowflake bridge selector.
let snowflake = TorConfig::get_snowflake();
let name = snowflake.protocol_name().to_uppercase();
View::radio_value(ui, &mut bridge, snowflake, name);
});
});
ui.add_space(12.0);
// Check if bridge type was changed to save.
if current_bridge != bridge {
self.settings_changed = true;
TorConfig::save_bridge(Some(bridge.clone()));
self.bridge_bin_path_edit = bridge.binary_path();
self.bridge_conn_line_edit = bridge.connection_line();
}
// Draw binary path text edit.
let bin_edit_id = Id::from(modal.id)
.with(wallet.get_config().id)
.with("_bin_edit");
let mut bin_edit = TextEdit::new(bin_edit_id)
.no_soft_keyboard()
.paste()
.focus(false);
let bin_edit_before = self.bridge_bin_path_edit.clone();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.bin_file"))
.size(17.0)
.color(Colors::inactive_text()));
ui.add_space(6.0);
bin_edit.ui(ui, &mut self.bridge_bin_path_edit, cb);
ui.add_space(6.0);
});
// Draw connection line text edit.
let conn_edit_before = self.bridge_conn_line_edit.clone();
let conn_edit_id = Id::from(modal.id)
.with(wallet.get_config().id)
.with("_conn_edit");
let mut conn_edit = TextEdit::new(conn_edit_id)
.no_soft_keyboard()
.paste()
.focus(false)
.scan_qr();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.conn_line"))
.size(17.0)
.color(Colors::inactive_text()));
ui.add_space(6.0);
conn_edit.ui(ui, &mut self.bridge_conn_line_edit, cb);
// Check if scan button was pressed.
if conn_edit.scan_pressed {
modal.disable_closing();
self.bridge_qr_scan_content = Some(CameraContent::default());
}
});
// Check if bin path or connection line text was changed to save bridge.
if conn_edit_before != self.bridge_conn_line_edit ||
bin_edit_before != self.bridge_bin_path_edit {
let bin_path = self.bridge_bin_path_edit.trim().to_string();
let conn_line = self.bridge_conn_line_edit.trim().to_string();
let b = match bridge {
TorBridge::Snowflake(_, _) => {
TorBridge::Snowflake(bin_path, conn_line)
},
TorBridge::Obfs4(_, _) => {
TorBridge::Obfs4(bin_path, conn_line)
}
};
TorConfig::save_bridge(Some(b));
self.settings_changed = true;
}
ui.add_space(2.0);
}
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
}
cb: &dyn PlatformCallbacks,
on_close: impl FnOnce()) {
ui.add_space(8.0);
ui.vertical_centered(|ui| {
// Do not show bridges settings on Android.
let os = OperatingSystem::from_target_os();
if os != OperatingSystem::Android {
// Show Tor settings.
self.tor_settings_content.ui(ui, cb);
if !self.tor_settings_content.settings_changed {
self.settings_changed = true;
}
ui.add_space(4.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
}
ui.label(RichText::new(t!("transport.tor_autorun_desc"))
.size(17.0)
.color(Colors::inactive_text()));
// Show Tor service autorun checkbox.
let autorun = wallet.auto_start_tor_listener();
View::checkbox(ui, autorun, t!("network.autorun"), || {
wallet.update_auto_start_tor_listener(!autorun);
self.settings_changed = true;
});
});
ui.add_space(6.0);
ui.add_space(8.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
if self.settings_changed {
@@ -248,7 +88,7 @@ impl TransportSettingsModal {
Tor::rebuild_client();
}
}
Modal::close();
on_close();
});
});
ui.add_space(6.0);
+37 -41
View File
@@ -12,30 +12,30 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::ops::Range;
use std::time::{SystemTime, UNIX_EPOCH};
use egui::{Align, Id, Layout, Rect, RichText, CornerRadius, ScrollArea, StrokeKind};
use egui::epaint::RectShape;
use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, CornerRadius, Id, Layout, Rect, RichText, ScrollArea, StrokeKind};
use grin_core::consensus::COINBASE_MATURITY;
use grin_core::core::amount_to_hr_string;
use grin_wallet_libwallet::TxLogEntryType;
use std::ops::Range;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::gui::Colors;
use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, DOTS_THREE_CIRCLE, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE};
use crate::gui::icons::{ARCHIVE_BOX, ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, CALENDAR_CHECK, CHECK, DOTS_THREE_CIRCLE, FILE_ARROW_DOWN, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, PullToRefresh, Content, View};
use crate::gui::views::types::{LinePosition, ModalPosition};
use crate::gui::views::wallets::types::WalletTab;
use crate::gui::views::wallets::wallet::types::{GRIN, WalletTabType};
use crate::gui::views::wallets::wallet::WalletTransactionModal;
use crate::gui::views::wallets::wallet::types::{WalletTabType, GRIN};
use crate::gui::views::wallets::wallet::WalletTransactionContent;
use crate::gui::views::{Content, Modal, PullToRefresh, View};
use crate::gui::Colors;
use crate::wallet::types::{WalletData, WalletTransaction};
use crate::wallet::Wallet;
/// Wallet transactions tab content.
pub struct WalletTransactions {
/// Transaction information [`Modal`] content.
tx_info_content: Option<WalletTransactionModal>,
tx_info_content: Option<WalletTransactionContent>,
/// Transaction identifier to use at confirmation [`Modal`].
confirm_cancel_tx_id: Option<u32>,
@@ -44,16 +44,6 @@ pub struct WalletTransactions {
manual_sync: Option<u128>
}
impl Default for WalletTransactions {
fn default() -> Self {
Self {
tx_info_content: None,
confirm_cancel_tx_id: None,
manual_sync: None,
}
}
}
impl WalletTab for WalletTransactions {
fn get_type(&self) -> WalletTabType {
WalletTabType::Txs
@@ -74,6 +64,19 @@ impl WalletTransactions {
/// Height of transaction list item.
pub const TX_ITEM_HEIGHT: f32 = 75.0;
/// Create new content instance with opening tx info.
pub fn new(tx: Option<WalletTransaction>) -> Self {
let mut content = Self {
tx_info_content: None,
confirm_cancel_tx_id: None,
manual_sync: None,
};
if let Some(tx) = &tx {
content.show_tx_info_modal(tx, false);
}
content
}
/// Draw transactions content.
fn txs_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
let data = wallet.get_data().unwrap();
@@ -84,15 +87,14 @@ impl WalletTransactions {
return;
}
let txs = data.txs.as_ref().unwrap();
let mut awaiting_amount = false;
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show message when txs are empty.
if txs.is_empty() {
View::center_content(ui, 96.0, |ui| {
let empty_text = t!(
"wallets.txs_empty",
"message" => CHAT_CIRCLE_TEXT,
"transport" => BRIDGE,
"message" => FILE_ARROW_DOWN,
"transport" => ARCHIVE_BOX,
"settings" => GEAR_FINE
);
ui.label(RichText::new(empty_text)
@@ -102,7 +104,7 @@ impl WalletTransactions {
return;
}
// Draw awaiting amount info if exists.
awaiting_amount = self.awaiting_info_ui(ui, &data);
self.awaiting_info_ui(ui, &data);
});
ui.add_space(4.0);
@@ -120,7 +122,7 @@ impl WalletTransactions {
.show_rows(ui, Self::TX_ITEM_HEIGHT, txs.len(), |ui, row_range| {
ui.add_space(1.0);
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tx_list_ui(ui, awaiting_amount, row_range, wallet, txs);
self.tx_list_ui(ui, row_range, wallet, txs);
});
})
});
@@ -137,25 +139,19 @@ impl WalletTransactions {
/// Draw transaction list content.
fn tx_list_ui(&mut self,
ui: &mut egui::Ui,
awaiting: bool,
row_range: Range<usize>,
wallet: &Wallet,
txs: &Vec<WalletTransaction>) {
for index in row_range {
let mut rect = if awaiting {
let mut rect = ui.available_rect_before_wrap();
rect.min += egui::emath::vec2(6.0, 0.0);
rect.max -= egui::emath::vec2(6.0, 0.0);
rect
} else {
ui.available_rect_before_wrap()
};
let mut rect = ui.available_rect_before_wrap();
rect.min += egui::emath::vec2(6.0, 0.0);
rect.max -= egui::emath::vec2(6.0, 0.0);
rect.set_height(Self::TX_ITEM_HEIGHT);
// Draw tx item background.
let mut r = View::item_rounding(index, txs.len(), false);
let p = ui.painter();
p.rect(rect, r, Colors::fill_lite(), View::item_stroke(), StrokeKind::Middle);
p.rect(rect, r, Colors::fill(), View::item_stroke(), StrokeKind::Middle);
let tx = txs.get(index).unwrap();
let data = wallet.get_data().unwrap();
@@ -195,14 +191,15 @@ impl WalletTransactions {
}
}
/// Draw information about locked, finalizing or confirming balance, return `true` if exists.
fn awaiting_info_ui(&mut self, ui: &mut egui::Ui, data: &WalletData) -> bool {
/// Draw information about locked, finalizing or confirming balance.
fn awaiting_info_ui(&mut self, ui: &mut egui::Ui, data: &WalletData) {
let amount_conf = data.info.amount_awaiting_confirmation;
let amount_fin = data.info.amount_awaiting_finalization;
let amount_locked = data.info.amount_locked;
if amount_conf == 0 && amount_fin == 0 && amount_locked == 0 {
return false;
return;
}
ui.add_space(-1.0);
let rect = ui.available_rect_before_wrap();
// Draw background.
let mut bg = RectShape::new(rect, CornerRadius {
@@ -210,7 +207,7 @@ impl WalletTransactions {
ne: 0.0 as u8,
sw: 8.0 as u8,
se: 8.0 as u8,
}, Colors::TRANSPARENT, View::item_stroke(), StrokeKind::Middle);
}, Colors::fill(), View::item_stroke(), StrokeKind::Middle);
let bg_idx = ui.painter().add(bg.clone());
let resp = ui.allocate_ui(rect.size(), |ui| {
ui.vertical_centered_justified(|ui| {
@@ -233,7 +230,6 @@ impl WalletTransactions {
// Setup background size.
bg.rect = resp.rect;
ui.painter().set(bg_idx, bg);
true
}
/// Draw [`Modal`] content for this ui container.
@@ -454,7 +450,7 @@ impl WalletTransactions {
/// Show transaction information [`Modal`].
fn show_tx_info_modal(&mut self, tx: &WalletTransaction, finalize: bool) {
let modal = WalletTransactionModal::new(Some(tx.data.id), finalize);
let modal = WalletTransactionContent::new(tx, finalize);
self.tx_info_content = Some(modal);
Modal::new(TX_INFO_MODAL)
.position(ModalPosition::CenterTop)
@@ -529,5 +525,5 @@ fn awaiting_item_ui(ui: &mut egui::Ui, amount: u64, label: String) {
ui.label(RichText::new(label)
.color(Colors::gray())
.size(15.0));
ui.add_space(4.0);
ui.add_space(8.0);
}
+7 -7
View File
@@ -24,14 +24,14 @@ use grin_wallet_libwallet::{Error, Slate, SlateState, TxLogEntryType};
use crate::gui::Colors;
use crate::gui::icons::{BROOM, CHECK, CLIPBOARD_TEXT, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, FilePickButton, Modal, QrCodeContent, View};
use crate::gui::views::{CameraContent, FilePickContent, FilePickContentType, Modal, QrCodeContent, View};
use crate::gui::views::wallets::wallet::txs::WalletTransactions;
use crate::gui::views::wallets::wallet::types::SLATEPACK_MESSAGE_HINT;
use crate::wallet::types::WalletTransaction;
use crate::wallet::Wallet;
/// Transaction information [`Modal`] content.
pub struct WalletTransactionModal {
pub struct WalletTransactionContent {
/// Transaction identifier.
tx_id: u32,
@@ -56,14 +56,14 @@ pub struct WalletTransactionModal {
scan_qr_content: Option<CameraContent>,
/// Button to parse picked file content.
file_pick_button: FilePickButton,
file_pick_button: FilePickContent,
}
impl WalletTransactionModal {
impl WalletTransactionContent {
/// Create new content instance with [`Wallet`] from provided [`WalletTransaction`].
pub fn new(tx_id: Option<u32>, show_finalization: bool) -> Self {
pub fn new(tx: &WalletTransaction, show_finalization: bool) -> Self {
Self {
tx_id: tx_id.unwrap_or(0),
tx_id: tx.data.id,
response_edit: None,
finalize_edit: "".to_string(),
finalize_error: false,
@@ -72,7 +72,7 @@ impl WalletTransactionModal {
final_result: Arc::new(RwLock::new(None)),
qr_code_content: None,
scan_qr_content: None,
file_pick_button: FilePickButton::default(),
file_pick_button: FilePickContent::new(FilePickContentType::Button),
}
}
-4
View File
@@ -57,8 +57,6 @@ pub trait WalletTab {
#[derive(PartialEq)]
pub enum WalletTabType {
Txs,
Messages,
Transport,
Settings
}
@@ -67,8 +65,6 @@ impl WalletTabType {
pub fn name(&self) -> String {
match *self {
WalletTabType::Txs => t!("wallets.txs"),
WalletTabType::Messages => t!("wallets.messages"),
WalletTabType::Transport => t!("wallets.transport"),
WalletTabType::Settings => t!("wallets.settings")
}
}