diff --git a/Yattee/Views/Player/ControlsSectionRenderer.swift b/Yattee/Views/Player/ControlsSectionRenderer.swift index 0c7ba687..c9569448 100644 --- a/Yattee/Views/Player/ControlsSectionRenderer.swift +++ b/Yattee/Views/Player/ControlsSectionRenderer.swift @@ -460,7 +460,7 @@ struct ControlsSectionRenderer: View { /// Brightness icon image using variable value SF Symbol. private var brightnessImage: Image { - Image(systemName: "sun.max.fill", variableValue: UIScreen.main.brightness) + Image(systemName: "sun.max.fill", variableValue: PlayerBrightness.value(for: actions.screen)) } /// Panel toggle button content with glass background support. @@ -796,9 +796,9 @@ struct ControlsSectionRenderer: View { if effectiveBehavior == .alwaysVisible || isBrightnessExpanded { Slider( value: Binding( - get: { UIScreen.main.brightness }, + get: { PlayerBrightness.value(for: actions.screen) }, set: { newValue in - UIScreen.main.brightness = newValue + PlayerBrightness.set(newValue, for: actions.screen) } ), in: 0...1, diff --git a/Yattee/Views/Player/PlayerBrightness.swift b/Yattee/Views/Player/PlayerBrightness.swift new file mode 100644 index 00000000..86a5c53e --- /dev/null +++ b/Yattee/Views/Player/PlayerBrightness.swift @@ -0,0 +1,36 @@ +// +// PlayerBrightness.swift +// Yattee +// +// Screen brightness for the player's brightness controls. On iPhone Duo the +// interior and exterior are separate screens, so `UIScreen.main` can name a +// display other than the one the player is on: brightness must go to the +// screen of the window hosting the controls. +// + +#if os(iOS) + +import UIKit + +@MainActor +enum PlayerBrightness { + /// The screen to control: the host window's screen, else the foreground + /// scene's, else `UIScreen.main`. + static func screen(for hostScreen: UIScreen?) -> UIScreen { + if let hostScreen { return hostScreen } + let activeScene = UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .first { $0.activationState == .foregroundActive } + return activeScene?.screen ?? UIScreen.main + } + + static func value(for hostScreen: UIScreen?) -> Double { + screen(for: hostScreen).brightness + } + + static func set(_ value: Double, for hostScreen: UIScreen?) { + screen(for: hostScreen).brightness = value + } +} + +#endif diff --git a/Yattee/Views/Player/PlayerControlsActions.swift b/Yattee/Views/Player/PlayerControlsActions.swift index 31aea796..6df44b9d 100644 --- a/Yattee/Views/Player/PlayerControlsActions.swift +++ b/Yattee/Views/Player/PlayerControlsActions.swift @@ -57,6 +57,10 @@ struct PlayerControlsActions { /// behavior (details toggle, no rotate glyph, no orientation lock). var canRotate: Bool = true + /// Screen of the window hosting the controls; brightness targets it + /// (iPhone Duo interior and exterior are separate screens). + var screen: UIScreen? + /// iPad, or an iPhone windowing mode that can't rotate on request. var usesRotationFreeFullscreen: Bool { isIPad || !canRotate } diff --git a/Yattee/Views/Player/PlayerControlsView.swift b/Yattee/Views/Player/PlayerControlsView.swift index 9eb6d947..244c3698 100644 --- a/Yattee/Views/Player/PlayerControlsView.swift +++ b/Yattee/Views/Player/PlayerControlsView.swift @@ -137,8 +137,8 @@ struct PlayerControlsView: View { @State private var targetSeekProgress: Double = 0 @State private var hideTimer: Timer? @State private var showControls: Bool? - /// Tracks brightness value for side slider (needed because UIScreen.main.brightness isn't observable) - @State private var currentBrightness: Double = UIScreen.main.brightness + /// Tracks brightness value for side slider (needed because UIScreen.brightness isn't observable) + @State private var currentBrightness: Double = PlayerBrightness.value(for: nil) /// Track safe area to force re-render when it changes @State private var currentSafeArea: UIEdgeInsets = .zero /// Window hosting these controls, captured by `HostWindowReader`. Safe @@ -146,6 +146,7 @@ struct PlayerControlsView: View { /// display's window (iPhone Duo exterior with its 84pt status bar inset /// while the player sits on the interior in Split View). @State private var hostWindow: UIWindow? + private var hostScreen: UIScreen? { hostWindow?.windowScene?.screen } @Environment(\.statusBarVisibilityController) private var statusBarController /// Active player controls layout from preset (passed from parent to avoid flashing during view recreation) var activeLayout: PlayerControlsLayout = .default @@ -272,6 +273,7 @@ struct PlayerControlsView: View { guard window !== hostWindow else { return } hostWindow = window currentSafeArea = windowSafeArea + currentBrightness = PlayerBrightness.value(for: hostScreen) }) .onAppear { // Update safe area on appear to ensure correct layout @@ -300,7 +302,7 @@ struct PlayerControlsView: View { appEnvironment?.navigationCoordinator.shouldSnapPanscan = newValue } .onReceive(NotificationCenter.default.publisher(for: UIScreen.brightnessDidChangeNotification)) { _ in - currentBrightness = UIScreen.main.brightness + currentBrightness = PlayerBrightness.value(for: hostScreen) } .sheet(isPresented: $showingCaptionsSheet) { captionsSheet @@ -429,6 +431,7 @@ struct PlayerControlsView: View { panelSide: panelSide, isIPad: isIPad, canRotate: OrientationManager.shared.canRotateProgrammatically, + screen: hostWindow?.windowScene?.screen, showVolumeControls: showVolumeControls, showDebugButton: onToggleDebug != nil, showCloseButton: onClose != nil, @@ -1585,7 +1588,7 @@ struct PlayerControlsView: View { /// Always goes to the next higher preset from current position, wrapping from 100 to 0. private func cycleBrightnessPreset() { let presets: [Double] = [0.0, 0.25, 0.5, 0.75, 1.0] - let current = UIScreen.main.brightness + let current = PlayerBrightness.value(for: hostScreen) // Find next higher preset (with small tolerance for floating point) let newValue: Double @@ -1596,7 +1599,7 @@ struct PlayerControlsView: View { newValue = 0.0 } - UIScreen.main.brightness = newValue + PlayerBrightness.set(newValue, for: hostScreen) currentBrightness = newValue } @@ -1632,7 +1635,7 @@ struct PlayerControlsView: View { value: Binding( get: { currentBrightness }, set: { newValue in - UIScreen.main.brightness = newValue + PlayerBrightness.set(newValue, for: hostScreen) currentBrightness = newValue } ),