Store providers to Core Data (#763)
Also, improve filters by constraining related fields: - Pick countries from the filtered category - Pick presets from those available in the currently filtered servers Closes #705
This commit is contained in:
parent
cc119e18ce
commit
0d383ec792
|
@ -41,7 +41,7 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
|
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "79ff98a69c87cc90ef213e00ab02c9d90d63baaf"
|
"revision" : "69b2ed730986d684195a39b592155b3384f3d857"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -28,7 +28,7 @@ let package = Package(
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.9.0"),
|
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.9.0"),
|
||||||
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "79ff98a69c87cc90ef213e00ab02c9d90d63baaf"),
|
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "69b2ed730986d684195a39b592155b3384f3d857"),
|
||||||
// .package(path: "../../../passepartoutkit-source"),
|
// .package(path: "../../../passepartoutkit-source"),
|
||||||
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", from: "0.9.1"),
|
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", from: "0.9.1"),
|
||||||
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", revision: "031863a1cd683962a7dfe68e20b91fa820a1ecce"),
|
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", revision: "031863a1cd683962a7dfe68e20b91fa820a1ecce"),
|
||||||
|
@ -56,6 +56,17 @@ let package = Package(
|
||||||
.process("Profiles.xcdatamodeld")
|
.process("Profiles.xcdatamodeld")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "AppDataProviders",
|
||||||
|
dependencies: [
|
||||||
|
"AppData",
|
||||||
|
"AppLibrary",
|
||||||
|
"UtilsLibrary"
|
||||||
|
],
|
||||||
|
resources: [
|
||||||
|
.process("Providers.xcdatamodeld")
|
||||||
|
]
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "AppLibrary",
|
name: "AppLibrary",
|
||||||
dependencies: ["CommonLibrary"]
|
dependencies: ["CommonLibrary"]
|
||||||
|
@ -64,6 +75,7 @@ let package = Package(
|
||||||
name: "AppUI",
|
name: "AppUI",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"AppDataProfiles",
|
"AppDataProfiles",
|
||||||
|
"AppDataProviders",
|
||||||
"AppLibrary",
|
"AppLibrary",
|
||||||
"Kvitto",
|
"Kvitto",
|
||||||
"LegacyV2",
|
"LegacyV2",
|
||||||
|
|
|
@ -28,6 +28,10 @@ import Foundation
|
||||||
|
|
||||||
@objc(CDProfileV3)
|
@objc(CDProfileV3)
|
||||||
final class CDProfileV3: NSManagedObject {
|
final class CDProfileV3: NSManagedObject {
|
||||||
|
@nonobjc static func fetchRequest() -> NSFetchRequest<CDProfileV3> {
|
||||||
|
NSFetchRequest<CDProfileV3>(entityName: "CDProfileV3")
|
||||||
|
}
|
||||||
|
|
||||||
@NSManaged var uuid: UUID?
|
@NSManaged var uuid: UUID?
|
||||||
@NSManaged var name: String?
|
@NSManaged var name: String?
|
||||||
@NSManaged var encoded: String?
|
@NSManaged var encoded: String?
|
|
@ -0,0 +1,39 @@
|
||||||
|
//
|
||||||
|
// AppData+Providers.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 AppData
|
||||||
|
import CoreData
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension AppData {
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
public static let cdProvidersModel: NSManagedObjectModel = {
|
||||||
|
guard let model: NSManagedObjectModel = .mergedModel(from: [.module]) else {
|
||||||
|
fatalError("Unable to build Core Data model (Providers v3)")
|
||||||
|
}
|
||||||
|
return model
|
||||||
|
}()
|
||||||
|
}
|
|
@ -0,0 +1,179 @@
|
||||||
|
//
|
||||||
|
// CDProviderRepositoryV3.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 AppData
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import Foundation
|
||||||
|
import PassepartoutKit
|
||||||
|
import UtilsLibrary
|
||||||
|
|
||||||
|
extension AppData {
|
||||||
|
public static func cdProviderRepositoryV3(
|
||||||
|
context: NSManagedObjectContext,
|
||||||
|
backgroundContext: NSManagedObjectContext
|
||||||
|
) -> ProviderRepository {
|
||||||
|
CDProviderRepositoryV3(context: context, backgroundContext: backgroundContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actor CDProviderRepositoryV3: NSObject, ProviderRepository {
|
||||||
|
private let context: NSManagedObjectContext
|
||||||
|
|
||||||
|
private let backgroundContext: NSManagedObjectContext
|
||||||
|
|
||||||
|
private let providersSubject: CurrentValueSubject<[ProviderMetadata], Never>
|
||||||
|
|
||||||
|
private let lastUpdatedSubject: CurrentValueSubject<[ProviderID: Date], Never>
|
||||||
|
|
||||||
|
private let providersController: NSFetchedResultsController<CDProviderV3>
|
||||||
|
|
||||||
|
init(context: NSManagedObjectContext, backgroundContext: NSManagedObjectContext) {
|
||||||
|
self.context = context
|
||||||
|
self.backgroundContext = backgroundContext
|
||||||
|
providersSubject = CurrentValueSubject([])
|
||||||
|
lastUpdatedSubject = 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<[ProviderMetadata], Never> {
|
||||||
|
providersSubject
|
||||||
|
.removeDuplicates()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated var lastUpdatedPublisher: AnyPublisher<[ProviderID: Date], Never> {
|
||||||
|
lastUpdatedSubject
|
||||||
|
.removeDuplicates()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func store(_ index: [ProviderMetadata]) async throws {
|
||||||
|
try await backgroundContext.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(backgroundContext.delete)
|
||||||
|
|
||||||
|
// replace but retain last update
|
||||||
|
let mapper = CoreDataMapper(context: backgroundContext)
|
||||||
|
index.forEach {
|
||||||
|
let lastUpdate = lastUpdatesByProvider[$0.id.rawValue]
|
||||||
|
mapper.cdProvider(from: $0, lastUpdate: lastUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
try backgroundContext.save()
|
||||||
|
} catch {
|
||||||
|
backgroundContext.rollback()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func store(_ infrastructure: VPNInfrastructure, for providerId: ProviderID) async throws {
|
||||||
|
try await backgroundContext.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.lastUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete all provider entities
|
||||||
|
let serverRequest = CDVPNServerV3.fetchRequest()
|
||||||
|
serverRequest.predicate = predicate
|
||||||
|
let servers = try serverRequest.execute()
|
||||||
|
servers.forEach(backgroundContext.delete)
|
||||||
|
|
||||||
|
let presetRequest = CDVPNPresetV3.fetchRequest()
|
||||||
|
presetRequest.predicate = predicate
|
||||||
|
let presets = try presetRequest.execute()
|
||||||
|
presets.forEach(backgroundContext.delete)
|
||||||
|
|
||||||
|
// create new entities
|
||||||
|
let mapper = CoreDataMapper(context: backgroundContext)
|
||||||
|
try infrastructure.servers.forEach {
|
||||||
|
try mapper.cdServer(from: $0)
|
||||||
|
}
|
||||||
|
try infrastructure.presets.forEach {
|
||||||
|
try mapper.cdPreset(from: $0)
|
||||||
|
}
|
||||||
|
|
||||||
|
try backgroundContext.save()
|
||||||
|
} catch {
|
||||||
|
backgroundContext.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:)))
|
||||||
|
lastUpdatedSubject.send(mapper.lastUpdated(from: entities))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
//
|
||||||
|
// CDVPNProviderServerRepositoryV3.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 AppData
|
||||||
|
import CoreData
|
||||||
|
import Foundation
|
||||||
|
import PassepartoutKit
|
||||||
|
import UtilsLibrary
|
||||||
|
|
||||||
|
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: ProviderConfigurationIdentifiable {
|
||||||
|
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()
|
||||||
|
let categoryNames = serversResults.compactMap {
|
||||||
|
$0.object(forKey: "categoryName") as? String
|
||||||
|
}
|
||||||
|
let countryCodes = serversResults.compactMap {
|
||||||
|
$0.object(forKey: "countryCode") as? String
|
||||||
|
}
|
||||||
|
|
||||||
|
let presetsRequest = CDVPNPresetV3.fetchRequest()
|
||||||
|
presetsRequest.predicate = NSPredicate(
|
||||||
|
format: "providerId == %@ AND configurationId == %@", self.providerId.rawValue,
|
||||||
|
Configuration.providerConfigurationIdentifier
|
||||||
|
)
|
||||||
|
let presetsResults = try presetsRequest.execute()
|
||||||
|
|
||||||
|
return VPNFilterOptions(
|
||||||
|
categoryNames: Set(categoryNames),
|
||||||
|
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:))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
//
|
||||||
|
// CDProviderV3.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 CoreData
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@objc(CDProviderV3)
|
||||||
|
final class CDProviderV3: NSManagedObject {
|
||||||
|
@nonobjc static func fetchRequest() -> NSFetchRequest<CDProviderV3> {
|
||||||
|
NSFetchRequest<CDProviderV3>(entityName: "CDProviderV3")
|
||||||
|
}
|
||||||
|
|
||||||
|
@NSManaged var providerId: String?
|
||||||
|
@NSManaged var fullName: String?
|
||||||
|
@NSManaged var supportedConfigurationIds: String?
|
||||||
|
@NSManaged var lastUpdate: Date?
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
//
|
||||||
|
// CDVPNPresetV3.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 CoreData
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@objc(CDVPNPresetV3)
|
||||||
|
final class CDVPNPresetV3: NSManagedObject {
|
||||||
|
@nonobjc static func fetchRequest() -> NSFetchRequest<CDVPNPresetV3> {
|
||||||
|
NSFetchRequest<CDVPNPresetV3>(entityName: "CDVPNPresetV3")
|
||||||
|
}
|
||||||
|
|
||||||
|
@NSManaged var presetId: String?
|
||||||
|
@NSManaged var presetDescription: String?
|
||||||
|
@NSManaged var providerId: String?
|
||||||
|
@NSManaged var endpoints: Data?
|
||||||
|
@NSManaged var configurationId: String?
|
||||||
|
@NSManaged var configuration: Data?
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
//
|
||||||
|
// CDVPNServerV3.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 CoreData
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@objc(CDVPNServerV3)
|
||||||
|
final class CDVPNServerV3: NSManagedObject {
|
||||||
|
@nonobjc static func fetchRequest() -> NSFetchRequest<CDVPNServerV3> {
|
||||||
|
NSFetchRequest<CDVPNServerV3>(entityName: "CDVPNServerV3")
|
||||||
|
}
|
||||||
|
|
||||||
|
@NSManaged var serverId: String?
|
||||||
|
@NSManaged var hostname: String?
|
||||||
|
@NSManaged var ipAddresses: Data?
|
||||||
|
@NSManaged var providerId: String?
|
||||||
|
@NSManaged var countryCode: String?
|
||||||
|
@NSManaged var supportedConfigurationIds: String?
|
||||||
|
@NSManaged var supportedPresetIds: String?
|
||||||
|
@NSManaged var categoryName: String?
|
||||||
|
@NSManaged var localizedCountry: String?
|
||||||
|
@NSManaged var otherCountryCodes: String?
|
||||||
|
@NSManaged var area: String?
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
//
|
||||||
|
// Mapper.swift
|
||||||
|
// Passepartout
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 10/28/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
|
||||||
|
import PassepartoutKit
|
||||||
|
|
||||||
|
struct CoreDataMapper {
|
||||||
|
let context: NSManagedObjectContext
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func cdProvider(from metadata: ProviderMetadata, lastUpdate: Date?) -> CDProviderV3 {
|
||||||
|
let entity = CDProviderV3(context: context)
|
||||||
|
entity.providerId = metadata.id.rawValue
|
||||||
|
entity.fullName = metadata.description
|
||||||
|
entity.supportedConfigurationIds = metadata.supportedConfigurationIdentifiers.joined(separator: ",")
|
||||||
|
entity.lastUpdate = lastUpdate
|
||||||
|
return entity
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func cdServer(from server: VPNServer) throws -> CDVPNServerV3 {
|
||||||
|
let entity = CDVPNServerV3(context: context)
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
entity.serverId = server.serverId
|
||||||
|
entity.hostname = server.hostname
|
||||||
|
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: ",")
|
||||||
|
return entity
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func cdPreset(from preset: AnyVPNPreset) throws -> CDVPNPresetV3 {
|
||||||
|
let entity = CDVPNPresetV3(context: self.context)
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
entity.presetId = preset.presetId
|
||||||
|
entity.providerId = preset.providerId.rawValue
|
||||||
|
entity.presetDescription = preset.description
|
||||||
|
entity.endpoints = try encoder.encode(preset.endpoints)
|
||||||
|
entity.configurationId = preset.configurationIdentifier
|
||||||
|
entity.configuration = preset.configuration
|
||||||
|
return entity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DomainMapper {
|
||||||
|
func provider(from entity: CDProviderV3) -> ProviderMetadata? {
|
||||||
|
guard let id = entity.providerId,
|
||||||
|
let fullName = entity.fullName,
|
||||||
|
let supportedConfigurationIds = entity.supportedConfigurationIds else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ProviderMetadata(
|
||||||
|
id,
|
||||||
|
description: fullName,
|
||||||
|
supportedConfigurationIdentifiers: Set(supportedConfigurationIds.components(separatedBy: ","))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lastUpdated(from entities: [CDProviderV3]) -> [ProviderID: Date] {
|
||||||
|
entities.reduce(into: [:]) {
|
||||||
|
guard let id = $1.providerId,
|
||||||
|
let lastUpdate = $1.lastUpdate else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$0[.init(rawValue: id)] = lastUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func preset(from entity: CDVPNPresetV3) throws -> AnyVPNPreset? {
|
||||||
|
guard let presetId = entity.presetId,
|
||||||
|
let presetDescription = entity.presetDescription,
|
||||||
|
let providerId = entity.providerId,
|
||||||
|
let configurationId = entity.configurationId,
|
||||||
|
let configuration = entity.configuration else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
let endpoints = try entity.endpoints.map {
|
||||||
|
try decoder.decode([EndpointProtocol].self, from: $0)
|
||||||
|
} ?? []
|
||||||
|
|
||||||
|
return AnyVPNPreset(
|
||||||
|
providerId: .init(rawValue: providerId),
|
||||||
|
presetId: presetId,
|
||||||
|
description: presetDescription,
|
||||||
|
endpoints: endpoints,
|
||||||
|
configurationIdentifier: configurationId,
|
||||||
|
configuration: configuration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func server(from entity: CDVPNServerV3) throws -> VPNServer? {
|
||||||
|
guard let serverId = entity.serverId,
|
||||||
|
let providerId = entity.providerId,
|
||||||
|
let categoryName = entity.categoryName,
|
||||||
|
let countryCode = entity.countryCode else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
let hostname = entity.hostname
|
||||||
|
let ipAddresses = try entity.ipAddresses.map {
|
||||||
|
Set(try decoder.decode([Data].self, from: $0))
|
||||||
|
}
|
||||||
|
let supportedConfigurationIds = entity.supportedConfigurationIds?.components(separatedBy: ",")
|
||||||
|
let supportedPresetIds = entity.supportedPresetIds?.components(separatedBy: ",")
|
||||||
|
let otherCountryCodes = entity.otherCountryCodes?.components(separatedBy: ",")
|
||||||
|
let area = entity.area
|
||||||
|
|
||||||
|
let provider = VPNServer.Provider(
|
||||||
|
id: .init(rawValue: providerId),
|
||||||
|
serverId: serverId,
|
||||||
|
supportedConfigurationIdentifiers: supportedConfigurationIds,
|
||||||
|
supportedPresetIds: supportedPresetIds,
|
||||||
|
categoryName: categoryName,
|
||||||
|
countryCode: countryCode,
|
||||||
|
otherCountryCodes: otherCountryCodes,
|
||||||
|
area: area
|
||||||
|
)
|
||||||
|
return VPNServer(provider: provider, hostname: hostname, ipAddresses: ipAddresses)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
//
|
||||||
|
// VPNServerParameters+CoreData.swift
|
||||||
|
// Passepartout
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 10/28/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
|
||||||
|
|
||||||
|
extension ProviderID {
|
||||||
|
var predicate: NSPredicate {
|
||||||
|
NSPredicate(format: predicateFormat, predicateArg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate var predicateFormat: String {
|
||||||
|
"providerId == %@"
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate var predicateArg: String {
|
||||||
|
rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VPNSortField {
|
||||||
|
var sortDescriptor: NSSortDescriptor {
|
||||||
|
switch self {
|
||||||
|
case .localizedCountry:
|
||||||
|
return NSSortDescriptor(key: "localizedCountry", ascending: true)
|
||||||
|
|
||||||
|
case .area:
|
||||||
|
return NSSortDescriptor(key: "area", ascending: true)
|
||||||
|
|
||||||
|
case .hostname:
|
||||||
|
return NSSortDescriptor(key: "hostname", ascending: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VPNFilters {
|
||||||
|
func predicate(for providerId: ProviderID) -> NSPredicate {
|
||||||
|
var formats: [String] = []
|
||||||
|
var args: [Any] = []
|
||||||
|
|
||||||
|
formats.append(providerId.predicateFormat)
|
||||||
|
args.append(providerId.rawValue)
|
||||||
|
|
||||||
|
if let configurationIdentifier {
|
||||||
|
formats.append("supportedConfigurationIds contains %@")
|
||||||
|
args.append(configurationIdentifier)
|
||||||
|
}
|
||||||
|
if let presetId {
|
||||||
|
formats.append("supportedPresetIds contains %@")
|
||||||
|
args.append(presetId)
|
||||||
|
}
|
||||||
|
if let categoryName {
|
||||||
|
formats.append("categoryName == %@")
|
||||||
|
args.append(categoryName)
|
||||||
|
}
|
||||||
|
if let countryCode {
|
||||||
|
formats.append("countryCode == %@")
|
||||||
|
args.append(countryCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
let format = formats.joined(separator: " AND ")
|
||||||
|
return NSPredicate(format: format, argumentArray: args)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?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="">
|
||||||
|
<entity name="CDProviderV3" representedClassName="CDProviderV3" syncable="YES">
|
||||||
|
<attribute name="fullName" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="lastUpdate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="providerId" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="supportedConfigurationIds" optional="YES" attributeType="String"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="CDVPNPresetV3" representedClassName="CDVPNPresetV3" syncable="YES">
|
||||||
|
<attribute name="configuration" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="configurationId" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="endpoints" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="presetDescription" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="presetId" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="providerId" optional="YES" attributeType="String"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="CDVPNServerV3" representedClassName="CDVPNServerV3" syncable="YES">
|
||||||
|
<attribute name="area" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="categoryName" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="countryCode" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="hostname" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="ipAddresses" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="localizedCountry" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="otherCountryCodes" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="providerId" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="serverId" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="supportedConfigurationIds" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="supportedPresetIds" optional="YES" attributeType="String"/>
|
||||||
|
</entity>
|
||||||
|
</model>
|
|
@ -31,10 +31,17 @@ extension VPNFiltersView {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class Model: ObservableObject {
|
final class Model: ObservableObject {
|
||||||
|
typealias CodeWithDescription = (code: String, description: String)
|
||||||
|
|
||||||
|
private var options: VPNFilterOptions
|
||||||
|
|
||||||
|
@Published
|
||||||
private(set) var categories: [String]
|
private(set) var categories: [String]
|
||||||
|
|
||||||
private(set) var countries: [(code: String, description: String)]
|
@Published
|
||||||
|
private(set) var countries: [CodeWithDescription]
|
||||||
|
|
||||||
|
@Published
|
||||||
private(set) var presets: [AnyVPNPreset]
|
private(set) var presets: [AnyVPNPreset]
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
|
@ -47,48 +54,69 @@ extension VPNFiltersView {
|
||||||
|
|
||||||
let onlyShowsFavoritesDidChange = PassthroughSubject<Bool, Never>()
|
let onlyShowsFavoritesDidChange = PassthroughSubject<Bool, Never>()
|
||||||
|
|
||||||
init(
|
init() {
|
||||||
categories: [String] = [],
|
options = VPNFilterOptions()
|
||||||
countries: [(code: String, description: String)] = [],
|
categories = []
|
||||||
presets: [AnyVPNPreset] = []
|
countries = []
|
||||||
) {
|
presets = []
|
||||||
self.categories = categories
|
|
||||||
self.countries = countries
|
|
||||||
self.presets = presets
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func load<C>(
|
func load(options: VPNFilterOptions, initialFilters: VPNFilters?) {
|
||||||
with vpnManager: VPNProviderManager<C>,
|
self.options = options
|
||||||
initialFilters: VPNFilters?
|
setCategories(withNames: options.categoryNames)
|
||||||
) where C: ProviderConfigurationIdentifiable {
|
setCountries(withCodes: options.countryCodes)
|
||||||
categories = vpnManager
|
setPresets(with: options.presets)
|
||||||
.allCategoryNames
|
|
||||||
.sorted()
|
|
||||||
|
|
||||||
countries = vpnManager
|
|
||||||
.allCountryCodes
|
|
||||||
.map {
|
|
||||||
(code: $0, description: $0.localizedAsRegionCode ?? $0)
|
|
||||||
}
|
|
||||||
.sorted {
|
|
||||||
$0.description < $1.description
|
|
||||||
}
|
|
||||||
|
|
||||||
presets = vpnManager
|
|
||||||
.allPresets
|
|
||||||
.values
|
|
||||||
.filter {
|
|
||||||
$0.configurationIdentifier == C.providerConfigurationIdentifier
|
|
||||||
}
|
|
||||||
.sorted {
|
|
||||||
$0.description < $1.description
|
|
||||||
}
|
|
||||||
|
|
||||||
if let initialFilters {
|
if let initialFilters {
|
||||||
filters = initialFilters
|
filters = initialFilters
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
objectWillChange.send()
|
func update(with servers: [VPNServer]) {
|
||||||
|
|
||||||
|
// only non-empty countries
|
||||||
|
let knownCountryCodes = Set(servers.map(\.provider.countryCode))
|
||||||
|
|
||||||
|
// only presets known in filtered servers
|
||||||
|
var knownPresets = options.presets
|
||||||
|
let allPresetIds = Set(servers.compactMap(\.provider.supportedPresetIds).joined())
|
||||||
|
if !allPresetIds.isEmpty {
|
||||||
|
knownPresets = knownPresets
|
||||||
|
.filter {
|
||||||
|
allPresetIds.contains($0.presetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCountries(withCodes: knownCountryCodes)
|
||||||
|
setPresets(with: knownPresets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension VPNFiltersView.Model {
|
||||||
|
func setCategories(withNames categoryNames: Set<String>) {
|
||||||
|
categories = categoryNames
|
||||||
|
.sorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCountries(withCodes codes: Set<String>) {
|
||||||
|
countries = codes
|
||||||
|
.map(\.asCountryCodeWithDescription)
|
||||||
|
.sorted {
|
||||||
|
$0.description < $1.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPresets(with presets: Set<AnyVPNPreset>) {
|
||||||
|
self.presets = presets
|
||||||
|
.sorted {
|
||||||
|
$0.description < $1.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
var asCountryCodeWithDescription: VPNFiltersView.Model.CodeWithDescription {
|
||||||
|
(self, localizedAsRegionCode ?? self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -53,6 +53,9 @@ struct VPNFiltersView: View {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: model.filters.categoryName) { _ in
|
||||||
|
model.filters.countryCode = nil
|
||||||
|
}
|
||||||
.onChange(of: model.filters) {
|
.onChange(of: model.filters) {
|
||||||
model.filtersDidChange.send($0)
|
model.filtersDidChange.send($0)
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,6 @@ struct VPNProviderServerView<Configuration>: View where Configuration: ProviderC
|
||||||
var body: some View {
|
var body: some View {
|
||||||
debugChanges()
|
debugChanges()
|
||||||
return contentView
|
return contentView
|
||||||
.navigationTitle(Strings.Global.servers)
|
|
||||||
.themeNavigationDetail()
|
.themeNavigationDetail()
|
||||||
.withErrorHandler(errorHandler)
|
.withErrorHandler(errorHandler)
|
||||||
}
|
}
|
||||||
|
@ -150,14 +149,12 @@ extension VPNProviderServerView {
|
||||||
.task {
|
.task {
|
||||||
do {
|
do {
|
||||||
favoritesManager.moduleId = moduleId
|
favoritesManager.moduleId = moduleId
|
||||||
vpnManager.repository = try await providerManager.vpnRepository(
|
let repository = try await providerManager.vpnServerRepository(
|
||||||
from: apis,
|
from: apis,
|
||||||
for: providerId
|
for: providerId
|
||||||
)
|
)
|
||||||
filtersViewModel.load(
|
try await vpnManager.setRepository(repository)
|
||||||
with: vpnManager,
|
filtersViewModel.load(options: vpnManager.options, initialFilters: initialFilters)
|
||||||
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 repository: \(error)")
|
||||||
|
@ -175,11 +172,16 @@ extension VPNProviderServerView {
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
favoritesManager.save()
|
favoritesManager.save()
|
||||||
}
|
}
|
||||||
|
.navigationTitle(title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension VPNProviderServerView.ServersView {
|
private extension VPNProviderServerView.ServersView {
|
||||||
|
var title: String {
|
||||||
|
providerManager.provider(withId: providerId)?.description ?? Strings.Global.servers
|
||||||
|
}
|
||||||
|
|
||||||
var filteredServers: [VPNServer] {
|
var filteredServers: [VPNServer] {
|
||||||
if onlyShowsFavorites {
|
if onlyShowsFavorites {
|
||||||
return servers.filter {
|
return servers.filter {
|
||||||
|
@ -191,10 +193,15 @@ private extension VPNProviderServerView.ServersView {
|
||||||
|
|
||||||
func reloadServers(filters: VPNFilters) async {
|
func reloadServers(filters: VPNFilters) async {
|
||||||
isFiltering = true
|
isFiltering = true
|
||||||
await Task {
|
do {
|
||||||
servers = await vpnManager.filteredServers(with: filters)
|
try await Task {
|
||||||
|
servers = try await vpnManager.filteredServers(with: filters)
|
||||||
|
filtersViewModel.update(with: servers)
|
||||||
isFiltering = false
|
isFiltering = false
|
||||||
}.value
|
}.value
|
||||||
|
} catch {
|
||||||
|
pp_log(.app, .error, "Unable to fetch filtered servers: \(error)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func compatiblePreset(with server: VPNServer) -> VPNPreset<Configuration>? {
|
func compatiblePreset(with server: VPNServer) -> VPNPreset<Configuration>? {
|
||||||
|
|
|
@ -74,6 +74,7 @@ extension VPNProviderServerView {
|
||||||
HStack {
|
HStack {
|
||||||
ThemeCountryFlag(code: server.provider.countryCode)
|
ThemeCountryFlag(code: server.provider.countryCode)
|
||||||
Text(server.region)
|
Text(server.region)
|
||||||
|
.help(server.region)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,6 +93,8 @@ extension VPNProviderServerView {
|
||||||
onSelect(server)
|
onSelect(server)
|
||||||
} label: {
|
} label: {
|
||||||
Text(selectTitle)
|
Text(selectTitle)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,8 @@ public struct Constants: Decodable, Sendable {
|
||||||
|
|
||||||
public let remote: String
|
public let remote: String
|
||||||
|
|
||||||
|
public let providers: String
|
||||||
|
|
||||||
public let legacyV2: String
|
public let legacyV2: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
"containers": {
|
"containers": {
|
||||||
"local": "Profiles-v3",
|
"local": "Profiles-v3",
|
||||||
"remote": "Profiles-v3.remote",
|
"remote": "Profiles-v3.remote",
|
||||||
|
"providers": "Providers-v3",
|
||||||
"legacyV2": "Profiles"
|
"legacyV2": "Profiles"
|
||||||
},
|
},
|
||||||
"websites": {
|
"websites": {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import Foundation
|
||||||
@objc(CDProfile)
|
@objc(CDProfile)
|
||||||
final class CDProfile: NSManagedObject {
|
final class CDProfile: NSManagedObject {
|
||||||
@nonobjc static func fetchRequest() -> NSFetchRequest<CDProfile> {
|
@nonobjc static func fetchRequest() -> NSFetchRequest<CDProfile> {
|
||||||
return NSFetchRequest<CDProfile>(entityName: "CDProfile")
|
NSFetchRequest<CDProfile>(entityName: "CDProfile")
|
||||||
}
|
}
|
||||||
|
|
||||||
@NSManaged var json: Data?
|
@NSManaged var json: Data?
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
|
|
||||||
import AppData
|
import AppData
|
||||||
import AppDataProfiles
|
import AppDataProfiles
|
||||||
|
import AppDataProviders
|
||||||
import AppLibrary
|
import AppLibrary
|
||||||
import AppUI
|
import AppUI
|
||||||
import CommonLibrary
|
import CommonLibrary
|
||||||
|
@ -226,11 +227,21 @@ private extension ProfileManager {
|
||||||
|
|
||||||
// MARK: -
|
// MARK: -
|
||||||
|
|
||||||
// FIXME: #705, store providers to Core Data
|
|
||||||
extension ProviderManager {
|
extension ProviderManager {
|
||||||
static let shared = ProviderManager(
|
static let shared: ProviderManager = {
|
||||||
repository: InMemoryProviderRepository()
|
let store = CoreDataPersistentStore(
|
||||||
|
logger: .default,
|
||||||
|
containerName: Constants.shared.containers.providers,
|
||||||
|
model: AppData.cdProvidersModel,
|
||||||
|
cloudKitIdentifier: nil,
|
||||||
|
author: nil
|
||||||
)
|
)
|
||||||
|
let repository = AppData.cdProviderRepositoryV3(
|
||||||
|
context: store.context,
|
||||||
|
backgroundContext: store.backgroundContext
|
||||||
|
)
|
||||||
|
return ProviderManager(repository: repository)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: -
|
// MARK: -
|
||||||
|
|
Loading…
Reference in New Issue