Refactor AppUI/AppUIMain to accomodate TV (#797)
- Move InteractiveView to AppUI for use in TV, with OpenVPNCredentialsView - Move non-UI entities to AppLibrary (IAP, ExtendedTunnel, ProfileProcessor) - Take API out of CommonLibrary (tunnel extension does not need it) - Reorganize theme views/modifiers into separate files
This commit is contained in:
parent
ca18aadddf
commit
357c505cc0
|
@ -12,7 +12,7 @@
|
||||||
{
|
{
|
||||||
"identity" : "generic-json-swift",
|
"identity" : "generic-json-swift",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/zoul/generic-json-swift",
|
"location" : "https://github.com/iwill/generic-json-swift",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "0a06575f4038b504e78ac330913d920f1630f510",
|
"revision" : "0a06575f4038b504e78ac330913d920f1630f510",
|
||||||
"version" : "2.0.2"
|
"version" : "2.0.2"
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
|
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "31aff403169c7cebe91a07fb8d225ab844a9a9ff"
|
"revision" : "e95c7b54dc11e744d9b40a722fccf752436ac0ef"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -49,6 +49,13 @@ let package = Package(
|
||||||
targets: [
|
targets: [
|
||||||
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
// 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.
|
// Targets can depend on other targets in this package and products from dependencies.
|
||||||
|
.target(
|
||||||
|
name: "APILibrary",
|
||||||
|
dependencies: ["CommonLibrary"],
|
||||||
|
resources: [
|
||||||
|
.copy("API")
|
||||||
|
]
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "AppData",
|
name: "AppData",
|
||||||
dependencies: []
|
dependencies: []
|
||||||
|
@ -57,8 +64,7 @@ let package = Package(
|
||||||
name: "AppDataProfiles",
|
name: "AppDataProfiles",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"AppData",
|
"AppData",
|
||||||
"AppLibrary",
|
"AppLibrary"
|
||||||
"UtilsLibrary"
|
|
||||||
],
|
],
|
||||||
resources: [
|
resources: [
|
||||||
.process("Profiles.xcdatamodeld")
|
.process("Profiles.xcdatamodeld")
|
||||||
|
@ -68,8 +74,7 @@ let package = Package(
|
||||||
name: "AppDataProviders",
|
name: "AppDataProviders",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"AppData",
|
"AppData",
|
||||||
"AppLibrary",
|
"AppLibrary"
|
||||||
"UtilsLibrary"
|
|
||||||
],
|
],
|
||||||
resources: [
|
resources: [
|
||||||
.process("Providers.xcdatamodeld")
|
.process("Providers.xcdatamodeld")
|
||||||
|
@ -77,16 +82,18 @@ let package = Package(
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "AppLibrary",
|
name: "AppLibrary",
|
||||||
dependencies: ["CommonLibrary"]
|
dependencies: [
|
||||||
|
"APILibrary",
|
||||||
|
"Kvitto",
|
||||||
|
"UtilsLibrary"
|
||||||
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "AppUI",
|
name: "AppUI",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"AppDataProfiles",
|
"AppDataProfiles",
|
||||||
"AppDataProviders",
|
"AppDataProviders",
|
||||||
"AppLibrary",
|
"AppLibrary"
|
||||||
"Kvitto",
|
|
||||||
"UtilsLibrary"
|
|
||||||
],
|
],
|
||||||
resources: [
|
resources: [
|
||||||
.process("Resources")
|
.process("Resources")
|
||||||
|
@ -114,7 +121,6 @@ let package = Package(
|
||||||
.product(name: "PassepartoutWireGuardGo", package: "passepartoutkit-source-wireguard-go")
|
.product(name: "PassepartoutWireGuardGo", package: "passepartoutkit-source-wireguard-go")
|
||||||
],
|
],
|
||||||
resources: [
|
resources: [
|
||||||
.copy("API"),
|
|
||||||
.process("Resources")
|
.process("Resources")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
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)
|
||||||
|
}()
|
||||||
|
}
|
|
@ -50,7 +50,7 @@ extension AppProduct: InAppIdentifierProviding {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppProduct {
|
extension AppProduct {
|
||||||
static var all: [Self] {
|
public static var all: [Self] {
|
||||||
Features.all + Full.all + Donations.all
|
Features.all + Full.all + Donations.all
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,18 +26,18 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import UtilsLibrary
|
import UtilsLibrary
|
||||||
|
|
||||||
actor MockAppProductHelper: AppProductHelper {
|
public actor MockAppProductHelper: AppProductHelper {
|
||||||
private(set) var products: [AppProduct: InAppProduct]
|
public private(set) var products: [AppProduct: InAppProduct]
|
||||||
|
|
||||||
init() {
|
public init() {
|
||||||
products = [:]
|
products = [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
nonisolated var canMakePurchases: Bool {
|
public nonisolated var canMakePurchases: Bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchProducts() async throws {
|
public func fetchProducts() async throws {
|
||||||
products = AppProduct.all.reduce(into: [:]) {
|
products = AppProduct.all.reduce(into: [:]) {
|
||||||
$0[$1] = InAppProduct(
|
$0[$1] = InAppProduct(
|
||||||
productIdentifier: $1.rawValue,
|
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
|
.done
|
||||||
}
|
}
|
||||||
|
|
||||||
func restorePurchases() async throws {
|
public func restorePurchases() async throws {
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -26,14 +26,14 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import UtilsLibrary
|
import UtilsLibrary
|
||||||
|
|
||||||
actor MockReceiptReader: AppReceiptReader {
|
public actor MockAppReceiptReader: AppReceiptReader {
|
||||||
private var receipt: InAppReceipt?
|
private var receipt: InAppReceipt?
|
||||||
|
|
||||||
init(receipt: InAppReceipt? = nil) {
|
public init(receipt: InAppReceipt? = nil) {
|
||||||
self.receipt = receipt
|
self.receipt = receipt
|
||||||
}
|
}
|
||||||
|
|
||||||
func setReceipt(withBuild build: Int, products: [AppProduct], cancelledProducts: Set<AppProduct> = []) {
|
public func setReceipt(withBuild build: Int, products: [AppProduct], cancelledProducts: Set<AppProduct> = []) {
|
||||||
receipt = InAppReceipt(originalBuildNumber: build, purchaseReceipts: products.map {
|
receipt = InAppReceipt(originalBuildNumber: build, purchaseReceipts: products.map {
|
||||||
.init(productIdentifier: $0.rawValue,
|
.init(productIdentifier: $0.rawValue,
|
||||||
cancellationDate: cancelledProducts.contains($0) ? Date() : nil,
|
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
|
receipt
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import APILibrary
|
||||||
import CommonLibrary
|
import CommonLibrary
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
|
|
|
@ -37,7 +37,7 @@ extension AppContext {
|
||||||
return AppContext(
|
return AppContext(
|
||||||
iapManager: IAPManager(
|
iapManager: IAPManager(
|
||||||
customUserLevel: nil,
|
customUserLevel: nil,
|
||||||
receiptReader: MockReceiptReader(),
|
receiptReader: MockAppReceiptReader(),
|
||||||
unrestrictedFeatures: [
|
unrestrictedFeatures: [
|
||||||
.interactiveLogin,
|
.interactiveLogin,
|
||||||
.onDemand,
|
.onDemand,
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
protocol InteractiveViewProviding {
|
public protocol InteractiveViewProviding {
|
||||||
associatedtype InteractiveContent: View
|
associatedtype InteractiveContent: View
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
|
@ -26,6 +26,7 @@
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UtilsLibrary
|
||||||
|
|
||||||
extension Theme {
|
extension Theme {
|
||||||
public convenience init() {
|
public convenience init() {
|
||||||
|
@ -35,7 +36,62 @@ extension Theme {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Modifiers
|
// MARK: - Shortcuts
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
public func themePopover<Content>(
|
||||||
|
isPresented: Binding<Bool>,
|
||||||
|
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<Popover>: 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 {
|
extension ThemeWindowModifier {
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
|
|
|
@ -36,6 +36,14 @@ extension Theme {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Shortcuts
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
public func themeLockScreen() -> some View {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Modifiers
|
// MARK: - Modifiers
|
||||||
|
|
||||||
extension ThemeWindowModifier {
|
extension ThemeWindowModifier {
|
||||||
|
|
|
@ -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
|
#endif
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// ExtendedTunnel+Theme.swift
|
// Theme+Extensions.swift
|
||||||
// Passepartout
|
// Passepartout
|
||||||
//
|
//
|
||||||
// Created by Davide De Rosa on 9/6/24.
|
// Created by Davide De Rosa on 9/6/24.
|
||||||
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
|
@ -28,6 +28,10 @@ import UtilsLibrary
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public final class Theme: ObservableObject {
|
public final class Theme: ObservableObject {
|
||||||
|
private var animation: Animation = .spring
|
||||||
|
|
||||||
|
public internal(set) var animationCategories: Set<ThemeAnimationCategory> = Set(ThemeAnimationCategory.allCases)
|
||||||
|
|
||||||
public internal(set) var rootModalSize: CGSize?
|
public internal(set) var rootModalSize: CGSize?
|
||||||
|
|
||||||
public internal(set) var secondaryModalSize: CGSize?
|
public internal(set) var secondaryModalSize: CGSize?
|
||||||
|
@ -64,10 +68,6 @@ public final class Theme: ObservableObject {
|
||||||
|
|
||||||
public internal(set) var errorColor: Color = .red
|
public internal(set) var errorColor: Color = .red
|
||||||
|
|
||||||
private var animation: Animation = .spring
|
|
||||||
|
|
||||||
public internal(set) var animationCategories: Set<ThemeAnimationCategory> = Set(ThemeAnimationCategory.allCases)
|
|
||||||
|
|
||||||
public internal(set) var logoImage = "Logo"
|
public internal(set) var logoImage = "Logo"
|
||||||
|
|
||||||
public internal(set) var systemImageName: (ImageName) -> String = Theme.ImageName.defaultSystemName
|
public internal(set) var systemImageName: (ImageName) -> String = Theme.ImageName.defaultSystemName
|
||||||
|
@ -81,163 +81,3 @@ public final class Theme: ObservableObject {
|
||||||
animationCategories.contains(category) ? animation : nil
|
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<Content>(
|
|
||||||
isPresented: Binding<Bool>,
|
|
||||||
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<Content, T>(
|
|
||||||
item: Binding<T?>,
|
|
||||||
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<Content>(
|
|
||||||
isPresented: Binding<Bool>,
|
|
||||||
content: @escaping () -> Content
|
|
||||||
) -> some View where Content: View {
|
|
||||||
modifier(ThemeBooleanPopoverModifier(
|
|
||||||
isPresented: isPresented,
|
|
||||||
popover: content
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
public func themeConfirmation(
|
|
||||||
isPresented: Binding<Bool>,
|
|
||||||
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<NavigationPath>) -> 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<T>(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<ItemView: View, T: EditableValue>(
|
|
||||||
_ title: String,
|
|
||||||
addTitle: String,
|
|
||||||
originalItems: Binding<[T]>,
|
|
||||||
emptyValue: (() async -> T)? = nil,
|
|
||||||
@ViewBuilder itemLabel: @escaping (Bool, Binding<T>) -> ItemView
|
|
||||||
) -> some View {
|
|
||||||
EditableListSection(
|
|
||||||
title,
|
|
||||||
addTitle: addTitle,
|
|
||||||
originalItems: originalItems,
|
|
||||||
emptyValue: emptyValue,
|
|
||||||
itemLabel: itemLabel,
|
|
||||||
removeLabel: ThemeEditableListSection.RemoveLabel.init(action:),
|
|
||||||
editLabel: ThemeEditableListSection.EditLabel.init
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum ThemeAnimationCategory: CaseIterable {
|
||||||
|
case diagnostics
|
||||||
|
|
||||||
|
case modules
|
||||||
|
|
||||||
|
case profiles
|
||||||
|
|
||||||
|
case profilesLayout
|
||||||
|
|
||||||
|
case providers
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
//
|
//
|
||||||
// Theme+UI.swift
|
// Theme+Modifiers.swift
|
||||||
// Passepartout
|
// 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.
|
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||||
//
|
//
|
||||||
// https://github.com/passepartoutvpn
|
// https://github.com/passepartoutvpn
|
||||||
|
@ -30,24 +30,130 @@ import LocalAuthentication
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UtilsLibrary
|
import UtilsLibrary
|
||||||
|
|
||||||
// MARK: - Modifiers
|
// MARK: Shortcuts
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
public func themeModal<Content>(
|
||||||
|
isPresented: Binding<Bool>,
|
||||||
|
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<Content, T>(
|
||||||
|
item: Binding<T?>,
|
||||||
|
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<Bool>,
|
||||||
|
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<NavigationPath>) -> 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)
|
#if !os(tvOS)
|
||||||
|
public func themeWindow(width: CGFloat, height: CGFloat) -> some View {
|
||||||
struct ThemeWindowModifier: ViewModifier {
|
modifier(ThemeWindowModifier(size: .init(width: width, height: height)))
|
||||||
let size: CGSize
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ThemeNavigationDetailModifier: ViewModifier {
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ThemeFormModifier: ViewModifier {
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
content
|
|
||||||
.formStyle(.grouped)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<T>(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<Modal>: ViewModifier where Modal: View {
|
struct ThemeBooleanModalModifier<Modal>: ViewModifier where Modal: View {
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
|
@ -106,41 +212,6 @@ struct ThemeItemModalModifier<Modal, T>: ViewModifier where Modal: View, T: Iden
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ThemeBooleanPopoverModifier<Popover>: 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 {
|
struct ThemeConfirmationModifier: ViewModifier {
|
||||||
|
|
||||||
@Binding
|
@Binding
|
||||||
|
@ -197,13 +268,37 @@ struct ThemeNavigationStackModifier: ViewModifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ThemePlainButtonModifier: ViewModifier {
|
// MARK: - Content modifiers
|
||||||
let action: () -> Void
|
|
||||||
|
struct ThemeFormModifier: ViewModifier {
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.formStyle(.grouped)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ThemeManualInputModifier: ViewModifier {
|
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 {
|
struct ThemeEmptyMessageModifier: ViewModifier {
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
|
@ -268,12 +363,6 @@ struct ThemeTrailingValueModifier: ViewModifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ThemeSectionWithHeaderFooterModifier: ViewModifier {
|
|
||||||
let header: String?
|
|
||||||
|
|
||||||
let footer: String?
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ThemeGridSectionModifier: ViewModifier {
|
struct ThemeGridSectionModifier: ViewModifier {
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
|
@ -394,312 +483,3 @@ struct ThemeTipModifier: ViewModifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#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<Content, Label>: 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<Value, ValueView>: 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<String>, 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<String>, 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<ItemView>: 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
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import UtilsLibrary
|
||||||
|
|
||||||
|
// MARK: Shortcuts
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
|
extension Theme {
|
||||||
|
public func listSection<ItemView: View, T: EditableValue>(
|
||||||
|
_ title: String,
|
||||||
|
addTitle: String,
|
||||||
|
originalItems: Binding<[T]>,
|
||||||
|
emptyValue: (() async -> T)? = nil,
|
||||||
|
@ViewBuilder itemLabel: @escaping (Bool, Binding<T>) -> 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<String>, 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<String>, 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<Content, Label>: 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<Value, ValueView>: 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<ItemView>: 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
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,59 +23,66 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UtilsLibrary
|
import UtilsLibrary
|
||||||
|
|
||||||
extension OpenVPNView {
|
public struct OpenVPNCredentialsView: View {
|
||||||
struct CredentialsView: View {
|
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var iapManager: IAPManager
|
private var iapManager: IAPManager
|
||||||
|
|
||||||
@Binding
|
@Binding
|
||||||
var isInteractive: Bool
|
private var isInteractive: Bool
|
||||||
|
|
||||||
@Binding
|
@Binding
|
||||||
var credentials: OpenVPN.Credentials?
|
private var credentials: OpenVPN.Credentials?
|
||||||
|
|
||||||
var isAuthenticating = false
|
private var isAuthenticating = false
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var builder = OpenVPN.Credentials.Builder()
|
private var builder = OpenVPN.Credentials.Builder()
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var paywallReason: PaywallReason?
|
private var paywallReason: PaywallReason?
|
||||||
|
|
||||||
var body: some View {
|
public init(
|
||||||
Form {
|
isInteractive: Binding<Bool>,
|
||||||
restrictedArea
|
credentials: Binding<OpenVPN.Credentials?>,
|
||||||
inputSection
|
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)
|
credentials = copy.build()
|
||||||
.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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension OpenVPNView.CredentialsView {
|
private extension OpenVPNCredentialsView {
|
||||||
var isEligibleForInteractiveLogin: Bool {
|
var isEligibleForInteractiveLogin: Bool {
|
||||||
iapManager.isEligible(for: .interactiveLogin)
|
iapManager.isEligible(for: .interactiveLogin)
|
||||||
}
|
}
|
||||||
|
@ -164,7 +171,7 @@ private extension OpenVPNView.CredentialsView {
|
||||||
var isInteractive = true
|
var isInteractive = true
|
||||||
|
|
||||||
return NavigationStack {
|
return NavigationStack {
|
||||||
OpenVPNView.CredentialsView(
|
OpenVPNCredentialsView(
|
||||||
isInteractive: $isInteractive,
|
isInteractive: $isInteractive,
|
||||||
credentials: $credentials
|
credentials: $credentials
|
||||||
)
|
)
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// ThemeFavoriteToggle.swift
|
// FavoriteToggle.swift
|
||||||
// Passepartout
|
// Passepartout
|
||||||
//
|
//
|
||||||
// Created by Davide De Rosa on 10/25/24.
|
// Created by Davide De Rosa on 10/25/24.
|
||||||
|
@ -25,16 +25,21 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct FavoriteToggle<ID>: View where ID: Hashable {
|
public struct FavoriteToggle<ID>: View where ID: Hashable {
|
||||||
let value: ID
|
private let value: ID
|
||||||
|
|
||||||
@Binding
|
@Binding
|
||||||
var selection: Set<ID>
|
private var selection: Set<ID>
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var hover: ID?
|
private var hover: ID?
|
||||||
|
|
||||||
var body: some View {
|
public init(value: ID, selection: Binding<Set<ID>>) {
|
||||||
|
self.value = value
|
||||||
|
_selection = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
Button {
|
Button {
|
||||||
if selection.contains(value) {
|
if selection.contains(value) {
|
||||||
selection.remove(value)
|
selection.remove(value)
|
||||||
|
@ -45,18 +50,20 @@ struct FavoriteToggle<ID>: View where ID: Hashable {
|
||||||
ThemeImage(selection.contains(value) ? .favoriteOn : .favoriteOff)
|
ThemeImage(selection.contains(value) ? .favoriteOn : .favoriteOff)
|
||||||
.opacity(opacity)
|
.opacity(opacity)
|
||||||
}
|
}
|
||||||
|
#if os(macOS)
|
||||||
.onHover {
|
.onHover {
|
||||||
hover = $0 ? value : nil
|
hover = $0 ? value : nil
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension FavoriteToggle {
|
private extension FavoriteToggle {
|
||||||
var opacity: Double {
|
var opacity: Double {
|
||||||
#if os(iOS)
|
#if os(macOS)
|
||||||
1.0
|
|
||||||
#else
|
|
||||||
selection.contains(value) || value == hover ? 1.0 : 0.0
|
selection.contains(value) || value == hover ? 1.0 : 0.0
|
||||||
|
#else
|
||||||
|
1.0
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -26,14 +26,19 @@
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct InteractiveView: View {
|
public struct InteractiveView: View {
|
||||||
|
|
||||||
@ObservedObject
|
@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
|
manager
|
||||||
.editor
|
.editor
|
||||||
.interactiveProvider
|
.interactiveProvider
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UtilsLibrary
|
import UtilsLibrary
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
import CommonLibrary
|
import CommonLibrary
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
import CommonLibrary
|
import CommonLibrary
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
|
@ -33,6 +34,7 @@ import UIKit
|
||||||
#else
|
#else
|
||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
|
import AppLibrary
|
||||||
import CommonLibrary
|
import CommonLibrary
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UtilsLibrary
|
import UtilsLibrary
|
|
@ -25,6 +25,7 @@
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
import CommonLibrary
|
import CommonLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
|
@ -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 {
|
extension OpenVPNModule: ProviderEntityViewProviding {
|
||||||
func providerEntityView(
|
func providerEntityView(
|
||||||
with provider: ModuleMetadata.Provider,
|
with provider: ModuleMetadata.Provider,
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UtilsLibrary
|
import UtilsLibrary
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
import CPassepartoutOpenVPNOpenSSL
|
import CPassepartoutOpenVPNOpenSSL
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
@ -225,10 +226,12 @@ private extension OpenVPNView {
|
||||||
}
|
}
|
||||||
|
|
||||||
case .credentials:
|
case .credentials:
|
||||||
CredentialsView(
|
OpenVPNCredentialsView(
|
||||||
isInteractive: draft.isInteractive,
|
isInteractive: draft.isInteractive,
|
||||||
credentials: draft.credentials
|
credentials: draft.credentials
|
||||||
)
|
)
|
||||||
|
.themeAnimation(on: draft.wrappedValue.isInteractive, category: .modules)
|
||||||
|
.modifier(PaywallModifier(reason: $paywallReason))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PaywallModifier: ViewModifier {
|
struct PaywallModifier: ViewModifier {
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PaywallView: View {
|
struct PaywallView: View {
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import AppLibrary
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct StorageSection: View {
|
struct StorageSection: View {
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,8 @@
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
|
import CommonLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
|
||||||
|
import CommonLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
|
@ -23,16 +23,15 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
import CommonLibrary
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class ProviderFavoritesManager: ObservableObject {
|
public final class ProviderFavoritesManager: ObservableObject {
|
||||||
private let defaults: UserDefaults
|
private let defaults: UserDefaults
|
||||||
|
|
||||||
private var allFavorites: ProviderFavoriteServers
|
private var allFavorites: ProviderFavoriteServers
|
||||||
|
|
||||||
var moduleId: UUID {
|
public var moduleId: UUID {
|
||||||
didSet {
|
didSet {
|
||||||
guard let rawValue = defaults.string(forKey: AppPreference.providerFavoriteServers.key) else {
|
guard let rawValue = defaults.string(forKey: AppPreference.providerFavoriteServers.key) else {
|
||||||
allFavorites = ProviderFavoriteServers()
|
allFavorites = ProviderFavoriteServers()
|
||||||
|
@ -42,7 +41,7 @@ final class ProviderFavoritesManager: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var serverIds: Set<String> {
|
public var serverIds: Set<String> {
|
||||||
get {
|
get {
|
||||||
allFavorites.servers(forModuleWithID: moduleId)
|
allFavorites.servers(forModuleWithID: moduleId)
|
||||||
}
|
}
|
||||||
|
@ -52,13 +51,13 @@ final class ProviderFavoritesManager: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(defaults: UserDefaults = .standard) {
|
public init(defaults: UserDefaults = .standard) {
|
||||||
self.defaults = defaults
|
self.defaults = defaults
|
||||||
allFavorites = ProviderFavoriteServers()
|
allFavorites = ProviderFavoriteServers()
|
||||||
moduleId = UUID()
|
moduleId = UUID()
|
||||||
}
|
}
|
||||||
|
|
||||||
func save() {
|
public func save() {
|
||||||
defaults.set(allFavorites.rawValue, forKey: AppPreference.providerFavoriteServers.key)
|
defaults.set(allFavorites.rawValue, forKey: AppPreference.providerFavoriteServers.key)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -100,8 +100,6 @@ public struct Constants: Decodable, Sendable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct API: Decodable, Sendable {
|
public struct API: Decodable, Sendable {
|
||||||
public let bundlePath: String
|
|
||||||
|
|
||||||
public let timeoutInterval: TimeInterval
|
public let timeoutInterval: TimeInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,6 @@
|
||||||
"refreshInterval": 3.0
|
"refreshInterval": 3.0
|
||||||
},
|
},
|
||||||
"api": {
|
"api": {
|
||||||
"bundlePath": "API",
|
|
||||||
"timeoutInterval": 5.0
|
"timeoutInterval": 5.0
|
||||||
},
|
},
|
||||||
"log": {
|
"log": {
|
||||||
|
|
|
@ -55,38 +55,3 @@ extension UserDefaults {
|
||||||
return defaults
|
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)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
@testable import AppUI
|
@testable import AppLibrary
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import XCTest
|
import XCTest
|
|
@ -23,7 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
@testable import AppUI
|
@testable import AppLibrary
|
||||||
import Foundation
|
import Foundation
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ extension IAPManagerTests {
|
||||||
// MARK: Build products
|
// MARK: Build products
|
||||||
|
|
||||||
func test_givenBuildProducts_whenOlder_thenFullVersion() async {
|
func test_givenBuildProducts_whenOlder_thenFullVersion() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
await reader.setReceipt(withBuild: olderBuildNumber, products: [])
|
await reader.setReceipt(withBuild: olderBuildNumber, products: [])
|
||||||
let sut = IAPManager(receiptReader: reader) { build in
|
let sut = IAPManager(receiptReader: reader) { build in
|
||||||
if build <= self.defaultBuildNumber {
|
if build <= self.defaultBuildNumber {
|
||||||
|
@ -56,7 +56,7 @@ extension IAPManagerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_givenBuildProducts_whenNewer_thenFreeVersion() async {
|
func test_givenBuildProducts_whenNewer_thenFreeVersion() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
await reader.setReceipt(withBuild: newerBuildNumber, products: [])
|
await reader.setReceipt(withBuild: newerBuildNumber, products: [])
|
||||||
let sut = IAPManager(receiptReader: reader) { build in
|
let sut = IAPManager(receiptReader: reader) { build in
|
||||||
if build <= self.defaultBuildNumber {
|
if build <= self.defaultBuildNumber {
|
||||||
|
@ -71,7 +71,7 @@ extension IAPManagerTests {
|
||||||
// MARK: Eligibility
|
// MARK: Eligibility
|
||||||
|
|
||||||
func test_givenPurchasedFeature_whenReloadReceipt_thenIsEligible() async {
|
func test_givenPurchasedFeature_whenReloadReceipt_thenIsEligible() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
let sut = IAPManager(receiptReader: reader)
|
let sut = IAPManager(receiptReader: reader)
|
||||||
|
|
||||||
XCTAssertFalse(sut.isEligible(for: AppFeature.fullVersionFeaturesV2))
|
XCTAssertFalse(sut.isEligible(for: AppFeature.fullVersionFeaturesV2))
|
||||||
|
@ -84,7 +84,7 @@ extension IAPManagerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_givenPurchasedFeatures_thenIsOnlyEligibleForFeatures() async {
|
func test_givenPurchasedFeatures_thenIsOnlyEligibleForFeatures() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
await reader.setReceipt(withBuild: defaultBuildNumber, products: [
|
await reader.setReceipt(withBuild: defaultBuildNumber, products: [
|
||||||
.Features.siriShortcuts,
|
.Features.siriShortcuts,
|
||||||
.Features.networkSettings
|
.Features.networkSettings
|
||||||
|
@ -102,7 +102,7 @@ extension IAPManagerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_givenPurchasedAndCancelledFeature_thenIsNotEligible() async {
|
func test_givenPurchasedAndCancelledFeature_thenIsNotEligible() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
await reader.setReceipt(
|
await reader.setReceipt(
|
||||||
withBuild: defaultBuildNumber,
|
withBuild: defaultBuildNumber,
|
||||||
products: [.Full.allPlatforms],
|
products: [.Full.allPlatforms],
|
||||||
|
@ -115,7 +115,7 @@ extension IAPManagerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_givenFreeVersion_thenIsNotEligibleForAnyFeature() async {
|
func test_givenFreeVersion_thenIsNotEligibleForAnyFeature() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
await reader.setReceipt(withBuild: defaultBuildNumber, products: [])
|
await reader.setReceipt(withBuild: defaultBuildNumber, products: [])
|
||||||
let sut = IAPManager(receiptReader: reader)
|
let sut = IAPManager(receiptReader: reader)
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ extension IAPManagerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_givenFreeVersion_thenIsNotEligibleForAppleTV() async {
|
func test_givenFreeVersion_thenIsNotEligibleForAppleTV() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
await reader.setReceipt(withBuild: defaultBuildNumber, products: [])
|
await reader.setReceipt(withBuild: defaultBuildNumber, products: [])
|
||||||
let sut = IAPManager(receiptReader: reader)
|
let sut = IAPManager(receiptReader: reader)
|
||||||
|
|
||||||
|
@ -136,7 +136,7 @@ extension IAPManagerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_givenFullVersion_thenIsEligibleForAnyFeatureExceptAppleTV() async {
|
func test_givenFullVersion_thenIsEligibleForAnyFeatureExceptAppleTV() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Full.allPlatforms])
|
await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Full.allPlatforms])
|
||||||
let sut = IAPManager(receiptReader: reader)
|
let sut = IAPManager(receiptReader: reader)
|
||||||
|
|
||||||
|
@ -148,7 +148,7 @@ extension IAPManagerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_givenAppleTV_thenIsEligibleForAppleTV() async {
|
func test_givenAppleTV_thenIsEligibleForAppleTV() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Features.appleTV])
|
await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Features.appleTV])
|
||||||
let sut = IAPManager(receiptReader: reader)
|
let sut = IAPManager(receiptReader: reader)
|
||||||
|
|
||||||
|
@ -157,7 +157,7 @@ extension IAPManagerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_givenPlatformVersion_thenIsFullVersionForPlatform() async {
|
func test_givenPlatformVersion_thenIsFullVersionForPlatform() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
let sut = IAPManager(receiptReader: reader)
|
let sut = IAPManager(receiptReader: reader)
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
@ -172,7 +172,7 @@ extension IAPManagerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_givenPlatformVersion_thenIsNotFullVersionForOtherPlatform() async {
|
func test_givenPlatformVersion_thenIsNotFullVersionForOtherPlatform() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
let sut = IAPManager(receiptReader: reader)
|
let sut = IAPManager(receiptReader: reader)
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
@ -193,7 +193,7 @@ extension IAPManagerTests {
|
||||||
// MARK: App level
|
// MARK: App level
|
||||||
|
|
||||||
func test_givenBetaApp_thenIsRestricted() async {
|
func test_givenBetaApp_thenIsRestricted() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
let sut = IAPManager(customUserLevel: .beta, receiptReader: reader)
|
let sut = IAPManager(customUserLevel: .beta, receiptReader: reader)
|
||||||
|
|
||||||
await sut.reloadReceipt()
|
await sut.reloadReceipt()
|
||||||
|
@ -201,7 +201,7 @@ extension IAPManagerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_givenBetaApp_thenIsNotEligibleForAnyFeature() async {
|
func test_givenBetaApp_thenIsNotEligibleForAnyFeature() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
let sut = IAPManager(customUserLevel: .beta, receiptReader: reader)
|
let sut = IAPManager(customUserLevel: .beta, receiptReader: reader)
|
||||||
|
|
||||||
await sut.reloadReceipt()
|
await sut.reloadReceipt()
|
||||||
|
@ -209,7 +209,7 @@ extension IAPManagerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_givenBetaApp_thenIsEligibleForUnrestrictedFeature() async {
|
func test_givenBetaApp_thenIsEligibleForUnrestrictedFeature() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
let sut = IAPManager(customUserLevel: .beta, receiptReader: reader, unrestrictedFeatures: [.onDemand])
|
let sut = IAPManager(customUserLevel: .beta, receiptReader: reader, unrestrictedFeatures: [.onDemand])
|
||||||
|
|
||||||
await sut.reloadReceipt()
|
await sut.reloadReceipt()
|
||||||
|
@ -223,7 +223,7 @@ extension IAPManagerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_givenFullApp_thenIsFullVersion() async {
|
func test_givenFullApp_thenIsFullVersion() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
let sut = IAPManager(customUserLevel: .fullVersion, receiptReader: reader)
|
let sut = IAPManager(customUserLevel: .fullVersion, receiptReader: reader)
|
||||||
|
|
||||||
await sut.reloadReceipt()
|
await sut.reloadReceipt()
|
||||||
|
@ -231,7 +231,7 @@ extension IAPManagerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_givenFullPlusTVApp_thenIsFullVersion() async {
|
func test_givenFullPlusTVApp_thenIsFullVersion() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
let sut = IAPManager(customUserLevel: .fullVersionPlusTV, receiptReader: reader)
|
let sut = IAPManager(customUserLevel: .fullVersionPlusTV, receiptReader: reader)
|
||||||
|
|
||||||
await sut.reloadReceipt()
|
await sut.reloadReceipt()
|
||||||
|
@ -239,7 +239,7 @@ extension IAPManagerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_givenFullApp_thenIsEligibleForAnyFeatureExceptAppleTV() async {
|
func test_givenFullApp_thenIsEligibleForAnyFeatureExceptAppleTV() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
let sut = IAPManager(customUserLevel: .fullVersion, receiptReader: reader)
|
let sut = IAPManager(customUserLevel: .fullVersion, receiptReader: reader)
|
||||||
|
|
||||||
await sut.reloadReceipt()
|
await sut.reloadReceipt()
|
||||||
|
@ -250,7 +250,7 @@ extension IAPManagerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_givenFullPlusTVApp_thenIsEligibleForAnyFeature() async {
|
func test_givenFullPlusTVApp_thenIsEligibleForAnyFeature() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockAppReceiptReader()
|
||||||
let sut = IAPManager(customUserLevel: .fullVersionPlusTV, receiptReader: reader)
|
let sut = IAPManager(customUserLevel: .fullVersionPlusTV, receiptReader: reader)
|
||||||
|
|
||||||
await sut.reloadReceipt()
|
await sut.reloadReceipt()
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
@testable import AppUIMain
|
@testable import AppUIMain
|
||||||
import Foundation
|
import Foundation
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
DESTINATION="Passepartout/Library/Sources/CommonLibrary/Resources/API"
|
DESTINATION="Passepartout/Library/Sources/APILibrary/API"
|
||||||
API_VERSION="v5"
|
API_VERSION="v5"
|
||||||
|
|
||||||
mkdir tmp
|
mkdir tmp
|
||||||
|
|
Loading…
Reference in New Issue