import Defaults import Foundation import Logging import Repeat import SwiftUI final class OrientationModel { static var shared = OrientationModel() let logger = Logger(label: "stream.yattee.orientation.model") var orientation = UIInterfaceOrientation.portrait var lastOrientation: UIInterfaceOrientation? var orientationDebouncer = Debouncer(.milliseconds(300)) var orientationObserver: Any? @Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing @Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape private var player = PlayerModel.shared func startOrientationUpdates() { // Ensure the orientation observer is active orientationObserver = NotificationCenter.default.addObserver( forName: OrientationTracker.deviceOrientationChangedNotification, object: nil, queue: .main ) { _ in self.logger.info("Notification received: Device orientation changed.") // We only allow .portrait and are not showing the player guard (!self.player.presentingPlayer && !self.lockPortraitWhenBrowsing) || self.player.presentingPlayer else { return } let orientation = OrientationTracker.shared.currentInterfaceOrientation self.logger.info("Current interface orientation: \(orientation)") // Always update lastOrientation to keep track of the latest state if self.lastOrientation != orientation { self.lastOrientation = orientation self.logger.info("Orientation changed to: \(orientation)") } else { self.logger.info("Orientation has not changed.") } // Only take action if the player is active and presenting guard (!self.player.isOrientationLocked && !self.player.playingInPictureInPicture) || (!self.lockPortraitWhenBrowsing && !self.player.presentingPlayer) || (!self.lockPortraitWhenBrowsing && self.player.presentingPlayer && !self.player.isOrientationLocked) else { self.logger.info("Only updating orientation without actions.") return } DispatchQueue.main.async { self.orientationDebouncer.callback = { DispatchQueue.main.async { if orientation.isLandscape { if self.enterFullscreenInLandscape, self.player.presentingPlayer { self.logger.info("Entering fullscreen because orientation is landscape.") self.player.controls.presentingControls = false self.player.enterFullScreen(showControls: false) } Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation) } else { self.logger.info("Exiting fullscreen because orientation is portrait.") if self.player.playingFullScreen { self.player.exitFullScreen(showControls: false) } if self.lockPortraitWhenBrowsing { Orientation.lockOrientation(.portrait, andRotateTo: .portrait) } else { Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation) } } } } self.orientationDebouncer.call() } } } func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation: UIInterfaceOrientation? = nil) { logger.info("Locking orientation to: \(orientation), rotating to: \(String(describing: rotateOrientation)).") if let rotateOrientation { self.orientation = rotateOrientation lastOrientation = rotateOrientation } Orientation.lockOrientation(orientation, andRotateTo: rotateOrientation) } }