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: if vpnManager.moduleStatusSystemImage == "exclamationmark.triangle" { return "exclamationmark.shield" } if vpnManager.moduleStatusSystemImage == "questionmark.circle" { return "questionmark.circle" } 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: vpnManager.moduleStatusSystemImage) .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(mode: .auto) } .keyboardShortcut("c") Button("Подключить вручную") { vpnManager.connect(mode: .manual) } .keyboardShortcut("m") Button("Подключить полностью вручную") { vpnManager.connect(mode: .manualFull) } .keyboardShortcut("f") } } 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() } } } }