Add download row context menu for pause, resume and cancel

Download row actions were wired only as swipe actions, and
SwipeActionModifier has #if !os(iOS) overloads that return the view
unmodified, so macOS had no way to pause, resume or cancel a download.
The video context menu could not cover it either, since the .downloads
context deliberately hides the built-in cancel item.

Give active download rows their own context menu with pause when queued
or downloading, resume when paused, retry when failed, and a destructive
cancel. Right-click on macOS, long-press on iOS.

Completed rows had a related bug: the delete item called an onDelete
callback that DownloadsView never passed, so it rendered but did nothing
on macOS. Drop the callback and delete via the download manager directly,
which is what already made the same menu work in HomeView.

Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
This commit is contained in:
Arkadiusz Fal
2026-07-06 08:19:56 +02:00
parent 6fed1b0ca7
commit 48378d7ca8
3 changed files with 76 additions and 20 deletions

View File

@@ -2287,6 +2287,17 @@
}
}
},
"downloads.cancel" : {
"comment" : "Cancel in-progress download context menu item",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cancel Download"
}
}
}
},
"downloads.delete" : {
"localizations" : {
"en" : {
@@ -2341,6 +2352,17 @@
}
}
},
"downloads.pause" : {
"comment" : "Pause download context menu item",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pause"
}
}
}
},
"downloads.retry" : {
"comment" : "Retry failed download button",
"localizations" : {
@@ -2352,6 +2374,17 @@
}
}
},
"downloads.resume" : {
"comment" : "Resume paused download context menu item",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Resume"
}
}
}
},
"downloads.search.placeholder" : {
"localizations" : {
"en" : {

View File

@@ -17,7 +17,6 @@ struct DownloadRowView: View {
let download: Download
let isActive: Bool
var onDelete: (() -> Void)? = nil
// Queue context (optional, enables auto-play when provided)
var queueSource: QueueSource? = nil
@@ -82,19 +81,8 @@ struct DownloadRowView: View {
if isActive {
// Active downloads: custom row with progress indicators, no tap actions
activeDownloadContent
.if(onDelete != nil) { view in
view.videoContextMenu(
video: video,
customActions: [
VideoContextAction(
String(localized: "downloads.delete"),
systemImage: "trash",
role: .destructive,
action: { onDelete?() }
)
],
context: .downloads
)
.contextMenu {
activeDownloadMenuItems
}
} else {
// Completed downloads: use VideoRowView with tap zones (thumbnail plays, text opens info)
@@ -120,7 +108,9 @@ struct DownloadRowView: View {
String(localized: "downloads.delete"),
systemImage: "trash",
role: .destructive,
action: { onDelete?() }
action: {
Task { await appEnvironment?.downloadManager.delete(download) }
}
)
],
context: .downloads
@@ -131,6 +121,44 @@ struct DownloadRowView: View {
}
}
/// Menu items for an in-progress download: pause/resume/retry plus cancel.
/// The `.downloads` video context menu hides the built-in cancel item, and swipe
/// actions are iOS-only, so this is the only cancel affordance on macOS.
@ViewBuilder
private var activeDownloadMenuItems: some View {
switch download.status {
case .queued, .downloading:
Button {
Task { await appEnvironment?.downloadManager.pause(download) }
} label: {
Label(String(localized: "downloads.pause"), systemImage: "pause.fill")
}
case .paused:
Button {
Task { await appEnvironment?.downloadManager.resume(download) }
} label: {
Label(String(localized: "downloads.resume"), systemImage: "play.fill")
}
case .failed:
Button {
Task { await appEnvironment?.downloadManager.resume(download) }
} label: {
Label(String(localized: "downloads.retry"), systemImage: "arrow.clockwise")
}
case .completed:
EmptyView()
}
Button(role: .destructive) {
Task { await appEnvironment?.downloadManager.cancel(download) }
} label: {
Label(String(localized: "downloads.cancel"), systemImage: "xmark.circle")
}
}
private var activeDownloadContent: some View {
HStack(spacing: 12) {
// Thumbnail (download status shown automatically)

View File

@@ -1315,11 +1315,6 @@ struct HomeView: View {
DownloadRowView(
download: download,
isActive: false,
onDelete: {
Task {
await downloadManager?.delete(download)
}
},
queueSource: .manual,
sourceLabel: String(localized: "queue.source.downloads"),
videoList: videoList,