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 <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
This commit is contained in:
Robin Kutesa
2026-05-21 15:06:43 +03:00
parent 02feba092c
commit 5a55931233

View File

@@ -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 {