parent
3abde3851a
commit
df4e3465f5
|
@ -461,6 +461,8 @@ public enum Strings {
|
||||||
}
|
}
|
||||||
/// None
|
/// None
|
||||||
public static let noProvider = Strings.tr("Localizable", "providers.no_provider", fallback: "None")
|
public static let noProvider = Strings.tr("Localizable", "providers.no_provider", fallback: "None")
|
||||||
|
/// Only favorites
|
||||||
|
public static let onlyFavorites = Strings.tr("Localizable", "providers.only_favorites", fallback: "Only favorites")
|
||||||
/// Refresh infrastructure
|
/// Refresh infrastructure
|
||||||
public static let refreshInfrastructure = Strings.tr("Localizable", "providers.refresh_infrastructure", fallback: "Refresh infrastructure")
|
public static let refreshInfrastructure = Strings.tr("Localizable", "providers.refresh_infrastructure", fallback: "Refresh infrastructure")
|
||||||
/// Select
|
/// Select
|
||||||
|
|
|
@ -228,6 +228,7 @@
|
||||||
"providers.no_provider" = "None";
|
"providers.no_provider" = "None";
|
||||||
"providers.select_provider" = "Select a provider";
|
"providers.select_provider" = "Select a provider";
|
||||||
"providers.select_entity" = "Select";
|
"providers.select_entity" = "Select";
|
||||||
|
"providers.only_favorites" = "Only favorites";
|
||||||
"providers.clear_filters" = "Clear filters";
|
"providers.clear_filters" = "Clear filters";
|
||||||
"providers.refresh_infrastructure" = "Refresh infrastructure";
|
"providers.refresh_infrastructure" = "Refresh infrastructure";
|
||||||
"providers.last_updated" = "Last updated on %@";
|
"providers.last_updated" = "Last updated on %@";
|
||||||
|
|
|
@ -48,6 +48,7 @@ extension ProviderEntityViewProviding where Self: ProviderCompatibleModule, Enti
|
||||||
} as? VPNEntity<EntityType.Configuration>
|
} as? VPNEntity<EntityType.Configuration>
|
||||||
|
|
||||||
return VPNProviderServerCoordinator(
|
return VPNProviderServerCoordinator(
|
||||||
|
moduleId: id,
|
||||||
providerId: provider.id,
|
providerId: provider.id,
|
||||||
selectedEntity: selectedEntity,
|
selectedEntity: selectedEntity,
|
||||||
onSelect: onSelect
|
onSelect: onSelect
|
||||||
|
|
|
@ -128,6 +128,7 @@ private extension OpenVPNView {
|
||||||
case .providerServer:
|
case .providerServer:
|
||||||
providerId.wrappedValue.map {
|
providerId.wrappedValue.map {
|
||||||
VPNProviderServerView(
|
VPNProviderServerView(
|
||||||
|
moduleId: module.id,
|
||||||
providerId: $0,
|
providerId: $0,
|
||||||
configurationType: OpenVPN.Configuration.self,
|
configurationType: OpenVPN.Configuration.self,
|
||||||
selectedEntity: providerEntity.wrappedValue,
|
selectedEntity: providerEntity.wrappedValue,
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import AppLibrary
|
import AppLibrary
|
||||||
|
import CommonLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@ -35,15 +36,41 @@ struct VPNFiltersView<Configuration>: View where Configuration: ProviderConfigur
|
||||||
@Binding
|
@Binding
|
||||||
var filters: VPNFilters
|
var filters: VPNFilters
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var onlyShowsFavorites: Bool
|
||||||
|
|
||||||
|
let favorites: Set<String>
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
debugChanges()
|
debugChanges()
|
||||||
return Subview(
|
return Subview(
|
||||||
filters: $filters,
|
filters: $filters,
|
||||||
|
onlyShowsFavorites: $onlyShowsFavorites,
|
||||||
categories: categories,
|
categories: categories,
|
||||||
countries: countries,
|
countries: countries,
|
||||||
presets: presets
|
presets: presets,
|
||||||
|
favorites: favorites
|
||||||
)
|
)
|
||||||
.onChange(of: filters, perform: manager.applyFilters)
|
.disabled(manager.isFiltering)
|
||||||
|
.onChange(of: filters) { filters in
|
||||||
|
Task {
|
||||||
|
await manager.applyFilters(filters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: favorites) {
|
||||||
|
if onlyShowsFavorites {
|
||||||
|
filters.serverIds = $0
|
||||||
|
Task {
|
||||||
|
await manager.applyFilters(filters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: onlyShowsFavorites) {
|
||||||
|
filters.serverIds = $0 ? favorites : nil
|
||||||
|
Task {
|
||||||
|
await manager.applyFilters(filters)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,12 +109,17 @@ private extension VPNFiltersView {
|
||||||
@Binding
|
@Binding
|
||||||
var filters: VPNFilters
|
var filters: VPNFilters
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var onlyShowsFavorites: Bool
|
||||||
|
|
||||||
let categories: [String]
|
let categories: [String]
|
||||||
|
|
||||||
let countries: [(code: String, description: String)]
|
let countries: [(code: String, description: String)]
|
||||||
|
|
||||||
let presets: [VPNPreset<Configuration>]
|
let presets: [VPNPreset<Configuration>]
|
||||||
|
|
||||||
|
let favorites: Set<String>
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
debugChanges()
|
debugChanges()
|
||||||
return Form {
|
return Form {
|
||||||
|
@ -95,6 +127,7 @@ private extension VPNFiltersView {
|
||||||
categoryPicker
|
categoryPicker
|
||||||
countryPicker
|
countryPicker
|
||||||
presetPicker
|
presetPicker
|
||||||
|
favoritesToggle
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
clearFiltersButton
|
clearFiltersButton
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
@ -144,8 +177,13 @@ private extension VPNFiltersView.Subview {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var favoritesToggle: some View {
|
||||||
|
Toggle(Strings.Providers.onlyFavorites, isOn: $onlyShowsFavorites)
|
||||||
|
}
|
||||||
|
|
||||||
var clearFiltersButton: some View {
|
var clearFiltersButton: some View {
|
||||||
Button(Strings.Providers.clearFilters, role: .destructive) {
|
Button(Strings.Providers.clearFilters, role: .destructive) {
|
||||||
|
onlyShowsFavorites = false
|
||||||
filters = VPNFilters()
|
filters = VPNFilters()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,7 +193,9 @@ private extension VPNFiltersView.Subview {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
VPNFiltersView<OpenVPN.Configuration>(
|
VPNFiltersView<OpenVPN.Configuration>(
|
||||||
manager: VPNProviderManager(),
|
manager: VPNProviderManager(),
|
||||||
filters: .constant(VPNFilters())
|
filters: .constant(VPNFilters()),
|
||||||
|
onlyShowsFavorites: .constant(false),
|
||||||
|
favorites: []
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,8 @@ struct VPNProviderServerCoordinator<Configuration>: View where Configuration: Pr
|
||||||
@Environment(\.dismiss)
|
@Environment(\.dismiss)
|
||||||
private var dismiss
|
private var dismiss
|
||||||
|
|
||||||
|
let moduleId: UUID
|
||||||
|
|
||||||
let providerId: ProviderID
|
let providerId: ProviderID
|
||||||
|
|
||||||
let selectedEntity: VPNEntity<Configuration>?
|
let selectedEntity: VPNEntity<Configuration>?
|
||||||
|
@ -44,6 +46,7 @@ struct VPNProviderServerCoordinator<Configuration>: View where Configuration: Pr
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
VPNProviderServerView(
|
VPNProviderServerView(
|
||||||
|
moduleId: moduleId,
|
||||||
providerId: providerId,
|
providerId: providerId,
|
||||||
configurationType: Configuration.self,
|
configurationType: Configuration.self,
|
||||||
selectedEntity: selectedEntity,
|
selectedEntity: selectedEntity,
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import AppLibrary
|
import AppLibrary
|
||||||
|
import CommonLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UtilsLibrary
|
import UtilsLibrary
|
||||||
|
@ -33,8 +34,13 @@ struct VPNProviderServerView<Configuration>: View where Configuration: ProviderC
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var providerManager: ProviderManager
|
private var providerManager: ProviderManager
|
||||||
|
|
||||||
|
@AppStorage(AppPreference.moduleFavoriteServers.key)
|
||||||
|
var allFavorites = ModuleFavoriteServers()
|
||||||
|
|
||||||
var apis: [APIMapper] = API.shared
|
var apis: [APIMapper] = API.shared
|
||||||
|
|
||||||
|
let moduleId: UUID
|
||||||
|
|
||||||
let providerId: ProviderID
|
let providerId: ProviderID
|
||||||
|
|
||||||
let configurationType: Configuration.Type
|
let configurationType: Configuration.Type
|
||||||
|
@ -57,6 +63,9 @@ struct VPNProviderServerView<Configuration>: View where Configuration: ProviderC
|
||||||
@State
|
@State
|
||||||
private var filters = VPNFilters()
|
private var filters = VPNFilters()
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var onlyShowsFavorites = false
|
||||||
|
|
||||||
@StateObject
|
@StateObject
|
||||||
private var errorHandler: ErrorHandler = .default()
|
private var errorHandler: ErrorHandler = .default()
|
||||||
|
|
||||||
|
@ -66,6 +75,8 @@ struct VPNProviderServerView<Configuration>: View where Configuration: ProviderC
|
||||||
manager: manager,
|
manager: manager,
|
||||||
selectedServer: selectedEntity?.server,
|
selectedServer: selectedEntity?.server,
|
||||||
filters: $filters,
|
filters: $filters,
|
||||||
|
onlyShowsFavorites: $onlyShowsFavorites,
|
||||||
|
favorites: favoritesBinding,
|
||||||
selectTitle: selectTitle,
|
selectTitle: selectTitle,
|
||||||
onSelect: selectServer
|
onSelect: selectServer
|
||||||
)
|
)
|
||||||
|
@ -84,7 +95,7 @@ struct VPNProviderServerView<Configuration>: View where Configuration: ProviderC
|
||||||
} else {
|
} else {
|
||||||
filters = VPNFilters()
|
filters = VPNFilters()
|
||||||
}
|
}
|
||||||
manager.applyFilters(filters)
|
await manager.applyFilters(filters)
|
||||||
} catch {
|
} catch {
|
||||||
pp_log(.app, .error, "Unable to load VPN repository: \(error)")
|
pp_log(.app, .error, "Unable to load VPN repository: \(error)")
|
||||||
errorHandler.handle(error, title: Strings.Global.servers)
|
errorHandler.handle(error, title: Strings.Global.servers)
|
||||||
|
@ -108,6 +119,14 @@ extension VPNProviderServerView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var favoritesBinding: Binding<Set<String>> {
|
||||||
|
Binding {
|
||||||
|
allFavorites.servers(forModuleWithID: moduleId)
|
||||||
|
} set: {
|
||||||
|
allFavorites.setServers($0, forModuleWithID: moduleId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func selectServer(_ server: VPNServer) {
|
func selectServer(_ server: VPNServer) {
|
||||||
guard let preset = compatiblePreset(with: server) else {
|
guard let preset = compatiblePreset(with: server) else {
|
||||||
pp_log(.app, .error, "Unable to find a compatible preset. Supported IDs: \(server.provider.supportedPresetIds ?? [])")
|
pp_log(.app, .error, "Unable to find a compatible preset. Supported IDs: \(server.provider.supportedPresetIds ?? [])")
|
||||||
|
@ -124,6 +143,7 @@ extension VPNProviderServerView {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
VPNProviderServerView(
|
VPNProviderServerView(
|
||||||
apis: [API.bundled],
|
apis: [API.bundled],
|
||||||
|
moduleId: UUID(),
|
||||||
providerId: .protonvpn,
|
providerId: .protonvpn,
|
||||||
configurationType: OpenVPN.Configuration.self,
|
configurationType: OpenVPN.Configuration.self,
|
||||||
selectedEntity: nil,
|
selectedEntity: nil,
|
||||||
|
|
|
@ -39,6 +39,12 @@ extension VPNProviderServerView {
|
||||||
@Binding
|
@Binding
|
||||||
var filters: VPNFilters
|
var filters: VPNFilters
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var onlyShowsFavorites: Bool
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var favorites: Set<String>
|
||||||
|
|
||||||
// unused
|
// unused
|
||||||
let selectTitle: String
|
let selectTitle: String
|
||||||
|
|
||||||
|
@ -82,7 +88,11 @@ private extension VPNProviderServerView.Subview {
|
||||||
manager
|
manager
|
||||||
.allCountryCodes
|
.allCountryCodes
|
||||||
.sorted {
|
.sorted {
|
||||||
$0.localizedAsRegionCode! < $1.localizedAsRegionCode!
|
guard let region1 = $0.localizedAsRegionCode,
|
||||||
|
let region2 = $1.localizedAsRegionCode else {
|
||||||
|
return $0 < $1
|
||||||
|
}
|
||||||
|
return region1 < region2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +113,7 @@ private extension VPNProviderServerView.Subview {
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
ThemeCountryFlag(code: code)
|
ThemeCountryFlag(code: code)
|
||||||
Text(code.localizedAsRegionCode!)
|
Text(code.localizedAsRegionCode ?? code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,10 +124,21 @@ private extension VPNProviderServerView.Subview {
|
||||||
onSelect(server)
|
onSelect(server)
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Text(server.hostname ?? server.serverId)
|
|
||||||
Spacer()
|
|
||||||
ThemeImage(.marked)
|
ThemeImage(.marked)
|
||||||
.opacity(server.id == selectedServer?.id ? 1.0 : 0.0)
|
.opacity(server.id == selectedServer?.id ? 1.0 : 0.0)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
if let area = server.provider.area {
|
||||||
|
Text(area)
|
||||||
|
.font(.headline)
|
||||||
|
Text(server.provider.serverId)
|
||||||
|
.font(.subheadline)
|
||||||
|
} else {
|
||||||
|
Text(server.provider.serverId)
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
FavoriteToggle(value: server.serverId, selection: $favorites)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,7 +158,9 @@ private extension VPNProviderServerView.Subview {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
VPNFiltersView(
|
VPNFiltersView(
|
||||||
manager: manager,
|
manager: manager,
|
||||||
filters: $filters
|
filters: $filters,
|
||||||
|
onlyShowsFavorites: $onlyShowsFavorites,
|
||||||
|
favorites: favorites
|
||||||
)
|
)
|
||||||
.navigationTitle(Strings.Global.filters)
|
.navigationTitle(Strings.Global.filters)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
@ -152,6 +175,7 @@ private extension VPNProviderServerView.Subview {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
VPNProviderServerView(
|
VPNProviderServerView(
|
||||||
apis: [API.bundled],
|
apis: [API.bundled],
|
||||||
|
moduleId: UUID(),
|
||||||
providerId: .tunnelbear,
|
providerId: .tunnelbear,
|
||||||
configurationType: OpenVPN.Configuration.self,
|
configurationType: OpenVPN.Configuration.self,
|
||||||
selectedEntity: nil,
|
selectedEntity: nil,
|
||||||
|
|
|
@ -39,10 +39,19 @@ extension VPNProviderServerView {
|
||||||
@Binding
|
@Binding
|
||||||
var filters: VPNFilters
|
var filters: VPNFilters
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var onlyShowsFavorites: Bool
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var favorites: Set<String>
|
||||||
|
|
||||||
let selectTitle: String
|
let selectTitle: String
|
||||||
|
|
||||||
let onSelect: (VPNServer) -> Void
|
let onSelect: (VPNServer) -> Void
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var hoveringServerId: String?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
filtersView
|
filtersView
|
||||||
|
@ -70,6 +79,15 @@ private extension VPNProviderServerView.Subview {
|
||||||
|
|
||||||
TableColumn(Strings.Global.address, value: \.address)
|
TableColumn(Strings.Global.address, value: \.address)
|
||||||
|
|
||||||
|
TableColumn("") { server in
|
||||||
|
FavoriteToggle(value: server.serverId, selection: $favorites)
|
||||||
|
.opacity(favorites.contains(server.serverId) || server.serverId == hoveringServerId ? 1.0 : 0.0)
|
||||||
|
.onHover {
|
||||||
|
hoveringServerId = $0 ? server.serverId : nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width(20.0)
|
||||||
|
|
||||||
TableColumn("") { server in
|
TableColumn("") { server in
|
||||||
Button {
|
Button {
|
||||||
onSelect(server)
|
onSelect(server)
|
||||||
|
@ -84,7 +102,9 @@ private extension VPNProviderServerView.Subview {
|
||||||
var filtersView: some View {
|
var filtersView: some View {
|
||||||
VPNFiltersView(
|
VPNFiltersView(
|
||||||
manager: manager,
|
manager: manager,
|
||||||
filters: $filters
|
filters: $filters,
|
||||||
|
onlyShowsFavorites: $onlyShowsFavorites,
|
||||||
|
favorites: favorites
|
||||||
)
|
)
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
@ -96,6 +116,7 @@ private extension VPNProviderServerView.Subview {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
VPNProviderServerView(
|
VPNProviderServerView(
|
||||||
apis: [API.bundled],
|
apis: [API.bundled],
|
||||||
|
moduleId: UUID(),
|
||||||
providerId: .tunnelbear,
|
providerId: .tunnelbear,
|
||||||
configurationType: OpenVPN.Configuration.self,
|
configurationType: OpenVPN.Configuration.self,
|
||||||
selectedEntity: nil,
|
selectedEntity: nil,
|
||||||
|
|
|
@ -36,6 +36,8 @@ extension Theme {
|
||||||
case disclose
|
case disclose
|
||||||
case editableSectionEdit
|
case editableSectionEdit
|
||||||
case editableSectionRemove
|
case editableSectionRemove
|
||||||
|
case favoriteOff
|
||||||
|
case favoriteOn
|
||||||
case filters
|
case filters
|
||||||
case footerAdd
|
case footerAdd
|
||||||
case hide
|
case hide
|
||||||
|
|
|
@ -89,6 +89,8 @@ 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 .favoriteOff: return "star"
|
||||||
|
case .favoriteOn: return "star.fill"
|
||||||
case .filters: return "line.3.horizontal.decrease"
|
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"
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
//
|
||||||
|
// ThemeFavoriteToggle.swift
|
||||||
|
// Passepartout
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 10/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 SwiftUI
|
||||||
|
|
||||||
|
struct FavoriteToggle<ID>: View where ID: Hashable {
|
||||||
|
let value: ID
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var selection: Set<ID>
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button {
|
||||||
|
if selection.contains(value) {
|
||||||
|
selection.remove(value)
|
||||||
|
} else {
|
||||||
|
selection.insert(value)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
ThemeImage(selection.contains(value) ? .favoriteOn : .favoriteOff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,6 +32,8 @@ public enum AppPreference: String {
|
||||||
|
|
||||||
case logsPrivateData
|
case logsPrivateData
|
||||||
|
|
||||||
|
case moduleFavoriteServers
|
||||||
|
|
||||||
case profilesLayout
|
case profilesLayout
|
||||||
|
|
||||||
public var key: String {
|
public var key: String {
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
//
|
||||||
|
// ModuleFavoriteServers.swift
|
||||||
|
// Passepartout
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 10/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 SwiftUI
|
||||||
|
|
||||||
|
public struct ModuleFavoriteServers {
|
||||||
|
private var map: [UUID: Set<String>]
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
map = [:]
|
||||||
|
}
|
||||||
|
|
||||||
|
public func servers(forModuleWithID moduleId: UUID) -> Set<String> {
|
||||||
|
map[moduleId] ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
public mutating func setServers(_ servers: Set<String>, forModuleWithID moduleId: UUID) {
|
||||||
|
map[moduleId] = servers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ModuleFavoriteServers: RawRepresentable {
|
||||||
|
public var rawValue: String {
|
||||||
|
(try? JSONEncoder().encode(map))?.base64EncodedString() ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
public init?(rawValue: String) {
|
||||||
|
guard let data = Data(base64Encoded: rawValue) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
map = (try? JSONDecoder().decode([UUID: Set<String>].self, from: data)) ?? [:]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue