import SwiftUI import NetworkExtension struct ContentView: View { @EnvironmentObject private var model: AppModel var body: some View { NavigationStack { VStack(spacing: 0) { header controls Divider() profileList footer } .background(Color(uiColor: .systemGroupedBackground)) .navigationTitle("Navis") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarTrailing) { Text( Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "3.8.2" ) .font(.caption.monospaced()) .foregroundStyle(.secondary) } } } } private var header: some View { VStack(alignment: .leading, spacing: 8) { TextField("URL подписки", text: $model.subURL) .textInputAutocapitalization(.never) .autocorrectionDisabled() .textFieldStyle(.roundedBorder) TextField("Или вставьте vless/hy2/… ссылки", text: $model.importText, axis: .vertical) .lineLimit(2...4) .textInputAutocapitalization(.never) .autocorrectionDisabled() .textFieldStyle(.roundedBorder) } .padding() } private var controls: some View { VStack(spacing: 10) { HStack(spacing: 8) { Button("Подписка") { Task { await model.importSubscription() } } .buttonStyle(.borderedProminent) .disabled(model.busy || model.subURL.trimmingCharacters(in: .whitespaces).isEmpty) Button("Ссылки") { model.importPastedLinks() } .buttonStyle(.bordered) .disabled(model.busy || model.importText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) Button("Пинг") { Task { await model.pingAll() } } .buttonStyle(.bordered) .disabled(model.busy || model.profiles.isEmpty) } HStack { Toggle("Автолучший", isOn: Binding( get: { model.autoBest }, set: { model.setAutoBest($0) } )) .disabled(model.busy) Spacer() vpnButtons } } .padding(.horizontal) .padding(.bottom, 8) } @ViewBuilder private var vpnButtons: some View { if model.vpn.canDisconnect || model.busy { Button("Отключить") { Task { await model.disconnect() } } .buttonStyle(.borderedProminent) .tint(.red) } else { Button("Подключить") { Task { await model.connectSelected() } } .buttonStyle(.borderedProminent) .disabled(model.activeID == nil) } } private var profileList: some View { List(model.profiles) { p in Button { model.select(p.id) } label: { HStack { VStack(alignment: .leading, spacing: 2) { Text(p.name) .foregroundStyle(p.protocolKind.supportedOnIOS ? .primary : .secondary) .lineLimit(1) Text(subtitle(for: p)) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) } Spacer() if let ping = model.pings[p.id] { Text(ping.ok ? "\(ping.ms) ms" : "—") .font(.caption.monospaced()) .foregroundStyle(ping.ok ? .green : .secondary) } if model.activeID == p.id { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.blue) } } } } .listStyle(.plain) } private func subtitle(for p: Profile) -> String { let base = "\(p.protocolKind.label) · \(p.host)" if p.protocolKind.supportedOnIOS { return base } return base + " · недоступен на iOS" } private var footer: some View { VStack(alignment: .leading, spacing: 4) { Text(statusLine) .font(.caption) .foregroundStyle(.secondary) Text(model.meta) .font(.footnote) .foregroundStyle(model.metaErr ? .red : .primary) .frame(maxWidth: .infinity, alignment: .leading) } .padding() .background(Color(uiColor: .secondarySystemBackground)) } private var statusLine: String { switch model.vpn.status { case .invalid: return "VPN: ожидает разрешение — нажмите Подключить (нужен VLESS/Trojan)" case .disconnected: return "VPN: отключён" case .connecting: return "VPN: подключение…" case .connected: return "VPN: подключён" case .reasserting: return "VPN: переподключение…" case .disconnecting: return "VPN: отключение…" @unknown default: return "VPN: ?" } } }