Add "Refresh infrastructure" in server lists (#938)

Refactoring:

- Split Providers and VPN views
- Rename VPNProviderServerView subviews
- Reuse RefreshInfrastructureButton

Closes #929
This commit is contained in:
Davide 2024-11-26 01:04:58 +01:00 committed by GitHub
parent 8b043d8a4f
commit b357d985ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 136 additions and 47 deletions

View File

@ -27,6 +27,7 @@ import CommonAPI
import CommonLibrary
import PassepartoutKit
import SwiftUI
import UILibrary
struct ProviderContentModifier<Entity, ProviderRows>: ViewModifier where Entity: ProviderEntity, Entity.Configuration: ProviderConfigurationIdentifiable & Codable, ProviderRows: View {
@ -77,18 +78,10 @@ private extension ProviderContentModifier {
providerPicker
.themeSection()
if providerId != nil {
if let providerId {
Group {
providerRows
refreshButton {
HStack {
Text(Strings.Views.Providers.refreshInfrastructure)
if providerManager.isLoading {
Spacer()
ProgressView()
}
}
}
RefreshInfrastructureButton(apis: apis, providerId: providerId)
}
.themeSection(footer: lastUpdatedString)
}
@ -99,7 +92,7 @@ private extension ProviderContentModifier {
Section {
providerPicker
}
if providerId != nil {
if let providerId {
Section {
providerRows
HStack {
@ -108,9 +101,7 @@ private extension ProviderContentModifier {
.foregroundStyle(.secondary)
}
Spacer()
refreshButton {
Text(Strings.Views.Providers.refreshInfrastructure)
}
RefreshInfrastructureButton(apis: apis, providerId: providerId)
}
}
}
@ -127,10 +118,6 @@ private extension ProviderContentModifier {
)
}
func refreshButton<Label>(label: () -> Label) -> some View where Label: View {
Button(action: onRefreshInfrastructure, label: label)
}
var supportedProviders: [ProviderMetadata] {
providerManager
.providers
@ -185,15 +172,6 @@ private extension ProviderContentModifier {
return false
}
}
func onRefreshInfrastructure() {
guard let providerId else {
return
}
Task {
await refreshInfrastructure(for: providerId)
}
}
}
// MARK: - Preview

View File

@ -29,6 +29,9 @@ import PassepartoutKit
import SwiftUI
struct VPNFiltersView: View {
let apis: [APIMapper]
let providerId: ProviderID
@ObservedObject
var model: Model
@ -47,6 +50,7 @@ struct VPNFiltersView: View {
HStack {
favoritesToggle
Spacer()
RefreshInfrastructureButton(apis: apis, providerId: providerId)
clearFiltersButton
}
#endif
@ -88,7 +92,7 @@ private extension VPNFiltersView {
}
var presetPicker: some View {
Picker(Strings.Views.Providers.Vpn.preset, selection: $model.filters.presetId) {
Picker(Strings.Views.Vpn.preset, selection: $model.filters.presetId) {
Text(Strings.Global.Nouns.any)
.tag(nil as String?)
ForEach(model.presets, id: \.presetId) {
@ -111,6 +115,10 @@ private extension VPNFiltersView {
#Preview {
NavigationStack {
VPNFiltersView(model: .init())
VPNFiltersView(
apis: [API.bundled],
providerId: .mullvad,
model: .init()
)
}
}

View File

@ -71,6 +71,8 @@ extension VPNProviderServerView {
var body: some View {
debugChanges()
return ContentView(
apis: apis,
providerId: providerId,
servers: filteredServers,
selectedServer: selectedServer,
isFiltering: isFiltering,

View File

@ -84,7 +84,11 @@ extension VPNProviderServerView {
}
var filtersView: some View {
VPNFiltersView(model: filtersViewModel)
VPNFiltersView(
apis: apis,
providerId: providerId,
model: filtersViewModel
)
}
var initialFilters: VPNFilters? {

View File

@ -32,6 +32,10 @@ import SwiftUI
extension VPNProviderServerView {
struct ContentView: View {
let apis: [APIMapper]
let providerId: ProviderID
let servers: [VPNServer]
let selectedServer: VPNServer?
@ -68,6 +72,7 @@ private extension VPNProviderServerView.ContentView {
List {
Section {
Toggle(Strings.Views.Providers.onlyFavorites, isOn: $filtersViewModel.onlyShowsFavorites)
RefreshInfrastructureButton(apis: apis, providerId: providerId)
}
Group {
if isFiltering || !servers.isEmpty {
@ -82,7 +87,7 @@ private extension VPNProviderServerView.ContentView {
}
}
.themeSection(
header: filtersViewModel.filters.categoryName ?? Strings.Views.Providers.Vpn.Category.any
header: filtersViewModel.filters.categoryName ?? Strings.Views.Vpn.Category.any
)
.onLoad {
if let selectedServer = selectedServer {
@ -93,7 +98,7 @@ private extension VPNProviderServerView.ContentView {
}
var emptyView: some View {
Text(Strings.Views.Providers.Vpn.noServers)
Text(Strings.Views.Vpn.noServers)
}
}
@ -160,7 +165,9 @@ private extension VPNProviderServerView.ContentView {
list.append($0)
map[code] = list
}
serversByCountryCode = map
withAnimation {
serversByCountryCode = map
}
}
}

View File

@ -36,6 +36,10 @@ extension VPNProviderServerView {
@EnvironmentObject
private var theme: Theme
let apis: [APIMapper]
let providerId: ProviderID
let servers: [VPNServer]
let selectedServer: VPNServer?

View File

@ -26,7 +26,7 @@
import SwiftUI
extension View {
public func debugChanges(condition: Bool = true) {
public func debugChanges(condition: Bool = false) {
if condition {
Self._printChanges()
}

View File

@ -830,16 +830,6 @@ public enum Strings {
/// Loading...
public static let loading = Strings.tr("Localizable", "views.providers.last_updated.loading", fallback: "Loading...")
}
public enum Vpn {
/// No servers
public static let noServers = Strings.tr("Localizable", "views.providers.vpn.no_servers", fallback: "No servers")
/// Preset
public static let preset = Strings.tr("Localizable", "views.providers.vpn.preset", fallback: "Preset")
public enum Category {
/// All categories
public static let any = Strings.tr("Localizable", "views.providers.vpn.category.any", fallback: "All categories")
}
}
}
public enum Purchased {
/// No purchases
@ -883,6 +873,16 @@ public enum Strings {
}
}
}
public enum Vpn {
/// No servers
public static let noServers = Strings.tr("Localizable", "views.vpn.no_servers", fallback: "No servers")
/// Preset
public static let preset = Strings.tr("Localizable", "views.vpn.preset", fallback: "Preset")
public enum Category {
/// All categories
public static let any = Strings.tr("Localizable", "views.vpn.category.any", fallback: "All categories")
}
}
}
}
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length

View File

@ -115,9 +115,6 @@
"views.providers.refresh_infrastructure" = "Refresh infrastructure";
"views.providers.last_updated" = "Last updated on %@";
"views.providers.last_updated.loading" = "Loading...";
"views.providers.vpn.category.any" = "All categories";
"views.providers.vpn.preset" = "Preset";
"views.providers.vpn.no_servers" = "No servers";
"views.purchased.title" = "Purchased";
"views.purchased.sections.download.header" = "First download";
@ -131,6 +128,10 @@
"views.ui.purchase_required.purchase.help" = "Purchase required";
"views.ui.purchase_required.restricted.help" = "Feature is restricted";
"views.vpn.category.any" = "All categories";
"views.vpn.preset" = "Preset";
"views.vpn.no_servers" = "No servers";
// MARK: Views (Modules)
"modules.general.sections.storage.header" = "%@";

View File

@ -0,0 +1,85 @@
//
// RefreshInfrastructureButton.swift
// Passepartout
//
// Created by Davide De Rosa on 11/25/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 PassepartoutKit
import SwiftUI
public struct RefreshInfrastructureButton<Label>: View where Label: View {
@EnvironmentObject
private var providerManager: ProviderManager
private let apis: [APIMapper]
private let providerId: ProviderID
private let label: () -> Label
public init(apis: [APIMapper], providerId: ProviderID, label: @escaping () -> Label) {
self.apis = apis
self.providerId = providerId
self.label = label
}
public var body: some View {
Button {
Task {
try await providerManager.fetchVPNInfrastructure(from: apis, for: providerId)
}
} label: {
label()
}
}
}
extension RefreshInfrastructureButton where Label == RefreshInfrastructureButtonProgressView {
public init(apis: [APIMapper], providerId: ProviderID) {
self.apis = apis
self.providerId = providerId
label = {
RefreshInfrastructureButtonProgressView()
}
}
}
public struct RefreshInfrastructureButtonProgressView: View {
@EnvironmentObject
private var providerManager: ProviderManager
public var body: some View {
#if os(iOS)
HStack {
Text(Strings.Views.Providers.refreshInfrastructure)
if providerManager.isLoading {
Spacer()
ProgressView()
}
}
#else
Text(Strings.Views.Providers.refreshInfrastructure)
#endif
}
}