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 0000000..aadf8b4 Binary files /dev/null and b/Sources/Firelink/Assets.xcassets/MenuBarIcon.imageset/MenuBarIconTemplate.png differ 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") + } } } }