mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 21:22:53 +00:00
The swipe-action share sheet, the expanded player's safe area and the Dynamic Island side check took connectedScenes.first, which ignores which scene is active (on the iPhone Duo it can be the other panel's) and presented the share sheet from the main window, behind an expanded player. UIApplication.foregroundWindowScene/foregroundWindow resolve the foreground-active scene (else foreground-inactive, else any) and its key window, the order the widescreen layout already used. The share sheet now presents from the key window, which is the player's while it is up. The tvOS display mode manager uses the same lookup.
224 lines
9.7 KiB
Swift
224 lines
9.7 KiB
Swift
//
|
|
// ExpandedPlayerSheet+Orientation.swift
|
|
// Yattee
|
|
//
|
|
// iOS-specific orientation, fullscreen, and ambient glow functionality.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
#if os(iOS)
|
|
import UIKit
|
|
|
|
extension ExpandedPlayerSheet {
|
|
// MARK: - Safe Area Helpers
|
|
|
|
/// Get safe area insets from the window.
|
|
var windowSafeAreaInsets: UIEdgeInsets {
|
|
UIApplication.shared.foregroundWindow?.safeAreaInsets ?? .zero
|
|
}
|
|
|
|
// MARK: - Orientation Lock
|
|
|
|
/// Set up in-app orientation lock callback.
|
|
func setupOrientationLockCallback() {
|
|
DeviceRotationManager.shared.isOrientationLocked = { [weak appEnvironment] in
|
|
appEnvironment?.settingsManager.inAppOrientationLock ?? false
|
|
}
|
|
}
|
|
|
|
// MARK: - Fullscreen Toggle
|
|
|
|
/// Toggle fullscreen by rotating between portrait and landscape.
|
|
/// When orientation lock is enabled, locks to the target orientation before rotating
|
|
/// (keeps orientation restricted the entire time, just changes which orientation is allowed).
|
|
func toggleFullscreen() {
|
|
// Windowing modes that refuse programmatic rotation (iPhone Duo
|
|
// interior) get the iPad behavior: toggle the details panel instead.
|
|
guard OrientationManager.shared.canRotateProgrammatically else {
|
|
togglePortraitDetailsPanel()
|
|
return
|
|
}
|
|
guard let windowScene = UIApplication.shared.connectedScenes
|
|
.compactMap({ $0 as? UIWindowScene })
|
|
.first(where: { $0.activationState == .foregroundActive }) else { return }
|
|
|
|
let screenBounds = windowScene.screen.bounds
|
|
let isCurrentlyLandscape = screenBounds.width > screenBounds.height
|
|
let orientationManager = OrientationManager.shared
|
|
let isOrientationLocked = appEnvironment?.settingsManager.inAppOrientationLock ?? false
|
|
|
|
MPVLogging.logTransition("toggleFullscreen",
|
|
fromSize: screenBounds.size,
|
|
toSize: isCurrentlyLandscape ? CGSize(width: screenBounds.height, height: screenBounds.width) : nil)
|
|
|
|
|
|
if isCurrentlyLandscape {
|
|
// Exit fullscreen → rotate to portrait
|
|
MPVLogging.log("toggleFullscreen: exiting to portrait")
|
|
// Lock to portrait first (if lock enabled) so system allows only portrait rotation
|
|
if isOrientationLocked {
|
|
orientationManager.lock(to: .portrait)
|
|
}
|
|
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait)) { [self] error in
|
|
handleFullscreenRotationFailure(error)
|
|
}
|
|
} else {
|
|
// Enter fullscreen → rotate to landscape
|
|
let targetOrientation = Self.currentLandscapeInterfaceOrientation()
|
|
MPVLogging.log("toggleFullscreen: entering landscape")
|
|
// Lock to landscape first (if lock enabled) so system allows landscape rotation
|
|
// Use .landscape to allow both directions initially
|
|
if isOrientationLocked {
|
|
orientationManager.lock(to: .landscape)
|
|
}
|
|
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: targetOrientation)) { [self] error in
|
|
handleFullscreenRotationFailure(error)
|
|
}
|
|
// After rotation completes, re-lock to the specific landscape orientation
|
|
// Use a delay since completion handler isn't reliable for waiting for rotation
|
|
if isOrientationLocked {
|
|
Task { @MainActor in
|
|
try? await Task.sleep(nanoseconds: 300_000_000) // 0.3 seconds
|
|
orientationManager.lockToCurrentOrientation()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A refused `requestGeometryUpdate` means this windowing mode never
|
|
/// rotates programmatically; remember that and do what the tap was meant
|
|
/// to do — fill the screen with the video — via the details panel.
|
|
private func handleFullscreenRotationFailure(_ error: Error) {
|
|
MPVLogging.log("toggleFullscreen: requestGeometryUpdate failed: \(error.localizedDescription)")
|
|
guard OrientationManager.shared.noteGeometryUpdateFailure(error) else { return }
|
|
if appEnvironment?.settingsManager.inAppOrientationLock ?? false {
|
|
OrientationManager.shared.unlock()
|
|
}
|
|
togglePortraitDetailsPanel()
|
|
}
|
|
|
|
/// Fullscreen fallback when rotation isn't available: show/hide the
|
|
/// portrait details panel, mirroring `onToggleDetailsVisibility`.
|
|
private func togglePortraitDetailsPanel() {
|
|
hideControlsDuringTransition = true
|
|
if !isPortraitPanelVisible {
|
|
navigationCoordinator?.pinchPanscan = 0
|
|
}
|
|
withAnimation(.easeInOut(duration: 0.3)) {
|
|
isPortraitPanelVisible.toggle()
|
|
}
|
|
navigationCoordinator?.isPortraitPanelVisible = isPortraitPanelVisible
|
|
appEnvironment?.settingsManager.portraitDetailsPanelLastVisible = isPortraitPanelVisible
|
|
Task { @MainActor in
|
|
try? await Task.sleep(for: .milliseconds(350))
|
|
hideControlsDuringTransition = false
|
|
}
|
|
}
|
|
|
|
// MARK: - Rotation Monitoring
|
|
|
|
/// Simplified rotation monitoring - handles layout transitions via accelerometer.
|
|
func setupRotationMonitoring() {
|
|
guard let appEnvironment else { return }
|
|
|
|
MPVLogging.log("setupRotationMonitoring: starting")
|
|
let rotationManager = DeviceRotationManager.shared
|
|
|
|
// Set up callback for landscape detection (request landscape rotation)
|
|
rotationManager.onLandscapeDetected = { [weak appEnvironment] in
|
|
Task { @MainActor in
|
|
guard let appEnvironment else { return }
|
|
guard !appEnvironment.settingsManager.inAppOrientationLock,
|
|
OrientationManager.shared.canRotateProgrammatically else { return }
|
|
|
|
let playerState = appEnvironment.playerService.state
|
|
|
|
// Only rotate if we have a video playing
|
|
guard playerState.currentVideo != nil,
|
|
playerState.pipState != .active else { return }
|
|
|
|
// Request landscape rotation
|
|
guard let windowScene = UIApplication.shared.connectedScenes
|
|
.compactMap({ $0 as? UIWindowScene })
|
|
.first(where: { $0.activationState == .foregroundActive }) else { return }
|
|
|
|
let screenBounds = windowScene.screen.bounds
|
|
if screenBounds.height > screenBounds.width {
|
|
let targetOrientation = Self.currentLandscapeInterfaceOrientation()
|
|
MPVLogging.logTransition("onLandscapeDetected: requesting landscape",
|
|
fromSize: screenBounds.size, toSize: nil)
|
|
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: targetOrientation)) { _ in }
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set up callback for portrait detection (request portrait rotation)
|
|
rotationManager.onPortraitDetected = { [weak appEnvironment] in
|
|
Task { @MainActor in
|
|
guard let appEnvironment else { return }
|
|
guard !appEnvironment.settingsManager.inAppOrientationLock,
|
|
OrientationManager.shared.canRotateProgrammatically else { return }
|
|
|
|
// Request portrait rotation
|
|
guard let windowScene = UIApplication.shared.connectedScenes
|
|
.compactMap({ $0 as? UIWindowScene })
|
|
.first(where: { $0.activationState == .foregroundActive }) else { return }
|
|
|
|
let screenBounds = windowScene.screen.bounds
|
|
if screenBounds.width > screenBounds.height {
|
|
MPVLogging.logTransition("onPortraitDetected: requesting portrait",
|
|
fromSize: screenBounds.size, toSize: nil)
|
|
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait)) { _ in }
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set up callback for landscape-to-landscape rotation (rotate between left and right)
|
|
rotationManager.onLandscapeOrientationChanged = { [weak appEnvironment] newOrientation in
|
|
Task { @MainActor in
|
|
guard let appEnvironment else { return }
|
|
guard !appEnvironment.settingsManager.inAppOrientationLock,
|
|
OrientationManager.shared.canRotateProgrammatically else { return }
|
|
|
|
guard let windowScene = UIApplication.shared.connectedScenes
|
|
.compactMap({ $0 as? UIWindowScene })
|
|
.first(where: { $0.activationState == .foregroundActive }) else { return }
|
|
|
|
// Device and interface orientations are inverted
|
|
let targetOrientation: UIInterfaceOrientationMask = switch newOrientation {
|
|
case .landscapeLeft:
|
|
.landscapeRight
|
|
case .landscapeRight:
|
|
.landscapeLeft
|
|
default:
|
|
.landscape
|
|
}
|
|
|
|
MPVLogging.logTransition("onLandscapeOrientationChanged: \(newOrientation.rawValue)")
|
|
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: targetOrientation)) { _ in }
|
|
}
|
|
}
|
|
|
|
// Always start monitoring
|
|
rotationManager.startMonitoring()
|
|
}
|
|
|
|
/// Get the interface orientation mask matching the device's current landscape orientation.
|
|
static func currentLandscapeInterfaceOrientation() -> UIInterfaceOrientationMask {
|
|
let deviceOrientation = DeviceRotationManager.shared.detectedOrientation
|
|
// Device and interface orientations are inverted
|
|
switch deviceOrientation {
|
|
case .landscapeLeft:
|
|
return .landscapeRight
|
|
case .landscapeRight:
|
|
return .landscapeLeft
|
|
default:
|
|
return .landscape // Fallback to any landscape
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|