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:
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user