diff --git a/Outpost/Features/Account/SettingsSidebarList.swift b/Outpost/Features/Account/SettingsSidebarList.swift index bb9392e..d855429 100644 --- a/Outpost/Features/Account/SettingsSidebarList.swift +++ b/Outpost/Features/Account/SettingsSidebarList.swift @@ -1,5 +1,6 @@ #if os(macOS) import SwiftUI +import OutlineKit /// Swapped into the real sidebar's content slot (search field, collections /// tree, account footer) while Settings is open — same sidebar, different @@ -8,13 +9,23 @@ import SwiftUI /// back. /// /// Grouped by `SettingsCategory` — `general` (ours) sits under an "Outpost" -/// header at the top, then Outline's own Account/Workspace/Integrations & -/// Installation groups, matching the settings page structure of the -/// Outline web app. +/// header at the top, then Outline's own Account/Workspace groups, matching +/// the settings page structure of the Outline web app. Outline's own server +/// version has no dedicated section (there used to be an Integrations & +/// Installation category for just that) — it's cheap enough to show +/// unconditionally in the footer here instead, alongside Outpost's own +/// version. struct SettingsSidebarList: View { @Binding var selection: SettingsSection? let onDone: () -> Void + @Environment(SessionStore.self) private var session + @State private var outlineVersion: String? + + private var outpostVersion: String { + Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.1" + } + var body: some View { VStack(spacing: 0) { HStack { @@ -46,12 +57,36 @@ struct SettingsSidebarList: View { Divider() + versionFooter + + Divider() + Button("Done", action: onDone) .keyboardShortcut(.cancelAction) .buttonStyle(.borderedProminent) .frame(maxWidth: .infinity) .padding(12) } + .task { await refreshOutlineVersion() } + } + + private var versionFooter: some View { + VStack(alignment: .leading, spacing: 2) { + Text("Outpost \(outpostVersion)") + if let outlineVersion { + Text("Outline \(outlineVersion)") + } + } + .font(.caption2) + .foregroundStyle(.tertiary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16) + .padding(.vertical, 8) + } + + private func refreshOutlineVersion() async { + guard let apiClient = session.apiClient else { return } + outlineVersion = try? await apiClient.installationInfo().version } } #endif diff --git a/Outpost/Features/Account/SettingsView.swift b/Outpost/Features/Account/SettingsView.swift index 48266ae..5de1f70 100644 --- a/Outpost/Features/Account/SettingsView.swift +++ b/Outpost/Features/Account/SettingsView.swift @@ -44,9 +44,6 @@ 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 @@ -90,7 +87,6 @@ struct SettingsView: View { case .notifications: notificationsDetail case .passkeys: passkeysDetail case .apiAccess: apiAccessDetail - case .installation: installationDetail case .offlineSync: offlineSyncDetail case .advanced: advancedDetail case .about: aboutDetail @@ -98,9 +94,8 @@ struct SettingsView: View { } } - /// Everything in Account/Workspace/Integrations & Installation that - /// isn't `.profile` — real content lands section by section; this is - /// just the nav skeleton until then. + /// Everything in Account/Workspace that isn't `.profile` — real content + /// lands section by section; this is just the nav skeleton until then. private var comingSoonDetail: some View { VStack(spacing: 16) { sectionHeader @@ -824,55 +819,6 @@ 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 { diff --git a/Outpost/Root/AppNavigation.swift b/Outpost/Root/AppNavigation.swift index 575bf5a..ce30d06 100644 --- a/Outpost/Root/AppNavigation.swift +++ b/Outpost/Root/AppNavigation.swift @@ -8,7 +8,6 @@ enum SettingsCategory: String, CaseIterable, Identifiable { case general case account case workspace - case integrationsInstallation var id: String { rawValue } @@ -17,19 +16,20 @@ enum SettingsCategory: String, CaseIterable, Identifiable { case .general: return "Outpost" case .account: return "Account" case .workspace: return "Workspace" - case .integrationsInstallation: return "Integrations & Installation" } } } /// One entry in the Settings sidebar. Mirrors Outline's own settings -/// categories (Account/Workspace/Integrations & Installation) so this app's -/// settings read as a native counterpart to the web app's, plus a `general` -/// group for things that are ours and don't map onto Outline's structure -/// (offline/sync, advanced, about, appearance). +/// categories (Account/Workspace) so this app's settings read as a native +/// counterpart to the web app's, plus a `general` group for things that are +/// ours and don't map onto Outline's structure (offline/sync, advanced, +/// about, appearance). Outline's own version info moved to the sidebar +/// footer (`SettingsSidebarList`) instead of a standalone +/// Integrations & Installation section. /// -/// Most of the Account/Workspace/Integrations cases are navigation-only for -/// now — `SettingsView` renders a "Coming Soon" placeholder for anything not +/// Most of the Account/Workspace cases are navigation-only for now — +/// `SettingsView` renders a "Coming Soon" placeholder for anything not /// explicitly built yet. Content lands section by section. enum SettingsSection: String, CaseIterable, Identifiable, Hashable { // General (ours) @@ -41,9 +41,6 @@ enum SettingsSection: String, CaseIterable, Identifiable, Hashable { // Workspace case details, authentication, security, ai, members, groups, templates, emojis, applications, shared, links, webhooks, importData, exportData - // Integrations & Installation - case installation - var id: String { rawValue } var category: SettingsCategory { @@ -54,8 +51,6 @@ enum SettingsSection: String, CaseIterable, Identifiable, Hashable { return .account case .details, .authentication, .security, .ai, .members, .groups, .templates, .emojis, .applications, .shared, .links, .webhooks, .importData, .exportData: return .workspace - case .installation: - return .integrationsInstallation } } @@ -84,7 +79,6 @@ enum SettingsSection: String, CaseIterable, Identifiable, Hashable { case .webhooks: return "Webhooks" case .importData: return "Import" case .exportData: return "Export" - case .installation: return "Installation" } } @@ -113,16 +107,15 @@ enum SettingsSection: String, CaseIterable, Identifiable, Hashable { case .webhooks: return "bolt.horizontal" case .importData: return "square.and.arrow.down" case .exportData: return "square.and.arrow.up" - case .installation: return "shippingbox" } } /// Everything actually built so far — everything else in Account/ - /// Workspace/Integrations & Installation renders a "Coming Soon" - /// placeholder until its content is specified and built. + /// Workspace renders a "Coming Soon" placeholder until its content is + /// specified and built. var isImplemented: Bool { switch self { - case .appearance, .offlineSync, .advanced, .about, .profile, .preferences, .notifications, .passkeys, .apiAccess, .installation: + case .appearance, .offlineSync, .advanced, .about, .profile, .preferences, .notifications, .passkeys, .apiAccess: return true default: return false