diff --git a/Passepartout/Library/Sources/AppUI/Domain/Issue.swift b/Passepartout/Library/Sources/AppUI/Domain/Issue.swift index 48d32dac..f915791e 100644 --- a/Passepartout/Library/Sources/AppUI/Domain/Issue.swift +++ b/Passepartout/Library/Sources/AppUI/Domain/Issue.swift @@ -81,7 +81,7 @@ struct Issue: Identifiable { .replacingOccurrences(of: "$appLine", with: appLine ?? "unknown") .replacingOccurrences(of: "$osLine", with: osLine) .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: "$providerLastUpdate", with: "unknown") .replacingOccurrences(of: "$purchasedProducts", with: purchasedProducts.map(\.rawValue).description) diff --git a/Passepartout/Library/Sources/AppUI/L10n/AppError+L10n.swift b/Passepartout/Library/Sources/AppUI/L10n/AppError+L10n.swift index 532ca118..57f6f7fb 100644 --- a/Passepartout/Library/Sources/AppUI/L10n/AppError+L10n.swift +++ b/Passepartout/Library/Sources/AppUI/L10n/AppError+L10n.swift @@ -73,6 +73,9 @@ extension PassepartoutError: LocalizedError { case .parsing: return reason?.localizedDescription ?? Strings.Errors.App.Passepartout.parsing + case .corruptProviderModule: + return Strings.Errors.App.Passepartout.corruptProviderModule(reason?.localizedDescription ?? "") + case .unhandled: return reason?.localizedDescription diff --git a/Passepartout/Library/Sources/AppUI/L10n/SwiftGen+Strings.swift b/Passepartout/Library/Sources/AppUI/L10n/SwiftGen+Strings.swift index d5872dee..2cb132e2 100644 --- a/Passepartout/Library/Sources/AppUI/L10n/SwiftGen+Strings.swift +++ b/Passepartout/Library/Sources/AppUI/L10n/SwiftGen+Strings.swift @@ -113,6 +113,10 @@ public enum Strings { /// 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 { + /// 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=%@). public static func `default`(_ p1: Any) -> String { 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") /// 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 public static let certificate = Strings.tr("Localizable", "global.certificate", fallback: "Certificate") /// Compression @@ -167,6 +173,8 @@ public enum Strings { public static let connect = Strings.tr("Localizable", "global.connect", fallback: "Connect") /// 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 public static let `default` = Strings.tr("Localizable", "global.default", fallback: "Default") /// Destination @@ -195,6 +203,8 @@ public enum Strings { public static let enabled = Strings.tr("Localizable", "global.enabled", fallback: "Enabled") /// 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 public static let folder = Strings.tr("Localizable", "global.folder", fallback: "Folder") /// Gateway @@ -251,6 +261,8 @@ public enum Strings { public static let publicKey = Strings.tr("Localizable", "global.public_key", fallback: "Public key") /// 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 public static let remove = Strings.tr("Localizable", "global.remove", fallback: "Delete") /// Restart @@ -590,19 +602,27 @@ public enum Strings { } } public enum Provider { - /// Last updated on %@ - public static func lastUpdated(_ p1: Any) -> String { - return Strings.tr("Localizable", "views.provider.last_updated", String(describing: p1), fallback: "Last updated on %@") - } + /// Clear filters + public static let clearFilters = Strings.tr("Localizable", "views.provider.clear_filters", fallback: "Clear filters") /// 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 public static let selectProvider = Strings.tr("Localizable", "views.provider.select_provider", fallback: "Select a provider") - public enum LastUpdated { - /// Loading... - public static let loading = Strings.tr("Localizable", "views.provider.last_updated.loading", fallback: "Loading...") + /// Select + public static let selectServer = Strings.tr("Localizable", "views.provider.select_server", fallback: "Select") + 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 { diff --git a/Passepartout/Library/Sources/AppUI/Resources/en.lproj/Localizable.strings b/Passepartout/Library/Sources/AppUI/Resources/en.lproj/Localizable.strings index 2c017c99..93b78736 100644 --- a/Passepartout/Library/Sources/AppUI/Resources/en.lproj/Localizable.strings +++ b/Passepartout/Library/Sources/AppUI/Resources/en.lproj/Localizable.strings @@ -6,10 +6,12 @@ "global.addresses" = "Addresses"; "global.any" = "Any"; "global.cancel" = "Cancel"; +"global.category" = "Category"; "global.certificate" = "Certificate"; "global.compression" = "Compression"; "global.connect" = "Connect"; "global.connection" = "Connection"; +"global.country" = "Country"; "global.default" = "Default"; "global.destination" = "Destination"; "global.disable" = "Disable"; @@ -24,6 +26,7 @@ "global.enable" = "Enable"; "global.enabled" = "Enabled"; "global.endpoint" = "Endpoint"; +"global.filters" = "Filters"; "global.folder" = "Folder"; "global.gateway" = "Gateway"; "global.general" = "General"; @@ -51,6 +54,7 @@ "global.provider" = "Provider"; "global.public_key" = "Public key"; "global.purchase" = "Purchase"; +"global.region" = "Region"; "global.remove" = "Delete"; "global.restart" = "Restart"; "global.route" = "Route"; @@ -131,9 +135,12 @@ "views.provider.no_provider" = "None"; "views.provider.select_provider" = "Select a provider"; -"views.provider.refresh_infrastructure" = "Refresh infrastructure"; -"views.provider.last_updated" = "Last updated on %@"; -"views.provider.last_updated.loading" = "Loading..."; +"views.provider.clear_filters" = "Clear filters"; +"views.provider.select_server" = "Select"; +"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.rows.confirm_quit" = "Ask before quit"; @@ -246,6 +253,7 @@ "errors.app.default" = "Unable to complete operation."; "errors.app.passepartout.invalid_fields" = "Invalid fields (%@)."; "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.tunnel.auth" = "Auth failed"; diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/ProviderContentModifier.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/ProviderContentModifier.swift index 971cad50..341b327a 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/ProviderContentModifier.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Provider/ProviderContentModifier.swift @@ -78,7 +78,7 @@ private extension ProviderContentModifier { providerRows refreshButton { HStack { - Text(Strings.Views.Provider.refreshInfrastructure) + Text(Strings.Views.Provider.Vpn.refreshInfrastructure) if providerManager.isLoading { Spacer() ProgressView() @@ -102,7 +102,7 @@ private extension ProviderContentModifier { } Spacer() refreshButton { - Text(Strings.Views.Provider.refreshInfrastructure) + Text(Strings.Views.Provider.Vpn.refreshInfrastructure) } } } @@ -141,9 +141,9 @@ private extension ProviderContentModifier { var lastUpdatedString: String? { 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) } } diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/ProviderPicker.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/ProviderPicker.swift index 96e90b21..6c020079 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/ProviderPicker.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Provider/ProviderPicker.swift @@ -26,6 +26,8 @@ import PassepartoutKit import SwiftUI +// FIXME: ###, providers UI, iPadOS (Simulator?) picker .navigationLink selection is blue (vs gray) and disclosed options are white + struct ProviderPicker: View { let providers: [ProviderMetadata] diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNFiltersView.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNFiltersView.swift index faaede66..bd73e9ac 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNFiltersView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNFiltersView.swift @@ -27,8 +27,6 @@ import AppLibrary import PassepartoutKit import SwiftUI -// FIXME: #703, providers UI - struct VPNFiltersView: View where Configuration: Decodable { @ObservedObject @@ -56,8 +54,8 @@ struct VPNFiltersView: View where Configuration: Decodable { private extension VPNFiltersView { var categoryPicker: some View { - Picker("Category", selection: $manager.parameters.filters.categoryName) { - Text("Any") + Picker(Strings.Global.category, selection: $manager.parameters.filters.categoryName) { + Text(Strings.Global.any) .tag(nil as String?) ForEach(categories, id: \.self) { Text($0.capitalized) @@ -67,8 +65,8 @@ private extension VPNFiltersView { } var countryPicker: some View { - Picker("Country", selection: $manager.parameters.filters.countryCode) { - Text("Any") + Picker(Strings.Global.country, selection: $manager.parameters.filters.countryCode) { + Text(Strings.Global.any) .tag(nil as String?) ForEach(countries, id: \.code) { Text($0.description) @@ -80,8 +78,8 @@ private extension VPNFiltersView { @ViewBuilder var presetPicker: some View { if manager.allPresets.count > 1 { - Picker("Preset", selection: $manager.parameters.filters.presetId) { - Text("Any") + Picker(Strings.Views.Provider.Vpn.preset, selection: $manager.parameters.filters.presetId) { + Text(Strings.Global.any) .tag(nil as String?) ForEach(presets, id: \.presetId) { Text($0.description) @@ -92,7 +90,7 @@ private extension VPNFiltersView { } var clearFiltersButton: some View { - Button("Clear filters", role: .destructive) { + Button(Strings.Views.Provider.clearFilters, role: .destructive) { Task { manager.resetFilters() } diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderServerView.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderServerView.swift index 87c40bdf..573d0b08 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderServerView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderServerView.swift @@ -49,7 +49,8 @@ struct VPNProviderServerView: View where Configuration: ProviderC extension VPNProviderServerView { func selectServer(_ server: VPNServer) { 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 } onSelect(server, preset) diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/iOS/VPNFiltersModifier+iOS.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/iOS/VPNFiltersModifier+iOS.swift index 1b3a982a..6165fb50 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/iOS/VPNFiltersModifier+iOS.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Provider/iOS/VPNFiltersModifier+iOS.swift @@ -27,24 +27,26 @@ import SwiftUI -// FIXME: #703, providers UI +// FIXME: ###, providers UI, iPadOS show filters in popover extension VPNFiltersModifier { func contentView(with content: Content) -> some View { content .toolbar { - Button { - isFiltersPresented = true - } label: { - Image(systemName: "line.3.horizontal.decrease") - } - .themeModal(isPresented: $isFiltersPresented) { - NavigationStack { - VPNFiltersView(manager: manager) - .navigationTitle("Filters") - .navigationBarTitleDisplayMode(.inline) + ToolbarItem(placement: .bottomBar) { + Button { + isFiltersPresented = true + } label: { + ThemeImage(.filters) + } + .themeModal(isPresented: $isFiltersPresented) { + NavigationStack { + VPNFiltersView(manager: manager) + .navigationTitle(Strings.Global.filters) + .navigationBarTitleDisplayMode(.inline) + } + .presentationDetents([.medium]) } - .presentationDetents([.medium]) } } } diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/iOS/VPNProviderServerView+iOS.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/iOS/VPNProviderServerView+iOS.swift index c27e94c9..3b1aeac0 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/iOS/VPNProviderServerView+iOS.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Provider/iOS/VPNProviderServerView+iOS.swift @@ -27,7 +27,7 @@ import SwiftUI -// FIXME: #703, providers UI +// FIXME: ###, providers UI, iOS server rows + country flags extension VPNProviderServerView { diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/macOS/VPNProviderServerView+macOS.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/macOS/VPNProviderServerView+macOS.swift index 2eafd2e4..8352dfd5 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/macOS/VPNProviderServerView+macOS.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Provider/macOS/VPNProviderServerView+macOS.swift @@ -27,25 +27,25 @@ import SwiftUI -// FIXME: #703, providers UI +// FIXME: ###, providers UI, macOS country flags extension VPNProviderServerView { @ViewBuilder var serversView: some View { Table(manager.filteredServers) { - TableColumn("Region") { server in + TableColumn(Strings.Global.region) { server in Text(server.region) } .width(max: 200.0) - TableColumn("Address", value: \.address) + TableColumn(Strings.Global.address, value: \.address) TableColumn("") { server in Button { selectServer(server) } label: { - Text("Select") + Text(Strings.Views.Provider.selectServer) } } .width(min: 100.0, max: 100.0) diff --git a/Passepartout/Library/Sources/AppUI/Views/Theme/Theme+ImageName.swift b/Passepartout/Library/Sources/AppUI/Views/Theme/Theme+ImageName.swift index 9aba8129..49ffacf7 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Theme/Theme+ImageName.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Theme/Theme+ImageName.swift @@ -36,6 +36,7 @@ extension Theme { case disclose case editableSectionEdit case editableSectionRemove + case filters case footerAdd case hide case info diff --git a/Passepartout/Library/Sources/AppUI/Views/Theme/Theme.swift b/Passepartout/Library/Sources/AppUI/Views/Theme/Theme.swift index f8c8d2ce..8a57100b 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Theme/Theme.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Theme/Theme.swift @@ -87,6 +87,7 @@ public final class Theme: ObservableObject { case .disclose: return "chevron.down" case .editableSectionEdit: return "arrow.up.arrow.down" case .editableSectionRemove: return "trash" + case .filters: return "line.3.horizontal.decrease" case .footerAdd: return "plus.circle" case .hide: return "eye.slash" case .info: return "info.circle" diff --git a/Passepartout/Shared/Shared+App.swift b/Passepartout/Shared/Shared+App.swift index 9d6474fb..2c91461a 100644 --- a/Passepartout/Shared/Shared+App.swift +++ b/Passepartout/Shared/Shared+App.swift @@ -141,7 +141,6 @@ extension ProfileProcessor { _ = try profile.withProviderModules() return profile } catch { - // FIXME: #703, alert unable to build provider server pp_log(.app, .error, "Unable to inject provider modules: \(error)") throw error }