mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
feat(ui): redesign settings panes and enhance update flows
- Implemented `InlineUpdateUserDriver` to handle inline update checks inside Settings panes. - Redesigned `AboutSettingsPane` with a modern inline update UI, release notes DisclosureGroup, and clear visual feedback states. - Rebuilt `EngineSettingsPane` using native macOS layouts, removing custom stacks for better native alignment. - Relocated browser cookie selection and media engine updates in `EngineSettingsPane` for better contextual grouping.
This commit is contained in:
@@ -2,59 +2,63 @@ import SwiftUI
|
||||
import Sparkle
|
||||
|
||||
final class SparkleUpdater: NSObject, ObservableObject, SPUUpdaterDelegate {
|
||||
private var _controller: SPUStandardUpdaterController?
|
||||
var controller: SPUStandardUpdaterController { _controller! }
|
||||
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: String?
|
||||
|
||||
var expectedContentLength: UInt64 = 0
|
||||
var receivedContentLength: UInt64 = 0
|
||||
var cancellation: (() -> Void)?
|
||||
var updateChoiceReply: ((SPUUserUpdateChoice) -> Void)?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
self._controller = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: self, userDriverDelegate: nil)
|
||||
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()
|
||||
} catch {
|
||||
print("Failed to start Sparkle updater: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func checkForUpdates() {
|
||||
guard controller.updater.canCheckForUpdates else {
|
||||
guard updater.canCheckForUpdates else {
|
||||
isChecking = false
|
||||
updateStatus = "Update check is already in progress."
|
||||
return
|
||||
}
|
||||
updater.checkForUpdates()
|
||||
}
|
||||
|
||||
isChecking = true
|
||||
updateStatus = "Checking for updates..."
|
||||
func resetState() {
|
||||
isChecking = false
|
||||
isDownloading = false
|
||||
isExtracting = false
|
||||
isReadyToInstall = false
|
||||
downloadProgress = 0.0
|
||||
extractionProgress = 0.0
|
||||
updateStatus = nil
|
||||
foundUpdateItem = nil
|
||||
controller.checkForUpdates(nil)
|
||||
}
|
||||
|
||||
func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) {
|
||||
DispatchQueue.main.async {
|
||||
self.isChecking = false
|
||||
self.foundUpdateItem = item
|
||||
self.updateStatus = "Update available: Version \(item.displayVersionString)"
|
||||
}
|
||||
}
|
||||
|
||||
func updaterDidNotFindUpdate(_ updater: SPUUpdater, error: Error) {
|
||||
DispatchQueue.main.async {
|
||||
self.isChecking = false
|
||||
self.updateStatus = "You're up to date!"
|
||||
}
|
||||
}
|
||||
|
||||
func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
|
||||
DispatchQueue.main.async {
|
||||
let nsError = error as NSError
|
||||
if nsError.domain == "SUSparkleErrorDomain" && nsError.code == 1001 {
|
||||
// SUNoUpdateError (1001), handled by updaterDidNotFindUpdate
|
||||
return
|
||||
}
|
||||
|
||||
self.isChecking = false
|
||||
self.updateStatus = "Update check failed: \(error.localizedDescription)"
|
||||
}
|
||||
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
|
||||
|
||||
@@ -40,41 +40,177 @@ struct AboutSettingsPane: View {
|
||||
}
|
||||
|
||||
Section("Updates") {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
if let status = sparkleUpdater.updateStatus {
|
||||
HStack {
|
||||
if sparkleUpdater.isChecking {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
} else if sparkleUpdater.foundUpdateItem != nil {
|
||||
Image(systemName: "exclamationmark.circle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
} else {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
Text(status)
|
||||
.font(.subheadline)
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
if sparkleUpdater.isChecking {
|
||||
HStack(spacing: 12) {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
Text("Checking for updates...")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Button {
|
||||
sparkleUpdater.checkForUpdates()
|
||||
} label: {
|
||||
Label("Check for Updates", systemImage: "arrow.clockwise")
|
||||
} else if sparkleUpdater.isDownloading || sparkleUpdater.isExtracting {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text(sparkleUpdater.isDownloading ? "Downloading update..." : "Extracting update...")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
Spacer()
|
||||
if sparkleUpdater.isDownloading && sparkleUpdater.downloadProgress > 0 {
|
||||
Text("\(Int(sparkleUpdater.downloadProgress * 100))%")
|
||||
.font(.caption.monospacedDigit())
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
ProgressView(value: sparkleUpdater.isDownloading ? sparkleUpdater.downloadProgress : sparkleUpdater.extractionProgress)
|
||||
.tint(.accentColor)
|
||||
|
||||
Button("Cancel") {
|
||||
sparkleUpdater.cancellation?()
|
||||
}
|
||||
.controlSize(.small)
|
||||
}
|
||||
.disabled(sparkleUpdater.isChecking)
|
||||
} else if sparkleUpdater.isReadyToInstall {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Image(systemName: "arrow.down.app.fill")
|
||||
.foregroundStyle(.green)
|
||||
.font(.title2)
|
||||
VStack(alignment: .leading) {
|
||||
Text("Update Ready")
|
||||
.font(.headline)
|
||||
Text("The new version is ready to be installed.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
sparkleUpdater.updateChoiceReply?(.install)
|
||||
} label: {
|
||||
Label("Install and Relaunch", systemImage: "sparkles")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
}
|
||||
} else if let item = sparkleUpdater.foundUpdateItem {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Image(systemName: "exclamationmark.arrow.circlepath")
|
||||
.foregroundStyle(.orange)
|
||||
.font(.title)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Update Available")
|
||||
.font(.headline)
|
||||
Text("Version \(item.displayVersionString) is available.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
if let notes = sparkleUpdater.releaseNotes, !notes.isEmpty {
|
||||
DisclosureGroup("What's New") {
|
||||
ScrollView {
|
||||
Text(notes)
|
||||
.font(.caption)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
.frame(maxHeight: 150)
|
||||
.padding(8)
|
||||
.background(Color(NSColor.controlBackgroundColor))
|
||||
.cornerRadius(6)
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Button {
|
||||
sparkleUpdater.updateChoiceReply?(.install)
|
||||
} label: {
|
||||
Text("Download & Install")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
Button("Skip This Version") {
|
||||
sparkleUpdater.updateChoiceReply?(.skip)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Up to date or initial state
|
||||
if let status = sparkleUpdater.updateStatus, status == "You're up to date!" {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
.font(.title)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("You're up to date!")
|
||||
.font(.headline)
|
||||
Text("Firelink \(appVersion) is the newest version available.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Button {
|
||||
sparkleUpdater.checkForUpdates()
|
||||
} label: {
|
||||
Label("Check Again", systemImage: "arrow.clockwise")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
Button {
|
||||
NSWorkspace.shared.open(projectURL.appendingPathComponent("releases"))
|
||||
} label: {
|
||||
Label("Release Notes", systemImage: "doc.text")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
HStack(spacing: 12) {
|
||||
if let status = sparkleUpdater.updateStatus {
|
||||
if status.lowercased().contains("failed") || status.lowercased().contains("error") {
|
||||
Image(systemName: "xmark.octagon.fill")
|
||||
.foregroundStyle(.red)
|
||||
} else {
|
||||
Image(systemName: "info.circle.fill")
|
||||
.foregroundStyle(.blue)
|
||||
}
|
||||
Text(status)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text("Keeping your app up to date ensures you have the latest features and security improvements.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
NSWorkspace.shared.open(projectURL.appendingPathComponent("releases"))
|
||||
} label: {
|
||||
Label("Open Releases", systemImage: "arrow.up.right.square")
|
||||
HStack(spacing: 12) {
|
||||
Button {
|
||||
sparkleUpdater.checkForUpdates()
|
||||
} label: {
|
||||
Label("Check for Updates", systemImage: "arrow.clockwise")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
Button {
|
||||
NSWorkspace.shared.open(projectURL.appendingPathComponent("releases"))
|
||||
} label: {
|
||||
Label("Release Notes", systemImage: "doc.text")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
.padding(.vertical, 8)
|
||||
.animation(.easeInOut, value: sparkleUpdater.isChecking)
|
||||
.animation(.easeInOut, value: sparkleUpdater.isDownloading)
|
||||
.animation(.easeInOut, value: sparkleUpdater.isExtracting)
|
||||
.animation(.easeInOut, value: sparkleUpdater.isReadyToInstall)
|
||||
.animation(.easeInOut, value: sparkleUpdater.foundUpdateItem != nil)
|
||||
}
|
||||
|
||||
Section {
|
||||
|
||||
@@ -15,94 +15,100 @@ struct EngineSettingsPane: View {
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section("Aria2 (HTTP/FTP)") {
|
||||
Section {
|
||||
LabeledContent("Status") {
|
||||
Label(
|
||||
executableURL == nil ? "Missing" : "Ready",
|
||||
systemImage: executableURL == nil ? "exclamationmark.triangle.fill" : "checkmark.seal.fill"
|
||||
)
|
||||
.foregroundStyle(executableURL == nil ? .orange : .green)
|
||||
if executableURL != nil {
|
||||
Label("Ready", systemImage: "checkmark.seal.fill")
|
||||
.foregroundStyle(.green)
|
||||
} else {
|
||||
Label("Missing", systemImage: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
}
|
||||
|
||||
LabeledContent("Binary") {
|
||||
Text(executableURL?.path ?? "Not found")
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
|
||||
|
||||
LabeledContent("Version") {
|
||||
Text(version)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
|
||||
|
||||
LabeledContent("Binary Path") {
|
||||
Text(executableURL?.path ?? "Not found")
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.foregroundStyle(.tertiary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
.help(executableURL?.path ?? "")
|
||||
} header: {
|
||||
Text("Core Downloader (Aria2)")
|
||||
} footer: {
|
||||
if executableURL == nil {
|
||||
Text("Install aria2 with Homebrew or bundle aria2c inside the app resources.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Install aria2 with Homebrew or ensure it is bundled inside the app resources.")
|
||||
.foregroundStyle(.red)
|
||||
} else {
|
||||
Text("Handles core HTTP/FTP and BitTorrent downloads.")
|
||||
}
|
||||
}
|
||||
|
||||
Section("Media Engine (yt-dlp & ffmpeg)") {
|
||||
addonStatusRow(title: "yt-dlp", state: engineManager.ytDlpState)
|
||||
addonStatusRow(title: "ffmpeg", state: engineManager.ffmpegState)
|
||||
Section {
|
||||
LabeledContent("Updates") {
|
||||
HStack(spacing: 8) {
|
||||
Button {
|
||||
checkMediaEngineUpdates()
|
||||
} label: {
|
||||
Text("Check for Updates")
|
||||
}
|
||||
.disabled(isDownloadingMediaEngines || isCheckingForUpdates)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Button("Check for Updates") {
|
||||
Task {
|
||||
isCheckingForUpdates = true
|
||||
updateCheckResult = nil
|
||||
try? await Task.sleep(nanoseconds: 800_000_000)
|
||||
|
||||
do {
|
||||
try await engineManager.ensureInstalled()
|
||||
updateCheckResult = "Up to date."
|
||||
} catch {
|
||||
updateCheckResult = "Update failed."
|
||||
}
|
||||
|
||||
isCheckingForUpdates = false
|
||||
try? await Task.sleep(nanoseconds: 3_000_000_000)
|
||||
if !isCheckingForUpdates {
|
||||
updateCheckResult = nil
|
||||
if isCheckingForUpdates {
|
||||
ProgressView().controlSize(.small)
|
||||
Text("Checking...")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.subheadline)
|
||||
} else if let result = updateCheckResult {
|
||||
if result == "Up to date" || result == "Updated successfully" {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
Text(result)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.subheadline)
|
||||
} else {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.red)
|
||||
Text(result)
|
||||
.foregroundStyle(.red)
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(isDownloadingMediaEngines || isCheckingForUpdates)
|
||||
|
||||
if isCheckingForUpdates {
|
||||
ProgressView().controlSize(.small)
|
||||
Text("Checking...")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.subheadline)
|
||||
} else if let result = updateCheckResult {
|
||||
Text(result)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Picker("Browser Cookies", selection: $settings.mediaCookieSource) {
|
||||
ForEach(BrowserCookieSource.allCases, id: \.self) { source in
|
||||
Text(source.rawValue).tag(source)
|
||||
addonStatusRow(title: "yt-dlp", state: engineManager.ytDlpState)
|
||||
|
||||
LabeledContent("Browser Cookies") {
|
||||
Picker("", selection: $settings.mediaCookieSource) {
|
||||
ForEach(BrowserCookieSource.allCases, id: \.self) { source in
|
||||
Text(source.rawValue).tag(source)
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
.frame(maxWidth: 200)
|
||||
}
|
||||
|
||||
addonStatusRow(title: "FFmpeg", state: engineManager.ffmpegState)
|
||||
} header: {
|
||||
Text("Media Extractors")
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Powers video and audio extraction from supported sites.")
|
||||
|
||||
if settings.mediaCookieSource != .none {
|
||||
Text(settings.mediaCookieSource.statusDetail)
|
||||
}
|
||||
}
|
||||
|
||||
LabeledContent("Cookie Status") {
|
||||
if settings.mediaCookieSource == .none {
|
||||
Label(settings.mediaCookieSource.statusTitle, systemImage: "circle")
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Label(settings.mediaCookieSource.statusTitle, systemImage: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
}
|
||||
|
||||
Text(settings.mediaCookieSource.statusDetail)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
@@ -111,6 +117,35 @@ struct EngineSettingsPane: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func checkMediaEngineUpdates() {
|
||||
Task {
|
||||
isCheckingForUpdates = true
|
||||
updateCheckResult = nil
|
||||
// Brief visual feedback delay
|
||||
try? await Task.sleep(nanoseconds: 800_000_000)
|
||||
|
||||
do {
|
||||
let wasDownloading = isDownloadingMediaEngines
|
||||
try await engineManager.ensureInstalled()
|
||||
if wasDownloading || isDownloadingMediaEngines {
|
||||
updateCheckResult = "Updated successfully"
|
||||
} else {
|
||||
updateCheckResult = "Up to date"
|
||||
}
|
||||
} catch {
|
||||
updateCheckResult = "Update failed: \(error.localizedDescription)"
|
||||
}
|
||||
|
||||
isCheckingForUpdates = false
|
||||
try? await Task.sleep(nanoseconds: 4_000_000_000)
|
||||
if !isCheckingForUpdates {
|
||||
withAnimation {
|
||||
updateCheckResult = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isDownloadingMediaEngines: Bool {
|
||||
if case .downloading = engineManager.ytDlpState { return true }
|
||||
if case .downloading = engineManager.ffmpegState { return true }
|
||||
@@ -136,8 +171,9 @@ struct EngineSettingsPane: View {
|
||||
.foregroundStyle(.green)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
case .failed(let error):
|
||||
Label("Failed: \(error)", systemImage: "exclamationmark.triangle.fill")
|
||||
Label("Failed", systemImage: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.red)
|
||||
.help(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
import Sparkle
|
||||
|
||||
class InlineUpdateUserDriver: NSObject, SPUUserDriver {
|
||||
weak var updater: SparkleUpdater?
|
||||
|
||||
init(updater: SparkleUpdater) {
|
||||
self.updater = updater
|
||||
}
|
||||
|
||||
func show(_ request: SPUUpdatePermissionRequest, reply: @escaping (SUUpdatePermissionResponse) -> Void) {
|
||||
reply(SUUpdatePermissionResponse(automaticUpdateChecks: true, sendSystemProfile: false))
|
||||
}
|
||||
|
||||
func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) {
|
||||
DispatchQueue.main.async {
|
||||
self.updater?.resetState()
|
||||
self.updater?.isChecking = true
|
||||
self.updater?.updateStatus = "Checking for updates..."
|
||||
self.updater?.cancellation = cancellation
|
||||
}
|
||||
}
|
||||
|
||||
func showUpdateFound(with appcastItem: SUAppcastItem, state: SPUUserUpdateState, reply: @escaping (SPUUserUpdateChoice) -> Void) {
|
||||
DispatchQueue.main.async {
|
||||
self.updater?.isChecking = false
|
||||
self.updater?.foundUpdateItem = appcastItem
|
||||
self.updater?.updateStatus = "Update available: Version \(appcastItem.displayVersionString)"
|
||||
self.updater?.updateChoiceReply = reply
|
||||
}
|
||||
}
|
||||
|
||||
func showUpdateReleaseNotes(with downloadData: SPUDownloadData) {
|
||||
DispatchQueue.main.async {
|
||||
if let htmlString = String(data: downloadData.data, encoding: .utf8) {
|
||||
self.updater?.releaseNotes = self.stripHTML(htmlString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stripHTML(_ string: String) -> String {
|
||||
guard let data = string.data(using: .utf8) else { return string }
|
||||
if let attributedString = try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil) {
|
||||
return attributedString.string.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
func showUpdateReleaseNotesFailedToDownloadWithError(_ error: Error) {
|
||||
}
|
||||
|
||||
func showUpdateNotFoundWithError(_ error: Error, acknowledgement: @escaping () -> Void) {
|
||||
DispatchQueue.main.async {
|
||||
self.updater?.isChecking = false
|
||||
let nsError = error as NSError
|
||||
if nsError.domain == SUSparkleErrorDomain && nsError.code == 1001 {
|
||||
self.updater?.updateStatus = "You're up to date!"
|
||||
} else {
|
||||
self.updater?.updateStatus = "Update check failed: \(error.localizedDescription)"
|
||||
}
|
||||
acknowledgement()
|
||||
}
|
||||
}
|
||||
|
||||
func showUpdaterError(_ error: Error, acknowledgement: @escaping () -> Void) {
|
||||
DispatchQueue.main.async {
|
||||
self.updater?.isChecking = false
|
||||
self.updater?.updateStatus = "Updater error: \(error.localizedDescription)"
|
||||
acknowledgement()
|
||||
}
|
||||
}
|
||||
|
||||
func showDownloadInitiated(cancellation: @escaping () -> Void) {
|
||||
DispatchQueue.main.async {
|
||||
self.updater?.isDownloading = true
|
||||
self.updater?.downloadProgress = 0.0
|
||||
self.updater?.cancellation = cancellation
|
||||
self.updater?.updateStatus = "Downloading update..."
|
||||
}
|
||||
}
|
||||
|
||||
func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) {
|
||||
DispatchQueue.main.async {
|
||||
self.updater?.expectedContentLength = expectedContentLength
|
||||
self.updater?.receivedContentLength = 0
|
||||
}
|
||||
}
|
||||
|
||||
func showDownloadDidReceiveData(ofLength length: UInt64) {
|
||||
DispatchQueue.main.async {
|
||||
if let updater = self.updater {
|
||||
updater.receivedContentLength += length
|
||||
if updater.expectedContentLength > 0 {
|
||||
updater.downloadProgress = Double(updater.receivedContentLength) / Double(updater.expectedContentLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showDownloadDidStartExtractingUpdate() {
|
||||
DispatchQueue.main.async {
|
||||
self.updater?.isDownloading = false
|
||||
self.updater?.isExtracting = true
|
||||
self.updater?.updateStatus = "Extracting update..."
|
||||
self.updater?.downloadProgress = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
func showExtractionReceivedProgress(_ progress: Double) {
|
||||
DispatchQueue.main.async {
|
||||
self.updater?.extractionProgress = progress
|
||||
}
|
||||
}
|
||||
|
||||
func showReady(toInstallAndRelaunch reply: @escaping (SPUUserUpdateChoice) -> Void) {
|
||||
DispatchQueue.main.async {
|
||||
self.updater?.isExtracting = false
|
||||
self.updater?.isReadyToInstall = true
|
||||
self.updater?.updateStatus = "Ready to install"
|
||||
self.updater?.updateChoiceReply = reply
|
||||
}
|
||||
}
|
||||
|
||||
func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) {
|
||||
}
|
||||
|
||||
func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) {
|
||||
acknowledgement()
|
||||
}
|
||||
|
||||
func dismissUpdateInstallation() {
|
||||
DispatchQueue.main.async {
|
||||
self.updater?.isChecking = false
|
||||
self.updater?.isDownloading = false
|
||||
self.updater?.isExtracting = false
|
||||
self.updater?.isReadyToInstall = false
|
||||
self.updater?.downloadProgress = 0.0
|
||||
self.updater?.extractionProgress = 0.0
|
||||
self.updater?.foundUpdateItem = nil
|
||||
self.updater?.releaseNotes = nil
|
||||
// Do not clear updateStatus here so success/error messages remain visible.
|
||||
}
|
||||
}
|
||||
|
||||
func showUpdateInFocus() {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user