// AdvancedPrefsView.swift // Tusker // // Created by Shadowfacts on 6/13/19. // Copyright © 2019 Shadowfacts. All rights reserved. // import SwiftUI import Pachyderm import CoreData import CloudKit 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? var body: some View { List { formattingSection cloudKitSection errorReportingSection cachingSection } .listStyle(.insetGrouped) .appGroupedScrollBackgroundIfAvailable() .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 = LocalData.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 } } .listRowBackground(Color.appGroupedCellBackground) } 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!)) } } } .listRowBackground(Color.appGroupedCellBackground) .task { CKContainer.default().accountStatus { status, error in if let error { Logging.general.error("Unable to get CloudKit status: \(String(describing: error))") } else { self.cloudKitStatus = status } } } } 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) } } .listRowBackground(Color.appGroupedCellBackground) } 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) } .listRowBackground(Color.appGroupedCellBackground) .task { imageCacheSize = [ ImageCache.avatars, .headers, .attachments, .emojis, ].map { $0.getDiskSizeInBytes() ?? 0 }.reduce(0, +) mastodonCacheSize = LocalData.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, +) } } private func clearCache() { for account in LocalData.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 = LocalData.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