mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
Player layout fixes
This commit is contained in:
parent
06b7bc79e8
commit
6c71cd72b1
@ -110,7 +110,9 @@ final class NavigationModel: ObservableObject {
|
||||
navigation.sidebarSectionChanged.toggle()
|
||||
navigation.tabSelection = .recentlyOpened(recent.tag)
|
||||
} else {
|
||||
navigation.presentingChannel = true
|
||||
withAnimation {
|
||||
navigation.presentingChannel = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -139,7 +141,9 @@ final class NavigationModel: ObservableObject {
|
||||
navigation.sidebarSectionChanged.toggle()
|
||||
navigation.tabSelection = .recentlyOpened(recent.tag)
|
||||
} else {
|
||||
navigation.presentingPlaylist = true
|
||||
withAnimation {
|
||||
navigation.presentingPlaylist = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,14 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
avPlayer.timeControlStatus == .playing
|
||||
}
|
||||
|
||||
var aspectRatio: Double {
|
||||
#if os(tvOS)
|
||||
VideoPlayerView.defaultAspectRatio
|
||||
#else
|
||||
controller?.aspectRatio ?? VideoPlayerView.defaultAspectRatio
|
||||
#endif
|
||||
}
|
||||
|
||||
var isSeeking: Bool {
|
||||
// TODO: implement this maybe?
|
||||
false
|
||||
@ -144,14 +152,10 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
func enterFullScreen() {
|
||||
controller?.playerView
|
||||
.perform(NSSelectorFromString("enterFullScreenAnimated:completionHandler:"), with: false, with: nil)
|
||||
model.toggleFullscreen(model?.playingFullScreen ?? false)
|
||||
}
|
||||
|
||||
func exitFullScreen() {
|
||||
controller?.playerView
|
||||
.perform(NSSelectorFromString("exitFullScreenAnimated:completionHandler:"), with: false, with: nil)
|
||||
}
|
||||
func exitFullScreen() {}
|
||||
|
||||
#if os(tvOS)
|
||||
func closePiP(wasPlaying: Bool) {
|
||||
|
@ -86,6 +86,10 @@ final class MPVBackend: PlayerBackend {
|
||||
client?.tracksCount ?? -1
|
||||
}
|
||||
|
||||
var aspectRatio: Double {
|
||||
client?.aspectRatio ?? VideoPlayerView.defaultAspectRatio
|
||||
}
|
||||
|
||||
var frameDropCount: Int {
|
||||
client?.frameDropCount ?? 0
|
||||
}
|
||||
|
@ -185,6 +185,20 @@ final class MPVClient: ObservableObject {
|
||||
mpv.isNil ? 0.0 : getDouble("demuxer-cache-duration")
|
||||
}
|
||||
|
||||
var aspectRatio: Double {
|
||||
guard !mpv.isNil else { return VideoPlayerView.defaultAspectRatio }
|
||||
let aspect = getDouble("video-params/aspect")
|
||||
return aspect.isZero ? VideoPlayerView.defaultAspectRatio : aspect
|
||||
}
|
||||
|
||||
var dh: Double {
|
||||
let defaultDh = 500.0
|
||||
guard !mpv.isNil else { return defaultDh }
|
||||
|
||||
let dh = getDouble("video-params/dh")
|
||||
return dh.isZero ? defaultDh : dh
|
||||
}
|
||||
|
||||
var duration: CMTime {
|
||||
CMTime.secondsInDefaultTimescale(mpv.isNil ? -1 : getDouble("duration"))
|
||||
}
|
||||
@ -240,7 +254,24 @@ final class MPVClient: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
glView?.frame = CGRect(x: 0, y: 0, width: roundedWidth, height: roundedHeight)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
UIView.animate(withDuration: 0.2, animations: {
|
||||
let height = [self.backend.model.playerSize.height, self.backend.model.playerSize.width / self.aspectRatio].min()!
|
||||
let offsetY = self.backend.model.playingFullScreen ? ((self.backend.model.playerSize.height / 2.0) - (height / 2)) : 0
|
||||
self.glView?.frame = CGRect(x: 0, y: offsetY, width: roundedWidth, height: height)
|
||||
}) { completion in
|
||||
if completion {
|
||||
self.logger.info("setting player size to \(roundedWidth),\(roundedHeight) FINISHED")
|
||||
|
||||
self.glView?.queue.async {
|
||||
self.glView.display()
|
||||
}
|
||||
self.backend?.controls?.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,8 @@ protocol PlayerBackend {
|
||||
var isSeeking: Bool { get }
|
||||
var playerItemDuration: CMTime? { get }
|
||||
|
||||
var aspectRatio: Double { get }
|
||||
|
||||
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream?
|
||||
func canPlay(_ stream: Stream) -> Bool
|
||||
|
||||
|
@ -170,7 +170,9 @@ final class PlayerModel: ObservableObject {
|
||||
#endif
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.presentingPlayer = true
|
||||
withAnimation {
|
||||
self?.presentingPlayer = true
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
@ -182,7 +184,9 @@ final class PlayerModel: ObservableObject {
|
||||
func hide() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.playingFullScreen = false
|
||||
self?.presentingPlayer = false
|
||||
withAnimation {
|
||||
self?.presentingPlayer = false
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
@ -625,26 +629,28 @@ final class PlayerModel: ObservableObject {
|
||||
controls.resetTimer()
|
||||
|
||||
#if os(macOS)
|
||||
Windows.player.toggleFullScreen()
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
setNeedsDrawing(false)
|
||||
#endif
|
||||
|
||||
playingFullScreen = !isFullScreen
|
||||
|
||||
#if os(iOS)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
||||
self?.setNeedsDrawing(true)
|
||||
if isFullScreen {
|
||||
Windows.player.toggleFullScreen()
|
||||
}
|
||||
#endif
|
||||
#if os(iOS)
|
||||
withAnimation(.linear(duration: 0.2)) {
|
||||
playingFullScreen = !isFullScreen
|
||||
}
|
||||
#else
|
||||
playingFullScreen = !isFullScreen
|
||||
#endif
|
||||
|
||||
if playingFullScreen {
|
||||
guard !(UIApplication.shared.windows.first?.windowScene?.interfaceOrientation.isLandscape ?? true) else {
|
||||
return
|
||||
#if os(macOS)
|
||||
if !isFullScreen {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
Windows.player.toggleFullScreen()
|
||||
}
|
||||
Orientation.lockOrientation(.landscape, andRotateTo: .landscapeRight)
|
||||
} else {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
if !playingFullScreen {
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
|
||||
}
|
||||
#endif
|
||||
|
@ -145,25 +145,35 @@ struct AppTabNavigation: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
private var channelView: some View {
|
||||
ChannelVideosView()
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
.environment(\.inChannelView, true)
|
||||
.environment(\.navigationStyle, .tab)
|
||||
.environmentObject(accounts)
|
||||
.environmentObject(navigation)
|
||||
.environmentObject(player)
|
||||
.environmentObject(subscriptions)
|
||||
.environmentObject(thumbnailsModel)
|
||||
@ViewBuilder private var channelView: some View {
|
||||
if navigation.presentingChannel {
|
||||
ChannelVideosView()
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
.environment(\.inChannelView, true)
|
||||
.environment(\.navigationStyle, .tab)
|
||||
.environmentObject(accounts)
|
||||
.environmentObject(navigation)
|
||||
.environmentObject(player)
|
||||
.environmentObject(subscriptions)
|
||||
.environmentObject(thumbnailsModel)
|
||||
.transition(.asymmetric(insertion: .flipFromBottom, removal: .move(edge: .bottom)))
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
private var playlistView: some View {
|
||||
ChannelPlaylistView()
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
.environmentObject(accounts)
|
||||
.environmentObject(navigation)
|
||||
.environmentObject(player)
|
||||
.environmentObject(subscriptions)
|
||||
.environmentObject(thumbnailsModel)
|
||||
@ViewBuilder private var playlistView: some View {
|
||||
if navigation.presentingPlaylist {
|
||||
ChannelPlaylistView()
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
.environmentObject(accounts)
|
||||
.environmentObject(navigation)
|
||||
.environmentObject(player)
|
||||
.environmentObject(subscriptions)
|
||||
.environmentObject(thumbnailsModel)
|
||||
.transition(.asymmetric(insertion: .flipFromBottom, removal: .move(edge: .bottom)))
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -130,7 +130,7 @@ struct ContentView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
var videoPlayer: some View {
|
||||
@ViewBuilder var videoPlayer: some View {
|
||||
VideoPlayerView()
|
||||
.environmentObject(accounts)
|
||||
.environmentObject(comments)
|
||||
|
@ -25,12 +25,15 @@ struct VideoPlayerSizeModifier: ViewModifier {
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.frame(maxHeight: fullScreen ? .infinity : maxHeight)
|
||||
.aspectRatio(usedAspectRatio, contentMode: .fit)
|
||||
.frame(width: geometry.size.width)
|
||||
.frame(maxHeight: maxHeight)
|
||||
#if !os(macOS)
|
||||
.aspectRatio(fullScreen ? nil : usedAspectRatio, contentMode: usedAspectRatioContentMode)
|
||||
#endif
|
||||
}
|
||||
|
||||
var usedAspectRatio: Double {
|
||||
guard aspectRatio != nil else {
|
||||
guard aspectRatio != nil, aspectRatio != 0 else {
|
||||
return VideoPlayerView.defaultAspectRatio
|
||||
}
|
||||
|
||||
@ -53,6 +56,10 @@ struct VideoPlayerSizeModifier: ViewModifier {
|
||||
}
|
||||
|
||||
var maxHeight: Double {
|
||||
guard !fullScreen else {
|
||||
return .infinity
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
let height = verticalSizeClass == .regular ? geometry.size.height - minimumHeightLeft : .infinity
|
||||
#else
|
||||
|
@ -21,10 +21,12 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
|
||||
@State private var playerSize: CGSize = .zero { didSet {
|
||||
if playerSize.width > 900 && Defaults[.playerSidebar] == .whenFits {
|
||||
sidebarQueue = true
|
||||
} else {
|
||||
sidebarQueue = false
|
||||
withAnimation {
|
||||
if playerSize.width > 900 && Defaults[.playerSidebar] == .whenFits {
|
||||
sidebarQueue = true
|
||||
} else {
|
||||
sidebarQueue = false
|
||||
}
|
||||
}
|
||||
}}
|
||||
@State private var hoveringPlayer = false
|
||||
@ -92,6 +94,7 @@ struct VideoPlayerView: View {
|
||||
playerSize = geometry.size
|
||||
}
|
||||
}
|
||||
// .ignoresSafeArea(.all, edges: playerEdgesIgnoringSafeArea)
|
||||
.onChange(of: geometry.size) { size in
|
||||
self.playerSize = size
|
||||
}
|
||||
@ -134,6 +137,15 @@ struct VideoPlayerView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
var playerEdgesIgnoringSafeArea: Edge.Set {
|
||||
#if os(iOS)
|
||||
if fullScreenLayout, UIDevice.current.orientation.isLandscape {
|
||||
return [.vertical]
|
||||
}
|
||||
#endif
|
||||
return []
|
||||
}
|
||||
|
||||
var content: some View {
|
||||
Group {
|
||||
ZStack(alignment: .bottomLeading) {
|
||||
@ -173,23 +185,25 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
#else
|
||||
GeometryReader { geometry in
|
||||
VStack(spacing: 0) {
|
||||
Group {
|
||||
if player.playingInPictureInPicture {
|
||||
pictureInPicturePlaceholder
|
||||
} else {
|
||||
playerView
|
||||
|
||||
#if !os(tvOS)
|
||||
.modifier(
|
||||
VideoPlayerSizeModifier(
|
||||
geometry: geometry,
|
||||
aspectRatio: player.avPlayerBackend.controller?.aspectRatio,
|
||||
fullScreen: player.playingFullScreen
|
||||
aspectRatio: player.backend.aspectRatio,
|
||||
fullScreen: fullScreenLayout
|
||||
)
|
||||
)
|
||||
.overlay(playerPlaceholder)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
// .ignoresSafeArea(.all, edges: fullScreenLayout ? .bottom : Edge.Set())
|
||||
.frame(maxWidth: fullScreenLayout ? .infinity : nil, maxHeight: fullScreenLayout ? .infinity : nil)
|
||||
.onHover { hovering in
|
||||
hoveringPlayer = hovering
|
||||
@ -197,7 +211,7 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
#if !os(macOS)
|
||||
.gesture(
|
||||
DragGesture(coordinateSpace: .global)
|
||||
DragGesture(minimumDistance: 0, coordinateSpace: .global)
|
||||
.onChanged { value in
|
||||
guard player.presentingPlayer,
|
||||
!playerControls.presentingControlsOverlay else { return }
|
||||
@ -242,20 +256,19 @@ struct VideoPlayerView: View {
|
||||
if !player.playingFullScreen {
|
||||
VStack(spacing: 0) {
|
||||
#if os(iOS)
|
||||
if verticalSizeClass == .regular {
|
||||
VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails)
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
}
|
||||
|
||||
VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails)
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
#else
|
||||
VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails)
|
||||
|
||||
#endif
|
||||
}
|
||||
#if !os(macOS)
|
||||
.transition(.move(edge: .bottom))
|
||||
#endif
|
||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||
.modifier(VideoDetailsPaddingModifier(
|
||||
playerSize: player.playerSize,
|
||||
aspectRatio: player.avPlayerBackend.controller?.aspectRatio,
|
||||
aspectRatio: player.backend.aspectRatio,
|
||||
fullScreen: fullScreenDetails
|
||||
))
|
||||
}
|
||||
@ -263,6 +276,7 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
.background(((colorScheme == .dark || fullScreenLayout) ? Color.black : Color.white).edgesIgnoringSafeArea(.all))
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 650)
|
||||
@ -272,6 +286,7 @@ struct VideoPlayerView: View {
|
||||
if sidebarQueue {
|
||||
PlayerQueueView(sidebarQueue: true, fullScreen: $fullScreenDetails)
|
||||
.frame(maxWidth: 350)
|
||||
.transition(.move(edge: .trailing))
|
||||
}
|
||||
#elseif os(macOS)
|
||||
if Defaults[.playerSidebar] != .never {
|
||||
@ -281,49 +296,66 @@ struct VideoPlayerView: View {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.all, edges: fullScreenLayout ? .vertical : Edge.Set())
|
||||
#if os(iOS)
|
||||
.statusBar(hidden: player.playingFullScreen)
|
||||
.navigationBarHidden(true)
|
||||
.statusBar(hidden: player.playingFullScreen)
|
||||
#endif
|
||||
}
|
||||
|
||||
var playerView: some View {
|
||||
ZStack(alignment: .top) {
|
||||
switch player.activeBackend {
|
||||
case .mpv:
|
||||
player.mpvPlayerView
|
||||
.overlay(GeometryReader { proxy in
|
||||
Color.clear
|
||||
.onAppear {
|
||||
player.playerSize = proxy.size
|
||||
}
|
||||
.onChange(of: proxy.size) { _ in
|
||||
player.playerSize = proxy.size
|
||||
}
|
||||
})
|
||||
case .appleAVPlayer:
|
||||
player.avPlayerView
|
||||
#if os(iOS)
|
||||
.onAppear {
|
||||
player.pipController = .init(playerLayer: player.playerLayerView.playerLayer)
|
||||
let pipDelegate = PiPDelegate()
|
||||
pipDelegate.player = player
|
||||
Group {
|
||||
switch player.activeBackend {
|
||||
case .mpv:
|
||||
player.mpvPlayerView
|
||||
.overlay(GeometryReader { proxy in
|
||||
Color.clear
|
||||
.onAppear {
|
||||
player.playerSize = proxy.size
|
||||
}
|
||||
.onChange(of: proxy.size) { _ in
|
||||
player.playerSize = proxy.size
|
||||
}
|
||||
})
|
||||
case .appleAVPlayer:
|
||||
player.avPlayerView
|
||||
#if os(iOS)
|
||||
.onAppear {
|
||||
player.pipController = .init(playerLayer: player.playerLayerView.playerLayer)
|
||||
let pipDelegate = PiPDelegate()
|
||||
pipDelegate.player = player
|
||||
|
||||
player.pipDelegate = pipDelegate
|
||||
player.pipController?.delegate = pipDelegate
|
||||
player.playerLayerView.playerLayer.player = player.avPlayerBackend.avPlayer
|
||||
}
|
||||
#endif
|
||||
player.pipDelegate = pipDelegate
|
||||
player.pipController?.delegate = pipDelegate
|
||||
player.playerLayerView.playerLayer.player = player.avPlayerBackend.avPlayer
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
.padding(.top, player.playingFullScreen && verticalSizeClass == .regular ? 20 : 0)
|
||||
#endif
|
||||
|
||||
#if !os(tvOS)
|
||||
PlayerGestures()
|
||||
PlayerControls(player: player, thumbnails: thumbnails)
|
||||
#if os(iOS)
|
||||
.padding(.top, fullScreenLayout ? (safeAreaInsets.top.isZero ? safeAreaInsets.bottom : safeAreaInsets.top) : 0)
|
||||
.padding(.bottom, fullScreenLayout ? safeAreaInsets.bottom : 0)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
.ignoresSafeArea(.all, edges: fullScreenLayout ? .vertical : Edge.Set())
|
||||
#if os(iOS)
|
||||
.statusBarHidden(fullScreenLayout)
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
var safeAreaInsets: UIEdgeInsets {
|
||||
UIApplication.shared.windows.first?.safeAreaInsets ?? .init()
|
||||
}
|
||||
#endif
|
||||
|
||||
var fullScreenLayout: Bool {
|
||||
#if os(iOS)
|
||||
player.playingFullScreen || verticalSizeClass == .compact
|
||||
@ -471,10 +503,6 @@ struct VideoPlayerView: View {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
||||
player.exitFullScreen()
|
||||
}
|
||||
|
||||
Orientation.lockOrientation(.portrait)
|
||||
}
|
||||
}
|
||||
|
@ -61,8 +61,6 @@ struct ChannelPlaylistView: View {
|
||||
viewVerticalOffset = Self.hiddenOffset
|
||||
}
|
||||
}
|
||||
.offset(y: viewVerticalOffset)
|
||||
.animation(.easeIn(duration: 0.2), value: viewVerticalOffset)
|
||||
#endif
|
||||
} else {
|
||||
BrowserPlayerControls {
|
||||
@ -105,7 +103,9 @@ struct ChannelPlaylistView: View {
|
||||
ToolbarItem(placement: .navigation) {
|
||||
if navigationStyle == .tab {
|
||||
Button("Done") {
|
||||
navigation.presentingPlaylist = false
|
||||
withAnimation {
|
||||
navigation.presentingPlaylist = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,8 +57,6 @@ struct ChannelVideosView: View {
|
||||
viewVerticalOffset = Self.hiddenOffset
|
||||
}
|
||||
}
|
||||
.offset(y: viewVerticalOffset)
|
||||
.animation(.easeIn(duration: 0.2), value: viewVerticalOffset)
|
||||
#endif
|
||||
} else {
|
||||
BrowserPlayerControls {
|
||||
@ -104,7 +102,9 @@ struct ChannelVideosView: View {
|
||||
ToolbarItem(placement: .navigation) {
|
||||
if navigationStyle == .tab {
|
||||
Button("Done") {
|
||||
navigation.presentingChannel = false
|
||||
withAnimation {
|
||||
navigation.presentingChannel = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user