Erase iCloud store from Settings (#675)
Also, fix SwiftUI not refreshing when remote profiles are updated. There was no objectWillChange nor Published around ProfileManager.allRemoteProfiles, and ProfileRowView was not treating it as ObservedObject. Closes #673
This commit is contained in:
parent
372e30cf68
commit
211b3b83d3
|
@ -55,8 +55,8 @@ struct PassepartoutApp: App {
|
|||
.defaultSize(width: 600.0, height: 400.0)
|
||||
|
||||
Settings {
|
||||
SettingsView()
|
||||
.frame(minWidth: 300, minHeight: 100)
|
||||
SettingsView(profileManager: context.profileManager)
|
||||
.frame(minWidth: 300, minHeight: 200)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
|
@ -174,6 +174,10 @@ extension ProfileManager {
|
|||
try await remoteRepository.removeEntities(withIds: [profileId])
|
||||
}
|
||||
}
|
||||
|
||||
public func eraseRemoteProfiles() async throws {
|
||||
try await remoteRepository?.removeEntities(withIds: Array(allRemoteProfiles.keys))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Shortcuts
|
||||
|
@ -258,6 +262,7 @@ private extension ProfileManager {
|
|||
allRemoteProfiles = result.entities.reduce(into: [:]) {
|
||||
$0[$1.id] = $1
|
||||
}
|
||||
objectWillChange.send()
|
||||
|
||||
// pull remote updates into local profiles (best-effort)
|
||||
let profilesToImport = allRemoteProfiles.values
|
||||
|
|
|
@ -108,6 +108,8 @@ extension Strings {
|
|||
|
||||
static let httpProxy = "HTTP Proxy"
|
||||
|
||||
static let iCloud = "iCloud"
|
||||
|
||||
static let ip = "IP"
|
||||
|
||||
static let ipv4 = "IPv4"
|
||||
|
|
|
@ -428,6 +428,12 @@ public enum Strings {
|
|||
public static let name = Strings.tr("Localizable", "placeholders.profile.name", fallback: "My profile")
|
||||
}
|
||||
}
|
||||
public enum Theme {
|
||||
public enum Confirmation {
|
||||
/// Are you sure?
|
||||
public static let message = Strings.tr("Localizable", "theme.confirmation.message", fallback: "Are you sure?")
|
||||
}
|
||||
}
|
||||
public enum Ui {
|
||||
public enum ConnectionStatus {
|
||||
/// (on-demand)
|
||||
|
@ -579,6 +585,8 @@ public enum Strings {
|
|||
public enum Rows {
|
||||
/// Confirm quit
|
||||
public static let confirmQuit = Strings.tr("Localizable", "views.settings.rows.confirm_quit", fallback: "Confirm quit")
|
||||
/// Erase iCloud store
|
||||
public static let eraseIcloud = Strings.tr("Localizable", "views.settings.rows.erase_icloud", fallback: "Erase iCloud store")
|
||||
/// Lock in background
|
||||
public static let lockInBackground = Strings.tr("Localizable", "views.settings.rows.lock_in_background", fallback: "Lock in background")
|
||||
public enum LockInBackground {
|
||||
|
@ -586,6 +594,12 @@ public enum Strings {
|
|||
public static let message = Strings.tr("Localizable", "views.settings.rows.lock_in_background.message", fallback: "Passepartout is locked")
|
||||
}
|
||||
}
|
||||
public enum Sections {
|
||||
public enum Icloud {
|
||||
/// To erase the iCloud store securely, do so on all your synced devices. This will not affect local profiles.
|
||||
public static let footer = Strings.tr("Localizable", "views.settings.sections.icloud.footer", fallback: "To erase the iCloud store securely, do so on all your synced devices. This will not affect local profiles.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,6 +102,10 @@
|
|||
"placeholders.username" = "username";
|
||||
"placeholders.secret" = "secret";
|
||||
|
||||
// MARK: - Theme
|
||||
|
||||
"theme.confirmation.message" = "Are you sure?";
|
||||
|
||||
// MARK: - Views
|
||||
|
||||
"views.profiles.rows.not_installed" = "Select a profile";
|
||||
|
@ -121,9 +125,11 @@
|
|||
"views.profile.rows.add_module" = "Add module";
|
||||
"views.profile.module_list.section.footer" = "Drag modules to rearrange them, as their order determines priority.";
|
||||
|
||||
"views.settings.sections.icloud.footer" = "To erase the iCloud store securely, do so on all your synced devices. This will not affect local profiles.";
|
||||
"views.settings.rows.confirm_quit" = "Confirm quit";
|
||||
"views.settings.rows.lock_in_background" = "Lock in background";
|
||||
"views.settings.rows.lock_in_background.message" = "Passepartout is locked";
|
||||
"views.settings.rows.erase_icloud" = "Erase iCloud store";
|
||||
|
||||
"views.about.title" = "About";
|
||||
"views.about.sections.resources" = "Resources";
|
||||
|
|
|
@ -33,14 +33,12 @@ struct AboutRouterView: View {
|
|||
@Environment(\.dismiss)
|
||||
var dismiss
|
||||
|
||||
let profileManager: ProfileManager
|
||||
|
||||
let tunnel: Tunnel
|
||||
|
||||
@State
|
||||
var navigationRoute: NavigationRoute?
|
||||
|
||||
init(tunnel: Tunnel) {
|
||||
self.tunnel = tunnel
|
||||
}
|
||||
}
|
||||
|
||||
extension AboutRouterView {
|
||||
|
@ -95,6 +93,7 @@ extension AboutRouterView {
|
|||
|
||||
#Preview {
|
||||
AboutRouterView(
|
||||
profileManager: .mock,
|
||||
tunnel: .mock
|
||||
)
|
||||
.withMockEnvironment()
|
||||
|
|
|
@ -23,12 +23,14 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import AppLibrary
|
||||
import CommonLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
import UtilsLibrary
|
||||
|
||||
struct AboutView: View {
|
||||
let profileManager: ProfileManager
|
||||
|
||||
@Binding
|
||||
var navigationRoute: AboutRouterView.NavigationRoute?
|
||||
|
@ -64,6 +66,8 @@ private extension AboutView {
|
|||
|
||||
#Preview {
|
||||
AboutView(
|
||||
profileManager: .mock,
|
||||
navigationRoute: .constant(nil)
|
||||
)
|
||||
.withMockEnvironment()
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ extension AboutRouterView {
|
|||
var body: some View {
|
||||
NavigationStack {
|
||||
AboutView(
|
||||
profileManager: profileManager,
|
||||
navigationRoute: $navigationRoute
|
||||
)
|
||||
.toolbar {
|
||||
|
|
|
@ -31,7 +31,7 @@ import SwiftUI
|
|||
extension AboutView {
|
||||
var listView: some View {
|
||||
List {
|
||||
SettingsSection()
|
||||
SettingsSectionGroup(profileManager: profileManager)
|
||||
Section {
|
||||
// TODO: #585, donations
|
||||
// donateLink
|
||||
|
|
|
@ -32,6 +32,7 @@ extension AboutRouterView {
|
|||
var body: some View {
|
||||
NavigationSplitView {
|
||||
AboutView(
|
||||
profileManager: profileManager,
|
||||
navigationRoute: $navigationRoute
|
||||
)
|
||||
} detail: {
|
||||
|
|
|
@ -129,10 +129,13 @@ private extension AppInlineCoordinator {
|
|||
func modalDestination(for item: ModalRoute?) -> some View {
|
||||
switch item {
|
||||
case .settings:
|
||||
SettingsView()
|
||||
SettingsView(profileManager: profileManager)
|
||||
|
||||
case .about:
|
||||
AboutRouterView(tunnel: tunnel)
|
||||
AboutRouterView(
|
||||
profileManager: profileManager,
|
||||
tunnel: tunnel
|
||||
)
|
||||
|
||||
default:
|
||||
EmptyView()
|
||||
|
|
|
@ -120,10 +120,13 @@ extension AppModalCoordinator {
|
|||
}
|
||||
|
||||
case .settings:
|
||||
SettingsView()
|
||||
SettingsView(profileManager: profileManager)
|
||||
|
||||
case .about:
|
||||
AboutRouterView(tunnel: tunnel)
|
||||
AboutRouterView(
|
||||
profileManager: profileManager,
|
||||
tunnel: tunnel
|
||||
)
|
||||
|
||||
default:
|
||||
EmptyView()
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
//
|
||||
// SettingsSectionGroup.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/3/24.
|
||||
// Copyright (c) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import AppLibrary
|
||||
import CommonLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
import UtilsLibrary
|
||||
|
||||
struct SettingsSectionGroup: View {
|
||||
|
||||
@AppStorage(AppPreference.confirmsQuit.key)
|
||||
private var confirmsQuit = true
|
||||
|
||||
@AppStorage(AppPreference.locksInBackground.key)
|
||||
private var locksInBackground = false
|
||||
|
||||
let profileManager: ProfileManager
|
||||
|
||||
@State
|
||||
private var isConfirmingEraseiCloud = false
|
||||
|
||||
@State
|
||||
private var isErasingiCloud = false
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
#if os(macOS)
|
||||
confirmsQuitToggle
|
||||
#endif
|
||||
#if os(iOS)
|
||||
lockInBackgroundToggle
|
||||
#endif
|
||||
} header: {
|
||||
Text(Strings.Global.general)
|
||||
}
|
||||
Group {
|
||||
eraseCloudKitButton
|
||||
}
|
||||
.themeSection(
|
||||
header: Strings.Unlocalized.iCloud,
|
||||
footer: Strings.Views.Settings.Sections.Icloud.footer
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private extension SettingsSectionGroup {
|
||||
var confirmsQuitToggle: some View {
|
||||
Toggle(Strings.Views.Settings.Rows.confirmQuit, isOn: $confirmsQuit)
|
||||
}
|
||||
|
||||
var lockInBackgroundToggle: some View {
|
||||
Toggle(Strings.Views.Settings.Rows.lockInBackground, isOn: $locksInBackground)
|
||||
}
|
||||
|
||||
var eraseCloudKitButton: some View {
|
||||
Button(Strings.Views.Settings.Rows.eraseIcloud, role: .destructive) {
|
||||
isConfirmingEraseiCloud = true
|
||||
}
|
||||
.themeConfirmation(
|
||||
isPresented: $isConfirmingEraseiCloud,
|
||||
title: Strings.Views.Settings.Rows.eraseIcloud
|
||||
) {
|
||||
isErasingiCloud = true
|
||||
Task {
|
||||
do {
|
||||
pp_log(.app, .info, "Erase CloudKit profiles...")
|
||||
try await profileManager.eraseRemoteProfiles()
|
||||
|
||||
let containerId = BundleConfiguration.mainString(for: .cloudKitId)
|
||||
pp_log(.app, .info, "Erase CloudKit store with identifier \(containerId)...")
|
||||
try await Utils.eraseCloudKitStore(fromContainerWithId: containerId)
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to erase CloudKit store: \(error)")
|
||||
}
|
||||
isErasingiCloud = false
|
||||
}
|
||||
}
|
||||
.disabled(isErasingiCloud)
|
||||
}
|
||||
}
|
|
@ -23,19 +23,22 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import AppLibrary
|
||||
import SwiftUI
|
||||
|
||||
public struct SettingsView: View {
|
||||
let profileManager: ProfileManager
|
||||
|
||||
@State
|
||||
private var path = NavigationPath()
|
||||
|
||||
public init() {
|
||||
public init(profileManager: ProfileManager) {
|
||||
self.profileManager = profileManager
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
Form {
|
||||
SettingsSection()
|
||||
SettingsSectionGroup(profileManager: profileManager)
|
||||
}
|
||||
.themeForm()
|
||||
.navigationTitle(Strings.Global.settings)
|
||||
|
|
|
@ -102,6 +102,26 @@ struct ThemeItemModalModifier<Modal, T>: ViewModifier where Modal: View, T: Iden
|
|||
}
|
||||
}
|
||||
|
||||
struct ThemeConfirmationModifier: ViewModifier {
|
||||
|
||||
@Binding
|
||||
var isPresented: Bool
|
||||
|
||||
let title: String
|
||||
|
||||
let action: () -> Void
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.confirmationDialog(title, isPresented: $isPresented) {
|
||||
Button(Strings.Global.ok, action: action)
|
||||
Text(Strings.Global.cancel)
|
||||
} message: {
|
||||
Text(Strings.Theme.Confirmation.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ThemeNavigationStackModifier: ViewModifier {
|
||||
|
||||
@Environment(\.dismiss)
|
||||
|
|
|
@ -162,6 +162,10 @@ extension View {
|
|||
))
|
||||
}
|
||||
|
||||
public func themeConfirmation(isPresented: Binding<Bool>, title: String, action: @escaping () -> Void) -> some View {
|
||||
modifier(ThemeConfirmationModifier(isPresented: isPresented, title: title, action: action))
|
||||
}
|
||||
|
||||
public func themeNavigationStack(if condition: Bool, closable: Bool = false, path: Binding<NavigationPath>) -> some View {
|
||||
modifier(ThemeNavigationStackModifier(condition: condition, closable: closable, path: path))
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import AppLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
|
@ -37,7 +38,8 @@ struct ProfileCardView: View {
|
|||
|
||||
let header: ProfileHeader
|
||||
|
||||
let isShared: Bool
|
||||
@ObservedObject
|
||||
var profileManager: ProfileManager
|
||||
|
||||
var body: some View {
|
||||
switch style {
|
||||
|
@ -70,6 +72,12 @@ struct ProfileCardView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private extension ProfileCardView {
|
||||
var isShared: Bool {
|
||||
profileManager.isRemotelyShared(profileWithId: header.id)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview {
|
||||
|
@ -78,14 +86,14 @@ struct ProfileCardView: View {
|
|||
ProfileCardView(
|
||||
style: .compact,
|
||||
header: Profile.mock.header(),
|
||||
isShared: true
|
||||
profileManager: .mock
|
||||
)
|
||||
}
|
||||
Section {
|
||||
ProfileCardView(
|
||||
style: .full,
|
||||
header: Profile.mock.header(),
|
||||
isShared: true
|
||||
profileManager: .mock
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ private extension ProfileRowView {
|
|||
ProfileCardView(
|
||||
style: style,
|
||||
header: header,
|
||||
isShared: profileManager.isRemotelyShared(profileWithId: header.id)
|
||||
profileManager: profileManager
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
.contentShape(.rect)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// SettingsSection.swift
|
||||
// Empty.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/3/24.
|
||||
|
@ -23,39 +23,15 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import SwiftUI
|
||||
import CloudKit
|
||||
import Foundation
|
||||
|
||||
struct SettingsSection: View {
|
||||
extension Utils {
|
||||
private static let cloudKitZone = CKRecordZone.ID(zoneName: "com.apple.coredata.cloudkit.zone")
|
||||
|
||||
@AppStorage(AppPreference.confirmsQuit.key)
|
||||
private var confirmsQuit = true
|
||||
|
||||
@AppStorage(AppPreference.locksInBackground.key)
|
||||
private var locksInBackground = false
|
||||
|
||||
var header: String?
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
#if os(macOS)
|
||||
confirmsQuitToggle
|
||||
#endif
|
||||
#if os(iOS)
|
||||
lockInBackgroundToggle
|
||||
#endif
|
||||
} header: {
|
||||
header.map(Text.init)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension SettingsSection {
|
||||
var confirmsQuitToggle: some View {
|
||||
Toggle(Strings.Views.Settings.Rows.confirmQuit, isOn: $confirmsQuit)
|
||||
}
|
||||
|
||||
var lockInBackgroundToggle: some View {
|
||||
Toggle(Strings.Views.Settings.Rows.lockInBackground, isOn: $locksInBackground)
|
||||
public static func eraseCloudKitStore(fromContainerWithId containerId: String) async throws {
|
||||
let container = CKContainer(identifier: containerId)
|
||||
let db = container.privateCloudDatabase
|
||||
try await db.deleteRecordZone(withID: Self.cloudKitZone)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue