diff --git a/Features/Backup/BackupView.swift b/Features/Backup/BackupView.swift index fe44232..6d669af 100644 --- a/Features/Backup/BackupView.swift +++ b/Features/Backup/BackupView.swift @@ -149,12 +149,13 @@ struct BackupView: View { private var ringHero: some View { ZStack { KisaniRingView( - outerProgress: outerRingProgress, - innerProgress: innerRingProgressWidget, - innerColor: innerRingColor + outerProgress: pageOuterProgress, + outerColor: pageOuterColor, + innerProgress: pageInnerProgress, + innerColor: pageInnerColor ) .accessibilityLabel("Backup progress") - .accessibilityValue("\(Int(innerRingProgressWidget * 100)) percent") + .accessibilityValue("\(Int(pageInnerProgress * 100)) percent") VStack(spacing: 4) { ringMainView @@ -166,11 +167,41 @@ struct BackupView: View { .animation(.spring(response: 0.4, dampingFraction: 0.8), value: engine.job.progress) } - private var ringContentID: String { String(describing: widgetState) } + private var ringContentID: String { "\(ringPage)-\(String(describing: widgetState))" } + + // Arc values routed per page so the ring stays physically fixed + private var pageOuterProgress: CGFloat { + ringPage == 1 ? usedFraction : outerRingProgress + } + private var pageOuterColor: Color { + ringPage == 1 + ? Color(red: 1.0, green: 0.58, blue: 0.0) + : Color(red: 0.42, green: 0.52, blue: 0.68) + } + private var pageInnerProgress: CGFloat { + ringPage == 1 ? safeFraction : innerRingProgressWidget + } + private var pageInnerColor: Color { + ringPage == 1 ? AppTheme.positive : innerRingColor + } @ViewBuilder private var ringMainView: some View { - switch widgetState { + if ringPage == 1 { + VStack(spacing: 6) { + Image(systemName: "iphone") + .font(.system(size: 14, weight: .ultraLight)) + .foregroundStyle(AppTheme.inkTertiary) + Text("\(Int(usedFraction * 100))%") + .font(.system(size: 46, weight: .ultraLight)) + .foregroundStyle(Color(red: 1.0, green: 0.58, blue: 0.0)) + .kerning(-2) + Text("STORAGE USED") + .font(.system(size: 8, weight: .regular, design: .monospaced)) + .foregroundStyle(AppTheme.inkTertiary) + .tracking(2).opacity(0.75) + } + } else { switch widgetState { case .safe: VStack(spacing: 6) { ringCheckmark @@ -221,7 +252,7 @@ struct BackupView: View { .foregroundStyle(AppTheme.inkTertiary) .tracking(2).opacity(0.75) } - } + } } // end else / switch widgetState } private var ringCheckmark: some View { @@ -495,27 +526,8 @@ struct BackupView: View { @ViewBuilder private var heroView: some View { - switch ringPage { - case 1: - ZStack { - StorageRingView(usedProgress: usedFraction, safeProgress: safeFraction) - VStack(spacing: 6) { - Image(systemName: "iphone") - .font(.system(size: 14, weight: .ultraLight)) - .foregroundStyle(AppTheme.inkTertiary) - Text("\(Int(usedFraction * 100))%") - .font(.system(size: 46, weight: .ultraLight)) - .foregroundStyle(Color(red: 1.0, green: 0.58, blue: 0.0)) - .kerning(-2) - Text("STORAGE USED") - .font(.system(size: 8, weight: .regular, design: .monospaced)) - .foregroundStyle(AppTheme.inkTertiary) - .tracking(2).opacity(0.75) - } - } - .gesture(pageSwitchGesture) - .padding(.bottom, 8) - case 2: + if ringPage == 2 { + // Cleanup page — no ring HStack(alignment: .lastTextBaseline, spacing: 6) { Text(String(format: "%.1f", totalFreeableGB)) .font(.system(size: 64, weight: .ultraLight)) @@ -531,7 +543,8 @@ struct BackupView: View { .padding(.horizontal, AppTheme.hPad) .padding(.bottom, 8) .gesture(pageSwitchGesture) - default: + } else { + // Pages 0 and 1 share the same circle — arc values switch in-place circleSection } } @@ -645,7 +658,7 @@ struct BackupView: View { private var circleSection: some View { VStack(spacing: 0) { ZStack { - // Ripple expands outward on backup trigger + // Ripple expands outward on backup trigger (page 0 only) Circle() .stroke(AppTheme.positive, lineWidth: 1.5) .frame(width: 288, height: 288) @@ -656,20 +669,28 @@ struct BackupView: View { ringHero } .gesture(pullGesture) + .simultaneousGesture(pageSwitchGesture) .padding(.bottom, 4) - cascadingArrows - .frame(height: 16) - .padding(.bottom, 8) + if ringPage == 0 { + cascadingArrows + .frame(height: 16) + .padding(.bottom, 8) - circleStatusRow - .padding(.horizontal, AppTheme.hPad) - .padding(.bottom, 20) + circleStatusRow + .padding(.horizontal, AppTheme.hPad) + .padding(.bottom, 20) + } else { + // Match page 0 spacing so the ring stays at the same Y + Color.clear.frame(height: 60) + } } - // Pull zone floats above the ring without affecting its layout position + // Pull zone floats above the ring on page 0 only .overlay(alignment: .top) { - pullZone - .offset(y: -max(0, pullProgress * 0.52)) + if ringPage == 0 { + pullZone + .offset(y: -max(0, pullProgress * 0.52)) + } } } @@ -1012,20 +1033,20 @@ struct BackupView: View { private struct KisaniRingView: View { let outerProgress: CGFloat + let outerColor: Color let innerProgress: CGFloat let innerColor: Color - private let track = Color(red: 0.922, green: 0.922, blue: 0.922) // #ebebeb - private let nasArc = Color(red: 0.42, green: 0.52, blue: 0.68) + private let track = Color(red: 0.922, green: 0.922, blue: 0.922) // #ebebeb var body: some View { ZStack { // Outer track Circle().stroke(track, lineWidth: 1).padding(10) - // Outer NAS progress (blue-slate + glow) + // Outer arc — color is caller-supplied (NAS slate or storage orange) Circle().trim(from: 0, to: outerProgress) - .stroke(nasArc, style: StrokeStyle(lineWidth: 1, lineCap: .round)) - .shadow(color: nasArc.opacity(0.55), radius: 5, x: 0, y: 0) + .stroke(outerColor, style: StrokeStyle(lineWidth: 1, lineCap: .round)) + .shadow(color: outerColor.opacity(0.55), radius: 5, x: 0, y: 0) .rotationEffect(.degrees(-90)) .padding(10) .animation(.spring(response: 0.7, dampingFraction: 0.8), value: outerProgress)