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 {