From f588bf717b1eb8bb00f837a31bbe5eb3df11bbe4 Mon Sep 17 00:00:00 2001 From: h3p Date: Mon, 30 Mar 2026 20:38:51 +0200 Subject: [PATCH] Make updater progress visible during install --- Neon Vision Editor.xcodeproj/project.pbxproj | 4 +- .../Core/AppUpdateManager.swift | 30 ++++++++- Neon Vision Editor/UI/AppUpdaterDialog.swift | 65 +++++++++++++------ Neon Vision Editor/UI/ContentView.swift | 38 +++++++---- Neon Vision Editor/UI/NeonSettingsView.swift | 41 ++++++++++++ 5 files changed, 143 insertions(+), 35 deletions(-) diff --git a/Neon Vision Editor.xcodeproj/project.pbxproj b/Neon Vision Editor.xcodeproj/project.pbxproj index 3955561..d7ace5a 100644 --- a/Neon Vision Editor.xcodeproj/project.pbxproj +++ b/Neon Vision Editor.xcodeproj/project.pbxproj @@ -361,7 +361,7 @@ CODE_SIGNING_ALLOWED = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 594; + CURRENT_PROJECT_VERSION = 595; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = CS727NF72U; ENABLE_APP_SANDBOX = YES; @@ -444,7 +444,7 @@ CODE_SIGNING_ALLOWED = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 594; + CURRENT_PROJECT_VERSION = 595; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = CS727NF72U; ENABLE_APP_SANDBOX = YES; diff --git a/Neon Vision Editor/Core/AppUpdateManager.swift b/Neon Vision Editor/Core/AppUpdateManager.swift index 899c2a5..4ab7151 100644 --- a/Neon Vision Editor/Core/AppUpdateManager.swift +++ b/Neon Vision Editor/Core/AppUpdateManager.swift @@ -272,8 +272,7 @@ final class AppUpdateManager: ObservableObject { automaticPromptToken &+= 1 } - if source == .automatic, - autoDownloadEnabled, + if autoDownloadEnabled, installNowSupported { Task { [weak self] in await self?.attemptAutoInstall(interactive: false) @@ -447,6 +446,33 @@ final class AppUpdateManager: ObservableObject { installNowDisabledReason == nil } + var isUserVisibleUpdateInProgress: Bool { + status == .checking || isInstalling || awaitingInstallCompletionAction + } + + var userVisibleUpdateStatusTitle: String { + if status == .checking { + return "Checking for updates…" + } + if isInstalling { + return installPhase.isEmpty ? "Installing update…" : installPhase + } + if awaitingInstallCompletionAction { + return "Update ready to install" + } + return lastCheckResultSummary + } + + var userVisibleUpdateStatusDetail: String? { + if status == .checking { + return "Current version: \(currentVersion)" + } + if awaitingInstallCompletionAction { + return installMessage ?? "The update is staged and will install after the app closes." + } + return installMessage + } + var installNowDisabledReason: String? { guard ReleaseRuntimePolicy.isUpdaterEnabledForCurrentDistribution else { return "Updater is disabled for this distribution channel." diff --git a/Neon Vision Editor/UI/AppUpdaterDialog.swift b/Neon Vision Editor/UI/AppUpdaterDialog.swift index 7ee2ff6..af37d60 100644 --- a/Neon Vision Editor/UI/AppUpdaterDialog.swift +++ b/Neon Vision Editor/UI/AppUpdaterDialog.swift @@ -77,12 +77,12 @@ struct AppUpdaterDialog: View { switch appUpdateManager.status { case .idle, .checking: VStack(alignment: .leading, spacing: 12) { - ProgressView() - Text("Checking for updates…") - .font(.headline) - Text("Current version: \(appUpdateManager.currentVersion)") - .font(.subheadline) - .foregroundStyle(.secondary) + liveUpdateStatusSection + if appUpdateManager.status == .checking { + Text("Current version: \(appUpdateManager.currentVersion)") + .font(.subheadline) + .foregroundStyle(.secondary) + } } .frame(maxWidth: .infinity, alignment: .leading) .padding(12) @@ -146,19 +146,8 @@ struct AppUpdaterDialog: View { .font(.caption) .foregroundStyle(.secondary) - if appUpdateManager.isInstalling { - VStack(alignment: .leading, spacing: 6) { - ProgressView(value: appUpdateManager.installProgress, total: 1.0) { - Text(appUpdateManager.installPhase.isEmpty ? "Installing update…" : appUpdateManager.installPhase) - .font(.caption) - } - Text("\(Int((appUpdateManager.installProgress * 100).rounded()))%") - .font(.caption2) - .foregroundStyle(.secondary) - } - .accessibilityElement(children: .combine) - .accessibilityLabel("Update install progress") - .accessibilityValue("\(Int((appUpdateManager.installProgress * 100).rounded())) percent") + if appUpdateManager.isUserVisibleUpdateInProgress { + liveUpdateStatusSection } if let installMessage = appUpdateManager.installMessage { @@ -179,6 +168,44 @@ struct AppUpdaterDialog: View { } } + @ViewBuilder + private var liveUpdateStatusSection: some View { + VStack(alignment: .leading, spacing: 6) { + if appUpdateManager.isInstalling { + ProgressView(value: appUpdateManager.installProgress, total: 1.0) { + Text(appUpdateManager.userVisibleUpdateStatusTitle) + .font(.caption) + } + Text("\(Int((appUpdateManager.installProgress * 100).rounded()))%") + .font(.caption2) + .foregroundStyle(.secondary) + } else { + ProgressView { + Text(appUpdateManager.userVisibleUpdateStatusTitle) + .font(.headline) + } + } + + if let detail = appUpdateManager.userVisibleUpdateStatusDetail, + !detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + Text(detail) + .font(.caption) + .foregroundStyle(.secondary) + .textSelection(.enabled) + } + } + .accessibilityElement(children: .combine) + .accessibilityLabel("Update status") + .accessibilityValue(accessibilityProgressValue) + } + + private var accessibilityProgressValue: String { + if appUpdateManager.isInstalling { + return "\(Int((appUpdateManager.installProgress * 100).rounded())) percent" + } + return appUpdateManager.userVisibleUpdateStatusTitle + } + @ViewBuilder private var actionRow: some View { HStack { diff --git a/Neon Vision Editor/UI/ContentView.swift b/Neon Vision Editor/UI/ContentView.swift index 17c5254..18d3c87 100644 --- a/Neon Vision Editor/UI/ContentView.swift +++ b/Neon Vision Editor/UI/ContentView.swift @@ -2153,7 +2153,7 @@ struct ContentView: View { } private var rootViewWithStateObservers: some View { - basePlatformRootView + applyUpdateVisibilityObservers(to: basePlatformRootView) .onAppear { handleSettingsAndEditorDefaultsOnAppear() } @@ -2163,23 +2163,12 @@ struct ContentView: View { viewModel.isLineWrapEnabled = target } } - .onReceive(NotificationCenter.default.publisher(for: .whitespaceScalarInspectionResult)) { notif in - guard matchesCurrentWindow(notif) else { return } - if let msg = notif.userInfo?[EditorCommandUserInfo.inspectionMessage] as? String { - whitespaceInspectorMessage = msg - } - } .onChange(of: viewModel.isLineWrapEnabled) { _, enabled in guard projectOverrideLineWrapEnabled == nil else { return } if settingsLineWrapEnabled != enabled { settingsLineWrapEnabled = enabled } } - .onChange(of: appUpdateManager.automaticPromptToken) { _, _ in - if appUpdateManager.consumeAutomaticPromptIfNeeded() { - showUpdaterDialog(checkNow: false) - } - } .onChange(of: settingsThemeName) { _, _ in scheduleHighlightRefresh() } @@ -2214,6 +2203,31 @@ struct ContentView: View { .onChange(of: showMarkdownPreviewPane) { _, _ in persistSessionIfReady() } + } + + private func applyUpdateVisibilityObservers(to view: Content) -> some View { + view + .onReceive(NotificationCenter.default.publisher(for: .whitespaceScalarInspectionResult)) { notif in + guard matchesCurrentWindow(notif) else { return } + if let msg = notif.userInfo?[EditorCommandUserInfo.inspectionMessage] as? String { + whitespaceInspectorMessage = msg + } + } + .onChange(of: appUpdateManager.automaticPromptToken) { _, _ in + if appUpdateManager.consumeAutomaticPromptIfNeeded() { + showUpdaterDialog(checkNow: false) + } + } + .onChange(of: appUpdateManager.isInstalling) { _, isInstalling in + if isInstalling && !showUpdateDialog { + showUpdaterDialog(checkNow: false) + } + } + .onChange(of: appUpdateManager.awaitingInstallCompletionAction) { _, awaitingAction in + if awaitingAction && !showUpdateDialog { + showUpdaterDialog(checkNow: false) + } + } } private var rootViewWithPlatformLifecycleObservers: some View { diff --git a/Neon Vision Editor/UI/NeonSettingsView.swift b/Neon Vision Editor/UI/NeonSettingsView.swift index 4f68db6..3cbb70e 100644 --- a/Neon Vision Editor/UI/NeonSettingsView.swift +++ b/Neon Vision Editor/UI/NeonSettingsView.swift @@ -3249,6 +3249,47 @@ struct NeonSettingsView: View { } } + if appUpdateManager.isUserVisibleUpdateInProgress { + VStack(alignment: .leading, spacing: UI.space8) { + Text("Update Activity") + .font(.subheadline.weight(.semibold)) + if appUpdateManager.isInstalling { + ProgressView(value: appUpdateManager.installProgress, total: 1.0) { + Text(appUpdateManager.userVisibleUpdateStatusTitle) + .font(Typography.footnote) + } + Text("\(Int((appUpdateManager.installProgress * 100).rounded()))%") + .font(Typography.footnote) + .foregroundStyle(.secondary) + } else { + ProgressView { + Text(appUpdateManager.userVisibleUpdateStatusTitle) + .font(Typography.footnote) + } + } + if let detail = appUpdateManager.userVisibleUpdateStatusDetail, + !detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + Text(detail) + .font(Typography.footnote) + .foregroundStyle(.secondary) + .textSelection(.enabled) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(UI.space12) + .background( + RoundedRectangle(cornerRadius: UI.cardCorner, style: .continuous) + .fill(Color.secondary.opacity(0.08)) + ) + .accessibilityElement(children: .combine) + .accessibilityLabel("Update activity") + .accessibilityValue( + appUpdateManager.isInstalling + ? "\(Int((appUpdateManager.installProgress * 100).rounded())) percent" + : appUpdateManager.userVisibleUpdateStatusTitle + ) + } + Text("Uses GitHub release assets only. App Store Connect releases are not used by this updater.") .font(Typography.footnote) .foregroundStyle(.secondary)