Files
Outpost/Outpost/Features/About/AboutView.swift
T
Puranjay Savar Mattas 4769645489 feat(offline): write queue with sync-on-reconnect + settings redesign
Extends CachingOutlineAPIClient with a scoped offline write queue:
updateDocument, updateCollection, pin/unpin, star/unstar, and
subscribe/unsubscribe now apply optimistically and queue via a new
PendingOperation (SwiftData) when they fail (or when a new manual
"Offline Mode" toggle forces it), then replay on reconnect via
flushPendingOperations(). Same-target edits coalesce into one queued
operation; a pin/unpin pair that never syncs cancels out instead of
queuing a delete the server never saw. Actions that would invent new
tree structure (create/move/archive/delete/duplicate) stay live-only —
reconciling a locally-invented id against the server's real one is a
separate, harder problem this pass doesn't take on. Sharing,
permissions, search, and export also stay live-only.

Added a "Full Local Sync" toggle that eagerly walks and caches the
whole workspace instead of only what's been opened, running
immediately on enable and every 20 minutes after while online.

Settings moved from a popup (Settings {} scene / PreferencesView) to
a full-page view rendered inside the root window (AppNavigation),
including the ⌘, shortcut. Folds in offline/sync management (storage
size, clear cache, pending-sync list with per-item retry) and the
About window's content (version, check for updates) so it's all in
one place.

8 new OutlineKit tests covering coalescing, cancel-out, flush
success/failure, and manual offline mode — 51/51 passing.
2026-08-15 00:12:05 +01:00

90 lines
3.3 KiB
Swift

#if os(macOS)
import AppKit
import SwiftUI
/// Bare content (icon, name, version, links) with no window chrome — reused
/// by both the standalone "About Outpost" window (`AboutView`, the standard
/// macOS app-menu affordance) and the Settings page's own About section, so
/// the two can't drift out of sync.
struct AboutInfoView: View {
private let repositoryURL = URL(string: "https://git.psmattas.com/psmattas/Outpost")!
private let releasesURL = URL(string: "https://git.psmattas.com/psmattas/Outpost/releases")!
var appName: String {
Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "Outpost"
}
/// Bumped alongside `MARKETING_VERSION` in the Xcode project — kept out
/// of the bundle version itself since `CFBundleShortVersionString` is
/// expected to stay a plain dotted-numeric string, not `0.0.1-ALPHA`.
private let releaseStage = "ALPHA"
var versionString: String {
let shortVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.1"
let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "1"
let stageSuffix = releaseStage.isEmpty ? "" : "-\(releaseStage)"
return "Version \(shortVersion)\(stageSuffix) (\(buildNumber))"
}
private var copyrightYear: String {
String(Calendar.current.component(.year, from: Date()))
}
var body: some View {
VStack(spacing: 16) {
// `NSImage(named: "AppIcon")` doesn't reliably resolve an App
// Icon asset-catalog entry on macOS — `NSApp.applicationIconImage`
// is the API that's actually guaranteed to match what the Dock
// and Finder show for the running app.
Image(nsImage: NSApp.applicationIconImage)
.resizable()
.frame(width: 80, height: 80)
VStack(spacing: 4) {
Text(appName)
.font(.title2.bold())
Text(versionString)
.font(.caption)
.foregroundStyle(.secondary)
}
Text("A native Apple client for self-hosted Outline, built for full editing parity with the web app.")
.font(.callout)
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity)
VStack(spacing: 10) {
Link(destination: repositoryURL) {
Label("View Source on Git", systemImage: "link")
}
.font(.callout)
Button("Check for Updates…") {
checkForUpdates()
}
}
Text(\(copyrightYear) Puranjay Savar Mattas")
.font(.caption2)
.foregroundStyle(.tertiary)
}
}
// No Sparkle-style in-app updater yet — this just opens the releases page
// on the self-hosted Gitea instance so the user can check/download manually.
private func checkForUpdates() {
NSWorkspace.shared.open(releasesURL)
}
}
struct AboutView: View {
var body: some View {
AboutInfoView()
.padding(32)
.frame(width: 320)
}
}
#endif