Compare commits
13 Commits
c7a56a9f61
...
1f9806d02f
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 1f9806d02f | |
Shadowfacts | c43c951b92 | |
Shadowfacts | 00c44c612f | |
Shadowfacts | e5c4fceacd | |
Shadowfacts | 70227a7fa1 | |
Shadowfacts | cb5488dcaa | |
Shadowfacts | 910e18fb5e | |
Shadowfacts | 66af946766 | |
Shadowfacts | 6784ed7fdf | |
Shadowfacts | 66f0ba6891 | |
Shadowfacts | ee7bf5138c | |
Shadowfacts | c32181818a | |
Shadowfacts | 4665df228d |
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,6 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct NotificationsModeKey: PreferenceKey {
|
struct NotificationsModeKey: MigratablePreferenceKey {
|
||||||
static var defaultValue: NotificationsMode { .allNotifications }
|
static var defaultValue: NotificationsMode { .allNotifications }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,7 +63,13 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,15 +27,24 @@ 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 {
|
||||||
var container = encoder.singleValueContainer()
|
if let keyType = Key.self as? any CustomCodablePreferenceKey.Type {
|
||||||
try container.encode(storedValue)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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?
|
||||||
|
}
|
||||||
|
|
|
@ -25,7 +25,13 @@ 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
|
||||||
@Preference<ReplyVisibilityKey> public var defaultReplyVisibility
|
@Preference<ReplyVisibilityKey> public var defaultReplyVisibility
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
@ -96,5 +96,33 @@ final class PreferenceStoreTests: XCTestCase {
|
||||||
store.test = true
|
store.test = true
|
||||||
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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,12 +26,19 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -83,22 +83,8 @@ struct AdvancedPrefsView : View {
|
||||||
HStack {
|
HStack {
|
||||||
Text("iCloud Status")
|
Text("iCloud Status")
|
||||||
Spacer()
|
Spacer()
|
||||||
switch cloudKitStatus {
|
cloudKitStatusLabel
|
||||||
case nil:
|
.foregroundStyle(.secondary)
|
||||||
EmptyView()
|
|
||||||
case .available:
|
|
||||||
Text("Available")
|
|
||||||
case .couldNotDetermine:
|
|
||||||
Text("Could not determine")
|
|
||||||
case .noAccount:
|
|
||||||
Text("No account")
|
|
||||||
case .restricted:
|
|
||||||
Text("Restricted")
|
|
||||||
case .temporarilyUnavailable:
|
|
||||||
Text("Temporarily Unavailable")
|
|
||||||
@unknown default:
|
|
||||||
Text(String(describing: cloudKitStatus!))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.appGroupedListRowBackground()
|
.appGroupedListRowBackground()
|
||||||
|
@ -112,6 +98,26 @@ struct AdvancedPrefsView : View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var cloudKitStatusLabel: some View {
|
||||||
|
switch cloudKitStatus {
|
||||||
|
case nil:
|
||||||
|
EmptyView()
|
||||||
|
case .available:
|
||||||
|
Text("Available")
|
||||||
|
case .couldNotDetermine:
|
||||||
|
Text("Could not determine")
|
||||||
|
case .noAccount:
|
||||||
|
Text("No account")
|
||||||
|
case .restricted:
|
||||||
|
Text("Restricted")
|
||||||
|
case .temporarilyUnavailable:
|
||||||
|
Text("Temporarily Unavailable")
|
||||||
|
@unknown default:
|
||||||
|
Text(String(describing: cloudKitStatus!))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var errorReportingSection: some View {
|
var errorReportingSection: some View {
|
||||||
Section {
|
Section {
|
||||||
Toggle("Report Errors Automatically", isOn: $preferences.reportErrorsAutomatically)
|
Toggle("Report Errors Automatically", isOn: $preferences.reportErrorsAutomatically)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,40 +12,45 @@ 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(
|
||||||
OptionView<MultiColumnNavigationPreview>(
|
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()
|
||||||
|
@ -53,19 +58,26 @@ struct WidescreenNavigationPrefsView: View {
|
||||||
.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
|
|
@ -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)
|
||||||
Toggle(isOn: $preferences.useInAppSafari) {
|
if !ProcessInfo.processInfo.isiOSAppOnMac {
|
||||||
Text("Use In-App Safari")
|
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)
|
||||||
}
|
}
|
||||||
Toggle(isOn: $preferences.inAppSafariAutomaticReaderMode) {
|
|
||||||
Text("Always Use Reader Mode in In-App Safari")
|
|
||||||
}.disabled(!preferences.useInAppSafari)
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.appGroupedListRowBackground()
|
.appGroupedListRowBackground()
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue