From 5d2e24792ca0222bebfa77f1c074a85e1b78c3dc Mon Sep 17 00:00:00 2001 From: Davide Date: Fri, 18 Oct 2024 18:12:28 +0200 Subject: [PATCH] Rewrite provider views (#740) Resolve some flickering and state inconsistency due to overextended observation of VPNProviderManager. Narrow down its scope to VPNProviderServerView. The downside of that, for now, is that servers are loaded "lazily late", but this flow will make region selection from home easier. Finally, show filters in popover on iPad. --- .../xcshareddata/swiftpm/Package.resolved | 2 +- Passepartout/Library/Package.swift | 2 +- .../AppUI/L10n/PassepartoutKit+L10n.swift | 2 +- .../Library/Sources/AppUI/Mock/Mock.swift | 3 +- .../Provider/ProviderContentModifier.swift | 2 +- .../Views/Provider/VPNFiltersModifier.swift | 45 ----- .../AppUI/Views/Provider/VPNFiltersView.swift | 181 ++++++++++-------- .../Provider/VPNProviderContentModifier.swift | 41 ++-- .../Provider/VPNProviderServerView.swift | 88 +++++++-- .../Provider/iOS/VPNFiltersModifier+iOS.swift | 55 ------ .../iOS/VPNProviderServerView+iOS.swift | 68 ++++++- .../macOS/VPNFiltersModifier+macOS.swift | 40 ---- .../macOS/VPNProviderServerView+macOS.swift | 34 +++- .../Sources/AppUI/Views/Theme/Theme+UI.swift | 21 ++ .../Sources/AppUI/Views/Theme/Theme+iOS.swift | 1 + .../Sources/AppUI/Views/Theme/Theme.swift | 12 ++ Passepartout/Shared/Shared+App.swift | 3 +- 17 files changed, 317 insertions(+), 283 deletions(-) delete mode 100644 Passepartout/Library/Sources/AppUI/Views/Provider/VPNFiltersModifier.swift delete mode 100644 Passepartout/Library/Sources/AppUI/Views/Provider/iOS/VPNFiltersModifier+iOS.swift delete mode 100644 Passepartout/Library/Sources/AppUI/Views/Provider/macOS/VPNFiltersModifier+macOS.swift diff --git a/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a8f4e559..74ac9a6c 100644 --- a/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,7 +41,7 @@ "kind" : "remoteSourceControl", "location" : "git@github.com:passepartoutvpn/passepartoutkit-source", "state" : { - "revision" : "046a7594fa9823407dd405ba700b1b81506ce9af" + "revision" : "9a43e23e9134c3e93926271b2d630be607433fa0" } }, { diff --git a/Passepartout/Library/Package.swift b/Passepartout/Library/Package.swift index 94e873a0..f8cbc816 100644 --- a/Passepartout/Library/Package.swift +++ b/Passepartout/Library/Package.swift @@ -28,7 +28,7 @@ let package = Package( ], dependencies: [ // .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.9.0"), - .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "046a7594fa9823407dd405ba700b1b81506ce9af"), + .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "9a43e23e9134c3e93926271b2d630be607433fa0"), // .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"), diff --git a/Passepartout/Library/Sources/AppUI/L10n/PassepartoutKit+L10n.swift b/Passepartout/Library/Sources/AppUI/L10n/PassepartoutKit+L10n.swift index a076507b..c6f40f2b 100644 --- a/Passepartout/Library/Sources/AppUI/L10n/PassepartoutKit+L10n.swift +++ b/Passepartout/Library/Sources/AppUI/L10n/PassepartoutKit+L10n.swift @@ -146,7 +146,7 @@ extension OnDemandModule.Policy: LocalizableEntity { extension VPNServer { public var region: String { - [provider.countryCodes.first?.localizedAsRegionCode, provider.area] + [provider.countryCode.localizedAsRegionCode, provider.area] .compactMap { $0 } .joined(separator: " - ") } diff --git a/Passepartout/Library/Sources/AppUI/Mock/Mock.swift b/Passepartout/Library/Sources/AppUI/Mock/Mock.swift index 1315a310..b04481b8 100644 --- a/Passepartout/Library/Sources/AppUI/Mock/Mock.swift +++ b/Passepartout/Library/Sources/AppUI/Mock/Mock.swift @@ -63,8 +63,7 @@ extension AppContext { tunnelEnvironment: env, registry: registry, providerManager: ProviderManager( - repository: InMemoryProviderRepository(), - vpnRepository: InMemoryVPNProviderRepository() + repository: InMemoryProviderRepository() ), constants: .shared ) diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/ProviderContentModifier.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/ProviderContentModifier.swift index 341b327a..73eb06d8 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/ProviderContentModifier.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Provider/ProviderContentModifier.swift @@ -31,7 +31,7 @@ struct ProviderContentModifier: ViewModifier where Entity: @EnvironmentObject private var providerManager: ProviderManager - var apis: [APIMapper] = API.shared + let apis: [APIMapper] @Binding var providerId: ProviderID? diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNFiltersModifier.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNFiltersModifier.swift deleted file mode 100644 index de6d7f7b..00000000 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNFiltersModifier.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// VPNFiltersModifier.swift -// Passepartout -// -// Created by Davide De Rosa on 10/9/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 . -// - -import PassepartoutKit -import SwiftUI - -struct VPNFiltersModifier: ViewModifier where Configuration: Decodable { - - @ObservedObject - var manager: VPNProviderManager - - @State - var isFiltersPresented = false - - func body(content: Content) -> some View { - contentView(with: content) - .onChange(of: manager.parameters.filters) { _ in - Task { - manager.applyFilters() - } - } - } -} diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNFiltersView.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNFiltersView.swift index bd73e9ac..c867ec43 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNFiltersView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNFiltersView.swift @@ -27,95 +27,36 @@ import AppLibrary import PassepartoutKit import SwiftUI -struct VPNFiltersView: View where Configuration: Decodable { +struct VPNFiltersView: View where Configuration: ProviderConfigurationIdentifiable & Decodable { @ObservedObject - var manager: VPNProviderManager + var manager: VPNProviderManager + + @Binding + var filters: VPNFilters var body: some View { - Form { - Section { - categoryPicker - countryPicker - presetPicker -#if os(iOS) - clearFiltersButton - .frame(maxWidth: .infinity, alignment: .center) -#else - HStack { - Spacer() - clearFiltersButton - } -#endif - } - } - } -} - -private extension VPNFiltersView { - var categoryPicker: some View { - Picker(Strings.Global.category, selection: $manager.parameters.filters.categoryName) { - Text(Strings.Global.any) - .tag(nil as String?) - ForEach(categories, id: \.self) { - Text($0.capitalized) - .tag($0 as String?) - } - } - } - - var countryPicker: some View { - Picker(Strings.Global.country, selection: $manager.parameters.filters.countryCode) { - Text(Strings.Global.any) - .tag(nil as String?) - ForEach(countries, id: \.code) { - Text($0.description) - .tag($0.code as String?) - } - } - } - - @ViewBuilder - var presetPicker: some View { - if manager.allPresets.count > 1 { - 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) - .tag($0.presetId as String?) - } - } - } - } - - var clearFiltersButton: some View { - Button(Strings.Views.Provider.clearFilters, role: .destructive) { - Task { - manager.resetFilters() - } - } + debugChanges() + return Subview( + filters: $filters, + categories: categories, + countries: countries, + presets: presets + ) + .onChange(of: filters, perform: manager.applyFilters) } } private extension VPNFiltersView { var categories: [String] { - let allCategories = manager - .allServers - .values - .map(\.provider.categoryName) - - return Set(allCategories) + manager + .allCategoryNames .sorted() } var countries: [(code: String, description: String)] { - let allCodes = manager - .allServers - .values - .flatMap(\.provider.countryCodes) - - return Set(allCodes) + manager + .allCountryCodes .map { (code: $0, description: $0.localizedAsRegionCode ?? $0) } @@ -126,15 +67,95 @@ private extension VPNFiltersView { var presets: [VPNPreset] { manager - .presets(ofType: Configuration.self) + .presets .sorted { $0.description < $1.description } } } -#Preview { - NavigationStack { - VPNFiltersView(manager: VPNProviderManager()) +// MARK: - + +private extension VPNFiltersView { + struct Subview: View { + + @Binding + var filters: VPNFilters + + let categories: [String] + + let countries: [(code: String, description: String)] + + let presets: [VPNPreset] + + var body: some View { + debugChanges() + return Form { + Section { + categoryPicker + countryPicker + presetPicker +#if os(iOS) + clearFiltersButton + .frame(maxWidth: .infinity, alignment: .center) +#else + HStack { + Spacer() + clearFiltersButton + } +#endif + } + } + } + } +} + +private extension VPNFiltersView.Subview { + var categoryPicker: some View { + Picker(Strings.Global.category, selection: $filters.categoryName) { + Text(Strings.Global.any) + .tag(nil as String?) + ForEach(categories, id: \.self) { + Text($0.capitalized) + .tag($0 as String?) + } + } + } + + var countryPicker: some View { + Picker(Strings.Global.country, selection: $filters.countryCode) { + Text(Strings.Global.any) + .tag(nil as String?) + ForEach(countries, id: \.code) { + Text($0.description) + .tag($0.code as String?) + } + } + } + + var presetPicker: some View { + Picker(Strings.Views.Provider.Vpn.preset, selection: $filters.presetId) { + Text(Strings.Global.any) + .tag(nil as String?) + ForEach(presets, id: \.presetId) { + Text($0.description) + .tag($0.presetId as String?) + } + } + } + + var clearFiltersButton: some View { + Button(Strings.Views.Provider.clearFilters, role: .destructive) { + filters = VPNFilters() + } + } +} + +#Preview { + NavigationStack { + VPNFiltersView( + manager: VPNProviderManager(), + filters: .constant(VPNFilters()) + ) } } diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderContentModifier.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderContentModifier.swift index 84a277eb..02d527a8 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderContentModifier.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderContentModifier.swift @@ -28,8 +28,11 @@ import PassepartoutKit import SwiftUI import UtilsLibrary +@MainActor struct VPNProviderContentModifier: ViewModifier where Configuration: ProviderConfigurationIdentifiable & Codable, ProviderRows: View { + var apis: [APIMapper] = API.shared + @Binding var providerId: ProviderID? @@ -43,12 +46,11 @@ struct VPNProviderContentModifier: ViewModifier whe @ViewBuilder let providerRows: ProviderRows - @StateObject - private var vpnProviderManager = VPNProviderManager() - func body(content: Content) -> some View { - content + debugChanges() + return content .modifier(ProviderContentModifier( + apis: apis, providerId: $providerId, entityType: VPNEntity.self, isRequired: isRequired, @@ -64,10 +66,15 @@ struct VPNProviderContentModifier: ViewModifier whe private extension VPNProviderContentModifier { var providerServerRow: some View { NavigationLink { - VPNProviderServerView( - manager: vpnProviderManager, - onSelect: onSelectServer - ) + providerId.map { + VPNProviderServerView( + apis: apis, + providerId: $0, + configurationType: Configuration.self, + selectedEntity: selectedEntity, + onSelect: onSelectServer + ) + } } label: { HStack { Text(Strings.Global.server) @@ -79,27 +86,13 @@ private extension VPNProviderContentModifier { } } } +} +private extension VPNProviderContentModifier { func onSelectProvider(manager: ProviderManager, providerId: ProviderID?, isInitial: Bool) { - guard let providerId else { - return - } - let initialEntity = isInitial ? selectedEntity : nil if !isInitial { selectedEntity = nil } - let view = manager.vpnView( - for: providerId, - configurationType: OpenVPN.Configuration.self, - initialParameters: .init( - sorting: [ - .localizedCountry, - .area, - .hostname - ] - ) - ) - vpnProviderManager.setView(view, filteringWith: initialEntity?.server.provider) } func onSelectServer(server: VPNServer, preset: VPNPreset) { diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderServerView.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderServerView.swift index 573d0b08..0790a153 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderServerView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderServerView.swift @@ -26,27 +26,85 @@ import AppLibrary import PassepartoutKit import SwiftUI +import UtilsLibrary struct VPNProviderServerView: View where Configuration: ProviderConfigurationIdentifiable & Codable { + @EnvironmentObject + private var providerManager: ProviderManager + @Environment(\.dismiss) private var dismiss - @ObservedObject - var manager: VPNProviderManager + let apis: [APIMapper] + + let providerId: ProviderID + + let configurationType: Configuration.Type + + var selectedEntity: VPNEntity? let onSelect: (_ server: VPNServer, _ preset: VPNPreset) -> Void + @StateObject + private var manager = VPNProviderManager(sorting: [ + .localizedCountry, + .area, + .hostname + ]) + + @State + private var filters = VPNFilters() + + @StateObject + private var errorHandler: ErrorHandler = .default() + var body: some View { - serversView - .modifier(VPNFiltersModifier(manager: manager)) - .navigationTitle(Strings.Global.servers) + debugChanges() + return Subview( + manager: manager, + filters: $filters, + onSelect: selectServer + ) + .withErrorHandler(errorHandler) + .navigationTitle(Strings.Global.servers) + .onLoad { + Task { + do { + manager.repository = try await providerManager.vpnRepository( + from: apis, + for: providerId, + configurationType: Configuration.self + ) + if let selectedEntity { + filters = VPNFilters(with: selectedEntity.server.provider) + } else { + filters = VPNFilters() + } + manager.applyFilters(filters) + } catch { + pp_log(.app, .error, "Unable to load VPN repository: \(error)") + errorHandler.handle(error, title: Strings.Global.servers) + } + } + } } } // MARK: - Actions extension VPNProviderServerView { + func compatiblePreset(with server: VPNServer) -> VPNPreset? { + manager + .presets + .first { + if let supportedIds = server.provider.supportedPresetIds { + return supportedIds.contains($0.presetId) + } + return true + } + } + func selectServer(_ server: VPNServer) { guard let preset = compatiblePreset(with: server) else { pp_log(.app, .error, "Unable to find a compatible preset. Supported IDs: \(server.provider.supportedPresetIds ?? [])") @@ -58,24 +116,16 @@ extension VPNProviderServerView { } } -private extension VPNProviderServerView { - func compatiblePreset(with server: VPNServer) -> VPNPreset? { - manager - .presets(ofType: Configuration.self) - .first { - if let supportedIds = server.provider.supportedPresetIds { - return supportedIds.contains($0.presetId) - } - return true - } - } -} - // MARK: - Preview #Preview { + NavigationStack { - VPNProviderServerView(manager: VPNProviderManager()) { _, _ in + VPNProviderServerView( + apis: [API.bundled], + providerId: .protonvpn, + configurationType: OpenVPN.Configuration.self + ) { _, _ in } } .withMockEnvironment() diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/iOS/VPNFiltersModifier+iOS.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/iOS/VPNFiltersModifier+iOS.swift deleted file mode 100644 index 6165fb50..00000000 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/iOS/VPNFiltersModifier+iOS.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// VPNFiltersModifier+iOS.swift -// Passepartout -// -// Created by Davide De Rosa on 10/9/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 . -// - -#if os(iOS) - -import SwiftUI - -// FIXME: ###, providers UI, iPadOS show filters in popover - -extension VPNFiltersModifier { - func contentView(with content: Content) -> some View { - content - .toolbar { - ToolbarItem(placement: .bottomBar) { - Button { - isFiltersPresented = true - } label: { - ThemeImage(.filters) - } - .themeModal(isPresented: $isFiltersPresented) { - NavigationStack { - VPNFiltersView(manager: manager) - .navigationTitle(Strings.Global.filters) - .navigationBarTitleDisplayMode(.inline) - } - .presentationDetents([.medium]) - } - } - } - } -} - -#endif 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 3b1aeac0..ead2c90a 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/iOS/VPNProviderServerView+iOS.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Provider/iOS/VPNProviderServerView+iOS.swift @@ -25,22 +25,72 @@ #if os(iOS) +import PassepartoutKit import SwiftUI -// FIXME: ###, providers UI, iOS server rows + country flags - extension VPNProviderServerView { + struct Subview: View { - @ViewBuilder - var serversView: some View { - List { - ForEach(manager.filteredServers, id: \.id) { server in - Button("\(server.hostname ?? server.id) \(server.provider.countryCodes)") { - selectServer(server) + @ObservedObject + var manager: VPNProviderManager + + @Binding + var filters: VPNFilters + + let onSelect: (VPNServer) -> Void + + @State + private var isFiltersPresented = false + + var body: some View { + listView + .disabled(manager.isFiltering) + .toolbar { + filtersItem } - } } } } +private extension VPNProviderServerView.Subview { + var listView: some View { + List { + // FIXME: ###, providers UI, iOS server rows + country flags + if manager.isFiltering { + ProgressView() + } else { + ForEach(manager.filteredServers, id: \.id) { server in + Button("\(server.hostname ?? server.id) \(server.provider.countryCode)") { + onSelect(server) + } + } + } + } + .themeAnimation(on: manager.isFiltering, category: .providers) + } + + var filtersItem: some ToolbarContent { + ToolbarItem { + Button { + isFiltersPresented = true + } label: { + ThemeImage(.filters) + } + .themePopover(isPresented: $isFiltersPresented, content: filtersView) + } + } + + func filtersView() -> some View { + NavigationStack { + VPNFiltersView( + manager: manager, + filters: $filters + ) + .navigationTitle(Strings.Global.filters) + .navigationBarTitleDisplayMode(.inline) + } + .presentationDetents([.medium]) + } +} + #endif diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/macOS/VPNFiltersModifier+macOS.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/macOS/VPNFiltersModifier+macOS.swift deleted file mode 100644 index 1646cebb..00000000 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/macOS/VPNFiltersModifier+macOS.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// VPNFiltersModifier+macOS.swift -// Passepartout -// -// Created by Davide De Rosa on 10/9/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 . -// - -#if os(macOS) - -import SwiftUI - -extension VPNFiltersModifier { - func contentView(with content: Content) -> some View { - VStack { - VPNFiltersView(manager: manager) - .padding() - content - } - } -} - -#endif 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 8352dfd5..9b811db5 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Provider/macOS/VPNProviderServerView+macOS.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Provider/macOS/VPNProviderServerView+macOS.swift @@ -25,14 +25,33 @@ #if os(macOS) +import PassepartoutKit import SwiftUI // FIXME: ###, providers UI, macOS country flags extension VPNProviderServerView { + struct Subview: View { - @ViewBuilder - var serversView: some View { + @ObservedObject + var manager: VPNProviderManager + + @Binding + var filters: VPNFilters + + let onSelect: (VPNServer) -> Void + + var body: some View { + VStack { + filtersView + tableView + } + } + } +} + +private extension VPNProviderServerView.Subview { + var tableView: some View { Table(manager.filteredServers) { TableColumn(Strings.Global.region) { server in Text(server.region) @@ -43,13 +62,22 @@ extension VPNProviderServerView { TableColumn("") { server in Button { - selectServer(server) + onSelect(server) } label: { Text(Strings.Views.Provider.selectServer) } } .width(min: 100.0, max: 100.0) } + .disabled(manager.isFiltering) + } + + var filtersView: some View { + VPNFiltersView( + manager: manager, + filters: $filters + ) + .padding() } } diff --git a/Passepartout/Library/Sources/AppUI/Views/Theme/Theme+UI.swift b/Passepartout/Library/Sources/AppUI/Views/Theme/Theme+UI.swift index 06cc8321..7a6d2ad9 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Theme/Theme+UI.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Theme/Theme+UI.swift @@ -102,6 +102,27 @@ struct ThemeItemModalModifier: ViewModifier where Modal: View, T: Iden } } +struct ThemeBooleanPopoverModifier: ViewModifier where Popover: View { + + @EnvironmentObject + private var theme: Theme + + @Binding + var isPresented: Bool + + @ViewBuilder + let popover: Popover + + func body(content: Content) -> some View { + content + .popover(isPresented: $isPresented) { + popover + .frame(minWidth: theme.popoverSize?.width, minHeight: theme.popoverSize?.height) + .themeLockScreen() + } + } +} + struct ThemeConfirmationModifier: ViewModifier { @Binding diff --git a/Passepartout/Library/Sources/AppUI/Views/Theme/Theme+iOS.swift b/Passepartout/Library/Sources/AppUI/Views/Theme/Theme+iOS.swift index 6d922b47..6981a957 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Theme/Theme+iOS.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Theme/Theme+iOS.swift @@ -31,6 +31,7 @@ extension Theme { public convenience init() { self.init(dummy: ()) animationCategories = [.diagnostics, .modules, .profiles, .providers] + popoverSize = .init(width: 400.0, height: 400.0) } } diff --git a/Passepartout/Library/Sources/AppUI/Views/Theme/Theme.swift b/Passepartout/Library/Sources/AppUI/Views/Theme/Theme.swift index 8a57100b..1e65681a 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Theme/Theme.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Theme/Theme.swift @@ -40,6 +40,8 @@ public final class Theme: ObservableObject { var secondaryModalSize: CGSize? + var popoverSize: CGSize? + var relevantWeight: Font.Weight = .semibold var titleColor: Color = .primary @@ -163,6 +165,16 @@ extension View { )) } + public func themePopover( + isPresented: Binding, + content: @escaping () -> Content + ) -> some View where Content: View { + modifier(ThemeBooleanPopoverModifier( + isPresented: isPresented, + popover: content + )) + } + public func themeConfirmation(isPresented: Binding, title: String, action: @escaping () -> Void) -> some View { modifier(ThemeConfirmationModifier(isPresented: isPresented, title: title, action: action)) } diff --git a/Passepartout/Shared/Shared+App.swift b/Passepartout/Shared/Shared+App.swift index 2c91461a..5368676a 100644 --- a/Passepartout/Shared/Shared+App.swift +++ b/Passepartout/Shared/Shared+App.swift @@ -209,8 +209,7 @@ private extension ProfileManager { // FIXME: #705, store providers to Core Data extension ProviderManager { static let shared = ProviderManager( - repository: InMemoryProviderRepository(), - vpnRepository: InMemoryVPNProviderRepository() + repository: InMemoryProviderRepository() ) }