Fix some key things about providers UI (#736)

Fixes #703
This commit is contained in:
Davide 2024-10-16 08:53:16 +02:00 committed by GitHub
parent 9656e5ed29
commit acf066571a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 80 additions and 45 deletions

View File

@ -81,7 +81,7 @@ struct Issue: Identifiable {
.replacingOccurrences(of: "$appLine", with: appLine ?? "unknown") .replacingOccurrences(of: "$appLine", with: appLine ?? "unknown")
.replacingOccurrences(of: "$osLine", with: osLine) .replacingOccurrences(of: "$osLine", with: osLine)
.replacingOccurrences(of: "$deviceLine", with: deviceLine ?? "unknown") .replacingOccurrences(of: "$deviceLine", with: deviceLine ?? "unknown")
// FIXME: #703, report provider in issue // FIXME: #710, report provider in issue
.replacingOccurrences(of: "$providerName", with: "none") .replacingOccurrences(of: "$providerName", with: "none")
.replacingOccurrences(of: "$providerLastUpdate", with: "unknown") .replacingOccurrences(of: "$providerLastUpdate", with: "unknown")
.replacingOccurrences(of: "$purchasedProducts", with: purchasedProducts.map(\.rawValue).description) .replacingOccurrences(of: "$purchasedProducts", with: purchasedProducts.map(\.rawValue).description)

View File

@ -73,6 +73,9 @@ extension PassepartoutError: LocalizedError {
case .parsing: case .parsing:
return reason?.localizedDescription ?? Strings.Errors.App.Passepartout.parsing return reason?.localizedDescription ?? Strings.Errors.App.Passepartout.parsing
case .corruptProviderModule:
return Strings.Errors.App.Passepartout.corruptProviderModule(reason?.localizedDescription ?? "")
case .unhandled: case .unhandled:
return reason?.localizedDescription return reason?.localizedDescription

View File

@ -113,6 +113,10 @@ public enum Strings {
/// Only one connection module can be active at a time. /// Only one connection module can be active at a time.
public static let multipleConnectionModules = Strings.tr("Localizable", "errors.app.multiple_connection_modules", fallback: "Only one connection module can be active at a time.") public static let multipleConnectionModules = Strings.tr("Localizable", "errors.app.multiple_connection_modules", fallback: "Only one connection module can be active at a time.")
public enum Passepartout { public enum Passepartout {
/// Unable to connect to provider server (reason=%@).
public static func corruptProviderModule(_ p1: Any) -> String {
return Strings.tr("Localizable", "errors.app.passepartout.corrupt_provider_module", String(describing: p1), fallback: "Unable to connect to provider server (reason=%@).")
}
/// Unable to complete operation (code=%@). /// Unable to complete operation (code=%@).
public static func `default`(_ p1: Any) -> String { public static func `default`(_ p1: Any) -> String {
return Strings.tr("Localizable", "errors.app.passepartout.default", String(describing: p1), fallback: "Unable to complete operation (code=%@).") return Strings.tr("Localizable", "errors.app.passepartout.default", String(describing: p1), fallback: "Unable to complete operation (code=%@).")
@ -159,6 +163,8 @@ public enum Strings {
public static let any = Strings.tr("Localizable", "global.any", fallback: "Any") public static let any = Strings.tr("Localizable", "global.any", fallback: "Any")
/// Cancel /// Cancel
public static let cancel = Strings.tr("Localizable", "global.cancel", fallback: "Cancel") public static let cancel = Strings.tr("Localizable", "global.cancel", fallback: "Cancel")
/// Category
public static let category = Strings.tr("Localizable", "global.category", fallback: "Category")
/// Certificate /// Certificate
public static let certificate = Strings.tr("Localizable", "global.certificate", fallback: "Certificate") public static let certificate = Strings.tr("Localizable", "global.certificate", fallback: "Certificate")
/// Compression /// Compression
@ -167,6 +173,8 @@ public enum Strings {
public static let connect = Strings.tr("Localizable", "global.connect", fallback: "Connect") public static let connect = Strings.tr("Localizable", "global.connect", fallback: "Connect")
/// Connection /// Connection
public static let connection = Strings.tr("Localizable", "global.connection", fallback: "Connection") public static let connection = Strings.tr("Localizable", "global.connection", fallback: "Connection")
/// Country
public static let country = Strings.tr("Localizable", "global.country", fallback: "Country")
/// Default /// Default
public static let `default` = Strings.tr("Localizable", "global.default", fallback: "Default") public static let `default` = Strings.tr("Localizable", "global.default", fallback: "Default")
/// Destination /// Destination
@ -195,6 +203,8 @@ public enum Strings {
public static let enabled = Strings.tr("Localizable", "global.enabled", fallback: "Enabled") public static let enabled = Strings.tr("Localizable", "global.enabled", fallback: "Enabled")
/// Endpoint /// Endpoint
public static let endpoint = Strings.tr("Localizable", "global.endpoint", fallback: "Endpoint") public static let endpoint = Strings.tr("Localizable", "global.endpoint", fallback: "Endpoint")
/// Filters
public static let filters = Strings.tr("Localizable", "global.filters", fallback: "Filters")
/// Folder /// Folder
public static let folder = Strings.tr("Localizable", "global.folder", fallback: "Folder") public static let folder = Strings.tr("Localizable", "global.folder", fallback: "Folder")
/// Gateway /// Gateway
@ -251,6 +261,8 @@ public enum Strings {
public static let publicKey = Strings.tr("Localizable", "global.public_key", fallback: "Public key") public static let publicKey = Strings.tr("Localizable", "global.public_key", fallback: "Public key")
/// Purchase /// Purchase
public static let purchase = Strings.tr("Localizable", "global.purchase", fallback: "Purchase") public static let purchase = Strings.tr("Localizable", "global.purchase", fallback: "Purchase")
/// Region
public static let region = Strings.tr("Localizable", "global.region", fallback: "Region")
/// Delete /// Delete
public static let remove = Strings.tr("Localizable", "global.remove", fallback: "Delete") public static let remove = Strings.tr("Localizable", "global.remove", fallback: "Delete")
/// Restart /// Restart
@ -590,19 +602,27 @@ public enum Strings {
} }
} }
public enum Provider { public enum Provider {
/// Last updated on %@ /// Clear filters
public static func lastUpdated(_ p1: Any) -> String { public static let clearFilters = Strings.tr("Localizable", "views.provider.clear_filters", fallback: "Clear filters")
return Strings.tr("Localizable", "views.provider.last_updated", String(describing: p1), fallback: "Last updated on %@")
}
/// None /// None
public static let noProvider = Strings.tr("Localizable", "views.provider.no_provider", fallback: "None") public static let noProvider = Strings.tr("Localizable", "views.provider.no_provider", fallback: "None")
/// Refresh infrastructure
public static let refreshInfrastructure = Strings.tr("Localizable", "views.provider.refresh_infrastructure", fallback: "Refresh infrastructure")
/// Select a provider /// Select a provider
public static let selectProvider = Strings.tr("Localizable", "views.provider.select_provider", fallback: "Select a provider") public static let selectProvider = Strings.tr("Localizable", "views.provider.select_provider", fallback: "Select a provider")
public enum LastUpdated { /// Select
/// Loading... public static let selectServer = Strings.tr("Localizable", "views.provider.select_server", fallback: "Select")
public static let loading = Strings.tr("Localizable", "views.provider.last_updated.loading", fallback: "Loading...") public enum Vpn {
/// Last updated on %@
public static func lastUpdated(_ p1: Any) -> String {
return Strings.tr("Localizable", "views.provider.vpn.last_updated", String(describing: p1), fallback: "Last updated on %@")
}
/// Preset
public static let preset = Strings.tr("Localizable", "views.provider.vpn.preset", fallback: "Preset")
/// Refresh infrastructure
public static let refreshInfrastructure = Strings.tr("Localizable", "views.provider.vpn.refresh_infrastructure", fallback: "Refresh infrastructure")
public enum LastUpdated {
/// Loading...
public static let loading = Strings.tr("Localizable", "views.provider.vpn.last_updated.loading", fallback: "Loading...")
}
} }
} }
public enum Settings { public enum Settings {

View File

@ -6,10 +6,12 @@
"global.addresses" = "Addresses"; "global.addresses" = "Addresses";
"global.any" = "Any"; "global.any" = "Any";
"global.cancel" = "Cancel"; "global.cancel" = "Cancel";
"global.category" = "Category";
"global.certificate" = "Certificate"; "global.certificate" = "Certificate";
"global.compression" = "Compression"; "global.compression" = "Compression";
"global.connect" = "Connect"; "global.connect" = "Connect";
"global.connection" = "Connection"; "global.connection" = "Connection";
"global.country" = "Country";
"global.default" = "Default"; "global.default" = "Default";
"global.destination" = "Destination"; "global.destination" = "Destination";
"global.disable" = "Disable"; "global.disable" = "Disable";
@ -24,6 +26,7 @@
"global.enable" = "Enable"; "global.enable" = "Enable";
"global.enabled" = "Enabled"; "global.enabled" = "Enabled";
"global.endpoint" = "Endpoint"; "global.endpoint" = "Endpoint";
"global.filters" = "Filters";
"global.folder" = "Folder"; "global.folder" = "Folder";
"global.gateway" = "Gateway"; "global.gateway" = "Gateway";
"global.general" = "General"; "global.general" = "General";
@ -51,6 +54,7 @@
"global.provider" = "Provider"; "global.provider" = "Provider";
"global.public_key" = "Public key"; "global.public_key" = "Public key";
"global.purchase" = "Purchase"; "global.purchase" = "Purchase";
"global.region" = "Region";
"global.remove" = "Delete"; "global.remove" = "Delete";
"global.restart" = "Restart"; "global.restart" = "Restart";
"global.route" = "Route"; "global.route" = "Route";
@ -131,9 +135,12 @@
"views.provider.no_provider" = "None"; "views.provider.no_provider" = "None";
"views.provider.select_provider" = "Select a provider"; "views.provider.select_provider" = "Select a provider";
"views.provider.refresh_infrastructure" = "Refresh infrastructure"; "views.provider.clear_filters" = "Clear filters";
"views.provider.last_updated" = "Last updated on %@"; "views.provider.select_server" = "Select";
"views.provider.last_updated.loading" = "Loading..."; "views.provider.vpn.refresh_infrastructure" = "Refresh infrastructure";
"views.provider.vpn.last_updated" = "Last updated on %@";
"views.provider.vpn.last_updated.loading" = "Loading...";
"views.provider.vpn.preset" = "Preset";
"views.settings.sections.icloud.footer" = "To erase the iCloud store securely, do so on all your synced devices. This will not affect local profiles."; "views.settings.sections.icloud.footer" = "To erase the iCloud store securely, do so on all your synced devices. This will not affect local profiles.";
"views.settings.rows.confirm_quit" = "Ask before quit"; "views.settings.rows.confirm_quit" = "Ask before quit";
@ -246,6 +253,7 @@
"errors.app.default" = "Unable to complete operation."; "errors.app.default" = "Unable to complete operation.";
"errors.app.passepartout.invalid_fields" = "Invalid fields (%@)."; "errors.app.passepartout.invalid_fields" = "Invalid fields (%@).";
"errors.app.passepartout.parsing" = "Unable to parse file."; "errors.app.passepartout.parsing" = "Unable to parse file.";
"errors.app.passepartout.corrupt_provider_module" = "Unable to connect to provider server (reason=%@).";
"errors.app.passepartout.default" = "Unable to complete operation (code=%@)."; "errors.app.passepartout.default" = "Unable to complete operation (code=%@).";
"errors.tunnel.auth" = "Auth failed"; "errors.tunnel.auth" = "Auth failed";

View File

@ -78,7 +78,7 @@ private extension ProviderContentModifier {
providerRows providerRows
refreshButton { refreshButton {
HStack { HStack {
Text(Strings.Views.Provider.refreshInfrastructure) Text(Strings.Views.Provider.Vpn.refreshInfrastructure)
if providerManager.isLoading { if providerManager.isLoading {
Spacer() Spacer()
ProgressView() ProgressView()
@ -102,7 +102,7 @@ private extension ProviderContentModifier {
} }
Spacer() Spacer()
refreshButton { refreshButton {
Text(Strings.Views.Provider.refreshInfrastructure) Text(Strings.Views.Provider.Vpn.refreshInfrastructure)
} }
} }
} }
@ -141,9 +141,9 @@ private extension ProviderContentModifier {
var lastUpdatedString: String? { var lastUpdatedString: String? {
guard let lastUpdated else { guard let lastUpdated else {
return providerManager.isLoading ? Strings.Views.Provider.LastUpdated.loading : nil return providerManager.isLoading ? Strings.Views.Provider.Vpn.LastUpdated.loading : nil
} }
return Strings.Views.Provider.lastUpdated(lastUpdated.timestamp) return Strings.Views.Provider.Vpn.lastUpdated(lastUpdated.timestamp)
} }
} }

View File

@ -26,6 +26,8 @@
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
// FIXME: ###, providers UI, iPadOS (Simulator?) picker .navigationLink selection is blue (vs gray) and disclosed options are white
struct ProviderPicker: View { struct ProviderPicker: View {
let providers: [ProviderMetadata] let providers: [ProviderMetadata]

View File

@ -27,8 +27,6 @@ import AppLibrary
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
// FIXME: #703, providers UI
struct VPNFiltersView<Configuration>: View where Configuration: Decodable { struct VPNFiltersView<Configuration>: View where Configuration: Decodable {
@ObservedObject @ObservedObject
@ -56,8 +54,8 @@ struct VPNFiltersView<Configuration>: View where Configuration: Decodable {
private extension VPNFiltersView { private extension VPNFiltersView {
var categoryPicker: some View { var categoryPicker: some View {
Picker("Category", selection: $manager.parameters.filters.categoryName) { Picker(Strings.Global.category, selection: $manager.parameters.filters.categoryName) {
Text("Any") Text(Strings.Global.any)
.tag(nil as String?) .tag(nil as String?)
ForEach(categories, id: \.self) { ForEach(categories, id: \.self) {
Text($0.capitalized) Text($0.capitalized)
@ -67,8 +65,8 @@ private extension VPNFiltersView {
} }
var countryPicker: some View { var countryPicker: some View {
Picker("Country", selection: $manager.parameters.filters.countryCode) { Picker(Strings.Global.country, selection: $manager.parameters.filters.countryCode) {
Text("Any") Text(Strings.Global.any)
.tag(nil as String?) .tag(nil as String?)
ForEach(countries, id: \.code) { ForEach(countries, id: \.code) {
Text($0.description) Text($0.description)
@ -80,8 +78,8 @@ private extension VPNFiltersView {
@ViewBuilder @ViewBuilder
var presetPicker: some View { var presetPicker: some View {
if manager.allPresets.count > 1 { if manager.allPresets.count > 1 {
Picker("Preset", selection: $manager.parameters.filters.presetId) { Picker(Strings.Views.Provider.Vpn.preset, selection: $manager.parameters.filters.presetId) {
Text("Any") Text(Strings.Global.any)
.tag(nil as String?) .tag(nil as String?)
ForEach(presets, id: \.presetId) { ForEach(presets, id: \.presetId) {
Text($0.description) Text($0.description)
@ -92,7 +90,7 @@ private extension VPNFiltersView {
} }
var clearFiltersButton: some View { var clearFiltersButton: some View {
Button("Clear filters", role: .destructive) { Button(Strings.Views.Provider.clearFilters, role: .destructive) {
Task { Task {
manager.resetFilters() manager.resetFilters()
} }

View File

@ -49,7 +49,8 @@ struct VPNProviderServerView<Configuration>: View where Configuration: ProviderC
extension VPNProviderServerView { extension VPNProviderServerView {
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 pp_log(.app, .error, "Unable to find a compatible preset. Supported IDs: \(server.provider.supportedPresetIds ?? [])")
assertionFailure("No compatible presets for server \(server.serverId) (manager=\(manager.providerId), configuration=\(Configuration.providerConfigurationIdentifier), supported=\(server.provider.supportedPresetIds ?? []))")
return return
} }
onSelect(server, preset) onSelect(server, preset)

View File

@ -27,24 +27,26 @@
import SwiftUI import SwiftUI
// FIXME: #703, providers UI // FIXME: ###, providers UI, iPadOS show filters in popover
extension VPNFiltersModifier { extension VPNFiltersModifier {
func contentView(with content: Content) -> some View { func contentView(with content: Content) -> some View {
content content
.toolbar { .toolbar {
Button { ToolbarItem(placement: .bottomBar) {
isFiltersPresented = true Button {
} label: { isFiltersPresented = true
Image(systemName: "line.3.horizontal.decrease") } label: {
} ThemeImage(.filters)
.themeModal(isPresented: $isFiltersPresented) { }
NavigationStack { .themeModal(isPresented: $isFiltersPresented) {
VPNFiltersView<Configuration>(manager: manager) NavigationStack {
.navigationTitle("Filters") VPNFiltersView<Configuration>(manager: manager)
.navigationBarTitleDisplayMode(.inline) .navigationTitle(Strings.Global.filters)
.navigationBarTitleDisplayMode(.inline)
}
.presentationDetents([.medium])
} }
.presentationDetents([.medium])
} }
} }
} }

View File

@ -27,7 +27,7 @@
import SwiftUI import SwiftUI
// FIXME: #703, providers UI // FIXME: ###, providers UI, iOS server rows + country flags
extension VPNProviderServerView { extension VPNProviderServerView {

View File

@ -27,25 +27,25 @@
import SwiftUI import SwiftUI
// FIXME: #703, providers UI // FIXME: ###, providers UI, macOS country flags
extension VPNProviderServerView { extension VPNProviderServerView {
@ViewBuilder @ViewBuilder
var serversView: some View { var serversView: some View {
Table(manager.filteredServers) { Table(manager.filteredServers) {
TableColumn("Region") { server in TableColumn(Strings.Global.region) { server in
Text(server.region) Text(server.region)
} }
.width(max: 200.0) .width(max: 200.0)
TableColumn("Address", value: \.address) TableColumn(Strings.Global.address, value: \.address)
TableColumn("") { server in TableColumn("") { server in
Button { Button {
selectServer(server) selectServer(server)
} label: { } label: {
Text("Select") Text(Strings.Views.Provider.selectServer)
} }
} }
.width(min: 100.0, max: 100.0) .width(min: 100.0, max: 100.0)

View File

@ -36,6 +36,7 @@ extension Theme {
case disclose case disclose
case editableSectionEdit case editableSectionEdit
case editableSectionRemove case editableSectionRemove
case filters
case footerAdd case footerAdd
case hide case hide
case info case info

View File

@ -87,6 +87,7 @@ public final class Theme: ObservableObject {
case .disclose: return "chevron.down" case .disclose: return "chevron.down"
case .editableSectionEdit: return "arrow.up.arrow.down" case .editableSectionEdit: return "arrow.up.arrow.down"
case .editableSectionRemove: return "trash" case .editableSectionRemove: return "trash"
case .filters: return "line.3.horizontal.decrease"
case .footerAdd: return "plus.circle" case .footerAdd: return "plus.circle"
case .hide: return "eye.slash" case .hide: return "eye.slash"
case .info: return "info.circle" case .info: return "info.circle"

View File

@ -141,7 +141,6 @@ extension ProfileProcessor {
_ = try profile.withProviderModules() _ = try profile.withProviderModules()
return profile return profile
} catch { } catch {
// FIXME: #703, alert unable to build provider server
pp_log(.app, .error, "Unable to inject provider modules: \(error)") pp_log(.app, .error, "Unable to inject provider modules: \(error)")
throw error throw error
} }