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",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
|
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "d753c05f36b789fe413aeccbb543fb8c383ddc2b"
|
"revision" : "406712a60faf8208a15c4ffaf286b1c71df7c6d2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -65,7 +65,7 @@ let package = Package(
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.12.0"),
|
// .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(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", from: "0.9.1"),
|
||||||
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", revision: "031863a1cd683962a7dfe68e20b91fa820a1ecce"),
|
// .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
|
import PassepartoutKit
|
||||||
|
|
||||||
extension AppData {
|
extension AppData {
|
||||||
|
|
||||||
@MainActor
|
|
||||||
public static func cdModulePreferencesRepositoryV3(context: NSManagedObjectContext) -> ModulePreferencesRepository {
|
public static func cdModulePreferencesRepositoryV3(context: NSManagedObjectContext) -> ModulePreferencesRepository {
|
||||||
CDModulePreferencesRepositoryV3(context: context)
|
CDModulePreferencesRepositoryV3(context: context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Repository
|
|
||||||
|
|
||||||
private final class CDModulePreferencesRepositoryV3: ModulePreferencesRepository {
|
private final class CDModulePreferencesRepositoryV3: ModulePreferencesRepository {
|
||||||
private nonisolated let context: NSManagedObjectContext
|
private nonisolated let context: NSManagedObjectContext
|
||||||
|
|
||||||
|
@ -46,36 +42,49 @@ private final class CDModulePreferencesRepositoryV3: ModulePreferencesRepository
|
||||||
self.context = context
|
self.context = context
|
||||||
}
|
}
|
||||||
|
|
||||||
func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy {
|
func preferences(for moduleIds: [UUID]) throws -> [UUID: ModulePreferences] {
|
||||||
let entity = try context.performAndWait {
|
try context.performAndWait {
|
||||||
let request = CDModulePreferencesV3.fetchRequest()
|
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 {
|
do {
|
||||||
let entity = try request.execute().first ?? CDModulePreferencesV3(context: context)
|
let preferences = try mapper.preferences(from: $1)
|
||||||
entity.uuid = moduleId
|
$0[moduleId] = preferences
|
||||||
return entity
|
|
||||||
} catch {
|
} catch {
|
||||||
pp_log(.app, .error, "Unable to load preferences for module \(moduleId): \(error)")
|
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 {
|
var entities = try request.execute()
|
||||||
private let context: NSManagedObjectContext
|
let existingIds = entities.compactMap(\.uuid)
|
||||||
|
let newIds = Set(preferences.keys).subtracting(existingIds)
|
||||||
private let entity: CDModulePreferencesV3
|
newIds.forEach {
|
||||||
|
let newEntity = CDModulePreferencesV3(context: context)
|
||||||
init(context: NSManagedObjectContext, entity: CDModulePreferencesV3) {
|
newEntity.uuid = $0
|
||||||
self.context = context
|
entities.append(newEntity)
|
||||||
self.entity = entity
|
}
|
||||||
|
|
||||||
|
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 {
|
guard context.hasChanges else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -87,3 +96,8 @@ private final class CDModulePreferencesProxy: ModulePreferencesProxy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rollback() {
|
||||||
|
context.rollback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -30,24 +30,20 @@ import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
|
|
||||||
extension AppData {
|
extension AppData {
|
||||||
|
public static func cdProviderPreferencesRepositoryV3(context: NSManagedObjectContext, providerId: ProviderID) throws -> ProviderPreferencesRepository {
|
||||||
@MainActor
|
try CDProviderPreferencesRepositoryV3(context: context, providerId: providerId)
|
||||||
public static func cdProviderPreferencesRepositoryV3(context: NSManagedObjectContext) -> ProviderPreferencesRepository {
|
|
||||||
CDProviderPreferencesRepositoryV3(context: context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Repository
|
|
||||||
|
|
||||||
private final class CDProviderPreferencesRepositoryV3: ProviderPreferencesRepository {
|
private final class CDProviderPreferencesRepositoryV3: ProviderPreferencesRepository {
|
||||||
private nonisolated let context: NSManagedObjectContext
|
private nonisolated let context: NSManagedObjectContext
|
||||||
|
|
||||||
init(context: NSManagedObjectContext) {
|
private let entity: CDProviderPreferencesV3
|
||||||
self.context = context
|
|
||||||
}
|
|
||||||
|
|
||||||
func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy {
|
init(context: NSManagedObjectContext, providerId: ProviderID) throws {
|
||||||
let entity = try context.performAndWait {
|
self.context = context
|
||||||
|
|
||||||
|
entity = try context.performAndWait {
|
||||||
let request = CDProviderPreferencesV3.fetchRequest()
|
let request = CDProviderPreferencesV3.fetchRequest()
|
||||||
request.predicate = NSPredicate(format: "providerId == %@", providerId.rawValue)
|
request.predicate = NSPredicate(format: "providerId == %@", providerId.rawValue)
|
||||||
do {
|
do {
|
||||||
|
@ -59,20 +55,6 @@ private final class CDProviderPreferencesRepositoryV3: ProviderPreferencesReposi
|
||||||
throw error
|
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> {
|
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 context: NSManagedObjectContext
|
||||||
|
|
||||||
private nonisolated let providersSubject: CurrentValueSubject<[Provider], Never>
|
private nonisolated let providersSubject: CurrentValueSubject<[Provider], Never>
|
||||||
|
|
|
@ -34,6 +34,9 @@ public struct AppCoordinator: View, AppCoordinatorConforming, SizeClassProviding
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
public var iapManager: IAPManager
|
public var iapManager: IAPManager
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
public var preferencesManager: PreferencesManager
|
||||||
|
|
||||||
@Environment(\.isUITesting)
|
@Environment(\.isUITesting)
|
||||||
private var isUITesting
|
private var isUITesting
|
||||||
|
|
||||||
|
@ -117,12 +120,7 @@ extension AppCoordinator {
|
||||||
isImporting: $isImporting,
|
isImporting: $isImporting,
|
||||||
errorHandler: errorHandler,
|
errorHandler: errorHandler,
|
||||||
flow: .init(
|
flow: .init(
|
||||||
onEditProfile: {
|
onEditProfile: onEditProfile,
|
||||||
guard let profile = profileManager.profile(withId: $0.id) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
enterDetail(of: profile.editable(), initialModuleId: nil)
|
|
||||||
},
|
|
||||||
onMigrateProfiles: {
|
onMigrateProfiles: {
|
||||||
modalRoute = .migrateProfiles
|
modalRoute = .migrateProfiles
|
||||||
},
|
},
|
||||||
|
@ -168,7 +166,7 @@ extension AppCoordinator {
|
||||||
onMigrateProfiles: {
|
onMigrateProfiles: {
|
||||||
present(.migrateProfiles)
|
present(.migrateProfiles)
|
||||||
},
|
},
|
||||||
onNewProfile: enterDetail
|
onNewProfile: onNewProfile
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,7 +230,11 @@ extension AppCoordinator {
|
||||||
extension AppCoordinator {
|
extension AppCoordinator {
|
||||||
public func onInteractiveLogin(_ profile: Profile, _ onComplete: @escaping InteractiveManager.CompletionBlock) {
|
public func onInteractiveLogin(_ profile: Profile, _ onComplete: @escaping InteractiveManager.CompletionBlock) {
|
||||||
pp_log(.app, .info, "Present interactive login")
|
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) {
|
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()
|
profilePath = NavigationPath()
|
||||||
let isShared = profileManager.isRemotelyShared(profileWithId: profile.id)
|
let isShared = profileManager.isRemotelyShared(profileWithId: profile.id)
|
||||||
profileEditor.editProfile(profile, isShared: isShared)
|
profileEditor.load(profile, isShared: isShared, preferencesManager: preferencesManager)
|
||||||
present(.editProfile(initialModuleId))
|
present(.editProfile(initialModuleId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -242,7 +242,6 @@ private extension OnDemandView {
|
||||||
module: $0,
|
module: $0,
|
||||||
parameters: .init(
|
parameters: .init(
|
||||||
editor: $1,
|
editor: $1,
|
||||||
preferences: nil,
|
|
||||||
impl: nil
|
impl: nil
|
||||||
),
|
),
|
||||||
observer: MockWifi()
|
observer: MockWifi()
|
||||||
|
|
|
@ -28,19 +28,12 @@ import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ModuleDetailView: View {
|
struct ModuleDetailView: View {
|
||||||
|
|
||||||
@EnvironmentObject
|
|
||||||
private var preferencesManager: PreferencesManager
|
|
||||||
|
|
||||||
let profileEditor: ProfileEditor
|
let profileEditor: ProfileEditor
|
||||||
|
|
||||||
let moduleId: UUID?
|
let moduleId: UUID?
|
||||||
|
|
||||||
let moduleViewFactory: any ModuleViewFactory
|
let moduleViewFactory: any ModuleViewFactory
|
||||||
|
|
||||||
@StateObject
|
|
||||||
private var modulePreferences = ModulePreferences(proxy: nil)
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
debugChanges()
|
debugChanges()
|
||||||
return Group {
|
return Group {
|
||||||
|
@ -50,16 +43,6 @@ struct ModuleDetailView: View {
|
||||||
emptyView
|
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 {
|
func editorView(forModuleWithId moduleId: UUID) -> some View {
|
||||||
AnyView(moduleViewFactory.view(
|
AnyView(moduleViewFactory.view(
|
||||||
with: profileEditor,
|
with: profileEditor,
|
||||||
preferences: modulePreferences,
|
|
||||||
moduleId: moduleId
|
moduleId: moduleId
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,9 @@ struct ProfileCoordinator: View {
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var iapManager: IAPManager
|
private var iapManager: IAPManager
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var preferencesManager: PreferencesManager
|
||||||
|
|
||||||
let profileManager: ProfileManager
|
let profileManager: ProfileManager
|
||||||
|
|
||||||
let profileEditor: ProfileEditor
|
let profileEditor: ProfileEditor
|
||||||
|
@ -133,7 +136,10 @@ private extension ProfileCoordinator {
|
||||||
|
|
||||||
// standard: always save, warn if purchase required
|
// standard: always save, warn if purchase required
|
||||||
func onCommitEditingStandard() async throws {
|
func onCommitEditingStandard() async throws {
|
||||||
let savedProfile = try await profileEditor.save(to: profileManager)
|
let savedProfile = try await profileEditor.save(
|
||||||
|
to: profileManager,
|
||||||
|
preferencesManager: preferencesManager
|
||||||
|
)
|
||||||
do {
|
do {
|
||||||
try iapManager.verify(savedProfile)
|
try iapManager.verify(savedProfile)
|
||||||
} catch AppError.ineligibleProfile(let requiredFeatures) {
|
} catch AppError.ineligibleProfile(let requiredFeatures) {
|
||||||
|
@ -151,11 +157,15 @@ private extension ProfileCoordinator {
|
||||||
paywallReason = .init(requiredFeatures)
|
paywallReason = .init(requiredFeatures)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try await profileEditor.save(to: profileManager)
|
try await profileEditor.save(
|
||||||
|
to: profileManager,
|
||||||
|
preferencesManager: preferencesManager
|
||||||
|
)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
func onCancelEditing() {
|
func onCancelEditing() {
|
||||||
|
profileEditor.discard()
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@ struct VPNProviderServerView<Configuration>: View where Configuration: Identifia
|
||||||
private var onlyShowsFavorites = false
|
private var onlyShowsFavorites = false
|
||||||
|
|
||||||
@StateObject
|
@StateObject
|
||||||
private var providerPreferences = ProviderPreferences(proxy: nil)
|
private var providerPreferences = ProviderPreferences()
|
||||||
|
|
||||||
@StateObject
|
@StateObject
|
||||||
private var filtersViewModel = VPNFiltersView.Model()
|
private var filtersViewModel = VPNFiltersView.Model()
|
||||||
|
@ -159,7 +159,7 @@ private extension VPNProviderServerView {
|
||||||
private extension VPNProviderServerView {
|
private extension VPNProviderServerView {
|
||||||
func loadInitialServers() async {
|
func loadInitialServers() async {
|
||||||
do {
|
do {
|
||||||
providerPreferences.proxy = try preferencesManager.providerPreferencesProxy(in: providerId)
|
providerPreferences.repository = try preferencesManager.preferencesRepository(forProviderWithId: providerId)
|
||||||
} catch {
|
} catch {
|
||||||
pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(error)")
|
pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(error)")
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,9 @@ public struct AppCoordinator: View, AppCoordinatorConforming {
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
public var iapManager: IAPManager
|
public var iapManager: IAPManager
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var preferencesManager: PreferencesManager
|
||||||
|
|
||||||
private let profileManager: ProfileManager
|
private let profileManager: ProfileManager
|
||||||
|
|
||||||
public let tunnel: ExtendedTunnel
|
public let tunnel: ExtendedTunnel
|
||||||
|
@ -136,7 +139,11 @@ private extension AppCoordinator {
|
||||||
extension AppCoordinator {
|
extension AppCoordinator {
|
||||||
public func onInteractiveLogin(_ profile: Profile, _ onComplete: @escaping InteractiveManager.CompletionBlock) {
|
public func onInteractiveLogin(_ profile: Profile, _ onComplete: @escaping InteractiveManager.CompletionBlock) {
|
||||||
pp_log(.app, .info, "Present interactive login")
|
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) {
|
public func onProviderEntityRequired(_ profile: Profile, force: Bool) {
|
||||||
|
|
|
@ -27,51 +27,66 @@ import CommonUtils
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
|
|
||||||
@MainActor
|
public final class PreferencesManager: ObservableObject, Sendable {
|
||||||
public final class PreferencesManager: ObservableObject {
|
|
||||||
private let modulesRepository: ModulePreferencesRepository
|
private let modulesRepository: ModulePreferencesRepository
|
||||||
|
|
||||||
private let providersRepository: ProviderPreferencesRepository
|
private let providersFactory: @Sendable (ProviderID) throws -> ProviderPreferencesRepository
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
modulesRepository: ModulePreferencesRepository? = nil,
|
modulesRepository: ModulePreferencesRepository? = nil,
|
||||||
providersRepository: ProviderPreferencesRepository? = nil
|
providersFactory: (@Sendable (ProviderID) throws -> ProviderPreferencesRepository)? = nil
|
||||||
) {
|
) {
|
||||||
self.modulesRepository = modulesRepository ?? DummyModulePreferencesRepository()
|
self.modulesRepository = modulesRepository ?? DummyModulePreferencesRepository()
|
||||||
self.providersRepository = providersRepository ?? DummyProviderPreferencesRepository()
|
self.providersFactory = providersFactory ?? { _ in
|
||||||
|
DummyProviderPreferencesRepository()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy {
|
// MARK: - Modules
|
||||||
try modulesRepository.modulePreferencesProxy(in: moduleId)
|
|
||||||
|
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 {
|
public func preferences(forProfile editableProfile: EditableProfile) throws -> [UUID: ModulePreferences] {
|
||||||
try providersRepository.providerPreferencesProxy(in: providerId)
|
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
|
// MARK: - Dummy
|
||||||
|
|
||||||
private final class DummyModulePreferencesRepository: ModulePreferencesRepository {
|
private final class DummyModulePreferencesRepository: ModulePreferencesRepository {
|
||||||
private final class Proxy: ModulePreferencesProxy {
|
func preferences(for moduleIds: [UUID]) throws -> [UUID: ModulePreferences] {
|
||||||
func save() throws {
|
[:]
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy {
|
func set(_ preferences: [UUID: ModulePreferences]) throws {
|
||||||
Proxy()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class DummyProviderPreferencesRepository: ProviderPreferencesRepository {
|
private final class DummyProviderPreferencesRepository: ProviderPreferencesRepository {
|
||||||
private final class Proxy: ProviderPreferencesProxy {
|
|
||||||
var favoriteServers: Set<String> = []
|
var favoriteServers: Set<String> = []
|
||||||
|
|
||||||
func save() throws {
|
func save() throws {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy {
|
|
||||||
Proxy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -27,35 +27,27 @@ import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public final class ProviderPreferences: ObservableObject {
|
public final class ProviderPreferences: ObservableObject, ProviderPreferencesRepository {
|
||||||
public var proxy: ProviderPreferencesProxy? {
|
public var repository: ProviderPreferencesRepository? {
|
||||||
didSet {
|
didSet {
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(proxy: ProviderPreferencesProxy?) {
|
public init() {
|
||||||
self.proxy = proxy
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public var favoriteServers: Set<String> {
|
public var favoriteServers: Set<String> {
|
||||||
get {
|
get {
|
||||||
proxy?.favoriteServers ?? []
|
repository?.favoriteServers ?? []
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
proxy?.favoriteServers = newValue
|
repository?.favoriteServers = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func save() throws {
|
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 activeModulesIds: Set<UUID>
|
||||||
|
|
||||||
public var modulesMetadata: [UUID: ModuleMetadata]?
|
|
||||||
|
|
||||||
public var userInfo: AnyHashable?
|
public var userInfo: AnyHashable?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
@ -44,14 +42,12 @@ public struct EditableProfile: MutableProfileType {
|
||||||
name: String = "",
|
name: String = "",
|
||||||
modules: [any ModuleBuilder] = [],
|
modules: [any ModuleBuilder] = [],
|
||||||
activeModulesIds: Set<UUID> = [],
|
activeModulesIds: Set<UUID> = [],
|
||||||
modulesMetadata: [UUID: ModuleMetadata]? = nil,
|
|
||||||
userInfo: AnyHashable? = nil
|
userInfo: AnyHashable? = nil
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.name = name
|
self.name = name
|
||||||
self.modules = modules
|
self.modules = modules
|
||||||
self.activeModulesIds = activeModulesIds
|
self.activeModulesIds = activeModulesIds
|
||||||
self.modulesMetadata = modulesMetadata
|
|
||||||
self.userInfo = userInfo
|
self.userInfo = userInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,16 +67,6 @@ public struct EditableProfile: MutableProfileType {
|
||||||
throw AppError.emptyProfileName
|
throw AppError.emptyProfileName
|
||||||
}
|
}
|
||||||
builder.name = trimmedName
|
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
|
builder.userInfo = userInfo
|
||||||
|
|
||||||
return builder
|
return builder
|
||||||
|
@ -105,7 +91,6 @@ extension Profile {
|
||||||
name: name,
|
name: name,
|
||||||
modules: modulesBuilders(),
|
modules: modulesBuilders(),
|
||||||
activeModulesIds: activeModulesIds,
|
activeModulesIds: activeModulesIds,
|
||||||
modulesMetadata: modulesMetadata,
|
|
||||||
userInfo: userInfo
|
userInfo: userInfo
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,24 +26,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
|
|
||||||
@MainActor
|
public struct ModulePreferences: Sendable {
|
||||||
public final class ModulePreferences: ObservableObject {
|
public init() {
|
||||||
public var proxy: ModulePreferencesProxy? {
|
|
||||||
didSet {
|
|
||||||
objectWillChange.send()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 Foundation
|
||||||
|
import PassepartoutKit
|
||||||
|
|
||||||
@MainActor
|
public protocol ModulePreferencesRepository: Sendable {
|
||||||
public protocol ModulePreferencesRepository {
|
func preferences(for moduleIds: [UUID]) throws -> [UUID: ModulePreferences]
|
||||||
func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy
|
|
||||||
|
func set(_ preferences: [UUID: ModulePreferences]) throws
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,8 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
|
|
||||||
@MainActor
|
|
||||||
public protocol ProviderPreferencesRepository {
|
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 CoreData
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public protocol CoreDataPersistentStoreLogger {
|
public protocol CoreDataPersistentStoreLogger: Sendable {
|
||||||
func debug(_ msg: String)
|
func debug(_ msg: String)
|
||||||
|
|
||||||
func warning(_ msg: String)
|
func warning(_ msg: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class CoreDataPersistentStore {
|
public final class CoreDataPersistentStore: Sendable {
|
||||||
private let logger: CoreDataPersistentStoreLogger?
|
private let logger: CoreDataPersistentStoreLogger?
|
||||||
|
|
||||||
private let container: NSPersistentContainer
|
private let container: NSPersistentContainer
|
||||||
|
|
|
@ -41,8 +41,9 @@ public final class InteractiveManager: ObservableObject {
|
||||||
public init() {
|
public init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func present(with profile: Profile, onComplete: CompletionBlock?) {
|
public func present(with profile: Profile, preferencesManager: PreferencesManager, onComplete: CompletionBlock?) {
|
||||||
editor = ProfileEditor(profile: profile)
|
editor = ProfileEditor()
|
||||||
|
editor.load(profile.editable(), isShared: false, preferencesManager: preferencesManager)
|
||||||
self.onComplete = onComplete
|
self.onComplete = onComplete
|
||||||
isPresented = true
|
isPresented = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,30 +37,31 @@ public final class ProfileEditor: ObservableObject {
|
||||||
@Published
|
@Published
|
||||||
public var isShared: Bool
|
public var isShared: Bool
|
||||||
|
|
||||||
|
@Published
|
||||||
|
public var preferences: [UUID: ModulePreferences]
|
||||||
|
|
||||||
private(set) var removedModules: [UUID: any ModuleBuilder]
|
private(set) var removedModules: [UUID: any ModuleBuilder]
|
||||||
|
|
||||||
public convenience init() {
|
public convenience init() {
|
||||||
self.init(modules: [])
|
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]) {
|
public init(modules: [any ModuleBuilder]) {
|
||||||
editableProfile = EditableProfile(
|
editableProfile = EditableProfile(
|
||||||
modules: modules,
|
modules: modules,
|
||||||
activeModulesIds: Set(modules.map(\.id))
|
activeModulesIds: Set(modules.map(\.id))
|
||||||
)
|
)
|
||||||
isShared = false
|
isShared = false
|
||||||
removedModules = [:]
|
preferences = [:]
|
||||||
}
|
|
||||||
|
|
||||||
public init(profile: Profile) {
|
|
||||||
editableProfile = profile.editable()
|
|
||||||
isShared = false
|
|
||||||
removedModules = [:]
|
|
||||||
}
|
|
||||||
|
|
||||||
public func editProfile(_ profile: EditableProfile, isShared: Bool) {
|
|
||||||
editableProfile = profile
|
|
||||||
self.isShared = isShared
|
|
||||||
removedModules = [:]
|
removedModules = [:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,21 +199,47 @@ extension ProfileEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Saving
|
// MARK: - Load/Save
|
||||||
|
|
||||||
extension ProfileEditor {
|
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
|
@discardableResult
|
||||||
public func save(to profileManager: ProfileManager) async throws -> Profile {
|
public func save(
|
||||||
|
to profileManager: ProfileManager,
|
||||||
|
preferencesManager: PreferencesManager
|
||||||
|
) async throws -> Profile {
|
||||||
do {
|
do {
|
||||||
let newProfile = try build()
|
let newProfile = try build()
|
||||||
try await profileManager.save(newProfile, isLocal: true, remotelyShared: isShared)
|
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
|
return newProfile
|
||||||
} catch {
|
} catch {
|
||||||
pp_log(.app, .fault, "Unable to save edited profile: \(error)")
|
pp_log(.app, .fault, "Unable to save edited profile: \(error)")
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func discard() {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Testing
|
// MARK: - Testing
|
||||||
|
|
|
@ -33,7 +33,6 @@ extension ModuleBuilder where Self: ModuleViewProviding {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
moduleView(with: .init(
|
moduleView(with: .init(
|
||||||
editor: ProfileEditor(modules: [self]),
|
editor: ProfileEditor(modules: [self]),
|
||||||
preferences: nil,
|
|
||||||
impl: nil
|
impl: nil
|
||||||
))
|
))
|
||||||
.navigationTitle(title)
|
.navigationTitle(title)
|
||||||
|
|
|
@ -23,18 +23,11 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CommonLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
extension ProfileEditor {
|
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 {
|
public subscript<T>(module: T) -> Binding<T> where T: ModuleBuilder {
|
||||||
Binding { [weak self] in
|
Binding { [weak self] in
|
||||||
guard let foundModule = self?.module(withId: module.id) else {
|
guard let foundModule = self?.module(withId: module.id) else {
|
||||||
|
@ -48,4 +41,12 @@ extension ProfileEditor {
|
||||||
self?.saveModule($0, activating: false)
|
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
|
@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)
|
let result = editor.moduleViewProvider(withId: moduleId, registry: registry)
|
||||||
if let result {
|
if let result {
|
||||||
AnyView(result.provider.moduleView(with: .init(
|
AnyView(result.provider.moduleView(with: .init(
|
||||||
editor: editor,
|
editor: editor,
|
||||||
preferences: preferences,
|
|
||||||
impl: result.impl
|
impl: result.impl
|
||||||
)))
|
)))
|
||||||
.navigationTitle(result.title)
|
.navigationTitle(result.title)
|
||||||
|
|
|
@ -31,5 +31,5 @@ public protocol ModuleViewFactory: AnyObject {
|
||||||
associatedtype Content: View
|
associatedtype Content: View
|
||||||
|
|
||||||
@MainActor
|
@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 struct ModuleViewParameters {
|
||||||
public let editor: ProfileEditor
|
public let editor: ProfileEditor
|
||||||
|
|
||||||
public let preferences: ModulePreferences
|
|
||||||
|
|
||||||
public let impl: (any ModuleImplementation)?
|
public let impl: (any ModuleImplementation)?
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public init(
|
public init(
|
||||||
editor: ProfileEditor,
|
editor: ProfileEditor,
|
||||||
preferences: ModulePreferences?,
|
|
||||||
impl: (any ModuleImplementation)?
|
impl: (any ModuleImplementation)?
|
||||||
) {
|
) {
|
||||||
self.editor = editor
|
self.editor = editor
|
||||||
self.preferences = preferences ?? ModulePreferences(proxy: nil)
|
|
||||||
self.impl = impl
|
self.impl = impl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -251,7 +251,7 @@ extension ProfileEditorTests {
|
||||||
}
|
}
|
||||||
.store(in: &subscriptions)
|
.store(in: &subscriptions)
|
||||||
|
|
||||||
try await sut.save(to: manager)
|
try await sut.save(to: manager, preferencesManager: PreferencesManager())
|
||||||
await fulfillment(of: [exp])
|
await fulfillment(of: [exp])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ import UITesting
|
||||||
extension AppContext {
|
extension AppContext {
|
||||||
static let shared: AppContext = {
|
static let shared: AppContext = {
|
||||||
let iapManager: IAPManager = .sharedForApp
|
let iapManager: IAPManager = .sharedForApp
|
||||||
let processor = InAppProcessor.shared(iapManager) {
|
let processor = InAppProcessor.sharedImplementation(with: iapManager) {
|
||||||
$0.localizedPreview
|
$0.localizedPreview
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ extension AppContext {
|
||||||
migrationManager: migrationManager,
|
migrationManager: migrationManager,
|
||||||
profileManager: profileManager,
|
profileManager: profileManager,
|
||||||
providerManager: providerManager,
|
providerManager: providerManager,
|
||||||
preferencesManager: .shared,
|
preferencesManager: .sharedForApp,
|
||||||
registry: .shared,
|
registry: .shared,
|
||||||
tunnel: tunnel,
|
tunnel: tunnel,
|
||||||
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
|
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
|
||||||
|
|
|
@ -39,6 +39,10 @@ extension IAPManager {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension PreferencesManager {
|
||||||
|
static let sharedForApp = PreferencesManager.sharedImplementation(withCloudKit: true)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Dependencies
|
// MARK: - Dependencies
|
||||||
|
|
||||||
private extension Dependencies.IAPManager {
|
private extension Dependencies.IAPManager {
|
||||||
|
|
|
@ -37,6 +37,10 @@ extension IAPManager {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension PreferencesManager {
|
||||||
|
static let sharedForTunnel = PreferencesManager.sharedImplementation(withCloudKit: false)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Dependencies
|
// MARK: - Dependencies
|
||||||
|
|
||||||
private extension Dependencies.IAPManager {
|
private extension Dependencies.IAPManager {
|
||||||
|
|
|
@ -92,7 +92,7 @@ extension TunnelEnvironment where Self == AppGroupEnvironment {
|
||||||
extension InAppProcessor {
|
extension InAppProcessor {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
static func shared(_ iapManager: IAPManager, preview: @escaping (Profile) -> ProfilePreview) -> InAppProcessor {
|
static func sharedImplementation(with iapManager: IAPManager, preview: @escaping (Profile) -> ProfilePreview) -> InAppProcessor {
|
||||||
InAppProcessor(
|
InAppProcessor(
|
||||||
iapManager: iapManager,
|
iapManager: iapManager,
|
||||||
title: {
|
title: {
|
||||||
|
@ -132,22 +132,24 @@ extension InAppProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PreferencesManager {
|
extension PreferencesManager {
|
||||||
static let shared: PreferencesManager = {
|
|
||||||
|
@MainActor
|
||||||
|
static func sharedImplementation(withCloudKit: Bool) -> PreferencesManager {
|
||||||
let preferencesStore = CoreDataPersistentStore(
|
let preferencesStore = CoreDataPersistentStore(
|
||||||
logger: .default,
|
logger: .default,
|
||||||
containerName: Constants.shared.containers.preferences,
|
containerName: Constants.shared.containers.preferences,
|
||||||
baseURL: BundleConfiguration.urlForGroupDocuments,
|
baseURL: BundleConfiguration.urlForGroupDocuments,
|
||||||
model: AppData.cdPreferencesModel,
|
model: AppData.cdPreferencesModel,
|
||||||
cloudKitIdentifier: BundleConfiguration.mainString(for: .cloudKitPreferencesId),
|
cloudKitIdentifier: withCloudKit ? BundleConfiguration.mainString(for: .cloudKitPreferencesId) : nil,
|
||||||
author: nil
|
author: nil
|
||||||
)
|
)
|
||||||
let modulePreferencesRepository = AppData.cdModulePreferencesRepositoryV3(context: preferencesStore.context)
|
|
||||||
let providerPreferencesRepository = AppData.cdProviderPreferencesRepositoryV3(context: preferencesStore.context)
|
|
||||||
return PreferencesManager(
|
return PreferencesManager(
|
||||||
modulesRepository: modulePreferencesRepository,
|
modulesRepository: AppData.cdModulePreferencesRepositoryV3(context: preferencesStore.context),
|
||||||
providersRepository: providerPreferencesRepository
|
providersFactory: {
|
||||||
|
try AppData.cdProviderPreferencesRepositoryV3(context: preferencesStore.context, providerId: $0)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Logging
|
// MARK: - Logging
|
||||||
|
|
|
@ -40,7 +40,7 @@ extension AppContext {
|
||||||
[]
|
[]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
let processor = InAppProcessor.shared(iapManager) {
|
let processor = InAppProcessor.sharedImplementation(with: iapManager) {
|
||||||
$0.localizedPreview
|
$0.localizedPreview
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,10 +41,7 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
|
||||||
provider: self,
|
provider: self,
|
||||||
decoder: Registry.sharedProtocolCoder,
|
decoder: Registry.sharedProtocolCoder,
|
||||||
registry: .shared,
|
registry: .shared,
|
||||||
environment: .shared,
|
environment: .shared
|
||||||
profileBlock: {
|
|
||||||
$0
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
guard let fwd else {
|
guard let fwd else {
|
||||||
fatalError("NEPTPForwarder nil without throwing error?")
|
fatalError("NEPTPForwarder nil without throwing error?")
|
||||||
|
|
Loading…
Reference in New Issue