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