diff --git a/src/ui/activities/filetransfer/browser.rs b/src/ui/activities/filetransfer/lib/browser.rs similarity index 95% rename from src/ui/activities/filetransfer/browser.rs rename to src/ui/activities/filetransfer/lib/browser.rs index df64876..5fe8147 100644 --- a/src/ui/activities/filetransfer/browser.rs +++ b/src/ui/activities/filetransfer/lib/browser.rs @@ -33,7 +33,7 @@ use crate::system::config_client::ConfigClient; /// /// File explorer tab #[derive(Clone, Copy)] -pub(super) enum FileExplorerTab { +pub enum FileExplorerTab { Local, Remote, FindLocal, // Find result tab @@ -43,7 +43,7 @@ pub(super) enum FileExplorerTab { /// ## Browser /// /// Browser contains the browser options -pub(super) struct Browser { +pub struct Browser { local: FileExplorer, // Local File explorer state remote: FileExplorer, // Remote File explorer state found: Option, // File explorer for find result @@ -55,7 +55,7 @@ impl Browser { /// ### new /// /// Build a new `Browser` struct - pub(super) fn new(cli: Option<&ConfigClient>) -> Self { + pub fn new(cli: Option<&ConfigClient>) -> Self { Self { local: Self::build_local_explorer(cli), remote: Self::build_remote_explorer(cli), @@ -99,14 +99,14 @@ impl Browser { self.found = None; } - pub(super) fn tab(&self) -> FileExplorerTab { + pub fn tab(&self) -> FileExplorerTab { self.tab } /// ### change_tab /// /// Update tab value - pub(super) fn change_tab(&mut self, tab: FileExplorerTab) { + pub fn change_tab(&mut self, tab: FileExplorerTab) { self.tab = tab; } diff --git a/src/ui/activities/filetransfer/lib/mod.rs b/src/ui/activities/filetransfer/lib/mod.rs new file mode 100644 index 0000000..4864265 --- /dev/null +++ b/src/ui/activities/filetransfer/lib/mod.rs @@ -0,0 +1,29 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +pub(crate) mod browser; +pub(crate) mod transfer; diff --git a/src/ui/activities/filetransfer/lib/transfer.rs b/src/ui/activities/filetransfer/lib/transfer.rs new file mode 100644 index 0000000..2fc7429 --- /dev/null +++ b/src/ui/activities/filetransfer/lib/transfer.rs @@ -0,0 +1,259 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use bytesize::ByteSize; +use std::fmt; +use std::time::Instant; + +/// ### TransferStates +/// +/// TransferStates contains the states related to the transfer process +pub struct TransferStates { + aborted: bool, // Describes whether the transfer process has been aborted + pub full: ProgressStates, // full transfer states + pub partial: ProgressStates, // Partial transfer states +} + +/// ### ProgressStates +/// +/// Progress states describes the states for the progress of a single transfer part +pub struct ProgressStates { + started: Instant, + total: usize, + written: usize, +} + +impl Default for TransferStates { + fn default() -> Self { + Self::new() + } +} + +impl TransferStates { + /// ### new + /// + /// Instantiates a new transfer states + pub fn new() -> TransferStates { + TransferStates { + aborted: false, + full: ProgressStates::default(), + partial: ProgressStates::default(), + } + } + + /// ### reset + /// + /// Re-intiialize transfer states + pub fn reset(&mut self) { + self.aborted = false; + } + + /// ### abort + /// + /// Set aborted to true + pub fn abort(&mut self) { + self.aborted = true; + } + + /// ### aborted + /// + /// Returns whether transfer has been aborted + pub fn aborted(&self) -> bool { + self.aborted + } +} + +impl Default for ProgressStates { + fn default() -> Self { + ProgressStates { + started: Instant::now(), + written: 0, + total: 0, + } + } +} + +impl fmt::Display for ProgressStates { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let eta: String = match self.calc_eta() { + 0 => String::from("--:--"), + seconds => format!( + "{:0width$}:{:0width$}", + (seconds / 60), + (seconds % 60), + width = 2 + ), + }; + write!( + f, + "{:.2}% - ETA {} ({}/s)", + self.calc_progress_percentage(), + eta, + ByteSize(self.calc_bytes_per_second()) + ) + } +} + +impl ProgressStates { + /// ### init + /// + /// Initialize a new Progress State + pub fn init(&mut self, sz: usize) { + self.started = Instant::now(); + self.total = sz; + self.written = 0; + } + + /// ### update_progress + /// + /// Update progress state + pub fn update_progress(&mut self, delta: usize) -> f64 { + self.written += delta; + self.calc_progress_percentage() + } + + /// ### calc_progress + /// + /// Calculate progress in a range between 0.0 to 1.0 + pub fn calc_progress(&self) -> f64 { + let prog: f64 = (self.written as f64) / (self.total as f64); + match prog > 1.0 { + true => 1.0, + false => prog, + } + } + + /// ### started + /// + /// Get started + pub fn started(&self) -> Instant { + self.started + } + + /// ### calc_progress_percentage + /// + /// Calculate the current transfer progress as percentage + fn calc_progress_percentage(&self) -> f64 { + self.calc_progress() * 100.0 + } + + /// ### calc_bytes_per_second + /// + /// Generic function to calculate bytes per second using elapsed time since transfer started and the bytes written + /// and the total amount of bytes to write + pub fn calc_bytes_per_second(&self) -> u64 { + // bytes_written : elapsed_secs = x : 1 + let elapsed_secs: u64 = self.started.elapsed().as_secs(); + match elapsed_secs { + 0 => match self.written == self.total { + // NOTE: would divide by 0 :D + true => self.total as u64, // Download completed in less than 1 second + false => 0, // 0 B/S + }, + _ => self.written as u64 / elapsed_secs, + } + } + + /// ### calc_eta + /// + /// Calculate ETA for current transfer as seconds + fn calc_eta(&self) -> u64 { + let elapsed_secs: u64 = self.started.elapsed().as_secs(); + let prog: f64 = self.calc_progress_percentage(); + match prog as u64 { + 0 => 0, + _ => ((elapsed_secs * 100) / (prog as u64)) - elapsed_secs, + } + } +} + +#[cfg(test)] +mod test { + + use super::*; + + use pretty_assertions::assert_eq; + use std::time::Duration; + + #[test] + fn test_ui_activities_filetransfer_lib_transfer_progress_states() { + let mut states: ProgressStates = ProgressStates::default(); + assert_eq!(states.total, 0); + assert_eq!(states.written, 0); + assert!(states.started().elapsed().as_secs() < 5); + // Init new transfer + states.init(1024); + assert_eq!(states.total, 1024); + assert_eq!(states.written, 0); + assert_eq!(states.calc_bytes_per_second(), 0); + assert_eq!(states.calc_eta(), 0); + assert_eq!(states.calc_progress_percentage(), 0.0); + assert_eq!(states.calc_progress(), 0.0); + assert_eq!(states.to_string().as_str(), "0.00% - ETA --:-- (0 B/s)"); + // Wait 4 second (virtually) + states.started = states.started.checked_sub(Duration::from_secs(4)).unwrap(); + // Update state + states.update_progress(256); + assert_eq!(states.total, 1024); + assert_eq!(states.written, 256); + assert_eq!(states.calc_bytes_per_second(), 64); // 256 bytes in 4 seconds + assert_eq!(states.calc_eta(), 12); // 16 total sub 4 + assert_eq!(states.calc_progress_percentage(), 25.0); + assert_eq!(states.calc_progress(), 0.25); + assert_eq!(states.to_string().as_str(), "25.00% - ETA 00:12 (64 B/s)"); + // 100% + states.started = states.started.checked_sub(Duration::from_secs(12)).unwrap(); + states.update_progress(768); + assert_eq!(states.total, 1024); + assert_eq!(states.written, 1024); + assert_eq!(states.calc_bytes_per_second(), 64); // 256 bytes in 4 seconds + assert_eq!(states.calc_eta(), 0); // 16 total sub 4 + assert_eq!(states.calc_progress_percentage(), 100.0); + assert_eq!(states.calc_progress(), 1.0); + assert_eq!(states.to_string().as_str(), "100.00% - ETA --:-- (64 B/s)"); + // Check if terminated at started + states.started = Instant::now(); + assert_eq!(states.calc_bytes_per_second(), 1024); + } + + #[test] + fn test_ui_activities_filetransfer_lib_transfer_states() { + let mut states: TransferStates = TransferStates::default(); + assert_eq!(states.aborted, false); + assert_eq!(states.full.total, 0); + assert_eq!(states.full.written, 0); + assert!(states.full.started.elapsed().as_secs() < 5); + assert_eq!(states.partial.total, 0); + assert_eq!(states.partial.written, 0); + assert!(states.partial.started.elapsed().as_secs() < 5); + // Aborted + states.abort(); + assert_eq!(states.aborted(), true); + states.reset(); + assert_eq!(states.aborted(), false); + } +} diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index 5327ba7..8ebdccc 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -27,7 +27,7 @@ */ // This module is split into files, cause it's just too big pub(self) mod actions; -pub(self) mod browser; +pub(self) mod lib; pub(self) mod misc; pub(self) mod session; pub(self) mod update; @@ -49,14 +49,15 @@ use crate::fs::explorer::FileExplorer; use crate::fs::FsEntry; use crate::host::Localhost; use crate::system::config_client::ConfigClient; -use browser::Browser; +pub(crate) use lib::browser; +use lib::browser::Browser; +use lib::transfer::TransferStates; // Includes use chrono::{DateTime, Local}; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use std::collections::VecDeque; use std::path::PathBuf; -use std::time::Instant; use tuirealm::View; // -- Storage keys @@ -120,81 +121,6 @@ impl LogRecord { } } -/// ### TransferStates -/// -/// TransferStates contains the states related to the transfer process -struct TransferStates { - pub progress: f64, // Current read/write progress (percentage) - pub started: Instant, // Instant the transfer process started - pub aborted: bool, // Describes whether the transfer process has been aborted - pub bytes_written: usize, // Bytes written during transfer - pub bytes_total: usize, // Total bytes to write -} - -impl TransferStates { - /// ### new - /// - /// Instantiates a new transfer states - pub fn new() -> TransferStates { - TransferStates { - progress: 0.0, - started: Instant::now(), - aborted: false, - bytes_written: 0, - bytes_total: 0, - } - } - - /// ### reset - /// - /// Re-intiialize transfer states - pub fn reset(&mut self) { - self.progress = 0.0; - self.started = Instant::now(); - self.aborted = false; - self.bytes_written = 0; - self.bytes_total = 0; - } - - /// ### set_progress - /// - /// Calculate progress percentage based on current progress - pub fn set_progress(&mut self, w: usize, sz: usize) { - self.bytes_written = w; - self.bytes_total = sz; - let mut prog: f64 = ((self.bytes_written as f64) * 100.0) / (self.bytes_total as f64); - // Check value - if prog > 100.0 { - prog = 100.0; - } else if prog < 0.0 { - prog = 0.0; - } - self.progress = prog; - } - - /// ### byte_per_second - /// - /// Calculate bytes per second - pub fn bytes_per_second(&self) -> u64 { - // bytes_written : elapsed_secs = x : 1 - let elapsed_secs: u64 = self.started.elapsed().as_secs(); - match elapsed_secs { - 0 => match self.bytes_written == self.bytes_total { - // NOTE: would divide by 0 :D - true => self.bytes_total as u64, // Download completed in less than 1 second - false => 0, // 0 B/S - }, - _ => self.bytes_written as u64 / elapsed_secs, - } - } -} - -impl Default for TransferStates { - fn default() -> Self { - Self::new() - } -} - /// ## FileTransferActivity /// /// FileTransferActivity is the data holder for the file transfer activity diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs index 5257531..38a5804 100644 --- a/src/ui/activities/filetransfer/session.rs +++ b/src/ui/activities/filetransfer/session.rs @@ -161,6 +161,8 @@ impl FileTransferActivity { curr_remote_path: &Path, dst_name: Option, ) { + // Reset states + self.transfer.reset(); // Write popup let file_name: String = match entry { FsEntry::Directory(dir) => dir.name.clone(), @@ -229,7 +231,7 @@ impl FileTransferActivity { // Iterate over files for entry in entries.iter() { // If aborted; break - if self.transfer.aborted { + if self.transfer.aborted() { break; } // Send entry; name is always None after first call @@ -264,14 +266,12 @@ impl FileTransferActivity { // Scan dir on remote self.reload_remote_dir(); // If aborted; show popup - if self.transfer.aborted { + if self.transfer.aborted() { // Log abort self.log_and_alert( LogLevel::Warn, format!("Upload aborted for \"{}\"!", entry.get_abs_path().display()), ); - // Set aborted to false - self.transfer.aborted = false; } else { // @! Successful // Eventually, Remove progress bar @@ -290,6 +290,8 @@ impl FileTransferActivity { local_path: &Path, dst_name: Option, ) { + // Reset states + self.transfer.reset(); // Write popup let file_name: String = match entry { FsEntry::Directory(dir) => dir.name.clone(), @@ -379,7 +381,7 @@ impl FileTransferActivity { // Iterate over files for entry in entries.iter() { // If transfer has been aborted; break - if self.transfer.aborted { + if self.transfer.aborted() { break; } // Receive entry; name is always None after first call @@ -415,7 +417,7 @@ impl FileTransferActivity { // Reload directory on local self.local_scan(local_path); // if aborted; show alert - if self.transfer.aborted { + if self.transfer.aborted() { // Log abort self.log_and_alert( LogLevel::Warn, @@ -424,8 +426,6 @@ impl FileTransferActivity { entry.get_abs_path().display() ), ); - // Reset aborted to false - self.transfer.aborted = false; } else { // Eventually, Reset input mode to explorer self.umount_progress_bar(); @@ -449,21 +449,21 @@ impl FileTransferActivity { // Write file let file_size: usize = fhnd.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize; + // Init transfer + self.transfer.partial.init(file_size); // rewind if let Err(err) = fhnd.seek(std::io::SeekFrom::Start(0)) { return Err(TransferErrorReason::CouldNotRewind(err)); } // Write remote file let mut total_bytes_written: usize = 0; - // Reset transfer states - self.transfer.reset(); let mut last_progress_val: f64 = 0.0; let mut last_input_event_fetch: Instant = Instant::now(); // Mount progress bar self.mount_progress_bar(); // While the entire file hasn't been completely written, // Or filetransfer has been aborted - while total_bytes_written < file_size && !self.transfer.aborted { + while total_bytes_written < file_size && !self.transfer.aborted() { // Handle input events (each 500ms) if last_input_event_fetch.elapsed().as_millis() >= 500 { // Read events @@ -473,18 +473,18 @@ impl FileTransferActivity { } // Read till you can let mut buffer: [u8; 65536] = [0; 65536]; - match fhnd.read(&mut buffer) { + let delta: usize = match fhnd.read(&mut buffer) { Ok(bytes_read) => { total_bytes_written += bytes_read; if bytes_read == 0 { continue; } else { - let mut buf_start: usize = 0; - while buf_start < bytes_read { + let mut delta: usize = 0; + while delta < bytes_read { // Write bytes - match rhnd.write(&buffer[buf_start..bytes_read]) { + match rhnd.write(&buffer[delta..bytes_read]) { Ok(bytes) => { - buf_start += bytes; + delta += bytes; } Err(err) => { self.umount_progress_bar(); @@ -494,21 +494,22 @@ impl FileTransferActivity { } } } + delta } } Err(err) => { self.umount_progress_bar(); return Err(TransferErrorReason::LocalIoError(err)); } - } + }; // Increase progress - self.transfer.set_progress(total_bytes_written, file_size); + self.transfer.partial.update_progress(delta); // Draw only if a significant progress has been made (performance improvement) - if last_progress_val < self.transfer.progress - 1.0 { + if last_progress_val < self.transfer.partial.calc_progress() - 0.01 { // Draw self.update_progress_bar(format!("Uploading \"{}\"...", file_name)); self.view(); - last_progress_val = self.transfer.progress; + last_progress_val = self.transfer.partial.calc_progress(); } } // Umount progress bar @@ -521,7 +522,7 @@ impl FileTransferActivity { ); } // if upload was abrupted, return error - if self.transfer.aborted { + if self.transfer.aborted() { return Err(TransferErrorReason::Abrupted); } self.log( @@ -530,8 +531,8 @@ impl FileTransferActivity { "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", local.abs_path.display(), remote.display(), - fmt_millis(self.transfer.started.elapsed()), - ByteSize(self.transfer.bytes_per_second()), + fmt_millis(self.transfer.partial.started().elapsed()), + ByteSize(self.transfer.partial.calc_bytes_per_second()), ), ); } @@ -558,8 +559,8 @@ impl FileTransferActivity { match self.client.recv_file(remote) { Ok(mut rhnd) => { let mut total_bytes_written: usize = 0; - // Reset transfer states - self.transfer.reset(); + // Init transfer + self.transfer.partial.init(remote.size); // Write local file let mut last_progress_val: f64 = 0.0; let mut last_input_event_fetch: Instant = Instant::now(); @@ -567,7 +568,7 @@ impl FileTransferActivity { self.mount_progress_bar(); // While the entire file hasn't been completely read, // Or filetransfer has been aborted - while total_bytes_written < remote.size && !self.transfer.aborted { + while total_bytes_written < remote.size && !self.transfer.aborted() { // Handle input events (each 500 ms) if last_input_event_fetch.elapsed().as_millis() >= 500 { // Read events @@ -577,17 +578,17 @@ impl FileTransferActivity { } // Read till you can let mut buffer: [u8; 65536] = [0; 65536]; - match rhnd.read(&mut buffer) { + let delta: usize = match rhnd.read(&mut buffer) { Ok(bytes_read) => { total_bytes_written += bytes_read; if bytes_read == 0 { continue; } else { - let mut buf_start: usize = 0; - while buf_start < bytes_read { + let mut delta: usize = 0; + while delta < bytes_read { // Write bytes - match local_file.write(&buffer[buf_start..bytes_read]) { - Ok(bytes) => buf_start += bytes, + match local_file.write(&buffer[delta..bytes_read]) { + Ok(bytes) => delta += bytes, Err(err) => { self.umount_progress_bar(); return Err(TransferErrorReason::LocalIoError( @@ -596,21 +597,22 @@ impl FileTransferActivity { } } } + delta } } Err(err) => { self.umount_progress_bar(); return Err(TransferErrorReason::RemoteIoError(err)); } - } + }; // Set progress - self.transfer.set_progress(total_bytes_written, remote.size); + self.transfer.partial.update_progress(delta); // Draw only if a significant progress has been made (performance improvement) - if last_progress_val < self.transfer.progress - 1.0 { + if last_progress_val < self.transfer.partial.calc_progress() - 0.01 { // Draw self.update_progress_bar(format!("Downloading \"{}\"", file_name)); self.view(); - last_progress_val = self.transfer.progress; + last_progress_val = self.transfer.partial.calc_progress(); } } // Umount progress bar @@ -623,7 +625,7 @@ impl FileTransferActivity { ); } // If download was abrupted, return Error - if self.transfer.aborted { + if self.transfer.aborted() { return Err(TransferErrorReason::Abrupted); } // Apply file mode to file @@ -648,8 +650,8 @@ impl FileTransferActivity { "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", remote.abs_path.display(), local.display(), - fmt_millis(self.transfer.started.elapsed()), - ByteSize(self.transfer.bytes_per_second()), + fmt_millis(self.transfer.partial.started().elapsed()), + ByteSize(self.transfer.partial.calc_bytes_per_second()), ), ); } diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index 4c3f156..c6e4f9e 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -42,7 +42,6 @@ use crate::fs::FsEntry; use crate::ui::components::{file_list::FileListPropsBuilder, logbox::LogboxPropsBuilder}; use crate::ui::keymap::*; // externals -use bytesize::ByteSize; use std::path::{Path, PathBuf}; use tuirealm::{ components::progress_bar::ProgressBarPropsBuilder, @@ -644,7 +643,7 @@ impl FileTransferActivity { // -- progress bar (COMPONENT_PROGRESS_BAR, &MSG_KEY_CTRL_C) => { // Set transfer aborted to True - self.transfer.aborted = true; + self.transfer.abort(); None } // -- fallback @@ -796,26 +795,9 @@ impl FileTransferActivity { pub(super) fn update_progress_bar(&mut self, text: String) -> Option<(String, Msg)> { match self.view.get_props(COMPONENT_PROGRESS_BAR) { Some(props) => { - // Calculate ETA - let elapsed_secs: u64 = self.transfer.started.elapsed().as_secs(); - let eta: String = match self.transfer.progress as u64 { - 0 => String::from("--:--"), // NOTE: would divide by 0 :D - _ => { - let eta: u64 = - ((elapsed_secs * 100) / (self.transfer.progress as u64)) - elapsed_secs; - format!("{:0width$}:{:0width$}", (eta / 60), (eta % 60), width = 2) - } - }; - // Calculate bytes/s - let label = format!( - "{:.2}% - ETA {} ({}/s)", - self.transfer.progress, - eta, - ByteSize(self.transfer.bytes_per_second()) - ); let props = ProgressBarPropsBuilder::from(props) - .with_texts(Some(text), label) - .with_progress(self.transfer.progress / 100.0) + .with_texts(Some(text), self.transfer.partial.to_string()) + .with_progress(self.transfer.partial.calc_progress()) .build(); self.view.update(COMPONENT_PROGRESS_BAR, props) }