From 78a6b377f238413205d6bf0c6b112449874bf87c Mon Sep 17 00:00:00 2001 From: Robin Kutesa Date: Mon, 18 May 2026 21:12:35 +0300 Subject: [PATCH] feat: move check status below ring, remove checking spinner, re-enable auto-backup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move "X sec ago / refresh" row from inside statsStrip to directly below the ring hero so the last-checked time is visible near the status it describes - Remove "Checking status…" spinner from the action button — button shows "Start backup" / "Back up again" at all times - Remove "Checking…" spinner from the ring center (page 0) — ring always shows the pending backup count or "All photos safe" - Re-enable autoBackupEnabled default (true) now that already-safe counting is accurate; auto backup will fire on LAN join when items need backup Co-Authored-By: Kutesir Co-Authored-By: Sentry --- Features/Backup/BackupView.swift | 147 +++++++++++------------ Shared/Persistence/ConnectionStore.swift | 2 +- 2 files changed, 69 insertions(+), 80 deletions(-) diff --git a/Features/Backup/BackupView.swift b/Features/Backup/BackupView.swift index 170e02c..582c3e4 100644 --- a/Features/Backup/BackupView.swift +++ b/Features/Backup/BackupView.swift @@ -54,7 +54,12 @@ struct BackupView: View { // ─── Ring ───────────────────────────────────── ringHero - .padding(.bottom, 44) + .padding(.bottom, 12) + + // ─── Check status ───────────────────────────── + checkStatusRow + .padding(.horizontal, AppTheme.hPad) + .padding(.bottom, 20) // ─── Stats ──────────────────────────────────── statsStrip @@ -229,14 +234,10 @@ struct BackupView: View { private var ringCenterContent: some View { switch ringPage { case 0: - // Engine activity always takes priority. - // Coordinator states are shown only when the engine is idle. if engine.job.status.isActive { ringMainView } else if coordinator.phase == .disconnected, statusService.snapshot.needBackup > 0 { disconnectedRingView - } else if coordinator.phase == .checking { - checkingRingView } else { ringMainView } @@ -487,42 +488,43 @@ struct BackupView: View { .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) pageIndicator - - // Subtle refresh status - HStack(spacing: 5) { - if statusService.isRefreshing { - ProgressView() - .scaleEffect(0.5) - .tint(AppTheme.inkQuaternary) - Text("Checking…") - .font(.system(size: 10, weight: .regular)) - .foregroundStyle(AppTheme.inkQuaternary) - } else if let checked = statusService.snapshot.lastCheckedAt { - Image(systemName: statusService.snapshot.connectionState == .offline - ? "wifi.slash" : "checkmark.circle") - .font(.system(size: 9, weight: .regular)) - .foregroundStyle(statusService.snapshot.connectionState == .offline - ? .orange : AppTheme.inkQuaternary) - Text(checked, style: .relative) - .font(.system(size: 10, weight: .regular)) - .foregroundColor(AppTheme.inkQuaternary) - + Text(" ago") - .font(.system(size: 10, weight: .regular)) - .foregroundColor(AppTheme.inkQuaternary) - } - Spacer() - Button { - statusService.refresh(force: true) - } label: { - Image(systemName: "arrow.clockwise") - .font(.system(size: 11, weight: .regular)) - .foregroundStyle(AppTheme.inkQuaternary) - } - } - .frame(height: 16) } } + private var checkStatusRow: some View { + HStack(spacing: 5) { + if statusService.isRefreshing { + ProgressView() + .scaleEffect(0.5) + .tint(AppTheme.inkQuaternary) + Text("Checking…") + .font(.system(size: 10, weight: .regular)) + .foregroundStyle(AppTheme.inkQuaternary) + } else if let checked = statusService.snapshot.lastCheckedAt { + Image(systemName: statusService.snapshot.connectionState == .offline + ? "wifi.slash" : "checkmark.circle") + .font(.system(size: 9, weight: .regular)) + .foregroundStyle(statusService.snapshot.connectionState == .offline + ? .orange : AppTheme.inkQuaternary) + Text(checked, style: .relative) + .font(.system(size: 10, weight: .regular)) + .foregroundColor(AppTheme.inkQuaternary) + + Text(" ago") + .font(.system(size: 10, weight: .regular)) + .foregroundColor(AppTheme.inkQuaternary) + } + Spacer() + Button { + statusService.refresh(force: true) + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 11, weight: .regular)) + .foregroundStyle(AppTheme.inkQuaternary) + } + } + .frame(height: 16) + } + private func compactStat(_ value: String, label: String, color: Color) -> some View { VStack(spacing: 2) { Text(value) @@ -701,56 +703,43 @@ struct BackupView: View { private var actionButton: some View { Group { - // Coordinator is running a status check — block the start button so - // a backup cannot be triggered before reconciliation completes. - if coordinator.phase == .checking { + switch resolvedStatus { + case .idle, .completed, .failed, .cancelled: + Button(action: startBackup) { + Text(resolvedStatus == .idle ? "Start backup" : "Back up again") + } + .buttonStyle(PrimaryButtonStyle( + color: ctaFlashGreen ? AppTheme.positive : AppTheme.ink + )) + .accessibilityHint("Starts backing up your photos to the NAS") + + case .running: + Button { engine.pause() } label: { Text("Pause") } + .buttonStyle(GhostButtonStyle()) + .accessibilityLabel("Pause backup") + + case .paused: + HStack(spacing: 10) { + Button { engine.resume() } label: { Text("Resume") } + .buttonStyle(PrimaryButtonStyle()) + .accessibilityLabel("Resume backup") + Button { engine.cancel() } label: { Text("Cancel") } + .buttonStyle(GhostButtonStyle()) + .frame(width: 90) + .accessibilityLabel("Cancel backup") + } + + case .preparing: HStack(spacing: 8) { ProgressView().scaleEffect(0.8).tint(AppTheme.inkSecondary) - Text("Checking status…") + Text("Preparing…") .font(AppTheme.body()) .foregroundStyle(AppTheme.inkSecondary) } .frame(height: 50) - } else { - switch resolvedStatus { - case .idle, .completed, .failed, .cancelled: - Button(action: startBackup) { - Text(resolvedStatus == .idle ? "Start backup" : "Back up again") - } - .buttonStyle(PrimaryButtonStyle( - color: ctaFlashGreen ? AppTheme.positive : AppTheme.ink - )) - .accessibilityHint("Starts backing up your photos to the NAS") - - case .running: - Button { engine.pause() } label: { Text("Pause") } - .buttonStyle(GhostButtonStyle()) - .accessibilityLabel("Pause backup") - - case .paused: - HStack(spacing: 10) { - Button { engine.resume() } label: { Text("Resume") } - .buttonStyle(PrimaryButtonStyle()) - .accessibilityLabel("Resume backup") - Button { engine.cancel() } label: { Text("Cancel") } - .buttonStyle(GhostButtonStyle()) - .frame(width: 90) - .accessibilityLabel("Cancel backup") - } - - case .preparing: - HStack(spacing: 8) { - ProgressView().scaleEffect(0.8).tint(AppTheme.inkSecondary) - Text("Preparing…") - .font(AppTheme.body()) - .foregroundStyle(AppTheme.inkSecondary) - } - .frame(height: 50) - } } } .animation(.spring(response: 0.3, dampingFraction: 0.82), value: engine.job.status == .running) - .animation(.spring(response: 0.3, dampingFraction: 0.82), value: coordinator.phase == .checking) } private func startBackup() { diff --git a/Shared/Persistence/ConnectionStore.swift b/Shared/Persistence/ConnectionStore.swift index e25ff32..59eec98 100644 --- a/Shared/Persistence/ConnectionStore.swift +++ b/Shared/Persistence/ConnectionStore.swift @@ -99,7 +99,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 ?? false + autoBackupEnabled = UserDefaults.standard.object(forKey: "autoBackupEnabled") as? Bool ?? true autoBackupOnOpen = UserDefaults.standard.object(forKey: "autoBackupOnOpen") as? Bool ?? false allowCellularBackup = UserDefaults.standard.object(forKey: "allowCellularBackup") as? Bool ?? false useTailscaleWhenRemote = UserDefaults.standard.object(forKey: "useTailscaleWhenRemote") as? Bool ?? false