Transfers: say so when a drop or a download is refused because the screen is busy

This commit is contained in:
jaap-jan committed 2026-10-02 14:11:41 +02:00
1 parent cc3d46f78c
commit 36fb0817a4
2 files changed
+41 -6

No files matched your search

@@ -2289,6 +2289,10 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
// ---- Moving files ---- // ---- Moving files ----
/// <summary>What a transfer asked for while another command is still running is told.</summary>
private const string StillBusy =
"Still working on the last request. Nothing was queued — try again when it has finished.";
/// <summary>Queues the chosen remote file for download into the local directory showing.</summary> /// <summary>Queues the chosen remote file for download into the local directory showing.</summary>
[RelayCommand] [RelayCommand]
private void Download() private void Download()
@@ -2306,7 +2310,8 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
await QueueDownloadsAsync(rows, CancellationToken.None).ConfigureAwait(true); await QueueDownloadsAsync(rows, CancellationToken.None).ConfigureAwait(true);
ClearRemoteChecks(); ClearRemoteChecks();
}); },
StillBusy);
} }
/// <summary>Queues the chosen local file for upload into the remote directory showing.</summary> /// <summary>Queues the chosen local file for upload into the remote directory showing.</summary>
@@ -2328,7 +2333,8 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
await QueueUploadsAsync(paths, CancellationToken.None).ConfigureAwait(true); await QueueUploadsAsync(paths, CancellationToken.None).ConfigureAwait(true);
ClearLocalChecks(); ClearLocalChecks();
}); },
StillBusy);
} }
/// <summary>Unticks everything on the host's side.</summary> /// <summary>Unticks everything on the host's side.</summary>
@@ -2393,13 +2399,15 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
internal void QueueUploads(IReadOnlyList<string> paths) => internal void QueueUploads(IReadOnlyList<string> paths) =>
_ = RunAsync( _ = RunAsync(
"Working out what to upload…", "Working out what to upload…",
() => QueueUploadsAsync(paths, CancellationToken.None)); () => QueueUploadsAsync(paths, CancellationToken.None),
StillBusy);
/// <inheritdoc cref="QueueUploads" /> /// <inheritdoc cref="QueueUploads" />
internal void QueueDownloads(IReadOnlyList<RemoteEntryRowViewModel> rows) => internal void QueueDownloads(IReadOnlyList<RemoteEntryRowViewModel> rows) =>
_ = RunAsync( _ = RunAsync(
"Working out what to download…", "Working out what to download…",
() => QueueDownloadsAsync(rows, CancellationToken.None)); () => QueueDownloadsAsync(rows, CancellationToken.None),
StillBusy);
internal async Task QueueUploadsAsync(IReadOnlyList<string> paths, CancellationToken cancellationToken) internal async Task QueueUploadsAsync(IReadOnlyList<string> paths, CancellationToken cancellationToken)
{ {
@@ -2580,7 +2588,8 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
_ = RunAsync( _ = RunAsync(
"Working out what to upload…", "Working out what to upload…",
() => QueueUploadsAsync(paths, CancellationToken.None)); () => QueueUploadsAsync(paths, CancellationToken.None),
StillBusy);
} }
/// <summary> /// <summary>
@@ -4154,11 +4163,21 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
/// The same funnel <c>VaultViewModel</c> uses, and here for the same reason: every command on this screen /// The same funnel <c>VaultViewModel</c> uses, and here for the same reason: every command on this screen
/// can fail with a path the server refused, and one that forgot to clear the busy flag would leave the /// can fail with a path the server refused, and one that forgot to clear the busy flag would leave the
/// pane permanently disabled. /// pane permanently disabled.
/// <para>
/// <paramref name="refusedWhileBusy"/> is for the commands somebody can fire while another is running —
/// a drop, or the download and upload buttons pressed twice — which used to vanish without a word: the
/// files were not queued and nothing said so. A command given one says why it did nothing instead.
/// </para>
/// </remarks> /// </remarks>
private async Task RunAsync(string busyMessage, Func<Task> work) private async Task RunAsync(string busyMessage, Func<Task> work, string? refusedWhileBusy = null)
{ {
if (IsBusy) if (IsBusy)
{ {
if (refusedWhileBusy is not null)
{
Status = refusedWhileBusy;
}
return; return;
} }
@@ -79,6 +79,22 @@ public sealed class TransferQueueingTests : IDisposable
transfers.Status.ShouldContain("Connect to a host first"); transfers.Status.ShouldContain("Connect to a host first");
} }
/// <remarks>
/// A drop landing while another command on this screen was running used to vanish: nothing queued and
/// nothing said. It still queues nothing — the command in flight owns the pane — but it says so.
/// </remarks>
[Fact]
public void DroppingFilesWhileBusy_QueuesNothingAndSaysWhy()
{
Connected();
transfers.IsBusy = true;
transfers.QueueUploads([File("one.txt")]);
Queued().ShouldBeEmpty();
transfers.Status.ShouldContain("Nothing was queued");
}
[Fact] [Fact]
public void DroppingSeveralFiles_QueuesEachOfThem() public void DroppingSeveralFiles_QueuesEachOfThem()
{ {