mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 21:22:53 +00:00
The interior display refuses programmatic rotation (UISceneErrorDomain 101: "The current windowing mode does not allow for programmatic changes to interface orientation"), so the rotate/fullscreen button did nothing there and the failure was swallowed. - OrientationManager tracks `canRotateProgrammatically` (from the hinge state, cleared when a geometry request is refused) and is @Observable. - The fullscreen button takes the iPad path when rotation is unavailable: details toggle in portrait, floating panel toggle in landscape, icon follows the panel state; orientation lock is hidden. The refused request itself falls back to the details toggle. Accelerometer-driven rotation requests are gated the same way. - Corner layout gate is size-only on iOS (≥900×600 including safe areas, so the status bar toggling with the layout can't oscillate it); the layout-mode setting is exposed on foldables via a hinge presence check. - Corner layout on iPhone always hides the status bar (the interior's landscape bar is a ~57pt column) and pads by the live top inset only, instead of the transient 82pt inset seen while unfolding. - The player hosting controller nudges its safe area after size transitions so fold/unfold doesn't leave SwiftUI with stale insets.
83 lines
2.8 KiB
Swift
83 lines
2.8 KiB
Swift
//
|
|
// HingeLayoutObserver.swift
|
|
// Yattee
|
|
//
|
|
// Observes the foldable device hinge (iPhone Duo) and reports whether the
|
|
// player should use the "tabletop" layout: hinge partially open, device
|
|
// resting like a laptop, video on the upper half and controls on the lower.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
#if os(iOS)
|
|
|
|
struct HingeLayoutObserver: ViewModifier {
|
|
@Binding var isTabletop: Bool
|
|
|
|
/// 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"
|
|
|
|
func body(content: Content) -> some View {
|
|
if Self.forceTabletop {
|
|
content.onAppear { isTabletop = true }
|
|
} else if #available(iOS 27.1, *) {
|
|
content.onHingeChange { _, context in
|
|
// `hinge` is nil on devices without a hinge and when the view
|
|
// leaves a hierarchy that provides hinge updates.
|
|
if let status = context.hinge?.status {
|
|
// Interior display (hinge open): the system refuses
|
|
// programmatic rotation, so the player must not offer it.
|
|
OrientationManager.shared.canRotateProgrammatically = status == .closed
|
|
}
|
|
let tabletop = context.hinge?.status == .partiallyOpen
|
|
guard tabletop != isTabletop else { return }
|
|
withAnimation(.easeInOut(duration: 0.3)) {
|
|
isTabletop = tabletop
|
|
}
|
|
}
|
|
} else {
|
|
content
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Reports whether the device has a hinge at all (foldable iPhone), for UI
|
|
/// that only applies to the interior display, such as the corner layout option.
|
|
private struct HingePresenceObserver: ViewModifier {
|
|
@Binding var hasHinge: Bool
|
|
|
|
func body(content: Content) -> some View {
|
|
if #available(iOS 27.1, *) {
|
|
content.onHingeChange { _, context in
|
|
// nil also when leaving a hinge-providing hierarchy; keep the last value then
|
|
guard context.hinge != nil else { return }
|
|
hasHinge = true
|
|
}
|
|
} else {
|
|
content
|
|
}
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
/// Tracks the device hinge and writes whether the tabletop player layout
|
|
/// should be active. No-op before iOS 27.1 and on devices without a hinge.
|
|
func observeHingeLayout(isTabletop: Binding<Bool>) -> some View {
|
|
modifier(HingeLayoutObserver(isTabletop: isTabletop))
|
|
}
|
|
|
|
/// Writes whether the device has a hinge. No-op before iOS 27.1.
|
|
func observeHinge(hasHinge: Binding<Bool>) -> some View {
|
|
modifier(HingePresenceObserver(hasHinge: hasHinge))
|
|
}
|
|
}
|
|
|
|
#else
|
|
|
|
extension View {
|
|
func observeHinge(hasHinge: Binding<Bool>) -> some View { self }
|
|
}
|
|
|
|
#endif
|