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() ) }