110 lines
4.1 KiB
Swift
110 lines
4.1 KiB
Swift
import SwiftUI
|
|
|
|
struct StatusIndicator: View {
|
|
let isProtected: Bool
|
|
let isProcessing: Bool
|
|
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@State private var pulseScale: CGFloat = 1.0
|
|
@State private var glowOpacity: Double = 0.4
|
|
@State private var rotationAngle: Double = 0
|
|
|
|
private var accentColor: Color {
|
|
if colorScheme == .light {
|
|
return isProtected
|
|
? Color(hue: 0.38, saturation: 0.85, brightness: 0.40)
|
|
: Color(hue: 0.05, saturation: 0.90, brightness: 0.45)
|
|
} else {
|
|
return isProtected
|
|
? Color(hue: 0.38, saturation: 0.75, brightness: 0.80)
|
|
: Color(hue: 0.06, saturation: 0.75, brightness: 0.85)
|
|
}
|
|
}
|
|
|
|
private var statusText: String {
|
|
if isProcessing { return "Processing..." }
|
|
return isProtected ? "PROTECTED" : "EXPOSED"
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 20) {
|
|
ZStack {
|
|
|
|
ForEach(0..<3, id: \.self) { ring in
|
|
Circle()
|
|
.stroke(
|
|
accentColor.opacity(colorScheme == .light ? (0.28 - Double(ring) * 0.07) : (0.15 - Double(ring) * 0.04)),
|
|
lineWidth: colorScheme == .light ? 2.0 : 1.5
|
|
)
|
|
.frame(
|
|
width: 140 + CGFloat(ring) * 22,
|
|
height: 140 + CGFloat(ring) * 22
|
|
)
|
|
.scaleEffect(pulseScale + CGFloat(ring) * 0.02)
|
|
}
|
|
|
|
Circle()
|
|
.fill(
|
|
RadialGradient(
|
|
colors: [
|
|
accentColor.opacity(0.25),
|
|
accentColor.opacity(0.08),
|
|
.clear
|
|
],
|
|
center: .center,
|
|
startRadius: 20,
|
|
endRadius: 75
|
|
)
|
|
)
|
|
.frame(width: 130, height: 130)
|
|
.scaleEffect(pulseScale)
|
|
|
|
// Antenna icon
|
|
Image(systemName: isProtected ? "antenna.radiowaves.left.and.right" : "antenna.radiowaves.left.and.right.slash")
|
|
.font(.system(size: 52, weight: .light))
|
|
.foregroundStyle(accentColor)
|
|
.symbolEffect(.bounce, value: isProtected)
|
|
.contentTransition(.symbolEffect(.replace.magic(fallback: .replace)))
|
|
.shadow(color: accentColor.opacity(0.5), radius: 16)
|
|
|
|
// Processing spinner
|
|
if isProcessing {
|
|
Circle()
|
|
.trim(from: 0, to: 0.7)
|
|
.stroke(
|
|
accentColor,
|
|
style: StrokeStyle(lineWidth: 2, lineCap: .round)
|
|
)
|
|
.frame(width: 125, height: 125)
|
|
.rotationEffect(.degrees(rotationAngle))
|
|
}
|
|
}
|
|
.animation(.easeInOut(duration: 0.8), value: isProtected)
|
|
.animation(.easeInOut(duration: 0.4), value: isProcessing)
|
|
|
|
// Status text
|
|
Text(statusText)
|
|
.font(.system(size: 16, weight: .semibold, design: .rounded))
|
|
.tracking(3)
|
|
.foregroundStyle(accentColor)
|
|
.contentTransition(.numericText())
|
|
.animation(.easeInOut, value: statusText)
|
|
}
|
|
.onAppear {
|
|
withAnimation(
|
|
.easeInOut(duration: 2.0)
|
|
.repeatForever(autoreverses: true)
|
|
) {
|
|
pulseScale = 1.04
|
|
glowOpacity = 0.7
|
|
}
|
|
|
|
withAnimation(
|
|
.linear(duration: 1.0)
|
|
.repeatForever(autoreverses: false)
|
|
) {
|
|
rotationAngle = 360
|
|
}
|
|
}
|
|
}
|
|
}
|