From 4ccbe153a74103ec18253c1d1877193fd99b275e Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Fri, 26 Jun 2026 23:30:30 +0200 Subject: [PATCH] Show macOS player controls immediately while video is loading The controls overlay was gated on the player backend existing, but the backend is created only after the video details fetch completes, so the loading phase showed just the thumbnail and spinner with no controls. Drop the backend gate (MPV is the only backend type) and resolve the PiP backend dynamically in the toggle callback. Also make ended/failed states hide controls directly in shouldShowControls instead of relying on catching the state transition, and reset the manual visibility override when a new load starts so controls reappear for the next video. --- .../Player/ExpandedPlayerSheet+Layouts.swift | 28 +++++++++---------- .../macOS/MacOSPlayerControlsView.swift | 24 ++++++++-------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift b/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift index 0fdef1a5..cf8c48cf 100644 --- a/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift +++ b/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift @@ -796,7 +796,7 @@ extension ExpandedPlayerSheet { closeVideo() }, onTogglePiP: { - if let mpvBackend = backend as? MPVBackend { + if let mpvBackend = playerService.currentBackend as? MPVBackend { mpvBackend.togglePiP() } }, @@ -873,9 +873,9 @@ extension ExpandedPlayerSheet { .offset(y: controlsVerticalOffset) } #elseif os(macOS) - if let backend = playerService?.currentBackend, - backend.backendType == .mpv, - let playerState, + // No backend gate: the backend is created only after the video + // details fetch, and controls should be visible while loading. + if let playerState, let playerService, playerState.pipState != .active && !playerState.showDebugOverlay { MacOSPlayerControlsView( @@ -897,7 +897,7 @@ extension ExpandedPlayerSheet { closeVideo() }, onTogglePiP: { - if let mpvBackend = backend as? MPVBackend { + if let mpvBackend = playerService.currentBackend as? MPVBackend { mpvBackend.togglePiP() } }, @@ -1142,9 +1142,9 @@ extension ExpandedPlayerSheet { .frame(width: controlsWidth, height: controlsHeight) } #elseif os(macOS) - if let backend = playerService?.currentBackend, - backend.backendType == .mpv, - let playerState, + // No backend gate: the backend is created only after the video + // details fetch, and controls should be visible while loading. + if let playerState, let playerService, playerState.pipState != .active && !playerState.showDebugOverlay { MacOSPlayerControlsView( @@ -1166,7 +1166,7 @@ extension ExpandedPlayerSheet { closeVideo() }, onTogglePiP: { - if let mpvBackend = backend as? MPVBackend { + if let mpvBackend = playerService.currentBackend as? MPVBackend { mpvBackend.togglePiP() } }, @@ -1447,7 +1447,7 @@ extension ExpandedPlayerSheet { closeVideo() }, onTogglePiP: { - if let mpvBackend = backend as? MPVBackend { + if let mpvBackend = playerService.currentBackend as? MPVBackend { mpvBackend.togglePiP() } }, @@ -1551,9 +1551,9 @@ extension ExpandedPlayerSheet { } } #elseif os(macOS) - if let backend = playerService?.currentBackend, - backend.backendType == .mpv, - let playerState, + // No backend gate: the backend is created only after the video + // details fetch, and controls should be visible while loading. + if let playerState, let playerService, playerState.pipState != .active && !playerState.showDebugOverlay { MacOSPlayerControlsView( @@ -1585,7 +1585,7 @@ extension ExpandedPlayerSheet { closeVideo() }, onTogglePiP: { - if let mpvBackend = backend as? MPVBackend { + if let mpvBackend = playerService.currentBackend as? MPVBackend { mpvBackend.togglePiP() } }, diff --git a/Yattee/Views/Player/macOS/MacOSPlayerControlsView.swift b/Yattee/Views/Player/macOS/MacOSPlayerControlsView.swift index 97104d55..76e2aadb 100644 --- a/Yattee/Views/Player/macOS/MacOSPlayerControlsView.swift +++ b/Yattee/Views/Player/macOS/MacOSPlayerControlsView.swift @@ -89,6 +89,11 @@ struct MacOSPlayerControlsView: View { /// Controls visibility - show when hovering, interacting, or paused private var shouldShowControls: Bool { + // Ended/failed overlays take over the surface; controls stay hidden + // regardless of hover or manual toggles. + if playerState.playbackState == .ended || playerState.isFailed { + return false + } if let showControls { return showControls } @@ -96,11 +101,9 @@ struct MacOSPlayerControlsView: View { if isHovering || isInteracting { return true } - // Show when paused, loading, or failed + // Show when paused or loading return playerState.playbackState == .paused || - playerState.playbackState == .loading || - playerState.playbackState == .ended || - playerState.isFailed + playerState.playbackState == .loading } /// Yattee Server URL used by `ChannelAvatarView` for avatar fallback. @@ -295,14 +298,13 @@ struct MacOSPlayerControlsView: View { if newState == .playing && shouldShowControls { startHideTimer() } - // Hide controls when video ends (ended overlay takes over) - if newState == .ended { - showControls = false - cancelHideTimer() + // A new load resets any manual show/hide override (e.g. hidden + // after ended/failed) so controls are visible while loading. + if newState == .loading { + showControls = nil } - // Hide controls when video fails - if case .failed = newState { - showControls = false + // Ended/failed hide via shouldShowControls; just stop the timer. + if newState == .ended || playerState.isFailed { cancelHideTimer() } }