diff --git a/NotchBuddy/Sources/App/NetPanel.swift b/NotchBuddy/Sources/App/NetPanel.swift index 0db8e1f..48f3810 100644 --- a/NotchBuddy/Sources/App/NetPanel.swift +++ b/NotchBuddy/Sources/App/NetPanel.swift @@ -35,6 +35,9 @@ final class NetCore: ObservableObject { @Published var autoPick = "" // what "auto" chose @Published var clients: [NetClient] = [] @Published var busy = false + /// TUN on = the core carries the Mac's traffic; off = idle (e.g. back on AmneziaVPN). + @Published var tunOn = false + @Published var lastError: String? /// Delays measured by the group test (covers the subscription nodes too). private var measured: [String: Int] = [:] @@ -80,6 +83,9 @@ final class NetCore: ObservableObject { return } running = true + if let cfg = await request("/configs") as? [String: Any], let tun = cfg["tun"] as? [String: Any] { + tunOn = tun["enable"] as? Bool ?? false + } // Subscription (VLESS) nodes keep their history in the provider, not in /proxies. var providerDelay: [String: Int] = [:] if let p = await request("/providers/proxies/vless-cluster") as? [String: Any], @@ -141,6 +147,26 @@ final class NetCore: ObservableObject { await refresh() } + static var amneziaRunning: Bool { + NSWorkspace.shared.runningApplications.contains { + ($0.bundleIdentifier ?? "").lowercased().contains("amnezia") || ($0.localizedName ?? "").contains("Amnezia") + } + } + + /// Turns traffic capture on/off without a password (the core keeps running). + func setTun(_ on: Bool) async { + if on && Self.amneziaRunning { + lastError = "Сначала выключи AmneziaVPN — иначе она и ядро подерутся за маршрут." + return + } + lastError = nil + tunOn = on + _ = await request("/configs", method: "PATCH", body: ["tun": ["enable": on]], timeout: 8) + SoundEngine.shared.play(on ? "pop" : "close") + try? await Task.sleep(for: .seconds(1)) + await refresh() + } + static func pretty(_ name: String) -> (String, String) { switch name { case "auto": return ("Авто", "первый живой") @@ -214,13 +240,26 @@ struct NetPanel: View { } } + if let err = net.lastError { + Text(err).font(.system(size: 11.5)).foregroundColor(Color(hex: "#FB923C")) + .fixedSize(horizontal: false, vertical: true).frame(width: 444, alignment: .leading) + } + if !net.running { - Text("Запусти ядро: в Терминале `~/Documents/brov-secrets/start.sh` (AmneziaVPN при этом выключи). Скоро это будет делать сам BroV.") + Text("Ядро не отвечает. Если служба ещё не установлена — выключи AmneziaVPN и один раз выполни в Терминале:\nsudo sh ~/Documents/brov-secrets/install-netd.sh\nДальше ядро будет запускаться само при включении Мака.") .font(.system(size: 11.5)) .foregroundColor(Color(hex: "#B0B5BE")) + .textSelection(.enabled) .fixedSize(horizontal: false, vertical: true) - .frame(width: 440, alignment: .leading) + .frame(width: 444, alignment: .leading) } else { + NetRow(title: "Ядро перехватывает трафик", + subtitle: net.tunOn ? "весь трафик Мака идёт через BroV" : "выключено — Мак ходит сам (можно включить AmneziaVPN)", + delay: nil, isOn: net.tunOn, accent: "#A78BFA") { + Task { await net.setTun(!net.tunOn) } + } + .frame(width: 444) + Rectangle().fill(Color.white.opacity(0.08)).frame(height: 1) HStack(alignment: .top, spacing: 14) { VStack(alignment: .leading, spacing: 4) { Text("ВЫХОД В ИНТЕРНЕТ").font(.system(size: 9.5, weight: .bold)).foregroundColor(Color(hex: "#6B7079"))