feat: automatic backup on launch, foreground, and photo library change
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>
This commit is contained in:
@@ -63,6 +63,11 @@ final class ConnectionStore: ObservableObject {
|
||||
didSet { UserDefaults.standard.set(localRetentionDays, forKey: "localRetentionDays") }
|
||||
}
|
||||
|
||||
// Auto-backup: default on after first destination is configured
|
||||
@Published var autoBackupEnabled: Bool {
|
||||
didSet { UserDefaults.standard.set(autoBackupEnabled, forKey: "autoBackupEnabled") }
|
||||
}
|
||||
|
||||
// Cellular & remote access
|
||||
@Published var allowCellularBackup: Bool {
|
||||
didSet { UserDefaults.standard.set(allowCellularBackup, forKey: "allowCellularBackup") }
|
||||
@@ -89,6 +94,7 @@ final class ConnectionStore: ObservableObject {
|
||||
onboardingPhotoShown = UserDefaults.standard.bool(forKey: "onboardingPhotoShown")
|
||||
appearanceMode = AppearanceMode(rawValue: UserDefaults.standard.string(forKey: "appearanceMode") ?? "") ?? .system
|
||||
localRetentionDays = UserDefaults.standard.object(forKey: "localRetentionDays") as? Int ?? 0
|
||||
autoBackupEnabled = UserDefaults.standard.object(forKey: "autoBackupEnabled") as? Bool ?? true
|
||||
allowCellularBackup = UserDefaults.standard.object(forKey: "allowCellularBackup") as? Bool ?? false
|
||||
useTailscaleWhenRemote = UserDefaults.standard.object(forKey: "useTailscaleWhenRemote") as? Bool ?? false
|
||||
tailscaleHost = UserDefaults.standard.string(forKey: "tailscaleHost") ?? ""
|
||||
|
||||
Reference in New Issue
Block a user