feat(settings): Installation section

Server version + up-to-date/behind indicator via installation.info.
This commit is contained in:
2026-08-18 14:20:26 +01:00
parent dc780e420f
commit adfa477ecf
@@ -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 {