Tusker/Packages/TuskerPreferences/Sources/TuskerPreferences/Preferences.swift

449 lines
20 KiB
Swift
Raw Normal View History

2018-08-28 23:16:17 +00:00
//
// Preferences.swift
2023-04-18 23:47:49 +00:00
// TuskerPreferences
2018-08-28 23:16:17 +00:00
//
// Created by Shadowfacts on 8/28/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
2018-09-11 14:52:21 +00:00
import Pachyderm
2019-06-14 00:53:17 +00:00
import Combine
2018-08-28 23:16:17 +00:00
public final class Preferences: Codable, ObservableObject {
2019-07-18 22:44:35 +00:00
2023-04-18 23:47:49 +00:00
public static var shared: Preferences = load()
2018-08-28 23:16:17 +00:00
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
2023-04-18 23:47:49 +00:00
private static var appGroupDirectory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!
private static var archiveURL = appGroupDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
2018-08-28 23:16:17 +00:00
2023-04-18 23:47:49 +00:00
public static func save() {
2018-08-28 23:16:17 +00:00
let encoder = PropertyListEncoder()
let data = try? encoder.encode(shared)
2018-10-23 02:09:11 +00:00
try? data?.write(to: archiveURL, options: .noFileProtection)
2018-08-28 23:16:17 +00:00
}
2023-04-18 23:47:49 +00:00
public static func load() -> Preferences {
2018-08-28 23:16:17 +00:00
let decoder = PropertyListDecoder()
2018-10-23 02:09:11 +00:00
if let data = try? Data(contentsOf: archiveURL),
2018-08-28 23:16:17 +00:00
let preferences = try? decoder.decode(Preferences.self, from: data) {
return preferences
}
return Preferences()
}
2023-04-18 23:47:49 +00:00
public static func migrate(from url: URL) -> Result<Void, any Error> {
do {
try? FileManager.default.removeItem(at: archiveURL)
try FileManager.default.moveItem(at: url, to: archiveURL)
} catch {
return .failure(error)
}
shared = load()
return .success(())
}
private init() {}
2018-08-28 23:16:17 +00:00
2023-04-18 23:47:49 +00:00
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.theme = try container.decode(UIUserInterfaceStyle.self, forKey: .theme)
2023-02-03 04:02:11 +00:00
self.pureBlackDarkMode = try container.decodeIfPresent(Bool.self, forKey: .pureBlackDarkMode) ?? true
2023-01-14 16:27:41 +00:00
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)
2020-06-17 21:45:34 +00:00
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
2022-12-13 04:09:04 +00:00
self.leadingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .leadingStatusSwipeActions) ?? leadingStatusSwipeActions
self.trailingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .trailingStatusSwipeActions) ?? trailingStatusSwipeActions
self.widescreenNavigationMode = try container.decodeIfPresent(WidescreenNavigationMode.self, forKey: .widescreenNavigationMode) ?? Self.defaultWidescreenNavigationMode
self.defaultPostVisibility = try container.decode(Visibility.self, forKey: .defaultPostVisibility)
self.defaultReplyVisibility = try container.decodeIfPresent(ReplyVisibility.self, forKey: .defaultReplyVisibility) ?? .sameAsPost
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
2020-06-17 21:38:00 +00:00
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
2020-06-17 21:38:00 +00:00
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.reportErrorsAutomatically = try container.decodeIfPresent(Bool.self, forKey: .reportErrorsAutomatically) ?? true
let featureFlagNames = (try? container.decodeIfPresent([String].self, forKey: .enabledFeatureFlags)) ?? []
self.enabledFeatureFlags = Set(featureFlagNames.compactMap(FeatureFlag.init))
2021-08-07 18:39:01 +00:00
self.hasShownLocalTimelineDescription = try container.decodeIfPresent(Bool.self, forKey: .hasShownLocalTimelineDescription) ?? false
self.hasShownFederatedTimelineDescription = try container.decodeIfPresent(Bool.self, forKey: .hasShownFederatedTimelineDescription) ?? false
}
2023-04-18 23:47:49 +00:00
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(theme, forKey: .theme)
2023-02-03 04:02:11 +00:00
try container.encode(pureBlackDarkMode, forKey: .pureBlackDarkMode)
2023-01-14 16:27:41 +00:00
try container.encode(accentColor, forKey: .accentColor)
try container.encode(avatarStyle, forKey: .avatarStyle)
try container.encode(hideCustomEmojiInUsernames, forKey: .hideCustomEmojiInUsernames)
2020-06-17 21:45:34 +00:00
try container.encode(showIsStatusReplyIcon, forKey: .showIsStatusReplyIcon)
try container.encode(alwaysShowStatusVisibilityIcon, forKey: .alwaysShowStatusVisibilityIcon)
try container.encode(hideActionsInTimeline, forKey: .hideActionsInTimeline)
try container.encode(showLinkPreviews, forKey: .showLinkPreviews)
2022-12-13 04:09:04 +00:00
try container.encode(leadingStatusSwipeActions, forKey: .leadingStatusSwipeActions)
try container.encode(trailingStatusSwipeActions, forKey: .trailingStatusSwipeActions)
try container.encode(widescreenNavigationMode, forKey: .widescreenNavigationMode)
try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility)
try container.encode(defaultReplyVisibility, forKey: .defaultReplyVisibility)
try container.encode(requireAttachmentDescriptions, forKey: .requireAttachmentDescriptions)
try container.encode(contentWarningCopyMode, forKey: .contentWarningCopyMode)
try container.encode(mentionReblogger, forKey: .mentionReblogger)
try container.encode(useTwitterKeyboard, forKey: .useTwitterKeyboard)
2020-06-17 21:38:00 +00:00
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)
2020-06-17 21:38:00 +00:00
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)
2020-11-01 18:59:58 +00:00
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(reportErrorsAutomatically, forKey: .reportErrorsAutomatically)
try container.encode(enabledFeatureFlags, forKey: .enabledFeatureFlags)
2021-08-07 18:39:01 +00:00
try container.encode(hasShownLocalTimelineDescription, forKey: .hasShownLocalTimelineDescription)
try container.encode(hasShownFederatedTimelineDescription, forKey: .hasShownFederatedTimelineDescription)
}
2020-06-17 21:38:00 +00:00
// MARK: Appearance
2023-04-18 23:47:49 +00:00
@Published public var theme = UIUserInterfaceStyle.unspecified
@Published public var pureBlackDarkMode = true
@Published public var accentColor = AccentColor.default
@Published public var avatarStyle = AvatarStyle.roundRect
@Published public var hideCustomEmojiInUsernames = false
@Published public var showIsStatusReplyIcon = false
@Published public var alwaysShowStatusVisibilityIcon = false
@Published public var hideActionsInTimeline = false
@Published public var showLinkPreviews = true
@Published public var leadingStatusSwipeActions: [StatusSwipeAction] = [.favorite, .reblog]
@Published public var trailingStatusSwipeActions: [StatusSwipeAction] = [.reply, .share]
private static var defaultWidescreenNavigationMode = WidescreenNavigationMode.splitScreen
@Published public var widescreenNavigationMode = Preferences.defaultWidescreenNavigationMode
2020-06-17 21:38:00 +00:00
// MARK: Composing
2023-04-18 23:47:49 +00:00
@Published public var defaultPostVisibility = Visibility.public
@Published public var defaultReplyVisibility = ReplyVisibility.sameAsPost
@Published public var requireAttachmentDescriptions = false
@Published public var contentWarningCopyMode = ContentWarningCopyMode.asIs
@Published public var mentionReblogger = false
@Published public var useTwitterKeyboard = false
2020-06-17 21:38:00 +00:00
// MARK: Media
2023-04-18 23:47:49 +00:00
@Published public var attachmentBlurMode = AttachmentBlurMode.useStatusSetting {
didSet {
if attachmentBlurMode == .always {
blurMediaBehindContentWarning = true
} else if attachmentBlurMode == .never {
blurMediaBehindContentWarning = false
}
}
}
2023-04-18 23:47:49 +00:00
@Published public var blurMediaBehindContentWarning = true
@Published public var automaticallyPlayGifs = true
@Published public var showUncroppedMediaInline = true
@Published public var showAttachmentBadges = true
2020-06-17 21:38:00 +00:00
// MARK: Behavior
2023-04-18 23:47:49 +00:00
@Published public var openLinksInApps = true
@Published public var useInAppSafari = true
@Published public var inAppSafariAutomaticReaderMode = false
@Published public var expandAllContentWarnings = false
@Published public var collapseLongPosts = true
@Published public var oppositeCollapseKeywords: [String] = []
@Published public var confirmBeforeReblog = false
@Published public var timelineStateRestoration = true
@Published public var timelineSyncMode = TimelineSyncMode.icloud
@Published public var hideReblogsInTimelines = false
@Published public var hideRepliesInTimelines = false
2018-10-23 02:09:11 +00:00
2020-06-17 21:38:00 +00:00
// MARK: Digital Wellness
2023-04-18 23:47:49 +00:00
@Published public var showFavoriteAndReblogCounts = true
@Published public var defaultNotificationsMode = NotificationsMode.allNotifications
@Published public var grayscaleImages = false
@Published public var disableInfiniteScrolling = false
@Published public var hideTrends = false
2020-06-17 21:38:00 +00:00
// MARK: Advanced
2023-04-18 23:47:49 +00:00
@Published public var statusContentType: StatusContentType = .plain
@Published public var reportErrorsAutomatically = true
@Published public var enabledFeatureFlags: Set<FeatureFlag> = []
2021-08-07 18:39:01 +00:00
// MARK:
2023-04-18 23:47:49 +00:00
@Published public var hasShownLocalTimelineDescription = false
@Published public var hasShownFederatedTimelineDescription = false
2021-08-07 18:39:01 +00:00
public func hasFeatureFlag(_ flag: FeatureFlag) -> Bool {
enabledFeatureFlags.contains(flag)
}
2020-11-01 18:59:58 +00:00
private enum CodingKeys: String, CodingKey {
case theme
2023-02-03 04:02:11 +00:00
case pureBlackDarkMode
2023-01-14 16:27:41 +00:00
case accentColor
case avatarStyle
case hideCustomEmojiInUsernames
2020-06-17 21:45:34 +00:00
case showIsStatusReplyIcon
case alwaysShowStatusVisibilityIcon
case hideActionsInTimeline
case showLinkPreviews
2022-12-13 04:09:04 +00:00
case leadingStatusSwipeActions
case trailingStatusSwipeActions
case widescreenNavigationMode
2019-06-14 00:53:17 +00:00
case defaultPostVisibility
case defaultReplyVisibility
case requireAttachmentDescriptions
case contentWarningCopyMode
case mentionReblogger
case useTwitterKeyboard
2020-06-17 21:38:00 +00:00
case blurAllMedia // only used for migration
case attachmentBlurMode
case blurMediaBehindContentWarning
case automaticallyPlayGifs
case showUncroppedMediaInline
case showAttachmentBadges
2020-06-17 21:38:00 +00:00
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
2020-11-01 18:59:58 +00:00
case grayscaleImages
case disableInfiniteScrolling
case hideTrends = "hideDiscover"
case statusContentType
case reportErrorsAutomatically
case enabledFeatureFlags
2021-08-07 18:39:01 +00:00
case hasShownLocalTimelineDescription
case hasShownFederatedTimelineDescription
2019-06-14 00:53:17 +00:00
}
}
extension Preferences {
2023-04-18 23:47:49 +00:00
public enum ReplyVisibility: Codable, Hashable, CaseIterable {
case sameAsPost
case visibility(Visibility)
2023-04-18 23:47:49 +00:00
public static var allCases: [Preferences.ReplyVisibility] = [.sameAsPost] + Visibility.allCases.map { .visibility($0) }
2023-04-18 23:47:49 +00:00
public var resolved: Visibility {
switch self {
case .sameAsPost:
return Preferences.shared.defaultPostVisibility
case .visibility(let vis):
return vis
}
}
2023-04-18 23:47:49 +00:00
public var displayName: String {
switch self {
case .sameAsPost:
return "Same as Default"
case .visibility(let vis):
return vis.displayName
}
}
2023-04-18 23:47:49 +00:00
public var imageName: String? {
switch self {
case .sameAsPost:
return nil
case .visibility(let vis):
return vis.imageName
}
}
}
}
extension Preferences {
2023-04-18 23:47:49 +00:00
public enum AttachmentBlurMode: Codable, Hashable, CaseIterable {
case useStatusSetting
case always
case never
2023-04-18 23:47:49 +00:00
public var displayName: String {
switch self {
case .useStatusSetting:
return "Default"
case .always:
return "Always"
case .never:
return "Never"
}
}
}
}
extension UIUserInterfaceStyle: Codable {}
2023-01-14 16:27:41 +00:00
extension Preferences {
2023-04-18 23:47:49 +00:00
public enum AccentColor: String, Codable, CaseIterable {
2023-01-14 16:27:41 +00:00
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
2023-04-18 23:47:49 +00:00
public var color: UIColor? {
2023-01-14 16:27:41 +00:00
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
}
}
2023-04-18 23:47:49 +00:00
public var name: String {
2023-01-14 16:27:41 +00:00
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 {
2023-04-18 23:47:49 +00:00
public enum TimelineSyncMode: String, Codable {
case mastodon
case icloud
}
}
extension Preferences {
public enum FeatureFlag: String, Codable {
case iPadMultiColumn = "ipad-multi-column"
case iPadBrowserNavigation = "ipad-browser-navigation"
}
}
extension Preferences {
public enum WidescreenNavigationMode: String, Codable {
case stack
case splitScreen
case multiColumn
}
}