// // Preferences.swift // Tusker // // Created by Shadowfacts on 8/28/18. // Copyright © 2018 Shadowfacts. All rights reserved. // import UIKit import Pachyderm import Combine class Preferences: Codable, ObservableObject { static var shared: Preferences = load() private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! private static var archiveURL = Preferences.documentsDirectory.appendingPathComponent("preferences").appendingPathExtension("plist") static func save() { let encoder = PropertyListEncoder() let data = try? encoder.encode(shared) try? data?.write(to: archiveURL, options: .noFileProtection) } static func load() -> Preferences { let decoder = PropertyListDecoder() if let data = try? Data(contentsOf: archiveURL), let preferences = try? decoder.decode(Preferences.self, from: data) { return preferences } return Preferences() } private init() {} required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.theme = try container.decode(UIUserInterfaceStyle.self, forKey: .theme) self.pureBlackDarkMode = try container.decodeIfPresent(Bool.self, forKey: .pureBlackDarkMode) ?? true self.accentColor = try container.decodeIfPresent(AccentColor.self, forKey: .accentColor) ?? .default self.avatarStyle = try container.decode(AvatarStyle.self, forKey: .avatarStyle) self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames) self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon) self.alwaysShowStatusVisibilityIcon = try container.decode(Bool.self, forKey: .alwaysShowStatusVisibilityIcon) self.hideActionsInTimeline = try container.decodeIfPresent(Bool.self, forKey: .hideActionsInTimeline) ?? false self.showLinkPreviews = try container.decodeIfPresent(Bool.self, forKey: .showLinkPreviews) ?? true self.leadingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .leadingStatusSwipeActions) ?? leadingStatusSwipeActions self.trailingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .trailingStatusSwipeActions) ?? trailingStatusSwipeActions self.defaultPostVisibility = try container.decode(Status.Visibility.self, forKey: .defaultPostVisibility) self.defaultReplyVisibility = try container.decodeIfPresent(ReplyVisibility.self, forKey: .defaultReplyVisibility) ?? .sameAsPost self.automaticallySaveDrafts = try container.decode(Bool.self, forKey: .automaticallySaveDrafts) self.requireAttachmentDescriptions = try container.decode(Bool.self, forKey: .requireAttachmentDescriptions) self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode) self.mentionReblogger = try container.decode(Bool.self, forKey: .mentionReblogger) self.useTwitterKeyboard = try container.decodeIfPresent(Bool.self, forKey: .useTwitterKeyboard) ?? false if let blurAllMedia = try? container.decodeIfPresent(Bool.self, forKey: .blurAllMedia) { self.attachmentBlurMode = blurAllMedia ? .always : .useStatusSetting } else { self.attachmentBlurMode = try container.decode(AttachmentBlurMode.self, forKey: .attachmentBlurMode) } self.blurMediaBehindContentWarning = try container.decodeIfPresent(Bool.self, forKey: .blurMediaBehindContentWarning) ?? true self.automaticallyPlayGifs = try container.decode(Bool.self, forKey: .automaticallyPlayGifs) self.showUncroppedMediaInline = try container.decodeIfPresent(Bool.self, forKey: .showUncroppedMediaInline) ?? true self.showAttachmentBadges = try container.decodeIfPresent(Bool.self, forKey: .showAttachmentBadges) ?? true self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps) self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari) self.inAppSafariAutomaticReaderMode = try container.decode(Bool.self, forKey: .inAppSafariAutomaticReaderMode) self.expandAllContentWarnings = try container.decodeIfPresent(Bool.self, forKey: .expandAllContentWarnings) ?? false self.collapseLongPosts = try container.decodeIfPresent(Bool.self, forKey: .collapseLongPosts) ?? true self.oppositeCollapseKeywords = try container.decodeIfPresent([String].self, forKey: .oppositeCollapseKeywords) ?? [] self.confirmBeforeReblog = try container.decodeIfPresent(Bool.self, forKey: .confirmBeforeReblog) ?? false self.timelineStateRestoration = try container.decodeIfPresent(Bool.self, forKey: .timelineStateRestoration) ?? true self.timelineSyncMode = try container.decodeIfPresent(TimelineSyncMode.self, forKey: .timelineSyncMode) ?? .icloud self.hideReblogsInTimelines = try container.decodeIfPresent(Bool.self, forKey: .hideReblogsInTimelines) ?? false self.hideRepliesInTimelines = try container.decodeIfPresent(Bool.self, forKey: .hideRepliesInTimelines) ?? false self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts) self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType) self.grayscaleImages = try container.decodeIfPresent(Bool.self, forKey: .grayscaleImages) ?? false self.disableInfiniteScrolling = try container.decodeIfPresent(Bool.self, forKey: .disableInfiniteScrolling) ?? false self.hideTrends = try container.decodeIfPresent(Bool.self, forKey: .hideTrends) ?? false self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType) self.hasShownLocalTimelineDescription = try container.decodeIfPresent(Bool.self, forKey: .hasShownLocalTimelineDescription) ?? false self.hasShownFederatedTimelineDescription = try container.decodeIfPresent(Bool.self, forKey: .hasShownFederatedTimelineDescription) ?? false } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(theme, forKey: .theme) try container.encode(pureBlackDarkMode, forKey: .pureBlackDarkMode) try container.encode(accentColor, forKey: .accentColor) try container.encode(avatarStyle, forKey: .avatarStyle) try container.encode(hideCustomEmojiInUsernames, forKey: .hideCustomEmojiInUsernames) try container.encode(showIsStatusReplyIcon, forKey: .showIsStatusReplyIcon) try container.encode(alwaysShowStatusVisibilityIcon, forKey: .alwaysShowStatusVisibilityIcon) try container.encode(hideActionsInTimeline, forKey: .hideActionsInTimeline) try container.encode(showLinkPreviews, forKey: .showLinkPreviews) try container.encode(leadingStatusSwipeActions, forKey: .leadingStatusSwipeActions) try container.encode(trailingStatusSwipeActions, forKey: .trailingStatusSwipeActions) try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility) try container.encode(defaultReplyVisibility, forKey: .defaultReplyVisibility) try container.encode(automaticallySaveDrafts, forKey: .automaticallySaveDrafts) try container.encode(requireAttachmentDescriptions, forKey: .requireAttachmentDescriptions) try container.encode(contentWarningCopyMode, forKey: .contentWarningCopyMode) try container.encode(mentionReblogger, forKey: .mentionReblogger) try container.encode(useTwitterKeyboard, forKey: .useTwitterKeyboard) try container.encode(attachmentBlurMode, forKey: .attachmentBlurMode) try container.encode(blurMediaBehindContentWarning, forKey: .blurMediaBehindContentWarning) try container.encode(automaticallyPlayGifs, forKey: .automaticallyPlayGifs) try container.encode(showUncroppedMediaInline, forKey: .showUncroppedMediaInline) try container.encode(showAttachmentBadges, forKey: .showAttachmentBadges) try container.encode(openLinksInApps, forKey: .openLinksInApps) try container.encode(useInAppSafari, forKey: .useInAppSafari) try container.encode(inAppSafariAutomaticReaderMode, forKey: .inAppSafariAutomaticReaderMode) try container.encode(expandAllContentWarnings, forKey: .expandAllContentWarnings) try container.encode(collapseLongPosts, forKey: .collapseLongPosts) try container.encode(oppositeCollapseKeywords, forKey: .oppositeCollapseKeywords) try container.encode(confirmBeforeReblog, forKey: .confirmBeforeReblog) try container.encode(timelineStateRestoration, forKey: .timelineStateRestoration) try container.encode(timelineSyncMode, forKey: .timelineSyncMode) try container.encode(hideReblogsInTimelines, forKey: .hideReblogsInTimelines) try container.encode(hideRepliesInTimelines, forKey: .hideRepliesInTimelines) try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts) try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType) try container.encode(grayscaleImages, forKey: .grayscaleImages) try container.encode(disableInfiniteScrolling, forKey: .disableInfiniteScrolling) try container.encode(hideTrends, forKey: .hideTrends) try container.encode(statusContentType, forKey: .statusContentType) try container.encode(hasShownLocalTimelineDescription, forKey: .hasShownLocalTimelineDescription) try container.encode(hasShownFederatedTimelineDescription, forKey: .hasShownFederatedTimelineDescription) } // MARK: Appearance @Published var theme = UIUserInterfaceStyle.unspecified @Published var pureBlackDarkMode = true @Published var accentColor = AccentColor.default @Published var avatarStyle = AvatarStyle.roundRect @Published var hideCustomEmojiInUsernames = false @Published var showIsStatusReplyIcon = false @Published var alwaysShowStatusVisibilityIcon = false @Published var hideActionsInTimeline = false @Published var showLinkPreviews = true @Published var leadingStatusSwipeActions: [StatusSwipeAction] = [.favorite, .reblog] @Published var trailingStatusSwipeActions: [StatusSwipeAction] = [.reply, .share] // MARK: Composing @Published var defaultPostVisibility = Status.Visibility.public @Published var defaultReplyVisibility = ReplyVisibility.sameAsPost @Published var automaticallySaveDrafts = true @Published var requireAttachmentDescriptions = false @Published var contentWarningCopyMode = ContentWarningCopyMode.asIs @Published var mentionReblogger = false @Published var useTwitterKeyboard = false // MARK: Media @Published var attachmentBlurMode = AttachmentBlurMode.useStatusSetting { didSet { if attachmentBlurMode == .always { blurMediaBehindContentWarning = true } else if attachmentBlurMode == .never { blurMediaBehindContentWarning = false } } } @Published var blurMediaBehindContentWarning = true @Published var automaticallyPlayGifs = true @Published var showUncroppedMediaInline = true @Published var showAttachmentBadges = true // MARK: Behavior @Published var openLinksInApps = true @Published var useInAppSafari = true @Published var inAppSafariAutomaticReaderMode = false @Published var expandAllContentWarnings = false @Published var collapseLongPosts = true @Published var oppositeCollapseKeywords: [String] = [] @Published var confirmBeforeReblog = false @Published var timelineStateRestoration = true @Published var timelineSyncMode = TimelineSyncMode.icloud @Published var hideReblogsInTimelines = false @Published var hideRepliesInTimelines = false // MARK: Digital Wellness @Published var showFavoriteAndReblogCounts = true @Published var defaultNotificationsMode = NotificationsMode.allNotifications @Published var grayscaleImages = false @Published var disableInfiniteScrolling = false @Published var hideTrends = false // MARK: Advanced @Published var statusContentType: StatusContentType = .plain @Published var reportErrorsAutomatically = true // MARK: @Published var hasShownLocalTimelineDescription = false @Published var hasShownFederatedTimelineDescription = false private enum CodingKeys: String, CodingKey { case theme case pureBlackDarkMode case accentColor case avatarStyle case hideCustomEmojiInUsernames case showIsStatusReplyIcon case alwaysShowStatusVisibilityIcon case hideActionsInTimeline case showLinkPreviews case leadingStatusSwipeActions case trailingStatusSwipeActions case defaultPostVisibility case defaultReplyVisibility case automaticallySaveDrafts case requireAttachmentDescriptions case contentWarningCopyMode case mentionReblogger case useTwitterKeyboard case blurAllMedia // only used for migration case attachmentBlurMode case blurMediaBehindContentWarning case automaticallyPlayGifs case showUncroppedMediaInline case showAttachmentBadges case openLinksInApps case useInAppSafari case inAppSafariAutomaticReaderMode case expandAllContentWarnings case collapseLongPosts case oppositeCollapseKeywords case confirmBeforeReblog case timelineStateRestoration case timelineSyncMode case hideReblogsInTimelines case hideRepliesInTimelines case showFavoriteAndReblogCounts case defaultNotificationsType case grayscaleImages case disableInfiniteScrolling case hideTrends = "hideDiscover" case statusContentType case hasShownLocalTimelineDescription case hasShownFederatedTimelineDescription } } extension Preferences { enum ReplyVisibility: Codable, Hashable, CaseIterable { case sameAsPost case visibility(Status.Visibility) static var allCases: [Preferences.ReplyVisibility] = [.sameAsPost] + Status.Visibility.allCases.map { .visibility($0) } var resolved: Status.Visibility { switch self { case .sameAsPost: return Preferences.shared.defaultPostVisibility case .visibility(let vis): return vis } } var displayName: String { switch self { case .sameAsPost: return "Same as Default" case .visibility(let vis): return vis.displayName } } var imageName: String? { switch self { case .sameAsPost: return nil case .visibility(let vis): return vis.imageName } } } } extension Preferences { enum AttachmentBlurMode: Codable, Hashable, CaseIterable { case useStatusSetting case always case never var displayName: String { switch self { case .useStatusSetting: return "Default" case .always: return "Always" case .never: return "Never" } } } } extension UIUserInterfaceStyle: Codable {} extension Preferences { enum AccentColor: String, Codable, CaseIterable { case `default` case purple case indigo case blue case cyan case teal case mint case green // case yellow case orange case red case pink // case brown var color: UIColor? { switch self { case .default: return nil case .blue: return .systemBlue // case .brown: // return .systemBrown case .cyan: return .systemCyan case .green: return .systemGreen case .indigo: return .systemIndigo case .mint: return .systemMint case .orange: return .systemOrange case .pink: return .systemPink case .purple: return .systemPurple case .red: return .systemRed case .teal: return .systemTeal // case .yellow: // return .systemYellow } } var name: String { switch self { case .default: return "Default" case .blue: return "Blue" // case .brown: // return "Brown" case .cyan: return "Cyan" case .green: return "Green" case .indigo: return "Indigo" case .mint: return "Mint" case .orange: return "Orange" case .pink: return "Pink" case .purple: return "Purple" case .red: return "Red" case .teal: return "Teal" // case .yellow: // return "Yellow" } } } } extension Preferences { enum TimelineSyncMode: String, Codable { case mastodon case icloud } }