import Foundation import IndieBackup import SwiftUI /// IndieBackup's app-specific wiring (indie-backup skill): what gets backed up, and how the /// app's in-memory board list is rebuilt after a restore. /// /// **Why this type is constructed rather than defaulted.** `BackupConfiguration.backupRoot` is a /// non-optional `URL`, but the ubiquity container resolves asynchronously and can fail outright /// (`CloudHomeResolver`, CloudHome.swift) — there is no root to hand IndieBackup until /// `BoardIndexStore.home` exists. Rather than answer with a placeholder path, this type is only /// ever constructed once a real `CloudHome` is in hand (`MobileApp`'s `.task(id:)`); before that /// there is no `BackupController` at all, and `SettingsTabView` renders a quiet disabled /// explanation instead of a section built on a fabricated root. final class MobileBackupConfiguration: BackupConfiguration { /// No dot. Shared by the Info.plist document-type/UTI registration /// (`dev.rzen.indie.kanban-backup`) and the archive filenames IndieBackup writes. static let fileExtension = "kanbanbackup" private let root: URL private let index: BoardIndexStore /// - Parameters: /// - root: The resolved `CloudHome.documentsURL` — the same `Documents/` folder every /// `.kanban` board lives directly inside. IndieBackup walks it file-by-file (it has no /// notion of "package"), so a board's directory structure round-trips through a backup /// as an ordinary tree of files — no directory registration or special-casing needed. /// - index: Rescanned after a restore replaces the tree, in `rebuildCacheAfterRestore`. init(root: URL, index: BoardIndexStore) { self.root = root self.index = index } var backupFileExtension: String { Self.fileExtension } var backupDisplayName: String { "Lanework Backup" } var backupRoot: URL { root } /// `BoardIndexStore` caches nothing durable — no Core Data, no SwiftData, no file on disk — /// only an in-memory `[BoardSummary]` scanned straight from the files under `backupRoot`. A /// restore already replaced that tree by the time this runs, so the only rebuild step is /// asking the store to rescan it; a `BoardSession` for a board that's open elsewhere reloads /// its own content from disk independently the next time it's asked (BoardIndexStore's doc /// comment on the observer flow). /// /// `refresh()` only *starts* the rescan (it hands off to a detached `Task` and returns), so /// this reports done as soon as the request lands, not once the boards tab has repainted — /// same as any other call to `refresh()` in this app (e.g. pull-to-refresh). func rebuildCacheAfterRestore(progress: @escaping (Double, String) -> Void) async throws { progress(0, "Rescanning boards…") await index.refresh() progress(1, "Rescanning boards…") } } /// Threads the lazily-constructed `BackupController` from `MobileApp` down to `SettingsTabView` /// (the backup section) and back up to `MobileApp`'s own `onOpenURL` handler (imported backup /// files), without forcing `RootTabView` — or anything else between them — to know backup exists. /// `nil` until the cloud home resolves; see `MobileBackupConfiguration`. private struct BackupControllerKey: EnvironmentKey { static let defaultValue: BackupController? = nil } extension EnvironmentValues { var backupController: BackupController? { get { self[BackupControllerKey.self] } set { self[BackupControllerKey.self] = newValue } } }