From bcc1d5aeaa1fdab04039d333b8ba2a1879fe92e0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Mon, 8 Aug 2022 20:02:46 +0200 Subject: [PATCH] Improve player transitions --- Fixtures/View+Fixtures.swift | 2 +- Model/Player/PlayerModel.swift | 59 +++++------- Shared/Defaults.swift | 1 + Shared/Navigation/ContentView.swift | 27 +++--- Shared/Player/VideoDetails.swift | 25 ++++-- Shared/Player/VideoPlayerView.swift | 134 ++++++++++++++++------------ Shared/Views/ControlsBar.swift | 2 + Shared/YatteeApp.swift | 2 + 8 files changed, 137 insertions(+), 115 deletions(-) diff --git a/Fixtures/View+Fixtures.swift b/Fixtures/View+Fixtures.swift index 080a8525..64d571e4 100644 --- a/Fixtures/View+Fixtures.swift +++ b/Fixtures/View+Fixtures.swift @@ -51,7 +51,7 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier { } private var playerControls: PlayerControlsModel { - PlayerControlsModel(presentingControls: true, presentingControlsOverlay: true, player: player) + PlayerControlsModel(presentingControls: true, presentingControlsOverlay: false, player: player) } private var subscriptions: SubscriptionsModel { diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 9b965a3a..c95a0c5d 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -40,7 +40,7 @@ final class PlayerModel: ObservableObject { var mpvPlayerView = MPVPlayerView() - @Published var presentingPlayer = false { didSet { handlePresentationChange() } } + @Published var presentingPlayer = true { didSet { handlePresentationChange() } } @Published var activeBackend = PlayerBackendType.mpv var avPlayerBackend: AVPlayerBackend! @@ -149,6 +149,7 @@ final class PlayerModel: ObservableObject { @Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer @Default(.closePiPOnNavigation) var closePiPOnNavigation @Default(.closePiPOnOpeningPlayer) var closePiPOnOpeningPlayer + @Default(.resetWatchedStatusOnPlaying) var resetWatchedStatusOnPlaying #if !os(macOS) @Default(.closePiPAndOpenPlayerOnEnteringForeground) var closePiPAndOpenPlayerOnEnteringForeground @@ -202,7 +203,7 @@ final class PlayerModel: ObservableObject { #endif DispatchQueue.main.async { [weak self] in - withAnimation { + withAnimation(.linear(duration: 0.25)) { self?.presentingPlayer = true } } @@ -214,11 +215,12 @@ final class PlayerModel: ObservableObject { } func hide() { + withAnimation(.linear(duration: 0.25)) { + presentingPlayer = false + } + DispatchQueue.main.async { [weak self] in self?.playingFullScreen = false - withAnimation { - self?.presentingPlayer = false - } } #if os(iOS) @@ -742,22 +744,22 @@ final class PlayerModel: ObservableObject { pause() } } - - func enterFullScreen(showControls: Bool = true) { - guard !playingFullScreen else { return } - - logger.info("entering fullscreen") - toggleFullscreen(false, showControls: showControls) - } - - func exitFullScreen(showControls: Bool = true) { - guard playingFullScreen else { return } - - logger.info("exiting fullscreen") - toggleFullscreen(true, showControls: showControls) - } #endif + func enterFullScreen(showControls: Bool = true) { + guard !playingFullScreen else { return } + + logger.info("entering fullscreen") + toggleFullscreen(false, showControls: showControls) + } + + func exitFullScreen(showControls: Bool = true) { + guard playingFullScreen else { return } + + logger.info("exiting fullscreen") + toggleFullscreen(true, showControls: showControls) + } + func updateNowPlayingInfo() { guard let video = currentItem?.video else { return @@ -818,25 +820,10 @@ final class PlayerModel: ObservableObject { controls.presentingControls = showControls && isFullScreen #if os(macOS) - if isFullScreen { - Windows.player.toggleFullScreen() - } - #endif - #if os(iOS) - withAnimation(.linear(duration: 0.2)) { - playingFullScreen = !isFullScreen - } - #else - playingFullScreen = !isFullScreen + Windows.player.toggleFullScreen() #endif - #if os(macOS) - if !isFullScreen { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { - Windows.player.toggleFullScreen() - } - } - #endif + playingFullScreen = !isFullScreen #if os(iOS) if !playingFullScreen { diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 7ac194b4..800223b2 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -90,6 +90,7 @@ extension Defaults.Keys { static let trendingCountry = Key("trendingCountry", default: .us) static let visibleSections = Key>("visibleSections", default: [.favorites, .subscriptions, .trending, .playlists]) + static let videoDetailsPage = Key("videoDetailsPage", default: .info) #if os(iOS) static let honorSystemOrientationLock = Key("honorSystemOrientationLock", default: true) diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 62a2534a..fa177d1e 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -131,18 +131,21 @@ struct ContentView: View { } @ViewBuilder var videoPlayer: some View { - VideoPlayerView() - .environmentObject(accounts) - .environmentObject(comments) - .environmentObject(instances) - .environmentObject(navigation) - .environmentObject(player) - .environmentObject(playerControls) - .environmentObject(playlists) - .environmentObject(recents) - .environmentObject(subscriptions) - .environmentObject(thumbnailsModel) - .environment(\.navigationStyle, navigationStyle) + if player.presentingPlayer { + VideoPlayerView() + .environmentObject(accounts) + .environmentObject(comments) + .environmentObject(instances) + .environmentObject(navigation) + .environmentObject(player) + .environmentObject(playerControls) + .environmentObject(playlists) + .environmentObject(recents) + .environmentObject(subscriptions) + .environmentObject(thumbnailsModel) + .environment(\.navigationStyle, navigationStyle) + .transition(.move(edge: .bottom)) + } } } diff --git a/Shared/Player/VideoDetails.swift b/Shared/Player/VideoDetails.swift index 6cba7ffc..37f29dea 100644 --- a/Shared/Player/VideoDetails.swift +++ b/Shared/Player/VideoDetails.swift @@ -5,7 +5,7 @@ import SwiftUI import SwiftUIPager struct VideoDetails: View { - enum DetailsPage: CaseIterable { + enum DetailsPage: String, CaseIterable, Defaults.Serializable { case info, chapters, comments, related, queue var index: Int { @@ -41,6 +41,7 @@ struct VideoDetails: View { @EnvironmentObject private var recents @EnvironmentObject private var subscriptions + @Default(.videoDetailsPage) private var videoDetailsPage @Default(.showKeywords) private var showKeywords @Default(.playerDetailsPageButtonLabelStyle) private var playerDetailsPageButtonLabelStyle @@ -53,7 +54,11 @@ struct VideoDetails: 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( fullScreen: $fullScreen, presentingControls: false, @@ -87,12 +92,12 @@ struct VideoDetails: View { if pageIndex == DetailsPage.comments.index { comments.load() } + + videoDetailsPage = DetailsPage.allCases.first { $0.index == pageIndex } ?? .info } } .onAppear { - if video.isNil && !sidebarQueue { - page.update(.new(index: DetailsPage.queue.index)) - } + page.update(.new(index: videoDetailsPage.index)) guard video != nil, accounts.app.supportsSubscriptions else { subscribed = false @@ -139,6 +144,7 @@ struct VideoDetails: View { Button(action: { page.update(.new(index: destination.index)) pageChangeAction?() + videoDetailsPage = destination }) { HStack { Spacer() @@ -180,13 +186,14 @@ struct VideoDetails: View { case .chapters: ChaptersView() - case .queue: - PlayerQueueView(sidebarQueue: sidebarQueue, fullScreen: $fullScreen) + case .comments: + CommentsView(embedInScrollView: true) case .related: RelatedView() - case .comments: - CommentsView(embedInScrollView: true) + + case .queue: + PlayerQueueView(sidebarQueue: sidebarQueue, fullScreen: $fullScreen) } } .contentShape(Rectangle()) diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 88079311..e38a9e7c 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -9,6 +9,9 @@ import SwiftUI struct VideoPlayerView: View { #if os(iOS) 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 static let defaultAspectRatio = 16 / 9.0 @@ -21,17 +24,11 @@ struct VideoPlayerView: View { } @State private var playerSize: CGSize = .zero { didSet { - withAnimation { - if playerSize.width > 900 && Defaults[.playerSidebar] == .whenFits { - sidebarQueue = true - } else { - sidebarQueue = false - } - } + sidebarQueue = playerSize.width > 900 && Defaults[.playerSidebar] == .whenFits }} @State private var hoveringPlayer = false @State private var fullScreenDetails = false - @State private var sidebarQueue = false + @State private var sidebarQueue = defaultSidebarQueueValue @Environment(\.colorScheme) private var colorScheme @@ -46,7 +43,9 @@ struct VideoPlayerView: View { #endif #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? #endif @@ -58,17 +57,11 @@ struct VideoPlayerView: View { @EnvironmentObject private var search @EnvironmentObject private var thumbnails - init() { - if Defaults[.playerSidebar] == .always { - sidebarQueue = true - } - } - var body: some View { #if DEBUG // TODO: remove if #available(iOS 15.0, macOS 12.0, *) { - _ = Self._printChanges() + Self._printChanges() } #endif @@ -105,10 +98,9 @@ struct VideoPlayerView: View { .onChange(of: fullScreenDetails) { value in player.backend.setNeedsDrawing(!value) } - #if os(iOS) - .onChange(of: player.presentingPlayer) { newValue in - if newValue { - viewVerticalOffset = 0 + .onAppear { + #if os(iOS) + viewDragOffset = 0.0 configureOrientationUpdatesBasedOnAccelerometer() 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 ) } - } else { + #endif + } + .onDisappear { + #if os(iOS) if Defaults[.lockPortraitWhenBrowsing] { Orientation.lockOrientation(.portrait, andRotateTo: .portrait) } else { Orientation.lockOrientation(.allButUpsideDown) } - viewVerticalOffset = Self.hiddenOffset stopOrientationUpdates() player.controls.hideOverlays() - } + + player.lockedOrientation = nil + #endif } - #endif } #if os(iOS) - .offset(y: viewVerticalOffset) - .animation(.easeOut(duration: 0.3), value: viewVerticalOffset) + .offset(y: playerOffset) + .animation(.linear(duration: 0.2), value: playerOffset) .backport .persistentSystemOverlays(!fullScreenLayout) #endif @@ -145,6 +140,10 @@ struct VideoPlayerView: View { } #if os(iOS) + var playerOffset: Double { + dragGestureState ? dragGestureOffset.height : viewDragOffset + } + var playerWidth: Double? { fullScreenLayout ? (UIScreen.main.bounds.size.width - SafeArea.insets.left - SafeArea.insets.right) : nil } @@ -224,6 +223,11 @@ struct VideoPlayerView: View { } #if os(iOS) .gesture(playerControls.presentingOverlays ? nil : playerDragGesture) + .onChange(of: dragGestureState) { _ in + if !dragGestureState { + onPlayerDragGestureEnded() + } + } #elseif os(macOS) .onAppear(perform: { NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) { @@ -242,21 +246,16 @@ struct VideoPlayerView: View { #if !os(tvOS) if !fullScreenLayout { - VStack(spacing: 0) { - #if os(iOS) - VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails) - #else - VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails) - #endif - } + VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails) #if os(iOS) - .transition(.asymmetric(insertion: .opacity, removal: .identity)) + .ignoresSafeArea(.all, edges: .bottom) + .transition(.move(edge: .bottom)) #endif - .background(colorScheme == .dark ? Color.black : Color.white) - .modifier(VideoDetailsPaddingModifier( - playerSize: player.playerSize, - fullScreen: fullScreenDetails - )) + .background(colorScheme == .dark ? Color.black : Color.white) + .modifier(VideoDetailsPaddingModifier( + playerSize: player.playerSize, + fullScreen: fullScreenDetails + )) } #endif } @@ -272,8 +271,8 @@ struct VideoPlayerView: View { if sidebarQueue { PlayerQueueView(sidebarQueue: true, fullScreen: $fullScreenDetails) .frame(maxWidth: 350) - .transition(.move(edge: .trailing)) .background(colorScheme == .dark ? Color.black : Color.white) + .transition(.move(edge: .bottom)) } #elseif os(macOS) if Defaults[.playerSidebar] != .never { @@ -366,6 +365,12 @@ struct VideoPlayerView: View { #if os(iOS) var playerDragGesture: some Gesture { 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 guard player.presentingPlayer, !playerControls.presentingControlsOverlay else { return } @@ -378,33 +383,46 @@ struct VideoPlayerView: View { guard drag > 0 else { return } + viewDragOffset = drag + if drag > 60, - player.playingFullScreen, - !OrientationTracker.shared.currentInterfaceOrientation.isLandscape + player.playingFullScreen { player.exitFullScreen() - player.lockedOrientation = nil + if Defaults[.rotateToPortraitOnExitFullScreen] { + Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait) + playerControls.show() + } } - - viewVerticalOffset = drag } .onEnded { _ in - guard player.presentingPlayer, - !playerControls.presentingControlsOverlay else { return } - if viewVerticalOffset > 100 { - player.backend.setNeedsDrawing(false) - player.hide() - player.exitFullScreen() - } else { - viewVerticalOffset = 0 - player.backend.setNeedsDrawing(true) - player.show() - } + onPlayerDragGestureEnded() } } + private func onPlayerDragGestureEnded() { + guard player.presentingPlayer, + !playerControls.presentingControlsOverlay else { return } + + if viewDragOffset > 100 { + player.hide() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { + player.backend.setNeedsDrawing(false) + player.exitFullScreen() + } + } else { + withAnimation(.linear(duration: 0.2)) { + viewDragOffset = 0 + } + player.backend.setNeedsDrawing(true) + player.show() + } + } + private func configureOrientationUpdatesBasedOnAccelerometer() { - if OrientationTracker.shared.currentInterfaceOrientation.isLandscape, + let currentOrientation = OrientationTracker.shared.currentInterfaceOrientation + if currentOrientation.isLandscape, Defaults[.enterFullscreenInLandscape], !player.playingFullScreen, !player.playingInPictureInPicture @@ -413,6 +431,8 @@ struct VideoPlayerView: View { player.controls.presentingControls = false player.enterFullScreen(showControls: false) } + + Orientation.lockOrientation(.allButUpsideDown, andRotateTo: currentOrientation) } orientationObserver = NotificationCenter.default.addObserver( diff --git a/Shared/Views/ControlsBar.swift b/Shared/Views/ControlsBar.swift index bfdcf0d2..1581cb83 100644 --- a/Shared/Views/ControlsBar.swift +++ b/Shared/Views/ControlsBar.swift @@ -63,6 +63,8 @@ struct ControlsBar: View { } } else if detailsToggleFullScreen { Button { + playerControls.presentingControlsOverlay = false + playerControls.presentingControls = false withAnimation { fullScreen.toggle() } diff --git a/Shared/YatteeApp.swift b/Shared/YatteeApp.swift index 61ee98b2..d97ec992 100644 --- a/Shared/YatteeApp.swift +++ b/Shared/YatteeApp.swift @@ -237,5 +237,7 @@ struct YatteeApp: App { #else player.updateRemoteCommandCenter() #endif + + player.presentingPlayer = false } }