ui: make list items clickable, ability to delete tx

This commit is contained in:
ardocrat
2026-03-23 01:21:09 +03:00
parent 9bc96de398
commit 6e50b2b38a
28 changed files with 1359 additions and 1206 deletions
+1
View File
@@ -142,6 +142,7 @@ wallets:
payment_proof_desc: 'Geben Sie den erhaltenen Zahlungsnachweis ein, um die Transaktion zu verifizieren:'
payment_proof_valid: 'Der eingegebene Zahlungsnachweis ist gültig:'
payment_proof_error: 'Der eingetragene Zahlungsnachweis ist nicht gültig:'
tx_delete_confirmation: Bist du sicher, dass du die Transaktion aus dem Verlauf löschen möchtest?
transport:
desc: 'Transport verwenden, um Nachrichten synchron zu empfangen oder zu senden:'
tor_network: Tor Netzwek
+1
View File
@@ -142,6 +142,7 @@ wallets:
payment_proof_desc: 'Enter received payment proof to verify transaction:'
payment_proof_valid: 'Entered payment proof is valid:'
payment_proof_error: 'Entered payment proof is not valid:'
tx_delete_confirmation: Are you sure you want to delete the transaction from history?
transport:
desc: 'Use transport to receive or send messages synchronously:'
tor_network: Tor network
+1
View File
@@ -142,6 +142,7 @@ wallets:
payment_proof_desc: 'Saisissez la preuve de paiement reçue pour vérifier la transaction:'
payment_proof_valid: 'La preuve de paiement saisie est valide:'
payment_proof_error: "La preuve de paiement saisie n'est pas valide:"
tx_delete_confirmation: Êtes-vous sûr de vouloir supprimer la transaction de l'historique?
transport:
desc: 'Utilisez le transport pour recevoir ou envoyer des messages de manière synchronisée:'
tor_network: Réseau Tor
+1
View File
@@ -142,6 +142,7 @@ wallets:
payment_proof_desc: 'Введите полученное подтверждение оплаты для проверки транзакции:'
payment_proof_valid: 'Введённое подтверждение оплаты действительно:'
payment_proof_error: 'Введённое подтверждение оплаты недействительно:'
tx_delete_confirmation: Вы уверены, что хотите удалить транзакцию из истории?
transport:
desc: 'Используйте транспорт для синхронных получения или отправки сообщений:'
tor_network: Сеть Tor
+1
View File
@@ -142,6 +142,7 @@ wallets:
payment_proof_desc: 'Islemi doğrulamak için alinan ödeme kanitini girin:'
payment_proof_valid: 'Girilen ödeme kaniti geçerlidir:'
payment_proof_error: 'Girilen ödeme kaniti geçerli değildir:'
tx_delete_confirmation: Islemi geçmişten silmek istediğinizden emin misiniz?
transport:
desc: 'Adresten senkronize GONDER veya AL:'
tor_network: Tor network
+1
View File
@@ -142,6 +142,7 @@ wallets:
payment_proof_desc: '輸入已收款證明以驗證交易:'
payment_proof_valid: '輸入的付款證明有效:'
payment_proof_error: '輸入的付款證明無效:'
tx_delete_confirmation: 你確定要從歷史紀錄中刪除這筆交易嗎?
transport:
desc: '使用传输同步接收或发送消息:'
tor_network: Tor 网络
+4 -4
View File
@@ -44,7 +44,7 @@ pub struct Modal {
/// Flag to check first content render.
first_draw: Arc<AtomicBool>,
/// Background color.
fill: Color32,
fill: Option<Color32>,
}
impl Modal {
@@ -63,7 +63,7 @@ impl Modal {
closeable: Arc::new(AtomicBool::new(true)),
title: None,
first_draw: Arc::new(AtomicBool::new(true)),
fill: Colors::fill(),
fill: None,
}
}
@@ -301,7 +301,7 @@ impl Modal {
/// Set custom background color.
pub fn set_background_color(&self, color: Color32) {
let mut w_state = MODAL_STATE.write();
w_state.modal.as_mut().unwrap().fill = color;
w_state.modal.as_mut().unwrap().fill = Some(color);
}
/// Draw provided content.
@@ -321,7 +321,7 @@ impl Modal {
sw: 8.0 as u8,
se: 8.0 as u8,
}
}, self.fill, Stroke::NONE, StrokeKind::Outside);
}, self.fill.unwrap_or(Colors::fill_lite()), Stroke::NONE, StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
rect.min += egui::emath::vec2(6.0, 0.0);
+147 -111
View File
@@ -12,18 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Align, Layout, RichText, CornerRadius, StrokeKind};
use eframe::epaint::RectShape;
use egui::{Align, Color32, CornerRadius, CursorIcon, Layout, RichText, Sense, StrokeKind, UiBuilder};
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{CARET_RIGHT, CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PENCIL, PLUS_CIRCLE, POWER, TRASH, WARNING_CIRCLE, X_CIRCLE};
use crate::gui::icons::{CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PLUS_CIRCLE, POWER, TRASH, WARNING_CIRCLE, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::network::modals::ExternalConnectionModal;
use crate::gui::views::network::NodeSetup;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{Modal, View};
use crate::gui::Colors;
use crate::node::{Node, NodeConfig};
use crate::wallet::{ConnectionsConfig, ExternalConnection};
use crate::AppConfig;
/// Network connections content.
pub struct ConnectionsContent {
@@ -81,12 +82,9 @@ impl ContentContainer for ConnectionsContent {
}
// Show integrated node info content.
Self::integrated_node_item_ui(ui, |ui| {
// Draw button to show integrated node info.
View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || {
AppConfig::toggle_show_connections_network_panel();
});
});
Self::integrated_node_item_ui(ui, Colors::fill_lite(), (true, || {
AppConfig::toggle_show_connections_network_panel();
}), |_| false);
// Show external connections.
ui.add_space(8.0);
@@ -102,19 +100,19 @@ impl ContentContainer for ConnectionsContent {
ui.add_space(4.0);
let ext_conn_list = ConnectionsConfig::ext_conn_list();
let ext_conn_size = ext_conn_list.len();
if ext_conn_size != 0 {
let len = ext_conn_list.len();
if len != 0 {
ui.add_space(8.0);
for (index, conn) in ext_conn_list.iter().enumerate() {
for (i, c) in ext_conn_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
// Draw connection list item.
Self::ext_conn_item_ui(ui, conn, index, ext_conn_size, |ui| {
let button_rounding = View::item_rounding(index, ext_conn_size, true);
// Draw external connection list item.
let bg = Colors::fill_lite();
Self::ext_conn_item_ui(ui, bg, c, i, len, (true, || {
self.show_add_ext_conn_modal(Some(c.clone()));
}), |ui| {
let button_rounding = View::item_rounding(i, len, true);
View::item_button(ui, button_rounding, TRASH, None, || {
ConnectionsConfig::remove_ext_conn(conn.id);
});
View::item_button(ui, CornerRadius::default(), PENCIL, None, || {
self.show_add_ext_conn_modal(Some(conn.clone()));
ConnectionsConfig::remove_ext_conn(c.id);
});
});
});
@@ -125,118 +123,156 @@ impl ContentContainer for ConnectionsContent {
impl ConnectionsContent {
/// Draw integrated node connection item content.
pub fn integrated_node_item_ui(ui: &mut egui::Ui, custom_button: impl FnOnce(&mut egui::Ui)) {
pub fn integrated_node_item_ui(ui: &mut egui::Ui,
bg: Color32,
on_click: (bool, impl FnOnce()),
custom_button: impl FnOnce(&mut egui::Ui) -> bool) {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
let rounding = View::item_rounding(0, 1, false);
ui.painter().rect(rect, rounding, Colors::fill(), View::item_stroke(), StrokeKind::Outside);
let r = View::item_rounding(0, 1, false);
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw custom button.
custom_button(ui);
let res = ui.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect), |ui| {
// Draw custom button.
let extra_button = custom_button(ui);
// Draw buttons to start/stop node.
if Node::get_error().is_none() {
if !Node::is_running() {
View::item_button(ui, CornerRadius::default(), POWER, Some(Colors::green()), || {
Node::start();
});
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
View::item_button(ui, CornerRadius::default(), POWER, Some(Colors::red()), || {
Node::stop(false);
});
}
}
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!("network.node"))
.size(18.0)
.color(Colors::title(false)));
});
// Setup node status text.
let has_error = Node::get_error().is_some();
let status_icon = if has_error {
WARNING_CIRCLE
} else if !Node::is_running() {
X_CIRCLE
} else if Node::not_syncing() {
CHECK_CIRCLE
// Draw buttons to start/stop node.
if Node::get_error().is_none() {
let rounding = if extra_button {
CornerRadius::default()
} else {
DOTS_THREE_CIRCLE
View::item_rounding(0, 1, true)
};
let status_text = format!("{} {}", status_icon, if has_error {
t!("error").into()
} else {
Node::get_sync_status_text()
});
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
ui.add_space(1.0);
if !Node::is_running() {
View::item_button(ui, rounding, POWER, Some(Colors::green()), || {
Node::start();
});
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
View::item_button(ui, rounding, POWER, Some(Colors::red()), || {
Node::stop(false);
});
}
}
// Setup node API address text.
let api_address = NodeConfig::get_api_address();
let address_text = format!("{} http://{}", COMPUTER_TOWER, api_address);
ui.label(RichText::new(address_text).size(15.0).color(Colors::gray()));
})
});
});
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!("network.node"))
.size(18.0)
.color(Colors::title(false)));
});
// Setup node status text.
let has_error = Node::get_error().is_some();
let status_icon = if has_error {
WARNING_CIRCLE
} else if !Node::is_running() {
X_CIRCLE
} else if Node::not_syncing() {
CHECK_CIRCLE
} else {
DOTS_THREE_CIRCLE
};
let status_text = format!("{} {}", status_icon, if has_error {
t!("error").into()
} else {
Node::get_sync_status_text()
});
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
ui.add_space(1.0);
// Setup node API address text.
let api_address = NodeConfig::get_api_address();
let address_text = format!("{} http://{}", COMPUTER_TOWER, api_address);
ui.label(RichText::new(address_text).size(15.0).color(Colors::gray()));
})
});
}).response;
let (clickable, on_click) = on_click;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if clickable && res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked && clickable {
on_click();
}
}
/// Draw external connection item content.
pub fn ext_conn_item_ui(ui: &mut egui::Ui,
bg: Color32,
conn: &ExternalConnection,
index: usize,
len: usize,
buttons_ui: impl FnOnce(&mut egui::Ui)) {
// Setup layout size.
on_click: (bool, impl FnOnce()),
custom_button: impl FnOnce(&mut egui::Ui)) {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(52.0);
let r = View::item_rounding(index, len, false);
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(index, len, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
let res = ui.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect), |ui| {
// Draw custom button.
custom_button(ui);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw provided buttons.
buttons_ui(ui);
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| {
// Draw connections URL.
ui.add_space(4.0);
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
ui.add_space(1.0);
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| {
// Draw connections URL.
ui.add_space(4.0);
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
ui.add_space(1.0);
// Setup connection status text.
let status_text = if let Some(available) = conn.available {
if available {
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
// Setup connection status text.
let status_text = if let Some(available) = conn.available {
if available {
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
} else {
format!("{} {}", X_CIRCLE, t!("network.not_available"))
}
} else {
format!("{} {}", X_CIRCLE, t!("network.not_available"))
}
} else {
format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check"))
};
ui.label(RichText::new(status_text).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check"))
};
ui.label(RichText::new(status_text).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
});
});
}
).response;
let (clickable, on_click) = on_click;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if clickable && res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked && clickable {
on_click();
}
}
/// Show [`Modal`] to add external connection.
+49 -39
View File
@@ -13,8 +13,8 @@
// limitations under the License.
use eframe::emath::Align;
use eframe::epaint::StrokeKind;
use egui::{Id, Layout, RichText};
use eframe::epaint::{RectShape, StrokeKind};
use egui::{CursorIcon, Id, Layout, RichText, Sense, UiBuilder};
use grin_core::global::ChainTypes;
use crate::gui::icons::{CLOCK_CLOCKWISE, COMPUTER_TOWER, FOLDERS, PENCIL, PLUG, POWER, SHIELD, SHIELD_SLASH};
@@ -173,7 +173,7 @@ impl ContentContainer for NodeSetup {
// Show data location selection for Desktop when it already started or turned off.
if !Node::is_restarting() && !Node::is_stopping() && !Node::is_starting() &&
View::is_desktop() {
self.pick_data_dir_ui(ui, cb);
self.data_dir_ui(ui, cb);
ui.add_space(6.0);
}
@@ -236,46 +236,56 @@ impl ContentContainer for NodeSetup {
impl NodeSetup {
/// Draw content to change chain data directory.
fn pick_data_dir_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Setup layout size.
fn data_dir_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
let r = View::item_rounding(0, 1, false);
let bg = Colors::fill_lite();
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(0, 1, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
self.pick_data_dir.ui(ui, cb, |path| {
Node::change_data_dir(path);
});
View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || {
self.data_path_edit = NodeConfig::get_chain_data_path();
// Show chain data path edit modal.
Modal::new(DATA_PATH_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network.node"))
.show();
});
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.vertical(|ui| {
ui.add_space(4.0);
let path = NodeConfig::get_chain_data_path();
View::ellipsize_text(ui, path, 18.0, Colors::title(false));
ui.add_space(1.0);
let desc = format!("{} {}", FOLDERS, t!("files_location"));
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
ui.add_space(8.0);
let res = ui.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect), |ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
self.pick_data_dir.ui(ui, cb, |path| {
Node::change_data_dir(path);
});
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.vertical(|ui| {
ui.add_space(4.0);
let path = NodeConfig::get_chain_data_path();
View::ellipsize_text(ui, path, 18.0, Colors::title(false));
ui.add_space(1.0);
let desc = format!("{} {}", FOLDERS, t!("files_location"));
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
ui.add_space(8.0);
});
});
});
});
});
}
).response;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked {
self.data_path_edit = NodeConfig::get_chain_data_path();
// Show chain data path edit modal.
Modal::new(DATA_PATH_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network.node"))
.show();
}
}
/// Draw data path input [`Modal`] content.
+1 -1
View File
@@ -786,7 +786,7 @@ fn peer_item_ui(ui: &mut egui::Ui,
let item_rounding = View::item_rounding(index, len, false);
ui.painter().rect(rect,
item_rounding,
Colors::white_or_black(false),
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
+42 -106
View File
@@ -12,12 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, Layout, RichText, ScrollArea, StrokeKind};
use eframe::epaint::RectShape;
use egui::{Align, CursorIcon, Layout, RichText, Sense, StrokeKind, UiBuilder};
use crate::gui::icons::{CHECK, PENCIL, TRANSLATE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::types::ContentContainer;
use crate::gui::views::{Modal, View};
use crate::gui::Colors;
use crate::AppConfig;
@@ -28,22 +27,10 @@ pub struct InterfaceSettingsContent {
locale: String,
}
/// Identifier for language selection [`Modal`].
const LANGUAGE_SELECTION_MODAL: &'static str = "language_selection_modal";
impl ContentContainer for InterfaceSettingsContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
LANGUAGE_SELECTION_MODAL
]
}
fn modal_ids(&self) -> Vec<&'static str> { vec![] }
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, _: &dyn PlatformCallbacks) {
match modal.id {
LANGUAGE_SELECTION_MODAL => self.language_selection_ui(ui),
_ => {}
}
}
fn modal_ui(&mut self, _: &mut egui::Ui, _: &Modal, _: &dyn PlatformCallbacks) {}
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
ui.add_space(5.0);
@@ -72,7 +59,10 @@ impl ContentContainer for InterfaceSettingsContent {
}
// Draw language selection.
self.language_item_ui(self.locale.clone().as_str(), ui, true, 0, 1);
let locales = rust_i18n::available_locales!();
for (index, locale) in locales.iter().enumerate() {
self.language_item_ui(locale, ui, index, locales.len());
}
ui.add_space(4.0);
}
}
@@ -91,96 +81,29 @@ impl Default for InterfaceSettingsContent {
}
impl InterfaceSettingsContent {
/// Draw language selection content.
fn language_selection_ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(4.0);
ScrollArea::vertical()
.max_height(373.0)
.id_salt("select_language_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([true; 2])
.show(ui, |ui| {
ui.add_space(2.0);
ui.vertical_centered(|ui| {
let locales = rust_i18n::available_locales!();
for (index, locale) in locales.iter().enumerate() {
self.language_item_ui(locale, ui, false, index, locales.len());
}
});
});
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
Modal::close();
});
});
ui.add_space(6.0);
}
/// Draw language selection item content.
fn language_item_ui(&mut self, locale: &str, ui: &mut egui::Ui, edit: bool, index: usize, len: usize) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
if edit {
rect.set_height(56.0);
} else {
rect.set_height(50.0);
}
fn language_item_ui(&mut self, locale: &str, ui: &mut egui::Ui, index: usize, len: usize) {
let is_current = self.locale == locale;
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(index, len, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
let mut rect = ui.available_rect_before_wrap();
rect.set_height(50.0);
let r = View::item_rounding(index, len, false);
let bg = if is_current {
Colors::fill()
} else {
Colors::fill_lite()
};
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
if edit {
View::item_button(ui, View::item_rounding(index, len, true), PENCIL, None, || {
// Show language selection modal.
Modal::new(LANGUAGE_SELECTION_MODAL)
.position(ModalPosition::Center)
.title(t!("language"))
.show();
});
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.vertical(|ui| {
ui.add_space(4.0);
View::ellipsize_text(ui,
t!("lang_name", locale = locale),
18.0,
Colors::title(false));
ui.add_space(1.0);
let value = format!("{} {}",
TRANSLATE,
t!("language"));
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
} else {
// Draw button to select language.
let is_current = self.locale == locale;
if !is_current {
View::item_button(ui, View::item_rounding(index, len, true), CHECK, None, || {
rust_i18n::set_locale(locale);
AppConfig::save_locale(locale);
self.locale = locale.to_string();
Modal::close();
});
} else {
let res = ui.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect), |ui| {
if is_current {
View::selected_item_check(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);
@@ -195,10 +118,23 @@ impl InterfaceSettingsContent {
ui.label(RichText::new(t!("lang_name", locale = locale))
.size(17.0)
.color(color));
ui.add_space(3.0);
ui.add_space(14.0);
});
});
}
});
).response;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if res.hovered() && !is_current {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked && !is_current {
rust_i18n::set_locale(locale);
AppConfig::save_locale(locale);
self.locale = locale.to_string();
}
}
}
+63 -54
View File
@@ -12,10 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Align, Id, Layout, RichText, StrokeKind};
use eframe::epaint::RectShape;
use egui::{Align, CursorIcon, Id, Layout, RichText, Sense, StrokeKind, UiBuilder};
use url::Url;
use crate::gui::icons::{CLOUD_CHECK, CLOUD_SLASH, PENCIL};
use crate::gui::icons::{CLOUD_CHECK, CLOUD_SLASH};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{Modal, TextEdit, View};
@@ -202,63 +203,71 @@ impl NetworkSettingsContent {
/// Draw proxy item content.
fn proxy_item_ui(&mut self, ui: &mut egui::Ui) {
// Setup layout size.
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
let r = View::item_rounding(0, 1, false);
let bg = Colors::fill_lite();
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(0, 1, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
let res = ui.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect), |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.vertical(|ui| {
ui.add_space(4.0);
let use_socks = AppConfig::use_socks_proxy();
let proxy_url = if use_socks {
AppConfig::socks_proxy_url()
} else {
AppConfig::http_proxy_url()
};
let (url, color, icon, text) = if let Some(url) = proxy_url {
(url, Colors::title(false), CLOUD_CHECK, t!("network_settings.enabled"))
} else {
(
t!("enter_url").into(),
Colors::inactive_text(),
CLOUD_SLASH,
t!("network_settings.disabled")
)
};
View::ellipsize_text(ui, url, 18.0, color);
ui.add_space(1.0);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
View::item_button(ui, View::item_rounding(0, 1, true), PENCIL, None, || {
let url = if AppConfig::use_socks_proxy() {
AppConfig::socks_proxy_url().unwrap_or("".to_string())
} else {
AppConfig::http_proxy_url().unwrap_or("".to_string())
};
self.proxy_url_edit = url;
// Show proxy URL edit modal.
Modal::new(PROXY_URL_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("app_settings.proxy"))
.show();
});
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.vertical(|ui| {
ui.add_space(4.0);
let use_socks = AppConfig::use_socks_proxy();
let proxy_url = if use_socks {
AppConfig::socks_proxy_url()
} else {
AppConfig::http_proxy_url()
};
let (url, color, icon, text) = if let Some(url) = proxy_url {
(url, Colors::title(false), CLOUD_CHECK, t!("network_settings.enabled"))
} else {
(
t!("enter_url").into(),
Colors::inactive_text(),
CLOUD_SLASH,
t!("network_settings.disabled")
)
};
View::ellipsize_text(ui, url, 18.0, color);
ui.add_space(1.0);
let value = format!("{} {}", icon, text);
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
let value = format!("{} {}", icon, text);
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
});
});
}
).response;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked {
let url = if AppConfig::use_socks_proxy() {
AppConfig::socks_proxy_url().unwrap_or("".to_string())
} else {
AppConfig::http_proxy_url().unwrap_or("".to_string())
};
self.proxy_url_edit = url;
// Show proxy URL edit modal.
Modal::new(PROXY_URL_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("app_settings.proxy"))
.show();
}
}
/// Draw proxy type selection.
+169 -154
View File
@@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::os::OperatingSystem;
use eframe::epaint::RectShape;
use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, Id, Layout, RichText, ScrollArea, StrokeKind};
use egui::{Align, CursorIcon, Id, Layout, RichText, ScrollArea, Sense, StrokeKind, UiBuilder};
use std::fs;
use url::Url;
@@ -194,9 +194,8 @@ impl ContentContainer for TorSettingsContent {
if bridge.is_some() {
ui.add_space(6.0);
// Show bridge selection for non-Android.
let is_android = OperatingSystem::from_target_os() == OperatingSystem::Android;
if !is_android {
// Show bridge selection for desktop.
if View::is_desktop() {
let current_bridge = bridge.unwrap();
let mut bridge = current_bridge.clone();
@@ -234,13 +233,12 @@ impl ContentContainer for TorSettingsContent {
}
if let Some(br) = TorConfig::get_bridge().as_ref() {
// Show bridge binary setup for non-Android.
if !is_android {
self.bridge_bin_ui(ui, br, cb);
ui.add_space(10.0);
}
// Show bridge connection line setup.
self.bridge_conn_line_ui(ui, br, cb);
self.conn_line_ui(ui, br, cb);
// Show bridge binary setup for desktop.
if View::is_desktop() {
self.bridge_bin_ui(ui, br, cb);
}
}
ui.add_space(8.0);
@@ -361,153 +359,61 @@ impl TorSettingsContent {
});
}
/// Draw bridge binary setup content.
fn bridge_bin_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(0, 1, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
self.bridge_bin_pick_file.ui(ui, cb, |path| {
if bridge.binary_path() != path {
TorBridge::save_bridge_bin_path(bridge, path);
self.settings_changed = true;
}
});
View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || {
self.bridge_bin_path_edit = bridge.binary_path();
// Show binary path edit modal.
let title = bridge.protocol_name();
Modal::new(BRIDGE_BIN_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(title)
.show();
});
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.vertical(|ui| {
ui.add_space(4.0);
View::ellipsize_text(ui, bridge.binary_path(), 18.0, Colors::title(false));
ui.add_space(1.0);
let value = format!("{} {}",
TERMINAL,
t!("transport.bin_file").replace(":", ""));
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
});
}
/// Draw bridge binary input [`Modal`] content.
fn bridge_bin_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut TorSettingsContent| {
let bridge = TorConfig::get_bridge().unwrap();
let exists = fs::exists(&c.bridge_bin_path_edit).unwrap_or_default();
if !exists {
return;
}
if bridge.binary_path() != c.bridge_bin_path_edit {
TorBridge::save_bridge_bin_path(&bridge, c.bridge_bin_path_edit.clone());
c.settings_changed = true;
}
Modal::close();
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.bin_file"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw bridge text edit.
let mut edit = TextEdit::new(Id::from(BRIDGE_BIN_EDIT_MODAL)).paste();
edit.ui(ui, &mut self.bridge_bin_path_edit, cb);
if edit.enter_pressed {
on_save(self);
}
ui.add_space(12.0);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
});
}
/// Draw bridge connection line setup content.
fn bridge_conn_line_ui(&mut self,
ui: &mut egui::Ui,
bridge: &TorBridge,
cb: &dyn PlatformCallbacks) {
// Setup layout size.
fn conn_line_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
let r = if View::is_desktop() {
View::item_rounding(0, 2, false)
} else {
View::item_rounding(0, 1, false)
};
let bg = Colors::fill_lite();
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(0, 1, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
View::item_button(ui, View::item_rounding(0, 1, true), SCAN, None, || {
self.show_qr_scan_bridge_modal(cb);
});
View::item_button(ui, View::item_rounding(1, 3 , true), PENCIL, None, || {
self.bridge_conn_line_edit = bridge.connection_line();
// Show connection line edit modal.
let title = bridge.protocol_name();
Modal::new(BRIDGE_CONN_LINE_EDIT_MODAL)
.position(ModalPosition::Center)
.title(title)
.show();
});
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.vertical(|ui| {
ui.add_space(4.0);
View::ellipsize_text(ui, bridge.connection_line(), 18.0, Colors::title(false));
ui.add_space(1.0);
let value = format!("{} {}",
NOTCHES,
t!("transport.conn_line").replace(":", ""));
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
let res = ui.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect), |ui| {
View::item_button(ui, View::item_rounding(0, 1, true), SCAN, None, || {
self.show_qr_scan_bridge_modal(cb);
});
});
});
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.vertical(|ui| {
ui.add_space(4.0);
let line_text = bridge.connection_line();
View::ellipsize_text(ui, line_text, 18.0, Colors::title(false));
ui.add_space(1.0);
let line_desc = t!("transport.conn_line").replace(":", "");
let value = format!("{} {}", NOTCHES, line_desc);
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
}
).response;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked {
self.bridge_conn_line_edit = bridge.connection_line();
// Show connection line edit modal.
let title = bridge.protocol_name();
Modal::new(BRIDGE_CONN_LINE_EDIT_MODAL)
.position(ModalPosition::Center)
.title(title)
.show();
}
}
/// Show bridge connection line QR code scanner.
@@ -557,7 +463,7 @@ impl TorSettingsContent {
.id(input_id)
.font(egui::TextStyle::Body)
.desired_rows(5)
.interactive(true)
.interactive(false)
.desired_width(f32::INFINITY)
.show(ui);
ui.add_space(6.0);
@@ -614,4 +520,113 @@ impl TorSettingsContent {
});
});
}
/// Draw bridge binary setup content.
fn bridge_bin_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
let r = View::item_rounding(1, 2, false);
let bg = Colors::fill_lite();
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
let res = ui.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect), |ui| {
self.bridge_bin_pick_file.ui(ui, cb, |path| {
if bridge.binary_path() != path {
TorBridge::save_bridge_bin_path(bridge, path);
self.settings_changed = true;
}
});
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.vertical(|ui| {
ui.add_space(4.0);
let bin_text = bridge.binary_path();
View::ellipsize_text(ui, bin_text, 18.0, Colors::title(false));
ui.add_space(1.0);
let bin_desc = t!("transport.bin_file").replace(":", "");
let value = format!("{} {}", TERMINAL, bin_desc);
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
}
).response;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked {
self.bridge_bin_path_edit = bridge.binary_path();
// Show binary path edit modal.
let title = bridge.protocol_name();
Modal::new(BRIDGE_BIN_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(title)
.show();
}
}
/// Draw bridge binary input [`Modal`] content.
fn bridge_bin_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut TorSettingsContent| {
let bridge = TorConfig::get_bridge().unwrap();
let exists = fs::exists(&c.bridge_bin_path_edit).unwrap_or_default();
if !exists {
return;
}
if bridge.binary_path() != c.bridge_bin_path_edit {
TorBridge::save_bridge_bin_path(&bridge, c.bridge_bin_path_edit.clone());
c.settings_changed = true;
}
Modal::close();
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.bin_file"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw bridge text edit.
let mut edit = TextEdit::new(Id::from(BRIDGE_BIN_EDIT_MODAL)).paste();
edit.ui(ui, &mut self.bridge_bin_path_edit, cb);
if edit.enter_pressed {
on_save(self);
}
ui.add_space(12.0);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
});
}
}
+3 -1
View File
@@ -186,6 +186,8 @@ impl View {
// Setup padding for title buttons.
if !View::is_desktop() {
ui.style_mut().spacing.button_padding = egui::vec2(20.0, 8.0);
} else {
ui.style_mut().spacing.button_padding = egui::vec2(16.0, 8.0);
}
// Disable strokes.
ui.style_mut().visuals.widgets.inactive.bg_stroke = Stroke::NONE;
@@ -358,7 +360,7 @@ impl View {
ui.scope(|ui| {
// Setup padding for item buttons.
let padding = if Self::is_desktop() {
14.0
15.0
} else {
18.0
};
+101 -75
View File
@@ -12,27 +12,29 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::time::Duration;
use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, CornerRadius, Id, Layout, Margin, OpenUrl, RichText, ScrollArea, StrokeKind};
use egui::os::OperatingSystem;
use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, CornerRadius, CursorIcon, Id, Layout, Margin, OpenUrl, RichText, ScrollArea, Sense, StrokeKind, UiBuilder};
use egui_async::Bind;
use crate::gui::icons::{ARROW_LEFT, BOOKMARKS, CALENDAR_CHECK, CARET_RIGHT, CLOUD_ARROW_DOWN, COMPUTER_TOWER, FOLDER_OPEN, FOLDER_PLUS, GEAR, GEAR_FINE, GLOBE, GLOBE_SIMPLE, LOCK_KEY, NOTEPAD, PLUS, SIDEBAR_SIMPLE, SUITCASE};
use std::time::Duration;
use eframe::epaint::RectShape;
use crate::gui::icons::{ARROW_LEFT, BOOKMARKS, CALENDAR_CHECK, CLOUD_ARROW_DOWN, COMPUTER_TOWER, FOLDER_PLUS, GEAR, GEAR_FINE, GLOBE, GLOBE_SIMPLE, LOCK_KEY, NOTEPAD, PLUS, SIDEBAR_SIMPLE, SUITCASE};
use crate::gui::platform::PlatformCallbacks;
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, WalletSettingsModal, WalletListModal, ChangelogContent};
use crate::gui::views::wallets::modals::{AddWalletModal, ChangelogContent, OpenWalletModal, WalletListModal, WalletSettingsModal};
use crate::gui::views::wallets::wallet::types::{wallet_status_text, WalletContentContainer};
use crate::gui::views::wallets::wallet::RecoverySettings;
use crate::gui::views::wallets::WalletContent;
use crate::gui::views::{Content, Modal, TitlePanel, View};
use crate::gui::Colors;
use crate::http::{retrieve_release, ReleaseInfo};
use crate::settings::AppUpdate;
use crate::wallet::types::{ConnectionMethod, WalletTask};
use crate::wallet::{Wallet, WalletList};
use crate::AppConfig;
use crate::gui::views::wallets::wallet::RecoverySettings;
use crate::http::{retrieve_release, ReleaseInfo};
use crate::settings::AppUpdate;
/// Wallets content.
pub struct WalletsContent {
@@ -444,7 +446,12 @@ impl WalletsContent {
|| (dual_panel && !show_list)) && !creating_wallet && !showing_settings {
let title = self.wallet_content.title().into();
let subtitle = self.wallets.selected().unwrap().get_config().name;
TitleType::Single(TitleContentType::WithSubTitle(title, subtitle, false))
let wallet_title_content = if self.wallet_content.settings_content.is_some() {
TitleContentType::Title(title)
} else {
TitleContentType::WithSubTitle(title, subtitle, false)
};
TitleType::Single(wallet_title_content)
} else {
let title_text = if showing_settings {
t!("settings")
@@ -458,7 +465,11 @@ impl WalletsContent {
if dual_title {
let title = self.wallet_content.title().into();
let subtitle = self.wallets.selected().unwrap().get_config().name;
let wallet_title_content = TitleContentType::WithSubTitle(title, subtitle, false);
let wallet_title_content = if self.wallet_content.settings_content.is_some() {
TitleContentType::Title(title)
} else {
TitleContentType::WithSubTitle(title, subtitle, false)
};
TitleType::Dual(TitleContentType::Title(title_text), wallet_title_content)
} else {
TitleType::Single(TitleContentType::Title(title_text))
@@ -579,8 +590,12 @@ impl WalletsContent {
} else {
false
};
// Unselect wallet when opening or settings modal was closed.
if current && !w.is_open() && Modal::opened().is_none() {
self.wallets.select(None);
}
self.wallet_item_ui(ui, w, current, cb);
ui.add_space(5.0);
ui.add_space(6.0);
}
});
});
@@ -593,88 +608,99 @@ impl WalletsContent {
current: bool,
cb: &dyn PlatformCallbacks) {
let config = wallet.get_config();
let can_open = !wallet.is_open() && !wallet.files_moving();
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
let rounding = View::item_rounding(0, 1, false);
let r = View::item_rounding(0, 1, false);
let bg = if current {
Colors::fill_deep()
} else {
Colors::fill()
};
ui.painter().rect(rect, rounding, bg, View::item_stroke(), StrokeKind::Outside);
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
if !wallet.is_open() && !wallet.files_moving() {
// Show button to open closed wallet.
View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || {
self.show_opening_modal(wallet, None, cb);
});
if !wallet.is_repairing() {
View::item_button(ui, CornerRadius::default(), GEAR_FINE, None, || {
self.select_wallet(wallet, None, cb);
let conn = wallet.get_current_connection();
self.wallet_settings_content = WalletSettingsModal::new(conn);
// Show connection selection modal.
Modal::new(WALLET_SETTINGS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.settings"))
.show();
});
}
} else {
if !current {
// Show button to select opened wallet.
View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || {
self.select_wallet(wallet, None, cb);
});
}
// Show button to close opened wallet.
if !wallet.is_closing() {
View::item_button(ui, if !current {
CornerRadius::default()
} else {
View::item_rounding(0, 1, true)
}, LOCK_KEY, None, || {
let res = ui.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect), |ui| {
if can_open {
if !wallet.is_repairing() {
View::item_button(ui, View::item_rounding(0, 1, true), GEAR_FINE, None, || {
self.select_wallet(wallet, None, cb);
let conn = wallet.get_current_connection();
self.wallet_settings_content = WalletSettingsModal::new(conn);
// Show connection selection modal.
Modal::new(WALLET_SETTINGS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.settings"))
.show();
});
}
} else if !wallet.is_closing() {
// Show button to close opened wallet.
View::item_button(ui, View::item_rounding(0, 1, true), LOCK_KEY, None, || {
wallet.close();
});
}
}
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 name text.
let name_color = if current {
Colors::white_or_black(true)
} else {
Colors::title(false)
};
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
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 name text.
let name_color = if current {
Colors::white_or_black(true)
} else {
Colors::title(false)
};
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
View::ellipsize_text(ui, config.name, 18.0, name_color);
});
// Show wallet status text.
let status_text = wallet_status_text(wallet);
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
ui.add_space(1.0);
// Show wallet connection text.
let connection = wallet.get_current_connection();
let conn_text = match connection {
ConnectionMethod::Integrated => {
format!("{} {}", COMPUTER_TOWER, t!("network.node"))
}
ConnectionMethod::External(_, url) => {
format!("{} {}", GLOBE_SIMPLE, url)
}
};
View::ellipsize_text(ui, conn_text, 15.0, Colors::gray());
ui.add_space(3.0);
});
// Show wallet status text.
View::ellipsize_text(ui, wallet_status_text(wallet), 15.0, Colors::text(false));
ui.add_space(1.0);
// Show wallet connection text.
let connection = wallet.get_current_connection();
let conn_text = match connection {
ConnectionMethod::Integrated => {
format!("{} {}", COMPUTER_TOWER, t!("network.node"))
}
ConnectionMethod::External(_, url) => format!("{} {}", GLOBE_SIMPLE, url)
};
View::ellipsize_text(ui, conn_text, 15.0, Colors::gray());
ui.add_space(3.0);
});
});
});
}
).response;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if res.hovered() && (can_open || !current) {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill_deep();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked {
if can_open {
// Show modal to open the wallet.
self.show_opening_modal(wallet, None, cb);
} else if !current {
// Select opened wallet.
self.select_wallet(wallet, None, cb);
}
}
}
/// Draw update information content.
+38 -34
View File
@@ -15,7 +15,7 @@
use egui::scroll_area::ScrollBarVisibility;
use egui::{RichText, ScrollArea};
use crate::gui::icons::{CHECK, PLUS_CIRCLE, TRASH};
use crate::gui::icons::{PLUS_CIRCLE, TRASH};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::network::modals::ExternalConnectionModal;
use crate::gui::views::network::ConnectionsContent;
@@ -79,18 +79,20 @@ impl WalletSettingsModal {
ui.add_space(2.0);
// Show integrated node selection.
ConnectionsContent::integrated_node_item_ui(ui, |ui| {
match self.conn {
ConnectionMethod::Integrated => {
View::selected_item_check(ui);
}
_ => {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
on_select(ConnectionMethod::Integrated);
Modal::close();
});
}
let cur_integrated = self.conn == ConnectionMethod::Integrated;
let bg = if cur_integrated {
Colors::fill()
} else {
Colors::fill_lite()
};
ConnectionsContent::integrated_node_item_ui(ui, bg, (!cur_integrated, || {
on_select(ConnectionMethod::Integrated);
Modal::close();
}), |ui| {
if cur_integrated {
View::selected_item_check(ui);
}
cur_integrated
});
ui.add_space(8.0);
@@ -109,29 +111,31 @@ impl WalletSettingsModal {
if !ext_conn_list.is_empty() {
ui.add_space(8.0);
for (index, conn) in ext_conn_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
let len = ext_conn_list.len();
ConnectionsContent::ext_conn_item_ui(ui, conn, index, len, |ui| {
let current_ext_conn = match self.conn {
ConnectionMethod::Integrated => false,
ConnectionMethod::External(id, _) => id == conn.id
};
if current_ext_conn {
View::selected_item_check(ui);
} else {
let button_rounding = View::item_rounding(index, len, true);
View::item_button(ui, button_rounding, CHECK, None, || {
on_select(
ConnectionMethod::External(conn.id, conn.url.clone())
);
Modal::close();
});
}
});
});
}
}
for (i, c) in ext_conn_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
let len = ext_conn_list.len();
let is_current = match self.conn {
ConnectionMethod::External(id, _) => id == c.id,
_ => false
};
let bg = if is_current {
Colors::fill()
} else {
Colors::fill_lite()
};
ConnectionsContent::ext_conn_item_ui(ui, bg, c, i, len, (!is_current, || {
on_select(
ConnectionMethod::External(c.id, c.url.clone())
);
Modal::close();
}), |ui| {
if is_current {
View::selected_item_check(ui);
}
});
});
}
ui.add_space(4.0);
});
+2 -2
View File
@@ -256,7 +256,7 @@ impl WalletContentContainer for WalletContent {
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
let show_settings = self.settings_content.is_some();
let show_txs = self.txs_content.is_some();
let show_txs = self.txs_content.is_some() && !top_panel_expanded;
let show_sync = (!show_settings || block_nav) &&
sync_ui(ui, &wallet);
if !show_sync {
@@ -457,7 +457,7 @@ impl WalletContent {
None => {
// Show transaction modal on wallet task result.
if let Some(id) = id {
let tx = wallet.get_data().unwrap().tx_by_slate_id(id);
let tx = wallet.get_data().unwrap().tx_by_id(id);
if tx.is_some() {
self.txs_content = Some(WalletTransactionsContent::new(tx));
self.settings_content = None;
+2 -2
View File
@@ -7,7 +7,7 @@ use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, COPY, FILE_TEXT, SEAL_CHECK};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{FilePickContent, FilePickContentType, Modal, View};
use crate::gui::Colors;
use crate::wallet::types::{WalletTask, WalletTransaction};
use crate::wallet::types::{WalletTask, WalletTx};
use crate::wallet::Wallet;
pub struct PaymentProofContent {
@@ -154,7 +154,7 @@ impl PaymentProofContent {
/// Draw transaction payment proof content to share.
pub fn share_ui(&mut self,
ui: &mut egui::Ui,
tx: &WalletTransaction,
tx: &WalletTx,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
+106 -86
View File
@@ -13,10 +13,10 @@
// limitations under the License.
use eframe::emath::Align;
use eframe::epaint::StrokeKind;
use egui::{Id, Layout, RichText};
use eframe::epaint::{RectShape, StrokeKind};
use egui::{CursorIcon, Id, Layout, RichText, Sense, UiBuilder};
use crate::gui::icons::{CLOCK_COUNTDOWN, FOLDERS, FOLDER_USER, PASSWORD, PENCIL};
use crate::gui::icons::{CLOCK_COUNTDOWN, FOLDERS, FOLDER_USER, PASSWORD};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::wallet::types::WalletContentContainer;
@@ -79,7 +79,11 @@ impl WalletContentContainer for CommonSettings {
}
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
ui.add_space(8.0);
if View::is_desktop() {
ui.add_space(1.0);
} else {
ui.add_space(8.0);
}
ui.vertical_centered(|ui| {
let config = wallet.get_config();
// Show wallet name.
@@ -106,7 +110,7 @@ impl WalletContentContainer for CommonSettings {
ui.add_space(8.0);
// Setup ability to post wallet transactions with Dandelion.
// Ability to post wallet transactions with Dandelion.
View::checkbox(ui, wallet.can_use_dandelion(), t!("wallets.use_dandelion"), || {
wallet.update_use_dandelion(!wallet.can_use_dandelion());
});
@@ -137,60 +141,68 @@ impl Default for CommonSettings {
impl CommonSettings {
/// Draw content to change wallet name and password.
fn name_ui(&mut self, ui: &mut egui::Ui, name: String) {
// Setup layout size.
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = if View::is_desktop() {
let r = if View::is_desktop() {
View::item_rounding(0, 2, false)
} else {
View::item_rounding(0, 1, false)
};
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
let bg = Colors::fill_lite();
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
let r = if View::is_desktop() {
View::item_rounding(0, 2, true)
} else {
View::item_rounding(0, 1, true)
};
View::item_button(ui, r, PASSWORD, None, || {
self.old_pass_edit = "".to_string();
self.new_pass_edit = "".to_string();
self.wrong_pass = false;
// Show wallet password modal.
Modal::new(PASS_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.wallet"))
.show();
});
View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || {
self.name_edit = name.clone();
// Show wallet name modal.
Modal::new(NAME_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.wallet"))
.show();
});
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.vertical(|ui| {
ui.add_space(4.0);
View::ellipsize_text(ui, name, 18.0, Colors::title(false));
ui.add_space(1.0);
let desc = format!("{} {}", FOLDER_USER, t!("wallets.name").replace(":", ""));
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
ui.add_space(8.0);
let res = ui.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect), |ui| {
let r = if View::is_desktop() {
View::item_rounding(0, 2, true)
} else {
View::item_rounding(0, 1, true)
};
View::item_button(ui, r, PASSWORD, None, || {
self.old_pass_edit = "".to_string();
self.new_pass_edit = "".to_string();
self.wrong_pass = false;
// Show wallet password modal.
Modal::new(PASS_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.wallet"))
.show();
});
});
});
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.vertical(|ui| {
ui.add_space(4.0);
View::ellipsize_text(ui, name.clone(), 18.0, Colors::title(false));
ui.add_space(1.0);
let desc = format!("{} {}", FOLDER_USER, t!("wallets.name").replace(":", ""));
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
ui.add_space(8.0);
});
});
}
).response;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked {
self.name_edit = name;
// Show wallet name modal.
Modal::new(NAME_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.wallet"))
.show();
}
}
/// Draw wallet name [`Modal`] content.
@@ -340,45 +352,53 @@ impl CommonSettings {
/// Draw content to change wallet data directory.
fn data_dir_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
// Setup layout size.
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
let r = View::item_rounding(1, 2, false);
let bg = Colors::fill_lite();
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
// 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::Outside);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
self.pick_data_dir.ui(ui, cb, |path| {
wallet.change_data_path(path);
});
View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || {
self.data_path_edit = wallet.get_config().data_path.unwrap_or_default();
// Show chain data path edit modal.
Modal::new(DATA_PATH_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.wallet"))
.show();
});
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.vertical(|ui| {
ui.add_space(4.0);
let path = wallet.get_config().data_path.unwrap_or_default();
View::ellipsize_text(ui, path, 18.0, Colors::title(false));
ui.add_space(1.0);
let desc = format!("{} {}", FOLDERS, t!("files_location"));
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
ui.add_space(8.0);
let res = ui.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect), |ui| {
self.pick_data_dir.ui(ui, cb, |path| {
wallet.change_data_path(path);
});
});
});
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.vertical(|ui| {
ui.add_space(4.0);
let path = wallet.get_config().data_path.unwrap_or_default();
View::ellipsize_text(ui, path, 18.0, Colors::title(false));
ui.add_space(1.0);
let desc = format!("{} {}", FOLDERS, t!("files_location"));
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
ui.add_space(8.0);
});
});
}
).response;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked {
self.data_path_edit = wallet.get_config().data_path.unwrap_or_default();
// Show chain data path edit modal.
Modal::new(DATA_PATH_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.wallet"))
.show();
}
}
/// Draw data path input [`Modal`] content.
@@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Align, Layout, RichText, StrokeKind};
use egui::RichText;
use crate::gui::icons::{CHECK, CHECK_CIRCLE, DOTS_THREE_CIRCLE, GLOBE, GLOBE_SIMPLE, PLUS_CIRCLE, X_CIRCLE};
use crate::gui::icons::{GLOBE, PLUS_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::network::modals::ExternalConnectionModal;
use crate::gui::views::network::ConnectionsContent;
@@ -70,15 +70,19 @@ impl ContentContainer for ConnectionSettings {
ui.vertical_centered(|ui| {
ui.add_space(6.0);
// Show integrated node selection.
ConnectionsContent::integrated_node_item_ui(ui, |ui| {
let is_current_method = self.method == ConnectionMethod::Integrated;
if !is_current_method {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
self.method = ConnectionMethod::Integrated;
});
} else {
let cur_integrated = self.method == ConnectionMethod::Integrated;
let bg = if cur_integrated {
Colors::fill_deep()
} else {
Colors::fill_lite()
};
ConnectionsContent::integrated_node_item_ui(ui, bg, (!cur_integrated, || {
self.method = ConnectionMethod::Integrated;
}), |ui| {
if cur_integrated {
View::selected_item_check(ui);
}
cur_integrated
});
ui.add_space(8.0);
@@ -119,8 +123,8 @@ impl ContentContainer for ConnectionSettings {
}
}
let ext_size = ext_conn_list.len();
if ext_size != 0 {
let len = ext_conn_list.len();
if len != 0 {
ui.add_space(8.0);
for (i, c) in ext_conn_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
@@ -129,73 +133,21 @@ impl ContentContainer for ConnectionSettings {
ConnectionMethod::External(id, url) => id == &c.id || url == &c.url,
_ => false
};
Self::ext_conn_item_ui(ui, c, is_current, i, ext_size, || {
let bg = if is_current {
Colors::fill()
} else {
Colors::fill_lite()
};
ConnectionsContent::ext_conn_item_ui(ui, bg, c, i, len, (!is_current, || {
self.method = ConnectionMethod::External(c.id, c.url.clone());
}), |ui| {
if is_current {
View::selected_item_check(ui);
}
});
});
}
}
});
}
}
impl ConnectionSettings {
/// Draw external connection item content.
fn ext_conn_item_ui(ui: &mut egui::Ui,
conn: &ExternalConnection,
is_current: bool,
index: usize,
len: usize,
mut on_select: impl FnMut()) {
// 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 = View::item_rounding(index, len, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
if is_current {
View::selected_item_check(ui);
} else {
// Draw button to select connection.
let button_rounding = View::item_rounding(index, len, true);
View::item_button(ui, button_rounding, CHECK, None, || {
on_select();
});
}
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| {
// Draw connections URL.
ui.add_space(4.0);
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
ui.add_space(1.0);
// Setup connection status text.
let status_text = if let Some(available) = conn.available {
if available {
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
} else {
format!("{} {}", X_CIRCLE, t!("network.not_available"))
}
} else {
format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check"))
};
ui.label(RichText::new(status_text).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
});
});
}
}
+312 -208
View File
@@ -14,21 +14,21 @@
use egui::epaint::RectShape;
use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, CornerRadius, Id, Layout, Rect, RichText, ScrollArea, StrokeKind};
use egui::{Align, Color32, CornerRadius, CursorIcon, Id, Layout, Rect, RichText, ScrollArea, Sense, StrokeKind, UiBuilder};
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::icons::{ARROWS_CLOCKWISE, ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, CALENDAR_CHECK, DOTS_THREE_CIRCLE, FILE_ARROW_DOWN, FILE_TEXT, GEAR_FINE, PROHIBIT, WARNING, X_CIRCLE};
use crate::gui::icons::{ARROWS_CLOCKWISE, ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, CALENDAR_CHECK, DOTS_THREE_CIRCLE, FILE_ARROW_DOWN, FILE_TEXT, FILE_X, GEAR_FINE, PROHIBIT, WARNING, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{LinePosition, ModalPosition};
use crate::gui::views::wallets::wallet::types::{WalletContentContainer, 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, WalletTask, WalletTransaction, WalletTransactionAction};
use crate::wallet::types::{WalletData, WalletTask, WalletTx, WalletTxAction};
use crate::wallet::Wallet;
/// Wallet transactions tab content.
@@ -36,8 +36,10 @@ pub struct WalletTransactionsContent {
/// Transaction information [`Modal`] content.
pub tx_info_content: Option<WalletTransactionContent>,
/// Transaction identifier to use at confirmation [`Modal`].
/// Transaction identifier to use at confirmation [`Modal`] to cancel.
confirm_cancel_tx_id: Option<u32>,
/// Transaction identifier to use at confirmation [`Modal`] to delete.
confirm_delete_tx_id: Option<u32>,
/// Flag to check if sync of wallet was initiated manually at time.
manual_sync: Option<u128>
@@ -45,19 +47,28 @@ pub struct WalletTransactionsContent {
impl WalletContentContainer for WalletTransactionsContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![TX_INFO_MODAL, CANCEL_TX_CONFIRMATION_MODAL]
vec![TX_INFO_MODAL, CANCEL_TX_CONFIRMATION_MODAL, DELETE_TX_CONFIRMATION_MODAL]
}
fn modal_ui(&mut self, ui: &mut egui::Ui, w: &Wallet, m: &Modal, cb: &dyn PlatformCallbacks) {
match m.id {
TX_INFO_MODAL => {
if let Some(content) = self.tx_info_content.as_mut() {
content.ui(ui, m, w, cb);
let mut on_delete_id = None;
content.ui(ui, m, w, cb, |id| {
on_delete_id = Some(id);
});
if let Some(id) = on_delete_id {
self.show_delete_confirmation_modal(id);
}
}
}
CANCEL_TX_CONFIRMATION_MODAL => {
self.cancel_confirmation_modal(ui, w);
}
DELETE_TX_CONFIRMATION_MODAL => {
self.delete_confirmation_modal(ui, w);
}
_ => {}
}
}
@@ -69,19 +80,21 @@ impl WalletContentContainer for WalletTransactionsContent {
/// Identifier for transaction information [`Modal`].
const TX_INFO_MODAL: &'static str = "tx_info_modal";
/// Identifier for transaction cancellation confirmation [`Modal`].
const CANCEL_TX_CONFIRMATION_MODAL: &'static str = "cancel_tx_conf_modal";
/// Identifier for transaction deletion confirmation [`Modal`].
const DELETE_TX_CONFIRMATION_MODAL: &'static str = "delete_tx_conf_modal";
impl WalletTransactionsContent {
/// Height of transaction list item.
pub const TX_ITEM_HEIGHT: f32 = 75.0;
pub const TX_ITEM_HEIGHT: f32 = 73.0;
/// Create new content instance with opening tx info.
pub fn new(tx: Option<WalletTransaction>) -> Self {
pub fn new(tx: Option<WalletTx>) -> Self {
let mut content = Self {
tx_info_content: None,
confirm_cancel_tx_id: None,
confirm_delete_tx_id: None,
manual_sync: None,
};
if let Some(tx) = &tx {
@@ -100,7 +113,10 @@ impl WalletTransactionsContent {
});
return;
}
let txs = data.txs.as_ref().unwrap();
let txs = data.txs.as_ref().unwrap()
.iter()
.filter(|tx| !tx.deleting())
.collect::<Vec<&WalletTx>>();
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show message when txs are empty.
if txs.is_empty() {
@@ -162,7 +178,7 @@ impl WalletTransactionsContent {
ui: &mut egui::Ui,
row_range: Range<usize>,
wallet: &Wallet,
txs: &Vec<WalletTransaction>) {
txs: Vec<&WalletTx>) {
let data = wallet.get_data().unwrap();
for index in row_range {
if index == txs.len() && ui.is_visible() {
@@ -183,34 +199,40 @@ impl WalletTransactionsContent {
}
return;
}
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(), View::item_stroke(), StrokeKind::Outside);
// Transaction item background setup.
let rect = {
let mut r = ui.available_rect_before_wrap();
r.min += egui::emath::vec2(6.0, 0.0);
r.max -= egui::emath::vec2(6.0, 0.0);
r.set_height(Self::TX_ITEM_HEIGHT);
r
};
let rounding = View::item_rounding(index, txs.len(), false);
let bg = Colors::fill();
let tx = txs.get(index).unwrap();
Self::tx_item_ui(ui, tx, rect, &data, |ui| {
// Draw button to show transaction info.
if tx.data.tx_slate_id.is_some() || tx.data.payment_proof.is_some() {
let mut show_tx_info = false;
// Draw transaction list item.
Self::tx_item_ui(ui, tx, rect, bg, rounding, &data, (true, || {
show_tx_info = true;
}), |ui| {
let btn_rounding = {
let mut r = rounding.clone();
r.nw = 0.0 as u8;
r.sw = 0.0 as u8;
View::item_button(ui, r, FILE_TEXT, None, || {
self.show_tx_info_modal(tx.data.id);
r
};
// Draw button to delete transaction.
if tx.data.confirmed || tx.cancelled() {
View::item_button(ui, btn_rounding, FILE_X, Some(Colors::inactive_text()), || {
self.show_delete_confirmation_modal(tx.data.id);
});
}
if wallet.synced_from_node() && !tx.cancelled() && !tx.cancelling() && !tx.posting() {
let resend = tx.broadcasting_timed_out(wallet);
} else if !tx.cancelled() && !tx.cancelling() && !tx.posting() &&
wallet.synced_from_node() {
let repeat = tx.broadcasting_timed_out(wallet);
// Draw button to cancel transaction.
if tx.can_cancel() || resend {
if tx.can_cancel() || repeat {
let (icon, color) = (PROHIBIT, Some(Colors::red()));
View::item_button(ui, CornerRadius::default(), icon, color, || {
View::item_button(ui, btn_rounding, icon, color, || {
self.confirm_cancel_tx_id = Some(tx.data.id);
// Show transaction cancellation confirmation modal.
Modal::new(CANCEL_TX_CONFIRMATION_MODAL)
@@ -219,13 +241,16 @@ impl WalletTransactionsContent {
.show();
});
}
// Draw button to repeat transaction action.
if tx.can_repeat_action() || resend {
Self::tx_repeat_button_ui(ui, CornerRadius::default(), tx, wallet, resend);
if tx.can_repeat_action() || repeat {
Self::tx_repeat_button_ui(ui, CornerRadius::default(), tx, wallet, repeat);
}
}
});
// Show transaction info on click.
if show_tx_info {
self.show_tx_info_modal(tx.data.id);
}
}
}
@@ -272,92 +297,135 @@ impl WalletTransactionsContent {
/// Draw transaction item.
pub fn tx_item_ui(ui: &mut egui::Ui,
tx: &WalletTransaction,
tx: &WalletTx,
rect: Rect,
bg: Color32,
r: CornerRadius,
data: &WalletData,
on_click: (bool, impl FnOnce()),
buttons_ui: impl FnOnce(&mut egui::Ui)) {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |ui| {
ui.horizontal_centered(|ui| {
// Draw buttons.
buttons_ui(ui);
});
// Draw background.
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
let res = ui.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect), |ui| {
ui.horizontal_centered(|ui| {
// Draw buttons.
buttons_ui(ui);
});
ui.with_layout(Layout::left_to_right(Align::Min), |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(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Setup transaction amount.
let mut amount_text = if tx.data.tx_type == TxLogEntryType::TxSent ||
tx.data.tx_type == TxLogEntryType::TxSentCancelled {
"-"
} else if tx.data.tx_type == TxLogEntryType::TxReceived ||
tx.data.tx_type == TxLogEntryType::TxReceivedCancelled {
"+"
} else {
""
}.to_string();
amount_text = format!("{}{} {}",
amount_text,
amount_to_hr_string(tx.amount, true),
GRIN);
// Setup transaction amount.
let mut amount_text = if tx.data.tx_type == TxLogEntryType::TxSent ||
tx.data.tx_type == TxLogEntryType::TxSentCancelled {
"-"
} else if tx.data.tx_type == TxLogEntryType::TxReceived ||
tx.data.tx_type == TxLogEntryType::TxReceivedCancelled {
"+"
} else {
""
}.to_string();
amount_text = format!("{}{} {}",
amount_text,
amount_to_hr_string(tx.amount, true),
GRIN);
// Setup amount color.
let amount_color = match tx.data.tx_type {
TxLogEntryType::ConfirmedCoinbase => Colors::white_or_black(true),
TxLogEntryType::TxReceived => Colors::white_or_black(true),
TxLogEntryType::TxSent => Colors::white_or_black(true),
TxLogEntryType::TxReceivedCancelled => Colors::text(false),
TxLogEntryType::TxSentCancelled => Colors::text(false),
TxLogEntryType::TxReverted => Colors::text(false)
};
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
View::ellipsize_text(ui, amount_text, 18.0, amount_color);
});
ui.add_space(-2.0);
// Setup amount color.
let amount_color = match tx.data.tx_type {
TxLogEntryType::ConfirmedCoinbase => Colors::white_or_black(true),
TxLogEntryType::TxReceived => Colors::white_or_black(true),
TxLogEntryType::TxSent => Colors::white_or_black(true),
TxLogEntryType::TxReceivedCancelled => Colors::text(false),
TxLogEntryType::TxSentCancelled => Colors::text(false),
TxLogEntryType::TxReverted => Colors::text(false)
};
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
View::ellipsize_text(ui, amount_text, 18.0, amount_color);
});
ui.add_space(-2.0);
// Setup transaction status text.
let height = data.info.last_confirmed_height;
let status_text = if !tx.data.confirmed {
let is_canceled = tx.data.tx_type == TxLogEntryType::TxSentCancelled
|| tx.data.tx_type == TxLogEntryType::TxReceivedCancelled;
if is_canceled {
format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled"))
} else if let Some(a) = &tx.action {
let error = if tx.action_error.is_none() {
"".to_string()
// Setup transaction status text.
let height = data.info.last_confirmed_height;
let status_text = if !tx.data.confirmed {
let is_canceled = tx.data.tx_type == TxLogEntryType::TxSentCancelled
|| tx.data.tx_type == TxLogEntryType::TxReceivedCancelled;
if is_canceled {
format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled"))
} else if let Some(action) = &tx.action {
let error = if tx.action_error.is_none() {
"".to_string()
} else {
format!("{}: ", t!("error"))
};
let status = match action {
WalletTxAction::Finalizing => t!("wallets.tx_finalizing"),
WalletTxAction::Posting => t!("wallets.tx_posting"),
WalletTxAction::SendingTor => t!("transport.tor_sending"),
_ => t!("wallets.tx_cancelling")
};
let icon = if error.is_empty() {
DOTS_THREE_CIRCLE
} else {
WARNING
};
format!("{} {}{}", icon, error, status)
} else {
format!("{}: ", t!("error"))
};
let status = match a {
WalletTransactionAction::Cancelling => t!("wallets.tx_cancelling"),
WalletTransactionAction::Finalizing => t!("wallets.tx_finalizing"),
WalletTransactionAction::Posting => t!("wallets.tx_posting"),
WalletTransactionAction::SendingTor => t!("transport.tor_sending")
};
let icon = if error.is_empty() {
DOTS_THREE_CIRCLE
} else {
WARNING
};
format!("{} {}{}", icon, error, status)
match tx.data.tx_type {
TxLogEntryType::TxReceived => {
let text = match tx.finalized() {
true => t!("wallets.await_fin_amount"),
false => t!("wallets.tx_receiving")
};
format!("{} {}", DOTS_THREE_CIRCLE, text)
},
TxLogEntryType::TxSent => {
let text = match tx.finalized() {
true => t!("wallets.await_fin_amount"),
false => t!("wallets.tx_sending")
};
format!("{} {}", DOTS_THREE_CIRCLE, text)
},
TxLogEntryType::ConfirmedCoinbase => {
let tx_h = tx.height.unwrap_or(1) - 1;
if tx_h != 0 {
let left_conf = height - tx_h;
if height >= tx_h && left_conf < COINBASE_MATURITY {
let conf_info = format!("{}/{}",
left_conf,
COINBASE_MATURITY);
format!("{} {} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"),
conf_info
)
} else {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"))
}
} else {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"))
}
},
_ => {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"))
}
}
}
} else {
match tx.data.tx_type {
TxLogEntryType::TxReceived => {
let text = match tx.finalized() {
true => t!("wallets.await_fin_amount"),
false => t!("wallets.tx_receiving")
};
format!("{} {}", DOTS_THREE_CIRCLE, text)
},
TxLogEntryType::TxSent => {
let text = match tx.finalized() {
true => t!("wallets.await_fin_amount"),
false => t!("wallets.tx_sending")
};
format!("{} {}", DOTS_THREE_CIRCLE, text)
},
TxLogEntryType::ConfirmedCoinbase => {
let tx_h = tx.height.unwrap_or(1) - 1;
if tx_h != 0 {
@@ -374,111 +442,91 @@ impl WalletTransactionsContent {
} else {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"))
t!("wallets.tx_confirmed"))
}
} else {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"))
}
},
_ => {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"))
}
}
}
} else {
match tx.data.tx_type {
TxLogEntryType::ConfirmedCoinbase => {
let tx_h = tx.height.unwrap_or(1) - 1;
if tx_h != 0 {
let left_conf = height - tx_h;
if height >= tx_h && left_conf < COINBASE_MATURITY {
let conf_info = format!("{}/{}",
left_conf,
COINBASE_MATURITY);
format!("{} {} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"),
conf_info
)
} else {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirmed"))
}
} else {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirmed"))
}
},
TxLogEntryType::TxSent | TxLogEntryType::TxReceived => {
let min_conf = data.info.minimum_confirmations;
if tx.height.is_none() || (tx.height.unwrap() != 0 &&
height - tx.height.unwrap() >= min_conf - 1) {
let (i, t) = if tx.data.tx_type == TxLogEntryType::TxSent {
(ARROW_CIRCLE_UP, t!("wallets.tx_sent"))
},
TxLogEntryType::TxSent | TxLogEntryType::TxReceived => {
let min_conf = data.info.minimum_confirmations;
if tx.height.is_none() || (tx.height.unwrap() != 0 &&
height - tx.height.unwrap() >= min_conf - 1) {
let (i, t) = if tx.data.tx_type == TxLogEntryType::TxSent {
(ARROW_CIRCLE_UP, t!("wallets.tx_sent"))
} else {
(ARROW_CIRCLE_DOWN, t!("wallets.tx_received"))
};
format!("{} {}", i, t)
} else {
(ARROW_CIRCLE_DOWN, t!("wallets.tx_received"))
};
format!("{} {}", i, t)
} else {
let tx_height = tx.height.unwrap() - 1;
let left_conf = height - tx_height;
let conf_info = if tx_height != 0 && height >= tx_height &&
left_conf < min_conf {
format!("{}/{}", left_conf, min_conf)
} else {
"".to_string()
};
format!("{} {} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"),
conf_info
)
}
let tx_height = tx.height.unwrap() - 1;
let left_conf = height - tx_height;
let conf_info = if tx_height != 0 && height >= tx_height &&
left_conf < min_conf {
format!("{}/{}", left_conf, min_conf)
} else {
"".to_string()
};
format!("{} {} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"),
conf_info
)
}
},
_ => format!("{} {}", X_CIRCLE, t!("wallets.canceled"))
}
};
// Setup status text color.
let status_color = match tx.data.tx_type {
TxLogEntryType::ConfirmedCoinbase => Colors::text(false),
TxLogEntryType::TxReceived => if tx.data.confirmed {
Colors::green()
} else {
Colors::text(false)
},
_ => format!("{} {}", X_CIRCLE, t!("wallets.canceled"))
}
};
TxLogEntryType::TxSent => if tx.data.confirmed {
Colors::red()
} else {
Colors::text(false)
},
TxLogEntryType::TxReceivedCancelled => Colors::inactive_text(),
TxLogEntryType::TxSentCancelled => Colors::inactive_text(),
TxLogEntryType::TxReverted => Colors::inactive_text(),
};
View::ellipsize_text(ui, status_text, 15.0, status_color);
// Setup status text color.
let status_color = match tx.data.tx_type {
TxLogEntryType::ConfirmedCoinbase => Colors::text(false),
TxLogEntryType::TxReceived => if tx.data.confirmed {
Colors::green()
} else {
Colors::text(false)
},
TxLogEntryType::TxSent => if tx.data.confirmed {
Colors::red()
} else {
Colors::text(false)
},
TxLogEntryType::TxReceivedCancelled => Colors::inactive_text(),
TxLogEntryType::TxSentCancelled => Colors::inactive_text(),
TxLogEntryType::TxReverted => Colors::inactive_text(),
};
View::ellipsize_text(ui, status_text, 15.0, status_color);
// Setup transaction time.
let tx_time = View::format_time(tx.data.creation_ts.timestamp());
let tx_time_text = format!("{} {}", CALENDAR_CHECK, tx_time);
ui.label(RichText::new(tx_time_text).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
// Setup transaction time.
let tx_time = View::format_time(tx.data.creation_ts.timestamp());
let tx_time_text = format!("{} {}", CALENDAR_CHECK, tx_time);
ui.label(RichText::new(tx_time_text).size(15.0).color(Colors::gray()));
ui.add_space(4.0);
});
});
});
});
}
).response;
let (clickable, on_click) = on_click;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if clickable && res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::TRANSPARENT;
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked && clickable {
on_click();
}
}
/// Draw button to repeat transaction action on error or repost.
pub fn tx_repeat_button_ui(ui: &mut egui::Ui,
rounding: CornerRadius,
tx: &WalletTransaction,
tx: &WalletTx,
wallet: &Wallet,
repost: bool) {
let (icon, color) = (ARROWS_CLOCKWISE, Some(Colors::green()));
@@ -487,21 +535,21 @@ impl WalletTransactionsContent {
wallet.task(WalletTask::Post(tx.data.id));
} else if let Some(action) = tx.action.as_ref() {
match action {
WalletTransactionAction::Finalizing => {
WalletTxAction::Finalizing => {
wallet.task(WalletTask::Finalize(tx.data.id));
}
WalletTransactionAction::Posting => {
WalletTxAction::Posting => {
wallet.task(WalletTask::Post(tx.data.id));
}
_ => {
if let Some(a) = &tx.receiver {
wallet.task(WalletTask::SendTor(tx.data.id, a.clone()));
wallet.task(WalletTask::SendTor(tx.data.clone(), a.clone()));
}
}
}
} else {
if let Some(a) = &tx.receiver {
wallet.task(WalletTask::SendTor(tx.data.id, a.clone()));
wallet.task(WalletTask::SendTor(tx.data.clone(), a.clone()));
}
}
});
@@ -522,8 +570,8 @@ impl WalletTransactionsContent {
let data = wallet.get_data().unwrap();
let data_txs = data.txs.unwrap();
let txs = data_txs.into_iter()
.filter(|tx| tx.data.id == self.confirm_cancel_tx_id.unwrap())
.collect::<Vec<WalletTransaction>>();
.filter(|tx| tx.data.id == self.confirm_cancel_tx_id.unwrap_or_default())
.collect::<Vec<WalletTx>>();
if txs.is_empty() {
Modal::close();
return;
@@ -562,7 +610,7 @@ impl WalletTransactionsContent {
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, "OK".to_string(), Colors::white_or_black(false), || {
wallet.task(WalletTask::Cancel(tx.data.clone()));
wallet.task(WalletTask::Cancel(tx.data.id));
self.confirm_cancel_tx_id = None;
Modal::close();
});
@@ -571,6 +619,62 @@ impl WalletTransactionsContent {
ui.add_space(6.0);
});
}
/// Show transaction deletion confirmation [`Modal`].
fn show_delete_confirmation_modal(&mut self, id: u32) {
self.confirm_delete_tx_id = Some(id);
// Show transaction deletion confirmation modal.
Modal::new(DELETE_TX_CONFIRMATION_MODAL)
.position(ModalPosition::Center)
.title(t!("confirmation"))
.show();
}
/// Confirmation [`Modal`] to delete transaction.
fn delete_confirmation_modal(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
let data = wallet.get_data().unwrap();
let data_txs = data.txs.unwrap();
let txs = data_txs.into_iter()
.filter(|tx| tx.data.id == self.confirm_delete_tx_id.unwrap_or_default())
.collect::<Vec<WalletTx>>();
if txs.is_empty() {
Modal::close();
return;
}
let tx = txs.get(0).unwrap();
// Show confirmation text.
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.tx_delete_confirmation"))
.size(17.0)
.color(Colors::text(false)));
ui.add_space(8.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.confirm_delete_tx_id = None;
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, "OK".to_string(), Colors::white_or_black(false), || {
wallet.task(WalletTask::Delete(tx.data.id));
self.confirm_delete_tx_id = None;
Modal::close();
});
});
});
ui.add_space(6.0);
});
}
}
/// Draw awaiting balance item content.
+29 -26
View File
@@ -19,15 +19,15 @@ use grin_util::ToHex;
use grin_wallet_libwallet::TxLogEntryType;
use std::fs;
use crate::AppConfig;
use crate::gui::icons::{CIRCLE_HALF, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SEAL_CHECK};
use crate::gui::icons::{CIRCLE_HALF, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, FILE_X, HASH_STRAIGHT, PROHIBIT, QR_CODE, SEAL_CHECK};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::wallets::wallet::proof::PaymentProofContent;
use crate::gui::views::wallets::wallet::txs::WalletTransactionsContent;
use crate::gui::views::{Modal, QrCodeContent, View};
use crate::gui::Colors;
use crate::wallet::types::{WalletTask, WalletTransaction};
use crate::wallet::types::{WalletTask, WalletTx};
use crate::wallet::Wallet;
use crate::AppConfig;
/// Transaction information [`Modal`] content.
pub struct WalletTransactionContent {
@@ -44,7 +44,7 @@ pub struct WalletTransactionContent {
}
impl WalletTransactionContent {
/// Create new content instance with [`Wallet`] from provided [`WalletTransaction`].
/// Create new content instance with [`Wallet`] from provided [`WalletTx`].
pub fn new(tx_id: u32) -> Self {
Self {
tx_id,
@@ -59,7 +59,8 @@ impl WalletTransactionContent {
ui: &mut egui::Ui,
modal: &Modal,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
cb: &dyn PlatformCallbacks,
on_delete: impl FnOnce(u32)) {
// Check values and setup transaction data.
let wallet_data = wallet.get_data();
if wallet_data.is_none() {
@@ -70,7 +71,7 @@ impl WalletTransactionContent {
let data_txs = data.txs.clone().unwrap();
let txs = data_txs.into_iter()
.filter(|tx| tx.data.id == self.tx_id)
.collect::<Vec<WalletTransaction>>();
.collect::<Vec<WalletTx>>();
if txs.is_empty() {
Modal::close();
return;
@@ -110,7 +111,7 @@ impl WalletTransactionContent {
} else {
modal.set_background_color(Colors::fill());
// Show transaction information.
self.info_ui(ui, tx, wallet, cb);
self.info_ui(ui, tx, wallet, cb, on_delete);
// Show transaction sharing content or payment proof.
if self.proof_content.is_none() && tx.can_cancel() && !tx.finalized() {
@@ -155,7 +156,7 @@ impl WalletTransactionContent {
fn share_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
tx: &WalletTransaction,
tx: &WalletTx,
cb: &dyn PlatformCallbacks) {
if self.message.is_none() {
let slatepack_path = wallet.get_config().get_tx_slate_path(tx);
@@ -258,22 +259,27 @@ impl WalletTransactionContent {
/// Draw transaction information content.
fn info_ui(&mut self,
ui: &mut egui::Ui,
tx: &WalletTransaction,
tx: &WalletTx,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
cb: &dyn PlatformCallbacks,
on_delete: impl FnOnce(u32)) {
ui.add_space(6.0);
// Transaction item background setup.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(WalletTransactionsContent::TX_ITEM_HEIGHT);
// Draw tx item background.
let p = ui.painter();
let r = View::item_rounding(0, 2, false);
p.rect(rect, r, Colors::TRANSPARENT, View::item_stroke(), StrokeKind::Outside);
let rounding = View::item_rounding(0, 2, false);
let bg = Colors::TRANSPARENT;
// Show transaction amount status and time.
let data = wallet.get_data().unwrap();
WalletTransactionsContent::tx_item_ui(ui, tx, rect, &data, |ui| {
let on_click = (false, || {});
WalletTransactionsContent::tx_item_ui(ui, tx, rect, bg, rounding, &data, on_click, |ui| {
// Show button to delete transaction from database.
if tx.data.confirmed || tx.cancelled() {
let r = View::item_rounding(0, 2, true);
View::item_button(ui, r, FILE_X, Some(Colors::inactive_text()), || {
on_delete(tx.data.id);
});
}
// Show block height or buttons.
if let Some(h) = tx.height {
if h != 0 {
@@ -288,27 +294,24 @@ impl WalletTransactionContent {
}
return;
}
if wallet.synced_from_node() && !tx.cancelled() && !tx.cancelling() && !tx.posting() {
let rebroadcast = tx.broadcasting_timed_out(&wallet);
let repeat = tx.broadcasting_timed_out(&wallet);
// Draw button to cancel transaction.
if tx.can_cancel() || rebroadcast {
if tx.can_cancel() || repeat {
let r = View::item_rounding(0, 2, true);
View::item_button(ui, r, PROHIBIT, Some(Colors::red()), || {
wallet.task(WalletTask::Cancel(tx.data.clone()));
wallet.task(WalletTask::Cancel(tx.data.id));
Modal::close();
});
}
// Draw button to repeat transaction action.
if tx.can_repeat_action() || rebroadcast {
if tx.can_repeat_action() || repeat {
let r = if tx.can_finalize() || tx.can_cancel() {
CornerRadius::default()
} else {
View::item_rounding(0, 2, true)
};
WalletTransactionsContent::tx_repeat_button_ui(ui, r, tx, wallet, rebroadcast);
WalletTransactionsContent::tx_repeat_button_ui(ui, r, tx, wallet, repeat);
}
}
});
+1 -1
View File
@@ -44,7 +44,7 @@ pub trait WalletContentContainer {
/// Get wallet status text.
pub fn wallet_status_text(wallet: &Wallet) -> String {
if wallet.sync_error() {
if wallet.sync_error() && wallet.is_open() {
format!("{} {}", WARNING_CIRCLE, t!("error"))
} else if wallet.is_closing() {
format!("{} {}", SPINNER, t!("wallets.closing"))
+2 -2
View File
@@ -404,8 +404,8 @@ impl Tor {
let mut w_client = TOR_STATE.client_config.write();
*w_client = None;
// Clear state.
fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default();}
fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default();
}
port_key
}
+2 -2
View File
@@ -23,7 +23,7 @@ use serde_derive::{Deserialize, Serialize};
use crate::{AppConfig, Settings};
use crate::wallet::ConnectionsConfig;
use crate::wallet::types::{ConnectionMethod, WalletTransaction};
use crate::wallet::types::{ConnectionMethod, WalletTx};
/// Wallet configuration.
#[derive(Serialize, Deserialize, Clone)]
@@ -228,7 +228,7 @@ impl WalletConfig {
}
/// Get Slatepack file path for transaction.
pub fn get_tx_slate_path(&self, tx: &WalletTransaction) -> PathBuf {
pub fn get_tx_slate_path(&self, tx: &WalletTx) -> PathBuf {
let mut path = PathBuf::from(self.get_base_data_path());
path.push(SLATEPACKS_DIR_NAME);
if !path.exists() {
+39 -47
View File
@@ -150,7 +150,7 @@ pub struct WalletData {
pub info: WalletInfo,
/// Transactions data.
pub txs: Option<Vec<WalletTransaction>>,
pub txs: Option<Vec<WalletTx>>,
/// Number of txs to show on select from database.
pub txs_limit: u32,
}
@@ -160,53 +160,34 @@ impl WalletData {
pub const TXS_LIMIT: u32 = 30;
/// Update transaction action status.
pub fn on_tx_action(&mut self, id: String, action: Option<WalletTransactionAction>) {
pub fn on_tx_action(&mut self, id: u32, action: Option<WalletTxAction>) {
if self.txs.is_none() {
return;
}
for tx in self.txs.as_mut().unwrap() {
if let Some(slate_id) = tx.data.tx_slate_id {
if slate_id.to_string() == id {
tx.action = action;
tx.action_error = None;
break;
}
if id == tx.data.id {
tx.action = action;
tx.action_error = None;
break;
}
}
}
/// Update transaction action error status.
pub fn on_tx_error(&mut self, id: String, err: Option<Error>) {
pub fn on_tx_error(&mut self, id: u32, err: Option<Error>) {
if self.txs.is_none() {
return;
}
for tx in self.txs.as_mut().unwrap() {
if let Some(slate_id) = tx.data.tx_slate_id {
if slate_id.to_string() == id {
tx.action_error = err;
break;
}
if id == tx.data.id {
tx.action_error = err;
break;
}
}
}
/// Get transaction by slate identifier.
pub fn tx_by_slate_id(&self, id: String) -> Option<WalletTransaction> {
if self.txs.is_none() {
return None;
}
for tx in self.txs.as_ref().unwrap() {
if let Some(slate_id) = tx.data.tx_slate_id {
if slate_id.to_string() == id {
return Some(tx.clone());
}
}
}
None
}
/// Get transaction by identifier.
pub fn tx_by_id(&self, id: u32) -> Option<WalletTransaction> {
pub fn tx_by_id(&self, id: u32) -> Option<WalletTx> {
if self.txs.is_none() {
return None;
}
@@ -221,13 +202,13 @@ impl WalletData {
/// Wallet transaction action.
#[derive(Clone, PartialEq)]
pub enum WalletTransactionAction {
Cancelling, Finalizing, Posting, SendingTor
pub enum WalletTxAction {
Cancelling, Finalizing, Posting, SendingTor, Deleting
}
/// Wallet transaction data.
#[derive(Clone)]
pub struct WalletTransaction {
pub struct WalletTx {
/// Information from database.
pub data: TxLogEntry,
/// State of transaction Slate.
@@ -247,19 +228,19 @@ pub struct WalletTransaction {
pub broadcasting_height: Option<u64>,
/// Action on transaction.
pub action: Option<WalletTransactionAction>,
pub action: Option<WalletTxAction>,
/// Action result error.
pub action_error: Option<Error>
}
impl WalletTransaction {
impl WalletTx {
/// Create new wallet transaction.
pub fn new(tx: TxLogEntry,
proof: Option<PaymentProof>,
wallet: &Wallet,
height: Option<u64>,
broadcasting_height: Option<u64>,
action: Option<WalletTransactionAction>,
action: Option<WalletTxAction>,
action_error: Option<Error>) -> Self {
let amount = if tx.amount_debited > tx.amount_credited {
tx.amount_debited - tx.amount_credited
@@ -352,7 +333,7 @@ impl WalletTransaction {
/// Check if transaction is sending over Tor.
pub fn sending_tor(&self) -> bool {
if let Some(a) = self.action.as_ref() {
return a == &WalletTransactionAction::SendingTor;
return a == &WalletTxAction::SendingTor;
}
false
}
@@ -360,7 +341,7 @@ impl WalletTransaction {
/// Check if transaction is cancelling.
pub fn cancelling(&self) -> bool {
if let Some(a) = self.action.as_ref() {
return a == &WalletTransactionAction::Cancelling;
return a == &WalletTxAction::Cancelling;
}
false
}
@@ -368,7 +349,7 @@ impl WalletTransaction {
/// Check if transaction is posting.
pub fn posting(&self) -> bool {
if let Some(a) = self.action.as_ref() {
return a == &WalletTransactionAction::Posting;
return a == &WalletTxAction::Posting;
}
false
}
@@ -390,7 +371,7 @@ impl WalletTransaction {
/// Check if transaction is finalizing.
pub fn finalizing(&self) -> bool {
if let Some(a) = self.action.as_ref() {
return a == &WalletTransactionAction::Finalizing;
return a == &WalletTxAction::Finalizing;
}
false
}
@@ -398,7 +379,7 @@ impl WalletTransaction {
/// Check if possible to repeat transaction action.
pub fn can_repeat_action(&self) -> bool {
if let Some(a) = &self.action {
self.action_error.is_some() && a != &WalletTransactionAction::Cancelling
self.action_error.is_some() && a != &WalletTxAction::Cancelling
} else {
// Can resend over Tor.
!self.data.confirmed && !self.sending_tor() && !self.broadcasting() &&
@@ -426,6 +407,14 @@ impl WalletTransaction {
}
false
}
/// Check if transaction is deleting.
pub fn deleting(&self) -> bool {
if let Some(a) = self.action.as_ref() {
return a == &WalletTxAction::Deleting;
}
false
}
}
/// Task for the wallet.
@@ -446,19 +435,22 @@ pub enum WalletTask {
/// * receiver
Send(u64, Option<SlatepackAddress>),
/// Send request over Tor.
/// * local tx id
/// * tx
/// * receiver
SendTor(u32, SlatepackAddress),
SendTor(TxLogEntry, SlatepackAddress),
/// Invoice creation.
/// * amount
Receive(u64),
/// Transaction finalization.
/// * local tx id
/// * tx id
Finalize(u32),
/// Post transaction to blockchain.
/// * local tx id
/// * tx id
Post(u32),
/// Cancel transaction.
/// * tx
Cancel(TxLogEntry),
/// * tx id
Cancel(u32),
/// Delete transaction.
/// * tx id
Delete(u32)
}
+215 -177
View File
@@ -16,7 +16,7 @@ use crate::node::{Node, NodeConfig};
use crate::tor::Tor;
use crate::wallet::seed::WalletSeed;
use crate::wallet::store::TxHeightStore;
use crate::wallet::types::{ConnectionMethod, PhraseMode, WalletAccount, WalletData, WalletInstance, WalletTask, WalletTransaction, WalletTransactionAction};
use crate::wallet::types::{ConnectionMethod, PhraseMode, WalletAccount, WalletData, WalletInstance, WalletTask, WalletTx, WalletTxAction};
use crate::wallet::{ConnectionsConfig, Mnemonic, WalletConfig};
use crate::AppConfig;
@@ -48,7 +48,7 @@ use std::sync::mpsc::Sender;
use std::sync::{mpsc, Arc};
use std::thread::Thread;
use std::time::Duration;
use std::{fs, thread};
use std::{fs, path, thread};
use chrono::Utc;
use log::error;
use num_bigint::BigInt;
@@ -131,7 +131,7 @@ pub struct Wallet {
/// Tasks sender.
tasks_sender: Arc<RwLock<Option<Sender<WalletTask>>>>,
/// Task result with optional transaction identifier.
task_result: Arc<RwLock<Option<(Option<String>, WalletTask)>>>,
task_result: Arc<RwLock<Option<(Option<u32>, WalletTask)>>>,
}
impl Wallet {
@@ -379,13 +379,8 @@ impl Wallet {
}
}
// Set Slatepack address and secret key.
if let Ok((key, addr)) = self.get_secret_key_addr() {
let mut w_key = self.secret_key.write();
*w_key = Some(key);
let mut w_address = self.slatepack_address.write();
*w_address = Some(addr.to_string());
}
// Update Slatepack address and secret key.
self.update_secret_key_addr()?;
Ok(())
}
@@ -403,7 +398,7 @@ impl Wallet {
}
/// Retrieve wallet [`SecretKey`] and Slatepack address for transport.
fn get_secret_key_addr(&self) -> Result<(SecretKey, SlatepackAddress), Error> {
fn update_secret_key_addr(&self) -> Result<(), Error> {
let r_inst = self.instance.as_ref().read();
let instance = r_inst.clone().unwrap();
let mut w_lock = instance.lock();
@@ -414,7 +409,11 @@ impl Wallet {
let sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0)
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
let addr = SlatepackAddress::try_from(&sec_key)?;
Ok((sec_key, addr))
let mut w_key = self.secret_key.write();
*w_key = Some(sec_key);
let mut w_address = self.slatepack_address.write();
*w_address = Some(addr.to_string());
Ok(())
}
/// Get unique opened wallet identifier, including current account.
@@ -628,12 +627,11 @@ impl Wallet {
}
/// Select transaction by slate id.
fn retrieve_tx_by_id(&self, id: Uuid) -> Option<TxLogEntry> {
fn retrieve_tx_by_id(&self, id: Option<u32>, slate_id: Option<Uuid>) -> Option<TxLogEntry> {
let r_inst = self.instance.as_ref().read();
let inst = r_inst.clone().unwrap();
let mask = self.keychain_mask();
let tx_id = Some(id);
if let Ok((_, txs)) = retrieve_txs(inst, mask.as_ref(), &None, false, None, tx_id, None) {
if let Ok((_, txs)) = retrieve_txs(inst, mask.as_ref(), &None, false, id, slate_id, None) {
if !txs.is_empty() {
return Some(txs.get(0).unwrap().clone())
}
@@ -740,13 +738,8 @@ impl Wallet {
Ok(())
})?;
// Setup secret key and Slatepack address.
if let Ok((key, addr)) = self.get_secret_key_addr() {
let mut w_key = self.secret_key.write();
*w_key = Some(key);
let mut w_address = self.slatepack_address.write();
*w_address = Some(addr.to_string());
}
// Update Slatepack address and secret key.
self.update_secret_key_addr()?;
// Save account label into config.
let mut w_config = self.config.write();
@@ -893,7 +886,7 @@ impl Wallet {
/// Check if Slatepack file exists.
pub fn slatepack_exists(&self, slate: &Slate) -> bool {
let slatepack_path = self.get_config().get_slate_path(slate);
fs::exists(slatepack_path).unwrap()
fs::exists(slatepack_path).unwrap_or(false)
}
/// Calculate transaction fee for provided amount.
@@ -969,12 +962,12 @@ impl Wallet {
}
/// Send slate to Tor address.
async fn send_tor(&self, slate: &Slate, addr: &SlatepackAddress) -> Result<Slate, Error> {
self.on_tx_action(slate.id.to_string(), Some(WalletTransactionAction::SendingTor));
async fn send_tor(&self, id: u32, s: &Slate, addr: &SlatepackAddress) -> Result<Slate, Error> {
self.on_tx_action(id, Some(WalletTxAction::SendingTor));
let tor_addr = OnionV3Address::try_from(addr).unwrap().to_http_str();
let url = format!("{}/v2/foreign", tor_addr);
let slate_send = VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?;
let slate_send = VersionedSlate::into_version(s.clone(), SlateVersion::V4)?;
let body = json!({
"jsonrpc": "2.0",
"method": "receive_tx",
@@ -1074,8 +1067,8 @@ impl Wallet {
}
/// Finalize transaction from provided message as sender or invoice issuer.
fn finalize(&self, slate: &Slate) -> Result<Slate, Error> {
self.on_tx_action(slate.id.to_string(), Some(WalletTransactionAction::Finalizing));
fn finalize(&self, slate: &Slate, id: u32) -> Result<Slate, Error> {
self.on_tx_action(id, Some(WalletTxAction::Finalizing));
let r_inst = self.instance.as_ref().read();
let instance = r_inst.clone().unwrap();
@@ -1090,14 +1083,16 @@ impl Wallet {
let _ = self.create_slatepack_message(&slate, None)?;
// Clear tx action.
self.on_tx_action(slate.id.to_string(), None);
self.on_tx_action(id, None);
Ok(slate)
}
/// Post transaction to blockchain.
fn post(&self, slate: &Slate) -> Result<(), Error> {
self.on_tx_action(slate.id.to_string(), Some(WalletTransactionAction::Posting));
fn post(&self, slate: &Slate, id: Option<u32>) -> Result<(), Error> {
if let Some(id) = id {
self.on_tx_action(id, Some(WalletTxAction::Posting));
}
let r_inst = self.instance.as_ref().read();
let instance = r_inst.clone().unwrap();
@@ -1108,19 +1103,19 @@ impl Wallet {
})?;
// Clear tx action.
self.on_tx_action(slate.id.to_string(), None);
if let Some(id) = id {
self.on_tx_action(id, None);
}
Ok(())
}
/// Cancel transaction.
fn cancel(&self, tx: &TxLogEntry) -> Result<(), Error> {
let id = tx.tx_slate_id.unwrap().to_string();
self.on_tx_action(id.clone(), Some(WalletTransactionAction::Cancelling));
fn cancel(&self, id: u32) -> Result<(), Error> {
self.on_tx_action(id, Some(WalletTxAction::Cancelling));
let r_inst = self.instance.as_ref().read();
let instance = r_inst.clone().unwrap();
cancel_tx(instance, self.keychain_mask().as_ref(), &None, Some(tx.id), None)?;
cancel_tx(instance, self.keychain_mask().as_ref(), &None, Some(id), None)?;
// Clear tx action.
self.on_tx_action(id, None);
@@ -1129,25 +1124,30 @@ impl Wallet {
}
/// Update transaction action status.
fn on_tx_action(&self, id: String, action: Option<WalletTransactionAction>) {
fn on_tx_action(&self, id: u32, action: Option<WalletTxAction>) {
let mut w_data = self.data.write();
w_data.as_mut().unwrap().on_tx_action(id, action);
}
/// Update transaction action error status.
fn on_tx_error(&self, id: String, err: Option<Error>) {
fn on_tx_error(&self, id: u32, err: Option<Error>) {
let mut w_data = self.data.write();
w_data.as_mut().unwrap().on_tx_error(id, err);
}
/// Save task result to consume later.
fn on_task_result(&self, id: Option<String>, task: &WalletTask) {
fn on_task_result(&self, tx: Option<TxLogEntry>, task: &WalletTask) {
let mut w_res = self.task_result.write();
let id = if let Some(t) = tx {
Some(t.id)
} else {
None
};
*w_res = Some((id, task.clone()));
}
/// Consume result of successful task.
pub fn consume_task_result(&self) -> Option<(Option<String>, WalletTask)> {
pub fn consume_task_result(&self) -> Option<(Option<u32>, WalletTask)> {
let res = {
let r_res = self.task_result.read();
r_res.clone()
@@ -1159,7 +1159,7 @@ impl Wallet {
}
/// Get possible transaction confirmation height.
fn tx_height(&self, tx: &WalletTransaction) -> Result<Option<u64>, Error> {
fn tx_height(&self, tx: &WalletTx) -> Result<Option<u64>, Error> {
let mut tx_height = None;
if tx.data.confirmed && tx.data.kernel_excess.is_some() {
let r_inst = self.instance.as_ref().read();
@@ -1185,17 +1185,45 @@ impl Wallet {
Ok(tx_height)
}
/// Get transaction Slate from database.
fn get_tx(&self, tx_id: u32) -> Option<Slate> {
/// Get stored transaction Slate.
fn get_tx_slate(&self, tx_id: Option<u32>, slate_id: Option<&Uuid>) -> Option<Slate> {
let r_inst = self.instance.as_ref().read();
let instance = r_inst.clone().unwrap();
let api = Owner::new(instance, None);
if let Ok(s) = api.get_stored_tx(self.keychain_mask().as_ref(), Some(tx_id), None) {
if let Ok(s) = api.get_stored_tx(self.keychain_mask().as_ref(), tx_id, slate_id) {
return s;
}
None
}
/// Delete transaction from database.
fn delete_tx(&self, id: u32) -> Result<(), Error> {
self.on_tx_action(id, Some(WalletTxAction::Deleting));
let slate = self.get_tx_slate(Some(id), None);
let r_inst = self.instance.as_ref().read();
let instance = r_inst.clone().unwrap();
let keychain_mask = self.keychain_mask();
let mut wallet_lock = instance.lock();
let lc = wallet_lock.lc_provider()?;
let w = lc.wallet_inst()?;
let parent_key = w.parent_key_id();
let mut batch = w.batch(keychain_mask.as_ref())?;
batch.delete_tx_log_entry(id, &parent_key)?;
batch.commit()?;
// Delete transaction files.
if let Some(s) = slate {
let slatepack_path = self.get_config().get_slate_path(&s);
fs::remove_file(&slatepack_path).unwrap_or_default();
let path = path::Path::new(&self.get_config().get_data_path())
.join("saved_txs")
.join(format!("{}.grintx", s.id));
fs::remove_file(&path).unwrap_or_default();
}
Ok(())
}
/// Change wallet password.
pub fn change_password(&self, old: String, new: String) -> Result<(), Error> {
let r_inst = self.instance.as_ref().read();
@@ -1577,36 +1605,33 @@ fn start_sync(wallet: Wallet) -> Thread {
/// Handle wallet task.
async fn handle_task(w: &Wallet, t: WalletTask) {
let send_tor = async |s: &Slate, r: &SlatepackAddress| {
let id = s.id.to_string();
match w.send_tor(&s, r).await {
let send_tor = async |tx: TxLogEntry, s: &Slate, r: &SlatepackAddress| {
match w.send_tor(tx.id, &s, r).await {
Ok(s) => {
match w.finalize(&s) {
match w.finalize(&s, tx.id) {
Ok(s) => {
match w.post(&s) {
match w.post(&s, Some(tx.id)) {
Ok(_) => {
sync_wallet_data(&w, false);
w.on_task_result(Some(id), &t);
w.on_task_result(Some(tx), &t);
}
Err(e) => {
error!("send tor post error: {:?}", e);
w.on_tx_error(id, Some(e));
w.on_tx_error(tx.id, Some(e));
}
}
}
Err(e) => {
error!("send tor finalize error: {:?}", e);
if let Some(tx) = w.retrieve_tx_by_id(s.id) {
let _ = w.cancel(&tx);
sync_wallet_data(&w, false);
}
let _ = w.cancel(tx.id);
sync_wallet_data(&w, false);
}
}
}
Err(e) => {
error!("send tor error: {:?}", e);
w.on_tx_error(id.clone(), Some(e));
w.on_task_result(Some(id), &t);
w.on_tx_error(tx.id, Some(e));
w.on_task_result(Some(tx), &t);
}
}
};
@@ -1616,74 +1641,71 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
return;
}
let w = w.clone();
let load = w.message_opening.clone();
let msg = m.clone();
thread::spawn(move || {
load.store(true, Ordering::Relaxed);
if let Ok((s, dest)) = w.parse_slatepack(&msg) {
let id = s.id.to_string();
// Check if message already exists.
let exists = {
let mut exists = w.slatepack_exists(&s);
if !exists && (s.state == SlateState::Invoice2 ||
s.state == SlateState::Standard2) {
let mut slate = s.clone();
slate.state = if s.state == SlateState::Standard2 {
SlateState::Standard3
} else {
SlateState::Invoice3
};
exists = w.slatepack_exists(&slate);
}
exists
};
if exists {
w.on_task_result(Some(id), &t);
load.store(false, Ordering::Relaxed);
return;
w.message_opening.store(true, Ordering::Relaxed);
if let Ok((s, dest)) = w.parse_slatepack(&msg) {
let tx = w.retrieve_tx_by_id(None, Some(s.id));
// Check if message already exists.
let exists = {
let mut exists = w.slatepack_exists(&s);
if !exists && (s.state == SlateState::Invoice2 ||
s.state == SlateState::Standard2) {
let mut slate = s.clone();
slate.state = if s.state == SlateState::Standard2 {
SlateState::Standard3
} else {
SlateState::Invoice3
};
exists = w.slatepack_exists(&slate);
}
// Create response or finalize.
match s.state {
SlateState::Standard1 | SlateState::Invoice1 => {
if s.state != SlateState::Standard1 {
if let Ok(_) = w.pay(&s) {
sync_wallet_data(&w, false);
w.on_task_result(Some(id), &t);
}
} else {
if let Ok(_) = w.receive(&s, dest) {
sync_wallet_data(&w, false);
w.on_task_result(Some(id), &t);
}
exists
};
if exists {
w.on_task_result(tx, &t);
w.message_opening.store(false, Ordering::Relaxed);
return;
}
// Create response or finalize.
match s.state {
SlateState::Standard1 | SlateState::Invoice1 => {
if s.state != SlateState::Standard1 {
if let Ok(_) = w.pay(&s) {
sync_wallet_data(&w, false);
w.on_task_result(tx, &t);
}
} else {
if let Ok(_) = w.receive(&s, dest) {
sync_wallet_data(&w, false);
w.on_task_result(tx, &t);
}
}
SlateState::Standard2 | SlateState::Invoice2 => {
match w.finalize(&s) {
}
SlateState::Standard2 | SlateState::Invoice2 => {
if let Some(tx) = tx {
match w.finalize(&s, tx.id) {
Ok(s) => {
match w.post(&s) {
match w.post(&s, Some(tx.id)) {
Ok(_) => {
sync_wallet_data(&w, false);
}
Err(e) => {
error!("message tx post error: {:?}", e);
w.on_tx_error(id, Some(e));
w.on_tx_error(tx.id, Some(e));
}
}
}
Err(e) => {
if let Some(tx) = w.retrieve_tx_by_id(s.id) {
let _ = w.cancel(&tx);
}
let _ = w.cancel(tx.id);
error!("message tx finalize error: {:?}", e);
w.on_tx_error(id, Some(e));
w.on_tx_error(tx.id, Some(e));
}
}
}
_ => {}
};
}
load.store(false, Ordering::Relaxed);
});
}
_ => {}
};
}
w.message_opening.store(false, Ordering::Relaxed);
}
WalletTask::CalculateFee(a, _) => {
// Wait if there are no more fee tasks or handle next input value.
@@ -1710,95 +1732,101 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
w.send_creating.store(true, Ordering::Relaxed);
if let Ok(s) = w.send(*a, r.clone()) {
sync_wallet_data(&w, false);
if let Some(addr) = r {
w.send_creating.store(false, Ordering::Relaxed);
send_tor(&s, addr).await;
return;
} else {
w.on_task_result(Some(s.id.to_string()), &t);
let tx = w.retrieve_tx_by_id(None, Some(s.id));
if let Some(tx) = tx {
if let Some(addr) = r {
w.send_creating.store(false, Ordering::Relaxed);
send_tor(tx, &s, addr).await;
return;
} else {
w.on_task_result(Some(tx), &t);
}
}
}
w.send_creating.store(false, Ordering::Relaxed);
}
WalletTask::SendTor(id, r) => {
if let Some(s) = w.get_tx(*id) {
send_tor(&s, r).await;
WalletTask::SendTor(tx, r) => {
if let Some(s) = w.get_tx_slate(Some(tx.id), None) {
send_tor(tx.clone(), &s, r).await;
}
}
WalletTask::Receive(a) => {
w.invoice_creating.store(true, Ordering::Relaxed);
if let Ok(s) = w.issue_invoice(*a) {
sync_wallet_data(&w, false);
w.on_task_result(Some(s.id.to_string()), &t);
let tx = w.retrieve_tx_by_id(None, Some(s.id));
if let Some(tx) = tx {
w.on_task_result(Some(tx), &t);
}
}
w.invoice_creating.store(false, Ordering::Relaxed);
},
WalletTask::Finalize(id) => {
let slate = &w.get_tx(*id).unwrap();
w.on_tx_error(slate.id.to_string(), None);
match w.finalize(slate) {
Ok(s) => {
match w.post(&s) {
Ok(_) => {
sync_wallet_data(&w, false);
}
Err(e) => {
error!("tx finalize post error: {:?}", e);
w.on_tx_error(slate.id.to_string(), Some(e));
if let Some(s) = w.get_tx_slate(Some(*id), None) {
w.on_tx_error(*id, None);
match w.finalize(&s, *id) {
Ok(s) => {
match w.post(&s, Some(*id)) {
Ok(_) => {
sync_wallet_data(&w, false);
}
Err(e) => {
error!("tx finalize post error: {:?}", e);
w.on_tx_error(*id, Some(e));
}
}
}
}
Err(e) => {
if let Some(tx) = w.retrieve_tx_by_id(slate.id) {
let _ = w.cancel(&tx);
Err(e) => {
let _ = w.cancel(*id);
error!("tx finalize error: {:?}", e);
w.on_tx_error(*id, Some(e));
}
error!("tx finalize error: {:?}", e);
w.on_tx_error(slate.id.to_string(), Some(e));
}
} else {
w.on_tx_error(*id, Some(Error::GenericError("tx slate not found".to_string())));
}
}
WalletTask::Post(id) => {
let slate = &w.get_tx(*id).unwrap();
w.on_tx_error(slate.id.to_string(), None);
// Cleanup broadcasting tx height.
let tx_height_store = TxHeightStore::new(w.get_config().get_extra_db_path());
tx_height_store.delete_broadcasting_height(&slate.id.to_string());
let has_data = {
let r_data = w.data.read();
r_data.is_some()
};
if has_data {
let mut w_data = w.data.write();
for tx in w_data.as_mut().unwrap().txs.as_mut().unwrap() {
if tx.data.id == *id {
tx.broadcasting_height = None;
break;
if let Some(s) = w.get_tx_slate(Some(*id), None) {
w.on_tx_error(*id, None);
// Cleanup broadcasting tx height.
let tx_height_store = TxHeightStore::new(w.get_config().get_extra_db_path());
tx_height_store.delete_broadcasting_height(&id.to_string());
let has_data = {
let r_data = w.data.read();
r_data.is_some()
};
if has_data {
let mut w_data = w.data.write();
for tx in w_data.as_mut().unwrap().txs.as_mut().unwrap() {
if tx.data.id == *id {
tx.broadcasting_height = None;
break;
}
}
}
}
match w.post(slate) {
Ok(_) => {
sync_wallet_data(&w, false);
}
Err(e) => {
error!("tx post error: {:?}", e);
w.on_tx_error(slate.id.to_string(), Some(e));
// Post transaction.
match w.post(&s, Some(*id)) {
Ok(_) => {
sync_wallet_data(&w, false);
}
Err(e) => {
error!("tx post error: {:?}", e);
w.on_tx_error(*id, Some(e));
}
}
} else {
w.on_tx_error(*id, Some(Error::GenericError("tx slate not found".to_string())));
}
}
WalletTask::Cancel(tx) => {
match w.cancel(tx) {
WalletTask::Cancel(id) => {
match w.cancel(*id) {
Ok(_) => {
sync_wallet_data(&w, false);
}
Err(e) => {
error!("tx cancel error: {:?}", e);
let id = tx.tx_slate_id.unwrap().to_string();
w.on_tx_error(id, Some(e));
w.on_tx_error(*id, Some(e));
}
}
}
@@ -1808,6 +1836,15 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
w.proof_verifying.store(false, Ordering::Relaxed);
w.on_task_result(None, &WalletTask::VerifyProof(p.clone(), Some(res)));
}
WalletTask::Delete(id) => {
match w.delete_tx(*id) {
Ok(_) => sync_wallet_data(&w, false),
Err(e) => {
error!("tx delete error: {:?}", e);
w.on_tx_error(*id, Some(e));
}
}
}
};
}
@@ -1947,11 +1984,11 @@ fn update_txs(wallet: &Wallet, mut txs_limit: u32) -> Result<(), Error> {
let tx_height_store = TxHeightStore::new(wallet.get_config().get_extra_db_path());
let data = wallet.get_data().unwrap();
let data_txs = data.txs.unwrap_or(vec![]);
let mut new_txs: Vec<WalletTransaction> = vec![];
let mut new_txs: Vec<WalletTx> = vec![];
for tx in &filter_txs {
let mut height: Option<u64> = None;
let mut broadcasting_height: Option<u64> = None;
let mut action: Option<WalletTransactionAction> = None;
let mut action: Option<WalletTxAction> = None;
let mut action_error: Option<Error> = None;
let mut proof: Option<PaymentProof> = None;
for t in &data_txs {
@@ -1964,13 +2001,13 @@ fn update_txs(wallet: &Wallet, mut txs_limit: u32) -> Result<(), Error> {
break;
}
}
let mut new = WalletTransaction::new(tx.clone(),
proof.clone(),
wallet,
height,
broadcasting_height,
action,
action_error);
let mut new = WalletTx::new(tx.clone(),
proof.clone(),
wallet,
height,
broadcasting_height,
action,
action_error);
// Update Slate state for unconfirmed.
let unconfirmed = !tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent ||
tx.tx_type == TxLogEntryType::TxReceived);
@@ -2014,8 +2051,9 @@ fn update_txs(wallet: &Wallet, mut txs_limit: u32) -> Result<(), Error> {
new.broadcasting_height = broadcasting_height;
}
}
new_txs.push(new);
if !new.deleting() {
new_txs.push(new);
}
}
// Update wallet txs.
let mut w_data = wallet.data.write();
+1 -1
Submodule wallet updated: a6e0cdae0c...da17e16541