Bind navigation to ProfileManager directly
- Do the profile loading inside the model - Allow setting current profile to a transient profile - Check .placeholder before saving current profile XXX: avoid loading active profile on iPad portrait.
This commit is contained in:
parent
7036ca5f41
commit
943bce5515
|
@ -139,12 +139,8 @@ class AppContext {
|
|||
profileManager.observeUpdates()
|
||||
vpnManager.observeUpdates()
|
||||
|
||||
if let activeProfileId = appManager.activeProfileId, profileManager.setActiveProfileId(activeProfileId) {
|
||||
do {
|
||||
try profileManager.loadCurrentProfile(withId: activeProfileId)
|
||||
} catch {
|
||||
pp_log.warning("Unable to load active profile: \(error)")
|
||||
}
|
||||
if let activeProfileId = appManager.activeProfileId {
|
||||
profileManager.setActiveProfileId(activeProfileId)
|
||||
}
|
||||
|
||||
// app
|
||||
|
|
|
@ -35,32 +35,6 @@ extension OrganizerView {
|
|||
// just to observe changes in profiles eligibility
|
||||
@ObservedObject private var productManager: ProductManager
|
||||
|
||||
@State private var isFirstLaunch = true
|
||||
|
||||
@State private var presentedProfileId: UUID?
|
||||
|
||||
private var presentedAndLoadedProfileId: Binding<UUID?> {
|
||||
.init {
|
||||
presentedProfileId
|
||||
} set: {
|
||||
guard $0 != presentedProfileId else {
|
||||
return
|
||||
}
|
||||
guard let id = $0 else {
|
||||
presentedProfileId = nil
|
||||
return
|
||||
}
|
||||
presentedProfileId = id
|
||||
|
||||
// load profile contextually with navigation
|
||||
do {
|
||||
try profileManager.loadCurrentProfile(withId: id)
|
||||
} catch {
|
||||
pp_log.error("Unable to load profile: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
profileManager = .shared
|
||||
providerManager = .shared
|
||||
|
@ -74,15 +48,10 @@ extension OrganizerView {
|
|||
if !profileManager.hasProfiles {
|
||||
emptyView
|
||||
}
|
||||
}
|
||||
|
||||
// events
|
||||
.onAppear {
|
||||
}.onAppear {
|
||||
performMigrationsIfNeeded()
|
||||
}.onChange(of: profileManager.headers) {
|
||||
dismissSelectionIfDeleted(headers: $0)
|
||||
}.onReceive(profileManager.didCreateProfile) {
|
||||
presentedAndLoadedProfileId.wrappedValue = $0.id
|
||||
profileManager.currentProfileId = $0.id
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,14 +85,12 @@ extension OrganizerView {
|
|||
}
|
||||
|
||||
private func profileRow(forHeader header: Profile.Header) -> some View {
|
||||
NavigationLink(tag: header.id, selection: presentedAndLoadedProfileId) {
|
||||
NavigationLink(tag: header.id, selection: $profileManager.currentProfileId) {
|
||||
ProfileView()
|
||||
} label: {
|
||||
profileLabel(forHeader: header)
|
||||
}.contextMenu {
|
||||
profileMenu(forHeader: header)
|
||||
}.onAppear {
|
||||
presentIfActiveProfile(header.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,46 +122,12 @@ extension OrganizerView {
|
|||
}
|
||||
}
|
||||
|
||||
private func presentIfActiveProfile(_ id: UUID) {
|
||||
guard id == profileManager.activeProfileId else {
|
||||
return
|
||||
}
|
||||
presentActiveProfile()
|
||||
}
|
||||
|
||||
private func presentActiveProfile() {
|
||||
guard isFirstLaunch else {
|
||||
return
|
||||
}
|
||||
isFirstLaunch = false
|
||||
|
||||
guard let activeProfileId = profileManager.activeProfileId else {
|
||||
return
|
||||
}
|
||||
|
||||
// FIXME: iPad portrait/compact, preselecting profile on launch adds ProfileView() twice
|
||||
// can notice becase "Back" needs to be tapped twice to show sidebar
|
||||
if themeIdiom != .pad {
|
||||
presentedProfileId = activeProfileId
|
||||
}
|
||||
}
|
||||
|
||||
private func removeProfiles(at offsets: IndexSet) {
|
||||
let currentHeaders = sortedHeaders
|
||||
var toDelete: [UUID] = []
|
||||
offsets.forEach {
|
||||
toDelete.append(currentHeaders[$0].id)
|
||||
}
|
||||
removeProfiles(withIds: toDelete)
|
||||
}
|
||||
|
||||
private func removeProfiles(withIds toDelete: [UUID]) {
|
||||
|
||||
// clear selection before removal to avoid triggering a bogus navigation push
|
||||
if toDelete.contains(profileManager.currentProfile.value.id) {
|
||||
presentedProfileId = nil
|
||||
}
|
||||
|
||||
withAnimation {
|
||||
profileManager.removeProfiles(withIds: toDelete)
|
||||
}
|
||||
|
@ -205,11 +138,5 @@ extension OrganizerView {
|
|||
await AppManager.shared.doMigrations(profileManager)
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissSelectionIfDeleted(headers: [Profile.Header]) {
|
||||
if let _ = presentedProfileId, !profileManager.isCurrentProfileExisting() {
|
||||
presentedProfileId = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,8 @@ extension OrganizerView {
|
|||
|
||||
@Binding private var didHandleSubreddit: Bool
|
||||
|
||||
@State private var isFirstLaunch = true
|
||||
|
||||
init(alertType: Binding<AlertType?>, didHandleSubreddit: Binding<Bool>) {
|
||||
profileManager = .shared
|
||||
vpnManager = .shared
|
||||
|
@ -58,8 +60,26 @@ extension OrganizerView {
|
|||
}
|
||||
|
||||
private func onAppear() {
|
||||
if !didHandleSubreddit {
|
||||
guard didHandleSubreddit else {
|
||||
alertType = .subscribeReddit
|
||||
return
|
||||
}
|
||||
|
||||
//
|
||||
// FIXME: iPad portrait/compact, loading current profile adds ProfileView() twice
|
||||
//
|
||||
// - from MainView
|
||||
// - from NavigationLink destination in OrganizerView
|
||||
//
|
||||
// can notice becase "Back" needs to be tapped twice to show sidebar
|
||||
// workaround: set active profile but do not load as current (prevents NavigationLink activation)
|
||||
//
|
||||
guard isFirstLaunch else {
|
||||
return
|
||||
}
|
||||
isFirstLaunch = false
|
||||
if !isiPadPortrait, let activeProfileId = profileManager.activeProfileId {
|
||||
profileManager.currentProfileId = activeProfileId
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,5 +103,10 @@ extension OrganizerView {
|
|||
private func persist() {
|
||||
profileManager.persist()
|
||||
}
|
||||
|
||||
private var isiPadPortrait: Bool {
|
||||
let device: UIDevice = .current
|
||||
return device.userInterfaceIdiom == .pad && device.orientation.isPortrait
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -212,11 +212,7 @@ extension ProfileView {
|
|||
return
|
||||
}
|
||||
if switchCurrentProfile {
|
||||
do {
|
||||
try profileManager.loadCurrentProfile(withId: copy.id)
|
||||
} catch {
|
||||
pp_log.warning("Unable to load profile duplicate: \(error)")
|
||||
}
|
||||
profileManager.currentProfileId = copy.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ extension VPNManager {
|
|||
}
|
||||
|
||||
public func connect(with profileId: UUID) async throws {
|
||||
let result = try profileManager.profileEx(withId: profileId)
|
||||
let result = try profileManager.liveProfileEx(withId: profileId)
|
||||
let profile = result.profile
|
||||
guard !profileManager.isActiveProfile(profileId) ||
|
||||
currentState.vpnStatus != .connected else {
|
||||
|
@ -64,7 +64,7 @@ extension VPNManager {
|
|||
}
|
||||
|
||||
public func connect(with profileId: UUID, toServer newServerId: String) async throws {
|
||||
let result = try profileManager.profileEx(withId: profileId)
|
||||
let result = try profileManager.liveProfileEx(withId: profileId)
|
||||
var profile = result.profile
|
||||
guard profile.isProvider else {
|
||||
assertionFailure("Profile \(profile.logDescription) is not a provider")
|
||||
|
|
|
@ -50,8 +50,21 @@ public class ProfileManager: ObservableObject {
|
|||
// MARK: Observables
|
||||
|
||||
@Published public private(set) var activeProfileId: UUID? {
|
||||
didSet {
|
||||
pp_log.debug("Active profile updated: \(activeProfileId?.uuidString ?? "nil")")
|
||||
willSet {
|
||||
pp_log.debug("Setting active profile: \(activeProfileId?.uuidString ?? "nil")")
|
||||
}
|
||||
}
|
||||
|
||||
@Published public var currentProfileId: UUID? {
|
||||
willSet {
|
||||
pp_log.debug("Setting current profile: \(newValue?.uuidString ?? "nil")")
|
||||
guard let id = newValue else {
|
||||
return
|
||||
}
|
||||
guard let profile = liveProfile(withId: id) else {
|
||||
return
|
||||
}
|
||||
setCurrentProfile(profile)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,13 +92,12 @@ public class ProfileManager: ObservableObject {
|
|||
currentProfile = ObservableProfile()
|
||||
}
|
||||
|
||||
public func setActiveProfileId(_ id: UUID) -> Bool {
|
||||
public func setActiveProfileId(_ id: UUID) {
|
||||
guard isExistingProfile(withId: id) else {
|
||||
pp_log.warning("Active profile \(id) does not exist, ignoring")
|
||||
return false
|
||||
return
|
||||
}
|
||||
activeProfileId = id
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,15 +151,15 @@ extension ProfileManager {
|
|||
guard let id = activeProfileId else {
|
||||
return nil
|
||||
}
|
||||
return profile(withId: id)
|
||||
return liveProfile(withId: id)
|
||||
}
|
||||
|
||||
public func activateProfile(_ profile: Profile) {
|
||||
saveProfile(profile, isActive: true)
|
||||
}
|
||||
|
||||
public func profileEx(withId id: UUID) throws -> ProfileEx {
|
||||
guard let profile = profile(withId: id) else {
|
||||
public func liveProfileEx(withId id: UUID) throws -> ProfileEx {
|
||||
guard let profile = liveProfile(withId: id) else {
|
||||
pp_log.error("Profile not found: \(id)")
|
||||
throw PassepartoutError.missingProfile
|
||||
}
|
||||
|
@ -155,7 +167,7 @@ extension ProfileManager {
|
|||
return (profile, isProfileReady(profile))
|
||||
}
|
||||
|
||||
private func profile(withId id: UUID) -> Profile? {
|
||||
private func liveProfile(withId id: UUID) -> Profile? {
|
||||
pp_log.debug("Searching profile \(id)")
|
||||
|
||||
// IMPORTANT: fetch live copy first (see intents)
|
||||
|
@ -228,58 +240,75 @@ extension ProfileManager {
|
|||
}
|
||||
|
||||
public func duplicateProfile(withId id: UUID) -> Profile? {
|
||||
guard let source = profile(withId: id) else {
|
||||
guard let source = liveProfile(withId: id) else {
|
||||
return nil
|
||||
}
|
||||
let copy = source
|
||||
.withNewId()
|
||||
.renamedUniquely(withLastUpdate: false)
|
||||
|
||||
|
||||
saveProfile(copy, isActive: nil)
|
||||
return copy
|
||||
}
|
||||
|
||||
public func persist() {
|
||||
pp_log.info("Persisting profiles")
|
||||
saveCurrentProfile()
|
||||
pp_log.info("Persisting pending profiles")
|
||||
if !currentProfile.value.isPlaceholder {
|
||||
saveProfile(currentProfile.value, isActive: nil, updateIfCurrent: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Observation
|
||||
|
||||
extension ProfileManager {
|
||||
public func loadCurrentProfile(withId id: UUID) throws {
|
||||
private func setCurrentProfile(_ profile: Profile) {
|
||||
guard !currentProfile.isLoading else {
|
||||
pp_log.warning("Already loading another profile")
|
||||
return
|
||||
}
|
||||
guard id != currentProfile.value.id else {
|
||||
pp_log.debug("Profile \(id) is already current profile")
|
||||
guard profile.id != currentProfile.value.id else {
|
||||
pp_log.debug("Profile \(profile.logDescription) is already current profile")
|
||||
return
|
||||
}
|
||||
|
||||
pp_log.info("Set current profile: \(profile.logDescription)")
|
||||
|
||||
//
|
||||
// IMPORTANT: this method is called on app launch if there is an active profile, which
|
||||
// means that carelessly calling .saveProfiles() may trigger an unnecessary
|
||||
// willUpdateProfiles() and a potential animation in subscribers (e.g. OrganizerView)
|
||||
//
|
||||
// current profile, when set on launch, is never fetched from pendingProfiles, so we take
|
||||
// care of checking that to avoid an undesired save
|
||||
//
|
||||
var profilesToSave: [Profile] = []
|
||||
if isExistingProfile(withId: currentProfile.value.id) {
|
||||
pp_log.info("Committing changes of former current profile \(currentProfile.value.logDescription)")
|
||||
saveCurrentProfile()
|
||||
pp_log.info("Defer saving of former current profile \(currentProfile.value.logDescription)")
|
||||
profilesToSave.append(currentProfile.value)
|
||||
}
|
||||
if let _ = pendingProfiles[profile.id] {
|
||||
pp_log.info("Defer saving of transient current profile \(profile.logDescription)")
|
||||
profilesToSave.append(profile)
|
||||
}
|
||||
defer {
|
||||
if !profilesToSave.isEmpty {
|
||||
strategy.saveProfiles(profilesToSave)
|
||||
}
|
||||
}
|
||||
|
||||
let result = try profileEx(withId: id)
|
||||
pp_log.info("Current profile: \(result.profile.logDescription)")
|
||||
if result.isReady {
|
||||
currentProfile.value = result.profile
|
||||
if isProfileReady(profile) {
|
||||
currentProfile.value = profile
|
||||
} else {
|
||||
currentProfile.isLoading = true
|
||||
Task {
|
||||
try await makeProfileReady(result.profile)
|
||||
currentProfile.value = result.profile
|
||||
try await makeProfileReady(profile)
|
||||
currentProfile.value = profile
|
||||
currentProfile.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func isCurrentProfileExisting() -> Bool {
|
||||
isExistingProfile(withId: currentProfile.value.id)
|
||||
}
|
||||
|
||||
|
||||
public func isCurrentProfileActive() -> Bool {
|
||||
currentProfile.value.id == activeProfileId
|
||||
}
|
||||
|
@ -291,10 +320,6 @@ extension ProfileManager {
|
|||
public func activateCurrentProfile() {
|
||||
saveProfile(currentProfile.value, isActive: true, updateIfCurrent: false)
|
||||
}
|
||||
|
||||
public func saveCurrentProfile() {
|
||||
saveProfile(currentProfile.value, isActive: nil, updateIfCurrent: false)
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileManager {
|
||||
|
@ -366,7 +391,7 @@ extension ProfileManager {
|
|||
headers.forEach { dupHeader in
|
||||
let uniqueHeader = dupHeader.renamedUniquely(withLastUpdate: true)
|
||||
pp_log.debug("Renaming duplicate profile \(dupHeader.logDescription) to \(uniqueHeader.logDescription)")
|
||||
guard var uniqueProfile = profile(withId: uniqueHeader.id) else {
|
||||
guard var uniqueProfile = liveProfile(withId: uniqueHeader.id) else {
|
||||
pp_log.warning("Skipping profile \(dupHeader.logDescription) renaming, not found")
|
||||
return
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue