P3: transfer engine with queue, progress, cancel + transfer bar UI

- TransferEngine: FIFO queue, provider-generic stream pipeline (covers all four
  panel combos), throttled transferUpdate push, cancel via stream destroy,
  self-copy guards, failSession hook for dead SSH sessions
- TransfersService + TransferBar: per-job rows, progress bars, cancel/dismiss,
  auto-dismiss completed, dst panel refresh
- F5 + Copy button wired to active-panel selection
- verified: 30MB nested tree copy (UTF-8 names, empty dirs), self-copy rejection,
  mid-transfer cancel leaving partial file

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jokin
2026-07-11 13:46:34 +02:00
co-authored by Claude Fable 5
parent b907e0a4ce
commit 88af44acfc
9 changed files with 455 additions and 7 deletions
+38
View File
@@ -0,0 +1,38 @@
@if (transfers.hasJobs()) {
<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">
<ng-icon [name]="stateIcon(job)" size="0.95rem" [class]="stateClass(job)" />
<span class="w-40 shrink-0 truncate font-medium" [title]="job.srcPath">{{ job.name }}</span>
<div class="min-w-0 flex-1">
<div class="truncate text-[11px] text-muted-foreground">{{ progressLabel(job) }}</div>
@if (isActive(job)) {
<div class="mt-0.5 h-1 overflow-hidden rounded-full bg-muted">
<div
class="h-full rounded-full bg-primary transition-[width] duration-100"
[style.width.%]="jobPercent(job)"
></div>
</div>
}
</div>
@if (isActive(job)) {
<button
(click)="transfers.cancel(job.id)"
title="Cancel"
class="shrink-0 rounded p-1 text-muted-foreground hover:bg-accent hover:text-destructive"
>
<ng-icon name="mynaX" size="0.9rem" />
</button>
} @else {
<button
(click)="transfers.dismiss(job.id)"
title="Dismiss"
class="shrink-0 rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
>
<ng-icon name="mynaX" size="0.9rem" />
</button>
}
</div>
}
</div>
}