Skip to content

Commit

Permalink
Merge #1496: gui: tx export feature
Browse files Browse the repository at this point in the history
22f4875 gui: add and export transactions feature (pythcoiner)

Pull request description:

  This PR add a feature to export transactions:
  Done:
   - [x] Subscription to run the  feature in a detached thread w/ possibility to send update about the ongoing progress.
   - [x] Let user choose the path using `rfd` crate
   - [x] Add a modal that show progress of the process
   - [x] estimate the progress
   - [x] cancel feature

ACKs for top commit:
  jp1ac4:
    Tested ACK 22f4875.

Tree-SHA512: 5cf271d52878c4845347c5951a562e08e7f7efea08f0dc702d0500e41d6ad8eab7cb31f7e8a7b4edba48916648759ce97a9f591d1bcb4564a0c7067d5274fa08
edouardparis committed Dec 30, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents ecce76a + 22f4875 commit cfe15b3
Showing 12 changed files with 1,155 additions and 23 deletions.
469 changes: 466 additions & 3 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions liana-gui/Cargo.toml
Original file line number Diff line number Diff line change
@@ -50,6 +50,7 @@ base64 = "0.21"
bitcoin_hashes = "0.12"
reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls"] }
rust-ini = "0.19.0"
rfd = "0.15.1"


[target.'cfg(windows)'.dependencies]
2 changes: 2 additions & 0 deletions liana-gui/src/app/message.rs
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ use lianad::config::Config as DaemonConfig;
use crate::{
app::{cache::Cache, error::Error, view, wallet::Wallet},
daemon::model::*,
export::ExportMessage,
hw::HardwareWalletMessage,
};

@@ -46,4 +47,5 @@ pub enum Message {
LabelsUpdated(Result<HashMap<String, Option<String>>, Error>),
BroadcastModal(Result<HashSet<Txid>, Error>),
RbfModal(Box<HistoryTransaction>, bool, Result<HashSet<Txid>, Error>),
Export(ExportMessage),
}
130 changes: 130 additions & 0 deletions liana-gui/src/app/state/export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use std::{
path::PathBuf,
sync::{Arc, Mutex},
};

use iced::{Command, Subscription};
use liana_ui::{component::modal::Modal, widget::Element};
use tokio::task::JoinHandle;

use crate::{
app::{
message::Message,
view::{self, export::export_modal},
},
daemon::Daemon,
export::{self, get_path, ExportMessage, ExportProgress, ExportState},
};

#[derive(Debug)]
pub struct ExportModal {
path: Option<PathBuf>,
handle: Option<Arc<Mutex<JoinHandle<()>>>>,
state: ExportState,
error: Option<export::Error>,
daemon: Arc<dyn Daemon + Sync + Send>,
}

impl ExportModal {
#[allow(clippy::new_without_default)]
pub fn new(daemon: Arc<dyn Daemon + Sync + Send>) -> Self {
Self {
path: None,
handle: None,
state: ExportState::Init,
error: None,
daemon,
}
}

pub fn launch(&self) -> Command<Message> {
Command::perform(get_path(), |m| {
Message::View(view::Message::Export(ExportMessage::Path(m)))
})
}

pub fn update(&mut self, message: Message) -> Command<Message> {
if let Message::View(view::Message::Export(m)) = message {
match m {
ExportMessage::ExportProgress(m) => match m {
ExportProgress::Started(handle) => {
self.handle = Some(handle);
self.state = ExportState::Progress(0.0);
}
ExportProgress::Progress(p) => {
if let ExportState::Progress(_) = self.state {
self.state = ExportState::Progress(p);
}
}
ExportProgress::Finished | ExportProgress::Ended => {
self.state = ExportState::Ended
}
ExportProgress::Error(e) => self.error = Some(e),
ExportProgress::None => {}
},
ExportMessage::TimedOut => {
self.stop(ExportState::TimedOut);
}
ExportMessage::UserStop => {
self.stop(ExportState::Aborted);
}
ExportMessage::Path(p) => {
if let Some(path) = p {
self.path = Some(path);
self.start();
} else {
return Command::perform(async {}, |_| {
Message::View(view::Message::Export(ExportMessage::Close))
});
}
}
ExportMessage::Close | ExportMessage::Open => { /* unreachable */ }
}
Command::none()
} else {
Command::none()
}
}
pub fn view<'a>(&'a self, content: Element<'a, view::Message>) -> Element<view::Message> {
let modal = Modal::new(
content,
export_modal(&self.state, self.error.as_ref(), "Transactions"),
);
match self.state {
ExportState::TimedOut
| ExportState::Aborted
| ExportState::Ended
| ExportState::Closed => modal.on_blur(Some(view::Message::Close)),
_ => modal,
}
.into()
}

pub fn start(&mut self) {
self.state = ExportState::Started;
}

pub fn stop(&mut self, state: ExportState) {
if let Some(handle) = self.handle.take() {
handle.lock().expect("poisoned").abort();
self.state = state;
}
}

pub fn subscription(&self) -> Option<Subscription<export::ExportProgress>> {
if let Some(path) = &self.path {
match &self.state {
ExportState::Started | ExportState::Progress(_) => {
Some(iced::subscription::unfold(
"transactions",
export::State::new(self.daemon.clone(), Box::new(path.to_path_buf())),
export::export_subscription,
))
}
_ => None,
}
} else {
None
}
}
}
1 change: 1 addition & 0 deletions liana-gui/src/app/state/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod coins;
mod export;
mod label;
mod psbt;
mod psbts;
72 changes: 56 additions & 16 deletions liana-gui/src/app/state/transactions.rs
Original file line number Diff line number Diff line change
@@ -28,20 +28,30 @@ use crate::{
wallet::Wallet,
},
daemon::model::{self, LabelsLoader},
export::ExportMessage,
};

use crate::daemon::{
model::{CreateSpendResult, HistoryTransaction, LabelItem, Labelled},
Daemon,
};

use super::export::ExportModal;

#[derive(Debug)]
pub enum TransactionsModal {
CreateRbf(CreateRbfModal),
Export(ExportModal),
None,
}

pub struct TransactionsPanel {
wallet: Arc<Wallet>,
txs: Vec<HistoryTransaction>,
labels_edited: LabelsEdited,
selected_tx: Option<HistoryTransaction>,
warning: Option<Error>,
create_rbf_modal: Option<CreateRbfModal>,
modal: TransactionsModal,
is_last_page: bool,
processing: bool,
}
@@ -54,7 +64,7 @@ impl TransactionsPanel {
txs: Vec::new(),
labels_edited: LabelsEdited::default(),
warning: None,
create_rbf_modal: None,
modal: TransactionsModal::None,
is_last_page: false,
processing: false,
}
@@ -63,7 +73,7 @@ impl TransactionsPanel {
pub fn preselect(&mut self, tx: HistoryTransaction) {
self.selected_tx = Some(tx);
self.warning = None;
self.create_rbf_modal = None;
self.modal = TransactionsModal::None;
}
}

@@ -76,19 +86,22 @@ impl State for TransactionsPanel {
self.labels_edited.cache(),
self.warning.as_ref(),
);
if let Some(modal) = &self.create_rbf_modal {
modal.view(content)
} else {
content
match &self.modal {
TransactionsModal::CreateRbf(rbf) => rbf.view(content),
_ => content,
}
} else {
view::transactions::transactions_view(
let content = view::transactions::transactions_view(
cache,
&self.txs,
self.warning.as_ref(),
self.is_last_page,
self.processing,
)
);
match &self.modal {
TransactionsModal::Export(export) => export.view(content),
_ => content,
}
}
}

@@ -134,7 +147,7 @@ impl State for TransactionsPanel {
Message::RbfModal(tx, is_cancel, res) => match res {
Ok(descendant_txids) => {
let modal = CreateRbfModal::new(*tx, is_cancel, descendant_txids);
self.create_rbf_modal = Some(modal);
self.modal = TransactionsModal::CreateRbf(modal);
}
Err(e) => {
self.warning = e.into();
@@ -146,16 +159,16 @@ impl State for TransactionsPanel {
Message::View(view::Message::Select(i)) => {
self.selected_tx = self.txs.get(i).cloned();
// Clear modal if it's for a different tx.
if let Some(modal) = &self.create_rbf_modal {
if let TransactionsModal::CreateRbf(modal) = &self.modal {
if Some(modal.tx.tx.txid())
!= self.selected_tx.as_ref().map(|selected| selected.tx.txid())
{
self.create_rbf_modal = None;
self.modal = TransactionsModal::None;
}
}
}
Message::View(view::Message::CreateRbf(view::CreateRbfMessage::Cancel)) => {
self.create_rbf_modal = None;
self.modal = TransactionsModal::None;
}
Message::View(view::Message::CreateRbf(view::CreateRbfMessage::New(is_cancel))) => {
if let Some(tx) = &self.selected_tx {
@@ -249,11 +262,26 @@ impl State for TransactionsPanel {
);
}
}
_ => {
if let Some(modal) = &mut self.create_rbf_modal {
return modal.update(daemon, _cache, message);
Message::View(view::Message::Export(ExportMessage::Open)) => {
if let TransactionsModal::None = &self.modal {
self.modal = TransactionsModal::Export(ExportModal::new(daemon));
if let TransactionsModal::Export(m) = &self.modal {
return m.launch();
}
}
}
Message::View(view::Message::Export(ExportMessage::Close)) => {
if let TransactionsModal::Export(_) = &self.modal {
self.modal = TransactionsModal::None;
}
}
_ => {
return match &mut self.modal {
TransactionsModal::CreateRbf(modal) => modal.update(daemon, _cache, message),
TransactionsModal::Export(modal) => modal.update(message),
TransactionsModal::None => Command::none(),
};
}
};
Command::none()
}
@@ -284,6 +312,17 @@ impl State for TransactionsPanel {
Message::HistoryTransactions,
)])
}

fn subscription(&self) -> iced::Subscription<Message> {
if let TransactionsModal::Export(modal) = &self.modal {
if let Some(sub) = modal.subscription() {
return sub.map(|m| {
Message::View(view::Message::Export(ExportMessage::ExportProgress(m)))
});
}
}
iced::Subscription::none()
}
}

impl From<TransactionsPanel> for Box<dyn State> {
@@ -292,6 +331,7 @@ impl From<TransactionsPanel> for Box<dyn State> {
}
}

#[derive(Debug)]
pub struct CreateRbfModal {
/// Transaction to replace.
tx: model::HistoryTransaction,
88 changes: 88 additions & 0 deletions liana-gui/src/app/view/export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use iced::{
widget::{progress_bar, Button, Column, Container, Row, Space},
Length,
};
use liana_ui::{
component::{
card,
text::{h4_bold, text},
},
theme,
widget::Element,
};

use crate::export::{Error, ExportMessage};
use crate::{app::view::message::Message, export::ExportState};

/// Return the modal view for an export task
pub fn export_modal<'a>(
state: &ExportState,
error: Option<&'a Error>,
export_type: &str,
) -> Element<'a, Message> {
let button = match state {
ExportState::Started | ExportState::Progress(_) => {
Some(Button::new("Cancel").on_press(ExportMessage::UserStop.into()))
}
ExportState::Ended | ExportState::TimedOut | ExportState::Aborted => {
Some(Button::new("Close").on_press(ExportMessage::Close.into()))
}
_ => None,
}
.map(|b| b.height(32).style(theme::Button::Primary));
let msg = if let Some(error) = error {
format!("{:?}", error)
} else {
match state {
ExportState::Init => "".to_string(),
ExportState::ChoosePath => {
"Select the path you want to export in the popup window...".into()
}
ExportState::Path(_) => "".into(),
ExportState::Started => "Starting export...".into(),
ExportState::Progress(p) => format!("Progress: {}%", p.round()),
ExportState::TimedOut => "Export failed: timeout".into(),
ExportState::Aborted => "Export canceled".into(),
ExportState::Ended => "Export successful!".into(),
ExportState::Closed => "".into(),
}
};
let p = match state {
ExportState::Init => 0.0,
ExportState::ChoosePath | ExportState::Path(_) | ExportState::Started => 5.0,
ExportState::Progress(p) => *p,
ExportState::TimedOut | ExportState::Aborted | ExportState::Ended | ExportState::Closed => {
100.0
}
};
let progress_bar_row = Row::new()
.push(Space::with_width(30))
.push(progress_bar(0.0..=100.0, p))
.push(Space::with_width(30));
let button_row = button.map(|b| {
Row::new()
.push(Space::with_width(Length::Fill))
.push(b)
.push(Space::with_width(Length::Fill))
});
card::simple(
Column::new()
.spacing(10)
.push(Container::new(h4_bold(format!("Export {export_type}"))).width(Length::Fill))
.push(Space::with_height(Length::Fill))
.push(progress_bar_row)
.push(Space::with_height(Length::Fill))
.push(
Row::new()
.push(Space::with_width(Length::Fill))
.push(text(msg))
.push(Space::with_width(Length::Fill)),
)
.push(Space::with_height(Length::Fill))
.push_maybe(button_row)
.push(Space::with_height(5)),
)
.width(Length::Fixed(500.0))
.height(Length::Fixed(220.0))
.into()
}
3 changes: 2 additions & 1 deletion liana-gui/src/app/view/message.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{app::menu::Menu, node::bitcoind::RpcAuthType};
use crate::{app::menu::Menu, export::ExportMessage, node::bitcoind::RpcAuthType};
use liana::miniscript::bitcoin::{bip32::Fingerprint, OutPoint};

#[derive(Debug, Clone)]
@@ -19,6 +19,7 @@ pub enum Message {
SelectHardwareWallet(usize),
CreateRbf(CreateRbfMessage),
ShowQrCode(usize),
Export(ExportMessage),
}

#[derive(Debug, Clone)]
1 change: 1 addition & 0 deletions liana-gui/src/app/view/mod.rs
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ mod message;
mod warning;

pub mod coins;
pub mod export;
pub mod home;
pub mod hw;
pub mod psbt;
24 changes: 21 additions & 3 deletions liana-gui/src/app/view/transactions.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use std::collections::{HashMap, HashSet};

use chrono::{DateTime, Local, Utc};
use iced::{alignment, widget::tooltip, Alignment, Length};
use iced::{
alignment,
widget::{tooltip, Space},
Alignment, Length,
};

use liana_ui::{
color,
@@ -15,9 +19,14 @@ use crate::{
cache::Cache,
error::Error,
menu::Menu,
view::{dashboard, label, message::CreateRbfMessage, message::Message, warning::warn},
view::{
dashboard, label,
message::{CreateRbfMessage, Message},
warning::warn,
},
},
daemon::model::{HistoryTransaction, Txid},
export::ExportMessage,
};

pub fn transactions_view<'a>(
@@ -32,7 +41,16 @@ pub fn transactions_view<'a>(
cache,
warning,
Column::new()
.push(Container::new(h3("Transactions")).width(Length::Fill))
.push(
Row::new()
.push(Container::new(h3("Transactions")))
.push(Space::with_width(Length::Fill))
.push(
Button::new("Export")
.on_press(ExportMessage::Open.into())
.style(theme::Button::Secondary),
),
)
.push(
Column::new()
.spacing(10)
386 changes: 386 additions & 0 deletions liana-gui/src/export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,386 @@
use std::{
collections::HashMap,
fs::{self, File},
io::Write,
path::PathBuf,
sync::{
mpsc::{channel, Receiver, Sender},
Arc, Mutex,
},
time::{self},
};

use chrono::{DateTime, Duration, Utc};
use liana::miniscript::bitcoin::{Amount, Txid};
use tokio::{
task::{JoinError, JoinHandle},
time::sleep,
};

use crate::{
app::view,
daemon::{
model::{HistoryTransaction, Labelled},
Daemon, DaemonBackend, DaemonError,
},
lianalite::client::backend::api::DEFAULT_LIMIT,
};

macro_rules! send_error {
($sender:ident, $error:ident) => {
if let Err(e) = $sender.send(ExportProgress::Error(Error::$error)) {
tracing::error!("ExportState::start() fail to send msg: {}", e);
}
};
($sender:ident, $error:expr) => {
if let Err(e) = $sender.send(ExportProgress::Error($error)) {
tracing::error!("ExportState::start() fail to send msg: {}", e);
}
};
}

macro_rules! send_progress {
($sender:ident, $progress:ident) => {
if let Err(e) = $sender.send(ExportProgress::$progress) {
tracing::error!("ExportState::start() fail to send msg: {}", e);
}
};
($sender:ident, $progress:ident($val:expr)) => {
if let Err(e) = $sender.send(ExportProgress::$progress($val)) {
tracing::error!("ExportState::start() fail to send msg: {}", e);
}
};
}

#[derive(Debug, Clone)]
pub enum ExportMessage {
Open,
ExportProgress(ExportProgress),
TimedOut,
UserStop,
Path(Option<PathBuf>),
Close,
}

impl From<ExportMessage> for view::Message {
fn from(value: ExportMessage) -> Self {
Self::Export(value)
}
}

#[derive(Debug, PartialEq)]
pub enum ExportState {
Init,
ChoosePath,
Path(PathBuf),
Started,
Progress(f32),
TimedOut,
Aborted,
Ended,
Closed,
}

#[derive(Debug, Clone)]
pub enum Error {
Io(String),
HandleLost,
UnexpectedEnd,
JoinError(String),
ChannelLost,
NoParentDir,
Daemon(String),
TxTimeMissing,
}

impl From<JoinError> for Error {
fn from(value: JoinError) -> Self {
Error::JoinError(format!("{:?}", value))
}
}

impl From<std::io::Error> for Error {
fn from(value: std::io::Error) -> Self {
Error::Io(format!("{:?}", value))
}
}

impl From<DaemonError> for Error {
fn from(value: DaemonError) -> Self {
Error::Daemon(format!("{:?}", value))
}
}

#[derive(Debug)]
pub enum Status {
Init,
Running,
Stopped,
}

#[derive(Debug, Clone)]
pub enum ExportProgress {
Started(Arc<Mutex<JoinHandle<()>>>),
Progress(f32),
Ended,
Finished,
Error(Error),
None,
}

pub struct State {
pub receiver: Receiver<ExportProgress>,
pub sender: Option<Sender<ExportProgress>>,
pub handle: Option<Arc<Mutex<JoinHandle<()>>>>,
pub daemon: Arc<dyn Daemon + Sync + Send>,
pub path: Box<PathBuf>,
}

impl State {
pub fn new(daemon: Arc<dyn Daemon + Sync + Send>, path: Box<PathBuf>) -> Self {
let (sender, receiver) = channel();
State {
receiver,
sender: Some(sender),
handle: None,
daemon,
path,
}
}

pub async fn start(&mut self) {
if let (true, Some(sender)) = (self.handle.is_none(), self.sender.take()) {
let daemon = self.daemon.clone();
let path = self.path.clone();

let cloned_sender = sender.clone();
let handle = tokio::spawn(async move {
let dir = match path.parent() {
Some(dir) => dir,
None => {
send_error!(sender, NoParentDir);
return;
}
};
if !dir.exists() {
if let Err(e) = fs::create_dir_all(dir) {
send_error!(sender, e.into());
return;
}
}
let mut file = match File::create(path.as_path()) {
Ok(f) => f,
Err(e) => {
send_error!(sender, e.into());
return;
}
};

let header = "Date,Label,Value,Fee,Txid,Block\n".to_string();
if let Err(e) = file.write_all(header.as_bytes()) {
send_error!(sender, e.into());
return;
}

// look 2 hour forward
// https://github.com/bitcoin/bitcoin/blob/62bd61de110b057cbfd6e31e4d0b727d93119c72/src/chain.h#L29
let mut end = ((Utc::now() + Duration::hours(2)).timestamp()) as u32;
let total_txs = daemon.list_confirmed_txs(0, end, u32::MAX as u64).await;
let total_txs = match total_txs {
Ok(r) => r.transactions.len(),
Err(e) => {
send_error!(sender, e.into());
return;
}
};

if total_txs == 0 {
send_progress!(sender, Ended);
} else {
send_progress!(sender, Progress(5.0));
}

let max = match daemon.backend() {
DaemonBackend::RemoteBackend => DEFAULT_LIMIT as u64,
_ => u32::MAX as u64,
};

// store txs in a map to avoid duplicates
let mut map = HashMap::<Txid, HistoryTransaction>::new();
let mut limit = max;

loop {
let history = daemon.list_history_txs(0, end, limit).await;
let history_txs = match history {
Ok(h) => h,
Err(e) => {
send_error!(sender, e.into());
return;
}
};
let dl = map.len() + history_txs.len();
if dl > 0 {
let progress = (dl as f32) / (total_txs as f32) * 80.0;
send_progress!(sender, Progress(progress));
}
// all txs have been fetched
if history_txs.is_empty() {
break;
}
if history_txs.len() == limit as usize {
let first = if let Some(t) = history_txs.first().expect("checked").time {
t
} else {
send_error!(sender, TxTimeMissing);
return;
};
let last = if let Some(t) = history_txs.last().expect("checked").time {
t
} else {
send_error!(sender, TxTimeMissing);
return;
};
// limit too low, all tx are in the same timestamp
// we must increase limit and retry
if first == last {
limit += DEFAULT_LIMIT as u64;
continue;
} else {
// add txs to map
for tx in history_txs {
let txid = tx.txid;
map.insert(txid, tx);
}
limit = max;
end = first.min(last);
continue;
}
} else
/* history_txs.len() < limit */
{
// add txs to map
for tx in history_txs {
let txid = tx.txid;
map.insert(txid, tx);
}
break;
}
}

let mut txs: Vec<_> = map.into_values().collect();
txs.sort_by(|a, b| a.compare(b));

for mut tx in txs {
let date_time = tx
.time
.map(|t| {
let mut str = DateTime::from_timestamp(t as i64, 0)
.expect("bitcoin timestamp")
.to_rfc3339();
//str has the form `1996-12-19T16:39:57-08:00`
// ^ ^^^^^^
// replace `T` by ` `| | drop this part
str = str.replace("T", " ");
str[0..(str.len() - 6)].to_string()
})
.unwrap_or("".to_string());

let txid = tx.txid.clone().to_string();
let txid_label = tx.labels().get(&txid).cloned();
let mut label = if let Some(txid) = txid_label {
txid
} else {
"".to_string()
};
if !label.is_empty() {
label = format!("\"{}\"", label);
}
let txid = tx.txid.to_string();
let fee = tx.fee_amount.unwrap_or(Amount::ZERO).to_sat() as i128;
let mut inputs_amount = 0;
tx.coins.iter().for_each(|(_, coin)| {
inputs_amount += coin.amount.to_sat() as i128;
});
let value = tx.incoming_amount.to_sat() as i128 - inputs_amount;
let value = value as f64 / 100_000_000.0;
let fee = fee as f64 / 100_000_000.0;
let block = tx.height.map(|h| h.to_string()).unwrap_or("".to_string());

let line = format!(
"{},{},{},{},{},{}\n",
date_time, label, value, fee, txid, block
);
if let Err(e) = file.write_all(line.as_bytes()) {
send_error!(sender, e.into());
return;
}
}
send_progress!(sender, Progress(100.0));
send_progress!(sender, Ended);
});
let handle = Arc::new(Mutex::new(handle));

// we send the handle to the GUI so we can kill the thread on timeout
// or user cancel action
send_progress!(cloned_sender, Started(handle.clone()));
self.handle = Some(handle);
} else {
tracing::error!("ExportState can start only once!");
}
}
pub fn state(&self) -> Status {
match (&self.sender, &self.handle) {
(Some(_), None) => Status::Init,
(None, Some(_)) => Status::Running,
(None, None) => Status::Stopped,
_ => unreachable!(),
}
}
}

pub async fn export_subscription(mut state: State) -> (ExportProgress, State) {
match state.state() {
Status::Init => {
state.start().await;
}
Status::Stopped => {
sleep(time::Duration::from_millis(1000)).await;
return (ExportProgress::None, state);
}
Status::Running => { /* continue */ }
}
let msg = state.receiver.try_recv();
let disconnected = match msg {
Ok(m) => return (m, state),
Err(e) => match e {
std::sync::mpsc::TryRecvError::Empty => false,
std::sync::mpsc::TryRecvError::Disconnected => true,
},
};

let handle = match state.handle.take() {
Some(h) => h,
None => return (ExportProgress::Error(Error::HandleLost), state),
};
{
let h = handle.lock().expect("should not fail");
if h.is_finished() {
return (ExportProgress::Finished, state);
} else if disconnected {
return (ExportProgress::Error(Error::ChannelLost), state);
}
} // => release handle lock
state.handle = Some(handle);

sleep(time::Duration::from_millis(100)).await;
(ExportProgress::None, state)
}

pub async fn get_path() -> Option<PathBuf> {
rfd::AsyncFileDialog::new()
.set_title("Choose a location to export...")
.set_file_name("liana.csv")
.save_file()
.await
.map(|fh| fh.path().to_path_buf())
}
1 change: 1 addition & 0 deletions liana-gui/src/lib.rs
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ pub mod app;
pub mod daemon;
pub mod datadir;
pub mod download;
pub mod export;
pub mod hw;
pub mod installer;
pub mod launcher;

0 comments on commit cfe15b3

Please sign in to comment.