diff options
| author | Dustin Mierau <dustin@mierau.me> | 2025-11-08 22:40:58 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2025-11-08 22:40:58 -0800 |
| commit | 6cfa1db09055a3b4ea64d3586a0b439d13498db6 (patch) | |
| tree | c272f453ec023f055222aa036114409a84ace96c /Hotline/macOS | |
| parent | 2caf86e633c1a121c8e23d068ac817ed8e80f6a7 (diff) | |
Fixing cancel transfer from transfer window. Transfer window ux improvements.
Diffstat (limited to 'Hotline/macOS')
| -rw-r--r-- | Hotline/macOS/TransfersView.swift | 255 |
1 files changed, 180 insertions, 75 deletions
diff --git a/Hotline/macOS/TransfersView.swift b/Hotline/macOS/TransfersView.swift index 9932c0a..6b7a21b 100644 --- a/Hotline/macOS/TransfersView.swift +++ b/Hotline/macOS/TransfersView.swift @@ -2,25 +2,40 @@ import SwiftUI struct TransfersView: View { @Environment(\.appState) private var appState - + + @State private var selectedTransfers = Set<TransferInfo>() + var body: some View { VStack(spacing: 0) { - if appState.transfers.isEmpty { - emptyState + if self.appState.transfers.isEmpty { + self.emptyState } else { - transfersList + self.transfersList } } .frame(minWidth: 500, minHeight: 200) .navigationTitle("Transfers") .toolbar { +// ToolbarItem(placement: .primaryAction) { +// Button { +// self.appState.sweepTransfers() +// self.selectedTransfers = [] +// } label: { +// Label("Remove Completed", systemImage: "checklist") +// } +// .disabled(self.appState.transfers.isEmpty) +// } + ToolbarItem(placement: .primaryAction) { Button { - appState.cancelAllTransfers() + for transfer in self.selectedTransfers { + self.appState.cancelTransfer(id: transfer.id) + } + self.selectedTransfers = [] } label: { - Label("Cancel All", systemImage: "xmark.circle") + Label("Cancel Transfer", systemImage: "xmark") } - .disabled(appState.transfers.isEmpty) + .disabled(self.selectedTransfers.isEmpty) } } } @@ -38,96 +53,186 @@ struct TransfersView: View { // MARK: - Transfers List private var transfersList: some View { - List { - ForEach(appState.transfers) { transfer in + List(selection: self.$selectedTransfers) { + ForEach(self.appState.transfers) { transfer in TransferRow(transfer: transfer) + .id(transfer) + } + } + .listStyle(.inset) + .environment(\.defaultMinListRowHeight, 56) + .contextMenu(forSelectionType: TransferInfo.self) { items in + if let item = items.first { + if item.completed, + let fileURL = item.fileURL { + Button("Remove Transfer") { + self.appState.cancelTransfer(id: item.id) + } + + Divider() + + Button("Show in Finder") { + NSWorkspace.shared.activateFileViewerSelecting([fileURL]) + } + + Button("Open") { + NSWorkspace.shared.open(fileURL) + } + + Divider() + + Button("Move to Trash") { + NSWorkspace.shared.recycle([fileURL]) + } + } + else if !item.done { + Button("Cancel Transfer") { + self.appState.cancelTransfer(id: item.id) + } + } + } + } primaryAction: { items in + let fileURLs: [URL] = items.compactMap { $0.fileURL } + if !fileURLs.isEmpty { + NSWorkspace.shared.activateFileViewerSelecting(fileURLs) } } - .listStyle(.inset(alternatesRowBackgrounds: true)) } } // MARK: - Transfer Row struct TransferRow: View { + @Environment(\.appState) private var appState + @Bindable var transfer: TransferInfo + + private var statsView: some View { + HStack(spacing: 8) { + // Progress percentage +// Text("\(Int(self.transfer.progress * 100))%") + + // Speed + if let speed = self.transfer.speed { + Text(self.formatSpeed(speed)) + } - var body: some View { - VStack(alignment: .leading, spacing: 8) { - // File name and server - HStack { - VStack(alignment: .leading, spacing: 2) { - Text(transfer.title) - .font(.system(.body, design: .default, weight: .medium)) + // Time remaining + if let timeRemaining = self.transfer.timeRemaining { + Text(self.formatTimeRemaining(timeRemaining)) + } + + // File size + Text(self.formatSize(self.transfer.size)) + } + .font(.subheadline) + .foregroundStyle(.secondary) + .monospacedDigit() + } + + private var fileIconView: some View { + FileIconView(filename: self.transfer.title, fileType: nil) + .frame(width: 32, height: 32) + .overlay(alignment: .bottomTrailing) { + if self.transfer.cancelled || self.transfer.failed { + Image(systemName: "exclamationmark.triangle.fill") + .resizable() + .symbolRenderingMode(.multicolor) + .scaledToFit() + .frame(width: 16, height: 16) + } + else if self.transfer.completed { + Image(systemName: "checkmark.circle.fill") + .resizable() + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .fileComplete) + .scaledToFit() + .frame(width: 16, height: 16) + } + } + } - if let serverName = transfer.serverName { - Text(serverName) - .font(.caption) - .foregroundStyle(.secondary) + var body: some View { + HStack(alignment: .center, spacing: 8) { + self.fileIconView + + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text(self.transfer.title) + .font(.headline) + .lineLimit(1) + .truncationMode(.tail) + + Spacer() + + if !self.transfer.done { + self.statsView } } - - Spacer() - - // Cancel button + + // Progress bar and status + if self.transfer.cancelled { + Text("Cancelled") + .font(.subheadline) + .foregroundStyle(.secondary) + } + else if self.transfer.failed { + Text("Failed") + .font(.subheadline) + .foregroundStyle(.secondary) + } + else if self.transfer.completed { + Text("Complete") + .font(.subheadline) + .foregroundStyle(.fileComplete) + } + else { + ProgressView(value: self.transfer.progress, total: 1.0) + .progressViewStyle(.linear) + .controlSize(.large) + } + } + + if self.transfer.completed { Button { - AppState.shared.cancelTransfer(id: transfer.id) + guard let fileURL = self.transfer.fileURL else { + return + } + + NSWorkspace.shared.activateFileViewerSelecting([fileURL]) } label: { - Image(systemName: "xmark.circle.fill") + Image(systemName: "eye.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) .foregroundStyle(.secondary) } + .buttonBorderShape(.circle) .buttonStyle(.plain) - .help("Cancel download") } + } + +// VStack(alignment: .leading, spacing: 2) { + - // Progress bar and status - VStack(alignment: .leading, spacing: 4) { - if transfer.failed { - Label("Failed", systemImage: "exclamationmark.triangle.fill") - .font(.caption) - .foregroundStyle(.red) - } else if transfer.completed { - Label("Complete", systemImage: "checkmark.circle.fill") - .font(.caption) - .foregroundStyle(.green) - } else { - // Progress bar - ProgressView(value: transfer.progress, total: 1.0) - .progressViewStyle(.linear) - - // Progress info - HStack(spacing: 8) { - // Progress percentage - Text("\(Int(transfer.progress * 100))%") - .font(.caption) - .foregroundStyle(.secondary) - .monospacedDigit() - - // File size - Text(formatSize(transfer.size)) - .font(.caption) - .foregroundStyle(.secondary) - - // Speed - if let speed = transfer.speed { - Text(formatSpeed(speed)) - .font(.caption) - .foregroundStyle(.secondary) - .monospacedDigit() - } +// if let serverName = self.transfer.serverName { +// Text(serverName) +// .font(.caption) +// .foregroundStyle(.secondary) +// } +// } - // Time remaining - if let timeRemaining = transfer.timeRemaining { - Text(formatTimeRemaining(timeRemaining)) - .font(.caption) - .foregroundStyle(.secondary) - .monospacedDigit() - } - } - } - } - } - .padding(.vertical, 4) +// // Cancel button +// Button { +// self.appState.cancelTransfer(id: transfer.id) +// } label: { +// Image(systemName: "xmark.circle.fill") +// .foregroundStyle(.secondary) +// } +// .buttonStyle(.plain) +// .help("Cancel download") +// } +// } } // MARK: - Formatting |