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.
|
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||||
.library(
|
.library(
|
||||||
name: "AppUIMain",
|
name: "AppUIMain",
|
||||||
targets: ["AppUIMainWrapper"]
|
targets: [
|
||||||
|
"AppDataPreferences",
|
||||||
|
"AppDataProfiles",
|
||||||
|
"AppDataProviders",
|
||||||
|
"AppUIMainWrapper"
|
||||||
|
]
|
||||||
),
|
),
|
||||||
.library(
|
.library(
|
||||||
name: "AppUITV",
|
name: "AppUITV",
|
||||||
targets: ["AppUITVWrapper"]
|
targets: [
|
||||||
|
"AppDataPreferences",
|
||||||
|
"AppDataProfiles",
|
||||||
|
"AppDataProviders",
|
||||||
|
"AppUITVWrapper"
|
||||||
|
]
|
||||||
),
|
),
|
||||||
.library(
|
.library(
|
||||||
name: "CommonIAP",
|
name: "CommonIAP",
|
||||||
|
@ -39,7 +49,10 @@ let package = Package(
|
||||||
),
|
),
|
||||||
.library(
|
.library(
|
||||||
name: "TunnelLibrary",
|
name: "TunnelLibrary",
|
||||||
targets: ["CommonLibrary"]
|
targets: [
|
||||||
|
"AppDataPreferences",
|
||||||
|
"CommonLibrary"
|
||||||
|
]
|
||||||
),
|
),
|
||||||
.library(
|
.library(
|
||||||
name: "UILibrary",
|
name: "UILibrary",
|
||||||
|
@ -69,6 +82,16 @@ let package = Package(
|
||||||
name: "AppData",
|
name: "AppData",
|
||||||
dependencies: []
|
dependencies: []
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "AppDataPreferences",
|
||||||
|
dependencies: [
|
||||||
|
"AppData",
|
||||||
|
"CommonLibrary"
|
||||||
|
],
|
||||||
|
resources: [
|
||||||
|
.process("Preferences.xcdatamodeld")
|
||||||
|
]
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "AppDataProfiles",
|
name: "AppDataProfiles",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
@ -166,8 +189,6 @@ let package = Package(
|
||||||
.target(
|
.target(
|
||||||
name: "UILibrary",
|
name: "UILibrary",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"AppDataProfiles",
|
|
||||||
"AppDataProviders",
|
|
||||||
"CommonAPI",
|
"CommonAPI",
|
||||||
"CommonLibrary",
|
"CommonLibrary",
|
||||||
"UITesting"
|
"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"?>
|
<?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">
|
<entity name="CDProfileV3" representedClassName="CDProfileV3" elementID="CDProfile" versionHashModifier="1" syncable="YES">
|
||||||
<attribute name="encoded" optional="YES" attributeType="String" allowsCloudEncryption="YES"/>
|
<attribute name="encoded" optional="YES" attributeType="String" allowsCloudEncryption="YES"/>
|
||||||
<attribute name="fingerprint" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
<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 context: NSManagedObjectContext
|
||||||
|
|
||||||
private nonisolated let providersSubject: CurrentValueSubject<[Provider], Never>
|
private nonisolated let providersSubject: CurrentValueSubject<[Provider], Never>
|
||||||
|
|
|
@ -33,10 +33,15 @@ struct DNSView: View, ModuleDraftEditing {
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var theme: Theme
|
private var theme: Theme
|
||||||
|
|
||||||
|
let module: DNSModule.Builder
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var editor: ProfileEditor
|
var editor: ProfileEditor
|
||||||
|
|
||||||
let module: DNSModule.Builder
|
init(module: DNSModule.Builder, parameters: ModuleViewParameters) {
|
||||||
|
self.module = module
|
||||||
|
editor = parameters.editor
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
debugChanges()
|
debugChanges()
|
||||||
|
|
|
@ -23,12 +23,13 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CommonLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UILibrary
|
import UILibrary
|
||||||
|
|
||||||
extension DNSModule.Builder: ModuleViewProviding {
|
extension DNSModule.Builder: ModuleViewProviding {
|
||||||
public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
|
public func moduleView(with parameters: ModuleViewParameters) -> some View {
|
||||||
DNSView(editor: editor, module: self)
|
DNSView(module: self, parameters: parameters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,12 +23,13 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CommonLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UILibrary
|
import UILibrary
|
||||||
|
|
||||||
extension HTTPProxyModule.Builder: ModuleViewProviding {
|
extension HTTPProxyModule.Builder: ModuleViewProviding {
|
||||||
public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
|
public func moduleView(with parameters: ModuleViewParameters) -> some View {
|
||||||
HTTPProxyView(editor: editor, module: self)
|
HTTPProxyView(module: self, parameters: parameters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,12 +23,13 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CommonLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UILibrary
|
import UILibrary
|
||||||
|
|
||||||
extension IPModule.Builder: ModuleViewProviding {
|
extension IPModule.Builder: ModuleViewProviding {
|
||||||
public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
|
public func moduleView(with parameters: ModuleViewParameters) -> some View {
|
||||||
IPView(editor: editor, module: self)
|
IPView(module: self, parameters: parameters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ import SwiftUI
|
||||||
import UILibrary
|
import UILibrary
|
||||||
|
|
||||||
extension OnDemandModule.Builder: ModuleViewProviding {
|
extension OnDemandModule.Builder: ModuleViewProviding {
|
||||||
public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
|
public func moduleView(with parameters: ModuleViewParameters) -> some View {
|
||||||
OnDemandView(editor: editor, module: self)
|
OnDemandView(module: self, parameters: parameters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,8 +29,8 @@ import SwiftUI
|
||||||
import UILibrary
|
import UILibrary
|
||||||
|
|
||||||
extension OpenVPNModule.Builder: ModuleViewProviding {
|
extension OpenVPNModule.Builder: ModuleViewProviding {
|
||||||
public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
|
public func moduleView(with parameters: ModuleViewParameters) -> some View {
|
||||||
OpenVPNView(editor: editor, module: self, impl: impl as? OpenVPNModule.Implementation)
|
OpenVPNView(module: self, parameters: parameters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,8 +29,8 @@ import SwiftUI
|
||||||
import UILibrary
|
import UILibrary
|
||||||
|
|
||||||
extension WireGuardModule.Builder: ModuleViewProviding {
|
extension WireGuardModule.Builder: ModuleViewProviding {
|
||||||
public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
|
public func moduleView(with parameters: ModuleViewParameters) -> some View {
|
||||||
WireGuardView(editor: editor, module: self, impl: impl as? WireGuardModule.Implementation)
|
WireGuardView(module: self, parameters: parameters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,10 +32,15 @@ struct HTTPProxyView: View, ModuleDraftEditing {
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var theme: Theme
|
private var theme: Theme
|
||||||
|
|
||||||
|
let module: HTTPProxyModule.Builder
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var editor: ProfileEditor
|
var editor: ProfileEditor
|
||||||
|
|
||||||
let module: HTTPProxyModule.Builder
|
init(module: HTTPProxyModule.Builder, parameters: ModuleViewParameters) {
|
||||||
|
self.module = module
|
||||||
|
editor = parameters.editor
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
|
|
|
@ -28,15 +28,19 @@ import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct IPView: View, ModuleDraftEditing {
|
struct IPView: View, ModuleDraftEditing {
|
||||||
|
let module: IPModule.Builder
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var editor: ProfileEditor
|
var editor: ProfileEditor
|
||||||
|
|
||||||
let module: IPModule.Builder
|
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var routePresentation: RoutePresentation?
|
private var routePresentation: RoutePresentation?
|
||||||
|
|
||||||
|
init(module: IPModule.Builder, parameters: ModuleViewParameters) {
|
||||||
|
self.module = module
|
||||||
|
editor = parameters.editor
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
ipSections(for: .v4)
|
ipSections(for: .v4)
|
||||||
|
|
|
@ -33,23 +33,23 @@ struct OnDemandView: View, ModuleDraftEditing {
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var theme: Theme
|
private var theme: Theme
|
||||||
|
|
||||||
|
let module: OnDemandModule.Builder
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var editor: ProfileEditor
|
var editor: ProfileEditor
|
||||||
|
|
||||||
let module: OnDemandModule.Builder
|
|
||||||
|
|
||||||
private let wifi: Wifi
|
private let wifi: Wifi
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var paywallReason: PaywallReason?
|
private var paywallReason: PaywallReason?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
editor: ProfileEditor,
|
|
||||||
module: OnDemandModule.Builder,
|
module: OnDemandModule.Builder,
|
||||||
|
parameters: ModuleViewParameters,
|
||||||
observer: WifiObserver? = nil
|
observer: WifiObserver? = nil
|
||||||
) {
|
) {
|
||||||
self.editor = editor
|
|
||||||
self.module = module
|
self.module = module
|
||||||
|
editor = parameters.editor
|
||||||
wifi = Wifi(observer: observer ?? CoreLocationWifiObserver())
|
wifi = Wifi(observer: observer ?? CoreLocationWifiObserver())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,8 +239,12 @@ private extension OnDemandView {
|
||||||
]
|
]
|
||||||
return module.preview {
|
return module.preview {
|
||||||
OnDemandView(
|
OnDemandView(
|
||||||
editor: $0,
|
module: $0,
|
||||||
module: $1,
|
parameters: .init(
|
||||||
|
editor: $1,
|
||||||
|
preferences: nil,
|
||||||
|
impl: nil
|
||||||
|
),
|
||||||
observer: MockWifi()
|
observer: MockWifi()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,11 +33,11 @@ struct OpenVPNView: View, ModuleDraftEditing {
|
||||||
@Environment(\.navigationPath)
|
@Environment(\.navigationPath)
|
||||||
private var path
|
private var path
|
||||||
|
|
||||||
|
let module: OpenVPNModule.Builder
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var editor: ProfileEditor
|
var editor: ProfileEditor
|
||||||
|
|
||||||
let module: OpenVPNModule.Builder
|
|
||||||
|
|
||||||
let impl: OpenVPNModule.Implementation?
|
let impl: OpenVPNModule.Implementation?
|
||||||
|
|
||||||
private let isServerPushed: Bool
|
private let isServerPushed: Bool
|
||||||
|
@ -61,20 +61,18 @@ struct OpenVPNView: View, ModuleDraftEditing {
|
||||||
private var errorHandler: ErrorHandler = .default()
|
private var errorHandler: ErrorHandler = .default()
|
||||||
|
|
||||||
init(serverConfiguration: OpenVPN.Configuration) {
|
init(serverConfiguration: OpenVPN.Configuration) {
|
||||||
let module = OpenVPNModule.Builder(configurationBuilder: serverConfiguration.builder())
|
module = OpenVPNModule.Builder(configurationBuilder: serverConfiguration.builder())
|
||||||
let editor = ProfileEditor(modules: [module])
|
editor = ProfileEditor(modules: [module])
|
||||||
assert(module.configurationBuilder != nil, "isServerPushed must imply module.configurationBuilder != nil")
|
assert(module.configurationBuilder != nil, "isServerPushed must imply module.configurationBuilder != nil")
|
||||||
|
|
||||||
self.editor = editor
|
|
||||||
self.module = module
|
|
||||||
impl = nil
|
impl = nil
|
||||||
isServerPushed = true
|
isServerPushed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
init(editor: ProfileEditor, module: OpenVPNModule.Builder, impl: OpenVPNModule.Implementation?) {
|
init(module: OpenVPNModule.Builder, parameters: ModuleViewParameters) {
|
||||||
self.editor = editor
|
|
||||||
self.module = module
|
self.module = module
|
||||||
self.impl = impl
|
editor = parameters.editor
|
||||||
|
impl = parameters.impl as? OpenVPNModule.Implementation
|
||||||
isServerPushed = false
|
isServerPushed = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,11 +33,11 @@ struct WireGuardView: View, ModuleDraftEditing {
|
||||||
@Environment(\.navigationPath)
|
@Environment(\.navigationPath)
|
||||||
private var path
|
private var path
|
||||||
|
|
||||||
|
let module: WireGuardModule.Builder
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var editor: ProfileEditor
|
var editor: ProfileEditor
|
||||||
|
|
||||||
let module: WireGuardModule.Builder
|
|
||||||
|
|
||||||
let impl: WireGuardModule.Implementation?
|
let impl: WireGuardModule.Implementation?
|
||||||
|
|
||||||
@State
|
@State
|
||||||
|
@ -46,6 +46,12 @@ struct WireGuardView: View, ModuleDraftEditing {
|
||||||
@State
|
@State
|
||||||
private var errorHandler: ErrorHandler = .default()
|
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 {
|
var body: some View {
|
||||||
contentView
|
contentView
|
||||||
.moduleView(editor: editor, draft: draft.wrappedValue)
|
.moduleView(editor: editor, draft: draft.wrappedValue)
|
||||||
|
|
|
@ -23,16 +23,24 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CommonLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ModuleDetailView: View {
|
struct ModuleDetailView: View {
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var preferencesManager: PreferencesManager
|
||||||
|
|
||||||
let profileEditor: ProfileEditor
|
let profileEditor: ProfileEditor
|
||||||
|
|
||||||
let moduleId: UUID?
|
let moduleId: UUID?
|
||||||
|
|
||||||
let moduleViewFactory: any ModuleViewFactory
|
let moduleViewFactory: any ModuleViewFactory
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var modulePreferences = ModulePreferences(proxy: nil)
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
debugChanges()
|
debugChanges()
|
||||||
return Group {
|
return Group {
|
||||||
|
@ -42,6 +50,16 @@ struct ModuleDetailView: View {
|
||||||
emptyView
|
emptyView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onLoad {
|
||||||
|
guard let moduleId else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
modulePreferences.proxy = try preferencesManager.modulePreferencesProxy(in: moduleId)
|
||||||
|
} catch {
|
||||||
|
pp_log(.app, .error, "Unable to load module preferences: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +67,11 @@ private extension ModuleDetailView {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func editorView(forModuleWithId moduleId: UUID) -> some View {
|
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 {
|
var emptyView: some View {
|
||||||
|
|
|
@ -34,6 +34,9 @@ struct VPNProviderServerView<Configuration>: View where Configuration: Identifia
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var providerManager: ProviderManager
|
private var providerManager: ProviderManager
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var preferencesManager: PreferencesManager
|
||||||
|
|
||||||
var apis: [APIMapper] = API.shared
|
var apis: [APIMapper] = API.shared
|
||||||
|
|
||||||
let moduleId: UUID
|
let moduleId: UUID
|
||||||
|
@ -65,10 +68,10 @@ struct VPNProviderServerView<Configuration>: View where Configuration: Identifia
|
||||||
private var onlyShowsFavorites = false
|
private var onlyShowsFavorites = false
|
||||||
|
|
||||||
@StateObject
|
@StateObject
|
||||||
private var filtersViewModel = VPNFiltersView.Model()
|
private var providerPreferences = ProviderPreferences(proxy: nil)
|
||||||
|
|
||||||
@StateObject
|
@StateObject
|
||||||
private var favoritesManager = ProviderFavoritesManager()
|
private var filtersViewModel = VPNFiltersView.Model()
|
||||||
|
|
||||||
@StateObject
|
@StateObject
|
||||||
private var errorHandler: ErrorHandler = .default()
|
private var errorHandler: ErrorHandler = .default()
|
||||||
|
@ -94,7 +97,7 @@ extension VPNProviderServerView {
|
||||||
selectedServer: selectedEntity?.server,
|
selectedServer: selectedEntity?.server,
|
||||||
isFiltering: isFiltering,
|
isFiltering: isFiltering,
|
||||||
filtersViewModel: filtersViewModel,
|
filtersViewModel: filtersViewModel,
|
||||||
favoritesManager: favoritesManager,
|
providerPreferences: providerPreferences,
|
||||||
selectTitle: selectTitle,
|
selectTitle: selectTitle,
|
||||||
onSelect: onSelectServer
|
onSelect: onSelectServer
|
||||||
)
|
)
|
||||||
|
@ -123,7 +126,7 @@ private extension VPNProviderServerView {
|
||||||
var filteredServers: [VPNServer] {
|
var filteredServers: [VPNServer] {
|
||||||
if onlyShowsFavorites {
|
if onlyShowsFavorites {
|
||||||
return servers.filter {
|
return servers.filter {
|
||||||
favoritesManager.serverIds.contains($0.serverId)
|
providerPreferences.favoriteServers.contains($0.serverId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return servers
|
return servers
|
||||||
|
@ -156,7 +159,11 @@ private extension VPNProviderServerView {
|
||||||
private extension VPNProviderServerView {
|
private extension VPNProviderServerView {
|
||||||
func loadInitialServers() async {
|
func loadInitialServers() async {
|
||||||
do {
|
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(
|
let repository = try await providerManager.vpnServerRepository(
|
||||||
from: apis,
|
from: apis,
|
||||||
for: providerId
|
for: providerId
|
||||||
|
@ -165,7 +172,7 @@ private extension VPNProviderServerView {
|
||||||
filtersViewModel.load(options: vpnManager.options, initialFilters: initialFilters)
|
filtersViewModel.load(options: vpnManager.options, initialFilters: initialFilters)
|
||||||
await reloadServers(filters: filtersViewModel.filters)
|
await reloadServers(filters: filtersViewModel.filters)
|
||||||
} catch {
|
} 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)
|
errorHandler.handle(error, title: Strings.Global.Nouns.servers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -194,7 +201,11 @@ private extension VPNProviderServerView {
|
||||||
}
|
}
|
||||||
|
|
||||||
func onDisappear() {
|
func onDisappear() {
|
||||||
favoritesManager.save()
|
do {
|
||||||
|
try providerPreferences.save()
|
||||||
|
} catch {
|
||||||
|
pp_log(.app, .error, "Unable to save preferences: \(error)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func onSelectServer(_ server: VPNServer) {
|
func onSelectServer(_ server: VPNServer) {
|
||||||
|
|
|
@ -46,7 +46,7 @@ extension VPNProviderServerView {
|
||||||
var filtersViewModel: VPNFiltersView.Model
|
var filtersViewModel: VPNFiltersView.Model
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var favoritesManager: ProviderFavoritesManager
|
var providerPreferences: ProviderPreferences
|
||||||
|
|
||||||
let selectTitle: String
|
let selectTitle: String
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ private extension VPNProviderServerView.ContentView {
|
||||||
Spacer()
|
Spacer()
|
||||||
FavoriteToggle(
|
FavoriteToggle(
|
||||||
value: server.serverId,
|
value: server.serverId,
|
||||||
selection: $favoritesManager.serverIds
|
selection: $providerPreferences.favoriteServers
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ extension VPNProviderServerView {
|
||||||
var filtersViewModel: VPNFiltersView.Model
|
var filtersViewModel: VPNFiltersView.Model
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var favoritesManager: ProviderFavoritesManager
|
var providerPreferences: ProviderPreferences
|
||||||
|
|
||||||
let selectTitle: String
|
let selectTitle: String
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ private extension VPNProviderServerView.ContentView {
|
||||||
TableColumn("") { server in
|
TableColumn("") { server in
|
||||||
FavoriteToggle(
|
FavoriteToggle(
|
||||||
value: server.serverId,
|
value: server.serverId,
|
||||||
selection: $favoritesManager.serverIds
|
selection: $providerPreferences.favoriteServers
|
||||||
)
|
)
|
||||||
.environmentObject(theme) // TODO: #873, Table loses environment
|
.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 {
|
extension BundleConfiguration {
|
||||||
public static var urlForAppLog: URL {
|
public static var urlForAppLog: URL {
|
||||||
cachesURL.appending(path: Constants.shared.log.appPath)
|
urlForGroupCaches.appending(path: Constants.shared.log.appPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var urlForTunnelLog: URL {
|
public static var urlForTunnelLog: URL {
|
||||||
cachesURL.appending(path: Constants.shared.log.tunnelPath)
|
urlForGroupCaches.appending(path: Constants.shared.log.tunnelPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var urlForBetaReceipt: URL {
|
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 {
|
private extension BundleConfiguration {
|
||||||
static var cachesURL: URL {
|
static var appGroupURL: URL {
|
||||||
let groupId = mainString(for: .groupId)
|
let groupId = mainString(for: .groupId)
|
||||||
guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupId) else {
|
guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupId) else {
|
||||||
pp_log(.app, .error, "Unable to access App Group container")
|
pp_log(.app, .error, "Unable to access App Group container")
|
||||||
return FileManager.default.temporaryDirectory
|
return FileManager.default.temporaryDirectory
|
||||||
}
|
}
|
||||||
return url.appending(components: "Library", "Caches")
|
return url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,8 @@ extension BundleConfiguration {
|
||||||
|
|
||||||
case cloudKitId
|
case cloudKitId
|
||||||
|
|
||||||
|
case cloudKitPreferencesId
|
||||||
|
|
||||||
case userLevel
|
case userLevel
|
||||||
|
|
||||||
case groupId
|
case groupId
|
||||||
|
|
|
@ -28,12 +28,14 @@ import PassepartoutKit
|
||||||
|
|
||||||
public struct Constants: Decodable, Sendable {
|
public struct Constants: Decodable, Sendable {
|
||||||
public struct Containers: 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 providers: String
|
||||||
|
|
||||||
|
public let preferences: String
|
||||||
|
|
||||||
public let legacyV2: String
|
public let legacyV2: String
|
||||||
|
|
||||||
public let legacyV2TV: 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",
|
"bundleKey": "AppConfig",
|
||||||
"containers": {
|
"containers": {
|
||||||
"local": "Profiles-v3",
|
"localProfiles": "Profiles-v3",
|
||||||
"remote": "Profiles-v3.remote",
|
"remoteProfiles": "Profiles-v3.remote",
|
||||||
"providers": "Providers-v3",
|
"providers": "Providers-v3",
|
||||||
|
"preferences": "Preferences-v3",
|
||||||
"legacyV2": "Profiles",
|
"legacyV2": "Profiles",
|
||||||
"legacyV2TV": "SharedProfiles"
|
"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
|
// 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.
|
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||||
//
|
//
|
||||||
// https://github.com/passepartoutvpn
|
// https://github.com/passepartoutvpn
|
||||||
|
@ -25,31 +25,12 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct ProviderFavoriteServers {
|
extension UUID: @retroactive RawRepresentable {
|
||||||
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() ?? ""
|
|
||||||
}
|
|
||||||
|
|
||||||
public init?(rawValue: String) {
|
public init?(rawValue: String) {
|
||||||
guard let data = Data(base64Encoded: rawValue) else {
|
self.init(uuidString: rawValue)
|
||||||
return nil
|
}
|
||||||
}
|
|
||||||
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 providerManager: ProviderManager
|
||||||
|
|
||||||
|
public let preferencesManager: PreferencesManager
|
||||||
|
|
||||||
public let registry: Registry
|
public let registry: Registry
|
||||||
|
|
||||||
public let tunnel: ExtendedTunnel
|
public let tunnel: ExtendedTunnel
|
||||||
|
@ -57,6 +59,7 @@ public final class AppContext: ObservableObject {
|
||||||
migrationManager: MigrationManager,
|
migrationManager: MigrationManager,
|
||||||
profileManager: ProfileManager,
|
profileManager: ProfileManager,
|
||||||
providerManager: ProviderManager,
|
providerManager: ProviderManager,
|
||||||
|
preferencesManager: PreferencesManager,
|
||||||
registry: Registry,
|
registry: Registry,
|
||||||
tunnel: ExtendedTunnel,
|
tunnel: ExtendedTunnel,
|
||||||
tunnelReceiptURL: URL
|
tunnelReceiptURL: URL
|
||||||
|
@ -65,6 +68,7 @@ public final class AppContext: ObservableObject {
|
||||||
self.migrationManager = migrationManager
|
self.migrationManager = migrationManager
|
||||||
self.profileManager = profileManager
|
self.profileManager = profileManager
|
||||||
self.providerManager = providerManager
|
self.providerManager = providerManager
|
||||||
|
self.preferencesManager = preferencesManager
|
||||||
self.registry = registry
|
self.registry = registry
|
||||||
self.tunnel = tunnel
|
self.tunnel = tunnel
|
||||||
self.tunnelReceiptURL = tunnelReceiptURL
|
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 profilesLayout
|
||||||
|
|
||||||
case providerFavoriteServers
|
|
||||||
|
|
||||||
public var key: String {
|
public var key: String {
|
||||||
"UI.\(rawValue)"
|
"UI.\(rawValue)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,16 +31,20 @@ extension ModuleBuilder where Self: ModuleViewProviding {
|
||||||
@MainActor
|
@MainActor
|
||||||
public func preview(title: String = "") -> some View {
|
public func preview(title: String = "") -> some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
moduleView(with: ProfileEditor(modules: [self]), impl: nil)
|
moduleView(with: .init(
|
||||||
.navigationTitle(title)
|
editor: ProfileEditor(modules: [self]),
|
||||||
|
preferences: nil,
|
||||||
|
impl: nil
|
||||||
|
))
|
||||||
|
.navigationTitle(title)
|
||||||
}
|
}
|
||||||
.withMockEnvironment()
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@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 {
|
NavigationStack {
|
||||||
content(ProfileEditor(modules: [self]), self)
|
content(self, ProfileEditor(modules: [self]))
|
||||||
}
|
}
|
||||||
.withMockEnvironment()
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ extension View {
|
||||||
environmentObject(theme)
|
environmentObject(theme)
|
||||||
.environmentObject(context.iapManager)
|
.environmentObject(context.iapManager)
|
||||||
.environmentObject(context.migrationManager)
|
.environmentObject(context.migrationManager)
|
||||||
|
.environmentObject(context.preferencesManager)
|
||||||
.environmentObject(context.providerManager)
|
.environmentObject(context.providerManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -83,6 +83,7 @@ extension AppContext {
|
||||||
migrationManager: migrationManager,
|
migrationManager: migrationManager,
|
||||||
profileManager: profileManager,
|
profileManager: profileManager,
|
||||||
providerManager: providerManager,
|
providerManager: providerManager,
|
||||||
|
preferencesManager: PreferencesManager(),
|
||||||
registry: Registry(),
|
registry: Registry(),
|
||||||
tunnel: tunnel,
|
tunnel: tunnel,
|
||||||
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
|
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CommonLibrary
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
@ -35,11 +36,15 @@ public final class DefaultModuleViewFactory: ModuleViewFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@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)
|
let result = editor.moduleViewProvider(withId: moduleId, registry: registry)
|
||||||
if let result {
|
if let result {
|
||||||
AnyView(result.provider.moduleView(with: editor, impl: result.impl))
|
AnyView(result.provider.moduleView(with: .init(
|
||||||
.navigationTitle(result.title)
|
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/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CommonLibrary
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@ -30,5 +31,5 @@ public protocol ModuleViewFactory: AnyObject {
|
||||||
associatedtype Content: View
|
associatedtype Content: View
|
||||||
|
|
||||||
@MainActor
|
@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/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CommonLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@ -30,5 +31,24 @@ public protocol ModuleViewProviding {
|
||||||
associatedtype Content: View
|
associatedtype Content: View
|
||||||
|
|
||||||
@MainActor
|
@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>
|
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||||
<array>
|
<array>
|
||||||
<string>$(CFG_CLOUDKIT_ID)</string>
|
<string>$(CFG_CLOUDKIT_ID)</string>
|
||||||
|
<string>$(CFG_CLOUDKIT_PREFERENCES_ID)</string>
|
||||||
<string>$(CFG_LEGACY_V2_CLOUDKIT_ID)</string>
|
<string>$(CFG_LEGACY_V2_CLOUDKIT_ID)</string>
|
||||||
<string>$(CFG_LEGACY_V2_TV_CLOUDKIT_ID)</string>
|
<string>$(CFG_LEGACY_V2_TV_CLOUDKIT_ID)</string>
|
||||||
</array>
|
</array>
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
<string>$(CFG_APP_STORE_ID)</string>
|
<string>$(CFG_APP_STORE_ID)</string>
|
||||||
<key>cloudKitId</key>
|
<key>cloudKitId</key>
|
||||||
<string>$(CFG_CLOUDKIT_ID)</string>
|
<string>$(CFG_CLOUDKIT_ID)</string>
|
||||||
|
<key>cloudKitPreferencesId</key>
|
||||||
|
<string>$(CFG_CLOUDKIT_PREFERENCES_ID)</string>
|
||||||
<key>groupId</key>
|
<key>groupId</key>
|
||||||
<string>$(CFG_GROUP_ID)</string>
|
<string>$(CFG_GROUP_ID)</string>
|
||||||
<key>iapBundlePrefix</key>
|
<key>iapBundlePrefix</key>
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
CFG_APP_ID = com.algoritmico.ios.Passepartout
|
CFG_APP_ID = com.algoritmico.ios.Passepartout
|
||||||
CFG_APP_STORE_ID = 1433648537
|
CFG_APP_STORE_ID = 1433648537
|
||||||
CFG_CLOUDKIT_ID = iCloud.com.algoritmico.Passepartout.v3
|
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_COPYRIGHT = Copyright © 2024 Davide De Rosa. All rights reserved.
|
||||||
CFG_DISPLAY_NAME = Passepartout
|
CFG_DISPLAY_NAME = Passepartout
|
||||||
CFG_GROUP_ID[sdk=appletvos*] = $(CFG_RAW_GROUP_ID)
|
CFG_GROUP_ID[sdk=appletvos*] = $(CFG_RAW_GROUP_ID)
|
||||||
|
|
|
@ -48,7 +48,7 @@ extension AppContext {
|
||||||
let remoteRepositoryBlock: (Bool) -> ProfileRepository = {
|
let remoteRepositoryBlock: (Bool) -> ProfileRepository = {
|
||||||
let remoteStore = CoreDataPersistentStore(
|
let remoteStore = CoreDataPersistentStore(
|
||||||
logger: .default,
|
logger: .default,
|
||||||
containerName: Constants.shared.containers.remote,
|
containerName: Constants.shared.containers.remoteProfiles,
|
||||||
model: AppData.cdProfilesModel,
|
model: AppData.cdProfilesModel,
|
||||||
cloudKitIdentifier: $0 ? BundleConfiguration.mainString(for: .cloudKitId) : nil,
|
cloudKitIdentifier: $0 ? BundleConfiguration.mainString(for: .cloudKitId) : nil,
|
||||||
author: nil
|
author: nil
|
||||||
|
@ -124,6 +124,7 @@ extension AppContext {
|
||||||
migrationManager: migrationManager,
|
migrationManager: migrationManager,
|
||||||
profileManager: profileManager,
|
profileManager: profileManager,
|
||||||
providerManager: providerManager,
|
providerManager: providerManager,
|
||||||
|
preferencesManager: .shared,
|
||||||
registry: .shared,
|
registry: .shared,
|
||||||
tunnel: tunnel,
|
tunnel: tunnel,
|
||||||
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
|
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
|
||||||
|
@ -199,7 +200,7 @@ private extension Dependencies.ProfileManager {
|
||||||
static func coreDataProfileRepository(observingResults: Bool) -> ProfileRepository {
|
static func coreDataProfileRepository(observingResults: Bool) -> ProfileRepository {
|
||||||
let store = CoreDataPersistentStore(
|
let store = CoreDataPersistentStore(
|
||||||
logger: .default,
|
logger: .default,
|
||||||
containerName: Constants.shared.containers.local,
|
containerName: Constants.shared.containers.localProfiles,
|
||||||
model: AppData.cdProfilesModel,
|
model: AppData.cdProfilesModel,
|
||||||
cloudKitIdentifier: nil,
|
cloudKitIdentifier: nil,
|
||||||
author: 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/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AppData
|
||||||
|
import AppDataPreferences
|
||||||
import CommonLibrary
|
import CommonLibrary
|
||||||
import CommonUtils
|
import CommonUtils
|
||||||
import CPassepartoutOpenVPNOpenSSL
|
import CPassepartoutOpenVPNOpenSSL
|
||||||
|
@ -30,8 +32,6 @@ import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import PassepartoutWireGuardGo
|
import PassepartoutWireGuardGo
|
||||||
|
|
||||||
// MARK: Registry
|
|
||||||
|
|
||||||
extension Registry {
|
extension Registry {
|
||||||
static let shared = Registry(
|
static let shared = Registry(
|
||||||
withKnownHandlers: true,
|
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,
|
migrationManager: migrationManager,
|
||||||
profileManager: profileManager,
|
profileManager: profileManager,
|
||||||
providerManager: providerManager,
|
providerManager: providerManager,
|
||||||
|
preferencesManager: PreferencesManager(),
|
||||||
registry: registry,
|
registry: registry,
|
||||||
tunnel: tunnel,
|
tunnel: tunnel,
|
||||||
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
|
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
<string>$(CFG_KEYCHAIN_GROUP_ID)</string>
|
<string>$(CFG_KEYCHAIN_GROUP_ID)</string>
|
||||||
<key>tunnelId</key>
|
<key>tunnelId</key>
|
||||||
<string>$(CFG_TUNNEL_ID)</string>
|
<string>$(CFG_TUNNEL_ID)</string>
|
||||||
|
<key>cloudKitPreferencesId</key>
|
||||||
|
<string>$(CFG_CLOUDKIT_PREFERENCES_ID)</string>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
|
Loading…
Reference in New Issue