mirror of
https://github.com/passepartoutvpn/passepartout-apple.git
synced 2024-12-25 02:42:40 +00:00
7c27125dd7
Move the following dependencies: - OpenVPN/OpenSSL - WireGuard/Go up the chain until the main App/Tunnel targets, so that UILibrary and CommonLibrary can abstract from these unnecessary details. Instead, give module views access to generic implementations via Registry. Incidentally, this fixes an issue preventing TV previews from working due to OpenSSL linkage.
217 lines
5.7 KiB
Swift
217 lines
5.7 KiB
Swift
//
|
|
// AppCoordinator.swift
|
|
// Passepartout
|
|
//
|
|
// Created by Davide De Rosa on 8/13/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 CommonLibrary
|
|
import CommonUtils
|
|
import PassepartoutKit
|
|
import SwiftUI
|
|
|
|
public struct AppCoordinator: View, AppCoordinatorConforming {
|
|
|
|
@AppStorage(AppPreference.profilesLayout.key)
|
|
private var layout: ProfilesLayout = .list
|
|
|
|
private let profileManager: ProfileManager
|
|
|
|
private let tunnel: ExtendedTunnel
|
|
|
|
private let registry: Registry
|
|
|
|
@StateObject
|
|
private var profileEditor = ProfileEditor()
|
|
|
|
@State
|
|
private var modalRoute: ModalRoute?
|
|
|
|
@State
|
|
private var isImporting = false
|
|
|
|
@State
|
|
private var profilePath = NavigationPath()
|
|
|
|
@StateObject
|
|
private var errorHandler: ErrorHandler = .default()
|
|
|
|
public init(
|
|
profileManager: ProfileManager,
|
|
tunnel: ExtendedTunnel,
|
|
registry: Registry
|
|
) {
|
|
self.profileManager = profileManager
|
|
self.tunnel = tunnel
|
|
self.registry = registry
|
|
}
|
|
|
|
public var body: some View {
|
|
NavigationStack {
|
|
contentView
|
|
.toolbar(content: toolbarContent)
|
|
}
|
|
.themeModal(
|
|
item: $modalRoute,
|
|
isRoot: true,
|
|
isInteractive: modalRoute?.isInteractive ?? true,
|
|
content: modalDestination
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Destinations
|
|
|
|
extension AppCoordinator {
|
|
enum ModalRoute: Identifiable {
|
|
case editProfile
|
|
|
|
case editProviderEntity(Profile, Module, ModuleMetadata.Provider)
|
|
|
|
case settings
|
|
|
|
case about
|
|
|
|
var id: Int {
|
|
switch self {
|
|
case .editProfile: return 1
|
|
case .editProviderEntity: return 2
|
|
case .settings: return 3
|
|
case .about: return 4
|
|
}
|
|
}
|
|
|
|
var isInteractive: Bool {
|
|
switch self {
|
|
case .editProfile:
|
|
return false
|
|
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
var contentView: some View {
|
|
ProfileContainerView(
|
|
layout: layout,
|
|
profileManager: profileManager,
|
|
tunnel: tunnel,
|
|
registry: registry,
|
|
isImporting: $isImporting,
|
|
errorHandler: errorHandler,
|
|
flow: .init(
|
|
onEditProfile: {
|
|
guard let profile = profileManager.profile(withId: $0.id) else {
|
|
return
|
|
}
|
|
enterDetail(of: profile)
|
|
},
|
|
onEditProviderEntity: {
|
|
guard let pair = $0.firstProviderModuleWithMetadata else {
|
|
return
|
|
}
|
|
present(.editProviderEntity($0, pair.0, pair.1))
|
|
}
|
|
)
|
|
)
|
|
}
|
|
|
|
func toolbarContent() -> some ToolbarContent {
|
|
AppToolbar(
|
|
profileManager: profileManager,
|
|
layout: $layout,
|
|
isImporting: $isImporting,
|
|
onSettings: {
|
|
present(.settings)
|
|
},
|
|
onAbout: {
|
|
present(.about)
|
|
},
|
|
onNewProfile: enterDetail
|
|
)
|
|
}
|
|
|
|
@ViewBuilder
|
|
func modalDestination(for item: ModalRoute?) -> some View {
|
|
switch item {
|
|
case .editProfile:
|
|
ProfileCoordinator(
|
|
profileManager: profileManager,
|
|
profileEditor: profileEditor,
|
|
registry: registry,
|
|
moduleViewFactory: DefaultModuleViewFactory(registry: registry),
|
|
modally: true,
|
|
path: $profilePath,
|
|
onDismiss: {
|
|
present(nil)
|
|
}
|
|
)
|
|
|
|
case .editProviderEntity(let profile, let module, let provider):
|
|
ProviderEntitySelector(
|
|
profileManager: profileManager,
|
|
tunnel: tunnel,
|
|
profile: profile,
|
|
module: module,
|
|
provider: provider,
|
|
errorHandler: errorHandler
|
|
)
|
|
|
|
case .settings:
|
|
SettingsView(profileManager: profileManager)
|
|
|
|
case .about:
|
|
AboutRouterView(
|
|
profileManager: profileManager,
|
|
tunnel: tunnel
|
|
)
|
|
|
|
default:
|
|
EmptyView()
|
|
}
|
|
}
|
|
|
|
func enterDetail(of profile: Profile) {
|
|
profilePath = NavigationPath()
|
|
let isShared = profileManager.isRemotelyShared(profileWithId: profile.id)
|
|
profileEditor.editProfile(profile, isShared: isShared)
|
|
present(.editProfile)
|
|
}
|
|
|
|
func present(_ route: ModalRoute?) {
|
|
// XXX: this is a workaround for #791 on iOS 16
|
|
Task {
|
|
try await Task.sleep(for: .milliseconds(50))
|
|
modalRoute = route
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
AppCoordinator(
|
|
profileManager: .mock,
|
|
tunnel: .mock,
|
|
registry: Registry()
|
|
)
|
|
.withMockEnvironment()
|
|
}
|