Track module preferences history in Core Data (#994)
Restore CDModulePreferencesV3 to track the history of module prefrences. This way, excluded endpoints may be saved globally to Core Data as a starting point. Then in Profile.userInfo we only save the relevant exclusions for the current configuration. The .excludedEndpoints relationship is therefore moved out of CDProviderPreferencesV3. Further refactoring: - ModuleViewParameters now includes a ModulePreferences observable that module views can observe - Tunnel doesn't need access to PreferencesManager anymore (exclusions are in Profile.userInfo)
This commit is contained in:
parent
aeec943c58
commit
6f9c78b257
|
@ -33,5 +33,5 @@ final class CDExcludedEndpoint: NSManagedObject {
|
|||
}
|
||||
|
||||
@NSManaged var endpoint: String?
|
||||
@NSManaged var providerPreferences: CDProviderPreferencesV3?
|
||||
@NSManaged var modulePreferences: CDModulePreferencesV3?
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
// CDModulePreferencesV3.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 12/10/24.
|
||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@objc(CDModulePreferencesV3)
|
||||
final class CDModulePreferencesV3: NSManagedObject {
|
||||
@nonobjc static func fetchRequest() -> NSFetchRequest<CDModulePreferencesV3> {
|
||||
NSFetchRequest<CDModulePreferencesV3>(entityName: "CDModulePreferencesV3")
|
||||
}
|
||||
|
||||
@NSManaged var moduleId: UUID?
|
||||
@NSManaged var lastUpdate: Date?
|
||||
@NSManaged var excludedEndpoints: Set<CDExcludedEndpoint>?
|
||||
}
|
|
@ -35,5 +35,4 @@ final class CDProviderPreferencesV3: NSManagedObject {
|
|||
@NSManaged var providerId: String?
|
||||
@NSManaged var lastUpdate: Date?
|
||||
@NSManaged var favoriteServerIds: Data?
|
||||
@NSManaged var excludedEndpoints: Set<CDExcludedEndpoint>?
|
||||
}
|
||||
|
|
|
@ -2,12 +2,16 @@
|
|||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23507" systemVersion="23H222" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
|
||||
<entity name="CDExcludedEndpoint" representedClassName="CDExcludedEndpoint" syncable="YES">
|
||||
<attribute name="endpoint" optional="YES" attributeType="String"/>
|
||||
<relationship name="providerPreferences" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CDProviderPreferencesV3" inverseName="excludedEndpoints" inverseEntity="CDProviderPreferencesV3"/>
|
||||
<relationship name="modulePreferences" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CDModulePreferencesV3" inverseName="excludedEndpoints" inverseEntity="CDModulePreferencesV3"/>
|
||||
</entity>
|
||||
<entity name="CDModulePreferencesV3" representedClassName="CDModulePreferencesV3" syncable="YES">
|
||||
<attribute name="lastUpdate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="moduleId" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<relationship name="excludedEndpoints" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="CDExcludedEndpoint" inverseName="modulePreferences" inverseEntity="CDExcludedEndpoint"/>
|
||||
</entity>
|
||||
<entity name="CDProviderPreferencesV3" representedClassName="CDProviderPreferencesV3" syncable="YES">
|
||||
<attribute name="favoriteServerIds" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="lastUpdate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="providerId" optional="YES" attributeType="String"/>
|
||||
<relationship name="excludedEndpoints" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="CDExcludedEndpoint" inverseName="providerPreferences" inverseEntity="CDExcludedEndpoint"/>
|
||||
</entity>
|
||||
</model>
|
|
@ -0,0 +1,120 @@
|
|||
//
|
||||
// CDModulePreferencesRepositoryV3.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 12/5/24.
|
||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import AppData
|
||||
import CommonLibrary
|
||||
import CoreData
|
||||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
extension AppData {
|
||||
public static func cdModulePreferencesRepositoryV3(context: NSManagedObjectContext, moduleId: UUID) throws -> ModulePreferencesRepository {
|
||||
try CDModulePreferencesRepositoryV3(context: context, moduleId: moduleId)
|
||||
}
|
||||
}
|
||||
|
||||
private final class CDModulePreferencesRepositoryV3: ModulePreferencesRepository {
|
||||
private nonisolated let context: NSManagedObjectContext
|
||||
|
||||
private let entity: CDModulePreferencesV3
|
||||
|
||||
init(context: NSManagedObjectContext, moduleId: UUID) throws {
|
||||
self.context = context
|
||||
|
||||
entity = try context.performAndWait {
|
||||
let request = CDModulePreferencesV3.fetchRequest()
|
||||
request.predicate = NSPredicate(format: "moduleId == %@", moduleId.uuidString)
|
||||
request.sortDescriptors = [.init(key: "lastUpdate", ascending: false)]
|
||||
do {
|
||||
let entities = try request.execute()
|
||||
|
||||
// dedup by lastUpdate
|
||||
entities.enumerated().forEach {
|
||||
guard $0.offset > 0 else {
|
||||
return
|
||||
}
|
||||
$0.element.excludedEndpoints?.forEach(context.delete(_:))
|
||||
context.delete($0.element)
|
||||
}
|
||||
|
||||
let entity = entities.first ?? CDModulePreferencesV3(context: context)
|
||||
entity.moduleId = moduleId
|
||||
entity.lastUpdate = Date()
|
||||
return entity
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to load preferences for module \(moduleId): \(error)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool {
|
||||
context.performAndWait {
|
||||
entity.excludedEndpoints?.contains {
|
||||
$0.endpoint == endpoint.rawValue
|
||||
} ?? false
|
||||
}
|
||||
}
|
||||
|
||||
func addExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
|
||||
context.performAndWait {
|
||||
guard entity.excludedEndpoints?.contains(where: {
|
||||
$0.endpoint == endpoint.rawValue
|
||||
}) != true else {
|
||||
return
|
||||
}
|
||||
let mapper = CoreDataMapper(context: context)
|
||||
let cdEndpoint = mapper.cdExcludedEndpoint(from: endpoint)
|
||||
cdEndpoint.modulePreferences = entity
|
||||
entity.excludedEndpoints?.insert(cdEndpoint)
|
||||
}
|
||||
}
|
||||
|
||||
func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
|
||||
context.performAndWait {
|
||||
guard let found = entity.excludedEndpoints?.first(where: {
|
||||
$0.endpoint == endpoint.rawValue
|
||||
}) else {
|
||||
return
|
||||
}
|
||||
entity.excludedEndpoints?.remove(found)
|
||||
context.delete(found)
|
||||
}
|
||||
}
|
||||
|
||||
func save() throws {
|
||||
try context.performAndWait {
|
||||
guard context.hasChanges else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -55,7 +55,6 @@ private final class CDProviderPreferencesRepositoryV3: ProviderPreferencesReposi
|
|||
guard $0.offset > 0 else {
|
||||
return
|
||||
}
|
||||
$0.element.excludedEndpoints?.forEach(context.delete(_:))
|
||||
context.delete($0.element)
|
||||
}
|
||||
|
||||
|
@ -95,35 +94,6 @@ private final class CDProviderPreferencesRepositoryV3: ProviderPreferencesReposi
|
|||
}
|
||||
}
|
||||
|
||||
func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool {
|
||||
context.performAndWait {
|
||||
entity.excludedEndpoints?.contains {
|
||||
$0.endpoint == endpoint.rawValue
|
||||
} ?? false
|
||||
}
|
||||
}
|
||||
|
||||
func addExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
|
||||
context.performAndWait {
|
||||
let mapper = CoreDataMapper(context: context)
|
||||
let cdEndpoint = mapper.cdExcludedEndpoint(from: endpoint)
|
||||
cdEndpoint.providerPreferences = entity
|
||||
entity.excludedEndpoints?.insert(cdEndpoint)
|
||||
}
|
||||
}
|
||||
|
||||
func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
|
||||
context.performAndWait {
|
||||
guard let found = entity.excludedEndpoints?.first(where: {
|
||||
$0.endpoint == endpoint.rawValue
|
||||
}) else {
|
||||
return
|
||||
}
|
||||
entity.excludedEndpoints?.remove(found)
|
||||
context.delete(found)
|
||||
}
|
||||
}
|
||||
|
||||
func save() throws {
|
||||
try context.performAndWait {
|
||||
guard context.hasChanges else {
|
||||
|
|
|
@ -34,9 +34,6 @@ public struct AppCoordinator: View, AppCoordinatorConforming, SizeClassProviding
|
|||
@EnvironmentObject
|
||||
public var iapManager: IAPManager
|
||||
|
||||
@EnvironmentObject
|
||||
public var preferencesManager: PreferencesManager
|
||||
|
||||
@Environment(\.isUITesting)
|
||||
private var isUITesting
|
||||
|
||||
|
|
|
@ -242,6 +242,7 @@ private extension OnDemandView {
|
|||
module: $0,
|
||||
parameters: .init(
|
||||
editor: $1,
|
||||
preferences: ModulePreferences(),
|
||||
impl: nil
|
||||
),
|
||||
observer: MockWifi()
|
||||
|
|
|
@ -30,9 +30,6 @@ import SwiftUI
|
|||
|
||||
struct OpenVPNView: View, ModuleDraftEditing {
|
||||
|
||||
@EnvironmentObject
|
||||
private var preferencesManager: PreferencesManager
|
||||
|
||||
@Environment(\.navigationPath)
|
||||
private var path
|
||||
|
||||
|
@ -41,6 +38,9 @@ struct OpenVPNView: View, ModuleDraftEditing {
|
|||
@ObservedObject
|
||||
var editor: ProfileEditor
|
||||
|
||||
@ObservedObject
|
||||
var modulePreferences: ModulePreferences
|
||||
|
||||
let impl: OpenVPNModule.Implementation?
|
||||
|
||||
private let isServerPushed: Bool
|
||||
|
@ -51,15 +51,13 @@ struct OpenVPNView: View, ModuleDraftEditing {
|
|||
@State
|
||||
private var paywallReason: PaywallReason?
|
||||
|
||||
@StateObject
|
||||
private var providerPreferences = ProviderPreferences()
|
||||
|
||||
@StateObject
|
||||
private var errorHandler: ErrorHandler = .default()
|
||||
|
||||
init(serverConfiguration: OpenVPN.Configuration) {
|
||||
module = OpenVPNModule.Builder(configurationBuilder: serverConfiguration.builder())
|
||||
editor = ProfileEditor(modules: [module])
|
||||
modulePreferences = ModulePreferences()
|
||||
assert(module.configurationBuilder != nil, "isServerPushed must imply module.configurationBuilder != nil")
|
||||
impl = nil
|
||||
isServerPushed = true
|
||||
|
@ -68,6 +66,7 @@ struct OpenVPNView: View, ModuleDraftEditing {
|
|||
init(module: OpenVPNModule.Builder, parameters: ModuleViewParameters) {
|
||||
self.module = module
|
||||
editor = parameters.editor
|
||||
modulePreferences = parameters.preferences
|
||||
impl = parameters.impl as? OpenVPNModule.Implementation
|
||||
isServerPushed = false
|
||||
}
|
||||
|
@ -129,7 +128,7 @@ private extension OpenVPNView {
|
|||
var providerModifier: some ViewModifier {
|
||||
VPNProviderContentModifier(
|
||||
providerId: providerId,
|
||||
providerPreferences: providerPreferences,
|
||||
providerPreferences: nil,
|
||||
selectedEntity: providerEntity,
|
||||
paywallReason: $paywallReason,
|
||||
entityDestination: Subroute.providerServer,
|
||||
|
@ -200,17 +199,32 @@ private extension OpenVPNView {
|
|||
|
||||
private extension OpenVPNView {
|
||||
var excludedEndpoints: ObservableList<ExtendedEndpoint> {
|
||||
if draft.wrappedValue.providerSelection != nil {
|
||||
return providerPreferences.excludedEndpoints()
|
||||
} else {
|
||||
return editor.excludedEndpoints(for: module.id)
|
||||
}
|
||||
editor.excludedEndpoints(for: module.id, preferences: modulePreferences)
|
||||
}
|
||||
|
||||
func onSelectServer(server: VPNServer, preset: VPNPreset<OpenVPN.Configuration>) {
|
||||
draft.wrappedValue.providerEntity = VPNEntity(server: server, preset: preset)
|
||||
resetExcludedEndpointsWithCurrentProviderEntity()
|
||||
path.wrappedValue.removeLast()
|
||||
}
|
||||
|
||||
// filter out exclusions unrelated to current server
|
||||
func resetExcludedEndpointsWithCurrentProviderEntity() {
|
||||
do {
|
||||
let cfg = try draft.wrappedValue.providerSelection?.configuration()
|
||||
editor.profile.attributes.editPreferences(inModule: module.id) {
|
||||
if let cfg {
|
||||
$0.excludedEndpoints = Set(cfg.remotes?.filter {
|
||||
modulePreferences.isExcludedEndpoint($0)
|
||||
} ?? [])
|
||||
} else {
|
||||
$0.excludedEndpoints = []
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to build provider configuration for excluded endpoints: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
|
|
@ -28,12 +28,19 @@ 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 preferences = ModulePreferences()
|
||||
|
||||
var body: some View {
|
||||
debugChanges()
|
||||
return Group {
|
||||
|
@ -52,8 +59,24 @@ private extension ModuleDetailView {
|
|||
func editorView(forModuleWithId moduleId: UUID) -> some View {
|
||||
AnyView(moduleViewFactory.view(
|
||||
with: profileEditor,
|
||||
preferences: preferences,
|
||||
moduleId: moduleId
|
||||
))
|
||||
.onLoad {
|
||||
do {
|
||||
let repository = try preferencesManager.preferencesRepository(forModuleWithId: moduleId)
|
||||
preferences.setRepository(repository)
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to load preferences for module \(moduleId): \(error)")
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
do {
|
||||
try preferences.save()
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to save preferences for module \(moduleId): \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var emptyView: some View {
|
||||
|
|
|
@ -43,9 +43,6 @@ struct ProfileCoordinator: View {
|
|||
@EnvironmentObject
|
||||
private var iapManager: IAPManager
|
||||
|
||||
@EnvironmentObject
|
||||
private var preferencesManager: PreferencesManager
|
||||
|
||||
let profileManager: ProfileManager
|
||||
|
||||
let profileEditor: ProfileEditor
|
||||
|
|
|
@ -188,13 +188,14 @@ private extension ProviderContentModifier {
|
|||
if let providerId {
|
||||
do {
|
||||
pp_log(.app, .debug, "Load preferences for provider \(providerId)")
|
||||
providerPreferences.repository = try preferencesManager.preferencesRepository(forProviderWithId: providerId)
|
||||
let repository = try preferencesManager.preferencesRepository(forProviderWithId: providerId)
|
||||
providerPreferences.setRepository(repository)
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(error)")
|
||||
providerPreferences.repository = nil
|
||||
providerPreferences.setRepository(nil)
|
||||
}
|
||||
} else {
|
||||
providerPreferences.repository = nil
|
||||
providerPreferences.setRepository(nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -164,7 +164,8 @@ private extension VPNProviderServerView {
|
|||
private extension VPNProviderServerView {
|
||||
func loadInitialServers() async {
|
||||
do {
|
||||
providerPreferences.repository = try preferencesManager.preferencesRepository(forProviderWithId: providerId)
|
||||
let repository = try preferencesManager.preferencesRepository(forProviderWithId: providerId)
|
||||
providerPreferences.setRepository(repository)
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(error)")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// ModulePreferences.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 12/10/24.
|
||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonUtils
|
||||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
@MainActor
|
||||
public final class ModulePreferences: ObservableObject {
|
||||
private var repository: ModulePreferencesRepository?
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
public func setRepository(_ repository: ModulePreferencesRepository?) {
|
||||
self.repository = repository
|
||||
}
|
||||
|
||||
public func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool {
|
||||
repository?.isExcludedEndpoint(endpoint) ?? false
|
||||
}
|
||||
|
||||
public func addExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
|
||||
repository?.addExcludedEndpoint(endpoint)
|
||||
}
|
||||
|
||||
public func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
|
||||
repository?.removeExcludedEndpoint(endpoint)
|
||||
}
|
||||
|
||||
public func save() throws {
|
||||
try repository?.save()
|
||||
}
|
||||
}
|
|
@ -28,11 +28,17 @@ import Foundation
|
|||
import PassepartoutKit
|
||||
|
||||
public final class PreferencesManager: ObservableObject, Sendable {
|
||||
private let modulesFactory: @Sendable (UUID) throws -> ModulePreferencesRepository
|
||||
|
||||
private let providersFactory: @Sendable (ProviderID) throws -> ProviderPreferencesRepository
|
||||
|
||||
public init(
|
||||
modulesFactory: (@Sendable (UUID) throws -> ModulePreferencesRepository)? = nil,
|
||||
providersFactory: (@Sendable (ProviderID) throws -> ProviderPreferencesRepository)? = nil
|
||||
) {
|
||||
self.modulesFactory = modulesFactory ?? { _ in
|
||||
DummyModulePreferencesRepository()
|
||||
}
|
||||
self.providersFactory = providersFactory ?? { _ in
|
||||
DummyProviderPreferencesRepository()
|
||||
}
|
||||
|
@ -40,25 +46,18 @@ public final class PreferencesManager: ObservableObject, Sendable {
|
|||
}
|
||||
|
||||
extension PreferencesManager {
|
||||
public func preferencesRepository(forModuleWithId moduleId: UUID) throws -> ModulePreferencesRepository {
|
||||
try modulesFactory(moduleId)
|
||||
}
|
||||
|
||||
public func preferencesRepository(forProviderWithId providerId: ProviderID) throws -> ProviderPreferencesRepository {
|
||||
try providersFactory(providerId)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
extension PreferencesManager {
|
||||
public func preferences(forProviderWithId providerId: ProviderID) throws -> ProviderPreferences {
|
||||
let object = ProviderPreferences()
|
||||
object.repository = try providersFactory(providerId)
|
||||
return object
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Dummy
|
||||
|
||||
private final class DummyProviderPreferencesRepository: ProviderPreferencesRepository {
|
||||
var favoriteServers: Set<String> = []
|
||||
|
||||
private final class DummyModulePreferencesRepository: ModulePreferencesRepository {
|
||||
func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool {
|
||||
false
|
||||
}
|
||||
|
@ -72,3 +71,10 @@ private final class DummyProviderPreferencesRepository: ProviderPreferencesRepos
|
|||
func save() throws {
|
||||
}
|
||||
}
|
||||
|
||||
private final class DummyProviderPreferencesRepository: ProviderPreferencesRepository {
|
||||
var favoriteServers: Set<String> = []
|
||||
|
||||
func save() throws {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,11 +29,15 @@ import PassepartoutKit
|
|||
|
||||
@MainActor
|
||||
public final class ProviderPreferences: ObservableObject {
|
||||
public var repository: ProviderPreferencesRepository?
|
||||
private var repository: ProviderPreferencesRepository?
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
public func setRepository(_ repository: ProviderPreferencesRepository?) {
|
||||
self.repository = repository
|
||||
}
|
||||
|
||||
public var favoriteServers: Set<String> {
|
||||
get {
|
||||
repository?.favoriteServers ?? []
|
||||
|
@ -44,16 +48,6 @@ public final class ProviderPreferences: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public func excludedEndpoints() -> ObservableList<ExtendedEndpoint> {
|
||||
ObservableList { [weak self] in
|
||||
self?.repository?.isExcludedEndpoint($0) == true
|
||||
} add: { [weak self] in
|
||||
self?.repository?.addExcludedEndpoint($0)
|
||||
} remove: { [weak self] in
|
||||
self?.repository?.removeExcludedEndpoint($0)
|
||||
}
|
||||
}
|
||||
|
||||
public func save() throws {
|
||||
try repository?.save()
|
||||
}
|
|
@ -40,17 +40,26 @@ extension ProfileAttributes {
|
|||
self.userInfo = userInfo ?? [:]
|
||||
}
|
||||
|
||||
public var excludedEndpoints: Set<ExtendedEndpoint> {
|
||||
get {
|
||||
Set(rawExcludedEndpoints.compactMap(ExtendedEndpoint.init(rawValue:)))
|
||||
}
|
||||
set {
|
||||
rawExcludedEndpoints = newValue.map(\.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
public func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool {
|
||||
excludedEndpoints.contains(endpoint.rawValue)
|
||||
rawExcludedEndpoints.contains(endpoint.rawValue)
|
||||
}
|
||||
|
||||
public mutating func addExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
|
||||
excludedEndpoints.append(endpoint.rawValue)
|
||||
rawExcludedEndpoints.append(endpoint.rawValue)
|
||||
}
|
||||
|
||||
public mutating func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
|
||||
let rawValue = endpoint.rawValue
|
||||
excludedEndpoints.removeAll {
|
||||
rawExcludedEndpoints.removeAll {
|
||||
$0 == rawValue
|
||||
}
|
||||
}
|
||||
|
@ -58,7 +67,7 @@ extension ProfileAttributes {
|
|||
}
|
||||
|
||||
extension ProfileAttributes.ModulePreferences {
|
||||
var excludedEndpoints: [String] {
|
||||
var rawExcludedEndpoints: [String] {
|
||||
get {
|
||||
userInfo[Key.excludedEndpoints.rawValue] as? [String] ?? []
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// ModulePreferencesRepository.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 12/10/24.
|
||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
public protocol ModulePreferencesRepository {
|
||||
func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool
|
||||
|
||||
func addExcludedEndpoint(_ endpoint: ExtendedEndpoint)
|
||||
|
||||
func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint)
|
||||
|
||||
func save() throws
|
||||
}
|
|
@ -29,11 +29,5 @@ import PassepartoutKit
|
|||
public protocol ProviderPreferencesRepository {
|
||||
var favoriteServers: Set<String> { get set }
|
||||
|
||||
func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool
|
||||
|
||||
func addExcludedEndpoint(_ endpoint: ExtendedEndpoint)
|
||||
|
||||
func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint)
|
||||
|
||||
func save() throws
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
|
@ -33,6 +34,7 @@ extension ModuleBuilder where Self: ModuleViewProviding {
|
|||
NavigationStack {
|
||||
moduleView(with: .init(
|
||||
editor: ProfileEditor(modules: [self]),
|
||||
preferences: ModulePreferences(),
|
||||
impl: nil
|
||||
))
|
||||
.navigationTitle(title)
|
||||
|
|
|
@ -47,7 +47,7 @@ extension ProfileEditor {
|
|||
// MARK: - ModulePreferences
|
||||
|
||||
extension ProfileEditor {
|
||||
public func excludedEndpoints(for moduleId: UUID) -> ObservableList<ExtendedEndpoint> {
|
||||
public func excludedEndpoints(for moduleId: UUID, preferences: ModulePreferences) -> ObservableList<ExtendedEndpoint> {
|
||||
ObservableList { [weak self] endpoint in
|
||||
self?.profile.attributes.preference(inModule: moduleId) {
|
||||
$0.isExcludedEndpoint(endpoint)
|
||||
|
@ -56,10 +56,12 @@ extension ProfileEditor {
|
|||
self?.profile.attributes.editPreferences(inModule: moduleId) {
|
||||
$0.addExcludedEndpoint(endpoint)
|
||||
}
|
||||
preferences.addExcludedEndpoint(endpoint)
|
||||
} remove: { [weak self] endpoint in
|
||||
self?.profile.attributes.editPreferences(inModule: moduleId) {
|
||||
$0.removeExcludedEndpoint(endpoint)
|
||||
}
|
||||
preferences.removeExcludedEndpoint(endpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,11 +36,12 @@ public final class DefaultModuleViewFactory: ModuleViewFactory {
|
|||
}
|
||||
|
||||
@ViewBuilder
|
||||
public func view(with editor: ProfileEditor, moduleId: UUID) -> some View {
|
||||
public func view(with editor: ProfileEditor, preferences: ModulePreferences, 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, moduleId: UUID) -> Content
|
||||
func view(with editor: ProfileEditor, preferences: ModulePreferences, moduleId: UUID) -> Content
|
||||
}
|
||||
|
|
|
@ -37,14 +37,18 @@ 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
|
||||
self.impl = impl
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ final class ProfileAttributesTests: XCTestCase {
|
|||
let excludedEndpoints: [String] = [
|
||||
"1.1.1.1:UDP6:1000",
|
||||
"2.2.2.2:TCP4:2000",
|
||||
"3.3.3.3:TCP:3000",
|
||||
"3.3.3.3:TCP:3000"
|
||||
]
|
||||
let moduleUserInfo: [String: AnyHashable] = [
|
||||
"excludedEndpoints": excludedEndpoints
|
||||
|
@ -89,7 +89,7 @@ final class ProfileAttributesTests: XCTestCase {
|
|||
for moduleId in [moduleId1, moduleId2] {
|
||||
let module = sut.preferences(inModule: moduleId)
|
||||
XCTAssertEqual(module.userInfo, moduleUserInfo)
|
||||
XCTAssertEqual(module.excludedEndpoints, excludedEndpoints)
|
||||
XCTAssertEqual(module.rawExcludedEndpoints, excludedEndpoints)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,7 @@ final class ProfileAttributesTests: XCTestCase {
|
|||
let excludedEndpoints: [String] = [
|
||||
"1.1.1.1:UDP6:1000",
|
||||
"2.2.2.2:TCP4:2000",
|
||||
"3.3.3.3:TCP:3000",
|
||||
"3.3.3.3:TCP:3000"
|
||||
]
|
||||
let moduleUserInfo: [String: AnyHashable] = [
|
||||
"excludedEndpoints": excludedEndpoints
|
||||
|
@ -114,7 +114,7 @@ final class ProfileAttributesTests: XCTestCase {
|
|||
var sut = ProfileAttributes(userInfo: nil)
|
||||
for moduleId in [moduleId1, moduleId2] {
|
||||
var module = sut.preferences(inModule: moduleId1)
|
||||
module.excludedEndpoints = excludedEndpoints
|
||||
module.rawExcludedEndpoints = excludedEndpoints
|
||||
XCTAssertEqual(module.userInfo, moduleUserInfo)
|
||||
sut.setPreferences(module, inModule: moduleId)
|
||||
}
|
||||
|
|
|
@ -36,8 +36,6 @@
|
|||
0E8DFD532D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD4C2D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift */; };
|
||||
0E8DFD542D05FE5A00531CDE /* Dependencies+IAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD4B2D05FE5A00531CDE /* Dependencies+IAPManager.swift */; };
|
||||
0E8DFD562D05FE5A00531CDE /* Dependencies+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD492D05FE5A00531CDE /* Dependencies+CoreData.swift */; };
|
||||
0E8DFD592D05FF0400531CDE /* Dependencies+PreferencesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD582D05FF0400531CDE /* Dependencies+PreferencesManager.swift */; };
|
||||
0E8DFD5A2D05FF0400531CDE /* Dependencies+PreferencesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD582D05FF0400531CDE /* Dependencies+PreferencesManager.swift */; };
|
||||
0E916B782CF80FD60072921A /* ProfileEditorScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E916B772CF80FD60072921A /* ProfileEditorScreen.swift */; };
|
||||
0E916B7C2CF811EB0072921A /* XCUIElement+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E916B7B2CF811EB0072921A /* XCUIElement+Extensions.swift */; };
|
||||
0E94EE582B93554B00588243 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7E3D672B9345FD002BBDB4 /* PacketTunnelProvider.swift */; };
|
||||
|
@ -170,7 +168,6 @@
|
|||
0E8DFD4B2D05FE5A00531CDE /* Dependencies+IAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dependencies+IAPManager.swift"; sourceTree = "<group>"; };
|
||||
0E8DFD4C2D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dependencies+PassepartoutKit.swift"; sourceTree = "<group>"; };
|
||||
0E8DFD4D2D05FE5A00531CDE /* Dependencies+Processors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dependencies+Processors.swift"; sourceTree = "<group>"; };
|
||||
0E8DFD582D05FF0400531CDE /* Dependencies+PreferencesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dependencies+PreferencesManager.swift"; sourceTree = "<group>"; };
|
||||
0E916B772CF80FD60072921A /* ProfileEditorScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditorScreen.swift; sourceTree = "<group>"; };
|
||||
0E916B7B2CF811EB0072921A /* XCUIElement+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIElement+Extensions.swift"; sourceTree = "<group>"; };
|
||||
0E94EE5C2B93570600588243 /* Tunnel.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Tunnel.plist; sourceTree = "<group>"; };
|
||||
|
@ -350,7 +347,6 @@
|
|||
0E8DFD492D05FE5A00531CDE /* Dependencies+CoreData.swift */,
|
||||
0E8DFD4B2D05FE5A00531CDE /* Dependencies+IAPManager.swift */,
|
||||
0E8DFD4C2D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift */,
|
||||
0E8DFD582D05FF0400531CDE /* Dependencies+PreferencesManager.swift */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
|
@ -720,7 +716,6 @@
|
|||
0E81955A2CFDA75200CC8FFD /* Dependencies.swift in Sources */,
|
||||
0E6EEEE32CF8CABA0076E2B0 /* AppContext+Testing.swift in Sources */,
|
||||
0E6EEEE42CF8CABA0076E2B0 /* ProfileManager+Testing.swift in Sources */,
|
||||
0E8DFD592D05FF0400531CDE /* Dependencies+PreferencesManager.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -769,7 +764,6 @@
|
|||
0E8DFD532D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift in Sources */,
|
||||
0E8DFD542D05FE5A00531CDE /* Dependencies+IAPManager.swift in Sources */,
|
||||
0E8DFD562D05FE5A00531CDE /* Dependencies+CoreData.swift in Sources */,
|
||||
0E8DFD5A2D05FF0400531CDE /* Dependencies+PreferencesManager.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
//
|
||||
|
||||
import AppData
|
||||
import AppDataPreferences
|
||||
import AppDataProfiles
|
||||
import AppDataProviders
|
||||
import CommonLibrary
|
||||
|
@ -122,7 +123,30 @@ extension AppContext {
|
|||
return MigrationManager(profileStrategy: profileStrategy, simulation: migrationSimulation)
|
||||
}()
|
||||
|
||||
let preferencesManager = dependencies.preferencesManager(withCloudKit: true)
|
||||
let preferencesManager: PreferencesManager = {
|
||||
let preferencesStore = CoreDataPersistentStore(
|
||||
logger: dependencies.coreDataLogger(),
|
||||
containerName: Constants.shared.containers.preferences,
|
||||
baseURL: BundleConfiguration.urlForGroupDocuments,
|
||||
model: AppData.cdPreferencesModel,
|
||||
cloudKitIdentifier: BundleConfiguration.mainString(for: .cloudKitPreferencesId),
|
||||
author: nil
|
||||
)
|
||||
return PreferencesManager(
|
||||
modulesFactory: {
|
||||
try AppData.cdModulePreferencesRepositoryV3(
|
||||
context: preferencesStore.context,
|
||||
moduleId: $0
|
||||
)
|
||||
},
|
||||
providersFactory: {
|
||||
try AppData.cdProviderPreferencesRepositoryV3(
|
||||
context: preferencesStore.context,
|
||||
providerId: $0
|
||||
)
|
||||
}
|
||||
)
|
||||
}()
|
||||
|
||||
return AppContext(
|
||||
iapManager: iapManager,
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
//
|
||||
// Dependencies+PreferencesManager.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 12/2/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 AppData
|
||||
import AppDataPreferences
|
||||
import CommonLibrary
|
||||
import CommonUtils
|
||||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
extension Dependencies {
|
||||
func preferencesManager(withCloudKit: Bool) -> PreferencesManager {
|
||||
let preferencesStore = CoreDataPersistentStore(
|
||||
logger: coreDataLogger(),
|
||||
containerName: Constants.shared.containers.preferences,
|
||||
baseURL: BundleConfiguration.urlForGroupDocuments,
|
||||
model: AppData.cdPreferencesModel,
|
||||
cloudKitIdentifier: withCloudKit ? BundleConfiguration.mainString(for: .cloudKitPreferencesId) : nil,
|
||||
author: nil
|
||||
)
|
||||
return PreferencesManager(
|
||||
providersFactory: {
|
||||
try AppData.cdProviderPreferencesRepositoryV3(
|
||||
context: preferencesStore.context,
|
||||
providerId: $0
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -28,10 +28,7 @@ import Foundation
|
|||
import PassepartoutKit
|
||||
|
||||
final class DefaultTunnelProcessor: Sendable {
|
||||
private let preferencesManager: PreferencesManager
|
||||
|
||||
init(preferencesManager: PreferencesManager) {
|
||||
self.preferencesManager = preferencesManager
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,13 +46,6 @@ extension DefaultTunnelProcessor: PacketTunnelProcessor {
|
|||
preferences.isExcludedEndpoint($0)
|
||||
}
|
||||
|
||||
if let providerId = moduleBuilder.providerId {
|
||||
let providerPreferences = try preferencesManager.preferencesRepository(forProviderWithId: providerId)
|
||||
moduleBuilder.configurationBuilder?.remotes?.removeAll {
|
||||
providerPreferences.isExcludedEndpoint($0)
|
||||
}
|
||||
}
|
||||
|
||||
let module = try moduleBuilder.tryBuild()
|
||||
builder.saveModule(module)
|
||||
}
|
||||
|
|
|
@ -37,10 +37,7 @@ extension TunnelContext {
|
|||
betaChecker: dependencies.betaChecker(),
|
||||
productsAtBuild: dependencies.productsAtBuild()
|
||||
)
|
||||
let processor: PacketTunnelProcessor = {
|
||||
let preferencesManager = dependencies.preferencesManager(withCloudKit: false)
|
||||
return DefaultTunnelProcessor(preferencesManager: preferencesManager)
|
||||
}()
|
||||
let processor = DefaultTunnelProcessor()
|
||||
return TunnelContext(
|
||||
iapManager: iapManager,
|
||||
processor: processor
|
||||
|
|
Loading…
Reference in New Issue