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 {
|
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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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? {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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: [:]
|
||||||
|
|
Loading…
Reference in New Issue