Neon-Vision-Editor/Neon Vision Editor/UI/AppUpdaterDialog.swift

303 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
/// MARK: - Types
struct AppUpdaterDialog: View {
@EnvironmentObject private var appUpdateManager: AppUpdateManager
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
@Environment(\.colorScheme) private var colorScheme
#if os(iOS)
@AppStorage("EnableTranslucentWindow") private var translucentWindow: Bool = true
#else
@AppStorage("EnableTranslucentWindow") private var translucentWindow: Bool = false
#endif
@AppStorage("SettingsLiquidGlassEnabled") private var liquidGlassEnabled: Bool = true
@Binding var isPresented: Bool
private var releaseTitle: String {
appUpdateManager.latestRelease?.title ?? "Latest Release"
}
private var shouldUsePanelGlass: Bool {
translucentWindow && liquidGlassEnabled && !reduceTransparency
}
var body: some View {
GlassSurface(
enabled: shouldUsePanelGlass,
material: colorScheme == .dark ? .regularMaterial : .ultraThinMaterial,
fallbackColor: Color.secondary.opacity(0.12),
shape: .rounded(16)
) {
VStack(spacing: 16) {
header
bodyContent
actionRow
}
.padding(20)
.frame(minWidth: 520, idealWidth: 560)
}
}
private var header: some View {
HStack(spacing: 14) {
Image(systemName: "arrow.triangle.2.circlepath.circle.fill")
.font(.system(size: 38, weight: .semibold))
.symbolRenderingMode(.palette)
.foregroundStyle(
LinearGradient(colors: [Color.blue, Color.cyan], startPoint: .topLeading, endPoint: .bottomTrailing),
Color.white.opacity(0.9)
)
VStack(alignment: .leading, spacing: 4) {
Text("Software Update")
.font(.title3.weight(.semibold))
Text("Checks GitHub releases for Neon Vision Editor updates.")
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
#if os(macOS)
Button("Show Installer Log") {
appUpdateManager.openUpdaterLog()
}
.buttonStyle(.bordered)
.accessibilityLabel("Show installer log")
.accessibilityHint("Opens the updater log file for troubleshooting")
#endif
}
.padding(14)
.background(Color.clear)
}
@ViewBuilder
private var bodyContent: some View {
switch appUpdateManager.status {
case .idle, .checking:
VStack(alignment: .leading, spacing: 12) {
liveUpdateStatusSection
if appUpdateManager.status == .checking {
Text("Current version: \(appUpdateManager.currentVersion)")
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
case .upToDate:
VStack(alignment: .leading, spacing: 10) {
Label("Youre up to date.", systemImage: "checkmark.circle.fill")
.font(.headline)
.foregroundStyle(.green)
Text("Current version: \(appUpdateManager.currentVersion)")
.font(.subheadline)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
case .failed:
VStack(alignment: .leading, spacing: 10) {
Label("Update check failed", systemImage: "exclamationmark.triangle.fill")
.font(.headline)
.foregroundStyle(.orange)
Text(appUpdateManager.errorMessage ?? "Unknown error")
.font(.subheadline)
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
case .updateAvailable:
VStack(alignment: .leading, spacing: 12) {
Label("\(releaseTitle) is available", systemImage: "sparkles")
.font(.headline)
Text("Current version: \(appUpdateManager.currentVersion) • New version: \(appUpdateManager.latestRelease?.version ?? "-")")
.font(.subheadline)
.foregroundStyle(.secondary)
if let date = appUpdateManager.latestRelease?.publishedAt {
Text("Published: \(date.formatted(date: .abbreviated, time: .omitted))")
.font(.caption)
.foregroundStyle(.secondary)
}
if let notes = appUpdateManager.latestRelease?.notes,
!notes.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
ScrollView {
Text(notes)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.footnote)
.textSelection(.enabled)
}
.frame(maxHeight: 180)
.padding(10)
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color.primary.opacity(0.05))
)
}
Text("Updates are delivered from GitHub release assets, not App Store updates.")
.font(.caption)
.foregroundStyle(.secondary)
if appUpdateManager.isUserVisibleUpdateInProgress {
liveUpdateStatusSection
}
if let installMessage = appUpdateManager.installMessage {
Text(installMessage)
.font(.caption)
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
if let reason = appUpdateManager.installNowDisabledReason {
Text(reason)
.font(.caption2)
.foregroundStyle(.orange)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
}
}
@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 {
switch appUpdateManager.status {
case .updateAvailable:
if appUpdateManager.awaitingInstallCompletionAction {
Button("Later") {
isPresented = false
}
Spacer()
#if os(macOS)
Button("Install and Close App") {
appUpdateManager.installAndCloseApp()
}
Button("Restart and Install") {
appUpdateManager.restartAndInstall()
}
.buttonStyle(.borderedProminent)
#else
Button("View Releases") {
appUpdateManager.openReleasePage()
}
#endif
} else {
Button("Skip This Version") {
appUpdateManager.skipCurrentVersion()
isPresented = false
}
Button("Remind Me Tomorrow") {
appUpdateManager.remindMeTomorrow()
isPresented = false
}
Spacer()
Button("View Releases") {
appUpdateManager.openReleasePage()
}
#if os(macOS)
Button("Install Update") {
Task {
await appUpdateManager.installUpdateNow()
}
}
.buttonStyle(.borderedProminent)
.disabled(appUpdateManager.isInstalling || !appUpdateManager.installNowSupported)
#endif
}
case .failed:
Button("Close") {
isPresented = false
}
Spacer()
Button("Try Again") {
Task {
await appUpdateManager.checkForUpdates(source: .manual)
}
}
.buttonStyle(.borderedProminent)
case .upToDate:
Button("Close") {
isPresented = false
}
Spacer()
Button("View Releases") {
appUpdateManager.openReleasePage()
}
Button("Check Again") {
Task {
await appUpdateManager.checkForUpdates(source: .manual)
}
}
.buttonStyle(.borderedProminent)
case .idle, .checking:
Spacer()
Button("Close") {
isPresented = false
}
.disabled(appUpdateManager.status == .checking)
}
}
}
}