mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
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:
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user