mirror of
https://github.com/passepartoutvpn/passepartout-apple.git
synced 2025-02-02 05:52:18 +00:00
Reuse provider entities for more than VPN (#1107)
Rename entities/views, and decouple provider templates from built configurations. Reuse ProviderServerCoordinator. This is in preparation for #507
This commit is contained in:
parent
cb93ee83ea
commit
8d269e7113
@ -32,9 +32,9 @@ final class CDVPNPresetV3: NSManagedObject {
|
||||
NSFetchRequest<CDVPNPresetV3>(entityName: "CDVPNPresetV3")
|
||||
}
|
||||
|
||||
@NSManaged var providerId: String?
|
||||
@NSManaged var presetId: String?
|
||||
@NSManaged var presetDescription: String?
|
||||
@NSManaged var providerId: String?
|
||||
@NSManaged var endpoints: Data?
|
||||
@NSManaged var configurationId: String?
|
||||
@NSManaged var configuration: Data?
|
||||
|
@ -42,7 +42,7 @@ struct CoreDataMapper {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func cdServer(from server: VPNServer) throws -> CDVPNServerV3 {
|
||||
func cdServer(from server: ProviderServer) throws -> CDVPNServerV3 {
|
||||
let entity = CDVPNServerV3(context: context)
|
||||
let encoder = JSONEncoder()
|
||||
entity.serverId = server.serverId
|
||||
@ -50,27 +50,27 @@ struct CoreDataMapper {
|
||||
entity.ipAddresses = try server.ipAddresses.map {
|
||||
try encoder.encode($0)
|
||||
}
|
||||
entity.providerId = server.provider.id.rawValue
|
||||
entity.countryCode = server.provider.countryCode
|
||||
entity.categoryName = server.provider.categoryName
|
||||
entity.localizedCountry = server.provider.countryCode.localizedAsRegionCode
|
||||
entity.otherCountryCodes = server.provider.otherCountryCodes?.joined(separator: ",")
|
||||
entity.area = server.provider.area
|
||||
entity.supportedConfigurationIds = server.provider.supportedConfigurationIdentifiers?.joined(separator: ",")
|
||||
entity.supportedPresetIds = server.provider.supportedPresetIds?.joined(separator: ",")
|
||||
entity.providerId = server.metadata.providerId.rawValue
|
||||
entity.countryCode = server.metadata.countryCode
|
||||
entity.categoryName = server.metadata.categoryName
|
||||
entity.localizedCountry = server.metadata.countryCode.localizedAsRegionCode
|
||||
entity.otherCountryCodes = server.metadata.otherCountryCodes?.joined(separator: ",")
|
||||
entity.area = server.metadata.area
|
||||
entity.supportedConfigurationIds = server.metadata.supportedConfigurationIdentifiers?.joined(separator: ",")
|
||||
entity.supportedPresetIds = server.metadata.supportedPresetIds?.joined(separator: ",")
|
||||
return entity
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func cdPreset(from preset: AnyVPNPreset) throws -> CDVPNPresetV3 {
|
||||
func cdPreset(from preset: AnyProviderPreset) throws -> CDVPNPresetV3 {
|
||||
let entity = CDVPNPresetV3(context: self.context)
|
||||
let encoder = JSONEncoder()
|
||||
entity.presetId = preset.presetId
|
||||
entity.providerId = preset.providerId.rawValue
|
||||
entity.presetId = preset.presetId
|
||||
entity.presetDescription = preset.description
|
||||
entity.endpoints = try encoder.encode(preset.endpoints)
|
||||
entity.configurationId = preset.configurationIdentifier
|
||||
entity.configuration = preset.configuration
|
||||
entity.configuration = preset.template
|
||||
return entity
|
||||
}
|
||||
}
|
||||
|
@ -63,12 +63,12 @@ struct DomainMapper {
|
||||
}
|
||||
}
|
||||
|
||||
func preset(from entity: CDVPNPresetV3) throws -> AnyVPNPreset? {
|
||||
func preset(from entity: CDVPNPresetV3) throws -> AnyProviderPreset? {
|
||||
guard let presetId = entity.presetId,
|
||||
let presetDescription = entity.presetDescription,
|
||||
let providerId = entity.providerId,
|
||||
let configurationId = entity.configurationId,
|
||||
let configuration = entity.configuration else {
|
||||
let template = entity.configuration else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -77,17 +77,17 @@ struct DomainMapper {
|
||||
try decoder.decode([EndpointProtocol].self, from: $0)
|
||||
} ?? []
|
||||
|
||||
return AnyVPNPreset(
|
||||
return AnyProviderPreset(
|
||||
providerId: .init(rawValue: providerId),
|
||||
presetId: presetId,
|
||||
description: presetDescription,
|
||||
endpoints: endpoints,
|
||||
configurationIdentifier: configurationId,
|
||||
configuration: configuration
|
||||
template: template
|
||||
)
|
||||
}
|
||||
|
||||
func server(from entity: CDVPNServerV3) throws -> VPNServer? {
|
||||
func server(from entity: CDVPNServerV3) throws -> ProviderServer? {
|
||||
guard let serverId = entity.serverId,
|
||||
let providerId = entity.providerId,
|
||||
let categoryName = entity.categoryName,
|
||||
@ -105,8 +105,8 @@ struct DomainMapper {
|
||||
let otherCountryCodes = entity.otherCountryCodes?.components(separatedBy: ",")
|
||||
let area = entity.area
|
||||
|
||||
let provider = VPNServer.Provider(
|
||||
id: .init(rawValue: providerId),
|
||||
let metadata = ProviderServer.Metadata(
|
||||
providerId: .init(rawValue: providerId),
|
||||
serverId: serverId,
|
||||
supportedConfigurationIdentifiers: supportedConfigurationIds,
|
||||
supportedPresetIds: supportedPresetIds,
|
||||
@ -115,6 +115,6 @@ struct DomainMapper {
|
||||
otherCountryCodes: otherCountryCodes,
|
||||
area: area
|
||||
)
|
||||
return VPNServer(provider: provider, hostname: hostname, ipAddresses: ipAddresses)
|
||||
return ProviderServer(metadata: metadata, hostname: hostname, ipAddresses: ipAddresses)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// VPNServerParameters+CoreData.swift
|
||||
// ProviderServerParameters+CoreData.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/28/24.
|
||||
@ -40,7 +40,7 @@ extension ProviderID {
|
||||
}
|
||||
}
|
||||
|
||||
extension VPNSortField {
|
||||
extension ProviderSortField {
|
||||
var sortDescriptor: NSSortDescriptor {
|
||||
switch self {
|
||||
case .localizedCountry:
|
||||
@ -58,7 +58,7 @@ extension VPNSortField {
|
||||
}
|
||||
}
|
||||
|
||||
extension VPNFilters {
|
||||
extension ProviderFilters {
|
||||
func predicate(for providerId: ProviderID) -> NSPredicate {
|
||||
var formats: [String] = []
|
||||
var args: [Any] = []
|
@ -0,0 +1,173 @@
|
||||
//
|
||||
// CDAPIRepositoryV3.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/26/24.
|
||||
// Copyright (c) 2025 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 Combine
|
||||
import CommonUtils
|
||||
import CoreData
|
||||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
extension AppData {
|
||||
public static func cdAPIRepositoryV3(context: NSManagedObjectContext) -> APIRepository {
|
||||
CDAPIRepositoryV3(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
private final class CDAPIRepositoryV3: NSObject, APIRepository {
|
||||
private nonisolated let context: NSManagedObjectContext
|
||||
|
||||
private nonisolated let providersSubject: CurrentValueSubject<[Provider], Never>
|
||||
|
||||
private nonisolated let lastUpdateSubject: CurrentValueSubject<[ProviderID: Date], Never>
|
||||
|
||||
private nonisolated let providersController: NSFetchedResultsController<CDProviderV3>
|
||||
|
||||
init(context: NSManagedObjectContext) {
|
||||
self.context = context
|
||||
providersSubject = CurrentValueSubject([])
|
||||
lastUpdateSubject = CurrentValueSubject([:])
|
||||
|
||||
let request = CDProviderV3.fetchRequest()
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(key: "providerId", ascending: true)
|
||||
]
|
||||
providersController = .init(
|
||||
fetchRequest: request,
|
||||
managedObjectContext: context,
|
||||
sectionNameKeyPath: nil,
|
||||
cacheName: nil
|
||||
)
|
||||
|
||||
super.init()
|
||||
|
||||
Task {
|
||||
try await context.perform { [weak self] in
|
||||
self?.providersController.delegate = self
|
||||
try self?.providersController.performFetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated var indexPublisher: AnyPublisher<[Provider], Never> {
|
||||
providersSubject
|
||||
.removeDuplicates()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
nonisolated var lastUpdatePublisher: AnyPublisher<[ProviderID: Date], Never> {
|
||||
lastUpdateSubject
|
||||
.removeDuplicates()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func store(_ index: [Provider]) async throws {
|
||||
try await context.perform { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
// fetch existing for last update and deletion
|
||||
let request = CDProviderV3.fetchRequest()
|
||||
let results = try request.execute()
|
||||
let lastUpdatesByProvider = results.reduce(into: [:]) {
|
||||
$0[$1.providerId] = $1.lastUpdate
|
||||
}
|
||||
results.forEach(context.delete)
|
||||
|
||||
// replace but retain last update
|
||||
let mapper = CoreDataMapper(context: context)
|
||||
try index.forEach {
|
||||
let lastUpdate = lastUpdatesByProvider[$0.id.rawValue]
|
||||
try mapper.cdProvider(from: $0, lastUpdate: lastUpdate)
|
||||
}
|
||||
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func store(_ infrastructure: ProviderInfrastructure, for providerId: ProviderID) async throws {
|
||||
try await context.perform { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
let predicate = providerId.predicate
|
||||
|
||||
// signal update of related provider
|
||||
let providerRequest = CDProviderV3.fetchRequest()
|
||||
providerRequest.predicate = predicate
|
||||
let providers = try providerRequest.execute()
|
||||
if let provider = providers.first {
|
||||
provider.lastUpdate = infrastructure.lastUpdate
|
||||
}
|
||||
|
||||
// delete all provider entities
|
||||
let serverRequest = CDVPNServerV3.fetchRequest()
|
||||
serverRequest.predicate = predicate
|
||||
let servers = try serverRequest.execute()
|
||||
servers.forEach(context.delete)
|
||||
|
||||
let presetRequest = CDVPNPresetV3.fetchRequest()
|
||||
presetRequest.predicate = predicate
|
||||
let presets = try presetRequest.execute()
|
||||
presets.forEach(context.delete)
|
||||
|
||||
// create new entities
|
||||
let mapper = CoreDataMapper(context: context)
|
||||
try infrastructure.servers.forEach {
|
||||
try mapper.cdServer(from: $0)
|
||||
}
|
||||
try infrastructure.presets.forEach {
|
||||
try mapper.cdPreset(from: $0)
|
||||
}
|
||||
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func providerRepository(for providerId: ProviderID) -> ProviderRepository {
|
||||
CDProviderRepositoryV3(context: context, providerId: providerId)
|
||||
}
|
||||
}
|
||||
|
||||
extension CDAPIRepositoryV3: NSFetchedResultsControllerDelegate {
|
||||
nonisolated func controllerDidChangeContent(_ controller: NSFetchedResultsController<any NSFetchRequestResult>) {
|
||||
guard let entities = controller.fetchedObjects as? [CDProviderV3] else {
|
||||
return
|
||||
}
|
||||
let mapper = DomainMapper()
|
||||
providersSubject.send(entities.compactMap(mapper.provider(from:)))
|
||||
lastUpdateSubject.send(mapper.lastUpdate(from: entities))
|
||||
}
|
||||
}
|
@ -24,150 +24,73 @@
|
||||
//
|
||||
|
||||
import AppData
|
||||
import Combine
|
||||
import CommonUtils
|
||||
import CoreData
|
||||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
extension AppData {
|
||||
public static func cdProviderRepositoryV3(context: NSManagedObjectContext) -> ProviderRepository {
|
||||
CDProviderRepositoryV3(context: context)
|
||||
}
|
||||
}
|
||||
final class CDProviderRepositoryV3: ProviderRepository {
|
||||
private let context: NSManagedObjectContext
|
||||
|
||||
private final class CDProviderRepositoryV3: NSObject, ProviderRepository {
|
||||
private nonisolated let context: NSManagedObjectContext
|
||||
let providerId: ProviderID
|
||||
|
||||
private nonisolated let providersSubject: CurrentValueSubject<[Provider], Never>
|
||||
|
||||
private nonisolated let lastUpdateSubject: CurrentValueSubject<[ProviderID: Date], Never>
|
||||
|
||||
private nonisolated let providersController: NSFetchedResultsController<CDProviderV3>
|
||||
|
||||
init(context: NSManagedObjectContext) {
|
||||
init(context: NSManagedObjectContext, providerId: ProviderID) {
|
||||
self.context = context
|
||||
providersSubject = CurrentValueSubject([])
|
||||
lastUpdateSubject = CurrentValueSubject([:])
|
||||
self.providerId = providerId
|
||||
}
|
||||
|
||||
let request = CDProviderV3.fetchRequest()
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(key: "providerId", ascending: true)
|
||||
]
|
||||
providersController = .init(
|
||||
fetchRequest: request,
|
||||
managedObjectContext: context,
|
||||
sectionNameKeyPath: nil,
|
||||
cacheName: nil
|
||||
)
|
||||
func availableOptions<Template>(for templateType: Template.Type) async throws -> ProviderFilterOptions where Template: IdentifiableConfiguration {
|
||||
try await context.perform {
|
||||
let mapper = DomainMapper()
|
||||
|
||||
super.init()
|
||||
let serversRequest = NSFetchRequest<NSDictionary>(entityName: "CDVPNServerV3")
|
||||
serversRequest.predicate = self.providerId.predicate
|
||||
serversRequest.resultType = .dictionaryResultType
|
||||
serversRequest.returnsDistinctResults = true
|
||||
serversRequest.propertiesToFetch = [
|
||||
"categoryName",
|
||||
"countryCode"
|
||||
]
|
||||
let serversResults = try serversRequest.execute()
|
||||
|
||||
Task {
|
||||
try await context.perform { [weak self] in
|
||||
self?.providersController.delegate = self
|
||||
try self?.providersController.performFetch()
|
||||
var countriesByCategoryName: [String: Set<String>] = [:]
|
||||
var countryCodes: Set<String> = []
|
||||
serversResults.forEach {
|
||||
guard let categoryName = $0.object(forKey: "categoryName") as? String,
|
||||
let countryCode = $0.object(forKey: "countryCode") as? String else {
|
||||
return
|
||||
}
|
||||
var codes: Set<String> = countriesByCategoryName[categoryName] ?? []
|
||||
codes.insert(countryCode)
|
||||
countriesByCategoryName[categoryName] = codes
|
||||
countryCodes.insert(countryCode)
|
||||
}
|
||||
|
||||
let presetsRequest = CDVPNPresetV3.fetchRequest()
|
||||
presetsRequest.predicate = NSPredicate(
|
||||
format: "providerId == %@ AND configurationId == %@", self.providerId.rawValue,
|
||||
Template.configurationIdentifier
|
||||
)
|
||||
let presetsResults = try presetsRequest.execute()
|
||||
|
||||
return ProviderFilterOptions(
|
||||
countriesByCategoryName: countriesByCategoryName,
|
||||
countryCodes: Set(countryCodes),
|
||||
presets: Set(try presetsResults.compactMap {
|
||||
try mapper.preset(from: $0)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated var indexPublisher: AnyPublisher<[Provider], Never> {
|
||||
providersSubject
|
||||
.removeDuplicates()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
nonisolated var lastUpdatePublisher: AnyPublisher<[ProviderID: Date], Never> {
|
||||
lastUpdateSubject
|
||||
.removeDuplicates()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func store(_ index: [Provider]) async throws {
|
||||
try await context.perform { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
// fetch existing for last update and deletion
|
||||
let request = CDProviderV3.fetchRequest()
|
||||
let results = try request.execute()
|
||||
let lastUpdatesByProvider = results.reduce(into: [:]) {
|
||||
$0[$1.providerId] = $1.lastUpdate
|
||||
}
|
||||
results.forEach(context.delete)
|
||||
|
||||
// replace but retain last update
|
||||
let mapper = CoreDataMapper(context: context)
|
||||
try index.forEach {
|
||||
let lastUpdate = lastUpdatesByProvider[$0.id.rawValue]
|
||||
try mapper.cdProvider(from: $0, lastUpdate: lastUpdate)
|
||||
}
|
||||
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
throw error
|
||||
}
|
||||
func filteredServers(with parameters: ProviderServerParameters?) async throws -> [ProviderServer] {
|
||||
try await context.perform {
|
||||
let request = CDVPNServerV3.fetchRequest()
|
||||
request.sortDescriptors = parameters?.sorting.map(\.sortDescriptor)
|
||||
request.predicate = parameters?.filters.predicate(for: self.providerId)
|
||||
let results = try request.execute()
|
||||
let mapper = DomainMapper()
|
||||
return try results.compactMap(mapper.server(from:))
|
||||
}
|
||||
}
|
||||
|
||||
func store(_ infrastructure: VPNInfrastructure, for providerId: ProviderID) async throws {
|
||||
try await context.perform { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
let predicate = providerId.predicate
|
||||
|
||||
// signal update of related provider
|
||||
let providerRequest = CDProviderV3.fetchRequest()
|
||||
providerRequest.predicate = predicate
|
||||
let providers = try providerRequest.execute()
|
||||
if let provider = providers.first {
|
||||
provider.lastUpdate = infrastructure.lastUpdate
|
||||
}
|
||||
|
||||
// delete all provider entities
|
||||
let serverRequest = CDVPNServerV3.fetchRequest()
|
||||
serverRequest.predicate = predicate
|
||||
let servers = try serverRequest.execute()
|
||||
servers.forEach(context.delete)
|
||||
|
||||
let presetRequest = CDVPNPresetV3.fetchRequest()
|
||||
presetRequest.predicate = predicate
|
||||
let presets = try presetRequest.execute()
|
||||
presets.forEach(context.delete)
|
||||
|
||||
// create new entities
|
||||
let mapper = CoreDataMapper(context: context)
|
||||
try infrastructure.servers.forEach {
|
||||
try mapper.cdServer(from: $0)
|
||||
}
|
||||
try infrastructure.presets.forEach {
|
||||
try mapper.cdPreset(from: $0)
|
||||
}
|
||||
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func vpnServerRepository(for providerId: ProviderID) -> VPNProviderServerRepository {
|
||||
CDVPNProviderServerRepositoryV3(context: context, providerId: providerId)
|
||||
}
|
||||
}
|
||||
|
||||
extension CDProviderRepositoryV3: NSFetchedResultsControllerDelegate {
|
||||
nonisolated func controllerDidChangeContent(_ controller: NSFetchedResultsController<any NSFetchRequestResult>) {
|
||||
guard let entities = controller.fetchedObjects as? [CDProviderV3] else {
|
||||
return
|
||||
}
|
||||
let mapper = DomainMapper()
|
||||
providersSubject.send(entities.compactMap(mapper.provider(from:)))
|
||||
lastUpdateSubject.send(mapper.lastUpdate(from: entities))
|
||||
}
|
||||
}
|
||||
|
@ -1,96 +0,0 @@
|
||||
//
|
||||
// CDVPNProviderServerRepositoryV3.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/26/24.
|
||||
// Copyright (c) 2025 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 CommonUtils
|
||||
import CoreData
|
||||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
final class CDVPNProviderServerRepositoryV3: VPNProviderServerRepository {
|
||||
private let context: NSManagedObjectContext
|
||||
|
||||
let providerId: ProviderID
|
||||
|
||||
init(context: NSManagedObjectContext, providerId: ProviderID) {
|
||||
self.context = context
|
||||
self.providerId = providerId
|
||||
}
|
||||
|
||||
func availableOptions<Configuration>(for configurationType: Configuration.Type) async throws -> VPNFilterOptions where Configuration: IdentifiableConfiguration {
|
||||
try await context.perform {
|
||||
let mapper = DomainMapper()
|
||||
|
||||
let serversRequest = NSFetchRequest<NSDictionary>(entityName: "CDVPNServerV3")
|
||||
serversRequest.predicate = self.providerId.predicate
|
||||
serversRequest.resultType = .dictionaryResultType
|
||||
serversRequest.returnsDistinctResults = true
|
||||
serversRequest.propertiesToFetch = [
|
||||
"categoryName",
|
||||
"countryCode"
|
||||
]
|
||||
let serversResults = try serversRequest.execute()
|
||||
|
||||
var countriesByCategoryName: [String: Set<String>] = [:]
|
||||
var countryCodes: Set<String> = []
|
||||
serversResults.forEach {
|
||||
guard let categoryName = $0.object(forKey: "categoryName") as? String,
|
||||
let countryCode = $0.object(forKey: "countryCode") as? String else {
|
||||
return
|
||||
}
|
||||
var codes: Set<String> = countriesByCategoryName[categoryName] ?? []
|
||||
codes.insert(countryCode)
|
||||
countriesByCategoryName[categoryName] = codes
|
||||
countryCodes.insert(countryCode)
|
||||
}
|
||||
|
||||
let presetsRequest = CDVPNPresetV3.fetchRequest()
|
||||
presetsRequest.predicate = NSPredicate(
|
||||
format: "providerId == %@ AND configurationId == %@", self.providerId.rawValue,
|
||||
Configuration.configurationIdentifier
|
||||
)
|
||||
let presetsResults = try presetsRequest.execute()
|
||||
|
||||
return VPNFilterOptions(
|
||||
countriesByCategoryName: countriesByCategoryName,
|
||||
countryCodes: Set(countryCodes),
|
||||
presets: Set(try presetsResults.compactMap {
|
||||
try mapper.preset(from: $0)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func filteredServers(with parameters: VPNServerParameters?) async throws -> [VPNServer] {
|
||||
try await context.perform {
|
||||
let request = CDVPNServerV3.fetchRequest()
|
||||
request.sortDescriptors = parameters?.sorting.map(\.sortDescriptor)
|
||||
request.predicate = parameters?.filters.predicate(for: self.providerId)
|
||||
let results = try request.execute()
|
||||
let mapper = DomainMapper()
|
||||
return try results.compactMap(mapper.server(from:))
|
||||
}
|
||||
}
|
||||
}
|
@ -53,10 +53,10 @@ private extension AppUIMain {
|
||||
fatalError("\(moduleType): is not ModuleViewProviding")
|
||||
}
|
||||
|
||||
// ProviderEntityViewProviding
|
||||
// ProviderServerCoordinatorSupporting
|
||||
if providerModuleTypes.contains(moduleType) {
|
||||
guard module is any ProviderEntityViewProviding else {
|
||||
fatalError("\(moduleType): is not ProviderEntityViewProviding")
|
||||
guard module is any ProviderServerCoordinatorSupporting else {
|
||||
fatalError("\(moduleType): is not ProviderServerCoordinatorSupporting")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
@ -114,7 +114,7 @@ private extension AddProfileMenu {
|
||||
private struct ProvidersSubmenu: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var providerManager: ProviderManager
|
||||
private var apiManager: APIManager
|
||||
|
||||
let moduleType: ModuleType
|
||||
|
||||
@ -124,7 +124,7 @@ private struct ProvidersSubmenu: View {
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
ForEach(providerManager.providers, content: profileButton(for:))
|
||||
ForEach(apiManager.providers, content: profileButton(for:))
|
||||
} label: {
|
||||
Text(moduleType.localizedDescription)
|
||||
}
|
||||
|
@ -191,7 +191,7 @@ extension AppCoordinator {
|
||||
)
|
||||
|
||||
case .editProviderEntity(let profile, let force, let module):
|
||||
ProviderEntitySelector(
|
||||
ProviderServerCoordinatorIfSupported(
|
||||
module: module,
|
||||
errorHandler: errorHandler,
|
||||
selectTitle: profile.providerServerSelectionTitle,
|
||||
@ -226,6 +226,30 @@ extension AppCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Providers
|
||||
|
||||
private struct ProviderServerCoordinatorIfSupported: View {
|
||||
let module: Module
|
||||
|
||||
let errorHandler: ErrorHandler
|
||||
|
||||
let selectTitle: String
|
||||
|
||||
let onSelect: (Module) async throws -> Void
|
||||
|
||||
var body: some View {
|
||||
if let supporting = module as? any ProviderServerCoordinatorSupporting {
|
||||
supporting.providerServerCoordinator(
|
||||
selectTitle: selectTitle,
|
||||
onSelect: onSelect,
|
||||
errorHandler: errorHandler
|
||||
)
|
||||
} else {
|
||||
fatalError("Module got too far without being ProviderServerCoordinatorSupporting: \(module)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Handlers
|
||||
|
||||
extension AppCoordinator {
|
||||
|
@ -133,7 +133,7 @@ private extension InstalledProfileView {
|
||||
Button {
|
||||
flow?.connectionFlow?.onProviderEntityRequired(profile!) // never nil due to .map
|
||||
} label: {
|
||||
providerSelectorLabel(with: selection.entity?.header)
|
||||
providerSelectorLabel(with: selection.entityHeader)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ import SwiftUI
|
||||
struct ReportIssueButton {
|
||||
|
||||
@EnvironmentObject
|
||||
private var providerManager: ProviderManager
|
||||
private var apiManager: APIManager
|
||||
|
||||
@ObservedObject
|
||||
var profileManager: ProfileManager
|
||||
@ -61,7 +61,7 @@ struct ReportIssueButton {
|
||||
guard let id = installedProfile?.selectedProvider?.selection.id else {
|
||||
return nil
|
||||
}
|
||||
let lastUpdate = providerManager.lastUpdate(for: id)
|
||||
let lastUpdate = apiManager.lastUpdate(for: id)
|
||||
return (id, lastUpdate)
|
||||
}
|
||||
}
|
||||
|
@ -34,26 +34,5 @@ extension OpenVPNModule.Builder: ModuleViewProviding {
|
||||
}
|
||||
}
|
||||
|
||||
extension OpenVPNModule: ProviderEntityViewProviding {
|
||||
public func providerEntityView(
|
||||
errorHandler: ErrorHandler,
|
||||
selectTitle: String,
|
||||
onSelect: @escaping (Module) async throws -> Void
|
||||
) -> some View {
|
||||
providerSelection.map {
|
||||
VPNProviderServerCoordinator(
|
||||
moduleId: id,
|
||||
providerId: $0.id,
|
||||
selectedEntity: $0.entity,
|
||||
selectTitle: selectTitle,
|
||||
onSelect: {
|
||||
var newBuilder = builder()
|
||||
newBuilder.providerEntity = $0
|
||||
let newModule = try newBuilder.tryBuild()
|
||||
try await onSelect(newModule)
|
||||
},
|
||||
errorHandler: errorHandler
|
||||
)
|
||||
}
|
||||
}
|
||||
extension OpenVPNModule: ProviderServerCoordinatorSupporting {
|
||||
}
|
||||
|
@ -34,26 +34,5 @@ extension WireGuardModule.Builder: ModuleViewProviding {
|
||||
}
|
||||
}
|
||||
|
||||
extension WireGuardModule: ProviderEntityViewProviding {
|
||||
public func providerEntityView(
|
||||
errorHandler: ErrorHandler,
|
||||
selectTitle: String,
|
||||
onSelect: @escaping (Module) async throws -> Void
|
||||
) -> some View {
|
||||
providerSelection.map {
|
||||
VPNProviderServerCoordinator(
|
||||
moduleId: id,
|
||||
providerId: $0.id,
|
||||
selectedEntity: $0.entity,
|
||||
selectTitle: selectTitle,
|
||||
onSelect: {
|
||||
var newBuilder = builder()
|
||||
newBuilder.providerEntity = $0
|
||||
let newModule = try newBuilder.tryBuild()
|
||||
try await onSelect(newModule)
|
||||
},
|
||||
errorHandler: errorHandler
|
||||
)
|
||||
}
|
||||
}
|
||||
extension WireGuardModule: ProviderServerCoordinatorSupporting {
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ private extension OpenVPNView {
|
||||
}
|
||||
|
||||
var providerModifier: some ViewModifier {
|
||||
VPNProviderContentModifier(
|
||||
ProviderContentModifier(
|
||||
providerId: providerId,
|
||||
providerPreferences: nil,
|
||||
selectedEntity: providerEntity,
|
||||
@ -157,7 +157,7 @@ private extension OpenVPNView {
|
||||
switch route {
|
||||
case .providerServer:
|
||||
draft.wrappedValue.providerSelection.map {
|
||||
VPNProviderServerView(
|
||||
ProviderServerView(
|
||||
moduleId: module.id,
|
||||
providerId: $0.id,
|
||||
selectedEntity: $0.entity,
|
||||
@ -215,8 +215,8 @@ private extension OpenVPNView {
|
||||
}
|
||||
}
|
||||
|
||||
func onSelectServer(server: VPNServer, preset: VPNPreset<OpenVPN.Configuration>) {
|
||||
draft.wrappedValue.providerEntity = VPNEntity(server: server, preset: preset)
|
||||
func onSelectServer(server: ProviderServer, preset: ProviderPreset<OpenVPNProviderTemplate>) {
|
||||
draft.wrappedValue.providerEntity = ProviderEntity(server: server, preset: preset)
|
||||
resetExcludedEndpointsWithCurrentProviderEntity()
|
||||
path.wrappedValue.removeLast()
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ private extension WireGuardView {
|
||||
}
|
||||
|
||||
var providerModifier: some ViewModifier {
|
||||
VPNProviderContentModifier(
|
||||
ProviderContentModifier(
|
||||
providerId: providerId,
|
||||
providerPreferences: nil,
|
||||
selectedEntity: providerEntity,
|
||||
@ -90,8 +90,8 @@ private extension WireGuardView {
|
||||
}
|
||||
|
||||
private extension WireGuardView {
|
||||
func onSelectServer(server: VPNServer, preset: VPNPreset<WireGuard.Configuration>) {
|
||||
draft.wrappedValue.providerEntity = VPNEntity(server: server, preset: preset)
|
||||
func onSelectServer(server: ProviderServer, preset: ProviderPreset<WireGuardProviderTemplate>) {
|
||||
draft.wrappedValue.providerEntity = ProviderEntity(server: server, preset: preset)
|
||||
path.wrappedValue.removeLast()
|
||||
}
|
||||
|
||||
@ -114,7 +114,7 @@ private extension WireGuardView {
|
||||
switch route {
|
||||
case .providerServer:
|
||||
draft.wrappedValue.providerSelection.map {
|
||||
VPNProviderServerView(
|
||||
ProviderServerView(
|
||||
moduleId: module.id,
|
||||
providerId: $0.id,
|
||||
selectedEntity: $0.entity,
|
||||
|
@ -0,0 +1,226 @@
|
||||
//
|
||||
// APIContentModifier.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/14/24.
|
||||
// Copyright (c) 2025 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 CommonAPI
|
||||
import CommonLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
import UILibrary
|
||||
|
||||
struct APIContentModifier<Template, ProviderRows>: ViewModifier where Template: IdentifiableConfiguration, ProviderRows: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var apiManager: APIManager
|
||||
|
||||
@EnvironmentObject
|
||||
private var preferencesManager: PreferencesManager
|
||||
|
||||
let apis: [APIMapper]
|
||||
|
||||
@Binding
|
||||
var providerId: ProviderID?
|
||||
|
||||
let providerPreferences: ProviderPreferences?
|
||||
|
||||
let templateType: Template.Type
|
||||
|
||||
@ViewBuilder
|
||||
let providerRows: ProviderRows
|
||||
|
||||
let onSelectProvider: (APIManager, ProviderID?, _ isInitial: Bool) -> Void
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
providerView
|
||||
.onLoad(perform: loadCurrentProvider)
|
||||
.onChange(of: providerId) { newId in
|
||||
Task {
|
||||
if let newId {
|
||||
await refreshInfrastructure(for: newId)
|
||||
}
|
||||
loadPreferences(for: newId)
|
||||
onSelectProvider(apiManager, newId, false)
|
||||
}
|
||||
}
|
||||
.onDisappear(perform: savePreferences)
|
||||
.disabled(apiManager.isLoading)
|
||||
|
||||
content
|
||||
}
|
||||
|
||||
static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.providerId == rhs.providerId
|
||||
}
|
||||
}
|
||||
|
||||
private extension APIContentModifier {
|
||||
|
||||
#if os(iOS)
|
||||
@ViewBuilder
|
||||
var providerView: some View {
|
||||
providerPicker
|
||||
.themeSection()
|
||||
|
||||
if let providerId {
|
||||
Group {
|
||||
providerRows
|
||||
RefreshInfrastructureButton(apis: apis, providerId: providerId)
|
||||
}
|
||||
.themeSection(footer: lastUpdatedString)
|
||||
}
|
||||
}
|
||||
#else
|
||||
@ViewBuilder
|
||||
var providerView: some View {
|
||||
Section {
|
||||
providerPicker
|
||||
}
|
||||
if let providerId {
|
||||
Section {
|
||||
providerRows
|
||||
HStack {
|
||||
lastUpdatedString.map {
|
||||
Text($0)
|
||||
.themeSubtitle()
|
||||
}
|
||||
Spacer()
|
||||
RefreshInfrastructureButton(apis: apis, providerId: providerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
var providerPicker: some View {
|
||||
ProviderPicker(
|
||||
providers: supportedProviders,
|
||||
providerId: $providerId,
|
||||
isRequired: true,
|
||||
isLoading: apiManager.isLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private extension APIContentModifier {
|
||||
var supportedProviders: [Provider] {
|
||||
apiManager
|
||||
.providers
|
||||
.filter {
|
||||
$0.supports(Template.self)
|
||||
}
|
||||
}
|
||||
|
||||
var lastUpdate: Date? {
|
||||
guard let providerId else {
|
||||
return nil
|
||||
}
|
||||
return apiManager.lastUpdate(for: providerId)
|
||||
}
|
||||
|
||||
var lastUpdatedString: String? {
|
||||
guard let lastUpdate else {
|
||||
return apiManager.isLoading ? Strings.Views.Providers.LastUpdated.loading : nil
|
||||
}
|
||||
return Strings.Views.Providers.lastUpdated(lastUpdate.localizedDescription(style: .timestamp))
|
||||
}
|
||||
|
||||
func loadCurrentProvider() {
|
||||
Task {
|
||||
await refreshIndex()
|
||||
if let providerId {
|
||||
onSelectProvider(apiManager, providerId, true)
|
||||
loadPreferences(for: providerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func refreshIndex() async -> Bool {
|
||||
do {
|
||||
try await apiManager.fetchIndex(from: apis)
|
||||
return true
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to fetch index: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func refreshInfrastructure(for providerId: ProviderID) async -> Bool {
|
||||
do {
|
||||
try await apiManager.fetchInfrastructure(from: apis, for: providerId)
|
||||
return true
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to refresh infrastructure: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func loadPreferences(for providerId: ProviderID?) {
|
||||
guard let providerPreferences else {
|
||||
return
|
||||
}
|
||||
if let providerId {
|
||||
do {
|
||||
pp_log(.app, .debug, "Load preferences for provider \(providerId)")
|
||||
let repository = try preferencesManager.preferencesRepository(forProviderWithId: providerId)
|
||||
providerPreferences.setRepository(repository)
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(error)")
|
||||
providerPreferences.setRepository(nil)
|
||||
}
|
||||
} else {
|
||||
providerPreferences.setRepository(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func savePreferences() {
|
||||
guard let providerPreferences else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
pp_log(.app, .debug, "Save preferences for provider \(providerId.debugDescription)")
|
||||
try providerPreferences.save()
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to save preferences for provider \(providerId.debugDescription): \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
List {
|
||||
EmptyView()
|
||||
.modifier(APIContentModifier(
|
||||
apis: [API.bundled],
|
||||
providerId: .constant(.hideme),
|
||||
providerPreferences: nil,
|
||||
templateType: OpenVPNProviderTemplate.self,
|
||||
providerRows: {},
|
||||
onSelectProvider: { _, _, _ in }
|
||||
))
|
||||
}
|
||||
.withMockEnvironment()
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
// ProviderContentModifier.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/14/24.
|
||||
// Created by Davide De Rosa on 10/7/24.
|
||||
// Copyright (c) 2025 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
@ -27,183 +27,59 @@ import CommonAPI
|
||||
import CommonLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
import UILibrary
|
||||
|
||||
struct ProviderContentModifier<Entity, ProviderRows>: ViewModifier where Entity: ProviderEntity, ProviderRows: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var providerManager: ProviderManager
|
||||
|
||||
@EnvironmentObject
|
||||
private var preferencesManager: PreferencesManager
|
||||
|
||||
let apis: [APIMapper]
|
||||
struct ProviderContentModifier<Template, ProviderRows>: ViewModifier where Template: IdentifiableConfiguration, ProviderRows: View {
|
||||
var apis: [APIMapper] = API.shared
|
||||
|
||||
@Binding
|
||||
var providerId: ProviderID?
|
||||
|
||||
let providerPreferences: ProviderPreferences?
|
||||
|
||||
let entityType: Entity.Type
|
||||
@Binding
|
||||
var selectedEntity: ProviderEntity<Template>?
|
||||
|
||||
let entityDestination: any Hashable
|
||||
|
||||
@ViewBuilder
|
||||
let providerRows: ProviderRows
|
||||
|
||||
let onSelectProvider: (ProviderManager, ProviderID?, _ isInitial: Bool) -> Void
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
providerView
|
||||
.onLoad(perform: loadCurrentProvider)
|
||||
.onChange(of: providerId) { newId in
|
||||
Task {
|
||||
if let newId {
|
||||
await refreshInfrastructure(for: newId)
|
||||
}
|
||||
loadPreferences(for: newId)
|
||||
onSelectProvider(providerManager, newId, false)
|
||||
}
|
||||
}
|
||||
.onDisappear(perform: savePreferences)
|
||||
.disabled(providerManager.isLoading)
|
||||
|
||||
content
|
||||
}
|
||||
|
||||
static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.providerId == rhs.providerId
|
||||
debugChanges()
|
||||
return content
|
||||
.modifier(APIContentModifier(
|
||||
apis: apis,
|
||||
providerId: $providerId,
|
||||
providerPreferences: providerPreferences,
|
||||
templateType: Template.self,
|
||||
providerRows: {
|
||||
providerEntityRow
|
||||
providerRows
|
||||
},
|
||||
onSelectProvider: onSelectProvider
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private extension ProviderContentModifier {
|
||||
|
||||
#if os(iOS)
|
||||
@ViewBuilder
|
||||
var providerView: some View {
|
||||
providerPicker
|
||||
.themeSection()
|
||||
|
||||
if let providerId {
|
||||
Group {
|
||||
providerRows
|
||||
RefreshInfrastructureButton(apis: apis, providerId: providerId)
|
||||
}
|
||||
.themeSection(footer: lastUpdatedString)
|
||||
}
|
||||
}
|
||||
#else
|
||||
@ViewBuilder
|
||||
var providerView: some View {
|
||||
Section {
|
||||
providerPicker
|
||||
}
|
||||
if let providerId {
|
||||
Section {
|
||||
providerRows
|
||||
HStack {
|
||||
lastUpdatedString.map {
|
||||
Text($0)
|
||||
.themeSubtitle()
|
||||
}
|
||||
var providerEntityRow: some View {
|
||||
NavigationLink(value: entityDestination) {
|
||||
HStack {
|
||||
Text(Strings.Global.Nouns.server)
|
||||
if let selectedEntity {
|
||||
Spacer()
|
||||
RefreshInfrastructureButton(apis: apis, providerId: providerId)
|
||||
Text(selectedEntity.server.hostname ?? selectedEntity.server.serverId)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
var providerPicker: some View {
|
||||
ProviderPicker(
|
||||
providers: supportedProviders,
|
||||
providerId: $providerId,
|
||||
isRequired: true,
|
||||
isLoading: providerManager.isLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private extension ProviderContentModifier {
|
||||
var supportedProviders: [Provider] {
|
||||
providerManager
|
||||
.providers
|
||||
.filter {
|
||||
$0.supports(Entity.Template.self)
|
||||
}
|
||||
}
|
||||
|
||||
var lastUpdate: Date? {
|
||||
guard let providerId else {
|
||||
return nil
|
||||
}
|
||||
return providerManager.lastUpdate(for: providerId)
|
||||
}
|
||||
|
||||
var lastUpdatedString: String? {
|
||||
guard let lastUpdate else {
|
||||
return providerManager.isLoading ? Strings.Views.Providers.LastUpdated.loading : nil
|
||||
}
|
||||
return Strings.Views.Providers.lastUpdated(lastUpdate.localizedDescription(style: .timestamp))
|
||||
}
|
||||
|
||||
func loadCurrentProvider() {
|
||||
Task {
|
||||
await refreshIndex()
|
||||
if let providerId {
|
||||
onSelectProvider(providerManager, providerId, true)
|
||||
loadPreferences(for: providerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func refreshIndex() async -> Bool {
|
||||
do {
|
||||
try await providerManager.fetchIndex(from: apis)
|
||||
return true
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to fetch index: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func refreshInfrastructure(for providerId: ProviderID) async -> Bool {
|
||||
do {
|
||||
try await providerManager.fetchVPNInfrastructure(from: apis, for: providerId)
|
||||
return true
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to refresh infrastructure: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func loadPreferences(for providerId: ProviderID?) {
|
||||
guard let providerPreferences else {
|
||||
return
|
||||
}
|
||||
if let providerId {
|
||||
do {
|
||||
pp_log(.app, .debug, "Load preferences for provider \(providerId)")
|
||||
let repository = try preferencesManager.preferencesRepository(forProviderWithId: providerId)
|
||||
providerPreferences.setRepository(repository)
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(error)")
|
||||
providerPreferences.setRepository(nil)
|
||||
}
|
||||
} else {
|
||||
providerPreferences.setRepository(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func savePreferences() {
|
||||
guard let providerPreferences else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
pp_log(.app, .debug, "Save preferences for provider \(providerId.debugDescription)")
|
||||
try providerPreferences.save()
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to save preferences for provider \(providerId.debugDescription): \(error)")
|
||||
func onSelectProvider(manager: APIManager, providerId: ProviderID?, isInitial: Bool) {
|
||||
if !isInitial {
|
||||
selectedEntity = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -211,16 +87,24 @@ private extension ProviderContentModifier {
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
List {
|
||||
EmptyView()
|
||||
.modifier(ProviderContentModifier(
|
||||
apis: [API.bundled],
|
||||
providerId: .constant(.hideme),
|
||||
providerPreferences: nil,
|
||||
entityType: VPNEntity<OpenVPN.Configuration>.self,
|
||||
providerRows: {},
|
||||
onSelectProvider: { _, _, _ in }
|
||||
))
|
||||
NavigationStack {
|
||||
List {
|
||||
EmptyView()
|
||||
.modifier(ProviderContentModifier(
|
||||
apis: [API.bundled],
|
||||
providerId: .constant(.hideme),
|
||||
providerPreferences: nil,
|
||||
selectedEntity: .constant(nil as ProviderEntity<OpenVPNProviderTemplate>?),
|
||||
entityDestination: "Destination",
|
||||
providerRows: {
|
||||
Text("Other")
|
||||
}
|
||||
))
|
||||
}
|
||||
.navigationTitle("Preview")
|
||||
.navigationDestination(for: String.self) {
|
||||
Text($0)
|
||||
}
|
||||
}
|
||||
.withMockEnvironment()
|
||||
}
|
||||
|
@ -1,51 +0,0 @@
|
||||
//
|
||||
// ProviderEntitySelector.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/22/24.
|
||||
// Copyright (c) 2025 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 CommonUtils
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
struct ProviderEntitySelector: View {
|
||||
let module: Module
|
||||
|
||||
let errorHandler: ErrorHandler
|
||||
|
||||
let selectTitle: String
|
||||
|
||||
let onSelect: (Module) async throws -> Void
|
||||
|
||||
var body: some View {
|
||||
if let viewProvider = module as? any ProviderEntityViewProviding {
|
||||
AnyView(viewProvider.providerEntityView(
|
||||
errorHandler: errorHandler,
|
||||
selectTitle: selectTitle,
|
||||
onSelect: onSelect
|
||||
))
|
||||
} else {
|
||||
fatalError("Module got too far without being ProviderEntityViewProviding: \(module)")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// VPNFiltersView+Model.swift
|
||||
// ProviderFiltersView+Model.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/26/24.
|
||||
@ -29,7 +29,7 @@ import Foundation
|
||||
import PassepartoutKit
|
||||
import UIAccessibility
|
||||
|
||||
extension VPNFiltersView {
|
||||
extension ProviderFiltersView {
|
||||
|
||||
@MainActor
|
||||
final class Model: ObservableObject {
|
||||
@ -37,7 +37,7 @@ extension VPNFiltersView {
|
||||
|
||||
private let defaults: UserDefaults
|
||||
|
||||
private var options: VPNFilterOptions
|
||||
private var options: ProviderFilterOptions
|
||||
|
||||
@Published
|
||||
private(set) var categories: [String]
|
||||
@ -46,10 +46,10 @@ extension VPNFiltersView {
|
||||
private(set) var countries: [CodeWithDescription]
|
||||
|
||||
@Published
|
||||
private(set) var presets: [AnyVPNPreset]
|
||||
private(set) var presets: [AnyProviderPreset]
|
||||
|
||||
@Published
|
||||
var filters: VPNFilters
|
||||
var filters: ProviderFilters
|
||||
|
||||
@Published
|
||||
var onlyShowsFavorites: Bool
|
||||
@ -58,11 +58,11 @@ extension VPNFiltersView {
|
||||
|
||||
init(defaults: UserDefaults = .standard) {
|
||||
self.defaults = defaults
|
||||
options = VPNFilterOptions()
|
||||
options = ProviderFilterOptions()
|
||||
categories = []
|
||||
countries = []
|
||||
presets = []
|
||||
filters = VPNFilters()
|
||||
filters = ProviderFilters()
|
||||
onlyShowsFavorites = false
|
||||
subscriptions = []
|
||||
|
||||
@ -71,7 +71,7 @@ extension VPNFiltersView {
|
||||
}
|
||||
}
|
||||
|
||||
func load(options: VPNFilterOptions, initialFilters: VPNFilters?) {
|
||||
func load(options: ProviderFilterOptions, initialFilters: ProviderFilters?) {
|
||||
self.options = options
|
||||
setCategories(withNames: Set(options.countriesByCategoryName.keys))
|
||||
setCountries(withCodes: options.countryCodes)
|
||||
@ -82,7 +82,7 @@ extension VPNFiltersView {
|
||||
}
|
||||
}
|
||||
|
||||
func update(with servers: [VPNServer]) {
|
||||
func update(with servers: [ProviderServer]) {
|
||||
|
||||
// only countries that have servers in this category
|
||||
let knownCountryCodes: Set<String>
|
||||
@ -94,7 +94,7 @@ extension VPNFiltersView {
|
||||
|
||||
// only presets known in filtered servers
|
||||
var knownPresets = options.presets
|
||||
let allPresetIds = Set(servers.compactMap(\.provider.supportedPresetIds).joined())
|
||||
let allPresetIds = Set(servers.compactMap(\.metadata.supportedPresetIds).joined())
|
||||
if !allPresetIds.isEmpty {
|
||||
knownPresets = knownPresets
|
||||
.filter {
|
||||
@ -108,7 +108,7 @@ extension VPNFiltersView {
|
||||
}
|
||||
}
|
||||
|
||||
private extension VPNFiltersView.Model {
|
||||
private extension ProviderFiltersView.Model {
|
||||
func setCategories(withNames categoryNames: Set<String>) {
|
||||
categories = categoryNames
|
||||
.sorted()
|
||||
@ -122,7 +122,7 @@ private extension VPNFiltersView.Model {
|
||||
}
|
||||
}
|
||||
|
||||
func setPresets(with presets: Set<AnyVPNPreset>) {
|
||||
func setPresets(with presets: Set<AnyProviderPreset>) {
|
||||
self.presets = presets
|
||||
.sorted {
|
||||
$0.description < $1.description
|
||||
@ -132,7 +132,7 @@ private extension VPNFiltersView.Model {
|
||||
|
||||
// MARK: - Observation
|
||||
|
||||
private extension VPNFiltersView.Model {
|
||||
private extension ProviderFiltersView.Model {
|
||||
func observeObjects() {
|
||||
$onlyShowsFavorites
|
||||
.dropFirst()
|
||||
@ -160,7 +160,7 @@ private extension UserDefaults {
|
||||
}
|
||||
|
||||
private extension String {
|
||||
var asCountryCodeWithDescription: VPNFiltersView.Model.CodeWithDescription {
|
||||
var asCountryCodeWithDescription: ProviderFiltersView.Model.CodeWithDescription {
|
||||
(self, localizedAsRegionCode ?? self)
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// VPNFiltersView.swift
|
||||
// ProviderFiltersView.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/9/24.
|
||||
@ -28,7 +28,7 @@ import CommonLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
struct VPNFiltersView: View {
|
||||
struct ProviderFiltersView: View {
|
||||
let apis: [APIMapper]
|
||||
|
||||
let providerId: ProviderID
|
||||
@ -59,7 +59,7 @@ struct VPNFiltersView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private extension VPNFiltersView {
|
||||
private extension ProviderFiltersView {
|
||||
var categoryNameBinding: Binding<String?> {
|
||||
Binding {
|
||||
model.filters.categoryName
|
||||
@ -108,14 +108,14 @@ private extension VPNFiltersView {
|
||||
|
||||
var clearFiltersButton: some View {
|
||||
Button(Strings.Views.Providers.clearFilters, role: .destructive) {
|
||||
model.filters = VPNFilters()
|
||||
model.filters = ProviderFilters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
VPNFiltersView(
|
||||
ProviderFiltersView(
|
||||
apis: [API.bundled],
|
||||
providerId: .mullvad,
|
||||
model: .init()
|
@ -0,0 +1,57 @@
|
||||
//
|
||||
// ProviderServerCoordinatorSupporting+Module.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 1/16/25.
|
||||
// Copyright (c) 2025 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 PassepartoutKit
|
||||
import SwiftUI
|
||||
import UILibrary
|
||||
|
||||
extension ProviderServerCoordinatorSupporting where Self: Module,
|
||||
Self: BuildableType,
|
||||
Self: ProviderSelecting,
|
||||
B: MutableProviderSelecting,
|
||||
CustomProviderSelection == B.CustomProviderSelection {
|
||||
public func providerServerCoordinator(
|
||||
selectTitle: String,
|
||||
onSelect: @escaping (Module) async throws -> Void,
|
||||
errorHandler: ErrorHandler
|
||||
) -> AnyView {
|
||||
AnyView(providerSelection.map {
|
||||
ProviderServerCoordinator(
|
||||
moduleId: id,
|
||||
providerId: $0.id,
|
||||
selectedEntity: $0.entity,
|
||||
selectTitle: selectTitle,
|
||||
onSelect: {
|
||||
var newBuilder = builder()
|
||||
newBuilder.providerEntity = $0
|
||||
let newModule = try newBuilder.tryBuild()
|
||||
try await onSelect(newModule)
|
||||
},
|
||||
errorHandler: errorHandler
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// VPNProviderServerCoordinator.swift
|
||||
// ProviderServerCoordinator.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/16/24.
|
||||
@ -27,7 +27,7 @@ import CommonUtils
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
struct VPNProviderServerCoordinator<Configuration>: View where Configuration: IdentifiableConfiguration {
|
||||
struct ProviderServerCoordinator<Template>: View where Template: IdentifiableConfiguration {
|
||||
|
||||
@Environment(\.dismiss)
|
||||
private var dismiss
|
||||
@ -36,17 +36,17 @@ struct VPNProviderServerCoordinator<Configuration>: View where Configuration: Id
|
||||
|
||||
let providerId: ProviderID
|
||||
|
||||
let selectedEntity: VPNEntity<Configuration>?
|
||||
let selectedEntity: ProviderEntity<Template>?
|
||||
|
||||
let selectTitle: String
|
||||
|
||||
let onSelect: (VPNEntity<Configuration>) async throws -> Void
|
||||
let onSelect: (ProviderEntity<Template>) async throws -> Void
|
||||
|
||||
@ObservedObject
|
||||
var errorHandler: ErrorHandler
|
||||
|
||||
var body: some View {
|
||||
VPNProviderServerView(
|
||||
ProviderServerView(
|
||||
moduleId: moduleId,
|
||||
providerId: providerId,
|
||||
selectedEntity: selectedEntity,
|
||||
@ -58,15 +58,15 @@ struct VPNProviderServerCoordinator<Configuration>: View where Configuration: Id
|
||||
}
|
||||
}
|
||||
|
||||
private extension VPNProviderServerCoordinator {
|
||||
func onSelect(server: VPNServer, preset: VPNPreset<Configuration>) {
|
||||
private extension ProviderServerCoordinator {
|
||||
func onSelect(server: ProviderServer, preset: ProviderPreset<Template>) {
|
||||
Task {
|
||||
do {
|
||||
let entity = VPNEntity(server: server, preset: preset)
|
||||
let entity = ProviderEntity(server: server, preset: preset)
|
||||
dismiss()
|
||||
try await onSelect(entity)
|
||||
} catch {
|
||||
pp_log(.app, .fault, "Unable to select server \(server.serverId) for provider \(server.provider.id): \(error)")
|
||||
pp_log(.app, .fault, "Unable to select server \(server.serverId) for provider \(server.metadata.providerId): \(error)")
|
||||
errorHandler.handle(error, title: Strings.Views.Providers.selectEntity)
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// VPNProviderServerView.swift
|
||||
// ProviderServerView.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/7/24.
|
||||
@ -29,10 +29,10 @@ import CommonUtils
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
struct VPNProviderServerView<Configuration>: View where Configuration: IdentifiableConfiguration {
|
||||
struct ProviderServerView<Template>: View where Template: IdentifiableConfiguration {
|
||||
|
||||
@EnvironmentObject
|
||||
private var providerManager: ProviderManager
|
||||
private var apiManager: APIManager
|
||||
|
||||
@EnvironmentObject
|
||||
private var preferencesManager: PreferencesManager
|
||||
@ -43,23 +43,23 @@ struct VPNProviderServerView<Configuration>: View where Configuration: Identifia
|
||||
|
||||
let providerId: ProviderID
|
||||
|
||||
let selectedEntity: VPNEntity<Configuration>?
|
||||
let selectedEntity: ProviderEntity<Template>?
|
||||
|
||||
let filtersWithSelection: Bool
|
||||
|
||||
var selectTitle = Strings.Views.Providers.selectEntity
|
||||
|
||||
let onSelect: (VPNServer, VPNPreset<Configuration>) -> Void
|
||||
let onSelect: (ProviderServer, ProviderPreset<Template>) -> Void
|
||||
|
||||
@StateObject
|
||||
private var vpnManager = VPNProviderManager<Configuration>(sorting: [
|
||||
private var providerManager = ProviderManager<Template>(sorting: [
|
||||
.localizedCountry,
|
||||
.area,
|
||||
.serverId
|
||||
])
|
||||
|
||||
@State
|
||||
private var servers: [VPNServer] = []
|
||||
private var servers: [ProviderServer] = []
|
||||
|
||||
@State
|
||||
private var isFiltering = false
|
||||
@ -71,7 +71,7 @@ struct VPNProviderServerView<Configuration>: View where Configuration: Identifia
|
||||
private var providerPreferences = ProviderPreferences()
|
||||
|
||||
@StateObject
|
||||
private var filtersViewModel = VPNFiltersView.Model()
|
||||
private var filtersViewModel = ProviderFiltersView.Model()
|
||||
|
||||
@StateObject
|
||||
private var errorHandler: ErrorHandler = .default()
|
||||
@ -88,7 +88,7 @@ struct VPNProviderServerView<Configuration>: View where Configuration: Identifia
|
||||
}
|
||||
}
|
||||
|
||||
extension VPNProviderServerView {
|
||||
extension ProviderServerView {
|
||||
func contentView() -> some View {
|
||||
ContentView(
|
||||
apis: apis,
|
||||
@ -110,7 +110,7 @@ extension VPNProviderServerView {
|
||||
}
|
||||
|
||||
func filtersView() -> some View {
|
||||
VPNFiltersView(
|
||||
ProviderFiltersView(
|
||||
apis: apis,
|
||||
providerId: providerId,
|
||||
model: filtersViewModel
|
||||
@ -118,12 +118,12 @@ extension VPNProviderServerView {
|
||||
}
|
||||
}
|
||||
|
||||
private extension VPNProviderServerView {
|
||||
private extension ProviderServerView {
|
||||
var title: String {
|
||||
providerManager.provider(withId: providerId)?.description ?? Strings.Global.Nouns.servers
|
||||
apiManager.provider(withId: providerId)?.description ?? Strings.Global.Nouns.servers
|
||||
}
|
||||
|
||||
var filteredServers: [VPNServer] {
|
||||
var filteredServers: [ProviderServer] {
|
||||
if onlyShowsFavorites {
|
||||
return servers.filter {
|
||||
providerPreferences.isFavoriteServer($0.serverId)
|
||||
@ -132,16 +132,16 @@ private extension VPNProviderServerView {
|
||||
return servers
|
||||
}
|
||||
|
||||
var initialFilters: VPNFilters? {
|
||||
var initialFilters: ProviderFilters? {
|
||||
guard let selectedEntity else {
|
||||
return nil
|
||||
}
|
||||
var filters = VPNFilters()
|
||||
var filters = ProviderFilters()
|
||||
filters.presetId = selectedEntity.preset.presetId
|
||||
if filtersWithSelection {
|
||||
filters.categoryName = selectedEntity.server.provider.categoryName
|
||||
filters.categoryName = selectedEntity.server.metadata.categoryName
|
||||
#if os(macOS)
|
||||
filters.countryCode = selectedEntity.server.provider.countryCode
|
||||
filters.countryCode = selectedEntity.server.metadata.countryCode
|
||||
#endif
|
||||
}
|
||||
return filters
|
||||
@ -155,12 +155,12 @@ private extension VPNProviderServerView {
|
||||
pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(error)")
|
||||
}
|
||||
do {
|
||||
let repository = try await providerManager.vpnServerRepository(
|
||||
let repository = try await apiManager.providerRepository(
|
||||
from: apis,
|
||||
for: providerId
|
||||
)
|
||||
try await vpnManager.setRepository(repository)
|
||||
filtersViewModel.load(options: vpnManager.options, initialFilters: initialFilters)
|
||||
try await providerManager.setRepository(repository)
|
||||
filtersViewModel.load(options: providerManager.options, initialFilters: initialFilters)
|
||||
await reloadServers(filters: filtersViewModel.filters)
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to load VPN servers for provider \(providerId): \(error)")
|
||||
@ -168,11 +168,11 @@ private extension VPNProviderServerView {
|
||||
}
|
||||
}
|
||||
|
||||
func reloadServers(filters: VPNFilters) async {
|
||||
func reloadServers(filters: ProviderFilters) async {
|
||||
isFiltering = true
|
||||
do {
|
||||
try await Task {
|
||||
servers = try await vpnManager.filteredServers(with: filters)
|
||||
servers = try await providerManager.filteredServers(with: filters)
|
||||
filtersViewModel.update(with: servers)
|
||||
isFiltering = false
|
||||
}.value
|
||||
@ -181,8 +181,8 @@ private extension VPNProviderServerView {
|
||||
}
|
||||
}
|
||||
|
||||
func compatiblePresets(with server: VPNServer) -> [VPNPreset<Configuration>] {
|
||||
vpnManager
|
||||
func compatiblePresets(with server: ProviderServer) -> [ProviderPreset<Template>] {
|
||||
providerManager
|
||||
.presets
|
||||
.filter {
|
||||
if let selectedId = filtersViewModel.filters.presetId {
|
||||
@ -191,14 +191,14 @@ private extension VPNProviderServerView {
|
||||
return true
|
||||
}
|
||||
.filter {
|
||||
if let supportedIds = server.provider.supportedPresetIds {
|
||||
if let supportedIds = server.metadata.supportedPresetIds {
|
||||
return supportedIds.contains($0.presetId)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func onNewFilters(_ filters: VPNFilters) {
|
||||
func onNewFilters(_ filters: ProviderFilters) {
|
||||
Task {
|
||||
await reloadServers(filters: filters)
|
||||
}
|
||||
@ -216,11 +216,11 @@ private extension VPNProviderServerView {
|
||||
}
|
||||
}
|
||||
|
||||
func onSelectServer(_ server: VPNServer) {
|
||||
func onSelectServer(_ server: ProviderServer) {
|
||||
let presets = compatiblePresets(with: server)
|
||||
guard let preset = presets.first else {
|
||||
pp_log(.app, .error, "Unable to find a compatible preset. Supported IDs: \(server.provider.supportedPresetIds ?? [])")
|
||||
assertionFailure("No compatible presets for server \(server.serverId) (provider=\(vpnManager.providerId), configuration=\(Configuration.configurationIdentifier), supported=\(server.provider.supportedPresetIds ?? []))")
|
||||
pp_log(.app, .error, "Unable to find a compatible preset. Supported IDs: \(server.metadata.supportedPresetIds ?? [])")
|
||||
assertionFailure("No compatible presets for server \(server.serverId) (provider=\(providerManager.providerId), template=\(Template.configurationIdentifier), supported=\(server.metadata.supportedPresetIds ?? []))")
|
||||
return
|
||||
}
|
||||
onSelect(server, preset)
|
||||
@ -231,11 +231,11 @@ private extension VPNProviderServerView {
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
VPNProviderServerView(
|
||||
ProviderServerView(
|
||||
apis: [API.bundled],
|
||||
moduleId: UUID(),
|
||||
providerId: .protonvpn,
|
||||
selectedEntity: nil as VPNEntity<OpenVPN.Configuration>?,
|
||||
selectedEntity: nil as ProviderEntity<OpenVPNProviderTemplate>?,
|
||||
filtersWithSelection: false,
|
||||
selectTitle: "Select",
|
||||
onSelect: { _, _ in }
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// VPNProviderServer+Container+macOS.swift
|
||||
// ProviderServer+Container+macOS.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/25/24.
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension VPNProviderServerView {
|
||||
extension ProviderServerView {
|
||||
struct ContainerView<Content, Filters>: View where Content: View, Filters: View {
|
||||
|
||||
@ViewBuilder
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// VPNProviderServer+Content+iOS.swift
|
||||
// ProviderServer+Content+iOS.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/9/24.
|
||||
@ -31,30 +31,30 @@ import PassepartoutKit
|
||||
import SwiftUI
|
||||
import UIAccessibility
|
||||
|
||||
extension VPNProviderServerView {
|
||||
extension ProviderServerView {
|
||||
struct ContentView: View {
|
||||
let apis: [APIMapper]
|
||||
|
||||
let providerId: ProviderID
|
||||
|
||||
let servers: [VPNServer]
|
||||
let servers: [ProviderServer]
|
||||
|
||||
let selectedServer: VPNServer?
|
||||
let selectedServer: ProviderServer?
|
||||
|
||||
let isFiltering: Bool
|
||||
|
||||
@ObservedObject
|
||||
var filtersViewModel: VPNFiltersView.Model
|
||||
var filtersViewModel: ProviderFiltersView.Model
|
||||
|
||||
@ObservedObject
|
||||
var providerPreferences: ProviderPreferences
|
||||
|
||||
let selectTitle: String
|
||||
|
||||
let onSelect: (VPNServer) -> Void
|
||||
let onSelect: (ProviderServer) -> Void
|
||||
|
||||
@State
|
||||
private var serversByCountryCode: [String: [VPNServer]] = [:]
|
||||
private var serversByCountryCode: [String: [ProviderServer]] = [:]
|
||||
|
||||
@State
|
||||
private var expandedCodes: Set<String> = []
|
||||
@ -68,7 +68,7 @@ extension VPNProviderServerView {
|
||||
}
|
||||
}
|
||||
|
||||
private extension VPNProviderServerView.ContentView {
|
||||
private extension ProviderServerView.ContentView {
|
||||
var listView: some View {
|
||||
List {
|
||||
Section {
|
||||
@ -92,7 +92,7 @@ private extension VPNProviderServerView.ContentView {
|
||||
)
|
||||
.onLoad {
|
||||
if let selectedServer {
|
||||
expandedCodes.insert(selectedServer.provider.countryCode)
|
||||
expandedCodes.insert(selectedServer.metadata.countryCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -110,7 +110,7 @@ private extension VPNProviderServerView.ContentView {
|
||||
}
|
||||
}
|
||||
|
||||
func serverView(for server: VPNServer) -> some View {
|
||||
func serverView(for server: ProviderServer) -> some View {
|
||||
Button {
|
||||
onSelect(server)
|
||||
} label: {
|
||||
@ -118,7 +118,7 @@ private extension VPNProviderServerView.ContentView {
|
||||
ThemeImage(.marked)
|
||||
.opaque(server.id == selectedServer?.id)
|
||||
VStack(alignment: .leading) {
|
||||
if let area = server.provider.area {
|
||||
if let area = server.metadata.area {
|
||||
Text(area)
|
||||
.font(.headline)
|
||||
}
|
||||
@ -140,7 +140,7 @@ private extension VPNProviderServerView.ContentView {
|
||||
}
|
||||
}
|
||||
|
||||
private extension VPNProviderServerView.ContentView {
|
||||
private extension ProviderServerView.ContentView {
|
||||
var countryCodes: [String] {
|
||||
filtersViewModel
|
||||
.countries
|
||||
@ -159,10 +159,10 @@ private extension VPNProviderServerView.ContentView {
|
||||
}
|
||||
}
|
||||
|
||||
func computeServersByCountry(_ servers: [VPNServer]) {
|
||||
var map: [String: [VPNServer]] = [:]
|
||||
func computeServersByCountry(_ servers: [ProviderServer]) {
|
||||
var map: [String: [ProviderServer]] = [:]
|
||||
servers.forEach {
|
||||
let code = $0.provider.countryCode
|
||||
let code = $0.metadata.countryCode
|
||||
var list = map[code] ?? []
|
||||
list.append($0)
|
||||
map[code] = list
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// VPNProviderServer+Container+macOS.swift
|
||||
// ProviderServer+Container+macOS.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/25/24.
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension VPNProviderServerView {
|
||||
extension ProviderServerView {
|
||||
struct ContainerView<Content, Filters>: View where Content: View, Filters: View {
|
||||
|
||||
@ViewBuilder
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// VPNProviderServer+Content+macOS.swift
|
||||
// ProviderServer+Content+macOS.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/9/24.
|
||||
@ -30,7 +30,7 @@ import CommonLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
extension VPNProviderServerView {
|
||||
extension ProviderServerView {
|
||||
struct ContentView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
@ -40,21 +40,21 @@ extension VPNProviderServerView {
|
||||
|
||||
let providerId: ProviderID
|
||||
|
||||
let servers: [VPNServer]
|
||||
let servers: [ProviderServer]
|
||||
|
||||
let selectedServer: VPNServer?
|
||||
let selectedServer: ProviderServer?
|
||||
|
||||
let isFiltering: Bool
|
||||
|
||||
@ObservedObject
|
||||
var filtersViewModel: VPNFiltersView.Model
|
||||
var filtersViewModel: ProviderFiltersView.Model
|
||||
|
||||
@ObservedObject
|
||||
var providerPreferences: ProviderPreferences
|
||||
|
||||
let selectTitle: String
|
||||
|
||||
let onSelect: (VPNServer) -> Void
|
||||
let onSelect: (ProviderServer) -> Void
|
||||
|
||||
@State
|
||||
private var hoveringServerId: String?
|
||||
@ -66,7 +66,7 @@ extension VPNProviderServerView {
|
||||
}
|
||||
}
|
||||
|
||||
private extension VPNProviderServerView.ContentView {
|
||||
private extension ProviderServerView.ContentView {
|
||||
var tableView: some View {
|
||||
Table(servers) {
|
||||
TableColumn("") { server in
|
||||
@ -77,7 +77,7 @@ private extension VPNProviderServerView.ContentView {
|
||||
.width(10.0)
|
||||
|
||||
TableColumn(Strings.Global.Nouns.region) { server in
|
||||
ThemeCountryText(server.provider.countryCode, title: server.region)
|
||||
ThemeCountryText(server.metadata.countryCode, title: server.region)
|
||||
.help(server.region)
|
||||
.environmentObject(theme) // TODO: #873, Table loses environment
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
//
|
||||
// VPNProviderContentModifier.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/7/24.
|
||||
// Copyright (c) 2025 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 CommonAPI
|
||||
import CommonLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
struct VPNProviderContentModifier<Configuration, ProviderRows>: ViewModifier where Configuration: IdentifiableConfiguration, ProviderRows: View {
|
||||
var apis: [APIMapper] = API.shared
|
||||
|
||||
@Binding
|
||||
var providerId: ProviderID?
|
||||
|
||||
let providerPreferences: ProviderPreferences?
|
||||
|
||||
@Binding
|
||||
var selectedEntity: VPNEntity<Configuration>?
|
||||
|
||||
let entityDestination: any Hashable
|
||||
|
||||
@ViewBuilder
|
||||
let providerRows: ProviderRows
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
debugChanges()
|
||||
return content
|
||||
.modifier(ProviderContentModifier(
|
||||
apis: apis,
|
||||
providerId: $providerId,
|
||||
providerPreferences: providerPreferences,
|
||||
entityType: VPNEntity<Configuration>.self,
|
||||
providerRows: {
|
||||
providerEntityRow
|
||||
providerRows
|
||||
},
|
||||
onSelectProvider: onSelectProvider
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private extension VPNProviderContentModifier {
|
||||
var providerEntityRow: some View {
|
||||
NavigationLink(value: entityDestination) {
|
||||
HStack {
|
||||
Text(Strings.Global.Nouns.server)
|
||||
if let selectedEntity {
|
||||
Spacer()
|
||||
Text(selectedEntity.server.hostname ?? selectedEntity.server.serverId)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension VPNProviderContentModifier {
|
||||
func onSelectProvider(manager: ProviderManager, providerId: ProviderID?, isInitial: Bool) {
|
||||
if !isInitial {
|
||||
selectedEntity = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
List {
|
||||
EmptyView()
|
||||
.modifier(VPNProviderContentModifier(
|
||||
apis: [API.bundled],
|
||||
providerId: .constant(.hideme),
|
||||
providerPreferences: nil,
|
||||
selectedEntity: .constant(nil as VPNEntity<OpenVPN.Configuration>?),
|
||||
entityDestination: "Destination",
|
||||
providerRows: {
|
||||
Text("Other")
|
||||
}
|
||||
))
|
||||
}
|
||||
.navigationTitle("Preview")
|
||||
.navigationDestination(for: String.self) {
|
||||
Text($0)
|
||||
}
|
||||
}
|
||||
.withMockEnvironment()
|
||||
}
|
@ -35,7 +35,7 @@ struct ActiveProfileView: View {
|
||||
private var theme: Theme
|
||||
|
||||
@EnvironmentObject
|
||||
private var providerManager: ProviderManager
|
||||
private var apiManager: APIManager
|
||||
|
||||
let profile: Profile?
|
||||
|
||||
@ -106,14 +106,14 @@ private extension ActiveProfileView {
|
||||
}
|
||||
}
|
||||
if let pair = profile.selectedProvider {
|
||||
if let provider = providerManager.provider(withId: pair.selection.id) {
|
||||
if let provider = apiManager.provider(withId: pair.selection.id) {
|
||||
ListRowView(title: Strings.Global.Nouns.provider) {
|
||||
Text(provider.description)
|
||||
}
|
||||
}
|
||||
if let entity = pair.selection.entity {
|
||||
if let entityHeader = pair.selection.entityHeader {
|
||||
ListRowView(title: Strings.Global.Nouns.country) {
|
||||
ThemeCountryText(entity.header.countryCode)
|
||||
ThemeCountryText(entityHeader.countryCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -217,7 +217,7 @@ private extension ActiveProfileView {
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.task {
|
||||
try? await ProviderManager.forPreviews.fetchIndex(from: [API.bundled])
|
||||
try? await APIManager.forPreviews.fetchIndex(from: [API.bundled])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,16 +32,16 @@ import UIAccessibility
|
||||
|
||||
@MainActor
|
||||
public final class AppContext: ObservableObject, Sendable {
|
||||
public let apiManager: APIManager
|
||||
|
||||
public let iapManager: IAPManager
|
||||
|
||||
public let migrationManager: MigrationManager
|
||||
|
||||
public let profileManager: ProfileManager
|
||||
|
||||
public let providerManager: ProviderManager
|
||||
|
||||
public let preferencesManager: PreferencesManager
|
||||
|
||||
public let profileManager: ProfileManager
|
||||
|
||||
public let registry: Registry
|
||||
|
||||
public let tunnel: ExtendedTunnel
|
||||
@ -57,21 +57,21 @@ public final class AppContext: ObservableObject, Sendable {
|
||||
private var subscriptions: Set<AnyCancellable>
|
||||
|
||||
public init(
|
||||
apiManager: APIManager,
|
||||
iapManager: IAPManager,
|
||||
migrationManager: MigrationManager,
|
||||
profileManager: ProfileManager,
|
||||
providerManager: ProviderManager,
|
||||
preferencesManager: PreferencesManager,
|
||||
profileManager: ProfileManager,
|
||||
registry: Registry,
|
||||
tunnel: ExtendedTunnel,
|
||||
tunnelReceiptURL: URL?,
|
||||
onEligibleFeaturesBlock: ((Set<AppFeature>) async -> Void)? = nil
|
||||
) {
|
||||
self.apiManager = apiManager
|
||||
self.iapManager = iapManager
|
||||
self.migrationManager = migrationManager
|
||||
self.profileManager = profileManager
|
||||
self.providerManager = providerManager
|
||||
self.preferencesManager = preferencesManager
|
||||
self.profileManager = profileManager
|
||||
self.registry = registry
|
||||
self.tunnel = tunnel
|
||||
self.tunnelReceiptURL = tunnelReceiptURL
|
||||
@ -150,7 +150,7 @@ private extension AppContext {
|
||||
|
||||
do {
|
||||
pp_log(.app, .info, "\tFetch providers index...")
|
||||
try await providerManager.fetchIndex(from: API.shared)
|
||||
try await apiManager.fetchIndex(from: API.shared)
|
||||
} catch {
|
||||
pp_log(.app, .error, "\tUnable to fetch providers index: \(error)")
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ extension ModuleDraftEditing where Draft: MutableProviderSelecting {
|
||||
}
|
||||
}
|
||||
|
||||
public var providerEntity: Binding<Draft.CustomProviderSelection.Entity?> {
|
||||
public var providerEntity: Binding<ProviderEntity<Draft.CustomProviderSelection.Template>?> {
|
||||
Binding {
|
||||
draft.providerEntity.wrappedValue
|
||||
} set: {
|
||||
|
@ -29,10 +29,10 @@ import SwiftUI
|
||||
extension View {
|
||||
public func withEnvironment(from context: AppContext, theme: Theme) -> some View {
|
||||
environmentObject(theme)
|
||||
.environmentObject(context.apiManager)
|
||||
.environmentObject(context.iapManager)
|
||||
.environmentObject(context.migrationManager)
|
||||
.environmentObject(context.preferencesManager)
|
||||
.environmentObject(context.providerManager)
|
||||
}
|
||||
|
||||
public func withMockEnvironment() -> some View {
|
||||
|
@ -181,9 +181,9 @@ extension ProviderID: @retroactive CustomDebugStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
extension VPNServer {
|
||||
extension ProviderServer {
|
||||
public var region: String {
|
||||
[provider.countryCode.localizedAsRegionCode, provider.area]
|
||||
[metadata.countryCode.localizedAsRegionCode, metadata.area]
|
||||
.compactMap { $0 }
|
||||
.joined(separator: " - ")
|
||||
}
|
||||
|
@ -54,16 +54,16 @@ extension AppContext {
|
||||
processor: processor,
|
||||
interval: Constants.shared.tunnel.refreshInterval
|
||||
)
|
||||
let providerManager = ProviderManager(
|
||||
repository: InMemoryProviderRepository()
|
||||
let apiManager = APIManager(
|
||||
repository: InMemoryAPIRepository()
|
||||
)
|
||||
let migrationManager = MigrationManager()
|
||||
return AppContext(
|
||||
apiManager: apiManager,
|
||||
iapManager: iapManager,
|
||||
migrationManager: migrationManager,
|
||||
profileManager: profileManager,
|
||||
providerManager: providerManager,
|
||||
preferencesManager: PreferencesManager(),
|
||||
profileManager: profileManager,
|
||||
registry: Registry(),
|
||||
tunnel: tunnel,
|
||||
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
|
||||
@ -91,8 +91,8 @@ extension ExtendedTunnel {
|
||||
}
|
||||
}
|
||||
|
||||
extension ProviderManager {
|
||||
public static var forPreviews: ProviderManager {
|
||||
AppContext.forPreviews.providerManager
|
||||
extension APIManager {
|
||||
public static var forPreviews: APIManager {
|
||||
AppContext.forPreviews.apiManager
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// ProviderEntityViewProviding.swift
|
||||
// ProviderServerCoordinatorSupporting.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/16/24.
|
||||
@ -27,13 +27,12 @@ import CommonUtils
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
public protocol ProviderEntityViewProviding {
|
||||
associatedtype EntityContent: View
|
||||
public protocol ProviderServerCoordinatorSupporting {
|
||||
|
||||
@MainActor
|
||||
func providerEntityView(
|
||||
errorHandler: ErrorHandler,
|
||||
func providerServerCoordinator(
|
||||
selectTitle: String,
|
||||
onSelect: @escaping (Module) async throws -> Void
|
||||
) -> EntityContent
|
||||
onSelect: @escaping (Module) async throws -> Void,
|
||||
errorHandler: ErrorHandler
|
||||
) -> AnyView
|
||||
}
|
@ -41,7 +41,7 @@ public struct OpenVPNCredentialsView: View {
|
||||
private var iapManager: IAPManager
|
||||
|
||||
@EnvironmentObject
|
||||
private var providerManager: ProviderManager
|
||||
private var apiManager: APIManager
|
||||
|
||||
@ObservedObject
|
||||
private var profileEditor: ProfileEditor
|
||||
@ -214,8 +214,8 @@ private extension OpenVPNCredentialsView {
|
||||
}
|
||||
|
||||
func onLoad() {
|
||||
if let providerId, let provider = providerManager.provider(withId: providerId) {
|
||||
providerCustomization = provider.customization(for: OpenVPN.Configuration.self)
|
||||
if let providerId, let provider = apiManager.provider(withId: providerId) {
|
||||
providerCustomization = provider.customization(for: OpenVPNProviderTemplate.self)
|
||||
}
|
||||
builder = credentials?.builder() ?? OpenVPN.Credentials.Builder()
|
||||
if ignoresPassword {
|
||||
|
@ -29,7 +29,7 @@ import SwiftUI
|
||||
public struct RefreshInfrastructureButton<Label>: View where Label: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var providerManager: ProviderManager
|
||||
private var apiManager: APIManager
|
||||
|
||||
private let apis: [APIMapper]
|
||||
|
||||
@ -46,7 +46,7 @@ public struct RefreshInfrastructureButton<Label>: View where Label: View {
|
||||
public var body: some View {
|
||||
Button {
|
||||
Task {
|
||||
try await providerManager.fetchVPNInfrastructure(from: apis, for: providerId)
|
||||
try await apiManager.fetchInfrastructure(from: apis, for: providerId)
|
||||
}
|
||||
} label: {
|
||||
label()
|
||||
@ -67,13 +67,13 @@ extension RefreshInfrastructureButton where Label == RefreshInfrastructureButton
|
||||
public struct RefreshInfrastructureButtonProgressView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var providerManager: ProviderManager
|
||||
private var apiManager: APIManager
|
||||
|
||||
public var body: some View {
|
||||
#if os(iOS)
|
||||
HStack {
|
||||
Text(Strings.Views.Providers.refreshInfrastructure)
|
||||
if providerManager.isLoading {
|
||||
if apiManager.isLoading {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
}
|
||||
|
@ -64,12 +64,6 @@ enum Environment {
|
||||
]
|
||||
))
|
||||
}
|
||||
targets.append(.testTarget(
|
||||
name: "TargetTests",
|
||||
dependencies: [
|
||||
.target(name: targetName)
|
||||
]
|
||||
))
|
||||
return targets
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
import PassepartoutKit
|
||||
import XCTest
|
||||
|
||||
final class Tests: XCTestCase {
|
||||
func test_dummy() {
|
||||
var profile = Profile.Builder(activatingModules: true)
|
||||
profile.name = "foobar"
|
||||
XCTAssertEqual(profile.name, "foobar")
|
||||
}
|
||||
}
|
@ -1 +1 @@
|
||||
Subproject commit c45a52251b7f5a59f6183bc8a4aae994fdf085e5
|
||||
Subproject commit 95a6074b3b1eca732a794098622d73c34ba08e0a
|
@ -122,9 +122,9 @@ extension AppContext {
|
||||
interval: Constants.shared.tunnel.refreshInterval
|
||||
)
|
||||
|
||||
let providerManager: ProviderManager = {
|
||||
let repository = AppData.cdProviderRepositoryV3(context: localStore.backgroundContext())
|
||||
return ProviderManager(repository: repository)
|
||||
let apiManager: APIManager = {
|
||||
let repository = AppData.cdAPIRepositoryV3(context: localStore.backgroundContext())
|
||||
return APIManager(repository: repository)
|
||||
}()
|
||||
|
||||
let migrationManager: MigrationManager = {
|
||||
@ -209,11 +209,11 @@ extension AppContext {
|
||||
// MARK: Build
|
||||
|
||||
return AppContext(
|
||||
apiManager: apiManager,
|
||||
iapManager: iapManager,
|
||||
migrationManager: migrationManager,
|
||||
profileManager: profileManager,
|
||||
providerManager: providerManager,
|
||||
preferencesManager: preferencesManager,
|
||||
profileManager: profileManager,
|
||||
registry: dependencies.registry,
|
||||
tunnel: tunnel,
|
||||
tunnelReceiptURL: tunnelReceiptURL,
|
||||
|
@ -56,18 +56,18 @@ extension AppContext {
|
||||
processor: processor,
|
||||
interval: Constants.shared.tunnel.refreshInterval
|
||||
)
|
||||
let providerManager = ProviderManager(
|
||||
repository: InMemoryProviderRepository()
|
||||
let apiManager = APIManager(
|
||||
repository: InMemoryAPIRepository()
|
||||
)
|
||||
let migrationManager = MigrationManager()
|
||||
let preferencesManager = PreferencesManager()
|
||||
|
||||
return AppContext(
|
||||
apiManager: apiManager,
|
||||
iapManager: iapManager,
|
||||
migrationManager: migrationManager,
|
||||
profileManager: profileManager,
|
||||
providerManager: providerManager,
|
||||
preferencesManager: preferencesManager,
|
||||
profileManager: profileManager,
|
||||
registry: registry,
|
||||
tunnel: tunnel,
|
||||
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
|
||||
|
@ -140,26 +140,26 @@ private extension ProfileManager {
|
||||
Parameters("Personal DoH", false, false, [.dns, .onDemand])
|
||||
]
|
||||
|
||||
static var mockHideMeEntity: VPNEntity<OpenVPN.Configuration> {
|
||||
static var mockHideMeEntity: ProviderEntity<OpenVPNProviderTemplate> {
|
||||
do {
|
||||
var cfgBuilder = OpenVPN.Configuration.Builder()
|
||||
cfgBuilder.ca = .init(pem: "...")
|
||||
let cfg = try cfgBuilder.tryBuild(isClient: false)
|
||||
let cfgData = try JSONEncoder().encode(cfg)
|
||||
|
||||
let preset = AnyVPNPreset(
|
||||
let preset = AnyProviderPreset(
|
||||
providerId: .hideme,
|
||||
presetId: "default",
|
||||
description: "Default",
|
||||
endpoints: [.init(.udp, 1194)],
|
||||
configurationIdentifier: "OpenVPN",
|
||||
configuration: cfgData
|
||||
template: cfgData
|
||||
)
|
||||
|
||||
return VPNEntity(
|
||||
return ProviderEntity(
|
||||
server: .init(
|
||||
provider: .init(
|
||||
id: .hideme,
|
||||
metadata: .init(
|
||||
providerId: .hideme,
|
||||
serverId: "be-v4",
|
||||
supportedConfigurationIdentifiers: ["OpenVPN"],
|
||||
supportedPresetIds: nil,
|
||||
@ -171,7 +171,7 @@ private extension ProfileManager {
|
||||
hostname: "be-v4.hideservers.net",
|
||||
ipAddresses: nil
|
||||
),
|
||||
preset: try preset.ofType(OpenVPN.Configuration.self)
|
||||
preset: try preset.ofType(OpenVPNProviderTemplate.self)
|
||||
)
|
||||
} catch {
|
||||
fatalError("Unable to build Hide.me entity: \(error)")
|
||||
|
Loading…
Reference in New Issue
Block a user