diff --git a/Passepartout/Library/Sources/AppUI/Views/Extensions/EnvironmentValues+Extensions.swift b/Passepartout/Library/Sources/AppUI/Views/Extensions/EnvironmentValues+Extensions.swift new file mode 100644 index 00000000..3ccc3dbb --- /dev/null +++ b/Passepartout/Library/Sources/AppUI/Views/Extensions/EnvironmentValues+Extensions.swift @@ -0,0 +1,41 @@ +// +// EnvironmentValues+Extensions.swift +// Passepartout +// +// Created by Davide De Rosa on 10/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 SwiftUI + +extension EnvironmentValues { + var navigationPath: Binding { + get { + self[NavigationPathKey.self] + } + set { + self[NavigationPathKey.self] = newValue + } + } +} + +private struct NavigationPathKey: EnvironmentKey { + static let defaultValue: Binding = .constant(NavigationPath()) +} diff --git a/Passepartout/Library/Sources/AppUI/Views/Extensions/ModuleDraftEditing.swift b/Passepartout/Library/Sources/AppUI/Views/Extensions/ModuleDraftEditing.swift new file mode 100644 index 00000000..c50b5365 --- /dev/null +++ b/Passepartout/Library/Sources/AppUI/Views/Extensions/ModuleDraftEditing.swift @@ -0,0 +1,43 @@ +// +// ModuleDraftEditing.swift +// Passepartout +// +// Created by Davide De Rosa on 10/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 + +protocol ModuleDraftEditing { + associatedtype Draft: ModuleBuilder + + var editor: ProfileEditor { get } + + var module: Draft { get } +} + +extension ModuleDraftEditing { + + @MainActor + var draft: Binding { + editor[module] + } +} diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/DNSView.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/DNSView.swift index 7fcbfd82..561f88cb 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Modules/DNSView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/DNSView.swift @@ -28,21 +28,15 @@ import PassepartoutKit import SwiftUI import UtilsLibrary -struct DNSView: View { +struct DNSView: View, ModuleDraftEditing { @EnvironmentObject private var theme: Theme @ObservedObject - private var editor: ProfileEditor + var editor: ProfileEditor - @Binding - private var draft: DNSModule.Builder - - init(editor: ProfileEditor, module: DNSModule.Builder) { - self.editor = editor - _draft = editor.binding(forModule: module) - } + let module: DNSModule.Builder var body: some View { debugChanges() @@ -56,7 +50,7 @@ struct DNSView: View { .labelsHidden() } .themeManualInput() - .moduleView(editor: editor, draft: draft) + .moduleView(editor: editor, draft: draft.wrappedValue) } } @@ -69,21 +63,21 @@ private extension DNSView { var protocolSection: some View { Section { - Picker(Strings.Global.protocol, selection: $draft.protocolType) { + Picker(Strings.Global.protocol, selection: draft.protocolType) { ForEach(Self.allProtocols, id: \.self) { Text($0.localizedDescription) } } - switch draft.protocolType { + switch draft.wrappedValue.protocolType { case .cleartext: EmptyView() case .https: - ThemeTextField(Strings.Unlocalized.url, text: $draft.dohURL, placeholder: Strings.Unlocalized.Placeholders.dohURL) + ThemeTextField(Strings.Unlocalized.url, text: draft.dohURL, placeholder: Strings.Unlocalized.Placeholders.dohURL) .labelsHidden() case .tls: - ThemeTextField(Strings.Global.hostname, text: $draft.dotHostname, placeholder: Strings.Unlocalized.Placeholders.dotHostname) + ThemeTextField(Strings.Global.hostname, text: draft.dotHostname, placeholder: Strings.Unlocalized.Placeholders.dotHostname) .labelsHidden() } } @@ -91,7 +85,7 @@ private extension DNSView { var domainSection: some View { Group { - ThemeTextField(Strings.Global.domain, text: $draft.domainName ?? "", placeholder: Strings.Unlocalized.Placeholders.hostname) + ThemeTextField(Strings.Global.domain, text: draft.domainName ?? "", placeholder: Strings.Unlocalized.Placeholders.hostname) } .themeSection(header: Strings.Global.domain) } @@ -100,7 +94,7 @@ private extension DNSView { theme.listSection( Strings.Entities.Dns.servers, addTitle: Strings.Modules.Dns.Servers.add, - originalItems: $draft.servers, + originalItems: draft.servers, itemLabel: { if $0 { Text($1.wrappedValue) @@ -115,7 +109,7 @@ private extension DNSView { theme.listSection( Strings.Entities.Dns.searchDomains, addTitle: Strings.Modules.Dns.SearchDomains.add, - originalItems: $draft.searchDomains ?? [], + originalItems: draft.searchDomains ?? [], itemLabel: { if $0 { Text($1.wrappedValue) diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/OpenVPNModule+Extensions.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/OpenVPNModule+Extensions.swift index a2de730d..80fa95f0 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/OpenVPNModule+Extensions.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/OpenVPNModule+Extensions.swift @@ -34,7 +34,7 @@ extension OpenVPNModule.Builder: ModuleViewProviding { extension OpenVPNModule.Builder: InteractiveViewProviding { func interactiveView(with editor: ProfileEditor) -> some View { - let draft = editor.binding(forModule: self) + let draft = editor[self] return OpenVPNView.CredentialsView( isInteractive: draft.isInteractive, diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/HTTPProxyView.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/HTTPProxyView.swift index 63ac3e49..594af411 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Modules/HTTPProxyView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/HTTPProxyView.swift @@ -27,21 +27,15 @@ import PassepartoutKit import SwiftUI import UtilsLibrary -struct HTTPProxyView: View { +struct HTTPProxyView: View, ModuleDraftEditing { @EnvironmentObject private var theme: Theme @ObservedObject - private var editor: ProfileEditor + var editor: ProfileEditor - @Binding - private var draft: HTTPProxyModule.Builder - - init(editor: ProfileEditor, module: HTTPProxyModule.Builder) { - self.editor = editor - _draft = editor.binding(forModule: module) - } + let module: HTTPProxyModule.Builder var body: some View { Group { @@ -52,30 +46,30 @@ struct HTTPProxyView: View { } .labelsHidden() .themeManualInput() - .moduleView(editor: editor, draft: draft) + .moduleView(editor: editor, draft: draft.wrappedValue) } } private extension HTTPProxyView { var httpSection: some View { Group { - ThemeTextField(Strings.Global.address, text: $draft.address, placeholder: Strings.Unlocalized.Placeholders.proxyIPv4Address) - ThemeTextField(Strings.Global.port, text: $draft.port.toString(omittingZero: true), placeholder: Strings.Unlocalized.Placeholders.proxyPort) + ThemeTextField(Strings.Global.address, text: draft.address, placeholder: Strings.Unlocalized.Placeholders.proxyIPv4Address) + ThemeTextField(Strings.Global.port, text: draft.port.toString(omittingZero: true), placeholder: Strings.Unlocalized.Placeholders.proxyPort) } .themeSection(header: Strings.Unlocalized.http) } var httpsSection: some View { Group { - ThemeTextField(Strings.Global.address, text: $draft.secureAddress, placeholder: Strings.Unlocalized.Placeholders.proxyIPv4Address) - ThemeTextField(Strings.Global.port, text: $draft.securePort.toString(omittingZero: true), placeholder: Strings.Unlocalized.Placeholders.proxyPort) + ThemeTextField(Strings.Global.address, text: draft.secureAddress, placeholder: Strings.Unlocalized.Placeholders.proxyIPv4Address) + ThemeTextField(Strings.Global.port, text: draft.securePort.toString(omittingZero: true), placeholder: Strings.Unlocalized.Placeholders.proxyPort) } .themeSection(header: Strings.Unlocalized.https) } var pacSection: some View { Group { - ThemeTextField(Strings.Unlocalized.url, text: $draft.pacURLString, placeholder: Strings.Unlocalized.Placeholders.pacURL) + ThemeTextField(Strings.Unlocalized.url, text: draft.pacURLString, placeholder: Strings.Unlocalized.Placeholders.pacURL) } .themeSection(header: Strings.Unlocalized.pac) } @@ -85,7 +79,7 @@ private extension HTTPProxyView { theme.listSection( Strings.Entities.HttpProxy.bypassDomains, addTitle: Strings.Modules.HttpProxy.BypassDomains.add, - originalItems: $draft.bypassDomains, + originalItems: draft.bypassDomains, itemLabel: { if $0 { Text($1.wrappedValue) diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/IPView.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/IPView.swift index 937e6910..4ffbcf6a 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Modules/IPView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/IPView.swift @@ -27,29 +27,23 @@ import PassepartoutKit import SwiftUI import UtilsLibrary -struct IPView: View { +struct IPView: View, ModuleDraftEditing { @ObservedObject - private var editor: ProfileEditor + var editor: ProfileEditor - @Binding - private var draft: IPModule.Builder + let module: IPModule.Builder @State private var routePresentation: RoutePresentation? - init(editor: ProfileEditor, module: IPModule.Builder) { - self.editor = editor - _draft = editor.binding(forModule: module) - } - var body: some View { Group { ipSections(for: .v4) ipSections(for: .v6) interfaceSection } - .moduleView(editor: editor, draft: draft) + .moduleView(editor: editor, draft: draft.wrappedValue) .themeModal(item: $routePresentation, content: routeModal) } } @@ -137,9 +131,9 @@ private extension IPView { ThemeTextField( Strings.Unlocalized.mtu, text: Binding { - draft.mtu?.description ?? "" + draft.wrappedValue.mtu?.description ?? "" } set: { - draft.mtu = Int($0) + draft.wrappedValue.mtu = Int($0) }, placeholder: Strings.Unlocalized.Placeholders.mtu ) @@ -153,16 +147,16 @@ private extension IPView { switch family { case .v4: return Binding { - draft.ipv4 ?? IPSettings(subnet: nil) + draft.wrappedValue.ipv4 ?? IPSettings(subnet: nil) } set: { - draft.ipv4 = $0 + draft.wrappedValue.ipv4 = $0 } case .v6: return Binding { - draft.ipv6 ?? IPSettings(subnet: nil) + draft.wrappedValue.ipv6 ?? IPSettings(subnet: nil) } set: { - draft.ipv6 = $0 + draft.wrappedValue.ipv6 = $0 } } } @@ -180,31 +174,31 @@ private extension IPView { case .included(let family): switch family { case .v4: - if draft.ipv4 == nil { - draft.ipv4 = IPSettings(subnet: nil) + if draft.wrappedValue.ipv4 == nil { + draft.wrappedValue.ipv4 = IPSettings(subnet: nil) } - draft.ipv4?.include(route) + draft.wrappedValue.ipv4?.include(route) case .v6: - if draft.ipv6 == nil { - draft.ipv6 = IPSettings(subnet: nil) + if draft.wrappedValue.ipv6 == nil { + draft.wrappedValue.ipv6 = IPSettings(subnet: nil) } - draft.ipv6?.include(route) + draft.wrappedValue.ipv6?.include(route) } case .excluded(let family): switch family { case .v4: - if draft.ipv4 == nil { - draft.ipv4 = IPSettings(subnet: nil) + if draft.wrappedValue.ipv4 == nil { + draft.wrappedValue.ipv4 = IPSettings(subnet: nil) } - draft.ipv4?.exclude(route) + draft.wrappedValue.ipv4?.exclude(route) case .v6: - if draft.ipv6 == nil { - draft.ipv6 = IPSettings(subnet: nil) + if draft.wrappedValue.ipv6 == nil { + draft.wrappedValue.ipv6 = IPSettings(subnet: nil) } - draft.ipv6?.exclude(route) + draft.wrappedValue.ipv6?.exclude(route) } } } diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/OnDemandView.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/OnDemandView.swift index 79a29f1b..6ecdbb6f 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Modules/OnDemandView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/OnDemandView.swift @@ -27,7 +27,7 @@ import PassepartoutKit import SwiftUI import UtilsLibrary -struct OnDemandView: View { +struct OnDemandView: View, ModuleDraftEditing { @EnvironmentObject private var theme: Theme @@ -36,13 +36,12 @@ struct OnDemandView: View { private var iapManager: IAPManager @ObservedObject - private var editor: ProfileEditor + var editor: ProfileEditor + + let module: OnDemandModule.Builder private let wifi: Wifi - @Binding - private var draft: OnDemandModule.Builder - @State private var paywallReason: PaywallReason? @@ -52,8 +51,8 @@ struct OnDemandView: View { observer: WifiObserver? = nil ) { self.editor = editor + self.module = module wifi = Wifi(observer: observer ?? CoreLocationWifiObserver()) - _draft = editor.binding(forModule: module) } var body: some View { @@ -61,7 +60,7 @@ struct OnDemandView: View { enabledSection restrictedArea } - .moduleView(editor: editor, draft: draft) + .moduleView(editor: editor, draft: draft.wrappedValue) .modifier(PaywallModifier(reason: $paywallReason)) } } @@ -75,7 +74,7 @@ private extension OnDemandView { var enabledSection: some View { Section { - Toggle(Strings.Global.enabled, isOn: $draft.isEnabled) + Toggle(Strings.Global.enabled, isOn: draft.isEnabled) } } @@ -91,9 +90,9 @@ private extension OnDemandView { EmptyView() default: - if draft.isEnabled { + if draft.wrappedValue.isEnabled { policySection - if draft.policy != .any { + if draft.wrappedValue.policy != .any { networkSection wifiSection } @@ -102,7 +101,7 @@ private extension OnDemandView { } var policySection: some View { - Picker(Strings.Modules.OnDemand.policy, selection: $draft.policy) { + Picker(Strings.Modules.OnDemand.policy, selection: draft.policy) { ForEach(Self.allPolicies, id: \.self) { Text($0.localizedDescription) } @@ -111,16 +110,16 @@ private extension OnDemandView { } var policyFooterDescription: String { - guard draft.isEnabled else { + guard draft.wrappedValue.isEnabled else { return "" // better animation than removing footer completely } let suffix: String - switch draft.policy { + switch draft.wrappedValue.policy { case .any: suffix = Strings.Modules.OnDemand.Policy.Footer.any case .including, .excluding: - if draft.policy == .including { + if draft.wrappedValue.policy == .including { suffix = Strings.Modules.OnDemand.Policy.Footer.including } else { suffix = Strings.Modules.OnDemand.Policy.Footer.excluding @@ -132,9 +131,9 @@ private extension OnDemandView { var networkSection: some View { Group { if Utils.hasCellularData() { - Toggle(Strings.Modules.OnDemand.mobile, isOn: $draft.withMobileNetwork) + Toggle(Strings.Modules.OnDemand.mobile, isOn: draft.withMobileNetwork) } else if Utils.hasEthernet() { - Toggle(Strings.Modules.OnDemand.ethernet, isOn: $draft.withEthernetNetwork) + Toggle(Strings.Modules.OnDemand.ethernet, isOn: draft.withEthernetNetwork) } } .themeSection(header: Strings.Global.networks) @@ -173,53 +172,53 @@ private extension OnDemandView { private extension OnDemandView { var allSSIDs: Binding<[String]> { .init { - Array(draft.withSSIDs.keys) + Array(draft.wrappedValue.withSSIDs.keys) } set: { newValue in - draft.withSSIDs.forEach { + draft.wrappedValue.withSSIDs.forEach { guard newValue.contains($0.key) else { - draft.withSSIDs.removeValue(forKey: $0.key) + draft.wrappedValue.withSSIDs.removeValue(forKey: $0.key) return } } newValue.forEach { - guard draft.withSSIDs[$0] == nil else { + guard draft.wrappedValue.withSSIDs[$0] == nil else { return } - draft.withSSIDs[$0] = false + draft.wrappedValue.withSSIDs[$0] = false } } } var onSSIDs: Binding> { .init { - Set(draft.withSSIDs.filter { + Set(draft.wrappedValue.withSSIDs.filter { $0.value }.map(\.key)) } set: { newValue in - draft.withSSIDs.forEach { + draft.wrappedValue.withSSIDs.forEach { guard newValue.contains($0.key) else { - if draft.withSSIDs[$0.key] != nil { - draft.withSSIDs[$0.key] = false + if draft.wrappedValue.withSSIDs[$0.key] != nil { + draft.wrappedValue.withSSIDs[$0.key] = false } else { - draft.withSSIDs.removeValue(forKey: $0.key) + draft.wrappedValue.withSSIDs.removeValue(forKey: $0.key) } return } } newValue.forEach { - guard !(draft.withSSIDs[$0] ?? false) else { + guard !(draft.wrappedValue.withSSIDs[$0] ?? false) else { return } - draft.withSSIDs[$0] = true + draft.wrappedValue.withSSIDs[$0] = true } } } func isSSIDOn(_ ssid: String) -> Binding { .init { - draft.withSSIDs[ssid] ?? false + draft.wrappedValue.withSSIDs[ssid] ?? false } set: { - draft.withSSIDs[ssid] = $0 + draft.wrappedValue.withSSIDs[ssid] = $0 } } } @@ -228,7 +227,7 @@ private extension OnDemandView { func requestSSID(_ text: Binding) { Task { @MainActor in let ssid = try await wifi.currentSSID() - if !draft.withSSIDs.keys.contains(ssid) { + if !draft.wrappedValue.withSSIDs.keys.contains(ssid) { text.wrappedValue = ssid } } diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView.swift index 5048e11c..13bf1b0f 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView.swift @@ -26,35 +26,36 @@ import PassepartoutKit import SwiftUI -struct OpenVPNView: View { +struct OpenVPNView: View, ModuleDraftEditing { + + @Environment(\.navigationPath) + private var path @ObservedObject - private var editor: ProfileEditor + var editor: ProfileEditor + + let module: OpenVPNModule.Builder private let isServerPushed: Bool - @Binding - private var draft: OpenVPNModule.Builder - init(serverConfiguration: OpenVPN.Configuration) { let module = OpenVPNModule.Builder(configurationBuilder: serverConfiguration.builder()) let editor = ProfileEditor(modules: [module]) self.editor = editor - _draft = .constant(module) + self.module = module isServerPushed = true } init(editor: ProfileEditor, module: OpenVPNModule.Builder) { self.editor = editor - _draft = editor.binding(forModule: module) + self.module = module isServerPushed = false } var body: some View { contentView - .themeAnimation(on: draft, category: .modules) - .moduleView(editor: editor, draft: draft, withName: !isServerPushed) + .moduleView(editor: editor, draft: draft.wrappedValue, withName: !isServerPushed) .navigationDestination(for: Subroute.self, destination: destination) } } @@ -63,7 +64,7 @@ struct OpenVPNView: View { private extension OpenVPNView { var configuration: OpenVPN.Configuration.Builder { - draft.configurationBuilder ?? .init(withFallbacks: true) + draft.wrappedValue.configurationBuilder ?? .init(withFallbacks: true) } @ViewBuilder @@ -80,7 +81,8 @@ private extension OpenVPNView { VPNProviderContentModifier( providerId: providerId, selectedEntity: providerEntity, - isRequired: draft.configurationBuilder == nil, + isRequired: draft.wrappedValue.configurationBuilder == nil, + entityDestination: Subroute.providerServer, providerRows: { moduleGroup(for: providerAccountRows) } @@ -88,11 +90,11 @@ private extension OpenVPNView { } var providerId: Binding { - editor.binding(forProviderOf: draft.id) + editor.binding(forProviderOf: module.id) } var providerEntity: Binding?> { - editor.binding(forProviderEntityOf: draft.id) + editor.binding(forProviderEntityOf: module.id) } var providerAccountRows: [ModuleRow]? { @@ -101,6 +103,11 @@ private extension OpenVPNView { } private extension OpenVPNView { + func onSelectServer(server: VPNServer, preset: VPNPreset) { + providerEntity.wrappedValue = VPNEntity(server: server, preset: preset) + path.wrappedValue.removeLast() + } + func importConfiguration(from url: URL) { // TODO: #657, import draft from external URL } @@ -110,16 +117,27 @@ private extension OpenVPNView { private extension OpenVPNView { enum Subroute: Hashable { + case providerServer + case credentials } @ViewBuilder func destination(for route: Subroute) -> some View { switch route { + case .providerServer: + providerId.wrappedValue.map { + VPNProviderServerView( + providerId: $0, + configurationType: OpenVPN.Configuration.self, + onSelect: onSelectServer + ) + } + case .credentials: CredentialsView( - isInteractive: $draft.isInteractive, - credentials: $draft.credentials + isInteractive: draft.isInteractive, + credentials: draft.credentials ) } } diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/WireGuardView.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/WireGuardView.swift index 3c71e18d..0d7cf516 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Modules/WireGuardView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/WireGuardView.swift @@ -28,41 +28,16 @@ import PassepartoutKit import PassepartoutWireGuardGo import SwiftUI -struct WireGuardView: View { - private enum Subroute: Hashable { - case providerServer(id: ProviderID) - } +struct WireGuardView: View, ModuleDraftEditing { @ObservedObject - private var editor: ProfileEditor + var editor: ProfileEditor - @Binding - private var draft: WireGuardModule.Builder - -// @Binding -// private var providerId: ProviderID? -// -// @State -// private var providerServer: VPNServer? - - init(editor: ProfileEditor, module: WireGuardModule.Builder) { - self.editor = editor - _draft = editor.binding(forModule: module) -// _providerId = editor.binding(forProviderOf: module.id) - } + let module: WireGuardModule.Builder var body: some View { contentView -// .modifier(providerModifier) - .moduleView(editor: editor, draft: draft) -// .navigationDestination(for: Subroute.self) { -// switch $0 { -// case .providerServer(let id): -// VPNProviderServerView(providerId: id) { -// providerServer = $1 -// } -// } -// } + .moduleView(editor: editor, draft: draft.wrappedValue) } } @@ -70,7 +45,7 @@ struct WireGuardView: View { private extension WireGuardView { var configuration: WireGuard.Configuration.Builder { - draft.configurationBuilder ?? .default + draft.wrappedValue.configurationBuilder ?? .default } @ViewBuilder @@ -81,17 +56,6 @@ private extension WireGuardView { moduleSection(for: peersRows(for: peer), header: Strings.Modules.Wireguard.peer(index + 1)) } } - -// var providerModifier: some ViewModifier { -// ProviderPanelModifier( -// providerId: $providerId, -// selectedServer: $providerServer, -// configurationType: WireGuard.Configuration.self, -// serverRoute: { -// Subroute.providerServer(id: $0) -// } -// ) -// } } // MARK: - Subviews diff --git a/Passepartout/Library/Sources/AppUI/Views/Profile/iOS/ProfileEditView+iOS.swift b/Passepartout/Library/Sources/AppUI/Views/Profile/iOS/ProfileEditView+iOS.swift index c390c0e5..a6c15b37 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Profile/iOS/ProfileEditView+iOS.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Profile/iOS/ProfileEditView+iOS.swift @@ -71,6 +71,7 @@ struct ProfileEditView: View, Routable { .navigationTitle(Strings.Global.profile) .navigationBarBackButtonHidden(true) .navigationDestination(for: NavigationRoute.self, destination: pushDestination) + .environment(\.navigationPath, $path) } } diff --git a/Passepartout/Library/Sources/AppUI/Views/Profile/macOS/ProfileSplitView+macOS.swift b/Passepartout/Library/Sources/AppUI/Views/Profile/macOS/ProfileSplitView+macOS.swift index 2e9eeca0..793f75d5 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Profile/macOS/ProfileSplitView+macOS.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Profile/macOS/ProfileSplitView+macOS.swift @@ -36,6 +36,9 @@ struct ProfileSplitView: View, Routable { var flow: ProfileCoordinator.Flow? + @State + private var detailPath = NavigationPath() + @State private var selectedModuleId: UUID? = ModuleListView.generalModuleId @@ -52,7 +55,7 @@ struct ProfileSplitView: View, Routable { flow: flow ) } detail: { - NavigationStack { + NavigationStack(path: $detailPath) { switch selectedModuleId { case ModuleListView.generalModuleId: detailView(for: .general) @@ -62,6 +65,7 @@ struct ProfileSplitView: View, Routable { } } .toolbar(content: toolbarContent) + .environment(\.navigationPath, $detailPath) } } } diff --git a/Passepartout/Library/Sources/AppUI/Views/ProfileEditor/ProfileEditor+UI.swift b/Passepartout/Library/Sources/AppUI/Views/ProfileEditor/ProfileEditor+UI.swift index a0bbbb62..cc0da83f 100644 --- a/Passepartout/Library/Sources/AppUI/Views/ProfileEditor/ProfileEditor+UI.swift +++ b/Passepartout/Library/Sources/AppUI/Views/ProfileEditor/ProfileEditor+UI.swift @@ -51,7 +51,7 @@ extension ProfileEditor { } } - func binding(forModule module: T) -> Binding where T: ModuleBuilder { + subscript(module: T) -> Binding where T: ModuleBuilder { Binding { [weak self] in guard let foundModule = self?.module(withId: module.id) else { fatalError("Module not found in editor: \(module.id)") diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderContentModifier.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderContentModifier.swift index 4d3244f6..2373ae24 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderContentModifier.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderContentModifier.swift @@ -28,8 +28,7 @@ import PassepartoutKit import SwiftUI import UtilsLibrary -@MainActor -struct VPNProviderContentModifier: ViewModifier where Configuration: ProviderConfigurationIdentifiable & Codable, ProviderRows: View { +struct VPNProviderContentModifier: ViewModifier where Configuration: ProviderConfigurationIdentifiable & Codable, Destination: Hashable, ProviderRows: View { var apis: [APIMapper] = API.shared @@ -41,6 +40,8 @@ struct VPNProviderContentModifier: ViewModifier whe let isRequired: Bool + let entityDestination: Destination + @ViewBuilder let providerRows: ProviderRows @@ -53,7 +54,7 @@ struct VPNProviderContentModifier: ViewModifier whe entityType: VPNEntity.self, isRequired: isRequired, providerRows: { - providerServerRow + providerEntityRow providerRows }, onSelectProvider: onSelectProvider @@ -62,18 +63,8 @@ struct VPNProviderContentModifier: ViewModifier whe } private extension VPNProviderContentModifier { - var providerServerRow: some View { - NavigationLink { - providerId.map { - VPNProviderServerView( - apis: apis, - providerId: $0, - configurationType: Configuration.self, - selectedEntity: selectedEntity, - onSelect: onSelectServer - ) - } - } label: { + var providerEntityRow: some View { + NavigationLink(value: entityDestination) { HStack { Text(Strings.Global.server) if let selectedEntity { @@ -92,25 +83,29 @@ private extension VPNProviderContentModifier { selectedEntity = nil } } - - func onSelectServer(server: VPNServer, preset: VPNPreset) { - selectedEntity = VPNEntity(server: server, preset: preset) - } } // MARK: - Preview #Preview { - List { - EmptyView() - .modifier(VPNProviderContentModifier( - providerId: .constant(.hideme), - selectedEntity: .constant(nil as VPNEntity?), - isRequired: false, - providerRows: { - Text("Other") - } - )) + NavigationStack { + List { + EmptyView() + .modifier(VPNProviderContentModifier( + apis: [API.bundled], + providerId: .constant(.hideme), + selectedEntity: .constant(nil as VPNEntity?), + isRequired: false, + entityDestination: "Destination", + providerRows: { + Text("Other") + } + )) + } + .navigationTitle("Preview") + .navigationDestination(for: String.self) { + Text($0) + } } .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderServerView.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderServerView.swift index 0790a153..1ccbefc0 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderServerView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderServerView.swift @@ -33,10 +33,7 @@ struct VPNProviderServerView: View where Configuration: ProviderC @EnvironmentObject private var providerManager: ProviderManager - @Environment(\.dismiss) - private var dismiss - - let apis: [APIMapper] + var apis: [APIMapper] = API.shared let providerId: ProviderID @@ -112,14 +109,12 @@ extension VPNProviderServerView { return } onSelect(server, preset) - dismiss() } } // MARK: - Preview #Preview { - NavigationStack { VPNProviderServerView( apis: [API.bundled],