mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
Improve player transitions
This commit is contained in:
parent
fb40f42c6c
commit
bcc1d5aeaa
@ -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 {
|
||||
|
@ -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,6 +744,7 @@ final class PlayerModel: ObservableObject {
|
||||
pause()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
func enterFullScreen(showControls: Bool = true) {
|
||||
guard !playingFullScreen else { return }
|
||||
@ -756,7 +759,6 @@ final class PlayerModel: ObservableObject {
|
||||
logger.info("exiting fullscreen")
|
||||
toggleFullscreen(true, showControls: showControls)
|
||||
}
|
||||
#endif
|
||||
|
||||
func updateNowPlayingInfo() {
|
||||
guard let video = currentItem?.video else {
|
||||
@ -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
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
if !isFullScreen {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
Windows.player.toggleFullScreen()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
playingFullScreen = !isFullScreen
|
||||
|
||||
#if os(iOS)
|
||||
if !playingFullScreen {
|
||||
|
@ -90,6 +90,7 @@ extension Defaults.Keys {
|
||||
static let trendingCountry = Key<Country>("trendingCountry", default: .us)
|
||||
|
||||
static let visibleSections = Key<Set<VisibleSection>>("visibleSections", default: [.favorites, .subscriptions, .trending, .playlists])
|
||||
static let videoDetailsPage = Key<VideoDetails.DetailsPage>("videoDetailsPage", default: .info)
|
||||
|
||||
#if os(iOS)
|
||||
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
|
||||
|
@ -131,6 +131,7 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder var videoPlayer: some View {
|
||||
if player.presentingPlayer {
|
||||
VideoPlayerView()
|
||||
.environmentObject(accounts)
|
||||
.environmentObject(comments)
|
||||
@ -143,6 +144,8 @@ struct ContentView: View {
|
||||
.environmentObject(subscriptions)
|
||||
.environmentObject(thumbnailsModel)
|
||||
.environment(\.navigationStyle, navigationStyle)
|
||||
.transition(.move(edge: .bottom))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<RecentsModel> private var recents
|
||||
@EnvironmentObject<SubscriptionsModel> 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())
|
||||
|
@ -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<SearchModel> private var search
|
||||
@EnvironmentObject<ThumbnailsModel> 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)
|
||||
}
|
||||
.onAppear {
|
||||
#if os(iOS)
|
||||
.onChange(of: player.presentingPlayer) { newValue in
|
||||
if newValue {
|
||||
viewVerticalOffset = 0
|
||||
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
|
||||
}
|
||||
}
|
||||
#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,15 +246,10 @@ 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
|
||||
}
|
||||
#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(
|
||||
@ -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
|
||||
onPlayerDragGestureEnded()
|
||||
}
|
||||
}
|
||||
|
||||
private func onPlayerDragGestureEnded() {
|
||||
guard player.presentingPlayer,
|
||||
!playerControls.presentingControlsOverlay else { return }
|
||||
if viewVerticalOffset > 100 {
|
||||
player.backend.setNeedsDrawing(false)
|
||||
|
||||
if viewDragOffset > 100 {
|
||||
player.hide()
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
||||
player.backend.setNeedsDrawing(false)
|
||||
player.exitFullScreen()
|
||||
}
|
||||
} else {
|
||||
viewVerticalOffset = 0
|
||||
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(
|
||||
|
@ -63,6 +63,8 @@ struct ControlsBar: View {
|
||||
}
|
||||
} else if detailsToggleFullScreen {
|
||||
Button {
|
||||
playerControls.presentingControlsOverlay = false
|
||||
playerControls.presentingControls = false
|
||||
withAnimation {
|
||||
fullScreen.toggle()
|
||||
}
|
||||
|
@ -237,5 +237,7 @@ struct YatteeApp: App {
|
||||
#else
|
||||
player.updateRemoteCommandCenter()
|
||||
#endif
|
||||
|
||||
player.presentingPlayer = false
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user