fix: media download UX and table row selection

- Fix: Remove aria2c from yt-dlp to allow native concurrent downloader to print progress
- Fix: Use generic 'Fetching media data...' message instead of hardcoding YouTube
- Fix: Remove onTapGesture from Table cells to restore native macOS SwiftUI row selection and use simultaneousGesture on the Table itself for double clicks
This commit is contained in:
nimbold
2026-06-07 23:39:18 +03:30
parent 49003665ef
commit fbae92aae9
6 changed files with 52 additions and 49 deletions
+23
View File
@@ -0,0 +1,23 @@
import re
with open("Sources/Firelink/DownloadTable.swift", "r") as f:
content = f.read()
# Remove doubleClickableCell block
content = re.sub(r' private func doubleClickableCell.*? }\n\n', '', content, flags=re.DOTALL)
# Remove doubleClickableCell wrappers from TableColumn
content = re.sub(r'doubleClickableCell\(for: item\) \{\n\s*(.*?)\n\s*\}', r'\1', content, flags=re.DOTALL)
# Add simultaneousGesture to Table
table_end = content.find(' .environment(\\.defaultMinListRowHeight, settings.listRowDensity.minRowHeight)')
gesture = ''' .simultaneousGesture(TapGesture(count: 2).onEnded {
if let id = selection.first, let item = controller.downloads.first(where: { $0.id == id }) {
performPrimaryAction(for: item)
}
})
'''
content = content[:table_end] + gesture + content[table_end:]
with open("Sources/Firelink/DownloadTable.swift", "w") as f:
f.write(content)
+8 -5
View File
@@ -39,7 +39,8 @@ struct AddDownloadsView: View {
url: mediaURL,
cookieSource: settings.mediaCookieSource,
credentials: metadataCredentials(for: mediaURL),
transferOptions: transferOptions
transferOptions: transferOptions,
onCancel: { dismiss() }
) { selectedFormat, metadata in
let cleanTitle = FileClassifier.sanitizedFileName(metadata.title ?? "Media")
let ext = selectedFormat.outputExtension
@@ -79,10 +80,12 @@ struct AddDownloadsView: View {
}
.padding(12)
}
Divider()
actionBar
.padding(16)
.background(.background)
if detectedMediaURL == nil {
Divider()
actionBar
.padding(16)
.background(.background)
}
}
.frame(minWidth: 640, idealWidth: 680, minHeight: 470, idealHeight: 500)
.onChange(of: linkText) { _, newValue in
+12 -33
View File
@@ -37,8 +37,7 @@ struct DownloadTable: View {
Table(sortedItems, selection: $selection, sortOrder: $sortOrder) {
TableColumn("File Name", value: \.fileName) { item in
doubleClickableCell(for: item) {
HStack(alignment: .top, spacing: 8) {
HStack(alignment: .top, spacing: 8) {
Image(systemName: item.category.symbolName)
.font(.title3)
.foregroundStyle(categoryColor(for: item.category))
@@ -47,63 +46,55 @@ struct DownloadTable: View {
.font(.headline)
.lineLimit(1)
.truncationMode(.tail)
}
.draggable(item.id.uuidString)
}
}
.width(min: 200, ideal: 340)
TableColumn("Size", value: \.sortableSize) { item in
doubleClickableCell(for: item) {
Text(ByteFormatter.string(item.sizeBytes))
Text(ByteFormatter.string(item.sizeBytes))
.monospacedDigit()
.lineLimit(1)
.truncationMode(.tail)
}
}
.width(min: 80, ideal: 100)
TableColumn("Progress", value: \.progress) { item in
doubleClickableCell(for: item) {
progressBarCell(for: item)
}
progressBarCell(for: item)
}
.width(min: 100, ideal: 115)
TableColumn("Status", value: \.status.rawValue) { item in
doubleClickableCell(for: item) {
statusCell(for: item)
}
statusCell(for: item)
}
.width(min: 115, ideal: 170)
TableColumn("Speed", value: \.displaySpeedText) { item in
doubleClickableCell(for: item) {
Text(item.displaySpeedText)
Text(item.displaySpeedText)
.lineLimit(1)
.truncationMode(.tail)
}
}
.width(min: 70, ideal: 90)
TableColumn("ETA", value: \.displayETAText) { item in
doubleClickableCell(for: item) {
Text(item.displayETAText)
Text(item.displayETAText)
.lineLimit(1)
.truncationMode(.tail)
}
}
.width(min: 70, ideal: 90)
TableColumn("Date Added", value: \.createdAt) { item in
doubleClickableCell(for: item) {
Text(formatted(item.createdAt))
Text(formatted(item.createdAt))
.lineLimit(1)
.truncationMode(.tail)
}
}
.width(min: 100, ideal: 155)
}
.simultaneousGesture(TapGesture(count: 2).onEnded {
if let id = selection.first, let item = controller.downloads.first(where: { $0.id == id }) {
performPrimaryAction(for: item)
}
})
.environment(\.defaultMinListRowHeight, settings.listRowDensity.minRowHeight)
.contextMenu(forSelectionType: DownloadItem.ID.self) { itemIDs in
rowContextMenu(for: itemIDs)
@@ -157,18 +148,6 @@ struct DownloadTable: View {
}
}
private func doubleClickableCell<Content: View>(
for item: DownloadItem,
@ViewBuilder content: () -> Content
) -> some View {
content()
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
.onTapGesture(count: 2) {
performPrimaryAction(for: item)
}
}
private func performPrimaryAction(for item: DownloadItem) {
if item.status == .completed {
openFile(item)
+2 -10
View File
@@ -122,7 +122,7 @@ final class MediaDownloadEngine: @unchecked Sendable {
}
try process.run()
messageUpdate("Fetching YouTube data...")
messageUpdate("Fetching media data...")
outputPipe.fileHandleForWriting.closeFile()
errorPipe.fileHandleForWriting.closeFile()
@@ -157,15 +157,7 @@ final class MediaDownloadEngine: @unchecked Sendable {
guard connections > 1 else { return }
arguments.append(contentsOf: ["--concurrent-fragments", "\(connections)"])
guard let aria2URL = Aria2DownloadEngine.findExecutable() else {
return
}
arguments.append(contentsOf: [
"--downloader", aria2URL.path,
"--downloader-args", "aria2c:-x \(connections) -s \(connections) -k 1M --file-allocation=none"
])
// Use yt-dlp's native concurrent downloader instead of aria2c to ensure progress parsing works via stdout
}
}
+1 -1
View File
@@ -66,7 +66,7 @@ enum MediaExtractionEngine {
throw ExtractionError.processFailed("yt-dlp binary not found.")
}
var args = ["-J", "--no-warnings", "--ignore-no-formats-error", "--no-playlist", "--extractor-args", "youtube:player_client=ios,android"]
var args = ["-J", "--no-warnings", "--ignore-no-formats-error", "--no-playlist", "--extractor-args", "youtube:player_client=ios,tv"]
appendCommonArguments(to: &args, cookieSource: cookieSource, credentials: credentials, transferOptions: transferOptions)
args.append(url.absoluteString)
@@ -5,6 +5,7 @@ struct MediaInspectorInlineView: View {
let cookieSource: BrowserCookieSource
let credentials: DownloadCredentials?
let transferOptions: DownloadTransferOptions
let onCancel: () -> Void
let onDownload: (CleanFormatOption, MediaMetadata) -> Void
@ObservedObject private var engineManager = MediaEngineManager.shared
@@ -120,6 +121,11 @@ struct MediaInspectorInlineView: View {
Spacer(minLength: 16)
Button("Cancel") {
onCancel()
}
.keyboardShortcut(.cancelAction)
Button("Extract") {
if let selected = resolveSelectedOption() {
onDownload(selected, metadata)