From 8f97c402573a7dd12ea5b84c8ce6191c8ca72932 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Fri, 14 Nov 2025 18:58:28 +0100 Subject: [PATCH] Refactor player controls and improve custom controls visibility Restructured PlayerControls view hierarchy by extracting controls content into a separate computed property for better code organization. Added shouldShowCustomControls property to VideoPlayerView to properly determine when custom controls should be shown vs system controls. Updated hover logic to only show/hide custom controls when appropriate. --- Shared/Player/Controls/PlayerControls.swift | 244 ++++++++++---------- Shared/Player/VideoPlayerView.swift | 110 ++++----- 2 files changed, 182 insertions(+), 172 deletions(-) diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index c3754a2b..62d6e1a1 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -57,23 +57,47 @@ struct PlayerControls: View { } var body: some View { - ZStack(alignment: .topLeading) { + Group { if showControls { - Seek() - .zIndex(4) - .transition(.opacity) - .frame(maxWidth: .infinity, alignment: .topLeading) - #if os(tvOS) - .focused($focusedField, equals: .seekOSD) - .onChange(of: player.seek.lastSeekTime) { _ in - if !model.presentingControls { - focusedField = .seekOSD - } - } - #else - .offset(y: 2) - #endif + controlsContent } + } + .onChange(of: model.presentingOverlays) { newValue in + if newValue { + model.hide() + } + } + #if os(tvOS) + .onReceive(model.reporter) { value in + guard player.presentingPlayer else { return } + if value == "swipe down", !model.presentingControls, !model.presentingOverlays { + withAnimation(Self.animation) { + controlsOverlayModel.hide() + } + } else { + model.show() + } + model.resetTimer() + } + #endif + } + + var controlsContent: some View { + ZStack(alignment: .topLeading) { + Seek() + .zIndex(4) + .transition(.opacity) + .frame(maxWidth: .infinity, alignment: .topLeading) + #if os(tvOS) + .focused($focusedField, equals: .seekOSD) + .onChange(of: player.seek.lastSeekTime) { _ in + if !model.presentingControls { + focusedField = .seekOSD + } + } + #else + .offset(y: 2) + #endif VStack { ZStack { @@ -87,108 +111,106 @@ struct PlayerControls: View { } .offset(y: playerControlsLayout.osdVerticalOffset + 5) - if showControls { - Section { - #if !os(tvOS) - HStack { - seekBackwardButton - Spacer() - togglePlayButton - Spacer() - seekForwardButton - } - .font(.system(size: playerControlsLayout.bigButtonFontSize)) - #endif + Section { + #if !os(tvOS) + HStack { + seekBackwardButton + Spacer() + togglePlayButton + Spacer() + seekForwardButton + } + .font(.system(size: playerControlsLayout.bigButtonFontSize)) + #endif - ZStack(alignment: .bottom) { - VStack(spacing: 4) { - #if !os(tvOS) - buttonsBar + ZStack(alignment: .bottom) { + VStack(spacing: 4) { + #if !os(tvOS) + buttonsBar - HStack { - if !player.currentVideo.isNil, player.playingFullScreen { - Button { - withAnimation(Self.animation) { - model.presentingDetailsOverlay = true - } - } label: { - ControlsBar(fullScreen: $model.presentingDetailsOverlay, expansionState: .constant(.full), presentingControls: false, detailsTogglePlayer: false, detailsToggleFullScreen: false) - .clipShape(RoundedRectangle(cornerRadius: 4)) - .frame(maxWidth: 300, alignment: .leading) + HStack { + if !player.currentVideo.isNil, player.playingFullScreen { + Button { + withAnimation(Self.animation) { + model.presentingDetailsOverlay = true } - .buttonStyle(.plain) + } label: { + ControlsBar(fullScreen: $model.presentingDetailsOverlay, expansionState: .constant(.full), presentingControls: false, detailsTogglePlayer: false, detailsToggleFullScreen: false) + .clipShape(RoundedRectangle(cornerRadius: 4)) + .frame(maxWidth: 300, alignment: .leading) } - Spacer() + .buttonStyle(.plain) } - #endif - - Spacer() - - if playerControlsLayout.displaysTitleLine { - VStack(alignment: .leading) { - Text(player.videoForDisplay?.displayTitle ?? "Not Playing") - .shadow(radius: 10) - .font(.system(size: playerControlsLayout.titleLineFontSize).bold()) - .lineLimit(1) - - Text(player.currentVideo?.displayAuthor ?? "") - .fontWeight(.semibold) - .shadow(radius: 10) - .foregroundColor(.init(white: 0.8)) - .font(.system(size: playerControlsLayout.authorLineFontSize)) - .lineLimit(1) - } - .foregroundColor(.white) - .frame(maxWidth: .infinity, alignment: .leading) - .offset(y: -40) + Spacer() } + #endif - timeline - .padding(.bottom, 2) + Spacer() + + if playerControlsLayout.displaysTitleLine { + VStack(alignment: .leading) { + Text(player.videoForDisplay?.displayTitle ?? "Not Playing") + .shadow(radius: 10) + .font(.system(size: playerControlsLayout.titleLineFontSize).bold()) + .lineLimit(1) + + Text(player.currentVideo?.displayAuthor ?? "") + .fontWeight(.semibold) + .shadow(radius: 10) + .foregroundColor(.init(white: 0.8)) + .font(.system(size: playerControlsLayout.authorLineFontSize)) + .lineLimit(1) + } + .foregroundColor(.white) + .frame(maxWidth: .infinity, alignment: .leading) + .offset(y: -40) } - .zIndex(1) - .padding(.top, 2) - .transition(.opacity) - HStack(spacing: playerControlsLayout.buttonsSpacing) { - #if os(tvOS) - togglePlayButton - seekBackwardButton - seekForwardButton - #endif - if playerControlsRestartEnabled { - restartVideoButton - } - if playerControlsAdvanceToNextEnabled { - advanceToNextItemButton - } - Spacer() - #if os(tvOS) - if playerControlsSettingsEnabled { - settingsButton - } - #endif - if playerControlsPlaybackModeEnabled { - playbackModeButton - } - #if os(tvOS) - closeVideoButton - #else - if playerControlsMusicModeEnabled { - musicModeButton - } - #endif - } - .zIndex(0) + timeline + .padding(.bottom, 2) + } + .zIndex(1) + .padding(.top, 2) + .transition(.opacity) + + HStack(spacing: playerControlsLayout.buttonsSpacing) { #if os(tvOS) - .offset(y: -playerControlsLayout.timelineHeight - 30) + togglePlayButton + seekBackwardButton + seekForwardButton + #endif + if playerControlsRestartEnabled { + restartVideoButton + } + if playerControlsAdvanceToNextEnabled { + advanceToNextItemButton + } + Spacer() + #if os(tvOS) + if playerControlsSettingsEnabled { + settingsButton + } + #endif + if playerControlsPlaybackModeEnabled { + playbackModeButton + } + #if os(tvOS) + closeVideoButton #else - .offset(y: -playerControlsLayout.timelineHeight - 5) + if playerControlsMusicModeEnabled { + musicModeButton + } #endif } + .zIndex(0) + #if os(tvOS) + .offset(y: -playerControlsLayout.timelineHeight - 30) + #else + .offset(y: -playerControlsLayout.timelineHeight - 5) + #endif } - .opacity(model.presentingControls && !player.availableStreams.isEmpty ? 1 : 0) } + .opacity(model.presentingControls && !player.availableStreams.isEmpty ? 1 : 0) } } .frame(maxWidth: .infinity) @@ -215,24 +237,6 @@ struct PlayerControls: View { .frame(maxHeight: .infinity, alignment: .top) } } - .onChange(of: model.presentingOverlays) { newValue in - if newValue { - model.hide() - } - } - #if os(tvOS) - .onReceive(model.reporter) { value in - guard player.presentingPlayer else { return } - if value == "swipe down", !model.presentingControls, !model.presentingOverlays { - withAnimation(Self.animation) { - controlsOverlayModel.hide() - } - } else { - model.show() - } - model.resetTimer() - } - #endif } var detailsWidth: Double { diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index e7916909..a682ee8f 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -77,6 +77,10 @@ struct VideoPlayerView: View { @ObservedObject var controlsOverlayModel = ControlOverlaysModel.shared // swiftlint:disable:this swiftui_state_private + var shouldShowCustomControls: Bool { + player.activeBackend == .mpv || !avPlayerUsesSystemControls || player.musicMode + } + var body: some View { ZStack(alignment: overlayAlignment) { videoPlayer @@ -245,7 +249,7 @@ struct VideoPlayerView: View { var content: some View { Group { - ZStack(alignment: .bottomLeading) { + ZStack(alignment: .topLeading) { #if os(tvOS) ZStack { player.playerBackendView @@ -257,64 +261,66 @@ struct VideoPlayerView: View { .ignoresSafeArea() #else GeometryReader { geometry in - player.playerBackendView - .modifier( - VideoPlayerSizeModifier( - geometry: geometry, - aspectRatio: player.aspectRatio, - fullScreen: fullScreenPlayer, - detailsHiddenInFullScreen: detailsHiddenInFullScreen + VStack(spacing: 0) { + player.playerBackendView + .modifier( + VideoPlayerSizeModifier( + geometry: geometry, + aspectRatio: player.aspectRatio, + fullScreen: fullScreenPlayer, + detailsHiddenInFullScreen: detailsHiddenInFullScreen + ) ) - ) - .onHover { hovering in - hoveringPlayer = hovering - if hovering { - player.controls.show() - } else { - player.controls.hide() - } - } - .gesture(player.controls.presentingOverlays ? nil : playerDragGesture) - #if os(macOS) - .onAppear { - NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) { - hoverThrottle.execute { - if !player.currentItem.isNil, hoveringPlayer { - player.controls.resetTimer() + .onHover { hovering in + hoveringPlayer = hovering + // Only show/hide custom controls if they should be used + if shouldShowCustomControls { + if hovering { + player.controls.show() + } else { + player.controls.hide() } } - - return $0 } - } - #endif - - .background(Color.black) - - if !detailsHiddenInFullScreen { - VideoDetails( - video: player.videoForDisplay, - fullScreen: $fullScreenDetails, - sidebarQueue: $sidebarQueue - ) - .modifier(VideoDetailsPaddingModifier( - playerSize: player.playerSize, - fullScreen: fullScreenDetails - )) + .gesture(player.controls.presentingOverlays ? nil : playerDragGesture) #if os(macOS) - // TODO: Check whether this is needed on macOS. - .onDisappear { - if player.presentingPlayer { - player.setNeedsDrawing(true) + .onAppear { + NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) { + hoverThrottle.execute { + if !player.currentItem.isNil, hoveringPlayer, shouldShowCustomControls { + player.controls.resetTimer() + } + } + + return $0 + } } - } #endif - .id(player.currentVideo?.cacheKey) - .transition(.opacity) - .offset(y: detailViewDragOffset) - .gesture(detailsDragGesture) - } else { - VStack {} + + .background(Color.black) + + if !detailsHiddenInFullScreen { + VideoDetails( + video: player.videoForDisplay, + fullScreen: $fullScreenDetails, + sidebarQueue: $sidebarQueue + ) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + #if os(macOS) + // TODO: Check whether this is needed on macOS. + .onDisappear { + if player.presentingPlayer { + player.setNeedsDrawing(true) + } + } + #endif + .id(player.currentVideo?.cacheKey) + .transition(.opacity) + .offset(y: detailViewDragOffset) + .gesture(detailsDragGesture) + } else { + VStack {} + } } } #endif