import IndieAbout import SwiftData import SwiftUI @main struct NotesApp: App { #if os(iOS) @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate #endif @State private var appServices: AppServices? @State private var pendingBackupURL: URL? @Environment(\.scenePhase) private var scenePhase var body: some Scene { WindowGroup { Group { if let appServices { ContentView() .modelContainer(appServices.modelContainer) .environment(appServices.syncEngine) .environment(appServices.contextService) .environment(appServices.audioRecorder) .environment(appServices.transcriptionSettings) .environment(appServices.transcriptionService) .environmentObject(appServices.backupController) } else { ProgressView() } } .task { if appServices == nil { let services = await AppServices() appServices = services #if os(iOS) // Hand the (already-activated) watch receiver the live sync // engine so a delivery arriving now drains immediately, and // route ingest acks back so the watch can free its copies. let receiver = appDelegate.watchReceiver receiver.syncEngine = services.syncEngine services.syncEngine.onWatchRecordingHandled = { [weak receiver] id in receiver?.acknowledgeIngest(id: id) } #endif await services.startDeferredServices() if let pending = pendingBackupURL { pendingBackupURL = nil restore(pending, with: services) } } } .onOpenURL { url in handle(url: url) } .onChange(of: scenePhase) { _, phase in guard phase == .active, let appServices else { return } let syncEngine = appServices.syncEngine let contextService = appServices.contextService Task { // Fresh context whenever the user comes back to the app — // recall ranking should reflect where they are right now. await contextService.refresh() } Task { switch syncEngine.iCloudStatus { case .unavailable: // The user typically fixes iCloud in Settings and // switches straight back — retry on foreground. await syncEngine.connect() case .available: // Pull in records that synced down (and drop ones // deleted elsewhere) while the app was backgrounded. // Skip during a restore — it stops the monitors and // rebuilds the cache itself; a reconcile racing the // file swap would import a half-restored state. if !appServices.backupController.isRestoring { await syncEngine.reconcile() // Ingest any watch recordings staged since we last // looked (delivered while backgrounded). await syncEngine.ingestStagedWatchRecordings() } case .checking: break } } } } #if os(macOS) .defaultSize(width: 900, height: 700) .commands { IndieAboutCommand(configuration: .init( documents: [ .license(filename: "LICENSE", extension: "md"), .custom(title: "Changelog", filename: "CHANGELOG", extension: "md") ] )) } #endif } /// A tapped `.notesbackup` file (Files, AirDrop, Mail) restores the /// backup. If it arrives before services finish launching, it's queued. private func handle(url: URL) { guard url.isFileURL, url.pathExtension == "notesbackup" else { return } if let appServices { restore(url, with: appServices) } else { pendingBackupURL = url } } private func restore(_ url: URL, with services: AppServices) { let didScope = url.startAccessingSecurityScopedResource() Task { defer { if didScope { url.stopAccessingSecurityScopedResource() } } try? await services.backupController.restoreBackup(from: url) } } }