From 7710a300fc43908bbcd334f2d765c6b29f9a5a17 Mon Sep 17 00:00:00 2001 From: Autli Date: Tue, 6 Oct 2026 12:59:35 +0500 Subject: [PATCH] feat: show animated installation progress bar in menu bar during update --- Sources/blts_tracker/BLTSTrackerApp.swift | 7 ++ .../Services/UpdaterService.swift | 68 +++++++++++++++++-- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/Sources/blts_tracker/BLTSTrackerApp.swift b/Sources/blts_tracker/BLTSTrackerApp.swift index e36cc89..6224123 100644 --- a/Sources/blts_tracker/BLTSTrackerApp.swift +++ b/Sources/blts_tracker/BLTSTrackerApp.swift @@ -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 diff --git a/Sources/blts_tracker/Services/UpdaterService.swift b/Sources/blts_tracker/Services/UpdaterService.swift index 6a9c48b..54a81cd 100644 --- a/Sources/blts_tracker/Services/UpdaterService.swift +++ b/Sources/blts_tracker/Services/UpdaterService.swift @@ -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)"