diff --git a/Library/Sources/AppDataPreferences/Domain/CDExcludedEndpoint.swift b/Library/Sources/AppDataPreferences/Domain/CDExcludedEndpoint.swift
index 79c41a35..3d08a859 100644
--- a/Library/Sources/AppDataPreferences/Domain/CDExcludedEndpoint.swift
+++ b/Library/Sources/AppDataPreferences/Domain/CDExcludedEndpoint.swift
@@ -33,5 +33,5 @@ final class CDExcludedEndpoint: NSManagedObject {
}
@NSManaged var endpoint: String?
- @NSManaged var providerPreferences: CDProviderPreferencesV3?
+ @NSManaged var modulePreferences: CDModulePreferencesV3?
}
diff --git a/Library/Sources/AppDataPreferences/Domain/CDModulePreferencesV3.swift b/Library/Sources/AppDataPreferences/Domain/CDModulePreferencesV3.swift
new file mode 100644
index 00000000..6d3a93d7
--- /dev/null
+++ b/Library/Sources/AppDataPreferences/Domain/CDModulePreferencesV3.swift
@@ -0,0 +1,38 @@
+//
+// CDModulePreferencesV3.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 12/10/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 moduleId: UUID?
+ @NSManaged var lastUpdate: Date?
+ @NSManaged var excludedEndpoints: Set?
+}
diff --git a/Library/Sources/AppDataPreferences/Domain/CDProviderPreferencesV3.swift b/Library/Sources/AppDataPreferences/Domain/CDProviderPreferencesV3.swift
index 15ee8171..3d3fabc1 100644
--- a/Library/Sources/AppDataPreferences/Domain/CDProviderPreferencesV3.swift
+++ b/Library/Sources/AppDataPreferences/Domain/CDProviderPreferencesV3.swift
@@ -35,5 +35,4 @@ final class CDProviderPreferencesV3: NSManagedObject {
@NSManaged var providerId: String?
@NSManaged var lastUpdate: Date?
@NSManaged var favoriteServerIds: Data?
- @NSManaged var excludedEndpoints: Set?
}
diff --git a/Library/Sources/AppDataPreferences/Preferences.xcdatamodeld/Preferences.xcdatamodel/contents b/Library/Sources/AppDataPreferences/Preferences.xcdatamodeld/Preferences.xcdatamodel/contents
index a60d1ff3..abd570d6 100644
--- a/Library/Sources/AppDataPreferences/Preferences.xcdatamodeld/Preferences.xcdatamodel/contents
+++ b/Library/Sources/AppDataPreferences/Preferences.xcdatamodeld/Preferences.xcdatamodel/contents
@@ -2,12 +2,16 @@
-
+
+
+
+
+
+
-
\ 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..6ca884e6
--- /dev/null
+++ b/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift
@@ -0,0 +1,120 @@
+//
+// 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 {
+ public static func cdModulePreferencesRepositoryV3(context: NSManagedObjectContext, moduleId: UUID) throws -> ModulePreferencesRepository {
+ try CDModulePreferencesRepositoryV3(context: context, moduleId: moduleId)
+ }
+}
+
+private final class CDModulePreferencesRepositoryV3: ModulePreferencesRepository {
+ private nonisolated let context: NSManagedObjectContext
+
+ private let entity: CDModulePreferencesV3
+
+ init(context: NSManagedObjectContext, moduleId: UUID) throws {
+ self.context = context
+
+ entity = try context.performAndWait {
+ let request = CDModulePreferencesV3.fetchRequest()
+ request.predicate = NSPredicate(format: "moduleId == %@", moduleId.uuidString)
+ request.sortDescriptors = [.init(key: "lastUpdate", ascending: false)]
+ do {
+ let entities = try request.execute()
+
+ // dedup by lastUpdate
+ entities.enumerated().forEach {
+ guard $0.offset > 0 else {
+ return
+ }
+ $0.element.excludedEndpoints?.forEach(context.delete(_:))
+ context.delete($0.element)
+ }
+
+ let entity = entities.first ?? CDModulePreferencesV3(context: context)
+ entity.moduleId = moduleId
+ entity.lastUpdate = Date()
+ return entity
+ } catch {
+ pp_log(.app, .error, "Unable to load preferences for module \(moduleId): \(error)")
+ throw error
+ }
+ }
+ }
+
+ func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool {
+ context.performAndWait {
+ entity.excludedEndpoints?.contains {
+ $0.endpoint == endpoint.rawValue
+ } ?? false
+ }
+ }
+
+ func addExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
+ context.performAndWait {
+ guard entity.excludedEndpoints?.contains(where: {
+ $0.endpoint == endpoint.rawValue
+ }) != true else {
+ return
+ }
+ let mapper = CoreDataMapper(context: context)
+ let cdEndpoint = mapper.cdExcludedEndpoint(from: endpoint)
+ cdEndpoint.modulePreferences = entity
+ entity.excludedEndpoints?.insert(cdEndpoint)
+ }
+ }
+
+ func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
+ context.performAndWait {
+ guard let found = entity.excludedEndpoints?.first(where: {
+ $0.endpoint == endpoint.rawValue
+ }) else {
+ return
+ }
+ entity.excludedEndpoints?.remove(found)
+ context.delete(found)
+ }
+ }
+
+ func save() throws {
+ try context.performAndWait {
+ 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
index cdd5d32d..5fd0a9e8 100644
--- a/Library/Sources/AppDataPreferences/Strategy/CDProviderPreferencesRepositoryV3.swift
+++ b/Library/Sources/AppDataPreferences/Strategy/CDProviderPreferencesRepositoryV3.swift
@@ -55,7 +55,6 @@ private final class CDProviderPreferencesRepositoryV3: ProviderPreferencesReposi
guard $0.offset > 0 else {
return
}
- $0.element.excludedEndpoints?.forEach(context.delete(_:))
context.delete($0.element)
}
@@ -95,35 +94,6 @@ private final class CDProviderPreferencesRepositoryV3: ProviderPreferencesReposi
}
}
- func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool {
- context.performAndWait {
- entity.excludedEndpoints?.contains {
- $0.endpoint == endpoint.rawValue
- } ?? false
- }
- }
-
- func addExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
- context.performAndWait {
- let mapper = CoreDataMapper(context: context)
- let cdEndpoint = mapper.cdExcludedEndpoint(from: endpoint)
- cdEndpoint.providerPreferences = entity
- entity.excludedEndpoints?.insert(cdEndpoint)
- }
- }
-
- func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
- context.performAndWait {
- guard let found = entity.excludedEndpoints?.first(where: {
- $0.endpoint == endpoint.rawValue
- }) else {
- return
- }
- entity.excludedEndpoints?.remove(found)
- context.delete(found)
- }
- }
-
func save() throws {
try context.performAndWait {
guard context.hasChanges else {
diff --git a/Library/Sources/AppUIMain/Views/App/AppCoordinator.swift b/Library/Sources/AppUIMain/Views/App/AppCoordinator.swift
index cff37441..03edd4a2 100644
--- a/Library/Sources/AppUIMain/Views/App/AppCoordinator.swift
+++ b/Library/Sources/AppUIMain/Views/App/AppCoordinator.swift
@@ -34,9 +34,6 @@ public struct AppCoordinator: View, AppCoordinatorConforming, SizeClassProviding
@EnvironmentObject
public var iapManager: IAPManager
- @EnvironmentObject
- public var preferencesManager: PreferencesManager
-
@Environment(\.isUITesting)
private var isUITesting
diff --git a/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift b/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift
index 2f0aa232..ce436e0a 100644
--- a/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift
+++ b/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift
@@ -242,6 +242,7 @@ private extension OnDemandView {
module: $0,
parameters: .init(
editor: $1,
+ preferences: ModulePreferences(),
impl: nil
),
observer: MockWifi()
diff --git a/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift b/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift
index 37a6bdae..3380a8ed 100644
--- a/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift
+++ b/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift
@@ -30,9 +30,6 @@ import SwiftUI
struct OpenVPNView: View, ModuleDraftEditing {
- @EnvironmentObject
- private var preferencesManager: PreferencesManager
-
@Environment(\.navigationPath)
private var path
@@ -41,6 +38,9 @@ struct OpenVPNView: View, ModuleDraftEditing {
@ObservedObject
var editor: ProfileEditor
+ @ObservedObject
+ var modulePreferences: ModulePreferences
+
let impl: OpenVPNModule.Implementation?
private let isServerPushed: Bool
@@ -51,15 +51,13 @@ struct OpenVPNView: View, ModuleDraftEditing {
@State
private var paywallReason: PaywallReason?
- @StateObject
- private var providerPreferences = ProviderPreferences()
-
@StateObject
private var errorHandler: ErrorHandler = .default()
init(serverConfiguration: OpenVPN.Configuration) {
module = OpenVPNModule.Builder(configurationBuilder: serverConfiguration.builder())
editor = ProfileEditor(modules: [module])
+ modulePreferences = ModulePreferences()
assert(module.configurationBuilder != nil, "isServerPushed must imply module.configurationBuilder != nil")
impl = nil
isServerPushed = true
@@ -68,6 +66,7 @@ struct OpenVPNView: View, ModuleDraftEditing {
init(module: OpenVPNModule.Builder, parameters: ModuleViewParameters) {
self.module = module
editor = parameters.editor
+ modulePreferences = parameters.preferences
impl = parameters.impl as? OpenVPNModule.Implementation
isServerPushed = false
}
@@ -129,7 +128,7 @@ private extension OpenVPNView {
var providerModifier: some ViewModifier {
VPNProviderContentModifier(
providerId: providerId,
- providerPreferences: providerPreferences,
+ providerPreferences: nil,
selectedEntity: providerEntity,
paywallReason: $paywallReason,
entityDestination: Subroute.providerServer,
@@ -200,17 +199,32 @@ private extension OpenVPNView {
private extension OpenVPNView {
var excludedEndpoints: ObservableList {
- if draft.wrappedValue.providerSelection != nil {
- return providerPreferences.excludedEndpoints()
- } else {
- return editor.excludedEndpoints(for: module.id)
- }
+ editor.excludedEndpoints(for: module.id, preferences: modulePreferences)
}
func onSelectServer(server: VPNServer, preset: VPNPreset) {
draft.wrappedValue.providerEntity = VPNEntity(server: server, preset: preset)
+ resetExcludedEndpointsWithCurrentProviderEntity()
path.wrappedValue.removeLast()
}
+
+ // filter out exclusions unrelated to current server
+ func resetExcludedEndpointsWithCurrentProviderEntity() {
+ do {
+ let cfg = try draft.wrappedValue.providerSelection?.configuration()
+ editor.profile.attributes.editPreferences(inModule: module.id) {
+ if let cfg {
+ $0.excludedEndpoints = Set(cfg.remotes?.filter {
+ modulePreferences.isExcludedEndpoint($0)
+ } ?? [])
+ } else {
+ $0.excludedEndpoints = []
+ }
+ }
+ } catch {
+ pp_log(.app, .error, "Unable to build provider configuration for excluded endpoints: \(error)")
+ }
+ }
}
// MARK: - Previews
diff --git a/Library/Sources/AppUIMain/Views/Profile/ModuleDetailView.swift b/Library/Sources/AppUIMain/Views/Profile/ModuleDetailView.swift
index 062cb897..81b28413 100644
--- a/Library/Sources/AppUIMain/Views/Profile/ModuleDetailView.swift
+++ b/Library/Sources/AppUIMain/Views/Profile/ModuleDetailView.swift
@@ -28,12 +28,19 @@ 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 preferences = ModulePreferences()
+
var body: some View {
debugChanges()
return Group {
@@ -52,8 +59,24 @@ private extension ModuleDetailView {
func editorView(forModuleWithId moduleId: UUID) -> some View {
AnyView(moduleViewFactory.view(
with: profileEditor,
+ preferences: preferences,
moduleId: moduleId
))
+ .onLoad {
+ do {
+ let repository = try preferencesManager.preferencesRepository(forModuleWithId: moduleId)
+ preferences.setRepository(repository)
+ } catch {
+ pp_log(.app, .error, "Unable to load preferences for module \(moduleId): \(error)")
+ }
+ }
+ .onDisappear {
+ do {
+ try preferences.save()
+ } catch {
+ pp_log(.app, .error, "Unable to save preferences for module \(moduleId): \(error)")
+ }
+ }
}
var emptyView: some View {
diff --git a/Library/Sources/AppUIMain/Views/Profile/ProfileCoordinator.swift b/Library/Sources/AppUIMain/Views/Profile/ProfileCoordinator.swift
index 912c2861..d79aee7c 100644
--- a/Library/Sources/AppUIMain/Views/Profile/ProfileCoordinator.swift
+++ b/Library/Sources/AppUIMain/Views/Profile/ProfileCoordinator.swift
@@ -43,9 +43,6 @@ struct ProfileCoordinator: View {
@EnvironmentObject
private var iapManager: IAPManager
- @EnvironmentObject
- private var preferencesManager: PreferencesManager
-
let profileManager: ProfileManager
let profileEditor: ProfileEditor
diff --git a/Library/Sources/AppUIMain/Views/Providers/ProviderContentModifier.swift b/Library/Sources/AppUIMain/Views/Providers/ProviderContentModifier.swift
index c45407c9..f88923b1 100644
--- a/Library/Sources/AppUIMain/Views/Providers/ProviderContentModifier.swift
+++ b/Library/Sources/AppUIMain/Views/Providers/ProviderContentModifier.swift
@@ -188,13 +188,14 @@ private extension ProviderContentModifier {
if let providerId {
do {
pp_log(.app, .debug, "Load preferences for provider \(providerId)")
- providerPreferences.repository = try preferencesManager.preferencesRepository(forProviderWithId: providerId)
+ let repository = try preferencesManager.preferencesRepository(forProviderWithId: providerId)
+ providerPreferences.setRepository(repository)
} catch {
pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(error)")
- providerPreferences.repository = nil
+ providerPreferences.setRepository(nil)
}
} else {
- providerPreferences.repository = nil
+ providerPreferences.setRepository(nil)
}
}
diff --git a/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift b/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift
index 620e5d44..dad52cf5 100644
--- a/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift
+++ b/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift
@@ -164,7 +164,8 @@ private extension VPNProviderServerView {
private extension VPNProviderServerView {
func loadInitialServers() async {
do {
- providerPreferences.repository = try preferencesManager.preferencesRepository(forProviderWithId: providerId)
+ let repository = try preferencesManager.preferencesRepository(forProviderWithId: providerId)
+ providerPreferences.setRepository(repository)
} catch {
pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(error)")
}
diff --git a/Library/Sources/CommonLibrary/Business/ModulePreferences.swift b/Library/Sources/CommonLibrary/Business/ModulePreferences.swift
new file mode 100644
index 00000000..25642aff
--- /dev/null
+++ b/Library/Sources/CommonLibrary/Business/ModulePreferences.swift
@@ -0,0 +1,56 @@
+//
+// ModulePreferences.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 12/10/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 ModulePreferences: ObservableObject {
+ private var repository: ModulePreferencesRepository?
+
+ public init() {
+ }
+
+ public func setRepository(_ repository: ModulePreferencesRepository?) {
+ self.repository = repository
+ }
+
+ public func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool {
+ repository?.isExcludedEndpoint(endpoint) ?? false
+ }
+
+ public func addExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
+ repository?.addExcludedEndpoint(endpoint)
+ }
+
+ public func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
+ repository?.removeExcludedEndpoint(endpoint)
+ }
+
+ public func save() throws {
+ try repository?.save()
+ }
+}
diff --git a/Library/Sources/CommonLibrary/Business/PreferencesManager.swift b/Library/Sources/CommonLibrary/Business/PreferencesManager.swift
index a8cc2e49..ac8f662a 100644
--- a/Library/Sources/CommonLibrary/Business/PreferencesManager.swift
+++ b/Library/Sources/CommonLibrary/Business/PreferencesManager.swift
@@ -28,11 +28,17 @@ import Foundation
import PassepartoutKit
public final class PreferencesManager: ObservableObject, Sendable {
+ private let modulesFactory: @Sendable (UUID) throws -> ModulePreferencesRepository
+
private let providersFactory: @Sendable (ProviderID) throws -> ProviderPreferencesRepository
public init(
+ modulesFactory: (@Sendable (UUID) throws -> ModulePreferencesRepository)? = nil,
providersFactory: (@Sendable (ProviderID) throws -> ProviderPreferencesRepository)? = nil
) {
+ self.modulesFactory = modulesFactory ?? { _ in
+ DummyModulePreferencesRepository()
+ }
self.providersFactory = providersFactory ?? { _ in
DummyProviderPreferencesRepository()
}
@@ -40,25 +46,18 @@ public final class PreferencesManager: ObservableObject, Sendable {
}
extension PreferencesManager {
+ public func preferencesRepository(forModuleWithId moduleId: UUID) throws -> ModulePreferencesRepository {
+ try modulesFactory(moduleId)
+ }
+
public func preferencesRepository(forProviderWithId providerId: ProviderID) throws -> ProviderPreferencesRepository {
try providersFactory(providerId)
}
}
-@MainActor
-extension PreferencesManager {
- public func preferences(forProviderWithId providerId: ProviderID) throws -> ProviderPreferences {
- let object = ProviderPreferences()
- object.repository = try providersFactory(providerId)
- return object
- }
-}
-
// MARK: - Dummy
-private final class DummyProviderPreferencesRepository: ProviderPreferencesRepository {
- var favoriteServers: Set = []
-
+private final class DummyModulePreferencesRepository: ModulePreferencesRepository {
func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool {
false
}
@@ -72,3 +71,10 @@ private final class DummyProviderPreferencesRepository: ProviderPreferencesRepos
func save() throws {
}
}
+
+private final class DummyProviderPreferencesRepository: ProviderPreferencesRepository {
+ var favoriteServers: Set = []
+
+ func save() throws {
+ }
+}
diff --git a/Library/Sources/CommonLibrary/Domain/ProviderPreferences.swift b/Library/Sources/CommonLibrary/Business/ProviderPreferences.swift
similarity index 76%
rename from Library/Sources/CommonLibrary/Domain/ProviderPreferences.swift
rename to Library/Sources/CommonLibrary/Business/ProviderPreferences.swift
index 3afec295..707e2c6a 100644
--- a/Library/Sources/CommonLibrary/Domain/ProviderPreferences.swift
+++ b/Library/Sources/CommonLibrary/Business/ProviderPreferences.swift
@@ -29,11 +29,15 @@ import PassepartoutKit
@MainActor
public final class ProviderPreferences: ObservableObject {
- public var repository: ProviderPreferencesRepository?
+ private var repository: ProviderPreferencesRepository?
public init() {
}
+ public func setRepository(_ repository: ProviderPreferencesRepository?) {
+ self.repository = repository
+ }
+
public var favoriteServers: Set {
get {
repository?.favoriteServers ?? []
@@ -44,16 +48,6 @@ public final class ProviderPreferences: ObservableObject {
}
}
- public func excludedEndpoints() -> ObservableList {
- ObservableList { [weak self] in
- self?.repository?.isExcludedEndpoint($0) == true
- } add: { [weak self] in
- self?.repository?.addExcludedEndpoint($0)
- } remove: { [weak self] in
- self?.repository?.removeExcludedEndpoint($0)
- }
- }
-
public func save() throws {
try repository?.save()
}
diff --git a/Library/Sources/CommonLibrary/Domain/ProfileAttributes+ModulePreferences.swift b/Library/Sources/CommonLibrary/Domain/ProfileAttributes+ModulePreferences.swift
index a5b13842..6a8a0458 100644
--- a/Library/Sources/CommonLibrary/Domain/ProfileAttributes+ModulePreferences.swift
+++ b/Library/Sources/CommonLibrary/Domain/ProfileAttributes+ModulePreferences.swift
@@ -40,17 +40,26 @@ extension ProfileAttributes {
self.userInfo = userInfo ?? [:]
}
+ public var excludedEndpoints: Set {
+ get {
+ Set(rawExcludedEndpoints.compactMap(ExtendedEndpoint.init(rawValue:)))
+ }
+ set {
+ rawExcludedEndpoints = newValue.map(\.rawValue)
+ }
+ }
+
public func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool {
- excludedEndpoints.contains(endpoint.rawValue)
+ rawExcludedEndpoints.contains(endpoint.rawValue)
}
public mutating func addExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
- excludedEndpoints.append(endpoint.rawValue)
+ rawExcludedEndpoints.append(endpoint.rawValue)
}
public mutating func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
let rawValue = endpoint.rawValue
- excludedEndpoints.removeAll {
+ rawExcludedEndpoints.removeAll {
$0 == rawValue
}
}
@@ -58,7 +67,7 @@ extension ProfileAttributes {
}
extension ProfileAttributes.ModulePreferences {
- var excludedEndpoints: [String] {
+ var rawExcludedEndpoints: [String] {
get {
userInfo[Key.excludedEndpoints.rawValue] as? [String] ?? []
}
diff --git a/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift b/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift
new file mode 100644
index 00000000..a0481589
--- /dev/null
+++ b/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift
@@ -0,0 +1,37 @@
+//
+// ModulePreferencesRepository.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 12/10/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
+
+public protocol ModulePreferencesRepository {
+ func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool
+
+ func addExcludedEndpoint(_ endpoint: ExtendedEndpoint)
+
+ func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint)
+
+ func save() throws
+}
diff --git a/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift b/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift
index 88351944..477fd7f7 100644
--- a/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift
+++ b/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift
@@ -29,11 +29,5 @@ import PassepartoutKit
public protocol ProviderPreferencesRepository {
var favoriteServers: Set { get set }
- func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool
-
- func addExcludedEndpoint(_ endpoint: ExtendedEndpoint)
-
- func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint)
-
func save() throws
}
diff --git a/Library/Sources/UILibrary/Extensions/ModuleBuilder+Previews.swift b/Library/Sources/UILibrary/Extensions/ModuleBuilder+Previews.swift
index 9671b92a..5322d5c9 100644
--- a/Library/Sources/UILibrary/Extensions/ModuleBuilder+Previews.swift
+++ b/Library/Sources/UILibrary/Extensions/ModuleBuilder+Previews.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import CommonLibrary
import PassepartoutKit
import SwiftUI
@@ -33,6 +34,7 @@ extension ModuleBuilder where Self: ModuleViewProviding {
NavigationStack {
moduleView(with: .init(
editor: ProfileEditor(modules: [self]),
+ preferences: ModulePreferences(),
impl: nil
))
.navigationTitle(title)
diff --git a/Library/Sources/UILibrary/Extensions/ProfileEditor+UI.swift b/Library/Sources/UILibrary/Extensions/ProfileEditor+UI.swift
index ec704542..107abcb1 100644
--- a/Library/Sources/UILibrary/Extensions/ProfileEditor+UI.swift
+++ b/Library/Sources/UILibrary/Extensions/ProfileEditor+UI.swift
@@ -47,7 +47,7 @@ extension ProfileEditor {
// MARK: - ModulePreferences
extension ProfileEditor {
- public func excludedEndpoints(for moduleId: UUID) -> ObservableList {
+ public func excludedEndpoints(for moduleId: UUID, preferences: ModulePreferences) -> ObservableList {
ObservableList { [weak self] endpoint in
self?.profile.attributes.preference(inModule: moduleId) {
$0.isExcludedEndpoint(endpoint)
@@ -56,10 +56,12 @@ extension ProfileEditor {
self?.profile.attributes.editPreferences(inModule: moduleId) {
$0.addExcludedEndpoint(endpoint)
}
+ preferences.addExcludedEndpoint(endpoint)
} remove: { [weak self] endpoint in
self?.profile.attributes.editPreferences(inModule: moduleId) {
$0.removeExcludedEndpoint(endpoint)
}
+ preferences.removeExcludedEndpoint(endpoint)
}
}
}
diff --git a/Library/Sources/UILibrary/Strategy/ModuleViewFactory+Default.swift b/Library/Sources/UILibrary/Strategy/ModuleViewFactory+Default.swift
index df19d309..b41d7763 100644
--- a/Library/Sources/UILibrary/Strategy/ModuleViewFactory+Default.swift
+++ b/Library/Sources/UILibrary/Strategy/ModuleViewFactory+Default.swift
@@ -36,11 +36,12 @@ 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: .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 227f4b17..1b46d1ae 100644
--- a/Library/Sources/UILibrary/Strategy/ModuleViewFactory.swift
+++ b/Library/Sources/UILibrary/Strategy/ModuleViewFactory.swift
@@ -31,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 9b71dda0..e0d57862 100644
--- a/Library/Sources/UILibrary/Strategy/ModuleViewProviding.swift
+++ b/Library/Sources/UILibrary/Strategy/ModuleViewProviding.swift
@@ -37,14 +37,18 @@ public protocol ModuleViewProviding {
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
self.impl = impl
}
}
diff --git a/Library/Tests/CommonLibraryTests/Domain/ProfileAttributesTests.swift b/Library/Tests/CommonLibraryTests/Domain/ProfileAttributesTests.swift
index cf410513..20632f9a 100644
--- a/Library/Tests/CommonLibraryTests/Domain/ProfileAttributesTests.swift
+++ b/Library/Tests/CommonLibraryTests/Domain/ProfileAttributesTests.swift
@@ -72,7 +72,7 @@ final class ProfileAttributesTests: XCTestCase {
let excludedEndpoints: [String] = [
"1.1.1.1:UDP6:1000",
"2.2.2.2:TCP4:2000",
- "3.3.3.3:TCP:3000",
+ "3.3.3.3:TCP:3000"
]
let moduleUserInfo: [String: AnyHashable] = [
"excludedEndpoints": excludedEndpoints
@@ -89,7 +89,7 @@ final class ProfileAttributesTests: XCTestCase {
for moduleId in [moduleId1, moduleId2] {
let module = sut.preferences(inModule: moduleId)
XCTAssertEqual(module.userInfo, moduleUserInfo)
- XCTAssertEqual(module.excludedEndpoints, excludedEndpoints)
+ XCTAssertEqual(module.rawExcludedEndpoints, excludedEndpoints)
}
}
@@ -99,7 +99,7 @@ final class ProfileAttributesTests: XCTestCase {
let excludedEndpoints: [String] = [
"1.1.1.1:UDP6:1000",
"2.2.2.2:TCP4:2000",
- "3.3.3.3:TCP:3000",
+ "3.3.3.3:TCP:3000"
]
let moduleUserInfo: [String: AnyHashable] = [
"excludedEndpoints": excludedEndpoints
@@ -114,7 +114,7 @@ final class ProfileAttributesTests: XCTestCase {
var sut = ProfileAttributes(userInfo: nil)
for moduleId in [moduleId1, moduleId2] {
var module = sut.preferences(inModule: moduleId1)
- module.excludedEndpoints = excludedEndpoints
+ module.rawExcludedEndpoints = excludedEndpoints
XCTAssertEqual(module.userInfo, moduleUserInfo)
sut.setPreferences(module, inModule: moduleId)
}
diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj
index 1eb150f7..0b3fce12 100644
--- a/Passepartout.xcodeproj/project.pbxproj
+++ b/Passepartout.xcodeproj/project.pbxproj
@@ -36,8 +36,6 @@
0E8DFD532D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD4C2D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift */; };
0E8DFD542D05FE5A00531CDE /* Dependencies+IAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD4B2D05FE5A00531CDE /* Dependencies+IAPManager.swift */; };
0E8DFD562D05FE5A00531CDE /* Dependencies+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD492D05FE5A00531CDE /* Dependencies+CoreData.swift */; };
- 0E8DFD592D05FF0400531CDE /* Dependencies+PreferencesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD582D05FF0400531CDE /* Dependencies+PreferencesManager.swift */; };
- 0E8DFD5A2D05FF0400531CDE /* Dependencies+PreferencesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD582D05FF0400531CDE /* Dependencies+PreferencesManager.swift */; };
0E916B782CF80FD60072921A /* ProfileEditorScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E916B772CF80FD60072921A /* ProfileEditorScreen.swift */; };
0E916B7C2CF811EB0072921A /* XCUIElement+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E916B7B2CF811EB0072921A /* XCUIElement+Extensions.swift */; };
0E94EE582B93554B00588243 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7E3D672B9345FD002BBDB4 /* PacketTunnelProvider.swift */; };
@@ -170,7 +168,6 @@
0E8DFD4B2D05FE5A00531CDE /* Dependencies+IAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dependencies+IAPManager.swift"; sourceTree = ""; };
0E8DFD4C2D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dependencies+PassepartoutKit.swift"; sourceTree = ""; };
0E8DFD4D2D05FE5A00531CDE /* Dependencies+Processors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dependencies+Processors.swift"; sourceTree = ""; };
- 0E8DFD582D05FF0400531CDE /* Dependencies+PreferencesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dependencies+PreferencesManager.swift"; sourceTree = ""; };
0E916B772CF80FD60072921A /* ProfileEditorScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditorScreen.swift; sourceTree = ""; };
0E916B7B2CF811EB0072921A /* XCUIElement+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIElement+Extensions.swift"; sourceTree = ""; };
0E94EE5C2B93570600588243 /* Tunnel.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Tunnel.plist; sourceTree = ""; };
@@ -350,7 +347,6 @@
0E8DFD492D05FE5A00531CDE /* Dependencies+CoreData.swift */,
0E8DFD4B2D05FE5A00531CDE /* Dependencies+IAPManager.swift */,
0E8DFD4C2D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift */,
- 0E8DFD582D05FF0400531CDE /* Dependencies+PreferencesManager.swift */,
);
path = Shared;
sourceTree = "";
@@ -720,7 +716,6 @@
0E81955A2CFDA75200CC8FFD /* Dependencies.swift in Sources */,
0E6EEEE32CF8CABA0076E2B0 /* AppContext+Testing.swift in Sources */,
0E6EEEE42CF8CABA0076E2B0 /* ProfileManager+Testing.swift in Sources */,
- 0E8DFD592D05FF0400531CDE /* Dependencies+PreferencesManager.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -769,7 +764,6 @@
0E8DFD532D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift in Sources */,
0E8DFD542D05FE5A00531CDE /* Dependencies+IAPManager.swift in Sources */,
0E8DFD562D05FE5A00531CDE /* Dependencies+CoreData.swift in Sources */,
- 0E8DFD5A2D05FF0400531CDE /* Dependencies+PreferencesManager.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/Passepartout/App/Context/AppContext+Shared.swift b/Passepartout/App/Context/AppContext+Shared.swift
index 07ba7dcf..007ebc7c 100644
--- a/Passepartout/App/Context/AppContext+Shared.swift
+++ b/Passepartout/App/Context/AppContext+Shared.swift
@@ -24,6 +24,7 @@
//
import AppData
+import AppDataPreferences
import AppDataProfiles
import AppDataProviders
import CommonLibrary
@@ -122,7 +123,30 @@ extension AppContext {
return MigrationManager(profileStrategy: profileStrategy, simulation: migrationSimulation)
}()
- let preferencesManager = dependencies.preferencesManager(withCloudKit: true)
+ let preferencesManager: PreferencesManager = {
+ let preferencesStore = CoreDataPersistentStore(
+ logger: dependencies.coreDataLogger(),
+ containerName: Constants.shared.containers.preferences,
+ baseURL: BundleConfiguration.urlForGroupDocuments,
+ model: AppData.cdPreferencesModel,
+ cloudKitIdentifier: BundleConfiguration.mainString(for: .cloudKitPreferencesId),
+ author: nil
+ )
+ return PreferencesManager(
+ modulesFactory: {
+ try AppData.cdModulePreferencesRepositoryV3(
+ context: preferencesStore.context,
+ moduleId: $0
+ )
+ },
+ providersFactory: {
+ try AppData.cdProviderPreferencesRepositoryV3(
+ context: preferencesStore.context,
+ providerId: $0
+ )
+ }
+ )
+ }()
return AppContext(
iapManager: iapManager,
diff --git a/Passepartout/Shared/Dependencies+PreferencesManager.swift b/Passepartout/Shared/Dependencies+PreferencesManager.swift
deleted file mode 100644
index b4b8709c..00000000
--- a/Passepartout/Shared/Dependencies+PreferencesManager.swift
+++ /dev/null
@@ -1,52 +0,0 @@
-//
-// Dependencies+PreferencesManager.swift
-// Passepartout
-//
-// Created by Davide De Rosa on 12/2/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 AppDataPreferences
-import CommonLibrary
-import CommonUtils
-import Foundation
-import PassepartoutKit
-
-extension Dependencies {
- func preferencesManager(withCloudKit: Bool) -> PreferencesManager {
- let preferencesStore = CoreDataPersistentStore(
- logger: coreDataLogger(),
- containerName: Constants.shared.containers.preferences,
- baseURL: BundleConfiguration.urlForGroupDocuments,
- model: AppData.cdPreferencesModel,
- cloudKitIdentifier: withCloudKit ? BundleConfiguration.mainString(for: .cloudKitPreferencesId) : nil,
- author: nil
- )
- return PreferencesManager(
- providersFactory: {
- try AppData.cdProviderPreferencesRepositoryV3(
- context: preferencesStore.context,
- providerId: $0
- )
- }
- )
- }
-}
diff --git a/Passepartout/Tunnel/Context/DefaultTunnelProcessor.swift b/Passepartout/Tunnel/Context/DefaultTunnelProcessor.swift
index 7b80a578..64233577 100644
--- a/Passepartout/Tunnel/Context/DefaultTunnelProcessor.swift
+++ b/Passepartout/Tunnel/Context/DefaultTunnelProcessor.swift
@@ -28,10 +28,7 @@ import Foundation
import PassepartoutKit
final class DefaultTunnelProcessor: Sendable {
- private let preferencesManager: PreferencesManager
-
- init(preferencesManager: PreferencesManager) {
- self.preferencesManager = preferencesManager
+ init() {
}
}
@@ -49,13 +46,6 @@ extension DefaultTunnelProcessor: PacketTunnelProcessor {
preferences.isExcludedEndpoint($0)
}
- if let providerId = moduleBuilder.providerId {
- let providerPreferences = try preferencesManager.preferencesRepository(forProviderWithId: providerId)
- moduleBuilder.configurationBuilder?.remotes?.removeAll {
- providerPreferences.isExcludedEndpoint($0)
- }
- }
-
let module = try moduleBuilder.tryBuild()
builder.saveModule(module)
}
diff --git a/Passepartout/Tunnel/Context/TunnelContext+Shared.swift b/Passepartout/Tunnel/Context/TunnelContext+Shared.swift
index c67a3701..fada055d 100644
--- a/Passepartout/Tunnel/Context/TunnelContext+Shared.swift
+++ b/Passepartout/Tunnel/Context/TunnelContext+Shared.swift
@@ -37,10 +37,7 @@ extension TunnelContext {
betaChecker: dependencies.betaChecker(),
productsAtBuild: dependencies.productsAtBuild()
)
- let processor: PacketTunnelProcessor = {
- let preferencesManager = dependencies.preferencesManager(withCloudKit: false)
- return DefaultTunnelProcessor(preferencesManager: preferencesManager)
- }()
+ let processor = DefaultTunnelProcessor()
return TunnelContext(
iapManager: iapManager,
processor: processor