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

View File

@ -27,7 +27,7 @@ import SwiftUI
import PassepartoutLibrary
struct ProfileRow: View {
let header: Profile.Header
let profile: Profile
let isActiveProfile: Bool
@ -35,7 +35,7 @@ struct ProfileRow: View {
debugChanges()
return HStack {
VStack(alignment: .leading, spacing: 5) {
Text(header.name)
Text(profile.header.name)
.font(.headline)
.themeLongTextStyle()
@ -44,7 +44,7 @@ struct ProfileRow: View {
.themeSecondaryTextStyle()
}
Spacer()
VPNToggle(profileId: header.id, rateLimit: Constants.RateLimit.vpnToggle)
VPNToggle(profileId: profile.id, rateLimit: Constants.RateLimit.vpnToggle)
.labelsHidden()
}.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 {
public static func <(lhs: Self, rhs: Self) -> Bool {
lhs.name.lowercased() < rhs.name.lowercased()

View File

@ -129,9 +129,3 @@ extension Profile {
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 {
public var hasProfiles: Bool {
!headers.isEmpty
!profiles.isEmpty
}
public var activeProfile: Profile? {

View File

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

View File

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

View File

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

View File

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