update progress

This commit is contained in:
Ian Gulliver
2026-10-19 19:36:42 -07:00
parent 88d1146778
commit 52a231e635
2 changed files with 125 additions and 10 deletions
+124 -9
View File
@@ -1,12 +1,59 @@
import AppKit
import Sparkle
import SwiftUI
@MainActor
@Observable
final class UpdateProgress {
var title = ""
var done = 0.0
var total: Double?
var detail = ""
var cancel: (() -> Void)?
}
private struct UpdateProgressView: View {
let progress: UpdateProgress
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(progress.title)
.font(Fonts.font(13, weight: 700))
.foregroundStyle(Palette.text)
if let total = progress.total {
ProgressView(value: min(progress.done, total), total: total)
.tint(Palette.accent)
} else {
ProgressView()
.progressViewStyle(.linear)
.tint(Palette.accent)
}
HStack {
Text(progress.detail)
.font(Fonts.font(12))
.foregroundStyle(Palette.muted)
Spacer()
if let cancel = progress.cancel {
Button("Cancel", action: cancel)
.buttonStyle(WebButtonStyle())
}
}
}
.padding(16)
.frame(width: 360)
.background(Palette.bg.ignoresSafeArea())
}
}
@MainActor
final class Updater: NSObject, SPUUserDriver, SPUUpdaterDelegate {
static let shared = Updater()
private var updater: SPUUpdater?
private var now = false
private var version = ""
private var staged: (item: SUAppcastItem, install: () -> Void)?
private let progress = UpdateProgress()
private var panel: NSPanel?
func start() {
let updater = SPUUpdater(hostBundle: .main, applicationBundle: .main, userDriver: self, delegate: self)
@@ -20,7 +67,8 @@ final class Updater: NSObject, SPUUserDriver, SPUUpdaterDelegate {
func check() {
if let staged {
if ask(staged.item) {
now = ask(staged.item)
if now {
staged.install()
}
return
@@ -29,6 +77,7 @@ final class Updater: NSObject, SPUUserDriver, SPUUpdaterDelegate {
}
private func ask(_ item: SUAppcastItem) -> Bool {
version = item.displayVersionString
let current = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
let alert = NSAlert()
alert.messageText = "Byre \(item.displayVersionString) is available"
@@ -47,9 +96,38 @@ final class Updater: NSObject, SPUUserDriver, SPUUpdaterDelegate {
alert.runModal()
}
private func showProgress(_ title: String, total: Double?, cancel: (() -> Void)? = nil) {
progress.title = title
progress.done = 0
progress.total = total
progress.detail = ""
progress.cancel = cancel
if panel == nil {
let p = NSPanel(contentRect: .zero, styleMask: [.titled, .fullSizeContentView], backing: .buffered, defer: false)
p.titleVisibility = .hidden
p.titlebarAppearsTransparent = true
p.isReleasedWhenClosed = false
p.contentViewController = NSHostingController(rootView: UpdateProgressView(progress: progress))
p.center()
panel = p
}
NSApp.activate()
panel?.makeKeyAndOrderFront(nil)
}
private func hideProgress() {
panel?.orderOut(nil)
panel = nil
}
private func bytes(_ n: Double) -> String {
Int64(n).formatted(.byteCount(style: .file))
}
func updater(_ updater: SPUUpdater, willInstallUpdateOnQuit item: SUAppcastItem, immediateInstallationBlock immediateInstallHandler: @escaping () -> Void) -> Bool {
staged = (item, immediateInstallHandler)
if ask(item) {
now = ask(item)
if now {
immediateInstallHandler()
}
return true
@@ -80,29 +158,66 @@ final class Updater: NSObject, SPUUserDriver, SPUUpdaterDelegate {
}
func showUpdaterError(_ error: any Error, acknowledgement: @escaping () -> Void) {
hideProgress()
tell("Update failed", error.localizedDescription)
acknowledgement()
}
func showDownloadInitiated(cancellation: @escaping () -> Void) {}
func showDownloadInitiated(cancellation: @escaping () -> Void) {
guard now else {
return
}
showProgress("Downloading Byre \(version)", total: nil) { [weak self] in
self?.now = false
self?.hideProgress()
cancellation()
}
}
func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) {}
func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) {
guard expectedContentLength > 0 else {
return
}
progress.total = Double(expectedContentLength)
}
func showDownloadDidReceiveData(ofLength length: UInt64) {}
func showDownloadDidReceiveData(ofLength length: UInt64) {
progress.done += Double(length)
if let total = progress.total {
progress.detail = "\(bytes(progress.done)) of \(bytes(total))"
} else {
progress.detail = bytes(progress.done)
}
}
func showDownloadDidStartExtractingUpdate() {}
func showDownloadDidStartExtractingUpdate() {
guard now else {
return
}
showProgress("Extracting Byre \(version)", total: 1)
}
func showExtractionReceivedProgress(_ progress: Double) {}
func showExtractionReceivedProgress(_ progress: Double) {
self.progress.done = progress
}
func showReady(toInstallAndRelaunch reply: @escaping (SPUUserUpdateChoice) -> Void) {
reply(now ? .install : .dismiss)
}
func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) {}
func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) {
guard now else {
return
}
showProgress("Installing Byre \(version)", total: nil)
}
func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) {
hideProgress()
acknowledgement()
}
func dismissUpdateInstallation() {}
func dismissUpdateInstallation() {
hideProgress()
}
}