Compare commits
No commits in common. "798e0c0cf12c5c7ae5363512de30d9d2450679b6" and "7825ccbb3d3045462462bf259272e3adeeffc48a" have entirely different histories.
798e0c0cf1
...
7825ccbb3d
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -1,20 +1,5 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2024.2 (121)
|
|
||||||
This build introduces a new multi-column navigation mode on iPad. You can revert to the old mode under Preferences -> Appearance.
|
|
||||||
|
|
||||||
Features/Improvements:
|
|
||||||
- iPadOS: Enable multi-column navigation
|
|
||||||
- Add post preview to Appearance preferences
|
|
||||||
- Consolidate Media preferences section with Appearance
|
|
||||||
- Add icons to Preferences sections
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
- Fix push notifications not working on Pleroma/Akkoma and older Mastodon versions
|
|
||||||
- Fix push notifications not working with certain accounts
|
|
||||||
- Fix links on About screen not being aligned
|
|
||||||
- macOS: Remove non-functional in-app Safari preferences
|
|
||||||
|
|
||||||
## 2024.2 (120)
|
## 2024.2 (120)
|
||||||
This build adds push notifications, which can be enabled in Preferences -> Notifications.
|
This build adds push notifications, which can be enabled in Preferences -> Notifications.
|
||||||
|
|
||||||
|
|
|
@ -301,7 +301,6 @@ extension MainActor {
|
||||||
@available(iOS, obsoleted: 17.0)
|
@available(iOS, obsoleted: 17.0)
|
||||||
@available(watchOS, obsoleted: 10.0)
|
@available(watchOS, obsoleted: 10.0)
|
||||||
@available(tvOS, obsoleted: 17.0)
|
@available(tvOS, obsoleted: 17.0)
|
||||||
@available(visionOS 1.0, *)
|
|
||||||
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T {
|
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T {
|
||||||
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
|
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
|
||||||
return try MainActor.assumeIsolated(body)
|
return try MainActor.assumeIsolated(body)
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>NSPrivacyAccessedAPITypes</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
||||||
<array>
|
|
||||||
<string>1C8F.1</string>
|
|
||||||
</array>
|
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
|
||||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
|
@ -181,8 +181,13 @@ class ToolbarController: ViewController {
|
||||||
private var formatButtons: some View {
|
private var formatButtons: some View {
|
||||||
ForEach(StatusFormat.allCases, id: \.rawValue) { format in
|
ForEach(StatusFormat.allCases, id: \.rawValue) { format in
|
||||||
Button(action: controller.formatAction(format)) {
|
Button(action: controller.formatAction(format)) {
|
||||||
Image(systemName: format.imageName)
|
if let imageName = format.imageName {
|
||||||
|
Image(systemName: imageName)
|
||||||
.font(.system(size: imageSize))
|
.font(.system(size: imageSize))
|
||||||
|
} else if let (str, attrs) = format.title {
|
||||||
|
let container = try! AttributeContainer(attrs, including: \.uiKit)
|
||||||
|
Text(AttributedString(str, attributes: container))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.accessibilityLabel(format.accessibilityLabel)
|
.accessibilityLabel(format.accessibilityLabel)
|
||||||
.padding(5)
|
.padding(5)
|
||||||
|
|
|
@ -23,7 +23,7 @@ enum StatusFormat: Int, CaseIterable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var imageName: String {
|
var imageName: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .italics:
|
case .italics:
|
||||||
return "italic"
|
return "italic"
|
||||||
|
@ -31,8 +31,16 @@ enum StatusFormat: Int, CaseIterable {
|
||||||
return "bold"
|
return "bold"
|
||||||
case .strikethrough:
|
case .strikethrough:
|
||||||
return "strikethrough"
|
return "strikethrough"
|
||||||
case .code:
|
default:
|
||||||
return "chevron.left.forwardslash.chevron.right"
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var title: (String, [NSAttributedString.Key: Any])? {
|
||||||
|
if self == .code {
|
||||||
|
return ("</>", [.font: UIFont(name: "Menlo", size: 17)!])
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -259,7 +259,11 @@ fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable {
|
||||||
if range.length > 0 {
|
if range.length > 0 {
|
||||||
let formatMenu = suggestedActions[index] as! UIMenu
|
let formatMenu = suggestedActions[index] as! UIMenu
|
||||||
let newFormatMenu = formatMenu.replacingChildren(StatusFormat.allCases.map { fmt in
|
let newFormatMenu = formatMenu.replacingChildren(StatusFormat.allCases.map { fmt in
|
||||||
return UIAction(title: fmt.accessibilityLabel, image: UIImage(systemName: fmt.imageName)) { [weak self] _ in
|
var image: UIImage?
|
||||||
|
if let imageName = fmt.imageName {
|
||||||
|
image = UIImage(systemName: imageName)
|
||||||
|
}
|
||||||
|
return UIAction(title: fmt.accessibilityLabel, image: image) { [weak self] _ in
|
||||||
self?.applyFormat(fmt)
|
self?.applyFormat(fmt)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -184,31 +184,6 @@ public final class InstanceFeatures: ObservableObject {
|
||||||
hasMastodonVersion(4, 2, 0) || instanceType.isMastodon(.hometown(nil))
|
hasMastodonVersion(4, 2, 0) || instanceType.isMastodon(.hometown(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
public var pushNotificationTypeStatus: Bool {
|
|
||||||
hasMastodonVersion(3, 3, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var pushNotificationTypeFollowRequest: Bool {
|
|
||||||
hasMastodonVersion(3, 1, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var pushNotificationTypeUpdate: Bool {
|
|
||||||
hasMastodonVersion(3, 5, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var pushNotificationPolicy: Bool {
|
|
||||||
hasMastodonVersion(3, 5, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var pushNotificationPolicyMissingFromResponse: Bool {
|
|
||||||
switch instanceType {
|
|
||||||
case .mastodon(_, let version):
|
|
||||||
return version >= Version(3, 5, 0) && version < Version(4, 1, 0)
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,17 +25,6 @@ public struct Attachment: Codable, Sendable {
|
||||||
], nil))
|
], nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(id: String, kind: Attachment.Kind, url: URL, remoteURL: URL? = nil, previewURL: URL? = nil, meta: Attachment.Metadata? = nil, description: String? = nil, blurHash: String? = nil) {
|
|
||||||
self.id = id
|
|
||||||
self.kind = kind
|
|
||||||
self.url = url
|
|
||||||
self.remoteURL = remoteURL
|
|
||||||
self.previewURL = previewURL
|
|
||||||
self.meta = meta
|
|
||||||
self.description = description
|
|
||||||
self.blurHash = blurHash
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
self.id = try container.decode(String.self, forKey: .id)
|
self.id = try container.decode(String.self, forKey: .id)
|
||||||
|
|
|
@ -26,38 +26,6 @@ public struct Card: Codable, Sendable {
|
||||||
/// Only present when returned from the trending links endpoint
|
/// Only present when returned from the trending links endpoint
|
||||||
public let history: [History]?
|
public let history: [History]?
|
||||||
|
|
||||||
public init(
|
|
||||||
url: WebURL,
|
|
||||||
title: String,
|
|
||||||
description: String,
|
|
||||||
image: WebURL? = nil,
|
|
||||||
kind: Card.Kind,
|
|
||||||
authorName: String? = nil,
|
|
||||||
authorURL: WebURL? = nil,
|
|
||||||
providerName: String? = nil,
|
|
||||||
providerURL: WebURL? = nil,
|
|
||||||
html: String? = nil,
|
|
||||||
width: Int? = nil,
|
|
||||||
height: Int? = nil,
|
|
||||||
blurhash: String? = nil,
|
|
||||||
history: [History]? = nil
|
|
||||||
) {
|
|
||||||
self.url = url
|
|
||||||
self.title = title
|
|
||||||
self.description = description
|
|
||||||
self.image = image
|
|
||||||
self.kind = kind
|
|
||||||
self.authorName = authorName
|
|
||||||
self.authorURL = authorURL
|
|
||||||
self.providerName = providerName
|
|
||||||
self.providerURL = providerURL
|
|
||||||
self.html = html
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.blurhash = blurhash
|
|
||||||
self.history = history
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,11 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct PushSubscription: Decodable, Sendable {
|
public struct PushSubscription: Decodable, Sendable {
|
||||||
public var id: String
|
public let id: String
|
||||||
public var endpoint: URL
|
public let endpoint: URL
|
||||||
public var serverKey: String
|
public let serverKey: String
|
||||||
public var alerts: Alerts
|
public let alerts: Alerts
|
||||||
public var policy: Policy
|
public let policy: Policy
|
||||||
|
|
||||||
public init(from decoder: any Decoder) throws {
|
public init(from decoder: any Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
@ -27,8 +27,7 @@ public struct PushSubscription: Decodable, Sendable {
|
||||||
self.endpoint = try container.decode(URL.self, forKey: .endpoint)
|
self.endpoint = try container.decode(URL.self, forKey: .endpoint)
|
||||||
self.serverKey = try container.decode(String.self, forKey: .serverKey)
|
self.serverKey = try container.decode(String.self, forKey: .serverKey)
|
||||||
self.alerts = try container.decode(PushSubscription.Alerts.self, forKey: .alerts)
|
self.alerts = try container.decode(PushSubscription.Alerts.self, forKey: .alerts)
|
||||||
// added in mastodon 4.1.0
|
self.policy = try container.decode(PushSubscription.Policy.self, forKey: .policy)
|
||||||
self.policy = try container.decodeIfPresent(PushSubscription.Policy.self, forKey: .policy) ?? .all
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func create(endpoint: URL, publicKey: Data, authSecret: Data, alerts: Alerts, policy: Policy) -> Request<PushSubscription> {
|
public static func create(endpoint: URL, publicKey: Data, authSecret: Data, alerts: Alerts, policy: Policy) -> Request<PushSubscription> {
|
||||||
|
@ -97,21 +96,6 @@ extension PushSubscription {
|
||||||
self.update = update
|
self.update = update
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(from decoder: any Decoder) throws {
|
|
||||||
let container: KeyedDecodingContainer<PushSubscription.Alerts.CodingKeys> = try decoder.container(keyedBy: PushSubscription.Alerts.CodingKeys.self)
|
|
||||||
self.mention = try container.decode(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.mention)
|
|
||||||
// status added in mastodon 3.3.0
|
|
||||||
self.status = try container.decodeIfPresent(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.status) ?? false
|
|
||||||
self.reblog = try container.decode(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.reblog)
|
|
||||||
self.follow = try container.decode(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.follow)
|
|
||||||
// follow_request added in 3.1.0
|
|
||||||
self.followRequest = try container.decodeIfPresent(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.followRequest) ?? false
|
|
||||||
self.favourite = try container.decode(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.favourite)
|
|
||||||
self.poll = try container.decode(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.poll)
|
|
||||||
// update added in mastodon 3.5.0
|
|
||||||
self.update = try container.decodeIfPresent(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.update) ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case mention
|
case mention
|
||||||
case status
|
case status
|
||||||
|
|
|
@ -71,7 +71,7 @@ class PushManagerImpl: _PushManager {
|
||||||
|
|
||||||
private func endpointURL(deviceToken: Data, accountID: String) -> URL {
|
private func endpointURL(deviceToken: Data, accountID: String) -> URL {
|
||||||
var endpoint = URLComponents(url: endpoint, resolvingAgainstBaseURL: false)!
|
var endpoint = URLComponents(url: endpoint, resolvingAgainstBaseURL: false)!
|
||||||
let accountID = accountID.addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
|
let accountID = accountID.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!
|
||||||
endpoint.path = "/push/v1/\(apnsEnvironment)/\(deviceToken.hexEncodedString())/\(accountID)"
|
endpoint.path = "/push/v1/\(apnsEnvironment)/\(deviceToken.hexEncodedString())/\(accountID)"
|
||||||
return endpoint.url!
|
return endpoint.url!
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ public struct PushSubscription {
|
||||||
self.policy = policy
|
self.policy = policy
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Policy: String, CaseIterable, Identifiable, Sendable {
|
public enum Policy: String, CaseIterable, Identifiable {
|
||||||
case all, followed, followers
|
case all, followed, followers
|
||||||
|
|
||||||
public var id: some Hashable {
|
public var id: some Hashable {
|
||||||
|
@ -61,7 +61,7 @@ public struct PushSubscription {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Alerts: OptionSet, Hashable, Sendable {
|
public struct Alerts: OptionSet, Hashable {
|
||||||
public static let mention = Alerts(rawValue: 1 << 0)
|
public static let mention = Alerts(rawValue: 1 << 0)
|
||||||
public static let status = Alerts(rawValue: 1 << 1)
|
public static let status = Alerts(rawValue: 1 << 1)
|
||||||
public static let reblog = Alerts(rawValue: 1 << 2)
|
public static let reblog = Alerts(rawValue: 1 << 2)
|
||||||
|
|
|
@ -9,10 +9,8 @@ import SwiftUI
|
||||||
|
|
||||||
public struct AsyncPicker<V: Hashable, Content: View>: View {
|
public struct AsyncPicker<V: Hashable, Content: View>: View {
|
||||||
let titleKey: LocalizedStringKey
|
let titleKey: LocalizedStringKey
|
||||||
#if !os(visionOS)
|
|
||||||
@available(iOS, obsoleted: 16.0, message: "Switch to LabeledContent")
|
@available(iOS, obsoleted: 16.0, message: "Switch to LabeledContent")
|
||||||
let labelHidden: Bool
|
let labelHidden: Bool
|
||||||
#endif
|
|
||||||
let alignment: Alignment
|
let alignment: Alignment
|
||||||
@Binding var value: V
|
@Binding var value: V
|
||||||
let onChange: (V) async -> Bool
|
let onChange: (V) async -> Bool
|
||||||
|
@ -21,9 +19,7 @@ public struct AsyncPicker<V: Hashable, Content: View>: View {
|
||||||
|
|
||||||
public init(_ titleKey: LocalizedStringKey, labelHidden: Bool = false, alignment: Alignment = .center, value: Binding<V>, onChange: @escaping (V) async -> Bool, @ViewBuilder content: () -> Content) {
|
public init(_ titleKey: LocalizedStringKey, labelHidden: Bool = false, alignment: Alignment = .center, value: Binding<V>, onChange: @escaping (V) async -> Bool, @ViewBuilder content: () -> Content) {
|
||||||
self.titleKey = titleKey
|
self.titleKey = titleKey
|
||||||
#if !os(visionOS)
|
|
||||||
self.labelHidden = labelHidden
|
self.labelHidden = labelHidden
|
||||||
#endif
|
|
||||||
self.alignment = alignment
|
self.alignment = alignment
|
||||||
self._value = value
|
self._value = value
|
||||||
self.onChange = onChange
|
self.onChange = onChange
|
||||||
|
@ -31,11 +27,6 @@ public struct AsyncPicker<V: Hashable, Content: View>: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
#if os(visionOS)
|
|
||||||
LabeledContent(titleKey) {
|
|
||||||
picker
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
LabeledContent(titleKey) {
|
LabeledContent(titleKey) {
|
||||||
picker
|
picker
|
||||||
|
@ -49,7 +40,6 @@ public struct AsyncPicker<V: Hashable, Content: View>: View {
|
||||||
picker
|
picker
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var picker: some View {
|
private var picker: some View {
|
||||||
|
|
|
@ -10,28 +10,19 @@ import SwiftUI
|
||||||
|
|
||||||
public struct AsyncToggle: View {
|
public struct AsyncToggle: View {
|
||||||
let titleKey: LocalizedStringKey
|
let titleKey: LocalizedStringKey
|
||||||
#if !os(visionOS)
|
|
||||||
@available(iOS, obsoleted: 16.0, message: "Switch to LabeledContent")
|
@available(iOS, obsoleted: 16.0, message: "Switch to LabeledContent")
|
||||||
let labelHidden: Bool
|
let labelHidden: Bool
|
||||||
#endif
|
|
||||||
@Binding var mode: Mode
|
@Binding var mode: Mode
|
||||||
let onChange: (Bool) async -> Bool
|
let onChange: (Bool) async -> Bool
|
||||||
|
|
||||||
public init(_ titleKey: LocalizedStringKey, labelHidden: Bool = false, mode: Binding<Mode>, onChange: @escaping (Bool) async -> Bool) {
|
public init(_ titleKey: LocalizedStringKey, labelHidden: Bool = false, mode: Binding<Mode>, onChange: @escaping (Bool) async -> Bool) {
|
||||||
self.titleKey = titleKey
|
self.titleKey = titleKey
|
||||||
#if !os(visionOS)
|
|
||||||
self.labelHidden = labelHidden
|
self.labelHidden = labelHidden
|
||||||
#endif
|
|
||||||
self._mode = mode
|
self._mode = mode
|
||||||
self.onChange = onChange
|
self.onChange = onChange
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
#if os(visionOS)
|
|
||||||
LabeledContent(titleKey) {
|
|
||||||
toggleOrSpinner
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
LabeledContent(titleKey) {
|
LabeledContent(titleKey) {
|
||||||
toggleOrSpinner
|
toggleOrSpinner
|
||||||
|
@ -45,7 +36,6 @@ public struct AsyncToggle: View {
|
||||||
toggleOrSpinner
|
toggleOrSpinner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
{
|
|
||||||
"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,9 +24,5 @@ let package = Package(
|
||||||
name: "TuskerPreferences",
|
name: "TuskerPreferences",
|
||||||
dependencies: ["Pachyderm"]
|
dependencies: ["Pachyderm"]
|
||||||
),
|
),
|
||||||
.testTarget(
|
|
||||||
name: "TuskerPreferencesTests",
|
|
||||||
dependencies: ["TuskerPreferences"]
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,282 +0,0 @@
|
||||||
//
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
//
|
|
||||||
// AdvancedKeys.swift
|
|
||||||
// TuskerPreferences
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 4/13/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
struct StatusContentTypeKey: MigratablePreferenceKey {
|
|
||||||
static var defaultValue: StatusContentType { .plain }
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FeatureFlagsKey: MigratablePreferenceKey, CustomCodablePreferenceKey {
|
|
||||||
static var defaultValue: Set<FeatureFlag> { [] }
|
|
||||||
|
|
||||||
static func encode(value: Set<FeatureFlag>, to encoder: any Encoder) throws {
|
|
||||||
var container = encoder.singleValueContainer()
|
|
||||||
try container.encode(value.map(\.rawValue))
|
|
||||||
}
|
|
||||||
|
|
||||||
static func decode(from decoder: any Decoder) throws -> Set<FeatureFlag>? {
|
|
||||||
let container = try decoder.singleValueContainer()
|
|
||||||
let names = try container.decode([String].self)
|
|
||||||
return Set(names.compactMap(FeatureFlag.init(rawValue:)))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
//
|
|
||||||
// AppearanceKeys.swift
|
|
||||||
// TuskerPreferences
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 4/13/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
public struct ThemeKey: MigratablePreferenceKey {
|
|
||||||
public static var defaultValue: Theme { .unspecified }
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct AccentColorKey: MigratablePreferenceKey {
|
|
||||||
public static var defaultValue: AccentColor { .default }
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AvatarStyleKey: MigratablePreferenceKey {
|
|
||||||
static var defaultValue: AvatarStyle { .roundRect }
|
|
||||||
}
|
|
||||||
|
|
||||||
struct LeadingSwipeActionsKey: MigratablePreferenceKey {
|
|
||||||
static var defaultValue: [StatusSwipeAction] { [.favorite, .reblog] }
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TrailingSwipeActionsKey: MigratablePreferenceKey {
|
|
||||||
static var defaultValue: [StatusSwipeAction] { [.reply, .share] }
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct WidescreenNavigationModeKey: MigratablePreferenceKey {
|
|
||||||
public static var defaultValue: WidescreenNavigationMode { .multiColumn }
|
|
||||||
|
|
||||||
public static func shouldMigrate(oldValue: WidescreenNavigationMode) -> Bool {
|
|
||||||
oldValue != .splitScreen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AttachmentBlurModeKey: MigratablePreferenceKey {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
//
|
|
||||||
// BehaviorKeys.swift
|
|
||||||
// TuskerPreferences
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 4/13/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct OppositeCollapseKeywordsKey: MigratablePreferenceKey {
|
|
||||||
static var defaultValue: [String] { [] }
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ConfirmReblogKey: MigratablePreferenceKey {
|
|
||||||
static var defaultValue: Bool {
|
|
||||||
#if os(visionOS)
|
|
||||||
true
|
|
||||||
#else
|
|
||||||
false
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TimelineSyncModeKey: MigratablePreferenceKey {
|
|
||||||
static var defaultValue: TimelineSyncMode { .icloud }
|
|
||||||
}
|
|
||||||
|
|
||||||
struct InAppSafariKey: MigratablePreferenceKey {
|
|
||||||
static var defaultValue: Bool {
|
|
||||||
#if targetEnvironment(macCatalyst) || os(visionOS)
|
|
||||||
false
|
|
||||||
#else
|
|
||||||
if ProcessInfo.processInfo.isiOSAppOnMac {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
//
|
|
||||||
// CommonKeys.swift
|
|
||||||
// TuskerPreferences
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 4/13/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
public struct TrueKey: MigratablePreferenceKey {
|
|
||||||
public static var defaultValue: Bool { true }
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct FalseKey: MigratablePreferenceKey {
|
|
||||||
public static var defaultValue: Bool { false }
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
//
|
|
||||||
// ComposingKeys.swift
|
|
||||||
// TuskerPreferences
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 4/13/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct PostVisibilityKey: MigratablePreferenceKey {
|
|
||||||
static var defaultValue: PostVisibility { .serverDefault }
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ReplyVisibilityKey: MigratablePreferenceKey {
|
|
||||||
static var defaultValue: ReplyVisibility { .sameAsPost }
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ContentWarningCopyModeKey: MigratablePreferenceKey {
|
|
||||||
static var defaultValue: ContentWarningCopyMode { .asIs }
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
//
|
|
||||||
// DigitalWellnessKeys.swift
|
|
||||||
// TuskerPreferences
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 4/13/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct NotificationsModeKey: MigratablePreferenceKey {
|
|
||||||
static var defaultValue: NotificationsMode { .allNotifications }
|
|
||||||
}
|
|
|
@ -1,205 +0,0 @@
|
||||||
//
|
|
||||||
// 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 {}
|
|
|
@ -1,106 +0,0 @@
|
||||||
//
|
|
||||||
// PreferenceStore+Migrate.swift
|
|
||||||
// TuskerPreferences
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 4/13/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
extension PreferenceStore {
|
|
||||||
func migrate(from legacy: LegacyPreferences) {
|
|
||||||
var 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: \.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),
|
|
||||||
]
|
|
||||||
#if !targetEnvironment(macCatalyst) && !os(visionOS)
|
|
||||||
migrations.append(contentsOf: [
|
|
||||||
Migration(from: \.useInAppSafari, to: \.$useInAppSafari),
|
|
||||||
Migration(from: \.inAppSafariAutomaticReaderMode, to: \.$inAppSafariAutomaticReaderMode),
|
|
||||||
] as [any MigrationProtocol])
|
|
||||||
#endif
|
|
||||||
|
|
||||||
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: MigratablePreferenceKey>: 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 Key.shouldMigrate(oldValue: value) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,7 +12,7 @@ public enum PostVisibility: Codable, Hashable, CaseIterable, Sendable {
|
||||||
case serverDefault
|
case serverDefault
|
||||||
case visibility(Visibility)
|
case visibility(Visibility)
|
||||||
|
|
||||||
public private(set) static var allCases: [PostVisibility] = [.serverDefault] + Visibility.allCases.map { .visibility($0) }
|
public static var allCases: [PostVisibility] = [.serverDefault] + Visibility.allCases.map { .visibility($0) }
|
||||||
|
|
||||||
public func resolved(withServerDefault serverDefault: Visibility?) -> Visibility {
|
public func resolved(withServerDefault serverDefault: Visibility?) -> Visibility {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -57,7 +57,7 @@ public enum ReplyVisibility: Codable, Hashable, CaseIterable {
|
||||||
case sameAsPost
|
case sameAsPost
|
||||||
case visibility(Visibility)
|
case visibility(Visibility)
|
||||||
|
|
||||||
public private(set) static var allCases: [ReplyVisibility] = [.sameAsPost] + Visibility.allCases.map { .visibility($0) }
|
public static var allCases: [ReplyVisibility] = [.sameAsPost] + Visibility.allCases.map { .visibility($0) }
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public func resolved(withServerDefault serverDefault: Visibility?) -> Visibility {
|
public func resolved(withServerDefault serverDefault: Visibility?) -> Visibility {
|
|
@ -1,101 +0,0 @@
|
||||||
//
|
|
||||||
// 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 keyType = Key.self as? any CustomCodablePreferenceKey.Type {
|
|
||||||
self.storedValue = try keyType.decode(from: decoder) as! Key.Value?
|
|
||||||
} else if let container = try? decoder.singleValueContainer() {
|
|
||||||
self.storedValue = try? container.decode(Key.Value.self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func encode(to encoder: any Encoder) throws {
|
|
||||||
if let storedValue {
|
|
||||||
if let keyType = Key.self as? any CustomCodablePreferenceKey.Type {
|
|
||||||
func encode<K: CustomCodablePreferenceKey>(_: K.Type) throws {
|
|
||||||
try K.encode(value: storedValue as! K.Value, to: encoder)
|
|
||||||
}
|
|
||||||
return try _openExistential(keyType, do: encode)
|
|
||||||
} else {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct PreferencePublisher<Key: PreferenceKey>: Publisher {
|
|
||||||
public typealias Output = Key.Value
|
|
||||||
public typealias Failure = Never
|
|
||||||
|
|
||||||
let preference: Preference<Key>
|
|
||||||
|
|
||||||
public func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, Key.Value == S.Input {
|
|
||||||
preference.$storedValue.map { $0 ?? Key.defaultValue }.receive(subscriber: subscriber)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
//
|
|
||||||
// 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 {
|
|
||||||
public static func didSet(in store: PreferenceStore, newValue: Value) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol MigratablePreferenceKey: PreferenceKey where Value: Equatable {
|
|
||||||
static func shouldMigrate(oldValue: Value) -> Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
extension MigratablePreferenceKey {
|
|
||||||
static func shouldMigrate(oldValue: Value) -> Bool {
|
|
||||||
oldValue != defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol CustomCodablePreferenceKey: PreferenceKey {
|
|
||||||
static func encode(value: Value, to encoder: any Encoder) throws
|
|
||||||
static func decode(from decoder: any Decoder) throws -> Value?
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
//
|
|
||||||
// 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
|
|
||||||
@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: 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: Behavior
|
|
||||||
@Preference<TrueKey> public var openLinksInApps
|
|
||||||
@Preference<InAppSafariKey> 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,42 +2,430 @@
|
||||||
// Preferences.swift
|
// Preferences.swift
|
||||||
// TuskerPreferences
|
// TuskerPreferences
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 4/12/24.
|
// Created by Shadowfacts on 8/28/18.
|
||||||
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
public final class Preferences: Codable, ObservableObject {
|
||||||
|
|
||||||
public struct Preferences {
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public static let shared: PreferenceStore = load()
|
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 legacyURL = appGroupDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
|
private static var archiveURL = appGroupDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
|
||||||
private static var preferencesURL = appGroupDirectory.appendingPathComponent("preferences.v2").appendingPathExtension("plist")
|
|
||||||
private static var nonAppGroupURL = documentsDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
|
|
||||||
|
|
||||||
private init() {}
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public static func save() {
|
public static func save() {
|
||||||
let encoder = PropertyListEncoder()
|
let encoder = PropertyListEncoder()
|
||||||
let data = try? encoder.encode(PreferenceCoding(wrapped: shared))
|
let data = try? encoder.encode(shared)
|
||||||
try? data?.write(to: preferencesURL, options: .noFileProtection)
|
try? data?.write(to: archiveURL, options: .noFileProtection)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func load() -> PreferenceStore {
|
public static func load() -> Preferences {
|
||||||
let decoder = PropertyListDecoder()
|
let decoder = PropertyListDecoder()
|
||||||
if let data = try? Data(contentsOf: preferencesURL),
|
if let data = try? Data(contentsOf: archiveURL),
|
||||||
let store = try? decoder.decode(PreferenceCoding<PreferenceStore>.self, from: data) {
|
let preferences = try? decoder.decode(Preferences.self, from: data) {
|
||||||
return store.wrapped
|
return preferences
|
||||||
} else if let legacyData = (try? Data(contentsOf: legacyURL)) ?? (try? Data(contentsOf: nonAppGroupURL)),
|
}
|
||||||
let legacy = try? decoder.decode(LegacyPreferences.self, from: legacyData) {
|
return Preferences()
|
||||||
let store = PreferenceStore()
|
}
|
||||||
store.migrate(from: legacy)
|
|
||||||
return store
|
@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() {}
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
return PreferenceStore()
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
//
|
|
||||||
// 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
//
|
|
||||||
// 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
//
|
|
||||||
// FeatureFlag.swift
|
|
||||||
// TuskerPreferences
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 4/13/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
public enum FeatureFlag: String, Codable {
|
|
||||||
case iPadBrowserNavigation = "ipad-browser-navigation"
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
//
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
//
|
|
||||||
// TimelineSyncMode.swift
|
|
||||||
// TuskerPreferences
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 4/13/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
public enum TimelineSyncMode: String, Codable {
|
|
||||||
case mastodon
|
|
||||||
case icloud
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
//
|
|
||||||
// WidescreenNavigationMode.swift
|
|
||||||
// TuskerPreferences
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 4/13/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
public enum WidescreenNavigationMode: String, Codable {
|
|
||||||
case stack
|
|
||||||
case splitScreen
|
|
||||||
case multiColumn
|
|
||||||
}
|
|
|
@ -1,128 +0,0 @@
|
||||||
//
|
|
||||||
// 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<Key: PreferenceKey>: Codable, ObservableObject {
|
|
||||||
private var _test = Preference<Key>()
|
|
||||||
|
|
||||||
// the acutal subscript expects the enclosingInstance to be a PreferenceStore, so do it manually
|
|
||||||
var test: Key.Value {
|
|
||||||
get {
|
|
||||||
Preference.get(enclosingInstance: self, storage: \._test)
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
Preference.set(enclosingInstance: self, storage: \._test, newValue: newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var testPublisher: some Publisher<Key.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<Key>.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<TestKey>>.self, from: Data("""
|
|
||||||
{"test": true}
|
|
||||||
""".utf8)).wrapped
|
|
||||||
XCTAssertEqual(present.test, true)
|
|
||||||
let absent = try decoder.decode(PreferenceCoding<TestStore<TestKey>>.self, from: Data("""
|
|
||||||
{}
|
|
||||||
""".utf8)).wrapped
|
|
||||||
XCTAssertEqual(absent.test, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testEncoding() throws {
|
|
||||||
let store = TestStore<TestKey>()
|
|
||||||
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<TestKey>()
|
|
||||||
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])
|
|
||||||
}
|
|
||||||
|
|
||||||
func testCustomCodable() throws {
|
|
||||||
struct Key: CustomCodablePreferenceKey {
|
|
||||||
static let defaultValue = 1
|
|
||||||
static func encode(value: Int, to encoder: any Encoder) throws {
|
|
||||||
var container = encoder.singleValueContainer()
|
|
||||||
try container.encode(2)
|
|
||||||
}
|
|
||||||
static func decode(from decoder: any Decoder) throws -> Int? {
|
|
||||||
3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let store = TestStore<Key>()
|
|
||||||
store.test = 123
|
|
||||||
let encoder = JSONEncoder()
|
|
||||||
XCTAssertEqual(String(data: try encoder.encode(PreferenceCoding(wrapped: store)), encoding: .utf8)!, """
|
|
||||||
{"test":2}
|
|
||||||
""")
|
|
||||||
let decoder = JSONDecoder()
|
|
||||||
let present = try decoder.decode(PreferenceCoding<TestStore<Key>>.self, from: Data("""
|
|
||||||
{"test":2}
|
|
||||||
""".utf8)).wrapped
|
|
||||||
XCTAssertEqual(present.test, 3)
|
|
||||||
let absent = try decoder.decode(PreferenceCoding<TestStore<Key>>.self, from: Data("""
|
|
||||||
{}
|
|
||||||
""".utf8)).wrapped
|
|
||||||
XCTAssertEqual(absent.test, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -8,8 +8,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
// Sendability: UserDefaults is not marked Sendable, but is documented as being thread safe
|
public class UserAccountsManager: ObservableObject {
|
||||||
public final class UserAccountsManager: ObservableObject, @unchecked Sendable {
|
|
||||||
|
|
||||||
public static let shared = UserAccountsManager()
|
public static let shared = UserAccountsManager()
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */; };
|
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */; };
|
||||||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; };
|
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; };
|
||||||
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4022B2FFB10021BD04 /* PreferencesView.swift */; };
|
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4022B2FFB10021BD04 /* PreferencesView.swift */; };
|
||||||
|
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4222B301470021BD04 /* AppearancePrefsView.swift */; };
|
||||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
|
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
|
||||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
|
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
|
||||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
|
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
|
||||||
|
@ -169,6 +170,7 @@
|
||||||
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
|
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
|
||||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
|
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
|
||||||
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
|
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
|
||||||
|
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; };
|
||||||
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; };
|
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; };
|
||||||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
|
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
|
||||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
|
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
|
||||||
|
@ -289,8 +291,6 @@
|
||||||
D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4FA299035650009FCFF /* TrendsViewController.swift */; };
|
D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4FA299035650009FCFF /* TrendsViewController.swift */; };
|
||||||
D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */; };
|
D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */; };
|
||||||
D6C3F5192991F5D60009FCFF /* TimelineJumpButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F5182991F5D60009FCFF /* TimelineJumpButton.swift */; };
|
D6C3F5192991F5D60009FCFF /* TimelineJumpButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F5182991F5D60009FCFF /* TimelineJumpButton.swift */; };
|
||||||
D6C4532D2BCB86AC00E26A0E /* AppearancePrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C4532C2BCB86AC00E26A0E /* AppearancePrefsView.swift */; };
|
|
||||||
D6C4532F2BCB873400E26A0E /* MockStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C4532E2BCB873400E26A0E /* MockStatusView.swift */; };
|
|
||||||
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
|
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
|
||||||
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
|
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
|
||||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
|
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
|
||||||
|
@ -435,6 +435,7 @@
|
||||||
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedPrefsView.swift; sourceTree = "<group>"; };
|
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedPrefsView.swift; sourceTree = "<group>"; };
|
||||||
0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = "<group>"; };
|
0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = "<group>"; };
|
||||||
04586B4022B2FFB10021BD04 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
04586B4022B2FFB10021BD04 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
||||||
|
04586B4222B301470021BD04 /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = "<group>"; };
|
||||||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
|
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
|
||||||
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
|
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
|
||||||
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
|
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -591,6 +592,7 @@
|
||||||
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
|
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
|
||||||
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
|
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
|
||||||
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
|
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
|
||||||
|
D68015412401A74600D6103B /* MediaPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPrefsView.swift; sourceTree = "<group>"; };
|
||||||
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = "<group>"; };
|
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; };
|
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
|
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -711,9 +713,6 @@
|
||||||
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendsViewController.swift; sourceTree = "<group>"; };
|
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendsViewController.swift; sourceTree = "<group>"; };
|
||||||
D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AppListStyle.swift"; sourceTree = "<group>"; };
|
D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AppListStyle.swift"; sourceTree = "<group>"; };
|
||||||
D6C3F5182991F5D60009FCFF /* TimelineJumpButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineJumpButton.swift; sourceTree = "<group>"; };
|
D6C3F5182991F5D60009FCFF /* TimelineJumpButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineJumpButton.swift; sourceTree = "<group>"; };
|
||||||
D6C4532A2BCAD7F900E26A0E /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
|
||||||
D6C4532C2BCB86AC00E26A0E /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = "<group>"; };
|
|
||||||
D6C4532E2BCB873400E26A0E /* MockStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStatusView.swift; sourceTree = "<group>"; };
|
|
||||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; };
|
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; };
|
||||||
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
|
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
|
||||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -1010,7 +1009,6 @@
|
||||||
D630C3D92BC61B6100208903 /* NotificationExtension.entitlements */,
|
D630C3D92BC61B6100208903 /* NotificationExtension.entitlements */,
|
||||||
D630C3D32BC61B6100208903 /* NotificationService.swift */,
|
D630C3D32BC61B6100208903 /* NotificationService.swift */,
|
||||||
D630C3D52BC61B6100208903 /* Info.plist */,
|
D630C3D52BC61B6100208903 /* Info.plist */,
|
||||||
D6C4532A2BCAD7F900E26A0E /* PrivacyInfo.xcprivacy */,
|
|
||||||
);
|
);
|
||||||
path = NotificationExtension;
|
path = NotificationExtension;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1170,14 +1168,17 @@
|
||||||
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */,
|
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */,
|
||||||
04586B4022B2FFB10021BD04 /* PreferencesView.swift */,
|
04586B4022B2FFB10021BD04 /* PreferencesView.swift */,
|
||||||
D64B96802BC3279D002C8990 /* PrefsAccountView.swift */,
|
D64B96802BC3279D002C8990 /* PrefsAccountView.swift */,
|
||||||
|
04586B4222B301470021BD04 /* AppearancePrefsView.swift */,
|
||||||
|
D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */,
|
||||||
|
D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */,
|
||||||
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */,
|
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */,
|
||||||
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */,
|
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */,
|
||||||
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */,
|
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */,
|
||||||
|
D68015412401A74600D6103B /* MediaPrefsView.swift */,
|
||||||
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */,
|
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */,
|
||||||
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */,
|
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */,
|
||||||
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */,
|
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */,
|
||||||
D68A76ED295369C7001DA1B3 /* AcknowledgementsView.swift */,
|
D68A76ED295369C7001DA1B3 /* AcknowledgementsView.swift */,
|
||||||
D6C4532B2BCB86A100E26A0E /* Appearance */,
|
|
||||||
D64B96822BC3892B002C8990 /* Notifications */,
|
D64B96822BC3892B002C8990 /* Notifications */,
|
||||||
D60089172981FEA4005B4D00 /* Tip Jar */,
|
D60089172981FEA4005B4D00 /* Tip Jar */,
|
||||||
D68A76EF2953910A001DA1B3 /* About */,
|
D68A76EF2953910A001DA1B3 /* About */,
|
||||||
|
@ -1482,17 +1483,6 @@
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D6C4532B2BCB86A100E26A0E /* Appearance */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
D6C4532C2BCB86AC00E26A0E /* AppearancePrefsView.swift */,
|
|
||||||
D6C4532E2BCB873400E26A0E /* MockStatusView.swift */,
|
|
||||||
D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */,
|
|
||||||
D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */,
|
|
||||||
);
|
|
||||||
path = Appearance;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
D6C693FA2162FE5D007D6A6D /* Utilities */ = {
|
D6C693FA2162FE5D007D6A6D /* Utilities */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -2110,7 +2100,6 @@
|
||||||
D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */,
|
D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */,
|
||||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
||||||
D6934F342BA8D65A002B1C8D /* ImageGalleryDataSource.swift in Sources */,
|
D6934F342BA8D65A002B1C8D /* ImageGalleryDataSource.swift in Sources */,
|
||||||
D6C4532D2BCB86AC00E26A0E /* AppearancePrefsView.swift in Sources */,
|
|
||||||
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */,
|
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */,
|
||||||
D6BC74862AFC4772000DD603 /* SuggestedProfileCardView.swift in Sources */,
|
D6BC74862AFC4772000DD603 /* SuggestedProfileCardView.swift in Sources */,
|
||||||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
||||||
|
@ -2299,6 +2288,7 @@
|
||||||
D60089192981FEBA005B4D00 /* ConfettiView.swift in Sources */,
|
D60089192981FEBA005B4D00 /* ConfettiView.swift in Sources */,
|
||||||
D61F759929384D4D00C0B37F /* CustomizeTimelinesView.swift in Sources */,
|
D61F759929384D4D00C0B37F /* CustomizeTimelinesView.swift in Sources */,
|
||||||
D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */,
|
D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */,
|
||||||
|
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
||||||
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
||||||
D646DCD42A0729440059ECEB /* ActionNotificationGroupCollectionViewCell.swift in Sources */,
|
D646DCD42A0729440059ECEB /* ActionNotificationGroupCollectionViewCell.swift in Sources */,
|
||||||
D69261272BB3BA610023152C /* Box.swift in Sources */,
|
D69261272BB3BA610023152C /* Box.swift in Sources */,
|
||||||
|
@ -2345,6 +2335,7 @@
|
||||||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */,
|
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */,
|
||||||
D6CF5B892AC9BA6E00F15D83 /* MastodonSearchController.swift in Sources */,
|
D6CF5B892AC9BA6E00F15D83 /* MastodonSearchController.swift in Sources */,
|
||||||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */,
|
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */,
|
||||||
|
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */,
|
||||||
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
|
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
|
||||||
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */,
|
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */,
|
||||||
D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */,
|
D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */,
|
||||||
|
@ -2366,7 +2357,6 @@
|
||||||
D65B4B562971F98300DABDFB /* ReportView.swift in Sources */,
|
D65B4B562971F98300DABDFB /* ReportView.swift in Sources */,
|
||||||
D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */,
|
D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */,
|
||||||
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */,
|
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */,
|
||||||
D6C4532F2BCB873400E26A0E /* MockStatusView.swift in Sources */,
|
|
||||||
D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */,
|
D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */,
|
||||||
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
||||||
D6D12B5A292D684600D528E1 /* AccountListViewController.swift in Sources */,
|
D6D12B5A292D684600D528E1 /* AccountListViewController.swift in Sources */,
|
||||||
|
@ -2490,6 +2480,7 @@
|
||||||
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2499,12 +2490,11 @@
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.NotificationExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.NotificationExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
|
|
||||||
SUPPORTS_MACCATALYST = YES;
|
SUPPORTS_MACCATALYST = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,7";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
|
@ -2522,6 +2512,7 @@
|
||||||
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2531,11 +2522,10 @@
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.NotificationExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.NotificationExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
|
|
||||||
SUPPORTS_MACCATALYST = YES;
|
SUPPORTS_MACCATALYST = YES;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,7";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
@ -2553,6 +2543,7 @@
|
||||||
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2562,11 +2553,10 @@
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.NotificationExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.NotificationExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
|
|
||||||
SUPPORTS_MACCATALYST = YES;
|
SUPPORTS_MACCATALYST = YES;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,7";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
name = Dist;
|
name = Dist;
|
||||||
};
|
};
|
||||||
|
@ -2618,7 +2608,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
@ -2626,7 +2616,6 @@
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
XROS_DEPLOYMENT_TARGET = 1.1;
|
|
||||||
};
|
};
|
||||||
name = Dist;
|
name = Dist;
|
||||||
};
|
};
|
||||||
|
@ -2642,6 +2631,7 @@
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2708,6 +2698,8 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2735,6 +2727,7 @@
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2763,6 +2756,7 @@
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2791,6 +2785,7 @@
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2861,7 +2856,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
@ -2869,7 +2864,6 @@
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||||
XROS_DEPLOYMENT_TARGET = 1.1;
|
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
|
@ -2921,7 +2915,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
@ -2929,7 +2923,6 @@
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
XROS_DEPLOYMENT_TARGET = 1.1;
|
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
@ -2945,6 +2938,7 @@
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2976,6 +2970,7 @@
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -3083,6 +3078,8 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -3107,6 +3104,8 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
|
|
@ -23,10 +23,10 @@ class LogoutService {
|
||||||
|
|
||||||
func run() {
|
func run() {
|
||||||
let accountInfo = self.accountInfo
|
let accountInfo = self.accountInfo
|
||||||
Task.detached { @MainActor in
|
Task.detached {
|
||||||
if PushManager.shared.pushSubscription(account: accountInfo) != nil {
|
if await PushManager.shared.pushSubscription(account: accountInfo) != nil {
|
||||||
_ = try? await self.mastodonController.run(Pachyderm.PushSubscription.delete())
|
_ = try? await self.mastodonController.run(Pachyderm.PushSubscription.delete())
|
||||||
PushManager.shared.removeSubscription(account: accountInfo)
|
await PushManager.shared.removeSubscription(account: accountInfo)
|
||||||
}
|
}
|
||||||
try? await self.mastodonController.client.revokeAccessToken()
|
try? await self.mastodonController.client.revokeAccessToken()
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,13 +33,7 @@ extension MastodonController {
|
||||||
|
|
||||||
func updatePushSubscription(alerts: PushNotifications.PushSubscription.Alerts, policy: PushNotifications.PushSubscription.Policy) async throws -> Pachyderm.PushSubscription {
|
func updatePushSubscription(alerts: PushNotifications.PushSubscription.Alerts, policy: PushNotifications.PushSubscription.Policy) async throws -> Pachyderm.PushSubscription {
|
||||||
let req = Pachyderm.PushSubscription.update(alerts: .init(alerts), policy: .init(policy))
|
let req = Pachyderm.PushSubscription.update(alerts: .init(alerts), policy: .init(policy))
|
||||||
var result = try await run(req).0
|
return try await run(req).0
|
||||||
if instanceFeatures.pushNotificationPolicyMissingFromResponse {
|
|
||||||
// see https://github.com/mastodon/mastodon/issues/23145
|
|
||||||
// so just assume if the request was successful that it worked
|
|
||||||
result.policy = .init(policy)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func deletePushSubscription() async throws {
|
func deletePushSubscription() async throws {
|
||||||
|
|
|
@ -26,11 +26,7 @@ class SaveToPhotosActivity: UIActivity {
|
||||||
// Just using the symbol image directly causes it to be stretched.
|
// Just using the symbol image directly causes it to be stretched.
|
||||||
let symbol = UIImage(systemName: "square.and.arrow.down", withConfiguration: UIImage.SymbolConfiguration(scale: .large))!
|
let symbol = UIImage(systemName: "square.and.arrow.down", withConfiguration: UIImage.SymbolConfiguration(scale: .large))!
|
||||||
let format = UIGraphicsImageRendererFormat()
|
let format = UIGraphicsImageRendererFormat()
|
||||||
#if os(visionOS)
|
|
||||||
format.scale = 2
|
|
||||||
#else
|
|
||||||
format.scale = UIScreen.main.scale
|
format.scale = UIScreen.main.scale
|
||||||
#endif
|
|
||||||
return UIGraphicsImageRenderer(size: CGSize(width: 76, height: 76), format: format).image { ctx in
|
return UIGraphicsImageRenderer(size: CGSize(width: 76, height: 76), format: format).image { ctx in
|
||||||
let rect = AVMakeRect(aspectRatio: symbol.size, insideRect: CGRect(x: 0, y: 0, width: 76, height: 76))
|
let rect = AVMakeRect(aspectRatio: symbol.size, insideRect: CGRect(x: 0, y: 0, width: 76, height: 76))
|
||||||
symbol.draw(in: rect)
|
symbol.draw(in: rect)
|
||||||
|
|
|
@ -54,12 +54,21 @@ 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) {
|
||||||
|
@ -175,9 +184,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
private func initializePushNotifications() {
|
private func initializePushNotifications() {
|
||||||
UNUserNotificationCenter.current().delegate = self
|
UNUserNotificationCenter.current().delegate = self
|
||||||
Task {
|
Task {
|
||||||
#if canImport(Sentry)
|
|
||||||
PushManager.captureError = { SentrySDK.capture(error: $0) }
|
PushManager.captureError = { SentrySDK.capture(error: $0) }
|
||||||
#endif
|
|
||||||
await PushManager.shared.updateIfNecessary(updateSubscription: {
|
await PushManager.shared.updateIfNecessary(updateSubscription: {
|
||||||
guard let account = UserAccountsManager.shared.getAccount(id: $0.accountID) else {
|
guard let account = UserAccountsManager.shared.getAccount(id: $0.accountID) else {
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -51,7 +51,6 @@ public extension MainActor {
|
||||||
@available(iOS, obsoleted: 17.0)
|
@available(iOS, obsoleted: 17.0)
|
||||||
@available(watchOS, obsoleted: 10.0)
|
@available(watchOS, obsoleted: 10.0)
|
||||||
@available(tvOS, obsoleted: 17.0)
|
@available(tvOS, obsoleted: 17.0)
|
||||||
@available(visionOS 1.0, *)
|
|
||||||
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T {
|
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T {
|
||||||
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
|
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
|
||||||
return try MainActor.assumeIsolated(body)
|
return try MainActor.assumeIsolated(body)
|
||||||
|
|
|
@ -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.userInterfaceStyle
|
window.overrideUserInterfaceStyle = Preferences.shared.theme
|
||||||
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
|
||||||
|
|
|
@ -18,9 +18,7 @@ class VideoControlsViewController: UIViewController {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private let player: AVPlayer
|
private let player: AVPlayer
|
||||||
#if !os(visionOS)
|
|
||||||
@Box private var playbackSpeed: Float
|
@Box private var playbackSpeed: Float
|
||||||
#endif
|
|
||||||
|
|
||||||
private lazy var muteButton = MuteButton().configure {
|
private lazy var muteButton = MuteButton().configure {
|
||||||
$0.addTarget(self, action: #selector(muteButtonPressed), for: .touchUpInside)
|
$0.addTarget(self, action: #selector(muteButtonPressed), for: .touchUpInside)
|
||||||
|
@ -46,13 +44,8 @@ class VideoControlsViewController: UIViewController {
|
||||||
|
|
||||||
private lazy var optionsButton = MenuButton { [unowned self] in
|
private lazy var optionsButton = MenuButton { [unowned self] in
|
||||||
let imageName: String
|
let imageName: String
|
||||||
#if os(visionOS)
|
|
||||||
let playbackSpeed = player.defaultRate
|
|
||||||
#else
|
|
||||||
let playbackSpeed = self.playbackSpeed
|
|
||||||
#endif
|
|
||||||
if #available(iOS 17.0, *) {
|
if #available(iOS 17.0, *) {
|
||||||
switch playbackSpeed {
|
switch self.playbackSpeed {
|
||||||
case 0.5:
|
case 0.5:
|
||||||
imageName = "gauge.with.dots.needle.0percent"
|
imageName = "gauge.with.dots.needle.0percent"
|
||||||
case 1:
|
case 1:
|
||||||
|
@ -68,12 +61,8 @@ class VideoControlsViewController: UIViewController {
|
||||||
imageName = "speedometer"
|
imageName = "speedometer"
|
||||||
}
|
}
|
||||||
let speedMenu = UIMenu(title: "Playback Speed", image: UIImage(systemName: imageName), children: PlaybackSpeed.allCases.map { speed in
|
let speedMenu = UIMenu(title: "Playback Speed", image: UIImage(systemName: imageName), children: PlaybackSpeed.allCases.map { speed in
|
||||||
UIAction(title: speed.displayName, state: playbackSpeed == speed.rate ? .on : .off) { [unowned self] _ in
|
UIAction(title: speed.displayName, state: self.playbackSpeed == speed.rate ? .on : .off) { [unowned self] _ in
|
||||||
#if os(visionOS)
|
|
||||||
self.player.defaultRate = speed.rate
|
|
||||||
#else
|
|
||||||
self.playbackSpeed = speed.rate
|
self.playbackSpeed = speed.rate
|
||||||
#endif
|
|
||||||
if self.player.rate > 0 {
|
if self.player.rate > 0 {
|
||||||
self.player.rate = speed.rate
|
self.player.rate = speed.rate
|
||||||
}
|
}
|
||||||
|
@ -101,20 +90,12 @@ class VideoControlsViewController: UIViewController {
|
||||||
private var scrubbingTargetTime: CMTime?
|
private var scrubbingTargetTime: CMTime?
|
||||||
private var isSeeking = false
|
private var isSeeking = false
|
||||||
|
|
||||||
#if os(visionOS)
|
|
||||||
init(player: AVPlayer) {
|
|
||||||
self.player = player
|
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
init(player: AVPlayer, playbackSpeed: Box<Float>) {
|
init(player: AVPlayer, playbackSpeed: Box<Float>) {
|
||||||
self.player = player
|
self.player = player
|
||||||
self._playbackSpeed = playbackSpeed
|
self._playbackSpeed = playbackSpeed
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
@ -189,11 +170,7 @@ class VideoControlsViewController: UIViewController {
|
||||||
@objc private func scrubbingEnded() {
|
@objc private func scrubbingEnded() {
|
||||||
scrubbingChanged()
|
scrubbingChanged()
|
||||||
if wasPlayingWhenScrubbingStarted {
|
if wasPlayingWhenScrubbingStarted {
|
||||||
#if os(visionOS)
|
|
||||||
player.play()
|
|
||||||
#else
|
|
||||||
player.rate = playbackSpeed
|
player.rate = playbackSpeed
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,8 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
private var item: AVPlayerItem
|
private var item: AVPlayerItem
|
||||||
let player: AVPlayer
|
let player: AVPlayer
|
||||||
|
|
||||||
#if !os(visionOS)
|
|
||||||
@available(iOS, obsoleted: 16.0, message: "Use AVPlayer.defaultRate")
|
@available(iOS, obsoleted: 16.0, message: "Use AVPlayer.defaultRate")
|
||||||
@Box private var playbackSpeed: Float = 1
|
@Box private var playbackSpeed: Float = 1
|
||||||
#endif
|
|
||||||
|
|
||||||
private var isGrayscale: Bool
|
private var isGrayscale: Bool
|
||||||
|
|
||||||
|
@ -127,11 +125,7 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
player.replaceCurrentItem(with: item)
|
player.replaceCurrentItem(with: item)
|
||||||
updateItemObservations()
|
updateItemObservations()
|
||||||
if isPlaying {
|
if isPlaying {
|
||||||
#if os(visionOS)
|
|
||||||
player.play()
|
|
||||||
#else
|
|
||||||
player.rate = playbackSpeed
|
player.rate = playbackSpeed
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -148,20 +142,12 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
[VideoActivityItemSource(asset: item.asset, url: url)]
|
[VideoActivityItemSource(asset: item.asset, url: url)]
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(visionOS)
|
|
||||||
private lazy var overlayVC = VideoOverlayViewController(player: player)
|
|
||||||
#else
|
|
||||||
private lazy var overlayVC = VideoOverlayViewController(player: player, playbackSpeed: _playbackSpeed)
|
private lazy var overlayVC = VideoOverlayViewController(player: player, playbackSpeed: _playbackSpeed)
|
||||||
#endif
|
|
||||||
var contentOverlayAccessoryViewController: UIViewController? {
|
var contentOverlayAccessoryViewController: UIViewController? {
|
||||||
overlayVC
|
overlayVC
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(visionOS)
|
|
||||||
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player)
|
|
||||||
#else
|
|
||||||
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
|
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
|
||||||
#endif
|
|
||||||
|
|
||||||
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
||||||
overlayVC.setVisible(visible)
|
overlayVC.setVisible(visible)
|
||||||
|
|
|
@ -15,9 +15,7 @@ class VideoOverlayViewController: UIViewController {
|
||||||
private static let pauseImage = UIImage(systemName: "pause.fill")!
|
private static let pauseImage = UIImage(systemName: "pause.fill")!
|
||||||
|
|
||||||
private let player: AVPlayer
|
private let player: AVPlayer
|
||||||
#if !os(visionOS)
|
|
||||||
@Box private var playbackSpeed: Float
|
@Box private var playbackSpeed: Float
|
||||||
#endif
|
|
||||||
|
|
||||||
private var dimmingView: UIView!
|
private var dimmingView: UIView!
|
||||||
private var controlsStack: UIStackView!
|
private var controlsStack: UIStackView!
|
||||||
|
@ -26,18 +24,11 @@ class VideoOverlayViewController: UIViewController {
|
||||||
|
|
||||||
private var rateObservation: NSKeyValueObservation?
|
private var rateObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
#if os(visionOS)
|
|
||||||
init(player: AVPlayer) {
|
|
||||||
self.player = player
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
init(player: AVPlayer, playbackSpeed: Box<Float>) {
|
init(player: AVPlayer, playbackSpeed: Box<Float>) {
|
||||||
self.player = player
|
self.player = player
|
||||||
self._playbackSpeed = playbackSpeed
|
self._playbackSpeed = playbackSpeed
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
@ -106,11 +97,7 @@ class VideoOverlayViewController: UIViewController {
|
||||||
if player.rate > 0 {
|
if player.rate > 0 {
|
||||||
player.rate = 0
|
player.rate = 0
|
||||||
} else {
|
} else {
|
||||||
#if os(visionOS)
|
|
||||||
player.play()
|
|
||||||
#else
|
|
||||||
player.rate = playbackSpeed
|
player.rate = playbackSpeed
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import TuskerPreferences
|
|
||||||
|
|
||||||
class MainSplitViewController: UISplitViewController {
|
class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
|
@ -22,7 +21,7 @@ class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
private var tabBarViewController: MainTabBarViewController!
|
private var tabBarViewController: MainTabBarViewController!
|
||||||
|
|
||||||
private var navigationMode: WidescreenNavigationMode!
|
private var navigationMode: Preferences.WidescreenNavigationMode!
|
||||||
private var secondaryNavController: NavigationControllerProtocol! {
|
private var secondaryNavController: NavigationControllerProtocol! {
|
||||||
viewController(for: .secondary) as? NavigationControllerProtocol
|
viewController(for: .secondary) as? NavigationControllerProtocol
|
||||||
}
|
}
|
||||||
|
@ -66,8 +65,6 @@ class MainSplitViewController: UISplitViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
let nav: UIViewController
|
let nav: UIViewController
|
||||||
let visionIdiom = UIUserInterfaceIdiom(rawValue: 6)
|
|
||||||
if [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom) {
|
|
||||||
navigationMode = Preferences.shared.widescreenNavigationMode
|
navigationMode = Preferences.shared.widescreenNavigationMode
|
||||||
switch navigationMode! {
|
switch navigationMode! {
|
||||||
case .stack:
|
case .stack:
|
||||||
|
@ -77,10 +74,6 @@ class MainSplitViewController: UISplitViewController {
|
||||||
case .multiColumn:
|
case .multiColumn:
|
||||||
nav = MultiColumnNavigationController()
|
nav = MultiColumnNavigationController()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
navigationMode = .stack
|
|
||||||
nav = EnhancedNavigationViewController()
|
|
||||||
}
|
|
||||||
setViewController(nav, for: .secondary)
|
setViewController(nav, for: .secondary)
|
||||||
|
|
||||||
// don't unnecesarily construct a content VC unless the we're in actually split mode
|
// don't unnecesarily construct a content VC unless the we're in actually split mode
|
||||||
|
@ -120,10 +113,8 @@ class MainSplitViewController: UISplitViewController {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateNavigationMode(_ mode: WidescreenNavigationMode) {
|
private func updateNavigationMode(_ mode: Preferences.WidescreenNavigationMode) {
|
||||||
let visionIdiom = UIUserInterfaceIdiom(rawValue: 6)
|
guard mode != navigationMode else {
|
||||||
guard [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom),
|
|
||||||
mode != navigationMode else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
navigationMode = mode
|
navigationMode = mode
|
||||||
|
|
|
@ -59,12 +59,7 @@ struct AboutView: View {
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Label {
|
Label("Get Support", systemImage: "envelope")
|
||||||
Text("Get Support")
|
|
||||||
} icon: {
|
|
||||||
Image(systemName: "envelope")
|
|
||||||
.foregroundStyle(.tint)
|
|
||||||
}
|
|
||||||
Spacer()
|
Spacer()
|
||||||
if isGettingLogData {
|
if isGettingLogData {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
|
@ -80,6 +75,7 @@ struct AboutView: View {
|
||||||
Label("Issue Tracker", systemImage: "checklist")
|
Label("Issue Tracker", systemImage: "checklist")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.labelStyle(AboutLinksLabelStyle())
|
||||||
.appGroupedListRowBackground()
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
|
@ -104,9 +100,7 @@ struct AboutView: View {
|
||||||
|
|
||||||
private var appIcon: some View {
|
private var appIcon: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Image("AboutIcon")
|
AppIconView()
|
||||||
.resizable()
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 256 / 6.4))
|
|
||||||
.shadow(radius: 6, y: 3)
|
.shadow(radius: 6, y: 3)
|
||||||
.frame(width: 256, height: 256)
|
.frame(width: 256, height: 256)
|
||||||
|
|
||||||
|
@ -127,6 +121,20 @@ struct AboutView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct AppIconView: UIViewRepresentable {
|
||||||
|
func makeUIView(context: Context) -> UIImageView {
|
||||||
|
let view = UIImageView(image: UIImage(named: "AboutIcon"))
|
||||||
|
view.contentMode = .scaleAspectFit
|
||||||
|
view.layer.cornerRadius = 256 / 6.4
|
||||||
|
view.layer.cornerCurve = .continuous
|
||||||
|
view.layer.masksToBounds = true
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UIImageView, context: Context) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private struct MailSheet: UIViewControllerRepresentable {
|
private struct MailSheet: UIViewControllerRepresentable {
|
||||||
typealias UIViewControllerType = MFMailComposeViewController
|
typealias UIViewControllerType = MFMailComposeViewController
|
||||||
|
|
||||||
|
@ -166,6 +174,15 @@ private struct MailSheet: UIViewControllerRepresentable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct AboutLinksLabelStyle: LabelStyle {
|
||||||
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
|
HStack(alignment: .lastTextBaseline, spacing: 8) {
|
||||||
|
configuration.icon
|
||||||
|
configuration.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct AboutView_Previews: PreviewProvider {
|
struct AboutView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
AboutView()
|
AboutView()
|
||||||
|
|
|
@ -107,7 +107,7 @@ struct FlipEffect: GeometryEffect {
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct WidthPrefKey: PreferenceKey {
|
private struct WidthPrefKey: PreferenceKey {
|
||||||
static let defaultValue: CGFloat = 0
|
static var defaultValue: CGFloat = 0
|
||||||
|
|
||||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||||
value = nextValue()
|
value = nextValue()
|
||||||
|
|
|
@ -292,7 +292,7 @@ private extension AttributeScopes {
|
||||||
private enum HeadingLevelAttributes: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey {
|
private enum HeadingLevelAttributes: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey {
|
||||||
public typealias Value = Int
|
public typealias Value = Int
|
||||||
|
|
||||||
public static let name = "headingLevel"
|
public static var name = "headingLevel"
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension AttributeDynamicLookup {
|
private extension AttributeDynamicLookup {
|
||||||
|
|
|
@ -10,7 +10,6 @@ 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
|
||||||
|
@ -42,7 +41,7 @@ struct AdvancedPrefsView : View {
|
||||||
|
|
||||||
Button("Cancel", role: .cancel) {}
|
Button("Cancel", role: .cancel) {}
|
||||||
Button("Enable") {
|
Button("Enable") {
|
||||||
if let flag = FeatureFlag(rawValue: featureFlagName) {
|
if let flag = Preferences.FeatureFlag(rawValue: featureFlagName) {
|
||||||
preferences.enabledFeatureFlags.insert(flag)
|
preferences.enabledFeatureFlags.insert(flag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,23 +82,6 @@ struct AdvancedPrefsView : View {
|
||||||
HStack {
|
HStack {
|
||||||
Text("iCloud Status")
|
Text("iCloud Status")
|
||||||
Spacer()
|
Spacer()
|
||||||
cloudKitStatusLabel
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.appGroupedListRowBackground()
|
|
||||||
.task {
|
|
||||||
do {
|
|
||||||
let status = try await CKContainer.default().accountStatus()
|
|
||||||
self.cloudKitStatus = status
|
|
||||||
} catch {
|
|
||||||
Logging.general.error("Unable to get CloudKit status: \(String(describing: error))")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var cloudKitStatusLabel: some View {
|
|
||||||
switch cloudKitStatus {
|
switch cloudKitStatus {
|
||||||
case nil:
|
case nil:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
|
@ -117,6 +99,17 @@ struct AdvancedPrefsView : View {
|
||||||
Text(String(describing: cloudKitStatus!))
|
Text(String(describing: cloudKitStatus!))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
.task {
|
||||||
|
do {
|
||||||
|
let status = try await CKContainer.default().accountStatus()
|
||||||
|
self.cloudKitStatus = status
|
||||||
|
} catch {
|
||||||
|
Logging.general.error("Unable to get CloudKit status: \(String(describing: error))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var errorReportingSection: some View {
|
var errorReportingSection: some View {
|
||||||
Section {
|
Section {
|
||||||
|
|
|
@ -1,270 +0,0 @@
|
||||||
//
|
|
||||||
// MockStatusView.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 4/13/24.
|
|
||||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import Pachyderm
|
|
||||||
import WebURL
|
|
||||||
|
|
||||||
struct MockStatusView: View {
|
|
||||||
@ObservedObject private var preferences = Preferences.shared
|
|
||||||
@ScaledMetric(relativeTo: .body) private var attachmentsLabelHeight = 17
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(alignment: .top, spacing: 8) {
|
|
||||||
VStack(spacing: 4) {
|
|
||||||
Image("AboutIcon")
|
|
||||||
.resizable()
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: preferences.avatarStyle.cornerRadiusFraction * 50))
|
|
||||||
.frame(width: 50, height: 50)
|
|
||||||
|
|
||||||
MockMetaIndicatorsView()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
MockDisplayNameLabel()
|
|
||||||
Text(verbatim: "@tusker@example.com")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.font(.body.weight(.light))
|
|
||||||
.lineLimit(1)
|
|
||||||
.truncationMode(.tail)
|
|
||||||
.layoutPriority(-100)
|
|
||||||
Spacer()
|
|
||||||
Text("1h")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.font(.body.weight(.light))
|
|
||||||
}
|
|
||||||
|
|
||||||
MockStatusContentView()
|
|
||||||
|
|
||||||
if preferences.showLinkPreviews {
|
|
||||||
MockStatusCardView()
|
|
||||||
.frame(height: StatusContentContainer.cardViewHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
MockAttachmentsContainerView()
|
|
||||||
.aspectRatio(preferences.showAttachmentsInTimeline ? 16/9 : nil, contentMode: .fill)
|
|
||||||
.frame(height: preferences.showAttachmentsInTimeline ? nil : attachmentsLabelHeight)
|
|
||||||
.padding(.bottom, preferences.showAttachmentsInTimeline && preferences.hideActionsInTimeline ? 8 : 0)
|
|
||||||
|
|
||||||
if !preferences.hideActionsInTimeline {
|
|
||||||
MockStatusActionButtons()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.layoutPriority(100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct MockMetaIndicatorsView: UIViewRepresentable {
|
|
||||||
@ObservedObject private var preferences = Preferences.shared
|
|
||||||
|
|
||||||
func makeUIView(context: Context) -> StatusMetaIndicatorsView {
|
|
||||||
let view = StatusMetaIndicatorsView()
|
|
||||||
view.primaryAxis = .vertical
|
|
||||||
view.secondaryAxisAlignment = .trailing
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIView(_ uiView: StatusMetaIndicatorsView, context: Context) {
|
|
||||||
var indicators: StatusMetaIndicatorsView.Indicator = []
|
|
||||||
if preferences.showIsStatusReplyIcon {
|
|
||||||
indicators.insert(.reply)
|
|
||||||
}
|
|
||||||
if preferences.alwaysShowStatusVisibilityIcon {
|
|
||||||
indicators.insert(.visibility)
|
|
||||||
}
|
|
||||||
uiView.setIndicators(indicators, visibility: .public)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct MockDisplayNameLabel: View {
|
|
||||||
@ObservedObject private var preferences = Preferences.shared
|
|
||||||
@ScaledMetric(relativeTo: .body) private var emojiSize = 17
|
|
||||||
@State var textWithImage = Text("Tusker")
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
displayName
|
|
||||||
.font(.body.weight(.semibold))
|
|
||||||
// don't let the height change depending on whether emojis are present or not
|
|
||||||
.frame(height: emojiSize)
|
|
||||||
.task(id: emojiSize) {
|
|
||||||
let size = CGSize(width: emojiSize, height: emojiSize)
|
|
||||||
let renderer = UIGraphicsImageRenderer(size: size)
|
|
||||||
let image = renderer.image { ctx in
|
|
||||||
let bounds = CGRect(origin: .zero, size: size)
|
|
||||||
UIBezierPath(roundedRect: bounds, cornerRadius: 2).addClip()
|
|
||||||
UIImage(named: "AboutIcon")!.draw(in: bounds)
|
|
||||||
}
|
|
||||||
textWithImage = Text("Tusker \(Image(uiImage: image))")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var displayName: Text {
|
|
||||||
if preferences.hideCustomEmojiInUsernames {
|
|
||||||
Text("Tusker")
|
|
||||||
} else {
|
|
||||||
textWithImage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct MockStatusContentView: View {
|
|
||||||
@ObservedObject private var preferences = Preferences.shared
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Text("This is an example post so you can check out how things look.\n\nThanks for using \(link)!")
|
|
||||||
.lineLimit(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var link: Text {
|
|
||||||
Text("Tusker")
|
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
.underline(preferences.underlineTextLinks)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct MockStatusCardView: UIViewRepresentable {
|
|
||||||
func makeUIView(context: Context) -> StatusCardView {
|
|
||||||
let view = StatusCardView()
|
|
||||||
view.isUserInteractionEnabled = false
|
|
||||||
let card = Card(
|
|
||||||
url: WebURL("https://vaccor.space/tusker")!,
|
|
||||||
title: "Tusker",
|
|
||||||
description: "Tusker is an iOS app for Mastodon",
|
|
||||||
image: WebURL("https://vaccor.space/tusker/img/icon.png")!,
|
|
||||||
kind: .link
|
|
||||||
)
|
|
||||||
view.updateUI(card: card, sensitive: false)
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIView(_ uiView: StatusCardView, context: Context) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private actor MockAttachmentsGenerator {
|
|
||||||
static let shared = MockAttachmentsGenerator()
|
|
||||||
|
|
||||||
private var attachmentURLs: [URL]?
|
|
||||||
|
|
||||||
func getAttachmentURLs(displayScale: CGFloat) -> [URL] {
|
|
||||||
if let attachmentURLs,
|
|
||||||
attachmentURLs.allSatisfy({ FileManager.default.fileExists(atPath: $0.path) }) {
|
|
||||||
return attachmentURLs
|
|
||||||
}
|
|
||||||
|
|
||||||
let size = CGSize(width: 200, height: 200)
|
|
||||||
let bounds = CGRect(origin: .zero, size: size)
|
|
||||||
let format = UIGraphicsImageRendererFormat()
|
|
||||||
format.scale = displayScale
|
|
||||||
let renderer = UIGraphicsImageRenderer(size: size, format: format)
|
|
||||||
|
|
||||||
let firstImage = renderer.image { ctx in
|
|
||||||
UIColor(red: 0x56 / 255, green: 0x03 / 255, blue: 0xad / 255, alpha: 1).setFill()
|
|
||||||
ctx.fill(bounds)
|
|
||||||
ctx.cgContext.concatenate(CGAffineTransform(1, 0, -0.5, 1, 0, 0))
|
|
||||||
for x in 0..<9 {
|
|
||||||
UIColor(red: 0x83 / 255, green: 0x67 / 255, blue: 0xc7 / 255, alpha: 1).setFill()
|
|
||||||
ctx.fill(CGRect(x: CGFloat(x) * 30 + 20, y: 0, width: 15, height: bounds.height))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let secondImage = renderer.image { ctx in
|
|
||||||
UIColor(red: 0x00 / 255, green: 0x43 / 255, blue: 0x85 / 255, alpha: 1).setFill()
|
|
||||||
ctx.fill(bounds)
|
|
||||||
UIColor(red: 0x05 / 255, green: 0xb2 / 255, blue: 0xdc / 255, alpha: 1).setFill()
|
|
||||||
for y in 0..<4 {
|
|
||||||
for x in 0..<5 {
|
|
||||||
let rect = CGRect(x: x * 45 - 5, y: y * 50 + 15, width: 20, height: 20)
|
|
||||||
ctx.cgContext.fillEllipse(in: rect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
UIColor(red: 0x08 / 255, green: 0x7c / 255, blue: 0xa7 / 255, alpha: 1).setFill()
|
|
||||||
for y in 0..<5 {
|
|
||||||
for x in 0..<4 {
|
|
||||||
let rect = CGRect(x: CGFloat(x) * 45 + 22.5, y: CGFloat(y) * 50 - 5, width: 10, height: 10)
|
|
||||||
ctx.cgContext.fillEllipse(in: rect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let tempDirectory = FileManager.default.temporaryDirectory
|
|
||||||
let firstURL = tempDirectory.appendingPathComponent("\(UUID().description)", conformingTo: .png)
|
|
||||||
let secondURL = tempDirectory.appendingPathComponent("\(UUID().description)", conformingTo: .png)
|
|
||||||
|
|
||||||
do {
|
|
||||||
try firstImage.pngData()!.write(to: firstURL)
|
|
||||||
try secondImage.pngData()!.write(to: secondURL)
|
|
||||||
attachmentURLs = [firstURL, secondURL]
|
|
||||||
return [firstURL, secondURL]
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct MockAttachmentsContainerView: View {
|
|
||||||
@State private var attachments: [Attachment] = []
|
|
||||||
@Environment(\.displayScale) private var displayScale
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
MockAttachmentsContainerRepresentable(attachments: attachments)
|
|
||||||
.task {
|
|
||||||
let attachmentURLs = await MockAttachmentsGenerator.shared.getAttachmentURLs(displayScale: displayScale)
|
|
||||||
self.attachments = [
|
|
||||||
.init(id: "1", kind: .image, url: attachmentURLs[0], description: "test"),
|
|
||||||
.init(id: "2", kind: .image, url: attachmentURLs[1], description: nil),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct MockAttachmentsContainerRepresentable: UIViewRepresentable {
|
|
||||||
let attachments: [Attachment]
|
|
||||||
@ObservedObject private var preferences = Preferences.shared
|
|
||||||
|
|
||||||
func makeUIView(context: Context) -> AttachmentsContainerView {
|
|
||||||
let view = AttachmentsContainerView()
|
|
||||||
view.isUserInteractionEnabled = false
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIView(_ uiView: AttachmentsContainerView, context: Context) {
|
|
||||||
uiView.updateUI(attachments: attachments, labelOnly: !preferences.showAttachmentsInTimeline)
|
|
||||||
uiView.contentHidden = preferences.attachmentBlurMode == .always
|
|
||||||
for attachmentView in uiView.attachmentViews.allObjects {
|
|
||||||
attachmentView.updateBadges()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct MockStatusActionButtons: View {
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
Image(systemName: "arrowshape.turn.up.left.fill")
|
|
||||||
.foregroundStyle(.tint)
|
|
||||||
Spacer()
|
|
||||||
Image(systemName: "star.fill")
|
|
||||||
.foregroundStyle(.tint)
|
|
||||||
Spacer()
|
|
||||||
Image(systemName: "repeat")
|
|
||||||
.foregroundStyle(.yellow)
|
|
||||||
Spacer()
|
|
||||||
Image(systemName: "ellipsis")
|
|
||||||
.foregroundStyle(.tint)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
MockStatusView()
|
|
||||||
.frame(height: 300)
|
|
||||||
}
|
|
|
@ -1,4 +1,3 @@
|
||||||
//
|
|
||||||
// AppearancePrefsView.swift
|
// AppearancePrefsView.swift
|
||||||
// Tusker
|
// Tusker
|
||||||
//
|
//
|
||||||
|
@ -8,11 +7,9 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Combine
|
import Combine
|
||||||
import TuskerPreferences
|
|
||||||
|
|
||||||
struct AppearancePrefsView : View {
|
struct AppearancePrefsView : View {
|
||||||
@ObservedObject private var preferences = Preferences.shared
|
@ObservedObject var preferences = Preferences.shared
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
|
||||||
|
|
||||||
private var appearanceChangePublisher: some Publisher<Void, Never> {
|
private var appearanceChangePublisher: some Publisher<Void, Never> {
|
||||||
preferences.$theme
|
preferences.$theme
|
||||||
|
@ -24,7 +21,13 @@ struct AppearancePrefsView: View {
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static let accentColorsAndImages: [(AccentColor, UIImage?)] = AccentColor.allCases.map { color in
|
private var useCircularAvatars: Binding<Bool> = Binding(get: {
|
||||||
|
Preferences.shared.avatarStyle == .circle
|
||||||
|
}) {
|
||||||
|
Preferences.shared.avatarStyle = $0 ? .circle : .roundRect
|
||||||
|
}
|
||||||
|
|
||||||
|
private let accentColorsAndImages: [(Preferences.AccentColor, UIImage?)] = Preferences.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, *) {
|
||||||
|
@ -43,46 +46,21 @@ struct AppearancePrefsView: View {
|
||||||
List {
|
List {
|
||||||
themeSection
|
themeSection
|
||||||
interfaceSection
|
interfaceSection
|
||||||
|
|
||||||
Section("Post Preview") {
|
|
||||||
MockStatusView()
|
|
||||||
.padding(.top, 8)
|
|
||||||
.padding(.horizontal, UIDevice.current.userInterfaceIdiom == .pad ? 8 : 4)
|
|
||||||
}
|
|
||||||
.listRowBackground(mockStatusBackground)
|
|
||||||
|
|
||||||
accountsSection
|
accountsSection
|
||||||
postsSection
|
postsSection
|
||||||
mediaSection
|
|
||||||
}
|
}
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
||||||
.navigationTitle("Appearance")
|
.navigationBarTitle(Text("Appearance"))
|
||||||
}
|
|
||||||
|
|
||||||
private var mockStatusBackground: Color? {
|
|
||||||
#if targetEnvironment(macCatalyst)
|
|
||||||
nil
|
|
||||||
#else
|
|
||||||
if ProcessInfo.processInfo.isiOSAppOnMac {
|
|
||||||
nil
|
|
||||||
} else if !preferences.pureBlackDarkMode {
|
|
||||||
.appBackground
|
|
||||||
} else if colorScheme == .dark {
|
|
||||||
.black
|
|
||||||
} else {
|
|
||||||
.white
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var themeSection: some View {
|
private var themeSection: some View {
|
||||||
Section {
|
Section {
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
Picker(selection: $preferences.theme, label: Text("Theme")) {
|
Picker(selection: $preferences.theme, label: Text("Theme")) {
|
||||||
Text("Use System Theme").tag(Theme.unspecified)
|
Text("Use System Theme").tag(UIUserInterfaceStyle.unspecified)
|
||||||
Text("Light").tag(Theme.light)
|
Text("Light").tag(UIUserInterfaceStyle.light)
|
||||||
Text("Dark").tag(Theme.dark)
|
Text("Dark").tag(UIUserInterfaceStyle.dark)
|
||||||
}
|
}
|
||||||
|
|
||||||
// macOS system dark mode isn't pure black, so this isn't necessary
|
// macOS system dark mode isn't pure black, so this isn't necessary
|
||||||
|
@ -94,7 +72,7 @@ struct AppearancePrefsView: View {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Picker(selection: $preferences.accentColor, label: Text("Accent Color")) {
|
Picker(selection: $preferences.accentColor, label: Text("Accent Color")) {
|
||||||
ForEach(Self.accentColorsAndImages, id: \.0.rawValue) { (color, image) in
|
ForEach(accentColorsAndImages, id: \.0.rawValue) { (color, image) in
|
||||||
HStack {
|
HStack {
|
||||||
Text(color.name)
|
Text(color.name)
|
||||||
if let image {
|
if let image {
|
||||||
|
@ -124,12 +102,8 @@ struct AppearancePrefsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var accountsSection: some View {
|
private var accountsSection: some View {
|
||||||
Section("Accounts") {
|
Section(header: Text("Accounts")) {
|
||||||
Toggle(isOn: Binding(get: {
|
Toggle(isOn: useCircularAvatars) {
|
||||||
preferences.avatarStyle == .circle
|
|
||||||
}, set: {
|
|
||||||
preferences.avatarStyle = $0 ? .circle : .roundRect
|
|
||||||
})) {
|
|
||||||
Text("Use Circular Avatars")
|
Text("Use Circular Avatars")
|
||||||
}
|
}
|
||||||
Toggle(isOn: $preferences.hideCustomEmojiInUsernames) {
|
Toggle(isOn: $preferences.hideCustomEmojiInUsernames) {
|
||||||
|
@ -140,7 +114,7 @@ struct AppearancePrefsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var postsSection: some View {
|
private var postsSection: some View {
|
||||||
Section("Posts") {
|
Section(header: Text("Posts")) {
|
||||||
Toggle(isOn: $preferences.showIsStatusReplyIcon) {
|
Toggle(isOn: $preferences.showIsStatusReplyIcon) {
|
||||||
Text("Show Status Reply Icons")
|
Text("Show Status Reply Icons")
|
||||||
}
|
}
|
||||||
|
@ -172,41 +146,6 @@ struct AppearancePrefsView: View {
|
||||||
}
|
}
|
||||||
.appGroupedListRowBackground()
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mediaSection: some View {
|
|
||||||
Section("Media") {
|
|
||||||
Picker(selection: $preferences.attachmentBlurMode) {
|
|
||||||
ForEach(AttachmentBlurMode.allCases, id: \.self) { mode in
|
|
||||||
Text(mode.displayName).tag(mode)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Text("Blur Media")
|
|
||||||
}
|
|
||||||
|
|
||||||
Toggle(isOn: $preferences.blurMediaBehindContentWarning) {
|
|
||||||
Text("Blur Media Behind Content Warning")
|
|
||||||
}
|
|
||||||
.disabled(preferences.attachmentBlurMode != .useStatusSetting)
|
|
||||||
|
|
||||||
Toggle(isOn: $preferences.automaticallyPlayGifs) {
|
|
||||||
Text("Automatically Play GIFs")
|
|
||||||
}
|
|
||||||
|
|
||||||
Toggle(isOn: $preferences.showUncroppedMediaInline) {
|
|
||||||
Text("Show Uncropped Media Inline")
|
|
||||||
}
|
|
||||||
|
|
||||||
Toggle(isOn: $preferences.showAttachmentBadges) {
|
|
||||||
Text("Show GIF/\(Text("Alt").font(.body.lowercaseSmallCaps())) Badges")
|
|
||||||
}
|
|
||||||
|
|
||||||
Toggle(isOn: $preferences.attachmentAltBadgeInverted) {
|
|
||||||
Text("Show Badge when Missing \(Text("Alt").font(.body.lowercaseSmallCaps()))")
|
|
||||||
}
|
|
||||||
.disabled(!preferences.showAttachmentBadges)
|
|
||||||
}
|
|
||||||
.appGroupedListRowBackground()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
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
|
||||||
|
@ -40,8 +39,8 @@ struct BehaviorPrefsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Picker(selection: $preferences.timelineSyncMode) {
|
Picker(selection: $preferences.timelineSyncMode) {
|
||||||
Text("iCloud").tag(TimelineSyncMode.icloud)
|
Text("iCloud").tag(Preferences.TimelineSyncMode.icloud)
|
||||||
Text("Mastodon").tag(TimelineSyncMode.mastodon)
|
Text("Mastodon").tag(Preferences.TimelineSyncMode.mastodon)
|
||||||
} label: {
|
} label: {
|
||||||
Text("Sync Timeline Position via")
|
Text("Sync Timeline Position via")
|
||||||
}
|
}
|
||||||
|
@ -58,15 +57,13 @@ struct BehaviorPrefsView: View {
|
||||||
Toggle(isOn: $preferences.openLinksInApps) {
|
Toggle(isOn: $preferences.openLinksInApps) {
|
||||||
Text("Open Links in Apps")
|
Text("Open Links in Apps")
|
||||||
}
|
}
|
||||||
#if !targetEnvironment(macCatalyst) && !os(visionOS)
|
#if !os(visionOS)
|
||||||
if !ProcessInfo.processInfo.isiOSAppOnMac {
|
|
||||||
Toggle(isOn: $preferences.useInAppSafari) {
|
Toggle(isOn: $preferences.useInAppSafari) {
|
||||||
Text("Use In-App Safari")
|
Text("Use In-App Safari")
|
||||||
}
|
}
|
||||||
Toggle(isOn: $preferences.inAppSafariAutomaticReaderMode) {
|
Toggle(isOn: $preferences.inAppSafariAutomaticReaderMode) {
|
||||||
Text("Always Use Reader Mode in In-App Safari")
|
Text("Always Use Reader Mode in In-App Safari")
|
||||||
}.disabled(!preferences.useInAppSafari)
|
}.disabled(!preferences.useInAppSafari)
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.appGroupedListRowBackground()
|
.appGroupedListRowBackground()
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
//
|
||||||
|
// MediaPrefsView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 2/22/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct MediaPrefsView: View {
|
||||||
|
@ObservedObject var preferences = Preferences.shared
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
viewingSection
|
||||||
|
}
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
||||||
|
.navigationBarTitle("Media")
|
||||||
|
}
|
||||||
|
|
||||||
|
var viewingSection: some View {
|
||||||
|
Section(header: Text("Viewing")) {
|
||||||
|
Picker(selection: $preferences.attachmentBlurMode) {
|
||||||
|
ForEach(Preferences.AttachmentBlurMode.allCases, id: \.self) { mode in
|
||||||
|
Text(mode.displayName).tag(mode)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("Blur Media")
|
||||||
|
}
|
||||||
|
|
||||||
|
Toggle(isOn: $preferences.blurMediaBehindContentWarning) {
|
||||||
|
Text("Blur Media Behind Content Warning")
|
||||||
|
}
|
||||||
|
.disabled(preferences.attachmentBlurMode != .useStatusSetting)
|
||||||
|
|
||||||
|
Toggle(isOn: $preferences.automaticallyPlayGifs) {
|
||||||
|
Text("Automatically Play GIFs")
|
||||||
|
}
|
||||||
|
|
||||||
|
Toggle(isOn: $preferences.showUncroppedMediaInline) {
|
||||||
|
Text("Show Uncropped Media Inline")
|
||||||
|
}
|
||||||
|
|
||||||
|
Toggle(isOn: $preferences.showAttachmentBadges) {
|
||||||
|
Text("Show GIF/\(Text("Alt").font(.body.lowercaseSmallCaps())) Badges")
|
||||||
|
}
|
||||||
|
|
||||||
|
Toggle(isOn: $preferences.attachmentAltBadgeInverted) {
|
||||||
|
Text("Show Badge when Missing \(Text("Alt").font(.body.lowercaseSmallCaps()))")
|
||||||
|
}
|
||||||
|
.disabled(!preferences.showAttachmentBadges)
|
||||||
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MediaPrefsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
MediaPrefsView()
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import PushNotifications
|
||||||
import TuskerComponents
|
import TuskerComponents
|
||||||
|
|
||||||
struct NotificationsPrefsView: View {
|
struct NotificationsPrefsView: View {
|
||||||
|
@State private var error: NotificationsSetupError?
|
||||||
@ObservedObject private var userAccounts = UserAccountsManager.shared
|
@ObservedObject private var userAccounts = UserAccountsManager.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -47,3 +48,14 @@ struct NotificationsPrefsView: View {
|
||||||
.navigationTitle("Notifications")
|
.navigationTitle("Notifications")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum NotificationsSetupError: LocalizedError {
|
||||||
|
case requestingAuthorization(any Error)
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .requestingAuthorization(let error):
|
||||||
|
"Notifications authorization request failed: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ import TuskerComponents
|
||||||
|
|
||||||
struct PushInstanceSettingsView: View {
|
struct PushInstanceSettingsView: View {
|
||||||
let account: UserAccountInfo
|
let account: UserAccountInfo
|
||||||
let mastodonController: MastodonController
|
|
||||||
@State private var mode: AsyncToggle.Mode
|
@State private var mode: AsyncToggle.Mode
|
||||||
@State private var error: Error?
|
@State private var error: Error?
|
||||||
@State private var subscription: PushNotifications.PushSubscription?
|
@State private var subscription: PushNotifications.PushSubscription?
|
||||||
|
@ -23,7 +22,6 @@ struct PushInstanceSettingsView: View {
|
||||||
@MainActor
|
@MainActor
|
||||||
init(account: UserAccountInfo) {
|
init(account: UserAccountInfo) {
|
||||||
self.account = account
|
self.account = account
|
||||||
self.mastodonController = .getForAccount(account)
|
|
||||||
let subscription = PushManager.shared.pushSubscription(account: account)
|
let subscription = PushManager.shared.pushSubscription(account: account)
|
||||||
self.subscription = subscription
|
self.subscription = subscription
|
||||||
self.mode = subscription == nil ? .off : .on
|
self.mode = subscription == nil ? .off : .on
|
||||||
|
@ -37,7 +35,7 @@ struct PushInstanceSettingsView: View {
|
||||||
AsyncToggle("\(account.instanceURL.host!) notifications enabled", labelHidden: true, mode: $mode, onChange: updateNotificationsEnabled(enabled:))
|
AsyncToggle("\(account.instanceURL.host!) notifications enabled", labelHidden: true, mode: $mode, onChange: updateNotificationsEnabled(enabled:))
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
}
|
}
|
||||||
PushSubscriptionView(account: account, mastodonController: mastodonController, subscription: subscription, updateSubscription: updateSubscription)
|
PushSubscriptionView(account: account, subscription: subscription, updateSubscription: updateSubscription)
|
||||||
}
|
}
|
||||||
.alertWithData("An Error Occurred", data: $error) { data in
|
.alertWithData("An Error Occurred", data: $error) { data in
|
||||||
Button("OK") {}
|
Button("OK") {}
|
||||||
|
|
|
@ -13,13 +13,12 @@ import TuskerComponents
|
||||||
|
|
||||||
struct PushSubscriptionView: View {
|
struct PushSubscriptionView: View {
|
||||||
let account: UserAccountInfo
|
let account: UserAccountInfo
|
||||||
let mastodonController: MastodonController
|
|
||||||
let subscription: PushSubscription?
|
let subscription: PushSubscription?
|
||||||
let updateSubscription: (PushSubscription.Alerts, PushSubscription.Policy) async -> Bool
|
let updateSubscription: (PushSubscription.Alerts, PushSubscription.Policy) async -> Bool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if let subscription {
|
if let subscription {
|
||||||
PushSubscriptionSettingsView(account: account, mastodonController: mastodonController, subscription: subscription, updateSubscription: updateSubscription)
|
PushSubscriptionSettingsView(account: account, subscription: subscription, updateSubscription: updateSubscription)
|
||||||
} else {
|
} else {
|
||||||
Text("No notifications")
|
Text("No notifications")
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
|
@ -30,16 +29,20 @@ struct PushSubscriptionView: View {
|
||||||
|
|
||||||
private struct PushSubscriptionSettingsView: View {
|
private struct PushSubscriptionSettingsView: View {
|
||||||
let account: UserAccountInfo
|
let account: UserAccountInfo
|
||||||
let mastodonController: MastodonController
|
|
||||||
let subscription: PushSubscription
|
let subscription: PushSubscription
|
||||||
let updateSubscription: (PushSubscription.Alerts, PushSubscription.Policy) async -> Bool
|
let updateSubscription: (PushSubscription.Alerts, PushSubscription.Policy) async -> Bool
|
||||||
@State private var isLoading: [PushSubscription.Alerts: Bool] = [:]
|
@State private var isLoading: [PushSubscription.Alerts: Bool] = [:]
|
||||||
|
|
||||||
|
init(account: UserAccountInfo, subscription: PushSubscription, updateSubscription: @escaping (PushSubscription.Alerts, PushSubscription.Policy) async -> Bool) {
|
||||||
|
self.account = account
|
||||||
|
self.subscription = subscription
|
||||||
|
self.updateSubscription = updateSubscription
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
alertsToggles
|
alertsToggles
|
||||||
|
|
||||||
if mastodonController.instanceFeatures.pushNotificationPolicy {
|
|
||||||
AsyncPicker("From", alignment: .trailing, value: .constant(subscription.policy)) { newPolicy in
|
AsyncPicker("From", alignment: .trailing, value: .constant(subscription.policy)) { newPolicy in
|
||||||
await updateSubscription(subscription.alerts, newPolicy)
|
await updateSubscription(subscription.alerts, newPolicy)
|
||||||
} content: {
|
} content: {
|
||||||
|
@ -49,7 +52,6 @@ private struct PushSubscriptionSettingsView: View {
|
||||||
}
|
}
|
||||||
.pickerStyle(.menu)
|
.pickerStyle(.menu)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// this is the default value of the alignment guide, but this modifier is loading bearing
|
// this is the default value of the alignment guide, but this modifier is loading bearing
|
||||||
.alignmentGuide(.prefsAvatar, computeValue: { dimension in
|
.alignmentGuide(.prefsAvatar, computeValue: { dimension in
|
||||||
dimension[.leading]
|
dimension[.leading]
|
||||||
|
@ -61,35 +63,18 @@ private struct PushSubscriptionSettingsView: View {
|
||||||
private var alertsToggles: some View {
|
private var alertsToggles: some View {
|
||||||
GroupBox("Get notifications for") {
|
GroupBox("Get notifications for") {
|
||||||
VStack {
|
VStack {
|
||||||
toggle("All", alert: allSupportedAlertTypes)
|
toggle("All", alert: [.mention, .favorite, .reblog, .follow, .followRequest, .poll, .update])
|
||||||
toggle("Mentions", alert: .mention)
|
toggle("Mentions", alert: .mention)
|
||||||
toggle("Favorites", alert: .favorite)
|
toggle("Favorites", alert: .favorite)
|
||||||
toggle("Reblogs", alert: .reblog)
|
toggle("Reblogs", alert: .reblog)
|
||||||
if mastodonController.instanceFeatures.pushNotificationTypeFollowRequest {
|
|
||||||
toggle("Follows", alert: [.follow, .followRequest])
|
toggle("Follows", alert: [.follow, .followRequest])
|
||||||
} else {
|
|
||||||
toggle("Follows", alert: .follow)
|
|
||||||
}
|
|
||||||
toggle("Polls finishing", alert: .poll)
|
toggle("Polls finishing", alert: .poll)
|
||||||
if mastodonController.instanceFeatures.pushNotificationTypeUpdate {
|
|
||||||
toggle("Edits", alert: .update)
|
toggle("Edits", alert: .update)
|
||||||
}
|
|
||||||
// status notifications not supported until we can enable/disable them in the app
|
// status notifications not supported until we can enable/disable them in the app
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var allSupportedAlertTypes: PushSubscription.Alerts {
|
|
||||||
var alerts: PushSubscription.Alerts = [.mention, .favorite, .reblog, .follow, .poll]
|
|
||||||
if mastodonController.instanceFeatures.pushNotificationTypeFollowRequest {
|
|
||||||
alerts.insert(.followRequest)
|
|
||||||
}
|
|
||||||
if mastodonController.instanceFeatures.pushNotificationTypeUpdate {
|
|
||||||
alerts.insert(.update)
|
|
||||||
}
|
|
||||||
return alerts
|
|
||||||
}
|
|
||||||
|
|
||||||
private func toggle(_ titleKey: LocalizedStringKey, alert: PushSubscription.Alerts) -> some View {
|
private func toggle(_ titleKey: LocalizedStringKey, alert: PushSubscription.Alerts) -> some View {
|
||||||
let binding: Binding<AsyncToggle.Mode> = Binding {
|
let binding: Binding<AsyncToggle.Mode> = Binding {
|
||||||
isLoading[alert] == true ? .loading : subscription.alerts.contains(alert) ? .on : .off
|
isLoading[alert] == true ? .loading : subscription.alerts.contains(alert) ? .on : .off
|
||||||
|
|
|
@ -23,6 +23,7 @@ struct PreferencesView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
accountsSection
|
accountsSection
|
||||||
|
notificationsSection
|
||||||
preferencesSection
|
preferencesSection
|
||||||
aboutSection
|
aboutSection
|
||||||
}
|
}
|
||||||
|
@ -91,27 +92,36 @@ struct PreferencesView: View {
|
||||||
.appGroupedListRowBackground()
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var preferencesSection: some View {
|
private var notificationsSection: some View {
|
||||||
Section {
|
Section {
|
||||||
NavigationLink(destination: AppearancePrefsView()) {
|
|
||||||
PreferenceSectionLabel(title: "Appearance", systemImageName: "textformat", backgroundColor: .indigo)
|
|
||||||
}
|
|
||||||
NavigationLink(destination: BehaviorPrefsView()) {
|
|
||||||
PreferenceSectionLabel(title: "Behavior", systemImageName: "flowchart.fill", backgroundColor: .green)
|
|
||||||
}
|
|
||||||
NavigationLink(isActive: $navigationState.showNotificationPreferences) {
|
NavigationLink(isActive: $navigationState.showNotificationPreferences) {
|
||||||
NotificationsPrefsView()
|
NotificationsPrefsView()
|
||||||
} label: {
|
} label: {
|
||||||
PreferenceSectionLabel(title: "Notifications", systemImageName: "bell.fill", backgroundColor: .red)
|
Text("Notifications")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var preferencesSection: some View {
|
||||||
|
Section {
|
||||||
|
NavigationLink(destination: AppearancePrefsView()) {
|
||||||
|
Text("Appearance")
|
||||||
}
|
}
|
||||||
NavigationLink(destination: ComposingPrefsView()) {
|
NavigationLink(destination: ComposingPrefsView()) {
|
||||||
PreferenceSectionLabel(title: "Composing", systemImageName: "pencil", backgroundColor: .blue)
|
Text("Composing")
|
||||||
|
}
|
||||||
|
NavigationLink(destination: MediaPrefsView()) {
|
||||||
|
Text("Media")
|
||||||
|
}
|
||||||
|
NavigationLink(destination: BehaviorPrefsView()) {
|
||||||
|
Text("Behavior")
|
||||||
}
|
}
|
||||||
NavigationLink(destination: WellnessPrefsView()) {
|
NavigationLink(destination: WellnessPrefsView()) {
|
||||||
PreferenceSectionLabel(title: "Digital Wellness", systemImageName: "brain.fill", backgroundColor: .purple)
|
Text("Digital Wellness")
|
||||||
}
|
}
|
||||||
NavigationLink(destination: AdvancedPrefsView()) {
|
NavigationLink(destination: AdvancedPrefsView()) {
|
||||||
PreferenceSectionLabel(title: "Advanced", systemImageName: "gearshape.2.fill", backgroundColor: .gray)
|
Text("Advanced")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.appGroupedListRowBackground()
|
.appGroupedListRowBackground()
|
||||||
|
@ -119,28 +129,14 @@ struct PreferencesView: View {
|
||||||
|
|
||||||
private var aboutSection: some View {
|
private var aboutSection: some View {
|
||||||
Section {
|
Section {
|
||||||
NavigationLink {
|
NavigationLink("About") {
|
||||||
AboutView()
|
AboutView()
|
||||||
} label: {
|
|
||||||
Label {
|
|
||||||
Text("About")
|
|
||||||
} icon: {
|
|
||||||
Image("AboutIcon")
|
|
||||||
.resizable()
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
|
||||||
.frame(width: 30, height: 30)
|
|
||||||
}
|
}
|
||||||
}
|
NavigationLink("Tip Jar") {
|
||||||
NavigationLink {
|
|
||||||
TipJarView()
|
TipJarView()
|
||||||
} label: {
|
|
||||||
// TODO: custom tip jar icon?
|
|
||||||
PreferenceSectionLabel(title: "Tip Jar", systemImageName: "dollarsign.square.fill", backgroundColor: .yellow)
|
|
||||||
}
|
}
|
||||||
NavigationLink {
|
NavigationLink("Acknowledgements") {
|
||||||
AcknowledgementsView()
|
AcknowledgementsView()
|
||||||
} label: {
|
|
||||||
PreferenceSectionLabel(title: "Acknowledgements", systemImageName: "doc.text.fill", backgroundColor: .gray)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.appGroupedListRowBackground()
|
.appGroupedListRowBackground()
|
||||||
|
@ -151,24 +147,6 @@ struct PreferencesView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct PreferenceSectionLabel: View {
|
|
||||||
let title: LocalizedStringKey
|
|
||||||
let systemImageName: String
|
|
||||||
let backgroundColor: Color
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Label {
|
|
||||||
Text(title)
|
|
||||||
} icon: {
|
|
||||||
Image(systemName: systemImageName)
|
|
||||||
.imageScale(.medium)
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
.frame(width: 30, height: 30)
|
|
||||||
.background(backgroundColor, in: RoundedRectangle(cornerRadius: 6))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//#if DEBUG
|
//#if DEBUG
|
||||||
//struct PreferencesView_Previews : PreviewProvider {
|
//struct PreferencesView_Previews : PreviewProvider {
|
||||||
// static var previews: some View {
|
// static var previews: some View {
|
||||||
|
|
|
@ -59,7 +59,7 @@ struct ConfettiView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct SizeKey: PreferenceKey {
|
private struct SizeKey: PreferenceKey {
|
||||||
static let defaultValue: CGSize = .zero
|
static var defaultValue: CGSize = .zero
|
||||||
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
|
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
|
||||||
value = nextValue()
|
value = nextValue()
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ struct TipJarView: View {
|
||||||
Text(error.localizedDescription)
|
Text(error.localizedDescription)
|
||||||
})
|
})
|
||||||
.task {
|
.task {
|
||||||
updatesObserver = Task.detached { @MainActor in
|
updatesObserver = Task.detached {
|
||||||
await observeTransactionUpdates()
|
await observeTransactionUpdates()
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
|
@ -95,7 +95,6 @@ struct TipJarView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func observeTransactionUpdates() async {
|
private func observeTransactionUpdates() async {
|
||||||
for await verificationResult in StoreKit.Transaction.updates {
|
for await verificationResult in StoreKit.Transaction.updates {
|
||||||
guard let index = products.firstIndex(where: { $0.0.id == verificationResult.unsafePayloadValue.productID }) else {
|
guard let index = products.firstIndex(where: { $0.0.id == verificationResult.unsafePayloadValue.productID }) else {
|
||||||
|
@ -176,7 +175,6 @@ private struct TipRow: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func purchase() async {
|
private func purchase() async {
|
||||||
isPurchasing = true
|
isPurchasing = true
|
||||||
let result: Product.PurchaseResult
|
let result: Product.PurchaseResult
|
||||||
|
@ -231,7 +229,7 @@ extension HorizontalAlignment {
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct ButtonWidthKey: PreferenceKey {
|
private struct ButtonWidthKey: PreferenceKey {
|
||||||
static let defaultValue: CGFloat = 0
|
static var defaultValue: CGFloat = 0
|
||||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||||
value = max(value, nextValue())
|
value = max(value, nextValue())
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,76 +8,63 @@
|
||||||
|
|
||||||
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
|
||||||
@State private var startAnimation = CurrentValueSubject<Bool, Never>(false)
|
@State private var startAnimation = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
private var startAnimationSignal: some Publisher<Void, Never> {
|
|
||||||
startAnimation.filter { $0 }.removeDuplicates().map { _ in () }
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
OptionView(
|
OptionView<StackNavigationPreview>(
|
||||||
content: StackNavigationPreview.self,
|
|
||||||
value: .stack,
|
value: .stack,
|
||||||
selection: $preferences.widescreenNavigationMode,
|
selection: $preferences.widescreenNavigationMode,
|
||||||
startAnimation: startAnimationSignal
|
startAnimation: startAnimation
|
||||||
) {
|
) {
|
||||||
Text("Stack")
|
Text("Stack")
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(minLength: 32)
|
Spacer(minLength: 32)
|
||||||
|
|
||||||
OptionView(
|
OptionView<SplitNavigationPreview>(
|
||||||
content: SplitNavigationPreview.self,
|
|
||||||
value: .splitScreen,
|
value: .splitScreen,
|
||||||
selection: $preferences.widescreenNavigationMode,
|
selection: $preferences.widescreenNavigationMode,
|
||||||
startAnimation: startAnimationSignal
|
startAnimation: startAnimation
|
||||||
) {
|
) {
|
||||||
Text("Split Screen")
|
Text("Split Screen")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if preferences.hasFeatureFlag(.iPadMultiColumn) {
|
||||||
Spacer(minLength: 32)
|
Spacer(minLength: 32)
|
||||||
|
|
||||||
OptionView(
|
OptionView<MultiColumnNavigationPreview>(
|
||||||
content: MultiColumnNavigationPreview.self,
|
|
||||||
value: .multiColumn,
|
value: .multiColumn,
|
||||||
selection: $preferences.widescreenNavigationMode,
|
selection: $preferences.widescreenNavigationMode,
|
||||||
startAnimation: startAnimationSignal
|
startAnimation: startAnimation
|
||||||
) {
|
) {
|
||||||
Text("Multi-Column")
|
Text("Multi-Column")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(height: 100)
|
.frame(height: 100)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
|
||||||
startAnimation.send(true)
|
startAnimation.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct OptionView<Content: NavigationModePreview, P: Publisher<Void, Never>>: View {
|
private struct OptionView<Content: NavigationModePreview>: View {
|
||||||
let value: WidescreenNavigationMode
|
let value: Preferences.WidescreenNavigationMode
|
||||||
@Binding var selection: WidescreenNavigationMode
|
@Binding var selection: Preferences.WidescreenNavigationMode
|
||||||
let startAnimation: P
|
let startAnimation: PassthroughSubject<Void, Never>
|
||||||
let label: Text
|
@ViewBuilder let label: Text
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
init(content _: Content.Type, value: WidescreenNavigationMode, selection: Binding<WidescreenNavigationMode>, startAnimation: P, @ViewBuilder label: () -> Text) {
|
|
||||||
self.value = value
|
|
||||||
self._selection = selection
|
|
||||||
self.startAnimation = startAnimation
|
|
||||||
self.label = label()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var selected: Bool {
|
private var selected: Bool {
|
||||||
selection == value
|
selection == value
|
||||||
}
|
}
|
||||||
|
@ -96,7 +83,7 @@ private struct OptionView<Content: NavigationModePreview, P: Publisher<Void, Nev
|
||||||
}
|
}
|
||||||
|
|
||||||
private var preview: some View {
|
private var preview: some View {
|
||||||
NavigationModeRepresentable(content: Content.self, startAnimation: startAnimation)
|
NavigationModeRepresentable<Content>(startAnimation: startAnimation)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12.5, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: 12.5, style: .continuous))
|
||||||
.overlay {
|
.overlay {
|
||||||
RoundedRectangle(cornerRadius: 12.5, style: .continuous)
|
RoundedRectangle(cornerRadius: 12.5, style: .continuous)
|
||||||
|
@ -118,15 +105,11 @@ private struct WideCapsule: Shape {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private protocol NavigationModePreview: UIView {
|
private protocol NavigationModePreview: UIView {
|
||||||
init(startAnimation: some Publisher<Void, Never>)
|
init(startAnimation: PassthroughSubject<Void, Never>)
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct NavigationModeRepresentable<UIViewType: NavigationModePreview, P: Publisher<Void, Never>>: UIViewRepresentable {
|
private struct NavigationModeRepresentable<UIViewType: NavigationModePreview>: UIViewRepresentable {
|
||||||
let startAnimation: P
|
let startAnimation: PassthroughSubject<Void, Never>
|
||||||
|
|
||||||
init(content _: UIViewType.Type, startAnimation: P) {
|
|
||||||
self.startAnimation = startAnimation
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UIViewType {
|
func makeUIView(context: Context) -> UIViewType {
|
||||||
UIViewType(startAnimation: startAnimation)
|
UIViewType(startAnimation: startAnimation)
|
||||||
|
@ -144,7 +127,7 @@ private final class StackNavigationPreview: UIView, NavigationModePreview {
|
||||||
private let destinationView = UIView()
|
private let destinationView = UIView()
|
||||||
private var cancellable: AnyCancellable?
|
private var cancellable: AnyCancellable?
|
||||||
|
|
||||||
init(startAnimation: some Publisher<Void, Never>) {
|
init(startAnimation: PassthroughSubject<Void, Never>) {
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
backgroundColor = .appBackground
|
backgroundColor = .appBackground
|
||||||
|
@ -219,7 +202,7 @@ private final class SplitNavigationPreview: UIView, NavigationModePreview {
|
||||||
private var cellStackTrailingConstraint: NSLayoutConstraint!
|
private var cellStackTrailingConstraint: NSLayoutConstraint!
|
||||||
private var cancellable: AnyCancellable?
|
private var cancellable: AnyCancellable?
|
||||||
|
|
||||||
init(startAnimation: some Publisher<Void, Never>) {
|
init(startAnimation: PassthroughSubject<Void, Never>) {
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
backgroundColor = .appBackground
|
backgroundColor = .appBackground
|
||||||
|
@ -313,7 +296,7 @@ private final class MultiColumnNavigationPreview: UIView, NavigationModePreview
|
||||||
|
|
||||||
private var startedAnimation = false
|
private var startedAnimation = false
|
||||||
|
|
||||||
init(startAnimation: some Publisher<Void, Never>) {
|
init(startAnimation: PassthroughSubject<Void, Never>) {
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
backgroundColor = .appSecondaryBackground
|
backgroundColor = .appSecondaryBackground
|
|
@ -47,11 +47,10 @@ extension TuskerNavigationDelegate {
|
||||||
|
|
||||||
func selected(url: URL, allowResolveStatuses: Bool = true, allowUniversalLinks: Bool = true) {
|
func selected(url: URL, allowResolveStatuses: Bool = true, allowUniversalLinks: Bool = true) {
|
||||||
func openSafari() {
|
func openSafari() {
|
||||||
#if targetEnvironment(macCatalyst) || os(visionOS)
|
#if os(visionOS)
|
||||||
UIApplication.shared.open(url)
|
UIApplication.shared.open(url)
|
||||||
#else
|
#else
|
||||||
if !ProcessInfo.processInfo.isiOSAppOnMac,
|
if Preferences.shared.useInAppSafari,
|
||||||
Preferences.shared.useInAppSafari,
|
|
||||||
url.scheme == "https" || url.scheme == "http" {
|
url.scheme == "https" || url.scheme == "http" {
|
||||||
let config = SFSafariViewController.Configuration()
|
let config = SFSafariViewController.Configuration()
|
||||||
config.entersReaderIfAvailable = Preferences.shared.inAppSafariAutomaticReaderMode
|
config.entersReaderIfAvailable = Preferences.shared.inAppSafariAutomaticReaderMode
|
||||||
|
|
|
@ -111,10 +111,6 @@ class AttachmentView: GIFImageView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateBadges() {
|
|
||||||
createBadgesView(getBadges())
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func gifPlaybackModeChanged() {
|
@objc private func gifPlaybackModeChanged() {
|
||||||
// NSProcessInfoPowerStateDidChange is sometimes fired on a background thread
|
// NSProcessInfoPowerStateDidChange is sometimes fired on a background thread
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
@ -374,8 +370,6 @@ class AttachmentView: GIFImageView {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.badgeContainer?.removeFromSuperview()
|
|
||||||
|
|
||||||
let stack = UIStackView()
|
let stack = UIStackView()
|
||||||
self.badgeContainer = stack
|
self.badgeContainer = stack
|
||||||
stack.axis = .horizontal
|
stack.axis = .horizontal
|
||||||
|
|
|
@ -72,35 +72,21 @@ class AttachmentsContainerView: UIView {
|
||||||
func updateUI(attachments: [Attachment], labelOnly: Bool = false) {
|
func updateUI(attachments: [Attachment], labelOnly: Bool = false) {
|
||||||
let newTokens = attachments.map { AttachmentToken(attachment: $0) }
|
let newTokens = attachments.map { AttachmentToken(attachment: $0) }
|
||||||
|
|
||||||
guard labelOnly != (label != nil) || self.attachmentTokens != newTokens else {
|
guard !labelOnly else {
|
||||||
self.attachments = attachments
|
self.attachments = attachments
|
||||||
self.attachmentTokens = newTokens
|
self.attachmentTokens = newTokens
|
||||||
|
updateLabel(attachments: attachments)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard self.attachmentTokens != newTokens else {
|
||||||
self.isHidden = attachments.isEmpty
|
self.isHidden = attachments.isEmpty
|
||||||
if labelOnly && !attachments.isEmpty {
|
|
||||||
updateLabel(attachments: attachments)
|
|
||||||
} else {
|
|
||||||
label?.removeFromSuperview()
|
|
||||||
label = nil
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.attachments = attachments
|
self.attachments = attachments
|
||||||
self.attachmentTokens = newTokens
|
self.attachmentTokens = newTokens
|
||||||
|
|
||||||
if labelOnly {
|
|
||||||
if !attachments.isEmpty {
|
|
||||||
updateLabel(attachments: attachments)
|
|
||||||
} else {
|
|
||||||
label?.removeFromSuperview()
|
|
||||||
label = nil
|
|
||||||
}
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
label?.removeFromSuperview()
|
|
||||||
label = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAttachmentViews()
|
removeAttachmentViews()
|
||||||
hideButtonView?.isHidden = false
|
hideButtonView?.isHidden = false
|
||||||
|
|
||||||
|
|
|
@ -29,9 +29,7 @@ class GifvController {
|
||||||
self.isGrayscale = Preferences.shared.grayscaleImages
|
self.isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
|
||||||
player.isMuted = true
|
player.isMuted = true
|
||||||
#if !os(visionOS)
|
|
||||||
player.preventsDisplaySleepDuringVideoPlayback = false
|
player.preventsDisplaySleepDuringVideoPlayback = false
|
||||||
#endif
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(restartItem), name: .AVPlayerItemDidPlayToEndTime, object: item)
|
NotificationCenter.default.addObserver(self, selector: #selector(restartItem), name: .AVPlayerItemDidPlayToEndTime, object: item)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
|
|
|
@ -20,8 +20,8 @@ class StatusCardView: UIView {
|
||||||
private var statusID: String?
|
private var statusID: String?
|
||||||
private(set) var card: Card?
|
private(set) var card: Card?
|
||||||
|
|
||||||
private static let activeBackgroundColor = UIColor.secondarySystemFill
|
private let activeBackgroundColor = UIColor.secondarySystemFill
|
||||||
private static let inactiveBackgroundColor = UIColor.secondarySystemBackground
|
private let inactiveBackgroundColor = UIColor.secondarySystemBackground
|
||||||
|
|
||||||
private var isGrayscale = false
|
private var isGrayscale = false
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ class StatusCardView: UIView {
|
||||||
hStack.clipsToBounds = true
|
hStack.clipsToBounds = true
|
||||||
hStack.layer.borderWidth = 0.5
|
hStack.layer.borderWidth = 0.5
|
||||||
hStack.layer.cornerCurve = .continuous
|
hStack.layer.cornerCurve = .continuous
|
||||||
hStack.backgroundColor = StatusCardView.inactiveBackgroundColor
|
hStack.backgroundColor = inactiveBackgroundColor
|
||||||
updateBorderColor()
|
updateBorderColor()
|
||||||
|
|
||||||
addSubview(hStack)
|
addSubview(hStack)
|
||||||
|
@ -173,12 +173,8 @@ class StatusCardView: UIView {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUI(card: card, sensitive: status.sensitive)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUI(card: Card, sensitive: Bool) {
|
|
||||||
if let image = card.image {
|
if let image = card.image {
|
||||||
if sensitive {
|
if status.sensitive {
|
||||||
if let blurhash = card.blurhash {
|
if let blurhash = card.blurhash {
|
||||||
imageView.blurImage = false
|
imageView.blurImage = false
|
||||||
imageView.showOnlyBlurHash(blurhash, for: URL(image)!)
|
imageView.showOnlyBlurHash(blurhash, for: URL(image)!)
|
||||||
|
@ -223,7 +219,7 @@ class StatusCardView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
hStack.backgroundColor = StatusCardView.activeBackgroundColor
|
hStack.backgroundColor = activeBackgroundColor
|
||||||
setNeedsDisplay()
|
setNeedsDisplay()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,7 +227,7 @@ class StatusCardView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
hStack.backgroundColor = StatusCardView.inactiveBackgroundColor
|
hStack.backgroundColor = inactiveBackgroundColor
|
||||||
setNeedsDisplay()
|
setNeedsDisplay()
|
||||||
|
|
||||||
if let card = card, let delegate = navigationDelegate {
|
if let card = card, let delegate = navigationDelegate {
|
||||||
|
@ -240,7 +236,7 @@ class StatusCardView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
hStack.backgroundColor = StatusCardView.inactiveBackgroundColor
|
hStack.backgroundColor = inactiveBackgroundColor
|
||||||
setNeedsDisplay()
|
setNeedsDisplay()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,35 +80,20 @@ class StatusMetaIndicatorsView: UIView {
|
||||||
}
|
}
|
||||||
statusID = status.id
|
statusID = status.id
|
||||||
|
|
||||||
var indicators: Indicator = []
|
var images: [UIImage] = []
|
||||||
|
|
||||||
if allowedIndicators.contains(.reply) && Preferences.shared.showIsStatusReplyIcon && status.inReplyToID != nil {
|
if allowedIndicators.contains(.reply) && Preferences.shared.showIsStatusReplyIcon && status.inReplyToID != nil {
|
||||||
indicators.insert(.reply)
|
images.append(UIImage(systemName: "bubble.left.and.bubble.right")!)
|
||||||
}
|
}
|
||||||
|
|
||||||
if allowedIndicators.contains(.visibility) && Preferences.shared.alwaysShowStatusVisibilityIcon {
|
if allowedIndicators.contains(.visibility) && Preferences.shared.alwaysShowStatusVisibilityIcon {
|
||||||
indicators.insert(.visibility)
|
images.append(UIImage(systemName: status.visibility.unfilledImageName)!)
|
||||||
}
|
}
|
||||||
|
|
||||||
if allowedIndicators.contains(.localOnly) && status.localOnly {
|
if allowedIndicators.contains(.localOnly) && status.localOnly {
|
||||||
indicators.insert(.localOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
setIndicators(indicators, visibility: status.visibility)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used by MockStatusView
|
|
||||||
func setIndicators(_ indicators: Indicator, visibility: Visibility) {
|
|
||||||
var images: [UIImage] = []
|
|
||||||
if indicators.contains(.reply) {
|
|
||||||
images.append(UIImage(systemName: "bubble.left.and.bubble.right")!)
|
|
||||||
}
|
|
||||||
if indicators.contains(.visibility) {
|
|
||||||
images.append(UIImage(systemName: visibility.unfilledImageName)!)
|
|
||||||
}
|
|
||||||
if indicators.contains(.localOnly) {
|
|
||||||
images.append(UIImage(named: "link.broken")!)
|
images.append(UIImage(named: "link.broken")!)
|
||||||
}
|
}
|
||||||
|
|
||||||
let views = images.map {
|
let views = images.map {
|
||||||
let v = UIImageView(image: $0)
|
let v = UIImageView(image: $0)
|
||||||
v.translatesAutoresizingMaskIntoConstraints = false
|
v.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
|
@ -47,4 +47,30 @@ class AttributedStringHelperTests: XCTestCase {
|
||||||
XCTAssertEqual(d, NSAttributedString(string: "abc"))
|
XCTAssertEqual(d, NSAttributedString(string: "abc"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCollapsingWhitespace() {
|
||||||
|
var str = NSAttributedString(string: "test 1\n")
|
||||||
|
XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "test 1\n"))
|
||||||
|
|
||||||
|
str = NSAttributedString(string: "test 2 \n")
|
||||||
|
XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "test 2\n"))
|
||||||
|
|
||||||
|
str = NSAttributedString(string: "test 3\n ")
|
||||||
|
XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "test 3\n"))
|
||||||
|
|
||||||
|
str = NSAttributedString(string: "test 4 \n ")
|
||||||
|
XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "test 4\n"))
|
||||||
|
|
||||||
|
str = NSAttributedString(string: "test 5 \n blah")
|
||||||
|
XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "test 5\nblah"))
|
||||||
|
|
||||||
|
str = NSAttributedString(string: "\ntest 6")
|
||||||
|
XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "\ntest 6"))
|
||||||
|
|
||||||
|
str = NSAttributedString(string: " \ntest 7")
|
||||||
|
XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "\ntest 7"))
|
||||||
|
|
||||||
|
str = NSAttributedString(string: " \n test 8")
|
||||||
|
XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "\ntest 8"))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
// https://help.apple.com/xcode/#/dev745c5c974
|
// https://help.apple.com/xcode/#/dev745c5c974
|
||||||
|
|
||||||
MARKETING_VERSION = 2024.2
|
MARKETING_VERSION = 2024.2
|
||||||
CURRENT_PROJECT_VERSION = 121
|
CURRENT_PROJECT_VERSION = 120
|
||||||
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
|
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
|
||||||
|
|
||||||
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev
|
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev
|
||||||
|
|
Loading…
Reference in New Issue