diff --git a/Passepartout/Library/Package.resolved b/Passepartout/Library/Package.resolved
index 187019fc..8be5350d 100644
--- a/Passepartout/Library/Package.resolved
+++ b/Passepartout/Library/Package.resolved
@@ -12,7 +12,7 @@
{
"identity" : "generic-json-swift",
"kind" : "remoteSourceControl",
- "location" : "https://github.com/zoul/generic-json-swift",
+ "location" : "https://github.com/iwill/generic-json-swift",
"state" : {
"revision" : "0a06575f4038b504e78ac330913d920f1630f510",
"version" : "2.0.2"
@@ -41,7 +41,7 @@
"kind" : "remoteSourceControl",
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
"state" : {
- "revision" : "31aff403169c7cebe91a07fb8d225ab844a9a9ff"
+ "revision" : "e95c7b54dc11e744d9b40a722fccf752436ac0ef"
}
},
{
diff --git a/Passepartout/Library/Package.swift b/Passepartout/Library/Package.swift
index 618476d7..ffd923ab 100644
--- a/Passepartout/Library/Package.swift
+++ b/Passepartout/Library/Package.swift
@@ -49,6 +49,13 @@ let package = Package(
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
+ .target(
+ name: "APILibrary",
+ dependencies: ["CommonLibrary"],
+ resources: [
+ .copy("API")
+ ]
+ ),
.target(
name: "AppData",
dependencies: []
@@ -57,8 +64,7 @@ let package = Package(
name: "AppDataProfiles",
dependencies: [
"AppData",
- "AppLibrary",
- "UtilsLibrary"
+ "AppLibrary"
],
resources: [
.process("Profiles.xcdatamodeld")
@@ -68,8 +74,7 @@ let package = Package(
name: "AppDataProviders",
dependencies: [
"AppData",
- "AppLibrary",
- "UtilsLibrary"
+ "AppLibrary"
],
resources: [
.process("Providers.xcdatamodeld")
@@ -77,16 +82,18 @@ let package = Package(
),
.target(
name: "AppLibrary",
- dependencies: ["CommonLibrary"]
+ dependencies: [
+ "APILibrary",
+ "Kvitto",
+ "UtilsLibrary"
+ ]
),
.target(
name: "AppUI",
dependencies: [
"AppDataProfiles",
"AppDataProviders",
- "AppLibrary",
- "Kvitto",
- "UtilsLibrary"
+ "AppLibrary"
],
resources: [
.process("Resources")
@@ -114,7 +121,6 @@ let package = Package(
.product(name: "PassepartoutWireGuardGo", package: "passepartoutkit-source-wireguard-go")
],
resources: [
- .copy("API"),
.process("Resources")
]
),
diff --git a/Passepartout/Library/Sources/CommonLibrary/API/v5/providers/hideme/ovpn.json b/Passepartout/Library/Sources/APILibrary/API/v5/providers/hideme/ovpn.json
similarity index 100%
rename from Passepartout/Library/Sources/CommonLibrary/API/v5/providers/hideme/ovpn.json
rename to Passepartout/Library/Sources/APILibrary/API/v5/providers/hideme/ovpn.json
diff --git a/Passepartout/Library/Sources/CommonLibrary/API/v5/providers/index.json b/Passepartout/Library/Sources/APILibrary/API/v5/providers/index.json
similarity index 100%
rename from Passepartout/Library/Sources/CommonLibrary/API/v5/providers/index.json
rename to Passepartout/Library/Sources/APILibrary/API/v5/providers/index.json
diff --git a/Passepartout/Library/Sources/CommonLibrary/API/v5/providers/ivpn/ovpn.json b/Passepartout/Library/Sources/APILibrary/API/v5/providers/ivpn/ovpn.json
similarity index 100%
rename from Passepartout/Library/Sources/CommonLibrary/API/v5/providers/ivpn/ovpn.json
rename to Passepartout/Library/Sources/APILibrary/API/v5/providers/ivpn/ovpn.json
diff --git a/Passepartout/Library/Sources/CommonLibrary/API/v5/providers/mullvad/ovpn.json b/Passepartout/Library/Sources/APILibrary/API/v5/providers/mullvad/ovpn.json
similarity index 100%
rename from Passepartout/Library/Sources/CommonLibrary/API/v5/providers/mullvad/ovpn.json
rename to Passepartout/Library/Sources/APILibrary/API/v5/providers/mullvad/ovpn.json
diff --git a/Passepartout/Library/Sources/CommonLibrary/API/v5/providers/nordvpn/ovpn.json b/Passepartout/Library/Sources/APILibrary/API/v5/providers/nordvpn/ovpn.json
similarity index 100%
rename from Passepartout/Library/Sources/CommonLibrary/API/v5/providers/nordvpn/ovpn.json
rename to Passepartout/Library/Sources/APILibrary/API/v5/providers/nordvpn/ovpn.json
diff --git a/Passepartout/Library/Sources/CommonLibrary/API/v5/providers/oeck/ovpn.json b/Passepartout/Library/Sources/APILibrary/API/v5/providers/oeck/ovpn.json
similarity index 100%
rename from Passepartout/Library/Sources/CommonLibrary/API/v5/providers/oeck/ovpn.json
rename to Passepartout/Library/Sources/APILibrary/API/v5/providers/oeck/ovpn.json
diff --git a/Passepartout/Library/Sources/CommonLibrary/API/v5/providers/pia/ovpn.json b/Passepartout/Library/Sources/APILibrary/API/v5/providers/pia/ovpn.json
similarity index 100%
rename from Passepartout/Library/Sources/CommonLibrary/API/v5/providers/pia/ovpn.json
rename to Passepartout/Library/Sources/APILibrary/API/v5/providers/pia/ovpn.json
diff --git a/Passepartout/Library/Sources/CommonLibrary/API/v5/providers/protonvpn/ovpn.json b/Passepartout/Library/Sources/APILibrary/API/v5/providers/protonvpn/ovpn.json
similarity index 100%
rename from Passepartout/Library/Sources/CommonLibrary/API/v5/providers/protonvpn/ovpn.json
rename to Passepartout/Library/Sources/APILibrary/API/v5/providers/protonvpn/ovpn.json
diff --git a/Passepartout/Library/Sources/CommonLibrary/API/v5/providers/surfshark/ovpn.json b/Passepartout/Library/Sources/APILibrary/API/v5/providers/surfshark/ovpn.json
similarity index 100%
rename from Passepartout/Library/Sources/CommonLibrary/API/v5/providers/surfshark/ovpn.json
rename to Passepartout/Library/Sources/APILibrary/API/v5/providers/surfshark/ovpn.json
diff --git a/Passepartout/Library/Sources/CommonLibrary/API/v5/providers/torguard/ovpn.json b/Passepartout/Library/Sources/APILibrary/API/v5/providers/torguard/ovpn.json
similarity index 100%
rename from Passepartout/Library/Sources/CommonLibrary/API/v5/providers/torguard/ovpn.json
rename to Passepartout/Library/Sources/APILibrary/API/v5/providers/torguard/ovpn.json
diff --git a/Passepartout/Library/Sources/CommonLibrary/API/v5/providers/tunnelbear/ovpn.json b/Passepartout/Library/Sources/APILibrary/API/v5/providers/tunnelbear/ovpn.json
similarity index 100%
rename from Passepartout/Library/Sources/CommonLibrary/API/v5/providers/tunnelbear/ovpn.json
rename to Passepartout/Library/Sources/APILibrary/API/v5/providers/tunnelbear/ovpn.json
diff --git a/Passepartout/Library/Sources/CommonLibrary/API/v5/providers/vyprvpn/ovpn.json b/Passepartout/Library/Sources/APILibrary/API/v5/providers/vyprvpn/ovpn.json
similarity index 100%
rename from Passepartout/Library/Sources/CommonLibrary/API/v5/providers/vyprvpn/ovpn.json
rename to Passepartout/Library/Sources/APILibrary/API/v5/providers/vyprvpn/ovpn.json
diff --git a/Passepartout/Library/Sources/CommonLibrary/API/v5/providers/windscribe/ovpn.json b/Passepartout/Library/Sources/APILibrary/API/v5/providers/windscribe/ovpn.json
similarity index 100%
rename from Passepartout/Library/Sources/CommonLibrary/API/v5/providers/windscribe/ovpn.json
rename to Passepartout/Library/Sources/APILibrary/API/v5/providers/windscribe/ovpn.json
diff --git a/Passepartout/Library/Sources/APILibrary/Shared.swift b/Passepartout/Library/Sources/APILibrary/Shared.swift
new file mode 100644
index 00000000..0b59bdb6
--- /dev/null
+++ b/Passepartout/Library/Sources/APILibrary/Shared.swift
@@ -0,0 +1,63 @@
+//
+// Shared.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 11/1/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 .
+//
+
+import CommonLibrary
+import Foundation
+import PassepartoutKit
+
+// TODO: #716, move to Environment
+extension API {
+ public static var shared: [APIMapper] {
+#if DEBUG
+ [API.bundled]
+#else
+ API.remoteThenBundled
+#endif
+ }
+
+ private static let remoteThenBundled: [APIMapper] = [
+ Self.remote,
+ Self.bundled
+ ]
+
+ public static let bundled: APIMapper = {
+ guard let url = Bundle.module.url(forResource: "API", withExtension: nil) else {
+ fatalError("Unable to find bundled API")
+ }
+ let ws = API.V5.DefaultWebServices(
+ url,
+ timeout: Constants.shared.api.timeoutInterval
+ )
+ return API.V5.Mapper(webServices: ws)
+ }()
+
+ public static let remote: APIMapper = {
+ let ws = API.V5.DefaultWebServices(
+ Constants.shared.websites.api,
+ timeout: Constants.shared.api.timeoutInterval
+ )
+ return API.V5.Mapper(webServices: ws)
+ }()
+}
diff --git a/Passepartout/Library/Sources/AppUI/Business/ExtendedTunnel.swift b/Passepartout/Library/Sources/AppLibrary/Business/ExtendedTunnel.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUI/Business/ExtendedTunnel.swift
rename to Passepartout/Library/Sources/AppLibrary/Business/ExtendedTunnel.swift
diff --git a/Passepartout/Library/Sources/AppUI/Business/ProfileProcessor.swift b/Passepartout/Library/Sources/AppLibrary/Business/ProfileProcessor.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUI/Business/ProfileProcessor.swift
rename to Passepartout/Library/Sources/AppLibrary/Business/ProfileProcessor.swift
diff --git a/Passepartout/Library/Sources/AppUI/IAP/AppFeature.swift b/Passepartout/Library/Sources/AppLibrary/IAP/AppFeature.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUI/IAP/AppFeature.swift
rename to Passepartout/Library/Sources/AppLibrary/IAP/AppFeature.swift
diff --git a/Passepartout/Library/Sources/AppUI/IAP/AppFeatureProviding.swift b/Passepartout/Library/Sources/AppLibrary/IAP/AppFeatureProviding.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUI/IAP/AppFeatureProviding.swift
rename to Passepartout/Library/Sources/AppLibrary/IAP/AppFeatureProviding.swift
diff --git a/Passepartout/Library/Sources/AppUI/IAP/AppProduct+Donations.swift b/Passepartout/Library/Sources/AppLibrary/IAP/AppProduct+Donations.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUI/IAP/AppProduct+Donations.swift
rename to Passepartout/Library/Sources/AppLibrary/IAP/AppProduct+Donations.swift
diff --git a/Passepartout/Library/Sources/AppUI/IAP/AppProduct+Features.swift b/Passepartout/Library/Sources/AppLibrary/IAP/AppProduct+Features.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUI/IAP/AppProduct+Features.swift
rename to Passepartout/Library/Sources/AppLibrary/IAP/AppProduct+Features.swift
diff --git a/Passepartout/Library/Sources/AppUI/IAP/AppProduct+Providers.swift b/Passepartout/Library/Sources/AppLibrary/IAP/AppProduct+Providers.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUI/IAP/AppProduct+Providers.swift
rename to Passepartout/Library/Sources/AppLibrary/IAP/AppProduct+Providers.swift
diff --git a/Passepartout/Library/Sources/AppUI/IAP/AppProduct.swift b/Passepartout/Library/Sources/AppLibrary/IAP/AppProduct.swift
similarity index 98%
rename from Passepartout/Library/Sources/AppUI/IAP/AppProduct.swift
rename to Passepartout/Library/Sources/AppLibrary/IAP/AppProduct.swift
index 23416b7d..cae43a03 100644
--- a/Passepartout/Library/Sources/AppUI/IAP/AppProduct.swift
+++ b/Passepartout/Library/Sources/AppLibrary/IAP/AppProduct.swift
@@ -50,7 +50,7 @@ extension AppProduct: InAppIdentifierProviding {
}
extension AppProduct {
- static var all: [Self] {
+ public static var all: [Self] {
Features.all + Full.all + Donations.all
}
diff --git a/Passepartout/Library/Sources/AppUI/IAP/AppProductHelper.swift b/Passepartout/Library/Sources/AppLibrary/IAP/AppProductHelper.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUI/IAP/AppProductHelper.swift
rename to Passepartout/Library/Sources/AppLibrary/IAP/AppProductHelper.swift
diff --git a/Passepartout/Library/Sources/AppUI/IAP/AppReceiptReader.swift b/Passepartout/Library/Sources/AppLibrary/IAP/AppReceiptReader.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUI/IAP/AppReceiptReader.swift
rename to Passepartout/Library/Sources/AppLibrary/IAP/AppReceiptReader.swift
diff --git a/Passepartout/Library/Sources/AppUI/IAP/AppUserLevel.swift b/Passepartout/Library/Sources/AppLibrary/IAP/AppUserLevel.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUI/IAP/AppUserLevel.swift
rename to Passepartout/Library/Sources/AppLibrary/IAP/AppUserLevel.swift
diff --git a/Passepartout/Library/Sources/AppUI/IAP/IAPManager.swift b/Passepartout/Library/Sources/AppLibrary/IAP/IAPManager.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUI/IAP/IAPManager.swift
rename to Passepartout/Library/Sources/AppLibrary/IAP/IAPManager.swift
diff --git a/Passepartout/Library/Sources/AppUI/IAP/KvittoReceiptReader.swift b/Passepartout/Library/Sources/AppLibrary/IAP/KvittoReceiptReader.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUI/IAP/KvittoReceiptReader.swift
rename to Passepartout/Library/Sources/AppLibrary/IAP/KvittoReceiptReader.swift
diff --git a/Passepartout/Library/Sources/AppUI/IAP/PaywallReason.swift b/Passepartout/Library/Sources/AppLibrary/IAP/PaywallReason.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUI/IAP/PaywallReason.swift
rename to Passepartout/Library/Sources/AppLibrary/IAP/PaywallReason.swift
diff --git a/Passepartout/Library/Sources/AppUI/Mock/MockAppProductHelper.swift b/Passepartout/Library/Sources/AppLibrary/Mock/MockAppProductHelper.swift
similarity index 76%
rename from Passepartout/Library/Sources/AppUI/Mock/MockAppProductHelper.swift
rename to Passepartout/Library/Sources/AppLibrary/Mock/MockAppProductHelper.swift
index 13e2b584..84f5524c 100644
--- a/Passepartout/Library/Sources/AppUI/Mock/MockAppProductHelper.swift
+++ b/Passepartout/Library/Sources/AppLibrary/Mock/MockAppProductHelper.swift
@@ -26,18 +26,18 @@
import Foundation
import UtilsLibrary
-actor MockAppProductHelper: AppProductHelper {
- private(set) var products: [AppProduct: InAppProduct]
+public actor MockAppProductHelper: AppProductHelper {
+ public private(set) var products: [AppProduct: InAppProduct]
- init() {
+ public init() {
products = [:]
}
- nonisolated var canMakePurchases: Bool {
+ public nonisolated var canMakePurchases: Bool {
true
}
- func fetchProducts() async throws {
+ public func fetchProducts() async throws {
products = AppProduct.all.reduce(into: [:]) {
$0[$1] = InAppProduct(
productIdentifier: $1.rawValue,
@@ -48,10 +48,10 @@ actor MockAppProductHelper: AppProductHelper {
}
}
- func purchase(productWithIdentifier productIdentifier: AppProduct) async throws -> InAppPurchaseResult {
+ public func purchase(productWithIdentifier productIdentifier: AppProduct) async throws -> InAppPurchaseResult {
.done
}
- func restorePurchases() async throws {
+ public func restorePurchases() async throws {
}
}
diff --git a/Passepartout/Library/Sources/AppUI/Mock/MockAppReceiptReader.swift b/Passepartout/Library/Sources/AppLibrary/Mock/MockAppReceiptReader.swift
similarity index 82%
rename from Passepartout/Library/Sources/AppUI/Mock/MockAppReceiptReader.swift
rename to Passepartout/Library/Sources/AppLibrary/Mock/MockAppReceiptReader.swift
index f12abbfe..78628bfe 100644
--- a/Passepartout/Library/Sources/AppUI/Mock/MockAppReceiptReader.swift
+++ b/Passepartout/Library/Sources/AppLibrary/Mock/MockAppReceiptReader.swift
@@ -26,14 +26,14 @@
import Foundation
import UtilsLibrary
-actor MockReceiptReader: AppReceiptReader {
+public actor MockAppReceiptReader: AppReceiptReader {
private var receipt: InAppReceipt?
- init(receipt: InAppReceipt? = nil) {
+ public init(receipt: InAppReceipt? = nil) {
self.receipt = receipt
}
- func setReceipt(withBuild build: Int, products: [AppProduct], cancelledProducts: Set = []) {
+ public func setReceipt(withBuild build: Int, products: [AppProduct], cancelledProducts: Set = []) {
receipt = InAppReceipt(originalBuildNumber: build, purchaseReceipts: products.map {
.init(productIdentifier: $0.rawValue,
cancellationDate: cancelledProducts.contains($0) ? Date() : nil,
@@ -41,7 +41,7 @@ actor MockReceiptReader: AppReceiptReader {
})
}
- func receipt(for userLevel: AppUserLevel) -> InAppReceipt? {
+ public func receipt(for userLevel: AppUserLevel) -> InAppReceipt? {
receipt
}
}
diff --git a/Passepartout/Library/Sources/AppUI/AppUI.swift b/Passepartout/Library/Sources/AppUI/AppUI.swift
index cd87279b..e071f2e2 100644
--- a/Passepartout/Library/Sources/AppUI/AppUI.swift
+++ b/Passepartout/Library/Sources/AppUI/AppUI.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import APILibrary
import CommonLibrary
import Foundation
import PassepartoutKit
diff --git a/Passepartout/Library/Sources/AppUI/UI/ProfileEditor+UI.swift b/Passepartout/Library/Sources/AppUI/Extensions/ProfileEditor+UI.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUI/UI/ProfileEditor+UI.swift
rename to Passepartout/Library/Sources/AppUI/Extensions/ProfileEditor+UI.swift
diff --git a/Passepartout/Library/Sources/AppUI/Mock/Mock.swift b/Passepartout/Library/Sources/AppUI/Mock/Mock.swift
index f0033b12..8f53f995 100644
--- a/Passepartout/Library/Sources/AppUI/Mock/Mock.swift
+++ b/Passepartout/Library/Sources/AppUI/Mock/Mock.swift
@@ -37,7 +37,7 @@ extension AppContext {
return AppContext(
iapManager: IAPManager(
customUserLevel: nil,
- receiptReader: MockReceiptReader(),
+ receiptReader: MockAppReceiptReader(),
unrestrictedFeatures: [
.interactiveLogin,
.onDemand,
diff --git a/Passepartout/Library/Sources/AppUIMain/Protocols/InteractiveViewProviding.swift b/Passepartout/Library/Sources/AppUI/Protocols/InteractiveViewProviding.swift
similarity index 96%
rename from Passepartout/Library/Sources/AppUIMain/Protocols/InteractiveViewProviding.swift
rename to Passepartout/Library/Sources/AppUI/Protocols/InteractiveViewProviding.swift
index b79ca17d..31063522 100644
--- a/Passepartout/Library/Sources/AppUIMain/Protocols/InteractiveViewProviding.swift
+++ b/Passepartout/Library/Sources/AppUI/Protocols/InteractiveViewProviding.swift
@@ -25,7 +25,7 @@
import SwiftUI
-protocol InteractiveViewProviding {
+public protocol InteractiveViewProviding {
associatedtype InteractiveContent: View
@MainActor
diff --git a/Passepartout/Library/Sources/AppUI/Theme/Platforms/Theme+iOS.swift b/Passepartout/Library/Sources/AppUI/Theme/Platforms/Theme+iOS.swift
index 99650a85..328cb5df 100644
--- a/Passepartout/Library/Sources/AppUI/Theme/Platforms/Theme+iOS.swift
+++ b/Passepartout/Library/Sources/AppUI/Theme/Platforms/Theme+iOS.swift
@@ -26,6 +26,7 @@
#if os(iOS)
import SwiftUI
+import UtilsLibrary
extension Theme {
public convenience init() {
@@ -35,7 +36,62 @@ extension Theme {
}
}
-// MARK: - Modifiers
+// MARK: - Shortcuts
+
+extension View {
+ public func themePopover(
+ isPresented: Binding,
+ content: @escaping () -> Content
+ ) -> some View where Content: View {
+ modifier(ThemeBooleanPopoverModifier(
+ isPresented: isPresented,
+ popover: content
+ ))
+ }
+
+ public func themeLockScreen() -> some View {
+ modifier(ThemeLockScreenModifier(lockedContent: LogoView.init))
+ }
+}
+
+// MARK: - Presentation modifiers
+
+struct ThemeBooleanPopoverModifier: ViewModifier, SizeClassProviding where Popover: View {
+
+ @EnvironmentObject
+ private var theme: Theme
+
+ @Environment(\.horizontalSizeClass)
+ var hsClass
+
+ @Environment(\.verticalSizeClass)
+ var vsClass
+
+ @Binding
+ var isPresented: Bool
+
+ @ViewBuilder
+ let popover: Popover
+
+ func body(content: Content) -> some View {
+ if isBigDevice {
+ content
+ .popover(isPresented: $isPresented) {
+ popover
+ .frame(minWidth: theme.popoverSize?.width, minHeight: theme.popoverSize?.height)
+ .themeLockScreen()
+ }
+ } else {
+ content
+ .sheet(isPresented: $isPresented) {
+ popover
+ .themeLockScreen()
+ }
+ }
+ }
+}
+
+// MARK: - Content modifiers
extension ThemeWindowModifier {
func body(content: Content) -> some View {
diff --git a/Passepartout/Library/Sources/AppUI/Theme/Platforms/Theme+macOS.swift b/Passepartout/Library/Sources/AppUI/Theme/Platforms/Theme+macOS.swift
index 57a74e2e..a1186ad8 100644
--- a/Passepartout/Library/Sources/AppUI/Theme/Platforms/Theme+macOS.swift
+++ b/Passepartout/Library/Sources/AppUI/Theme/Platforms/Theme+macOS.swift
@@ -36,6 +36,14 @@ extension Theme {
}
}
+// MARK: - Shortcuts
+
+extension View {
+ public func themeLockScreen() -> some View {
+ self
+ }
+}
+
// MARK: - Modifiers
extension ThemeWindowModifier {
diff --git a/Passepartout/Library/Sources/AppUI/Theme/Platforms/Theme+tvOS.swift b/Passepartout/Library/Sources/AppUI/Theme/Platforms/Theme+tvOS.swift
index 8bdee039..fff06092 100644
--- a/Passepartout/Library/Sources/AppUI/Theme/Platforms/Theme+tvOS.swift
+++ b/Passepartout/Library/Sources/AppUI/Theme/Platforms/Theme+tvOS.swift
@@ -33,4 +33,48 @@ extension Theme {
}
}
+// MARK: - Shortcuts
+
+extension View {
+ public func themeLockScreen() -> some View {
+ self
+ }
+}
+
+// MARK: - Modifiers
+
+extension ThemeManualInputModifier {
+ func body(content: Content) -> some View {
+ content
+ .autocorrectionDisabled()
+ .textInputAutocapitalization(.never)
+ }
+}
+
+extension ThemeSectionWithHeaderFooterModifier {
+ func body(content: Content) -> some View {
+ Section {
+ content
+ } header: {
+ header.map(Text.init)
+ } footer: {
+ footer.map(Text.init)
+ }
+ }
+}
+
+// MARK: - Views
+
+extension ThemeTextField {
+ public var body: some View {
+ commonView
+ }
+}
+
+extension ThemeSecureField {
+ public var body: some View {
+ commonView
+ }
+}
+
#endif
diff --git a/Passepartout/Library/Sources/AppUI/UI/ExtendedTunnel+Theme.swift b/Passepartout/Library/Sources/AppUI/Theme/Theme+Extensions.swift
similarity index 97%
rename from Passepartout/Library/Sources/AppUI/UI/ExtendedTunnel+Theme.swift
rename to Passepartout/Library/Sources/AppUI/Theme/Theme+Extensions.swift
index dd565155..494c77b7 100644
--- a/Passepartout/Library/Sources/AppUI/UI/ExtendedTunnel+Theme.swift
+++ b/Passepartout/Library/Sources/AppUI/Theme/Theme+Extensions.swift
@@ -1,5 +1,5 @@
//
-// ExtendedTunnel+Theme.swift
+// Theme+Extensions.swift
// Passepartout
//
// Created by Davide De Rosa on 9/6/24.
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import AppLibrary
import PassepartoutKit
import SwiftUI
diff --git a/Passepartout/Library/Sources/AppUI/Theme/Theme.swift b/Passepartout/Library/Sources/AppUI/Theme/Theme.swift
index b084a961..b0ca3a51 100644
--- a/Passepartout/Library/Sources/AppUI/Theme/Theme.swift
+++ b/Passepartout/Library/Sources/AppUI/Theme/Theme.swift
@@ -28,6 +28,10 @@ import UtilsLibrary
@MainActor
public final class Theme: ObservableObject {
+ private var animation: Animation = .spring
+
+ public internal(set) var animationCategories: Set = Set(ThemeAnimationCategory.allCases)
+
public internal(set) var rootModalSize: CGSize?
public internal(set) var secondaryModalSize: CGSize?
@@ -64,10 +68,6 @@ public final class Theme: ObservableObject {
public internal(set) var errorColor: Color = .red
- private var animation: Animation = .spring
-
- public internal(set) var animationCategories: Set = Set(ThemeAnimationCategory.allCases)
-
public internal(set) var logoImage = "Logo"
public internal(set) var systemImageName: (ImageName) -> String = Theme.ImageName.defaultSystemName
@@ -81,163 +81,3 @@ public final class Theme: ObservableObject {
animationCategories.contains(category) ? animation : nil
}
}
-
-#if !os(tvOS)
-
-// MARK: - Modifiers
-
-extension View {
- public func themeWindow(width: CGFloat, height: CGFloat) -> some View {
- modifier(ThemeWindowModifier(size: .init(width: width, height: height)))
- }
-
- public func themeNavigationDetail() -> some View {
- modifier(ThemeNavigationDetailModifier())
- }
-
- public func themeForm() -> some View {
- modifier(ThemeFormModifier())
- }
-
- public func themeModal(
- isPresented: Binding,
- isRoot: Bool = false,
- isInteractive: Bool = true,
- content: @escaping () -> Content
- ) -> some View where Content: View {
- modifier(ThemeBooleanModalModifier(
- isPresented: isPresented,
- isRoot: isRoot,
- isInteractive: isInteractive,
- modal: content
- ))
- }
-
- public func themeModal(
- item: Binding,
- isRoot: Bool = false,
- isInteractive: Bool = true,
- content: @escaping (T) -> Content
- ) -> some View where Content: View, T: Identifiable {
- modifier(ThemeItemModalModifier(
- item: item,
- isRoot: isRoot,
- isInteractive: isInteractive,
- modal: content
- ))
- }
-
- public func themePopover(
- isPresented: Binding,
- content: @escaping () -> Content
- ) -> some View where Content: View {
- modifier(ThemeBooleanPopoverModifier(
- isPresented: isPresented,
- popover: content
- ))
- }
-
- public func themeConfirmation(
- isPresented: Binding,
- title: String,
- isDestructive: Bool = false,
- action: @escaping () -> Void
- ) -> some View {
- modifier(ThemeConfirmationModifier(
- isPresented: isPresented,
- title: title,
- isDestructive: isDestructive,
- action: action
- ))
- }
-
- public func themeNavigationStack(if condition: Bool, closable: Bool = false, path: Binding) -> some View {
- modifier(ThemeNavigationStackModifier(condition: condition, closable: closable, path: path))
- }
-
- public func themePlainButton(action: @escaping () -> Void) -> some View {
- modifier(ThemePlainButtonModifier(action: action))
- }
-
- public func themeManualInput() -> some View {
- modifier(ThemeManualInputModifier())
- }
-
- @ViewBuilder
- public func themeMultiLine(_ isMultiLine: Bool) -> some View {
- if isMultiLine {
- multilineTextAlignment(.leading)
- } else {
- themeTruncating()
- }
- }
-
- public func themeTruncating(_ mode: Text.TruncationMode = .middle) -> some View {
- lineLimit(1)
- .truncationMode(mode)
- }
-
- public func themeEmptyMessage() -> some View {
- modifier(ThemeEmptyMessageModifier())
- }
-
- public func themeError(_ isError: Bool) -> some View {
- modifier(ThemeErrorModifier(isError: isError))
- }
-
- public func themeAnimation(on value: T, category: ThemeAnimationCategory) -> some View where T: Equatable {
- modifier(ThemeAnimationModifier(value: value, category: category))
- }
-
- public func themeTrailingValue(_ value: CustomStringConvertible?, truncationMode: Text.TruncationMode = .tail) -> some View {
- modifier(ThemeTrailingValueModifier(value: value, truncationMode: truncationMode))
- }
-
- public func themeSection(header: String? = nil, footer: String? = nil) -> some View {
- modifier(ThemeSectionWithHeaderFooterModifier(header: header, footer: footer))
- }
-
- public func themeGridHeader(title: String?) -> some View {
- modifier(ThemeGridSectionModifier(title: title))
- }
-
- public func themeGridCell(isSelected: Bool) -> some View {
- modifier(ThemeGridCellModifier(isSelected: isSelected))
- }
-
- public func themeHoverListRow() -> some View {
- modifier(ThemeHoverListRowModifier())
- }
-
- public func themeLockScreen() -> some View {
- modifier(ThemeLockScreenModifier(lockedContent: LogoView.init))
- }
-
- public func themeTip(_ text: String, edge: Edge) -> some View {
- modifier(ThemeTipModifier(text: text, edge: edge))
- }
-}
-
-// MARK: - Views
-
-extension Theme {
- public func listSection(
- _ title: String,
- addTitle: String,
- originalItems: Binding<[T]>,
- emptyValue: (() async -> T)? = nil,
- @ViewBuilder itemLabel: @escaping (Bool, Binding) -> ItemView
- ) -> some View {
- EditableListSection(
- title,
- addTitle: addTitle,
- originalItems: originalItems,
- emptyValue: emptyValue,
- itemLabel: itemLabel,
- removeLabel: ThemeEditableListSection.RemoveLabel.init(action:),
- editLabel: ThemeEditableListSection.EditLabel.init
- )
- }
-}
-
-#endif
diff --git a/Passepartout/Library/Sources/AppUI/Theme/ThemeAnimationCategory.swift b/Passepartout/Library/Sources/AppUI/Theme/ThemeAnimationCategory.swift
new file mode 100644
index 00000000..e02f256a
--- /dev/null
+++ b/Passepartout/Library/Sources/AppUI/Theme/ThemeAnimationCategory.swift
@@ -0,0 +1,38 @@
+//
+// ThemeAnimationCategory.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 11/1/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 .
+//
+
+import Foundation
+
+public enum ThemeAnimationCategory: CaseIterable {
+ case diagnostics
+
+ case modules
+
+ case profiles
+
+ case profilesLayout
+
+ case providers
+}
diff --git a/Passepartout/Library/Sources/AppUI/Theme/Theme+UI.swift b/Passepartout/Library/Sources/AppUI/Theme/UI/Theme+Modifiers.swift
similarity index 54%
rename from Passepartout/Library/Sources/AppUI/Theme/Theme+UI.swift
rename to Passepartout/Library/Sources/AppUI/Theme/UI/Theme+Modifiers.swift
index 7b892394..80bed062 100644
--- a/Passepartout/Library/Sources/AppUI/Theme/Theme+UI.swift
+++ b/Passepartout/Library/Sources/AppUI/Theme/UI/Theme+Modifiers.swift
@@ -1,8 +1,8 @@
//
-// Theme+UI.swift
+// Theme+Modifiers.swift
// Passepartout
//
-// Created by Davide De Rosa on 8/28/24.
+// Created by Davide De Rosa on 11/1/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
@@ -30,24 +30,130 @@ import LocalAuthentication
import SwiftUI
import UtilsLibrary
-// MARK: - Modifiers
+// MARK: Shortcuts
+
+extension View {
+ public func themeModal(
+ isPresented: Binding,
+ isRoot: Bool = false,
+ isInteractive: Bool = true,
+ content: @escaping () -> Content
+ ) -> some View where Content: View {
+ modifier(ThemeBooleanModalModifier(
+ isPresented: isPresented,
+ isRoot: isRoot,
+ isInteractive: isInteractive,
+ modal: content
+ ))
+ }
+
+ public func themeModal(
+ item: Binding,
+ isRoot: Bool = false,
+ isInteractive: Bool = true,
+ content: @escaping (T) -> Content
+ ) -> some View where Content: View, T: Identifiable {
+ modifier(ThemeItemModalModifier(
+ item: item,
+ isRoot: isRoot,
+ isInteractive: isInteractive,
+ modal: content
+ ))
+ }
+
+ public func themeConfirmation(
+ isPresented: Binding,
+ title: String,
+ isDestructive: Bool = false,
+ action: @escaping () -> Void
+ ) -> some View {
+ modifier(ThemeConfirmationModifier(
+ isPresented: isPresented,
+ title: title,
+ isDestructive: isDestructive,
+ action: action
+ ))
+ }
+
+ public func themeNavigationStack(if condition: Bool, closable: Bool = false, path: Binding) -> some View {
+ modifier(ThemeNavigationStackModifier(condition: condition, closable: closable, path: path))
+ }
+
+ public func themeForm() -> some View {
+ modifier(ThemeFormModifier())
+ }
+
+ public func themeManualInput() -> some View {
+ modifier(ThemeManualInputModifier())
+ }
+
+ public func themeSection(header: String? = nil, footer: String? = nil) -> some View {
+ modifier(ThemeSectionWithHeaderFooterModifier(header: header, footer: footer))
+ }
#if !os(tvOS)
-
-struct ThemeWindowModifier: ViewModifier {
- let size: CGSize
-}
-
-struct ThemeNavigationDetailModifier: ViewModifier {
-}
-
-struct ThemeFormModifier: ViewModifier {
- func body(content: Content) -> some View {
- content
- .formStyle(.grouped)
+ public func themeWindow(width: CGFloat, height: CGFloat) -> some View {
+ modifier(ThemeWindowModifier(size: .init(width: width, height: height)))
}
+
+ public func themeNavigationDetail() -> some View {
+ modifier(ThemeNavigationDetailModifier())
+ }
+
+ public func themePlainButton(action: @escaping () -> Void) -> some View {
+ modifier(ThemePlainButtonModifier(action: action))
+ }
+
+ @ViewBuilder
+ public func themeMultiLine(_ isMultiLine: Bool) -> some View {
+ if isMultiLine {
+ multilineTextAlignment(.leading)
+ } else {
+ themeTruncating()
+ }
+ }
+
+ public func themeTruncating(_ mode: Text.TruncationMode = .middle) -> some View {
+ lineLimit(1)
+ .truncationMode(mode)
+ }
+
+ public func themeEmptyMessage() -> some View {
+ modifier(ThemeEmptyMessageModifier())
+ }
+
+ public func themeError(_ isError: Bool) -> some View {
+ modifier(ThemeErrorModifier(isError: isError))
+ }
+
+ public func themeAnimation(on value: T, category: ThemeAnimationCategory) -> some View where T: Equatable {
+ modifier(ThemeAnimationModifier(value: value, category: category))
+ }
+
+ public func themeTrailingValue(_ value: CustomStringConvertible?, truncationMode: Text.TruncationMode = .tail) -> some View {
+ modifier(ThemeTrailingValueModifier(value: value, truncationMode: truncationMode))
+ }
+
+ public func themeGridHeader(title: String?) -> some View {
+ modifier(ThemeGridSectionModifier(title: title))
+ }
+
+ public func themeGridCell(isSelected: Bool) -> some View {
+ modifier(ThemeGridCellModifier(isSelected: isSelected))
+ }
+
+ public func themeHoverListRow() -> some View {
+ modifier(ThemeHoverListRowModifier())
+ }
+
+ public func themeTip(_ text: String, edge: Edge) -> some View {
+ modifier(ThemeTipModifier(text: text, edge: edge))
+ }
+#endif
}
+// MARK: - Presentation modifiers
+
struct ThemeBooleanModalModifier: ViewModifier where Modal: View {
@EnvironmentObject
@@ -106,41 +212,6 @@ struct ThemeItemModalModifier: ViewModifier where Modal: View, T: Iden
}
}
-struct ThemeBooleanPopoverModifier: ViewModifier, SizeClassProviding where Popover: View {
-
- @EnvironmentObject
- private var theme: Theme
-
- @Environment(\.horizontalSizeClass)
- var hsClass
-
- @Environment(\.verticalSizeClass)
- var vsClass
-
- @Binding
- var isPresented: Bool
-
- @ViewBuilder
- let popover: Popover
-
- func body(content: Content) -> some View {
- if isBigDevice {
- content
- .popover(isPresented: $isPresented) {
- popover
- .frame(minWidth: theme.popoverSize?.width, minHeight: theme.popoverSize?.height)
- .themeLockScreen()
- }
- } else {
- content
- .sheet(isPresented: $isPresented) {
- popover
- .themeLockScreen()
- }
- }
- }
-}
-
struct ThemeConfirmationModifier: ViewModifier {
@Binding
@@ -197,13 +268,37 @@ struct ThemeNavigationStackModifier: ViewModifier {
}
}
-struct ThemePlainButtonModifier: ViewModifier {
- let action: () -> Void
+// MARK: - Content modifiers
+
+struct ThemeFormModifier: ViewModifier {
+ func body(content: Content) -> some View {
+ content
+ .formStyle(.grouped)
+ }
}
struct ThemeManualInputModifier: ViewModifier {
}
+struct ThemeSectionWithHeaderFooterModifier: ViewModifier {
+ let header: String?
+
+ let footer: String?
+}
+
+#if !os(tvOS)
+
+struct ThemeWindowModifier: ViewModifier {
+ let size: CGSize
+}
+
+struct ThemeNavigationDetailModifier: ViewModifier {
+}
+
+struct ThemePlainButtonModifier: ViewModifier {
+ let action: () -> Void
+}
+
struct ThemeEmptyMessageModifier: ViewModifier {
@EnvironmentObject
@@ -268,12 +363,6 @@ struct ThemeTrailingValueModifier: ViewModifier {
}
}
-struct ThemeSectionWithHeaderFooterModifier: ViewModifier {
- let header: String?
-
- let footer: String?
-}
-
struct ThemeGridSectionModifier: ViewModifier {
@EnvironmentObject
@@ -394,312 +483,3 @@ struct ThemeTipModifier: ViewModifier {
}
#endif
-
-// MARK: - Views
-
-public enum ThemeAnimationCategory: CaseIterable {
- case diagnostics
-
- case modules
-
- case profiles
-
- case profilesLayout
-
- case providers
-}
-
-public struct ThemeImage: View {
-
- @EnvironmentObject
- private var theme: Theme
-
- private let name: Theme.ImageName
-
- public init(_ name: Theme.ImageName) {
- self.name = name
- }
-
- public var body: some View {
- Image(systemName: theme.systemImageName(name))
- }
-}
-
-public struct ThemeImageLabel: View {
-
- @EnvironmentObject
- private var theme: Theme
-
- private let title: String
-
- private let name: Theme.ImageName
-
- public init(_ title: String, _ name: Theme.ImageName) {
- self.title = title
- self.name = name
- }
-
- public var body: some View {
- Label {
- Text(title)
- } icon: {
- ThemeImage(name)
- }
- }
-}
-
-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 {
- if let code {
- text(withString: code.asCountryCodeEmoji, tip: countryTip?(code))
- } else {
- text(withString: "🌐", tip: placeholderTip)
- }
- }
-
- @ViewBuilder
- private func text(withString string: String, tip: String?) -> some View {
- if let tip {
- Text(verbatim: string)
- .help(tip)
- } else {
- Text(verbatim: string)
- }
- }
-}
-
-#if !os(tvOS)
-
-public struct ThemeMenuImage: View {
-
- @EnvironmentObject
- private var theme: Theme
-
- private let name: Theme.MenuImageName
-
- public init(_ name: Theme.MenuImageName) {
- self.name = name
- }
-
- public var body: some View {
- Image(theme.menuImageName(name))
- }
-}
-
-public struct ThemeDisclosableMenu: View where Content: View, Label: View {
-
- @ViewBuilder
- private let content: () -> Content
-
- @ViewBuilder
- private let label: () -> Label
-
- 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()
- ThemeImage(.disclose)
- }
- .contentShape(.rect)
- }
- .foregroundStyle(.primary)
-#if os(macOS)
- .buttonStyle(.plain)
-#endif
- }
-}
-
-public struct ThemeCopiableText: View where Value: CustomStringConvertible, ValueView: View {
-
- @EnvironmentObject
- private var theme: Theme
-
- private let title: String?
-
- private let value: Value
-
- private let isMultiLine: Bool
-
- private let valueView: (Value) -> ValueView
-
- 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)
- Spacer()
- }
- valueView(value)
- .foregroundStyle(title == nil ? theme.titleColor : theme.valueColor)
- .themeMultiLine(isMultiLine)
- if title == nil {
- Spacer()
- }
- Button {
- copyToPasteboard(value.description)
- } label: {
- ThemeImage(.copy)
- }
- // TODO: #584, necessary to avoid cell selection
- .buttonStyle(.borderless)
- }
- }
-}
-
-public struct ThemeTappableText: View {
- private let title: String
-
- private let action: () -> Void
-
- public init(title: String, action: @escaping () -> Void) {
- self.title = title
- self.action = action
- }
-
- var commonView: some View {
- Button(action: action) {
- Text(title)
- .themeTruncating()
- }
- }
-}
-
-public struct ThemeTextField: View {
- private let title: String?
-
- @Binding
- private var text: String
-
- private let placeholder: String
-
- public init(_ title: String, text: Binding, placeholder: String) {
- self.title = title
- _text = text
- self.placeholder = placeholder
- }
-
- @ViewBuilder
- var commonView: some View {
- if let title {
- LabeledContent {
- fieldView
- } label: {
- Text(title)
- }
- } else {
- fieldView
- }
- }
-
- private var fieldView: some View {
- TextField(title ?? "", text: $text, prompt: Text(placeholder))
- }
-}
-
-public struct ThemeSecureField: View {
- private let title: String?
-
- @Binding
- private var text: String
-
- private let placeholder: String
-
- public init(title: String?, text: Binding, placeholder: String) {
- self.title = title
- _text = text
- self.placeholder = placeholder
- }
-
- @ViewBuilder
- var commonView: some View {
- if let title {
- LabeledContent {
- fieldView
- } label: {
- Text(title)
- }
- } else {
- fieldView
- }
- }
-
- private var fieldView: some View {
- RevealingSecureField(title ?? "", text: $text, prompt: Text(placeholder), imageWidth: 30.0) {
- ThemeImage(.hide)
- .foregroundStyle(Color.accentColor)
- } revealImage: {
- ThemeImage(.show)
- .foregroundStyle(Color.accentColor)
- }
- }
-}
-
-public struct ThemeRemovableItemRow: View where ItemView: View {
- private let isEditing: Bool
-
- @ViewBuilder
- private let itemView: () -> ItemView
-
- let removeAction: () -> Void
-
- 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,
- removeView: removeView
- )
- }
-}
-
-public enum ThemeEditableListSection {
- public struct RemoveLabel: View {
- let action: () -> Void
-
- public init(action: @escaping () -> Void) {
- self.action = action
- }
- }
-
- public struct EditLabel: View {
- }
-}
-
-#endif
diff --git a/Passepartout/Library/Sources/AppUI/Theme/UI/Theme+Views.swift b/Passepartout/Library/Sources/AppUI/Theme/UI/Theme+Views.swift
new file mode 100644
index 00000000..4b93c567
--- /dev/null
+++ b/Passepartout/Library/Sources/AppUI/Theme/UI/Theme+Views.swift
@@ -0,0 +1,348 @@
+//
+// Theme+Views.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 11/1/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 .
+//
+
+import SwiftUI
+import UtilsLibrary
+
+// MARK: Shortcuts
+
+#if !os(tvOS)
+extension Theme {
+ public func listSection(
+ _ title: String,
+ addTitle: String,
+ originalItems: Binding<[T]>,
+ emptyValue: (() async -> T)? = nil,
+ @ViewBuilder itemLabel: @escaping (Bool, Binding) -> ItemView
+ ) -> some View {
+ EditableListSection(
+ title,
+ addTitle: addTitle,
+ originalItems: originalItems,
+ emptyValue: emptyValue,
+ itemLabel: itemLabel,
+ removeLabel: ThemeEditableListSection.RemoveLabel.init(action:),
+ editLabel: ThemeEditableListSection.EditLabel.init
+ )
+ }
+}
+#endif
+
+// MARK: - Views
+
+public struct ThemeImage: View {
+
+ @EnvironmentObject
+ private var theme: Theme
+
+ private let name: Theme.ImageName
+
+ public init(_ name: Theme.ImageName) {
+ self.name = name
+ }
+
+ public var body: some View {
+ Image(systemName: theme.systemImageName(name))
+ }
+}
+
+public struct ThemeImageLabel: View {
+
+ @EnvironmentObject
+ private var theme: Theme
+
+ private let title: String
+
+ private let name: Theme.ImageName
+
+ public init(_ title: String, _ name: Theme.ImageName) {
+ self.title = title
+ self.name = name
+ }
+
+ public var body: some View {
+ Label {
+ Text(title)
+ } icon: {
+ ThemeImage(name)
+ }
+ }
+}
+
+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 {
+ if let code {
+ text(withString: code.asCountryCodeEmoji, tip: countryTip?(code))
+ } else {
+ text(withString: "🌐", tip: placeholderTip)
+ }
+ }
+
+ @ViewBuilder
+ private func text(withString string: String, tip: String?) -> some View {
+ if let tip {
+ Text(verbatim: string)
+ .help(tip)
+ } else {
+ Text(verbatim: string)
+ }
+ }
+}
+
+public struct ThemeTextField: View {
+ private let title: String?
+
+ @Binding
+ private var text: String
+
+ private let placeholder: String
+
+ public init(_ title: String, text: Binding, placeholder: String) {
+ self.title = title
+ _text = text
+ self.placeholder = placeholder
+ }
+
+ @ViewBuilder
+ var commonView: some View {
+ if let title {
+ LabeledContent {
+ fieldView
+ } label: {
+ Text(title)
+ }
+ } else {
+ fieldView
+ }
+ }
+
+ private var fieldView: some View {
+ TextField(title ?? "", text: $text, prompt: Text(placeholder))
+ }
+}
+
+public struct ThemeSecureField: View {
+ private let title: String?
+
+ @Binding
+ private var text: String
+
+ private let placeholder: String
+
+ public init(title: String?, text: Binding, placeholder: String) {
+ self.title = title
+ _text = text
+ self.placeholder = placeholder
+ }
+
+ @ViewBuilder
+ var commonView: some View {
+ if let title {
+ LabeledContent {
+ fieldView
+ } label: {
+ Text(title)
+ }
+ } else {
+ fieldView
+ }
+ }
+
+ private var fieldView: some View {
+ RevealingSecureField(title ?? "", text: $text, prompt: Text(placeholder), imageWidth: 30.0) {
+ ThemeImage(.hide)
+ .foregroundStyle(Color.accentColor)
+ } revealImage: {
+ ThemeImage(.show)
+ .foregroundStyle(Color.accentColor)
+ }
+ }
+}
+
+#if !os(tvOS)
+
+public struct ThemeMenuImage: View {
+
+ @EnvironmentObject
+ private var theme: Theme
+
+ private let name: Theme.MenuImageName
+
+ public init(_ name: Theme.MenuImageName) {
+ self.name = name
+ }
+
+ public var body: some View {
+ Image(theme.menuImageName(name))
+ }
+}
+
+public struct ThemeDisclosableMenu: View where Content: View, Label: View {
+
+ @ViewBuilder
+ private let content: () -> Content
+
+ @ViewBuilder
+ private let label: () -> Label
+
+ 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()
+ ThemeImage(.disclose)
+ }
+ .contentShape(.rect)
+ }
+ .foregroundStyle(.primary)
+#if os(macOS)
+ .buttonStyle(.plain)
+#endif
+ }
+}
+
+public struct ThemeCopiableText: View where Value: CustomStringConvertible, ValueView: View {
+
+ @EnvironmentObject
+ private var theme: Theme
+
+ private let title: String?
+
+ private let value: Value
+
+ private let isMultiLine: Bool
+
+ private let valueView: (Value) -> ValueView
+
+ 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)
+ Spacer()
+ }
+ valueView(value)
+ .foregroundStyle(title == nil ? theme.titleColor : theme.valueColor)
+ .themeMultiLine(isMultiLine)
+ if title == nil {
+ Spacer()
+ }
+ Button {
+ copyToPasteboard(value.description)
+ } label: {
+ ThemeImage(.copy)
+ }
+ // TODO: #584, necessary to avoid cell selection
+ .buttonStyle(.borderless)
+ }
+ }
+}
+
+public struct ThemeTappableText: View {
+ private let title: String
+
+ private let action: () -> Void
+
+ public init(title: String, action: @escaping () -> Void) {
+ self.title = title
+ self.action = action
+ }
+
+ var commonView: some View {
+ Button(action: action) {
+ Text(title)
+ .themeTruncating()
+ }
+ }
+}
+
+public struct ThemeRemovableItemRow: View where ItemView: View {
+ private let isEditing: Bool
+
+ @ViewBuilder
+ private let itemView: () -> ItemView
+
+ let removeAction: () -> Void
+
+ 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,
+ removeView: removeView
+ )
+ }
+}
+
+public enum ThemeEditableListSection {
+ public struct RemoveLabel: View {
+ let action: () -> Void
+
+ public init(action: @escaping () -> Void) {
+ self.action = action
+ }
+ }
+
+ public struct EditLabel: View {
+ }
+}
+
+#endif
diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/OpenVPNModule+Extensions.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/OpenVPNModule+Extensions.swift
new file mode 100644
index 00000000..dcf7eb5e
--- /dev/null
+++ b/Passepartout/Library/Sources/AppUI/Views/Modules/Extensions/OpenVPNModule+Extensions.swift
@@ -0,0 +1,39 @@
+//
+// OpenVPNModule+Extensions.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 11/1/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 .
+//
+
+import PassepartoutKit
+import SwiftUI
+
+extension OpenVPNModule.Builder: InteractiveViewProviding {
+ public func interactiveView(with editor: ProfileEditor) -> some View {
+ let draft = editor[self]
+
+ return OpenVPNCredentialsView(
+ isInteractive: draft.isInteractive,
+ credentials: draft.credentials,
+ isAuthenticating: true
+ )
+ }
+}
diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Modules/OpenVPNView+Credentials.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView+Credentials.swift
similarity index 73%
rename from Passepartout/Library/Sources/AppUIMain/Views/Modules/OpenVPNView+Credentials.swift
rename to Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView+Credentials.swift
index cfeecc5e..297d35ed 100644
--- a/Passepartout/Library/Sources/AppUIMain/Views/Modules/OpenVPNView+Credentials.swift
+++ b/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView+Credentials.swift
@@ -23,59 +23,66 @@
// along with Passepartout. If not, see .
//
+import AppLibrary
import PassepartoutKit
import SwiftUI
import UtilsLibrary
-extension OpenVPNView {
- struct CredentialsView: View {
+public struct OpenVPNCredentialsView: View {
- @EnvironmentObject
- private var iapManager: IAPManager
+ @EnvironmentObject
+ private var iapManager: IAPManager
- @Binding
- var isInteractive: Bool
+ @Binding
+ private var isInteractive: Bool
- @Binding
- var credentials: OpenVPN.Credentials?
+ @Binding
+ private var credentials: OpenVPN.Credentials?
- var isAuthenticating = false
+ private var isAuthenticating = false
- @State
- private var builder = OpenVPN.Credentials.Builder()
+ @State
+ private var builder = OpenVPN.Credentials.Builder()
- @State
- private var paywallReason: PaywallReason?
+ @State
+ private var paywallReason: PaywallReason?
- var body: some View {
- Form {
- restrictedArea
- inputSection
+ public init(
+ isInteractive: Binding,
+ credentials: Binding,
+ isAuthenticating: Bool = false
+ ) {
+ _isInteractive = isInteractive
+ _credentials = credentials
+ self.isAuthenticating = isAuthenticating
+ }
+
+ public var body: some View {
+ Form {
+ restrictedArea
+ inputSection
+ }
+ .themeManualInput()
+ .themeForm()
+ .navigationTitle(Strings.Modules.Openvpn.credentials)
+ .onLoad {
+ builder = credentials?.builder() ?? OpenVPN.Credentials.Builder()
+ builder.otp = nil
+ }
+ .onChange(of: builder) {
+ var copy = $0
+ if isEligibleForInteractiveLogin {
+ copy.otp = copy.otp ?? ""
+ } else {
+ copy.otpMethod = .none
+ copy.otp = nil
}
- .themeAnimation(on: isInteractive, category: .modules)
- .themeManualInput()
- .themeForm()
- .navigationTitle(Strings.Modules.Openvpn.credentials)
- .onLoad {
- builder = credentials?.builder() ?? OpenVPN.Credentials.Builder()
- builder.otp = nil
- }
- .onChange(of: builder) {
- var copy = $0
- if isEligibleForInteractiveLogin {
- copy.otp = copy.otp ?? ""
- } else {
- copy.otpMethod = .none
- copy.otp = nil
- }
- credentials = copy.build()
- }
- .modifier(PaywallModifier(reason: $paywallReason))
+ credentials = copy.build()
}
}
}
-private extension OpenVPNView.CredentialsView {
+private extension OpenVPNCredentialsView {
var isEligibleForInteractiveLogin: Bool {
iapManager.isEligible(for: .interactiveLogin)
}
@@ -164,7 +171,7 @@ private extension OpenVPNView.CredentialsView {
var isInteractive = true
return NavigationStack {
- OpenVPNView.CredentialsView(
+ OpenVPNCredentialsView(
isInteractive: $isInteractive,
credentials: $credentials
)
diff --git a/Passepartout/Library/Sources/AppUI/UI/ConnectionStatusView.swift b/Passepartout/Library/Sources/AppUI/Views/UI/ConnectionStatusView.swift
similarity index 99%
rename from Passepartout/Library/Sources/AppUI/UI/ConnectionStatusView.swift
rename to Passepartout/Library/Sources/AppUI/Views/UI/ConnectionStatusView.swift
index d163f444..cec6b47c 100644
--- a/Passepartout/Library/Sources/AppUI/UI/ConnectionStatusView.swift
+++ b/Passepartout/Library/Sources/AppUI/Views/UI/ConnectionStatusView.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import AppLibrary
import Foundation
import PassepartoutKit
import SwiftUI
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/FavoriteToggle.swift b/Passepartout/Library/Sources/AppUI/Views/UI/FavoriteToggle.swift
similarity index 81%
rename from Passepartout/Library/Sources/AppUIMain/UI/FavoriteToggle.swift
rename to Passepartout/Library/Sources/AppUI/Views/UI/FavoriteToggle.swift
index 6cb0742c..31463079 100644
--- a/Passepartout/Library/Sources/AppUIMain/UI/FavoriteToggle.swift
+++ b/Passepartout/Library/Sources/AppUI/Views/UI/FavoriteToggle.swift
@@ -1,5 +1,5 @@
//
-// ThemeFavoriteToggle.swift
+// FavoriteToggle.swift
// Passepartout
//
// Created by Davide De Rosa on 10/25/24.
@@ -25,16 +25,21 @@
import SwiftUI
-struct FavoriteToggle: View where ID: Hashable {
- let value: ID
+public struct FavoriteToggle: View where ID: Hashable {
+ private let value: ID
@Binding
- var selection: Set
+ private var selection: Set
@State
private var hover: ID?
- var body: some View {
+ public init(value: ID, selection: Binding>) {
+ self.value = value
+ _selection = selection
+ }
+
+ public var body: some View {
Button {
if selection.contains(value) {
selection.remove(value)
@@ -45,18 +50,20 @@ struct FavoriteToggle: View where ID: Hashable {
ThemeImage(selection.contains(value) ? .favoriteOn : .favoriteOff)
.opacity(opacity)
}
+#if os(macOS)
.onHover {
hover = $0 ? value : nil
}
+#endif
}
}
private extension FavoriteToggle {
var opacity: Double {
-#if os(iOS)
- 1.0
-#else
+#if os(macOS)
selection.contains(value) || value == hover ? 1.0 : 0.0
+#else
+ 1.0
#endif
}
}
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/InteractiveView.swift b/Passepartout/Library/Sources/AppUI/Views/UI/InteractiveView.swift
similarity index 88%
rename from Passepartout/Library/Sources/AppUIMain/UI/InteractiveView.swift
rename to Passepartout/Library/Sources/AppUI/Views/UI/InteractiveView.swift
index 11c25ad2..947f109f 100644
--- a/Passepartout/Library/Sources/AppUIMain/UI/InteractiveView.swift
+++ b/Passepartout/Library/Sources/AppUI/Views/UI/InteractiveView.swift
@@ -26,14 +26,19 @@
import PassepartoutKit
import SwiftUI
-struct InteractiveView: View {
+public struct InteractiveView: View {
@ObservedObject
- var manager: InteractiveManager
+ private var manager: InteractiveManager
- let onError: (Error) -> Void
+ private let onError: (Error) -> Void
- var body: some View {
+ public init(manager: InteractiveManager, onError: @escaping (Error) -> Void) {
+ self.manager = manager
+ self.onError = onError
+ }
+
+ public var body: some View {
manager
.editor
.interactiveProvider
diff --git a/Passepartout/Library/Sources/AppUI/UI/LogoView.swift b/Passepartout/Library/Sources/AppUI/Views/UI/LogoView.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUI/UI/LogoView.swift
rename to Passepartout/Library/Sources/AppUI/Views/UI/LogoView.swift
diff --git a/Passepartout/Library/Sources/AppUI/UI/TunnelToggleButton.swift b/Passepartout/Library/Sources/AppUI/Views/UI/TunnelToggleButton.swift
similarity index 99%
rename from Passepartout/Library/Sources/AppUI/UI/TunnelToggleButton.swift
rename to Passepartout/Library/Sources/AppUI/Views/UI/TunnelToggleButton.swift
index 93df8505..27512384 100644
--- a/Passepartout/Library/Sources/AppUI/UI/TunnelToggleButton.swift
+++ b/Passepartout/Library/Sources/AppUI/Views/UI/TunnelToggleButton.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import AppLibrary
import PassepartoutKit
import SwiftUI
import UtilsLibrary
diff --git a/Passepartout/Library/Sources/AppUIMain/Domain/Issue+Metadata.swift b/Passepartout/Library/Sources/AppUIMain/Domain/Issue+Metadata.swift
index 838a7498..cf858466 100644
--- a/Passepartout/Library/Sources/AppUIMain/Domain/Issue+Metadata.swift
+++ b/Passepartout/Library/Sources/AppUIMain/Domain/Issue+Metadata.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import AppLibrary
import CommonLibrary
import Foundation
import PassepartoutKit
diff --git a/Passepartout/Library/Sources/AppUIMain/Domain/Issue.swift b/Passepartout/Library/Sources/AppUIMain/Domain/Issue.swift
index ddb33e21..a4cf5526 100644
--- a/Passepartout/Library/Sources/AppUIMain/Domain/Issue.swift
+++ b/Passepartout/Library/Sources/AppUIMain/Domain/Issue.swift
@@ -25,6 +25,7 @@
#if os(iOS)
+import AppLibrary
import CommonLibrary
import Foundation
import PassepartoutKit
@@ -33,6 +34,7 @@ import UIKit
#else
import AppKit
+import AppLibrary
import CommonLibrary
import Foundation
import PassepartoutKit
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/EditableModule+Previews.swift b/Passepartout/Library/Sources/AppUIMain/Extensions/EditableModule+Previews.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUIMain/UI/EditableModule+Previews.swift
rename to Passepartout/Library/Sources/AppUIMain/Extensions/EditableModule+Previews.swift
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/DefaultModuleViewFactory.swift b/Passepartout/Library/Sources/AppUIMain/Protocols/DefaultModuleViewFactory.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUIMain/UI/DefaultModuleViewFactory.swift
rename to Passepartout/Library/Sources/AppUIMain/Protocols/DefaultModuleViewFactory.swift
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/AddProfileMenu.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/AddProfileMenu.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUIMain/UI/AddProfileMenu.swift
rename to Passepartout/Library/Sources/AppUIMain/Views/App/AddProfileMenu.swift
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/InstalledProfileView.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/InstalledProfileView.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUIMain/UI/InstalledProfileView.swift
rename to Passepartout/Library/Sources/AppUIMain/Views/App/InstalledProfileView.swift
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/ProfileCardView.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileCardView.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUIMain/UI/ProfileCardView.swift
rename to Passepartout/Library/Sources/AppUIMain/Views/App/ProfileCardView.swift
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/ProfileContextMenu.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileContextMenu.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUIMain/UI/ProfileContextMenu.swift
rename to Passepartout/Library/Sources/AppUIMain/Views/App/ProfileContextMenu.swift
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/ProfileDuplicateButton.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileDuplicateButton.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUIMain/UI/ProfileDuplicateButton.swift
rename to Passepartout/Library/Sources/AppUIMain/Views/App/ProfileDuplicateButton.swift
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/ProfileFlow.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileFlow.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUIMain/UI/ProfileFlow.swift
rename to Passepartout/Library/Sources/AppUIMain/Views/App/ProfileFlow.swift
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/ProfileInfoButton.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileInfoButton.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUIMain/UI/ProfileInfoButton.swift
rename to Passepartout/Library/Sources/AppUIMain/Views/App/ProfileInfoButton.swift
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/ProfileRemoveButton.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileRemoveButton.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUIMain/UI/ProfileRemoveButton.swift
rename to Passepartout/Library/Sources/AppUIMain/Views/App/ProfileRemoveButton.swift
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/ProfileRowView.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUIMain/UI/ProfileRowView.swift
rename to Passepartout/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/ProfilesLayoutPicker.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfilesLayoutPicker.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUIMain/UI/ProfilesLayoutPicker.swift
rename to Passepartout/Library/Sources/AppUIMain/Views/App/ProfilesLayoutPicker.swift
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/TunnelRestartButton.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/TunnelRestartButton.swift
similarity index 99%
rename from Passepartout/Library/Sources/AppUIMain/UI/TunnelRestartButton.swift
rename to Passepartout/Library/Sources/AppUIMain/Views/App/TunnelRestartButton.swift
index 691927ff..5f35a30b 100644
--- a/Passepartout/Library/Sources/AppUIMain/UI/TunnelRestartButton.swift
+++ b/Passepartout/Library/Sources/AppUIMain/Views/App/TunnelRestartButton.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import AppLibrary
import PassepartoutKit
import SwiftUI
import UtilsLibrary
diff --git a/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenuImage.swift b/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenuImage.swift
index dbe7cb91..559e022b 100644
--- a/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenuImage.swift
+++ b/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenuImage.swift
@@ -25,6 +25,7 @@
#if os(macOS)
+import AppLibrary
import PassepartoutKit
import SwiftUI
diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Diagnostics/DebugLogView.swift b/Passepartout/Library/Sources/AppUIMain/Views/Diagnostics/DebugLogView.swift
index f59eaf7a..95111661 100644
--- a/Passepartout/Library/Sources/AppUIMain/Views/Diagnostics/DebugLogView.swift
+++ b/Passepartout/Library/Sources/AppUIMain/Views/Diagnostics/DebugLogView.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import AppLibrary
import CommonLibrary
import PassepartoutKit
import SwiftUI
diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Modules/Extensions/OpenVPNModule+Extensions.swift b/Passepartout/Library/Sources/AppUIMain/Views/Modules/Extensions/OpenVPNModule+Extensions.swift
index e9d4a360..4016f7dc 100644
--- a/Passepartout/Library/Sources/AppUIMain/Views/Modules/Extensions/OpenVPNModule+Extensions.swift
+++ b/Passepartout/Library/Sources/AppUIMain/Views/Modules/Extensions/OpenVPNModule+Extensions.swift
@@ -32,18 +32,6 @@ extension OpenVPNModule.Builder: ModuleViewProviding {
}
}
-extension OpenVPNModule.Builder: InteractiveViewProviding {
- func interactiveView(with editor: ProfileEditor) -> some View {
- let draft = editor[self]
-
- return OpenVPNView.CredentialsView(
- isInteractive: draft.isInteractive,
- credentials: draft.credentials,
- isAuthenticating: true
- )
- }
-}
-
extension OpenVPNModule: ProviderEntityViewProviding {
func providerEntityView(
with provider: ModuleMetadata.Provider,
diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift b/Passepartout/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift
index 6ecdbb6f..239b0df8 100644
--- a/Passepartout/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift
+++ b/Passepartout/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import AppLibrary
import PassepartoutKit
import SwiftUI
import UtilsLibrary
diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift b/Passepartout/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift
index 7e5472d1..a05d499f 100644
--- a/Passepartout/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift
+++ b/Passepartout/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import AppLibrary
import CPassepartoutOpenVPNOpenSSL
import PassepartoutKit
import SwiftUI
@@ -225,10 +226,12 @@ private extension OpenVPNView {
}
case .credentials:
- CredentialsView(
+ OpenVPNCredentialsView(
isInteractive: draft.isInteractive,
credentials: draft.credentials
)
+ .themeAnimation(on: draft.wrappedValue.isInteractive, category: .modules)
+ .modifier(PaywallModifier(reason: $paywallReason))
}
}
}
diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Paywall/PaywallModifier.swift b/Passepartout/Library/Sources/AppUIMain/Views/Paywall/PaywallModifier.swift
index 70ff7bda..2847ca5e 100644
--- a/Passepartout/Library/Sources/AppUIMain/Views/Paywall/PaywallModifier.swift
+++ b/Passepartout/Library/Sources/AppUIMain/Views/Paywall/PaywallModifier.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import AppLibrary
import SwiftUI
struct PaywallModifier: ViewModifier {
diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Paywall/PaywallView.swift b/Passepartout/Library/Sources/AppUIMain/Views/Paywall/PaywallView.swift
index 4394612c..852c8ded 100644
--- a/Passepartout/Library/Sources/AppUIMain/Views/Paywall/PaywallView.swift
+++ b/Passepartout/Library/Sources/AppUIMain/Views/Paywall/PaywallView.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import AppLibrary
import SwiftUI
struct PaywallView: View {
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/EditorModuleToggle.swift b/Passepartout/Library/Sources/AppUIMain/Views/Profile/EditorModuleToggle.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUIMain/UI/EditorModuleToggle.swift
rename to Passepartout/Library/Sources/AppUIMain/Views/Profile/EditorModuleToggle.swift
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/ModuleSection.swift b/Passepartout/Library/Sources/AppUIMain/Views/Profile/ModuleSection.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUIMain/UI/ModuleSection.swift
rename to Passepartout/Library/Sources/AppUIMain/Views/Profile/ModuleSection.swift
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/ModuleViewModifier.swift b/Passepartout/Library/Sources/AppUIMain/Views/Profile/ModuleViewModifier.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUIMain/UI/ModuleViewModifier.swift
rename to Passepartout/Library/Sources/AppUIMain/Views/Profile/ModuleViewModifier.swift
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/NameSection.swift b/Passepartout/Library/Sources/AppUIMain/Views/Profile/NameSection.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUIMain/UI/NameSection.swift
rename to Passepartout/Library/Sources/AppUIMain/Views/Profile/NameSection.swift
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/ProfileSaveButton.swift b/Passepartout/Library/Sources/AppUIMain/Views/Profile/ProfileSaveButton.swift
similarity index 100%
rename from Passepartout/Library/Sources/AppUIMain/UI/ProfileSaveButton.swift
rename to Passepartout/Library/Sources/AppUIMain/Views/Profile/ProfileSaveButton.swift
diff --git a/Passepartout/Library/Sources/AppUIMain/UI/StorageSection.swift b/Passepartout/Library/Sources/AppUIMain/Views/Profile/StorageSection.swift
similarity index 99%
rename from Passepartout/Library/Sources/AppUIMain/UI/StorageSection.swift
rename to Passepartout/Library/Sources/AppUIMain/Views/Profile/StorageSection.swift
index 38aa0e2c..865c9beb 100644
--- a/Passepartout/Library/Sources/AppUIMain/UI/StorageSection.swift
+++ b/Passepartout/Library/Sources/AppUIMain/Views/Profile/StorageSection.swift
@@ -23,7 +23,7 @@
// along with Passepartout. If not, see .
//
-import Foundation
+import AppLibrary
import SwiftUI
struct StorageSection: View {
diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Provider/ProviderContentModifier.swift b/Passepartout/Library/Sources/AppUIMain/Views/Provider/ProviderContentModifier.swift
index fd67ce79..fb0ca538 100644
--- a/Passepartout/Library/Sources/AppUIMain/Views/Provider/ProviderContentModifier.swift
+++ b/Passepartout/Library/Sources/AppUIMain/Views/Provider/ProviderContentModifier.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import AppLibrary
import PassepartoutKit
import SwiftUI
diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Provider/iOS/VPNProviderServerView+iOS.swift b/Passepartout/Library/Sources/AppUIMain/Views/Provider/iOS/VPNProviderServerView+iOS.swift
index 1e7fd744..ed6b94ec 100644
--- a/Passepartout/Library/Sources/AppUIMain/Views/Provider/iOS/VPNProviderServerView+iOS.swift
+++ b/Passepartout/Library/Sources/AppUIMain/Views/Provider/iOS/VPNProviderServerView+iOS.swift
@@ -25,6 +25,8 @@
#if os(iOS)
+import AppLibrary
+import CommonLibrary
import PassepartoutKit
import SwiftUI
diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Provider/macOS/VPNProviderServerView+macOS.swift b/Passepartout/Library/Sources/AppUIMain/Views/Provider/macOS/VPNProviderServerView+macOS.swift
index 9780826b..ddd7771c 100644
--- a/Passepartout/Library/Sources/AppUIMain/Views/Provider/macOS/VPNProviderServerView+macOS.swift
+++ b/Passepartout/Library/Sources/AppUIMain/Views/Provider/macOS/VPNProviderServerView+macOS.swift
@@ -25,6 +25,7 @@
#if os(macOS)
+import CommonLibrary
import PassepartoutKit
import SwiftUI
diff --git a/Passepartout/Library/Sources/AppUIMain/Business/ProviderFavoritesManager.swift b/Passepartout/Library/Sources/CommonLibrary/Business/ProviderFavoritesManager.swift
similarity index 89%
rename from Passepartout/Library/Sources/AppUIMain/Business/ProviderFavoritesManager.swift
rename to Passepartout/Library/Sources/CommonLibrary/Business/ProviderFavoritesManager.swift
index 4be27bb0..762376d0 100644
--- a/Passepartout/Library/Sources/AppUIMain/Business/ProviderFavoritesManager.swift
+++ b/Passepartout/Library/Sources/CommonLibrary/Business/ProviderFavoritesManager.swift
@@ -23,16 +23,15 @@
// along with Passepartout. If not, see .
//
-import CommonLibrary
import Foundation
@MainActor
-final class ProviderFavoritesManager: ObservableObject {
+public final class ProviderFavoritesManager: ObservableObject {
private let defaults: UserDefaults
private var allFavorites: ProviderFavoriteServers
- var moduleId: UUID {
+ public var moduleId: UUID {
didSet {
guard let rawValue = defaults.string(forKey: AppPreference.providerFavoriteServers.key) else {
allFavorites = ProviderFavoriteServers()
@@ -42,7 +41,7 @@ final class ProviderFavoritesManager: ObservableObject {
}
}
- var serverIds: Set {
+ public var serverIds: Set {
get {
allFavorites.servers(forModuleWithID: moduleId)
}
@@ -52,13 +51,13 @@ final class ProviderFavoritesManager: ObservableObject {
}
}
- init(defaults: UserDefaults = .standard) {
+ public init(defaults: UserDefaults = .standard) {
self.defaults = defaults
allFavorites = ProviderFavoriteServers()
moduleId = UUID()
}
- func save() {
+ public func save() {
defaults.set(allFavorites.rawValue, forKey: AppPreference.providerFavoriteServers.key)
}
}
diff --git a/Passepartout/Library/Sources/CommonLibrary/Domain/Constants.swift b/Passepartout/Library/Sources/CommonLibrary/Domain/Constants.swift
index 95767760..090e8325 100644
--- a/Passepartout/Library/Sources/CommonLibrary/Domain/Constants.swift
+++ b/Passepartout/Library/Sources/CommonLibrary/Domain/Constants.swift
@@ -100,8 +100,6 @@ public struct Constants: Decodable, Sendable {
}
public struct API: Decodable, Sendable {
- public let bundlePath: String
-
public let timeoutInterval: TimeInterval
}
diff --git a/Passepartout/Library/Sources/CommonLibrary/Resources/Constants.json b/Passepartout/Library/Sources/CommonLibrary/Resources/Constants.json
index ea9b7e65..3def3bdc 100644
--- a/Passepartout/Library/Sources/CommonLibrary/Resources/Constants.json
+++ b/Passepartout/Library/Sources/CommonLibrary/Resources/Constants.json
@@ -26,7 +26,6 @@
"refreshInterval": 3.0
},
"api": {
- "bundlePath": "API",
"timeoutInterval": 5.0
},
"log": {
diff --git a/Passepartout/Library/Sources/CommonLibrary/Shared.swift b/Passepartout/Library/Sources/CommonLibrary/Shared.swift
index d0201006..a5b260ad 100644
--- a/Passepartout/Library/Sources/CommonLibrary/Shared.swift
+++ b/Passepartout/Library/Sources/CommonLibrary/Shared.swift
@@ -55,38 +55,3 @@ extension UserDefaults {
return defaults
}()
}
-
-// TODO: #716, move to Environment
-extension API {
- public static var shared: [APIMapper] {
-#if DEBUG
- [API.bundled]
-#else
- API.remoteThenBundled
-#endif
- }
-
- private static let remoteThenBundled: [APIMapper] = [
- Self.remote,
- Self.bundled
- ]
-
- public static let bundled: APIMapper = {
- guard let url = Bundle.module.url(forResource: Constants.shared.api.bundlePath, withExtension: nil) else {
- fatalError("Unable to find bundled API")
- }
- let ws = API.V5.DefaultWebServices(
- url,
- timeout: Constants.shared.api.timeoutInterval
- )
- return API.V5.Mapper(webServices: ws)
- }()
-
- public static let remote: APIMapper = {
- let ws = API.V5.DefaultWebServices(
- Constants.shared.websites.api,
- timeout: Constants.shared.api.timeoutInterval
- )
- return API.V5.Mapper(webServices: ws)
- }()
-}
diff --git a/Passepartout/Library/Sources/UtilsLibrary/Views/ErrorHandler.swift b/Passepartout/Library/Sources/UtilsLibrary/Business/ErrorHandler.swift
similarity index 100%
rename from Passepartout/Library/Sources/UtilsLibrary/Views/ErrorHandler.swift
rename to Passepartout/Library/Sources/UtilsLibrary/Business/ErrorHandler.swift
diff --git a/Passepartout/Library/Sources/UtilsLibrary/Views/Routable.swift b/Passepartout/Library/Sources/UtilsLibrary/Business/Routable.swift
similarity index 100%
rename from Passepartout/Library/Sources/UtilsLibrary/Views/Routable.swift
rename to Passepartout/Library/Sources/UtilsLibrary/Business/Routable.swift
diff --git a/Passepartout/Library/Tests/AppUITests/ExtendedTunnelTests.swift b/Passepartout/Library/Tests/AppLibraryTests/ExtendedTunnelTests.swift
similarity index 98%
rename from Passepartout/Library/Tests/AppUITests/ExtendedTunnelTests.swift
rename to Passepartout/Library/Tests/AppLibraryTests/ExtendedTunnelTests.swift
index 20581247..854e69de 100644
--- a/Passepartout/Library/Tests/AppUITests/ExtendedTunnelTests.swift
+++ b/Passepartout/Library/Tests/AppLibraryTests/ExtendedTunnelTests.swift
@@ -23,7 +23,7 @@
// along with Passepartout. If not, see .
//
-@testable import AppUI
+@testable import AppLibrary
import Foundation
import PassepartoutKit
import XCTest
diff --git a/Passepartout/Library/Tests/AppUITests/IAPManagerTests.swift b/Passepartout/Library/Tests/AppLibraryTests/IAPManagerTests.swift
similarity index 91%
rename from Passepartout/Library/Tests/AppUITests/IAPManagerTests.swift
rename to Passepartout/Library/Tests/AppLibraryTests/IAPManagerTests.swift
index 4431bb78..1d6cddbf 100644
--- a/Passepartout/Library/Tests/AppUITests/IAPManagerTests.swift
+++ b/Passepartout/Library/Tests/AppLibraryTests/IAPManagerTests.swift
@@ -23,7 +23,7 @@
// along with Passepartout. If not, see .
//
-@testable import AppUI
+@testable import AppLibrary
import Foundation
import XCTest
@@ -43,7 +43,7 @@ extension IAPManagerTests {
// MARK: Build products
func test_givenBuildProducts_whenOlder_thenFullVersion() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
await reader.setReceipt(withBuild: olderBuildNumber, products: [])
let sut = IAPManager(receiptReader: reader) { build in
if build <= self.defaultBuildNumber {
@@ -56,7 +56,7 @@ extension IAPManagerTests {
}
func test_givenBuildProducts_whenNewer_thenFreeVersion() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
await reader.setReceipt(withBuild: newerBuildNumber, products: [])
let sut = IAPManager(receiptReader: reader) { build in
if build <= self.defaultBuildNumber {
@@ -71,7 +71,7 @@ extension IAPManagerTests {
// MARK: Eligibility
func test_givenPurchasedFeature_whenReloadReceipt_thenIsEligible() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
let sut = IAPManager(receiptReader: reader)
XCTAssertFalse(sut.isEligible(for: AppFeature.fullVersionFeaturesV2))
@@ -84,7 +84,7 @@ extension IAPManagerTests {
}
func test_givenPurchasedFeatures_thenIsOnlyEligibleForFeatures() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
await reader.setReceipt(withBuild: defaultBuildNumber, products: [
.Features.siriShortcuts,
.Features.networkSettings
@@ -102,7 +102,7 @@ extension IAPManagerTests {
}
func test_givenPurchasedAndCancelledFeature_thenIsNotEligible() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
await reader.setReceipt(
withBuild: defaultBuildNumber,
products: [.Full.allPlatforms],
@@ -115,7 +115,7 @@ extension IAPManagerTests {
}
func test_givenFreeVersion_thenIsNotEligibleForAnyFeature() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
await reader.setReceipt(withBuild: defaultBuildNumber, products: [])
let sut = IAPManager(receiptReader: reader)
@@ -127,7 +127,7 @@ extension IAPManagerTests {
}
func test_givenFreeVersion_thenIsNotEligibleForAppleTV() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
await reader.setReceipt(withBuild: defaultBuildNumber, products: [])
let sut = IAPManager(receiptReader: reader)
@@ -136,7 +136,7 @@ extension IAPManagerTests {
}
func test_givenFullVersion_thenIsEligibleForAnyFeatureExceptAppleTV() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Full.allPlatforms])
let sut = IAPManager(receiptReader: reader)
@@ -148,7 +148,7 @@ extension IAPManagerTests {
}
func test_givenAppleTV_thenIsEligibleForAppleTV() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Features.appleTV])
let sut = IAPManager(receiptReader: reader)
@@ -157,7 +157,7 @@ extension IAPManagerTests {
}
func test_givenPlatformVersion_thenIsFullVersionForPlatform() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
let sut = IAPManager(receiptReader: reader)
#if os(macOS)
@@ -172,7 +172,7 @@ extension IAPManagerTests {
}
func test_givenPlatformVersion_thenIsNotFullVersionForOtherPlatform() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
let sut = IAPManager(receiptReader: reader)
#if os(macOS)
@@ -193,7 +193,7 @@ extension IAPManagerTests {
// MARK: App level
func test_givenBetaApp_thenIsRestricted() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
let sut = IAPManager(customUserLevel: .beta, receiptReader: reader)
await sut.reloadReceipt()
@@ -201,7 +201,7 @@ extension IAPManagerTests {
}
func test_givenBetaApp_thenIsNotEligibleForAnyFeature() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
let sut = IAPManager(customUserLevel: .beta, receiptReader: reader)
await sut.reloadReceipt()
@@ -209,7 +209,7 @@ extension IAPManagerTests {
}
func test_givenBetaApp_thenIsEligibleForUnrestrictedFeature() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
let sut = IAPManager(customUserLevel: .beta, receiptReader: reader, unrestrictedFeatures: [.onDemand])
await sut.reloadReceipt()
@@ -223,7 +223,7 @@ extension IAPManagerTests {
}
func test_givenFullApp_thenIsFullVersion() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
let sut = IAPManager(customUserLevel: .fullVersion, receiptReader: reader)
await sut.reloadReceipt()
@@ -231,7 +231,7 @@ extension IAPManagerTests {
}
func test_givenFullPlusTVApp_thenIsFullVersion() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
let sut = IAPManager(customUserLevel: .fullVersionPlusTV, receiptReader: reader)
await sut.reloadReceipt()
@@ -239,7 +239,7 @@ extension IAPManagerTests {
}
func test_givenFullApp_thenIsEligibleForAnyFeatureExceptAppleTV() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
let sut = IAPManager(customUserLevel: .fullVersion, receiptReader: reader)
await sut.reloadReceipt()
@@ -250,7 +250,7 @@ extension IAPManagerTests {
}
func test_givenFullPlusTVApp_thenIsEligibleForAnyFeature() async {
- let reader = MockReceiptReader()
+ let reader = MockAppReceiptReader()
let sut = IAPManager(customUserLevel: .fullVersionPlusTV, receiptReader: reader)
await sut.reloadReceipt()
diff --git a/Passepartout/Library/Tests/AppUIMainTests/IssueTests.swift b/Passepartout/Library/Tests/AppUIMainTests/IssueTests.swift
index 43f93b7b..cdf2d60d 100644
--- a/Passepartout/Library/Tests/AppUIMainTests/IssueTests.swift
+++ b/Passepartout/Library/Tests/AppUIMainTests/IssueTests.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import AppLibrary
@testable import AppUIMain
import Foundation
import XCTest
diff --git a/ci/update-bundled-api.sh b/ci/update-bundled-api.sh
index cefd1378..827eca96 100755
--- a/ci/update-bundled-api.sh
+++ b/ci/update-bundled-api.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-DESTINATION="Passepartout/Library/Sources/CommonLibrary/Resources/API"
+DESTINATION="Passepartout/Library/Sources/APILibrary/API"
API_VERSION="v5"
mkdir tmp