178 lines
5.2 KiB
Swift
178 lines
5.2 KiB
Swift
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()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|