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

181 lines
6.9 KiB
Swift

import SwiftUI
struct MenuBarView: View {
@Environment(TTLManager.self) private var ttlManager
@Environment(\.openWindow) private var openWindow
@Environment(\.colorScheme) private var colorScheme
@State private var isProcessing = false
@State private var showError = false
@State private var errorMessage = ""
private let authService = AuthenticationService()
private let commandService = SystemCommandService()
private var accentColor: Color {
if colorScheme == .light {
return ttlManager.isProtected
? Color(hue: 0.38, saturation: 0.85, brightness: 0.40)
: Color(hue: 0.05, saturation: 0.90, brightness: 0.45)
} else {
return ttlManager.isProtected
? Color(hue: 0.38, saturation: 0.70, brightness: 0.85)
: Color(hue: 0.06, saturation: 0.70, brightness: 0.90)
}
}
var body: some View {
VStack(spacing: 14) {
// Header
HStack(spacing: 10) {
Image(systemName: ttlManager.isProtected ? "antenna.radiowaves.left.and.right" : "antenna.radiowaves.left.and.right.slash")
.font(.system(size: 20, weight: .medium))
.foregroundStyle(accentColor)
.symbolEffect(.bounce, value: ttlManager.isProtected)
VStack(alignment: .leading, spacing: 2) {
Text("Tethering Hider")
.font(.system(size: 13, weight: .bold, design: .rounded))
Text(ttlManager.isProtected ? "Protected (TTL 65)" : "Exposed (TTL 64)")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(accentColor)
}
Spacer()
// Toggle Switch
Toggle("", isOn: Binding(
get: { ttlManager.isProtected },
set: { newValue in
if newValue {
activateProtection()
} else {
deactivateProtection()
}
}
))
.toggleStyle(.switch)
.disabled(isProcessing)
.tint(accentColor)
}
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(.ultraThinMaterial)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(accentColor.opacity(0.2), lineWidth: 0.5)
)
)
.modifier(GlassCardModifier(tintColor: accentColor))
// Status Badges
HStack(spacing: 6) {
badgeItem(title: "IPv4", val: "\(ttlManager.currentTTL)", active: ttlManager.currentTTL == 65)
badgeItem(title: "IPv6", val: "\(ttlManager.currentIPv6HopLimit)", active: ttlManager.currentIPv6HopLimit == 65)
badgeItem(title: "DNS", val: "FLUSH", active: ttlManager.isProtected)
}
Divider()
.opacity(0.3)
// Action Links
VStack(spacing: 6) {
Button {
NSApp.activate(ignoringOtherApps: true)
openWindow(id: "mainWindow")
} label: {
Label("Open Dashboard", systemImage: "macwindow")
.font(.system(size: 12, weight: .medium))
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(RoundedRectangle(cornerRadius: 6).fill(Color.primary.opacity(0.05)))
Button(role: .destructive) {
NSApp.terminate(nil)
} label: {
Label("Quit Tethering Hider", systemImage: "power")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(.red)
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.padding(.horizontal, 10)
.padding(.vertical, 6)
}
}
.padding(14)
.frame(width: 280)
.alert("Error", isPresented: $showError) {
Button("OK", role: .cancel) { }
} message: {
Text(errorMessage)
}
.task {
ttlManager.refreshStatus()
}
}
private func badgeItem(title: String, val: String, active: Bool) -> some View {
HStack(spacing: 3) {
Circle()
.fill(active ? Color.green : Color.orange)
.frame(width: 4, height: 4)
Text("\(title): \(val)")
.font(.system(size: 9, weight: .medium, design: .monospaced))
.foregroundStyle(.secondary)
}
.padding(.horizontal, 6)
.padding(.vertical, 3)
.background(Capsule().fill(.ultraThinMaterial))
}
private func activateProtection() {
Task {
isProcessing = true
defer { isProcessing = false }
do {
let authenticated = try await authService.authenticateWithBiometrics()
guard authenticated else { return }
try await commandService.setTTL(ttlManager.targetTTL)
ttlManager.didSetTTL(ttlManager.targetTTL)
} catch is AuthenticationError {
} catch SystemCommandError.userCancelledAuth {
} catch {
errorMessage = error.localizedDescription
showError = true
ttlManager.setError(error.localizedDescription)
}
}
}
private func deactivateProtection() {
Task {
isProcessing = true
defer { isProcessing = false }
do {
let authenticated = try await authService.authenticateWithBiometrics()
guard authenticated else { return }
try await commandService.resetTTL()
ttlManager.didSetTTL(ttlManager.defaultTTL)
} catch is AuthenticationError {
} catch SystemCommandError.userCancelledAuth {
} catch {
errorMessage = error.localizedDescription
showError = true
ttlManager.setError(error.localizedDescription)
}
}
}
}