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
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)")

View File

@ -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:

View File

@ -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.")

View File

@ -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=%@).";

View File

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

View File

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

View File

@ -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()

View File

@ -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()

View File

@ -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()
}

View File

@ -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

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 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

View File

@ -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

View File

@ -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

View File

@ -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<ProviderID?> {
editor.binding(forProviderOf: draft.id)
}
var providerEntity: Binding<VPNEntity<OpenVPN.Configuration>?> {
editor.binding(forProviderEntityOf: draft.id)
}
var providerAccountRows: [ModuleRow]? {
[.push(caption: Strings.Modules.Openvpn.credentials, route: HashableRoute(Subroute.credentials))]
}

View File

@ -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)
}

View File

@ -39,8 +39,6 @@ struct VPNProviderContentModifier<Configuration, ProviderRows>: ViewModifier whe
@Binding
var selectedEntity: VPNEntity<Configuration>?
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<OpenVPN.Configuration>?),
isRequired: false,
providerRows: {
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 {
@EnvironmentObject

View File

@ -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) {
ThemeDisclosableMenu {
nameView
ThemeImage(.disclose)
} menu: {
menuContent
}
.contentShape(.rect)
}
.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,