207 lines
7.2 KiB
Swift
207 lines
7.2 KiB
Swift
|
|
import SwiftUI
|
||
|
|
|
||
|
|
struct TTLControlPanel: View {
|
||
|
|
let currentTTL: Int
|
||
|
|
let currentIPv6HopLimit: Int
|
||
|
|
let targetTTL: Int
|
||
|
|
let defaultTTL: Int
|
||
|
|
let isProtected: Bool
|
||
|
|
let isProcessing: Bool
|
||
|
|
let onActivate: () -> Void
|
||
|
|
let onDeactivate: () -> Void
|
||
|
|
|
||
|
|
@AppStorage("startMinimized") private var startMinimized = false
|
||
|
|
@Environment(\.colorScheme) private var colorScheme
|
||
|
|
@Namespace private var controlNamespace
|
||
|
|
|
||
|
|
@State private var isHovering = false
|
||
|
|
|
||
|
|
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.70, brightness: 0.80)
|
||
|
|
: Color(hue: 0.06, saturation: 0.70, brightness: 0.85)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private var buttonTint: Color {
|
||
|
|
if colorScheme == .light {
|
||
|
|
return isProtected
|
||
|
|
? Color(hue: 0.05, saturation: 0.85, brightness: isHovering ? 0.60 : 0.50)
|
||
|
|
: Color(hue: 0.38, saturation: 0.85, brightness: isHovering ? 0.50 : 0.42)
|
||
|
|
} else {
|
||
|
|
return isProtected
|
||
|
|
? Color(hue: 0.06, saturation: 0.65, brightness: isHovering ? 0.90 : 0.80)
|
||
|
|
: Color(hue: 0.38, saturation: 0.65, brightness: isHovering ? 0.90 : 0.80)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var body: some View {
|
||
|
|
VStack(spacing: 12) {
|
||
|
|
ttlInfoCard
|
||
|
|
protocolBadges
|
||
|
|
actionButton
|
||
|
|
settingsCard
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private var ttlInfoCard: some View {
|
||
|
|
HStack(spacing: 20) {
|
||
|
|
ttlValueDisplay(
|
||
|
|
label: "CURRENT",
|
||
|
|
value: currentTTL,
|
||
|
|
highlight: isProtected
|
||
|
|
)
|
||
|
|
|
||
|
|
Image(systemName: "arrow.right")
|
||
|
|
.font(.system(size: 14, weight: .medium))
|
||
|
|
.foregroundStyle(.tertiary)
|
||
|
|
|
||
|
|
ttlValueDisplay(
|
||
|
|
label: "TARGET",
|
||
|
|
value: targetTTL,
|
||
|
|
highlight: true
|
||
|
|
)
|
||
|
|
}
|
||
|
|
.padding(.horizontal, 24)
|
||
|
|
.padding(.vertical, 12)
|
||
|
|
.frame(maxWidth: .infinity)
|
||
|
|
.background {
|
||
|
|
RoundedRectangle(cornerRadius: 16)
|
||
|
|
.fill(.ultraThinMaterial)
|
||
|
|
.overlay {
|
||
|
|
RoundedRectangle(cornerRadius: 16)
|
||
|
|
.stroke(accentColor.opacity(0.15), lineWidth: 0.5)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.modifier(GlassCardModifier(tintColor: accentColor))
|
||
|
|
}
|
||
|
|
|
||
|
|
private func ttlValueDisplay(label: String, value: Int, highlight: Bool) -> some View {
|
||
|
|
VStack(spacing: 4) {
|
||
|
|
Text(label)
|
||
|
|
.font(.system(size: 10, weight: .semibold, design: .rounded))
|
||
|
|
.tracking(2)
|
||
|
|
.foregroundStyle(.secondary)
|
||
|
|
|
||
|
|
Text("\(value)")
|
||
|
|
.font(.system(size: 30, weight: .bold, design: .monospaced))
|
||
|
|
.foregroundStyle(highlight ? accentColor : .primary)
|
||
|
|
.contentTransition(.numericText())
|
||
|
|
.animation(.spring(duration: 0.5), value: value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Protocol Badges
|
||
|
|
|
||
|
|
private var protocolBadges: some View {
|
||
|
|
HStack(spacing: 8) {
|
||
|
|
badgeView(title: "IPv4 TTL", value: "\(currentTTL)", active: currentTTL == targetTTL)
|
||
|
|
badgeView(title: "IPv6 HLIM", value: "\(currentIPv6HopLimit)", active: currentIPv6HopLimit == targetTTL)
|
||
|
|
badgeView(title: "DNS FLUSH", value: "AUTO", active: isProtected)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private func badgeView(title: String, value: String, active: Bool) -> some View {
|
||
|
|
HStack(spacing: 4) {
|
||
|
|
Circle()
|
||
|
|
.fill(active ? Color.green : Color.orange)
|
||
|
|
.frame(width: 5, height: 5)
|
||
|
|
|
||
|
|
Text("\(title): \(value)")
|
||
|
|
.font(.system(size: 9, weight: .medium, design: .monospaced))
|
||
|
|
.foregroundStyle(.secondary)
|
||
|
|
}
|
||
|
|
.padding(.horizontal, 8)
|
||
|
|
.padding(.vertical, 4)
|
||
|
|
.background(
|
||
|
|
Capsule()
|
||
|
|
.fill(.ultraThinMaterial)
|
||
|
|
.overlay(
|
||
|
|
Capsule().stroke(active ? Color.green.opacity(0.3) : Color.orange.opacity(0.2), lineWidth: 0.5)
|
||
|
|
)
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Action Button
|
||
|
|
|
||
|
|
private var actionButton: some View {
|
||
|
|
Button {
|
||
|
|
if isProtected {
|
||
|
|
onDeactivate()
|
||
|
|
} else {
|
||
|
|
onActivate()
|
||
|
|
}
|
||
|
|
} label: {
|
||
|
|
HStack(spacing: 10) {
|
||
|
|
if isProcessing {
|
||
|
|
ProgressView()
|
||
|
|
.controlSize(.small)
|
||
|
|
.tint(.white)
|
||
|
|
} else {
|
||
|
|
Image(systemName: isProtected ? "antenna.radiowaves.left.and.right.slash" : "antenna.radiowaves.left.and.right")
|
||
|
|
.font(.system(size: 16, weight: .semibold))
|
||
|
|
.symbolEffect(.bounce, value: isHovering)
|
||
|
|
.contentTransition(.symbolEffect(.replace.magic(fallback: .replace)))
|
||
|
|
}
|
||
|
|
|
||
|
|
Text(isProtected ? "Deactivate Hider" : "Activate Hider")
|
||
|
|
.font(.system(size: 15, weight: .semibold, design: .rounded))
|
||
|
|
.contentTransition(.numericText())
|
||
|
|
}
|
||
|
|
.frame(maxWidth: .infinity)
|
||
|
|
.padding(.vertical, 12)
|
||
|
|
}
|
||
|
|
.modifier(GlassButtonModifier())
|
||
|
|
.tint(buttonTint)
|
||
|
|
.scaleEffect(isHovering ? 1.025 : 1.0)
|
||
|
|
.shadow(
|
||
|
|
color: (isProtected
|
||
|
|
? Color(hue: 0.06, saturation: 0.8, brightness: colorScheme == .light ? 0.5 : 0.9)
|
||
|
|
: Color(hue: 0.38, saturation: 0.8, brightness: colorScheme == .light ? 0.4 : 0.9)
|
||
|
|
).opacity(isHovering ? (colorScheme == .light ? 0.35 : 0.45) : 0.0),
|
||
|
|
radius: isHovering ? 12 : 0,
|
||
|
|
x: 0,
|
||
|
|
y: isHovering ? 4 : 0
|
||
|
|
)
|
||
|
|
.onHover { hovering in
|
||
|
|
withAnimation(.spring(response: 0.35, dampingFraction: 0.7)) {
|
||
|
|
isHovering = hovering
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.disabled(isProcessing)
|
||
|
|
.animation(.easeInOut(duration: 0.3), value: isProtected)
|
||
|
|
.animation(.easeInOut(duration: 0.3), value: isProcessing)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Settings Card
|
||
|
|
|
||
|
|
private var settingsCard: some View {
|
||
|
|
HStack {
|
||
|
|
Label("Start minimized in menu bar", systemImage: "macwindow.on.rectangle")
|
||
|
|
.font(.system(size: 11, weight: .medium))
|
||
|
|
.foregroundStyle(.secondary)
|
||
|
|
|
||
|
|
Spacer()
|
||
|
|
|
||
|
|
Toggle("", isOn: $startMinimized)
|
||
|
|
.toggleStyle(.switch)
|
||
|
|
.controlSize(.small)
|
||
|
|
.tint(accentColor)
|
||
|
|
}
|
||
|
|
.padding(.horizontal, 12)
|
||
|
|
.padding(.vertical, 7)
|
||
|
|
.background(
|
||
|
|
RoundedRectangle(cornerRadius: 10)
|
||
|
|
.fill(.ultraThinMaterial)
|
||
|
|
.overlay(
|
||
|
|
RoundedRectangle(cornerRadius: 10)
|
||
|
|
.stroke(accentColor.opacity(0.12), lineWidth: 0.5)
|
||
|
|
)
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|