AutoBackupCoordinator (new):
- @MainActor singleton that owns all auto-backup decisions
- Phase model: idle / checking / autoBackingUp / disconnected
- idle: nothing pending or destination not set
- checking: reconciliation in progress before decision
- autoBackingUp: coordinator-triggered BackupEngine run active
- disconnected: NAS offline, pending count queued for retry
- Combine subscription on BackupStatusService.$isRefreshing fires
handleReconciliationComplete() on every reconcile completion — the
single entry point for all auto-backup decisions
- Combine subscription on LANMonitor.$nasReachable fires
handleNASReachable() which retries only when phase == .disconnected
- Guards: autoBackupEnabled, quiet hours, chargingOnlyMode + battery state
- canAutoBackup also blocks when engine.job.status.isActive to prevent
double-start from concurrent photo library change + foreground events
BackupView:
- .task + .onChange(of: scenePhase) now call coordinator.onActive() instead
of statusService.refresh() directly — coordinator decides whether to also
start a backup after reconciliation completes
- ringContentID includes coordinator phase suffix → ring animates on
checking/disconnected transitions
- ringCenterContent case 0: when engine is idle, shows:
- disconnectedRingView if phase == .disconnected and needBackup > 0
(wifi.slash icon, "Waiting for NAS", queued count)
- checkingRingView if phase == .checking (spinner, "Checking…")
- existing ringMainView otherwise
- ringColor: dim orange (0.35 opacity) when disconnected with pending items
- Pull-to-refresh reconciliation triggers auto-backup via Combine chain
ConnectionStore:
- autoBackupEnabled: Bool, default true, UserDefaults-persisted
SettingsView:
- AUTO BACKUP section at top with autoBackupEnabled toggle
BackgroundTaskManager:
- BGProcessingTask: quiet-hours guard, requiresExternalPower mirrors
chargingOnlyMode, expiration handler calls engine.cancel() for clean exit
- BGAppRefreshTask: lightweight refresh only (no upload in background refresh)
- Both tasks reschedule themselves before doing any work
AppDelegate:
- UIDevice.current.isBatteryMonitoringEnabled = true for battery checks
NASBackupApp:
- AutoBackupCoordinator.shared injected as @StateObject + environmentObject
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
54 lines
1.9 KiB
Swift
54 lines
1.9 KiB
Swift
import SwiftUI
|
|
import Photos
|
|
|
|
@main
|
|
struct NASBackupApp: App {
|
|
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
|
|
|
@StateObject private var store = ConnectionStore.shared
|
|
@StateObject private var engine = BackupEngine.shared
|
|
@StateObject private var lanMonitor = LANMonitor.shared
|
|
@StateObject private var statusService = BackupStatusService.shared
|
|
@StateObject private var coordinator = AutoBackupCoordinator.shared
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
RootView()
|
|
.environmentObject(store)
|
|
.environmentObject(engine)
|
|
.environmentObject(lanMonitor)
|
|
.environmentObject(statusService)
|
|
.environmentObject(coordinator)
|
|
.preferredColorScheme(store.appearanceMode.resolvedScheme)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct RootView: View {
|
|
@EnvironmentObject var store: ConnectionStore
|
|
@EnvironmentObject var lanMonitor: LANMonitor
|
|
|
|
var body: some View {
|
|
Group {
|
|
if store.savedConnection == nil {
|
|
ConnectView()
|
|
} else if !store.isSessionActive {
|
|
LoginView()
|
|
} else if store.savedConnection?.remotePath == "/" {
|
|
FolderSetupView()
|
|
} else if !store.onboardingPhotoShown {
|
|
PhotoPermissionView()
|
|
} else {
|
|
MainTabView()
|
|
}
|
|
}
|
|
.animation(.spring(response: 0.4, dampingFraction: 0.85), value: store.isSessionActive)
|
|
.animation(.spring(response: 0.4, dampingFraction: 0.85), value: store.savedConnection?.remotePath)
|
|
.animation(.spring(response: 0.4, dampingFraction: 0.85), value: store.onboardingPhotoShown)
|
|
.task(id: store.isSessionActive) {
|
|
guard store.isSessionActive, let conn = store.savedConnection else { return }
|
|
await lanMonitor.checkNASReachability(host: conn.host, port: conn.port)
|
|
}
|
|
}
|
|
}
|