// // OrganizerView+Profiles.swift // Passepartout // // Created by Davide De Rosa on 4/2/22. // Copyright (c) 2022 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn // // This file is part of Passepartout. // // Passepartout is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Passepartout is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Passepartout. If not, see . // import SwiftUI import PassepartoutCore extension OrganizerView { struct ProfilesList: View { @ObservedObject private var appManager: AppManager @ObservedObject private var profileManager: ProfileManager @ObservedObject private var providerManager: ProviderManager // just to observe changes in profiles eligibility @ObservedObject private var productManager: ProductManager @Binding private var alertType: AlertType? @State private var isFirstLaunch = true @State private var selectedProfileId: UUID? init(alertType: Binding) { appManager = .shared profileManager = .shared providerManager = .shared productManager = .shared _alertType = alertType } var body: some View { debugChanges() return Group { mainView if profileManager.headers.isEmpty { emptyView } }.onAppear { performMigrationsIfNeeded() }.onChange(of: profileManager.headers) { dismissSelectionIfDeleted(headers: $0) } // from AddProfileView .onReceive(profileManager.didCreateProfile) { selectedProfileId = $0.id } } private var mainView: some View { List { Section { ForEach(sortedHeaders, content: navigationLink(forHeader:)) .onDelete(perform: removeProfiles) } }.animation(.default, value: profileManager.headers) } // FIXME: l10n private var emptyView: some View { VStack { Text("No profiles") .themeInformativeText() } } private func navigationLink(forHeader header: Profile.Header) -> some View { NavigationLink(tag: header.id, selection: $selectedProfileId) { ProfileView(header: header) } label: { if profileManager.isActiveProfile(header.id) { ActiveProfileHeaderRow(header: header) } else { ProfileHeaderRow(header: header) } }.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 } } } } } } extension OrganizerView.ProfilesList { struct ActiveProfileHeaderRow: View { @ObservedObject private var currentVPNState: VPNManager.ObservableState private let header: Profile.Header init(header: Profile.Header) { currentVPNState = .shared self.header = header } var body: some View { debugChanges() return ProfileHeaderRow(header: header) .withTrailingText(statusDescription) } private var statusDescription: String { return currentVPNState.localizedStatusDescription( withErrors: false, withDataCount: false ) } } } extension OrganizerView.ProfilesList { private var sortedHeaders: [Profile.Header] { profileManager.headers.sorted() } private func preselectIfActiveProfile(_ id: UUID) { // 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 } isFirstLaunch = false selectedProfileId = id } private func performMigrationsIfNeeded() { Task { await appManager.doMigrations(profileManager) } } private func removeProfiles(_ indexSet: IndexSet) { let currentHeaders = sortedHeaders var toDelete: [UUID] = [] indexSet.forEach { toDelete.append(currentHeaders[$0].id) } // clear selection before removal to avoid triggering a bogus navigation push if let selectedProfileId = selectedProfileId, toDelete.contains(selectedProfileId) { self.selectedProfileId = nil } profileManager.removeProfiles(withIds: toDelete) } private func dismissSelectionIfDeleted(headers: [Profile.Header]) { if let selectedProfileId = selectedProfileId, !profileManager.isExistingProfile(withId: selectedProfileId) { self.selectedProfileId = nil } } }