diff --git a/NotificationExtension/NotificationService.swift b/NotificationExtension/NotificationService.swift index 2afff31a..3b9a4883 100644 --- a/NotificationExtension/NotificationService.swift +++ b/NotificationExtension/NotificationService.swift @@ -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(_ 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) diff --git a/Packages/TuskerComponents/Sources/TuskerComponents/AsyncPicker.swift b/Packages/TuskerComponents/Sources/TuskerComponents/AsyncPicker.swift index dc4ebd1a..d1b78a2c 100644 --- a/Packages/TuskerComponents/Sources/TuskerComponents/AsyncPicker.swift +++ b/Packages/TuskerComponents/Sources/TuskerComponents/AsyncPicker.swift @@ -9,8 +9,10 @@ import SwiftUI public struct AsyncPicker: 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: View { public init(_ titleKey: LocalizedStringKey, labelHidden: Bool = false, alignment: Alignment = .center, value: Binding, 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: 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: View { picker } } + #endif } private var picker: some View { diff --git a/Packages/TuskerComponents/Sources/TuskerComponents/AsyncToggle.swift b/Packages/TuskerComponents/Sources/TuskerComponents/AsyncToggle.swift index 7f97802a..4e0b00ac 100644 --- a/Packages/TuskerComponents/Sources/TuskerComponents/AsyncToggle.swift +++ b/Packages/TuskerComponents/Sources/TuskerComponents/AsyncToggle.swift @@ -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, 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 diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index f8091112..19b06d38 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -2499,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; }; @@ -2530,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; }; @@ -2560,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; }; diff --git a/Tusker/Activities/SaveToPhotosActivity.swift b/Tusker/Activities/SaveToPhotosActivity.swift index b4bb99ac..ec858dfc 100644 --- a/Tusker/Activities/SaveToPhotosActivity.swift +++ b/Tusker/Activities/SaveToPhotosActivity.swift @@ -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) diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 9977e820..82400f80 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -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 diff --git a/Tusker/Extensions/MainActor+Unsafe.swift b/Tusker/Extensions/MainActor+Unsafe.swift index d4e4ee5f..8ac882b7 100644 --- a/Tusker/Extensions/MainActor+Unsafe.swift +++ b/Tusker/Extensions/MainActor+Unsafe.swift @@ -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(_ 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) diff --git a/Tusker/Screens/Gallery/VideoControlsViewController.swift b/Tusker/Screens/Gallery/VideoControlsViewController.swift index 526b266d..08d18faa 100644 --- a/Tusker/Screens/Gallery/VideoControlsViewController.swift +++ b/Tusker/Screens/Gallery/VideoControlsViewController.swift @@ -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) { 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 } } diff --git a/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift b/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift index a2ff8187..162b4885 100644 --- a/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift +++ b/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift @@ -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) diff --git a/Tusker/Screens/Gallery/VideoOverlayViewController.swift b/Tusker/Screens/Gallery/VideoOverlayViewController.swift index 07b606d8..63eb056a 100644 --- a/Tusker/Screens/Gallery/VideoOverlayViewController.swift +++ b/Tusker/Screens/Gallery/VideoOverlayViewController.swift @@ -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) { 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 } } diff --git a/Tusker/Views/Attachments/GifvController.swift b/Tusker/Views/Attachments/GifvController.swift index 971e11b7..bc6737da 100644 --- a/Tusker/Views/Attachments/GifvController.swift +++ b/Tusker/Views/Attachments/GifvController.swift @@ -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)