diff --git a/Library/Sources/AppDataPreferences/Domain/CDExcludedEndpoint.swift b/Library/Sources/AppDataPreferences/Domain/CDExcludedEndpoint.swift
index adb04bb2..79c41a35 100644
--- a/Library/Sources/AppDataPreferences/Domain/CDExcludedEndpoint.swift
+++ b/Library/Sources/AppDataPreferences/Domain/CDExcludedEndpoint.swift
@@ -33,6 +33,5 @@ final class CDExcludedEndpoint: NSManagedObject {
}
@NSManaged var endpoint: String?
- @NSManaged var modulePreferences: CDModulePreferencesV3?
@NSManaged var providerPreferences: CDProviderPreferencesV3?
}
diff --git a/Library/Sources/AppDataPreferences/Domain/CDModulePreferencesV3.swift b/Library/Sources/AppDataPreferences/Domain/CDModulePreferencesV3.swift
deleted file mode 100644
index 833bfeb2..00000000
--- a/Library/Sources/AppDataPreferences/Domain/CDModulePreferencesV3.swift
+++ /dev/null
@@ -1,38 +0,0 @@
-//
-// 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?
- @NSManaged var lastUpdate: Date?
- @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 827465fd..a60d1ff3 100644
--- a/Library/Sources/AppDataPreferences/Preferences.xcdatamodeld/Preferences.xcdatamodel/contents
+++ b/Library/Sources/AppDataPreferences/Preferences.xcdatamodeld/Preferences.xcdatamodel/contents
@@ -2,14 +2,8 @@
-
-
-
-
-
-
diff --git a/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift b/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift
deleted file mode 100644
index 6a9deea2..00000000
--- a/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift
+++ /dev/null
@@ -1,121 +0,0 @@
-//
-// 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: "uuid == %@", 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.uuid = 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 {
- 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
- }
- }
- }
-
- func discard() {
- context.performAndWait {
- context.rollback()
- }
- }
-}
diff --git a/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift b/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift
index 44b4af4c..37a6bdae 100644
--- a/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift
+++ b/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift
@@ -51,9 +51,6 @@ struct OpenVPNView: View, ModuleDraftEditing {
@State
private var paywallReason: PaywallReason?
- @StateObject
- private var preferences = ModulePreferences()
-
@StateObject
private var providerPreferences = ProviderPreferences()
@@ -88,13 +85,6 @@ struct OpenVPNView: View, ModuleDraftEditing {
.navigationDestination(for: Subroute.self, destination: destination)
.themeAnimation(on: draft.wrappedValue.providerId, category: .modules)
.withErrorHandler(errorHandler)
- .onLoad {
- editor.loadPreferences(
- preferences,
- from: preferencesManager,
- forModuleWithId: module.id
- )
- }
}
}
@@ -213,7 +203,7 @@ private extension OpenVPNView {
if draft.wrappedValue.providerSelection != nil {
return providerPreferences.excludedEndpoints()
} else {
- return preferences.excludedEndpoints()
+ return editor.excludedEndpoints(for: module.id)
}
}
diff --git a/Library/Sources/CommonLibrary/Business/PreferencesManager.swift b/Library/Sources/CommonLibrary/Business/PreferencesManager.swift
index 53fcd228..a8cc2e49 100644
--- a/Library/Sources/CommonLibrary/Business/PreferencesManager.swift
+++ b/Library/Sources/CommonLibrary/Business/PreferencesManager.swift
@@ -28,17 +28,11 @@ 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()
}
@@ -46,10 +40,6 @@ 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)
}
@@ -57,12 +47,6 @@ extension PreferencesManager {
@MainActor
extension PreferencesManager {
- public func preferences(forModuleWithId moduleId: UUID) throws -> ModulePreferences {
- let object = ModulePreferences()
- object.repository = try modulesFactory(moduleId)
- return object
- }
-
public func preferences(forProviderWithId providerId: ProviderID) throws -> ProviderPreferences {
let object = ProviderPreferences()
object.repository = try providersFactory(providerId)
@@ -72,24 +56,6 @@ extension PreferencesManager {
// MARK: - Dummy
-private final class DummyModulePreferencesRepository: ModulePreferencesRepository {
- func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool {
- false
- }
-
- func addExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
- }
-
- func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
- }
-
- func save() throws {
- }
-
- func discard() {
- }
-}
-
private final class DummyProviderPreferencesRepository: ProviderPreferencesRepository {
var favoriteServers: Set = []
diff --git a/Library/Sources/CommonLibrary/Business/ProfileManager.swift b/Library/Sources/CommonLibrary/Business/ProfileManager.swift
index 732968c3..28db6f3e 100644
--- a/Library/Sources/CommonLibrary/Business/ProfileManager.swift
+++ b/Library/Sources/CommonLibrary/Business/ProfileManager.swift
@@ -440,15 +440,15 @@ private extension ProfileManager {
pp_log(.App.profiles, .info, "Start importing remote profiles: \(profiles.map(\.id))")
assert(profiles.count == Set(profiles.map(\.id)).count, "Remote repository must not have duplicates")
- pp_log(.App.profiles, .debug, "Local attributes:")
- let localAttributes: [Profile.ID: ProfileAttributes] = allProfiles.values.reduce(into: [:]) {
- $0[$1.id] = $1.attributes
- pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes)")
+ pp_log(.App.profiles, .debug, "Local fingerprints:")
+ let localFingerprints: [Profile.ID: UUID] = allProfiles.values.reduce(into: [:]) {
+ $0[$1.id] = $1.attributes.fingerprint
+ pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes.fingerprint.debugDescription)")
}
- pp_log(.App.profiles, .debug, "Remote attributes:")
- let remoteAttributes: [Profile.ID: ProfileAttributes] = profiles.reduce(into: [:]) {
- $0[$1.id] = $1.attributes
- pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes)")
+ pp_log(.App.profiles, .debug, "Remote fingerprints:")
+ let remoteFingerprints: [Profile.ID: UUID] = profiles.reduce(into: [:]) {
+ $0[$1.id] = $1.attributes.fingerprint
+ pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes.fingerprint.debugDescription)")
}
let remotelyDeletedIds = Set(allProfiles.keys).subtracting(Set(allRemoteProfiles.keys))
@@ -473,8 +473,8 @@ private extension ProfileManager {
idsToRemove.append(remoteProfile.id)
continue
}
- if let localFingerprint = localAttributes[remoteProfile.id]?.fingerprint {
- guard let remoteFingerprint = remoteAttributes[remoteProfile.id]?.fingerprint,
+ if let localFingerprint = localFingerprints[remoteProfile.id] {
+ guard let remoteFingerprint = remoteFingerprints[remoteProfile.id],
remoteFingerprint != localFingerprint else {
pp_log(.App.profiles, .info, "Skip re-importing local profile \(remoteProfile.id)")
continue
diff --git a/Library/Sources/CommonLibrary/Domain/EditableProfile.swift b/Library/Sources/CommonLibrary/Domain/EditableProfile.swift
index c4d5ce46..463ddfb3 100644
--- a/Library/Sources/CommonLibrary/Domain/EditableProfile.swift
+++ b/Library/Sources/CommonLibrary/Domain/EditableProfile.swift
@@ -73,17 +73,6 @@ public struct EditableProfile: MutableProfileType {
}
}
-extension EditableProfile {
- public var attributes: ProfileAttributes {
- get {
- userInfo() ?? ProfileAttributes()
- }
- set {
- setUserInfo(newValue)
- }
- }
-}
-
extension Profile {
public func editable() -> EditableProfile {
EditableProfile(
@@ -112,6 +101,8 @@ extension Module {
}
}
+// MARK: -
+
private extension EditableProfile {
var activeConnectionModule: (any ModuleBuilder)? {
modules.first {
diff --git a/Library/Sources/CommonLibrary/Domain/ModulePreferences.swift b/Library/Sources/CommonLibrary/Domain/ModulePreferences.swift
deleted file mode 100644
index 8186adc9..00000000
--- a/Library/Sources/CommonLibrary/Domain/ModulePreferences.swift
+++ /dev/null
@@ -1,46 +0,0 @@
-//
-// 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 CommonUtils
-import Foundation
-import PassepartoutKit
-
-@MainActor
-public final class ModulePreferences: ObservableObject {
- public var repository: ModulePreferencesRepository?
-
- public init() {
- }
-
- 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)
- }
- }
-}
diff --git a/Library/Sources/CommonLibrary/Domain/ProfileAttributes+ModulePreferences.swift b/Library/Sources/CommonLibrary/Domain/ProfileAttributes+ModulePreferences.swift
new file mode 100644
index 00000000..a5b13842
--- /dev/null
+++ b/Library/Sources/CommonLibrary/Domain/ProfileAttributes+ModulePreferences.swift
@@ -0,0 +1,69 @@
+//
+// ProfileAttributes+ModulePreferences.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 12/9/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 GenericJSON
+import PassepartoutKit
+
+extension ProfileAttributes {
+ public struct ModulePreferences {
+ private enum Key: String {
+ case excludedEndpoints
+ }
+
+ private(set) var userInfo: [String: AnyHashable]
+
+ init(userInfo: [String: AnyHashable]?) {
+ self.userInfo = userInfo ?? [:]
+ }
+
+ public func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool {
+ excludedEndpoints.contains(endpoint.rawValue)
+ }
+
+ public mutating func addExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
+ excludedEndpoints.append(endpoint.rawValue)
+ }
+
+ public mutating func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
+ let rawValue = endpoint.rawValue
+ excludedEndpoints.removeAll {
+ $0 == rawValue
+ }
+ }
+ }
+}
+
+extension ProfileAttributes.ModulePreferences {
+ var excludedEndpoints: [String] {
+ get {
+ userInfo[Key.excludedEndpoints.rawValue] as? [String] ?? []
+ }
+ set {
+ userInfo[Key.excludedEndpoints.rawValue] = newValue
+ }
+ }
+}
diff --git a/Library/Sources/CommonLibrary/Domain/ProfileAttributes.swift b/Library/Sources/CommonLibrary/Domain/ProfileAttributes.swift
index 61b6da66..e768576f 100644
--- a/Library/Sources/CommonLibrary/Domain/ProfileAttributes.swift
+++ b/Library/Sources/CommonLibrary/Domain/ProfileAttributes.swift
@@ -23,31 +23,122 @@
// along with Passepartout. If not, see .
//
-import CommonUtils
import Foundation
import PassepartoutKit
-public struct ProfileAttributes: Hashable, Codable {
- public var fingerprint: UUID?
+// WARNING: upcast to [String: AnyHashable] relies on CodableProfileCoder
+// implementation returning JSONSerialization
- public var lastUpdate: Date?
-
- public var isAvailableForTV: Bool?
-
- public init() {
- }
-
- public init(
- fingerprint: UUID?,
- lastUpdate: Date?,
- isAvailableForTV: Bool?
- ) {
- self.fingerprint = fingerprint
- self.lastUpdate = lastUpdate
- self.isAvailableForTV = isAvailableForTV
+extension ProfileType where UserInfoType == AnyHashable {
+ public var attributes: ProfileAttributes {
+ ProfileAttributes(userInfo: userInfo as? [String: AnyHashable])
}
}
+extension MutableProfileType where UserInfoType == AnyHashable {
+ public var attributes: ProfileAttributes {
+ get {
+ ProfileAttributes(userInfo: userInfo as? [String: AnyHashable])
+ }
+ set {
+ userInfo = newValue.userInfo
+ }
+ }
+}
+
+// MARK: - ProfileAttributes
+
+public struct ProfileAttributes {
+ fileprivate enum Key: String {
+ case fingerprint
+
+ case lastUpdate
+
+ case isAvailableForTV
+
+ case preferences
+ }
+
+ private(set) var userInfo: [String: AnyHashable]
+
+ init(userInfo: [String: AnyHashable]?) {
+ self.userInfo = userInfo ?? [:]
+ }
+}
+
+// MARK: Basic
+
+extension ProfileAttributes {
+ public var fingerprint: UUID? {
+ get {
+ guard let string = userInfo[Key.fingerprint.rawValue] as? String else {
+ return nil
+ }
+ return UUID(uuidString: string)
+ }
+ set {
+ userInfo[Key.fingerprint.rawValue] = newValue?.uuidString
+ }
+ }
+
+ public var lastUpdate: Date? {
+ get {
+ guard let interval = userInfo[Key.lastUpdate.rawValue] as? TimeInterval else {
+ return nil
+ }
+ return Date(timeIntervalSinceReferenceDate: interval)
+ }
+ set {
+ userInfo[Key.lastUpdate.rawValue] = newValue?.timeIntervalSinceReferenceDate
+ }
+ }
+
+ public var isAvailableForTV: Bool? {
+ get {
+ userInfo[Key.isAvailableForTV.rawValue] as? Bool
+ }
+ set {
+ userInfo[Key.isAvailableForTV.rawValue] = newValue
+ }
+ }
+}
+
+// MARK: Preferences
+
+extension ProfileAttributes {
+ public func preferences(inModule moduleId: UUID) -> ModulePreferences {
+ ModulePreferences(userInfo: allPreferences[moduleId.uuidString] as? [String: AnyHashable])
+ }
+
+ public mutating func setPreferences(_ module: ModulePreferences, inModule moduleId: UUID) {
+ allPreferences[moduleId.uuidString] = module.userInfo
+ }
+
+ public func preference(inModule moduleId: UUID, block: (ModulePreferences) -> T) -> T? {
+ let module = preferences(inModule: moduleId)
+ return block(module)
+ }
+
+ public mutating func editPreferences(inModule moduleId: UUID, block: (inout ModulePreferences) -> Void) {
+ var module = preferences(inModule: moduleId)
+ block(&module)
+ setPreferences(module, inModule: moduleId)
+ }
+}
+
+private extension ProfileAttributes {
+ var allPreferences: [String: AnyHashable] {
+ get {
+ userInfo[Key.preferences.rawValue] as? [String: AnyHashable] ?? [:]
+ }
+ set {
+ userInfo[Key.preferences.rawValue] = newValue
+ }
+ }
+}
+
+// MARK: -
+
extension ProfileAttributes: CustomDebugStringConvertible {
public var debugDescription: String {
let descs = [
@@ -59,50 +150,10 @@ extension ProfileAttributes: CustomDebugStringConvertible {
},
isAvailableForTV.map {
"isAvailableForTV: \($0)"
- }
+ },
+ "allPreferences: \(allPreferences)"
].compactMap { $0 }
return "{\(descs.joined(separator: ", "))}"
}
}
-
-// MARK: - UserInfoCodable
-
-extension ProfileAttributes: UserInfoCodable {
- public init?(userInfo: AnyHashable?) {
- do {
- let data = try JSONSerialization.data(withJSONObject: userInfo ?? [:])
- self = try JSONDecoder().decode(ProfileAttributes.self, from: data)
- } catch {
- pp_log(.App.profiles, .error, "Unable to decode ProfileAttributes from dictionary: \(error)")
- return nil
- }
- }
-
- public var userInfo: AnyHashable? {
- do {
- let data = try JSONEncoder().encode(self)
- return try JSONSerialization.jsonObject(with: data) as? AnyHashable
- } catch {
- pp_log(.App.profiles, .error, "Unable to encode ProfileAttributes to dictionary: \(error)")
- return nil
- }
- }
-}
-
-extension Profile {
- public var attributes: ProfileAttributes {
- userInfo() ?? ProfileAttributes()
- }
-}
-
-extension Profile.Builder {
- public var attributes: ProfileAttributes {
- get {
- userInfo() ?? ProfileAttributes()
- }
- set {
- setUserInfo(newValue)
- }
- }
-}
diff --git a/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift b/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift
deleted file mode 100644
index be704164..00000000
--- a/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift
+++ /dev/null
@@ -1,39 +0,0 @@
-//
-// 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
-import PassepartoutKit
-
-public protocol ModulePreferencesRepository {
- func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool
-
- func addExcludedEndpoint(_ endpoint: ExtendedEndpoint)
-
- func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint)
-
- func save() throws
-
- func discard()
-}
diff --git a/Library/Sources/UILibrary/Business/ProfileEditor.swift b/Library/Sources/UILibrary/Business/ProfileEditor.swift
index 30d5b52c..bf6e6383 100644
--- a/Library/Sources/UILibrary/Business/ProfileEditor.swift
+++ b/Library/Sources/UILibrary/Business/ProfileEditor.swift
@@ -37,9 +37,6 @@ public final class ProfileEditor: ObservableObject {
@Published
public var isShared: Bool
- @Published
- private var trackedPreferences: [UUID: ModulePreferencesRepository]
-
private(set) var removedModules: [UUID: any ModuleBuilder]
public convenience init() {
@@ -50,7 +47,6 @@ public final class ProfileEditor: ObservableObject {
public init(profile: Profile) {
editableProfile = profile.editable()
isShared = false
- trackedPreferences = [:]
removedModules = [:]
}
@@ -61,7 +57,6 @@ public final class ProfileEditor: ObservableObject {
activeModulesIds: Set(modules.map(\.id))
)
isShared = false
- trackedPreferences = [:]
removedModules = [:]
}
}
@@ -208,35 +203,11 @@ extension ProfileEditor {
removedModules = [:]
}
- public func loadPreferences(
- _ preferences: ModulePreferences,
- from manager: PreferencesManager,
- forModuleWithId moduleId: UUID
- ) {
- do {
- pp_log(.App.profiles, .debug, "Track preferences for module \(moduleId)")
- let repository = try trackedPreferences[moduleId] ?? manager.preferencesRepository(forModuleWithId: moduleId)
- preferences.repository = repository
- trackedPreferences[moduleId] = repository // @Published
- } catch {
- pp_log(.App.profiles, .error, "Unable to track preferences for module \(moduleId): \(error)")
- }
- }
-
@discardableResult
public func save(to profileManager: ProfileManager) async throws -> Profile {
do {
let newProfile = try build()
try await profileManager.save(newProfile, isLocal: true, remotelyShared: isShared)
- trackedPreferences.forEach {
- do {
- pp_log(.App.profiles, .debug, "Save tracked preferences for module \($0.key)")
- try $0.value.save()
- } catch {
- pp_log(.App.profiles, .error, "Unable to save preferences for profile \(profile.id): \(error)")
- }
- }
- trackedPreferences.removeAll()
return newProfile
} catch {
pp_log(.App.profiles, .fault, "Unable to save edited profile: \(error)")
@@ -245,11 +216,6 @@ extension ProfileEditor {
}
public func discard() {
- trackedPreferences.forEach {
- pp_log(.App.profiles, .debug, "Discard tracked preferences for module \($0.key)")
- $0.value.discard()
- }
- trackedPreferences.removeAll()
}
}
diff --git a/Library/Sources/UILibrary/Extensions/ProfileEditor+UI.swift b/Library/Sources/UILibrary/Extensions/ProfileEditor+UI.swift
index b9512617..ec704542 100644
--- a/Library/Sources/UILibrary/Extensions/ProfileEditor+UI.swift
+++ b/Library/Sources/UILibrary/Extensions/ProfileEditor+UI.swift
@@ -24,6 +24,7 @@
//
import CommonLibrary
+import CommonUtils
import PassepartoutKit
import SwiftUI
@@ -42,3 +43,23 @@ extension ProfileEditor {
}
}
}
+
+// MARK: - ModulePreferences
+
+extension ProfileEditor {
+ public func excludedEndpoints(for moduleId: UUID) -> ObservableList {
+ ObservableList { [weak self] endpoint in
+ self?.profile.attributes.preference(inModule: moduleId) {
+ $0.isExcludedEndpoint(endpoint)
+ } ?? false
+ } add: { [weak self] endpoint in
+ self?.profile.attributes.editPreferences(inModule: moduleId) {
+ $0.addExcludedEndpoint(endpoint)
+ }
+ } remove: { [weak self] endpoint in
+ self?.profile.attributes.editPreferences(inModule: moduleId) {
+ $0.removeExcludedEndpoint(endpoint)
+ }
+ }
+ }
+}
diff --git a/Library/Tests/CommonLibraryTests/ExtendedTunnelTests.swift b/Library/Tests/CommonLibraryTests/Business/ExtendedTunnelTests.swift
similarity index 100%
rename from Library/Tests/CommonLibraryTests/ExtendedTunnelTests.swift
rename to Library/Tests/CommonLibraryTests/Business/ExtendedTunnelTests.swift
diff --git a/Library/Tests/CommonLibraryTests/IAPManagerTests.swift b/Library/Tests/CommonLibraryTests/Business/IAPManagerTests.swift
similarity index 100%
rename from Library/Tests/CommonLibraryTests/IAPManagerTests.swift
rename to Library/Tests/CommonLibraryTests/Business/IAPManagerTests.swift
diff --git a/Library/Tests/CommonLibraryTests/MigrationManagerTests.swift b/Library/Tests/CommonLibraryTests/Business/MigrationManagerTests.swift
similarity index 100%
rename from Library/Tests/CommonLibraryTests/MigrationManagerTests.swift
rename to Library/Tests/CommonLibraryTests/Business/MigrationManagerTests.swift
diff --git a/Library/Tests/CommonLibraryTests/ProfileManagerTests.swift b/Library/Tests/CommonLibraryTests/Business/ProfileManagerTests.swift
similarity index 100%
rename from Library/Tests/CommonLibraryTests/ProfileManagerTests.swift
rename to Library/Tests/CommonLibraryTests/Business/ProfileManagerTests.swift
diff --git a/Library/Tests/CommonLibraryTests/Domain/ProfileAttributesTests.swift b/Library/Tests/CommonLibraryTests/Domain/ProfileAttributesTests.swift
new file mode 100644
index 00000000..cf410513
--- /dev/null
+++ b/Library/Tests/CommonLibraryTests/Domain/ProfileAttributesTests.swift
@@ -0,0 +1,123 @@
+//
+// ProfileAttributesTests.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 .
+//
+
+@testable import CommonLibrary
+import Foundation
+import PassepartoutKit
+import XCTest
+
+final class ProfileAttributesTests: XCTestCase {
+ func test_givenUserInfo_whenInit_thenReturnsAttributes() {
+ let fingerprint = UUID()
+ let lastUpdate = Date()
+ let isAvailableForTV = true
+ let userInfo: [String: AnyHashable] = [
+ "fingerprint": fingerprint.uuidString,
+ "lastUpdate": lastUpdate.timeIntervalSinceReferenceDate,
+ "isAvailableForTV": isAvailableForTV
+ ]
+
+ let sut = ProfileAttributes(userInfo: userInfo)
+ XCTAssertEqual(sut.userInfo, userInfo)
+ XCTAssertEqual(sut.fingerprint, fingerprint)
+ XCTAssertEqual(sut.lastUpdate, lastUpdate)
+ XCTAssertEqual(sut.isAvailableForTV, isAvailableForTV)
+ }
+
+ func test_givenUserInfo_whenSet_thenReturnsAttributes() {
+ let fingerprint = UUID()
+ let lastUpdate = Date()
+ let isAvailableForTV = true
+ let userInfo: [String: AnyHashable] = [
+ "fingerprint": fingerprint.uuidString,
+ "lastUpdate": lastUpdate.timeIntervalSinceReferenceDate,
+ "isAvailableForTV": isAvailableForTV
+ ]
+
+ var sut = ProfileAttributes(userInfo: nil)
+ sut.fingerprint = fingerprint
+ sut.lastUpdate = lastUpdate
+ sut.isAvailableForTV = isAvailableForTV
+ XCTAssertEqual(sut.userInfo, userInfo)
+ XCTAssertEqual(sut.fingerprint, fingerprint)
+ XCTAssertEqual(sut.lastUpdate, lastUpdate)
+ XCTAssertEqual(sut.isAvailableForTV, isAvailableForTV)
+ }
+
+ func test_givenUserInfo_whenInit_thenReturnsModulePreferences() {
+ let moduleId1 = UUID()
+ let moduleId2 = UUID()
+ let excludedEndpoints: [String] = [
+ "1.1.1.1:UDP6:1000",
+ "2.2.2.2:TCP4:2000",
+ "3.3.3.3:TCP:3000",
+ ]
+ let moduleUserInfo: [String: AnyHashable] = [
+ "excludedEndpoints": excludedEndpoints
+ ]
+ let userInfo: [String: AnyHashable] = [
+ "preferences": [
+ moduleId1.uuidString: moduleUserInfo,
+ moduleId2.uuidString: moduleUserInfo
+ ]
+ ]
+
+ let sut = ProfileAttributes(userInfo: userInfo)
+ XCTAssertEqual(sut.userInfo, userInfo)
+ for moduleId in [moduleId1, moduleId2] {
+ let module = sut.preferences(inModule: moduleId)
+ XCTAssertEqual(module.userInfo, moduleUserInfo)
+ XCTAssertEqual(module.excludedEndpoints, excludedEndpoints)
+ }
+ }
+
+ func test_givenUserInfo_whenSet_thenReturnsModulePreferences() {
+ let moduleId1 = UUID()
+ let moduleId2 = UUID()
+ let excludedEndpoints: [String] = [
+ "1.1.1.1:UDP6:1000",
+ "2.2.2.2:TCP4:2000",
+ "3.3.3.3:TCP:3000",
+ ]
+ let moduleUserInfo: [String: AnyHashable] = [
+ "excludedEndpoints": excludedEndpoints
+ ]
+ let userInfo: [String: AnyHashable] = [
+ "preferences": [
+ moduleId1.uuidString: moduleUserInfo,
+ moduleId2.uuidString: moduleUserInfo
+ ]
+ ]
+
+ var sut = ProfileAttributes(userInfo: nil)
+ for moduleId in [moduleId1, moduleId2] {
+ var module = sut.preferences(inModule: moduleId1)
+ module.excludedEndpoints = excludedEndpoints
+ XCTAssertEqual(module.userInfo, moduleUserInfo)
+ sut.setPreferences(module, inModule: moduleId)
+ }
+ XCTAssertEqual(sut.userInfo, userInfo)
+ }
+}
diff --git a/Passepartout/Shared/Dependencies+PreferencesManager.swift b/Passepartout/Shared/Dependencies+PreferencesManager.swift
index f64a3148..b4b8709c 100644
--- a/Passepartout/Shared/Dependencies+PreferencesManager.swift
+++ b/Passepartout/Shared/Dependencies+PreferencesManager.swift
@@ -41,15 +41,9 @@ extension Dependencies {
author: nil
)
return PreferencesManager(
- modulesFactory: {
- try AppData.cdModulePreferencesRepositoryV3(
- context: preferencesStore.backgroundContext(),
- moduleId: $0
- )
- },
providersFactory: {
try AppData.cdProviderPreferencesRepositoryV3(
- context: preferencesStore.backgroundContext(),
+ context: preferencesStore.context,
providerId: $0
)
}
diff --git a/Passepartout/Tunnel/Context/DefaultTunnelProcessor.swift b/Passepartout/Tunnel/Context/DefaultTunnelProcessor.swift
index 88939043..7b80a578 100644
--- a/Passepartout/Tunnel/Context/DefaultTunnelProcessor.swift
+++ b/Passepartout/Tunnel/Context/DefaultTunnelProcessor.swift
@@ -44,9 +44,9 @@ extension DefaultTunnelProcessor: PacketTunnelProcessor {
return
}
- let modulesPreferences = try preferencesManager.preferencesRepository(forModuleWithId: moduleBuilder.id)
+ let preferences = builder.attributes.preferences(inModule: moduleBuilder.id)
moduleBuilder.configurationBuilder?.remotes?.removeAll {
- modulesPreferences.isExcludedEndpoint($0)
+ preferences.isExcludedEndpoint($0)
}
if let providerId = moduleBuilder.providerId {