Добавь установку Swift-приложения VPN
This commit is contained in:
177
app/Sources/LemanaVPN/LemanaVPNApp.swift
Normal file
177
app/Sources/LemanaVPN/LemanaVPNApp.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user