Compare commits

...

13 Commits

36 changed files with 479 additions and 277 deletions

View File

@ -301,6 +301,7 @@ 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)

View File

@ -181,13 +181,8 @@ 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)) {
if let imageName = format.imageName { Image(systemName: 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)

View File

@ -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,16 +31,8 @@ enum StatusFormat: Int, CaseIterable {
return "bold" return "bold"
case .strikethrough: case .strikethrough:
return "strikethrough" return "strikethrough"
default: case .code:
return nil return "chevron.left.forwardslash.chevron.right"
}
}
var title: (String, [NSAttributedString.Key: Any])? {
if self == .code {
return ("</>", [.font: UIFont(name: "Menlo", size: 17)!])
} else {
return nil
} }
} }

View File

@ -259,11 +259,7 @@ 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
var image: UIImage? return UIAction(title: fmt.accessibilityLabel, image: UIImage(systemName: fmt.imageName)) { [weak self] _ in
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)
} }
}) })

View File

@ -9,8 +9,10 @@ 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
@ -19,7 +21,9 @@ 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
@ -27,6 +31,11 @@ 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
@ -40,6 +49,7 @@ public struct AsyncPicker<V: Hashable, Content: View>: View {
picker picker
} }
} }
#endif
} }
private var picker: some View { private var picker: some View {

View File

@ -10,19 +10,28 @@ 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
@ -36,6 +45,7 @@ public struct AsyncToggle: View {
toggleOrSpinner toggleOrSpinner
} }
} }
#endif
} }
@ViewBuilder @ViewBuilder

View File

@ -8,10 +8,21 @@
import Foundation import Foundation
import Pachyderm import Pachyderm
struct StatusContentTypeKey: PreferenceKey { struct StatusContentTypeKey: MigratablePreferenceKey {
static var defaultValue: StatusContentType { .plain } static var defaultValue: StatusContentType { .plain }
} }
struct FeatureFlagsKey: PreferenceKey { struct FeatureFlagsKey: MigratablePreferenceKey, CustomCodablePreferenceKey {
static var defaultValue: Set<FeatureFlag> { [] } 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:)))
}
} }

View File

@ -8,26 +8,42 @@
import Foundation import Foundation
import UIKit import UIKit
struct ThemeKey: PreferenceKey { struct ThemeKey: MigratablePreferenceKey {
static var defaultValue: Theme { .unspecified } static var defaultValue: Theme { .unspecified }
} }
struct AccentColorKey: PreferenceKey { struct AccentColorKey: MigratablePreferenceKey {
static var defaultValue: AccentColor { .default } static var defaultValue: AccentColor { .default }
} }
struct AvatarStyleKey: PreferenceKey { struct AvatarStyleKey: MigratablePreferenceKey {
static var defaultValue: AvatarStyle { .roundRect } static var defaultValue: AvatarStyle { .roundRect }
} }
struct LeadingSwipeActionsKey: PreferenceKey { struct LeadingSwipeActionsKey: MigratablePreferenceKey {
static var defaultValue: [StatusSwipeAction] { [.favorite, .reblog] } static var defaultValue: [StatusSwipeAction] { [.favorite, .reblog] }
} }
struct TrailingSwipeActionsKey: PreferenceKey { struct TrailingSwipeActionsKey: MigratablePreferenceKey {
static var defaultValue: [StatusSwipeAction] { [.reply, .share] } static var defaultValue: [StatusSwipeAction] { [.reply, .share] }
} }
struct WidescreenNavigationModeKey: PreferenceKey { struct WidescreenNavigationModeKey: MigratablePreferenceKey {
static var defaultValue: WidescreenNavigationMode { .splitScreen } static var defaultValue: WidescreenNavigationMode { .multiColumn }
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
}
}
} }

View File

@ -7,11 +7,11 @@
import Foundation import Foundation
struct OppositeCollapseKeywordsKey: PreferenceKey { struct OppositeCollapseKeywordsKey: MigratablePreferenceKey {
static var defaultValue: [String] { [] } static var defaultValue: [String] { [] }
} }
struct ConfirmReblogKey: PreferenceKey { struct ConfirmReblogKey: MigratablePreferenceKey {
static var defaultValue: Bool { static var defaultValue: Bool {
#if os(visionOS) #if os(visionOS)
true true
@ -21,6 +21,20 @@ struct ConfirmReblogKey: PreferenceKey {
} }
} }
struct TimelineSyncModeKey: PreferenceKey { struct TimelineSyncModeKey: MigratablePreferenceKey {
static var defaultValue: TimelineSyncMode { .icloud } 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
}
}

View File

@ -7,10 +7,10 @@
import Foundation import Foundation
struct TrueKey: PreferenceKey { struct TrueKey: MigratablePreferenceKey {
static var defaultValue: Bool { true } static var defaultValue: Bool { true }
} }
struct FalseKey: PreferenceKey { struct FalseKey: MigratablePreferenceKey {
static var defaultValue: Bool { false } static var defaultValue: Bool { false }
} }

View File

@ -7,14 +7,14 @@
import Foundation import Foundation
struct PostVisibilityKey: PreferenceKey { struct PostVisibilityKey: MigratablePreferenceKey {
static var defaultValue: PostVisibility { .serverDefault } static var defaultValue: PostVisibility { .serverDefault }
} }
struct ReplyVisibilityKey: PreferenceKey { struct ReplyVisibilityKey: MigratablePreferenceKey {
static var defaultValue: ReplyVisibility { .sameAsPost } static var defaultValue: ReplyVisibility { .sameAsPost }
} }
struct ContentWarningCopyModeKey: PreferenceKey { struct ContentWarningCopyModeKey: MigratablePreferenceKey {
static var defaultValue: ContentWarningCopyMode { .asIs } static var defaultValue: ContentWarningCopyMode { .asIs }
} }

View File

@ -7,6 +7,6 @@
import Foundation import Foundation
struct NotificationsModeKey: PreferenceKey { struct NotificationsModeKey: MigratablePreferenceKey {
static var defaultValue: NotificationsMode { .allNotifications } static var defaultValue: NotificationsMode { .allNotifications }
} }

View File

@ -1,20 +0,0 @@
//
// MediaKeys.swift
// TuskerPreferences
//
// Created by Shadowfacts on 4/13/24.
//
import Foundation
struct AttachmentBlurModeKey: PreferenceKey {
static var defaultValue: AttachmentBlurMode { .useStatusSetting }
static func didSet(in store: PreferenceStore, newValue: AttachmentBlurMode) {
if newValue == .always {
store.blurMediaBehindContentWarning = true
} else if newValue == .never {
store.blurMediaBehindContentWarning = false
}
}
}

View File

@ -10,7 +10,7 @@ import UIKit
extension PreferenceStore { extension PreferenceStore {
func migrate(from legacy: LegacyPreferences) { func migrate(from legacy: LegacyPreferences) {
let migrations: [any MigrationProtocol] = [ var migrations: [any MigrationProtocol] = [
Migration(from: \.theme.theme, to: \.$theme), Migration(from: \.theme.theme, to: \.$theme),
Migration(from: \.pureBlackDarkMode, to: \.$pureBlackDarkMode), Migration(from: \.pureBlackDarkMode, to: \.$pureBlackDarkMode),
Migration(from: \.accentColor, to: \.$accentColor), Migration(from: \.accentColor, to: \.$accentColor),
@ -41,8 +41,6 @@ extension PreferenceStore {
Migration(from: \.attachmentAltBadgeInverted, to: \.$attachmentAltBadgeInverted), Migration(from: \.attachmentAltBadgeInverted, to: \.$attachmentAltBadgeInverted),
Migration(from: \.openLinksInApps, to: \.$openLinksInApps), Migration(from: \.openLinksInApps, to: \.$openLinksInApps),
Migration(from: \.useInAppSafari, to: \.$useInAppSafari),
Migration(from: \.inAppSafariAutomaticReaderMode, to: \.$inAppSafariAutomaticReaderMode),
Migration(from: \.expandAllContentWarnings, to: \.$expandAllContentWarnings), Migration(from: \.expandAllContentWarnings, to: \.$expandAllContentWarnings),
Migration(from: \.collapseLongPosts, to: \.$collapseLongPosts), Migration(from: \.collapseLongPosts, to: \.$collapseLongPosts),
Migration(from: \.oppositeCollapseKeywords, to: \.$oppositeCollapseKeywords), Migration(from: \.oppositeCollapseKeywords, to: \.$oppositeCollapseKeywords),
@ -65,6 +63,12 @@ extension PreferenceStore {
Migration(from: \.hasShownLocalTimelineDescription, to: \.$hasShownLocalTimelineDescription), Migration(from: \.hasShownLocalTimelineDescription, to: \.$hasShownLocalTimelineDescription),
Migration(from: \.hasShownFederatedTimelineDescription, to: \.$hasShownFederatedTimelineDescription), 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 { for migration in migrations {
migration.migrate(from: legacy, to: self) migration.migrate(from: legacy, to: self)
@ -76,13 +80,13 @@ private protocol MigrationProtocol {
func migrate(from legacy: LegacyPreferences, to store: PreferenceStore) func migrate(from legacy: LegacyPreferences, to store: PreferenceStore)
} }
private struct Migration<Key: PreferenceKey>: MigrationProtocol where Key.Value: Equatable { private struct Migration<Key: MigratablePreferenceKey>: MigrationProtocol where Key.Value: Equatable {
let from: KeyPath<LegacyPreferences, Key.Value> let from: KeyPath<LegacyPreferences, Key.Value>
let to: KeyPath<PreferenceStore, PreferencePublisher<Key>> let to: KeyPath<PreferenceStore, PreferencePublisher<Key>>
func migrate(from legacy: LegacyPreferences, to store: PreferenceStore) { func migrate(from legacy: LegacyPreferences, to store: PreferenceStore) {
let value = legacy[keyPath: from] let value = legacy[keyPath: from]
if value != Key.defaultValue { if Key.shouldMigrate(oldValue: value) {
Preference.set(enclosingInstance: store, storage: to.appending(path: \.preference), newValue: value) Preference.set(enclosingInstance: store, storage: to.appending(path: \.preference), newValue: value)
} }
} }

View File

@ -27,17 +27,26 @@ final class Preference<Key: PreferenceKey>: Codable {
} }
init(from decoder: any Decoder) throws { init(from decoder: any Decoder) throws {
if let container = try? decoder.singleValueContainer() { 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) self.storedValue = try? container.decode(Key.Value.self)
} }
} }
func encode(to encoder: any Encoder) throws { func encode(to encoder: any Encoder) throws {
if let storedValue { 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() var container = encoder.singleValueContainer()
try container.encode(storedValue) try container.encode(storedValue)
} }
} }
}
static subscript( static subscript(
_enclosingInstance instance: PreferenceStore, _enclosingInstance instance: PreferenceStore,

View File

@ -18,3 +18,18 @@ public protocol PreferenceKey {
extension PreferenceKey { extension PreferenceKey {
static func didSet(in store: PreferenceStore, newValue: Value) {} 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?
}

View File

@ -25,6 +25,12 @@ public final class PreferenceStore: ObservableObject, Codable {
@Preference<WidescreenNavigationModeKey> public var widescreenNavigationMode @Preference<WidescreenNavigationModeKey> public var widescreenNavigationMode
@Preference<FalseKey> public var underlineTextLinks @Preference<FalseKey> public var underlineTextLinks
@Preference<TrueKey> public var showAttachmentsInTimeline @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 // MARK: Composing
@Preference<PostVisibilityKey> public var defaultPostVisibility @Preference<PostVisibilityKey> public var defaultPostVisibility
@ -34,17 +40,9 @@ public final class PreferenceStore: ObservableObject, Codable {
@Preference<FalseKey> public var mentionReblogger @Preference<FalseKey> public var mentionReblogger
@Preference<FalseKey> public var useTwitterKeyboard @Preference<FalseKey> public var useTwitterKeyboard
// MARK: Media
@Preference<AttachmentBlurModeKey> public var attachmentBlurMode
@Preference<TrueKey> public var blurMediaBehindContentWarning
@Preference<TrueKey> public var automaticallyPlayGifs
@Preference<TrueKey> public var showUncroppedMediaInline
@Preference<TrueKey> public var showAttachmentBadges
@Preference<FalseKey> public var attachmentAltBadgeInverted
// MARK: Behavior // MARK: Behavior
@Preference<TrueKey> public var openLinksInApps @Preference<TrueKey> public var openLinksInApps
@Preference<TrueKey> public var useInAppSafari @Preference<InAppSafariKey> public var useInAppSafari
@Preference<FalseKey> public var inAppSafariAutomaticReaderMode @Preference<FalseKey> public var inAppSafariAutomaticReaderMode
@Preference<FalseKey> public var expandAllContentWarnings @Preference<FalseKey> public var expandAllContentWarnings
@Preference<TrueKey> public var collapseLongPosts @Preference<TrueKey> public var collapseLongPosts

View File

@ -8,6 +8,5 @@
import Foundation import Foundation
public enum FeatureFlag: String, Codable { public enum FeatureFlag: String, Codable {
case iPadMultiColumn = "ipad-multi-column"
case iPadBrowserNavigation = "ipad-browser-navigation" case iPadBrowserNavigation = "ipad-browser-navigation"
} }

View File

@ -15,11 +15,11 @@ final class PreferenceStoreTests: XCTestCase {
static let defaultValue = false static let defaultValue = false
} }
final class TestStore: Codable, ObservableObject { final class TestStore<Key: PreferenceKey>: Codable, ObservableObject {
private var _test = Preference<TestKey>() private var _test = Preference<Key>()
// the acutal subscript expects the enclosingInstance to be a PreferenceStore, so do it manually // the acutal subscript expects the enclosingInstance to be a PreferenceStore, so do it manually
var test: Bool { var test: Key.Value {
get { get {
Preference.get(enclosingInstance: self, storage: \._test) Preference.get(enclosingInstance: self, storage: \._test)
} }
@ -28,7 +28,7 @@ final class PreferenceStoreTests: XCTestCase {
} }
} }
var testPublisher: some Publisher<TestKey.Value, Never> { var testPublisher: some Publisher<Key.Value, Never> {
_test.projectedValue _test.projectedValue
} }
@ -37,7 +37,7 @@ final class PreferenceStoreTests: XCTestCase {
init(from decoder: any Decoder) throws { init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self._test = try container.decode(Preference<TestKey>.self, forKey: .test) self._test = try container.decode(Preference<Key>.self, forKey: .test)
} }
enum CodingKeys: CodingKey { enum CodingKeys: CodingKey {
@ -52,18 +52,18 @@ final class PreferenceStoreTests: XCTestCase {
func testDecoding() throws { func testDecoding() throws {
let decoder = JSONDecoder() let decoder = JSONDecoder()
let present = try decoder.decode(PreferenceCoding<TestStore>.self, from: Data(""" let present = try decoder.decode(PreferenceCoding<TestStore<TestKey>>.self, from: Data("""
{"test": true} {"test": true}
""".utf8)).wrapped """.utf8)).wrapped
XCTAssertEqual(present.test, true) XCTAssertEqual(present.test, true)
let absent = try decoder.decode(PreferenceCoding<TestStore>.self, from: Data(""" let absent = try decoder.decode(PreferenceCoding<TestStore<TestKey>>.self, from: Data("""
{} {}
""".utf8)).wrapped """.utf8)).wrapped
XCTAssertEqual(absent.test, false) XCTAssertEqual(absent.test, false)
} }
func testEncoding() throws { func testEncoding() throws {
let store = TestStore() let store = TestStore<TestKey>()
let encoder = JSONEncoder() let encoder = JSONEncoder()
XCTAssertEqual(String(data: try encoder.encode(PreferenceCoding(wrapped: store)), encoding: .utf8)!, """ XCTAssertEqual(String(data: try encoder.encode(PreferenceCoding(wrapped: store)), encoding: .utf8)!, """
{} {}
@ -83,7 +83,7 @@ final class PreferenceStoreTests: XCTestCase {
let specificPref = expectation(description: "preference publisher") let specificPref = expectation(description: "preference publisher")
// initial and on change // initial and on change
specificPref.expectedFulfillmentCount = 2 specificPref.expectedFulfillmentCount = 2
let store = TestStore() let store = TestStore<TestKey>()
var cancellables = Set<AnyCancellable>() var cancellables = Set<AnyCancellable>()
store.objectWillChange.sink { store.objectWillChange.sink {
topLevel.fulfill() topLevel.fulfill()
@ -97,4 +97,32 @@ final class PreferenceStoreTests: XCTestCase {
wait(for: [topLevel, specificPref]) 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)
}
} }

View File

@ -169,7 +169,6 @@
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 */; };
@ -592,7 +591,6 @@
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>"; };
@ -1172,12 +1170,9 @@
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */, D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */,
04586B4022B2FFB10021BD04 /* PreferencesView.swift */, 04586B4022B2FFB10021BD04 /* PreferencesView.swift */,
D64B96802BC3279D002C8990 /* PrefsAccountView.swift */, D64B96802BC3279D002C8990 /* PrefsAccountView.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 */,
@ -1492,6 +1487,8 @@
children = ( children = (
D6C4532C2BCB86AC00E26A0E /* AppearancePrefsView.swift */, D6C4532C2BCB86AC00E26A0E /* AppearancePrefsView.swift */,
D6C4532E2BCB873400E26A0E /* MockStatusView.swift */, D6C4532E2BCB873400E26A0E /* MockStatusView.swift */,
D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */,
D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */,
); );
path = Appearance; path = Appearance;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2348,7 +2345,6 @@
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 */,
@ -2494,7 +2490,6 @@
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",
@ -2504,11 +2499,12 @@
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"; TARGETED_DEVICE_FAMILY = "1,2,7";
}; };
name = Debug; name = Debug;
}; };
@ -2526,7 +2522,6 @@
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",
@ -2536,10 +2531,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_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2,7";
}; };
name = Release; name = Release;
}; };
@ -2557,7 +2553,6 @@
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",
@ -2567,10 +2562,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_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2,7";
}; };
name = Dist; name = Dist;
}; };
@ -2622,7 +2618,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 = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -2630,6 +2626,7 @@
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;
}; };
@ -2645,7 +2642,6 @@
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",
@ -2712,8 +2708,6 @@
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",
@ -2741,7 +2735,6 @@
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",
@ -2770,7 +2763,6 @@
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",
@ -2799,7 +2791,6 @@
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",
@ -2870,7 +2861,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 = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
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;
@ -2878,6 +2869,7 @@
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;
}; };
@ -2929,7 +2921,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 = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -2937,6 +2929,7 @@
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;
}; };
@ -2952,7 +2945,6 @@
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",
@ -2984,7 +2976,6 @@
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",
@ -3092,8 +3083,6 @@
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",
@ -3118,8 +3107,6 @@
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",

View File

@ -26,7 +26,11 @@ 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)

View File

@ -175,7 +175,9 @@ 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

View File

@ -51,6 +51,7 @@ 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)

View File

@ -18,7 +18,9 @@ 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)
@ -44,8 +46,13 @@ 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 self.playbackSpeed { switch playbackSpeed {
case 0.5: case 0.5:
imageName = "gauge.with.dots.needle.0percent" imageName = "gauge.with.dots.needle.0percent"
case 1: case 1:
@ -61,8 +68,12 @@ 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: self.playbackSpeed == speed.rate ? .on : .off) { [unowned self] _ in UIAction(title: speed.displayName, state: 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
} }
@ -90,12 +101,20 @@ 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")
@ -170,7 +189,11 @@ 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
} }
} }

View File

@ -17,8 +17,10 @@ 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
@ -125,7 +127,11 @@ 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
} }
} }
} }
@ -142,12 +148,20 @@ 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)

View File

@ -15,7 +15,9 @@ 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!
@ -24,11 +26,18 @@ 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")
@ -97,7 +106,11 @@ 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
} }
} }

View File

@ -83,6 +83,23 @@ 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()
@ -100,17 +117,6 @@ 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 {

View File

@ -7,18 +7,49 @@
// //
import SwiftUI import SwiftUI
import Combine
import TuskerPreferences import TuskerPreferences
struct AppearancePrefsView: View { struct AppearancePrefsView: View {
@ObservedObject private var preferences = Preferences.shared @ObservedObject private var preferences = Preferences.shared
@Environment(\.colorScheme) private var colorScheme
private var appearanceChangePublisher: some Publisher<Void, Never> {
preferences.$theme
.map { _ in () }
.merge(with: preferences.$pureBlackDarkMode.map { _ in () },
preferences.$accentColor.map { _ in () }
)
// the prefrence publishers are all willSet, but want to notify after the change, so wait one runloop iteration
.receive(on: DispatchQueue.main)
}
private static let accentColorsAndImages: [(AccentColor, UIImage?)] = AccentColor.allCases.map { color in
var image: UIImage?
if let color = color.color {
if #available(iOS 16.0, *) {
image = UIImage(systemName: "circle.fill")!.withTintColor(color, renderingMode: .alwaysTemplate).withRenderingMode(.alwaysOriginal)
} else {
image = UIGraphicsImageRenderer(size: CGSize(width: 20, height: 20)).image { context in
color.setFill()
context.cgContext.fillEllipse(in: CGRect(x: 0, y: 0, width: 20, height: 20))
}
}
}
return (color, image)
}
var body: some View { var body: some View {
List { List {
Section { themeSection
interfaceSection
Section("Post Preview") {
MockStatusView() MockStatusView()
.padding(.top, 8) .padding(.top, 8)
.padding(.horizontal, UIDevice.current.userInterfaceIdiom == .pad ? 8 : 4)
} }
.appGroupedListRowBackground() .listRowBackground(mockStatusBackground)
accountsSection accountsSection
postsSection postsSection
@ -29,6 +60,69 @@ struct AppearancePrefsView: View {
.navigationTitle("Appearance") .navigationTitle("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 {
Section {
#if !os(visionOS)
Picker(selection: $preferences.theme, label: Text("Theme")) {
Text("Use System Theme").tag(Theme.unspecified)
Text("Light").tag(Theme.light)
Text("Dark").tag(Theme.dark)
}
// macOS system dark mode isn't pure black, so this isn't necessary
if !ProcessInfo.processInfo.isMacCatalystApp && !ProcessInfo.processInfo.isiOSAppOnMac {
Toggle(isOn: $preferences.pureBlackDarkMode) {
Text("Pure Black Dark Mode")
}
}
#endif
Picker(selection: $preferences.accentColor, label: Text("Accent Color")) {
ForEach(Self.accentColorsAndImages, id: \.0.rawValue) { (color, image) in
HStack {
Text(color.name)
if let image {
Spacer()
Image(uiImage: image)
}
}
.tag(color)
}
}
}
.onReceive(appearanceChangePublisher) { _ in
NotificationCenter.default.post(name: .themePreferenceChanged, object: nil)
}
.appGroupedListRowBackground()
}
@ViewBuilder
private var interfaceSection: some View {
let visionIdiom = UIUserInterfaceIdiom(rawValue: 6)
if [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom) {
Section(header: Text("Interface")) {
WidescreenNavigationPrefsView()
}
.appGroupedListRowBackground()
}
}
private var accountsSection: some View { private var accountsSection: some View {
Section("Accounts") { Section("Accounts") {
Toggle(isOn: Binding(get: { Toggle(isOn: Binding(get: {
@ -65,15 +159,16 @@ struct AppearancePrefsView: View {
Toggle(isOn: $preferences.underlineTextLinks) { Toggle(isOn: $preferences.underlineTextLinks) {
Text("Underline Links") Text("Underline Links")
} }
// NavigationLink("Leading Swipe Actions") { NavigationLink("Leading Swipe Actions") {
// SwipeActionsPrefsView(selection: $preferences.leadingStatusSwipeActions) SwipeActionsPrefsView(selection: $preferences.leadingStatusSwipeActions)
// .edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
// .navigationTitle("Leading Swipe Actions") .navigationTitle("Leading Swipe Actions")
// } }
// NavigationLink("Trailing Swipe Actions") { NavigationLink("Trailing Swipe Actions") {
// SwipeActionsPrefsView(selection: $preferences.trailingStatusSwipeActions) SwipeActionsPrefsView(selection: $preferences.trailingStatusSwipeActions)
// .edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
// .navigationTitle("Trailing Swipe Actions") .navigationTitle("Trailing Swipe Actions")
}
} }
.appGroupedListRowBackground() .appGroupedListRowBackground()
} }

View File

@ -161,7 +161,7 @@ private actor MockAttachmentsGenerator {
return attachmentURLs return attachmentURLs
} }
let size = CGSize(width: 100, height: 100) let size = CGSize(width: 200, height: 200)
let bounds = CGRect(origin: .zero, size: size) let bounds = CGRect(origin: .zero, size: size)
let format = UIGraphicsImageRendererFormat() let format = UIGraphicsImageRendererFormat()
format.scale = displayScale format.scale = displayScale
@ -171,24 +171,24 @@ private actor MockAttachmentsGenerator {
UIColor(red: 0x56 / 255, green: 0x03 / 255, blue: 0xad / 255, alpha: 1).setFill() UIColor(red: 0x56 / 255, green: 0x03 / 255, blue: 0xad / 255, alpha: 1).setFill()
ctx.fill(bounds) ctx.fill(bounds)
ctx.cgContext.concatenate(CGAffineTransform(1, 0, -0.5, 1, 0, 0)) ctx.cgContext.concatenate(CGAffineTransform(1, 0, -0.5, 1, 0, 0))
for minX in stride(from: 0, through: 100, by: 30) { for x in 0..<9 {
UIColor(red: 0x83 / 255, green: 0x67 / 255, blue: 0xc7 / 255, alpha: 1).setFill() UIColor(red: 0x83 / 255, green: 0x67 / 255, blue: 0xc7 / 255, alpha: 1).setFill()
ctx.fill(CGRect(x: minX + 20, y: 0, width: 15, height: 100)) ctx.fill(CGRect(x: CGFloat(x) * 30 + 20, y: 0, width: 15, height: bounds.height))
} }
} }
let secondImage = renderer.image { ctx in let secondImage = renderer.image { ctx in
UIColor(red: 0x00 / 255, green: 0x43 / 255, blue: 0x85 / 255, alpha: 1).setFill() UIColor(red: 0x00 / 255, green: 0x43 / 255, blue: 0x85 / 255, alpha: 1).setFill()
ctx.fill(bounds) ctx.fill(bounds)
UIColor(red: 0x05 / 255, green: 0xb2 / 255, blue: 0xdc / 255, alpha: 1).setFill() UIColor(red: 0x05 / 255, green: 0xb2 / 255, blue: 0xdc / 255, alpha: 1).setFill()
for y in 0..<2 { for y in 0..<4 {
for x in 0..<4 { for x in 0..<5 {
let rect = CGRect(x: x * 45 - 5, y: y * 50 + 15, width: 20, height: 20) let rect = CGRect(x: x * 45 - 5, y: y * 50 + 15, width: 20, height: 20)
ctx.cgContext.fillEllipse(in: rect) ctx.cgContext.fillEllipse(in: rect)
} }
} }
UIColor(red: 0x08 / 255, green: 0x7c / 255, blue: 0xa7 / 255, alpha: 1).setFill() UIColor(red: 0x08 / 255, green: 0x7c / 255, blue: 0xa7 / 255, alpha: 1).setFill()
for y in 0..<3 { for y in 0..<5 {
for x in 0..<2 { for x in 0..<4 {
let rect = CGRect(x: CGFloat(x) * 45 + 22.5, y: CGFloat(y) * 50 - 5, width: 10, height: 10) let rect = CGRect(x: CGFloat(x) * 45 + 22.5, y: CGFloat(y) * 50 - 5, width: 10, height: 10)
ctx.cgContext.fillEllipse(in: rect) ctx.cgContext.fillEllipse(in: rect)
} }

View File

@ -12,60 +12,72 @@ import TuskerPreferences
struct WidescreenNavigationPrefsView: View { struct WidescreenNavigationPrefsView: View {
@ObservedObject private var preferences = Preferences.shared @ObservedObject private var preferences = Preferences.shared
@State private var startAnimation = PassthroughSubject<Void, Never>() @State private var startAnimation = CurrentValueSubject<Bool, Never>(false)
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<StackNavigationPreview>( OptionView(
content: StackNavigationPreview.self,
value: .stack, value: .stack,
selection: $preferences.widescreenNavigationMode, selection: $preferences.widescreenNavigationMode,
startAnimation: startAnimation startAnimation: startAnimationSignal
) { ) {
Text("Stack") Text("Stack")
} }
Spacer(minLength: 32) Spacer(minLength: 32)
OptionView<SplitNavigationPreview>( OptionView(
content: SplitNavigationPreview.self,
value: .splitScreen, value: .splitScreen,
selection: $preferences.widescreenNavigationMode, selection: $preferences.widescreenNavigationMode,
startAnimation: startAnimation startAnimation: startAnimationSignal
) { ) {
Text("Split Screen") Text("Split Screen")
} }
if preferences.hasFeatureFlag(.iPadMultiColumn) {
Spacer(minLength: 32) Spacer(minLength: 32)
OptionView<MultiColumnNavigationPreview>( OptionView(
content: MultiColumnNavigationPreview.self,
value: .multiColumn, value: .multiColumn,
selection: $preferences.widescreenNavigationMode, selection: $preferences.widescreenNavigationMode,
startAnimation: startAnimation startAnimation: startAnimationSignal
) { ) {
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() startAnimation.send(true)
} }
} }
} }
} }
private struct OptionView<Content: NavigationModePreview>: View { private struct OptionView<Content: NavigationModePreview, P: Publisher<Void, Never>>: View {
let value: WidescreenNavigationMode let value: WidescreenNavigationMode
@Binding var selection: WidescreenNavigationMode @Binding var selection: WidescreenNavigationMode
let startAnimation: PassthroughSubject<Void, Never> let startAnimation: P
@ViewBuilder let label: Text 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
} }
@ -84,7 +96,7 @@ private struct OptionView<Content: NavigationModePreview>: View {
} }
private var preview: some View { private var preview: some View {
NavigationModeRepresentable<Content>(startAnimation: startAnimation) NavigationModeRepresentable(content: Content.self, 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)
@ -106,11 +118,15 @@ private struct WideCapsule: Shape {
@MainActor @MainActor
private protocol NavigationModePreview: UIView { private protocol NavigationModePreview: UIView {
init(startAnimation: PassthroughSubject<Void, Never>) init(startAnimation: some Publisher<Void, Never>)
} }
private struct NavigationModeRepresentable<UIViewType: NavigationModePreview>: UIViewRepresentable { private struct NavigationModeRepresentable<UIViewType: NavigationModePreview, P: Publisher<Void, Never>>: UIViewRepresentable {
let startAnimation: PassthroughSubject<Void, Never> let startAnimation: P
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)
@ -128,7 +144,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: PassthroughSubject<Void, Never>) { init(startAnimation: some Publisher<Void, Never>) {
super.init(frame: .zero) super.init(frame: .zero)
backgroundColor = .appBackground backgroundColor = .appBackground
@ -203,7 +219,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: PassthroughSubject<Void, Never>) { init(startAnimation: some Publisher<Void, Never>) {
super.init(frame: .zero) super.init(frame: .zero)
backgroundColor = .appBackground backgroundColor = .appBackground
@ -297,7 +313,7 @@ private final class MultiColumnNavigationPreview: UIView, NavigationModePreview
private var startedAnimation = false private var startedAnimation = false
init(startAnimation: PassthroughSubject<Void, Never>) { init(startAnimation: some Publisher<Void, Never>) {
super.init(frame: .zero) super.init(frame: .zero)
backgroundColor = .appSecondaryBackground backgroundColor = .appSecondaryBackground

View File

@ -58,13 +58,15 @@ struct BehaviorPrefsView: View {
Toggle(isOn: $preferences.openLinksInApps) { Toggle(isOn: $preferences.openLinksInApps) {
Text("Open Links in Apps") Text("Open Links in Apps")
} }
#if !os(visionOS) #if !targetEnvironment(macCatalyst) && !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()

View File

@ -1,64 +0,0 @@
//
// MediaPrefsView.swift
// Tusker
//
// Created by Shadowfacts on 2/22/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import SwiftUI
import TuskerPreferences
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(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()
}
}

View File

@ -23,7 +23,6 @@ struct PreferencesView: View {
var body: some View { var body: some View {
List { List {
accountsSection accountsSection
notificationsSection
preferencesSection preferencesSection
aboutSection aboutSection
} }
@ -92,36 +91,27 @@ struct PreferencesView: View {
.appGroupedListRowBackground() .appGroupedListRowBackground()
} }
private var notificationsSection: some View {
Section {
NavigationLink(isActive: $navigationState.showNotificationPreferences) {
NotificationsPrefsView()
} label: {
Text("Notifications")
}
}
.appGroupedListRowBackground()
}
private var preferencesSection: some View { private var preferencesSection: some View {
Section { Section {
NavigationLink(destination: AppearancePrefsView()) { NavigationLink(destination: AppearancePrefsView()) {
Text("Appearance") PreferenceSectionLabel(title: "Appearance", systemImageName: "textformat", backgroundColor: .indigo)
}
NavigationLink(destination: ComposingPrefsView()) {
Text("Composing")
}
NavigationLink(destination: MediaPrefsView()) {
Text("Media")
} }
NavigationLink(destination: BehaviorPrefsView()) { NavigationLink(destination: BehaviorPrefsView()) {
Text("Behavior") PreferenceSectionLabel(title: "Behavior", systemImageName: "flowchart.fill", backgroundColor: .green)
}
NavigationLink(isActive: $navigationState.showNotificationPreferences) {
NotificationsPrefsView()
} label: {
PreferenceSectionLabel(title: "Notifications", systemImageName: "bell.fill", backgroundColor: .red)
}
NavigationLink(destination: ComposingPrefsView()) {
PreferenceSectionLabel(title: "Composing", systemImageName: "pencil", backgroundColor: .blue)
} }
NavigationLink(destination: WellnessPrefsView()) { NavigationLink(destination: WellnessPrefsView()) {
Text("Digital Wellness") PreferenceSectionLabel(title: "Digital Wellness", systemImageName: "brain.fill", backgroundColor: .purple)
} }
NavigationLink(destination: AdvancedPrefsView()) { NavigationLink(destination: AdvancedPrefsView()) {
Text("Advanced") PreferenceSectionLabel(title: "Advanced", systemImageName: "gearshape.2.fill", backgroundColor: .gray)
} }
} }
.appGroupedListRowBackground() .appGroupedListRowBackground()
@ -129,14 +119,28 @@ struct PreferencesView: View {
private var aboutSection: some View { private var aboutSection: some View {
Section { Section {
NavigationLink("About") { NavigationLink {
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("Acknowledgements") { NavigationLink {
AcknowledgementsView() AcknowledgementsView()
} label: {
PreferenceSectionLabel(title: "Acknowledgements", systemImageName: "doc.text.fill", backgroundColor: .gray)
} }
} }
.appGroupedListRowBackground() .appGroupedListRowBackground()
@ -147,6 +151,24 @@ 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 {

View File

@ -47,10 +47,11 @@ 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 os(visionOS) #if targetEnvironment(macCatalyst) || os(visionOS)
UIApplication.shared.open(url) UIApplication.shared.open(url)
#else #else
if Preferences.shared.useInAppSafari, if !ProcessInfo.processInfo.isiOSAppOnMac,
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

View File

@ -29,7 +29,9 @@ 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)