diff --git a/Yattee/Services/OrientationManager.swift b/Yattee/Services/OrientationManager.swift index 25743d90..da34e5aa 100644 --- a/Yattee/Services/OrientationManager.swift +++ b/Yattee/Services/OrientationManager.swift @@ -26,7 +26,17 @@ final class OrientationManager { /// the same way it does for iPad multitasking. Set from the hinge state and /// cleared when a request is refused, so the player falls back to the /// iPad-style details toggle instead of doing nothing. - var canRotateProgrammatically = true + /// While false, the in-app orientation lock is suspended as well: the + /// system decides the orientation there, so pinning `supportedOrientations` + /// only fights the user's physical rotation (Duo hinge open, simulator + /// forced tabletop). + var canRotateProgrammatically = true { + didSet { + if !canRotateProgrammatically && isLocked { + unlock() + } + } + } /// `UISceneErrorDomain` code returned when the windowing mode forbids /// programmatic interface orientation changes. @@ -52,7 +62,7 @@ final class OrientationManager { /// Lock orientation to the current orientation func lock() { - guard !isLocked else { return } + guard !isLocked, canRotateProgrammatically else { return } lockedOrientation = currentInterfaceOrientationMask isLocked = true notifyOrientationChange() @@ -60,6 +70,7 @@ final class OrientationManager { /// Lock to a specific orientation func lock(to orientation: UIInterfaceOrientationMask) { + guard canRotateProgrammatically else { return } lockedOrientation = orientation isLocked = true notifyOrientationChange() @@ -67,6 +78,7 @@ final class OrientationManager { /// Re-lock to the current orientation (updates locked orientation even if already locked) func lockToCurrentOrientation() { + guard canRotateProgrammatically else { return } lockedOrientation = currentInterfaceOrientationMask isLocked = true notifyOrientationChange() diff --git a/Yattee/Views/Player/HingeLayoutObserver.swift b/Yattee/Views/Player/HingeLayoutObserver.swift index 8908851d..4bd50cbe 100644 --- a/Yattee/Views/Player/HingeLayoutObserver.swift +++ b/Yattee/Views/Player/HingeLayoutObserver.swift @@ -16,11 +16,24 @@ struct HingeLayoutObserver: ViewModifier { /// Debug override: launch with `YATTEE_FORCE_TABLETOP=1` to force the /// tabletop layout on simulators without a hinge control. - private static let forceTabletop = ProcessInfo.processInfo.environment["YATTEE_FORCE_TABLETOP"] == "1" + static let forceTabletop = ProcessInfo.processInfo.environment["YATTEE_FORCE_TABLETOP"] == "1" func body(content: Content) -> some View { if Self.forceTabletop { - content.onAppear { isTabletop = true } + // The override stands in for a half-open hinge, which only exists + // on the interior display: folded (exterior, 466×678) keeps the + // regular layout, like the real `.closed` state would. + content.onGeometryChange(for: Bool.self) { proxy in + min(proxy.size.width, proxy.size.height) >= ExpandedPlayerSheet.minHeightForCornerLayout + } action: { isInterior in + // Mirror the real hinge state: no programmatic rotation (and no + // orientation lock) while the hinge is open. + OrientationManager.shared.canRotateProgrammatically = !isInterior + guard isInterior != isTabletop else { return } + withAnimation(.easeInOut(duration: 0.3)) { + isTabletop = isInterior + } + } } else if #available(iOS 27.1, *) { content.onHingeChange { _, context in // `hinge` is nil on devices without a hinge and when the view