Merge branch 'prefs-refactor' into develop
This commit is contained in:
commit
216e58e5ec
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "swift-system",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-system.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "025bcb1165deab2e20d4eaba79967ce73013f496",
|
||||||
|
"version" : "1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-url",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/karwa/swift-url.git",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "main",
|
||||||
|
"revision" : "01ad5a103d14839a68c55ee556513e5939008e9e"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
|
@ -24,5 +24,9 @@ let package = Package(
|
||||||
name: "TuskerPreferences",
|
name: "TuskerPreferences",
|
||||||
dependencies: ["Pachyderm"]
|
dependencies: ["Pachyderm"]
|
||||||
),
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "TuskerPreferencesTests",
|
||||||
|
dependencies: ["TuskerPreferences"]
|
||||||
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,282 @@
|
||||||
|
//
|
||||||
|
// Coding.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
private protocol PreferenceProtocol {
|
||||||
|
associatedtype Key: PreferenceKey
|
||||||
|
var storedValue: Key.Value? { get }
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Preference: PreferenceProtocol {
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PreferenceCoding<Wrapped: Codable>: Codable {
|
||||||
|
let wrapped: Wrapped
|
||||||
|
|
||||||
|
init(wrapped: Wrapped) {
|
||||||
|
self.wrapped = wrapped
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: any Decoder) throws {
|
||||||
|
self.wrapped = try Wrapped(from: PreferenceDecoder(wrapped: decoder))
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: any Encoder) throws {
|
||||||
|
try wrapped.encode(to: PreferenceEncoder(wrapped: encoder))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PreferenceDecoder: Decoder {
|
||||||
|
let wrapped: any Decoder
|
||||||
|
|
||||||
|
var codingPath: [any CodingKey] {
|
||||||
|
wrapped.codingPath
|
||||||
|
}
|
||||||
|
|
||||||
|
var userInfo: [CodingUserInfoKey : Any] {
|
||||||
|
wrapped.userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
|
||||||
|
KeyedDecodingContainer(PreferenceDecodingContainer(wrapped: try wrapped.container(keyedBy: type)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
||||||
|
throw Error.onlyKeyedContainerSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() throws -> any SingleValueDecodingContainer {
|
||||||
|
throw Error.onlyKeyedContainerSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Error: Swift.Error {
|
||||||
|
case onlyKeyedContainerSupported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PreferenceDecodingContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
|
||||||
|
let wrapped: KeyedDecodingContainer<Key>
|
||||||
|
|
||||||
|
var codingPath: [any CodingKey] {
|
||||||
|
wrapped.codingPath
|
||||||
|
}
|
||||||
|
|
||||||
|
var allKeys: [Key] {
|
||||||
|
wrapped.allKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(_ key: Key) -> Bool {
|
||||||
|
wrapped.contains(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeNil(forKey key: Key) throws -> Bool {
|
||||||
|
try wrapped.decodeNil(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: String.Type, forKey key: Key) throws -> String {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Double.Type, forKey key: Key) throws -> Double {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int.Type, forKey key: Key) throws -> Int {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
|
||||||
|
if let type = type as? any PreferenceProtocol.Type,
|
||||||
|
!contains(key) {
|
||||||
|
func makePreference<P: PreferenceProtocol>(_: P.Type) -> T {
|
||||||
|
P() as! T
|
||||||
|
}
|
||||||
|
return _openExistential(type, do: makePreference)
|
||||||
|
}
|
||||||
|
return try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||||
|
try wrapped.nestedContainer(keyedBy: type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedUnkeyedContainer(forKey key: Key) throws -> any UnkeyedDecodingContainer {
|
||||||
|
try wrapped.nestedUnkeyedContainer(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func superDecoder() throws -> any Decoder {
|
||||||
|
try wrapped.superDecoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func superDecoder(forKey key: Key) throws -> any Decoder {
|
||||||
|
try wrapped.superDecoder(forKey: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PreferenceEncoder: Encoder {
|
||||||
|
let wrapped: any Encoder
|
||||||
|
|
||||||
|
var codingPath: [any CodingKey] {
|
||||||
|
wrapped.codingPath
|
||||||
|
}
|
||||||
|
|
||||||
|
var userInfo: [CodingUserInfoKey : Any] {
|
||||||
|
wrapped.userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey {
|
||||||
|
KeyedEncodingContainer(PreferenceEncodingContainer(wrapped: wrapped.container(keyedBy: type)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||||
|
fatalError("Only keyed containers supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() -> any SingleValueEncodingContainer {
|
||||||
|
fatalError("Only keyed containers supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PreferenceEncodingContainer<Key: CodingKey>: KeyedEncodingContainerProtocol {
|
||||||
|
var wrapped: KeyedEncodingContainer<Key>
|
||||||
|
|
||||||
|
var codingPath: [any CodingKey] {
|
||||||
|
wrapped.codingPath
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encodeNil(forKey key: Key) throws {
|
||||||
|
try wrapped.encodeNil(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: Bool, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: String, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: Double, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: Float, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: Int, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: Int8, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: Int16, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: Int32, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: Int64, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: UInt, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: UInt8, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: UInt16, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: UInt32, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: UInt64, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode<T>(_ value: T, forKey key: Key) throws where T : Encodable {
|
||||||
|
if let value = value as? any PreferenceProtocol,
|
||||||
|
value.storedValue == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||||
|
wrapped.nestedContainer(keyedBy: keyType, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer {
|
||||||
|
wrapped.nestedUnkeyedContainer(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func superEncoder() -> any Encoder {
|
||||||
|
wrapped.superEncoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func superEncoder(forKey key: Key) -> any Encoder {
|
||||||
|
wrapped.superEncoder(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
//
|
||||||
|
// AdvancedKeys.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
struct StatusContentTypeKey: PreferenceKey {
|
||||||
|
static var defaultValue: StatusContentType { .plain }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FeatureFlagsKey: PreferenceKey {
|
||||||
|
static var defaultValue: Set<FeatureFlag> { [] }
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
//
|
||||||
|
// AppearanceKeys.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct ThemeKey: PreferenceKey {
|
||||||
|
static var defaultValue: Theme { .unspecified }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AccentColorKey: PreferenceKey {
|
||||||
|
static var defaultValue: AccentColor { .default }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AvatarStyleKey: PreferenceKey {
|
||||||
|
static var defaultValue: AvatarStyle { .roundRect }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LeadingSwipeActionsKey: PreferenceKey {
|
||||||
|
static var defaultValue: [StatusSwipeAction] { [.favorite, .reblog] }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TrailingSwipeActionsKey: PreferenceKey {
|
||||||
|
static var defaultValue: [StatusSwipeAction] { [.reply, .share] }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WidescreenNavigationModeKey: PreferenceKey {
|
||||||
|
static var defaultValue: WidescreenNavigationMode { .splitScreen }
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
//
|
||||||
|
// BehaviorKeys.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct OppositeCollapseKeywordsKey: PreferenceKey {
|
||||||
|
static var defaultValue: [String] { [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConfirmReblogKey: PreferenceKey {
|
||||||
|
static var defaultValue: Bool {
|
||||||
|
#if os(visionOS)
|
||||||
|
true
|
||||||
|
#else
|
||||||
|
false
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TimelineSyncModeKey: PreferenceKey {
|
||||||
|
static var defaultValue: TimelineSyncMode { .icloud }
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
//
|
||||||
|
// CommonKeys.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct TrueKey: PreferenceKey {
|
||||||
|
static var defaultValue: Bool { true }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FalseKey: PreferenceKey {
|
||||||
|
static var defaultValue: Bool { false }
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
//
|
||||||
|
// ComposingKeys.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct PostVisibilityKey: PreferenceKey {
|
||||||
|
static var defaultValue: PostVisibility { .serverDefault }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ReplyVisibilityKey: PreferenceKey {
|
||||||
|
static var defaultValue: ReplyVisibility { .sameAsPost }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContentWarningCopyModeKey: PreferenceKey {
|
||||||
|
static var defaultValue: ContentWarningCopyMode { .asIs }
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
//
|
||||||
|
// DigitalWellnessKeys.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct NotificationsModeKey: PreferenceKey {
|
||||||
|
static var defaultValue: NotificationsMode { .allNotifications }
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
//
|
||||||
|
// MediaKeys.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct AttachmentBlurModeKey: PreferenceKey {
|
||||||
|
static var defaultValue: AttachmentBlurMode { .useStatusSetting }
|
||||||
|
|
||||||
|
static func didSet(in store: PreferenceStore, newValue: AttachmentBlurMode) {
|
||||||
|
if newValue == .always {
|
||||||
|
store.blurMediaBehindContentWarning = true
|
||||||
|
} else if newValue == .never {
|
||||||
|
store.blurMediaBehindContentWarning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,205 @@
|
||||||
|
//
|
||||||
|
// LegacyPreferences.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/28/18.
|
||||||
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
public final class LegacyPreferences: Decodable {
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
public 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.widescreenNavigationMode = try container.decodeIfPresent(WidescreenNavigationMode.self, forKey: .widescreenNavigationMode) ?? Self.defaultWidescreenNavigationMode
|
||||||
|
self.underlineTextLinks = try container.decodeIfPresent(Bool.self, forKey: .underlineTextLinks) ?? false
|
||||||
|
self.showAttachmentsInTimeline = try container.decodeIfPresent(Bool.self, forKey: .showAttachmentsInTimeline) ?? true
|
||||||
|
|
||||||
|
if let existing = try? container.decode(Visibility.self, forKey: .defaultPostVisibility) {
|
||||||
|
self.defaultPostVisibility = .visibility(existing)
|
||||||
|
} else {
|
||||||
|
self.defaultPostVisibility = try container.decode(PostVisibility.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
|
||||||
|
|
||||||
|
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.attachmentAltBadgeInverted = try container.decodeIfPresent(Bool.self, forKey: .attachmentAltBadgeInverted) ?? false
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
self.hasShownLocalTimelineDescription = try container.decodeIfPresent(Bool.self, forKey: .hasShownLocalTimelineDescription) ?? false
|
||||||
|
self.hasShownFederatedTimelineDescription = try container.decodeIfPresent(Bool.self, forKey: .hasShownFederatedTimelineDescription) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Appearance
|
||||||
|
@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 = LegacyPreferences.defaultWidescreenNavigationMode
|
||||||
|
@Published public var underlineTextLinks = false
|
||||||
|
@Published public var showAttachmentsInTimeline = true
|
||||||
|
|
||||||
|
// MARK: Composing
|
||||||
|
@Published public var defaultPostVisibility = PostVisibility.serverDefault
|
||||||
|
@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
|
||||||
|
|
||||||
|
// MARK: Media
|
||||||
|
@Published public var attachmentBlurMode = AttachmentBlurMode.useStatusSetting
|
||||||
|
@Published public var blurMediaBehindContentWarning = true
|
||||||
|
@Published public var automaticallyPlayGifs = true
|
||||||
|
@Published public var showUncroppedMediaInline = true
|
||||||
|
@Published public var showAttachmentBadges = true
|
||||||
|
@Published public var attachmentAltBadgeInverted = false
|
||||||
|
|
||||||
|
// MARK: Behavior
|
||||||
|
@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
|
||||||
|
|
||||||
|
// MARK: Digital Wellness
|
||||||
|
@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
|
||||||
|
|
||||||
|
// MARK: Advanced
|
||||||
|
@Published public var statusContentType: StatusContentType = .plain
|
||||||
|
@Published public var reportErrorsAutomatically = true
|
||||||
|
@Published public var enabledFeatureFlags: Set<FeatureFlag> = []
|
||||||
|
|
||||||
|
// MARK:
|
||||||
|
@Published public var hasShownLocalTimelineDescription = false
|
||||||
|
@Published public 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 widescreenNavigationMode
|
||||||
|
case underlineTextLinks
|
||||||
|
case showAttachmentsInTimeline
|
||||||
|
|
||||||
|
case defaultPostVisibility
|
||||||
|
case defaultReplyVisibility
|
||||||
|
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 attachmentAltBadgeInverted
|
||||||
|
|
||||||
|
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 reportErrorsAutomatically
|
||||||
|
case enabledFeatureFlags
|
||||||
|
|
||||||
|
case hasShownLocalTimelineDescription
|
||||||
|
case hasShownFederatedTimelineDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIUserInterfaceStyle: Codable {}
|
|
@ -0,0 +1,102 @@
|
||||||
|
//
|
||||||
|
// PreferenceStore+Migrate.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension PreferenceStore {
|
||||||
|
func migrate(from legacy: LegacyPreferences) {
|
||||||
|
let migrations: [any MigrationProtocol] = [
|
||||||
|
Migration(from: \.theme.theme, to: \.$theme),
|
||||||
|
Migration(from: \.pureBlackDarkMode, to: \.$pureBlackDarkMode),
|
||||||
|
Migration(from: \.accentColor, to: \.$accentColor),
|
||||||
|
Migration(from: \.avatarStyle, to: \.$avatarStyle),
|
||||||
|
Migration(from: \.hideCustomEmojiInUsernames, to: \.$hideCustomEmojiInUsernames),
|
||||||
|
Migration(from: \.showIsStatusReplyIcon, to: \.$showIsStatusReplyIcon),
|
||||||
|
Migration(from: \.alwaysShowStatusVisibilityIcon, to: \.$alwaysShowStatusVisibilityIcon),
|
||||||
|
Migration(from: \.hideActionsInTimeline, to: \.$hideActionsInTimeline),
|
||||||
|
Migration(from: \.showLinkPreviews, to: \.$showLinkPreviews),
|
||||||
|
Migration(from: \.leadingStatusSwipeActions, to: \.$leadingStatusSwipeActions),
|
||||||
|
Migration(from: \.trailingStatusSwipeActions, to: \.$trailingStatusSwipeActions),
|
||||||
|
Migration(from: \.widescreenNavigationMode, to: \.$widescreenNavigationMode),
|
||||||
|
Migration(from: \.underlineTextLinks, to: \.$underlineTextLinks),
|
||||||
|
Migration(from: \.showAttachmentsInTimeline, to: \.$showAttachmentsInTimeline),
|
||||||
|
|
||||||
|
Migration(from: \.defaultPostVisibility, to: \.$defaultPostVisibility),
|
||||||
|
Migration(from: \.defaultReplyVisibility, to: \.$defaultReplyVisibility),
|
||||||
|
Migration(from: \.requireAttachmentDescriptions, to: \.$requireAttachmentDescriptions),
|
||||||
|
Migration(from: \.contentWarningCopyMode, to: \.$contentWarningCopyMode),
|
||||||
|
Migration(from: \.mentionReblogger, to: \.$mentionReblogger),
|
||||||
|
Migration(from: \.useTwitterKeyboard, to: \.$useTwitterKeyboard),
|
||||||
|
|
||||||
|
Migration(from: \.attachmentBlurMode, to: \.$attachmentBlurMode),
|
||||||
|
Migration(from: \.blurMediaBehindContentWarning, to: \.$blurMediaBehindContentWarning),
|
||||||
|
Migration(from: \.automaticallyPlayGifs, to: \.$automaticallyPlayGifs),
|
||||||
|
Migration(from: \.showUncroppedMediaInline, to: \.$showUncroppedMediaInline),
|
||||||
|
Migration(from: \.showAttachmentBadges, to: \.$showAttachmentBadges),
|
||||||
|
Migration(from: \.attachmentAltBadgeInverted, to: \.$attachmentAltBadgeInverted),
|
||||||
|
|
||||||
|
Migration(from: \.openLinksInApps, to: \.$openLinksInApps),
|
||||||
|
Migration(from: \.useInAppSafari, to: \.$useInAppSafari),
|
||||||
|
Migration(from: \.inAppSafariAutomaticReaderMode, to: \.$inAppSafariAutomaticReaderMode),
|
||||||
|
Migration(from: \.expandAllContentWarnings, to: \.$expandAllContentWarnings),
|
||||||
|
Migration(from: \.collapseLongPosts, to: \.$collapseLongPosts),
|
||||||
|
Migration(from: \.oppositeCollapseKeywords, to: \.$oppositeCollapseKeywords),
|
||||||
|
Migration(from: \.confirmBeforeReblog, to: \.$confirmBeforeReblog),
|
||||||
|
Migration(from: \.timelineStateRestoration, to: \.$timelineStateRestoration),
|
||||||
|
Migration(from: \.timelineSyncMode, to: \.$timelineSyncMode),
|
||||||
|
Migration(from: \.hideReblogsInTimelines, to: \.$hideReblogsInTimelines),
|
||||||
|
Migration(from: \.hideRepliesInTimelines, to: \.$hideRepliesInTimelines),
|
||||||
|
|
||||||
|
Migration(from: \.showFavoriteAndReblogCounts, to: \.$showFavoriteAndReblogCounts),
|
||||||
|
Migration(from: \.defaultNotificationsMode, to: \.$defaultNotificationsMode),
|
||||||
|
Migration(from: \.grayscaleImages, to: \.$grayscaleImages),
|
||||||
|
Migration(from: \.disableInfiniteScrolling, to: \.$disableInfiniteScrolling),
|
||||||
|
Migration(from: \.hideTrends, to: \.$hideTrends),
|
||||||
|
|
||||||
|
Migration(from: \.statusContentType, to: \.$statusContentType),
|
||||||
|
Migration(from: \.reportErrorsAutomatically, to: \.$reportErrorsAutomatically),
|
||||||
|
Migration(from: \.enabledFeatureFlags, to: \.$enabledFeatureFlags),
|
||||||
|
|
||||||
|
Migration(from: \.hasShownLocalTimelineDescription, to: \.$hasShownLocalTimelineDescription),
|
||||||
|
Migration(from: \.hasShownFederatedTimelineDescription, to: \.$hasShownFederatedTimelineDescription),
|
||||||
|
]
|
||||||
|
|
||||||
|
for migration in migrations {
|
||||||
|
migration.migrate(from: legacy, to: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private protocol MigrationProtocol {
|
||||||
|
func migrate(from legacy: LegacyPreferences, to store: PreferenceStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Migration<Key: PreferenceKey>: MigrationProtocol where Key.Value: Equatable {
|
||||||
|
let from: KeyPath<LegacyPreferences, Key.Value>
|
||||||
|
let to: KeyPath<PreferenceStore, PreferencePublisher<Key>>
|
||||||
|
|
||||||
|
func migrate(from legacy: LegacyPreferences, to store: PreferenceStore) {
|
||||||
|
let value = legacy[keyPath: from]
|
||||||
|
if value != Key.defaultValue {
|
||||||
|
Preference.set(enclosingInstance: store, storage: to.appending(path: \.preference), newValue: value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension UIUserInterfaceStyle {
|
||||||
|
var theme: Theme {
|
||||||
|
switch self {
|
||||||
|
case .light:
|
||||||
|
.light
|
||||||
|
case .dark:
|
||||||
|
.dark
|
||||||
|
default:
|
||||||
|
.unspecified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
//
|
||||||
|
// Preference.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
// TODO: once we target iOS 17, use Observable for this
|
||||||
|
@propertyWrapper
|
||||||
|
final class Preference<Key: PreferenceKey>: Codable {
|
||||||
|
@Published private(set) var storedValue: Key.Value?
|
||||||
|
|
||||||
|
var wrappedValue: Key.Value {
|
||||||
|
get {
|
||||||
|
storedValue ?? Key.defaultValue
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
fatalError("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.storedValue = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: any Decoder) throws {
|
||||||
|
if let container = try? decoder.singleValueContainer() {
|
||||||
|
self.storedValue = try? container.decode(Key.Value.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: any Encoder) throws {
|
||||||
|
if let storedValue {
|
||||||
|
var container = encoder.singleValueContainer()
|
||||||
|
try container.encode(storedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static subscript(
|
||||||
|
_enclosingInstance instance: PreferenceStore,
|
||||||
|
wrapped wrappedKeyPath: ReferenceWritableKeyPath<PreferenceStore, Key.Value>,
|
||||||
|
storage storageKeyPath: ReferenceWritableKeyPath<PreferenceStore, Preference>
|
||||||
|
) -> Key.Value {
|
||||||
|
get {
|
||||||
|
get(enclosingInstance: instance, storage: storageKeyPath)
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
set(enclosingInstance: instance, storage: storageKeyPath, newValue: newValue)
|
||||||
|
Key.didSet(in: instance, newValue: newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for testing only
|
||||||
|
@inline(__always)
|
||||||
|
static func get<Enclosing>(
|
||||||
|
enclosingInstance: Enclosing,
|
||||||
|
storage: KeyPath<Enclosing, Preference>
|
||||||
|
) -> Key.Value where Enclosing: ObservableObject, Enclosing.ObjectWillChangePublisher == ObservableObjectPublisher {
|
||||||
|
let pref = enclosingInstance[keyPath: storage]
|
||||||
|
return pref.storedValue ?? Key.defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// for testing only
|
||||||
|
@inline(__always)
|
||||||
|
static func set<Enclosing>(
|
||||||
|
enclosingInstance: Enclosing,
|
||||||
|
storage: KeyPath<Enclosing, Preference>,
|
||||||
|
newValue: Key.Value
|
||||||
|
) where Enclosing: ObservableObject, Enclosing.ObjectWillChangePublisher == ObservableObjectPublisher {
|
||||||
|
enclosingInstance.objectWillChange.send()
|
||||||
|
let pref = enclosingInstance[keyPath: storage]
|
||||||
|
pref.storedValue = newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
var projectedValue: PreferencePublisher<Key> {
|
||||||
|
.init(preference: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PreferencePublisher<Key: PreferenceKey>: Publisher {
|
||||||
|
typealias Output = Key.Value
|
||||||
|
typealias Failure = Never
|
||||||
|
|
||||||
|
let preference: Preference<Key>
|
||||||
|
|
||||||
|
func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, Key.Value == S.Input {
|
||||||
|
preference.$storedValue.map { $0 ?? Key.defaultValue }.receive(subscriber: subscriber)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
//
|
||||||
|
// PreferenceKey.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/12/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public protocol PreferenceKey {
|
||||||
|
associatedtype Value: Codable
|
||||||
|
|
||||||
|
static var defaultValue: Value { get }
|
||||||
|
|
||||||
|
static func didSet(in store: PreferenceStore, newValue: Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PreferenceKey {
|
||||||
|
static func didSet(in store: PreferenceStore, newValue: Value) {}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
//
|
||||||
|
// PreferenceStore.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/12/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
public final class PreferenceStore: ObservableObject, Codable {
|
||||||
|
// MARK: Appearance
|
||||||
|
@Preference<ThemeKey> public var theme
|
||||||
|
@Preference<TrueKey> public var pureBlackDarkMode
|
||||||
|
@Preference<AccentColorKey> public var accentColor
|
||||||
|
@Preference<AvatarStyleKey> public var avatarStyle
|
||||||
|
@Preference<FalseKey> public var hideCustomEmojiInUsernames
|
||||||
|
@Preference<FalseKey> public var showIsStatusReplyIcon
|
||||||
|
@Preference<FalseKey> public var alwaysShowStatusVisibilityIcon
|
||||||
|
@Preference<FalseKey> public var hideActionsInTimeline
|
||||||
|
@Preference<TrueKey> public var showLinkPreviews
|
||||||
|
@Preference<LeadingSwipeActionsKey> public var leadingStatusSwipeActions
|
||||||
|
@Preference<TrailingSwipeActionsKey> public var trailingStatusSwipeActions
|
||||||
|
@Preference<WidescreenNavigationModeKey> public var widescreenNavigationMode
|
||||||
|
@Preference<FalseKey> public var underlineTextLinks
|
||||||
|
@Preference<TrueKey> public var showAttachmentsInTimeline
|
||||||
|
|
||||||
|
// MARK: Composing
|
||||||
|
@Preference<PostVisibilityKey> public var defaultPostVisibility
|
||||||
|
@Preference<ReplyVisibilityKey> public var defaultReplyVisibility
|
||||||
|
@Preference<FalseKey> public var requireAttachmentDescriptions
|
||||||
|
@Preference<ContentWarningCopyModeKey> public var contentWarningCopyMode
|
||||||
|
@Preference<FalseKey> public var mentionReblogger
|
||||||
|
@Preference<FalseKey> public var useTwitterKeyboard
|
||||||
|
|
||||||
|
// MARK: Media
|
||||||
|
@Preference<AttachmentBlurModeKey> public var attachmentBlurMode
|
||||||
|
@Preference<TrueKey> public var blurMediaBehindContentWarning
|
||||||
|
@Preference<TrueKey> public var automaticallyPlayGifs
|
||||||
|
@Preference<TrueKey> public var showUncroppedMediaInline
|
||||||
|
@Preference<TrueKey> public var showAttachmentBadges
|
||||||
|
@Preference<FalseKey> public var attachmentAltBadgeInverted
|
||||||
|
|
||||||
|
// MARK: Behavior
|
||||||
|
@Preference<TrueKey> public var openLinksInApps
|
||||||
|
@Preference<TrueKey> public var useInAppSafari
|
||||||
|
@Preference<FalseKey> public var inAppSafariAutomaticReaderMode
|
||||||
|
@Preference<FalseKey> public var expandAllContentWarnings
|
||||||
|
@Preference<TrueKey> public var collapseLongPosts
|
||||||
|
@Preference<OppositeCollapseKeywordsKey> public var oppositeCollapseKeywords
|
||||||
|
@Preference<ConfirmReblogKey> public var confirmBeforeReblog
|
||||||
|
@Preference<TrueKey> public var timelineStateRestoration
|
||||||
|
@Preference<TimelineSyncModeKey> public var timelineSyncMode
|
||||||
|
@Preference<FalseKey> public var hideReblogsInTimelines
|
||||||
|
@Preference<FalseKey> public var hideRepliesInTimelines
|
||||||
|
|
||||||
|
// MARK: Digital Wellness
|
||||||
|
@Preference<TrueKey> public var showFavoriteAndReblogCounts
|
||||||
|
@Preference<NotificationsModeKey> public var defaultNotificationsMode
|
||||||
|
@Preference<FalseKey> public var grayscaleImages
|
||||||
|
@Preference<FalseKey> public var disableInfiniteScrolling
|
||||||
|
@Preference<FalseKey> public var hideTrends
|
||||||
|
|
||||||
|
// MARK: Advanced
|
||||||
|
@Preference<StatusContentTypeKey> public var statusContentType
|
||||||
|
@Preference<TrueKey> public var reportErrorsAutomatically
|
||||||
|
@Preference<FeatureFlagsKey> public var enabledFeatureFlags
|
||||||
|
|
||||||
|
// MARK: Internal
|
||||||
|
@Preference<FalseKey> public var hasShownLocalTimelineDescription
|
||||||
|
@Preference<FalseKey> public var hasShownFederatedTimelineDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PreferenceStore {
|
||||||
|
public func hasFeatureFlag(_ flag: FeatureFlag) -> Bool {
|
||||||
|
enabledFeatureFlags.contains(flag)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,430 +2,40 @@
|
||||||
// Preferences.swift
|
// Preferences.swift
|
||||||
// TuskerPreferences
|
// TuskerPreferences
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 8/28/18.
|
// Created by Shadowfacts on 4/12/24.
|
||||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import Foundation
|
||||||
import Pachyderm
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
public final class Preferences: Codable, ObservableObject {
|
public struct Preferences {
|
||||||
|
public static let shared: PreferenceStore = load()
|
||||||
@MainActor
|
|
||||||
public static var shared: Preferences = load()
|
|
||||||
|
|
||||||
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||||
private static var appGroupDirectory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!
|
private static var appGroupDirectory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!
|
||||||
private static var archiveURL = appGroupDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
|
private static var legacyURL = appGroupDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
|
||||||
|
private static var preferencesURL = appGroupDirectory.appendingPathComponent("preferences.v2").appendingPathExtension("plist")
|
||||||
@MainActor
|
private static var nonAppGroupURL = documentsDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
|
||||||
public static func save() {
|
|
||||||
let encoder = PropertyListEncoder()
|
|
||||||
let data = try? encoder.encode(shared)
|
|
||||||
try? data?.write(to: archiveURL, options: .noFileProtection)
|
|
||||||
}
|
|
||||||
|
|
||||||
public 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
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() {}
|
private init() {}
|
||||||
|
|
||||||
public required init(from decoder: Decoder) throws {
|
public static func save() {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let encoder = PropertyListEncoder()
|
||||||
|
let data = try? encoder.encode(PreferenceCoding(wrapped: shared))
|
||||||
|
try? data?.write(to: preferencesURL, options: .noFileProtection)
|
||||||
|
}
|
||||||
|
|
||||||
self.theme = try container.decode(UIUserInterfaceStyle.self, forKey: .theme)
|
private static func load() -> PreferenceStore {
|
||||||
self.pureBlackDarkMode = try container.decodeIfPresent(Bool.self, forKey: .pureBlackDarkMode) ?? true
|
let decoder = PropertyListDecoder()
|
||||||
self.accentColor = try container.decodeIfPresent(AccentColor.self, forKey: .accentColor) ?? .default
|
if let data = try? Data(contentsOf: preferencesURL),
|
||||||
self.avatarStyle = try container.decode(AvatarStyle.self, forKey: .avatarStyle)
|
let store = try? decoder.decode(PreferenceCoding<PreferenceStore>.self, from: data) {
|
||||||
self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames)
|
return store.wrapped
|
||||||
self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon)
|
} else if let legacyData = (try? Data(contentsOf: legacyURL)) ?? (try? Data(contentsOf: nonAppGroupURL)),
|
||||||
self.alwaysShowStatusVisibilityIcon = try container.decode(Bool.self, forKey: .alwaysShowStatusVisibilityIcon)
|
let legacy = try? decoder.decode(LegacyPreferences.self, from: legacyData) {
|
||||||
self.hideActionsInTimeline = try container.decodeIfPresent(Bool.self, forKey: .hideActionsInTimeline) ?? false
|
let store = PreferenceStore()
|
||||||
self.showLinkPreviews = try container.decodeIfPresent(Bool.self, forKey: .showLinkPreviews) ?? true
|
store.migrate(from: legacy)
|
||||||
self.leadingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .leadingStatusSwipeActions) ?? leadingStatusSwipeActions
|
return store
|
||||||
self.trailingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .trailingStatusSwipeActions) ?? trailingStatusSwipeActions
|
|
||||||
self.widescreenNavigationMode = try container.decodeIfPresent(WidescreenNavigationMode.self, forKey: .widescreenNavigationMode) ?? Self.defaultWidescreenNavigationMode
|
|
||||||
self.underlineTextLinks = try container.decodeIfPresent(Bool.self, forKey: .underlineTextLinks) ?? false
|
|
||||||
self.showAttachmentsInTimeline = try container.decodeIfPresent(Bool.self, forKey: .showAttachmentsInTimeline) ?? true
|
|
||||||
|
|
||||||
if let existing = try? container.decode(Visibility.self, forKey: .defaultPostVisibility) {
|
|
||||||
self.defaultPostVisibility = .visibility(existing)
|
|
||||||
} else {
|
} else {
|
||||||
self.defaultPostVisibility = try container.decode(PostVisibility.self, forKey: .defaultPostVisibility)
|
return PreferenceStore()
|
||||||
}
|
|
||||||
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
|
|
||||||
|
|
||||||
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.attachmentAltBadgeInverted = try container.decodeIfPresent(Bool.self, forKey: .attachmentAltBadgeInverted) ?? false
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
self.hasShownLocalTimelineDescription = try container.decodeIfPresent(Bool.self, forKey: .hasShownLocalTimelineDescription) ?? false
|
|
||||||
self.hasShownFederatedTimelineDescription = try container.decodeIfPresent(Bool.self, forKey: .hasShownFederatedTimelineDescription) ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
public 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(widescreenNavigationMode, forKey: .widescreenNavigationMode)
|
|
||||||
try container.encode(underlineTextLinks, forKey: .underlineTextLinks)
|
|
||||||
try container.encode(showAttachmentsInTimeline, forKey: .showAttachmentsInTimeline)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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(attachmentAltBadgeInverted, forKey: .attachmentAltBadgeInverted)
|
|
||||||
|
|
||||||
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(reportErrorsAutomatically, forKey: .reportErrorsAutomatically)
|
|
||||||
try container.encode(enabledFeatureFlags, forKey: .enabledFeatureFlags)
|
|
||||||
|
|
||||||
try container.encode(hasShownLocalTimelineDescription, forKey: .hasShownLocalTimelineDescription)
|
|
||||||
try container.encode(hasShownFederatedTimelineDescription, forKey: .hasShownFederatedTimelineDescription)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Appearance
|
|
||||||
@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
|
|
||||||
@Published public var underlineTextLinks = false
|
|
||||||
@Published public var showAttachmentsInTimeline = true
|
|
||||||
|
|
||||||
// MARK: Composing
|
|
||||||
@Published public var defaultPostVisibility = PostVisibility.serverDefault
|
|
||||||
@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
|
|
||||||
|
|
||||||
// MARK: Media
|
|
||||||
@Published public var attachmentBlurMode = AttachmentBlurMode.useStatusSetting {
|
|
||||||
didSet {
|
|
||||||
if attachmentBlurMode == .always {
|
|
||||||
blurMediaBehindContentWarning = true
|
|
||||||
} else if attachmentBlurMode == .never {
|
|
||||||
blurMediaBehindContentWarning = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Published public var blurMediaBehindContentWarning = true
|
|
||||||
@Published public var automaticallyPlayGifs = true
|
|
||||||
@Published public var showUncroppedMediaInline = true
|
|
||||||
@Published public var showAttachmentBadges = true
|
|
||||||
@Published public var attachmentAltBadgeInverted = false
|
|
||||||
|
|
||||||
// MARK: Behavior
|
|
||||||
@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
|
|
||||||
|
|
||||||
// MARK: Digital Wellness
|
|
||||||
@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
|
|
||||||
|
|
||||||
// MARK: Advanced
|
|
||||||
@Published public var statusContentType: StatusContentType = .plain
|
|
||||||
@Published public var reportErrorsAutomatically = true
|
|
||||||
@Published public var enabledFeatureFlags: Set<FeatureFlag> = []
|
|
||||||
|
|
||||||
// MARK:
|
|
||||||
@Published public var hasShownLocalTimelineDescription = false
|
|
||||||
@Published public var hasShownFederatedTimelineDescription = false
|
|
||||||
|
|
||||||
public func hasFeatureFlag(_ flag: FeatureFlag) -> Bool {
|
|
||||||
enabledFeatureFlags.contains(flag)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 widescreenNavigationMode
|
|
||||||
case underlineTextLinks
|
|
||||||
case showAttachmentsInTimeline
|
|
||||||
|
|
||||||
case defaultPostVisibility
|
|
||||||
case defaultReplyVisibility
|
|
||||||
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 attachmentAltBadgeInverted
|
|
||||||
|
|
||||||
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 reportErrorsAutomatically
|
|
||||||
case enabledFeatureFlags
|
|
||||||
|
|
||||||
case hasShownLocalTimelineDescription
|
|
||||||
case hasShownFederatedTimelineDescription
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Preferences {
|
|
||||||
public enum AttachmentBlurMode: Codable, Hashable, CaseIterable {
|
|
||||||
case useStatusSetting
|
|
||||||
case always
|
|
||||||
case never
|
|
||||||
|
|
||||||
public var displayName: String {
|
|
||||||
switch self {
|
|
||||||
case .useStatusSetting:
|
|
||||||
return "Default"
|
|
||||||
case .always:
|
|
||||||
return "Always"
|
|
||||||
case .never:
|
|
||||||
return "Never"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension UIUserInterfaceStyle: Codable {}
|
|
||||||
|
|
||||||
extension Preferences {
|
|
||||||
public 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
|
|
||||||
|
|
||||||
public 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public 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 {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
//
|
||||||
|
// AccentColor.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public 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
|
||||||
|
|
||||||
|
public 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
//
|
||||||
|
// AttachmentBlurMode.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum AttachmentBlurMode: Codable, Hashable, CaseIterable {
|
||||||
|
case useStatusSetting
|
||||||
|
case always
|
||||||
|
case never
|
||||||
|
|
||||||
|
public var displayName: String {
|
||||||
|
switch self {
|
||||||
|
case .useStatusSetting:
|
||||||
|
return "Default"
|
||||||
|
case .always:
|
||||||
|
return "Always"
|
||||||
|
case .never:
|
||||||
|
return "Never"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
//
|
||||||
|
// FeatureFlag.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum FeatureFlag: String, Codable {
|
||||||
|
case iPadMultiColumn = "ipad-multi-column"
|
||||||
|
case iPadBrowserNavigation = "ipad-browser-navigation"
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
//
|
||||||
|
// Theme.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public enum Theme: String, Codable {
|
||||||
|
case unspecified, light, dark
|
||||||
|
|
||||||
|
public var userInterfaceStyle: UIUserInterfaceStyle {
|
||||||
|
switch self {
|
||||||
|
case .unspecified:
|
||||||
|
.unspecified
|
||||||
|
case .light:
|
||||||
|
.light
|
||||||
|
case .dark:
|
||||||
|
.dark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
//
|
||||||
|
// TimelineSyncMode.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum TimelineSyncMode: String, Codable {
|
||||||
|
case mastodon
|
||||||
|
case icloud
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
//
|
||||||
|
// WidescreenNavigationMode.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum WidescreenNavigationMode: String, Codable {
|
||||||
|
case stack
|
||||||
|
case splitScreen
|
||||||
|
case multiColumn
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
//
|
||||||
|
// PreferenceStoreTests.swift
|
||||||
|
// TuskerPreferencesTests
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/12/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import TuskerPreferences
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
final class PreferenceStoreTests: XCTestCase {
|
||||||
|
|
||||||
|
struct TestKey: PreferenceKey {
|
||||||
|
static let defaultValue = false
|
||||||
|
}
|
||||||
|
|
||||||
|
final class TestStore: Codable, ObservableObject {
|
||||||
|
private var _test = Preference<TestKey>()
|
||||||
|
|
||||||
|
// the acutal subscript expects the enclosingInstance to be a PreferenceStore, so do it manually
|
||||||
|
var test: Bool {
|
||||||
|
get {
|
||||||
|
Preference.get(enclosingInstance: self, storage: \._test)
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
Preference.set(enclosingInstance: self, storage: \._test, newValue: newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var testPublisher: some Publisher<TestKey.Value, Never> {
|
||||||
|
_test.projectedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: any Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self._test = try container.decode(Preference<TestKey>.self, forKey: .test)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CodingKeys: CodingKey {
|
||||||
|
case test
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: any Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(self._test, forKey: .test)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecoding() throws {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
let present = try decoder.decode(PreferenceCoding<TestStore>.self, from: Data("""
|
||||||
|
{"test": true}
|
||||||
|
""".utf8)).wrapped
|
||||||
|
XCTAssertEqual(present.test, true)
|
||||||
|
let absent = try decoder.decode(PreferenceCoding<TestStore>.self, from: Data("""
|
||||||
|
{}
|
||||||
|
""".utf8)).wrapped
|
||||||
|
XCTAssertEqual(absent.test, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncoding() throws {
|
||||||
|
let store = TestStore()
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
XCTAssertEqual(String(data: try encoder.encode(PreferenceCoding(wrapped: store)), encoding: .utf8)!, """
|
||||||
|
{}
|
||||||
|
""")
|
||||||
|
store.test = true
|
||||||
|
XCTAssertEqual(String(data: try encoder.encode(PreferenceCoding(wrapped: store)), encoding: .utf8)!, """
|
||||||
|
{"test":true}
|
||||||
|
""")
|
||||||
|
store.test = false
|
||||||
|
XCTAssertEqual(String(data: try encoder.encode(PreferenceCoding(wrapped: store)), encoding: .utf8)!, """
|
||||||
|
{"test":false}
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPublisher() {
|
||||||
|
let topLevel = expectation(description: "top level publisher")
|
||||||
|
let specificPref = expectation(description: "preference publisher")
|
||||||
|
// initial and on change
|
||||||
|
specificPref.expectedFulfillmentCount = 2
|
||||||
|
let store = TestStore()
|
||||||
|
var cancellables = Set<AnyCancellable>()
|
||||||
|
store.objectWillChange.sink {
|
||||||
|
topLevel.fulfill()
|
||||||
|
// fires on will change
|
||||||
|
XCTAssertEqual(store.test, false)
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
store.testPublisher.sink { _ in
|
||||||
|
specificPref.fulfill()
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
store.test = true
|
||||||
|
wait(for: [topLevel, specificPref])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -54,21 +54,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
||||||
let oldPreferencesFile = documentsDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
|
|
||||||
if FileManager.default.fileExists(atPath: oldPreferencesFile.path) {
|
|
||||||
if case .failure(let error) = Preferences.migrate(from: oldPreferencesFile) {
|
|
||||||
#if canImport(Sentry)
|
|
||||||
SentrySDK.capture(error: error)
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure the persistent container is initialized on the main thread
|
// make sure the persistent container is initialized on the main thread
|
||||||
// otherwise initializing it on the background thread can deadlock with accessing it on the main thread elsewhere
|
// otherwise initializing it on the background thread can deadlock with accessing it on the main thread elsewhere
|
||||||
_ = DraftsPersistentContainer.shared
|
_ = DraftsPersistentContainer.shared
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||||
let oldDraftsFile = documentsDirectory.appendingPathComponent("drafts").appendingPathExtension("plist")
|
let oldDraftsFile = documentsDirectory.appendingPathComponent("drafts").appendingPathExtension("plist")
|
||||||
let appGroupDraftsFile = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!.appendingPathComponent("drafts").appendingPathExtension("plist")
|
let appGroupDraftsFile = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!.appendingPathComponent("drafts").appendingPathExtension("plist")
|
||||||
for url in [oldDraftsFile, appGroupDraftsFile] where FileManager.default.fileExists(atPath: url.path) {
|
for url in [oldDraftsFile, appGroupDraftsFile] where FileManager.default.fileExists(atPath: url.path) {
|
||||||
|
|
|
@ -33,7 +33,7 @@ extension TuskerSceneDelegate {
|
||||||
|
|
||||||
func applyAppearancePreferences() {
|
func applyAppearancePreferences() {
|
||||||
guard let window else { return }
|
guard let window else { return }
|
||||||
window.overrideUserInterfaceStyle = Preferences.shared.theme
|
window.overrideUserInterfaceStyle = Preferences.shared.theme.userInterfaceStyle
|
||||||
window.tintColor = Preferences.shared.accentColor.color
|
window.tintColor = Preferences.shared.accentColor.color
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
window.traitOverrides.pureBlackDarkMode = Preferences.shared.pureBlackDarkMode
|
window.traitOverrides.pureBlackDarkMode = Preferences.shared.pureBlackDarkMode
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
|
import TuskerPreferences
|
||||||
|
|
||||||
class MainSplitViewController: UISplitViewController {
|
class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
private var tabBarViewController: MainTabBarViewController!
|
private var tabBarViewController: MainTabBarViewController!
|
||||||
|
|
||||||
private var navigationMode: Preferences.WidescreenNavigationMode!
|
private var navigationMode: WidescreenNavigationMode!
|
||||||
private var secondaryNavController: NavigationControllerProtocol! {
|
private var secondaryNavController: NavigationControllerProtocol! {
|
||||||
viewController(for: .secondary) as? NavigationControllerProtocol
|
viewController(for: .secondary) as? NavigationControllerProtocol
|
||||||
}
|
}
|
||||||
|
@ -113,7 +114,7 @@ class MainSplitViewController: UISplitViewController {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateNavigationMode(_ mode: Preferences.WidescreenNavigationMode) {
|
private func updateNavigationMode(_ mode: WidescreenNavigationMode) {
|
||||||
guard mode != navigationMode else {
|
guard mode != navigationMode else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import Pachyderm
|
||||||
import CoreData
|
import CoreData
|
||||||
import CloudKit
|
import CloudKit
|
||||||
import UserAccounts
|
import UserAccounts
|
||||||
|
import TuskerPreferences
|
||||||
|
|
||||||
struct AdvancedPrefsView : View {
|
struct AdvancedPrefsView : View {
|
||||||
@ObservedObject var preferences = Preferences.shared
|
@ObservedObject var preferences = Preferences.shared
|
||||||
|
@ -41,7 +42,7 @@ struct AdvancedPrefsView : View {
|
||||||
|
|
||||||
Button("Cancel", role: .cancel) {}
|
Button("Cancel", role: .cancel) {}
|
||||||
Button("Enable") {
|
Button("Enable") {
|
||||||
if let flag = Preferences.FeatureFlag(rawValue: featureFlagName) {
|
if let flag = FeatureFlag(rawValue: featureFlagName) {
|
||||||
preferences.enabledFeatureFlags.insert(flag)
|
preferences.enabledFeatureFlags.insert(flag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Combine
|
import Combine
|
||||||
|
import TuskerPreferences
|
||||||
|
|
||||||
struct AppearancePrefsView : View {
|
struct AppearancePrefsView : View {
|
||||||
@ObservedObject var preferences = Preferences.shared
|
@ObservedObject var preferences = Preferences.shared
|
||||||
|
@ -27,7 +28,7 @@ struct AppearancePrefsView : View {
|
||||||
Preferences.shared.avatarStyle = $0 ? .circle : .roundRect
|
Preferences.shared.avatarStyle = $0 ? .circle : .roundRect
|
||||||
}
|
}
|
||||||
|
|
||||||
private let accentColorsAndImages: [(Preferences.AccentColor, UIImage?)] = Preferences.AccentColor.allCases.map { color in
|
private let accentColorsAndImages: [(AccentColor, UIImage?)] = AccentColor.allCases.map { color in
|
||||||
var image: UIImage?
|
var image: UIImage?
|
||||||
if let color = color.color {
|
if let color = color.color {
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
import TuskerPreferences
|
||||||
|
|
||||||
struct BehaviorPrefsView: View {
|
struct BehaviorPrefsView: View {
|
||||||
@ObservedObject var preferences = Preferences.shared
|
@ObservedObject var preferences = Preferences.shared
|
||||||
|
@ -39,8 +40,8 @@ struct BehaviorPrefsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Picker(selection: $preferences.timelineSyncMode) {
|
Picker(selection: $preferences.timelineSyncMode) {
|
||||||
Text("iCloud").tag(Preferences.TimelineSyncMode.icloud)
|
Text("iCloud").tag(TimelineSyncMode.icloud)
|
||||||
Text("Mastodon").tag(Preferences.TimelineSyncMode.mastodon)
|
Text("Mastodon").tag(TimelineSyncMode.mastodon)
|
||||||
} label: {
|
} label: {
|
||||||
Text("Sync Timeline Position via")
|
Text("Sync Timeline Position via")
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import TuskerPreferences
|
||||||
|
|
||||||
struct MediaPrefsView: View {
|
struct MediaPrefsView: View {
|
||||||
@ObservedObject var preferences = Preferences.shared
|
@ObservedObject var preferences = Preferences.shared
|
||||||
|
@ -23,7 +24,7 @@ struct MediaPrefsView: View {
|
||||||
var viewingSection: some View {
|
var viewingSection: some View {
|
||||||
Section(header: Text("Viewing")) {
|
Section(header: Text("Viewing")) {
|
||||||
Picker(selection: $preferences.attachmentBlurMode) {
|
Picker(selection: $preferences.attachmentBlurMode) {
|
||||||
ForEach(Preferences.AttachmentBlurMode.allCases, id: \.self) { mode in
|
ForEach(AttachmentBlurMode.allCases, id: \.self) { mode in
|
||||||
Text(mode.displayName).tag(mode)
|
Text(mode.displayName).tag(mode)
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Combine
|
import Combine
|
||||||
|
import TuskerPreferences
|
||||||
|
|
||||||
struct WidescreenNavigationPrefsView: View {
|
struct WidescreenNavigationPrefsView: View {
|
||||||
@ObservedObject private var preferences = Preferences.shared
|
@ObservedObject private var preferences = Preferences.shared
|
||||||
|
@ -59,8 +60,8 @@ struct WidescreenNavigationPrefsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct OptionView<Content: NavigationModePreview>: View {
|
private struct OptionView<Content: NavigationModePreview>: View {
|
||||||
let value: Preferences.WidescreenNavigationMode
|
let value: WidescreenNavigationMode
|
||||||
@Binding var selection: Preferences.WidescreenNavigationMode
|
@Binding var selection: WidescreenNavigationMode
|
||||||
let startAnimation: PassthroughSubject<Void, Never>
|
let startAnimation: PassthroughSubject<Void, Never>
|
||||||
@ViewBuilder let label: Text
|
@ViewBuilder let label: Text
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
Loading…
Reference in New Issue