Fetch full profiles from Core Data (#258)
* Fetch full profiles * Manage full profiles in organizer
This commit is contained in:
parent
17b01a4dbc
commit
44ccd21536
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -129,9 +129,3 @@ extension Profile {
|
|||
header.id == Self.placeholder.id
|
||||
}
|
||||
}
|
||||
|
||||
extension Profile.Header {
|
||||
public var isPlaceholder: Bool {
|
||||
id == Profile.placeholder.id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import PassepartoutCore
|
|||
|
||||
extension ProfileManager {
|
||||
public var hasProfiles: Bool {
|
||||
!headers.isEmpty
|
||||
!profiles.isEmpty
|
||||
}
|
||||
|
||||
public var activeProfile: Profile? {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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: [:]
|
||||
|
|
Loading…
Reference in New Issue