Improve player transitions

This commit is contained in:
Arkadiusz Fal 2022-08-08 20:02:46 +02:00
parent fb40f42c6c
commit bcc1d5aeaa
8 changed files with 137 additions and 115 deletions

View File

@ -51,7 +51,7 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
} }
private var playerControls: PlayerControlsModel { private var playerControls: PlayerControlsModel {
PlayerControlsModel(presentingControls: true, presentingControlsOverlay: true, player: player) PlayerControlsModel(presentingControls: true, presentingControlsOverlay: false, player: player)
} }
private var subscriptions: SubscriptionsModel { private var subscriptions: SubscriptionsModel {

View File

@ -40,7 +40,7 @@ final class PlayerModel: ObservableObject {
var mpvPlayerView = MPVPlayerView() var mpvPlayerView = MPVPlayerView()
@Published var presentingPlayer = false { didSet { handlePresentationChange() } } @Published var presentingPlayer = true { didSet { handlePresentationChange() } }
@Published var activeBackend = PlayerBackendType.mpv @Published var activeBackend = PlayerBackendType.mpv
var avPlayerBackend: AVPlayerBackend! var avPlayerBackend: AVPlayerBackend!
@ -149,6 +149,7 @@ final class PlayerModel: ObservableObject {
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer @Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
@Default(.closePiPOnNavigation) var closePiPOnNavigation @Default(.closePiPOnNavigation) var closePiPOnNavigation
@Default(.closePiPOnOpeningPlayer) var closePiPOnOpeningPlayer @Default(.closePiPOnOpeningPlayer) var closePiPOnOpeningPlayer
@Default(.resetWatchedStatusOnPlaying) var resetWatchedStatusOnPlaying
#if !os(macOS) #if !os(macOS)
@Default(.closePiPAndOpenPlayerOnEnteringForeground) var closePiPAndOpenPlayerOnEnteringForeground @Default(.closePiPAndOpenPlayerOnEnteringForeground) var closePiPAndOpenPlayerOnEnteringForeground
@ -202,7 +203,7 @@ final class PlayerModel: ObservableObject {
#endif #endif
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
withAnimation { withAnimation(.linear(duration: 0.25)) {
self?.presentingPlayer = true self?.presentingPlayer = true
} }
} }
@ -214,11 +215,12 @@ final class PlayerModel: ObservableObject {
} }
func hide() { func hide() {
withAnimation(.linear(duration: 0.25)) {
presentingPlayer = false
}
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.playingFullScreen = false self?.playingFullScreen = false
withAnimation {
self?.presentingPlayer = false
}
} }
#if os(iOS) #if os(iOS)
@ -742,6 +744,7 @@ final class PlayerModel: ObservableObject {
pause() pause()
} }
} }
#endif
func enterFullScreen(showControls: Bool = true) { func enterFullScreen(showControls: Bool = true) {
guard !playingFullScreen else { return } guard !playingFullScreen else { return }
@ -756,7 +759,6 @@ final class PlayerModel: ObservableObject {
logger.info("exiting fullscreen") logger.info("exiting fullscreen")
toggleFullscreen(true, showControls: showControls) toggleFullscreen(true, showControls: showControls)
} }
#endif
func updateNowPlayingInfo() { func updateNowPlayingInfo() {
guard let video = currentItem?.video else { guard let video = currentItem?.video else {
@ -818,25 +820,10 @@ final class PlayerModel: ObservableObject {
controls.presentingControls = showControls && isFullScreen controls.presentingControls = showControls && isFullScreen
#if os(macOS) #if os(macOS)
if isFullScreen {
Windows.player.toggleFullScreen() Windows.player.toggleFullScreen()
}
#endif
#if os(iOS)
withAnimation(.linear(duration: 0.2)) {
playingFullScreen = !isFullScreen
}
#else
playingFullScreen = !isFullScreen
#endif #endif
#if os(macOS) playingFullScreen = !isFullScreen
if !isFullScreen {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
Windows.player.toggleFullScreen()
}
}
#endif
#if os(iOS) #if os(iOS)
if !playingFullScreen { if !playingFullScreen {

View File

@ -90,6 +90,7 @@ extension Defaults.Keys {
static let trendingCountry = Key<Country>("trendingCountry", default: .us) static let trendingCountry = Key<Country>("trendingCountry", default: .us)
static let visibleSections = Key<Set<VisibleSection>>("visibleSections", default: [.favorites, .subscriptions, .trending, .playlists]) static let visibleSections = Key<Set<VisibleSection>>("visibleSections", default: [.favorites, .subscriptions, .trending, .playlists])
static let videoDetailsPage = Key<VideoDetails.DetailsPage>("videoDetailsPage", default: .info)
#if os(iOS) #if os(iOS)
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true) static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)

View File

@ -131,6 +131,7 @@ struct ContentView: View {
} }
@ViewBuilder var videoPlayer: some View { @ViewBuilder var videoPlayer: some View {
if player.presentingPlayer {
VideoPlayerView() VideoPlayerView()
.environmentObject(accounts) .environmentObject(accounts)
.environmentObject(comments) .environmentObject(comments)
@ -143,6 +144,8 @@ struct ContentView: View {
.environmentObject(subscriptions) .environmentObject(subscriptions)
.environmentObject(thumbnailsModel) .environmentObject(thumbnailsModel)
.environment(\.navigationStyle, navigationStyle) .environment(\.navigationStyle, navigationStyle)
.transition(.move(edge: .bottom))
}
} }
} }

View File

@ -5,7 +5,7 @@ import SwiftUI
import SwiftUIPager import SwiftUIPager
struct VideoDetails: View { struct VideoDetails: View {
enum DetailsPage: CaseIterable { enum DetailsPage: String, CaseIterable, Defaults.Serializable {
case info, chapters, comments, related, queue case info, chapters, comments, related, queue
var index: Int { var index: Int {
@ -41,6 +41,7 @@ struct VideoDetails: View {
@EnvironmentObject<RecentsModel> private var recents @EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SubscriptionsModel> private var subscriptions @EnvironmentObject<SubscriptionsModel> private var subscriptions
@Default(.videoDetailsPage) private var videoDetailsPage
@Default(.showKeywords) private var showKeywords @Default(.showKeywords) private var showKeywords
@Default(.playerDetailsPageButtonLabelStyle) private var playerDetailsPageButtonLabelStyle @Default(.playerDetailsPageButtonLabelStyle) private var playerDetailsPageButtonLabelStyle
@ -53,7 +54,11 @@ struct VideoDetails: View {
} }
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { if #available(iOS 15, macOS 12, *) {
Self._printChanges()
}
return VStack(alignment: .leading, spacing: 0) {
ControlsBar( ControlsBar(
fullScreen: $fullScreen, fullScreen: $fullScreen,
presentingControls: false, presentingControls: false,
@ -87,12 +92,12 @@ struct VideoDetails: View {
if pageIndex == DetailsPage.comments.index { if pageIndex == DetailsPage.comments.index {
comments.load() comments.load()
} }
videoDetailsPage = DetailsPage.allCases.first { $0.index == pageIndex } ?? .info
} }
} }
.onAppear { .onAppear {
if video.isNil && !sidebarQueue { page.update(.new(index: videoDetailsPage.index))
page.update(.new(index: DetailsPage.queue.index))
}
guard video != nil, accounts.app.supportsSubscriptions else { guard video != nil, accounts.app.supportsSubscriptions else {
subscribed = false subscribed = false
@ -139,6 +144,7 @@ struct VideoDetails: View {
Button(action: { Button(action: {
page.update(.new(index: destination.index)) page.update(.new(index: destination.index))
pageChangeAction?() pageChangeAction?()
videoDetailsPage = destination
}) { }) {
HStack { HStack {
Spacer() Spacer()
@ -180,13 +186,14 @@ struct VideoDetails: View {
case .chapters: case .chapters:
ChaptersView() ChaptersView()
case .queue: case .comments:
PlayerQueueView(sidebarQueue: sidebarQueue, fullScreen: $fullScreen) CommentsView(embedInScrollView: true)
case .related: case .related:
RelatedView() RelatedView()
case .comments:
CommentsView(embedInScrollView: true) case .queue:
PlayerQueueView(sidebarQueue: sidebarQueue, fullScreen: $fullScreen)
} }
} }
.contentShape(Rectangle()) .contentShape(Rectangle())

View File

@ -9,6 +9,9 @@ import SwiftUI
struct VideoPlayerView: View { struct VideoPlayerView: View {
#if os(iOS) #if os(iOS)
static let hiddenOffset = YatteeApp.isForPreviews ? 0 : max(UIScreen.main.bounds.height, UIScreen.main.bounds.width) + 100 static let hiddenOffset = YatteeApp.isForPreviews ? 0 : max(UIScreen.main.bounds.height, UIScreen.main.bounds.width) + 100
static let defaultSidebarQueueValue = UIScreen.main.bounds.width > 900 && Defaults[.playerSidebar] == .whenFits
#else
static let defaultSidebarQueueValue = Defaults[.playerSidebar] != .never
#endif #endif
static let defaultAspectRatio = 16 / 9.0 static let defaultAspectRatio = 16 / 9.0
@ -21,17 +24,11 @@ struct VideoPlayerView: View {
} }
@State private var playerSize: CGSize = .zero { didSet { @State private var playerSize: CGSize = .zero { didSet {
withAnimation { sidebarQueue = playerSize.width > 900 && Defaults[.playerSidebar] == .whenFits
if playerSize.width > 900 && Defaults[.playerSidebar] == .whenFits {
sidebarQueue = true
} else {
sidebarQueue = false
}
}
}} }}
@State private var hoveringPlayer = false @State private var hoveringPlayer = false
@State private var fullScreenDetails = false @State private var fullScreenDetails = false
@State private var sidebarQueue = false @State private var sidebarQueue = defaultSidebarQueueValue
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
@ -46,7 +43,9 @@ struct VideoPlayerView: View {
#endif #endif
#if os(iOS) #if os(iOS)
@State private var viewVerticalOffset = Self.hiddenOffset @GestureState private var dragGestureState = false
@GestureState private var dragGestureOffset = CGSize.zero
@State private var viewDragOffset = 0.0
@State private var orientationObserver: Any? @State private var orientationObserver: Any?
#endif #endif
@ -58,17 +57,11 @@ struct VideoPlayerView: View {
@EnvironmentObject<SearchModel> private var search @EnvironmentObject<SearchModel> private var search
@EnvironmentObject<ThumbnailsModel> private var thumbnails @EnvironmentObject<ThumbnailsModel> private var thumbnails
init() {
if Defaults[.playerSidebar] == .always {
sidebarQueue = true
}
}
var body: some View { var body: some View {
#if DEBUG #if DEBUG
// TODO: remove // TODO: remove
if #available(iOS 15.0, macOS 12.0, *) { if #available(iOS 15.0, macOS 12.0, *) {
_ = Self._printChanges() Self._printChanges()
} }
#endif #endif
@ -105,10 +98,9 @@ struct VideoPlayerView: View {
.onChange(of: fullScreenDetails) { value in .onChange(of: fullScreenDetails) { value in
player.backend.setNeedsDrawing(!value) player.backend.setNeedsDrawing(!value)
} }
.onAppear {
#if os(iOS) #if os(iOS)
.onChange(of: player.presentingPlayer) { newValue in viewDragOffset = 0.0
if newValue {
viewVerticalOffset = 0
configureOrientationUpdatesBasedOnAccelerometer() configureOrientationUpdatesBasedOnAccelerometer()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak player] in DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak player] in
@ -122,22 +114,25 @@ struct VideoPlayerView: View {
andRotateTo: orientationMask == .landscapeLeft ? .landscapeLeft : orientationMask == .landscapeRight ? .landscapeRight : .portrait andRotateTo: orientationMask == .landscapeLeft ? .landscapeLeft : orientationMask == .landscapeRight ? .landscapeRight : .portrait
) )
} }
} else { #endif
}
.onDisappear {
#if os(iOS)
if Defaults[.lockPortraitWhenBrowsing] { if Defaults[.lockPortraitWhenBrowsing] {
Orientation.lockOrientation(.portrait, andRotateTo: .portrait) Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
} else { } else {
Orientation.lockOrientation(.allButUpsideDown) Orientation.lockOrientation(.allButUpsideDown)
} }
viewVerticalOffset = Self.hiddenOffset
stopOrientationUpdates() stopOrientationUpdates()
player.controls.hideOverlays() player.controls.hideOverlays()
}
} player.lockedOrientation = nil
#endif #endif
} }
}
#if os(iOS) #if os(iOS)
.offset(y: viewVerticalOffset) .offset(y: playerOffset)
.animation(.easeOut(duration: 0.3), value: viewVerticalOffset) .animation(.linear(duration: 0.2), value: playerOffset)
.backport .backport
.persistentSystemOverlays(!fullScreenLayout) .persistentSystemOverlays(!fullScreenLayout)
#endif #endif
@ -145,6 +140,10 @@ struct VideoPlayerView: View {
} }
#if os(iOS) #if os(iOS)
var playerOffset: Double {
dragGestureState ? dragGestureOffset.height : viewDragOffset
}
var playerWidth: Double? { var playerWidth: Double? {
fullScreenLayout ? (UIScreen.main.bounds.size.width - SafeArea.insets.left - SafeArea.insets.right) : nil fullScreenLayout ? (UIScreen.main.bounds.size.width - SafeArea.insets.left - SafeArea.insets.right) : nil
} }
@ -224,6 +223,11 @@ struct VideoPlayerView: View {
} }
#if os(iOS) #if os(iOS)
.gesture(playerControls.presentingOverlays ? nil : playerDragGesture) .gesture(playerControls.presentingOverlays ? nil : playerDragGesture)
.onChange(of: dragGestureState) { _ in
if !dragGestureState {
onPlayerDragGestureEnded()
}
}
#elseif os(macOS) #elseif os(macOS)
.onAppear(perform: { .onAppear(perform: {
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) { NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
@ -242,15 +246,10 @@ struct VideoPlayerView: View {
#if !os(tvOS) #if !os(tvOS)
if !fullScreenLayout { if !fullScreenLayout {
VStack(spacing: 0) {
#if os(iOS)
VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails) VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails)
#else
VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails)
#endif
}
#if os(iOS) #if os(iOS)
.transition(.asymmetric(insertion: .opacity, removal: .identity)) .ignoresSafeArea(.all, edges: .bottom)
.transition(.move(edge: .bottom))
#endif #endif
.background(colorScheme == .dark ? Color.black : Color.white) .background(colorScheme == .dark ? Color.black : Color.white)
.modifier(VideoDetailsPaddingModifier( .modifier(VideoDetailsPaddingModifier(
@ -272,8 +271,8 @@ struct VideoPlayerView: View {
if sidebarQueue { if sidebarQueue {
PlayerQueueView(sidebarQueue: true, fullScreen: $fullScreenDetails) PlayerQueueView(sidebarQueue: true, fullScreen: $fullScreenDetails)
.frame(maxWidth: 350) .frame(maxWidth: 350)
.transition(.move(edge: .trailing))
.background(colorScheme == .dark ? Color.black : Color.white) .background(colorScheme == .dark ? Color.black : Color.white)
.transition(.move(edge: .bottom))
} }
#elseif os(macOS) #elseif os(macOS)
if Defaults[.playerSidebar] != .never { if Defaults[.playerSidebar] != .never {
@ -366,6 +365,12 @@ struct VideoPlayerView: View {
#if os(iOS) #if os(iOS)
var playerDragGesture: some Gesture { var playerDragGesture: some Gesture {
DragGesture(minimumDistance: 0, coordinateSpace: .global) DragGesture(minimumDistance: 0, coordinateSpace: .global)
.updating($dragGestureOffset) { value, state, _ in
state = value.translation.height > 0 ? value.translation : .zero
}
.updating($dragGestureState) { _, state, _ in
state = true
}
.onChanged { value in .onChanged { value in
guard player.presentingPlayer, guard player.presentingPlayer,
!playerControls.presentingControlsOverlay else { return } !playerControls.presentingControlsOverlay else { return }
@ -378,33 +383,46 @@ struct VideoPlayerView: View {
guard drag > 0 else { return } guard drag > 0 else { return }
viewDragOffset = drag
if drag > 60, if drag > 60,
player.playingFullScreen, player.playingFullScreen
!OrientationTracker.shared.currentInterfaceOrientation.isLandscape
{ {
player.exitFullScreen() player.exitFullScreen()
player.lockedOrientation = nil if Defaults[.rotateToPortraitOnExitFullScreen] {
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
playerControls.show()
}
} }
viewVerticalOffset = drag
} }
.onEnded { _ in .onEnded { _ in
onPlayerDragGestureEnded()
}
}
private func onPlayerDragGestureEnded() {
guard player.presentingPlayer, guard player.presentingPlayer,
!playerControls.presentingControlsOverlay else { return } !playerControls.presentingControlsOverlay else { return }
if viewVerticalOffset > 100 {
player.backend.setNeedsDrawing(false) if viewDragOffset > 100 {
player.hide() player.hide()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
player.backend.setNeedsDrawing(false)
player.exitFullScreen() player.exitFullScreen()
}
} else { } else {
viewVerticalOffset = 0 withAnimation(.linear(duration: 0.2)) {
viewDragOffset = 0
}
player.backend.setNeedsDrawing(true) player.backend.setNeedsDrawing(true)
player.show() player.show()
} }
} }
}
private func configureOrientationUpdatesBasedOnAccelerometer() { private func configureOrientationUpdatesBasedOnAccelerometer() {
if OrientationTracker.shared.currentInterfaceOrientation.isLandscape, let currentOrientation = OrientationTracker.shared.currentInterfaceOrientation
if currentOrientation.isLandscape,
Defaults[.enterFullscreenInLandscape], Defaults[.enterFullscreenInLandscape],
!player.playingFullScreen, !player.playingFullScreen,
!player.playingInPictureInPicture !player.playingInPictureInPicture
@ -413,6 +431,8 @@ struct VideoPlayerView: View {
player.controls.presentingControls = false player.controls.presentingControls = false
player.enterFullScreen(showControls: false) player.enterFullScreen(showControls: false)
} }
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: currentOrientation)
} }
orientationObserver = NotificationCenter.default.addObserver( orientationObserver = NotificationCenter.default.addObserver(

View File

@ -63,6 +63,8 @@ struct ControlsBar: View {
} }
} else if detailsToggleFullScreen { } else if detailsToggleFullScreen {
Button { Button {
playerControls.presentingControlsOverlay = false
playerControls.presentingControls = false
withAnimation { withAnimation {
fullScreen.toggle() fullScreen.toggle()
} }

View File

@ -237,5 +237,7 @@ struct YatteeApp: App {
#else #else
player.updateRemoteCommandCenter() player.updateRemoteCommandCenter()
#endif #endif
player.presentingPlayer = false
} }
} }