Redo provider managers lifecycle (#732)
Update library with more efficient choices for interacting with the providers API. Fixes #731
This commit is contained in:
parent
a5d4f6aee5
commit
87c7d63678
|
@ -41,8 +41,7 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
|
||||
"state" : {
|
||||
"revision" : "ebb142e836e9e2a8a1867c7ae3d4f44b6b96e917",
|
||||
"version" : "0.9.1"
|
||||
"revision" : "aeb982951e2798863e28f55081dd25e2221083e3"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -27,8 +27,8 @@ let package = Package(
|
|||
)
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.9.1"),
|
||||
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "0bfd4578b71a905584cdd5c9c39ab3087521af78"),
|
||||
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.9.0"),
|
||||
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "aeb982951e2798863e28f55081dd25e2221083e3"),
|
||||
// .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", revision: "031863a1cd683962a7dfe68e20b91fa820a1ecce"),
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
//
|
||||
// ProviderFactory.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/8/24.
|
||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
@MainActor
|
||||
public final class ProviderFactory: ObservableObject {
|
||||
public let providerManager: ProviderManager
|
||||
|
||||
public let vpnProviderManager: VPNProviderManager
|
||||
|
||||
public init(providerManager: ProviderManager, vpnProviderManager: VPNProviderManager) {
|
||||
self.providerManager = providerManager
|
||||
self.vpnProviderManager = vpnProviderManager
|
||||
}
|
||||
}
|
|
@ -46,7 +46,7 @@ public final class AppContext: ObservableObject {
|
|||
|
||||
public let registry: Registry
|
||||
|
||||
public let providerFactory: ProviderFactory
|
||||
public let providerManager: ProviderManager
|
||||
|
||||
private let constants: Constants
|
||||
|
||||
|
@ -59,7 +59,7 @@ public final class AppContext: ObservableObject {
|
|||
tunnel: Tunnel,
|
||||
tunnelEnvironment: TunnelEnvironment,
|
||||
registry: Registry,
|
||||
providerFactory: ProviderFactory,
|
||||
providerManager: ProviderManager,
|
||||
constants: Constants
|
||||
) {
|
||||
self.iapManager = iapManager
|
||||
|
@ -73,7 +73,7 @@ public final class AppContext: ObservableObject {
|
|||
interval: constants.tunnel.refreshInterval
|
||||
)
|
||||
self.registry = registry
|
||||
self.providerFactory = providerFactory
|
||||
self.providerManager = providerManager
|
||||
self.constants = constants
|
||||
subscriptions = []
|
||||
|
||||
|
|
|
@ -145,13 +145,13 @@ extension OnDemandModule.Policy: LocalizableEntity {
|
|||
}
|
||||
|
||||
extension VPNServer {
|
||||
public var sortableRegion: String {
|
||||
[countryCodes.first?.localizedAsRegionCode, area]
|
||||
public var region: String {
|
||||
[provider.countryCodes.first?.localizedAsRegionCode, provider.area]
|
||||
.compactMap { $0 }
|
||||
.joined(separator: " - ")
|
||||
}
|
||||
|
||||
public var sortableAddresses: String {
|
||||
public var address: String {
|
||||
if let hostname {
|
||||
return hostname
|
||||
}
|
||||
|
|
|
@ -62,9 +62,9 @@ extension AppContext {
|
|||
tunnel: Tunnel(strategy: FakeTunnelStrategy(environment: env)),
|
||||
tunnelEnvironment: env,
|
||||
registry: registry,
|
||||
providerFactory: ProviderFactory(
|
||||
providerManager: ProviderManager(repository: InMemoryProviderRepository()),
|
||||
vpnProviderManager: VPNProviderManager(repository: InMemoryVPNProviderRepository())
|
||||
providerManager: ProviderManager(
|
||||
repository: InMemoryProviderRepository(),
|
||||
vpnRepository: InMemoryVPNProviderRepository()
|
||||
),
|
||||
constants: .shared
|
||||
)
|
||||
|
@ -83,12 +83,6 @@ extension ProfileManager {
|
|||
}
|
||||
}
|
||||
|
||||
extension ProviderFactory {
|
||||
public static var mock: ProviderFactory {
|
||||
AppContext.mock.providerFactory
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileProcessor {
|
||||
public static var mock: ProfileProcessor {
|
||||
AppContext.mock.profileProcessor
|
||||
|
@ -107,6 +101,12 @@ extension ConnectionObserver {
|
|||
}
|
||||
}
|
||||
|
||||
extension ProviderManager {
|
||||
public static var mock: ProviderManager {
|
||||
AppContext.mock.providerManager
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Profile
|
||||
|
||||
extension Profile {
|
||||
|
|
|
@ -60,6 +60,9 @@ struct OpenVPNView: View {
|
|||
@Binding
|
||||
private var providerEntity: VPNEntity<OpenVPN.Configuration>?
|
||||
|
||||
@StateObject
|
||||
private var vpnProviderManager = VPNProviderManager()
|
||||
|
||||
init(serverConfiguration: OpenVPN.Configuration) {
|
||||
let module = OpenVPNModule.Builder(configurationBuilder: serverConfiguration.builder())
|
||||
let editor = ProfileEditor(modules: [module])
|
||||
|
@ -99,41 +102,35 @@ private extension OpenVPNView {
|
|||
var providerModifier: some ViewModifier {
|
||||
ProviderPanelModifier(
|
||||
isRequired: draft.configurationBuilder == nil,
|
||||
entityType: VPNEntity<OpenVPN.Configuration>.self,
|
||||
providerId: $providerId,
|
||||
selectedEntity: $providerEntity,
|
||||
providerContent: providerContentView
|
||||
providerContent: providerContentView,
|
||||
onSelectProvider: onSelectProvider
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var contentView: some View {
|
||||
credentialsView
|
||||
if providerId == nil {
|
||||
manualView
|
||||
}
|
||||
func providerContentView(providerId: ProviderID) -> some View {
|
||||
providerServerRow
|
||||
moduleGroup(for: accountRows)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func providerContentView(providerId: ProviderID, entity: VPNEntity<OpenVPN.Configuration>?) -> some View {
|
||||
NavigationLink(value: Subroute.providerServer(id: providerId)) {
|
||||
var providerServerRow: some View {
|
||||
NavigationLink(value: Subroute.providerServer) {
|
||||
HStack {
|
||||
Text("Server")
|
||||
if let entity {
|
||||
Text(Strings.Global.server)
|
||||
if let providerEntity {
|
||||
Spacer()
|
||||
Text(entity.server.hostname ?? entity.server.serverId)
|
||||
Text(providerEntity.server.hostname ?? providerEntity.server.serverId)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
credentialsView
|
||||
}
|
||||
|
||||
var credentialsView: some View {
|
||||
moduleSection(for: accountRows, header: Strings.Global.account)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var manualView: some View {
|
||||
var contentView: some View {
|
||||
moduleSection(for: accountRows, header: Strings.Global.account)
|
||||
moduleSection(for: remotesRows, header: Strings.Modules.Openvpn.remotes)
|
||||
if !isServerPushed {
|
||||
moduleSection(for: pullRows, header: Strings.Modules.Openvpn.pull)
|
||||
|
@ -159,6 +156,20 @@ private extension OpenVPNView {
|
|||
}
|
||||
|
||||
private extension OpenVPNView {
|
||||
func onSelectProvider(manager: ProviderManager) {
|
||||
guard let providerId else {
|
||||
return
|
||||
}
|
||||
vpnProviderManager.view = manager.vpnView(
|
||||
withId: providerId,
|
||||
initialParameters: .init(sorting: [
|
||||
.localizedCountry,
|
||||
.area,
|
||||
.hostname
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
func onSelect(server: VPNServer, preset: VPNPreset<OpenVPN.Configuration>) {
|
||||
providerEntity = VPNEntity(server: server, preset: preset)
|
||||
}
|
||||
|
@ -172,7 +183,7 @@ private extension OpenVPNView {
|
|||
|
||||
private extension OpenVPNView {
|
||||
enum Subroute: Hashable {
|
||||
case providerServer(id: ProviderID)
|
||||
case providerServer
|
||||
|
||||
case credentials
|
||||
}
|
||||
|
@ -180,9 +191,9 @@ private extension OpenVPNView {
|
|||
@ViewBuilder
|
||||
func destination(for route: Subroute) -> some View {
|
||||
switch route {
|
||||
case .providerServer(let id):
|
||||
case .providerServer:
|
||||
VPNProviderServerView<OpenVPN.Configuration>(
|
||||
providerId: id,
|
||||
manager: vpnProviderManager,
|
||||
onSelect: onSelect
|
||||
)
|
||||
|
||||
|
|
|
@ -71,11 +71,17 @@ struct HashableRoute: Hashable {
|
|||
}
|
||||
|
||||
extension View {
|
||||
func moduleSection(for rows: [ModuleRow]?, header: String) -> some View {
|
||||
func moduleGroup(for rows: [ModuleRow]?) -> some View {
|
||||
rows.map { rows in
|
||||
Group {
|
||||
ForEach(rows, id: \.self, content: moduleRowView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func moduleSection(for rows: [ModuleRow]?, header: String) -> some View {
|
||||
rows.map { rows in
|
||||
moduleGroup(for: rows)
|
||||
.themeSection(header: header)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,9 @@
|
|||
import AppLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
import UtilsLibrary
|
||||
|
||||
// FIXME: #703, providers UI, reorg subviews
|
||||
|
||||
struct ProviderPanelModifier<Entity, ProviderContent>: ViewModifier where Entity: ProviderEntity, Entity.Configuration: ProviderConfigurationIdentifiable & Codable, ProviderContent: View {
|
||||
|
||||
|
@ -36,28 +39,34 @@ struct ProviderPanelModifier<Entity, ProviderContent>: ViewModifier where Entity
|
|||
|
||||
let isRequired: Bool
|
||||
|
||||
let entityType: Entity.Type
|
||||
|
||||
@Binding
|
||||
var providerId: ProviderID?
|
||||
|
||||
@Binding
|
||||
var selectedEntity: Entity?
|
||||
|
||||
@ViewBuilder
|
||||
let providerContent: (ProviderID, Entity?) -> ProviderContent
|
||||
let providerContent: (ProviderID) -> ProviderContent
|
||||
|
||||
let onSelectProvider: (ProviderManager) -> Void
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
debugChanges()
|
||||
return Group {
|
||||
providerPicker
|
||||
.task {
|
||||
await refreshIndex()
|
||||
}
|
||||
.onLoad(perform: loadCurrentProvider)
|
||||
|
||||
if let providerId {
|
||||
providerContent(providerId, selectedEntity)
|
||||
providerContent(providerId)
|
||||
.asSectionWithTrailingContent {
|
||||
refreshButton
|
||||
}
|
||||
.disabled(providerManager.isLoading)
|
||||
} else if !isRequired {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ProviderPanelModifier {
|
||||
var supportedProviders: [ProviderMetadata] {
|
||||
|
@ -70,33 +79,91 @@ private extension ProviderPanelModifier {
|
|||
let hasProviders = !supportedProviders.isEmpty
|
||||
return Picker(Strings.Global.provider, selection: $providerId) {
|
||||
if hasProviders {
|
||||
Text(Strings.Global.none)
|
||||
// FIXME: #703, providers UI
|
||||
Text("Select a provider")
|
||||
.tag(nil as ProviderID?)
|
||||
ForEach(supportedProviders, id: \.id) {
|
||||
Text($0.description)
|
||||
.tag($0.id as ProviderID?)
|
||||
}
|
||||
} else {
|
||||
Text(" ") // enforce constant picker height on iOS
|
||||
// enforce constant picker height on iOS
|
||||
Text(providerManager.isLoading ? "..." : "Unavailable")
|
||||
.tag(providerId) // tag always exists
|
||||
}
|
||||
}
|
||||
.onChange(of: providerId) { _ in
|
||||
selectedEntity = nil
|
||||
.onChange(of: providerId) { newId in
|
||||
Task {
|
||||
if let newId {
|
||||
await refreshInfrastructure(for: newId)
|
||||
}
|
||||
onSelectProvider(providerManager)
|
||||
}
|
||||
}
|
||||
.disabled(!hasProviders)
|
||||
}
|
||||
|
||||
var refreshButton: some View {
|
||||
Button {
|
||||
guard let providerId else {
|
||||
return
|
||||
}
|
||||
Task {
|
||||
await refreshInfrastructure(for: providerId)
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text(Strings.Views.Provider.Vpn.refreshInfrastructure)
|
||||
#if os(iOS)
|
||||
if let providerId, providerManager.pendingServices.contains(.provider(providerId)) {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.disabled(providerManager.isLoading || providerId == nil)
|
||||
}
|
||||
}
|
||||
|
||||
private extension ProviderPanelModifier {
|
||||
func loadCurrentProvider() {
|
||||
Task {
|
||||
if let providerId {
|
||||
async let index = await refreshIndex()
|
||||
async let provider = await refreshInfrastructure(for: providerId)
|
||||
_ = await (index, provider)
|
||||
onSelectProvider(providerManager)
|
||||
} else {
|
||||
await refreshIndex()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: #707, fetch bundled providers on launch
|
||||
// FIXME: #704, rate-limit fetch
|
||||
func refreshIndex() async {
|
||||
@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,
|
||||
ofType: Entity.Configuration.self
|
||||
)
|
||||
return true
|
||||
} catch {
|
||||
// FIXME: #703, alert unable to refresh infrastructure
|
||||
pp_log(.app, .error, "Unable to refresh infrastructure: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -110,26 +177,17 @@ private extension ProviderID {
|
|||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
@State
|
||||
var providerId: ProviderID? = .hideme
|
||||
|
||||
@State
|
||||
var vpnEntity: VPNEntity<OpenVPN.Configuration>?
|
||||
|
||||
return List {
|
||||
List {
|
||||
EmptyView()
|
||||
.modifier(ProviderPanelModifier(
|
||||
apis: [API.bundled],
|
||||
isRequired: false,
|
||||
providerId: $providerId,
|
||||
selectedEntity: $vpnEntity,
|
||||
providerContent: { _, entity in
|
||||
HStack {
|
||||
entityType: VPNEntity<OpenVPN.Configuration>.self,
|
||||
providerId: .constant(.hideme),
|
||||
providerContent: { _ in
|
||||
Text("Server")
|
||||
Spacer()
|
||||
Text(entity?.server.serverId ?? "None")
|
||||
}
|
||||
}
|
||||
},
|
||||
onSelectProvider: { _ in }
|
||||
))
|
||||
}
|
||||
.withMockEnvironment()
|
||||
|
|
|
@ -31,16 +31,12 @@ struct VPNFiltersModifier<Configuration>: ViewModifier where Configuration: Deco
|
|||
@ObservedObject
|
||||
var manager: VPNProviderManager
|
||||
|
||||
let providerId: ProviderID
|
||||
|
||||
let onRefresh: () async -> Void
|
||||
|
||||
@State
|
||||
var isFiltersPresented = false
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
contentView(with: content)
|
||||
.onChange(of: manager.filters) { _ in
|
||||
.onChange(of: manager.parameters.filters) { _ in
|
||||
Task {
|
||||
await manager.applyFilters()
|
||||
}
|
||||
|
|
|
@ -34,10 +34,6 @@ struct VPNFiltersView<Configuration>: View where Configuration: Decodable {
|
|||
@ObservedObject
|
||||
var manager: VPNProviderManager
|
||||
|
||||
let providerId: ProviderID
|
||||
|
||||
let onRefresh: () async -> Void
|
||||
|
||||
@State
|
||||
private var isRefreshing = false
|
||||
|
||||
|
@ -54,22 +50,16 @@ struct VPNFiltersView<Configuration>: View where Configuration: Decodable {
|
|||
HStack {
|
||||
Spacer()
|
||||
clearFiltersButton
|
||||
refreshButton
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if os(iOS)
|
||||
Section {
|
||||
refreshButton
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension VPNFiltersView {
|
||||
var categoryPicker: some View {
|
||||
Picker("Category", selection: $manager.filters.categoryName) {
|
||||
Picker("Category", selection: $manager.parameters.filters.categoryName) {
|
||||
Text("Any")
|
||||
.tag(nil as String?)
|
||||
ForEach(categories, id: \.self) {
|
||||
|
@ -80,7 +70,7 @@ private extension VPNFiltersView {
|
|||
}
|
||||
|
||||
var countryPicker: some View {
|
||||
Picker("Country", selection: $manager.filters.countryCode) {
|
||||
Picker("Country", selection: $manager.parameters.filters.countryCode) {
|
||||
Text("Any")
|
||||
.tag(nil as String?)
|
||||
ForEach(countries, id: \.code) {
|
||||
|
@ -92,8 +82,8 @@ private extension VPNFiltersView {
|
|||
|
||||
@ViewBuilder
|
||||
var presetPicker: some View {
|
||||
if manager.anyPresets.count > 1 {
|
||||
Picker("Preset", selection: $manager.filters.presetId) {
|
||||
if manager.allPresets.count > 1 {
|
||||
Picker("Preset", selection: $manager.parameters.filters.presetId) {
|
||||
Text("Any")
|
||||
.tag(nil as String?)
|
||||
ForEach(presets, id: \.presetId) {
|
||||
|
@ -111,27 +101,6 @@ private extension VPNFiltersView {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
var refreshButton: some View {
|
||||
Button {
|
||||
Task {
|
||||
isRefreshing = true
|
||||
await onRefresh()
|
||||
isRefreshing = false
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text(Strings.Views.Provider.Vpn.refreshInfrastructure)
|
||||
#if os(iOS)
|
||||
if isRefreshing {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.disabled(isRefreshing)
|
||||
}
|
||||
}
|
||||
|
||||
private extension VPNFiltersView {
|
||||
|
@ -149,7 +118,7 @@ private extension VPNFiltersView {
|
|||
let allCodes = manager
|
||||
.allServers
|
||||
.values
|
||||
.flatMap(\.countryCodes)
|
||||
.flatMap(\.provider.countryCodes)
|
||||
|
||||
return Set(allCodes)
|
||||
.map {
|
||||
|
@ -171,10 +140,6 @@ private extension VPNFiltersView {
|
|||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
VPNFiltersView<String>(
|
||||
manager: ProviderFactory.mock.vpnProviderManager,
|
||||
providerId: .hideme,
|
||||
onRefresh: {}
|
||||
)
|
||||
VPNFiltersView<String>(manager: VPNProviderManager())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,66 +29,24 @@ import SwiftUI
|
|||
|
||||
struct VPNProviderServerView<Configuration>: View where Configuration: ProviderConfigurationIdentifiable & Hashable & Codable {
|
||||
|
||||
@EnvironmentObject
|
||||
private var providerManager: ProviderManager
|
||||
|
||||
@EnvironmentObject
|
||||
private var vpnProviderManager: VPNProviderManager
|
||||
|
||||
@Environment(\.dismiss)
|
||||
private var dismiss
|
||||
|
||||
var apis: [APIMapper] = API.shared
|
||||
|
||||
let providerId: ProviderID
|
||||
@ObservedObject
|
||||
var manager: VPNProviderManager
|
||||
|
||||
let onSelect: (_ server: VPNServer, _ preset: VPNPreset<Configuration>) -> Void
|
||||
|
||||
@State
|
||||
private var isLoading = true
|
||||
|
||||
@State
|
||||
var sortOrder = [
|
||||
KeyPathComparator(\VPNServer.sortableRegion)
|
||||
]
|
||||
|
||||
@State
|
||||
var sortedServers: [VPNServer] = []
|
||||
|
||||
// FIXME: #703, flickers on appear
|
||||
var body: some View {
|
||||
serversView
|
||||
.modifier(VPNFiltersModifier<Configuration>(
|
||||
manager: vpnProviderManager,
|
||||
providerId: providerId,
|
||||
onRefresh: {
|
||||
await refreshInfrastructure(for: providerId)
|
||||
}
|
||||
))
|
||||
.themeAnimation(on: isLoading, category: .providers)
|
||||
.navigationTitle(providerMetadata?.description ?? Strings.Global.servers)
|
||||
.task {
|
||||
await loadInfrastructure(for: providerId)
|
||||
}
|
||||
.onReceive(vpnProviderManager.$filteredServers, perform: onFilteredServers)
|
||||
}
|
||||
}
|
||||
|
||||
private extension VPNProviderServerView {
|
||||
var providerMetadata: ProviderMetadata? {
|
||||
providerManager.metadata(withId: providerId)
|
||||
.modifier(VPNFiltersModifier<Configuration>(manager: manager))
|
||||
.navigationTitle(Strings.Global.servers)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
extension VPNProviderServerView {
|
||||
func onFilteredServers(_ servers: [String: VPNServer]) {
|
||||
sortedServers = servers
|
||||
.values
|
||||
.sorted(using: sortOrder)
|
||||
}
|
||||
|
||||
func selectServer(_ server: VPNServer) {
|
||||
guard let preset = compatiblePreset(with: server) else {
|
||||
// FIXME: #703, alert select a preset
|
||||
|
@ -101,7 +59,7 @@ extension VPNProviderServerView {
|
|||
|
||||
private extension VPNProviderServerView {
|
||||
func compatiblePreset(with server: VPNServer) -> VPNPreset<Configuration>? {
|
||||
vpnProviderManager
|
||||
manager
|
||||
.presets(ofType: Configuration.self)
|
||||
.first {
|
||||
if let supportedIds = server.provider.supportedPresetIds {
|
||||
|
@ -110,38 +68,13 @@ private extension VPNProviderServerView {
|
|||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func loadInfrastructure(for providerId: ProviderID) async {
|
||||
await vpnProviderManager.setProvider(providerId)
|
||||
if await vpnProviderManager.lastUpdated() == nil {
|
||||
await refreshInfrastructure(for: providerId)
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
// FIXME: #704, rate-limit fetch
|
||||
func refreshInfrastructure(for providerId: ProviderID) async {
|
||||
do {
|
||||
isLoading = true
|
||||
try await vpnProviderManager.fetchInfrastructure(
|
||||
from: apis,
|
||||
for: providerId,
|
||||
ofType: Configuration.self
|
||||
)
|
||||
isLoading = false
|
||||
} catch {
|
||||
// FIXME: #703, alert unable to refresh infrastructure
|
||||
pp_log(.app, .error, "Unable to refresh infrastructure: \(error)")
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
VPNProviderServerView<OpenVPN.Configuration>(apis: [API.bundled], providerId: .protonvpn) { _, _ in
|
||||
VPNProviderServerView<OpenVPN.Configuration>(manager: VPNProviderManager()) { _, _ in
|
||||
}
|
||||
}
|
||||
.withMockEnvironment()
|
||||
|
|
|
@ -43,7 +43,7 @@ extension VPNFiltersModifier {
|
|||
}
|
||||
.themeModal(isPresented: $isFiltersPresented) {
|
||||
NavigationStack {
|
||||
VPNFiltersView<Configuration>(manager: manager, providerId: providerId, onRefresh: onRefresh)
|
||||
VPNFiltersView<Configuration>(manager: manager)
|
||||
.navigationTitle("Filters")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
|
|
@ -30,15 +30,15 @@ import SwiftUI
|
|||
// FIXME: #703, providers UI
|
||||
|
||||
extension VPNProviderServerView {
|
||||
|
||||
@ViewBuilder
|
||||
var serversView: some View {
|
||||
sortedServers.nilIfEmpty.map { servers in
|
||||
ForEach(sortedServers) { server in
|
||||
Button("\(server.hostname ?? server.id) \(server.countryCodes)") {
|
||||
ForEach(manager.filteredServers) { server in
|
||||
Button("\(server.hostname ?? server.id) \(server.provider.countryCodes)") {
|
||||
selectServer(server)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -30,7 +30,7 @@ import SwiftUI
|
|||
extension VPNFiltersModifier {
|
||||
func contentView(with content: Content) -> some View {
|
||||
VStack {
|
||||
VPNFiltersView<Configuration>(manager: manager, providerId: providerId, onRefresh: onRefresh)
|
||||
VPNFiltersView<Configuration>(manager: manager)
|
||||
.padding()
|
||||
content
|
||||
}
|
||||
|
|
|
@ -30,14 +30,18 @@ import SwiftUI
|
|||
// FIXME: #703, providers UI
|
||||
|
||||
extension VPNProviderServerView {
|
||||
|
||||
@ViewBuilder
|
||||
var serversView: some View {
|
||||
Table(sortedServers, sortOrder: $sortOrder) {
|
||||
TableColumn("Region", value: \.sortableRegion)
|
||||
Table(manager.filteredServers) {
|
||||
TableColumn("Region") { server in
|
||||
Text(server.region)
|
||||
}
|
||||
.width(max: 200.0)
|
||||
|
||||
TableColumn("Address", value: \.sortableAddresses)
|
||||
TableColumn("Address", value: \.address)
|
||||
|
||||
TableColumn("", value: \.serverId) { server in
|
||||
TableColumn("") { server in
|
||||
Button {
|
||||
selectServer(server)
|
||||
} label: {
|
||||
|
|
|
@ -31,10 +31,9 @@ extension View {
|
|||
public func withEnvironment(from context: AppContext, theme: Theme) -> some View {
|
||||
environmentObject(theme)
|
||||
.environmentObject(context.iapManager)
|
||||
.environmentObject(context.connectionObserver)
|
||||
.environmentObject(context.providerFactory.providerManager)
|
||||
.environmentObject(context.providerFactory.vpnProviderManager)
|
||||
.environmentObject(context.profileProcessor)
|
||||
.environmentObject(context.connectionObserver)
|
||||
.environmentObject(context.providerManager)
|
||||
}
|
||||
|
||||
public func withMockEnvironment() -> some View {
|
||||
|
|
|
@ -63,7 +63,7 @@ extension API {
|
|||
#if DEBUG
|
||||
[API.bundled]
|
||||
#else
|
||||
[API.remoteThenBundled]
|
||||
API.remoteThenBundled
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ extension AppContext {
|
|||
tunnel: .shared,
|
||||
tunnelEnvironment: .shared,
|
||||
registry: .shared,
|
||||
providerFactory: .shared,
|
||||
providerManager: .shared,
|
||||
constants: .shared
|
||||
)
|
||||
}
|
||||
|
@ -208,10 +208,10 @@ private extension ProfileManager {
|
|||
// MARK: -
|
||||
|
||||
// FIXME: #705, store providers to Core Data
|
||||
extension ProviderFactory {
|
||||
static let shared = ProviderFactory(
|
||||
providerManager: ProviderManager(repository: InMemoryProviderRepository()),
|
||||
vpnProviderManager: VPNProviderManager(repository: InMemoryVPNProviderRepository())
|
||||
extension ProviderManager {
|
||||
static let shared = ProviderManager(
|
||||
repository: InMemoryProviderRepository(),
|
||||
vpnRepository: InMemoryVPNProviderRepository()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue