Redo provider managers lifecycle (#732)

Update library with more efficient choices for interacting with the
providers API.

Fixes #731
This commit is contained in:
Davide 2024-10-13 11:36:34 +02:00 committed by GitHub
parent a5d4f6aee5
commit 87c7d63678
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 189 additions and 257 deletions

View File

@ -41,8 +41,7 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source", "location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
"state" : { "state" : {
"revision" : "ebb142e836e9e2a8a1867c7ae3d4f44b6b96e917", "revision" : "aeb982951e2798863e28f55081dd25e2221083e3"
"version" : "0.9.1"
} }
}, },
{ {

View File

@ -27,8 +27,8 @@ let package = Package(
) )
], ],
dependencies: [ dependencies: [
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.9.1"), // .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.9.0"),
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "0bfd4578b71a905584cdd5c9c39ab3087521af78"), .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "aeb982951e2798863e28f55081dd25e2221083e3"),
// .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"),

View File

@ -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
}
}

View File

@ -46,7 +46,7 @@ public final class AppContext: ObservableObject {
public let registry: Registry public let registry: Registry
public let providerFactory: ProviderFactory public let providerManager: ProviderManager
private let constants: Constants private let constants: Constants
@ -59,7 +59,7 @@ public final class AppContext: ObservableObject {
tunnel: Tunnel, tunnel: Tunnel,
tunnelEnvironment: TunnelEnvironment, tunnelEnvironment: TunnelEnvironment,
registry: Registry, registry: Registry,
providerFactory: ProviderFactory, providerManager: ProviderManager,
constants: Constants constants: Constants
) { ) {
self.iapManager = iapManager self.iapManager = iapManager
@ -73,7 +73,7 @@ public final class AppContext: ObservableObject {
interval: constants.tunnel.refreshInterval interval: constants.tunnel.refreshInterval
) )
self.registry = registry self.registry = registry
self.providerFactory = providerFactory self.providerManager = providerManager
self.constants = constants self.constants = constants
subscriptions = [] subscriptions = []

View File

@ -145,13 +145,13 @@ extension OnDemandModule.Policy: LocalizableEntity {
} }
extension VPNServer { extension VPNServer {
public var sortableRegion: String { public var region: String {
[countryCodes.first?.localizedAsRegionCode, area] [provider.countryCodes.first?.localizedAsRegionCode, provider.area]
.compactMap { $0 } .compactMap { $0 }
.joined(separator: " - ") .joined(separator: " - ")
} }
public var sortableAddresses: String { public var address: String {
if let hostname { if let hostname {
return hostname return hostname
} }

View File

@ -62,9 +62,9 @@ extension AppContext {
tunnel: Tunnel(strategy: FakeTunnelStrategy(environment: env)), tunnel: Tunnel(strategy: FakeTunnelStrategy(environment: env)),
tunnelEnvironment: env, tunnelEnvironment: env,
registry: registry, registry: registry,
providerFactory: ProviderFactory( providerManager: ProviderManager(
providerManager: ProviderManager(repository: InMemoryProviderRepository()), repository: InMemoryProviderRepository(),
vpnProviderManager: VPNProviderManager(repository: InMemoryVPNProviderRepository()) vpnRepository: InMemoryVPNProviderRepository()
), ),
constants: .shared constants: .shared
) )
@ -83,12 +83,6 @@ extension ProfileManager {
} }
} }
extension ProviderFactory {
public static var mock: ProviderFactory {
AppContext.mock.providerFactory
}
}
extension ProfileProcessor { extension ProfileProcessor {
public static var mock: ProfileProcessor { public static var mock: ProfileProcessor {
AppContext.mock.profileProcessor AppContext.mock.profileProcessor
@ -107,6 +101,12 @@ extension ConnectionObserver {
} }
} }
extension ProviderManager {
public static var mock: ProviderManager {
AppContext.mock.providerManager
}
}
// MARK: - Profile // MARK: - Profile
extension Profile { extension Profile {

View File

@ -60,6 +60,9 @@ struct OpenVPNView: View {
@Binding @Binding
private var providerEntity: VPNEntity<OpenVPN.Configuration>? private var providerEntity: VPNEntity<OpenVPN.Configuration>?
@StateObject
private var vpnProviderManager = VPNProviderManager()
init(serverConfiguration: OpenVPN.Configuration) { init(serverConfiguration: OpenVPN.Configuration) {
let module = OpenVPNModule.Builder(configurationBuilder: serverConfiguration.builder()) let module = OpenVPNModule.Builder(configurationBuilder: serverConfiguration.builder())
let editor = ProfileEditor(modules: [module]) let editor = ProfileEditor(modules: [module])
@ -99,41 +102,35 @@ private extension OpenVPNView {
var providerModifier: some ViewModifier { var providerModifier: some ViewModifier {
ProviderPanelModifier( ProviderPanelModifier(
isRequired: draft.configurationBuilder == nil, isRequired: draft.configurationBuilder == nil,
entityType: VPNEntity<OpenVPN.Configuration>.self,
providerId: $providerId, providerId: $providerId,
selectedEntity: $providerEntity, providerContent: providerContentView,
providerContent: providerContentView onSelectProvider: onSelectProvider
) )
} }
@ViewBuilder @ViewBuilder
var contentView: some View { func providerContentView(providerId: ProviderID) -> some View {
credentialsView providerServerRow
if providerId == nil { moduleGroup(for: accountRows)
manualView
}
} }
@ViewBuilder var providerServerRow: some View {
func providerContentView(providerId: ProviderID, entity: VPNEntity<OpenVPN.Configuration>?) -> some View { NavigationLink(value: Subroute.providerServer) {
NavigationLink(value: Subroute.providerServer(id: providerId)) {
HStack { HStack {
Text("Server") Text(Strings.Global.server)
if let entity { if let providerEntity {
Spacer() Spacer()
Text(entity.server.hostname ?? entity.server.serverId) Text(providerEntity.server.hostname ?? providerEntity.server.serverId)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} }
} }
credentialsView
}
var credentialsView: some View {
moduleSection(for: accountRows, header: Strings.Global.account)
} }
@ViewBuilder @ViewBuilder
var manualView: some View { var contentView: some View {
moduleSection(for: accountRows, header: Strings.Global.account)
moduleSection(for: remotesRows, header: Strings.Modules.Openvpn.remotes) moduleSection(for: remotesRows, header: Strings.Modules.Openvpn.remotes)
if !isServerPushed { if !isServerPushed {
moduleSection(for: pullRows, header: Strings.Modules.Openvpn.pull) moduleSection(for: pullRows, header: Strings.Modules.Openvpn.pull)
@ -159,6 +156,20 @@ private extension OpenVPNView {
} }
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>) { func onSelect(server: VPNServer, preset: VPNPreset<OpenVPN.Configuration>) {
providerEntity = VPNEntity(server: server, preset: preset) providerEntity = VPNEntity(server: server, preset: preset)
} }
@ -172,7 +183,7 @@ private extension OpenVPNView {
private extension OpenVPNView { private extension OpenVPNView {
enum Subroute: Hashable { enum Subroute: Hashable {
case providerServer(id: ProviderID) case providerServer
case credentials case credentials
} }
@ -180,9 +191,9 @@ private extension OpenVPNView {
@ViewBuilder @ViewBuilder
func destination(for route: Subroute) -> some View { func destination(for route: Subroute) -> some View {
switch route { switch route {
case .providerServer(let id): case .providerServer:
VPNProviderServerView<OpenVPN.Configuration>( VPNProviderServerView<OpenVPN.Configuration>(
providerId: id, manager: vpnProviderManager,
onSelect: onSelect onSelect: onSelect
) )

View File

@ -71,12 +71,18 @@ struct HashableRoute: Hashable {
} }
extension View { extension View {
func moduleSection(for rows: [ModuleRow]?, header: String) -> some View { func moduleGroup(for rows: [ModuleRow]?) -> some View {
rows.map { rows in rows.map { rows in
Group { Group {
ForEach(rows, id: \.self, content: moduleRowView) ForEach(rows, id: \.self, content: moduleRowView)
} }
.themeSection(header: header) }
}
func moduleSection(for rows: [ModuleRow]?, header: String) -> some View {
rows.map { rows in
moduleGroup(for: rows)
.themeSection(header: header)
} }
} }
} }

View File

@ -26,6 +26,9 @@
import AppLibrary import AppLibrary
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
import UtilsLibrary
// FIXME: #703, providers UI, reorg subviews
struct ProviderPanelModifier<Entity, ProviderContent>: ViewModifier where Entity: ProviderEntity, Entity.Configuration: ProviderConfigurationIdentifiable & Codable, ProviderContent: View { struct ProviderPanelModifier<Entity, ProviderContent>: ViewModifier where Entity: ProviderEntity, Entity.Configuration: ProviderConfigurationIdentifiable & Codable, ProviderContent: View {
@ -36,25 +39,31 @@ struct ProviderPanelModifier<Entity, ProviderContent>: ViewModifier where Entity
let isRequired: Bool let isRequired: Bool
let entityType: Entity.Type
@Binding @Binding
var providerId: ProviderID? var providerId: ProviderID?
@Binding
var selectedEntity: Entity?
@ViewBuilder @ViewBuilder
let providerContent: (ProviderID, Entity?) -> ProviderContent let providerContent: (ProviderID) -> ProviderContent
let onSelectProvider: (ProviderManager) -> Void
func body(content: Content) -> some View { func body(content: Content) -> some View {
providerPicker debugChanges()
.task { return Group {
await refreshIndex() providerPicker
} .onLoad(perform: loadCurrentProvider)
if let providerId { if let providerId {
providerContent(providerId, selectedEntity) providerContent(providerId)
} else if !isRequired { .asSectionWithTrailingContent {
content refreshButton
}
.disabled(providerManager.isLoading)
} else if !isRequired {
content
}
} }
} }
} }
@ -70,33 +79,91 @@ private extension ProviderPanelModifier {
let hasProviders = !supportedProviders.isEmpty let hasProviders = !supportedProviders.isEmpty
return Picker(Strings.Global.provider, selection: $providerId) { return Picker(Strings.Global.provider, selection: $providerId) {
if hasProviders { if hasProviders {
Text(Strings.Global.none) // FIXME: #703, providers UI
Text("Select a provider")
.tag(nil as ProviderID?) .tag(nil as ProviderID?)
ForEach(supportedProviders, id: \.id) { ForEach(supportedProviders, id: \.id) {
Text($0.description) Text($0.description)
.tag($0.id as ProviderID?) .tag($0.id as ProviderID?)
} }
} else { } else {
Text(" ") // enforce constant picker height on iOS // enforce constant picker height on iOS
Text(providerManager.isLoading ? "..." : "Unavailable")
.tag(providerId) // tag always exists .tag(providerId) // tag always exists
} }
} }
.onChange(of: providerId) { _ in .onChange(of: providerId) { newId in
selectedEntity = nil Task {
if let newId {
await refreshInfrastructure(for: newId)
}
onSelectProvider(providerManager)
}
} }
.disabled(!hasProviders) .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 { 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 @discardableResult
// FIXME: #704, rate-limit fetch func refreshIndex() async -> Bool {
func refreshIndex() async {
do { do {
try await providerManager.fetchIndex(from: apis) try await providerManager.fetchIndex(from: apis)
return true
} catch { } catch {
pp_log(.app, .error, "Unable to fetch index: \(error)") 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 // MARK: - Preview
#Preview { #Preview {
@State List {
var providerId: ProviderID? = .hideme
@State
var vpnEntity: VPNEntity<OpenVPN.Configuration>?
return List {
EmptyView() EmptyView()
.modifier(ProviderPanelModifier( .modifier(ProviderPanelModifier(
apis: [API.bundled], apis: [API.bundled],
isRequired: false, isRequired: false,
providerId: $providerId, entityType: VPNEntity<OpenVPN.Configuration>.self,
selectedEntity: $vpnEntity, providerId: .constant(.hideme),
providerContent: { _, entity in providerContent: { _ in
HStack { Text("Server")
Text("Server") },
Spacer() onSelectProvider: { _ in }
Text(entity?.server.serverId ?? "None")
}
}
)) ))
} }
.withMockEnvironment() .withMockEnvironment()

View File

@ -31,16 +31,12 @@ struct VPNFiltersModifier<Configuration>: ViewModifier where Configuration: Deco
@ObservedObject @ObservedObject
var manager: VPNProviderManager var manager: VPNProviderManager
let providerId: ProviderID
let onRefresh: () async -> Void
@State @State
var isFiltersPresented = false var isFiltersPresented = false
func body(content: Content) -> some View { func body(content: Content) -> some View {
contentView(with: content) contentView(with: content)
.onChange(of: manager.filters) { _ in .onChange(of: manager.parameters.filters) { _ in
Task { Task {
await manager.applyFilters() await manager.applyFilters()
} }

View File

@ -34,10 +34,6 @@ struct VPNFiltersView<Configuration>: View where Configuration: Decodable {
@ObservedObject @ObservedObject
var manager: VPNProviderManager var manager: VPNProviderManager
let providerId: ProviderID
let onRefresh: () async -> Void
@State @State
private var isRefreshing = false private var isRefreshing = false
@ -54,22 +50,16 @@ struct VPNFiltersView<Configuration>: View where Configuration: Decodable {
HStack { HStack {
Spacer() Spacer()
clearFiltersButton clearFiltersButton
refreshButton
} }
#endif #endif
} }
#if os(iOS)
Section {
refreshButton
}
#endif
} }
} }
} }
private extension VPNFiltersView { private extension VPNFiltersView {
var categoryPicker: some View { var categoryPicker: some View {
Picker("Category", selection: $manager.filters.categoryName) { Picker("Category", selection: $manager.parameters.filters.categoryName) {
Text("Any") Text("Any")
.tag(nil as String?) .tag(nil as String?)
ForEach(categories, id: \.self) { ForEach(categories, id: \.self) {
@ -80,7 +70,7 @@ private extension VPNFiltersView {
} }
var countryPicker: some View { var countryPicker: some View {
Picker("Country", selection: $manager.filters.countryCode) { Picker("Country", selection: $manager.parameters.filters.countryCode) {
Text("Any") Text("Any")
.tag(nil as String?) .tag(nil as String?)
ForEach(countries, id: \.code) { ForEach(countries, id: \.code) {
@ -92,8 +82,8 @@ private extension VPNFiltersView {
@ViewBuilder @ViewBuilder
var presetPicker: some View { var presetPicker: some View {
if manager.anyPresets.count > 1 { if manager.allPresets.count > 1 {
Picker("Preset", selection: $manager.filters.presetId) { Picker("Preset", selection: $manager.parameters.filters.presetId) {
Text("Any") Text("Any")
.tag(nil as String?) .tag(nil as String?)
ForEach(presets, id: \.presetId) { 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 { private extension VPNFiltersView {
@ -149,7 +118,7 @@ private extension VPNFiltersView {
let allCodes = manager let allCodes = manager
.allServers .allServers
.values .values
.flatMap(\.countryCodes) .flatMap(\.provider.countryCodes)
return Set(allCodes) return Set(allCodes)
.map { .map {
@ -171,10 +140,6 @@ private extension VPNFiltersView {
#Preview { #Preview {
NavigationStack { NavigationStack {
VPNFiltersView<String>( VPNFiltersView<String>(manager: VPNProviderManager())
manager: ProviderFactory.mock.vpnProviderManager,
providerId: .hideme,
onRefresh: {}
)
} }
} }

View File

@ -29,66 +29,24 @@ import SwiftUI
struct VPNProviderServerView<Configuration>: View where Configuration: ProviderConfigurationIdentifiable & Hashable & Codable { struct VPNProviderServerView<Configuration>: View where Configuration: ProviderConfigurationIdentifiable & Hashable & Codable {
@EnvironmentObject
private var providerManager: ProviderManager
@EnvironmentObject
private var vpnProviderManager: VPNProviderManager
@Environment(\.dismiss) @Environment(\.dismiss)
private var dismiss private var dismiss
var apis: [APIMapper] = API.shared @ObservedObject
var manager: VPNProviderManager
let providerId: ProviderID
let onSelect: (_ server: VPNServer, _ preset: VPNPreset<Configuration>) -> Void 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 { var body: some View {
serversView serversView
.modifier(VPNFiltersModifier<Configuration>( .modifier(VPNFiltersModifier<Configuration>(manager: manager))
manager: vpnProviderManager, .navigationTitle(Strings.Global.servers)
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)
} }
} }
// MARK: - Actions // MARK: - Actions
extension VPNProviderServerView { extension VPNProviderServerView {
func onFilteredServers(_ servers: [String: VPNServer]) {
sortedServers = servers
.values
.sorted(using: sortOrder)
}
func selectServer(_ server: VPNServer) { func selectServer(_ server: VPNServer) {
guard let preset = compatiblePreset(with: server) else { guard let preset = compatiblePreset(with: server) else {
// FIXME: #703, alert select a preset // FIXME: #703, alert select a preset
@ -101,7 +59,7 @@ extension VPNProviderServerView {
private extension VPNProviderServerView { private extension VPNProviderServerView {
func compatiblePreset(with server: VPNServer) -> VPNPreset<Configuration>? { func compatiblePreset(with server: VPNServer) -> VPNPreset<Configuration>? {
vpnProviderManager manager
.presets(ofType: Configuration.self) .presets(ofType: Configuration.self)
.first { .first {
if let supportedIds = server.provider.supportedPresetIds { if let supportedIds = server.provider.supportedPresetIds {
@ -110,38 +68,13 @@ private extension VPNProviderServerView {
return true 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 // MARK: - Preview
#Preview { #Preview {
NavigationStack { NavigationStack {
VPNProviderServerView<OpenVPN.Configuration>(apis: [API.bundled], providerId: .protonvpn) { _, _ in VPNProviderServerView<OpenVPN.Configuration>(manager: VPNProviderManager()) { _, _ in
} }
} }
.withMockEnvironment() .withMockEnvironment()

View File

@ -43,7 +43,7 @@ extension VPNFiltersModifier {
} }
.themeModal(isPresented: $isFiltersPresented) { .themeModal(isPresented: $isFiltersPresented) {
NavigationStack { NavigationStack {
VPNFiltersView<Configuration>(manager: manager, providerId: providerId, onRefresh: onRefresh) VPNFiltersView<Configuration>(manager: manager)
.navigationTitle("Filters") .navigationTitle("Filters")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }

View File

@ -30,12 +30,12 @@ import SwiftUI
// FIXME: #703, providers UI // FIXME: #703, providers UI
extension VPNProviderServerView { extension VPNProviderServerView {
@ViewBuilder
var serversView: some View { var serversView: some View {
sortedServers.nilIfEmpty.map { servers in ForEach(manager.filteredServers) { server in
ForEach(sortedServers) { server in Button("\(server.hostname ?? server.id) \(server.provider.countryCodes)") {
Button("\(server.hostname ?? server.id) \(server.countryCodes)") { selectServer(server)
selectServer(server)
}
} }
} }
} }

View File

@ -30,7 +30,7 @@ import SwiftUI
extension VPNFiltersModifier { extension VPNFiltersModifier {
func contentView(with content: Content) -> some View { func contentView(with content: Content) -> some View {
VStack { VStack {
VPNFiltersView<Configuration>(manager: manager, providerId: providerId, onRefresh: onRefresh) VPNFiltersView<Configuration>(manager: manager)
.padding() .padding()
content content
} }

View File

@ -30,14 +30,18 @@ import SwiftUI
// FIXME: #703, providers UI // FIXME: #703, providers UI
extension VPNProviderServerView { extension VPNProviderServerView {
@ViewBuilder
var serversView: some View { var serversView: some View {
Table(sortedServers, sortOrder: $sortOrder) { Table(manager.filteredServers) {
TableColumn("Region", value: \.sortableRegion) TableColumn("Region") { server in
.width(max: 200.0) Text(server.region)
}
.width(max: 200.0)
TableColumn("Address", value: \.sortableAddresses) TableColumn("Address", value: \.address)
TableColumn("", value: \.serverId) { server in TableColumn("") { server in
Button { Button {
selectServer(server) selectServer(server)
} label: { } label: {

View File

@ -31,10 +31,9 @@ extension View {
public func withEnvironment(from context: AppContext, theme: Theme) -> some View { public func withEnvironment(from context: AppContext, theme: Theme) -> some View {
environmentObject(theme) environmentObject(theme)
.environmentObject(context.iapManager) .environmentObject(context.iapManager)
.environmentObject(context.connectionObserver)
.environmentObject(context.providerFactory.providerManager)
.environmentObject(context.providerFactory.vpnProviderManager)
.environmentObject(context.profileProcessor) .environmentObject(context.profileProcessor)
.environmentObject(context.connectionObserver)
.environmentObject(context.providerManager)
} }
public func withMockEnvironment() -> some View { public func withMockEnvironment() -> some View {

View File

@ -63,7 +63,7 @@ extension API {
#if DEBUG #if DEBUG
[API.bundled] [API.bundled]
#else #else
[API.remoteThenBundled] API.remoteThenBundled
#endif #endif
} }

View File

@ -40,7 +40,7 @@ extension AppContext {
tunnel: .shared, tunnel: .shared,
tunnelEnvironment: .shared, tunnelEnvironment: .shared,
registry: .shared, registry: .shared,
providerFactory: .shared, providerManager: .shared,
constants: .shared constants: .shared
) )
} }
@ -208,10 +208,10 @@ private extension ProfileManager {
// MARK: - // MARK: -
// FIXME: #705, store providers to Core Data // FIXME: #705, store providers to Core Data
extension ProviderFactory { extension ProviderManager {
static let shared = ProviderFactory( static let shared = ProviderManager(
providerManager: ProviderManager(repository: InMemoryProviderRepository()), repository: InMemoryProviderRepository(),
vpnProviderManager: VPNProviderManager(repository: InMemoryVPNProviderRepository()) vpnRepository: InMemoryVPNProviderRepository()
) )
} }