Refactor a few things about provider flows (#748)

- Move disclosable menu from installed profile view to
ThemeDisclosableMenu
- Drop unnecessary configurationType modifier parameter
- Reorg view-related module extensions to separate files
- Reuse .flow fields instead of single blocks
- Show specific error on missing provider server selection
This commit is contained in:
Davide 2024-10-22 15:06:13 +02:00 committed by GitHub
parent 39bdf145e8
commit a94db35d01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 320 additions and 113 deletions

View File

@ -28,6 +28,8 @@ import CoreData
import Foundation import Foundation
extension AppData { extension AppData {
@MainActor
public static let cdProfilesModel: NSManagedObjectModel = { public static let cdProfilesModel: NSManagedObjectModel = {
guard let model: NSManagedObjectModel = .mergedModel(from: [.module]) else { guard let model: NSManagedObjectModel = .mergedModel(from: [.module]) else {
fatalError("Unable to build Core Data model (Profiles v3)") fatalError("Unable to build Core Data model (Profiles v3)")

View File

@ -56,6 +56,9 @@ extension PassepartoutError: LocalizedError {
return Strings.Errors.App.Passepartout.connectionModuleRequired return Strings.Errors.App.Passepartout.connectionModuleRequired
case .corruptProviderModule: case .corruptProviderModule:
if let ppReason = reason as? PassepartoutError, ppReason.code == .notFound {
return Strings.Errors.App.missingProviderEntity
}
return Strings.Errors.App.Passepartout.corruptProviderModule(reason?.localizedDescription ?? "") return Strings.Errors.App.Passepartout.corruptProviderModule(reason?.localizedDescription ?? "")
case .incompatibleModules: case .incompatibleModules:

View File

@ -108,6 +108,8 @@ public enum Strings {
public static func malformedModule(_ p1: Any, _ p2: Any) -> String { 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. %@") 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 { public enum Passepartout {
/// Routing module can only be enabled together with a connection. /// 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.") public static let connectionModuleRequired = Strings.tr("Localizable", "errors.app.passepartout.connection_module_required", fallback: "Routing module can only be enabled together with a connection.")

View File

@ -248,6 +248,7 @@
"errors.app.empty_profile_name" = "Profile name is empty."; "errors.app.empty_profile_name" = "Profile name is empty.";
"errors.app.malformed_module" = "Module %@ is malformed. %@"; "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.default" = "Unable to complete operation.";
"errors.app.passepartout.connection_module_required" = "Routing module can only be enabled together with a connection."; "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=%@)."; "errors.app.passepartout.corrupt_provider_module" = "Unable to connect to provider server (reason=%@).";

View File

@ -85,13 +85,15 @@ private extension AppInlineCoordinator {
tunnel: tunnel, tunnel: tunnel,
registry: registry, registry: registry,
isImporting: $isImporting, isImporting: $isImporting,
onEdit: { flow: .init(
onEditProfile: {
guard let profile = profileManager.profile(withId: $0.id) else { guard let profile = profileManager.profile(withId: $0.id) else {
return return
} }
enterDetail(of: profile) enterDetail(of: profile)
} }
) )
)
} }
func toolbarContent() -> some ToolbarContent { func toolbarContent() -> some ToolbarContent {
@ -143,7 +145,10 @@ private extension AppInlineCoordinator {
} }
func enterDetail(of profile: Profile) { 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) push(.editProfile)
} }

View File

@ -81,13 +81,15 @@ extension AppModalCoordinator {
tunnel: tunnel, tunnel: tunnel,
registry: registry, registry: registry,
isImporting: $isImporting, isImporting: $isImporting,
onEdit: { flow: .init(
onEditProfile: {
guard let profile = profileManager.profile(withId: $0.id) else { guard let profile = profileManager.profile(withId: $0.id) else {
return return
} }
enterDetail(of: profile) enterDetail(of: profile)
} }
) )
)
} }
func toolbarContent() -> some ToolbarContent { func toolbarContent() -> some ToolbarContent {
@ -135,7 +137,10 @@ extension AppModalCoordinator {
func enterDetail(of profile: Profile) { func enterDetail(of profile: Profile) {
profilePath = NavigationPath() profilePath = NavigationPath()
profileEditor.editProfile(profile, isShared: profileManager.isRemotelyShared(profileWithId: profile.id)) profileEditor.editProfile(
profile,
isShared: profileManager.isRemotelyShared(profileWithId: profile.id)
)
modalRoute = .editProfile modalRoute = .editProfile
} }
} }

View File

@ -28,7 +28,11 @@ import PassepartoutKit
import SwiftUI import SwiftUI
import UtilsLibrary import UtilsLibrary
struct ProfileContainerView: View, TunnelInstallationProviding { struct ProfileContainerView: View, Routable, TunnelInstallationProviding {
struct Flow {
let onEditProfile: (ProfileHeader) -> Void
}
let layout: ProfilesLayout let layout: ProfilesLayout
let profileManager: ProfileManager let profileManager: ProfileManager
@ -40,7 +44,7 @@ struct ProfileContainerView: View, TunnelInstallationProviding {
@Binding @Binding
var isImporting: Bool var isImporting: Bool
let onEdit: (ProfileHeader) -> Void var flow: Flow?
@StateObject @StateObject
private var interactiveManager = InteractiveManager() private var interactiveManager = InteractiveManager()
@ -77,7 +81,7 @@ private extension ProfileContainerView {
tunnel: tunnel, tunnel: tunnel,
interactiveManager: interactiveManager, interactiveManager: interactiveManager,
errorHandler: errorHandler, errorHandler: errorHandler,
onEdit: onEdit flow: flow
) )
case .grid: case .grid:
@ -86,7 +90,7 @@ private extension ProfileContainerView {
tunnel: tunnel, tunnel: tunnel,
interactiveManager: interactiveManager, interactiveManager: interactiveManager,
errorHandler: errorHandler, errorHandler: errorHandler,
onEdit: onEdit flow: flow
) )
} }
} }
@ -149,8 +153,7 @@ private struct PreviewView: View {
profileManager: .mock, profileManager: .mock,
tunnel: .mock, tunnel: .mock,
registry: Registry(), registry: Registry(),
isImporting: .constant(false), isImporting: .constant(false)
onEdit: { _ in }
) )
} }
.withMockEnvironment() .withMockEnvironment()

View File

@ -28,7 +28,7 @@ import PassepartoutKit
import SwiftUI import SwiftUI
import UtilsLibrary import UtilsLibrary
struct ProfileGridView: View, TunnelInstallationProviding { struct ProfileGridView: View, Routable, TunnelInstallationProviding {
@Environment(\.isSearching) @Environment(\.isSearching)
private var isSearching private var isSearching
@ -43,7 +43,7 @@ struct ProfileGridView: View, TunnelInstallationProviding {
let errorHandler: ErrorHandler let errorHandler: ErrorHandler
let onEdit: (ProfileHeader) -> Void var flow: ProfileContainerView.Flow?
@State @State
private var nextProfileId: Profile.ID? private var nextProfileId: Profile.ID?
@ -94,9 +94,7 @@ private extension ProfileGridView {
interactiveManager: interactiveManager, interactiveManager: interactiveManager,
errorHandler: errorHandler, errorHandler: errorHandler,
nextProfileId: $nextProfileId, nextProfileId: $nextProfileId,
flow: .init( flow: flow
onEditProfile: onEdit
)
) )
.contextMenu { .contextMenu {
currentProfile.map { currentProfile.map {
@ -107,7 +105,9 @@ private extension ProfileGridView {
interactiveManager: interactiveManager, interactiveManager: interactiveManager,
errorHandler: errorHandler, errorHandler: errorHandler,
isInstalledProfile: true, isInstalledProfile: true,
onEdit: onEdit onEdit: {
flow?.onEditProfile($0)
}
) )
} }
} }
@ -123,7 +123,9 @@ private extension ProfileGridView {
errorHandler: errorHandler, errorHandler: errorHandler,
nextProfileId: $nextProfileId, nextProfileId: $nextProfileId,
withMarker: true, withMarker: true,
onEdit: onEdit onEdit: {
flow?.onEditProfile($0)
}
) )
.themeGridCell(isSelected: header.id == nextProfileId ?? tunnel.currentProfile?.id) .themeGridCell(isSelected: header.id == nextProfileId ?? tunnel.currentProfile?.id)
.contextMenu { .contextMenu {
@ -134,7 +136,9 @@ private extension ProfileGridView {
interactiveManager: interactiveManager, interactiveManager: interactiveManager,
errorHandler: errorHandler, errorHandler: errorHandler,
isInstalledProfile: false, isInstalledProfile: false,
onEdit: onEdit onEdit: {
flow?.onEditProfile($0)
}
) )
} }
.id(header.id) .id(header.id)
@ -148,8 +152,7 @@ private extension ProfileGridView {
profileManager: .mock, profileManager: .mock,
tunnel: .mock, tunnel: .mock,
interactiveManager: InteractiveManager(), interactiveManager: InteractiveManager(),
errorHandler: .default(), errorHandler: .default()
onEdit: { _ in }
) )
.themeWindow(width: 600, height: 300) .themeWindow(width: 600, height: 300)
.withMockEnvironment() .withMockEnvironment()

View File

@ -28,7 +28,7 @@ import PassepartoutKit
import SwiftUI import SwiftUI
import UtilsLibrary import UtilsLibrary
struct ProfileListView: View, TunnelInstallationProviding { struct ProfileListView: View, Routable, TunnelInstallationProviding {
@Environment(\.horizontalSizeClass) @Environment(\.horizontalSizeClass)
private var hsClass private var hsClass
@ -49,7 +49,7 @@ struct ProfileListView: View, TunnelInstallationProviding {
let errorHandler: ErrorHandler let errorHandler: ErrorHandler
let onEdit: (ProfileHeader) -> Void var flow: ProfileContainerView.Flow?
@State @State
private var nextProfileId: Profile.ID? private var nextProfileId: Profile.ID?
@ -90,9 +90,7 @@ private extension ProfileListView {
interactiveManager: interactiveManager, interactiveManager: interactiveManager,
errorHandler: errorHandler, errorHandler: errorHandler,
nextProfileId: $nextProfileId, nextProfileId: $nextProfileId,
flow: .init( flow: flow
onEditProfile: onEdit
)
) )
.contextMenu { .contextMenu {
currentProfile.map { currentProfile.map {
@ -103,7 +101,9 @@ private extension ProfileListView {
interactiveManager: interactiveManager, interactiveManager: interactiveManager,
errorHandler: errorHandler, errorHandler: errorHandler,
isInstalledProfile: true, isInstalledProfile: true,
onEdit: onEdit onEdit: {
flow?.onEditProfile($0)
}
) )
} }
} }
@ -119,7 +119,9 @@ private extension ProfileListView {
errorHandler: errorHandler, errorHandler: errorHandler,
nextProfileId: $nextProfileId, nextProfileId: $nextProfileId,
withMarker: true, withMarker: true,
onEdit: onEdit onEdit: {
flow?.onEditProfile($0)
}
) )
.contextMenu { .contextMenu {
ProfileContextMenu( ProfileContextMenu(
@ -129,7 +131,9 @@ private extension ProfileListView {
interactiveManager: interactiveManager, interactiveManager: interactiveManager,
errorHandler: errorHandler, errorHandler: errorHandler,
isInstalledProfile: false, isInstalledProfile: false,
onEdit: onEdit onEdit: {
flow?.onEditProfile($0)
}
) )
} }
.id(header.id) .id(header.id)
@ -153,8 +157,7 @@ private extension ProfileListView {
profileManager: .mock, profileManager: .mock,
tunnel: .mock, tunnel: .mock,
interactiveManager: InteractiveManager(), interactiveManager: InteractiveManager(),
errorHandler: .default(), errorHandler: .default()
onEdit: { _ in }
) )
.withMockEnvironment() .withMockEnvironment()
} }

View File

@ -28,13 +28,7 @@ import PassepartoutKit
import SwiftUI import SwiftUI
import UtilsLibrary import UtilsLibrary
extension DNSModule.Builder: ModuleViewProviding { struct DNSView: View {
func moduleView(with editor: ProfileEditor) -> some View {
DNSView(editor: editor, module: self)
}
}
private struct DNSView: View {
@EnvironmentObject @EnvironmentObject
private var theme: Theme private var theme: Theme

View File

@ -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 <http://www.gnu.org/licenses/>.
//
import PassepartoutKit
import SwiftUI
extension DNSModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor) -> some View {
DNSView(editor: editor, module: self)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
import PassepartoutKit
import SwiftUI
extension HTTPProxyModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor) -> some View {
HTTPProxyView(editor: editor, module: self)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
import PassepartoutKit
import SwiftUI
extension IPModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor) -> some View {
IPView(editor: editor, module: self)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
import PassepartoutKit
import SwiftUI
extension OnDemandModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor) -> some View {
OnDemandView(editor: editor, module: self)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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
)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
import PassepartoutKit
import SwiftUI
extension WireGuardModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor) -> some View {
WireGuardView(editor: editor, module: self)
}
}

View File

@ -27,13 +27,7 @@ import PassepartoutKit
import SwiftUI import SwiftUI
import UtilsLibrary import UtilsLibrary
extension HTTPProxyModule.Builder: ModuleViewProviding { struct HTTPProxyView: View {
func moduleView(with editor: ProfileEditor) -> some View {
HTTPProxyView(editor: editor, module: self)
}
}
private struct HTTPProxyView: View {
@EnvironmentObject @EnvironmentObject
private var theme: Theme private var theme: Theme

View File

@ -27,12 +27,6 @@ import PassepartoutKit
import SwiftUI import SwiftUI
import UtilsLibrary import UtilsLibrary
extension IPModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor) -> some View {
IPView(editor: editor, module: self)
}
}
struct IPView: View { struct IPView: View {
@ObservedObject @ObservedObject

View File

@ -27,13 +27,7 @@ import PassepartoutKit
import SwiftUI import SwiftUI
import UtilsLibrary import UtilsLibrary
extension OnDemandModule.Builder: ModuleViewProviding { struct OnDemandView: View {
func moduleView(with editor: ProfileEditor) -> some View {
OnDemandView(editor: editor, module: self)
}
}
private struct OnDemandView: View {
@EnvironmentObject @EnvironmentObject
private var theme: Theme private var theme: Theme

View File

@ -26,24 +26,6 @@
import PassepartoutKit import PassepartoutKit
import SwiftUI 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 { struct OpenVPNView: View {
@ObservedObject @ObservedObject
@ -96,9 +78,8 @@ private extension OpenVPNView {
var providerModifier: some ViewModifier { var providerModifier: some ViewModifier {
VPNProviderContentModifier( VPNProviderContentModifier(
providerId: editor.binding(forProviderOf: draft.id), providerId: providerId,
selectedEntity: editor.binding(forProviderEntityOf: draft.id), selectedEntity: providerEntity,
configurationType: OpenVPN.Configuration.self,
isRequired: draft.configurationBuilder == nil, isRequired: draft.configurationBuilder == nil,
providerRows: { providerRows: {
moduleGroup(for: providerAccountRows) moduleGroup(for: providerAccountRows)
@ -106,6 +87,14 @@ private extension OpenVPNView {
) )
} }
var providerId: Binding<ProviderID?> {
editor.binding(forProviderOf: draft.id)
}
var providerEntity: Binding<VPNEntity<OpenVPN.Configuration>?> {
editor.binding(forProviderEntityOf: draft.id)
}
var providerAccountRows: [ModuleRow]? { var providerAccountRows: [ModuleRow]? {
[.push(caption: Strings.Modules.Openvpn.credentials, route: HashableRoute(Subroute.credentials))] [.push(caption: Strings.Modules.Openvpn.credentials, route: HashableRoute(Subroute.credentials))]
} }

View File

@ -28,13 +28,7 @@ import PassepartoutKit
import PassepartoutWireGuardGo import PassepartoutWireGuardGo
import SwiftUI import SwiftUI
extension WireGuardModule.Builder: ModuleViewProviding { struct WireGuardView: View {
func moduleView(with editor: ProfileEditor) -> some View {
WireGuardView(editor: editor, module: self)
}
}
private struct WireGuardView: View {
private enum Subroute: Hashable { private enum Subroute: Hashable {
case providerServer(id: ProviderID) case providerServer(id: ProviderID)
} }

View File

@ -39,8 +39,6 @@ struct VPNProviderContentModifier<Configuration, ProviderRows>: ViewModifier whe
@Binding @Binding
var selectedEntity: VPNEntity<Configuration>? var selectedEntity: VPNEntity<Configuration>?
let configurationType: Configuration.Type
let isRequired: Bool let isRequired: Bool
@ViewBuilder @ViewBuilder
@ -107,8 +105,7 @@ private extension VPNProviderContentModifier {
EmptyView() EmptyView()
.modifier(VPNProviderContentModifier( .modifier(VPNProviderContentModifier(
providerId: .constant(.hideme), providerId: .constant(.hideme),
selectedEntity: .constant(nil), selectedEntity: .constant(nil as VPNEntity<OpenVPN.Configuration>?),
configurationType: OpenVPN.Configuration.self,
isRequired: false, isRequired: false,
providerRows: { providerRows: {
Text("Other") Text("Other")

View File

@ -400,6 +400,29 @@ struct ThemeImageLabel: View {
} }
} }
struct ThemeDisclosableMenu<NameContent, MenuContent>: 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<Value, ValueView>: View where Value: CustomStringConvertible, ValueView: View { struct ThemeCopiableText<Value, ValueView>: View where Value: CustomStringConvertible, ValueView: View {
@EnvironmentObject @EnvironmentObject

View File

@ -33,10 +33,6 @@ struct InstalledProfileView: View, Routable {
@EnvironmentObject @EnvironmentObject
var theme: Theme var theme: Theme
struct Flow {
let onEditProfile: (ProfileHeader) -> Void
}
let layout: ProfilesLayout let layout: ProfilesLayout
let profileManager: ProfileManager let profileManager: ProfileManager
@ -52,7 +48,7 @@ struct InstalledProfileView: View, Routable {
@Binding @Binding
var nextProfileId: Profile.ID? var nextProfileId: Profile.ID?
var flow: Flow? var flow: ProfileContainerView.Flow?
var body: some View { var body: some View {
debugChanges() debugChanges()
@ -86,21 +82,16 @@ private extension InstalledProfileView {
} }
var actionableNameView: some View { var actionableNameView: some View {
Menu(content: menuContent) { ThemeDisclosableMenu {
HStack(alignment: .firstTextBaseline) {
nameView nameView
ThemeImage(.disclose) } menu: {
menuContent
} }
.contentShape(.rect)
}
.foregroundStyle(.primary)
#if os(macOS)
.buttonStyle(.plain)
#endif
} }
var nameView: some View { var nameView: some View {
Text(profile?.name ?? Strings.Views.Profiles.Rows.notInstalled) Text(profile?.name ?? Strings.Views.Profiles.Rows.notInstalled)
.fixedSize()
.font(.title2) .font(.title2)
.fontWeight(theme.relevantWeight) .fontWeight(theme.relevantWeight)
.themeTruncating(.tail) .themeTruncating(.tail)
@ -128,7 +119,7 @@ private extension InstalledProfileView {
.opacity(installedOpacity) .opacity(installedOpacity)
} }
func menuContent() -> some View { var menuContent: some View {
ProfileContextMenu( ProfileContextMenu(
profileManager: profileManager, profileManager: profileManager,
tunnel: tunnel, tunnel: tunnel,