tethering-hider/TetheringHider/Views/StatusIndicator.swift
2026-08-12 23:44:59 -05:00

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
}
}
}
}