mirror of
https://github.com/yattee/yattee.git
synced 2025-08-05 18:24:02 +00:00
Details panels in controls
This commit is contained in:
@@ -29,7 +29,7 @@ struct PlayerControls: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
ZStack(alignment: .topLeading) {
|
||||
VStack {
|
||||
ZStack(alignment: .center) {
|
||||
OpeningStream()
|
||||
@@ -39,19 +39,20 @@ struct PlayerControls: View {
|
||||
VStack(spacing: 4) {
|
||||
buttonsBar
|
||||
|
||||
if let video = player.currentVideo, player.playingFullScreen {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(video.title)
|
||||
.font(.caption.bold())
|
||||
|
||||
Text(video.author)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
HStack {
|
||||
if !player.currentVideo.isNil, player.playingFullScreen {
|
||||
Button {
|
||||
withAnimation(Self.animation) {
|
||||
model.presentingDetailsOverlay = true
|
||||
}
|
||||
} label: {
|
||||
ControlsBar(fullScreen: $model.presentingDetailsOverlay, presentingControls: false, detailsTogglePlayer: false, detailsToggleFullScreen: false)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.frame(maxWidth: 300, alignment: .leading)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(4)
|
||||
.modifier(ControlBackgroundModifier())
|
||||
.clipShape(RoundedRectangle(cornerRadius: 2))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -69,9 +70,6 @@ struct PlayerControls: View {
|
||||
.offset(y: -25)
|
||||
.zIndex(1)
|
||||
}
|
||||
#if os(tvOS)
|
||||
.offset(y: -50)
|
||||
#endif
|
||||
.frame(maxWidth: 500)
|
||||
.padding(.bottom, 2)
|
||||
}
|
||||
@@ -79,7 +77,7 @@ struct PlayerControls: View {
|
||||
.padding(.top, 2)
|
||||
.padding(.horizontal, 2)
|
||||
}
|
||||
.opacity(model.presentingControlsOverlay ? 1 : model.presentingControls ? 1 : 0)
|
||||
.opacity(model.presentingOverlays ? 0 : model.presentingControls ? 1 : 0)
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
@@ -99,11 +97,16 @@ struct PlayerControls: View {
|
||||
ControlsOverlay()
|
||||
.frame(height: overlayHeight)
|
||||
.padding()
|
||||
.modifier(ControlBackgroundModifier(enabled: true))
|
||||
.modifier(ControlBackgroundModifier())
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.offset(x: -2, y: 40)
|
||||
.opacity(model.presentingControlsOverlay ? 1 : 0)
|
||||
|
||||
VideoDetailsOverlay()
|
||||
.frame(maxWidth: detailsWidth, maxHeight: 450)
|
||||
.modifier(ControlBackgroundModifier())
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.opacity(model.presentingDetailsOverlay ? 1 : 0)
|
||||
|
||||
Button {
|
||||
player.restoreLastSkippedSegment()
|
||||
} label: {
|
||||
@@ -124,13 +127,18 @@ struct PlayerControls: View {
|
||||
.offset(x: -2, y: -2)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.opacity(model.presentingControls ? 0 : player.lastSkipped.isNil ? 0 : 1)
|
||||
.opacity(model.presentingControls || model.presentingOverlays ? 0 : player.lastSkipped.isNil ? 0 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
var overlayHeight: Double {
|
||||
guard let player = player, player.playerSize.height.isFinite else { return 0 }
|
||||
return [0, [player.playerSize.height - 80, 140].min()!].max()!
|
||||
return [0, [player.playerSize.height - 40, 140].min()!].max()!
|
||||
}
|
||||
|
||||
var detailsWidth: Double {
|
||||
guard let player = player, player.playerSize.width.isFinite else { return 200 }
|
||||
return [player.playerSize.width, 600].min()!
|
||||
}
|
||||
|
||||
@ViewBuilder var controlsBackground: some View {
|
||||
|
25
Shared/Player/Controls/VideoDetailsOverlay.swift
Normal file
25
Shared/Player/Controls/VideoDetailsOverlay.swift
Normal file
@@ -0,0 +1,25 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct VideoDetailsOverlay: View {
|
||||
@EnvironmentObject<PlayerControlsModel> private var controls
|
||||
|
||||
var body: some View {
|
||||
VideoDetails(sidebarQueue: false, fullScreen: fullScreenBinding)
|
||||
}
|
||||
|
||||
var fullScreenBinding: Binding<Bool> {
|
||||
.init(get: {
|
||||
controls.presentingDetailsOverlay
|
||||
}, set: { newValue in
|
||||
controls.presentingDetailsOverlay = newValue
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct VideoDetailsOverlay_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VideoDetailsOverlay()
|
||||
.injectFixtureEnvironmentObjects()
|
||||
}
|
||||
}
|
@@ -9,17 +9,7 @@ struct PlayerGestures: View {
|
||||
gestureRectangle
|
||||
.tapRecognizer(
|
||||
tapSensitivity: 0.2,
|
||||
singleTapAction: {
|
||||
if model.presentingControlsOverlay {
|
||||
model.presentingControls = true
|
||||
model.resetTimer()
|
||||
withAnimation(PlayerControls.animation) {
|
||||
model.presentingControlsOverlay = false
|
||||
}
|
||||
} else {
|
||||
model.toggle()
|
||||
}
|
||||
},
|
||||
singleTapAction: { singleTapAction() },
|
||||
doubleTapAction: {
|
||||
player.backend.seek(relative: .secondsInDefaultTimescale(-10))
|
||||
},
|
||||
@@ -31,17 +21,7 @@ struct PlayerGestures: View {
|
||||
gestureRectangle
|
||||
.tapRecognizer(
|
||||
tapSensitivity: 0.2,
|
||||
singleTapAction: {
|
||||
if model.presentingControlsOverlay {
|
||||
model.presentingControls = true
|
||||
model.resetTimer()
|
||||
withAnimation(PlayerControls.animation) {
|
||||
model.presentingControlsOverlay = false
|
||||
}
|
||||
} else {
|
||||
model.toggle()
|
||||
}
|
||||
},
|
||||
singleTapAction: { singleTapAction() },
|
||||
doubleTapAction: {
|
||||
player.backend.togglePlay()
|
||||
},
|
||||
@@ -53,17 +33,7 @@ struct PlayerGestures: View {
|
||||
gestureRectangle
|
||||
.tapRecognizer(
|
||||
tapSensitivity: 0.2,
|
||||
singleTapAction: {
|
||||
if model.presentingControlsOverlay {
|
||||
model.presentingControls = true
|
||||
model.resetTimer()
|
||||
withAnimation(PlayerControls.animation) {
|
||||
model.presentingControlsOverlay = false
|
||||
}
|
||||
} else {
|
||||
model.toggle()
|
||||
}
|
||||
},
|
||||
singleTapAction: { singleTapAction() },
|
||||
doubleTapAction: {
|
||||
player.backend.seek(relative: .secondsInDefaultTimescale(10))
|
||||
},
|
||||
@@ -74,6 +44,16 @@ struct PlayerGestures: View {
|
||||
}
|
||||
}
|
||||
|
||||
func singleTapAction() {
|
||||
if model.presentingOverlays {
|
||||
withAnimation(PlayerControls.animation) {
|
||||
model.hideOverlays()
|
||||
}
|
||||
} else {
|
||||
model.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
var gestureRectangle: some View {
|
||||
Color.clear
|
||||
.contentShape(Rectangle())
|
||||
|
@@ -26,34 +26,32 @@ struct PlayerQueueRow: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
Button {
|
||||
player.prepareCurrentItemForHistory()
|
||||
Button {
|
||||
player.prepareCurrentItemForHistory()
|
||||
|
||||
player.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture
|
||||
player.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture
|
||||
|
||||
player.videoBeingOpened = item.video
|
||||
player.videoBeingOpened = item.video
|
||||
|
||||
if history {
|
||||
player.playHistory(item, at: watchStoppedAt)
|
||||
} else {
|
||||
player.advanceToItem(item, at: watchStoppedAt)
|
||||
}
|
||||
|
||||
if fullScreen {
|
||||
withAnimation {
|
||||
fullScreen = false
|
||||
}
|
||||
}
|
||||
|
||||
if closePiPOnNavigation, player.playingInPictureInPicture {
|
||||
player.closePiP()
|
||||
}
|
||||
} label: {
|
||||
VideoBanner(video: item.video, playbackTime: watchStoppedAt, videoDuration: watch?.videoDuration)
|
||||
if history {
|
||||
player.playHistory(item, at: watchStoppedAt)
|
||||
} else {
|
||||
player.advanceToItem(item, at: watchStoppedAt)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
if fullScreen {
|
||||
withAnimation {
|
||||
fullScreen = false
|
||||
}
|
||||
}
|
||||
|
||||
if closePiPOnNavigation, player.playingInPictureInPicture {
|
||||
player.closePiP()
|
||||
}
|
||||
} label: {
|
||||
VideoBanner(video: item.video, playbackTime: watchStoppedAt, videoDuration: watch?.videoDuration)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private var watch: Watch? {
|
||||
|
@@ -28,9 +28,10 @@ struct PlayerQueueView: View {
|
||||
playedPreviously
|
||||
}
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
#if !os(iOS)
|
||||
.padding(.vertical, 5)
|
||||
.listRowInsets(EdgeInsets())
|
||||
.padding(.vertical, 5)
|
||||
.listRowInsets(EdgeInsets())
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -38,6 +39,8 @@ struct PlayerQueueView: View {
|
||||
.listStyle(.inset)
|
||||
#elseif os(iOS)
|
||||
.listStyle(.grouped)
|
||||
.backport
|
||||
.scrollContentBackground(false)
|
||||
#else
|
||||
.listStyle(.plain)
|
||||
#endif
|
||||
|
@@ -13,6 +13,7 @@ struct RelatedView: View {
|
||||
Section(header: Text("Related")) {
|
||||
ForEach(related) { video in
|
||||
PlayerQueueRow(item: PlayerQueueItem(video))
|
||||
.listRowBackground(Color.clear)
|
||||
.contextMenu {
|
||||
Section {
|
||||
Button {
|
||||
@@ -53,6 +54,8 @@ struct RelatedView: View {
|
||||
.listStyle(.inset)
|
||||
#elseif os(iOS)
|
||||
.listStyle(.grouped)
|
||||
.backport
|
||||
.scrollContentBackground(false)
|
||||
#else
|
||||
.listStyle(.plain)
|
||||
#endif
|
||||
|
@@ -108,7 +108,6 @@ struct VideoDetails: View {
|
||||
page.update(.moveToLast)
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
|
||||
}
|
||||
|
||||
@@ -178,18 +177,14 @@ struct VideoDetails: View {
|
||||
}
|
||||
case .chapters:
|
||||
ChaptersView()
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
|
||||
case .queue:
|
||||
PlayerQueueView(sidebarQueue: sidebarQueue, fullScreen: $fullScreen)
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
|
||||
case .related:
|
||||
RelatedView()
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
case .comments:
|
||||
CommentsView(embedInScrollView: true)
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
@@ -209,16 +204,16 @@ struct VideoDetails: View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
if !player.videoBeingOpened.isNil && (video.description.isNil || video.description!.isEmpty) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ForEach(1 ... Int.random(in: 3 ... 5), id: \.self) { _ in
|
||||
Text(String(repeating: Video.fixture.description!, count: Int.random(in: 1 ... 4)))
|
||||
.redacted(reason: .placeholder)
|
||||
}
|
||||
Text(String(repeating: Video.fixture.description ?? "", count: Int.random(in: 1 ... 30)))
|
||||
.redacted(reason: .placeholder)
|
||||
}
|
||||
} else if let description = video.description {
|
||||
Group {
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
Text(description)
|
||||
#if !os(tvOS)
|
||||
.textSelection(.enabled)
|
||||
#endif
|
||||
} else {
|
||||
Text(description)
|
||||
}
|
||||
|
@@ -119,6 +119,7 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
viewVerticalOffset = Self.hiddenOffset
|
||||
stopOrientationUpdates()
|
||||
player.controls.hideOverlays()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -203,9 +204,9 @@ struct VideoPlayerView: View {
|
||||
hoveringPlayer = hovering
|
||||
hovering ? playerControls.show() : playerControls.hide()
|
||||
}
|
||||
#if !os(macOS)
|
||||
.gesture(playerDragGesture)
|
||||
#else
|
||||
#if os(iOS)
|
||||
.gesture(isPlayerDragGestureEnabled ? playerDragGesture : nil)
|
||||
#elseif os(macOS)
|
||||
.onAppear(perform: {
|
||||
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
|
||||
if !player.currentItem.isNil, hoveringPlayer {
|
||||
@@ -296,6 +297,9 @@ struct VideoPlayerView: View {
|
||||
.onChange(of: proxy.size) { _ in
|
||||
player.playerSize = proxy.size
|
||||
}
|
||||
.onChange(of: player.controls.presentingOverlays) { _ in
|
||||
player.playerSize = proxy.size
|
||||
}
|
||||
})
|
||||
#if os(iOS)
|
||||
.padding(.top, player.playingFullScreen && verticalSizeClass == .regular ? 20 : 0)
|
||||
@@ -351,6 +355,10 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var isPlayerDragGestureEnabled: Bool {
|
||||
!player.controls.presentingDetailsOverlay && !player.controls.presentingDetailsOverlay
|
||||
}
|
||||
|
||||
var controlsTopPadding: Double {
|
||||
guard fullScreenLayout else { return 0 }
|
||||
|
||||
|
Reference in New Issue
Block a user