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 {
|
private var ringHero: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
KisaniRingView(
|
KisaniRingView(
|
||||||
outerProgress: outerRingProgress,
|
outerProgress: pageOuterProgress,
|
||||||
innerProgress: innerRingProgressWidget,
|
outerColor: pageOuterColor,
|
||||||
innerColor: innerRingColor
|
innerProgress: pageInnerProgress,
|
||||||
|
innerColor: pageInnerColor
|
||||||
)
|
)
|
||||||
.accessibilityLabel("Backup progress")
|
.accessibilityLabel("Backup progress")
|
||||||
.accessibilityValue("\(Int(innerRingProgressWidget * 100)) percent")
|
.accessibilityValue("\(Int(pageInnerProgress * 100)) percent")
|
||||||
|
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
ringMainView
|
ringMainView
|
||||||
@@ -166,11 +167,41 @@ struct BackupView: View {
|
|||||||
.animation(.spring(response: 0.4, dampingFraction: 0.8), value: engine.job.progress)
|
.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
|
@ViewBuilder
|
||||||
private var ringMainView: some View {
|
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:
|
case .safe:
|
||||||
VStack(spacing: 6) {
|
VStack(spacing: 6) {
|
||||||
ringCheckmark
|
ringCheckmark
|
||||||
@@ -221,7 +252,7 @@ struct BackupView: View {
|
|||||||
.foregroundStyle(AppTheme.inkTertiary)
|
.foregroundStyle(AppTheme.inkTertiary)
|
||||||
.tracking(2).opacity(0.75)
|
.tracking(2).opacity(0.75)
|
||||||
}
|
}
|
||||||
}
|
} } // end else / switch widgetState
|
||||||
}
|
}
|
||||||
|
|
||||||
private var ringCheckmark: some View {
|
private var ringCheckmark: some View {
|
||||||
@@ -495,27 +526,8 @@ struct BackupView: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var heroView: some View {
|
private var heroView: some View {
|
||||||
switch ringPage {
|
if ringPage == 2 {
|
||||||
case 1:
|
// Cleanup page — no ring
|
||||||
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:
|
|
||||||
HStack(alignment: .lastTextBaseline, spacing: 6) {
|
HStack(alignment: .lastTextBaseline, spacing: 6) {
|
||||||
Text(String(format: "%.1f", totalFreeableGB))
|
Text(String(format: "%.1f", totalFreeableGB))
|
||||||
.font(.system(size: 64, weight: .ultraLight))
|
.font(.system(size: 64, weight: .ultraLight))
|
||||||
@@ -531,7 +543,8 @@ struct BackupView: View {
|
|||||||
.padding(.horizontal, AppTheme.hPad)
|
.padding(.horizontal, AppTheme.hPad)
|
||||||
.padding(.bottom, 8)
|
.padding(.bottom, 8)
|
||||||
.gesture(pageSwitchGesture)
|
.gesture(pageSwitchGesture)
|
||||||
default:
|
} else {
|
||||||
|
// Pages 0 and 1 share the same circle — arc values switch in-place
|
||||||
circleSection
|
circleSection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -645,7 +658,7 @@ struct BackupView: View {
|
|||||||
private var circleSection: some View {
|
private var circleSection: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
ZStack {
|
ZStack {
|
||||||
// Ripple expands outward on backup trigger
|
// Ripple expands outward on backup trigger (page 0 only)
|
||||||
Circle()
|
Circle()
|
||||||
.stroke(AppTheme.positive, lineWidth: 1.5)
|
.stroke(AppTheme.positive, lineWidth: 1.5)
|
||||||
.frame(width: 288, height: 288)
|
.frame(width: 288, height: 288)
|
||||||
@@ -656,8 +669,10 @@ struct BackupView: View {
|
|||||||
ringHero
|
ringHero
|
||||||
}
|
}
|
||||||
.gesture(pullGesture)
|
.gesture(pullGesture)
|
||||||
|
.simultaneousGesture(pageSwitchGesture)
|
||||||
.padding(.bottom, 4)
|
.padding(.bottom, 4)
|
||||||
|
|
||||||
|
if ringPage == 0 {
|
||||||
cascadingArrows
|
cascadingArrows
|
||||||
.frame(height: 16)
|
.frame(height: 16)
|
||||||
.padding(.bottom, 8)
|
.padding(.bottom, 8)
|
||||||
@@ -665,13 +680,19 @@ struct BackupView: View {
|
|||||||
circleStatusRow
|
circleStatusRow
|
||||||
.padding(.horizontal, AppTheme.hPad)
|
.padding(.horizontal, AppTheme.hPad)
|
||||||
.padding(.bottom, 20)
|
.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) {
|
.overlay(alignment: .top) {
|
||||||
|
if ringPage == 0 {
|
||||||
pullZone
|
pullZone
|
||||||
.offset(y: -max(0, pullProgress * 0.52))
|
.offset(y: -max(0, pullProgress * 0.52))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var pullZone: some View {
|
private var pullZone: some View {
|
||||||
let pct = min(pullProgress / 58, 1.0)
|
let pct = min(pullProgress / 58, 1.0)
|
||||||
@@ -1012,20 +1033,20 @@ struct BackupView: View {
|
|||||||
|
|
||||||
private struct KisaniRingView: View {
|
private struct KisaniRingView: View {
|
||||||
let outerProgress: CGFloat
|
let outerProgress: CGFloat
|
||||||
|
let outerColor: Color
|
||||||
let innerProgress: CGFloat
|
let innerProgress: CGFloat
|
||||||
let innerColor: Color
|
let innerColor: Color
|
||||||
|
|
||||||
private let track = Color(red: 0.922, green: 0.922, blue: 0.922) // #ebebeb
|
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)
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
// Outer track
|
// Outer track
|
||||||
Circle().stroke(track, lineWidth: 1).padding(10)
|
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)
|
Circle().trim(from: 0, to: outerProgress)
|
||||||
.stroke(nasArc, style: StrokeStyle(lineWidth: 1, lineCap: .round))
|
.stroke(outerColor, style: StrokeStyle(lineWidth: 1, lineCap: .round))
|
||||||
.shadow(color: nasArc.opacity(0.55), radius: 5, x: 0, y: 0)
|
.shadow(color: outerColor.opacity(0.55), radius: 5, x: 0, y: 0)
|
||||||
.rotationEffect(.degrees(-90))
|
.rotationEffect(.degrees(-90))
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.animation(.spring(response: 0.7, dampingFraction: 0.8), value: outerProgress)
|
.animation(.spring(response: 0.7, dampingFraction: 0.8), value: outerProgress)
|
||||||
|
|||||||
Reference in New Issue
Block a user