Finished-transfers setting + transfer-list header with Clear finished

- Options popover: 'Finished transfers' Auto-clear | Keep (default auto-clear,
  the previous behavior); persisted like the rest
- Transfer list header: 'N active · M finished' + Clear finished button
  (dismisses completed/failed/cancelled only, never running/queued)
- Failed rows now name the file that failed alongside the error message

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jokin
2026-07-11 19:56:24 +02:00
co-authored by Claude Fable 5
parent 45df0e450f
commit eaa2d00115
6 changed files with 62 additions and 2 deletions
+13 -1
View File
@@ -3,6 +3,7 @@ import type { FileEntry, TransferSnapshot } from '@shared/types';
import { DialogsService } from './dialogs.service';
import { PanelsService, type Side } from './panels.service';
import { RpcService } from './rpc.service';
import { SettingsService } from './settings.service';
import { ToastService } from './toast.service';
const AUTO_DISMISS_COMPLETED_MS = 5000;
@@ -16,6 +17,7 @@ export class TransfersService {
private readonly panels = inject(PanelsService);
private readonly toast = inject(ToastService);
private readonly dialogs = inject(DialogsService);
private readonly settings = inject(SettingsService);
private readonly jobMap = signal<ReadonlyMap<string, TransferSnapshot>>(new Map());
/** Smoothed bytes/sec per running job (UI-computed from progress deltas). */
@@ -29,6 +31,7 @@ export class TransfersService {
);
readonly queuedCount = computed(() => this.jobs().filter((j) => j.state === 'queued').length);
readonly failedJobs = computed(() => this.jobs().filter((j) => j.state === 'failed'));
readonly finishedJobs = computed(() => this.jobs().filter((j) => !ACTIVE_STATES.includes(j.state)));
/** Whether the detailed per-job list is shown (toggled from the status bar). */
readonly expanded = signal(false);
@@ -131,6 +134,13 @@ export class TransfersService {
await this.rpc.request.cancelTransfer({ jobId });
}
/** Remove every finished (completed/failed/cancelled) entry; running ones stay. */
clearFinished(): void {
for (const job of this.finishedJobs()) {
void this.dismiss(job.id);
}
}
async dismiss(jobId: string): Promise<void> {
this.jobMap.update((prev) => {
const next = new Map(prev);
@@ -156,7 +166,9 @@ export class TransfersService {
if (snapshot.state === 'completed') {
this.refreshPanelsOn(snapshot.dstSessionId);
setTimeout(() => void this.dismiss(snapshot.id), AUTO_DISMISS_COMPLETED_MS);
if (this.settings.autoClearCompleted()) {
setTimeout(() => void this.dismiss(snapshot.id), AUTO_DISMISS_COMPLETED_MS);
}
} else if (snapshot.state === 'failed') {
this.expanded.set(true);
this.toast.error(`Transfer "${snapshot.name}" failed: ${snapshot.error ?? 'unknown error'}`);