diff --git a/Outpost/Features/Account/SettingsView.swift b/Outpost/Features/Account/SettingsView.swift index 938246e..48266ae 100644 --- a/Outpost/Features/Account/SettingsView.swift +++ b/Outpost/Features/Account/SettingsView.swift @@ -44,6 +44,9 @@ struct SettingsView: View { @State private var apiKeys: [OutlineAPIKey] = [] @State private var isLoadingApiKeys = false @State private var apiKeysErrorMessage: String? + @State private var installationInfo: OutlineInstallationInfo? + @State private var isLoadingInstallationInfo = false + @State private var installationInfoErrorMessage: String? /// Full Local Sync and cache-clearing both need a real connection to be /// safe — clearing while offline (or letting Full Local Sync think it @@ -87,6 +90,7 @@ struct SettingsView: View { case .notifications: notificationsDetail case .passkeys: passkeysDetail case .apiAccess: apiAccessDetail + case .installation: installationDetail case .offlineSync: offlineSyncDetail case .advanced: advancedDetail case .about: aboutDetail @@ -820,6 +824,55 @@ struct SettingsView: View { } } + // MARK: - Installation + + private var installationDetail: some View { + VStack(alignment: .leading, spacing: 16) { + sectionHeader + + if isLoadingInstallationInfo && installationInfo == nil { + ProgressView().controlSize(.small) + } else if let installationInfoErrorMessage { + Text(installationInfoErrorMessage) + .font(.caption) + .foregroundStyle(.red) + } else if let installationInfo { + VStack(alignment: .leading, spacing: 6) { + labeledRow("Version", installationInfo.version) + Divider() + if installationInfo.versionsBehind > 0 { + labeledRow("Latest Version", installationInfo.latestVersion) + Divider() + Label( + "\(installationInfo.versionsBehind) version\(installationInfo.versionsBehind == 1 ? "" : "s") behind", + systemImage: "exclamationmark.triangle.fill" + ) + .font(.caption) + .foregroundStyle(.orange) + } else { + Label("Up to date", systemImage: "checkmark.circle.fill") + .font(.caption) + .foregroundStyle(.green) + } + } + .frame(maxWidth: 420, alignment: .leading) + } + } + .task { await refreshInstallationInfo() } + } + + private func refreshInstallationInfo() async { + guard let apiClient = session.apiClient else { return } + isLoadingInstallationInfo = true + defer { isLoadingInstallationInfo = false } + do { + installationInfo = try await apiClient.installationInfo() + installationInfoErrorMessage = nil + } catch { + installationInfoErrorMessage = outlineErrorMessage(error, fallback: "Couldn't load installation info.") + } + } + // MARK: - Offline & Sync private var offlineSyncDetail: some View {