diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index a337a4a7..9a263009 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -92,7 +92,6 @@ extension Defaults.Keys { #if os(iOS) static let honorSystemOrientationLock = Key("honorSystemOrientationLock", default: true) static let enterFullscreenInLandscape = Key("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone) - static let lockOrientationInFullScreen = Key("lockOrientationInFullScreen", default: false) #endif static let showMPVPlaybackStats = Key("showMPVPlaybackStats", default: false) diff --git a/Shared/Player/AppleAVPlayerViewController.swift b/Shared/Player/AppleAVPlayerViewController.swift index e29a9eb0..2a1a5cad 100644 --- a/Shared/Player/AppleAVPlayerViewController.swift +++ b/Shared/Player/AppleAVPlayerViewController.swift @@ -136,40 +136,13 @@ extension AppleAVPlayerViewController: AVPlayerViewControllerDelegate { func playerViewController( _: AVPlayerViewController, - willBeginFullScreenPresentationWithAnimationCoordinator context: UIViewControllerTransitionCoordinator - ) { - #if os(iOS) - if !context.isCancelled, Defaults[.lockOrientationInFullScreen] { - Orientation.lockOrientation(.landscape, andRotateTo: UIDevice.current.orientation.isLandscape ? nil : .landscapeRight) - } - #endif - } + willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator + ) {} func playerViewController( _: AVPlayerViewController, - willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator - ) { - let wasPlaying = playerModel.isPlaying - coordinator.animate(alongsideTransition: nil) { context in - #if os(iOS) - if wasPlaying { - self.playerModel.play() - } - #endif - if !context.isCancelled { - #if os(iOS) - self.playerModel.lockedOrientation = nil - if Defaults[.enterFullscreenInLandscape] { - Orientation.lockOrientation(.portrait, andRotateTo: .portrait) - } - - if wasPlaying { - self.playerModel.play() - } - #endif - } - } - } + willEndFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator + ) {} func playerViewController( _: AVPlayerViewController, diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index f0a24832..448f63dd 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -38,7 +38,6 @@ struct VideoPlayerView: View { #if os(iOS) @Environment(\.verticalSizeClass) private var verticalSizeClass - @State private var motionManager: CMMotionManager! @State private var orientation = UIInterfaceOrientation.portrait @State private var lastOrientation: UIInterfaceOrientation? #elseif os(macOS) @@ -94,7 +93,7 @@ struct VideoPlayerView: View { playerSize = geometry.size } } -// .ignoresSafeArea(.all, edges: playerEdgesIgnoringSafeArea) + .ignoresSafeArea(.all, edges: playerEdgesIgnoringSafeArea) .onChange(of: geometry.size) { size in self.playerSize = size } @@ -102,9 +101,6 @@ struct VideoPlayerView: View { player.backend.setNeedsDrawing(!value) } #if os(iOS) - .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in - handleOrientationDidChangeNotification() - } .onChange(of: player.presentingPlayer) { newValue in if newValue { viewVerticalOffset = 0 @@ -120,9 +116,6 @@ struct VideoPlayerView: View { } else { Orientation.lockOrientation(.allButUpsideDown) } - - motionManager?.stopAccelerometerUpdates() - motionManager = nil viewVerticalOffset = Self.hiddenOffset } } @@ -203,7 +196,6 @@ struct VideoPlayerView: View { #endif } } -// .ignoresSafeArea(.all, edges: fullScreenLayout ? .bottom : Edge.Set()) .frame(maxWidth: fullScreenLayout ? .infinity : nil, maxHeight: fullScreenLayout ? .infinity : nil) .onHover { hovering in hoveringPlayer = hovering @@ -253,7 +245,7 @@ struct VideoPlayerView: View { .background(Color.black) #if !os(tvOS) - if !player.playingFullScreen { + if !fullScreenLayout { VStack(spacing: 0) { #if os(iOS) VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails) @@ -281,7 +273,7 @@ struct VideoPlayerView: View { #if os(macOS) .frame(minWidth: 650) #endif - if !player.playingFullScreen { + if !fullScreenLayout { #if os(iOS) if sidebarQueue { PlayerQueueView(sidebarQueue: true, fullScreen: $fullScreenDetails) @@ -297,7 +289,7 @@ struct VideoPlayerView: View { } } #if os(iOS) - .statusBar(hidden: player.playingFullScreen) + .statusBar(hidden: fullScreenLayout) #endif } @@ -339,18 +331,26 @@ struct VideoPlayerView: View { PlayerGestures() PlayerControls(player: player, thumbnails: thumbnails) #if os(iOS) - .padding(.top, fullScreenLayout ? (safeAreaInsets.top.isZero ? safeAreaInsets.bottom : safeAreaInsets.top) : 0) + .padding(.top, controlsTopPadding) .padding(.bottom, fullScreenLayout ? safeAreaInsets.bottom : 0) #endif #endif } - .ignoresSafeArea(.all, edges: fullScreenLayout ? .vertical : Edge.Set()) #if os(iOS) - .statusBarHidden(fullScreenLayout) + .statusBarHidden(fullScreenLayout) #endif } #if os(iOS) + var controlsTopPadding: Double { + guard fullScreenLayout else { return 0 } + + let idiom = UIDevice.current.userInterfaceIdiom + guard idiom == .pad else { return safeAreaInsets.top } + + return safeAreaInsets.top.isZero ? safeAreaInsets.bottom : safeAreaInsets.top + } + var safeAreaInsets: UIEdgeInsets { UIApplication.shared.windows.first?.safeAreaInsets ?? .init() } @@ -432,7 +432,7 @@ struct VideoPlayerView: View { #if os(iOS) private func configureOrientationUpdatesBasedOnAccelerometer() { - if UIDevice.current.orientation.isLandscape, + if OrientationTracker.shared.currentInterfaceOrientation.isLandscape, Defaults[.enterFullscreenInLandscape], !player.playingFullScreen, !player.playingInPictureInPicture @@ -442,32 +442,16 @@ struct VideoPlayerView: View { } } - guard !Defaults[.honorSystemOrientationLock], motionManager.isNil else { - return - } - - motionManager = CMMotionManager() - motionManager.accelerometerUpdateInterval = 0.2 - motionManager.startAccelerometerUpdates(to: OperationQueue()) { data, _ in - guard player.presentingPlayer, !player.playingInPictureInPicture, !data.isNil else { + NotificationCenter.default.addObserver( + forName: OrientationTracker.deviceOrientationChangedNotification, + object: nil, + queue: .main + ) { _ in + guard !Defaults[.honorSystemOrientationLock], player.presentingPlayer, !player.playingInPictureInPicture else { return } - guard let acceleration = data?.acceleration else { - return - } - - var orientation = UIInterfaceOrientation.unknown - - if acceleration.x >= 0.65 { - orientation = .landscapeLeft - } else if acceleration.x <= -0.65 { - orientation = .landscapeRight - } else if acceleration.y <= -0.65 { - orientation = .portrait - } else if acceleration.y >= 0.65 { - orientation = .portraitUpsideDown - } + let orientation = OrientationTracker.shared.currentInterfaceOrientation guard lastOrientation != orientation else { return @@ -475,67 +459,21 @@ struct VideoPlayerView: View { lastOrientation = orientation - if orientation.isLandscape { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { - guard Defaults[.enterFullscreenInLandscape] else { - return - } - - player.enterFullScreen() - - let orientationLockMask = orientation == .landscapeLeft ? - UIInterfaceOrientationMask.landscapeLeft : .landscapeRight - - Orientation.lockOrientation(orientationLockMask, andRotateTo: orientation) - - guard Defaults[.lockOrientationInFullScreen] else { - return - } - - player.lockedOrientation = orientation - } - } else { - guard abs(acceleration.z) <= 0.74, - player.lockedOrientation.isNil, - Defaults[.enterFullscreenInLandscape], - !Defaults[.lockOrientationInFullScreen] - else { + DispatchQueue.main.async { + guard Defaults[.enterFullscreenInLandscape] else { return } - Orientation.lockOrientation(.portrait) - } - } - } - - private func handleOrientationDidChangeNotification() { - viewVerticalOffset = viewVerticalOffset == 0 ? 0 : Self.hiddenOffset - let newOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation - if newOrientation?.isLandscape ?? false, - player.presentingPlayer, - Defaults[.lockOrientationInFullScreen], - !player.lockedOrientation.isNil - { - Orientation.lockOrientation(.landscape, andRotateTo: newOrientation) - return - } - - guard player.presentingPlayer, Defaults[.enterFullscreenInLandscape], Defaults[.honorSystemOrientationLock] else { - return - } - - if UIDevice.current.orientation.isLandscape { - DispatchQueue.main.async { - player.lockedOrientation = newOrientation - player.enterFullScreen() - } - } else { - DispatchQueue.main.async { - player.exitFullScreen() - } - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { - player.exitFullScreen() + if orientation.isLandscape { + player.enterFullScreen() + Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation) + } else { + if !player.playingFullScreen { + player.exitFullScreen() + } else { + Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait) + } + } } } } diff --git a/Shared/Settings/PlayerSettings.swift b/Shared/Settings/PlayerSettings.swift index c3b3cc08..9f93a66e 100644 --- a/Shared/Settings/PlayerSettings.swift +++ b/Shared/Settings/PlayerSettings.swift @@ -13,7 +13,6 @@ struct PlayerSettings: View { @Default(.closeLastItemOnPlaybackEnd) private var closeLastItemOnPlaybackEnd #if os(iOS) @Default(.honorSystemOrientationLock) private var honorSystemOrientationLock - @Default(.lockOrientationInFullScreen) private var lockOrientationInFullScreen @Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape #endif @Default(.closePiPOnNavigation) private var closePiPOnNavigation @@ -93,7 +92,6 @@ struct PlayerSettings: View { enterFullscreenInLandscapeToggle } honorSystemOrientationLockToggle - lockOrientationInFullScreenToggle } #endif } @@ -186,11 +184,6 @@ struct PlayerSettings: View { private var enterFullscreenInLandscapeToggle: some View { Toggle("Enter fullscreen in landscape", isOn: $enterFullscreenInLandscape) } - - private var lockOrientationInFullScreenToggle: some View { - Toggle("Lock orientation in fullscreen", isOn: $lockOrientationInFullScreen) - .disabled(!enterFullscreenInLandscape) - } #endif private var closePiPOnNavigationToggle: some View { diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index b50ae203..9afb6144 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -511,6 +511,7 @@ 379775932689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; }; 379775942689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; }; 379775952689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; }; + 379B0253287A1CDF001015B5 /* OrientationTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379B0252287A1CDF001015B5 /* OrientationTracker.swift */; }; 37A3B15A27255E7F000FB5EE /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A3B15927255E7F000FB5EE /* SafariWebExtensionHandler.swift */; }; 37A3B15F27255E7F000FB5EE /* images in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B15E27255E7F000FB5EE /* images */; }; 37A3B16127255E7F000FB5EE /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B16027255E7F000FB5EE /* manifest.json */; }; @@ -1073,6 +1074,7 @@ 3797758A2689345500DD52A8 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; 379775922689365600DD52A8 /* Array+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Next.swift"; sourceTree = ""; }; 37992DC726CC50BC003D4C27 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 379B0252287A1CDF001015B5 /* OrientationTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrientationTracker.swift; sourceTree = ""; }; 37A3B15727255E7F000FB5EE /* Open in Yattee - macOS.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Open in Yattee - macOS.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 37A3B15927255E7F000FB5EE /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = ""; }; 37A3B15E27255E7F000FB5EE /* images */ = {isa = PBXFileReference; lastKnownFileType = folder; path = images; sourceTree = ""; }; @@ -1775,6 +1777,7 @@ children = ( 37B4E802277D0A72004BF56A /* AppDelegate.swift */, 37B4E804277D0AB4004BF56A /* Orientation.swift */, + 379B0252287A1CDF001015B5 /* OrientationTracker.swift */, 3784B23A272894DA00B09468 /* ShareSheet.swift */, 3749BF9227ADA142000480FF /* BridgingHeader.h */, 37992DC726CC50BC003D4C27 /* Info.plist */, @@ -2832,6 +2835,7 @@ 37E70923271CD43000D34DDE /* WelcomeScreen.swift in Sources */, 37BD07BB2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */, 37C0697A2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */, + 379B0253287A1CDF001015B5 /* OrientationTracker.swift in Sources */, 370B79C9286279810045DB77 /* NSObject+Swizzle.swift in Sources */, 37D4B0E42671614900C925CA /* YatteeApp.swift in Sources */, 37C3A241272359900087A57A /* Double+Format.swift in Sources */, diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index b76d1a82..93a2d07c 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -19,6 +19,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { UITabBar.appearance().backgroundImage = UIImage() UITabBar.appearance().isTranslucent = true UITabBar.appearance().backgroundColor = .clear + + OrientationTracker.shared.startDeviceOrientationTracking() #endif return true } diff --git a/iOS/Orientation.swift b/iOS/Orientation.swift index 6fa2badc..68d605d7 100644 --- a/iOS/Orientation.swift +++ b/iOS/Orientation.swift @@ -21,11 +21,16 @@ struct Orientation { static func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation: UIInterfaceOrientation? = nil) { lockOrientation(orientation) - guard !rotateOrientation.isNil else { + guard let rotateOrientation = rotateOrientation else { return } - UIDevice.current.setValue(rotateOrientation!.rawValue, forKey: "orientation") + let orientationString = rotateOrientation == .portrait ? "portrait" : rotateOrientation == .landscapeLeft ? "landscapeLeft" : + rotateOrientation == .landscapeRight ? "landscapeRight" : rotateOrientation == .portraitUpsideDown ? "portraitUpsideDown" : "allButUpsideDown" + + logger.info("rotating to \(orientationString)") + + UIDevice.current.setValue(rotateOrientation.rawValue, forKey: "orientation") UINavigationController.attemptRotationToDeviceOrientation() } } diff --git a/iOS/OrientationTracker.swift b/iOS/OrientationTracker.swift new file mode 100644 index 00000000..15c2cdaf --- /dev/null +++ b/iOS/OrientationTracker.swift @@ -0,0 +1,92 @@ +import CoreMotion +import UIKit + +public class OrientationTracker { + public static let shared = OrientationTracker() + + public static let deviceOrientationChangedNotification = NSNotification.Name("DeviceOrientationChangedNotification") + + public var currentDeviceOrientation: UIDeviceOrientation = .portrait + + public var currentInterfaceOrientation: UIInterfaceOrientation { + switch currentDeviceOrientation { + case .landscapeLeft: + return .landscapeLeft + case .landscapeRight: + return .landscapeRight + default: + return .portrait + } + } + + public var currentInterfaceOrientationMask: UIInterfaceOrientationMask { + switch currentInterfaceOrientation { + case .landscapeLeft: + return .landscapeLeft + case .landscapeRight: + return .landscapeRight + default: + return .portrait + } + } + + public var affineTransform: CGAffineTransform { + var angleRadians: Double + switch currentDeviceOrientation { + case .portrait: + angleRadians = 0 + case .landscapeLeft: + angleRadians = -0.5 * .pi + case .landscapeRight: + angleRadians = 0.5 * .pi + case .portraitUpsideDown: + angleRadians = .pi + default: + return .identity + } + return CGAffineTransform(rotationAngle: angleRadians) + } + + private let motionManager: CMMotionManager + private let queue: OperationQueue + + private init() { + motionManager = CMMotionManager() + motionManager.accelerometerUpdateInterval = 0.1 + queue = OperationQueue() + } + + public func startDeviceOrientationTracking() { + motionManager.startAccelerometerUpdates(to: queue) { accelerometerData, error in + guard error == nil else { return } + guard let accelerometerData = accelerometerData else { return } + + let newDeviceOrientation = self.deviceOrientation(forAccelerometerData: accelerometerData) + guard newDeviceOrientation != self.currentDeviceOrientation else { return } + self.currentDeviceOrientation = newDeviceOrientation + + NotificationCenter.default.post(name: Self.deviceOrientationChangedNotification, + object: nil, + userInfo: nil) + } + } + + public func stopDeviceOrientationTracking() { + motionManager.stopAccelerometerUpdates() + } + + private func deviceOrientation(forAccelerometerData accelerometerData: CMAccelerometerData) -> UIDeviceOrientation { + let treshold = 0.55 + if accelerometerData.acceleration.x >= treshold { + return .landscapeLeft + } else if accelerometerData.acceleration.x <= -treshold { + return .landscapeRight + } else if accelerometerData.acceleration.y <= -treshold { + return .portrait + } else if accelerometerData.acceleration.y >= treshold { + return .portraitUpsideDown + } else { + return currentDeviceOrientation + } + } +}