mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-28 04:49:39 +00:00
f887c62195
- Generate random pairing token in AppSettings - Update LocalExtensionServer to strict check token on non-OPTIONS endpoints - Add GET /ping endpoint for extension connection verification - Redesign Integration settings pane with modern step-by-step UI and Toast notifications - Harden media extraction and aria2 engine paths - Update IP resolution with getaddrinfo
214 lines
8.3 KiB
Swift
214 lines
8.3 KiB
Swift
import SwiftUI
|
|
import Sparkle
|
|
|
|
final class SparkleUpdater: NSObject, ObservableObject, SPUUpdaterDelegate {
|
|
private var _updater: SPUUpdater?
|
|
var updater: SPUUpdater { _updater! }
|
|
|
|
@Published var isChecking = false
|
|
@Published var isDownloading = false
|
|
@Published var isExtracting = false
|
|
@Published var isReadyToInstall = false
|
|
@Published var downloadProgress: Double = 0.0
|
|
@Published var extractionProgress: Double = 0.0
|
|
|
|
@Published var updateStatus: String?
|
|
@Published var foundUpdateItem: SUAppcastItem?
|
|
@Published var releaseNotes: AttributedString?
|
|
@Published var automaticallyChecksForUpdates: Bool = true {
|
|
didSet {
|
|
_updater?.automaticallyChecksForUpdates = automaticallyChecksForUpdates
|
|
}
|
|
}
|
|
|
|
var expectedContentLength: UInt64 = 0
|
|
var receivedContentLength: UInt64 = 0
|
|
var cancellation: (() -> Void)?
|
|
var updateChoiceReply: ((SPUUserUpdateChoice) -> Void)?
|
|
|
|
override init() {
|
|
super.init()
|
|
let driver = InlineUpdateUserDriver(updater: self)
|
|
let hostBundle = Bundle.main
|
|
self._updater = SPUUpdater(hostBundle: hostBundle, applicationBundle: hostBundle, userDriver: driver, delegate: self)
|
|
do {
|
|
try self._updater?.start()
|
|
self.automaticallyChecksForUpdates = self._updater?.automaticallyChecksForUpdates ?? true
|
|
} catch {
|
|
print("Failed to start Sparkle updater: \(error)")
|
|
}
|
|
}
|
|
|
|
func checkForUpdates() {
|
|
guard updater.canCheckForUpdates else {
|
|
isChecking = false
|
|
updateStatus = "Update check is already in progress."
|
|
return
|
|
}
|
|
updater.checkForUpdates()
|
|
}
|
|
|
|
func resetState() {
|
|
isChecking = false
|
|
isDownloading = false
|
|
isExtracting = false
|
|
isReadyToInstall = false
|
|
downloadProgress = 0.0
|
|
extractionProgress = 0.0
|
|
updateStatus = nil
|
|
foundUpdateItem = nil
|
|
releaseNotes = nil
|
|
expectedContentLength = 0
|
|
receivedContentLength = 0
|
|
cancellation = nil
|
|
updateChoiceReply = nil
|
|
}
|
|
|
|
// Delegate methods can be left mostly empty or minimal since the UserDriver handles the UI state now.
|
|
func updater(_ updater: SPUUpdater, didFinishUpdateCycleFor updateCheck: SPUUpdateCheck, error: Error?) {
|
|
DispatchQueue.main.async {
|
|
self.isChecking = false
|
|
}
|
|
}
|
|
}
|
|
|
|
@main
|
|
struct FirelinkApp: App {
|
|
@StateObject private var sparkleUpdater: SparkleUpdater
|
|
|
|
@StateObject private var settings: AppSettings
|
|
@StateObject private var controller: DownloadController
|
|
@StateObject private var schedulerController: SchedulerController
|
|
@AppStorage("showMenuBarIcon") private var showMenuBarIcon = true
|
|
|
|
// Server must be retained to keep listening
|
|
private let extensionServer: LocalExtensionServer?
|
|
|
|
init() {
|
|
self._sparkleUpdater = StateObject(wrappedValue: SparkleUpdater())
|
|
|
|
let settings = AppSettings()
|
|
let controller = DownloadController(settings: settings)
|
|
_settings = StateObject(wrappedValue: settings)
|
|
_controller = StateObject(wrappedValue: controller)
|
|
_schedulerController = StateObject(wrappedValue: SchedulerController(downloadController: controller))
|
|
|
|
extensionServer = LocalExtensionServer(downloadController: controller, settings: settings)
|
|
extensionServer?.start()
|
|
controller.extensionServerPort = extensionServer?.port
|
|
}
|
|
|
|
var body: some Scene {
|
|
WindowGroup(id: "main") {
|
|
ContentView()
|
|
.environmentObject(controller)
|
|
.environmentObject(settings)
|
|
.environmentObject(schedulerController)
|
|
.environmentObject(sparkleUpdater)
|
|
.modifier(AppThemeModifier(theme: settings.appTheme))
|
|
.modifier(AppFontSizeModifier(fontSize: settings.appFontSize))
|
|
.onOpenURL { url in
|
|
if url.scheme == "firelink" {
|
|
if url.host == "add",
|
|
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
|
let queryItems = components.queryItems,
|
|
let link = queryItems.first(where: { $0.name == "url" })?.value {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
controller.pendingPasteboardText = link
|
|
controller.pendingReferer = nil
|
|
NotificationCenter.default.post(name: NSNotification.Name("OpenAddDownloadsWindow"), object: nil)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
controller.pendingPasteboardText = url.absoluteString
|
|
controller.pendingReferer = nil
|
|
NotificationCenter.default.post(name: NSNotification.Name("OpenAddDownloadsWindow"), object: nil)
|
|
}
|
|
}
|
|
.frame(minWidth: 1180, idealWidth: 1280, minHeight: 720, idealHeight: 760)
|
|
}
|
|
.windowStyle(.titleBar)
|
|
|
|
Window("Add Downloads", id: "add-downloads") {
|
|
AddDownloadsView()
|
|
.environmentObject(controller)
|
|
.environmentObject(settings)
|
|
.modifier(AppThemeModifier(theme: settings.appTheme))
|
|
.modifier(AppFontSizeModifier(fontSize: settings.appFontSize))
|
|
}
|
|
.windowResizability(.contentSize)
|
|
|
|
WindowGroup("Download Properties", id: "download-properties", for: UUID.self) { $downloadID in
|
|
if let downloadID {
|
|
DownloadPropertiesWindow(downloadID: downloadID)
|
|
.environmentObject(controller)
|
|
.environmentObject(settings)
|
|
.modifier(AppThemeModifier(theme: settings.appTheme))
|
|
.modifier(AppFontSizeModifier(fontSize: settings.appFontSize))
|
|
} else {
|
|
ContentUnavailableView("Download Not Found", systemImage: "questionmark.circle")
|
|
.modifier(AppThemeModifier(theme: settings.appTheme))
|
|
.modifier(AppFontSizeModifier(fontSize: settings.appFontSize))
|
|
}
|
|
}
|
|
.windowResizability(.contentSize)
|
|
|
|
.commands {
|
|
CommandGroup(after: .newItem) {
|
|
Button("Add Downloads...") {
|
|
NotificationCenter.default.post(name: NSNotification.Name("OpenAddDownloadsWindow"), object: nil)
|
|
}
|
|
.keyboardShortcut("n", modifiers: [.command])
|
|
|
|
Divider()
|
|
|
|
Button("Start Queue") {
|
|
controller.startQueue()
|
|
}
|
|
.keyboardShortcut("r", modifiers: [.command])
|
|
|
|
Button("Stop Downloads") {
|
|
controller.pauseActiveDownloads()
|
|
}
|
|
.disabled(controller.activeCount == 0)
|
|
}
|
|
}
|
|
|
|
MenuBarExtra(isInserted: $showMenuBarIcon) {
|
|
TrayMenuView()
|
|
.environmentObject(controller)
|
|
.environmentObject(sparkleUpdater)
|
|
} label: {
|
|
if let nsImage = { () -> NSImage? in
|
|
guard let url = menuBarIconURL(),
|
|
let img = NSImage(contentsOf: url) else { return nil }
|
|
img.size = NSSize(width: 23, height: 23)
|
|
img.isTemplate = true
|
|
return img
|
|
}() {
|
|
Image(nsImage: nsImage)
|
|
} else {
|
|
Image(systemName: "arrow.down.circle")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func menuBarIconURL() -> URL? {
|
|
if let bundled = Bundle.main.url(forResource: "MenuBarIconTemplate", withExtension: "png") {
|
|
return bundled
|
|
}
|
|
|
|
let sourceFile = URL(fileURLWithPath: #filePath)
|
|
let projectRoot = sourceFile
|
|
.deletingLastPathComponent()
|
|
.deletingLastPathComponent()
|
|
.deletingLastPathComponent()
|
|
let sourceTreeIcon = projectRoot
|
|
.appendingPathComponent("Sources/Firelink/Assets.xcassets/MenuBarIcon.imageset/MenuBarIconTemplate.png")
|
|
return FileManager.default.fileExists(atPath: sourceTreeIcon.path) ? sourceTreeIcon : nil
|
|
}
|
|
}
|