// // OpenVPNView.swift // Passepartout // // Created by Davide De Rosa on 2/17/24. // Copyright (c) 2024 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn // // This file is part of Passepartout. // // Passepartout is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Passepartout is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Passepartout. If not, see . // import CommonLibrary import CommonUtils import PassepartoutKit import SwiftUI struct OpenVPNView: View, ModuleDraftEditing { @Environment(\.navigationPath) private var path let module: OpenVPNModule.Builder @ObservedObject var editor: ProfileEditor @ObservedObject var modulePreferences: ModulePreferences let impl: OpenVPNModule.Implementation? private let isServerPushed: Bool @State private var isImporting = false @State private var paywallReason: PaywallReason? @StateObject private var errorHandler: ErrorHandler = .default() init(serverConfiguration: OpenVPN.Configuration) { module = OpenVPNModule.Builder(configurationBuilder: serverConfiguration.builder()) editor = ProfileEditor(modules: [module]) modulePreferences = ModulePreferences() assert(module.configurationBuilder != nil, "isServerPushed must imply module.configurationBuilder != nil") impl = nil isServerPushed = true } init(module: OpenVPNModule.Builder, parameters: ModuleViewParameters) { self.module = module editor = parameters.editor modulePreferences = parameters.preferences impl = parameters.impl as? OpenVPNModule.Implementation isServerPushed = false } var body: some View { contentView .moduleView(editor: editor, draft: draft.wrappedValue) .modifier(ImportModifier( draft: draft, impl: impl, isImporting: $isImporting, errorHandler: errorHandler )) .modifier(PaywallModifier(reason: $paywallReason)) .navigationDestination(for: Subroute.self, destination: destination) .themeAnimation(on: draft.wrappedValue.providerId, category: .modules) .withErrorHandler(errorHandler) } } // MARK: - Content private extension OpenVPNView { @ViewBuilder var contentView: some View { if let configuration = draft.wrappedValue.configurationBuilder { ConfigurationView( isServerPushed: isServerPushed, configuration: configuration, credentialsRoute: Subroute.credentials, excludedEndpoints: excludedEndpoints ) } else { emptyConfigurationView .modifier(providerModifier) } } @ViewBuilder var emptyConfigurationView: some View { if draft.wrappedValue.providerSelection == nil { importButton } else if let configuration = try? draft.wrappedValue.providerSelection?.configuration() { providerConfigurationLink(with: configuration) } } func providerConfigurationLink(with configuration: OpenVPN.Configuration) -> some View { NavigationLink(Strings.Global.Nouns.configuration, value: Subroute.providerConfiguration(configuration)) } var importButton: some View { Button(Strings.Modules.General.Rows.importFromFile.withTrailingDots) { isImporting = true } } var providerModifier: some ViewModifier { VPNProviderContentModifier( providerId: providerId, providerPreferences: nil, selectedEntity: providerEntity, paywallReason: $paywallReason, entityDestination: Subroute.providerServer, providerRows: { moduleGroup(for: providerAccountRows) } ) } var providerAccountRows: [ModuleRow]? { [.push(caption: Strings.Modules.Openvpn.credentials, route: HashableRoute(Subroute.credentials))] } } // MARK: - Destinations private extension OpenVPNView { enum Subroute: Hashable { case providerServer case providerConfiguration(OpenVPN.Configuration) case credentials } @ViewBuilder func destination(for route: Subroute) -> some View { switch route { case .providerServer: draft.wrappedValue.providerSelection.map { VPNProviderServerView( moduleId: module.id, providerId: $0.id, selectedEntity: $0.entity, filtersWithSelection: true, onSelect: onSelectServer ) } case .providerConfiguration(let configuration): Form { ConfigurationView( isServerPushed: false, configuration: configuration.builder(), credentialsRoute: nil, excludedEndpoints: excludedEndpoints ) } .themeForm() .navigationTitle(Strings.Global.Nouns.configuration) case .credentials: Form { OpenVPNCredentialsView( providerId: draft.wrappedValue.providerId, isInteractive: draft.isInteractive, credentials: draft.credentials ) } .navigationTitle(Strings.Modules.Openvpn.credentials) .themeForm() .themeAnimation(on: draft.wrappedValue.isInteractive, category: .modules) } } } // MARK: - Logic private extension OpenVPNView { var excludedEndpoints: ObservableList { editor.excludedEndpoints(for: module.id, preferences: modulePreferences) } func onSelectServer(server: VPNServer, preset: VPNPreset) { draft.wrappedValue.providerEntity = VPNEntity(server: server, preset: preset) resetExcludedEndpointsWithCurrentProviderEntity() path.wrappedValue.removeLast() } // filter out exclusions unrelated to current server func resetExcludedEndpointsWithCurrentProviderEntity() { do { let cfg = try draft.wrappedValue.providerSelection?.configuration() editor.profile.attributes.editPreferences(inModule: module.id) { if let cfg { $0.excludedEndpoints = Set(cfg.remotes?.filter { modulePreferences.isExcludedEndpoint($0) } ?? []) } else { $0.excludedEndpoints = [] } } } catch { pp_log(.app, .error, "Unable to build provider configuration for excluded endpoints: \(error)") } } } // MARK: - Previews #Preview { let module = OpenVPNModule.Builder(configurationBuilder: .forPreviews) return module.preview(title: "OpenVPN") }