diff --git a/Library/Package.resolved b/Library/Package.resolved
index a0560756..2e9bc074 100644
--- a/Library/Package.resolved
+++ b/Library/Package.resolved
@@ -41,7 +41,7 @@
"kind" : "remoteSourceControl",
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
"state" : {
- "revision" : "d753c05f36b789fe413aeccbb543fb8c383ddc2b"
+ "revision" : "406712a60faf8208a15c4ffaf286b1c71df7c6d2"
}
},
{
diff --git a/Library/Package.swift b/Library/Package.swift
index 809dc11b..88dd3cda 100644
--- a/Library/Package.swift
+++ b/Library/Package.swift
@@ -65,7 +65,7 @@ let package = Package(
],
dependencies: [
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.12.0"),
- .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "d753c05f36b789fe413aeccbb543fb8c383ddc2b"),
+ .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "406712a60faf8208a15c4ffaf286b1c71df7c6d2"),
// .package(path: "../../passepartoutkit-source"),
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", from: "0.9.1"),
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", revision: "031863a1cd683962a7dfe68e20b91fa820a1ecce"),
diff --git a/Library/Sources/AppDataPreferences/Domain/Mapper.swift b/Library/Sources/AppDataPreferences/Domain/Mapper.swift
new file mode 100644
index 00000000..ccbb0979
--- /dev/null
+++ b/Library/Sources/AppDataPreferences/Domain/Mapper.swift
@@ -0,0 +1,40 @@
+//
+// Mapper.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 12/7/24.
+// Copyright (c) 2024 Davide De Rosa. All rights reserved.
+//
+// https://github.com/passepartoutvpn
+//
+// This file is part of Passepartout.
+//
+// Passepartout is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Passepartout is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Passepartout. If not, see .
+//
+
+import CommonLibrary
+import CoreData
+import Foundation
+import PassepartoutKit
+
+struct DomainMapper {
+ func preferences(from entity: CDModulePreferencesV3) throws -> ModulePreferences {
+ ModulePreferences()
+ }
+}
+
+struct CoreDataMapper {
+ func set(_ entity: CDModulePreferencesV3, from preferences: ModulePreferences) throws {
+ }
+}
diff --git a/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift b/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift
index 007fd49b..d279797f 100644
--- a/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift
+++ b/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift
@@ -30,15 +30,11 @@ import Foundation
import PassepartoutKit
extension AppData {
-
- @MainActor
public static func cdModulePreferencesRepositoryV3(context: NSManagedObjectContext) -> ModulePreferencesRepository {
CDModulePreferencesRepositoryV3(context: context)
}
}
-// MARK: - Repository
-
private final class CDModulePreferencesRepositoryV3: ModulePreferencesRepository {
private nonisolated let context: NSManagedObjectContext
@@ -46,44 +42,62 @@ private final class CDModulePreferencesRepositoryV3: ModulePreferencesRepository
self.context = context
}
- func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy {
- let entity = try context.performAndWait {
+ func preferences(for moduleIds: [UUID]) throws -> [UUID: ModulePreferences] {
+ try context.performAndWait {
let request = CDModulePreferencesV3.fetchRequest()
- request.predicate = NSPredicate(format: "uuid == %@", moduleId.uuidString)
+ request.predicate = NSPredicate(format: "any uuid in %@", moduleIds.map(\.uuidString))
+
+ let entities = try request.execute()
+ let mapper = DomainMapper()
+ return entities.reduce(into: [:]) {
+ guard let moduleId = $1.uuid else {
+ return
+ }
+ do {
+ let preferences = try mapper.preferences(from: $1)
+ $0[moduleId] = preferences
+ } catch {
+ pp_log(.app, .error, "Unable to load preferences for module \(moduleId): \(error)")
+ }
+ }
+ }
+ }
+
+ func set(_ preferences: [UUID: ModulePreferences]) throws {
+ try context.performAndWait {
+ let request = CDModulePreferencesV3.fetchRequest()
+ request.predicate = NSPredicate(format: "any uuid in %@", Array(preferences.keys))
+
+ var entities = try request.execute()
+ let existingIds = entities.compactMap(\.uuid)
+ let newIds = Set(preferences.keys).subtracting(existingIds)
+ newIds.forEach {
+ let newEntity = CDModulePreferencesV3(context: context)
+ newEntity.uuid = $0
+ entities.append(newEntity)
+ }
+
+ let mapper = CoreDataMapper()
+ try entities.forEach {
+ guard let id = $0.uuid, let entityPreferences = preferences[id] else {
+ return
+ }
+ try mapper.set($0, from: entityPreferences)
+ }
+
+ guard context.hasChanges else {
+ return
+ }
do {
- let entity = try request.execute().first ?? CDModulePreferencesV3(context: context)
- entity.uuid = moduleId
- return entity
+ try context.save()
} catch {
- pp_log(.app, .error, "Unable to load preferences for module \(moduleId): \(error)")
+ context.rollback()
throw error
}
}
- return CDModulePreferencesProxy(context: context, entity: entity)
- }
-}
-
-// MARK: - Preference
-
-private final class CDModulePreferencesProxy: ModulePreferencesProxy {
- private let context: NSManagedObjectContext
-
- private let entity: CDModulePreferencesV3
-
- init(context: NSManagedObjectContext, entity: CDModulePreferencesV3) {
- self.context = context
- self.entity = entity
- }
-
- func save() throws {
- guard context.hasChanges else {
- return
- }
- do {
- try context.save()
- } catch {
- context.rollback()
- throw error
- }
+ }
+
+ func rollback() {
+ context.rollback()
}
}
diff --git a/Library/Sources/AppDataPreferences/Strategy/CDProviderPreferencesRepositoryV3.swift b/Library/Sources/AppDataPreferences/Strategy/CDProviderPreferencesRepositoryV3.swift
index ace7bc1b..fd86e42e 100644
--- a/Library/Sources/AppDataPreferences/Strategy/CDProviderPreferencesRepositoryV3.swift
+++ b/Library/Sources/AppDataPreferences/Strategy/CDProviderPreferencesRepositoryV3.swift
@@ -30,24 +30,20 @@ import Foundation
import PassepartoutKit
extension AppData {
-
- @MainActor
- public static func cdProviderPreferencesRepositoryV3(context: NSManagedObjectContext) -> ProviderPreferencesRepository {
- CDProviderPreferencesRepositoryV3(context: context)
+ public static func cdProviderPreferencesRepositoryV3(context: NSManagedObjectContext, providerId: ProviderID) throws -> ProviderPreferencesRepository {
+ try CDProviderPreferencesRepositoryV3(context: context, providerId: providerId)
}
}
-// MARK: - Repository
-
private final class CDProviderPreferencesRepositoryV3: ProviderPreferencesRepository {
private nonisolated let context: NSManagedObjectContext
- init(context: NSManagedObjectContext) {
- self.context = context
- }
+ private let entity: CDProviderPreferencesV3
- func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy {
- let entity = try context.performAndWait {
+ init(context: NSManagedObjectContext, providerId: ProviderID) throws {
+ self.context = context
+
+ entity = try context.performAndWait {
let request = CDProviderPreferencesV3.fetchRequest()
request.predicate = NSPredicate(format: "providerId == %@", providerId.rawValue)
do {
@@ -59,20 +55,6 @@ private final class CDProviderPreferencesRepositoryV3: ProviderPreferencesReposi
throw error
}
}
- return CDProviderPreferencesProxy(context: context, entity: entity)
- }
-}
-
-// MARK: - Preference
-
-private final class CDProviderPreferencesProxy: ProviderPreferencesProxy {
- private let context: NSManagedObjectContext
-
- private let entity: CDProviderPreferencesV3
-
- init(context: NSManagedObjectContext, entity: CDProviderPreferencesV3) {
- self.context = context
- self.entity = entity
}
var favoriteServers: Set {
diff --git a/Library/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift b/Library/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift
index 0fff2ec9..14656166 100644
--- a/Library/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift
+++ b/Library/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift
@@ -36,7 +36,7 @@ extension AppData {
}
}
-private actor CDProviderRepositoryV3: NSObject, ProviderRepository {
+private final class CDProviderRepositoryV3: NSObject, ProviderRepository {
private nonisolated let context: NSManagedObjectContext
private nonisolated let providersSubject: CurrentValueSubject<[Provider], Never>
diff --git a/Library/Sources/AppUIMain/Views/App/AppCoordinator.swift b/Library/Sources/AppUIMain/Views/App/AppCoordinator.swift
index 453e64b4..192bfbc1 100644
--- a/Library/Sources/AppUIMain/Views/App/AppCoordinator.swift
+++ b/Library/Sources/AppUIMain/Views/App/AppCoordinator.swift
@@ -34,6 +34,9 @@ public struct AppCoordinator: View, AppCoordinatorConforming, SizeClassProviding
@EnvironmentObject
public var iapManager: IAPManager
+ @EnvironmentObject
+ public var preferencesManager: PreferencesManager
+
@Environment(\.isUITesting)
private var isUITesting
@@ -117,12 +120,7 @@ extension AppCoordinator {
isImporting: $isImporting,
errorHandler: errorHandler,
flow: .init(
- onEditProfile: {
- guard let profile = profileManager.profile(withId: $0.id) else {
- return
- }
- enterDetail(of: profile.editable(), initialModuleId: nil)
- },
+ onEditProfile: onEditProfile,
onMigrateProfiles: {
modalRoute = .migrateProfiles
},
@@ -168,7 +166,7 @@ extension AppCoordinator {
onMigrateProfiles: {
present(.migrateProfiles)
},
- onNewProfile: enterDetail
+ onNewProfile: onNewProfile
)
}
@@ -232,7 +230,11 @@ extension AppCoordinator {
extension AppCoordinator {
public func onInteractiveLogin(_ profile: Profile, _ onComplete: @escaping InteractiveManager.CompletionBlock) {
pp_log(.app, .info, "Present interactive login")
- interactiveManager.present(with: profile, onComplete: onComplete)
+ interactiveManager.present(
+ with: profile,
+ preferencesManager: preferencesManager,
+ onComplete: onComplete
+ )
}
public func onProviderEntityRequired(_ profile: Profile, force: Bool) {
@@ -287,10 +289,21 @@ private extension AppCoordinator {
}
}
- func enterDetail(of profile: EditableProfile, initialModuleId: UUID?) {
+ func onNewProfile(_ profile: EditableProfile, initialModuleId: UUID?) {
+ editProfile(profile, initialModuleId: initialModuleId)
+ }
+
+ func onEditProfile(_ preview: ProfilePreview) {
+ guard let profile = profileManager.profile(withId: preview.id) else {
+ return
+ }
+ editProfile(profile.editable(), initialModuleId: nil)
+ }
+
+ func editProfile(_ profile: EditableProfile, initialModuleId: UUID?) {
profilePath = NavigationPath()
let isShared = profileManager.isRemotelyShared(profileWithId: profile.id)
- profileEditor.editProfile(profile, isShared: isShared)
+ profileEditor.load(profile, isShared: isShared, preferencesManager: preferencesManager)
present(.editProfile(initialModuleId))
}
}
diff --git a/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift b/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift
index f027a843..2f0aa232 100644
--- a/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift
+++ b/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift
@@ -242,7 +242,6 @@ private extension OnDemandView {
module: $0,
parameters: .init(
editor: $1,
- preferences: nil,
impl: nil
),
observer: MockWifi()
diff --git a/Library/Sources/AppUIMain/Views/Profile/ModuleDetailView.swift b/Library/Sources/AppUIMain/Views/Profile/ModuleDetailView.swift
index d13fe879..062cb897 100644
--- a/Library/Sources/AppUIMain/Views/Profile/ModuleDetailView.swift
+++ b/Library/Sources/AppUIMain/Views/Profile/ModuleDetailView.swift
@@ -28,19 +28,12 @@ import PassepartoutKit
import SwiftUI
struct ModuleDetailView: View {
-
- @EnvironmentObject
- private var preferencesManager: PreferencesManager
-
let profileEditor: ProfileEditor
let moduleId: UUID?
let moduleViewFactory: any ModuleViewFactory
- @StateObject
- private var modulePreferences = ModulePreferences(proxy: nil)
-
var body: some View {
debugChanges()
return Group {
@@ -50,16 +43,6 @@ struct ModuleDetailView: View {
emptyView
}
}
- .onLoad {
- guard let moduleId else {
- return
- }
- do {
- modulePreferences.proxy = try preferencesManager.modulePreferencesProxy(in: moduleId)
- } catch {
- pp_log(.app, .error, "Unable to load module preferences: \(error)")
- }
- }
}
}
@@ -69,7 +52,6 @@ private extension ModuleDetailView {
func editorView(forModuleWithId moduleId: UUID) -> some View {
AnyView(moduleViewFactory.view(
with: profileEditor,
- preferences: modulePreferences,
moduleId: moduleId
))
}
diff --git a/Library/Sources/AppUIMain/Views/Profile/ProfileCoordinator.swift b/Library/Sources/AppUIMain/Views/Profile/ProfileCoordinator.swift
index 2ff12892..80a2c10c 100644
--- a/Library/Sources/AppUIMain/Views/Profile/ProfileCoordinator.swift
+++ b/Library/Sources/AppUIMain/Views/Profile/ProfileCoordinator.swift
@@ -43,6 +43,9 @@ struct ProfileCoordinator: View {
@EnvironmentObject
private var iapManager: IAPManager
+ @EnvironmentObject
+ private var preferencesManager: PreferencesManager
+
let profileManager: ProfileManager
let profileEditor: ProfileEditor
@@ -133,7 +136,10 @@ private extension ProfileCoordinator {
// standard: always save, warn if purchase required
func onCommitEditingStandard() async throws {
- let savedProfile = try await profileEditor.save(to: profileManager)
+ let savedProfile = try await profileEditor.save(
+ to: profileManager,
+ preferencesManager: preferencesManager
+ )
do {
try iapManager.verify(savedProfile)
} catch AppError.ineligibleProfile(let requiredFeatures) {
@@ -151,11 +157,15 @@ private extension ProfileCoordinator {
paywallReason = .init(requiredFeatures)
return
}
- try await profileEditor.save(to: profileManager)
+ try await profileEditor.save(
+ to: profileManager,
+ preferencesManager: preferencesManager
+ )
onDismiss()
}
func onCancelEditing() {
+ profileEditor.discard()
onDismiss()
}
}
diff --git a/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift b/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift
index c1a0b90f..75c05fa1 100644
--- a/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift
+++ b/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift
@@ -68,7 +68,7 @@ struct VPNProviderServerView: View where Configuration: Identifia
private var onlyShowsFavorites = false
@StateObject
- private var providerPreferences = ProviderPreferences(proxy: nil)
+ private var providerPreferences = ProviderPreferences()
@StateObject
private var filtersViewModel = VPNFiltersView.Model()
@@ -159,7 +159,7 @@ private extension VPNProviderServerView {
private extension VPNProviderServerView {
func loadInitialServers() async {
do {
- providerPreferences.proxy = try preferencesManager.providerPreferencesProxy(in: providerId)
+ providerPreferences.repository = try preferencesManager.preferencesRepository(forProviderWithId: providerId)
} catch {
pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(error)")
}
diff --git a/Library/Sources/AppUITV/Views/App/AppCoordinator.swift b/Library/Sources/AppUITV/Views/App/AppCoordinator.swift
index c9189652..28b76f90 100644
--- a/Library/Sources/AppUITV/Views/App/AppCoordinator.swift
+++ b/Library/Sources/AppUITV/Views/App/AppCoordinator.swift
@@ -34,6 +34,9 @@ public struct AppCoordinator: View, AppCoordinatorConforming {
@EnvironmentObject
public var iapManager: IAPManager
+ @EnvironmentObject
+ private var preferencesManager: PreferencesManager
+
private let profileManager: ProfileManager
public let tunnel: ExtendedTunnel
@@ -136,7 +139,11 @@ private extension AppCoordinator {
extension AppCoordinator {
public func onInteractiveLogin(_ profile: Profile, _ onComplete: @escaping InteractiveManager.CompletionBlock) {
pp_log(.app, .info, "Present interactive login")
- interactiveManager.present(with: profile, onComplete: onComplete)
+ interactiveManager.present(
+ with: profile,
+ preferencesManager: preferencesManager,
+ onComplete: onComplete
+ )
}
public func onProviderEntityRequired(_ profile: Profile, force: Bool) {
diff --git a/Library/Sources/CommonLibrary/Business/PreferencesManager.swift b/Library/Sources/CommonLibrary/Business/PreferencesManager.swift
index 8db9d4f5..479ed33f 100644
--- a/Library/Sources/CommonLibrary/Business/PreferencesManager.swift
+++ b/Library/Sources/CommonLibrary/Business/PreferencesManager.swift
@@ -27,51 +27,66 @@ import CommonUtils
import Foundation
import PassepartoutKit
-@MainActor
-public final class PreferencesManager: ObservableObject {
+public final class PreferencesManager: ObservableObject, Sendable {
private let modulesRepository: ModulePreferencesRepository
- private let providersRepository: ProviderPreferencesRepository
+ private let providersFactory: @Sendable (ProviderID) throws -> ProviderPreferencesRepository
public init(
modulesRepository: ModulePreferencesRepository? = nil,
- providersRepository: ProviderPreferencesRepository? = nil
+ providersFactory: (@Sendable (ProviderID) throws -> ProviderPreferencesRepository)? = nil
) {
self.modulesRepository = modulesRepository ?? DummyModulePreferencesRepository()
- self.providersRepository = providersRepository ?? DummyProviderPreferencesRepository()
+ self.providersFactory = providersFactory ?? { _ in
+ DummyProviderPreferencesRepository()
+ }
+ }
+}
+
+// MARK: - Modules
+
+extension PreferencesManager {
+ public func preferences(forProfile profile: Profile) throws -> [UUID: ModulePreferences] {
+ try preferences(forModulesWithIds: profile.modules.map(\.id))
}
- public func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy {
- try modulesRepository.modulePreferencesProxy(in: moduleId)
+ public func preferences(forProfile editableProfile: EditableProfile) throws -> [UUID: ModulePreferences] {
+ try preferences(forModulesWithIds: editableProfile.modules.map(\.id))
}
- public func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy {
- try providersRepository.providerPreferencesProxy(in: providerId)
+ public func savePreferences(_ preferences: [UUID: ModulePreferences]) throws {
+ try modulesRepository.set(preferences)
+ }
+}
+
+private extension PreferencesManager {
+ func preferences(forModulesWithIds moduleIds: [UUID]) throws -> [UUID: ModulePreferences] {
+ try modulesRepository.preferences(for: moduleIds)
+ }
+}
+
+// MARK: - Providers
+
+extension PreferencesManager {
+ public func preferencesRepository(forProviderWithId providerId: ProviderID) throws -> ProviderPreferencesRepository {
+ try providersFactory(providerId)
}
}
// MARK: - Dummy
private final class DummyModulePreferencesRepository: ModulePreferencesRepository {
- private final class Proxy: ModulePreferencesProxy {
- func save() throws {
- }
+ func preferences(for moduleIds: [UUID]) throws -> [UUID: ModulePreferences] {
+ [:]
}
- func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy {
- Proxy()
+ func set(_ preferences: [UUID: ModulePreferences]) throws {
}
}
private final class DummyProviderPreferencesRepository: ProviderPreferencesRepository {
- private final class Proxy: ProviderPreferencesProxy {
- var favoriteServers: Set = []
+ var favoriteServers: Set = []
- func save() throws {
- }
- }
-
- func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy {
- Proxy()
+ func save() throws {
}
}
diff --git a/Library/Sources/CommonLibrary/Domain/ProviderPreferences.swift b/Library/Sources/CommonLibrary/Business/ProviderPreferences.swift
similarity index 73%
rename from Library/Sources/CommonLibrary/Domain/ProviderPreferences.swift
rename to Library/Sources/CommonLibrary/Business/ProviderPreferences.swift
index 81f42bc8..a7e54a5b 100644
--- a/Library/Sources/CommonLibrary/Domain/ProviderPreferences.swift
+++ b/Library/Sources/CommonLibrary/Business/ProviderPreferences.swift
@@ -27,35 +27,27 @@ import Foundation
import PassepartoutKit
@MainActor
-public final class ProviderPreferences: ObservableObject {
- public var proxy: ProviderPreferencesProxy? {
+public final class ProviderPreferences: ObservableObject, ProviderPreferencesRepository {
+ public var repository: ProviderPreferencesRepository? {
didSet {
objectWillChange.send()
}
}
- public init(proxy: ProviderPreferencesProxy?) {
- self.proxy = proxy
+ public init() {
}
public var favoriteServers: Set {
get {
- proxy?.favoriteServers ?? []
+ repository?.favoriteServers ?? []
}
set {
objectWillChange.send()
- proxy?.favoriteServers = newValue
+ repository?.favoriteServers = newValue
}
}
public func save() throws {
- try proxy?.save()
+ try repository?.save()
}
}
-
-@MainActor
-public protocol ProviderPreferencesProxy {
- var favoriteServers: Set { get set }
-
- func save() throws
-}
diff --git a/Library/Sources/CommonLibrary/Domain/EditableProfile.swift b/Library/Sources/CommonLibrary/Domain/EditableProfile.swift
index ffd53dda..c4d5ce46 100644
--- a/Library/Sources/CommonLibrary/Domain/EditableProfile.swift
+++ b/Library/Sources/CommonLibrary/Domain/EditableProfile.swift
@@ -35,8 +35,6 @@ public struct EditableProfile: MutableProfileType {
public var activeModulesIds: Set
- public var modulesMetadata: [UUID: ModuleMetadata]?
-
public var userInfo: AnyHashable?
public init(
@@ -44,14 +42,12 @@ public struct EditableProfile: MutableProfileType {
name: String = "",
modules: [any ModuleBuilder] = [],
activeModulesIds: Set = [],
- modulesMetadata: [UUID: ModuleMetadata]? = nil,
userInfo: AnyHashable? = nil
) {
self.id = id
self.name = name
self.modules = modules
self.activeModulesIds = activeModulesIds
- self.modulesMetadata = modulesMetadata
self.userInfo = userInfo
}
@@ -71,16 +67,6 @@ public struct EditableProfile: MutableProfileType {
throw AppError.emptyProfileName
}
builder.name = trimmedName
-
- builder.modulesMetadata = modulesMetadata?.reduce(into: [:]) {
- var metadata = $1.value
- if var trimmedName = metadata.name {
- trimmedName = trimmedName.trimmingCharacters(in: .whitespaces)
- metadata.name = !trimmedName.isEmpty ? trimmedName : nil
- }
- $0[$1.key] = metadata
- }
-
builder.userInfo = userInfo
return builder
@@ -105,7 +91,6 @@ extension Profile {
name: name,
modules: modulesBuilders(),
activeModulesIds: activeModulesIds,
- modulesMetadata: modulesMetadata,
userInfo: userInfo
)
}
diff --git a/Library/Sources/CommonLibrary/Domain/ModulePreferences.swift b/Library/Sources/CommonLibrary/Domain/ModulePreferences.swift
index 5a5ad185..467be7d0 100644
--- a/Library/Sources/CommonLibrary/Domain/ModulePreferences.swift
+++ b/Library/Sources/CommonLibrary/Domain/ModulePreferences.swift
@@ -26,24 +26,7 @@
import Foundation
import PassepartoutKit
-@MainActor
-public final class ModulePreferences: ObservableObject {
- public var proxy: ModulePreferencesProxy? {
- didSet {
- objectWillChange.send()
- }
- }
-
- public init(proxy: ModulePreferencesProxy?) {
- self.proxy = proxy
- }
-
- public func save() throws {
- try proxy?.save()
+public struct ModulePreferences: Sendable {
+ public init() {
}
}
-
-@MainActor
-public protocol ModulePreferencesProxy {
- func save() throws
-}
diff --git a/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift b/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift
index 5f911a29..4b2787ad 100644
--- a/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift
+++ b/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift
@@ -24,8 +24,10 @@
//
import Foundation
+import PassepartoutKit
-@MainActor
-public protocol ModulePreferencesRepository {
- func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy
+public protocol ModulePreferencesRepository: Sendable {
+ func preferences(for moduleIds: [UUID]) throws -> [UUID: ModulePreferences]
+
+ func set(_ preferences: [UUID: ModulePreferences]) throws
}
diff --git a/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift b/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift
index 7454a77d..477fd7f7 100644
--- a/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift
+++ b/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift
@@ -26,7 +26,8 @@
import Foundation
import PassepartoutKit
-@MainActor
public protocol ProviderPreferencesRepository {
- func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy
+ var favoriteServers: Set { get set }
+
+ func save() throws
}
diff --git a/Library/Sources/CommonUtils/Business/CoreDataPersistentStore.swift b/Library/Sources/CommonUtils/Business/CoreDataPersistentStore.swift
index e746410d..ff74fd8a 100644
--- a/Library/Sources/CommonUtils/Business/CoreDataPersistentStore.swift
+++ b/Library/Sources/CommonUtils/Business/CoreDataPersistentStore.swift
@@ -28,13 +28,13 @@ import Combine
import CoreData
import Foundation
-public protocol CoreDataPersistentStoreLogger {
+public protocol CoreDataPersistentStoreLogger: Sendable {
func debug(_ msg: String)
func warning(_ msg: String)
}
-public final class CoreDataPersistentStore {
+public final class CoreDataPersistentStore: Sendable {
private let logger: CoreDataPersistentStoreLogger?
private let container: NSPersistentContainer
diff --git a/Library/Sources/UILibrary/Business/InteractiveManager.swift b/Library/Sources/UILibrary/Business/InteractiveManager.swift
index 05c02be1..89e23e3e 100644
--- a/Library/Sources/UILibrary/Business/InteractiveManager.swift
+++ b/Library/Sources/UILibrary/Business/InteractiveManager.swift
@@ -41,8 +41,9 @@ public final class InteractiveManager: ObservableObject {
public init() {
}
- public func present(with profile: Profile, onComplete: CompletionBlock?) {
- editor = ProfileEditor(profile: profile)
+ public func present(with profile: Profile, preferencesManager: PreferencesManager, onComplete: CompletionBlock?) {
+ editor = ProfileEditor()
+ editor.load(profile.editable(), isShared: false, preferencesManager: preferencesManager)
self.onComplete = onComplete
isPresented = true
}
diff --git a/Library/Sources/UILibrary/Business/ProfileEditor.swift b/Library/Sources/UILibrary/Business/ProfileEditor.swift
index a220274e..12429566 100644
--- a/Library/Sources/UILibrary/Business/ProfileEditor.swift
+++ b/Library/Sources/UILibrary/Business/ProfileEditor.swift
@@ -37,30 +37,31 @@ public final class ProfileEditor: ObservableObject {
@Published
public var isShared: Bool
+ @Published
+ public var preferences: [UUID: ModulePreferences]
+
private(set) var removedModules: [UUID: any ModuleBuilder]
public convenience init() {
self.init(modules: [])
}
+ // for testing/previews
+ public init(profile: Profile) {
+ editableProfile = profile.editable()
+ isShared = false
+ preferences = [:]
+ removedModules = [:]
+ }
+
+ // for testing/previews
public init(modules: [any ModuleBuilder]) {
editableProfile = EditableProfile(
modules: modules,
activeModulesIds: Set(modules.map(\.id))
)
isShared = false
- removedModules = [:]
- }
-
- public init(profile: Profile) {
- editableProfile = profile.editable()
- isShared = false
- removedModules = [:]
- }
-
- public func editProfile(_ profile: EditableProfile, isShared: Bool) {
- editableProfile = profile
- self.isShared = isShared
+ preferences = [:]
removedModules = [:]
}
}
@@ -198,21 +199,47 @@ extension ProfileEditor {
}
}
-// MARK: - Saving
+// MARK: - Load/Save
extension ProfileEditor {
+ public func load(
+ _ profile: EditableProfile,
+ isShared: Bool,
+ preferencesManager: PreferencesManager
+ ) {
+ editableProfile = profile
+ self.isShared = isShared
+ do {
+ preferences = try preferencesManager.preferences(forProfile: profile)
+ } catch {
+ preferences = [:]
+ pp_log(.app, .error, "Unable to load preferences for profile \(profile.id): \(error)")
+ }
+ removedModules = [:]
+ }
@discardableResult
- public func save(to profileManager: ProfileManager) async throws -> Profile {
+ public func save(
+ to profileManager: ProfileManager,
+ preferencesManager: PreferencesManager
+ ) async throws -> Profile {
do {
let newProfile = try build()
try await profileManager.save(newProfile, isLocal: true, remotelyShared: isShared)
+ do {
+ try preferencesManager.savePreferences(preferences)
+ } catch {
+ pp_log(.App.profiles, .error, "Unable to save preferences for profile \(profile.id): \(error)")
+ }
return newProfile
} catch {
pp_log(.app, .fault, "Unable to save edited profile: \(error)")
throw error
}
}
+
+ public func discard() {
+ }
}
// MARK: - Testing
diff --git a/Library/Sources/UILibrary/Extensions/ModuleBuilder+Previews.swift b/Library/Sources/UILibrary/Extensions/ModuleBuilder+Previews.swift
index 49610f19..9671b92a 100644
--- a/Library/Sources/UILibrary/Extensions/ModuleBuilder+Previews.swift
+++ b/Library/Sources/UILibrary/Extensions/ModuleBuilder+Previews.swift
@@ -33,7 +33,6 @@ extension ModuleBuilder where Self: ModuleViewProviding {
NavigationStack {
moduleView(with: .init(
editor: ProfileEditor(modules: [self]),
- preferences: nil,
impl: nil
))
.navigationTitle(title)
diff --git a/Library/Sources/UILibrary/Extensions/ProfileEditor+UI.swift b/Library/Sources/UILibrary/Extensions/ProfileEditor+UI.swift
index e82d71f7..7333d3bc 100644
--- a/Library/Sources/UILibrary/Extensions/ProfileEditor+UI.swift
+++ b/Library/Sources/UILibrary/Extensions/ProfileEditor+UI.swift
@@ -23,18 +23,11 @@
// along with Passepartout. If not, see .
//
+import CommonLibrary
import PassepartoutKit
import SwiftUI
extension ProfileEditor {
- public func binding(forNameOf moduleId: UUID) -> Binding {
- Binding { [weak self] in
- self?.profile.name(forModuleWithId: moduleId) ?? ""
- } set: { [weak self] in
- self?.profile.setName($0, forModuleWithId: moduleId)
- }
- }
-
public subscript(module: T) -> Binding where T: ModuleBuilder {
Binding { [weak self] in
guard let foundModule = self?.module(withId: module.id) else {
@@ -48,4 +41,12 @@ extension ProfileEditor {
self?.saveModule($0, activating: false)
}
}
+
+ public func binding(forPreferencesOf moduleId: UUID) -> Binding {
+ Binding { [weak self] in
+ self?.preferences[moduleId] ?? ModulePreferences()
+ } set: { [weak self] in
+ self?.preferences[moduleId] = $0
+ }
+ }
}
diff --git a/Library/Sources/UILibrary/Strategy/ModuleViewFactory+Default.swift b/Library/Sources/UILibrary/Strategy/ModuleViewFactory+Default.swift
index b41d7763..df19d309 100644
--- a/Library/Sources/UILibrary/Strategy/ModuleViewFactory+Default.swift
+++ b/Library/Sources/UILibrary/Strategy/ModuleViewFactory+Default.swift
@@ -36,12 +36,11 @@ public final class DefaultModuleViewFactory: ModuleViewFactory {
}
@ViewBuilder
- public func view(with editor: ProfileEditor, preferences: ModulePreferences, moduleId: UUID) -> some View {
+ public func view(with editor: ProfileEditor, 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 1b46d1ae..227f4b17 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, preferences: ModulePreferences, moduleId: UUID) -> Content
+ func view(with editor: ProfileEditor, moduleId: UUID) -> Content
}
diff --git a/Library/Sources/UILibrary/Strategy/ModuleViewProviding.swift b/Library/Sources/UILibrary/Strategy/ModuleViewProviding.swift
index bfc47208..9b71dda0 100644
--- a/Library/Sources/UILibrary/Strategy/ModuleViewProviding.swift
+++ b/Library/Sources/UILibrary/Strategy/ModuleViewProviding.swift
@@ -37,18 +37,14 @@ 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 ?? ModulePreferences(proxy: nil)
self.impl = impl
}
}
diff --git a/Library/Tests/UILibraryTests/ProfileEditorTests.swift b/Library/Tests/UILibraryTests/ProfileEditorTests.swift
index cd7a6cc9..15ce4182 100644
--- a/Library/Tests/UILibraryTests/ProfileEditorTests.swift
+++ b/Library/Tests/UILibraryTests/ProfileEditorTests.swift
@@ -251,7 +251,7 @@ extension ProfileEditorTests {
}
.store(in: &subscriptions)
- try await sut.save(to: manager)
+ try await sut.save(to: manager, preferencesManager: PreferencesManager())
await fulfillment(of: [exp])
}
}
diff --git a/Passepartout/Shared/AppContext+Shared.swift b/Passepartout/Shared/AppContext+Shared.swift
index fd2beef1..66b0cf8e 100644
--- a/Passepartout/Shared/AppContext+Shared.swift
+++ b/Passepartout/Shared/AppContext+Shared.swift
@@ -39,7 +39,7 @@ import UITesting
extension AppContext {
static let shared: AppContext = {
let iapManager: IAPManager = .sharedForApp
- let processor = InAppProcessor.shared(iapManager) {
+ let processor = InAppProcessor.sharedImplementation(with: iapManager) {
$0.localizedPreview
}
@@ -124,7 +124,7 @@ extension AppContext {
migrationManager: migrationManager,
profileManager: profileManager,
providerManager: providerManager,
- preferencesManager: .shared,
+ preferencesManager: .sharedForApp,
registry: .shared,
tunnel: tunnel,
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
diff --git a/Passepartout/Shared/Shared+App.swift b/Passepartout/Shared/Shared+App.swift
index e3f2d024..5720906c 100644
--- a/Passepartout/Shared/Shared+App.swift
+++ b/Passepartout/Shared/Shared+App.swift
@@ -39,6 +39,10 @@ extension IAPManager {
)
}
+extension PreferencesManager {
+ static let sharedForApp = PreferencesManager.sharedImplementation(withCloudKit: true)
+}
+
// MARK: - Dependencies
private extension Dependencies.IAPManager {
diff --git a/Passepartout/Shared/Shared+Tunnel.swift b/Passepartout/Shared/Shared+Tunnel.swift
index 8aaaa6ed..563e6847 100644
--- a/Passepartout/Shared/Shared+Tunnel.swift
+++ b/Passepartout/Shared/Shared+Tunnel.swift
@@ -37,6 +37,10 @@ extension IAPManager {
)
}
+extension PreferencesManager {
+ static let sharedForTunnel = PreferencesManager.sharedImplementation(withCloudKit: false)
+}
+
// MARK: - Dependencies
private extension Dependencies.IAPManager {
diff --git a/Passepartout/Shared/Shared.swift b/Passepartout/Shared/Shared.swift
index 002789b4..8e2e1c5b 100644
--- a/Passepartout/Shared/Shared.swift
+++ b/Passepartout/Shared/Shared.swift
@@ -92,7 +92,7 @@ extension TunnelEnvironment where Self == AppGroupEnvironment {
extension InAppProcessor {
@MainActor
- static func shared(_ iapManager: IAPManager, preview: @escaping (Profile) -> ProfilePreview) -> InAppProcessor {
+ static func sharedImplementation(with iapManager: IAPManager, preview: @escaping (Profile) -> ProfilePreview) -> InAppProcessor {
InAppProcessor(
iapManager: iapManager,
title: {
@@ -132,22 +132,24 @@ extension InAppProcessor {
}
extension PreferencesManager {
- static let shared: PreferencesManager = {
+
+ @MainActor
+ static func sharedImplementation(withCloudKit: Bool) -> PreferencesManager {
let preferencesStore = CoreDataPersistentStore(
logger: .default,
containerName: Constants.shared.containers.preferences,
baseURL: BundleConfiguration.urlForGroupDocuments,
model: AppData.cdPreferencesModel,
- cloudKitIdentifier: BundleConfiguration.mainString(for: .cloudKitPreferencesId),
+ cloudKitIdentifier: withCloudKit ? BundleConfiguration.mainString(for: .cloudKitPreferencesId) : nil,
author: nil
)
- let modulePreferencesRepository = AppData.cdModulePreferencesRepositoryV3(context: preferencesStore.context)
- let providerPreferencesRepository = AppData.cdProviderPreferencesRepositoryV3(context: preferencesStore.context)
return PreferencesManager(
- modulesRepository: modulePreferencesRepository,
- providersRepository: providerPreferencesRepository
+ modulesRepository: AppData.cdModulePreferencesRepositoryV3(context: preferencesStore.context),
+ providersFactory: {
+ try AppData.cdProviderPreferencesRepositoryV3(context: preferencesStore.context, providerId: $0)
+ }
)
- }()
+ }
}
// MARK: - Logging
diff --git a/Passepartout/Shared/Testing/AppContext+Testing.swift b/Passepartout/Shared/Testing/AppContext+Testing.swift
index 1452a7ae..33f4bc29 100644
--- a/Passepartout/Shared/Testing/AppContext+Testing.swift
+++ b/Passepartout/Shared/Testing/AppContext+Testing.swift
@@ -40,7 +40,7 @@ extension AppContext {
[]
}
)
- let processor = InAppProcessor.shared(iapManager) {
+ let processor = InAppProcessor.sharedImplementation(with: iapManager) {
$0.localizedPreview
}
diff --git a/Passepartout/Tunnel/PacketTunnelProvider.swift b/Passepartout/Tunnel/PacketTunnelProvider.swift
index 06441516..9a165977 100644
--- a/Passepartout/Tunnel/PacketTunnelProvider.swift
+++ b/Passepartout/Tunnel/PacketTunnelProvider.swift
@@ -41,10 +41,7 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
provider: self,
decoder: Registry.sharedProtocolCoder,
registry: .shared,
- environment: .shared,
- profileBlock: {
- $0
- }
+ environment: .shared
)
guard let fwd else {
fatalError("NEPTPForwarder nil without throwing error?")