From 42660095a57eef8009acebcba120d7bcba3a42f5 Mon Sep 17 00:00:00 2001 From: Kaede Akino Date: Thu, 17 Sep 2026 15:10:26 +0800 Subject: [PATCH] Fix SFTP status bar transfer direction --- TFM/Features/StatusBar/StatusBarView.swift | 31 ++++++++-- TFMTests/StatusBarPresentationTests.swift | 68 ++++++++++++++++++++++ 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/TFM/Features/StatusBar/StatusBarView.swift b/TFM/Features/StatusBar/StatusBarView.swift index 8aa8854f..2680d77f 100644 --- a/TFM/Features/StatusBar/StatusBarView.swift +++ b/TFM/Features/StatusBar/StatusBarView.swift @@ -33,6 +33,24 @@ nonisolated enum StatusBarPresentation { } } +/// The direction worth showing inside the active transfer progress ring. +/// +/// Only work that is actually moving bytes participates. Queued and historical +/// transfers still contribute to the transfer center's existing counts and +/// state, but they must not make a running upload or download look mixed. +nonisolated enum StatusBarTransferPresentation { + static func runningDirectionSymbol(for transfers: [SFTPTransfer]) -> String { + let directions = Set( + transfers.lazy + .filter { $0.state == .running } + .map(\.direction) + ) + if directions == [.upload] { return "arrow.up" } + if directions == [.download] { return "arrow.down" } + return "arrow.up.arrow.down" + } +} + /// Focused-session context and app-wide transfers, as one floating capsule. /// /// It used to be a docked strip with a hairline above it, which read as a piece @@ -179,8 +197,11 @@ struct StatusBarView: View { height: Metrics.Sidebar.statusDot ) case .transfers: - TransferGlyph(summary: app.sftpTransfers.summary) - .frame(width: 18, height: 18) + TransferGlyph( + summary: app.sftpTransfers.summary, + transfers: app.sftpTransfers.sftpTransfers + ) + .frame(width: 18, height: 18) } } .frame( @@ -372,6 +393,7 @@ private struct StatusBarPathLabel: View { /// front of it. private struct TransferGlyph: View { let summary: SFTPTransferSummary + let transfers: [SFTPTransfer] @ViewBuilder var body: some View { @@ -389,7 +411,7 @@ private struct TransferGlyph: View { } else { ProgressView().controlSize(.mini) } - Image(systemName: "arrow.up") + Image(systemName: StatusBarTransferPresentation.runningDirectionSymbol(for: transfers)) .font(.system(size: 8, weight: .bold)) } } else if summary.failedCount > 0 { @@ -414,7 +436,8 @@ private struct TransferCenterButton: View { isPresented.toggle() } label: { HStack(spacing: Metrics.Space.xs) { - TransferGlyph(summary: summaryValue).frame(width: 18, height: 18) + TransferGlyph(summary: summaryValue, transfers: coordinator.sftpTransfers) + .frame(width: 18, height: 18) if summaryValue.activeCount > 0 { Text(summaryValue.activeCount, format: .number) .monospacedDigit() diff --git a/TFMTests/StatusBarPresentationTests.swift b/TFMTests/StatusBarPresentationTests.swift index 35671a8d..4b24ab89 100644 --- a/TFMTests/StatusBarPresentationTests.swift +++ b/TFMTests/StatusBarPresentationTests.swift @@ -82,4 +82,72 @@ struct StatusBarPresentationTests { ) #expect(Metrics.StatusBar.overlayClearance > Metrics.StatusBar.height) } + + @Test + func runningUploadsUseTheUploadSymbol() { + let transfers = [ + transfer(direction: .upload, state: .running), + transfer(direction: .upload, state: .running), + ] + + #expect(StatusBarTransferPresentation.runningDirectionSymbol(for: transfers) == "arrow.up") + } + + @Test + func runningDownloadsUseTheDownloadSymbol() { + let transfers = [ + transfer(direction: .download, state: .running), + transfer(direction: .download, state: .running), + ] + + #expect(StatusBarTransferPresentation.runningDirectionSymbol(for: transfers) == "arrow.down") + } + + @Test + func copiesAndMixedDirectionsUseTheBidirectionalSymbol() { + #expect( + StatusBarTransferPresentation.runningDirectionSymbol( + for: [transfer(direction: .copy, state: .running)] + ) == "arrow.up.arrow.down" + ) + #expect( + StatusBarTransferPresentation.runningDirectionSymbol( + for: [ + transfer(direction: .upload, state: .running), + transfer(direction: .download, state: .running), + ] + ) == "arrow.up.arrow.down" + ) + } + + @Test + func onlyRunningTransfersChooseTheDirectionSymbol() { + let transfers = [ + transfer(direction: .upload, state: .running), + transfer(direction: .download, state: .queued), + transfer(direction: .download, state: .paused), + transfer(direction: .download, state: .completed), + transfer(direction: .download, state: .cancelled), + transfer(direction: .download, state: .failed("Offline")), + ] + + #expect(StatusBarTransferPresentation.runningDirectionSymbol(for: transfers) == "arrow.up") + #expect( + StatusBarTransferPresentation.runningDirectionSymbol( + for: [transfer(direction: .download, state: .queued)] + ) == "arrow.up.arrow.down" + ) + } + + private func transfer( + direction: SFTPTransfer.Direction, + state: SFTPTransfer.State + ) -> SFTPTransfer { + SFTPTransfer( + direction: direction, + localPath: "/tmp/file.txt", + remotePath: "/srv/file.txt", + state: state + ) + } }