Tusker/Tusker/Screens/Preferences/AdvancedPrefsView.swift

254 lines
9.5 KiB
Swift

// AdvancedPrefsView.swift
// Tusker
//
// Created by Shadowfacts on 6/13/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import SwiftUI
import Pachyderm
import CoreData
import CloudKit
import UserAccounts
struct AdvancedPrefsView : View {
@ObservedObject var preferences = Preferences.shared
@State private var imageCacheSize: Int64 = 0
@State private var mastodonCacheSize: Int64 = 0
@State private var cloudKitStatus: CKAccountStatus?
@State private var isShowingFeatureFlagAlert = false
@State private var featureFlagName = ""
var body: some View {
List {
formattingSection
cloudKitSection
errorReportingSection
cachingSection
featureFlagSection
}
.listStyle(.insetGrouped)
.appGroupedListBackground(container: PreferencesNavigationController.self)
.onTapGesture(count: 3) {
featureFlagName = ""
isShowingFeatureFlagAlert = true
}
.alert("Enable Feature Flag", isPresented: $isShowingFeatureFlagAlert) {
TextField("Flag Name", text: $featureFlagName)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
Button("Cancel", role: .cancel) {}
Button("Enable") {
if let flag = Preferences.FeatureFlag(rawValue: featureFlagName) {
preferences.enabledFeatureFlags.insert(flag)
}
}
} message: {
Text("Warning: Feature flags are intended for development and debugging use only. They are experimental and subject to change at any time.")
}
.navigationBarTitle(Text("Advanced"))
}
var formattingFooter: some View {
var s: AttributedString = "This option is only supported with Pleroma and some compatible Mastodon instances (such as Glitch).\n"
if let account = UserAccountsManager.shared.getMostRecentAccount() {
let mastodonController = MastodonController.getForAccount(account)
// shouldn't need to load the instance here, because loading it is kicked off my the scene delegate
if !mastodonController.instanceFeatures.probablySupportsMarkdown {
var warning = AttributedString("\(account.instanceURL.host!) does not appear to support formatting. Using formatting symbols may not have an effect.")
warning[AttributeScopes.SwiftUIAttributes.FontAttribute.self] = .caption.bold()
s += warning
}
}
return Text(s).lineLimit(nil)
}
var formattingSection: some View {
Section(footer: formattingFooter) {
Picker(selection: $preferences.statusContentType, label: Text("Post Content Type")) {
ForEach(StatusContentType.allCases, id: \.self) { type in
Text(type.displayName).tag(type)
}//.navigationBarTitle("Post Content Type")
// see FB6838291
}
}
.appGroupedListRowBackground()
}
var cloudKitSection: some View {
Section {
HStack {
Text("iCloud Status")
Spacer()
switch cloudKitStatus {
case nil:
EmptyView()
case .available:
Text("Available")
case .couldNotDetermine:
Text("Could not determine")
case .noAccount:
Text("No account")
case .restricted:
Text("Restricted")
case .temporarilyUnavailable:
Text("Temporarily Unavailable")
@unknown default:
Text(String(describing: cloudKitStatus!))
}
}
}
.appGroupedListRowBackground()
.task {
do {
let status = try await CKContainer.default().accountStatus()
self.cloudKitStatus = status
} catch {
Logging.general.error("Unable to get CloudKit status: \(String(describing: error))")
}
}
}
var errorReportingSection: some View {
Section {
Toggle("Report Errors Automatically", isOn: $preferences.reportErrorsAutomatically)
} footer: {
var privacyPolicy: AttributedString = "Privacy Policy"
let _ = privacyPolicy.link = URL(string: "https://vaccor.space/tusker#privacy")!
if preferences.reportErrorsAutomatically {
Text(AttributedString("App crashes and errors will be automatically reported to the developer. You may be prompted to add additional information.\n") + privacyPolicy)
.lineLimit(nil)
} else {
Text(AttributedString("Errors will not be reported automatically. When a crash occurs, you may be asked to report it manually.\n") + privacyPolicy)
.lineLimit(nil)
}
}
.appGroupedListRowBackground()
}
var cachingSection: some View {
Section {
Button(action: clearCache) {
Text("Clear Mastodon Cache")
}.foregroundColor(.red)
Button(action: clearImageCaches) {
Text("Clear Image Caches")
}.foregroundColor(.red)
} header: {
Text("Caching")
} footer: {
var s: AttributedString = "Clearing caches will restart the app."
if imageCacheSize != 0 {
s += AttributedString("\nImage cache size: \(ByteCountFormatter().string(fromByteCount: imageCacheSize))")
}
if mastodonCacheSize != 0 {
s += AttributedString("\nMastodon cache size: \(ByteCountFormatter().string(fromByteCount: mastodonCacheSize))")
}
return Text(s)
}
.appGroupedListRowBackground()
.task {
imageCacheSize = [
ImageCache.avatars,
.headers,
.attachments,
.emojis,
].map {
$0.getDiskSizeInBytes() ?? 0
}.reduce(0, +)
mastodonCacheSize = UserAccountsManager.shared.accounts.map {
let descriptions = MastodonController.getForAccount($0).persistentContainer.persistentStoreDescriptions
return descriptions.map {
guard let url = $0.url else {
return 0
}
return FileManager.default.recursiveSize(url: url) ?? 0
}.reduce(0, +)
}.reduce(0, +)
}
}
@ViewBuilder
var featureFlagSection: some View {
if !preferences.enabledFeatureFlags.isEmpty {
Section {
ForEach(preferences.enabledFeatureFlags.map(\.rawValue).sorted(), id: \.self) { name in
Text(verbatim: name)
.contextMenu {
Button(role: .destructive) {
preferences.enabledFeatureFlags.remove(.init(rawValue: name)!)
} label: {
Label("Remove", systemImage: "trash")
}
}
}
.onDelete { indices in
let sortedFlags = preferences.enabledFeatureFlags.sorted(by: { $0.rawValue < $1.rawValue })
let removed = indices.map { sortedFlags[$0] }
preferences.enabledFeatureFlags.subtract(removed)
}
} header: {
Text("Feature Flags")
}
}
}
private func clearCache() {
for account in UserAccountsManager.shared.accounts {
let controller = MastodonController.getForAccount(account)
let container = controller.persistentContainer
do {
let statusesReq = NSBatchDeleteRequest(fetchRequest: StatusMO.fetchRequest())
try container.viewContext.execute(statusesReq)
let accountsReq = NSBatchDeleteRequest(fetchRequest: AccountMO.fetchRequest())
try container.viewContext.execute(accountsReq)
let relationshipsReq = NSBatchDeleteRequest(fetchRequest: RelationshipMO.fetchRequest())
try container.viewContext.execute(relationshipsReq)
} catch {
Logging.general.error("Error while clearing Mastodon cache: \(String(describing: error), privacy: .public)")
}
}
resetUI()
}
private func clearImageCaches() {
[
ImageCache.avatars,
ImageCache.headers,
ImageCache.attachments,
ImageCache.emojis,
].forEach {
try! $0.reset()
}
resetUI()
}
private func resetUI() {
let mostRecent = UserAccountsManager.shared.getMostRecentAccount()!
NotificationCenter.default.post(name: .activateAccount, object: nil, userInfo: ["account": mostRecent])
}
}
extension StatusContentType {
var displayName: String {
switch self {
case .plain:
return "Plain"
case .markdown:
return "Markdown"
case .html:
return "HTML"
}
}
}
#if DEBUG
struct AdvancedPrefsView_Previews : PreviewProvider {
static var previews: some View {
AdvancedPrefsView()
}
}
#endif