diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index 5e20df5b..7cd87d60 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 0E0392772818732D00827C10 /* BuildProducts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0392762818732D00827C10 /* BuildProducts.swift */; }; + 0E039279281890B100827C10 /* AddHostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E039278281890B100827C10 /* AddHostView.swift */; }; 0E065F112813269500062CAF /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E065F102813269500062CAF /* WelcomeView.swift */; }; 0E0BD27327B2EA2C00583AC5 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0BD27227B2EA2C00583AC5 /* MainView.swift */; }; 0E0BD27627B2EB2200583AC5 /* DonateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0BD27527B2EB2200583AC5 /* DonateView.swift */; }; @@ -29,7 +30,7 @@ 0E34AC7C27F845510042F2AB /* OrganizerView+Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC7B27F845510042F2AB /* OrganizerView+Profiles.swift */; }; 0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC8127F892C40042F2AB /* OnDemandView+SSID.swift */; }; 0E35C09A280E95BB0071FA35 /* ProviderProfileAvailability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E35C099280E95BB0071FA35 /* ProviderProfileAvailability.swift */; }; - 0E3B7FCD27E47B3700C66F13 /* AddHostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3B7FCC27E47B3700C66F13 /* AddHostView.swift */; }; + 0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3B7FCC27E47B3700C66F13 /* AddHostView+Name.swift */; }; 0E3B7FD627E5173A00C66F13 /* ProfileView+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3B7FD527E5173A00C66F13 /* ProfileView+VPN.swift */; }; 0E3B7FDA27E51A0200C66F13 /* ProfileView+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3B7FD927E51A0200C66F13 /* ProfileView+Provider.swift */; }; 0E3CD47F280DA14B007075C0 /* OrganizerView+AddMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3CD47E280DA14B007075C0 /* OrganizerView+AddMenu.swift */; }; @@ -190,6 +191,7 @@ /* Begin PBXFileReference section */ 0E0392762818732D00827C10 /* BuildProducts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildProducts.swift; sourceTree = ""; }; + 0E039278281890B100827C10 /* AddHostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddHostView.swift; sourceTree = ""; }; 0E065F102813269500062CAF /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; 0E0BD27227B2EA2C00583AC5 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 0E0BD27527B2EB2200583AC5 /* DonateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonateView.swift; sourceTree = ""; }; @@ -214,7 +216,7 @@ 0E34AC7B27F845510042F2AB /* OrganizerView+Profiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+Profiles.swift"; sourceTree = ""; }; 0E34AC8127F892C40042F2AB /* OnDemandView+SSID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnDemandView+SSID.swift"; sourceTree = ""; }; 0E35C099280E95BB0071FA35 /* ProviderProfileAvailability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderProfileAvailability.swift; sourceTree = ""; }; - 0E3B7FCC27E47B3700C66F13 /* AddHostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddHostView.swift; sourceTree = ""; }; + 0E3B7FCC27E47B3700C66F13 /* AddHostView+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AddHostView+Name.swift"; sourceTree = ""; }; 0E3B7FD527E5173A00C66F13 /* ProfileView+VPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+VPN.swift"; sourceTree = ""; }; 0E3B7FD927E51A0200C66F13 /* ProfileView+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Provider.swift"; sourceTree = ""; }; 0E3CD47E280DA14B007075C0 /* OrganizerView+AddMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+AddMenu.swift"; sourceTree = ""; }; @@ -419,7 +421,8 @@ children = ( 0E44689B27B11B5300A14CE4 /* AboutView.swift */, 0ECF71ED27B6A99300CDB528 /* AccountView.swift */, - 0E3B7FCC27E47B3700C66F13 /* AddHostView.swift */, + 0E039278281890B100827C10 /* AddHostView.swift */, + 0E3B7FCC27E47B3700C66F13 /* AddHostView+Name.swift */, 0E90DFE527BACC1500EF5078 /* AddHostViewModel.swift */, 0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */, 0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */, @@ -901,7 +904,7 @@ 0EF708322811CC8400A3A308 /* VPNStatusText.swift in Sources */, 0E7577DD2816C3AD00081CBE /* ProfileView+Buttons.swift in Sources */, 0E5324A627D297BB002565C3 /* InApp.swift in Sources */, - 0E3B7FCD27E47B3700C66F13 /* AddHostView.swift in Sources */, + 0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */, 0E7577D72816A3B200081CBE /* DestructiveButton.swift in Sources */, 0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */, 0E35C09A280E95BB0071FA35 /* ProviderProfileAvailability.swift in Sources */, @@ -957,6 +960,7 @@ 0EBC075527EBC83800208AD9 /* MailComposerView.swift in Sources */, 0EF0FAF727DD159C007EB181 /* IntentDispatcher.swift in Sources */, 0E12BC8F27F62C8600B2F912 /* Validators.swift in Sources */, + 0E039279281890B100827C10 /* AddHostView.swift in Sources */, 0E9ED48127FD9BAE003B2316 /* CopySavingButton.swift in Sources */, 0EE11CD2280D8317003BE431 /* OrganizerView+SettingsMenu.swift in Sources */, 0E44689C27B11B5300A14CE4 /* AboutView.swift in Sources */, diff --git a/Passepartout/App/Views/AddHostView+Name.swift b/Passepartout/App/Views/AddHostView+Name.swift new file mode 100644 index 00000000..f30fbdaf --- /dev/null +++ b/Passepartout/App/Views/AddHostView+Name.swift @@ -0,0 +1,191 @@ +// +// AddHostView+Name.swift +// Passepartout +// +// Created by Davide De Rosa on 3/18/22. +// Copyright (c) 2022 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 SwiftUI +import PassepartoutCore +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 + + 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 { + if viewModel.processedProfile.isPlaceholder { + processingView + } else { + completeView + } + }.themeAnimation(on: viewModel) + }.toolbar { + themeCloseItem(isPresented: bindings.$isPresented) + ToolbarItem(placement: .primaryAction) { + Button(nextString) { + if !viewModel.processedProfile.isPlaceholder { + saveProfile() + } else { + processProfile(replacingExisting: false) + } + } + } + }.alert(isPresented: $viewModel.isAskingOverwrite, content: alertOverwriteExistingProfile) + .onAppear(perform: requestResourcePermissions) + .onDisappear(perform: dropResourcePermissions) + .navigationTitle(L10n.AddProfile.Shared.title) + .themeSecondaryView() + } + + @ViewBuilder + private var processingView: some View { + AddProfileView.ProfileNameSection( + profileName: $viewModel.profileName, + errorMessage: viewModel.errorMessage + ) { + processProfile(replacingExisting: false) + }.onAppear { + viewModel.presetName(withURL: url) + } + if viewModel.requiresPassphrase { + encryptionSection + } + let headers = profileManager.headers.sorted() + if !headers.isEmpty { + AddProfileView.ExistingProfilesSection( + headers: headers, + profileName: $viewModel.profileName + ) + } + } + + private var encryptionSection: some View { + Section( + header: Text(L10n.Global.Strings.encryption) + ) { + SecureField(L10n.AddProfile.Host.Sections.Encryption.footer, text: $viewModel.encryptionPassphrase) { + processProfile(replacingExisting: false) + } + } + } + + private var completeView: some View { + Section( + footer: themeErrorMessage(viewModel.errorMessage) + ) { + Text(L10n.Global.Strings.name) + .withTrailingText(viewModel.processedProfile.header.name) + Text(Unlocalized.Network.url) + .withTrailingText(url.lastPathComponent) + viewModel.processedProfile.vpnProtocols.first.map { + Text(L10n.Global.Strings.protocol) + .withTrailingText($0.description) + } + } + } + + private var hiddenAccountLink: some View { + NavigationLink("", isActive: $isEnteringCredentials) { + AddProfileView.AccountWrapperView( + profile: $viewModel.processedProfile, + bindings: bindings + ) + } + } + + private 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 + } + } + + private func requestResourcePermissions() { + _ = url.startAccessingSecurityScopedResource() + } + + private func dropResourcePermissions() { + url.stopAccessingSecurityScopedResource() + } + + private func alertOverwriteExistingProfile() -> Alert { + return Alert( + title: Text(L10n.AddProfile.Shared.title), + message: Text(L10n.AddProfile.Shared.Alerts.Overwrite.message), + primaryButton: .destructive(Text(L10n.Global.Strings.ok)) { + processProfile(replacingExisting: true) + }, + secondaryButton: .cancel(Text(L10n.Global.Strings.cancel)) + ) + } + + private func processProfile(replacingExisting: Bool) { + viewModel.processURL( + url, + with: profileManager, + replacingExisting: replacingExisting, + deletingURLOnSuccess: deletingURLOnSuccess + ) + } + + private 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) + } + } + } +} diff --git a/Passepartout/App/Views/AddHostView.swift b/Passepartout/App/Views/AddHostView.swift index d776cf87..bc52a6d6 100644 --- a/Passepartout/App/Views/AddHostView.swift +++ b/Passepartout/App/Views/AddHostView.swift @@ -2,7 +2,7 @@ // AddHostView.swift // Passepartout // -// Created by Davide De Rosa on 3/18/22. +// Created by Davide De Rosa on 4/26/22. // Copyright (c) 2022 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn @@ -24,166 +24,6 @@ // import SwiftUI -import PassepartoutCore -import TunnelKitOpenVPN -import TunnelKitWireGuard -struct AddHostView: 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 - - 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 { - if viewModel.processedProfile.isPlaceholder { - processingView - } else { - completeView - } - }.themeAnimation(on: viewModel) - }.toolbar { - themeCloseItem(isPresented: bindings.$isPresented) - ToolbarItem(placement: .primaryAction) { - Button(nextString) { - if !viewModel.processedProfile.isPlaceholder { - saveProfile() - } else { - processProfile(replacingExisting: false) - } - } - } - }.alert(isPresented: $viewModel.isAskingOverwrite, content: alertOverwriteExistingProfile) - .onAppear(perform: requestResourcePermissions) - .onDisappear(perform: dropResourcePermissions) - .navigationTitle(L10n.AddProfile.Shared.title) - .themeSecondaryView() - } - - @ViewBuilder - private var processingView: some View { - AddProfileView.ProfileNameSection( - profileName: $viewModel.profileName, - errorMessage: viewModel.errorMessage - ) { - processProfile(replacingExisting: false) - }.onAppear { - viewModel.presetName(withURL: url) - } - if viewModel.requiresPassphrase { - encryptionSection - } - let headers = profileManager.headers.sorted() - if !headers.isEmpty { - AddProfileView.ExistingProfilesSection( - headers: headers, - profileName: $viewModel.profileName - ) - } - } - - private var encryptionSection: some View { - Section( - header: Text(L10n.Global.Strings.encryption) - ) { - SecureField(L10n.AddProfile.Host.Sections.Encryption.footer, text: $viewModel.encryptionPassphrase) { - processProfile(replacingExisting: false) - } - } - } - - private var completeView: some View { - Section( - footer: themeErrorMessage(viewModel.errorMessage) - ) { - Text(L10n.Global.Strings.name) - .withTrailingText(viewModel.processedProfile.header.name) - Text(Unlocalized.Network.url) - .withTrailingText(url.lastPathComponent) - viewModel.processedProfile.vpnProtocols.first.map { - Text(L10n.Global.Strings.protocol) - .withTrailingText($0.description) - } - } - } - - private var hiddenAccountLink: some View { - NavigationLink("", isActive: $isEnteringCredentials) { - AddProfileView.AccountWrapperView( - profile: $viewModel.processedProfile, - bindings: bindings - ) - } - } - - private 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 - } - } - - private func requestResourcePermissions() { - _ = url.startAccessingSecurityScopedResource() - } - - private func dropResourcePermissions() { - url.stopAccessingSecurityScopedResource() - } - - private func alertOverwriteExistingProfile() -> Alert { - return Alert( - title: Text(L10n.AddProfile.Shared.title), - message: Text(L10n.AddProfile.Shared.Alerts.Overwrite.message), - primaryButton: .destructive(Text(L10n.Global.Strings.ok)) { - processProfile(replacingExisting: true) - }, - secondaryButton: .cancel(Text(L10n.Global.Strings.cancel)) - ) - } - - private func processProfile(replacingExisting: Bool) { - viewModel.processURL( - url, - with: profileManager, - replacingExisting: replacingExisting, - deletingURLOnSuccess: deletingURLOnSuccess - ) - } - - private 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) - } - } +enum AddHostView { } diff --git a/Passepartout/App/Views/OrganizerView.swift b/Passepartout/App/Views/OrganizerView.swift index f8fe3107..65c25fa8 100644 --- a/Passepartout/App/Views/OrganizerView.swift +++ b/Passepartout/App/Views/OrganizerView.swift @@ -132,7 +132,7 @@ extension OrganizerView { case .addHost(let url, let deletingURLOnSuccess): NavigationView { - AddHostView( + AddHostView.NameView( url: url, deletingURLOnSuccess: deletingURLOnSuccess, bindings: .init(