Fix player brightness controls on iPhone Duo

Brightness controls used UIScreen.main, which on iPhone Duo can be a
different display than the one hosting the player. Route all brightness
reads and writes through PlayerBrightness, which targets the screen of
the window hosting the controls.
This commit is contained in:
Arkadiusz Fal
2026-10-22 23:16:11 +02:00
parent 52774fc7cc
commit fb4da41877
4 changed files with 52 additions and 9 deletions

View File

@@ -460,7 +460,7 @@ struct ControlsSectionRenderer: View {
/// Brightness icon image using variable value SF Symbol. /// Brightness icon image using variable value SF Symbol.
private var brightnessImage: Image { 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. /// Panel toggle button content with glass background support.
@@ -796,9 +796,9 @@ struct ControlsSectionRenderer: View {
if effectiveBehavior == .alwaysVisible || isBrightnessExpanded { if effectiveBehavior == .alwaysVisible || isBrightnessExpanded {
Slider( Slider(
value: Binding( value: Binding(
get: { UIScreen.main.brightness }, get: { PlayerBrightness.value(for: actions.screen) },
set: { newValue in set: { newValue in
UIScreen.main.brightness = newValue PlayerBrightness.set(newValue, for: actions.screen)
} }
), ),
in: 0...1, in: 0...1,

View File

@@ -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

View File

@@ -57,6 +57,10 @@ struct PlayerControlsActions {
/// behavior (details toggle, no rotate glyph, no orientation lock). /// behavior (details toggle, no rotate glyph, no orientation lock).
var canRotate: Bool = true 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. /// iPad, or an iPhone windowing mode that can't rotate on request.
var usesRotationFreeFullscreen: Bool { isIPad || !canRotate } var usesRotationFreeFullscreen: Bool { isIPad || !canRotate }

View File

@@ -137,8 +137,8 @@ struct PlayerControlsView: View {
@State private var targetSeekProgress: Double = 0 @State private var targetSeekProgress: Double = 0
@State private var hideTimer: Timer? @State private var hideTimer: Timer?
@State private var showControls: Bool? @State private var showControls: Bool?
/// Tracks brightness value for side slider (needed because UIScreen.main.brightness isn't observable) /// Tracks brightness value for side slider (needed because UIScreen.brightness isn't observable)
@State private var currentBrightness: Double = UIScreen.main.brightness @State private var currentBrightness: Double = PlayerBrightness.value(for: nil)
/// Track safe area to force re-render when it changes /// Track safe area to force re-render when it changes
@State private var currentSafeArea: UIEdgeInsets = .zero @State private var currentSafeArea: UIEdgeInsets = .zero
/// Window hosting these controls, captured by `HostWindowReader`. Safe /// 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 /// display's window (iPhone Duo exterior with its 84pt status bar inset
/// while the player sits on the interior in Split View). /// while the player sits on the interior in Split View).
@State private var hostWindow: UIWindow? @State private var hostWindow: UIWindow?
private var hostScreen: UIScreen? { hostWindow?.windowScene?.screen }
@Environment(\.statusBarVisibilityController) private var statusBarController @Environment(\.statusBarVisibilityController) private var statusBarController
/// Active player controls layout from preset (passed from parent to avoid flashing during view recreation) /// Active player controls layout from preset (passed from parent to avoid flashing during view recreation)
var activeLayout: PlayerControlsLayout = .default var activeLayout: PlayerControlsLayout = .default
@@ -272,6 +273,7 @@ struct PlayerControlsView: View {
guard window !== hostWindow else { return } guard window !== hostWindow else { return }
hostWindow = window hostWindow = window
currentSafeArea = windowSafeArea currentSafeArea = windowSafeArea
currentBrightness = PlayerBrightness.value(for: hostScreen)
}) })
.onAppear { .onAppear {
// Update safe area on appear to ensure correct layout // Update safe area on appear to ensure correct layout
@@ -300,7 +302,7 @@ struct PlayerControlsView: View {
appEnvironment?.navigationCoordinator.shouldSnapPanscan = newValue appEnvironment?.navigationCoordinator.shouldSnapPanscan = newValue
} }
.onReceive(NotificationCenter.default.publisher(for: UIScreen.brightnessDidChangeNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: UIScreen.brightnessDidChangeNotification)) { _ in
currentBrightness = UIScreen.main.brightness currentBrightness = PlayerBrightness.value(for: hostScreen)
} }
.sheet(isPresented: $showingCaptionsSheet) { .sheet(isPresented: $showingCaptionsSheet) {
captionsSheet captionsSheet
@@ -429,6 +431,7 @@ struct PlayerControlsView: View {
panelSide: panelSide, panelSide: panelSide,
isIPad: isIPad, isIPad: isIPad,
canRotate: OrientationManager.shared.canRotateProgrammatically, canRotate: OrientationManager.shared.canRotateProgrammatically,
screen: hostWindow?.windowScene?.screen,
showVolumeControls: showVolumeControls, showVolumeControls: showVolumeControls,
showDebugButton: onToggleDebug != nil, showDebugButton: onToggleDebug != nil,
showCloseButton: onClose != 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. /// Always goes to the next higher preset from current position, wrapping from 100 to 0.
private func cycleBrightnessPreset() { private func cycleBrightnessPreset() {
let presets: [Double] = [0.0, 0.25, 0.5, 0.75, 1.0] 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) // Find next higher preset (with small tolerance for floating point)
let newValue: Double let newValue: Double
@@ -1596,7 +1599,7 @@ struct PlayerControlsView: View {
newValue = 0.0 newValue = 0.0
} }
UIScreen.main.brightness = newValue PlayerBrightness.set(newValue, for: hostScreen)
currentBrightness = newValue currentBrightness = newValue
} }
@@ -1632,7 +1635,7 @@ struct PlayerControlsView: View {
value: Binding( value: Binding(
get: { currentBrightness }, get: { currentBrightness },
set: { newValue in set: { newValue in
UIScreen.main.brightness = newValue PlayerBrightness.set(newValue, for: hostScreen)
currentBrightness = newValue currentBrightness = newValue
} }
), ),