Details panels in controls

This commit is contained in:
Arkadiusz Fal
2022-07-10 19:51:46 +02:00
parent db46289813
commit f0b8e7f655
15 changed files with 203 additions and 123 deletions

View File

@@ -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 {

View 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()
}
}

View File

@@ -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())

View File

@@ -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? {

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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 }