Lock circle during page swipes — unified ring for pages 0 and 1
Pages 0 (backup) and 1 (storage) now share a single KisaniRingView instance. Arc values (progress, color) swap in-place via computed properties so the ring never re-mounts or shifts position during horizontal swipes. Page 2 (cleanup) retains its own hero layout. Pull gesture and page-switch gesture coexist via simultaneousGesture. Co-Authored-By: Kutesir <kutesir@provoc.ug> Co-Authored-By: Sentry <sentry@provoc.ug>
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user