From fbae92aae9eab01d295ebd8fa58568c654a08a8c Mon Sep 17 00:00:00 2001 From: nimbold <11913706+nimbold@users.noreply.github.com> Date: Sun, 7 Jun 2026 23:39:18 +0330 Subject: [PATCH] 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 --- Scripts/fix_table.py | 23 ++++++++++ Sources/Firelink/AddDownloadsView.swift | 13 +++--- Sources/Firelink/DownloadTable.swift | 45 +++++-------------- Sources/Firelink/MediaDownloadEngine.swift | 12 +---- Sources/Firelink/MediaExtractionEngine.swift | 2 +- .../Firelink/MediaInspectorInlineView.swift | 6 +++ 6 files changed, 52 insertions(+), 49 deletions(-) create mode 100644 Scripts/fix_table.py diff --git a/Scripts/fix_table.py b/Scripts/fix_table.py new file mode 100644 index 0000000..41a57a5 --- /dev/null +++ b/Scripts/fix_table.py @@ -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) diff --git a/Sources/Firelink/AddDownloadsView.swift b/Sources/Firelink/AddDownloadsView.swift index 72dfaca..804e78a 100644 --- a/Sources/Firelink/AddDownloadsView.swift +++ b/Sources/Firelink/AddDownloadsView.swift @@ -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 diff --git a/Sources/Firelink/DownloadTable.swift b/Sources/Firelink/DownloadTable.swift index 200bfaf..707b518 100644 --- a/Sources/Firelink/DownloadTable.swift +++ b/Sources/Firelink/DownloadTable.swift @@ -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( - 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) diff --git a/Sources/Firelink/MediaDownloadEngine.swift b/Sources/Firelink/MediaDownloadEngine.swift index 5150bdd..b0977ac 100644 --- a/Sources/Firelink/MediaDownloadEngine.swift +++ b/Sources/Firelink/MediaDownloadEngine.swift @@ -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 } } diff --git a/Sources/Firelink/MediaExtractionEngine.swift b/Sources/Firelink/MediaExtractionEngine.swift index e7ea94c..c97c41b 100644 --- a/Sources/Firelink/MediaExtractionEngine.swift +++ b/Sources/Firelink/MediaExtractionEngine.swift @@ -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) diff --git a/Sources/Firelink/MediaInspectorInlineView.swift b/Sources/Firelink/MediaInspectorInlineView.swift index 0bf41cc..85fa19d 100644 --- a/Sources/Firelink/MediaInspectorInlineView.swift +++ b/Sources/Firelink/MediaInspectorInlineView.swift @@ -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)