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