feat: move check status below ring, remove checking spinner, re-enable auto-backup

- 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 <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
This commit is contained in:
Robin Kutesa
2026-05-18 21:12:35 +03:00
parent 2384a74517
commit 78a6b377f2
2 changed files with 69 additions and 80 deletions

View File

@@ -54,7 +54,12 @@ struct BackupView: View {
// Ring // Ring
ringHero ringHero
.padding(.bottom, 44) .padding(.bottom, 12)
// Check status
checkStatusRow
.padding(.horizontal, AppTheme.hPad)
.padding(.bottom, 20)
// Stats // Stats
statsStrip statsStrip
@@ -229,14 +234,10 @@ struct BackupView: View {
private var ringCenterContent: some View { private var ringCenterContent: some View {
switch ringPage { switch ringPage {
case 0: case 0:
// Engine activity always takes priority.
// Coordinator states are shown only when the engine is idle.
if engine.job.status.isActive { if engine.job.status.isActive {
ringMainView ringMainView
} else if coordinator.phase == .disconnected, statusService.snapshot.needBackup > 0 { } else if coordinator.phase == .disconnected, statusService.snapshot.needBackup > 0 {
disconnectedRingView disconnectedRingView
} else if coordinator.phase == .checking {
checkingRingView
} else { } else {
ringMainView ringMainView
} }
@@ -487,42 +488,43 @@ struct BackupView: View {
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
pageIndicator 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 { private func compactStat(_ value: String, label: String, color: Color) -> some View {
VStack(spacing: 2) { VStack(spacing: 2) {
Text(value) Text(value)
@@ -701,56 +703,43 @@ struct BackupView: View {
private var actionButton: some View { private var actionButton: some View {
Group { Group {
// Coordinator is running a status check block the start button so switch resolvedStatus {
// a backup cannot be triggered before reconciliation completes. case .idle, .completed, .failed, .cancelled:
if coordinator.phase == .checking { 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) { HStack(spacing: 8) {
ProgressView().scaleEffect(0.8).tint(AppTheme.inkSecondary) ProgressView().scaleEffect(0.8).tint(AppTheme.inkSecondary)
Text("Checking status") Text("Preparing")
.font(AppTheme.body()) .font(AppTheme.body())
.foregroundStyle(AppTheme.inkSecondary) .foregroundStyle(AppTheme.inkSecondary)
} }
.frame(height: 50) .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: engine.job.status == .running)
.animation(.spring(response: 0.3, dampingFraction: 0.82), value: coordinator.phase == .checking)
} }
private func startBackup() { private func startBackup() {

View File

@@ -99,7 +99,7 @@ final class ConnectionStore: ObservableObject {
onboardingPhotoShown = UserDefaults.standard.bool(forKey: "onboardingPhotoShown") onboardingPhotoShown = UserDefaults.standard.bool(forKey: "onboardingPhotoShown")
appearanceMode = AppearanceMode(rawValue: UserDefaults.standard.string(forKey: "appearanceMode") ?? "") ?? .system appearanceMode = AppearanceMode(rawValue: UserDefaults.standard.string(forKey: "appearanceMode") ?? "") ?? .system
localRetentionDays = UserDefaults.standard.object(forKey: "localRetentionDays") as? Int ?? 0 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 autoBackupOnOpen = UserDefaults.standard.object(forKey: "autoBackupOnOpen") as? Bool ?? false
allowCellularBackup = UserDefaults.standard.object(forKey: "allowCellularBackup") as? Bool ?? false allowCellularBackup = UserDefaults.standard.object(forKey: "allowCellularBackup") as? Bool ?? false
useTailscaleWhenRemote = UserDefaults.standard.object(forKey: "useTailscaleWhenRemote") as? Bool ?? false useTailscaleWhenRemote = UserDefaults.standard.object(forKey: "useTailscaleWhenRemote") as? Bool ?? false