ui: file picker button for slatepacks messages (text parsing), check if tx was synced from node, do not show txs actions on wallet loading

This commit is contained in:
ardocrat
2024-06-04 22:50:41 +03:00
parent 951886f5c7
commit be80e82727
14 changed files with 199 additions and 27 deletions
+10
View File
@@ -33,6 +33,8 @@ const GREEN: Color32 = Color32::from_rgb(0, 0x64, 0);
const RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4);
const FILL: Color32 = Color32::from_gray(244);
const FILL_DARK: Color32 = Color32::from_gray(24);
@@ -132,6 +134,14 @@ impl Colors {
}
}
pub fn blue() -> Color32 {
if use_dark() {
BLUE.linear_multiply(1.3)
} else {
BLUE
}
}
pub fn fill() -> Color32 {
if use_dark() {
FILL_DARK
+4
View File
@@ -143,6 +143,10 @@ impl PlatformCallbacks for Android {
&[JValue::Object(&JObject::from(arg_value))]).unwrap();
Ok(())
}
fn pick_file(&self) -> Option<String> {
None
}
}
lazy_static! {
+12
View File
@@ -131,6 +131,7 @@ impl PlatformCallbacks for Desktop {
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
let folder = FileDialog::new()
.set_title(t!("share"))
.set_directory(dirs::home_dir().unwrap())
.set_file_name(name.clone())
.save_file();
@@ -141,6 +142,17 @@ impl PlatformCallbacks for Desktop {
}
Ok(())
}
fn pick_file(&self) -> Option<String> {
let file = FileDialog::new()
.set_title(t!("choose_file"))
.set_directory(dirs::home_dir().unwrap())
.pick_file();
if let Some(file) = file {
return Some(file.to_str().unwrap_or_default().to_string());
}
None
}
}
lazy_static! {
+1
View File
@@ -32,4 +32,5 @@ pub trait PlatformCallbacks {
fn can_switch_camera(&self) -> bool;
fn switch_camera(&self);
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
fn pick_file(&self) -> Option<String>;
}
+95
View File
@@ -0,0 +1,95 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{fs, thread};
use parking_lot::RwLock;
use crate::gui::Colors;
use crate::gui::icons::FILE_ARROW_UP;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::View;
/// Button to pick file and parse its data into text.
pub struct FilePickButton {
/// Flag to check if file is parsing.
pub file_parsing: Arc<AtomicBool>,
/// File parsing result.
pub file_parsing_result: Arc<RwLock<Option<String>>>
}
impl Default for FilePickButton {
fn default() -> Self {
Self {
file_parsing: Arc::new(AtomicBool::new(false)),
file_parsing_result: Arc::new(RwLock::new(None))
}
}
}
impl FilePickButton {
/// Draw button content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
on_result: impl FnOnce(String)) {
if self.file_parsing.load(Ordering::Relaxed) {
// Draw loading spinner on file parsing.
View::small_loading_spinner(ui);
// Check file parsing result.
let has_result = {
let r_res = self.file_parsing_result.read();
r_res.is_some()
};
if has_result {
let text = {
let r_res = self.file_parsing_result.read();
r_res.clone().unwrap()
};
// Callback on result.
on_result(text);
// Clear result.
let mut w_res = self.file_parsing_result.write();
*w_res = None;
self.file_parsing.store(false, Ordering::Relaxed);
}
} else {
// Draw button to pick file.
let file_text = format!("{} {}", FILE_ARROW_UP, t!("choose_file"));
View::colored_text_button(ui, file_text, Colors::blue(), Colors::button(), || {
if let Some(path) = cb.pick_file() {
// Parse file at new thread.
self.file_parsing.store(true, Ordering::Relaxed);
let result = self.file_parsing_result.clone();
thread::spawn(move || {
if path.ends_with(".gif") {
//TODO: Detect QR codes on GIF file.
} else if path.ends_with(".jpeg") || path.ends_with(".jpg") ||
path.ends_with(".png") {
//TODO: Detect QR codes on image files.
} else {
// Parse file as plain text.
if let Ok(text) = fs::read_to_string(path) {
let mut w_res = result.write();
*w_res = Some(text);
}
}
});
}
});
}
}
}
+4 -1
View File
@@ -36,4 +36,7 @@ mod camera;
pub use camera::*;
mod qr;
pub use qr::*;
pub use qr::*;
mod file;
pub use file::*;
+1 -1
View File
@@ -43,7 +43,7 @@ pub struct Modal {
impl Modal {
/// Margin from [`Modal`] window at top/left/right.
const DEFAULT_MARGIN: f32 = 8.0;
const DEFAULT_MARGIN: f32 = 4.0;
/// Maximum width of the content.
const DEFAULT_WIDTH: f32 = Root::SIDE_PANEL_WIDTH - (2.0 * Self::DEFAULT_MARGIN);
+20 -4
View File
@@ -24,7 +24,7 @@ use parking_lot::RwLock;
use crate::gui::Colors;
use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, COPY, DOWNLOAD_SIMPLE, PROHIBIT, QR_CODE, SCAN, UPLOAD_SIMPLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, QrCodeContent, Root, View};
use crate::gui::views::{CameraContent, FilePickButton, Modal, QrCodeContent, Root, View};
use crate::gui::views::types::{ModalPosition, QrScanResult, TextEditOptions};
use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::WalletContent;
@@ -72,6 +72,8 @@ pub struct WalletMessages {
response_edit: String,
/// Flag to check if Dandelion is needed to finalize transaction.
dandelion: bool,
/// Button to parse picked file content.
file_pick_button: FilePickButton,
/// Flag to check if invoice or sending request was opened for [`Modal`].
request_invoice: bool,
@@ -168,6 +170,7 @@ impl WalletMessages {
message_error: None,
response_edit: "".to_string(),
dandelion,
file_pick_button: FilePickButton::default(),
request_amount_edit: "".to_string(),
request_edit: "".to_string(),
request_error: None,
@@ -795,7 +798,9 @@ impl WalletMessages {
ui.add_space(10.0);
// Draw clear button on message input or cancel and clear buttons on response.
// Draw clear button on message input,
// cancel and clear buttons on response
// or button to choose text or image file.
if !self.message_loading {
if self.message_slate.is_none() && !self.message_edit.is_empty() {
// Draw button to clear message input.
@@ -808,8 +813,8 @@ impl WalletMessages {
});
} else if !self.response_edit.is_empty() && self.message_slate.is_some() {
// Draw cancel button.
let cancel = format!("{} {}", PROHIBIT, t!("modal.cancel"));
View::colored_text_button(ui, cancel, Colors::red(), Colors::button(), || {
let cancel_text = format!("{} {}", PROHIBIT, t!("modal.cancel"));
View::colored_text_button(ui, cancel_text, Colors::red(), Colors::button(), || {
let slate = self.message_slate.clone().unwrap();
if let Some(tx) = wallet.tx_by_slate(&slate) {
wallet.cancel(tx.data.id);
@@ -818,6 +823,17 @@ impl WalletMessages {
self.message_slate = None;
}
});
} else {
// Draw button to choose file.
let mut parsed_text = "".to_string();
self.file_pick_button.ui(ui, cb, |text| {
parsed_text = text;
});
if !parsed_text.is_empty() {
// Parse Slatepack message from file content.
self.message_edit = parsed_text;
self.parse_message(wallet);
}
}
}
});
+33 -10
View File
@@ -24,9 +24,9 @@ use grin_wallet_libwallet::{Error, Slate, SlateState, TxLogEntryType};
use parking_lot::RwLock;
use crate::gui::Colors;
use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, ARROW_CLOCKWISE, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, CHECK_CIRCLE, CLIPBOARD_TEXT, COPY, DOTS_THREE_CIRCLE, FILE_ARCHIVE, FILE_TEXT, GEAR_FINE, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN, X_CIRCLE};
use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, ARROW_CLOCKWISE, BRIDGE, BROOM, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, CHECK_CIRCLE, CLIPBOARD_TEXT, COPY, DOTS_THREE_CIRCLE, FILE_ARCHIVE, FILE_TEXT, GEAR_FINE, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, QrCodeContent, Root, View};
use crate::gui::views::{CameraContent, FilePickButton, Modal, QrCodeContent, Root, View};
use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::types::WalletTab;
use crate::gui::views::wallets::wallet::types::{GRIN, SLATEPACK_MESSAGE_HINT, WalletTabType};
@@ -60,6 +60,8 @@ pub struct WalletTransactions {
tx_info_show_scanner: bool,
/// QR code scanner [`Modal`] content.
tx_info_scanner_content: CameraContent,
/// Button to parse picked file content at [`Modal`].
tx_info_file_pick_button: FilePickButton,
/// Transaction identifier to use at confirmation [`Modal`].
confirm_cancel_tx_id: Option<u32>,
@@ -83,6 +85,7 @@ impl Default for WalletTransactions {
tx_info_qr_code_content: QrCodeContent::new("".to_string(), true),
tx_info_show_scanner: false,
tx_info_scanner_content: CameraContent::default(),
tx_info_file_pick_button: FilePickButton::default(),
confirm_cancel_tx_id: None,
manual_sync: None,
}
@@ -146,8 +149,6 @@ impl WalletTransactions {
let amount_conf = data.info.amount_awaiting_confirmation;
let amount_fin = data.info.amount_awaiting_finalization;
let amount_locked = data.info.amount_locked;
// Show transactions info.
View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show non-zero awaiting confirmation amount.
if amount_conf != 0 {
@@ -228,10 +229,10 @@ impl WalletTransactions {
View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.3, |ui| {
let padding = amount_conf != 0 || amount_fin != 0 || amount_locked != 0;
for index in row_range {
// Show transaction item.
let tx = txs.get(index).unwrap();
let rounding = View::item_rounding(index, txs.len(), false);
self.tx_item_ui(ui, tx, rounding, padding, true, &data, wallet);
let r = View::item_rounding(index, txs.len(), false);
let show_info = tx.data.tx_slate_id.is_some();
self.tx_item_ui(ui, tx, r, padding, show_info, &data, wallet);
}
});
})
@@ -318,7 +319,7 @@ impl WalletTransactions {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to show transaction info.
if can_show_info && tx.data.tx_slate_id.is_some() {
if can_show_info && tx.from_node {
rounding.nw = 0.0;
rounding.sw = 0.0;
View::item_button(ui, rounding, FILE_TEXT, None, || {
@@ -357,7 +358,7 @@ impl WalletTransactions {
}
// Draw cancel button for tx that can be reposted and canceled.
let wallet_loaded = wallet.foreign_api_port().is_some();
let wallet_loaded = tx.from_node && wallet.foreign_api_port().is_some();
if wallet_loaded && ((!can_show_info && !self.tx_info_finalizing) || can_show_info) &&
(tx.can_repost(data) || tx.can_cancel()) {
View::item_button(ui, Rounding::default(), PROHIBIT, Some(Colors::red()), || {
@@ -828,7 +829,7 @@ impl WalletTransactions {
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
ui.add_space(10.0);
// Do not show buttons on finalization.
if self.tx_info_finalizing {
@@ -862,6 +863,28 @@ impl WalletTransactions {
}
});
});
ui.add_space(8.0);
ui.vertical_centered(|ui| {
if self.tx_info_finalize_error {
// Draw button to clear message input.
let clear_text = format!("{} {}", BROOM, t!("clear"));
View::button(ui, clear_text, Colors::button(), || {
self.tx_info_finalize_edit.clear();
self.tx_info_finalize_error = false;
});
} else {
// Draw button to choose file.
let mut parsed_text = "".to_string();
self.tx_info_file_pick_button.ui(ui, cb, |text| {
parsed_text = text;
});
if !parsed_text.is_empty() {
// Parse Slatepack message from file content.
self.tx_info_finalize_edit = parsed_text;
self.on_finalization_input_change(tx, wallet, modal, cb);
}
}
});
} else {
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {