Compare commits
No commits in common. "1f9806d02face6b734c36159e11660222cbcebfa" and "c7a56a9f6173ed284fe7849a34c0ecc8d90a256f" have entirely different histories.
1f9806d02f
...
c7a56a9f61
|
@ -301,7 +301,6 @@ extension MainActor {
|
|||
@available(iOS, obsoleted: 17.0)
|
||||
@available(watchOS, obsoleted: 10.0)
|
||||
@available(tvOS, obsoleted: 17.0)
|
||||
@available(visionOS 1.0, *)
|
||||
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T {
|
||||
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
|
||||
return try MainActor.assumeIsolated(body)
|
||||
|
|
|
@ -181,8 +181,13 @@ class ToolbarController: ViewController {
|
|||
private var formatButtons: some View {
|
||||
ForEach(StatusFormat.allCases, id: \.rawValue) { format in
|
||||
Button(action: controller.formatAction(format)) {
|
||||
Image(systemName: format.imageName)
|
||||
if let imageName = format.imageName {
|
||||
Image(systemName: imageName)
|
||||
.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)
|
||||
.padding(5)
|
||||
|
|
|
@ -23,7 +23,7 @@ enum StatusFormat: Int, CaseIterable {
|
|||
}
|
||||
}
|
||||
|
||||
var imageName: String {
|
||||
var imageName: String? {
|
||||
switch self {
|
||||
case .italics:
|
||||
return "italic"
|
||||
|
@ -31,8 +31,16 @@ enum StatusFormat: Int, CaseIterable {
|
|||
return "bold"
|
||||
case .strikethrough:
|
||||
return "strikethrough"
|
||||
case .code:
|
||||
return "chevron.left.forwardslash.chevron.right"
|
||||
default:
|
||||
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 {
|
||||
let formatMenu = suggestedActions[index] as! UIMenu
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -9,10 +9,8 @@ import SwiftUI
|
|||
|
||||
public struct AsyncPicker<V: Hashable, Content: View>: View {
|
||||
let titleKey: LocalizedStringKey
|
||||
#if !os(visionOS)
|
||||
@available(iOS, obsoleted: 16.0, message: "Switch to LabeledContent")
|
||||
let labelHidden: Bool
|
||||
#endif
|
||||
let alignment: Alignment
|
||||
@Binding var value: V
|
||||
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) {
|
||||
self.titleKey = titleKey
|
||||
#if !os(visionOS)
|
||||
self.labelHidden = labelHidden
|
||||
#endif
|
||||
self.alignment = alignment
|
||||
self._value = value
|
||||
self.onChange = onChange
|
||||
|
@ -31,11 +27,6 @@ public struct AsyncPicker<V: Hashable, Content: View>: View {
|
|||
}
|
||||
|
||||
public var body: some View {
|
||||
#if os(visionOS)
|
||||
LabeledContent(titleKey) {
|
||||
picker
|
||||
}
|
||||
#else
|
||||
if #available(iOS 16.0, *) {
|
||||
LabeledContent(titleKey) {
|
||||
picker
|
||||
|
@ -49,7 +40,6 @@ public struct AsyncPicker<V: Hashable, Content: View>: View {
|
|||
picker
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private var picker: some View {
|
||||
|
|
|
@ -10,28 +10,19 @@ import SwiftUI
|
|||
|
||||
public struct AsyncToggle: View {
|
||||
let titleKey: LocalizedStringKey
|
||||
#if !os(visionOS)
|
||||
@available(iOS, obsoleted: 16.0, message: "Switch to LabeledContent")
|
||||
let labelHidden: Bool
|
||||
#endif
|
||||
@Binding var mode: Mode
|
||||
let onChange: (Bool) async -> Bool
|
||||
|
||||
public init(_ titleKey: LocalizedStringKey, labelHidden: Bool = false, mode: Binding<Mode>, onChange: @escaping (Bool) async -> Bool) {
|
||||
self.titleKey = titleKey
|
||||
#if !os(visionOS)
|
||||
self.labelHidden = labelHidden
|
||||
#endif
|
||||
self._mode = mode
|
||||
self.onChange = onChange
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
#if os(visionOS)
|
||||
LabeledContent(titleKey) {
|
||||
toggleOrSpinner
|
||||
}
|
||||
#else
|
||||
if #available(iOS 16.0, *) {
|
||||
LabeledContent(titleKey) {
|
||||
toggleOrSpinner
|
||||
|
@ -45,7 +36,6 @@ public struct AsyncToggle: View {
|
|||
toggleOrSpinner
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
|
@ -8,21 +8,10 @@
|
|||
import Foundation
|
||||
import Pachyderm
|
||||
|
||||
struct StatusContentTypeKey: MigratablePreferenceKey {
|
||||
struct StatusContentTypeKey: PreferenceKey {
|
||||
static var defaultValue: StatusContentType { .plain }
|
||||
}
|
||||
|
||||
struct FeatureFlagsKey: MigratablePreferenceKey, CustomCodablePreferenceKey {
|
||||
struct FeatureFlagsKey: PreferenceKey {
|
||||
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:)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,42 +8,26 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct ThemeKey: MigratablePreferenceKey {
|
||||
struct ThemeKey: PreferenceKey {
|
||||
static var defaultValue: Theme { .unspecified }
|
||||
}
|
||||
|
||||
struct AccentColorKey: MigratablePreferenceKey {
|
||||
struct AccentColorKey: PreferenceKey {
|
||||
static var defaultValue: AccentColor { .default }
|
||||
}
|
||||
|
||||
struct AvatarStyleKey: MigratablePreferenceKey {
|
||||
struct AvatarStyleKey: PreferenceKey {
|
||||
static var defaultValue: AvatarStyle { .roundRect }
|
||||
}
|
||||
|
||||
struct LeadingSwipeActionsKey: MigratablePreferenceKey {
|
||||
struct LeadingSwipeActionsKey: PreferenceKey {
|
||||
static var defaultValue: [StatusSwipeAction] { [.favorite, .reblog] }
|
||||
}
|
||||
|
||||
struct TrailingSwipeActionsKey: MigratablePreferenceKey {
|
||||
struct TrailingSwipeActionsKey: PreferenceKey {
|
||||
static var defaultValue: [StatusSwipeAction] { [.reply, .share] }
|
||||
}
|
||||
|
||||
struct WidescreenNavigationModeKey: MigratablePreferenceKey {
|
||||
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
|
||||
}
|
||||
}
|
||||
struct WidescreenNavigationModeKey: PreferenceKey {
|
||||
static var defaultValue: WidescreenNavigationMode { .splitScreen }
|
||||
}
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
struct OppositeCollapseKeywordsKey: MigratablePreferenceKey {
|
||||
struct OppositeCollapseKeywordsKey: PreferenceKey {
|
||||
static var defaultValue: [String] { [] }
|
||||
}
|
||||
|
||||
struct ConfirmReblogKey: MigratablePreferenceKey {
|
||||
struct ConfirmReblogKey: PreferenceKey {
|
||||
static var defaultValue: Bool {
|
||||
#if os(visionOS)
|
||||
true
|
||||
|
@ -21,20 +21,6 @@ struct ConfirmReblogKey: MigratablePreferenceKey {
|
|||
}
|
||||
}
|
||||
|
||||
struct TimelineSyncModeKey: MigratablePreferenceKey {
|
||||
struct TimelineSyncModeKey: PreferenceKey {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
struct TrueKey: MigratablePreferenceKey {
|
||||
struct TrueKey: PreferenceKey {
|
||||
static var defaultValue: Bool { true }
|
||||
}
|
||||
|
||||
struct FalseKey: MigratablePreferenceKey {
|
||||
struct FalseKey: PreferenceKey {
|
||||
static var defaultValue: Bool { false }
|
||||
}
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
struct PostVisibilityKey: MigratablePreferenceKey {
|
||||
struct PostVisibilityKey: PreferenceKey {
|
||||
static var defaultValue: PostVisibility { .serverDefault }
|
||||
}
|
||||
|
||||
struct ReplyVisibilityKey: MigratablePreferenceKey {
|
||||
struct ReplyVisibilityKey: PreferenceKey {
|
||||
static var defaultValue: ReplyVisibility { .sameAsPost }
|
||||
}
|
||||
|
||||
struct ContentWarningCopyModeKey: MigratablePreferenceKey {
|
||||
struct ContentWarningCopyModeKey: PreferenceKey {
|
||||
static var defaultValue: ContentWarningCopyMode { .asIs }
|
||||
}
|
||||
|
|
|
@ -7,6 +7,6 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
struct NotificationsModeKey: MigratablePreferenceKey {
|
||||
struct NotificationsModeKey: PreferenceKey {
|
||||
static var defaultValue: NotificationsMode { .allNotifications }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// MediaKeys.swift
|
||||
// TuskerPreferences
|
||||
//
|
||||
// Created by Shadowfacts on 4/13/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct AttachmentBlurModeKey: PreferenceKey {
|
||||
static var defaultValue: AttachmentBlurMode { .useStatusSetting }
|
||||
|
||||
static func didSet(in store: PreferenceStore, newValue: AttachmentBlurMode) {
|
||||
if newValue == .always {
|
||||
store.blurMediaBehindContentWarning = true
|
||||
} else if newValue == .never {
|
||||
store.blurMediaBehindContentWarning = false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ import UIKit
|
|||
|
||||
extension PreferenceStore {
|
||||
func migrate(from legacy: LegacyPreferences) {
|
||||
var migrations: [any MigrationProtocol] = [
|
||||
let migrations: [any MigrationProtocol] = [
|
||||
Migration(from: \.theme.theme, to: \.$theme),
|
||||
Migration(from: \.pureBlackDarkMode, to: \.$pureBlackDarkMode),
|
||||
Migration(from: \.accentColor, to: \.$accentColor),
|
||||
|
@ -41,6 +41,8 @@ extension PreferenceStore {
|
|||
Migration(from: \.attachmentAltBadgeInverted, to: \.$attachmentAltBadgeInverted),
|
||||
|
||||
Migration(from: \.openLinksInApps, to: \.$openLinksInApps),
|
||||
Migration(from: \.useInAppSafari, to: \.$useInAppSafari),
|
||||
Migration(from: \.inAppSafariAutomaticReaderMode, to: \.$inAppSafariAutomaticReaderMode),
|
||||
Migration(from: \.expandAllContentWarnings, to: \.$expandAllContentWarnings),
|
||||
Migration(from: \.collapseLongPosts, to: \.$collapseLongPosts),
|
||||
Migration(from: \.oppositeCollapseKeywords, to: \.$oppositeCollapseKeywords),
|
||||
|
@ -63,12 +65,6 @@ extension PreferenceStore {
|
|||
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)
|
||||
|
@ -80,13 +76,13 @@ private protocol MigrationProtocol {
|
|||
func migrate(from legacy: LegacyPreferences, to store: PreferenceStore)
|
||||
}
|
||||
|
||||
private struct Migration<Key: MigratablePreferenceKey>: MigrationProtocol where Key.Value: Equatable {
|
||||
private struct Migration<Key: PreferenceKey>: MigrationProtocol where Key.Value: Equatable {
|
||||
let from: KeyPath<LegacyPreferences, Key.Value>
|
||||
let to: KeyPath<PreferenceStore, PreferencePublisher<Key>>
|
||||
|
||||
func migrate(from legacy: LegacyPreferences, to store: PreferenceStore) {
|
||||
let value = legacy[keyPath: from]
|
||||
if Key.shouldMigrate(oldValue: value) {
|
||||
if value != Key.defaultValue {
|
||||
Preference.set(enclosingInstance: store, storage: to.appending(path: \.preference), newValue: value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,26 +27,17 @@ final class Preference<Key: PreferenceKey>: Codable {
|
|||
}
|
||||
|
||||
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() {
|
||||
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,
|
||||
|
|
|
@ -18,18 +18,3 @@ public protocol PreferenceKey {
|
|||
extension PreferenceKey {
|
||||
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?
|
||||
}
|
||||
|
|
|
@ -25,12 +25,6 @@ public final class PreferenceStore: ObservableObject, Codable {
|
|||
@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
|
||||
|
@ -40,9 +34,17 @@ public final class PreferenceStore: ObservableObject, Codable {
|
|||
@Preference<FalseKey> public var mentionReblogger
|
||||
@Preference<FalseKey> public var useTwitterKeyboard
|
||||
|
||||
// MARK: Media
|
||||
@Preference<AttachmentBlurModeKey> public var attachmentBlurMode
|
||||
@Preference<TrueKey> public var blurMediaBehindContentWarning
|
||||
@Preference<TrueKey> public var automaticallyPlayGifs
|
||||
@Preference<TrueKey> public var showUncroppedMediaInline
|
||||
@Preference<TrueKey> public var showAttachmentBadges
|
||||
@Preference<FalseKey> public var attachmentAltBadgeInverted
|
||||
|
||||
// MARK: Behavior
|
||||
@Preference<TrueKey> public var openLinksInApps
|
||||
@Preference<InAppSafariKey> public var useInAppSafari
|
||||
@Preference<TrueKey> public var useInAppSafari
|
||||
@Preference<FalseKey> public var inAppSafariAutomaticReaderMode
|
||||
@Preference<FalseKey> public var expandAllContentWarnings
|
||||
@Preference<TrueKey> public var collapseLongPosts
|
||||
|
|
|
@ -8,5 +8,6 @@
|
|||
import Foundation
|
||||
|
||||
public enum FeatureFlag: String, Codable {
|
||||
case iPadMultiColumn = "ipad-multi-column"
|
||||
case iPadBrowserNavigation = "ipad-browser-navigation"
|
||||
}
|
||||
|
|
|
@ -15,11 +15,11 @@ final class PreferenceStoreTests: XCTestCase {
|
|||
static let defaultValue = false
|
||||
}
|
||||
|
||||
final class TestStore<Key: PreferenceKey>: Codable, ObservableObject {
|
||||
private var _test = Preference<Key>()
|
||||
final class TestStore: Codable, ObservableObject {
|
||||
private var _test = Preference<TestKey>()
|
||||
|
||||
// the acutal subscript expects the enclosingInstance to be a PreferenceStore, so do it manually
|
||||
var test: Key.Value {
|
||||
var test: Bool {
|
||||
get {
|
||||
Preference.get(enclosingInstance: self, storage: \._test)
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ final class PreferenceStoreTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
var testPublisher: some Publisher<Key.Value, Never> {
|
||||
var testPublisher: some Publisher<TestKey.Value, Never> {
|
||||
_test.projectedValue
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ final class PreferenceStoreTests: XCTestCase {
|
|||
|
||||
init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self._test = try container.decode(Preference<Key>.self, forKey: .test)
|
||||
self._test = try container.decode(Preference<TestKey>.self, forKey: .test)
|
||||
}
|
||||
|
||||
enum CodingKeys: CodingKey {
|
||||
|
@ -52,18 +52,18 @@ final class PreferenceStoreTests: XCTestCase {
|
|||
|
||||
func testDecoding() throws {
|
||||
let decoder = JSONDecoder()
|
||||
let present = try decoder.decode(PreferenceCoding<TestStore<TestKey>>.self, from: Data("""
|
||||
let present = try decoder.decode(PreferenceCoding<TestStore>.self, from: Data("""
|
||||
{"test": true}
|
||||
""".utf8)).wrapped
|
||||
XCTAssertEqual(present.test, true)
|
||||
let absent = try decoder.decode(PreferenceCoding<TestStore<TestKey>>.self, from: Data("""
|
||||
let absent = try decoder.decode(PreferenceCoding<TestStore>.self, from: Data("""
|
||||
{}
|
||||
""".utf8)).wrapped
|
||||
XCTAssertEqual(absent.test, false)
|
||||
}
|
||||
|
||||
func testEncoding() throws {
|
||||
let store = TestStore<TestKey>()
|
||||
let store = TestStore()
|
||||
let encoder = JSONEncoder()
|
||||
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")
|
||||
// initial and on change
|
||||
specificPref.expectedFulfillmentCount = 2
|
||||
let store = TestStore<TestKey>()
|
||||
let store = TestStore()
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
store.objectWillChange.sink {
|
||||
topLevel.fulfill()
|
||||
|
@ -97,32 +97,4 @@ final class PreferenceStoreTests: XCTestCase {
|
|||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -169,6 +169,7 @@
|
|||
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.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 */; };
|
||||
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; };
|
||||
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; };
|
||||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
|
||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
|
||||
|
@ -591,6 +592,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1170,9 +1172,12 @@
|
|||
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */,
|
||||
04586B4022B2FFB10021BD04 /* PreferencesView.swift */,
|
||||
D64B96802BC3279D002C8990 /* PrefsAccountView.swift */,
|
||||
D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */,
|
||||
D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */,
|
||||
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */,
|
||||
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */,
|
||||
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */,
|
||||
D68015412401A74600D6103B /* MediaPrefsView.swift */,
|
||||
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */,
|
||||
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */,
|
||||
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */,
|
||||
|
@ -1487,8 +1492,6 @@
|
|||
children = (
|
||||
D6C4532C2BCB86AC00E26A0E /* AppearancePrefsView.swift */,
|
||||
D6C4532E2BCB873400E26A0E /* MockStatusView.swift */,
|
||||
D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */,
|
||||
D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */,
|
||||
);
|
||||
path = Appearance;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2345,6 +2348,7 @@
|
|||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */,
|
||||
D6CF5B892AC9BA6E00F15D83 /* MastodonSearchController.swift in Sources */,
|
||||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */,
|
||||
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */,
|
||||
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
|
||||
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */,
|
||||
D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */,
|
||||
|
@ -2490,6 +2494,7 @@
|
|||
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -2499,12 +2504,11 @@
|
|||
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.NotificationExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2,7";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
@ -2522,6 +2526,7 @@
|
|||
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -2531,11 +2536,10 @@
|
|||
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.NotificationExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2,7";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
@ -2553,6 +2557,7 @@
|
|||
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -2562,11 +2567,10 @@
|
|||
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.NotificationExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2,7";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Dist;
|
||||
};
|
||||
|
@ -2618,7 +2622,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -2626,7 +2630,6 @@
|
|||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
XROS_DEPLOYMENT_TARGET = 1.1;
|
||||
};
|
||||
name = Dist;
|
||||
};
|
||||
|
@ -2642,6 +2645,7 @@
|
|||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -2708,6 +2712,8 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -2735,6 +2741,7 @@
|
|||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -2763,6 +2770,7 @@
|
|||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -2791,6 +2799,7 @@
|
|||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -2861,7 +2870,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
|
@ -2869,7 +2878,6 @@
|
|||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||
XROS_DEPLOYMENT_TARGET = 1.1;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
@ -2921,7 +2929,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -2929,7 +2937,6 @@
|
|||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
XROS_DEPLOYMENT_TARGET = 1.1;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
@ -2945,6 +2952,7 @@
|
|||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -2976,6 +2984,7 @@
|
|||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -3083,6 +3092,8 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -3107,6 +3118,8 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
|
|
@ -26,11 +26,7 @@ class SaveToPhotosActivity: UIActivity {
|
|||
// 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 format = UIGraphicsImageRendererFormat()
|
||||
#if os(visionOS)
|
||||
format.scale = 2
|
||||
#else
|
||||
format.scale = UIScreen.main.scale
|
||||
#endif
|
||||
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))
|
||||
symbol.draw(in: rect)
|
||||
|
|
|
@ -175,9 +175,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
private func initializePushNotifications() {
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
Task {
|
||||
#if canImport(Sentry)
|
||||
PushManager.captureError = { SentrySDK.capture(error: $0) }
|
||||
#endif
|
||||
await PushManager.shared.updateIfNecessary(updateSubscription: {
|
||||
guard let account = UserAccountsManager.shared.getAccount(id: $0.accountID) else {
|
||||
return false
|
||||
|
|
|
@ -51,7 +51,6 @@ public extension MainActor {
|
|||
@available(iOS, obsoleted: 17.0)
|
||||
@available(watchOS, obsoleted: 10.0)
|
||||
@available(tvOS, obsoleted: 17.0)
|
||||
@available(visionOS 1.0, *)
|
||||
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T {
|
||||
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
|
||||
return try MainActor.assumeIsolated(body)
|
||||
|
|
|
@ -18,9 +18,7 @@ class VideoControlsViewController: UIViewController {
|
|||
}()
|
||||
|
||||
private let player: AVPlayer
|
||||
#if !os(visionOS)
|
||||
@Box private var playbackSpeed: Float
|
||||
#endif
|
||||
|
||||
private lazy var muteButton = MuteButton().configure {
|
||||
$0.addTarget(self, action: #selector(muteButtonPressed), for: .touchUpInside)
|
||||
|
@ -46,13 +44,8 @@ class VideoControlsViewController: UIViewController {
|
|||
|
||||
private lazy var optionsButton = MenuButton { [unowned self] in
|
||||
let imageName: String
|
||||
#if os(visionOS)
|
||||
let playbackSpeed = player.defaultRate
|
||||
#else
|
||||
let playbackSpeed = self.playbackSpeed
|
||||
#endif
|
||||
if #available(iOS 17.0, *) {
|
||||
switch playbackSpeed {
|
||||
switch self.playbackSpeed {
|
||||
case 0.5:
|
||||
imageName = "gauge.with.dots.needle.0percent"
|
||||
case 1:
|
||||
|
@ -68,12 +61,8 @@ class VideoControlsViewController: UIViewController {
|
|||
imageName = "speedometer"
|
||||
}
|
||||
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
|
||||
#if os(visionOS)
|
||||
self.player.defaultRate = speed.rate
|
||||
#else
|
||||
UIAction(title: speed.displayName, state: self.playbackSpeed == speed.rate ? .on : .off) { [unowned self] _ in
|
||||
self.playbackSpeed = speed.rate
|
||||
#endif
|
||||
if self.player.rate > 0 {
|
||||
self.player.rate = speed.rate
|
||||
}
|
||||
|
@ -101,20 +90,12 @@ class VideoControlsViewController: UIViewController {
|
|||
private var scrubbingTargetTime: CMTime?
|
||||
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>) {
|
||||
self.player = player
|
||||
self._playbackSpeed = playbackSpeed
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
#endif
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
|
@ -189,11 +170,7 @@ class VideoControlsViewController: UIViewController {
|
|||
@objc private func scrubbingEnded() {
|
||||
scrubbingChanged()
|
||||
if wasPlayingWhenScrubbingStarted {
|
||||
#if os(visionOS)
|
||||
player.play()
|
||||
#else
|
||||
player.rate = playbackSpeed
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,10 +17,8 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
|||
private var item: AVPlayerItem
|
||||
let player: AVPlayer
|
||||
|
||||
#if !os(visionOS)
|
||||
@available(iOS, obsoleted: 16.0, message: "Use AVPlayer.defaultRate")
|
||||
@Box private var playbackSpeed: Float = 1
|
||||
#endif
|
||||
|
||||
private var isGrayscale: Bool
|
||||
|
||||
|
@ -127,11 +125,7 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
|||
player.replaceCurrentItem(with: item)
|
||||
updateItemObservations()
|
||||
if isPlaying {
|
||||
#if os(visionOS)
|
||||
player.play()
|
||||
#else
|
||||
player.rate = playbackSpeed
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -148,20 +142,12 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
|||
[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)
|
||||
#endif
|
||||
var contentOverlayAccessoryViewController: UIViewController? {
|
||||
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)
|
||||
#endif
|
||||
|
||||
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
||||
overlayVC.setVisible(visible)
|
||||
|
|
|
@ -15,9 +15,7 @@ class VideoOverlayViewController: UIViewController {
|
|||
private static let pauseImage = UIImage(systemName: "pause.fill")!
|
||||
|
||||
private let player: AVPlayer
|
||||
#if !os(visionOS)
|
||||
@Box private var playbackSpeed: Float
|
||||
#endif
|
||||
|
||||
private var dimmingView: UIView!
|
||||
private var controlsStack: UIStackView!
|
||||
|
@ -26,18 +24,11 @@ class VideoOverlayViewController: UIViewController {
|
|||
|
||||
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>) {
|
||||
self.player = player
|
||||
self._playbackSpeed = playbackSpeed
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
#endif
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
|
@ -106,11 +97,7 @@ class VideoOverlayViewController: UIViewController {
|
|||
if player.rate > 0 {
|
||||
player.rate = 0
|
||||
} else {
|
||||
#if os(visionOS)
|
||||
player.play()
|
||||
#else
|
||||
player.rate = playbackSpeed
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -83,23 +83,6 @@ struct AdvancedPrefsView : View {
|
|||
HStack {
|
||||
Text("iCloud Status")
|
||||
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 {
|
||||
case nil:
|
||||
EmptyView()
|
||||
|
@ -117,6 +100,17 @@ struct AdvancedPrefsView : View {
|
|||
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 {
|
||||
Section {
|
||||
|
|
|
@ -7,49 +7,18 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import TuskerPreferences
|
||||
|
||||
struct AppearancePrefsView: View {
|
||||
@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 {
|
||||
List {
|
||||
themeSection
|
||||
interfaceSection
|
||||
|
||||
Section("Post Preview") {
|
||||
Section {
|
||||
MockStatusView()
|
||||
.padding(.top, 8)
|
||||
.padding(.horizontal, UIDevice.current.userInterfaceIdiom == .pad ? 8 : 4)
|
||||
}
|
||||
.listRowBackground(mockStatusBackground)
|
||||
.appGroupedListRowBackground()
|
||||
|
||||
accountsSection
|
||||
postsSection
|
||||
|
@ -60,69 +29,6 @@ struct AppearancePrefsView: View {
|
|||
.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 {
|
||||
Section("Accounts") {
|
||||
Toggle(isOn: Binding(get: {
|
||||
|
@ -159,16 +65,15 @@ struct AppearancePrefsView: View {
|
|||
Toggle(isOn: $preferences.underlineTextLinks) {
|
||||
Text("Underline Links")
|
||||
}
|
||||
NavigationLink("Leading Swipe Actions") {
|
||||
SwipeActionsPrefsView(selection: $preferences.leadingStatusSwipeActions)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.navigationTitle("Leading Swipe Actions")
|
||||
}
|
||||
NavigationLink("Trailing Swipe Actions") {
|
||||
SwipeActionsPrefsView(selection: $preferences.trailingStatusSwipeActions)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.navigationTitle("Trailing Swipe Actions")
|
||||
}
|
||||
// NavigationLink("Leading Swipe Actions") {
|
||||
// SwipeActionsPrefsView(selection: $preferences.leadingStatusSwipeActions)
|
||||
// .edgesIgnoringSafeArea(.all)
|
||||
// .navigationTitle("Leading Swipe Actions")
|
||||
// }
|
||||
// NavigationLink("Trailing Swipe Actions") {
|
||||
// SwipeActionsPrefsView(selection: $preferences.trailingStatusSwipeActions)
|
||||
// .edgesIgnoringSafeArea(.all)
|
||||
// .navigationTitle("Trailing Swipe Actions")
|
||||
}
|
||||
.appGroupedListRowBackground()
|
||||
}
|
||||
|
|
|
@ -161,7 +161,7 @@ private actor MockAttachmentsGenerator {
|
|||
return attachmentURLs
|
||||
}
|
||||
|
||||
let size = CGSize(width: 200, height: 200)
|
||||
let size = CGSize(width: 100, height: 100)
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
format.scale = displayScale
|
||||
|
@ -171,24 +171,24 @@ private actor MockAttachmentsGenerator {
|
|||
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 {
|
||||
for minX in stride(from: 0, through: 100, by: 30) {
|
||||
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))
|
||||
ctx.fill(CGRect(x: minX + 20, y: 0, width: 15, height: 100))
|
||||
}
|
||||
}
|
||||
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 {
|
||||
for y in 0..<2 {
|
||||
for x in 0..<4 {
|
||||
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 {
|
||||
for y in 0..<3 {
|
||||
for x in 0..<2 {
|
||||
let rect = CGRect(x: CGFloat(x) * 45 + 22.5, y: CGFloat(y) * 50 - 5, width: 10, height: 10)
|
||||
ctx.cgContext.fillEllipse(in: rect)
|
||||
}
|
||||
|
|
|
@ -58,15 +58,13 @@ struct BehaviorPrefsView: View {
|
|||
Toggle(isOn: $preferences.openLinksInApps) {
|
||||
Text("Open Links in Apps")
|
||||
}
|
||||
#if !targetEnvironment(macCatalyst) && !os(visionOS)
|
||||
if !ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
#if !os(visionOS)
|
||||
Toggle(isOn: $preferences.useInAppSafari) {
|
||||
Text("Use In-App Safari")
|
||||
}
|
||||
Toggle(isOn: $preferences.inAppSafariAutomaticReaderMode) {
|
||||
Text("Always Use Reader Mode in In-App Safari")
|
||||
}.disabled(!preferences.useInAppSafari)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.appGroupedListRowBackground()
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ struct PreferencesView: View {
|
|||
var body: some View {
|
||||
List {
|
||||
accountsSection
|
||||
notificationsSection
|
||||
preferencesSection
|
||||
aboutSection
|
||||
}
|
||||
|
@ -91,27 +92,36 @@ struct PreferencesView: View {
|
|||
.appGroupedListRowBackground()
|
||||
}
|
||||
|
||||
private var preferencesSection: some View {
|
||||
private var notificationsSection: some View {
|
||||
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) {
|
||||
NotificationsPrefsView()
|
||||
} 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()) {
|
||||
PreferenceSectionLabel(title: "Composing", systemImageName: "pencil", backgroundColor: .blue)
|
||||
Text("Composing")
|
||||
}
|
||||
NavigationLink(destination: MediaPrefsView()) {
|
||||
Text("Media")
|
||||
}
|
||||
NavigationLink(destination: BehaviorPrefsView()) {
|
||||
Text("Behavior")
|
||||
}
|
||||
NavigationLink(destination: WellnessPrefsView()) {
|
||||
PreferenceSectionLabel(title: "Digital Wellness", systemImageName: "brain.fill", backgroundColor: .purple)
|
||||
Text("Digital Wellness")
|
||||
}
|
||||
NavigationLink(destination: AdvancedPrefsView()) {
|
||||
PreferenceSectionLabel(title: "Advanced", systemImageName: "gearshape.2.fill", backgroundColor: .gray)
|
||||
Text("Advanced")
|
||||
}
|
||||
}
|
||||
.appGroupedListRowBackground()
|
||||
|
@ -119,28 +129,14 @@ struct PreferencesView: View {
|
|||
|
||||
private var aboutSection: some View {
|
||||
Section {
|
||||
NavigationLink {
|
||||
NavigationLink("About") {
|
||||
AboutView()
|
||||
} label: {
|
||||
Label {
|
||||
Text("About")
|
||||
} icon: {
|
||||
Image("AboutIcon")
|
||||
.resizable()
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.frame(width: 30, height: 30)
|
||||
}
|
||||
}
|
||||
NavigationLink {
|
||||
NavigationLink("Tip Jar") {
|
||||
TipJarView()
|
||||
} label: {
|
||||
// TODO: custom tip jar icon?
|
||||
PreferenceSectionLabel(title: "Tip Jar", systemImageName: "dollarsign.square.fill", backgroundColor: .yellow)
|
||||
}
|
||||
NavigationLink {
|
||||
NavigationLink("Acknowledgements") {
|
||||
AcknowledgementsView()
|
||||
} label: {
|
||||
PreferenceSectionLabel(title: "Acknowledgements", systemImageName: "doc.text.fill", backgroundColor: .gray)
|
||||
}
|
||||
}
|
||||
.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
|
||||
//struct PreferencesView_Previews : PreviewProvider {
|
||||
// static var previews: some View {
|
||||
|
|
|
@ -12,72 +12,60 @@ import TuskerPreferences
|
|||
|
||||
struct WidescreenNavigationPrefsView: View {
|
||||
@ObservedObject private var preferences = Preferences.shared
|
||||
@State private var startAnimation = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
private var startAnimationSignal: some Publisher<Void, Never> {
|
||||
startAnimation.filter { $0 }.removeDuplicates().map { _ in () }
|
||||
}
|
||||
@State private var startAnimation = PassthroughSubject<Void, Never>()
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
OptionView(
|
||||
content: StackNavigationPreview.self,
|
||||
OptionView<StackNavigationPreview>(
|
||||
value: .stack,
|
||||
selection: $preferences.widescreenNavigationMode,
|
||||
startAnimation: startAnimationSignal
|
||||
startAnimation: startAnimation
|
||||
) {
|
||||
Text("Stack")
|
||||
}
|
||||
|
||||
Spacer(minLength: 32)
|
||||
|
||||
OptionView(
|
||||
content: SplitNavigationPreview.self,
|
||||
OptionView<SplitNavigationPreview>(
|
||||
value: .splitScreen,
|
||||
selection: $preferences.widescreenNavigationMode,
|
||||
startAnimation: startAnimationSignal
|
||||
startAnimation: startAnimation
|
||||
) {
|
||||
Text("Split Screen")
|
||||
}
|
||||
|
||||
if preferences.hasFeatureFlag(.iPadMultiColumn) {
|
||||
Spacer(minLength: 32)
|
||||
|
||||
OptionView(
|
||||
content: MultiColumnNavigationPreview.self,
|
||||
OptionView<MultiColumnNavigationPreview>(
|
||||
value: .multiColumn,
|
||||
selection: $preferences.widescreenNavigationMode,
|
||||
startAnimation: startAnimationSignal
|
||||
startAnimation: startAnimation
|
||||
) {
|
||||
Text("Multi-Column")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(height: 100)
|
||||
.onAppear {
|
||||
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
|
||||
@Binding var selection: WidescreenNavigationMode
|
||||
let startAnimation: P
|
||||
let label: Text
|
||||
let startAnimation: PassthroughSubject<Void, Never>
|
||||
@ViewBuilder let label: Text
|
||||
@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 {
|
||||
selection == value
|
||||
}
|
||||
|
@ -96,7 +84,7 @@ private struct OptionView<Content: NavigationModePreview, P: Publisher<Void, Nev
|
|||
}
|
||||
|
||||
private var preview: some View {
|
||||
NavigationModeRepresentable(content: Content.self, startAnimation: startAnimation)
|
||||
NavigationModeRepresentable<Content>(startAnimation: startAnimation)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12.5, style: .continuous))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 12.5, style: .continuous)
|
||||
|
@ -118,15 +106,11 @@ private struct WideCapsule: Shape {
|
|||
|
||||
@MainActor
|
||||
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 {
|
||||
let startAnimation: P
|
||||
|
||||
init(content _: UIViewType.Type, startAnimation: P) {
|
||||
self.startAnimation = startAnimation
|
||||
}
|
||||
private struct NavigationModeRepresentable<UIViewType: NavigationModePreview>: UIViewRepresentable {
|
||||
let startAnimation: PassthroughSubject<Void, Never>
|
||||
|
||||
func makeUIView(context: Context) -> UIViewType {
|
||||
UIViewType(startAnimation: startAnimation)
|
||||
|
@ -144,7 +128,7 @@ private final class StackNavigationPreview: UIView, NavigationModePreview {
|
|||
private let destinationView = UIView()
|
||||
private var cancellable: AnyCancellable?
|
||||
|
||||
init(startAnimation: some Publisher<Void, Never>) {
|
||||
init(startAnimation: PassthroughSubject<Void, Never>) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
backgroundColor = .appBackground
|
||||
|
@ -219,7 +203,7 @@ private final class SplitNavigationPreview: UIView, NavigationModePreview {
|
|||
private var cellStackTrailingConstraint: NSLayoutConstraint!
|
||||
private var cancellable: AnyCancellable?
|
||||
|
||||
init(startAnimation: some Publisher<Void, Never>) {
|
||||
init(startAnimation: PassthroughSubject<Void, Never>) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
backgroundColor = .appBackground
|
||||
|
@ -313,7 +297,7 @@ private final class MultiColumnNavigationPreview: UIView, NavigationModePreview
|
|||
|
||||
private var startedAnimation = false
|
||||
|
||||
init(startAnimation: some Publisher<Void, Never>) {
|
||||
init(startAnimation: PassthroughSubject<Void, Never>) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
backgroundColor = .appSecondaryBackground
|
|
@ -47,11 +47,10 @@ extension TuskerNavigationDelegate {
|
|||
|
||||
func selected(url: URL, allowResolveStatuses: Bool = true, allowUniversalLinks: Bool = true) {
|
||||
func openSafari() {
|
||||
#if targetEnvironment(macCatalyst) || os(visionOS)
|
||||
#if os(visionOS)
|
||||
UIApplication.shared.open(url)
|
||||
#else
|
||||
if !ProcessInfo.processInfo.isiOSAppOnMac,
|
||||
Preferences.shared.useInAppSafari,
|
||||
if Preferences.shared.useInAppSafari,
|
||||
url.scheme == "https" || url.scheme == "http" {
|
||||
let config = SFSafariViewController.Configuration()
|
||||
config.entersReaderIfAvailable = Preferences.shared.inAppSafariAutomaticReaderMode
|
||||
|
|
|
@ -29,9 +29,7 @@ class GifvController {
|
|||
self.isGrayscale = Preferences.shared.grayscaleImages
|
||||
|
||||
player.isMuted = true
|
||||
#if !os(visionOS)
|
||||
player.preventsDisplaySleepDuringVideoPlayback = false
|
||||
#endif
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(restartItem), name: .AVPlayerItemDidPlayToEndTime, object: item)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||
|
|
Loading…
Reference in New Issue