diff --git a/Library/Sources/AppDataPreferences/Domain/CDFavoriteServer.swift b/Library/Sources/AppDataPreferences/Domain/CDFavoriteServer.swift
new file mode 100644
index 00000000..cbe11d40
--- /dev/null
+++ b/Library/Sources/AppDataPreferences/Domain/CDFavoriteServer.swift
@@ -0,0 +1,37 @@
+//
+// CDFavoriteServer.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 12/10/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 CoreData
+import Foundation
+
+@objc(CDFavoriteServer)
+final class CDFavoriteServer: NSManagedObject {
+ @nonobjc static func fetchRequest() -> NSFetchRequest {
+ NSFetchRequest(entityName: "CDFavoriteServer")
+ }
+
+ @NSManaged var serverId: String?
+ @NSManaged var providerPreferences: CDProviderPreferencesV3?
+}
diff --git a/Library/Sources/AppDataPreferences/Domain/CDProviderPreferencesV3.swift b/Library/Sources/AppDataPreferences/Domain/CDProviderPreferencesV3.swift
index 3d3fabc1..a7ba2ea6 100644
--- a/Library/Sources/AppDataPreferences/Domain/CDProviderPreferencesV3.swift
+++ b/Library/Sources/AppDataPreferences/Domain/CDProviderPreferencesV3.swift
@@ -35,4 +35,5 @@ final class CDProviderPreferencesV3: NSManagedObject {
@NSManaged var providerId: String?
@NSManaged var lastUpdate: Date?
@NSManaged var favoriteServerIds: Data?
+ @NSManaged var favoriteServers: Set?
}
diff --git a/Library/Sources/AppDataPreferences/Domain/Mapper.swift b/Library/Sources/AppDataPreferences/Domain/Mapper.swift
index 4f37d23e..5dd7fff6 100644
--- a/Library/Sources/AppDataPreferences/Domain/Mapper.swift
+++ b/Library/Sources/AppDataPreferences/Domain/Mapper.swift
@@ -29,15 +29,6 @@ import Foundation
import PassepartoutKit
struct DomainMapper {
- func excludedEndpoints(from entities: Set?) -> Set {
- entities.map {
- Set($0.compactMap {
- $0.endpoint.map {
- ExtendedEndpoint(rawValue: $0)
- } ?? nil
- })
- } ?? []
- }
}
struct CoreDataMapper {
@@ -49,7 +40,9 @@ struct CoreDataMapper {
return cdEndpoint
}
- func cdExcludedEndpoints(from endpoints: Set) -> Set {
- Set(endpoints.map(cdExcludedEndpoint(from:)))
+ func cdFavoriteServer(from serverId: String) -> CDFavoriteServer {
+ let cdFavorite = CDFavoriteServer(context: context)
+ cdFavorite.serverId = serverId
+ return cdFavorite
}
}
diff --git a/Library/Sources/AppDataPreferences/Preferences.xcdatamodeld/Preferences.xcdatamodel/contents b/Library/Sources/AppDataPreferences/Preferences.xcdatamodeld/Preferences.xcdatamodel/contents
index abd570d6..16aa7607 100644
--- a/Library/Sources/AppDataPreferences/Preferences.xcdatamodeld/Preferences.xcdatamodel/contents
+++ b/Library/Sources/AppDataPreferences/Preferences.xcdatamodeld/Preferences.xcdatamodel/contents
@@ -4,6 +4,10 @@
+
+
+
+
@@ -13,5 +17,6 @@
+
\ No newline at end of file
diff --git a/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift b/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift
index 6ca884e6..b07bbfa7 100644
--- a/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift
+++ b/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift
@@ -30,6 +30,8 @@ import Foundation
import PassepartoutKit
extension AppData {
+
+ @MainActor
public static func cdModulePreferencesRepositoryV3(context: NSManagedObjectContext, moduleId: UUID) throws -> ModulePreferencesRepository {
try CDModulePreferencesRepositoryV3(context: context, moduleId: moduleId)
}
diff --git a/Library/Sources/AppDataPreferences/Strategy/CDProviderPreferencesRepositoryV3.swift b/Library/Sources/AppDataPreferences/Strategy/CDProviderPreferencesRepositoryV3.swift
index 5fd0a9e8..63447789 100644
--- a/Library/Sources/AppDataPreferences/Strategy/CDProviderPreferencesRepositoryV3.swift
+++ b/Library/Sources/AppDataPreferences/Strategy/CDProviderPreferencesRepositoryV3.swift
@@ -30,6 +30,8 @@ import Foundation
import PassepartoutKit
extension AppData {
+
+ @MainActor
public static func cdProviderPreferencesRepositoryV3(context: NSManagedObjectContext, providerId: ProviderID) throws -> ProviderPreferencesRepository {
try CDProviderPreferencesRepositoryV3(context: context, providerId: providerId)
}
@@ -55,12 +57,26 @@ private final class CDProviderPreferencesRepositoryV3: ProviderPreferencesReposi
guard $0.offset > 0 else {
return
}
+ $0.element.favoriteServers?.forEach(context.delete(_:))
context.delete($0.element)
}
let entity = entities.first ?? CDProviderPreferencesV3(context: context)
entity.providerId = providerId.rawValue
entity.lastUpdate = Date()
+
+ // migrate favorite server ids
+ if let favoriteServerIds = entity.favoriteServerIds {
+ let mapper = CoreDataMapper(context: context)
+ let ids = try? JSONDecoder().decode(Set.self, from: favoriteServerIds)
+ var favoriteServers: Set = []
+ ids?.forEach {
+ favoriteServers.insert(mapper.cdFavoriteServer(from: $0))
+ }
+ entity.favoriteServers = favoriteServers
+ entity.favoriteServerIds = nil
+ }
+
return entity
} catch {
pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(error)")
@@ -69,28 +85,37 @@ private final class CDProviderPreferencesRepositoryV3: ProviderPreferencesReposi
}
}
- var favoriteServers: Set {
- get {
- do {
- return try context.performAndWait {
- guard let data = entity.favoriteServerIds else {
- return []
- }
- return try JSONDecoder().decode(Set.self, from: data)
- }
- } catch {
- pp_log(.app, .error, "Unable to get favoriteServers: \(error)")
- return []
- }
+ func isFavoriteServer(_ serverId: String) -> Bool {
+ context.performAndWait {
+ entity.favoriteServers?.contains {
+ $0.serverId == serverId
+ } ?? false
}
- set {
- do {
- try context.performAndWait {
- entity.favoriteServerIds = try JSONEncoder().encode(newValue)
- }
- } catch {
- pp_log(.app, .error, "Unable to set favoriteServers: \(error)")
+ }
+
+ func addFavoriteServer(_ serverId: String) {
+ context.performAndWait {
+ guard entity.favoriteServers?.contains(where: {
+ $0.serverId == serverId
+ }) != true else {
+ return
}
+ let mapper = CoreDataMapper(context: context)
+ let cdFavorite = mapper.cdFavoriteServer(from: serverId)
+ cdFavorite.providerPreferences = entity
+ entity.favoriteServers?.insert(cdFavorite)
+ }
+ }
+
+ func removeFavoriteServer(_ serverId: String) {
+ context.performAndWait {
+ guard let found = entity.favoriteServers?.first(where: {
+ $0.serverId == serverId
+ }) else {
+ return
+ }
+ entity.favoriteServers?.remove(found)
+ context.delete(found)
}
}
diff --git a/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift b/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift
index ac834623..0508add1 100644
--- a/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift
+++ b/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift
@@ -126,7 +126,7 @@ private extension VPNProviderServerView {
var filteredServers: [VPNServer] {
if onlyShowsFavorites {
return servers.filter {
- providerPreferences.favoriteServers.contains($0.serverId)
+ providerPreferences.isFavoriteServer($0.serverId)
}
}
return servers
diff --git a/Library/Sources/AppUIMain/Views/VPN/iOS/VPNProviderServer+Content+iOS.swift b/Library/Sources/AppUIMain/Views/VPN/iOS/VPNProviderServer+Content+iOS.swift
index a36a394d..3d4bfc57 100644
--- a/Library/Sources/AppUIMain/Views/VPN/iOS/VPNProviderServer+Content+iOS.swift
+++ b/Library/Sources/AppUIMain/Views/VPN/iOS/VPNProviderServer+Content+iOS.swift
@@ -151,7 +151,7 @@ private extension VPNProviderServerView.ContentView {
Spacer()
FavoriteToggle(
value: server.serverId,
- selection: $providerPreferences.favoriteServers
+ selection: providerPreferences.favoriteServers()
)
}
}
diff --git a/Library/Sources/AppUIMain/Views/VPN/macOS/VPNProviderServer+Content+macOS.swift b/Library/Sources/AppUIMain/Views/VPN/macOS/VPNProviderServer+Content+macOS.swift
index 5be6bf19..5d38cc21 100644
--- a/Library/Sources/AppUIMain/Views/VPN/macOS/VPNProviderServer+Content+macOS.swift
+++ b/Library/Sources/AppUIMain/Views/VPN/macOS/VPNProviderServer+Content+macOS.swift
@@ -87,7 +87,7 @@ private extension VPNProviderServerView.ContentView {
TableColumn("") { server in
FavoriteToggle(
value: server.serverId,
- selection: $providerPreferences.favoriteServers
+ selection: providerPreferences.favoriteServers()
)
.environmentObject(theme) // TODO: #873, Table loses environment
}
diff --git a/Library/Sources/CommonLibrary/Business/ModulePreferences.swift b/Library/Sources/CommonLibrary/Business/ModulePreferences.swift
index 25642aff..553897ec 100644
--- a/Library/Sources/CommonLibrary/Business/ModulePreferences.swift
+++ b/Library/Sources/CommonLibrary/Business/ModulePreferences.swift
@@ -27,8 +27,7 @@ import CommonUtils
import Foundation
import PassepartoutKit
-@MainActor
-public final class ModulePreferences: ObservableObject {
+public final class ModulePreferences: ObservableObject, ModulePreferencesRepository {
private var repository: ModulePreferencesRepository?
public init() {
@@ -43,10 +42,12 @@ public final class ModulePreferences: ObservableObject {
}
public func addExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
+ objectWillChange.send()
repository?.addExcludedEndpoint(endpoint)
}
public func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
+ objectWillChange.send()
repository?.removeExcludedEndpoint(endpoint)
}
diff --git a/Library/Sources/CommonLibrary/Business/PreferencesManager.swift b/Library/Sources/CommonLibrary/Business/PreferencesManager.swift
index ac8f662a..a31a0e56 100644
--- a/Library/Sources/CommonLibrary/Business/PreferencesManager.swift
+++ b/Library/Sources/CommonLibrary/Business/PreferencesManager.swift
@@ -27,14 +27,15 @@ import CommonUtils
import Foundation
import PassepartoutKit
-public final class PreferencesManager: ObservableObject, Sendable {
- private let modulesFactory: @Sendable (UUID) throws -> ModulePreferencesRepository
+@MainActor
+public final class PreferencesManager: ObservableObject {
+ private let modulesFactory: (UUID) throws -> ModulePreferencesRepository
- private let providersFactory: @Sendable (ProviderID) throws -> ProviderPreferencesRepository
+ private let providersFactory: (ProviderID) throws -> ProviderPreferencesRepository
public init(
- modulesFactory: (@Sendable (UUID) throws -> ModulePreferencesRepository)? = nil,
- providersFactory: (@Sendable (ProviderID) throws -> ProviderPreferencesRepository)? = nil
+ modulesFactory: ((UUID) throws -> ModulePreferencesRepository)? = nil,
+ providersFactory: ((ProviderID) throws -> ProviderPreferencesRepository)? = nil
) {
self.modulesFactory = modulesFactory ?? { _ in
DummyModulePreferencesRepository()
@@ -57,6 +58,7 @@ extension PreferencesManager {
// MARK: - Dummy
+@MainActor
private final class DummyModulePreferencesRepository: ModulePreferencesRepository {
func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool {
false
@@ -72,8 +74,17 @@ private final class DummyModulePreferencesRepository: ModulePreferencesRepositor
}
}
+@MainActor
private final class DummyProviderPreferencesRepository: ProviderPreferencesRepository {
- var favoriteServers: Set = []
+ func isFavoriteServer(_ serverId: String) -> Bool {
+ false
+ }
+
+ func addFavoriteServer(_ serverId: String) {
+ }
+
+ func removeFavoriteServer(_ serverId: String) {
+ }
func save() throws {
}
diff --git a/Library/Sources/CommonLibrary/Business/ProviderPreferences.swift b/Library/Sources/CommonLibrary/Business/ProviderPreferences.swift
index 707e2c6a..9171da9c 100644
--- a/Library/Sources/CommonLibrary/Business/ProviderPreferences.swift
+++ b/Library/Sources/CommonLibrary/Business/ProviderPreferences.swift
@@ -27,8 +27,7 @@ import CommonUtils
import Foundation
import PassepartoutKit
-@MainActor
-public final class ProviderPreferences: ObservableObject {
+public final class ProviderPreferences: ObservableObject, ProviderPreferencesRepository {
private var repository: ProviderPreferencesRepository?
public init() {
@@ -38,16 +37,30 @@ public final class ProviderPreferences: ObservableObject {
self.repository = repository
}
- public var favoriteServers: Set {
- get {
- repository?.favoriteServers ?? []
- }
- set {
- objectWillChange.send()
- repository?.favoriteServers = newValue
+ public func favoriteServers() -> ObservableList {
+ ObservableList { [weak self] in
+ self?.isFavoriteServer($0) ?? false
+ } add: { [weak self] in
+ self?.addFavoriteServer($0)
+ } remove: { [weak self] in
+ self?.removeFavoriteServer($0)
}
}
+ public func isFavoriteServer(_ serverId: String) -> Bool {
+ repository?.isFavoriteServer(serverId) ?? false
+ }
+
+ public func addFavoriteServer(_ serverId: String) {
+ objectWillChange.send()
+ repository?.addFavoriteServer(serverId)
+ }
+
+ public func removeFavoriteServer(_ serverId: String) {
+ objectWillChange.send()
+ repository?.removeFavoriteServer(serverId)
+ }
+
public func save() throws {
try repository?.save()
}
diff --git a/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift b/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift
index a0481589..bfa12049 100644
--- a/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift
+++ b/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift
@@ -26,6 +26,7 @@
import Foundation
import PassepartoutKit
+@MainActor
public protocol ModulePreferencesRepository {
func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool
diff --git a/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift b/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift
index 477fd7f7..01064770 100644
--- a/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift
+++ b/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift
@@ -26,8 +26,13 @@
import Foundation
import PassepartoutKit
+@MainActor
public protocol ProviderPreferencesRepository {
- var favoriteServers: Set { get set }
+ func isFavoriteServer(_ serverId: String) -> Bool
+
+ func addFavoriteServer(_ serverId: String)
+
+ func removeFavoriteServer(_ serverId: String)
func save() throws
}
diff --git a/Library/Sources/UILibrary/Views/UI/FavoriteToggle.swift b/Library/Sources/UILibrary/Views/UI/FavoriteToggle.swift
index f9391eb5..37d20a50 100644
--- a/Library/Sources/UILibrary/Views/UI/FavoriteToggle.swift
+++ b/Library/Sources/UILibrary/Views/UI/FavoriteToggle.swift
@@ -23,20 +23,21 @@
// along with Passepartout. If not, see .
//
+import CommonUtils
import SwiftUI
public struct FavoriteToggle: View where ID: Hashable {
private let value: ID
- @Binding
- private var selection: Set
+ @ObservedObject
+ private var selection: ObservableList
@State
private var hover: ID?
- public init(value: ID, selection: Binding>) {
+ public init(value: ID, selection: ObservableList) {
self.value = value
- _selection = selection
+ self.selection = selection
}
public var body: some View {
@@ -44,7 +45,7 @@ public struct FavoriteToggle: View where ID: Hashable {
if selection.contains(value) {
selection.remove(value)
} else {
- selection.insert(value)
+ selection.add(value)
}
} label: {
ThemeImage(selection.contains(value) ? .favoriteOn : .favoriteOff)