Fetch full profiles from Core Data (#258)

* Fetch full profiles

* Manage full profiles in organizer
This commit is contained in:
Davide De Rosa 2023-03-16 16:49:09 +01:00 committed by GitHub
parent 17b01a4dbc
commit 44ccd21536
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 57 additions and 58 deletions

View File

@ -68,7 +68,7 @@ extension OrganizerView {
} }
private var profilesView: some View { private var profilesView: some View {
ForEach(sortedHeaders, content: profileRow(forHeader:)) ForEach(sortedProfiles, content: profileRow(forProfile:))
.onDelete(perform: removeProfiles) .onDelete(perform: removeProfiles)
} }
@ -79,25 +79,25 @@ extension OrganizerView {
} }
} }
private func profileRow(forHeader header: Profile.Header) -> some View { private func profileRow(forProfile profile: Profile) -> some View {
NavigationLink(tag: header.id, selection: $profileManager.currentProfileId) { NavigationLink(tag: profile.id, selection: $profileManager.currentProfileId) {
ProfileView() ProfileView()
} label: { } label: {
profileLabel(forHeader: header) profileLabel(forProfile: profile)
}.contextMenu { }.contextMenu {
ProfileContextMenu(header: header) ProfileContextMenu(header: profile.header)
} }
} }
private func profileLabel(forHeader header: Profile.Header) -> some View { private func profileLabel(forProfile profile: Profile) -> some View {
ProfileRow( ProfileRow(
header: header, profile: profile,
isActiveProfile: profileManager.isActiveProfile(header.id) isActiveProfile: profileManager.isActiveProfile(profile.id)
) )
} }
private var sortedHeaders: [Profile.Header] { private var sortedProfiles: [Profile] {
profileManager.headers profileManager.profiles
.sorted() .sorted()
// .sorted { // .sorted {
// if profileManager.isActiveProfile($0.id) { // if profileManager.isActiveProfile($0.id) {
@ -111,7 +111,7 @@ extension OrganizerView {
} }
private func removeProfiles(at offsets: IndexSet) { private func removeProfiles(at offsets: IndexSet) {
let currentHeaders = sortedHeaders let currentHeaders = sortedProfiles
var toDelete: [UUID] = [] var toDelete: [UUID] = []
offsets.forEach { offsets.forEach {
toDelete.append(currentHeaders[$0].id) toDelete.append(currentHeaders[$0].id)

View File

@ -27,7 +27,7 @@ import SwiftUI
import PassepartoutLibrary import PassepartoutLibrary
struct ProfileRow: View { struct ProfileRow: View {
let header: Profile.Header let profile: Profile
let isActiveProfile: Bool let isActiveProfile: Bool
@ -35,7 +35,7 @@ struct ProfileRow: View {
debugChanges() debugChanges()
return HStack { return HStack {
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text(header.name) Text(profile.header.name)
.font(.headline) .font(.headline)
.themeLongTextStyle() .themeLongTextStyle()
@ -44,7 +44,7 @@ struct ProfileRow: View {
.themeSecondaryTextStyle() .themeSecondaryTextStyle()
} }
Spacer() Spacer()
VPNToggle(profileId: header.id, rateLimit: Constants.RateLimit.vpnToggle) VPNToggle(profileId: profile.id, rateLimit: Constants.RateLimit.vpnToggle)
.labelsHidden() .labelsHidden()
}.padding([.top, .bottom], 10) }.padding([.top, .bottom], 10)
} }

View File

@ -74,6 +74,12 @@ extension ObservableVPNState {
} }
} }
extension Profile: Comparable {
public static func <(lhs: Self, rhs: Self) -> Bool {
lhs.header < rhs.header
}
}
extension Profile.Header: Comparable { extension Profile.Header: Comparable {
public static func <(lhs: Self, rhs: Self) -> Bool { public static func <(lhs: Self, rhs: Self) -> Bool {
lhs.name.lowercased() < rhs.name.lowercased() lhs.name.lowercased() < rhs.name.lowercased()

View File

@ -129,9 +129,3 @@ extension Profile {
header.id == Self.placeholder.id header.id == Self.placeholder.id
} }
} }
extension Profile.Header {
public var isPlaceholder: Bool {
id == Profile.placeholder.id
}
}

View File

@ -28,7 +28,7 @@ import PassepartoutCore
extension ProfileManager { extension ProfileManager {
public var hasProfiles: Bool { public var hasProfiles: Bool {
!headers.isEmpty !profiles.isEmpty
} }
public var activeProfile: Profile? { public var activeProfile: Profile? {

View File

@ -31,15 +31,15 @@ import PassepartoutUtils
public class CoreDataProfileManagerStrategy: ProfileManagerStrategy { public class CoreDataProfileManagerStrategy: ProfileManagerStrategy {
private let profileRepository: ProfileRepository private let profileRepository: ProfileRepository
private let fetchedHeaders: FetchedValueHolder<[UUID: Profile.Header]> private let fetchedProfiles: FetchedValueHolder<[UUID: Profile]>
public init(persistence: Persistence) { public init(persistence: Persistence) {
profileRepository = ProfileRepository(persistence.context) profileRepository = ProfileRepository(persistence.context)
fetchedHeaders = profileRepository.fetchedHeaders() fetchedProfiles = profileRepository.fetchedProfiles()
} }
public var allHeaders: [UUID: Profile.Header] { public var allProfiles: [UUID: Profile] {
fetchedHeaders.value fetchedProfiles.value
} }
public func profiles() -> [Profile] { public func profiles() -> [Profile] {
@ -62,8 +62,8 @@ public class CoreDataProfileManagerStrategy: ProfileManagerStrategy {
profileRepository.removeProfiles(withIds: ids) profileRepository.removeProfiles(withIds: ids)
} }
public func willUpdateProfiles() -> AnyPublisher<[UUID : Profile.Header], Never> { public func willUpdateProfiles() -> AnyPublisher<[UUID : Profile], Never> {
fetchedHeaders.$value fetchedProfiles.$value
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
} }

View File

@ -113,25 +113,25 @@ public final class ProfileManager: ObservableObject {
// MARK: Index // MARK: Index
extension ProfileManager { extension ProfileManager {
private var allHeaders: [UUID: Profile.Header] { private var allProfiles: [UUID: Profile] {
strategy.allHeaders strategy.allProfiles
}
public var headers: [Profile.Header] {
Array(allHeaders.values)
} }
public var profiles: [Profile] { public var profiles: [Profile] {
strategy.profiles() strategy.profiles()
} }
public var headers: [Profile.Header] {
Array(allProfiles.values.map(\.header))
}
public func isExistingProfile(withId id: UUID) -> Bool { public func isExistingProfile(withId id: UUID) -> Bool {
allHeaders[id] != nil allProfiles[id] != nil
} }
public func isExistingProfile(withName name: String) -> Bool { public func isExistingProfile(withName name: String) -> Bool {
allHeaders.contains { allProfiles.contains {
$0.value.name == name $0.value.header.name == name
} }
} }
} }
@ -189,7 +189,7 @@ extension ProfileManager {
pp_log.info("\tDeactivating profile...") pp_log.info("\tDeactivating profile...")
activeProfileId = nil activeProfileId = nil
} }
} else if allHeaders.isEmpty { } else if allProfiles.isEmpty {
pp_log.info("\tActivating first profile...") pp_log.info("\tActivating first profile...")
activeProfileId = profile.id activeProfileId = profile.id
} }
@ -215,7 +215,7 @@ extension ProfileManager {
@available(*, deprecated, message: "only use for testing") @available(*, deprecated, message: "only use for testing")
public func removeAllProfiles() { public func removeAllProfiles() {
let ids = Array(allHeaders.keys) let ids = Array(allProfiles.keys)
removeProfiles(withIds: ids) removeProfiles(withIds: ids)
} }
@ -314,14 +314,14 @@ extension ProfileManager {
}.store(in: &cancellables) }.store(in: &cancellables)
} }
private func willUpdateProfiles(_ newHeaders: [UUID: Profile.Header]) { private func willUpdateProfiles(_ newProfiles: [UUID: Profile]) {
pp_log.debug("Profiles updated: \(newHeaders)") pp_log.debug("Profiles updated: \(newProfiles.values.map(\.header))")
defer { defer {
objectWillChange.send() objectWillChange.send()
} }
// IMPORTANT: invalidate current profile if deleted // IMPORTANT: invalidate current profile if deleted
if !currentProfile.value.isPlaceholder && !newHeaders.keys.contains(currentProfile.value.id) { if !currentProfile.value.isPlaceholder && !newProfiles.keys.contains(currentProfile.value.id) {
pp_log.info("\tCurrent profile deleted, invalidating...") pp_log.info("\tCurrent profile deleted, invalidating...")
currentProfile.value = .placeholder currentProfile.value = .placeholder
} }
@ -332,7 +332,7 @@ extension ProfileManager {
currentProfile.value = newProfile currentProfile.value = newProfile
} }
if let activeProfileId = activeProfileId, !newHeaders.keys.contains(activeProfileId) { if let activeProfileId = activeProfileId, !newProfiles.keys.contains(activeProfileId) {
pp_log.info("\tActive profile was deleted") pp_log.info("\tActive profile was deleted")
self.activeProfileId = nil self.activeProfileId = nil
} }
@ -342,12 +342,12 @@ extension ProfileManager {
// IMPORTANT: defer task to avoid recursive saves (is non-main thread an issue?) // IMPORTANT: defer task to avoid recursive saves (is non-main thread an issue?)
// FIXME: Core Data, not sure about this workaround // FIXME: Core Data, not sure about this workaround
Task { Task {
fixDuplicateNames(in: newHeaders) fixDuplicateNames(in: newProfiles)
} }
} }
private func fixDuplicateNames(in newHeaders: [UUID: Profile.Header]) { private func fixDuplicateNames(in newProfiles: [UUID: Profile]) {
var allNames = newHeaders.values.map(\.name) var allNames = newProfiles.values.map(\.header.name)
let distinctNames = Set(allNames) let distinctNames = Set(allNames)
distinctNames.forEach { distinctNames.forEach {
guard let i = allNames.firstIndex(of: $0) else { guard let i = allNames.firstIndex(of: $0) else {
@ -364,9 +364,11 @@ extension ProfileManager {
var renamedProfiles: [Profile] = [] var renamedProfiles: [Profile] = []
duplicates.forEach { name in duplicates.forEach { name in
let headers = newHeaders.values.filter { let headers = newProfiles.values
$0.name == name .map(\.header)
} .filter {
$0.name == name
}
guard headers.count > 1 else { guard headers.count > 1 else {
assertionFailure("Name '\(name)' marked as duplicate but headers.count not > 1") assertionFailure("Name '\(name)' marked as duplicate but headers.count not > 1")
return return

View File

@ -28,7 +28,7 @@ import Combine
import PassepartoutCore import PassepartoutCore
public protocol ProfileManagerStrategy { public protocol ProfileManagerStrategy {
var allHeaders: [UUID: Profile.Header] { get } var allProfiles: [UUID: Profile] { get }
func profiles() -> [Profile] func profiles() -> [Profile]
@ -38,7 +38,7 @@ public protocol ProfileManagerStrategy {
func removeProfiles(withIds ids: [UUID]) func removeProfiles(withIds ids: [UUID])
func willUpdateProfiles() -> AnyPublisher<[UUID: Profile.Header], Never> func willUpdateProfiles() -> AnyPublisher<[UUID: Profile], Never>
} }
extension ProfileManagerStrategy { extension ProfileManagerStrategy {

View File

@ -35,29 +35,26 @@ class ProfileRepository: Repository {
self.context = context self.context = context
} }
func fetchedHeaders() -> FetchedValueHolder<[UUID: Profile.Header]> { func fetchedProfiles() -> FetchedValueHolder<[UUID: Profile]> {
let request: NSFetchRequest<NSFetchRequestResult> = CDProfile.fetchRequest() let request: NSFetchRequest<NSFetchRequestResult> = CDProfile.fetchRequest()
request.sortDescriptors = [ request.sortDescriptors = [
.init(keyPath: \CDProfile.lastUpdate, ascending: true) .init(keyPath: \CDProfile.lastUpdate, ascending: true)
] ]
request.propertiesToFetch = [ request.propertiesToFetch = [
"uuid", "json"
"lastUpdate",
"name",
"providerName"
] ]
return .init( return .init(
context: context, context: context,
request: request, request: request,
mapping: { mapping: {
$0.reduce(into: [UUID: Profile.Header]()) { $0.reduce(into: [UUID: Profile]()) {
guard let dto = $1 as? CDProfile else { guard let dto = $1 as? CDProfile else {
return return
} }
guard let header = ProfileHeaderMapper.toModel(dto) else { guard let profile = try? ProfileMapper.toModel(dto) else {
return return
} }
$0[header.id] = header $0[profile.id] = profile
} }
}, },
initial: [:] initial: [:]