Files
lemana-vpn/app/Sources/LemanaVPN/LemanaVPNApp.swift

178 lines
5.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import AppKit
import SwiftUI
@main
struct LemanaVPNApp: App {
@StateObject private var vpnManager = VPNManager()
var body: some Scene {
MenuBarExtra {
VPNMenuView(vpnManager: vpnManager)
} label: {
StatusBarLabel(vpnManager: vpnManager)
}
}
}
struct StatusBarLabel: View {
@ObservedObject var vpnManager: VPNManager
var body: some View {
HStack(spacing: 4) {
Image(systemName: iconName)
if !vpnManager.statusBarTitle.isEmpty {
Text(vpnManager.statusBarTitle)
.monospacedDigit()
}
}
}
private var iconName: String {
switch vpnManager.state {
case .disconnected:
return "shield.slash"
case .unlocking, .connecting:
return "shield.lefthalf.filled"
case .connected:
return vpnManager.tunnelHealthy ? "checkmark.shield" : "exclamationmark.shield"
case .reconnecting:
return "arrow.trianglehead.2.clockwise"
case .error:
return "exclamationmark.shield"
}
}
}
struct VPNMenuView: View {
@ObservedObject var vpnManager: VPNManager
var body: some View {
switch vpnManager.state {
case .disconnected:
disconnectedMenu
case .unlocking(let tier):
unlockingMenu(tier: tier)
case .connecting:
connectingMenu
case .connected(let ip, _, let remainingSec):
connectedMenu(ip: ip, remainingSec: remainingSec)
case .reconnecting(let attempt):
reconnectingMenu(attempt: attempt)
case .error(let message):
errorMenu(message: message)
}
Divider()
Label(vpnManager.moduleSummary, systemImage: "puzzlepiece.extension")
.disabled(true)
Button("Обновить статус модулей") {
vpnManager.refreshStatus()
}
Divider()
Button("Открыть логи") {
let logPath = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/Logs/LemanaVPN.log")
NSWorkspace.shared.open(logPath)
}
Button("Выход") {
vpnManager.quit()
NSApplication.shared.terminate(nil)
}
.keyboardShortcut("q")
}
private var disconnectedMenu: some View {
Group {
Label("VPN отключён", systemImage: "circle")
.disabled(true)
Divider()
Button("Подключить") {
vpnManager.connect()
}
.keyboardShortcut("c")
}
}
private func unlockingMenu(tier: String) -> some View {
Group {
Label("Bitwarden: \(tier)...", systemImage: "key")
.disabled(true)
Divider()
Button("Отменить") {
vpnManager.disconnect()
}
}
}
private var connectingMenu: some View {
Group {
Label("Подключение...", systemImage: "circle.dotted")
.disabled(true)
Divider()
Button("Отменить") {
vpnManager.disconnect()
}
}
}
private func connectedMenu(ip: String, remainingSec: Int) -> some View {
let hours = remainingSec / 3600
let mins = (remainingSec % 3600) / 60
return Group {
Label("VPN подключён", systemImage: "circle.fill")
.disabled(true)
if !vpnManager.tunnelHealthy {
Label("Проблемы с подключением", systemImage: "exclamationmark.triangle")
.disabled(true)
}
if !ip.isEmpty {
Text("IP: \(ip)")
.foregroundColor(.secondary)
}
if remainingSec > 0 {
Text("Сессия: \(hours)ч \(mins)м")
.foregroundColor(.secondary)
}
Divider()
Button("Копировать IP") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(ip, forType: .string)
}
.keyboardShortcut("c", modifiers: [.command])
.disabled(ip.isEmpty)
Button("Отключить") {
vpnManager.disconnect()
}
.keyboardShortcut("d")
}
}
private func reconnectingMenu(attempt: Int) -> some View {
Group {
Label("Переподключение (\(attempt)/3)...", systemImage: "arrow.clockwise")
.disabled(true)
Divider()
Button("Отменить") {
vpnManager.disconnect()
}
}
}
private func errorMenu(message: String) -> some View {
Group {
Label("Ошибка", systemImage: "exclamationmark.triangle")
.disabled(true)
Text(message)
.foregroundColor(.secondary)
.lineLimit(3)
Divider()
Button("Подключить заново") {
vpnManager.connect()
}
}
}
}