aboutsummaryrefslogtreecommitdiff
path: root/Hotline/macOS
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2025-11-08 22:40:58 -0800
committerDustin Mierau <dustin@mierau.me>2025-11-08 22:40:58 -0800
commit6cfa1db09055a3b4ea64d3586a0b439d13498db6 (patch)
treec272f453ec023f055222aa036114409a84ace96c /Hotline/macOS
parent2caf86e633c1a121c8e23d068ac817ed8e80f6a7 (diff)
Fixing cancel transfer from transfer window. Transfer window ux improvements.
Diffstat (limited to 'Hotline/macOS')
-rw-r--r--Hotline/macOS/TransfersView.swift255
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