Refactor AppUI for TV target (#775)

Split AppUI into AppUI and AppUIMain to allow for a new, simplified
AppUITV target tailored for the Apple TV.

As a PoC, present a view with a list of the shared profiles.
This commit is contained in:
Davide 2024-10-29 14:30:41 +01:00 committed by GitHub
parent 8536aee755
commit 944d6f8c28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
150 changed files with 790 additions and 332 deletions

View File

@ -23,12 +23,14 @@
0EC797422B9378E000C093B7 /* Shared+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC797402B9378E000C093B7 /* Shared+App.swift */; };
0EC797432B9378E000C093B7 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC797412B9378E000C093B7 /* Shared.swift */; };
0EC797442B93790600C093B7 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC797412B9378E000C093B7 /* Shared.swift */; };
0EC9C0232CA5BD0B00C52954 /* AppUI in Frameworks */ = {isa = PBXBuildFile; productRef = 0EC9C0222CA5BD0B00C52954 /* AppUI */; };
0ED61CF82CD0418C008FE259 /* AppDelegate+macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED61CF72CD0418C008FE259 /* AppDelegate+macOS.swift */; };
0ED61CFA2CD04192008FE259 /* AppDelegate+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED61CF92CD04192008FE259 /* AppDelegate+iOS.swift */; };
0ED61CF82CD0418C008FE259 /* App+macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED61CF72CD0418C008FE259 /* App+macOS.swift */; };
0ED61CFA2CD04192008FE259 /* App+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED61CF92CD04192008FE259 /* App+iOS.swift */; };
0EDE56EA2CABE40D0082D21C /* Intents.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0EDE56E62CABE40D0082D21C /* Intents.plist */; };
0EDE56FA2CABE42E0082D21C /* PassepartoutIntents.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = 0EDE56F02CABE42E0082D21C /* PassepartoutIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
0EDE57002CABE4B50082D21C /* IntentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDE56E72CABE40D0082D21C /* IntentsExtension.swift */; };
0EE8D7DD2CD1107E00F6600C /* AppUIMain in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, macos, ); productRef = 0EE8D7DC2CD1107E00F6600C /* AppUIMain */; };
0EE8D7DF2CD1108900F6600C /* AppUITV in Frameworks */ = {isa = PBXBuildFile; platformFilters = (tvos, ); productRef = 0EE8D7DE2CD1108900F6600C /* AppUITV */; };
0EE8D7E12CD112C200F6600C /* App+tvOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE8D7E02CD112C200F6600C /* App+tvOS.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -115,12 +117,13 @@
0EC797402B9378E000C093B7 /* Shared+App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Shared+App.swift"; sourceTree = "<group>"; };
0EC797412B9378E000C093B7 /* Shared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shared.swift; sourceTree = "<group>"; };
0ED1EFDA2C33059600CBD9BD /* App.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = App.plist; sourceTree = "<group>"; };
0ED61CF72CD0418C008FE259 /* AppDelegate+macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+macOS.swift"; sourceTree = "<group>"; };
0ED61CF92CD04192008FE259 /* AppDelegate+iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+iOS.swift"; sourceTree = "<group>"; };
0ED61CF72CD0418C008FE259 /* App+macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+macOS.swift"; sourceTree = "<group>"; };
0ED61CF92CD04192008FE259 /* App+iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+iOS.swift"; sourceTree = "<group>"; };
0EDE56E52CABE40D0082D21C /* Intents.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Intents.entitlements; sourceTree = "<group>"; };
0EDE56E62CABE40D0082D21C /* Intents.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Intents.plist; sourceTree = "<group>"; };
0EDE56E72CABE40D0082D21C /* IntentsExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntentsExtension.swift; sourceTree = "<group>"; };
0EDE56F02CABE42E0082D21C /* PassepartoutIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = PassepartoutIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; };
0EE8D7E02CD112C200F6600C /* App+tvOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+tvOS.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -144,7 +147,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0EC9C0232CA5BD0B00C52954 /* AppUI in Frameworks */,
0EE8D7DF2CD1108900F6600C /* AppUITV in Frameworks */,
0EE8D7DD2CD1107E00F6600C /* AppUIMain in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -214,8 +218,7 @@
0E7E3D5A2B9345FD002BBDB4 /* App */ = {
isa = PBXGroup;
children = (
0ED61CF52CD0416A008FE259 /* iOS */,
0ED61CF62CD04174008FE259 /* macOS */,
0ED61CF62CD04174008FE259 /* Platforms */,
0ED1EFDA2C33059600CBD9BD /* App.plist */,
0E7E3D5B2B9345FD002BBDB4 /* App.entitlements */,
0E7C3CCC2C9AF44600B72E69 /* AppDelegate.swift */,
@ -247,20 +250,14 @@
path = Tunnel;
sourceTree = "<group>";
};
0ED61CF52CD0416A008FE259 /* iOS */ = {
0ED61CF62CD04174008FE259 /* Platforms */ = {
isa = PBXGroup;
children = (
0ED61CF92CD04192008FE259 /* AppDelegate+iOS.swift */,
0ED61CF92CD04192008FE259 /* App+iOS.swift */,
0ED61CF72CD0418C008FE259 /* App+macOS.swift */,
0EE8D7E02CD112C200F6600C /* App+tvOS.swift */,
);
path = iOS;
sourceTree = "<group>";
};
0ED61CF62CD04174008FE259 /* macOS */ = {
isa = PBXGroup;
children = (
0ED61CF72CD0418C008FE259 /* AppDelegate+macOS.swift */,
);
path = macOS;
path = Platforms;
sourceTree = "<group>";
};
0EDE56E82CABE40D0082D21C /* Intents */ = {
@ -298,7 +295,8 @@
);
name = Passepartout;
packageProductDependencies = (
0EC9C0222CA5BD0B00C52954 /* AppUI */,
0EE8D7DC2CD1107E00F6600C /* AppUIMain */,
0EE8D7DE2CD1108900F6600C /* AppUITV */,
);
productName = PassepartoutKit;
productReference = 0E06D18F2B87629100176E1D /* Passepartout.app */;
@ -466,11 +464,12 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0ED61CF82CD0418C008FE259 /* AppDelegate+macOS.swift in Sources */,
0ED61CF82CD0418C008FE259 /* App+macOS.swift in Sources */,
0E7C3CCD2C9AF44600B72E69 /* AppDelegate.swift in Sources */,
0ED61CFA2CD04192008FE259 /* AppDelegate+iOS.swift in Sources */,
0ED61CFA2CD04192008FE259 /* App+iOS.swift in Sources */,
0E7E3D6B2B9345FD002BBDB4 /* PassepartoutApp.swift in Sources */,
0EC797422B9378E000C093B7 /* Shared+App.swift in Sources */,
0EE8D7E12CD112C200F6600C /* App+tvOS.swift in Sources */,
0EC797432B9378E000C093B7 /* Shared.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -621,7 +620,7 @@
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TARGETED_DEVICE_FAMILY = "1,2,3";
TVOS_DEPLOYMENT_TARGET = 17.0;
VERSIONING_SYSTEM = "apple-generic";
};
@ -697,7 +696,7 @@
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TARGETED_DEVICE_FAMILY = "1,2,3";
TVOS_DEPLOYMENT_TARGET = 17.0;
VERSIONING_SYSTEM = "apple-generic";
};
@ -1001,9 +1000,13 @@
isa = XCSwiftPackageProductDependency;
productName = TunnelLibrary;
};
0EC9C0222CA5BD0B00C52954 /* AppUI */ = {
0EE8D7DC2CD1107E00F6600C /* AppUIMain */ = {
isa = XCSwiftPackageProductDependency;
productName = AppUI;
productName = AppUIMain;
};
0EE8D7DE2CD1108900F6600C /* AppUITV */ = {
isa = XCSwiftPackageProductDependency;
productName = AppUITV;
};
/* End XCSwiftPackageProductDependency section */
};

View File

@ -23,7 +23,12 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import AppUI
#if os(iOS) || os(macOS)
import AppUIMain
#elseif os(tvOS)
import AppUITV
#endif
import CommonLibrary
import PassepartoutKit
import SwiftUI
@ -31,56 +36,34 @@ import SwiftUI
@main
struct PassepartoutApp: App {
#if os(iOS)
#if os(iOS) || os(tvOS)
@UIApplicationDelegateAdaptor
private var appDelegate: AppDelegate
#else
#elseif os(macOS)
@NSApplicationDelegateAdaptor
private var appDelegate: AppDelegate
#endif
@Environment(\.scenePhase)
private var scenePhase
private var context: AppContext {
@StateObject
var theme = Theme()
}
extension PassepartoutApp {
var appName: String {
BundleConfiguration.mainDisplayName
}
var context: AppContext {
appDelegate.context
}
private let appName = BundleConfiguration.mainDisplayName
@StateObject
private var theme = Theme()
#if os(iOS)
var body: some Scene {
WindowGroup {
contentView()
.onOpenURL { url in
ImporterPipe.shared.send([url])
}
}
}
#else
var body: some Scene {
Window(appName, id: appName, content: contentView)
.defaultSize(width: 600, height: 400)
Settings {
SettingsView(profileManager: context.profileManager)
.frame(minWidth: 300, minHeight: 200)
}
MenuBarExtra {
AppMenu()
.withEnvironment(from: context, theme: theme)
} label: {
AppMenuImage(connectionObserver: context.connectionObserver)
.environmentObject(theme)
}
}
#endif
}
private extension PassepartoutApp {
func contentView() -> some View {
AppCoordinator(
profileManager: context.profileManager,
@ -103,7 +86,6 @@ private extension PassepartoutApp {
break
}
}
.themeLockScreen()
.withEnvironment(from: context, theme: theme)
}
}

View File

@ -0,0 +1,50 @@
//
// App+iOS.swift
// Passepartout
//
// Created by Davide De Rosa on 10/28/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/>.
//
#if os(iOS)
import AppUIMain
import SwiftUI
extension AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
configure()
return true
}
}
extension PassepartoutApp {
var body: some Scene {
WindowGroup {
contentView()
.onOpenURL { url in
ImporterPipe.shared.send([url])
}
.themeLockScreen()
}
}
}
#endif

View File

@ -1,5 +1,5 @@
//
// AppDelegate+macOS.swift
// App+macOS.swift
// Passepartout
//
// Created by Davide De Rosa on 10/28/24.
@ -25,9 +25,9 @@
#if os(macOS)
import AppKit
import AppUI
import AppUIMain
import PassepartoutKit
import SwiftUI
extension AppDelegate: NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
@ -82,4 +82,28 @@ private extension AppDelegate {
}
}
extension PassepartoutApp {
@SceneBuilder
var body: some Scene {
Window(appName, id: appName) {
contentView()
.themeLockScreen()
}
.defaultSize(width: 600, height: 400)
Settings {
SettingsView(profileManager: context.profileManager)
.frame(minWidth: 300, minHeight: 200)
}
MenuBarExtra {
AppMenu()
.withEnvironment(from: context, theme: theme)
} label: {
AppMenuImage(connectionObserver: context.connectionObserver)
.environmentObject(theme)
}
}
}
#endif

View File

@ -1,8 +1,8 @@
//
// AppDelegate+iOS.swift
// App+tvOS.swift
// Passepartout
//
// Created by Davide De Rosa on 10/28/24.
// Created by Davide De Rosa on 10/29/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
@ -23,10 +23,10 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if os(iOS)
#if os(tvOS)
import AppUI
import UIKit
import AppUITV
import SwiftUI
extension AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
@ -35,4 +35,10 @@ extension AppDelegate: UIApplicationDelegate {
}
}
extension PassepartoutApp {
var body: some Scene {
WindowGroup(content: contentView)
}
}
#endif

View File

@ -0,0 +1,85 @@
{
"pins" : [
{
"identity" : "dtfoundation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Cocoanetics/DTFoundation.git",
"state" : {
"revision" : "a61be65dd7d5b2cde3acabd13bf320b71f2907a5",
"version" : "1.7.19"
}
},
{
"identity" : "generic-json-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zoul/generic-json-swift",
"state" : {
"revision" : "0a06575f4038b504e78ac330913d920f1630f510",
"version" : "2.0.2"
}
},
{
"identity" : "kvitto",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Cocoanetics/Kvitto",
"state" : {
"revision" : "88888674d772ddcf19671159ed0022cb0bc37be2",
"version" : "1.0.6"
}
},
{
"identity" : "openssl-apple",
"kind" : "remoteSourceControl",
"location" : "https://github.com/passepartoutvpn/openssl-apple",
"state" : {
"revision" : "0edc07c7a0e4ec2ca0f448dd68314241ccc925b3",
"version" : "3.2.107"
}
},
{
"identity" : "passepartoutkit-source",
"kind" : "remoteSourceControl",
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
"state" : {
"revision" : "31aff403169c7cebe91a07fb8d225ab844a9a9ff"
}
},
{
"identity" : "passepartoutkit-source-openvpn-openssl",
"kind" : "remoteSourceControl",
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl",
"state" : {
"revision" : "3e687d2348e8e1cbc214e260df73890d6420b4ec",
"version" : "0.9.1"
}
},
{
"identity" : "passepartoutkit-source-wireguard-go",
"kind" : "remoteSourceControl",
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source-wireguard-go",
"state" : {
"revision" : "8d142c806fb7dc4a2cd754d38d99da0d6398b811",
"version" : "0.9.1"
}
},
{
"identity" : "wg-go-apple",
"kind" : "remoteSourceControl",
"location" : "https://github.com/passepartoutvpn/wg-go-apple",
"state" : {
"revision" : "860e82efaf261da37483a5f51555be83e5a79ad3",
"version" : "0.0.20240714"
}
},
{
"identity" : "wireguard-apple",
"kind" : "remoteSourceControl",
"location" : "https://github.com/passepartoutvpn/wireguard-apple",
"state" : {
"revision" : "a896f784bc5ed94f29d97e376be5cfa08d4a5d44",
"version" : "1.1.1"
}
}
],
"version" : 2
}

View File

@ -21,6 +21,14 @@ let package = Package(
name: "AppUI",
targets: ["AppUI"]
),
.library(
name: "AppUIMain",
targets: ["AppUIMain"]
),
.library(
name: "AppUITV",
targets: ["AppUITV"]
),
.library(
name: "TunnelLibrary",
targets: ["CommonLibrary"]
@ -78,13 +86,26 @@ let package = Package(
"AppDataProviders",
"AppLibrary",
"Kvitto",
"LegacyV2",
"UtilsLibrary"
],
resources: [
.process("Resources")
]
),
.target(
name: "AppUIMain",
dependencies: [
"AppUI",
"LegacyV2"
],
resources: [
.process("Resources")
]
),
.target(
name: "AppUITV",
dependencies: ["AppUI"]
),
.target(
name: "CommonLibrary",
dependencies: [
@ -114,6 +135,10 @@ let package = Package(
name: "AppLibraryTests",
dependencies: ["AppLibrary"]
),
.testTarget(
name: "AppUIMainTests",
dependencies: ["AppUIMain"]
),
.testTarget(
name: "AppUITests",
dependencies: ["AppUI"]

View File

@ -24,9 +24,12 @@
//
import Foundation
import NetworkExtension
import PassepartoutKit
public protocol AppUIConfiguring {
static func configure(with context: AppContext)
}
public enum AppUI {
public static func configure(with context: AppContext) {
assertMissingModuleImplementations()
@ -36,29 +39,13 @@ public enum AppUI {
}
}
private extension AppUI {
static func assertMissingModuleImplementations() {
let providerModuleTypes: Set<ModuleType> = [
.openVPN
]
extension AppUI {
public static func assertMissingModuleImplementations() {
ModuleType.allCases.forEach { moduleType in
let builder = moduleType.newModule()
guard builder is ModuleTypeProviding else {
fatalError("\(moduleType): is not ModuleTypeProviding")
}
guard builder is any ModuleViewProviding else {
fatalError("\(moduleType): is not ModuleViewProviding")
}
if providerModuleTypes.contains(moduleType) {
do {
let module = try builder.tryBuild()
guard module is any ProviderEntityViewProviding else {
fatalError("\(moduleType): is not ProviderEntityViewProviding")
}
} catch {
fatalError("\(moduleType): empty module is not buildable")
}
}
}
}
}

View File

@ -30,7 +30,7 @@ import PassepartoutKit
@MainActor
public final class ConnectionObserver: ObservableObject {
let tunnel: Tunnel
public let tunnel: Tunnel
private let environment: TunnelEnvironment

View File

@ -30,21 +30,21 @@ import Foundation
import PassepartoutKit
@MainActor
final class ProfileEditor: ObservableObject {
public final class ProfileEditor: ObservableObject {
@Published
private var editableProfile: EditableProfile
@Published
var isShared: Bool
public var isShared: Bool
private(set) var removedModules: [UUID: any ModuleBuilder]
convenience init() {
public convenience init() {
self.init(modules: [])
}
init(modules: [any ModuleBuilder]) {
public init(modules: [any ModuleBuilder]) {
editableProfile = EditableProfile(
modules: modules,
activeModulesIds: Set(modules.map(\.id))
@ -53,13 +53,13 @@ final class ProfileEditor: ObservableObject {
removedModules = [:]
}
init(profile: Profile) {
public init(profile: Profile) {
editableProfile = profile.editable()
isShared = false
removedModules = [:]
}
func editProfile(_ profile: Profile, isShared: Bool) {
public func editProfile(_ profile: Profile, isShared: Bool) {
editableProfile = profile.editable()
self.isShared = isShared
removedModules = [:]
@ -69,7 +69,7 @@ final class ProfileEditor: ObservableObject {
// MARK: - Types
extension ProfileEditor {
var moduleTypes: [ModuleType] {
public var moduleTypes: [ModuleType] {
editableProfile.modules
.compactMap {
$0 as? ModuleTypeProviding
@ -77,7 +77,7 @@ extension ProfileEditor {
.map(\.moduleType)
}
var availableModuleTypes: [ModuleType] {
public var availableModuleTypes: [ModuleType] {
ModuleType
.allCases
.filter {
@ -97,7 +97,7 @@ extension ProfileEditor {
// MARK: - Editing
extension ProfileEditor {
var profile: EditableProfile {
public var profile: EditableProfile {
get {
editableProfile
}
@ -108,21 +108,21 @@ extension ProfileEditor {
}
extension ProfileEditor {
var modules: [any ModuleBuilder] {
public var modules: [any ModuleBuilder] {
editableProfile.modules
}
func module(withId moduleId: UUID) -> (any ModuleBuilder)? {
public func module(withId moduleId: UUID) -> (any ModuleBuilder)? {
editableProfile.modules.first {
$0.id == moduleId
} ?? removedModules[moduleId]
}
func isActiveModule(withId moduleId: UUID) -> Bool {
public func isActiveModule(withId moduleId: UUID) -> Bool {
editableProfile.isActiveModule(withId: moduleId)
}
func toggleModule(withId moduleId: UUID) {
public func toggleModule(withId moduleId: UUID) {
guard let existingModule = module(withId: moduleId) else {
return
}
@ -133,11 +133,11 @@ extension ProfileEditor {
}
}
func moveModules(from offsets: IndexSet, to newOffset: Int) {
public func moveModules(from offsets: IndexSet, to newOffset: Int) {
editableProfile.modules.move(fromOffsets: offsets, toOffset: newOffset)
}
func removeModules(at offsets: IndexSet) {
public func removeModules(at offsets: IndexSet) {
offsets.forEach {
let module = editableProfile.modules[$0]
removedModules[module.id] = module
@ -145,7 +145,7 @@ extension ProfileEditor {
}
}
func removeModule(withId moduleId: UUID) {
public func removeModule(withId moduleId: UUID) {
guard let index = editableProfile.modules.firstIndex(where: { $0.id == moduleId }) else {
return
}
@ -154,7 +154,7 @@ extension ProfileEditor {
editableProfile.modules.remove(at: index)
}
func saveModule(_ module: any ModuleBuilder, activating: Bool) {
public func saveModule(_ module: any ModuleBuilder, activating: Bool) {
if let index = editableProfile.modules.firstIndex(where: { $0.id == module.id }) {
editableProfile.modules[index] = module
} else {
@ -175,7 +175,7 @@ private extension ProfileEditor {
// MARK: - Building
extension ProfileEditor {
func build() throws -> Profile {
public func build() throws -> Profile {
let builder = try editableProfile.builder()
let profile = try builder.tryBuild()
@ -190,7 +190,7 @@ extension ProfileEditor {
// MARK: - Saving
extension ProfileEditor {
func save(to profileManager: ProfileManager) async throws {
public func save(to profileManager: ProfileManager) async throws {
do {
let newProfile = try build()
try await profileManager.save(newProfile, isShared: isShared)

View File

@ -26,7 +26,7 @@
import Foundation
import PassepartoutKit
enum AppError {
public enum AppError {
case emptyProfileName
case malformedModule(any ModuleBuilder, error: Error)
@ -35,7 +35,7 @@ enum AppError {
case generic(PassepartoutError)
init(_ error: Error) {
public init(_ error: Error) {
if let spError = error as? AppError {
self = spError
} else {

View File

@ -26,18 +26,18 @@
import Foundation
import PassepartoutKit
struct EditableProfile: MutableProfileType {
var id = UUID()
public struct EditableProfile: MutableProfileType {
public var id = UUID()
var name: String = ""
public var name: String = ""
var modules: [any ModuleBuilder] = []
public var modules: [any ModuleBuilder] = []
var activeModulesIds: Set<UUID> = []
public var activeModulesIds: Set<UUID> = []
var modulesMetadata: [UUID: ModuleMetadata]?
public var modulesMetadata: [UUID: ModuleMetadata]?
func builder() throws -> Profile.Builder {
public func builder() throws -> Profile.Builder {
var builder = Profile.Builder(id: id)
builder.modules = try modules.compactMap {
do {
@ -68,7 +68,7 @@ struct EditableProfile: MutableProfileType {
}
extension Profile {
func editable() -> EditableProfile {
public func editable() -> EditableProfile {
EditableProfile(
id: id,
name: name,
@ -78,7 +78,7 @@ extension Profile {
)
}
var modulesBuilders: [any ModuleBuilder] {
public var modulesBuilders: [any ModuleBuilder] {
modules.compactMap {
guard let buildableModule = $0 as? any BuildableType else {
return nil

View File

@ -27,7 +27,7 @@ import Foundation
import PassepartoutKit
extension ModuleType {
func newModule() -> any ModuleBuilder {
public func newModule() -> any ModuleBuilder {
switch self {
case .openVPN:
return OpenVPNModule.Builder()

View File

@ -27,7 +27,7 @@ import Foundation
import PassepartoutKit
import PassepartoutWireGuardGo
enum ModuleType: String, CaseIterable {
public enum ModuleType: String, CaseIterable {
case openVPN
case wireGuard
@ -42,7 +42,7 @@ enum ModuleType: String, CaseIterable {
}
extension ModuleType: Identifiable {
var id: String {
public var id: String {
rawValue
}
}

View File

@ -26,8 +26,13 @@
import Foundation
import PassepartoutKit
struct TunnelInstallation {
let header: ProfileHeader
public struct TunnelInstallation {
public let header: ProfileHeader
let onDemand: Bool
public let onDemand: Bool
public init(header: ProfileHeader, onDemand: Bool) {
self.header = header
self.onDemand = onDemand
}
}

View File

@ -29,17 +29,17 @@ import PassepartoutKit
@MainActor
extension Tunnel {
func install(_ profile: Profile, processor: ProfileProcessor) async throws {
public func install(_ profile: Profile, processor: ProfileProcessor) async throws {
let newProfile = try processor.processed(profile)
try await install(newProfile, connect: false, title: processor.title)
}
func connect(with profile: Profile, processor: ProfileProcessor) async throws {
public func connect(with profile: Profile, processor: ProfileProcessor) async throws {
let newProfile = try processor.processed(profile)
try await install(newProfile, connect: true, title: processor.title)
}
func currentLog(parameters: Constants.Log) async -> [String] {
public func currentLog(parameters: Constants.Log) async -> [String] {
let output = try? await sendMessage(.localLog(
sinceLast: parameters.sinceLast,
maxLevel: parameters.maxLevel

View File

@ -43,7 +43,7 @@ public final class IAPManager: ObservableObject {
private var purchasedAppBuild: Int?
private(set) var purchasedProducts: Set<AppProduct>
public private(set) var purchasedProducts: Set<AppProduct>
private var eligibleFeatures: Set<AppFeature>

View File

@ -29,7 +29,7 @@ import PassepartoutKit
import UtilsLibrary
extension AppError: LocalizedError {
var errorDescription: String? {
public var errorDescription: String? {
let V = Strings.Errors.App.self
switch self {
case .emptyProfileName:

View File

@ -29,13 +29,13 @@ import PassepartoutKit
extension ModuleBuilder {
@MainActor
func description(inEditor editor: ProfileEditor) -> String {
public func description(inEditor editor: ProfileEditor) -> String {
editor.profile.displayName(forModuleWithId: id) ?? typeDescription
}
}
extension ModuleBuilder {
var typeDescription: String {
public var typeDescription: String {
guard let providing = self as? ModuleTypeProviding else {
return String(describing: self)
}

View File

@ -28,7 +28,7 @@ import SwiftUI
import UtilsLibrary
extension ErrorHandler {
static func `default`() -> ErrorHandler {
public static func `default`() -> ErrorHandler {
ErrorHandler(
defaultTitle: Strings.Unlocalized.appName,
dismissTitle: Strings.Global.ok,

View File

@ -64,7 +64,7 @@ extension Date: StyledLocalizableEntity {
}
extension UUID {
var flatString: String {
public var flatString: String {
let str = uuidString.replacingOccurrences(of: "-", with: "")
assert(str.count == 32)
return str

View File

@ -27,9 +27,9 @@ import Foundation
import PassepartoutKit
extension Strings {
enum Unlocalized {
enum OpenVPN {
enum XOR: String {
public enum Unlocalized {
public enum OpenVPN {
public enum XOR: String {
case xormask
case xorptrpos
@ -39,23 +39,23 @@ extension Strings {
case obfuscate
}
static let compLZO = "--comp-lzo"
public static let compLZO = "--comp-lzo"
static let compress = "--compress"
public static let compress = "--compress"
static let lzo = "LZO"
public static let lzo = "LZO"
}
enum Placeholders {
static let hostname = "example.com"
public enum Placeholders {
public static let hostname = "example.com"
static let dohURL = "https://1.2.3.4/some-query"
public static let dohURL = "https://1.2.3.4/some-query"
static let dotHostname = "dns-hostname.com"
public static let dotHostname = "dns-hostname.com"
static let ipV4DNS = "1.1.1.1"
public static let ipV4DNS = "1.1.1.1"
static func ipDestination(forFamily family: Address.Family) -> String {
public static func ipDestination(forFamily family: Address.Family) -> String {
switch family {
case .v4:
return "192.168.15.0/24"
@ -65,7 +65,7 @@ extension Strings {
}
}
static func ipGateway(forFamily family: Address.Family) -> String {
public static func ipGateway(forFamily family: Address.Family) -> String {
switch family {
case .v4:
return "192.168.15.1"
@ -75,67 +75,67 @@ extension Strings {
}
}
static let mtu = "1500"
public static let mtu = "1500"
static let proxyIPv4Address = "192.168.1.1"
public static let proxyIPv4Address = "192.168.1.1"
static let proxyPort = "1080"
public static let proxyPort = "1080"
static let pacURL = "http://proxy.com/pac.url"
public static let pacURL = "http://proxy.com/pac.url"
}
enum Issues {
static let subject = "\(appName) - Report issue"
public enum Issues {
public static let subject = "\(appName) - Report issue"
static let attachmentMimeType = "text/plain"
public static let attachmentMimeType = "text/plain"
static let appLogFilename = "app.log"
public static let appLogFilename = "app.log"
static let tunnelLogFilename = "tunnel.log"
public static let tunnelLogFilename = "tunnel.log"
}
static let appName = "Passepartout"
public static let appName = "Passepartout"
static let ca = "CA"
public static let ca = "CA"
static let dns = "DNS"
public static let dns = "DNS"
static let faq = "FAQ"
public static let faq = "FAQ"
static let http = "HTTP"
public static let http = "HTTP"
static let https = "HTTPS"
public static let https = "HTTPS"
static let httpProxy = "HTTP Proxy"
public static let httpProxy = "HTTP Proxy"
static let iCloud = "iCloud"
public static let iCloud = "iCloud"
static let ip = "IP"
public static let ip = "IP"
static let ipv4 = "IPv4"
public static let ipv4 = "IPv4"
static let ipv6 = "IPv6"
public static let ipv6 = "IPv6"
static let mtu = "MTU"
public static let mtu = "MTU"
static let openVPN = "OpenVPN"
public static let openVPN = "OpenVPN"
static let otp = "OTP"
public static let otp = "OTP"
static let pac = "PAC"
public static let pac = "PAC"
static let proxy = "Proxy"
public static let proxy = "Proxy"
static let tls = "TLS"
public static let tls = "TLS"
static let url = "URL"
public static let url = "URL"
static let uuid = "UUID"
public static let uuid = "UUID"
static let wifi = "Wi-Fi"
public static let wifi = "Wi-Fi"
static let wireGuard = "WireGuard"
public static let wireGuard = "WireGuard"
static let xor = "XOR"
public static let xor = "XOR"
}
}

View File

@ -50,7 +50,7 @@ extension AppContext {
profileManager: {
let profiles: [Profile] = (0..<20)
.reduce(into: []) { list, _ in
list.append(.newProfile())
list.append(.newMockProfile())
}
return ProfileManager(profiles: profiles)
}(),
@ -109,7 +109,7 @@ extension ProviderManager {
// MARK: - Profile
extension Profile {
static let mock: Profile = {
public static let mock: Profile = {
var profile = Profile.Builder()
profile.name = "Mock profile"
@ -144,7 +144,7 @@ extension Profile {
}
}()
static func newProfile() -> Profile {
public static func newMockProfile() -> Profile {
do {
var copy = mock.builder(withNewId: true)
copy.name = String(copy.id.uuidString.prefix(8))

View File

@ -26,13 +26,13 @@
import Foundation
import PassepartoutKit
protocol TunnelContextProviding {
public protocol TunnelContextProviding {
var connectionObserver: ConnectionObserver { get }
}
@MainActor
extension TunnelContextProviding {
var tunnelConnectionStatus: TunnelStatus {
public var tunnelConnectionStatus: TunnelStatus {
var status = connectionObserver.tunnel.status
if status == .active, let connectionStatus = connectionObserver.connectionStatus {
if connectionStatus == .connected {

View File

@ -27,7 +27,7 @@ import AppLibrary
import Foundation
import PassepartoutKit
protocol TunnelInstallationProviding {
public protocol TunnelInstallationProviding {
var profileManager: ProfileManager { get }
var tunnel: Tunnel { get }
@ -35,7 +35,7 @@ protocol TunnelInstallationProviding {
@MainActor
extension TunnelInstallationProviding {
var installation: TunnelInstallation? {
public var installation: TunnelInstallation? {
guard let currentProfile = tunnel.currentProfile else {
return nil
}
@ -47,7 +47,7 @@ extension TunnelInstallationProviding {
return TunnelInstallation(header: header, onDemand: currentProfile.onDemand)
}
var currentProfile: Profile? {
public var currentProfile: Profile? {
guard let id = tunnel.currentProfile?.id else {
return nil
}

View File

@ -84,20 +84,20 @@ extension ThemeSectionWithHeaderFooterModifier {
// MARK: - Views
extension ThemeTappableText {
var body: some View {
public var body: some View {
commonView
.foregroundStyle(.primary)
}
}
extension ThemeTextField {
var body: some View {
public var body: some View {
commonView
}
}
extension ThemeSecureField {
var body: some View {
public var body: some View {
commonView
}
}
@ -109,13 +109,13 @@ extension ThemeRemovableItemRow {
}
extension ThemeEditableListSection.RemoveLabel {
var body: some View {
public var body: some View {
EmptyView()
}
}
extension ThemeEditableListSection.EditLabel {
var body: some View {
public var body: some View {
EmptyView()
}
}

View File

@ -91,7 +91,7 @@ extension ThemeSectionWithHeaderFooterModifier {
// MARK: - Views
extension ThemeTappableText {
var body: some View {
public var body: some View {
commonView
.buttonStyle(.plain)
.cursor(.hand)
@ -99,14 +99,14 @@ extension ThemeTappableText {
}
extension ThemeTextField {
var body: some View {
public var body: some View {
commonView
.labelsHidden()
}
}
extension ThemeSecureField {
var body: some View {
public var body: some View {
commonView
.labelsHidden()
}
@ -122,7 +122,7 @@ extension ThemeRemovableItemRow {
}
extension ThemeEditableListSection.RemoveLabel {
var body: some View {
public var body: some View {
Button(action: action) {
ThemeImage(.editableSectionRemove)
}
@ -131,7 +131,7 @@ extension ThemeEditableListSection.RemoveLabel {
}
extension ThemeEditableListSection.EditLabel {
var body: some View {
public var body: some View {
ThemeImage(.editableSectionEdit)
}
}

View File

@ -0,0 +1,36 @@
//
// Theme+tvOS.swift
// Passepartout
//
// Created by Davide De Rosa on 10/29/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/>.
//
#if os(tvOS)
import SwiftUI
extension Theme {
public convenience init() {
self.init(dummy: Void())
}
}
#endif

View File

@ -24,12 +24,16 @@
//
import CommonLibrary
#if canImport(LocalAuthentication)
import LocalAuthentication
#endif
import SwiftUI
import UtilsLibrary
// MARK: - Modifiers
#if !os(tvOS)
struct ThemeWindowModifier: ViewModifier {
let size: CGSize
}
@ -381,6 +385,8 @@ struct ThemeTipModifier: ViewModifier {
}
}
#endif
// MARK: - Views
public enum ThemeAnimationCategory: CaseIterable {
@ -395,23 +401,23 @@ public enum ThemeAnimationCategory: CaseIterable {
case providers
}
struct ThemeImage: View {
public struct ThemeImage: View {
@EnvironmentObject
private var theme: Theme
private let name: Theme.ImageName
init(_ name: Theme.ImageName) {
public init(_ name: Theme.ImageName) {
self.name = name
}
var body: some View {
public var body: some View {
Image(systemName: theme.systemImageName(name))
}
}
struct ThemeImageLabel: View {
public struct ThemeImageLabel: View {
@EnvironmentObject
private var theme: Theme
@ -420,12 +426,12 @@ struct ThemeImageLabel: View {
private let name: Theme.ImageName
init(_ title: String, _ name: Theme.ImageName) {
public init(_ title: String, _ name: Theme.ImageName) {
self.title = title
self.name = name
}
var body: some View {
public var body: some View {
Label {
Text(title)
} icon: {
@ -434,34 +440,80 @@ struct ThemeImageLabel: View {
}
}
struct ThemeMenuImage: View {
public struct ThemeCountryFlag: View {
private let code: String?
private let placeholderTip: String?
private let countryTip: ((String) -> String?)?
public init(code: String?, placeholderTip: String? = nil, countryTip: ((String) -> String?)? = nil) {
self.code = code
self.placeholderTip = placeholderTip
self.countryTip = countryTip
}
public var body: some View {
Group {
if let code {
let image = Image("flags/\(code.lowercased())")
.resizable()
if let tip = countryTip?(code) {
image
.help(tip)
} else {
image
}
} else {
let image = Image(systemName: "globe")
if let placeholderTip {
image
.help(placeholderTip)
} else {
image
}
}
}
.frame(width: 20, height: 15)
}
}
#if !os(tvOS)
public struct ThemeMenuImage: View {
@EnvironmentObject
private var theme: Theme
private let name: Theme.MenuImageName
init(_ name: Theme.MenuImageName) {
public init(_ name: Theme.MenuImageName) {
self.name = name
}
var body: some View {
public var body: some View {
Image(theme.menuImageName(name))
}
}
struct ThemeDisclosableMenu<Content, Label>: View where Content: View, Label: View {
public struct ThemeDisclosableMenu<Content, Label>: View where Content: View, Label: View {
@ViewBuilder
let content: () -> Content
private let content: () -> Content
@ViewBuilder
let label: Label
private let label: () -> Label
var body: some View {
public init(content: @escaping () -> Content, label: @escaping () -> Label) {
self.content = content
self.label = label
}
public var body: some View {
Menu(content: content) {
HStack(alignment: .firstTextBaseline) {
label
label()
ThemeImage(.disclose)
}
.contentShape(.rect)
@ -473,20 +525,32 @@ struct ThemeDisclosableMenu<Content, Label>: View where Content: View, Label: Vi
}
}
struct ThemeCopiableText<Value, ValueView>: View where Value: CustomStringConvertible, ValueView: View {
public struct ThemeCopiableText<Value, ValueView>: View where Value: CustomStringConvertible, ValueView: View {
@EnvironmentObject
private var theme: Theme
var title: String?
private let title: String?
let value: Value
private let value: Value
var isMultiLine = true
private let isMultiLine: Bool
let valueView: (Value) -> ValueView
private let valueView: (Value) -> ValueView
var body: some View {
public init(
title: String? = nil,
value: Value,
isMultiLine: Bool = true,
valueView: @escaping (Value) -> ValueView
) {
self.title = title
self.value = value
self.isMultiLine = isMultiLine
self.valueView = valueView
}
public var body: some View {
HStack {
if let title {
Text(title)
@ -509,10 +573,15 @@ struct ThemeCopiableText<Value, ValueView>: View where Value: CustomStringConver
}
}
struct ThemeTappableText: View {
let title: String
public struct ThemeTappableText: View {
private let title: String
let action: () -> Void
private let action: () -> Void
public init(title: String, action: @escaping () -> Void) {
self.title = title
self.action = action
}
var commonView: some View {
Button(action: action) {
@ -522,15 +591,15 @@ struct ThemeTappableText: View {
}
}
struct ThemeTextField: View {
let title: String?
public struct ThemeTextField: View {
private let title: String?
@Binding
var text: String
private var text: String
let placeholder: String
private let placeholder: String
init(_ title: String, text: Binding<String>, placeholder: String) {
public init(_ title: String, text: Binding<String>, placeholder: String) {
self.title = title
_text = text
self.placeholder = placeholder
@ -554,13 +623,19 @@ struct ThemeTextField: View {
}
}
struct ThemeSecureField: View {
let title: String?
public struct ThemeSecureField: View {
private let title: String?
@Binding
var text: String
private var text: String
let placeholder: String
private let placeholder: String
public init(title: String?, text: Binding<String>, placeholder: String) {
self.title = title
_text = text
self.placeholder = placeholder
}
@ViewBuilder
var commonView: some View {
@ -586,15 +661,25 @@ struct ThemeSecureField: View {
}
}
struct ThemeRemovableItemRow<ItemView>: View where ItemView: View {
let isEditing: Bool
public struct ThemeRemovableItemRow<ItemView>: View where ItemView: View {
private let isEditing: Bool
@ViewBuilder
let itemView: () -> ItemView
private let itemView: () -> ItemView
let removeAction: () -> Void
var body: some View {
public init(
isEditing: Bool,
@ViewBuilder itemView: @escaping () -> ItemView,
removeAction: @escaping () -> Void
) {
self.isEditing = isEditing
self.itemView = itemView
self.removeAction = removeAction
}
public var body: some View {
RemovableItemRow(
isEditing: isEditing,
itemView: itemView,
@ -603,11 +688,17 @@ struct ThemeRemovableItemRow<ItemView>: View where ItemView: View {
}
}
enum ThemeEditableListSection {
struct RemoveLabel: View {
public enum ThemeEditableListSection {
public struct RemoveLabel: View {
let action: () -> Void
public init(action: @escaping () -> Void) {
self.action = action
}
}
struct EditLabel: View {
public struct EditLabel: View {
}
}
#endif

View File

@ -28,68 +28,62 @@ import UtilsLibrary
@MainActor
public final class Theme: ObservableObject {
public internal(set) var rootModalSize: CGSize?
// @Published
// private var palette: Palette
//
// public init(palette: Palette) {
// self.palette = palette
// }
public internal(set) var secondaryModalSize: CGSize?
var rootModalSize: CGSize?
public internal(set) var popoverSize: CGSize?
var secondaryModalSize: CGSize?
public internal(set) var relevantWeight: Font.Weight = .semibold
var popoverSize: CGSize?
public internal(set) var titleColor: Color = .primary
var relevantWeight: Font.Weight = .semibold
public internal(set) var valueColor: Color = .secondary
var titleColor: Color = .primary
public internal(set) var gridHeaderStyle: Font = .headline
var valueColor: Color = .secondary
public internal(set) var gridRadius: CGFloat = 12.0
var gridHeaderStyle: Font = .headline
public internal(set) var gridHeaderBottom: CGFloat = 8.0
var gridRadius: CGFloat = 12.0
public internal(set) var gridCellColor: HierarchicalShapeStyle = .quinary
var gridHeaderBottom: CGFloat = 8.0
public internal(set) var gridCellActiveColor: HierarchicalShapeStyle = .quaternary
var gridCellColor: HierarchicalShapeStyle = .quinary
public internal(set) var emptyMessageFont: Font = .title
var gridCellActiveColor: HierarchicalShapeStyle = .quaternary
public internal(set) var emptyMessageColor: Color = .secondary
var emptyMessageFont: Font = .title
public internal(set) var primaryColor = Color(red: 0.318, green: 0.365, blue: 0.443)
var emptyMessageColor: Color = .secondary
public internal(set) var activeColor = Color(red: .zero, green: Double(0xAA) / 255.0, blue: .zero)
var primaryColor = Color(red: 0.318, green: 0.365, blue: 0.443)
public internal(set) var inactiveColor: Color = .gray
var activeColor = Color(red: .zero, green: Double(0xAA) / 255.0, blue: .zero)
public internal(set) var pendingColor: Color = .orange
var inactiveColor: Color = .gray
var pendingColor: Color = .orange
var errorColor: Color = .red
public internal(set) var errorColor: Color = .red
private var animation: Animation = .spring
var animationCategories: Set<ThemeAnimationCategory> = Set(ThemeAnimationCategory.allCases)
public internal(set) var animationCategories: Set<ThemeAnimationCategory> = Set(ThemeAnimationCategory.allCases)
var logoImage = "Logo"
public internal(set) var logoImage = "Logo"
var systemImageName: (ImageName) -> String = Theme.ImageName.defaultSystemName
public internal(set) var systemImageName: (ImageName) -> String = Theme.ImageName.defaultSystemName
var menuImageName: (MenuImageName) -> String = Theme.MenuImageName.defaultImageName
public internal(set) var menuImageName: (MenuImageName) -> String = Theme.MenuImageName.defaultImageName
init(dummy: Void) {
}
func animation(for category: ThemeAnimationCategory) -> Animation? {
public func animation(for category: ThemeAnimationCategory) -> Animation? {
animationCategories.contains(category) ? animation : nil
}
}
#if !os(tvOS)
// MARK: - Modifiers
extension View {
@ -214,39 +208,6 @@ extension View {
}
}
struct ThemeCountryFlag: View {
let code: String?
var placeholderTip: String?
var countryTip: ((String) -> String?)?
var body: some View {
Group {
if let code {
let image = Image("flags/\(code.lowercased())")
.resizable()
if let tip = countryTip?(code) {
image
.help(tip)
} else {
image
}
} else {
let image = Image(systemName: "globe")
if let placeholderTip {
image
.help(placeholderTip)
} else {
image
}
}
}
.frame(width: 20, height: 15)
}
}
// MARK: - Views
extension Theme {
@ -268,3 +229,5 @@ extension Theme {
)
}
}
#endif

View File

@ -25,6 +25,6 @@
import Foundation
protocol ThemeProviding {
public protocol ThemeProviding {
var theme: Theme { get }
}

View File

@ -27,7 +27,7 @@ import PassepartoutKit
import SwiftUI
extension ProfileEditor {
func binding(forNameOf moduleId: UUID) -> Binding<String> {
public func binding(forNameOf moduleId: UUID) -> Binding<String> {
Binding { [weak self] in
self?.profile.name(forModuleWithId: moduleId) ?? ""
} set: { [weak self] in
@ -35,7 +35,7 @@ extension ProfileEditor {
}
}
func binding(forProviderOf moduleId: UUID) -> Binding<ProviderID?> {
public func binding(forProviderOf moduleId: UUID) -> Binding<ProviderID?> {
Binding { [weak self] in
self?.profile.providerId(forModuleWithId: moduleId)
} set: { [weak self] in
@ -43,7 +43,7 @@ extension ProfileEditor {
}
}
func binding<E>(forProviderEntityOf moduleId: UUID) -> Binding<E?> where E: ProviderEntity & Codable {
public func binding<E>(forProviderEntityOf moduleId: UUID) -> Binding<E?> where E: ProviderEntity & Codable {
Binding { [weak self] in
try? self?.profile.providerEntity(E.self, forModuleWithId: moduleId)
} set: { [weak self] in
@ -51,7 +51,7 @@ extension ProfileEditor {
}
}
subscript<T>(module: T) -> Binding<T> where T: ModuleBuilder {
public 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)")

View File

@ -0,0 +1,58 @@
//
// AppUIMain.swift
// Passepartout
//
// Created by Davide De Rosa on 10/29/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/>.
//
@_exported import AppUI
import Foundation
public enum AppUIMain: AppUIConfiguring {
public static func configure(with context: AppContext) {
assertMissingModuleImplementations()
AppUI.configure(with: context)
}
}
private extension AppUIMain {
static func assertMissingModuleImplementations() {
let providerModuleTypes: Set<ModuleType> = [
.openVPN
]
ModuleType.allCases.forEach { moduleType in
let builder = moduleType.newModule()
guard builder is any ModuleViewProviding else {
fatalError("\(moduleType): is not ModuleViewProviding")
}
if providerModuleTypes.contains(moduleType) {
do {
let module = try builder.tryBuild()
guard module is any ProviderEntityViewProviding else {
fatalError("\(moduleType): is not ProviderEntityViewProviding")
}
} catch {
fatalError("\(moduleType): empty module is not buildable")
}
}
}
}
}

View File

@ -24,15 +24,19 @@
//
#if os(iOS)
import CommonLibrary
import Foundation
import PassepartoutKit
import UIKit
#else
import AppKit
import CommonLibrary
import Foundation
import PassepartoutKit
#endif
struct Issue: Identifiable {

View File

@ -1,8 +1,8 @@
//
// ProviderEntityViewProviding.swift
// ProviderEntityViewProviding+Extensions.swift
// Passepartout
//
// Created by Davide De Rosa on 10/16/24.
// Created by Davide De Rosa on 10/29/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
@ -26,16 +26,6 @@
import PassepartoutKit
import SwiftUI
protocol ProviderEntityViewProviding {
associatedtype EntityContent: View
@MainActor
func providerEntityView(
with provider: ModuleMetadata.Provider,
onSelect: @escaping (any ProviderEntity & Encodable) async throws -> Void
) -> EntityContent
}
extension ProviderEntityViewProviding where Self: ProviderCompatibleModule, EntityType.Configuration: ProviderConfigurationIdentifiable & Codable {
func vpnProviderEntityView(
with provider: ModuleMetadata.Provider,

View File

@ -0,0 +1,37 @@
//
// ProviderEntityViewProviding.swift
// Passepartout
//
// Created by Davide De Rosa on 10/16/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 ProviderEntityViewProviding {
associatedtype EntityContent: View
@MainActor
func providerEntityView(
with provider: ModuleMetadata.Provider,
onSelect: @escaping (any ProviderEntity & Encodable) async throws -> Void
) -> EntityContent
}

View File

@ -39,11 +39,11 @@ public struct AppCoordinator: View {
@AppStorage(AppPreference.profilesLayout.key)
private var layout: ProfilesLayout = .list
let profileManager: ProfileManager
private let profileManager: ProfileManager
let tunnel: Tunnel
private let tunnel: Tunnel
let registry: Registry
private let registry: Registry
@StateObject
private var profileEditor = ProfileEditor()

View File

@ -75,7 +75,7 @@ private extension AppMenu {
}
var profilesList: some View {
ForEach(profileManager.headers, id: \.self, content: profileToggle)
ForEach(profileManager.headers, id: \.id, content: profileToggle)
}
func profileToggle(for header: ProfileHeader) -> some View {

View File

@ -31,7 +31,7 @@ import SwiftUI
public struct AppMenuImage: View, TunnelContextProviding {
@ObservedObject
var connectionObserver: ConnectionObserver
public var connectionObserver: ConnectionObserver
public init(connectionObserver: ConnectionObserver) {
self.connectionObserver = connectionObserver

Some files were not shown because too many files have changed in this diff Show More