Compare commits

...

13 Commits

36 changed files with 479 additions and 277 deletions

View File

@ -301,6 +301,7 @@ extension MainActor {
@available(iOS, obsoleted: 17.0)
@available(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)

View File

@ -181,13 +181,8 @@ class ToolbarController: ViewController {
private var formatButtons: some View {
ForEach(StatusFormat.allCases, id: \.rawValue) { format in
Button(action: controller.formatAction(format)) {
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))
}
Image(systemName: format.imageName)
.font(.system(size: imageSize))
}
.accessibilityLabel(format.accessibilityLabel)
.padding(5)

View File

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

View File

@ -259,11 +259,7 @@ fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable {
if range.length > 0 {
let formatMenu = suggestedActions[index] as! UIMenu
let newFormatMenu = formatMenu.replacingChildren(StatusFormat.allCases.map { fmt in
var image: UIImage?
if let imageName = fmt.imageName {
image = UIImage(systemName: imageName)
}
return UIAction(title: fmt.accessibilityLabel, image: image) { [weak self] _ in
return UIAction(title: fmt.accessibilityLabel, image: UIImage(systemName: fmt.imageName)) { [weak self] _ in
self?.applyFormat(fmt)
}
})

View File

@ -9,8 +9,10 @@ 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
@ -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) {
self.titleKey = titleKey
#if !os(visionOS)
self.labelHidden = labelHidden
#endif
self.alignment = alignment
self._value = value
self.onChange = onChange
@ -27,6 +31,11 @@ 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
@ -40,6 +49,7 @@ public struct AsyncPicker<V: Hashable, Content: View>: View {
picker
}
}
#endif
}
private var picker: some View {

View File

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

View File

@ -8,10 +8,21 @@
import Foundation
import Pachyderm
struct StatusContentTypeKey: PreferenceKey {
struct StatusContentTypeKey: MigratablePreferenceKey {
static var defaultValue: StatusContentType { .plain }
}
struct FeatureFlagsKey: PreferenceKey {
struct FeatureFlagsKey: MigratablePreferenceKey, CustomCodablePreferenceKey {
static var defaultValue: Set<FeatureFlag> { [] }
static func encode(value: Set<FeatureFlag>, to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value.map(\.rawValue))
}
static func decode(from decoder: any Decoder) throws -> Set<FeatureFlag>? {
let container = try decoder.singleValueContainer()
let names = try container.decode([String].self)
return Set(names.compactMap(FeatureFlag.init(rawValue:)))
}
}

View File

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

View File

@ -7,11 +7,11 @@
import Foundation
struct OppositeCollapseKeywordsKey: PreferenceKey {
struct OppositeCollapseKeywordsKey: MigratablePreferenceKey {
static var defaultValue: [String] { [] }
}
struct ConfirmReblogKey: PreferenceKey {
struct ConfirmReblogKey: MigratablePreferenceKey {
static var defaultValue: Bool {
#if os(visionOS)
true
@ -21,6 +21,20 @@ struct ConfirmReblogKey: PreferenceKey {
}
}
struct TimelineSyncModeKey: PreferenceKey {
struct TimelineSyncModeKey: MigratablePreferenceKey {
static var defaultValue: TimelineSyncMode { .icloud }
}
struct InAppSafariKey: MigratablePreferenceKey {
static var defaultValue: Bool {
#if targetEnvironment(macCatalyst) || os(visionOS)
false
#else
if ProcessInfo.processInfo.isiOSAppOnMac {
false
} else {
true
}
#endif
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import UIKit
extension PreferenceStore {
func migrate(from legacy: LegacyPreferences) {
let migrations: [any MigrationProtocol] = [
var migrations: [any MigrationProtocol] = [
Migration(from: \.theme.theme, to: \.$theme),
Migration(from: \.pureBlackDarkMode, to: \.$pureBlackDarkMode),
Migration(from: \.accentColor, to: \.$accentColor),
@ -41,8 +41,6 @@ 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),
@ -65,7 +63,13 @@ 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)
}
@ -76,13 +80,13 @@ private protocol MigrationProtocol {
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 to: KeyPath<PreferenceStore, PreferencePublisher<Key>>
func migrate(from legacy: LegacyPreferences, to store: PreferenceStore) {
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)
}
}

View File

@ -27,15 +27,24 @@ final class Preference<Key: PreferenceKey>: Codable {
}
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)
}
}
func encode(to encoder: any Encoder) throws {
if let storedValue {
var container = encoder.singleValueContainer()
try container.encode(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)
}
}
}

View File

@ -18,3 +18,18 @@ 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?
}

View File

@ -25,7 +25,13 @@ 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
@Preference<ReplyVisibilityKey> public var defaultReplyVisibility
@ -34,17 +40,9 @@ 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<TrueKey> public var useInAppSafari
@Preference<InAppSafariKey> public var useInAppSafari
@Preference<FalseKey> public var inAppSafariAutomaticReaderMode
@Preference<FalseKey> public var expandAllContentWarnings
@Preference<TrueKey> public var collapseLongPosts

View File

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

View File

@ -15,11 +15,11 @@ final class PreferenceStoreTests: XCTestCase {
static let defaultValue = false
}
final class TestStore: Codable, ObservableObject {
private var _test = Preference<TestKey>()
final class TestStore<Key: PreferenceKey>: Codable, ObservableObject {
private var _test = Preference<Key>()
// the acutal subscript expects the enclosingInstance to be a PreferenceStore, so do it manually
var test: Bool {
var test: Key.Value {
get {
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
}
@ -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<TestKey>.self, forKey: .test)
self._test = try container.decode(Preference<Key>.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>.self, from: Data("""
let present = try decoder.decode(PreferenceCoding<TestStore<TestKey>>.self, from: Data("""
{"test": true}
""".utf8)).wrapped
XCTAssertEqual(present.test, true)
let absent = try decoder.decode(PreferenceCoding<TestStore>.self, from: Data("""
let absent = try decoder.decode(PreferenceCoding<TestStore<TestKey>>.self, from: Data("""
{}
""".utf8)).wrapped
XCTAssertEqual(absent.test, false)
}
func testEncoding() throws {
let store = TestStore()
let store = TestStore<TestKey>()
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()
let store = TestStore<TestKey>()
var cancellables = Set<AnyCancellable>()
store.objectWillChange.sink {
topLevel.fulfill()
@ -96,5 +96,33 @@ final class PreferenceStoreTests: XCTestCase {
store.test = true
wait(for: [topLevel, specificPref])
}
func testCustomCodable() throws {
struct Key: CustomCodablePreferenceKey {
static let defaultValue = 1
static func encode(value: Int, to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(2)
}
static func decode(from decoder: any Decoder) throws -> Int? {
3
}
}
let store = TestStore<Key>()
store.test = 123
let encoder = JSONEncoder()
XCTAssertEqual(String(data: try encoder.encode(PreferenceCoding(wrapped: store)), encoding: .utf8)!, """
{"test":2}
""")
let decoder = JSONDecoder()
let present = try decoder.decode(PreferenceCoding<TestStore<Key>>.self, from: Data("""
{"test":2}
""".utf8)).wrapped
XCTAssertEqual(present.test, 3)
let absent = try decoder.decode(PreferenceCoding<TestStore<Key>>.self, from: Data("""
{}
""".utf8)).wrapped
XCTAssertEqual(absent.test, 1)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,9 @@ 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)
@ -44,8 +46,13 @@ 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 self.playbackSpeed {
switch playbackSpeed {
case 0.5:
imageName = "gauge.with.dots.needle.0percent"
case 1:
@ -61,8 +68,12 @@ 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: 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
#endif
if self.player.rate > 0 {
self.player.rate = speed.rate
}
@ -90,12 +101,20 @@ 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")
@ -170,7 +189,11 @@ class VideoControlsViewController: UIViewController {
@objc private func scrubbingEnded() {
scrubbingChanged()
if wasPlayingWhenScrubbingStarted {
#if os(visionOS)
player.play()
#else
player.rate = playbackSpeed
#endif
}
}

View File

@ -17,8 +17,10 @@ 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
@ -125,7 +127,11 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
player.replaceCurrentItem(with: item)
updateItemObservations()
if isPlaying {
#if os(visionOS)
player.play()
#else
player.rate = playbackSpeed
#endif
}
}
}
@ -142,12 +148,20 @@ 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)

View File

@ -15,7 +15,9 @@ 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!
@ -24,12 +26,19 @@ 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")
}
@ -97,7 +106,11 @@ class VideoOverlayViewController: UIViewController {
if player.rate > 0 {
player.rate = 0
} else {
#if os(visionOS)
player.play()
#else
player.rate = playbackSpeed
#endif
}
}

View File

@ -83,22 +83,8 @@ struct AdvancedPrefsView : View {
HStack {
Text("iCloud Status")
Spacer()
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!))
}
cloudKitStatusLabel
.foregroundStyle(.secondary)
}
}
.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 {
Section {
Toggle("Report Errors Automatically", isOn: $preferences.reportErrorsAutomatically)

View File

@ -7,18 +7,49 @@
//
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 {
Section {
themeSection
interfaceSection
Section("Post Preview") {
MockStatusView()
.padding(.top, 8)
.padding(.horizontal, UIDevice.current.userInterfaceIdiom == .pad ? 8 : 4)
}
.appGroupedListRowBackground()
.listRowBackground(mockStatusBackground)
accountsSection
postsSection
@ -29,6 +60,69 @@ 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: {
@ -65,15 +159,16 @@ 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()
}

View File

@ -161,7 +161,7 @@ private actor MockAttachmentsGenerator {
return attachmentURLs
}
let size = CGSize(width: 100, height: 100)
let size = CGSize(width: 200, height: 200)
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 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()
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
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..<2 {
for x in 0..<4 {
for y in 0..<4 {
for x in 0..<5 {
let rect = CGRect(x: x * 45 - 5, y: y * 50 + 15, width: 20, height: 20)
ctx.cgContext.fillEllipse(in: rect)
}
}
UIColor(red: 0x08 / 255, green: 0x7c / 255, blue: 0xa7 / 255, alpha: 1).setFill()
for y in 0..<3 {
for x in 0..<2 {
for y in 0..<5 {
for x in 0..<4 {
let rect = CGRect(x: CGFloat(x) * 45 + 22.5, y: CGFloat(y) * 50 - 5, width: 10, height: 10)
ctx.cgContext.fillEllipse(in: rect)
}

View File

@ -12,40 +12,45 @@ import TuskerPreferences
struct WidescreenNavigationPrefsView: View {
@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 {
HStack {
Spacer()
OptionView<StackNavigationPreview>(
OptionView(
content: StackNavigationPreview.self,
value: .stack,
selection: $preferences.widescreenNavigationMode,
startAnimation: startAnimation
startAnimation: startAnimationSignal
) {
Text("Stack")
}
Spacer(minLength: 32)
OptionView<SplitNavigationPreview>(
OptionView(
content: SplitNavigationPreview.self,
value: .splitScreen,
selection: $preferences.widescreenNavigationMode,
startAnimation: startAnimation
startAnimation: startAnimationSignal
) {
Text("Split Screen")
}
if preferences.hasFeatureFlag(.iPadMultiColumn) {
Spacer(minLength: 32)
OptionView<MultiColumnNavigationPreview>(
value: .multiColumn,
selection: $preferences.widescreenNavigationMode,
startAnimation: startAnimation
) {
Text("Multi-Column")
}
Spacer(minLength: 32)
OptionView(
content: MultiColumnNavigationPreview.self,
value: .multiColumn,
selection: $preferences.widescreenNavigationMode,
startAnimation: startAnimationSignal
) {
Text("Multi-Column")
}
Spacer()
@ -53,19 +58,26 @@ struct WidescreenNavigationPrefsView: View {
.frame(height: 100)
.onAppear {
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
@Binding var selection: WidescreenNavigationMode
let startAnimation: PassthroughSubject<Void, Never>
@ViewBuilder let label: Text
let startAnimation: P
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
}
@ -84,7 +96,7 @@ private struct OptionView<Content: NavigationModePreview>: View {
}
private var preview: some View {
NavigationModeRepresentable<Content>(startAnimation: startAnimation)
NavigationModeRepresentable(content: Content.self, startAnimation: startAnimation)
.clipShape(RoundedRectangle(cornerRadius: 12.5, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 12.5, style: .continuous)
@ -106,11 +118,15 @@ private struct WideCapsule: Shape {
@MainActor
private protocol NavigationModePreview: UIView {
init(startAnimation: PassthroughSubject<Void, Never>)
init(startAnimation: some Publisher<Void, Never>)
}
private struct NavigationModeRepresentable<UIViewType: NavigationModePreview>: UIViewRepresentable {
let 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
}
func makeUIView(context: Context) -> UIViewType {
UIViewType(startAnimation: startAnimation)
@ -128,7 +144,7 @@ private final class StackNavigationPreview: UIView, NavigationModePreview {
private let destinationView = UIView()
private var cancellable: AnyCancellable?
init(startAnimation: PassthroughSubject<Void, Never>) {
init(startAnimation: some Publisher<Void, Never>) {
super.init(frame: .zero)
backgroundColor = .appBackground
@ -203,7 +219,7 @@ private final class SplitNavigationPreview: UIView, NavigationModePreview {
private var cellStackTrailingConstraint: NSLayoutConstraint!
private var cancellable: AnyCancellable?
init(startAnimation: PassthroughSubject<Void, Never>) {
init(startAnimation: some Publisher<Void, Never>) {
super.init(frame: .zero)
backgroundColor = .appBackground
@ -297,7 +313,7 @@ private final class MultiColumnNavigationPreview: UIView, NavigationModePreview
private var startedAnimation = false
init(startAnimation: PassthroughSubject<Void, Never>) {
init(startAnimation: some Publisher<Void, Never>) {
super.init(frame: .zero)
backgroundColor = .appSecondaryBackground

View File

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

View File

@ -1,64 +0,0 @@
//
// MediaPrefsView.swift
// Tusker
//
// Created by Shadowfacts on 2/22/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import SwiftUI
import TuskerPreferences
struct MediaPrefsView: View {
@ObservedObject var preferences = Preferences.shared
var body: some View {
List {
viewingSection
}
.listStyle(.insetGrouped)
.appGroupedListBackground(container: PreferencesNavigationController.self)
.navigationBarTitle("Media")
}
var viewingSection: some View {
Section(header: Text("Viewing")) {
Picker(selection: $preferences.attachmentBlurMode) {
ForEach(AttachmentBlurMode.allCases, id: \.self) { mode in
Text(mode.displayName).tag(mode)
}
} label: {
Text("Blur Media")
}
Toggle(isOn: $preferences.blurMediaBehindContentWarning) {
Text("Blur Media Behind Content Warning")
}
.disabled(preferences.attachmentBlurMode != .useStatusSetting)
Toggle(isOn: $preferences.automaticallyPlayGifs) {
Text("Automatically Play GIFs")
}
Toggle(isOn: $preferences.showUncroppedMediaInline) {
Text("Show Uncropped Media Inline")
}
Toggle(isOn: $preferences.showAttachmentBadges) {
Text("Show GIF/\(Text("Alt").font(.body.lowercaseSmallCaps())) Badges")
}
Toggle(isOn: $preferences.attachmentAltBadgeInverted) {
Text("Show Badge when Missing \(Text("Alt").font(.body.lowercaseSmallCaps()))")
}
.disabled(!preferences.showAttachmentBadges)
}
.appGroupedListRowBackground()
}
}
struct MediaPrefsView_Previews: PreviewProvider {
static var previews: some View {
MediaPrefsView()
}
}

View File

@ -23,7 +23,6 @@ struct PreferencesView: View {
var body: some View {
List {
accountsSection
notificationsSection
preferencesSection
aboutSection
}
@ -92,36 +91,27 @@ struct PreferencesView: View {
.appGroupedListRowBackground()
}
private var notificationsSection: some View {
Section {
NavigationLink(isActive: $navigationState.showNotificationPreferences) {
NotificationsPrefsView()
} label: {
Text("Notifications")
}
}
.appGroupedListRowBackground()
}
private var preferencesSection: some View {
Section {
NavigationLink(destination: AppearancePrefsView()) {
Text("Appearance")
}
NavigationLink(destination: ComposingPrefsView()) {
Text("Composing")
}
NavigationLink(destination: MediaPrefsView()) {
Text("Media")
PreferenceSectionLabel(title: "Appearance", systemImageName: "textformat", backgroundColor: .indigo)
}
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()) {
Text("Digital Wellness")
PreferenceSectionLabel(title: "Digital Wellness", systemImageName: "brain.fill", backgroundColor: .purple)
}
NavigationLink(destination: AdvancedPrefsView()) {
Text("Advanced")
PreferenceSectionLabel(title: "Advanced", systemImageName: "gearshape.2.fill", backgroundColor: .gray)
}
}
.appGroupedListRowBackground()
@ -129,14 +119,28 @@ struct PreferencesView: View {
private var aboutSection: some View {
Section {
NavigationLink("About") {
NavigationLink {
AboutView()
} label: {
Label {
Text("About")
} icon: {
Image("AboutIcon")
.resizable()
.clipShape(RoundedRectangle(cornerRadius: 6))
.frame(width: 30, height: 30)
}
}
NavigationLink("Tip Jar") {
NavigationLink {
TipJarView()
} label: {
// TODO: custom tip jar icon?
PreferenceSectionLabel(title: "Tip Jar", systemImageName: "dollarsign.square.fill", backgroundColor: .yellow)
}
NavigationLink("Acknowledgements") {
NavigationLink {
AcknowledgementsView()
} label: {
PreferenceSectionLabel(title: "Acknowledgements", systemImageName: "doc.text.fill", backgroundColor: .gray)
}
}
.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
//struct PreferencesView_Previews : PreviewProvider {
// static var previews: some View {

View File

@ -47,10 +47,11 @@ extension TuskerNavigationDelegate {
func selected(url: URL, allowResolveStatuses: Bool = true, allowUniversalLinks: Bool = true) {
func openSafari() {
#if os(visionOS)
#if targetEnvironment(macCatalyst) || os(visionOS)
UIApplication.shared.open(url)
#else
if Preferences.shared.useInAppSafari,
if !ProcessInfo.processInfo.isiOSAppOnMac,
Preferences.shared.useInAppSafari,
url.scheme == "https" || url.scheme == "http" {
let config = SFSafariViewController.Configuration()
config.entersReaderIfAvailable = Preferences.shared.inAppSafariAutomaticReaderMode

View File

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