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: "

", with: "\n\n", options: .caseInsensitive) - text = text.replacingOccurrences(of: "
  • ", with: "- ", options: .caseInsensitive) - text = text.replacingOccurrences(of: "
  • ", with: "\n", options: .caseInsensitive) - text = text.replacingOccurrences(of: "

    ", with: "# ", options: .caseInsensitive) - text = text.replacingOccurrences(of: "

    ", with: "\n\n", options: .caseInsensitive) - text = text.replacingOccurrences(of: "

    ", with: "## ", options: .caseInsensitive) - text = text.replacingOccurrences(of: "

    ", with: "\n\n", options: .caseInsensitive) - text = text.replacingOccurrences(of: "

    ", with: "### ", options: .caseInsensitive) - text = text.replacingOccurrences(of: "

    ", with: "\n\n", options: .caseInsensitive) - text = text.replacingOccurrences(of: "", with: "**", options: .caseInsensitive) - text = text.replacingOccurrences(of: "", with: "**", options: .caseInsensitive) - text = text.replacingOccurrences(of: "", with: "**", options: .caseInsensitive) - text = text.replacingOccurrences(of: "", with: "**", options: .caseInsensitive) - - if let regex = try? NSRegularExpression(pattern: "<[^>]+>", options: .caseInsensitive) { - let range = NSRange(location: 0, length: text.utf16.count) - text = regex.stringByReplacingMatches(in: text, options: [], range: range, withTemplate: "") - } - - text = text.replacingOccurrences(of: " ", with: " ") - text = text.replacingOccurrences(of: "&", with: "&") - text = text.replacingOccurrences(of: "<", with: "<") - text = text.replacingOccurrences(of: ">", with: ">") - text = text.replacingOccurrences(of: """, with: "\"") - text = text.replacingOccurrences(of: "'", with: "'") - - if let regex = try? NSRegularExpression(pattern: "\\n{3,}", options: []) { - let range = NSRange(location: 0, length: text.utf16.count) - text = regex.stringByReplacingMatches(in: text, options: [], range: range, withTemplate: "\n\n") - } - - return text.trimmingCharacters(in: .whitespacesAndNewlines) - } - func showUpdateReleaseNotesFailedToDownloadWithError(_ error: Error) { }