Avoid nested module navigation (#749)
A NavigationLink in VPNProviderContentModifier raised a few questions about the navigation approach in module views. It turned out that having a Binding to a local ObservedObject (ProfileEditor) is a recipe for disaster. Therefore: - We don't need a binding to the editor module (the draft), because by doing so we end up _observing_ the same changes from two properties, the binding and the editor. This seems to drive SwiftUI crazy and freezes the app once we navigate from the module to another view (e.g. in OpenVPN the credentials or the provider server). Use the module binding as a shortcut, but do not assign the binding to the view to avoid unnecessary observation. - Keep .navigationDestination() in the module view, and pass a known destination to VPNProviderContentModifier. This will save the modifier from creating a nested NavigationLink destination. The VPNProviderServerView is now openly instantiated by the module view when such destination is triggered by the NavigationLink in the modifier. - Do not implicitly dismiss VPNProviderServerView on selection, let the presenter take care. In order to do so, we add a .navigationPath environment key through which the module view can modify the current navigation stack.
This commit is contained in:
parent
a94db35d01
commit
ee8ef34f06
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension EnvironmentValues {
|
||||
var navigationPath: Binding<NavigationPath> {
|
||||
get {
|
||||
self[NavigationPathKey.self]
|
||||
}
|
||||
set {
|
||||
self[NavigationPathKey.self] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct NavigationPathKey: EnvironmentKey {
|
||||
static let defaultValue: Binding<NavigationPath> = .constant(NavigationPath())
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
protocol ModuleDraftEditing {
|
||||
associatedtype Draft: ModuleBuilder
|
||||
|
||||
var editor: ProfileEditor { get }
|
||||
|
||||
var module: Draft { get }
|
||||
}
|
||||
|
||||
extension ModuleDraftEditing {
|
||||
|
||||
@MainActor
|
||||
var draft: Binding<Draft> {
|
||||
editor[module]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Set<String>> {
|
||||
.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<Bool> {
|
||||
.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<String>) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ProviderID?> {
|
||||
editor.binding(forProviderOf: draft.id)
|
||||
editor.binding(forProviderOf: module.id)
|
||||
}
|
||||
|
||||
var providerEntity: Binding<VPNEntity<OpenVPN.Configuration>?> {
|
||||
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<OpenVPN.Configuration>) {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<WireGuard.Configuration>(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
|
||||
|
|
|
@ -71,6 +71,7 @@ struct ProfileEditView: View, Routable {
|
|||
.navigationTitle(Strings.Global.profile)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationDestination(for: NavigationRoute.self, destination: pushDestination)
|
||||
.environment(\.navigationPath, $path)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ extension ProfileEditor {
|
|||
}
|
||||
}
|
||||
|
||||
func binding<T>(forModule module: T) -> Binding<T> where T: ModuleBuilder {
|
||||
subscript<T>(module: T) -> Binding<T> where T: ModuleBuilder {
|
||||
Binding { [weak self] in
|
||||
guard let foundModule = self?.module(withId: module.id) else {
|
||||
fatalError("Module not found in editor: \(module.id)")
|
||||
|
|
|
@ -28,8 +28,7 @@ import PassepartoutKit
|
|||
import SwiftUI
|
||||
import UtilsLibrary
|
||||
|
||||
@MainActor
|
||||
struct VPNProviderContentModifier<Configuration, ProviderRows>: ViewModifier where Configuration: ProviderConfigurationIdentifiable & Codable, ProviderRows: View {
|
||||
struct VPNProviderContentModifier<Configuration, Destination, ProviderRows>: ViewModifier where Configuration: ProviderConfigurationIdentifiable & Codable, Destination: Hashable, ProviderRows: View {
|
||||
|
||||
var apis: [APIMapper] = API.shared
|
||||
|
||||
|
@ -41,6 +40,8 @@ struct VPNProviderContentModifier<Configuration, ProviderRows>: ViewModifier whe
|
|||
|
||||
let isRequired: Bool
|
||||
|
||||
let entityDestination: Destination
|
||||
|
||||
@ViewBuilder
|
||||
let providerRows: ProviderRows
|
||||
|
||||
|
@ -53,7 +54,7 @@ struct VPNProviderContentModifier<Configuration, ProviderRows>: ViewModifier whe
|
|||
entityType: VPNEntity<Configuration>.self,
|
||||
isRequired: isRequired,
|
||||
providerRows: {
|
||||
providerServerRow
|
||||
providerEntityRow
|
||||
providerRows
|
||||
},
|
||||
onSelectProvider: onSelectProvider
|
||||
|
@ -62,18 +63,8 @@ struct VPNProviderContentModifier<Configuration, ProviderRows>: ViewModifier whe
|
|||
}
|
||||
|
||||
private extension VPNProviderContentModifier {
|
||||
var providerServerRow: some View {
|
||||
NavigationLink {
|
||||
providerId.map {
|
||||
VPNProviderServerView<Configuration>(
|
||||
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<Configuration>) {
|
||||
selectedEntity = VPNEntity(server: server, preset: preset)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
List {
|
||||
EmptyView()
|
||||
.modifier(VPNProviderContentModifier(
|
||||
providerId: .constant(.hideme),
|
||||
selectedEntity: .constant(nil as VPNEntity<OpenVPN.Configuration>?),
|
||||
isRequired: false,
|
||||
providerRows: {
|
||||
Text("Other")
|
||||
}
|
||||
))
|
||||
NavigationStack {
|
||||
List {
|
||||
EmptyView()
|
||||
.modifier(VPNProviderContentModifier(
|
||||
apis: [API.bundled],
|
||||
providerId: .constant(.hideme),
|
||||
selectedEntity: .constant(nil as VPNEntity<OpenVPN.Configuration>?),
|
||||
isRequired: false,
|
||||
entityDestination: "Destination",
|
||||
providerRows: {
|
||||
Text("Other")
|
||||
}
|
||||
))
|
||||
}
|
||||
.navigationTitle("Preview")
|
||||
.navigationDestination(for: String.self) {
|
||||
Text($0)
|
||||
}
|
||||
}
|
||||
.withMockEnvironment()
|
||||
}
|
||||
|
|
|
@ -33,10 +33,7 @@ struct VPNProviderServerView<Configuration>: 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<OpenVPN.Configuration>(
|
||||
apis: [API.bundled],
|
||||
|
|
Loading…
Reference in New Issue