mirror of
https://github.com/yattee/yattee.git
synced 2025-12-07 08:38:14 +00:00
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:
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user