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 */; }; 0EC797422B9378E000C093B7 /* Shared+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC797402B9378E000C093B7 /* Shared+App.swift */; };
0EC797432B9378E000C093B7 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC797412B9378E000C093B7 /* Shared.swift */; }; 0EC797432B9378E000C093B7 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC797412B9378E000C093B7 /* Shared.swift */; };
0EC797442B93790600C093B7 /* 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 /* App+macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED61CF72CD0418C008FE259 /* App+macOS.swift */; };
0ED61CF82CD0418C008FE259 /* AppDelegate+macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED61CF72CD0418C008FE259 /* AppDelegate+macOS.swift */; }; 0ED61CFA2CD04192008FE259 /* App+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED61CF92CD04192008FE259 /* App+iOS.swift */; };
0ED61CFA2CD04192008FE259 /* AppDelegate+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED61CF92CD04192008FE259 /* AppDelegate+iOS.swift */; };
0EDE56EA2CABE40D0082D21C /* Intents.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0EDE56E62CABE40D0082D21C /* Intents.plist */; }; 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, ); }; }; 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 */; }; 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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy 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>"; }; 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>"; }; 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>"; }; 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>"; }; 0ED61CF72CD0418C008FE259 /* App+macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+macOS.swift"; sourceTree = "<group>"; };
0ED61CF92CD04192008FE259 /* AppDelegate+iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+iOS.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>"; }; 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>"; }; 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>"; }; 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; }; 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 */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -144,7 +147,8 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
0EC9C0232CA5BD0B00C52954 /* AppUI in Frameworks */, 0EE8D7DF2CD1108900F6600C /* AppUITV in Frameworks */,
0EE8D7DD2CD1107E00F6600C /* AppUIMain in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -214,8 +218,7 @@
0E7E3D5A2B9345FD002BBDB4 /* App */ = { 0E7E3D5A2B9345FD002BBDB4 /* App */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0ED61CF52CD0416A008FE259 /* iOS */, 0ED61CF62CD04174008FE259 /* Platforms */,
0ED61CF62CD04174008FE259 /* macOS */,
0ED1EFDA2C33059600CBD9BD /* App.plist */, 0ED1EFDA2C33059600CBD9BD /* App.plist */,
0E7E3D5B2B9345FD002BBDB4 /* App.entitlements */, 0E7E3D5B2B9345FD002BBDB4 /* App.entitlements */,
0E7C3CCC2C9AF44600B72E69 /* AppDelegate.swift */, 0E7C3CCC2C9AF44600B72E69 /* AppDelegate.swift */,
@ -247,20 +250,14 @@
path = Tunnel; path = Tunnel;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
0ED61CF52CD0416A008FE259 /* iOS */ = { 0ED61CF62CD04174008FE259 /* Platforms */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0ED61CF92CD04192008FE259 /* AppDelegate+iOS.swift */, 0ED61CF92CD04192008FE259 /* App+iOS.swift */,
0ED61CF72CD0418C008FE259 /* App+macOS.swift */,
0EE8D7E02CD112C200F6600C /* App+tvOS.swift */,
); );
path = iOS; path = Platforms;
sourceTree = "<group>";
};
0ED61CF62CD04174008FE259 /* macOS */ = {
isa = PBXGroup;
children = (
0ED61CF72CD0418C008FE259 /* AppDelegate+macOS.swift */,
);
path = macOS;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
0EDE56E82CABE40D0082D21C /* Intents */ = { 0EDE56E82CABE40D0082D21C /* Intents */ = {
@ -298,7 +295,8 @@
); );
name = Passepartout; name = Passepartout;
packageProductDependencies = ( packageProductDependencies = (
0EC9C0222CA5BD0B00C52954 /* AppUI */, 0EE8D7DC2CD1107E00F6600C /* AppUIMain */,
0EE8D7DE2CD1108900F6600C /* AppUITV */,
); );
productName = PassepartoutKit; productName = PassepartoutKit;
productReference = 0E06D18F2B87629100176E1D /* Passepartout.app */; productReference = 0E06D18F2B87629100176E1D /* Passepartout.app */;
@ -466,11 +464,12 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
0ED61CF82CD0418C008FE259 /* AppDelegate+macOS.swift in Sources */, 0ED61CF82CD0418C008FE259 /* App+macOS.swift in Sources */,
0E7C3CCD2C9AF44600B72E69 /* AppDelegate.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 */, 0E7E3D6B2B9345FD002BBDB4 /* PassepartoutApp.swift in Sources */,
0EC797422B9378E000C093B7 /* Shared+App.swift in Sources */, 0EC797422B9378E000C093B7 /* Shared+App.swift in Sources */,
0EE8D7E12CD112C200F6600C /* App+tvOS.swift in Sources */,
0EC797432B9378E000C093B7 /* Shared.swift in Sources */, 0EC797432B9378E000C093B7 /* Shared.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -621,7 +620,7 @@
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = complete; SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2,3";
TVOS_DEPLOYMENT_TARGET = 17.0; TVOS_DEPLOYMENT_TARGET = 17.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
@ -697,7 +696,7 @@
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_STRICT_CONCURRENCY = complete; SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2,3";
TVOS_DEPLOYMENT_TARGET = 17.0; TVOS_DEPLOYMENT_TARGET = 17.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
@ -1001,9 +1000,13 @@
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = TunnelLibrary; productName = TunnelLibrary;
}; };
0EC9C0222CA5BD0B00C52954 /* AppUI */ = { 0EE8D7DC2CD1107E00F6600C /* AppUIMain */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = AppUI; productName = AppUIMain;
};
0EE8D7DE2CD1108900F6600C /* AppUITV */ = {
isa = XCSwiftPackageProductDependency;
productName = AppUITV;
}; };
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
}; };

View File

@ -23,7 +23,12 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>. // 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 CommonLibrary
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
@ -31,56 +36,34 @@ import SwiftUI
@main @main
struct PassepartoutApp: App { struct PassepartoutApp: App {
#if os(iOS) #if os(iOS) || os(tvOS)
@UIApplicationDelegateAdaptor @UIApplicationDelegateAdaptor
private var appDelegate: AppDelegate private var appDelegate: AppDelegate
#else
#elseif os(macOS)
@NSApplicationDelegateAdaptor @NSApplicationDelegateAdaptor
private var appDelegate: AppDelegate private var appDelegate: AppDelegate
#endif #endif
@Environment(\.scenePhase) @Environment(\.scenePhase)
private var scenePhase private var scenePhase
private var context: AppContext { @StateObject
var theme = Theme()
}
extension PassepartoutApp {
var appName: String {
BundleConfiguration.mainDisplayName
}
var context: AppContext {
appDelegate.context 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 { func contentView() -> some View {
AppCoordinator( AppCoordinator(
profileManager: context.profileManager, profileManager: context.profileManager,
@ -103,7 +86,6 @@ private extension PassepartoutApp {
break break
} }
} }
.themeLockScreen()
.withEnvironment(from: context, theme: theme) .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 // Passepartout
// //
// Created by Davide De Rosa on 10/28/24. // Created by Davide De Rosa on 10/28/24.
@ -25,9 +25,9 @@
#if os(macOS) #if os(macOS)
import AppKit import AppUIMain
import AppUI
import PassepartoutKit import PassepartoutKit
import SwiftUI
extension AppDelegate: NSApplicationDelegate { extension AppDelegate: NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) { 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 #endif

View File

@ -1,8 +1,8 @@
// //
// AppDelegate+iOS.swift // App+tvOS.swift
// Passepartout // 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. // Copyright (c) 2024 Davide De Rosa. All rights reserved.
// //
// https://github.com/passepartoutvpn // https://github.com/passepartoutvpn
@ -23,10 +23,10 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>. // along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
// //
#if os(iOS) #if os(tvOS)
import AppUI import AppUITV
import UIKit import SwiftUI
extension AppDelegate: UIApplicationDelegate { extension AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { 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 #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", name: "AppUI",
targets: ["AppUI"] targets: ["AppUI"]
), ),
.library(
name: "AppUIMain",
targets: ["AppUIMain"]
),
.library(
name: "AppUITV",
targets: ["AppUITV"]
),
.library( .library(
name: "TunnelLibrary", name: "TunnelLibrary",
targets: ["CommonLibrary"] targets: ["CommonLibrary"]
@ -78,13 +86,26 @@ let package = Package(
"AppDataProviders", "AppDataProviders",
"AppLibrary", "AppLibrary",
"Kvitto", "Kvitto",
"LegacyV2",
"UtilsLibrary" "UtilsLibrary"
], ],
resources: [ resources: [
.process("Resources") .process("Resources")
] ]
), ),
.target(
name: "AppUIMain",
dependencies: [
"AppUI",
"LegacyV2"
],
resources: [
.process("Resources")
]
),
.target(
name: "AppUITV",
dependencies: ["AppUI"]
),
.target( .target(
name: "CommonLibrary", name: "CommonLibrary",
dependencies: [ dependencies: [
@ -114,6 +135,10 @@ let package = Package(
name: "AppLibraryTests", name: "AppLibraryTests",
dependencies: ["AppLibrary"] dependencies: ["AppLibrary"]
), ),
.testTarget(
name: "AppUIMainTests",
dependencies: ["AppUIMain"]
),
.testTarget( .testTarget(
name: "AppUITests", name: "AppUITests",
dependencies: ["AppUI"] dependencies: ["AppUI"]

View File

@ -24,9 +24,12 @@
// //
import Foundation import Foundation
import NetworkExtension
import PassepartoutKit import PassepartoutKit
public protocol AppUIConfiguring {
static func configure(with context: AppContext)
}
public enum AppUI { public enum AppUI {
public static func configure(with context: AppContext) { public static func configure(with context: AppContext) {
assertMissingModuleImplementations() assertMissingModuleImplementations()
@ -36,29 +39,13 @@ public enum AppUI {
} }
} }
private extension AppUI { extension AppUI {
static func assertMissingModuleImplementations() { public static func assertMissingModuleImplementations() {
let providerModuleTypes: Set<ModuleType> = [
.openVPN
]
ModuleType.allCases.forEach { moduleType in ModuleType.allCases.forEach { moduleType in
let builder = moduleType.newModule() let builder = moduleType.newModule()
guard builder is ModuleTypeProviding else { guard builder is ModuleTypeProviding else {
fatalError("\(moduleType): is not ModuleTypeProviding") 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 @MainActor
public final class ConnectionObserver: ObservableObject { public final class ConnectionObserver: ObservableObject {
let tunnel: Tunnel public let tunnel: Tunnel
private let environment: TunnelEnvironment private let environment: TunnelEnvironment

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,8 +26,13 @@
import Foundation import Foundation
import PassepartoutKit import PassepartoutKit
struct TunnelInstallation { public struct TunnelInstallation {
let header: ProfileHeader 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 @MainActor
extension Tunnel { 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) let newProfile = try processor.processed(profile)
try await install(newProfile, connect: false, title: processor.title) 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) let newProfile = try processor.processed(profile)
try await install(newProfile, connect: true, title: processor.title) 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( let output = try? await sendMessage(.localLog(
sinceLast: parameters.sinceLast, sinceLast: parameters.sinceLast,
maxLevel: parameters.maxLevel maxLevel: parameters.maxLevel

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,9 +27,9 @@ import Foundation
import PassepartoutKit import PassepartoutKit
extension Strings { extension Strings {
enum Unlocalized { public enum Unlocalized {
enum OpenVPN { public enum OpenVPN {
enum XOR: String { public enum XOR: String {
case xormask case xormask
case xorptrpos case xorptrpos
@ -39,23 +39,23 @@ extension Strings {
case obfuscate 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 { public enum Placeholders {
static let hostname = "example.com" 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 { switch family {
case .v4: case .v4:
return "192.168.15.0/24" 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 { switch family {
case .v4: case .v4:
return "192.168.15.1" 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 { public enum Issues {
static let subject = "\(appName) - Report issue" 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: { profileManager: {
let profiles: [Profile] = (0..<20) let profiles: [Profile] = (0..<20)
.reduce(into: []) { list, _ in .reduce(into: []) { list, _ in
list.append(.newProfile()) list.append(.newMockProfile())
} }
return ProfileManager(profiles: profiles) return ProfileManager(profiles: profiles)
}(), }(),
@ -109,7 +109,7 @@ extension ProviderManager {
// MARK: - Profile // MARK: - Profile
extension Profile { extension Profile {
static let mock: Profile = { public static let mock: Profile = {
var profile = Profile.Builder() var profile = Profile.Builder()
profile.name = "Mock profile" profile.name = "Mock profile"
@ -144,7 +144,7 @@ extension Profile {
} }
}() }()
static func newProfile() -> Profile { public static func newMockProfile() -> Profile {
do { do {
var copy = mock.builder(withNewId: true) var copy = mock.builder(withNewId: true)
copy.name = String(copy.id.uuidString.prefix(8)) copy.name = String(copy.id.uuidString.prefix(8))

View File

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

View File

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

View File

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

View File

@ -91,7 +91,7 @@ extension ThemeSectionWithHeaderFooterModifier {
// MARK: - Views // MARK: - Views
extension ThemeTappableText { extension ThemeTappableText {
var body: some View { public var body: some View {
commonView commonView
.buttonStyle(.plain) .buttonStyle(.plain)
.cursor(.hand) .cursor(.hand)
@ -99,14 +99,14 @@ extension ThemeTappableText {
} }
extension ThemeTextField { extension ThemeTextField {
var body: some View { public var body: some View {
commonView commonView
.labelsHidden() .labelsHidden()
} }
} }
extension ThemeSecureField { extension ThemeSecureField {
var body: some View { public var body: some View {
commonView commonView
.labelsHidden() .labelsHidden()
} }
@ -122,7 +122,7 @@ extension ThemeRemovableItemRow {
} }
extension ThemeEditableListSection.RemoveLabel { extension ThemeEditableListSection.RemoveLabel {
var body: some View { public var body: some View {
Button(action: action) { Button(action: action) {
ThemeImage(.editableSectionRemove) ThemeImage(.editableSectionRemove)
} }
@ -131,7 +131,7 @@ extension ThemeEditableListSection.RemoveLabel {
} }
extension ThemeEditableListSection.EditLabel { extension ThemeEditableListSection.EditLabel {
var body: some View { public var body: some View {
ThemeImage(.editableSectionEdit) 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 import CommonLibrary
#if canImport(LocalAuthentication)
import LocalAuthentication import LocalAuthentication
#endif
import SwiftUI import SwiftUI
import UtilsLibrary import UtilsLibrary
// MARK: - Modifiers // MARK: - Modifiers
#if !os(tvOS)
struct ThemeWindowModifier: ViewModifier { struct ThemeWindowModifier: ViewModifier {
let size: CGSize let size: CGSize
} }
@ -381,6 +385,8 @@ struct ThemeTipModifier: ViewModifier {
} }
} }
#endif
// MARK: - Views // MARK: - Views
public enum ThemeAnimationCategory: CaseIterable { public enum ThemeAnimationCategory: CaseIterable {
@ -395,23 +401,23 @@ public enum ThemeAnimationCategory: CaseIterable {
case providers case providers
} }
struct ThemeImage: View { public struct ThemeImage: View {
@EnvironmentObject @EnvironmentObject
private var theme: Theme private var theme: Theme
private let name: Theme.ImageName private let name: Theme.ImageName
init(_ name: Theme.ImageName) { public init(_ name: Theme.ImageName) {
self.name = name self.name = name
} }
var body: some View { public var body: some View {
Image(systemName: theme.systemImageName(name)) Image(systemName: theme.systemImageName(name))
} }
} }
struct ThemeImageLabel: View { public struct ThemeImageLabel: View {
@EnvironmentObject @EnvironmentObject
private var theme: Theme private var theme: Theme
@ -420,12 +426,12 @@ struct ThemeImageLabel: View {
private let name: Theme.ImageName private let name: Theme.ImageName
init(_ title: String, _ name: Theme.ImageName) { public init(_ title: String, _ name: Theme.ImageName) {
self.title = title self.title = title
self.name = name self.name = name
} }
var body: some View { public var body: some View {
Label { Label {
Text(title) Text(title)
} icon: { } 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 @EnvironmentObject
private var theme: Theme private var theme: Theme
private let name: Theme.MenuImageName private let name: Theme.MenuImageName
init(_ name: Theme.MenuImageName) { public init(_ name: Theme.MenuImageName) {
self.name = name self.name = name
} }
var body: some View { public var body: some View {
Image(theme.menuImageName(name)) 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 @ViewBuilder
let content: () -> Content private let content: () -> Content
@ViewBuilder @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) { Menu(content: content) {
HStack(alignment: .firstTextBaseline) { HStack(alignment: .firstTextBaseline) {
label label()
ThemeImage(.disclose) ThemeImage(.disclose)
} }
.contentShape(.rect) .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 @EnvironmentObject
private var theme: Theme 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 { HStack {
if let title { if let title {
Text(title) Text(title)
@ -509,10 +573,15 @@ struct ThemeCopiableText<Value, ValueView>: View where Value: CustomStringConver
} }
} }
struct ThemeTappableText: View { public struct ThemeTappableText: View {
let title: String 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 { var commonView: some View {
Button(action: action) { Button(action: action) {
@ -522,15 +591,15 @@ struct ThemeTappableText: View {
} }
} }
struct ThemeTextField: View { public struct ThemeTextField: View {
let title: String? private let title: String?
@Binding @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 self.title = title
_text = text _text = text
self.placeholder = placeholder self.placeholder = placeholder
@ -554,13 +623,19 @@ struct ThemeTextField: View {
} }
} }
struct ThemeSecureField: View { public struct ThemeSecureField: View {
let title: String? private let title: String?
@Binding @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 @ViewBuilder
var commonView: some View { var commonView: some View {
@ -586,15 +661,25 @@ struct ThemeSecureField: View {
} }
} }
struct ThemeRemovableItemRow<ItemView>: View where ItemView: View { public struct ThemeRemovableItemRow<ItemView>: View where ItemView: View {
let isEditing: Bool private let isEditing: Bool
@ViewBuilder @ViewBuilder
let itemView: () -> ItemView private let itemView: () -> ItemView
let removeAction: () -> Void 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( RemovableItemRow(
isEditing: isEditing, isEditing: isEditing,
itemView: itemView, itemView: itemView,
@ -603,11 +688,17 @@ struct ThemeRemovableItemRow<ItemView>: View where ItemView: View {
} }
} }
enum ThemeEditableListSection { public enum ThemeEditableListSection {
struct RemoveLabel: View { public struct RemoveLabel: View {
let action: () -> Void 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 @MainActor
public final class Theme: ObservableObject { public final class Theme: ObservableObject {
public internal(set) var rootModalSize: CGSize?
// @Published public internal(set) var secondaryModalSize: CGSize?
// private var palette: Palette
//
// public init(palette: Palette) {
// self.palette = palette
// }
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 public internal(set) var errorColor: Color = .red
var pendingColor: Color = .orange
var errorColor: Color = .red
private var animation: Animation = .spring 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) { init(dummy: Void) {
} }
func animation(for category: ThemeAnimationCategory) -> Animation? { public func animation(for category: ThemeAnimationCategory) -> Animation? {
animationCategories.contains(category) ? animation : nil animationCategories.contains(category) ? animation : nil
} }
} }
#if !os(tvOS)
// MARK: - Modifiers // MARK: - Modifiers
extension View { 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 // MARK: - Views
extension Theme { extension Theme {
@ -268,3 +229,5 @@ extension Theme {
) )
} }
} }
#endif

View File

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

View File

@ -27,7 +27,7 @@ import PassepartoutKit
import SwiftUI import SwiftUI
extension ProfileEditor { extension ProfileEditor {
func binding(forNameOf moduleId: UUID) -> Binding<String> { public func binding(forNameOf moduleId: UUID) -> Binding<String> {
Binding { [weak self] in Binding { [weak self] in
self?.profile.name(forModuleWithId: moduleId) ?? "" self?.profile.name(forModuleWithId: moduleId) ?? ""
} set: { [weak self] in } 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 Binding { [weak self] in
self?.profile.providerId(forModuleWithId: moduleId) self?.profile.providerId(forModuleWithId: moduleId)
} set: { [weak self] in } 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 Binding { [weak self] in
try? self?.profile.providerEntity(E.self, forModuleWithId: moduleId) try? self?.profile.providerEntity(E.self, forModuleWithId: moduleId)
} set: { [weak self] in } 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 Binding { [weak self] in
guard let foundModule = self?.module(withId: module.id) else { guard let foundModule = self?.module(withId: module.id) else {
fatalError("Module not found in editor: \(module.id)") 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) #if os(iOS)
import CommonLibrary import CommonLibrary
import Foundation import Foundation
import PassepartoutKit import PassepartoutKit
import UIKit import UIKit
#else #else
import AppKit import AppKit
import CommonLibrary import CommonLibrary
import Foundation import Foundation
import PassepartoutKit import PassepartoutKit
#endif #endif
struct Issue: Identifiable { struct Issue: Identifiable {

View File

@ -1,8 +1,8 @@
// //
// ProviderEntityViewProviding.swift // ProviderEntityViewProviding+Extensions.swift
// Passepartout // 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. // Copyright (c) 2024 Davide De Rosa. All rights reserved.
// //
// https://github.com/passepartoutvpn // https://github.com/passepartoutvpn
@ -26,16 +26,6 @@
import PassepartoutKit import PassepartoutKit
import SwiftUI 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 { extension ProviderEntityViewProviding where Self: ProviderCompatibleModule, EntityType.Configuration: ProviderConfigurationIdentifiable & Codable {
func vpnProviderEntityView( func vpnProviderEntityView(
with provider: ModuleMetadata.Provider, 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) @AppStorage(AppPreference.profilesLayout.key)
private var layout: ProfilesLayout = .list 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 @StateObject
private var profileEditor = ProfileEditor() private var profileEditor = ProfileEditor()

View File

@ -75,7 +75,7 @@ private extension AppMenu {
} }
var profilesList: some View { 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 { func profileToggle(for header: ProfileHeader) -> some View {

View File

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

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