diff --git a/Passepartout/Library/Sources/AppDataProfiles/AppData+Profiles.swift b/Passepartout/Library/Sources/AppDataProfiles/AppData+Profiles.swift index 4284eadc..3977f8e8 100644 --- a/Passepartout/Library/Sources/AppDataProfiles/AppData+Profiles.swift +++ b/Passepartout/Library/Sources/AppDataProfiles/AppData+Profiles.swift @@ -28,6 +28,8 @@ import CoreData import Foundation extension AppData { + + @MainActor public static let cdProfilesModel: NSManagedObjectModel = { guard let model: NSManagedObjectModel = .mergedModel(from: [.module]) else { fatalError("Unable to build Core Data model (Profiles v3)") diff --git a/Passepartout/Library/Sources/AppUI/L10n/AppError+L10n.swift b/Passepartout/Library/Sources/AppUI/L10n/AppError+L10n.swift index 33c9e246..d5c3e78e 100644 --- a/Passepartout/Library/Sources/AppUI/L10n/AppError+L10n.swift +++ b/Passepartout/Library/Sources/AppUI/L10n/AppError+L10n.swift @@ -56,6 +56,9 @@ extension PassepartoutError: LocalizedError { return Strings.Errors.App.Passepartout.connectionModuleRequired case .corruptProviderModule: + if let ppReason = reason as? PassepartoutError, ppReason.code == .notFound { + return Strings.Errors.App.missingProviderEntity + } return Strings.Errors.App.Passepartout.corruptProviderModule(reason?.localizedDescription ?? "") case .incompatibleModules: diff --git a/Passepartout/Library/Sources/AppUI/L10n/SwiftGen+Strings.swift b/Passepartout/Library/Sources/AppUI/L10n/SwiftGen+Strings.swift index f4476cbe..6b6480f0 100644 --- a/Passepartout/Library/Sources/AppUI/L10n/SwiftGen+Strings.swift +++ b/Passepartout/Library/Sources/AppUI/L10n/SwiftGen+Strings.swift @@ -108,6 +108,8 @@ public enum Strings { public static func malformedModule(_ p1: Any, _ p2: Any) -> String { return Strings.tr("Localizable", "errors.app.malformed_module", String(describing: p1), String(describing: p2), fallback: "Module %@ is malformed. %@") } + /// No provider server selected. + public static let missingProviderEntity = Strings.tr("Localizable", "errors.app.missing_provider_entity", fallback: "No provider server selected.") public enum Passepartout { /// Routing module can only be enabled together with a connection. public static let connectionModuleRequired = Strings.tr("Localizable", "errors.app.passepartout.connection_module_required", fallback: "Routing module can only be enabled together with a connection.") diff --git a/Passepartout/Library/Sources/AppUI/Resources/en.lproj/Localizable.strings b/Passepartout/Library/Sources/AppUI/Resources/en.lproj/Localizable.strings index 3b3b0d00..42b9e822 100644 --- a/Passepartout/Library/Sources/AppUI/Resources/en.lproj/Localizable.strings +++ b/Passepartout/Library/Sources/AppUI/Resources/en.lproj/Localizable.strings @@ -248,6 +248,7 @@ "errors.app.empty_profile_name" = "Profile name is empty."; "errors.app.malformed_module" = "Module %@ is malformed. %@"; +"errors.app.missing_provider_entity" = "No provider server selected."; "errors.app.default" = "Unable to complete operation."; "errors.app.passepartout.connection_module_required" = "Routing module can only be enabled together with a connection."; "errors.app.passepartout.corrupt_provider_module" = "Unable to connect to provider server (reason=%@)."; diff --git a/Passepartout/Library/Sources/AppUI/Views/App/AppInlineCoordinator.swift b/Passepartout/Library/Sources/AppUI/Views/App/AppInlineCoordinator.swift index 7b4cb939..78c16d96 100644 --- a/Passepartout/Library/Sources/AppUI/Views/App/AppInlineCoordinator.swift +++ b/Passepartout/Library/Sources/AppUI/Views/App/AppInlineCoordinator.swift @@ -85,12 +85,14 @@ private extension AppInlineCoordinator { tunnel: tunnel, registry: registry, isImporting: $isImporting, - onEdit: { - guard let profile = profileManager.profile(withId: $0.id) else { - return + flow: .init( + onEditProfile: { + guard let profile = profileManager.profile(withId: $0.id) else { + return + } + enterDetail(of: profile) } - enterDetail(of: profile) - } + ) ) } @@ -143,7 +145,10 @@ private extension AppInlineCoordinator { } func enterDetail(of profile: Profile) { - profileEditor.editProfile(profile, isShared: profileManager.isRemotelyShared(profileWithId: profile.id)) + profileEditor.editProfile( + profile, + isShared: profileManager.isRemotelyShared(profileWithId: profile.id) + ) push(.editProfile) } diff --git a/Passepartout/Library/Sources/AppUI/Views/App/AppModalCoordinator.swift b/Passepartout/Library/Sources/AppUI/Views/App/AppModalCoordinator.swift index 27e8da34..f86519db 100644 --- a/Passepartout/Library/Sources/AppUI/Views/App/AppModalCoordinator.swift +++ b/Passepartout/Library/Sources/AppUI/Views/App/AppModalCoordinator.swift @@ -81,12 +81,14 @@ extension AppModalCoordinator { tunnel: tunnel, registry: registry, isImporting: $isImporting, - onEdit: { - guard let profile = profileManager.profile(withId: $0.id) else { - return + flow: .init( + onEditProfile: { + guard let profile = profileManager.profile(withId: $0.id) else { + return + } + enterDetail(of: profile) } - enterDetail(of: profile) - } + ) ) } @@ -135,7 +137,10 @@ extension AppModalCoordinator { func enterDetail(of profile: Profile) { profilePath = NavigationPath() - profileEditor.editProfile(profile, isShared: profileManager.isRemotelyShared(profileWithId: profile.id)) + profileEditor.editProfile( + profile, + isShared: profileManager.isRemotelyShared(profileWithId: profile.id) + ) modalRoute = .editProfile } } diff --git a/Passepartout/Library/Sources/AppUI/Views/App/ProfileContainerView.swift b/Passepartout/Library/Sources/AppUI/Views/App/ProfileContainerView.swift index ac7a2643..561b31b5 100644 --- a/Passepartout/Library/Sources/AppUI/Views/App/ProfileContainerView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/App/ProfileContainerView.swift @@ -28,7 +28,11 @@ import PassepartoutKit import SwiftUI import UtilsLibrary -struct ProfileContainerView: View, TunnelInstallationProviding { +struct ProfileContainerView: View, Routable, TunnelInstallationProviding { + struct Flow { + let onEditProfile: (ProfileHeader) -> Void + } + let layout: ProfilesLayout let profileManager: ProfileManager @@ -40,7 +44,7 @@ struct ProfileContainerView: View, TunnelInstallationProviding { @Binding var isImporting: Bool - let onEdit: (ProfileHeader) -> Void + var flow: Flow? @StateObject private var interactiveManager = InteractiveManager() @@ -77,7 +81,7 @@ private extension ProfileContainerView { tunnel: tunnel, interactiveManager: interactiveManager, errorHandler: errorHandler, - onEdit: onEdit + flow: flow ) case .grid: @@ -86,7 +90,7 @@ private extension ProfileContainerView { tunnel: tunnel, interactiveManager: interactiveManager, errorHandler: errorHandler, - onEdit: onEdit + flow: flow ) } } @@ -149,8 +153,7 @@ private struct PreviewView: View { profileManager: .mock, tunnel: .mock, registry: Registry(), - isImporting: .constant(false), - onEdit: { _ in } + isImporting: .constant(false) ) } .withMockEnvironment() diff --git a/Passepartout/Library/Sources/AppUI/Views/App/ProfileGridView.swift b/Passepartout/Library/Sources/AppUI/Views/App/ProfileGridView.swift index f8fde3db..226916e4 100644 --- a/Passepartout/Library/Sources/AppUI/Views/App/ProfileGridView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/App/ProfileGridView.swift @@ -28,7 +28,7 @@ import PassepartoutKit import SwiftUI import UtilsLibrary -struct ProfileGridView: View, TunnelInstallationProviding { +struct ProfileGridView: View, Routable, TunnelInstallationProviding { @Environment(\.isSearching) private var isSearching @@ -43,7 +43,7 @@ struct ProfileGridView: View, TunnelInstallationProviding { let errorHandler: ErrorHandler - let onEdit: (ProfileHeader) -> Void + var flow: ProfileContainerView.Flow? @State private var nextProfileId: Profile.ID? @@ -94,9 +94,7 @@ private extension ProfileGridView { interactiveManager: interactiveManager, errorHandler: errorHandler, nextProfileId: $nextProfileId, - flow: .init( - onEditProfile: onEdit - ) + flow: flow ) .contextMenu { currentProfile.map { @@ -107,7 +105,9 @@ private extension ProfileGridView { interactiveManager: interactiveManager, errorHandler: errorHandler, isInstalledProfile: true, - onEdit: onEdit + onEdit: { + flow?.onEditProfile($0) + } ) } } @@ -123,7 +123,9 @@ private extension ProfileGridView { errorHandler: errorHandler, nextProfileId: $nextProfileId, withMarker: true, - onEdit: onEdit + onEdit: { + flow?.onEditProfile($0) + } ) .themeGridCell(isSelected: header.id == nextProfileId ?? tunnel.currentProfile?.id) .contextMenu { @@ -134,7 +136,9 @@ private extension ProfileGridView { interactiveManager: interactiveManager, errorHandler: errorHandler, isInstalledProfile: false, - onEdit: onEdit + onEdit: { + flow?.onEditProfile($0) + } ) } .id(header.id) @@ -148,8 +152,7 @@ private extension ProfileGridView { profileManager: .mock, tunnel: .mock, interactiveManager: InteractiveManager(), - errorHandler: .default(), - onEdit: { _ in } + errorHandler: .default() ) .themeWindow(width: 600, height: 300) .withMockEnvironment() diff --git a/Passepartout/Library/Sources/AppUI/Views/App/ProfileListView.swift b/Passepartout/Library/Sources/AppUI/Views/App/ProfileListView.swift index f09cbd5f..8aec9eff 100644 --- a/Passepartout/Library/Sources/AppUI/Views/App/ProfileListView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/App/ProfileListView.swift @@ -28,7 +28,7 @@ import PassepartoutKit import SwiftUI import UtilsLibrary -struct ProfileListView: View, TunnelInstallationProviding { +struct ProfileListView: View, Routable, TunnelInstallationProviding { @Environment(\.horizontalSizeClass) private var hsClass @@ -49,7 +49,7 @@ struct ProfileListView: View, TunnelInstallationProviding { let errorHandler: ErrorHandler - let onEdit: (ProfileHeader) -> Void + var flow: ProfileContainerView.Flow? @State private var nextProfileId: Profile.ID? @@ -90,9 +90,7 @@ private extension ProfileListView { interactiveManager: interactiveManager, errorHandler: errorHandler, nextProfileId: $nextProfileId, - flow: .init( - onEditProfile: onEdit - ) + flow: flow ) .contextMenu { currentProfile.map { @@ -103,7 +101,9 @@ private extension ProfileListView { interactiveManager: interactiveManager, errorHandler: errorHandler, isInstalledProfile: true, - onEdit: onEdit + onEdit: { + flow?.onEditProfile($0) + } ) } } @@ -119,7 +119,9 @@ private extension ProfileListView { errorHandler: errorHandler, nextProfileId: $nextProfileId, withMarker: true, - onEdit: onEdit + onEdit: { + flow?.onEditProfile($0) + } ) .contextMenu { ProfileContextMenu( @@ -129,7 +131,9 @@ private extension ProfileListView { interactiveManager: interactiveManager, errorHandler: errorHandler, isInstalledProfile: false, - onEdit: onEdit + onEdit: { + flow?.onEditProfile($0) + } ) } .id(header.id) @@ -153,8 +157,7 @@ private extension ProfileListView { profileManager: .mock, tunnel: .mock, interactiveManager: InteractiveManager(), - errorHandler: .default(), - onEdit: { _ in } + errorHandler: .default() ) .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/DNSView.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/DNSView.swift index 599c66d5..7fcbfd82 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Modules/DNSView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/DNSView.swift @@ -28,13 +28,7 @@ import PassepartoutKit import SwiftUI import UtilsLibrary -extension DNSModule.Builder: ModuleViewProviding { - func moduleView(with editor: ProfileEditor) -> some View { - DNSView(editor: editor, module: self) - } -} - -private struct DNSView: View { +struct DNSView: View { @EnvironmentObject private var theme: Theme diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/DNSModule+Extensions.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/DNSModule+Extensions.swift new file mode 100644 index 00000000..45afa7cf --- /dev/null +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/DNSModule+Extensions.swift @@ -0,0 +1,33 @@ +// +// DNSModule+Extensions.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 PassepartoutKit +import SwiftUI + +extension DNSModule.Builder: ModuleViewProviding { + func moduleView(with editor: ProfileEditor) -> some View { + DNSView(editor: editor, module: self) + } +} diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/HTTPProxyModule+Extensions.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/HTTPProxyModule+Extensions.swift new file mode 100644 index 00000000..305a419a --- /dev/null +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/HTTPProxyModule+Extensions.swift @@ -0,0 +1,33 @@ +// +// HTTPProxyModule+Extensions.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 PassepartoutKit +import SwiftUI + +extension HTTPProxyModule.Builder: ModuleViewProviding { + func moduleView(with editor: ProfileEditor) -> some View { + HTTPProxyView(editor: editor, module: self) + } +} diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/IPModule+Extensions.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/IPModule+Extensions.swift new file mode 100644 index 00000000..4ab413fe --- /dev/null +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/IPModule+Extensions.swift @@ -0,0 +1,33 @@ +// +// IPModule+Extensions.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 PassepartoutKit +import SwiftUI + +extension IPModule.Builder: ModuleViewProviding { + func moduleView(with editor: ProfileEditor) -> some View { + IPView(editor: editor, module: self) + } +} diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/OnDemandModule+Extensions.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/OnDemandModule+Extensions.swift new file mode 100644 index 00000000..eb6b0e7b --- /dev/null +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/OnDemandModule+Extensions.swift @@ -0,0 +1,33 @@ +// +// OnDemandModule+Extensions.swift +// Passepartout +// +// Created by Davide De Rosa on 2/23/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 PassepartoutKit +import SwiftUI + +extension OnDemandModule.Builder: ModuleViewProviding { + func moduleView(with editor: ProfileEditor) -> some View { + OnDemandView(editor: editor, module: self) + } +} diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/OpenVPNModule+Extensions.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/OpenVPNModule+Extensions.swift new file mode 100644 index 00000000..a2de730d --- /dev/null +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/OpenVPNModule+Extensions.swift @@ -0,0 +1,45 @@ +// +// OpenVPNModule+Extensions.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 PassepartoutKit +import SwiftUI + +extension OpenVPNModule.Builder: ModuleViewProviding { + func moduleView(with editor: ProfileEditor) -> some View { + OpenVPNView(editor: editor, module: self) + } +} + +extension OpenVPNModule.Builder: InteractiveViewProviding { + func interactiveView(with editor: ProfileEditor) -> some View { + let draft = editor.binding(forModule: self) + + return OpenVPNView.CredentialsView( + isInteractive: draft.isInteractive, + credentials: draft.credentials, + isAuthenticating: true + ) + } +} diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/WireGuardModule+Extensions.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/WireGuardModule+Extensions.swift new file mode 100644 index 00000000..1cdccd81 --- /dev/null +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/WireGuardModule+Extensions.swift @@ -0,0 +1,33 @@ +// +// WireGuardModule+Extensions.swift +// Passepartout +// +// Created by Davide De Rosa on 7/31/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 PassepartoutKit +import SwiftUI + +extension WireGuardModule.Builder: ModuleViewProviding { + func moduleView(with editor: ProfileEditor) -> some View { + WireGuardView(editor: editor, module: self) + } +} diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/HTTPProxyView.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/HTTPProxyView.swift index d8796a78..63ac3e49 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Modules/HTTPProxyView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/HTTPProxyView.swift @@ -27,13 +27,7 @@ import PassepartoutKit import SwiftUI import UtilsLibrary -extension HTTPProxyModule.Builder: ModuleViewProviding { - func moduleView(with editor: ProfileEditor) -> some View { - HTTPProxyView(editor: editor, module: self) - } -} - -private struct HTTPProxyView: View { +struct HTTPProxyView: View { @EnvironmentObject private var theme: Theme diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/IPView.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/IPView.swift index ac0358bd..937e6910 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Modules/IPView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/IPView.swift @@ -27,12 +27,6 @@ import PassepartoutKit import SwiftUI import UtilsLibrary -extension IPModule.Builder: ModuleViewProviding { - func moduleView(with editor: ProfileEditor) -> some View { - IPView(editor: editor, module: self) - } -} - struct IPView: View { @ObservedObject diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/OnDemandView.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/OnDemandView.swift index 7b462426..79a29f1b 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Modules/OnDemandView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/OnDemandView.swift @@ -27,13 +27,7 @@ import PassepartoutKit import SwiftUI import UtilsLibrary -extension OnDemandModule.Builder: ModuleViewProviding { - func moduleView(with editor: ProfileEditor) -> some View { - OnDemandView(editor: editor, module: self) - } -} - -private struct OnDemandView: View { +struct OnDemandView: View { @EnvironmentObject private var theme: Theme diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView.swift index 98393604..5048e11c 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView.swift @@ -26,24 +26,6 @@ import PassepartoutKit import SwiftUI -extension OpenVPNModule.Builder: ModuleViewProviding { - func moduleView(with editor: ProfileEditor) -> some View { - OpenVPNView(editor: editor, module: self) - } -} - -extension OpenVPNModule.Builder: InteractiveViewProviding { - func interactiveView(with editor: ProfileEditor) -> some View { - let draft = editor.binding(forModule: self) - - return OpenVPNView.CredentialsView( - isInteractive: draft.isInteractive, - credentials: draft.credentials, - isAuthenticating: true - ) - } -} - struct OpenVPNView: View { @ObservedObject @@ -96,9 +78,8 @@ private extension OpenVPNView { var providerModifier: some ViewModifier { VPNProviderContentModifier( - providerId: editor.binding(forProviderOf: draft.id), - selectedEntity: editor.binding(forProviderEntityOf: draft.id), - configurationType: OpenVPN.Configuration.self, + providerId: providerId, + selectedEntity: providerEntity, isRequired: draft.configurationBuilder == nil, providerRows: { moduleGroup(for: providerAccountRows) @@ -106,6 +87,14 @@ private extension OpenVPNView { ) } + var providerId: Binding { + editor.binding(forProviderOf: draft.id) + } + + var providerEntity: Binding?> { + editor.binding(forProviderEntityOf: draft.id) + } + var providerAccountRows: [ModuleRow]? { [.push(caption: Strings.Modules.Openvpn.credentials, route: HashableRoute(Subroute.credentials))] } diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/WireGuardView.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/WireGuardView.swift index 0b3df84e..3c71e18d 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Modules/WireGuardView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/WireGuardView.swift @@ -28,13 +28,7 @@ import PassepartoutKit import PassepartoutWireGuardGo import SwiftUI -extension WireGuardModule.Builder: ModuleViewProviding { - func moduleView(with editor: ProfileEditor) -> some View { - WireGuardView(editor: editor, module: self) - } -} - -private struct WireGuardView: View { +struct WireGuardView: View { private enum Subroute: Hashable { case providerServer(id: ProviderID) } diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderContentModifier.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderContentModifier.swift index 02d527a8..4d3244f6 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderContentModifier.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderContentModifier.swift @@ -39,8 +39,6 @@ struct VPNProviderContentModifier: ViewModifier whe @Binding var selectedEntity: VPNEntity? - let configurationType: Configuration.Type - let isRequired: Bool @ViewBuilder @@ -107,8 +105,7 @@ private extension VPNProviderContentModifier { EmptyView() .modifier(VPNProviderContentModifier( providerId: .constant(.hideme), - selectedEntity: .constant(nil), - configurationType: OpenVPN.Configuration.self, + selectedEntity: .constant(nil as VPNEntity?), isRequired: false, providerRows: { Text("Other") diff --git a/Passepartout/Library/Sources/AppUI/Views/Theme/Theme+UI.swift b/Passepartout/Library/Sources/AppUI/Views/Theme/Theme+UI.swift index 7a6d2ad9..314e5981 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Theme/Theme+UI.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Theme/Theme+UI.swift @@ -400,6 +400,29 @@ struct ThemeImageLabel: View { } } +struct ThemeDisclosableMenu: View where NameContent: View, MenuContent: View { + + @ViewBuilder + let name: NameContent + + @ViewBuilder + let menu: () -> MenuContent + + var body: some View { + Menu(content: menu) { + HStack(alignment: .firstTextBaseline) { + name + ThemeImage(.disclose) + } + .contentShape(.rect) + } + .foregroundStyle(.primary) +#if os(macOS) + .buttonStyle(.plain) +#endif + } +} + struct ThemeCopiableText: View where Value: CustomStringConvertible, ValueView: View { @EnvironmentObject diff --git a/Passepartout/Library/Sources/AppUI/Views/UI/InstalledProfileView.swift b/Passepartout/Library/Sources/AppUI/Views/UI/InstalledProfileView.swift index f371ac55..0676652e 100644 --- a/Passepartout/Library/Sources/AppUI/Views/UI/InstalledProfileView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/UI/InstalledProfileView.swift @@ -33,10 +33,6 @@ struct InstalledProfileView: View, Routable { @EnvironmentObject var theme: Theme - struct Flow { - let onEditProfile: (ProfileHeader) -> Void - } - let layout: ProfilesLayout let profileManager: ProfileManager @@ -52,7 +48,7 @@ struct InstalledProfileView: View, Routable { @Binding var nextProfileId: Profile.ID? - var flow: Flow? + var flow: ProfileContainerView.Flow? var body: some View { debugChanges() @@ -86,21 +82,16 @@ private extension InstalledProfileView { } var actionableNameView: some View { - Menu(content: menuContent) { - HStack(alignment: .firstTextBaseline) { - nameView - ThemeImage(.disclose) - } - .contentShape(.rect) + ThemeDisclosableMenu { + nameView + } menu: { + menuContent } - .foregroundStyle(.primary) -#if os(macOS) - .buttonStyle(.plain) -#endif } var nameView: some View { Text(profile?.name ?? Strings.Views.Profiles.Rows.notInstalled) + .fixedSize() .font(.title2) .fontWeight(theme.relevantWeight) .themeTruncating(.tail) @@ -128,7 +119,7 @@ private extension InstalledProfileView { .opacity(installedOpacity) } - func menuContent() -> some View { + var menuContent: some View { ProfileContextMenu( profileManager: profileManager, tunnel: tunnel,