From 322a550666444f8bc432660b3ce970b51bdb9983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Sun, 1 Sep 2024 12:42:31 +0200 Subject: [PATCH] simplified fullscreen and orientation handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - iPad: rotate to device orientation on startup - fixed controls in portrait fullscreen - iOS: don’t call setNeedsDrawing multiple times - On iOS we call set needs drawing only once. - Added cooldown time to MPV.Client setNeedsDrawing to avoid multiple successive calls - make fullscreen animation smoother - dragGesture now calls toggleFullScreenAction - fix tvOS and macOS build Signed-off-by: Toni Förster --- .../PlayerSettingsGroupExporter.swift | 2 +- .../PlayerSettingsGroupImporter.swift | 4 +- Model/Player/Backends/MPVClient.swift | 22 +++ Model/Player/PlayerModel.swift | 139 ++++++++++-------- Shared/Defaults.swift | 16 +- Shared/Player/AppleAVPlayerView.swift | 5 +- Shared/Player/Controls/PlayerControls.swift | 2 +- Shared/Player/PlayerDragGesture.swift | 6 +- .../Player/Video Details/VideoActions.swift | 2 +- Shared/Player/VideoPlayerView.swift | 19 +-- Shared/Settings/BrowsingSettings.swift | 19 ++- Shared/Settings/PlayerSettings.swift | 22 ++- Shared/YatteeApp.swift | 36 ++++- Yattee.xcodeproj/project.pbxproj | 8 +- iOS/AppDelegate.swift | 8 +- iOS/OrientationModel.swift | 80 +++++----- 16 files changed, 213 insertions(+), 177 deletions(-) diff --git a/Model/Import Export Settings/Exporters/PlayerSettingsGroupExporter.swift b/Model/Import Export Settings/Exporters/PlayerSettingsGroupExporter.swift index 11330500..2eec37c9 100644 --- a/Model/Import Export Settings/Exporters/PlayerSettingsGroupExporter.swift +++ b/Model/Import Export Settings/Exporters/PlayerSettingsGroupExporter.swift @@ -44,7 +44,7 @@ final class PlayerSettingsGroupExporter: SettingsGroupExporter { #endif #if os(iOS) - export["honorSystemOrientationLock"].bool = Defaults[.honorSystemOrientationLock] + export["isOrientationLocked"].bool = Defaults[.isOrientationLocked] export["enterFullscreenInLandscape"].bool = Defaults[.enterFullscreenInLandscape] export["rotateToLandscapeOnEnterFullScreen"].string = Defaults[.rotateToLandscapeOnEnterFullScreen].rawValue #endif diff --git a/Model/Import Export Settings/Importers/PlayerSettingsGroupImporter.swift b/Model/Import Export Settings/Importers/PlayerSettingsGroupImporter.swift index 2553edbb..b18bff28 100644 --- a/Model/Import Export Settings/Importers/PlayerSettingsGroupImporter.swift +++ b/Model/Import Export Settings/Importers/PlayerSettingsGroupImporter.swift @@ -97,8 +97,8 @@ struct PlayerSettingsGroupImporter { #endif #if os(iOS) - if let honorSystemOrientationLock = json["honorSystemOrientationLock"].bool { - Defaults[.honorSystemOrientationLock] = honorSystemOrientationLock + if let isOrientationLocked = json["isOrientationLocked"].bool { + Defaults[.isOrientationLocked] = isOrientationLocked } if let enterFullscreenInLandscape = json["enterFullscreenInLandscape"].bool { diff --git a/Model/Player/Backends/MPVClient.swift b/Model/Player/Backends/MPVClient.swift index 85650e9d..0ef5832b 100644 --- a/Model/Player/Backends/MPVClient.swift +++ b/Model/Player/Backends/MPVClient.swift @@ -14,6 +14,8 @@ final class MPVClient: ObservableObject { } private var logger = Logger(label: "mpv-client") + private var needsDrawingCooldown = false + private var needsDrawingWorkItem: DispatchWorkItem? var mpv: OpaquePointer! var mpvGL: OpaquePointer! @@ -389,10 +391,30 @@ final class MPVClient: ObservableObject { } func setNeedsDrawing(_ needsDrawing: Bool) { + // Check if we are currently in a cooldown period + guard !needsDrawingCooldown else { + logger.info("Not drawing, cooldown in progress") + return + } + logger.info("needs drawing: \(needsDrawing)") + + // Set the cooldown flag to true and cancel any existing work item + needsDrawingCooldown = true + needsDrawingWorkItem?.cancel() + #if !os(macOS) glView?.needsDrawing = needsDrawing #endif + + // Create a new DispatchWorkItem to reset the cooldown flag after 0.1 seconds + let workItem = DispatchWorkItem { [weak self] in + self?.needsDrawingCooldown = false + } + needsDrawingWorkItem = workItem + + // Schedule the cooldown reset after 0.1 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: workItem) } func command( diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index aa99e7d5..d02b8af9 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -56,7 +56,6 @@ final class PlayerModel: ObservableObject { @Published var presentingPlayer = false { didSet { handlePresentationChange() } } @Published var activeBackend = PlayerBackendType.mpv @Published var forceBackendOnPlay: PlayerBackendType? - @Published var wasFullscreen = false var avPlayerBackend = AVPlayerBackend() var mpvBackend = MPVBackend() @@ -131,6 +130,12 @@ final class PlayerModel: ObservableObject { #if os(iOS) @Published var lockedOrientation: UIInterfaceOrientationMask? + @Published var isOrientationLocked: Bool { + didSet { + Defaults[.isOrientationLocked] = isOrientationLocked + } + } + @Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen #endif @@ -201,6 +206,16 @@ final class PlayerModel: ObservableObject { #endif init() { + #if os(iOS) + isOrientationLocked = Defaults[.isOrientationLocked] + + if isOrientationLocked, Defaults[.lockPortraitWhenBrowsing] { + lockedOrientation = UIInterfaceOrientationMask.portrait + Orientation.lockOrientation(.portrait, andRotateTo: .portrait) + } else if isOrientationLocked { + lockOrientationAction() + } + #endif #if !os(macOS) mpvBackend.controller = mpvController mpvBackend.client = mpvController.client @@ -517,7 +532,10 @@ final class PlayerModel: ObservableObject { } private func handlePresentationChange() { - backend.setNeedsDrawing(presentingPlayer) + #if !os(iOS) + // TODO: Check whether this is neede on tvOS and macOS + backend.setNeedsDrawing(presentingPlayer) + #endif #if os(iOS) if presentingPlayer, activeBackend == .appleAVPlayer, avPlayerUsesSystemControls, Constants.isIPhone { @@ -551,8 +569,6 @@ final class PlayerModel: ObservableObject { } else { Orientation.lockOrientation(.allButUpsideDown) } - - OrientationModel.shared.stopOrientationUpdates() #endif } } @@ -659,32 +675,37 @@ final class PlayerModel: ObservableObject { } func closeCurrentItem(finished: Bool = false) { - pause() - videoBeingOpened = nil - advancing = false - forceBackendOnPlay = nil - + guard !closing else { return } closing = true - controls.presentingControls = false - self.prepareCurrentItemForHistory(finished: finished) + if playingFullScreen { exitFullScreen() } - self.hide() - - Delay.by(0.8) { [weak self] in + Delay.by(0.3) { [weak self] in guard let self else { return } - self.closePiP() + pause() + videoBeingOpened = nil + advancing = false + forceBackendOnPlay = nil - withAnimation { - self.currentItem = nil + controls.presentingControls = false + + self.prepareCurrentItemForHistory(finished: finished) + self.hide() + + Delay.by(0.7) { [weak self] in + guard let self else { return } + if playingInPictureInPicture { self.closePiP() } + + withAnimation { + self.currentItem = nil + } + + self.updateNowPlayingInfo() + self.backend.closeItem() + self.aspectRatio = VideoPlayerView.defaultAspectRatio + self.resetAutoplay() + self.closing = false } - self.updateNowPlayingInfo() - - self.backend.closeItem() - self.aspectRatio = VideoPlayerView.defaultAspectRatio - self.resetAutoplay() - self.closing = false - self.playingFullScreen = false } } @@ -773,7 +794,7 @@ final class PlayerModel: ObservableObject { } func toggleFullScreenAction() { - toggleFullscreen(playingFullScreen, showControls: false) + toggleFullscreen(playingFullScreen, showControls: false, initiatedByButton: true) } func togglePiPAction() { @@ -786,20 +807,21 @@ final class PlayerModel: ObservableObject { #if os(iOS) var lockOrientationImage: String { - lockedOrientation.isNil ? "lock.rotation.open" : "lock.rotation" + isOrientationLocked ? "lock.rotation" : "lock.rotation.open" } func lockOrientationAction() { - if lockedOrientation.isNil { + // This makes toggling orientation lock more robust + if lockedOrientation.isNil || !isOrientationLocked { + isOrientationLocked = true let orientationMask = OrientationTracker.shared.currentInterfaceOrientationMask lockedOrientation = orientationMask let orientation = OrientationTracker.shared.currentInterfaceOrientation - Orientation.lockOrientation(orientationMask, andRotateTo: .landscapeLeft) - // iOS 16 workaround - Orientation.lockOrientation(orientationMask, andRotateTo: orientation) + Orientation.lockOrientation(orientationMask, andRotateTo: playingFullScreen ? nil : orientation) } else { + isOrientationLocked = false lockedOrientation = nil - Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation) + Orientation.lockOrientation(.allButUpsideDown) } } #endif @@ -985,7 +1007,14 @@ final class PlayerModel: ObservableObject { } #else func handleEnterForeground() { - setNeedsDrawing(presentingPlayer) + #if os(iOS) + OrientationTracker.shared.startDeviceOrientationTracking() + #endif + + #if os(tvOS) + // TODO: Not sure if this is realy needed on tvOS, maybe it can be removed. + setNeedsDrawing(presentingPlayer) + #endif if !musicMode, activeBackend == .mpv { mpvBackend.addVideoTrackFromStream() @@ -995,17 +1024,6 @@ final class PlayerModel: ObservableObject { avPlayerBackend.bindPlayerToLayer() } - #if os(iOS) - if wasFullscreen { - wasFullscreen = false - DispatchQueue.main.async { [weak self] in - Delay.by(0.3) { - self?.enterFullScreen() - } - } - } - #endif - guard closePiPAndOpenPlayerOnEnteringForeground, playingInPictureInPicture else { return } @@ -1018,6 +1036,10 @@ final class PlayerModel: ObservableObject { } func handleEnterBackground() { + #if os(iOS) + OrientationTracker.shared.stopDeviceOrientationTracking() + #endif + if Defaults[.pauseOnEnteringBackground], !playingInPictureInPicture, !musicMode { pause() } else if !playingInPictureInPicture, activeBackend == .appleAVPlayer { @@ -1025,15 +1047,6 @@ final class PlayerModel: ObservableObject { } else if activeBackend == .mpv, !musicMode { mpvBackend.setVideoToNo() } - #if os(iOS) - guard playingFullScreen else { return } - wasFullscreen = playingFullScreen - DispatchQueue.main.async { [weak self] in - Delay.by(0.3) { - self?.exitFullScreen(showControls: false) - } - } - #endif } #endif @@ -1124,7 +1137,7 @@ final class PlayerModel: ObservableObject { task.resume() } - func toggleFullscreen(_ isFullScreen: Bool, showControls: Bool = true) { + func toggleFullscreen(_ isFullScreen: Bool, showControls: Bool = true, initiatedByButton: Bool = false) { controls.presentingControls = showControls && isFullScreen #if os(macOS) @@ -1139,15 +1152,13 @@ final class PlayerModel: ObservableObject { avPlayerBackend.controller.enterFullScreen(animated: true) return } - guard rotateToLandscapeOnEnterFullScreen.isRotating else { return } + let lockOrientation = rotateToLandscapeOnEnterFullScreen.interfaceOrientation if currentVideoIsLandscape { - let delay = activeBackend == .appleAVPlayer && avPlayerUsesSystemControls ? 0.8 : 0 - // not sure why but first rotation call is ignore so doing rotate to same orientation first - Delay.by(delay) { - let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape ? OrientationTracker.shared.currentInterfaceOrientation : self.rotateToLandscapeOnEnterFullScreen.interaceOrientation - Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation) - Orientation.lockOrientation(.allButUpsideDown, andRotateTo: orientation) + if initiatedByButton { + Orientation.lockOrientation(self.isOrientationLocked ? (lockOrientation == .landscapeRight ? .landscapeRight : .landscapeLeft) : .landscape) } + let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape ? OrientationTracker.shared.currentInterfaceOrientation : self.rotateToLandscapeOnEnterFullScreen.interfaceOrientation + Orientation.lockOrientation(self.isOrientationLocked ? (lockOrientation == .landscapeRight ? .landscapeRight : .landscapeLeft) : .landscape, andRotateTo: orientation) } } else { if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls { @@ -1155,10 +1166,12 @@ final class PlayerModel: ObservableObject { avPlayerBackend.controller.dismiss(animated: true) return } - let rotationOrientation = Constants.isIPhone ? UIInterfaceOrientation.portrait : nil - Orientation.lockOrientation(.allButUpsideDown, andRotateTo: rotationOrientation) + if Defaults[.lockPortraitWhenBrowsing] { + lockedOrientation = UIInterfaceOrientationMask.portrait + } + let rotationOrientation = Defaults[.lockPortraitWhenBrowsing] ? UIInterfaceOrientation.portrait : nil + Orientation.lockOrientation(Defaults[.lockPortraitWhenBrowsing] ? .portrait : .allButUpsideDown, andRotateTo: rotationOrientation) } - #endif } diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 52a80787..3cce49f8 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -93,12 +93,9 @@ extension Defaults.Keys { static let enableReturnYouTubeDislike = Key("enableReturnYouTubeDislike", default: false) #if os(iOS) - static let honorSystemOrientationLock = Key("honorSystemOrientationLock", default: true) + static let isOrientationLocked = Key("isOrientationLocked", default: Constants.isIPhone) static let enterFullscreenInLandscape = Key("enterFullscreenInLandscape", default: Constants.isIPhone) - static let rotateToLandscapeOnEnterFullScreen = Key( - "rotateToLandscapeOnEnterFullScreen", - default: Constants.isIPhone ? .landscapeRight : .disabled - ) + static let rotateToLandscapeOnEnterFullScreen = Key("rotateToLandscapeOnEnterFullScreen", default: .landscapeRight) #endif static let closePiPOnNavigation = Key("closePiPOnNavigation", default: false) @@ -612,26 +609,19 @@ enum PlayerTapGestureAction: String, CaseIterable, Defaults.Serializable { } enum FullScreenRotationSetting: String, CaseIterable, Defaults.Serializable { - case disabled case landscapeLeft case landscapeRight #if os(iOS) - var interaceOrientation: UIInterfaceOrientation { + var interfaceOrientation: UIInterfaceOrientation { switch self { case .landscapeLeft: return .landscapeLeft case .landscapeRight: return .landscapeRight - default: - return .portrait } } #endif - - var isRotating: Bool { - self != .disabled - } } struct WidgetSettings: Defaults.Serializable { diff --git a/Shared/Player/AppleAVPlayerView.swift b/Shared/Player/AppleAVPlayerView.swift index 294bee82..a0b78a8e 100644 --- a/Shared/Player/AppleAVPlayerView.swift +++ b/Shared/Player/AppleAVPlayerView.swift @@ -17,12 +17,11 @@ import SwiftUI #if os(iOS) func playerViewController(_: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator) { - guard rotateToLandscapeOnEnterFullScreen.isRotating else { return } if PlayerModel.shared.currentVideoIsLandscape { let delay = PlayerModel.shared.activeBackend == .appleAVPlayer && avPlayerUsesSystemControls ? 0.8 : 0 // not sure why but first rotation call is ignore so doing rotate to same orientation first Delay.by(delay) { - let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape ? OrientationTracker.shared.currentInterfaceOrientation : self.rotateToLandscapeOnEnterFullScreen.interaceOrientation + let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape ? OrientationTracker.shared.currentInterfaceOrientation : self.rotateToLandscapeOnEnterFullScreen.interfaceOrientation Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation) Orientation.lockOrientation(.allButUpsideDown, andRotateTo: orientation) } @@ -37,8 +36,6 @@ import SwiftUI } if !context.isCancelled { #if os(iOS) - self.player.lockedOrientation = nil - if Constants.isIPhone { Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait) } diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index c8bfeae1..f4b23335 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -389,7 +389,7 @@ struct PlayerControls: View { #if os(iOS) private var lockOrientationButton: some View { - button("Lock Rotation", systemImage: player.lockOrientationImage, active: !player.lockedOrientation.isNil, action: player.lockOrientationAction) + button("Lock Rotation", systemImage: player.lockOrientationImage, active: player.isOrientationLocked, action: player.lockOrientationAction) } #endif diff --git a/Shared/Player/PlayerDragGesture.swift b/Shared/Player/PlayerDragGesture.swift index 64975eb4..69c0c965 100644 --- a/Shared/Player/PlayerDragGesture.swift +++ b/Shared/Player/PlayerDragGesture.swift @@ -64,11 +64,7 @@ extension VideoPlayerView { // Toggle fullscreen on upward drag only when not disabled if verticalDrag < -50 { - if player.playingFullScreen { - player.exitFullScreen(showControls: false) - } else { - player.enterFullScreen() - } + player.toggleFullScreenAction() disableGestureTemporarily() return } diff --git a/Shared/Player/Video Details/VideoActions.swift b/Shared/Player/Video Details/VideoActions.swift index a36a67a7..4175a1e1 100644 --- a/Shared/Player/Video Details/VideoActions.swift +++ b/Shared/Player/Video Details/VideoActions.swift @@ -158,7 +158,7 @@ struct VideoActions: View { actionButton("PiP", systemImage: player.pipImage, active: player.playingInPictureInPicture, action: player.togglePiPAction) #if os(iOS) case .lockOrientation: - actionButton("Lock", systemImage: player.lockOrientationImage, active: player.lockedOrientation != nil, action: player.lockOrientationAction) + actionButton("Lock", systemImage: player.lockOrientationImage, active: player.isOrientationLocked, action: player.lockOrientationAction) #endif case .restart: actionButton("Replay", systemImage: "backward.end.fill", action: player.replayAction) diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 6127a076..be678a8c 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -111,9 +111,6 @@ struct VideoPlayerView: View { .onChange(of: geometry.size) { _ in self.playerSize = geometry.size } - .onChange(of: fullScreenDetails) { value in - player.backend.setNeedsDrawing(!value) - } #if os(iOS) .onChange(of: player.presentingPlayer) { newValue in if newValue { @@ -127,19 +124,6 @@ struct VideoPlayerView: View { } #endif viewDragOffset = 0 - - Delay.by(0.2) { - orientationModel.configureOrientationUpdatesBasedOnAccelerometer() - - if let orientationMask = player.lockedOrientation { - Orientation.lockOrientation( - orientationMask, - andRotateTo: orientationMask == .landscapeLeft ? .landscapeLeft : orientationMask == .landscapeRight ? .landscapeRight : .portrait - ) - } else { - Orientation.lockOrientation(.allButUpsideDown) - } - } } .onAnimationCompleted(for: viewDragOffset) { guard !dragGestureState else { return } @@ -313,11 +297,14 @@ struct VideoPlayerView: View { playerSize: player.playerSize, fullScreen: fullScreenDetails )) + #if os(macOS) + // TODO: Check whether this is needed on macOS. .onDisappear { if player.presentingPlayer { player.setNeedsDrawing(true) } } + #endif .id(player.currentVideo?.cacheKey) .transition(.opacity) } else { diff --git a/Shared/Settings/BrowsingSettings.swift b/Shared/Settings/BrowsingSettings.swift index f687d0f8..632834aa 100644 --- a/Shared/Settings/BrowsingSettings.swift +++ b/Shared/Settings/BrowsingSettings.swift @@ -10,6 +10,7 @@ struct BrowsingSettings: View { @Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges @Default(.keepChannelsWithUnwatchedFeedOnTop) private var keepChannelsWithUnwatchedFeedOnTop #if os(iOS) + @Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape @Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing @Default(.showDocuments) private var showDocuments #endif @@ -161,14 +162,18 @@ struct BrowsingSettings: View { #if os(iOS) Toggle("Show Documents", isOn: $showDocuments) - Toggle("Lock portrait mode", isOn: $lockPortraitWhenBrowsing) - .onChange(of: lockPortraitWhenBrowsing) { lock in - if lock { - Orientation.lockOrientation(.portrait, andRotateTo: .portrait) - } else { - Orientation.lockOrientation(.allButUpsideDown) + if Constants.isIPad { + Toggle("Lock portrait mode", isOn: $lockPortraitWhenBrowsing) + .onChange(of: lockPortraitWhenBrowsing) { lock in + if lock { + enterFullscreenInLandscape = true + Orientation.lockOrientation(.portrait, andRotateTo: .portrait) + } else { + enterFullscreenInLandscape = false + Orientation.lockOrientation(.allButUpsideDown) + } } - } + } #endif if !accounts.isEmpty { diff --git a/Shared/Settings/PlayerSettings.swift b/Shared/Settings/PlayerSettings.swift index dc9657d9..4bdc40bf 100644 --- a/Shared/Settings/PlayerSettings.swift +++ b/Shared/Settings/PlayerSettings.swift @@ -18,8 +18,8 @@ struct PlayerSettings: View { @Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer @Default(.closeVideoOnEOF) private var closeVideoOnEOF #if os(iOS) - @Default(.honorSystemOrientationLock) private var honorSystemOrientationLock @Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape + @Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing @Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen #endif @Default(.closePiPOnNavigation) private var closePiPOnNavigation @@ -87,7 +87,7 @@ struct PlayerSettings: View { } pauseOnHidingPlayerToggle closeVideoOnEOFToggle - #if !os(tvOS) + #if os(macOS) exitFullscreenOnEOFToggle #endif #if !os(macOS) @@ -202,11 +202,12 @@ struct PlayerSettings: View { #endif #if os(iOS) - Section(header: SettingsHeader(text: "Orientation".localized())) { - if idiom == .pad { + Section(header: SettingsHeader(text: "Fullscreen".localized())) { + if Constants.isIPad { enterFullscreenInLandscapeToggle } - honorSystemOrientationLockToggle + + exitFullscreenOnEOFToggle rotateToLandscapeOnEnterFullScreenPicker } #endif @@ -318,20 +319,15 @@ struct PlayerSettings: View { #endif #if os(iOS) - private var honorSystemOrientationLockToggle: some View { - Toggle("Honor orientation lock", isOn: $honorSystemOrientationLock) - .disabled(!enterFullscreenInLandscape) - } - private var enterFullscreenInLandscapeToggle: some View { - Toggle("Enter fullscreen in landscape", isOn: $enterFullscreenInLandscape) + Toggle("Enter fullscreen in landscape orientation", isOn: $enterFullscreenInLandscape) + .disabled(lockPortraitWhenBrowsing) } private var rotateToLandscapeOnEnterFullScreenPicker: some View { - Picker("Rotate when entering fullscreen on landscape video", selection: $rotateToLandscapeOnEnterFullScreen) { + Picker("Default orientation", selection: $rotateToLandscapeOnEnterFullScreen) { Text("Landscape left").tag(FullScreenRotationSetting.landscapeLeft) Text("Landscape right").tag(FullScreenRotationSetting.landscapeRight) - Text("No rotation").tag(FullScreenRotationSetting.disabled) } .modifier(SettingsPickerModifier()) } diff --git a/Shared/YatteeApp.swift b/Shared/YatteeApp.swift index 09a9a746..8ed8cf94 100644 --- a/Shared/YatteeApp.swift +++ b/Shared/YatteeApp.swift @@ -204,9 +204,14 @@ struct YatteeApp: App { } #if os(iOS) - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { if Defaults[.lockPortraitWhenBrowsing] { - Orientation.lockOrientation(.all, andRotateTo: .portrait) + Orientation.lockOrientation(.portrait, andRotateTo: .portrait) + } else { + let rotationOrientation = + OrientationTracker.shared.currentDeviceOrientation.rawValue == 4 ? UIInterfaceOrientation.landscapeRight : + (OrientationTracker.shared.currentDeviceOrientation.rawValue == 3 ? UIInterfaceOrientation.landscapeLeft : UIInterfaceOrientation.portrait) + Orientation.lockOrientation(.allButUpsideDown, andRotateTo: rotationOrientation) } } #endif @@ -225,6 +230,17 @@ struct YatteeApp: App { DispatchQueue.global(qos: .userInitiated).async { self.migrateQualityProfiles() } + + #if os(iOS) + DispatchQueue.global(qos: .userInitiated).async { + self.migrateRotateToLandscapeOnEnterFullScreen() + } + + DispatchQueue.global(qos: .userInitiated).async { + self.migrateLockPortraitWhenBrowsing() + } + + #endif } } @@ -253,6 +269,22 @@ struct YatteeApp: App { } } + #if os(iOS) + func migrateRotateToLandscapeOnEnterFullScreen() { + if Defaults[.rotateToLandscapeOnEnterFullScreen] != .landscapeRight || Defaults[.rotateToLandscapeOnEnterFullScreen] != .landscapeLeft { + Defaults[.rotateToLandscapeOnEnterFullScreen] = .landscapeRight + } + } + + func migrateLockPortraitWhenBrowsing() { + if Constants.isIPhone { + Defaults[.lockPortraitWhenBrowsing] = true + } else if Constants.isIPad, Defaults[.lockPortraitWhenBrowsing] { + Defaults[.enterFullscreenInLandscape] = true + } + } + #endif + var navigationStyle: NavigationStyle { #if os(iOS) return horizontalSizeClass == .compact ? .tab : .sidebar diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 98579882..7711a39e 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -4366,7 +4366,9 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UIRequiresFullScreen = YES; INFOPLIST_KEY_UIStatusBarHidden = NO; - INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UIStatusBarStyle = ""; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -4415,7 +4417,9 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UIRequiresFullScreen = YES; INFOPLIST_KEY_UIStatusBarHidden = NO; - INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UIStatusBarStyle = ""; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index a883355b..e6ee8a08 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -1,16 +1,17 @@ import AVFoundation +import Defaults import Foundation import Logging import UIKit final class AppDelegate: UIResponder, UIApplicationDelegate { - var orientationLock = UIInterfaceOrientationMask.all + var orientationLock = UIInterfaceOrientationMask.allButUpsideDown - private var logger = Logger(label: "stream.yattee.app.delegalate") + private var logger = Logger(label: "stream.yattee.app.delegate") private(set) static var instance: AppDelegate! func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask { - orientationLock + return orientationLock } func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // swiftlint:disable:this discouraged_optional_collection @@ -19,6 +20,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { #if !os(macOS) UIViewController.swizzleHomeIndicatorProperty() OrientationTracker.shared.startDeviceOrientationTracking() + OrientationModel.shared.startOrientationUpdates() // Configure the audio session for playback do { diff --git a/iOS/OrientationModel.swift b/iOS/OrientationModel.swift index 7f804899..e4beb232 100644 --- a/iOS/OrientationModel.swift +++ b/iOS/OrientationModel.swift @@ -1,10 +1,12 @@ 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? @@ -13,79 +15,69 @@ final class OrientationModel { private var player = PlayerModel.shared - func configureOrientationUpdatesBasedOnAccelerometer() { - let currentOrientation = OrientationTracker.shared.currentInterfaceOrientation - if currentOrientation.isLandscape, - Defaults[.enterFullscreenInLandscape], - !Defaults[.honorSystemOrientationLock], - !player.playingFullScreen, - !player.currentItem.isNil, - player.lockedOrientation.isNil || player.lockedOrientation!.contains(.landscape), - !player.playingInPictureInPicture, - player.presentingPlayer - { - DispatchQueue.main.async { - self.player.controls.presentingControls = false - self.player.enterFullScreen(showControls: false) - } - - player.onPresentPlayer.append { - Orientation.lockOrientation(.allButUpsideDown, andRotateTo: currentOrientation) - } - } - + func startOrientationUpdates() { + // Ensure the orientation observer is active orientationObserver = NotificationCenter.default.addObserver( forName: OrientationTracker.deviceOrientationChangedNotification, object: nil, queue: .main ) { _ in - guard !Defaults[.honorSystemOrientationLock], - self.player.presentingPlayer, - !self.player.playingInPictureInPicture, - self.player.lockedOrientation.isNil + self.logger.info("Notification received: Device orientation changed.") + + // We only allow .portrait and are not showing the player + guard (!self.player.presentingPlayer && !Defaults[.lockPortraitWhenBrowsing]) || self.player.presentingPlayer else { return } let orientation = OrientationTracker.shared.currentInterfaceOrientation + self.logger.info("Current interface orientation: \(orientation)") - guard self.lastOrientation != orientation else { + // 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) || (!Defaults[.lockPortraitWhenBrowsing] && !self.player.presentingPlayer) || (!Defaults[.lockPortraitWhenBrowsing] && self.player.presentingPlayer && !self.player.isOrientationLocked) + else { + self.logger.info("Only updating orientation without actions.") return } - self.lastOrientation = orientation - DispatchQueue.main.async { - guard Defaults[.enterFullscreenInLandscape], - self.player.presentingPlayer - else { - return - } - self.orientationDebouncer.callback = { DispatchQueue.main.async { if orientation.isLandscape { - self.player.controls.presentingControls = false - self.player.enterFullScreen(showControls: false) + if Defaults[.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.player.exitFullScreen(showControls: false) - Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait) + self.logger.info("Exiting fullscreen because orientation is portrait.") + if self.player.playingFullScreen { + self.player.exitFullScreen(showControls: false) + } + if Defaults[.lockPortraitWhenBrowsing] { + Orientation.lockOrientation(.portrait, andRotateTo: .portrait) + } else { + Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation) + } } } } - self.orientationDebouncer.call() } } } - func stopOrientationUpdates() { - guard let observer = orientationObserver else { return } - NotificationCenter.default.removeObserver(observer) - } - 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