Store complex preferences to Core Data (#981)
Replace favorites entities with a PreferencesManager, that returns observables for: - Module preferences (by module UUID) - Provider preferences (by ProviderID) Automate preferences availability in: - Module views (empty for now) - VPN server view (favorites) Synchronize preferences by making this a CloudKit container. Preferences are also available in the Tunnel by storing the container in the App Group.
This commit is contained in:
parent
f8655b09af
commit
dfae6afcb4
|
@ -15,11 +15,21 @@ let package = Package(
|
|||
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||
.library(
|
||||
name: "AppUIMain",
|
||||
targets: ["AppUIMainWrapper"]
|
||||
targets: [
|
||||
"AppDataPreferences",
|
||||
"AppDataProfiles",
|
||||
"AppDataProviders",
|
||||
"AppUIMainWrapper"
|
||||
]
|
||||
),
|
||||
.library(
|
||||
name: "AppUITV",
|
||||
targets: ["AppUITVWrapper"]
|
||||
targets: [
|
||||
"AppDataPreferences",
|
||||
"AppDataProfiles",
|
||||
"AppDataProviders",
|
||||
"AppUITVWrapper"
|
||||
]
|
||||
),
|
||||
.library(
|
||||
name: "CommonIAP",
|
||||
|
@ -39,7 +49,10 @@ let package = Package(
|
|||
),
|
||||
.library(
|
||||
name: "TunnelLibrary",
|
||||
targets: ["CommonLibrary"]
|
||||
targets: [
|
||||
"AppDataPreferences",
|
||||
"CommonLibrary"
|
||||
]
|
||||
),
|
||||
.library(
|
||||
name: "UILibrary",
|
||||
|
@ -69,6 +82,16 @@ let package = Package(
|
|||
name: "AppData",
|
||||
dependencies: []
|
||||
),
|
||||
.target(
|
||||
name: "AppDataPreferences",
|
||||
dependencies: [
|
||||
"AppData",
|
||||
"CommonLibrary"
|
||||
],
|
||||
resources: [
|
||||
.process("Preferences.xcdatamodeld")
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "AppDataProfiles",
|
||||
dependencies: [
|
||||
|
@ -166,8 +189,6 @@ let package = Package(
|
|||
.target(
|
||||
name: "UILibrary",
|
||||
dependencies: [
|
||||
"AppDataProfiles",
|
||||
"AppDataProviders",
|
||||
"CommonAPI",
|
||||
"CommonLibrary",
|
||||
"UITesting"
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// AppData+Preferences.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 12/6/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 CoreData
|
||||
import Foundation
|
||||
|
||||
extension AppData {
|
||||
|
||||
@MainActor
|
||||
public static let cdPreferencesModel: NSManagedObjectModel = {
|
||||
guard let model: NSManagedObjectModel = .mergedModel(from: [.module]) else {
|
||||
fatalError("Unable to build Core Data model (Preferences v3)")
|
||||
}
|
||||
return model
|
||||
}()
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// CDModulePreferencesV3.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 12/5/24.
|
||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <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 uuid: UUID?
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// CDProviderPreferencesV3.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 CoreData
|
||||
import Foundation
|
||||
|
||||
@objc(CDProviderPreferencesV3)
|
||||
final class CDProviderPreferencesV3: NSManagedObject {
|
||||
@nonobjc static func fetchRequest() -> NSFetchRequest<CDProviderPreferencesV3> {
|
||||
NSFetchRequest<CDProviderPreferencesV3>(entityName: "CDProviderPreferencesV3")
|
||||
}
|
||||
|
||||
@NSManaged var providerId: String?
|
||||
@NSManaged var favoriteServerIds: Data?
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23507" systemVersion="23H222" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
|
||||
<entity name="CDModulePreferencesV3" representedClassName="CDModulePreferencesV3" syncable="YES">
|
||||
<attribute name="uuid" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
</entity>
|
||||
<entity name="CDProviderPreferencesV3" representedClassName="CDProviderPreferencesV3" syncable="YES">
|
||||
<attribute name="favoriteServerIds" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="providerId" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
</model>
|
|
@ -0,0 +1,89 @@
|
|||
//
|
||||
// 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 {
|
||||
|
||||
@MainActor
|
||||
public static func cdModulePreferencesRepositoryV3(context: NSManagedObjectContext) -> ModulePreferencesRepository {
|
||||
CDModulePreferencesRepositoryV3(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Repository
|
||||
|
||||
private final class CDModulePreferencesRepositoryV3: ModulePreferencesRepository {
|
||||
private nonisolated let context: NSManagedObjectContext
|
||||
|
||||
init(context: NSManagedObjectContext) {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy {
|
||||
let entity = try context.performAndWait {
|
||||
let request = CDModulePreferencesV3.fetchRequest()
|
||||
request.predicate = NSPredicate(format: "uuid == %@", moduleId.uuidString)
|
||||
do {
|
||||
let entity = try request.execute().first ?? CDModulePreferencesV3(context: context)
|
||||
entity.uuid = moduleId
|
||||
return entity
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to load preferences for module \(moduleId): \(error)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return CDModulePreferencesProxy(context: context, entity: entity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preference
|
||||
|
||||
private final class CDModulePreferencesProxy: ModulePreferencesProxy {
|
||||
private let context: NSManagedObjectContext
|
||||
|
||||
private let entity: CDModulePreferencesV3
|
||||
|
||||
init(context: NSManagedObjectContext, entity: CDModulePreferencesV3) {
|
||||
self.context = context
|
||||
self.entity = entity
|
||||
}
|
||||
|
||||
func save() throws {
|
||||
guard context.hasChanges else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
//
|
||||
// CDProviderPreferencesRepositoryV3.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 {
|
||||
|
||||
@MainActor
|
||||
public static func cdProviderPreferencesRepositoryV3(context: NSManagedObjectContext) -> ProviderPreferencesRepository {
|
||||
CDProviderPreferencesRepositoryV3(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Repository
|
||||
|
||||
private final class CDProviderPreferencesRepositoryV3: ProviderPreferencesRepository {
|
||||
private nonisolated let context: NSManagedObjectContext
|
||||
|
||||
init(context: NSManagedObjectContext) {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy {
|
||||
let entity = try context.performAndWait {
|
||||
let request = CDProviderPreferencesV3.fetchRequest()
|
||||
request.predicate = NSPredicate(format: "providerId == %@", providerId.rawValue)
|
||||
do {
|
||||
let entity = try request.execute().first ?? CDProviderPreferencesV3(context: context)
|
||||
entity.providerId = providerId.rawValue
|
||||
return entity
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(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> {
|
||||
get {
|
||||
do {
|
||||
return try context.performAndWait {
|
||||
guard let data = entity.favoriteServerIds else {
|
||||
return []
|
||||
}
|
||||
return try JSONDecoder().decode(Set<String>.self, from: data)
|
||||
}
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to get favoriteServers: \(error)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
set {
|
||||
do {
|
||||
try context.performAndWait {
|
||||
entity.favoriteServerIds = try JSONEncoder().encode(newValue)
|
||||
}
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to set favoriteServers: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func save() throws {
|
||||
guard context.hasChanges else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23H124" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23507" systemVersion="23H222" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
|
||||
<entity name="CDProfileV3" representedClassName="CDProfileV3" elementID="CDProfile" versionHashModifier="1" syncable="YES">
|
||||
<attribute name="encoded" optional="YES" attributeType="String" allowsCloudEncryption="YES"/>
|
||||
<attribute name="fingerprint" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
|
|
|
@ -36,7 +36,7 @@ extension AppData {
|
|||
}
|
||||
}
|
||||
|
||||
actor CDProviderRepositoryV3: NSObject, ProviderRepository {
|
||||
private actor CDProviderRepositoryV3: NSObject, ProviderRepository {
|
||||
private nonisolated let context: NSManagedObjectContext
|
||||
|
||||
private nonisolated let providersSubject: CurrentValueSubject<[Provider], Never>
|
||||
|
|
|
@ -33,10 +33,15 @@ struct DNSView: View, ModuleDraftEditing {
|
|||
@EnvironmentObject
|
||||
private var theme: Theme
|
||||
|
||||
let module: DNSModule.Builder
|
||||
|
||||
@ObservedObject
|
||||
var editor: ProfileEditor
|
||||
|
||||
let module: DNSModule.Builder
|
||||
init(module: DNSModule.Builder, parameters: ModuleViewParameters) {
|
||||
self.module = module
|
||||
editor = parameters.editor
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
debugChanges()
|
||||
|
|
|
@ -23,12 +23,13 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
import UILibrary
|
||||
|
||||
extension DNSModule.Builder: ModuleViewProviding {
|
||||
public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
|
||||
DNSView(editor: editor, module: self)
|
||||
public func moduleView(with parameters: ModuleViewParameters) -> some View {
|
||||
DNSView(module: self, parameters: parameters)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,12 +23,13 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
import UILibrary
|
||||
|
||||
extension HTTPProxyModule.Builder: ModuleViewProviding {
|
||||
public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
|
||||
HTTPProxyView(editor: editor, module: self)
|
||||
public func moduleView(with parameters: ModuleViewParameters) -> some View {
|
||||
HTTPProxyView(module: self, parameters: parameters)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,12 +23,13 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
import UILibrary
|
||||
|
||||
extension IPModule.Builder: ModuleViewProviding {
|
||||
public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
|
||||
IPView(editor: editor, module: self)
|
||||
public func moduleView(with parameters: ModuleViewParameters) -> some View {
|
||||
IPView(module: self, parameters: parameters)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import SwiftUI
|
|||
import UILibrary
|
||||
|
||||
extension OnDemandModule.Builder: ModuleViewProviding {
|
||||
public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
|
||||
OnDemandView(editor: editor, module: self)
|
||||
public func moduleView(with parameters: ModuleViewParameters) -> some View {
|
||||
OnDemandView(module: self, parameters: parameters)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,8 +29,8 @@ import SwiftUI
|
|||
import UILibrary
|
||||
|
||||
extension OpenVPNModule.Builder: ModuleViewProviding {
|
||||
public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
|
||||
OpenVPNView(editor: editor, module: self, impl: impl as? OpenVPNModule.Implementation)
|
||||
public func moduleView(with parameters: ModuleViewParameters) -> some View {
|
||||
OpenVPNView(module: self, parameters: parameters)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,8 +29,8 @@ import SwiftUI
|
|||
import UILibrary
|
||||
|
||||
extension WireGuardModule.Builder: ModuleViewProviding {
|
||||
public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
|
||||
WireGuardView(editor: editor, module: self, impl: impl as? WireGuardModule.Implementation)
|
||||
public func moduleView(with parameters: ModuleViewParameters) -> some View {
|
||||
WireGuardView(module: self, parameters: parameters)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,10 +32,15 @@ struct HTTPProxyView: View, ModuleDraftEditing {
|
|||
@EnvironmentObject
|
||||
private var theme: Theme
|
||||
|
||||
let module: HTTPProxyModule.Builder
|
||||
|
||||
@ObservedObject
|
||||
var editor: ProfileEditor
|
||||
|
||||
let module: HTTPProxyModule.Builder
|
||||
init(module: HTTPProxyModule.Builder, parameters: ModuleViewParameters) {
|
||||
self.module = module
|
||||
editor = parameters.editor
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
|
|
|
@ -28,15 +28,19 @@ import PassepartoutKit
|
|||
import SwiftUI
|
||||
|
||||
struct IPView: View, ModuleDraftEditing {
|
||||
let module: IPModule.Builder
|
||||
|
||||
@ObservedObject
|
||||
var editor: ProfileEditor
|
||||
|
||||
let module: IPModule.Builder
|
||||
|
||||
@State
|
||||
private var routePresentation: RoutePresentation?
|
||||
|
||||
init(module: IPModule.Builder, parameters: ModuleViewParameters) {
|
||||
self.module = module
|
||||
editor = parameters.editor
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
ipSections(for: .v4)
|
||||
|
|
|
@ -33,23 +33,23 @@ struct OnDemandView: View, ModuleDraftEditing {
|
|||
@EnvironmentObject
|
||||
private var theme: Theme
|
||||
|
||||
let module: OnDemandModule.Builder
|
||||
|
||||
@ObservedObject
|
||||
var editor: ProfileEditor
|
||||
|
||||
let module: OnDemandModule.Builder
|
||||
|
||||
private let wifi: Wifi
|
||||
|
||||
@State
|
||||
private var paywallReason: PaywallReason?
|
||||
|
||||
init(
|
||||
editor: ProfileEditor,
|
||||
module: OnDemandModule.Builder,
|
||||
parameters: ModuleViewParameters,
|
||||
observer: WifiObserver? = nil
|
||||
) {
|
||||
self.editor = editor
|
||||
self.module = module
|
||||
editor = parameters.editor
|
||||
wifi = Wifi(observer: observer ?? CoreLocationWifiObserver())
|
||||
}
|
||||
|
||||
|
@ -239,8 +239,12 @@ private extension OnDemandView {
|
|||
]
|
||||
return module.preview {
|
||||
OnDemandView(
|
||||
editor: $0,
|
||||
module: $1,
|
||||
module: $0,
|
||||
parameters: .init(
|
||||
editor: $1,
|
||||
preferences: nil,
|
||||
impl: nil
|
||||
),
|
||||
observer: MockWifi()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -33,11 +33,11 @@ struct OpenVPNView: View, ModuleDraftEditing {
|
|||
@Environment(\.navigationPath)
|
||||
private var path
|
||||
|
||||
let module: OpenVPNModule.Builder
|
||||
|
||||
@ObservedObject
|
||||
var editor: ProfileEditor
|
||||
|
||||
let module: OpenVPNModule.Builder
|
||||
|
||||
let impl: OpenVPNModule.Implementation?
|
||||
|
||||
private let isServerPushed: Bool
|
||||
|
@ -61,20 +61,18 @@ struct OpenVPNView: View, ModuleDraftEditing {
|
|||
private var errorHandler: ErrorHandler = .default()
|
||||
|
||||
init(serverConfiguration: OpenVPN.Configuration) {
|
||||
let module = OpenVPNModule.Builder(configurationBuilder: serverConfiguration.builder())
|
||||
let editor = ProfileEditor(modules: [module])
|
||||
module = OpenVPNModule.Builder(configurationBuilder: serverConfiguration.builder())
|
||||
editor = ProfileEditor(modules: [module])
|
||||
assert(module.configurationBuilder != nil, "isServerPushed must imply module.configurationBuilder != nil")
|
||||
|
||||
self.editor = editor
|
||||
self.module = module
|
||||
impl = nil
|
||||
isServerPushed = true
|
||||
}
|
||||
|
||||
init(editor: ProfileEditor, module: OpenVPNModule.Builder, impl: OpenVPNModule.Implementation?) {
|
||||
self.editor = editor
|
||||
init(module: OpenVPNModule.Builder, parameters: ModuleViewParameters) {
|
||||
self.module = module
|
||||
self.impl = impl
|
||||
editor = parameters.editor
|
||||
impl = parameters.impl as? OpenVPNModule.Implementation
|
||||
isServerPushed = false
|
||||
}
|
||||
|
||||
|
|
|
@ -33,11 +33,11 @@ struct WireGuardView: View, ModuleDraftEditing {
|
|||
@Environment(\.navigationPath)
|
||||
private var path
|
||||
|
||||
let module: WireGuardModule.Builder
|
||||
|
||||
@ObservedObject
|
||||
var editor: ProfileEditor
|
||||
|
||||
let module: WireGuardModule.Builder
|
||||
|
||||
let impl: WireGuardModule.Implementation?
|
||||
|
||||
@State
|
||||
|
@ -46,6 +46,12 @@ struct WireGuardView: View, ModuleDraftEditing {
|
|||
@State
|
||||
private var errorHandler: ErrorHandler = .default()
|
||||
|
||||
init(module: WireGuardModule.Builder, parameters: ModuleViewParameters) {
|
||||
self.module = module
|
||||
editor = parameters.editor
|
||||
impl = parameters.impl as? WireGuardModule.Implementation
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
contentView
|
||||
.moduleView(editor: editor, draft: draft.wrappedValue)
|
||||
|
|
|
@ -23,16 +23,24 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
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 {
|
||||
|
@ -42,6 +50,16 @@ 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,7 +67,11 @@ private extension ModuleDetailView {
|
|||
|
||||
@MainActor
|
||||
func editorView(forModuleWithId moduleId: UUID) -> some View {
|
||||
AnyView(moduleViewFactory.view(with: profileEditor, moduleId: moduleId))
|
||||
AnyView(moduleViewFactory.view(
|
||||
with: profileEditor,
|
||||
preferences: modulePreferences,
|
||||
moduleId: moduleId
|
||||
))
|
||||
}
|
||||
|
||||
var emptyView: some View {
|
||||
|
|
|
@ -34,6 +34,9 @@ struct VPNProviderServerView<Configuration>: View where Configuration: Identifia
|
|||
@EnvironmentObject
|
||||
private var providerManager: ProviderManager
|
||||
|
||||
@EnvironmentObject
|
||||
private var preferencesManager: PreferencesManager
|
||||
|
||||
var apis: [APIMapper] = API.shared
|
||||
|
||||
let moduleId: UUID
|
||||
|
@ -65,10 +68,10 @@ struct VPNProviderServerView<Configuration>: View where Configuration: Identifia
|
|||
private var onlyShowsFavorites = false
|
||||
|
||||
@StateObject
|
||||
private var filtersViewModel = VPNFiltersView.Model()
|
||||
private var providerPreferences = ProviderPreferences(proxy: nil)
|
||||
|
||||
@StateObject
|
||||
private var favoritesManager = ProviderFavoritesManager()
|
||||
private var filtersViewModel = VPNFiltersView.Model()
|
||||
|
||||
@StateObject
|
||||
private var errorHandler: ErrorHandler = .default()
|
||||
|
@ -94,7 +97,7 @@ extension VPNProviderServerView {
|
|||
selectedServer: selectedEntity?.server,
|
||||
isFiltering: isFiltering,
|
||||
filtersViewModel: filtersViewModel,
|
||||
favoritesManager: favoritesManager,
|
||||
providerPreferences: providerPreferences,
|
||||
selectTitle: selectTitle,
|
||||
onSelect: onSelectServer
|
||||
)
|
||||
|
@ -123,7 +126,7 @@ private extension VPNProviderServerView {
|
|||
var filteredServers: [VPNServer] {
|
||||
if onlyShowsFavorites {
|
||||
return servers.filter {
|
||||
favoritesManager.serverIds.contains($0.serverId)
|
||||
providerPreferences.favoriteServers.contains($0.serverId)
|
||||
}
|
||||
}
|
||||
return servers
|
||||
|
@ -156,7 +159,11 @@ private extension VPNProviderServerView {
|
|||
private extension VPNProviderServerView {
|
||||
func loadInitialServers() async {
|
||||
do {
|
||||
favoritesManager.moduleId = moduleId
|
||||
providerPreferences.proxy = try preferencesManager.providerPreferencesProxy(in: providerId)
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(error)")
|
||||
}
|
||||
do {
|
||||
let repository = try await providerManager.vpnServerRepository(
|
||||
from: apis,
|
||||
for: providerId
|
||||
|
@ -165,7 +172,7 @@ private extension VPNProviderServerView {
|
|||
filtersViewModel.load(options: vpnManager.options, initialFilters: initialFilters)
|
||||
await reloadServers(filters: filtersViewModel.filters)
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to load VPN repository: \(error)")
|
||||
pp_log(.app, .error, "Unable to load VPN servers for provider \(providerId): \(error)")
|
||||
errorHandler.handle(error, title: Strings.Global.Nouns.servers)
|
||||
}
|
||||
}
|
||||
|
@ -194,7 +201,11 @@ private extension VPNProviderServerView {
|
|||
}
|
||||
|
||||
func onDisappear() {
|
||||
favoritesManager.save()
|
||||
do {
|
||||
try providerPreferences.save()
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to save preferences: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func onSelectServer(_ server: VPNServer) {
|
||||
|
|
|
@ -46,7 +46,7 @@ extension VPNProviderServerView {
|
|||
var filtersViewModel: VPNFiltersView.Model
|
||||
|
||||
@ObservedObject
|
||||
var favoritesManager: ProviderFavoritesManager
|
||||
var providerPreferences: ProviderPreferences
|
||||
|
||||
let selectTitle: String
|
||||
|
||||
|
@ -151,7 +151,7 @@ private extension VPNProviderServerView.ContentView {
|
|||
Spacer()
|
||||
FavoriteToggle(
|
||||
value: server.serverId,
|
||||
selection: $favoritesManager.serverIds
|
||||
selection: $providerPreferences.favoriteServers
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ extension VPNProviderServerView {
|
|||
var filtersViewModel: VPNFiltersView.Model
|
||||
|
||||
@ObservedObject
|
||||
var favoritesManager: ProviderFavoritesManager
|
||||
var providerPreferences: ProviderPreferences
|
||||
|
||||
let selectTitle: String
|
||||
|
||||
|
@ -87,7 +87,7 @@ private extension VPNProviderServerView.ContentView {
|
|||
TableColumn("") { server in
|
||||
FavoriteToggle(
|
||||
value: server.serverId,
|
||||
selection: $favoritesManager.serverIds
|
||||
selection: $providerPreferences.favoriteServers
|
||||
)
|
||||
.environmentObject(theme) // TODO: #873, Table loses environment
|
||||
}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// PreferencesManager.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 12/4/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 PreferencesManager: ObservableObject {
|
||||
private let modulesRepository: ModulePreferencesRepository
|
||||
|
||||
private let providersRepository: ProviderPreferencesRepository
|
||||
|
||||
public init(
|
||||
modulesRepository: ModulePreferencesRepository? = nil,
|
||||
providersRepository: ProviderPreferencesRepository? = nil
|
||||
) {
|
||||
self.modulesRepository = modulesRepository ?? DummyModulePreferencesRepository()
|
||||
self.providersRepository = providersRepository ?? DummyProviderPreferencesRepository()
|
||||
}
|
||||
|
||||
public func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy {
|
||||
try modulesRepository.modulePreferencesProxy(in: moduleId)
|
||||
}
|
||||
|
||||
public func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy {
|
||||
try providersRepository.providerPreferencesProxy(in: providerId)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Dummy
|
||||
|
||||
private final class DummyModulePreferencesRepository: ModulePreferencesRepository {
|
||||
private final class Proxy: ModulePreferencesProxy {
|
||||
func save() throws {
|
||||
}
|
||||
}
|
||||
|
||||
func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy {
|
||||
Proxy()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -30,25 +30,35 @@ import PassepartoutKit
|
|||
|
||||
extension BundleConfiguration {
|
||||
public static var urlForAppLog: URL {
|
||||
cachesURL.appending(path: Constants.shared.log.appPath)
|
||||
urlForGroupCaches.appending(path: Constants.shared.log.appPath)
|
||||
}
|
||||
|
||||
public static var urlForTunnelLog: URL {
|
||||
cachesURL.appending(path: Constants.shared.log.tunnelPath)
|
||||
urlForGroupCaches.appending(path: Constants.shared.log.tunnelPath)
|
||||
}
|
||||
|
||||
public static var urlForBetaReceipt: URL {
|
||||
cachesURL.appending(path: Constants.shared.tunnel.betaReceiptPath)
|
||||
urlForGroupCaches.appending(path: Constants.shared.tunnel.betaReceiptPath)
|
||||
}
|
||||
}
|
||||
|
||||
extension BundleConfiguration {
|
||||
public static var urlForGroupCaches: URL {
|
||||
appGroupURL.appending(components: "Library", "Caches")
|
||||
}
|
||||
|
||||
public static var urlForGroupDocuments: URL {
|
||||
appGroupURL.appending(components: "Library", "Documents")
|
||||
}
|
||||
}
|
||||
|
||||
private extension BundleConfiguration {
|
||||
static var cachesURL: URL {
|
||||
static var appGroupURL: URL {
|
||||
let groupId = mainString(for: .groupId)
|
||||
guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupId) else {
|
||||
pp_log(.app, .error, "Unable to access App Group container")
|
||||
return FileManager.default.temporaryDirectory
|
||||
}
|
||||
return url.appending(components: "Library", "Caches")
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,8 @@ extension BundleConfiguration {
|
|||
|
||||
case cloudKitId
|
||||
|
||||
case cloudKitPreferencesId
|
||||
|
||||
case userLevel
|
||||
|
||||
case groupId
|
||||
|
|
|
@ -28,12 +28,14 @@ import PassepartoutKit
|
|||
|
||||
public struct Constants: Decodable, Sendable {
|
||||
public struct Containers: Decodable, Sendable {
|
||||
public let local: String
|
||||
public let localProfiles: String
|
||||
|
||||
public let remote: String
|
||||
public let remoteProfiles: String
|
||||
|
||||
public let providers: String
|
||||
|
||||
public let preferences: String
|
||||
|
||||
public let legacyV2: String
|
||||
|
||||
public let legacyV2TV: String
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
//
|
||||
// ModulePreferences.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 12/5/24.
|
||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
@MainActor
|
||||
public final class ModulePreferences: ObservableObject {
|
||||
public var proxy: ModulePreferencesProxy? {
|
||||
didSet {
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
public init(proxy: ModulePreferencesProxy?) {
|
||||
self.proxy = proxy
|
||||
}
|
||||
|
||||
public func save() throws {
|
||||
try proxy?.save()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public protocol ModulePreferencesProxy {
|
||||
func save() throws
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
// ProviderPreferences.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 Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
@MainActor
|
||||
public final class ProviderPreferences: ObservableObject {
|
||||
public var proxy: ProviderPreferencesProxy? {
|
||||
didSet {
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
public init(proxy: ProviderPreferencesProxy?) {
|
||||
self.proxy = proxy
|
||||
}
|
||||
|
||||
public var favoriteServers: Set<String> {
|
||||
get {
|
||||
proxy?.favoriteServers ?? []
|
||||
}
|
||||
set {
|
||||
objectWillChange.send()
|
||||
proxy?.favoriteServers = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public func save() throws {
|
||||
try proxy?.save()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public protocol ProviderPreferencesProxy {
|
||||
var favoriteServers: Set<String> { get set }
|
||||
|
||||
func save() throws
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
{
|
||||
"bundleKey": "AppConfig",
|
||||
"containers": {
|
||||
"local": "Profiles-v3",
|
||||
"remote": "Profiles-v3.remote",
|
||||
"localProfiles": "Profiles-v3",
|
||||
"remoteProfiles": "Profiles-v3.remote",
|
||||
"providers": "Providers-v3",
|
||||
"preferences": "Preferences-v3",
|
||||
"legacyV2": "Profiles",
|
||||
"legacyV2TV": "SharedProfiles"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// ModulePreferencesRepository.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 12/5/24.
|
||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
public protocol ModulePreferencesRepository {
|
||||
func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// ProviderPreferencesRepository.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 Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
@MainActor
|
||||
public protocol ProviderPreferencesRepository {
|
||||
func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
//
|
||||
// ProviderFavoriteServers.swift
|
||||
// UUID+RawRepresentable.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/25/24.
|
||||
// Created by Davide De Rosa on 12/4/24.
|
||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
|
@ -25,31 +25,12 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public struct ProviderFavoriteServers {
|
||||
private var map: [UUID: Set<String>]
|
||||
|
||||
public init() {
|
||||
map = [:]
|
||||
}
|
||||
|
||||
public func servers(forModuleWithId moduleId: UUID) -> Set<String> {
|
||||
map[moduleId] ?? []
|
||||
}
|
||||
|
||||
public mutating func setServers(_ servers: Set<String>, forModuleWithId moduleId: UUID) {
|
||||
map[moduleId] = servers
|
||||
}
|
||||
}
|
||||
|
||||
extension ProviderFavoriteServers: RawRepresentable {
|
||||
public var rawValue: String {
|
||||
(try? JSONEncoder().encode(map))?.base64EncodedString() ?? ""
|
||||
}
|
||||
|
||||
extension UUID: @retroactive RawRepresentable {
|
||||
public init?(rawValue: String) {
|
||||
guard let data = Data(base64Encoded: rawValue) else {
|
||||
return nil
|
||||
self.init(uuidString: rawValue)
|
||||
}
|
||||
map = (try? JSONDecoder().decode([UUID: Set<String>].self, from: data)) ?? [:]
|
||||
|
||||
public var rawValue: String {
|
||||
uuidString
|
||||
}
|
||||
}
|
|
@ -40,6 +40,8 @@ public final class AppContext: ObservableObject {
|
|||
|
||||
public let providerManager: ProviderManager
|
||||
|
||||
public let preferencesManager: PreferencesManager
|
||||
|
||||
public let registry: Registry
|
||||
|
||||
public let tunnel: ExtendedTunnel
|
||||
|
@ -57,6 +59,7 @@ public final class AppContext: ObservableObject {
|
|||
migrationManager: MigrationManager,
|
||||
profileManager: ProfileManager,
|
||||
providerManager: ProviderManager,
|
||||
preferencesManager: PreferencesManager,
|
||||
registry: Registry,
|
||||
tunnel: ExtendedTunnel,
|
||||
tunnelReceiptURL: URL
|
||||
|
@ -65,6 +68,7 @@ public final class AppContext: ObservableObject {
|
|||
self.migrationManager = migrationManager
|
||||
self.profileManager = profileManager
|
||||
self.providerManager = providerManager
|
||||
self.preferencesManager = preferencesManager
|
||||
self.registry = registry
|
||||
self.tunnel = tunnel
|
||||
self.tunnelReceiptURL = tunnelReceiptURL
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
//
|
||||
// ProviderFavoritesManager.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/26/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 Foundation
|
||||
|
||||
@MainActor
|
||||
public final class ProviderFavoritesManager: ObservableObject {
|
||||
private let defaults: UserDefaults
|
||||
|
||||
private var allFavorites: ProviderFavoriteServers
|
||||
|
||||
public var moduleId: UUID {
|
||||
didSet {
|
||||
guard let rawValue = defaults.string(forKey: UIPreference.providerFavoriteServers.key) else {
|
||||
allFavorites = ProviderFavoriteServers()
|
||||
return
|
||||
}
|
||||
allFavorites = ProviderFavoriteServers(rawValue: rawValue) ?? ProviderFavoriteServers()
|
||||
}
|
||||
}
|
||||
|
||||
public var serverIds: Set<String> {
|
||||
get {
|
||||
allFavorites.servers(forModuleWithId: moduleId)
|
||||
}
|
||||
set {
|
||||
objectWillChange.send()
|
||||
allFavorites.setServers(newValue, forModuleWithId: moduleId)
|
||||
}
|
||||
}
|
||||
|
||||
public init(defaults: UserDefaults = .standard) {
|
||||
self.defaults = defaults
|
||||
allFavorites = ProviderFavoriteServers()
|
||||
moduleId = UUID()
|
||||
}
|
||||
|
||||
public func save() {
|
||||
defaults.set(allFavorites.rawValue, forKey: UIPreference.providerFavoriteServers.key)
|
||||
}
|
||||
}
|
|
@ -37,8 +37,6 @@ public enum UIPreference: String, PreferenceProtocol {
|
|||
|
||||
case profilesLayout
|
||||
|
||||
case providerFavoriteServers
|
||||
|
||||
public var key: String {
|
||||
"UI.\(rawValue)"
|
||||
}
|
||||
|
|
|
@ -31,16 +31,20 @@ extension ModuleBuilder where Self: ModuleViewProviding {
|
|||
@MainActor
|
||||
public func preview(title: String = "") -> some View {
|
||||
NavigationStack {
|
||||
moduleView(with: ProfileEditor(modules: [self]), impl: nil)
|
||||
moduleView(with: .init(
|
||||
editor: ProfileEditor(modules: [self]),
|
||||
preferences: nil,
|
||||
impl: nil
|
||||
))
|
||||
.navigationTitle(title)
|
||||
}
|
||||
.withMockEnvironment()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func preview<C: View>(with content: (ProfileEditor, Self) -> C) -> some View {
|
||||
public func preview<C: View>(with content: (Self, ProfileEditor) -> C) -> some View {
|
||||
NavigationStack {
|
||||
content(ProfileEditor(modules: [self]), self)
|
||||
content(self, ProfileEditor(modules: [self]))
|
||||
}
|
||||
.withMockEnvironment()
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ extension View {
|
|||
environmentObject(theme)
|
||||
.environmentObject(context.iapManager)
|
||||
.environmentObject(context.migrationManager)
|
||||
.environmentObject(context.preferencesManager)
|
||||
.environmentObject(context.providerManager)
|
||||
}
|
||||
|
||||
|
|
|
@ -83,6 +83,7 @@ extension AppContext {
|
|||
migrationManager: migrationManager,
|
||||
profileManager: profileManager,
|
||||
providerManager: providerManager,
|
||||
preferencesManager: PreferencesManager(),
|
||||
registry: Registry(),
|
||||
tunnel: tunnel,
|
||||
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import Foundation
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
@ -35,10 +36,14 @@ 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: editor, impl: result.impl))
|
||||
AnyView(result.provider.moduleView(with: .init(
|
||||
editor: editor,
|
||||
preferences: preferences,
|
||||
impl: result.impl
|
||||
)))
|
||||
.navigationTitle(result.title)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
@ -30,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
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
|
@ -30,5 +31,24 @@ public protocol ModuleViewProviding {
|
|||
associatedtype Content: View
|
||||
|
||||
@MainActor
|
||||
func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> Content
|
||||
func moduleView(with parameters: ModuleViewParameters) -> Content
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>$(CFG_CLOUDKIT_ID)</string>
|
||||
<string>$(CFG_CLOUDKIT_PREFERENCES_ID)</string>
|
||||
<string>$(CFG_LEGACY_V2_CLOUDKIT_ID)</string>
|
||||
<string>$(CFG_LEGACY_V2_TV_CLOUDKIT_ID)</string>
|
||||
</array>
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
<string>$(CFG_APP_STORE_ID)</string>
|
||||
<key>cloudKitId</key>
|
||||
<string>$(CFG_CLOUDKIT_ID)</string>
|
||||
<key>cloudKitPreferencesId</key>
|
||||
<string>$(CFG_CLOUDKIT_PREFERENCES_ID)</string>
|
||||
<key>groupId</key>
|
||||
<string>$(CFG_GROUP_ID)</string>
|
||||
<key>iapBundlePrefix</key>
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
CFG_APP_ID = com.algoritmico.ios.Passepartout
|
||||
CFG_APP_STORE_ID = 1433648537
|
||||
CFG_CLOUDKIT_ID = iCloud.com.algoritmico.Passepartout.v3
|
||||
CFG_CLOUDKIT_PREFERENCES_ID = iCloud.com.algoritmico.Passepartout.v3.Preferences
|
||||
CFG_COPYRIGHT = Copyright © 2024 Davide De Rosa. All rights reserved.
|
||||
CFG_DISPLAY_NAME = Passepartout
|
||||
CFG_GROUP_ID[sdk=appletvos*] = $(CFG_RAW_GROUP_ID)
|
||||
|
|
|
@ -48,7 +48,7 @@ extension AppContext {
|
|||
let remoteRepositoryBlock: (Bool) -> ProfileRepository = {
|
||||
let remoteStore = CoreDataPersistentStore(
|
||||
logger: .default,
|
||||
containerName: Constants.shared.containers.remote,
|
||||
containerName: Constants.shared.containers.remoteProfiles,
|
||||
model: AppData.cdProfilesModel,
|
||||
cloudKitIdentifier: $0 ? BundleConfiguration.mainString(for: .cloudKitId) : nil,
|
||||
author: nil
|
||||
|
@ -124,6 +124,7 @@ extension AppContext {
|
|||
migrationManager: migrationManager,
|
||||
profileManager: profileManager,
|
||||
providerManager: providerManager,
|
||||
preferencesManager: .shared,
|
||||
registry: .shared,
|
||||
tunnel: tunnel,
|
||||
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
|
||||
|
@ -199,7 +200,7 @@ private extension Dependencies.ProfileManager {
|
|||
static func coreDataProfileRepository(observingResults: Bool) -> ProfileRepository {
|
||||
let store = CoreDataPersistentStore(
|
||||
logger: .default,
|
||||
containerName: Constants.shared.containers.local,
|
||||
containerName: Constants.shared.containers.localProfiles,
|
||||
model: AppData.cdProfilesModel,
|
||||
cloudKitIdentifier: nil,
|
||||
author: nil
|
||||
|
@ -215,21 +216,3 @@ private extension Dependencies.ProfileManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Logging
|
||||
|
||||
private extension CoreDataPersistentStoreLogger where Self == DefaultCoreDataPersistentStoreLogger {
|
||||
static var `default`: CoreDataPersistentStoreLogger {
|
||||
DefaultCoreDataPersistentStoreLogger()
|
||||
}
|
||||
}
|
||||
|
||||
private struct DefaultCoreDataPersistentStoreLogger: CoreDataPersistentStoreLogger {
|
||||
func debug(_ msg: String) {
|
||||
pp_log(.app, .info, msg)
|
||||
}
|
||||
|
||||
func warning(_ msg: String) {
|
||||
pp_log(.app, .error, msg)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import AppData
|
||||
import AppDataPreferences
|
||||
import CommonLibrary
|
||||
import CommonUtils
|
||||
import CPassepartoutOpenVPNOpenSSL
|
||||
|
@ -30,8 +32,6 @@ import Foundation
|
|||
import PassepartoutKit
|
||||
import PassepartoutWireGuardGo
|
||||
|
||||
// MARK: Registry
|
||||
|
||||
extension Registry {
|
||||
static let shared = Registry(
|
||||
withKnownHandlers: true,
|
||||
|
@ -130,3 +130,40 @@ extension InAppProcessor {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension PreferencesManager {
|
||||
static let shared: PreferencesManager = {
|
||||
let preferencesStore = CoreDataPersistentStore(
|
||||
logger: .default,
|
||||
containerName: Constants.shared.containers.preferences,
|
||||
baseURL: BundleConfiguration.urlForGroupDocuments,
|
||||
model: AppData.cdPreferencesModel,
|
||||
cloudKitIdentifier: BundleConfiguration.mainString(for: .cloudKitPreferencesId),
|
||||
author: nil
|
||||
)
|
||||
let modulePreferencesRepository = AppData.cdModulePreferencesRepositoryV3(context: preferencesStore.context)
|
||||
let providerPreferencesRepository = AppData.cdProviderPreferencesRepositoryV3(context: preferencesStore.context)
|
||||
return PreferencesManager(
|
||||
modulesRepository: modulePreferencesRepository,
|
||||
providersRepository: providerPreferencesRepository
|
||||
)
|
||||
}()
|
||||
}
|
||||
|
||||
// MARK: - Logging
|
||||
|
||||
extension CoreDataPersistentStoreLogger where Self == DefaultCoreDataPersistentStoreLogger {
|
||||
static var `default`: CoreDataPersistentStoreLogger {
|
||||
DefaultCoreDataPersistentStoreLogger()
|
||||
}
|
||||
}
|
||||
|
||||
struct DefaultCoreDataPersistentStoreLogger: CoreDataPersistentStoreLogger {
|
||||
func debug(_ msg: String) {
|
||||
pp_log(.app, .info, msg)
|
||||
}
|
||||
|
||||
func warning(_ msg: String) {
|
||||
pp_log(.app, .error, msg)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@ extension AppContext {
|
|||
migrationManager: migrationManager,
|
||||
profileManager: profileManager,
|
||||
providerManager: providerManager,
|
||||
preferencesManager: PreferencesManager(),
|
||||
registry: registry,
|
||||
tunnel: tunnel,
|
||||
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
<string>$(CFG_KEYCHAIN_GROUP_ID)</string>
|
||||
<key>tunnelId</key>
|
||||
<string>$(CFG_TUNNEL_ID)</string>
|
||||
<key>cloudKitPreferencesId</key>
|
||||
<string>$(CFG_CLOUDKIT_PREFERENCES_ID)</string>
|
||||
</dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
|
|
Loading…
Reference in New Issue