mirror of
https://github.com/passepartoutvpn/passepartout-apple.git
synced 2025-01-19 06:59:10 +00:00
Rewrite ProfileView as a view of currentProfile
Do not load profile in View, instead: - Load active profile on app launch - Load selected profile on organizer selection
This commit is contained in:
parent
8838e9d130
commit
2432f0d97a
@ -132,7 +132,13 @@ class AppContext {
|
|||||||
profileManager.availabilityFilter = {
|
profileManager.availabilityFilter = {
|
||||||
self.isEligibleProfile(withHeader: $0)
|
self.isEligibleProfile(withHeader: $0)
|
||||||
}
|
}
|
||||||
profileManager.activeProfileId = appManager.activeProfileId
|
if let activeProfileId = appManager.activeProfileId {
|
||||||
|
do {
|
||||||
|
try profileManager.loadActiveProfile(withId: activeProfileId)
|
||||||
|
} catch {
|
||||||
|
pp_log.warning("Unable to load active profile: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
providerManager.rateLimitMilliseconds = Constants.RateLimit.providerManager
|
providerManager.rateLimitMilliseconds = Constants.RateLimit.providerManager
|
||||||
vpnManager.rateLimitMilliseconds = Constants.RateLimit.vpnManager
|
vpnManager.rateLimitMilliseconds = Constants.RateLimit.vpnManager
|
||||||
vpnManager.isOnDemandRulesSupported = {
|
vpnManager.isOnDemandRulesSupported = {
|
||||||
|
@ -31,7 +31,7 @@ struct MainView: View {
|
|||||||
OrganizerView()
|
OrganizerView()
|
||||||
.themePrimaryView()
|
.themePrimaryView()
|
||||||
|
|
||||||
ProfileView(header: nil)
|
ProfileView()
|
||||||
}.themeGlobal()
|
}.themeGlobal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ extension OrganizerView {
|
|||||||
|
|
||||||
@State private var isFirstLaunch = true
|
@State private var isFirstLaunch = true
|
||||||
|
|
||||||
@State private var selectedProfileId: UUID?
|
@State private var isPresentingProfile = false
|
||||||
|
|
||||||
init(alertType: Binding<AlertType?>) {
|
init(alertType: Binding<AlertType?>) {
|
||||||
appManager = .shared
|
appManager = .shared
|
||||||
@ -53,7 +53,11 @@ extension OrganizerView {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
debugChanges()
|
debugChanges()
|
||||||
return Group {
|
return ZStack {
|
||||||
|
NavigationLink("", isActive: $isPresentingProfile) {
|
||||||
|
ProfileView()
|
||||||
|
}.onAppear(perform: presentActiveProfile)
|
||||||
|
|
||||||
mainView
|
mainView
|
||||||
if profileManager.headers.isEmpty {
|
if profileManager.headers.isEmpty {
|
||||||
emptyView
|
emptyView
|
||||||
@ -66,14 +70,14 @@ extension OrganizerView {
|
|||||||
|
|
||||||
// from AddProfileView
|
// from AddProfileView
|
||||||
.onReceive(profileManager.didCreateProfile) {
|
.onReceive(profileManager.didCreateProfile) {
|
||||||
selectedProfileId = $0.id
|
presentProfile(withId: $0.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mainView: some View {
|
private var mainView: some View {
|
||||||
List {
|
List {
|
||||||
Section {
|
Section {
|
||||||
ForEach(sortedHeaders, content: navigationLink(forHeader:))
|
ForEach(sortedHeaders, content: profileButton(forHeader:))
|
||||||
.onDelete(perform: removeProfiles)
|
.onDelete(perform: removeProfiles)
|
||||||
}
|
}
|
||||||
}.animation(.default, value: profileManager.headers)
|
}.animation(.default, value: profileManager.headers)
|
||||||
@ -86,27 +90,14 @@ extension OrganizerView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func navigationLink(forHeader header: Profile.Header) -> some View {
|
private func profileButton(forHeader header: Profile.Header) -> some View {
|
||||||
NavigationLink(tag: header.id, selection: $selectedProfileId) {
|
Button {
|
||||||
ProfileView(header: header)
|
presentProfile(withId: header.id)
|
||||||
} label: {
|
} label: {
|
||||||
ProfileHeaderRow(
|
ProfileHeaderRow(
|
||||||
header: header,
|
header: header,
|
||||||
isActive: profileManager.isActiveProfile(header.id)
|
isActive: profileManager.isActiveProfile(header.id)
|
||||||
)
|
)
|
||||||
}.onAppear {
|
|
||||||
preselectIfActiveProfile(header.id)
|
|
||||||
|
|
||||||
// XXX: iOS 14 bug, if selectedProfileId is set before its NavigationLink
|
|
||||||
// has appeared, the NavigationLink will not auto-activate once appeared
|
|
||||||
// enforce activation by clearing and resetting selectedProfileId to its
|
|
||||||
// current value
|
|
||||||
withAnimation {
|
|
||||||
if let tmp = selectedProfileId, tmp == header.id {
|
|
||||||
selectedProfileId = nil
|
|
||||||
selectedProfileId = tmp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -117,22 +108,21 @@ extension OrganizerView.ProfilesList {
|
|||||||
profileManager.headers.sorted()
|
profileManager.headers.sorted()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func preselectIfActiveProfile(_ id: UUID) {
|
private func presentActiveProfile() {
|
||||||
|
guard isFirstLaunch, profileManager.hasActiveProfile else {
|
||||||
// do not push profile if:
|
|
||||||
//
|
|
||||||
// - an alert is active, as it would break navigation
|
|
||||||
// - on iPad, as it's already shown
|
|
||||||
//
|
|
||||||
guard alertType == nil, themeIdiom != .pad, id == profileManager.activeHeader?.id else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard isFirstLaunch else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isFirstLaunch = false
|
isFirstLaunch = false
|
||||||
|
isPresentingProfile = true
|
||||||
selectedProfileId = id
|
}
|
||||||
|
|
||||||
|
private func presentProfile(withId id: UUID) {
|
||||||
|
do {
|
||||||
|
try profileManager.loadCurrentProfile(withId: id, makeReady: true)
|
||||||
|
isPresentingProfile = true
|
||||||
|
} catch {
|
||||||
|
pp_log.error("Unable to load profile: \(error)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func performMigrationsIfNeeded() {
|
private func performMigrationsIfNeeded() {
|
||||||
@ -149,18 +139,16 @@ extension OrganizerView.ProfilesList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// clear selection before removal to avoid triggering a bogus navigation push
|
// clear selection before removal to avoid triggering a bogus navigation push
|
||||||
if let selectedProfileId = selectedProfileId, toDelete.contains(selectedProfileId) {
|
if toDelete.contains(profileManager.currentProfile.value.id) {
|
||||||
self.selectedProfileId = nil
|
isPresentingProfile = false
|
||||||
}
|
}
|
||||||
|
|
||||||
profileManager.removeProfiles(withIds: toDelete)
|
profileManager.removeProfiles(withIds: toDelete)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func dismissSelectionIfDeleted(headers: [Profile.Header]) {
|
private func dismissSelectionIfDeleted(headers: [Profile.Header]) {
|
||||||
if let selectedProfileId = selectedProfileId,
|
if isPresentingProfile, !profileManager.isCurrentProfileExisting() {
|
||||||
!profileManager.isExistingProfile(withId: selectedProfileId) {
|
isPresentingProfile = false
|
||||||
|
|
||||||
self.selectedProfileId = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ extension ProfileView {
|
|||||||
|
|
||||||
@ObservedObject private var currentProfile: ObservableProfile
|
@ObservedObject private var currentProfile: ObservableProfile
|
||||||
|
|
||||||
private let isLoaded: Bool
|
private let isLoading: Bool
|
||||||
|
|
||||||
private var isActiveProfile: Bool {
|
private var isActiveProfile: Bool {
|
||||||
profileManager.isCurrentProfileActive()
|
profileManager.isCurrentProfileActive()
|
||||||
@ -52,7 +52,7 @@ extension ProfileView {
|
|||||||
productManager.isEligible(forFeature: .siriShortcuts)
|
productManager.isEligible(forFeature: .siriShortcuts)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(currentProfile: ObservableProfile, isLoaded: Bool) {
|
init(currentProfile: ObservableProfile, isLoading: Bool) {
|
||||||
appManager = .shared
|
appManager = .shared
|
||||||
profileManager = .shared
|
profileManager = .shared
|
||||||
providerManager = .shared
|
providerManager = .shared
|
||||||
@ -60,11 +60,11 @@ extension ProfileView {
|
|||||||
currentVPNState = .shared
|
currentVPNState = .shared
|
||||||
productManager = .shared
|
productManager = .shared
|
||||||
self.currentProfile = currentProfile
|
self.currentProfile = currentProfile
|
||||||
self.isLoaded = isLoaded
|
self.isLoading = isLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if isLoaded {
|
if !isLoading {
|
||||||
if isActiveProfile {
|
if isActiveProfile {
|
||||||
activeView
|
activeView
|
||||||
} else {
|
} else {
|
||||||
@ -114,7 +114,7 @@ extension ProfileView {
|
|||||||
profileManager.activateCurrentProfile()
|
profileManager.activateCurrentProfile()
|
||||||
|
|
||||||
// IMPORTANT: save immediately to keep in sync with VPN status
|
// IMPORTANT: save immediately to keep in sync with VPN status
|
||||||
appManager.activeProfileId = profileManager.activeProfileId
|
appManager.activeProfileId = profileManager.activeHeader?.id
|
||||||
}
|
}
|
||||||
Task {
|
Task {
|
||||||
await vpnManager.disable()
|
await vpnManager.disable()
|
||||||
|
@ -47,21 +47,18 @@ struct ProfileView: View {
|
|||||||
|
|
||||||
@ObservedObject private var profileManager: ProfileManager
|
@ObservedObject private var profileManager: ProfileManager
|
||||||
|
|
||||||
private let header: Profile.Header
|
private var isLoading: Bool {
|
||||||
|
profileManager.isLoadingCurrentProfile
|
||||||
|
}
|
||||||
|
|
||||||
private var isExisting: Bool {
|
private var isExisting: Bool {
|
||||||
profileManager.isExistingProfile(withId: header.id)
|
profileManager.isCurrentProfileExisting()
|
||||||
}
|
}
|
||||||
|
|
||||||
@State private var modalType: ModalType?
|
@State private var modalType: ModalType?
|
||||||
|
|
||||||
@State private var isLoaded = false
|
init() {
|
||||||
|
profileManager = .shared
|
||||||
init(header: Profile.Header?) {
|
|
||||||
let profileManager: ProfileManager = .shared
|
|
||||||
|
|
||||||
self.profileManager = profileManager
|
|
||||||
self.header = header ?? profileManager.activeHeader ?? Profile.placeholder.header
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -84,22 +81,21 @@ struct ProfileView: View {
|
|||||||
).disabled(!isExisting)
|
).disabled(!isExisting)
|
||||||
}
|
}
|
||||||
}.sheet(item: $modalType, content: presentedModal)
|
}.sheet(item: $modalType, content: presentedModal)
|
||||||
.onAppear(perform: loadProfileIfNeeded)
|
|
||||||
.navigationTitle(title)
|
.navigationTitle(title)
|
||||||
.themeSecondaryView()
|
.themeSecondaryView()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var title: String {
|
private var title: String {
|
||||||
isExisting ? header.name : ""
|
profileManager.currentProfile.name
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mainView: some View {
|
private var mainView: some View {
|
||||||
List {
|
List {
|
||||||
VPNSection(
|
VPNSection(
|
||||||
currentProfile: profileManager.currentProfile,
|
currentProfile: profileManager.currentProfile,
|
||||||
isLoaded: isLoaded
|
isLoading: isLoading
|
||||||
)
|
)
|
||||||
if isLoaded {
|
if !isLoading {
|
||||||
ProviderSection(currentProfile: profileManager.currentProfile)
|
ProviderSection(currentProfile: profileManager.currentProfile)
|
||||||
ConfigurationSection(
|
ConfigurationSection(
|
||||||
currentProfile: profileManager.currentProfile,
|
currentProfile: profileManager.currentProfile,
|
||||||
@ -156,37 +152,6 @@ struct ProfileView: View {
|
|||||||
}.themeGlobal()
|
}.themeGlobal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadProfileIfNeeded() {
|
|
||||||
guard !isLoaded else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard !header.isPlaceholder else {
|
|
||||||
pp_log.debug("ProfileView is a placeholder for WelcomeView, no active profile")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
let result = try profileManager.loadCurrentProfile(withId: header.id)
|
|
||||||
if result.isReady {
|
|
||||||
isLoaded = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
try await profileManager.makeProfileReady(result.profile)
|
|
||||||
withAnimation {
|
|
||||||
isLoaded = true
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
pp_log.error("Profile \(header.id) could not be made ready: \(error)")
|
|
||||||
presentationMode.wrappedValue.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
pp_log.error("Profile \(header.id) could not be loaded: \(error)")
|
|
||||||
presentationMode.wrappedValue.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func presentPaywallTrustedNetworks() {
|
private func presentPaywallTrustedNetworks() {
|
||||||
modalType = .paywallTrustedNetworks
|
modalType = .paywallTrustedNetworks
|
||||||
|
@ -47,7 +47,7 @@ public class ProfileManager: ObservableObject {
|
|||||||
|
|
||||||
public var availabilityFilter: ((Profile.Header) -> Bool)?
|
public var availabilityFilter: ((Profile.Header) -> Bool)?
|
||||||
|
|
||||||
public var activeProfileId: UUID? {
|
private var activeProfileId: UUID? {
|
||||||
willSet {
|
willSet {
|
||||||
willUpdateActiveId.send(newValue)
|
willUpdateActiveId.send(newValue)
|
||||||
}
|
}
|
||||||
@ -58,6 +58,8 @@ public class ProfileManager: ObservableObject {
|
|||||||
|
|
||||||
// MARK: Observables
|
// MARK: Observables
|
||||||
|
|
||||||
|
@Published public private(set) var isLoadingCurrentProfile = false
|
||||||
|
|
||||||
public let currentProfile: ObservableProfile
|
public let currentProfile: ObservableProfile
|
||||||
|
|
||||||
public let willUpdateActiveId = PassthroughSubject<UUID?, Never>()
|
public let willUpdateActiveId = PassthroughSubject<UUID?, Never>()
|
||||||
@ -85,6 +87,15 @@ public class ProfileManager: ObservableObject {
|
|||||||
|
|
||||||
currentProfile = ObservableProfile()
|
currentProfile = ObservableProfile()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func loadActiveProfile(withId id: UUID) throws {
|
||||||
|
guard isExistingProfile(withId: id) else {
|
||||||
|
pp_log.warning("Active profile \(id) does not exist, ignoring")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activeProfileId = id
|
||||||
|
try loadCurrentProfile(withId: id, makeReady: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Index
|
// MARK: Index
|
||||||
@ -104,6 +115,10 @@ extension ProfileManager {
|
|||||||
public var headers: [Profile.Header] {
|
public var headers: [Profile.Header] {
|
||||||
availableHeaders
|
availableHeaders
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var hasActiveProfile: Bool {
|
||||||
|
activeHeader != nil
|
||||||
|
}
|
||||||
|
|
||||||
public var activeHeader: Profile.Header? {
|
public var activeHeader: Profile.Header? {
|
||||||
availableHeaders.first {
|
availableHeaders.first {
|
||||||
@ -232,7 +247,16 @@ extension ProfileManager {
|
|||||||
// MARK: Observation
|
// MARK: Observation
|
||||||
|
|
||||||
extension ProfileManager {
|
extension ProfileManager {
|
||||||
public func loadCurrentProfile(withId id: UUID) throws -> LoadResult {
|
public func loadCurrentProfile(withId id: UUID, makeReady: Bool) throws {
|
||||||
|
guard !isLoadingCurrentProfile else {
|
||||||
|
pp_log.warning("Already loading another profile")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard id != currentProfile.value.id else {
|
||||||
|
pp_log.debug("Profile \(id) is already current profile")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isLoadingCurrentProfile = true
|
||||||
if isExistingProfile(withId: currentProfile.value.id) {
|
if isExistingProfile(withId: currentProfile.value.id) {
|
||||||
pp_log.info("Committing changes of former current profile \(currentProfile.value.logDescription)")
|
pp_log.info("Committing changes of former current profile \(currentProfile.value.logDescription)")
|
||||||
saveCurrentProfile()
|
saveCurrentProfile()
|
||||||
@ -240,14 +264,28 @@ extension ProfileManager {
|
|||||||
do {
|
do {
|
||||||
let result = try loadProfile(withId: id)
|
let result = try loadProfile(withId: id)
|
||||||
pp_log.info("Current profile: \(result.profile.logDescription)")
|
pp_log.info("Current profile: \(result.profile.logDescription)")
|
||||||
currentProfile.value = result.profile
|
|
||||||
return result
|
if !makeReady || result.isReady {
|
||||||
|
currentProfile.value = result.profile
|
||||||
|
isLoadingCurrentProfile = false
|
||||||
|
} else {
|
||||||
|
Task {
|
||||||
|
try await makeProfileReady(result.profile)
|
||||||
|
currentProfile.value = result.profile
|
||||||
|
isLoadingCurrentProfile = false
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
currentProfile.value = .placeholder
|
currentProfile.value = .placeholder
|
||||||
|
isLoadingCurrentProfile = false
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func isCurrentProfileExisting() -> Bool {
|
||||||
|
isExistingProfile(withId: currentProfile.value.id)
|
||||||
|
}
|
||||||
|
|
||||||
public func isCurrentProfileActive() -> Bool {
|
public func isCurrentProfileActive() -> Bool {
|
||||||
currentProfile.value.id == activeProfileId
|
currentProfile.value.id == activeProfileId
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ public class ObservableProfile: ValueHolder, ObservableObject {
|
|||||||
@Published public var value: Profile
|
@Published public var value: Profile
|
||||||
|
|
||||||
public var name: String {
|
public var name: String {
|
||||||
value.header.name
|
!value.header.isPlaceholder ? value.header.name : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
|
Loading…
Reference in New Issue
Block a user