diff --git a/Library/Package.swift b/Library/Package.swift
index e76e7cdb..1611be44 100644
--- a/Library/Package.swift
+++ b/Library/Package.swift
@@ -15,11 +15,21 @@ let package = Package(
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "AppUIMain",
- targets: ["AppUIMainWrapper"]
+ targets: [
+ "AppDataPreferences",
+ "AppDataProfiles",
+ "AppDataProviders",
+ "AppUIMainWrapper"
+ ]
),
.library(
name: "AppUITV",
- targets: ["AppUITVWrapper"]
+ targets: [
+ "AppDataPreferences",
+ "AppDataProfiles",
+ "AppDataProviders",
+ "AppUITVWrapper"
+ ]
),
.library(
name: "CommonIAP",
@@ -39,7 +49,10 @@ let package = Package(
),
.library(
name: "TunnelLibrary",
- targets: ["CommonLibrary"]
+ targets: [
+ "AppDataPreferences",
+ "CommonLibrary"
+ ]
),
.library(
name: "UILibrary",
@@ -69,6 +82,16 @@ let package = Package(
name: "AppData",
dependencies: []
),
+ .target(
+ name: "AppDataPreferences",
+ dependencies: [
+ "AppData",
+ "CommonLibrary"
+ ],
+ resources: [
+ .process("Preferences.xcdatamodeld")
+ ]
+ ),
.target(
name: "AppDataProfiles",
dependencies: [
@@ -166,8 +189,6 @@ let package = Package(
.target(
name: "UILibrary",
dependencies: [
- "AppDataProfiles",
- "AppDataProviders",
"CommonAPI",
"CommonLibrary",
"UITesting"
diff --git a/Library/Sources/AppDataPreferences/AppData+Preferences.swift b/Library/Sources/AppDataPreferences/AppData+Preferences.swift
new file mode 100644
index 00000000..25758ae3
--- /dev/null
+++ b/Library/Sources/AppDataPreferences/AppData+Preferences.swift
@@ -0,0 +1,39 @@
+//
+// AppData+Preferences.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 12/6/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 AppData
+import CoreData
+import Foundation
+
+extension AppData {
+
+ @MainActor
+ public static let cdPreferencesModel: NSManagedObjectModel = {
+ guard let model: NSManagedObjectModel = .mergedModel(from: [.module]) else {
+ fatalError("Unable to build Core Data model (Preferences v3)")
+ }
+ return model
+ }()
+}
diff --git a/Library/Sources/AppDataPreferences/Domain/CDModulePreferencesV3.swift b/Library/Sources/AppDataPreferences/Domain/CDModulePreferencesV3.swift
new file mode 100644
index 00000000..18d43570
--- /dev/null
+++ b/Library/Sources/AppDataPreferences/Domain/CDModulePreferencesV3.swift
@@ -0,0 +1,36 @@
+//
+// CDModulePreferencesV3.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 12/5/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 CoreData
+import Foundation
+
+@objc(CDModulePreferencesV3)
+final class CDModulePreferencesV3: NSManagedObject {
+ @nonobjc static func fetchRequest() -> NSFetchRequest {
+ NSFetchRequest(entityName: "CDModulePreferencesV3")
+ }
+
+ @NSManaged var uuid: UUID?
+}
diff --git a/Library/Sources/AppDataPreferences/Domain/CDProviderPreferencesV3.swift b/Library/Sources/AppDataPreferences/Domain/CDProviderPreferencesV3.swift
new file mode 100644
index 00000000..2062996a
--- /dev/null
+++ b/Library/Sources/AppDataPreferences/Domain/CDProviderPreferencesV3.swift
@@ -0,0 +1,37 @@
+//
+// CDProviderPreferencesV3.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 12/5/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 CoreData
+import Foundation
+
+@objc(CDProviderPreferencesV3)
+final class CDProviderPreferencesV3: NSManagedObject {
+ @nonobjc static func fetchRequest() -> NSFetchRequest {
+ NSFetchRequest(entityName: "CDProviderPreferencesV3")
+ }
+
+ @NSManaged var providerId: String?
+ @NSManaged var favoriteServerIds: Data?
+}
diff --git a/Library/Sources/AppDataPreferences/Preferences.xcdatamodeld/Preferences.xcdatamodel/contents b/Library/Sources/AppDataPreferences/Preferences.xcdatamodeld/Preferences.xcdatamodel/contents
new file mode 100644
index 00000000..4ad3589c
--- /dev/null
+++ b/Library/Sources/AppDataPreferences/Preferences.xcdatamodeld/Preferences.xcdatamodel/contents
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift b/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift
new file mode 100644
index 00000000..007fd49b
--- /dev/null
+++ b/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift
@@ -0,0 +1,89 @@
+//
+// CDModulePreferencesRepositoryV3.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 12/5/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 AppData
+import CommonLibrary
+import CoreData
+import Foundation
+import PassepartoutKit
+
+extension AppData {
+
+ @MainActor
+ public static func cdModulePreferencesRepositoryV3(context: NSManagedObjectContext) -> ModulePreferencesRepository {
+ CDModulePreferencesRepositoryV3(context: context)
+ }
+}
+
+// MARK: - Repository
+
+private final class CDModulePreferencesRepositoryV3: ModulePreferencesRepository {
+ private nonisolated let context: NSManagedObjectContext
+
+ init(context: NSManagedObjectContext) {
+ self.context = context
+ }
+
+ func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy {
+ let entity = try context.performAndWait {
+ let request = CDModulePreferencesV3.fetchRequest()
+ request.predicate = NSPredicate(format: "uuid == %@", moduleId.uuidString)
+ do {
+ let entity = try request.execute().first ?? CDModulePreferencesV3(context: context)
+ entity.uuid = moduleId
+ return entity
+ } catch {
+ pp_log(.app, .error, "Unable to load preferences for module \(moduleId): \(error)")
+ throw error
+ }
+ }
+ return CDModulePreferencesProxy(context: context, entity: entity)
+ }
+}
+
+// MARK: - Preference
+
+private final class CDModulePreferencesProxy: ModulePreferencesProxy {
+ private let context: NSManagedObjectContext
+
+ private let entity: CDModulePreferencesV3
+
+ init(context: NSManagedObjectContext, entity: CDModulePreferencesV3) {
+ self.context = context
+ self.entity = entity
+ }
+
+ func save() throws {
+ guard context.hasChanges else {
+ return
+ }
+ do {
+ try context.save()
+ } catch {
+ context.rollback()
+ throw error
+ }
+ }
+}
diff --git a/Library/Sources/AppDataPreferences/Strategy/CDProviderPreferencesRepositoryV3.swift b/Library/Sources/AppDataPreferences/Strategy/CDProviderPreferencesRepositoryV3.swift
new file mode 100644
index 00000000..ace7bc1b
--- /dev/null
+++ b/Library/Sources/AppDataPreferences/Strategy/CDProviderPreferencesRepositoryV3.swift
@@ -0,0 +1,114 @@
+//
+// CDProviderPreferencesRepositoryV3.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 12/5/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 AppData
+import CommonLibrary
+import CoreData
+import Foundation
+import PassepartoutKit
+
+extension AppData {
+
+ @MainActor
+ public static func cdProviderPreferencesRepositoryV3(context: NSManagedObjectContext) -> ProviderPreferencesRepository {
+ CDProviderPreferencesRepositoryV3(context: context)
+ }
+}
+
+// MARK: - Repository
+
+private final class CDProviderPreferencesRepositoryV3: ProviderPreferencesRepository {
+ private nonisolated let context: NSManagedObjectContext
+
+ init(context: NSManagedObjectContext) {
+ self.context = context
+ }
+
+ func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy {
+ let entity = try context.performAndWait {
+ let request = CDProviderPreferencesV3.fetchRequest()
+ request.predicate = NSPredicate(format: "providerId == %@", providerId.rawValue)
+ do {
+ let entity = try request.execute().first ?? CDProviderPreferencesV3(context: context)
+ entity.providerId = providerId.rawValue
+ return entity
+ } catch {
+ pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(error)")
+ throw error
+ }
+ }
+ return CDProviderPreferencesProxy(context: context, entity: entity)
+ }
+}
+
+// MARK: - Preference
+
+private final class CDProviderPreferencesProxy: ProviderPreferencesProxy {
+ private let context: NSManagedObjectContext
+
+ private let entity: CDProviderPreferencesV3
+
+ init(context: NSManagedObjectContext, entity: CDProviderPreferencesV3) {
+ self.context = context
+ self.entity = entity
+ }
+
+ var favoriteServers: Set {
+ get {
+ do {
+ return try context.performAndWait {
+ guard let data = entity.favoriteServerIds else {
+ return []
+ }
+ return try JSONDecoder().decode(Set.self, from: data)
+ }
+ } catch {
+ pp_log(.app, .error, "Unable to get favoriteServers: \(error)")
+ return []
+ }
+ }
+ set {
+ do {
+ try context.performAndWait {
+ entity.favoriteServerIds = try JSONEncoder().encode(newValue)
+ }
+ } catch {
+ pp_log(.app, .error, "Unable to set favoriteServers: \(error)")
+ }
+ }
+ }
+
+ func save() throws {
+ guard context.hasChanges else {
+ return
+ }
+ do {
+ try context.save()
+ } catch {
+ context.rollback()
+ throw error
+ }
+ }
+}
diff --git a/Library/Sources/AppDataProfiles/Profiles.xcdatamodeld/ProfilesV3.xcdatamodel/contents b/Library/Sources/AppDataProfiles/Profiles.xcdatamodeld/ProfilesV3.xcdatamodel/contents
index 05026434..2f65bbfd 100644
--- a/Library/Sources/AppDataProfiles/Profiles.xcdatamodeld/ProfilesV3.xcdatamodel/contents
+++ b/Library/Sources/AppDataProfiles/Profiles.xcdatamodeld/ProfilesV3.xcdatamodel/contents
@@ -1,5 +1,5 @@
-
+
diff --git a/Library/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift b/Library/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift
index ef237453..0fff2ec9 100644
--- a/Library/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift
+++ b/Library/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift
@@ -36,7 +36,7 @@ extension AppData {
}
}
-actor CDProviderRepositoryV3: NSObject, ProviderRepository {
+private actor CDProviderRepositoryV3: NSObject, ProviderRepository {
private nonisolated let context: NSManagedObjectContext
private nonisolated let providersSubject: CurrentValueSubject<[Provider], Never>
diff --git a/Library/Sources/AppUIMain/Views/Modules/DNSView.swift b/Library/Sources/AppUIMain/Views/Modules/DNSView.swift
index 506cd1a1..7f2b8d11 100644
--- a/Library/Sources/AppUIMain/Views/Modules/DNSView.swift
+++ b/Library/Sources/AppUIMain/Views/Modules/DNSView.swift
@@ -33,10 +33,15 @@ struct DNSView: View, ModuleDraftEditing {
@EnvironmentObject
private var theme: Theme
+ let module: DNSModule.Builder
+
@ObservedObject
var editor: ProfileEditor
- let module: DNSModule.Builder
+ init(module: DNSModule.Builder, parameters: ModuleViewParameters) {
+ self.module = module
+ editor = parameters.editor
+ }
var body: some View {
debugChanges()
diff --git a/Library/Sources/AppUIMain/Views/Modules/Extensions/DNSModule+Extensions.swift b/Library/Sources/AppUIMain/Views/Modules/Extensions/DNSModule+Extensions.swift
index d51bd724..46c96ffe 100644
--- a/Library/Sources/AppUIMain/Views/Modules/Extensions/DNSModule+Extensions.swift
+++ b/Library/Sources/AppUIMain/Views/Modules/Extensions/DNSModule+Extensions.swift
@@ -23,12 +23,13 @@
// along with Passepartout. If not, see .
//
+import CommonLibrary
import PassepartoutKit
import SwiftUI
import UILibrary
extension DNSModule.Builder: ModuleViewProviding {
- public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
- DNSView(editor: editor, module: self)
+ public func moduleView(with parameters: ModuleViewParameters) -> some View {
+ DNSView(module: self, parameters: parameters)
}
}
diff --git a/Library/Sources/AppUIMain/Views/Modules/Extensions/HTTPProxyModule+Extensions.swift b/Library/Sources/AppUIMain/Views/Modules/Extensions/HTTPProxyModule+Extensions.swift
index 0b331206..6ef4f0e2 100644
--- a/Library/Sources/AppUIMain/Views/Modules/Extensions/HTTPProxyModule+Extensions.swift
+++ b/Library/Sources/AppUIMain/Views/Modules/Extensions/HTTPProxyModule+Extensions.swift
@@ -23,12 +23,13 @@
// along with Passepartout. If not, see .
//
+import CommonLibrary
import PassepartoutKit
import SwiftUI
import UILibrary
extension HTTPProxyModule.Builder: ModuleViewProviding {
- public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
- HTTPProxyView(editor: editor, module: self)
+ public func moduleView(with parameters: ModuleViewParameters) -> some View {
+ HTTPProxyView(module: self, parameters: parameters)
}
}
diff --git a/Library/Sources/AppUIMain/Views/Modules/Extensions/IPModule+Extensions.swift b/Library/Sources/AppUIMain/Views/Modules/Extensions/IPModule+Extensions.swift
index 62b381f4..fffb89e9 100644
--- a/Library/Sources/AppUIMain/Views/Modules/Extensions/IPModule+Extensions.swift
+++ b/Library/Sources/AppUIMain/Views/Modules/Extensions/IPModule+Extensions.swift
@@ -23,12 +23,13 @@
// along with Passepartout. If not, see .
//
+import CommonLibrary
import PassepartoutKit
import SwiftUI
import UILibrary
extension IPModule.Builder: ModuleViewProviding {
- public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
- IPView(editor: editor, module: self)
+ public func moduleView(with parameters: ModuleViewParameters) -> some View {
+ IPView(module: self, parameters: parameters)
}
}
diff --git a/Library/Sources/AppUIMain/Views/Modules/Extensions/OnDemandModule+Extensions.swift b/Library/Sources/AppUIMain/Views/Modules/Extensions/OnDemandModule+Extensions.swift
index 49d21ae7..88b862b8 100644
--- a/Library/Sources/AppUIMain/Views/Modules/Extensions/OnDemandModule+Extensions.swift
+++ b/Library/Sources/AppUIMain/Views/Modules/Extensions/OnDemandModule+Extensions.swift
@@ -28,7 +28,7 @@ import SwiftUI
import UILibrary
extension OnDemandModule.Builder: ModuleViewProviding {
- public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
- OnDemandView(editor: editor, module: self)
+ public func moduleView(with parameters: ModuleViewParameters) -> some View {
+ OnDemandView(module: self, parameters: parameters)
}
}
diff --git a/Library/Sources/AppUIMain/Views/Modules/Extensions/OpenVPNModule+Extensions.swift b/Library/Sources/AppUIMain/Views/Modules/Extensions/OpenVPNModule+Extensions.swift
index bf3dcb6f..5b510c61 100644
--- a/Library/Sources/AppUIMain/Views/Modules/Extensions/OpenVPNModule+Extensions.swift
+++ b/Library/Sources/AppUIMain/Views/Modules/Extensions/OpenVPNModule+Extensions.swift
@@ -29,8 +29,8 @@ import SwiftUI
import UILibrary
extension OpenVPNModule.Builder: ModuleViewProviding {
- public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
- OpenVPNView(editor: editor, module: self, impl: impl as? OpenVPNModule.Implementation)
+ public func moduleView(with parameters: ModuleViewParameters) -> some View {
+ OpenVPNView(module: self, parameters: parameters)
}
}
diff --git a/Library/Sources/AppUIMain/Views/Modules/Extensions/WireGuardModule+Extensions.swift b/Library/Sources/AppUIMain/Views/Modules/Extensions/WireGuardModule+Extensions.swift
index 347a673b..cf1d8014 100644
--- a/Library/Sources/AppUIMain/Views/Modules/Extensions/WireGuardModule+Extensions.swift
+++ b/Library/Sources/AppUIMain/Views/Modules/Extensions/WireGuardModule+Extensions.swift
@@ -29,8 +29,8 @@ import SwiftUI
import UILibrary
extension WireGuardModule.Builder: ModuleViewProviding {
- public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
- WireGuardView(editor: editor, module: self, impl: impl as? WireGuardModule.Implementation)
+ public func moduleView(with parameters: ModuleViewParameters) -> some View {
+ WireGuardView(module: self, parameters: parameters)
}
}
diff --git a/Library/Sources/AppUIMain/Views/Modules/HTTPProxyView.swift b/Library/Sources/AppUIMain/Views/Modules/HTTPProxyView.swift
index 5656d264..dfdde2a9 100644
--- a/Library/Sources/AppUIMain/Views/Modules/HTTPProxyView.swift
+++ b/Library/Sources/AppUIMain/Views/Modules/HTTPProxyView.swift
@@ -32,10 +32,15 @@ struct HTTPProxyView: View, ModuleDraftEditing {
@EnvironmentObject
private var theme: Theme
+ let module: HTTPProxyModule.Builder
+
@ObservedObject
var editor: ProfileEditor
- let module: HTTPProxyModule.Builder
+ init(module: HTTPProxyModule.Builder, parameters: ModuleViewParameters) {
+ self.module = module
+ editor = parameters.editor
+ }
var body: some View {
Group {
diff --git a/Library/Sources/AppUIMain/Views/Modules/IPView.swift b/Library/Sources/AppUIMain/Views/Modules/IPView.swift
index 4296b11b..3703750b 100644
--- a/Library/Sources/AppUIMain/Views/Modules/IPView.swift
+++ b/Library/Sources/AppUIMain/Views/Modules/IPView.swift
@@ -28,15 +28,19 @@ import PassepartoutKit
import SwiftUI
struct IPView: View, ModuleDraftEditing {
+ let module: IPModule.Builder
@ObservedObject
var editor: ProfileEditor
- let module: IPModule.Builder
-
@State
private var routePresentation: RoutePresentation?
+ init(module: IPModule.Builder, parameters: ModuleViewParameters) {
+ self.module = module
+ editor = parameters.editor
+ }
+
var body: some View {
Group {
ipSections(for: .v4)
diff --git a/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift b/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift
index 92a90d37..f027a843 100644
--- a/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift
+++ b/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift
@@ -33,23 +33,23 @@ struct OnDemandView: View, ModuleDraftEditing {
@EnvironmentObject
private var theme: Theme
+ let module: OnDemandModule.Builder
+
@ObservedObject
var editor: ProfileEditor
- let module: OnDemandModule.Builder
-
private let wifi: Wifi
@State
private var paywallReason: PaywallReason?
init(
- editor: ProfileEditor,
module: OnDemandModule.Builder,
+ parameters: ModuleViewParameters,
observer: WifiObserver? = nil
) {
- self.editor = editor
self.module = module
+ editor = parameters.editor
wifi = Wifi(observer: observer ?? CoreLocationWifiObserver())
}
@@ -239,8 +239,12 @@ private extension OnDemandView {
]
return module.preview {
OnDemandView(
- editor: $0,
- module: $1,
+ module: $0,
+ parameters: .init(
+ editor: $1,
+ preferences: nil,
+ impl: nil
+ ),
observer: MockWifi()
)
}
diff --git a/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift b/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift
index 9e7e74c3..b2aa002f 100644
--- a/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift
+++ b/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift
@@ -33,11 +33,11 @@ struct OpenVPNView: View, ModuleDraftEditing {
@Environment(\.navigationPath)
private var path
+ let module: OpenVPNModule.Builder
+
@ObservedObject
var editor: ProfileEditor
- let module: OpenVPNModule.Builder
-
let impl: OpenVPNModule.Implementation?
private let isServerPushed: Bool
@@ -61,20 +61,18 @@ struct OpenVPNView: View, ModuleDraftEditing {
private var errorHandler: ErrorHandler = .default()
init(serverConfiguration: OpenVPN.Configuration) {
- let module = OpenVPNModule.Builder(configurationBuilder: serverConfiguration.builder())
- let editor = ProfileEditor(modules: [module])
+ module = OpenVPNModule.Builder(configurationBuilder: serverConfiguration.builder())
+ editor = ProfileEditor(modules: [module])
assert(module.configurationBuilder != nil, "isServerPushed must imply module.configurationBuilder != nil")
- self.editor = editor
- self.module = module
impl = nil
isServerPushed = true
}
- init(editor: ProfileEditor, module: OpenVPNModule.Builder, impl: OpenVPNModule.Implementation?) {
- self.editor = editor
+ init(module: OpenVPNModule.Builder, parameters: ModuleViewParameters) {
self.module = module
- self.impl = impl
+ editor = parameters.editor
+ impl = parameters.impl as? OpenVPNModule.Implementation
isServerPushed = false
}
diff --git a/Library/Sources/AppUIMain/Views/Modules/WireGuardView.swift b/Library/Sources/AppUIMain/Views/Modules/WireGuardView.swift
index 025189b0..5ac8bdc5 100644
--- a/Library/Sources/AppUIMain/Views/Modules/WireGuardView.swift
+++ b/Library/Sources/AppUIMain/Views/Modules/WireGuardView.swift
@@ -33,11 +33,11 @@ struct WireGuardView: View, ModuleDraftEditing {
@Environment(\.navigationPath)
private var path
+ let module: WireGuardModule.Builder
+
@ObservedObject
var editor: ProfileEditor
- let module: WireGuardModule.Builder
-
let impl: WireGuardModule.Implementation?
@State
@@ -46,6 +46,12 @@ struct WireGuardView: View, ModuleDraftEditing {
@State
private var errorHandler: ErrorHandler = .default()
+ init(module: WireGuardModule.Builder, parameters: ModuleViewParameters) {
+ self.module = module
+ editor = parameters.editor
+ impl = parameters.impl as? WireGuardModule.Implementation
+ }
+
var body: some View {
contentView
.moduleView(editor: editor, draft: draft.wrappedValue)
diff --git a/Library/Sources/AppUIMain/Views/Profile/ModuleDetailView.swift b/Library/Sources/AppUIMain/Views/Profile/ModuleDetailView.swift
index 02a138f9..d13fe879 100644
--- a/Library/Sources/AppUIMain/Views/Profile/ModuleDetailView.swift
+++ b/Library/Sources/AppUIMain/Views/Profile/ModuleDetailView.swift
@@ -23,16 +23,24 @@
// along with Passepartout. If not, see .
//
+import CommonLibrary
import PassepartoutKit
import SwiftUI
struct ModuleDetailView: View {
+
+ @EnvironmentObject
+ private var preferencesManager: PreferencesManager
+
let profileEditor: ProfileEditor
let moduleId: UUID?
let moduleViewFactory: any ModuleViewFactory
+ @StateObject
+ private var modulePreferences = ModulePreferences(proxy: nil)
+
var body: some View {
debugChanges()
return Group {
@@ -42,6 +50,16 @@ struct ModuleDetailView: View {
emptyView
}
}
+ .onLoad {
+ guard let moduleId else {
+ return
+ }
+ do {
+ modulePreferences.proxy = try preferencesManager.modulePreferencesProxy(in: moduleId)
+ } catch {
+ pp_log(.app, .error, "Unable to load module preferences: \(error)")
+ }
+ }
}
}
@@ -49,7 +67,11 @@ private extension ModuleDetailView {
@MainActor
func editorView(forModuleWithId moduleId: UUID) -> some View {
- AnyView(moduleViewFactory.view(with: profileEditor, moduleId: moduleId))
+ AnyView(moduleViewFactory.view(
+ with: profileEditor,
+ preferences: modulePreferences,
+ moduleId: moduleId
+ ))
}
var emptyView: some View {
diff --git a/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift b/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift
index 0d59b032..c1a0b90f 100644
--- a/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift
+++ b/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift
@@ -34,6 +34,9 @@ struct VPNProviderServerView: View where Configuration: Identifia
@EnvironmentObject
private var providerManager: ProviderManager
+ @EnvironmentObject
+ private var preferencesManager: PreferencesManager
+
var apis: [APIMapper] = API.shared
let moduleId: UUID
@@ -65,10 +68,10 @@ struct VPNProviderServerView: View where Configuration: Identifia
private var onlyShowsFavorites = false
@StateObject
- private var filtersViewModel = VPNFiltersView.Model()
+ private var providerPreferences = ProviderPreferences(proxy: nil)
@StateObject
- private var favoritesManager = ProviderFavoritesManager()
+ private var filtersViewModel = VPNFiltersView.Model()
@StateObject
private var errorHandler: ErrorHandler = .default()
@@ -94,7 +97,7 @@ extension VPNProviderServerView {
selectedServer: selectedEntity?.server,
isFiltering: isFiltering,
filtersViewModel: filtersViewModel,
- favoritesManager: favoritesManager,
+ providerPreferences: providerPreferences,
selectTitle: selectTitle,
onSelect: onSelectServer
)
@@ -123,7 +126,7 @@ private extension VPNProviderServerView {
var filteredServers: [VPNServer] {
if onlyShowsFavorites {
return servers.filter {
- favoritesManager.serverIds.contains($0.serverId)
+ providerPreferences.favoriteServers.contains($0.serverId)
}
}
return servers
@@ -156,7 +159,11 @@ private extension VPNProviderServerView {
private extension VPNProviderServerView {
func loadInitialServers() async {
do {
- favoritesManager.moduleId = moduleId
+ providerPreferences.proxy = try preferencesManager.providerPreferencesProxy(in: providerId)
+ } catch {
+ pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(error)")
+ }
+ do {
let repository = try await providerManager.vpnServerRepository(
from: apis,
for: providerId
@@ -165,7 +172,7 @@ private extension VPNProviderServerView {
filtersViewModel.load(options: vpnManager.options, initialFilters: initialFilters)
await reloadServers(filters: filtersViewModel.filters)
} catch {
- pp_log(.app, .error, "Unable to load VPN repository: \(error)")
+ pp_log(.app, .error, "Unable to load VPN servers for provider \(providerId): \(error)")
errorHandler.handle(error, title: Strings.Global.Nouns.servers)
}
}
@@ -194,7 +201,11 @@ private extension VPNProviderServerView {
}
func onDisappear() {
- favoritesManager.save()
+ do {
+ try providerPreferences.save()
+ } catch {
+ pp_log(.app, .error, "Unable to save preferences: \(error)")
+ }
}
func onSelectServer(_ server: VPNServer) {
diff --git a/Library/Sources/AppUIMain/Views/VPN/iOS/VPNProviderServer+Content+iOS.swift b/Library/Sources/AppUIMain/Views/VPN/iOS/VPNProviderServer+Content+iOS.swift
index e06458ce..a36a394d 100644
--- a/Library/Sources/AppUIMain/Views/VPN/iOS/VPNProviderServer+Content+iOS.swift
+++ b/Library/Sources/AppUIMain/Views/VPN/iOS/VPNProviderServer+Content+iOS.swift
@@ -46,7 +46,7 @@ extension VPNProviderServerView {
var filtersViewModel: VPNFiltersView.Model
@ObservedObject
- var favoritesManager: ProviderFavoritesManager
+ var providerPreferences: ProviderPreferences
let selectTitle: String
@@ -151,7 +151,7 @@ private extension VPNProviderServerView.ContentView {
Spacer()
FavoriteToggle(
value: server.serverId,
- selection: $favoritesManager.serverIds
+ selection: $providerPreferences.favoriteServers
)
}
}
diff --git a/Library/Sources/AppUIMain/Views/VPN/macOS/VPNProviderServer+Content+macOS.swift b/Library/Sources/AppUIMain/Views/VPN/macOS/VPNProviderServer+Content+macOS.swift
index 1af171b9..5be6bf19 100644
--- a/Library/Sources/AppUIMain/Views/VPN/macOS/VPNProviderServer+Content+macOS.swift
+++ b/Library/Sources/AppUIMain/Views/VPN/macOS/VPNProviderServer+Content+macOS.swift
@@ -50,7 +50,7 @@ extension VPNProviderServerView {
var filtersViewModel: VPNFiltersView.Model
@ObservedObject
- var favoritesManager: ProviderFavoritesManager
+ var providerPreferences: ProviderPreferences
let selectTitle: String
@@ -87,7 +87,7 @@ private extension VPNProviderServerView.ContentView {
TableColumn("") { server in
FavoriteToggle(
value: server.serverId,
- selection: $favoritesManager.serverIds
+ selection: $providerPreferences.favoriteServers
)
.environmentObject(theme) // TODO: #873, Table loses environment
}
diff --git a/Library/Sources/CommonLibrary/Business/PreferencesManager.swift b/Library/Sources/CommonLibrary/Business/PreferencesManager.swift
new file mode 100644
index 00000000..8db9d4f5
--- /dev/null
+++ b/Library/Sources/CommonLibrary/Business/PreferencesManager.swift
@@ -0,0 +1,77 @@
+//
+// PreferencesManager.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 12/4/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 CommonUtils
+import Foundation
+import PassepartoutKit
+
+@MainActor
+public final class PreferencesManager: ObservableObject {
+ private let modulesRepository: ModulePreferencesRepository
+
+ private let providersRepository: ProviderPreferencesRepository
+
+ public init(
+ modulesRepository: ModulePreferencesRepository? = nil,
+ providersRepository: ProviderPreferencesRepository? = nil
+ ) {
+ self.modulesRepository = modulesRepository ?? DummyModulePreferencesRepository()
+ self.providersRepository = providersRepository ?? DummyProviderPreferencesRepository()
+ }
+
+ public func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy {
+ try modulesRepository.modulePreferencesProxy(in: moduleId)
+ }
+
+ public func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy {
+ try providersRepository.providerPreferencesProxy(in: providerId)
+ }
+}
+
+// MARK: - Dummy
+
+private final class DummyModulePreferencesRepository: ModulePreferencesRepository {
+ private final class Proxy: ModulePreferencesProxy {
+ func save() throws {
+ }
+ }
+
+ func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy {
+ Proxy()
+ }
+}
+
+private final class DummyProviderPreferencesRepository: ProviderPreferencesRepository {
+ private final class Proxy: ProviderPreferencesProxy {
+ var favoriteServers: Set = []
+
+ func save() throws {
+ }
+ }
+
+ func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy {
+ Proxy()
+ }
+}
diff --git a/Library/Sources/CommonLibrary/Domain/BundleConfiguration+AppGroup.swift b/Library/Sources/CommonLibrary/Domain/BundleConfiguration+AppGroup.swift
index 5a35cb56..5dc30bfd 100644
--- a/Library/Sources/CommonLibrary/Domain/BundleConfiguration+AppGroup.swift
+++ b/Library/Sources/CommonLibrary/Domain/BundleConfiguration+AppGroup.swift
@@ -30,25 +30,35 @@ import PassepartoutKit
extension BundleConfiguration {
public static var urlForAppLog: URL {
- cachesURL.appending(path: Constants.shared.log.appPath)
+ urlForGroupCaches.appending(path: Constants.shared.log.appPath)
}
public static var urlForTunnelLog: URL {
- cachesURL.appending(path: Constants.shared.log.tunnelPath)
+ urlForGroupCaches.appending(path: Constants.shared.log.tunnelPath)
}
public static var urlForBetaReceipt: URL {
- cachesURL.appending(path: Constants.shared.tunnel.betaReceiptPath)
+ urlForGroupCaches.appending(path: Constants.shared.tunnel.betaReceiptPath)
+ }
+}
+
+extension BundleConfiguration {
+ public static var urlForGroupCaches: URL {
+ appGroupURL.appending(components: "Library", "Caches")
+ }
+
+ public static var urlForGroupDocuments: URL {
+ appGroupURL.appending(components: "Library", "Documents")
}
}
private extension BundleConfiguration {
- static var cachesURL: URL {
+ static var appGroupURL: URL {
let groupId = mainString(for: .groupId)
guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupId) else {
pp_log(.app, .error, "Unable to access App Group container")
return FileManager.default.temporaryDirectory
}
- return url.appending(components: "Library", "Caches")
+ return url
}
}
diff --git a/Library/Sources/CommonLibrary/Domain/BundleConfiguration+Main.swift b/Library/Sources/CommonLibrary/Domain/BundleConfiguration+Main.swift
index d49150e4..9a9272e3 100644
--- a/Library/Sources/CommonLibrary/Domain/BundleConfiguration+Main.swift
+++ b/Library/Sources/CommonLibrary/Domain/BundleConfiguration+Main.swift
@@ -34,6 +34,8 @@ extension BundleConfiguration {
case cloudKitId
+ case cloudKitPreferencesId
+
case userLevel
case groupId
diff --git a/Library/Sources/CommonLibrary/Domain/Constants.swift b/Library/Sources/CommonLibrary/Domain/Constants.swift
index d67517b9..83319b8e 100644
--- a/Library/Sources/CommonLibrary/Domain/Constants.swift
+++ b/Library/Sources/CommonLibrary/Domain/Constants.swift
@@ -28,12 +28,14 @@ import PassepartoutKit
public struct Constants: Decodable, Sendable {
public struct Containers: Decodable, Sendable {
- public let local: String
+ public let localProfiles: String
- public let remote: String
+ public let remoteProfiles: String
public let providers: String
+ public let preferences: String
+
public let legacyV2: String
public let legacyV2TV: String
diff --git a/Library/Sources/CommonLibrary/Domain/ModulePreferences.swift b/Library/Sources/CommonLibrary/Domain/ModulePreferences.swift
new file mode 100644
index 00000000..5a5ad185
--- /dev/null
+++ b/Library/Sources/CommonLibrary/Domain/ModulePreferences.swift
@@ -0,0 +1,49 @@
+//
+// ModulePreferences.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 12/5/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
+import PassepartoutKit
+
+@MainActor
+public final class ModulePreferences: ObservableObject {
+ public var proxy: ModulePreferencesProxy? {
+ didSet {
+ objectWillChange.send()
+ }
+ }
+
+ public init(proxy: ModulePreferencesProxy?) {
+ self.proxy = proxy
+ }
+
+ public func save() throws {
+ try proxy?.save()
+ }
+}
+
+@MainActor
+public protocol ModulePreferencesProxy {
+ func save() throws
+}
diff --git a/Library/Sources/CommonLibrary/Domain/ProviderPreferences.swift b/Library/Sources/CommonLibrary/Domain/ProviderPreferences.swift
new file mode 100644
index 00000000..81f42bc8
--- /dev/null
+++ b/Library/Sources/CommonLibrary/Domain/ProviderPreferences.swift
@@ -0,0 +1,61 @@
+//
+// ProviderPreferences.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 12/5/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
+import PassepartoutKit
+
+@MainActor
+public final class ProviderPreferences: ObservableObject {
+ public var proxy: ProviderPreferencesProxy? {
+ didSet {
+ objectWillChange.send()
+ }
+ }
+
+ public init(proxy: ProviderPreferencesProxy?) {
+ self.proxy = proxy
+ }
+
+ public var favoriteServers: Set {
+ get {
+ proxy?.favoriteServers ?? []
+ }
+ set {
+ objectWillChange.send()
+ proxy?.favoriteServers = newValue
+ }
+ }
+
+ public func save() throws {
+ try proxy?.save()
+ }
+}
+
+@MainActor
+public protocol ProviderPreferencesProxy {
+ var favoriteServers: Set { get set }
+
+ func save() throws
+}
diff --git a/Library/Sources/CommonLibrary/Resources/Constants.json b/Library/Sources/CommonLibrary/Resources/Constants.json
index d4a1e6e9..677fea5c 100644
--- a/Library/Sources/CommonLibrary/Resources/Constants.json
+++ b/Library/Sources/CommonLibrary/Resources/Constants.json
@@ -1,9 +1,10 @@
{
"bundleKey": "AppConfig",
"containers": {
- "local": "Profiles-v3",
- "remote": "Profiles-v3.remote",
+ "localProfiles": "Profiles-v3",
+ "remoteProfiles": "Profiles-v3.remote",
"providers": "Providers-v3",
+ "preferences": "Preferences-v3",
"legacyV2": "Profiles",
"legacyV2TV": "SharedProfiles"
},
diff --git a/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift b/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift
new file mode 100644
index 00000000..5f911a29
--- /dev/null
+++ b/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift
@@ -0,0 +1,31 @@
+//
+// ModulePreferencesRepository.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 12/5/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
+
+@MainActor
+public protocol ModulePreferencesRepository {
+ func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy
+}
diff --git a/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift b/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift
new file mode 100644
index 00000000..7454a77d
--- /dev/null
+++ b/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift
@@ -0,0 +1,32 @@
+//
+// ProviderPreferencesRepository.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 12/5/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
+import PassepartoutKit
+
+@MainActor
+public protocol ProviderPreferencesRepository {
+ func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy
+}
diff --git a/Library/Sources/UILibrary/Domain/ProviderFavoriteServers.swift b/Library/Sources/CommonUtils/Extensions/UUID+RawRepresentable.swift
similarity index 55%
rename from Library/Sources/UILibrary/Domain/ProviderFavoriteServers.swift
rename to Library/Sources/CommonUtils/Extensions/UUID+RawRepresentable.swift
index 14dee1a7..2c1c2fe0 100644
--- a/Library/Sources/UILibrary/Domain/ProviderFavoriteServers.swift
+++ b/Library/Sources/CommonUtils/Extensions/UUID+RawRepresentable.swift
@@ -1,8 +1,8 @@
//
-// ProviderFavoriteServers.swift
+// UUID+RawRepresentable.swift
// Passepartout
//
-// Created by Davide De Rosa on 10/25/24.
+// Created by Davide De Rosa on 12/4/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
@@ -25,31 +25,12 @@
import Foundation
-public struct ProviderFavoriteServers {
- private var map: [UUID: Set]
-
- public init() {
- map = [:]
- }
-
- public func servers(forModuleWithId moduleId: UUID) -> Set {
- map[moduleId] ?? []
- }
-
- public mutating func setServers(_ servers: Set, forModuleWithId moduleId: UUID) {
- map[moduleId] = servers
- }
-}
-
-extension ProviderFavoriteServers: RawRepresentable {
- public var rawValue: String {
- (try? JSONEncoder().encode(map))?.base64EncodedString() ?? ""
- }
-
+extension UUID: @retroactive RawRepresentable {
public init?(rawValue: String) {
- guard let data = Data(base64Encoded: rawValue) else {
- return nil
- }
- map = (try? JSONDecoder().decode([UUID: Set].self, from: data)) ?? [:]
+ self.init(uuidString: rawValue)
+ }
+
+ public var rawValue: String {
+ uuidString
}
}
diff --git a/Library/Sources/UILibrary/Business/AppContext.swift b/Library/Sources/UILibrary/Business/AppContext.swift
index 4b48c642..d0901307 100644
--- a/Library/Sources/UILibrary/Business/AppContext.swift
+++ b/Library/Sources/UILibrary/Business/AppContext.swift
@@ -40,6 +40,8 @@ public final class AppContext: ObservableObject {
public let providerManager: ProviderManager
+ public let preferencesManager: PreferencesManager
+
public let registry: Registry
public let tunnel: ExtendedTunnel
@@ -57,6 +59,7 @@ public final class AppContext: ObservableObject {
migrationManager: MigrationManager,
profileManager: ProfileManager,
providerManager: ProviderManager,
+ preferencesManager: PreferencesManager,
registry: Registry,
tunnel: ExtendedTunnel,
tunnelReceiptURL: URL
@@ -65,6 +68,7 @@ public final class AppContext: ObservableObject {
self.migrationManager = migrationManager
self.profileManager = profileManager
self.providerManager = providerManager
+ self.preferencesManager = preferencesManager
self.registry = registry
self.tunnel = tunnel
self.tunnelReceiptURL = tunnelReceiptURL
diff --git a/Library/Sources/UILibrary/Business/ProviderFavoritesManager.swift b/Library/Sources/UILibrary/Business/ProviderFavoritesManager.swift
deleted file mode 100644
index a41858f8..00000000
--- a/Library/Sources/UILibrary/Business/ProviderFavoritesManager.swift
+++ /dev/null
@@ -1,64 +0,0 @@
-//
-// ProviderFavoritesManager.swift
-// Passepartout
-//
-// Created by Davide De Rosa on 10/26/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
-
-@MainActor
-public final class ProviderFavoritesManager: ObservableObject {
- private let defaults: UserDefaults
-
- private var allFavorites: ProviderFavoriteServers
-
- public var moduleId: UUID {
- didSet {
- guard let rawValue = defaults.string(forKey: UIPreference.providerFavoriteServers.key) else {
- allFavorites = ProviderFavoriteServers()
- return
- }
- allFavorites = ProviderFavoriteServers(rawValue: rawValue) ?? ProviderFavoriteServers()
- }
- }
-
- public var serverIds: Set {
- get {
- allFavorites.servers(forModuleWithId: moduleId)
- }
- set {
- objectWillChange.send()
- allFavorites.setServers(newValue, forModuleWithId: moduleId)
- }
- }
-
- public init(defaults: UserDefaults = .standard) {
- self.defaults = defaults
- allFavorites = ProviderFavoriteServers()
- moduleId = UUID()
- }
-
- public func save() {
- defaults.set(allFavorites.rawValue, forKey: UIPreference.providerFavoriteServers.key)
- }
-}
diff --git a/Library/Sources/UILibrary/Domain/UIPreference.swift b/Library/Sources/UILibrary/Domain/UIPreference.swift
index a6b1e0cd..f246df3b 100644
--- a/Library/Sources/UILibrary/Domain/UIPreference.swift
+++ b/Library/Sources/UILibrary/Domain/UIPreference.swift
@@ -37,8 +37,6 @@ public enum UIPreference: String, PreferenceProtocol {
case profilesLayout
- case providerFavoriteServers
-
public var key: String {
"UI.\(rawValue)"
}
diff --git a/Library/Sources/UILibrary/Extensions/ModuleBuilder+Previews.swift b/Library/Sources/UILibrary/Extensions/ModuleBuilder+Previews.swift
index 54189a31..49610f19 100644
--- a/Library/Sources/UILibrary/Extensions/ModuleBuilder+Previews.swift
+++ b/Library/Sources/UILibrary/Extensions/ModuleBuilder+Previews.swift
@@ -31,16 +31,20 @@ extension ModuleBuilder where Self: ModuleViewProviding {
@MainActor
public func preview(title: String = "") -> some View {
NavigationStack {
- moduleView(with: ProfileEditor(modules: [self]), impl: nil)
- .navigationTitle(title)
+ moduleView(with: .init(
+ editor: ProfileEditor(modules: [self]),
+ preferences: nil,
+ impl: nil
+ ))
+ .navigationTitle(title)
}
.withMockEnvironment()
}
@MainActor
- public func preview(with content: (ProfileEditor, Self) -> C) -> some View {
+ public func preview(with content: (Self, ProfileEditor) -> C) -> some View {
NavigationStack {
- content(ProfileEditor(modules: [self]), self)
+ content(self, ProfileEditor(modules: [self]))
}
.withMockEnvironment()
}
diff --git a/Library/Sources/UILibrary/Extensions/View+Environment.swift b/Library/Sources/UILibrary/Extensions/View+Environment.swift
index 04a2c08a..22925468 100644
--- a/Library/Sources/UILibrary/Extensions/View+Environment.swift
+++ b/Library/Sources/UILibrary/Extensions/View+Environment.swift
@@ -31,6 +31,7 @@ extension View {
environmentObject(theme)
.environmentObject(context.iapManager)
.environmentObject(context.migrationManager)
+ .environmentObject(context.preferencesManager)
.environmentObject(context.providerManager)
}
diff --git a/Library/Sources/UILibrary/Previews/AppContext+Previews.swift b/Library/Sources/UILibrary/Previews/AppContext+Previews.swift
index 40a003ab..acd8d2d5 100644
--- a/Library/Sources/UILibrary/Previews/AppContext+Previews.swift
+++ b/Library/Sources/UILibrary/Previews/AppContext+Previews.swift
@@ -83,6 +83,7 @@ extension AppContext {
migrationManager: migrationManager,
profileManager: profileManager,
providerManager: providerManager,
+ preferencesManager: PreferencesManager(),
registry: Registry(),
tunnel: tunnel,
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
diff --git a/Library/Sources/UILibrary/Strategy/ModuleViewFactory+Default.swift b/Library/Sources/UILibrary/Strategy/ModuleViewFactory+Default.swift
index 502bd528..b41d7763 100644
--- a/Library/Sources/UILibrary/Strategy/ModuleViewFactory+Default.swift
+++ b/Library/Sources/UILibrary/Strategy/ModuleViewFactory+Default.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import CommonLibrary
import Foundation
import PassepartoutKit
import SwiftUI
@@ -35,11 +36,15 @@ public final class DefaultModuleViewFactory: ModuleViewFactory {
}
@ViewBuilder
- public func view(with editor: ProfileEditor, moduleId: UUID) -> some View {
+ public func view(with editor: ProfileEditor, preferences: ModulePreferences, moduleId: UUID) -> some View {
let result = editor.moduleViewProvider(withId: moduleId, registry: registry)
if let result {
- AnyView(result.provider.moduleView(with: editor, impl: result.impl))
- .navigationTitle(result.title)
+ AnyView(result.provider.moduleView(with: .init(
+ editor: editor,
+ preferences: preferences,
+ impl: result.impl
+ )))
+ .navigationTitle(result.title)
}
}
}
diff --git a/Library/Sources/UILibrary/Strategy/ModuleViewFactory.swift b/Library/Sources/UILibrary/Strategy/ModuleViewFactory.swift
index 620b3c33..1b46d1ae 100644
--- a/Library/Sources/UILibrary/Strategy/ModuleViewFactory.swift
+++ b/Library/Sources/UILibrary/Strategy/ModuleViewFactory.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import CommonLibrary
import Foundation
import SwiftUI
@@ -30,5 +31,5 @@ public protocol ModuleViewFactory: AnyObject {
associatedtype Content: View
@MainActor
- func view(with editor: ProfileEditor, moduleId: UUID) -> Content
+ func view(with editor: ProfileEditor, preferences: ModulePreferences, moduleId: UUID) -> Content
}
diff --git a/Library/Sources/UILibrary/Strategy/ModuleViewProviding.swift b/Library/Sources/UILibrary/Strategy/ModuleViewProviding.swift
index e0b07b86..bfc47208 100644
--- a/Library/Sources/UILibrary/Strategy/ModuleViewProviding.swift
+++ b/Library/Sources/UILibrary/Strategy/ModuleViewProviding.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import CommonLibrary
import PassepartoutKit
import SwiftUI
@@ -30,5 +31,24 @@ public protocol ModuleViewProviding {
associatedtype Content: View
@MainActor
- func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> Content
+ func moduleView(with parameters: ModuleViewParameters) -> Content
+}
+
+public struct ModuleViewParameters {
+ public let editor: ProfileEditor
+
+ public let preferences: ModulePreferences
+
+ public let impl: (any ModuleImplementation)?
+
+ @MainActor
+ public init(
+ editor: ProfileEditor,
+ preferences: ModulePreferences?,
+ impl: (any ModuleImplementation)?
+ ) {
+ self.editor = editor
+ self.preferences = preferences ?? ModulePreferences(proxy: nil)
+ self.impl = impl
+ }
}
diff --git a/Passepartout/App/App.entitlements b/Passepartout/App/App.entitlements
index e567aaa8..1f592f6a 100644
--- a/Passepartout/App/App.entitlements
+++ b/Passepartout/App/App.entitlements
@@ -9,6 +9,7 @@
com.apple.developer.icloud-container-identifiers
$(CFG_CLOUDKIT_ID)
+ $(CFG_CLOUDKIT_PREFERENCES_ID)
$(CFG_LEGACY_V2_CLOUDKIT_ID)
$(CFG_LEGACY_V2_TV_CLOUDKIT_ID)
diff --git a/Passepartout/App/App.plist b/Passepartout/App/App.plist
index b2e85be7..35ccd315 100644
--- a/Passepartout/App/App.plist
+++ b/Passepartout/App/App.plist
@@ -8,6 +8,8 @@
$(CFG_APP_STORE_ID)
cloudKitId
$(CFG_CLOUDKIT_ID)
+ cloudKitPreferencesId
+ $(CFG_CLOUDKIT_PREFERENCES_ID)
groupId
$(CFG_GROUP_ID)
iapBundlePrefix
diff --git a/Passepartout/Config.xcconfig b/Passepartout/Config.xcconfig
index d31e9408..8156f511 100644
--- a/Passepartout/Config.xcconfig
+++ b/Passepartout/Config.xcconfig
@@ -29,6 +29,7 @@
CFG_APP_ID = com.algoritmico.ios.Passepartout
CFG_APP_STORE_ID = 1433648537
CFG_CLOUDKIT_ID = iCloud.com.algoritmico.Passepartout.v3
+CFG_CLOUDKIT_PREFERENCES_ID = iCloud.com.algoritmico.Passepartout.v3.Preferences
CFG_COPYRIGHT = Copyright © 2024 Davide De Rosa. All rights reserved.
CFG_DISPLAY_NAME = Passepartout
CFG_GROUP_ID[sdk=appletvos*] = $(CFG_RAW_GROUP_ID)
diff --git a/Passepartout/Shared/AppContext+Shared.swift b/Passepartout/Shared/AppContext+Shared.swift
index dbc2593d..fd2beef1 100644
--- a/Passepartout/Shared/AppContext+Shared.swift
+++ b/Passepartout/Shared/AppContext+Shared.swift
@@ -48,7 +48,7 @@ extension AppContext {
let remoteRepositoryBlock: (Bool) -> ProfileRepository = {
let remoteStore = CoreDataPersistentStore(
logger: .default,
- containerName: Constants.shared.containers.remote,
+ containerName: Constants.shared.containers.remoteProfiles,
model: AppData.cdProfilesModel,
cloudKitIdentifier: $0 ? BundleConfiguration.mainString(for: .cloudKitId) : nil,
author: nil
@@ -124,6 +124,7 @@ extension AppContext {
migrationManager: migrationManager,
profileManager: profileManager,
providerManager: providerManager,
+ preferencesManager: .shared,
registry: .shared,
tunnel: tunnel,
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
@@ -199,7 +200,7 @@ private extension Dependencies.ProfileManager {
static func coreDataProfileRepository(observingResults: Bool) -> ProfileRepository {
let store = CoreDataPersistentStore(
logger: .default,
- containerName: Constants.shared.containers.local,
+ containerName: Constants.shared.containers.localProfiles,
model: AppData.cdProfilesModel,
cloudKitIdentifier: nil,
author: nil
@@ -215,21 +216,3 @@ private extension Dependencies.ProfileManager {
}
}
}
-
-// MARK: - Logging
-
-private extension CoreDataPersistentStoreLogger where Self == DefaultCoreDataPersistentStoreLogger {
- static var `default`: CoreDataPersistentStoreLogger {
- DefaultCoreDataPersistentStoreLogger()
- }
-}
-
-private struct DefaultCoreDataPersistentStoreLogger: CoreDataPersistentStoreLogger {
- func debug(_ msg: String) {
- pp_log(.app, .info, msg)
- }
-
- func warning(_ msg: String) {
- pp_log(.app, .error, msg)
- }
-}
diff --git a/Passepartout/Shared/Shared.swift b/Passepartout/Shared/Shared.swift
index a757d960..002789b4 100644
--- a/Passepartout/Shared/Shared.swift
+++ b/Passepartout/Shared/Shared.swift
@@ -23,6 +23,8 @@
// along with Passepartout. If not, see .
//
+import AppData
+import AppDataPreferences
import CommonLibrary
import CommonUtils
import CPassepartoutOpenVPNOpenSSL
@@ -30,8 +32,6 @@ import Foundation
import PassepartoutKit
import PassepartoutWireGuardGo
-// MARK: Registry
-
extension Registry {
static let shared = Registry(
withKnownHandlers: true,
@@ -130,3 +130,40 @@ extension InAppProcessor {
)
}
}
+
+extension PreferencesManager {
+ static let shared: PreferencesManager = {
+ let preferencesStore = CoreDataPersistentStore(
+ logger: .default,
+ containerName: Constants.shared.containers.preferences,
+ baseURL: BundleConfiguration.urlForGroupDocuments,
+ model: AppData.cdPreferencesModel,
+ cloudKitIdentifier: BundleConfiguration.mainString(for: .cloudKitPreferencesId),
+ author: nil
+ )
+ let modulePreferencesRepository = AppData.cdModulePreferencesRepositoryV3(context: preferencesStore.context)
+ let providerPreferencesRepository = AppData.cdProviderPreferencesRepositoryV3(context: preferencesStore.context)
+ return PreferencesManager(
+ modulesRepository: modulePreferencesRepository,
+ providersRepository: providerPreferencesRepository
+ )
+ }()
+}
+
+// MARK: - Logging
+
+extension CoreDataPersistentStoreLogger where Self == DefaultCoreDataPersistentStoreLogger {
+ static var `default`: CoreDataPersistentStoreLogger {
+ DefaultCoreDataPersistentStoreLogger()
+ }
+}
+
+struct DefaultCoreDataPersistentStoreLogger: CoreDataPersistentStoreLogger {
+ func debug(_ msg: String) {
+ pp_log(.app, .info, msg)
+ }
+
+ func warning(_ msg: String) {
+ pp_log(.app, .error, msg)
+ }
+}
diff --git a/Passepartout/Shared/Testing/AppContext+Testing.swift b/Passepartout/Shared/Testing/AppContext+Testing.swift
index e84397e3..1452a7ae 100644
--- a/Passepartout/Shared/Testing/AppContext+Testing.swift
+++ b/Passepartout/Shared/Testing/AppContext+Testing.swift
@@ -65,6 +65,7 @@ extension AppContext {
migrationManager: migrationManager,
profileManager: profileManager,
providerManager: providerManager,
+ preferencesManager: PreferencesManager(),
registry: registry,
tunnel: tunnel,
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
diff --git a/Passepartout/Tunnel/Tunnel.plist b/Passepartout/Tunnel/Tunnel.plist
index db76be5d..22a65386 100644
--- a/Passepartout/Tunnel/Tunnel.plist
+++ b/Passepartout/Tunnel/Tunnel.plist
@@ -10,6 +10,8 @@
$(CFG_KEYCHAIN_GROUP_ID)
tunnelId
$(CFG_TUNNEL_ID)
+ cloudKitPreferencesId
+ $(CFG_CLOUDKIT_PREFERENCES_ID)
NSExtension