Add source code
This commit is contained in:
parent
69c08de01c
commit
0416d157d7
14 changed files with 1672 additions and 0 deletions
110
TetheringHider/Views/StatusIndicator.swift
Normal file
110
TetheringHider/Views/StatusIndicator.swift
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue