Files · Transfers: file operations, a transfer queue, downloads and storage
A file on its way is a Transfer with a state: waiting, moving at a rate, paused, done or failed with a reason. One row draws it for the queue and the downloads: its icon, its way, a bar while it moves or waits paused, a status with the time left, and only the actions its state allows. TransferQueue counts what moves and waits. DownloadManager keeps the finished below under Clear, opening on a press and showing their folder. FileOperationProgress tells a copy or move with its bar, time left and current file, its header wrapping by the header rule; a failure speaks in the danger tone. StorageUsage points to data_display's UsageBar.
This commit is contained in:
1 parent
b280929d07
commit
4ef9830def
11 files changed
+1180
-5
No files matched your search
@@ -9,6 +9,7 @@ use crate::{lists::DirEntry, primitives::FocusNext, theme::Theme};
|
||||
|
||||
mod explorer;
|
||||
mod find;
|
||||
mod transfers;
|
||||
|
||||
pub(super) fn setup(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
use gpui::{
|
||||
Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, TestAppContext,
|
||||
VisualTestContext, Window, div, px,
|
||||
};
|
||||
|
||||
use super::{press, settle, setup};
|
||||
use crate::files::{
|
||||
DownloadManager, FileOperation, FileOperationProgress, Transfer, TransferQueue, TransferState,
|
||||
operation::headline, transfer::status, transfers::tally,
|
||||
};
|
||||
|
||||
/// Moves focus to the `nth` Tab stop from none.
|
||||
fn tab_to(nth: usize, cx: &mut VisualTestContext) {
|
||||
cx.update(|window, _| {
|
||||
window.blur();
|
||||
for _ in 0..nth {
|
||||
window.focus_next();
|
||||
}
|
||||
});
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
fn transfer(key: &str, state: TransferState) -> Transfer {
|
||||
Transfer {
|
||||
key: SharedString::from(key.to_string()),
|
||||
name: SharedString::from(format!("{key}.mov")),
|
||||
upload: false,
|
||||
moved: 4_000_000,
|
||||
total: 16_000_000,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_status_says_how_far_how_fast_and_how_long() {
|
||||
let moving = transfer("a", TransferState::Moving { rate: 2_000_000 });
|
||||
assert_eq!(status(&moving), "4.0 MB of 16 MB · 2.0 MB/s · 6s left");
|
||||
assert_eq!(
|
||||
status(&transfer("a", TransferState::Paused)),
|
||||
"Paused at 4.0 MB of 16 MB"
|
||||
);
|
||||
assert_eq!(
|
||||
status(&transfer("a", TransferState::Queued)),
|
||||
"Waiting · 16 MB"
|
||||
);
|
||||
let failed = transfer("a", TransferState::Failed("The disk is full".into()));
|
||||
assert_eq!(status(&failed), "Failed: The disk is full");
|
||||
assert_eq!(status(&transfer("a", TransferState::Done)), "16 MB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tally_counts_the_moving_and_the_waiting() {
|
||||
let all = [
|
||||
transfer("a", TransferState::Moving { rate: 1 }),
|
||||
transfer("b", TransferState::Queued),
|
||||
transfer("c", TransferState::Paused),
|
||||
transfer("d", TransferState::Done),
|
||||
];
|
||||
assert_eq!(tally(&all), "1 moving · 2 waiting");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_headline_says_what_an_operation_does_and_where() {
|
||||
let moving = TransferState::Moving { rate: 1 };
|
||||
assert_eq!(
|
||||
headline(FileOperation::Copy, 12, "Atrium", &moving),
|
||||
"Copying 12 items to Atrium"
|
||||
);
|
||||
assert_eq!(
|
||||
headline(FileOperation::Move, 1, "Atrium", &TransferState::Done),
|
||||
"Moved 1 item to Atrium"
|
||||
);
|
||||
}
|
||||
|
||||
/// A queue of three, one moving, one paused, one failed, and what its buttons asked.
|
||||
struct Queued {
|
||||
asked: Vec<String>,
|
||||
}
|
||||
|
||||
impl Render for Queued {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let ask = |what: &'static str, cx: &mut Context<Self>| {
|
||||
let owner = cx.entity();
|
||||
move |key: &SharedString, _: &mut Window, cx: &mut gpui::App| {
|
||||
owner.update(cx, |host, _| host.asked.push(format!("{what} {key}")))
|
||||
}
|
||||
};
|
||||
let transfers = [
|
||||
transfer("a", TransferState::Moving { rate: 1_000_000 }),
|
||||
transfer("b", TransferState::Paused),
|
||||
transfer("c", TransferState::Failed("Lost the network".into())),
|
||||
];
|
||||
div().w(px(420.0)).child(
|
||||
TransferQueue::new("queue", transfers)
|
||||
.pause(ask("pause", cx))
|
||||
.resume(ask("resume", cx))
|
||||
.retry(ask("retry", cx))
|
||||
.cancel(ask("cancel", cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn each_state_offers_what_it_can_do(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (host, cx) = cx.add_window_view(|_, _| Queued { asked: Vec::new() });
|
||||
cx.update(|window, _| window.activate_window());
|
||||
settle(cx);
|
||||
for nth in 1..=6 {
|
||||
tab_to(nth, cx);
|
||||
press("space", cx);
|
||||
}
|
||||
let asked = host.read_with(cx, |host, _| host.asked.clone());
|
||||
assert_eq!(
|
||||
asked,
|
||||
[
|
||||
"pause a", "cancel a", "resume b", "cancel b", "retry c", "cancel c"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Two downloads, one on its way and one done, and what was asked.
|
||||
struct Downloading {
|
||||
asked: Vec<String>,
|
||||
}
|
||||
|
||||
impl Render for Downloading {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let (open, show, clear) = (cx.entity(), cx.entity(), cx.entity());
|
||||
let downloads = [
|
||||
transfer("done", TransferState::Done),
|
||||
transfer("going", TransferState::Moving { rate: 1_000_000 }),
|
||||
];
|
||||
div().w(px(420.0)).child(
|
||||
DownloadManager::new("downloads", downloads)
|
||||
.open(move |key, _, cx| {
|
||||
open.update(cx, |host, _| host.asked.push(format!("open {key}")))
|
||||
})
|
||||
.show(move |key, _, cx| {
|
||||
show.update(cx, |host, _| host.asked.push(format!("show {key}")))
|
||||
})
|
||||
.on_clear(move |_, cx| clear.update(cx, |host, _| host.asked.push("clear".into()))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_finished_sit_below_and_open_show_and_clear(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (host, cx) = cx.add_window_view(|_, _| Downloading { asked: Vec::new() });
|
||||
cx.update(|window, _| window.activate_window());
|
||||
settle(cx);
|
||||
for nth in 1..=3 {
|
||||
tab_to(nth, cx);
|
||||
press("space", cx);
|
||||
}
|
||||
let asked = host.read_with(cx, |host, _| host.asked.clone());
|
||||
assert_eq!(
|
||||
asked,
|
||||
["clear", "open done", "show done"],
|
||||
"the one on its way has no handlers"
|
||||
);
|
||||
let going = cx
|
||||
.debug_bounds("transfer-row going")
|
||||
.expect("the one on its way draws");
|
||||
let done = cx
|
||||
.debug_bounds("transfer-row done")
|
||||
.expect("the finished one draws");
|
||||
assert!(going.bottom() <= done.top(), "{going:?} above {done:?}");
|
||||
}
|
||||
|
||||
/// A copy in one state, and what its buttons asked.
|
||||
struct Copying {
|
||||
state: TransferState,
|
||||
asked: Vec<&'static str>,
|
||||
}
|
||||
|
||||
impl Render for Copying {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let (pause, resume, cancel) = (cx.entity(), cx.entity(), cx.entity());
|
||||
div().w(px(420.0)).child(
|
||||
FileOperationProgress::new(
|
||||
"copy",
|
||||
FileOperation::Copy,
|
||||
3,
|
||||
"Atrium",
|
||||
(4_000, 16_000),
|
||||
self.state.clone(),
|
||||
)
|
||||
.current("Walkthrough.mov")
|
||||
.on_pause(move |_, cx| pause.update(cx, |host, _| host.asked.push("pause")))
|
||||
.on_resume(move |_, cx| resume.update(cx, |host, _| host.asked.push("resume")))
|
||||
.on_cancel(move |_, cx| cancel.update(cx, |host, _| host.asked.push("cancel"))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn copying(
|
||||
state: TransferState,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<Copying>, &mut VisualTestContext) {
|
||||
setup(cx);
|
||||
let (host, cx) = cx.add_window_view(move |_, _| Copying {
|
||||
state,
|
||||
asked: Vec::new(),
|
||||
});
|
||||
cx.update(|window, _| window.activate_window());
|
||||
settle(cx);
|
||||
(host, cx)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_running_copy_pauses_and_cancels_and_a_paused_one_resumes(cx: &mut TestAppContext) {
|
||||
let (host, cx) = copying(TransferState::Moving { rate: 1_000 }, cx);
|
||||
for nth in 1..=2 {
|
||||
tab_to(nth, cx);
|
||||
press("space", cx);
|
||||
}
|
||||
host.update(cx, |host, cx| {
|
||||
host.state = TransferState::Paused;
|
||||
cx.notify();
|
||||
});
|
||||
settle(cx);
|
||||
tab_to(1, cx);
|
||||
press("space", cx);
|
||||
assert_eq!(
|
||||
host.read_with(cx, |host, _| host.asked.clone()),
|
||||
["pause", "cancel", "resume"]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_finished_copy_offers_nothing(cx: &mut TestAppContext) {
|
||||
let (_, cx) = copying(TransferState::Done, cx);
|
||||
tab_to(1, cx);
|
||||
assert!(cx.update(|window, cx| window.focused(cx).is_none()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "transfer a twice")]
|
||||
fn a_transfer_is_listed_once() {
|
||||
let _ = TransferQueue::new(
|
||||
"queue",
|
||||
[
|
||||
transfer("a", TransferState::Queued),
|
||||
transfer("a", TransferState::Done),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "a goes up, not down")]
|
||||
fn a_download_comes_down() {
|
||||
let mut up = transfer("a", TransferState::Queued);
|
||||
up.upload = true;
|
||||
let _ = DownloadManager::new("downloads", [up]);
|
||||
}
|
||||
Reference in new issue
Block a user