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:
parent
951886f5c7
commit
be80e82727
14 changed files with 199 additions and 27 deletions
|
@ -25,6 +25,7 @@ share: Share
|
|||
theme: 'Theme:'
|
||||
dark: Dark
|
||||
light: Light
|
||||
choose_file: Choose file
|
||||
wallets:
|
||||
await_conf_amount: Awaiting confirmation
|
||||
await_fin_amount: Awaiting finalization
|
||||
|
|
|
@ -25,6 +25,7 @@ share: Поделиться
|
|||
theme: 'Тема:'
|
||||
dark: Тёмная
|
||||
light: Светлая
|
||||
choose_file: Выбрать файл
|
||||
wallets:
|
||||
await_conf_amount: Ожидает подтверждения
|
||||
await_fin_amount: Ожидает завершения
|
||||
|
|
|
@ -25,6 +25,7 @@ share: Paylasmak
|
|||
theme: 'Tema:'
|
||||
dark: Karanlik
|
||||
light: Isik
|
||||
choose_file: Dosya seçin
|
||||
wallets:
|
||||
await_conf_amount: Onay bekleniyor
|
||||
await_fin_amount: Tamamlanma bekleniyor
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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! {
|
||||
|
|
|
@ -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! {
|
||||
|
|
|
@ -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
src/gui/views/file.rs
Normal file
95
src/gui/views/file.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -37,3 +37,6 @@ pub use camera::*;
|
|||
|
||||
mod qr;
|
||||
pub use qr::*;
|
||||
|
||||
mod file;
|
||||
pub use 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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -156,13 +156,15 @@ pub struct WalletTransaction {
|
|||
/// Block height when tx was confirmed.
|
||||
pub conf_height: Option<u64>,
|
||||
/// Block height when tx was reposted.
|
||||
pub repost_height: Option<u64>
|
||||
pub repost_height: Option<u64>,
|
||||
/// Flag to check if tx was received after sync from node.
|
||||
pub from_node: bool,
|
||||
}
|
||||
|
||||
impl WalletTransaction {
|
||||
/// Check if transaction can be cancelled.
|
||||
pub fn can_cancel(&self) -> bool {
|
||||
!self.cancelling && !self.posting && !self.data.confirmed &&
|
||||
self.from_node && !self.cancelling && !self.posting && !self.data.confirmed &&
|
||||
self.data.tx_type != TxLogEntryType::TxReceivedCancelled
|
||||
&& self.data.tx_type != TxLogEntryType::TxSentCancelled
|
||||
}
|
||||
|
@ -171,7 +173,7 @@ impl WalletTransaction {
|
|||
pub fn can_repost(&self, data: &WalletData) -> bool {
|
||||
let last_height = data.info.last_confirmed_height;
|
||||
let min_conf = data.info.minimum_confirmations;
|
||||
self.posting && self.repost_height.is_some() &&
|
||||
self.from_node && self.posting && self.repost_height.is_some() &&
|
||||
last_height - self.repost_height.unwrap() > min_conf
|
||||
}
|
||||
}
|
|
@ -584,9 +584,9 @@ impl Wallet {
|
|||
}
|
||||
|
||||
/// Parse Slatepack message into [`Slate`].
|
||||
pub fn parse_slatepack(&self, message: &String) -> Result<Slate, grin_wallet_controller::Error> {
|
||||
pub fn parse_slatepack(&self, text: &String) -> Result<Slate, grin_wallet_controller::Error> {
|
||||
let mut api = Owner::new(self.instance.clone().unwrap(), None);
|
||||
return match parse_slatepack(&mut api, None, None, Some(message.clone())) {
|
||||
return match parse_slatepack(&mut api, None, None, Some(text.clone())) {
|
||||
Ok(s) => Ok(s.0),
|
||||
Err(e) => Err(e)
|
||||
}
|
||||
|
@ -1168,7 +1168,9 @@ fn start_sync(wallet: Wallet) -> Thread {
|
|||
let failed_sync = wallet.sync_error() || wallet.get_sync_attempts() != 0;
|
||||
|
||||
// Clear syncing status.
|
||||
wallet.syncing.store(false, Ordering::Relaxed);
|
||||
if !failed_sync {
|
||||
wallet.syncing.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
// Repeat after default or attempt delay if synchronization was not successful.
|
||||
let delay = if failed_sync {
|
||||
|
@ -1288,12 +1290,12 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
|
|||
tx.amount_credited - tx.amount_debited
|
||||
};
|
||||
|
||||
let unconfirmed_sent_or_received = tx.tx_slate_id.is_some() &&
|
||||
let unc_sent_or_received = tx.tx_slate_id.is_some() &&
|
||||
!tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent ||
|
||||
tx.tx_type == TxLogEntryType::TxReceived);
|
||||
|
||||
// Setup transaction posting status based on slate state.
|
||||
let posting = if unconfirmed_sent_or_received {
|
||||
let posting = if unc_sent_or_received {
|
||||
// Create slate to check existing file.
|
||||
let is_invoice = tx.tx_type == TxLogEntryType::TxReceived;
|
||||
let mut slate = Slate::blank(0, is_invoice);
|
||||
|
@ -1322,8 +1324,8 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
|
|||
};
|
||||
|
||||
// Setup flag for ability to finalize transaction.
|
||||
let can_finalize = if !posting && unconfirmed_sent_or_received {
|
||||
// Create slate to check existing file.
|
||||
let can_finalize = if from_node && !posting && unc_sent_or_received {
|
||||
// Check existing file.
|
||||
let mut slate = Slate::blank(1, false);
|
||||
slate.id = tx.tx_slate_id.unwrap();
|
||||
slate.state = match tx.tx_type {
|
||||
|
@ -1400,7 +1402,8 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
|
|||
posting,
|
||||
can_finalize,
|
||||
conf_height,
|
||||
repost_height
|
||||
repost_height,
|
||||
from_node
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue