Добавь установку Swift-приложения VPN

This commit is contained in:
2026-05-19 12:46:32 +03:00
parent d999be49ee
commit 89e899dfa1
8 changed files with 992 additions and 6 deletions

View File

@@ -0,0 +1,177 @@
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()
}
}
}
}