feat: show animated installation progress bar in menu bar during update

This commit is contained in:
Nikita 2026-10-06 12:59:35 +05:00
parent 715b5b26f2
commit 7710a300fc
2 changed files with 70 additions and 5 deletions

View file

@ -56,6 +56,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private func updateStatusItem() {
guard let button = statusItem?.button else { return }
if updater.isUpdating {
button.image = NSImage(systemSymbolName: "arrow.triangle.2.circlepath", accessibilityDescription: "Updating")
let text = updater.progressText.isEmpty ? "Обновление" : updater.progressText
button.title = " \(updater.progressVisual) \(text)"
return
}
let active = tracker.activeHeadphone
let isConn = active?.isConnected == true
let percent = active?.primaryBatteryPercent

View file

@ -8,18 +8,64 @@ public final class UpdaterService: ObservableObject {
@Published public var isUpdating: Bool = false
@Published public var updateStatusMessage: String = ""
@Published public var progressText: String = ""
@Published public var progressVisual: String = "▰▱▱▱▱"
private var animationTimer: Timer?
private var animIndex = 0
private let animFrames = ["▰▱▱▱▱", "▰▰▱▱▱", "▰▰▰▱▱", "▰▰▰▰▱", "▰▰▰▰▰", "▱▱▱▱▱"]
private init() {}
private func startAnimation(initialStage: String) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.progressText = initialStage
self.progressVisual = self.animFrames[0]
self.animationTimer?.invalidate()
self.animIndex = 0
self.animationTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] _ in
guard let self = self, self.isUpdating else { return }
self.animIndex = (self.animIndex + 1) % self.animFrames.count
self.progressVisual = self.animFrames[self.animIndex]
NotificationCenter.default.post(name: UpdaterService.didUpdateStateNotification, object: self)
}
RunLoop.main.add(self.animationTimer!, forMode: .common)
NotificationCenter.default.post(name: UpdaterService.didUpdateStateNotification, object: self)
}
}
private func updateStage(stage: String, visual: String? = nil) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.progressText = stage
if let v = visual { self.progressVisual = v }
NotificationCenter.default.post(name: UpdaterService.didUpdateStateNotification, object: self)
}
}
private func stopAnimation() {
DispatchQueue.main.async { [weak self] in
self?.animationTimer?.invalidate()
self?.animationTimer = nil
self?.isUpdating = false
NotificationCenter.default.post(name: UpdaterService.didUpdateStateNotification, object: self)
}
}
public func checkForUpdatesAndApply() {
guard !isUpdating else { return }
isUpdating = true
updateStatusMessage = "Проверка обновлений на GitHub..."
NotificationCenter.default.post(name: UpdaterService.didUpdateStateNotification, object: self)
startAnimation(initialStage: "Проверка")
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
let projectDir = "/Users/autli/Documents/blts_tracker"
self?.updateStage(stage: "Связь с GitHub", visual: "▰▰▱▱▱")
let updateScript = """
cd "\(projectDir)"
git -c http.timeout=8 fetch origin main 2>&1
@ -66,16 +112,29 @@ public final class UpdaterService: ObservableObject {
do {
try process.run()
// Advance visual stages during execution
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
if self?.isUpdating == true {
self?.updateStage(stage: "Загрузка", visual: "▰▰▰▱▱")
}
}
DispatchQueue.global().asyncAfter(deadline: .now() + 2.5) {
if self?.isUpdating == true {
self?.updateStage(stage: "Сборка", visual: "▰▰▰▰▱")
}
}
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8) ?? ""
DispatchQueue.main.async {
self?.isUpdating = false
NotificationCenter.default.post(name: UpdaterService.didUpdateStateNotification, object: self)
self?.stopAnimation()
if output.contains("SUCCESS") {
self?.updateStage(stage: "Готово", visual: "▰▰▰▰▰")
self?.sendNotification(
title: "🎉 Обновление установлено",
body: "BLTS Tracker успешно обновлён с GitHub! Перезапуск..."
@ -107,8 +166,7 @@ public final class UpdaterService: ObservableObject {
}
} catch {
DispatchQueue.main.async {
self?.isUpdating = false
NotificationCenter.default.post(name: UpdaterService.didUpdateStateNotification, object: self)
self?.stopAnimation()
self?.sendNotification(
title: "⚠️ Ошибка",
body: "Не удалось выполнить проверку: \(error.localizedDescription)"