// // AddHostView+Name.swift // Passepartout // // Created by Davide De Rosa on 3/18/22. // 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 PassepartoutLibrary import SwiftUI import TunnelKitOpenVPN import TunnelKitWireGuard extension AddHostView { struct NameView: View { @ObservedObject private var profileManager: ProfileManager private let url: URL private let deletingURLOnSuccess: Bool private let bindings: AddProfileView.Bindings @State private var viewModel = ViewModel() @State private var isEnteringCredentials = false @FocusState private var focusedField: AddProfileView.Field? init( url: URL, deletingURLOnSuccess: Bool, bindings: AddProfileView.Bindings ) { profileManager = .shared self.url = url self.deletingURLOnSuccess = deletingURLOnSuccess self.bindings = bindings } var body: some View { ZStack { hiddenAccountLink List { mainView }.themeAnimation(on: viewModel) }.toolbar { themeCloseItem(isPresented: bindings.$isPresented) ToolbarItem(placement: .primaryAction) { Button(nextString) { if !viewModel.processedProfile.isPlaceholder { saveProfile() } else { processProfile(replacingExisting: false) } } } }.alert( L10n.AddProfile.Shared.title, isPresented: $viewModel.isAskingOverwrite, actions: alertOverwriteActions, message: alertOverwriteMessage ).onChange(of: viewModel.requiresPassphrase) { if $0 { focusedField = .passphrase } }.onAppear(perform: requestResourcePermissions) .onDisappear(perform: dropResourcePermissions) .navigationTitle(L10n.AddProfile.Shared.title) .themeSecondaryView() } } } // MARK: - private extension AddHostView.NameView { @ViewBuilder var mainView: some View { AddProfileView.ProfileNameSection( profileName: $viewModel.profileName, focusedField: $focusedField, errorMessage: viewModel.errorMessage ) { processProfile(replacingExisting: false) }.onAppear { viewModel.presetName(withURL: url) }.disabled(isComplete) if !isComplete { if viewModel.requiresPassphrase { encryptionSection } let headers = profileManager.headers.sorted() if !headers.isEmpty { AddProfileView.ExistingProfilesSection( headers: headers, profileName: $viewModel.profileName ) } } else { completeSection } } var encryptionSection: some View { Section { SecureField(L10n.AddProfile.Host.Sections.Encryption.footer, text: $viewModel.encryptionPassphrase) { processProfile(replacingExisting: false) }.focused($focusedField, equals: .passphrase) } header: { Text(L10n.Global.Strings.encryption) } } var completeSection: some View { Section { Text(Unlocalized.Network.url) .withTrailingText(url.lastPathComponent) viewModel.processedProfile.vpnProtocols.first.map { Text(L10n.Global.Strings.protocol) .withTrailingText($0.description) } } header: { Text(L10n.AddProfile.Shared.title) } footer: { themeErrorMessage(viewModel.errorMessage) } } var hiddenAccountLink: some View { NavigationLink("", isActive: $isEnteringCredentials) { AddProfileView.AccountWrapperView( profile: $viewModel.processedProfile, bindings: bindings ) } } var nextString: String { if !viewModel.processedProfile.isPlaceholder { return viewModel.processedProfile.requiresCredentials ? L10n.Global.Strings.next : L10n.Global.Strings.save } else { return L10n.Global.Strings.next } } @ViewBuilder func alertOverwriteActions() -> some View { Button(role: .destructive) { processProfile(replacingExisting: true) } label: { Text(L10n.Global.Strings.ok) } Button(role: .cancel) { } label: { Text(L10n.Global.Strings.cancel) } } func alertOverwriteMessage() -> some View { Text(L10n.AddProfile.Shared.Alerts.Overwrite.message) } var isComplete: Bool { !viewModel.processedProfile.isPlaceholder } } // MARK: - private extension AddHostView.NameView { func requestResourcePermissions() { _ = url.startAccessingSecurityScopedResource() } func dropResourcePermissions() { url.stopAccessingSecurityScopedResource() } func processProfile(replacingExisting: Bool) { viewModel.processURL( url, with: profileManager, replacingExisting: replacingExisting, deletingURLOnSuccess: deletingURLOnSuccess ) } func saveProfile() { let result = viewModel.addProcessedProfile(to: profileManager) guard result else { return } let profile = viewModel.processedProfile if profile.requiresCredentials { isEnteringCredentials = true } else { bindings.isPresented = false profileManager.didCreateProfile.send(profile) } } }