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:
@@ -9,6 +9,7 @@ export class SettingsService {
|
||||
readonly textSize = signal<AppSettings['textSize']>(DEFAULT_SETTINGS.textSize);
|
||||
readonly transferMode = signal<AppSettings['transferMode']>(DEFAULT_SETTINGS.transferMode);
|
||||
readonly transferConcurrency = signal(DEFAULT_SETTINGS.transferConcurrency);
|
||||
readonly autoClearCompleted = signal(DEFAULT_SETTINGS.autoClearCompleted);
|
||||
|
||||
private readonly rpc = inject(RpcService);
|
||||
|
||||
@@ -21,6 +22,7 @@ export class SettingsService {
|
||||
this.textSize.set(res.settings.textSize);
|
||||
this.transferMode.set(res.settings.transferMode);
|
||||
this.transferConcurrency.set(res.settings.transferConcurrency);
|
||||
this.autoClearCompleted.set(res.settings.autoClearCompleted);
|
||||
this.applyTheme();
|
||||
this.applyTextSize();
|
||||
});
|
||||
@@ -53,6 +55,11 @@ export class SettingsService {
|
||||
void this.persist();
|
||||
}
|
||||
|
||||
setAutoClearCompleted(auto: boolean): void {
|
||||
this.autoClearCompleted.set(auto);
|
||||
void this.persist();
|
||||
}
|
||||
|
||||
private applyTheme(): void {
|
||||
const theme = this.theme();
|
||||
if (theme === 'system') {
|
||||
@@ -78,6 +85,7 @@ export class SettingsService {
|
||||
textSize: this.textSize(),
|
||||
transferMode: this.transferMode(),
|
||||
transferConcurrency: this.transferConcurrency(),
|
||||
autoClearCompleted: this.autoClearCompleted(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -187,6 +187,30 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mt-2.5 flex items-center justify-between gap-2">
|
||||
<span class="text-xs text-muted-foreground">Finished transfers</span>
|
||||
<div class="flex rounded-md border border-border p-0.5">
|
||||
<button
|
||||
(click)="settings.setAutoClearCompleted(true)"
|
||||
class="rounded px-2 py-0.5 text-[11px]"
|
||||
[class.bg-accent]="settings.autoClearCompleted()"
|
||||
[class.text-foreground]="settings.autoClearCompleted()"
|
||||
[class.text-muted-foreground]="!settings.autoClearCompleted()"
|
||||
>
|
||||
Auto-clear
|
||||
</button>
|
||||
<button
|
||||
(click)="settings.setAutoClearCompleted(false)"
|
||||
class="rounded px-2 py-0.5 text-[11px]"
|
||||
[class.bg-accent]="!settings.autoClearCompleted()"
|
||||
[class.text-foreground]="!settings.autoClearCompleted()"
|
||||
[class.text-muted-foreground]="settings.autoClearCompleted()"
|
||||
>
|
||||
Keep
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
@if (transfers.expanded() && transfers.hasJobs()) {
|
||||
<div class="flex items-center justify-between border-t border-border px-3 py-1 text-[11px] text-muted-foreground">
|
||||
<span>
|
||||
{{ transfers.activeJobs().length }} active · {{ transfers.finishedJobs().length }} finished
|
||||
</span>
|
||||
<button
|
||||
(click)="transfers.clearFinished()"
|
||||
[disabled]="!transfers.finishedJobs().length"
|
||||
class="flex items-center gap-1 rounded px-1.5 py-0.5 hover:bg-accent hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent"
|
||||
>
|
||||
<ng-icon name="mynaTrash" size="0.75rem" /> Clear finished
|
||||
</button>
|
||||
</div>
|
||||
<div class="max-h-40 overflow-y-auto border-t border-border">
|
||||
@for (job of transfers.jobs(); track job.id) {
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 text-xs">
|
||||
|
||||
@@ -63,7 +63,8 @@ export class TransferBar {
|
||||
progressLabel(job: TransferSnapshot): string {
|
||||
if (job.state === 'queued') return 'Queued';
|
||||
if (job.state === 'scanning') return 'Scanning…';
|
||||
if (job.state === 'failed') return job.error ?? 'Failed';
|
||||
if (job.state === 'failed')
|
||||
return `${job.currentFile ? `${job.currentFile} — ` : ''}${job.error ?? 'Failed'}`;
|
||||
if (job.state === 'cancelled') return 'Cancelled';
|
||||
if (job.state === 'completed')
|
||||
return `${job.doneFiles} file${job.doneFiles === 1 ? '' : 's'} · ${formatSize(job.doneBytes)}`;
|
||||
|
||||
@@ -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'}`);
|
||||
|
||||
Reference in New Issue
Block a user