Migrate provider favorites to Core Data entities (#997)
Rather than a Codable array.
This commit is contained in:
parent
c7bba033fe
commit
2b3c28832b
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@objc(CDFavoriteServer)
|
||||
final class CDFavoriteServer: NSManagedObject {
|
||||
@nonobjc static func fetchRequest() -> NSFetchRequest<CDFavoriteServer> {
|
||||
NSFetchRequest<CDFavoriteServer>(entityName: "CDFavoriteServer")
|
||||
}
|
||||
|
||||
@NSManaged var serverId: String?
|
||||
@NSManaged var providerPreferences: CDProviderPreferencesV3?
|
||||
}
|
|
@ -35,4 +35,5 @@ final class CDProviderPreferencesV3: NSManagedObject {
|
|||
@NSManaged var providerId: String?
|
||||
@NSManaged var lastUpdate: Date?
|
||||
@NSManaged var favoriteServerIds: Data?
|
||||
@NSManaged var favoriteServers: Set<CDFavoriteServer>?
|
||||
}
|
||||
|
|
|
@ -29,15 +29,6 @@ import Foundation
|
|||
import PassepartoutKit
|
||||
|
||||
struct DomainMapper {
|
||||
func excludedEndpoints(from entities: Set<CDExcludedEndpoint>?) -> Set<ExtendedEndpoint> {
|
||||
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<ExtendedEndpoint>) -> Set<CDExcludedEndpoint> {
|
||||
Set(endpoints.map(cdExcludedEndpoint(from:)))
|
||||
func cdFavoriteServer(from serverId: String) -> CDFavoriteServer {
|
||||
let cdFavorite = CDFavoriteServer(context: context)
|
||||
cdFavorite.serverId = serverId
|
||||
return cdFavorite
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
<attribute name="endpoint" optional="YES" attributeType="String"/>
|
||||
<relationship name="modulePreferences" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CDModulePreferencesV3" inverseName="excludedEndpoints" inverseEntity="CDModulePreferencesV3"/>
|
||||
</entity>
|
||||
<entity name="CDFavoriteServer" representedClassName="CDFavoriteServer" syncable="YES">
|
||||
<attribute name="serverId" optional="YES" attributeType="String"/>
|
||||
<relationship name="providerPreferences" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CDProviderPreferencesV3" inverseName="favoriteServers" inverseEntity="CDProviderPreferencesV3"/>
|
||||
</entity>
|
||||
<entity name="CDModulePreferencesV3" representedClassName="CDModulePreferencesV3" syncable="YES">
|
||||
<attribute name="lastUpdate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="moduleId" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
|
@ -13,5 +17,6 @@
|
|||
<attribute name="favoriteServerIds" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="lastUpdate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="providerId" optional="YES" attributeType="String"/>
|
||||
<relationship name="favoriteServers" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="CDFavoriteServer" inverseName="providerPreferences" inverseEntity="CDFavoriteServer"/>
|
||||
</entity>
|
||||
</model>
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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<String>.self, from: favoriteServerIds)
|
||||
var favoriteServers: Set<CDFavoriteServer> = []
|
||||
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,29 +85,38 @@ private final class CDProviderPreferencesRepositoryV3: ProviderPreferencesReposi
|
|||
}
|
||||
}
|
||||
|
||||
var favoriteServers: Set<String> {
|
||||
get {
|
||||
do {
|
||||
return try context.performAndWait {
|
||||
guard let data = entity.favoriteServerIds else {
|
||||
return []
|
||||
}
|
||||
return try JSONDecoder().decode(Set<String>.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)
|
||||
|
||||
func addFavoriteServer(_ serverId: String) {
|
||||
context.performAndWait {
|
||||
guard entity.favoriteServers?.contains(where: {
|
||||
$0.serverId == serverId
|
||||
}) != true else {
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to set favoriteServers: \(error)")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func save() throws {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -151,7 +151,7 @@ private extension VPNProviderServerView.ContentView {
|
|||
Spacer()
|
||||
FavoriteToggle(
|
||||
value: server.serverId,
|
||||
selection: $providerPreferences.favoriteServers
|
||||
selection: providerPreferences.favoriteServers()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String> = []
|
||||
func isFavoriteServer(_ serverId: String) -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
func addFavoriteServer(_ serverId: String) {
|
||||
}
|
||||
|
||||
func removeFavoriteServer(_ serverId: String) {
|
||||
}
|
||||
|
||||
func save() throws {
|
||||
}
|
||||
|
|
|
@ -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,14 +37,28 @@ public final class ProviderPreferences: ObservableObject {
|
|||
self.repository = repository
|
||||
}
|
||||
|
||||
public var favoriteServers: Set<String> {
|
||||
get {
|
||||
repository?.favoriteServers ?? []
|
||||
public func favoriteServers() -> ObservableList<String> {
|
||||
ObservableList { [weak self] in
|
||||
self?.isFavoriteServer($0) ?? false
|
||||
} add: { [weak self] in
|
||||
self?.addFavoriteServer($0)
|
||||
} remove: { [weak self] in
|
||||
self?.removeFavoriteServer($0)
|
||||
}
|
||||
set {
|
||||
}
|
||||
|
||||
public func isFavoriteServer(_ serverId: String) -> Bool {
|
||||
repository?.isFavoriteServer(serverId) ?? false
|
||||
}
|
||||
|
||||
public func addFavoriteServer(_ serverId: String) {
|
||||
objectWillChange.send()
|
||||
repository?.favoriteServers = newValue
|
||||
repository?.addFavoriteServer(serverId)
|
||||
}
|
||||
|
||||
public func removeFavoriteServer(_ serverId: String) {
|
||||
objectWillChange.send()
|
||||
repository?.removeFavoriteServer(serverId)
|
||||
}
|
||||
|
||||
public func save() throws {
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
@MainActor
|
||||
public protocol ModulePreferencesRepository {
|
||||
func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool
|
||||
|
||||
|
|
|
@ -26,8 +26,13 @@
|
|||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
@MainActor
|
||||
public protocol ProviderPreferencesRepository {
|
||||
var favoriteServers: Set<String> { get set }
|
||||
func isFavoriteServer(_ serverId: String) -> Bool
|
||||
|
||||
func addFavoriteServer(_ serverId: String)
|
||||
|
||||
func removeFavoriteServer(_ serverId: String)
|
||||
|
||||
func save() throws
|
||||
}
|
||||
|
|
|
@ -23,20 +23,21 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonUtils
|
||||
import SwiftUI
|
||||
|
||||
public struct FavoriteToggle<ID>: View where ID: Hashable {
|
||||
private let value: ID
|
||||
|
||||
@Binding
|
||||
private var selection: Set<ID>
|
||||
@ObservedObject
|
||||
private var selection: ObservableList<ID>
|
||||
|
||||
@State
|
||||
private var hover: ID?
|
||||
|
||||
public init(value: ID, selection: Binding<Set<ID>>) {
|
||||
public init(value: ID, selection: ObservableList<ID>) {
|
||||
self.value = value
|
||||
_selection = selection
|
||||
self.selection = selection
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
|
@ -44,7 +45,7 @@ public struct FavoriteToggle<ID>: 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)
|
||||
|
|
Loading…
Reference in New Issue