From 1093d73b0e16c1eef843a6999523272b149d1f1f Mon Sep 17 00:00:00 2001 From: nimbold <11913706+nimbold@users.noreply.github.com> Date: Wed, 3 Jun 2026 06:23:03 +0330 Subject: [PATCH] feat: add monochrome tray icon and download table enhancements - Added proper monochrome template tray icon loaded explicitly with precise dimensions - Added redownload functionality for completed or failed items - Added double-click to open completed files directly from the table - Added 'Copy Address' context menu action - Improved context menu organization and conditionally displayed actions --- Scripts/create_app_bundle.sh | 1 + .../MenuBarIcon.imageset/Contents.json | 16 +++++ .../MenuBarIconTemplate.png | Bin 0 -> 4640 bytes Sources/Firelink/DownloadController.swift | 18 +++++ Sources/Firelink/DownloadTable.swift | 65 +++++++++++++++--- Sources/Firelink/FirelinkApp.swift | 14 +++- 6 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 Sources/Firelink/Assets.xcassets/MenuBarIcon.imageset/Contents.json create mode 100644 Sources/Firelink/Assets.xcassets/MenuBarIcon.imageset/MenuBarIconTemplate.png diff --git a/Scripts/create_app_bundle.sh b/Scripts/create_app_bundle.sh index 512e2e3..4436998 100755 --- a/Scripts/create_app_bundle.sh +++ b/Scripts/create_app_bundle.sh @@ -19,6 +19,7 @@ rm -rf "$APP_DIR" mkdir -p "$MACOS_DIR" "$RESOURCES_DIR" cp ".build/$CONFIGURATION/$APP_NAME" "$MACOS_DIR/$APP_NAME" cp "$ROOT_DIR/Resources/$ICON_NAME.icns" "$RESOURCES_DIR/$ICON_NAME.icns" +cp "$ROOT_DIR/Sources/Firelink/Assets.xcassets/MenuBarIcon.imageset/MenuBarIconTemplate.png" "$RESOURCES_DIR/MenuBarIconTemplate.png" ARIA2C_PATH=$(which aria2c || true) if [[ -n "$ARIA2C_PATH" && -x "$ARIA2C_PATH" ]]; then diff --git a/Sources/Firelink/Assets.xcassets/MenuBarIcon.imageset/Contents.json b/Sources/Firelink/Assets.xcassets/MenuBarIcon.imageset/Contents.json new file mode 100644 index 0000000..c586150 --- /dev/null +++ b/Sources/Firelink/Assets.xcassets/MenuBarIcon.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "MenuBarIconTemplate.png", + "idiom" : "mac", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Sources/Firelink/Assets.xcassets/MenuBarIcon.imageset/MenuBarIconTemplate.png b/Sources/Firelink/Assets.xcassets/MenuBarIcon.imageset/MenuBarIconTemplate.png new file mode 100644 index 0000000000000000000000000000000000000000..aadf8b4c08ad2d126067e7a8788c5ecd46433727 GIT binary patch literal 4640 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 KT!oBz#Qy*n_PyBv literal 0 HcmV?d00001 diff --git a/Sources/Firelink/DownloadController.swift b/Sources/Firelink/DownloadController.swift index add2db5..f3f3f85 100644 --- a/Sources/Firelink/DownloadController.swift +++ b/Sources/Firelink/DownloadController.swift @@ -244,6 +244,24 @@ final class DownloadController: ObservableObject { pumpQueue() } + func redownload(_ item: DownloadItem) { + trashFiles(for: item) + restrictQueueToAutoResume = false + update(item.id) { + $0.status = .queued + $0.progress = 0 + $0.speedText = "-" + $0.etaText = "-" + $0.connectionCount = 0 + $0.message = "Redownloading" + $0.autoResumeOnLaunch = true + } + queuePumpScope = queuePumpScope.includingItem(item.id) + automaticRetryCounts[item.id] = nil + saveDownloads() + pumpQueue() + } + func cancel(_ item: DownloadItem) { activeHandles[item.id]?.cancel() activeHandles[item.id] = nil diff --git a/Sources/Firelink/DownloadTable.swift b/Sources/Firelink/DownloadTable.swift index 7b2e936..6f8cd3d 100644 --- a/Sources/Firelink/DownloadTable.swift +++ b/Sources/Firelink/DownloadTable.swift @@ -218,6 +218,13 @@ struct DownloadTable: View { .frame(width: tableWidth, alignment: .leading) .background(selection.contains(item.id) ? Color.accentColor.opacity(0.12) : Color.clear) .contentShape(Rectangle()) + .onTapGesture(count: 2) { + if item.status == .completed { + openFile(item) + } else { + openWindow(value: item.id) + } + } .onTapGesture { let index = sortedItems.firstIndex(where: { $0.id == item.id }) @@ -326,12 +333,14 @@ struct DownloadTable: View { private func rowContextMenu(for item: DownloadItem) -> some View { let targetItems = selection.contains(item.id) ? controller.downloads.filter { selection.contains($0.id) } : [item] - Button { - for target in targetItems { - openWindow(value: target.id) + if targetItems.allSatisfy({ $0.status == .completed }) { + Button { + for target in targetItems { + openFile(target) + } + } label: { + Label(targetItems.count > 1 ? "Open (\(targetItems.count))" : "Open", systemImage: "doc") } - } label: { - Label(targetItems.count > 1 ? "Properties (\(targetItems.count))" : "Properties", systemImage: "info.circle") } Button { @@ -354,9 +363,9 @@ struct DownloadTable: View { } } - if targetItems.contains(where: { $0.status == .downloading }) { + if targetItems.contains(where: { $0.status == .downloading || $0.status == .queued }) { Button { - for target in targetItems where target.status == .downloading { + for target in targetItems where target.status == .downloading || target.status == .queued { controller.pause(target) } } label: { @@ -364,6 +373,18 @@ struct DownloadTable: View { } } + if targetItems.contains(where: { $0.status == .completed || $0.status == .failed || $0.status == .canceled }) { + Button { + for target in targetItems where target.status == .completed || target.status == .failed || target.status == .canceled { + controller.redownload(target) + } + } label: { + Label("Redownload", systemImage: "arrow.clockwise") + } + } + + Divider() + if targetItems.contains(where: { $0.status != .completed && $0.status != .downloading }) { Menu { ForEach(controller.queues) { queue in @@ -375,8 +396,25 @@ struct DownloadTable: View { } } } label: { - Label("Add to Queue", systemImage: "list.bullet") + Label("Move to Queue", systemImage: "list.bullet") } + Divider() + } + + Button { + NSPasteboard.general.clearContents() + let urls = targetItems.map { $0.url.absoluteString }.joined(separator: "\n") + NSPasteboard.general.setString(urls, forType: .string) + } label: { + Label(targetItems.count > 1 ? "Copy Addresses" : "Copy Address", systemImage: "link") + } + + Button { + for target in targetItems { + openWindow(value: target.id) + } + } label: { + Label(targetItems.count > 1 ? "Properties (\(targetItems.count))" : "Properties", systemImage: "info.circle") } Divider() @@ -384,7 +422,7 @@ struct DownloadTable: View { Button(role: .destructive) { pendingDeleteItems = Set(targetItems.map(\.id)) } label: { - Label("Delete", systemImage: "trash") + Label("Remove", systemImage: "trash") } } @@ -523,6 +561,15 @@ struct DownloadTable: View { } return candidate } + + private func openFile(_ item: DownloadItem) { + let fileURL = item.destinationDirectory.appendingPathComponent(item.fileName) + if FileManager.default.fileExists(atPath: fileURL.path) { + NSWorkspace.shared.open(fileURL) + } else { + NSWorkspace.shared.open(existingFolder(for: item.destinationDirectory)) + } + } } private struct DownloadRow: View { diff --git a/Sources/Firelink/FirelinkApp.swift b/Sources/Firelink/FirelinkApp.swift index b04e838..28fa581 100644 --- a/Sources/Firelink/FirelinkApp.swift +++ b/Sources/Firelink/FirelinkApp.swift @@ -60,9 +60,21 @@ struct FirelinkApp: App { } } - MenuBarExtra("Firelink", systemImage: "arrow.down.circle", isInserted: $showMenuBarIcon) { + MenuBarExtra(isInserted: $showMenuBarIcon) { TrayMenuView() .environmentObject(controller) + } label: { + if let nsImage = { () -> NSImage? in + guard let url = Bundle.main.url(forResource: "MenuBarIconTemplate", withExtension: "png"), + let img = NSImage(contentsOf: url) else { return nil } + img.size = NSSize(width: 21, height: 21) + img.isTemplate = true + return img + }() { + Image(nsImage: nsImage) + } else { + Image(systemName: "arrow.down.circle") + } } } }