181 lines
6.9 KiB
Swift
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)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|