A trailing swipe on a board row opens Board Settings, whose first setting is location: iCloud or Local, with a confirmed move to the other side — destructive-styled only outbound, because leaving iCloud is the direction that sheds protection. The move is setUbiquitous against the real container and a coordinated move under the DEBUG stand-in; evacuation sweeps materialization first and refuses honestly while content is still downloading. The local home is the sandbox Documents folder, published to the Files app, so a local board is still a folder the user owns. With a second home the iCloud wall softens (user-ruled 2026-08-08): the index always reaches ready, cloud unavailability becomes an inline notice with a retry, creates land locally when there is no account, and LANEWORK_FORCE_NO_ICLOUD makes that state reproducible in tests regardless of the machine's sign-in. Known gap, now user-reachable: backup remains iCloud-only, so local boards sit outside it. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
63 lines
2.7 KiB
Swift
63 lines
2.7 KiB
Swift
import SwiftUI
|
|
import IndieAbout
|
|
import IndieBackup
|
|
|
|
/// Backup/restore (indie-backup skill) and the About section (indie-about skill).
|
|
struct SettingsTabView: View {
|
|
@Environment(BoardIndexStore.self) private var index
|
|
@Environment(\.backupController) private var backupController
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
backupSection
|
|
|
|
// Last section by convention. The version label doubles as the changelog link,
|
|
// which is why the changelog sits in its dedicated slot and not in `documents`.
|
|
// Same voice as the Mac app's `AboutBox.configuration`; the changelog itself is
|
|
// the phone app's own (see project.yml — separate product, separate 1.0).
|
|
Section {
|
|
IndieAbout(configuration: .init(
|
|
copyrightText: "© 2026 rzen",
|
|
documents: [.license(extension: "md")],
|
|
changelogDocument: .changelog()
|
|
))
|
|
}
|
|
}
|
|
.navigationTitle("Settings")
|
|
}
|
|
// Idempotent (BoardIndexStore.start()), and harmless if BoardsTabView already called it —
|
|
// this tab can be the first one the user opens, and `backupController` only ever appears
|
|
// once `index.home` resolves.
|
|
.task { index.start() }
|
|
}
|
|
|
|
/// `backupController` is `nil` until `MobileApp` has a resolved `CloudHome` to build one from
|
|
/// — which is also exactly the condition under which there is a `Documents/` folder to back
|
|
/// up. Everything short of that gets the same quiet explanation rather than a section that
|
|
/// looks broken or, worse, a crash on a nil root.
|
|
@ViewBuilder
|
|
private var backupSection: some View {
|
|
if let backupController {
|
|
BackupsSectionView(controller: backupController)
|
|
} else {
|
|
Section {
|
|
switch index.phase {
|
|
case .idle, .loading:
|
|
LabeledContent("Backup") {
|
|
ProgressView()
|
|
}
|
|
case .ready:
|
|
// `.ready` is both cases now: the run with no iCloud at all, and the runloop turn
|
|
// `backupController` lags `index.home` by (MobileApp's `.task(id:)`). Backup is
|
|
// still iCloud-only — a known gap now that local boards exist.
|
|
Label("Backup and restore need iCloud Drive.", systemImage: "icloud.slash")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
} footer: {
|
|
Text("Sign into iCloud in Settings, then come back here to back up your boards.")
|
|
}
|
|
}
|
|
}
|
|
}
|