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.
This commit is contained in:
Arkadiusz Fal
2025-11-14 18:58:28 +01:00
parent b8cde410c5
commit 8f97c40257
2 changed files with 182 additions and 172 deletions

View File

@@ -57,23 +57,47 @@ struct PlayerControls: View {
} }
var body: some View { var body: some View {
ZStack(alignment: .topLeading) { Group {
if showControls { if showControls {
Seek() controlsContent
.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
} }
}
.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 { VStack {
ZStack { ZStack {
@@ -87,108 +111,106 @@ struct PlayerControls: View {
} }
.offset(y: playerControlsLayout.osdVerticalOffset + 5) .offset(y: playerControlsLayout.osdVerticalOffset + 5)
if showControls { Section {
Section { #if !os(tvOS)
#if !os(tvOS) HStack {
HStack { seekBackwardButton
seekBackwardButton Spacer()
Spacer() togglePlayButton
togglePlayButton Spacer()
Spacer() seekForwardButton
seekForwardButton }
} .font(.system(size: playerControlsLayout.bigButtonFontSize))
.font(.system(size: playerControlsLayout.bigButtonFontSize)) #endif
#endif
ZStack(alignment: .bottom) { ZStack(alignment: .bottom) {
VStack(spacing: 4) { VStack(spacing: 4) {
#if !os(tvOS) #if !os(tvOS)
buttonsBar buttonsBar
HStack { HStack {
if !player.currentVideo.isNil, player.playingFullScreen { if !player.currentVideo.isNil, player.playingFullScreen {
Button { Button {
withAnimation(Self.animation) { withAnimation(Self.animation) {
model.presentingDetailsOverlay = true 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)
} }
.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()
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)
} }
#endif
timeline Spacer()
.padding(.bottom, 2)
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) { timeline
#if os(tvOS) .padding(.bottom, 2)
togglePlayButton }
seekBackwardButton .zIndex(1)
seekForwardButton .padding(.top, 2)
#endif .transition(.opacity)
if playerControlsRestartEnabled {
restartVideoButton HStack(spacing: playerControlsLayout.buttonsSpacing) {
}
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)
#if os(tvOS) #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 #else
.offset(y: -playerControlsLayout.timelineHeight - 5) if playerControlsMusicModeEnabled {
musicModeButton
}
#endif #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) .frame(maxWidth: .infinity)
@@ -215,24 +237,6 @@ struct PlayerControls: View {
.frame(maxHeight: .infinity, alignment: .top) .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 { var detailsWidth: Double {

View File

@@ -77,6 +77,10 @@ struct VideoPlayerView: View {
@ObservedObject var controlsOverlayModel = ControlOverlaysModel.shared // swiftlint:disable:this swiftui_state_private @ObservedObject var controlsOverlayModel = ControlOverlaysModel.shared // swiftlint:disable:this swiftui_state_private
var shouldShowCustomControls: Bool {
player.activeBackend == .mpv || !avPlayerUsesSystemControls || player.musicMode
}
var body: some View { var body: some View {
ZStack(alignment: overlayAlignment) { ZStack(alignment: overlayAlignment) {
videoPlayer videoPlayer
@@ -245,7 +249,7 @@ struct VideoPlayerView: View {
var content: some View { var content: some View {
Group { Group {
ZStack(alignment: .bottomLeading) { ZStack(alignment: .topLeading) {
#if os(tvOS) #if os(tvOS)
ZStack { ZStack {
player.playerBackendView player.playerBackendView
@@ -257,64 +261,66 @@ struct VideoPlayerView: View {
.ignoresSafeArea() .ignoresSafeArea()
#else #else
GeometryReader { geometry in GeometryReader { geometry in
player.playerBackendView VStack(spacing: 0) {
.modifier( player.playerBackendView
VideoPlayerSizeModifier( .modifier(
geometry: geometry, VideoPlayerSizeModifier(
aspectRatio: player.aspectRatio, geometry: geometry,
fullScreen: fullScreenPlayer, aspectRatio: player.aspectRatio,
detailsHiddenInFullScreen: detailsHiddenInFullScreen fullScreen: fullScreenPlayer,
detailsHiddenInFullScreen: detailsHiddenInFullScreen
)
) )
) .onHover { hovering in
.onHover { hovering in hoveringPlayer = hovering
hoveringPlayer = hovering // Only show/hide custom controls if they should be used
if hovering { if shouldShowCustomControls {
player.controls.show() if hovering {
} else { player.controls.show()
player.controls.hide() } 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()
} }
} }
return $0
} }
} .gesture(player.controls.presentingOverlays ? nil : playerDragGesture)
#endif
.background(Color.black)
if !detailsHiddenInFullScreen {
VideoDetails(
video: player.videoForDisplay,
fullScreen: $fullScreenDetails,
sidebarQueue: $sidebarQueue
)
.modifier(VideoDetailsPaddingModifier(
playerSize: player.playerSize,
fullScreen: fullScreenDetails
))
#if os(macOS) #if os(macOS)
// TODO: Check whether this is needed on macOS. .onAppear {
.onDisappear { NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
if player.presentingPlayer { hoverThrottle.execute {
player.setNeedsDrawing(true) if !player.currentItem.isNil, hoveringPlayer, shouldShowCustomControls {
player.controls.resetTimer()
}
}
return $0
}
} }
}
#endif #endif
.id(player.currentVideo?.cacheKey)
.transition(.opacity) .background(Color.black)
.offset(y: detailViewDragOffset)
.gesture(detailsDragGesture) if !detailsHiddenInFullScreen {
} else { VideoDetails(
VStack {} 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 #endif