From 6b2901bd50f9f875d07d67b4cf73abdb2308d9a5 Mon Sep 17 00:00:00 2001
From: nimbold <11913706+nimbold@users.noreply.github.com>
Date: Mon, 8 Jun 2026 21:17:44 +0330
Subject: [PATCH] feat(updater): upgrade sparkle to 2.9.3 and enhance
integration
- Update Sparkle dependency to 2.9.3
- Replace brittle HTML parsing with native NSAttributedString parsing
- Fix dangling updater callbacks by handling view disappear events
- Add 'Remind Me Later' button in update prompt
- Add toggle for automatic background update checks
---
Package.resolved | 6 +-
Package.swift | 2 +-
Sources/Firelink/FirelinkApp.swift | 8 ++-
.../Firelink/Settings/AboutSettingsPane.swift | 31 ++++++++--
.../Settings/InlineUpdateUserDriver.swift | 56 ++++---------------
5 files changed, 50 insertions(+), 53 deletions(-)
diff --git a/Package.resolved b/Package.resolved
index cccaae6..410effa 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -1,13 +1,13 @@
{
- "originHash" : "048cca0a42e966dd91de6a4753f25d908574338fda8bf9b8bcae473cf159ebf4",
+ "originHash" : "c1cb50a392a5949f6fb77fb4800ef2ea6c811268af19d41dd83b3be29f0321a8",
"pins" : [
{
"identity" : "sparkle",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle",
"state" : {
- "revision" : "6276ba2b404829d139c45ff98427cf90e2efc59b",
- "version" : "2.9.2"
+ "revision" : "d46d456107feacc80711b21847b82b07bd9fb46e",
+ "version" : "2.9.3"
}
}
],
diff --git a/Package.swift b/Package.swift
index c11c656..366df71 100644
--- a/Package.swift
+++ b/Package.swift
@@ -11,7 +11,7 @@ let package = Package(
.executable(name: "Firelink", targets: ["Firelink"])
],
dependencies: [
- .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.6.4")
+ .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.9.3")
],
targets: [
.executableTarget(
diff --git a/Sources/Firelink/FirelinkApp.swift b/Sources/Firelink/FirelinkApp.swift
index 66257ca..4bd3859 100644
--- a/Sources/Firelink/FirelinkApp.swift
+++ b/Sources/Firelink/FirelinkApp.swift
@@ -14,7 +14,12 @@ final class SparkleUpdater: NSObject, ObservableObject, SPUUpdaterDelegate {
@Published var updateStatus: String?
@Published var foundUpdateItem: SUAppcastItem?
- @Published var releaseNotes: String?
+ @Published var releaseNotes: AttributedString?
+ @Published var automaticallyChecksForUpdates: Bool = true {
+ didSet {
+ _updater?.automaticallyChecksForUpdates = automaticallyChecksForUpdates
+ }
+ }
var expectedContentLength: UInt64 = 0
var receivedContentLength: UInt64 = 0
@@ -28,6 +33,7 @@ final class SparkleUpdater: NSObject, ObservableObject, SPUUpdaterDelegate {
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)")
}
diff --git a/Sources/Firelink/Settings/AboutSettingsPane.swift b/Sources/Firelink/Settings/AboutSettingsPane.swift
index 337ea9c..53b3ca9 100644
--- a/Sources/Firelink/Settings/AboutSettingsPane.swift
+++ b/Sources/Firelink/Settings/AboutSettingsPane.swift
@@ -85,7 +85,9 @@ struct AboutSettingsPane: View {
}
Button {
- sparkleUpdater.updateChoiceReply?(.install)
+ let reply = sparkleUpdater.updateChoiceReply
+ sparkleUpdater.updateChoiceReply = nil
+ reply?(.install)
} label: {
Label("Install and Relaunch", systemImage: "sparkles")
.frame(maxWidth: .infinity)
@@ -108,7 +110,7 @@ struct AboutSettingsPane: View {
}
}
- if let notes = sparkleUpdater.releaseNotes, !notes.isEmpty {
+ if let notes = sparkleUpdater.releaseNotes {
DisclosureGroup("What's New") {
ScrollView {
Text(notes)
@@ -125,14 +127,24 @@ struct AboutSettingsPane: View {
HStack(spacing: 12) {
Button {
- sparkleUpdater.updateChoiceReply?(.install)
+ let reply = sparkleUpdater.updateChoiceReply
+ sparkleUpdater.updateChoiceReply = nil
+ reply?(.install)
} label: {
Text("Download & Install")
}
.buttonStyle(.borderedProminent)
+ Button("Remind Me Later") {
+ let reply = sparkleUpdater.updateChoiceReply
+ sparkleUpdater.updateChoiceReply = nil
+ reply?(.dismiss)
+ }
+
Button("Skip This Version") {
- sparkleUpdater.updateChoiceReply?(.skip)
+ let reply = sparkleUpdater.updateChoiceReply
+ sparkleUpdater.updateChoiceReply = nil
+ reply?(.skip)
}
}
}
@@ -204,6 +216,11 @@ struct AboutSettingsPane: View {
}
}
}
+
+ Divider()
+ .padding(.vertical, 4)
+
+ Toggle("Automatically check for updates", isOn: $sparkleUpdater.automaticallyChecksForUpdates)
}
.padding(.vertical, 8)
.animation(.easeInOut, value: sparkleUpdater.isChecking)
@@ -250,5 +267,11 @@ struct AboutSettingsPane: View {
}
}
.formStyle(.grouped)
+ .onDisappear {
+ if let reply = sparkleUpdater.updateChoiceReply {
+ sparkleUpdater.updateChoiceReply = nil
+ reply(.dismiss)
+ }
+ }
}
}
diff --git a/Sources/Firelink/Settings/InlineUpdateUserDriver.swift b/Sources/Firelink/Settings/InlineUpdateUserDriver.swift
index d4c3941..5213c11 100644
--- a/Sources/Firelink/Settings/InlineUpdateUserDriver.swift
+++ b/Sources/Firelink/Settings/InlineUpdateUserDriver.swift
@@ -32,55 +32,23 @@ class InlineUpdateUserDriver: NSObject, SPUUserDriver {
}
func showUpdateReleaseNotes(with downloadData: SPUDownloadData) {
- DispatchQueue.global(qos: .userInitiated).async {
- if let htmlString = String(data: downloadData.data, encoding: .utf8) {
- let parsedText = self.fastHTMLToMarkdown(htmlString)
- DispatchQueue.main.async {
- self.updater?.releaseNotes = parsedText
+ DispatchQueue.main.async {
+ let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [
+ .documentType: NSAttributedString.DocumentType.html,
+ .characterEncoding: String.Encoding.utf8.rawValue
+ ]
+ if let nsAttrString = try? NSMutableAttributedString(data: downloadData.data, options: options, documentAttributes: nil) {
+ let range = NSRange(location: 0, length: nsAttrString.length)
+ nsAttrString.removeAttribute(.foregroundColor, range: range)
+ nsAttrString.removeAttribute(.font, range: range)
+
+ if let attrString = try? AttributedString(nsAttrString, including: \.appKit) {
+ self.updater?.releaseNotes = attrString
}
}
}
}
- nonisolated private func fastHTMLToMarkdown(_ html: String) -> String {
- var text = html
- text = text.replacingOccurrences(of: "
", with: "\n", options: .caseInsensitive)
- text = text.replacingOccurrences(of: "
", with: "\n", options: .caseInsensitive)
- text = text.replacingOccurrences(of: "
", with: "\n", options: .caseInsensitive)
- text = text.replacingOccurrences(of: "