Handle load/save preferences inside ProfileEditor (#982)
Simplify preferences model by doing a bulk load/save together with load/save Profile. ModulePreferences is now a struct rather than an ObservableObject, because it doesn't need ad hoc observation. It's just a binding to ProfileEditor.preferences Fix: - Disable CloudKit in tunnel singleton of PreferencesManager (.sharedForTunnel) Additionally: - Replace MainActor in PreferencesManager with Sendable (immutable) - Replace MainActor from ProviderPreferencesRepository with Sendable (syncs on NSManagedObjectContext) - Drop ModuleMetadata for good
This commit is contained in:
parent
14847b2de5
commit
a4ebea1f95
|
@ -41,7 +41,7 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
|
||||
"state" : {
|
||||
"revision" : "d753c05f36b789fe413aeccbb543fb8c383ddc2b"
|
||||
"revision" : "406712a60faf8208a15c4ffaf286b1c71df7c6d2"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
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 {
|
||||
}
|
||||
}
|
|
@ -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,36 +42,49 @@ 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 entity = try request.execute().first ?? CDModulePreferencesV3(context: context)
|
||||
entity.uuid = moduleId
|
||||
return entity
|
||||
let preferences = try mapper.preferences(from: $1)
|
||||
$0[moduleId] = preferences
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to load preferences for module \(moduleId): \(error)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return CDModulePreferencesProxy(context: context, entity: entity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preference
|
||||
func set(_ preferences: [UUID: ModulePreferences]) throws {
|
||||
try context.performAndWait {
|
||||
let request = CDModulePreferencesV3.fetchRequest()
|
||||
request.predicate = NSPredicate(format: "any uuid in %@", Array(preferences.keys))
|
||||
|
||||
private final class CDModulePreferencesProxy: ModulePreferencesProxy {
|
||||
private let context: NSManagedObjectContext
|
||||
|
||||
private let entity: CDModulePreferencesV3
|
||||
|
||||
init(context: NSManagedObjectContext, entity: CDModulePreferencesV3) {
|
||||
self.context = context
|
||||
self.entity = entity
|
||||
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)
|
||||
}
|
||||
|
||||
func save() throws {
|
||||
guard context.hasChanges else {
|
||||
return
|
||||
}
|
||||
|
@ -87,3 +96,8 @@ private final class CDModulePreferencesProxy: ModulePreferencesProxy {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func rollback() {
|
||||
context.rollback()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -242,7 +242,6 @@ private extension OnDemandView {
|
|||
module: $0,
|
||||
parameters: .init(
|
||||
editor: $1,
|
||||
preferences: nil,
|
||||
impl: nil
|
||||
),
|
||||
observer: MockWifi()
|
||||
|
|
|
@ -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
|
||||
))
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ struct VPNProviderServerView<Configuration>: 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)")
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy {
|
||||
try modulesRepository.modulePreferencesProxy(in: moduleId)
|
||||
// MARK: - Modules
|
||||
|
||||
extension PreferencesManager {
|
||||
public func preferences(forProfile profile: Profile) throws -> [UUID: ModulePreferences] {
|
||||
try preferences(forModulesWithIds: profile.modules.map(\.id))
|
||||
}
|
||||
|
||||
public func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy {
|
||||
try providersRepository.providerPreferencesProxy(in: providerId)
|
||||
public func preferences(forProfile editableProfile: EditableProfile) throws -> [UUID: ModulePreferences] {
|
||||
try preferences(forModulesWithIds: editableProfile.modules.map(\.id))
|
||||
}
|
||||
|
||||
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<String> = []
|
||||
|
||||
func save() throws {
|
||||
}
|
||||
}
|
||||
|
||||
func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy {
|
||||
Proxy()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> {
|
||||
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<String> { get set }
|
||||
|
||||
func save() throws
|
||||
}
|
|
@ -35,8 +35,6 @@ public struct EditableProfile: MutableProfileType {
|
|||
|
||||
public var activeModulesIds: Set<UUID>
|
||||
|
||||
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<UUID> = [],
|
||||
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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -26,24 +26,7 @@
|
|||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
@MainActor
|
||||
public final class ModulePreferences: ObservableObject {
|
||||
public var proxy: ModulePreferencesProxy? {
|
||||
didSet {
|
||||
objectWillChange.send()
|
||||
public struct ModulePreferences: Sendable {
|
||||
public init() {
|
||||
}
|
||||
}
|
||||
|
||||
public init(proxy: ModulePreferencesProxy?) {
|
||||
self.proxy = proxy
|
||||
}
|
||||
|
||||
public func save() throws {
|
||||
try proxy?.save()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public protocol ModulePreferencesProxy {
|
||||
func save() throws
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
@MainActor
|
||||
public protocol ProviderPreferencesRepository {
|
||||
func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy
|
||||
var favoriteServers: Set<String> { get set }
|
||||
|
||||
func save() throws
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -33,7 +33,6 @@ extension ModuleBuilder where Self: ModuleViewProviding {
|
|||
NavigationStack {
|
||||
moduleView(with: .init(
|
||||
editor: ProfileEditor(modules: [self]),
|
||||
preferences: nil,
|
||||
impl: nil
|
||||
))
|
||||
.navigationTitle(title)
|
||||
|
|
|
@ -23,18 +23,11 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
extension ProfileEditor {
|
||||
public func binding(forNameOf moduleId: UUID) -> Binding<String> {
|
||||
Binding { [weak self] in
|
||||
self?.profile.name(forModuleWithId: moduleId) ?? ""
|
||||
} set: { [weak self] in
|
||||
self?.profile.setName($0, forModuleWithId: moduleId)
|
||||
}
|
||||
}
|
||||
|
||||
public subscript<T>(module: T) -> Binding<T> 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<ModulePreferences> {
|
||||
Binding { [weak self] in
|
||||
self?.preferences[moduleId] ?? ModulePreferences()
|
||||
} set: { [weak self] in
|
||||
self?.preferences[moduleId] = $0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -39,6 +39,10 @@ extension IAPManager {
|
|||
)
|
||||
}
|
||||
|
||||
extension PreferencesManager {
|
||||
static let sharedForApp = PreferencesManager.sharedImplementation(withCloudKit: true)
|
||||
}
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private extension Dependencies.IAPManager {
|
||||
|
|
|
@ -37,6 +37,10 @@ extension IAPManager {
|
|||
)
|
||||
}
|
||||
|
||||
extension PreferencesManager {
|
||||
static let sharedForTunnel = PreferencesManager.sharedImplementation(withCloudKit: false)
|
||||
}
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private extension Dependencies.IAPManager {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -40,7 +40,7 @@ extension AppContext {
|
|||
[]
|
||||
}
|
||||
)
|
||||
let processor = InAppProcessor.shared(iapManager) {
|
||||
let processor = InAppProcessor.sharedImplementation(with: iapManager) {
|
||||
$0.localizedPreview
|
||||
}
|
||||
|
||||
|
|
|
@ -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?")
|
||||
|
|
Loading…
Reference in New Issue