From 5a5593123346906058e780691a9ffe908ec18b1f Mon Sep 17 00:00:00 2001 From: Robin Kutesa Date: Thu, 21 May 2026 15:06:43 +0300 Subject: [PATCH] Replace clean confirm dialog with multi-option sheet Pull-to-clean on pages 1 and 2 now opens a half-sheet with: - Backed-up photos row (always selected, shows ~GB) - Screenshots section with 7 / 21 / 30 / 60 day toggle buttons - CLEAN NOW button proceeds to final confirmation Co-Authored-By: Kutesir Co-Authored-By: Sentry --- Features/Backup/BackupView.swift | 138 ++++++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 1 deletion(-) diff --git a/Features/Backup/BackupView.swift b/Features/Backup/BackupView.swift index f6dea03..907eb5a 100644 --- a/Features/Backup/BackupView.swift +++ b/Features/Backup/BackupView.swift @@ -21,6 +21,8 @@ struct BackupView: View { @State private var deviceTotalGB: Double = 0 @State private var deviceFreeGB: Double = 0 @State private var showCleanConfirm = false + @State private var showCleanSheet = false + @State private var cleanScreenshotDays: Int? = nil @State private var pullProgress: CGFloat = 0 @State private var rippleScale: CGFloat = 0.88 @State private var rippleOpacity: Double = 0 @@ -118,6 +120,7 @@ struct BackupView: View { } message: { Text("This removes photos already safely backed up to your NAS.") } + .sheet(isPresented: $showCleanSheet) { cleanOptionsSheet } .task { coordinator.onActive() loadDeviceStorage() @@ -832,7 +835,7 @@ struct BackupView: View { private func triggerCleanViaPull() { rippleScale = 0.88; rippleOpacity = 0.6 withAnimation(.easeOut(duration: 0.65)) { rippleScale = 1.35; rippleOpacity = 0 } - showCleanConfirm = true + showCleanSheet = true } private func arrowFlowOpacity(_ phase: Double) -> Double { @@ -976,6 +979,139 @@ struct BackupView: View { return sin(t / duration * .pi) } + // MARK: — Clean options sheet + + private var cleanOptionsSheet: some View { + VStack(spacing: 0) { + // Handle + Capsule() + .fill(AppTheme.inkQuaternary.opacity(0.4)) + .frame(width: 36, height: 4) + .padding(.top, 12) + .padding(.bottom, 20) + + // Header + HStack { + Text("Clean Storage") + .font(.system(size: 17, weight: .semibold)) + .foregroundStyle(AppTheme.ink) + Spacer() + Button { showCleanSheet = false } label: { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 22)) + .foregroundStyle(AppTheme.inkQuaternary) + } + } + .padding(.horizontal, AppTheme.hPad) + .padding(.bottom, 24) + + // Row: backed-up photos (always included, non-toggleable) + HStack(spacing: 12) { + ZStack { + Circle().fill(AppTheme.positive.opacity(0.12)).frame(width: 36, height: 36) + Image(systemName: "photo.on.rectangle") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(AppTheme.positive) + } + VStack(alignment: .leading, spacing: 2) { + Text("Backed-up photos") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(AppTheme.ink) + Text(String(format: "~%.1f GB · already safe on NAS", junkGB)) + .font(.system(size: 11)) + .foregroundStyle(AppTheme.inkTertiary) + } + Spacer() + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 18)) + .foregroundStyle(AppTheme.positive) + } + .padding(.horizontal, AppTheme.hPad) + .padding(.vertical, 14) + .background(AppTheme.surfaceSunken) + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + .padding(.horizontal, AppTheme.hPad) + .padding(.bottom, 12) + + // Screenshots section + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 12) { + ZStack { + Circle().fill(AppTheme.interactive.opacity(0.12)).frame(width: 36, height: 36) + Image(systemName: "camera.viewfinder") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(AppTheme.interactive) + } + VStack(alignment: .leading, spacing: 2) { + Text("Screenshots") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(AppTheme.ink) + Text(cleanScreenshotDays == nil ? "Not selected" : "Older than \(cleanScreenshotDays!) days") + .font(.system(size: 11)) + .foregroundStyle(AppTheme.inkTertiary) + } + Spacer() + } + + // Day picker + HStack(spacing: 8) { + ForEach([7, 21, 30, 60], id: \.self) { days in + Button { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + cleanScreenshotDays = cleanScreenshotDays == days ? nil : days + } + } label: { + Text("\(days) days") + .font(.system(size: 12, weight: .medium, design: .monospaced)) + .foregroundStyle(cleanScreenshotDays == days ? .white : AppTheme.inkSecondary) + .frame(maxWidth: .infinity) + .frame(height: 34) + .background(cleanScreenshotDays == days + ? AppTheme.interactive + : AppTheme.surfaceSunken) + .clipShape(RoundedRectangle(cornerRadius: 9, style: .continuous)) + } + .buttonStyle(.plain) + } + } + } + .padding(.horizontal, AppTheme.hPad) + .padding(.vertical, 14) + .background(AppTheme.surfaceSunken) + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + .padding(.horizontal, AppTheme.hPad) + .padding(.bottom, 28) + + Spacer(minLength: 0) + + // Action button + Button { + showCleanSheet = false + showCleanConfirm = true + } label: { + Text("CLEAN NOW") + .font(.system(size: 13, weight: .medium, design: .monospaced)) + .tracking(2) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .frame(height: 54) + .background(Color(red: 0.08, green: 0.08, blue: 0.08)) + .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) + } + .buttonStyle(.plain) + .padding(.horizontal, AppTheme.hPad) + + Text("This cannot be undone") + .font(.system(size: 10, weight: .regular, design: .monospaced)) + .foregroundStyle(AppTheme.inkQuaternary) + .tracking(1) + .padding(.top, 10) + .padding(.bottom, 28) + } + .presentationDetents([.medium]) + .presentationDragIndicator(.hidden) + } + // MARK: — Storage helpers private func storageStat(_ value: String, label: String, color: Color) -> some View {