import ApplicationLibrary import Libbox import Library import NetworkExtension import SwiftUI struct MainView: View { @Environment(\.scenePhase) private var scenePhase @EnvironmentObject private var environments: ExtensionEnvironments @State private var selection = NavigationPage.dashboard @State private var importProfile: LibboxProfileContent? @State private var importRemoteProfile: LibboxImportRemoteProfile? @State private var alert: AlertState? @State private var showGroups = false @State private var showConnections = false @State private var buttonState = ButtonVisibilityState() private let profileEditor: (Binding, Bool) -> AnyView = { text, isEditable in AnyView(ProfileEditorWrapperView(text: text, isEditable: isEditable)) } private var shouldShowBottomAccessory: Bool { guard !environments.extensionProfileLoading else { return false } guard !environments.emptyProfiles else { return false } guard environments.extensionProfile != nil else { return false } return true } @ViewBuilder private var tabViewContent: some View { if shouldShowBottomAccessory { if #available(iOS 26.0, *), !Variant.debugNoIOS26 { baseTabView .tabViewBottomAccessory { bottomAccessoryContent } } else { legacyTabView } } else { baseTabView } } var body: some View { if Variant.screenshotMode { mainBody.preferredColorScheme(.dark) } else { mainBody } } private var baseTabView: some View { tabView(showsBottomAccessory: false) } private var legacyTabView: some View { tabView(showsBottomAccessory: shouldShowBottomAccessory) } private func tabView(showsBottomAccessory: Bool) -> some View { TabView(selection: $selection) { ForEach(NavigationPage.allCases, id: \.self) { page in NavigationStackCompat { tabContent(for: page, showsBottomAccessory: showsBottomAccessory) } .tag(page) .tabItem { page.label } .badge(page == .tools ? environments.totalUnreadReportCount : 0) } } } @ViewBuilder private func tabContent(for page: NavigationPage, showsBottomAccessory: Bool) -> some View { if showsBottomAccessory { let content = page.contentView .navigationTitle(page.title) .tabViewBottomAccessoryCompat(useSystemAccessory: false) { bottomAccessoryContent } if page == .logs { tabBarBackgroundIfAvailable( content .navigationBarTitleDisplayMode(.inline) ) } else { tabBarBackgroundIfAvailable(content) } } else { let content = page.contentView .navigationTitle(page.title) if page == .logs { content .navigationBarTitleDisplayMode(.inline) } else { content } } } private func tabBarBackgroundIfAvailable(_ content: some View) -> some View { content } private var bottomAccessoryContent: some View { HStack(spacing: 12) { if let profile = environments.extensionProfile { StatusText(profile: profile) .frame(maxWidth: .infinity, alignment: .leading) } NavigationButtonsView( showGroupsButton: buttonState.showGroupsButton, showConnectionsButton: buttonState.showConnectionsButton, groupsCount: buttonState.groupsCount, connectionsCount: buttonState.connectionsCount, onGroupsTap: { showGroups = true }, onConnectionsTap: { showConnections = true } ) Divider() StartStopButton(showsRuntimeDuration: true) } .padding(.horizontal) .tint(.primary) } private var mainBody: some View { Group { tabViewContent .onAppear { updateButtonVisibility() } .onReceive(environments.commandClient.$groups) { _ in Task { @MainActor in updateButtonVisibility() } } .onReceive(environments.commandClient.$connections) { _ in Task { @MainActor in updateButtonVisibility() } } .onReceive(environments.commandClient.$hasAnyConnection) { _ in Task { @MainActor in updateButtonVisibility() } } .onReceive(NotificationCenter.default.publisher(for: .NEVPNStatusDidChange)) { _ in Task { @MainActor in updateButtonVisibility() } } .onReceive(environments.$extensionProfile) { _ in Task { @MainActor in updateButtonVisibility() } } .onReceive(environments.$emptyProfiles) { _ in Task { @MainActor in updateButtonVisibility() } } .sheet(isPresented: $showGroups) { GroupsSheetContent() } .sheet(isPresented: $showConnections) { ConnectionsSheetContent() } } .onAppear { environments.postReload() } .alert($alert) .globalChecks() .onChangeCompat(of: scenePhase) { newValue in if newValue == .active { environments.postReload() } } .onChangeCompat(of: selection) { newValue in if newValue == .logs { environments.connect() } } .onReceive(NotificationCenter.default.publisher(for: .reportReceived)) { _ in Task { await environments.crashReportManager.refresh() await environments.oomReportManager.refresh() selection = .tools } } .environment(\.selection, $selection) .environment(\.importProfile, $importProfile) .environment(\.importRemoteProfile, $importRemoteProfile) .environment(\.profileEditor, profileEditor) .handlesExternalEvents(preferring: [], allowing: ["*"]) .onOpenURL(perform: openURL) } private func updateButtonVisibility() { buttonState.update( profile: environments.extensionProfile, commandClient: environments.commandClient ) } private struct StatusText: View { @ObservedObject var profile: ExtensionProfile var body: some View { statusText .font(.subheadline) .foregroundStyle(.secondary) .lineLimit(1) .fixedSize() } private var statusText: Text { switch profile.status { case .disconnected: return Text("Stopped") case .connecting: return Text("Starting") case .connected: return Text("Started") case .reasserting: return Text("Reasserting") case .disconnecting: return Text("Stopping") default: return Text("Unknown") .foregroundColor(.red) } } } private func openURL(url: URL) { if url.host == "import-remote-profile" { var error: NSError? importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error) if let error { alert = AlertState(action: "parse remote profile import link", error: error) } } else if url.pathExtension == "bpf" { do { importProfile = try url.withSecurityScopedAccess { try .from(Data(contentsOf: url)) } } catch { alert = AlertState(action: "import profile from URL", error: error) } } else { alert = AlertState(errorMessage: String(localized: "Handled unknown URL \(url.absoluteString)")) } } }