IceCubesApp/Packages/Env/Sources/Env/UserPreferences.swift
Thomas Ricouard 1f858414d8 format .
2024-02-14 12:48:14 +01:00

520 lines
15 KiB
Swift

import Combine
import Foundation
import Models
import Network
import SwiftUI
@MainActor
@Observable public class UserPreferences {
class Storage {
@AppStorage("preferred_browser") public var preferredBrowser: PreferredBrowser = .inAppSafari
@AppStorage("show_translate_button_inline") public var showTranslateButton: Bool = true
@AppStorage("show_pending_at_bottom") public var pendingShownAtBottom: Bool = false
@AppStorage("show_pending_left") public var pendingShownLeft: Bool = false
@AppStorage("is_open_ai_enabled") public var isOpenAIEnabled: Bool = true
@AppStorage("recently_used_languages") public var recentlyUsedLanguages: [String] = []
@AppStorage("social_keyboard_composer") public var isSocialKeyboardEnabled: Bool = false
@AppStorage("use_instance_content_settings") public var useInstanceContentSettings: Bool = true
@AppStorage("app_auto_expand_spoilers") public var appAutoExpandSpoilers = false
@AppStorage("app_auto_expand_media") public var appAutoExpandMedia: ServerPreferences.AutoExpandMedia = .hideSensitive
@AppStorage("app_default_post_visibility") public var appDefaultPostVisibility: Models.Visibility = .pub
@AppStorage("app_default_reply_visibility") public var appDefaultReplyVisibility: Models.Visibility = .pub
@AppStorage("app_default_posts_sensitive") public var appDefaultPostsSensitive = false
@AppStorage("app_require_alt_text") public var appRequireAltText = false
@AppStorage("autoplay_video") public var autoPlayVideo = true
@AppStorage("mute_video") public var muteVideo = true
@AppStorage("always_use_deepl") public var alwaysUseDeepl = false
@AppStorage("user_deepl_api_free") public var userDeeplAPIFree = true
@AppStorage("auto_detect_post_language") public var autoDetectPostLanguage = true
@AppStorage("inAppBrowserReaderView") public var inAppBrowserReaderView = false
@AppStorage("haptic_tab") public var hapticTabSelectionEnabled = true
@AppStorage("haptic_timeline") public var hapticTimelineEnabled = true
@AppStorage("haptic_button_press") public var hapticButtonPressEnabled = true
@AppStorage("sound_effect_enabled") public var soundEffectEnabled = true
@AppStorage("show_tab_label_iphone") public var showiPhoneTabLabel = true
@AppStorage("show_alt_text_for_media") public var showAltTextForMedia = true
@AppStorage("show_second_column_ipad") public var showiPadSecondaryColumn = true
@AppStorage("swipeactions-status-trailing-right") public var swipeActionsStatusTrailingRight = StatusAction.favorite
@AppStorage("swipeactions-status-trailing-left") public var swipeActionsStatusTrailingLeft = StatusAction.boost
@AppStorage("swipeactions-status-leading-left") public var swipeActionsStatusLeadingLeft = StatusAction.reply
@AppStorage("swipeactions-status-leading-right") public var swipeActionsStatusLeadingRight = StatusAction.none
@AppStorage("swipeactions-use-theme-color") public var swipeActionsUseThemeColor = false
@AppStorage("swipeactions-icon-style") public var swipeActionsIconStyle: SwipeActionsIconStyle = .iconWithText
@AppStorage("requested_review") public var requestedReview = false
@AppStorage("collapse-long-posts") public var collapseLongPosts = true
@AppStorage("share-button-behavior") public var shareButtonBehavior: PreferredShareButtonBehavior = .linkOnly
@AppStorage("fast_refresh") public var fastRefreshEnabled: Bool = false
@AppStorage("max_reply_indentation") public var maxReplyIndentation: UInt = 7
@AppStorage("show_reply_indentation") public var showReplyIndentation: Bool = true
@AppStorage("show_account_popover") public var showAccountPopover: Bool = true
init() {}
}
public static let sharedDefault = UserDefaults(suiteName: "group.com.thomasricouard.IceCubesApp")
public static let shared = UserPreferences()
private let storage = Storage()
private var client: Client?
public var preferredBrowser: PreferredBrowser {
didSet {
storage.preferredBrowser = preferredBrowser
}
}
public var showTranslateButton: Bool {
didSet {
storage.showTranslateButton = showTranslateButton
}
}
public var pendingShownAtBottom: Bool {
didSet {
storage.pendingShownAtBottom = pendingShownAtBottom
}
}
public var pendingShownLeft: Bool {
didSet {
storage.pendingShownLeft = pendingShownLeft
}
}
public var pendingLocation: Alignment {
let fromLeft = Locale.current.language.characterDirection == .leftToRight ? pendingShownLeft : !pendingShownLeft
if pendingShownAtBottom {
if fromLeft {
return .bottomLeading
} else {
return .bottomTrailing
}
} else {
if fromLeft {
return .topLeading
} else {
return .topTrailing
}
}
}
public var isOpenAIEnabled: Bool {
didSet {
storage.isOpenAIEnabled = isOpenAIEnabled
}
}
public var recentlyUsedLanguages: [String] {
didSet {
storage.recentlyUsedLanguages = recentlyUsedLanguages
}
}
public var isSocialKeyboardEnabled: Bool {
didSet {
storage.isSocialKeyboardEnabled = isSocialKeyboardEnabled
}
}
public var useInstanceContentSettings: Bool {
didSet {
storage.useInstanceContentSettings = useInstanceContentSettings
}
}
public var appAutoExpandSpoilers: Bool {
didSet {
storage.appAutoExpandSpoilers = appAutoExpandSpoilers
}
}
public var appAutoExpandMedia: ServerPreferences.AutoExpandMedia {
didSet {
storage.appAutoExpandMedia = appAutoExpandMedia
}
}
public var appDefaultPostVisibility: Models.Visibility {
didSet {
storage.appDefaultPostVisibility = appDefaultPostVisibility
}
}
public var appDefaultReplyVisibility: Models.Visibility {
didSet {
storage.appDefaultReplyVisibility = appDefaultReplyVisibility
}
}
public var appDefaultPostsSensitive: Bool {
didSet {
storage.appDefaultPostsSensitive = appDefaultPostsSensitive
}
}
public var appRequireAltText: Bool {
didSet {
storage.appRequireAltText = appRequireAltText
}
}
public var autoPlayVideo: Bool {
didSet {
storage.autoPlayVideo = autoPlayVideo
}
}
public var muteVideo: Bool {
didSet {
storage.muteVideo = muteVideo
}
}
public var alwaysUseDeepl: Bool {
didSet {
storage.alwaysUseDeepl = alwaysUseDeepl
}
}
public var userDeeplAPIFree: Bool {
didSet {
storage.userDeeplAPIFree = userDeeplAPIFree
}
}
public var autoDetectPostLanguage: Bool {
didSet {
storage.autoDetectPostLanguage = autoDetectPostLanguage
}
}
public var inAppBrowserReaderView: Bool {
didSet {
storage.inAppBrowserReaderView = inAppBrowserReaderView
}
}
public var hapticTabSelectionEnabled: Bool {
didSet {
storage.hapticTabSelectionEnabled = hapticTabSelectionEnabled
}
}
public var hapticTimelineEnabled: Bool {
didSet {
storage.hapticTimelineEnabled = hapticTimelineEnabled
}
}
public var hapticButtonPressEnabled: Bool {
didSet {
storage.hapticButtonPressEnabled = hapticButtonPressEnabled
}
}
public var soundEffectEnabled: Bool {
didSet {
storage.soundEffectEnabled = soundEffectEnabled
}
}
public var showiPhoneTabLabel: Bool {
didSet {
storage.showiPhoneTabLabel = showiPhoneTabLabel
}
}
public var showAltTextForMedia: Bool {
didSet {
storage.showAltTextForMedia = showAltTextForMedia
}
}
public var showiPadSecondaryColumn: Bool {
didSet {
storage.showiPadSecondaryColumn = showiPadSecondaryColumn
}
}
public var swipeActionsStatusTrailingRight: StatusAction {
didSet {
storage.swipeActionsStatusTrailingRight = swipeActionsStatusTrailingRight
}
}
public var swipeActionsStatusTrailingLeft: StatusAction {
didSet {
storage.swipeActionsStatusTrailingLeft = swipeActionsStatusTrailingLeft
}
}
public var swipeActionsStatusLeadingLeft: StatusAction {
didSet {
storage.swipeActionsStatusLeadingLeft = swipeActionsStatusLeadingLeft
}
}
public var swipeActionsStatusLeadingRight: StatusAction {
didSet {
storage.swipeActionsStatusLeadingRight = swipeActionsStatusLeadingRight
}
}
public var swipeActionsUseThemeColor: Bool {
didSet {
storage.swipeActionsUseThemeColor = swipeActionsUseThemeColor
}
}
public var swipeActionsIconStyle: SwipeActionsIconStyle {
didSet {
storage.swipeActionsIconStyle = swipeActionsIconStyle
}
}
public var requestedReview: Bool {
didSet {
storage.requestedReview = requestedReview
}
}
public var collapseLongPosts: Bool {
didSet {
storage.collapseLongPosts = collapseLongPosts
}
}
public var shareButtonBehavior: PreferredShareButtonBehavior {
didSet {
storage.shareButtonBehavior = shareButtonBehavior
}
}
public var fastRefreshEnabled: Bool {
didSet {
storage.fastRefreshEnabled = fastRefreshEnabled
}
}
public var maxReplyIndentation: UInt {
didSet {
storage.maxReplyIndentation = maxReplyIndentation
}
}
public var showReplyIndentation: Bool {
didSet {
storage.showReplyIndentation = showReplyIndentation
}
}
public var showAccountPopover: Bool {
didSet {
storage.showAccountPopover = showAccountPopover
}
}
public func getRealMaxIndent() -> UInt {
showReplyIndentation ? maxReplyIndentation : 0
}
public enum SwipeActionsIconStyle: String, CaseIterable {
case iconWithText, iconOnly
public var description: LocalizedStringKey {
switch self {
case .iconWithText:
"enum.swipeactions.icon-with-text"
case .iconOnly:
"enum.swipeactions.icon-only"
}
}
// Have to implement this manually here due to compiler not implicitly
// inserting `nonisolated`, which leads to a warning:
//
// Main actor-isolated static property 'allCases' cannot be used to
// satisfy nonisolated protocol requirement
//
public nonisolated static var allCases: [Self] {
[.iconWithText, .iconOnly]
}
}
public var postVisibility: Models.Visibility {
if useInstanceContentSettings {
serverPreferences?.postVisibility ?? .pub
} else {
appDefaultPostVisibility
}
}
public func conformReplyVisibilityConstraints() {
appDefaultReplyVisibility = getReplyVisibility()
}
private func getReplyVisibility() -> Models.Visibility {
getMinVisibility(postVisibility, appDefaultReplyVisibility)
}
public func getReplyVisibility(of status: Status) -> Models.Visibility {
getMinVisibility(getReplyVisibility(), status.visibility)
}
private func getMinVisibility(_ vis1: Models.Visibility, _ vis2: Models.Visibility) -> Models.Visibility {
let no1 = Self.getIntOfVisibility(vis1)
let no2 = Self.getIntOfVisibility(vis2)
return no1 < no2 ? vis1 : vis2
}
public var postIsSensitive: Bool {
if useInstanceContentSettings {
serverPreferences?.postIsSensitive ?? false
} else {
appDefaultPostsSensitive
}
}
public var autoExpandSpoilers: Bool {
if useInstanceContentSettings {
serverPreferences?.autoExpandSpoilers ?? true
} else {
appAutoExpandSpoilers
}
}
public var autoExpandMedia: ServerPreferences.AutoExpandMedia {
if useInstanceContentSettings {
serverPreferences?.autoExpandMedia ?? .hideSensitive
} else {
appAutoExpandMedia
}
}
public var notificationsCount: [OauthToken: Int] = [:] {
didSet {
for (key, value) in notificationsCount {
Self.sharedDefault?.set(value, forKey: "push_notifications_count_\(key.createdAt)")
}
}
}
public var totalNotificationsCount: Int {
notificationsCount.compactMap { $0.value }.reduce(0, +)
}
public func reloadNotificationsCount(tokens: [OauthToken]) {
notificationsCount = [:]
for token in tokens {
notificationsCount[token] = Self.sharedDefault?.integer(forKey: "push_notifications_count_\(token.createdAt)") ?? 0
}
}
public var serverPreferences: ServerPreferences?
public func setClient(client: Client) {
self.client = client
Task {
await refreshServerPreferences()
}
}
public func refreshServerPreferences() async {
guard let client, client.isAuth else { return }
serverPreferences = try? await client.get(endpoint: Accounts.preferences)
}
public func markLanguageAsSelected(isoCode: String) {
var copy = recentlyUsedLanguages
if let index = copy.firstIndex(of: isoCode) {
copy.remove(at: index)
}
copy.insert(isoCode, at: 0)
recentlyUsedLanguages = Array(copy.prefix(3))
}
public static func getIntOfVisibility(_ vis: Models.Visibility) -> Int {
switch vis {
case .direct:
0
case .priv:
1
case .unlisted:
2
case .pub:
3
}
}
private init() {
preferredBrowser = storage.preferredBrowser
showTranslateButton = storage.showTranslateButton
isOpenAIEnabled = storage.isOpenAIEnabled
recentlyUsedLanguages = storage.recentlyUsedLanguages
isSocialKeyboardEnabled = storage.isSocialKeyboardEnabled
useInstanceContentSettings = storage.useInstanceContentSettings
appAutoExpandSpoilers = storage.appAutoExpandSpoilers
appAutoExpandMedia = storage.appAutoExpandMedia
appDefaultPostVisibility = storage.appDefaultPostVisibility
appDefaultReplyVisibility = storage.appDefaultReplyVisibility
appDefaultPostsSensitive = storage.appDefaultPostsSensitive
appRequireAltText = storage.appRequireAltText
autoPlayVideo = storage.autoPlayVideo
alwaysUseDeepl = storage.alwaysUseDeepl
userDeeplAPIFree = storage.userDeeplAPIFree
autoDetectPostLanguage = storage.autoDetectPostLanguage
inAppBrowserReaderView = storage.inAppBrowserReaderView
hapticTabSelectionEnabled = storage.hapticTabSelectionEnabled
hapticTimelineEnabled = storage.hapticTimelineEnabled
hapticButtonPressEnabled = storage.hapticButtonPressEnabled
soundEffectEnabled = storage.soundEffectEnabled
showiPhoneTabLabel = storage.showiPhoneTabLabel
showAltTextForMedia = storage.showAltTextForMedia
showiPadSecondaryColumn = storage.showiPadSecondaryColumn
swipeActionsStatusTrailingRight = storage.swipeActionsStatusTrailingRight
swipeActionsStatusTrailingLeft = storage.swipeActionsStatusTrailingLeft
swipeActionsStatusLeadingLeft = storage.swipeActionsStatusLeadingLeft
swipeActionsStatusLeadingRight = storage.swipeActionsStatusLeadingRight
swipeActionsUseThemeColor = storage.swipeActionsUseThemeColor
swipeActionsIconStyle = storage.swipeActionsIconStyle
requestedReview = storage.requestedReview
collapseLongPosts = storage.collapseLongPosts
shareButtonBehavior = storage.shareButtonBehavior
pendingShownAtBottom = storage.pendingShownAtBottom
pendingShownLeft = storage.pendingShownLeft
fastRefreshEnabled = storage.fastRefreshEnabled
maxReplyIndentation = storage.maxReplyIndentation
showReplyIndentation = storage.showReplyIndentation
showAccountPopover = storage.showAccountPopover
muteVideo = storage.muteVideo
}
}
extension UInt: RawRepresentable {
public var rawValue: Int {
Int(self)
}
public init?(rawValue: Int) {
if rawValue >= 0 {
self.init(rawValue)
} else {
return nil
}
}
}